@sveltejs/kit 3.0.0-next.13 → 3.0.0-next.14

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.
Files changed (55) hide show
  1. package/package.json +2 -2
  2. package/src/cli.js +2 -2
  3. package/src/core/adapt/builder.js +8 -5
  4. package/src/core/generate_manifest/index.js +6 -0
  5. package/src/core/postbuild/entities.js +8 -2
  6. package/src/core/postbuild/fallback.js +2 -1
  7. package/src/core/postbuild/prerender.js +12 -3
  8. package/src/core/sync/create_manifest_data/conflict.js +1 -1
  9. package/src/core/sync/create_manifest_data/index.js +33 -2
  10. package/src/core/sync/write_app_types.js +72 -27
  11. package/src/core/sync/write_tsconfig/index.js +2 -1
  12. package/src/core/sync/write_types/index.js +5 -2
  13. package/src/exports/node/index.js +2 -7
  14. package/src/exports/public.d.ts +3 -2
  15. package/src/exports/vite/index.js +45 -11
  16. package/src/exports/vite/utils.js +1 -7
  17. package/src/runtime/app/server/remote/query.js +0 -6
  18. package/src/runtime/client/client.js +148 -38
  19. package/src/runtime/client/fetcher.js +2 -13
  20. package/src/runtime/client/remote-functions/form.svelte.js +3 -7
  21. package/src/runtime/client/remote-functions/prerender.svelte.js +1 -1
  22. package/src/runtime/client/remote-functions/query/index.js +1 -1
  23. package/src/runtime/client/remote-functions/query-batch.svelte.js +1 -1
  24. package/src/runtime/client/remote-functions/query-live/proxy.js +0 -10
  25. package/src/runtime/client/remote-functions/shared.svelte.js +1 -1
  26. package/src/runtime/client/utils.js +1 -0
  27. package/src/runtime/form-utils.js +4 -6
  28. package/src/runtime/pathname.js +37 -0
  29. package/src/runtime/server/fetch.js +5 -8
  30. package/src/runtime/server/index.js +10 -24
  31. package/src/runtime/server/page/crypto.js +2 -2
  32. package/src/runtime/server/page/csp.js +2 -1
  33. package/src/runtime/server/page/load_data.js +2 -29
  34. package/src/runtime/server/page/render.js +19 -1
  35. package/src/runtime/server/page/serialize_data.js +2 -13
  36. package/src/runtime/server/page/server_routing.js +58 -9
  37. package/src/runtime/server/respond.js +20 -27
  38. package/src/runtime/server/state.js +43 -0
  39. package/src/runtime/shared.js +4 -2
  40. package/src/runtime/utils.js +3 -0
  41. package/src/types/ambient-private.d.ts +1 -1
  42. package/src/types/ambient.d.ts +58 -7
  43. package/src/types/global-private.d.ts +1 -1
  44. package/src/types/internal.d.ts +2 -1
  45. package/src/utils/hash.js +21 -0
  46. package/src/utils/http.js +8 -7
  47. package/src/utils/import.js +2 -1
  48. package/src/utils/params.js +1 -1
  49. package/src/utils/regex.js +9 -0
  50. package/src/utils/routing.js +52 -27
  51. package/src/utils/url.js +1 -0
  52. package/src/version.js +1 -1
  53. package/types/index.d.ts +74 -10
  54. package/types/index.d.ts.map +1 -1
  55. package/src/exports/node/polyfills.js +0 -30
@@ -11,7 +11,11 @@ import { uneval_action_response } from './actions.js';
11
11
  import { SVELTE_KIT_ASSETS } from '../../../constants.js';
12
12
  import { SCHEME } from '../../../utils/url.js';
13
13
  import { create_server_routing_response, generate_route_object } from './server_routing.js';
14
- import { add_data_suffix, add_resolution_suffix } from '../../pathname.js';
14
+ import {
15
+ add_data_suffix,
16
+ add_resolution_suffix,
17
+ route_id_resolution_pathname
18
+ } from '../../pathname.js';
15
19
  import { try_get_request_store, with_request_store } from '@sveltejs/kit/internal/server';
16
20
  import { text_encoder } from '../../utils.js';
17
21
  import { count_non_ssi_comments, create_replacer, get_global_name } from '../utils.js';
