@sveltejs/kit 1.15.11 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "1.15.11",
3
+ "version": "1.16.0",
4
4
  "description": "The fastest way to build Svelte apps",
5
5
  "repository": {
6
6
  "type": "git",
@@ -229,6 +229,20 @@ const options = object(
229
229
  }
230
230
  ),
231
231
 
232
+ handleEntryGeneratorMismatch: validate(
233
+ (/** @type {any} */ { message }) => {
234
+ throw new Error(
235
+ message +
236
+ `\nTo suppress or handle this error, implement \`handleEntryGeneratorMismatch\` in https://kit.svelte.dev/docs/configuration#prerender`
237
+ );
238
+ },
239
+ (input, keypath) => {
240
+ if (typeof input === 'function') return input;
241
+ if (['fail', 'warn', 'ignore'].includes(input)) return input;
242
+ throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`);
243
+ }
244
+ ),
245
+
232
246
  origin: validate('http://sveltekit-prerender', (input, keypath) => {
233
247
  assert_string(input, keypath);
234
248
 
@@ -2,7 +2,9 @@ import { join } from 'node:path';
2
2
  import { pathToFileURL } from 'node:url';
3
3
  import { get_option } from '../../utils/options.js';
4
4
  import {
5
- validate_common_exports,
5
+ validate_layout_exports,
6
+ validate_layout_server_exports,
7
+ validate_page_exports,
6
8
  validate_page_server_exports,
7
9
  validate_server_exports
8
10
  } from '../../utils/exports.js';
@@ -10,6 +12,7 @@ import { load_config } from '../config/index.js';
10
12
  import { forked } from '../../utils/fork.js';
11
13
  import { should_polyfill } from '../../utils/platform.js';
12
14
  import { installPolyfills } from '../../exports/node/polyfills.js';
15
+ import { resolve_entry } from '../../utils/routing.js';
13
16
 
14
17
  export default forked(import.meta.url, analyse);
15
18
 
@@ -72,6 +75,8 @@ async function analyse({ manifest_path, env }) {
72
75
  let prerender = undefined;
73
76
  /** @type {any} */
74
77
  let config = undefined;
78
+ /** @type {import('types').PrerenderEntryGenerator | undefined} */
79
+ let entries = undefined;
75
80
 
76
81
  if (route.endpoint) {
77
82
  const mod = await route.endpoint();
@@ -95,6 +100,7 @@ async function analyse({ manifest_path, env }) {
95
100
  if (mod.OPTIONS) api_methods.push('OPTIONS');
96
101
 
97
102
  config = mod.config;
103
+ entries = mod.entries;
98
104
  }
99
105
 
100
106
  if (route.page) {
@@ -109,8 +115,8 @@ async function analyse({ manifest_path, env }) {
109
115
 
110
116
  for (const layout of layouts) {
111
117
  if (layout) {
112
- validate_common_exports(layout.server, layout.server_id);
113
- validate_common_exports(layout.universal, layout.universal_id);
118
+ validate_layout_server_exports(layout.server, layout.server_id);
119
+ validate_layout_exports(layout.universal, layout.universal_id);
114
120
  }
115
121
  }
116
122
 
@@ -119,12 +125,13 @@ async function analyse({ manifest_path, env }) {
119
125
  if (page.server?.actions) page_methods.push('POST');
120
126
 
121
127
  validate_page_server_exports(page.server, page.server_id);
122
- validate_common_exports(page.universal, page.universal_id);
128
+ validate_page_exports(page.universal, page.universal_id);
123
129
  }
124
130
 
125
131
  prerender = get_option(nodes, 'prerender') ?? false;
126
132
 
127
133
  config = get_config(nodes);
134
+ entries ??= get_option(nodes, 'entries');
128
135
  }
129
136
 
130
137
  metadata.routes.set(route.id, {
@@ -136,7 +143,9 @@ async function analyse({ manifest_path, env }) {
136
143
  api: {
137
144
  methods: api_methods
138
145
  },
139
- prerender
146
+ prerender,
147
+ entries:
148
+ entries && (await entries()).map((entry_object) => resolve_entry(route.id, entry_object))
140
149
  });
141
150
  }
142
151
 
@@ -127,6 +127,14 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
127
127
  }
128
128
  );
129
129
 
130
+ const handle_entry_generator_mismatch = normalise_error_handler(
131
+ log,
132
+ config.prerender.handleEntryGeneratorMismatch,
133
+ ({ generatedFromId, entry, matchedId }) => {
134
+ 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.`;
135
+ }
136
+ );
137
+
130
138
  const q = queue(config.prerender.concurrency);
