microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
b7ee5fed48782f7c7e7a33b44b7e94cac8d06b21

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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