@sveltejs/kit 3.0.0-next.12 → 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 (77) hide show
  1. package/package.json +11 -11
  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 +15 -6
  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 +73 -29
  11. package/src/core/sync/write_tsconfig/index.js +15 -2
  12. package/src/core/sync/write_types/index.js +5 -2
  13. package/src/exports/index.js +23 -12
  14. package/src/exports/internal/shared.js +4 -12
  15. package/src/exports/node/index.js +2 -7
  16. package/src/exports/public.d.ts +61 -12
  17. package/src/exports/vite/build/remote.js +10 -12
  18. package/src/exports/vite/dev/index.js +32 -19
  19. package/src/exports/vite/index.js +162 -133
  20. package/src/exports/vite/utils.js +1 -7
  21. package/src/runtime/app/server/remote/form.js +9 -1
  22. package/src/runtime/app/server/remote/prerender.js +13 -9
  23. package/src/runtime/app/server/remote/query.js +0 -6
  24. package/src/runtime/app/server/remote/requested.js +4 -4
  25. package/src/runtime/app/state/client.js +3 -0
  26. package/src/runtime/app/state/index.js +2 -2
  27. package/src/runtime/app/state/server.js +3 -0
  28. package/src/runtime/client/client.js +731 -342
  29. package/src/runtime/client/constants.js +2 -6
  30. package/src/runtime/client/fetcher.js +24 -25
  31. package/src/runtime/client/remote-functions/form.svelte.js +42 -14
  32. package/src/runtime/client/remote-functions/prerender.svelte.js +2 -2
  33. package/src/runtime/client/remote-functions/query/index.js +1 -1
  34. package/src/runtime/client/remote-functions/query/instance.svelte.js +2 -2
  35. package/src/runtime/client/remote-functions/query-batch.svelte.js +2 -2
  36. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +2 -2
  37. package/src/runtime/client/remote-functions/query-live/iterator.js +10 -8
  38. package/src/runtime/client/remote-functions/query-live/proxy.js +0 -10
  39. package/src/runtime/client/remote-functions/shared.svelte.js +12 -11
  40. package/src/runtime/client/state.svelte.js +10 -22
  41. package/src/runtime/client/types.d.ts +1 -2
  42. package/src/runtime/client/utils.js +22 -14
  43. package/src/runtime/components/root.svelte +4 -14
  44. package/src/runtime/form-utils.js +4 -6
  45. package/src/runtime/pathname.js +37 -0
  46. package/src/runtime/props.svelte.js +71 -0
  47. package/src/runtime/server/fetch.js +9 -14
  48. package/src/runtime/server/index.js +10 -24
  49. package/src/runtime/server/page/crypto.js +2 -2
  50. package/src/runtime/server/page/csp.js +88 -104
  51. package/src/runtime/server/page/index.js +1 -2
  52. package/src/runtime/server/page/load_data.js +16 -33
  53. package/src/runtime/server/page/render.js +46 -25
  54. package/src/runtime/server/page/respond_with_error.js +1 -3
  55. package/src/runtime/server/page/serialize_data.js +2 -13
  56. package/src/runtime/server/page/server_routing.js +58 -9
  57. package/src/runtime/server/respond.js +20 -29
  58. package/src/runtime/server/state.js +43 -0
  59. package/src/runtime/shared.js +4 -2
  60. package/src/runtime/utils.js +3 -0
  61. package/src/types/ambient-private.d.ts +2 -2
  62. package/src/types/ambient.d.ts +59 -8
  63. package/src/types/global-private.d.ts +1 -1
  64. package/src/types/internal.d.ts +4 -8
  65. package/src/types/private.d.ts +3 -12
  66. package/src/utils/hash.js +21 -0
  67. package/src/utils/http.js +8 -7
  68. package/src/utils/import.js +2 -1
  69. package/src/utils/params.js +1 -1
  70. package/src/utils/regex.js +9 -0
  71. package/src/utils/routing.js +52 -27
  72. package/src/utils/url.js +1 -0
  73. package/src/version.js +1 -1
  74. package/types/index.d.ts +165 -66
  75. package/types/index.d.ts.map +2 -1
  76. package/src/exports/node/polyfills.js +0 -30
  77. package/src/runtime/types.d.ts +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "3.0.0-next.12",
3
+ "version": "3.0.0-next.14",
4
4
  "description": "SvelteKit is the fastest way to build Svelte apps",
