@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "1.0.0-next.460",
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.3",
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, native_fetch } from './fetcher.js';
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<((href: string) => boolean)>} */
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 normalized = new URL(requested, url).href;
574
- depends(normalized);
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 normalized
577
- return started ? native_fetch(normalized, init) : initial_fetch(requested, init);
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 dep of uses.dependencies) {
652
- if (invalidated.some((fn) => fn(dep))) return true;
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
- // Force rerun of all load functions, regardless of their dependencies
1061
- for (const node of current.branch) {
1062
- node?.server?.uses.dependencies.add('');
1063
- node?.shared?.uses.dependencies.add('');
1064
- }
1065
- invalidated.push(() => true);
1066
- } else if (typeof resource === 'function') {
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((dep) => dep === href);
1088
+ invalidated.push((url) => url.href === href);
1071
1089
  }
1072
1090
 
1073
- if (!invalidating) {
1074
- invalidating = Promise.resolve().then(async () => {
1075
- await update(new URL(location.href), []);
1076
-
1077
- invalidating = null;
1078
- });
1079
- }
1091
+ return invalidate();
1092
+ },
1080
1093
 
1081
- return invalidating;
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
- export const native_fetch = window.fetch;
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 && script.textContent) {
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
 
@@ -214,6 +214,7 @@ export function create_fetch({ event, options, state, route, prerender_default }
214
214
 
215
215
  fetched.push({
216
216
  url: requested,
217
+ method: opts.method || 'GET',
217
218
  body: opts.body,
218
219
  response: {
219
220
  status: status_number,
@@ -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
  }
@@ -3,6 +3,7 @@ import { HttpError } from '../../control.js';
3
3
 
4
4
  export interface Fetched {
5
5
  url: string;
6
+ method: string;
6
7
  body?: string | null;
7
8
  response: {
8
9
  status: number;
@@ -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
- 'text/html',
198
- 'application/json'
197
+ 'application/json',
198
+ 'text/html'
199
199
  ]);
200
200
 
201
201
  if (event.url.pathname.endsWith(DATA_SUFFIX) || type === 'application/json') {
@@ -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 `fetch` the resource in question. If no argument is given, all resources will be invalidated. Returns a `Promise` that resolves when the page is subsequently updated.
117
- * @param dependency The invalidated resource
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 invalidate(dependency?: string | ((href: string) => boolean)): Promise<void>;
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
@@ -128,6 +128,9 @@ export interface KitConfig {
128
128
  directives?: CspDirectives;
129
129
  reportOnly?: CspDirectives;
130
130
  };
131
+ csrf?: {
132
+ checkOrigin?: boolean;
133
+ };
131
134
  env?: {
132
135
  dir?: string;
133
136
  publicPrefix?: string;
@@ -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;