microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
cc7d2e1bd7c2985d11d65ac2268248b4e9fcbc2f

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/common/exponent/exponentHelper.ts

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