microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
b0af599c8ab7e27aa0876cf864d54790280e1c18

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_IOS_INDEX = "index.ios.js";
20const DEFAULT_ANDROID_INDEX = "index.android.js";
21const EXP_JSON = "exp.json";
22const SECONDS_IN_DAY = 86400;
23
24enum ReactNativePackageStatus {
25 FACEBOOK_PACKAGE,
26 EXPONENT_PACKAGE,
27 UNKNOWN
28}
29
30export class ExponentHelper {
31 private rootPath: string;
32 private fileSystem: FileSystem;
33 private commandExecutor: CommandExecutor;
34
35 private expSdkVersion: string;
36 private entrypointFilename: string;
37 private entrypointComponentName: string;
38
39 private dependencyPackage: ReactNativePackageStatus;
40 private hasInitialized: boolean;
41
42 public constructor(projectRootPath: string) {
43 this.rootPath = projectRootPath;
44 this.hasInitialized = false;
45 // Constructor is slim by design. This is to add as less computation as possible
46 // to the initialization of the extension. If a public method is added, make sure
47 // to call this.lazilyInitialize() at the begining of the code to be sure all variables
48 // are correctly initialized.
49 }
50
51 /**
52 * File used as an entrypoint for exponent. This file's component should be registered as "main"
53 * in the AppRegistry and should only render a entrypoint component.
54 */
55 public createIndex(): Q.Promise<void> {
56 this.lazilyInitialize();
57 const pkg = require("../../../package.json");
58 const extensionVersionNumber = pkg.version;
59 const extensionName = pkg.name;
60
61 return this.entrypointComponent()
62 .then((componentName) => {
63 const fileContents =
64 `// This file is automatically generated by ${extensionName}@${extensionVersionNumber}
65// Please do not modify it manually. All changes will be lost.
66var React = require('react');
67var {Component} = React;
68
69var ReactNative = require('react-native');
70var {AppRegistry} = ReactNative;
71
72var EntryPoint = require('./exponentEntrypoint.js');
73var {${componentName}} = EntryPoint;
74
75class ExponentVSCodeEntryPoint extends Component {
76 render() {
77 return (
78 <${componentName} />
79 );
80 }
81}
82AppRegistry.registerComponent('main', () => ExponentVSCodeEntryPoint);`;
83 return this.fileSystem.writeFile(this.dotvscodePath(EXPONENT_INDEX), fileContents);
84 });
85 }
86
87 /**
88 * Entrypoint file for exponent. Should copy the file used by the user as an entrypoint and modify it so that it can
89 * live inside .vscode and can be imported by exponentIndex.
90 * More specifically it will export the class and update all the relative references.
91 */
92 public createExponentEntrypoint(): Q.Promise<void> {
93 this.lazilyInitialize();
94 const pkg = require("../../../package.json");
95 const extensionVersionNumber = pkg.version;
96 const extensionName = pkg.name;
97 let entrypointComponentName = "";
98
99 const fileHeader =
100 `// This file is automatically generated by ${extensionName}@${extensionVersionNumber}
101// Please do not modify it manually. All changes will be lost.\n`;
102
103 return this.entrypointComponent()
104 .then(component => {
105 entrypointComponentName = component;
106 return this.entrypoint();
107 })
108 .then(filename => {
109 const pathToEntrypoint = this.pathToFileInWorkspace(filename);
110 return this.fileSystem.readFile(pathToEntrypoint, "utf-8");
111 })
112 .then(entrypointContents => {
113 // Export class
114 let modifiedContents = fileHeader + entrypointContents;
115 modifiedContents = modifiedContents.replace(`class ${entrypointComponentName}`, `export class ${entrypointComponentName}`);
116
117 // Prepend ../ to relative imports that refer to something in a parent directory
118 modifiedContents = modifiedContents.replace(/\.\.\/.*/, (substr) => { return `../${substr}`; });
119
120 // Replace ./ with ../ in relative imports that refer to something in the same directory
121 modifiedContents = modifiedContents.replace(/\.\/.*/, (substr) => { return `.${substr}`; });
122
123 return this.fileSystem.writeFile(this.dotvscodePath(EXPONENT_ENTRYPOINT), modifiedContents);
124 }).catch(() => {
125 return Q.reject<void>(new Error("Unable to read entrypoint. Make sure the file exists and it's under the root directory."));
126 });
127 }
128
129 /**
130 * Convert react native project to exponent.
131 * This consists on three steps:
132 * 1. Change the dependency from facebook's react-native to exponent's fork
133 * 2. Create exp.json
134 * 3. Create index and entrypoint for exponent
135 */
136 public configureExponentEnvironment(): Q.Promise<void> {
137 this.lazilyInitialize();
138 Log.logMessage("Making sure your project uses the correct dependencies for exponent. This may take a while...");
139 return this.changeReactNativeToExponent()
140 .then(() => {
141 Log.logMessage("Dependencies are correct. Making sure you have any necessary configuration file.");
142 return this.ensureExpJson();
143 }).then(() => {
144 Log.logMessage("Project setup is correct. Generating entrypoint code.");
145 return this.createExponentEntrypoint()
146 .then(() =>
147 this.createIndex());
148 });
149 }
150
151 /**
152 * Change dependencies to point to original react-native repo
153 */
154 public configureReactNativeEnvironment(): Q.Promise<void> {
155 this.lazilyInitialize();
156 Log.logMessage("Checking react native is correctly setup. This may take a while...");
157 return this.changeExponentToReactNative();
158 }
159
160 /**
161 * Returns the current user. If there is none, asks user for username and password and logins to exponent servers.
162 */
163 public loginToExponent(promptForInformation: (message: string, password: boolean) => Q.Promise<string>, showMessage: (message: string) => Q.Promise<string>): Q.Promise<XDL.IUser> {
164 this.lazilyInitialize();
165 return XDL.currentUser()
166 .then((user) => {
167 if (!user) {
168 let username = "";
169 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.")
170 .then(() =>
171 promptForInformation("Exponent username", false)
172 ).then((name) => {
173 username = name;
174 return promptForInformation("Exponent password", true);
175 })
176 .then((password) =>
177 XDL.login(username, password));
178 }
179 return user;
180 })
181 .catch(error => {
182 return Q.reject<XDL.IUser>(error);
183 });
184 }
185
186 /**
187 * Create exp.json file in the workspace root if not present
188 */
189 private ensureExpJson(): Q.Promise<void> {
190 this.lazilyInitialize();
191 let defaultSettings = {
192 "sdkVersion": "",
193 "entryPoint": EXPONENT_INDEX,
194 "slug": "",
195 "name": "",
196 };
197 return this.readVscodeExponentSettingFile()
198 .then(exponentJson => {
199 const expJsonPath = this.pathToFileInWorkspace(EXP_JSON);
200 if (!this.fileSystem.existsSync(expJsonPath) || exponentJson.overwriteExpJson) {
201 return this.getPackageName()
202 .then(name => {
203 // Name and slug are supposed to be the same,
204 // but slug only supports alpha numeric and -,
205 // while name should be human readable
206 defaultSettings.slug = name.replace(" ", "-");
207 defaultSettings.name = name;
208 return this.exponentSdk();
209 })
210 .then(exponentVersion => {
211 if (!exponentVersion) {
212 return XDL.supportedVersions()
213 .then((versions) => {
214 return Q.reject<void>(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`));
215 });
216 }
217 defaultSettings.sdkVersion = exponentVersion;
218 return this.fileSystem.writeFile(expJsonPath, JSON.stringify(defaultSettings, null, 4));
219 });
220 }
221 });
222 }
223
224 /**
225 * Changes npm dependency from react native to exponent's fork
226 */
227 private changeReactNativeToExponent(): Q.Promise<void> {
228 Log.logString("Checking if react native is from exponent.");
229 return this.usingReactNativeExponent(true)
230 .then(usingExponent => {
231 Log.logString(".\n");
232 if (usingExponent) {
233 return Q.resolve<void>(void 0);
234 }
235 Log.logString("Getting appropriate Exponent SDK Version to install.");
236 return this.exponentSdk(true)
237 .then(sdkVersion => {
238 Log.logString(".\n");
239 if (!sdkVersion) {
240 return XDL.supportedVersions()
241 .then((versions) => {
242 return Q.reject<void>(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`));
243 });
244 }
245 const exponentFork = `github:exponentjs/react-native#sdk-${sdkVersion}`;
246 Log.logString("Uninstalling current react native package.");
247 return Q(this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["uninstall", "react-native", "--verbose"], { verbosity: CommandVerbosity.PROGRESS }))
248 .then(() => {
249 Log.logString("Installing exponent react native package.");
250 return this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["install", exponentFork, "--cache-min", SECONDS_IN_DAY.toString(10), "--verbose"], { verbosity: CommandVerbosity.PROGRESS });
251 });
252 });
253 })
254 .then(() => {
255 this.dependencyPackage = ReactNativePackageStatus.EXPONENT_PACKAGE;
256 });
257 }
258
259 /**
260 * Changes npm dependency from exponent's fork to react native
261 */
262 private changeExponentToReactNative(): Q.Promise<void> {
263 Log.logString("Checking if the correct react native is installed.");
264 return this.usingReactNativeExponent()
265 .then(usingExponent => {
266 Log.logString(".\n");
267 if (!usingExponent) {
268 return Q.resolve<void>(void 0);
269 }
270 Log.logString("Uninstalling current react native package.");
271 return Q(this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["uninstall", "react-native", "--verbose"], { verbosity: CommandVerbosity.PROGRESS }))
272 .then(() => {
273 Log.logString("Installing correct react native package.");
274 return this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["install", "react-native", "--cache-min", SECONDS_IN_DAY.toString(10), "--verbose"], { verbosity: CommandVerbosity.PROGRESS });
275 });
276 })
277 .then(() => {
278 this.dependencyPackage = ReactNativePackageStatus.FACEBOOK_PACKAGE;
279 });
280 }
281
282 /**
283 * Reads VSCODE_EXPONENT Settings file. If it doesn't exists it creates one by
284 * guessing which entrypoint and filename to use.
285 */
286 private readVscodeExponentSettingFile(): Q.Promise<any> {
287 // Only create a new one if there is not one already
288 return this.fileSystem.exists(this.dotvscodePath(VSCODE_EXPONENT_JSON))
289 .then((vscodeExponentExists: boolean) => {
290 if (vscodeExponentExists) {
291 return this.fileSystem.readFile(this.dotvscodePath(VSCODE_EXPONENT_JSON), "utf-8")
292 .then(function (jsonContents: string): Q.Promise<any> {
293 return JSON.parse(stripJsonComments(jsonContents));
294 });
295 } else {
296 let defaultSettings = {
297 "entryPointFilename": "",
298 "entryPointComponent": "",
299 "overwriteExpJson": false,
300 };
301 return this.getPackageName()
302 .then(packageName => {
303 // By default react-native uses the package name for the entry component. This is our safest guess.
304 defaultSettings.entryPointComponent = packageName;
305 this.entrypointComponentName = defaultSettings.entryPointComponent;
306 return this.fileSystem.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX));
307 })
308 .then((indexIosExists: boolean) => {
309 // If there is an ios entrypoint we want to use that, if not let's go with android
310 defaultSettings.entryPointFilename = indexIosExists ? DEFAULT_IOS_INDEX : 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