microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.8.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/exponent/exponentHelper.ts

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