@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 +1 -1
- package/src/core/config/options.js +4 -0
- package/src/core/postbuild/analyse.js +21 -1
- package/src/core/postbuild/prerender.js +20 -1
- package/src/core/sync/create_manifest_data/index.js +6 -0
- package/src/core/sync/write_client_manifest.js +2 -0
- package/src/core/sync/write_server.js +1 -0
- package/src/exports/public.d.ts +12 -0
- package/src/exports/vite/index.js +2 -0
- package/src/runtime/client/client.js +109 -38
- package/src/runtime/client/types.d.ts +5 -0
- package/src/runtime/client/utils.js +14 -4
- package/src/runtime/server/respond.js +5 -1
- package/src/types/internal.d.ts +1 -0
- package/src/utils/url.js +11 -14
- package/src/version.js +1 -1
- package/types/index.d.ts +12 -0
- package/types/index.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -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({
|
|
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,
|
package/src/exports/public.d.ts
CHANGED
|
@@ -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 {
|
|
428
|
-
async function _preload_code(
|
|
429
|
-
const route = routes.find((route) => route.exec(get_url_path(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
1213
|
-
function get_url_path(
|
|
1214
|
-
return
|
|
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
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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 =
|
|
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
|
|
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)
|
|
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)
|
|
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(
|
|
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(
|
|
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
|
|
|
@@ -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
|
|
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,
|
package/src/types/internal.d.ts
CHANGED
|
@@ -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
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.
|
package/types/index.d.ts.map
CHANGED
|
@@ -159,6 +159,6 @@
|
|
|
159
159
|
null,
|
|
160
160
|
null
|
|
161
161
|
],
|
|
162
|
-
"mappings": ";;;;;;;;;kBA2BiBA,OAAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;aA2BZC,cAAcA;;;;;;aAMdC,cAAcA;;;;;;;;;;;;;;;kBAeTC,aAAaA;;;;;;;;;;;;;;;;;kBAiBbC,OAAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkGPC,MAAMA;;;;;;;;;;;;;;;;;;;;;kBAqBNC,OAAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA4DPC,QAAQA;;;;;;;;kBAQRC,SAASA
|
|
162
|
+
"mappings": "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",
|
|
163
163
|
"ignoreList": []
|
|
164
164
|
}
|