@sveltejs/kit 1.0.0-next.490 → 1.0.0-next.492

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.490",
3
+ "version": "1.0.0-next.492",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -1,5 +1,6 @@
1
1
  import { s } from '../../utils/misc.js';
2
2
  import { get_mime_lookup } from '../utils.js';
3
+ import { resolve_symlinks } from '../../exports/vite/build/utils.js';
3
4
 
4
5
  /**
5
6
  * Generates the data used to write the server-side manifest.js file. This data is used in the Vite
@@ -65,7 +66,7 @@ export function generate_manifest({ build_data, relative_path, routes, format =
65
66
  names: ${s(route.names)},
66
67
  types: ${s(route.types)},
67
68
  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
+ endpoint: ${route.endpoint ? loader(`${relative_path}/${resolve_symlinks(build_data.server.vite_manifest, route.endpoint.file).chunk.file}`) : 'null'}
69
70
  }`;
70
71
  }).filter(Boolean).join(',\n\t\t\t\t')}
71
72
  ],
@@ -164,13 +164,16 @@ function create_routes_and_nodes(cwd, config, fallback) {
164
164
 
165
165
  const dir = path.join(cwd, routes_base, id);
166
166
 
167
- const files = fs.readdirSync(dir, {
168
- withFileTypes: true
169
- });
167
+ // We can't use withFileTypes because of a NodeJs bug which returns wrong results
168
+ // with isDirectory() in case of symlinks: https://github.com/nodejs/node/issues/30646
169
+ const files = fs.readdirSync(dir).map((name) => ({
170
+ is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
171
+ name
172
+ }));
170
173
 
171
174
  // process files first
172
175
  for (const file of files) {
173
- if (file.isDirectory()) continue;
176
+ if (file.is_dir) continue;
174
177
  if (!file.name.startsWith('+')) continue;
175
178
  if (!valid_extensions.find((ext) => file.name.endsWith(ext))) continue;
176
179
 
@@ -213,7 +216,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
213
216
 
214
217
  // then handle children
215
218
  for (const file of files) {
216
- if (file.isDirectory()) {
219
+ if (file.is_dir) {
217
220
  walk(depth + 1, path.posix.join(id, file.name), file.name, route);
218
221
  }
219
222
  }
@@ -1,4 +1,5 @@
1
1
  import * as set_cookie_parser from 'set-cookie-parser';
2
+ import { error } from '../index.js';
2
3
 
3
4
  /**
4
5
  * @param {import('http').IncomingMessage} req
@@ -27,7 +28,8 @@ function get_raw_body(req, body_size_limit) {
27
28
  if (!length) {
28
29
  length = body_size_limit;
29
30
  } else if (length > body_size_limit) {
30
- throw new Error(
31
+ throw error(
32
+ 413,
31
33
  `Received content-length of ${length}, but only accept up to ${body_size_limit} bytes.`
32
34
  );
33
35
  }
@@ -45,6 +47,7 @@ function get_raw_body(req, body_size_limit) {
45
47
  return new ReadableStream({
46
48
  start(controller) {
47
49
  req.on('error', (error) => {
50
+ cancelled = true;
48
51
  controller.error(error);
49
52
  });
50
53
 
@@ -58,8 +61,10 @@ function get_raw_body(req, body_size_limit) {
58
61
 
59
62
  size += chunk.length;
60
63
  if (size > length) {
61
- req.destroy(
62
- new Error(
64
+ cancelled = true;
65
+ controller.error(
66
+ error(
67
+ 413,
63
68
  `request body size exceeded ${
64
69
  content_length ? "'content-length'" : 'BODY_SIZE_LIMIT'
65
70
  } of ${length}`
@@ -4,7 +4,13 @@ import { mkdirp, posixify, resolve_entry } from '../../../utils/filesystem.js';
4
4
  import { get_vite_config, merge_vite_configs } from '../utils.js';
5
5
  import { load_error_page, load_template } from '../../../core/config/index.js';
6
6
  import { runtime_directory } from '../../../core/utils.js';
7
- import { create_build, find_deps, get_default_build_config, is_http_method } from './utils.js';
7
+ import {
8
+ create_build,
9
+ find_deps,
10
+ get_default_build_config,
11
+ is_http_method,
12
+ resolve_symlinks
13
+ } from './utils.js';
8
14
  import { s } from '../../../utils/misc.js';
9
15
 
10
16
  /**
@@ -285,7 +291,7 @@ export async function build_server(options, client) {
285
291
 
286
292
  exports.push(
287
293
  `export const component = async () => (await import('../${
288
- vite_manifest[node.component].file
294
+ resolve_symlinks(vite_manifest, node.component).chunk.file
289
295
  }')).default;`,
290
296
  `export const file = '${entry.file}';` // TODO what is this?
291
297
  );
@@ -1,3 +1,5 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import * as vite from 'vite';
2
4
  import { get_aliases } from '../utils.js';
3
5
 
@@ -44,14 +46,14 @@ export function find_deps(manifest, entry, add_dynamic_css) {
44
46
  const stylesheets = new Set();
45
47
 
46
48
  /**
47
- * @param {string} file
49
+ * @param {string} current
48
50
  * @param {boolean} add_js
49
51
  */
