import fs from 'node:fs'; import { isDeepStrictEqual } from 'node:util'; import { createRequire } from 'node:module'; import path from 'node:path'; import type { AncestorSnapshot, AncestorSummary, ComponentAncestorClassification, ModuleAncestorAnalysis, SpreadKeys, } from '../ancestor-types'; const patchProperty = '__reactNativeBoostGraphPatch'; const requireFromGraph = createRequire(import.meta.url); type OutputMetadata = { injectionId: string; analysis: ModuleAncestorAnalysis }; type MetroDependency = { absolutePath?: string; data: { name: string } }; type MetroModule = { dependencies: Map; output: Array<{ data: { reactNativeBoost?: OutputMetadata } }>; unstable_transformResultKey?: string; }; type MetroDelta = { added: Map; deleted: Set; modified: Map }; type MetroGraph = { dependencies: Map; transformOptions?: { platform?: string }; }; type Traverse = (this: MetroGraph, paths: string[], options: unknown) => Promise; type InitialTraverse = (this: MetroGraph, options: unknown) => Promise; type ConsumerImports = Record>>; type ResolvedGraph = { consumers: ConsumerImports; spreadConsumers: Record>>; staleConsumers: string[]; }; type Adapter = { injectionId: string; queue: Promise; revision: number; snapshotPath: string; platforms: AncestorSnapshot['platforms']; spreadPlatforms: NonNullable; }; type Patch = { adapters: Adapter[]; graphs: WeakMap; initial: InitialTraverse; traverse: Traverse; }; type GraphConstructor = { prototype: { initialTraverseDependencies: InitialTraverse; traverseDependencies: Traverse }; [patchProperty]?: Patch; }; export function installMetroGraphPatch( graphPath: string, options: { injectionId: string; snapshotPath: string } ): void { const loaded = requireFromGraph(graphPath) as { Graph?: GraphConstructor; default?: GraphConstructor }; const Graph = loaded.Graph ?? loaded.default; if (!Graph?.prototype?.initialTraverseDependencies || !Graph.prototype.traverseDependencies) { throw new Error('[react-native-boost] This Metro Graph implementation is not supported.'); } let patch = Graph[patchProperty]; if (!patch) { patch = { adapters: [], graphs: new WeakMap(), initial: Graph.prototype.initialTraverseDependencies, traverse: Graph.prototype.traverseDependencies, }; Graph[patchProperty] = patch; Graph.prototype.initialTraverseDependencies = patchedInitialTraverse; Graph.prototype.traverseDependencies = patchedTraverse; } if (patch.adapters.some((adapter) => adapter.injectionId === options.injectionId)) return; patch.adapters.push({ ...options, queue: Promise.resolve(), revision: 0, platforms: {}, spreadPlatforms: {} }); async function patchedInitialTraverse(this: MetroGraph, transformOptions: unknown): Promise { const activePatch = Graph![patchProperty]!; const result = await activePatch.initial.call(this, transformOptions); await updateAncestors(activePatch, this, result, transformOptions); return { ...result, added: this.dependencies }; } async function patchedTraverse( this: MetroGraph, changedPaths: string[], transformOptions: unknown ): Promise { const activePatch = Graph![patchProperty]!; const result = await activePatch.traverse.call(this, changedPaths, transformOptions); return updateAncestors(activePatch, this, result, transformOptions); } } async function updateAncestors( patch: Patch, graph: MetroGraph, result: MetroDelta, transformOptions: unknown ): Promise { const adapter = patch.graphs.get(graph) ?? findAdapter(patch.adapters, graph); if (!adapter) return result; patch.graphs.set(graph, adapter); return runExclusive(adapter, async () => { while (true) { const { consumers, spreadConsumers, staleConsumers } = resolveGraph(graph, adapter.injectionId); writeSnapshot(adapter, graph.transformOptions?.platform, consumers, spreadConsumers); if (staleConsumers.length === 0) return result; markTransformResultsStale(graph, staleConsumers, adapter.revision); const next = await patch.traverse.call(graph, staleConsumers, transformOptions); result = mergeDeltas(graph, result, next); } }); } export function resolveGraph(graph: MetroGraph, injectionId: string): ResolvedGraph { const consumers: ConsumerImports = {}; const spreadConsumers: ResolvedGraph['spreadConsumers'] = {}; const staleConsumers: string[] = []; for (const [consumerPath, module] of graph.dependencies) { const analysis = getAnalysis(module, injectionId); if (!analysis || (analysis.references.length === 0 && !analysis.spreadReferences?.length)) continue; const imports = Object.create(null) as ConsumerImports[string]; let stale = false; for (const reference of analysis.references) { const classification = resolveImport( graph, consumerPath, reference.source, reference.imported, injectionId, new Set() ); (imports[reference.source] ??= {})[reference.imported] = classification; if (classification !== reference.classification) stale = true; } consumers[consumerPath] = imports; for (const reference of analysis.spreadReferences ?? []) { const target = findDependency(module, reference.source, injectionId); const keys = target ? resolveSpreadExport(graph, target, reference.imported, injectionId, new Set()).keys : null; ((spreadConsumers[consumerPath] ??= Object.create(null))[reference.source] ??= Object.create(null))[ reference.imported ] = keys; if (!isDeepStrictEqual(keys, reference.keys)) stale = true; } if (stale) staleConsumers.push(consumerPath); } return { consumers, spreadConsumers, staleConsumers }; } function resolveImport( graph: MetroGraph, modulePath: string, source: string, imported: string, injectionId: string, visiting: Set ): ComponentAncestorClassification { const targetPath = findDependency(graph.dependencies.get(modulePath), source, injectionId); return targetPath ? resolveExport(graph, targetPath, imported, injectionId, visiting).value : 'unknown'; } function resolveExport( graph: MetroGraph, modulePath: string, exported: string, injectionId: string, visiting: Set ): { found: boolean; value: ComponentAncestorClassification } { const key = `${modulePath}\0${exported}`; if (visiting.has(key)) return { found: true, value: 'unknown' }; const analysis = getAnalysis(graph.dependencies.get(modulePath), injectionId); if (!analysis) return { found: false, value: 'unknown' }; visiting.add(key); const summary = analysis.exports[exported]; if (summary) { const value = evaluateSummary(graph, modulePath, summary, injectionId, visiting); visiting.delete(key); return { found: true, value }; } if (exported === 'default') { visiting.delete(key); return { found: false, value: 'unknown' }; } const matches = analysis.exportAll .map((source) => { const targetPath = findDependency(graph.dependencies.get(modulePath), source, injectionId); return targetPath ? resolveExport(graph, targetPath, exported, injectionId, visiting) : { found: false, value: 'unknown' as const }; }) .filter((result) => result.found); visiting.delete(key); return matches.length === 1 ? matches[0]! : { found: matches.length > 0, value: 'unknown' }; } function evaluateSummary( graph: MetroGraph, modulePath: string, summary: AncestorSummary, injectionId: string, visiting: Set ): ComponentAncestorClassification { if (typeof summary === 'string') return summary; if (summary.kind === 'import') { return resolveImport(graph, modulePath, summary.source, summary.imported, injectionId, visiting); } const values = summary.values.map((value) => evaluateSummary(graph, modulePath, value, injectionId, visiting)); if (summary.kind === 'ancestors') return values.find((value) => value !== 'transparent') ?? 'transparent'; if (values.includes('unknown')) return 'unknown'; return values.every((value) => value === values[0]) ? values[0]! : 'context'; } function resolveSpreadExport( graph: MetroGraph, modulePath: string, exported: string, injectionId: string, visiting: Set ): { found: boolean; keys: SpreadKeys } { const key = `${modulePath}\0${exported}`; if (visiting.has(key)) return { found: true, keys: null }; const module = graph.dependencies.get(modulePath); const analysis = getAnalysis(module, injectionId); if (!analysis) return { found: false, keys: null }; visiting.add(key); const follow = (source: string, imported: string) => { const target = findDependency(module, source, injectionId); return target ? resolveSpreadExport(graph, target, imported, injectionId, visiting) : { found: false, keys: null }; }; const summary = analysis.spreadExports?.[exported]; let result: { found: boolean; keys: SpreadKeys }; if (summary === undefined) { const matches = exported === 'default' ? [] : analysis.exportAll.map((source) => follow(source, exported)).filter((entry) => entry.found); result = matches.length === 1 ? matches[0]! : { found: matches.length > 0, keys: null }; } else { result = { found: true, keys: summary === null || Array.isArray(summary) ? summary : follow(summary.source, summary.imported).keys, }; } visiting.delete(key); return result; } function findDependency(module: MetroModule | undefined, source: string, injectionId: string): string | undefined { source = getAnalysis(module, injectionId)?.sources?.[source] ?? source; let resolved: string | undefined; for (const dependency of module?.dependencies.values() ?? []) { if (dependency.data.name !== source || !dependency.absolutePath) continue; if (resolved && resolved !== dependency.absolutePath) return; resolved = dependency.absolutePath; } return resolved; } function getAnalysis(module: MetroModule | undefined, injectionId: string): ModuleAncestorAnalysis | undefined { for (const output of module?.output ?? []) { const metadata = output.data.reactNativeBoost; if (metadata?.injectionId === injectionId) return metadata.analysis; } } function findAdapter(adapters: Adapter[], graph: MetroGraph): Adapter | undefined { return adapters.find((adapter) => [...graph.dependencies.values()].some((module) => getAnalysis(module, adapter.injectionId)) ); } // ponytail: serialize passes per project; use graph-specific worker inputs if concurrent builds become slow. function runExclusive(adapter: Adapter, operation: () => Promise): Promise { const result = adapter.queue.then(operation); adapter.queue = result.then( () => {}, () => {} ); return result; } function writeSnapshot( adapter: Adapter, platform: string | undefined, consumers: ConsumerImports, spreads: ResolvedGraph['spreadConsumers'] ): void { adapter.platforms[platform ?? ''] = consumers; adapter.spreadPlatforms[platform ?? ''] = spreads; const snapshot: AncestorSnapshot = { version: 1, revision: ++adapter.revision, platforms: adapter.platforms, spreadPlatforms: adapter.spreadPlatforms, }; const temporaryPath = `${adapter.snapshotPath}.${process.pid}.${adapter.revision}`; fs.mkdirSync(path.dirname(adapter.snapshotPath), { recursive: true }); fs.writeFileSync(temporaryPath, JSON.stringify(snapshot)); fs.renameSync(temporaryPath, adapter.snapshotPath); } function markTransformResultsStale(graph: MetroGraph, consumers: string[], revision: number): void { for (const consumer of consumers) { const module = graph.dependencies.get(consumer); if (module) module.unstable_transformResultKey = `${module.unstable_transformResultKey ?? ''}.boost.${revision}`; } } function mergeDeltas(graph: MetroGraph, first: MetroDelta, second: MetroDelta): MetroDelta { const added = new Map(first.added); const modified = new Map(first.modified); const deleted = new Set([...first.deleted, ...second.deleted]); for (const path of second.added.keys()) { added.set(path, graph.dependencies.get(path)!); modified.delete(path); deleted.delete(path); } for (const path of second.modified.keys()) { const module = graph.dependencies.get(path)!; if (added.has(path)) added.set(path, module); else modified.set(path, module); deleted.delete(path); } for (const path of deleted) { added.delete(path); modified.delete(path); } return { added, deleted, modified }; }