microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
6a465861663d7c6f9433e89c6f6619df320dcef3

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

401lines · 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, PackagerRunAs} from "../common/packager";
10import {AndroidPlatform} from "./android/androidPlatform";
11import {IOSPlatform} from "./ios/iOSPlatform";
12import {PackagerStatus} from "./packagerStatusIndicator";
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 } from "./launchArgs";
20import { AppCenterCommandPalleteHandler } from "./appcenter/appCenterCommandPalleteHandler";
21import { AppCenterCommandType } from "./appcenter/appCenterConstants";
22import { AppCenterExtensionManager } from "./appcenter/appCenterExtensionManager";
23
24interface IReactNativeStuff {
25 packager: Packager;
26 exponentHelper: ExponentHelper;
27 reactDirManager: ReactDirManager;
28 extensionServer: ExtensionServer;
29 appCenterManager: AppCenterExtensionManager; // Actually not an RN Stuff, but this is RN only extension so no other than RN stuff could exist at all, yeah?
30}
31
32interface IReactNativeProject extends IReactNativeStuff {
33 workspaceFolder: vscode.WorkspaceFolder;
34}
35
36export class CommandPaletteHandler {
37 private static projectsCache: {[key: string]: IReactNativeProject} = {};
38 private static logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
39
40 // Use this class to further cmd exec delegation and not to pollute this class with AppCenter logic
41 private static appCenterCommandPalleteHandler: AppCenterCommandPalleteHandler;
42
43 public static addFolder(workspaceFolder: vscode.WorkspaceFolder, stuff: IReactNativeStuff): void {
44 this.projectsCache[workspaceFolder.uri.fsPath] = {
45 ...stuff,
46 workspaceFolder,
47 };
48 }
49
50 public static getFolder(workspaceFolder: vscode.WorkspaceFolder): IReactNativeProject {
51 return this.projectsCache[workspaceFolder.uri.fsPath];
52 }
53
54 public static delFolder(workspaceFolder: vscode.WorkspaceFolder): void {
55 delete this.projectsCache[workspaceFolder.uri.fsPath];
56 }
57
58 /**
59 * Starts the React Native packager
60 */
61 public static startPackager(): Q.Promise<void> {
62 return this.selectProject()
63 .then((project: IReactNativeProject) => {
64 return this.executeCommandInContext("startPackager", project.workspaceFolder, () =>
65 project.packager.isRunning()
66 .then((running) => {
67 return running ? project.packager.stop() : Q.resolve(void 0);
68 })
69 )
70 .then(() => this.runStartPackagerCommandAndUpdateStatus(project));
71 });
72 }
73
74 /**
75 * Starts the Exponent packager
76 */
77 public static startExponentPackager(): Q.Promise<void> {
78 return this.selectProject()
79 .then((project: IReactNativeProject) => {
80 return this.executeCommandInContext("startExponentPackager", project.workspaceFolder, () =>
81 project.packager.isRunning()
82 .then((running) => {
83 return running ? project.packager.stop() : Q.resolve(void 0);
84 })
85 ).then(() =>
86 project.exponentHelper.configureExponentEnvironment()
87 ).then(() => this.runStartPackagerCommandAndUpdateStatus(project, PackagerRunAs.EXPONENT));
88 });
89 }
90
91 /**
92 * Kills the React Native packager invoked by the extension's packager
93 */
94 public static stopPackager(): Q.Promise<void> {
95 return this.selectProject()
96 .then((project: IReactNativeProject) => {
97 return this.executeCommandInContext("stopPackager", project.workspaceFolder, () => project.packager.stop())
98 .then(() => project.packager.statusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STOPPED));
99 });
100 }
101
102 public static stopAllPackagers(): Q.Promise<void> {
103 let keys = Object.keys(this.projectsCache);
104 let promises: Q.Promise<void>[] = [];
105 keys.forEach((key) => {
106 let project = this.projectsCache[key];
107 promises.push(this.executeCommandInContext("stopPackager", project.workspaceFolder, () => project.packager.stop())
108 .then(() => project.packager.statusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STOPPED)));
109 });
110
111 return Q.all(promises).then(() => {});
112 }
113
114 /**
115 * Restarts the React Native packager
116 */
117 public static restartPackager(): Q.Promise<void> {
118 return this.selectProject()
119 .then((project: IReactNativeProject) => {
120 return this.executeCommandInContext("restartPackager", project.workspaceFolder, () =>
121 this.runRestartPackagerCommandAndUpdateStatus(project));
122 });
123 }
124
125 /**
126 * Execute command to publish to exponent host.
127 */
128 public static publishToExpHost(): Q.Promise<void> {
129 return this.selectProject()
130 .then((project: IReactNativeProject) => {
131 return this.executeCommandInContext("publishToExpHost", project.workspaceFolder, () => {
132 return this.executePublishToExpHost(project).then((didPublish) => {
133 if (!didPublish) {
134 CommandPaletteHandler.logger.warning("Publishing was unsuccessful. Please make sure you are logged in Exponent and your project is a valid Exponentjs project");
135 }
136 });
137 });
138 });
139 }
140
141 /**
142 * Executes the 'react-native run-android' command
143 */
144 public static runAndroid(target: "device" | "simulator" = "simulator"): Q.Promise<void> {
145 TargetPlatformHelper.checkTargetPlatformSupport("android");
146 return this.selectProject()
147 .then((project: IReactNativeProject) => {
148 return this.executeCommandInContext("runAndroid", project.workspaceFolder, () => this.executeWithPackagerRunning(project, () => {
149 const packagerPort = SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath);
150 const runArgs = SettingsHelper.getRunArgs("android", target, project.workspaceFolder.uri);
151 const envArgs = SettingsHelper.getEnvArgs("android", target, project.workspaceFolder.uri);
152 const envFile = SettingsHelper.getEnvFile("android", target, project.workspaceFolder.uri);
153 const projectRoot = SettingsHelper.getReactNativeProjectRoot(project.workspaceFolder.uri.fsPath);
154 const runOptions: IAndroidRunOptions = {
155 platform: "android",
156 workspaceRoot: project.workspaceFolder.uri.fsPath,
157 projectRoot: projectRoot,
158 packagerPort: packagerPort,
159 runArguments: runArgs,
160 env: envArgs,
161 envFile: envFile,
162 };
163 const platform = new AndroidPlatform(runOptions, {
164 packager: project.packager,
165 });
166 return platform.runApp(/*shouldLaunchInAllDevices*/true)
167 .then(() => {
168 return platform.disableJSDebuggingMode();
169 });
170 }));
171 });
172 }
173
174 /**
175 * Executes the 'react-native run-ios' command
176 */
177 public static runIos(target: "device" | "simulator" = "simulator"): Q.Promise<void> {
178 TargetPlatformHelper.checkTargetPlatformSupport("ios");
179 return this.selectProject()
180 .then((project: IReactNativeProject) => {
181 return this.executeCommandInContext("runIos", project.workspaceFolder, () => this.executeWithPackagerRunning(project, () => {
182 const packagerPort = SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath);
183 const runArgs = SettingsHelper.getRunArgs("ios", target, project.workspaceFolder.uri);
184 const envArgs = SettingsHelper.getEnvArgs("ios", target, project.workspaceFolder.uri);
185 const envFile = SettingsHelper.getEnvFile("ios", target, project.workspaceFolder.uri);
186 const platform = new IOSPlatform({
187 platform: "ios",
188 workspaceRoot: project.workspaceFolder.uri.fsPath,
189 projectRoot: project.workspaceFolder.uri.fsPath,
190 packagerPort: packagerPort,
191 runArguments: runArgs,
192 env: envArgs,
193 envFile: envFile,
194 }, { packager: project.packager });
195
196 // Set the Debugging setting to disabled, because in iOS it's persisted across runs of the app
197 return platform.disableJSDebuggingMode()
198 .catch(() => { }) // If setting the debugging mode fails, we ignore the error and we run the run ios command anyways
199 .then(() => {
200 return platform.runApp();
201 });
202 }));
203 });
204 }
205
206 public static showDevMenu(): Q.Promise<void> {
207 return this.selectProject()
208 .then((project: IReactNativeProject) => {
209 AndroidPlatform.showDevMenu()
210 .catch(() => { }); // Ignore any errors
211 IOSPlatform.showDevMenu(project.workspaceFolder.uri.fsPath)
212 .catch(() => { }); // Ignore any errors
213 return Q.resolve(void 0);
214 });
215 }
216
217 public static reloadApp(): Q.Promise<void> {
218 return this.selectProject()
219 .then((project: IReactNativeProject) => {
220 AndroidPlatform.reloadApp()
221 .catch(() => { }); // Ignore any errors
222 IOSPlatform.reloadApp(project.workspaceFolder.uri.fsPath)
223 .catch(() => { }); // Ignore any errors
224 return Q.resolve(void 0);
225 });
226 }
227
228 public static appCenterLogin(): Q.Promise<void> {
229 return this.selectProject()
230 .then((project: IReactNativeProject) => {
231 return CommandPaletteHandler.getAppCenterCommandPalleteHandler().run(AppCenterCommandType.Login, project.appCenterManager);
232 });
233 }
234
235 public static appCenterLogout(): Q.Promise<void> {
236 return this.selectProject()
237 .then((project: IReactNativeProject) => {
238 return CommandPaletteHandler.getAppCenterCommandPalleteHandler().run(AppCenterCommandType.Logout, project.appCenterManager);
239 });
240 }
241
242 public static appCenterWhoAmI(): Q.Promise<void> {
243 return this.selectProject()
244 .then((project: IReactNativeProject) => {
245 return CommandPaletteHandler.getAppCenterCommandPalleteHandler().run(AppCenterCommandType.Whoami, project.appCenterManager);
246 });
247 }
248
249 public static appCenterSetCurrentApp(): Q.Promise<void> {
250 return this.selectProject()
251 .then((project: IReactNativeProject) => {
252 return CommandPaletteHandler.getAppCenterCommandPalleteHandler().run(AppCenterCommandType.SetCurrentApp, project.appCenterManager);
253 });
254 }
255
256 public static appCenterGetCurrentApp(): Q.Promise<void> {
257 return this.selectProject()
258 .then((project: IReactNativeProject) => {
259 return CommandPaletteHandler.getAppCenterCommandPalleteHandler().run(AppCenterCommandType.GetCurrentApp, project.appCenterManager);
260 });
261 }
262
263 public static appCenterCodePushReleaseReact(): Q.Promise<void> {
264 return this.selectProject()
265 .then((project: IReactNativeProject) => {
266 return CommandPaletteHandler.getAppCenterCommandPalleteHandler().run(AppCenterCommandType.CodePushReleaseReact, project.appCenterManager);
267 });
268 }
269
270 private static runRestartPackagerCommandAndUpdateStatus(project: IReactNativeProject): Q.Promise<void> {
271 return project.packager.restart(SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath))
272 .then(() => project.packager.statusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED));
273 }
274
275 /**
276 * Helper method to run packager and update appropriate configurations
277 */
278 private static runStartPackagerCommandAndUpdateStatus(project: IReactNativeProject, startAs: PackagerRunAs = PackagerRunAs.REACT_NATIVE): Q.Promise<any> {
279 if (startAs === PackagerRunAs.EXPONENT) {
280 return this.loginToExponent(project)
281 .then(() =>
282 project.packager.startAsExponent()
283 ).then(exponentUrl => {
284 project.packager.statusIndicator.updatePackagerStatus(PackagerStatus.EXPONENT_PACKAGER_STARTED);
285 CommandPaletteHandler.logger.info("Application is running on Exponent.");
286 const exponentOutput = `Open your exponent app at ${exponentUrl}`;
287 CommandPaletteHandler.logger.info(exponentOutput);
288 vscode.commands.executeCommand("vscode.previewHtml", vscode.Uri.parse(exponentUrl), 1, "Expo QR code");
289 });
290 }
291 return project.packager.startAsReactNative()
292 .then(() => project.packager.statusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED));
293 }
294
295 /**
296 * Executes a lambda function after starting the packager
297 * {lambda} The lambda function to be executed
298 */
299 private static executeWithPackagerRunning(project: IReactNativeProject, lambda: () => Q.Promise<void>): Q.Promise<void> {
300 // Start the packager before executing the React-Native command
301 CommandPaletteHandler.logger.info("Attempting to start the React Native packager");
302 return this.runStartPackagerCommandAndUpdateStatus(project).then(lambda);
303 }
304
305 /**
306 * Ensures that we are in a React Native project and then executes the operation
307 * Otherwise, displays an error message banner
308 * {operation} - a function that performs the expected operation
309 */
310 private static executeCommandInContext(rnCommand: string, workspaceFolder: vscode.WorkspaceFolder, operation: () => Q.Promise<void>): Q.Promise<void> {
311 return TelemetryHelper.generate("RNCommand", (generator) => {
312 generator.add("command", rnCommand, false);
313 const projectRoot = SettingsHelper.getReactNativeProjectRoot(workspaceFolder.uri.fsPath);
314 return ReactNativeProjectHelper.isReactNativeProject(projectRoot).then(isRNProject => {
315 generator.add("isRNProject", isRNProject, false);
316 if (isRNProject) {
317 // Bring the log channel to focus
318 CommandPaletteHandler.logger.setFocusOnLogChannel();
319
320 // Execute the operation
321 return operation();
322 } else {
323 vscode.window.showErrorMessage("Current workspace is not a React Native project.");
324 return;
325 }
326 });
327 });
328 }
329
330 /**
331 * 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.
332 */
333 private static executePublishToExpHost(project: IReactNativeProject): Q.Promise<boolean> {
334 CommandPaletteHandler.logger.info("Publishing app to Exponent server. This might take a moment.");
335 return this.loginToExponent(project)
336 .then(user => {
337 CommandPaletteHandler.logger.debug(`Publishing as ${user.username}...`);
338 return this.startExponentPackager()
339 .then(() =>
340 XDL.publish(project.workspaceFolder.uri.fsPath))
341 .then(response => {
342 if (response.err || !response.url) {
343 return false;
344 }
345 const publishedOutput = `App successfully published to ${response.url}`;
346 CommandPaletteHandler.logger.info(publishedOutput);
347 vscode.window.showInformationMessage(publishedOutput);
348 return true;
349 });
350 }).catch(() => {
351 CommandPaletteHandler.logger.warning("An error has occured. Please make sure you are logged in to exponent, your project is setup correctly for publishing and your packager is running as exponent.");
352 return false;
353 });
354 }
355
356 private static loginToExponent(project: IReactNativeProject): Q.Promise<XDL.IUser> {
357 return project.exponentHelper.loginToExponent(
358 (message, password) => {
359 return Q.Promise((resolve, reject) => {
360 vscode.window.showInputBox({ placeHolder: message, password: password })
361 .then(login => {
362 resolve(login || "");
363 }, reject);
364 });
365 },
366 (message) => {
367 return Q.Promise((resolve, reject) => {
368 vscode.window.showInformationMessage(message)
369 .then(password => {
370 resolve(password || "");
371 }, reject);
372 });
373 }
374 );
375 }
376
377 private static getAppCenterCommandPalleteHandler(): AppCenterCommandPalleteHandler {
378 if (!CommandPaletteHandler.appCenterCommandPalleteHandler) {
379 CommandPaletteHandler.appCenterCommandPalleteHandler = new AppCenterCommandPalleteHandler(CommandPaletteHandler.logger);
380 }
381 return CommandPaletteHandler.appCenterCommandPalleteHandler;
382 }
383
384 private static selectProject(): Q.Promise<IReactNativeProject> {
385 let keys = Object.keys(this.projectsCache);
386 if (keys.length > 1) {
387 return Q.Promise((resolve, reject) => {
388 vscode.window.showQuickPick(keys)
389 .then((selected) => {
390 if (selected) {
391 resolve(this.projectsCache[selected]);
392 }
393 }, reject);
394 });
395 } else if (keys.length === 1) {
396 return Q.resolve(this.projectsCache[keys[0]]);
397 } else {
398 return Q.reject();
399 }
400 }
401}
402