microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
dev/v-peq/add-expo-packager-command-tests

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/ios/iOSPlatform.ts

454lines · 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 path from "path";
5import * as semver from "semver";
6
7import * as nls from "vscode-nls";
8import { ChildProcess } from "../../common/node/childProcess";
9import { CommandExecutor } from "../../common/commandExecutor";
10import { MobilePlatformDeps, TargetType } from "../generalPlatform";
11import { IIOSRunOptions, PlatformType } from "../launchArgs";
12import { OutputVerifier, PatternToFailure } from "../../common/outputVerifier";
13import { TelemetryHelper } from "../../common/telemetryHelper";
14import { InternalErrorCode } from "../../common/error/internalErrorCode";
15import { AppLauncher } from "../appLauncher";
16import { GeneralMobilePlatform } from "../generalMobilePlatform";
17import { ErrorHelper } from "../../common/error/errorHelper";
18import { ProjectVersionHelper } from "../../common/projectVersionHelper";
19import { IDebuggableIOSTarget, IOSTarget, IOSTargetManager } from "./iOSTargetManager";
20import { IOSDebugModeManager } from "./iOSDebugModeManager";
21import { PlistBuddy } from "./plistBuddy";
22
23nls.config({
24 messageFormat: nls.MessageFormat.bundle,
25 bundleFormat: nls.BundleFormat.standalone,
26})();
27const localize = nls.loadMessageBundle();
28
29export class IOSPlatform extends GeneralMobilePlatform {
30 public static DEFAULT_IOS_PROJECT_RELATIVE_PATH = "ios";
31
32 private static readonly NEW_RN_CLI_BEHAVIOUR_VERSION = "0.60.0";
33
34 private plistBuddy = new PlistBuddy();
35 private iosProjectRoot: string;
36 private iosDebugModeManager: IOSDebugModeManager;
37
38 private defaultConfiguration: string = "Debug";
39 private configurationArgumentName: string = "--configuration";
40
41 protected target?: IOSTarget;
42
43 // We should add the common iOS build/run errors we find to this list
44 private static RUN_IOS_FAILURE_PATTERNS: PatternToFailure[] = [
45 {
46 pattern: "No devices are booted",
47 errorCode: InternalErrorCode.IOSSimulatorNotLaunchable,
48 },
49 {
50 pattern: "FBSOpenApplicationErrorDomain",
51 errorCode: InternalErrorCode.IOSSimulatorNotLaunchable,
52 },
53 {
54 pattern: "ios-deploy",
55 errorCode: InternalErrorCode.IOSDeployNotFound,
56 },
57 ];
58
59 private static readonly RUN_IOS_SUCCESS_PATTERNS = [
60 "BUILD SUCCEEDED|success Successfully built the app",
61 ];
62
63 constructor(protected runOptions: IIOSRunOptions, platformDeps: MobilePlatformDeps = {}) {
64 super(runOptions, platformDeps);
65
66 this.targetManager = new IOSTargetManager();
67 this.runOptions.configuration = this.getConfiguration();
68
69 const iosProjectFolderPath = IOSPlatform.getOptFromRunArgs(
70 this.runArguments,
71 "--project-path",
72 false,
73 );
74 this.iosProjectRoot = path.join(
75 this.projectPath,
76 iosProjectFolderPath || IOSPlatform.DEFAULT_IOS_PROJECT_RELATIVE_PATH,
77 );
78 const schemeFromArgs = IOSPlatform.getOptFromRunArgs(this.runArguments, "--scheme", false);
79 this.iosDebugModeManager = new IOSDebugModeManager(
80 this.iosProjectRoot,
81 this.projectPath,
82 schemeFromArgs ? schemeFromArgs : this.runOptions.scheme,
83 );
84 }
85
86 public async getTarget(): Promise<IOSTarget> {
87 if (!this.target) {
88 const targetFromRunArgs = await this.getTargetFromRunArgs();
89 if (targetFromRunArgs) {
90 this.target = targetFromRunArgs;
91 } else {
92 const targets =
93 (await this.targetManager.getTargetList()) as IDebuggableIOSTarget[];
94 const targetsBySpecifiedType = targets.filter(target => {
95 switch (this.runOptions.target) {
96 case TargetType.Simulator:
97 return target.isVirtualTarget;
98 case TargetType.Device:
99 return !target.isVirtualTarget;
100 case undefined:
101 case "":
102 return true;
103 default:
104 return (
105 target.id === this.runOptions.target ||
106 target.name === this.runOptions.target
107 );
108 }
109 });
110 if (targetsBySpecifiedType.length) {
111 this.target = IOSTarget.fromInterface(targetsBySpecifiedType[0]);
112 } else if (targets.length) {
113 this.logger.warning(
114 localize(
115 "ThereIsNoTargetWithSpecifiedTargetType",
116 "There is no any target with specified target type '{0}'. Continue with any target.",
117 this.runOptions.target,
118 ),
119 );
120 this.target = IOSTarget.fromInterface(targets[0]);
121 } else {
122 throw ErrorHelper.getInternalError(
123 InternalErrorCode.IOSThereIsNoAnyDebuggableTarget,
124 );
125 }
126 }
127 }
128 return this.target;
129 }
130
131 public async showDevMenu(appLauncher: AppLauncher): Promise<void> {
132 const worker = appLauncher.getAppWorker();
133 if (worker) {
134 worker.showDevMenuCommand();
135 }
136 }
137
138 public async reloadApp(appLauncher: AppLauncher): Promise<void> {
139 const worker = appLauncher.getAppWorker();
140 if (worker) {
141 worker.reloadAppCommand();
142 }
143 }
144
145 public async runApp(): Promise<void> {
146 let extProps: any = {
147 platform: {
148 value: PlatformType.iOS,
149 isPii: false,
150 },
151 };
152
153 if (this.runOptions.isDirect) {
154 extProps.isDirect = {
155 value: true,
156 isPii: false,
157 };
158 this.projectObserver?.updateRNIosHermesProjectState(true);
159 }
160
161 extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties(
162 this.runOptions,
163 this.runOptions.reactNativeVersions,
164 extProps,
165 );
166
167 await TelemetryHelper.generate("iOSPlatform.runApp", extProps, async () => {
168 // Compile, deploy, and launch the app on either a simulator or a device
169 const env = GeneralMobilePlatform.getEnvArgument(
170 process.env,
171 this.runOptions.env,
172 this.runOptions.envFile,
173 );
174
175 if (
176 !semver.valid(
177 this.runOptions.reactNativeVersions.reactNativeVersion,
178 ) /* Custom RN implementations should support this flag*/ ||
179 semver.gte(
180 this.runOptions.reactNativeVersions.reactNativeVersion,
181 IOSPlatform.NO_PACKAGER_VERSION,
182 ) ||
183 ProjectVersionHelper.isCanaryVersion(
184 this.runOptions.reactNativeVersions.reactNativeVersion,
185 )
186 ) {
187 this.runArguments.push("--no-packager");
188 }
189 // Since @react-native-community/cli@2.1.0 build output are hidden by default
190 // we are using `--verbose` to show it as it contains `BUILD SUCCESSFUL` and other patterns
191 if (
192 semver.gte(
193 this.runOptions.reactNativeVersions.reactNativeVersion,
194 IOSPlatform.NEW_RN_CLI_BEHAVIOUR_VERSION,
195 ) ||
196 ProjectVersionHelper.isCanaryVersion(
197 this.runOptions.reactNativeVersions.reactNativeVersion,
198 )
199 ) {
200 this.runArguments.push("--verbose");
201 }
202 const runIosSpawn = new CommandExecutor(
203 this.runOptions.nodeModulesRoot,
204 this.projectPath,
205 this.logger,
206 ).spawnReactCommand("run-ios", this.runArguments, { env });
207 await new OutputVerifier(
208 () =>
209 this.generateSuccessPatterns(
210 this.runOptions.reactNativeVersions.reactNativeVersion,
211 ),
212 () => Promise.resolve(IOSPlatform.RUN_IOS_FAILURE_PATTERNS),
213 PlatformType.iOS,
214 ).process(runIosSpawn);
215
216 // Save target info for status indicator
217 if (!this.target) {
218 const target = await this.getTarget();
219 if (target && target.name) {
220 this.target = new IOSTarget(
221 target.isOnline,
222 target.isVirtualTarget,
223 target.id,
224 target.name,
225 target.system,
226 );
227 }
228 }
229 });
230 }
231
232 public async enableJSDebuggingMode(): Promise<void> {
233 // Configure the app for debugging
234 if (!(await this.getTarget()).isVirtualTarget) {
235 // Note that currently we cannot automatically switch the device into debug mode.
236 this.logger.info(
237 "Application is running on a device, please shake device and select 'Debug JS Remotely' to enable debugging.",
238 );
239 return;
240 }
241
242 // Wait until the configuration file exists, and check to see if debugging is enabled
243 const [debugModeEnabled, bundleId] = await Promise.all<boolean | string>([
244 this.iosDebugModeManager.getAppRemoteDebuggingSetting(
245 this.runOptions.configuration,
246 this.runOptions.productName,
247 ),
248 this.getBundleId(),
249 ]);
250 if (debugModeEnabled) {
251 return;
252 }
253 // Debugging must still be enabled
254 // We enable debugging by writing to a plist file that backs a NSUserDefaults object,
255 // but that file is written to by the app on occasion. To avoid races, we shut the app
256 // down before writing to the file.
257 const childProcess = new ChildProcess();
258 const output = await childProcess.execToString("xcrun simctl spawn booted launchctl list");
259 // Try to find an entry that looks like UIKitApplication:com.example.myApp[0x4f37]
260 const regex = new RegExp(`(\\S+${String(bundleId)}\\S+)`);
261 const match = regex.exec(output);
262 // If we don't find a match, the app must not be running and so we do not need to close it
263 if (match) {
264 await childProcess.exec(`xcrun simctl spawn booted launchctl stop ${match[1]}`);
265 }
266 // Write to the settings file while the app is not running to avoid races
267 await this.iosDebugModeManager.setAppRemoteDebuggingSetting(
268 /* enable=*/ true,
269 this.runOptions.configuration,
270 this.runOptions.productName,
271 );
272 // Relaunch the app
273 return await this.runApp();
274 }
275
276 public async disableJSDebuggingMode(): Promise<void> {
277 if (!(await this.getTarget()).isVirtualTarget) {
278 return;
279 }
280 return this.iosDebugModeManager.setAppRemoteDebuggingSetting(
281 /* enable=*/ false,
282 this.runOptions.configuration,
283 this.runOptions.productName,
284 );
285 }
286
287 public prewarmBundleCache(): Promise<void> {
288 return this.packager.prewarmBundleCache(PlatformType.iOS);
289 }
290
291 public getRunArguments(): string[] {
292 let runArguments: string[] = [];
293
294 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
295 runArguments = this.runOptions.runArguments;
296 if (this.runOptions.scheme) {
297 const schemeFromArgs = IOSPlatform.getOptFromRunArgs(
298 runArguments,
299 "--scheme",
300 false,
301 );
302 if (!schemeFromArgs) {
303 runArguments.push("--scheme", this.runOptions.scheme);
304 } else {
305 this.logger.warning(
306 localize(
307 "iosSchemeParameterAlreadySetInRunArguments",
308 "'--scheme' is set as 'runArguments' configuration parameter value, 'scheme' configuration parameter value will be omitted",
309 ),
310 );
311 }
312 }
313 } else {
314 if (this.runOptions.target) {
315 runArguments.push(...this.handleTargetArg(this.runOptions.target));
316 }
317
318 // provide any defined scheme
319 if (this.runOptions.scheme) {
320 runArguments.push("--scheme", this.runOptions.scheme);
321 }
322 }
323
324 return runArguments;
325 }
326
327 public async getTargetFromRunArgs(): Promise<IOSTarget | undefined> {
328 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
329 const targets = (await this.targetManager.getTargetList()) as IDebuggableIOSTarget[];
330
331 const udid = GeneralMobilePlatform.getOptFromRunArgs(
332 this.runOptions.runArguments,
333 "--udid",
334 );
335 if (udid) {
336 const target = targets.find(target => target.id === udid);
337 if (target) {
338 return IOSTarget.fromInterface(target);
339 }
340 this.logger.warning(
341 localize(
342 "ThereIsNoIosTargetWithSuchUdid",
343 "There is no iOS target with such UDID: {0}",
344 udid,
345 ),
346 );
347 }
348
349 const device = GeneralMobilePlatform.getOptFromRunArgs(
350 this.runOptions.runArguments,
351 "--device",
352 );
353 if (device) {
354 const target = targets.find(
355 target => !target.isVirtualTarget && target.name === device,
356 );
357 if (target) {
358 return IOSTarget.fromInterface(target);
359 }
360 this.logger.warning(
361 localize(
362 "ThereIsNoIosDeviceWithSuchName",
363 "There is no iOS device with such name: {0}",
364 device,
365 ),
366 );
367 }
368
369 const simulator = GeneralMobilePlatform.getOptFromRunArgs(
370 this.runOptions.runArguments,
371 "--simulator",
372 );
373 if (simulator) {
374 const target = targets.find(
375 target => target.isVirtualTarget && target.name === simulator,
376 );
377 if (target) {
378 return IOSTarget.fromInterface(target);
379 }
380 this.logger.warning(
381 localize(
382 "ThereIsNoIosSimulatorWithSuchName",
383 "There is no iOS simulator with such name: {0}",
384 simulator,
385 ),
386 );
387 }
388 }
389
390 return undefined;
391 }
392
393 private handleTargetArg(target: string): string[] {
394 return target === TargetType.Device || target === TargetType.Simulator
395 ? [`--${target}`]
396 : ["--udid", target];
397 }
398
399 private async generateSuccessPatterns(version: string): Promise<string[]> {
400 // Clone RUN_IOS_SUCCESS_PATTERNS to avoid its runtime mutation
401 const successPatterns = [...IOSPlatform.RUN_IOS_SUCCESS_PATTERNS];
402 if (!(await this.getTarget()).isVirtualTarget) {
403 if (
404 semver.gte(version, IOSPlatform.NEW_RN_CLI_BEHAVIOUR_VERSION) ||
405 ProjectVersionHelper.isCanaryVersion(version)
406 ) {
407 successPatterns.push("success Installed the app on the device");
408 } else {
409 successPatterns.push("INSTALLATION SUCCEEDED");
410 }
411 return successPatterns;
412 }
413 const bundleId = await this.getBundleId();
414 if (
415 semver.gte(version, IOSPlatform.NEW_RN_CLI_BEHAVIOUR_VERSION) ||
416 ProjectVersionHelper.isCanaryVersion(version)
417 ) {
418 successPatterns.push(`Launching "${bundleId}"\nsuccess Successfully launched the app`);
419 } else {
420 successPatterns.push(`Launching ${bundleId}\n${bundleId}: `);
421 }
422 return successPatterns;
423 }
424
425 private getConfiguration(): string {
426 return (
427 IOSPlatform.getOptFromRunArgs(this.runArguments, this.configurationArgumentName) ||
428 this.defaultConfiguration
429 );
430 }
431
432 private getBundleId(): Promise<string> {
433 let scheme = this.runOptions.scheme;
434 if (!scheme) {
435 const schemeFromArgs = IOSPlatform.getOptFromRunArgs(
436 this.runArguments,
437 "--scheme",
438 false,
439 );
440 if (schemeFromArgs) {
441 scheme = schemeFromArgs;
442 }
443 }
444 return this.plistBuddy.getBundleId(
445 this.iosProjectRoot,
446 this.projectPath,
447 PlatformType.iOS,
448 true,
449 this.runOptions.configuration,
450 this.runOptions.productName,
451 scheme,
452 );
453 }
454}