@absolutejs/absolute 0.20.0-beta.98 → 0.20.0-beta.99

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/CHANGELOG.md CHANGED
@@ -6,6 +6,13 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
8
 
9
+ ## 0.20.0-beta.99 — 2026-09-15
10
+
11
+ ### Fixed
12
+
13
+ - **Expo development recovers from Metro startup port races** (`AbsoluteExpoDevPhaseTiming`, `AbsoluteExpoDevSession`, `StartAbsoluteExpoDevOptions`, `startAbsoluteExpoDevSession`)
14
+ AbsoluteJS retries a newly allocated loopback port without prompting, propagates the final port through local, WSL, and Remote Mac native targets, preserves it across rebuilds, and emits only a categorical reason plus bounded retry count.
15
+
9
16
  ## 0.20.0-beta.98 — 2026-09-15
10
17
 
11
18
  ### Added
package/changelog.json CHANGED
@@ -2,6 +2,23 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/absolute",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "detail": "AbsoluteJS retries a newly allocated loopback port without prompting, propagates the final port through local, WSL, and Remote Mac native targets, preserves it across rebuilds, and emits only a categorical reason plus bounded retry count.",
9
+ "kind": "fixed",
10
+ "summary": "Expo development recovers from Metro startup port races",
11
+ "symbols": [
12
+ "AbsoluteExpoDevPhaseTiming",
13
+ "AbsoluteExpoDevSession",
14
+ "StartAbsoluteExpoDevOptions",
15
+ "startAbsoluteExpoDevSession"
16
+ ]
17
+ }
18
+ ],
19
+ "date": "2026-09-15",
20
+ "version": "0.20.0-beta.99"
21
+ },
5
22
  {
6
23
  "changes": [
7
24
  {
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  var __require = import.meta.require;
3
3
 
4
- // .angular-partial-tmp-QYKjNJ/src/core/streamingSlotRegistrar.ts
4
+ // .angular-partial-tmp-lbMe6j/src/core/streamingSlotRegistrar.ts
5
5
  var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
6
6
  var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
7
7
  var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  var __require = import.meta.require;
3
3
 
4
- // .angular-partial-tmp-QYKjNJ/src/core/streamingSlotRegistrar.ts
4
+ // .angular-partial-tmp-lbMe6j/src/core/streamingSlotRegistrar.ts
5
5
  var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
6
6
  var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
7
7
  var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
@@ -48,7 +48,7 @@ var warnMissingStreamingSlotCollector = (primitiveName) => {
48
48
  getWarningController()?.maybeWarn(primitiveName);
49
49
  };
50
50
 
51
- // .angular-partial-tmp-QYKjNJ/src/core/streamingSlotRegistry.ts
51
+ // .angular-partial-tmp-lbMe6j/src/core/streamingSlotRegistry.ts
52
52
  var STREAMING_SLOT_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotAsyncLocalStorage");
53
53
  var isObjectRecord2 = (value) => Boolean(value) && typeof value === "object";
54
54
  var isAsyncLocalStorage = (value) => isObjectRecord2(value) && ("getStore" in value) && typeof value.getStore === "function" && ("run" in value) && typeof value.run === "function";
@@ -1287,7 +1287,7 @@ var loadAbsoluteMobileDevModules = async () => {
1287
1287
  ] = await Promise.all([
1288
1288
  import("./config-vy04mqce.js"),
1289
1289
  import("./expoProject-ceey4cfg.js"),
1290
- import("./expoDevController-a0jk8d43.js"),
1290
+ import("./expoDevController-8wyrcy6a.js"),
1291
1291
  import("./nativeAuth-zx4tsb1f.js"),
1292
1292
  import("./androidEmulatorController-nybhzc6c.js"),
1293
1293
  import("./emulatorDoctor-y2nqmaha.js"),
@@ -2174,13 +2174,15 @@ var dev = async (serverEntry, configPath, options = {}) => {
2174
2174
  platforms: [],
2175
2175
  signal: expoDevAbort.signal,
2176
2176
  log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo ${message}`)),
2177
- onPhaseTiming: ({ durationMs, phase }) => {
2177
+ onPhaseTiming: ({ durationMs, phase, reason, retryCount }) => {
2178
2178
  sendTelemetryEvent("mobile:expo-dev-phase", {
2179
2179
  durationMs: Math.round(durationMs),
2180
2180
  host: mobile.detectAbsoluteMobileHost(),
2181
2181
  phase,
2182
2182
  platform: expoTelemetryPlatform(phase),
2183
- provider: "expo"
2183
+ provider: "expo",
2184
+ reason,
2185
+ retryCount
2184
2186
  });
2185
2187
  },
2186
2188
  onStateChange: (state) => {
@@ -2193,7 +2195,7 @@ var dev = async (serverEntry, configPath, options = {}) => {
2193
2195
  metroSession = session;
2194
2196
  return session;
2195
2197
  });
2196
- const localNativeStart = metroStart.then(async () => {
2198
+ const localNativeStart = metroStart.then(async (metro) => {
2197
2199
  if (project.platforms.length === 0)
2198
2200
  return;
2199
2201
  const session = await mobile.startAbsoluteExpoDevSession({
@@ -2205,17 +2207,19 @@ var dev = async (serverEntry, configPath, options = {}) => {
2205
2207
  iosDevice: options.iosDevice,
2206
2208
  iosOrigin,
2207
2209
  metro: "external",
2208
- metroPort: expoMetroPort,
2210
+ metroPort: metro.metroPort,
2209
2211
  platforms: project.platforms,
2210
2212
  signal: expoDevAbort.signal,
2211
2213
  log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo ${message}`)),
2212
- onPhaseTiming: ({ durationMs, phase }) => {
2214
+ onPhaseTiming: ({ durationMs, phase, reason, retryCount }) => {
2213
2215
  sendTelemetryEvent("mobile:expo-dev-phase", {
2214
2216
  durationMs: Math.round(durationMs),
2215
2217
  host: mobile.detectAbsoluteMobileHost(),
2216
2218
  phase,
2217
2219
  platform: expoTelemetryPlatform(phase),
2218
- provider: "expo"
2220
+ provider: "expo",
2221
+ reason,
2222
+ retryCount
2219
2223
  });
2220
2224
  },
2221
2225
  onStateChange: (state) => {
@@ -2229,24 +2233,31 @@ var dev = async (serverEntry, configPath, options = {}) => {
2229
2233
  return session;
2230
2234
  });
2231
2235
  const remoteIosProject = project.remoteIos;
2232
- const remoteStart = remoteIosProject ? metroStart.then(async () => {
2236
+ const remoteStart = remoteIosProject ? metroStart.then(async (metro) => {
2233
2237
  const session = await mobile.startAbsoluteRemoteExpoIosDevSession({
2234
2238
  certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
2235
2239
  deviceIdentifier: options.iosDevice,
2236
2240
  https: httpsEnabled,
2237
- metroPort: expoMetroPort,
2241
+ metroPort: metro.metroPort,
2238
2242
  port,
2239
2243
  project: remoteIosProject,
2240
2244
  serverHost: options.iosDevice ? iosPhysicalServerHost : undefined,
2241
2245
  signal: expoDevAbort.signal,
2242
2246
  log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo [remote-ios] ${message}`)),
2243
- onPhaseTiming: ({ durationMs, phase }) => {
2247
+ onPhaseTiming: ({
2248
+ durationMs,
2249
+ phase,
2250
+ reason,
2251
+ retryCount
2252
+ }) => {
2244
2253
  sendTelemetryEvent("mobile:expo-dev-phase", {
2245
2254
  durationMs: Math.round(durationMs),
2246
2255
  host: "remote-macos",
2247
2256
  phase,
2248
2257
  platform: expoTelemetryPlatform(phase),
2249
- provider: "expo"
2258
+ provider: "expo",
2259
+ reason,
2260
+ retryCount
2250
2261
  });
2251
2262
  },
2252
2263
  onStateChange: (state) => {
@@ -15,9 +15,39 @@ import"./index-mywsk201.js";
15
15
  // src/mobile/expoDevController.ts
16
16
  import { spawn } from "child_process";
17
17
  import { access } from "fs/promises";
18
+ import { createServer } from "net";
18
19
  import { join, resolve as resolvePath } from "path";
19
20
  var METRO_READY_TIMEOUT_MS = 120000;
21
+ var METRO_START_MAX_ATTEMPTS = 4;
22
+ var MAX_TCP_PORT = 65535;
20
23
  var PROCESS_CLOSE_TIMEOUT_MS = 2000;
24
+ var allocateAvailableMetroPort = () => new Promise((resolve, reject) => {
25
+ const server = createServer();
26
+ server.unref();
27
+ server.once("error", reject);
28
+ server.listen(0, "127.0.0.1", () => {
29
+ const address = server.address();
30
+ if (!address || typeof address === "string") {
31
+ server.close();
32
+ reject(new Error("Failed to allocate a replacement Metro port."));
33
+ return;
34
+ }
35
+ server.close((error) => {
36
+ if (error)
37
+ reject(error);
38
+ else
39
+ resolve(address.port);
40
+ });
41
+ });
42
+ });
43
+ var metroPortConflict = (line) => /(?:EADDRINUSE|address already in use|port \d+ is being used by another process)/iu.test(line);
44
+
45
+ class MetroStartupError extends Error {
46
+ constructor(exitCode, conflict) {
47
+ super(`Expo Metro exited with status ${exitCode}.`);
48
+ this.conflict = conflict;
49
+ }
50
+ }
21
51
  var preserveWindowsSubstRealpaths = `
22
52
  const fs = require('node:fs');
23
53
  const path = require('node:path');
@@ -512,7 +542,7 @@ var parseBootedAbsoluteExpoIosSimulators = (source) => {
512
542
  });
513
543
  };
514
544
  var startAbsoluteExpoDevSession = async (options) => {
515
- const plan = planAbsoluteExpoDevSession(options.config, options);
545
+ let plan = planAbsoluteExpoDevSession(options.config, options);
516
546
  const executable = options.executable ?? await absoluteExpoExecutable(plan.project);
517
547
  const run = options.spawnProcess ?? spawn;
518
548
  const host = options.host ?? detectAbsoluteMobileHost();
@@ -527,9 +557,9 @@ var startAbsoluteExpoDevSession = async (options) => {
527
557
  caEnrollmentServer = null;
528
558
  await server?.close();
529
559
  };
530
- const publishTiming = (phase, durationMs) => {
560
+ const publishTiming = (phase, durationMs, details = {}) => {
531
561
  timings[phase] = (timings[phase] ?? 0) + durationMs;
532
- options.onPhaseTiming?.({ durationMs, phase });
562
+ options.onPhaseTiming?.({ durationMs, phase, ...details });
533
563
  };
534
564
  const setState = (state) => {
535
565
  options.onStateChange?.(state);
@@ -537,8 +567,8 @@ var startAbsoluteExpoDevSession = async (options) => {
537
567
  if (options.signal?.aborted)
538
568
  throw abortError();
539
569
  const prepareCommand = plan.commands.find((command) => command.role === "native-prepare");
540
- const metroCommand = plan.commands.find((command) => command.role === "metro");
541
- const nativeCommands = plan.commands.filter((command) => command.role === "native-build");
570
+ let metroCommand = plan.commands.find((command) => command.role === "metro");
571
+ let nativeCommands = plan.commands.filter((command) => command.role === "native-build");
542
572
  const runPrepareCommand = async (command) => {
543
573
  setState("preparing-native");
544
574
  const prepareStarted = performance.now();
@@ -569,41 +599,67 @@ var startAbsoluteExpoDevSession = async (options) => {
569
599
  if (managedMetro)
570
600
  setState("starting-metro");
571
601
  const metroStarted = performance.now();
572
- const metro = metroCommand ? run(executable, metroCommand.args, {
573
- cwd: plan.project,
574
- env: { ...process.env, ...metroCommand.env },
575
- stdio: ["ignore", "pipe", "pipe"]
576
- }) : undefined;
577
- let metroReady = false;
578
- let resolveMetro;
579
- const metroPromise = new Promise((resolve, reject) => {
580
- if (!metro) {
581
- resolve();
582
- return;
583
- }
602
+ let metro;
603
+ let metroRetryCount = 0;
604
+ const attemptedMetroPorts = [plan.metroPort];
605
+ const waitForMetroReady = (child) => new Promise((resolve, reject) => {
606
+ let ready = false;
607
+ let conflict = false;
584
608
  const timeout = setTimeout(() => {
585
609
  reject(new Error("Expo Metro did not become ready within 120 seconds."));
586
610
  }, METRO_READY_TIMEOUT_MS);
587
- resolveMetro = () => {
588
- clearTimeout(timeout);
589
- resolve();
590
- };
591
- metro.once("exit", (code) => {
592
- if (!metroReady) {
611
+ forwardLines(child, (line) => {
612
+ if (line)
613
+ log(`[metro] ${line}`);
614
+ if (metroPortConflict(line))
615
+ conflict = true;
616
+ if (!ready && /(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(line)) {
617
+ ready = true;
593
618
  clearTimeout(timeout);
594
- reject(new Error(`Expo Metro exited with status ${code ?? 1}.`));
619
+ resolve();
595
620
  }
596
621
  });
622
+ child.once("exit", (code) => {
623
+ if (ready)
624
+ return;
625
+ clearTimeout(timeout);
626
+ reject(new MetroStartupError(code ?? 1, conflict));
627
+ });
597
628
  });
598
- if (metro)
599
- forwardLines(metro, (line) => {
600
- if (line)
601
- log(`[metro] ${line}`);
602
- if (!metroReady && /(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(line)) {
603
- metroReady = true;
604
- resolveMetro?.();
605
- }
629
+ const startManagedMetro = async () => {
630
+ if (!metroCommand)
631
+ return;
632
+ if (options.signal?.aborted)
633
+ throw abortError();
634
+ metro = run(executable, metroCommand.args, {
635
+ cwd: plan.project,
636
+ env: { ...process.env, ...metroCommand.env },
637
+ stdio: ["ignore", "pipe", "pipe"]
606
638
  });
639
+ try {
640
+ await waitForMetroReady(metro);
641
+ } catch (error) {
642
+ if (!(error instanceof MetroStartupError) || !error.conflict || attemptedMetroPorts.length >= METRO_START_MAX_ATTEMPTS) {
643
+ throw error;
644
+ }
645
+ await stopProcess(metro);
646
+ const allocate = options.allocateMetroPort ?? allocateAvailableMetroPort;
647
+ const replacementPort = await allocate(attemptedMetroPorts);
648
+ if (!Number.isSafeInteger(replacementPort) || replacementPort < 1 || replacementPort > MAX_TCP_PORT || attemptedMetroPorts.includes(replacementPort)) {
649
+ throw new Error("Could not allocate a distinct replacement Metro port.", { cause: error });
650
+ }
651
+ attemptedMetroPorts.push(replacementPort);
652
+ metroRetryCount += 1;
653
+ log("Metro port was claimed during startup; retrying automatically on a new port.");
654
+ plan = planAbsoluteExpoDevSession(options.config, {
655
+ ...options,
656
+ metroPort: replacementPort
657
+ });
658
+ metroCommand = plan.commands.find((command) => command.role === "metro");
659
+ nativeCommands = plan.commands.filter((command) => command.role === "native-build");
660
+ await startManagedMetro();
661
+ }
662
+ };
607
663
  const abort = () => {
608
664
  if (metro)
609
665
  stopProcess(metro);
@@ -618,7 +674,7 @@ var startAbsoluteExpoDevSession = async (options) => {
618
674
  const state = platform === "android" ? "building-android" : "building-ios";
619
675
  setState(state);
620
676
  const started = performance.now();
621
- const invocation = platform === "android" && host === "wsl" ? encodedWindowsExpoAndroidCommand(plan.project, options.androidRoot ?? absoluteManagedAndroidSdkRoot(host), options.config.appId, command.args, capture, options.metroPort, options.androidDevice) : [executable, ...command.args];
677
+ const invocation = platform === "android" && host === "wsl" ? encodedWindowsExpoAndroidCommand(plan.project, options.androidRoot ?? absoluteManagedAndroidSdkRoot(host), options.config.appId, command.args, capture, plan.metroPort, options.androidDevice) : [executable, ...command.args];
622
678
  if (platform === "android" && host === "wsl") {
623
679
  log("[android] Mirroring native inputs to the Windows host for accelerated Expo build and launch.");
624
680
  }
@@ -646,7 +702,7 @@ var startAbsoluteExpoDevSession = async (options) => {
646
702
  if (platform === "android" && host !== "wsl") {
647
703
  const androidRoot = options.androidRoot ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? absoluteManagedAndroidSdkRoot(host);
648
704
  const adb = options.androidAdb ?? join(androidRoot, "platform-tools", host === "windows" ? "adb.exe" : "adb");
649
- connectLocalExpoAndroid(adb, options.config.appId, options.metroPort, capture, options.androidDevice);
705
+ connectLocalExpoAndroid(adb, options.config.appId, plan.metroPort, capture, options.androidDevice);
650
706
  log("Connected the Expo Android development client to Metro.");
651
707
  }
652
708
  if (platform === "ios" && options.certificateAuthorityPath && options.iosOrigin && new URL(options.iosOrigin).protocol === "https:" && !options.iosDevice) {
@@ -684,7 +740,7 @@ var startAbsoluteExpoDevSession = async (options) => {
684
740
  "simctl",
685
741
  "openurl",
686
742
  "booted",
687
- expoDevelopmentClientUrl(options.config.appId, options.metroHost ?? "localhost", options.metroPort)
743
+ expoDevelopmentClientUrl(options.config.appId, options.metroHost ?? "localhost", plan.metroPort)
688
744
  ], utilityOptions);
689
745
  if (connectExit !== 0)
690
746
  throw new Error(`Expo iOS Simulator Metro connection exited with status ${connectExit}.`);
@@ -717,9 +773,14 @@ var startAbsoluteExpoDevSession = async (options) => {
717
773
  };
718
774
  try {
719
775
  await startPhysicalIosEnrollment();
720
- await metroPromise;
776
+ await startManagedMetro();
721
777
  if (managedMetro)
722
- publishTiming("starting-metro", performance.now() - metroStarted);
778
+ publishTiming("starting-metro", performance.now() - metroStarted, {
779
+ ...metroRetryCount > 0 ? {
780
+ reason: "metro-port-conflict",
781
+ retryCount: metroRetryCount
782
+ } : {}
783
+ });
723
784
  await runNativeCommands(nativeCommands);
724
785
  setState("ready");
725
786
  let closed = false;
package/dist/cli/index.js CHANGED
@@ -58,7 +58,7 @@ if (command === "dev") {
58
58
  }
59
59
  const positionalArgs = stripNamedArgs("--config", "--android-device", "--ios-device").filter((arg) => arg !== "--no-mobile" && arg !== "--eager");
60
60
  const serverEntry = positionalArgs[0] ?? DEFAULT_SERVER_ENTRY;
61
- const { dev } = await import("./dev-r8dztrp8.js");
61
+ const { dev } = await import("./dev-r4vwdz6b.js");
62
62
  await dev(serverEntry, configPath, {
63
63
  androidDevice,
64
64
  eager: args.includes("--eager"),
@@ -30584,9 +30584,39 @@ var generateAbsoluteExpoCodeSigning = async (options) => {
30584
30584
  // src/mobile/expoDevController.ts
30585
30585
  import { spawn } from "child_process";
30586
30586
  import { access as access11 } from "fs/promises";
30587
+ import { createServer as createServer2 } from "net";
30587
30588
  import { join as join18, resolve as resolvePath4 } from "path";
30588
30589
  var METRO_READY_TIMEOUT_MS = 120000;
30590
+ var METRO_START_MAX_ATTEMPTS = 4;
30591
+ var MAX_TCP_PORT = 65535;
30589
30592
  var PROCESS_CLOSE_TIMEOUT_MS = 2000;
30593
+ var allocateAvailableMetroPort = () => new Promise((resolve15, reject) => {
30594
+ const server = createServer2();
30595
+ server.unref();
30596
+ server.once("error", reject);
30597
+ server.listen(0, "127.0.0.1", () => {
30598
+ const address = server.address();
30599
+ if (!address || typeof address === "string") {
30600
+ server.close();
30601
+ reject(new Error("Failed to allocate a replacement Metro port."));
30602
+ return;
30603
+ }
30604
+ server.close((error) => {
30605
+ if (error)
30606
+ reject(error);
30607
+ else
30608
+ resolve15(address.port);
30609
+ });
30610
+ });
30611
+ });
30612
+ var metroPortConflict = (line) => /(?:EADDRINUSE|address already in use|port \d+ is being used by another process)/iu.test(line);
30613
+
30614
+ class MetroStartupError extends Error {
30615
+ constructor(exitCode, conflict) {
30616
+ super(`Expo Metro exited with status ${exitCode}.`);
30617
+ this.conflict = conflict;
30618
+ }
30619
+ }
30590
30620
  var preserveWindowsSubstRealpaths = `
30591
30621
  const fs = require('node:fs');
30592
30622
  const path = require('node:path');
@@ -31081,7 +31111,7 @@ var parseBootedAbsoluteExpoIosSimulators = (source) => {
31081
31111
  });
31082
31112
  };
31083
31113
  var startAbsoluteExpoDevSession = async (options) => {
31084
- const plan = planAbsoluteExpoDevSession(options.config, options);
31114
+ let plan = planAbsoluteExpoDevSession(options.config, options);
31085
31115
  const executable = options.executable ?? await absoluteExpoExecutable(plan.project);
31086
31116
  const run = options.spawnProcess ?? spawn;
31087
31117
  const host3 = options.host ?? detectAbsoluteMobileHost();
@@ -31096,9 +31126,9 @@ var startAbsoluteExpoDevSession = async (options) => {
31096
31126
  caEnrollmentServer = null;
31097
31127
  await server?.close();
31098
31128
  };
31099
- const publishTiming = (phase, durationMs) => {
31129
+ const publishTiming = (phase, durationMs, details = {}) => {
31100
31130
  timings[phase] = (timings[phase] ?? 0) + durationMs;
31101
- options.onPhaseTiming?.({ durationMs, phase });
31131
+ options.onPhaseTiming?.({ durationMs, phase, ...details });
31102
31132
  };
31103
31133
  const setState = (state) => {
31104
31134
  options.onStateChange?.(state);
@@ -31106,8 +31136,8 @@ var startAbsoluteExpoDevSession = async (options) => {
31106
31136
  if (options.signal?.aborted)
31107
31137
  throw abortError();
31108
31138
  const prepareCommand = plan.commands.find((command) => command.role === "native-prepare");
31109
- const metroCommand = plan.commands.find((command) => command.role === "metro");
31110
- const nativeCommands = plan.commands.filter((command) => command.role === "native-build");
31139
+ let metroCommand = plan.commands.find((command) => command.role === "metro");
31140
+ let nativeCommands = plan.commands.filter((command) => command.role === "native-build");
31111
31141
  const runPrepareCommand = async (command) => {
31112
31142
  setState("preparing-native");
31113
31143
  const prepareStarted = performance.now();
@@ -31138,41 +31168,67 @@ var startAbsoluteExpoDevSession = async (options) => {
31138
31168
  if (managedMetro)
31139
31169
  setState("starting-metro");
31140
31170
  const metroStarted = performance.now();
31141
- const metro = metroCommand ? run(executable, metroCommand.args, {
31142
- cwd: plan.project,
31143
- env: { ...process.env, ...metroCommand.env },
31144
- stdio: ["ignore", "pipe", "pipe"]
31145
- }) : undefined;
31146
- let metroReady = false;
31147
- let resolveMetro;
31148
- const metroPromise = new Promise((resolve15, reject) => {
31149
- if (!metro) {
31150
- resolve15();
31151
- return;
31152
- }
31171
+ let metro;
31172
+ let metroRetryCount = 0;
31173
+ const attemptedMetroPorts = [plan.metroPort];
31174
+ const waitForMetroReady = (child) => new Promise((resolve15, reject) => {
31175
+ let ready = false;
31176
+ let conflict = false;
31153
31177
  const timeout = setTimeout(() => {
31154
31178
  reject(new Error("Expo Metro did not become ready within 120 seconds."));
31155
31179
  }, METRO_READY_TIMEOUT_MS);
31156
- resolveMetro = () => {
31157
- clearTimeout(timeout);
31158
- resolve15();
31159
- };
31160
- metro.once("exit", (code) => {
31161
- if (!metroReady) {
31180
+ forwardLines(child, (line) => {
31181
+ if (line)
31182
+ log(`[metro] ${line}`);
31183
+ if (metroPortConflict(line))
31184
+ conflict = true;
31185
+ if (!ready && /(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(line)) {
31186
+ ready = true;
31162
31187
  clearTimeout(timeout);
31163
- reject(new Error(`Expo Metro exited with status ${code ?? 1}.`));
31188
+ resolve15();
31164
31189
  }
31165
31190
  });
31191
+ child.once("exit", (code) => {
31192
+ if (ready)
31193
+ return;
31194
+ clearTimeout(timeout);
31195
+ reject(new MetroStartupError(code ?? 1, conflict));
31196
+ });
31166
31197
  });
31167
- if (metro)
31168
- forwardLines(metro, (line) => {
31169
- if (line)
31170
- log(`[metro] ${line}`);
31171
- if (!metroReady && /(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(line)) {
31172
- metroReady = true;
31173
- resolveMetro?.();
31174
- }
31198
+ const startManagedMetro = async () => {
31199
+ if (!metroCommand)
31200
+ return;
31201
+ if (options.signal?.aborted)
31202
+ throw abortError();
31203
+ metro = run(executable, metroCommand.args, {
31204
+ cwd: plan.project,
31205
+ env: { ...process.env, ...metroCommand.env },
31206
+ stdio: ["ignore", "pipe", "pipe"]
31175
31207
  });
31208
+ try {
31209
+ await waitForMetroReady(metro);
31210
+ } catch (error) {
31211
+ if (!(error instanceof MetroStartupError) || !error.conflict || attemptedMetroPorts.length >= METRO_START_MAX_ATTEMPTS) {
31212
+ throw error;
31213
+ }
31214
+ await stopProcess(metro);
31215
+ const allocate = options.allocateMetroPort ?? allocateAvailableMetroPort;
31216
+ const replacementPort = await allocate(attemptedMetroPorts);
31217
+ if (!Number.isSafeInteger(replacementPort) || replacementPort < 1 || replacementPort > MAX_TCP_PORT || attemptedMetroPorts.includes(replacementPort)) {
31218
+ throw new Error("Could not allocate a distinct replacement Metro port.", { cause: error });
31219
+ }
31220
+ attemptedMetroPorts.push(replacementPort);
31221
+ metroRetryCount += 1;
31222
+ log("Metro port was claimed during startup; retrying automatically on a new port.");
31223
+ plan = planAbsoluteExpoDevSession(options.config, {
31224
+ ...options,
31225
+ metroPort: replacementPort
31226
+ });
31227
+ metroCommand = plan.commands.find((command) => command.role === "metro");
31228
+ nativeCommands = plan.commands.filter((command) => command.role === "native-build");
31229
+ await startManagedMetro();
31230
+ }
31231
+ };
31176
31232
  const abort = () => {
31177
31233
  if (metro)
31178
31234
  stopProcess(metro);
@@ -31187,7 +31243,7 @@ var startAbsoluteExpoDevSession = async (options) => {
31187
31243
  const state = platform2 === "android" ? "building-android" : "building-ios";
31188
31244
  setState(state);
31189
31245
  const started = performance.now();
31190
- const invocation = platform2 === "android" && host3 === "wsl" ? encodedWindowsExpoAndroidCommand(plan.project, options.androidRoot ?? absoluteManagedAndroidSdkRoot(host3), options.config.appId, command.args, capture, options.metroPort, options.androidDevice) : [executable, ...command.args];
31246
+ const invocation = platform2 === "android" && host3 === "wsl" ? encodedWindowsExpoAndroidCommand(plan.project, options.androidRoot ?? absoluteManagedAndroidSdkRoot(host3), options.config.appId, command.args, capture, plan.metroPort, options.androidDevice) : [executable, ...command.args];
31191
31247
  if (platform2 === "android" && host3 === "wsl") {
31192
31248
  log("[android] Mirroring native inputs to the Windows host for accelerated Expo build and launch.");
31193
31249
  }
@@ -31215,7 +31271,7 @@ var startAbsoluteExpoDevSession = async (options) => {
31215
31271
  if (platform2 === "android" && host3 !== "wsl") {
31216
31272
  const androidRoot = options.androidRoot ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? absoluteManagedAndroidSdkRoot(host3);
31217
31273
  const adb = options.androidAdb ?? join18(androidRoot, "platform-tools", host3 === "windows" ? "adb.exe" : "adb");
31218
- connectLocalExpoAndroid(adb, options.config.appId, options.metroPort, capture, options.androidDevice);
31274
+ connectLocalExpoAndroid(adb, options.config.appId, plan.metroPort, capture, options.androidDevice);
31219
31275
  log("Connected the Expo Android development client to Metro.");
31220
31276
  }
31221
31277
  if (platform2 === "ios" && options.certificateAuthorityPath && options.iosOrigin && new URL(options.iosOrigin).protocol === "https:" && !options.iosDevice) {
@@ -31253,7 +31309,7 @@ var startAbsoluteExpoDevSession = async (options) => {
31253
31309
  "simctl",
31254
31310
  "openurl",
31255
31311
  "booted",
31256
- expoDevelopmentClientUrl(options.config.appId, options.metroHost ?? "localhost", options.metroPort)
31312
+ expoDevelopmentClientUrl(options.config.appId, options.metroHost ?? "localhost", plan.metroPort)
31257
31313
  ], utilityOptions);
31258
31314
  if (connectExit !== 0)
31259
31315
  throw new Error(`Expo iOS Simulator Metro connection exited with status ${connectExit}.`);
@@ -31286,9 +31342,14 @@ var startAbsoluteExpoDevSession = async (options) => {
31286
31342
  };
31287
31343
  try {
31288
31344
  await startPhysicalIosEnrollment();
31289
- await metroPromise;
31345
+ await startManagedMetro();
31290
31346
  if (managedMetro)
31291
- publishTiming("starting-metro", performance.now() - metroStarted);
31347
+ publishTiming("starting-metro", performance.now() - metroStarted, {
31348
+ ...metroRetryCount > 0 ? {
31349
+ reason: "metro-port-conflict",
31350
+ retryCount: metroRetryCount
31351
+ } : {}
31352
+ });
31292
31353
  await runNativeCommands(nativeCommands);
31293
31354
  setState("ready");
31294
31355
  let closed = false;
@@ -41095,5 +41156,5 @@ export {
41095
41156
  writeAbsoluteMobileUpdateRegistry
41096
41157
  };
41097
41158
 
41098
- //# debugId=35DF25B2FF31C85164756E2164756E21
41159
+ //# debugId=9BE9503423C665BD64756E2164756E21
41099
41160
  //# sourceMappingURL=index.js.map