@sveltejs/kit 1.0.0-next.461 → 1.0.0-next.464
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 +2 -2
- package/src/core/config/options.js +4 -0
- package/src/core/generate_manifest/index.js +21 -7
- package/src/exports/vite/build/build_server.js +3 -0
- package/src/exports/vite/dev/index.js +3 -0
- package/src/runtime/client/client.js +14 -6
- package/src/runtime/client/fetcher.js +50 -3
- package/src/runtime/server/index.js +15 -0
- package/src/runtime/server/page/fetch.js +1 -0
- package/src/runtime/server/page/render.js +1 -1
- package/src/runtime/server/page/serialize_data.js +15 -1
- package/src/runtime/server/page/types.d.ts +1 -0
- package/types/ambient.d.ts +1 -0
- package/types/index.d.ts +3 -0
- package/types/internal.d.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sveltejs/kit",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.464",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/sveltejs/kit",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"homepage": "https://kit.svelte.dev",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@sveltejs/vite-plugin-svelte": "^1.0.
|
|
13
|
+
"@sveltejs/vite-plugin-svelte": "^1.0.4",
|
|
14
14
|
"cookie": "^0.5.0",
|
|
15
15
|
"devalue": "^3.1.2",
|
|
16
16
|
"kleur": "^4.1.4",
|
|
@@ -125,6 +125,10 @@ const options = object(
|
|
|
125
125
|
reportOnly: directives
|
|
126
126
|
}),
|
|
127
127
|
|
|
128
|
+
csrf: object({
|
|
129
|
+
checkOrigin: boolean(true)
|
|
130
|
+
}),
|
|
131
|
+
|
|
128
132
|
// TODO: remove this for the 1.0 release
|
|
129
133
|
endpointExtensions: error(
|
|
130
134
|
(keypath) => `${keypath} has been renamed to config.kit.moduleExtensions`
|
|
@@ -60,13 +60,13 @@ export function generate_manifest({ build_data, relative_path, routes, format =
|
|
|
60
60
|
if (!route.page && !route.endpoint) return;
|
|
61
61
|
|
|
62
62
|
return `{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
id: ${s(route.id)},
|
|
64
|
+
pattern: ${route.pattern},
|
|
65
|
+
names: ${s(route.names)},
|
|
66
|
+
types: ${s(route.types)},
|
|
67
|
+
page: ${route.page ? `{ layouts: ${get_nodes(route.page.layouts)}, errors: ${get_nodes(route.page.errors)}, leaf: ${route.page.leaf} }` : 'null'},
|
|
68
|
+
endpoint: ${route.endpoint ? loader(`${relative_path}/${build_data.server.vite_manifest[route.endpoint.file].file}`) : 'null'}
|
|
69
|
+
}`;
|
|
70
70
|
}).filter(Boolean).join(',\n\t\t\t\t')}
|
|
71
71
|
],
|
|
72
72
|
matchers: async () => {
|
|
@@ -76,3 +76,17 @@ export function generate_manifest({ build_data, relative_path, routes, format =
|
|
|
76
76
|
}
|
|
77
77
|
}`.replace(/^\t/gm, '');
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/** @param {Array<number | undefined>} indexes */
|
|
81
|
+
function get_nodes(indexes) {
|
|
82
|
+
let string = indexes.map((n) => n ?? '').join(',');
|
|
83
|
+
|
|
84
|
+
if (indexes.at(-1) === undefined) {
|
|
85
|
+
// since JavaScript ignores trailing commas, we need to insert a dummy
|
|
86
|
+
// comma so that the array has the correct length if the last item
|
|
87
|
+
// is undefined
|
|
88
|
+
string += ',';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return `[${string}]`;
|
|
92
|
+
}
|
|
@@ -54,6 +54,9 @@ export class Server {
|
|
|
54
54
|
constructor(manifest) {
|
|
55
55
|
this.options = {
|
|
56
56
|
csp: ${s(config.kit.csp)},
|
|
57
|
+
csrf: {
|
|
58
|
+
check_origin: ${s(config.kit.csrf.checkOrigin)},
|
|
59
|
+
},
|
|
57
60
|
dev: false,
|
|
58
61
|
get_stack: error => String(error), // for security
|
|
59
62
|
handle_error: (error, event) => {
|
|
@@ -377,6 +377,9 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
|
|
|
377
377
|
request,
|
|
378
378
|
{
|
|
379
379
|
csp: svelte_config.kit.csp,
|
|
380
|
+
csrf: {
|
|
381
|
+
check_origin: svelte_config.kit.csrf.checkOrigin
|
|
382
|
+
},
|
|
380
383
|
dev: true,
|
|
381
384
|
get_stack: (error) => fix_stack_trace(error),
|
|
382
385
|
handle_error: (error, event) => {
|
|
@@ -2,7 +2,7 @@ import { onMount, tick } from 'svelte';
|
|
|
2
2
|
import { normalize_error } from '../../utils/error.js';
|
|
3
3
|
import { make_trackable, decode_params, normalize_path } from '../../utils/url.js';
|
|
4
4
|
import { find_anchor, get_base_uri, scroll_state } from './utils.js';
|
|
5
|
-
import { lock_fetch, unlock_fetch, initial_fetch,
|
|
5
|
+
import { lock_fetch, unlock_fetch, initial_fetch, subsequent_fetch } from './fetcher.js';
|
|
6
6
|
import { parse } from './parse.js';
|
|
7
7
|
import { error } from '../../exports/index.js';
|
|
8
8
|
|
|
@@ -584,11 +584,13 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
584
584
|
}
|
|
585
585
|
|
|
586
586
|
// we must fixup relative urls so they are resolved from the target page
|
|
587
|
-
const
|
|
588
|
-
depends(
|
|
587
|
+
const resolved = new URL(requested, url).href;
|
|
588
|
+
depends(resolved);
|
|
589
589
|
|
|
590
|
-
// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be
|
|
591
|
-
return started
|
|
590
|
+
// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved
|
|
591
|
+
return started
|
|
592
|
+
? subsequent_fetch(resolved, init)
|
|
593
|
+
: initial_fetch(requested, resolved, init);
|
|
592
594
|
},
|
|
593
595
|
setHeaders: () => {}, // noop
|
|
594
596
|
depends,
|
|
@@ -941,7 +943,7 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
941
943
|
|
|
942
944
|
/** @param {URL} url */
|
|
943
945
|
function get_navigation_intent(url) {
|
|
944
|
-
if (url
|
|
946
|
+
if (is_external_url(url)) return;
|
|
945
947
|
|
|
946
948
|
const path = decodeURI(url.pathname.slice(base.length) || '/');
|
|
947
949
|
|
|
@@ -960,6 +962,11 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
960
962
|
}
|
|
961
963
|
}
|
|
962
964
|
|
|
965
|
+
/** @param {URL} url */
|
|
966
|
+
function is_external_url(url) {
|
|
967
|
+
return url.origin !== location.origin || !url.pathname.startsWith(base);
|
|
968
|
+
}
|
|
969
|
+
|
|
963
970
|
/**
|
|
964
971
|
* @param {{
|
|
965
972
|
* url: URL;
|
|
@@ -1154,6 +1161,7 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
1154
1161
|
const trigger_prefetch = (event) => {
|
|
1155
1162
|
const { url, options } = find_anchor(event);
|
|
1156
1163
|
if (url && options.prefetch === '') {
|
|
1164
|
+
if (is_external_url(url)) return;
|
|
1157
1165
|
prefetch(url);
|
|
1158
1166
|
}
|
|
1159
1167
|
};
|
|
@@ -2,7 +2,7 @@ import { hash } from '../hash.js';
|
|
|
2
2
|
|
|
3
3
|
let loading = 0;
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const native_fetch = window.fetch;
|
|
6
6
|
|
|
7
7
|
export function lock_fetch() {
|
|
8
8
|
loading += 1;
|
|
@@ -33,15 +33,40 @@ if (import.meta.env.DEV) {
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const method = input instanceof Request ? input.method : init?.method || 'GET';
|
|
37
|
+
|
|
38
|
+
if (method !== 'GET') {
|
|
39
|
+
const url = new URL(input instanceof Request ? input.url : input.toString(), document.baseURI)
|
|
40
|
+
.href;
|
|
41
|
+
cache.delete(url);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return native_fetch(input, init);
|
|
45
|
+
};
|
|
46
|
+
} else {
|
|
47
|
+
window.fetch = (input, init) => {
|
|
48
|
+
const method = input instanceof Request ? input.method : init?.method || 'GET';
|
|
49
|
+
|
|
50
|
+
if (method !== 'GET') {
|
|
51
|
+
const url = new URL(input instanceof Request ? input.url : input.toString(), document.baseURI)
|
|
52
|
+
.href;
|
|
53
|
+
cache.delete(url);
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
return native_fetch(input, init);
|
|
37
57
|
};
|
|
38
58
|
}
|
|
39
59
|
|
|
60
|
+
const cache = new Map();
|
|
61
|
+
|
|
40
62
|
/**
|
|
63
|
+
* Should be called on the initial run of load functions that hydrate the page.
|
|
64
|
+
* Saves any requests with cache-control max-age to the cache.
|
|
41
65
|
* @param {RequestInfo} resource
|
|
66
|
+
* @param {string} resolved
|
|
42
67
|
* @param {RequestInit} [opts]
|
|
43
68
|
*/
|
|
44
|
-
export function initial_fetch(resource, opts) {
|
|
69
|
+
export function initial_fetch(resource, resolved, opts) {
|
|
45
70
|
const url = JSON.stringify(typeof resource === 'string' ? resource : resource.url);
|
|
46
71
|
|
|
47
72
|
let selector = `script[data-sveltekit-fetched][data-url=${url}]`;
|
|
@@ -51,10 +76,32 @@ export function initial_fetch(resource, opts) {
|
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
const script = document.querySelector(selector);
|
|
54
|
-
if (script
|
|
79
|
+
if (script?.textContent) {
|
|
55
80
|
const { body, ...init } = JSON.parse(script.textContent);
|
|
81
|
+
|
|
82
|
+
const ttl = script.getAttribute('data-ttl');
|
|
83
|
+
if (ttl) cache.set(resolved, { body, init, ttl: 1000 * Number(ttl) });
|
|
84
|
+
|
|
56
85
|
return Promise.resolve(new Response(body, init));
|
|
57
86
|
}
|
|
58
87
|
|
|
59
88
|
return native_fetch(resource, opts);
|
|
60
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Tries to get the response from the cache, if max-age allows it, else does a fetch.
|
|
93
|
+
* @param {string} resolved
|
|
94
|
+
* @param {RequestInit} [opts]
|
|
95
|
+
*/
|
|
96
|
+
export function subsequent_fetch(resolved, opts) {
|
|
97
|
+
const cached = cache.get(resolved);
|
|
98
|
+
if (cached) {
|
|
99
|
+
if (performance.now() < cached.ttl) {
|
|
100
|
+
return new Response(cached.body, cached.init);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
cache.delete(resolved);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return native_fetch(resolved, opts);
|
|
107
|
+
}
|
|
@@ -18,6 +18,21 @@ const default_transform = ({ html }) => html;
|
|
|
18
18
|
export async function respond(request, options, state) {
|
|
19
19
|
let url = new URL(request.url);
|
|
20
20
|
|
|
21
|
+
if (options.csrf.check_origin) {
|
|
22
|
+
const type = request.headers.get('content-type')?.split(';')[0];
|
|
23
|
+
|
|
24
|
+
const forbidden =
|
|
25
|
+
request.method === 'POST' &&
|
|
26
|
+
request.headers.get('origin') !== url.origin &&
|
|
27
|
+
(type === 'application/x-www-form-urlencoded' || type === 'multipart/form-data');
|
|
28
|
+
|
|
29
|
+
if (forbidden) {
|
|
30
|
+
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
|
|
31
|
+
status: 403
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
const { parameter, allowed } = options.method_override;
|
|
22
37
|
const method_override = url.searchParams.get(parameter)?.toUpperCase();
|
|
23
38
|
|
|
@@ -284,7 +284,7 @@ export async function render_response({
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
if (page_config.ssr && page_config.csr) {
|
|
287
|
-
body += `\n\t${fetched.map(serialize_data).join('\n\t')}`;
|
|
287
|
+
body += `\n\t${fetched.map((item) => serialize_data(item, !!state.prerendering)).join('\n\t')}`;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
if (options.service_worker) {
|
|
@@ -35,10 +35,11 @@ const pattern = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g');
|
|
|
35
35
|
* and that the resulting string isn't further modified.
|
|
36
36
|
*
|
|
37
37
|
* @param {import('./types.js').Fetched} fetched
|
|
38
|
+
* @param {boolean} [prerendering]
|
|
38
39
|
* @returns {string} The raw HTML of a script element carrying the JSON payload.
|
|
39
40
|
* @example const html = serialize_data('/data.json', null, { foo: 'bar' });
|
|
40
41
|
*/
|
|
41
|
-
export function serialize_data(fetched) {
|
|
42
|
+
export function serialize_data(fetched, prerendering = false) {
|
|
42
43
|
const safe_payload = JSON.stringify(fetched.response).replace(
|
|
43
44
|
pattern,
|
|
44
45
|
(match) => replacements[match]
|
|
@@ -54,5 +55,18 @@ export function serialize_data(fetched) {
|
|
|
54
55
|
attrs.push(`data-hash=${escape_html_attr(hash(fetched.body))}`);
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
if (!prerendering && fetched.method === 'GET') {
|
|
59
|
+
const cache_control = /** @type {string} */ (fetched.response.headers['cache-control']);
|
|
60
|
+
if (cache_control) {
|
|
61
|
+
const match = /s-maxage=(\d+)/g.exec(cache_control) ?? /max-age=(\d+)/g.exec(cache_control);
|
|
62
|
+
if (match) {
|
|
63
|
+
const age = /** @type {string} */ (fetched.response.headers['age']) ?? '0';
|
|
64
|
+
|
|
65
|
+
const ttl = +match[1] - +age;
|
|
66
|
+
attrs.push(`data-ttl="${ttl}"`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
return `<script ${attrs.join(' ')}>${safe_payload}</script>`;
|
|
58
72
|
}
|
package/types/ambient.d.ts
CHANGED
package/types/index.d.ts
CHANGED
package/types/internal.d.ts
CHANGED
|
@@ -290,6 +290,9 @@ export type SSRNodeLoader = () => Promise<SSRNode>;
|
|
|
290
290
|
|
|
291
291
|
export interface SSROptions {
|
|
292
292
|
csp: ValidatedConfig['kit']['csp'];
|
|
293
|
+
csrf: {
|
|
294
|
+
check_origin: boolean;
|
|
295
|
+
};
|
|
293
296
|
dev: boolean;
|
|
294
297
|
get_stack: (error: Error) => string | undefined;
|
|
295
298
|
handle_error(error: Error & { frame?: string }, event: RequestEvent): void;
|