microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
14fc31721677a63847ccda02e588e2b4bea71e57

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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