microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.3.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/appLauncher.ts

480lines · 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 { Packager } from "../common/packager";
6import { RNPackageVersions } from "../common/projectVersionHelper";
7import { ExponentHelper } from "./exponent/exponentHelper";
8import { ReactDirManager } from "./reactDirManager";
9import { SettingsHelper } from "./settingsHelper";
10import { PackagerStatusIndicator } from "./packagerStatusIndicator";
11import { CommandExecutor } from "../common/commandExecutor";
12import { isNullOrUndefined } from "../common/utils";
13import { OutputChannelLogger } from "./log/OutputChannelLogger";
14import { MobilePlatformDeps, GeneralMobilePlatform } from "./generalMobilePlatform";
15import { PlatformResolver } from "./platformResolver";
16import { ProjectVersionHelper } from "../common/projectVersionHelper";
17import { TelemetryHelper } from "../common/telemetryHelper";
18import { ErrorHelper } from "../common/error/errorHelper";
19import { InternalErrorCode } from "../common/error/internalErrorCode";
20import { TargetPlatformHelper } from "../common/targetPlatformHelper";
21import { ProjectsStorage } from "./projectsStorage";
22import { ReactNativeCDPProxy } from "../cdp-proxy/reactNativeCDPProxy";
23import { generateRandomPortNumber } from "../common/extensionHelper";
24import { DEBUG_TYPES } from "./debuggingConfiguration/debugConfigTypesAndConstants";
25import * as nls from "vscode-nls";
26import { MultipleLifetimesAppWorker } from "../debugger/appWorker";
27import { PlatformType } from "./launchArgs";
28import { LaunchScenariosManager } from "./launchScenariosManager";
29import { IVirtualDevice } from "./VirtualDeviceManager";
30nls.config({
31 messageFormat: nls.MessageFormat.bundle,
32 bundleFormat: nls.BundleFormat.standalone,
33})();
34const localize = nls.loadMessageBundle();
35
36export class AppLauncher {
37 private readonly cdpProxyPort: number;
38 private readonly cdpProxyHostAddress: string;
39
40 private appWorker: MultipleLifetimesAppWorker | null;
41 private packager: Packager;
42 private exponentHelper: ExponentHelper;
43 private reactDirManager: ReactDirManager;
44 private workspaceFolder: vscode.WorkspaceFolder;
45 private reactNativeVersions?: RNPackageVersions;
46 private rnCdpProxy: ReactNativeCDPProxy;
47 private logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
48 private mobilePlatform: GeneralMobilePlatform;
49 private launchScenariosManager: LaunchScenariosManager;
50
51 public static getAppLauncherByProjectRootPath(projectRootPath: string): AppLauncher {
52 const appLauncher = ProjectsStorage.projectsCache[projectRootPath.toLowerCase()];
53 if (!appLauncher) {
54 throw new Error(
55 `Could not find AppLauncher by the project root path ${projectRootPath}`,
56 );
57 }
58
59 return appLauncher;
60 }
61
62 constructor(reactDirManager: ReactDirManager, workspaceFolder: vscode.WorkspaceFolder) {
63 // constants definition
64 this.cdpProxyPort = generateRandomPortNumber();
65 this.cdpProxyHostAddress = "127.0.0.1"; // localhost
66
67 const rootPath = workspaceFolder.uri.fsPath;
68 this.launchScenariosManager = new LaunchScenariosManager(rootPath);
69 const projectRootPath = SettingsHelper.getReactNativeProjectRoot(rootPath);
70 this.exponentHelper = new ExponentHelper(rootPath, projectRootPath);
71 const packagerStatusIndicator: PackagerStatusIndicator = new PackagerStatusIndicator(
72 rootPath,
73 );
74 this.packager = new Packager(
75 rootPath,
76 projectRootPath,
77 SettingsHelper.getPackagerPort(workspaceFolder.uri.fsPath),
78 packagerStatusIndicator,
79 );
80 this.packager.setExponentHelper(this.exponentHelper);
81 this.reactDirManager = reactDirManager;
82 this.workspaceFolder = workspaceFolder;
83 this.rnCdpProxy = new ReactNativeCDPProxy(this.cdpProxyHostAddress, this.cdpProxyPort);
84 }
85
86 public getCdpProxyPort(): number {
87 return this.cdpProxyPort;
88 }
89
90 public getRnCdpProxy(): ReactNativeCDPProxy {
91 return this.rnCdpProxy;
92 }
93
94 public getPackager(): Packager {
95 return this.packager;
96 }
97
98 public getWorkspaceFolderUri(): vscode.Uri {
99 return this.workspaceFolder.uri;
100 }
101
102 public getWorkspaceFolder(): vscode.WorkspaceFolder {
103 return this.workspaceFolder;
104 }
105
106 public getReactNativeVersions(): RNPackageVersions | undefined {
107 return this.reactNativeVersions;
108 }
109
110 public getExponentHelper(): ExponentHelper {
111 return this.exponentHelper;
112 }
113
114 public getReactDirManager(): ReactDirManager {
115 return this.reactDirManager;
116 }
117
118 public setReactNativeVersions(reactNativeVersions: RNPackageVersions): void {
119 this.reactNativeVersions = reactNativeVersions;
120 }
121
122 public setAppWorker(appWorker: MultipleLifetimesAppWorker): void {
123 this.appWorker = appWorker;
124 }
125
126 public getAppWorker(): MultipleLifetimesAppWorker | null {
127 return this.appWorker;
128 }
129
130 public getMobilePlatform(): GeneralMobilePlatform {
131 return this.mobilePlatform;
132 }
133
134 public dispose(): void {
135 this.packager.getStatusIndicator().dispose();
136 this.packager.stop(true);
137 this.mobilePlatform.dispose();
138 }
139
140 public openFileAtLocation(filename: string, lineNumber: number): Promise<void> {
141 return new Promise(resolve => {
142 vscode.workspace
143 .openTextDocument(vscode.Uri.file(filename))
144 .then((document: vscode.TextDocument) => {
145 vscode.window.showTextDocument(document).then((editor: vscode.TextEditor) => {
146 let range = editor.document.lineAt(lineNumber - 1).range;
147 editor.selection = new vscode.Selection(range.start, range.end);
148 editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
149 resolve();
150 });
151 });
152 });
153 }
154
155 public getPackagerPort(projectFolder: string): number {
156 return SettingsHelper.getPackagerPort(projectFolder);
157 }
158
159 public launch(launchArgs: any): Promise<any> {
160 let mobilePlatformOptions = this.requestSetup(launchArgs);
161
162 // We add the parameter if it's defined (adapter crashes otherwise)
163 if (!isNullOrUndefined(launchArgs.logCatArguments)) {
164 mobilePlatformOptions.logCatArguments = [
165 this.parseLogCatArguments(launchArgs.logCatArguments),
166 ];
167 }
168
169 if (!isNullOrUndefined(launchArgs.variant)) {
170 mobilePlatformOptions.variant = launchArgs.variant;
171 }
172
173 if (!isNullOrUndefined(launchArgs.scheme)) {
174 mobilePlatformOptions.scheme = launchArgs.scheme;
175 }
176
177 if (!isNullOrUndefined(launchArgs.productName)) {
178 mobilePlatformOptions.productName = launchArgs.productName;
179 }
180
181 if (!isNullOrUndefined(launchArgs.launchActivity)) {
182 mobilePlatformOptions.debugLaunchActivity = launchArgs.launchActivity;
183 }
184
185 if (launchArgs.type === DEBUG_TYPES.REACT_NATIVE_DIRECT) {
186 mobilePlatformOptions.isDirect = true;
187 }
188
189 mobilePlatformOptions.packagerPort = SettingsHelper.getPackagerPort(
190 launchArgs.cwd || launchArgs.program,
191 );
192 const platformDeps: MobilePlatformDeps = {
193 packager: this.packager,
194 };
195 this.mobilePlatform = new PlatformResolver().resolveMobilePlatform(
196 launchArgs.platform,
197 mobilePlatformOptions,
198 platformDeps,
199 );
200 return new Promise((resolve, reject) => {
201 let extProps: any = {
202 platform: {
203 value: launchArgs.platform,
204 isPii: false,
205 },
206 };
207
208 if (mobilePlatformOptions.isDirect) {
209 extProps.isDirect = {
210 value: true,
211 isPii: false,
212 };
213 }
214
215 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
216 mobilePlatformOptions.projectRoot,
217 ProjectVersionHelper.generateAdditionalPackagesToCheckByPlatform(launchArgs),
218 )
219 .then(versions => {
220 mobilePlatformOptions.reactNativeVersions = versions;
221 extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties(
222 launchArgs,
223 versions,
224 extProps,
225 );
226
227 TelemetryHelper.generate("launch", extProps, generator => {
228 generator.step("resolveEmulator");
229 return this.resolveAndSaveVirtualDevice(
230 this.mobilePlatform,
231 launchArgs,
232 mobilePlatformOptions,
233 )
234 .then(() => this.mobilePlatform.beforeStartPackager())
235 .then(() => {
236 generator.step("checkPlatformCompatibility");
237 TargetPlatformHelper.checkTargetPlatformSupport(
238 mobilePlatformOptions.platform,
239 );
240 })
241 .then(() => {
242 generator.step("startPackager");
243 return this.mobilePlatform.startPackager();
244 })
245 .then(() => {
246 // We've seen that if we don't prewarm the bundle cache, the app fails on the first attempt to connect to the debugger logic
247 // and the user needs to Reload JS manually. We prewarm it to prevent that issue
248 generator.step("prewarmBundleCache");
249 this.logger.info(
250 localize(
251 "PrewarmingBundleCache",
252 "Prewarming bundle cache. This may take a while ...",
253 ),
254 );
255 return this.mobilePlatform.prewarmBundleCache();
256 })
257 .then(() => {
258 generator
259 .step("mobilePlatform.runApp")
260 .add("target", mobilePlatformOptions.target, false);
261 this.logger.info(
262 localize(
263 "BuildingAndRunningApplication",
264 "Building and running application.",
265 ),
266 );
267 return this.mobilePlatform.runApp();
268 })
269 .then(() => {
270 if (
271 mobilePlatformOptions.isDirect ||
272 !mobilePlatformOptions.enableDebug
273 ) {
274 if (
275 mobilePlatformOptions.isDirect &&
276 launchArgs.platform === PlatformType.Android
277 ) {
278 generator.step("mobilePlatform.enableDirectDebuggingMode");
279 if (mobilePlatformOptions.enableDebug) {
280 this.logger.info(
281 localize(
282 "PrepareHermesDebugging",
283 "Prepare Hermes debugging (experimental)",
284 ),
285 );
286 } else {
287 this.logger.info(
288 localize(
289 "PrepareHermesLaunch",
290 "Prepare Hermes launch (experimental)",
291 ),
292 );
293 }
294 } else {
295 generator.step("mobilePlatform.disableJSDebuggingMode");
296 this.logger.info(
297 localize("DisableJSDebugging", "Disable JS Debugging"),
298 );
299 }
300 return this.mobilePlatform.disableJSDebuggingMode();
301 }
302 generator.step("mobilePlatform.enableJSDebuggingMode");
303 this.logger.info(
304 localize("EnableJSDebugging", "Enable JS Debugging"),
305 );
306 return this.mobilePlatform.enableJSDebuggingMode();
307 })
308 .then(resolve)
309 .catch(error => {
310 if (
311 !mobilePlatformOptions.enableDebug &&
312 launchArgs.platform === PlatformType.iOS &&
313 launchArgs.type === DEBUG_TYPES.REACT_NATIVE
314 ) {
315 // If we disable debugging mode for iOS scenarios, we'll we ignore the error and run the 'run-ios' command anyway,
316 // since the error doesn't affects an application launch process
317 return resolve();
318 }
319 generator.addError(error);
320 this.logger.error(error);
321 reject(error);
322 });
323 });
324 })
325 .catch(error => {
326 if (error && error.errorCode) {
327 if (
328 error.errorCode === InternalErrorCode.ReactNativePackageIsNotInstalled
329 ) {
330 TelemetryHelper.sendErrorEvent(
331 "ReactNativePackageIsNotInstalled",
332 ErrorHelper.getInternalError(
333 InternalErrorCode.ReactNativePackageIsNotInstalled,
334 ),
335 );
336 } else if (
337 error.errorCode === InternalErrorCode.ReactNativeWindowsIsNotInstalled
338 ) {
339 TelemetryHelper.sendErrorEvent(
340 "ReactNativeWindowsPackageIsNotInstalled",
341 ErrorHelper.getInternalError(
342 InternalErrorCode.ReactNativeWindowsIsNotInstalled,
343 ),
344 );
345 }
346 }
347 this.logger.error(error);
348 reject(error);
349 });
350 });
351 }
352
353 private resolveAndSaveVirtualDevice(
354 mobilePlatform: GeneralMobilePlatform,
355 launchArgs: any,
356 mobilePlatformOptions: any,
357 ): Promise<void> {
358 if (
359 launchArgs.target &&
360 (mobilePlatformOptions.platform === PlatformType.Android ||
361 mobilePlatformOptions.platform === PlatformType.iOS)
362 ) {
363 return mobilePlatform
364 .resolveVirtualDevice(launchArgs.target)
365 .then((emulator: IVirtualDevice | null) => {
366 if (emulator) {
367 if (emulator.name && launchArgs.platform === PlatformType.Android) {
368 mobilePlatformOptions.target = emulator.id;
369 this.launchScenariosManager.updateLaunchScenario(launchArgs, {
370 target: emulator.name,
371 });
372 }
373 if (launchArgs.platform === PlatformType.iOS) {
374 this.launchScenariosManager.updateLaunchScenario(launchArgs, {
375 target: emulator.id,
376 });
377 }
378 launchArgs.target = emulator.id;
379 } else if (
380 mobilePlatformOptions.target.indexOf("device") < 0 &&
381 launchArgs.platform === PlatformType.Android
382 ) {
383 // We should cleanup target only for Android platform,
384 // because react-native-cli does not support launch with Android emulator name
385 this.cleanupTargetModifications(mobilePlatform, mobilePlatformOptions);
386 }
387 })
388 .catch(error => {
389 if (
390 error &&
391 error.errorCode &&
392 error.errorCode === InternalErrorCode.VirtualDeviceSelectionError
393 ) {
394 TelemetryHelper.sendErrorEvent(
395 "VirtualDeviceSelectionError",
396 ErrorHelper.getInternalError(
397 InternalErrorCode.VirtualDeviceSelectionError,
398 ),
399 );
400
401 this.logger.warning(error);
402 this.logger.warning(
403 localize(
404 "ContinueWithRnCliWorkflow",
405 "Continue using standard RN CLI workflow.",
406 ),
407 );
408
409 if (mobilePlatformOptions.target.indexOf("device") < 0) {
410 this.cleanupTargetModifications(mobilePlatform, mobilePlatformOptions);
411 }
412 return Promise.resolve();
413 } else {
414 return Promise.reject(error);
415 }
416 });
417 }
418 return Promise.resolve();
419 }
420
421 private cleanupTargetModifications(
422 mobilePlatform: GeneralMobilePlatform,
423 mobilePlatformOptions: any,
424 ) {
425 mobilePlatformOptions.target = "simulator";
426 mobilePlatform.runArguments = mobilePlatform.getRunArguments();
427 }
428
429 private requestSetup(args: any): any {
430 const workspaceFolder: vscode.WorkspaceFolder = <vscode.WorkspaceFolder>(
431 vscode.workspace.getWorkspaceFolder(vscode.Uri.file(args.cwd || args.program))
432 );
433 const projectRootPath = this.getProjectRoot(args);
434 let mobilePlatformOptions: any = {
435 workspaceRoot: workspaceFolder.uri.fsPath,
436 projectRoot: projectRootPath,
437 platform: args.platform,
438 env: args.env,
439 envFile: args.envFile,
440 target: args.target || "simulator",
441 enableDebug: args.enableDebug,
442 };
443
444 if (args.platform === PlatformType.Exponent) {
445 mobilePlatformOptions.expoHostType = args.expoHostType || "tunnel";
446 mobilePlatformOptions.openExpoQR =
447 typeof args.openExpoQR !== "boolean" ? true : args.openExpoQR;
448 }
449
450 CommandExecutor.ReactNativeCommand = SettingsHelper.getReactNativeGlobalCommandName(
451 workspaceFolder.uri,
452 );
453
454 if (!args.runArguments) {
455 let runArgs = SettingsHelper.getRunArgs(
456 args.platform,
457 args.target || "simulator",
458 workspaceFolder.uri,
459 );
460 mobilePlatformOptions.runArguments = runArgs;
461 } else {
462 mobilePlatformOptions.runArguments = args.runArguments;
463 }
464
465 return mobilePlatformOptions;
466 }
467
468 private getProjectRoot(args: any): string {
469 return SettingsHelper.getReactNativeProjectRoot(args.cwd || args.program);
470 }
471
472 /**
473 * Parses log cat arguments to a string
474 */
475 private parseLogCatArguments(userProvidedLogCatArguments: any): string {
476 return Array.isArray(userProvidedLogCatArguments)
477 ? userProvidedLogCatArguments.join(" ") // If it's an array, we join the arguments
478 : userProvidedLogCatArguments; // If not, we leave it as-is
479 }
480}
481