microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
bb8693435b23e79ae6f266a128e4fd98f0f198d0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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