From 2b98134697b4c00f0461c74bb73c1b8d58bc3e3e Mon Sep 17 00:00:00 2001 From: delta Date: Sun, 10 May 2026 01:00:27 +0200 Subject: [PATCH 1/4] feat: allow passing data alongside routes --- src/index.d.ts | 2 + src/plugins/prerender-plugin.js | 47 +++++++++++++++----- src/plugins/types.d.ts | 10 ++++- tests/complex-routes.test.js | 26 +++++++++++ tests/config.test.js | 16 +++++++ tests/fixtures/complex-routes/index.html | 9 ++++ tests/fixtures/complex-routes/src/index.js | 11 +++++ tests/fixtures/complex-routes/vite.config.js | 6 +++ 8 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 tests/complex-routes.test.js create mode 100644 tests/fixtures/complex-routes/index.html create mode 100644 tests/fixtures/complex-routes/src/index.js create mode 100644 tests/fixtures/complex-routes/vite.config.js diff --git a/src/index.d.ts b/src/index.d.ts index bd5f476..7f1f519 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,9 +1,11 @@ import { Plugin } from 'vite'; +import { ComplexRoute } from './plugins/types.d.ts'; export interface PrerenderOptions { prerenderScript?: string; renderTarget?: string; additionalPrerenderRoutes?: string[]; + resolveRoute?: (route: ComplexRoute) => string; } export interface PreviewMiddlewareOptions { diff --git a/src/plugins/prerender-plugin.js b/src/plugins/prerender-plugin.js index 1f01a4e..75b628e 100644 --- a/src/plugins/prerender-plugin.js +++ b/src/plugins/prerender-plugin.js @@ -1,5 +1,6 @@ import path from 'node:path'; import { promises as fs } from 'node:fs'; +import { isDeepStrictEqual } from 'node:util'; import { createLogger } from 'vite'; import MagicString from 'magic-string'; @@ -71,7 +72,7 @@ function serializeElement(element) { * @param {import('../index.d.ts').PrerenderOptions} options * @returns {import('vite').Plugin} */ -export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrerenderRoutes } = {}) { +export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrerenderRoutes, resolveRoute } = {}) { let viteConfig = {}; let userEnabledSourceMaps; let ssrBuild = false; @@ -82,6 +83,7 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere renderTarget ||= 'body'; additionalPrerenderRoutes ||= []; + resolveRoute ||= (route) => route.url const preloadHelperId = 'vite/preload-helper'; const preloadPolyfillId = 'vite/modulepreload-polyfill'; @@ -430,17 +432,40 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere this.error('Detected `prerender` export, but it is not a function'); } + /** + * @param route {import('./types.d.ts').Route} + * @returns {import('./types.d.ts').ComplexRoute} + */ + const normalizeRoute = (route) => { + if (typeof route === "string") { + return ({ url: route }) + } else if (typeof route === "object") { + if (typeof route.url === "undefined") { + this.error('Route objects should have a defined url') + } else if (typeof route.url !== "string") { + this.error('Route url should be a string') + } + + return route + } + } + // We start by pre-rendering the home page. // Links discovered during pre-rendering get pushed into the list of routes. - const seen = new Set(['/', ...additionalPrerenderRoutes]); + /** @type {Map} */ + const seen = new Map(['/', ...additionalPrerenderRoutes].map((route) => { + let normalizedRoute = normalizeRoute(route); + return [resolveRoute(normalizedRoute), normalizedRoute]; + })); - routes = [...seen].map((link) => ({ url: link })); + routes = [...seen.values()]; for (const route of routes) { if (!route.url) continue; - const outDir = route.url.replace(/(^\/|\/$)/g, ''); + const outDir = resolveRoute(route).replace(/(^\/|\/$)/g, ''); const assetName = path.join(outDir, outDir.endsWith('.html') ? '' : 'index.html'); + console.log(assetName) // Update `location` to current URL so routers can use things like `location.pathname` const u = new URL(route.url, 'http://localhost'); @@ -470,13 +495,15 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere // Add any discovered links to the list of routes to pre-render: if (result.links) { - for (let url of result.links) { - const parsed = new URL(url, 'http://localhost'); - url = parsed.pathname.replace(/\/$/, '') || '/'; + for (let link of result.links) { + link = normalizeRoute(link); + const parsed = new URL(link.url, 'http://localhost'); + link.url = parsed.pathname.replace(/\/$/, '') || '/'; + let resolved = resolveRoute(link); // ignore external links and ones we've already picked up - if (seen.has(url) || parsed.origin !== 'http://localhost') continue; - seen.add(url); - routes.push({ url, _discoveredBy: route }); + if (seen.has(resolved) || parsed.origin !== 'http://localhost') continue; + seen.set(resolved, link); + routes.push({ ...link, _discoveredBy: route }); } } diff --git a/src/plugins/types.d.ts b/src/plugins/types.d.ts index 7f4dc80..b3b816e 100644 --- a/src/plugins/types.d.ts +++ b/src/plugins/types.d.ts @@ -10,8 +10,14 @@ export interface Head { elements: Set; } -export interface PrerenderedRoute { +export type Route = ComplexRoute | ComplexRoute["url"]; + +export interface ComplexRoute { url: string; + data?: any; +} + +export interface PrerenderedRoute extends ComplexRoute { _discoveredBy?: PrerenderedRoute; } @@ -23,7 +29,7 @@ export interface PrerenderArguments { export type PrerenderResult = { html: string; - links?: Set; + links?: Set; data?: any; head?: Partial; } | string diff --git a/tests/complex-routes.test.js b/tests/complex-routes.test.js new file mode 100644 index 0000000..ff747a4 --- /dev/null +++ b/tests/complex-routes.test.js @@ -0,0 +1,26 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; + +import { setupTest, teardownTest, loadFixture, viteBuild } from './lib/lifecycle.js'; +import { getOutputFile } from './lib/utils.js'; + +let env; +test.before.each(async () => { + env = await setupTest(); +}); + +test.after.each(async () => { + await teardownTest(env); +}); + +test('Should pass data to prerender script', async () => { + await loadFixture('complex-routes', env); + await viteBuild(env.tmp.path); + + const prerenderedIndexHtml = await getOutputFile(env.tmp.path, 'index.html'); + const prerenderedDataHtml = await getOutputFile(env.tmp.path, 'data/index.html'); + assert.match(prerenderedIndexHtml, '

