@sveltejs/kit 3.0.0-next.4 → 3.0.0-next.7

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 (91) hide show
  1. package/package.json +23 -18
  2. package/src/core/adapt/builder.js +29 -43
  3. package/src/core/adapt/index.js +1 -4
  4. package/src/core/config/index.js +135 -71
  5. package/src/core/config/options.js +291 -244
  6. package/src/core/config/types.d.ts +1 -1
  7. package/src/core/env.js +120 -5
  8. package/src/core/generate_manifest/index.js +12 -15
  9. package/src/core/postbuild/analyse.js +8 -12
  10. package/src/core/postbuild/crawl.js +22 -6
  11. package/src/core/postbuild/prerender.js +40 -23
  12. package/src/core/sync/create_manifest_data/index.js +29 -49
  13. package/src/core/sync/write_client_manifest.js +14 -14
  14. package/src/core/sync/write_non_ambient.js +10 -7
  15. package/src/core/sync/write_root.js +9 -30
  16. package/src/core/sync/write_server.js +13 -11
  17. package/src/core/sync/write_tsconfig.js +2 -4
  18. package/src/core/sync/write_types/index.js +11 -10
  19. package/src/exports/index.js +34 -12
  20. package/src/exports/internal/client.js +5 -0
  21. package/src/exports/internal/env.js +1 -1
  22. package/src/exports/internal/index.js +1 -90
  23. package/src/exports/internal/{event.js → server/event.js} +1 -2
  24. package/src/exports/internal/server/index.js +33 -0
  25. package/src/exports/internal/shared.js +89 -0
  26. package/src/exports/node/index.js +58 -7
  27. package/src/exports/params.js +63 -0
  28. package/src/exports/public.d.ts +299 -145
  29. package/src/exports/url.js +84 -0
  30. package/src/exports/vite/build/build_server.js +6 -1
  31. package/src/exports/vite/dev/index.js +38 -37
  32. package/src/exports/vite/index.js +498 -353
  33. package/src/exports/vite/preview/index.js +16 -8
  34. package/src/exports/vite/utils.js +7 -11
  35. package/src/runtime/app/env/types.d.ts +1 -1
  36. package/src/runtime/app/forms.js +22 -5
  37. package/src/runtime/app/paths/client.js +4 -10
  38. package/src/runtime/app/paths/public.d.ts +0 -28
  39. package/src/runtime/app/paths/server.js +8 -4
  40. package/src/runtime/app/server/remote/form.js +10 -3
  41. package/src/runtime/app/server/remote/query.js +9 -18
  42. package/src/runtime/app/server/remote/requested.js +8 -4
  43. package/src/runtime/app/server/remote/shared.js +5 -7
  44. package/src/runtime/app/state/client.js +1 -2
  45. package/src/runtime/app/stores.js +13 -76
  46. package/src/runtime/client/client.js +205 -167
  47. package/src/runtime/client/fetcher.js +3 -2
  48. package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
  49. package/src/runtime/client/remote-functions/form.svelte.js +122 -31
  50. package/src/runtime/client/remote-functions/prerender.svelte.js +17 -4
  51. package/src/runtime/client/remote-functions/query/index.js +3 -2
  52. package/src/runtime/client/remote-functions/query/instance.svelte.js +27 -11
  53. package/src/runtime/client/remote-functions/query-batch.svelte.js +4 -3
  54. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +42 -15
  55. package/src/runtime/client/remote-functions/shared.svelte.js +18 -9
  56. package/src/runtime/client/state.svelte.js +66 -49
  57. package/src/runtime/client/types.d.ts +10 -2
  58. package/src/runtime/client/utils.js +0 -96
  59. package/src/runtime/form-utils.js +99 -18
  60. package/src/runtime/server/cookie.js +22 -33
  61. package/src/runtime/server/csrf.js +65 -0
  62. package/src/runtime/server/data/index.js +7 -7
  63. package/src/runtime/server/env_module.js +0 -5
  64. package/src/runtime/server/index.js +2 -2
  65. package/src/runtime/server/page/actions.js +41 -26
  66. package/src/runtime/server/page/index.js +2 -1
  67. package/src/runtime/server/page/load_data.js +1 -1
  68. package/src/runtime/server/page/render.js +36 -47
  69. package/src/runtime/server/page/respond_with_error.js +7 -8
  70. package/src/runtime/server/page/server_routing.js +13 -9
  71. package/src/runtime/server/remote.js +85 -39
  72. package/src/runtime/server/respond.js +92 -58
  73. package/src/runtime/server/utils.js +7 -7
  74. package/src/runtime/telemetry/otel.js +1 -1
  75. package/src/types/ambient.d.ts +10 -5
  76. package/src/types/global-private.d.ts +4 -4
  77. package/src/types/internal.d.ts +17 -20
  78. package/src/types/private.d.ts +33 -1
  79. package/src/types/synthetic/$lib.md +1 -1
  80. package/src/utils/error.js +12 -4
  81. package/src/utils/mime.js +9 -0
  82. package/src/utils/params.js +66 -0
  83. package/src/utils/routing.js +84 -38
  84. package/src/utils/shared-iterator.js +5 -0
  85. package/src/utils/streaming.js +14 -4
  86. package/src/utils/url.js +20 -2
  87. package/src/version.js +1 -1
  88. package/types/index.d.ts +434 -269
  89. package/types/index.d.ts.map +19 -15
  90. package/src/exports/internal/server.js +0 -22
  91. /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
