microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.11.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/exponent/exponentHelper.ts

376lines · 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 { ReactNativeProjectHelper } from "../../common/reactNativeProjectHelper";
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";
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(): Q.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) => Q.Promise<string>,
78 showMessage: (message: string) => Q.Promise<string>
79 ): Q.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 Q.reject<XDL.IUser>(error);
99 });
100 }
101
102 public getExpPackagerOptions(): Q.Promise<ExpConfigPackager> {
103 this.lazilyInitialize();
104 return this.getFromExpConfig("packagerOpts")
105 .then(opts => opts || {});
106 }
107
108 public appHasExpoInstalled(): Q.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(): Q.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): Q.Promise<boolean> {
137 if (showProgress) {
138 this.logger.logStream("...");
139 }
140
141 return Q.all([
142 this.appHasExpoInstalled(),
143 this.appHasExpoRNSDKInstalled(),
144 ]).spread((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): Q.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(): Q.Promise<string> {
179 this.lazilyInitialize();
180 return Q.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 ])
185 .spread((expo: boolean, ios: boolean): string => {
186 return expo ? this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX) :
187 ios ? this.pathToFileInWorkspace(DEFAULT_IOS_INDEX) :
188 this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX);
189 });
190 }
191
192 private generateFileContent(name: string, entryPoint: string): string {
193 return `// This file is automatically generated by VS Code
194// Please do not modify it manually. All changes will be lost.
195var React = require('${this.pathToFileInWorkspace("/node_modules/react")}');
196var { Component } = React;
197var ReactNative = require('${this.pathToFileInWorkspace("/node_modules/react-native")}');
198var { AppRegistry } = ReactNative;
199var entryPoint = require('${entryPoint}');
200AppRegistry.registerRunnable('main', function(appParameters) {
201 AppRegistry.runApplication('${name}', appParameters);
202});`;
203 }
204
205 private patchAppJson(isExpo: boolean = true): Q.Promise<void> {
206 return this.readAppJson()
207 .catch(() => {
208 // if app.json doesn't exist but it's ok, we will create it
209 return {};
210 })
211 .then((config: AppJson) => {
212 let expoConfig = <ExpConfig>(config.expo || {});
213 if (!expoConfig.name || !expoConfig.slug) {
214 return this.getPackageName()
215 .then((name: string) => {
216 expoConfig.slug = expoConfig.slug || config.name || name.replace(" ", "-");
217 expoConfig.name = expoConfig.name || config.name || name;
218 config.expo = expoConfig;
219 return config;
220 });
221 }
222
223 return config;
224 })
225 .then((config: AppJson) => {
226 if (!config.name) {
227 return this.getPackageName()
228 .then((name: string) => {
229 config.name = name;
230 return config;
231 });
232 }
233
234 return config;
235 })
236 .then((config: AppJson) => {
237 if (!config.expo.sdkVersion) {
238 return this.exponentSdk(true)
239 .then(sdkVersion => {
240 config.expo.sdkVersion = sdkVersion;
241 return config;
242 });
243 }
244
245 return config;
246 })
247 .then((config: AppJson) => {
248 if (!isExpo) {
249 // entryPoint must be relative
250 // https://docs.expo.io/versions/latest/workflow/configuration/#entrypoint
251 config.expo.entryPoint = this.dotvscodePath(EXPONENT_INDEX, false);
252 }
253
254 return config;
255 })
256 .then((config: AppJson) => {
257 return config ? this.writeAppJson(config) : config;
258 })
259 .then((config: AppJson) => {
260 return isExpo ? Q.resolve(void 0) : this.createExpoEntry(config.expo.name);
261 });
262 }
263
264 /**
265 * Exponent sdk version that maps to the current react-native version
266 * If react native version is not supported it returns null.
267 */
268 private exponentSdk(showProgress: boolean = false): Q.Promise<string> {
269 if (showProgress) {
270 this.logger.logStream("...");
271 }
272
273 return ReactNativeProjectHelper.getReactNativeVersion(this.projectRootPath)
274 .then(version => {
275 if (showProgress) this.logger.logStream(".");
276 return XDL.mapVersion(version)
277 .then(sdkVersion => {
278 if (!sdkVersion) {
279 return XDL.supportedVersions()
280 .then((versions) => {
281 return Q.reject<string>(ErrorHelper.getInternalError(InternalErrorCode.RNVersionNotSupportedByExponent, versions.join(", ")));
282 });
283 }
284 return sdkVersion;
285 });
286 });
287 }
288
289
290 /**
291 * Name specified on user's package.json
292 */
293 private getPackageName(): Q.Promise<string> {
294 return new Package(this.projectRootPath, { fileSystem: this.fs }).name();
295 }
296
297 private getExpConfig(): Q.Promise<ExpConfig> {
298 return this.readExpJson()
299 .catch(err => {
300 if (err.code === "ENOENT") {
301 return this.readAppJson()
302 .then((config: AppJson) => {
303 return config.expo || {};
304 });
305 }
306
307 return err;
308 });
309 }
310
311 private getFromExpConfig(key: string): Q.Promise<any> {
312 return this.getExpConfig()
313 .then((config: ExpConfig) => config[key]);
314 }
315
316 /**
317 * Returns the specified setting from exp.json if it exists
318 */
319 private readExpJson(): Q.Promise<ExpConfig> {
320 const expJsonPath = this.pathToFileInWorkspace(EXP_JSON);
321 return this.fs.readFile(expJsonPath)
322 .then(content => {
323 return JSON.parse(stripJSONComments(content));
324 });
325 }
326
327 private readAppJson(): Q.Promise<AppJson> {
328 const appJsonPath = this.pathToFileInWorkspace(APP_JSON);
329 return this.fs.readFile(appJsonPath)
330 .then(content => {
331 return JSON.parse(stripJSONComments(content));
332 });
333 }
334
335 private writeAppJson(config: AppJson): Q.Promise<AppJson> {
336 const appJsonPath = this.pathToFileInWorkspace(APP_JSON);
337 return this.fs.writeFile(appJsonPath, JSON.stringify(config, null, 2))
338 .then(() => config);
339 }
340
341 private getAppPackageInformation(): Q.Promise<IPackageInformation> {
342 return new Package(this.projectRootPath, { fileSystem: this.fs }).parsePackageInformation();
343 }
344
345 /**
346 * Path to a given file from the workspace root
347 */
348 private pathToFileInWorkspace(filename: string): string {
349 return path.join(this.projectRootPath, filename).replace(DBL_SLASHES, "/");
350 }
351
352 /**
353 * Works as a constructor but only initiliazes when it's actually needed.
354 */
355 private lazilyInitialize(): void {
356 if (!this.hasInitialized) {
357 this.hasInitialized = true;
358
359 XDL.configReactNativeVersionWargnings();
360 XDL.attachLoggerStream(this.projectRootPath, {
361 stream: {
362 write: (chunk: any) => {
363 if (chunk.level <= 30) {
364 this.logger.logStream(chunk.msg);
365 } else if (chunk.level === 40) {
366 this.logger.warning(chunk.msg);
367 } else {
368 this.logger.error(chunk.msg);
369 }
370 },
371 },
372 type: "raw",
373 });
374 }
375 }
376}
377