diff --git a/.changeset/proud-laws-exist.md b/.changeset/proud-laws-exist.md new file mode 100644 index 000000000000..9ac3739fb24b --- /dev/null +++ b/.changeset/proud-laws-exist.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-static': patch +--- + +[breaking] require all routes to be prerenderable when not using fallback option diff --git a/.changeset/spicy-taxis-wave.md b/.changeset/spicy-taxis-wave.md new file mode 100644 index 000000000000..ead4036116f6 --- /dev/null +++ b/.changeset/spicy-taxis-wave.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[breaking] add `prerender = 'auto'` option, and extend `prerender` option to endpoints diff --git a/documentation/docs/12-page-options.md b/documentation/docs/12-page-options.md index a00448f1078c..e0ed3ef0cdb6 100644 --- a/documentation/docs/12-page-options.md +++ b/documentation/docs/12-page-options.md @@ -32,22 +32,29 @@ export const hydrate = false; ### prerender -It's likely that at least some pages of your app can be represented as a simple HTML file generated at build time. These pages can be [_prerendered_](/docs/appendix#prerendering). +It's likely that at least some routes of your app can be represented as a simple HTML file generated at build time. These routes can be [_prerendered_](/docs/appendix#prerendering). -Prerendering happens automatically for any page with the `prerender` annotation: +Prerendering happens automatically for any `+page` or `+server` file with the `prerender` annotation: ```js -/// file: +page.js/+page.server.js +/// file: +page.js/+page.server.js/+server.js export const prerender = true; ``` Alternatively, you can set [`config.kit.prerender.default`](/docs/configuration#prerender) to `true` and prerender everything except pages that are explicitly marked as _not_ prerenderable: ```js -/// file: +page.js/+page.server.js +/// file: +page.js/+page.server.js/+server.js export const prerender = false; ``` +Routes with `prerender = true` will be excluded from manifests used for dynamic SSR, making your server (or serverless/edge functions) smaller. In some cases you might want to prerender a route but also include it in the manifest (for example, you want to prerender your most recent/popular content but server-render the long tail) — for these cases, there's a third option, 'auto': + +```js +/// file: +page.js/+page.server.js/+server.js +export const prerender = 'auto'; +``` + > If your entire app is suitable for prerendering, you can use [`adapter-static`](https://github.com/sveltejs/kit/tree/master/packages/adapter-static), which will output files suitable for use with any static webserver. The prerenderer will start at the root of your app and generate HTML for any prerenderable pages it finds. Each page is scanned for `` elements that point to other pages that are candidates for prerendering — because of this, you generally don't need to specify which pages should be accessed. If you _do_ need to specify which pages should be accessed by the prerenderer, you can do so with the `entries` option in the [prerender configuration](/docs/configuration#prerender). diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js index 9068a0730dac..8d6deae6ff8b 100644 --- a/packages/adapter-static/index.js +++ b/packages/adapter-static/index.js @@ -1,3 +1,4 @@ +import path from 'path'; import { platforms } from './platforms.js'; /** @type {import('.').default} */ @@ -6,10 +7,33 @@ export default function (options) { name: '@sveltejs/adapter-static', async adapt(builder) { - if (!options?.fallback && !builder.config.kit.prerender.default) { - throw Error( - 'adapter-static requires `config.kit.prerender.default` to be `true` unless you set the `fallback: true` option to create a single-page app. See https://github.com/sveltejs/kit/tree/master/packages/adapter-static#spa-mode for more information' - ); + if (!options?.fallback) { + /** @type {string[]} */ + const dynamic_routes = []; + + // this is a bit of a hack — it allows us to know whether there are dynamic + // (i.e. prerender = false/'auto') routes without having dedicated API + // surface area for it + builder.createEntries((route) => { + dynamic_routes.push(route.id); + + return { + id: '', + filter: () => false, + complete: () => {} + }; + }); + + if (dynamic_routes.length > 0) { + const prefix = path.relative('.', builder.config.kit.files.routes); + builder.log.error( + `@sveltejs/adapter-static: cannot have dynamic routes unless using the 'fallback' option. See https://github.com/sveltejs/kit/tree/master/packages/adapter-static#spa-mode for more information` + ); + builder.log.error( + dynamic_routes.map((id) => ` - ${path.posix.join(prefix, id)}`).join('\n') + ); + throw new Error('Encountered dynamic routes'); + } } const platform = platforms.find((platform) => platform.test()); diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/+page.js b/packages/adapter-static/test/apps/prerendered/src/routes/+page.js new file mode 100644 index 000000000000..868703b63372 --- /dev/null +++ b/packages/adapter-static/test/apps/prerendered/src/routes/+page.js @@ -0,0 +1,5 @@ +/** @type {import('./$types').PageLoad} */ +export async function load({ fetch }) { + const res = await fetch('/endpoint/implicit.json'); + return await res.json(); +} diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/+page.svelte b/packages/adapter-static/test/apps/prerendered/src/routes/+page.svelte index 404fd77511d2..d92d424b6686 100644 --- a/packages/adapter-static/test/apps/prerendered/src/routes/+page.svelte +++ b/packages/adapter-static/test/apps/prerendered/src/routes/+page.svelte @@ -1 +1,7 @@ -

This page was prerendered

\ No newline at end of file + + +

This page was prerendered

+

answer: {data.answer}

diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/endpoint/explicit.json/+server.js b/packages/adapter-static/test/apps/prerendered/src/routes/endpoint/explicit.json/+server.js new file mode 100644 index 000000000000..fc4b44c582d4 --- /dev/null +++ b/packages/adapter-static/test/apps/prerendered/src/routes/endpoint/explicit.json/+server.js @@ -0,0 +1,8 @@ +import { json } from '@sveltejs/kit'; + +export const prerender = true; + +/** @type {import('./$types').RequestHandler} */ +export function GET() { + return json({ answer: 42 }); +} diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/endpoint/implicit.json/+server.js b/packages/adapter-static/test/apps/prerendered/src/routes/endpoint/implicit.json/+server.js new file mode 100644 index 000000000000..b579cdd18224 --- /dev/null +++ b/packages/adapter-static/test/apps/prerendered/src/routes/endpoint/implicit.json/+server.js @@ -0,0 +1,9 @@ +import { json } from '@sveltejs/kit'; + +// no export const prerender here, it should be prerendered by virtue +// of being fetched from a prerendered page + +/** @type {import('./$types').RequestHandler} */ +export function GET() { + return json({ answer: 42 }); +} diff --git a/packages/adapter-static/test/test.js b/packages/adapter-static/test/test.js index 97bef6e5d5ce..4069190d217f 100644 --- a/packages/adapter-static/test/test.js +++ b/packages/adapter-static/test/test.js @@ -7,9 +7,18 @@ run('prerendered', (test) => { assert.ok(fs.existsSync(`${cwd}/build/index.html`)); }); - test('prerenders content', async ({ base, page }) => { + test('prerenders a page', async ({ base, page }) => { await page.goto(base); assert.equal(await page.textContent('h1'), 'This page was prerendered'); + assert.equal(await page.textContent('p'), 'answer: 42'); + }); + + test('prerenders an unreferenced endpoint with explicit `prerender` setting', async ({ cwd }) => { + assert.ok(fs.existsSync(`${cwd}/build/endpoint/explicit.json`)); + }); + + test('prerenders a referenced endpoint with implicit `prerender` setting', async ({ cwd }) => { + assert.ok(fs.existsSync(`${cwd}/build/endpoint/implicit.json`)); }); }); diff --git a/packages/adapter-static/test/utils.js b/packages/adapter-static/test/utils.js index 914f80ec4ad5..5f7bd122e4e8 100644 --- a/packages/adapter-static/test/utils.js +++ b/packages/adapter-static/test/utils.js @@ -38,7 +38,6 @@ export function run(app, callback) { console.error(`---\nstdout:\n${e.stdout}`); console.error(`---\nstderr:\n${e.stderr}`); console.groupEnd(); - assert.unreachable(e.message); } context.cwd = cwd; diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index e434d947fae7..b79fb68d7a93 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -5,57 +5,21 @@ 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_path } from '../../utils/routing.js'; + +const pipe = promisify(pipeline); /** * Creates the Builder which is passed to adapters for building the application. * @param {{ * config: import('types').ValidatedConfig; * build_data: import('types').BuildData; + * routes: import('types').RouteData[]; * prerendered: import('types').Prerendered; * log: import('types').Logger; * }} opts * @returns {import('types').Builder} */ -export function create_builder({ config, build_data, prerendered, log }) { - /** @type {Set} */ - const prerendered_paths = new Set(prerendered.paths); - - /** @param {import('types').RouteData} route */ - // TODO routes should come pre-filtered - function not_prerendered(route) { - const path = route.page && get_path(route.id); - if (path) { - return !prerendered_paths.has(path) && !prerendered_paths.has(path + '/'); - } - - return true; - } - - const pipe = promisify(pipeline); - - /** - * @param {string} file - * @param {'gz' | 'br'} format - */ - async function compress_file(file, format = 'gz') { - const compress = - format == 'br' - ? zlib.createBrotliCompress({ - params: { - [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, - [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY, - [zlib.constants.BROTLI_PARAM_SIZE_HINT]: statSync(file).size - } - }) - : zlib.createGzip({ level: zlib.constants.Z_BEST_COMPRESSION }); - - const source = createReadStream(file); - const destination = createWriteStream(`${file}.${format}`); - - await pipe(source, compress, destination); - } - +export function create_builder({ config, build_data, routes, prerendered, log }) { return { log, rimraf, @@ -66,8 +30,6 @@ export function create_builder({ config, build_data, prerendered, log }) { prerendered, async createEntries(fn) { - const { routes } = build_data.manifest_data; - /** @type {import('types').RouteDefinition[]} */ const facades = routes.map((route) => { const methods = new Set(); @@ -113,7 +75,7 @@ export function create_builder({ config, build_data, prerendered, log }) { } } - const filtered = new Set(group.filter(not_prerendered)); + const filtered = new Set(group); // heuristic: if /foo/[bar] is included, /foo/[bar].json should // also be included, since the page likely needs the endpoint @@ -146,7 +108,7 @@ export function create_builder({ config, build_data, prerendered, log }) { return generate_manifest({ build_data, relative_path: relativePath, - routes: build_data.manifest_data.routes.filter(not_prerendered), + routes, format }); }, @@ -221,3 +183,25 @@ export function create_builder({ config, build_data, prerendered, log }) { } }; } + +/** + * @param {string} file + * @param {'gz' | 'br'} format + */ +async function compress_file(file, format = 'gz') { + const compress = + format == 'br' + ? zlib.createBrotliCompress({ + params: { + [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, + [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY, + [zlib.constants.BROTLI_PARAM_SIZE_HINT]: statSync(file).size + } + }) + : zlib.createGzip({ level: zlib.constants.Z_BEST_COMPRESSION }); + + const source = createReadStream(file); + const destination = createWriteStream(`${file}.${format}`); + + await pipe(source, compress, destination); +} diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js index 2f2c82f4a78f..af35cc19fd51 100644 --- a/packages/kit/src/core/adapt/builder.spec.js +++ b/packages/kit/src/core/adapt/builder.spec.js @@ -29,6 +29,7 @@ test('copy files', () => { config: /** @type {import('types').ValidatedConfig} */ (mocked), // @ts-expect-error build_data: {}, + routes: [], // @ts-expect-error prerendered: { paths: [] diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js index 25d02039f065..26feb567498b 100644 --- a/packages/kit/src/core/adapt/index.js +++ b/packages/kit/src/core/adapt/index.js @@ -5,14 +5,26 @@ import { create_builder } from './builder.js'; * @param {import('types').ValidatedConfig} config * @param {import('types').BuildData} build_data * @param {import('types').Prerendered} prerendered + * @param {import('types').PrerenderMap} prerender_map * @param {{ log: import('types').Logger }} opts */ -export async function adapt(config, build_data, prerendered, { log }) { +export async function adapt(config, build_data, prerendered, prerender_map, { log }) { const { name, adapt } = config.kit.adapter; console.log(colors.bold().cyan(`\n> Using ${name}`)); - const builder = create_builder({ config, build_data, prerendered, log }); + const builder = create_builder({ + config, + build_data, + routes: build_data.manifest_data.routes.filter((route) => { + if (!route.page && !route.endpoint) return false; + + const prerender = prerender_map.get(route.id); + return prerender === false || prerender === undefined || prerender === 'auto'; + }), + prerendered, + log + }); await adapt(builder); log.success('done'); diff --git a/packages/kit/src/core/prerender/prerender.js b/packages/kit/src/core/prerender/prerender.js index 5c89fd3e837f..2cdf7b89d164 100644 --- a/packages/kit/src/core/prerender/prerender.js +++ b/packages/kit/src/core/prerender/prerender.js @@ -9,15 +9,14 @@ import { crawl } from './crawl.js'; import { escape_html_attr } from '../../utils/escape.js'; import { logger } from '../utils.js'; import { load_config } from '../config/index.js'; -import { compact } from '../../utils/array.js'; -import { get_path } from '../../utils/routing.js'; +import { affects_path } from '../../utils/routing.js'; /** * @typedef {import('types').PrerenderErrorHandler} PrerenderErrorHandler * @typedef {import('types').Logger} Logger */ -const [, , client_out_dir, results_path, manifest_path, verbose, env] = process.argv; +const [, , client_out_dir, results_path, verbose, env] = process.argv; prerender(); @@ -58,12 +57,15 @@ const OK = 2; const REDIRECT = 3; /** - * @param {import('types').Prerendered} prerendered + * @param {{ + * prerendered: import('types').Prerendered; + * prerender_map: import('types').PrerenderMap; + * }} data */ -const output_and_exit = (prerendered) => { +const output_and_exit = (data) => { writeFileSync( results_path, - JSON.stringify(prerendered, (_key, value) => + JSON.stringify(data, (_key, value) => value instanceof Map ? Array.from(value.entries()) : value ) ); @@ -79,11 +81,14 @@ export async function prerender() { paths: [] }; + /** @type {import('types').PrerenderMap} */ + const prerender_map = new Map(); + /** @type {import('types').ValidatedKitConfig} */ const config = (await load_config()).kit; if (!config.prerender.enabled) { - output_and_exit(prerendered); + output_and_exit({ prerendered, prerender_map }); return; } @@ -231,6 +236,16 @@ export async function prerender() { const encoded_dependency_path = new URL(dependency_path, 'http://localhost').pathname; const decoded_dependency_path = decodeURI(encoded_dependency_path); + const prerender = result.response.headers.get('x-sveltekit-prerender'); + + if (prerender) { + const route_id = /** @type {string} */ (result.response.headers.get('x-sveltekit-routeid')); + const existing_value = prerender_map.get(route_id); + if (existing_value !== 'auto') { + prerender_map.set(route_id, prerender === 'true' ? true : 'auto'); + } + } + const body = result.body ?? new Uint8Array(await result.response.arrayBuffer()); save( 'dependencies', @@ -340,25 +355,63 @@ export async function prerender() { } } - if (config.prerender.enabled) { - for (const entry of config.prerender.entries) { - if (entry === '*') { - /** @type {import('types').SSRManifest} */ - const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; - const { routes } = manifest._; - const entries = compact(routes.map((route) => route.page && get_path(route.id))); + for (const route of manifest._.routes) { + try { + if (route.endpoint) { + const mod = await route.endpoint(); + if (mod.prerender !== undefined) { + if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) { + throw new Error( + `Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})` + ); + } - for (const entry of entries) { - enqueue(null, config.paths.base + entry); // TODO can we pre-normalize these? + prerender_map.set(route.id, mod.prerender); } - } else { - enqueue(null, config.paths.base + entry); } + + if (route.page) { + // TODO use setting from layouts, in https://github.com/sveltejs/kit/pull/6197 + // const nodes = await Promise.all( + // [...route.page.layouts, route.page.leaf].map((n) => { + // if (n !== undefined) return manifest._.nodes[n](); + // }) + // ); + + // let prerender = config.prerender.default; + // for (const node of nodes) { + // prerender = node?.shared?.prerender ?? node?.server?.prerender ?? prerender; + // } + + const leaf = await manifest._.nodes[route.page.leaf](); + let prerender = + leaf.shared?.prerender ?? leaf.server?.prerender ?? config.prerender.default; + + prerender_map.set(route.id, prerender); + } + } catch (e) { + // We failed to import the module, which indicates it can't be prerendered + // TODO should we catch these? It's almost certainly a bug in the app + console.error(e); } + } - await q.done(); + for (const entry of config.prerender.entries) { + if (entry === '*') { + for (const [id, prerender] of prerender_map) { + if (prerender) { + if (id.includes('[')) continue; + const path = `/${id.split('/').filter(affects_path).join('/')}`; + enqueue(null, config.paths.base + path); + } + } + } else { + enqueue(null, config.paths.base + entry); + } } + await q.done(); + const rendered = await server.respond(new Request(config.prerender.origin + '/[fallback]'), { getClientAddress, prerendering: { @@ -371,7 +424,7 @@ export async function prerender() { mkdirp(dirname(file)); writeFileSync(file, await rendered.text()); - output_and_exit(prerendered); + output_and_exit({ prerendered, prerender_map }); } /** @return {string} */ diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 7f56d9e46ad3..c5de104275da 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -97,6 +97,9 @@ function kit() { /** @type {import('types').Prerendered} */ let prerendered; + /** @type {import('types').PrerenderMap} */ + let prerender_map; + /** @type {import('types').BuildData} */ let build_data; @@ -418,7 +421,6 @@ function kit() { [ vite_config.build.outDir, results_path, - manifest_path, '' + verbose, JSON.stringify({ ...env.private, ...env.public }) ], @@ -431,12 +433,16 @@ function kit() { if (code) { reject(new Error(`Prerendering failed with code ${code}`)); } else { - prerendered = JSON.parse(fs.readFileSync(results_path, 'utf8'), (key, value) => { + const results = JSON.parse(fs.readFileSync(results_path, 'utf8'), (key, value) => { if (key === 'pages' || key === 'assets' || key === 'redirects') { return new Map(value); } return value; }); + + prerendered = results.prerendered; + prerender_map = new Map(results.prerender_map); + fulfil(undefined); } }); @@ -475,7 +481,7 @@ function kit() { if (svelte_config.kit.adapter) { const { adapt } = await import('../../core/adapt/index.js'); - await adapt(svelte_config, build_data, prerendered, { log }); + await adapt(svelte_config, build_data, prerendered, prerender_map, { log }); } else { console.log(colors.bold().yellow('\nNo adapter specified')); diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 2f25bf010f1c..9585e027e66b 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -4,9 +4,10 @@ import { check_method_names, method_not_allowed } from './utils.js'; /** * @param {import('types').RequestEvent} event * @param {import('types').SSREndpoint} mod + * @param {import('types').SSRState} state * @returns {Promise} */ -export async function render_endpoint(event, mod) { +export async function render_endpoint(event, mod, state) { const method = /** @type {import('types').HttpMethod} */ (event.request.method); // TODO: Remove for 1.0 @@ -22,6 +23,16 @@ export async function render_endpoint(event, mod) { return method_not_allowed(mod, method); } + const prerender = mod.prerender ?? state.prerender_default; + + if (prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) { + throw new Error('Cannot prerender endpoints that have mutative methods'); + } + + if (state.prerendering && !prerender) { + throw new Error(`${event.routeId} is not prerenderable`); + } + try { const response = await handler( /** @type {import('types').RequestEvent>} */ (event) @@ -34,6 +45,11 @@ export async function render_endpoint(event, mod) { ); } + if (state.prerendering) { + response.headers.set('x-sveltekit-routeid', /** @type {string} */ (event.routeId)); + response.headers.set('x-sveltekit-prerender', String(prerender)); + } + return response; } catch (error) { if (error instanceof HttpError) { diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 5c03fb045901..7cd89d2ff8a6 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -240,7 +240,7 @@ export async function respond(request, options, state) { } else if (route.page) { response = await render_page(event, route, route.page, options, state, resolve_opts); } else if (route.endpoint) { - response = await render_endpoint(event, await route.endpoint()); + response = await render_endpoint(event, await route.endpoint(), state); } else { // a route will always have a page or an endpoint, but TypeScript // doesn't know that diff --git a/packages/kit/src/runtime/server/page/fetch.js b/packages/kit/src/runtime/server/page/fetch.js index e6ef5aae7c89..efef5f7b3e95 100644 --- a/packages/kit/src/runtime/server/page/fetch.js +++ b/packages/kit/src/runtime/server/page/fetch.js @@ -10,9 +10,10 @@ import { domain_matches, path_matches } from './cookie.js'; * options: import('types').SSROptions; * state: import('types').SSRState; * route: import('types').SSRRoute | import('types').SSRErrorPage; + * prerender_default?: import('types').PrerenderOption; * }} opts */ -export function create_fetch({ event, options, state, route }) { +export function create_fetch({ event, options, state, route, prerender_default }) { /** @type {import('./types').Fetched[]} */ const fetched = []; @@ -135,6 +136,7 @@ export function create_fetch({ event, options, state, route }) { new Request(new URL(requested, event.url).href, { ...opts }), options, { + prerender_default, ...state, initiator: route } diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index d350441a7158..7e50694a3134 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -51,8 +51,6 @@ export async function render_page(event, route, page, options, state, resolve_op } } - const { fetcher, fetched, cookies } = create_fetch({ event, options, state, route }); - try { const nodes = await Promise.all([ // we use == here rather than === because [undefined] serializes as "[null]" @@ -109,24 +107,31 @@ export async function render_page(event, route, page, options, state, resolve_op // it's crucial that we do this before returning the non-SSR response, otherwise // SvelteKit will erroneously believe that the path has been prerendered, // causing functions to be omitted from the manifesst generated later + // TODO incorporate layout options in https://github.com/sveltejs/kit/pull/6197 const should_prerender = leaf_node.shared?.prerender ?? leaf_node.server?.prerender ?? options.prerender.default; if (should_prerender) { const mod = leaf_node.server; if (mod && (mod.POST || mod.PUT || mod.DELETE || mod.PATCH)) { - throw new Error('Cannot prerender pages that have endpoints with mutative methods'); + throw new Error('Cannot prerender pages that have mutative methods'); } } else if (state.prerendering) { // if the page isn't marked as prerenderable (or is explicitly // marked NOT prerenderable, if `prerender.default` is `true`), // then bail out at this point - if (!should_prerender) { - return new Response(undefined, { - status: 204 - }); - } + return new Response(undefined, { + status: 204 + }); } + const { fetcher, fetched, cookies } = create_fetch({ + event, + options, + state, + route, + prerender_default: should_prerender + }); + if (!resolve_opts.ssr) { return await render_response({ branch: [], diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js index 67c6e0c6a887..3ff8c92443cb 100644 --- a/packages/kit/src/utils/routing.js +++ b/packages/kit/src/utils/routing.js @@ -96,15 +96,6 @@ export function affects_path(segment) { return !/^\([^)]+\)$/.test(segment); } -/** - * Turns a route ID into a path, if possible - * @param {string} id - */ -export function get_path(id) { - if (id.includes('[')) return null; - return `/${id.split('/').filter(affects_path).join('/')}`; -} - /** * @param {RegExpMatchArray} match * @param {string[]} names diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 03da4032df27..1e49285d9cb1 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -672,12 +672,12 @@ test.describe.serial('Invalidation', () => { await page.click('button'); await page.waitForLoadState('networkidle'); - await page.waitForTimeout(0); // apparently necessary + await page.waitForTimeout(200); // apparently necessary expect(await page.textContent('h1')).toBe('a: 2, b: 3'); await page.click('button'); await page.waitForLoadState('networkidle'); - await page.waitForTimeout(0); + await page.waitForTimeout(200); expect(await page.textContent('h1')).toBe('a: 4, b: 5'); }); }); diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index c659740d2f98..3720f72f97cd 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -554,7 +554,7 @@ test.describe('Errors', () => { expect(await page.textContent('h1')).toBe('500'); expect(await page.textContent('#message')).toBe( - 'This is your custom error page saying: "Cannot prerender pages that have endpoints with mutative methods"' + 'This is your custom error page saying: "Cannot prerender pages that have mutative methods"' ); }); diff --git a/packages/kit/test/prerendering/basics/src/routes/fetch-image/[...slug]/+server.js b/packages/kit/test/prerendering/basics/src/routes/fetch-image/[...slug]/+server.js index 6d3a67e8e53f..e1494d291ad7 100644 --- a/packages/kit/test/prerendering/basics/src/routes/fetch-image/[...slug]/+server.js +++ b/packages/kit/test/prerendering/basics/src/routes/fetch-image/[...slug]/+server.js @@ -1,5 +1,7 @@ import * as fs from 'fs'; +export const prerender = true; + export async function GET({ params }) { const slug = params.slug.split('/'); const extension = slug[0].split('.').pop(); diff --git a/packages/kit/test/prerendering/basics/src/routes/origin/+page.server.js b/packages/kit/test/prerendering/basics/src/routes/origin/+page.server.js index 984bab08bc55..beb4362c1f23 100644 --- a/packages/kit/test/prerendering/basics/src/routes/origin/+page.server.js +++ b/packages/kit/test/prerendering/basics/src/routes/origin/+page.server.js @@ -1,3 +1,5 @@ +export const prerender = true; + export async function load({ url }) { const res = await fetch(new URL('/origin/message.json', url.origin).href); const { message } = await res.json(); diff --git a/packages/kit/test/prerendering/basics/src/routes/origin/message.json/+server.js b/packages/kit/test/prerendering/basics/src/routes/origin/message.json/+server.js index 2e53051d87ab..04e6ce42c8a8 100644 --- a/packages/kit/test/prerendering/basics/src/routes/origin/message.json/+server.js +++ b/packages/kit/test/prerendering/basics/src/routes/origin/message.json/+server.js @@ -1,5 +1,9 @@ import { json } from '@sveltejs/kit'; +// TODO remove this when we're able to replace the global `fetch` call in the +// neighbouring `+page.server.js` with SvelteKit's `fetch` +export const prerender = true; + export function GET() { return json({ message: 'hello' diff --git a/packages/kit/test/prerendering/basics/src/routes/page-server-options/+page.server.js b/packages/kit/test/prerendering/basics/src/routes/page-server-options/+page.server.js deleted file mode 100644 index d43d0cd2a55d..000000000000 --- a/packages/kit/test/prerendering/basics/src/routes/page-server-options/+page.server.js +++ /dev/null @@ -1 +0,0 @@ -export const prerender = false; diff --git a/packages/kit/test/prerendering/basics/src/routes/page-server-options/+page.svelte b/packages/kit/test/prerendering/basics/src/routes/page-server-options/+page.svelte deleted file mode 100644 index 3c3d6df7a541..000000000000 --- a/packages/kit/test/prerendering/basics/src/routes/page-server-options/+page.svelte +++ /dev/null @@ -1 +0,0 @@ -

This should not be visible

diff --git a/packages/kit/test/prerendering/basics/test/test.js b/packages/kit/test/prerendering/basics/test/test.js index 29a62b3cebe0..44d80bfff518 100644 --- a/packages/kit/test/prerendering/basics/test/test.js +++ b/packages/kit/test/prerendering/basics/test/test.js @@ -112,11 +112,6 @@ test('does not prerender page with shadow endpoint with non-load handler', () => assert.ok(!fs.existsSync(`${build}/shadowed-post/__data.js`)); }); -test('does not prerender page with prerender = false in +page.server.js', () => { - assert.ok(!fs.existsSync(`${build}/page-server-options.html`)); - assert.ok(!fs.existsSync(`${build}/page-server-options/__data.js`)); -}); - test('decodes paths when writing files', () => { let content = read('encoding/path with spaces.html'); assert.ok(content.includes('

path with spaces

')); diff --git a/packages/kit/test/prerendering/trailing-slash/src/routes/standalone-endpoint.json/+server.js b/packages/kit/test/prerendering/trailing-slash/src/routes/standalone-endpoint.json/+server.js index 82d3cb7b8345..f8d1402d3a3f 100644 --- a/packages/kit/test/prerendering/trailing-slash/src/routes/standalone-endpoint.json/+server.js +++ b/packages/kit/test/prerendering/trailing-slash/src/routes/standalone-endpoint.json/+server.js @@ -1,5 +1,7 @@ import { json } from '@sveltejs/kit'; +export const prerender = true; + export async function GET() { return json({ answer: 42 }); } diff --git a/packages/kit/test/prerendering/trailing-slash/svelte.config.js b/packages/kit/test/prerendering/trailing-slash/svelte.config.js index de260fe7c5fb..41249f6dd2f1 100644 --- a/packages/kit/test/prerendering/trailing-slash/svelte.config.js +++ b/packages/kit/test/prerendering/trailing-slash/svelte.config.js @@ -1,5 +1,7 @@ import adapter from '../../../../adapter-static/index.js'; +export const prerender = true; + /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { @@ -8,8 +10,7 @@ const config = { }), prerender: { - default: true, - entries: ['*', '/standalone-endpoint.json'] + default: true }, trailingSlash: 'always' diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index a4b9cb024db4..f7525c71c730 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -19,6 +19,8 @@ import { import { SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js'; import { HttpError, Redirect } from '../src/runtime/control.js'; +export { PrerenderOption } from './private.js'; + export interface Adapter { name: string; adapt(builder: Builder): MaybePromise; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index a5813e4f78e8..d94fc29adb83 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -16,7 +16,13 @@ import { ServerInitOptions, SSRManifest } from './index.js'; -import { HttpMethod, MaybePromise, RequestOptions, TrailingSlash } from './private.js'; +import { + HttpMethod, + MaybePromise, + PrerenderOption, + RequestOptions, + TrailingSlash +} from './private.js'; export interface ServerModule { Server: typeof InternalServer; @@ -267,13 +273,13 @@ export interface SSRNode { shared: { load?: Load; hydrate?: boolean; - prerender?: boolean; + prerender?: PrerenderOption; router?: boolean; }; server: { load?: ServerLoad; - prerender?: boolean; + prerender?: PrerenderOption; POST?: Action; PATCH?: Action; PUT?: Action; @@ -333,7 +339,9 @@ export interface PageNodeIndexes { leaf: number; } -export type SSREndpoint = Partial>; +export type SSREndpoint = Partial> & { + prerender?: PrerenderOption; +}; export interface SSRRoute { id: string; @@ -352,6 +360,11 @@ export interface SSRState { initiator?: SSRRoute | SSRErrorPage; platform?: any; prerendering?: PrerenderOptions; + /** + * When fetching data from a +server.js endpoint in `load`, the page's + * prerender option is inherited by the endpoint, unless overridden + */ + prerender_default?: PrerenderOption; } export type StrictBody = string | Uint8Array; diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts index f517091c29a5..12fe1323ad2c 100644 --- a/packages/kit/types/private.d.ts +++ b/packages/kit/types/private.d.ts @@ -184,6 +184,10 @@ export interface PrerenderErrorHandler { export type PrerenderOnErrorValue = 'fail' | 'continue' | PrerenderErrorHandler; +export type PrerenderOption = boolean | 'auto'; + +export type PrerenderMap = Map; + export interface RequestOptions { getClientAddress: () => string; platform?: App.Platform; diff --git a/sites/kit.svelte.dev/src/routes/content.json/+server.js b/sites/kit.svelte.dev/src/routes/content.json/+server.js index a0333f13e88b..a757fd60764a 100644 --- a/sites/kit.svelte.dev/src/routes/content.json/+server.js +++ b/sites/kit.svelte.dev/src/routes/content.json/+server.js @@ -1,6 +1,8 @@ import { content } from '$lib/search/content.server.js'; import { json } from '@sveltejs/kit'; +export const prerender = true; + /** @type {import('./$types').RequestHandler} */ export function GET() { return json({ diff --git a/sites/kit.svelte.dev/svelte.config.js b/sites/kit.svelte.dev/svelte.config.js index f59272ed425e..e46f95df7b58 100644 --- a/sites/kit.svelte.dev/svelte.config.js +++ b/sites/kit.svelte.dev/svelte.config.js @@ -6,8 +6,7 @@ const config = { adapter: adapter(), prerender: { - default: true, - entries: ['*', '/content.json'] + default: true } } };