Skip to content

feat: Primitives for i18n routing #11396

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
18623cc
Add `resolveDestination` hokk
LorisSigrist Dec 11, 2023
65a6141
Catch `redirect`s throw in Endpoints
LorisSigrist Dec 11, 2023
4aafd06
rewrite attributes generically
LorisSigrist Dec 12, 2023
11c3b26
Diff from & to urls & use the shortest href between them
LorisSigrist Dec 12, 2023
10e0b75
Refactor & simplify
LorisSigrist Dec 12, 2023
a5cdc0d
Also diff credentials
LorisSigrist Dec 12, 2023
d91b053
prefer absolute path when paths are tied
LorisSigrist Dec 12, 2023
b6efd99
Merge remote-tracking branch 'origin/master' into 11223-rewrite-outgo…
LorisSigrist Dec 13, 2023
1a5d950
Use canonical url diffing function
LorisSigrist Dec 14, 2023
60efea0
make less fragile
LorisSigrist Dec 14, 2023
b66abd1
Pass existing tests
LorisSigrist Dec 14, 2023
c777ba7
Add remapURL hook
LorisSigrist Dec 14, 2023
9ab0644
add basic rewrite support
LorisSigrist Dec 14, 2023
219bdc4
Fix credential handling
LorisSigrist Dec 14, 2023
b3aa7fd
REWRITES LETS GO
LorisSigrist Dec 14, 2023
ea4dac0
Temporarily disable relative urls
LorisSigrist Dec 14, 2023
d092fa8
rename to rewriteURL
LorisSigrist Dec 14, 2023
b9e28bf
Only use absolute URLs
LorisSigrist Dec 14, 2023
0bd8a1b
Load data correctly
LorisSigrist Dec 14, 2023
c6907fd
properly detect URL changes
LorisSigrist Dec 14, 2023
65ebea9
fmt & lint
LorisSigrist Dec 14, 2023
2c11e27
Fix preloading & invalidadte
LorisSigrist Dec 15, 2023
a475427
Merge branch 'master' into 5703-rewrites
LorisSigrist Dec 15, 2023
1b6e0ed
Make goto work + fmt
LorisSigrist Dec 15, 2023
556bd65
Support shallow routing
LorisSigrist Dec 15, 2023
f517dd2
Change rewriteURL signature
LorisSigrist Dec 15, 2023
e670a6b
fix typo
LorisSigrist Dec 15, 2023
2fe5a53
Fix trailing slash
LorisSigrist Dec 16, 2023
6712659
Fix unnecessary invalidations
LorisSigrist Dec 16, 2023
1779b16
fmt
LorisSigrist Dec 17, 2023
1b28075
Merge branch 'master' into 5703-rewrites
LorisSigrist Dec 17, 2023
058b4fc
Fix hash based invalidation
LorisSigrist Dec 18, 2023
2a5233b
Create Test Project for rewrites
LorisSigrist Dec 18, 2023
0f224ff
FMT
LorisSigrist Dec 18, 2023
723b696
Add resolveDestination Tests
LorisSigrist Dec 18, 2023
6da980e
Add form action test
LorisSigrist Dec 18, 2023
7731649
Handle Redirects in load functions
LorisSigrist Dec 18, 2023
692e2cb
redirects in form actions
LorisSigrist Dec 18, 2023
d1bf85d
fmt & lint
LorisSigrist Dec 18, 2023
da7357f
Merge branch 'master' into 5703-rewrites
LorisSigrist Dec 18, 2023
373e528
Add changeset
LorisSigrist Dec 18, 2023
fd9981d
Add docs
LorisSigrist Dec 18, 2023
37b057f
weird race condition
LorisSigrist Dec 18, 2023
e4322ab
more weird race conditions
LorisSigrist Dec 18, 2023
0e3b3a1
Add prerendering Test
LorisSigrist Dec 19, 2023
2f82e86
Add chaining test
LorisSigrist Dec 19, 2023
e5b50d9
Added "once" test
LorisSigrist Dec 19, 2023
62fb363
fmt
LorisSigrist Dec 19, 2023
abbc45d
Remove debug logs
LorisSigrist Dec 19, 2023
c8ee14a
Fix lockfile
LorisSigrist Dec 19, 2023
7803fad
Update generated types
LorisSigrist Dec 19, 2023
d957029
Add placeholder test for cross-platform
LorisSigrist Dec 19, 2023
49cc7ef
fmt
LorisSigrist Dec 19, 2023
c63b0d4
Merge branch 'master' into primitives-for-i18n-routing
LorisSigrist Dec 22, 2023
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/curvy-cats-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sveltejs/kit": minor
---

