microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
34472878f9e8d227bd5d0902161c571864c5d12d

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

631lines · 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 XDL from "./exponent/xdlInterface";
6import { SettingsHelper } from "./settingsHelper";
7import { OutputChannelLogger } from "./log/OutputChannelLogger";
8import { TargetType, GeneralMobilePlatform } from "./generalMobilePlatform";
9import { AndroidPlatform } from "./android/androidPlatform";
10import { IOSPlatform } from "./ios/iOSPlatform";
11import { ProjectVersionHelper } from "../common/projectVersionHelper";
12import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
13import { TargetPlatformHelper } from "../common/targetPlatformHelper";
14import { TelemetryHelper } from "../common/telemetryHelper";
15import { ProjectsStorage } from "./projectsStorage";
16import { IAndroidRunOptions, IIOSRunOptions, PlatformType } from "./launchArgs";
17import { ExponentPlatform } from "./exponent/exponentPlatform";
18import { spawn, ChildProcess } from "child_process";
19import { HostPlatform } from "../common/hostPlatform";
20import { CommandExecutor } from "../common/commandExecutor";
21import * as nls from "vscode-nls";
22import { ErrorHelper } from "../common/error/errorHelper";
23import { InternalErrorCode } from "../common/error/internalErrorCode";
24import { AppLauncher } from "./appLauncher";
25import { AndroidEmulatorManager } from "./android/androidEmulatorManager";
26import { AdbHelper } from "./android/adb";
27import { LogCatMonitor } from "./android/logCatMonitor";
28import { LogCatMonitorManager } from "./android/logCatMonitorManager";
29nls.config({
30 messageFormat: nls.MessageFormat.bundle,
31 bundleFormat: nls.BundleFormat.standalone,
32})();
33const localize = nls.loadMessageBundle();
34
35export class CommandPaletteHandler {
36 public static elementInspector: ChildProcess | null;
37 private static logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
38
39 /**
40 * Starts the React Native packager
41 */
42 public static startPackager(): Promise<void> {
43 return this.selectProject().then((appLauncher: AppLauncher) => {
44 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
45 appLauncher.getPackager().getProjectPath(),
46 // eslint-disable-next-line @typescript-eslint/no-unused-vars
47 ).then(versions => {
48 return this.executeCommandInContext(
49 "startPackager",
50 appLauncher.getWorkspaceFolder(),
51 () => {
52 return appLauncher
53 .getPackager()
54 .isRunning()
55 .then(running => {
56 return running
57 ? appLauncher.getPackager().stop()
58 : Promise.resolve();
59 });
60 },
61 ).then(() => appLauncher.getPackager().start());
62 });
63 });
64 }
65
66 /**
67 * Kills the React Native packager invoked by the extension's packager
68 */
69 public static stopPackager(): Promise<void> {
70 return this.selectProject().then((appLauncher: AppLauncher) => {
71 return this.executeCommandInContext(
72 "stopPackager",
73 appLauncher.getWorkspaceFolder(),
74 () => appLauncher.getPackager().stop(),
75 );
76 });
77 }
78
79 public static stopAllPackagers(): Promise<void> {
80 let keys = Object.keys(ProjectsStorage.projectsCache);
81 let promises: Promise<void>[] = [];
82 keys.forEach(key => {
83 let appLauncher = ProjectsStorage.projectsCache[key];
84 promises.push(
85 this.executeCommandInContext("stopPackager", appLauncher.getWorkspaceFolder(), () =>
86 appLauncher.getPackager().stop(),
87 ),
88 );
89 });
90
91 return Promise.all(promises).then(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
92 }
93
94 /**
95 * Restarts the React Native packager
96 */
97 public static restartPackager(): Promise<void> {
98 return this.selectProject().then((appLauncher: AppLauncher) => {
99 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
100 appLauncher.getPackager().getProjectPath(),
101 // eslint-disable-next-line @typescript-eslint/no-unused-vars
102 ).then(versions => {
103 return this.executeCommandInContext(
104 "restartPackager",
105 appLauncher.getWorkspaceFolder(),
106 () => this.runRestartPackagerCommandAndUpdateStatus(appLauncher),
107 );
108 });
109 });
110 }
111
112 /**
113 * Execute command to publish to exponent host.
114 */
115 public static publishToExpHost(): Promise<void> {
116 return this.selectProject().then((appLauncher: AppLauncher) => {
117 return this.executeCommandInContext(
118 "publishToExpHost",
119 appLauncher.getWorkspaceFolder(),
120 () => {
121 return this.executePublishToExpHost(appLauncher).then(didPublish => {
122 if (!didPublish) {
123 CommandPaletteHandler.logger.warning(
124 localize(
125 "ExponentPublishingWasUnsuccessfulMakeSureYoureLoggedInToExpo",
126 "Publishing was unsuccessful. Please make sure you are logged in Expo and your project is a valid Expo project",
127 ),
128 );
129 }
130 });
131 },
132 );
133 });
134 }
135
136 public static async launchAndroidEmulator(): Promise<void> {
137 const appLauncher = await this.selectProject();
138 const adbHelper = new AdbHelper(appLauncher.getPackager().getProjectPath());
139 const androidEmulatorManager = new AndroidEmulatorManager(adbHelper);
140 const emulator = await androidEmulatorManager.startSelection();
141 if (emulator) {
142 androidEmulatorManager.tryLaunchEmulatorByName(emulator);
143 }
144 }
145
146 /**
147 * Executes the 'react-native run-android' command
148 */
149 public static runAndroid(target: TargetType = "simulator"): Promise<void> {
150 return this.selectProject().then((appLauncher: AppLauncher) => {
151 TargetPlatformHelper.checkTargetPlatformSupport(PlatformType.Android);
152 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
153 appLauncher.getPackager().getProjectPath(),
154 ).then(versions => {
155 appLauncher.setReactNativeVersions(versions);
156 return this.executeCommandInContext(
157 "runAndroid",
158 appLauncher.getWorkspaceFolder(),
159 () => {
160 const platform = <AndroidPlatform>(
161 this.createPlatform(
162 appLauncher,
163 PlatformType.Android,
164 AndroidPlatform,
165 target,
166 )
167 );
168 return platform
169 .resolveVirtualDevice(target)
170 .then(() => platform.beforeStartPackager())
171 .then(() => {
172 return platform.startPackager();
173 })
174 .then(() => {
175 return platform.runApp(/*shouldLaunchInAllDevices*/ true);
176 })
177 .then(() => {
178 return platform.disableJSDebuggingMode();
179 });
180 },
181 );
182 });
183 });
184 }
185
186 /**
187 * Executes the 'react-native run-ios' command
188 */
189 public static runIos(target: TargetType = "simulator"): Promise<void> {
190 return this.selectProject().then((appLauncher: AppLauncher) => {
191 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
192 appLauncher.getPackager().getProjectPath(),
193 ).then(versions => {
194 appLauncher.setReactNativeVersions(versions);
195 TargetPlatformHelper.checkTargetPlatformSupport(PlatformType.iOS);
196 return this.executeCommandInContext(
197 "runIos",
198 appLauncher.getWorkspaceFolder(),
199 () => {
200 const platform = <IOSPlatform>(
201 this.createPlatform(appLauncher, PlatformType.iOS, IOSPlatform, target)
202 );
203 return (
204 platform
205 .resolveVirtualDevice(target)
206 .then(() => platform.beforeStartPackager())
207 .then(() => {
208 return platform.startPackager();
209 })
210 .then(() => {
211 // Set the Debugging setting to disabled, because in iOS it's persisted across runs of the app
212 return platform.disableJSDebuggingMode();
213 })
214 // eslint-disable-next-line @typescript-eslint/no-empty-function
215 .catch(() => {}) // If setting the debugging mode fails, we ignore the error and we run the run ios command anyways
216 .then(() => {
217 return platform.runApp();
218 })
219 );
220 },
221 );
222 });
223 });
224 }
225
226 /**
227 * Starts the Exponent packager
228 */
229 public static runExponent(): Promise<void> {
230 return this.selectProject().then((appLauncher: AppLauncher) => {
231 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
232 appLauncher.getPackager().getProjectPath(),
233 ).then(versions => {
234 return this.loginToExponent(appLauncher).then(() => {
235 return this.executeCommandInContext(
236 "runExponent",
237 appLauncher.getWorkspaceFolder(),
238 () => {
239 appLauncher.setReactNativeVersions(versions);
240 const platform = <ExponentPlatform>(
241 this.createPlatform(
242 appLauncher,
243 PlatformType.Exponent,
244 ExponentPlatform,
245 )
246 );
247 return platform
248 .beforeStartPackager()
249 .then(() => {
250 return platform.startPackager();
251 })
252 .then(() => {
253 return platform.runApp();
254 });
255 },
256 );
257 });
258 });
259 });
260 }
261
262 public static showDevMenu(): Promise<void> {
263 return this.selectProject().then((appLauncher: AppLauncher) => {
264 const androidPlatform = <AndroidPlatform>(
265 this.createPlatform(appLauncher, PlatformType.Android, AndroidPlatform)
266 );
267 androidPlatform
268 .showDevMenu()
269 // eslint-disable-next-line @typescript-eslint/no-empty-function
270 .catch(() => {}); // Ignore any errors
271
272 if (process.platform === "darwin") {
273 const iosPlatform = <IOSPlatform>(
274 this.createPlatform(appLauncher, PlatformType.iOS, IOSPlatform)
275 );
276 iosPlatform
277 .showDevMenu(appLauncher)
278 // eslint-disable-next-line @typescript-eslint/no-empty-function
279 .catch(() => {}); // Ignore any errors
280 }
281 return Promise.resolve();
282 });
283 }
284
285 public static reloadApp(): Promise<void> {
286 return this.selectProject().then((appLauncher: AppLauncher) => {
287 const androidPlatform = <AndroidPlatform>(
288 this.createPlatform(appLauncher, PlatformType.Android, AndroidPlatform)
289 );
290 androidPlatform
291 .reloadApp()
292 // eslint-disable-next-line @typescript-eslint/no-empty-function
293 .catch(() => {}); // Ignore any errors
294
295 if (process.platform === "darwin") {
296 const iosPlatform = <IOSPlatform>(
297 this.createPlatform(appLauncher, PlatformType.iOS, IOSPlatform)
298 );
299 iosPlatform
300 .reloadApp(appLauncher)
301 // eslint-disable-next-line @typescript-eslint/no-empty-function
302 .catch(() => {}); // Ignore any errors
303 }
304 return Promise.resolve();
305 });
306 }
307
308 public static runElementInspector(): Promise<void> {
309 if (!CommandPaletteHandler.elementInspector) {
310 // Remove the following env variables to prevent running electron app in node mode.
311 // https://github.com/microsoft/vscode/issues/3011#issuecomment-184577502
312 let env = Object.assign({}, process.env);
313 delete env.ATOM_SHELL_INTERNAL_RUN_AS_NODE;
314 delete env.ELECTRON_RUN_AS_NODE;
315 let command = HostPlatform.getNpmCliCommand("react-devtools");
316 CommandPaletteHandler.elementInspector = spawn(command, [], {
317 env,
318 });
319 if (!CommandPaletteHandler.elementInspector.pid) {
320 CommandPaletteHandler.elementInspector = null;
321 return Promise.reject(
322 ErrorHelper.getInternalError(InternalErrorCode.ReactDevtoolsIsNotInstalled),
323 );
324 }
325 CommandPaletteHandler.elementInspector.stdout.on("data", (data: string) => {
326 this.logger.info(data);
327 });
328 CommandPaletteHandler.elementInspector.stderr.on("data", (data: string) => {
329 this.logger.error(data);
330 });
331 CommandPaletteHandler.elementInspector.once("exit", () => {
332 CommandPaletteHandler.elementInspector = null;
333 });
334 } else {
335 this.logger.info(
336 localize(
337 "AnotherElementInspectorAlreadyRun",
338 "Another element inspector already run",
339 ),
340 );
341 }
342 return Promise.resolve();
343 }
344
345 public static stopElementInspector(): void {
346 return CommandPaletteHandler.elementInspector
347 ? CommandPaletteHandler.elementInspector.kill()
348 : void 0;
349 }
350
351 public static getPlatformByCommandName(commandName: string): string {
352 commandName = commandName.toLocaleLowerCase();
353
354 if (commandName.indexOf(PlatformType.Android) > -1) {
355 return PlatformType.Android;
356 }
357
358 if (commandName.indexOf(PlatformType.iOS) > -1) {
359 return PlatformType.iOS;
360 }
361
362 if (commandName.indexOf(PlatformType.Exponent) > -1) {
363 return PlatformType.Exponent;
364 }
365
366 return "";
367 }
368
369 public static startLogCatMonitor(): Promise<void> {
370 return this.selectProject().then(appLauncher => {
371 const adbHelper = new AdbHelper(appLauncher.getPackager().getProjectPath());
372 const avdManager = new AndroidEmulatorManager(adbHelper);
373 return avdManager.selectOnlineDevice().then(deviceId => {
374 if (deviceId) {
375 LogCatMonitorManager.delMonitor(deviceId); // Stop previous logcat monitor if it's running
376 let logCatArguments = SettingsHelper.getLogCatFilteringArgs(
377 appLauncher.getWorkspaceFolderUri(),
378 );
379 // this.logCatMonitor can be mutated, so we store it locally too
380 let logCatMonitor = new LogCatMonitor(deviceId, adbHelper, logCatArguments);
381 LogCatMonitorManager.addMonitor(logCatMonitor);
382 logCatMonitor
383 .start() // The LogCat will continue running forever, so we don't wait for it
384 .catch(() =>
385 this.logger.warning(
386 localize(
387 "ErrorWhileMonitoringLogCat",
388 "Error while monitoring LogCat",
389 ),
390 ),
391 );
392 } else {
393 vscode.window.showErrorMessage(
394 localize(
395 "OnlineAndroidDeviceNotFound",
396 "Could not find a proper online Android device to start a LogCat monitor",
397 ),
398 );
399 }
400 });
401 });
402 }
403
404 public static stopLogCatMonitor(): Promise<void> {
405 return this.selectLogCatMonitor().then(monitor => {
406 LogCatMonitorManager.delMonitor(monitor.deviceId);
407 });
408 }
409
410 private static createPlatform(
411 appLauncher: AppLauncher,
412 platform: PlatformType.iOS | PlatformType.Android | PlatformType.Exponent,
413 platformClass: typeof GeneralMobilePlatform,
414 target?: TargetType,
415 ): GeneralMobilePlatform {
416 const runOptions = CommandPaletteHandler.getRunOptions(appLauncher, platform, target);
417 return new platformClass(runOptions, {
418 packager: appLauncher.getPackager(),
419 });
420 }
421
422 private static runRestartPackagerCommandAndUpdateStatus(
423 appLauncher: AppLauncher,
424 ): Promise<void> {
425 return appLauncher
426 .getPackager()
427 .restart(SettingsHelper.getPackagerPort(appLauncher.getWorkspaceFolderUri().fsPath));
428 }
429
430 /**
431 * Ensures that we are in a React Native project and then executes the operation
432 * Otherwise, displays an error message banner
433 * {operation} - a function that performs the expected operation
434 */
435 private static executeCommandInContext(
436 rnCommand: string,
437 workspaceFolder: vscode.WorkspaceFolder,
438 operation: () => Promise<void>,
439 ): Promise<void> {
440 const extProps = {
441 platform: {
442 value: CommandPaletteHandler.getPlatformByCommandName(rnCommand),
443 isPii: false,
444 },
445 };
446
447 return TelemetryHelper.generate("RNCommand", extProps, generator => {
448 generator.add("command", rnCommand, false);
449 const projectRoot = SettingsHelper.getReactNativeProjectRoot(
450 workspaceFolder.uri.fsPath,
451 );
452 this.logger.debug(`Command palette: run project ${projectRoot} in context`);
453 return ReactNativeProjectHelper.isReactNativeProject(projectRoot).then(isRNProject => {
454 generator.add("isRNProject", isRNProject, false);
455 if (isRNProject) {
456 // Bring the log channel to focus
457 this.logger.setFocusOnLogChannel();
458
459 // Execute the operation
460 return operation();
461 } else {
462 vscode.window.showErrorMessage(
463 `${projectRoot} workspace is not a React Native project.`,
464 );
465 return;
466 }
467 });
468 });
469 }
470
471 /**
472 * 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.
473 */
474 private static executePublishToExpHost(appLauncher: AppLauncher): Promise<boolean> {
475 CommandPaletteHandler.logger.info(
476 localize(
477 "PublishingAppToExponentServer",
478 "Publishing app to Expo server. This might take a moment.",
479 ),
480 );
481 return this.loginToExponent(appLauncher).then(user => {
482 CommandPaletteHandler.logger.debug(`Publishing as ${user.username}...`);
483 return this.runExponent()
484 .then(() => XDL.publish(appLauncher.getWorkspaceFolderUri().fsPath))
485 .then(response => {
486 if (response.err || !response.url) {
487 return false;
488 }
489 const publishedOutput = localize(
490 "ExpoAppSuccessfullyPublishedTo",
491 "Expo app successfully published to {0}",
492 response.url,
493 );
494 CommandPaletteHandler.logger.info(publishedOutput);
495 vscode.window.showInformationMessage(publishedOutput);
496 return true;
497 });
498 });
499 }
500
501 private static loginToExponent(appLauncher: AppLauncher): Promise<XDL.IUser> {
502 return appLauncher
503 .getExponentHelper()
504 .loginToExponent(
505 (message, password) => {
506 return new Promise((resolve, reject) => {
507 vscode.window
508 .showInputBox({ placeHolder: message, password: password })
509 .then(login => {
510 resolve(login || "");
511 }, reject);
512 });
513 },
514 message => {
515 return new Promise((resolve, reject) => {
516 vscode.window.showInformationMessage(message).then(password => {
517 resolve(password || "");
518 }, reject);
519 });
520 },
521 )
522 .catch(err => {
523 CommandPaletteHandler.logger.warning(
524 localize(
525 "ExpoErrorOccuredMakeSureYouAreLoggedIn",
526 "An error has occured. Please make sure you are logged in to Expo, your project is setup correctly for publishing and your packager is running as Expo.",
527 ),
528 );
529 throw err;
530 });
531 }
532
533 private static selectProject(): Promise<AppLauncher> {
534 let keys = Object.keys(ProjectsStorage.projectsCache);
535 if (keys.length > 1) {
536 return new Promise((resolve, reject) => {
537 vscode.window.showQuickPick(keys).then(selected => {
538 if (selected) {
539 this.logger.debug(`Command palette: selected project ${selected}`);
540 resolve(ProjectsStorage.projectsCache[selected]);
541 }
542 }, reject);
543 });
544 } else if (keys.length === 1) {
545 this.logger.debug(`Command palette: once project ${keys[0]}`);
546 return Promise.resolve(ProjectsStorage.projectsCache[keys[0]]);
547 } else {
548 return Promise.reject(
549 ErrorHelper.getInternalError(
550 InternalErrorCode.WorkspaceNotFound,
551 "Current workspace does not contain React Native projects.",
552 ),
553 );
554 }
555 }
556
557 private static selectLogCatMonitor(): Promise<LogCatMonitor> {
558 let keys = Object.keys(LogCatMonitorManager.logCatMonitorsCache);
559 if (keys.length > 1) {
560 return new Promise((resolve, reject) => {
561 vscode.window.showQuickPick(keys).then(selected => {
562 if (selected) {
563 this.logger.debug(`Command palette: selected LogCat monitor ${selected}`);
564 resolve(LogCatMonitorManager.logCatMonitorsCache[selected]);
565 }
566 }, reject);
567 });
568 } else if (keys.length === 1) {
569 this.logger.debug(`Command palette: once LogCat monitor ${keys[0]}`);
570 return Promise.resolve(LogCatMonitorManager.logCatMonitorsCache[keys[0]]);
571 } else {
572 return Promise.reject(
573 ErrorHelper.getInternalError(
574 InternalErrorCode.AndroidCouldNotFindActiveLogCatMonitor,
575 ),
576 );
577 }
578 }
579
580 private static getRunOptions(
581 appLauncher: AppLauncher,
582 platform: PlatformType.iOS | PlatformType.Android | PlatformType.Exponent,
583 target: TargetType = "simulator",
584 ): IAndroidRunOptions | IIOSRunOptions {
585 const packagerPort = SettingsHelper.getPackagerPort(
586 appLauncher.getWorkspaceFolderUri().fsPath,
587 );
588 const runArgs = SettingsHelper.getRunArgs(
589 platform,
590 target,
591 appLauncher.getWorkspaceFolderUri(),
592 );
593 const envArgs = SettingsHelper.getEnvArgs(
594 platform,
595 target,
596 appLauncher.getWorkspaceFolderUri(),
597 );
598 const envFile = SettingsHelper.getEnvFile(
599 platform,
600 target,
601 appLauncher.getWorkspaceFolderUri(),
602 );
603 const projectRoot = SettingsHelper.getReactNativeProjectRoot(
604 appLauncher.getWorkspaceFolderUri().fsPath,
605 );
606 const runOptions: IAndroidRunOptions | IIOSRunOptions = {
607 platform: platform,
608 workspaceRoot: appLauncher.getWorkspaceFolderUri().fsPath,
609 projectRoot: projectRoot,
610 packagerPort: packagerPort,
611 runArguments: runArgs,
612 env: envArgs,
613 envFile: envFile,
614 reactNativeVersions: appLauncher.getReactNativeVersions() || {
615 reactNativeVersion: "",
616 reactNativeWindowsVersion: "",
617 reactNativeMacOSVersion: "",
618 },
619 };
620
621 if (platform === PlatformType.iOS && target === "device") {
622 runOptions.target = "device";
623 }
624
625 CommandExecutor.ReactNativeCommand = SettingsHelper.getReactNativeGlobalCommandName(
626 appLauncher.getWorkspaceFolderUri(),
627 );
628
629 return runOptions;
630 }
631}
632