@sveltejs/kit 1.10.0 → 1.12.0

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.10.0",
3
+ "version": "1.12.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -22,7 +22,7 @@
22
22
  "set-cookie-parser": "^2.5.1",
23
23
  "sirv": "^2.0.2",
24
24
  "tiny-glob": "^0.2.9",
25
- "undici": "5.20.0"
25
+ "undici": "5.21.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@playwright/test": "^1.29.2",
@@ -34,7 +34,7 @@
34
34
  "@types/set-cookie-parser": "^2.4.2",
35
35
  "marked": "^4.2.3",
36
36
  "rollup": "^3.7.0",
37
- "svelte": "^3.55.1",
37
+ "svelte": "^3.56.0",
38
38
  "svelte-preprocess": "^5.0.0",
39
39
  "typescript": "^4.9.4",
40
40
  "uvu": "^0.5.6",
@@ -122,15 +122,7 @@ async function analyse({ manifest_path, env }) {
122
122
  validate_common_exports(page.universal, page.universal_id);
123
123
  }
124
124
 
125
- const should_prerender = get_option(nodes, 'prerender');
126
- prerender =
127
- should_prerender === true ||
128
- // Try prerendering if ssr is false and no server needed. Set it to 'auto' so that
129
- // the route is not removed from the manifest, there could be a server load function.
130
- // People can opt out of this behavior by explicitly setting prerender to false
131
- (should_prerender !== false && get_option(nodes, 'ssr') === false && !page?.server?.actions
132
- ? 'auto'
133
- : should_prerender ?? false);
125
+ prerender = get_option(nodes, 'prerender') ?? false;
134
126
 
135
127
  config = get_config(nodes);
136
128
  }
@@ -1,3 +1,4 @@
1
+ import { resolve } from '../../utils/url.js';
1
2
  import { decode } from './entities.js';
2
3
 
3
4
  const DOCTYPE = 'DOCTYPE';
