@sveltejs/kit 2.13.0 → 2.14.1

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": "2.13.0",
3
+ "version": "2.14.1",
4
4
  "description": "SvelteKit is the fastest way to build Svelte apps",
5
5
  "keywords": [
6
6
  "framework",
@@ -260,6 +260,10 @@ const options = object(
260
260
  })
261
261
  }),
262
262
 
263
+ router: object({
264
+ type: list(['pathname', 'hash'])
265
+ }),
266
+
263
267
  serviceWorker: object({
264
268
  register: boolean(true),
265
269
  files: fun((filename) => !/\.DS_Store/.test(filename))
@@ -22,6 +22,7 @@ export default forked(import.meta.url, analyse);
22
22
 
23
23
  /**
24
24
  * @param {{
25
+ * hash: boolean;
25
26
  * manifest_path: string;
26
27
  * manifest_data: import('types').ManifestData;
27
28
  * server_manifest: import('vite').Manifest;
@@ -29,7 +30,14 @@ export default forked(import.meta.url, analyse);
29
30
  * env: Record<string, string>
30
31
  * }} opts
31
32
  */
32
- async function analyse({ manifest_path, manifest_data, server_manifest, tracked_features, env }) {
33
+ async function analyse({
34
+ hash,
35
+ manifest_path,
36
+ manifest_data,
37
+ server_manifest,
38
+ tracked_features,
39
+ env
40
+ }) {
33
41
  /** @type {import('@sveltejs/kit').SSRManifest} */
34
42
  const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
35
43
 
@@ -67,6 +75,18 @@ async function analyse({ manifest_path, manifest_data, server_manifest, tracked_
67
75
 
68
76
  // analyse nodes
69
77
  for (const node of nodes) {
78
+ if (hash && node.universal) {
79
+ const options = Object.keys(node.universal).filter((o) => o !== 'load');
80
+ if (options.length > 0) {
81
+ throw new Error(
82
+ `Page options are ignored when \`router.type === 'hash'\` (${node.universal_id} has ${options
83
+ .filter((o) => o !== 'load')
84
+ .map((o) => `'${o}'`)
85
+ .join(', ')})`
86
+ );
87
+ }
88
+ }
89
+
70
90
  metadata.nodes[node.index] = {
71
91
  has_server_load: node.server?.load !== undefined || node.server?.trailingSlash !== undefined
72
92
  };
@@ -13,6 +13,7 @@ import { crawl } from './crawl.js';
13
13
  import { forked } from '../../utils/fork.js';
14
14
  import * as devalue from 'devalue';
15
15
  import { createReadableStream } from '@sveltejs/kit/node';
16
+ import generate_fallback from './fallback.js';
16
17
 
17
18
  export default forked(import.meta.url, prerender);
18
19
 
@@ -24,6 +25,7 @@ const SPECIAL_HASHLINKS = new Set(['', 'top']);
24
25
 
25
26
  /**
26
27
  * @param {{
28
+ * hash: boolean;
27
29
  * out: string;
28
30
  * manifest_path: string;
29
31
  * metadata: import('types').ServerMetadata;
@@ -31,7 +33,7 @@ const SPECIAL_HASHLINKS = new Set(['', 'top']);
31
33
  * env: Record<string, string>
32
34
  * }} opts
33
35
  */
34
- async function prerender({ out, manifest_path, metadata, verbose, env }) {
36
+ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
35
37
  /** @type {import('@sveltejs/kit').SSRManifest} */
36
38
  const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
37
39
 
@@ -98,6 +100,23 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
98
100
  /** @type {import('types').ValidatedKitConfig} */
99
101
  const config = (await load_config()).kit;
100
102
 
103
+ if (hash) {
104
+ const fallback = await generate_fallback({
105
+ manifest_path,
106
+ env
107
+ });
108
+
109
+ const file = output_filename('/', true);
110
+ const dest = `${config.outDir}/output/prerendered/pages/${file}`;
111
+
112
+ mkdirp(dirname(dest));
113
+ writeFileSync(dest, fallback);
114
+
115
+ prerendered.pages.set('/', { file });
116
+
117
+ return { prerendered, prerender_map };
118
+ }
119
+
101
120
  const emulator = await config.adapter?.emulate?.();
102
121
 
103
122
  /** @type {import('types').Logger} */
@@ -271,6 +271,12 @@ function create_routes_and_nodes(cwd, config, fallback) {
271
271
  config.kit.moduleExtensions
272
272
  );
273
273
 
274
+ if (config.kit.router.type === 'hash' && item.kind === 'server') {
275
+ throw new Error(
276
+ `Cannot use server-only files in an app with \`router.type === 'hash': ${project_relative}`
277
+ );
278
+ }
279
+
274
280
  /**
275
281
  * @param {string} type
276
282
  * @param {string} existing_file
@@ -158,6 +158,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
158
158
 
159
159
  export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
160
160
 
161
+ export const hash = ${JSON.stringify(kit.router.type === 'hash')};
162
+
161
163
  export const decode = (type, value) => decoders[type](value);
162
164
 
163
165
  export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
@@ -42,6 +42,7 @@ export const options = {
42
42
  embedded: ${config.kit.embedded},
43
43
  env_public_prefix: '${config.kit.env.publicPrefix}',
44
44
  env_private_prefix: '${config.kit.env.privatePrefix}',
45
+ hash_routing: ${s(config.kit.router.type === 'hash')},
45
46
  hooks: null, // added lazily, via \`get_hooks\`
46
47
  preload_strategy: ${s(config.kit.output.preloadStrategy)},
47
48
  root,
@@ -617,6 +617,18 @@ export interface KitConfig {
617
617
  */
618
618
  origin?: string;
619
619
  };
620
+ router?: {
621
+ /**
622
+ * What type of client-side router to use.
623
+ * - `'pathname'` is the default and means the current URL pathname determines the route
624
+ * - `'hash'` means the route is determined by `location.hash`. In this case, SSR and prerendering are disabled. This is only recommended if `pathname` is not an option, for example because you don't control the webserver where your app is deployed.
625
+ * It comes with some caveats: you can't use server-side rendering (or indeed any server logic), and you have to make sure that the links in your app all start with /#/, or they won't work. Beyond that, everything works exactly like a normal SvelteKit app.
626
+ *
627
+ * @default "pathname"
628
+ * @since 2.14.0
629
+ */
630
+ type?: 'pathname' | 'hash';
631
+ };
620
632
  serviceWorker?: {
621
633
  /**
622
634
  * Whether to automatically register the service worker, if it exists.
@@ -793,6 +793,7 @@ async function kit({ svelte_config }) {
793
793
  );
794
794
 
795
795
  const metadata = await analyse({
796
+ hash: kit.router.type === 'hash',
796
797
  manifest_path,
797
798
  manifest_data,
798
799
  server_manifest,
@@ -897,6 +898,7 @@ async function kit({ svelte_config }) {
897
898
 
898
899
  // ...and prerender
899
900
  const { prerendered, prerender_map } = await prerender({
901
+ hash: kit.router.type === 'hash',
900
902
  out,
901
903
  manifest_path,
902
904
  metadata,
@@ -424,9 +424,9 @@ async function _preload_data(intent) {
424
424
  return load_cache.promise;
425
425
  }
426
426
 
427
- /** @param {string} pathname */
428
- async function _preload_code(pathname) {
429
- const route = routes.find((route) => route.exec(get_url_path(pathname)));
427
+ /** @param {URL} url */
428
+ async function _preload_code(url) {
429
+ const route = routes.find((route) => route.exec(get_url_path(url)));
430
430
 
431
431
  if (route) {
432
432
  await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]()));
@@ -610,6 +610,19 @@ async function load_node({ loader, parent, url, params, route, server_data_node
610
610
 
611
611
  if (DEV) {
612
612
  validate_page_exports(node.universal);
613
+
614
+ if (node.universal && app.hash) {
615
+ const options = Object.keys(node.universal).filter((o) => o !== 'load');
616
+
617
+ if (options.length > 0) {
618
+ throw new Error(
619
+ `Page options are ignored when \`router.type === 'hash'\` (${route.id} has ${options
620
+ .filter((o) => o !== 'load')
621
+ .map((o) => `'${o}'`)
622
+ .join(', ')})`
623
+ );
624
+ }
625
+ }
613
626
  }
614
627
 
615
628
  if (node.universal?.load) {
@@ -653,7 +666,8 @@ async function load_node({ loader, parent, url, params, route, server_data_node
653
666
  if (is_tracking) {
654
667
  uses.search_params.add(param);
655
668
  }
656
- }
669
+ },
670
+ app.hash
657
671
  ),
658
672
  async fetch(resource, init) {
659
673
  /** @type {URL | string} */
@@ -810,7 +824,6 @@ function create_data_node(node, previous) {
810
824
  }
811
825
 
812
826
  /**
813
- *
814
827
  * @param {URL | null} old_url
815
828
  * @param {URL} new_url
816
829
  */
@@ -875,7 +888,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
875
888
 
876
889
  /** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
877
890
  let server_data = null;
878
- const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
891
+ const url_changed = current.url ? id !== get_page_key(current.url) : false;
879
892
  const route_changed = current.route ? route.id !== current.route.id : false;
880
893
  const search_params_changed = diff_search_params(current.url, url);
881
894
 
@@ -1169,13 +1182,25 @@ async function load_root_error_page({ status, error, url, route }) {
1169
1182
  * @param {boolean} invalidating
1170
1183
  */
1171
1184
  function get_navigation_intent(url, invalidating) {
1172
- if (!url) return undefined;
1173
- if (is_external_url(url, base)) return;
1185
+ if (!url) return;
1186
+ if (is_external_url(url, base, app.hash)) return;
1174
1187
 
1175
1188
  // reroute could alter the given URL, so we pass a copy
1176
1189
  let rerouted;
1177
1190
  try {
1178
- rerouted = app.hooks.reroute({ url: new URL(url) }) ?? url.pathname;
1191
+ rerouted = app.hooks.reroute({ url: new URL(url) }) ?? url;
1192
+
1193
+ if (typeof rerouted === 'string') {
1194
+ const tmp = new URL(url); // do not mutate the incoming URL
1195
+
1196
+ if (app.hash) {
1197
+ tmp.hash = rerouted;
1198
+ } else {
1199
+ tmp.pathname = rerouted;
1200
+ }
1201
+
1202
+ rerouted = tmp;
1203
+ }
1179
1204
  } catch (e) {
1180
1205
  if (DEV) {
1181
1206
  // in development, print the error...
@@ -1195,7 +1220,7 @@ function get_navigation_intent(url, invalidating) {
1195
1220
  const params = route.exec(path);
1196
1221
 
1197
1222
  if (params) {
1198
- const id = url.pathname + url.search;
1223
+ const id = get_page_key(url);
1199
1224
  /** @type {import('./types.js').NavigationIntent} */
1200
1225
  const intent = {
1201
1226
  id,
@@ -1209,9 +1234,18 @@ function get_navigation_intent(url, invalidating) {
1209
1234
  }
1210
1235
  }
1211
1236
 
1212
- /** @param {string} pathname */
1213
- function get_url_path(pathname) {
1214
- return decode_pathname(pathname.slice(base.length) || '/');
1237
+ /** @param {URL} url */
1238
+ function get_url_path(url) {
1239
+ return (
1240
+ decode_pathname(
1241
+ app.hash ? url.hash.replace(/^#/, '').replace(/[?#].+/, '') : url.pathname.slice(base.length)
1242
+ ) || '/'
1243
+ );
1244
+ }
1245
+
1246
+ /** @param {URL} url */
1247
+ function get_page_key(url) {
1248
+ return (app.hash ? url.hash.replace(/^#/, '') : url.pathname) + url.search;
1215
1249
  }
1216
1250
 
1217
1251
  /**
@@ -1303,19 +1337,42 @@ async function navigate({
1303
1337
  let navigation_result = intent && (await load_route(intent));
1304
1338
 
1305
1339
  if (!navigation_result) {
1306
- if (is_external_url(url, base)) {
1307
- return await native_navigation(url);
1308
- }
1309
- navigation_result = await server_fallback(
1310
- url,
1311
- { id: null },
1312
- await handle_error(new SvelteKitError(404, 'Not Found', `Not found: ${url.pathname}`), {
1340
+ if (is_external_url(url, base, app.hash)) {
1341
+ if (DEV && app.hash) {
1342
+ // Special case for hash mode during DEV: If someone accidentally forgets to use a hash for the link,
1343
+ // they would end up here in an endless loop. Fall back to error page in that case
1344
+ navigation_result = await server_fallback(
1345
+ url,
1346
+ { id: null },
1347
+ await handle_error(
1348
+ new SvelteKitError(
1349
+ 404,
1350
+ 'Not Found',
1351
+ `Not found: ${url.pathname} (did you forget the hash?)`
1352
+ ),
1353
+ {
1354
+ url,
1355
+ params: {},
1356
+ route: { id: null }
1357
+ }
1358
+ ),
1359
+ 404
1360
+ );
1361
+ } else {
1362
+ return await native_navigation(url);
1363
+ }
1364
+ } else {
1365
+ navigation_result = await server_fallback(
1313
1366
  url,
1314
- params: {},
1315
- route: { id: null }
1316
- }),
1317
- 404
1318
- );
1367
+ { id: null },
1368
+ await handle_error(new SvelteKitError(404, 'Not Found', `Not found: ${url.pathname}`), {
1369
+ url,
1370
+ params: {},
1371
+ route: { id: null }
1372
+ }),
1373
+ 404
1374
+ );
1375
+ }
1319
1376
  }
1320
1377
 
1321
1378
  // if this is an internal navigation intent, use the normalized
@@ -1437,7 +1494,11 @@ async function navigate({
1437
1494
  const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;
1438
1495
 
1439
1496
  if (autoscroll) {
1440
- const deep_linked = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
1497
+ const deep_linked =
1498
+ url.hash &&
1499
+ document.getElementById(
1500
+ decodeURIComponent(app.hash ? (url.hash.split('#')[2] ?? '') : url.hash.slice(1))
1501
+ );
1441
1502
  if (scroll) {
1442
1503
  scrollTo(scroll.x, scroll.y);
1443
1504
  } else if (deep_linked) {
@@ -1547,7 +1608,7 @@ function setup_preload() {
1547
1608
  (entries) => {
1548
1609
  for (const entry of entries) {
1549
1610
  if (entry.isIntersecting) {
1550
- _preload_code(/** @type {HTMLAnchorElement} */ (entry.target).href);
1611
+ _preload_code(new URL(/** @type {HTMLAnchorElement} */ (entry.target).href));
1551
1612
  observer.unobserve(entry.target);
1552
1613
  }
1553
1614
  }
@@ -1563,13 +1624,13 @@ function setup_preload() {
1563
1624
  const a = find_anchor(element, container);
1564
1625
  if (!a) return;
1565
1626
 
1566
- const { url, external, download } = get_link_info(a, base);
1627
+ const { url, external, download } = get_link_info(a, base, app.hash);
1567
1628
  if (external || download) return;
1568
1629
 
1569
1630
  const options = get_router_options(a);
1570
1631
 
1571
1632
  // we don't want to preload data for a page we're already on
1572
- const same_url = url && current.url.pathname + current.url.search === url.pathname + url.search;
1633
+ const same_url = url && get_page_key(current.url) === get_page_key(url);
1573
1634
 
1574
1635
  if (!options.reload && !same_url) {
1575
1636
  if (priority <= options.preload_data) {
@@ -1591,7 +1652,7 @@ function setup_preload() {
1591
1652
  }
1592
1653
  }
1593
1654
  } else if (priority <= options.preload_code) {
1594
- _preload_code(/** @type {URL} */ (url).pathname);
1655
+ _preload_code(/** @type {URL} */ (url));
1595
1656
  }
1596
1657
  }
1597
1658
  }
@@ -1600,7 +1661,7 @@ function setup_preload() {
1600
1661
  observer.disconnect();
1601
1662
 
1602
1663
  for (const a of container.querySelectorAll('a')) {
1603
- const { url, external, download } = get_link_info(a, base);
1664
+ const { url, external, download } = get_link_info(a, base, app.hash);
1604
1665
  if (external || download) continue;
1605
1666
 
1606
1667
  const options = get_router_options(a);
@@ -1611,7 +1672,7 @@ function setup_preload() {
1611
1672
  }
1612
1673
 
1613
1674
  if (options.preload_code === PRELOAD_PRIORITIES.eager) {
1614
- _preload_code(/** @type {URL} */ (url).pathname);
1675
+ _preload_code(/** @type {URL} */ (url));
1615
1676
  }
1616
1677
  }
1617
1678
  }
@@ -1741,7 +1802,7 @@ export function goto(url, opts = {}) {
1741
1802
  throw new Error('Cannot call goto(...) on the server');
1742
1803
  }
1743
1804
 
1744
- url = resolve_url(url);
1805
+ url = new URL(resolve_url(url));
1745
1806
 
1746
1807
  if (url.origin !== origin) {
1747
1808
  return Promise.reject(
@@ -1855,6 +1916,8 @@ export function preloadCode(pathname) {
1855
1916
  throw new Error('Cannot call preloadCode(...) on the server');
1856
1917
  }
1857
1918
 
1919
+ const url = new URL(pathname, current.url);
1920
+
1858
1921
  if (DEV) {
1859
1922
  if (!pathname.startsWith(base)) {
1860
1923
  throw new Error(
@@ -1862,12 +1925,12 @@ export function preloadCode(pathname) {
1862
1925
  );
1863
1926
  }
1864
1927
 
1865
- if (!routes.find((route) => route.exec(get_url_path(pathname)))) {
1928
+ if (!routes.find((route) => route.exec(get_url_path(url)))) {
1866
1929
  throw new Error(`'${pathname}' did not match any routes`);
1867
1930
  }
1868
1931
  }
1869
1932
 
1870
- return _preload_code(pathname);
1933
+ return _preload_code(url);
1871
1934
  }
1872
1935
 
1873
1936
  /**
@@ -2073,7 +2136,7 @@ function _start_router() {
2073
2136
  const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
2074
2137
  if (!a) return;
2075
2138
 
2076
- const { url, external, target, download } = get_link_info(a, base);
2139
+ const { url, external, target, download } = get_link_info(a, base, app.hash);
2077
2140
  if (!url) return;
2078
2141
 
2079
2142
  // bail out before `beforeNavigate` if link opens in a different tab
@@ -2103,7 +2166,7 @@ function _start_router() {
2103
2166
 
2104
2167
  if (download) return;
2105
2168
 
2106
- const [nonhash, hash] = url.href.split('#');
2169
+ const [nonhash, hash] = (app.hash ? url.hash.replace(/^#/, '') : url.href).split('#');
2107
2170
  const same_pathname = nonhash === strip_hash(location);
2108
2171
 
2109
2172
  // Ignore the following but fire beforeNavigate
@@ -2198,11 +2261,12 @@ function _start_router() {
2198
2261
 
2199
2262
  if (method !== 'get') return;
2200
2263
 
2264
+ // It is impossible to use form actions with hash router, so we just ignore handling them here
2201
2265
  const url = new URL(
2202
2266
  (submitter?.hasAttribute('formaction') && submitter?.formAction) || form.action
2203
2267
  );
2204
2268
 
2205
- if (is_external_url(url, base)) return;
2269
+ if (is_external_url(url, base, false)) return;
2206
2270
 
2207
2271
  const event_form = /** @type {HTMLFormElement} */ (event.target);
2208
2272
 
@@ -2311,6 +2375,13 @@ function _start_router() {
2311
2375
  '',
2312
2376
  location.href
2313
2377
  );
2378
+ } else if (app.hash) {
2379
+ // If the user edits the hash via the browser URL bar, it
2380
+ // (surprisingly!) mutates `current.url`, allowing us to
2381
+ // detect it and trigger a navigation
2382
+ if (current.url.hash === location.hash) {
2383
+ navigate({ type: 'goto', url: current.url });
2384
+ }
2314
2385
  }
2315
2386
  });
2316
2387
 
@@ -38,6 +38,11 @@ export interface SvelteKitApp {
38
38
 
39
39
  decoders: Record<string, (data: any) => any>;
40
40
 
41
+ /**
42
+ * Whether or not we're using hash-based routing
43
+ */
44
+ hash: boolean;
45
+
41
46
  root: typeof SvelteComponent;
42
47
  }
43
48
 
@@ -122,8 +122,9 @@ export function find_anchor(element, target) {
122
122
  /**
123
123
  * @param {HTMLAnchorElement | SVGAElement} a
124
124
  * @param {string} base
125
+ * @param {boolean} uses_hash_router
125
126
  */
126
- export function get_link_info(a, base) {
127
+ export function get_link_info(a, base, uses_hash_router) {
127
128
  /** @type {URL | undefined} */
128
129
  let url;
129
130
 
@@ -136,7 +137,7 @@ export function get_link_info(a, base) {
136
137
  const external =
137
138
  !url ||
138
139
  !!target ||
139
- is_external_url(url, base) ||
140
+ is_external_url(url, base, uses_hash_router) ||
140
141
  (a.getAttribute('rel') || '').split(/\s+/).includes('external');
141
142
 
142
143
  const download = url?.origin === origin && a.hasAttribute('download');
@@ -296,9 +297,18 @@ export function create_updated_store() {
296
297
  }
297
298
 
298
299
  /**
300
+ * Is external if
301
+ * - origin different
302
+ * - path doesn't start with base
303
+ * - uses hash router and pathname is more than base
299
304
  * @param {URL} url
300
305
  * @param {string} base
306
+ * @param {boolean} has_pathname_in_hash
301
307
  */
302
- export function is_external_url(url, base) {
303
- return url.origin !== origin || !url.pathname.startsWith(base);
308
+ export function is_external_url(url, base, has_pathname_in_hash) {
309
+ return (
310
+ url.origin !== origin ||
311
+ !url.pathname.startsWith(base) ||
312
+ (has_pathname_in_hash && url.pathname !== (base || '/'))
313
+ );
304
314
  }
@@ -81,6 +81,10 @@ export async function respond(request, options, manifest, state) {
81
81
  }
82
82
  }
83
83
 
84
+ if (options.hash_routing && url.pathname !== base + '/' && url.pathname !== '/[fallback]') {
85
+ return text('Not found', { status: 404 });
86
+ }
87
+
84
88
  // reroute could alter the given URL, so we pass a copy
85
89
  let rerouted_path;
86
90
  try {
@@ -416,7 +420,7 @@ export async function respond(request, options, manifest, state) {
416
420
  };
417
421
  }
418
422
 
419
- if (state.prerendering?.fallback) {
423
+ if (options.hash_routing || state.prerendering?.fallback) {
420
424
  return await render_response({
421
425
  event,
422
426
  options,
@@ -370,6 +370,7 @@ export interface SSROptions {
370
370
  embedded: boolean;
371
371
  env_public_prefix: string;
372
372
  env_private_prefix: string;
373
+ hash_routing: boolean;
373
374
  hooks: ServerHooks;
374
375
  preload_strategy: ValidatedConfig['kit']['output']['preloadStrategy'];
375
376
  root: SSRComponent['default'];
package/src/utils/url.js CHANGED
@@ -85,24 +85,13 @@ export function strip_hash({ href }) {
85
85
  return href.split('#')[0];
86
86
  }
87
87
 
88
- /**
89
- * URL properties that could change during the lifetime of the page,
90
- * which excludes things like `origin`
91
- */
92
- const tracked_url_properties = /** @type {const} */ ([
93
- 'href',
94
- 'pathname',
95
- 'search',
96
- 'toString',
97
- 'toJSON'
98
- ]);
99
-
100
88
  /**
101
89
  * @param {URL} url
102
90
  * @param {() => void} callback
103
91
  * @param {(search_param: string) => void} search_params_callback
92
+ * @param {boolean} [allow_hash]
104
93
  */
105
- export function make_trackable(url, callback, search_params_callback) {
94
+ export function make_trackable(url, callback, search_params_callback, allow_hash = false) {
106
95
  const tracked = new URL(url);
107
96
 
108
97
  Object.defineProperty(tracked, 'searchParams', {
@@ -127,10 +116,18 @@ export function make_trackable(url, callback, search_params_callback) {
127
116
  configurable: true
128
117
  });
129
118
 
119
+ /**
120
+ * URL properties that could change during the lifetime of the page,
121
+ * which excludes things like `origin`
122
+ */
123
+ const tracked_url_properties = ['href', 'pathname', 'search', 'toString', 'toJSON'];
124
+ if (allow_hash) tracked_url_properties.push('hash');
125
+
130
126
  for (const property of tracked_url_properties) {
131
127
  Object.defineProperty(tracked, property, {
132
128
  get() {
133
129
  callback();
130
+ // @ts-expect-error
134
131
  return url[property];
135
132
  },
136
133
 
@@ -151,7 +148,7 @@ export function make_trackable(url, callback, search_params_callback) {
151
148
  };
152
149
  }
153
150
 
154
- if (DEV || !BROWSER) {
151
+ if ((DEV || !BROWSER) && !allow_hash) {
155
152
  disable_hash(tracked);
156
153
  }
157
154
 
package/src/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // generated during release, do not modify
2
2
 
3
3
  /** @type {string} */
4
- export const VERSION = '2.13.0';
4
+ export const VERSION = '2.14.1';
package/types/index.d.ts CHANGED
@@ -599,6 +599,18 @@ declare module '@sveltejs/kit' {
599
599
  */
600
600
  origin?: string;
601
601
  };
602
+ router?: {
603
+ /**
604
+ * What type of client-side router to use.
605
+ * - `'pathname'` is the default and means the current URL pathname determines the route
606
+ * - `'hash'` means the route is determined by `location.hash`. In this case, SSR and prerendering are disabled. This is only recommended if `pathname` is not an option, for example because you don't control the webserver where your app is deployed.
607
+ * It comes with some caveats: you can't use server-side rendering (or indeed any server logic), and you have to make sure that the links in your app all start with /#/, or they won't work. Beyond that, everything works exactly like a normal SvelteKit app.
608
+ *
609
+ * @default "pathname"
610
+ * @since 2.14.0
611
+ */
612
+ type?: 'pathname' | 'hash';
613
+ };
602
614
  serviceWorker?: {
603
615
  /**
604
616
  * Whether to automatically register the service worker, if it exists.
@@ -159,6 +159,6 @@
159
159
  null,
160
160
  null
161
161
  ],
162
- "mappings": "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",
162
+ "mappings": "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",
163
163
  "ignoreList": []
164
164
  }