From 154ee7440fe9bad408b454e5dbf77d016f882874 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Dec 2022 05:13:33 -0500 Subject: [PATCH] [breaking] add builder.generateFallback(fallback) API (#8013) closes #7899 --- .changeset/shaggy-bikes-wink.md | 6 +++ packages/adapter-static/index.js | 6 ++- packages/kit/src/core/adapt/builder.js | 53 ++++++++++++++++---- packages/kit/src/core/prerender/fallback.js | 43 ++++++++++++++++ packages/kit/src/core/prerender/prerender.js | 12 ----- packages/kit/types/index.d.ts | 13 +++-- 6 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 .changeset/shaggy-bikes-wink.md create mode 100644 packages/kit/src/core/prerender/fallback.js diff --git a/.changeset/shaggy-bikes-wink.md b/.changeset/shaggy-bikes-wink.md new file mode 100644 index 000000000000..0b51a1816cf4 --- /dev/null +++ b/.changeset/shaggy-bikes-wink.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-static': patch +'@sveltejs/kit': patch +--- + +[breaking] replace automatic fallback generation with `builder.generateFallback(fallback)` diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js index 8e9bf8b84ea2..b43956ae2938 100644 --- a/packages/adapter-static/index.js +++ b/packages/adapter-static/index.js @@ -79,7 +79,11 @@ See https://kit.svelte.dev/docs/page-options#prerender for more details` builder.rimraf(pages); builder.writeClient(assets); - builder.writePrerendered(pages, { fallback }); + builder.writePrerendered(pages); + + if (fallback) { + builder.generateFallback(path.join(pages, fallback)); + } if (precompress) { builder.log.minor('Compressing assets and pages'); diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index dc8d1b7e4d62..adca1e7a9309 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -1,11 +1,14 @@ +import { existsSync, statSync, createReadStream, createWriteStream } from 'node:fs'; +import { pipeline } from 'node:stream'; +import { promisify } from 'node:util'; +import { fork } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; import glob from 'tiny-glob'; import zlib from 'zlib'; -import { existsSync, statSync, createReadStream, createWriteStream } from 'fs'; -import { pipeline } from 'stream'; -import { promisify } from 'util'; import { copy, rimraf, mkdirp } from '../../utils/filesystem.js'; import { generate_manifest } from '../generate_manifest/index.js'; import { get_route_segments } from '../../utils/routing.js'; +import { get_env } from '../../exports/vite/utils.js'; const pipe = promisify(pipeline); @@ -104,6 +107,33 @@ export function create_builder({ config, build_data, routes, prerendered, log }) } }, + generateFallback(dest) { + // do prerendering in a subprocess so any dangling stuff gets killed upon completion + const script = fileURLToPath(new URL('../prerender/fallback.js', import.meta.url)); + + const manifest_path = `${config.kit.outDir}/output/server/manifest-full.js`; + + const env = get_env(config.kit.env, 'production'); + + return new Promise((fulfil, reject) => { + const child = fork( + script, + [dest, manifest_path, JSON.stringify({ ...env.private, ...env.public })], + { + stdio: 'inherit' + } + ); + + child.on('exit', (code) => { + if (code) { + reject(new Error(`Could not create a fallback page — failed with code ${code}`)); + } else { + fulfil(undefined); + } + }); + }); + }, + generateManifest: ({ relativePath }) => { return generate_manifest({ build_data, @@ -132,16 +162,17 @@ export function create_builder({ config, build_data, routes, prerendered, log }) return [...copy(`${config.kit.outDir}/output/client`, dest)]; }, - writePrerendered(dest, { fallback } = {}) { - const source = `${config.kit.outDir}/output/prerendered`; - const files = [...copy(`${source}/pages`, dest), ...copy(`${source}/dependencies`, dest)]; - - if (fallback) { - files.push(fallback); - copy(`${source}/fallback.html`, `${dest}/${fallback}`); + // @ts-expect-error + writePrerendered(dest, opts) { + // TODO remove for 1.0 + if (opts?.fallback) { + throw new Error( + 'The fallback option no longer exists — use builder.generateFallback(fallback) instead' + ); } - return files; + const source = `${config.kit.outDir}/output/prerendered`; + return [...copy(`${source}/pages`, dest), ...copy(`${source}/dependencies`, dest)]; }, writeServer(dest) { diff --git a/packages/kit/src/core/prerender/fallback.js b/packages/kit/src/core/prerender/fallback.js new file mode 100644 index 000000000000..1ac26ac1d9e8 --- /dev/null +++ b/packages/kit/src/core/prerender/fallback.js @@ -0,0 +1,43 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { pathToFileURL } from 'url'; +import { mkdirp } from '../../utils/filesystem.js'; +import { installPolyfills } from '../../exports/node/polyfills.js'; +import { load_config } from '../config/index.js'; + +const [, , dest, manifest_path, env] = process.argv; + +/** @type {import('types').ValidatedKitConfig} */ +const config = (await load_config()).kit; + +installPolyfills(); + +const server_root = join(config.outDir, 'output'); + +/** @type {import('types').ServerModule} */ +const { Server, override } = await import(pathToFileURL(`${server_root}/server/index.js`).href); + +/** @type {import('types').SSRManifest} */ +const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; + +override({ + building: true, + paths: config.paths, + read: (file) => readFileSync(join(config.files.assets, file)) +}); + +const server = new Server(manifest); +await server.init({ env: JSON.parse(env) }); + +const rendered = await server.respond(new Request(config.prerender.origin + '/[fallback]'), { + getClientAddress: () => { + throw new Error('Cannot read clientAddress during prerendering'); + }, + prerendering: { + fallback: true, + dependencies: new Map() + } +}); + +mkdirp(dirname(dest)); +writeFileSync(dest, await rendered.text()); diff --git a/packages/kit/src/core/prerender/prerender.js b/packages/kit/src/core/prerender/prerender.js index 8d4471943166..0b6e6a896d0e 100644 --- a/packages/kit/src/core/prerender/prerender.js +++ b/packages/kit/src/core/prerender/prerender.js @@ -453,18 +453,6 @@ export async function prerender() { ); } - const rendered = await server.respond(new Request(config.prerender.origin + '/[fallback]'), { - getClientAddress, - prerendering: { - fallback: true, - dependencies: new Map() - } - }); - - const file = `${config.outDir}/output/prerendered/fallback.html`; - mkdirp(dirname(file)); - writeFileSync(file, await rendered.text()); - output_and_exit({ prerendered, prerender_map }); } diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 5572d3475534..eacb5ee890c0 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -90,6 +90,11 @@ export interface Builder { */ createEntries(fn: (route: RouteDefinition) => AdapterEntry): Promise; + /** + * Generate a fallback page for a static webserver to use when no route is matched. Useful for single-page apps. + */ + generateFallback(dest: string): Promise; + /** * Generate a server-side manifest to initialise the SvelteKit [server](https://kit.svelte.dev/docs/types#public-types-server) with. * @param opts a relative path to the base directory of the app and optionally in which format (esm or cjs) the manifest should be generated @@ -117,15 +122,9 @@ export interface Builder { /** * Write prerendered files to `dest`. * @param dest the destination folder - * @param opts.fallback the name of a file for fallback responses, like `200.html` or `404.html` depending on where the app is deployed * @returns an array of files written to `dest` */ - writePrerendered( - dest: string, - opts?: { - fallback?: string; - } - ): string[]; + writePrerendered(dest: string): string[]; /** * Write server-side code to `dest`. * @param dest the destination folder