@@ -12,8 +13,11 @@ const ATTRIBUTE_NAME = /[^\t\n\f />"'=]/;
12
13
 
13
14
  const WHITESPACE = /[\s\n\r]/;
14
15
 
15
- /** @param {string} html */
16
- export function crawl(html) {
16
+ /**
17
+ * @param {string} html
18
+ * @param {string} base
19
+ */
20
+ export function crawl(html, base) {
17
21
  /** @type {string[]} */
18
22
  const ids = [];
19
23
 
@@ -157,7 +161,11 @@ export function crawl(html) {
157
161
  value = decode(value);
158
162
 
159
163
  if (name === 'href') {
160
- href = value;
164
+ if (tag === 'BASE') {
165
+ base = resolve(base, value);
166
+ } else {
167
+ href = resolve(base, value);
168
+ }
161
169
  } else if (name === 'id') {
162
170
  ids.push(value);
163
171
  } else if (name === 'name') {
@@ -165,25 +173,28 @@ export function crawl(html) {
165
173
  } else if (name === 'rel') {
166
174
  rel = value;
167
175
  } else if (name === 'src') {
168
- if (value) hrefs.push(value);
176
+ if (value) hrefs.push(resolve(base, value));
169
177
  } else if (name === 'srcset') {
170
178
  const candidates = [];
171
179
  let insideURL = true;
172
180
  value = value.trim();
173
181
  for (let i = 0; i < value.length; i++) {
174
- if (value[i] === ',' && (!insideURL || (insideURL && value[i + 1] === ' '))) {
182
+ if (
183
+ value[i] === ',' &&
184
+ (!insideURL || (insideURL && WHITESPACE.test(value[i + 1])))
185
+ ) {
175
186
  candidates.push(value.slice(0, i));
176
187
  value = value.substring(i + 1).trim();
177
188
  i = 0;
178
189
  insideURL = true;
179
- } else if (value[i] === ' ') {
190
+ } else if (WHITESPACE.test(value[i])) {
180
191
  insideURL = false;
181
192
  }
182
193
  }
183
194
  candidates.push(value);
184
195
  for (const candidate of candidates) {
185
196
  const src = candidate.split(WHITESPACE)[0];
186
- if (src) hrefs.push(src);
197
+ if (src) hrefs.push(resolve(base, src));
187
198
  }
188
199
  }
189
200
  } else {
@@ -195,7 +206,7 @@ export function crawl(html) {
195
206
  }
196
207
 
197
208
  if (href && !/\bexternal\b/i.test(rel)) {
198
- hrefs.push(href);
209
+ hrefs.push(resolve(base, href));
199
210
  }
200
211
  }
201
212
  }
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { installPolyfills } from '../../exports/node/polyfills.js';
@@ -144,6 +144,13 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
144
144
  }
145
145
 
146
146
  const files = new Set(walk(`${out}/client`).map(posixify));
147
+
148
+ const immutable = `${config.appDir}/immutable`;
149
+ if (existsSync(`${out}/server/${immutable}`)) {
150
+ for (const file of walk(`${out}/server/${immutable}`)) {
151
+ files.add(posixify(`${config.appDir}/immutable/${file}`));
152
+ }
153
+ }
147
154
  const seen = new Set();
148
155
  const written = new Set();
149
156
 
@@ -240,17 +247,14 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
240
247
  const headers = Object.fromEntries(response.headers);
241
248
 
242
249
  if (config.prerender.crawl && headers['content-type'] === 'text/html') {
243
- const { ids, hrefs } = crawl(body.toString());
250
+ const { ids, hrefs } = crawl(body.toString(), decoded);
244
251
 
245
252
  actual_hashlinks.set(decoded, ids);
246
253
 
247
254
  for (const href of hrefs) {
248
- if (href.startsWith('data:')) continue;
249
-
250
- const resolved = resolve(encoded, href);
251
- if (!is_root_relative(resolved)) continue;
255
+ if (!is_root_relative(href)) continue;
252
256
 
253
- const { pathname, search, hash } = new URL(resolved, 'http://localhost');
257
+ const { pathname, search, hash } = new URL(href, 'http://localhost');
254
258
 
255
259
  if (search) {
256
260
  // TODO warn that query strings have no effect on statically-exported pages
@@ -176,7 +176,12 @@ function load_user_tsconfig(cwd) {
176
176
  function validate_user_config(kit, cwd, out, config) {
177
177
  // we need to check that the user's tsconfig extends the framework config
178
178
  const extend = config.options.extends;
179
- const extends_framework_config = extend && path.resolve(cwd, extend) === out;
179
+ const extends_framework_config =
180
+ typeof extend === 'string'
181
+ ? path.resolve(cwd, extend) === out
182
+ : Array.isArray(extend)
183
+ ? extend.some((e) => path.resolve(cwd, e) === out)
184
+ : false;
180
185
 
181
186
  const options = config.options.compilerOptions || {};
182
187
 
@@ -592,7 +592,10 @@ export function tweak_types(content, is_server) {
592
592
  });
593
593
  }
594
594
 
595
- if (node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
595
+ if (
596
+ ts.canHaveModifiers(node) &&
597
+ ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
598
+ ) {
596
599
  if (ts.isFunctionDeclaration(node) && node.name?.text && names.has(node.name?.text)) {
597
600
  exports.set(node.name.text, node.name.text);
598
601
  }
@@ -33,24 +33,30 @@ export async function build_service_worker(
33
33
 
34
34
  const service_worker = `${kit.outDir}/generated/service-worker.js`;
35
35
 
36
+ // in a service worker, `location` is the location of the service worker itself,
37
+ // which is guaranteed to be `<base>/service-worker.js`
38
+ const base = `location.pathname.split('/').slice(0, -1).join('/')`;
39
+
36
40
  fs.writeFileSync(
37
41
  service_worker,
38
42
  dedent`
43
+ export const base = /*@__PURE__*/ ${base};
44
+
39
45
  export const build = [
40
46
  ${Array.from(build)
41
- .map((file) => `${s(`${kit.paths.base}/${file}`)}`)
47
+ .map((file) => `base + ${s(`/${file}`)}`)
42
48
  .join(',\n')}
43
49
  ];
44
50
 
45
51
  export const files = [
46
52
  ${manifest_data.assets
47
53
  .filter((asset) => kit.serviceWorker.files(asset.file))
48
- .map((asset) => `${s(`${kit.paths.base}/${asset.file}`)}`)
54
+ .map((asset) => `base + ${s(`/${asset.file}`)}`)
49
55
  .join(',\n')}
50
56
  ];
51
57
 
52
58
  export const prerendered = [
53
- ${prerendered.paths.map((path) => s(path)).join(',\n')}
59
+ ${prerendered.paths.map((path) => `base + ${s(path.replace(kit.paths.base, ''))}`).join(',\n')}
54
60
  ];
55
61
 
56
62
  export const version = ${s(kit.version.name)};
@@ -175,15 +175,20 @@ export async function dev(vite, vite_config, svelte_config) {
175
175
  const styles = {};
176
176
 
177
177
  for (const dep of deps) {
178
- const parsed = new URL(dep.url, 'http://localhost/');
179
- const query = parsed.searchParams;
178
+ const url = new URL(dep.url, 'http://localhost/');
179
+ const query = url.searchParams;
180
180
 
181
181
  if (
182
182
  isCSSRequest(dep.file) ||
183
183
  (query.has('svelte') && query.get('type') === 'style')
184
184
  ) {
185
+ // setting `?inline` to load CSS modules as css string
186
+ query.set('inline', '');
187
+
185
188
  try {
186
- const mod = await loud_ssr_load_module(dep.url);
189
+ const mod = await loud_ssr_load_module(
190
+ `${url.pathname}${url.search}${url.hash}`
191
+ );
187
192
  styles[dep.url] = mod.default;
188
193
  } catch {
189
194
  // this can happen with dynamically imported modules, I think
@@ -335,7 +335,10 @@ function kit({ svelte_config }) {
335
335
 
336
336
  async load(id, options) {
337
337
  const browser = !options?.ssr;
338
- const global = `globalThis.__sveltekit_${version_hash}`;
338
+
339
+ const global = is_build
340
+ ? `globalThis.__sveltekit_${version_hash}`
341
+ : `globalThis.__sveltekit_dev`;
339
342
 
340
343
  if (options?.ssr === false && process.env.TEST !== 'true') {
341
344
  const normalized_cwd = vite.normalizePath(cwd);
@@ -108,8 +108,31 @@ export async function preview(vite, vite_config, svelte_config) {
108
108
  let prerendered = is_file(filename);
109
109
 
110
110
  if (!prerendered) {
111
- filename += filename.endsWith('/') ? 'index.html' : '.html';
112
- prerendered = is_file(filename);
111
+ const has_trailing_slash = pathname.endsWith('/');
112
+ const html_filename = `${filename}${has_trailing_slash ? 'index.html' : '.html'}`;
113
+
114
+ let redirect;
115
+
116
+ if (is_file(html_filename)) {
117
+ filename = html_filename;
118
+ prerendered = true;
119
+ } else if (has_trailing_slash) {
120
+ if (is_file(filename.slice(0, -1) + '.html')) {
121
+ redirect = pathname.slice(0, -1);
122
+ }
123
+ } else if (is_file(filename + '/index.html')) {
124
+ redirect = pathname + '/';
125
+ }
126
+
127
+ if (redirect) {
128
+ res.writeHead(307, {
129
+ location: redirect
130
+ });
131
+
132
+ res.end();
133
+
134
+ return;
135
+ }
113
136
  }
114
137
 
115
138
  if (prerendered) {
@@ -91,7 +91,8 @@ export function enhance(form, submit = () => {}) {
91
91
  cancel,
92
92
  controller,
93
93
  data,
94
- form
94
+ form,
95
+ submitter: event.submitter
95
96
  })) ?? fallback_callback;
96
97
  if (cancelled) return;
97
98
 
@@ -177,6 +177,14 @@ export function create_client(app, target) {
177
177
  });
178
178
  }
179
179
 
180
+ function persist_state() {
181
+ update_scroll_positions(current_history_index);
182
+ storage.set(SCROLL_KEY, scroll_positions);
183
+
184
+ capture_snapshot(current_history_index);
185
+ storage.set(SNAPSHOT_KEY, snapshots);
186
+ }
187
+
180
188
  /**
181
189
  * @param {string | URL} url
182
190
  * @param {{ noScroll?: boolean; replaceState?: boolean; keepFocus?: boolean; state?: any; invalidateAll?: boolean }} opts
@@ -269,6 +277,9 @@ export function create_client(app, target) {
269
277
  let navigation_result = intent && (await load_route(intent));
270
278
 
271
279
  if (!navigation_result) {
280
+ if (is_external_url(url, base)) {
281
+ return await native_navigation(url);
282
+ }
272
283
  navigation_result = await server_fallback(
273
284
  url,
274
285
  { id: null },
@@ -379,17 +390,7 @@ export function create_client(app, target) {
379
390
  // need to render the DOM before we can scroll to the rendered elements and do focus management
380
391
  await tick();
381
392
 
382
- const changed_focus =
383
- // reset focus only if any manual focus management didn't override it
384
- document.activeElement !== activeElement &&
385
- // also refocus when activeElement is body already because the
386
- // focus event might not have been fired on it yet
387
- document.activeElement !== document.body;
388
-
389
- if (!keepfocus && !changed_focus) {
390
- await reset_focus();
391
- }
392
-
393
+ // we reset scroll before dealing with focus, to avoid a flash of unscrolled content
393
394
  if (autoscroll) {
394
395
  const deep_linked =
395
396
  url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
@@ -404,6 +405,17 @@ export function create_client(app, target) {
404
405
  scrollTo(0, 0);
405
406
  }
406
407
  }
408
+
409
+ const changed_focus =
410
+ // reset focus only if any manual focus management didn't override it
411
+ document.activeElement !== activeElement &&
412
+ // also refocus when activeElement is body already because the
413
+ // focus event might not have been fired on it yet
414
+ document.activeElement !== document.body;
415
+
416
+ if (!keepfocus && !changed_focus) {
417
+ await reset_focus();
418
+ }
407
419
  } else {
408
420
  await tick();
409
421
  }
@@ -1180,6 +1192,15 @@ export function create_client(app, target) {
1180
1192
  route
1181
1193
  });
1182
1194
  }
1195
+
1196
+ if (__SVELTEKIT_DEV__ && status !== 404) {
1197
+ console.error(
1198
+ 'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)'
1199
+ );
1200
+
1201
+ debugger;
1202
+ }
1203
+
1183
1204
  return await native_navigation(url);
1184
1205
  }
1185
1206
 
@@ -1437,6 +1458,8 @@ export function create_client(app, target) {
1437
1458
  addEventListener('beforeunload', (e) => {
1438
1459
  let should_block = false;
1439
1460
 
1461
+ persist_state();
1462
+
1440
1463
  if (!navigating) {
1441
1464
  // If we're navigating, beforeNavigate was already called. If we end up in here during navigation,
1442
1465
  // it's due to an external or full-page-reload link, for which we don't want to call the hook again.
@@ -1466,11 +1489,7 @@ export function create_client(app, target) {
1466
1489
 
1467
1490
  addEventListener('visibilitychange', () => {
1468
1491
  if (document.visibilityState === 'hidden') {
1469
- update_scroll_positions(current_history_index);
1470
- storage.set(SCROLL_KEY, scroll_positions);
1471
-
1472
- capture_snapshot(current_history_index);
1473
- storage.set(SNAPSHOT_KEY, snapshots);
1492
+ persist_state();
1474
1493
  }
1475
1494
  });
1476
1495
 
@@ -1520,11 +1539,14 @@ export function create_client(app, target) {
1520
1539
 
1521
1540
  // Ignore the following but fire beforeNavigate
1522
1541
  if (external || options.reload) {
1523
- const navigation = before_navigate({ url, type: 'link' });
1524
- if (!navigation) {
1542
+ if (before_navigate({ url, type: 'link' })) {
1543
+ // set `navigating` to `true` to prevent `beforeNavigate` callbacks
1544
+ // being called when the page unloads
1545
+ navigating = true;
1546
+ } else {
1525
1547
  event.preventDefault();
1526
1548
  }
1527
- navigating = true;
1549
+
1528
1550
  return;
1529
1551
  }
1530
1552
 
@@ -1550,11 +1572,11 @@ export function create_client(app, target) {
1550
1572
  navigate({
1551
1573
  url,
1552
1574
  scroll: options.noscroll ? scroll_state() : null,
1553
- keepfocus: false,
1575
+ keepfocus: options.keep_focus ?? false,
1554
1576
  redirect_chain: [],
1555
1577
  details: {
1556
1578
  state: {},
1557
- replaceState: url.href === location.href
1579
+ replaceState: options.replace_state ?? url.href === location.href
1558
1580
  },
1559
1581
  accepted: () => event.preventDefault(),
1560
1582
  blocked: () => event.preventDefault(),
@@ -1585,7 +1607,7 @@ export function create_client(app, target) {
1585
1607
 
1586
1608
  const event_form = /** @type {HTMLFormElement} */ (event.target);
1587
1609
 
1588
- const { noscroll, reload } = get_router_options(event_form);
1610
+ const { keep_focus, noscroll, reload, replace_state } = get_router_options(event_form);
1589
1611
  if (reload) return;
1590
1612
 
1591
1613
  event.preventDefault();
@@ -1604,11 +1626,11 @@ export function create_client(app, target) {
1604
1626
  navigate({
1605
1627
  url,
1606
1628
  scroll: noscroll ? scroll_state() : null,
1607
- keepfocus: false,
1629
+ keepfocus: keep_focus ?? false,
1608
1630
  redirect_chain: [],
1609
1631
  details: {
1610
1632
  state: {},
1611
- replaceState: false
1633
+ replaceState: replace_state ?? url.href === location.href
1612
1634
  },
1613
1635
  nav_token: {},
1614
1636
  accepted: () => {},
@@ -32,8 +32,10 @@ const warned = new WeakSet();
32
32
  const valid_link_options = /** @type {const} */ ({
33
33
  'preload-code': ['', 'off', 'tap', 'hover', 'viewport', 'eager'],
34
34
  'preload-data': ['', 'off', 'tap', 'hover'],
35
+ keepfocus: ['', 'off'],
35
36
  noscroll: ['', 'off'],
36
- reload: ['', 'off']
37
+ reload: ['', 'off'],
38
+ replacestate: ['', 'off']
37
39
  });
38
40
 
39
41
  /**
@@ -141,6 +143,9 @@ export function get_link_info(a, base) {
141
143
  * @param {HTMLFormElement | HTMLAnchorElement | SVGAElement} element
142
144
  */
143
145
  export function get_router_options(element) {
146
+ /** @type {ValidLinkOptions<'keepfocus'> | null} */
147
+ let keep_focus = null;
148
+
144
149
  /** @type {ValidLinkOptions<'noscroll'> | null} */
145
150
  let noscroll = null;
146
151
 
@@ -153,14 +158,19 @@ export function get_router_options(element) {
153
158
  /** @type {ValidLinkOptions<'reload'> | null} */
154
159
  let reload = null;
155
160
 
161
+ /** @type {ValidLinkOptions<'replacestate'> | null} */
162
+ let replace_state = null;
163
+
156
164
  /** @type {Element} */
157
165
  let el = element;
158
166
 
159
167
  while (el && el !== document.documentElement) {
160
168
  if (preload_code === null) preload_code = link_option(el, 'preload-code');
161
169
  if (preload_data === null) preload_data = link_option(el, 'preload-data');
170
+ if (keep_focus === null) keep_focus = link_option(el, 'keepfocus');
162
171
  if (noscroll === null) noscroll = link_option(el, 'noscroll');
163
172
  if (reload === null) reload = link_option(el, 'reload');
173
+ if (replace_state === null) replace_state = link_option(el, 'replacestate');
164
174
 
165
175
  el = /** @type {Element} */ (parent_element(el));
166
176
  }
@@ -168,8 +178,10 @@ export function get_router_options(element) {
168
178
  return {
169
179
  preload_code: levels[preload_code ?? 'off'],
170
180
  preload_data: levels[preload_data ?? 'off'],
181
+ keep_focus: keep_focus === 'off' ? false : keep_focus === '' ? true : null,
171
182
  noscroll: noscroll === 'off' ? false : noscroll === '' ? true : null,
172
- reload: reload === 'off' ? false : reload === '' ? true : null
183
+ reload: reload === 'off' ? false : reload === '' ? true : null,
184
+ replace_state: replace_state === 'off' ? false : replace_state === '' ? true : null
173
185
  };
174
186
  }
175
187
 
@@ -27,26 +27,6 @@ export function get_cookies(request, url, trailing_slash) {
27
27
  // Emulate browser-behavior: if the cookie is set at '/foo/bar', its path is '/foo'
28
28
  const default_path = normalized_url.split('/').slice(0, -1).join('/') || '/';
29
29
 
30
- if (__SVELTEKIT_DEV__) {
31
- // TODO this could theoretically be wrong if the cookie was set unencoded?
32
- const initial_decoded_cookies = parse(header, { decode: decodeURIComponent });
33
- // Remove all cookies that no longer exist according to the request
34
- for (const name of Object.keys(cookie_paths)) {
35
- cookie_paths[name] = new Set(
36
- [...cookie_paths[name]].filter(
37
- (path) => !path_matches(normalized_url, path) || name in initial_decoded_cookies
38
- )
39
- );
40
- }
41
- // Add all new cookies we might not have seen before
42
- for (const name in initial_decoded_cookies) {
43
- cookie_paths[name] = cookie_paths[name] ?? new Set();
44
- if (![...cookie_paths[name]].some((path) => path_matches(normalized_url, path))) {
45
- cookie_paths[name].add(default_path);
46
- }
47
- }
48
- }
49
-
50
30
  /** @type {Record<string, import('./page/types').Cookie>} */
51
31
  const new_cookies = {};
52
32
 
@@ -82,20 +62,24 @@ export function get_cookies(request, url, trailing_slash) {
82
62
  const req_cookies = parse(header, { decode: decoder });
83
63
  const cookie = req_cookies[name]; // the decoded string or undefined
84
64
 
85
- if (!__SVELTEKIT_DEV__ || cookie) {
86
- return cookie;
65
+ // in development, if the cookie was set during this session with `cookies.set`,
66
+ // but at a different path, warn the user. (ignore cookies from request headers,
67
+ // since we don't know which path they were set at)
68
+ if (__SVELTEKIT_DEV__ && !cookie) {
69
+ const paths = Array.from(cookie_paths[name] ?? []).filter((path) => {
70
+ // we only care about paths that are _more_ specific than the current path
71
+ return path_matches(path, url.pathname) && path !== url.pathname;
72
+ });
73
+
74
+ if (paths.length > 0) {
75
+ console.warn(
76
+ // prettier-ignore
77
+ `'${name}' cookie does not exist for ${url.pathname}, but was previously set at ${conjoin([...paths])}. Did you mean to set its 'path' to '/' instead?`
78
+ );
79
+ }
87
80
  }
88
81
 
89
- const paths = new Set([...(cookie_paths[name] ?? [])]);
90
- if (c) {
91
- paths.add(c.options.path ?? default_path);
92
- }
93
- if (paths.size > 0) {
94
- console.warn(
95
- // prettier-ignore
96
- `Cookie with name '${name}' was not found at path '${url.pathname}', but a cookie with that name exists at these paths: '${[...paths].join("', '")}'. Did you mean to set its 'path' to '/' instead?`
97
- );
98
- }
82
+ return cookie;
99
83
  },
100
84
 
101
85
  /**
@@ -141,18 +125,11 @@ export function get_cookies(request, url, trailing_slash) {
141
125
  throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`);
142
126
  }
143
127
 
144
- cookie_paths[name] = cookie_paths[name] ?? new Set();
128
+ cookie_paths[name] ??= new Set();
129
+
145
130
  if (!value) {
146
- if (!cookie_paths[name].has(path) && cookie_paths[name].size > 0) {
147
- const paths = `'${Array.from(cookie_paths[name]).join("', '")}'`;
148
- console.warn(
149
- `Trying to delete cookie '${name}' at path '${path}', but a cookie with that name only exists at these paths: ${paths}.`
150
- );
151
- }
152
131
  cookie_paths[name].delete(path);
153
132
  } else {
154
- // We could also emit a warning here if the cookie already exists at a different path,
155
- // but that's more likely a false positive because it's valid to set the same name at different paths
156
133
  cookie_paths[name].add(path);
157
134
  }
158
135
  }
@@ -255,3 +232,11 @@ export function add_cookies_to_headers(headers, cookies) {
255
232
  headers.append('set-cookie', serialize(name, value, options));
256
233
  }
257
234
  }
235
+
236
+ /**
237
+ * @param {string[]} array
238
+ */
239
+ function conjoin(array) {
240
+ if (array.length <= 2) return array.join(' and ');
241
+ return `${array.slice(0, -1).join(', ')} and ${array.at(-1)}`;
242
+ }
@@ -79,38 +79,13 @@ export async function render_page(event, page, options, manifest, state, resolve
79
79
  // it's crucial that we do this before returning the non-SSR response, otherwise
80
80
  // SvelteKit will erroneously believe that the path has been prerendered,
81
81
  // causing functions to be omitted from the manifesst generated later
82
- const should_prerender = get_option(nodes, 'prerender');
83
-
82
+ const should_prerender = get_option(nodes, 'prerender') ?? false;
84
83
  if (should_prerender) {
85
84
  const mod = leaf_node.server;
86
85
  if (mod?.actions) {
87
86
  throw new Error('Cannot prerender pages with actions');
88
87
  }
89
88
  } else if (state.prerendering) {
90
- // Try to render the shell when ssr is false and prerendering not explicitly disabled.
91
- // People can opt out of this behavior by explicitly setting prerender to false.
92
- if (
93
- should_prerender !== false &&
94
- get_option(nodes, 'ssr') === false &&
95
- !leaf_node.server?.actions
96
- ) {
97
- return await render_response({
98
- branch: [],
99
- fetched: [],
100
- page_config: {
101
- ssr: false,
102
- csr: get_option(nodes, 'csr') ?? true
103
- },
104
- status,
105
- error: null,
106
- event,
107
- options,
108
- manifest,
109
- state,
110
- resolve_opts
111
- });
112
- }
113
-
114
89
  // if the page isn't marked as prerenderable, then bail out at this point
115
90
  return new Response(undefined, {
116
91
  status: 204
@@ -268,7 +268,7 @@ export async function render_response({
268
268
  }
269
269
  }
270
270
 
271
- const global = `__sveltekit_${options.version_hash}`;
271
+ const global = __SVELTEKIT_DEV__ ? `__sveltekit_dev` : `__sveltekit_${options.version_hash}`;
272
272
 
273
273
  const { data, chunks } = get_data(
274
274
  event,
@@ -213,7 +213,7 @@ export async function respond(request, options, manifest, state) {
213
213
 
214
214
  if (normalized !== url.pathname && !state.prerendering?.fallback) {
215
215
  return new Response(undefined, {
216
- status: 301,
216
+ status: 308,
217
217
  headers: {
218
218
  'x-sveltekit-normalize': '1',
219
219
  location:
@@ -157,7 +157,7 @@ export function exec(match, params, matchers) {
157
157
  // and the next value is defined, otherwise the buffer will cause us to skip values
158
158
  const next_param = params[i + 1];
159
159
  const next_value = values[i + 1];
160
- if (next_param && !next_param.rest && next_value) {
160
+ if (next_param && !next_param.rest && next_param.optional && next_value) {
161
161
  buffered = 0;
162
162
  }
163
163
  continue;
@@ -23,22 +23,29 @@ function defer() {
23
23
  * }}
24
24
  */
25
25
  export function create_async_iterator() {
26
- let deferred = defer();
26
+ let deferred = [defer()];
27
27
 
28
28
  return {
29
29
  iterator: {
30
30
  [Symbol.asyncIterator]() {
31
31
  return {
32
- next: () => deferred.promise
32
+ next: async () => {
33
+ const next = await deferred[0].promise;
34
+ if (!next.done) deferred.shift();
35
+ return next;
36
+ }
33
37
  };
34
38
  }
35
39
  },
36
40
  push: (value) => {
37
- deferred.fulfil({ value, done: false });
38
- deferred = defer();
41
+ deferred[deferred.length - 1].fulfil({
42
+ value,
43
+ done: false
44
+ });
45
+ deferred.push(defer());
39
46
  },
40
47
  done: () => {
41
- deferred.fulfil({ done: true });
48
+ deferred[deferred.length - 1].fulfil({ done: true });
42
49
  }
43
50
  };
44
51
  }
@@ -84,6 +84,7 @@ declare module '$app/forms' {
84
84
  form: HTMLFormElement;
85
85
  controller: AbortController;
86
86
  cancel(): void;
87
+ submitter: HTMLElement | null;
87
88
  }) => MaybePromise<
88
89
  | void
89
90
  | ((opts: {
@@ -326,6 +327,11 @@ declare module '$app/stores' {
326
327
  * This module is only available to [service workers](https://kit.svelte.dev/docs/service-workers).
327
328
  */
328
329
  declare module '$service-worker' {
330
+ /**
331
+ * The `base` path of the deployment. Typically this is equivalent to `config.kit.paths.base`, but it is calculated from `location.pathname` meaning that it will continue to work correctly if the site is deployed to a subdirectory.
332
+ * Note that there is a `base` but no `assets`, since service workers cannot be used if `config.kit.paths.assets` is specified.
333
+ */
334
+ export const base: string;
329
335
  /**
330
336
  * An array of URL strings representing the files generated by Vite, suitable for caching with `cache.addAll(build)`.
331
337
  * During development, this is an empty array.
package/types/index.d.ts CHANGED
@@ -458,6 +458,8 @@ export interface KitConfig {
458
458
  *
459
459
  * If `true`, `base` and `assets` imported from `$app/paths` will be replaced with relative asset paths during server-side rendering, resulting in portable HTML.
460
460
  * If `false`, `%sveltekit.assets%` and references to build artifacts will always be root-relative paths, unless `paths.assets` is an external URL
461
+ *
462
+ * If your app uses a `<base>` element, you should set this to `false`, otherwise asset URLs will incorrectly be resolved against the `<base>` URL rather than the current page.
461
463
  * @default undefined
462
464
  */
463
465
  relative?: boolean | undefined;
@@ -572,7 +574,22 @@ export interface KitConfig {
572
574
  */
573
575
  version?: {
574
576
  /**
575
- * The current app version string. If specified, this must be deterministic (e.g. a commit ref rather than `Math.random()` or `Date.now().toString()`), otherwise defaults to a timestamp of the build
577
+ * The current app version string. If specified, this must be deterministic (e.g. a commit ref rather than `Math.random()` or `Date.now().toString()`), otherwise defaults to a timestamp of the build.
578
+ *
579
+ * For example, to use the current commit hash, you could do use `git rev-parse HEAD`:
580
+ *
581
+ * ```js
582
+ * /// file: svelte.config.js
583
+ * import * as child_process from 'node:child_process';
584
+ *
585
+ * export default {
586
+ * kit: {
587
+ * version: {
588
+ * name: child_process.execSync('git rev-parse HEAD').toString().trim()
589
+ * }
590
+ * }
591
+ * };
592
+ * ```
576
593
  */
577
594
  name?: string;
578
595
  /**