microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
36e9730fc3e5568605829ee7ab2d402bca4c18ac

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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