Skip to content

Commit 66bdeec

Browse files
committed
fix: Source maps & error positioning
1 parent 0bf98c5 commit 66bdeec

File tree

6 files changed

+164
-43
lines changed

6 files changed

+164
-43
lines changed

src/plugins/prerender-plugin.js

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -104,28 +104,33 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere
104104
name: 'vite-prerender-plugin',
105105
apply: 'build',
106106
enforce: 'post',
107-
configResolved(config) {
108-
userEnabledSourceMaps = !!config.build.sourcemap;
107+
// Vite is pretty inconsistent with how it resolves config options, both
108+
// hooks are needed to set their respective options. ¯\_(ツ)_/¯
109+
config(config) {
110+
userEnabledSourceMaps = !!config.build?.sourcemap;
111+
109112
// Enable sourcemaps for generating more actionable error messages
113+
config.build ??= {};
110114
config.build.sourcemap = true;
115+
},
116+
configResolved(config) {
117+
// We're only going to alter the chunking behavior in the default cases, where the user and/or
118+
// other plugins haven't already configured this. It'd be impossible to avoid breakages otherwise.
119+
if (
120+
Array.isArray(config.build.rollupOptions.output) ||
121+
config.build.rollupOptions.output?.manualChunks
122+
) {
123+
return;
124+
}
111125

112-
viteConfig = config;
126+
config.build.rollupOptions.output ??= {};
127+
config.build.rollupOptions.output.manualChunks = (id) => {
128+
if (id.includes(prerenderScript) || id.includes(preloadPolyfillId)) {
129+
return "index";
130+
}
131+
};
113132

114-
// We're only going to alter the chunking behavior in the default cases, where the user and/or
115-
// other plugins haven't already configured this. It'd be impossible to avoid breakages otherwise.
116-
if (
117-
Array.isArray(config.build.rollupOptions.output) ||
118-
config.build.rollupOptions.output?.manualChunks
119-
) {
120-
return;
121-
}
122-
123-
config.build.rollupOptions.output ??= {};
124-
config.build.rollupOptions.output.manualChunks = (id) => {
125-
if (id.includes(prerenderScript) || id.includes(preloadPolyfillId)) {
126-
return "index";
127-
}
128-
};
133+
viteConfig = config;
129134
},
130135
async options(opts) {
131136
if (!opts.input) return;
@@ -226,13 +231,18 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere
226231
);
227232
let htmlDoc = htmlParse(tpl);
228233

234+
// Workaround for PNPM mangling file paths with their symlinks
235+
const tmpDirRelative = path.join(
236+
'node_modules',
237+
'vite-prerender-plugin',
238+
'headless-prerender',
239+
);
240+
229241
// Create a tmp dir to allow importing & consuming the built modules,
230242
// before Rollup writes them to the disk
231243
const tmpDir = path.join(
232244
viteConfig.root,
233-
'node_modules',
234-
'vite-prerender-plugin',
235-
'headless-prerender',
245+
tmpDirRelative,
236246
);
237247
try {
238248
await fs.rm(tmpDir, { recursive: true });
@@ -249,26 +259,6 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere
249259
/** @type {OutputChunk | undefined} */
250260
let prerenderEntry;
251261
for (const output of Object.keys(bundle)) {
252-
// Clean up source maps if the user didn't enable them themselves
253-
if (!userEnabledSourceMaps) {
254-
if (output.endsWith('.map')) {
255-
delete bundle[output];
256-
continue;
257-
}
258-
if (output.endsWith('.js')) {
259-
if (bundle[output].type == 'chunk') {
260-
bundle[output].code = bundle[output].code.replace(
261-
/\n\/\/#\ssourceMappingURL=.*/,
262-
'',
263-
);
264-
} else {
265-
// Workers and similar
266-
bundle[output].source = /** @type {string} */ (
267-
bundle[output].source
268-
).replace(/\n\/\/#\ssourceMappingURL=.*/, '');
269-
}
270-
}
271-
}
272262
if (!output.endsWith('.js') || bundle[output].type !== 'chunk') continue;
273263

274264
await fs.writeFile(
@@ -310,7 +300,7 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere
310300
}
311301
`.replace(/^\t{5}/gm, '');
312302

313-
const stack = StackTraceParse(e).find((s) => s.getFileName().includes(tmpDir));
303+
const stack = StackTraceParse(e).find((s) => s.getFileName().includes(tmpDirRelative));
314304

315305
const sourceMapContent = prerenderEntry.map;
316306
if (stack && sourceMapContent) {
@@ -443,13 +433,31 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere
443433

444434
// Add generated HTML to compilation:
445435
route.url == '/'
446-
? /** @type {OutputAsset} */ ((bundle['index.html']).source =
436+
? (/** @type {OutputAsset} */ (bundle['index.html']).source =
447437
htmlDoc.toString())
448438
: this.emitFile({
449439
type: 'asset',
450440
fileName: assetName,
451441
source: htmlDoc.toString(),
452442
});
443+
444+
// Clean up source maps if the user didn't enable them themselves
445+
if (!userEnabledSourceMaps) {
446+
for (const output of Object.keys(bundle)) {
447+
if (output.endsWith('.map')) {
448+
delete bundle[output];
449+
continue;
450+
}
451+
452+
if (output.endsWith('.js')) {
453+
const codeOrSource = bundle[output].type == 'chunk' ? 'code' : 'source';
454+
bundle[output][codeOrSource] = bundle[output][codeOrSource].replace(
455+
/\n\/\/#\ssourceMappingURL=.*/,
456+
'',
457+
);
458+
}
459+
}
460+
}
453461
}
454462
},
455463
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
</head>
6+
<body>
7+
<script prerender type="module" src="/src/index.js"></script>
8+
</body>
9+
</html>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
if (typeof window !== "undefined") {
2+
const worker = new Worker(new URL("./worker.js", import.meta.url));
3+
4+
worker.postMessage({ type: "init" });
5+
}
6+
7+
export async function prerender() {
8+
return `<h1>Simple Test Result</h1>`;
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
addEventListener('message', (e) => {
2+
postMessage({ type: 'init' });
3+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineConfig } from 'vite';
2+
import { vitePrerenderPlugin } from 'vite-prerender-plugin';
3+
4+
export default defineConfig({
5+
plugins: [vitePrerenderPlugin()],
6+
});

tests/source-maps.test.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { test } from 'uvu';
2+
import * as assert from 'uvu/assert';
3+
import path from 'node:path';
4+
import { promises as fs } from 'node:fs';
5+
6+
import { setupTest, teardownTest, loadFixture, viteBuild } from './lib/lifecycle.js';
7+
import { getOutputFile, outputFileExists, writeFixtureFile } from './lib/utils.js';
8+
9+
const writeConfig = async (dir, content) => writeFixtureFile(dir, 'vite.config.js', content);
10+
11+
let env;
12+
test.before.each(async () => {
13+
env = await setupTest();
14+
});
15+
16+
test.after.each(async () => {
17+
await teardownTest(env);
18+
});
19+
20+
test('Should strip sourcemaps by default', async () => {
21+
await loadFixture('source-maps', env);
22+
await viteBuild(env.tmp.path);
23+
24+
const outDir = path.join(env.tmp.path, 'dist', 'assets');
25+
const outDirAssets = await fs.readdir(outDir);
26+
27+
assert.not.ok(outDirAssets.find((f) => f.endsWith('.map')));
28+
29+
30+
const outputChunk = path.join(outDir, outDirAssets.find((f) => /^index-.*\.js$/.test(f)));
31+
const outputChunkCode = await fs.readFile(outputChunk, 'utf-8');
32+
assert.is(outputChunkCode.match(/\/\/#\ssourceMappingURL=(.*)/), null)
33+
34+
const outputAsset = path.join(outDir, outDirAssets.find((f) => /^worker-.*\.js$/.test(f)));
35+
const outputAssetSource = await fs.readFile(outputAsset, 'utf-8');
36+
assert.is(outputAssetSource.match(/\/\/#\ssourceMappingURL=(.*)/), null)
37+
});
38+
39+
test('Should preserve sourcemaps if user has enabled them', async () => {
40+
await loadFixture('simple', env);
41+
await writeConfig(env.tmp.path, `
42+
import { defineConfig } from 'vite';
43+
import { vitePrerenderPlugin } from 'vite-prerender-plugin';
44+
45+
export default defineConfig({
46+
build: { sourcemap: true },
47+
plugins: [vitePrerenderPlugin()],
48+
});
49+
`);
50+
51+
await viteBuild(env.tmp.path);
52+
53+
const outDir = path.join(env.tmp.path, 'dist', 'assets');
54+
const outDirAssets = await fs.readdir(outDir);
55+
56+
const outputJsFileName = outDirAssets.find((f) => f.endsWith('.js'));
57+
assert.ok(outputJsFileName);
58+
const outputJs = await fs.readFile(path.join(outDir, outputJsFileName), 'utf-8');
59+
assert.match(outputJs, '//# sourceMappingURL=');
60+
61+
const outputMap = outputJs.match(/\/\/#\ssourceMappingURL=(.*)/)[1];
62+
assert.ok(outDirAssets.includes(outputMap));
63+
});
64+
65+
test('Should use sourcemaps to display error positioning when possible', async () => {
66+
await loadFixture('simple', env);
67+
await writeFixtureFile(env.tmp.path, 'src/index.js', `
68+
document.createElement('div');
69+
export async function prerender() {
70+
return '<h1>Simple Test Result</h1>';
71+
}`
72+
);
73+
74+
let message = '';
75+
try {
76+
await viteBuild(env.tmp.path);
77+
} catch (error) {
78+
message = error.message;
79+
}
80+
81+
assert.match(message, 'ReferenceError: document is not defined');
82+
assert.match(message, 'src/index.js:2:9');
83+
});
84+
85+
test.run();
86+

0 commit comments

Comments
 (0)