@absolutejs/absolute 0.20.0-beta.41 → 0.20.0-beta.43

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/cli/index.js CHANGED
@@ -935,7 +935,7 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
935
935
  slug: config.appId.toLowerCase().replaceAll(".", "-"),
936
936
  version: config.iosVersion ?? "0.1.0"
937
937
  }
938
- }), metroConfig = (projectRoot) => `${EXPO_GENERATED_HEADER}const { getDefaultConfig } = require('expo/metro-config');
938
+ }), expoDynamicAppConfig, expoDevelopmentCaPlugin, metroConfig = (projectRoot) => `${EXPO_GENERATED_HEADER}const { getDefaultConfig } = require('expo/metro-config');
939
939
  const path = require('node:path');
940
940
 
941
941
  const projectRoot = __dirname;
@@ -1223,8 +1223,13 @@ node_modules/
1223
1223
  `
1224
1224
  ],
1225
1225
  [join5(project, "app.json"), jsonSource(expoAppConfig(config))],
1226
+ [join5(project, "app.config.js"), expoDynamicAppConfig],
1226
1227
  [join5(project, "package.json"), jsonSource(expoPackage())],
1227
1228
  [join5(project, "metro.config.js"), metroConfig(projectRoot)],
1229
+ [
1230
+ join5(project, "plugins", "withAbsoluteDevelopmentCa.js"),
1231
+ expoDevelopmentCaPlugin
1232
+ ],
1228
1233
  [
1229
1234
  join5(project, "tsconfig.json"),
1230
1235
  jsonSource(expoTsConfig(projectRoot, project))
@@ -1360,6 +1365,57 @@ export const materializeAbsoluteWebBundle = async () => {
1360
1365
  return { appBuild, assets: assets.length, bundleId, path: destination };
1361
1366
  };
1362
1367
  var init_expoProject = __esm(() => {
1368
+ expoDynamicAppConfig = `${EXPO_GENERATED_HEADER}const config = require('./app.json');
1369
+
1370
+ if (process.env.ABSOLUTE_EXPO_DEVELOPMENT === '1' && process.env.ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH) {
1371
+ config.expo.plugins = [
1372
+ ...config.expo.plugins,
1373
+ './plugins/withAbsoluteDevelopmentCa'
1374
+ ];
1375
+ }
1376
+
1377
+ module.exports = config;
1378
+ `;
1379
+ expoDevelopmentCaPlugin = `${EXPO_GENERATED_HEADER}const { AndroidConfig, withAndroidManifest, withDangerousMod } = require('expo/config-plugins');
1380
+ const { copyFile, mkdir } = require('node:fs/promises');
1381
+ const path = require('node:path');
1382
+
1383
+ const NETWORK_CONFIG = [
1384
+ '<?xml version="1.0" encoding="utf-8"?>',
1385
+ '<network-security-config>',
1386
+ ' <debug-overrides>',
1387
+ ' <trust-anchors>',
1388
+ ' <certificates src="@raw/absolutejs_dev_ca" />',
1389
+ ' </trust-anchors>',
1390
+ ' </debug-overrides>',
1391
+ '</network-security-config>',
1392
+ ''
1393
+ ].join('\\n');
1394
+
1395
+ const withAbsoluteDevelopmentCa = config => {
1396
+ config = withAndroidManifest(config, value => {
1397
+ const application = AndroidConfig.Manifest.getMainApplicationOrThrow(value.modResults);
1398
+ application.$['android:networkSecurityConfig'] = '@xml/absolutejs_dev_network_security';
1399
+ return value;
1400
+ });
1401
+ return withDangerousMod(config, ['android', async value => {
1402
+ const certificate = process.env.ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH;
1403
+ if (!certificate) throw new Error('AbsoluteJS Expo HTTPS development requires its development CA path.');
1404
+ const resources = path.join(value.modRequest.platformProjectRoot, 'app', 'src', 'main', 'res');
1405
+ await Promise.all([
1406
+ mkdir(path.join(resources, 'raw'), { recursive: true }),
1407
+ mkdir(path.join(resources, 'xml'), { recursive: true })
1408
+ ]);
1409
+ await Promise.all([
1410
+ copyFile(certificate, path.join(resources, 'raw', 'absolutejs_dev_ca.pem')),
1411
+ require('node:fs/promises').writeFile(path.join(resources, 'xml', 'absolutejs_dev_network_security.xml'), NETWORK_CONFIG)
1412
+ ]);
1413
+ return value;
1414
+ }]);
1415
+ };
1416
+
1417
+ module.exports = withAbsoluteDevelopmentCa;
1418
+ `;
1363
1419
  layoutSource = `${EXPO_GENERATED_HEADER}import { Stack } from 'expo-router';
1364
1420
 
1365
1421
  export default function AbsoluteLayout() {
@@ -1411,6 +1467,97 @@ export default AbsoluteWebHost;
1411
1467
  `;
1412
1468
  });
1413
1469
 
1470
+ // src/mobile/iosPhysicalDeviceTransport.ts
1471
+ import { randomUUID, X509Certificate } from "crypto";
1472
+ import { createServer as createServer2 } from "http";
1473
+ import {
1474
+ connect as connectTcp,
1475
+ createServer as createTcpServer,
1476
+ isIP
1477
+ } from "net";
1478
+ import { readFile as readFile2 } from "fs/promises";
1479
+ var closeServer = (server) => new Promise((resolve4, reject) => {
1480
+ server.close((error) => {
1481
+ if (error)
1482
+ reject(error);
1483
+ else
1484
+ resolve4();
1485
+ });
1486
+ }), listen = (server, port) => new Promise((resolve4, reject) => {
1487
+ server.once("error", reject);
1488
+ server.listen(port, "0.0.0.0", () => {
1489
+ server.off("error", reject);
1490
+ const address = server.address();
1491
+ if (!address || typeof address === "string") {
1492
+ reject(new Error("Could not determine the iOS device helper port."));
1493
+ return;
1494
+ }
1495
+ resolve4(address.port);
1496
+ });
1497
+ }), findEphemeralPort = async () => {
1498
+ const probe = createTcpServer();
1499
+ const port = await new Promise((resolve4, reject) => {
1500
+ probe.once("error", reject);
1501
+ probe.listen(0, "127.0.0.1", () => {
1502
+ const address = probe.address();
1503
+ if (!address || typeof address === "string") {
1504
+ reject(new Error("Could not allocate the iOS CA enrollment port."));
1505
+ return;
1506
+ }
1507
+ resolve4(address.port);
1508
+ });
1509
+ });
1510
+ await closeServer(probe);
1511
+ return port;
1512
+ }, normalizeAbsoluteIosDeviceHost = (value) => {
1513
+ const normalized = value.trim();
1514
+ if (!normalized || normalized.length > 253 || /[\0\s/?#]/u.test(normalized))
1515
+ throw new TypeError("Physical iOS development requires a valid LAN host.");
1516
+ return normalized;
1517
+ }, normalizeAbsoluteIosDeviceIdentifier = (value) => {
1518
+ const normalized = value.trim();
1519
+ if (!normalized || normalized.length > 256 || /[\0\r\n]/u.test(normalized))
1520
+ throw new TypeError("--ios-device requires a valid Xcode device identifier or name.");
1521
+ return normalized;
1522
+ }, urlForHost = (protocol, host, port) => {
1523
+ const url = new URL(`${protocol}://localhost:${port}`);
1524
+ const normalizedHost = normalizeAbsoluteIosDeviceHost(host);
1525
+ url.hostname = isIP(normalizedHost) === 6 ? `[${normalizedHost}]` : normalizedHost;
1526
+ return url;
1527
+ }, startAbsoluteIosCaEnrollmentServer = async (options) => {
1528
+ const certificate = new X509Certificate(await readFile2(options.certificateAuthorityPath));
1529
+ const certificateBytes = certificate.raw;
1530
+ const token = randomUUID().replaceAll("-", "");
1531
+ const certificatePath = `/${token}/absolutejs-development-ca.cer`;
1532
+ const server = createServer2((request, response) => {
1533
+ if (request.method !== "GET" || request.url !== certificatePath) {
1534
+ response.writeHead(404, {
1535
+ "Cache-Control": "no-store",
1536
+ "Content-Type": "text/plain; charset=utf-8"
1537
+ });
1538
+ response.end("Not found.");
1539
+ return;
1540
+ }
1541
+ response.writeHead(200, {
1542
+ "Cache-Control": "no-store",
1543
+ "Content-Disposition": 'attachment; filename="absolutejs-development-ca.cer"',
1544
+ "Content-Length": String(certificateBytes.byteLength),
1545
+ "Content-Type": "application/x-x509-ca-cert",
1546
+ "X-Content-Type-Options": "nosniff"
1547
+ });
1548
+ response.end(certificateBytes);
1549
+ });
1550
+ const port = await findEphemeralPort();
1551
+ await listen(server, port);
1552
+ const url = urlForHost("http", options.displayHost, port);
1553
+ url.pathname = certificatePath;
1554
+ return {
1555
+ url: url.href,
1556
+ close: () => closeServer(server)
1557
+ };
1558
+ };
1559
+ var init_iosPhysicalDeviceTransport = () => {};
1560
+
1414
1561
  // src/mobile/nativeAuth.ts
