microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
cf6dd6b9348f1c8adb045db2bfda5432796bfe77

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 {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 const iosPlatform = <IOSPlatform>this.createPlatform(project, "ios", IOSPlatform);
201 iosPlatform.showDevMenu()
202 .catch(() => { }); // Ignore any errors
203 return Q.resolve(void 0);
204 });
205 }
206
207 public static reloadApp(): Q.Promise<void> {
208 return this.selectProject()
209 .then((project: IReactNativeProject) => {
210 const androidPlatform = <AndroidPlatform>this.createPlatform(project, "android", AndroidPlatform);
211 androidPlatform.reloadApp()
212 .catch(() => { }); // Ignore any errors
213 const iosPlatform = <IOSPlatform>this.createPlatform(project, "ios", IOSPlatform);
214 iosPlatform.reloadApp()
215 .catch(() => { }); // Ignore any errors
216 return Q.resolve(void 0);
217 });
218 }
219
220 public static runElementInspector(): Q.Promise<void> {
221 if (!CommandPaletteHandler.elementInspector) {
222 // Remove the following env variables to prevent running electron app in node mode.
223 // https://github.com/Microsoft/vscode/issues/3011#issuecomment-184577502
224 let env = Object.assign({}, process.env);
225 delete env.ATOM_SHELL_INTERNAL_RUN_AS_NODE;
226 delete env.ELECTRON_RUN_AS_NODE;
227 let command = HostPlatform.getNpmCliCommand("react-devtools");
228 CommandPaletteHandler.elementInspector = spawn(command, [], {
229 env,
230 });
231 if (!CommandPaletteHandler.elementInspector.pid) {
232 CommandPaletteHandler.elementInspector = null;
233 return Q.reject(ErrorHelper.getInternalError(InternalErrorCode.ReactDevtoolsIsNotInstalled));
234 }
235 CommandPaletteHandler.elementInspector.stdout.on("data", (data: string) => {
236 this.logger.info(data);
237 });
238 CommandPaletteHandler.elementInspector.stderr.on("data", (data: string) => {
239 this.logger.error(data);
240 });
241 CommandPaletteHandler.elementInspector.once("exit", () => {
242 CommandPaletteHandler.elementInspector = null;
243 });
244 } else {
245 this.logger.info(localize("AnotherElementInspectorAlreadyRun", "Another element inspector already run"));
246 }
247 return Q(void 0);
248 }
249
250 public static stopElementInspector(): void {
251 return CommandPaletteHandler.elementInspector ? CommandPaletteHandler.elementInspector.kill() : void 0;
252 }
253
254 public static getPlatformByCommandName(commandName: string): string {
255 commandName = commandName.toLocaleLowerCase();
256
257 if (commandName.indexOf("android") > -1) {
258 return "android";
259 }
260
261 if (commandName.indexOf("ios") > -1) {
262 return "ios";
263 }
264
265 if (commandName.indexOf("exponent") > -1) {
266 return "exponent";
267 }
268
269 return "";
270 }
271
272 private static createPlatform(project: IReactNativeProject, platform: "ios" | "android" | "exponent", platformClass: typeof GeneralMobilePlatform, target?: TargetType): GeneralMobilePlatform {
273 const runOptions = CommandPaletteHandler.getRunOptions(project, platform, target);
274 return new platformClass(runOptions, {
275 packager: project.packager,
276 });
277 }
278
279 private static runRestartPackagerCommandAndUpdateStatus(project: IReactNativeProject): Q.Promise<void> {
280 return project.packager.restart(SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath));
281 }
282
283 /**
284 * Ensures that we are in a React Native project and then executes the operation
285 * Otherwise, displays an error message banner
286 * {operation} - a function that performs the expected operation
287 */
288 private static executeCommandInContext(rnCommand: string, workspaceFolder: vscode.WorkspaceFolder, operation: () => Q.Promise<void>): Q.Promise<void> {
289 const extProps = {
290 platform: {
291 value: CommandPaletteHandler.getPlatformByCommandName(rnCommand),
292 isPii: false,
293 },
294 };
295
296 return TelemetryHelper.generate("RNCommand", extProps, (generator) => {
297 generator.add("command", rnCommand, false);
298 const projectRoot = SettingsHelper.getReactNativeProjectRoot(workspaceFolder.uri.fsPath);
299 this.logger.debug(`Command palette: run project ${projectRoot} in context`);
300 return ReactNativeProjectHelper.isReactNativeProject(projectRoot)
301 .then(isRNProject => {
302 generator.add("isRNProject", isRNProject, false);
303 if (isRNProject) {
304 // Bring the log channel to focus
305 this.logger.setFocusOnLogChannel();
306
307 // Execute the operation
308 return operation();
309 } else {
310 vscode.window.showErrorMessage(`${projectRoot} workspace is not a React Native project.`);
311 return;
312 }
313 });
314 });
315 }
316
317 /**
318 * 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.
319 */
320 private static executePublishToExpHost(project: IReactNativeProject): Q.Promise<boolean> {
321 CommandPaletteHandler.logger.info(localize("PublishingAppToExponentServer", "Publishing app to Expo server. This might take a moment."));
322 return this.loginToExponent(project)
323 .then(user => {
324 CommandPaletteHandler.logger.debug(`Publishing as ${user.username}...`);
325 return this.runExponent()
326 .then(() =>
327 XDL.publish(project.workspaceFolder.uri.fsPath))
328 .then(response => {
329 if (response.err || !response.url) {
330 return false;
331 }
332 const publishedOutput = localize("ExpoAppSuccessfullyPublishedTo", "Expo app successfully published to {0}", response.url);
333 CommandPaletteHandler.logger.info(publishedOutput);
334 vscode.window.showInformationMessage(publishedOutput);
335 return true;
336 });
337 });
338 }
339
340 private static loginToExponent(project: IReactNativeProject): Q.Promise<XDL.IUser> {
341 return project.exponentHelper.loginToExponent(
342 (message, password) => {
343 return Q.Promise((resolve, reject) => {
344 vscode.window.showInputBox({ placeHolder: message, password: password })
345 .then(login => {
346 resolve(login || "");
347 }, reject);
348 });
349 },
350 (message) => {
351 return Q.Promise((resolve, reject) => {
352 vscode.window.showInformationMessage(message)
353 .then(password => {
354 resolve(password || "");
355 }, reject);
356 });
357 }
358 )
359 .catch((err) => {
360 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."));
361 throw err;
362 });
363 }
364
365 private static selectProject(): Q.Promise<IReactNativeProject> {
366 let keys = Object.keys(this.projectsCache);
367 if (keys.length > 1) {
368 return Q.Promise((resolve, reject) => {
369 vscode.window.showQuickPick(keys)
370 .then((selected) => {
371 if (selected) {
372 this.logger.debug(`Command palette: selected project ${selected}`);
373 resolve(this.projectsCache[selected]);
374 }
375 }, reject);
376 });
377 } else if (keys.length === 1) {
378 this.logger.debug(`Command palette: once project ${keys[0]}`);
379 return Q.resolve(this.projectsCache[keys[0]]);
380 } else {
381 return Q.reject(ErrorHelper.getInternalError(InternalErrorCode.WorkspaceNotFound, "Current workspace does not contain React Native projects."));
382 }
383 }
384
385 private static getRunOptions(project: IReactNativeProject, platform: "ios" | "android" | "exponent", target: TargetType = "simulator"): IAndroidRunOptions | IIOSRunOptions {
386 const packagerPort = SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath);
387 const runArgs = SettingsHelper.getRunArgs(platform, target, project.workspaceFolder.uri);
388 const envArgs = SettingsHelper.getEnvArgs(platform, target, project.workspaceFolder.uri);
389 const envFile = SettingsHelper.getEnvFile(platform, target, project.workspaceFolder.uri);
390 const projectRoot = SettingsHelper.getReactNativeProjectRoot(project.workspaceFolder.uri.fsPath);
391 const runOptions: IAndroidRunOptions | IIOSRunOptions = {
392 platform: platform,
393 workspaceRoot: project.workspaceFolder.uri.fsPath,
394 projectRoot: projectRoot,
395 packagerPort: packagerPort,
396 runArguments: runArgs,
397 env: envArgs,
398 envFile: envFile,
399 };
400
401 return runOptions;
402 }
403}
404