microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.5.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

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