microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
c036b0d343245c82886cd5906e37ff50b8b30d34

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/exponent/exponentHelper.ts

377lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4/// <reference path="exponentHelper.d.ts" />
5
6import * as path from "path";
7import * as Q from "q";
8import * as XDL from "./xdlInterface";
9import { Package, IPackageInformation } from "../../common/node/package";
10import { ProjectVersionHelper } from "../../common/projectVersionHelper";
11import { FileSystem } from "../../common/node/fileSystem";
12import {OutputChannelLogger} from "../log/OutputChannelLogger";
13import stripJSONComments = require("strip-json-comments");
14import * as nls from "vscode-nls";
15import { ErrorHelper } from "../../common/error/errorHelper";
16import { InternalErrorCode } from "../../common/error/internalErrorCode";
17nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
18const localize = nls.loadMessageBundle();
19
20const APP_JSON = "app.json";
21const EXP_JSON = "exp.json";
22
23const EXPONENT_INDEX = "exponentIndex.js";
24const DEFAULT_EXPONENT_INDEX = "index.js";
25const DEFAULT_IOS_INDEX = "index.ios.js";
26const DEFAULT_ANDROID_INDEX = "index.android.js";
27
28const DBL_SLASHES = /\\/g;
29
30export class ExponentHelper {
31 private workspaceRootPath: string;
32 private projectRootPath: string;
33 private fs: FileSystem;
34 private hasInitialized: boolean;
35 private logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
36
37 public constructor(workspaceRootPath: string, projectRootPath: string, fs: FileSystem = new FileSystem()) {
38 this.workspaceRootPath = workspaceRootPath;
39 this.projectRootPath = projectRootPath;
40 this.fs = fs;
41 this.hasInitialized = false;
42 // Constructor is slim by design. This is to add as less computation as possible
43 // to the initialization of the extension. If a public method is added, make sure
44 // to call this.lazilyInitialize() at the begining of the code to be sure all variables
45 // are correctly initialized.
46 }
47
48 public configureExponentEnvironment(): Q.Promise<void> {
49 this.lazilyInitialize();
50 this.logger.info(localize("MakingSureYourProjectUsesCorrectExponentDependencies", "Making sure your project uses the correct dependencies for Expo. This may take a while..."));
51 this.logger.logStream(localize("CheckingIfThisIsExpoApp", "Checking if this is Expo app."));
52 let isExpo: boolean;
53 return this.isExpoApp(true)
54 .then(result => {
55 isExpo = result;
56 if (!isExpo) {
57 return this.appHasExpoInstalled().then((expoInstalled) => {
58 if (!expoInstalled) {
59 // Expo requires expo package to be installed inside RN application in order to be able to run it
60 // https://github.com/expo/expo-cli/issues/255#issuecomment-453214632
61 this.logger.logStream("\n");
62 this.logger.logStream(localize("ExpoPackageIsNotInstalled", "[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."));
63 this.logger.logStream("\n");
64 }
65 });
66 }
67 return;
68 }).then(() => {
69 this.logger.logStream(".\n");
70 return this.patchAppJson(isExpo);
71 });
72 }
73
74 /**
75 * Returns the current user. If there is none, asks user for username and password and logins to exponent servers.
76 */
77 public loginToExponent(
78 promptForInformation: (message: string, password: boolean) => Q.Promise<string>,
79 showMessage: (message: string) => Q.Promise<string>
80 ): Q.Promise<XDL.IUser> {
81 this.lazilyInitialize();
82 return XDL.currentUser()
83 .then((user) => {
84 if (!user) {
85 let username = "";
86 return showMessage(localize("YouNeedToLoginToExpo", "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."))
87 .then(() =>
88 promptForInformation(localize("ExpoUsername", "Expo username"), false)
89 ).then((name: string) => {
90 username = name;
91 return promptForInformation(localize("ExpoPassword", "Expo password"), true);
92 })
93 .then((password: string) =>
94 XDL.login(username, password));
95 }
96 return user;
97 })
98 .catch(error => {
99 return Q.reject<XDL.IUser>(error);
100 });
101 }
102
103 public getExpPackagerOptions(): Q.Promise<ExpConfigPackager> {
104 this.lazilyInitialize();
105 return this.getFromExpConfig("packagerOpts")
106 .then(opts => opts || {});
107 }
108
109 public appHasExpoInstalled(): Q.Promise<boolean> {
110 return this.getAppPackageInformation()
111 .then((packageJson: IPackageInformation) => {
112 if (packageJson.dependencies && packageJson.dependencies.expo) {
113 this.logger.debug("'expo' package is found in 'dependencies' section of package.json");
114 return true;
115 } else if (packageJson.devDependencies && packageJson.devDependencies.expo) {
116 this.logger.debug("'expo' package is found in 'devDependencies' section of package.json");
117 return true;
118 }
119 return false;
120 });
121 }
122
123 public appHasExpoRNSDKInstalled(): Q.Promise<boolean> {
124 return this.getAppPackageInformation()
125 .then((packageJson: IPackageInformation) => {
126 const reactNativeValue: string | undefined = packageJson.dependencies && packageJson.dependencies["react-native"];
127 if (reactNativeValue) {
128 this.logger.debug(`'react-native' package with value '${reactNativeValue}' is found in 'dependencies' section of package.json`);
129 if (reactNativeValue.startsWith("https://github.com/expo/react-native/archive/sdk")) {
130 return true;
131 }
132 }
133 return false;
134 });
135 }
136
137 public isExpoApp(showProgress: boolean = false): Q.Promise<boolean> {
138 if (showProgress) {
139 this.logger.logStream("...");
140 }
141
142 return Q.all([
143 this.appHasExpoInstalled(),
144 this.appHasExpoRNSDKInstalled(),
145 ]).spread((expoInstalled, expoRNSDKInstalled) => {
146 if (showProgress) this.logger.logStream(".");
147 return expoInstalled && expoRNSDKInstalled;
148 }).catch((e) => {
149 this.logger.error(e.message, e, e.stack);
150 if (showProgress) {
151 this.logger.logStream(".");
152 }
153 // Not in a react-native project
154 return false;
155 });
156 }
157
158 /**
159 * Path to a given file inside the .vscode directory
160 */
161 private dotvscodePath(filename: string, isAbsolute: boolean): string {
162 let paths = [".vscode", filename];
163 if (isAbsolute) {
164 paths = [this.workspaceRootPath].concat(...paths);
165 }
166 return path.join(...paths);
167 }
168
169 private createExpoEntry(name: string): Q.Promise<void> {
170 this.lazilyInitialize();
171 return this.detectEntry()
172 .then((entryPoint: string) => {
173 const content = this.generateFileContent(name, entryPoint);
174 return this.fs.writeFile(this.dotvscodePath(EXPONENT_INDEX, true), content);
175 });
176
177 }
178
179 private detectEntry(): Q.Promise<string> {
180 this.lazilyInitialize();
181 return Q.all([
182 this.fs.exists(this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX)),
183 this.fs.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX)),
184 this.fs.exists(this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX)),
185 ])
186 .spread((expo: boolean, ios: boolean): string => {
187 return expo ? this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX) :
188 ios ? this.pathToFileInWorkspace(DEFAULT_IOS_INDEX) :
189 this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX);
190 });
191 }
192
193 private generateFileContent(name: string, entryPoint: string): string {
194 return `// This file is automatically generated by VS Code
195// Please do not modify it manually. All changes will be lost.
196var React = require('${this.pathToFileInWorkspace("/node_modules/react")}');
197var { Component } = React;
198var ReactNative = require('${this.pathToFileInWorkspace("/node_modules/react-native")}');
199var { AppRegistry } = ReactNative;
200var entryPoint = require('${entryPoint}');
201AppRegistry.registerRunnable('main', function(appParameters) {
202 AppRegistry.runApplication('${name}', appParameters);
203});`;
204 }
205
206 private patchAppJson(isExpo: boolean = true): Q.Promise<void> {
207 return this.readAppJson()
208 .catch(() => {
209 // if app.json doesn't exist but it's ok, we will create it
210 return {};
211 })
212 .then((config: AppJson) => {
213 let expoConfig = <ExpConfig>(config.expo || {});
214 if (!expoConfig.name || !expoConfig.slug) {
215 return this.getPackageName()
216 .then((name: string) => {
217 expoConfig.slug = expoConfig.slug || config.name || name.replace(" ", "-");
218 expoConfig.name = expoConfig.name || config.name || name;
219 config.expo = expoConfig;
220 return config;
221 });
222 }
223
224 return config;
225 })
226 .then((config: AppJson) => {
227 if (!config.name) {
228 return this.getPackageName()
229 .then((name: string) => {
230 config.name = name;
231 return config;
232 });
233 }
234
235 return config;
236 })
237 .then((config: AppJson) => {
238 if (!config.expo.sdkVersion) {
239 return this.exponentSdk(true)
240 .then(sdkVersion => {
241 config.expo.sdkVersion = sdkVersion;
242 return config;
243 });
244 }
245
246 return config;
247 })
248 .then((config: AppJson) => {
249 if (!isExpo) {
250 // entryPoint must be relative
251 // https://docs.expo.io/versions/latest/workflow/configuration/#entrypoint
252 config.expo.entryPoint = this.dotvscodePath(EXPONENT_INDEX, false);
253 }
254
255 return config;
256 })
257 .then((config: AppJson) => {
258 return config ? this.writeAppJson(config) : config;
259 })
260 .then((config: AppJson) => {
261 return isExpo ? Q.resolve(void 0) : this.createExpoEntry(config.expo.name);
262 });
263 }
264
265 /**
266 * Exponent sdk version that maps to the current react-native version
267 * If react native version is not supported it returns null.
268 */
269 private exponentSdk(showProgress: boolean = false): Q.Promise<string> {
270 if (showProgress) {
271 this.logger.logStream("...");
272 }
273
274 return ProjectVersionHelper.getReactNativeVersions(this.projectRootPath)
275 .then(versions => {
276 if (showProgress) this.logger.logStream(".");
277 return XDL.mapVersion(versions.reactNativeVersion)
278 .then(sdkVersion => {
279 if (!sdkVersion) {
280 return XDL.supportedVersions()
281 .then((versions) => {
282 return Q.reject<string>(ErrorHelper.getInternalError(InternalErrorCode.RNVersionNotSupportedByExponent, versions.join(", ")));
283 });
284 }
285 return sdkVersion;
286 });
287 });
288 }
289
290
291 /**
292 * Name specified on user's package.json
293 */
294 private getPackageName(): Q.Promise<string> {
295 return new Package(this.projectRootPath, { fileSystem: this.fs }).name();
296 }
297
298 private getExpConfig(): Q.Promise<ExpConfig> {
299 return this.readExpJson()
300 .catch(err => {
301 if (err.code === "ENOENT") {
302 return this.readAppJson()
303 .then((config: AppJson) => {
304 return config.expo || {};
305 });
306 }
307
308 return err;
309 });
310 }
311
312 private getFromExpConfig(key: string): Q.Promise<any> {
313 return this.getExpConfig()
314 .then((config: ExpConfig) => config[key]);
315 }
316
317 /**
318 * Returns the specified setting from exp.json if it exists
319 */
320 private readExpJson(): Q.Promise<ExpConfig> {
321 const expJsonPath = this.pathToFileInWorkspace(EXP_JSON);
322 return this.fs.readFile(expJsonPath)
323 .then(content => {
324 return JSON.parse(stripJSONComments(content));
325 });
326 }
327
328 private readAppJson(): Q.Promise<AppJson> {
329 const appJsonPath = this.pathToFileInWorkspace(APP_JSON);
330 return this.fs.readFile(appJsonPath)
331 .then(content => {
332 return JSON.parse(stripJSONComments(content));
333 });
334 }
335
336 private writeAppJson(config: AppJson): Q.Promise<AppJson> {
337 const appJsonPath = this.pathToFileInWorkspace(APP_JSON);
338 return this.fs.writeFile(appJsonPath, JSON.stringify(config, null, 2))
339 .then(() => config);
340 }
341
342 private getAppPackageInformation(): Q.Promise<IPackageInformation> {
343 return new Package(this.projectRootPath, { fileSystem: this.fs }).parsePackageInformation();
344 }
345
346 /**
347 * Path to a given file from the workspace root
348 */
349 private pathToFileInWorkspace(filename: string): string {
350 return path.join(this.projectRootPath, filename).replace(DBL_SLASHES, "/");
351 }
352
353 /**
354 * Works as a constructor but only initiliazes when it's actually needed.
355 */
356 private lazilyInitialize(): void {
357 if (!this.hasInitialized) {
358 this.hasInitialized = true;
359
360 XDL.configReactNativeVersionWargnings();
361 XDL.attachLoggerStream(this.projectRootPath, {
362 stream: {
363 write: (chunk: any) => {
364 if (chunk.level <= 30) {
365 this.logger.logStream(chunk.msg);
366 } else if (chunk.level === 40) {
367 this.logger.warning(chunk.msg);
368 } else {
369 this.logger.error(chunk.msg);
370 }
371 },
372 },
373 type: "raw",
374 });
375 }
376 }
377}
378