data: no

'); + assert.match(prerenderedDataHtml, '

data: yes

'); +}); + +test.run(); diff --git a/tests/config.test.js b/tests/config.test.js index 9829e89..0cb68e6 100644 --- a/tests/config.test.js +++ b/tests/config.test.js @@ -143,4 +143,20 @@ test('Should support the `additionalPrerenderRoutes` plugin option', async () => assert.ok(await outputFileExists(env.tmp.path, 'not-found/index.html')); }); +test('Should support the `resolveRoute` plugin optionm', async () => { + await loadFixture('complex-routes', env); + await writeConfig(env.tmp.path, ` + import { defineConfig } from 'vite'; + import { vitePrerenderPlugin } from 'vite-prerender-plugin'; + + export default defineConfig({ + plugins: [vitePrerenderPlugin({ resolveRoute: (route) => { console.log(route); return \`\${route.data ? "/data" : ""}\${route.url}\` } })], + }); + `); + await viteBuild(env.tmp.path); + + assert.ok(await outputFileExists(env.tmp.path, 'index.html')); + assert.ok(await outputFileExists(env.tmp.path, 'data/data/index.html')); +}) + test.run(); diff --git a/tests/fixtures/complex-routes/index.html b/tests/fixtures/complex-routes/index.html new file mode 100644 index 0000000..3d387f8 --- /dev/null +++ b/tests/fixtures/complex-routes/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/fixtures/complex-routes/src/index.js b/tests/fixtures/complex-routes/src/index.js new file mode 100644 index 0000000..e7e19d8 --- /dev/null +++ b/tests/fixtures/complex-routes/src/index.js @@ -0,0 +1,11 @@ +export async function prerender({ route }) { + return { + html: `

data: ${route.data ? "yes" : "no"}

