@sveltejs/kit 1.0.0-next.460 → 1.0.0-next.463
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/exports/vite/build/build_server.js +3 -0
- package/src/exports/vite/dev/index.js +3 -0
- package/src/runtime/app/navigation.js +1 -0
- package/src/runtime/client/client.js +39 -24
- package/src/runtime/client/fetcher.js +50 -3
- package/src/runtime/client/types.d.ts +2 -0
- 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/src/runtime/server/utils.js +2 -2
- package/types/ambient.d.ts +19 -3
- 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.463",
|
|
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`
|
|
@@ -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) => {
|
|
@@ -16,6 +16,7 @@ export const disableScrollHandling = ssr
|
|
|
16
16
|
: client.disable_scroll_handling;
|
|
17
17
|
export const goto = ssr ? guard('goto') : client.goto;
|
|
18
18
|
export const invalidate = ssr ? guard('invalidate') : client.invalidate;
|
|
19
|
+
export const invalidateAll = ssr ? guard('invalidateAll') : client.invalidateAll;
|
|
19
20
|
export const prefetch = ssr ? guard('prefetch') : client.prefetch;
|
|
20
21
|
export const prefetchRoutes = ssr ? guard('prefetchRoutes') : client.prefetch_routes;
|
|
21
22
|
export const beforeNavigate = ssr ? () => {} : client.before_navigate;
|
|
@@ -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
|
|
|
@@ -70,7 +70,7 @@ function check_for_removed_attributes() {
|
|
|
70
70
|
* @returns {import('./types').Client}
|
|
71
71
|
*/
|
|
72
72
|
export function create_client({ target, base, trailing_slash }) {
|
|
73
|
-
/** @type {Array<((
|
|
73
|
+
/** @type {Array<((url: URL) => boolean)>} */
|
|
74
74
|
const invalidated = [];
|
|
75
75
|
|
|
76
76
|
/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
|
|
@@ -103,6 +103,7 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
103
103
|
|
|
104
104
|
/** @type {Promise<void> | null} */
|
|
105
105
|
let invalidating = null;
|
|
106
|
+
let force_invalidation = false;
|
|
106
107
|
|
|
107
108
|
/** @type {import('svelte').SvelteComponent} */
|
|
108
109
|
let root;
|
|
@@ -139,6 +140,19 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
139
140
|
/** @type {{}} */
|
|
140
141
|
let token;
|
|
141
142
|
|
|
143
|
+
function invalidate() {
|
|
144
|
+
if (!invalidating) {
|
|
145
|
+
invalidating = Promise.resolve().then(async () => {
|
|
146
|
+
await update(new URL(location.href), []);
|
|
147
|
+
|
|
148
|
+
invalidating = null;
|
|
149
|
+
force_invalidation = false;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return invalidating;
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
/**
|
|
143
157
|
* @param {string | URL} url
|
|
144
158
|
* @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any }} opts
|
|
@@ -570,11 +584,13 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
570
584
|
}
|
|
571
585
|
|
|
572
586
|
// we must fixup relative urls so they are resolved from the target page
|
|
573
|
-
const
|
|
574
|
-
depends(
|
|
587
|
+
const resolved = new URL(requested, url).href;
|
|
588
|
+
depends(resolved);
|
|
575
589
|
|
|
576
|
-
// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be
|
|
577
|
-
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);
|
|
578
594
|
},
|
|
579
595
|
setHeaders: () => {}, // noop
|
|
580
596
|
depends,
|
|
@@ -639,6 +655,8 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
639
655
|
* @param {{ url: boolean, params: string[] }} changed
|
|
640
656
|
*/
|
|
641
657
|
function has_changed(changed, parent_changed, uses) {
|
|
658
|
+
if (force_invalidation) return true;
|
|
659
|
+
|
|
642
660
|
if (!uses) return false;
|
|
643
661
|
|
|
644
662
|
if (uses.parent && parent_changed) return true;
|
|
@@ -648,8 +666,8 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
648
666
|
if (uses.params.has(param)) return true;
|
|
649
667
|
}
|
|
650
668
|
|
|
651
|
-
for (const
|
|
652
|
-
if (invalidated.some((fn) => fn(
|
|
669
|
+
for (const href of uses.dependencies) {
|
|
670
|
+
if (invalidated.some((fn) => fn(new URL(href)))) return true;
|
|
653
671
|
}
|
|
654
672
|
|
|
655
673
|
return false;
|
|
@@ -1057,28 +1075,25 @@ export function create_client({ target, base, trailing_slash }) {
|
|
|
1057
1075
|
|
|
1058
1076
|
invalidate: (resource) => {
|
|
1059
1077
|
if (resource === undefined) {
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1078
|
+
// TODO remove for 1.0
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
'`invalidate()` (with no arguments) has been replaced by `invalidateAll()`'
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (typeof resource === 'function') {
|
|
1067
1085
|
invalidated.push(resource);
|
|
1068
1086
|
} else {
|
|
1069
1087
|
const { href } = new URL(resource, location.href);
|
|
1070
|
-
invalidated.push((
|
|
1088
|
+
invalidated.push((url) => url.href === href);
|
|
1071
1089
|
}
|
|
1072
1090
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
await update(new URL(location.href), []);
|
|
1076
|
-
|
|
1077
|
-
invalidating = null;
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1091
|
+
return invalidate();
|
|
1092
|
+
},
|
|
1080
1093
|
|
|
1081
|
-
|
|
1094
|
+
invalidateAll: () => {
|
|
1095
|
+
force_invalidation = true;
|
|
1096
|
+
return invalidate();
|
|
1082
1097
|
},
|
|
1083
1098
|
|
|
1084
1099
|
prefetch: async (href) => {
|
|
@@ -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
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
beforeNavigate,
|
|
4
4
|
goto,
|
|
5
5
|
invalidate,
|
|
6
|
+
invalidateAll,
|
|
6
7
|
prefetch,
|
|
7
8
|
prefetchRoutes
|
|
8
9
|
} from '$app/navigation';
|
|
@@ -17,6 +18,7 @@ export interface Client {
|
|
|
17
18
|
disable_scroll_handling: () => void;
|
|
18
19
|
goto: typeof goto;
|
|
19
20
|
invalidate: typeof invalidate;
|
|
21
|
+
invalidateAll: typeof invalidateAll;
|
|
20
22
|
prefetch: typeof prefetch;
|
|
21
23
|
prefetch_routes: typeof prefetchRoutes;
|
|
22
24
|
|
|
@@ -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
|
}
|
|
@@ -194,8 +194,8 @@ export function handle_fatal_error(event, options, error) {
|
|
|
194
194
|
|
|
195
195
|
// ideally we'd use sec-fetch-dest instead, but Safari — quelle surprise — doesn't support it
|
|
196
196
|
const type = negotiate(event.request.headers.get('accept') || 'text/html', [
|
|
197
|
-
'
|
|
198
|
-
'
|
|
197
|
+
'application/json',
|
|
198
|
+
'text/html'
|
|
199
199
|
]);
|
|
200
200
|
|
|
201
201
|
if (event.url.pathname.endsWith(DATA_SUFFIX) || type === 'application/json') {
|
package/types/ambient.d.ts
CHANGED
|
@@ -88,6 +88,7 @@ declare module '$app/environment' {
|
|
|
88
88
|
* disableScrollHandling,
|
|
89
89
|
* goto,
|
|
90
90
|
* invalidate,
|
|
91
|
+
* invalidateAll,
|
|
91
92
|
* prefetch,
|
|
92
93
|
* prefetchRoutes
|
|
93
94
|
* } from '$app/navigation';
|
|
@@ -113,10 +114,25 @@ declare module '$app/navigation' {
|
|
|
113
114
|
opts?: { replaceState?: boolean; noscroll?: boolean; keepfocus?: boolean; state?: any }
|
|
114
115
|
): Promise<void>;
|
|
115
116
|
/**
|
|
116
|
-
* Causes any `load` functions belonging to the currently active page to re-run if they
|
|
117
|
-
*
|
|
117
|
+
* Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated.
|
|
118
|
+
*
|
|
119
|
+
* If the argument is given as a `string` or `URL`, it must resolve to the same URL that was passed to `fetch` or `depends` (including query parameters).
|
|
120
|
+
* To create a custom identifier, use a string beginning with `[a-z]+:` (e.g. `custom:state`) — this is a valid URL.
|
|
121
|
+
*
|
|
122
|
+
* The `function` argument can be used define a custom predicate. It receives the full `URL` and causes `load` to rerun if `true` is returned.
|
|
123
|
+
* This can be useful if you want to invalidate based on a pattern instead of a exact match.
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* // Example: Match '/path' regardless of the query parameters
|
|
127
|
+
* invalidate((url) => url.pathname === '/path');
|
|
128
|
+
* ```
|
|
129
|
+
* @param url The invalidated URL
|
|
130
|
+
*/
|
|
131
|
+
export function invalidate(url: string | URL | ((url: URL) => boolean)): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Causes all `load` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated.
|
|
118
134
|
*/
|
|
119
|
-
export function
|
|
135
|
+
export function invalidateAll(): Promise<void>;
|
|
120
136
|
/**
|
|
121
137
|
* Programmatically prefetches the given page, which means
|
|
122
138
|
* 1. ensuring that the code for the page is loaded, and
|
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;
|