Skip to content

Commit

Permalink
Switch to @ampproject/remapping to merge source maps (#14209)
Browse files Browse the repository at this point in the history
  • Loading branch information
jridgewell committed Jan 29, 2022
1 parent 6b427ce commit 7d32f49
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 329 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions packages/babel-core/package.json
Expand Up @@ -48,6 +48,7 @@
"./src/transformation/util/clone-deep.ts": "./src/transformation/util/clone-deep-browser.ts"
},
"dependencies": {
"@ampproject/remapping": "^2.0.0",
"@babel/code-frame": "workspace:^",
"@babel/generator": "workspace:^",
"@babel/helper-compilation-targets": "workspace:^",
Expand All @@ -61,8 +62,7 @@
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.1.2",
"semver": "condition:BABEL_8_BREAKING ? ^7.3.4 : ^6.3.0",
"source-map": "^0.5.0"
"semver": "condition:BABEL_8_BREAKING ? ^7.3.4 : ^6.3.0"
},
"devDependencies": {
"@babel/helper-transform-fixture-test-runner": "workspace:^",
Expand All @@ -71,7 +71,8 @@
"@types/debug": "^4.1.0",
"@types/resolve": "^1.3.2",
"@types/semver": "^5.4.0",
"@types/source-map": "^0.5.0"
"@types/source-map": "^0.5.0",
"source-map": "0.6.1"
},
"conditions": {
"BABEL_8_BREAKING": [
Expand Down
314 changes: 9 additions & 305 deletions packages/babel-core/src/transformation/file/merge-map.ts
@@ -1,321 +1,25 @@
type SourceMap = any;
import sourceMap from "source-map";
import remapping from "@ampproject/remapping";

export default function mergeSourceMap(
inputMap: SourceMap,
map: SourceMap,
): SourceMap {
const input = buildMappingData(inputMap);
const output = buildMappingData(map);
const result = remapping([rootless(map), rootless(inputMap)], () => null);

const mergedGenerator = new sourceMap.SourceMapGenerator();
for (const { source } of input.sources) {
if (typeof source.content === "string") {
mergedGenerator.setSourceContent(source.path, source.content);
}
}

if (output.sources.length === 1) {
const defaultSource = output.sources[0];

const insertedMappings = new Map();

// Process each generated range in the input map, e.g. each range over the
// code that Babel was originally given.
eachInputGeneratedRange(input, (generated, original, source) => {
// Then pick out each range over Babel's _output_ that corresponds with
// the given range on the code given to Babel.
eachOverlappingGeneratedOutputRange(defaultSource, generated, item => {
// It's possible that multiple input ranges will overlap the same
// generated range. Since sourcemap don't traditionally represent
// generated locations with multiple original locations, we explicitly
// skip generated locations once we've seen them the first time.
const key = makeMappingKey(item);
if (insertedMappings.has(key)) return;
insertedMappings.set(key, item);

mergedGenerator.addMapping({
source: source.path,
original: {
line: original.line,
column: original.columnStart,
},
generated: {
line: item.line,
column: item.columnStart,
},
name: original.name,
});
});
});

// Since mappings are manipulated using single locations, but are interpreted
// as ranges, the insertions above may not actually have their ending
// locations mapped yet. Here be go through each one and ensure that it has
// a well-defined ending location, if one wasn't already created by the start
// of a different range.
for (const item of insertedMappings.values()) {
if (item.columnEnd === Infinity) {
continue;
}

const clearItem = {
line: item.line,
columnStart: item.columnEnd,
};

const key = makeMappingKey(clearItem);
if (insertedMappings.has(key)) {
continue;
}

// Insert mappings with no original position to terminate any mappings
// that were found above, so that they don't expand beyond their correct
// range.
// @ts-expect-error todo(flow->ts) original and source field are missing
mergedGenerator.addMapping({
generated: {
line: clearItem.line,
column: clearItem.columnStart,
},
});
}
}

const result = mergedGenerator.toJSON();
// addMapping expects a relative path, and setSourceContent expects an
// absolute path. To avoid this whole confusion, we leave the root out
// entirely, and add it at the end here.
if (typeof input.sourceRoot === "string") {
result.sourceRoot = input.sourceRoot;
if (typeof inputMap.sourceRoot === "string") {
result.sourceRoot = inputMap.sourceRoot;
}
return result;
}

function makeMappingKey(item: { line: number; columnStart: number }) {
return `${item.line}/${item.columnStart}`;
}

function eachOverlappingGeneratedOutputRange(
outputFile: ResolvedFileMappings,
inputGeneratedRange: ResolvedGeneratedRange,
callback: (range: ResolvedGeneratedRange) => unknown,
) {
// Find the Babel-generated mappings that overlap with this range in the
// input sourcemap. Generated locations within the input sourcemap
// correspond with the original locations in the map Babel generates.
const overlappingOriginal = filterApplicableOriginalRanges(
outputFile,
inputGeneratedRange,
);

for (const { generated } of overlappingOriginal) {
for (const item of generated) {
callback(item);
}
}
}

function filterApplicableOriginalRanges(
{ mappings }: ResolvedFileMappings,
{ line, columnStart, columnEnd }: ResolvedGeneratedRange,
): OriginalMappings {
// The mapping array is sorted by original location, so we can
// binary-search it for the overlapping ranges.
return filterSortedArray(mappings, ({ original: outOriginal }) => {
if (line > outOriginal.line) return -1;
if (line < outOriginal.line) return 1;

if (columnStart >= outOriginal.columnEnd) return -1;
if (columnEnd <= outOriginal.columnStart) return 1;

return 0;
});
}

function eachInputGeneratedRange(
map: ResolvedMappings,
callback: (
c: ResolvedGeneratedRange,
b: ResolvedOriginalRange,
a: ResolvedSource,
) => unknown,
) {
for (const { source, mappings } of map.sources) {
for (const { original, generated } of mappings) {
for (const item of generated) {
callback(item, original, source);
}
}
}
}

type ResolvedMappings = {
file: string | undefined | null;
sourceRoot: string | undefined | null;
sources: Array<ResolvedFileMappings>;
};

type ResolvedFileMappings = {
source: ResolvedSource;
mappings: OriginalMappings;
};

type OriginalMappings = Array<{
original: ResolvedOriginalRange;
generated: Array<ResolvedGeneratedRange>;
}>;

type ResolvedSource = {
path: string;
content: string | null;
};

type ResolvedOriginalRange = {
line: number;
columnStart: number;
columnEnd: number;
name: string | null;
};

type ResolvedGeneratedRange = {
line: number;
columnStart: number;
columnEnd: number;
};

function buildMappingData(map: SourceMap): ResolvedMappings {
const consumer = new sourceMap.SourceMapConsumer({
function rootless(map: SourceMap): SourceMap {
return {
...map,

// This is a bit hack. .addMapping expects source values to be relative,
// but eachMapping returns mappings with absolute paths. To avoid that
// incompatibility, we leave the sourceRoot out here and add it to the
// final map at the end instead.
// This is a bit hack. Remapping will create absolute sources in our
// sourcemap, but we want to maintain sources relative to the sourceRoot.
// We'll re-add the sourceRoot after remapping.
sourceRoot: null,
});

const sources = new Map();
const mappings = new Map();

let last = null;

consumer.computeColumnSpans();

consumer.eachMapping(
m => {
if (m.originalLine === null) return;

let source = sources.get(m.source);
if (!source) {
source = {
path: m.source,
content: consumer.sourceContentFor(m.source, true),
};
sources.set(m.source, source);
}

let sourceData = mappings.get(source);
if (!sourceData) {
sourceData = {
source,
mappings: [],
};
mappings.set(source, sourceData);
}

const obj = {
line: m.originalLine,
columnStart: m.originalColumn,
columnEnd: Infinity,
name: m.name,
};

if (
last &&
last.source === source &&
last.mapping.line === m.originalLine
) {
last.mapping.columnEnd = m.originalColumn;
}

last = {
source,
mapping: obj,
};

sourceData.mappings.push({
original: obj,
generated: consumer
.allGeneratedPositionsFor({
source: m.source,
line: m.originalLine,
column: m.originalColumn,
})
.map(item => ({
line: item.line,
columnStart: item.column,
// source-map's lastColumn is inclusive, not exclusive, so we need
// to add 1 to it.
columnEnd: item.lastColumn + 1,
})),
});
},
null,
sourceMap.SourceMapConsumer.ORIGINAL_ORDER,
);

return {
file: map.file,
sourceRoot: map.sourceRoot,
sources: Array.from(mappings.values()),
};
}

function findInsertionLocation<T>(
array: Array<T>,
callback: (item: T) => number,
): number {
let left = 0;
let right = array.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
const item = array[mid];

const result = callback(item);
if (result === 0) {
left = mid;
break;
}
if (result >= 0) {
right = mid;
} else {
left = mid + 1;
}
}

// Ensure the value is the start of any set of matches.
let i = left;
if (i < array.length) {
while (i >= 0 && callback(array[i]) >= 0) {
i--;
}
return i + 1;
}

return i;
}

function filterSortedArray<T>(
array: Array<T>,
callback: (item: T) => number,
): Array<T> {
const start = findInsertionLocation(array, callback);

const results = [];
for (let i = start; i < array.length && callback(array[i]) === 0; i++) {
results.push(array[i]);
}

return results;
}
2 changes: 1 addition & 1 deletion packages/babel-core/test/api.js
Expand Up @@ -540,7 +540,7 @@ describe("api", function () {
column: 4,
}),
).toEqual({
name: null,
name: "Foo",
source: "stdout",
line: 1,
column: 6,
Expand Down
@@ -1,10 +1,8 @@
{
"version": 3,
"sources": [
"HelloWorld.vue"
],
"names": [],
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAsFA;AACA,EAAA,IAAA,EAAA,YADA;;AAEA,EAAA,IAAA,GAAA;AACA,WAAA;AACA,MAAA,GAAA,EAAA;AADA,KAAA;AAGA;;AANA,C",
"sources": ["HelloWorld.vue"],
"names": ["name", "data", "msg"],
"mappings": ";;;;;;AAsFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAAA;AACAA,oBADA;;AAEAC;AACA;AACAC;AADA;AAGA;;AANA,C",
"sourceRoot": "src/components",
"sourcesContent": [
"<template>\n <div class=\"hello\">\n <h1>{{ msg }}</h1>\n <h2>Essential Links</h2>\n <ul>\n <li>\n <a\n href=\"https://vuejs.org\"\n target=\"_blank\"\n >\n Core Docs\n </a>\n </li>\n <li>\n <a\n href=\"https://forum.vuejs.org\"\n target=\"_blank\"\n >\n Forum\n </a>\n </li>\n <li>\n <a\n href=\"https://chat.vuejs.org\"\n target=\"_blank\"\n >\n Community Chat\n </a>\n </li>\n <li>\n <a\n href=\"https://twitter.com/vuejs\"\n target=\"_blank\"\n >\n Twitter\n </a>\n </li>\n <br>\n <li>\n <a\n href=\"http://vuejs-templates.github.io/webpack/\"\n target=\"_blank\"\n >\n Docs for This Template\n </a>\n </li>\n </ul>\n <h2>Ecosystem</h2>\n <ul>\n <li>\n <a\n href=\"http://router.vuejs.org/\"\n target=\"_blank\"\n >\n vue-router\n </a>\n </li>\n <li>\n <a\n href=\"http://vuex.vuejs.org/\"\n target=\"_blank\"\n >\n vuex\n </a>\n </li>\n <li>\n <a\n href=\"http://vue-loader.vuejs.org/\"\n target=\"_blank\"\n >\n vue-loader\n </a>\n </li>\n <li>\n <a\n href=\"https://github.com/vuejs/awesome-vue\"\n target=\"_blank\"\n >\n awesome-vue\n </a>\n </li>\n </ul>\n </div>\n</template>\n\n<script>\nexport default {\n name: 'HelloWorld',\n data () {\n return {\n msg: 'Welcome to Your Vue.js App'\n }\n }\n}\n</script>\n\n<!-- Add \"scoped\" attribute to limit CSS to this component only -->\n<style scoped>\nh1, h2 {\n font-weight: normal;\n}\nul {\n list-style-type: none;\n padding: 0;\n}\nli {\n display: inline-block;\n margin: 0 10px;\n}\na {\n color: #42b983;\n}\n</style>\n"
Expand Down
@@ -1,5 +1,5 @@
{
"mappings": "AAAA,IAAA,GAAA,GAAU,Y;SAAM,C;AAAC,CAAjB",
"mappings": "AAAA,UAAU,Y;SAAM,C;AAAC,CAAjB",
"names": [],
"sources": ["original.js"],
"sourcesContent": ["var foo = () => 4;"],
Expand Down

0 comments on commit 7d32f49

Please sign in to comment.