Skip to content

Commit dc481b1

Browse files
Merge pull request #715 from preactjs/sourcmap-infra
2 parents 945591d + e093373 commit dc481b1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1010
-84
lines changed

.changeset/mighty-ligers-beam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'wmr': patch
3+
---
4+
5+
Rewrite internal source map handling. This adds full support for source maps during `development` and `production` and ensures that `.map` files are served correctly.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"demo": "yarn workspace @examples/demo run",
1717
"docs": "yarn workspace docs",
1818
"iso": "yarn workspace preact-iso",
19-
"ci": "yarn wmr build && yarn --check-files && yarn demo build:prod"
19+
"ci": "yarn wmr build && yarn --check-files && yarn demo build:prod",
20+
"postinstall": "patch-package --exclude 'nothing'"
2021
},
2122
"eslintConfig": {
2223
"extends": [
@@ -91,6 +92,7 @@
9192
"eslint-plugin-prettier": "^3.1.4",
9293
"husky": "^4.2.5",
9394
"lint-staged": "^10.2.11",
95+
"patch-package": "^6.4.7",
9496
"prettier": "^2.0.5"
9597
}
9698
}

packages/wmr/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"semver": "^7.3.2",
8383
"simple-code-frame": "^1.1.1",
8484
"sirv": "^1.0.6",
85+
"sourcemap-codec": "^1.4.8",
8586
"stylis": "^4.0.10",
8687
"sucrase": "^3.17.0",
8788
"tar-stream": "^2.1.3",

packages/wmr/src/lib/acorn-traverse.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as jsxWalk from 'acorn-jsx-walk';
33
import MagicString from 'magic-string';
44
import * as astringLib from 'astring';
55
import { codeFrame } from './output-utils.js';
6+
import { posix } from 'path';
67