@@ -390,6 +394,20 @@ export async function render_response({
390
394
  pathname,
391
395
  create_server_routing_response(route, event.params, new URL(pathname, event.url), client)
392
396
  );
397
+
398
+ // Prerender a route-ID-keyed `/_app/routes/<id>/__route.js` module alongside the
399
+ // pathname-keyed one above, so that `preloadCode(id)` can resolve a route ID without
400
+ // hitting the server.
401
+ if (route && !state.prerendering.resolved_route_ids.has(route.id)) {
402
+ state.prerendering.resolved_route_ids.add(route.id);
403
+
404
+ const id_pathname = paths.base + route_id_resolution_pathname(paths.app_dir, route.id);
405
+
406
+ state.prerendering.dependencies.set(
407
+ id_pathname,
408
+ create_server_routing_response(route, null, new URL(id_pathname, event.url), client)
409
+ );
410
+ }
393
411
  }
394
412
 
395
413
  const blocks = [];
@@ -1,5 +1,5 @@
1
1
  import { escape_html } from '../../../utils/escape.js';
2
- import { hash } from '../../../utils/hash.js';
2
+ import { hash_request } from '../../../utils/hash.js';
3
3
 
4
4
  /**
5
5
  * Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser.
@@ -78,18 +78,7 @@ export function serialize_data(fetched, filter, prerendering = false) {
78
78
  }
79
79
 
80
80
  if (fetched.request_headers || fetched.request_body) {
81
- /** @type {import('types').StrictBody[]} */
82
- const values = [];
83
-
84
- if (fetched.request_headers) {
85
- values.push([...new Headers(fetched.request_headers)].join(','));
86
- }
87
-
88
- if (fetched.request_body) {
89
- values.push(fetched.request_body);
90
- }
91
-
92
- attrs.push(`data-hash="${hash(...values)}"`);
81
+ attrs.push(`data-hash="${hash_request(fetched.request_headers, fetched.request_body)}"`);
93
82
  }
94
83
 
95
84
  // Compute the time the response should be cached, taking into account max-age and age.
@@ -84,26 +84,75 @@ export async function resolve_route(resolved_path, url, manifest) {
84
84
  }
85
85
  }
86
86
 
87
+ /**
88
+ * Resolve a route-ID resolution request (`/_app/routes/<id>/__route.js`) to a
89
+ * JS module containing the route's node loaders. Params are always `{}` since
90
+ * this endpoint exists to support `preloadCode(routeId)`, which doesn't need them.
91
+ *
92
+ * The module has one of three shapes, which the client uses to tell three cases apart:
93
+ *
94
+ * - `export const route = {...}` — a page route, with loaders to import
95
+ * - `export const endpoint_only = true` — a real route with no `+page`, so there is
96
+ * nothing to preload, but the client can cache that fact and stop asking
97
+ * - an empty module — no such route
98
+ *
99
+ * @param {string} route_id
100
+ * @param {URL} url
101
+ * @param {SSRManifest} manifest
102
+ * @returns {Response}
103
+ */
104
+ export function resolve_route_by_id(route_id, url, manifest) {
105
+ if (!manifest._.client?.routes) {
106
+ return text('Server-side route resolution disabled', { status: 400 });
107
+ }
108
+
109
+ try {
110
+ const route = manifest._.client.routes.find((r) => r.id === route_id);
111
+
112
+ if (route) {
113
+ return create_server_routing_response(route, null, url, manifest._.client).response;
114
+ }
115
+
116
+ // `client.routes` only contains routes with a `+page`, so a miss above doesn't mean the
117
+ // route doesn't exist — it might be a `+server.js`-only route. `_.routes` includes those
118
+ // (with `page: null`), so we can distinguish "exists but has no code" from "unknown".
119
+ if (manifest._.routes.some((r) => r.id === route_id && !r.page)) {
120
+ return text('export const endpoint_only = true;', { headers: js_headers() });
121
+ }
122
+
123
+ return create_server_routing_response(null, null, url, manifest._.client).response;
124
+ } catch {
125
+ return text('Error resolving route', { status: 500 });
126
+ }
127
+ }
128
+
129
+ function js_headers() {
130
+ return new Headers({
131
+ 'content-type': 'application/javascript; charset=utf-8'
132
+ });
133
+ }
134
+
87
135
  /**
88
136
  * @param {import('types').SSRClientRoute | null} route
89
- * @param {Partial<Record<string, string>>} params
137
+ * @param {Partial<Record<string, string>> | null} params
90
138
  * @param {URL} url
91
139
  * @param {NonNullable<SSRManifest['_']['client']>} client
92
140
  * @returns {{response: Response, body: string}}
93
141
  */
