From 74016df3c281046d3955f8ee14550e665c438ec7 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Thu, 30 Mar 2023 19:11:39 -0600 Subject: [PATCH 01/21] feat: It basically works lol --- packages/kit/src/core/postbuild/analyse.js | 9 +++- packages/kit/src/core/postbuild/prerender.js | 15 +++++++ packages/kit/src/utils/exports.js | 17 ++++++-- packages/kit/src/utils/options.js | 4 +- packages/kit/src/utils/routing.js | 43 ++++++++++++++++++++ packages/kit/types/internal.d.ts | 6 +++ 6 files changed, 88 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index a9b1a1d1fb51..4a554a4f4120 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -10,6 +10,7 @@ import { load_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; import { should_polyfill } from '../../utils/platform.js'; import { installPolyfills } from '../../exports/node/polyfills.js'; +import { route_from_entry } from '../../utils/routing.js'; export default forked(import.meta.url, analyse); @@ -72,6 +73,8 @@ async function analyse({ manifest_path, env }) { let prerender = undefined; /** @type {any} */ let config = undefined; + /** @type {import('types').PrerenderEntriesGenerator | undefined} */ + let entries = undefined; if (route.endpoint) { const mod = await route.endpoint(); @@ -95,6 +98,7 @@ async function analyse({ manifest_path, env }) { if (mod.OPTIONS) api_methods.push('OPTIONS'); config = mod.config; + entries = mod.entries; } if (route.page) { @@ -125,6 +129,7 @@ async function analyse({ manifest_path, env }) { prerender = get_option(nodes, 'prerender') ?? false; config = get_config(nodes); + entries = get_option(nodes, 'entries'); } metadata.routes.set(route.id, { @@ -136,7 +141,9 @@ async function analyse({ manifest_path, env }) { api: { methods: api_methods }, - prerender + prerender, + entries: + entries && (await entries()).map((entryObject) => route_from_entry(route.id, entryObject)) }); } diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index f37373be44fa..ef7836cbf63c 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -363,9 +363,18 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { saved.set(file, dest); } + /** @type {Array} */ + const route_level_entries = []; + for (const route of metadata.routes.values()) { + if (route.entries) { + route_level_entries.push(...route.entries); + } + } + if ( config.prerender.entries.length > 1 || config.prerender.entries[0] !== '*' || + route_level_entries.length > 0 || prerender_map.size > 0 ) { // Only log if we're actually going to do something to not confuse users @@ -386,6 +395,12 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { } } + console.log({ route_level_entries }); + + for (const entry of route_level_entries) { + enqueue(null, config.paths.base + entry); + } + await q.done(); // handle invalid fragment links diff --git a/packages/kit/src/utils/exports.js b/packages/kit/src/utils/exports.js index 7f78ec0381a7..aa9d8ed145a1 100644 --- a/packages/kit/src/utils/exports.js +++ b/packages/kit/src/utils/exports.js @@ -50,7 +50,16 @@ function hint_for_supported_files(key, ext = '.js') { } } -const valid_common_exports = ['load', 'prerender', 'csr', 'ssr', 'trailingSlash', 'config']; +// TODO: Need to distinguish between pages and layouts; should not be able to export entries from layouts +const valid_common_exports = [ + 'load', + 'prerender', + 'csr', + 'ssr', + 'trailingSlash', + 'config', + 'entries' +]; const valid_page_server_exports = [ 'load', 'prerender', @@ -58,7 +67,8 @@ const valid_page_server_exports = [ 'ssr', 'actions', 'trailingSlash', - 'config' + 'config', + 'entries' ]; const valid_server_exports = [ 'GET', @@ -69,7 +79,8 @@ const valid_server_exports = [ 'OPTIONS', 'prerender', 'trailingSlash', - 'config' + 'config', + 'entries' ]; export const validate_common_exports = validator(valid_common_exports); diff --git a/packages/kit/src/utils/options.js b/packages/kit/src/utils/options.js index a1c9088a4c74..8da664f2529a 100644 --- a/packages/kit/src/utils/options.js +++ b/packages/kit/src/utils/options.js @@ -1,6 +1,6 @@ /** - * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash'} Option - * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : boolean} Value + * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash' | 'entries'} Option + * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : Option extends 'entries' ? import('types').PrerenderEntriesGenerator : boolean} Value * * @param {Array} nodes * @param {Option} option diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js index a5a9fdb938af..5ef8a77f7d74 100644 --- a/packages/kit/src/utils/routing.js +++ b/packages/kit/src/utils/routing.js @@ -96,6 +96,49 @@ export function parse_route_id(id) { return { pattern, params }; } +const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(.+?)(?:=(.+))?\]\]?/; + +/** + * Parses a route ID, then resolves it to a route by replacing parameters with actual values from `entry`. + * @param {string} id The route id + * @param {Record} entry The entry meant to populate the route. For example, if the route is `/blog/[slug]`, the entry would be `{ slug: 'hello-world' }` + * @example + * ```js + * route_from_entry(`/blog/[slug]/[...somethingElse]`, { slug: 'hello-world', somethingElse: 'something/else' }); // `/blog/hello-world/something/else` + * ``` + */ +export function route_from_entry(id, entry) { + const segments = get_route_segments(id); + return ( + '/' + + segments + .map((segment) => { + const match = basic_param_pattern.exec(segment); + + // static content -- i.e. not a param + if (!match) return segment; + + const optional = !!match[1]; + const name = match[2]; + const paramValue = entry[name]; + + // If the param is optional and there's no value, don't do anything to the output string + if (!paramValue && optional) return ''; + + if (!paramValue && !optional) + throw new Error(`Missing parameter '${name}' in route '${id}'`); + + if (paramValue.startsWith('/') || paramValue.endsWith('/')) + throw new Error( + `Parameter '${name}' in route '${id}' cannot start or end with a slash -- this would cause an invalid route like 'foo//bar'` + ); + + return paramValue; + }) + .join('/') + ); +} + /** * Returns `false` for `(group)` segments * @param {string} segment diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 363be1f45397..f2faf0105750 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -264,6 +264,7 @@ export interface ServerMetadataRoute { }; methods: HttpMethod[]; prerender: PrerenderOption | undefined; + entries: Array | undefined; } export interface ServerMetadata { @@ -308,6 +309,7 @@ export interface SSRNode { csr?: boolean; trailingSlash?: TrailingSlash; config?: any; + entries?: PrerenderEntriesGenerator; }; server: { @@ -318,6 +320,7 @@ export interface SSRNode { trailingSlash?: TrailingSlash; actions?: Actions; config?: any; + entries?: PrerenderEntriesGenerator; }; universal_id: string; @@ -355,10 +358,13 @@ export interface PageNodeIndexes { leaf: number; } +export type PrerenderEntriesGenerator = () => MaybePromise>>; + export type SSREndpoint = Partial> & { prerender?: PrerenderOption; trailingSlash?: TrailingSlash; config?: any; + entries?: PrerenderEntriesGenerator; }; export interface SSRRoute { From 05c7d4e1686ccd8b522c48dbda6cf380a3337297 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Sat, 1 Apr 2023 22:10:49 -0600 Subject: [PATCH 02/21] feat: Typings for page routes --- packages/kit/src/core/sync/write_types/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index 0ae180492f12..83abfa3b4fc0 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -442,6 +442,15 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true exports.push(`export type ${prefix}Data = ${data};`); + if ( + proxies?.server?.exports.includes('entries') || + proxies?.universal?.exports.includes('entries') + ) { + exports.push( + `export type EntryGenerator = () => Promise> | Array;` + ); + } + return { declarations, exports, proxies }; /** @@ -560,7 +569,7 @@ function replace_ext_with_js(file_path) { * @returns {Omit, 'file_name'> | null} */ export function tweak_types(content, is_server) { - const names = new Set(is_server ? ['load', 'actions'] : ['load']); + const names = new Set(is_server ? ['load', 'actions', 'entries'] : ['load', 'entries']); try { let modified = false; From 1b2e1e07171a4a8e9f56e6970f7de75b548fe482 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 10:12:11 -0600 Subject: [PATCH 03/21] Revert "feat: Typings for page routes" This reverts commit 05c7d4e1686ccd8b522c48dbda6cf380a3337297. --- packages/kit/src/core/sync/write_types/index.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index 83abfa3b4fc0..0ae180492f12 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -442,15 +442,6 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true exports.push(`export type ${prefix}Data = ${data};`); - if ( - proxies?.server?.exports.includes('entries') || - proxies?.universal?.exports.includes('entries') - ) { - exports.push( - `export type EntryGenerator = () => Promise> | Array;` - ); - } - return { declarations, exports, proxies }; /** @@ -569,7 +560,7 @@ function replace_ext_with_js(file_path) { * @returns {Omit, 'file_name'> | null} */ export function tweak_types(content, is_server) { - const names = new Set(is_server ? ['load', 'actions', 'entries'] : ['load', 'entries']); + const names = new Set(is_server ? ['load', 'actions'] : ['load']); try { let modified = false; From a3a7189c4ee80d1ca5e3cb60b1da026003029c22 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 11:39:40 -0600 Subject: [PATCH 04/21] feat: Types! --- packages/kit/src/core/postbuild/analyse.js | 2 +- packages/kit/src/core/postbuild/prerender.js | 2 -- packages/kit/src/core/sync/write_types/index.js | 6 ++++++ packages/kit/src/utils/options.js | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 4a554a4f4120..a0e0239bae67 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -129,7 +129,7 @@ async function analyse({ manifest_path, env }) { prerender = get_option(nodes, 'prerender') ?? false; config = get_config(nodes); - entries = get_option(nodes, 'entries'); + entries ??= get_option(nodes, 'entries'); } metadata.routes.set(route.id, { diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index ef7836cbf63c..33d8d145eee3 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -395,8 +395,6 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { } } - console.log({ route_level_entries }); - for (const entry of route_level_entries) { enqueue(null, config.paths.base + entry); } diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index 0ae180492f12..eadc9317470f 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -194,6 +194,12 @@ function update_types(config, routes, route, to_delete = new Set()) { .join('; ')} }` ); + if (route.params.length > 0) { + exports.push( + `export type EntryGenerator = () => Promise> | Array;` + ); + } + declarations.push(`type RouteId = '${route.id}';`); // These could also be placed in our public types, but it would bloat them unnecessarily and we may want to change these in the future diff --git a/packages/kit/src/utils/options.js b/packages/kit/src/utils/options.js index 8da664f2529a..92385cfdd530 100644 --- a/packages/kit/src/utils/options.js +++ b/packages/kit/src/utils/options.js @@ -9,7 +9,7 @@ */ export function get_option(nodes, option) { return nodes.reduce((value, node) => { - return /** @type {any} TypeScript's too dumb to understand this */ ( + return /** @type {Value} TypeScript's too dumb to understand this */ ( node?.universal?.[option] ?? node?.server?.[option] ?? value ); }, /** @type {Value | undefined} */ (undefined)); From d0389cabe9dc73a21ed4127c798781f61f4d6981 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 11:57:57 -0600 Subject: [PATCH 05/21] fix: naming --- packages/kit/src/core/postbuild/analyse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index a0e0239bae67..41741b17333d 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -143,7 +143,7 @@ async function analyse({ manifest_path, env }) { }, prerender, entries: - entries && (await entries()).map((entryObject) => route_from_entry(route.id, entryObject)) + entries && (await entries()).map((entry_object) => route_from_entry(route.id, entry_object)) }); } From fa48df5c52239d428114a56bdf112c0ff57dec96 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 12:00:49 -0600 Subject: [PATCH 06/21] fix: naming --- packages/kit/src/core/postbuild/analyse.js | 2 +- packages/kit/src/utils/options.js | 2 +- packages/kit/types/internal.d.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 41741b17333d..1dff80adfffa 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -73,7 +73,7 @@ async function analyse({ manifest_path, env }) { let prerender = undefined; /** @type {any} */ let config = undefined; - /** @type {import('types').PrerenderEntriesGenerator | undefined} */ + /** @type {import('types').PrerenderEntryGenerator | undefined} */ let entries = undefined; if (route.endpoint) { diff --git a/packages/kit/src/utils/options.js b/packages/kit/src/utils/options.js index 92385cfdd530..b2f42882bec2 100644 --- a/packages/kit/src/utils/options.js +++ b/packages/kit/src/utils/options.js @@ -1,6 +1,6 @@ /** * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash' | 'entries'} Option - * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : Option extends 'entries' ? import('types').PrerenderEntriesGenerator : boolean} Value + * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : Option extends 'entries' ? import('types').PrerenderEntryGenerator : boolean} Value * * @param {Array} nodes * @param {Option} option diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index f2faf0105750..aa84cacdb873 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -309,7 +309,7 @@ export interface SSRNode { csr?: boolean; trailingSlash?: TrailingSlash; config?: any; - entries?: PrerenderEntriesGenerator; + entries?: PrerenderEntryGenerator; }; server: { @@ -320,7 +320,7 @@ export interface SSRNode { trailingSlash?: TrailingSlash; actions?: Actions; config?: any; - entries?: PrerenderEntriesGenerator; + entries?: PrerenderEntryGenerator; }; universal_id: string; @@ -358,13 +358,13 @@ export interface PageNodeIndexes { leaf: number; } -export type PrerenderEntriesGenerator = () => MaybePromise>>; +export type PrerenderEntryGenerator = () => MaybePromise>>; export type SSREndpoint = Partial> & { prerender?: PrerenderOption; trailingSlash?: TrailingSlash; config?: any; - entries?: PrerenderEntriesGenerator; + entries?: PrerenderEntryGenerator; }; export interface SSRRoute { From 7412ea5185528fb0e20041146f3101ed87d86292 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 18:06:09 -0600 Subject: [PATCH 07/21] fix: More future-proof types --- packages/kit/src/utils/options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/utils/options.js b/packages/kit/src/utils/options.js index b2f42882bec2..46a32535128c 100644 --- a/packages/kit/src/utils/options.js +++ b/packages/kit/src/utils/options.js @@ -1,6 +1,6 @@ /** * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash' | 'entries'} Option - * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : Option extends 'entries' ? import('types').PrerenderEntryGenerator : boolean} Value + * @template {(import('types').SSRNode['universal'] | import('types').SSRNode['server'])[Option]} Value * * @param {Array} nodes * @param {Option} option From e21340c9558c80de3f18a6c1a5ce1645f5207f10 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 19:16:29 -0600 Subject: [PATCH 08/21] feat: Throw by default if entry generator matches a different route ID --- packages/kit/src/core/config/index.spec.js | 1 + packages/kit/src/core/config/options.js | 6 +++ packages/kit/src/core/postbuild/prerender.js | 44 ++++++++++++++++---- packages/kit/types/index.d.ts | 2 + packages/kit/types/private.d.ts | 9 ++++ 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 78142e7623b0..c637c29358a8 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -107,6 +107,7 @@ const get_defaults = (prefix = '') => ({ entries: ['*'], handleHttpError: 'fail', handleMissingId: 'fail', + handleEntryGeneratorMismatch: 'fail', origin: 'http://sveltekit-prerender' }, version: { diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index cdaf2d3be02c..38a0034d474d 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -213,6 +213,12 @@ const options = object( throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`); }), + handleEntryGeneratorMismatch: validate('fail', (input, keypath) => { + if (typeof input === 'function') return input; + if (['fail', 'warn', 'ignore'].includes(input)) return input; + throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`); + }), + origin: validate('http://sveltekit-prerender', (input, keypath) => { assert_string(input, keypath); diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 33d8d145eee3..12edce8b7d09 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -127,6 +127,14 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { } ); + const handle_entry_generator_mismatch = normalise_error_handler( + log, + config.prerender.handleEntryGeneratorMismatch, + ({ generatedFromId, entry, matchedId }) => { + return `The entries export from ${generatedFromId} generated entry ${entry}, which was matched by ${matchedId} - see the \`handleEntryGeneratorMismatch\` option in https://kit.svelte.dev/docs/configuration#prerender for more info.`; + } + ); + const q = queue(config.prerender.concurrency); /** @@ -164,23 +172,25 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { * @param {string | null} referrer * @param {string} decoded * @param {string} [encoded] + * @param {string} [generated_from_id] */ - function enqueue(referrer, decoded, encoded) { + function enqueue(referrer, decoded, encoded, generated_from_id) { if (seen.has(decoded)) return; seen.add(decoded); const file = decoded.slice(config.paths.base.length + 1); if (files.has(file)) return; - return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer)); + return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id)); } /** * @param {string} decoded * @param {string} encoded * @param {string?} referrer + * @param {string} [generated_from_id] */ - async function visit(decoded, encoded, referrer) { + async function visit(decoded, encoded, referrer, generated_from_id) { if (!decoded.startsWith(config.paths.base)) { handle_http_error({ status: 404, path: decoded, referrer, referenceType: 'linked' }); return; @@ -206,6 +216,20 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { } }); + const encoded_id = response.headers.get('x-sveltekit-routeid'); + const decoded_id = encoded_id && decode_uri(encoded_id); + if ( + decoded_id !== null && + generated_from_id !== undefined && + decoded_id !== generated_from_id + ) { + handle_entry_generator_mismatch({ + generatedFromId: generated_from_id, + entry: decoded, + matchedId: decoded_id + }); + } + const body = Buffer.from(await response.arrayBuffer()); save('pages', response, body, decoded, encoded, referrer, 'linked'); @@ -363,11 +387,11 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { saved.set(file, dest); } - /** @type {Array} */ + /** @type {Array<{ id: string, entries: Array}>} */ const route_level_entries = []; - for (const route of metadata.routes.values()) { - if (route.entries) { - route_level_entries.push(...route.entries); + for (const [id, { entries }] of metadata.routes.entries()) { + if (entries) { + route_level_entries.push({ id, entries }); } } @@ -395,8 +419,10 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { } } - for (const entry of route_level_entries) { - enqueue(null, config.paths.base + entry); + for (const { id, entries } of route_level_entries) { + for (const entry of entries) { + enqueue(null, config.paths.base + entry, undefined, id); + } } await q.done(); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index c2019c4e27b0..5e193daafc18 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -11,6 +11,7 @@ import { Logger, MaybePromise, Prerendered, + PrerenderEntryGeneratorMismatchHandlerValue, PrerenderHttpErrorHandlerValue, PrerenderMissingIdHandlerValue, PrerenderOption, @@ -525,6 +526,7 @@ export interface KitConfig { * @default "fail" */ handleMissingId?: PrerenderMissingIdHandlerValue; + handleEntryGeneratorMismatch?: PrerenderEntryGeneratorMismatchHandlerValue; /** * The value of `url.origin` during prerendering; useful if it is included in rendered content. * @default "http://sveltekit-prerender" diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts index 4ca5b9752279..ad02da171242 100644 --- a/packages/kit/types/private.d.ts +++ b/packages/kit/types/private.d.ts @@ -205,8 +205,17 @@ export interface PrerenderMissingIdHandler { (details: { path: string; id: string; referrers: string[]; message: string }): void; } +export interface PrerenderEntryGeneratorMismatchHandler { + (details: { generatedFromId: string; entry: string; matchedId: string; message: string }): void; +} + export type PrerenderHttpErrorHandlerValue = 'fail' | 'warn' | 'ignore' | PrerenderHttpErrorHandler; export type PrerenderMissingIdHandlerValue = 'fail' | 'warn' | 'ignore' | PrerenderMissingIdHandler; +export type PrerenderEntryGeneratorMismatchHandlerValue = + | 'fail' + | 'warn' + | 'ignore' + | PrerenderEntryGeneratorMismatchHandler; export type PrerenderOption = boolean | 'auto'; From 7f885574bbcd90c3ee07a44eae781a2cd9867667 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 20:00:08 -0600 Subject: [PATCH 09/21] feat: Better validation, separage +page and +layout --- packages/kit/src/core/postbuild/analyse.js | 10 +- packages/kit/src/runtime/client/client.js | 4 +- packages/kit/src/runtime/server/respond.js | 13 ++- packages/kit/src/utils/exports.js | 57 +++++------ packages/kit/src/utils/exports.spec.js | 104 ++++++++++++++++++--- 5 files changed, 138 insertions(+), 50 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 1dff80adfffa..f8cb111abe53 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -2,7 +2,9 @@ import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { get_option } from '../../utils/options.js'; import { - validate_common_exports, + validate_layout_exports, + validate_layout_server_exports, + validate_page_exports, validate_page_server_exports, validate_server_exports } from '../../utils/exports.js'; @@ -113,8 +115,8 @@ async function analyse({ manifest_path, env }) { for (const layout of layouts) { if (layout) { - validate_common_exports(layout.server, layout.server_id); - validate_common_exports(layout.universal, layout.universal_id); + validate_layout_server_exports(layout.server, layout.server_id); + validate_layout_exports(layout.universal, layout.universal_id); } } @@ -123,7 +125,7 @@ async function analyse({ manifest_path, env }) { if (page.server?.actions) page_methods.push('POST'); validate_page_server_exports(page.server, page.server_id); - validate_common_exports(page.universal, page.universal_id); + validate_page_exports(page.universal, page.universal_id); } prerender = get_option(nodes, 'prerender') ?? false; diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index a049d2d3eeaf..d91104c8cfb8 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -31,7 +31,7 @@ import { stores } from './singletons.js'; import { unwrap_promises } from '../../utils/promises.js'; import * as devalue from 'devalue'; import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './constants.js'; -import { validate_common_exports } from '../../utils/exports.js'; +import { validate_page_exports } from '../../utils/exports.js'; import { compact } from '../../utils/array.js'; import { validate_depends } from '../shared.js'; @@ -594,7 +594,7 @@ export function create_client(app, target) { const node = await loader(); if (DEV) { - validate_common_exports(node.universal); + validate_page_exports(node.universal); } if (node.universal?.load) { diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 13d89588adca..79222ebad4dc 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -20,7 +20,9 @@ import { add_cookies_to_headers, get_cookies } from './cookie.js'; import { create_fetch } from './fetch.js'; import { Redirect } from '../control.js'; import { - validate_common_exports, + validate_layout_exports, + validate_layout_server_exports, + validate_page_exports, validate_page_server_exports, validate_server_exports } from '../../utils/exports.js'; @@ -185,8 +187,11 @@ export async function respond(request, options, manifest, state) { for (const layout of layouts) { if (layout) { - validate_common_exports(layout.server, /** @type {string} */ (layout.server_id)); - validate_common_exports( + validate_layout_server_exports( + layout.server, + /** @type {string} */ (layout.server_id) + ); + validate_layout_exports( layout.universal, /** @type {string} */ (layout.universal_id) ); @@ -195,7 +200,7 @@ export async function respond(request, options, manifest, state) { if (page) { validate_page_server_exports(page.server, /** @type {string} */ (page.server_id)); - validate_common_exports(page.universal, /** @type {string} */ (page.universal_id)); + validate_page_exports(page.universal, /** @type {string} */ (page.universal_id)); } } diff --git a/packages/kit/src/utils/exports.js b/packages/kit/src/utils/exports.js index aa9d8ed145a1..bcf830926028 100644 --- a/packages/kit/src/utils/exports.js +++ b/packages/kit/src/utils/exports.js @@ -1,9 +1,7 @@ /** - * @param {string[]} expected + * @param {Set} expected */ function validator(expected) { - const set = new Set(expected); - /** * @param {any} module * @param {string} [file] @@ -12,11 +10,13 @@ function validator(expected) { if (!module) return; for (const key in module) { - if (key[0] === '_' || set.has(key)) continue; // key is valid in this module + if (key[0] === '_' || expected.has(key)) continue; // key is valid in this module + + const values = [...expected.values()]; const hint = hint_for_supported_files(key, file?.slice(file.lastIndexOf('.'))) ?? - `valid exports are ${expected.join(', ')}, or anything with a '_' prefix`; + `valid exports are ${values.join(', ')}, or anything with a '_' prefix`; throw new Error(`Invalid export '${key}'${file ? ` in ${file}` : ''} (${hint})`); } @@ -33,44 +33,45 @@ function validator(expected) { function hint_for_supported_files(key, ext = '.js') { let supported_files = []; - if (valid_common_exports.includes(key)) { + if (valid_layout_exports.has(key)) { + supported_files.push(`+layout${ext}`); + } + + if (valid_page_exports.has(key)) { supported_files.push(`+page${ext}`); } - if (valid_page_server_exports.includes(key)) { + if (valid_layout_server_exports.has(key)) { + supported_files.push(`+layout.server${ext}`); + } + + if (valid_page_server_exports.has(key)) { supported_files.push(`+page.server${ext}`); } - if (valid_server_exports.includes(key)) { + if (valid_server_exports.has(key)) { supported_files.push(`+server${ext}`); } if (supported_files.length > 0) { - return `'${key}' is a valid export in ${supported_files.join(` or `)}`; + return `'${key}' is a valid export in ${supported_files.slice(0, -1).join(`, `)}${ + supported_files.length > 1 ? ' or ' : '' + }${supported_files.at(-1)}`; } } -// TODO: Need to distinguish between pages and layouts; should not be able to export entries from layouts -const valid_common_exports = [ +const valid_layout_exports = new Set([ 'load', 'prerender', 'csr', 'ssr', 'trailingSlash', - 'config', - 'entries' -]; -const valid_page_server_exports = [ - 'load', - 'prerender', - 'csr', - 'ssr', - 'actions', - 'trailingSlash', - 'config', - 'entries' -]; -const valid_server_exports = [ + 'config' +]); +const valid_page_exports = new Set([...valid_layout_exports, 'entries']); +const valid_layout_server_exports = new Set([...valid_layout_exports, 'actions']); +const valid_page_server_exports = new Set([...valid_layout_server_exports, 'entries']); +const valid_server_exports = new Set([ 'GET', 'POST', 'PATCH', @@ -81,8 +82,10 @@ const valid_server_exports = [ 'trailingSlash', 'config', 'entries' -]; +]); -export const validate_common_exports = validator(valid_common_exports); +export const validate_layout_exports = validator(valid_layout_exports); +export const validate_page_exports = validator(valid_page_exports); +export const validate_layout_server_exports = validator(valid_layout_server_exports); export const validate_page_server_exports = validator(valid_page_server_exports); export const validate_server_exports = validator(valid_server_exports); diff --git a/packages/kit/src/utils/exports.spec.js b/packages/kit/src/utils/exports.spec.js index bbca70d0e7a1..fbbdf0be46a5 100644 --- a/packages/kit/src/utils/exports.spec.js +++ b/packages/kit/src/utils/exports.spec.js @@ -1,7 +1,9 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; import { - validate_common_exports, + validate_layout_exports, + validate_layout_server_exports, + validate_page_exports, validate_page_server_exports, validate_server_exports } from './exports.js'; @@ -22,41 +24,117 @@ function check_error(fn, message) { assert.equal(error?.message, message); } -test('validates +layout.server.js, +layout.js, +page.js', () => { - validate_common_exports({ - load: () => {} +test('validates +layout.js', () => { + validate_layout_exports({ + load: () => {}, + prerender: false, + csr: false, + ssr: false, + trailingSlash: false, + config: {} }); - validate_common_exports({ + validate_layout_exports({ _unknown: () => {} }); check_error(() => { - validate_common_exports({ + validate_layout_exports({ answer: 42 }); }, `Invalid export 'answer' (valid exports are load, prerender, csr, ssr, trailingSlash, config, or anything with a '_' prefix)`); check_error(() => { - validate_common_exports( + validate_layout_exports( + { + actions: {} + }, + 'src/routes/foo/+page.ts' + ); + }, `Invalid export 'actions' in src/routes/foo/+page.ts ('actions' is a valid export in +layout.server.ts or +page.server.ts)`); + + check_error(() => { + validate_layout_exports({ + GET: {} + }); + }, `Invalid export 'GET' ('GET' is a valid export in +server.js)`); +}); + +test('validates +page.js', () => { + validate_page_exports({ + load: () => {}, + prerender: false, + csr: false, + ssr: false, + trailingSlash: false, + config: {}, + entries: () => {} + }); + + validate_page_exports({ + _unknown: () => {} + }); + + check_error(() => { + validate_page_exports({ + answer: 42 + }); + }, `Invalid export 'answer' (valid exports are load, prerender, csr, ssr, trailingSlash, config, entries, or anything with a '_' prefix)`); + + check_error(() => { + validate_page_exports( { actions: {} }, 'src/routes/foo/+page.ts' ); - }, `Invalid export 'actions' in src/routes/foo/+page.ts ('actions' is a valid export in +page.server.ts)`); + }, `Invalid export 'actions' in src/routes/foo/+page.ts ('actions' is a valid export in +layout.server.ts or +page.server.ts)`); check_error(() => { - validate_common_exports({ + validate_page_exports({ GET: {} }); }, `Invalid export 'GET' ('GET' is a valid export in +server.js)`); }); +test('validates +layout.server.js', () => { + validate_layout_server_exports({ + load: () => {}, + prerender: false, + csr: false, + ssr: false, + trailingSlash: false, + config: {}, + actions: {} + }); + + validate_layout_server_exports({ + _unknown: () => {} + }); + + check_error(() => { + validate_layout_server_exports({ + answer: 42 + }); + }, `Invalid export 'answer' (valid exports are load, prerender, csr, ssr, trailingSlash, config, actions, or anything with a '_' prefix)`); + + check_error(() => { + validate_layout_server_exports({ + POST: {} + }); + }, `Invalid export 'POST' ('POST' is a valid export in +server.js)`); +}); + test('validates +page.server.js', () => { validate_page_server_exports({ load: () => {}, - actions: {} + prerender: false, + csr: false, + ssr: false, + trailingSlash: false, + config: {}, + actions: {}, + entries: () => {} }); validate_page_server_exports({ @@ -67,7 +145,7 @@ test('validates +page.server.js', () => { validate_page_server_exports({ answer: 42 }); - }, `Invalid export 'answer' (valid exports are load, prerender, csr, ssr, actions, trailingSlash, config, or anything with a '_' prefix)`); + }, `Invalid export 'answer' (valid exports are load, prerender, csr, ssr, trailingSlash, config, actions, entries, or anything with a '_' prefix)`); check_error(() => { validate_page_server_exports({ @@ -89,13 +167,13 @@ test('validates +server.js', () => { validate_server_exports({ answer: 42 }); - }, `Invalid export 'answer' (valid exports are GET, POST, PATCH, PUT, DELETE, OPTIONS, prerender, trailingSlash, config, or anything with a '_' prefix)`); + }, `Invalid export 'answer' (valid exports are GET, POST, PATCH, PUT, DELETE, OPTIONS, prerender, trailingSlash, config, entries, or anything with a '_' prefix)`); check_error(() => { validate_server_exports({ csr: false }); - }, `Invalid export 'csr' ('csr' is a valid export in +page.js or +page.server.js)`); + }, `Invalid export 'csr' ('csr' is a valid export in +layout.js, +page.js, +layout.server.js or +page.server.js)`); }); test.run(); From d6655f40e927875e189856cdeb676078b6c8e7c0 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 21:18:04 -0600 Subject: [PATCH 10/21] feat: Tests for mismatch errors --- .../package.json | 20 +++++++++++++++ .../src/app.html | 12 +++++++++ .../src/routes/+layout.js | 1 + .../src/routes/[slug]/[notSpecific]/+page.ts | 4 +++ .../src/routes/[slug]/specific/+page.svelte | 1 + .../static/favicon.png | Bin 0 -> 1571 bytes .../svelte.config.js | 10 ++++++++ .../tsconfig.json | 15 ++++++++++++ .../vite.config.js | 23 ++++++++++++++++++ .../kit/test/build-errors/prerender.spec.js | 12 +++++++++ pnpm-lock.yaml | 18 +++++++++++++- 11 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/package.json create mode 100644 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/app.html create mode 100644 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/+layout.js create mode 100644 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/[slug]/[notSpecific]/+page.ts create mode 100644 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/[slug]/specific/+page.svelte create mode 100644 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/static/favicon.png create mode 100644 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/svelte.config.js create mode 100644 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/tsconfig.json create mode 100644 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/vite.config.js diff --git a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/package.json b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/package.json new file mode 100644 index 000000000000..a1176e486f96 --- /dev/null +++ b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/package.json @@ -0,0 +1,20 @@ +{ + "name": "prerenderable-incorrect-fragment", + "private": true, + "version": "0.0.1", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && tsc && svelte-check" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "workspace:^", + "@sveltejs/kit": "workspace:^", + "svelte": "^3.56.0", + "svelte-check": "^3.0.2", + "typescript": "^4.9.4", + "vite": "^4.2.0" + }, + "type": "module" +} diff --git a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/app.html b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/app.html new file mode 100644 index 000000000000..5b53ef7e3ae7 --- /dev/null +++ b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/+layout.js b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/+layout.js new file mode 100644 index 000000000000..189f71e2e1b3 --- /dev/null +++ b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/+layout.js @@ -0,0 +1 @@ +export const prerender = true; diff --git a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/[slug]/[notSpecific]/+page.ts b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/[slug]/[notSpecific]/+page.ts new file mode 100644 index 000000000000..9e91d7e49886 --- /dev/null +++ b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/[slug]/[notSpecific]/+page.ts @@ -0,0 +1,4 @@ +/** @type {import('./$types').EntryGenerator} */ +export const entries = () => { + return [{ slug: 'whatever', notSpecific: 'specific' }]; +}; diff --git a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/[slug]/specific/+page.svelte b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/[slug]/specific/+page.svelte new file mode 100644 index 000000000000..2a38aa473510 --- /dev/null +++ b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/src/routes/[slug]/specific/+page.svelte @@ -0,0 +1 @@ +
This will be matched
diff --git a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/static/favicon.png b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH { ); }); +test('entry generators should match their own route', () => { + assert.throws( + () => + execSync('pnpm build', { + cwd: path.join(process.cwd(), 'apps/entry-generator-mismatch'), + stdio: 'pipe', + timeout: 60000 + }), + `The entries export from /[slug]/[notSpecific] generated entry /whatever/specific, which was matched by /[slug]/specific - see the \`handleEntryGeneratorMismatch\` option in https://kit.svelte.dev/docs/configuration#prerender for more info.` + ); +}); + test.run(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c7b645652e2..94e918baed28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -432,6 +432,22 @@ importers: devDependencies: uvu: 0.5.6 + packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch: + specifiers: + '@sveltejs/adapter-auto': workspace:^ + '@sveltejs/kit': workspace:^ + svelte: ^3.56.0 + svelte-check: ^3.0.2 + typescript: ^4.9.4 + vite: ^4.2.0 + devDependencies: + '@sveltejs/adapter-auto': link:../../../../../adapter-auto + '@sveltejs/kit': link:../../../.. + svelte: 3.56.0 + svelte-check: 3.0.2_svelte@3.56.0 + typescript: 4.9.4 + vite: 4.2.0 + packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment: specifiers: '@sveltejs/adapter-auto': workspace:^ @@ -4638,7 +4654,7 @@ packages: '@typescript/twoslash': 3.1.0 '@typescript/vfs': 1.3.4 shiki: 0.10.1 - typescript: 4.9.4 + typescript: 5.0.2 transitivePeerDependencies: - supports-color dev: true From d369212234041d471a89657f755ad7e607532c7a Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 21:34:05 -0600 Subject: [PATCH 11/21] feat: Tests for route_from_entry --- packages/kit/src/utils/routing.js | 15 ++++---- packages/kit/src/utils/routing.spec.js | 47 ++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js index 5ef8a77f7d74..f36a29ce06a4 100644 --- a/packages/kit/src/utils/routing.js +++ b/packages/kit/src/utils/routing.js @@ -101,7 +101,7 @@ const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(.+?)(?:=(.+))?\]\]?/; /** * Parses a route ID, then resolves it to a route by replacing parameters with actual values from `entry`. * @param {string} id The route id - * @param {Record} entry The entry meant to populate the route. For example, if the route is `/blog/[slug]`, the entry would be `{ slug: 'hello-world' }` + * @param {Record} entry The entry meant to populate the route. For example, if the route is `/blog/[slug]`, the entry would be `{ slug: 'hello-world' }` * @example * ```js * route_from_entry(`/blog/[slug]/[...somethingElse]`, { slug: 'hello-world', somethingElse: 'something/else' }); // `/blog/hello-world/something/else` @@ -122,19 +122,20 @@ export function route_from_entry(id, entry) { const name = match[2]; const paramValue = entry[name]; - // If the param is optional and there's no value, don't do anything to the output string - if (!paramValue && optional) return ''; - - if (!paramValue && !optional) - throw new Error(`Missing parameter '${name}' in route '${id}'`); + // This is nested so TS correctly narrows the type + if (!paramValue) { + if (optional) return ''; + throw new Error(`Missing parameter '${name}' in route ${id}`); + } if (paramValue.startsWith('/') || paramValue.endsWith('/')) throw new Error( - `Parameter '${name}' in route '${id}' cannot start or end with a slash -- this would cause an invalid route like 'foo//bar'` + `Parameter '${name}' in route ${id} cannot start or end with a slash -- this would cause an invalid route like foo//bar` ); return paramValue; }) + .filter(Boolean) .join('/') ); } diff --git a/packages/kit/src/utils/routing.spec.js b/packages/kit/src/utils/routing.spec.js index 4ba8737fe479..98b9e3f7761d 100644 --- a/packages/kit/src/utils/routing.spec.js +++ b/packages/kit/src/utils/routing.spec.js @@ -1,6 +1,6 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import { exec, parse_route_id } from './routing.js'; +import { exec, parse_route_id, route_from_entry } from './routing.js'; const tests = { '/': { @@ -218,9 +218,52 @@ for (const { path, route, expected } of exec_tests) { }); } -test('errors on bad param name', () => { +test('parse_route_id errors on bad param name', () => { assert.throws(() => parse_route_id('abc/[b-c]'), /Invalid param: b-c/); assert.throws(() => parse_route_id('abc/[bc=d-e]'), /Invalid param: bc=d-e/); }); +const from_entry_tests = [ + { + route: '/blog/[one]/[two]', + entry: { one: 'one', two: 'two' }, + expected: '/blog/one/two' + }, + { + route: '/blog/[one=matcher]/[...two]', + entry: { one: 'one', two: 'two/three' }, + expected: '/blog/one/two/three' + }, + { + route: '/blog/[one=matcher]/[[two]]', + entry: { one: 'one' }, + expected: '/blog/one' + } +]; + +for (const { route, entry, expected } of from_entry_tests) { + test(`route_from_entry generates correct path for ${route}`, () => { + const result = route_from_entry(route, entry); + assert.equal(result, expected); + }); +} + +test('route_from_entry errors on missing entry for required param', () => { + assert.throws( + () => route_from_entry('/blog/[one]/[two]', { one: 'one' }), + "Missing param 'two' in route /blog/[one]/[two]" + ); +}); + +test('route_from_entry errors on entry values starting or ending with slashes', () => { + assert.throws( + () => route_from_entry('/blog/[one]/[two]', { one: 'one', two: '/two' }), + "Parameter 'two' in route /blog/[one]/[two] cannot start or end with a slash -- this would cause an invalid route like foo//bar" + ); + assert.throws( + () => route_from_entry('/blog/[one]/[two]', { one: 'one', two: 'two/' }), + "Parameter 'two' in route /blog/[one]/[two] cannot start or end with a slash -- this would cause an invalid route like foo//bar" + ); +}); + test.run(); From a07bf200ed187fb1deb38bb1e694d557f6f3ca02 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 21:34:31 -0600 Subject: [PATCH 12/21] fix: naming --- packages/kit/src/utils/routing.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js index f36a29ce06a4..06f3a6bc22f2 100644 --- a/packages/kit/src/utils/routing.js +++ b/packages/kit/src/utils/routing.js @@ -120,20 +120,20 @@ export function route_from_entry(id, entry) { const optional = !!match[1]; const name = match[2]; - const paramValue = entry[name]; + const param_value = entry[name]; // This is nested so TS correctly narrows the type - if (!paramValue) { + if (!param_value) { if (optional) return ''; throw new Error(`Missing parameter '${name}' in route ${id}`); } - if (paramValue.startsWith('/') || paramValue.endsWith('/')) + if (param_value.startsWith('/') || param_value.endsWith('/')) throw new Error( `Parameter '${name}' in route ${id} cannot start or end with a slash -- this would cause an invalid route like foo//bar` ); - return paramValue; + return param_value; }) .filter(Boolean) .join('/') From df1acb9736bdb1ab5f6c8e3c8de30bf122bda9dc Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 21:38:16 -0600 Subject: [PATCH 13/21] feat: typedoc --- packages/kit/types/index.d.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 5e193daafc18..8d5222152ed9 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -516,7 +516,7 @@ export interface KitConfig { */ handleHttpError?: PrerenderHttpErrorHandlerValue; /** - * How to respond to hash links from one prerendered page to another that don't correspond to an `id` on the destination page + * How to respond when hash links from one prerendered page to another don't correspond to an `id` on the destination page. * * - `'fail'` — fail the build * - `'ignore'` - silently ignore the failure and continue @@ -526,6 +526,16 @@ export interface KitConfig { * @default "fail" */ handleMissingId?: PrerenderMissingIdHandlerValue; + /** + * How to respond when an entry generated by the `entries` export doesn't match the route it was generated from. + * + * - `'fail'` — fail the build + * - `'ignore'` - silently ignore the failure and continue + * - `'warn'` — continue, but print a warning + * - `(details) => void` — a custom error handler that takes a `details` object with `path`, `id`, `referrers` and `message` properties. If you `throw` from this function, the build will fail + * + * @default "fail" + */ handleEntryGeneratorMismatch?: PrerenderEntryGeneratorMismatchHandlerValue; /** * The value of `url.origin` during prerendering; useful if it is included in rendered content. From 7758ea91e0356cad32033a388a6a15b24813694a Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 21:39:27 -0600 Subject: [PATCH 14/21] fix: blargh --- packages/kit/types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 8d5222152ed9..57ce37863841 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -532,7 +532,7 @@ export interface KitConfig { * - `'fail'` — fail the build * - `'ignore'` - silently ignore the failure and continue * - `'warn'` — continue, but print a warning - * - `(details) => void` — a custom error handler that takes a `details` object with `path`, `id`, `referrers` and `message` properties. If you `throw` from this function, the build will fail + * - `(details) => void` — a custom error handler that takes a `details` object with `generatedFromId`, `entry`, `matchedId` and `message` properties. If you `throw` from this function, the build will fail * * @default "fail" */ From b0e0a54b5806cc6b307b6729c99bb29ee9a6c46f Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 21:43:02 -0600 Subject: [PATCH 15/21] feat: One-line docs -- where should we put an example? --- documentation/docs/20-core-concepts/40-page-options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/40-page-options.md b/documentation/docs/20-core-concepts/40-page-options.md index 434f8eaa179e..a8d2c8fd74a6 100644 --- a/documentation/docs/20-core-concepts/40-page-options.md +++ b/documentation/docs/20-core-concepts/40-page-options.md @@ -33,7 +33,7 @@ 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 files for any prerenderable pages or `+server.js` routes 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](configuration#prerender). +The prerenderer will start at the root of your app and generate files for any prerenderable pages or `+server.js` routes 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](configuration#prerender), or by exporting an entry generator from your dynamic route. While prerendering, the value of `building` imported from [`$app/environment`](modules#$app-environment) will be `true`. From 41d78951414ddd8071fe0c7c0796726615355d83 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 22:17:30 -0600 Subject: [PATCH 16/21] fix: nittiest of nits --- packages/kit/src/utils/routing.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js index 06f3a6bc22f2..8d7c766fc466 100644 --- a/packages/kit/src/utils/routing.js +++ b/packages/kit/src/utils/routing.js @@ -96,7 +96,7 @@ export function parse_route_id(id) { return { pattern, params }; } -const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(.+?)(?:=(.+))?\]\]?/; +const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/; /** * Parses a route ID, then resolves it to a route by replacing parameters with actual values from `entry`. @@ -140,6 +140,17 @@ export function route_from_entry(id, entry) { ); } +const optional_param_regex = /\/\[\[\w+?(?:=\w+)?\]\]/; + +/** + * Removes optional params from a route ID. + * @param {string} id + * @returns The route id with optional params removed + */ +export function remove_optional_params(id) { + return id.replace(optional_param_regex, ''); +} + /** * Returns `false` for `(group)` segments * @param {string} segment From 0ae2351551d2ae3e3cb964f0b6c1f46f6c2e3cec Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Mon, 3 Apr 2023 22:25:31 -0600 Subject: [PATCH 17/21] feat: Changeset --- .changeset/fuzzy-insects-shake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fuzzy-insects-shake.md diff --git a/.changeset/fuzzy-insects-shake.md b/.changeset/fuzzy-insects-shake.md new file mode 100644 index 000000000000..282d7ba94443 --- /dev/null +++ b/.changeset/fuzzy-insects-shake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: route-level entry generators via `export const entries` From 57632cdeab3c1c3ac8ba31e3c956c41081ebb6ca Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Tue, 11 Apr 2023 10:11:32 -0600 Subject: [PATCH 18/21] Update packages/kit/src/utils/routing.js Co-authored-by: Rich Harris --- packages/kit/src/utils/routing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js index 8d7c766fc466..fff70ea7f9b5 100644 --- a/packages/kit/src/utils/routing.js +++ b/packages/kit/src/utils/routing.js @@ -99,7 +99,7 @@ export function parse_route_id(id) { const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/; /** - * Parses a route ID, then resolves it to a route by replacing parameters with actual values from `entry`. + * Parses a route ID, then resolves it to a path by replacing parameters with actual values from `entry`. * @param {string} id The route id * @param {Record} entry The entry meant to populate the route. For example, if the route is `/blog/[slug]`, the entry would be `{ slug: 'hello-world' }` * @example From 19ef1b8f946aaa5b4f355f6a8e46c1c7d8451773 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Apr 2023 13:53:41 -0400 Subject: [PATCH 19/21] rename route_from_entry -> resolve_entry --- packages/kit/src/core/postbuild/analyse.js | 4 ++-- packages/kit/src/utils/routing.js | 4 ++-- packages/kit/src/utils/routing.spec.js | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index f8cb111abe53..01ad617f5249 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -12,7 +12,7 @@ import { load_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; import { should_polyfill } from '../../utils/platform.js'; import { installPolyfills } from '../../exports/node/polyfills.js'; -import { route_from_entry } from '../../utils/routing.js'; +import { resolve_entry } from '../../utils/routing.js'; export default forked(import.meta.url, analyse); @@ -145,7 +145,7 @@ async function analyse({ manifest_path, env }) { }, prerender, entries: - entries && (await entries()).map((entry_object) => route_from_entry(route.id, entry_object)) + entries && (await entries()).map((entry_object) => resolve_entry(route.id, entry_object)) }); } diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js index fff70ea7f9b5..4318fe08dbc5 100644 --- a/packages/kit/src/utils/routing.js +++ b/packages/kit/src/utils/routing.js @@ -104,10 +104,10 @@ const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/; * @param {Record} entry The entry meant to populate the route. For example, if the route is `/blog/[slug]`, the entry would be `{ slug: 'hello-world' }` * @example * ```js - * route_from_entry(`/blog/[slug]/[...somethingElse]`, { slug: 'hello-world', somethingElse: 'something/else' }); // `/blog/hello-world/something/else` + * resolve_entry(`/blog/[slug]/[...somethingElse]`, { slug: 'hello-world', somethingElse: 'something/else' }); // `/blog/hello-world/something/else` * ``` */ -export function route_from_entry(id, entry) { +export function resolve_entry(id, entry) { const segments = get_route_segments(id); return ( '/' + diff --git a/packages/kit/src/utils/routing.spec.js b/packages/kit/src/utils/routing.spec.js index 98b9e3f7761d..d20ce871558b 100644 --- a/packages/kit/src/utils/routing.spec.js +++ b/packages/kit/src/utils/routing.spec.js @@ -1,6 +1,6 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import { exec, parse_route_id, route_from_entry } from './routing.js'; +import { exec, parse_route_id, resolve_entry } from './routing.js'; const tests = { '/': { @@ -242,26 +242,26 @@ const from_entry_tests = [ ]; for (const { route, entry, expected } of from_entry_tests) { - test(`route_from_entry generates correct path for ${route}`, () => { - const result = route_from_entry(route, entry); + test(`resolve_entry generates correct path for ${route}`, () => { + const result = resolve_entry(route, entry); assert.equal(result, expected); }); } -test('route_from_entry errors on missing entry for required param', () => { +test('resolve_entry errors on missing entry for required param', () => { assert.throws( - () => route_from_entry('/blog/[one]/[two]', { one: 'one' }), + () => resolve_entry('/blog/[one]/[two]', { one: 'one' }), "Missing param 'two' in route /blog/[one]/[two]" ); }); -test('route_from_entry errors on entry values starting or ending with slashes', () => { +test('resolve_entry errors on entry values starting or ending with slashes', () => { assert.throws( - () => route_from_entry('/blog/[one]/[two]', { one: 'one', two: '/two' }), + () => resolve_entry('/blog/[one]/[two]', { one: 'one', two: '/two' }), "Parameter 'two' in route /blog/[one]/[two] cannot start or end with a slash -- this would cause an invalid route like foo//bar" ); assert.throws( - () => route_from_entry('/blog/[one]/[two]', { one: 'one', two: 'two/' }), + () => resolve_entry('/blog/[one]/[two]', { one: 'one', two: 'two/' }), "Parameter 'two' in route /blog/[one]/[two] cannot start or end with a slash -- this would cause an invalid route like foo//bar" ); }); From 7ae49fcd58dfef2bd0c5a8e0c555b57ccf6905c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Apr 2023 13:54:13 -0400 Subject: [PATCH 20/21] fix lockfile --- pnpm-lock.yaml | 110 +++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 59 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8abf3ff896d9..9125ec541440 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: version: 1.1.0 '@typescript-eslint/eslint-plugin': specifier: ^5.53.0 - version: 5.53.0(@typescript-eslint/parser@5.58.0)(eslint@8.33.0)(typescript@4.9.4) + version: 5.53.0(@typescript-eslint/parser@5.59.0)(eslint@8.33.0)(typescript@4.9.4) eslint: specifier: ^8.33.0 version: 8.33.0 @@ -588,20 +588,25 @@ importers: version: 0.5.6 packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch: - specifiers: - '@sveltejs/adapter-auto': workspace:^ - '@sveltejs/kit': workspace:^ - svelte: ^3.56.0 - svelte-check: ^3.0.2 - typescript: ^4.9.4 - vite: ^4.2.0 devDependencies: - '@sveltejs/adapter-auto': link:../../../../../adapter-auto - '@sveltejs/kit': link:../../../.. - svelte: 3.56.0 - svelte-check: 3.0.2_svelte@3.56.0 - typescript: 4.9.4 - vite: 4.2.0 + '@sveltejs/adapter-auto': + specifier: workspace:^ + version: link:../../../../../adapter-auto + '@sveltejs/kit': + specifier: workspace:^ + version: link:../../../.. + svelte: + specifier: ^3.56.0 + version: 3.56.0 + svelte-check: + specifier: ^3.0.2 + version: 3.0.2(svelte@3.56.0) + typescript: + specifier: ^4.9.4 + version: 4.9.4 + vite: + specifier: ^4.2.0 + version: 4.2.0(@types/node@16.18.6) packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment: devDependencies: @@ -2005,7 +2010,7 @@ packages: '@types/node': 16.18.6 dev: true - /@typescript-eslint/eslint-plugin@5.53.0(@typescript-eslint/parser@5.58.0)(eslint@8.33.0)(typescript@4.9.4): + /@typescript-eslint/eslint-plugin@5.53.0(@typescript-eslint/parser@5.59.0)(eslint@8.33.0)(typescript@4.9.4): resolution: {integrity: sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2016,7 +2021,7 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.58.0(eslint@8.33.0)(typescript@4.9.4) + '@typescript-eslint/parser': 5.59.0(eslint@8.33.0)(typescript@4.9.4) '@typescript-eslint/scope-manager': 5.53.0 '@typescript-eslint/type-utils': 5.53.0(eslint@8.33.0)(typescript@4.9.4) '@typescript-eslint/utils': 5.53.0(eslint@8.33.0)(typescript@4.9.4) @@ -2033,8 +2038,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@5.58.0(eslint@8.33.0)(typescript@4.9.4): - resolution: {integrity: sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==} + /@typescript-eslint/parser@5.59.0(eslint@8.33.0)(typescript@4.9.4): + resolution: {integrity: sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2043,9 +2048,9 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 5.58.0 - '@typescript-eslint/types': 5.58.0 - '@typescript-eslint/typescript-estree': 5.58.0(typescript@4.9.4) + '@typescript-eslint/scope-manager': 5.59.0 + '@typescript-eslint/types': 5.59.0 + '@typescript-eslint/typescript-estree': 5.59.0(typescript@4.9.4) debug: 4.3.4 eslint: 8.33.0 typescript: 4.9.4 @@ -2061,12 +2066,12 @@ packages: '@typescript-eslint/visitor-keys': 5.53.0 dev: true - /@typescript-eslint/scope-manager@5.58.0: - resolution: {integrity: sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==} + /@typescript-eslint/scope-manager@5.59.0: + resolution: {integrity: sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.58.0 - '@typescript-eslint/visitor-keys': 5.58.0 + '@typescript-eslint/types': 5.59.0 + '@typescript-eslint/visitor-keys': 5.59.0 dev: true /@typescript-eslint/type-utils@5.53.0(eslint@8.33.0)(typescript@4.9.4): @@ -2094,8 +2099,8 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/types@5.58.0: - resolution: {integrity: sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==} + /@typescript-eslint/types@5.59.0: + resolution: {integrity: sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -2120,8 +2125,8 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree@5.58.0(typescript@4.9.4): - resolution: {integrity: sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==} + /@typescript-eslint/typescript-estree@5.59.0(typescript@4.9.4): + resolution: {integrity: sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -2129,12 +2134,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 5.58.0 - '@typescript-eslint/visitor-keys': 5.58.0 + '@typescript-eslint/types': 5.59.0 + '@typescript-eslint/visitor-keys': 5.59.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.4.0 + semver: 7.3.8 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: @@ -2169,12 +2174,12 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@typescript-eslint/visitor-keys@5.58.0: - resolution: {integrity: sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==} + /@typescript-eslint/visitor-keys@5.59.0: + resolution: {integrity: sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.58.0 - eslint-visitor-keys: 3.4.0 + '@typescript-eslint/types': 5.59.0 + eslint-visitor-keys: 3.3.0 dev: true /@typescript/twoslash@3.1.0: @@ -3073,11 +3078,6 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint-visitor-keys@3.4.0: - resolution: {integrity: sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /eslint@8.33.0: resolution: {integrity: sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3440,14 +3440,14 @@ packages: is-glob: 4.0.3 dev: true - /glob@10.0.0: - resolution: {integrity: sha512-zmp9ZDC6NpDNLujV2W2n+3lH+BafIVZ4/ct+Yj3BMZTH/+bgm/eVjHzeFLwxJrrIGgjjS2eiQLlpurHsNlEAtQ==} + /glob@10.1.0: + resolution: {integrity: sha512-daGobsYuT0G4hng24B5LbeLNvwKZYRhWyDl3RvqqAGZjJnCopWWK6PWnAGBY1M/vdA63QE+jddhZcYp+74Bq6Q==} engines: {node: '>=16 || 14 >=14.17'} dependencies: fs.realpath: 1.0.0 minimatch: 9.0.0 minipass: 5.0.0 - path-scurry: 1.6.4 + path-scurry: 1.7.0 dev: true /glob@7.1.6: @@ -4025,9 +4025,9 @@ packages: dependencies: yallist: 4.0.0 - /lru-cache@9.0.0: - resolution: {integrity: sha512-9AEKXzvOZc4BMacFnYiTOlDH/197LNnQIK9wZ6iMB5NXPzuv4bWR/Msv7iUMplkiMQ1qQL+KSv/JF1mZAB5Lrg==} - engines: {node: '>=16.14'} + /lru-cache@9.0.3: + resolution: {integrity: sha512-cyjNRew29d4kbgnz1sjDqxg7qg8NW4s+HQzCGjeon7DV5T2yDije16W9HaUFV1dhVEMh+SjrOcK0TomBmf3Egg==} + engines: {node: 14 || >=16.14} dev: true /lz-string@1.4.4: @@ -4495,11 +4495,11 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - /path-scurry@1.6.4: - resolution: {integrity: sha512-Qp/9IHkdNiXJ3/Kon++At2nVpnhRiPq/aSvQN+H3U1WZbvNRK0RIQK/o4HMqPoXjpuGJUEWpHSs6Mnjxqh3TQg==} + /path-scurry@1.7.0: + resolution: {integrity: sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==} engines: {node: '>=16 || 14 >=14.17'} dependencies: - lru-cache: 9.0.0 + lru-cache: 9.0.3 minipass: 5.0.0 dev: true @@ -4866,7 +4866,7 @@ packages: engines: {node: '>=14'} hasBin: true dependencies: - glob: 10.0.0 + glob: 10.1.0 dev: true /rollup-pluginutils@2.8.2: @@ -4952,14 +4952,6 @@ packages: dependencies: lru-cache: 6.0.0 - /semver@7.4.0: - resolution: {integrity: sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} From fcb442a9d7ce8c9e30d170062bfed351a521c399 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 3 May 2023 23:22:19 -0400 Subject: [PATCH 21/21] add docs --- .../docs/20-core-concepts/40-page-options.md | 35 +++++++++++++++++-- .../src/lib/docs/server/index.js | 3 +- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/documentation/docs/20-core-concepts/40-page-options.md b/documentation/docs/20-core-concepts/40-page-options.md index 91f72d662a70..5804fea5df44 100644 --- a/documentation/docs/20-core-concepts/40-page-options.md +++ b/documentation/docs/20-core-concepts/40-page-options.md @@ -33,7 +33,7 @@ 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 files for any prerenderable pages or `+server.js` routes 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](configuration#prerender), or by exporting an entry generator from your dynamic route. +The prerenderer will start at the root of your app and generate files for any prerenderable pages or `+server.js` routes 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 [`config.kit.prerender.entries`](configuration#prerender), or by exporting an [`entries`](#entries) function from your dynamic route. While prerendering, the value of `building` imported from [`$app/environment`](modules#$app-environment) will be `true`. @@ -84,9 +84,40 @@ If you encounter an error like 'The following routes were marked as prerenderabl Since these routes cannot be dynamically server-rendered, this will cause errors when people try to access the route in question. There are two ways to fix it: -* Ensure that SvelteKit can find the route by following links from [`config.kit.prerender.entries`](configuration#prerender). Add links to dynamic routes (i.e. pages with `[parameters]` ) to this option if they are not found through crawling the other entry points, else they are not prerendered because SvelteKit doesn't know what value the parameters should have. Pages not marked as prerenderable will be ignored and their links to other pages will not be crawled, even if some of them would be prerenderable. +* Ensure that SvelteKit can find the route by following links from [`config.kit.prerender.entries`](configuration#prerender) or the [`entries`](#entries) page option. Add links to dynamic routes (i.e. pages with `[parameters]` ) to this option if they are not found through crawling the other entry points, else they are not prerendered because SvelteKit doesn't know what value the parameters should have. Pages not marked as prerenderable will be ignored and their links to other pages will not be crawled, even if some of them would be prerenderable. * Change `export const prerender = true` to `export const prerender = 'auto'`. Routes with `'auto'` can be dynamically server rendered +## entries + +SvelteKit will discover pages to prerender automatically, by starting at _entry points_ and crawling them. By default, all your non-dynamic routes are considered entry points — for example, if you have these routes... + +```bash +/ # non-dynamic +/blog # non-dynamic +/blog/[slug] # dynamic, because of `[slug]` +``` + +...SvelteKit will prerender `/` and `/blog`, and in the process discover links like `` which give it new pages to prerender. + +Most of the time, that's enough. In some situations, links to pages like `/blog/hello-world` might not exist (or might not exist on prerendered pages), in which case we need to tell SvelteKit about their existence. + +This can be done with [`config.kit.prerender.entries`](configuration#prerender), or by exporting an `entries` function from a `+page.js` or `+page.server.js` belonging to a dynamic route: + +```js +/// file: src/routes/blog/[slug]/+page.server.js +/** @type {import('./$types').EntryGenerator} */ +export function entries() { + return [ + { slug: 'hello-world' }, + { slug: 'another-blog-post' } + ]; +} + +export const prerender = true; +``` + +`entries` can be an `async` function, allowing you to (for example) retrieve a list of posts from a CMS or database, in the example above. + ## ssr Normally, SvelteKit renders your page on the server first and sends that HTML to the client where it's [hydrated](glossary#hydration). If you set `ssr` to `false`, it renders an empty 'shell' page instead. This is useful if your page is unable to be rendered on the server (because you use browser-only globals like `document` for example), but in most situations it's not recommended ([see appendix](glossary#ssr)). diff --git a/sites/kit.svelte.dev/src/lib/docs/server/index.js b/sites/kit.svelte.dev/src/lib/docs/server/index.js index a3f69404a173..26d49e6671bc 100644 --- a/sites/kit.svelte.dev/src/lib/docs/server/index.js +++ b/sites/kit.svelte.dev/src/lib/docs/server/index.js @@ -179,7 +179,8 @@ export async function read_file(file) { `export type LayoutServerLoad = Kit.ServerLoad<{${params}}>;`, `export type RequestHandler = Kit.RequestHandler<{${params}}>;`, `export type Action = Kit.Action<{${params}}>;`, - `export type Actions = Kit.Actions<{${params}}>;` + `export type Actions = Kit.Actions<{${params}}>;`, + `export type EntryGenerator = () => Promise> | Array<{${params}}>;` ); }