Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Plugin } from 'vite';
import type { PrerenderedRoute } from './plugins/types.d.ts';

export interface PrerenderOptions {
prerenderScript?: string;
renderTarget?: string;
additionalPrerenderRoutes?: string[];
resolveRoute?: (route: PrerenderedRoute) => string;
}

export interface PreviewMiddlewareOptions {
Expand Down
46 changes: 36 additions & 10 deletions src/plugins/prerender-plugin.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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';
Expand Down Expand Up @@ -430,16 +432,38 @@ 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<string, import('./types.d.ts').ComplexRoute>} */
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');

// Update `location` to current URL so routers can use things like `location.pathname`
Expand Down Expand Up @@ -470,13 +494,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 });
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/plugins/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ export interface Head {
elements: Set<HeadElement>;
}

export interface PrerenderedRoute {
export type Route = ComplexRoute | ComplexRoute["url"];

export interface ComplexRoute {
url: string;
data?: any;
}

export interface PrerenderedRoute extends ComplexRoute {
_discoveredBy?: PrerenderedRoute;
}

Expand All @@ -23,7 +29,7 @@ export interface PrerenderArguments {

export type PrerenderResult = {
html: string;
links?: Set<string>;
links?: Set<Route>;
data?: any;
head?: Partial<Head>;
} | string
26 changes: 26 additions & 0 deletions tests/complex-routes.test.js
Original file line number Diff line number Diff line change
@@ -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, '<h1>data: no</h1>');
assert.match(prerenderedDataHtml, '<h1>data: yes</h1>');
});

test.run();
16 changes: 16 additions & 0 deletions tests/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
9 changes: 9 additions & 0 deletions tests/fixtures/complex-routes/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>
11 changes: 11 additions & 0 deletions tests/fixtures/complex-routes/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export async function prerender({ route }) {
return {
html: `<h1>data: ${route.data ? "yes" : "no"}</h1>`,
links: new Set([
{
url: "/data",
data: true
}
])
}
}
6 changes: 6 additions & 0 deletions tests/fixtures/complex-routes/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()],
});