94
142
  export function create_server_routing_response(route, params, url, client) {
95
- const headers = new Headers({
96
- 'content-type': 'application/javascript; charset=utf-8'
97
- });
143
+ const headers = js_headers();
144
+ let body = '';
98
145
 
99
146
  if (route) {
100
147
  const csr_route = generate_route_object(route, url, client);
101
- const body = `${create_css_import(route, url, client)}\nexport const route = ${csr_route}; export const params = ${JSON.stringify(params)};`;
148
+ body = `${create_css_import(route, url, client)}export const route = ${csr_route};`;
102
149
 
103
- return { response: text(body, { headers }), body };
104
- } else {
105
- return { response: text('', { headers }), body: '' };
150
+ if (params !== null) {
151
+ body += `\nexport const params = ${JSON.stringify(params)}`;
152
+ }
106
153
  }
154
+
155
+ return { response: text(body, { headers }), body };
107
156
  }
108
157
 
109
158
  /**
@@ -132,5 +181,5 @@ function create_css_import(route, url, client) {
132
181
 
133
182
  if (!css) return '';
134
183
 
135
- return `${create_client_import(client.start, url)}.then(x => x.load_css([${css}]));`;
184
+ return `${create_client_import(client.start, url)}.then(x => x.load_css([${css}]));\n`;
136
185
  }
@@ -1,4 +1,4 @@
1
- /** @import { RequestState, SSRNode } from 'types' */
1
+ /** @import { SSRNode } from 'types' */
2
2
  import { DEV } from 'esm-env';
3
3
  import { json, text } from '@sveltejs/kit';
4
4
  import { Redirect, SvelteKitError } from '@sveltejs/kit/internal';
@@ -26,13 +26,15 @@ import { validate_server_exports } from '../../utils/exports.js';
26
26
  import { action_json_redirect, is_action_json_request } from './page/actions.js';
27
27
  import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js';
28
28
  import { get_public_env } from './env_module.js';
29
- import { resolve_route } from './page/server_routing.js';
29
+ import { resolve_route, resolve_route_by_id } from './page/server_routing.js';
30
30
  import { validateHeaders } from './validate-headers.js';
31
31
  import {
32
32
  add_data_suffix,
33
33
  add_resolution_suffix,
34
+ extract_route_id,
34
35
  has_data_suffix,
35
36
  has_resolution_suffix,
37
+ is_route_id_resolution_path,
36
38
  strip_data_suffix,
37
39
  strip_resolution_suffix
38
40
  } from '../pathname.js';
@@ -40,6 +42,7 @@ import { server_data_serializer } from './page/data_serializer.js';
40
42
  import { get_remote_id, handle_remote_call } from './remote-functions.js';
41
43
  import { record_span } from '../telemetry/record_span.js';
42
44
  import { otel } from '../telemetry/otel.js';
45
+ import { create_request_state } from './state.js';
43
46
 
44
47
  /** @type {import('types').RequiredResolveOptions['transformPageChunk']} */
45
48
  const default_transform = ({ html }) => html;
