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.
- package/README.md +31 -9
- package/cli/commands/box.ts +10 -3
- package/cli/outbound-endpoints.ts +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +5 -2
- package/dist-lib/capture-contract.mjs +5 -2
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +3 -2
- package/dist-lib/cloud-contract.mjs +3 -2
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +63 -14
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +131 -17
- package/dist-lib/metro-production-bundle.mjs +125 -16
- package/dist-lib/metro.cjs +67 -12
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +58 -25
- package/dist-lib/vite.cjs +6 -1
- package/package.json +8 -1
- package/src/bridge-contract.ts +20 -0
- package/src/capture-contract.ts +8 -0
- package/src/cloud-contract.ts +1 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +73 -24
- package/src/metro-production-bundle.ts +213 -19
- 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
|
|
5
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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(
|
|
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 =
|
|
37
|
-
if (footerStart < 0
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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(
|
|
48
|
-
if (typeof modulePath !== 'string')
|
|
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
|
-
|
|
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,...])
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
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
|
|
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
|
+
}
|
package/src/runtime-assets.ts
CHANGED
|
@@ -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
|