feat: add `resolveDestination` and `rewriteURL` hooks, enabling i18n routing
59 changes: 57 additions & 2 deletions documentation/docs/30-advanced/20-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ 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.router.js` — your app's router hooks

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

> You can configure the location of these files with [`config.kit.files.hooks`](configuration#files).

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

> Make sure that `handleError` _never_ throws an error

## Router hooks

The following can be added to `src/hooks.router.js`. Router hooks run both on the server and the client.

### rewriteURL

This function allows you to rewrite URLs before they are processed by SvelteKit. It receives a `url` object and should return a `URL` object.

```js
/// file: src/hooks.router.js
// @errors: 2345
// @errors: 2304
/** @type {import('@sveltejs/kit').RewriteURL} */
export function rewriteURL({ url }) {
//Process requests to '/<lang>/about' as if they were to '/about'
const language = getLanguageFromPath(url.pathname);
if(language) url.pathname = url.pathname.slice(language.length + 1);

return url;
}
```

Rewrites happen in place, and are completely invisible to the user. For example, if you rewrite `/about` to `/about-us`, the user will still see `/about` in their browser's address bar. Only the server will know that the URL has been rewritten.

### resolveDestination

This function allows you to change the destination of an outgoing navigation event, such as:
- A link click
- A `goto` call
- A `redirect` call

It receives the current url, and the destination url, and should return a `URL` object.

```js
/// file: src/hooks.router.js
// @errors: 2345
// @errors: 2304
/** @type {import('@sveltejs/kit').ResolveDestination} */
export function resolveDestination({ from, to }) {
if(from.origin !== to.origin) return to; //Ignore cross-origin navigations

//If the destination-path already includes a language, leave it
const destinationLanguage = getLanguageFromPath(to.pathname);
if(destinationLanguage) return to;

//Otherwise, add the language from the current page
const language = getLanguageFromPath(from.pathname) ?? defaultLanguage;
to.pathname = `/${language}${to.pathname}`;
return to;
}
```

The `resolveDestination` hook is applied to all links and forms during prerendering and SSR, so it can safely be used even when JavaScript is disabled.

## 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.router = path.resolve(cwd, validated.kit.files.hooks.router);
} 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'),
router: join(prefix, 'src/hooks.router')
},
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')),
router: string(join('src', 'hooks.router'))
}),
lib: string(join('src', 'lib')),
params: string(join('src', 'params')),
Expand Down
22 changes: 19 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 router_hooks_file = resolve_entry(kit.files.hooks.router);

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)}';`
: ''
}
${
router_hooks_file
? `import * as router_hooks from '${relative_path(output, router_hooks_file)}';`
: ''
}

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

Expand All @@ -139,8 +149,14 @@ 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) }),

resolveDestination: ${
router_hooks_file ? 'router_hooks.resolveDestination || ' : ''
}((event) => event.to),

rewriteURL: ${router_hooks_file ? 'router_hooks.rewriteURL || ' : ''}(({url}) => url)
};

export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
Expand Down
11 changes: 9 additions & 2 deletions packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import colors from 'kleur';
/**
* @param {{
* hooks: string | null;
* router_hooks: string | null;
* config: import('types').ValidatedConfig;
* has_service_worker: boolean;
* runtime_directory: string;
Expand All @@ -20,6 +21,7 @@ import colors from 'kleur';
const server_template = ({
config,
hooks,
router_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 {
${hooks ? `...(await import(${s(hooks)})),` : ''}
${router_hooks ? `...(await import(${s(router_hooks)})),` : ''}
};
}

export { set_assets, set_building, set_prerendering, set_private_env, set_public_env, set_safe_public_env };
Expand All @@ -77,6 +82,7 @@ export { set_assets, set_building, set_prerendering, set_private_env, set_public
*/
export function write_server(config, output) {
const hooks_file = resolve_entry(config.kit.files.hooks.server);
const router_hooks_file = resolve_entry(config.kit.files.hooks.router);

const typo = resolve_entry('src/+hooks.server');
if (typo) {
Expand All @@ -100,6 +106,7 @@ export function write_server(config, output) {
server_template({
config,
hooks: hooks_file ? relative(hooks_file) : null,
router_hooks: router_hooks_file ? relative(router_hooks_file) : null,
has_service_worker:
config.kit.serviceWorker.register && !!resolve_entry(config.kit.files.serviceWorker),
runtime_directory: relative(runtime_directory),
Expand Down
31 changes: 31 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,12 @@ export interface KitConfig {
* @default "src/hooks.server"
*/
server?: string;

/**
* The location of your router hooks.
* @default "src/hooks.router"
*/
router?: string;
};
/**
* your app's internal library, accessible throughout the codebase as `$lib`
Expand Down Expand Up @@ -674,6 +680,31 @@ export type HandleClientError = (input: {
message: string;
}) => MaybePromise<void | App.Error>;

/**
* Maps an href value to a destination
* @example
* ```js
* export const resolveDestination = ({ from, to }) => {
* if(to.host !== from.host) return to; //Don't remap external links
* const lang = getLanguageFromURL(from);
* return applyLanguage(to, lang);
* }
* ```
*/
export type ResolveDestination = (event: { from: URL; to: URL }) => URL;

/**
* Remap an incoming URL to a different URL.
*
* @example
* ```js
* export const rewriteURL = (url) => {
* return urlWithoutLanguage(url);
* }
* ```
*/
export type RewriteURL = (event: { url: URL }) => URL;

/**
* The [`handleFetch`](https://kit.svelte.dev/docs/hooks#server-hooks-handlefetch) hook allows you to modify (or replace) a `fetch` request that happens inside a `load` function that runs on the server (or during pre-rendering)
*/
Expand Down
16 changes: 15 additions & 1 deletion packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import analyse from '../../core/postbuild/analyse.js';
import { s } from '../../utils/misc.js';
import { hash } from '../../runtime/hash.js';
import { dedent, isSvelte5Plus } from '../../core/sync/utils.js';
import { resolve_destination_preprocessor } from './preprocessor/resolveDestination.js';

import {
env_dynamic_private,
env_dynamic_public,
Expand Down Expand Up @@ -857,7 +859,19 @@ function kit({ svelte_config }) {
}
};

return [plugin_setup, plugin_virtual_modules, plugin_guard, plugin_compile];
const router_hook_entry = resolve_entry(kit.files.hooks.router);
/** @type {import('vite').Plugin} */
const resolve_destination = {
name: 'vite-plugin-sveltekit-resolve-destination',

api: {
sveltePreprocess: router_hook_entry
? resolve_destination_preprocessor({ router_hook_entry })
: undefined
}
};

return [plugin_setup, plugin_virtual_modules, plugin_guard, plugin_compile, resolve_destination];
}

/**
Expand Down
Loading