Init
This commit is contained in:
21
node_modules/vite-prerender-plugin/LICENSE
generated
vendored
Normal file
21
node_modules/vite-prerender-plugin/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 The Preact Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
124
node_modules/vite-prerender-plugin/README.md
generated
vendored
Normal file
124
node_modules/vite-prerender-plugin/README.md
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
<h1 align="center">vite-prerender-plugin</h1>
|
||||
|
||||
<p align="center">
|
||||
<picture width="100">
|
||||
<img src="./media/logo.png">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<p align="center">Effortless prerendering in every framework</p>
|
||||
|
||||
---
|
||||
|
||||
This is largely an extracted implementation of [@preact/preset-vite](https://github.com/preactjs/preset-vite)'s prererender functionality ([license](https://github.com/preactjs/preset-vite/blob/main/LICENSE)), which in turn is a reimplementation of [WMR](https://github.com/preactjs/wmr)'s prerendering ([license](https://github.com/preactjs/wmr/blob/main/LICENSE)).
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
$ npm install vite-prerender-plugin
|
||||
```
|
||||
|
||||
```js
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite';
|
||||
import { vitePrerenderPlugin } from 'vite-prerender-plugin';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vitePrerenderPlugin()],
|
||||
});
|
||||
```
|
||||
|
||||
To prerender your app, you'll need to do three things:
|
||||
|
||||
1. Set your `renderTarget` via the plugin option. This should, in all likelihood, match the query selector for where you render your app client-side, i.e., `render(<App />, document.querySelector('#app'))` -> `'#app'`
|
||||
|
||||
2. Specify your prerender script, which can be done by a) adding a `prerender` attribute to one of the scripts listed in your entry HTML (`<script prerender src="./my-prerender-script.js">`) or b) use the `prerenderScript` plugin option to specify the location of your script with an absolute path
|
||||
|
||||
3. Export a function named `prerender()` from your prerender script (see below for an example)
|
||||
|
||||
The plugin simply calls the prerender function you provide so it's up to you to determine how your app should be prerendered, likely you'll want to use the `render-to-string` implementation of your framework. This prerender function can be sync or async, so feel free to initialize your app data with `fetch()` calls, read local data with `fs.readFile()`, etc. All that's required is that your return an object containing an `html` property which is the string of HTML you want injected into your HTML document.
|
||||
|
||||
With that, you're all ready to build!
|
||||
|
||||
For full examples, please see the [examples directory](./examples), and if you don't see your framework listed, let me know! I can take a look to see at adding it.
|
||||
|
||||
## Options
|
||||
|
||||
| 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` |
|
||||
|
||||
## Advanced Prerender Options
|
||||
|
||||
Additionally, your `prerender()` function can return more than just HTML -- it can return additional links to prerender as well as information that should be set in the `<head>` of the HTML document, such as title, language, or meta tags. For example:
|
||||
|
||||
```js
|
||||
export async function prerender(data) {
|
||||
const html = '<h1>hello world</h1>';
|
||||
|
||||
return {
|
||||
html,
|
||||
|
||||
// Optionally add additional links that should be
|
||||
// prerendered (if they haven't already been)
|
||||
links: new Set(['/foo', '/bar']),
|
||||
|
||||
// Optional data to serialize into a script tag for use on the client:
|
||||
// <script type="application/json" id="prerender-data">{"url":"/"}</script>
|
||||
data: { url: data.url },
|
||||
|
||||
// Optionally configure and add elements to the `<head>` of
|
||||
// the prerendered HTML document
|
||||
head: {
|
||||
// Sets the "lang" attribute: `<html lang="en">`
|
||||
lang: 'en',
|
||||
|
||||
// Sets the title for the current page: `<title>My cool page</title>`
|
||||
title: 'My cool page',
|
||||
|
||||
// Sets any additional elements you want injected into the `<head>`:
|
||||
// <link rel="stylesheet" href="foo.css">
|
||||
// <meta property="og:title" content="Social media title">
|
||||
elements: new Set([
|
||||
{ type: 'link', props: { rel: 'stylesheet', href: 'foo.css' } },
|
||||
{ type: 'meta', props: { property: 'og:title', content: 'Social media title' } },
|
||||
]),
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
For those not using `preact-iso` (be it not using Preact at all or simply using other tools), this library exposes a `parseLinks` function which you can use to crawl your site for links to prerender. The function takes an HTML string and returns an array of links found in the document. To be valid, they must have an `href` attribute set and the `target` attribute, if set, must be `_self`.
|
||||
|
||||
```js
|
||||
export async function prerender() {
|
||||
const html = `
|
||||
<div>
|
||||
<a href="/foo">Foo</a>
|
||||
<a href="/bar" target="_blank">Bar</a>
|
||||
<a href="/baz" target="_top">Baz</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const { parseLinks } = await import('vite-prerender-plugin/parse');
|
||||
const links = parseLinks(html); // ['/foo']
|
||||
|
||||
return {
|
||||
html,
|
||||
links: new Set(links),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: Anything you want to be server-only, like `parseLinks` from the example above, should be dynamically imported in the prerender function. A static import will see that code included in your client bundle, inflating it for a code path that will never run.
|
||||
|
||||
## Licenses
|
||||
|
||||
[MIT](https://github.com/preactjs/vite-prerender-plugin/blob/master/LICENSE)
|
||||
|
||||
[WMR - MIT](https://github.com/preactjs/wmr/blob/main/LICENSE)
|
||||
|
||||
[Preact Vite Preset - MIT](https://github.com/preactjs/preset-vite/blob/main/LICENSE)
|
50
node_modules/vite-prerender-plugin/package.json
generated
vendored
Normal file
50
node_modules/vite-prerender-plugin/package.json
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "vite-prerender-plugin",
|
||||
"version": "0.5.10",
|
||||
"type": "module",
|
||||
"types": "./src/index.d.ts",
|
||||
"exports": {
|
||||
".": "./src/index.js",
|
||||
"./parse": "./src/parse.js"
|
||||
},
|
||||
"authors": "The Preact Authors (https://preactjs.com)",
|
||||
"license": "MIT",
|
||||
"description": "A Vite plugin for prerendering apps to HTML at build time",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/preactjs/vite-prerender-plugin.git"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"LICENSE",
|
||||
"package.json",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "pnpm test",
|
||||
"test": "uvu tests '.test.js'",
|
||||
"format": "prettier --write --ignore-path .gitignore ."
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "5.x || 6.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"kolorist": "^1.8.0",
|
||||
"magic-string": "0.x >= 0.26.0",
|
||||
"node-html-parser": "^6.1.12",
|
||||
"simple-code-frame": "^1.3.0",
|
||||
"source-map": "^0.7.4",
|
||||
"stack-trace": "^1.0.0-pre2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.0.8",
|
||||
"@types/node": "^20.11.16",
|
||||
"dedent": "^1.5.3",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-config-rschristian": "^0.1.1",
|
||||
"tmp-promise": "^3.0.3",
|
||||
"uvu": "^0.5.6",
|
||||
"vite": "^6.3.1"
|
||||
},
|
||||
"prettier": "prettier-config-rschristian"
|
||||
}
|
15
node_modules/vite-prerender-plugin/src/index.d.ts
generated
vendored
Normal file
15
node_modules/vite-prerender-plugin/src/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Plugin } from 'vite';
|
||||
|
||||
export interface PrerenderOptions {
|
||||
prerenderScript?: string;
|
||||
renderTarget?: string;
|
||||
additionalPrerenderRoutes?: string[];
|
||||
}
|
||||
|
||||
export interface PreviewMiddlewareOptions {
|
||||
previewMiddlewareFallback?: string;
|
||||
}
|
||||
|
||||
export type Options = PrerenderOptions & PreviewMiddlewareOptions;
|
||||
|
||||
export function vitePrerenderPlugin(options?: Options): Plugin[];
|
15
node_modules/vite-prerender-plugin/src/index.js
generated
vendored
Normal file
15
node_modules/vite-prerender-plugin/src/index.js
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import { prerenderPlugin } from './plugins/prerender-plugin.js';
|
||||
import { previewMiddlewarePlugin } from './plugins/preview-middleware-plugin.js';
|
||||
|
||||
/**
|
||||
* @param {import('./index.d.ts').Options} options
|
||||
* @returns {import('vite').Plugin[]}
|
||||
*/
|
||||
export function vitePrerenderPlugin(options = {}) {
|
||||
const { previewMiddlewareFallback, ...prerenderOptions } = options;
|
||||
|
||||
return [
|
||||
prerenderPlugin(prerenderOptions),
|
||||
previewMiddlewarePlugin({ previewMiddlewareFallback }),
|
||||
];
|
||||
}
|
1
node_modules/vite-prerender-plugin/src/parse.d.ts
generated
vendored
Normal file
1
node_modules/vite-prerender-plugin/src/parse.d.ts
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export function parseLinks(html: string): string[];
|
16
node_modules/vite-prerender-plugin/src/parse.js
generated
vendored
Normal file
16
node_modules/vite-prerender-plugin/src/parse.js
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { parse as htmlParse } from 'node-html-parser';
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
export function parseLinks(html) {
|
||||
const doc = htmlParse(html);
|
||||
return doc
|
||||
.querySelectorAll('a')
|
||||
.filter(
|
||||
(a) =>
|
||||
a.hasAttribute('href') &&
|
||||
(!a.getAttribute('target') || a.getAttribute('target') === '_self'),
|
||||
)
|
||||
.map((a) => a.getAttribute('href'));
|
||||
}
|
536
node_modules/vite-prerender-plugin/src/plugins/prerender-plugin.js
generated
vendored
Normal file
536
node_modules/vite-prerender-plugin/src/plugins/prerender-plugin.js
generated
vendored
Normal file
@@ -0,0 +1,536 @@
|
||||
import path from 'node:path';
|
||||
import { promises as fs } from 'node:fs';
|
||||
|
||||
import { createLogger } from 'vite';
|
||||
import MagicString from 'magic-string';
|
||||
import { parse as htmlParse } from 'node-html-parser';
|
||||
import { SourceMapConsumer } from 'source-map';
|
||||
import { parse as StackTraceParse } from 'stack-trace';
|
||||
import { createCodeFrame } from 'simple-code-frame';
|
||||
import * as kl from 'kolorist';
|
||||
|
||||
/**
|
||||
* @typedef {import('vite').Rollup.OutputChunk} OutputChunk
|
||||
* @typedef {import('vite').Rollup.OutputAsset} OutputAsset
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {import('./types.d.ts').PrerenderedRoute[]} routes
|
||||
*/
|
||||
export function prerenderedRoutes(routes) {
|
||||
return routes.reduce((s, r) => {
|
||||
s += `\n ${r.url}`;
|
||||
if (r._discoveredBy) s += kl.dim(` [from ${r._discoveredBy.url}]`);
|
||||
return s;
|
||||
}, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
*/
|
||||
function enc(str) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./types.d.ts')} HeadElement
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {HeadElement | HeadElement[] | string} element
|
||||
* @returns {string}
|
||||
*/
|
||||
function serializeElement(element) {
|
||||
if (element == null) return '';
|
||||
if (typeof element !== 'object') return String(element);
|
||||
if (Array.isArray(element)) return element.map(serializeElement).join('');
|
||||
const type = element.type;
|
||||
let s = `<${type}`;
|
||||
const props = element.props || {};
|
||||
let children = element.children;
|
||||
for (const prop of Object.keys(props)) {
|
||||
const value = props[prop];
|
||||
// Filter out empty values:
|
||||
if (value == null) continue;
|
||||
if (prop === 'children' || prop === 'textContent') children = value;
|
||||
else s += ` ${prop}="${enc(value)}"`;
|
||||
}
|
||||
s += '>';
|
||||
if (!/link|meta|base/.test(type)) {
|
||||
if (children) s += serializeElement(children);
|
||||
s += `</${type}>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../index.d.ts').PrerenderOptions} options
|
||||
* @returns {import('vite').Plugin}
|
||||
*/
|
||||
export function prerenderPlugin({ prerenderScript, renderTarget, additionalPrerenderRoutes } = {}) {
|
||||
let viteConfig = {};
|
||||
let userEnabledSourceMaps;
|
||||
let ssrBuild = false;
|
||||
|
||||
/** @type {import('./types.d.ts').PrerenderedRoute[]} */
|
||||
let routes = [];
|
||||
|
||||
renderTarget ||= 'body';
|
||||
additionalPrerenderRoutes ||= [];
|
||||
|
||||
const preloadHelperId = 'vite/preload-helper';
|
||||
const preloadPolyfillId = 'vite/modulepreload-polyfill';
|
||||
|
||||
// PNPM, Yalc, and anything else utilizing symlinks mangle the file
|
||||
// path a bit so we need a minimal, fairly unique ID to check against
|
||||
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) => {
|
||||
// prettier-ignore
|
||||
const entryHtml =
|
||||
typeof input === "string"
|
||||
? input
|
||||
: Array.isArray(input)
|
||||
? input.find(i => /html$/.test(i))
|
||||
: Object.values(input).find(i => /html$/.test(i));
|
||||
|
||||
if (!entryHtml) throw new Error('Unable to detect entry HTML');
|
||||
|
||||
const htmlDoc = htmlParse(await fs.readFile(entryHtml, 'utf-8'));
|
||||
|
||||
const entryScriptTag = htmlDoc
|
||||
.getElementsByTagName('script')
|
||||
.find((s) => s.hasAttribute('prerender'));
|
||||
|
||||
if (!entryScriptTag) throw new Error('Unable to detect prerender entry script');
|
||||
|
||||
const entrySrc = entryScriptTag.getAttribute('src');
|
||||
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 {
|
||||
name: 'vite-prerender-plugin',
|
||||
apply: 'build',
|
||||
enforce: 'post',
|
||||
applyToEnvironment(environment) {
|
||||
return environment.name == 'client';
|
||||
},
|
||||
// Vite is pretty inconsistent with how it resolves config options, both
|
||||
// hooks are needed to set their respective options. ¯\_(ツ)_/¯
|
||||
config(config) {
|
||||
// Only required for Vite 5 and older. In 6+, this is handled by the
|
||||
// Environment API (`applyToEnvironment`)
|
||||
if (config.build?.ssr) {
|
||||
ssrBuild = true
|
||||
return;
|
||||
}
|
||||
|
||||
userEnabledSourceMaps = !!config.build?.sourcemap;
|
||||
|
||||
if (!config.customLogger) {
|
||||
const logger = createLogger(config.logLevel || 'info');
|
||||
const loggerInfo = logger.info;
|
||||
|
||||
config.customLogger = {
|
||||
...logger,
|
||||
info: (msg, options) => {
|
||||
if (msg.includes(' │ map:') && !userEnabledSourceMaps) {
|
||||
msg = msg.replace(/ │ map:.*/, '');
|
||||
}
|
||||
|
||||
loggerInfo(msg, options);
|
||||
|
||||
if (msg.includes('built in')) {
|
||||
loggerInfo(
|
||||
kl.bold(
|
||||
`Prerendered ${routes.length} ${
|
||||
routes.length > 1 ? 'pages' : 'page'
|
||||
}:`,
|
||||
) + prerenderedRoutes(routes),
|
||||
options,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Enable sourcemaps for generating more actionable error messages
|
||||
config.build ??= {};
|
||||
config.build.sourcemap = true;
|
||||
},
|
||||
configResolved(config) {
|
||||
if (ssrBuild) return;
|
||||
// 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
|
||||
) {
|
||||
viteConfig = config;
|
||||
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 (ssrBuild || !opts.input) return;
|
||||
if (!prerenderScript) {
|
||||
prerenderScript = await getPrerenderScriptFromHTML(opts.input);
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
opts.input =
|
||||
typeof opts.input === "string"
|
||||
? [opts.input, prerenderScript]
|
||||
: Array.isArray(opts.input)
|
||||
? [...opts.input, prerenderScript]
|
||||
: { ...opts.input, prerenderEntry: prerenderScript };
|
||||
opts.preserveEntrySignatures = 'allow-extension';
|
||||
},
|
||||
// Injects window checks into Vite's preload helper & modulepreload polyfill
|
||||
transform(code, id) {
|
||||
if (ssrBuild) return;
|
||||
if (id.includes(preloadHelperId)) {
|
||||
// Injects a window check into Vite's preload helper, instantly resolving
|
||||
// the module rather than attempting to add a <link> to the document.
|
||||
const s = new MagicString(code);
|
||||
|
||||
// Through v5.0.4
|
||||
// https://github.com/vitejs/vite/blob/b93dfe3e08f56cafe2e549efd80285a12a3dc2f0/packages/vite/src/node/plugins/importAnalysisBuild.ts#L95-L98
|
||||
s.replace(
|
||||
`if (!__VITE_IS_MODERN__ || !deps || deps.length === 0) {`,
|
||||
`if (!__VITE_IS_MODERN__ || !deps || deps.length === 0 || typeof window === 'undefined') {`,
|
||||
);
|
||||
// 5.0.5+
|
||||
// https://github.com/vitejs/vite/blob/c902545476a4e7ba044c35b568e73683758178a3/packages/vite/src/node/plugins/importAnalysisBuild.ts#L93
|
||||
s.replace(
|
||||
`if (__VITE_IS_MODERN__ && deps && deps.length > 0) {`,
|
||||
`if (__VITE_IS_MODERN__ && deps && deps.length > 0 && typeof window !== 'undefined') {`,
|
||||
);
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: s.generateMap({ hires: true }),
|
||||
};
|
||||
} else if (id.includes(preloadPolyfillId)) {
|
||||
const s = new MagicString(code);
|
||||
// Replacement for `'link'` && `"link"` as the output from their tooling has
|
||||
// differed over the years. Should be better than switching to regex.
|
||||
// https://github.com/vitejs/vite/blob/20fdf210ee0ac0824b2db74876527cb7f378a9e8/packages/vite/src/node/plugins/modulePreloadPolyfill.ts#L62
|
||||
s.replace(
|
||||
`const relList = document.createElement('link').relList;`,
|
||||
`if (typeof window === "undefined") return;\n const relList = document.createElement('link').relList;`,
|
||||
);
|
||||
s.replace(
|
||||
`const relList = document.createElement("link").relList;`,
|
||||
`if (typeof window === "undefined") return;\n const relList = document.createElement("link").relList;`,
|
||||
);
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: s.generateMap({ hires: true }),
|
||||
};
|
||||
}
|
||||
},
|
||||
async generateBundle(_opts, bundle) {
|
||||
if (ssrBuild) return;
|
||||
// @ts-ignore
|
||||
globalThis.location = {};
|
||||
// @ts-ignore
|
||||
globalThis.self = globalThis;
|
||||
|
||||
// As of Vite 5.3.0-beta.0, Vite injects an undefined `__VITE_PRELOAD__` var
|
||||
// Swapping in an empty array is fine as we have no need to preload whilst prerendering
|
||||
// https://github.com/vitejs/vite/pull/16562
|
||||
// @ts-ignore
|
||||
globalThis.__VITE_PRELOAD__ = [];
|
||||
|
||||
globalThis.unpatchedFetch = globalThis.fetch;
|
||||
// Local, fs-based fetch implementation for prerendering
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
if (/^\//.test(url)) {
|
||||
try {
|
||||
return new Response(
|
||||
await fs.readFile(
|
||||
`${path.join(
|
||||
viteConfig.root,
|
||||
viteConfig.build.outDir,
|
||||
)}/${url.replace(/^\//, '')}`,
|
||||
'utf-8',
|
||||
),
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') throw e;
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
return globalThis.unpatchedFetch(url, opts);
|
||||
};
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// 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',
|
||||
tmpDirId,
|
||||
);
|
||||
try {
|
||||
await fs.rm(tmpDir, { recursive: true });
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') throw e;
|
||||
}
|
||||
await fs.mkdir(tmpDir, { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'package.json'),
|
||||
JSON.stringify({ type: 'module' }),
|
||||
);
|
||||
|
||||
/** @type {OutputChunk | undefined} */
|
||||
let prerenderEntry;
|
||||
for (const output of Object.keys(bundle)) {
|
||||
if (!output.endsWith('.js') || bundle[output].type !== 'chunk') continue;
|
||||
|
||||
const assetPath = path.join(tmpDir, output);
|
||||
await fs.mkdir(path.dirname(assetPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
assetPath,
|
||||
/** @type {OutputChunk} */ (bundle[output]).code,
|
||||
);
|
||||
|
||||
if (/** @type {OutputChunk} */ (bundle[output]).exports?.includes('prerender')) {
|
||||
prerenderEntry = /** @type {OutputChunk} */ (bundle[output]);
|
||||
}
|
||||
}
|
||||
if (!prerenderEntry) {
|
||||
this.error('Cannot detect module with `prerender` export');
|
||||
}
|
||||
|
||||
const handlePrerenderError = async (e) => {
|
||||
const isReferenceError = e instanceof ReferenceError;
|
||||
|
||||
let message = `\n
|
||||
${e}
|
||||
|
||||
This ${
|
||||
isReferenceError ? 'is most likely' : 'could be'
|
||||
} caused by using DOM/Web APIs which are not available
|
||||
available to the prerendering process running in Node. Consider wrapping
|
||||
the offending code in a window check like so:
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
// do something in browsers only
|
||||
}`.replace(/^ {20}/gm, '');
|
||||
|
||||
const stack = StackTraceParse(e).find((s) =>
|
||||
s.getFileName().includes(tmpDirId),
|
||||
);
|
||||
|
||||
const sourceMapContent = prerenderEntry.map;
|
||||
if (stack && sourceMapContent) {
|
||||
await SourceMapConsumer.with(sourceMapContent, null, async (consumer) => {
|
||||
let { source, line, column } = consumer.originalPositionFor({
|
||||
line: stack.getLineNumber(),
|
||||
column: stack.getColumnNumber(),
|
||||
});
|
||||
|
||||
if (!source || line == null || column == null) {
|
||||
message += `\nUnable to locate source map for error!\n`;
|
||||
this.error(message);
|
||||
}
|
||||
|
||||
const sourcePath = path.join(
|
||||
viteConfig.root,
|
||||
source.replace(/^(..\/)*/, ''),
|
||||
);
|
||||
const sourceContent = await fs.readFile(sourcePath, 'utf-8');
|
||||
|
||||
// `simple-code-frame` has 1-based line numbers
|
||||
const frame = createCodeFrame(sourceContent, line - 1, column);
|
||||
message += `\n
|
||||
> ${sourcePath}:${line}:${column + 1}\n
|
||||
${frame}`.replace(/^ {28}/gm, '');
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
/** @type {import('./types.d.ts').Head} */
|
||||
let head = { lang: '', title: '', elements: new Set() };
|
||||
|
||||
let prerender;
|
||||
try {
|
||||
const m = await import(
|
||||
`file://${path.join(tmpDir, prerenderEntry.fileName)}`
|
||||
);
|
||||
prerender = m.prerender;
|
||||
} catch (e) {
|
||||
const message = await handlePrerenderError(e);
|
||||
this.error(message);
|
||||
}
|
||||
|
||||
if (typeof prerender !== 'function') {
|
||||
this.error('Detected `prerender` export, but it is not a function');
|
||||
}
|
||||
|
||||
// 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]);
|
||||
|
||||
routes = [...seen].map((link) => ({ url: link }));
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.url) continue;
|
||||
|
||||
const outDir = route.url.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`
|
||||
const u = new URL(route.url, 'http://localhost');
|
||||
for (const i in u) {
|
||||
try {
|
||||
globalThis.location[i] = i == 'toString' ? u[i].bind(u) : String(u[i]);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await prerender({ ssr: true, url: route.url, route });
|
||||
} catch (e) {
|
||||
const message = await handlePrerenderError(e);
|
||||
this.error(message);
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
this.warn(`No result returned for route: ${route.url}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset HTML doc & head data
|
||||
const htmlDoc = htmlParse(tpl, { comment: true });
|
||||
head = { lang: '', title: '', elements: new Set() };
|
||||
|
||||
// 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(/\/$/, '') || '/';
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
|
||||
let body;
|
||||
if (result && typeof result === 'object') {
|
||||
if (typeof result.html !== 'undefined') body = result.html;
|
||||
if (result.head) {
|
||||
head = result.head;
|
||||
}
|
||||
if (result.data) {
|
||||
body += `<script type="application/json" id="prerender-data">${JSON.stringify(
|
||||
result.data,
|
||||
)}</script>`;
|
||||
}
|
||||
} else {
|
||||
body = result;
|
||||
}
|
||||
|
||||
const htmlHead = htmlDoc.querySelector('head');
|
||||
if (htmlHead) {
|
||||
if (head.title) {
|
||||
const htmlTitle = htmlHead.querySelector('title');
|
||||
htmlTitle
|
||||
? htmlTitle.set_content(enc(head.title))
|
||||
: htmlHead.insertAdjacentHTML(
|
||||
'afterbegin',
|
||||
`<title>${enc(head.title)}</title>`,
|
||||
);
|
||||
}
|
||||
|
||||
if (head.lang) {
|
||||
htmlDoc.querySelector('html').setAttribute('lang', enc(head.lang));
|
||||
}
|
||||
|
||||
if (head.elements) {
|
||||
// Inject HTML links at the end of <head> for any stylesheets injected during rendering of the page:
|
||||
htmlHead.insertAdjacentHTML(
|
||||
'beforeend',
|
||||
Array.from(
|
||||
new Set(Array.from(head.elements).map(serializeElement)),
|
||||
).join('\n'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const target = htmlDoc.querySelector(renderTarget);
|
||||
if (!target)
|
||||
this.error(
|
||||
result.renderTarget == 'body'
|
||||
? '`renderTarget` was not specified in plugin options and <body> does not exist in input HTML template'
|
||||
: `Unable to detect prerender renderTarget "${result.selector}" in input HTML template`,
|
||||
);
|
||||
target.insertAdjacentHTML('afterbegin', body);
|
||||
|
||||
// Add generated HTML to compilation:
|
||||
route.url == '/'
|
||||
? (/** @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';
|
||||
if (typeof bundle[output][codeOrSource] !== 'string') continue;
|
||||
|
||||
const linesOfCode = bundle[output][codeOrSource].trimEnd().split('\n');
|
||||
if (/^\/\/#\ssourceMappingURL=.*\.map$/.test(linesOfCode.at(-1))) {
|
||||
linesOfCode.pop();
|
||||
bundle[output][codeOrSource] = linesOfCode.join('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
45
node_modules/vite-prerender-plugin/src/plugins/preview-middleware-plugin.js
generated
vendored
Normal file
45
node_modules/vite-prerender-plugin/src/plugins/preview-middleware-plugin.js
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
import path from 'node:path';
|
||||
import { promises as fs } from 'node:fs';
|
||||
|
||||
/**
|
||||
* Vite's preview server won't route to anything but `/index.html` without
|
||||
* a file extension, e.g., `/tutorial` won't serve `/tutorial/index.html`.
|
||||
* This leads to some surprises & hydration issues, so we'll fix it ourselves.
|
||||
*
|
||||
* @param {import('../index.d.ts').PreviewMiddlewareOptions} options
|
||||
* @returns {import('vite').Plugin}
|
||||
*/
|
||||
export function previewMiddlewarePlugin({ previewMiddlewareFallback } = {}) {
|
||||
let outDir;
|
||||
|
||||
return {
|
||||
name: 'serve-prerendered-html',
|
||||
configResolved(config) {
|
||||
outDir = path.resolve(config.root, config.build.outDir);
|
||||
},
|
||||
configurePreviewServer(server) {
|
||||
server.middlewares.use(async (req, _res, next) => {
|
||||
if (!req.url) return next();
|
||||
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
// If URL has a file extension, bail
|
||||
if (url.pathname != url.pathname.split('.').pop()) return next();
|
||||
|
||||
const file = path.join(
|
||||
outDir,
|
||||
url.pathname.split(path.posix.sep).join(path.sep),
|
||||
'index.html',
|
||||
);
|
||||
|
||||
try {
|
||||
await fs.access(file);
|
||||
req.url = url.pathname + '/index.html' + url.search;
|
||||
} catch {
|
||||
req.url = (previewMiddlewareFallback || '') + '/index.html';
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
16
node_modules/vite-prerender-plugin/src/plugins/types.d.ts
generated
vendored
Normal file
16
node_modules/vite-prerender-plugin/src/plugins/types.d.ts
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface HeadElement {
|
||||
type: string;
|
||||
props: Record<string, string>;
|
||||
children?: string;
|
||||
}
|
||||
|
||||
export interface Head {
|
||||
lang: string;
|
||||
title: string;
|
||||
elements: Set<HeadElement>;
|
||||
}
|
||||
|
||||
export interface PrerenderedRoute {
|
||||
url: string;
|
||||
_discoveredBy?: PrerenderedRoute;
|
||||
}
|
Reference in New Issue
Block a user