microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8a67e140aee58da792b1e9c1c17358a98d5a3704

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/common/exponent/exponentHelper.ts

308lines · 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
4/// <reference path="exponentHelper.d.ts" />
5
6import * as path from "path";
7import * as Q from "q";
8import * as XDL from "./xdlInterface";
9import { Package } from "../node/package";
10import { ReactNativeProjectHelper } from "../reactNativeProjectHelper";
11import { FileSystem } from "../node/fileSystem";
12import { Log } from "../log/log";
13import stripJSONComments = require("strip-json-comments");
14
15const APP_JSON = "app.json";
16const EXP_JSON = "exp.json";
17
18const EXPONENT_INDEX = "exponentIndex.js";
19const DEFAULT_EXPONENT_INDEX = "index.js";
20const DEFAULT_IOS_INDEX = "index.ios.js";
21const DEFAULT_ANDROID_INDEX = "index.android.js";
22
23const DBL_SLASHES = /\\/g;
24
25export class ExponentHelper {
26 private workspaceRootPath: string;
27 private projectRootPath: string;
28 private fs: FileSystem;
29 private hasInitialized: boolean;
30
31 public constructor(workspaceRootPath: string, projectRootPath: string) {
32 this.workspaceRootPath = workspaceRootPath;
33 this.projectRootPath = projectRootPath;
34 this.hasInitialized = false;
35 // Constructor is slim by design. This is to add as less computation as possible
36 // to the initialization of the extension. If a public method is added, make sure
37 // to call this.lazilyInitialize() at the begining of the code to be sure all variables
38 // are correctly initialized.
39 }
40
41 public configureExponentEnvironment(): Q.Promise<void> {
42 this.lazilyInitialize();
43 Log.logMessage("Making sure your project uses the correct dependencies for exponent. This may take a while...");
44 return this.isExpoApp(true)
45 .then(isExpo => {
46 Log.logString(".\n");
47
48 return this.patchAppJson(isExpo);
49 });
50 }
51
52 /**
53 * Returns the current user. If there is none, asks user for username and password and logins to exponent servers.
54 */
55 public loginToExponent(
56 promptForInformation: (message: string, password: boolean) => Q.Promise<string>,
57 showMessage: (message: string) => Q.Promise<string>
58 ): Q.Promise<XDL.IUser> {
59 this.lazilyInitialize();
60 return XDL.currentUser()
61 .then((user) => {
62 if (!user) {
63 let username = "";
64 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.")
65 .then(() =>
66 promptForInformation("Exponent username", false)
67 ).then((name: string) => {
68 username = name;
69 return promptForInformation("Exponent password", true);
70 })
71 .then((password: string) =>
72 XDL.login(username, password));
73 }
74 return user;
75 })
76 .catch(error => {
77 return Q.reject<XDL.IUser>(error);
78 });
79 }
80
81 public getExpPackagerOptions(): Q.Promise<ExpConfigPackager> {
82 this.lazilyInitialize();
83 return this.getFromExpConfig("packagerOpts")
84 .then(opts => opts || {});
85 }
86
87 /**
88 * Path to a given file inside the .vscode directory
89 */
90 private dotvscodePath(filename: string): string {
91 return path.join(this.workspaceRootPath, ".vscode", filename);
92 }
93
94 private createExpoEntry(name: string): Q.Promise<void> {
95 this.lazilyInitialize();
96 return this.detectEntry()
97 .then((entryPoint: string) => {
98 const content = this.generateFileContent(name, entryPoint);
99 return this.fs.writeFile(this.dotvscodePath(EXPONENT_INDEX), content);
100 });
101
102 }
103
104 private detectEntry(): Q.Promise<string> {
105 this.lazilyInitialize();
106 return Q.all([
107 this.fs.exists(this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX)),
108 this.fs.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX)),
109 this.fs.exists(this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX)),
110 ])
111 .spread((expo: boolean, ios: boolean): string => {
112 return expo ? this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX) :
113 ios ? this.pathToFileInWorkspace(DEFAULT_IOS_INDEX) :
114 this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX);
115 });
116 }
117
118 private generateFileContent(name: string, entryPoint: string): string {
119 return `// This file is automatically generated by VS Code
120// Please do not modify it manually. All changes will be lost.
121var React = require('${this.pathToFileInWorkspace("/node_modules/react")}');
122var { Component } = React;
123var ReactNative = require('${this.pathToFileInWorkspace("/node_modules/react-native")}');
124var { AppRegistry } = ReactNative;
125var entryPoint = require('${entryPoint}');
126AppRegistry.registerRunnable('main', function(appParameters) {
127 AppRegistry.runApplication('${name}', appParameters);
128});`;
129 }
130
131 private patchAppJson(isExpo: boolean = true): Q.Promise<void> {
132 return this.readAppJson()
133 .catch(() => {
134 // if app.json doesn't exist but it's ok, we will create it
135 return {};
136 })
137 .then((config: AppJson) => {
138 let expoConfig = <ExpConfig>(config.expo || {});
139 if (!expoConfig.name || !expoConfig.slug) {
140 return this.getPackageName()
141 .then((name: string) => {
142 expoConfig.slug = expoConfig.slug || config.name || name.replace(" ", "-");
143 expoConfig.name = expoConfig.name || config.name || name;
144 config.expo = expoConfig;
145 return config;
146 });
147 }
148
149 return config;
150 })
151 .then((config: AppJson) => {
152 if (!config.expo.sdkVersion) {
153 return this.exponentSdk(true)
154 .then(sdkVersion => {
155 config.expo.sdkVersion = sdkVersion;
156 return config;
157 });
158 }
159 return config;
160 })
161 .then((config: AppJson) => {
162 if (!isExpo) {
163 config.expo.entryPoint = this.dotvscodePath(EXPONENT_INDEX);
164 }
165
166 return config;
167 })
168 .then((config: AppJson) => {
169 return config ? this.writeAppJson(config) : config;
170 })
171 .then((config: AppJson) => {
172 return isExpo ? Q.resolve(void 0) : this.createExpoEntry(config.expo.name);
173 });
174 }
175
176 /**
177 * Exponent sdk version that maps to the current react-native version
178 * If react native version is not supported it returns null.
179 */
180 private exponentSdk(showProgress: boolean = false): Q.Promise<string> {
181 if (showProgress) {
182 Log.logString("...");
183 }
184
185 let reactNativeProjectHelper = new ReactNativeProjectHelper(this.projectRootPath);
186 return reactNativeProjectHelper.getReactNativeVersion()
187 .then(version => {
188 if (showProgress) Log.logString(".");
189 return XDL.mapVersion(version)
190 .then(sdkVersion => {
191 if (!sdkVersion) {
192 return XDL.supportedVersions()
193 .then((versions) => {
194 return Q.reject<string>(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`));
195 });
196 }
197 return sdkVersion;
198 });
199 });
200 }
201
202
203 /**
204 * Name specified on user's package.json
205 */
206 private getPackageName(): Q.Promise<string> {
207 return new Package(this.projectRootPath, { fileSystem: this.fs }).name();
208 }
209
210 private getExpConfig(): Q.Promise<ExpConfig> {
211 return this.readExpJson()
212 .catch(err => {
213 if (err.code === "ENOENT") {
214 return this.readAppJson()
215 .then((config: AppJson) => {
216 return config.expo || {};
217 });
218 }
219
220 return err;
221 });
222 }
223
224 private getFromExpConfig(key: string): Q.Promise<any> {
225 return this.getExpConfig()
226 .then((config: ExpConfig) => config[key]);
227 }
228
229 /**
230 * Returns the specified setting from exp.json if it exists
231 */
232 private readExpJson(): Q.Promise<ExpConfig> {
233 const expJsonPath = this.pathToFileInWorkspace(EXP_JSON);
234 return this.fs.readFile(expJsonPath)
235 .then(content => {
236 return JSON.parse(stripJSONComments(content));
237 });
238 }
239
240 private readAppJson(): Q.Promise<AppJson> {
241 const appJsonPath = this.pathToFileInWorkspace(APP_JSON);
242 return this.fs.readFile(appJsonPath)
243 .then(content => {
244 return JSON.parse(stripJSONComments(content));
245 });
246 }
247
248 private writeAppJson(config: AppJson): Q.Promise<AppJson> {
249 const appJsonPath = this.pathToFileInWorkspace(APP_JSON);
250 return this.fs.writeFile(appJsonPath, JSON.stringify(config, null, 2))
251 .then(() => config);
252 }
253
254 /**
255 * Path to a given file from the workspace root
256 */
257 private pathToFileInWorkspace(filename: string): string {
258 return path.join(this.projectRootPath, filename).replace(DBL_SLASHES, "/");
259 }
260
261 private isExpoApp(showProgress: boolean = false): Q.Promise<boolean> {
262 Log.logString("Checking if this is Expo app.");
263 if (showProgress) {
264 Log.logString("...");
265 }
266
267 const packageJsonPath = this.pathToFileInWorkspace("package.json");
268 return this.fs.readFile(packageJsonPath)
269 .then(content => {
270 const packageJson = JSON.parse(content);
271 const isExp = packageJson.dependencies && !!packageJson.dependencies.expo || false;
272 if (showProgress) Log.logString(".");
273 return isExp;
274 }).catch(() => {
275 if (showProgress) {
276 Log.logString(".");
277 }
278 // Not in a react-native project
279 return false;
280 });
281 }
282
283 /**
284 * Works as a constructor but only initiliazes when it's actually needed.
285 */
286 private lazilyInitialize(): void {
287 if (!this.hasInitialized) {
288 this.hasInitialized = true;
289 this.fs = new FileSystem();
290
291 XDL.configReactNativeVersionWargnings();
292 XDL.attachLoggerStream(this.projectRootPath, {
293 stream: {
294 write: (chunk: any) => {
295 if (chunk.level <= 30) {
296 Log.logString(chunk.msg);
297 } else if (chunk.level === 40) {
298 Log.logWarning(chunk.msg);
299 } else {
300 Log.logError(chunk.msg);
301 }
302 },
303 },
304 type: "raw",
305 });
306 }
307 }
308}