1415
1562
  import { readFileSync as readFileSync5 } from "fs";
1416
1563
  import { join as join7 } from "path";
@@ -1459,7 +1606,7 @@ var init_nativeAuth = __esm(() => {
1459
1606
  var {$: $2 } = globalThis.Bun;
1460
1607
  import { execSync } from "child_process";
1461
1608
  import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1462
- import { createServer as createServer2 } from "net";
1609
+ import { createServer as createServer3 } from "net";
1463
1610
  import { resolve as resolve4 } from "path";
1464
1611
  var COMPOSE_PATH = "db/docker-compose.db.yml", DEFAULT_SERVER_ENTRY = "src/backend/server.ts", isWSLEnvironment = () => {
1465
1612
  try {
@@ -1473,7 +1620,7 @@ var COMPOSE_PATH = "db/docker-compose.db.yml", DEFAULT_SERVER_ENTRY = "src/backe
1473
1620
  process.kill(pid, "SIGTERM");
1474
1621
  } catch {}
1475
1622
  }, findFreePort = () => new Promise((_resolve, reject) => {
1476
- const server = createServer2();
1623
+ const server = createServer3();
1477
1624
  server.unref();
1478
1625
  server.once("error", reject);
1479
1626
  server.listen(0, "127.0.0.1", () => {
@@ -1809,7 +1956,7 @@ var init_emulatorDoctor = __esm(() => {
1809
1956
  });
1810
1957
 
1811
1958
  // src/mobile/capacitorProject.ts
1812
- import { access as access4, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1959
+ import { access as access4, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1813
1960
  import { relative as relative2, resolve as resolve5 } from "path";
1814
1961
  var CONFIG_FILE = "capacitor.config.ts", portableRelative2 = (root, path) => relative2(root, path).replaceAll("\\", "/"), capacitorConfigSource = (config, projectRoot) => `import type { CapacitorConfig } from '@capacitor/cli';
1815
1962
 
@@ -1838,7 +1985,7 @@ export default config;
1838
1985
  const destination = resolve5(projectRoot, CONFIG_FILE);
1839
1986
  const source = capacitorConfigSource(config, projectRoot);
1840
1987
  if (await exists2(destination)) {
1841
- const current = await readFile2(destination, "utf8");
1988
+ const current = await readFile3(destination, "utf8");
1842
1989
  if (current === source)
1843
1990
  return { changed: false, path: destination };
1844
1991
  if (!options.force) {
@@ -1858,7 +2005,7 @@ import {
1858
2005
  copyFile,
1859
2006
  lstat,
1860
2007
  mkdir as mkdir2,
1861
- readFile as readFile3,
2008
+ readFile as readFile4,
1862
2009
  readdir as readdir2,
1863
2010
  readlink,
1864
2011
  realpath,
@@ -1866,7 +2013,7 @@ import {
1866
2013
  rm as rm2,
1867
2014
  writeFile as writeFile3
1868
2015
  } from "fs/promises";
1869
- import { createHash as createHash2, randomUUID } from "crypto";
2016
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
1870
2017
  import {
1871
2018
  dirname as dirname4,
1872
2019
  isAbsolute,
@@ -2072,9 +2219,9 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2072
2219
  String(identity)
2073
2220
  ]))
2074
2221
  };
2075
- }, readNativeCache = async (projectRoot) => readFile3(nativeCachePath(projectRoot), "utf8").then((source) => parseNativeCache(JSON.parse(source))).catch(() => null), writeNativeCache = async (projectRoot, cache) => {
2222
+ }, readNativeCache = async (projectRoot) => readFile4(nativeCachePath(projectRoot), "utf8").then((source) => parseNativeCache(JSON.parse(source))).catch(() => null), writeNativeCache = async (projectRoot, cache) => {
2076
2223
  const destination = nativeCachePath(projectRoot);
2077
- const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
2224
+ const temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
2078
2225
  await mkdir2(dirname4(destination), { recursive: true });
2079
2226
  try {
2080
2227
  await writeFile3(temporary, `${JSON.stringify(cache, null, "\t")}
@@ -2088,7 +2235,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2088
2235
  });
2089
2236
  }
2090
2237
  }, nativeDependencySources = async (nativeDirectory) => {
2091
- const settings = await readFile3(join9(nativeDirectory, "capacitor.settings.gradle"), "utf8");
2238
+ const settings = await readFile4(join9(nativeDirectory, "capacitor.settings.gradle"), "utf8");
2092
2239
  const pattern = new RegExp(CAPACITOR_PROJECT_DIRECTORY_PATTERN.source, CAPACITOR_PROJECT_DIRECTORY_PATTERN.flags);
2093
2240
  const dependencies = [...settings.matchAll(pattern)].map((match) => ({
2094
2241
  name: (match[1] ?? "").slice(1).replaceAll(/[^a-zA-Z0-9_.-]/gu, "_"),
@@ -2119,7 +2266,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2119
2266
  return [];
2120
2267
  const [metadata, contents] = await Promise.all([
2121
2268
  lstat(path),
2122
- readFile3(path)
2269
+ readFile4(path)
2123
2270
  ]);
2124
2271
  const contentDigest = createHash2("sha256").update(contents).digest("hex");
2125
2272
  return [
@@ -2189,7 +2336,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2189
2336
  await rm2(paths.root, { force: true, recursive: true });
2190
2337
  return false;
2191
2338
  }
2192
- const journal = await readFile3(paths.journal, "utf8").then((source) => parseJournal(JSON.parse(source))).catch(() => null);
2339
+ const journal = await readFile4(paths.journal, "utf8").then((source) => parseJournal(JSON.parse(source))).catch(() => null);
2193
2340
  if (!journal || !isInside(projectRoot, journal.nativeConfigPath) || !isInside(paths.root, journal.backupPath) || journal.nativeManifestPath !== undefined && !isInside(projectRoot, journal.nativeManifestPath) || journal.manifestBackupPath !== undefined && !isInside(paths.root, journal.manifestBackupPath) || journal.projectedFiles?.some((file) => !isInside(projectRoot, file.path) || file.backupPath !== undefined && !isInside(paths.root, file.backupPath))) {
2194
2341
  throw new Error(`Refusing unsafe or invalid mobile dev journal at ${paths.journal}.`);
2195
2342
  }
@@ -2248,8 +2395,8 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2248
2395
  }, writeDevConfig = async (projectRoot, nativeConfigPath, nativeManifestPath, port, https, entry, embeddedBundle, serverHost, certificateAuthorityPath) => {
2249
2396
  const paths = journalPaths(projectRoot);
2250
2397
  await repairAbsoluteAndroidDevSession(projectRoot);
2251
- const source = await readFile3(nativeConfigPath, "utf8");
2252
- const manifestSource = await readFile3(nativeManifestPath, "utf8");
2398
+ const source = await readFile4(nativeConfigPath, "utf8");
2399
+ const manifestSource = await readFile4(nativeManifestPath, "utf8");
2253
2400
  const parsed = JSON.parse(source);
2254
2401
  if (!isRecord(parsed)) {
2255
2402
  throw new Error(`Invalid Capacitor native config at ${nativeConfigPath}.`);
@@ -2302,7 +2449,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2302
2449
  mkdir2(dirname4(networkConfigPath), { recursive: true })
2303
2450
  ]);
2304
2451
  await copyFile(certificateAuthorityPath, caPath);
2305
- const existingNetworkConfigSource = existingNetworkConfig ? await readFile3(networkConfigPath, "utf8") : `<?xml version="1.0" encoding="utf-8"?>
2452
+ const existingNetworkConfigSource = existingNetworkConfig ? await readFile4(networkConfigPath, "utf8") : `<?xml version="1.0" encoding="utf-8"?>
2306
2453
  <network-security-config>
2307
2454
  </network-security-config>
2308
2455
  `;
@@ -2355,7 +2502,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2355
2502
  return result.stdout.trim();
2356
2503
  }, mirroredCapacitorDependencies = async (project, capture) => {
2357
2504
  const settingsPath = join9(project.nativeDirectory, "capacitor.settings.gradle");
2358
- const settings = await readFile3(settingsPath, "utf8");
2505
+ const settings = await readFile4(settingsPath, "utf8");
2359
2506
  const dependencies = [];
2360
2507
  const rewrittenSettings = settings.replace(CAPACITOR_PROJECT_DIRECTORY_PATTERN, (_statement, projectName, sourcePath) => {
2361
2508
  const name = projectName.slice(1).replaceAll(/[^a-zA-Z0-9_.-]/gu, "_");
@@ -2855,7 +3002,7 @@ var init_androidEmulatorController = __esm(() => {
2855
3002
 
2856
3003
  // src/mobile/emulatorInstaller.ts
2857
3004
  import { createHash as createHash3 } from "crypto";
2858
- import { cp as cp2, mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile4, rm as rm3 } from "fs/promises";
3005
+ import { cp as cp2, mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile5, rm as rm3 } from "fs/promises";
2859
3006
  import { tmpdir } from "os";
2860
3007
  import { basename as basename3, join as join10 } from "path";
2861
3008
  var ANDROID_API = 36, ANDROID_BUILD_TOOLS = "36.0.0", COMMAND_LINE_TOOLS_VERSION = "15859902", LICENSE_ACCEPTANCE_RESPONSES = 100, COMMAND_LINE_TOOLS, defaultRun = async (command, options = {}) => {
@@ -3064,7 +3211,7 @@ var ANDROID_API = 36, ANDROID_BUILD_TOOLS = "36.0.0", COMMAND_LINE_TOOLS_VERSION
3064
3211
  const arch2 = options.arch ?? process.arch;
3065
3212
  const exists3 = options.exists ?? (async (path) => {
3066
3213
  try {
3067
- await readFile4(path);
3214
+ await readFile5(path);
3068
3215
  return true;
3069
3216
  } catch {
3070
3217
  return false;
@@ -3167,7 +3314,7 @@ import {
3167
3314
  mkdir as mkdir4,
3168
3315
  mkdtemp as mkdtemp3,
3169
3316
  readdir as readdir3,
3170
- readFile as readFile5,
3317
+ readFile as readFile6,
3171
3318
  rename as rename4,
3172
3319
  rm as rm4,
3173
3320
  stat,
@@ -3243,7 +3390,7 @@ var developmentTeamArgument = (value) => {
3243
3390
  }, fingerprintAbsoluteIosNativeProject = async (nativeDirectory, options = {}) => {
3244
3391
  const hasher = createHash4("sha256");
3245
3392
  const files = await fingerprintFiles(nativeDirectory, nativeDirectory, options);
3246
- const contents = await Promise.all(files.map((file) => readFile5(file)));
3393
+ const contents = await Promise.all(files.map((file) => readFile6(file)));
3247
3394
  files.forEach((file, index) => {
3248
3395
  hasher.update(relative4(nativeDirectory, file).replaceAll("\\", "/"));
3249
3396
  hasher.update("\x00");
@@ -3259,7 +3406,7 @@ var developmentTeamArgument = (value) => {
3259
3406
  throw new TypeError("mobile build --outdir must remain inside the project.");
3260
3407
  }
3261
3408
  return output;
3262
- }, sha256File = async (path) => createHash4("sha256").update(await readFile5(path)).digest("hex"), findByExtension = async (root, extension) => {
3409
+ }, sha256File = async (path) => createHash4("sha256").update(await readFile6(path)).digest("hex"), findByExtension = async (root, extension) => {
3263
3410
  if (!await pathExists3(root))
3264
3411
  return;
3265
3412
  const entries = await readdir3(root, { withFileTypes: true });
@@ -3290,7 +3437,7 @@ var developmentTeamArgument = (value) => {
3290
3437
  const releaseRoot = join11(outputRoot, metadata.releaseId);
3291
3438
  const destination = join11(releaseRoot, "App.ipa");
3292
3439
  if (await pathExists3(releaseRoot)) {
3293
- const value = JSON.parse(await readFile5(join11(releaseRoot, "release.json"), "utf8"));
3440
+ const value = JSON.parse(await readFile6(join11(releaseRoot, "release.json"), "utf8"));
3294
3441
  if (!isRecord2(value) || value.artifact !== "App.ipa" || Object.entries(metadata).some(([key, expected]) => Reflect.get(value, key) !== expected)) {
3295
3442
  throw new TypeError(`Immutable iOS release ${metadata.releaseId} does not match its content.`);
3296
3443
  }
@@ -3334,7 +3481,7 @@ var developmentTeamArgument = (value) => {
3334
3481
  const marketingVersion = options.config.iosVersion;
3335
3482
  if (!marketingVersion)
3336
3483
  throw new TypeError("iOS release builds require mobile.ios.version in absolutejs.config.ts.");
3337
- const manifest = requireManifest(JSON.parse(await readFile5(join11(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
3484
+ const manifest = requireManifest(JSON.parse(await readFile6(join11(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
3338
3485
  if (manifest.appId !== options.config.appId)
3339
3486
  throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
3340
3487
  const nativeDirectory = join11(options.config.nativeProjectDirectory, "ios");
@@ -3439,97 +3586,6 @@ var init_iosRelease = __esm(() => {
3439
3586
  ]);
3440
3587
  });
3441
3588
 
3442
- // src/mobile/iosPhysicalDeviceTransport.ts
3443
- import { randomUUID as randomUUID2, X509Certificate } from "crypto";
3444
- import { createServer as createServer3 } from "http";
3445
- import {
3446
- connect as connectTcp,
3447
- createServer as createTcpServer,
3448
- isIP
3449
- } from "net";
3450
- import { readFile as readFile6 } from "fs/promises";
3451
- var closeServer = (server) => new Promise((resolve8, reject) => {
3452
- server.close((error) => {
3453
- if (error)
3454
- reject(error);
3455
- else
3456
- resolve8();
3457
- });
3458
- }), listen = (server, port) => new Promise((resolve8, reject) => {
3459
- server.once("error", reject);
3460
- server.listen(port, "0.0.0.0", () => {
3461
- server.off("error", reject);
3462
- const address = server.address();
3463
- if (!address || typeof address === "string") {
3464
- reject(new Error("Could not determine the iOS device helper port."));
3465
- return;
3466
- }
3467
- resolve8(address.port);
3468
- });
3469
- }), findEphemeralPort = async () => {
3470
- const probe = createTcpServer();
3471
- const port = await new Promise((resolve8, reject) => {
3472
- probe.once("error", reject);
3473
- probe.listen(0, "127.0.0.1", () => {
3474
- const address = probe.address();
3475
- if (!address || typeof address === "string") {
3476
- reject(new Error("Could not allocate the iOS CA enrollment port."));
3477
- return;
3478
- }
3479
- resolve8(address.port);
3480
- });
3481
- });
3482
- await closeServer(probe);
3483
- return port;
3484
- }, normalizeAbsoluteIosDeviceHost = (value) => {
3485
- const normalized = value.trim();
3486
- if (!normalized || normalized.length > 253 || /[\0\s/?#]/u.test(normalized))
3487
- throw new TypeError("Physical iOS development requires a valid LAN host.");
3488
- return normalized;
3489
- }, normalizeAbsoluteIosDeviceIdentifier = (value) => {
3490
- const normalized = value.trim();
3491
- if (!normalized || normalized.length > 256 || /[\0\r\n]/u.test(normalized))
3492
- throw new TypeError("--ios-device requires a valid Xcode device identifier or name.");
3493
- return normalized;
3494
- }, urlForHost = (protocol, host, port) => {
3495
- const url = new URL(`${protocol}://localhost:${port}`);
3496
- const normalizedHost = normalizeAbsoluteIosDeviceHost(host);
3497
- url.hostname = isIP(normalizedHost) === 6 ? `[${normalizedHost}]` : normalizedHost;
3498
- return url;
3499
- }, startAbsoluteIosCaEnrollmentServer = async (options) => {
3500
- const certificate = new X509Certificate(await readFile6(options.certificateAuthorityPath));
3501
- const certificateBytes = certificate.raw;
3502
- const token = randomUUID2().replaceAll("-", "");
3503
- const certificatePath = `/${token}/absolutejs-development-ca.cer`;
3504
- const server = createServer3((request, response) => {
3505
- if (request.method !== "GET" || request.url !== certificatePath) {
3506
- response.writeHead(404, {
3507
- "Cache-Control": "no-store",
3508
- "Content-Type": "text/plain; charset=utf-8"
3509
- });
3510
- response.end("Not found.");
3511
- return;
3512
- }
3513
- response.writeHead(200, {
3514
- "Cache-Control": "no-store",
3515
- "Content-Disposition": 'attachment; filename="absolutejs-development-ca.cer"',
3516
- "Content-Length": String(certificateBytes.byteLength),
3517
- "Content-Type": "application/x-x509-ca-cert",
3518
- "X-Content-Type-Options": "nosniff"
3519
- });
3520
- response.end(certificateBytes);
3521
- });
3522
- const port = await findEphemeralPort();
3523
- await listen(server, port);
3524
- const url = urlForHost("http", options.displayHost, port);
3525
- url.pathname = certificatePath;
3526
- return {
3527
- url: url.href,
3528
- close: () => closeServer(server)
3529
- };
3530
- };
3531
- var init_iosPhysicalDeviceTransport = () => {};
3532
-
3533
3589
  // src/mobile/iosSimulatorController.ts
3534
3590
  import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
3535
3591
  import {
@@ -4432,7 +4488,7 @@ var init_iosSimulatorController = __esm(() => {
4432
4488
  });
4433
4489
 
4434
4490
  // src/mobile/remoteMacWire.ts
4435
- var ABSOLUTE_REMOTE_MAC_EVENT_PREFIX = "ABSOLUTE_REMOTE_MAC\t", ABSOLUTE_REMOTE_MAC_PROTOCOL_VERSION = 1;
4491
+ var ABSOLUTE_REMOTE_MAC_EVENT_PREFIX = "ABSOLUTE_REMOTE_MAC\t", ABSOLUTE_REMOTE_MAC_PROTOCOL_VERSION = 2;
4436
4492
 
4437
4493
  // src/mobile/remoteMacProtocol.ts
4438
4494
  import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
@@ -4448,7 +4504,7 @@ import {
4448
4504
  resolve as resolvePath,
4449
4505
  sep as sep4
4450
4506
  } from "path";
4451
- var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTINATION, defaultProfilePath = () => join13(homedir4(), ".absolutejs", "mobile", "remote-macs.json"), emptyStore = () => ({
4507
+ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, REMOTE_SESSION_CLOSE_TIMEOUT_MS = 2000, PROFILE_NAME, SSH_DESTINATION, defaultProfilePath = () => join13(homedir4(), ".absolutejs", "mobile", "remote-macs.json"), emptyStore = () => ({
4452
4508
  format: PROFILE_FORMAT,
4453
4509
  profiles: {}
4454
4510
  }), loadStore = async (path = defaultProfilePath()) => {
@@ -4619,7 +4675,11 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4619
4675
  }
4620
4676
  await saveStore(store, profilePath);
4621
4677
  return true;
4622
- }, projectIdentity = (projectRoot, appId) => createHash6("sha256").update(`${resolvePath(projectRoot)}\x00${appId}`).digest("hex").slice(0, 20), createAbsoluteRemoteIosDevProject = (config, projectRoot, profile) => ({
4678
+ }, projectIdentity = (projectRoot, appId) => createHash6("sha256").update(`${resolvePath(projectRoot)}\x00${appId}`).digest("hex").slice(0, 20), createAbsoluteRemoteExpoIosDevProject = (config, projectRoot, profile) => {
4679
+ if (config.engine !== "expo")
4680
+ throw new TypeError("Remote Expo execution requires mobile.engine: expo.");
4681
+ return createAbsoluteRemoteIosDevProject(config, projectRoot, profile);
4682
+ }, createAbsoluteRemoteIosDevProject = (config, projectRoot, profile) => ({
4623
4683
  cap: join13(resolvePath(projectRoot), "node_modules", ".bin", "cap"),
4624
4684
  config,
4625
4685
  nativeDirectory: join13(config.nativeProjectDirectory, "ios"),
@@ -4706,6 +4766,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4706
4766
  appId: project.config.appId,
4707
4767
  appName: project.config.appName,
4708
4768
  bundleDirectory: portableRelativePath(project.projectRoot, project.config.bundleDirectory),
4769
+ engine: project.config.engine,
4709
4770
  ...project.config.deepLinkScheme || project.config.deepLinkHosts.length > 1 || project.config.appleAppIdPrefix ? {
4710
4771
  deepLinks: {
4711
4772
  ...project.config.deepLinkScheme ? { scheme: project.config.deepLinkScheme } : {},
@@ -4718,6 +4779,16 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4718
4779
  }
4719
4780
  } : {},
4720
4781
  entry: project.config.entry,
4782
+ ...project.config.engine === "expo" ? {
4783
+ expo: { sdkVersion: project.config.expoSdkVersion ?? 57 },
4784
+ routes: {
4785
+ default: "web",
4786
+ native: Object.fromEntries(Object.entries(project.config.expoNativeRoutes).map(([route, module]) => [
4787
+ route,
4788
+ portableRelativePath(project.projectRoot, module)
4789
+ ]))
4790
+ }
4791
+ } : {},
4721
4792
  ...project.config.iosVersion ? { ios: { version: project.config.iosVersion } } : {},
4722
4793
  nativeProject: {
4723
4794
  directory: portableRelativePath(project.projectRoot, project.config.nativeProjectDirectory),
@@ -4805,7 +4876,30 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4805
4876
  } finally {
4806
4877
  reader.releaseLock();
4807
4878
  }
4808
- }, startAbsoluteRemoteIosDevSession = async (options) => {
4879
+ }, startAbsoluteRemoteExpoIosDevSession = async (options) => {
4880
+ const session = await startAbsoluteRemoteDevSession({
4881
+ certificateAuthorityPath: options.certificateAuthorityPath,
4882
+ deviceIdentifier: options.deviceIdentifier,
4883
+ https: options.https,
4884
+ installAgent: options.installAgent,
4885
+ log: options.log,
4886
+ metroPort: options.metroPort,
4887
+ onExpoPhaseTiming: options.onPhaseTiming,
4888
+ onExpoStateChange: options.onStateChange,
4889
+ port: options.port,
4890
+ project: options.project,
4891
+ serverHost: options.serverHost,
4892
+ signal: options.signal,
4893
+ syncProject: options.syncProject,
4894
+ transport: options.transport
4895
+ });
4896
+ return {
4897
+ close: session.close,
4898
+ metroPort: options.metroPort,
4899
+ platforms: ["ios"],
4900
+ timings: session.timings
4901
+ };
4902
+ }, startAbsoluteRemoteIosDevSession = (options) => startAbsoluteRemoteDevSession(options), startAbsoluteRemoteDevSession = async (options) => {
4809
4903
  const startedAt = performance.now();
4810
4904
  const transport = options.transport ?? defaultTransport;
4811
4905
  const installAgent = options.installAgent ?? installAbsoluteRemoteMacAgent;
@@ -4819,9 +4913,22 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4819
4913
  const encodedConfig = Buffer.from(JSON.stringify(portableMobileConfig(options.project))).toString("base64url");
4820
4914
  const encodedCertificateAuthority = options.certificateAuthorityPath ? (await readFile8(options.certificateAuthorityPath)).toString("base64url") : undefined;
4821
4915
  const physicalDevice = options.deviceIdentifier !== undefined;
4822
- let relayPort;
4823
- if (physicalDevice)
4824
- relayPort = options.port <= 49151 ? options.port + 16384 : options.port - 16384;
4916
+ const expo = options.project.config.engine === "expo";
4917
+ if (expo && options.metroPort === undefined)
4918
+ throw new TypeError("Remote Expo execution requires a Metro port.");
4919
+ if (options.metroPort === options.port)
4920
+ throw new TypeError("Expo Metro and AbsoluteJS must use different ports.");
4921
+ const translatedRelayPort = (sourcePort) => sourcePort <= 49151 ? sourcePort + 16384 : sourcePort - 16384;
4922
+ const occupiedRemotePorts = new Set([options.port, options.metroPort].filter((value) => value !== undefined));
4923
+ const reserveRelayPort = (sourcePort) => {
4924
+ let candidate = translatedRelayPort(sourcePort);
4925
+ while (occupiedRemotePorts.has(candidate))
4926
+ candidate = candidate === 65535 ? 1024 : candidate + 1;
4927
+ occupiedRemotePorts.add(candidate);
4928
+ return candidate;
4929
+ };
4930
+ const relayPort = physicalDevice ? reserveRelayPort(options.port) : undefined;
4931
+ const metroRelayPort = physicalDevice && options.metroPort !== undefined ? reserveRelayPort(options.metroPort) : undefined;
4825
4932
  if (physicalDevice && !options.serverHost)
4826
4933
  throw new Error("Remote physical iOS development requires the Remote Mac LAN host.");
4827
4934
  const remoteCommand = [
@@ -4834,6 +4941,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4834
4941
  String(options.port),
4835
4942
  "--mobile-config",
4836
4943
  shellQuote(encodedConfig),
4944
+ ...expo && options.metroPort !== undefined ? ["--metro-port", String(options.metroPort)] : [],
4837
4945
  ...encodedCertificateAuthority ? [
4838
4946
  "--certificate-authority",
4839
4947
  shellQuote(encodedCertificateAuthority)
@@ -4845,7 +4953,8 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4845
4953
  "--server-host",
4846
4954
  shellQuote(options.serverHost ?? ""),
4847
4955
  "--relay-port",
4848
- String(relayPort)
4956
+ String(relayPort),
4957
+ ...expo && metroRelayPort !== undefined ? ["--metro-relay-port", String(metroRelayPort)] : []
4849
4958
  ] : []
4850
4959
  ].join(" ");
4851
4960
  const command = [
@@ -4854,6 +4963,10 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4854
4963
  "ExitOnForwardFailure=yes",
4855
4964
  "-R",
4856
4965
  physicalDevice ? `127.0.0.1:${relayPort}:127.0.0.1:${options.port}` : `${options.port}:127.0.0.1:${options.port}`,
4966
+ ...expo && options.metroPort !== undefined ? [
4967
+ "-R",
4968
+ physicalDevice ? `127.0.0.1:${metroRelayPort}:127.0.0.1:${options.metroPort}` : `${options.metroPort}:127.0.0.1:${options.metroPort}`
4969
+ ] : [],
4857
4970
  "/bin/sh -lc",
4858
4971
  shellQuote(remoteCommand)
4859
4972
  ];
@@ -4879,11 +4992,19 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4879
4992
  if (event.type === "native-log")
4880
4993
  options.nativeLog?.(event.entry);
4881
4994
  if (event.type === "state") {
4882
- ({ state } = event);
4883
- options.onStateChange?.(state);
4995
+ if (expo)
4996
+ options.onExpoStateChange?.(event.state);
4997
+ else {
4998
+ state = event.state;
4999
+ options.onStateChange?.(state);
5000
+ }
5001
+ }
5002
+ if (event.type === "timing") {
5003
+ if (expo)
5004
+ options.onExpoPhaseTiming?.(event);
5005
+ else
5006
+ options.onPhaseTiming?.(event);
4884
5007
  }
4885
- if (event.type === "timing")
4886
- options.onPhaseTiming?.(event);
4887
5008
  if (event.type === "ready") {
4888
5009
  ready = event;
4889
5010
  resolveReady();
@@ -4925,9 +5046,26 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4925
5046
  pending.clear();
4926
5047
  return;
4927
5048
  });
4928
- await readyPromise;
5049
+ try {
5050
+ await readyPromise;
5051
+ } catch (error) {
5052
+ process2.stdin.end();
5053
+ process2.kill();
5054
+ await process2.exited.catch(() => {
5055
+ return;
5056
+ });
5057
+ throw error;
5058
+ }
4929
5059
  if (!ready)
4930
5060
  throw fatal ?? new Error("Remote Mac did not become ready.");
5061
+ if (ready.engine !== options.project.config.engine) {
5062
+ process2.stdin.end();
5063
+ process2.kill();
5064
+ await process2.exited.catch(() => {
5065
+ return;
5066
+ });
5067
+ throw new Error("Remote Mac executor returned the wrong mobile engine.");
5068
+ }
4931
5069
  const totalDuration = performance.now() - startedAt;
4932
5070
  let currentReady = {
4933
5071
  ...ready,
@@ -4939,11 +5077,11 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4939
5077
  total: totalDuration
4940
5078
  }
4941
5079
  };
4942
- options.log?.(`Remote Mac connected (${options.project.profile.name}); agent ${agent.uploaded ? "uploaded" : "cache hit"}, project synced, and iOS ready in ${totalDuration.toFixed(2)}ms.`);
5080
+ options.log?.(`Remote Mac connected (${options.project.profile.name}); agent ${agent.uploaded ? "uploaded" : "cache hit"}, project synced, and ${expo ? "Expo " : ""}iOS ready in ${totalDuration.toFixed(2)}ms.`);
4943
5081
  const request = (commandName) => {
4944
5082
  const id = randomUUID4();
4945
5083
  const response = new Promise((resolve9, reject) => pending.set(id, { reject, resolve: resolve9 }));
4946
- process2.stdin.write(`${JSON.stringify({ command: commandName, id, v: 1 })}
5084
+ process2.stdin.write(`${JSON.stringify({ command: commandName, id, v: ABSOLUTE_REMOTE_MAC_PROTOCOL_VERSION })}
4947
5085
  `);
4948
5086
  const flush = async () => {
4949
5087
  for (let attempt = 0;attempt < REMOTE_STDIN_FLUSH_ATTEMPTS; attempt++) {
@@ -4962,13 +5100,24 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4962
5100
  if (closed)
4963
5101
  return;
4964
5102
  closed = true;
4965
- await request("close").catch(() => {
4966
- return;
4967
- });
5103
+ const timeout = () => new Promise((resolve9) => setTimeout(() => resolve9("timeout"), REMOTE_SESSION_CLOSE_TIMEOUT_MS));
5104
+ await Promise.race([
5105
+ request("close").catch(() => {
5106
+ return;
5107
+ }),
5108
+ timeout()
5109
+ ]);
4968
5110
  process2.stdin.end();
4969
- await process2.exited.catch(() => {
4970
- return;
4971
- });
5111
+ const exit = await Promise.race([
5112
+ process2.exited.then(() => "exited").catch(() => "exited"),
5113
+ timeout()
5114
+ ]);
5115
+ if (exit === "timeout") {
5116
+ process2.kill();
5117
+ await process2.exited.catch(() => {
5118
+ return;
5119
+ });
5120
+ }
4972
5121
  };
4973
5122
  const makeSession = () => ({
4974
5123
  close,
@@ -23303,6 +23452,7 @@ init_config();
23303
23452
  init_expoProject();
23304
23453
 
23305
23454
  // src/mobile/expoDevController.ts
23455
+ init_iosPhysicalDeviceTransport();
23306
23456
  import { spawn } from "child_process";
23307
23457
  import { access as access2 } from "fs/promises";
23308
23458
  import { join as join6 } from "path";
@@ -23310,8 +23460,12 @@ var METRO_READY_TIMEOUT_MS = 60000;
23310
23460
  var PROCESS_CLOSE_TIMEOUT_MS = 2000;
23311
23461
  var commandEnvironment = (options) => ({
23312
23462
  ABSOLUTE_EXPO_DEVELOPMENT: "1",
23463
+ ...options.certificateAuthorityPath ? {
23464
+ ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH: options.certificateAuthorityPath
23465
+ } : {},
23313
23466
  ...options.androidOrigin ? { EXPO_PUBLIC_ABSOLUTE_DEV_ANDROID_ORIGIN: options.androidOrigin } : {},
23314
- ...options.iosOrigin ? { EXPO_PUBLIC_ABSOLUTE_DEV_IOS_ORIGIN: options.iosOrigin } : {}
23467
+ ...options.iosOrigin ? { EXPO_PUBLIC_ABSOLUTE_DEV_IOS_ORIGIN: options.iosOrigin } : {},
23468
+ ...options.metroHost ? { REACT_NATIVE_PACKAGER_HOSTNAME: options.metroHost } : {}
23315
23469
  });
23316
23470
  var absoluteExpoExecutable = async (project) => {
23317
23471
  const executable = join6(project, "node_modules", ".bin", "expo");
@@ -23325,8 +23479,11 @@ var absoluteExpoExecutable = async (project) => {
23325
23479
  var planAbsoluteExpoDevSession = (config, options) => {
23326
23480
  if (config.engine !== "expo")
23327
23481
  throw new TypeError("The Expo development controller requires Expo.");
23328
- if (options.platforms.length === 0)
23329
- throw new TypeError("Expo development requires a local target platform.");
23482
+ if (options.platforms.length === 0 && options.metro === "external")
23483
+ throw new TypeError("External Expo execution requires a target platform.");
23484
+ const secureOrigin = [options.androidOrigin, options.iosOrigin].some((origin) => origin && new URL(origin).protocol === "https:");
23485
+ if (secureOrigin && !options.certificateAuthorityPath)
23486
+ throw new TypeError("Expo HTTPS development requires the AbsoluteJS development CA certificate.");
23330
23487
  const env = commandEnvironment(options);
23331
23488
  const metro = {
23332
23489
  args: [
@@ -23340,7 +23497,7 @@ var planAbsoluteExpoDevSession = (config, options) => {
23340
23497
  env,
23341
23498
  role: "metro"
23342
23499
  };
23343
- const prepare = {
23500
+ const prepare = options.platforms.length === 0 ? undefined : {
23344
23501
  args: [
23345
23502
  "prebuild",
23346
23503
  "--clean",
@@ -23368,7 +23525,11 @@ var planAbsoluteExpoDevSession = (config, options) => {
23368
23525
  return command;
23369
23526
  });
23370
23527
  return {
23371
- commands: [prepare, metro, ...native],
23528
+ commands: [
23529
+ ...prepare ? [prepare] : [],
23530
+ ...options.metro === "external" ? [] : [metro],
23531
+ ...native
23532
+ ],
23372
23533
  metroPort: options.metroPort,
23373
23534
  project: config.nativeProjectDirectory
23374
23535
  };
@@ -23416,6 +23577,21 @@ var waitForExit = (process2) => new Promise((resolve4) => {
23416
23577
  }
23417
23578
  process2.once("exit", (code) => resolve4(code ?? 1));
23418
23579
  });
23580
+ var runUtilityCommand = async (run, command, args, options) => {
23581
+ const child = run(command, args, {
23582
+ cwd: options.cwd,
23583
+ env: process.env,
23584
+ stdio: ["ignore", "pipe", "pipe"]
23585
+ });
23586
+ forwardLines(child, options.log);
23587
+ const abort = () => void stopProcess(child);
23588
+ options.signal?.addEventListener("abort", abort, { once: true });
23589
+ const exitCode = await waitForExit(child);
23590
+ options.signal?.removeEventListener("abort", abort);
23591
+ if (options.signal?.aborted)
23592
+ throw abortError();
23593
+ return exitCode;
23594
+ };
23419
23595
  var startAbsoluteExpoDevSession = async (options) => {
23420
23596
  const plan = planAbsoluteExpoDevSession(options.config, options);
23421
23597
  const executable = options.executable ?? await absoluteExpoExecutable(plan.project);
@@ -23424,6 +23600,16 @@ var startAbsoluteExpoDevSession = async (options) => {
23424
23600
  return;
23425
23601
  });
23426
23602
  const timings = {};
23603
+ let caEnrollmentServer = null;
23604
+ const closeEnrollmentServer = async () => {
23605
+ const server = caEnrollmentServer;
23606
+ caEnrollmentServer = null;
23607
+ await server?.close();
23608
+ };
23609
+ const publishTiming = (phase, durationMs) => {
23610
+ timings[phase] = (timings[phase] ?? 0) + durationMs;
23611
+ options.onPhaseTiming?.({ durationMs, phase });
23612
+ };
23427
23613
  const setState = (state) => {
23428
23614
  options.onStateChange?.(state);
23429
23615
  };
@@ -23432,47 +23618,48 @@ var startAbsoluteExpoDevSession = async (options) => {
23432
23618
  const prepareCommand = plan.commands.find((command) => command.role === "native-prepare");
23433
23619
  const metroCommand = plan.commands.find((command) => command.role === "metro");
23434
23620
  const nativeCommands = plan.commands.filter((command) => command.role === "native-build");
23435
- if (!prepareCommand)
23436
- throw new TypeError("Expo native preparation command is missing.");
23437
- if (!metroCommand)
23438
- throw new TypeError("Expo Metro command is missing.");
23439
- setState("preparing-native");
23440
- const prepareStarted = performance.now();
23441
- const prepareProcess = run(executable, prepareCommand.args, {
23442
- cwd: plan.project,
23443
- env: { ...process.env, ...prepareCommand.env },
23444
- stdio: ["ignore", "pipe", "pipe"]
23445
- });
23446
- forwardLines(prepareProcess, (line) => {
23447
- if (line)
23448
- log(`[prebuild] ${line}`);
23449
- });
23450
- const abortPrepare = () => void stopProcess(prepareProcess);
23451
- options.signal?.addEventListener("abort", abortPrepare, { once: true });
23452
- const prepareExit = await waitForExit(prepareProcess);
23453
- options.signal?.removeEventListener("abort", abortPrepare);
23454
- if (options.signal?.aborted)
23455
- throw abortError();
23456
- if (prepareExit !== 0) {
23457
- setState("failed");
23458
- throw new Error(`Expo native preparation exited with status ${prepareExit}.`);
23459
- }
23460
- const prepareMs = performance.now() - prepareStarted;
23461
- timings["preparing-native"] = prepareMs;
23462
- options.onPhaseTiming?.({
23463
- durationMs: prepareMs,
23464
- phase: "preparing-native"
23465
- });
23466
- setState("starting-metro");
23621
+ const runPrepareCommand = async (command) => {
23622
+ setState("preparing-native");
23623
+ const prepareStarted = performance.now();
23624
+ const prepareProcess = run(executable, command.args, {
23625
+ cwd: plan.project,
23626
+ env: { ...process.env, ...command.env },
23627
+ stdio: ["ignore", "pipe", "pipe"]
23628
+ });
23629
+ forwardLines(prepareProcess, (line) => {
23630
+ if (line)
23631
+ log(`[prebuild] ${line}`);
23632
+ });
23633
+ const abortPrepare = () => void stopProcess(prepareProcess);
23634
+ options.signal?.addEventListener("abort", abortPrepare, { once: true });
23635
+ const prepareExit = await waitForExit(prepareProcess);
23636
+ options.signal?.removeEventListener("abort", abortPrepare);
23637
+ if (options.signal?.aborted)
23638
+ throw abortError();
23639
+ if (prepareExit !== 0) {
23640
+ setState("failed");
23641
+ throw new Error(`Expo native preparation exited with status ${prepareExit}.`);
23642
+ }
23643
+ publishTiming("preparing-native", performance.now() - prepareStarted);
23644
+ };
23645
+ if (prepareCommand)
23646
+ await runPrepareCommand(prepareCommand);
23647
+ const managedMetro = metroCommand !== undefined;
23648
+ if (managedMetro)
23649
+ setState("starting-metro");
23467
23650
  const metroStarted = performance.now();
23468
- const metro = run(executable, metroCommand.args, {
23651
+ const metro = metroCommand ? run(executable, metroCommand.args, {
23469
23652
  cwd: plan.project,
23470
23653
  env: { ...process.env, ...metroCommand.env },
23471
23654
  stdio: ["ignore", "pipe", "pipe"]
23472
- });
23655
+ }) : undefined;
23473
23656
  let metroReady = false;
23474
23657
  let resolveMetro;
23475
23658
  const metroPromise = new Promise((resolve4, reject) => {
23659
+ if (!metro) {
23660
+ resolve4();
23661
+ return;
23662
+ }
23476
23663
  const timeout = setTimeout(() => {
23477
23664
  reject(new Error("Expo Metro did not become ready within 60 seconds."));
23478
23665
  }, METRO_READY_TIMEOUT_MS);
@@ -23487,15 +23674,19 @@ var startAbsoluteExpoDevSession = async (options) => {
23487
23674
  }
23488
23675
  });
23489
23676
  });
23490
- forwardLines(metro, (line) => {
23491
- if (line)
23492
- log(`[metro] ${line}`);
23493
- if (!metroReady && /(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(line)) {
23494
- metroReady = true;
23495
- resolveMetro?.();
23496
- }
23497
- });
23498
- const abort = () => void stopProcess(metro);
23677
+ if (metro)
23678
+ forwardLines(metro, (line) => {
23679
+ if (line)
23680
+ log(`[metro] ${line}`);
23681
+ if (!metroReady && /(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(line)) {
23682
+ metroReady = true;
23683
+ resolveMetro?.();
23684
+ }
23685
+ });
23686
+ const abort = () => {
23687
+ if (metro)
23688
+ stopProcess(metro);
23689
+ };
23499
23690
  options.signal?.addEventListener("abort", abort, { once: true });
23500
23691
  const runNativeCommand = async (command) => {
23501
23692
  if (options.signal?.aborted)
@@ -23524,6 +23715,30 @@ var startAbsoluteExpoDevSession = async (options) => {
23524
23715
  if (exitCode !== 0) {
23525
23716
  throw new Error(`Expo ${platform2} development build exited with status ${exitCode}.`);
23526
23717
  }
23718
+ if (platform2 === "ios" && options.certificateAuthorityPath && options.iosOrigin && new URL(options.iosOrigin).protocol === "https:" && !options.iosDevice) {
23719
+ setState("enrolling-trust");
23720
+ const trustStarted = performance.now();
23721
+ const utilityOptions = {
23722
+ cwd: plan.project,
23723
+ signal: options.signal,
23724
+ log: (line) => line && log(`[ios-trust] ${line}`)
23725
+ };
23726
+ const trustExit = await runUtilityCommand(run, "xcrun", [
23727
+ "simctl",
23728
+ "keychain",
23729
+ "booted",
23730
+ "add-root-cert",
23731
+ options.certificateAuthorityPath
23732
+ ], utilityOptions);
23733
+ if (trustExit !== 0)
23734
+ throw new Error(`Expo iOS Simulator development CA trust exited with status ${trustExit}.`);
23735
+ await runUtilityCommand(run, "xcrun", ["simctl", "terminate", "booted", options.config.appId], utilityOptions);
23736
+ const launchExit = await runUtilityCommand(run, "xcrun", ["simctl", "launch", "booted", options.config.appId], utilityOptions);
23737
+ if (launchExit !== 0)
23738
+ throw new Error(`Expo iOS Simulator relaunch exited with status ${launchExit}.`);
23739
+ log("Installed the AbsoluteJS development CA into the Expo iOS Simulator and relaunched the app.");
23740
+ publishTiming("enrolling-trust", performance.now() - trustStarted);
23741
+ }
23527
23742
  const durationMs = performance.now() - started;
23528
23743
  timings[state] = durationMs;
23529
23744
  options.onPhaseTiming?.({ durationMs, phase: state });
@@ -23535,14 +23750,25 @@ var startAbsoluteExpoDevSession = async (options) => {
23535
23750
  await runNativeCommand(command);
23536
23751
  await runNativeCommands(remaining);
23537
23752
  };
23753
+ const startPhysicalIosEnrollment = async () => {
23754
+ if (!options.iosDevice || !options.certificateAuthorityPath || !options.iosOrigin || new URL(options.iosOrigin).protocol !== "https:") {
23755
+ return;
23756
+ }
23757
+ setState("enrolling-trust");
23758
+ const trustStarted = performance.now();
23759
+ const startEnrollment = options.startCaEnrollmentServer ?? startAbsoluteIosCaEnrollmentServer;
23760
+ caEnrollmentServer = await startEnrollment({
23761
+ certificateAuthorityPath: options.certificateAuthorityPath,
23762
+ displayHost: new URL(options.iosOrigin).hostname
23763
+ });
23764
+ log(`On the iOS device, open ${caEnrollmentServer.url}, install the AbsoluteJS development CA profile, then enable it under Settings > General > About > Certificate Trust Settings. This public CA endpoint exists only for this dev session.`);
23765
+ publishTiming("enrolling-trust", performance.now() - trustStarted);
23766
+ };
23538
23767
  try {
23768
+ await startPhysicalIosEnrollment();
23539
23769
  await metroPromise;
23540
- const metroMs = performance.now() - metroStarted;
23541
- timings["starting-metro"] = metroMs;
23542
- options.onPhaseTiming?.({
23543
- durationMs: metroMs,
23544
- phase: "starting-metro"
23545
- });
23770
+ if (managedMetro)
23771
+ publishTiming("starting-metro", performance.now() - metroStarted);
23546
23772
  await runNativeCommands(nativeCommands);
23547
23773
  setState("ready");
23548
23774
  return {
@@ -23551,13 +23777,17 @@ var startAbsoluteExpoDevSession = async (options) => {
23551
23777
  timings,
23552
23778
  close: async () => {
23553
23779
  options.signal?.removeEventListener("abort", abort);
23554
- await stopProcess(metro);
23780
+ if (metro)
23781
+ await stopProcess(metro);
23782
+ await closeEnrollmentServer();
23555
23783
  setState("closed");
23556
23784
  }
23557
23785
  };
23558
23786
  } catch (error) {
23559
23787
  options.signal?.removeEventListener("abort", abort);
23560
- await stopProcess(metro);
23788
+ if (metro)
23789
+ await stopProcess(metro);
23790
+ await closeEnrollmentServer();
23561
23791
  setState("failed");
23562
23792
  throw error;
23563
23793
  }
@@ -23930,14 +24160,17 @@ var dev = async (serverEntry, configPath2, options = {}) => {
23930
24160
  let resolvedDev;
23931
24161
  let buildDirectory = resolvePath2(process.cwd(), "build");
23932
24162
  let mobileConfig;
24163
+ let normalizedMobileConfig;
23933
24164
  let iosPhysicalServerHost;
23934
24165
  let selectedRemoteMacProfile;
23935
24166
  let expoDevProject;
23936
24167
  try {
23937
24168
  const config = await loadConfig(configPath2);
23938
24169
  mobileConfig = config?.mobile;
23939
- if (mobileConfig)
23940
- installAbsoluteMobileAuthEnvironment(process.cwd(), normalizeAbsoluteMobileConfig(mobileConfig, process.cwd()));
24170
+ if (mobileConfig) {
24171
+ normalizedMobileConfig = normalizeAbsoluteMobileConfig(mobileConfig, process.cwd());
24172
+ installAbsoluteMobileAuthEnvironment(process.cwd(), normalizedMobileConfig);
24173
+ }
23941
24174
  resolvedDev = resolveDevConfig(config?.dev);
23942
24175
  httpsEnabled = resolvedDev.https;
23943
24176
  if (config?.buildDirectory) {
@@ -23960,9 +24193,6 @@ var dev = async (serverEntry, configPath2, options = {}) => {
23960
24193
  throw new TypeError("--ios-device requires ios in mobile.platforms.");
23961
24194
  }
23962
24195
  if (options.iosDevice) {
23963
- if (mobileConfig?.engine === "expo" && detectAbsoluteMobileHost() !== "macos") {
23964
- throw new Error("Expo physical iOS development currently requires local macOS; Expo Remote Mac execution is the next adapter milestone.");
23965
- }
23966
24196
  if (detectAbsoluteMobileHost() === "macos")
23967
24197
  iosPhysicalServerHost = mobileReachableHost(resolvedDev.host);
23968
24198
  else {
@@ -23974,6 +24204,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
23974
24204
  }
23975
24205
  if (httpsEnabled) {
23976
24206
  const certificateHosts = [
24207
+ ...normalizedMobileConfig?.engine === "expo" && !options.androidDevice && normalizedMobileConfig.platforms.includes("android") ? ["10.0.2.2"] : [],
23977
24208
  ...options.androidDevice ? [mobileReachableHost(resolvedDev.host)] : [],
23978
24209
  ...iosPhysicalServerHost ? [iosPhysicalServerHost] : []
23979
24210
  ];
@@ -23982,13 +24213,10 @@ var dev = async (serverEntry, configPath2, options = {}) => {
23982
24213
  let androidDevProject = null;
23983
24214
  let iosDevProject = null;
23984
24215
  const mobileInteractive = options.mobile !== false && process.env.ABSOLUTE_NO_MOBILE !== "1" && process.stdin.isTTY === true && process.stdout.isTTY === true;
23985
- if (mobileConfig && mobileInteractive) {
24216
+ if (normalizedMobileConfig && mobileInteractive) {
23986
24217
  try {
23987
- const normalized = normalizeAbsoluteMobileConfig(mobileConfig, process.cwd());
24218
+ const normalized = normalizedMobileConfig;
23988
24219
  if (normalized.engine === "expo") {
23989
- if (httpsEnabled) {
23990
- throw new TypeError("Combined Expo development currently requires dev.https: false while debug-only CA projection is completed; productionOrigin remains HTTPS.");
23991
- }
23992
24220
  const generated = await writeAbsoluteExpoProject(normalized, {
23993
24221
  projectRoot: process.cwd()
23994
24222
  });
@@ -24008,6 +24236,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24008
24236
  }
24009
24237
  }
24010
24238
  const platforms = [];
24239
+ let remoteIos;
24011
24240
  if (normalized.platforms.includes("android")) {
24012
24241
  const target = options.androidDevice ? "device" : "emulator";
24013
24242
  let ready = androidToolchainReady(await inspectAbsoluteMobileToolchain(), target);
@@ -24023,7 +24252,14 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24023
24252
  }
24024
24253
  if (normalized.platforms.includes("ios")) {
24025
24254
  if (detectAbsoluteMobileHost() !== "macos") {
24026
- console.log(cliTag("\x1B[33m", "Expo iOS target skipped on this host; Expo Remote Mac execution is the next adapter milestone."));
24255
+ const remote = selectedRemoteMacProfile ?? await getAbsoluteRemoteMacProfile();
24256
+ if (!remote) {
24257
+ console.log(cliTag("\x1B[33m", "Expo iOS target skipped. Pair a Mac with `absolute mobile pair mac <name> <user@host>`."));
24258
+ } else {
24259
+ selectedRemoteMacProfile = remote;
24260
+ remoteIos = createAbsoluteRemoteExpoIosDevProject(normalized, process.cwd(), remote);
24261
+ console.log(cliTag("\x1B[35m", `Using remote Mac ${remote.name} for Expo iOS development.`));
24262
+ }
24027
24263
  } else {
24028
24264
  const target = options.iosDevice ? "device" : "simulator";
24029
24265
  let ready = iosToolchainReady(await inspectAbsoluteMobileToolchain(), target);
@@ -24038,13 +24274,17 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24038
24274
  platforms.push("ios");
24039
24275
  }
24040
24276
  }
24041
- if (platforms.length > 0) {
24277
+ if (platforms.length > 0 || remoteIos) {
24042
24278
  expoDevProject = {
24043
24279
  executable: await absoluteExpoExecutable(generated.path),
24044
24280
  mobile: normalized,
24045
- platforms
24281
+ platforms,
24282
+ ...remoteIos ? { remoteIos } : {}
24046
24283
  };
24047
- console.log(cliTag("\x1B[35m", `Expo development build queued for ${platforms.join(" + ")}; Metro and native launch will start with the Bun server.`));
24284
+ console.log(cliTag("\x1B[35m", `Expo development build queued for ${[
24285
+ ...platforms,
24286
+ ...remoteIos ? ["ios (Remote Mac)"] : []
24287
+ ].join(" + ")}; Metro and native launch will start with the Bun server.`));
24048
24288
  }
24049
24289
  } else if (normalized.platforms.includes("android")) {
24050
24290
  const androidTarget = options.androidDevice ? "device" : "emulator";
@@ -24461,7 +24701,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24461
24701
  const expoTelemetryPlatform = (phase) => {
24462
24702
  if (phase.endsWith("android"))
24463
24703
  return "android";
24464
- if (phase.endsWith("ios"))
24704
+ if (phase.endsWith("ios") || phase === "enrolling-trust")
24465
24705
  return "ios";
24466
24706
  return "shared";
24467
24707
  };
@@ -24471,53 +24711,166 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24471
24711
  return;
24472
24712
  }
24473
24713
  const androidHost = options.androidDevice ? mobileReachableHost(resolvedDev.host) : "10.0.2.2";
24474
- const iosHost = options.iosDevice ? mobileReachableHost(resolvedDev.host) : "localhost";
24475
- expoDevStart = startAbsoluteExpoDevSession({
24476
- androidDevice: options.androidDevice,
24477
- androidOrigin: project.platforms.includes("android") ? absoluteDevOrigin(androidHost) : undefined,
24714
+ const iosHost = options.iosDevice ? iosPhysicalServerHost ?? mobileReachableHost(resolvedDev.host) : "localhost";
24715
+ const androidOrigin = project.platforms.includes("android") ? absoluteDevOrigin(androidHost) : undefined;
24716
+ const iosOrigin = project.platforms.includes("ios") || project.remoteIos ? absoluteDevOrigin(iosHost) : undefined;
24717
+ let metroSession;
24718
+ let localNativeSession;
24719
+ let remoteSession;
24720
+ const metroStart = startAbsoluteExpoDevSession({
24721
+ androidOrigin,
24722
+ certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
24478
24723
  config: project.mobile,
24479
24724
  executable: project.executable,
24480
- iosDevice: options.iosDevice,
24481
- iosOrigin: project.platforms.includes("ios") ? absoluteDevOrigin(iosHost) : undefined,
24725
+ iosOrigin,
24482
24726
  metroPort: expoMetroPort,
24483
- platforms: project.platforms,
24727
+ platforms: [],
24484
24728
  signal: expoDevAbort.signal,
24485
24729
  log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo ${message}`)),
24486
24730
  onPhaseTiming: ({ durationMs, phase }) => {
24487
24731
  sendTelemetryEvent("mobile:expo-dev-phase", {
24488
24732
  durationMs: Math.round(durationMs),
24733
+ host: detectAbsoluteMobileHost(),
24489
24734
  phase,
24490
24735
  platform: expoTelemetryPlatform(phase),
24491
24736
  provider: "expo"
24492
24737
  });
24493
24738
  },
24494
24739
  onStateChange: (state) => {
24495
- expoDevState = state;
24496
24740
  if (state === "ready" || state === "closed")
24497
24741
  return;
24742
+ expoDevState = state;
24498
24743
  printNativeOutput(cliTag("\x1B[35m", `Expo development: ${state}.`));
24499
24744
  }
24500
- }).then(async (session) => {
24745
+ }).then((session) => {
24746
+ metroSession = session;
24747
+ return session;
24748
+ });
24749
+ const localNativeStart = metroStart.then(async () => {
24750
+ if (project.platforms.length === 0)
24751
+ return;
24752
+ const session = await startAbsoluteExpoDevSession({
24753
+ androidDevice: options.androidDevice,
24754
+ androidOrigin,
24755
+ certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
24756
+ config: project.mobile,
24757
+ executable: project.executable,
24758
+ iosDevice: options.iosDevice,
24759
+ iosOrigin,
24760
+ metro: "external",
24761
+ metroPort: expoMetroPort,
24762
+ platforms: project.platforms,
24763
+ signal: expoDevAbort.signal,
24764
+ log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo ${message}`)),
24765
+ onPhaseTiming: ({ durationMs, phase }) => {
24766
+ sendTelemetryEvent("mobile:expo-dev-phase", {
24767
+ durationMs: Math.round(durationMs),
24768
+ host: detectAbsoluteMobileHost(),
24769
+ phase,
24770
+ platform: expoTelemetryPlatform(phase),
24771
+ provider: "expo"
24772
+ });
24773
+ },
24774
+ onStateChange: (state) => {
24775
+ if (state === "ready" || state === "closed")
24776
+ return;
24777
+ expoDevState = state;
24778
+ printNativeOutput(cliTag("\x1B[35m", `Expo development: ${state}.`));
24779
+ }
24780
+ });
24781
+ localNativeSession = session;
24782
+ return session;
24783
+ });
24784
+ const remoteIosProject = project.remoteIos;
24785
+ const remoteStart = remoteIosProject ? metroStart.then(async () => {
24786
+ const session = await startAbsoluteRemoteExpoIosDevSession({
24787
+ certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
24788
+ deviceIdentifier: options.iosDevice,
24789
+ https: httpsEnabled,
24790
+ metroPort: expoMetroPort,
24791
+ port,
24792
+ project: remoteIosProject,
24793
+ serverHost: options.iosDevice ? iosPhysicalServerHost : undefined,
24794
+ signal: expoDevAbort.signal,
24795
+ log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo [remote-ios] ${message}`)),
24796
+ onPhaseTiming: ({ durationMs, phase }) => {
24797
+ sendTelemetryEvent("mobile:expo-dev-phase", {
24798
+ durationMs: Math.round(durationMs),
24799
+ host: "remote-macos",
24800
+ phase,
24801
+ platform: expoTelemetryPlatform(phase),
24802
+ provider: "expo"
24803
+ });
24804
+ },
24805
+ onStateChange: (state) => {
24806
+ if (state === "ready" || state === "closed")
24807
+ return;
24808
+ expoDevState = state;
24809
+ printNativeOutput(cliTag("\x1B[35m", `Expo Remote Mac iOS: ${state}.`));
24810
+ }
24811
+ });
24812
+ remoteSession = session;
24813
+ return session;
24814
+ }) : Promise.resolve(undefined);
24815
+ expoDevStart = Promise.all([metroStart, localNativeStart, remoteStart]).then(async ([metro, localNative, remote]) => {
24816
+ const session = {
24817
+ metroPort: metro.metroPort,
24818
+ platforms: [
24819
+ ...new Set([
24820
+ ...localNative?.platforms ?? [],
24821
+ ...remote?.platforms ?? []
24822
+ ])
24823
+ ],
24824
+ timings: {
24825
+ ...metro.timings,
24826
+ ...localNative?.timings,
24827
+ ...remote?.timings
24828
+ },
24829
+ close: async () => {
24830
+ await Promise.all([
24831
+ metro.close(),
24832
+ localNative?.close(),
24833
+ remote?.close()
24834
+ ]);
24835
+ }
24836
+ };
24501
24837
  if (cleaning) {
24502
24838
  await session.close();
24503
24839
  } else {
24504
24840
  expoDevSession = session;
24841
+ expoDevState = "ready";
24505
24842
  sendTelemetryEvent("mobile:expo-dev-ready", {
24506
24843
  metroPort: session.metroPort,
24507
24844
  platforms: session.platforms,
24508
24845
  provider: "expo",
24846
+ remoteIos: project.remoteIos !== undefined,
24509
24847
  timings: session.timings
24510
24848
  });
24511
24849
  printNativeOutput(cliTag("\x1B[35m", `Expo ${session.platforms.join(" + ")} connected; native Fast Refresh uses Metro on ${session.metroPort}, and AbsoluteJS pages use HMR on ${port}.`));
24512
24850
  }
24513
24851
  }).catch((error) => {
24852
+ expoDevAbort.abort();
24853
+ Promise.all([
24854
+ metroSession?.close().catch(() => {
24855
+ return;
24856
+ }),
24857
+ localNativeSession?.close().catch(() => {
24858
+ return;
24859
+ }),
24860
+ remoteSession?.close().catch(() => {
24861
+ return;
24862
+ })
24863
+ ]);
24514
24864
  expoDevState = "failed";
24515
24865
  if (cleaning && error instanceof Error && error.name === "AbortError") {
24516
24866
  return;
24517
24867
  }
24518
24868
  sendTelemetryEvent("mobile:expo-dev-failed", {
24519
24869
  phase: expoDevState,
24520
- platforms: project.platforms,
24870
+ platforms: [
24871
+ ...project.platforms,
24872
+ ...project.remoteIos ? ["ios"] : []
24873
+ ],
24521
24874
  provider: "expo"
24522
24875
  });
24523
24876
  console.error(cliTag("\x1B[31m", `Expo development failed: ${error instanceof Error ? error.message : String(error)}`));
@@ -25055,7 +25408,10 @@ var dev = async (serverEntry, configPath2, options = {}) => {
25055
25408
  console.log(cliTag("\x1B[35m", `iOS target: ${target}; HMR port ${port}.`));
25056
25409
  }
25057
25410
  if (expoDevProject) {
25058
- console.log(cliTag("\x1B[35m", `Expo targets: ${expoDevProject.platforms.join(", ")}; state ${expoDevState}; Metro ${expoMetroPort ?? "not allocated"}; AbsoluteJS HMR ${port}.`));
25411
+ console.log(cliTag("\x1B[35m", `Expo targets: ${[
25412
+ ...expoDevProject.platforms,
25413
+ ...expoDevProject.remoteIos ? ["ios (Remote Mac)"] : []
25414
+ ].join(", ")}; state ${expoDevState}; Metro ${expoMetroPort ?? "not allocated"}; AbsoluteJS HMR ${port}.`));
25059
25415
  }
25060
25416
  },
25061
25417
  relaunchDevice: async () => {