Skip to content

Commit

Permalink
Support x_google_ignoreList in source map infra (#973)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #973

Changelog:
* **[Feature]**: Basic support for `x_google_ignoreList` in `metro-source-map` and `metro-symbolicate`

Enables Metro's source map infrastructure to produce and consume the `x_google_ignoreList` field, documented [here](https://developer.chrome.com/blog/devtools-better-angular-debugging/#the-x_google_ignorelist-source-map-extension).

An upcoming diff will use this infra support to add ignore lists to Metro's source maps by default.

The changes here include:

* An option to mark source files as ignored in `metro-source-map`'s `Generator` (used by the serializer).
* Preserving ignore lists in `composeSourceMaps`. `composeSourceMaps` is used by React Native in [OSS](https://github.com/facebook/react-native/blob/c1304d938da9cd5016da0bded4d618c42efbd7e4/scripts/compose-source-maps.js#L13) to generate source maps for Hermes bytecode compiled from Metro bundles.
* Including an `isIgnored` field in source locations returned via the `metro-symbolicate` API. `metro-symbolicate` is the reference implementation of symbolication based on Metro's source maps.

Reviewed By: GijsWeterings

Differential Revision: D43554204

fbshipit-source-id: 22a8b5a7657e7887c6f01ae17ba40f4ec1c7016a
  • Loading branch information
motiz88 authored and facebook-github-bot committed Apr 25, 2023
1 parent 071ecc3 commit d8ca3f2
Show file tree
Hide file tree
Showing 12 changed files with 691 additions and 24 deletions.
82 changes: 59 additions & 23 deletions packages/metro-source-map/src/Generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @flow strict-local
* @format
* @oncall react_native
*/
Expand All @@ -19,6 +19,10 @@ import type {

const B64Builder = require('./B64Builder');

type FileFlags = $ReadOnly<{
addToIgnoreList?: boolean,
}>;

/**
* Generates a source map from raw mappings.
*
Expand All @@ -45,6 +49,8 @@ class Generator {
sources: Array<string>;
sourcesContent: Array<?string>;
x_facebook_sources: Array<?FBSourceMetadata>;
// https://developer.chrome.com/blog/devtools-better-angular-debugging/#the-x_google_ignorelist-source-map-extension
x_google_ignoreList: Array<number>;

constructor() {
this.builder = new B64Builder();
Expand All @@ -61,15 +67,26 @@ class Generator {
this.sources = [];
this.sourcesContent = [];
this.x_facebook_sources = [];
this.x_google_ignoreList = [];
}

/**
* Mark the beginning of a new source file.
*/
startFile(file: string, code: string, functionMap: ?FBSourceFunctionMap) {
this.source = this.sources.push(file) - 1;
startFile(
file: string,
code: string,
functionMap: ?FBSourceFunctionMap,
flags?: FileFlags,
) {
const {addToIgnoreList = false} = flags ?? {};
const sourceIndex = this.sources.push(file) - 1;
this.source = sourceIndex;
this.sourcesContent.push(code);
this.x_facebook_sources.push(functionMap ? [functionMap] : null);
if (addToIgnoreList) {
this.x_google_ignoreList.push(sourceIndex);
}
}

/**
Expand Down Expand Up @@ -159,32 +176,41 @@ class Generator {
file?: string,
options?: {excludeSource?: boolean, ...},
): BasicSourceMap {
let content, sourcesMetadata;
const content: {
sourcesContent?: Array<?string>,
} =
options && options.excludeSource === true
? {}
: {sourcesContent: this.sourcesContent.slice()};

if (options && options.excludeSource) {
content = {};
} else {
content = {sourcesContent: this.sourcesContent.slice()};
}
const sourcesMetadata: {
x_facebook_sources?: Array<FBSourceMetadata>,
} = this.hasSourcesMetadata()
? {
x_facebook_sources: JSON.parse(
JSON.stringify(this.x_facebook_sources),
),
}
: {};

if (this.hasSourcesMetadata()) {
sourcesMetadata = {
x_facebook_sources: JSON.parse(JSON.stringify(this.x_facebook_sources)),
};
} else {
sourcesMetadata = {};
}
const ignoreList: {
x_google_ignoreList?: Array<number>,
} = this.x_google_ignoreList.length
? {
x_google_ignoreList: this.x_google_ignoreList,
}
: {};

return {
return ({
version: 3,
file,
sources: this.sources.slice(),
// $FlowFixMe[exponential-spread]
...content,
...sourcesMetadata,
...ignoreList,
names: this.names.items(),
mappings: this.builder.toString(),
};
}: BasicSourceMap);
}

/**
Expand All @@ -193,14 +219,14 @@ class Generator {
* This is ~2.5x faster than calling `JSON.stringify(generator.toMap())`
*/
toString(file?: string, options?: {excludeSource?: boolean, ...}): string {
let content, sourcesMetadata;

if (options && options.excludeSource) {
let content;
if (options && options.excludeSource === true) {
content = '';
} else {
content = `"sourcesContent":${JSON.stringify(this.sourcesContent)},`;
}

let sourcesMetadata;
if (this.hasSourcesMetadata()) {
sourcesMetadata = `"x_facebook_sources":${JSON.stringify(
this.x_facebook_sources,
Expand All @@ -209,13 +235,23 @@ class Generator {
sourcesMetadata = '';
}

let ignoreList;
if (this.x_google_ignoreList.length) {
ignoreList = `"x_google_ignoreList":${JSON.stringify(
this.x_google_ignoreList,
)},`;
} else {
ignoreList = '';
}

return (
'{' +
'"version":3,' +
(file ? `"file":${JSON.stringify(file)},` : '') +
(file != null ? `"file":${JSON.stringify(file)},` : '') +
`"sources":${JSON.stringify(this.sources)},` +
content +
sourcesMetadata +
ignoreList +
`"names":${JSON.stringify(this.names.items())},` +
`"mappings":"${this.builder.toString()}"` +
'}'
Expand Down
40 changes: 39 additions & 1 deletion packages/metro-source-map/src/__tests__/Generator-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ describe('full map generation', () => {
generator.addSimpleMapping(1, 2);
generator.addNamedSourceMapping(3, 4, 5, 6, 'plums');
generator.endFile();
generator.startFile('lemons', 'oranges');
generator.startFile('lemons', 'oranges', undefined, {
addToIgnoreList: true,
});
generator.addNamedSourceMapping(7, 8, 9, 10, 'tangerines');
generator.addNamedSourceMapping(11, 12, 13, 14, 'tangerines');
generator.addSimpleMapping(15, 16);
Expand All @@ -113,6 +115,7 @@ describe('full map generation', () => {
sources: ['apples', 'lemons'],
sourcesContent: ['pears', 'oranges'],
names: ['plums', 'tangerines'],
x_google_ignoreList: [1],
});
});

Expand All @@ -133,3 +136,38 @@ describe('full map generation', () => {
expect(JSON.parse(generator.toString(file))).toEqual(generator.toMap(file));
});
});

describe('x_google_ignoreList', () => {
it('add files to ignore list', () => {
const file1 = 'just/a/file';
const file2 = 'another/file';
const file3 = 'file3';
const source1 = 'var a = 1;';
const source2 = 'var a = 2;';

generator.startFile(file1, source1, undefined, {addToIgnoreList: true});
generator.startFile(file2, source2, undefined, {addToIgnoreList: false});
generator.startFile(file3, source2, undefined, {addToIgnoreList: true});

expect(generator.toMap()).toEqual(
objectContaining({
sources: [file1, file2, file3],
x_google_ignoreList: [0, 2],
}),
);
});

it('not emitted if no files are ignored', () => {
const file1 = 'just/a/file';
const file2 = 'another/file';
const file3 = 'file3';
const source1 = 'var a = 1;';
const source2 = 'var a = 2;';

generator.startFile(file1, source1);
generator.startFile(file2, source2, undefined, {addToIgnoreList: false});
generator.startFile(file3, source2, undefined, {addToIgnoreList: false});

expect(generator.toMap()).not.toHaveProperty('x_google_ignoreList');
});
});
87 changes: 87 additions & 0 deletions packages/metro-source-map/src/__tests__/composeSourceMaps-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const {add0, add1} = require('ob1');
const path = require('path');
const terser = require('terser');

const {objectContaining} = expect;

/* eslint-disable no-multi-str */

const TestScript1 =
Expand Down Expand Up @@ -165,6 +167,91 @@ describe('composeSourceMaps', () => {
]);
});

it('preserves and reindexes x_google_ignoreList', () => {
const map1 = {
version: 3,
sections: [
{
offset: {line: 0, column: 0},
map: {
version: 3,
sources: ['unused.js', 'src.js'],
x_google_ignoreList: [1],
names: ['global'],
mappings: ';CCCCA',
},
},
],
};

const map2 = {
version: 3,
sources: ['src-transformed.js'],
names: ['gLoBAl'],
mappings: ';CACCA',
};

const mergedMap = composeSourceMaps([map1, map2]);

expect(mergedMap).toEqual(
objectContaining({
sources: ['src.js'],
x_google_ignoreList: [0],
}),
);
});

it('x_google_ignoreList: a source with inconsistent ignore status is considered to be ignored', () => {
const map1 = {
version: 3,
sections: [
{
offset: {line: 0, column: 0},
map: {
version: 3,
sources: ['unused.js', 'src.js'],
x_google_ignoreList: [1],
names: ['global'],
// First line of src-transformed.js maps to src.js (source #1 here)
mappings: 'ACAAA',
},
},
{
offset: {line: 1, column: 0},
map: {
version: 3,
sources: ['src.js', 'other.js'],
x_google_ignoreList: ([]: Array<number>),
names: ['global'],

mappings:
// Second line of src-transformed.js maps to src.js (source #0 here)
'AAAAA' +
// Third line of src-transformed.js maps to other.js (source #1 here)
';ACAAA',
},
},
],
};

const map2 = {
version: 3,
sources: ['src-transformed.js'],
names: ['gLoBAl'],
// Map each line to itself
mappings: 'AAAAA;AACAA;AACAA',
};

const mergedMap = composeSourceMaps([map1, map2]);

expect(mergedMap).toEqual(
objectContaining({
sources: ['src.js', 'other.js'],
x_google_ignoreList: [0],
}),
);
});

it('preserves sourcesContent', () => {
const map1 = {
version: 3,
Expand Down
6 changes: 6 additions & 0 deletions packages/metro-source-map/src/composeSourceMaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function composeSourceMaps(
): MixedSourceMap {
// NOTE: require() here to break dependency cycle
const SourceMetadataMapConsumer = require('metro-symbolicate/src/SourceMetadataMapConsumer');
const GoogleIgnoreListConsumer = require('metro-symbolicate/src/GoogleIgnoreListConsumer');
if (maps.length < 1) {
throw new Error('composeSourceMaps: Expected at least one map');
}
Expand Down Expand Up @@ -81,6 +82,11 @@ function composeSourceMaps(
if (function_offsets) {
composedMap.x_hermes_function_offsets = function_offsets;
}
const ignoreListConsumer = new GoogleIgnoreListConsumer(firstMap);
const x_google_ignoreList = ignoreListConsumer.toArray(composedMap.sources);
if (x_google_ignoreList.length) {
composedMap.x_google_ignoreList = x_google_ignoreList;
}
return composedMap;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/metro-source-map/src/source-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type BasicSourceMap = {
+x_facebook_sources?: FBSourcesArray,
+x_facebook_segments?: FBSegmentMap,
+x_hermes_function_offsets?: HermesFunctionOffsets,
+x_google_ignoreList?: Array<number>,
};

export type IndexMapSection = {
Expand All @@ -82,6 +83,7 @@ export type IndexMap = {
+x_facebook_sources?: void,
+x_facebook_segments?: FBSegmentMap,
+x_hermes_function_offsets?: HermesFunctionOffsets,
+x_google_ignoreList?: void,
};

export type MixedSourceMap = IndexMap | BasicSourceMap;
Expand Down
Loading

0 comments on commit d8ca3f2

Please sign in to comment.