microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8df5011e47ada44be3951868ba32e5fae98f48bb

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/appLauncher.ts

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