78
/**
89
* @fileoverview
@@ -100,7 +101,16 @@ let codeGenerator = {
100101
// import(source)
101102
ImportExpression(node, state) {
102103
state.write('import(');
103-
this[node.source.type](node.source, state);
104+
105+
// TODO: Sometimes this seems to have a source and sometimes
106+
// an expression. I don't understand why. The expression seems
107+
// to be only set when calling `t.importExpression()`
108+
if (node.source) {
109+
this[node.source.type](node.source, state);
110+
} else {
111+
this[node.expression.type](node.expression, state);
112+
}
113+
104114
state.write(')');
105115
},
106116
JSXFragment(node, state) {
@@ -745,8 +755,10 @@ export function transform(
745755
function getSourceMap() {
746756
if (!map) {
747757
map = out.generateMap({
748-
includeContent: false,
749-
source: sourceFileName
758+
includeContent: true,
759+
// Must be set for most source map verifiers to work
760+
source: sourceFileName || filename,
761+
file: posix.basename(sourceFileName || filename || '')
750762
});
751763
}
752764
return map;

packages/wmr/src/lib/normalize-options.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export async function normalizeOptions(options, mode, configWatchFiles = []) {
2121

2222
options.root = options.cwd;
2323

24-
options.sourcemap = false;
2524
options.minify = mode === 'build';
2625
options.plugins = [];
2726
options.output = [];

packages/wmr/src/lib/npm-middleware.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ async function bundleNpmModule(mod, { source, alias, cwd }) {
142142
aliasPlugin({ alias, cwd }),
143143
npmProviderPlugin,
144144
processGlobalPlugin({
145+
sourcemap: false,
145146
NODE_ENV: 'development'
146147
}),
147148
commonjs({

packages/wmr/src/lib/plugins.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,22 @@ export function getPlugins(options) {
5353
production
5454
}),
5555
// Transpile import assertion syntax to WMR prefixes
56-
importAssertionPlugin(),
56+
importAssertionPlugin({ sourcemap }),
5757
production &&
5858
(dynamicImportVars.default || dynamicImportVars)({
5959
include: /\.(m?jsx?|tsx?)$/,
6060
exclude: /\/node_modules\//
6161
}),
6262
production && publicPathPlugin({ publicPath }),
6363
sassPlugin({ production, sourcemap, root }),
64-
wmrStylesPlugin({ hot: !production, root, production, alias }),
64+
wmrStylesPlugin({ hot: !production, root, production, alias, sourcemap }),
6565
processGlobalPlugin({
66+
sourcemap,
6667
env,
6768
NODE_ENV: production ? 'production' : 'development'
6869
}),
69-
htmPlugin({ production }),
70-
wmrPlugin({ hot: !production, preact: features.preact }),
70+
htmPlugin({ production, sourcemap: options.sourcemap }),
71+
wmrPlugin({ hot: !production, preact: features.preact, sourcemap: options.sourcemap }),
7172
fastCjsPlugin({
7273
// Only transpile CommonJS in node_modules and explicit .cjs files:
7374
include: /(^npm\/|[/\\]node_modules[/\\]|\.cjs$)/

packages/wmr/src/lib/rollup-plugin-container.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { createHash } from 'crypto';
33
import { promises as fs } from 'fs';
44
import * as acorn from 'acorn';
55
import * as kl from 'kolorist';
6-
import { debug, formatResolved, formatPath } from './output-utils.js';
6+
import { debug, formatResolved, formatPath, hasDebugFlag } from './output-utils.js';
7+
import { mergeSourceMaps } from './sourcemap.js';
78

89
// Rollup respects "module", Node 14 doesn't.
910
const cjsDefault = m => ('default' in m ? m.default : m);
@@ -45,7 +46,7 @@ function identifierPair(id, importer) {
4546

4647
/**
4748
* @param {Plugin[]} plugins
48-
* @param {import('rollup').InputOptions & PluginContainerOptions} [opts]
49+
* @param {import('rollup').InputOptions & PluginContainerOptions & {sourcemap?: boolean}} [opts]
4950
*/
5051
export function createPluginContainer(plugins, opts = {}) {
5152
if (!Array.isArray(plugins)) plugins = [plugins];
@@ -279,19 +280,42 @@ export function createPluginContainer(plugins, opts = {}) {
279280
* @param {string} id
280281
*/
281282
async transform(code, id) {
283+
/** @type {import('./sourcemap.js').SourceMap[]} */
284+
const sourceMaps = [];
285+
282286
for (plugin of plugins) {
283287
if (!plugin.transform) continue;
284288
const result = await plugin.transform.call(ctx, code, id);
285289
if (!result) continue;
286290

287291
logTransform(`${kl.dim(formatPath(id))} [${plugin.name}]`);
288292
if (typeof result === 'object') {
293+
if (result.map) {
294+
// Normalize source map sources URLs for the browser
295+
result.map.sources = result.map.sources.map(s => {
296+
if (typeof s === 'string') {
297+
return `/${posix.normalize(s)}`;
298+
} else if (hasDebugFlag()) {
299+
logTransform(kl.yellow(`Invalid source map returned by plugin `) + kl.magenta(plugin.name));
300+
}
301+
302+
return s;
303+
});
304+
305+
sourceMaps.push(result.map);
306+
} else if (opts.sourcemap && result.code !== code) {
307+
logTransform(kl.yellow(`Missing sourcemap result in transform() method of `) + kl.magenta(plugin.name));
308+
}
309+
289310
code = result.code;
290311
} else {
312+
if (opts.sourcemap && code !== result) {
313+
logTransform(kl.yellow(`Missing sourcemap result in transform() method of `) + kl.magenta(plugin.name));
314+
}
291315
code = result;
292316
}
293317
}
294-
return code;
318+
return { code, map: sourceMaps.length ? mergeSourceMaps(sourceMaps) : null };
295319
},
296320

297321
/**

packages/wmr/src/lib/sourcemap.js

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { encode, decode } from 'sourcemap-codec';
2+
3+
/**
4+
* @typedef {{ version: number, file?: string, sourceRoot?: string, sources: string[], sourcesContent: Array<string | null>, names: string[], mappings: string}} SourceMap
5+
*/
6+
7+
/**
8+
*
9+
* @param {SourceMap[]} maps
10+
* @param {import('sourcemap-codec').SourceMapMappings[]} parsedMappings
11+
* @param {Map<string, { index: number, content: string | null}>} sourcesCache
12+
* @param {number} source
13+
* @param {number} line
14+
* @param {number} column
15+
* @returns {{ line: number, column: number, source: number }}
16+
*/
17+
function getOriginalPosition(maps, parsedMappings, sourcesCache, source, line, column) {
18+
let originalLine = line;
19+
let originalColumn = column;
20+
let originalSource = source;
21+
22+
// Traverse maps backwards, but skip last map as that's the
23+
// one we requested the position from
24+
for (let i = parsedMappings.length - 1; i >= 0; i--) {
25+
const map = parsedMappings[i];
26+
27+
const segments = map[originalLine];
28+
if (!segments) break;
29+
30+
// TODO: Binary search
31+
let lastFound;
32+
for (let j = 0; j < segments.length; j++) {
33+
const segment = segments[j];
34+
if (segment[0] === originalColumn) {
35+
if (segment.length === 1) {
36+
break;
37+
}
38+
39+
const sourceName = maps[i].sources[segment[1]];
40+
const mappedName = sourcesCache.get(sourceName);
41+
if (!mappedName) {
42+
originalSource = 0;
43+
} else {
44+
originalSource = mappedName.index;
45+
}
46+
47+
originalLine = segment[2];
48+
originalColumn = segment[3];
49+
break;
50+
} else if (segment[0] < originalColumn) {
51+
lastFound = segment;
52+
}
53+
}
54+
55+
if (lastFound !== undefined) {
56+
if (lastFound.length === 1) {
57+
break;
58+
}
59+
60+
const sourceName = maps[i].sources[lastFound[1]];
61+
const mappedName = sourcesCache.get(sourceName);
62+
if (!mappedName) {
63+
originalSource = 0;
64+
} else {
65+
originalSource = mappedName.index;
66+
}
67+
68+
originalLine = lastFound[2];
69+
originalColumn = lastFound[3] + (originalColumn - lastFound[3]);
70+
}
71+
}
72+
73+
return {
74+
line: originalLine,
75+
column: originalColumn,
76+
source: originalSource
77+
};
78+
}
79+
80+
/**
81+
* Combine an array of sourcemaps into one
82+
* @param {SourceMap[]} sourceMaps
83+
* @returns {SourceMap}
84+
*/
85+
export function mergeSourceMaps(sourceMaps) {
86+
let file;
87+
let sourceRoot;
88+
89+
/** @type {import('sourcemap-codec').SourceMapMappings[]} */
90+
const parsedMappings = [];
91+
92+
/** @type {Map<string, number>} */
93+
const names = new Map();
94+
95+
/** @type {Map<string, { index: number, content: string | null}>} */
96+
const sourcesCache = new Map();
97+
98+
for (let i = 0; i < sourceMaps.length; i++) {
99+
const map = sourceMaps[i];
100+
101+
if (map.file) {
102+
file = map.file;
103+
}
104+
105+
if (map.sourceRoot) {
106+
sourceRoot = map.sourceRoot;
107+
}
108+
109+
for (let j = 0; j < map.sources.length; j++) {
110+
const source = map.sources[j];
111+
if (!sourcesCache.has(source)) {
112+
sourcesCache.set(source, { index: sourcesCache.size, content: map.sourcesContent[j] || null });
113+
}
114+
}
115+
116+
for (let j = 0; j < map.names.length; j++) {
117+
const name = map.names[j];
118+
if (!names.has(name)) {
119+
names.set(name, names.size - 1);
120+
}
121+
}
122+
123+
// Merge mappings
124+
parsedMappings.push(decode(map.mappings));
125+
}
126+
127+
const sources = [];
128+
const sourcesContent = [];
129+
for (const [key, value] of sourcesCache.entries()) {
130+
sources.push(key);
131+
sourcesContent.push(value.content);
132+
}
133+
134+
/** @type {import('sourcemap-codec').SourceMapMappings} */
135+
const outMappings = [];
136+
const lastMap = parsedMappings[parsedMappings.length - 1];
137+
138+
// Loop over the mappings of the last source map and retrieve
139+
// original position for each mapping segment
140+
for (let i = 0; i < lastMap.length; i++) {
141+
const line = lastMap[i];
142+
143+
/** @type {import('sourcemap-codec').SourceMapSegment[]} */
144+
const rewrittenSegments = [];
145+
for (let j = 0; j < line.length; j++) {
146+
const segment = line[j];
147+
148+
if (segment.length === 4) {
149+
const original = getOriginalPosition(
150+
sourceMaps,
151+
parsedMappings,
152+
sourcesCache,
153+
segment[1],
154+
segment[2],
155+
segment[3]
156+
);
157+
rewrittenSegments.push([segment[0], original.source, original.line, original.column]);
158+
} else if (segment.length === 5) {
159+
const original = getOriginalPosition(
160+
sourceMaps,
161+
parsedMappings,
162+
sourcesCache,
163+
segment[1],
164+
segment[2],
165+
segment[3]
166+
);
167+
rewrittenSegments.push([segment[0], original.source, original.line, original.column, segment[4]]);
168+
} else {
169+
rewrittenSegments.push(segment);
170+
}
171+
}
172+
173+
outMappings.push(rewrittenSegments);
174+
}
175+
176+
return {
177+
version: 3,
178+
file,
179+
names: Array.from(names.keys()),
180+
sourceRoot,
181+
sources,
182+
sourcesContent,
183+
mappings: encode(outMappings)
184+
};
185+
}

0 commit comments

Comments
 (0)