rnxsim 0.1.392 → 0.1.394

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.
Files changed (61) hide show
  1. package/cli/cloud-client.ts +74 -52
  2. package/cli/cloud-dispatch.ts +2 -1
  3. package/cli/commands/inspect/core.ts +33 -2
  4. package/cli/commands/platform.ts +34 -9
  5. package/detox/attempt-events.cjs +23 -0
  6. package/detox/element-types.ts +11 -2
  7. package/detox/expectations.ts +7 -3
  8. package/detox/gestures.ts +23 -2
  9. package/detox/index.ts +42 -62
  10. package/detox/navigation-lifecycle.ts +290 -0
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +12 -1
  22. package/dist-lib/bridge-contract-input.mjs +10 -1
  23. package/dist-lib/bridge-contract.cjs +7 -1
  24. package/dist-lib/bridge-contract.mjs +5 -1
  25. package/dist-lib/capture-contract.cjs +1 -1
  26. package/dist-lib/capture-contract.mjs +1 -1
  27. package/dist-lib/cli-constants.cjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +29 -3
  29. package/dist-lib/cloud-contract.mjs +26 -2
  30. package/dist-lib/config.cjs +1 -1
  31. package/dist-lib/detox/index.cjs +240 -48
  32. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  33. package/dist-lib/home-paths.cjs +1 -1
  34. package/dist-lib/host/bridge-host.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  38. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  39. package/dist-lib/host/websocket-proxy.cjs +1 -1
  40. package/dist-lib/index.cjs +23 -3
  41. package/dist-lib/jump-to-source-babel.cjs +1 -1
  42. package/dist-lib/menu.cjs +1 -1
  43. package/dist-lib/menu.mjs +1 -1
  44. package/dist-lib/metro-production-bundle.cjs +132 -3
  45. package/dist-lib/metro-production-bundle.mjs +129 -2
  46. package/dist-lib/metro.cjs +1 -1
  47. package/dist-lib/profiles.cjs +1 -1
  48. package/dist-lib/public-brand.cjs +1 -1
  49. package/dist-lib/react-native-host-modules.cjs +1 -1
  50. package/dist-lib/react-native-host-modules.mjs +1 -1
  51. package/dist-lib/render-mode.cjs +1 -1
  52. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  53. package/dist-lib/sdk.cjs +24 -3
  54. package/dist-lib/sdk.mjs +24 -3
  55. package/dist-lib/skills.cjs +51 -80
  56. package/dist-lib/vite.cjs +1 -1
  57. package/package.json +1 -1
  58. package/src/bridge-contract-input.ts +12 -0
  59. package/src/bridge-contract.ts +7 -0
  60. package/src/cloud-contract.ts +29 -0
  61. package/src/metro-production-bundle.ts +200 -0
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.392 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.394 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1347,35 +1347,23 @@ var init_supported_native_packages = __esm({
1347
1347
  "expo-apple-authentication",
1348
1348
  // preset
1349
1349
  "expo-application",
1350
- // visual-proof+coverage
1350
+ // coverage
1351
1351
  "expo-asset",
1352
1352
  // preset+coverage
1353
1353
  "expo-audio",
1354
1354
  // coverage
1355
1355
  "expo-av",
1356
1356
  // coverage
1357
- "expo-background-fetch",
1358
- // visual-proof
1359
- "expo-background-task",
1360
- // visual-proof
1361
1357
  "expo-battery",
1362
- // visual-proof+coverage
1358
+ // coverage
1363
1359
  "expo-blur",
1364
1360
  // preset+coverage
1365
- "expo-brightness",
1366
- // visual-proof
1367
- "expo-calendar",
1368
- // visual-proof
1369
1361
  "expo-camera",
1370
1362
  // coverage
1371
- "expo-cellular",
1372
- // visual-proof
1373
1363
  "expo-clipboard",
1374
1364
  // preset+coverage
1375
1365
  "expo-constants",
1376
- // preset+visual-proof+coverage
1377
- "expo-contacts",
1378
- // visual-proof
1366
+ // preset+coverage
1379
1367
  "expo-crypto",
1380
1368
  // preset+coverage
1381
1369
  "expo-dev-client",
@@ -1397,27 +1385,23 @@ var init_supported_native_packages = __esm({
1397
1385
  "expo-ignore-battery-optimizations",
1398
1386
  // coverage
1399
1387
  "expo-image",
1400
- // preset+visual-proof+coverage
1388
+ // preset+coverage
1401
1389
  "expo-image-manipulator",
1402
1390
  // coverage
1403
1391
  "expo-image-picker",
1404
1392
  // preset+coverage
1405
1393
  "expo-insights",
1406
1394
  // coverage
1407
- "expo-intent-launcher",
1408
- // visual-proof
1409
1395
  "expo-keep-awake",
1410
1396
  // coverage
1411
1397
  "expo-linear-gradient",
1412
- // preset+visual-proof+coverage
1398
+ // preset+coverage
1413
1399
  "expo-linking",
1414
1400
  // preset+coverage
1415
1401
  "expo-local-authentication",
1416
1402
  // coverage
1417
1403
  "expo-localization",
1418
1404
  // coverage
1419
- "expo-mail-composer",
1420
- // visual-proof
1421
1405
  "expo-media-library",
1422
1406
  // coverage
1423
1407
  "expo-mesh-gradient",
@@ -1425,51 +1409,35 @@ var init_supported_native_packages = __esm({
1425
1409
  "expo-modules-core",
1426
1410
  // preset
1427
1411
  "expo-network",
1428
- // preset+visual-proof+coverage
1412
+ // preset+coverage
1429
1413
  "expo-notifications",
1430
1414
  // preset+coverage
1431
- "expo-print",
1432
- // visual-proof
1433
- "expo-screen-capture",
1434
- // visual-proof
1435
1415
  "expo-screen-corner-radius",
1436
1416
  // coverage
1437
1417
  "expo-screen-orientation",
1438
- // visual-proof+coverage
1418
+ // coverage
1439
1419
  "expo-secure-store",
1440
- // visual-proof+coverage
1441
- "expo-sensors",
1442
- // visual-proof
1443
- "expo-sharing",
1444
- // visual-proof
1445
- "expo-sms",
1446
- // visual-proof
1447
- "expo-speech",
1448
- // visual-proof
1420
+ // coverage
1449
1421
  "expo-speech-recognition",
1450
1422
  // preset
1451
1423
  "expo-splash-screen",
1452
- // visual-proof+coverage
1424
+ // coverage
1453
1425
  "expo-sqlite",
1454
1426
  // preset+coverage
1455
1427
  "expo-store-review",
1456
- // visual-proof+coverage
1428
+ // coverage
1457
1429
  "expo-symbols",
1458
1430
  // coverage
1459
1431
  "expo-system-ui",
1460
1432
  // coverage
1461
1433
  "expo-task-manager",
1462
- // visual-proof+coverage
1434
+ // coverage
1463
1435
  "expo-testflight",
1464
1436
  // coverage
1465
1437
  "expo-tracking-transparency",
1466
- // visual-proof+coverage
1438
+ // coverage
1467
1439
  "expo-updates",
1468
1440
  // preset+coverage
1469
- "expo-video",
1470
- // visual-proof
1471
- "expo-video-thumbnails",
1472
- // visual-proof
1473
1441
  "expo-web-browser",
1474
1442
  // preset
1475
1443
  "expo-winter-fetch",
@@ -1493,7 +1461,7 @@ var init_supported_native_packages = __esm({
1493
1461
  "posthog-react-native-session-replay",
1494
1462
  // preset+coverage
1495
1463
  "react-native",
1496
- // preset+visual-proof+coverage
1464
+ // preset+coverage
1497
1465
  "react-native-actions-shortcuts",
1498
1466
  // coverage
1499
1467
  "react-native-adjust",
@@ -1517,7 +1485,7 @@ var init_supported_native_packages = __esm({
1517
1485
  "react-native-bootsplash",
1518
1486
  // preset+coverage
1519
1487
  "react-native-bottom-tabs",
1520
- // preset+visual-proof
1488
+ // preset
1521
1489
  "react-native-branch",
1522
1490
  // coverage
1523
1491
  "react-native-config",
@@ -1539,7 +1507,7 @@ var init_supported_native_packages = __esm({
1539
1507
  "react-native-fs",
1540
1508
  // coverage
1541
1509
  "react-native-gesture-handler",
1542
- // preset+visual-proof+coverage
1510
+ // preset+coverage
1543
1511
  "react-native-get-device-locale",
1544
1512
  // coverage
1545
1513
  "react-native-get-location",
@@ -1567,7 +1535,7 @@ var init_supported_native_packages = __esm({
1567
1535
  "react-native-key-command",
1568
1536
  // coverage
1569
1537
  "react-native-keyboard-controller",
1570
- // preset+visual-proof
1538
+ // preset
1571
1539
  "react-native-keychain",
1572
1540
  // coverage
1573
1541
  "react-native-keys",
@@ -1597,7 +1565,7 @@ var init_supported_native_packages = __esm({
1597
1565
  "react-native-notification-badge",
1598
1566
  // coverage
1599
1567
  "react-native-pager-view",
1600
- // preset+visual-proof
1568
+ // preset
1601
1569
  "react-native-pdf",
1602
1570
  // coverage
1603
1571
  "react-native-permissions",
@@ -1617,7 +1585,7 @@ var init_supported_native_packages = __esm({
1617
1585
  "react-native-randombytes",
1618
1586
  // coverage
1619
1587
  "react-native-reanimated",
1620
- // preset+visual-proof+coverage
1588
+ // preset+coverage
1621
1589
  "react-native-restart",
1622
1590
  // coverage
1623
1591
  "react-native-restart-newarch",
@@ -1625,11 +1593,11 @@ var init_supported_native_packages = __esm({
1625
1593
  "react-native-rsa-signer",
1626
1594
  // coverage
1627
1595
  "react-native-safe-area-context",
1628
- // preset+visual-proof+coverage
1596
+ // preset+coverage
1629
1597
  "react-native-screen-corner-radius",
1630
1598
  // coverage
1631
1599
  "react-native-screens",
1632
- // preset+visual-proof+coverage
1600
+ // preset+coverage
1633
1601
  "react-native-securerandom",
1634
1602
  // coverage
1635
1603
  "react-native-settings",
@@ -1645,7 +1613,7 @@ var init_supported_native_packages = __esm({
1645
1613
  "react-native-splash-screen",
1646
1614
  // coverage
1647
1615
  "react-native-svg",
1648
- // preset+visual-proof
1616
+ // preset
1649
1617
  "react-native-system-bars",
1650
1618
  // coverage
1651
1619
  "react-native-teleport",
@@ -1659,7 +1627,7 @@ var init_supported_native_packages = __esm({
1659
1627
  "react-native-turbo-haptics",
1660
1628
  // coverage
1661
1629
  "react-native-uitextview",
1662
- // visual-proof+coverage
1630
+ // coverage
1663
1631
  "react-native-ulid-jsi",
1664
1632
  // coverage
1665
1633
  "react-native-ux-cam",
@@ -1679,7 +1647,7 @@ var init_supported_native_packages = __esm({
1679
1647
  "react-native-webp-format",
1680
1648
  // coverage
1681
1649
  "react-native-webview",
1682
- // preset+visual-proof
1650
+ // preset
1683
1651
  "react-native-worklets",
1684
1652
  // preset+coverage
1685
1653
  "react-native-xxhash",
@@ -21428,12 +21396,34 @@ var init_cli_constants = __esm({
21428
21396
  });
21429
21397
 
21430
21398
  // src/cloud-contract.ts
21431
- var RNX_CLOUD_MAX_ARTIFACT_BYTES, RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES;
21399
+ function isRnxCloudCommandType(value) {
21400
+ return RNX_CLOUD_COMMAND_TYPE_SET.has(value);
21401
+ }
21402
+ var RNX_CLOUD_MAX_ARTIFACT_BYTES, RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES, RNX_CLOUD_COMMAND_TYPES, RNX_CLOUD_COMMAND_TYPE_SET;
21432
21403
  var init_cloud_contract = __esm({
21433
21404
  "src/cloud-contract.ts"() {
21434
21405
  "use strict";
21435
21406
  RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024;
21436
21407
  RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES = 1024 * 1024;
21408
+ RNX_CLOUD_COMMAND_TYPES = Object.freeze([
21409
+ "state",
21410
+ "query",
21411
+ "resolve",
21412
+ "perform",
21413
+ "waitFor",
21414
+ "reset",
21415
+ "screenshot",
21416
+ "capture",
21417
+ "captureRegions",
21418
+ "settle",
21419
+ "setAppearance",
21420
+ "openUrl",
21421
+ "tree",
21422
+ "tap",
21423
+ "longPress",
21424
+ "memory"
21425
+ ]);
21426
+ RNX_CLOUD_COMMAND_TYPE_SET = new Set(RNX_CLOUD_COMMAND_TYPES);
21437
21427
  }
21438
21428
  });
21439
21429
 
@@ -21552,27 +21542,7 @@ function cloudCommand(command) {
21552
21542
  for (const [key, value] of Object.entries(command)) {
21553
21543
  if (key !== "id" && key !== "simId") clean[key] = value;
21554
21544
  }
21555
- if (command.type === "query") {
21556
- const query = command.query;
21557
- if (!isRecord4(query) || query.kind !== "tree" && query.kind !== "find" && query.kind !== "count") {
21558
- throw new Error("RNX Cloud supports only structured tree, node, and count queries");
21559
- }
21560
- return clean;
21561
- }
21562
- if (command.type === "waitFor") {
21563
- const options = command.waitForOptions;
21564
- const condition = isRecord4(options) ? options.condition : null;
21565
- if (!isRecord4(condition) || condition.type !== "ready" && condition.type !== "present" && condition.type !== "absent") {
21566
- throw new Error("RNX Cloud supports only ready and selector waits");
21567
- }
21568
- return clean;
21569
- }
21570
- if (command.type === "state") return clean;
21571
- if (command.type === "capture" || command.type === "captureRegions") return clean;
21572
- if (command.type === "setAppearance" || command.type === "openUrl") return clean;
21573
- if (command.type === "screenshot" && command.crop === void 0 && command.captureFilter === void 0 && (command.layers === void 0 || command.layers === "tenant")) {
21574
- return clean;
21575
- }
21545
+ if (isRnxCloudCommandType(command.type)) return clean;
21576
21546
  throw new Error(`RNX Cloud does not support bridge command type ${command.type}`);
21577
21547
  }
21578
21548
  function createCloudBridgeForParsed(parsed) {
@@ -23636,8 +23606,9 @@ async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
23636
23606
  };
23637
23607
  }
23638
23608
  const probe3 = result.probe;
23609
+ const externalError = probe3?.externalError ?? "";
23639
23610
  const status = {
23640
- ready: result.matched,
23611
+ ready: result.matched && !externalError,
23641
23612
  elapsedMs: result.elapsedMs,
23642
23613
  nodes: probe3?.nodes ?? 0,
23643
23614
  targets: probe3?.targets ?? 0,
@@ -23648,7 +23619,7 @@ async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
23648
23619
  loadingText: probe3?.loadingText ?? "",
23649
23620
  externalReady: probe3?.externalReady ?? null,
23650
23621
  externalStatus: probe3?.externalStatus ?? "",
23651
- externalError: probe3?.externalError ?? "",
23622
+ externalError,
23652
23623
  suppressedEntryError: probe3?.suppressedEntryError ?? "",
23653
23624
  errors: probe3?.errors ?? 0,
23654
23625
  bridgeError: result.error ?? ""
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.392 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.394 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.392",
3
+ "version": "0.1.394",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for rnx.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -3,6 +3,7 @@
3
3
  // WebSocket routes) validates its request bodies here rather than growing a
4
4
  // second reader for the same shapes.
5
5
 
6
+ import { SIM_LONG_PRESS_MAX_MS } from './bridge-contract'
6
7
  import type {
7
8
  ResetOptions,
8
9
  SemanticWaitOptions,
@@ -130,6 +131,17 @@ export function parsePerformStep(value: unknown, index: number): SimPerformStep
130
131
  }
131
132
  case 'longPress': {
132
133
  const durationMs = optionalNumber(step, 'durationMs', path)
134
+ if (
135
+ durationMs !== undefined &&
136
+ (!Number.isInteger(durationMs) ||
137
+ durationMs < 1 ||
138
+ durationMs > SIM_LONG_PRESS_MAX_MS)
139
+ ) {
140
+ validationFailure(
141
+ `${path}.durationMs`,
142
+ `must be an integer between 1 and ${SIM_LONG_PRESS_MAX_MS}`,
143
+ )
144
+ }
133
145
  return {
134
146
  type,
135
147
  x: numberValue(step.x, `${path}.x`),
@@ -22,6 +22,7 @@ export type BridgeCommandType =
22
22
  | 'settle'
23
23
  | 'setAppearance'
24
24
  | 'openUrl'
25
+ | 'memory'
25
26
  | 'tap'
26
27
  | 'longPress'
27
28
  | 'keyboard'
@@ -216,6 +217,12 @@ export interface SemanticReadyProbe {
216
217
  suppressedEntryError: string
217
218
  }
218
219
 
220
+ // react native's own Pressable/Touchable delayLongPress default, and the
221
+ // ceiling any transport accepts for a press-and-hold. a driver that holds
222
+ // longer than this is describing a drag, not a long press.
223
+ export const SIM_LONG_PRESS_DEFAULT_MS = 500
224
+ export const SIM_LONG_PRESS_MAX_MS = 5_000
225
+
219
226
  export const SEMANTIC_READY_CONTENT_NODE_FLOOR = 100
220
227
  export const SEMANTIC_READY_NODE_STABLE_MS = 750
221
228
  export const SEMANTIC_READY_POLL_MS = 150
@@ -1,8 +1,37 @@
1
+ import type { BridgeCommandType } from './bridge-contract'
2
+
1
3
  export const RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024
2
4
  export const RNX_CLOUD_DESIGN_PARALLEL_JOB_LIMIT = 16
3
5
  export const RNX_CLOUD_DESIGN_MAX_STORAGE_ENTRIES = 256
4
6
  export const RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES = 1024 * 1024
5
7
 
8
+ export const RNX_CLOUD_COMMAND_TYPES = Object.freeze([
9
+ 'state',
10
+ 'query',
11
+ 'resolve',
12
+ 'perform',
13
+ 'waitFor',
14
+ 'reset',
15
+ 'screenshot',
16
+ 'capture',
17
+ 'captureRegions',
18
+ 'settle',
19
+ 'setAppearance',
20
+ 'openUrl',
21
+ 'tree',
22
+ 'tap',
23
+ 'longPress',
24
+ 'memory',
25
+ ]) satisfies readonly BridgeCommandType[]
26
+
27
+ const RNX_CLOUD_COMMAND_TYPE_SET = new Set<string>(RNX_CLOUD_COMMAND_TYPES)
28
+
29
+ export function isRnxCloudCommandType(
30
+ value: string,
31
+ ): value is (typeof RNX_CLOUD_COMMAND_TYPES)[number] {
32
+ return RNX_CLOUD_COMMAND_TYPE_SET.has(value)
33
+ }
34
+
6
35
  export interface RnxCloudClaim {
7
36
  id: string
8
37
  expiresAt: number
@@ -49,3 +49,203 @@ export function readRNXMetroModuleIdentity(
49
49
  }
50
50
  return { source: bundleText.slice(0, footerStart), modulePaths }
51
51
  }
52
+
53
+ // the exact layout Metro's baseJSBundle emits and the rnx runtime contract
54
+ // depends on: a prelude, then one `__d(` or `__r(` statement per line, each
55
+ // running until the next statement starts its own line. one reader owns this
56
+ // recognition so the loader's identity validation, the source-map attribution
57
+ // pass, and the cloud artifact producer cannot drift apart.
58
+ export interface RNXMetroDefine {
59
+ kind: 'define'
60
+ moduleId: number
61
+ start: number
62
+ end: number
63
+ lineIndex: number
64
+ endLineIndex: number
65
+ /** the factory expression, between `__d(` and the module ID. */
66
+ factory: { start: number; end: number }
67
+ /** the dependency array, brackets included. */
68
+ dependencies: { start: number; end: number }
69
+ }
70
+
71
+ export interface RNXMetroRun {
72
+ kind: 'run'
73
+ start: number
74
+ end: number
75
+ lineIndex: number
76
+ endLineIndex: number
77
+ }
78
+
79
+ export type RNXMetroStatement = RNXMetroDefine | RNXMetroRun
80
+
81
+ export interface RNXMetroBundleLayout {
82
+ /** everything before the first `__d(`/`__r(` line: prelude vars and polyfills. */
83
+ prelude: string
84
+ statements: RNXMetroStatement[]
85
+ /** `__d(` statements whose module ID could not be read. */
86
+ moduleIdFailures: number
87
+ }
88
+
89
+ interface DefineTail {
90
+ moduleId: number
91
+ factoryEnd: number
92
+ dependenciesStart: number
93
+ dependenciesEnd: number
94
+ }
95
+
96
+ // each `__d(...)` ends with `),<id>,[dep,dep,...]);?`. the dependency array can
97
+ // be hundreds of entries long, so walk back from `end` instead of searching a
98
+ // fixed tail window: skip trailing `;` and whitespace, consume the closing `)`,
99
+ // balance `[...]` to find its opening bracket, then read the integer before it.
100
+ function readDefineTail(source: string, start: number, end: number): DefineTail | null {
101
+ let cursor = end
102
+ while (
103
+ cursor > start &&
104
+ (source[cursor] === ' ' ||
105
+ source[cursor] === '\n' ||
106
+ source[cursor] === '\t' ||
107
+ source[cursor] === ';')
108
+ ) {
109
+ cursor--
110
+ }
111
+ if (source[cursor] !== ')') return null
112
+ cursor--
113
+ while (cursor > start && /\s/.test(source[cursor])) cursor--
114
+ if (source[cursor] !== ']') return null
115
+ const dependenciesEnd = cursor + 1
116
+ let depth = 1
117
+ cursor--
118
+ while (cursor > start && depth > 0) {
119
+ const character = source[cursor]
120
+ if (character === ']') depth++
121
+ else if (character === '[') depth--
122
+ if (depth === 0) break
123
+ cursor--
124
+ }
125
+ if (depth !== 0) return null
126
+ const dependenciesStart = cursor
127
+ let idEnd = cursor - 1
128
+ while (idEnd > start && /\s/.test(source[idEnd])) idEnd--
129
+ if (source[idEnd] !== ',') return null
130
+ // a minifier is free to write the module ID as `1e3` or `0x3e8`, so read the
131
+ // whole numeric literal rather than a run of decimal digits. the factory
132
+ // before it always ends in `}` or `)`, which stops this walk immediately.
133
+ let idStart = idEnd
134
+ while (idStart > start && /[0-9a-fA-FxX.+\-]/.test(source[idStart - 1])) idStart--
135
+ const literal = source.slice(idStart, idEnd)
136
+ if (!/^-?(?:0[xXbBoO][0-9a-fA-F]+|\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)$/.test(literal)) {
137
+ return null
138
+ }
139
+ const moduleId = Number(literal)
140
+ if (!Number.isSafeInteger(moduleId)) return null
141
+ let factoryEnd = idStart - 1
142
+ while (factoryEnd > start && /\s/.test(source[factoryEnd])) factoryEnd--
143
+ if (source[factoryEnd] !== ',') return null
144
+ return {
145
+ moduleId,
146
+ factoryEnd,
147
+ dependenciesStart,
148
+ dependenciesEnd,
149
+ }
150
+ }
151
+
152
+ /** reads the canonical Metro production layout from footer-stripped source. */
153
+ export function readRNXMetroBundleLayout(source: string): RNXMetroBundleLayout {
154
+ const lineOffsets = [0]
155
+ for (let index = 0; index < source.length; index++) {
156
+ if (source.charCodeAt(index) === 10) lineOffsets.push(index + 1)
157
+ }
158
+
159
+ const boundaries: number[] = []
160
+ for (let lineIndex = 0; lineIndex < lineOffsets.length; lineIndex++) {
161
+ const start = lineOffsets[lineIndex]
162
+ if (source.startsWith('__d(', start) || source.startsWith('__r(', start)) {
163
+ boundaries.push(lineIndex)
164
+ }
165
+ }
166
+
167
+ const statements: RNXMetroStatement[] = []
168
+ let moduleIdFailures = 0
169
+ for (let index = 0; index < boundaries.length; index++) {
170
+ const lineIndex = boundaries[index]
171
+ const start = lineOffsets[lineIndex]
172
+ const nextLineIndex = boundaries[index + 1]
173
+ const endLineIndex =
174
+ nextLineIndex !== undefined ? nextLineIndex - 1 : lineOffsets.length - 1
175
+ const end = nextLineIndex !== undefined ? lineOffsets[nextLineIndex] : source.length
176
+ if (!source.startsWith('__d(', start)) {
177
+ statements.push({ kind: 'run', start, end, lineIndex, endLineIndex })
178
+ continue
179
+ }
180
+ const tail = readDefineTail(source, start, end - 1)
181
+ if (!tail) {
182
+ moduleIdFailures++
183
+ continue
184
+ }
185
+ statements.push({
186
+ kind: 'define',
187
+ moduleId: tail.moduleId,
188
+ start,
189
+ end,
190
+ lineIndex,
191
+ endLineIndex,
192
+ factory: { start: start + '__d('.length, end: tail.factoryEnd },
193
+ dependencies: { start: tail.dependenciesStart, end: tail.dependenciesEnd },
194
+ })
195
+ }
196
+
197
+ return {
198
+ prelude: source.slice(
199
+ 0,
200
+ boundaries.length > 0 ? lineOffsets[boundaries[0]] : source.length,
201
+ ),
202
+ statements,
203
+ moduleIdFailures,
204
+ }
205
+ }
206
+
207
+ /**
208
+ * validates the exact identity contract before a loader installs a Metro
209
+ * factory trap or a producer ships an artifact: every define carries a unique,
210
+ * readable module ID, and the identity map names those IDs and no others.
211
+ * serializers that emit another layout need an ingestion adapter that
212
+ * normalizes them to this format.
213
+ */
214
+ export function validateRNXMetroModulePaths(
215
+ source: string,
216
+ modulePaths: RNXMetroModulePathMap,
217
+ ): void {
218
+ const layout = readRNXMetroBundleLayout(source)
219
+ if (layout.moduleIdFailures > 0) {
220
+ throw new Error(
221
+ `Metro module identity could not read ${layout.moduleIdFailures} module IDs`,
222
+ )
223
+ }
224
+ const definedIds = new Set<number>()
225
+ for (const statement of layout.statements) {
226
+ if (statement.kind !== 'define') continue
227
+ if (definedIds.has(statement.moduleId)) {
228
+ throw new Error(`Metro module identity has duplicate module ${statement.moduleId}`)
229
+ }
230
+ definedIds.add(statement.moduleId)
231
+ if (!Object.prototype.hasOwnProperty.call(modulePaths, String(statement.moduleId))) {
232
+ throw new Error(`Metro module identity is missing ${statement.moduleId}`)
233
+ }
234
+ }
235
+ if (definedIds.size === 0) {
236
+ throw new Error('Metro module identity found no canonical JavaScript modules')
237
+ }
238
+
239
+ for (const [key, modulePath] of Object.entries(modulePaths)) {
240
+ const moduleId = Number(key)
241
+ if (!Number.isSafeInteger(moduleId) || String(moduleId) !== key) {
242
+ throw new Error(`Metro module identity has invalid module ID ${key}`)
243
+ }
244
+ if (typeof modulePath !== 'string' || modulePath.length === 0) {
245
+ throw new Error(`Metro module identity has no path for ${key}`)
246
+ }
247
+ if (!definedIds.has(moduleId)) {
248
+ throw new Error(`Metro module identity has no module ${key}`)
249
+ }
250
+ }
251
+ }