UNPKG

@expo/metro-config

Version:

A Metro config for running React Native projects with the Metro bundler

358 lines 14 kB
"use strict"; /** * Copyright © 2026 650 Industries. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.appendDebugIdToSourceMap = appendDebugIdToSourceMap; exports.sourceMapString = sourceMapString; exports.sourceMapStringNonBlocking = sourceMapStringNonBlocking; exports.patchMetroSourceMapStringForPackedMaps = patchMetroSourceMapStringForPackedMaps; exports.composeSourceMaps = composeSourceMaps; exports.rawMappingsToEncodedMap = rawMappingsToEncodedMap; const packedMap_1 = require("./packedMap"); let _remapping; function loadRemapping() { if (!_remapping) { _remapping = require('@jridgewell/remapping'); } return _remapping; } let _sourcemapCodec; function loadSourcemapCodec() { if (!_sourcemapCodec) { _sourcemapCodec = require('@jridgewell/sourcemap-codec'); } return _sourcemapCodec; } let _Generator; function loadGenerator() { if (!_Generator) { _Generator = require('@expo/metro/metro-source-map/Generator').default; } return _Generator; } let _isJsModule; function loadMetroSerializerHelpers() { if (!_isJsModule) { _isJsModule = require('@expo/metro/metro/DeltaBundler/Serializers/helpers/js.js').isJsModule; } return { isJsModule: _isJsModule }; } function feedModuleSegments(generator, tuples, carryOver) { for (let i = 0, n = tuples.length; i < n; i++) { const tuple = tuples[i]; const line = tuple[0] + carryOver; const column = tuple[1]; if (tuple.length === 2) { generator.addSimpleMapping(line, column); } else if (tuple.length === 4) { generator.addSourceMapping(line, column, tuple[2], tuple[3]); } else { generator.addNamedSourceMapping(line, column, tuple[2], tuple[3], tuple[4]); } } } // Encoder fast path: read the `Int32Array` directly, skipping the Proxy // and the tuple-per-segment allocation that Proxy iteration would incur. // Without this, encoding a large bundle would allocate millions of // transient tuples per pass — defeating the in-memory storage win. function feedModuleSegmentsPacked(generator, packed, carryOver) { const buf = packed.buf; const names = packed.names; const count = packed.count; for (let i = 0; i < count; i++) { const off = i * packedMap_1.STRIDE; const line = buf[off] + carryOver; const column = buf[off + 1]; const srcLine = buf[off + 2]; if (srcLine === packedMap_1.SENTINEL) { generator.addSimpleMapping(line, column); continue; } const srcCol = buf[off + 3]; const nameIdx = buf[off + 4]; if (nameIdx === packedMap_1.SENTINEL) { generator.addSourceMapping(line, column, srcLine, srcCol); } else { generator.addNamedSourceMapping(line, column, srcLine, srcCol, names[nameIdx]); } } } // Inlined rather than going through Metro's `getSourceMapInfo` because // it spreads `data` and drops the non-enumerable `__packedMap`, // silently forcing the encoder onto the slow Proxy path. function readSourceMapInfo(module, options) { const data = getModuleJsData(module); // Don't read `data.map` when `__packedMap` is set — `data.map` is a // lazy accessor that materializes the Proxy on first read, and the // fast path below doesn't need it. let packed = data.data.__packedMap; let map; if (!packed) { map = data.data.map; // Self-heal a `SerializableSourceMap` `data.map` that bypassed the wrapper. if ((0, packedMap_1.isSerializableSourceMap)(map)) { packed = packedMap_1.PackedMap.deserialize(map); map = undefined; } } return { path: options.getSourceUrl?.(module) ?? module.path, source: options.excludeSource || data.type === 'js/module/asset' ? '' : module.getSource().toString(), functionMap: data.data.functionMap, isIgnored: options.shouldAddToIgnoreList(module), map, packed, lineCount: data.data.lineCount, }; } function getModuleJsData(module) { for (const out of module.output) { const type = out.type; if (typeof type === 'string' && type.startsWith('js/')) { return out; } } throw new Error(`[expo-metro-config] Module "${module.path}" has no JS output. Cannot build a sourcemap entry.`); } function processModuleIntoGenerator(generator, module, options, carryOver) { const info = readSourceMapInfo(module, options); generator.startFile(info.path, info.source, info.functionMap ?? null, { addToIgnoreList: info.isIgnored, }); if (info.packed) { feedModuleSegmentsPacked(generator, info.packed, carryOver); } else if (Array.isArray(info.map)) { // Legacy plain-tuple path — hits for cache entries written before // `data.map` switched to `SerializableSourceMap`, and for modules from custom // transformers that don't emit packed format. feedModuleSegments(generator, info.map, carryOver); } generator.endFile(); return carryOver + info.lineCount; } function filterModules(modules, processModuleFilter) { const { isJsModule } = loadMetroSerializerHelpers(); const out = []; for (const m of modules) { if (isJsModule(m) && processModuleFilter(m)) { out.push(m); } } return out; } // Splice `,"debugId":"..."` in front of the trailing `}` of an already // JSON-encoded sourcemap. Used for Hermes output where we don't control // the JSON producer. function appendDebugIdToSourceMap(sourceMap, debugId) { return sourceMap.slice(0, -1) + `,"debugId":${JSON.stringify(debugId)}}`; } function emitGeneratorJson(generator, options) { const json = generator.toString(undefined, { excludeSource: options.excludeSource }); return options.debugId ? appendDebugIdToSourceMap(json, options.debugId) : json; } function sourceMapString(modules, options) { const Generator = loadGenerator(); const generator = new Generator(); const filtered = filterModules(modules, options.processModuleFilter); let carryOver = 0; for (const mod of filtered) { carryOver = processModuleIntoGenerator(generator, mod, options, carryOver); } return emitGeneratorJson(generator, options); } // Yields back to the event loop every ~50 ms so the node server stays // responsive on very large bundles. Matches Metro's `getSourceMapInfosImpl` // pacing. async function sourceMapStringNonBlocking(modules, options) { const Generator = loadGenerator(); const generator = new Generator(); const filtered = filterModules(modules, options.processModuleFilter); const NS_IN_MS = 1_000_000; const SLICE_NS = 50 * NS_IN_MS; let carryOver = 0; let i = 0; await new Promise((resolve) => { const tick = () => { const start = process.hrtime(); while (i < filtered.length) { carryOver = processModuleIntoGenerator(generator, filtered[i], options, carryOver); i++; const diff = process.hrtime(start); if (diff[1] > SLICE_NS || diff[0] > 0) { setImmediate(tick); return; } } resolve(); }; tick(); }); return emitGeneratorJson(generator, options); } // Metro's `Server._processSourceMapRequest` calls // `sourceMapStringNonBlocking` directly, bypassing the `customSerializer` // chain — so without rerouting it, every dev `.map` fetch iterates the // `data.map` Proxy and the encoder fast path is unreachable. function patchMetroSourceMapStringForPackedMaps() { const stock = require('@expo/metro/metro/DeltaBundler/Serializers/sourceMapString'); stock.sourceMapString = sourceMapString; stock.sourceMapStringNonBlocking = sourceMapStringNonBlocking; } function repairInvalidNegativeIndices(map) { const { decode, encode } = loadSourcemapCodec(); const decoded = decode(map.mappings); let changed = false; for (const line of decoded) { for (let i = 0; i < line.length; i++) { const segment = line[i]; // A `[genCol, srcIdx, srcLine, ...]` segment crashes `trace-mapping` // when either the source index or the source line is negative // (Hermes' no-location sentinel) if (segment != null && segment.length > 1 && (segment[1] < 0 || segment[2] < 0)) { line[i] = [segment[0]]; changed = true; } } } return changed ? { ...map, mappings: encode(decoded) } : map; } // `maps[0]` is the original-most transform; `maps[maps.length - 1]` is // the most recent. Built on `@jridgewell/remapping` instead of mozilla's // `SourceMapConsumer`-based composer. // // Two shims around remapping: // - Translate `x_google_ignoreList` to the standard `ignoreList` on // each input. Metro's `Generator` emits only the legacy alias, but // remapping's `TraceMap` reads only the standard field, so without // this ignore-list info disappears across compose. // - Carry over `x_hermes_function_offsets` (load-bearing for Hermes // bytecode-frame symbolication) and re-emit the legacy // `x_google_ignoreList` alias for devtools that haven't yet picked up // the rename. // // `x_facebook_sources` is deliberately dropped — Expo doesn't ship to FB // symbolicators. function composeSourceMaps(maps) { if (maps.length < 1) { throw new Error('composeSourceMaps: Expected at least one map'); } const normalized = maps.map((map) => { if (map.ignoreList || !map.x_google_ignoreList) { return map; } return { ...map, ignoreList: map.x_google_ignoreList }; }); // Metro convention is original-first; remapping is most-recent first. let input = normalized.slice().reverse(); const remap = loadRemapping(); let composed; try { composed = remap(input, () => null); } catch (error) { // `@jridgewell/trace-mapping` (through `@jridgewell/remapping`) crashes // on any mapping where original line/column is negative. Hermes may emit // such segments and we should gracefully recover from this on a crash. // See: // - https://github.com/jridgewell/sourcemaps/issues/54 // - https://github.com/expo/expo/issues/46843 let didRepair = false; try { input = input.map((map) => { const fixed = repairInvalidNegativeIndices(map); didRepair ||= fixed !== map; return fixed; }); } catch { throw error; } if (!didRepair) { throw error; } else { composed = remap(input, () => null); } } // Re-emit as a plain object — remapping returns a `SourceMap` class // instance, which doesn't round-trip JSON cleanly. const result = { version: composed.version, mappings: composed.mappings, names: composed.names, sources: composed.sources, }; if (composed.file != null) { result.file = composed.file; } if (composed.sourceRoot != null) { result.sourceRoot = composed.sourceRoot; } if (composed.sourcesContent) { result.sourcesContent = composed.sourcesContent; } // Required by Hermes for bytecode-frame symbolication. Lives on the // most-recent map (the Hermes map) and passes through unchanged. const last = maps[maps.length - 1]; if (last.x_hermes_function_offsets) { result.x_hermes_function_offsets = last.x_hermes_function_offsets; } // Emit both standard and legacy keys so devtools either side of the // rename keep working. if (composed.ignoreList && composed.ignoreList.length > 0) { result.ignoreList = composed.ignoreList; result.x_google_ignoreList = composed.ignoreList; } return result; } let _genMapping; function loadGenMapping() { if (!_genMapping) { _genMapping = require('@jridgewell/gen-mapping'); } return _genMapping; } // Convert Babel's `result.rawMappings` directly to an encoded sourcemap // for feeding into a minifier. `BabelSourceMapSegment` lines are // 1-based; `gen-mapping`'s `addSegment` is 0-based — subtract 1 at the // boundary. Skipping a tuple intermediate avoids one Array allocation // per segment. function rawMappingsToEncodedMap(opts) { const { GenMapping, addSegment, setSourceContent, toEncodedMap } = loadGenMapping(); const map = new GenMapping({ file: opts.filename }); setSourceContent(map, opts.filename, opts.source); for (const m of opts.rawMappings) { const genLine = m.generated.line - 1; const genCol = m.generated.column; if (m.original == null) { addSegment(map, genLine, genCol); } else if (typeof m.name !== 'string') { addSegment(map, genLine, genCol, opts.filename, m.original.line - 1, m.original.column); } else { addSegment(map, genLine, genCol, opts.filename, m.original.line - 1, m.original.column, m.name); } } const encoded = toEncodedMap(map); // gen-mapping always registers `opts.filename` as the single source, so // hard-coding avoids a `.map(...)` allocation and a `(string | null)[]` // narrowing. return { version: encoded.version, file: encoded.file ?? undefined, mappings: encoded.mappings, names: encoded.names, sources: [opts.filename], sourcesContent: encoded.sourcesContent, }; } //# sourceMappingURL=sourceMap.js.map