microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
ed7c9f3c98dea57702552b19bd268d60e5e6bada

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/common/exponent/exponentHelper.ts

473lines · 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
4import * as path from "path";
5import * as Q from "q";
6import * as XDL from "./xdlInterface";
7import stripJsonComments = require("strip-json-comments");
8
9import {FileSystem} from "../node/fileSystem";
10import {Package} from "../node/package";
11import {ReactNativeProjectHelper} from "../reactNativeProjectHelper";
12import {CommandVerbosity, CommandExecutor} from "../commandExecutor";
13import {HostPlatform} from "../hostPlatform";
14import {Log} from "../log/log";
15
16const VSCODE_EXPONENT_JSON = "vscodeExponent.json";
17const EXPONENT_ENTRYPOINT = "exponentEntrypoint.js";
18const EXPONENT_INDEX = "exponentIndex.js";
19const DEFAULT_EXPONENT_INDEX = "main.js";
20const DEFAULT_IOS_INDEX = "index.ios.js";
21const DEFAULT_ANDROID_INDEX = "index.android.js";
22const EXP_JSON = "exp.json";
23const SECONDS_IN_DAY = 86400;
24
25enum ReactNativePackageStatus {
26 FACEBOOK_PACKAGE,
27 EXPONENT_PACKAGE,
28 UNKNOWN
29}
30
31export class ExponentHelper {
32 private rootPath: string;
33 private fileSystem: FileSystem;
34 private commandExecutor: CommandExecutor;
35
36 private expSdkVersion: string;
37 private entrypointFilename: string;
38 private entrypointComponentName: string;
39
40 private dependencyPackage: ReactNativePackageStatus;
41 private hasInitialized: boolean;
42
43 public constructor(projectRootPath: string) {
44 this.rootPath = projectRootPath;
45 this.hasInitialized = false;
46 // Constructor is slim by design. This is to add as less computation as possible
47 // to the initialization of the extension. If a public method is added, make sure
48 // to call this.lazilyInitialize() at the begining of the code to be sure all variables
49 // are correctly initialized.
50 }
51
52 /**
53 * Convert react native project to exponent.
54 * This consists on three steps:
55 * 1. Change the dependency from facebook's react-native to exponent's fork
56 * 2. Create exp.json
57 * 3. Create index and entrypoint for exponent
58 */
59 public configureExponentEnvironment(): Q.Promise<void> {
60 this.lazilyInitialize();
61 Log.logMessage("Making sure your project uses the correct dependencies for exponent. This may take a while...");
62 return this.changeReactNativeToExponent()
63 .then(() => {
64 Log.logMessage("Dependencies are correct. Making sure you have any necessary configuration file.");
65 return this.ensureExpJson();
66 }).then(() => {
67 Log.logMessage("Project setup is correct. Generating entrypoint code.");
68 return this.createExponentEntrypoint()
69 .then(() =>
70 this.createIndex());
71 });
72 }
73
74 /**
75 * Change dependencies to point to original react-native repo
76 */
77 public configureReactNativeEnvironment(): Q.Promise<void> {
78 this.lazilyInitialize();
79 Log.logMessage("Checking react native is correctly setup. This may take a while...");
80 return this.changeExponentToReactNative();
81 }
82
83 /**
84 * Returns the current user. If there is none, asks user for username and password and logins to exponent servers.
85 */
86 public loginToExponent(promptForInformation: (message: string, password: boolean) => Q.Promise<string>, showMessage: (message: string) => Q.Promise<string>): Q.Promise<XDL.IUser> {
87 this.lazilyInitialize();
88 return XDL.currentUser()
89 .then((user) => {
90 if (!user) {
91 let username = "";
92 return showMessage("You need to login to exponent. Please provide username and password to login. If you don't have an account we will create one for you.")
93 .then(() =>
94 promptForInformation("Exponent username", false)
95 ).then((name) => {
96 username = name;
97 return promptForInformation("Exponent password", true);
98 })
99 .then((password) =>
100 XDL.login(username, password));
101 }
102 return user;
103 })
104 .catch(error => {
105 return Q.reject<XDL.IUser>(error);
106 });
107 }
108
109 /**
110 * File used as an entrypoint for exponent. This file's component should be registered as "main"
111 * in the AppRegistry and should only render a entrypoint component.
112 */
113 private createIndex(): Q.Promise<void> {
114 this.lazilyInitialize();
115 const pkg = require("../../../package.json");
116 const extensionVersionNumber = pkg.version;
117 const extensionName = pkg.name;
118
119 return this.entrypointComponent()
120 .then((componentName) => {
121 const fileContents =
122 `// This file is automatically generated by ${extensionName}@${extensionVersionNumber}
123// Please do not modify it manually. All changes will be lost.
124var React = require('react');
125var {Component} = React;
126
127var ReactNative = require('react-native');
128var {AppRegistry} = ReactNative;
129
130var EntryPoint = require('./exponentEntrypoint.js');
131var {${componentName}} = EntryPoint;
132
133AppRegistry.registerComponent('main', () => ${componentName});`;
134 return this.fileSystem.writeFile(this.dotvscodePath(EXPONENT_INDEX), fileContents);
135 });
136 }
137
138 /**
139 * Entrypoint file for exponent. Should copy the file used by the user as an entrypoint and modify it so that it can
140 * live inside .vscode and can be imported by exponentIndex.
141 * More specifically it will export the class and update all the relative references.
142 */
143 private createExponentEntrypoint(): Q.Promise<void> {
144 this.lazilyInitialize();
145 const pkg = require("../../../package.json");
146 const extensionVersionNumber = pkg.version;
147 const extensionName = pkg.name;
148 let entrypointComponentName = "";
149
150 const fileHeader =
151 `// This file is automatically generated by ${extensionName}@${extensionVersionNumber}
152// Please do not modify it manually. All changes will be lost.\n`;
153
154 return this.entrypointComponent()
155 .then(component => {
156 entrypointComponentName = component;
157 return this.entrypoint();
158 })
159 .then(filename => {
160 const pathToEntrypoint = this.pathToFileInWorkspace(filename);
161 return this.fileSystem.readFile(pathToEntrypoint, "utf-8");
162 })
163 .then(entrypointContents => {
164 // Export class
165 let modifiedContents = fileHeader + entrypointContents;
166 modifiedContents = modifiedContents.replace(`class ${entrypointComponentName}`, `export class ${entrypointComponentName}`);
167
168 // Prepend ../ to relative imports that refer to something in a parent directory
169 modifiedContents = modifiedContents.replace(/\.\.\/.*/, (substr) => { return `../${substr}`; });
170
171 // Replace ./ with ../ in relative imports that refer to something in the same directory
172 modifiedContents = modifiedContents.replace(/\.\/.*/, (substr) => { return `.${substr}`; });
173
174 return this.fileSystem.writeFile(this.dotvscodePath(EXPONENT_ENTRYPOINT), modifiedContents);
175 }).catch(() => {
176 return Q.reject<void>(new Error("Unable to read entrypoint. Make sure the file exists and it's under the root directory."));
177 });
178 }
179
180 /**
181 * Create exp.json file in the workspace root if not present
182 */
183 private ensureExpJson(): Q.Promise<void> {
184 this.lazilyInitialize();
185 let defaultSettings = {
186 "sdkVersion": "",
187 "entryPoint": EXPONENT_INDEX,
188 "slug": "",
189 "name": "",
190 };
191 return this.readVscodeExponentSettingFile()
192 .then(exponentJson => {
193 const expJsonPath = this.pathToFileInWorkspace(EXP_JSON);
194 if (!this.fileSystem.existsSync(expJsonPath) || exponentJson.overwriteExpJson) {
195 return this.getPackageName()
196 .then(name => {
197 // Name and slug are supposed to be the same,
198 // but slug only supports alpha numeric and -,
199 // while name should be human readable
200 defaultSettings.slug = name.replace(" ", "-");
201 defaultSettings.name = name;
202 return this.exponentSdk();
203 })
204 .then(exponentVersion => {
205 if (!exponentVersion) {
206 return XDL.supportedVersions()
207 .then((versions) => {
208 return Q.reject<void>(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`));
209 });
210 }
211 defaultSettings.sdkVersion = exponentVersion;
212 return this.fileSystem.writeFile(expJsonPath, JSON.stringify(defaultSettings, null, 4));
213 });
214 }
215 });
216 }
217
218 /**
219 * Changes npm dependency from react native to exponent's fork
220 */
221 private changeReactNativeToExponent(): Q.Promise<void> {
222 Log.logString("Checking if react native is from exponent.");
223 return this.usingReactNativeExponent(true)
224 .then(usingExponent => {
225 Log.logString(".\n");
226 if (usingExponent) {
227 return Q.resolve<void>(void 0);
228 }
229 Log.logString("Getting appropriate Exponent SDK Version to install.");
230 return this.exponentSdk(true)
231 .then(sdkVersion => {
232 Log.logString(".\n");
233 if (!sdkVersion) {
234 return XDL.supportedVersions()
235 .then((versions) => {
236 return Q.reject<void>(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`));
237 });
238 }
239 const exponentFork = `github:exponentjs/react-native#sdk-${sdkVersion}`;
240 Log.logString("Uninstalling current react native package.");
241 return Q(this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["uninstall", "react-native", "--verbose"], { verbosity: CommandVerbosity.PROGRESS }))
242 .then(() => {
243 Log.logString("Installing exponent react native package.");
244 return this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["install", exponentFork, "--cache-min", SECONDS_IN_DAY.toString(10), "--verbose"], { verbosity: CommandVerbosity.PROGRESS });
245 });
246 });
247 })
248 .then(() => {
249 this.dependencyPackage = ReactNativePackageStatus.EXPONENT_PACKAGE;
250 });
251 }
252
253 /**
254 * Changes npm dependency from exponent's fork to react native
255 */
256 private changeExponentToReactNative(): Q.Promise<void> {
257 Log.logString("Checking if the correct react native is installed.");
258 return this.usingReactNativeExponent()
259 .then(usingExponent => {
260 Log.logString(".\n");
261 if (!usingExponent) {
262 return Q.resolve<void>(void 0);
263 }
264 Log.logString("Uninstalling current react native package.");
265 return Q(this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["uninstall", "react-native", "--verbose"], { verbosity: CommandVerbosity.PROGRESS }))
266 .then(() => {
267 Log.logString("Installing correct react native package.");
268 return this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["install", "react-native", "--cache-min", SECONDS_IN_DAY.toString(10), "--verbose"], { verbosity: CommandVerbosity.PROGRESS });
269 });
270 })
271 .then(() => {
272 this.dependencyPackage = ReactNativePackageStatus.FACEBOOK_PACKAGE;
273 });
274 }
275
276 /**
277 * Reads VSCODE_EXPONENT Settings file. If it doesn't exists it creates one by
278 * guessing which entrypoint and filename to use.
279 */
280 private readVscodeExponentSettingFile(): Q.Promise<any> {
281 // Only create a new one if there is not one already
282 return this.fileSystem.exists(this.dotvscodePath(VSCODE_EXPONENT_JSON))
283 .then((vscodeExponentExists: boolean) => {
284 if (vscodeExponentExists) {
285 return this.fileSystem.readFile(this.dotvscodePath(VSCODE_EXPONENT_JSON), "utf-8")
286 .then(function (jsonContents: string): Q.Promise<any> {
287 return JSON.parse(stripJsonComments(jsonContents));
288 });
289 } else {
290 let defaultSettings = {
291 "entryPointFilename": "",
292 "entryPointComponent": "",
293 "overwriteExpJson": false,
294 };
295 return this.getPackageName()
296 .then(packageName => {
297 // By default react-native uses the package name for the entry component. This is our safest guess.
298 defaultSettings.entryPointComponent = packageName;
299 this.entrypointComponentName = defaultSettings.entryPointComponent;
300 return Q.all([
301 this.fileSystem.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX)),
302 this.fileSystem.exists(this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX)),
303 ]);
304 })
305 .spread((indexIosExists: boolean, mainExists: boolean) => {
306 // If there is an ios entrypoint we want to use that, if not let's go with android
307 defaultSettings.entryPointFilename =
308 mainExists ? DEFAULT_EXPONENT_INDEX
309 : indexIosExists ? DEFAULT_IOS_INDEX
310 : DEFAULT_ANDROID_INDEX;
311 this.entrypointFilename = defaultSettings.entryPointFilename;
312 return this.fileSystem.writeFile(this.dotvscodePath(VSCODE_EXPONENT_JSON), JSON.stringify(defaultSettings, null, 4));
313 })
314 .then(() => {
315 return defaultSettings;
316 });
317 }
318 });
319 }
320
321 /**
322 * Exponent sdk version that maps to the current react-native version
323 * If react native version is not supported it returns null.
324 */
325 private exponentSdk(showProgress: boolean = false): Q.Promise<string> {
326 if (showProgress) Log.logString("...");
327 if (this.expSdkVersion) {
328 return Q(this.expSdkVersion);
329 }
330 return this.readFromExpJson<string>("sdkVersion")
331 .then((sdkVersion) => {
332 if (showProgress) Log.logString(".");
333 if (sdkVersion) {
334 this.expSdkVersion = sdkVersion;
335 return this.expSdkVersion;
336 }
337 let reactNativeProjectHelper = new ReactNativeProjectHelper(this.rootPath);
338 return reactNativeProjectHelper.getReactNativeVersion()
339 .then(version => {
340 if (showProgress) Log.logString(".");
341 return XDL.mapVersion(version)
342 .then(exponentVersion => {
343 this.expSdkVersion = exponentVersion;
344 return this.expSdkVersion;
345 });
346 });
347 });
348 }
349
350 /**
351 * Returns the specified setting from exp.json if it exists
352 */
353 private readFromExpJson<T>(setting: string): Q.Promise<T> {
354 const expJsonPath = this.pathToFileInWorkspace(EXP_JSON);
355 return this.fileSystem.exists(expJsonPath)
356 .then((exists: boolean) => {
357 if (!exists) {
358 return null;
359 }
360 return this.fileSystem.readFile(expJsonPath, "utf-8")
361 .then(function (jsonContents: string): Q.Promise<T> {
362 return JSON.parse(stripJsonComments(jsonContents))[setting];
363 });
364 });
365 }
366
367 /**
368 * Looks at the _from attribute in the package json of the react-native dependency
369 * to figure out if it's using exponent.
370 */
371 private usingReactNativeExponent(showProgress: boolean = false): Q.Promise<boolean> {
372 if (showProgress) Log.logString("...");
373 if (this.dependencyPackage !== ReactNativePackageStatus.UNKNOWN) {
374 return Q(this.dependencyPackage === ReactNativePackageStatus.EXPONENT_PACKAGE);
375 }
376 // Look for the package.json of the dependecy
377 const pathToReactNativePackageJson = path.resolve(this.rootPath, "node_modules", "react-native", "package.json");
378 return this.fileSystem.readFile(pathToReactNativePackageJson, "utf-8")
379 .then(jsonContents => {
380 const packageJson = JSON.parse(jsonContents);
381 const isExp = /\bexponentjs\/react-native\b/.test(packageJson._from);
382 this.dependencyPackage = isExp ? ReactNativePackageStatus.EXPONENT_PACKAGE : ReactNativePackageStatus.FACEBOOK_PACKAGE;
383 if (showProgress) Log.logString(".");
384 return isExp;
385 }).catch(() => {
386 if (showProgress) Log.logString(".");
387 // Not in a react-native project
388 return false;
389 });
390 }
391
392 /**
393 * Name of the file (we assume it lives in the workspace root) that should be used as entrypoint.
394 * e.g. index.ios.js
395 */
396 private entrypoint(): Q.Promise<string> {
397 if (this.entrypointFilename) {
398 return Q(this.entrypointFilename);
399 }
400 return this.readVscodeExponentSettingFile()
401 .then((settingsJson) => {
402 // Let's load both to memory to make sure we are not reading from memory next time we query for this.
403 this.entrypointFilename = settingsJson.entryPointFilename;
404 this.entrypointComponentName = settingsJson.entryPointComponent;
405 return this.entrypointFilename;
406 });
407 }
408
409 /**
410 * Name of the component used as an entrypoint for the app.
411 */
412 private entrypointComponent(): Q.Promise<string> {
413 if (this.entrypointComponentName) {
414 return Q(this.entrypointComponentName);
415 }
416 return this.readVscodeExponentSettingFile()
417 .then((settingsJson) => {
418 // Let's load both to memory to make sure we are not reading from memory next time we query for this.
419 this.entrypointComponentName = settingsJson.entryPointComponent;
420 this.entrypointFilename = settingsJson.entrypointFilename;
421 return this.entrypointComponentName;
422 });
423 }
424
425 /**
426 * Path to the a given file inside the .vscode directory
427 */
428 private dotvscodePath(filename: string): string {
429 return path.join(this.rootPath, ".vscode", filename);
430 }
431
432 /**
433 * Path to the a given file from the workspace root
434 */
435 private pathToFileInWorkspace(filename: string): string {
436 return path.join(this.rootPath, filename);
437 }
438
439 /**
440 * Name specified on user's package.json
441 */
442 private getPackageName(): Q.Promise<string> {
443 return new Package(this.rootPath, { fileSystem: this.fileSystem }).name();
444 }
445
446 /**
447 * Works as a constructor but only initiliazes when it's actually needed.
448 */
449 private lazilyInitialize(): void {
450 if (!this.hasInitialized) {
451 this.hasInitialized = true;
452 this.fileSystem = new FileSystem();
453 this.commandExecutor = new CommandExecutor(this.rootPath);
454 this.dependencyPackage = ReactNativePackageStatus.UNKNOWN;
455
456 XDL.configReactNativeVersionWargnings();
457 XDL.attachLoggerStream(this.rootPath, {
458 stream: {
459 write: (chunk: any) => {
460 if (chunk.level <= 30) {
461 Log.logString(chunk.msg);
462 } else if (chunk.level === 40) {
463 Log.logWarning(chunk.msg);
464 } else {
465 Log.logError(chunk.msg);
466 }
467 },
468 },
469 type: "raw",
470 });
471 }
472 }
473}
474