rnxsim 0.1.402 → 0.1.404

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 (59) hide show
  1. package/README.md +31 -9
  2. package/cli/commands/box.ts +10 -3
  3. package/cli/outbound-endpoints.ts +2 -2
  4. package/dist-lib/agent-daemon-client.cjs +1 -1
  5. package/dist-lib/agent-events.cjs +1 -1
  6. package/dist-lib/agent-identity.cjs +1 -1
  7. package/dist-lib/agent-sessions.cjs +1 -1
  8. package/dist-lib/attached-projects.cjs +1 -1
  9. package/dist-lib/auth/shared-session.cjs +1 -1
  10. package/dist-lib/backend-origin.cjs +1 -1
  11. package/dist-lib/beta.cjs +1 -1
  12. package/dist-lib/beta.mjs +1 -1
  13. package/dist-lib/bridge-constants.cjs +1 -1
  14. package/dist-lib/bridge-contract-input.cjs +1 -1
  15. package/dist-lib/bridge-contract-input.mjs +1 -1
  16. package/dist-lib/bridge-contract.cjs +1 -1
  17. package/dist-lib/bridge-contract.mjs +1 -1
  18. package/dist-lib/capture-contract.cjs +5 -2
  19. package/dist-lib/capture-contract.mjs +5 -2
  20. package/dist-lib/cli-constants.cjs +1 -1
  21. package/dist-lib/cloud-contract.cjs +3 -2
  22. package/dist-lib/cloud-contract.mjs +3 -2
  23. package/dist-lib/config.cjs +1 -1
  24. package/dist-lib/detox/index.cjs +1 -1
  25. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  26. package/dist-lib/home-paths.cjs +1 -1
  27. package/dist-lib/host/bridge-host.cjs +1 -1
  28. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  29. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  30. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  31. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  32. package/dist-lib/host/websocket-proxy.cjs +1 -1
  33. package/dist-lib/index.cjs +63 -14
  34. package/dist-lib/jump-to-source-babel.cjs +1 -1
  35. package/dist-lib/menu.cjs +1 -1
  36. package/dist-lib/menu.mjs +1 -1
  37. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  38. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  39. package/dist-lib/metro-production-bundle.cjs +131 -17
  40. package/dist-lib/metro-production-bundle.mjs +125 -16
  41. package/dist-lib/metro.cjs +67 -12
  42. package/dist-lib/profiles.cjs +1 -1
  43. package/dist-lib/public-brand.cjs +1 -1
  44. package/dist-lib/react-native-host-modules.cjs +1 -1
  45. package/dist-lib/react-native-host-modules.mjs +1 -1
  46. package/dist-lib/render-mode.cjs +1 -1
  47. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  48. package/dist-lib/sdk.cjs +1 -1
  49. package/dist-lib/sdk.mjs +1 -1
  50. package/dist-lib/skills.cjs +58 -25
  51. package/dist-lib/vite.cjs +6 -1
  52. package/package.json +8 -1
  53. package/src/bridge-contract.ts +20 -0
  54. package/src/capture-contract.ts +8 -0
  55. package/src/cloud-contract.ts +1 -0
  56. package/src/metro-fingerprint-registry.ts +302 -0
  57. package/src/metro-plugin.ts +73 -24
  58. package/src/metro-production-bundle.ts +213 -19
  59. package/src/runtime-assets.ts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.402",
3
+ "version": "0.1.404",
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",
@@ -45,6 +45,13 @@
45
45
  "import": "./dist-lib/metro-production-bundle.mjs",
46
46
  "default": "./dist-lib/metro-production-bundle.cjs"
47
47
  },
