https://github.com/wjsoftware/wjfe-n-savant
The client-side router for Svelte v5 SPA's that invented multi hash routing.
https://github.com/wjsoftware/wjfe-n-savant
mfe microfrontend router svelte
Last synced: 17 days ago
JSON representation
The client-side router for Svelte v5 SPA's that invented multi hash routing.
- Host: GitHub
- URL: https://github.com/wjsoftware/wjfe-n-savant
- Owner: WJSoftware
- License: mit
- Created: 2025-01-23T05:41:27.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-09-27T03:50:59.000Z (27 days ago)
- Last Synced: 2025-09-27T05:43:48.594Z (27 days ago)
- Topics: mfe, microfrontend, router, svelte
- Language: TypeScript
- Homepage: https://wjfe-n-savant.hashnode.space/
- Size: 532 KB
- Stars: 32
- Watchers: 1
- Forks: 0
- Open Issues: 10
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
#
@wjfe/n-savant
> The client-side router for Svelte v5 SPA's that invented multi hash routing.
[REPL Demo](https://svelte.dev/playground/d273d356947e48c0822a65402fd06fac)
## Features
> **📝 Small and Unique!**
>
> + Less than **1,450** lines of code, including TypeScript typing.
> + Always-on path and hash routing. Simultaneous and independent routing modes.
> + The router that invented multi hash routing.
> + **NEW!** Supports Sveltekit (via [@wjfe/n-savant-sk](https://github.com/WJSoftware/wjfe-n-savant-sk))+ **Electron support**: Works with Electron (all routing modes)
+ **Reactivity-based**: All data is reactive, reducing the need for events and imperative programming.
+ **Always-on path and hash routing**: Add routers that use the URL's path name or the URL's hash value in the same
application. Both routing modes are possible simultaneously.
+ **Multi-hash routing**: This is the first router in the world to do this: You can create named paths in the hash
value of the URL and create router hierarchies that use a specific named path.### `` Component
+ **Multi-matching routes**: All routes are evaluated, which is useful to mount micro-frontends.
+ **Base paths**: Specify base paths that are inherited by routes and nesting routers.
+ **Nesting routers**: Add child routers inside routers for fine-grained control.
+ **Liberty**: Place anything anywhere inside. No child restrictions.### `` Component
+ **Exact path matching**: Exact match by default; specify the rest parameter to relax the condition.
+ **Path as string or regular expression**: Define paths however's best for you.
+ **Route parameters**: Define route parameters inside string paths or regular expression paths.
+ **Rest parameter**: Collect "the rest" of the path.
+ **Optional parameters**: Parameters may be specified as optional.
+ **Additional matching logic**: Add a predicate function to further restrict a route's matching ability.
+ **Path is optional**: Forgo path specification entirely and handle route matching entirely with code.
+ **Superb Intellisense**: The route parameters are strongly typed when defining them inside a string path.
+ **Disconnected UI pieces**: Repeat route keys in `Route` components to show disconnected pieces of UI for a given
route's key.### `` Component
+ **Non-matching content**: Show users something when there are no matching routes.
+ **Disconnected content**: Add as many `Fallback` components as needed in various places.### `` Component
[^1]: For hyperlink components that only specify a hash and are converted to hash-routing `` components, remove
the pound sign (`#`) from the href.### `` Component
+ **Centralize `` configuration**: Configures a special context that all `` components follow.
### `` Component
+ **Tracing Information**: Drop it inside a router to display its route status data, including the internal regular
expressions that are generated from string path patterns.
+ **Specify a specific router**: Ability to give it a specific router engine object, allowing tracing of router engine
objects created in code.
+ **Track child routers**: See and traverse the router hierarchy.### `location` Global Object
+ **Reactive URL**: URL object that's always in sync with the browser's URL.
+ **Reactive state**: Reactive state property that's always in sync with the history state.
+ **Reactive hash paths**: Reactive dictionary object for all hash paths.
+ **Programatic navigation**: Use the the `navigate()` method to trigger navigation programatically.#### In Full Mode...
+ **Cancellable `beforeNavigate` event**: Get notified of navigation events, and cancel when appropriate.
+ **`navigationCancelled` event**: Get notified whenever navigation is cancelled.
+ **History API interception**: Gain control over the history object to avoid external code/routers from
de-synchronizing state.
+ **Micro-frontends**: Full mode's features are great for micro-frontend scenarios where other routers (from
potentially other technologies) might interfere with the router's functionality.## Quickstart
1. Install the package.
2. Initialize the library.
3. Define the routes inside routers.
4. Modify/Add your navigation links.### Install the package
```bash
npm i @wjfe/n-savant
```### Initialize the Library
```typescript
// In your main.ts, or somewhere BEFORE any routers are created:
import { init } from "@wjfe/n-savant";// Default: Lite mode, implicit path routing, no router hierarchy tracing, single hash mode.
init();// If all you care about is (traditional) hash routing, the recommendation is to change the implicit mode:
init({ implicitMode: 'hash' });
```#### Electron Variant
In Electron, we must immediately navigate to the homepage (or your preferred initial route) right after initializing if you use path routing:
```typescript
import { init, location } from "@wjfe/n-savant";init();
location.goTo('/');
```For applications that also run in the browser, condition the navigation to Electron only. See the [Electron page](https://wjfe-n-savant.hashnode.space/wjfe-n-savant/introduction/electron-support) online for more details.
> **⚠️ Important:** Hash routing doesn't require this extra navigation step.
### Define the Routes
``s are added inside ``s. ``s can be nested inside other ``s. ``s can render
``s or other ``s, etc. You get the idea: You do as you wish.```svelte
import { Router, Route } from "@wjfe/n-savant";
import NavBar from "./lib/NavBar.svelte";
import UserView from "./lib/UserView.svelte";
Routing Demo
{#snippet children(params)}
{/snippet}
...
```
### Navigation Links
The previous step added the `` component inside the router. This is the best practice for full ``
functionality. Still, this is not mandatory.```svelte
import { Link } from "@wjfe/n-savant";
```
## Micro-Frontend Goodness
This router's implementation intends to cater for micro-frontends as best as possible. The following are features and
strategies that are possible with this router.### Multi-Route Matching
Routers always evaluate all defined routes, so it is possible for more than one route to match. This facilitates the
layout of micro-frontends. For example, a navigation micro-frontend could be inside a route that either always matches
or matches most of the time, so navigation links are available the majority/all of the time.### Simultaneous, Always-On Path and Hash Routing
Components (`Router`, `Route`, `Link`, `Fallback` and `RouterTrace`) with the same value of the `hash` property belong
to the same "universe". Components with different hash values belong to different universes, and these universes are
parallel universes. Components with hash value `false` use the URL's path name and will never interfere with routers
that use hash routing (hash value `true` or a path's name). The main micro-frontend(s) may route using the URL's path
name, while specialty MFE's could route using the path in the hash part of the URL.### Multi-Hash Routing
As of February 2025, no other router in the world can do this.
Imagine a scenario where your MFE application would like to show side-by-side two micro-frontends that are
router-enabled (meaning they use or need to work with a path). With traditional routing, you could not have this setup
because one MFE would take over the path, leaving the other MFE without one.Multi-hash routing creates named paths in the hash value, giving routers the ability to share the hash value with other
routers. A hash value of the form `#path1=/path/1;path2=/path/2;...` could power side-by-side MFE's on, say, 4K
layouts.### EXPERIMENTAL - Replacing the `single-spa` Router
It is the author's intent to implement micro-frontends with only `single-spa` parcels and this router. In other words,
abandon the use of `registerApplication()` and `start()` and just mount parcels using this router.[single-spa](https://single-spa.js.org)
## Unobtrusive Philosophy
This mini router library imposes minimal restrictions. Here are some features provided by other much larger codebases
that are not provided here because Svelte already has the capability.### Transitions
Nothing prevents you to add transitions to anything.
```svelte
{#snippet children(params)}
...
{/snippet}```
> **📝 Note:** This one item might be worthwhile revisiting for the cases where synchronized transitions are desired. This,
> however, won't be looked at until Svelte attachments become a thing.### Guarded Routes
Guard routes however you wish. Maybe with an `{#if}` block, or maybe using the route's `and` property that allows you
to specify a predicate function. There are probably many other ways.### `Exact` Property on Routes
Not needed. All matching is exact path matching, and if you want to opt out of the exact route matching, simply add
the `rest` parameter specifier (`/*`):```svelte
...
```
Now route matching for this route will behave as "starts with". If you don't care about the value of the parameter,
just ignore it.### Lazy-Loading
Lazy-loading components is very simple:
```svelte
function loadUsersComponent() {
return import('./lib/Users.svelte').then(m => m.default);
}{#await loadUsersComponent()}
Loading...
{:then Users}
{:catch}
Oops!
{/await}```
### Navigation Events
There are no navigation events defined. Simply write effects or derived computations based on the global `location`
object's `url`, `state` or `hashPaths` properties, which are reactive.```typescript
import { location } from "@wjfe/n-savant";// Or $derived, whichever you need.
$effect(() => {
// Read location.url to re-run on URL changes (navigation).
location.url;
// Read location.state to re-run on state changes.
location.state;
// Read location.hashPaths to re-run on hash changes (hash navigation).
// The route named "single" is the one you want if doing hash routing.
location.hashPaths.single;
});
```### Parameter Types
There is no parameter type specification. All parameter values go through type parsing:
+ If the value represents a number, then the parameter value will be a `number`.
+ If the value is the word `'true'` or `'false'`, then the parameter value will be a `boolean`.
+ If none of the above, the value will be a `string`.If the demand for parameter value types grow, this might be reconsidered, but know that this is easily achievable with
the `and` property on routes, or by specifying the path as a regular expression.In the context of the following code, the path `'/users/summary'` would match both routes, so the one that needs a
numeric parameter value uses the `and` property to type-check the value:```svelte
typeof rp.userId === 'number'}>
{#snippet children(rp)}
{/snippet}
```
This is the version using a regular expression for the `path` property:
```svelte
\d+)/i}>
{#snippet children(rp)}
{/snippet}
```
### Reacting to Route Matching Events
If you're interested in reacting whenever (a) particular route(~~s~~) match(es), you can get a hold of the `routeStatus`
property of router engines (which is reactive) by binding to a router's `router` property:```svelte
import { RouterEngine } from "@wjfe/n-savant/core";
let router: $state<RouterEngine>();
$effect(() => {
for (let [key, rs] of Object.entries(router.routeStatus)) {
// key: Route's key
// rs: RouteStatus for the route.
if (rs.match) {
// Do stuff with rs.routeParams, for example.
}
}
});...
```
## Navigation
> Since **v0.4.0**
> **💥 BREAKING CHANGE:** Navigation has been re-done in v0.4.0.
The recommended way of navigating is to create `` component instances to render links on the document(s). If
needed, however, there are 2 navigation functions in the `location` object: `navigate()` and `goTo()`.### `navigate(url, options)` - Routing Universe Aware
This is the preferred method for programmatic navigation as it understands routing universes and properly manages state:
```typescript
import { location } from "@wjfe/n-savant";// Path routing navigation:
location.navigate('/new/path', {
replace: true,
state: { custom: 'Hi' },
hash: false
});// Hash routing navigation:
location.navigate('/new/path', {
replace: true,
state: { custom: 'Hi' },
hash: true
});// Multi-hash routing navigation:
location.navigate('/new/path', {
replace: true,
state: { custom: 'Hi' },
hash: 'path1'
});// Preserve existing query parameters:
location.navigate('/new/path', {
preserveQuery: true,
hash: false
});
```The `navigate()` method automatically:
- Associates state with the correct routing universe based on the `hash` option
- Preserves other routing universe states (e.g., when navigating `path1`, other named paths remain intact)
- Handles URL construction using the robust `calculateHref()` logic### `goTo(url, options)` - Direct URL Navigation
This method provides direct URL navigation without routing universe awareness:
```typescript
import { location } from "@wjfe/n-savant";// Direct URL navigation:
location.goTo('https://example.com/new/path', {
replace: true,
state: { path: undefined, hash: {} } // Must provide complete State object
});// Shallow routing (navigate to current URL):
location.goTo('', { replace: true });// Preserve query parameters:
location.goTo('/new/path', {
preserveQuery: ['param1', 'param2']
});
```**⚠️ Important:** `goTo()` requires you to provide a complete `State` object and does not understand routing universes. Use `navigate()` unless you specifically need direct URL control.
### Options Reference
Both methods support these common options:
- **`replace?: boolean`** - Replace current URL instead of pushing new entry (default: `false`)
- **`preserveQuery?: PreserveQuery`** - Preserve current query parameters
- `true` - Preserve all query parameters
- `string` - Preserve specific parameter by name
- `string[]` - Preserve multiple specific parametersAdditional `navigate()` options:
- **`hash?: Hash`** - Routing universe to associate with (`false`, `true`, or named hash)
- **`state?: any`** - State data to associate with the navigationAdditional `goTo()` options:
- **`state?: State`** - Complete state object conforming to library expectations### Navigation Best Practices
1. **Use `` components** for user-triggered navigation
2. **Use `navigate()`** for programmatic navigation within routing universes
3. **Use `goTo()`** only for direct URL manipulation or external navigation
4. **Always specify `hash`** in `navigate()` to ensure proper state managementJust in case you are wondering: The navigation logic is already there in `` components:
```svelte
Click Me!
Click Me!
Click Me!
```> **⚠️ Important:** Not setting the `hash` property is **not the same** as setting it to `false`. When `hash` is `undefined`, either
> because the property is not specified at all, or its value is set to `undefined` explicitly, the value of the
> `implicitMode` routing option, which is set when the library is initialized, will be used to resolve a `true` or
> `false` value.
>
> This is true for all components.## Playing with Fire
At your own risk, you could use exported API like `getRouterContext()` and `setRouterContext()` to perform unholy acts
on the router layouts, again, **at your own risk**.---
[Full Documentation @ Hashnode Space](https://wjfe-n-savant.hashnode.space)
If you would like to report a bug or request a feature, head to the [Issues page](https://github.com/WJSoftware/wjfe-n-savant/issues).