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

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.469",
3
+ "version": "1.0.0-next.471",
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.4",
13
+ "@sveltejs/vite-plugin-svelte": "^1.0.5",
14
14
  "cookie": "^0.5.0",
15
15
  "devalue": "^3.1.2",
16
16
  "kleur": "^4.1.4",
@@ -38,11 +38,11 @@
38
38
  "svelte-preprocess": "^4.10.6",
39
39
  "typescript": "^4.8.2",
40
40
  "uvu": "^0.5.3",
41
- "vite": "^3.1.0-beta.2"
41
+ "vite": "^3.1.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "svelte": "^3.44.0",
45
- "vite": "^3.1.0-beta.1"
45
+ "vite": "^3.1.0"
46
46
  },
47
47
  "bin": {
48
48
  "svelte-kit": "svelte-kit.js"
package/src/core/env.js CHANGED
@@ -24,8 +24,17 @@ export function create_static_module(id, env) {
24
24
  return GENERATED_COMMENT + declarations.join('\n\n');
25
25
  }
26
26
 
27
- /** @param {'public' | 'private'} type */
28
- export function create_dynamic_module(type) {
27
+ /**
28
+ * @param {'public' | 'private'} type
29
+ * @param {Record<string, string> | undefined} dev_values If in a development mode, values to pre-populate the module with.
30
+ */
31
+ export function create_dynamic_module(type, dev_values) {
32
+ if (dev_values) {
33
+ const objectKeys = Object.entries(dev_values).map(
34
+ ([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
35
+ );
36
+ return `const env = {\n${objectKeys.join(',\n')}\n}\n\nexport { env }`;
37
+ }
29
38
  return `export { env } from '${runtime_base}/env-${type}.js';`;
30
39
  }
31
40
 
@@ -237,10 +237,11 @@ export async function prerender() {
237
237
  const encoded_dependency_path = new URL(dependency_path, 'http://localhost').pathname;
238
238
  const decoded_dependency_path = decodeURI(encoded_dependency_path);
239
239
 
240
- const prerender = result.response.headers.get('x-sveltekit-prerender');
240
+ const headers = Object.fromEntries(result.response.headers);
241
241
 
242
+ const prerender = headers['x-sveltekit-prerender'];
242
243
  if (prerender) {
243
- const route_id = /** @type {string} */ (result.response.headers.get('x-sveltekit-routeid'));
244
+ const route_id = headers['x-sveltekit-routeid'];
244
245
  const existing_value = prerender_map.get(route_id);
245
246
  if (existing_value !== 'auto') {
246
247
  prerender_map.set(route_id, prerender === 'true' ? true : 'auto');
@@ -259,7 +260,10 @@ export async function prerender() {
259
260
  );
260
261
  }
261
262
 
262
- if (config.prerender.crawl && response.headers.get('content-type') === 'text/html') {
263
+ // avoid triggering `filterSerializeResponseHeaders` guard
264
+ const headers = Object.fromEntries(response.headers);
265
+
266
+ if (config.prerender.crawl && headers['content-type'] === 'text/html') {
263
267
  for (const href of crawl(body.toString())) {
264
268
  if (href.startsWith('data:') || href.startsWith('#')) continue;
265
269
 
@@ -288,7 +292,9 @@ export async function prerender() {
288
292
  */
289
293
  function save(category, response, body, decoded, encoded, referrer, referenceType) {
290
294
  const response_type = Math.floor(response.status / 100);
291
- const type = /** @type {string} */ (response.headers.get('content-type'));
295
+ const headers = Object.fromEntries(response.headers);
296
+
297
+ const type = headers['content-type'];
292
298
  const is_html = response_type === REDIRECT || type === 'text/html';
293
299
 
294
300
  const file = output_filename(decoded, is_html);
@@ -297,7 +303,7 @@ export async function prerender() {
297
303
  if (written.has(file)) return;
298
304
 
299
305
  if (response_type === REDIRECT) {
300
- const location = response.headers.get('location');
306
+ const location = headers['location'];
301
307
 
302
308
  if (location) {
303
309
  const resolved = resolve(encoded, location);
@@ -305,7 +311,7 @@ export async function prerender() {
305
311
  enqueue(decoded, decodeURI(resolved), resolved);
306
312
  }
307
313
 
308
- if (!response.headers.get('x-sveltekit-normalize')) {
314
+ if (!headers['x-sveltekit-normalize']) {
309
315
  mkdirp(dirname(dest));
310
316
 
311
317
  log.warn(`${response.status} ${decoded} -> ${location}`);
@@ -103,6 +103,14 @@ function create_routes_and_nodes(cwd, config, fallback) {
103
103
  * @param {import('types').RouteData | null} parent
104
104
  */
105
105
  const walk = (depth, id, segment, parent) => {
106
+ if (/\]\[/.test(id)) {
107
+ throw new Error(`Invalid route ${id} — parameters must be separated`);
108
+ }
109
+
110
+ if (count_occurrences('[', id) !== count_occurrences(']', id)) {
111
+ throw new Error(`Invalid route ${id} — brackets are unbalanced`);
112
+ }
113
+
106
114
  const { pattern, names, types } = parse_route_id(id);
107
115
 
108
116
  const segments = id.split('/');
@@ -450,3 +458,15 @@ function list_files(dir) {
450
458
 
451
459
  return files;
452
460
  }
461
+
462
+ /**
463
+ * @param {string} needle
464
+ * @param {string} haystack
465
+ */
466
+ function count_occurrences(needle, haystack) {
467
+ let count = 0;
468
+ for (let i = 0; i < haystack.length; i += 1) {
469
+ if (haystack[i] === needle) count += 1;
470
+ }
471
+ return count;
472
+ }
@@ -110,10 +110,16 @@ export class Server {
110
110
 
111
111
  if (!this.options.hooks) {
112
112
  const module = await import(${s(hooks)});
113
+
114
+ // TODO remove this for 1.0
115
+ if (module.externalFetch) {
116
+ throw new Error('externalFetch has been removed — use handleFetch instead. See https://github.com/sveltejs/kit/pull/6565 for details');
117
+ }
118
+
113
119
  this.options.hooks = {
114
120
  handle: module.handle || (({ event, resolve }) => resolve(event)),
115
121
  handleError: module.handleError || (({ error }) => console.error(error.stack)),
116
- externalFetch: module.externalFetch || fetch
122
+ handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request))
117
123
  };
118
124
  }
119
125
  }
@@ -11,8 +11,9 @@ import { load_error_page, load_template } from '../../../core/config/index.js';
11
11
  import { SVELTE_KIT_ASSETS } from '../../../constants.js';
12
12
  import * as sync from '../../../core/sync/sync.js';
13
13
  import { get_mime_lookup, runtime_base, runtime_prefix } from '../../../core/utils.js';
14
- import { get_env, prevent_illegal_vite_imports, resolve_entry } from '../utils.js';
14
+ import { prevent_illegal_vite_imports, resolve_entry } from '../utils.js';
15
15
  import { compact } from '../../../utils/array.js';
16
+ import { normalizePath } from 'vite';
16
17
 
17
18
  // Vite doesn't expose this so we just copy the list for now
18
19
  // https://github.com/vitejs/vite/blob/3edd1af56e980aef56641a5a51cf2932bb580d41/packages/vite/src/node/plugins/css.ts#L96
@@ -24,10 +25,9 @@ const cwd = process.cwd();
24
25
  * @param {import('vite').ViteDevServer} vite
25
26
  * @param {import('vite').ResolvedConfig} vite_config
26
27
  * @param {import('types').ValidatedConfig} svelte_config
27
- * @param {Set<string>} illegal_imports
28
28
  * @return {Promise<Promise<() => void>>}
29
29
  */
30
- export async function dev(vite, vite_config, svelte_config, illegal_imports) {
30
+ export async function dev(vite, vite_config, svelte_config) {
31
31
  installPolyfills();
32
32
 
33
33
  sync.init(svelte_config, vite_config.mode);
@@ -90,7 +90,11 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
90
90
  module_nodes.push(module_node);
91
91
  result.file = url.endsWith('.svelte') ? url : url + '?import'; // TODO what is this for?
92
92
 
93
- prevent_illegal_vite_imports(module_node, illegal_imports, extensions);
93
+ prevent_illegal_vite_imports(
94
+ module_node,
95
+ normalizePath(svelte_config.kit.files.lib),
96
+ extensions
97
+ );
94
98
 
95
99
  return module.default;
96
100
  };
@@ -103,7 +107,11 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
103
107
 
104
108
  result.shared = module;
105
109
 
106
- prevent_illegal_vite_imports(module_node, illegal_imports, extensions);
110
+ prevent_illegal_vite_imports(
111
+ module_node,
112
+ normalizePath(svelte_config.kit.files.lib),
113
+ extensions
114
+ );
107
115
  }
108
116
 
109
117
  if (node.server) {
@@ -269,13 +277,6 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
269
277
  }
270
278
  });
271
279
 
272
- const { set_private_env } = await vite.ssrLoadModule(`${runtime_base}/env-private.js`);
273
- const { set_public_env } = await vite.ssrLoadModule(`${runtime_base}/env-public.js`);
274
-
275
- const env = get_env(svelte_config.kit.env, vite_config.mode);
276
- set_private_env(env.private);
277
- set_public_env(env.public);
278
-
279
280
  return () => {
280
281
  const serve_static_middleware = vite.middlewares.stack.find(
281
282
  (middleware) =>
@@ -317,6 +318,14 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
317
318
 
318
319
  const handle = user_hooks.handle || (({ event, resolve }) => resolve(event));
319
320
 
321
+ // TODO remove for 1.0
322
+ // @ts-expect-error
323
+ if (user_hooks.externalFetch) {
324
+ throw new Error(
325
+ 'externalFetch has been removed — use handleFetch instead. See https://github.com/sveltejs/kit/pull/6565 for details'
326
+ );
327
+ }
328
+
320
329
  /** @type {import('types').Hooks} */
321
330
  const hooks = {
322
331
  handle,
@@ -331,7 +340,7 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
331
340
  console.error(colors.gray(error.stack));
332
341
  }
333
342
  }),
334
- externalFetch: user_hooks.externalFetch || fetch
343
+ handleFetch: user_hooks.handleFetch || (({ request, fetch }) => fetch(request))
335
344
  };
336
345
 
337
346
  if (/** @type {any} */ (hooks).getContext) {
@@ -411,7 +420,7 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
411
420
  base: svelte_config.kit.paths.base,
412
421
  assets
413
422
  },
414
- public_env: env.public,
423
+ public_env: {},
415
424
  read: (file) => fs.readFileSync(path.join(svelte_config.kit.files.assets, file)),
416
425
  root,
417
426
  app_template: ({ head, body, assets, nonce }) => {
@@ -103,9 +103,6 @@ function kit() {
103
103
  /** @type {import('types').BuildData} */
104
104
  let build_data;
105
105
 
106
- /** @type {Set<string>} */
107
- let illegal_imports;
108
-
109
106
  /** @type {string | undefined} */
110
107
  let deferred_warning;
111
108
 
@@ -212,13 +209,6 @@ function kit() {
212
209
  client_out_dir: `${svelte_config.kit.outDir}/output/client`
213
210
  };
214
211
 
215
- illegal_imports = new Set([
216
- '/@id/__x00__$env/dynamic/private', //dev
217
- '\0$env/dynamic/private', // prod
218
- '/@id/__x00__$env/static/private', // dev
219
- '\0$env/static/private' // prod
220
- ]);
221
-
222
212
  if (is_build) {
223
213
  manifest_data = (await sync.all(svelte_config, config_env.mode)).manifest_data;
224
214
 
@@ -299,9 +289,15 @@ function kit() {
299
289
  case '\0$env/static/public':
300
290
  return create_static_module('$env/static/public', env.public);
301
291
  case '\0$env/dynamic/private':
302
- return create_dynamic_module('private');
292
+ return create_dynamic_module(
293
+ 'private',
294
+ vite_config_env.command === 'serve' ? env.private : undefined
295
+ );
303
296
  case '\0$env/dynamic/public':
304
- return create_dynamic_module('public');
297
+ return create_dynamic_module(
298
+ 'public',
299
+ vite_config_env.command === 'serve' ? env.public : undefined
300
+ );
305
301
  }
306
302
  },
307
303
 
@@ -355,7 +351,7 @@ function kit() {
355
351
  prevent_illegal_rollup_imports(
356
352
  this.getModuleInfo.bind(this),
357
353
  module_node,
358
- illegal_imports
354
+ vite.normalizePath(svelte_config.kit.files.lib)
359
355
  );
360
356
  }
361
357
  });
@@ -511,7 +507,7 @@ function kit() {
511
507
  if (deferred_warning) console.error('\n' + deferred_warning);
512
508
  };
513
509
 
514
- return await dev(vite, vite_config, svelte_config, illegal_imports);
510
+ return await dev(vite, vite_config, svelte_config);
515
511
  },
516
512
 
517
513
  /**
@@ -4,6 +4,26 @@ import { loadConfigFromFile, loadEnv, normalizePath } from 'vite';
4
4
  import { runtime_directory } from '../../core/utils.js';
5
5
  import { posixify } from '../../utils/filesystem.js';
6
6
 
7
+ const illegal_imports = new Set([
8
+ '/@id/__x00__$env/dynamic/private', //dev
9
+ '\0$env/dynamic/private', // prod
10
+ '/@id/__x00__$env/static/private', // dev
11
+ '\0$env/static/private' // prod
12
+ ]);
13
+
14
+ /** @param {string} id */
15
+ function is_illegal(id) {
16
+ if (illegal_imports.has(id)) return true;
17
+
18
+ // files outside the project root are ignored
19
+ if (!id.startsWith(normalizePath(process.cwd()))) return false;
20
+
21
+ // so are files inside node_modules
22
+ if (id.startsWith(normalizePath(node_modules_dir))) return false;
23
+
24
+ return /.*\.server\..+/.test(path.basename(id));
25
+ }
26
+
7
27
  /**
8
28
  * @param {import('vite').ResolvedConfig} config
9
29
  * @param {import('vite').ConfigEnv} config_env
@@ -183,8 +203,9 @@ function repeat(str, times) {
183
203
  /**
184
204
  * Create a formatted error for an illegal import.
185
205
  * @param {Array<{name: string, dynamic: boolean}>} stack
206
+ * @param {string} lib_dir
186
207
  */
187
- function format_illegal_import_chain(stack) {
208
+ function format_illegal_import_chain(stack, lib_dir) {
188
209
  const dev_virtual_prefix = '/@id/__x00__';
189
210
  const prod_virtual_prefix = '\0';
190
211
 
@@ -195,6 +216,9 @@ function format_illegal_import_chain(stack) {
195
216
  if (file.name.startsWith(prod_virtual_prefix)) {
196
217
  return { ...file, name: file.name.replace(prod_virtual_prefix, '') };
197
218
  }
219
+ if (file.name.startsWith(lib_dir)) {
220
+ return { ...file, name: file.name.replace(lib_dir, '$lib') };
221
+ }
198
222
 
199
223
  return { ...file, name: path.relative(process.cwd(), file.name) };
200
224
  });
@@ -211,6 +235,8 @@ function format_illegal_import_chain(stack) {
211
235
  return `Cannot import ${stack.at(-1)?.name} into client-side code:\n${pyramid}`;
212
236
  }
213
237
 
238
+ const node_modules_dir = path.resolve(process.cwd(), 'node_modules');
239
+
214
240
  /**
215
241
  * Load environment variables from process.env and .env files
216
242
  * @param {import('types').ValidatedKitConfig['env']} env_config
@@ -228,11 +254,11 @@ export function get_env(env_config, mode) {
228
254
  /**
229
255
  * @param {(id: string) => import('rollup').ModuleInfo | null} node_getter
230
256
  * @param {import('rollup').ModuleInfo} node
231
- * @param {Set<string>} illegal_imports Illegal module IDs -- be sure to call vite.normalizePath!
257
+ * @param {string} lib_dir
232
258
  */
233
- export function prevent_illegal_rollup_imports(node_getter, node, illegal_imports) {
234
- const chain = find_illegal_rollup_imports(node_getter, node, false, illegal_imports);
235
- if (chain) throw new Error(format_illegal_import_chain(chain));
259
+ export function prevent_illegal_rollup_imports(node_getter, node, lib_dir) {
260
+ const chain = find_illegal_rollup_imports(node_getter, node, false);
261
+ if (chain) throw new Error(format_illegal_import_chain(chain, lib_dir));
236
262
  }
237
263
 
238
264
  const query_pattern = /\?.*$/s;
@@ -246,36 +272,27 @@ function remove_query_from_path(path) {
246
272
  * @param {(id: string) => import('rollup').ModuleInfo | null} node_getter
247
273
  * @param {import('rollup').ModuleInfo} node
248
274
  * @param {boolean} dynamic
249
- * @param {Set<string>} illegal_imports Illegal module IDs -- be sure to call vite.normalizePath!
250
275
  * @param {Set<string>} seen
251
276
  * @returns {Array<import('types').ImportNode> | null}
252
277
  */
253
- const find_illegal_rollup_imports = (
254
- node_getter,
255
- node,
256
- dynamic,
257
- illegal_imports,
258
- seen = new Set()
259
- ) => {
278
+ const find_illegal_rollup_imports = (node_getter, node, dynamic, seen = new Set()) => {
260
279
  const name = remove_query_from_path(normalizePath(node.id));
261
280
  if (seen.has(name)) return null;
262
281
  seen.add(name);
263
282
 
264
- if (illegal_imports.has(name)) {
283
+ if (is_illegal(name)) {
265
284
  return [{ name, dynamic }];
266
285
  }
267
286
 
268
287
  for (const id of node.importedIds) {
269
288
  const child = node_getter(id);
270
- const chain =
271
- child && find_illegal_rollup_imports(node_getter, child, false, illegal_imports, seen);
289
+ const chain = child && find_illegal_rollup_imports(node_getter, child, false, seen);
272
290
  if (chain) return [{ name, dynamic }, ...chain];
273
291
  }
274
292
 
275
293
  for (const id of node.dynamicallyImportedIds) {
276
294
  const child = node_getter(id);
277
- const chain =
278
- child && find_illegal_rollup_imports(node_getter, child, true, illegal_imports, seen);
295
+ const chain = child && find_illegal_rollup_imports(node_getter, child, true, seen);
279
296
  if (chain) return [{ name, dynamic }, ...chain];
280
297
  }
281
298
 
@@ -308,22 +325,21 @@ const get_module_types = (config_module_types) => {
308
325
  /**
309
326
  * Throw an error if a private module is imported from a client-side node.
310
327
  * @param {import('vite').ModuleNode} node
311
- * @param {Set<string>} illegal_imports Illegal module IDs -- be sure to call vite.normalizePath!
328
+ * @param {string} lib_dir
312
329
  * @param {Iterable<string>} module_types File extensions to analyze in addition to the defaults: `.ts`, `.js`, etc.
313
330
  */
314
- export function prevent_illegal_vite_imports(node, illegal_imports, module_types) {
315
- const chain = find_illegal_vite_imports(node, illegal_imports, get_module_types(module_types));
316
- if (chain) throw new Error(format_illegal_import_chain(chain));
331
+ export function prevent_illegal_vite_imports(node, lib_dir, module_types) {
332
+ const chain = find_illegal_vite_imports(node, get_module_types(module_types));
333
+ if (chain) throw new Error(format_illegal_import_chain(chain, lib_dir));
317
334
  }
318
335
 
319
336
  /**
320
337
  * @param {import('vite').ModuleNode} node
321
- * @param {Set<string>} illegal_imports Illegal module IDs -- be sure to call vite.normalizePath!
322
338
  * @param {Set<string>} module_types File extensions to analyze: `.ts`, `.js`, etc.
323
339
  * @param {Set<string>} seen
324
340
  * @returns {Array<import('types').ImportNode> | null}
325
341
  */
326
- function find_illegal_vite_imports(node, illegal_imports, module_types, seen = new Set()) {
342
+ function find_illegal_vite_imports(node, module_types, seen = new Set()) {
327
343
  if (!node.id) return null; // TODO when does this happen?
328
344
  const name = remove_query_from_path(normalizePath(node.id));
329
345
 
@@ -332,12 +348,12 @@ function find_illegal_vite_imports(node, illegal_imports, module_types, seen = n
332
348
  }
333
349
  seen.add(name);
334
350
 
335
- if (name && illegal_imports.has(name)) {
351
+ if (is_illegal(name)) {
336
352
  return [{ name, dynamic: false }];
337
353
  }
338
354
 
339
355
  for (const child of node.importedModules) {
340
- const chain = child && find_illegal_vite_imports(child, illegal_imports, module_types, seen);
356
+ const chain = child && find_illegal_vite_imports(child, module_types, seen);
341
357
  if (chain) return [{ name, dynamic: false }, ...chain];
342
358
  }
343
359
 
@@ -14,6 +14,8 @@ import { DATA_SUFFIX } from '../../constants.js';
14
14
  /** @param {{ html: string }} opts */
15
15
  const default_transform = ({ html }) => html;
16
16
 
17
+ const default_filter = () => false;
18
+
17
19
  /** @type {import('types').Respond} */
18
20
  export async function respond(request, options, state) {
19
21
  let url = new URL(request.url);
@@ -201,7 +203,8 @@ export async function respond(request, options, state) {
201
203
 
202
204
  /** @type {import('types').RequiredResolveOptions} */
203
205
  let resolve_opts = {
204
- transformPageChunk: default_transform
206
+ transformPageChunk: default_transform,
207
+ filterSerializedResponseHeaders: default_filter
205
208
  };
206
209
 
207
210
  /**
@@ -226,7 +229,8 @@ export async function respond(request, options, state) {
226
229
  }
227
230
 
228
231
  resolve_opts = {
229
- transformPageChunk: opts.transformPageChunk || default_transform
232
+ transformPageChunk: opts.transformPageChunk || default_transform,
233
+ filterSerializedResponseHeaders: opts.filterSerializedResponseHeaders || default_filter
230
234
  };
231
235
  }
232
236
 
@@ -1,7 +1,6 @@
1
1
  import * as cookie from 'cookie';
2
2
  import * as set_cookie_parser from 'set-cookie-parser';
3
3
  import { respond } from '../index.js';
4
- import { is_root_relative, resolve } from '../../../utils/url.js';
5
4
  import { domain_matches, path_matches } from './cookie.js';
6
5
 
7
6
  /**
@@ -11,194 +10,175 @@ import { domain_matches, path_matches } from './cookie.js';
11
10
  * state: import('types').SSRState;
12
11
  * route: import('types').SSRRoute | import('types').SSRErrorPage;
13
12
  * prerender_default?: import('types').PrerenderOption;
13
+ * resolve_opts: import('types').RequiredResolveOptions;
14
14
  * }} opts
15
15
  */
16
- export function create_fetch({ event, options, state, route, prerender_default }) {
16
+ export function create_fetch({ event, options, state, route, prerender_default, resolve_opts }) {
17
17
  /** @type {import('./types').Fetched[]} */
18
18
  const fetched = [];
19
19
 
20
20
  const initial_cookies = cookie.parse(event.request.headers.get('cookie') || '');
21
21
 
22
22
  /** @type {import('set-cookie-parser').Cookie[]} */
23
- const cookies = [];
23
+ const set_cookies = [];
24
24
 
25
- /** @type {typeof fetch} */
26
- const fetcher = async (resource, opts = {}) => {
27
- /** @type {string} */
28
- let requested;
29
-
30
- if (typeof resource === 'string' || resource instanceof URL) {
31
- requested = resource.toString();
32
- } else {
33
- requested = resource.url;
34
-
35
- opts = {
36
- method: resource.method,
37
- headers: resource.headers,
38
- body: resource.body,
39
- mode: resource.mode,
40
- credentials: resource.credentials,
41
- cache: resource.cache,
42
- redirect: resource.redirect,
43
- referrer: resource.referrer,
44
- integrity: resource.integrity,
45
- ...opts
46
- };
47
- }
25
+ /**
26
+ * @param {URL} url
27
+ * @param {string | null} header
28
+ */
29
+ function get_cookie_header(url, header) {
30
+ /** @type {Record<string, string>} */
31
+ const new_cookies = {};
48
32
 
49
- opts.headers = new Headers(opts.headers);
50
-
51
- // merge headers from request
52
- for (const [key, value] of event.request.headers) {
53
- if (
54
- key !== 'authorization' &&
55
- key !== 'connection' &&
56
- key !== 'content-length' &&
57
- key !== 'cookie' &&
58
- key !== 'host' &&
59
- key !== 'if-none-match' &&
60
- !opts.headers.has(key)
61
- ) {
62
- opts.headers.set(key, value);
63
- }
33
+ for (const cookie of set_cookies) {
34
+ if (!domain_matches(url.hostname, cookie.domain)) continue;
35
+ if (!path_matches(url.pathname, cookie.path)) continue;
36
+
37
+ new_cookies[cookie.name] = cookie.value;
64
38
  }
65
39
 
66
- const resolved = resolve(event.url.pathname, requested.split('?')[0]).replace(/#.+$/, '');
40
+ // cookies from explicit `cookie` header take precedence over cookies previously set
41
+ // during this load with `set-cookie`, which take precedence over the cookies
42
+ // sent by the user agent
43
+ const combined_cookies = {
44
+ ...initial_cookies,
45
+ ...new_cookies,
46
+ ...cookie.parse(header ?? '')
47
+ };
48
+
49
+ return Object.entries(combined_cookies)
50
+ .map(([name, value]) => `${name}=${value}`)
51
+ .join('; ');
52
+ }
53
+
54
+ /** @type {typeof fetch} */
55
+ const fetcher = async (info, init) => {
56
+ const request = normalize_fetch_input(info, init, event.url);
67
57
 
68
- /** @type {Response} */
69
- let response;
58
+ const request_body = init?.body;
70
59
 
71
60
  /** @type {import('types').PrerenderDependency} */
72
61
  let dependency;
73
62
 
74
- // handle fetch requests for static assets. e.g. prebaked data, etc.
75
- // we need to support everything the browser's fetch supports
76
- const prefix = options.paths.assets || options.paths.base;
77
- const filename = decodeURIComponent(
78
- resolved.startsWith(prefix) ? resolved.slice(prefix.length) : resolved
79
- ).slice(1);
80
- const filename_html = `${filename}/index.html`; // path may also match path/index.html
63
+ const response = await options.hooks.handleFetch({
64
+ event,
65
+ request,
66
+ fetch: async (info, init) => {
67
+ const request = normalize_fetch_input(info, init, event.url);
81
68
 
82
- const is_asset = options.manifest.assets.has(filename);
83
- const is_asset_html = options.manifest.assets.has(filename_html);
69
+ const url = new URL(request.url);
84
70
 
85
- if (is_asset || is_asset_html) {
86
- const file = is_asset ? filename : filename_html;
71
+ if (url.origin !== event.url.origin) {
72
+ // allow cookie passthrough for "same-origin"
73
+ // if SvelteKit is serving my.domain.com:
74
+ // - domain.com WILL NOT receive cookies
75
+ // - my.domain.com WILL receive cookies
76
+ // - api.domain.dom WILL NOT receive cookies
77
+ // - sub.my.domain.com WILL receive cookies
78
+ // ports do not affect the resolution
79
+ // leading dot prevents mydomain.com matching domain.com
80
+ if (
81
+ `.${url.hostname}`.endsWith(`.${event.url.hostname}`) &&
82
+ request.credentials !== 'omit'
83
+ ) {
84
+ const cookie = get_cookie_header(url, request.headers.get('cookie'));
85
+ if (cookie) request.headers.set('cookie', cookie);
86
+ }
87
87
 
88
- if (options.read) {
89
- const type = is_asset
90
- ? options.manifest.mimeTypes[filename.slice(filename.lastIndexOf('.'))]
91
- : 'text/html';
88
+ let response = await fetch(request);
92
89
 
93
- response = new Response(options.read(file), {
94
- headers: type ? { 'content-type': type } : {}
95
- });
96
- } else {
97
- response = await fetch(`${event.url.origin}/${file}`, /** @type {RequestInit} */ (opts));
98
- }
99
- } else if (is_root_relative(resolved)) {
100
- if (opts.credentials !== 'omit') {
101
- const authorization = event.request.headers.get('authorization');
90
+ if (request.mode === 'no-cors') {
91
+ response = new Response('', {
92
+ status: response.status,
93
+ statusText: response.statusText,
94
+ headers: response.headers
95
+ });
96
+ } else {
97
+ if (url.origin !== event.url.origin) {
98
+ const acao = response.headers.get('access-control-allow-origin');
99
+ if (!acao || (acao !== event.url.origin && acao !== '*')) {
100
+ throw new Error(
101
+ `CORS error: ${
102
+ acao ? 'Incorrect' : 'No'
103
+ } 'Access-Control-Allow-Origin' header is present on the requested resource`
104
+ );
105
+ }
106
+ }
107
+ }
102
108
 
103
- // combine cookies from the initiating request with any that were
104
- // added via set-cookie
105
- const combined_cookies = { ...initial_cookies };
109
+ return response;
110
+ }
106
111
 
107
- for (const cookie of cookies) {
108
- if (!domain_matches(event.url.hostname, cookie.domain)) continue;
109
- if (!path_matches(resolved, cookie.path)) continue;
112
+ /** @type {Response} */
113
+ let response;
110
114
 
111
- combined_cookies[cookie.name] = cookie.value;
112
- }
115
+ // handle fetch requests for static assets. e.g. prebaked data, etc.
116
+ // we need to support everything the browser's fetch supports
117
+ const prefix = options.paths.assets || options.paths.base;
118
+ const decoded = decodeURIComponent(url.pathname);
119
+ const filename = (
120
+ decoded.startsWith(prefix) ? decoded.slice(prefix.length) : decoded
121
+ ).slice(1);
122
+ const filename_html = `${filename}/index.html`; // path may also match path/index.html
113
123
 
114
- const cookie = Object.entries(combined_cookies)
115
- .map(([name, value]) => `${name}=${value}`)
116
- .join('; ');
124
+ const is_asset = options.manifest.assets.has(filename);
125
+ const is_asset_html = options.manifest.assets.has(filename_html);
117
126
 
118
- if (cookie) {
119
- opts.headers.set('cookie', cookie);
120
- }
127
+ if (is_asset || is_asset_html) {
128
+ const file = is_asset ? filename : filename_html;
121
129
 
122
- if (authorization && !opts.headers.has('authorization')) {
123
- opts.headers.set('authorization', authorization);
124
- }
125
- }
130
+ if (options.read) {
131
+ const type = is_asset
132
+ ? options.manifest.mimeTypes[filename.slice(filename.lastIndexOf('.'))]
133
+ : 'text/html';
126
134
 
127
- if (opts.body && typeof opts.body !== 'string') {
128
- // per https://developer.mozilla.org/en-US/docs/Web/API/Request/Request, this can be a
129
- // Blob, BufferSource, FormData, URLSearchParams, USVString, or ReadableStream object.
130
- // non-string bodies are irksome to deal with, but luckily aren't particularly useful
131
- // in this context anyway, so we take the easy route and ban them
132
- throw new Error('Request body must be a string');
133
- }
135
+ return new Response(options.read(file), {
136
+ headers: type ? { 'content-type': type } : {}
137
+ });
138
+ }
134
139
 
135
- response = await respond(
136
- new Request(new URL(requested, event.url).href, { ...opts }),
137
- options,
138
- {
139
- prerender_default,
140
- ...state,
141
- initiator: route
140
+ return await fetch(request);
142
141
  }
143
- );
144
-
145
- if (state.prerendering) {
146
- dependency = { response, body: null };
147
- state.prerendering.dependencies.set(resolved, dependency);
148
- }
149
- } else {
150
- // external
151
- if (resolved.startsWith('//')) {
152
- requested = event.url.protocol + requested;
153
- }
154
142
 
155
- const url = new URL(requested);
156
-
157
- // external fetch
158
- // allow cookie passthrough for "same-origin"
159
- // if SvelteKit is serving my.domain.com:
160
- // - domain.com WILL NOT receive cookies
161
- // - my.domain.com WILL receive cookies
162
- // - api.domain.dom WILL NOT receive cookies
163
- // - sub.my.domain.com WILL receive cookies
164
- // ports do not affect the resolution
165
- // leading dot prevents mydomain.com matching domain.com
166
- if (`.${url.hostname}`.endsWith(`.${event.url.hostname}`) && opts.credentials !== 'omit') {
167
- const cookie = event.request.headers.get('cookie');
168
- if (cookie) opts.headers.set('cookie', cookie);
169
- }
143
+ if (request.credentials !== 'omit') {
144
+ const cookie = get_cookie_header(url, request.headers.get('cookie'));
145
+ if (cookie) {
146
+ request.headers.set('cookie', cookie);
147
+ }
170
148
 
171
- // we need to delete the connection header, as explained here:
172
- // https://github.com/nodejs/undici/issues/1470#issuecomment-1140798467
173
- // TODO this may be a case for being selective about which headers we let through
174
- opts.headers.delete('connection');
149
+ const authorization = event.request.headers.get('authorization');
150
+ if (authorization && !request.headers.has('authorization')) {
151
+ request.headers.set('authorization', authorization);
152
+ }
153
+ }
175
154
 
176
- const external_request = new Request(requested, /** @type {RequestInit} */ (opts));
177
- response = await options.hooks.externalFetch.call(null, external_request);
155
+ if (request_body && typeof request_body !== 'string') {
156
+ // TODO is this still necessary? we just bail out below
157
+ // per https://developer.mozilla.org/en-US/docs/Web/API/Request/Request, this can be a
158
+ // Blob, BufferSource, FormData, URLSearchParams, USVString, or ReadableStream object.
159
+ // non-string bodies are irksome to deal with, but luckily aren't particularly useful
160
+ // in this context anyway, so we take the easy route and ban them
161
+ throw new Error('Request body must be a string');
162
+ }
178
163
 
179
- if (opts.mode === 'no-cors') {
180
- response = new Response('', {
181
- status: response.status,
182
- statusText: response.statusText,
183
- headers: response.headers
164
+ response = await respond(request, options, {
165
+ prerender_default,
166
+ ...state,
167
+ initiator: route
184
168
  });
185
- } else {
186
- if (url.origin !== event.url.origin) {
187
- const acao = response.headers.get('access-control-allow-origin');
188
- if (!acao || (acao !== event.url.origin && acao !== '*')) {
189
- throw new Error(
190
- `CORS error: ${
191
- acao ? 'Incorrect' : 'No'
192
- } 'Access-Control-Allow-Origin' header is present on the requested resource`
193
- );
194
- }
169
+
170
+ if (state.prerendering) {
171
+ dependency = { response, body: null };
172
+ state.prerendering.dependencies.set(url.pathname, dependency);
195
173
  }
174
+
175
+ return response;
196
176
  }
197
- }
177
+ });
198
178
 
199
179
  const set_cookie = response.headers.get('set-cookie');
200
180
  if (set_cookie) {
201
- cookies.push(
181
+ set_cookies.push(
202
182
  ...set_cookie_parser
203
183
  .splitCookiesString(set_cookie)
204
184
  .map((str) => set_cookie_parser.parseString(str))
@@ -210,17 +190,7 @@ export function create_fetch({ event, options, state, route, prerender_default }
210
190
  async function text() {
211
191
  const body = await response.text();
212
192
 
213
- // TODO just pass `response.headers`, for processing inside `serialize_data`
214
- /** @type {import('types').ResponseHeaders} */
215
- const headers = {};
216
- for (const [key, value] of response.headers) {
217
- // TODO skip others besides set-cookie and etag?
218
- if (key !== 'set-cookie' && key !== 'etag') {
219
- headers[key] = value;
220
- }
221
- }
222
-
223
- if (!opts.body || typeof opts.body === 'string') {
193
+ if (!body || typeof body === 'string') {
224
194
  const status_number = Number(response.status);
225
195
  if (isNaN(status_number)) {
226
196
  throw new Error(
@@ -231,16 +201,31 @@ export function create_fetch({ event, options, state, route, prerender_default }
231
201
  }
232
202
 
233
203
  fetched.push({
234
- url: requested,
235
- method: opts.method || 'GET',
236
- body: opts.body,
237
- response: {
238
- status: status_number,
239
- statusText: response.statusText,
240
- headers,
241
- body
242
- }
204
+ url: request.url.startsWith(event.url.origin)
205
+ ? request.url.slice(event.url.origin.length)
206
+ : request.url,
207
+ method: request.method,
208
+ request_body: /** @type {string | undefined} */ (request_body),
209
+ response_body: body,
210
+ response: response
243
211
  });
212
+
213
+ // ensure that excluded headers can't be read
214
+ const get = response.headers.get;
215
+ response.headers.get = (key) => {
216
+ const lower = key.toLowerCase();
217
+ const value = get.call(response.headers, lower);
218
+ if (value && !lower.startsWith('x-sveltekit-')) {
219
+ const included = resolve_opts.filterSerializedResponseHeaders(lower, value);
220
+ if (!included) {
221
+ throw new Error(
222
+ `Failed to get response header "${lower}" — it must be included by the \`filterSerializedResponseHeaders\` option: https://kit.svelte.dev/docs/hooks#handle`
223
+ );
224
+ }
225
+ }
226
+
227
+ return value;
228
+ };
244
229
  }
245
230
 
246
231
  if (dependency) {
@@ -284,5 +269,18 @@ export function create_fetch({ event, options, state, route, prerender_default }
284
269
  return proxy;
285
270
  };
286
271
 
287
- return { fetcher, fetched, cookies };
272
+ return { fetcher, fetched, cookies: set_cookies };
273
+ }
274
+
275
+ /**
276
+ * @param {RequestInfo | URL} info
277
+ * @param {RequestInit | undefined} init
278
+ * @param {URL} url
279
+ */
280
+ function normalize_fetch_input(info, init, url) {
281
+ if (info instanceof Request) {
282
+ return info;
283
+ }
284
+
285
+ return new Request(typeof info === 'string' ? new URL(info, url) : info, init);
288
286
  }
@@ -131,7 +131,8 @@ export async function render_page(event, route, page, options, state, resolve_op
131
131
  options,
132
132
  state,
133
133
  route,
134
- prerender_default: should_prerender
134
+ prerender_default: should_prerender,
135
+ resolve_opts
135
136
  });
136
137
 
137
138
  if (get_option(nodes, 'ssr') === false) {
@@ -284,7 +284,11 @@ export async function render_response({
284
284
  }
285
285
 
286
286
  if (page_config.ssr && page_config.csr) {
287
- body += `\n\t${fetched.map((item) => serialize_data(item, !!state.prerendering)).join('\n\t')}`;
287
+ body += `\n\t${fetched
288
+ .map((item) =>
289
+ serialize_data(item, resolve_opts.filterSerializedResponseHeaders, !!state.prerendering)
290
+ )
291
+ .join('\n\t')}`;
288
292
  }
289
293
 
290
294
  if (options.service_worker) {
@@ -321,6 +325,7 @@ export async function render_response({
321
325
  })) || '';
322
326
 
323
327
  const headers = new Headers({
328
+ 'x-sveltekit-page': 'true',
324
329
  'content-type': 'text/html',
325
330
  etag: `"${hash(html)}"`
326
331
  });
@@ -25,7 +25,8 @@ export async function respond_with_error({ event, options, state, status, error,
25
25
  event,
26
26
  options,
27
27
  state,
28
- route: GENERIC_ERROR
28
+ route: GENERIC_ERROR,
29
+ resolve_opts
29
30
  });
30
31
 
31
32
  try {
@@ -35,15 +35,35 @@ 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 {(name: string, value: string) => boolean} filter
38
39
  * @param {boolean} [prerendering]
39
40
  * @returns {string} The raw HTML of a script element carrying the JSON payload.
40
41
  * @example const html = serialize_data('/data.json', null, { foo: 'bar' });
41
42
  */
42
- export function serialize_data(fetched, prerendering = false) {
43
- const safe_payload = JSON.stringify(fetched.response).replace(
44
- pattern,
45
- (match) => replacements[match]
46
- );
43
+ export function serialize_data(fetched, filter, prerendering = false) {
44
+ /** @type {Record<string, string>} */
45
+ const headers = {};
46
+
47
+ let cache_control = null;
48
+ let age = null;
49
+
50
+ for (const [key, value] of fetched.response.headers) {
51
+ if (filter(key, value)) {
52
+ headers[key] = value;
53
+ }
54
+
55
+ if (key === 'cache-control') cache_control = value;
56
+ if (key === 'age') age = value;
57
+ }
58
+
59
+ const payload = {
60
+ status: fetched.response.status,
61
+ statusText: fetched.response.statusText,
62
+ headers,
63
+ body: fetched.response_body
64
+ };
65
+
66
+ const safe_payload = JSON.stringify(payload).replace(pattern, (match) => replacements[match]);
47
67
 
48
68
  const attrs = [
49
69
  'type="application/json"',
@@ -51,20 +71,15 @@ export function serialize_data(fetched, prerendering = false) {
51
71
  `data-url=${escape_html_attr(fetched.url)}`
52
72
  ];
53
73
 
54
- if (fetched.body) {
55
- attrs.push(`data-hash=${escape_html_attr(hash(fetched.body))}`);
74
+ if (fetched.request_body) {
75
+ attrs.push(`data-hash=${escape_html_attr(hash(fetched.request_body))}`);
56
76
  }
57
77
 
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
- }
78
+ if (!prerendering && fetched.method === 'GET' && cache_control) {
79
+ const match = /s-maxage=(\d+)/g.exec(cache_control) ?? /max-age=(\d+)/g.exec(cache_control);
80
+ if (match) {
81
+ const ttl = +match[1] - +(age ?? '0');
82
+ attrs.push(`data-ttl="${ttl}"`);
68
83
  }
69
84
  }
70
85
 
@@ -1,16 +1,12 @@
1
- import { ResponseHeaders, SSRNode, CspDirectives } from 'types';
1
+ import { SSRNode, CspDirectives } from 'types';
2
2
  import { HttpError } from '../../control.js';
3
3
 
4
4
  export interface Fetched {
5
5
  url: string;
6
6
  method: string;
7
- body?: string | null;
8
- response: {
9
- status: number;
10
- statusText: string;
11
- headers: ResponseHeaders;
12
- body: string;
13
- };
7
+ request_body?: string | null;
8
+ response_body: string;
9
+ response: Response;
14
10
  }
15
11
 
16
12
  export interface FetchState {
@@ -12,14 +12,6 @@ export function parse_route_id(id) {
12
12
  // const add_trailing_slash = !/\.[a-z]+$/.test(key);
13
13
  let add_trailing_slash = true;
14
14
 
15
- if (/\]\[/.test(id)) {
16
- throw new Error(`Invalid route ${id} — parameters must be separated`);
17
- }
18
-
19
- if (count_occurrences('[', id) !== count_occurrences(']', id)) {
20
- throw new Error(`Invalid route ${id} — brackets are unbalanced`);
21
- }
22
-
23
15
  const pattern =
24
16
  id === ''
25
17
  ? /^\/$/
@@ -123,15 +115,3 @@ export function exec(match, names, types, matchers) {
123
115
 
124
116
  return params;
125
117
  }
126
-
127
- /**
128
- * @param {string} needle
129
- * @param {string} haystack
130
- */
131
- function count_occurrences(needle, haystack) {
132
- let count = 0;
133
- for (let i = 0; i < haystack.length; i += 1) {
134
- if (haystack[i] === needle) count += 1;
135
- }
136
- return count;
137
- }
package/types/index.d.ts CHANGED
@@ -176,10 +176,6 @@ export interface KitConfig {
176
176
  };
177
177
  }
178
178
 
179
- export interface ExternalFetch {
180
- (req: Request): Promise<Response>;
181
- }
182
-
183
179
  export interface Handle {
184
180
  (input: {
185
181
  event: RequestEvent;
@@ -191,6 +187,10 @@ export interface HandleError {
191
187
  (input: { error: Error & { frame?: string }; event: RequestEvent }): void;
192
188
  }
193
189
 
190
+ export interface HandleFetch {
191
+ (input: { event: RequestEvent; request: Request; fetch: typeof fetch }): MaybePromise<Response>;
192
+ }
193
+
194
194
  /**
195
195
  * The generic form of `PageLoad` and `LayoutLoad`. You should import those from `./$types` (see [generated types](https://kit.svelte.dev/docs/types#generated-types))
196
196
  * rather than using `Load` directly.
@@ -273,6 +273,7 @@ export interface RequestHandler<
273
273
 
274
274
  export interface ResolveOptions {
275
275
  transformPageChunk?: (input: { html: string; done: boolean }) => MaybePromise<string | undefined>;
276
+ filterSerializedResponseHeaders?: (name: string, value: string) => boolean;
276
277
  }
277
278
 
278
279
  export class Server {
@@ -3,7 +3,6 @@ import { SvelteComponent } from 'svelte/internal';
3
3
  import {
4
4
  Action,
5
5
  Config,
6
- ExternalFetch,
7
6
  ServerLoad,
8
7
  Handle,
9
8
  HandleError,
@@ -14,7 +13,8 @@ import {
14
13
  ResolveOptions,
15
14
  Server,
16
15
  ServerInitOptions,
17
- SSRManifest
16
+ SSRManifest,
17
+ HandleFetch
18
18
  } from './index.js';
19
19
  import {
20
20
  HttpMethod,
@@ -90,9 +90,9 @@ export type CSRRoute = {
90
90
  export type GetParams = (match: RegExpExecArray) => Record<string, string>;
91
91
 
92
92
  export interface Hooks {
93
- externalFetch: ExternalFetch;
94
93
  handle: Handle;
95
94
  handleError: HandleError;
95
+ handleFetch: HandleFetch;
96
96
  }
97
97
 
98
98
  export interface ImportNode {