microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
eba0de58017cd198f69bffca9be3d8e0e4df4d6a

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

403lines · 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 vscode from "vscode";
5import * as Q from "q";
6import * as XDL from "./exponent/xdlInterface";
7import {SettingsHelper} from "./settingsHelper";
8import {OutputChannelLogger} from "./log/OutputChannelLogger";
9import {Packager} from "../common/packager";
10import {TargetType, GeneralMobilePlatform} from "./generalMobilePlatform";
11import {AndroidPlatform} from "./android/androidPlatform";
12import {IOSPlatform} from "./ios/iOSPlatform";
13import {ReactNativeProjectHelper} from "../common/reactNativeProjectHelper";
14import {TargetPlatformHelper} from "../common/targetPlatformHelper";
15import {TelemetryHelper} from "../common/telemetryHelper";
16import {ExponentHelper} from "./exponent/exponentHelper";
17import {ReactDirManager} from "./reactDirManager";
18import {ExtensionServer} from "./extensionServer";
19import {IAndroidRunOptions, IIOSRunOptions} from "./launchArgs";
20import {ExponentPlatform} from "./exponent/exponentPlatform";
21import { spawn, ChildProcess } from "child_process";
22import * as path from "path";
23import * as fs from "fs";
24const electron = require("electron");
25
26interface IReactNativeStuff {
27 packager: Packager;
28 exponentHelper: ExponentHelper;
29 reactDirManager: ReactDirManager;
30 extensionServer: ExtensionServer;
31}
32
33interface IReactNativeProject extends IReactNativeStuff {
34 workspaceFolder: vscode.WorkspaceFolder;
35}
36
37export class CommandPaletteHandler {
38 public static elementInspector: ChildProcess | null;
39 private static projectsCache: {[key: string]: IReactNativeProject} = {};
40 private static logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
41
42 public static addFolder(workspaceFolder: vscode.WorkspaceFolder, stuff: IReactNativeStuff): void {
43 this.logger.debug(`Command palette: added folder ${workspaceFolder.uri.fsPath}`);
44 this.projectsCache[workspaceFolder.uri.fsPath] = {
45 ...stuff,
46 workspaceFolder,
47 };
48 }
49
50 public static getFolder(workspaceFolder: vscode.WorkspaceFolder): IReactNativeProject {
51 return this.projectsCache[workspaceFolder.uri.fsPath];
52 }
53
54 public static delFolder(workspaceFolder: vscode.WorkspaceFolder): void {
55 delete this.projectsCache[workspaceFolder.uri.fsPath];
56 }
57
58 /**
59 * Starts the React Native packager
60 */
61 public static startPackager(): Q.Promise<void> {
62 return this.selectProject()
63 .then((project: IReactNativeProject) => {
64 return this.executeCommandInContext("startPackager", project.workspaceFolder, () => {
65 return project.packager.isRunning()
66 .then((running) => {
67 return running ? project.packager.stop() : Q.resolve(void 0);
68 });
69 })
70 .then(() => project.packager.start());
71 });
72 }
73
74 /**
75 * Kills the React Native packager invoked by the extension's packager
76 */
77 public static stopPackager(): Q.Promise<void> {
78 return this.selectProject()
79 .then((project: IReactNativeProject) => {
80 return this.executeCommandInContext("stopPackager", project.workspaceFolder, () => project.packager.stop());
81 });
82 }
83
84 public static stopAllPackagers(): Q.Promise<void> {
85 let keys = Object.keys(this.projectsCache);
86 let promises: Q.Promise<void>[] = [];
87 keys.forEach((key) => {
88 let project = this.projectsCache[key];
89 promises.push(this.executeCommandInContext("stopPackager", project.workspaceFolder, () => project.packager.stop()));
90 });
91
92 return Q.all(promises).then(() => {});
93 }
94
95 /**
96 * Restarts the React Native packager
97 */
98 public static restartPackager(): Q.Promise<void> {
99 return this.selectProject()
100 .then((project: IReactNativeProject) => {
101 return this.executeCommandInContext("restartPackager", project.workspaceFolder, () =>
102 this.runRestartPackagerCommandAndUpdateStatus(project));
103 });
104 }
105
106 /**
107 * Execute command to publish to exponent host.
108 */
109 public static publishToExpHost(): Q.Promise<void> {
110 return this.selectProject()
111 .then((project: IReactNativeProject) => {
112 return this.executeCommandInContext("publishToExpHost", project.workspaceFolder, () => {
113 return this.executePublishToExpHost(project).then((didPublish) => {
114 if (!didPublish) {
115 CommandPaletteHandler.logger.warning("Publishing was unsuccessful. Please make sure you are logged in Exponent and your project is a valid Exponentjs project");
116 }
117 });
118 });
119 });
120 }
121
122 /**
123 * Executes the 'react-native run-android' command
124 */
125 public static runAndroid(target: TargetType = "simulator"): Q.Promise<void> {
126 return this.selectProject()
127 .then((project: IReactNativeProject) => {
128 TargetPlatformHelper.checkTargetPlatformSupport("android");
129 return this.executeCommandInContext("runAndroid", project.workspaceFolder, () => {
130 const platform = <AndroidPlatform>this.createPlatform(project, "android", AndroidPlatform, target);
131 return platform.beforeStartPackager()
132 .then(() => {
133 return platform.startPackager();
134 })
135 .then(() => {
136 return platform.runApp(/*shouldLaunchInAllDevices*/true);
137 })
138 .then(() => {
139 return platform.disableJSDebuggingMode();
140 });
141 });
142 });
143 }
144
145 /**
146 * Executes the 'react-native run-ios' command
147 */
148 public static runIos(target: TargetType = "simulator"): Q.Promise<void> {
149 return this.selectProject()
150 .then((project: IReactNativeProject) => {
151 TargetPlatformHelper.checkTargetPlatformSupport("ios");
152 return this.executeCommandInContext("runIos", project.workspaceFolder, () => {
153 const platform = <IOSPlatform>this.createPlatform(project, "ios", IOSPlatform, target);
154 return platform.beforeStartPackager()
155 .then(() => {
156 return platform.startPackager();
157 })
158 .then(() => {
159 // Set the Debugging setting to disabled, because in iOS it's persisted across runs of the app
160 return platform.disableJSDebuggingMode();
161 })
162 .catch(() => { }) // If setting the debugging mode fails, we ignore the error and we run the run ios command anyways
163 .then(() => {
164 return platform.runApp();
165 });
166 });
167 });
168 }
169
170 /**
171 * Starts the Exponent packager
172 */
173 public static runExponent(): Q.Promise<void> {
174 return this.selectProject()
175 .then((project: IReactNativeProject) => {
176 return this.loginToExponent(project)
177 .then(() => {
178 return this.executeCommandInContext("runExponent", project.workspaceFolder, () => {
179 const platform = <ExponentPlatform>this.createPlatform(project, "exponent", ExponentPlatform);
180 return platform.beforeStartPackager()
181 .then(() => {
182 return platform.startPackager();
183 })
184 .then(() => {
185 return platform.runApp();
186 });
187 });
188 });
189 });
190 }
191
192 public static showDevMenu(): Q.Promise<void> {
193 return this.selectProject()
194 .then((project: IReactNativeProject) => {
195 const androidPlatform = <AndroidPlatform>this.createPlatform(project, "android", AndroidPlatform);
196 androidPlatform.showDevMenu()
197 .catch(() => { }); // Ignore any errors
198 const iosPlatform = <IOSPlatform>this.createPlatform(project, "ios", IOSPlatform);
199 iosPlatform.showDevMenu()
200 .catch(() => { }); // Ignore any errors
201 return Q.resolve(void 0);
202 });
203 }
204
205 public static reloadApp(): Q.Promise<void> {
206 return this.selectProject()
207 .then((project: IReactNativeProject) => {
208 const androidPlatform = <AndroidPlatform>this.createPlatform(project, "android", AndroidPlatform);
209 androidPlatform.reloadApp()
210 .catch(() => { }); // Ignore any errors
211 const iosPlatform = <IOSPlatform>this.createPlatform(project, "ios", IOSPlatform);
212 iosPlatform.reloadApp()
213 .catch(() => { }); // Ignore any errors
214 return Q.resolve(void 0);
215 });
216 }
217
218 public static runElementInspector(): Q.Promise<void> {
219 if (!CommandPaletteHandler.elementInspector) {
220 // DO NOT CHANGE THIS PATH
221 const devToolsPath = path.resolve(__dirname, "..", "..", "node_modules", "react-devtools", "app.js");
222 if (fs.existsSync(devToolsPath)) {
223 // Remove the following env variables to prevent running electron app in node mode.
224 // https://github.com/Microsoft/vscode/issues/3011#issuecomment-184577502
225 let env = Object.assign({}, process.env);
226 delete env.ATOM_SHELL_INTERNAL_RUN_AS_NODE;
227 delete env.ELECTRON_RUN_AS_NODE;
228 CommandPaletteHandler.elementInspector = spawn(electron, [devToolsPath], {
229 env,
230 });
231 CommandPaletteHandler.elementInspector.stdout.on("data", (data: string) => {
232 this.logger.info(data);
233 });
234 CommandPaletteHandler.elementInspector.stderr.on("data", (data: string) => {
235 this.logger.error(data);
236 });
237 CommandPaletteHandler.elementInspector.once("exit", () => {
238 CommandPaletteHandler.elementInspector = null;
239 });
240 } else {
241 this.logger.error("Not found element inspector file, maybe not installed or damaged");
242 throw new Error("Not found element inspector file, maybe not installed or damaged");
243 }
244 } else {
245 this.logger.info("Another element inspector already run");
246 }
247 return Q(void 0);
248 }
249
250 public static stopElementInspector(): void {
251 return CommandPaletteHandler.elementInspector ? CommandPaletteHandler.elementInspector.kill() : void 0;
252 }
253
254 public static getPlatformByCommandName(commandName: string): string {
255 commandName = commandName.toLocaleLowerCase();
256
257 if (commandName.indexOf("android") > -1) {
258 return "android";
259 }
260
261 if (commandName.indexOf("ios") > -1) {
262 return "ios";
263 }
264
265 if (commandName.indexOf("exponent") > -1) {
266 return "exponent";
267 }
268
269 return "";
270 }
271
272 private static createPlatform(project: IReactNativeProject, platform: "ios" | "android" | "exponent", platformClass: typeof GeneralMobilePlatform, target?: TargetType): GeneralMobilePlatform {
273 const runOptions = CommandPaletteHandler.getRunOptions(project, platform, target);
274 return new platformClass(runOptions, {
275 packager: project.packager,
276 });
277 }
278
279 private static runRestartPackagerCommandAndUpdateStatus(project: IReactNativeProject): Q.Promise<void> {
280 return project.packager.restart(SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath));
281 }
282
283 /**
284 * Ensures that we are in a React Native project and then executes the operation
285 * Otherwise, displays an error message banner
286 * {operation} - a function that performs the expected operation
287 */
288 private static executeCommandInContext(rnCommand: string, workspaceFolder: vscode.WorkspaceFolder, operation: () => Q.Promise<void>): Q.Promise<void> {
289 const extProps = {
290 platform: {
291 value: CommandPaletteHandler.getPlatformByCommandName(rnCommand),
292 isPii: false,
293 },
294 };
295
296 return TelemetryHelper.generate("RNCommand", extProps, (generator) => {
297 generator.add("command", rnCommand, false);
298 const projectRoot = SettingsHelper.getReactNativeProjectRoot(workspaceFolder.uri.fsPath);
299 this.logger.debug(`Command palette: run project ${projectRoot} in context`);
300 return ReactNativeProjectHelper.isReactNativeProject(projectRoot)
301 .then(isRNProject => {
302 generator.add("isRNProject", isRNProject, false);
303 if (isRNProject) {
304 // Bring the log channel to focus
305 this.logger.setFocusOnLogChannel();
306
307 // Execute the operation
308 return operation();
309 } else {
310 vscode.window.showErrorMessage(`${projectRoot} workspace is not a React Native project.`);
311 return;
312 }
313 });
314 });
315 }
316
317 /**
318 * Publish project to exponent server. In order to do this we need to make sure the user is logged in exponent and the packager is running.
319 */
320 private static executePublishToExpHost(project: IReactNativeProject): Q.Promise<boolean> {
321 CommandPaletteHandler.logger.info("Publishing app to Exponent server. This might take a moment.");
322 return this.loginToExponent(project)
323 .then(user => {
324 CommandPaletteHandler.logger.debug(`Publishing as ${user.username}...`);
325 return this.runExponent()
326 .then(() =>
327 XDL.publish(project.workspaceFolder.uri.fsPath))
328 .then(response => {
329 if (response.err || !response.url) {
330 return false;
331 }
332 const publishedOutput = `App successfully published to ${response.url}`;
333 CommandPaletteHandler.logger.info(publishedOutput);
334 vscode.window.showInformationMessage(publishedOutput);
335 return true;
336 });
337 });
338 }
339
340 private static loginToExponent(project: IReactNativeProject): Q.Promise<XDL.IUser> {
341 return project.exponentHelper.loginToExponent(
342 (message, password) => {
343 return Q.Promise((resolve, reject) => {
344 vscode.window.showInputBox({ placeHolder: message, password: password })
345 .then(login => {
346 resolve(login || "");
347 }, reject);
348 });
349 },
350 (message) => {
351 return Q.Promise((resolve, reject) => {
352 vscode.window.showInformationMessage(message)
353 .then(password => {
354 resolve(password || "");
355 }, reject);
356 });
357 }
358 )
359 .catch((err) => {
360 CommandPaletteHandler.logger.warning("An error has occured. Please make sure you are logged in to exponent, your project is setup correctly for publishing and your packager is running as exponent.");
361 throw err;
362 });
363 }
364
365 private static selectProject(): Q.Promise<IReactNativeProject> {
366 let keys = Object.keys(this.projectsCache);
367 if (keys.length > 1) {
368 return Q.Promise((resolve, reject) => {
369 vscode.window.showQuickPick(keys)
370 .then((selected) => {
371 if (selected) {
372 this.logger.debug(`Command palette: selected project ${selected}`);
373 resolve(this.projectsCache[selected]);
374 }
375 }, reject);
376 });
377 } else if (keys.length === 1) {
378 this.logger.debug(`Command palette: once project ${keys[0]}`);
379 return Q.resolve(this.projectsCache[keys[0]]);
380 } else {
381 return Q.reject(new Error("Current workspace does not contain React Native projects."));
382 }
383 }
384
385 private static getRunOptions(project: IReactNativeProject, platform: "ios" | "android" | "exponent", target: TargetType = "simulator"): IAndroidRunOptions | IIOSRunOptions {
386 const packagerPort = SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath);
387 const runArgs = SettingsHelper.getRunArgs(platform, target, project.workspaceFolder.uri);
388 const envArgs = SettingsHelper.getEnvArgs(platform, target, project.workspaceFolder.uri);
389 const envFile = SettingsHelper.getEnvFile(platform, target, project.workspaceFolder.uri);
390 const projectRoot = SettingsHelper.getReactNativeProjectRoot(project.workspaceFolder.uri.fsPath);
391 const runOptions: IAndroidRunOptions | IIOSRunOptions = {
392 platform: platform,
393 workspaceRoot: project.workspaceFolder.uri.fsPath,
394 projectRoot: projectRoot,
395 packagerPort: packagerPort,
396 runArguments: runArgs,
397 env: envArgs,
398 envFile: envFile,
399 };
400
401 return runOptions;
402 }
403}