From e22ac1a236154b6c810f4d69c142afa4e8df50cc Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Tue, 21 Apr 2026 20:55:33 +0200 Subject: [PATCH 1/2] Fix alternative index.html path --- src/plugins/prerender-plugin.js | 67 ++++++++++++------- .../nested-entry/src/dashboard/index.html | 9 +++ .../nested-entry/src/dashboard/main.js | 3 + tests/fixtures/nested-entry/vite.config.js | 17 +++++ tests/index.test.js | 8 +++ 5 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 tests/fixtures/nested-entry/src/dashboard/index.html create mode 100644 tests/fixtures/nested-entry/src/dashboard/main.js create mode 100644 tests/fixtures/nested-entry/vite.config.js diff --git a/src/plugins/prerender-plugin.js b/src/plugins/prerender-plugin.js index 36e1e98..9536d12 100644 --- a/src/plugins/prerender-plugin.js +++ b/src/plugins/prerender-plugin.js @@ -75,6 +75,7 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere let viteConfig = {}; let userEnabledSourceMaps; let ssrBuild = false; + let prerenderEntryHtml; /** @type {import('./types.d.ts').PrerenderedRoute[]} */ let routes = []; @@ -90,12 +91,9 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere const tmpDirId = 'headless-prerender'; /** - * From the non-external scripts in entry HTML document, find the one (if any) - * that provides a `prerender` export - * * @param {import('vite').Rollup.InputOption} input */ - const getPrerenderScriptFromHTML = async (input) => { + const getPrerenderEntryHtml = (input) => { // prettier-ignore const entryHtml = typeof input === "string" @@ -106,7 +104,17 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere if (!entryHtml) throw new Error('Unable to detect entry HTML'); - const htmlDoc = htmlParse(await fs.readFile(entryHtml, 'utf-8')); + return path.resolve(viteConfig.root, entryHtml); + }; + + /** + * From the non-external scripts in the entry HTML document, find the one (if any) + * that provides a `prerender` export + */ + const getPrerenderScriptFromHtml = async () => { + if (!prerenderEntryHtml) throw new Error('Unable to detect entry HTML'); + + const htmlDoc = htmlParse(await fs.readFile(prerenderEntryHtml, 'utf-8')); const entryScriptTag = htmlDoc .getElementsByTagName('script') @@ -118,7 +126,9 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere if (!entrySrc || /^https:/.test(entrySrc)) throw new Error('Prerender entry script must have a `src` attribute and be local'); - return path.join(viteConfig.root, entrySrc); + return entrySrc.startsWith('/') + ? path.join(viteConfig.root, entrySrc) + : path.resolve(path.dirname(prerenderEntryHtml), entrySrc); }; return { @@ -134,7 +144,7 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere // Only required for Vite 5 and older. In 6+, this is handled by the // Environment API (`applyToEnvironment`) if (config.build?.ssr) { - ssrBuild = true + ssrBuild = true; return; } @@ -194,8 +204,9 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere }, async options(opts) { if (ssrBuild || !opts.input) return; + prerenderEntryHtml = getPrerenderEntryHtml(opts.input); if (!prerenderScript) { - prerenderScript = await getPrerenderScriptFromHTML(opts.input); + prerenderScript = await getPrerenderScriptFromHtml(); } // prettier-ignore @@ -288,9 +299,25 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere }; // Grab the generated HTML file, we'll use it as a template for all pages: - const tpl = /** @type {string} */ ( - /** @type {OutputAsset} */ (bundle['index.html']).source - ); + const entryHtmlAsset = + (prerenderEntryHtml && + Object.values(bundle).find( + (output) => + output.type === 'asset' && + output.fileName.endsWith('.html') && + (output.originalFileName === prerenderEntryHtml || + output.originalFileNames?.includes(prerenderEntryHtml)), + )) || + /** @type {OutputAsset | undefined} */ (bundle['index.html']) || + Object.values(bundle).find( + (output) => output.type === 'asset' && output.fileName.endsWith('.html'), + ); + + if (!entryHtmlAsset) { + this.error('Unable to detect generated entry HTML asset'); + } + + const tpl = /** @type {string} */ (entryHtmlAsset.source); // Create a tmp dir to allow importing & consuming the built modules, // before Rollup writes them to the disk @@ -319,10 +346,7 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere const assetPath = path.join(tmpDir, output); await fs.mkdir(path.dirname(assetPath), { recursive: true }); - await fs.writeFile( - assetPath, - /** @type {OutputChunk} */ (bundle[output]).code, - ); + await fs.writeFile(assetPath, /** @type {OutputChunk} */ (bundle[output]).code); if (/** @type {OutputChunk} */ (bundle[output]).exports?.includes('prerender')) { prerenderEntry = /** @type {OutputChunk} */ (bundle[output]); @@ -348,9 +372,7 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere // do something in browsers only }`.replace(/^ {20}/gm, ''); - const stack = StackTraceParse(e).find((s) => - s.getFileName()?.includes(tmpDirId), - ); + const stack = StackTraceParse(e).find((s) => s.getFileName()?.includes(tmpDirId)); const sourceMapContent = prerenderEntry.map; if (stack && sourceMapContent) { @@ -390,9 +412,7 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere let prerender; try { - const m = await import( - `file://${path.join(tmpDir, prerenderEntry.fileName)}` - ); + const m = await import(`file://${path.join(tmpDir, prerenderEntry.fileName)}`); prerender = m.prerender; } catch (e) { const message = await handlePrerenderError(e); @@ -506,8 +526,7 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere // Add generated HTML to compilation: route.url == '/' - ? (/** @type {OutputAsset} */ (bundle['index.html']).source = - htmlDoc.toString()) + ? (entryHtmlAsset.source = htmlDoc.toString()) : this.emitFile({ type: 'asset', fileName: assetName, @@ -535,6 +554,6 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere } } } - } + }, }; } diff --git a/tests/fixtures/nested-entry/src/dashboard/index.html b/tests/fixtures/nested-entry/src/dashboard/index.html new file mode 100644 index 0000000..ede5005 --- /dev/null +++ b/tests/fixtures/nested-entry/src/dashboard/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/fixtures/nested-entry/src/dashboard/main.js b/tests/fixtures/nested-entry/src/dashboard/main.js new file mode 100644 index 0000000..cd16f51 --- /dev/null +++ b/tests/fixtures/nested-entry/src/dashboard/main.js @@ -0,0 +1,3 @@ +export async function prerender() { + return `

Nested Entry Test Result

`; +} diff --git a/tests/fixtures/nested-entry/vite.config.js b/tests/fixtures/nested-entry/vite.config.js new file mode 100644 index 0000000..57e9544 --- /dev/null +++ b/tests/fixtures/nested-entry/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; +import path from 'node:path'; +import url from 'node:url'; +import { vitePrerenderPlugin } from 'vite-prerender-plugin'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +export default defineConfig({ + build: { + rollupOptions: { + input: { + dashboard: path.resolve(__dirname, 'src/dashboard/index.html'), + }, + }, + }, + plugins: [vitePrerenderPlugin()], +}); diff --git a/tests/index.test.js b/tests/index.test.js index 82ab0ac..7b6bbfc 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -37,6 +37,14 @@ test('Should merge preload and entry chunks', async () => { assert.equal((await fs.readdir(outDirAssets)).length, 1); }); +test('Should support nested HTML entrypoints', async () => { + await loadFixture('nested-entry', env); + await viteBuild(env.tmp.path); + + const prerenderedHtml = await getOutputFile(env.tmp.path, 'src/dashboard/index.html'); + assert.match(prerenderedHtml, '

Nested Entry Test Result

'); +}); + test('Should bail on merging preload & entry chunks if user configures `manualChunks`', async () => { await loadFixture('simple', env); await writeConfig(env.tmp.path, ` From b58032a5e7e91187d691896b82baa7c920ffcee0 Mon Sep 17 00:00:00 2001 From: Ryan Christian <33403762+rschristian@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:47:18 -0500 Subject: [PATCH 2/2] chore: revert minor formatting changes Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com> --- src/plugins/prerender-plugin.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/plugins/prerender-plugin.js b/src/plugins/prerender-plugin.js index 9536d12..1f01a4e 100644 --- a/src/plugins/prerender-plugin.js +++ b/src/plugins/prerender-plugin.js @@ -346,7 +346,10 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere const assetPath = path.join(tmpDir, output); await fs.mkdir(path.dirname(assetPath), { recursive: true }); - await fs.writeFile(assetPath, /** @type {OutputChunk} */ (bundle[output]).code); + await fs.writeFile( + assetPath, + /** @type {OutputChunk} */ (bundle[output]).code, + ); if (/** @type {OutputChunk} */ (bundle[output]).exports?.includes('prerender')) { prerenderEntry = /** @type {OutputChunk} */ (bundle[output]); @@ -372,7 +375,9 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere // do something in browsers only }`.replace(/^ {20}/gm, ''); - const stack = StackTraceParse(e).find((s) => s.getFileName()?.includes(tmpDirId)); + const stack = StackTraceParse(e).find((s) => + s.getFileName()?.includes(tmpDirId), + ); const sourceMapContent = prerenderEntry.map; if (stack && sourceMapContent) { @@ -412,7 +417,9 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere let prerender; try { - const m = await import(`file://${path.join(tmpDir, prerenderEntry.fileName)}`); + const m = await import( + `file://${path.join(tmpDir, prerenderEntry.fileName)}` + ); prerender = m.prerender; } catch (e) { const message = await handlePrerenderError(e);