Skip to content

Commit

Permalink
fix: Source maps & error positioning
Browse files Browse the repository at this point in the history
  • Loading branch information
rschristian committed Dec 30, 2024
1 parent 0bf98c5 commit 66bdeec
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 43 deletions.
94 changes: 51 additions & 43 deletions src/plugins/prerender-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,28 +104,33 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere
name: 'vite-prerender-plugin',
apply: 'build',
enforce: 'post',
configResolved(config) {
userEnabledSourceMaps = !!config.build.sourcemap;
// Vite is pretty inconsistent with how it resolves config options, both
// hooks are needed to set their respective options. ¯\_(ツ)_/¯
config(config) {
userEnabledSourceMaps = !!config.build?.sourcemap;

// Enable sourcemaps for generating more actionable error messages
config.build ??= {};
config.build.sourcemap = true;
},
configResolved(config) {
// We're only going to alter the chunking behavior in the default cases, where the user and/or
// other plugins haven't already configured this. It'd be impossible to avoid breakages otherwise.
if (
Array.isArray(config.build.rollupOptions.output) ||
config.build.rollupOptions.output?.manualChunks
) {
return;
}

viteConfig = config;
config.build.rollupOptions.output ??= {};
config.build.rollupOptions.output.manualChunks = (id) => {
if (id.includes(prerenderScript) || id.includes(preloadPolyfillId)) {
return "index";
}
};

// We're only going to alter the chunking behavior in the default cases, where the user and/or
// other plugins haven't already configured this. It'd be impossible to avoid breakages otherwise.
if (
Array.isArray(config.build.rollupOptions.output) ||
config.build.rollupOptions.output?.manualChunks
) {
return;
}

config.build.rollupOptions.output ??= {};
config.build.rollupOptions.output.manualChunks = (id) => {
if (id.includes(prerenderScript) || id.includes(preloadPolyfillId)) {
return "index";
}
};
viteConfig = config;
},
async options(opts) {
if (!opts.input) return;
Expand Down Expand Up @@ -226,13 +231,18 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere
);
let htmlDoc = htmlParse(tpl);

// Workaround for PNPM mangling file paths with their symlinks
const tmpDirRelative = path.join(
'node_modules',
'vite-prerender-plugin',
'headless-prerender',
);

// Create a tmp dir to allow importing & consuming the built modules,
// before Rollup writes them to the disk
const tmpDir = path.join(
viteConfig.root,
'node_modules',
'vite-prerender-plugin',
'headless-prerender',
tmpDirRelative,
);
try {
await fs.rm(tmpDir, { recursive: true });
Expand All @@ -249,26 +259,6 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere
/** @type {OutputChunk | undefined} */
let prerenderEntry;
for (const output of Object.keys(bundle)) {
// Clean up source maps if the user didn't enable them themselves
if (!userEnabledSourceMaps) {
if (output.endsWith('.map')) {
delete bundle[output];
continue;
}
if (output.endsWith('.js')) {
if (bundle[output].type == 'chunk') {
bundle[output].code = bundle[output].code.replace(
/\n\/\/#\ssourceMappingURL=.*/,
'',
);
} else {
// Workers and similar
bundle[output].source = /** @type {string} */ (
bundle[output].source
).replace(/\n\/\/#\ssourceMappingURL=.*/, '');
}
}
}
if (!output.endsWith('.js') || bundle[output].type !== 'chunk') continue;

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

const stack = StackTraceParse(e).find((s) => s.getFileName().includes(tmpDir));
const stack = StackTraceParse(e).find((s) => s.getFileName().includes(tmpDirRelative));

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

// Add generated HTML to compilation:
route.url == '/'
? /** @type {OutputAsset} */ ((bundle['index.html']).source =
? (/** @type {OutputAsset} */ (bundle['index.html']).source =
htmlDoc.toString())
: this.emitFile({
type: 'asset',
fileName: assetName,
source: htmlDoc.toString(),
});

// Clean up source maps if the user didn't enable them themselves
if (!userEnabledSourceMaps) {
for (const output of Object.keys(bundle)) {
if (output.endsWith('.map')) {
delete bundle[output];
continue;
}

if (output.endsWith('.js')) {
const codeOrSource = bundle[output].type == 'chunk' ? 'code' : 'source';
bundle[output][codeOrSource] = bundle[output][codeOrSource].replace(
/\n\/\/#\ssourceMappingURL=.*/,
'',
);
}
}
}
}
},
};
Expand Down
9 changes: 9 additions & 0 deletions tests/fixtures/source-maps/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<script prerender type="module" src="/src/index.js"></script>
</body>
</html>
9 changes: 9 additions & 0 deletions tests/fixtures/source-maps/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
if (typeof window !== "undefined") {
const worker = new Worker(new URL("./worker.js", import.meta.url));

worker.postMessage({ type: "init" });
}

export async function prerender() {
return `<h1>Simple Test Result</h1>`;
}
3 changes: 3 additions & 0 deletions tests/fixtures/source-maps/src/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
addEventListener('message', (e) => {
postMessage({ type: 'init' });
})
6 changes: 6 additions & 0 deletions tests/fixtures/source-maps/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import { vitePrerenderPlugin } from 'vite-prerender-plugin';

export default defineConfig({
plugins: [vitePrerenderPlugin()],
});
86 changes: 86 additions & 0 deletions tests/source-maps.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { test } from 'uvu';
import * as assert from 'uvu/assert';
import path from 'node:path';
import { promises as fs } from 'node:fs';

import { setupTest, teardownTest, loadFixture, viteBuild } from './lib/lifecycle.js';
import { getOutputFile, outputFileExists, writeFixtureFile } from './lib/utils.js';

const writeConfig = async (dir, content) => writeFixtureFile(dir, 'vite.config.js', content);

let env;
test.before.each(async () => {
env = await setupTest();
});

test.after.each(async () => {
await teardownTest(env);
});

test('Should strip sourcemaps by default', async () => {
await loadFixture('source-maps', env);
await viteBuild(env.tmp.path);

const outDir = path.join(env.tmp.path, 'dist', 'assets');
const outDirAssets = await fs.readdir(outDir);

assert.not.ok(outDirAssets.find((f) => f.endsWith('.map')));


const outputChunk = path.join(outDir, outDirAssets.find((f) => /^index-.*\.js$/.test(f)));
const outputChunkCode = await fs.readFile(outputChunk, 'utf-8');
assert.is(outputChunkCode.match(/\/\/#\ssourceMappingURL=(.*)/), null)

const outputAsset = path.join(outDir, outDirAssets.find((f) => /^worker-.*\.js$/.test(f)));
const outputAssetSource = await fs.readFile(outputAsset, 'utf-8');
assert.is(outputAssetSource.match(/\/\/#\ssourceMappingURL=(.*)/), null)
});

test('Should preserve sourcemaps if user has enabled them', async () => {
await loadFixture('simple', env);
await writeConfig(env.tmp.path, `
import { defineConfig } from 'vite';
import { vitePrerenderPlugin } from 'vite-prerender-plugin';
export default defineConfig({
build: { sourcemap: true },
plugins: [vitePrerenderPlugin()],
});
`);

await viteBuild(env.tmp.path);

const outDir = path.join(env.tmp.path, 'dist', 'assets');
const outDirAssets = await fs.readdir(outDir);

const outputJsFileName = outDirAssets.find((f) => f.endsWith('.js'));
assert.ok(outputJsFileName);
const outputJs = await fs.readFile(path.join(outDir, outputJsFileName), 'utf-8');
assert.match(outputJs, '//# sourceMappingURL=');

const outputMap = outputJs.match(/\/\/#\ssourceMappingURL=(.*)/)[1];
assert.ok(outDirAssets.includes(outputMap));
});

test('Should use sourcemaps to display error positioning when possible', async () => {
await loadFixture('simple', env);
await writeFixtureFile(env.tmp.path, 'src/index.js', `
document.createElement('div');
export async function prerender() {
return '<h1>Simple Test Result</h1>';
}`
);

let message = '';
try {
await viteBuild(env.tmp.path);
} catch (error) {
message = error.message;
}

assert.match(message, 'ReferenceError: document is not defined');
assert.match(message, 'src/index.js:2:9');
});

test.run();

0 comments on commit 66bdeec

Please sign in to comment.