microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
b7451aef6bd5846d7333cd23872aaadd1d57136c

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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