package/src/core/env.js CHANGED
@@ -51,6 +51,10 @@ export async function load_explicit_env(kit, file, root, mode) {
51
51
  /** @type {Record<string, EnvVarConfig<any>>} */
52
52
  let variables;
53
53
 
54
+ /** @type {import('../runtime/app/env/internal.js')} */ (
55
+ await server.ssrLoadModule(`${runtime_directory}/app/env/internal.js`)
56
+ ).set_building();
57
+
54
58
  try {
55
59
  ({ variables } = await server.ssrLoadModule(file));
56
60
 
@@ -87,11 +91,12 @@ export async function load_explicit_env(kit, file, root, mode) {
87
91
 
88
92
  /**
89
93
  * Creates the `__sveltekit/env` module
90
- * @param {Record<string, EnvVarConfig<any>> | null} variables
94
+ * @param {Record<string, EnvVarConfig<any> | undefined> | null} variables
91
95
  * @param {Record<string, string>} env
92
96
  * @param {string | null} entry
97
+ * @param {boolean} is_dev
93
98
  */
94
- export function create_sveltekit_env(variables, env, entry) {
99
+ export function create_sveltekit_env(variables, env, entry, is_dev) {
95
100
  const imports = entry
96
101
  ? [
97
102
  `import { variables } from ${JSON.stringify(entry)};`,
@@ -106,7 +111,7 @@ export function create_sveltekit_env(variables, env, entry) {
106
111
  const issues = {};
107
112
 
108
113
  for (const [name, config] of Object.entries(variables ?? {})) {
109
- if (config.static) {
114
+ if (config?.static) {
110
115
  if (config.public) {
111
116
  const value = validate(variables ?? {}, env[name], name, issues);
112
117
  declarations.push(`explicit_public_env.${name} = ${devalue.uneval(value)};`);
@@ -116,7 +121,7 @@ export function create_sveltekit_env(variables, env, entry) {
116
121
  `const ${name} = validate(variables, env.${name}, ${JSON.stringify(name)}, issues);`
117
122
  );
118
123
 
119
- if (config.public) {
124
+ if (config?.public) {
120
125
  setters.push(`explicit_public_env.${name} = ${name};`);
121
126
  setters.push(`rendered_env.${name} = ${name};`);
122
127
  } else {
@@ -145,6 +150,19 @@ export function create_sveltekit_env(variables, env, entry) {
145
150
  }`
146
151
  ];
147
152
 
153
+ // In dev, initialise the env immediately. Tools like `vite-node` load modules
154
+ // through the Vite config but don't run the SvelteKit dev server, which is what
155
+ // normally calls `set_env`. Without this, dynamic env vars imported from
156
+ // `$app/env/public` and `$app/env/private` would be `undefined` in such contexts.
157
+ if (is_dev) {
158
+ /** @type {Record<string, string>} */
159
+ const dev_env = {};
160
+ for (const name of Object.keys(variables ?? {})) {
161
+ if (name in env) dev_env[name] = env[name];
162
+ }
163
+ blocks.push(`set_env(${devalue.uneval(dev_env)});`);
164
+ }
165
+
148
166
  const module = blocks.join('\n\n');
149
167
 
150
168
  return module;
@@ -213,9 +231,36 @@ export function create_sveltekit_env_public(variables, env, prelude) {
213
231
  return `${prelude}\n\n${exports.join('')}`;
214
232
  }
215
233
 
234
+ /**
235
+ * Creates the `__sveltekit/env/service-worker` module used in production. When an app uses
236
+ * dynamic public env vars, they're loaded at runtime via an import of the prerendered
237
+ * `env.js`. If there are none, values are inlined.
238
+ * @param {Record<string, EnvVarConfig<any>> | null} variables
239
+ * @param {Record<string, string>} env
240
+ * @param {string} global
241
+ * @param {string} base
242
+ * @param {string} app_dir
243
+ */
244
+ export function create_sveltekit_env_service_worker(variables, env, global, base, app_dir) {
245
+ const has_dynamic_public_env = Object.values(variables ?? {}).some(
246
+ (config) => config.public && !config.static
247
+ );
248
+
249
+ if (!has_dynamic_public_env) {
250
+ return create_sveltekit_env_service_worker_dev(variables, env, global);
251
+ }
252
+
253
+ return dedent`
254
+ import { env } from '${base}/${app_dir}/env.js';
255
+
256
+ globalThis.__SVELTEKIT_EXPERIMENTAL_EXPLICIT_ENVIRONMENT_VARIABLES__ = true;
257
+
258
+ ${global} = { env };
259
+ `;
260
+ }
261
+
216
262
  /**
217
263
  * Creates the `__sveltekit/env/service-worker` module used in development
218
- * (but not in prod, which goes through build_service_worker instead)
219
264
  * @param {Record<string, EnvVarConfig<any>> | null} variables
220
265
  * @param {Record<string, string>} env
221
266
  * @param {string} global
@@ -330,3 +375,73 @@ export const reserved = new Set([
330
375
  ]);
331
376
 
332
377
  export const valid_identifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
378
+
379
+ /**
380
+ * Generates `export const` declarations (and, for reserved-word names that need
381
+ * aliasing, `const` + re-export specifiers) for a set of named exports.
382
+ *
383
+ * For regular names, emits a single efficient `export const name = expr;` statement.
384
+ * For reserved-word names (e.g. `delete`, `class`), emits `const alias = expr;` plus
385
+ * a re-export specifier (`alias as name`), since reserved words can't be `const`
386
+ * binding names but CAN appear in export specifiers.
387
+ *
388
+ * You can do evil things like `export { c as class }`. In order to import/re-export
389
+ * these, you need to alias the binding, then un-alias it when re-exporting:
390
+ *
391
+ * const _0 = ...; // safe binding name
392
+ * export { _0 as class }; // valid — `class` is allowed in export specifiers
393
+ *
394
+ * Aliases are chosen to avoid collisions with any of the supplied names. The
395
+ * namespace binding (used to hold the imported module) is likewise chosen to
396
+ * avoid collisions.
397
+ *
398
+ * @param {Iterable<string>} names — the export names
399
+ * @param {(name: string, namespace: string) => string} build_expression —
400
+ * called for each name to produce the right-hand side of the declaration;
401
+ * receives the chosen namespace binding so it can reference the imported module
402
+ * @param {string} namespace_prefix — the preferred binding name for the namespace
403
+ * (suffixed with a number if it collides with any export name)
404
+ * @returns {{ namespace: string, declarations: string[], reexports: string[] }}
405
+ */
406
+ export function create_exported_declarations(names, build_expression, namespace_prefix) {
407
+ /** @type {Set<string>} */
408
+ const set = new Set(names);
409
+
410
+ let namespace = namespace_prefix;
411
+ let namespace_index = 0;
412
+ while (set.has(namespace)) {
413
+ namespace = `${namespace_prefix}${namespace_index++}`;
414
+ }
415
+
416
+ let alias_index = 0;
417
+ /** @type {Map<string, string>} */
418
+ const aliases = new Map();
419
+
420
+ for (const name of set) {
421
+ if (!reserved.has(name)) continue;
422
+
423
+ let alias = `_${alias_index++}`;
424
+ while (set.has(alias)) {
425
+ alias = `_${alias_index++}`;
426
+ }
427
+ aliases.set(name, alias);
428
+ }
429
+
430
+ /** @type {string[]} */
431
+ const declarations = [];
432
+ /** @type {string[]} */
433
+ const reexports = [];
434
+
435
+ for (const name of set) {
436
+ const alias = aliases.get(name);
437
+ const expr = build_expression(name, namespace);
438
+ if (alias) {
439
+ declarations.push(`const ${alias} = ${expr};`);
440
+ reexports.push(`${alias} as ${name}`);
441
+ } else {
442
+ declarations.push(`export const ${name} = ${expr};`);
443
+ }
444
+ }
445
+
446
+ return { namespace, declarations, reexports };
447
+ }
@@ -1,7 +1,7 @@
1
1
  /** @import { RemoteChunk } from 'types' */
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import * as mime from 'mrmime';
4
+ import { lookup as mime_lookup } from '../../utils/mime.js';
5
5
  import { s } from '../../utils/misc.js';
6
6
  import { get_mime_lookup } from '../utils.js';
7
7
  import { resolve_symlinks } from '../../exports/vite/build/utils.js';
@@ -69,10 +69,8 @@ export function generate_manifest({
69
69
  assets.push(build_data.service_worker);
70
70
  }
71
71
 
72
- // In case of server-side route resolution, we need to include all matchers. Prerendered routes are not part
73
- // of the server manifest, and they could reference matchers that then would not be included.
74
- const matchers = new Set(
75
- build_data.client?.nodes ? Object.keys(build_data.manifest_data.matchers) : undefined
72
+ const uses_matchers = build_data.manifest_data.routes.some((route) =>
73
+ route.params.some((param) => param.matcher)
76
74
  );
77
75
 
78
76
  /** @param {Array<number | undefined>} indexes */
@@ -93,7 +91,7 @@ export function generate_manifest({
93
91
  files[file] = fs.statSync(path.resolve(build_data.out_dir, 'server', file)).size;
94
92
 
95
93
  const ext = path.extname(file);
96
- mime_types[ext] ??= mime.lookup(ext) || '';
94
+ mime_types[ext] ??= mime_lookup(ext) || '';
97
95
  }
98
96
 
99
97
  // prettier-ignore
@@ -117,10 +115,6 @@ export function generate_manifest({
117
115
  ${routes.map(route => {
118
116
  if (!route.page && !route.endpoint) return;
119
117
 
120
- route.params.forEach(param => {
121
- if (param.matcher) matchers.add(param.matcher);
122
- });
123
-
124
118
  return dedent`
125
119
  {
126
120
  id: ${s(route.id)},
@@ -134,11 +128,14 @@ export function generate_manifest({
134
128
  ],
135
129
  prerendered_routes: new Set(${s(prerendered)}),
136
130
  matchers: async () => {
137
- ${Array.from(
138
- matchers,
139
- type => `const { match: ${type} } = await import ('${(join_relative(relative_path, `/entries/matchers/${type}.js`))}')`
140
- ).join('\n')}
141
- return { ${Array.from(matchers).join(', ')} };
131
+ ${
132
+ uses_matchers && build_data.manifest_data.params
133
+ ? dedent`
134
+ const { params } = await import('${join_relative(relative_path, '/entries/params.js')}');
135
+ return params;
136
+ `
137
+ : 'return {};'
138
+ }
142
139
  },
143
140
  server_assets: ${s(files)}
144
141
  }
@@ -1,4 +1,3 @@
1
- /** @import { Adapter } from '@sveltejs/kit' */
2
1
  /** @import { RemoteChunk } from 'types' */
3
2
  import { join } from 'node:path';
4
3
  import { pathToFileURL } from 'node:url';
@@ -39,15 +38,7 @@ async function analyse({
39
38
  const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
40
39
 
41
40
  const vite_config = await load_vite_config(vite_config_file);
42
-
43
41
  const config = extract_svelte_config(vite_config).kit;
44
-
45
- // TODO i think this can just be config.adapter?
46
- /** @type {Adapter | undefined} */
47
- const adapter = vite_config.plugins.find(
48
- (plugin) => plugin.name === 'vite-plugin-sveltekit-adapter'
49
- )?.api?.adapter;
50
-
51
42
  const server_root = join(config.outDir, 'output');
52
43
 
53
44
  /** @type {import('types').ServerInternalModule} */
@@ -57,11 +48,16 @@ async function analyse({
57
48
  // essential we do this before analysing the code
58
49
  internal.set_building();
59
50
 
60
- // set env, `read`, and `manifest`, in case they're used in initialisation
61
- internal.set_env(env);
51
+ // set `read` and `manifest`, in case they're used in initialisation
62
52
  internal.set_manifest(manifest);
63
53
  internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`));
64
54
 
55
+ // `set_env` lives in a separate module that imports the user's `src/env` config. We import it
56
+ // *after* `set_building()` so that `building`-dependent expressions resolve correctly
57
+ /** @type {import('__sveltekit/env')} */
58
+ const { set_env } = await import(pathToFileURL(`${server_root}/server/env.js`).href);
59
+ set_env(env);
60
+
65
61
  /** @type {import('types').ServerMetadata} */
66
62
  const metadata = {
67
63
  nodes: [],
@@ -126,7 +122,7 @@ async function analyse({
126
122
  server_manifest,
127
123
  tracked_features
128
124
  )) {
129
- check_feature(route.id, route_config, feature, adapter);
125
+ check_feature(route.id, route_config, feature, config.adapter);
130
126
  }
131
127
  }
132
128
 
@@ -38,6 +38,18 @@ export function crawl(html, base) {
38
38
  /** @type {string[]} */
39
39
  const hrefs = [];
40
40
 
41
+ /** @type {string[]} */
42
+ const invalid = [];
43
+
44
+ /** @param {string} url */
45
+ const push_href = (url) => {
46
+ try {
47
+ hrefs.push(resolve(base, url));
48
+ } catch {
49
+ invalid.push(url);
50
+ }
51
+ };
52
+
41
53
  let i = 0;
42
54
  main: while (i < html.length) {
43
55
  const char = html[i];
@@ -186,9 +198,13 @@ export function crawl(html, base) {
186
198
 
187
199
  if (href) {
188
200
  if (tag === 'BASE') {
189
- base = resolve(base, href);
201
+ try {
202
+ base = resolve(base, href);
203
+ } catch {
204
+ invalid.push(href);
205
+ }
190
206
  } else if (!rel || !/\bexternal\b/i.test(rel)) {
191
- hrefs.push(resolve(base, href));
207
+ push_href(href);
192
208
  }
193
209
  }
194
210
 
@@ -201,7 +217,7 @@ export function crawl(html, base) {
201
217
  }
202
218
 
203
219
  if (src) {
204
- hrefs.push(resolve(base, src));
220
+ push_href(src);
205
221
  }
206
222
 
207
223
  if (srcset) {
@@ -222,7 +238,7 @@ export function crawl(html, base) {
222
238
  candidates.push(value);
223
239
  for (const candidate of candidates) {
224
240
  const src = candidate.split(WHITESPACE)[0];
225
- if (src) hrefs.push(resolve(base, src));
241
+ if (src) push_href(src);
226
242
  }
227
243
  }
228
244
 
@@ -230,7 +246,7 @@ export function crawl(html, base) {
230
246
  const attr = name ?? property;
231
247
 
232
248
  if (attr && CRAWLABLE_META_NAME_ATTRS.has(attr)) {
233
- hrefs.push(resolve(base, content));
249
+ push_href(content);
234
250
  }
235
251
  }
236
252
  }
@@ -239,5 +255,5 @@ export function crawl(html, base) {
239
255
  i += 1;
240
256
  }
241
257
 
242
- return { ids, hrefs };
258
+ return { ids, hrefs, invalid };
243
259
  }
@@ -1,4 +1,3 @@
1
- /** @import { Adapter } from '@sveltejs/kit' */
2
1
  import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
2
  import { dirname, join } from 'node:path';
4
3
  import { pathToFileURL } from 'node:url';
@@ -44,14 +43,20 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
44
43
  /** @type {import('types').ServerInternalModule} */
45
44
  const internal = await import(pathToFileURL(`${out}/server/internal.js`).href);
46
45
 
47
- /** @type {import('types').ServerModule} */
48
- const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href);
49
-
50
46
  // configure `import { building } from `$app/env` —
51
47
  // essential we do this before analysing the code
52
48
  internal.set_building();
53
49
  internal.set_prerendering();
54
50
 
51
+ // `set_env` and `Server` live in modules that import the user's `src/env` config. We import them
52
+ // *after* `set_building()` so that `building`-dependent expressions resolve correctly
53
+ /** @type {import('__sveltekit/env')} */
54
+ const { set_env } = await import(pathToFileURL(`${out}/server/env.js`).href);
55
+ set_env(env);
56
+
57
+ /** @type {import('types').ServerModule} */
58
+ const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href);
59
+
55
60
  /**
56
61
  * @template {{message: string}} T
57
62
  * @template {Omit<T, 'message'>} K
@@ -105,12 +110,14 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
105
110
 
106
111
  const config = extract_svelte_config(vite_config).kit;
107
112
 
113
+ const prerender_origin = config.paths.origin || 'http://sveltekit-prerender';
114
+
108
115
  if (hash) {
109
116
  const fallback = await generate_fallback({
110
117
  manifest_path,
111
118
  env,
112
119
  out_dir: config.outDir,
113
- origin: config.prerender.origin,
120
+ origin: prerender_origin,
114
121
  assets: config.files.assets
115
122
  });
116
123
 
@@ -125,13 +132,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
125
132
  return { prerendered, prerender_map };
126
133
  }
127
134
 
128
- // TODO this can just be config.adapter?
129
- /** @type {Adapter | undefined} */
130
- const adapter = vite_config.plugins.find(
131
- (plugin) => plugin.name === 'vite-plugin-sveltekit-adapter'
132
- )?.api?.adapter;
133
-
134
- const emulator = await adapter?.emulate?.();
135
+ const emulator = await config.adapter?.emulate?.();
135
136
 
136
137
  /** @type {import('types').Logger} */
137
138
  const log = logger({ verbose });
@@ -145,7 +146,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
145
146
  ({ status, path, referrer, referenceType }) => {
146
147
  const message =
147
148
  status === 404 && !path.startsWith(config.paths.base)
148
- ? `${path} does not begin with \`base\`, which is configured in \`paths.base\` and can be imported from \`$app/paths\` - see https://svelte.dev/docs/kit/configuration#paths for more info`
149
+ ? `${path} does not begin with \`base\`. You can fix this by using \`resolve('${path}')\` from \`$app/paths\`. The base path is configurable from \`paths.base\` - see https://svelte.dev/docs/kit/configuration#paths for more info`
149
150
  : path;
150
151
 
151
152
  return `${status} ${message}${referrer ? ` (${referenceType} from ${referrer})` : ''}`;
@@ -180,6 +181,14 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
180
181
  }
181
182
  );
182
183
 
184
+ const handle_invalid_url = normalise_error_handler(
185
+ log,
186
+ config.prerender.handleInvalidUrl,
187
+ ({ href, referrer }) => {
188
+ return `Invalid URL ${href}${referrer ? ` (linked from ${referrer})` : ''}`;
189
+ }
190
+ );
191
+
183
192
  const q = queue(config.prerender.concurrency);
184
193
 
185
194
  /**
@@ -251,7 +260,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
251
260
  /** @type {Map<string, import('types').PrerenderDependency>} */
252
261
  const dependencies = new Map();
253
262
 
254
- const response = await server.respond(new Request(config.prerender.origin + encoded), {
263
+ const response = await server.respond(new Request(prerender_origin + encoded), {
255
264
  getClientAddress() {
256
265
  throw new Error('Cannot read clientAddress during prerendering');
257
266
  },
@@ -334,16 +343,20 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
334
343
 
335
344
  // if it's a 200 HTML response, crawl it. Skip error responses, as we don't save those
336
345
  if (response.ok && config.prerender.crawl && headers['content-type'] === 'text/html') {
337
- const { ids, hrefs } = crawl(body.toString(), decoded);
346
+ const { ids, hrefs, invalid } = crawl(body.toString(), decoded);
347
+
348
+ for (const href of invalid) {
349
+ handle_invalid_url({ href, referrer: decoded });
350
+ }
338
351
 
339
352
  actual_hashlinks.set(decoded, ids);
340
353
 
341
354
  /** @param {string} href */
342
355
  const removePrerenderOrigin = (href) => {
343
- if (href.startsWith(config.prerender.origin)) {
344
- if (href === config.prerender.origin) return '/';
345
- if (href.at(config.prerender.origin.length) !== '/') return href;
346
- return href.slice(config.prerender.origin.length);
356
+ if (href.startsWith(prerender_origin)) {
357
+ if (href === prerender_origin) return '/';
358
+ if (href.at(prerender_origin.length) !== '/') return href;
359
+ return href.slice(prerender_origin.length);
347
360
  }
348
361
  return href;
349
362
  };
@@ -388,6 +401,12 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
388
401
  const type = headers['content-type'];
389
402
  const is_html = response_type === REDIRECT || type === 'text/html';
390
403
 
404
+ if (!is_html && response.status === 200 && decoded.slice(config.paths.base.length + 1) === '') {
405
+ throw new Error(
406
+ `Cannot prerender a root +server.js that returns a non-HTML response - static hosts always serve an HTML file for \`${config.paths.base || '/'}\``
407
+ );
408
+ }
409
+
391
410
  const file = output_filename(decoded, is_html);
392
411
  const dest = `${config.outDir}/output/prerendered/${category}/${file}`;
393
412
 
@@ -497,10 +516,8 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
497
516
  }
498
517
  }
499
518
 
500
- // the user's remote function modules may reference environment variables,
501
- // `read` or the `manifest` at the top-level so we need to set them before
502
- // evaluating those modules to avoid potential runtime errors
503
- internal.set_env(env);
519
+ // the user's remote function modules may reference `read` or the `manifest` at the top-level
520
+ // so we need to set them before evaluating those modules to avoid potential runtime errors
504
521
  internal.set_manifest(manifest);
505
522
  internal.set_read_implementation((file) => createReadableStream(`${out}/server/${file}`));
506
523
 
@@ -1,4 +1,4 @@
1
- import { lookup } from 'mrmime';
1
+ import { lookup } from '../../../utils/mime.js';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { styleText } from 'node:util';
@@ -29,22 +29,13 @@ export default function create_manifest_data({
29
29
  }) {
30
30
  const assets = create_assets(config);
31
31
  const hooks = create_hooks(config, cwd);
32
- const matchers = create_matchers(config, cwd);
32
+ const params = resolve_params(config, cwd);
33
33
  const { nodes, routes } = create_routes_and_nodes(cwd, config, fallback);
34
34
 
35
- // validate matcher names used in parameterised routes
36
- for (const route of routes) {
37
- for (const param of route.params) {
38
- if (param.matcher && !matchers[param.matcher]) {
39
- throw new Error(`No matcher found for parameter '${param.matcher}' in route ${route.id}`);
40
- }
41
- }
42
- }
43
-
44
35
  return {
45
36
  assets,
46
37
  hooks,
47
- matchers,
38
+ params,
48
39
  nodes,
49
40
  routes
50
41
  };
@@ -82,38 +73,9 @@ function create_hooks(config, cwd) {
82
73
  * @param {import('types').ValidatedConfig} config
83
74
  * @param {string} cwd
84
75
  */
85
- function create_matchers(config, cwd) {
86
- const params_base = path.relative(cwd, config.kit.files.params);
87
-
88
- /** @type {Record<string, string>} */
89
- const matchers = {};
90
- if (fs.existsSync(config.kit.files.params)) {
91
- for (const file of fs.readdirSync(config.kit.files.params)) {
92
- const ext = path.extname(file);
93
- if (!config.kit.moduleExtensions.includes(ext)) continue;
94
- const type = file.slice(0, -ext.length);
95
-
96
- if (/^\w+$/.test(type)) {
97
- const matcher_file = path.join(params_base, file);
98
-
99
- // Disallow same matcher with different extensions
100
- if (matchers[type]) {
101
- throw new Error(`Duplicate matchers: ${matcher_file} and ${matchers[type]}`);
102
- } else {
103
- matchers[type] = matcher_file;
104
- }
105
- } else {
106
- // Allow for matcher test collocation
107
- if (type.endsWith('.test') || type.endsWith('.spec')) continue;
108
-
109
- throw new Error(
110
- `Matcher names can only have underscores and alphanumeric characters — "${file}" is invalid`
111
- );
112
- }
113
- }
114
- }
115
-
116
- return matchers;
76
+ function resolve_params(config, cwd) {
77
+ const params_file = resolve_entry(config.kit.files.params);
78
+ return params_file ? posixify(path.relative(cwd, params_file)) : null;
117
79
  }
118
80
 
119
81
  /**
@@ -221,10 +183,17 @@ function create_routes_and_nodes(cwd, config, fallback) {
221
183
 
222
184
  // We can't use withFileTypes because of a NodeJs bug which returns wrong results
223
185
  // with isDirectory() in case of symlinks: https://github.com/nodejs/node/issues/30646
224
- const files = fs.readdirSync(dir).map((name) => ({
225
- is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
226
- name
227
- }));
186
+ // We sort the entries because `readdirSync` order is not guaranteed and differs
187
+ // between runtimes (e.g. Node returns entries alphabetically, Bun in directory
188
+ // order). Node indices are assigned from this traversal order, so without sorting
189
+ // the SSR and client manifests can disagree, causing hydration mismatches.
190
+ const files = fs
191
+ .readdirSync(dir)
192
+ .sort()
193
+ .map((name) => ({
194
+ is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
195
+ name
196
+ }));
228
197
 
229
198
  // process files first
230
199
  for (const file of files) {
@@ -368,7 +337,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
368
337
  const root = routes[0];
369
338
  if (!root.leaf && !root.error && !root.layout && !root.endpoint) {
370
339
  throw new Error(
371
- 'No routes found. If you are using a custom src/routes directory, make sure it is specified in your Svelte config file'
340
+ 'No routes found. If you are using a custom src/routes directory, make sure it is specified in your SvelteKit Vite plugin options'
372
341
  );
373
342
  }
374
343
  }
@@ -495,6 +464,17 @@ function create_routes_and_nodes(cwd, config, fallback) {
495
464
  if (route.endpoint) {
496
465
  route.endpoint.page_options = get_page_options(route.endpoint.file, cwd);
497
466
  }
467
+
468
+ if (route.page && route.endpoint) {
469
+ const page = nodes[route.page.leaf];
470
+ if (page.page_options?.prerender || route.endpoint.page_options?.prerender) {
471
+ const endpoint_file = route.endpoint.file.split('/').pop();
472
+
473
+ throw new Error(
474
+ `Cannot prerender a route (${route.id}) with both a \`+page.svelte\` and a \`${endpoint_file}\``
475
+ );
476
+ }
477
+ }
498
478
  }
499
479
 
500
480
  return {
@@ -139,6 +139,11 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
139
139
  write_if_changed(
140
140
  `${output}/app.js`,
141
141
  dedent`
142
+ // in dev, this makes Vite inject its client as this module's first dependency,
143
+ // so that global constant replacements are installed before any other module
144
+ // (including user hooks) evaluates. In build it's inert.
145
+ import.meta.hot;
146
+
142
147
  ${
143
148
  client_hooks_file
144
149
  ? `import * as client_hooks from '${relative_path(output, client_hooks_file)}';`
@@ -177,25 +182,20 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
177
182
  export const decode = (type, value) => decoders[type](value);
178
183
 
179
184
  export { default as root } from '../root.js';
185
+
186
+ export const get_error_template = () => import('../shared/error-template.js').then(m => m.default);
180
187
  `
181
188
  );
182
189
 
183
190
  if (client_routing) {
184
- // write matchers to a separate module so that we don't
185
- // need to worry about name conflicts
186
- const imports = [];
187
- const matchers = [];
188
-
189
- for (const key in manifest_data.matchers) {
190
- const src = manifest_data.matchers[key];
191
-
192
- imports.push(`import { match as ${key} } from ${s(relative_path(output, src))};`);
193
- matchers.push(key);
194
- }
191
+ const uses_matchers = manifest_data.routes.some((route) =>
192
+ route.params.some((param) => param.matcher)
193
+ );
195
194
 
196
- const module = imports.length
197
- ? `${imports.join('\n')}\n\nexport const matchers = { ${matchers.join(', ')} };`
198
- : 'export const matchers = {};';
195
+ const module =
196
+ !manifest_data.params || !uses_matchers
197
+ ? 'export const matchers = {};'
198
+ : `import { params as matchers } from ${s(relative_path(output, manifest_data.params))};\n\nexport { matchers };`;
199
199
 
200
200
  write_if_changed(`${output}/matchers.js`, module);
201
201
  }