@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "1.0.0-next.461",
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.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`
@@ -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
- id: ${s(route.id)},
64
- pattern: ${route.pattern},
65
- names: ${s(route.names)},
66
- types: ${s(route.types)},
67
- page: ${s(route.page)},
68
- endpoint: ${route.endpoint ? loader(`${relative_path}/${build_data.server.vite_manifest[route.endpoint.file].file}`) : 'null'}
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, 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
 
@@ -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 normalized = new URL(requested, url).href;
588
- depends(normalized);
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 normalized
591
- 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);
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.origin !== location.origin || !url.pathname.startsWith(base)) return;
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
- 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
+ }
@@ -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;
@@ -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';
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;