microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8b42d59fede8b87d9c60d394184c0413ae01c11d

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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