Skip to content

feat: add reroute hook #11537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 49 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e6120b1
Add `rewriteUrl` hook
LorisSigrist Jan 8, 2024
0aee821
Fix client manifest
LorisSigrist Jan 8, 2024
daeb203
Add docs
LorisSigrist Jan 8, 2024
395aa01
Add changeset
LorisSigrist Jan 8, 2024
faf7bfe
Add tests
LorisSigrist Jan 8, 2024
d1324ff
Add more tests
LorisSigrist Jan 8, 2024
794753c
Add recursive note to docs
LorisSigrist Jan 8, 2024
83727dd
Remove empty test files
LorisSigrist Jan 8, 2024
934ad8f
404 on external rewrites
LorisSigrist Jan 8, 2024
d945538
fmt & lint
LorisSigrist Jan 8, 2024
f7c1ce2
Make sure the rewrites don't get applied to external URLs
LorisSigrist Jan 8, 2024
1fafa8a
Fix lockfile
LorisSigrist Jan 8, 2024
ef605ac
Remove unused test command
LorisSigrist Jan 8, 2024
72d0b00
Error Handling
LorisSigrist Jan 8, 2024
cf3326b
Use obfuscated error message
LorisSigrist Jan 8, 2024
d3140f7
Change to `hooks.js`
LorisSigrist Jan 8, 2024
707e07c
fmt
LorisSigrist Jan 8, 2024
1713097
Generate Types
LorisSigrist Jan 8, 2024
de8eca2
Move rewrite tests into `basic` test suite
LorisSigrist Jan 8, 2024
d2f3e29
snake_case internal variables
LorisSigrist Jan 8, 2024
e3da812
fmt
LorisSigrist Jan 8, 2024
8238dc0
Improve test organisation
LorisSigrist Jan 8, 2024
2594d8e
fmt
LorisSigrist Jan 8, 2024
e6bcf35
Update documentation/docs/30-advanced/20-hooks.md
LorisSigrist Jan 8, 2024
78e1fc6
Update documentation/docs/30-advanced/20-hooks.md
LorisSigrist Jan 8, 2024
bf2f490
Remove unnecessary lockfile changes
LorisSigrist Jan 8, 2024
a972b35
Update documentation/docs/30-advanced/20-hooks.md
LorisSigrist Jan 8, 2024
4adb7c5
Merge branch 'master' into feat-rewrite-url
LorisSigrist Jan 9, 2024
70b9787
Complete Mereg
LorisSigrist Jan 9, 2024
5f393f9
Remove unused type annotatios
LorisSigrist Jan 9, 2024
dd0cff7
isomorphic -> universal
Rich-Harris Jan 9, 2024
cf73099
Update packages/kit/src/runtime/server/ambient.d.ts
Rich-Harris Jan 9, 2024
7c29a50
remove junk
Rich-Harris Jan 9, 2024
a54f9ac
Merge branch 'feat-rewrite-url' of github.com:LorisSigrist/kit into p…
Rich-Harris Jan 9, 2024
ab2485d
Update documentation/docs/30-advanced/20-hooks.md
LorisSigrist Jan 10, 2024
b620c48
Update documentation/docs/30-advanced/20-hooks.md
LorisSigrist Jan 10, 2024
897ea4a
rename `rewriteUrl` to `reroute`
LorisSigrist Jan 10, 2024
c8eb680
Return pathname instead of url from `reroute`
LorisSigrist Jan 10, 2024
66848f6
Make return value optional
LorisSigrist Jan 10, 2024
209d12d
Update docs
LorisSigrist Jan 10, 2024
4f15f09
Merge commit 'b620c48109f18e10b67ff09bbce5d92a925c1f45' into feat-rew…
LorisSigrist Jan 10, 2024
ad201ef
fmt & lint
LorisSigrist Jan 10, 2024
f009ad8
Apply suggestions from code review
Rich-Harris Jan 10, 2024
c2def01
Update .changeset/thin-ears-double.md
Rich-Harris Jan 10, 2024
6acd801
clarify reloading behaviour, print error and break in dev
Rich-Harris Jan 10, 2024
f76da81
Merge branch 'main' into pr/11537
Rich-Harris Jan 10, 2024
976e0ae
@since
Rich-Harris Jan 10, 2024
6a59dfb
update types
Rich-Harris Jan 10, 2024
689811e
Update packages/kit/test/apps/basics/src/routes/reroute/error-handlin…
Rich-Harris Jan 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thin-ears-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sveltejs/kit": minor
---

