@sveltejs/kit 1.0.0-next.471 → 1.0.0-next.472

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.
@@ -145,7 +145,7 @@ export function create_client({ target, base, trailing_slash }) {
145
145
  const url = new URL(location.href);
146
146
 
147
147
  invalidating = Promise.resolve().then(async () => {
148
- const intent = get_navigation_intent(url);
148
+ const intent = get_navigation_intent(url, true);
149
149
  await update(intent, url, []);
150
150
 
151
151
  invalidating = null;
@@ -187,7 +187,7 @@ export function create_client({ target, base, trailing_slash }) {
187
187
 
188
188
  /** @param {URL} url */
189
189
  async function prefetch(url) {
190
- const intent = get_navigation_intent(url);
190
+ const intent = get_navigation_intent(url, false);
191
191
 
192
192
  if (!intent) {
193
193
  throw new Error('Attempted to prefetch a URL that does not belong to this app');
@@ -278,24 +278,9 @@ export function create_client({ target, base, trailing_slash }) {
278
278
  navigation_result.props.page.url = url;
279
279
  }
280
280
 
281
- if (import.meta.env.DEV) {
282
- // Nasty hack to silence harmless warnings the user can do nothing about
283
- const warn = console.warn;
284
- console.warn = (...args) => {
285
- if (
286
- args.length !== 1 ||
287
- !/<(Layout|Page)(_[\w$]+)?> was created with unknown prop '(data|errors)'/.test(args[0])
288
- ) {
289
- warn(...args);
290
- }
291
- };
292
- root.$set(navigation_result.props);
293
- tick().then(() => (console.warn = warn));
294
-
295
- check_for_removed_attributes();
296
- } else {
297
- root.$set(navigation_result.props);
298
- }
281
+ const post_update = pre_update();
282
+ root.$set(navigation_result.props);
283
+ post_update();
299
284
  } else {
300
285
  initialize(navigation_result);
301
286
  }
@@ -371,32 +356,13 @@ export function create_client({ target, base, trailing_slash }) {
371
356
 
372
357
  page = result.props.page;
373
358
 
374
- if (import.meta.env.DEV) {
375
- // Nasty hack to silence harmless warnings the user can do nothing about
376
- const warn = console.warn;
377
- console.warn = (...args) => {
378
- if (
379
- args.length !== 1 ||
380
- !/<(Layout|Page)(_[\w$]+)?> was created with unknown prop '(data|errors)'/.test(args[0])
381
- ) {
382
- warn(...args);
383
- }
384
- };
385
- root = new Root({
386
- target,
387
- props: { ...result.props, stores },
388
- hydrate: true
389
- });
390
- console.warn = warn;
391
-
392
- check_for_removed_attributes();
393
- } else {
394
- root = new Root({
395
- target,
396
- props: { ...result.props, stores },
397
- hydrate: true
398
- });
399
- }
359
+ const post_update = pre_update();
360
+ root = new Root({
361
+ target,
362
+ props: { ...result.props, stores },
363
+ hydrate: true
364
+ });
365
+ post_update();
400
366
 
401
367
  /** @type {import('types').Navigation} */
402
368
  const navigation = {
@@ -422,7 +388,7 @@ export function create_client({ target, base, trailing_slash }) {
422
388
  * status: number;
423
389
  * error: HttpError | Error | null;
424
390
  * route: import('types').CSRRoute | null;
425
- * validation_errors?: Record<string, any> | null;
391
+ * form?: Record<string, any> | null;
426
392
  * }} opts
427
393
  */
428
394
  async function get_navigation_result_from_branch({
@@ -432,7 +398,7 @@ export function create_client({ target, base, trailing_slash }) {
432
398
  status,
433
399
  error,
434
400
  route,
435
- validation_errors
401
+ form
436
402
  }) {
437
403
  const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
438
404
 
@@ -448,11 +414,14 @@ export function create_client({ target, base, trailing_slash }) {
448
414
  session_id
449
415
  },
450
416
  props: {
451
- components: filtered.map((branch_node) => branch_node.node.component),
452
- errors: validation_errors
417
+ components: filtered.map((branch_node) => branch_node.node.component)
453
418
  }
454
419
  };
455
420
 
421
+ if (form !== undefined) {
422
+ result.props.form = form;
423
+ }
424
+
456
425
  let data = {};
457
426
  let data_changed = !page;
458
427
  for (let i = 0; i < filtered.length; i += 1) {
@@ -713,7 +682,7 @@ export function create_client({ target, base, trailing_slash }) {
713
682
  * @param {import('./types').NavigationIntent} intent
714
683
  * @returns {Promise<import('./types').NavigationResult | undefined>}
715
684
  */
716
- async function load_route({ id, url, params, route }) {
685
+ async function load_route({ id, invalidating, url, params, route }) {
717
686
  if (load_cache.id === id && load_cache.promise) {
718
687
  return load_cache.promise;
719
688
  }
@@ -881,7 +850,9 @@ export function create_client({ target, base, trailing_slash }) {
881
850
  branch,
882
851
  status: 200,
883
852
  error: null,
884
- route
853
+ route,
854
+ // Reset `form` on navigation, but not invalidation
855
+ form: invalidating ? undefined : null
885
856
  });
886
857
  }
887
858
 
@@ -954,8 +925,11 @@ export function create_client({ target, base, trailing_slash }) {
954
925
  });
955
926
  }
956
927
 
957
- /** @param {URL} url */
958
- function get_navigation_intent(url) {
928
+ /**
929
+ * @param {URL} url
930
+ * @param {boolean} invalidating
931
+ */
932
+ function get_navigation_intent(url, invalidating) {
959
933
  if (is_external_url(url)) return;
960
934
 
961
935
  const path = decodeURI(url.pathname.slice(base.length) || '/');
@@ -969,7 +943,7 @@ export function create_client({ target, base, trailing_slash }) {
969
943
  );
970
944
  const id = normalized.pathname + normalized.search;
971
945
  /** @type {import('./types').NavigationIntent} */
972
- const intent = { id, route, params: decode_params(params), url: normalized };
946
+ const intent = { id, invalidating, route, params: decode_params(params), url: normalized };
973
947
  return intent;
974
948
  }
975
949
  }
@@ -1009,7 +983,7 @@ export function create_client({ target, base, trailing_slash }) {
1009
983
  }) {
1010
984
  let should_block = false;
1011
985
 
1012
- const intent = get_navigation_intent(url);
986
+ const intent = get_navigation_intent(url, false);
1013
987
 
1014
988
  /** @type {import('types').Navigation} */
1015
989
  const navigation = {
@@ -1161,6 +1135,71 @@ export function create_client({ target, base, trailing_slash }) {
1161
1135
  await Promise.all(promises);
1162
1136
  },
1163
1137
 
1138
+ apply_action: async (result) => {
1139
+ if (result.type === 'error') {
1140
+ const url = new URL(location.href);
1141
+
1142
+ const { branch, route } = current;
1143
+ if (!route) return;
1144
+
1145
+ let i = current.branch.length;
1146
+
1147
+ while (i--) {
1148
+ if (route.errors[i]) {
1149
+ /** @type {import('./types').BranchNode | undefined} */
1150
+ let error_loaded;
1151
+
1152
+ let j = i;
1153
+ while (!branch[j]) j -= 1;
1154
+ try {
1155
+ error_loaded = {
1156
+ node: await /** @type {import('types').CSRPageNodeLoader } */ (route.errors[i])(),
1157
+ loader: /** @type {import('types').CSRPageNodeLoader } */ (route.errors[i]),
1158
+ data: {},
1159
+ server: null,
1160
+ shared: null
1161
+ };
1162
+
1163
+ const navigation_result = await get_navigation_result_from_branch({
1164
+ url,
1165
+ params: current.params,
1166
+ branch: branch.slice(0, j + 1).concat(error_loaded),
1167
+ status: 500, // TODO might not be 500?
1168
+ error: result.error,
1169
+ route
1170
+ });
1171
+
1172
+ current = navigation_result.state;
1173
+
1174
+ const post_update = pre_update();
1175
+ root.$set(navigation_result.props);
1176
+ post_update();
1177
+
1178
+ return;
1179
+ } catch (e) {
1180
+ continue;
1181
+ }
1182
+ }
1183
+ }
1184
+ } else if (result.type === 'redirect') {
1185
+ goto(result.location, {}, []);
1186
+ } else {
1187
+ /** @type {Record<string, any>} */
1188
+ const props = { form: result.data };
1189
+
1190
+ if (result.status !== page.status) {
1191
+ props.page = {
1192
+ ...page,
1193
+ status: result.status
1194
+ };
1195
+ }
1196
+
1197
+ const post_update = pre_update();
1198
+ root.$set(props);
1199
+ post_update();
1200
+ }
1201
+ },
1202
+
1164
1203
  _start_router: () => {
1165
1204
  history.scrollRestoration = 'manual';
1166
1205
 
@@ -1361,7 +1400,7 @@ export function create_client({ target, base, trailing_slash }) {
1361
1400
  params,
1362
1401
  routeId,
1363
1402
  data: server_data_nodes,
1364
- errors: validation_errors
1403
+ form
1365
1404
  }) => {
1366
1405
  const url = new URL(location.href);
1367
1406
 
@@ -1402,7 +1441,7 @@ export function create_client({ target, base, trailing_slash }) {
1402
1441
  original_error.message
1403
1442
  )
1404
1443
  : original_error,
1405
- validation_errors,
1444
+ form,
1406
1445
  route: routes.find((route) => route.id === routeId) ?? null
1407
1446
  });
1408
1447
  } catch (e) {
@@ -1492,3 +1531,28 @@ function add_url_properties(type, target) {
1492
1531
 
1493
1532
  return target;
1494
1533
  }
1534
+
1535
+ function pre_update() {
1536
+ if (__SVELTEKIT_DEV__) {
1537
+ // Nasty hack to silence harmless warnings the user can do nothing about
1538
+ const warn = console.warn;
1539
+ console.warn = (...args) => {
1540
+ if (
1541
+ args.length === 1 &&
1542
+ /<(Layout|Page)(_[\w$]+)?> was created (with unknown|without expected) prop '(data|form)'/.test(
1543
+ args[0]
1544
+ )
1545
+ ) {
1546
+ return;
1547
+ }
1548
+ warn(...args);
1549
+ };
1550
+
1551
+ return () => {
1552
+ tick().then(() => (console.warn = warn));
1553
+ check_for_removed_attributes();
1554
+ };
1555
+ }
1556
+
1557
+ return () => {};
1558
+ }
@@ -6,15 +6,7 @@ import { set_public_env } from '../env-public.js';
6
6
  /**
7
7
  * @param {{
8
8
  * env: Record<string, string>;
9
- * hydrate: {
10
- * status: number;
11
- * error: Error | (import('../server/page/types').SerializedHttpError);
12
- * node_ids: number[];
13
- * params: Record<string, string>;
14
- * routeId: string | null;
15
- * data: Array<import('types').ServerDataNode | null>;
16
- * errors: Record<string, any> | null;
17
- * };
9
+ * hydrate: Parameters<import('./types').Client['_hydrate']>[0];
18
10
  * paths: {
19
11
  * assets: string;
20
12
  * base: string;
@@ -1,3 +1,4 @@
1
+ import { applyAction } from '$app/forms';
1
2
  import {
2
3
  afterNavigate,
3
4
  beforeNavigate,
@@ -21,6 +22,7 @@ export interface Client {
21
22
  invalidateAll: typeof invalidateAll;
22
23
  prefetch: typeof prefetch;
23
24
  prefetch_routes: typeof prefetchRoutes;
25
+ apply_action: typeof applyAction;
24
26
 
25
27
  // private API
26
28
  _hydrate: (opts: {
@@ -30,27 +32,21 @@ export interface Client {
30
32
  params: Record<string, string>;
31
33
  routeId: string | null;
32
34
  data: Array<import('types').ServerDataNode | null>;
33
- errors: Record<string, any> | null;
35
+ form: Record<string, any> | null;
34
36
  }) => Promise<void>;
35
37
  _start_router: () => void;
36
38
  }
37
39
 
38
40
  export type NavigationIntent = {
39
- /**
40
- * `url.pathname + url.search`
41
- */
41
+ /** `url.pathname + url.search` */
42
42
  id: string;
43
- /**
44
- * The route parameters
45
- */
43
+ /** Whether we are invalidating or navigating */
44
+ invalidating: boolean;
45
+ /** The route parameters */
46
46
  params: Record<string, string>;
47
- /**
48
- * The route that matches `path`
49
- */
47
+ /** The route that matches `path` */
50
48
  route: CSRRoute;
51
- /**
52
- * The destination URL
53
- */
49
+ /** The destination URL */
54
50
  url: URL;
55
51
  };
56
52
 
@@ -31,3 +31,70 @@ export class Redirect {
31
31
  this.location = location;
32
32
  }
33
33
  }
34
+
35
+ /**
36
+ * @template {Record<string, unknown> | undefined} [T=undefined]
37
+ */
38
+ export class ValidationError {
39
+ /**
40
+ * @param {number} status
41
+ * @param {T} [data]
42
+ */
43
+ constructor(status, data) {
44
+ this.status = status;
45
+ this.data = data;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Creates an `HttpError` object with an HTTP status code and an optional message.
51
+ * This object, if thrown during request handling, will cause SvelteKit to
52
+ * return an error response without invoking `handleError`
53
+ * @param {number} status
54
+ * @param {string | undefined} [message]
55
+ */
56
+ export function error(status, message) {
57
+ return new HttpError(status, message);
58
+ }
59
+
60
+ /**
61
+ * Creates a `Redirect` object. If thrown during request handling, SvelteKit will
62
+ * return a redirect response.
63
+ * @param {number} status
64
+ * @param {string} location
65
+ */
66
+ export function redirect(status, location) {
67
+ if (isNaN(status) || status < 300 || status > 399) {
68
+ throw new Error('Invalid status code');
69
+ }
70
+
71
+ return new Redirect(status, location);
72
+ }
73
+
74
+ /**
75
+ * Generates a JSON `Response` object from the supplied data.
76
+ * @param {any} data
77
+ * @param {ResponseInit} [init]
78
+ */
79
+ export function json(data, init) {
80
+ // TODO deprecate this in favour of `Response.json` when it's
81
+ // more widely supported
82
+ const headers = new Headers(init?.headers);
83
+ if (!headers.has('content-type')) {
84
+ headers.set('content-type', 'application/json');
85
+ }
86
+
87
+ return new Response(JSON.stringify(data), {
88
+ ...init,
89
+ headers
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Generates a `ValidationError` object.
95
+ * @param {number} status
96
+ * @param {Record<string, any> | undefined} [data]
97
+ */
98
+ export function invalid(status, data) {
99
+ return new ValidationError(status, data);
100
+ }
@@ -0,0 +1,76 @@
1
+ import * as cookie from 'cookie';
2
+
3
+ /**
4
+ * @param {Request} request
5
+ * @param {URL} url
6
+ */
7
+ export function get_cookies(request, url) {
8
+ const initial_cookies = cookie.parse(request.headers.get('cookie') ?? '');
9
+
10
+ /** @type {Array<{ name: string, value: string, options: import('cookie').CookieSerializeOptions }>} */
11
+ const new_cookies = [];
12
+
13
+ /** @type {import('types').Cookies} */
14
+ const cookies = {
15
+ get(name, opts) {
16
+ const decode = opts?.decode || decodeURIComponent;
17
+
18
+ let i = new_cookies.length;
19
+ while (i--) {
20
+ const cookie = new_cookies[i];
21
+
22
+ if (
23
+ cookie.name === name &&
24
+ domain_matches(url.hostname, cookie.options.domain) &&
25
+ path_matches(url.pathname, cookie.options.path)
26
+ ) {
27
+ return cookie.value;
28
+ }
29
+ }
30
+
31
+ return name in initial_cookies ? decode(initial_cookies[name]) : undefined;
32
+ },
33
+ set(name, value, options = {}) {
34
+ new_cookies.push({
35
+ name,
36
+ value,
37
+ options: {
38
+ httpOnly: true,
39
+ secure: true,
40
+ ...options
41
+ }
42
+ });
43
+ },
44
+ delete(name) {
45
+ new_cookies.push({ name, value: '', options: { expires: new Date(0) } });
46
+ }
47
+ };
48
+
49
+ return { cookies, new_cookies };
50
+ }
51
+
52
+ /**
53
+ * @param {string} hostname
54
+ * @param {string} [constraint]
55
+ */
56
+ export function domain_matches(hostname, constraint) {
57
+ if (!constraint) return true;
58
+
59
+ const normalized = constraint[0] === '.' ? constraint.slice(1) : constraint;
60
+
61
+ if (hostname === normalized) return true;
62
+ return hostname.endsWith('.' + normalized);
63
+ }
64
+
65
+ /**
66
+ * @param {string} path
67
+ * @param {string} [constraint]
68
+ */
69
+ export function path_matches(path, constraint) {
70
+ if (!constraint) return true;
71
+
72
+ const normalized = constraint.endsWith('/') ? constraint.slice(0, -1) : constraint;
73
+
74
+ if (path === normalized) return true;
75
+ return path.startsWith(normalized + '/');
76
+ }
@@ -1,4 +1,5 @@
1
- import { Redirect } from '../control.js';
1
+ import { json } from '../../exports/index.js';
2
+ import { Redirect, ValidationError } from '../control.js';
2
3
  import { check_method_names, method_not_allowed } from './utils.js';
3
4
 
4
5
  /**
@@ -56,6 +57,8 @@ export async function render_endpoint(event, mod, state) {
56
57
  status: error.status,
57
58
  headers: { location: error.location }
58
59
  });
60
+ } else if (error instanceof ValidationError) {
61
+ return json(error.data, { status: error.status });
59
62
  }
60
63
 
61
64
  throw error;
@@ -1,3 +1,4 @@
1
+ import * as cookie from 'cookie';
1
2
  import { render_endpoint } from './endpoint.js';
2
3
  import { render_page } from './page/index.js';
3
4
  import { render_response } from './page/render.js';
@@ -8,6 +9,7 @@ import { decode_params, disable_search, normalize_path } from '../../utils/url.j
8
9
  import { exec } from '../../utils/routing.js';
9
10
  import { render_data } from './data/index.js';
10
11
  import { DATA_SUFFIX } from '../../constants.js';
12
+ import { get_cookies } from './cookie.js';
11
13
 
12
14
  /* global __SVELTEKIT_ADAPTER_NAME__ */
13
15
 
@@ -35,31 +37,6 @@ export async function respond(request, options, state) {
35
37
  }
36
38
  }
37
39
 
38
- const { parameter, allowed } = options.method_override;
39
- const method_override = url.searchParams.get(parameter)?.toUpperCase();
40
-
41
- if (method_override) {
42
- if (request.method === 'POST') {
43
- if (allowed.includes(method_override)) {
44
- request = new Proxy(request, {
45
- get: (target, property, _receiver) => {
46
- if (property === 'method') return method_override;
47
- return Reflect.get(target, property, target);
48
- }
49
- });
50
- } else {
51
- const verb = allowed.length === 0 ? 'enabled' : 'allowed';
52
- const body = `${parameter}=${method_override} is not ${verb}. See https://kit.svelte.dev/docs/configuration#methodoverride`;
53
-
54
- return new Response(body, {
55
- status: 400
56
- });
57
- }
58
- } else {
59
- throw new Error(`${parameter}=${method_override} is only allowed with POST requests`);
60
- }
61
- }
62
-
63
40
  let decoded;
64
41
  try {
65
42
  decoded = decodeURI(url.pathname);
@@ -116,16 +93,16 @@ export async function respond(request, options, state) {
116
93
  }
117
94
  }
118
95
 
119
- /** @type {import('types').ResponseHeaders} */
96
+ /** @type {Record<string, string>} */
120
97
  const headers = {};
121
98
 
122
- /** @type {string[]} */
123
- const cookies = [];
99
+ const { cookies, new_cookies } = get_cookies(request, url);
124
100
 
125
101
  if (state.prerendering) disable_search(url);
126
102
 
127
103
  /** @type {import('types').RequestEvent} */
128
104
  const event = {
105
+ cookies,
129
106
  getClientAddress:
130
107
  state.getClientAddress ||
131
108
  (() => {
@@ -144,15 +121,9 @@ export async function respond(request, options, state) {
144
121
  const value = new_headers[key];
145
122
 
146
123
  if (lower === 'set-cookie') {
147
- const new_cookies = /** @type {string[]} */ (Array.isArray(value) ? value : [value]);
148
-
149
- for (const cookie of new_cookies) {
150
- if (cookies.includes(cookie)) {
151
- throw new Error(`"${key}" header already has cookie with same value`);
152
- }
153
-
154
- cookies.push(cookie);
155
- }
124
+ throw new Error(
125
+ `Use \`event.cookie.set(name, value, options)\` instead of \`event.setHeaders\` to set cookies`
126
+ );
156
127
  } else if (lower in headers) {
157
128
  throw new Error(`"${key}" header is already set`);
158
129
  } else {
@@ -244,7 +215,6 @@ export async function respond(request, options, state) {
244
215
  error: null,
245
216
  branch: [],
246
217
  fetched: [],
247
- validation_errors: undefined,
248
218
  cookies: [],
249
219
  resolve_opts
250
220
  });
@@ -275,8 +245,11 @@ export async function respond(request, options, state) {
275
245
  }
276
246
  }
277
247
 
278
- for (const cookie of cookies) {
279
- response.headers.append('set-cookie', cookie);
248
+ for (const new_cookie of new_cookies) {
249
+ response.headers.append(
250
+ 'set-cookie',
251
+ cookie.serialize(new_cookie.name, new_cookie.value, new_cookie.options)
252
+ );
280
253
  }
281
254
 
282
255
  // respond with 304 if etag matches
@@ -338,6 +311,14 @@ export async function respond(request, options, state) {
338
311
  } catch (e) {
339
312
  const error = coalesce_to_error(e);
340
313
  return handle_fatal_error(event, options, error);
314
+ } finally {
315
+ event.cookies.set = () => {
316
+ throw new Error('Cannot use `cookies.set(...)` after the response has been generated');
317
+ };
318
+
319
+ event.setHeaders = () => {
320
+ throw new Error('Cannot use `setHeaders(...)` after the response has been generated');
321
+ };
341
322
  }
342
323
  }
343
324