microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
c036b0d343245c82886cd5906e37ff50b8b30d34

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 {TargetType, GeneralMobilePlatform} from "./generalMobilePlatform";
10import {AndroidPlatform} from "./android/androidPlatform";
11import {IOSPlatform} from "./ios/iOSPlatform";
12import {ProjectVersionHelper} from "../common/projectVersionHelper";
13import {ReactNativeProjectHelper} from "../common/reactNativeProjectHelper";
14import {TargetPlatformHelper} from "../common/targetPlatformHelper";
15import {TelemetryHelper} from "../common/telemetryHelper";
16import {ProjectsStorage} from "./projectsStorage";
17import {IAndroidRunOptions, IIOSRunOptions} from "./launchArgs";
18import {ExponentPlatform} from "./exponent/exponentPlatform";
19import {spawn, ChildProcess} from "child_process";
20import {HostPlatform} from "../common/hostPlatform";
21import {CommandExecutor} from "../common/commandExecutor";
22import * as nls from "vscode-nls";
23import {ErrorHelper} from "../common/error/errorHelper";
24import {InternalErrorCode} from "../common/error/internalErrorCode";
25import {AppLauncher} from "./appLauncher";
26nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
27const localize = nls.loadMessageBundle();
28
29export class CommandPaletteHandler {
30 public static elementInspector: ChildProcess | null;
31 private static logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
32
33 /**
34 * Starts the React Native packager
35 */
36 public static startPackager(): Q.Promise<void> {
37 return this.selectProject()
38 .then((appLauncher: AppLauncher) => {
39 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
40 .then(versions => {
41 return this.executeCommandInContext("startPackager", appLauncher.getWorkspaceFolder(), () => {
42 return appLauncher.getPackager().isRunning()
43 .then((running) => {
44 return running ? appLauncher.getPackager().stop() : Q.resolve(void 0);
45 });
46 })
47 .then(() => appLauncher.getPackager().start());
48 });
49 });
50 }
51
52 /**
53 * Kills the React Native packager invoked by the extension's packager
54 */
55 public static stopPackager(): Q.Promise<void> {
56 return this.selectProject()
57 .then((appLauncher: AppLauncher) => {
58 return this.executeCommandInContext("stopPackager", appLauncher.getWorkspaceFolder(), () => appLauncher.getPackager().stop());
59 });
60 }
61
62 public static stopAllPackagers(): Q.Promise<void> {
63 let keys = Object.keys(ProjectsStorage.projectsCache);
64 let promises: Q.Promise<void>[] = [];
65 keys.forEach((key) => {
66 let appLauncher = ProjectsStorage.projectsCache[key];
67 promises.push(this.executeCommandInContext("stopPackager", appLauncher.getWorkspaceFolder(), () => appLauncher.getPackager().stop()));
68 });
69
70 return Q.all(promises).then(() => {});
71 }
72
73 /**
74 * Restarts the React Native packager
75 */
76 public static restartPackager(): Q.Promise<void> {
77 return this.selectProject()
78 .then((appLauncher: AppLauncher) => {
79 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
80 .then(versions => {
81 return this.executeCommandInContext("restartPackager", appLauncher.getWorkspaceFolder(), () =>
82 this.runRestartPackagerCommandAndUpdateStatus(appLauncher));
83 });
84 });
85 }
86
87 /**
88 * Execute command to publish to exponent host.
89 */
90 public static publishToExpHost(): Q.Promise<void> {
91 return this.selectProject()
92 .then((appLauncher: AppLauncher) => {
93 return this.executeCommandInContext("publishToExpHost", appLauncher.getWorkspaceFolder(), () => {
94 return this.executePublishToExpHost(appLauncher).then((didPublish) => {
95 if (!didPublish) {
96 CommandPaletteHandler.logger.warning(localize("ExponentPublishingWasUnsuccessfulMakeSureYoureLoggedInToExpo", "Publishing was unsuccessful. Please make sure you are logged in Expo and your project is a valid Expo project"));
97 }
98 });
99 });
100 });
101 }
102
103 /**
104 * Executes the 'react-native run-android' command
105 */
106 public static runAndroid(target: TargetType = "simulator"): Q.Promise<void> {
107 return this.selectProject()
108 .then((appLauncher: AppLauncher) => {
109 TargetPlatformHelper.checkTargetPlatformSupport("android");
110 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
111 .then(versions => {
112 appLauncher.setReactNativeVersions(versions);
113 return this.executeCommandInContext("runAndroid", appLauncher.getWorkspaceFolder(), () => {
114 const platform = <AndroidPlatform>this.createPlatform(appLauncher, "android", AndroidPlatform, target);
115 return platform.beforeStartPackager()
116 .then(() => {
117 return platform.startPackager();
118 })
119 .then(() => {
120 return platform.runApp(/*shouldLaunchInAllDevices*/true);
121 })
122 .then(() => {
123 return platform.disableJSDebuggingMode();
124 });
125 });
126 });
127 });
128 }
129
130 /**
131 * Executes the 'react-native run-ios' command
132 */
133 public static runIos(target: TargetType = "simulator"): Q.Promise<void> {
134 return this.selectProject()
135 .then((appLauncher: AppLauncher) => {
136 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
137 .then(versions => {
138 appLauncher.setReactNativeVersions(versions);
139 TargetPlatformHelper.checkTargetPlatformSupport("ios");
140 return this.executeCommandInContext("runIos", appLauncher.getWorkspaceFolder(), () => {
141 const platform = <IOSPlatform>this.createPlatform(appLauncher, "ios", IOSPlatform, target);
142 return platform.beforeStartPackager()
143 .then(() => {
144 return platform.startPackager();
145 })
146 .then(() => {
147 // Set the Debugging setting to disabled, because in iOS it's persisted across runs of the app
148 return platform.disableJSDebuggingMode();
149 })
150 .catch(() => { }) // If setting the debugging mode fails, we ignore the error and we run the run ios command anyways
151 .then(() => {
152 return platform.runApp();
153 });
154 });
155 });
156 });
157 }
158
159 /**
160 * Starts the Exponent packager
161 */
162 public static runExponent(): Q.Promise<void> {
163 return this.selectProject()
164 .then((appLauncher: AppLauncher) => {
165 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(appLauncher.getPackager().getProjectPath())
166 .then(versions => {
167 return this.loginToExponent(appLauncher)
168 .then(() => {
169 return this.executeCommandInContext("runExponent", appLauncher.getWorkspaceFolder(), () => {
170 appLauncher.setReactNativeVersions(versions);
171 const platform = <ExponentPlatform>this.createPlatform(appLauncher, "exponent", ExponentPlatform);
172 return platform.beforeStartPackager()
173 .then(() => {
174 return platform.startPackager();
175 })
176 .then(() => {
177 return platform.runApp();
178 });
179 });
180 });
181 });
182 });
183 }
184
185 public static showDevMenu(): Q.Promise<void> {
186 return this.selectProject()
187 .then((appLauncher: AppLauncher) => {
188 const androidPlatform = <AndroidPlatform>this.createPlatform(appLauncher, "android", AndroidPlatform);
189 androidPlatform.showDevMenu()
190 .catch(() => { }); // Ignore any errors
191
192 if (process.platform === "darwin") {
193 const iosPlatform = <IOSPlatform>this.createPlatform(appLauncher, "ios", IOSPlatform);
194 iosPlatform.showDevMenu(appLauncher)
195 .catch(() => { }); // Ignore any errors
196 }
197 return Q.resolve(void 0);
198 });
199 }
200
201 public static reloadApp(): Q.Promise<void> {
202 return this.selectProject()
203 .then((appLauncher: AppLauncher) => {
204 const androidPlatform = <AndroidPlatform>this.createPlatform(appLauncher, "android", AndroidPlatform);
205 androidPlatform.reloadApp()
206 .catch(() => { }); // Ignore any errors
207
208 if (process.platform === "darwin") {
209 const iosPlatform = <IOSPlatform>this.createPlatform(appLauncher, "ios", IOSPlatform);
210 iosPlatform.reloadApp(appLauncher)
211 .catch(() => { }); // Ignore any errors
212 }
213 return Q.resolve(void 0);
214 });
215 }
216
217 public static runElementInspector(): Q.Promise<void> {
218 if (!CommandPaletteHandler.elementInspector) {
219 // Remove the following env variables to prevent running electron app in node mode.
220 // https://github.com/Microsoft/vscode/issues/3011#issuecomment-184577502
221 let env = Object.assign({}, process.env);
222 delete env.ATOM_SHELL_INTERNAL_RUN_AS_NODE;
223 delete env.ELECTRON_RUN_AS_NODE;
224 let command = HostPlatform.getNpmCliCommand("react-devtools");
225 CommandPaletteHandler.elementInspector = spawn(command, [], {
226 env,
227 });
228 if (!CommandPaletteHandler.elementInspector.pid) {
229 CommandPaletteHandler.elementInspector = null;
230 return Q.reject(ErrorHelper.getInternalError(InternalErrorCode.ReactDevtoolsIsNotInstalled));
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(localize("AnotherElementInspectorAlreadyRun", "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(appLauncher: AppLauncher, platform: "ios" | "android" | "exponent", platformClass: typeof GeneralMobilePlatform, target?: TargetType): GeneralMobilePlatform {
270 const runOptions = CommandPaletteHandler.getRunOptions(appLauncher, platform, target);
271 return new platformClass(runOptions, {
272 packager: appLauncher.getPackager(),
273 });
274 }
275
276 private static runRestartPackagerCommandAndUpdateStatus(appLauncher: AppLauncher): Q.Promise<void> {
277 return appLauncher.getPackager().restart(SettingsHelper.getPackagerPort(appLauncher.getWorkspaceFolderUri().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(appLauncher: AppLauncher): Q.Promise<boolean> {
318 CommandPaletteHandler.logger.info(localize("PublishingAppToExponentServer", "Publishing app to Expo server. This might take a moment."));
319 return this.loginToExponent(appLauncher)
320 .then(user => {
321 CommandPaletteHandler.logger.debug(`Publishing as ${user.username}...`);
322 return this.runExponent()
323 .then(() =>
324 XDL.publish(appLauncher.getWorkspaceFolderUri().fsPath))
325 .then(response => {
326 if (response.err || !response.url) {
327 return false;
328 }
329 const publishedOutput = localize("ExpoAppSuccessfullyPublishedTo", "Expo app successfully published to {0}", response.url);
330 CommandPaletteHandler.logger.info(publishedOutput);
331 vscode.window.showInformationMessage(publishedOutput);
332 return true;
333 });
334 });
335 }
336
337 private static loginToExponent(appLauncher: AppLauncher): Q.Promise<XDL.IUser> {
338 return appLauncher.getExponentHelper().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(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."));
358 throw err;
359 });
360 }
361
362 private static selectProject(): Q.Promise<AppLauncher> {
363 let keys = Object.keys(ProjectsStorage.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(ProjectsStorage.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(ProjectsStorage.projectsCache[keys[0]]);
377 } else {
378 return Q.reject(ErrorHelper.getInternalError(InternalErrorCode.WorkspaceNotFound, "Current workspace does not contain React Native projects."));
379 }
380 }
381
382 private static getRunOptions(appLauncher: AppLauncher, platform: "ios" | "android" | "exponent", target: TargetType = "simulator"): IAndroidRunOptions | IIOSRunOptions {
383 const packagerPort = SettingsHelper.getPackagerPort(appLauncher.getWorkspaceFolderUri().fsPath);
384 const runArgs = SettingsHelper.getRunArgs(platform, target, appLauncher.getWorkspaceFolderUri());
385 const envArgs = SettingsHelper.getEnvArgs(platform, target, appLauncher.getWorkspaceFolderUri());
386 const envFile = SettingsHelper.getEnvFile(platform, target, appLauncher.getWorkspaceFolderUri());
387 const projectRoot = SettingsHelper.getReactNativeProjectRoot(appLauncher.getWorkspaceFolderUri().fsPath);
388 const runOptions: IAndroidRunOptions | IIOSRunOptions = {
389 platform: platform,
390 workspaceRoot: appLauncher.getWorkspaceFolderUri().fsPath,
391 projectRoot: projectRoot,
392 packagerPort: packagerPort,
393 runArguments: runArgs,
394 env: envArgs,
395 envFile: envFile,
396 reactNativeVersions: appLauncher.getReactNativeVersions() || {reactNativeVersion: "", reactNativeWindowsVersion: ""},
397 };
398
399 CommandExecutor.ReactNativeCommand = SettingsHelper.getReactNativeGlobalCommandName(appLauncher.getWorkspaceFolderUri());
400
401 return runOptions;
402 }
403}
404