microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e67ace8aab1da5ee01f5a9eec52e406501d301e4

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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