Skip to content

Commit e14d939

Browse files
committed
feat!: Add Redirector
BREAKING CHANGE
1 parent 65667d8 commit e14d939

23 files changed

+2430
-157
lines changed

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
[@svelte-router/kit](https://github.com/WJSoftware/svelte-router-kit)
2626
+ **Electron support**: Works with Electron (all routing modes)
2727
+ **Reactivity-based**: All data is reactive, reducing the need for events and imperative programming.
28+
+ **⚡NEW! URL Redirection**: Use `Redirector` instances to route users from deprecated URL's to new URL's, even across
29+
routing universes.
2830

2931
**Components**:
3032

@@ -470,6 +472,70 @@ As seen, the value of the `href` property never changes. It's always a path, re
470472
At your own risk, you could use exported API like `getRouterContext()` and `setRouterContext()` to perform unholy acts
471473
on the router layouts, again, **at your own risk**.
472474

475+
## URL Redirection
476+
477+
Create `Redirector` class instances to route users from deprecated URL's to new URL's. The redirection can even cross
478+
the routing universe boundary. In other words, URL's from one routing universe can be redirected to a different
479+
routing universe.
480+
481+
This is a same-universe example:
482+
483+
```svelte
484+
<script lang="ts">
485+
import { Redirector } from "@svelte-router/core";
486+
import { onMount } from "svelte";
487+
488+
const redirector = new Redirector(/* hash value, or nothing for default universe */);
489+
redirector.redirections.push({
490+
pattern: `/orders/:id`,
491+
href: (rp) => `/profile/my-orders/${rp?.id}`
492+
});
493+
494+
onMount(() => () => redirector.dispose());
495+
...
496+
</script>
497+
```
498+
499+
The constructor of the class sets a Svelte `$effect` up, so instances of this class must be created in places where
500+
Svelte effects are acceptable, like the initialization code of a component (like in the example).
501+
502+
Redirections are almost identical to route definitions, and even use the same matching algorithm. The `pattern` is
503+
used to match the current URL (it defines the deprecated URL), while `href` defines the new URL users will be
504+
redirected to. As seen in the example, parameters can be defined, and `href`, when written as a function, receives
505+
the route parameters as the first argument.
506+
507+
### Cross-Universe Redirection
508+
509+
Crossing the universe boundary when redirecting is very simple, but there's a catch: Cleaning up the old URL.
510+
511+
```svelte
512+
<script lang="ts">
513+
import { Redirector } from "@svelte-router/core";
514+
515+
const redirector = new Redirector(false);
516+
redirector.redirections.push({
517+
pattern: `/orders/:id`,
518+
href: (rp) => `/profile/my-orders/${rp?.id}`,
519+
options: { hash: true }
520+
});
521+
...
522+
</script>
523+
```
524+
525+
The modifications in the example are:
526+
527+
1. Explicit hash value in the redirector's constructor.
528+
2. Destination hash value specifications via options.
529+
530+
Now comes the aforementioned catch: The "final" URL will be looking like this: `https://example.com/orders/123#/profile/my-orders/123`.
531+
532+
There's no good way for this library to provide a safe way to "clean up" the path in the deprecated routing universe,
533+
so it is up to consumers of this library to clean up. How? The recommendation is to tell the redirector to use
534+
`location.goTo()` and provide a full HREF with all universes accounted for.
535+
536+
See the [Redirecting](https://wjfe-n-savant.hashnode.space/wjfe-n-savant/navigating/redirecting) topic in the online
537+
documentation for full details, including helper functions available to you.
538+
473539
---
474540

475541
[Issues Here](https://github.com/WJSoftware/svelte-router-core/issues)

src/lib/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('index', () => {
1919
'setRouterContext',
2020
'isRouteActive',
2121
'activeBehavior',
22+
'Redirector',
2223
];
2324

2425
// Act.

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export * from './RouterTrace/RouterTrace.svelte';
1414
export { default as RouterTrace } from './RouterTrace/RouterTrace.svelte';
1515
export * from "./public-utils.js";
1616
export * from "./behaviors/active.svelte.js";
17+
export { Redirector } from "./kernel/Redirector.svelte.js";

src/lib/kernel/LocationLite.svelte.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,32 @@ describe("LocationLite", () => {
264264
expect(act).toThrow();
265265
});
266266
});
267+
describe('path', () => {
268+
const initialPath = '/initial/path';
269+
beforeEach(() => {
270+
browserMocks.simulateHistoryChange(undefined, `http://example.com${initialPath}`);
271+
});
272+
273+
test("Should return the URL's path.", () => {
274+
expect(location.path).toBe(initialPath);
275+
});
276+
277+
test("Should update when location changes.", () => {
278+
expect(location.path).toBe(initialPath);
279+
const newPath = '/new/path/value';
280+
location.navigate(newPath);
281+
expect(location.path).toBe(newPath);
282+
});
283+
284+
test("Should remove the drive letter on Windows file URLs.", () => {
285+
// Arrange.
286+
const fileUrl = 'file:///C:/path/to/file.txt';
287+
288+
// Act.
289+
browserMocks.simulateHistoryChange(undefined, fileUrl);
290+
291+
// Assert.
292+
expect(location.path).toBe('/path/to/file.txt');
293+
});
294+
});
267295
});

src/lib/kernel/LocationLite.svelte.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { assertAllowedRoutingMode } from "$lib/utils.js";
1414
*/
1515
export class LocationLite implements Location {
1616
#historyApi: HistoryApi;
17-
17+
1818
hashPaths = $derived.by(() => {
1919
if (routingOptions.hashMode === 'single') {
2020
return { single: this.#historyApi.url.hash.substring(1) };
@@ -31,6 +31,11 @@ export class LocationLite implements Location {
3131
return result;
3232
});
3333

34+
path = $derived.by(() => {
35+
const hasDriveLetter = this.url.protocol.startsWith('file:') && this.url.pathname[2] === ':';
36+
return hasDriveLetter ? this.url.pathname.substring(3) : this.url.pathname;
37+
});
38+
3439
constructor(historyApi?: HistoryApi) {
3540
this.#historyApi = historyApi ?? new StockHistoryApi();
3641
}

0 commit comments

Comments
 (0)