microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.8.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/exponent/exponentHelper.ts

489lines · modeblame

1c32fe84Patricio Beltran9 years ago1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
94cd5149Artem Egorov8 years ago4/// <reference path="exponentHelper.d.ts" />
5
1c32fe84Patricio Beltran9 years ago6import * as path from "path";
8f50947fRedMickey5 years ago7import * as semver from "semver";
efb436fcRedMickey5 years ago8import * as vscode from "vscode";
866e020bRedMickey4 years ago9import { sync as globSync } from "glob";
7059d307Patricio Beltran9 years ago10import * as XDL from "./xdlInterface";
66412fdfRuslan Bikkinin7 years ago11import { Package, IPackageInformation } from "../../common/node/package";
e3706a1cRedMickey6 years ago12import { ProjectVersionHelper } from "../../common/projectVersionHelper";
34472878RedMickey5 years ago13import { OutputChannelLogger } from "../log/OutputChannelLogger";
94cd5149Artem Egorov8 years ago14import stripJSONComments = require("strip-json-comments");
d7d405aeYuri Skorokhodov7 years ago15import * as nls from "vscode-nls";
16import { ErrorHelper } from "../../common/error/errorHelper";
efb436fcRedMickey5 years ago17import { getNodeModulesGlobalPath } from "../../common/utils";
892f0aa6JiglioNero4 years ago18import { PackageLoader, PackageConfig } from "../../common/packageLoader";
d7d405aeYuri Skorokhodov7 years ago19import { InternalErrorCode } from "../../common/error/internalErrorCode";
ce5e88eeYuri Skorokhodov5 years ago20import { FileSystem } from "../../common/node/fileSystem";
892f0aa6JiglioNero4 years ago21import { SettingsHelper } from "../settingsHelper";
34472878RedMickey5 years ago22nls.config({
23messageFormat: nls.MessageFormat.bundle,
24bundleFormat: nls.BundleFormat.standalone,
25})();
d7d405aeYuri Skorokhodov7 years ago26const localize = nls.loadMessageBundle();
1c32fe84Patricio Beltran9 years ago27
94cd5149Artem Egorov8 years ago28const APP_JSON = "app.json";
29const EXP_JSON = "exp.json";
1c32fe84Patricio Beltran9 years ago30
31const EXPONENT_INDEX = "exponentIndex.js";
94cd5149Artem Egorov8 years ago32const DEFAULT_EXPONENT_INDEX = "index.js";
1c32fe84Patricio Beltran9 years ago33const DEFAULT_IOS_INDEX = "index.ios.js";
34const DEFAULT_ANDROID_INDEX = "index.android.js";
35
94cd5149Artem Egorov8 years ago36const DBL_SLASHES = /\\/g;
1c32fe84Patricio Beltran9 years ago37
efb436fcRedMickey5 years ago38const NGROK_PACKAGE = "@expo/ngrok";
39
1c32fe84Patricio Beltran9 years ago40export class ExponentHelper {
38edb09eAlexander Sorokin9 years ago41private workspaceRootPath: string;
94cd5149Artem Egorov8 years ago42private projectRootPath: string;
2956dba4Yuri Skorokhodov7 years ago43private fs: FileSystem;
a57e740bPatricio Beltran9 years ago44private hasInitialized: boolean;
efb436fcRedMickey5 years ago45private nodeModulesGlobalPathAddedToEnv: boolean;
0a68f8dbArtem Egorov8 years ago46private logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
1c32fe84Patricio Beltran9 years ago47
34472878RedMickey5 years ago48public constructor(
49workspaceRootPath: string,
50projectRootPath: string,
51fs: FileSystem = new FileSystem(),
52) {
38edb09eAlexander Sorokin9 years ago53this.workspaceRootPath = workspaceRootPath;
54this.projectRootPath = projectRootPath;
2956dba4Yuri Skorokhodov7 years ago55this.fs = fs;
a57e740bPatricio Beltran9 years ago56this.hasInitialized = false;
57// Constructor is slim by design. This is to add as less computation as possible
58// to the initialization of the extension. If a public method is added, make sure
b0af599cJimmy Thomson9 years ago59// to call this.lazilyInitialize() at the begining of the code to be sure all variables
a57e740bPatricio Beltran9 years ago60// are correctly initialized.
efb436fcRedMickey5 years ago61this.nodeModulesGlobalPathAddedToEnv = false;
1c32fe84Patricio Beltran9 years ago62}
63
716f31d0JiglioNero5 years ago64public async preloadExponentDependency(): Promise<[typeof xdl, typeof metroConfig]> {
34472878RedMickey5 years ago65this.logger.info(
66localize(
67"MakingSureYourProjectUsesCorrectExponentDependencies",
68"Making sure your project uses the correct dependencies for Expo. This may take a while...",
69),
70);
716f31d0JiglioNero5 years ago71return Promise.all([XDL.getXDLPackage(), XDL.getMetroConfigPackage()]);
72}
73
0d77292aJiglioNero4 years ago74public async configureExponentEnvironment(): Promise<void> {
66412fdfRuslan Bikkinin7 years ago75let isExpo: boolean;
0d77292aJiglioNero4 years ago76await this.lazilyInitialize();
77this.logger.logStream(
78localize("CheckingIfThisIsExpoApp", "Checking if this is an Expo app."),
79);
866e020bRedMickey4 years ago80isExpo = await this.isExpoManagedApp(true);
0d77292aJiglioNero4 years ago81if (!isExpo) {
82if (!(await this.appHasExpoInstalled())) {
83// Expo requires expo package to be installed inside RN application in order to be able to run it
84// https://github.com/expo/expo-cli/issues/255#issuecomment-453214632
85this.logger.logStream("\n");
716f31d0JiglioNero5 years ago86this.logger.logStream(
0d77292aJiglioNero4 years ago87localize(
88"ExpoPackageIsNotInstalled",
89'[Warning] Please make sure that expo package is installed locally for your project, otherwise further errors may occur. Please, run "npm install expo --save-dev" inside your project to install it.',
90),
716f31d0JiglioNero5 years ago91);
0d77292aJiglioNero4 years ago92this.logger.logStream("\n");
93}
94}
95this.logger.logStream(".\n");
96await this.patchAppJson(isExpo);
d1d77244Jimmy Thomson9 years ago97}
98
99/**
100* Returns the current user. If there is none, asks user for username and password and logins to exponent servers.
101*/
0d77292aJiglioNero4 years ago102public async loginToExponent(
ce5e88eeYuri Skorokhodov5 years ago103promptForInformation: (message: string, password: boolean) => Promise<string>,
34472878RedMickey5 years ago104showMessage: (message: string) => Promise<string>,
ce5e88eeYuri Skorokhodov5 years ago105): Promise<XDL.IUser> {
0d77292aJiglioNero4 years ago106await this.lazilyInitialize();
107let user = await XDL.currentUser();
108if (!user) {
109await showMessage(
110localize(
111"YouNeedToLoginToExpo",
112"You need to login to Expo. Please provide your Expo account username and password in the input boxes after closing this window. If you don't have an account, please go to https://expo.io to create one.",
113),
114);
115const username = await promptForInformation(
116localize("ExpoUsername", "Expo username"),
117false,
118);
119const password = await promptForInformation(
120localize("ExpoPassword", "Expo password"),
121true,
122);
123user = await XDL.login(username, password);
124}
125return user;
d1d77244Jimmy Thomson9 years ago126}
127
242759feJiglioNero5 years ago128public async getExpPackagerOptions(projectRoot: string): Promise<ExpMetroConfig> {
716f31d0JiglioNero5 years ago129await this.lazilyInitialize();
242759feJiglioNero5 years ago130const options = await this.getFromExpConfig<any>("packagerOpts").then(opts => opts || {});
131const metroConfig = await this.getArgumentsFromExpoMetroConfig(projectRoot);
132return { ...options, ...metroConfig };
6458f408Nikita Matrosov9 years ago133}
134
0d77292aJiglioNero4 years ago135public async appHasExpoInstalled(): Promise<boolean> {
136const packageJson = await this.getAppPackageInformation();
137if (packageJson.dependencies && packageJson.dependencies.expo) {
138this.logger.debug("'expo' package is found in 'dependencies' section of package.json");
139return true;
140} else if (packageJson.devDependencies && packageJson.devDependencies.expo) {
141this.logger.debug(
142"'expo' package is found in 'devDependencies' section of package.json",
143);
144return true;
145}
146return false;
66412fdfRuslan Bikkinin7 years ago147}
148
866e020bRedMickey4 years ago149public async isExpoManagedApp(showProgress: boolean = false): Promise<boolean> {
db6fd42aRuslan Bikkinin7 years ago150if (showProgress) {
151this.logger.logStream("...");
152}
153
0d77292aJiglioNero4 years ago154try {
866e020bRedMickey4 years ago155const expoInstalled = await this.appHasExpoInstalled();
156if (!expoInstalled) return false;
157
158const isBareWorkflowProject = await this.isBareWorkflowProject();
0d77292aJiglioNero4 years ago159if (showProgress) this.logger.logStream(".");
866e020bRedMickey4 years ago160return !isBareWorkflowProject;
0d77292aJiglioNero4 years ago161} catch (e) {
162this.logger.error(e.message, e, e.stack);
163if (showProgress) {
164this.logger.logStream(".");
165}
166// Not in a react-native project
167return false;
168}
db6fd42aRuslan Bikkinin7 years ago169}
170
0d77292aJiglioNero4 years ago171public async findOrInstallNgrokGlobally(): Promise<void> {
172let ngrokInstalled: boolean;
173try {
174await this.addNodeModulesPathToEnvIfNotPresent();
175ngrokInstalled = await XDL.isNgrokInstalled(this.projectRootPath);
176} catch (e) {
177ngrokInstalled = false;
178}
179if (!ngrokInstalled) {
c0dc4020etatanova4 years ago180const ngrokVersion = SettingsHelper.getExpoDependencyVersion("@expo/ngrok");
0d77292aJiglioNero4 years ago181const ngrokPackageConfig = new PackageConfig(NGROK_PACKAGE, ngrokVersion);
182
183const outputMessage = localize(
184"ExpoInstallNgrokGlobally",
185'It seems that "{0}" package isn\'t installed globally. This package is required to use Expo tunnels, would you like to install it globally?',
186ngrokPackageConfig.getStringForInstall(),
187);
188const installButton = localize("InstallNgrokGloballyButtonOK", "Install");
189const cancelButton = localize("InstallNgrokGloballyButtonCancel", "Cancel");
190
191const selectedItem = await vscode.window.showWarningMessage(
192outputMessage,
193installButton,
194cancelButton,
195);
196if (selectedItem === installButton) {
197await PackageLoader.getInstance().installGlobalPackage(
198ngrokPackageConfig,
199this.projectRootPath,
200);
201this.logger.info(
202localize(
203"NgrokInstalledGlobally",
204'"{0}" package has been successfully installed globally.',
0690ab22RedMickey4 years ago205ngrokPackageConfig.getStringForInstall(),
0d77292aJiglioNero4 years ago206),
207);
208} else {
209throw ErrorHelper.getInternalError(
210InternalErrorCode.NgrokIsNotInstalledGlobally,
211ngrokPackageConfig.getVersion(true),
212);
213}
214}
efb436fcRedMickey5 years ago215}
216
217public removeNodeModulesPathFromEnvIfWasSet(): void {
218if (this.nodeModulesGlobalPathAddedToEnv) {
219delete process.env["NODE_MODULES"];
220this.nodeModulesGlobalPathAddedToEnv = false;
221}
222}
223
0d77292aJiglioNero4 years ago224public async addNodeModulesPathToEnvIfNotPresent(): Promise<void> {
efb436fcRedMickey5 years ago225if (!process.env["NODE_MODULES"]) {
0d77292aJiglioNero4 years ago226process.env["NODE_MODULES"] = await getNodeModulesGlobalPath();
227this.nodeModulesGlobalPathAddedToEnv = true;
efb436fcRedMickey5 years ago228}
229}
230
866e020bRedMickey4 years ago231private async isBareWorkflowProject(): Promise<boolean> {
232const packageJson = await this.getAppPackageInformation();
233
234if (packageJson.dependencies && packageJson.dependencies.expokit) {
235return false;
236}
237if (packageJson.devDependencies && packageJson.devDependencies.expokit) {
238return false;
239}
240
241const xcodeprojFiles = globSync("ios/**/*.xcodeproj", {
242absolute: true,
243cwd: this.projectRootPath,
244});
245if (xcodeprojFiles.length) {
246return true;
247}
248const gradleFiles = globSync("android/**/*.gradle", {
249absolute: true,
250cwd: this.projectRootPath,
251});
252if (gradleFiles.length) {
253return true;
254}
255
256return false;
257}
258
efb436fcRedMickey5 years ago259private async getArgumentsFromExpoMetroConfig(projectRoot: string): Promise<ExpMetroConfig> {
0d77292aJiglioNero4 years ago260const config = await XDL.getMetroConfig(projectRoot);
261return { sourceExts: config.resolver.sourceExts };
efb436fcRedMickey5 years ago262}
263
1c32fe84Patricio Beltran9 years ago264/**
94cd5149Artem Egorov8 years ago265* Path to a given file inside the .vscode directory
1c32fe84Patricio Beltran9 years ago266*/
66412fdfRuslan Bikkinin7 years ago267private dotvscodePath(filename: string, isAbsolute: boolean): string {
268let paths = [".vscode", filename];
269if (isAbsolute) {
270paths = [this.workspaceRootPath].concat(...paths);
271}
272return path.join(...paths);
94cd5149Artem Egorov8 years ago273}
274
0d77292aJiglioNero4 years ago275private async createExpoEntry(name: string): Promise<void> {
276await this.lazilyInitialize();
277const entryPoint = await this.detectEntry();
278const content = this.generateFileContent(name, entryPoint);
279return await this.fs.writeFile(this.dotvscodePath(EXPONENT_INDEX, true), content);
94cd5149Artem Egorov8 years ago280}
1c32fe84Patricio Beltran9 years ago281
0d77292aJiglioNero4 years ago282private async detectEntry(): Promise<string> {
283await this.lazilyInitialize();
284const [expo, ios] = await Promise.all([
285this.fs.exists(this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX)),
286this.fs.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX)),
287this.fs.exists(this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX)),
288]);
289return expo
290? this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX)
291: ios
292? this.pathToFileInWorkspace(DEFAULT_IOS_INDEX)
293: this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX);
94cd5149Artem Egorov8 years ago294}
1c32fe84Patricio Beltran9 years ago295
94cd5149Artem Egorov8 years ago296private generateFileContent(name: string, entryPoint: string): string {
297return `// This file is automatically generated by VS Code
298// Please do not modify it manually. All changes will be lost.
299var React = require('${this.pathToFileInWorkspace("/node_modules/react")}');
300var { Component } = React;
301var ReactNative = require('${this.pathToFileInWorkspace("/node_modules/react-native")}');
302var { AppRegistry } = ReactNative;
f6b41bbfAlexander Sorokin9 years ago303AppRegistry.registerRunnable('main', function(appParameters) {
94cd5149Artem Egorov8 years ago304AppRegistry.runApplication('${name}', appParameters);
253b6e8eRedMickey5 years ago305});
306var entryPoint = require('${entryPoint}');`;
1c32fe84Patricio Beltran9 years ago307}
308
0d77292aJiglioNero4 years ago309private async patchAppJson(isExpo: boolean = true): Promise<void> {
310let appJson: AppJson;
311try {
312appJson = await this.readAppJson();
313} catch {
314// if app.json doesn't exist but it's ok, we will create it
315appJson = <AppJson>{};
316}
317const packageName = await this.getPackageName();
318
319const expoConfig = <ExpConfig>(appJson.expo || {});
320if (!expoConfig.name || !expoConfig.slug) {
321expoConfig.slug = expoConfig.slug || appJson.name || packageName.replace(" ", "-");
322expoConfig.name = expoConfig.name || appJson.name || packageName;
323appJson.expo = expoConfig;
324}
325
326if (!appJson.name) {
327appJson.name = packageName;
328}
329
330if (!appJson.expo.sdkVersion) {
331const sdkVersion = await this.exponentSdk(true);
332appJson.expo.sdkVersion = sdkVersion;
333}
334
335if (!isExpo) {
336// entryPoint must be relative
337// https://docs.expo.io/versions/latest/workflow/configuration/#entrypoint
338appJson.expo.entryPoint = this.dotvscodePath(EXPONENT_INDEX, false);
339}
340
341appJson = appJson ? await this.writeAppJson(appJson) : appJson;
342
343if (!isExpo) {
344await this.createExpoEntry(appJson.expo.name);
345}
27710197Vladimir Kotikov8 years ago346}
1c32fe84Patricio Beltran9 years ago347
348/**
349* Exponent sdk version that maps to the current react-native version
350* If react native version is not supported it returns null.
351*/
0d77292aJiglioNero4 years ago352private async exponentSdk(showProgress: boolean = false): Promise<string> {
94cd5149Artem Egorov8 years ago353if (showProgress) {
0a68f8dbArtem Egorov8 years ago354this.logger.logStream("...");
1c32fe84Patricio Beltran9 years ago355}
94cd5149Artem Egorov8 years ago356
0d77292aJiglioNero4 years ago357const versions = await ProjectVersionHelper.getReactNativeVersions(this.projectRootPath);
358if (showProgress) {
359this.logger.logStream(".");
360}
361const sdkVersion = await this.mapFacebookReactNativeVersionToExpoVersion(
362versions.reactNativeVersion,
363);
364if (!sdkVersion) {
365const supportedVersions = await this.getFacebookReactNativeVersions();
366throw ErrorHelper.getInternalError(
367InternalErrorCode.RNVersionNotSupportedByExponent,
368supportedVersions.join(", "),
369);
370}
371return sdkVersion;
1c32fe84Patricio Beltran9 years ago372}
373
0d77292aJiglioNero4 years ago374private async getFacebookReactNativeVersions(): Promise<string[]> {
375const sdkVersions = await XDL.getExpoSdkVersions();
376const facebookReactNativeVersions = new Set(
377Object.values(sdkVersions)
378.map(data => data.facebookReactNativeVersion)
379.filter(version => version),
380);
381return Array.from(facebookReactNativeVersions);
8f50947fRedMickey5 years ago382}
383
0d77292aJiglioNero4 years ago384private async mapFacebookReactNativeVersionToExpoVersion(
8f50947fRedMickey5 years ago385outerFacebookReactNativeVersion: string,
386): Promise<string | null> {
387if (!semver.valid(outerFacebookReactNativeVersion)) {
0d77292aJiglioNero4 years ago388throw new Error(
389`${outerFacebookReactNativeVersion} is not a valid version. It must be in the form of x.y.z`,
8f50947fRedMickey5 years ago390);
391}
392
0d77292aJiglioNero4 years ago393const sdkVersions = await XDL.getReleasedExpoSdkVersions();
394let currentSdkVersion: string | null = null;
395for (const [version, { facebookReactNativeVersion }] of Object.entries(sdkVersions)) {
396if (
397semver.major(outerFacebookReactNativeVersion) ===
398semver.major(facebookReactNativeVersion) &&
399semver.minor(outerFacebookReactNativeVersion) ===
400semver.minor(facebookReactNativeVersion) &&
401(!currentSdkVersion || semver.gt(version, currentSdkVersion))
402) {
403currentSdkVersion = version;
8f50947fRedMickey5 years ago404}
0d77292aJiglioNero4 years ago405}
406return currentSdkVersion;
8f50947fRedMickey5 years ago407}
408
1c32fe84Patricio Beltran9 years ago409/**
94cd5149Artem Egorov8 years ago410* Name specified on user's package.json
1c32fe84Patricio Beltran9 years ago411*/
ce5e88eeYuri Skorokhodov5 years ago412private getPackageName(): Promise<string> {
94cd5149Artem Egorov8 years ago413return new Package(this.projectRootPath, { fileSystem: this.fs }).name();
414}
415
0d77292aJiglioNero4 years ago416private async getExpConfig(): Promise<ExpConfig> {
417try {
418return this.readExpJson();
419} catch (err) {
34472878RedMickey5 years ago420if (err.code === "ENOENT") {
0d77292aJiglioNero4 years ago421const appJson = await this.readAppJson();
422return appJson.expo || {};
34472878RedMickey5 years ago423}
0d77292aJiglioNero4 years ago424throw err;
425}
1c32fe84Patricio Beltran9 years ago426}
427
0d77292aJiglioNero4 years ago428private async getFromExpConfig<T>(key: string): Promise<T> {
429const config = await this.getExpConfig();
430return config[key];
1c32fe84Patricio Beltran9 years ago431}
432
433/**
94cd5149Artem Egorov8 years ago434* Returns the specified setting from exp.json if it exists
1c32fe84Patricio Beltran9 years ago435*/
0d77292aJiglioNero4 years ago436private async readExpJson(): Promise<ExpConfig> {
94cd5149Artem Egorov8 years ago437const expJsonPath = this.pathToFileInWorkspace(EXP_JSON);
0d77292aJiglioNero4 years ago438const content = await this.fs.readFile(expJsonPath);
439return JSON.parse(stripJSONComments(content.toString()));
1c32fe84Patricio Beltran9 years ago440}
441
0d77292aJiglioNero4 years ago442private async readAppJson(): Promise<AppJson> {
94cd5149Artem Egorov8 years ago443const appJsonPath = this.pathToFileInWorkspace(APP_JSON);
0d77292aJiglioNero4 years ago444const content = await this.fs.readFile(appJsonPath);
445return JSON.parse(stripJSONComments(content.toString()));
1c32fe84Patricio Beltran9 years ago446}
447
0d77292aJiglioNero4 years ago448private async writeAppJson(config: AppJson): Promise<AppJson> {
94cd5149Artem Egorov8 years ago449const appJsonPath = this.pathToFileInWorkspace(APP_JSON);
0d77292aJiglioNero4 years ago450await this.fs.writeFile(appJsonPath, JSON.stringify(config, null, 2));
451return config;
1c32fe84Patricio Beltran9 years ago452}
453
ce5e88eeYuri Skorokhodov5 years ago454private getAppPackageInformation(): Promise<IPackageInformation> {
66412fdfRuslan Bikkinin7 years ago455return new Package(this.projectRootPath, { fileSystem: this.fs }).parsePackageInformation();
456}
457
1c32fe84Patricio Beltran9 years ago458/**
38edb09eAlexander Sorokin9 years ago459* Path to a given file from the workspace root
1c32fe84Patricio Beltran9 years ago460*/
461private pathToFileInWorkspace(filename: string): string {
94cd5149Artem Egorov8 years ago462return path.join(this.projectRootPath, filename).replace(DBL_SLASHES, "/");
1c32fe84Patricio Beltran9 years ago463}
464
a57e740bPatricio Beltran9 years ago465/**
466* Works as a constructor but only initiliazes when it's actually needed.
467*/
716f31d0JiglioNero5 years ago468private async lazilyInitialize(): Promise<void> {
a57e740bPatricio Beltran9 years ago469if (!this.hasInitialized) {
470this.hasInitialized = true;
716f31d0JiglioNero5 years ago471await this.preloadExponentDependency();
472XDL.configReactNativeVersionWarnings();
38edb09eAlexander Sorokin9 years ago473XDL.attachLoggerStream(this.projectRootPath, {
a57e740bPatricio Beltran9 years ago474stream: {
475write: (chunk: any) => {
476if (chunk.level <= 30) {
0a68f8dbArtem Egorov8 years ago477this.logger.logStream(chunk.msg);
a57e740bPatricio Beltran9 years ago478} else if (chunk.level === 40) {
0a68f8dbArtem Egorov8 years ago479this.logger.warning(chunk.msg);
a57e740bPatricio Beltran9 years ago480} else {
0a68f8dbArtem Egorov8 years ago481this.logger.error(chunk.msg);
a57e740bPatricio Beltran9 years ago482}
483},
484},
485type: "raw",
486});
487}
488}
5c8365a6Artem Egorov8 years ago489}