feat: add `reroute` hook
38 changes: 37 additions & 1 deletion documentation/docs/30-advanced/20-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ title: Hooks

'Hooks' are app-wide functions you declare that SvelteKit will call in response to specific events, giving you fine-grained control over the framework's behaviour.

There are two hooks files, both optional:
There are three hooks files, all optional:

- `src/hooks.server.js` — your app's server hooks
- `src/hooks.client.js` — your app's client hooks
- `src/hooks.js` — your app's hooks that run on both the client and server

Code in these modules will run when the application starts up, making them useful for initializing database clients and so on.

Expand Down Expand Up @@ -232,6 +233,41 @@ During development, if an error occurs because of a syntax error in your Svelte

> Make sure that `handleError` _never_ throws an error

## Universal hooks

The following can be added to `src/hooks.js`. Universal hooks run on both server and client (not to be confused with shared hooks, which are environment-specific).

### reroute

This function allows you to change how URLs are translated into routes. The returned pathname (which defaults to `url.pathname`) is used to select the route and its parameters.

For example, you might have a `src/routes/[[lang]]/about/+page.svelte` page, which should be accessible as `/en/about` or `/de/ueber-uns` or `/fr/a-propos`. You could implement this with `reroute`:

```js
/// file: src/hooks.router.js
// @errors: 2345
// @errors: 2304

/** @type {Record<string, string>} */
const translated = {
'/en/about': '/en/about',
'/de/ueber-uns': '/de/about',
'/fr/a-propos': '/fr/about',
};

/** @type {import('@sveltejs/kit').Reroute} */
export function reroute({ url }) {
if (url.pathname in translated) {
return translated[url.pathname];
}
}
```

The `lang` parameter will be correctly derived from the returned pathname.

Using `reroute` will _not_ change the contents of the browser's address bar, or the value of `event.url`.


## Further reading

