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
@@ -1,9 +1,27 @@
1
1
  const RNXSIM_METRO_MODULE_PATHS_PREFIX = 'globalThis.__sootsimModulePaths='
2
+ export const RNX_METRO_MODULE_IDENTITY_VERSION = 2
2
3
 
3
4
  export type RNXMetroModulePathMap = Record<string, string>
4
- export interface RNXMetroModuleIdentity {
5
- source: string
5
+ export type RNXMetroLogicalSpecifierMap = Record<string, string[]>
6
+ export type RNXMetroModuleIdentitySource =
7
+ | 'rnx-plugin'
8
+ | 'contrast-bundler'
9
+ | 'metro-source-map'
10
+
11
+ export interface RNXMetroModuleIdentityMetadata {
12
+ version: typeof RNX_METRO_MODULE_IDENTITY_VERSION
13
+ identitySource: RNXMetroModuleIdentitySource
6
14
  modulePaths: RNXMetroModulePathMap
15
+ logicalSpecifiers: RNXMetroLogicalSpecifierMap
16
+ }
17
+
18
+ export interface RNXMetroModuleIdentity extends RNXMetroModuleIdentityMetadata {
19
+ source: string
20
+ /**
21
+ * Metro's trailing `//# sourceMappingURL=` comment when the graph carried
22
+ * one, already split off `source`. A production graph has none.
23
+ */
24
+ sourceMappingUrl: string | null
7
25
  }
8
26
 
9
27
  // the production bundle rnx loads is the app's ORDINARY production bundle.
@@ -19,36 +37,156 @@ export function toRNXProductionBundleUrl(bundleUrl: string): string {
19
37
  return relative ? `${url.pathname}${url.search}${url.hash}` : url.toString()
20
38
  }
21
39
 
22
- export function createRNXMetroModulePathsFooter(
23
- modulePaths: RNXMetroModulePathMap,
40
+ /**
41
+ * the ordinary development counterpart. Metro keeps `__DEV__` true, verbose
42
+ * module names, and its source map. `hot` stays false because the artifact is
43
+ * immutable once produced.
44
+ */
45
+ export function toRNXDevelopmentBundleUrl(bundleUrl: string): string {
46
+ const relative = bundleUrl.startsWith('/')
47
+ const url = new URL(bundleUrl, 'http://rnxsim.local')
48
+ url.searchParams.set('dev', 'true')
49
+ url.searchParams.set('hot', 'false')
50
+ url.searchParams.set('minify', 'false')
51
+ return relative ? `${url.pathname}${url.search}${url.hash}` : url.toString()
52
+ }
53
+
54
+ const RNXSIM_METRO_SOURCE_MAP_COMMENT = '//# sourceMappingURL='
55
+
56
+ export interface RNXMetroDetachedSourceMapUrl {
57
+ source: string
58
+ /** the comment's URL, which is a whole inline map when Metro inlined it. */
59
+ sourceMappingUrl: string | null
60
+ }
61
+
62
+ /**
63
+ * splits Metro's trailing `//# sourceMappingURL=` comment off the bundle.
64
+ *
65
+ * a development bundle ends with that comment, and Metro inlines the entire
66
+ * map into it as a data URL whenever the caller did not ask for an external
67
+ * map file. leaving it attached makes the last `__r(` statement carry the map
68
+ * as source text: the layout reader would hand megabytes of base64 to the
69
+ * minifier, and the artifact the runner loads would keep the map resident in
70
+ * child memory for the life of the simulator.
71
+ */
72
+ export function detachRNXMetroSourceMapUrl(
73
+ bundleText: string,
74
+ ): RNXMetroDetachedSourceMapUrl {
75
+ const trimmedEnd = bundleText.replace(/\s+$/, '')
76
+ const commentStart = trimmedEnd.lastIndexOf(`\n${RNXSIM_METRO_SOURCE_MAP_COMMENT}`)
77
+ if (commentStart < 0) return { source: bundleText, sourceMappingUrl: null }
78
+ const url = trimmedEnd.slice(commentStart + 1 + RNXSIM_METRO_SOURCE_MAP_COMMENT.length)
79
+ if (url.includes('\n')) return { source: bundleText, sourceMappingUrl: null }
80
+ return { source: trimmedEnd.slice(0, commentStart), sourceMappingUrl: url }
81
+ }
82
+
83
+ export function createRNXMetroModuleIdentityFooter(
84
+ identity: RNXMetroModuleIdentityMetadata,
24
85
  ): string {
25
- const payload = JSON.stringify(modulePaths)
86
+ const payload = JSON.stringify(identity)
26
87
  .replace(/\u2028/g, '\\u2028')
27
88
  .replace(/\u2029/g, '\\u2029')
28
89
  .replace(/<\//g, '<\\/')
29
90
  return `\n${RNXSIM_METRO_MODULE_PATHS_PREFIX}${payload};`
30
91
  }
31
92
 
93
+ /**
94
+ * writes the identity footer into a serialized bundle, keeping Metro's
95
+ * `//# sourceMappingURL=` comment on the last line when the graph has one. a
96
+ * production graph has no such comment and gets the footer appended, exactly
97
+ * as before.
98
+ */
99
+ export function appendRNXMetroModuleIdentityFooter(
100
+ bundleText: string,
101
+ identity: RNXMetroModuleIdentityMetadata,
102
+ ): string {
103
+ const footer = createRNXMetroModuleIdentityFooter(identity)
104
+ const detached = detachRNXMetroSourceMapUrl(bundleText)
105
+ return detached.sourceMappingUrl === null
106
+ ? bundleText + footer
107
+ : `${detached.source}${footer}\n${RNXSIM_METRO_SOURCE_MAP_COMMENT}${detached.sourceMappingUrl}`
108
+ }
109
+
32
110
  export function readRNXMetroModuleIdentity(
33
111
  bundleText: string,
34
112
  ): RNXMetroModuleIdentity | null {
113
+ const detached = detachRNXMetroSourceMapUrl(bundleText)
114
+ const withoutMap = detached.source
35
115
  const footerPrefix = `\n${RNXSIM_METRO_MODULE_PATHS_PREFIX}`
36
- const footerStart = bundleText.lastIndexOf(footerPrefix)
37
- if (footerStart < 0 || !bundleText.endsWith(';')) return null
116
+ const footerStart = withoutMap.lastIndexOf(footerPrefix)
117
+ if (footerStart < 0) return null
118
+ if (!withoutMap.endsWith(';')) {
119
+ throw new Error('Metro module identity footer is truncated')
120
+ }
38
121
 
39
- const parsed: unknown = JSON.parse(
40
- bundleText.slice(footerStart + footerPrefix.length, -1),
41
- )
122
+ let parsed: unknown
123
+ try {
124
+ parsed = JSON.parse(withoutMap.slice(footerStart + footerPrefix.length, -1))
125
+ } catch {
126
+ throw new Error('Metro module identity footer is not valid JSON')
127
+ }
42
128
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
43
- return null
129
+ throw new Error('Metro module identity footer is malformed')
130
+ }
131
+
132
+ const version = Reflect.get(parsed, 'version')
133
+ // stored artifacts from before the versioned footer carry a bare module-path
134
+ // map with no version field. they are development bundles whose identity
135
+ // comes from Metro's verbose names, so treat their footer as absent rather
136
+ // than failing every stored replay.
137
+ if (version === undefined) return null
138
+ const identitySource = Reflect.get(parsed, 'identitySource')
139
+ const parsedModulePaths = Reflect.get(parsed, 'modulePaths')
140
+ const parsedLogicalSpecifiers = Reflect.get(parsed, 'logicalSpecifiers')
141
+ if (
142
+ version !== RNX_METRO_MODULE_IDENTITY_VERSION ||
143
+ (identitySource !== 'rnx-plugin' &&
144
+ identitySource !== 'contrast-bundler' &&
145
+ identitySource !== 'metro-source-map') ||
146
+ typeof parsedModulePaths !== 'object' ||
147
+ parsedModulePaths === null ||
148
+ Array.isArray(parsedModulePaths) ||
149
+ typeof parsedLogicalSpecifiers !== 'object' ||
150
+ parsedLogicalSpecifiers === null ||
151
+ Array.isArray(parsedLogicalSpecifiers)
152
+ ) {
153
+ throw new Error('Metro module identity footer is malformed')
44
154
  }
45
155
 
46
156
  const modulePaths: RNXMetroModulePathMap = {}
47
- for (const [moduleId, modulePath] of Object.entries(parsed)) {
48
- if (typeof modulePath !== 'string') return null
157
+ for (const [moduleId, modulePath] of Object.entries(parsedModulePaths)) {
158
+ if (typeof modulePath !== 'string') {
159
+ throw new Error(`Metro module identity has an invalid path for ${moduleId}`)
160
+ }
49
161
  modulePaths[moduleId] = modulePath
50
162
  }
51
- return { source: bundleText.slice(0, footerStart), modulePaths }
163
+ const logicalSpecifiers: RNXMetroLogicalSpecifierMap = {}
164
+ for (const [moduleId, specifiers] of Object.entries(parsedLogicalSpecifiers)) {
165
+ if (
166
+ !Array.isArray(specifiers) ||
167
+ specifiers.length === 0 ||
168
+ !specifiers.every(
169
+ (specifier) => typeof specifier === 'string' && specifier.length > 0,
170
+ )
171
+ ) {
172
+ throw new Error(`Metro module identity has invalid specifiers for ${moduleId}`)
173
+ }
174
+ const unique = [...new Set(specifiers)].sort((left, right) =>
175
+ left.localeCompare(right),
176
+ )
177
+ if (unique.length !== specifiers.length) {
178
+ throw new Error(`Metro module identity repeats a specifier for ${moduleId}`)
179
+ }
180
+ logicalSpecifiers[moduleId] = unique
181
+ }
182
+ return {
183
+ source: withoutMap.slice(0, footerStart),
184
+ version,
185
+ identitySource,
186
+ modulePaths,
187
+ logicalSpecifiers,
188
+ sourceMappingUrl: detached.sourceMappingUrl,
189
+ }
52
190
  }
53
191
 
54
192
  // the exact layout Metro's baseJSBundle emits and the rnx runtime contract
@@ -67,6 +205,12 @@ export interface RNXMetroDefine {
67
205
  factory: { start: number; end: number }
68
206
  /** the dependency array, brackets included. */
69
207
  dependencies: { start: number; end: number }
208
+ /**
209
+ * the module's verbose name, quotes included, when the graph carries one.
210
+ * Metro emits it as a fourth `__d` argument on a development graph and omits
211
+ * it entirely on a production one.
212
+ */
213
+ verboseName?: { start: number; end: number }
70
214
  }
71
215
 
72
216
  export interface RNXMetroRun {
@@ -92,12 +236,33 @@ interface DefineTail {
92
236
  factoryEnd: number
93
237
  dependenciesStart: number
94
238
  dependenciesEnd: number
239
+ verboseNameStart?: number
240
+ verboseNameEnd?: number
241
+ }
242
+
243
+ // walks back over a single-quoted or double-quoted string literal ending at
244
+ // `end`, returning the index of its opening quote. an odd run of backslashes
245
+ // before a quote escapes it, so count them before accepting a candidate.
246
+ function readStringLiteralStart(source: string, start: number, end: number): number {
247
+ const quote = source[end]
248
+ if (quote !== '"' && quote !== "'") return -1
249
+ for (let index = end - 1; index > start; index--) {
250
+ if (source[index] !== quote) continue
251
+ let backslashes = 0
252
+ while (index - 1 - backslashes > start && source[index - 1 - backslashes] === '\\') {
253
+ backslashes++
254
+ }
255
+ if (backslashes % 2 === 0) return index
256
+ }
257
+ return -1
95
258
  }
96
259
 
97
- // each `__d(...)` ends with `),<id>,[dep,dep,...]);?`. the dependency array can
98
- // be hundreds of entries long, so walk back from `end` instead of searching a
99
- // fixed tail window: skip trailing `;` and whitespace, consume the closing `)`,
100
- // balance `[...]` to find its opening bracket, then read the integer before it.
260
+ // each `__d(...)` ends with `),<id>,[dep,dep,...]);?`, and a development graph
261
+ // adds a fourth verbose-name string after the dependency array. the dependency
262
+ // array can be hundreds of entries long, so walk back from `end` instead of
263
+ // searching a fixed tail window: skip trailing `;` and whitespace, consume the
264
+ // closing `)`, take the verbose name when one is there, balance `[...]` to find
265
+ // its opening bracket, then read the integer before it.
101
266
  function readDefineTail(source: string, start: number, end: number): DefineTail | null {
102
267
  let cursor = end
103
268
  while (
@@ -112,6 +277,19 @@ function readDefineTail(source: string, start: number, end: number): DefineTail
112
277
  if (source[cursor] !== ')') return null
113
278
  cursor--
114
279
  while (cursor > start && /\s/.test(source[cursor])) cursor--
280
+ let verboseNameStart: number | undefined
281
+ let verboseNameEnd: number | undefined
282
+ if (source[cursor] === '"' || source[cursor] === "'") {
283
+ const literalStart = readStringLiteralStart(source, start, cursor)
284
+ if (literalStart < 0) return null
285
+ verboseNameStart = literalStart
286
+ verboseNameEnd = cursor + 1
287
+ cursor = literalStart - 1
288
+ while (cursor > start && /\s/.test(source[cursor])) cursor--
289
+ if (source[cursor] !== ',') return null
290
+ cursor--
291
+ while (cursor > start && /\s/.test(source[cursor])) cursor--
292
+ }
115
293
  if (source[cursor] !== ']') return null
116
294
  const dependenciesEnd = cursor + 1
117
295
  let depth = 1
@@ -132,7 +310,7 @@ function readDefineTail(source: string, start: number, end: number): DefineTail
132
310
  // whole numeric literal rather than a run of decimal digits. the factory
133
311
  // before it always ends in `}` or `)`, which stops this walk immediately.
134
312
  let idStart = idEnd
135
- while (idStart > start && /[0-9a-fA-FxX.+\-]/.test(source[idStart - 1])) idStart--
313
+ while (idStart > start && /[0-9a-fA-FxX.+-]/.test(source[idStart - 1])) idStart--
136
314
  const literal = source.slice(idStart, idEnd)
137
315
  if (!/^-?(?:0[xXbBoO][0-9a-fA-F]+|\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)$/.test(literal)) {
138
316
  return null
@@ -147,6 +325,8 @@ function readDefineTail(source: string, start: number, end: number): DefineTail
147
325
  factoryEnd,
148
326
  dependenciesStart,
149
327
  dependenciesEnd,
328
+ verboseNameStart,
329
+ verboseNameEnd,
150
330
  }
151
331
  }
152
332
 
@@ -192,6 +372,9 @@ export function readRNXMetroBundleLayout(source: string): RNXMetroBundleLayout {
192
372
  endLineIndex,
193
373
  factory: { start: start + '__d('.length, end: tail.factoryEnd },
194
374
  dependencies: { start: tail.dependenciesStart, end: tail.dependenciesEnd },
375
+ ...(tail.verboseNameStart !== undefined && tail.verboseNameEnd !== undefined
376
+ ? { verboseName: { start: tail.verboseNameStart, end: tail.verboseNameEnd } }
377
+ : {}),
195
378
  })
196
379
  }
197
380
 
@@ -250,3 +433,14 @@ export function validateRNXMetroModulePaths(
250
433
  }
251
434
  }
252
435
  }
436
+
437
+ export function validateRNXMetroModuleIdentity(identity: RNXMetroModuleIdentity): void {
438
+ validateRNXMetroModulePaths(identity.source, identity.modulePaths)
439
+ for (const moduleId of Object.keys(identity.logicalSpecifiers)) {
440
+ if (!Object.prototype.hasOwnProperty.call(identity.modulePaths, moduleId)) {
441
+ throw new Error(
442
+ `Metro module identity has logical specifiers for unknown module ${moduleId}`,
443
+ )
444
+ }
445
+ }
446
+ }
@@ -211,6 +211,11 @@ export function serveRuntimeFile(req: any, res: any, fullPath: string) {
211
211
  const stat = fs.statSync(fullPath)
212
212
  const lastModified = stat.mtime.toUTCString()
213
213
  res.setHeader('content-type', MIME_TYPES[ext] || 'application/octet-stream')
214
+ if (/^canvaskit-graphite-pipelines-[0-9a-f]{8}\.bin$/.test(path.basename(fullPath))) {
215
+ res.setHeader('cache-control', 'public, max-age=31536000, immutable')
216
+ fs.createReadStream(fullPath).pipe(res)
217
+ return
218
+ }
214
219
  // these urls are STABLE across dev rebuilds and runtime version switches
215
220
  // (worker.js, canvaskit-graphite.wasm, the html shells are unhashed), so the
216
221
  // browser must revalidate every load — the old year-long immutable here made every