metro-source-map
Version:
🚇 Source map generator for Metro.
373 lines (336 loc) • 9.57 kB
Flow
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {IConsumer} from './Consumer/types';
import {BundleBuilder, createIndexMap} from './BundleBuilder';
import composeSourceMaps from './composeSourceMaps';
import Consumer from './Consumer';
// We need to export this for `metro-symbolicate`
import normalizeSourcePath from './Consumer/normalizeSourcePath';
import {
functionMapBabelPlugin,
generateFunctionMap,
} from './generateFunctionMap';
import Generator from './Generator';
// $FlowFixMe[untyped-import] - source-map
import SourceMap from 'source-map';
export type {IConsumer};
type GeneratedCodeMapping = [number, number];
type SourceMapping = [number, number, number, number];
type SourceMappingWithName = [number, number, number, number, string];
export type MetroSourceMapSegmentTuple =
| SourceMappingWithName
| SourceMapping
| GeneratedCodeMapping;
export type HermesFunctionOffsets = {[number]: $ReadOnlyArray<number>, ...};
export type FBSourcesArray = $ReadOnlyArray<?FBSourceMetadata>;
export type FBSourceMetadata = [?FBSourceFunctionMap];
export type FBSourceFunctionMap = {
+names: $ReadOnlyArray<string>,
+mappings: string,
};
export type BabelSourceMapSegment = $ReadOnly<{
generated: $ReadOnly<{column: number, line: number, ...}>,
original?: $ReadOnly<{column: number, line: number, ...}>,
source?: ?string,
name?: ?string,
...
}>;
export type FBSegmentMap = {[id: string]: MixedSourceMap, ...};
export type BasicSourceMap = {
+file?: string,
+mappings: string,
+names: Array<string>,
+sourceRoot?: string,
+sources: Array<string>,
+sourcesContent?: Array<?string>,
+version: number,
+x_facebook_offsets?: Array<number>,
+x_metro_module_paths?: Array<string>,
+x_facebook_sources?: FBSourcesArray,
+x_facebook_segments?: FBSegmentMap,
+x_hermes_function_offsets?: HermesFunctionOffsets,
+x_google_ignoreList?: Array<number>,
};
export type IndexMapSection = {
map: IndexMap | BasicSourceMap,
offset: {
line: number,
column: number,
...
},
...
};
export type IndexMap = {
+file?: string,
+mappings?: void, // avoids SourceMap being a disjoint union
+sourcesContent?: void,
+sections: Array<IndexMapSection>,
+version: number,
+x_facebook_offsets?: Array<number>,
+x_metro_module_paths?: Array<string>,
+x_facebook_sources?: void,
+x_facebook_segments?: FBSegmentMap,
+x_hermes_function_offsets?: HermesFunctionOffsets,
+x_google_ignoreList?: void,
};
export type MixedSourceMap = IndexMap | BasicSourceMap;
type SourceMapConsumerMapping = {
generatedLine: number,
generatedColumn: number,
originalLine: ?number,
originalColumn: ?number,
source: ?string,
name: ?string,
};
function fromRawMappingsImpl(
isBlocking: boolean,
onDone: Generator => void,
modules: $ReadOnlyArray<{
+map: ?$ReadOnlyArray<MetroSourceMapSegmentTuple>,
+functionMap: ?FBSourceFunctionMap,
+path: string,
+source: string,
+code: string,
+isIgnored: boolean,
+lineCount?: number,
}>,
offsetLines: number,
): void {
const modulesToProcess = modules.slice();
const generator = new Generator();
let carryOver = offsetLines;
function processNextModule() {
if (modulesToProcess.length === 0) {
return true;
}
const mod = modulesToProcess.shift();
// $FlowFixMe[incompatible-use]
const {code, map} = mod;
if (Array.isArray(map)) {
// $FlowFixMe[incompatible-type]
addMappingsForFile(generator, map, mod, carryOver);
} else if (map != null) {
throw new Error(
// $FlowFixMe[incompatible-use]
`Unexpected module with full source map found: ${mod.path}`,
);
}
carryOver = carryOver + countLines(code);
return false;
}
function workLoop() {
const time = process.hrtime();
while (true) {
const isDone = processNextModule();
if (isDone) {
onDone(generator);
break;
}
if (!isBlocking) {
// Keep the loop running but try to avoid blocking
// for too long because this is not in a worker yet.
const diff = process.hrtime(time);
const NS_IN_MS = 1000000;
if (diff[1] > 50 * NS_IN_MS) {
// We've blocked for more than 50ms.
// This code currently runs on the main thread,
// so let's give Metro an opportunity to handle requests.
setImmediate(workLoop);
break;
}
}
}
}
workLoop();
}
/**
* Creates a source map from modules with "raw mappings", i.e. an array of
* tuples with either 2, 4, or 5 elements:
* generated line, generated column, source line, source line, symbol name.
* Accepts an `offsetLines` argument in case modules' code is to be offset in
* the resulting bundle, e.g. by some prefix code.
*/
function fromRawMappings(
modules: $ReadOnlyArray<{
+map: ?$ReadOnlyArray<MetroSourceMapSegmentTuple>,
+functionMap: ?FBSourceFunctionMap,
+path: string,
+source: string,
+code: string,
+isIgnored: boolean,
+lineCount?: number,
}>,
offsetLines: number = 0,
): Generator {
let generator: void | Generator;
fromRawMappingsImpl(
true,
g => {
generator = g;
},
modules,
offsetLines,
);
if (generator == null) {
throw new Error('Expected fromRawMappingsImpl() to finish synchronously.');
}
return generator;
}
async function fromRawMappingsNonBlocking(
modules: $ReadOnlyArray<{
+map: ?$ReadOnlyArray<MetroSourceMapSegmentTuple>,
+functionMap: ?FBSourceFunctionMap,
+path: string,
+source: string,
+code: string,
+isIgnored: boolean,
+lineCount?: number,
}>,
offsetLines: number = 0,
): Promise<Generator> {
return new Promise(resolve => {
fromRawMappingsImpl(false, resolve, modules, offsetLines);
});
}
/**
* Transforms a standard source map object into a Raw Mappings object, to be
* used across the bundler.
*/
function toBabelSegments(
sourceMap: BasicSourceMap,
): Array<BabelSourceMapSegment> {
const rawMappings: Array<BabelSourceMapSegment> = [];
new SourceMap.SourceMapConsumer(sourceMap).eachMapping(
(map: SourceMapConsumerMapping) => {
rawMappings.push(
map.originalLine == null || map.originalColumn == null
? {
generated: {
line: map.generatedLine,
column: map.generatedColumn,
},
source: map.source,
name: map.name,
}
: {
generated: {
line: map.generatedLine,
column: map.generatedColumn,
},
original: {
line: map.originalLine,
column: map.originalColumn,
},
source: map.source,
name: map.name,
},
);
},
);
return rawMappings;
}
function toSegmentTuple(
mapping: BabelSourceMapSegment,
): MetroSourceMapSegmentTuple {
const {column, line} = mapping.generated;
const {name, original} = mapping;
if (original == null) {
return [line, column];
}
if (typeof name !== 'string') {
return [line, column, original.line, original.column];
}
return [line, column, original.line, original.column, name];
}
function addMappingsForFile(
generator: Generator,
mappings: Array<MetroSourceMapSegmentTuple>,
module: {
+code: string,
+functionMap: ?FBSourceFunctionMap,
+map: ?Array<MetroSourceMapSegmentTuple>,
+path: string,
+source: string,
+isIgnored: boolean,
+lineCount?: number,
},
carryOver: number,
) {
generator.startFile(module.path, module.source, module.functionMap, {
addToIgnoreList: module.isIgnored,
});
for (let i = 0, n = mappings.length; i < n; ++i) {
addMapping(generator, mappings[i], carryOver);
}
generator.endFile();
}
function addMapping(
generator: Generator,
mapping: MetroSourceMapSegmentTuple,
carryOver: number,
) {
const line = mapping[0] + carryOver;
// lines start at 1, columns start at 0
const column = mapping[1];
switch (mapping.length) {
case 2:
generator.addSimpleMapping(line, column);
return;
case 4:
generator.addSourceMapping(line, column, mapping[2], mapping[3]);
return;
case 5:
generator.addNamedSourceMapping(
line,
column,
mapping[2],
mapping[3],
mapping[4],
);
return;
}
throw new Error(`Invalid mapping: [${mapping.join(', ')}]`);
}
const newline = /\r\n?|\n|\u2028|\u2029/g;
const countLines = (string: string): number =>
(string.match(newline) || []).length + 1;
export {
BundleBuilder,
composeSourceMaps,
Consumer,
createIndexMap,
generateFunctionMap,
fromRawMappings,
fromRawMappingsNonBlocking,
functionMapBabelPlugin,
normalizeSourcePath,
toBabelSegments,
toSegmentTuple,
};
/**
* Backwards-compatibility with CommonJS consumers using interopRequireDefault.
* Do not add to this list.
*
* @deprecated Default import from 'metro-source-map' is deprecated, use named exports.
*/
export default {
BundleBuilder,
composeSourceMaps,
Consumer,
createIndexMap,
generateFunctionMap,
fromRawMappings,
fromRawMappingsNonBlocking,
functionMapBabelPlugin,
normalizeSourcePath,
toBabelSegments,
toSegmentTuple,
};