@@ -138,12 +141,16 @@ export async function internal_respond(request, options, manifest, state) {
138
141
 
139
142
  let skip_route_resolution = false;
140
143
 
144
+ /** Whether this is a `/${app_dir}/routes/<route_id>/__route.js` request, used by `preloadCode` */
145
+ let is_route_id_resolution_request = false;
146
+
141
147
  if (is_route_resolution_request) {
142
148
  /**
143
149
  * If the request is for a route resolution, first modify the URL, then continue as normal
144
150
  * for path resolution, then return the route object as a JS file.
145
151
  */
146
152
  url.pathname = strip_resolution_suffix(url.pathname);
153
+ is_route_id_resolution_request = is_route_id_resolution_path(url.pathname, base, app_dir);
147
154
  } else if (is_data_request) {
148
155
  url.pathname =
149
156
  strip_data_suffix(url.pathname) +
@@ -174,30 +181,7 @@ export async function internal_respond(request, options, manifest, state) {
174
181
  url
175
182
  );
176
183
 
177
- /** @type {RequestState} */
178
- const event_state = {
179
- prerendering: state.prerendering,
180
- transport: options.hooks.transport,
181
- handleValidationError: options.hooks.handleValidationError,
182
- tracing: {
183
- record_span
184
- },
185
- remote: {
186
- data: null,
187
- explicit: null,
188
- implicit: null,
189
- forms: null,
190
- requested: null,
191
- batches: null,
192
- live_iterators: null
193
- },
194
- is_in_remote_function: false,
195
- is_in_remote_form_or_command: false,
196
- is_in_remote_query: false,
197
- is_in_remote_prerender: false,
198
- is_in_render: false,
199
- is_in_universal_load: false
200
- };
184
+ const event_state = create_request_state(state, options.hooks);
201
185
 
202
186
  /** @type {import('@sveltejs/kit').RequestEvent} */
203
187
  const event = {
@@ -270,7 +254,8 @@ export async function internal_respond(request, options, manifest, state) {
270
254
  /** @type {string | null} */
271
255
  let resolved_path = url.pathname;
272
256
 
273
- if (!remote_id) {
257
+ // `reroute` hooks receive pathnames, so they must not run for route-ID resolution requests
258
+ if (!remote_id && !is_route_id_resolution_request) {
274
259
  const prerendering_reroute_state = state.prerendering?.inside_reroute;
275
260
  try {
276
261
  // For the duration or a reroute, disable the prerendering state as reroute could call API endpoints
@@ -354,6 +339,14 @@ export async function internal_respond(request, options, manifest, state) {
354
339
  }
355
340
 
356
341
  if (is_route_resolution_request) {
342
+ if (is_route_id_resolution_request) {
343
+ return resolve_route_by_id(
344
+ extract_route_id(resolved_path, app_dir),
345
+ new URL(request.url),
346
+ manifest
347
+ );
348
+ }
349
+
357
350
  return resolve_route(resolved_path, new URL(request.url), manifest);
358
351
  }
359
352
 
@@ -0,0 +1,43 @@
1
+ /** @import { RequestState, ServerHooks, SSRState } from 'types' */
2
+ import { record_span } from '../telemetry/record_span.js';
3
+
4
+ /**
5
+ * @param {SSRState} state
6
+ * @param {ServerHooks} hooks
7
+ * @returns {RequestState}
8
+ */
9
+ export function create_request_state(state, hooks) {
10
+ // Request state is rebuilt fresh, resetting remote caches and context flags.
11
+ return {
12
+ prerendering: state.prerendering,
13
+ transport: hooks.transport,
14
+ handleValidationError: hooks.handleValidationError,
15
+ tracing: {
16
+ record_span
17
+ },
18
+ remote: {
19
+ data: null,
20
+ explicit: null,
21
+ implicit: null,
22
+ forms: null,
23
+ requested: null,
24
+ batches: null,
25
+ live_iterators: null
26
+ },
27
+ is_in_remote_function: false,
28
+ is_in_remote_form_or_command: false,
29
+ is_in_remote_query: false,
30
+ is_in_remote_prerender: false,
31
+ is_in_render: false,
32
+ is_in_universal_load: false
33
+ };
34
+ }
35
+
36
+ /**
37
+ * @param {SSRState} state
38
+ * @returns {SSRState}
39
+ */
40
+ export function fork_state_for_subrequest(state) {
41
+ // Sub-requests inherit all server state except for the incremented depth.
42
+ return { ...state, depth: state.depth + 1 };
43
+ }
@@ -1,6 +1,6 @@
1
1
  /** @import { Transport } from '@sveltejs/kit' */
2
2
  import * as devalue from 'devalue';
3
- import { base64_decode, base64_encode, text_encoder } from './utils.js';
3
+ import { base64_decode, base64_encode, text_decoder, text_encoder } from './utils.js';
4
4
 
5
5
  /**
6
6
  * @param {string} route_id
@@ -331,6 +331,7 @@ export async function stringify_command_arg(value, transport) {
331
331
  */
332
332
  function url_friendly_base64_encode(string) {
333
333
  const bytes = text_encoder.encode(string);
334
+ // TODO replace with `bytes.toBase64({ alphabet: 'base64url', omitPadding: true })` when we require Node >= 25
334
335
  return base64_encode(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
335
336
  }
336
337
 
@@ -342,7 +343,8 @@ function url_friendly_base64_encode(string) {
342
343
  export function parse_remote_arg(string, transport) {
343
344
  if (!string) return undefined;
344
345
 
345
- const json_string = new TextDecoder().decode(
346
+ const json_string = text_decoder.decode(
347
+ // TODO replace with `Uint8Array.fromBase64(string, { alphabet: 'base64url' })` when we require Node >= 25
346
348
  // no need to add back `=` characters, atob can handle it
347
349
  base64_decode(string.replaceAll('-', '+').replaceAll('_', '/'))
348
350
  );
@@ -1,6 +1,7 @@
1
1
  import { BROWSER } from 'esm-env';
2
2
 
3
3
  export const text_encoder = new TextEncoder();
4
+ export const text_decoder = new TextDecoder();
4
5
 
5
6
  /**
6
7
  * Like node's path.relative, but without using node
@@ -28,6 +29,7 @@ export function get_relative_path(from, to) {
28
29
  * @returns {string}
29
30
  */
30
31
  export function base64_encode(bytes) {
32
+ // TODO replace with `bytes.toBase64()` when we require Node >= 25
31
33
  // Using `Buffer` is faster than iterating
32
34
  if (!BROWSER && globalThis.Buffer) {
33
35
  return globalThis.Buffer.from(bytes).toString('base64');
@@ -47,6 +49,7 @@ export function base64_encode(bytes) {
47
49
  * @returns {Uint8Array}
48
50
  */
49
51
  export function base64_decode(encoded) {
52
+ // TODO replace with `Uint8Array.fromBase64(encoded)` when we require Node >= 25
50
53
  // Using `Buffer` is faster than iterating
51
54
  if (!BROWSER && globalThis.Buffer) {
52
55
  const buffer = globalThis.Buffer.from(encoded, 'base64');
@@ -51,5 +51,5 @@ declare module '__sveltekit/manifest-data' {
51
51
  export const immutable: Array<{ path: string }>;
52
52
  export const assets: Array<{ path: string }>;
53
53
  export const prerendered: Array<{ path: string }>;
54
- export const routes: Array<{ id: string }>;
54
+ export const routes: Array<{ id: string; page: boolean; endpoint: boolean }>;
55
55
  }
@@ -78,9 +78,41 @@ declare module '$app/manifest' {
78
78
  */
79
79
  export const prerendered: Array<{ path: import('$app/types').Path }>;
80
80
  /**
81
- * An array of objects with an `id` property representing the routes in your app.
82
- */
83
- export const routes: Array<{ id: import('$app/types').RouteId }>;
81
+ * A route in your app, along with its capabilities. `page` indicates the presence of a `+page`,
82
+ * while `endpoint` indicates the presence of a `+server`. Both are `true` when both files exist.
83
+ */
84
+ export type ManifestRoute =
85
+ | {
86
+ id: Exclude<import('$app/types').PageRouteId, import('$app/types').EndpointRouteId>;
87
+ page: true;
88
+ endpoint: false;
89
+ }
90
+ | {
91
+ id: Exclude<import('$app/types').EndpointRouteId, import('$app/types').PageRouteId>;
92
+ page: false;
93
+ endpoint: true;
94
+ }
95
+ | {
96
+ id: Extract<import('$app/types').PageRouteId, import('$app/types').EndpointRouteId>;
97
+ page: true;
98
+ endpoint: true;
99
+ };
100
+ /**
101
+ * An array of objects representing the routes in your app. Only routes that the router can match
102
+ * are included — directories that merely hold a `+layout` are not routes of their own.
103
+ *
104
+ * Each object has an `id`, plus `page` and `endpoint` booleans describing whether the route has a
105
+ * `+page` and/or a `+server`. Both are `true` for a route that has both, so the capabilities can
106
+ * be filtered independently:
107
+ *
108
+ * ```js
109
+ * import { routes } from '$app/manifest';
110
+ *
111
+ * const pages = routes.filter((route) => route.page);
112
+ * const endpoints = routes.filter((route) => route.endpoint);
113
+ * ```
114
+ */
115
+ export const routes: ManifestRoute[];
84
116
  }
85
117
 
86
118
  /**
@@ -94,6 +126,8 @@ declare module '$app/types' {
94
126
  // These are all functions so that we can leverage function overloads to get the correct type.
95
127
  // Using the return types directly would error with a "not the same type" error.
96
128
  // https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
129
+ PageRouteId(): string;
130
+ EndpointRouteId(): string;
97
131
  RouteId(): string;
98
132
  RouteParams(): Record<string, Record<string, string>>;
99
133
  LayoutParams(): Record<string, Record<string, string>>;
@@ -103,7 +137,21 @@ declare module '$app/types' {
103
137
  }
104
138
 
105
139
  /**
106
- * A union of all the route IDs in your app. Used for `page.route.id` and `event.route.id`.
140
+ * A union of the route IDs in your app that have a `+page`.
141
+ *
142
+ * A route ID can be in both `PageRouteId` and `EndpointRouteId`, if its directory contains both a `+page` and a `+server`.
143
+ */
144
+ export type PageRouteId = ReturnType<AppTypes['PageRouteId']>;
145
+
146
+ /**
147
+ * A union of the route IDs in your app that have a `+server`.
148
+ *
149
+ * A route ID can be in both `PageRouteId` and `EndpointRouteId`, if its directory contains both a `+page` and a `+server`.
150
+ */
151
+ export type EndpointRouteId = ReturnType<AppTypes['EndpointRouteId']>;
152
+
153
+ /**
154
+ * A union of all the route IDs in your app — the union of `PageRouteId` and `EndpointRouteId`. Used for `page.route.id` and `event.route.id`.
107
155
  */
108
156
  export type RouteId = ReturnType<AppTypes['RouteId']>;
109
157
 
@@ -119,12 +167,15 @@ declare module '$app/types' {
119
167
  ? ReturnType<AppTypes['RouteParams']>[T]
120
168
  : Record<string, never>;
121
169
 
170
+ /**
171
+ * The route IDs accepted by `LayoutParams`. Like `RouteId`, these preserve route groups and `[param]` syntax, but they identify directories containing layouts rather than matchable routes.
172
+ */
173
+ type LayoutParamsId = keyof ReturnType<AppTypes['LayoutParams']>;
174
+
122
175
  /**
123
176
  * A utility for getting the parameters associated with a given layout, which is similar to `RouteParams` but also includes optional parameters for any child route.
124
177
  */
125
- export type LayoutParams<T extends RouteId> = T extends keyof ReturnType<AppTypes['LayoutParams']>
126
- ? ReturnType<AppTypes['LayoutParams']>[T]
127
- : Record<string, never>;
178
+ export type LayoutParams<T extends LayoutParamsId> = ReturnType<AppTypes['LayoutParams']>[T];
128
179
 
129
180
  /**
130
181
  * A union of all valid paths in your app, relative to the `base` path.
@@ -53,7 +53,7 @@ declare global {
53
53
  const __SVELTEKIT_MANIFEST_IMMUTABLE__: string[];
54
54
  const __SVELTEKIT_MANIFEST_ASSETS__: string[];
55
55
  const __SVELTEKIT_MANIFEST_PRERENDERED__: string[];
56
- const __SVELTEKIT_MANIFEST_ROUTES__: { id: string }[];
56
+ const __SVELTEKIT_MANIFEST_ROUTES__: Array<{ id: string }>;
57
57
  /**
58
58
  * This makes the use of specific features visible at both dev and build time, in such a
59
59
  * way that we can error when they are not supported by the target platform.
@@ -239,6 +239,8 @@ export interface PrerenderOptions {
239
239
  dependencies: Map<string, PrerenderDependency>;
240
240
  /** Results of remote `prerender` functions, shared across the whole prerender run so that each only executes once */
241
241
  remote_responses: Map<string, Promise<any>>;
242
+ /** Route IDs whose resolution module has been emitted, shared across the whole prerender run so that each only generates once */
243
+ resolved_route_ids: Set<string>;
242
244
  /** True for the duration of a call to the `reroute` hook */
243
245
  inside_reroute?: boolean;
244
246
  }
@@ -536,7 +538,6 @@ export interface SSRClientRoute {
536
538
  }
537
539
 
538
540
  export interface SSRState {
539
- fallback?: string;
540
541
  getClientAddress(): string;
541
542
  /**
542
543
  * True if we're currently attempting to render an error page.
package/src/utils/hash.js CHANGED
@@ -20,3 +20,24 @@ export function hash(...values) {
20
20
 
21
21
  return (hash >>> 0).toString(36);
22
22
  }
23
+
24
+ /**
25
+ * Hash of the headers and body a `fetch` was called with. The server-side serializer and the
26
+ * client-side cache lookup must produce identical values for cached responses to be found.
27
+ * @param {HeadersInit | undefined} headers
28
+ * @param {import('types').StrictBody | null | undefined} body
29
+ */
30
+ export function hash_request(headers, body) {
31
+ /** @type {import('types').StrictBody[]} */
32
+ const values = [];
33
+
34
+ if (headers) {
35
+ values.push([...new Headers(headers)].join(','));
36
+ }
37
+
38
+ if (body) {
39
+ values.push(body);
40
+ }
41
+
42
+ return hash(...values);
43
+ }
package/src/utils/http.js CHANGED
@@ -11,7 +11,7 @@ export function negotiate(accept, types) {
11
11
  const parts = [];
12
12
 
13
13
  accept.split(',').forEach((str, i) => {
14
- const match = /([^/ \t]+)\/([^; \t]+)[ \t]*(?:;[ \t]*q=([0-9.]+))?/.exec(str);
14
+ const match = /^[ \t]*([^/ \t]+)\/([^; \t]+)[ \t]*(?:;[ \t]*q=([0-9.]+))?/.exec(str);
15
15
 
16
16
  // no match equals invalid header — ignore
17
17
  if (match) {
@@ -57,12 +57,13 @@ export function negotiate(accept, types) {
57
57
  }
58
58
 
59
59
  /**
60
- * Returns `true` if the request contains a `content-type` header with the given type
61
- * @param {Request} request
60
+ * Returns `true` if a `content-type` header value is one of the given types, ignoring
61
+ * parameters such as `charset` and comparing case-insensitively
62
+ * @param {string | null | undefined} header
62
63
  * @param {...string} types
63
64
  */
64
- function is_content_type(request, ...types) {
65
- const type = request.headers.get('content-type')?.split(';', 1)[0].trim() ?? '';
65
+ export function matches_content_type(header, ...types) {
66
+ const type = header?.split(';', 1)[0].trim() ?? '';
66
67
  return types.includes(type.toLowerCase());
67
68
  }
68
69
 
@@ -72,8 +73,8 @@ function is_content_type(request, ...types) {
72
73
  export function is_form_content_type(request) {
73
74
  // These content types must be protected against CSRF
74
75
  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/enctype
75
- return is_content_type(
76
- request,
76
+ return matches_content_type(
77
+ request.headers.get('content-type'),
77
78
  'application/x-www-form-urlencoded',
78
79
  'multipart/form-data',
79
80
  'text/plain',
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
3
4
 
4
5
  /**
5
6
  * Resolves a peer dependency relative to the current working directory.
@@ -51,7 +52,7 @@ function resolve_peer(dependency, root) {
51
52
  */
52
53
  export async function import_peer(dependency, root) {
53
54
  try {
54
- return await import(/* @vite-ignore */ resolve_peer(dependency, root));
55
+ return await import(/* @vite-ignore */ pathToFileURL(resolve_peer(dependency, root)).href);
55
56
  } catch {
56
57
  return await import(/* @vite-ignore */ dependency);
57
58
  }
@@ -25,7 +25,7 @@ export function collect_matcher_names(routes) {
25
25
  */
26
26
  export function validate_param_matchers(params, names, file) {
27
27
  for (const name of names) {
28
- if (!(name in params)) {
28
+ if (!Object.hasOwn(params, name)) {
29
29
  throw new Error(`No matcher found for parameter '${name}'${file ? ` in ${file}` : ''}`);
30
30
  }
31
31
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Escapes characters that have special meaning in a regular expression.
3
+ * @param {string} str
4
+ * @returns {string} escaped string
5
+ */
6
+ export function escape_for_regexp(str) {
7
+ // TODO replace with `RegExp.escape(str)` when we require Node >= 24
8
+ return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match);
9
+ }