@sveltejs/kit 1.15.10 → 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.10",
3
+ "version": "1.16.0",
4
4
  "description": "The fastest way to build Svelte apps",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,7 +36,7 @@
36
36
  "marked": "^4.2.3",
37
37
  "rollup": "^3.7.0",
38
38
  "svelte": "^3.56.0",
39
- "svelte-preprocess": "^5.0.0",
39
+ "svelte-preprocess": "^5.0.3",
40
40
  "typescript": "^4.9.4",
41
41
  "uvu": "^0.5.6",
42
42
  "vite": "^4.3.0"
@@ -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
 
@@ -43,17 +43,6 @@ export function generate_manifest({ build_data, relative_path, routes }) {
43
43
  })
44
44
  );
45
45
 
46
- /** @typedef {{ index: number, path: string }} LookupEntry */
47
- /** @type {Map<import('types').PageNode, LookupEntry>} */
48
- const bundled_nodes = new Map();
49
-
50
- build_data.manifest_data.nodes.forEach((node, i) => {
51
- bundled_nodes.set(node, {
52
- path: join_relative(relative_path, `/nodes/${i}.js`),
53
- index: i
54
- });
55
- });
56
-
57
46
  /** @type {(path: string) => string} */
58
47
  const loader = (path) => `() => import('${path}')`;
59
48
 
