microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.4.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/appLauncher.ts

493lines · 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 (mobilePlatformOptions.isDirect) {
271 if (launchArgs.useHermesEngine) {
272 generator.step("mobilePlatform.enableHermesDebuggingMode");
273 if (mobilePlatformOptions.enableDebug) {
274 this.logger.info(
275 localize(
276 "PrepareHermesDebugging",
277 "Prepare Hermes debugging (experimental)",
278 ),
279 );
280 } else {
281 this.logger.info(
282 localize(
283 "PrepareHermesLaunch",
284 "Prepare Hermes launch (experimental)",
285 ),
286 );
287 }
288 } else if (launchArgs.platform === PlatformType.iOS) {
289 generator.step(
290 "mobilePlatform.enableIosDirectDebuggingMode",
291 );
292 if (mobilePlatformOptions.enableDebug) {
293 this.logger.info(
294 localize(
295 "PrepareDirectIosDebugging",
296 "Prepare direct iOS debugging (experimental)",
297 ),
298 );
299 } else {
300 this.logger.info(
301 localize(
302 "PrepareDirectIosLaunch",
303 "Prepare direct iOS launch (experimental)",
304 ),
305 );
306 }
307 }
308 generator.step("mobilePlatform.disableJSDebuggingMode");
309 this.logger.info(
310 localize("DisableJSDebugging", "Disable JS Debugging"),
311 );
312 return this.mobilePlatform.disableJSDebuggingMode();
313 } else {
314 generator.step("mobilePlatform.enableJSDebuggingMode");
315 this.logger.info(
316 localize("EnableJSDebugging", "Enable JS Debugging"),
317 );
318 return this.mobilePlatform.enableJSDebuggingMode();
319 }
320 })
321 .then(resolve)
322 .catch(error => {
323 if (
324 !mobilePlatformOptions.enableDebug &&
325 launchArgs.platform === PlatformType.iOS &&
326 launchArgs.type === DEBUG_TYPES.REACT_NATIVE
327 ) {
328 // If we disable debugging mode for iOS scenarios, we'll we ignore the error and run the 'run-ios' command anyway,
329 // since the error doesn't affects an application launch process
330 return resolve();
331 }
332 generator.addError(error);
333 this.logger.error(error);
334 reject(error);
335 });
336 });
337 })
338 .catch(error => {
339 if (error && error.errorCode) {
340 if (
341 error.errorCode === InternalErrorCode.ReactNativePackageIsNotInstalled
342 ) {
343 TelemetryHelper.sendErrorEvent(
344 "ReactNativePackageIsNotInstalled",
345 ErrorHelper.getInternalError(
346 InternalErrorCode.ReactNativePackageIsNotInstalled,
347 ),
348 );
349 } else if (
350 error.errorCode === InternalErrorCode.ReactNativeWindowsIsNotInstalled
351 ) {
352 TelemetryHelper.sendErrorEvent(
353 "ReactNativeWindowsPackageIsNotInstalled",
354 ErrorHelper.getInternalError(
355 InternalErrorCode.ReactNativeWindowsIsNotInstalled,
356 ),
357 );
358 }
359 }
360 this.logger.error(error);
361 reject(error);
362 });
363 });
364 }
365
366 private resolveAndSaveVirtualDevice(
367 mobilePlatform: GeneralMobilePlatform,
368 launchArgs: any,
369 mobilePlatformOptions: any,
370 ): Promise<void> {
371 if (
372 launchArgs.target &&
373 (mobilePlatformOptions.platform === PlatformType.Android ||
374 mobilePlatformOptions.platform === PlatformType.iOS)
375 ) {
376 return mobilePlatform
377 .resolveVirtualDevice(launchArgs.target)
378 .then((emulator: IVirtualDevice | null) => {
379 if (emulator) {
380 if (emulator.name && launchArgs.platform === PlatformType.Android) {
381 mobilePlatformOptions.target = emulator.id;
382 this.launchScenariosManager.updateLaunchScenario(launchArgs, {
383 target: emulator.name,
384 });
385 }
386 if (launchArgs.platform === PlatformType.iOS) {
387 this.launchScenariosManager.updateLaunchScenario(launchArgs, {
388 target: emulator.id,
389 });
390 }
391 launchArgs.target = emulator.id;
392 } else if (
393 mobilePlatformOptions.target.indexOf("device") < 0 &&
394 launchArgs.platform === PlatformType.Android
395 ) {
396 // We should cleanup target only for Android platform,
397 // because react-native-cli does not support launch with Android emulator name
398 this.cleanupTargetModifications(mobilePlatform, mobilePlatformOptions);
399 }
400 })
401 .catch(error => {
402 if (
403 error &&
404 error.errorCode &&
405 error.errorCode === InternalErrorCode.VirtualDeviceSelectionError
406 ) {
407 TelemetryHelper.sendErrorEvent(
408 "VirtualDeviceSelectionError",
409 ErrorHelper.getInternalError(
410 InternalErrorCode.VirtualDeviceSelectionError,
411 ),
412 );
413
414 this.logger.warning(error);
415 this.logger.warning(
416 localize(
417 "ContinueWithRnCliWorkflow",
418 "Continue using standard RN CLI workflow.",
419 ),
420 );
421
422 if (mobilePlatformOptions.target.indexOf("device") < 0) {
423 this.cleanupTargetModifications(mobilePlatform, mobilePlatformOptions);
424 }
425 return Promise.resolve();
426 } else {
427 return Promise.reject(error);
428 }
429 });
430 }
431 return Promise.resolve();
432 }
433
434 private cleanupTargetModifications(
435 mobilePlatform: GeneralMobilePlatform,
436 mobilePlatformOptions: any,
437 ) {
438 mobilePlatformOptions.target = "simulator";
439 mobilePlatform.runArguments = mobilePlatform.getRunArguments();
440 }
441
442 private requestSetup(args: any): any {
443 const workspaceFolder: vscode.WorkspaceFolder = <vscode.WorkspaceFolder>(
444 vscode.workspace.getWorkspaceFolder(vscode.Uri.file(args.cwd || args.program))
445 );
446 const projectRootPath = this.getProjectRoot(args);
447 let mobilePlatformOptions: any = {
448 workspaceRoot: workspaceFolder.uri.fsPath,
449 projectRoot: projectRootPath,
450 platform: args.platform,
451 env: args.env,
452 envFile: args.envFile,
453 target: args.target || "simulator",
454 enableDebug: args.enableDebug,
455 };
456
457 if (args.platform === PlatformType.Exponent) {
458 mobilePlatformOptions.expoHostType = args.expoHostType || "tunnel";
459 mobilePlatformOptions.openExpoQR =
460 typeof args.openExpoQR !== "boolean" ? true : args.openExpoQR;
461 }
462
463 CommandExecutor.ReactNativeCommand = SettingsHelper.getReactNativeGlobalCommandName(
464 workspaceFolder.uri,
465 );
466
467 if (!args.runArguments) {
468 let runArgs = SettingsHelper.getRunArgs(
469 args.platform,
470 args.target || "simulator",
471 workspaceFolder.uri,
472 );
473 mobilePlatformOptions.runArguments = runArgs;
474 } else {
475 mobilePlatformOptions.runArguments = args.runArguments;
476 }
477
478 return mobilePlatformOptions;
479 }
480
481 private getProjectRoot(args: any): string {
482 return SettingsHelper.getReactNativeProjectRoot(args.cwd || args.program);
483 }
484
485 /**
486 * Parses log cat arguments to a string
487 */
488 private parseLogCatArguments(userProvidedLogCatArguments: any): string {
489 return Array.isArray(userProvidedLogCatArguments)
490 ? userProvidedLogCatArguments.join(" ") // If it's an array, we join the arguments
491 : userProvidedLogCatArguments; // If not, we leave it as-is
492 }
493}
494