microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1030acf8149f71146468a2fa8364be809072bfbb

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/common/exponent/exponentHelper.ts

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