const RNXSIM_METRO_MODULE_PATHS_PREFIX = 'globalThis.__sootsimModulePaths=' export const RNX_METRO_MODULE_IDENTITY_VERSION = 2 export type RNXMetroModulePathMap = Record export type RNXMetroLogicalSpecifierMap = Record export type RNXMetroModuleIdentitySource = | 'rnx-plugin' | 'contrast-bundler' | 'metro-source-map' export interface RNXMetroModuleIdentityMetadata { version: typeof RNX_METRO_MODULE_IDENTITY_VERSION identitySource: RNXMetroModuleIdentitySource modulePaths: RNXMetroModulePathMap logicalSpecifiers: RNXMetroLogicalSpecifierMap } export interface RNXMetroModuleIdentity extends RNXMetroModuleIdentityMetadata { source: string /** * Metro's trailing `//# sourceMappingURL=` comment when the graph carried * one, already split off `source`. A production graph has none. */ sourceMappingUrl: string | null } // the production bundle rnx loads is the app's ORDINARY production bundle. // nothing about resolution changes, so these are plain Metro production // options and the artifact is byte-identical to what the app ships apart // from the identity footer the plugin's serializer appends. export function toRNXProductionBundleUrl(bundleUrl: string): string { const relative = bundleUrl.startsWith('/') const url = new URL(bundleUrl, 'http://rnxsim.local') url.searchParams.set('dev', 'false') url.searchParams.set('hot', 'false') url.searchParams.set('minify', 'true') return relative ? `${url.pathname}${url.search}${url.hash}` : url.toString() } /** * the ordinary development counterpart. Metro keeps `__DEV__` true, verbose * module names, and its source map. `hot` stays false because the artifact is * immutable once produced. */ export function toRNXDevelopmentBundleUrl(bundleUrl: string): string { const relative = bundleUrl.startsWith('/') const url = new URL(bundleUrl, 'http://rnxsim.local') url.searchParams.set('dev', 'true') url.searchParams.set('hot', 'false') url.searchParams.set('minify', 'false') return relative ? `${url.pathname}${url.search}${url.hash}` : url.toString() } const RNXSIM_METRO_SOURCE_MAP_COMMENT = '//# sourceMappingURL=' export interface RNXMetroDetachedSourceMapUrl { source: string /** the comment's URL, which is a whole inline map when Metro inlined it. */ sourceMappingUrl: string | null } /** * splits Metro's trailing `//# sourceMappingURL=` comment off the bundle. * * a development bundle ends with that comment, and Metro inlines the entire * map into it as a data URL whenever the caller did not ask for an external * map file. leaving it attached makes the last `__r(` statement carry the map * as source text: the layout reader would hand megabytes of base64 to the * minifier, and the artifact the runner loads would keep the map resident in * child memory for the life of the simulator. */ export function detachRNXMetroSourceMapUrl( bundleText: string, ): RNXMetroDetachedSourceMapUrl { const trimmedEnd = bundleText.replace(/\s+$/, '') const commentStart = trimmedEnd.lastIndexOf(`\n${RNXSIM_METRO_SOURCE_MAP_COMMENT}`) if (commentStart < 0) return { source: bundleText, sourceMappingUrl: null } const url = trimmedEnd.slice(commentStart + 1 + RNXSIM_METRO_SOURCE_MAP_COMMENT.length) if (url.includes('\n')) return { source: bundleText, sourceMappingUrl: null } return { source: trimmedEnd.slice(0, commentStart), sourceMappingUrl: url } } export function createRNXMetroModuleIdentityFooter( identity: RNXMetroModuleIdentityMetadata, ): string { const payload = JSON.stringify(identity) .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029') .replace(/<\//g, '<\\/') return `\n${RNXSIM_METRO_MODULE_PATHS_PREFIX}${payload};` } /** * writes the identity footer into a serialized bundle, keeping Metro's * `//# sourceMappingURL=` comment on the last line when the graph has one. a * production graph has no such comment and gets the footer appended, exactly * as before. */ export function appendRNXMetroModuleIdentityFooter( bundleText: string, identity: RNXMetroModuleIdentityMetadata, ): string { const footer = createRNXMetroModuleIdentityFooter(identity) const detached = detachRNXMetroSourceMapUrl(bundleText) return detached.sourceMappingUrl === null ? bundleText + footer : `${detached.source}${footer}\n${RNXSIM_METRO_SOURCE_MAP_COMMENT}${detached.sourceMappingUrl}` } export function readRNXMetroModuleIdentity( bundleText: string, ): RNXMetroModuleIdentity | null { const detached = detachRNXMetroSourceMapUrl(bundleText) const withoutMap = detached.source const footerPrefix = `\n${RNXSIM_METRO_MODULE_PATHS_PREFIX}` const footerStart = withoutMap.lastIndexOf(footerPrefix) if (footerStart < 0) return null if (!withoutMap.endsWith(';')) { throw new Error('Metro module identity footer is truncated') } let parsed: unknown try { parsed = JSON.parse(withoutMap.slice(footerStart + footerPrefix.length, -1)) } catch { throw new Error('Metro module identity footer is not valid JSON') } if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { throw new Error('Metro module identity footer is malformed') } const version = Reflect.get(parsed, 'version') // stored artifacts from before the versioned footer carry a bare // `{"":"", ...}` map with no wrapper — the exact shape // appendRNXModulePaths wrote before the footer was versioned, and the only // writer of that shape used identitySource 'rnx-plugin'. normalize it into // the current in-memory identity so callers get the same exact modulePaths // map instead of falling back to name-scanning or fingerprint inference. // validateRNXMetroModuleIdentity still detects a missing, extra, duplicate, // or truncated module ID against the bundle's own __d() defines, so those // legacy-map corruptions are caught there, not trusted here — it does not // corroborate that a path or factory is itself correct. if (version === undefined) { const modulePaths: RNXMetroModulePathMap = Object.fromEntries( Object.entries(parsed).map(([moduleId, modulePath]) => { if (typeof modulePath !== 'string') { throw new Error(`Metro module identity has an invalid path for ${moduleId}`) } return [moduleId, modulePath] }), ) return { source: withoutMap.slice(0, footerStart), version: RNX_METRO_MODULE_IDENTITY_VERSION, identitySource: 'rnx-plugin', modulePaths, logicalSpecifiers: {}, sourceMappingUrl: detached.sourceMappingUrl, } } const identitySource = Reflect.get(parsed, 'identitySource') const parsedModulePaths = Reflect.get(parsed, 'modulePaths') const parsedLogicalSpecifiers = Reflect.get(parsed, 'logicalSpecifiers') if ( version !== RNX_METRO_MODULE_IDENTITY_VERSION || (identitySource !== 'rnx-plugin' && identitySource !== 'contrast-bundler' && identitySource !== 'metro-source-map') || typeof parsedModulePaths !== 'object' || parsedModulePaths === null || Array.isArray(parsedModulePaths) || typeof parsedLogicalSpecifiers !== 'object' || parsedLogicalSpecifiers === null || Array.isArray(parsedLogicalSpecifiers) ) { throw new Error('Metro module identity footer is malformed') } const modulePaths: RNXMetroModulePathMap = Object.fromEntries( Object.entries(parsedModulePaths).map(([moduleId, modulePath]) => { if (typeof modulePath !== 'string') { throw new Error(`Metro module identity has an invalid path for ${moduleId}`) } return [moduleId, modulePath] }), ) const logicalSpecifiers: RNXMetroLogicalSpecifierMap = Object.fromEntries( Object.entries(parsedLogicalSpecifiers).map(([moduleId, specifiers]) => { if ( !Array.isArray(specifiers) || specifiers.length === 0 || !specifiers.every( (specifier) => typeof specifier === 'string' && specifier.length > 0, ) ) { throw new Error(`Metro module identity has invalid specifiers for ${moduleId}`) } const unique = [...new Set(specifiers)].sort((left, right) => left.localeCompare(right), ) if (unique.length !== specifiers.length) { throw new Error(`Metro module identity repeats a specifier for ${moduleId}`) } return [moduleId, unique] }), ) return { source: withoutMap.slice(0, footerStart), version, identitySource, modulePaths, logicalSpecifiers, sourceMappingUrl: detached.sourceMappingUrl, } } // the exact layout Metro's baseJSBundle emits and the rnx runtime contract // depends on: a prelude, then one `__d(` or `__r(` statement per line, each // running until the next statement starts its own line. one reader owns this // recognition so the loader's identity validation, the source-map attribution // pass, and the cloud artifact producer cannot drift apart. export interface RNXMetroDefine { kind: 'define' moduleId: number start: number end: number lineIndex: number endLineIndex: number /** the factory expression, between `__d(` and the module ID. */ factory: { start: number; end: number } /** the dependency array, brackets included. */ dependencies: { start: number; end: number } /** * the module's verbose name, quotes included, when the graph carries one. * Metro emits it as a fourth `__d` argument on a development graph and omits * it entirely on a production one. */ verboseName?: { start: number; end: number } } export interface RNXMetroRun { kind: 'run' start: number end: number lineIndex: number endLineIndex: number } export type RNXMetroStatement = RNXMetroDefine | RNXMetroRun export interface RNXMetroBundleLayout { /** everything before the first `__d(`/`__r(` line: prelude vars and polyfills. */ prelude: string statements: RNXMetroStatement[] /** `__d(` statements whose module ID could not be read. */ moduleIdFailures: number } interface DefineTail { moduleId: number factoryEnd: number dependenciesStart: number dependenciesEnd: number verboseNameStart?: number verboseNameEnd?: number } // walks back over a single-quoted or double-quoted string literal ending at // `end`, returning the index of its opening quote. an odd run of backslashes // before a quote escapes it, so count them before accepting a candidate. function readStringLiteralStart(source: string, start: number, end: number): number { const quote = source[end] if (quote !== '"' && quote !== "'") return -1 for (let index = end - 1; index > start; index--) { if (source[index] !== quote) continue let backslashes = 0 while (index - 1 - backslashes > start && source[index - 1 - backslashes] === '\\') { backslashes++ } if (backslashes % 2 === 0) return index } return -1 } // each `__d(...)` ends with `),,[dep,dep,...]);?`, and a development graph // adds a fourth verbose-name string after the dependency array. the dependency // array can be hundreds of entries long, so walk back from `end` instead of // searching a fixed tail window: skip trailing `;` and whitespace, consume the // closing `)`, take the verbose name when one is there, balance `[...]` to find // its opening bracket, then read the integer before it. function readDefineTail(source: string, start: number, end: number): DefineTail | null { let cursor = end while ( cursor > start && (source[cursor] === ' ' || source[cursor] === '\n' || source[cursor] === '\t' || source[cursor] === ';') ) { cursor-- } if (source[cursor] !== ')') return null cursor-- while (cursor > start && /\s/.test(source[cursor])) cursor-- let verboseNameStart: number | undefined let verboseNameEnd: number | undefined if (source[cursor] === '"' || source[cursor] === "'") { const literalStart = readStringLiteralStart(source, start, cursor) if (literalStart < 0) return null verboseNameStart = literalStart verboseNameEnd = cursor + 1 cursor = literalStart - 1 while (cursor > start && /\s/.test(source[cursor])) cursor-- if (source[cursor] !== ',') return null cursor-- while (cursor > start && /\s/.test(source[cursor])) cursor-- } if (source[cursor] !== ']') return null const dependenciesEnd = cursor + 1 let depth = 1 cursor-- while (cursor > start && depth > 0) { const character = source[cursor] if (character === ']') depth++ else if (character === '[') depth-- if (depth === 0) break cursor-- } if (depth !== 0) return null const dependenciesStart = cursor let idEnd = cursor - 1 while (idEnd > start && /\s/.test(source[idEnd])) idEnd-- if (source[idEnd] !== ',') return null // a minifier is free to write the module ID as `1e3` or `0x3e8`, so read the // whole numeric literal rather than a run of decimal digits. the factory // before it always ends in `}` or `)`, which stops this walk immediately. let idStart = idEnd while (idStart > start && /[0-9a-fA-FxX.+-]/.test(source[idStart - 1])) idStart-- const literal = source.slice(idStart, idEnd) if (!/^-?(?:0[xXbBoO][0-9a-fA-F]+|\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)$/.test(literal)) { return null } const moduleId = Number(literal) if (!Number.isSafeInteger(moduleId)) return null let factoryEnd = idStart - 1 while (factoryEnd > start && /\s/.test(source[factoryEnd])) factoryEnd-- if (source[factoryEnd] !== ',') return null return { moduleId, factoryEnd, dependenciesStart, dependenciesEnd, verboseNameStart, verboseNameEnd, } } /** reads the canonical Metro production layout from footer-stripped source. */ export function readRNXMetroBundleLayout(source: string): RNXMetroBundleLayout { const lineOffsets = [0] for (let index = 0; index < source.length; index++) { if (source.charCodeAt(index) === 10) lineOffsets.push(index + 1) } const boundaries: number[] = [] for (let lineIndex = 0; lineIndex < lineOffsets.length; lineIndex++) { const start = lineOffsets[lineIndex] if (source.startsWith('__d(', start) || source.startsWith('__r(', start)) { boundaries.push(lineIndex) } } const statements: RNXMetroStatement[] = [] let moduleIdFailures = 0 for (let index = 0; index < boundaries.length; index++) { const lineIndex = boundaries[index] const start = lineOffsets[lineIndex] const nextLineIndex = boundaries[index + 1] const endLineIndex = nextLineIndex !== undefined ? nextLineIndex - 1 : lineOffsets.length - 1 const end = nextLineIndex !== undefined ? lineOffsets[nextLineIndex] : source.length if (!source.startsWith('__d(', start)) { statements.push({ kind: 'run', start, end, lineIndex, endLineIndex }) continue } const tail = readDefineTail(source, start, end - 1) if (!tail) { moduleIdFailures++ continue } statements.push({ kind: 'define', moduleId: tail.moduleId, start, end, lineIndex, endLineIndex, factory: { start: start + '__d('.length, end: tail.factoryEnd }, dependencies: { start: tail.dependenciesStart, end: tail.dependenciesEnd }, ...(tail.verboseNameStart !== undefined && tail.verboseNameEnd !== undefined ? { verboseName: { start: tail.verboseNameStart, end: tail.verboseNameEnd } } : {}), }) } return { prelude: source.slice( 0, boundaries.length > 0 ? lineOffsets[boundaries[0]] : source.length, ), statements, moduleIdFailures, } } /** * validates the exact identity contract before a loader installs a Metro * factory trap or a producer ships an artifact: every define carries a unique, * readable module ID, and the identity map names those IDs and no others. * serializers that emit another layout need an ingestion adapter that * normalizes them to this format. */ export function validateRNXMetroModulePaths( source: string, modulePaths: RNXMetroModulePathMap, ): void { const layout = readRNXMetroBundleLayout(source) if (layout.moduleIdFailures > 0) { throw new Error( `Metro module identity could not read ${layout.moduleIdFailures} module IDs`, ) } const definedIds = new Set() for (const statement of layout.statements) { if (statement.kind !== 'define') continue if (definedIds.has(statement.moduleId)) { throw new Error(`Metro module identity has duplicate module ${statement.moduleId}`) } definedIds.add(statement.moduleId) if (!Object.prototype.hasOwnProperty.call(modulePaths, String(statement.moduleId))) { throw new Error(`Metro module identity is missing ${statement.moduleId}`) } } if (definedIds.size === 0) { throw new Error('Metro module identity found no canonical JavaScript modules') } for (const [key, modulePath] of Object.entries(modulePaths)) { const moduleId = Number(key) if (!Number.isSafeInteger(moduleId) || String(moduleId) !== key) { throw new Error(`Metro module identity has invalid module ID ${key}`) } if (typeof modulePath !== 'string' || modulePath.length === 0) { throw new Error(`Metro module identity has no path for ${key}`) } if (!definedIds.has(moduleId)) { throw new Error(`Metro module identity has no module ${key}`) } } } export function validateRNXMetroModuleIdentity(identity: RNXMetroModuleIdentity): void { validateRNXMetroModulePaths(identity.source, identity.modulePaths) for (const moduleId of Object.keys(identity.logicalSpecifiers)) { if (!Object.prototype.hasOwnProperty.call(identity.modulePaths, moduleId)) { throw new Error( `Metro module identity has logical specifiers for unknown module ${moduleId}`, ) } } }