microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8df5011e47ada44be3951868ba32e5fae98f48bb

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

471lines · 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({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
30const localize = nls.loadMessageBundle();
31
32export class CommandPaletteHandler {
33 public static elementInspector: ChildProcess | null;
34 private static logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
35
36 /**
37 * Starts the React Native packager
38 */
39 public static startPackager(): Promise<void> {
40 return this.selectProject()
41 .then((appLauncher: AppLauncher) => {
42 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
43 .then(versions => {
44 return this.executeCommandInContext("startPackager", appLauncher.getWorkspaceFolder(), () => {
45 return appLauncher.getPackager().isRunning()
46 .then((running) => {
47 return running ? appLauncher.getPackager().stop() : Promise.resolve();
48 });
49 })
50 .then(() => appLauncher.getPackager().start());
51 });
52 });
53 }
54
55 /**
56 * Kills the React Native packager invoked by the extension's packager
57 */
58 public static stopPackager(): Promise<void> {
59 return this.selectProject()
60 .then((appLauncher: AppLauncher) => {
61 return this.executeCommandInContext("stopPackager", appLauncher.getWorkspaceFolder(), () => appLauncher.getPackager().stop());
62 });
63 }
64
65 public static stopAllPackagers(): Promise<void> {
66 let keys = Object.keys(ProjectsStorage.projectsCache);
67 let promises: Promise<void>[] = [];
68 keys.forEach((key) => {
69 let appLauncher = ProjectsStorage.projectsCache[key];
70 promises.push(this.executeCommandInContext("stopPackager", appLauncher.getWorkspaceFolder(), () => appLauncher.getPackager().stop()));
71 });
72
73 return Promise.all(promises).then(() => {});
74 }
75
76 /**
77 * Restarts the React Native packager
78 */
79 public static restartPackager(): Promise<void> {
80 return this.selectProject()
81 .then((appLauncher: AppLauncher) => {
82 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
83 .then(versions => {
84 return this.executeCommandInContext("restartPackager", appLauncher.getWorkspaceFolder(), () =>
85 this.runRestartPackagerCommandAndUpdateStatus(appLauncher));
86 });
87 });
88 }
89
90 /**
91 * Execute command to publish to exponent host.
92 */
93 public static publishToExpHost(): Promise<void> {
94 return this.selectProject()
95 .then((appLauncher: AppLauncher) => {
96 return this.executeCommandInContext("publishToExpHost", appLauncher.getWorkspaceFolder(), () => {
97 return this.executePublishToExpHost(appLauncher).then((didPublish) => {
98 if (!didPublish) {
99 CommandPaletteHandler.logger.warning(localize("ExponentPublishingWasUnsuccessfulMakeSureYoureLoggedInToExpo", "Publishing was unsuccessful. Please make sure you are logged in Expo and your project is a valid Expo project"));
100 }
101 });
102 });
103 });
104 }
105
106 public static async launchAndroidEmulator(): Promise<void> {
107 const appLauncher = await this.selectProject();
108 const adbHelper = new AdbHelper(appLauncher.getPackager().getProjectPath());
109 const androidEmulatorManager = new AndroidEmulatorManager(adbHelper);
110 const emulator = await androidEmulatorManager.startSelection();
111 if (emulator) {
112 androidEmulatorManager.tryLaunchEmulatorByName(emulator);
113 }
114 }
115
116 /**
117 * Executes the 'react-native run-android' command
118 */
119 public static runAndroid(target: TargetType = "simulator"): Promise<void> {
120 return this.selectProject()
121 .then((appLauncher: AppLauncher) => {
122 TargetPlatformHelper.checkTargetPlatformSupport(PlatformType.Android);
123 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
124 .then(versions => {
125 appLauncher.setReactNativeVersions(versions);
126 return this.executeCommandInContext("runAndroid", appLauncher.getWorkspaceFolder(), () => {
127 const platform = <AndroidPlatform>this.createPlatform(appLauncher, PlatformType.Android, AndroidPlatform, target);
128 return platform.resolveVirtualDevice(target)
129 .then(() => 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 /**
145 * Executes the 'react-native run-ios' command
146 */
147 public static runIos(target: TargetType = "simulator"): Promise<void> {
148 return this.selectProject()
149 .then((appLauncher: AppLauncher) => {
150 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
151 .then(versions => {
152 appLauncher.setReactNativeVersions(versions);
153 TargetPlatformHelper.checkTargetPlatformSupport(PlatformType.iOS);
154 return this.executeCommandInContext("runIos", appLauncher.getWorkspaceFolder(), () => {
155 const platform = <IOSPlatform>this.createPlatform(appLauncher, PlatformType.iOS, IOSPlatform, target);
156 return platform.resolveVirtualDevice(target)
157 .then(() => platform.beforeStartPackager())
158 .then(() => {
159 return platform.startPackager();
160 })
161 .then(() => {
162 // Set the Debugging setting to disabled, because in iOS it's persisted across runs of the app
163 return platform.disableJSDebuggingMode();
164 })
165 .catch(() => { }) // If setting the debugging mode fails, we ignore the error and we run the run ios command anyways
166 .then(() => {
167 return platform.runApp();
168 });
169 });
170 });
171 });
172 }
173
174 /**
175 * Starts the Exponent packager
176 */
177 public static runExponent(): Promise<void> {
178 return this.selectProject()
179 .then((appLauncher: AppLauncher) => {
180 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
181 .then(versions => {
182 return this.loginToExponent(appLauncher)
183 .then(() => {
184 return this.executeCommandInContext("runExponent", appLauncher.getWorkspaceFolder(), () => {
185 appLauncher.setReactNativeVersions(versions);
186 const platform = <ExponentPlatform>this.createPlatform(appLauncher, PlatformType.Exponent, ExponentPlatform);
187 return platform.beforeStartPackager()
188 .then(() => {
189 return platform.startPackager();
190 })
191 .then(() => {
192 return platform.runApp();
193 });
194 });
195 });
196 });
197 });
198 }
199
200 public static showDevMenu(): Promise<void> {
201 return this.selectProject()
202 .then((appLauncher: AppLauncher) => {
203 const androidPlatform = <AndroidPlatform>this.createPlatform(appLauncher, PlatformType.Android, AndroidPlatform);
204 androidPlatform.showDevMenu()
205 .catch(() => { }); // Ignore any errors
206
207 if (process.platform === "darwin") {
208 const iosPlatform = <IOSPlatform>this.createPlatform(appLauncher, PlatformType.iOS, IOSPlatform);
209 iosPlatform.showDevMenu(appLauncher)
210 .catch(() => { }); // Ignore any errors
211 }
212 return Promise.resolve();
213 });
214 }
215
216 public static reloadApp(): Promise<void> {
217 return this.selectProject()
218 .then((appLauncher: AppLauncher) => {
219 const androidPlatform = <AndroidPlatform>this.createPlatform(appLauncher, PlatformType.Android, AndroidPlatform);
220 androidPlatform.reloadApp()
221 .catch(() => { }); // Ignore any errors
222
223 if (process.platform === "darwin") {
224 const iosPlatform = <IOSPlatform>this.createPlatform(appLauncher, PlatformType.iOS, IOSPlatform);
225 iosPlatform.reloadApp(appLauncher)
226 .catch(() => { }); // Ignore any errors
227 }
228 return Promise.resolve();
229 });
230 }
231
232 public static runElementInspector(): Promise<void> {
233 if (!CommandPaletteHandler.elementInspector) {
234 // Remove the following env variables to prevent running electron app in node mode.
235 // https://github.com/microsoft/vscode/issues/3011#issuecomment-184577502
236 let env = Object.assign({}, process.env);
237 delete env.ATOM_SHELL_INTERNAL_RUN_AS_NODE;
238 delete env.ELECTRON_RUN_AS_NODE;
239 let command = HostPlatform.getNpmCliCommand("react-devtools");
240 CommandPaletteHandler.elementInspector = spawn(command, [], {
241 env,
242 });
243 if (!CommandPaletteHandler.elementInspector.pid) {
244 CommandPaletteHandler.elementInspector = null;
245 return Promise.reject(ErrorHelper.getInternalError(InternalErrorCode.ReactDevtoolsIsNotInstalled));
246 }
247 CommandPaletteHandler.elementInspector.stdout.on("data", (data: string) => {
248 this.logger.info(data);
249 });
250 CommandPaletteHandler.elementInspector.stderr.on("data", (data: string) => {
251 this.logger.error(data);
252 });
253 CommandPaletteHandler.elementInspector.once("exit", () => {
254 CommandPaletteHandler.elementInspector = null;
255 });
256 } else {
257 this.logger.info(localize("AnotherElementInspectorAlreadyRun", "Another element inspector already run"));
258 }
259 return Promise.resolve();
260 }
261
262 public static stopElementInspector(): void {
263 return CommandPaletteHandler.elementInspector ? CommandPaletteHandler.elementInspector.kill() : void 0;
264 }
265
266 public static getPlatformByCommandName(commandName: string): string {
267 commandName = commandName.toLocaleLowerCase();
268
269 if (commandName.indexOf(PlatformType.Android) > -1) {
270 return PlatformType.Android;
271 }
272
273 if (commandName.indexOf(PlatformType.iOS) > -1) {
274 return PlatformType.iOS;
275 }
276
277 if (commandName.indexOf(PlatformType.Exponent) > -1) {
278 return PlatformType.Exponent;
279 }
280
281 return "";
282 }
283
284 public static startLogCatMonitor(): Promise<void> {
285 return this.selectProject()
286 .then(appLauncher => {
287 const adbHelper = new AdbHelper(appLauncher.getPackager().getProjectPath());
288 const avdManager = new AndroidEmulatorManager(adbHelper);
289 return avdManager.selectOnlineDevice()
290 .then((deviceId) => {
291 if (deviceId) {
292 LogCatMonitorManager.delMonitor(deviceId); // Stop previous logcat monitor if it's running
293 let logCatArguments = SettingsHelper.getLogCatFilteringArgs(appLauncher.getWorkspaceFolderUri());
294 // this.logCatMonitor can be mutated, so we store it locally too
295 let logCatMonitor = new LogCatMonitor(deviceId, adbHelper, logCatArguments);
296 LogCatMonitorManager.addMonitor(logCatMonitor);
297 logCatMonitor.start() // The LogCat will continue running forever, so we don't wait for it
298 .catch(error => this.logger.warning(localize("ErrorWhileMonitoringLogCat", "Error while monitoring LogCat"), error));
299 } else {
300 vscode.window.showErrorMessage(localize("OnlineAndroidDeviceNotFound", "Could not find a proper online Android device to start a LogCat monitor"));
301 }
302 });
303 });
304 }
305
306 public static stopLogCatMonitor(): Promise<void> {
307 return this.selectLogCatMonitor()
308 .then(monitor => {
309 LogCatMonitorManager.delMonitor(monitor.deviceId);
310 });
311 }
312
313 private static createPlatform(appLauncher: AppLauncher, platform: PlatformType.iOS | PlatformType.Android | PlatformType.Exponent, platformClass: typeof GeneralMobilePlatform, target?: TargetType): GeneralMobilePlatform {
314 const runOptions = CommandPaletteHandler.getRunOptions(appLauncher, platform, target);
315 return new platformClass(runOptions, {
316 packager: appLauncher.getPackager(),
317 });
318 }
319
320 private static runRestartPackagerCommandAndUpdateStatus(appLauncher: AppLauncher): Promise<void> {
321 return appLauncher.getPackager().restart(SettingsHelper.getPackagerPort(appLauncher.getWorkspaceFolderUri().fsPath));
322 }
323
324 /**
325 * Ensures that we are in a React Native project and then executes the operation
326 * Otherwise, displays an error message banner
327 * {operation} - a function that performs the expected operation
328 */
329 private static executeCommandInContext(rnCommand: string, workspaceFolder: vscode.WorkspaceFolder, operation: () => Promise<void>): Promise<void> {
330 const extProps = {
331 platform: {
332 value: CommandPaletteHandler.getPlatformByCommandName(rnCommand),
333 isPii: false,
334 },
335 };
336
337 return TelemetryHelper.generate("RNCommand", extProps, (generator) => {
338 generator.add("command", rnCommand, false);
339 const projectRoot = SettingsHelper.getReactNativeProjectRoot(workspaceFolder.uri.fsPath);
340 this.logger.debug(`Command palette: run project ${projectRoot} in context`);
341 return ReactNativeProjectHelper.isReactNativeProject(projectRoot)
342 .then(isRNProject => {
343 generator.add("isRNProject", isRNProject, false);
344 if (isRNProject) {
345 // Bring the log channel to focus
346 this.logger.setFocusOnLogChannel();
347
348 // Execute the operation
349 return operation();
350 } else {
351 vscode.window.showErrorMessage(`${projectRoot} workspace is not a React Native project.`);
352 return;
353 }
354 });
355 });
356 }
357
358 /**
359 * 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.
360 */
361 private static executePublishToExpHost(appLauncher: AppLauncher): Promise<boolean> {
362 CommandPaletteHandler.logger.info(localize("PublishingAppToExponentServer", "Publishing app to Expo server. This might take a moment."));
363 return this.loginToExponent(appLauncher)
364 .then(user => {
365 CommandPaletteHandler.logger.debug(`Publishing as ${user.username}...`);
366 return this.runExponent()
367 .then(() =>
368 XDL.publish(appLauncher.getWorkspaceFolderUri().fsPath))
369 .then(response => {
370 if (response.err || !response.url) {
371 return false;
372 }
373 const publishedOutput = localize("ExpoAppSuccessfullyPublishedTo", "Expo app successfully published to {0}", response.url);
374 CommandPaletteHandler.logger.info(publishedOutput);
375 vscode.window.showInformationMessage(publishedOutput);
376 return true;
377 });
378 });
379 }
380
381 private static loginToExponent(appLauncher: AppLauncher): Promise<XDL.IUser> {
382 return appLauncher.getExponentHelper().loginToExponent(
383 (message, password) => {
384 return new Promise((resolve, reject) => {
385 vscode.window.showInputBox({ placeHolder: message, password: password })
386 .then(login => {
387 resolve(login || "");
388 }, reject);
389 });
390 },
391 (message) => {
392 return new Promise((resolve, reject) => {
393 vscode.window.showInformationMessage(message)
394 .then(password => {
395 resolve(password || "");
396 }, reject);
397 });
398 }
399 )
400 .catch((err) => {
401 CommandPaletteHandler.logger.warning(localize("ExpoErrorOccuredMakeSureYouAreLoggedIn", "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."));
402 throw err;
403 });
404 }
405
406 private static selectProject(): Promise<AppLauncher> {
407 let keys = Object.keys(ProjectsStorage.projectsCache);
408 if (keys.length > 1) {
409 return new Promise((resolve, reject) => {
410 vscode.window.showQuickPick(keys)
411 .then((selected) => {
412 if (selected) {
413 this.logger.debug(`Command palette: selected project ${selected}`);
414 resolve(ProjectsStorage.projectsCache[selected]);
415 }
416 }, reject);
417 });
418 } else if (keys.length === 1) {
419 this.logger.debug(`Command palette: once project ${keys[0]}`);
420 return Promise.resolve(ProjectsStorage.projectsCache[keys[0]]);
421 } else {
422 return Promise.reject(ErrorHelper.getInternalError(InternalErrorCode.WorkspaceNotFound, "Current workspace does not contain React Native projects."));
423 }
424 }
425
426 private static selectLogCatMonitor(): Promise<LogCatMonitor> {
427 let keys = Object.keys(LogCatMonitorManager.logCatMonitorsCache);
428 if (keys.length > 1) {
429 return new Promise((resolve, reject) => {
430 vscode.window.showQuickPick(keys)
431 .then((selected) => {
432 if (selected) {
433 this.logger.debug(`Command palette: selected LogCat monitor ${selected}`);
434 resolve(LogCatMonitorManager.logCatMonitorsCache[selected]);
435 }
436 }, reject);
437 });
438 } else if (keys.length === 1) {
439 this.logger.debug(`Command palette: once LogCat monitor ${keys[0]}`);
440 return Promise.resolve(LogCatMonitorManager.logCatMonitorsCache[keys[0]]);
441 } else {
442 return Promise.reject(ErrorHelper.getInternalError(InternalErrorCode.AndroidCouldNotFindActiveLogCatMonitor));
443 }
444 }
445
446 private static getRunOptions(appLauncher: AppLauncher, platform: PlatformType.iOS | PlatformType.Android | PlatformType.Exponent, target: TargetType = "simulator"): IAndroidRunOptions | IIOSRunOptions {
447 const packagerPort = SettingsHelper.getPackagerPort(appLauncher.getWorkspaceFolderUri().fsPath);
448 const runArgs = SettingsHelper.getRunArgs(platform, target, appLauncher.getWorkspaceFolderUri());
449 const envArgs = SettingsHelper.getEnvArgs(platform, target, appLauncher.getWorkspaceFolderUri());
450 const envFile = SettingsHelper.getEnvFile(platform, target, appLauncher.getWorkspaceFolderUri());
451 const projectRoot = SettingsHelper.getReactNativeProjectRoot(appLauncher.getWorkspaceFolderUri().fsPath);
452 const runOptions: IAndroidRunOptions | IIOSRunOptions = {
453 platform: platform,
454 workspaceRoot: appLauncher.getWorkspaceFolderUri().fsPath,
455 projectRoot: projectRoot,
456 packagerPort: packagerPort,
457 runArguments: runArgs,
458 env: envArgs,
459 envFile: envFile,
460 reactNativeVersions: appLauncher.getReactNativeVersions() || { reactNativeVersion: "", reactNativeWindowsVersion: "", reactNativeMacOSVersion: "" },
461 };
462
463 if (platform === PlatformType.iOS && target === "device") {
464 runOptions.target = "device";
465 }
466
467 CommandExecutor.ReactNativeCommand = SettingsHelper.getReactNativeGlobalCommandName(appLauncher.getWorkspaceFolderUri());
468
469 return runOptions;
470 }
471}
472