Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/metro-config/src/defaults/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({
customSerializer: null,
isThirdPartyModule: module =>
/(?:^|[/\\])node_modules[/\\]/.test(module.path),
unstable_allowIndexMap: false,
},

server: {
Expand Down
7 changes: 7 additions & 0 deletions packages/metro-config/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ type SerializerConfigT = {
polyfillModuleNames: ReadonlyArray<string>,
processModuleFilter: (modules: Module<>) => boolean,
isThirdPartyModule: (module: Readonly<{path: string, ...}>) => boolean,
// When source maps are stored compactly as VLQ (see
// `transformer.unstable_compactSourceMaps`), allow the whole-bundle map to be
// emitted as an index map (sectioned) that passes the VLQ through verbatim,
// instead of decoding + re-encoding into a flat map. Cheaper to serialize, but
// requires consumers that understand index source maps. No-op unless compact
// VLQ maps are actually present, and ignored when a `customSerializer` is set.
unstable_allowIndexMap: boolean,
};

type TransformerConfigT = {
Expand Down
3 changes: 2 additions & 1 deletion packages/metro-config/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
* @noformat
* @oncall react_native
* @generated SignedSource<<9c62bc2ca711f9693edc135a382a382a>>
* @generated SignedSource<<926fc453e7c2af496911a003ca20e556>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-config/src/types.js
Expand Down Expand Up @@ -140,6 +140,7 @@ type SerializerConfigT = {
polyfillModuleNames: ReadonlyArray<string>;
processModuleFilter: (modules: Module) => boolean;
isThirdPartyModule: (module: Readonly<{path: string}>) => boolean;
unstable_allowIndexMap: boolean;
};
type TransformerConfigT = Omit<
JsTransformerConfig,
Expand Down
4 changes: 2 additions & 2 deletions packages/metro-source-map/src/BundleBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,12 @@ function measureString(str: string): {
}

export function createIndexMap(
file: string,
file: ?string,
sections: Array<IndexMapSection>,
): IndexMap {
return {
version: 3,
file,
...(file != null ? {file} : null),
sections,
};
}
107 changes: 106 additions & 1 deletion packages/metro-source-map/src/__tests__/source-map-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
* @oncall react_native
*/

import type {BabelDecodedMap, MetroSourceMapSegmentTuple} from '../source-map';
import type {
BabelDecodedMap,
IndexMap,
MetroSourceMapSegmentTuple,
MixedSourceMap,
} from '../source-map';

import Generator from '../Generator';
import {
fromRawMappings,
fromRawMappingsIndexed,
isVlqMap,
toBabelSegments,
toSegmentTuple,
Expand Down Expand Up @@ -291,6 +297,105 @@ describe('fromRawMappings with VlqMap', () => {
});
});

describe('fromRawMappingsIndexed', () => {
// fromRawMappingsIndexed always yields an indexed (sectioned) map.
const asIndexMap = (map: MixedSourceMap): IndexMap => {
// eslint-disable-next-line lint/strictly-null
if (map.mappings !== undefined) {
throw new Error('Expected an indexed source map');
}
return map;
};

test('produces an indexed map, passing VLQ through verbatim', () => {
const input = [
{
code: lines(11),
functionMap: null,
map: makeVlqMap('E;;IAKMA;;;;QAII;;;;YAIIC', ['apples', 'pears']),
source: 'code1',
path: 'path1',
isIgnored: false,
},
{
code: lines(3),
functionMap: null,
map: makeVlqMap('E;;IAegBA', ['bananas']),
source: 'code2',
path: 'path2',
isIgnored: true,
},
];

const map = asIndexMap(fromRawMappingsIndexed(input).toMap());
expect(map.version).toBe(3);
expect(map.sections).toHaveLength(2);

const [s0, s1] = map.sections;
expect(s0.offset).toEqual({line: 0, column: 0});
expect(s0.map.sources).toEqual(['path1']);
expect(s0.map.sourcesContent).toEqual(['code1']);
// VLQ string passes through unchanged (no decode/re-encode).
expect(s0.map.mappings).toBe('E;;IAKMA;;;;QAII;;;;YAIIC');
expect(s0.map.names).toEqual(['apples', 'pears']);

expect(s1.offset).toEqual({line: 11, column: 0});
expect(s1.map.mappings).toBe('E;;IAegBA');
expect(s1.map.x_google_ignoreList).toEqual([0]);
});

test('preserves functionMap as per-section x_facebook_sources', () => {
const functionMap = {names: ['<global>'], mappings: 'AAA'};
const map = asIndexMap(
fromRawMappingsIndexed([
{
code: 'x\n',
functionMap,
map: makeVlqMap('AAAA', []),
source: 'src',
path: 'file.js',
isIgnored: false,
},
]).toMap(),
);
expect(map.sections[0].map.x_facebook_sources).toEqual([[functionMap]]);
});

test('toString produces valid indexed JSON', () => {
const parsed = JSON.parse(
fromRawMappingsIndexed([
{
code: 'x\n',
functionMap: null,
map: makeVlqMap('AAAA', []),
source: 'src',
path: 'file.js',
isIgnored: false,
},
]).toString(),
);
expect(parsed.version).toBe(3);
expect(parsed.sections).toHaveLength(1);
expect(parsed.sections[0].map.mappings).toBe('AAAA');
});

test('excludeSource omits sourcesContent', () => {
const map = asIndexMap(
fromRawMappingsIndexed([
{
code: 'x\n',
functionMap: null,
map: makeVlqMap('AAAA', []),
source: 'src',
path: 'file.js',
isIgnored: false,
},
]).toMap(undefined, {excludeSource: true}),
);
expect(map.sections[0].map.sourcesContent).toBeUndefined();
});
});

describe('vlqMapFromTuples', () => {
// Decode via Metro's existing string->tuples path, the inverse of
// vlqMapFromTuples.
Expand Down
102 changes: 102 additions & 0 deletions packages/metro-source-map/src/source-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,45 @@ export type RawMappingsModule = {
readonly lineCount?: number,
};

// Common shape of the flat `Generator` and the indexed `IndexedSourceMapResult`,
// so serializers can hold either and call `toMap`/`toString` uniformly.
export interface SourceMapGenerator {
toMap(file?: string, options?: {excludeSource?: boolean}): MixedSourceMap;
toString(file?: string, options?: {excludeSource?: boolean}): string;
}

/**
* Result of `fromRawMappingsIndexed`: a sectioned (indexed) source map where
* each module is one section. VLQ-stored modules pass through verbatim, which is
* why building this is cheap compared to flattening into a single map.
*/
class IndexedSourceMapResult implements SourceMapGenerator {
#sections: Array<IndexMapSection>;

constructor(sections: Array<IndexMapSection>) {
this.#sections = sections;
}

toMap(file?: string, options?: {excludeSource?: boolean}): MixedSourceMap {
const sections =
options?.excludeSource === true
? this.#sections.map(section => {
// exclude source
const {sourcesContent: _, ...map} = section.map;
return {
...section,
map,
};
})
: this.#sections;
return createIndexMap(file, sections);
}

toString(file?: string, options?: {excludeSource?: boolean}): string {
return JSON.stringify(this.toMap(file, options));
}
}

function isVlqMap(
map: ?ReadonlyArray<MetroSourceMapSegmentTuple> | VlqMap,
): implies map is VlqMap {
Expand Down Expand Up @@ -241,6 +280,68 @@ async function fromRawMappingsNonBlocking(
});
}

/**
* Like `fromRawMappings`, but produces an indexed (sectioned) source map with
* one section per module. VLQ-stored modules pass through verbatim — no
* decode/re-encode — which is the whole point: it's much cheaper to serialize
* than the flat path, at the cost of emitting an indexed map that consumers must
* understand. Per-module work is trivial, so this runs synchronously.
*/
function fromRawMappingsIndexed(
modules: ReadonlyArray<RawMappingsModule>,
offsetLines: number = 0,
): IndexedSourceMapResult {
const sections: Array<IndexMapSection> = [];
let carryOver = offsetLines;

for (const mod of modules) {
if (mod.map != null) {
sections.push({
offset: {line: carryOver, column: 0},
map: toIndexMapSection(mod),
});
}
carryOver = carryOver + countLines(mod.code);
}

return new IndexedSourceMapResult(sections);
}

/**
* Builds a single section of an indexed source map. VLQ maps pass through
* verbatim, while tuple maps are encoded with a fresh per-section Generator.
*/
function toIndexMapSection(module: RawMappingsModule): BasicSourceMap {
const {map, path, source, functionMap, isIgnored} = module;

if (isVlqMap(map)) {
let sectionMap: BasicSourceMap = {
version: 3,
sources: [path],
sourcesContent: [source],
names: [...map.names],
mappings: map.mappings,
};
// The Generator bakes these in for tuple maps; for passthrough VLQ maps we
// have to attach them ourselves.
if (functionMap != null) {
sectionMap = {...sectionMap, x_facebook_sources: [[functionMap]]};
}
if (isIgnored) {
sectionMap = {...sectionMap, x_google_ignoreList: [0]};
}
return sectionMap;
}

if (Array.isArray(map)) {
const generator = new Generator();
addMappingsForFile(generator, map, module, 0);
return generator.toMap();
}

throw new Error(`Unexpected module with full source map found: ${path}`);
}

/**
* Transforms a standard source map object into a Raw Mappings object, to be
* used across the bundler.
Expand Down Expand Up @@ -504,6 +605,7 @@ export {
createIndexMap,
generateFunctionMap,
fromRawMappings,
fromRawMappingsIndexed,
fromRawMappingsNonBlocking,
functionMapBabelPlugin,
isVlqMap,
Expand Down
4 changes: 2 additions & 2 deletions packages/metro-source-map/types/BundleBuilder.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
* @noformat
* @oncall react_native
* @generated SignedSource<<920bacbb8042b15a2cd4888e0ca47b8c>>
* @generated SignedSource<<a3b673eadec9c804b8a10df9d304100e>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-source-map/src/BundleBuilder.js
Expand Down Expand Up @@ -37,6 +37,6 @@ export declare class BundleBuilder {
getCode(): string;
}
export declare function createIndexMap(
file: string,
file: null | undefined | string,
sections: Array<IndexMapSection>,
): IndexMap;
28 changes: 27 additions & 1 deletion packages/metro-source-map/types/source-map.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
* @noformat
* @oncall react_native
* @generated SignedSource<<9ec89353742743678e422f0bf81e488d>>
* @generated SignedSource<<13fbae6a38a28c6a6e3a2be58804c33d>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-source-map/src/source-map.js
Expand Down Expand Up @@ -107,6 +107,20 @@ export type RawMappingsModule = {
readonly isIgnored: boolean;
readonly lineCount?: number;
};
export interface SourceMapGenerator {
toMap(file?: string, options?: {excludeSource?: boolean}): MixedSourceMap;
toString(file?: string, options?: {excludeSource?: boolean}): string;
}
/**
* Result of `fromRawMappingsIndexed`: a sectioned (indexed) source map where
* each module is one section. VLQ-stored modules pass through verbatim, which is
* why building this is cheap compared to flattening into a single map.
*/
declare class IndexedSourceMapResult implements SourceMapGenerator {
constructor(sections: Array<IndexMapSection>);
toMap(file?: string, options?: {excludeSource?: boolean}): MixedSourceMap;
toString(file?: string, options?: {excludeSource?: boolean}): string;
}
declare function isVlqMap(
map: (null | undefined | ReadonlyArray<MetroSourceMapSegmentTuple>) | VlqMap,
): map is VlqMap;
Expand All @@ -125,6 +139,17 @@ declare function fromRawMappingsNonBlocking(
modules: ReadonlyArray<RawMappingsModule>,
offsetLines?: number,
): Promise<Generator>;
/**
* Like `fromRawMappings`, but produces an indexed (sectioned) source map with
* one section per module. VLQ-stored modules pass through verbatim — no
* decode/re-encode — which is the whole point: it's much cheaper to serialize
* than the flat path, at the cost of emitting an indexed map that consumers must
* understand. Per-module work is trivial, so this runs synchronously.
*/
declare function fromRawMappingsIndexed(
modules: ReadonlyArray<RawMappingsModule>,
offsetLines?: number,
): IndexedSourceMapResult;
/**
* Transforms a standard source map object into a Raw Mappings object, to be
* used across the bundler.
Expand Down Expand Up @@ -183,6 +208,7 @@ export {
createIndexMap,
generateFunctionMap,
fromRawMappings,
fromRawMappingsIndexed,
fromRawMappingsNonBlocking,
functionMapBabelPlugin,
isVlqMap,
Expand Down
Loading
Loading