{"id":26206403,"url":"https://github.com/WJSoftware/wjfe-n-savant","last_synced_at":"2025-03-12T05:01:49.445Z","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-03-06T05:11:51.000Z","size":166,"stargazers_count":5,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-06T05:24:15.620Z","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":null,"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":null,"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}},"created_at":"2025-01-23T05:41:27.000Z","updated_at":"2025-03-06T05:11:34.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":5,"template":false,"template_full_name":null,"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","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243158972,"owners_count":20245671,"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","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-03-12T05:01:05.967Z","updated_at":"2025-03-12T05:01:49.438Z","avatar_url":"https://github.com/WJSoftware.png","language":"TypeScript","funding_links":[],"categories":["Routers"],"sub_categories":["Internationalization"],"readme":"# @wjfe/n-savant\n\n\u003e The client-side router for Svelte v5 SPA's that invented multi hash routing.\n\n## Features\n\n\u003e [!NOTE]\n\u003e #### Small and Unique!\n\u003e \n\u003e + Less than **1,050** 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\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### 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\" activeState={{ key: 'users', class: 'active' }}\u003eAll Users\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 mayority/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 Februrary 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\nMutli-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## Unintrusive 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]\n\u003e 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\u003eOoops!\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\nThe recommended way of navigating is to create `\u003cLink\u003e` component instances to render links on the document(s).  If \nneeded, however, there's a programmatic way of navigating:  `location.navigate()`.\n\n```typescript\nimport { location } from \"@wjfe/n-savant\";\n\n// Path routing navigation:\nlocation.navigate('/new/path', { replace: true, state: { custom: 'Hi' }});\n\n// Hash routing navigation:\nlocation.navigate('#/new/path', { replace: true, state: { custom: 'Hi' }});\n\n// Multi-hash routing navigation:\nlocation.navigate('/new/path', 'path1', { replace: true, state: { custom: 'Hi' }});\n```\n\nNavigation in multi-hash scenarios is tricky:  One must make sure other paths remain untouched, and the information \nabout these other paths is stored in `location.hashPaths`.  You could use the second form above for multi-hash routing \nas long as you understand that it is your responsibility to (possibly) ensure the integrity of other paths defined in \nthe URL's hash value.\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]\n\u003e 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"}