`, + links: new Set([ + { + url: "/data", + data: true + } + ]) + } +} diff --git a/tests/fixtures/complex-routes/vite.config.js b/tests/fixtures/complex-routes/vite.config.js new file mode 100644 index 0000000..4f19296 --- /dev/null +++ b/tests/fixtures/complex-routes/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import { vitePrerenderPlugin } from 'vite-prerender-plugin'; + +export default defineConfig({ + plugins: [vitePrerenderPlugin()], +}); From 061a17a4fc40dc9033c1a30816d8ce00eb33178b Mon Sep 17 00:00:00 2001 From: delta Date: Sun, 10 May 2026 01:12:34 +0200 Subject: [PATCH 2/4] ref: make the type for `resolveRoute` more accurate --- src/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 7f1f519..340392c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,11 +1,11 @@ import { Plugin } from 'vite'; -import { ComplexRoute } from './plugins/types.d.ts'; +import type { PrerenderedRoute } from './plugins/types.d.ts'; export interface PrerenderOptions { prerenderScript?: string; renderTarget?: string; additionalPrerenderRoutes?: string[]; - resolveRoute?: (route: ComplexRoute) => string; + resolveRoute?: (route: PrerenderedRoute) => string; } export interface PreviewMiddlewareOptions { From e8211f133f30cbbce968baf090c3ac6c031f9e52 Mon Sep 17 00:00:00 2001 From: delta Date: Sun, 10 May 2026 01:15:57 +0200 Subject: [PATCH 3/4] doc: add docs for `resolveRoute` --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fcb9362..3fa629b 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,11 @@ For full examples, please see the [examples directory](./examples), and if you d | Option | Type | Default | Description | | --------------------------- | -------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `renderTarget` | `string` | `"body"` | Query selector for where to insert prerender result in your HTML template | -| `prerenderScript` | `string` | `undefined` | Absolute path to script containing exported `prerender()` function. If not provided, the plugin will try to find the prerender script in the scripts listed in your HTML entrypoint | -| `additionalPrerenderRoutes` | `string` | `undefined` | While the prerendering process can automatically find new links in your app to prerender, sometimes you will have pages that are not linked to but you still want them prerendered (such as a `/404` page). Use this option to add them to the prerender queue | -| `previewMiddlewareFallback` | `string` | `/index.html` | Fallback path to be used when an HTML document cannot be found via the preview middleware, e.g., `/404` or `/not-found` will be returned when the user requests `/some-path-that-does-not-exist` | +| `renderTarget` | `string` | `"body"` | Query selector for where to insert prerender result in your HTML template | +| `prerenderScript` | `string` | `undefined` | Absolute path to script containing exported `prerender()` function. If not provided, the plugin will try to find the prerender script in the scripts listed in your HTML entrypoint | +| `additionalPrerenderRoutes` | `string` | `undefined` | While the prerendering process can automatically find new links in your app to prerender, sometimes you will have pages that are not linked to but you still want them prerendered (such as a `/404` page). Use this option to add them to the prerender queue | +| `previewMiddlewareFallback` | `string` | `/index.html` | Fallback path to be used when an HTML document cannot be found via the preview middleware, e.g., `/404` or `/not-found` will be returned when the user requests `/some-path-that-does-not-exist` | +| `resolveRoute` | `(PrerenderedRoute) => string` | `(route) => route.url` | A function that resolves a route into a unique route. Should be used to differentiate between several routes with the same url but different data e.g. `{ url: "/" }` and `{ url: "/", data: true }` ## Advanced Prerender Options From cef83031812816d36eda889f062e1337fe824219 Mon Sep 17 00:00:00 2001 From: delta Date: Sun, 10 May 2026 22:27:36 +0200 Subject: [PATCH 4/4] ref: remove leftover console.log --- src/plugins/prerender-plugin.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/prerender-plugin.js b/src/plugins/prerender-plugin.js index 75b628e..1368bcf 100644 --- a/src/plugins/prerender-plugin.js +++ b/src/plugins/prerender-plugin.js @@ -465,7 +465,6 @@ export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrere const outDir = resolveRoute(route).replace(/(^\/|\/$)/g, ''); const assetName = path.join(outDir, outDir.endsWith('.html') ? '' : 'index.html'); - console.log(assetName) // Update `location` to current URL so routers can use things like `location.pathname` const u = new URL(route.url, 'http://localhost');