microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.0.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/exponent/exponentHelper.ts

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