microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.5.4

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

344lines · 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 envArgs = SettingsHelper.getEnvArgs("android", target, project.workspaceFolder.uri);
144 const envFile = SettingsHelper.getEnvFile("android", target, project.workspaceFolder.uri);
145 const projectRoot = SettingsHelper.getReactNativeProjectRoot(project.workspaceFolder.uri.fsPath);
146 const runOptions: IAndroidRunOptions = {
147 platform: "android",
148 workspaceRoot: project.workspaceFolder.uri.fsPath,
149 projectRoot: projectRoot,
150 packagerPort: packagerPort,
151 runArguments: runArgs,
152 env: envArgs,
153 envFile: envFile,
154 };
155 const platform = new AndroidPlatform(runOptions, {
156 packager: project.packager,
157 });
158 return platform.runApp(/*shouldLaunchInAllDevices*/true)
159 .then(() => {
160 return platform.disableJSDebuggingMode();
161 });
162 }));
163 });
164 }
165
166 /**
167 * Executes the 'react-native run-ios' command
168 */
169 public static runIos(target: "device" | "simulator" = "simulator"): Q.Promise<void> {
170 TargetPlatformHelper.checkTargetPlatformSupport("ios");
171 return this.selectProject()
172 .then((project: IReactNativeProject) => {
173 return this.executeCommandInContext("runIos", project.workspaceFolder, () => this.executeWithPackagerRunning(project, () => {
174 const packagerPort = SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath);
175 const runArgs = SettingsHelper.getRunArgs("ios", target, project.workspaceFolder.uri);
176 const envArgs = SettingsHelper.getEnvArgs("ios", target, project.workspaceFolder.uri);
177 const envFile = SettingsHelper.getEnvFile("ios", target, project.workspaceFolder.uri);
178 const platform = new IOSPlatform({
179 platform: "ios",
180 workspaceRoot: project.workspaceFolder.uri.fsPath,
181 projectRoot: project.workspaceFolder.uri.fsPath,
182 packagerPort: packagerPort,
183 runArguments: runArgs,
184 env: envArgs,
185 envFile: envFile,
186 }, { packager: project.packager });
187
188 // Set the Debugging setting to disabled, because in iOS it's persisted across runs of the app
189 return platform.disableJSDebuggingMode()
190 .catch(() => { }) // If setting the debugging mode fails, we ignore the error and we run the run ios command anyways
191 .then(() => {
192 return platform.runApp();
193 });
194 }));
195 });
196 }
197
198 public static showDevMenu(): Q.Promise<void> {
199 return this.selectProject()
200 .then((project: IReactNativeProject) => {
201 AndroidPlatform.showDevMenu()
202 .catch(() => { }); // Ignore any errors
203 IOSPlatform.showDevMenu(project.workspaceFolder.uri.fsPath)
204 .catch(() => { }); // Ignore any errors
205 return Q.resolve(void 0);
206 });
207 }
208
209 public static reloadApp(): Q.Promise<void> {
210 return this.selectProject()
211 .then((project: IReactNativeProject) => {
212 AndroidPlatform.reloadApp()
213 .catch(() => { }); // Ignore any errors
214 IOSPlatform.reloadApp(project.workspaceFolder.uri.fsPath)
215 .catch(() => { }); // Ignore any errors
216 return Q.resolve(void 0);
217 });
218 }
219
220 private static runRestartPackagerCommandAndUpdateStatus(project: IReactNativeProject): Q.Promise<void> {
221 return project.packager.restart(SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath))
222 .then(() => project.packager.statusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED));
223 }
224
225 /**
226 * Helper method to run packager and update appropriate configurations
227 */
228 private static runStartPackagerCommandAndUpdateStatus(project: IReactNativeProject, startAs: PackagerRunAs = PackagerRunAs.REACT_NATIVE): Q.Promise<any> {
229 if (startAs === PackagerRunAs.EXPONENT) {
230 return this.loginToExponent(project)
231 .then(() =>
232 project.packager.startAsExponent()
233 ).then(exponentUrl => {
234 project.packager.statusIndicator.updatePackagerStatus(PackagerStatus.EXPONENT_PACKAGER_STARTED);
235 CommandPaletteHandler.logger.info("Application is running on Exponent.");
236 const exponentOutput = `Open your exponent app at ${exponentUrl}`;
237 CommandPaletteHandler.logger.info(exponentOutput);
238 vscode.commands.executeCommand("vscode.previewHtml", vscode.Uri.parse(exponentUrl), 1, "Expo QR code");
239 });
240 }
241 return project.packager.startAsReactNative()
242 .then(() => project.packager.statusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED));
243 }
244
245 /**
246 * Executes a lambda function after starting the packager
247 * {lambda} The lambda function to be executed
248 */
249 private static executeWithPackagerRunning(project: IReactNativeProject, lambda: () => Q.Promise<void>): Q.Promise<void> {
250 // Start the packager before executing the React-Native command
251 CommandPaletteHandler.logger.info("Attempting to start the React Native packager");
252 return this.runStartPackagerCommandAndUpdateStatus(project).then(lambda);
253 }
254
255 /**
256 * Ensures that we are in a React Native project and then executes the operation
257 * Otherwise, displays an error message banner
258 * {operation} - a function that performs the expected operation
259 */
260 private static executeCommandInContext(rnCommand: string, workspaceFolder: vscode.WorkspaceFolder, operation: () => Q.Promise<void>): Q.Promise<void> {
261 return TelemetryHelper.generate("RNCommand", (generator) => {
262 generator.add("command", rnCommand, false);
263 const projectRoot = SettingsHelper.getReactNativeProjectRoot(workspaceFolder.uri.fsPath);
264 return ReactNativeProjectHelper.isReactNativeProject(projectRoot).then(isRNProject => {
265 generator.add("isRNProject", isRNProject, false);
266 if (isRNProject) {
267 // Bring the log channel to focus
268 CommandPaletteHandler.logger.setFocusOnLogChannel();
269
270 // Execute the operation
271 return operation();
272 } else {
273 vscode.window.showErrorMessage("Current workspace is not a React Native project.");
274 return;
275 }
276 });
277 });
278 }
279
280 /**
281 * 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.
282 */
283 private static executePublishToExpHost(project: IReactNativeProject): Q.Promise<boolean> {
284 CommandPaletteHandler.logger.info("Publishing app to Exponent server. This might take a moment.");
285 return this.loginToExponent(project)
286 .then(user => {
287 CommandPaletteHandler.logger.debug(`Publishing as ${user.username}...`);
288 return this.startExponentPackager()
289 .then(() =>
290 XDL.publish(project.workspaceFolder.uri.fsPath))
291 .then(response => {
292 if (response.err || !response.url) {
293 return false;
294 }
295 const publishedOutput = `App successfully published to ${response.url}`;
296 CommandPaletteHandler.logger.info(publishedOutput);
297 vscode.window.showInformationMessage(publishedOutput);
298 return true;
299 });
300 }).catch(() => {
301 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.");
302 return false;
303 });
304 }
305
306 private static loginToExponent(project: IReactNativeProject): Q.Promise<XDL.IUser> {
307 return project.exponentHelper.loginToExponent(
308 (message, password) => {
309 return Q.Promise((resolve, reject) => {
310 vscode.window.showInputBox({ placeHolder: message, password: password })
311 .then(login => {
312 resolve(login || "");
313 }, reject);
314 });
315 },
316 (message) => {
317 return Q.Promise((resolve, reject) => {
318 vscode.window.showInformationMessage(message)
319 .then(password => {
320 resolve(password || "");
321 }, reject);
322 });
323 }
324 );
325 }
326
327 private static selectProject(): Q.Promise<IReactNativeProject> {
328 let keys = Object.keys(this.projectsCache);
329 if (keys.length > 1) {
330 return Q.Promise((resolve, reject) => {
331 vscode.window.showQuickPick(keys)
332 .then((selected) => {
333 if (selected) {
334 resolve(this.projectsCache[selected]);
335 }
336 }, reject);
337 });
338 } else if (keys.length === 1) {
339 return Q.resolve(this.projectsCache[keys[0]]);
340 } else {
341 return Q.reject();
342 }
343 }
344}
345