50
- function traverse(file, add_js) {
51
- if (seen.has(file)) return;
52
- seen.add(file);
52
+ function traverse(current, add_js) {
53
+ if (seen.has(current)) return;
54
+ seen.add(current);
53
55
 
54
- const chunk = manifest[file];
56
+ const { chunk } = resolve_symlinks(manifest, current);
55
57
 
56
58
  if (add_js) imports.add(chunk.file);
57
59
 
@@ -68,15 +70,31 @@ export function find_deps(manifest, entry, add_dynamic_css) {
68
70
  }
69
71
  }
70
72
 
71
- traverse(entry, true);
73
+ const { chunk, file } = resolve_symlinks(manifest, entry);
74
+
75
+ traverse(file, true);
72
76
 
73
77
  return {
74
- file: manifest[entry].file,
78
+ file: chunk.file,
75
79
  imports: Array.from(imports),
76
80
  stylesheets: Array.from(stylesheets)
77
81
  };
78
82
  }
79
83
 
84
+ /**
85
+ * @param {import('vite').Manifest} manifest
86
+ * @param {string} file
87
+ */
88
+ export function resolve_symlinks(manifest, file) {
89
+ while (!manifest[file]) {
90
+ file = path.relative('.', fs.realpathSync(file));
91
+ }
92
+
93
+ const chunk = manifest[file];
94
+
95
+ return { chunk, file };
96
+ }
97
+
80
98
  /**
81
99
  * The Vite configuration that we use by default.
82
100
  * @param {{
@@ -397,7 +397,7 @@ export async function dev(vite, vite_config, svelte_config) {
397
397
  });
398
398
  } catch (/** @type {any} */ err) {
399
399
  res.statusCode = err.status || 400;
400
- return res.end(err.message || 'Invalid request body');
400
+ return res.end('Invalid request body');
401
401
  }
402
402
 
403
403
  const template = load_template(cwd, svelte_config);
@@ -137,7 +137,7 @@ export async function preview(vite, vite_config, svelte_config) {
137
137
  });
138
138
  } catch (/** @type {any} */ err) {
139
139
  res.statusCode = err.status || 400;
140
- return res.end(err.message || 'Invalid request body');
140
+ return res.end('Invalid request body');
141
141
  }
142
142
 
