microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
57deace7f89d033ec2db480ff2484490b194b54c

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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