diff --git a/src/plugins/prerender-plugin.js b/src/plugins/prerender-plugin.js index 7290daf..1359789 100644 --- a/src/plugins/prerender-plugin.js +++ b/src/plugins/prerender-plugin.js @@ -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; @@ -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 }); @@ -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( @@ -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) { @@ -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=.*/, + '', + ); + } + } + } } }, }; diff --git a/tests/fixtures/source-maps/index.html b/tests/fixtures/source-maps/index.html new file mode 100644 index 0000000..3d387f8 --- /dev/null +++ b/tests/fixtures/source-maps/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/fixtures/source-maps/src/index.js b/tests/fixtures/source-maps/src/index.js new file mode 100644 index 0000000..e40675e --- /dev/null +++ b/tests/fixtures/source-maps/src/index.js @@ -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 `

Simple Test Result

`; +} diff --git a/tests/fixtures/source-maps/src/worker.js b/tests/fixtures/source-maps/src/worker.js new file mode 100644 index 0000000..384aa0d --- /dev/null +++ b/tests/fixtures/source-maps/src/worker.js @@ -0,0 +1,3 @@ +addEventListener('message', (e) => { + postMessage({ type: 'init' }); +}) diff --git a/tests/fixtures/source-maps/vite.config.js b/tests/fixtures/source-maps/vite.config.js new file mode 100644 index 0000000..4f19296 --- /dev/null +++ b/tests/fixtures/source-maps/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import { vitePrerenderPlugin } from 'vite-prerender-plugin'; + +export default defineConfig({ + plugins: [vitePrerenderPlugin()], +}); diff --git a/tests/source-maps.test.js b/tests/source-maps.test.js new file mode 100644 index 0000000..78bfdd9 --- /dev/null +++ b/tests/source-maps.test.js @@ -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 '

Simple Test Result

'; + }` + ); + + 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(); +