131
139
 
132
140
  /**
@@ -164,23 +172,25 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
164
172
  * @param {string | null} referrer
165
173
  * @param {string} decoded
166
174
  * @param {string} [encoded]
175
+ * @param {string} [generated_from_id]
167
176
  */
168
- function enqueue(referrer, decoded, encoded) {
177
+ function enqueue(referrer, decoded, encoded, generated_from_id) {
169
178
  if (seen.has(decoded)) return;
170
179
  seen.add(decoded);
171
180
 
172
181
  const file = decoded.slice(config.paths.base.length + 1);
173
182
  if (files.has(file)) return;
174
183
 
175
- return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer));
184
+ return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id));
176
185
  }
177
186
 
178
187
  /**
179
188
  * @param {string} decoded
180
189
  * @param {string} encoded
181
190
  * @param {string?} referrer
191
+ * @param {string} [generated_from_id]
182
192
  */
183
- async function visit(decoded, encoded, referrer) {
193
+ async function visit(decoded, encoded, referrer, generated_from_id) {
184
194
  if (!decoded.startsWith(config.paths.base)) {
185
195
  handle_http_error({ status: 404, path: decoded, referrer, referenceType: 'linked' });
186
196
  return;
@@ -206,6 +216,20 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
206
216
  }
207
217
  });
208
218
 
219
+ const encoded_id = response.headers.get('x-sveltekit-routeid');
220
+ const decoded_id = encoded_id && decode_uri(encoded_id);
221
+ if (
222
+ decoded_id !== null &&
223
+ generated_from_id !== undefined &&
224
+ decoded_id !== generated_from_id
225
+ ) {
226
+ handle_entry_generator_mismatch({
227
+ generatedFromId: generated_from_id,
228
+ entry: decoded,
229
+ matchedId: decoded_id
230
+ });
231
+ }
232
+
209
233
  const body = Buffer.from(await response.arrayBuffer());
210
234
 
211
235
  save('pages', response, body, decoded, encoded, referrer, 'linked');
@@ -378,9 +402,18 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
378
402
  saved.set(file, dest);
379
403
  }
380
404
 
405
+ /** @type {Array<{ id: string, entries: Array<string>}>} */
406
+ const route_level_entries = [];
407
+ for (const [id, { entries }] of metadata.routes.entries()) {
408
+ if (entries) {
409
+ route_level_entries.push({ id, entries });
410
+ }
411
+ }
412
+
381
413
  if (
382
414
  config.prerender.entries.length > 1 ||
383
415
  config.prerender.entries[0] !== '*' ||
416
+ route_level_entries.length > 0 ||
384
417
  prerender_map.size > 0
385
418
  ) {
386
419
  // Only log if we're actually going to do something to not confuse users
@@ -401,6 +434,12 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
401
434
  }
402
435
  }
403
436
 
437
+ for (const { id, entries } of route_level_entries) {
438
+ for (const entry of entries) {
439
+ enqueue(null, config.paths.base + entry, undefined, id);
440
+ }
441
+ }
442
+
404
443
  await q.done();
405
444
 
406
445
  // handle invalid fragment links
