{"id":27404697,"url":"https://github.com/wjsoftware/wjfe-n-savant","last_synced_at":"2025-10-07T02:08:20.782Z","repository":{"id":277387496,"uuid":"920980625","full_name":"WJSoftware/wjfe-n-savant","owner":"WJSoftware","description":"The client-side router for Svelte v5 SPA's that invented multi hash routing.","archived":false,"fork":false,"pushed_at":"2025-09-27T03:50:59.000Z","size":545,"stargazers_count":32,"open_issues_count":10,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-27T05:43:48.594Z","etag":null,"topics":["mfe","microfrontend","router","svelte"],"latest_commit_sha":null,"homepage":"https://wjfe-n-savant.hashnode.space/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/WJSoftware.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-01-23T05:41:27.000Z","updated_at":"2025-09-27T03:50:56.000Z","dependencies_parsed_at":"2025-02-13T17:48:57.263Z","dependency_job_id":"46899637-307f-4325-ac91-d59375c0b512","html_url":"https://github.com/WJSoftware/wjfe-n-savant","commit_stats":null,"previous_names":["wjsoftware/wjfe-n-savant"],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/WJSoftware/wjfe-n-savant","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WJSoftware%2Fwjfe-n-savant","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WJSoftware%2Fwjfe-n-savant/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WJSoftware%2Fwjfe-n-savant/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WJSoftware%2Fwjfe-n-savant/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/WJSoftware","download_url":"https://codeload.github.com/WJSoftware/wjfe-n-savant/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WJSoftware%2Fwjfe-n-savant/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278708004,"owners_count":26031932,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-07T02:00:06.786Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mfe","microfrontend","router","svelte"],"created_at":"2025-04-14T05:39:39.233Z","updated_at":"2025-10-07T02:08:20.776Z","avatar_url":"https://github.com/WJSoftware.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# \u003cimg src=\"src/lib/logo/logo-48.svg\" alt=\"N-Savant Logo\" width=\"48\" height=\"48\" align=\"left\"\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;@wjfe/n-savant\n\n\u003e The client-side router for Svelte v5 SPA's that invented multi hash routing.\n\n[REPL Demo](https://svelte.dev/playground/d273d356947e48c0822a65402fd06fac)\n\n## Features\n\n\u003e **📝 Small and Unique!**\n\u003e \n\u003e + Less than **1,450** lines of code, including TypeScript typing.\n\u003e + Always-on path and hash routing.  Simultaneous and independent routing modes.\n\u003e + The router that invented multi hash routing.\n\u003e + **NEW!** Supports Sveltekit (via [@wjfe/n-savant-sk](https://github.com/WJSoftware/wjfe-n-savant-sk))\n\n+ **Electron support**:  Works with Electron (all routing modes)\n+ **Reactivity-based**:  All data is reactive, reducing the need for events and imperative programming.\n+ **Always-on path and hash routing**:  Add routers that use the URL's path name or the URL's hash value in the same \napplication.  Both routing modes are possible simultaneously.\n+ **Multi-hash routing**:  This is the first router in the world to do this:  You can create named paths in the hash \nvalue of the URL and create router hierarchies that use a specific named path.\n\n### `\u003cRouter\u003e` Component\n\n+ **Multi-matching routes**:  All routes are evaluated, which is useful to mount micro-frontends.\n+ **Base paths**:  Specify base paths that are inherited by routes and nesting routers.\n+ **Nesting routers**:  Add child routers inside routers for fine-grained control.\n+ **Liberty**:  Place anything anywhere inside.  No child restrictions.\n\n### `\u003cRoute\u003e` Component\n\n+ **Exact path matching**:  Exact match by default; specify the rest parameter to relax the condition.\n+ **Path as string or regular expression**:  Define paths however's best for you.\n+ **Route parameters**:  Define route parameters inside string paths or regular expression paths.\n+ **Rest parameter**:  Collect \"the rest\" of the path.\n+ **Optional parameters**:  Parameters may be specified as optional.\n+ **Additional matching logic**:  Add a predicate function to further restrict a route's matching ability.\n+ **Path is optional**:  Forgo path specification entirely and handle route matching entirely with code.\n+ **Superb Intellisense**:  The route parameters are strongly typed when defining them inside a string path.\n+ **Disconnected UI pieces**:  Repeat route keys in `Route` components to show disconnected pieces of UI for a given \nroute's key.\n\n### `\u003cFallback\u003e` Component\n\n+ **Non-matching content**:  Show users something when there are no matching routes.\n+ **Disconnected content**:  Add as many `Fallback` components as needed in various places.\n\n### `\u003cLink\u003e` Component\n\n+ **Drop-in replacement**:  Exchange `\u003ca\u003e` tags with `\u003cLink\u003e` tags and you're done.[^1]\n+ **Specify state**:  Set history state upon hyperlink click.\n+ **Active state based on route key**:  Automatically set active state and `aria-current` by specifying the route's key.\n+ **Replace or push**:  Select the method for pushing state.\n+ **Preserve query string**:  Opt in to preserve existing query string values.\n+ **Shallow routing**:  [This Sveltekit](https://svelte.dev/docs/kit/shallow-routing) document explains the concept.\n\n[^1]:  For hyperlink components that only specify a hash and are converted to hash-routing `\u003cLink\u003e` components, remove \nthe pound sign (`#`) from the href.\n\n### `\u003cLinkContext\u003e` Component\n\n+ **Centralize `\u003cLink\u003e` configuration**:  Configures a special context that all `\u003cLink\u003e` components follow.\n\n### `\u003cRouterTrace\u003e` Component\n\n+ **Tracing Information**:  Drop it inside a router to display its route status data, including the internal regular \nexpressions that are generated from string path patterns.\n+ **Specify a specific router**:  Ability to give it a specific router engine object, allowing tracing of router engine \nobjects created in code.\n+ **Track child routers**:  See and traverse the router hierarchy.\n\n### `location` Global Object\n\n+ **Reactive URL**:  URL object that's always in sync with the browser's URL.\n+ **Reactive state**:  Reactive state property that's always in sync with the history state.\n+ **Reactive hash paths**:  Reactive dictionary object for all hash paths.\n+ **Programatic navigation**:  Use the the `navigate()` method to trigger navigation programatically.\n\n#### In Full Mode...\n\n+ **Cancellable `beforeNavigate` event**:  Get notified of navigation events, and cancel when appropriate.\n+ **`navigationCancelled` event**:  Get notified whenever navigation is cancelled.\n+ **History API interception**:  Gain control over the history object to avoid external code/routers from \nde-synchronizing state.\n+ **Micro-frontends**:  Full mode's features are great for micro-frontend scenarios where other routers (from \npotentially other technologies) might interfere with the router's functionality.\n\n## Quickstart\n\n1. Install the package.\n2. Initialize the library.\n3. Define the routes inside routers.\n4. Modify/Add your navigation links.\n\n### Install the package\n\n```bash\nnpm i @wjfe/n-savant\n```\n\n### Initialize the Library\n\n```typescript\n// In your main.ts, or somewhere BEFORE any routers are created:\nimport { init } from \"@wjfe/n-savant\";\n\n// Default:  Lite mode, implicit path routing, no router hierarchy tracing, single hash mode.\ninit();\n\n// If all you care about is (traditional) hash routing, the recommendation is to change the implicit mode:\ninit({ implicitMode: 'hash' });\n```\n\n#### Electron Variant\n\nIn Electron, we must immediately navigate to the homepage (or your preferred initial route) right after initializing if you use path routing:\n\n```typescript\nimport { init, location } from \"@wjfe/n-savant\";\n\ninit();\nlocation.goTo('/');\n```\n\nFor 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.\n\n\u003e **⚠️ Important:** Hash routing doesn't require this extra navigation step.\n\n### Define the Routes\n\n`\u003cRoute\u003e`s are added inside `\u003cRouter\u003e`s.  `\u003cRouter\u003e`s can be nested inside other `\u003cRouter\u003e`s.  `\u003cRoute\u003e`s can render \n`\u003cRouter\u003e`s or other `\u003cRoute\u003e`s, etc.  You get the idea:  You do as you wish.\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n  import { Router, Route } from \"@wjfe/n-savant\";\n  import NavBar from \"./lib/NavBar.svelte\";\n  import UserView from \"./lib/UserView.svelte\";\n\u003c/script\u003e\n\n\u003cRouter\u003e\n  \u003cNavBar /\u003e\n  \u003cdiv class=\"container\"\u003e\n    \u003c!-- content outside routes is always rendered --\u003e\n    \u003ch1\u003eRouting Demo\u003c/h1\u003e\n    \u003cRoute key=\"users\" path=\"/users\"\u003e\n      \u003c!-- content here --\u003e\n    \u003c/Route\u003e\n    \u003cRoute key=\"user\" path=\"/users/:userId\"\u003e\n      \u003c!-- access parameters via the snippet parameter --\u003e\n      {#snippet children(params)}\n        \u003cUserView id={params.userId} /\u003e \u003c!-- Intellisense will work here!! --\u003e\n      {/snippet}\n    \u003c/Route\u003e\n    ...\n  \u003c/div\u003e\n\u003c/Router\u003e\n```\n\n### Navigation Links\n\nThe previous step added the `\u003cNavBar /\u003e` component inside the router.  This is the best practice for full `\u003cLink\u003e` \nfunctionality.  Still, this is not mandatory.\n\n```svelte\n\u003c!-- NavBar.svelte --\u003e\n\u003cscript lang=\"ts\"\u003e\n  import { Link } from \"@wjfe/n-savant\";\n\u003c/script\u003e\n\n\u003cnav\u003e\n  \u003cdiv class=\"nav-links\"\u003e\n    \u003cul\u003e\n      \u003cli class=\"nav-link\"\u003e\n        \u003cLink href=\"/users\" activeFor=\"users\" activeState={{ class: 'active' }}\u003e\n          All Users\n        \u003c/Link\u003e\n      \u003c/li\u003e\n      ...\n    \u003c/ul\u003e\n  \u003c/div\u003e\n\u003c/nav\u003e\n```\n\n## Micro-Frontend Goodness\n\nThis router's implementation intends to cater for micro-frontends as best as possible.  The following are features and \nstrategies that are possible with this router.\n\n### Multi-Route Matching\n\nRouters always evaluate all defined routes, so it is possible for more than one route to match.  This facilitates the \nlayout of micro-frontends.  For example, a navigation micro-frontend could be inside a route that either always matches \nor matches most of the time, so navigation links are available the majority/all of the time.\n\n### Simultaneous, Always-On Path and Hash Routing\n\nComponents (`Router`, `Route`, `Link`, `Fallback` and `RouterTrace`) with the same value of the `hash` property belong \nto the same \"universe\".  Components with different hash values belong to different universes, and these universes are \nparallel universes.  Components with hash value `false` use the URL's path name and will never interfere with routers \nthat use hash routing (hash value `true` or a path's name).  The main micro-frontend(s) may route using the URL's path \nname, while specialty MFE's could route using the path in the hash part of the URL.\n\n### Multi-Hash Routing\n\nAs of February 2025, no other router in the world can do this.\n\nImagine a scenario where your MFE application would like to show side-by-side two micro-frontends that are \nrouter-enabled (meaning they use or need to work with a path).  With traditional routing, you could not have this setup \nbecause one MFE would take over the path, leaving the other MFE without one.\n\nMulti-hash routing creates named paths in the hash value, giving routers the ability to share the hash value with other \nrouters.  A hash value of the form `#path1=/path/1;path2=/path/2;...` could power side-by-side MFE's on, say, 4K \nlayouts.\n\n### EXPERIMENTAL - Replacing the `single-spa` Router\n\nIt is the author's intent to implement micro-frontends with only `single-spa` parcels and this router.  In other words, \nabandon the use of `registerApplication()` and `start()` and just mount parcels using this router.\n\n[single-spa](https://single-spa.js.org)\n\n## Unobtrusive Philosophy\n\nThis mini router library imposes minimal restrictions.  Here are some features provided by other much larger codebases \nthat are not provided here because Svelte already has the capability.\n\n### Transitions\n\nNothing prevents you to add transitions to anything.\n\n```svelte\n\u003cRoute key=\"users\" path=\"/users/:userId\"\u003e\n  {#snippet children(params)}\n    \u003cdiv transition:fade\u003e\n      ...\n    \u003c/div\u003e\n  {/snippet}\n\u003c/Route\u003e\n```\n\n\u003e **📝 Note:** This one item might be worthwhile revisiting for the cases where synchronized transitions are desired.  This, \n\u003e however, won't be looked at until Svelte attachments become a thing.\n\n### Guarded Routes\n\nGuard routes however you wish.  Maybe with an `{#if}` block, or maybe using the route's `and` property that allows you \nto specify a predicate function.  There are probably many other ways.\n\n### `Exact` Property on Routes\n\nNot needed.  All matching is exact path matching, and if you want to opt out of the exact route matching, simply add \nthe `rest` parameter specifier (`/*`):\n\n```svelte\n\u003cRoute key=\"admin\" path=\"/admin/*\"\u003e\n  ...\n\u003c/Route\u003e\n```\n\nNow route matching for this route will behave as \"starts with\".  If you don't care about the value of the parameter, \njust ignore it.\n\n### Lazy-Loading\n\nLazy-loading components is very simple:\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n  function loadUsersComponent() {\n    return import('./lib/Users.svelte').then(m =\u003e m.default);\n  }\n\u003c/script\u003e\n\n\u003cRoute key=\"users\" path=\"/users\"\u003e\n  {#await loadUsersComponent()}\n    \u003cspan\u003eLoading...\u003c/span\u003e\n  {:then Users}\n    \u003cUsers /\u003e\n  {:catch}\n    \u003cp\u003eOops!\u003c/p\u003e\n  {/await}\n\u003c/Route\u003e\n```\n\n### Navigation Events\n\nThere are no navigation events defined.  Simply write effects or derived computations based on the global `location` \nobject's `url`, `state` or `hashPaths` properties, which are reactive.\n\n```typescript\nimport { location } from \"@wjfe/n-savant\";\n\n// Or $derived, whichever you need.\n$effect(() =\u003e {\n  // Read location.url to re-run on URL changes (navigation).\n  location.url;\n  // Read location.state to re-run on state changes.\n  location.state;\n  // Read location.hashPaths to re-run on hash changes (hash navigation).\n  // The route named \"single\" is the one you want if doing hash routing.\n  location.hashPaths.single;\n});\n```\n\n### Parameter Types\n\nThere is no parameter type specification.  All parameter values go through type parsing:\n\n+ If the value represents a number, then the parameter value will be a `number`.\n+ If the value is the word `'true'` or `'false'`, then the parameter value will be a `boolean`.\n+ If none of the above, the value will be a `string`.\n\nIf the demand for parameter value types grow, this might be reconsidered, but know that this is easily achievable with \nthe `and` property on routes, or by specifying the path as a regular expression.\n\nIn the context of the following code, the path `'/users/summary'` would match both routes, so the one that needs a \nnumeric parameter value uses the `and` property to type-check the value:\n\n```svelte\n\u003cRoute path=\"/users/:userId\" and={(rp) =\u003e typeof rp.userId === 'number'}\u003e\n  {#snippet children(rp)}\n    \u003cUserDetails userId={rp.userId} /\u003e\n  {/snippet}\n\u003c/Route\u003e\n\u003cRoute path=\"/users/summary\"\u003e\n  \u003cUsersSummary /\u003e\n\u003c/Route\u003e\n```\n\nThis is the version using a regular expression for the `path` property:\n\n```svelte\n\u003cRoute path={/\\/users\\/(?\u003cuserId\u003e\\d+)/i}\u003e\n  {#snippet children(rp)}\n    \u003cUserDetails userId={rp.userId} /\u003e\n  {/snippet}\n\u003c/Route\u003e\n\u003cRoute path=\"/users/summary\"\u003e\n  \u003cUsersSummary /\u003e\n\u003c/Route\u003e\n```\n\n### Reacting to Route Matching Events\n\nIf you're interested in reacting whenever (a) particular route(~~s~~) match(es), you can get a hold of the `routeStatus` \nproperty of router engines (which is reactive) by binding to a router's `router` property:\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n  import { RouterEngine } from \"@wjfe/n-savant/core\";\n\n  let router: $state\u003cRouterEngine\u003e();\n\n  $effect(() =\u003e {\n    for (let [key, rs] of Object.entries(router.routeStatus)) {\n      // key: Route's key\n      // rs:  RouteStatus for the route.\n      if (rs.match) {\n        // Do stuff with rs.routeParams, for example.\n      }\n    }\n  });\n\u003c/script\u003e\n\n\u003cRouter bind:router\u003e\n  ...\n\u003c/Router\u003e\n```\n\n## Navigation\n\n\u003e Since **v0.4.0**\n\n\u003e **💥 BREAKING CHANGE:**  Navigation has been re-done in v0.4.0.\n\nThe recommended way of navigating is to create `\u003cLink\u003e` component instances to render links on the document(s).  If \nneeded, however, there are 2 navigation functions in the `location` object:  `navigate()` and `goTo()`.\n\n### `navigate(url, options)` - Routing Universe Aware\n\nThis is the preferred method for programmatic navigation as it understands routing universes and properly manages state:\n\n```typescript\nimport { location } from \"@wjfe/n-savant\";\n\n// Path routing navigation:\nlocation.navigate('/new/path', { \n  replace: true, \n  state: { custom: 'Hi' },\n  hash: false \n});\n\n// Hash routing navigation:\nlocation.navigate('/new/path', { \n  replace: true, \n  state: { custom: 'Hi' },\n  hash: true \n});\n\n// Multi-hash routing navigation:\nlocation.navigate('/new/path', { \n  replace: true, \n  state: { custom: 'Hi' },\n  hash: 'path1' \n});\n\n// Preserve existing query parameters:\nlocation.navigate('/new/path', { \n  preserveQuery: true,\n  hash: false\n});\n```\n\nThe `navigate()` method automatically:\n- Associates state with the correct routing universe based on the `hash` option\n- Preserves other routing universe states (e.g., when navigating `path1`, other named paths remain intact)\n- Handles URL construction using the robust `calculateHref()` logic\n\n### `goTo(url, options)` - Direct URL Navigation  \n\nThis method provides direct URL navigation without routing universe awareness:\n\n```typescript\nimport { location } from \"@wjfe/n-savant\";\n\n// Direct URL navigation:\nlocation.goTo('https://example.com/new/path', { \n  replace: true,\n  state: { path: undefined, hash: {} }  // Must provide complete State object\n});\n\n// Shallow routing (navigate to current URL):\nlocation.goTo('', { replace: true });\n\n// Preserve query parameters:\nlocation.goTo('/new/path', { \n  preserveQuery: ['param1', 'param2'] \n});\n```\n\n**⚠️ 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.\n\n### Options Reference\n\nBoth methods support these common options:\n\n- **`replace?: boolean`** - Replace current URL instead of pushing new entry (default: `false`)\n- **`preserveQuery?: PreserveQuery`** - Preserve current query parameters\n  - `true` - Preserve all query parameters\n  - `string` - Preserve specific parameter by name  \n  - `string[]` - Preserve multiple specific parameters\n\nAdditional `navigate()` options:\n- **`hash?: Hash`** - Routing universe to associate with (`false`, `true`, or named hash)\n- **`state?: any`** - State data to associate with the navigation\n\nAdditional `goTo()` options:\n- **`state?: State`** - Complete state object conforming to library expectations\n\n### Navigation Best Practices\n\n1. **Use `\u003cLink\u003e` components** for user-triggered navigation\n2. **Use `navigate()`** for programmatic navigation within routing universes\n3. **Use `goTo()`** only for direct URL manipulation or external navigation\n4. **Always specify `hash`** in `navigate()` to ensure proper state management\n\nJust in case you are wondering:  The navigation logic is already there in `\u003cLink\u003e` components:\n\n```svelte\n\u003c!-- Path Routing =\u003e https://example.com/new/path --\u003e\n\u003cLink hash=\"false\" href=\"/new/path\"\u003eClick Me!\u003c/Link\u003e\n\n\u003c!-- Hash Routing =\u003e https://example.com/#/new/path --\u003e\n\u003cLink hash=\"true\" href=\"/new/path\"\u003eClick Me!\u003c/Link\u003e\n\n\u003c!-- Multi Hash Routing =\u003e https://example.com/#path1=/new/path --\u003e\n\u003c!-- Will also preserve any other named paths --\u003e\n\u003cLink hash=\"path1\" href=\"/new/path\"\u003eClick Me!\u003c/Link\u003e\n```\n\n\u003e **⚠️ Important:** Not setting the `hash` property is **not the same** as setting it to `false`.  When `hash` is `undefined`, either \n\u003e because the property is not specified at all, or its value is set to `undefined` explicitly, the value of the \n\u003e `implicitMode` routing option, which is set when the library is initialized, will be used to resolve a `true` or \n\u003e `false` value.\n\u003e\n\u003e This is true for all components.\n\n## Playing with Fire\n\nAt your own risk, you could use exported API like `getRouterContext()` and `setRouterContext()` to perform unholy acts \non the router layouts, again, **at your own risk**.\n\n---\n\n[Full Documentation @ Hashnode Space](https://wjfe-n-savant.hashnode.space)\n\nIf you would like to report a bug or request a feature, head to the [Issues page](https://github.com/WJSoftware/wjfe-n-savant/issues).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwjsoftware%2Fwjfe-n-savant","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwjsoftware%2Fwjfe-n-savant","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwjsoftware%2Fwjfe-n-savant/lists"}