Merge pull request #7761 from loganfsmyth/bad-map-merge

Reimplement input sourcemap merging using range matching instead of closest-position matching
This commit is contained in:
Logan Smyth 2018-04-25 12:29:19 -07:00 committed by GitHub
commit 138d60922c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 769 additions and 55 deletions

View File

@ -26,22 +26,124 @@ declare module "lodash/defaults" {
declare module "lodash/clone" {
declare export default <T>(obj: T) => T;
}
}
declare module "lodash/merge" {
declare export default <T: Object>(T, Object) => T;
}
declare module "convert-source-map" {
declare module "source-map" {
declare export type SourceMap = {
version: 3,
file: ?string,
sourceRoot: ?string,
sources: [?string],
sourcesContent: [?string],
names: [?string],
mappings: string,
};
declare module.exports: {
SourceMapConsumer: typeof SourceMapConsumer,
SourceMapGenerator: typeof SourceMapGenerator,
}
declare class SourceMapConsumer {
static GENERATED_ORDER: 1;
static ORIGINAL_ORDER: 2;
file: string | null;
sourceRoot: string | null;
sources: Array<string>;
constructor(?SourceMap): this;
computeColumnSpans(): string;
originalPositionFor({
line: number,
column: number,
}): {|
source: string,
line: number,
column: number,
name: string | null
|} | {|
source: null,
line: null,
column: null,
name: null
|};
generatedPositionFor({
source: string,
line: number,
column: number,
}): {|
line: number,
column: number,
lastColumn: number | null | void,
|} | {|
line: null,
column: null,
lastColumn: null | void,
|};
allGeneratedPositionsFor({
source: string,
line: number,
column: number,
}): Array<{|
line: number,
column: number,
lastColumn: number,
|}>;
sourceContentFor(string, boolean | void): string | null;
eachMapping(
({|
generatedLine: number,
generatedColumn: number,
source: string,
originalLine: number,
originalColumn: number,
name: string | null,
|} | {|
generatedLine: number,
generatedColumn: number,
source: null,
originalLine: null,
originalColumn: null,
name: null,
|}) => mixed,
context: mixed,
order: ?(1 | 2),
): void;
}
declare class SourceMapGenerator {
constructor(?{
file?: string | null,
sourceRoot?: string | null,
skipValidation?: boolean | null,
}): this;
addMapping({
generated: {
line: number,
column: number,
}
}): void;
setSourceContent(string, string): void;
toJSON(): SourceMap;
}
}
declare module "convert-source-map" {
import type { SourceMap } from "source-map";
declare class Converter {
toJSON(): string;
toBase64(): string;

View File

@ -4,4 +4,4 @@ var foo = function foo() {
return 4;
};
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm9yaWdpbmFsLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsVUFBVSxlO1MsQUFBTTtBQUFDIiwiZmlsZSI6InNjcmlwdDIuanMiLCJzb3VyY2VzQ29udGVudCI6WyJ2YXIgZm9vID0gKCkgPT4gNDsiXX0=
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm9yaWdpbmFsLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsSUFBQSxNQUFVLFNBQVYsR0FBVSxHO1NBQU0sQztBQUFDLENBQWpCIiwiZmlsZSI6InNjcmlwdDIuanMiLCJzb3VyY2VzQ29udGVudCI6WyJ2YXIgZm9vID0gKCkgPT4gNDsiXX0=

View File

@ -2,10 +2,10 @@
import type { PluginPasses } from "../../config";
import convertSourceMap, { type SourceMap } from "convert-source-map";
import sourceMap from "source-map";
import generate from "@babel/generator";
import type File from "./file";
import mergeSourceMap from "./merge-map";
export default function generateCode(
pluginPasses: PluginPasses,
@ -72,46 +72,3 @@ export default function generateCode(
return { outputCode, outputMap };
}
function mergeSourceMap(inputMap: SourceMap, map: SourceMap): SourceMap {
const inputMapConsumer = new sourceMap.SourceMapConsumer(inputMap);
const outputMapConsumer = new sourceMap.SourceMapConsumer(map);
const mergedGenerator = new sourceMap.SourceMapGenerator({
file: inputMapConsumer.file,
sourceRoot: inputMapConsumer.sourceRoot,
});
// This assumes the output map always has a single source, since Babel always compiles a
// single source file to a single output file.
const source = outputMapConsumer.sources[0];
inputMapConsumer.eachMapping(function(mapping) {
const generatedPosition = outputMapConsumer.generatedPositionFor({
line: mapping.generatedLine,
column: mapping.generatedColumn,
source: source,
});
if (generatedPosition.column != null) {
mergedGenerator.addMapping({
source: mapping.source,
original:
mapping.source == null
? null
: {
line: mapping.originalLine,
column: mapping.originalColumn,
},
generated: generatedPosition,
name: mapping.name,
});
}
});
const mergedMap = mergedGenerator.toJSON();
inputMap.mappings = mergedMap.mappings;
return inputMap;
}

View File

@ -0,0 +1,319 @@
// @flow
import type { SourceMap } from "convert-source-map";
import sourceMap from "source-map";
export default function mergeSourceMap(
inputMap: SourceMap,
map: SourceMap,
): SourceMap {
const input = buildMappingData(inputMap);
const output = buildMappingData(map);
// Babel-generated maps always map to a single input filename.
if (output.sources.length !== 1) {
throw new Error("Assertion failure - expected a single output file");
}
const defaultSource = output.sources[0];
const mergedGenerator = new sourceMap.SourceMapGenerator();
for (const { source } of input.sources) {
if (typeof source.content === "string") {
mergedGenerator.setSourceContent(source.path, source.content);
}
}
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.
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;
}
return result;
}
function makeMappingKey(item: { line: number, columnStart: number }) {
return JSON.stringify([item.line, item.columnStart]);
}
function eachOverlappingGeneratedOutputRange(
outputFile: ResolvedFileMappings,
inputGeneratedRange: ResolvedGeneratedRange,
callback: ResolvedGeneratedRange => mixed,
) {
// 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: (
ResolvedGeneratedRange,
ResolvedOriginalRange,
ResolvedSource,
) => mixed,
) {
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,
sourceRoot: ?string,
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({
...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.
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: 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: 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;
}

View File

@ -0,0 +1,12 @@
{
"version": 3,
"sources": [
"HelloWorld.vue"
],
"names": [],
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA",
"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"
]
}

View File

@ -0,0 +1,113 @@
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

View File

@ -0,0 +1,5 @@
{
"plugins": [
"transform-modules-commonjs"
]
}

View File

@ -0,0 +1,120 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
var _default = {
name: 'HelloWorld',
data() {
return {
msg: 'Welcome to Your Vue.js App'
};
}
}; //
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
exports.default = _default;

View File

@ -0,0 +1,12 @@
{
"version": 3,
"sources": [
"HelloWorld.vue"
],
"names": [],
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAsFA;AACA,QAAA,YADA;;AAEA,SAAA;AACA,WAAA;AACA,WAAA;AADA,KAAA;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"
]
}

View File

@ -0,0 +1,72 @@
[
{
"generated": {
"line": 92,
"column": 15
},
"original": {
"line": 87,
"column": 0
}
},
{
"generated": {
"line": 93,
"column": 0
},
"original": {
"line": 88,
"column": 0
}
},
{
"generated": {
"line": 95,
"column": 9
},
"original": {
"line": 89,
"column": 0
}
},
{
"generated": {
"line": 96,
"column": 0
},
"original": {
"line": 90,
"column": 0
}
},
{
"generated": {
"line": 97,
"column": 0
},
"original": {
"line": 91,
"column": 0
}
},
{
"generated": {
"line": 98,
"column": 0
},
"original": {
"line": 90,
"column": 0
}
},
{
"generated": {
"line": 99,
"column": 0
},
"original": {
"line": 93,
"column": 0
}
}
]

View File

@ -1,11 +1,7 @@
{
"version": 3,
"mappings": "AAAA,UAAU,Y;SAAM,A;AAAC",
"mappings": "AAAA,IAAA,MAAU,Y;SAAM,C;AAAC,CAAjB",
"names": [],
"sources": [
"original.js"
],
"sourcesContent": [
"var foo = () => 4;"
]
"sources": ["original.js"],
"sourcesContent": ["var foo = () => 4;"],
"version": 3
}

View File

@ -196,6 +196,11 @@ export default function get(entryLoc): Array<Suite> {
if (fs.existsSync(sourceMapLoc)) {
test.sourceMap = JSON.parse(readFile(sourceMapLoc));
}
const inputMapLoc = taskDir + "/input-source-map.json";
if (fs.existsSync(inputMapLoc)) {
test.inputSourceMap = JSON.parse(readFile(inputMapLoc));
}
}
}

View File

@ -325,6 +325,7 @@ function run(task) {
sourceFileName: self.filename,
sourceType: "script",
babelrc: false,
inputSourceMap: task.inputSourceMap || undefined,
},
opts,
);