@hot-updater/apple-helper 0.25.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,848 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ let __hot_updater_cli_tools = require("@hot-updater/cli-tools");
25
+ __hot_updater_cli_tools = __toESM(__hot_updater_cli_tools);
26
+ let fs = require("fs");
27
+ fs = __toESM(fs);
28
+ let path = require("path");
29
+ path = __toESM(path);
30
+ let __hot_updater_plugin_core = require("@hot-updater/plugin-core");
31
+ __hot_updater_plugin_core = __toESM(__hot_updater_plugin_core);
32
+ let execa = require("execa");
33
+ execa = __toESM(execa);
34
+ let node_fs_promises = require("node:fs/promises");
35
+ node_fs_promises = __toESM(node_fs_promises);
36
+ let node_os = require("node:os");
37
+ node_os = __toESM(node_os);
38
+ let node_path = require("node:path");
39
+ node_path = __toESM(node_path);
40
+
41
+ //#region src/utils/cocoapods.ts
42
+ const installPodsIfNeeded = async (sourceDir) => {
43
+ const podfilePath = path.default.join(sourceDir, "Podfile");
44
+ if (!fs.default.existsSync(podfilePath)) {
45
+ __hot_updater_cli_tools.p.log.info("No Podfile found, skipping CocoaPods installation");
46
+ return;
47
+ }
48
+ const gemfilePaths = [path.default.join(sourceDir, "Gemfile"), path.default.join((0, __hot_updater_cli_tools.getCwd)(), "Gemfile")];
49
+ const shouldUseBundler = (await Promise.all(gemfilePaths.map(checkShouldUseBundler))).some(Boolean);
50
+ try {
51
+ if (shouldUseBundler) {
52
+ __hot_updater_cli_tools.p.log.info("Using bundler for CocoaPods installation");
53
+ const bundleSpinner = __hot_updater_cli_tools.p.spinner();
54
+ bundleSpinner.start("Installing Ruby gems");
55
+ await (0, execa.execa)("bundle", ["install"], { cwd: sourceDir });
56
+ bundleSpinner.stop("Ruby gems installed");
57
+ const podSpinner = __hot_updater_cli_tools.p.spinner();
58
+ podSpinner.start("Installing CocoaPods dependencies");
59
+ await (0, execa.execa)("bundle", [
60
+ "exec",
61
+ "pod",
62
+ "install"
63
+ ], {
64
+ cwd: sourceDir,
65
+ env: preparePodInstallEnvVars()
66
+ });
67
+ podSpinner.stop("CocoaPods dependencies installed");
68
+ } else {
69
+ const spinner = __hot_updater_cli_tools.p.spinner();
70
+ spinner.start("Installing CocoaPods dependencies");
71
+ await (0, execa.execa)("pod", ["install"], {
72
+ cwd: sourceDir,
73
+ env: preparePodInstallEnvVars()
74
+ });
75
+ spinner.stop("CocoaPods dependencies installed");
76
+ }
77
+ } catch (error) {
78
+ throw new Error(`pod install failed: ${error}`);
79
+ }
80
+ };
81
+ const checkShouldUseBundler = async (gemfilePath) => {
82
+ try {
83
+ if (!fs.default.existsSync(gemfilePath)) return false;
84
+ return (await fs.default.promises.readFile(gemfilePath, "utf-8")).includes("cocoapods");
85
+ } catch (_error) {
86
+ return false;
87
+ }
88
+ };
89
+ const preparePodInstallEnvVars = () => {
90
+ const { minor } = (0, __hot_updater_cli_tools.getReactNativeMetadatas)().version;
91
+ const usePrebuiltReactNative = minor >= 81;
92
+ return {
93
+ RCT_IGNORE_PODS_DEPRECATION: "1",
94
+ RCT_USE_RN_DEP: process.env["RCT_USE_RN_DEP"] !== void 0 ? String(process.env["RCT_USE_RN_DEP"]) : usePrebuiltReactNative ? "1" : "0",
95
+ RCT_USE_PREBUILT_RNCORE: process.env["RCT_USE_PREBUILT_RNCORE"] !== void 0 ? String(process.env["RCT_USE_PREBUILT_RNCORE"]) : usePrebuiltReactNative ? "1" : "0"
96
+ };
97
+ };
98
+
99
+ //#endregion
100
+ //#region src/utils/createRandomTmpDir.ts
101
+ const createRandomTmpDir = async (prefix = "apple-helper-") => {
102
+ return await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), prefix));
103
+ };
104
+
105
+ //#endregion
106
+ //#region src/utils/createXcodebuildLogger.ts
107
+ const createXcodebuildLogger = () => new __hot_updater_cli_tools.BuildLogger({
108
+ failurePatterns: ["BUILD FAILED", "ARCHIVE FAILED"],
109
+ importantLogPatterns: [
110
+ "error:",
111
+ "** BUILD FAILED **",
112
+ "** ARCHIVE FAILED **",
113
+ "The following build commands failed:"
114
+ ],
115
+ progressMapping: [
116
+ [["[CP-User] [RN]Check rncore"], 10],
117
+ [["[CP-User] [Hermes] Replace Hermes"], 35],
118
+ [["[CP-User] [RN]Check FBReactNativeSpec"], 53],
119
+ [["React-FabricComponents"], 66],
120
+ [["[CP] Check Pods Manifest.lock"], 90],
121
+ [["BUILD SUCCEEDED", "ARCHIVE SUCCEEDED"], 100]
122
+ ]
123
+ });
124
+
125
+ //#endregion
126
+ //#region src/utils/platform.ts
127
+ /**
128
+ * Platform configurations for different Apple platforms
129
+ */
130
+ const platformConfigs = { ios: {
131
+ name: "ios",
132
+ deviceSdk: "iphoneos",
133
+ simulatorSdk: "iphonesimulator",
134
+ supportsSimulator: true,
135
+ deviceDestination: "platform=iOS",
136
+ simulatorDestination: "platform=iOS Simulator"
137
+ } };
138
+
139
+ //#endregion
140
+ //#region src/utils/destination.ts
141
+ const buildDestinationString = ({ deviceType, platform, useGeneric }) => `${useGeneric ? "generic/" : ""}${deviceType === "device" ? platformConfigs[platform].deviceDestination : platformConfigs[platform].simulatorDestination}`;
142
+ /**
143
+ * Resolves IosBuildDestination to xcodebuild destination format
144
+ */
145
+ const resolveDestination = ({ destination, useGeneric }) => {
146
+ if (typeof destination === "object") {
147
+ if ("id" in destination) return `id=${destination.id}`;
148
+ if ("name" in destination) return `name=${destination.name}`;
149
+ }
150
+ switch (destination) {
151
+ case "ios-device": return buildDestinationString({
152
+ platform: "ios",
153
+ deviceType: "device",
154
+ useGeneric
155
+ });
156
+ case "ios-simulator": return buildDestinationString({
157
+ platform: "ios",
158
+ deviceType: "simulator",
159
+ useGeneric
160
+ });
161
+ }
162
+ };
163
+ const resolveDestinations = ({ destinations, useGeneric }) => {
164
+ return destinations.map((destination) => resolveDestination({
165
+ destination,
166
+ useGeneric
167
+ }));
168
+ };
169
+ const getDefaultDestination = ({ platform, useGeneric, deviceType }) => {
170
+ return buildDestinationString({
171
+ deviceType,
172
+ platform,
173
+ useGeneric
174
+ });
175
+ };
176
+
177
+ //#endregion
178
+ //#region src/utils/parseXcodeProjectInfo.ts
179
+ const parseXcodeProjectInfo = async (sourceDir) => {
180
+ const files = await fs.default.promises.readdir(sourceDir);
181
+ const workspace = files.find((file) => file.endsWith(".xcworkspace"));
182
+ if (workspace) return {
183
+ isWorkspace: true,
184
+ name: workspace,
185
+ path: path.default.join(sourceDir, workspace)
186
+ };
187
+ const project = files.find((file) => file.endsWith(".xcodeproj"));
188
+ if (project) return {
189
+ isWorkspace: false,
190
+ name: project,
191
+ path: path.default.join(sourceDir, project)
192
+ };
193
+ throw new Error(`No Xcode project or workspace found in ${sourceDir}`);
194
+ };
195
+
196
+ //#endregion
197
+ //#region src/utils/prettifyXcodebuildError.ts
198
+ const prettifyXcodebuildError = (error) => {
199
+ let errorString = "";
200
+ if (error instanceof execa.ExecaError || error instanceof Error || "message" in error) errorString = error.message;
201
+ if (errorString.includes("error: No profiles for") || errorString.includes("error: Provisioning profile")) return /* @__PURE__ */ new Error("Xcodebuild failed: Not valid provisioning profile is set in the project. Check out Singing & Capabilities tab in the Xcode.");
202
+ return /* @__PURE__ */ new Error(`Xcodebuild failed: ${error}`);
203
+ };
204
+
205
+ //#endregion
206
+ //#region src/builder/archiveXcodeProject.ts
207
+ const archiveXcodeProject = async ({ sourceDir, platform, schemeConfig }) => {
208
+ const xcodeProject = await parseXcodeProjectInfo(sourceDir);
209
+ if (schemeConfig.installPods ?? true) await installPodsIfNeeded(sourceDir);
210
+ const tmpDir = await createRandomTmpDir();
211
+ const archiveName = `${xcodeProject.name.replace(".xcworkspace", "").replace(".xcodeproj", "")}.xcarchive`;
212
+ const archivePath = path.default.join(tmpDir, archiveName);
213
+ const archiveArgs = prepareArchiveArgs({
214
+ archivePath,
215
+ platform,
216
+ schemeConfig,
217
+ sourceDir,
218
+ xcodeProject
219
+ });
220
+ __hot_updater_cli_tools.p.log.info(`Xcode Archive Settings:
221
+ Project ${xcodeProject.name}
222
+ Scheme ${schemeConfig.scheme}
223
+ Platform ${platform}
224
+ Command xcodebuild ${archiveArgs.join(" ")}
225
+ `);
226
+ const logger = createXcodebuildLogger();
227
+ logger.start(`${xcodeProject.name} (Archive)`);
228
+ try {
229
+ const process$1 = (0, execa.execa)("xcodebuild", archiveArgs, { cwd: sourceDir });
230
+ for await (const line of process$1) logger.processLine(line);
231
+ logger.stop("Archive completed successfully");
232
+ return { archivePath };
233
+ } catch (error) {
234
+ logger.stop("Archive failed", false);
235
+ throw prettifyXcodebuildError(error);
236
+ }
237
+ };
238
+ const prepareArchiveArgs = ({ archivePath, platform, schemeConfig, sourceDir, xcodeProject }) => {
239
+ const args = [
240
+ xcodeProject.isWorkspace ? "-workspace" : "-project",
241
+ path.default.join(sourceDir, xcodeProject.name),
242
+ "-scheme",
243
+ schemeConfig.scheme,
244
+ "-configuration",
245
+ schemeConfig.configuration || "Release",
246
+ "archive",
247
+ "-archivePath",
248
+ archivePath,
249
+ `HOT_UPDATER_MIN_BUNDLE_ID=${(0, __hot_updater_plugin_core.generateMinBundleId)()}`
250
+ ];
251
+ if (schemeConfig.xcconfig) args.push("-xcconfig", schemeConfig.xcconfig);
252
+ if (schemeConfig.extraParams) args.push(...schemeConfig.extraParams);
253
+ const resolvedDestinations = resolveDestinations({
254
+ destinations: schemeConfig.destination || [],
255
+ useGeneric: true
256
+ });
257
+ if (resolvedDestinations.length === 0) resolvedDestinations.push(getDefaultDestination({
258
+ deviceType: "device",
259
+ platform,
260
+ useGeneric: true
261
+ }));
262
+ for (const destination of resolvedDestinations) args.push("-destination", destination);
263
+ return args;
264
+ };
265
+
266
+ //#endregion
267
+ //#region src/builder/exportXcodeArchive.ts
268
+ const exportXcodeArchive = async ({ archivePath, sourceDir, exportExtraParams, exportOptionsPlist }) => {
269
+ const exportPath = await createRandomTmpDir();
270
+ const exportArgs = prepareExportArgs({
271
+ archivePath,
272
+ exportPath,
273
+ exportOptionsPlist,
274
+ exportExtraParams
275
+ });
276
+ const spinner = __hot_updater_cli_tools.p.spinner();
277
+ spinner.start("Exporting archive to IPA");
278
+ try {
279
+ await (0, execa.execa)("xcodebuild", exportArgs, { cwd: sourceDir });
280
+ spinner.stop("Archive exported successfully");
281
+ return { exportPath };
282
+ } catch (error) {
283
+ spinner.stop("Export failed");
284
+ throw prettifyXcodebuildError(error);
285
+ }
286
+ };
287
+ const prepareExportArgs = ({ exportPath, archivePath, exportExtraParams, exportOptionsPlist }) => {
288
+ const args = [
289
+ "-exportArchive",
290
+ "-archivePath",
291
+ archivePath,
292
+ "-exportPath",
293
+ exportPath
294
+ ];
295
+ if (exportExtraParams) args.push(...exportExtraParams);
296
+ args.push("-exportOptionsPlist", exportOptionsPlist);
297
+ return args;
298
+ };
299
+
300
+ //#endregion
301
+ //#region src/utils/assertXcodebuildExist.ts
302
+ const assertXcodebuildExist = async () => {
303
+ try {
304
+ await (0, execa.execa)("which", ["xcodebuild"]);
305
+ } catch {
306
+ __hot_updater_cli_tools.p.log.error(`${__hot_updater_cli_tools.colors.blueBright("xcodebuild")} resolve failed. Ensure that xcode is installed on your machine or xcodebuild is included in your path`);
307
+ process.exit(1);
308
+ }
309
+ };
310
+
311
+ //#endregion
312
+ //#region src/utils/enrichNativeBuildIosScheme.ts
313
+ const resolveExportOptionsPlist = (exportOptionsPlist) => {
314
+ const resolvedExportOptionsPlist = exportOptionsPlist ? path.default.isAbsolute(exportOptionsPlist) ? exportOptionsPlist : path.default.resolve((0, __hot_updater_cli_tools.getCwd)(), exportOptionsPlist) : void 0;
315
+ if (resolvedExportOptionsPlist && !fs.default.existsSync(resolvedExportOptionsPlist)) {
316
+ __hot_updater_cli_tools.p.log.error(`exportOptionsPlist doesn't exist in ${resolvedExportOptionsPlist}`);
317
+ process.exit(1);
318
+ }
319
+ return resolvedExportOptionsPlist;
320
+ };
321
+ const enrichNativeBuildIosScheme = async (scheme) => {
322
+ return {
323
+ platform: "ios",
324
+ installPods: true,
325
+ configuration: "Release",
326
+ destination: [],
327
+ ...scheme,
328
+ exportOptionsPlist: resolveExportOptionsPlist(scheme.exportOptionsPlist)
329
+ };
330
+ };
331
+
332
+ //#endregion
333
+ //#region src/buildIos.ts
334
+ const buildIos = async ({ schemeConfig: _schemeConfig }) => {
335
+ await assertXcodebuildExist();
336
+ const iosProjectRoot = path.default.join((0, __hot_updater_cli_tools.getCwd)(), "ios");
337
+ const schemeConfig = await enrichNativeBuildIosScheme(_schemeConfig);
338
+ const buildDirectory = await createRandomTmpDir();
339
+ const archiveDir = path.default.join(buildDirectory, "archive");
340
+ const exportDir = path.default.join(buildDirectory, "export");
341
+ await fs.default.promises.mkdir(archiveDir, { recursive: true });
342
+ const { archivePath } = await archiveXcodeProject({
343
+ platform: schemeConfig.platform,
344
+ schemeConfig,
345
+ sourceDir: iosProjectRoot
346
+ });
347
+ const finalArchivePath = path.default.join(archiveDir, path.default.basename(archivePath));
348
+ await fs.default.promises.cp(archivePath, finalArchivePath, { recursive: true });
349
+ const appFromArchive = extractAppFromXcarchive(finalArchivePath);
350
+ if (appFromArchive) {
351
+ await fs.default.promises.cp(appFromArchive, path.default.join(archiveDir, path.default.basename(appFromArchive)), { recursive: true });
352
+ __hot_updater_cli_tools.p.log.success(".app extracted from .xcarchive");
353
+ }
354
+ if (schemeConfig.exportOptionsPlist) {
355
+ await fs.default.promises.mkdir(exportDir, { recursive: true });
356
+ const { exportPath } = await exportXcodeArchive({
357
+ archivePath,
358
+ exportExtraParams: schemeConfig.exportExtraParams,
359
+ exportOptionsPlist: schemeConfig.exportOptionsPlist,
360
+ sourceDir: iosProjectRoot
361
+ });
362
+ await fs.default.promises.cp(exportPath, exportDir, { recursive: true });
363
+ const ipaFile = fs.default.readdirSync(exportDir).find((file) => file.endsWith(".ipa"));
364
+ if (ipaFile) return {
365
+ buildDirectory,
366
+ buildArtifactPath: path.default.join(exportDir, ipaFile)
367
+ };
368
+ throw new Error("IPA file not found after export");
369
+ }
370
+ return {
371
+ buildDirectory,
372
+ buildArtifactPath: finalArchivePath
373
+ };
374
+ };
375
+ const extractAppFromXcarchive = (archivePath) => {
376
+ const productsPath = path.default.join(archivePath, "Products", "Applications");
377
+ if (!fs.default.existsSync(productsPath)) return;
378
+ const appFile = fs.default.readdirSync(productsPath).find((file) => file.endsWith(".app"));
379
+ if (appFile) return path.default.join(productsPath, appFile);
380
+ __hot_updater_cli_tools.p.log.warn("Failed to extract .app from xcarchive");
381
+ };
382
+
383
+ //#endregion
384
+ //#region src/builder/buildXcodeProject.ts
385
+ const buildXcodeProject = async ({ sourceDir, platform, scheme, configuration, deviceType, udid, installPods, extraParams }) => {
386
+ const xcodeProject = await parseXcodeProjectInfo(sourceDir);
387
+ if (installPods ?? true) await installPodsIfNeeded(sourceDir);
388
+ const derivedDataPath = await createRandomTmpDir();
389
+ const buildArgs = prepareBuildArgs({
390
+ configuration,
391
+ derivedDataPath,
392
+ deviceType,
393
+ extraParams,
394
+ platform,
395
+ scheme,
396
+ sourceDir,
397
+ udid,
398
+ xcodeProject
399
+ });
400
+ __hot_updater_cli_tools.p.log.info(`Xcode Build Settings:
401
+ Project ${xcodeProject.name}
402
+ Scheme ${scheme}
403
+ Configuration ${configuration}
404
+ Platform ${platform}
405
+ Device Type ${deviceType}
406
+ Command xcodebuild ${buildArgs.join(" ")}
407
+ `);
408
+ const logger = createXcodebuildLogger();
409
+ logger.start(`${xcodeProject.name} (Build)`);
410
+ try {
411
+ const process$1 = (0, execa.execa)("xcodebuild", buildArgs, { cwd: sourceDir });
412
+ for await (const line of process$1) logger.processLine(line);
413
+ logger.stop("Build completed successfully");
414
+ return await getBuildSettings({
415
+ configuration,
416
+ derivedDataPath,
417
+ deviceType,
418
+ platform,
419
+ scheme,
420
+ sourceDir,
421
+ udid,
422
+ xcodeProject
423
+ });
424
+ } catch (error) {
425
+ logger.stop("Build failed", false);
426
+ throw prettifyXcodebuildError(error);
427
+ }
428
+ };
429
+ const prepareBuildArgs = ({ xcodeProject, sourceDir, platform, scheme, configuration, deviceType, udid, derivedDataPath, extraParams }) => {
430
+ const sdk = deviceType === "simulator" ? platformConfigs[platform].simulatorSdk : platformConfigs[platform].deviceSdk;
431
+ const destination = udid ? `id=${udid}` : deviceType === "simulator" ? platformConfigs[platform].simulatorDestination : platformConfigs[platform].deviceDestination;
432
+ const args = [
433
+ xcodeProject.isWorkspace ? "-workspace" : "-project",
434
+ path.default.join(sourceDir, xcodeProject.name),
435
+ "-scheme",
436
+ scheme,
437
+ "-configuration",
438
+ configuration,
439
+ "-sdk",
440
+ sdk,
441
+ "-destination",
442
+ destination,
443
+ "-derivedDataPath",
444
+ derivedDataPath,
445
+ `HOT_UPDATER_MIN_BUNDLE_ID=${(0, __hot_updater_plugin_core.generateMinBundleId)()}`,
446
+ "build"
447
+ ];
448
+ if (extraParams) args.push(...extraParams);
449
+ return args;
450
+ };
451
+ const getBuildSettings = async ({ xcodeProject, sourceDir, platform, scheme, configuration, deviceType, udid, derivedDataPath }) => {
452
+ const sdk = deviceType === "simulator" ? platformConfigs[platform].simulatorSdk : platformConfigs[platform].deviceSdk;
453
+ const destination = udid ? `id=${udid}` : deviceType === "simulator" ? platformConfigs[platform].simulatorDestination : platformConfigs[platform].deviceDestination;
454
+ const { stdout: buildSettings } = await (0, execa.execa)("xcodebuild", [
455
+ xcodeProject.isWorkspace ? "-workspace" : "-project",
456
+ xcodeProject.name,
457
+ "-scheme",
458
+ scheme,
459
+ "-configuration",
460
+ configuration,
461
+ "-sdk",
462
+ sdk,
463
+ "-destination",
464
+ destination,
465
+ "-derivedDataPath",
466
+ derivedDataPath,
467
+ "-showBuildSettings",
468
+ "-json"
469
+ ], { cwd: sourceDir });
470
+ const settings = JSON.parse(buildSettings).filter(({ target }) => target !== "React" && target !== "React-Core");
471
+ if (settings.length === 0) throw new Error("Failed to get build settings for your project");
472
+ const targetSettings = settings[0].buildSettings;
473
+ const wrapperExtension = targetSettings.WRAPPER_EXTENSION;
474
+ if (wrapperExtension !== "app" && wrapperExtension !== "framework") throw new Error(`Expected wrapper extension to be "app" or "framework" but found: ${wrapperExtension}`);
475
+ const targetBuildDir = targetSettings.TARGET_BUILD_DIR;
476
+ const executableFolderPath = targetSettings.EXECUTABLE_FOLDER_PATH;
477
+ const fullProductName = targetSettings.FULL_PRODUCT_NAME;
478
+ const infoPlistPath = targetSettings.INFOPLIST_PATH;
479
+ if (!targetBuildDir || !executableFolderPath || !fullProductName) throw new Error("Failed to get build paths from build settings");
480
+ return {
481
+ appPath: path.default.join(targetBuildDir, executableFolderPath),
482
+ infoPlistPath: path.default.join(targetBuildDir, infoPlistPath)
483
+ };
484
+ };
485
+
486
+ //#endregion
487
+ //#region src/runner/deviceRunner.ts
488
+ const installAndLaunchOnDevice = async ({ device, appPath, options }) => {
489
+ await installOnDevice({
490
+ device,
491
+ appPath,
492
+ options
493
+ });
494
+ if (options.launch !== false) await launchAppOnDevice({
495
+ device,
496
+ options
497
+ });
498
+ };
499
+ const installOnDevice = async ({ device, appPath, options }) => {
500
+ const deviceCtlArgs = [
501
+ "devicectl",
502
+ "device",
503
+ "install",
504
+ "app",
505
+ "--device",
506
+ device.udid,
507
+ appPath
508
+ ];
509
+ const spinner = __hot_updater_cli_tools.p.spinner();
510
+ spinner.start(`Installing app on ${device.name}`);
511
+ try {
512
+ await (0, execa.execa)("xcrun", deviceCtlArgs, { cwd: options.sourceDir });
513
+ spinner.stop(`Successfully installed app on ${device.name}`);
514
+ } catch (error) {
515
+ spinner.stop(`Failed to install app on ${device.name}`);
516
+ throw new Error(`Failed to install the app on ${device.name}: ${error}`);
517
+ }
518
+ };
519
+ const launchAppOnDevice = async ({ device, options }) => {
520
+ const deviceCtlArgs = [
521
+ "devicectl",
522
+ "device",
523
+ "process",
524
+ "launch",
525
+ "--device",
526
+ device.udid,
527
+ options.bundleIdentifier
528
+ ];
529
+ const spinner = __hot_updater_cli_tools.p.spinner();
530
+ spinner.start(`Launching app on ${device.name}`);
531
+ try {
532
+ await (0, execa.execa)("xcrun", deviceCtlArgs, { cwd: options.sourceDir });
533
+ spinner.stop(`Successfully launched app on ${device.name}`);
534
+ } catch (error) {
535
+ spinner.stop(`Failed to launch app on ${device.name}`);
536
+ throw new Error(`Failed to launch the app on ${device.name}: ${error}`);
537
+ }
538
+ };
539
+
540
+ //#endregion
541
+ //#region src/runner/simulatorRunner.ts
542
+ const installAndLaunchOnSimulator = async ({ device, appPath, options }) => {
543
+ await launchSimulator({ device });
544
+ await installOnSimulator({
545
+ device,
546
+ appPath,
547
+ options
548
+ });
549
+ if (options.launch !== false) await launchAppOnSimulator({
550
+ device,
551
+ options
552
+ });
553
+ };
554
+ const launchSimulator = async ({ device }) => {
555
+ if (device.type !== "simulator") return;
556
+ const { stdout: activeDeveloperDir } = await (0, execa.execa)("xcode-select", ["-p"]);
557
+ await (0, execa.execa)("open", [
558
+ `${activeDeveloperDir}/Applications/Simulator.app`,
559
+ "--args",
560
+ "-CurrentDeviceUDID",
561
+ device.udid
562
+ ]);
563
+ if (device.state !== "Booted") await bootSimulator({ device });
564
+ };
565
+ const installOnSimulator = async ({ device, appPath, options }) => {
566
+ const spinner = __hot_updater_cli_tools.p.spinner();
567
+ spinner.start(`Installing app on "${device.name}"`);
568
+ try {
569
+ await (0, execa.execa)("xcrun", [
570
+ "simctl",
571
+ "install",
572
+ device.udid,
573
+ appPath
574
+ ], { cwd: options.sourceDir });
575
+ spinner.stop(`Successfully installed app on "${device.name}"`);
576
+ } catch (error) {
577
+ spinner.stop(`Failed to install app on "${device.name}"`);
578
+ throw new Error(`Failed to install the app on simulator: ${error}`);
579
+ }
580
+ };
581
+ const launchAppOnSimulator = async ({ device, options }) => {
582
+ const spinner = __hot_updater_cli_tools.p.spinner();
583
+ spinner.start(`Launching app on "${device.name}"`);
584
+ try {
585
+ await (0, execa.execa)("xcrun", [
586
+ "simctl",
587
+ "launch",
588
+ device.udid,
589
+ options.bundleIdentifier
590
+ ], { cwd: options.sourceDir });
591
+ spinner.stop(`Successfully launched app on "${device.name}"`);
592
+ } catch (error) {
593
+ spinner.stop(`Failed to launch app on "${device.name}"`);
594
+ throw new Error(`Failed to launch the app on simulator: ${error}`);
595
+ }
596
+ };
597
+ const bootSimulator = async ({ device }) => {
598
+ try {
599
+ await (0, execa.execa)("xcrun", [
600
+ "simctl",
601
+ "boot",
602
+ device.udid
603
+ ]);
604
+ } catch (error) {
605
+ if ((error instanceof Error ? error.message : String(error)).includes("Unable to boot device in current state: Booted")) {
606
+ __hot_updater_cli_tools.p.log.info(`Simulator ${device.udid} already booted. Skipping.`);
607
+ return;
608
+ }
609
+ throw new Error(`Failed to boot simulator: ${error}`);
610
+ }
611
+ };
612
+
613
+ //#endregion
614
+ //#region src/utils/device.ts
615
+ /**
616
+ * Parses devicectl output to Device array
617
+ * @param devicectlOutput - Raw devicectl JSON output
618
+ * @returns Array of parsed devices
619
+ */
620
+ const parseDevicectlList = (devicectlOutput) => {
621
+ return devicectlOutput.map((device) => ({
622
+ name: device.deviceProperties.name,
623
+ udid: device.hardwareProperties.udid,
624
+ version: `${device.hardwareProperties.platform} ${device.deviceProperties.osVersionNumber}`,
625
+ platform: getPlatformFromOsVersion(device.hardwareProperties.platform),
626
+ state: device.deviceProperties.bootState === "booted" ? "Booted" : "Shutdown",
627
+ type: "device"
628
+ }));
629
+ };
630
+ /**
631
+ * Gets physical iOS devices using devicectl
632
+ */
633
+ const getDevices = async () => {
634
+ const tmpPath = path.default.resolve(node_os.default.tmpdir(), "iosPhysicalDevices.json");
635
+ try {
636
+ await (0, execa.execa)("xcrun", [
637
+ "devicectl",
638
+ "list",
639
+ "devices",
640
+ "-j",
641
+ tmpPath
642
+ ]);
643
+ return parseDevicectlList(JSON.parse(fs.default.readFileSync(tmpPath, "utf8")).result.devices);
644
+ } catch (error) {
645
+ throw new Error(`Failed to get devices: ${error}`);
646
+ }
647
+ };
648
+ /**
649
+ * Gets iOS simulators using simctl with JSON output
650
+ */
651
+ const getSimulators = async () => {
652
+ try {
653
+ const { stdout } = await (0, execa.execa)("xcrun", [
654
+ "simctl",
655
+ "list",
656
+ "devices",
657
+ "available",
658
+ "-j"
659
+ ]);
660
+ return parseSimctlJson(JSON.parse(stdout));
661
+ } catch (error) {
662
+ throw new Error(`Failed to get simulators: ${error}`);
663
+ }
664
+ };
665
+ /**
666
+ * Parses simctl JSON output to Device array
667
+ */
668
+ const parseSimctlJson = (simctlOutput) => {
669
+ const devices = [];
670
+ for (const [runtime, runtimeDevices] of Object.entries(simctlOutput.devices)) {
671
+ const osVersion = runtime.replace("com.apple.CoreSimulator.SimRuntime.", "").replace(/-/g, " ");
672
+ const platform = getPlatformFromOsVersion(osVersion.split(" ")[0]);
673
+ if (!platform) continue;
674
+ for (const device of runtimeDevices) if (device.isAvailable) devices.push({
675
+ name: device.name,
676
+ udid: device.udid,
677
+ version: osVersion,
678
+ platform,
679
+ state: device.state,
680
+ type: "simulator"
681
+ });
682
+ }
683
+ return devices;
684
+ };
685
+ /**
686
+ * Maps OS version string to Apple platform
687
+ * @param osVersion - OS version string from devicectl/simctl
688
+ * @returns Apple platform or undefined if unknown
689
+ */
690
+ const getPlatformFromOsVersion = (osVersion) => {
691
+ switch (osVersion) {
692
+ case "iOS": return "ios";
693
+ default: return "ios";
694
+ }
695
+ };
696
+ /**
697
+ * Lists devices and simulators for a specific platform
698
+ * @param platform - Apple platform to filter by
699
+ * @param options - Filtering options
700
+ * @returns Array of devices filtered by options
701
+ */
702
+ const listDevices = async (platform, options = {}) => {
703
+ const spinner = __hot_updater_cli_tools.p.spinner();
704
+ spinner.start(`Discovering ${platform} devices`);
705
+ try {
706
+ const [simulators, devices] = await Promise.all([getSimulators(), getDevices().catch(() => [])]);
707
+ let allDevices = [...devices, ...simulators].filter((device) => device.platform === platform);
708
+ if (options.deviceType) allDevices = allDevices.filter((d) => d.type === options.deviceType);
709
+ if (options.state && options.state !== "all") allDevices = allDevices.filter((d) => d.state === options.state);
710
+ spinner.stop(`Found ${allDevices.length} ${platform} devices`);
711
+ return allDevices;
712
+ } catch (error) {
713
+ spinner.stop(`Failed to discover ${platform} devices`);
714
+ throw error;
715
+ }
716
+ };
717
+ const matchingDevice = (devices, deviceArg, options = {}) => {
718
+ let filtered = [...devices];
719
+ if (options.deviceType) filtered = filtered.filter((d) => d.type === options.deviceType);
720
+ if (options.bootedOnly) filtered = filtered.filter((d) => d.state === "Booted");
721
+ const byName = filtered.find((d) => d.name === deviceArg);
722
+ if (byName) return byName;
723
+ const byFormattedName = filtered.find((d) => formatDeviceName(d) === deviceArg);
724
+ if (byFormattedName) return byFormattedName;
725
+ const byUdid = filtered.find((d) => d.udid === deviceArg);
726
+ if (byUdid) return byUdid;
727
+ };
728
+ const formatDeviceName = (device) => {
729
+ return device.version ? `${device.name} (${device.version})` : device.name;
730
+ };
731
+ const selectTargetDevice = async ({ platform, interactive, deviceOption }) => {
732
+ const availableDevices = await listDevices(platform);
733
+ if (deviceOption === true && !interactive) {
734
+ __hot_updater_cli_tools.p.log.error("you can't select device manually with non-interactive cli mode(without -i option).");
735
+ process.exit(1);
736
+ }
737
+ if (typeof deviceOption === "string") {
738
+ const matchedDevice = matchingDevice(availableDevices, deviceOption);
739
+ if (!matchedDevice) {
740
+ __hot_updater_cli_tools.p.log.error(`device '${deviceOption}' isn't included in the attached devices.`);
741
+ process.exit(1);
742
+ }
743
+ return matchedDevice;
744
+ }
745
+ if (interactive) {
746
+ if (!availableDevices.length) {
747
+ __hot_updater_cli_tools.p.log.error("you passed -d option but there is no attached devices.");
748
+ process.exit(1);
749
+ }
750
+ const device = await __hot_updater_cli_tools.p.select({
751
+ message: "Target Device",
752
+ options: availableDevices.map((d) => ({
753
+ value: d,
754
+ label: `${d.name} (${d.type}) - ${d.state}`,
755
+ hint: d.udid
756
+ }))
757
+ });
758
+ if (__hot_updater_cli_tools.p.isCancel(device)) process.exit(1);
759
+ return device;
760
+ }
761
+ const bootedDevices = await listDevices(platform, { state: "Booted" });
762
+ if (bootedDevices.length > 0) return bootedDevices[0];
763
+ };
764
+ const Device = {
765
+ listDevices,
766
+ selectTargetDevice
767
+ };
768
+
769
+ //#endregion
770
+ //#region src/runIos.ts
771
+ const runIos = async ({ schemeConfig: _schemeConfig, runOption }) => {
772
+ const { interactive, device: deviceOption } = runOption;
773
+ const iosProjectPath = path.default.join((0, __hot_updater_cli_tools.getCwd)(), "ios");
774
+ const schemeConfig = await enrichNativeBuildIosScheme(_schemeConfig);
775
+ const device = await Device.selectTargetDevice({
776
+ platform: "ios",
777
+ deviceOption,
778
+ interactive
779
+ });
780
+ if (!device) {
781
+ const simulators = await Device.listDevices("ios", { deviceType: "simulator" });
782
+ if (simulators.length === 0) {
783
+ __hot_updater_cli_tools.p.log.error("No simulators found. Please create a simulator in Xcode.");
784
+ process.exit(1);
785
+ }
786
+ __hot_updater_cli_tools.p.log.info(`Using first available simulator: ${simulators[0].name}`);
787
+ const selectedDevice = simulators[0];
788
+ __hot_updater_cli_tools.p.log.info(`Building for ${selectedDevice.name}...`);
789
+ const result$1 = await buildXcodeProject({
790
+ sourceDir: iosProjectPath,
791
+ platform: schemeConfig.platform,
792
+ scheme: schemeConfig.scheme,
793
+ configuration: schemeConfig.configuration,
794
+ deviceType: selectedDevice.type,
795
+ udid: selectedDevice.udid,
796
+ installPods: schemeConfig.installPods,
797
+ extraParams: schemeConfig.extraParams
798
+ });
799
+ const runnerOptions$1 = {
800
+ bundleIdentifier: schemeConfig.bundleIdentifier,
801
+ sourceDir: iosProjectPath,
802
+ launch: true,
803
+ infoPlistPath: result$1.infoPlistPath
804
+ };
805
+ await installAndLaunchOnSimulator({
806
+ device: selectedDevice,
807
+ appPath: result$1.appPath,
808
+ options: runnerOptions$1
809
+ });
810
+ __hot_updater_cli_tools.p.outro("Success 🎉");
811
+ return result$1;
812
+ }
813
+ __hot_updater_cli_tools.p.log.info(`Building for ${device.name}...`);
814
+ const result = await buildXcodeProject({
815
+ sourceDir: iosProjectPath,
816
+ platform: schemeConfig.platform,
817
+ scheme: schemeConfig.scheme,
818
+ configuration: schemeConfig.configuration,
819
+ deviceType: device.type,
820
+ udid: device.udid,
821
+ installPods: schemeConfig.installPods,
822
+ extraParams: schemeConfig.extraParams
823
+ });
824
+ const runnerOptions = {
825
+ bundleIdentifier: schemeConfig.bundleIdentifier,
826
+ sourceDir: iosProjectPath,
827
+ launch: true
828
+ };
829
+ if (device.type === "simulator") await installAndLaunchOnSimulator({
830
+ device,
831
+ appPath: result.appPath,
832
+ options: {
833
+ ...runnerOptions,
834
+ infoPlistPath: result.infoPlistPath
835
+ }
836
+ });
837
+ else await installAndLaunchOnDevice({
838
+ device,
839
+ appPath: result.appPath,
840
+ options: runnerOptions
841
+ });
842
+ __hot_updater_cli_tools.p.outro("Success 🎉");
843
+ return result;
844
+ };
845
+
846
+ //#endregion
847
+ exports.buildIos = buildIos;
848
+ exports.runIos = runIos;