@@ -194,6 +194,12 @@ function update_types(config, routes, route, to_delete = new Set()) {
194
194
  .join('; ')} }`
195
195
  );
196
196
 
197
+ if (route.params.length > 0) {
198
+ exports.push(
199
+ `export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;`
200
+ );
201
+ }
202
+
197
203
  declarations.push(`type RouteId = '${route.id}';`);
198
204
 
199
205
  // 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
@@ -31,7 +31,7 @@ import { stores } from './singletons.js';
31
31
  import { unwrap_promises } from '../../utils/promises.js';
32
32
  import * as devalue from 'devalue';
33
33
  import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './constants.js';
34
- import { validate_common_exports } from '../../utils/exports.js';
34
+ import { validate_page_exports } from '../../utils/exports.js';
35
35
  import { compact } from '../../utils/array.js';
36
36
  import { INVALIDATED_PARAM, validate_depends } from '../shared.js';
37
37
 
@@ -428,7 +428,7 @@ export function create_client(app, target) {
428
428
  const node = await loader();
429
429
 
430
430
  if (DEV) {
431
- validate_common_exports(node.universal);
431
+ validate_page_exports(node.universal);
432
432
  }
433
433
 
434
434
  if (node.universal?.load) {
@@ -20,7 +20,9 @@ import { add_cookies_to_headers, get_cookies } from './cookie.js';
20
20
  import { create_fetch } from './fetch.js';
21
21
  import { Redirect } from '../control.js';
22
22
  import {
23
- validate_common_exports,
23
+ validate_layout_exports,
24
+ validate_layout_server_exports,
25
+ validate_page_exports,
24
26
  validate_page_server_exports,
25
27
  validate_server_exports
26
28
  } from '../../utils/exports.js';
@@ -197,8 +199,11 @@ export async function respond(request, options, manifest, state) {
197
199
 
198
200
  for (const layout of layouts) {
199
201
  if (layout) {
200
- validate_common_exports(layout.server, /** @type {string} */ (layout.server_id));
201
- validate_common_exports(
202
+ validate_layout_server_exports(
203
+ layout.server,
204
+ /** @type {string} */ (layout.server_id)
205
+ );
206
+ validate_layout_exports(
202
207
  layout.universal,
203
208
  /** @type {string} */ (layout.universal_id)
204
209
  );
@@ -207,7 +212,7 @@ export async function respond(request, options, manifest, state) {
207
212
 
208
213
  if (page) {
209
214
  validate_page_server_exports(page.server, /** @type {string} */ (page.server_id));
210
- validate_common_exports(page.universal, /** @type {string} */ (page.universal_id));
215
+ validate_page_exports(page.universal, /** @type {string} */ (page.universal_id));
211
216
  }
212
217
  }
213
218
 
@@ -1,9 +1,7 @@
1
1
  /**
2
- * @param {string[]} expected
2
+ * @param {Set<string>} expected
3
3
  */
4
4
  function validator(expected) {
5
- const set = new Set(expected);
6
-
7
5
  /**
8
6
  * @param {any} module
9
7
  * @param {string} [file]
@@ -12,11 +10,13 @@ function validator(expected) {
12
10
  if (!module) return;
13
11
 
14
12
  for (const key in module) {
15
- if (key[0] === '_' || set.has(key)) continue; // key is valid in this module
13
+ if (key[0] === '_' || expected.has(key)) continue; // key is valid in this module
14
+
15
+ const values = [...expected.values()];
16
16
 
17
17
  const hint =
18
18
  hint_for_supported_files(key, file?.slice(file.lastIndexOf('.'))) ??
19
- `valid exports are ${expected.join(', ')}, or anything with a '_' prefix`;
19
+ `valid exports are ${values.join(', ')}, or anything with a '_' prefix`;
20
20
 
21
21
  throw new Error(`Invalid export '${key}'${file ? ` in ${file}` : ''} (${hint})`);
22
22
  }
@@ -33,34 +33,45 @@ function validator(expected) {
33
33
  function hint_for_supported_files(key, ext = '.js') {
34
34
  let supported_files = [];
35
35
 
36
- if (valid_common_exports.includes(key)) {
36
+ if (valid_layout_exports.has(key)) {
37
+ supported_files.push(`+layout${ext}`);
38
+ }
39
+
40
+ if (valid_page_exports.has(key)) {
37
41
  supported_files.push(`+page${ext}`);
38
42
  }
39
43
 
40
- if (valid_page_server_exports.includes(key)) {
44
+ if (valid_layout_server_exports.has(key)) {
45
+ supported_files.push(`+layout.server${ext}`);
46
+ }
47
+
48
+ if (valid_page_server_exports.has(key)) {
41
49
  supported_files.push(`+page.server${ext}`);
42
50
  }
43
51
 
44
- if (valid_server_exports.includes(key)) {
52
+ if (valid_server_exports.has(key)) {
45
53
  supported_files.push(`+server${ext}`);
46
54
  }
47
55
 
48
56
  if (supported_files.length > 0) {
49
- return `'${key}' is a valid export in ${supported_files.join(` or `)}`;
57
+ return `'${key}' is a valid export in ${supported_files.slice(0, -1).join(`, `)}${
58
+ supported_files.length > 1 ? ' or ' : ''
59
+ }${supported_files.at(-1)}`;
50
60
  }
51
61
  }
52
62
 
53
- const valid_common_exports = ['load', 'prerender', 'csr', 'ssr', 'trailingSlash', 'config'];
54
- const valid_page_server_exports = [
63
+ const valid_layout_exports = new Set([
55
64
  'load',
56
65
  'prerender',
57
66
  'csr',
58
67
  'ssr',
59
- 'actions',
60
68
  'trailingSlash',
61
69
  'config'
62
- ];
63
- const valid_server_exports = [
70
+ ]);
71
+ const valid_page_exports = new Set([...valid_layout_exports, 'entries']);
72
+ const valid_layout_server_exports = new Set([...valid_layout_exports, 'actions']);
73
+ const valid_page_server_exports = new Set([...valid_layout_server_exports, 'entries']);
74
+ const valid_server_exports = new Set([
64
75
  'GET',
65
76
  'POST',
66
77
  'PATCH',
@@ -69,9 +80,12 @@ const valid_server_exports = [
69
80
  'OPTIONS',
70
81
  'prerender',
71
82
  'trailingSlash',
72
- 'config'
73
- ];
83
+ 'config',
84
+ 'entries'
85
+ ]);
74
86
 
75
- export const validate_common_exports = validator(valid_common_exports);
87
+ export const validate_layout_exports = validator(valid_layout_exports);
88
+ export const validate_page_exports = validator(valid_page_exports);
89
+ export const validate_layout_server_exports = validator(valid_layout_server_exports);
76
90
  export const validate_page_server_exports = validator(valid_page_server_exports);
77
91
  export const validate_server_exports = validator(valid_server_exports);
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash'} Option
3
- * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : boolean} Value
2
+ * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash' | 'entries'} Option
3
+ * @template {(import('types').SSRNode['universal'] | import('types').SSRNode['server'])[Option]} Value
4
4
  *
5
5
  * @param {Array<import('types').SSRNode | undefined>} nodes
6
6
  * @param {Option} option
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export function get_option(nodes, option) {
11
11
  return nodes.reduce((value, node) => {
12
- return /** @type {any} TypeScript's too dumb to understand this */ (
12
+ return /** @type {Value} TypeScript's too dumb to understand this */ (
13
13
  node?.universal?.[option] ?? node?.server?.[option] ?? value
14
14
  );
15
15
  }, /** @type {Value | undefined} */ (undefined));
@@ -96,6 +96,61 @@ export function parse_route_id(id) {
96
96
  return { pattern, params };
97
97
  }
98
98
 
99
+ const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/;
100
+
101
+ /**
102
+ * Parses a route ID, then resolves it to a path by replacing parameters with actual values from `entry`.
103
+ * @param {string} id The route id
104
+ * @param {Record<string, string | undefined>} entry The entry meant to populate the route. For example, if the route is `/blog/[slug]`, the entry would be `{ slug: 'hello-world' }`
105
+ * @example
106
+ * ```js
107
+ * resolve_entry(`/blog/[slug]/[...somethingElse]`, { slug: 'hello-world', somethingElse: 'something/else' }); // `/blog/hello-world/something/else`
108
+ * ```
109
+ */
110
+ export function resolve_entry(id, entry) {
111
+ const segments = get_route_segments(id);
112
+ return (
113
+ '/' +
114
+ segments
115
+ .map((segment) => {
116
+ const match = basic_param_pattern.exec(segment);
117
+
118
+ // static content -- i.e. not a param
119
+ if (!match) return segment;
120
+
121
+ const optional = !!match[1];
122
+ const name = match[2];
123
+ const param_value = entry[name];
124
+
125
+ // This is nested so TS correctly narrows the type
126
+ if (!param_value) {
127
+ if (optional) return '';
128
+ throw new Error(`Missing parameter '${name}' in route ${id}`);
129
+ }
130
+
131
+ if (param_value.startsWith('/') || param_value.endsWith('/'))
132
+ throw new Error(
133
+ `Parameter '${name}' in route ${id} cannot start or end with a slash -- this would cause an invalid route like foo//bar`
134
+ );
135
+
136
+ return param_value;
137
+ })
138
+ .filter(Boolean)
139
+ .join('/')
140
+ );
141
+ }
142
+
143
+ const optional_param_regex = /\/\[\[\w+?(?:=\w+)?\]\]/;
144
+
145
+ /**
146
+ * Removes optional params from a route ID.
147
+ * @param {string} id
148
+ * @returns The route id with optional params removed
149
+ */
150
+ export function remove_optional_params(id) {
151
+ return id.replace(optional_param_regex, '');
152
+ }
153
+
99
154
  /**
100
155
  * Returns `false` for `(group)` segments
101
156
  * @param {string} segment
package/types/index.d.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  Logger,
12
12
  MaybePromise,
13
13
  Prerendered,
14
+ PrerenderEntryGeneratorMismatchHandlerValue,
14
15
  PrerenderHttpErrorHandlerValue,
15
16
  PrerenderMissingIdHandlerValue,
16
17
  PrerenderOption,
@@ -515,7 +516,7 @@ export interface KitConfig {
515
516
  */