5
5
  "keywords": [
6
6
  "framework",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@opentelemetry/api": "^1.9.0",
33
- "@playwright/test": "^1.61.1",
33
+ "@playwright/test": "~1.61.0",
34
34
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
35
35
  "@types/connect": "^3.4.38",
36
36
  "@types/node": "^22.19.19",
@@ -40,7 +40,7 @@
40
40
  "svelte": "^5.56.3",
41
41
  "svelte-preprocess": "^6.0.5",
42
42
  "typescript": "~6.0.3",
43
- "valibot": "^1.2.0",
43
+ "valibot": "^1.4.2",
44
44
  "vite": "^8.1.5",
45
45
  "vitest": "^4.1.10"
46
46
  },
@@ -127,7 +127,7 @@
127
127
  },
128
128
  "types": "types/index.d.ts",
129
129
  "engines": {
130
- "node": ">=22"
130
+ "node": ">=22.17"
131
131
  },
132
132
  "scripts": {
133
133
  "lint": "prettier --config ../../.prettierrc --check .",
@@ -136,15 +136,15 @@
136
136
  "format": "prettier --config ../../.prettierrc --write .",
137
137
  "test": "pnpm test:unit && pnpm test:integration",
138
138
  "test:integration": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test",
139
- "test:cross-platform:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:dev",
140
- "test:cross-platform:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:build",
139
+ "test:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:dev",
140
+ "test:build": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:build",
141
+ "test:cross-platform:dev": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:dev",
142
+ "test:cross-platform:build": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:build",
141
143
  "test:server-side-route-resolution:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:server-side-route-resolution:dev",
142
- "test:server-side-route-resolution:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:server-side-route-resolution:build",
144
+ "test:server-side-route-resolution:build": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:server-side-route-resolution:build",
143
145
  "test:svelte-async:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:svelte-async:dev",
144
- "test:svelte-async:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:svelte-async:build",
145
- "test:unit:dev": "vitest --config kit.vitest.config.js run",
146
- "test:unit:prod": "NODE_ENV=production vitest --config kit.vitest.config.js run csp.spec.js cookie.spec.js",
147
- "test:unit": "pnpm test:unit:dev && pnpm test:unit:prod",
146
+ "test:svelte-async:build": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:svelte-async:build",
147
+ "test:unit": "vitest run ./",
148
148
  "generate:version": "node scripts/generate-version.js",
149
149
  "generate:types": "node scripts/generate-dts.js"
150
150
  }
package/src/cli.js CHANGED
@@ -75,10 +75,10 @@ if (!command) {
75
75
  }
76
76
 
