Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add getCombinedSourceMap in transform plugin context (#2983) #2993

Merged
merged 5 commits into from Jul 15, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/05-plugin-development.md
Expand Up @@ -414,6 +414,10 @@ Use the second form if you need to add additional properties to your warning obj

The `position` argument is a character index where the warning was raised. If present, Rollup will augment the warning object with `pos`, `loc` (a standard `{ file, line, column }` object) and `frame` (a snippet of code showing the error).

### `this.getCombinedSourceMap() => SourceMap`
billowz marked this conversation as resolved.
Show resolved Hide resolved

Get the combined source maps of all previous plugins. This context function can only be used in [`transform`](guide/en/#transform) plugin hook.

### Deprecated Context Functions

☢️ These context utility functions have been deprecated and may be removed in a future Rollup version.
Expand Down
2 changes: 1 addition & 1 deletion src/Chunk.ts
Expand Up @@ -23,7 +23,7 @@ import {
} from './rollup/types';
import { Addons } from './utils/addons';
import { toBase64 } from './utils/base64';
import collapseSourcemaps from './utils/collapseSourcemaps';
import { collapseSourcemaps } from './utils/collapseSourcemaps';
import { deconflictChunk } from './utils/deconflictChunk';
import { error } from './utils/error';
import { sortByExecutionOrder } from './utils/executionOrder';
Expand Down
105 changes: 71 additions & 34 deletions src/utils/collapseSourcemaps.ts
@@ -1,5 +1,6 @@
import { DecodedSourceMap, SourceMap } from 'magic-string';
import Chunk from '../Chunk';
import Graph from '../Graph';
import Module from '../Module';
import {
DecodedSourceMapOrMissing,
Expand Down Expand Up @@ -145,20 +146,13 @@ class Link {
}
}

export default function collapseSourcemaps(
bundle: Chunk,
file: string,
map: DecodedSourceMap,
modules: Module[],
bundleSourcemapChain: DecodedSourceMapOrMissing[],
excludeContent: boolean
) {
function linkMap(source: Source | Link, map: DecodedSourceMapOrMissing) {
function linkMapper(graph: Graph) {
billowz marked this conversation as resolved.
Show resolved Hide resolved
return function linkMap(source: Source | Link, map: DecodedSourceMapOrMissing) {
if (map.mappings) {
return new Link(map, [source]);
}

bundle.graph.warn({
graph.warn({
code: 'SOURCEMAP_BROKEN',
message: `Sourcemap is likely to be incorrect: a plugin${
map.plugin ? ` ('${map.plugin}')` : ``
Expand All @@ -174,34 +168,55 @@ export default function collapseSourcemaps(
},
[source]
);
};
}

function unionSourcemaps(
billowz marked this conversation as resolved.
Show resolved Hide resolved
id: string,
originalCode: string,
originalSourcemap: ExistingDecodedSourceMap | null,
sourcemapChain: DecodedSourceMapOrMissing[],
linkMap: (source: Source | Link, map: DecodedSourceMapOrMissing) => Link
) {
billowz marked this conversation as resolved.
Show resolved Hide resolved
let source: Source | Link;
if (!originalSourcemap) {
source = new Source(id, originalCode);
} else {
const sources = originalSourcemap.sources;
const sourcesContent = originalSourcemap.sourcesContent || [];

// TODO indiscriminately treating IDs and sources as normal paths is probably bad.
const directory = dirname(id) || '.';
const sourceRoot = originalSourcemap.sourceRoot || '.';

const baseSources = sources.map(
(source, i) => new Source(resolve(directory, sourceRoot, source), sourcesContent[i])
);
source = new Link(originalSourcemap, baseSources);
}
return sourcemapChain.reduce(linkMap, source);
}

export function collapseSourcemaps(
bundle: Chunk,
file: string,
map: DecodedSourceMap,
modules: Module[],
bundleSourcemapChain: DecodedSourceMapOrMissing[],
excludeContent: boolean
) {
const linkMap = linkMapper(bundle.graph);
const moduleSources = modules
.filter(module => !module.excludeFromSourcemap)
.map(module => {
let source: Source | Link;
const originalSourcemap = module.originalSourcemap;
if (!originalSourcemap) {
source = new Source(module.id, module.originalCode);
} else {
const sources = originalSourcemap.sources;
const sourcesContent = originalSourcemap.sourcesContent || [];

// TODO indiscriminately treating IDs and sources as normal paths is probably bad.
const directory = dirname(module.id) || '.';
const sourceRoot = originalSourcemap.sourceRoot || '.';

const baseSources = sources.map(
(source, i) => new Source(resolve(directory, sourceRoot, source), sourcesContent[i])
);

source = new Link(originalSourcemap, baseSources);
}

source = module.sourcemapChain.reduce(linkMap, source);

return source;
});
.map(module =>
unionSourcemaps(
module.id,
module.originalCode,
module.originalSourcemap,
module.sourcemapChain,
linkMap
)
);

// DecodedSourceMap (from magic-string) uses a number[] instead of the more
// correct SourceMapSegment tuples. Cast it here to gain type safety.
Expand All @@ -221,3 +236,25 @@ export default function collapseSourcemaps(

return new SourceMap({ file, sources, sourcesContent, names, mappings });
}

export function collapseSourcemap(
graph: Graph,
id: string,
originalCode: string,
originalSourcemap: ExistingDecodedSourceMap,
sourcemapChain: DecodedSourceMapOrMissing[]
) {
if (!sourcemapChain.length) {
return originalSourcemap;
}

const source = unionSourcemaps(
id,
originalCode,
originalSourcemap,
sourcemapChain,
linkMapper(graph)
) as Link;

return source.traceMappings();
}
26 changes: 25 additions & 1 deletion src/utils/transform.ts
@@ -1,10 +1,12 @@
import MagicString, { DecodedSourceMap, SourceMap } from 'magic-string';
import Graph from '../Graph';
import Module from '../Module';
import {
Asset,
DecodedSourceMapOrMissing,
EmitAsset,
EmittedChunk,
ExistingDecodedSourceMap,
Plugin,
PluginCache,
PluginContext,
Expand All @@ -15,6 +17,7 @@ import {
TransformSourceDescription
} from '../rollup/types';
import { createTransformEmitAsset } from './assetHooks';
import { collapseSourcemap } from './collapseSourcemaps';
import { decodedSourcemap } from './decodedSourcemap';
import { augmentCodeLocation, error } from './error';
import { dirname, resolve } from './path';
Expand All @@ -28,7 +31,7 @@ export default function transform(
const id = module.id;
const sourcemapChain: DecodedSourceMapOrMissing[] = [];

const originalSourcemap = source.map === null ? null : decodedSourcemap(source.map);
let originalSourcemap = source.map === null ? null : decodedSourcemap(source.map);
const baseEmitAsset = graph.pluginDriver.emitAsset;
const originalCode = source.code;
let ast = source.ast;
Expand Down Expand Up @@ -159,6 +162,27 @@ export default function transform(
setAssetSourceErr = err;
}
}
},
getCombinedSourceMap() {
const combinedMap = collapseSourcemap(
graph,
id,
originalCode,
originalSourcemap as ExistingDecodedSourceMap,
sourcemapChain
);
if (!combinedMap) {
const magicString = new MagicString(originalCode);
return magicString.generateMap({ includeContent: true, hires: true, source: id });
}
if (originalSourcemap !== combinedMap) {
originalSourcemap = { version: 3, ...combinedMap };
sourcemapChain.length = 0;
}
return new SourceMap({
...combinedMap,
file: null as any
} as DecodedSourceMap);
}
};
}
Expand Down
97 changes: 97 additions & 0 deletions test/sourcemaps/samples/combined-sourcemap-with-loader/_config.js
@@ -0,0 +1,97 @@
const fs = require('fs');
const buble = require('buble');
const MagicString = require('magic-string');
const assert = require('assert');
const getLocation = require('../../getLocation');
const SourceMapConsumer = require('source-map').SourceMapConsumer;

module.exports = {
description: 'get combined sourcemap in transforming with loader',
options: {
plugins: [
{
load(id) {
const code = fs.readFileSync(id, 'utf-8');
const out = buble.transform(code, {
transforms: { modules: false },
sourceMap: true,
source: id
});

return { code: out.code, map: out.map };
}
},
{
transform(code, id) {
const sourcemap = this.getCombinedSourceMap();
const smc = new SourceMapConsumer(sourcemap);
const s = new MagicString(code);

if (/foo.js$/.test(id)) {
testFoo(code, smc);

s.prepend('console.log("foo start");\n\n');
s.append('\nconsole.log("foo end");');
} else {
testMain(code, smc);

s.appendRight(code.indexOf('console'), 'console.log("main start");\n\n');
s.append('\nconsole.log("main end");');
}

return {
code: s.toString(),
map: s.generateMap({ hires: true })
};
}
},
{
transform(code, id) {
const sourcemap = this.getCombinedSourceMap();
const smc = new SourceMapConsumer(sourcemap);
const s = new MagicString(code);

if (/foo.js$/.test(id)) {
testFoo(code, smc);

s.prepend('console.log("-- foo ---");\n\n');
s.append('\nconsole.log("-----");');
} else {
testMain(code, smc);

s.appendRight(code.indexOf('console'), 'console.log("-- main ---");\n\n');
s.append('\nconsole.log("-----");');
}

return {
code: s.toString(),
map: s.generateMap({ hires: true })
};
}
}
]
},
test(code, map) {
const smc = new SourceMapConsumer(map);
testFoo(code, smc);
testMain(code, smc);
}
};

function testFoo(code, smc) {
const generatedLoc = getLocation(code, code.indexOf(42));
const originalLoc = smc.originalPositionFor(generatedLoc);

assert.ok(/foo/.test(originalLoc.source));
assert.equal(originalLoc.line, 1);
assert.equal(originalLoc.column, 25);
}

function testMain(code, smc) {
const generatedLoc = getLocation(code, code.indexOf('info'));
const originalLoc = smc.originalPositionFor(generatedLoc);

assert.ok(/main/.test(originalLoc.source));
assert.equal(originalLoc.line, 3);
assert.equal(originalLoc.column, 8);
}
@@ -0,0 +1 @@
export const foo = () => 42;
@@ -0,0 +1,3 @@
import { foo } from './foo';

console.info( `the answer is ${foo()}` );