microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.3.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/common/exponent/exponentHelper.ts

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