@absolutejs/absolute 0.20.0-beta.42 → 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
@@ -4488,7 +4488,7 @@ var init_iosSimulatorController = __esm(() => {
4488
4488
  });
4489
4489
 
4490
4490
  // src/mobile/remoteMacWire.ts
4491
- 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;
4492
4492
 
4493
4493
  // src/mobile/remoteMacProtocol.ts
4494
4494
  import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
@@ -4504,7 +4504,7 @@ import {
4504
4504
  resolve as resolvePath,
4505
4505
  sep as sep4
4506
4506
  } from "path";
4507
- 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 = () => ({
4508
4508
  format: PROFILE_FORMAT,
4509
4509
  profiles: {}
4510
4510
  }), loadStore = async (path = defaultProfilePath()) => {
@@ -4675,7 +4675,11 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4675
4675
  }
4676
4676
  await saveStore(store, profilePath);
4677
4677
  return true;
4678
- }, 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) => ({
4679
4683
  cap: join13(resolvePath(projectRoot), "node_modules", ".bin", "cap"),
4680
4684
  config,
4681
4685
  nativeDirectory: join13(config.nativeProjectDirectory, "ios"),
@@ -4762,6 +4766,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4762
4766
  appId: project.config.appId,
4763
4767
  appName: project.config.appName,
4764
4768
  bundleDirectory: portableRelativePath(project.projectRoot, project.config.bundleDirectory),
4769
+ engine: project.config.engine,
4765
4770
  ...project.config.deepLinkScheme || project.config.deepLinkHosts.length > 1 || project.config.appleAppIdPrefix ? {
4766
4771
  deepLinks: {
4767
4772
  ...project.config.deepLinkScheme ? { scheme: project.config.deepLinkScheme } : {},
@@ -4774,6 +4779,16 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4774
4779
  }
4775
4780
  } : {},
4776
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
+ } : {},
4777
4792
  ...project.config.iosVersion ? { ios: { version: project.config.iosVersion } } : {},
4778
4793
  nativeProject: {
4779
4794
  directory: portableRelativePath(project.projectRoot, project.config.nativeProjectDirectory),
@@ -4861,7 +4876,30 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4861
4876
  } finally {
4862
4877
  reader.releaseLock();
4863
4878
  }
4864
- }, 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) => {
4865
4903
  const startedAt = performance.now();
4866
4904
  const transport = options.transport ?? defaultTransport;
4867
4905
  const installAgent = options.installAgent ?? installAbsoluteRemoteMacAgent;
@@ -4875,9 +4913,22 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4875
4913
  const encodedConfig = Buffer.from(JSON.stringify(portableMobileConfig(options.project))).toString("base64url");
4876
4914
  const encodedCertificateAuthority = options.certificateAuthorityPath ? (await readFile8(options.certificateAuthorityPath)).toString("base64url") : undefined;
4877
4915
  const physicalDevice = options.deviceIdentifier !== undefined;
4878
- let relayPort;
4879
- if (physicalDevice)
4880
- 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;
4881
4932
  if (physicalDevice && !options.serverHost)
4882
4933
  throw new Error("Remote physical iOS development requires the Remote Mac LAN host.");
4883
4934
  const remoteCommand = [
@@ -4890,6 +4941,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4890
4941
  String(options.port),
4891
4942
  "--mobile-config",
4892
4943
  shellQuote(encodedConfig),
4944
+ ...expo && options.metroPort !== undefined ? ["--metro-port", String(options.metroPort)] : [],
4893
4945
  ...encodedCertificateAuthority ? [
4894
4946
  "--certificate-authority",
4895
4947
  shellQuote(encodedCertificateAuthority)
@@ -4901,7 +4953,8 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4901
4953
  "--server-host",
4902
4954
  shellQuote(options.serverHost ?? ""),
4903
4955
  "--relay-port",
4904
- String(relayPort)
4956
+ String(relayPort),
4957
+ ...expo && metroRelayPort !== undefined ? ["--metro-relay-port", String(metroRelayPort)] : []
4905
4958
  ] : []
4906
4959
  ].join(" ");
4907
4960
  const command = [
@@ -4910,6 +4963,10 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4910
4963
  "ExitOnForwardFailure=yes",
4911
4964
  "-R",
4912
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
+ ] : [],
4913
4970
  "/bin/sh -lc",
4914
4971
  shellQuote(remoteCommand)
4915
4972
  ];
@@ -4935,11 +4992,19 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4935
4992
  if (event.type === "native-log")
4936
4993
  options.nativeLog?.(event.entry);
4937
4994
  if (event.type === "state") {
4938
- ({ state } = event);
4939
- 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);
4940
5007
  }
