@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 +1 -1
- package/src/core/config/options.js +9 -6
- package/src/core/env.js +29 -12
- package/src/core/prerender/prerender.js +6 -4
- package/src/core/sync/write_ambient.js +9 -6
- package/src/exports/vite/build/build_server.js +0 -1
- package/src/exports/vite/dev/index.js +0 -1
- package/src/exports/vite/index.js +7 -12
- package/src/runtime/client/client.js +16 -10
- package/src/runtime/client/start.js +2 -4
- package/src/runtime/client/types.d.ts +3 -1
- package/src/runtime/server/cookie.js +7 -6
- package/src/runtime/server/data/index.js +3 -2
- package/src/runtime/server/index.js +130 -105
- package/src/runtime/server/page/load_data.js +2 -1
- package/src/runtime/server/page/render.js +3 -2
- package/src/runtime/server/utils.js +5 -3
- package/types/internal.d.ts +5 -1
package/package.json
CHANGED
|
@@ -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/
|
|
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:
|
|
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, (
|
|
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 {
|
|
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
|
|
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${
|
|
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 {
|
|
43
|
-
* @param {
|
|
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 {
|
|
57
|
-
* @param {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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(
|
|
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 {
|
|
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('
|
|
30
|
+
${create_static_types('private', env)}
|
|
31
31
|
|
|
32
32
|
${read_description('$env+static+public.md')}
|
|
33
|
-
${create_static_types('
|
|
33
|
+
${create_static_types('public', env)}
|
|
34
34
|
|
|
35
35
|
${read_description('$env+dynamic+private.md')}
|
|
36
|
-
${create_dynamic_types('
|
|
36
|
+
${create_dynamic_types('private', env)}
|
|
37
37
|
|
|
38
38
|
${read_description('$env+dynamic+public.md')}
|
|
39
|
-
${create_dynamic_types('
|
|
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(
|
|
52
|
+
write_if_changed(
|
|
53
|
+
path.join(config.outDir, 'ambient.d.ts'),
|
|
54
|
+
template({ ...env, prefix: config.env.publicPrefix })
|
|
55
|
+
);
|
|
53
56
|
}
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 {
|
|
14
|
+
* @param {boolean} dev
|
|
15
|
+
* @param {import('types').TrailingSlash} trailing_slash
|
|
15
16
|
*/
|
|
16
|
-
export function get_cookies(request, url,
|
|
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
|
-
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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),
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
}
|
package/types/internal.d.ts
CHANGED
|
@@ -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 {
|