48
+ "./metro-fingerprint-registry": {
49
+ "types": "./src/metro-fingerprint-registry.ts",
50
+ "source": "./src/metro-fingerprint-registry.ts",
51
+ "bun": "./src/metro-fingerprint-registry.ts",
52
+ "import": "./dist-lib/metro-fingerprint-registry.mjs",
53
+ "default": "./dist-lib/metro-fingerprint-registry.cjs"
54
+ },
48
55
  "./react-native-host-modules": {
49
56
  "types": "./src/react-native-host-modules.ts",
50
57
  "source": "./src/react-native-host-modules.ts",
@@ -37,6 +37,7 @@ export type BridgeCommandType =
37
37
  | 'resolve'
38
38
  | 'reset'
39
39
  | 'camera'
40
+ | 'diagnostics'
40
41
 
41
42
  export interface WsCommand {
42
43
  id: string | number
@@ -86,6 +87,9 @@ export interface WsCommand {
86
87
  resetOptions?: ResetOptions
87
88
  // camera — fixture injection into the host camera pipeline
88
89
  camera?: CameraCommand
90
+ // diagnostics — read-only engine debug state: channel recording, the sheet /
91
+ // portal / boundary finders, and the recent event ring.
92
+ diagnostics?: DiagnosticsOptions
89
93
  }
90
94
 
91
95
  // ---------------------------------------------------------------------------
@@ -258,6 +262,22 @@ export interface SemanticWaitResult {
258
262
  // state — one round-trip situational awareness
259
263
  // ---------------------------------------------------------------------------
260
264
 
265
+ export interface DiagnosticsOptions {
266
+ // debug channels to record from this point on. omitted leaves recording as
267
+ // it is; an empty list stops recording.
268
+ channels?: readonly string[]
269
+ find?: 'sheets' | 'portals' | 'boundaries'
270
+ recentChannel?: string
271
+ recentLimit?: number
272
+ }
273
+
274
+ export interface DiagnosticsResult {
275
+ channels: string[]
276
+ runtime: unknown
277
+ found: unknown[] | null
278
+ events: unknown[]
279
+ }
280
+
261
281
  export interface SimStateOptions {
262
282
  // omit a section an agent does not need this turn
263
283
  screenshot?: boolean
@@ -162,6 +162,7 @@ export interface RnxRasterRegionTree {
162
162
  nodeId: number
163
163
  parentNodeId: number | null
164
164
  type: string
165
+ text?: string | null
165
166
  style: Record<string, unknown>
166
167
  layout: RnxCaptureRect
167
168
  frame: RnxCaptureRect | null
@@ -178,6 +179,12 @@ export function selectRnxRasterNodeIds(tree: RnxRasterRegionTree): Set<number> {
178
179
  if (!visibleFrame || visibleFrame.width <= 0 || visibleFrame.height <= 0) continue
179
180
  if (
180
181
  node.type === 'masked-view' ||
182
+ (node.type === 'text' &&
183
+ typeof node.text === 'string' &&
184
+ (/\p{Emoji_Presentation}/u.test(node.text) ||
185
+ /\uFE0F|\u200D|\u20E3|\p{Emoji_Modifier}|\p{Regional_Indicator}/u.test(
186
+ node.text,
187
+ ))) ||
181
188
  node.style._liquidGlass !== undefined ||
182
189
  node.style._liquidGlassEdge !== undefined
183
190
  ) {
@@ -293,6 +300,7 @@ export function rnxCaptureRasterRegions(
293
300
  nodeId: node.nodeId,
294
301
  parentNodeId: node.parentNodeId,
295
302
  type: node.type,
303
+ text: node.text,
296
304
  style: node.style.resolved,
297
305
  layout: node.layout.local,
298
306
  frame: node.layout.frame,
@@ -22,6 +22,7 @@ export const RNX_CLOUD_COMMAND_TYPES = Object.freeze([
22
22
  'tap',
23
23
  'longPress',
24
24
  'memory',
25
+ 'diagnostics',
25
26
  ]) satisfies readonly BridgeCommandType[]
26
27
 
27
28
  const RNX_CLOUD_COMMAND_TYPE_SET = new Set<string>(RNX_CLOUD_COMMAND_TYPES)
@@ -0,0 +1,302 @@
1
+ export const RNX_METRO_FINGERPRINT_SCHEMA_VERSION = 2
2
+ export const RNX_METRO_FINGERPRINT_POINTER_PATH = '/compile/metro-fingerprints.json'
3
+ export const RNX_METRO_FINGERPRINT_MAX_POINTER_BYTES = 16 * 1024
4
+ export const RNX_METRO_FINGERPRINT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024
5
+
6
+ export type RNXMetroFingerprintPlatform = 'ios' | 'android'
7
+
8
+ export interface RNXMetroFingerprintAtom {
9
+ key: string
10
+ exports: string[]
11
+ literals: string[]
12
+ dependencyArity: number
13
+ factoryDigest: string
14
+ }
15
+
16
+ export interface RNXMetroFingerprintNode {
17
+ atom: number
18
+ internalDependencies: Array<{ position: number; node: number }>
19
+ landmarkPath?: string
20
+ }
21
+
22
+ export interface RNXMetroFingerprintRoot {
23
+ node: number
24
+ }
25
+
26
+ export interface RNXMetroFingerprintTemplate {
27
+ packageName: string
28
+ packageVersion: string
29
+ sourceIntegrity: string
30
+ profile: string
31
+ platform: RNXMetroFingerprintPlatform
32
+ nodes: RNXMetroFingerprintNode[]
33
+ roots: RNXMetroFingerprintRoot[]
34
+ }
35
+
36
+ export interface RNXMetroFingerprintCandidate {
37
+ template: number
38
+ root: number
39
+ }
40
+
41
+ export interface RNXMetroFingerprintRegistry {
42
+ schemaVersion: typeof RNX_METRO_FINGERPRINT_SCHEMA_VERSION
43
+ revision: string
44
+ atoms: RNXMetroFingerprintAtom[]
45
+ templates: RNXMetroFingerprintTemplate[]
46
+ candidatesByFactoryDigest: Record<string, RNXMetroFingerprintCandidate[]>
47
+ candidatesByDiscriminator: Record<string, RNXMetroFingerprintCandidate[]>
48
+ }
49
+
50
+ export interface RNXMetroFingerprintPointer {
51
+ schemaVersion: typeof RNX_METRO_FINGERPRINT_SCHEMA_VERSION
52
+ revision: string
53
+ integrity: string
54
+ bytes: number
55
+ payloadUrl: string
56
+ }
57
+
58
+ function isRecord(value: unknown): value is Record<string, unknown> {
59
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
60
+ }
61
+
62
+ function isSha256(value: unknown): value is string {
63
+ return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)
64
+ }
65
+
66
+ function readStringArray(value: unknown): string[] | null {
67
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string')) {
68
+ return null
69
+ }
70
+ const entries = [...value]
71
+ if (
72
+ new Set(entries).size !== entries.length ||
73
+ entries.some((entry, index) => index > 0 && entries[index - 1] >= entry)
74
+ ) {
75
+ return null
76
+ }
77
+ return entries
78
+ }
79
+
80
+ export function parseRNXMetroFingerprintPointer(
81
+ value: unknown,
82
+ ): RNXMetroFingerprintPointer | null {
83
+ if (!isRecord(value)) return null
84
+ const { schemaVersion, revision, integrity, bytes, payloadUrl } = value
85
+ if (
86
+ schemaVersion !== RNX_METRO_FINGERPRINT_SCHEMA_VERSION ||
87
+ !isSha256(revision) ||
88
+ !isSha256(integrity) ||
89
+ typeof bytes !== 'number' ||
90
+ !Number.isSafeInteger(bytes) ||
91
+ bytes < 1 ||
92
+ typeof payloadUrl !== 'string' ||
93
+ payloadUrl.length === 0
94
+ ) {
95
+ return null
96
+ }
97
+ return { schemaVersion, revision, integrity, bytes, payloadUrl }
98
+ }
99
+
100
+ export function parseRNXMetroFingerprintRegistry(
101
+ value: unknown,
102
+ ): RNXMetroFingerprintRegistry | null {
103
+ if (!isRecord(value)) return null
104
+ const { schemaVersion, revision } = value
105
+ if (
106
+ schemaVersion !== RNX_METRO_FINGERPRINT_SCHEMA_VERSION ||
107
+ !isSha256(revision) ||
108
+ !Array.isArray(value.atoms) ||
109
+ !Array.isArray(value.templates) ||
110
+ !isRecord(value.candidatesByFactoryDigest) ||
111
+ !isRecord(value.candidatesByDiscriminator)
112
+ ) {
113
+ return null
114
+ }
115
+
116
+ const atoms: RNXMetroFingerprintAtom[] = []
117
+ for (const atomValue of value.atoms) {
118
+ if (!isRecord(atomValue)) return null
119
+ const exports = readStringArray(atomValue.exports)
120
+ const literals = readStringArray(atomValue.literals)
121
+ if (
122
+ typeof atomValue.key !== 'string' ||
123
+ atomValue.key.length === 0 ||
124
+ !exports ||
125
+ !literals ||
126
+ typeof atomValue.dependencyArity !== 'number' ||
127
+ !Number.isSafeInteger(atomValue.dependencyArity) ||
128
+ atomValue.dependencyArity < 0 ||
129
+ !isSha256(atomValue.factoryDigest)
130
+ ) {
131
+ return null
132
+ }
133
+ atoms.push({
134
+ key: atomValue.key,
135
+ exports,
136
+ literals,
137
+ dependencyArity: atomValue.dependencyArity,
138
+ factoryDigest: atomValue.factoryDigest,
139
+ })
140
+ }
141
+
142
+ const templates: RNXMetroFingerprintTemplate[] = []
143
+ for (const templateValue of value.templates) {
144
+ if (
145
+ !isRecord(templateValue) ||
146
+ typeof templateValue.packageName !== 'string' ||
147
+ templateValue.packageName.length === 0 ||
148
+ typeof templateValue.packageVersion !== 'string' ||
149
+ templateValue.packageVersion.length === 0 ||
150
+ !isSha256(templateValue.sourceIntegrity) ||
151
+ typeof templateValue.profile !== 'string' ||
152
+ templateValue.profile.length === 0 ||
153
+ (templateValue.platform !== 'ios' && templateValue.platform !== 'android') ||
154
+ !Array.isArray(templateValue.nodes) ||
155
+ templateValue.nodes.length === 0 ||
156
+ !Array.isArray(templateValue.roots) ||
157
+ templateValue.roots.length === 0
158
+ ) {
159
+ return null
160
+ }
161
+ const nodes: RNXMetroFingerprintNode[] = []
162
+ for (const nodeValue of templateValue.nodes) {
163
+ if (
164
+ !isRecord(nodeValue) ||
165
+ typeof nodeValue.atom !== 'number' ||
166
+ !Number.isSafeInteger(nodeValue.atom) ||
167
+ nodeValue.atom < 0 ||
168
+ nodeValue.atom >= atoms.length ||
169
+ !Array.isArray(nodeValue.internalDependencies) ||
170
+ (nodeValue.landmarkPath !== undefined &&
171
+ (typeof nodeValue.landmarkPath !== 'string' ||
172
+ nodeValue.landmarkPath.length === 0))
173
+ ) {
174
+ return null
175
+ }
176
+ const internalDependencies: Array<{ position: number; node: number }> = []
177
+ const positions = new Set<number>()
178
+ for (const edgeValue of nodeValue.internalDependencies) {
179
+ if (
180
+ !isRecord(edgeValue) ||
181
+ typeof edgeValue.position !== 'number' ||
182
+ !Number.isSafeInteger(edgeValue.position) ||
183
+ edgeValue.position < 0 ||
184
+ positions.has(edgeValue.position) ||
185
+ typeof edgeValue.node !== 'number' ||
186
+ !Number.isSafeInteger(edgeValue.node) ||
187
+ edgeValue.node < 0 ||
188
+ edgeValue.node >= templateValue.nodes.length
189
+ ) {
190
+ return null
191
+ }
192
+ positions.add(edgeValue.position)
193
+ internalDependencies.push({
194
+ position: edgeValue.position,
195
+ node: edgeValue.node,
196
+ })
197
+ }
198
+ internalDependencies.sort((left, right) => left.position - right.position)
199
+ nodes.push({
200
+ atom: nodeValue.atom,
201
+ internalDependencies,
202
+ ...(typeof nodeValue.landmarkPath === 'string'
203
+ ? { landmarkPath: nodeValue.landmarkPath }
204
+ : {}),
205
+ })
206
+ }
207
+ const roots: RNXMetroFingerprintRoot[] = []
208
+ for (const rootValue of templateValue.roots) {
209
+ if (
210
+ !isRecord(rootValue) ||
211
+ typeof rootValue.node !== 'number' ||
212
+ !Number.isSafeInteger(rootValue.node) ||
213
+ rootValue.node < 0 ||
214
+ rootValue.node >= nodes.length ||
215
+ !nodes[rootValue.node].landmarkPath
216
+ ) {
217
+ return null
218
+ }
219
+ roots.push({ node: rootValue.node })
220
+ }
221
+ templates.push({
222
+ packageName: templateValue.packageName,
223
+ packageVersion: templateValue.packageVersion,
224
+ sourceIntegrity: templateValue.sourceIntegrity,
225
+ profile: templateValue.profile,
226
+ platform: templateValue.platform,
227
+ nodes,
228
+ roots,
229
+ })
230
+ }
231
+
232
+ const candidatesByFactoryDigest: Record<string, RNXMetroFingerprintCandidate[]> = {}
233
+ for (const [factoryDigest, candidateValues] of Object.entries(
234
+ value.candidatesByFactoryDigest,
235
+ )) {
236
+ if (!isSha256(factoryDigest) || !Array.isArray(candidateValues)) return null
237
+ const candidates: RNXMetroFingerprintCandidate[] = []
238
+ for (const candidateValue of candidateValues) {
239
+ if (
240
+ !isRecord(candidateValue) ||
241
+ typeof candidateValue.template !== 'number' ||
242
+ !Number.isSafeInteger(candidateValue.template) ||
243
+ candidateValue.template < 0 ||
244
+ candidateValue.template >= templates.length ||
245
+ typeof candidateValue.root !== 'number' ||
246
+ !Number.isSafeInteger(candidateValue.root) ||
247
+ candidateValue.root < 0 ||
248
+ candidateValue.root >= templates[candidateValue.template].roots.length
249
+ ) {
250
+ return null
251
+ }
252
+ const template = templates[candidateValue.template]
253
+ const root = template.roots[candidateValue.root]
254
+ if (atoms[template.nodes[root.node].atom].factoryDigest !== factoryDigest) {
255
+ return null
256
+ }
257
+ candidates.push({
258
+ template: candidateValue.template,
259
+ root: candidateValue.root,
260
+ })
261
+ }
262
+ candidatesByFactoryDigest[factoryDigest] = candidates
263
+ }
264
+
265
+ const candidatesByDiscriminator: Record<string, RNXMetroFingerprintCandidate[]> = {}
266
+ for (const [discriminator, candidateValues] of Object.entries(
267
+ value.candidatesByDiscriminator,
268
+ )) {
269
+ if (!/^(?:e|l|ea|la|el|ela):[a-f0-9]{16}$/.test(discriminator)) return null
270
+ if (!Array.isArray(candidateValues) || candidateValues.length === 0) return null
271
+ const candidates: RNXMetroFingerprintCandidate[] = []
272
+ for (const candidateValue of candidateValues) {
273
+ if (
274
+ !isRecord(candidateValue) ||
275
+ typeof candidateValue.template !== 'number' ||
276
+ !Number.isSafeInteger(candidateValue.template) ||
277
+ candidateValue.template < 0 ||
278
+ candidateValue.template >= templates.length ||
279
+ typeof candidateValue.root !== 'number' ||
280
+ !Number.isSafeInteger(candidateValue.root) ||
281
+ candidateValue.root < 0 ||
282
+ candidateValue.root >= templates[candidateValue.template].roots.length
283
+ ) {
284
+ return null
285
+ }
286
+ candidates.push({
287
+ template: candidateValue.template,
288
+ root: candidateValue.root,
289
+ })
290
+ }
291
+ candidatesByDiscriminator[discriminator] = candidates
292
+ }
293
+
294
+ return {
295
+ schemaVersion,
296
+ revision,
297
+ atoms,
298
+ templates,
299
+ candidatesByFactoryDigest,
300
+ candidatesByDiscriminator,
301
+ }
302
+ }
@@ -24,8 +24,11 @@ import {
24
24
  isReplacementModuleRequestUrl,
25
25
  } from './host/replacement-module-handler'
26
26
  import {
27
- createRNXMetroModulePathsFooter,
27
+ appendRNXMetroModuleIdentityFooter,
28
+ RNX_METRO_MODULE_IDENTITY_VERSION,
29
+ toRNXDevelopmentBundleUrl,
28
30
  toRNXProductionBundleUrl,
31
+ type RNXMetroModuleIdentityMetadata,
29
32
  } from './metro-production-bundle'
30
33
  import {
31
34
  isRootRuntimeAssetPath,
@@ -36,13 +39,21 @@ import {
36
39
  } from './runtime-assets'
37
40
 
38
41
  const prefix = '/__soot'
39
- export { toRNXProductionBundleUrl }
42
+ export { toRNXDevelopmentBundleUrl, toRNXProductionBundleUrl }
43
+
44
+ type MetroSerializerModule = {
45
+ path: string
46
+ dependencies?: ReadonlyMap<
47
+ string,
48
+ {
49
+ absolutePath?: string
50
+ data?: { name?: string }
51
+ }
52
+ >
53
+ }
40
54
 
41
55
  type MetroSerializerGraph = {
42
- dependencies?: ReadonlyMap<unknown, { path: string }>
43
- transformOptions?: {
44
- dev?: boolean
45
- }
56
+ dependencies?: ReadonlyMap<unknown, MetroSerializerModule>
46
57
  }
47
58
 
48
59
  type MetroSerializerOptions = {
@@ -70,6 +81,12 @@ export interface RNXMetroOptions {
70
81
  * bundle the app ships and rnx can load it directly.
71
82
  */
72
83
  productionBundles?: boolean
84
+ /**
85
+ * append the `moduleId -> sourcePath` identity footer to every development
86
+ * bundle. resolution and transforms stay ordinary, while Metro keeps verbose
87
+ * module names and its source map.
88
+ */
89
+ developmentBundles?: boolean
73
90
  }
74
91
 
75
92
  // the slice of metro's config we touch — keeps `T` open for whatever
@@ -130,11 +147,10 @@ function createMetroDefaultSerializer(projectRoot: string): MetroSerializer {
130
147
  }
131
148
  }
132
149
 
133
- // the only thing rnx adds to a production bundle. Metro drops the fourth `__d`
134
- // argument when `dev` is false, so nothing in the artifact names its modules;
135
- // the engine's `__d` trap needs those names to swap react, react-native, and
136
- // the native-seam packages for its own. the footer restores exactly that, and
137
- // is inert everywhere else — on a device it assigns one unused global.
150
+ // the only thing rnx adds to an ordinary Metro bundle. production needs the
151
+ // footer because Metro drops the fourth `__d` argument when `dev` is false;
152
+ // development keeps that verbose name and receives the same identity without
153
+ // changing resolution, transforms, or the trailing source map.
138
154
  function appendRNXModulePaths(
139
155
  result: MetroSerializerResult,
140
156
  graph: MetroSerializerGraph,
@@ -144,9 +160,7 @@ function appendRNXModulePaths(
144
160
  const dependencies = graph.dependencies
145
161
  const createModuleId = options.createModuleId
146
162
  if (!dependencies || !createModuleId) {
147
- throw new Error(
148
- 'rnxsim: Metro did not provide the production module graph and createModuleId',
149
- )
163
+ throw new Error('rnxsim: Metro did not provide the module graph and createModuleId')
150
164
  }
151
165
 
152
166
  const modulePaths: Record<string, string> = {}
@@ -156,20 +170,56 @@ function appendRNXModulePaths(
156
170
  .replaceAll(path.sep, '/')
157
171
  }
158
172
  if (Object.keys(modulePaths).length !== dependencies.size) {
159
- throw new Error('rnxsim: production module IDs were not unique')
173
+ throw new Error('rnxsim: bundle module IDs were not unique')
160
174
  }
161
175
 
162
- const footer = createRNXMetroModulePathsFooter(modulePaths)
176
+ const specifiersByModuleId = new Map<string, Set<string>>()
177
+ for (const module of dependencies.values()) {
178
+ for (const [dependencyKey, dependency] of module.dependencies ?? []) {
179
+ const specifier = dependency.data?.name ?? dependencyKey
180
+ if (
181
+ specifier.length === 0 ||
182
+ specifier.startsWith('.') ||
183
+ specifier.startsWith('/') ||
184
+ typeof dependency.absolutePath !== 'string'
185
+ ) {
186
+ continue
187
+ }
188
+ const moduleId = String(createModuleId(dependency.absolutePath))
189
+ if (!Object.prototype.hasOwnProperty.call(modulePaths, moduleId)) continue
190
+ const found = specifiersByModuleId.get(moduleId)
191
+ if (found) found.add(specifier)
192
+ else specifiersByModuleId.set(moduleId, new Set([specifier]))
193
+ }
194
+ }
195
+ const logicalSpecifiers = Object.fromEntries(
196
+ [...specifiersByModuleId]
197
+ .sort(([left], [right]) => Number(left) - Number(right))
198
+ .map(([moduleId, specifiers]) => [moduleId, [...specifiers].sort()]),
199
+ )
200
+ const identity: RNXMetroModuleIdentityMetadata = {
201
+ version: RNX_METRO_MODULE_IDENTITY_VERSION,
202
+ identitySource: 'rnx-plugin',
203
+ modulePaths,
204
+ logicalSpecifiers,
205
+ }
163
206
  return typeof result === 'string'
164
- ? result + footer
165
- : { ...result, code: result.code + footer }
207
+ ? appendRNXMetroModuleIdentityFooter(result, identity)
208
+ : { ...result, code: appendRNXMetroModuleIdentityFooter(result.code, identity) }
166
209
  }
167
210
 
168
211
  export function withRNX<T extends Record<string, unknown>>(
169
212
  config: T,
170
213
  options: RNXMetroOptions = {},
171
214
  ): T {
172
- if (!options.devServer && !options.open && !options.productionBundles) return config
215
+ if (
216
+ !options.devServer &&
217
+ !options.open &&
218
+ !options.productionBundles &&
219
+ !options.developmentBundles
220
+ ) {
221
+ return config
222
+ }
173
223
 
174
224
  const cfgProjectRoot = (config as MetroConfigWithServer).projectRoot || process.cwd()
175
225
  const appName = resolveRnxDesktopAppName(options.open, cfgProjectRoot)
@@ -191,7 +241,7 @@ export function withRNX<T extends Record<string, unknown>>(
191
241
  const existingSerializer = cfgSerializer?.customSerializer
192
242
  let productionConfig: Partial<MetroConfigWithServer> = {}
193
243
 
194
- if (options.productionBundles) {
244
+ if (options.productionBundles || options.developmentBundles) {
195
245
  let defaultSerializer: MetroSerializer | undefined
196
246
 
197
247
  productionConfig = {
@@ -212,10 +262,9 @@ export function withRNX<T extends Record<string, unknown>>(
212
262
  graph,
213
263
  serializerOptions,
214
264
  )
215
- // every production build gets the footer, so the artifact the app
216
- // already ships is the one rnx loads. a development build keeps
217
- // Metro's fourth `__d` argument and needs nothing.
218
- return graph.transformOptions?.dev === false
265
+ const dev = serializerOptions.dev
266
+ return (dev === false && options.productionBundles === true) ||
267
+ (dev === true && options.developmentBundles === true)
219
268
  ? appendRNXModulePaths(result, graph, serializerOptions, cfgProjectRoot)
220
269
  : result
221
270
  },