rnxsim 0.1.402 → 0.1.403

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 (56) hide show
  1. package/README.md +16 -7
  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 +37 -12
  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-production-bundle.cjs +72 -7
  38. package/dist-lib/metro-production-bundle.mjs +69 -7
  39. package/dist-lib/metro.cjs +41 -10
  40. package/dist-lib/profiles.cjs +1 -1
  41. package/dist-lib/public-brand.cjs +1 -1
  42. package/dist-lib/react-native-host-modules.cjs +1 -1
  43. package/dist-lib/react-native-host-modules.mjs +1 -1
  44. package/dist-lib/render-mode.cjs +1 -1
  45. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  46. package/dist-lib/sdk.cjs +1 -1
  47. package/dist-lib/sdk.mjs +1 -1
  48. package/dist-lib/skills.cjs +3 -2
  49. package/dist-lib/vite.cjs +6 -1
  50. package/package.json +1 -1
  51. package/src/bridge-contract.ts +20 -0
  52. package/src/capture-contract.ts +8 -0
  53. package/src/cloud-contract.ts +1 -0
  54. package/src/metro-plugin.ts +29 -23
  55. package/src/metro-production-bundle.ts +124 -8
  56. package/src/runtime-assets.ts +5 -0
@@ -24,7 +24,8 @@ import {
24
24
  isReplacementModuleRequestUrl,
25
25
  } from './host/replacement-module-handler'
26
26
  import {
27
- createRNXMetroModulePathsFooter,
27
+ appendRNXMetroModulePathsFooter,
28
+ toRNXDevelopmentBundleUrl,
28
29
  toRNXProductionBundleUrl,
29
30
  } from './metro-production-bundle'
