Skip to content

Commit 67b7419

Browse files
fix: allow devtool and SourceMapDevToolPlugin to coexist on the same asset (#21001)
1 parent 294197c commit 67b7419

8 files changed

Lines changed: 487 additions & 46 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Allow `devtool` and `SourceMapDevToolPlugin` (or multiple `SourceMapDevToolPlugin` instances) to coexist on the same asset. Previously the second instance would silently skip any asset whose `info.related.sourceMap` had already been set by an earlier instance, and even when it ran the asset had been rewrapped as a `RawSource` so no source map could be recovered — producing an empty `.map` file. The plugin now keeps a per-compilation stash of pristine source maps, namespaces its persistent cache entries by the options that affect output, and appends additional `related.sourceMap` entries instead of overwriting them. The classic workaround of pairing `devtool: 'hidden-source-map'` with a `new webpack.SourceMapDevToolPlugin({ filename: '[file].secondary.map', noSources: true })` now produces both maps in a single build.

lib/SourceMapDevToolPlugin.js

Lines changed: 247 additions & 46 deletions
Large diffs are not rendered by default.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use strict";
2+
3+
const fs = require("fs");
4+
const path = require("path");
5+
6+
const readFile = (filename) =>
7+
fs.readFileSync(path.join(__dirname, filename), "utf-8");
8+
const readMap = (filename) => JSON.parse(readFile(filename));
9+
10+
const DEBUG_ID_RE =
11+
/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i;
12+
13+
it("wraps a function `append` so the `debugId` comment prepends at call time", () => {
14+
const bundle = readFile("debug-fn.js");
15+
16+
// The append function was wrapped, not stringified — its dynamic URL
17+
// should appear in the bundle and the debug-id comment should be in front
18+
// of it, both produced at call time.
19+
const debugIdMatch = bundle.match(/\n\/\/# debugId\s*=\s*(\S+)/);
20+
expect(debugIdMatch).not.toBeNull();
21+
expect(DEBUG_ID_RE.test(debugIdMatch[1])).toBe(true);
22+
23+
expect(bundle).toMatch(
24+
"//# sourceMappingURL=https://example.invalid/debug-fn.js.map"
25+
);
26+
27+
// Make sure the function wasn't stringified into the output (would
28+
// contain `(pathData) =>` or similar source-text fragments).
29+
expect(bundle).not.toMatch(/=>\s*`\\n\/\/# source/);
30+
31+
const map = readMap("debug-fn.js.map");
32+
expect(map.debugId).toBe(debugIdMatch[1]);
33+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use strict";
2+
3+
const fs = require("fs");
4+
const path = require("path");
5+
6+
const readFile = (filename) =>
7+
fs.readFileSync(path.join(__dirname, filename), "utf-8");
8+
const readMap = (filename) => JSON.parse(readFile(filename));
9+
10+
const URL_COMMENT_RE = /\n\/\/# sourceMappingURL=(\S+)\s*$/;
11+
12+
it("supports two SourceMapDevToolPlugin instances on the same asset", () => {
13+
const bundle = readFile("dual.js");
14+
15+
// The first plugin uses append:false, the second appends, so only the
16+
// second plugin's URL ends up in the bundle.
17+
const match = URL_COMMENT_RE.exec(bundle);
18+
expect(match && match[1]).toBe("dual.js.nosources.map");
19+
20+
const fullMap = readMap("dual.js.full.map");
21+
expect(fullMap.version).toBe(3);
22+
expect(fullMap.sourcesContent).toBeDefined();
23+
expect(fullMap.sourcesContent.length).toBeGreaterThan(0);
24+
expect(fullMap.sources.some((s) => /dual\.js$/.test(s))).toBe(true);
25+
26+
const nosourcesMap = readMap("dual.js.nosources.map");
27+
expect(nosourcesMap.version).toBe(3);
28+
expect(nosourcesMap.sourcesContent).toBeUndefined();
29+
expect(nosourcesMap.sources.some((s) => /dual\.js$/.test(s))).toBe(true);
30+
// The two maps should describe the same generated file with the same
31+
// mappings — they only differ in whether sourcesContent is embedded.
32+
expect(nosourcesMap.mappings).toBe(fullMap.mappings);
33+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use strict";
2+
3+
const fs = require("fs");
4+
const path = require("path");
5+
6+
const readFile = (filename) =>
7+
fs.readFileSync(path.join(__dirname, filename), "utf-8");
8+
const readMap = (filename) => JSON.parse(readFile(filename));
9+
10+
const URL_COMMENT_RE = /\n\/\/# sourceMappingURL=(\S+)\s*$/;
11+
12+
it("emits both maps and only the explicit plugin appends a sourceMappingURL", () => {
13+
const bundle = readFile("primary.js");
14+
// hidden-source-map has append:false; only the user's plugin appends.
15+
const match = URL_COMMENT_RE.exec(bundle);
16+
expect(match && match[1]).toBe("primary.js.secondary.map");
17+
18+
// devtool=hidden-source-map produces the primary map (with sources).
19+
const primaryMap = readMap("primary.js.map");
20+
expect(primaryMap.version).toBe(3);
21+
expect(primaryMap.sourcesContent).toBeDefined();
22+
expect(primaryMap.sourcesContent.length).toBeGreaterThan(0);
23+
expect(primaryMap.sources.some((s) => /primary\.js$/.test(s))).toBe(true);
24+
25+
// The explicit plugin produces a separate nosources map.
26+
const secondaryMap = readMap("primary.js.secondary.map");
27+
expect(secondaryMap.version).toBe(3);
28+
expect(secondaryMap.sourcesContent).toBeUndefined();
29+
expect(secondaryMap.sources.some((s) => /primary\.js$/.test(s))).toBe(true);
30+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"use strict";
2+
3+
module.exports = {
4+
findBundle(i, options) {
5+
if (
6+
options &&
7+
options.output &&
8+
typeof options.output.filename === "string"
9+
) {
10+
return [options.output.filename];
11+
}
12+
13+
const bundles = ["primary.js", "dual.js", "triple.js", "debug-fn.js"];
14+
return bundles[i] ? [bundles[i]] : [];
15+
}
16+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"use strict";
2+
3+
const fs = require("fs");
4+
const path = require("path");
5+
6+
const readFile = (filename) =>
7+
fs.readFileSync(path.join(__dirname, filename), "utf-8");
8+
const readMap = (filename) => JSON.parse(readFile(filename));
9+
10+
const URL_COMMENT_RE = /\n\/\/# sourceMappingURL=(\S+)\s*$/;
11+
12+
it("supports three SourceMapDevToolPlugin instances on the same asset", () => {
13+
const bundle = readFile("triple.js");
14+
15+
// Only the last plugin appends (the first two use `append: false`).
16+
const match = URL_COMMENT_RE.exec(bundle);
17+
expect(match && match[1]).toBe("triple.js.c.map");
18+
19+
const a = readMap("triple.js.a.map");
20+
const b = readMap("triple.js.b.map");
21+
const c = readMap("triple.js.c.map");
22+
23+
expect(a.version).toBe(3);
24+
expect(b.version).toBe(3);
25+
expect(c.version).toBe(3);
26+
27+
// All three describe the same generated file, so the mappings line up.
28+
expect(b.mappings).toBe(a.mappings);
29+
expect(c.mappings).toBe(a.mappings);
30+
31+
// `noSources: true` only on the middle plugin.
32+
expect(a.sourcesContent).toBeDefined();
33+
expect(b.sourcesContent).toBeUndefined();
34+
expect(c.sourcesContent).toBeDefined();
35+
36+
// `sourceRoot: "triple"` only on the last plugin.
37+
expect(c.sourceRoot).toBe("triple");
38+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"use strict";
2+
3+
const webpack = require("../../../../");
4+
5+
/** @type {import("../../../../").Configuration[]} */
6+
module.exports = [
7+
// 1. `devtool: 'hidden-source-map'` (no sourceMappingURL appended) +
8+
// a `SourceMapDevToolPlugin` that emits a second nosources map and appends
9+
// its URL. This is the exact configuration sokra suggested in #6813.
10+
{
11+
devtool: "hidden-source-map",
12+
entry: "./primary.js",
13+
output: {
14+
filename: "primary.js"
15+
},
16+
plugins: [
17+
new webpack.SourceMapDevToolPlugin({
18+
filename: "[file].secondary.map",
19+
noSources: true
20+
})
21+
]
22+
},
23+
// 2. `devtool: false` + two `SourceMapDevToolPlugin` instances writing to
24+
// different filenames for the same asset.
25+
{
26+
devtool: false,
27+
entry: "./dual.js",
28+
output: {
29+
filename: "dual.js"
30+
},
31+
plugins: [
32+
new webpack.SourceMapDevToolPlugin({
33+
filename: "[file].full.map",
34+
append: false
35+
}),
36+
new webpack.SourceMapDevToolPlugin({
37+
filename: "[file].nosources.map",
38+
noSources: true
39+
})
40+
]
41+
},
42+
// 3. Three `SourceMapDevToolPlugin` instances on the same asset — exercises
43+
// the `related.sourceMap` array-merge path where the asset info already
44+
// has an array (rather than a single string) from the prior two plugins.
45+
{
46+
devtool: false,
47+
entry: "./triple.js",
48+
output: {
49+
filename: "triple.js"
50+
},
51+
plugins: [
52+
new webpack.SourceMapDevToolPlugin({
53+
filename: "[file].a.map",
54+
append: false
55+
}),
56+
new webpack.SourceMapDevToolPlugin({
57+
filename: "[file].b.map",
58+
append: false,
59+
noSources: true
60+
}),
61+
new webpack.SourceMapDevToolPlugin({
62+
filename: "[file].c.map",
63+
sourceRoot: "triple"
64+
})
65+
]
66+
},
67+
// 4. `debugIds: true` combined with a function `append` — exercises the
68+
// branch that wraps the user's callback so the `//# debugId=` comment is
69+
// prepended at call time instead of stringifying the function.
70+
{
71+
devtool: false,
72+
entry: "./debug-fn.js",
73+
output: {
74+
filename: "debug-fn.js"
75+
},
76+
plugins: [
77+
new webpack.SourceMapDevToolPlugin({
78+
filename: "[file].map",
79+
debugIds: true,
80+
append: (pathData) =>
81+
`\n//# sourceMappingURL=https://example.invalid/${pathData.filename}.map`
82+
})
83+
]
84+
}
85+
];

0 commit comments

Comments
 (0)