77
77
  if (command === 'sync') {
78
- // create placeholder node_modules/$app/tsconfig/tsconfig.json if necessary, to squelch warnings.
78
+ // create placeholder node_modules/$app/tsconfig.json if necessary, to squelch warnings.
79
79
  // this isn't bulletproof — if someone has some esoteric config, it will continue
80
80
  // to harmlessly warn — but we handle the 90% case and clean up after ourselves
81
- const dir = 'node_modules/$app/tsconfig';
81
+ const dir = 'node_modules/$app';
82
82
  const base_tsconfig = `${dir}/tsconfig.json`;
83
83
  const base_tsconfig_json = '{}';
84
84
 
@@ -109,16 +109,19 @@ export function create_builder({
109
109
 
110
110
  async compress(directory) {
111
111
  if (!existsSync(directory)) {
112
- return;
112
+ return [];
113
113
  }
114
114
 
115
- const files = list_files(directory, (file) => extensions.includes(extname(file))).map(
116
- (file) => resolve(directory, file)
117
- );
115
+ const files = list_files(directory, (file) => extensions.includes(extname(file)));
118
116
 
119
117
  await Promise.all(
120
- files.flatMap((file) => [compress_file(file, 'gz'), compress_file(file, 'br')])
118
+ files.flatMap((file) => {
119
+ const abs = resolve(directory, file);
120
+ return [compress_file(abs, 'gz'), compress_file(abs, 'br')];
121
+ })
121
122
  );
123
+
124
+ return files;
122
125
  },
123
126
 
124
127
  findServerAssets(route_data) {
@@ -94,6 +94,12 @@ export function generate_manifest({
94
94
  mime_types[ext] ??= mime_lookup(ext) || '';
95
95
  }
96
96
 
97
+ // record extensions that only exist in prerendered output, e.g. a prerendered favicon.ico
98
+ for (const pathname of prerendered) {
99
+ const ext = path.extname(pathname);
100
+ if (ext) mime_types[ext] ??= mime_lookup(ext) || '';
101
+ }
102
+
97
103
  // prettier-ignore
98
104
  // String representation of
99
105
  /** @template {import('@sveltejs/kit').SSRManifest} T */
@@ -2236,7 +2236,7 @@ const entities = {
2236
2236
  'zwnj;': '‌'
2237
2237
  };
2238
2238
 
2239
- const numeric = /&#(x)?([0-9a-f]+);/i;
2239
+ const numeric = /&#(x)?([0-9a-f]+);/gi;
2240
2240
  const named = new RegExp(
2241
2241
  `&(${Object.keys(entities)
2242
2242
  .sort((a, b) => b.length - a.length)
@@ -2247,6 +2247,12 @@ const named = new RegExp(
2247
2247
  /** @param {string} str */
2248
2248
  export function decode(str) {
2249
2249
  return str
2250
- .replace(numeric, (_match, hex, code) => String.fromCharCode(hex ? parseInt(code, 16) : +code))
2250
+ .replace(numeric, (_match, hex, code) => {
2251
+ const codepoint = hex ? parseInt(code, 16) : +code;
2252
+ // mirror the HTML parser, which replaces invalid numeric references with U+FFFD
2253
+ return Number.isInteger(codepoint) && codepoint <= 0x10ffff
2254
+ ? String.fromCodePoint(codepoint)
2255
+ : '\ufffd';
2256
+ })
2251
2257
  .replace(named, (_match, entity) => entities[entity]);
2252
2258
  }
@@ -39,7 +39,8 @@ async function generate_fallback({ manifest_path, env, out_dir, origin, assets }
39
39
  prerendering: {
40
40
  fallback: true,
41
41
  dependencies: new Map(),
42
- remote_responses: new Map()
42
+ remote_responses: new Map(),
43
+ resolved_route_ids: new Set()
43
44
  },
44
45
  read: (file) => readFileSync(join(assets, file))
45
46
  });
@@ -17,6 +17,7 @@ import { createReadableStream } from '@sveltejs/kit/node';
17
17
  import generate_fallback from './fallback.js';
18
18
  import { stringify_remote_arg } from '../../runtime/shared.js';
19
19
  import { log_response } from '../../exports/vite/utils.js';
20
+ import { matches_content_type } from '../../utils/http.js';
20
21
 
21
22
  export default forked(import.meta.url, prerender);
22
23
 
@@ -262,10 +263,13 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
262
263
  /** @type {Map<string, Promise<any>>} */
263
264
  const remote_responses = new Map();
264
265
 
266
+ /** @type {Set<string>} */
267
+ const resolved_route_ids = new Set();
268
+
265
269
  /** @type {Map<string, Set<string>>} */
266
270
  const expected_hashlinks = new Map();
267
271
 
268
- /** @type {Map<string, string[]>} */
272
+ /** @type {Map<string, Set<string>>} */
269
273
  const actual_hashlinks = new Map();
270
274
 
271
275
  /**
@@ -307,7 +311,8 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
307
311
  },
308
312
  prerendering: {
309
313
  dependencies,
310
- remote_responses
314
+ remote_responses,
315
+ resolved_route_ids
311
316
  },
312
317
  read: (file) => {
313
318
  // stuff we just wrote
@@ -387,14 +392,18 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
387
392
  const headers = Object.fromEntries(response.headers);
388
393
 
389
394
  // if it's a 200 HTML response, crawl it. Skip error responses, as we don't save those
390
- if (response.ok && config.prerender.crawl && headers['content-type'] === 'text/html') {
395
+ if (
396
+ response.ok &&
397
+ config.prerender.crawl &&
398
+ matches_content_type(headers['content-type'], 'text/html')
399
+ ) {
391
400
  const { ids, hrefs, invalid } = crawl(body.toString(), decoded);
392
401
 
393
402
  for (const href of invalid) {
394
403
  handle_invalid_url({ href, referrer: decoded });
395
404
  }
396
405
 
397
- actual_hashlinks.set(decoded, ids);
406
+ actual_hashlinks.set(decoded, new Set(ids));
398
407
 
399
408
  /** @param {string} href */
400
409
  const removePrerenderOrigin = (href) => {
@@ -444,7 +453,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
444
453
  const headers = Object.fromEntries(response.headers);
445
454
 
446
455
  const type = headers['content-type'];
447
- const is_html = response_type === REDIRECT || type === 'text/html';
456
+ const is_html = response_type === REDIRECT || matches_content_type(type, 'text/html');
448
457
 
449
458
  if (!is_html && response.status === 200 && decoded.slice(config.paths.base.length + 1) === '') {
450
459
  throw new Error(
@@ -646,7 +655,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
646
655
  // ignore fragment links to pages that were not prerendered
647
656
  if (!hashlinks) continue;
648
657
 
649
- if (!hashlinks.includes(id) && !SPECIAL_HASHLINKS.has(id)) {
658
+ if (!hashlinks.has(id) && !SPECIAL_HASHLINKS.has(id)) {
650
659
  handle_missing_id({ id, path, referrers: Array.from(referrers) });
651
660
  }
652
661
  }
@@ -60,7 +60,7 @@ function normalize_route_id(id) {
60
60
  .replace(/(?<=^|\/)\(.+?\)(?=$|\/)/g, '')
61
61
 
62
62
  .replace(/\[[ux]\+([0-9a-f]+)\]/g, (_, x) =>
63
- String.fromCharCode(parseInt(x, 16)).replace(/\//g, '%2f')
63
+ String.fromCodePoint(parseInt(x, 16)).replace(/\//g, '%2f')
64
64
  )
65
65
 
66
66
  // replace `[param]` with `<*>`, `[param=x]` with `<x>`, and `[[param]]` with `<?*>`
@@ -41,6 +41,34 @@ export default function create_manifest_data({
41
41
  };
42
42
  }
43
43
 
44
+ /**
45
+ * Whether this route has a `+page`. Independent of `is_endpoint_route` — a route can be both.
46
+ * @param {import('types').RouteData} route
47
+ * @returns {boolean}
48
+ */
49
+ export function is_page_route(route) {
50
+ return !!route.page;
51
+ }
52
+
53
+ /**
54
+ * Whether this route has a `+server`. Independent of `is_page_route` — a route can be both.
55
+ * @param {import('types').RouteData} route
56
+ * @returns {boolean}
57
+ */
58
+ export function is_endpoint_route(route) {
59
+ return !!route.endpoint;
60
+ }
61
+
62
+ /**
63
+ * Whether the router can match this route. `manifest_data.routes` also contains entries for
64
+ * directories with layouts or errors, which never reach `event.route.id`.
65
+ * @param {import('types').RouteData} route
66
+ * @returns {boolean}
67
+ */
68
+ export function is_app_route(route) {
69
+ return is_page_route(route) || is_endpoint_route(route);
70
+ }
71
+
44
72
  /**
45
73
  * Returns a list of files in the `static` directory.
46
74
  * @param {import('types').ValidatedConfig} config
@@ -85,7 +113,7 @@ function resolve_params(config, cwd) {
85
113
  */
86
114
  function create_routes_and_nodes(cwd, config, fallback) {
87
115
  /** @type {import('types').RouteData[]} */
88
- const routes = [];
116
+ let routes = [];
89
117
 
90
118
  const routes_base = posixify(path.relative(cwd, config.kit.files.routes));
91
119
 
@@ -125,7 +153,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
125
153
  );
126
154
  }
127
155
 
128
- return String.fromCharCode(parseInt(code, 16));
156
+ return String.fromCodePoint(parseInt(code, 16));
129
157
  }
130
158
  });
131
159
 
@@ -440,6 +468,9 @@ function create_routes_and_nodes(cwd, config, fallback) {
440
468
  }
441
469
  }
442
470
 
471
+ // remove route objects with no route file
472
+ routes = routes.filter((route) => route.endpoint || route.leaf || route.layout || route.error);
473
+
443
474
  // add parents to error nodes so that we can compute which page options apply to them
444
475
  for (const route of routes) {
445
476
  if (!route.error) continue;
@@ -4,15 +4,42 @@ import { resolve_entry } from '../../utils/filesystem.js';
4
4
  import { posixify } from '../../utils/os.js';
5
5
  import { write_if_changed } from './utils.js';
6
6
  import { s } from '../../utils/misc.js';
7
- import { get_route_segments } from '../../utils/routing.js';
8
-
9
- const replace_optional_params = (/** @type {string} */ id) =>
10
- id.replace(/\/\[\[[^\]]+\]\]/g, '${string}');
11
- const replace_required_params = (/** @type {string} */ id) =>
12
- id.replace(/\/\[[^\]]+\]/g, '/${string}');
13
- /** Convert route ID to pathname by removing layout groups */
14
- const remove_group_segments = (/** @type {string} */ id) => {
15
- return '/' + get_route_segments(id).join('/');
7
+ import {
8
+ decode_escape_sequence,
9
+ encode_pathname_chars,
10
+ get_route_segments,
11
+ segment_pattern
12
+ } from '../../utils/routing.js';
13
+ import { is_app_route, is_endpoint_route, is_page_route } from './create_manifest_data/index.js';
14
+
15
+ const optional_param_pattern = /^\[\[[\w-]+(?:=[\w-]+)?\]\]$/;
16
+ const rest_param_pattern = /^\[\.\.\.[\w-]+(?:=[\w-]+)?\]$/;
17
+
18
+ /**
19
+ * Convert a route ID to the pathnames it can match (relative to the base path), in which each
20
+ * param is replaced with `${string}` and each escape sequence is expanded. A param that fills an
21
+ * entire segment can be absent, so it contributes a pathname with the segment and one without,
22
+ * rather than one that absorbs the `/`
23
+ * @param {string} id
24
+ */
25
+ const get_pathname_patterns = (id) => {
26
+ let pathnames = [''];
27
+
28
+ for (const segment of get_route_segments(id)) {
29
+ const omittable = optional_param_pattern.test(segment) || rest_param_pattern.test(segment);
30
+ const content = omittable
31
+ ? '${string}'
32
+ : segment.replace(segment_pattern, (_, escape_type, escape_code) =>
33
+ escape_type ? encode_pathname_chars(decode_escape_sequence(escape_code)) : '${string}'
34
+ );
35
+
36
+ pathnames = pathnames.flatMap((pathname) => {
37
+ const joined = pathname === '' ? content : `${pathname}/${content}`;
38
+ return omittable ? [joined, pathname] : [joined];
39
+ });
40
+ }
41
+
42
+ return [...new Set(pathnames)];
16
43
  };
17
44
 
18
45
  /**
@@ -53,8 +80,6 @@ function get_pathnames_for_trailing_slash(pathname, route) {
53
80
  const template = `
54
81
  declare module "svelte/elements" {
55
82
  export interface HTMLAttributes<T> {
56
- 'data-sveltekit-keepfocus'?: true | false | '' | undefined | null;
57
- 'data-sveltekit-noscroll'?: true | false | '' | undefined | null;
58
83
  'data-sveltekit-preload-code'?:
59
84
  | true
60
85
  | false
@@ -68,6 +93,7 @@ declare module "svelte/elements" {
68
93
  'data-sveltekit-preload-data'?: true | false | '' | 'hover' | 'tap' | undefined | null;
69
94
  'data-sveltekit-reload'?: true | false | '' | undefined | null;
70
95
  'data-sveltekit-replacestate'?: true | false | '' | undefined | null;
96
+ 'data-sveltekit-reset'?: true | false | '' | undefined | null;
71
97
  }
72
98
  }
73
99
  `;
@@ -143,6 +169,15 @@ function generate_app_types(manifest_data, config, dir) {
143
169
  /** @type {Set<string>} */
144
170
  const pathnames = new Set();
145
171
 
172
+ /** @type {string[]} */
173
+ const page_route_ids = [];
174
+
175
+ /** @type {string[]} */
176
+ const endpoint_route_ids = [];
177
+
178
+ /** @type {string[]} */
179
+ const app_route_ids = [];
180
+
146
181
  /** @type {string[]} */
147
182
  const dynamic_routes = [];
148
183
 
@@ -151,15 +186,17 @@ function generate_app_types(manifest_data, config, dir) {
151
186
 
152
187
  /** @type {Map<string, Map<string, { optional: boolean, matchers: Set<string> | null }>>} */
153
188
  const layout_params_by_route = new Map(
154
- manifest_data.routes.map((route) => [
155
- route.id,
156
- new Map(
157
- route.params.map((p) => [
158
- p.name,
159
- { optional: p.optional, matchers: p.matcher ? new Set([p.matcher]) : null }
160
- ])
161
- )
162
- ])
189
+ manifest_data.routes
190
+ .filter((route) => route.layout)
191
+ .map((route) => [
192
+ route.id,
193
+ new Map(
194
+ route.params.map((p) => [
195
+ p.name,
196
+ { optional: p.optional, matchers: p.matcher ? new Set([p.matcher]) : null }
197
+ ])
198
+ )
199
+ ])
163
200
  );
164
201
 
165
202
  for (const route of manifest_data.routes) {
@@ -193,8 +230,11 @@ function generate_app_types(manifest_data, config, dir) {
193
230
  }
194
231
 
195
232
  for (const route of manifest_data.routes) {
196
- const pathname = remove_group_segments(route.id);
197
- let normalized_pathname = pathname.slice(1);
233
+ const id = s(route.id);
234
+
235
+ if (is_page_route(route)) page_route_ids.push(id);
236
+ if (is_endpoint_route(route)) endpoint_route_ids.push(id);
237
+ if (is_app_route(route)) app_route_ids.push(id);
198
238
 
199
239
  /** @type {(path: string) => string} */
200
240
  let serialise = s;
@@ -204,16 +244,18 @@ function generate_app_types(manifest_data, config, dir) {
204
244
  const type = get_matcher_type(p.matcher);
205
245
  return `${/^\w+$/.test(p.name) ? p.name : `'${p.name}'`}${p.optional ? '?:' : ':'} ${type}${p.optional ? ' | undefined' : ''}`;
206
246
  });
207
- const route_type = `${s(route.id)}: { ${params.join('; ')} }`;
208
247
 
209
- dynamic_routes.push(route_type);
248
+ if (is_app_route(route)) {
249
+ dynamic_routes.push(`${s(route.id)}: { ${params.join('; ')} }`);
250
+ }
210
251
 
211
- normalized_pathname = replace_required_params(replace_optional_params(pathname)).slice(1);
212
252
  serialise = (p) => `\`${p}\` & {}`;
213
253
  }
214
254
 
215
- for (const p of get_pathnames_for_trailing_slash(normalized_pathname, route)) {
216
- pathnames.add(serialise(p));
255
+ for (const pathname of get_pathname_patterns(route.id)) {
256
+ for (const p of get_pathnames_for_trailing_slash(pathname, route)) {
257
+ pathnames.add(serialise(p));
258
+ }
217
259
  }
218
260
 
219
261
  let layout_type = 'Record<string, never>';
@@ -230,7 +272,7 @@ function generate_app_types(manifest_data, config, dir) {
230
272
  if (params.length > 0) layout_type = `{ ${params} }`;
231
273
  }
232
274
 
233
- layouts.push(`${s(route.id)}: ${layout_type}`);
275
+ if (route.layout) layouts.push(`${s(route.id)}: ${layout_type}`);
234
276
  }
235
277
 
236
278
  const assets = manifest_data.assets.map((asset) => s(asset.file));
@@ -238,7 +280,9 @@ function generate_app_types(manifest_data, config, dir) {
238
280
  return [
239
281
  'declare module "$app/types" {',
240
282
  '\texport interface AppTypes {',
241
- `\t\tRouteId(): ${manifest_data.routes.map((r) => s(r.id)).join(' | ')};`,
283
+ `\t\tPageRouteId(): ${page_route_ids.join(' | ') || 'never'};`,
284
+ `\t\tEndpointRouteId(): ${endpoint_route_ids.join(' | ') || 'never'};`,
285
+ `\t\tRouteId(): ${app_route_ids.join(' | ') || 'never'};`,
242
286
  `\t\tRouteParams(): {\n\t\t\t${dynamic_routes.join(';\n\t\t\t')}\n\t\t};`,
243
287
  `\t\tLayoutParams(): {\n\t\t\t${layouts.join(';\n\t\t\t')}\n\t\t};`,
244
288
  `\t\tPath(): ${Array.from(pathnames).join(' | ')};`,
@@ -2,7 +2,6 @@
2
2
  import process from 'node:process';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import ts from 'typescript';
6
5
  import { styleText } from 'node:util';
7
6
  import { write_if_changed } from '../utils.js';
8
7
  import {
@@ -15,6 +14,14 @@ import {
15
14
  validate_resolved_config
16
15
  } from './utils.js';
17
16
 
17
+ /** @type {import('typescript')} */
18
+ let ts;
19
+ try {
20
+ ts = await import('typescript');
21
+ } catch {
22
+ // The user has not installed TypeScript.
23
+ }
24
+
18
25
  /**
19
26
  * Generates the tsconfig that the user's tsconfig inherits from.
20
27
  * @param {import('types').ValidatedKitConfig} kit
@@ -78,13 +85,19 @@ export function write_tsconfig(kit, root) {
78
85
  * @param {ValidatedKitConfig['typescript']['config']} [transform] TODO get rid of this
79
86
  */
80
87
  function write_parent_tsconfig(root, dir, id, config, example, transform) {
81
- const out_file = path.join(root, `node_modules/${id}/tsconfig.json`);
88
+ // simplified tsconfig resolvers (e.g. Playwright's) only find `${id}.json`, not `${id}/tsconfig.json`
89
+ const out_file = path.join(root, `node_modules/${id}.json`);
82
90
 
83
91
  let normalized = normalize_config(out_file, config);
84
92
  normalized = transform?.(normalized) ?? normalized;
85
93
 
86
94
  write_if_changed(out_file, JSON.stringify(normalized, null, '\t'));
87
95
 
96
+ if (!ts) {
97
+ // The user has not installed TypeScript. Skip validation of config.
98
+ return;
99
+ }
100
+
88
101
  const user_config = load_user_tsconfig(dir);
89
102
 
90
103
  if (user_config && modified_since_last_check(user_config.file)) {
@@ -5,6 +5,7 @@ import { rimraf, walk, resolve_entry } from '../../../utils/filesystem.js';
5
5
  import { compact } from '../../../utils/array.js';
6
6
  import { posixify } from '../../../utils/os.js';
7
7
  import { ts } from '../ts.js';
8
+ import { is_page_route } from '../create_manifest_data/index.js';
8
9
  const remove_relative_parent_traversals = (/** @type {string} */ path) =>
9
10
  path.replace(/\.\.\//g, '');
10
11
  const is_whitespace = (/** @type {string} */ char) => /\s/.test(char);
@@ -278,7 +279,9 @@ function update_types(config, routes, route, root, to_delete = new Set()) {
278
279
  let all_pages_have_load = true;
279
280
  /** @type {import('types').RouteParam[]} */
280
281
  const layout_params = [];
281
- const ids = ['RouteId'];
282
+ // a layout can live in a directory that has no `+page` of its own, in which case its own id
283
+ // is never the matched route — a request to a colocated `+server` doesn't execute layouts
284
+ const ids = is_page_route(route) ? ['RouteId'] : [];
282
285
 
283
286
  route.layout.child_pages?.forEach((page) => {
284
287
  const leaf = routes.get(page);
@@ -312,7 +315,7 @@ function update_types(config, routes, route, root, to_delete = new Set()) {
312
315
  ids.push('null');
313
316
  }
314
317
 
315
- declarations.push(`type LayoutRouteId = ${ids.join(' | ')}`);
318
+ declarations.push(`type LayoutRouteId = ${ids.join(' | ') || 'never'}`);
316
319
 
317
320
  declarations.push(
318
321
  'type LayoutParams = RouteParams & ' + generate_params_type(layout_params, outdir, config)
@@ -27,10 +27,10 @@ export { defineParams } from './params.js';
27
27
  * return an error response without invoking `handleError`.
28
28
  * Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
29
29
  * @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
30
- * @param {Omit<App.Error, 'status'> & { status?: App.Error['status'] }} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
30
+ * @param {string} [message] The error message.
31
31
  * @overload
32
- * @param {number} status
33
- * @param {Omit<App.Error, 'status'> & { status?: App.Error['status'] }} body
32
+ * @param {{ status: number; message: string } extends App.Error ? number : never} status
33
+ * @param {{ status: number; message: string } extends App.Error ? string : never} [message]
34
34
  * @return {never}
35
35
  * @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
36
36
  * @throws {Error} If the provided status is invalid (not between 400 and 599).
@@ -41,10 +41,12 @@ export { defineParams } from './params.js';
41
41
  * return an error response without invoking `handleError`.
42
42
  * Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
43
43
  * @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
44
- * @param {{ status: number; message: string } extends App.Error ? string | void | undefined : never} body The error message.
44
+ * @param {string} message The error message.
45
+ * @param {{ status: number; message: string } extends App.Error ? never : Omit<App.Error, 'status' | 'message'>} properties Additional properties of the App.Error type.
45
46
  * @overload
46
47
  * @param {number} status
47
- * @param {{ status: number; message: string } extends App.Error ? string | void | undefined : never} body
48
+ * @param {string} message
49
+ * @param {{ status: number; message: string } extends App.Error ? never : Omit<App.Error, 'status' | 'message'>} properties
48
50
  * @return {never}
49
51
  * @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
50
52
  * @throws {Error} If the provided status is invalid (not between 400 and 599).
@@ -54,13 +56,12 @@ export { defineParams } from './params.js';
54
56
  * When called during request handling, this will cause SvelteKit to
55
57
  * return an error response without invoking `handleError`.
56
58
  * Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
59
+ * @deprecated Passing an `App.Error` body as the second argument is deprecated — pass the `message` as the second argument, and any additional properties as the third
57
60
  * @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
58
- * @param {string} body The error message.
59
- * @param {{ status: number; message: string } extends App.Error ? never : Omit<App.Error, 'status' | 'message'>} properties Additional properties of the App.Error type.
61
+ * @param {Omit<App.Error, 'status'> & { status?: App.Error['status'] }} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
60
62
  * @overload
61
63
  * @param {number} status
62
- * @param {string} body
63
- * @param {{ status: number; message: string } extends App.Error ? never : Omit<App.Error, 'status' | 'message'>} properties
64
+ * @param {Omit<App.Error, 'status'> & { status?: App.Error['status'] }} properties
64
65
  * @return {never}
65
66
  * @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
66
67
  * @throws {Error} If the provided status is invalid (not between 400 and 599).
@@ -71,18 +72,28 @@ export { defineParams } from './params.js';
71
72
  * return an error response without invoking `handleError`.
72
73
  * Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
73
74
  * @param {any} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
74
- * @param {any} [body] An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
75
+ * @param {any} [message] A string, or (deprecated) a partial App.Error object
75
76
  * @param {any} [properties] Additional properties of the App.Error type when passing a string message.
76
77
  * @return {never}
77
78
  * @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
78
79
  * @throws {Error} If the provided status is invalid (not between 400 and 599).
79
80
  */
80
- export function error(status, body, properties) {
81
+ export function error(status, message, properties) {
81
82
  if ((!BROWSER || DEV) && (isNaN(status) || status < 400 || status > 599)) {
82
83
  throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
83
84
  }
84
85
 
85
- throw new HttpError(status, body, properties);
86
+ if (message !== undefined && typeof message !== 'string') {
87
+ if (DEV) {
88
+ console.warn(
89
+ 'Passing an `App.Error` body as the second argument is deprecated — pass the `message` as the second argument, and any additional properties as the third'
90
+ );
91
+ }
92
+
93
+ ({ message, ...properties } = message);
94
+ }
95
+
96
+ throw new HttpError({ ...properties, status, message: message ?? `Error: ${status}` });
86
97
  }
87
98
 
88
99
  /**
@@ -2,19 +2,11 @@
2
2
 
3
3
  export class HttpError {
4
4
  /**
5
- * @param {number} status
6
- * @param {Omit<App.Error, 'status'> | string | undefined} body
7
- * @param {Omit<App.Error, 'status' | 'message'>} [properties]
5
+ * @param {App.Error} error
8
6
  */
9
- constructor(status, body, properties) {
10
- this.status = status;
11
- if (typeof body === 'string') {
12
- this.body = { ...properties, message: body, status };
13
- } else if (body) {
14
- this.body = { ...body, status };
15
- } else {
16
- this.body = { message: `Error: ${status}`, status };
17
- }
7
+ constructor(error) {
8
+ this.status = error.status;
9
+ this.body = error;
18
10
  }
19
11
 
20
12
  toString() {
@@ -137,14 +137,9 @@ export function getRequest({ request, base, bodySizeLimit }) {
137
137
  }
138
138
 
139
139
  const controller = new AbortController();
140
- // TODO: Whenever Node >=22.17 is the minimum supported version, we can do `if (request.readableAborted) controller.abort()` instead
141
- // see https://github.com/nodejs/node/blob/5cf3c3e24c7257a0c6192ed8ef71efec8ddac22b/lib/internal/streams/readable.js#L1443-L1453
142
- let errored = false;
143
- let end_emitted = false;
144
- request.once('error', () => (errored = true));
145
- request.once('end', () => (end_emitted = true));
140
+
146
141
  request.once('close', () => {
147
- if ((errored || request.destroyed) && !end_emitted) {
142
+ if (request.readableAborted) {
148
143
  controller.abort();
149
144
  }
150
145
  });