143
143
  setResponse(
@@ -73,11 +73,8 @@ export function create_client({ target, base, trailing_slash }) {
73
73
  /** @type {Array<((url: URL) => boolean)>} */
74
74
  const invalidated = [];
75
75
 
76
- /** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
77
- const load_cache = {
78
- id: null,
79
- promise: null
80
- };
76
+ /** @type {{id: string, promise: Promise<import('./types').NavigationResult | undefined>} | null} */
77
+ let load_cache = null;
81
78
 
82
79
  const callbacks = {
83
80
  /** @type {Array<(navigation: import('types').Navigation & { cancel: () => void }) => void>} */
@@ -91,7 +88,6 @@ export function create_client({ target, base, trailing_slash }) {
91
88
  let current = {
92
89
  branch: [],
93
90
  error: null,
94
- session_id: 0,
95
91
  // @ts-ignore - we need the initial value to be null
96
92
  url: null
97
93
  };
@@ -99,10 +95,6 @@ export function create_client({ target, base, trailing_slash }) {
99
95
  let started = false;
100
96
  let autoscroll = true;
101
97
  let updating = false;
102
- let session_id = 1;
103
-
104
- /** @type {Promise<void> | null} */
105
- let invalidating = null;
106
98
  let force_invalidation = false;
107
99
 
108
100
  /** @type {import('svelte').SvelteComponent} */
@@ -140,31 +132,38 @@ export function create_client({ target, base, trailing_slash }) {
140
132
  /** @type {{}} */
141
133
  let token;
142
134
 
143
- function invalidate() {
144
- if (!invalidating) {
145
- const url = new URL(location.href);
146
-
147
- invalidating = Promise.resolve().then(async () => {
148
- const intent = get_navigation_intent(url, true);
149
- await update(intent, url, []);
150
-
151
- invalidating = null;
152
- force_invalidation = false;
153
- });
154
- }
155
-
156
- return invalidating;
135
+ /** @type {Promise<void> | null} */
136
+ let pending_invalidate;
137
+
138
+ async function invalidate() {
139
+ // Accept all invalidations as they come, don't swallow any while another invalidation
140
+ // is running because subsequent invalidations may make earlier ones outdated,
141
+ // but batch multiple synchronous invalidations.
142
+ pending_invalidate = pending_invalidate || Promise.resolve();
143
+ await pending_invalidate;
144
+ pending_invalidate = null;
145
+
146
+ const url = new URL(location.href);
147
+ const intent = get_navigation_intent(url, true);
148
+ // Clear prefetch, it might be affected by the invalidation.
149
+ // Also solves an edge case where a prefetch is triggered, the navigation for it
150
+ // was then triggered and is still running while the invalidation kicks in,
151
+ // at which point the invalidation should take over and "win".
152
+ load_cache = null;
153
+ await update(intent, url, []);
157
154
  }
158
155
 
159
156
  /**
160
157
  * @param {string | URL} url
161
158
  * @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any }} opts
162
159
  * @param {string[]} redirect_chain
160
+ * @param {{}} [nav_token]
163
161
  */
164
162
  async function goto(
165
163
  url,
166
164
  { noscroll = false, replaceState = false, keepfocus = false, state = {} },
167
- redirect_chain
165
+ redirect_chain,
166
+ nav_token
168
167
  ) {
169
168
  if (typeof url === 'string') {
170
169
  url = new URL(url, get_base_uri(document));
@@ -179,6 +178,7 @@ export function create_client({ target, base, trailing_slash }) {
179
178
  state,
180
179
  replaceState
181
180
  },
181
+ nav_token,
182
182
  accepted: () => {},
183
183
  blocked: () => {},
184
184
  type: 'goto'
@@ -193,8 +193,7 @@ export function create_client({ target, base, trailing_slash }) {
193
193
  throw new Error('Attempted to prefetch a URL that does not belong to this app');
194
194
  }
195
195
 
196
- load_cache.promise = load_route(intent);
197
- load_cache.id = intent.id;
196
+ load_cache = { id: intent.id, promise: load_route(intent) };
198
197
 
199
198
  return load_cache.promise;
200
199
  }
@@ -205,10 +204,11 @@ export function create_client({ target, base, trailing_slash }) {
205
204
  * @param {URL} url
206
205
  * @param {string[]} redirect_chain
207
206
  * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts]
207
+ * @param {{}} [nav_token] To distinguish between different navigation events and determine the latest. Needed for example for redirects to keep the original token
208
208
  * @param {() => void} [callback]
209
209
  */
210
- async function update(intent, url, redirect_chain, opts, callback) {
211
- const current_token = (token = {});
210
+ async function update(intent, url, redirect_chain, opts, nav_token = {}, callback) {
211
+ token = nav_token;
212
212
  let navigation_result = intent && (await load_route(intent));
213
213
 
214
214
  if (
@@ -239,9 +239,7 @@ export function create_client({ target, base, trailing_slash }) {
239
239
  url = intent?.url || url;
240
240
 
241
241
  // abort if user navigated during update
242
- if (token !== current_token) return false;
243
-
244
- invalidated.length = 0;
242
+ if (token !== nav_token) return false;
245
243
 
246
244
  if (navigation_result.type === 'redirect') {
247
245
  if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
@@ -252,7 +250,12 @@ export function create_client({ target, base, trailing_slash }) {
252
250
  routeId: null
253
251
  });
254
252
  } else {
255
- goto(new URL(navigation_result.location, url).href, {}, [...redirect_chain, url.pathname]);
253
+ goto(
254
+ new URL(navigation_result.location, url).href,
255
+ {},
256
+ [...redirect_chain, url.pathname],
257
+ nav_token
258
+ );
256
259
  return false;
257
260
  }
258
261
  } else if (navigation_result.props?.page?.status >= 400) {
@@ -262,6 +265,11 @@ export function create_client({ target, base, trailing_slash }) {
262
265
  }
263
266
  }
264
267
 
268
+ // reset invalidation only after a finished navigation. If there are redirects or
269
+ // additional invalidations, they should get the same invalidation treatment
270
+ invalidated.length = 0;
271
+ force_invalidation = false;
272
+
265
273
  updating = true;
266
274
 
267
275
  if (opts && opts.details) {
@@ -271,6 +279,9 @@ export function create_client({ target, base, trailing_slash }) {
271
279
  history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url);
272
280
  }
273
281
 
282
+ // reset prefetch synchronously after the history state has been set to avoid race conditions
283
+ load_cache = null;
284
+
274
285
  if (started) {
275
286
  current = navigation_result.state;
276
287
 
@@ -334,8 +345,6 @@ export function create_client({ target, base, trailing_slash }) {
334
345
  await tick();
335
346
  }
336
347
 
337
- load_cache.promise = null;
338
- load_cache.id = null;
339
348
  autoscroll = true;
340
349
 
341
350
  if (navigation_result.props.page) {
@@ -410,8 +419,7 @@ export function create_client({ target, base, trailing_slash }) {
410
419
  params,
411
420
  branch,
412
421
  error,
413
- route,
414
- session_id
422
+ route
415
423
  },
416
424
  props: {
417
425
  components: filtered.map((branch_node) => branch_node.node.component)
@@ -683,7 +691,7 @@ export function create_client({ target, base, trailing_slash }) {
683
691
  * @returns {Promise<import('./types').NavigationResult | undefined>}
684
692
  */
685
693
  async function load_route({ id, invalidating, url, params, route }) {
686
- if (load_cache.id === id && load_cache.promise) {
694
+ if (load_cache?.id === id) {
687
695
  return load_cache.promise;
688
696
  }
689
697
 
@@ -981,6 +989,7 @@ export function create_client({ target, base, trailing_slash }) {
981
989
  * } | null;
982
990
  * type: import('types').NavigationType;
983
991
  * delta?: number;
992
+ * nav_token?: {};
984
993
  * accepted: () => void;
985
994
  * blocked: () => void;
986
995
  * }} opts
@@ -993,6 +1002,7 @@ export function create_client({ target, base, trailing_slash }) {
993
1002
  details,
994
1003
  type,
995
1004
  delta,
1005
+ nav_token,
996
1006
  accepted,
997
1007
  blocked
998
1008
  }) {
@@ -1050,6 +1060,7 @@ export function create_client({ target, base, trailing_slash }) {
1050
1060
  keepfocus,
1051
1061
  details
1052
1062
  },
1063
+ nav_token,
1053
1064
  () => {
1054
1065
  callbacks.after_navigate.forEach((fn) => fn(navigation));
1055
1066
  stores.navigating.set(null);
@@ -80,6 +80,5 @@ export interface NavigationState {
80
80
  error: App.PageError | null;
81
81
  params: Record<string, string>;
82
82
  route: CSRRoute | null;
83
- session_id: number;
84
83
  url: URL;
85
84
  }
@@ -71,6 +71,18 @@ export function get_cookies(request, url) {
71
71
  maxAge: 0
72
72
  }
73
73
  });
74
+ },
75
+
76
+ /**
77
+ * @param {string} name
78
+ * @param {string} value
79
+ * @param {import('cookie').CookieSerializeOptions} opts
80
+ */
81
+ serialize(name, value, opts) {
82
+ return serialize(name, value, {
83
+ ...DEFAULT_SERIALIZE_OPTIONS,
84
+ ...opts
85
+ });
74
86
  }
75
87
  };
76
88
 
@@ -236,17 +236,6 @@ export async function respond(request, options, state) {
236
236
  throw new Error('This should never happen');
237
237
  }
238
238
 
239
- if (!is_data_request) {
240
- // we only want to set cookies on __data.js requests, we don't
241
- // want to cache stuff erroneously etc
242
- for (const key in headers) {
243
- const value = headers[key];
244
- response.headers.set(key, /** @type {string} */ (value));
245
- }
246
- }
247
-
248
- add_cookies_to_headers(response.headers, Array.from(new_cookies.values()));
249
-
250
239
  return response;
251
240
  }
252
241
 
@@ -294,7 +283,21 @@ export async function respond(request, options, state) {
294
283
  try {
295
284
  const response = await options.hooks.handle({
296
285
  event,
297
- resolve,
286
+ resolve: (event, opts) =>
287
+ resolve(event, opts).then((response) => {
288
+ // add headers/cookies here, rather than inside `resolve`, so that we
289
+ // can do it once for all responses instead of once per `return`
290
+ if (!is_data_request) {
291
+ // we only want to set cookies on __data.js requests, we don't
292
+ // want to cache stuff erroneously etc
293
+ for (const key in headers) {
294
+ const value = headers[key];
295
+ response.headers.set(key, /** @type {string} */ (value));
296
+ }
297
+ }
298
+ add_cookies_to_headers(response.headers, Array.from(new_cookies.values()));
299
+ return response;
300
+ }),
298
301
  // TODO remove for 1.0
299
302
  // @ts-expect-error
300
303
  get request() {
package/types/index.d.ts CHANGED
@@ -155,6 +155,19 @@ export interface Cookies {
155
155
  * Deletes a cookie by setting its value to an empty string and setting the expiry date in the past.
156
156
  */
157
157
  delete(name: string, opts?: import('cookie').CookieSerializeOptions): void;
158
+
159
+ /**
160
+ * Serialize a cookie name-value pair into a Set-Cookie header string.
161
+ *
162
+ * The `httpOnly` and `secure` options are `true` by default, and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. The `sameSite` option defaults to `lax`.
163
+ *
164
+ * By default, the `path` of a cookie is the current pathname. In most cases you should explicitly set `path: '/'` to make the cookie available throughout your app.
165
+ *
166
+ * @param name the name for the cookie
167
+ * @param value value to set the cookie to
168
+ * @param options object containing serialization options
169
+ */
170
+ serialize(name: string, value: string, opts?: import('cookie').CookieSerializeOptions): string;
158
171
  }
159
172
 
160
173
  export interface KitConfig {
@@ -368,6 +381,7 @@ export interface ServerLoadEvent<
368
381
  ParentData extends Record<string, any> = Record<string, any>
369
382
  > extends RequestEvent<Params> {
370
383
  parent: () => Promise<ParentData>;
384
+ depends: (...deps: string[]) => void;
371
385
  }
372
386
 
373
387
  export interface Action<