4941
- if (event.type === "timing")
4942
- options.onPhaseTiming?.(event);
4943
5008
  if (event.type === "ready") {
4944
5009
  ready = event;
4945
5010
  resolveReady();
@@ -4981,9 +5046,26 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4981
5046
  pending.clear();
4982
5047
  return;
4983
5048
  });
4984
- 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
+ }
4985
5059
  if (!ready)
4986
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
+ }
4987
5069
  const totalDuration = performance.now() - startedAt;
4988
5070
  let currentReady = {
4989
5071
  ...ready,
@@ -4995,11 +5077,11 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4995
5077
  total: totalDuration
4996
5078
  }
4997
5079
  };
4998
- 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.`);
4999
5081
  const request = (commandName) => {
5000
5082
  const id = randomUUID4();
5001
5083
  const response = new Promise((resolve9, reject) => pending.set(id, { reject, resolve: resolve9 }));
5002
- 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 })}
5003
5085
  `);
5004
5086
  const flush = async () => {
5005
5087
  for (let attempt = 0;attempt < REMOTE_STDIN_FLUSH_ATTEMPTS; attempt++) {
@@ -5018,13 +5100,24 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
5018
5100
  if (closed)
5019
5101
  return;
5020
5102
  closed = true;
5021
- await request("close").catch(() => {
5022
- return;
5023
- });
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
+ ]);
5024
5110
  process2.stdin.end();
5025
- await process2.exited.catch(() => {
5026
- return;
5027
- });
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
+ }
5028
5121
  };
5029
5122
  const makeSession = () => ({
5030
5123
  close,
@@ -23371,7 +23464,8 @@ var commandEnvironment = (options) => ({
23371
23464
  ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH: options.certificateAuthorityPath
23372
23465
  } : {},
23373
23466
  ...options.androidOrigin ? { EXPO_PUBLIC_ABSOLUTE_DEV_ANDROID_ORIGIN: options.androidOrigin } : {},
23374
- ...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 } : {}
23375
23469
  });