- [Tutorial: Hooks](https://learn.svelte.dev/tutorial/handle)
1 change: 1 addition & 0 deletions packages/kit/src/core/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function process_config(config, { cwd = process.cwd() } = {}) {
if (key === 'hooks') {
validated.kit.files.hooks.client = path.resolve(cwd, validated.kit.files.hooks.client);
validated.kit.files.hooks.server = path.resolve(cwd, validated.kit.files.hooks.server);
validated.kit.files.hooks.universal = path.resolve(cwd, validated.kit.files.hooks.universal);
} else {
// @ts-expect-error
validated.kit.files[key] = path.resolve(cwd, validated.kit.files[key]);
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ const get_defaults = (prefix = '') => ({
assets: join(prefix, 'static'),
hooks: {
client: join(prefix, 'src/hooks.client'),
server: join(prefix, 'src/hooks.server')
server: join(prefix, 'src/hooks.server'),
universal: join(prefix, 'src/hooks')
},
lib: join(prefix, 'src/lib'),
params: join(prefix, 'src/params'),
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ const options = object(
assets: string('static'),
hooks: object({
client: string(join('src', 'hooks.client')),
server: string(join('src', 'hooks.server'))
server: string(join('src', 'hooks.server')),
universal: string(join('src', 'hooks'))
}),
lib: string(join('src', 'lib')),
params: string(join('src', 'params')),
Expand Down
18 changes: 15 additions & 3 deletions packages/kit/src/core/sync/write_client_manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
}
`;

const hooks_file = resolve_entry(kit.files.hooks.client);
const client_hooks_file = resolve_entry(kit.files.hooks.client);
const universal_hooks_file = resolve_entry(kit.files.hooks.universal);

const typo = resolve_entry('src/+hooks.client');
if (typo) {
Expand All @@ -125,7 +126,16 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
write_if_changed(
`${output}/app.js`,
dedent`
${hooks_file ? `import * as client_hooks from '${relative_path(output, hooks_file)}';` : ''}
${
client_hooks_file
? `import * as client_hooks from '${relative_path(output, client_hooks_file)}';`
: ''
}
${
universal_hooks_file
? `import * as universal_hooks from '${relative_path(output, universal_hooks_file)}';`
: ''
}

export { matchers } from './matchers.js';

Expand All @@ -139,8 +149,10 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {

export const hooks = {
handleError: ${
hooks_file ? 'client_hooks.handleError || ' : ''
client_hooks_file ? 'client_hooks.handleError || ' : ''
}(({ error }) => { console.error(error) }),

reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {})
};

export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
Expand Down
19 changes: 13 additions & 6 deletions packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import colors from 'kleur';

/**
* @param {{
* hooks: string | null;
* server_hooks: string | null;
* universal_hooks: string | null;
* config: import('types').ValidatedConfig;
* has_service_worker: boolean;
* runtime_directory: string;
Expand All @@ -19,7 +20,8 @@ import colors from 'kleur';
*/
const server_template = ({
config,
hooks,
server_hooks,
universal_hooks,
has_service_worker,
runtime_directory,
template,
Expand Down Expand Up @@ -59,8 +61,11 @@ export const options = {
version_hash: ${s(hash(config.kit.version.name))}
};

export function get_hooks() {
return ${hooks ? `import(${s(hooks)})` : '{}'};
export async function get_hooks() {
return {
${server_hooks ? `...(await import(${s(server_hooks)})),` : ''}
${universal_hooks ? `...(await import(${s(universal_hooks)})),` : ''}
};
}

export { set_assets, set_building, set_prerendering, set_private_env, set_public_env, set_safe_public_env };
Expand All @@ -76,7 +81,8 @@ export { set_assets, set_building, set_prerendering, set_private_env, set_public
* @param {string} output
*/
export function write_server(config, output) {
const hooks_file = resolve_entry(config.kit.files.hooks.server);
const server_hooks_file = resolve_entry(config.kit.files.hooks.server);
const universal_hooks_file = resolve_entry(config.kit.files.hooks.universal);

const typo = resolve_entry('src/+hooks.server');
if (typo) {
Expand All @@ -99,7 +105,8 @@ export function write_server(config, output) {
`${output}/server/internal.js`,
server_template({
config,
hooks: hooks_file ? relative(hooks_file) : null,
server_hooks: server_hooks_file ? relative(server_hooks_file) : null,
universal_hooks: universal_hooks_file ? relative(universal_hooks_file) : null,
has_service_worker:
config.kit.serviceWorker.register && !!resolve_entry(config.kit.files.serviceWorker),
runtime_directory: relative(runtime_directory),
Expand Down
12 changes: 12 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,12 @@ export interface KitConfig {
* @default "src/hooks.server"
*/
server?: string;
/**
* The location of your universal [hooks](https://kit.svelte.dev/docs/hooks).
* @default "src/hooks"
* @since 2.3.0
*/
universal?: string;
};
/**
* your app's internal library, accessible throughout the codebase as `$lib`
Expand Down Expand Up @@ -683,6 +689,12 @@ export type HandleFetch = (input: {
fetch: typeof fetch;
}) => MaybePromise<Response>;

/**
* The [`reroute`](https://kit.svelte.dev/docs/hooks#universal-hooks-reroute) hook allows you to modify the URL before it is used to determine which route to render.
* @since 2.3.0
*/
export type Reroute = (event: { url: URL }) => void | string;

/**
* The generic form of `PageLoad` and `LayoutLoad`. You should import those from `./$types` (see [generated types](https://kit.svelte.dev/docs/types#generated-types))
* rather than using `Load` directly.
Expand Down
32 changes: 28 additions & 4 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1082,21 +1082,45 @@ async function load_root_error_page({ status, error, url, route }) {
}

/**
* @param {URL} url
* @param {URL | undefined} url
* @param {boolean} invalidating
*/
function get_navigation_intent(url, invalidating) {
if (!url) return undefined;
if (is_external_url(url, base)) return;

const path = get_url_path(url.pathname);
// reroute could alter the given URL, so we pass a copy
let rerouted;
try {
rerouted = app.hooks.reroute({ url: new URL(url) }) ?? url.pathname;
} catch (e) {
if (DEV) {
// in development, print the error...
console.error(e);

// ...and pause execution, since otherwise we will immediately reload the page
debugger; // eslint-disable-line
}

// fall back to native navigation
return undefined;
}

const path = get_url_path(rerouted);

for (const route of routes) {
const params = route.exec(path);

if (params) {
const id = url.pathname + url.search;
/** @type {import('./types.js').NavigationIntent} */
const intent = { id, invalidating, route, params: decode_params(params), url };
const intent = {
id,
invalidating,
route,
params: decode_params(params),
url
};
return intent;
}
}
Expand Down Expand Up @@ -1462,7 +1486,7 @@ function setup_preload() {

if (!options.reload) {
if (priority <= options.preload_data) {
const intent = get_navigation_intent(/** @type {URL} */ (url), false);
const intent = get_navigation_intent(url, false);
if (intent) {
if (DEV) {
_preload_data(intent).then((result) => {
Expand Down
6 changes: 1 addition & 5 deletions packages/kit/src/runtime/server/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
declare module '__SERVER__/internal.js' {
export const options: import('types').SSROptions;
export const get_hooks: () => Promise<{
handle?: import('@sveltejs/kit').Handle;
handleError?: import('@sveltejs/kit').HandleServerError;
handleFetch?: import('@sveltejs/kit').HandleFetch;
}>;
export const get_hooks: () => Promise<Partial<import('types').ServerHooks>>;
}
6 changes: 4 additions & 2 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export class Server {
this.#options.hooks = {
handle: module.handle || (({ event, resolve }) => resolve(event)),
handleError: module.handleError || (({ error }) => console.error(error)),
handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request))
handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)),
reroute: module.reroute || (() => {})
};
} catch (error) {
if (DEV) {
Expand All @@ -71,7 +72,8 @@ export class Server {
throw error;
},
handleError: ({ error }) => console.error(error),
handleFetch: ({ request, fetch }) => fetch(request)
handleFetch: ({ request, fetch }) => fetch(request),
reroute: () => {}
};
} else {
throw error;
Expand Down
12 changes: 11 additions & 1 deletion packages/kit/src/runtime/server/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,19 @@ export async function respond(request, options, manifest, state) {
}
}

// reroute could alter the given URL, so we pass a copy
let rerouted_path;
try {
rerouted_path = options.hooks.reroute({ url: new URL(url) }) ?? url.pathname;
} catch (e) {
return text('Internal Server Error', {
status: 500
});
}

let decoded;
try {
decoded = decode_pathname(url.pathname);
decoded = decode_pathname(rerouted_path);
} catch {
return text('Malformed URI', { status: 400 });
}
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
ServerInitOptions,
HandleFetch,
Actions,
HandleClientError
HandleClientError,
Reroute
} from '@sveltejs/kit';
import {
HttpMethod,
Expand Down Expand Up @@ -99,10 +100,12 @@ export interface ServerHooks {
handleFetch: HandleFetch;
handle: Handle;
handleError: HandleServerError;
reroute: Reroute;
}

export interface ClientHooks {
handleError: HandleClientError;
reroute: Reroute;
}

export interface Env {
Expand Down
31 changes: 31 additions & 0 deletions packages/kit/test/apps/basics/src/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { browser } from '$app/environment';

const mapping = {
'/reroute/basic/a': '/reroute/basic/b',
'/reroute/client-only-redirect/a': '/reroute/client-only-redirect/b',
'/reroute/preload-data/a': '/reroute/preload-data/b'
};

/** @type {import("@sveltejs/kit").Reroute} */
export const reroute = ({ url }) => {
//Try to rewrite the external url used in /reroute/external to the homepage - This should not work
if (browser && url.href.startsWith('https://expired.badssl.com')) {
return '/';
}

if (url.pathname === '/reroute/error-handling/client-error') {
if (browser) {
throw new Error('Client Error');
} else {
return '/reroute/error-handling/client-error-rewritten';
}
}

if (url.pathname === '/reroute/error-handling/server-error') {
throw new Error('Server Error - Should trigger 500 response');
}

if (url.pathname in mapping) {
return mapping[url.pathname];
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="/reroute/basic/a">Go to url that should be rewritten</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Should have been rewritten to <code>/reroute/basic/b</code></h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Successfully rewritten</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { browser } from '$app/environment';
import { redirect } from '@sveltejs/kit';

export async function load() {
if (browser) {
redirect(302, '/reroute/client-only-redirect/a');
}

return {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Should be redirected</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Should have been rewritten to <code>/reroute/client-only-redirect/b</code></h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Successfully rewritten</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<a href="/reroute/error-handling/client-error" id="client-error">Url with client error</a>
<a href="/reroute/error-handling/server-error" id="server-error">Url with server error</a>
Loading