microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.13.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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