microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.3.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/exponent/exponentHelper.ts

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