Skip to content

Commit fe8c37c

Browse files
feat: hash based routing (#13191)
* hash based routing * tests * fix test * fml this took me 40 minutes to figure out * for push/replaceState, too * more robust tests * please? * change approach: make hash visible in page.url etc, require user to pass in hash links * lint, fix test * Apply suggestions from code review * hashchange handling * rename 'history' to 'pathname', update docs * update internal jsdoc * disable SSR and server-only files * disallow page options at build time * oops * allow access to event.url.hash in hash mode * prerender shell * add failing reroute test * fix * make hash trackable * skip test for now * don't normalise * lint * i don't understand this code — even with the prior normalization it's unclear what it would do. no tests fail without it so for now i'm just gonna remove to shut typescript up * get_route_id -> get_page_key * detect hash changes via URL bar --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 7afa167 commit fe8c37c

35 files changed

+461
-46
lines changed

.changeset/thirty-wasps-itch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: add hash-based routing option

packages/kit/src/core/config/index.spec.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ const get_defaults = (prefix = '') => ({
9494
moduleExtensions: ['.js', '.ts'],
9595
output: { preloadStrategy: 'modulepreload', bundleStrategy: 'split' },
9696
outDir: join(prefix, '.svelte-kit'),
97+
router: {
98+
type: 'pathname'
99+
},
97100
serviceWorker: {
98101
register: true
99102
},

packages/kit/src/core/config/options.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ const options = object(
260260
})
261261
}),
262262

263+
router: object({
264+
type: list(['pathname', 'hash'])
265+
}),
266+
263267
serviceWorker: object({
264268
register: boolean(true),
265269
files: fun((filename) => !/\.DS_Store/.test(filename))

packages/kit/src/core/postbuild/analyse.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,22 @@ export default forked(import.meta.url, analyse);
2222

2323
/**
2424
* @param {{
25+
* hash: boolean;
2526
* manifest_path: string;
2627
* manifest_data: import('types').ManifestData;
2728
* server_manifest: import('vite').Manifest;
2829
* tracked_features: Record<string, string[]>;
2930
* env: Record<string, string>
3031
* }} opts
3132
*/
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+
}) {
3341
/** @type {import('@sveltejs/kit').SSRManifest} */
3442
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
3543

@@ -67,6 +75,18 @@ async function analyse({ manifest_path, manifest_data, server_manifest, tracked_
6775

6876
// analyse nodes
6977
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+
7090
metadata.nodes[node.index] = {
7191
has_server_load: node.server?.load !== undefined || node.server?.trailingSlash !== undefined
7292
};

packages/kit/src/core/postbuild/prerender.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { crawl } from './crawl.js';
1313
import { forked } from '../../utils/fork.js';
1414
import * as devalue from 'devalue';
1515
import { createReadableStream } from '@sveltejs/kit/node';
16+
import generate_fallback from './fallback.js';
1617

1718
export default forked(import.meta.url, prerender);
1819

@@ -24,14 +25,15 @@ const SPECIAL_HASHLINKS = new Set(['', 'top']);
2425

2526
/**
2627
* @param {{
28+
* hash: boolean;
2729
* out: string;
2830
* manifest_path: string;
2931
* metadata: import('types').ServerMetadata;
3032
* verbose: boolean;
3133
* env: Record<string, string>
3234
* }} opts
3335
*/
34-
async function prerender({ out, manifest_path, metadata, verbose, env }) {
36+
async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
3537
/** @type {import('@sveltejs/kit').SSRManifest} */
3638
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
3739

@@ -98,6 +100,23 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
98100
/** @type {import('types').ValidatedKitConfig} */
99101
const config = (await load_config()).kit;
100102

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+
101120
const emulator = await config.adapter?.emulate?.();
102121

103122
/** @type {import('types').Logger} */

packages/kit/src/core/sync/create_manifest_data/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,12 @@ function create_routes_and_nodes(cwd, config, fallback) {
271271
config.kit.moduleExtensions
272272
);
273273

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+
274280
/**
275281
* @param {string} type
276282
* @param {string} existing_file

packages/kit/src/core/sync/write_client_manifest.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
158158
159159
export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
160160
161+
export const hash = ${JSON.stringify(kit.router.type === 'hash')};
162+
161163
export const decode = (type, value) => decoders[type](value);
162164
163165
export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';

packages/kit/src/core/sync/write_server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const options = {
4242
embedded: ${config.kit.embedded},
4343
env_public_prefix: '${config.kit.env.publicPrefix}',
4444
env_private_prefix: '${config.kit.env.privatePrefix}',
45+
hash_routing: ${s(config.kit.router.type === 'hash')},
4546
hooks: null, // added lazily, via \`get_hooks\`
4647
preload_strategy: ${s(config.kit.output.preloadStrategy)},
4748
root,

packages/kit/src/exports/public.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,16 @@ export interface KitConfig {
617617
*/
618618
origin?: string;
619619
};
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+
*
626+
* @default "pathname"
627+
*/
628+
type?: 'pathname' | 'hash';
629+
};
620630
serviceWorker?: {
621631
/**
622632
* Whether to automatically register the service worker, if it exists.

packages/kit/src/exports/vite/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,7 @@ async function kit({ svelte_config }) {
793793
);
794794

795795
const metadata = await analyse({
796+
hash: kit.router.type === 'hash',
796797
manifest_path,
797798
manifest_data,
798799
server_manifest,
@@ -897,6 +898,7 @@ async function kit({ svelte_config }) {
897898

898899
// ...and prerender
899900
const { prerendered, prerender_map } = await prerender({
901+
hash: kit.router.type === 'hash',
900902
out,
901903
manifest_path,
902904
metadata,

0 commit comments

Comments
 (0)