30
31
  import {
@@ -36,13 +37,10 @@ import {
36
37
  } from './runtime-assets'
37
38
 
38
39
  const prefix = '/__soot'
39
- export { toRNXProductionBundleUrl }
40
+ export { toRNXDevelopmentBundleUrl, toRNXProductionBundleUrl }
40
41
 
41
42
  type MetroSerializerGraph = {
42
43
  dependencies?: ReadonlyMap<unknown, { path: string }>
43
- transformOptions?: {
44
- dev?: boolean
45
- }
46
44
  }
47
45
 
48
46
  type MetroSerializerOptions = {
@@ -70,6 +68,12 @@ export interface RNXMetroOptions {
70
68
  * bundle the app ships and rnx can load it directly.
71
69
  */
72
70
  productionBundles?: boolean
71
+ /**
72
+ * append the `moduleId -> sourcePath` identity footer to every development
73
+ * bundle. resolution and transforms stay ordinary, while Metro keeps verbose
74
+ * module names and its source map.
75
+ */
76
+ developmentBundles?: boolean
73
77
  }
74
78
 
75
79
  // the slice of metro's config we touch — keeps `T` open for whatever
@@ -130,11 +134,10 @@ function createMetroDefaultSerializer(projectRoot: string): MetroSerializer {
130
134
  }
131
135
  }
132
136
 
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.
137
+ // the only thing rnx adds to an ordinary Metro bundle. production needs the
138
+ // footer because Metro drops the fourth `__d` argument when `dev` is false;
139
+ // development keeps that verbose name and receives the same identity without
140
+ // changing resolution, transforms, or the trailing source map.
138
141
  function appendRNXModulePaths(
139
142
  result: MetroSerializerResult,
140
143
  graph: MetroSerializerGraph,
@@ -144,9 +147,7 @@ function appendRNXModulePaths(
144
147
  const dependencies = graph.dependencies
145
148
  const createModuleId = options.createModuleId
146
149
  if (!dependencies || !createModuleId) {
147
- throw new Error(
148
- 'rnxsim: Metro did not provide the production module graph and createModuleId',
149
- )
150
+ throw new Error('rnxsim: Metro did not provide the module graph and createModuleId')
150
151
  }
151
152
 
152
153
  const modulePaths: Record<string, string> = {}
@@ -156,20 +157,26 @@ function appendRNXModulePaths(
156
157
  .replaceAll(path.sep, '/')
157
158
  }
158
159
  if (Object.keys(modulePaths).length !== dependencies.size) {
159
- throw new Error('rnxsim: production module IDs were not unique')
160
+ throw new Error('rnxsim: bundle module IDs were not unique')
160
161
  }
161
162
 
162
- const footer = createRNXMetroModulePathsFooter(modulePaths)
163
163
  return typeof result === 'string'
164
- ? result + footer
165
- : { ...result, code: result.code + footer }
164
+ ? appendRNXMetroModulePathsFooter(result, modulePaths)
165
+ : { ...result, code: appendRNXMetroModulePathsFooter(result.code, modulePaths) }
166
166
  }
167
167
 
168
168
  export function withRNX<T extends Record<string, unknown>>(
169
169
  config: T,
170
170
  options: RNXMetroOptions = {},
171
171
  ): T {
172
- if (!options.devServer && !options.open && !options.productionBundles) return config
172
+ if (
173
+ !options.devServer &&
174
+ !options.open &&
175
+ !options.productionBundles &&
176
+ !options.developmentBundles
177
+ ) {
178
+ return config
179
+ }
173
180
 
174
181
  const cfgProjectRoot = (config as MetroConfigWithServer).projectRoot || process.cwd()
175
182
  const appName = resolveRnxDesktopAppName(options.open, cfgProjectRoot)
@@ -191,7 +198,7 @@ export function withRNX<T extends Record<string, unknown>>(
191
198
  const existingSerializer = cfgSerializer?.customSerializer
192
199
  let productionConfig: Partial<MetroConfigWithServer> = {}
193
200
 
194
- if (options.productionBundles) {
201
+ if (options.productionBundles || options.developmentBundles) {
195
202
  let defaultSerializer: MetroSerializer | undefined
196
203
 
197
204
  productionConfig = {
@@ -212,10 +219,9 @@ export function withRNX<T extends Record<string, unknown>>(
212
219
  graph,
213
220
  serializerOptions,
214
221
  )
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
222
+ const dev = serializerOptions.dev
223
+ return (dev === false && options.productionBundles === true) ||
224
+ (dev === true && options.developmentBundles === true)
219
225
  ? appendRNXModulePaths(result, graph, serializerOptions, cfgProjectRoot)
220
226
  : result
221
227
  },
@@ -4,6 +4,11 @@ export type RNXMetroModulePathMap = Record<string, string>
4
4
  export interface RNXMetroModuleIdentity {
5
5
  source: string
6
6
  modulePaths: RNXMetroModulePathMap
7
+ /**
8
+ * Metro's trailing `//# sourceMappingURL=` comment when the graph carried
9
+ * one, already split off `source`. A production graph has none.
10
+ */
11
+ sourceMappingUrl: string | null
7
12
  }
8
13
 
9
14
  // the production bundle rnx loads is the app's ORDINARY production bundle.
@@ -19,6 +24,49 @@ export function toRNXProductionBundleUrl(bundleUrl: string): string {
19
24
  return relative ? `${url.pathname}${url.search}${url.hash}` : url.toString()
20
25
  }
21
26
 
27
+ /**
28
+ * the ordinary development counterpart. Metro keeps `__DEV__` true, verbose
29
+ * module names, and its source map. `hot` stays false because the artifact is
30
+ * immutable once produced.
31
+ */
32
+ export function toRNXDevelopmentBundleUrl(bundleUrl: string): string {
33
+ const relative = bundleUrl.startsWith('/')
34
+ const url = new URL(bundleUrl, 'http://rnxsim.local')
35
+ url.searchParams.set('dev', 'true')
36
+ url.searchParams.set('hot', 'false')
37
+ url.searchParams.set('minify', 'false')
38
+ return relative ? `${url.pathname}${url.search}${url.hash}` : url.toString()
39
+ }
40
+
41
+ const RNXSIM_METRO_SOURCE_MAP_COMMENT = '//# sourceMappingURL='
42
+
43
+ export interface RNXMetroDetachedSourceMapUrl {
44
+ source: string
45
+ /** the comment's URL, which is a whole inline map when Metro inlined it. */
46
+ sourceMappingUrl: string | null
47
+ }
48
+
49
+ /**
50
+ * splits Metro's trailing `//# sourceMappingURL=` comment off the bundle.
51
+ *
52
+ * a development bundle ends with that comment, and Metro inlines the entire
53
+ * map into it as a data URL whenever the caller did not ask for an external
54
+ * map file. leaving it attached makes the last `__r(` statement carry the map
55
+ * as source text: the layout reader would hand megabytes of base64 to the
56
+ * minifier, and the artifact the runner loads would keep the map resident in
57
+ * child memory for the life of the simulator.
58
+ */
59
+ export function detachRNXMetroSourceMapUrl(
60
+ bundleText: string,
61
+ ): RNXMetroDetachedSourceMapUrl {
62
+ const trimmedEnd = bundleText.replace(/\s+$/, '')
63
+ const commentStart = trimmedEnd.lastIndexOf(`\n${RNXSIM_METRO_SOURCE_MAP_COMMENT}`)
64
+ if (commentStart < 0) return { source: bundleText, sourceMappingUrl: null }
65
+ const url = trimmedEnd.slice(commentStart + 1 + RNXSIM_METRO_SOURCE_MAP_COMMENT.length)
66
+ if (url.includes('\n')) return { source: bundleText, sourceMappingUrl: null }
67
+ return { source: trimmedEnd.slice(0, commentStart), sourceMappingUrl: url }
68
+ }
69
+
22
70
  export function createRNXMetroModulePathsFooter(
23
71
  modulePaths: RNXMetroModulePathMap,
24
72
  ): string {
@@ -29,15 +77,34 @@ export function createRNXMetroModulePathsFooter(
29
77
  return `\n${RNXSIM_METRO_MODULE_PATHS_PREFIX}${payload};`
30
78
  }
31
79
 
80
+ /**
81
+ * writes the identity footer into a serialized bundle, keeping Metro's
82
+ * `//# sourceMappingURL=` comment on the last line when the graph has one. a
83
+ * production graph has no such comment and gets the footer appended, exactly
84
+ * as before.
85
+ */
86
+ export function appendRNXMetroModulePathsFooter(
87
+ bundleText: string,
88
+ modulePaths: RNXMetroModulePathMap,
89
+ ): string {
90
+ const footer = createRNXMetroModulePathsFooter(modulePaths)
91
+ const detached = detachRNXMetroSourceMapUrl(bundleText)
92
+ return detached.sourceMappingUrl === null
93
+ ? bundleText + footer
94
+ : `${detached.source}${footer}\n${RNXSIM_METRO_SOURCE_MAP_COMMENT}${detached.sourceMappingUrl}`
95
+ }
96
+
32
97
  export function readRNXMetroModuleIdentity(
33
98
  bundleText: string,
34
99
  ): RNXMetroModuleIdentity | null {
100
+ const detached = detachRNXMetroSourceMapUrl(bundleText)
101
+ const withoutMap = detached.source
35
102
  const footerPrefix = `\n${RNXSIM_METRO_MODULE_PATHS_PREFIX}`
36
- const footerStart = bundleText.lastIndexOf(footerPrefix)
37
- if (footerStart < 0 || !bundleText.endsWith(';')) return null
103
+ const footerStart = withoutMap.lastIndexOf(footerPrefix)
104
+ if (footerStart < 0 || !withoutMap.endsWith(';')) return null
38
105
 
39
106
  const parsed: unknown = JSON.parse(
40
- bundleText.slice(footerStart + footerPrefix.length, -1),
107
+ withoutMap.slice(footerStart + footerPrefix.length, -1),
41
108
  )
42
109
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
43
110
  return null
@@ -48,7 +115,11 @@ export function readRNXMetroModuleIdentity(
48
115
  if (typeof modulePath !== 'string') return null
49
116
  modulePaths[moduleId] = modulePath
50
117
  }
51
- return { source: bundleText.slice(0, footerStart), modulePaths }
118
+ return {
119
+ source: withoutMap.slice(0, footerStart),
120
+ modulePaths,
121
+ sourceMappingUrl: detached.sourceMappingUrl,
122
+ }
52
123
  }
53
124
 
54
125
  // the exact layout Metro's baseJSBundle emits and the rnx runtime contract
@@ -67,6 +138,12 @@ export interface RNXMetroDefine {
67
138
  factory: { start: number; end: number }
68
139
  /** the dependency array, brackets included. */
69
140
  dependencies: { start: number; end: number }
141
+ /**
142
+ * the module's verbose name, quotes included, when the graph carries one.
143
+ * Metro emits it as a fourth `__d` argument on a development graph and omits
144
+ * it entirely on a production one.
145
+ */
146
+ verboseName?: { start: number; end: number }
70
147
  }
71
148
 
72
149
  export interface RNXMetroRun {
@@ -92,12 +169,33 @@ interface DefineTail {
92
169
  factoryEnd: number
93
170
  dependenciesStart: number
94
171
  dependenciesEnd: number
172
+ verboseNameStart?: number
173
+ verboseNameEnd?: number
174
+ }
175
+
176
+ // walks back over a single-quoted or double-quoted string literal ending at
177
+ // `end`, returning the index of its opening quote. an odd run of backslashes
178
+ // before a quote escapes it, so count them before accepting a candidate.
179
+ function readStringLiteralStart(source: string, start: number, end: number): number {
180
+ const quote = source[end]
181
+ if (quote !== '"' && quote !== "'") return -1
182
+ for (let index = end - 1; index > start; index--) {
183
+ if (source[index] !== quote) continue
184
+ let backslashes = 0
185
+ while (index - 1 - backslashes > start && source[index - 1 - backslashes] === '\\') {
186
+ backslashes++
187
+ }
188
+ if (backslashes % 2 === 0) return index
189
+ }
190
+ return -1
95
191
  }
96
192
 
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.
193
+ // each `__d(...)` ends with `),<id>,[dep,dep,...]);?`, and a development graph
194
+ // adds a fourth verbose-name string after the dependency array. the dependency
195
+ // array can be hundreds of entries long, so walk back from `end` instead of
196
+ // searching a fixed tail window: skip trailing `;` and whitespace, consume the
197
+ // closing `)`, take the verbose name when one is there, balance `[...]` to find
198
+ // its opening bracket, then read the integer before it.
101
199
  function readDefineTail(source: string, start: number, end: number): DefineTail | null {
102
200
  let cursor = end
103
201
  while (
@@ -112,6 +210,19 @@ function readDefineTail(source: string, start: number, end: number): DefineTail
112
210
  if (source[cursor] !== ')') return null
113
211
  cursor--
114
212
  while (cursor > start && /\s/.test(source[cursor])) cursor--
213
+ let verboseNameStart: number | undefined
214
+ let verboseNameEnd: number | undefined
215
+ if (source[cursor] === '"' || source[cursor] === "'") {
216
+ const literalStart = readStringLiteralStart(source, start, cursor)
217
+ if (literalStart < 0) return null
218
+ verboseNameStart = literalStart
219
+ verboseNameEnd = cursor + 1
220
+ cursor = literalStart - 1
221
+ while (cursor > start && /\s/.test(source[cursor])) cursor--
222
+ if (source[cursor] !== ',') return null
223
+ cursor--
224
+ while (cursor > start && /\s/.test(source[cursor])) cursor--
225
+ }
115
226
  if (source[cursor] !== ']') return null
116
227
  const dependenciesEnd = cursor + 1
117
228
  let depth = 1
@@ -147,6 +258,8 @@ function readDefineTail(source: string, start: number, end: number): DefineTail
147
258
  factoryEnd,
148
259
  dependenciesStart,
149
260
  dependenciesEnd,
261
+ verboseNameStart,
262
+ verboseNameEnd,
150
263
  }
151
264
  }
152
265
 
@@ -192,6 +305,9 @@ export function readRNXMetroBundleLayout(source: string): RNXMetroBundleLayout {
192
305
  endLineIndex,
193
306
  factory: { start: start + '__d('.length, end: tail.factoryEnd },
194
307
  dependencies: { start: tail.dependenciesStart, end: tail.dependenciesEnd },
308
+ ...(tail.verboseNameStart !== undefined && tail.verboseNameEnd !== undefined
309
+ ? { verboseName: { start: tail.verboseNameStart, end: tail.verboseNameEnd } }
310
+ : {}),
195
311
  })
196
312
  }
197
313
 
@@ -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