@@ -68,14 +57,10 @@ export function generate_manifest({ build_data, relative_path, routes }) {
68
57
  function get_nodes(indexes) {
69
58
  let string = indexes.map((n) => reindexed.get(n) ?? '').join(',');
70
59
 
71
- if (indexes.at(-1) === undefined) {
72
- // since JavaScript ignores trailing commas, we need to insert a dummy
73
- // comma so that the array has the correct length if the last item
74
- // is undefined
75
- string += ',';
76
- }
77
-
78
- return `[${string}]`;
60
+ // since JavaScript ignores trailing commas, we need to insert a dummy
61
+ // comma so that the array has the correct length if the last item
62
+ // is undefined
63
+ return `[${string},]`;
79
64
  }
80
65
 
81
66
  // prettier-ignore
@@ -94,12 +79,12 @@ export function generate_manifest({ build_data, relative_path, routes }) {
94
79
  ],
95
80
  routes: [
96
81
  ${routes.map(route => {
82
+ if (!route.page && !route.endpoint) return;
83
+
97
84
  route.params.forEach(param => {
98
85
  if (param.matcher) matchers.add(param.matcher);
99
86
  });
100
87
 
101
- if (!route.page && !route.endpoint) return;
102
-
103
88
  return dedent`
104
89
  {
105
90
  id: ${s(route.id)},
@@ -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
@@ -21,7 +21,7 @@ export function sequence(...handlers) {
21
21
  return handle({
22
22
  event,
23
23
  resolve: (event, options) => {
24
- /** @param {{ html: string, done: boolean }} opts */
24
+ /** @type {import('types').ResolveOptions['transformPageChunk']} */
25
25
  const transformPageChunk = async ({ html, done }) => {
26
26
  if (options?.transformPageChunk) {
27
27
  html = (await options.transformPageChunk({ html, done })) ?? '';
@@ -34,9 +34,21 @@ export function sequence(...handlers) {
34
34
  return html;
35
35
  };
36
36
 
37
+ /** @type {import('types').ResolveOptions['filterSerializedResponseHeaders']} */
38
+ const filterSerializedResponseHeaders =
39
+ parent_options?.filterSerializedResponseHeaders ??
40
+ options?.filterSerializedResponseHeaders;
41
+
42
+ /** @type {import('types').ResolveOptions['preload']} */
43
+ const preload = parent_options?.preload ?? options?.preload;
44
+
37
45
  return i < length - 1
38
- ? apply_handle(i + 1, event, { transformPageChunk })
39
- : resolve(event, { transformPageChunk });
46
+ ? apply_handle(i + 1, event, {
47
+ transformPageChunk,
48
+ filterSerializedResponseHeaders,
49
+ preload
50
+ })
51
+ : resolve(event, { transformPageChunk, filterSerializedResponseHeaders, preload });
40
52
  }
41
53
  });
42
54
  }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import { mkdirp } from '../../../utils/filesystem.js';
3
3
  import { find_deps, resolve_symlinks } from './utils.js';
4
4
  import { s } from '../../../utils/misc.js';
5
+ import { normalizePath } from 'vite';
5
6
 
6
7
  /**
7
8
  * @param {string} out
@@ -48,12 +49,6 @@ export function build_server_nodes(out, kit, manifest_data, server_manifest, cli
48
49
  const fonts = [];
49
50
 
50
51
  if (node.component && client_manifest) {
51
- const entry = find_deps(client_manifest, node.component, true);
52
-
53
- imported.push(...entry.imports);
54
- stylesheets.push(...entry.stylesheets);
55
- fonts.push(...entry.fonts);
56
-
57
52
  exports.push(
58
53
  `export const component = async () => (await import('../${
59
54
  resolve_symlinks(server_manifest, node.component).chunk.file
@@ -62,14 +57,6 @@ export function build_server_nodes(out, kit, manifest_data, server_manifest, cli
62
57
  }
63
58
 
64
59
  if (node.universal) {
65
- if (client_manifest) {
66
- const entry = find_deps(client_manifest, node.universal, true);
67
-
68
- imported.push(...entry.imports);
69
- stylesheets.push(...entry.stylesheets);
70
- fonts.push(...entry.fonts);
71
- }
72
-
73
60
  imports.push(`import * as universal from '../${server_manifest[node.universal].file}';`);
74
61
  exports.push(`export { universal };`);
75
62
  exports.push(`export const universal_id = ${s(node.universal)};`);
@@ -81,6 +68,18 @@ export function build_server_nodes(out, kit, manifest_data, server_manifest, cli
81
68
  exports.push(`export const server_id = ${s(node.server)};`);
82
69
  }
83
70
 
71
+ if (client_manifest && (node.universal || node.component)) {
72
+ const entry = find_deps(
73
+ client_manifest,
74
+ `${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`,
75
+ true
76
+ );
77
+
78
+ imported.push(...entry.imports);
79
+ stylesheets.push(...entry.stylesheets);
80
+ fonts.push(...entry.fonts);
81
+ }
82
+
84
83
  exports.push(
85
84
  `export const imports = ${s(imported)};`,
86
85
  `export const stylesheets = ${s(stylesheets)};`,
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { normalizePath } from 'vite';
3
4
 
4
5
  /**
5
6
  * Adds transitive JS and CSS dependencies to the js and css inputs.
@@ -72,7 +73,7 @@ export function find_deps(manifest, entry, add_dynamic_css) {
72
73
  */
73
74
  export function resolve_symlinks(manifest, file) {
74
75
  while (!manifest[file]) {
75
- const next = path.relative('.', fs.realpathSync(file));
76
+ const next = normalizePath(path.relative('.', fs.realpathSync(file)));
76
77
  if (next === file) throw new Error(`Could not find file "${file}" in Vite manifest`);
77
78
  file = next;
78
79
  }
@@ -116,7 +116,7 @@ export async function dev(vite, vite_config, svelte_config) {
116
116
  _: {
117
117
  client: {
118
118
  start: `${runtime_base}/client/start.js`,
119
- app: `${svelte_config.kit.outDir}/generated/client/app.js`,
119
+ app: `${to_fs(svelte_config.kit.outDir)}/generated/client/app.js`,
120
120
  imports: [],
121
121
  stylesheets: [],
122
122
  fonts: []
@@ -525,27 +525,6 @@ function kit({ svelte_config }) {
525
525
  } else {
526
526
  input['entry/start'] = `${runtime_directory}/client/start.js`;
527
527
  input['entry/app'] = `${kit.outDir}/generated/client-optimized/app.js`;
528
-
529
- /**
530
- * @param {string | undefined} file
531
- */
532
- function add_input(file) {
533
- if (!file) return;
534
-
535
- const resolved = path.resolve(file);
536
- const relative = decodeURIComponent(path.relative(kit.files.routes, resolved));
537
-
538
- const name = relative.startsWith('..')
539
- ? path.basename(file).replace(/^\+/, '')
540
- : relative.replace(/(\\|\/)\+/g, '-').replace(/[\\/]/g, '-');
541
-
542
- input[`entry/${name}`] = resolved;
543
- }
544
-
545
- for (const node of manifest_data.nodes) {
546
- add_input(node.component);
547
- add_input(node.universal);
548
- }
549
528
  }
550
529
 
551
530
  // see the kit.output.preloadStrategy option for details on why we have multiple options here
@@ -1,19 +1,9 @@
1
1
  import * as devalue from 'devalue';
2
- import { client } from '../client/singletons.js';
2
+ import { DEV } from 'esm-env';
3
+ import { client_method } from '../client/singletons.js';
3
4
  import { invalidateAll } from './navigation.js';
4
- import { BROWSER, DEV } from 'esm-env';
5
-
6
- /**
7
- * @param {string} name
8
- */
9
- function guard(name) {
10
- return () => {
11
- throw new Error(`Cannot call ${name}(...) on the server`);
12
- };
13
- }
14
5
 
15
- /** @type {import('$app/forms').applyAction} */
16
- export const applyAction = BROWSER ? client.apply_action : guard('applyAction');
6
+ export const applyAction = client_method('apply_action');
17
7
 
18
8
  /** @type {import('$app/forms').deserialize} */
19
9
  export function deserialize(result) {
@@ -1,22 +1,17 @@
1
- import { BROWSER } from 'esm-env';
2
- import { client } from '../client/singletons.js';
3
-
4
- /**
5
- * @param {string} name
6
- */
7
- function guard(name) {
8
- return () => {
9
- throw new Error(`Cannot call ${name}(...) on the server`);
10
- };
11
- }
12
-
13
- export const disableScrollHandling = BROWSER
14
- ? client.disable_scroll_handling
15
- : guard('disableScrollHandling');
16
- export const goto = BROWSER ? client.goto : guard('goto');
17
- export const invalidate = BROWSER ? client.invalidate : guard('invalidate');
18
- export const invalidateAll = BROWSER ? client.invalidateAll : guard('invalidateAll');
19
- export const preloadData = BROWSER ? client.preload_data : guard('preloadData');
20
- export const preloadCode = BROWSER ? client.preload_code : guard('preloadCode');
21
- export const beforeNavigate = BROWSER ? client.before_navigate : () => {};
22
- export const afterNavigate = BROWSER ? client.after_navigate : () => {};
1
+ import { client_method } from '../client/singletons.js';
2
+
3
+ export const disableScrollHandling = /* @__PURE__ */ client_method('disable_scroll_handling');
4
+
5
+ export const goto = /* @__PURE__ */ client_method('goto');
6
+
7
+ export const invalidate = /* @__PURE__ */ client_method('invalidate');
8
+
9
+ export const invalidateAll = /* @__PURE__ */ client_method('invalidate_all');
10
+
11
+ export const preloadData = /* @__PURE__ */ client_method('preload_data');
12
+
13
+ export const preloadCode = /* @__PURE__ */ client_method('preload_code');
14
+
15
+ export const beforeNavigate = /* @__PURE__ */ client_method('before_navigate');
16
+
17
+ export const afterNavigate = /* @__PURE__ */ client_method('after_navigate');
@@ -31,9 +31,9 @@ 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
- import { validate_depends } from '../shared.js';
36
+ import { INVALIDATED_PARAM, validate_depends } from '../shared.js';
37
37
 
38
38
  let errored = false;
39
39
 
@@ -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) {
@@ -1354,7 +1354,7 @@ export function create_client(app, target) {
1354
1354
  return invalidate();
1355
1355
  },
1356
1356
 
1357
- invalidateAll: () => {
1357
+ invalidate_all: () => {
1358
1358
  force_invalidation = true;
1359
1359
  return invalidate();
1360
1360
  },
@@ -1775,13 +1775,10 @@ export function create_client(app, target) {
1775
1775
  async function load_data(url, invalid) {
1776
1776
  const data_url = new URL(url);
1777
1777
  data_url.pathname = add_data_suffix(url.pathname);
1778
- if (DEV && url.searchParams.has('x-sveltekit-invalidated')) {
1779
- throw new Error('Cannot used reserved query parameter "x-sveltekit-invalidated"');
1778
+ if (DEV && url.searchParams.has(INVALIDATED_PARAM)) {
1779
+ throw new Error(`Cannot used reserved query parameter "${INVALIDATED_PARAM}"`);
1780
1780
  }
1781
- data_url.searchParams.append(
1782
- 'x-sveltekit-invalidated',
1783
- invalid.map((x) => (x ? '1' : '')).join('_')
1784
- );
1781
+ data_url.searchParams.append(INVALIDATED_PARAM, invalid.map((i) => (i ? '1' : '0')).join(''));
1785
1782
 
1786
1783
  const res = await native_fetch(data_url.href);
1787
1784
 
@@ -1,5 +1,6 @@
1
1
  import { writable } from 'svelte/store';
2
2
  import { create_updated_store, notifiable_store } from './utils.js';
3
+ import { BROWSER } from 'esm-env';
3
4
 
4
5
  /** @type {import('./types').Client} */
5
6
  export let client;
@@ -13,6 +14,35 @@ export function init(opts) {
13
14
  client = opts.client;
14
15
  }
15
16
 
17
+ /**
18
+ * @template {keyof typeof client} T
19
+ * @param {T} key
20
+ * @returns {typeof client[T]}
21
+ */
22
+ export function client_method(key) {
23
+ if (!BROWSER) {
24
+ if (key === 'before_navigate' || key === 'after_navigate') {
25
+ // @ts-expect-error doesn't recognize that both keys here return void so expects a async function
26
+ return () => {};
27
+ } else {
28
+ /** @type {Record<string, string>} */
29
+ const name_lookup = {
30
+ disable_scroll_handling: 'disableScrollHandling',
31
+ preload_data: 'preloadData',
32
+ preload_code: 'preloadCode',
33
+ invalidate_all: 'invalidateAll'
34
+ };
35
+
36
+ return () => {
37
+ throw new Error(`Cannot call ${name_lookup[key] ?? key}(...) on the server`);
38
+ };
39
+ }
40
+ } else {
41
+ // @ts-expect-error
42
+ return (...args) => client[key](...args);
43
+ }
44
+ }
45
+
16
46
  export const stores = {
17
47
  url: notifiable_store({}),
18
48
  page: notifiable_store({}),
@@ -54,7 +54,7 @@ export interface Client {
54
54
  disable_scroll_handling(): void;
55
55
  goto: typeof goto;
56
56
  invalidate: typeof invalidate;
57
- invalidateAll: typeof invalidateAll;
57
+ invalidate_all: typeof invalidateAll;
58
58
  preload_code: typeof preloadCode;
59
59
  preload_data: typeof preloadData;
60
60
  apply_action: typeof applyAction;
@@ -8,8 +8,6 @@ import { text } from '../../../exports/index.js';
8
8
  import * as devalue from 'devalue';
9
9
  import { create_async_iterator } from '../../../utils/streaming.js';
10
10
 
11
- export const INVALIDATED_PARAM = 'x-sveltekit-invalidated';
12
-
13
11
  const encoder = new TextEncoder();
14
12
 
15
13
  /**
@@ -16,8 +16,6 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
16
16
  return async (info, init) => {
17
17
  const original_request = normalize_fetch_input(info, init, event.url);
18
18
 
19
- const request_body = init?.body;
20
-
21
19
  // some runtimes (e.g. Cloudflare) error if you access `request.mode`,
22
20
  // annoyingly, so we need to read the value from the `init` object instead
23
21
  let mode = (info instanceof Request ? info.mode : init?.mode) ?? 'cors';
@@ -111,15 +109,6 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
111
109
  }
112
110
  }
113
111
 
114
- if (request_body && typeof request_body !== 'string' && !ArrayBuffer.isView(request_body)) {
115
- // TODO is this still necessary? we just bail out below
116
- // per https://developer.mozilla.org/en-US/docs/Web/API/Request/Request, this can be a
117
- // Blob, BufferSource, FormData, URLSearchParams, USVString, or ReadableStream object.
118
- // non-string bodies are irksome to deal with, but luckily aren't particularly useful
119
- // in this context anyway, so we take the easy route and ban them
120
- throw new Error('Request body must be a string or TypedArray');
121
- }
122
-
123
112
  if (!request.headers.has('accept')) {
124
113
  request.headers.set('accept', '*/*');
125
114
  }
@@ -15,18 +15,21 @@ import {
15
15
  strip_data_suffix
16
16
  } from '../../utils/url.js';
17
17
  import { exec } from '../../utils/routing.js';
18
- import { INVALIDATED_PARAM, redirect_json_response, render_data } from './data/index.js';
18
+ import { redirect_json_response, render_data } from './data/index.js';
19
19
  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';
27
29
  import { get_option } from '../../utils/options.js';
28
30
  import { error, json, text } from '../../exports/index.js';
29
31
  import { action_json_redirect, is_action_json_request } from './page/actions.js';
32
+ import { INVALIDATED_PARAM } from '../shared.js';
30
33
 
31
34
  /* global __SVELTEKIT_ADAPTER_NAME__ */
32
35
 
@@ -94,7 +97,10 @@ export async function respond(request, options, manifest, state) {
94
97
  if (is_data_request) {
95
98
  decoded = strip_data_suffix(decoded) || '/';
96
99
  url.pathname = strip_data_suffix(url.pathname) || '/';
97
- invalidated_data_nodes = url.searchParams.get(INVALIDATED_PARAM)?.split('_').map(Boolean);
100
+ invalidated_data_nodes = url.searchParams
101
+ .get(INVALIDATED_PARAM)
102
+ ?.split('')
103
+ .map((node) => node === '1');
98
104
  url.searchParams.delete(INVALIDATED_PARAM);
99
105
  }
100
106
 
@@ -193,8 +199,11 @@ export async function respond(request, options, manifest, state) {
193
199
 
194
200
  for (const layout of layouts) {
195
201
  if (layout) {
196
- validate_common_exports(layout.server, /** @type {string} */ (layout.server_id));
197
- validate_common_exports(
202
+ validate_layout_server_exports(
203
+ layout.server,
204
+ /** @type {string} */ (layout.server_id)
205
+ );
206
+ validate_layout_exports(
198
207
  layout.universal,
199
208
  /** @type {string} */ (layout.universal_id)
200
209
  );
@@ -203,7 +212,7 @@ export async function respond(request, options, manifest, state) {
203
212
 
204
213
  if (page) {
205
214
  validate_page_server_exports(page.server, /** @type {string} */ (page.server_id));
206
- validate_common_exports(page.universal, /** @type {string} */ (page.universal_id));
215
+ validate_page_exports(page.universal, /** @type {string} */ (page.universal_id));
207
216
  }
208
217
  }
209
218
 
@@ -10,3 +10,5 @@ export function validate_depends(route_id, dep) {
10
10
  );
11
11
  }
12
12
  }
13
+
14
+ export const INVALIDATED_PARAM = 'x-sveltekit-invalidated';
@@ -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
@@ -359,6 +359,10 @@ declare module '@sveltejs/kit/hooks' {
359
359
 
360
360
  /**
361
361
  * A helper function for sequencing multiple `handle` calls in a middleware-like manner.
362
+ * The behavior for the `handle` options is as follows:
363
+ * - `transformPageChunk` is applied in reverse order and merged
364
+ * - `preload` is applied in forward order, the first option "wins" and no `preload` options after it are called
365
+ * - `filterSerializedResponseHeaders` behaves the same as `preload`
362
366
  *
363
367
  * ```js
364
368
  * /// file: src/hooks.server.js
@@ -372,6 +376,10 @@ declare module '@sveltejs/kit/hooks' {
372
376
  * // transforms are applied in reverse order
373
377
  * console.log('first transform');
374
378
  * return html;
379
+ * },
380
+ * preload: () => {
381
+ * // this one wins as it's the first defined in the chain
382
+ * console.log('first preload');
375
383
  * }
376
384
  * });
377
385
  * console.log('first post-processing');
@@ -385,6 +393,13 @@ declare module '@sveltejs/kit/hooks' {
385
393
  * transformPageChunk: ({ html }) => {
386
394
  * console.log('second transform');
387
395
  * return html;
396
+ * },
397
+ * preload: () => {
398
+ * console.log('second preload');
399
+ * },
400
+ * filterSerializedResponseHeaders: () => {
401
+ * // this one wins as it's the first defined in the chain
402
+ * console.log('second filterSerializedResponseHeaders');
388
403
  * }
389
404
  * });
390
405
  * console.log('second post-processing');
@@ -398,7 +413,9 @@ declare module '@sveltejs/kit/hooks' {
398
413
  *
399
414
  * ```
400
415
  * first pre-processing
416
+ * first preload
401
417
  * second pre-processing
418
+ * second filterSerializedResponseHeaders
402
419
  * second transform
403
420
  * first transform
404
421
  * second post-processing
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"
@@ -557,16 +569,16 @@ export interface KitConfig {
557
569
  * If SvelteKit encounters an error while loading the page and detects that a new version has been deployed (using the `name` specified here, which defaults to a timestamp of the build) it will fall back to traditional full-page navigation.
558
570
  * Not all navigations will result in an error though, for example if the JavaScript for the next page is already loaded. If you still want to force a full-page navigation in these cases, use techniques such as setting the `pollInterval` and then using `beforeNavigate`:
559
571
  * ```html
560
- * /// +layout.svelte
572
+ * /// file: +layout.svelte
561
573
  * <script>
562
- * import { beforeNavigate } from '$app/navigation';
563
- * import { updated } from '$app/stores';
574
+ * import { beforeNavigate } from '$app/navigation';
575
+ * import { updated } from '$app/stores';
564
576
  *
565
- * beforeNavigate(({ willUnload, to }) => {
566
- * if ($updated && !willUnload && to?.url) {
567
- * location.href = to.url.href;
568
- * }
569
- * });
577
+ * beforeNavigate(({ willUnload, to }) => {
578
+ * if ($updated && !willUnload && to?.url) {
579
+ * location.href = to.url.href;
580
+ * }
581
+ * });
570
582
  * </script>
571
583
  * ```
572
584
  *
@@ -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