microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0d827d9b424481979deb5f868a141d64c307491b

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/common/exponent/exponentHelper.ts

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