23376
23470
  var absoluteExpoExecutable = async (project) => {
23377
23471
  const executable = join6(project, "node_modules", ".bin", "expo");
@@ -23385,8 +23479,8 @@ var absoluteExpoExecutable = async (project) => {
23385
23479
  var planAbsoluteExpoDevSession = (config, options) => {
23386
23480
  if (config.engine !== "expo")
23387
23481
  throw new TypeError("The Expo development controller requires Expo.");
23388
- if (options.platforms.length === 0)
23389
- 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.");
23390
23484
  const secureOrigin = [options.androidOrigin, options.iosOrigin].some((origin) => origin && new URL(origin).protocol === "https:");
23391
23485
  if (secureOrigin && !options.certificateAuthorityPath)
23392
23486
  throw new TypeError("Expo HTTPS development requires the AbsoluteJS development CA certificate.");
@@ -23403,7 +23497,7 @@ var planAbsoluteExpoDevSession = (config, options) => {
23403
23497
  env,
23404
23498
  role: "metro"
23405
23499
  };
23406
- const prepare = {
23500
+ const prepare = options.platforms.length === 0 ? undefined : {
23407
23501
  args: [
23408
23502
  "prebuild",
23409
23503
  "--clean",
@@ -23431,7 +23525,11 @@ var planAbsoluteExpoDevSession = (config, options) => {
23431
23525
  return command;
23432
23526
  });
23433
23527
  return {
23434
- commands: [prepare, metro, ...native],
23528
+ commands: [
23529
+ ...prepare ? [prepare] : [],
23530
+ ...options.metro === "external" ? [] : [metro],
23531
+ ...native
23532
+ ],
23435
23533
  metroPort: options.metroPort,
23436
23534
  project: config.nativeProjectDirectory
23437
23535
  };
@@ -23520,47 +23618,48 @@ var startAbsoluteExpoDevSession = async (options) => {
23520
23618
  const prepareCommand = plan.commands.find((command) => command.role === "native-prepare");
23521
23619
  const metroCommand = plan.commands.find((command) => command.role === "metro");
23522
23620
  const nativeCommands = plan.commands.filter((command) => command.role === "native-build");
23523
- if (!prepareCommand)
23524
- throw new TypeError("Expo native preparation command is missing.");
23525
- if (!metroCommand)
23526
- throw new TypeError("Expo Metro command is missing.");
23527
- setState("preparing-native");
23528
- const prepareStarted = performance.now();
23529
- const prepareProcess = run(executable, prepareCommand.args, {
23530
- cwd: plan.project,
23531
- env: { ...process.env, ...prepareCommand.env },
23532
- stdio: ["ignore", "pipe", "pipe"]
23533
- });
23534
- forwardLines(prepareProcess, (line) => {
23535
- if (line)
23536
- log(`[prebuild] ${line}`);
23537
- });
23538
- const abortPrepare = () => void stopProcess(prepareProcess);
23539
- options.signal?.addEventListener("abort", abortPrepare, { once: true });
23540
- const prepareExit = await waitForExit(prepareProcess);
23541
- options.signal?.removeEventListener("abort", abortPrepare);
23542
- if (options.signal?.aborted)
23543
- throw abortError();
23544
- if (prepareExit !== 0) {
23545
- setState("failed");
23546
- throw new Error(`Expo native preparation exited with status ${prepareExit}.`);
23547
- }
23548
- const prepareMs = performance.now() - prepareStarted;
23549
- timings["preparing-native"] = prepareMs;
23550
- options.onPhaseTiming?.({
23551
- durationMs: prepareMs,
23552
- phase: "preparing-native"
23553
- });
23554
- 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");
23555
23650
  const metroStarted = performance.now();
23556
- const metro = run(executable, metroCommand.args, {
23651
+ const metro = metroCommand ? run(executable, metroCommand.args, {
23557
23652
  cwd: plan.project,
23558
23653
  env: { ...process.env, ...metroCommand.env },
23559
23654
  stdio: ["ignore", "pipe", "pipe"]
23560
- });
23655
+ }) : undefined;
23561
23656
  let metroReady = false;
23562
23657
  let resolveMetro;
23563
23658
  const metroPromise = new Promise((resolve4, reject) => {
23659
+ if (!metro) {
23660
+ resolve4();
23661
+ return;
23662
+ }
23564
23663
  const timeout = setTimeout(() => {
23565
23664
  reject(new Error("Expo Metro did not become ready within 60 seconds."));
23566
23665
  }, METRO_READY_TIMEOUT_MS);
@@ -23575,15 +23674,19 @@ var startAbsoluteExpoDevSession = async (options) => {
23575
23674
  }
23576
23675
  });
23577
23676
  });
23578
- forwardLines(metro, (line) => {
23579
- if (line)
23580
- log(`[metro] ${line}`);
23581
- if (!metroReady && /(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(line)) {
23582
- metroReady = true;
23583
- resolveMetro?.();
23584
- }
23585
- });
23586
- 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
+ };
23587
23690
  options.signal?.addEventListener("abort", abort, { once: true });
23588
23691
  const runNativeCommand = async (command) => {
23589
23692
  if (options.signal?.aborted)
@@ -23664,12 +23767,8 @@ var startAbsoluteExpoDevSession = async (options) => {
23664
23767
  try {
23665
23768
  await startPhysicalIosEnrollment();
23666
23769
  await metroPromise;
23667
- const metroMs = performance.now() - metroStarted;
23668
- timings["starting-metro"] = metroMs;
23669
- options.onPhaseTiming?.({
23670
- durationMs: metroMs,
23671
- phase: "starting-metro"
23672
- });
23770
+ if (managedMetro)
23771
+ publishTiming("starting-metro", performance.now() - metroStarted);
23673
23772
  await runNativeCommands(nativeCommands);
23674
23773
  setState("ready");
23675
23774
  return {
@@ -23678,14 +23777,16 @@ var startAbsoluteExpoDevSession = async (options) => {
23678
23777
  timings,
23679
23778
  close: async () => {
23680
23779
  options.signal?.removeEventListener("abort", abort);
23681
- await stopProcess(metro);
23780
+ if (metro)
23781
+ await stopProcess(metro);
23682
23782
  await closeEnrollmentServer();
23683
23783
  setState("closed");
23684
23784
  }
23685
23785
  };
23686
23786
  } catch (error) {
23687
23787
  options.signal?.removeEventListener("abort", abort);
23688
- await stopProcess(metro);
23788
+ if (metro)
23789
+ await stopProcess(metro);
23689
23790
  await closeEnrollmentServer();
23690
23791
  setState("failed");
23691
23792
  throw error;
@@ -24092,9 +24193,6 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24092
24193
  throw new TypeError("--ios-device requires ios in mobile.platforms.");
24093
24194
  }
24094
24195
  if (options.iosDevice) {
24095
- if (mobileConfig?.engine === "expo" && detectAbsoluteMobileHost() !== "macos") {
24096
- throw new Error("Expo physical iOS development currently requires local macOS; Expo Remote Mac execution is the next adapter milestone.");
24097
- }
24098
24196
  if (detectAbsoluteMobileHost() === "macos")
24099
24197
  iosPhysicalServerHost = mobileReachableHost(resolvedDev.host);
24100
24198
  else {
@@ -24138,6 +24236,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24138
24236
  }
24139
24237
  }
24140
24238
  const platforms = [];
24239
+ let remoteIos;
24141
24240
  if (normalized.platforms.includes("android")) {
24142
24241
  const target = options.androidDevice ? "device" : "emulator";
24143
24242
  let ready = androidToolchainReady(await inspectAbsoluteMobileToolchain(), target);
@@ -24153,7 +24252,14 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24153
24252
  }
24154
24253
  if (normalized.platforms.includes("ios")) {
24155
24254
  if (detectAbsoluteMobileHost() !== "macos") {
24156
- 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
+ }
24157
24263
  } else {
24158
24264
  const target = options.iosDevice ? "device" : "simulator";
24159
24265
  let ready = iosToolchainReady(await inspectAbsoluteMobileToolchain(), target);
@@ -24168,13 +24274,17 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24168
24274
  platforms.push("ios");
24169
24275
  }
24170
24276
  }
24171
- if (platforms.length > 0) {
24277
+ if (platforms.length > 0 || remoteIos) {
24172
24278
  expoDevProject = {
24173
24279
  executable: await absoluteExpoExecutable(generated.path),
24174
24280
  mobile: normalized,
24175
- platforms
24281
+ platforms,
24282
+ ...remoteIos ? { remoteIos } : {}
24176
24283
  };
24177
- 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.`));
24178
24288
  }
24179
24289
  } else if (normalized.platforms.includes("android")) {
24180
24290
  const androidTarget = options.androidDevice ? "device" : "emulator";
@@ -24601,54 +24711,166 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24601
24711
  return;
24602
24712
  }
24603
24713
  const androidHost = options.androidDevice ? mobileReachableHost(resolvedDev.host) : "10.0.2.2";
24604
- const iosHost = options.iosDevice ? mobileReachableHost(resolvedDev.host) : "localhost";
24605
- expoDevStart = startAbsoluteExpoDevSession({
24606
- androidDevice: options.androidDevice,
24607
- 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,
24608
24722
  certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
24609
24723
  config: project.mobile,
24610
24724
  executable: project.executable,
24611
- iosDevice: options.iosDevice,
24612
- iosOrigin: project.platforms.includes("ios") ? absoluteDevOrigin(iosHost) : undefined,
24725
+ iosOrigin,
24613
24726
  metroPort: expoMetroPort,
24614
- platforms: project.platforms,
24727
+ platforms: [],
24615
24728
  signal: expoDevAbort.signal,
24616
24729
  log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo ${message}`)),
24617
24730
  onPhaseTiming: ({ durationMs, phase }) => {
24618
24731
  sendTelemetryEvent("mobile:expo-dev-phase", {
24619
24732
  durationMs: Math.round(durationMs),
24733
+ host: detectAbsoluteMobileHost(),
24620
24734
  phase,
24621
24735
  platform: expoTelemetryPlatform(phase),
24622
24736
  provider: "expo"
24623
24737
  });
24624
24738
  },
24625
24739
  onStateChange: (state) => {
24626
- expoDevState = state;
24627
24740
  if (state === "ready" || state === "closed")
24628
24741
  return;
24742
+ expoDevState = state;
24629
24743
  printNativeOutput(cliTag("\x1B[35m", `Expo development: ${state}.`));
24630
24744
  }
24631
- }).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
+ };
24632
24837
  if (cleaning) {
24633
24838
  await session.close();
24634
24839
  } else {
24635
24840
  expoDevSession = session;
24841
+ expoDevState = "ready";
24636
24842
  sendTelemetryEvent("mobile:expo-dev-ready", {
24637
24843
  metroPort: session.metroPort,
24638
24844
  platforms: session.platforms,
24639
24845
  provider: "expo",
24846
+ remoteIos: project.remoteIos !== undefined,
24640
24847
  timings: session.timings
24641
24848
  });
24642
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}.`));
24643
24850
  }
24644
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
+ ]);
24645
24864
  expoDevState = "failed";
24646
24865
  if (cleaning && error instanceof Error && error.name === "AbortError") {
24647
24866
  return;
24648
24867
  }
24649
24868
  sendTelemetryEvent("mobile:expo-dev-failed", {
24650
24869
  phase: expoDevState,
24651
- platforms: project.platforms,
24870
+ platforms: [
24871
+ ...project.platforms,
24872
+ ...project.remoteIos ? ["ios"] : []
24873
+ ],
24652
24874
  provider: "expo"
24653
24875
  });
24654
24876
  console.error(cliTag("\x1B[31m", `Expo development failed: ${error instanceof Error ? error.message : String(error)}`));
@@ -25186,7 +25408,10 @@ var dev = async (serverEntry, configPath2, options = {}) => {
25186
25408
  console.log(cliTag("\x1B[35m", `iOS target: ${target}; HMR port ${port}.`));
25187
25409
  }
25188
25410
  if (expoDevProject) {
25189
- 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}.`));
25190
25415
  }
25191
25416
  },
25192
25417
  relaunchDevice: async () => {