microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.1.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/appLauncher.ts

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