@sveltejs/kit 1.0.0-next.552 → 1.0.0-next.554

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "1.0.0-next.552",
3
+ "version": "1.0.0-next.554",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -232,7 +232,7 @@ const options = object(
232
232
  crawl: boolean(true),
233
233
  createIndexFiles: error(
234
234
  (keypath) =>
235
- `${keypath} has been removed — it is now controlled by the trailingSlash option. See https://kit.svelte.dev/docs/configuration#trailingslash`
235
+ `${keypath} has been removed — it is now controlled by the trailingSlash option. See https://kit.svelte.dev/docs/page-options#trailingslash`
236
236
  ),
237
237
  default: error(
238
238
  (keypath) =>
@@ -320,7 +320,7 @@ const options = object(
320
320
  // TODO remove for 1.0
321
321
  router: error(
322
322
  (keypath) =>
323
- `${keypath} has been removed. You can set \`export const csr = false\` inside the top level +layout.js instead. See the PR for more information: https://github.com/sveltejs/kit/pull/6197`
323
+ `${keypath} has been removed. You can set \`export const csr = false\` inside the top level +layout.js (or +layout.server.js) instead. See the PR for more information: https://github.com/sveltejs/kit/pull/6197`
324
324
  ),
325
325
 
326
326
  // TODO remove for 1.0
@@ -343,7 +343,10 @@ const options = object(
343
343
  // TODO remove this for 1.0
344
344
  target: error((keypath) => `${keypath} is no longer required, and should be removed`),
345
345
 
346
- trailingSlash: list(['never', 'always', 'ignore']),
346
+ trailingSlash: error(
347
+ (keypath, input) =>
348
+ `${keypath} has been removed. You can set \`export const trailingSlash = '${input}'\` inside a top level +layout.js (or +layout.server.js) instead. See the PR for more information: https://github.com/sveltejs/kit/pull/7719`
349
+ ),
347
350
 
348
351
  version: object({
349
352
  name: string(Date.now().toString()),
@@ -506,10 +509,10 @@ function assert_string(input, keypath) {
506
509
  }
507
510
  }
508
511
 
509
- /** @param {(keypath?: string) => string} fn */
512
+ /** @param {(keypath?: string, input?: any) => string} fn */
510
513
  function error(fn) {
511
- return validate(undefined, (_, keypath) => {
512
- throw new Error(fn(keypath));
514
+ return validate(undefined, (input, keypath) => {
515
+ throw new Error(fn(keypath, input));
513
516
  });
514
517
  }
515
518
 
package/src/core/env.js CHANGED
@@ -1,6 +1,15 @@
1
1
  import { GENERATED_COMMENT } from '../constants.js';
2
2
  import { runtime_base } from './utils.js';
3
3
 
4
+ /**
5
+ * @typedef {'public' | 'private'} EnvType
6
+ * @typedef {{
7
+ * public: Record<string, string>;
8
+ * private: Record<string, string>;
9
+ * prefix: string;
10
+ * }} EnvData
11
+ */
12
+
4
13
  /**
5
14
  * @param {string} id
6
15
  * @param {Record<string, string>} env
@@ -25,46 +34,54 @@ export function create_static_module(id, env) {
25
34
  }
26
35
 
27
36
  /**
28
- * @param {'public' | 'private'} type
37
+ * @param {EnvType} type
29
38
  * @param {Record<string, string> | undefined} dev_values If in a development mode, values to pre-populate the module with.
30
39
  */
31
40
  export function create_dynamic_module(type, dev_values) {
32
41
  if (dev_values) {
33
- const objectKeys = Object.entries(dev_values).map(
42
+ const keys = Object.entries(dev_values).map(
34
43
  ([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
35
44
  );
36
- return `const env = {\n${objectKeys.join(',\n')}\n}\n\nexport { env }`;
45
+ return `export const env = {\n${keys.join(',\n')}\n}`;
37
46
  }
38
47
  return `export { env } from '${runtime_base}/env-${type}.js';`;
39
48
  }
40
49
 
41
50
  /**
42
- * @param {string} id
43
- * @param {Record<string, string>} env
51
+ * @param {EnvType} id
52
+ * @param {EnvData} env
44
53
  * @returns {string}
45
54
  */
46
55
  export function create_static_types(id, env) {
47
- const declarations = Object.keys(env)
56
+ const declarations = Object.keys(env[id])
48
57
  .filter((k) => valid_identifier.test(k))
49
58
  .map((k) => `\texport const ${k}: string;`)
50
59
  .join('\n');
51
60
 
52
- return `declare module '${id}' {\n${declarations}\n}`;
61
+ return `declare module '$env/static/${id}' {\n${declarations}\n}`;
53
62
  }
54
63
 
55
64
  /**
56
- * @param {string} id
57
- * @param {Record<string, string>} env
65
+ * @param {EnvType} id
66
+ * @param {EnvData} env
58
67
  * @returns {string}
59
68
  */
60
69
  export function create_dynamic_types(id, env) {
61
- const properties = Object.keys(env)
70
+ const properties = Object.keys(env[id])
62
71
  .filter((k) => valid_identifier.test(k))
63
72
  .map((k) => `\t\t${k}: string;`);
64
73
 
65
- properties.push(`\t\t[key: string]: string | undefined;`);
74
+ const prefixed = `[key: \`${env.prefix}\${string}\`]`;
75
+
76
+ if (id === 'private') {
77
+ properties.push(`\t\t${prefixed}: undefined;`);
78
+ properties.push(`\t\t[key: string]: string | undefined;`);
79
+ } else {
80
+ properties.push(`\t\t${prefixed}: string | undefined;`);
81
+ }
66
82
 
67
- return `declare module '${id}' {\n\texport const env: {\n${properties.join('\n')}\n\t}\n}`;
83
+ const declaration = `export const env: {\n${properties.join('\n')}\n\t}`;
84
+ return `declare module '$env/dynamic/${id}' {\n\t${declaration}\n}`;
68
85
  }
69
86
 
70
87
  export const reserved = new Set([
@@ -137,7 +137,7 @@ export async function prerender() {
137
137
  config.prerender.handleMissingId,
138
138
  ({ path, id, referrers }) => {
139
139
  return (
140
- `The following pages contain links to ${path}#${id}, but no element with id="${id}" exists on ${path}:` +
140
+ `The following pages contain links to ${path}#${id}, but no element with id="${id}" exists on ${path} - see the \`handleMissingId\` option in https://kit.svelte.dev/docs/configuration#prerender for more info:` +
141
141
  referrers.map((l) => `\n - ${l}`).join('')
142
142
  );
143
143
  }
@@ -262,11 +262,13 @@ export async function prerender() {
262
262
  }
263
263
 
264
264
  if (hash) {
265
- if (!expected_hashlinks.has(pathname + hash)) {
266
- expected_hashlinks.set(pathname + hash, new Set());
265
+ const key = decodeURI(pathname + hash);
266
+
267
+ if (!expected_hashlinks.has(key)) {
268
+ expected_hashlinks.set(key, new Set());
267
269
  }
268
270
 
269
- /** @type {Set<string>} */ (expected_hashlinks.get(pathname + hash)).add(decoded);
271
+ /** @type {Set<string>} */ (expected_hashlinks.get(key)).add(decoded);
270
272
  }
271
273
 
272
274
  enqueue(decoded, decodeURI(pathname), pathname);
@@ -19,7 +19,7 @@ function read_description(filename) {
19
19
  }
20
20
 
21
21
  /**
22
- * @param {{ public: Record<string, string>, private: Record<string, string> }} env
22
+ * @param {import('../env.js').EnvData} env
23
23
  */
24
24
  const template = (env) => `
25
25
  ${GENERATED_COMMENT}
@@ -27,16 +27,16 @@ ${GENERATED_COMMENT}
27
27
  /// <reference types="@sveltejs/kit" />
28
28
 
29
29
  ${read_description('$env+static+private.md')}
30
- ${create_static_types('$env/static/private', env.private)}
30
+ ${create_static_types('private', env)}
31
31
 
32
32
  ${read_description('$env+static+public.md')}
33
- ${create_static_types('$env/static/public', env.public)}
33
+ ${create_static_types('public', env)}
34
34
 
35
35
  ${read_description('$env+dynamic+private.md')}
36
- ${create_dynamic_types('$env/dynamic/private', env.private)}
36
+ ${create_dynamic_types('private', env)}
37
37
 
38
38
  ${read_description('$env+dynamic+public.md')}
39
- ${create_dynamic_types('$env/dynamic/public', env.public)}
39
+ ${create_dynamic_types('public', env)}
40
40
  `;
41
41
 
42
42
  /**
@@ -49,5 +49,8 @@ ${create_dynamic_types('$env/dynamic/public', env.public)}
49
49
  export function write_ambient(config, mode) {
50
50
  const env = get_env(config.env, mode);
51
51
 
52
- write_if_changed(path.join(config.outDir, 'ambient.d.ts'), template(env));
52
+ write_if_changed(
53
+ path.join(config.outDir, 'ambient.d.ts'),
54
+ template({ ...env, prefix: config.env.publicPrefix })
55
+ );
53
56
  }
@@ -87,7 +87,6 @@ export class Server {
87
87
  app_template,
88
88
  app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
89
89
  error_template,
90
- trailing_slash: ${s(config.kit.trailingSlash)},
91
90
  version: ${s(config.kit.version.name)}
92
91
  };
93
92
  }
@@ -480,7 +480,6 @@ export async function dev(vite, vite_config, svelte_config) {
480
480
  service_worker:
481
481
  svelte_config.kit.serviceWorker.register &&
482
482
  !!resolve_entry(svelte_config.kit.files.serviceWorker),
483
- trailing_slash: svelte_config.kit.trailingSlash,
484
483
  version: svelte_config.kit.version.name
485
484
  },
486
485
  {
@@ -107,9 +107,6 @@ function kit() {
107
107
  /** @type {import('types').BuildData} */
108
108
  let build_data;
109
109
 
110
- /** @type {string | undefined} */
111
- let deferred_warning;
112
-
113
110
  /** @type {{ public: Record<string, string>; private: Record<string, string> }} */
114
111
  let env;
115
112
 
@@ -284,7 +281,9 @@ function kit() {
284
281
  }
285
282
  };
286
283
 
287
- deferred_warning = warn_overridden_config(config, result);
284
+ const warning = warn_overridden_config(config, result);
285
+ if (warning) console.error(warning);
286
+
288
287
  return result;
289
288
  },
290
289
 
@@ -334,6 +333,10 @@ function kit() {
334
333
  */
335
334
  configResolved(config) {
336
335
  vite_config = config;
336
+
337
+ // This is a hack to prevent Vite from nuking useful logs,
338
+ // pending https://github.com/vitejs/vite/issues/9378
339
+ config.logger.warn('');
337
340
  },
338
341
 
339
342
  /**
@@ -540,14 +543,6 @@ function kit() {
540
543
  * @see https://vitejs.dev/guide/api-plugin.html#configureserver
541
544
  */
542
545
  async configureServer(vite) {
543
- // This method is called by Vite after clearing the screen.
544
- // This patch ensures we can log any important messages afterwards for the user to see.
545
- const print_urls = vite.printUrls;
546
- vite.printUrls = function () {
547
- print_urls.apply(this);
548
- if (deferred_warning) console.error('\n' + deferred_warning);
549
- };
550
-
551
546
  return await dev(vite, vite_config, svelte_config);
552
547
  },
553
548
 
@@ -77,11 +77,10 @@ function check_for_removed_attributes() {
77
77
  * @param {{
78
78
  * target: Element;
79
79
  * base: string;
80
- * trailing_slash: import('types').TrailingSlash;
81
80
  * }} opts
82
81
  * @returns {import('./types').Client}
83
82
  */
84
- export function create_client({ target, base, trailing_slash }) {
83
+ export function create_client({ target, base }) {
85
84
  /** @type {Array<((url: URL) => boolean)>} */
86
85
  const invalidated = [];
87
86
 
@@ -416,6 +415,14 @@ export function create_client({ target, base, trailing_slash }) {
416
415
  }) {
417
416
  const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
418
417
 
418
+ /** @type {import('types').TrailingSlash} */
419
+ let slash = 'never';
420
+ for (const node of branch) {
421
+ if (node?.slash !== undefined) slash = node.slash;
422
+ }
423
+ url.pathname = normalize_path(url.pathname, slash);
424
+ url.search = url.search; // turn `/?` into `/`
425
+
419
426
  /** @type {import('./types').NavigationFinished} */
420
427
  const result = {
421
428
  type: 'loaded',
@@ -657,7 +664,8 @@ export function create_client({ target, base, trailing_slash }) {
657
664
  loader,
658
665
  server: server_data_node,
659
666
  shared: node.shared?.load ? { type: 'data', data, uses } : null,
660
- data: data ?? server_data_node?.data ?? null
667
+ data: data ?? server_data_node?.data ?? null,
668
+ slash: node.shared?.trailingSlash ?? server_data_node?.slash
661
669
  };
662
670
  }
663
671
 
@@ -704,7 +712,8 @@ export function create_client({ target, base, trailing_slash }) {
704
712
  parent: !!node.uses.parent,
705
713
  route: !!node.uses.route,
706
714
  url: !!node.uses.url
707
- }
715
+ },
716
+ slash: node.slash
708
717
  };
709
718
  } else if (node?.type === 'skip') {
710
719
  return previous ?? null;
@@ -999,12 +1008,9 @@ export function create_client({ target, base, trailing_slash }) {
999
1008
  const params = route.exec(path);
1000
1009
 
1001
1010
  if (params) {
1002
- const normalized = new URL(
1003
- url.origin + normalize_path(url.pathname, trailing_slash) + url.search + url.hash
1004
- );
1005
- const id = normalized.pathname + normalized.search;
1011
+ const id = url.pathname + url.search;
1006
1012
  /** @type {import('./types').NavigationIntent} */
1007
- const intent = { id, invalidating, route, params: decode_params(params), url: normalized };
1013
+ const intent = { id, invalidating, route, params: decode_params(params), url };
1008
1014
  return intent;
1009
1015
  }
1010
1016
  }
@@ -1458,7 +1464,7 @@ export function create_client({ target, base, trailing_slash }) {
1458
1464
  });
1459
1465
 
1460
1466
  addEventListener('popstate', (event) => {
1461
- if (event.state) {
1467
+ if (event.state?.[INDEX_KEY]) {
1462
1468
  // if a popstate-driven navigation is cancelled, we need to counteract it
1463
1469
  // with history.go, which means we end up back here, hence this check
1464
1470
  if (event.state[INDEX_KEY] === current_history_index) return;
@@ -13,11 +13,10 @@ import { set_version } from '../env.js';
13
13
  * base: string;
14
14
  * },
15
15
  * target: Element;
16
- * trailing_slash: import('types').TrailingSlash;
17
16
  * version: string;
18
17
  * }} opts
19
18
  */
20
- export async function start({ env, hydrate, paths, target, trailing_slash, version }) {
19
+ export async function start({ env, hydrate, paths, target, version }) {
21
20
  set_public_env(env);
22
21
  set_paths(paths);
23
22
  set_version(version);
@@ -30,8 +29,7 @@ export async function start({ env, hydrate, paths, target, trailing_slash, versi
30
29
 
31
30
  const client = create_client({
32
31
  target,
33
- base: paths.base,
34
- trailing_slash
32
+ base: paths.base
35
33
  });
36
34
 
37
35
  init({ client });
@@ -8,7 +8,7 @@ import {
8
8
  prefetch,
9
9
  prefetchRoutes
10
10
  } from '$app/navigation';
11
- import { CSRPageNode, CSRPageNodeLoader, CSRRoute, Uses } from 'types';
11
+ import { CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types';
12
12
 
13
13
  export interface Client {
14
14
  // public API, exposed via $app/navigation
@@ -67,12 +67,14 @@ export type BranchNode = {
67
67
  server: DataNode | null;
68
68
  shared: DataNode | null;
69
69
  data: Record<string, any> | null;
70
+ slash?: TrailingSlash;
70
71
  };
71
72
 
72
73
  export interface DataNode {
73
74
  type: 'data';
74
75
  data: Record<string, any> | null;
75
76
  uses: Uses;
77
+ slash?: TrailingSlash;
76
78
  }
77
79
 
78
80
  export interface NavigationState {
@@ -11,9 +11,10 @@ const cookie_paths = {};
11
11
  /**
12
12
  * @param {Request} request
13
13
  * @param {URL} url
14
- * @param {Pick<import('types').SSROptions, 'dev' | 'trailing_slash'>} options
14
+ * @param {boolean} dev
15
+ * @param {import('types').TrailingSlash} trailing_slash
15
16
  */
16
- export function get_cookies(request, url, options) {
17
+ export function get_cookies(request, url, dev, trailing_slash) {
17
18
  const header = request.headers.get('cookie') ?? '';
18
19
  const initial_cookies = parse(header);
19
20
 
@@ -21,12 +22,12 @@ export function get_cookies(request, url, options) {
21
22
  // Remove suffix: 'foo/__data.json' would mean the cookie path is '/foo',
22
23
  // whereas a direct hit of /foo would mean the cookie path is '/'
23
24
  has_data_suffix(url.pathname) ? strip_data_suffix(url.pathname) : url.pathname,
24
- options.trailing_slash
25
+ trailing_slash
25
26
  );
26
27
  // Emulate browser-behavior: if the cookie is set at '/foo/bar', its path is '/foo'
27
28
  const default_path = normalized_url.split('/').slice(0, -1).join('/') || '/';
28
29
 
29
- if (options.dev) {
30
+ if (dev) {
30
31
  // Remove all cookies that no longer exist according to the request
31
32
  for (const name of Object.keys(cookie_paths)) {
32
33
  cookie_paths[name] = new Set(
@@ -79,7 +80,7 @@ export function get_cookies(request, url, options) {
79
80
  const req_cookies = parse(header, { decode });
80
81
  const cookie = req_cookies[name]; // the decoded string or undefined
81
82
 
82
- if (!options.dev || cookie) {
83
+ if (!dev || cookie) {
83
84
  return cookie;
84
85
  }
85
86
 
@@ -113,7 +114,7 @@ export function get_cookies(request, url, options) {
113
114
  }
114
115
  };
115
116
 
116
- if (options.dev) {
117
+ if (dev) {
117
118
  cookie_paths[name] = cookie_paths[name] ?? new Set();
118
119
  if (!value) {
119
120
  if (!cookie_paths[name].has(path) && cookie_paths[name].size > 0) {
@@ -12,9 +12,10 @@ export const INVALIDATED_HEADER = 'x-sveltekit-invalidated';
12
12
  * @param {import('types').SSRRoute} route
13
13
  * @param {import('types').SSROptions} options
14
14
  * @param {import('types').SSRState} state
15
+ * @param {import('types').TrailingSlash} trailing_slash
15
16
  * @returns {Promise<Response>}
16
17
  */
17
- export async function render_data(event, route, options, state) {
18
+ export async function render_data(event, route, options, state, trailing_slash) {
18
19
  if (!route.page) {
19
20
  // requesting /__data.json should fail for a +server.js
20
21
  return new Response(undefined, {
@@ -32,7 +33,7 @@ export async function render_data(event, route, options, state) {
32
33
  let aborted = false;
33
34
 
34
35
  const url = new URL(event.url);
35
- url.pathname = normalize_path(strip_data_suffix(url.pathname), options.trailing_slash);
36
+ url.pathname = normalize_path(strip_data_suffix(url.pathname), trailing_slash);
36
37
 
37
38
  const new_event = { ...event, url };
38
39
 
@@ -3,7 +3,7 @@ import { render_page } from './page/index.js';
3
3
  import { render_response } from './page/render.js';
4
4
  import { respond_with_error } from './page/respond_with_error.js';
5
5
  import { is_form_content_type } from '../../utils/http.js';
6
- import { GENERIC_ERROR, handle_fatal_error, redirect_response } from './utils.js';
6
+ import { GENERIC_ERROR, get_option, handle_fatal_error, redirect_response } from './utils.js';
7
7
  import {
8
8
  decode_pathname,
9
9
  decode_params,
@@ -70,6 +70,7 @@ export async function respond(request, options, state) {
70
70
  if (is_data_request) decoded = strip_data_suffix(decoded) || '/';
71
71
 
72
72
  if (!state.prerendering?.fallback) {
73
+ // TODO this could theoretically break — should probably be inside a try-catch
73
74
  const matchers = await options.manifest._.matchers();
74
75
 
75
76
  for (const candidate of options.manifest._.routes) {
@@ -85,34 +86,17 @@ export async function respond(request, options, state) {
85
86
  }
86
87
  }
87
88
 
88
- if (route?.page && !is_data_request) {
89
- const normalized = normalize_path(url.pathname, options.trailing_slash);
90
-
91
- if (normalized !== url.pathname && !state.prerendering?.fallback) {
92
- return new Response(undefined, {
93
- status: 301,
94
- headers: {
95
- 'x-sveltekit-normalize': '1',
96
- location:
97
- // ensure paths starting with '//' are not treated as protocol-relative
98
- (normalized.startsWith('//') ? url.origin + normalized : normalized) +
99
- (url.search === '?' ? '' : url.search)
100
- }
101
- });
102
- }
103
- }
89
+ /** @type {import('types').TrailingSlash | void} */
90
+ let trailing_slash = undefined;
104
91
 
105
92
  /** @type {Record<string, string>} */
106
93
  const headers = {};
107
94
 
108
- const { cookies, new_cookies, get_cookie_header } = get_cookies(request, url, options);
109
-
110
- if (state.prerendering && !state.prerendering.fallback) disable_search(url);
111
-
112
95
  /** @type {import('types').RequestEvent} */
113
96
  const event = {
114
- cookies,
115
- // @ts-expect-error this is added in the next step, because `create_fetch` needs a reference to `event`
97
+ // @ts-expect-error `cookies` and `fetch` need to be created after the `event` itself
98
+ cookies: null,
99
+ // @ts-expect-error
116
100
  fetch: null,
117
101
  getClientAddress:
118
102
  state.getClientAddress ||
@@ -149,8 +133,6 @@ export async function respond(request, options, state) {
149
133
  url
150
134
  };
151
135
 
152
- event.fetch = create_fetch({ event, options, state, get_cookie_header });
153
-
154
136
  // TODO remove this for 1.0
155
137
  /**
156
138
  * @param {string} property
@@ -193,6 +175,128 @@ export async function respond(request, options, state) {
193
175
  preload: default_preload
194
176
  };
195
177
 
178
+ try {
179
+ // determine whether we need to redirect to add/remove a trailing slash
180
+ if (route && !is_data_request) {
181
+ if (route.page) {
182
+ const nodes = await Promise.all([
183
+ // we use == here rather than === because [undefined] serializes as "[null]"
184
+ ...route.page.layouts.map((n) => (n == undefined ? n : options.manifest._.nodes[n]())),
185
+ options.manifest._.nodes[route.page.leaf]()
186
+ ]);
187
+
188
+ trailing_slash = get_option(nodes, 'trailingSlash');
189
+ } else if (route.endpoint) {
190
+ const node = await route.endpoint();
191
+ trailing_slash = node.trailingSlash;
192
+ }
193
+
194
+ const normalized = normalize_path(url.pathname, trailing_slash ?? 'never');
195
+
196
+ if (normalized !== url.pathname && !state.prerendering?.fallback) {
197
+ return new Response(undefined, {
198
+ status: 301,
199
+ headers: {
200
+ 'x-sveltekit-normalize': '1',
201
+ location:
202
+ // ensure paths starting with '//' are not treated as protocol-relative
203
+ (normalized.startsWith('//') ? url.origin + normalized : normalized) +
204
+ (url.search === '?' ? '' : url.search)
205
+ }
206
+ });
207
+ }
208
+ }
209
+
210
+ const { cookies, new_cookies, get_cookie_header } = get_cookies(
211
+ request,
212
+ url,
213
+ options.dev,
214
+ trailing_slash ?? 'never'
215
+ );
216
+
217
+ event.cookies = cookies;
218
+ event.fetch = create_fetch({ event, options, state, get_cookie_header });
219
+
220
+ if (state.prerendering && !state.prerendering.fallback) disable_search(url);
221
+
222
+ const response = await options.hooks.handle({
223
+ event,
224
+ resolve: (event, opts) =>
225
+ resolve(event, opts).then((response) => {
226
+ // add headers/cookies here, rather than inside `resolve`, so that we
227
+ // can do it once for all responses instead of once per `return`
228
+ for (const key in headers) {
229
+ const value = headers[key];
230
+ response.headers.set(key, /** @type {string} */ (value));
231
+ }
232
+
233
+ if (is_data_request) {
234
+ // set the Vary header on __data.json requests to ensure we don't cache
235
+ // incomplete responses with skipped data loads
236
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
237
+ const vary = response.headers.get('Vary');
238
+ if (vary !== '*') {
239
+ response.headers.append('Vary', INVALIDATED_HEADER);
240
+ }
241
+ }
242
+
243
+ add_cookies_to_headers(response.headers, Object.values(new_cookies));
244
+
245
+ if (state.prerendering && event.route.id !== null) {
246
+ response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id));
247
+ }
248
+
249
+ return response;
250
+ }),
251
+ // TODO remove for 1.0
252
+ // @ts-expect-error
253
+ get request() {
254
+ throw new Error('request in handle has been replaced with event' + details);
255
+ }
256
+ });
257
+
258
+ // respond with 304 if etag matches
259
+ if (response.status === 200 && response.headers.has('etag')) {
260
+ let if_none_match_value = request.headers.get('if-none-match');
261
+
262
+ // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
263
+ if (if_none_match_value?.startsWith('W/"')) {
264
+ if_none_match_value = if_none_match_value.substring(2);
265
+ }
266
+
267
+ const etag = /** @type {string} */ (response.headers.get('etag'));
268
+
269
+ if (if_none_match_value === etag) {
270
+ const headers = new Headers({ etag });
271
+
272
+ // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + set-cookie
273
+ for (const key of [
274
+ 'cache-control',
275
+ 'content-location',
276
+ 'date',
277
+ 'expires',
278
+ 'vary',
279
+ 'set-cookie'
280
+ ]) {
281
+ const value = response.headers.get(key);
282
+ if (value) headers.set(key, value);
283
+ }
284
+
285
+ return new Response(undefined, {
286
+ status: 304,
287
+ headers
288
+ });
289
+ }
290
+ }
291
+
292
+ return response;
293
+ } catch (error) {
294
+ if (error instanceof Redirect) {
295
+ return redirect_response(error.status, error.location);
296
+ }
297
+ return handle_fatal_error(event, options, error);
298
+ }
299
+
196
300
  /**
197
301
  *
198
302
  * @param {import('types').RequestEvent} event
@@ -240,7 +344,7 @@ export async function respond(request, options, state) {
240
344
  let response;
241
345
 
242
346
  if (is_data_request) {
243
- response = await render_data(event, route, options, state);
347
+ response = await render_data(event, route, options, state, trailing_slash ?? 'never');
244
348
  } else if (route.endpoint && (!route.page || is_endpoint_request(event))) {
245
349
  response = await render_endpoint(event, await route.endpoint(), state);
246
350
  } else if (route.page) {
@@ -293,83 +397,4 @@ export async function respond(request, options, state) {
293
397
  };
294
398
  }
295
399
  }
296
-
297
- try {
298
- const response = await options.hooks.handle({
299
- event,
300
- resolve: (event, opts) =>
301
- resolve(event, opts).then((response) => {
302
- // add headers/cookies here, rather than inside `resolve`, so that we
303
- // can do it once for all responses instead of once per `return`
304
- for (const key in headers) {
305
- const value = headers[key];
306
- response.headers.set(key, /** @type {string} */ (value));
307
- }
308
-
309
- if (is_data_request) {
310
- // set the Vary header on __data.json requests to ensure we don't cache
311
- // incomplete responses with skipped data loads
312
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
313
- const vary = response.headers.get('Vary');
314
- if (vary !== '*') {
315
- response.headers.append('Vary', INVALIDATED_HEADER);
316
- }
317
- }
318
-
319
- add_cookies_to_headers(response.headers, Object.values(new_cookies));
320
-
321
- if (state.prerendering && event.route.id !== null) {
322
- response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id));
323
- }
324
-
325
- return response;
326
- }),
327
- // TODO remove for 1.0
328
- // @ts-expect-error
329
- get request() {
330
- throw new Error('request in handle has been replaced with event' + details);
331
- }
332
- });
333
-
334
- // respond with 304 if etag matches
335
- if (response.status === 200 && response.headers.has('etag')) {
336
- let if_none_match_value = request.headers.get('if-none-match');
337
-
338
- // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
339
- if (if_none_match_value?.startsWith('W/"')) {
340
- if_none_match_value = if_none_match_value.substring(2);
341
- }
342
-
343
- const etag = /** @type {string} */ (response.headers.get('etag'));
344
-
345
- if (if_none_match_value === etag) {
346
- const headers = new Headers({ etag });
347
-
348
- // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + set-cookie
349
- for (const key of [
350
- 'cache-control',
351
- 'content-location',
352
- 'date',
353
- 'expires',
354
- 'vary',
355
- 'set-cookie'
356
- ]) {
357
- const value = response.headers.get(key);
358
- if (value) headers.set(key, value);
359
- }
360
-
361
- return new Response(undefined, {
362
- status: 304,
363
- headers
364
- });
365
- }
366
- }
367
-
368
- return response;
369
- } catch (error) {
370
- if (error instanceof Redirect) {
371
- return redirect_response(error.status, error.location);
372
- }
373
- return handle_fatal_error(event, options, error);
374
- }
375
400
  }
@@ -63,7 +63,8 @@ export async function load_server_data({ event, state, node, parent }) {
63
63
  return {
64
64
  type: 'data',
65
65
  data,
66
- uses
66
+ uses,
67
+ slash: node.server.trailingSlash
67
68
  };
68
69
  }
69
70
 
@@ -195,7 +195,9 @@ export async function render_response({
195
195
  if (server_data.uses.route) uses.push(`route:1`);
196
196
  if (server_data.uses.url) uses.push(`url:1`);
197
197
 
198
- return `{type:"data",data:${data},uses:{${uses.join(',')}}}`;
198
+ return `{type:"data",data:${data},uses:{${uses.join(',')}}${
199
+ server_data.slash ? `,slash:${s(server_data.slash)}` : ''
200
+ }}`;
199
201
  }
200
202
 
201
203
  return s(server_data);
@@ -281,7 +283,6 @@ export async function render_response({
281
283
  }` : 'null'},
282
284
  paths: ${s(options.paths)},
283
285
  target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode,
284
- trailing_slash: ${s(options.trailing_slash)},
285
286
  version: ${s(options.version)}
286
287
  });
287
288
  `;
@@ -69,8 +69,8 @@ export function allowed_methods(mod) {
69
69
  }
70
70
 
71
71
  /**
72
- * @template {'prerender' | 'ssr' | 'csr'} Option
73
- * @template {Option extends 'prerender' ? import('types').PrerenderOption : boolean} Value
72
+ * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash'} Option
73
+ * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : boolean} Value
74
74
  *
75
75
  * @param {Array<import('types').SSRNode | undefined>} nodes
76
76
  * @param {Option} option
@@ -201,5 +201,7 @@ export function serialize_data_node(node) {
201
201
  if (node.uses.route) uses.push(`"route":1`);
202
202
  if (node.uses.url) uses.push(`"url":1`);
203
203
 
204
- return `{"type":"data","data":${stringified},"uses":{${uses.join(',')}}}`;
204
+ return `{"type":"data","data":${stringified},"uses":{${uses.join(',')}}${
205
+ node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''
206
+ }}`;
205
207
  }
@@ -72,6 +72,7 @@ export interface CSRPageNode {
72
72
  component: typeof SvelteComponent;
73
73
  shared: {
74
74
  load?: Load;
75
+ trailingSlash?: TrailingSlash;
75
76
  };
76
77
  server: boolean;
77
78
  }
@@ -209,6 +210,7 @@ export interface ServerDataNode {
209
210
  type: 'data';
210
211
  data: Record<string, any> | null;
211
212
  uses: Uses;
213
+ slash?: TrailingSlash;
212
214
  }
213
215
 
214
216
  /**
@@ -266,6 +268,7 @@ export interface SSRNode {
266
268
  prerender?: PrerenderOption;
267
269
  ssr?: boolean;
268
270
  csr?: boolean;
271
+ trailingSlash?: TrailingSlash;
269
272
  };
270
273
 
271
274
  server: {
@@ -273,6 +276,7 @@ export interface SSRNode {
273
276
  prerender?: PrerenderOption;
274
277
  ssr?: boolean;
275
278
  csr?: boolean;
279
+ trailingSlash?: TrailingSlash;
276
280
  actions?: Actions;
277
281
  };
278
282
 
@@ -312,7 +316,6 @@ export interface SSROptions {
312
316
  }): string;
313
317
  app_template_contains_nonce: boolean;
314
318
  error_template({ message, status }: { message: string; status: number }): string;
315
- trailing_slash: TrailingSlash;
316
319
  version: string;
317
320
  }
318
321
 
@@ -328,6 +331,7 @@ export interface PageNodeIndexes {
328
331
 
329
332
  export type SSREndpoint = Partial<Record<HttpMethod, RequestHandler>> & {
330
333
  prerender?: PrerenderOption;
334
+ trailingSlash?: TrailingSlash;
331
335
  };
332
336
 
333
337
  export interface SSRRoute {