516
517
  handleHttpError?: PrerenderHttpErrorHandlerValue;
517
518
  /**
518
- * How to respond to hash links from one prerendered page to another that don't correspond to an `id` on the destination page
519
+ * How to respond when hash links from one prerendered page to another don't correspond to an `id` on the destination page.
519
520
  *
520
521
  * - `'fail'` — fail the build
521
522
  * - `'ignore'` - silently ignore the failure and continue
@@ -525,6 +526,17 @@ export interface KitConfig {
525
526
  * @default "fail"
526
527
  */
527
528
  handleMissingId?: PrerenderMissingIdHandlerValue;
529
+ /**
530
+ * How to respond when an entry generated by the `entries` export doesn't match the route it was generated from.
531
+ *
532
+ * - `'fail'` — fail the build
533
+ * - `'ignore'` - silently ignore the failure and continue
534
+ * - `'warn'` — continue, but print a warning
535
+ * - `(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
536
+ *
537
+ * @default "fail"
538
+ */
539
+ handleEntryGeneratorMismatch?: PrerenderEntryGeneratorMismatchHandlerValue;
528
540
  /**
529
541
  * The value of `url.origin` during prerendering; useful if it is included in rendered content.
530
542
  * @default "http://sveltekit-prerender"
@@ -266,6 +266,7 @@ export interface ServerMetadataRoute {
266
266
  };
267
267
  methods: HttpMethod[];
268
268
  prerender: PrerenderOption | undefined;
269
+ entries: Array<string> | undefined;
269
270
  }
270
271
 
271
272
  export interface ServerMetadata {
@@ -308,6 +309,7 @@ export interface SSRNode {
308
309
  csr?: boolean;
309
310
  trailingSlash?: TrailingSlash;
310
311
  config?: any;
312
+ entries?: PrerenderEntryGenerator;
311
313
  };
312
314
 
313
315
  server: {
@@ -318,6 +320,7 @@ export interface SSRNode {
318
320
  trailingSlash?: TrailingSlash;
319
321
  actions?: Actions;
320
322
  config?: any;
323
+ entries?: PrerenderEntryGenerator;
321
324
  };
322
325
 
323
326
  universal_id: string;
@@ -355,10 +358,13 @@ export interface PageNodeIndexes {
355
358
  leaf: number;
356
359
  }
357
360
 
361
+ export type PrerenderEntryGenerator = () => MaybePromise<Array<Record<string, string>>>;
362
+
358
363
  export type SSREndpoint = Partial<Record<HttpMethod, RequestHandler>> & {
359
364
  prerender?: PrerenderOption;
360
365
  trailingSlash?: TrailingSlash;
361
366
  config?: any;
367
+ entries?: PrerenderEntryGenerator;
362
368
  };
363
369
 
364
370
  export interface SSRRoute {
@@ -205,8 +205,17 @@ export interface PrerenderMissingIdHandler {
205
205
  (details: { path: string; id: string; referrers: string[]; message: string }): void;
206
206
  }
207
207
 
208
+ export interface PrerenderEntryGeneratorMismatchHandler {
209
+ (details: { generatedFromId: string; entry: string; matchedId: string; message: string }): void;
210
+ }
211
+
208
212
  export type PrerenderHttpErrorHandlerValue = 'fail' | 'warn' | 'ignore' | PrerenderHttpErrorHandler;
209
213
  export type PrerenderMissingIdHandlerValue = 'fail' | 'warn' | 'ignore' | PrerenderMissingIdHandler;
214
+ export type PrerenderEntryGeneratorMismatchHandlerValue =
215
+ | 'fail'
216
+ | 'warn'
217
+ | 'ignore'
218
+ | PrerenderEntryGeneratorMismatchHandler;
210
219
 
211
220
  export type PrerenderOption = boolean | 'auto';
212
221