├── .npmrc ├── playground ├── src │ ├── assets │ │ ├── logo.svg │ │ ├── test.svg │ │ └── svelte.svg │ ├── vite-env.d.ts │ ├── app.scss │ ├── lib │ │ ├── views │ │ │ ├── hash-routing │ │ │ │ ├── code-samples │ │ │ │ │ ├── basic-hash-router.txt │ │ │ │ │ ├── basic-hash-router.svelte │ │ │ │ │ ├── hash-url-examples.ts │ │ │ │ │ ├── multi-hash-router.txt │ │ │ │ │ ├── multi-hash-router.svelte │ │ │ │ │ ├── links-and-navigation.svelte │ │ │ │ │ ├── path-and-hash.txt │ │ │ │ │ └── live-hash-display.svelte │ │ │ │ ├── HashRoutingView.svelte │ │ │ │ └── ShowCodeButton.svelte │ │ │ ├── home │ │ │ │ ├── code-samples │ │ │ │ │ ├── path-routing-example.txt │ │ │ │ │ ├── hash-routing-example.txt │ │ │ │ │ └── multi-hash-example.txt │ │ │ │ ├── Hero.svelte │ │ │ │ └── Quickstart.svelte │ │ │ ├── path-routing │ │ │ │ ├── code-samples │ │ │ │ │ ├── basic-path-router.txt │ │ │ │ │ ├── link-component.txt │ │ │ │ │ ├── route-guards.txt │ │ │ │ │ ├── url-examples.ts │ │ │ │ │ ├── nested-routing.txt │ │ │ │ │ └── route-parameters.txt │ │ │ │ └── PathRoutingView.svelte │ │ │ ├── ViewSkeleton.svelte │ │ │ └── redirected │ │ │ │ └── RedirectedView.svelte │ │ ├── HashRoutes.svelte │ │ ├── NotFound.svelte │ │ ├── FeatureItem.svelte │ │ ├── BreadcrumbItem.svelte │ │ ├── ListGroupItem.svelte │ │ ├── ListGroup.svelte │ │ ├── FeatureList.svelte │ │ ├── Breadcrumb.svelte │ │ ├── hash-routing.ts │ │ ├── demo │ │ │ ├── LinkFeaturesView.svelte │ │ │ ├── ApiFeaturesView.svelte │ │ │ ├── RouterEngineFeaturesView.svelte │ │ │ ├── RouterFeaturesView.svelte │ │ │ ├── RouteFeaturesView.svelte │ │ │ ├── LocationFeaturesView.svelte │ │ │ ├── DemoView.svelte │ │ │ └── NavBar.svelte │ │ ├── ThemeSwitch.svelte │ │ ├── CardBody.svelte │ │ ├── state │ │ │ ├── title.svelte.ts │ │ │ └── theme.svelte.ts │ │ ├── types.ts │ │ ├── DemoBc.svelte │ │ ├── Card.svelte │ │ ├── CardHeader.svelte │ │ ├── Badge.svelte │ │ ├── SubNav.svelte │ │ └── Alert.svelte │ └── main.ts ├── .vscode │ └── extensions.json ├── tsconfig.json ├── vite.config.ts ├── svelte.config.js ├── .gitignore ├── index.html ├── tsconfig.app.json ├── tsconfig.node.json ├── package.json ├── public │ └── vite.svg └── README.md ├── .prettierignore ├── static ├── favicon.png ├── _redirects ├── robots.txt └── logo-64.svg ├── src ├── routes │ ├── +layout.svelte │ ├── api │ │ ├── kit │ │ │ ├── objects-and-classes │ │ │ │ └── +page.md │ │ │ ├── kitfallback │ │ │ │ └── +page.md │ │ │ └── functions │ │ │ │ └── +page.md │ │ ├── core │ │ │ ├── linkcontext │ │ │ │ └── +page.md │ │ │ ├── fallback │ │ │ │ └── +page.md │ │ │ └── router │ │ │ │ └── +page.md │ │ ├── +layout.svelte │ │ └── techArticleMetaData.json │ ├── +page.svelte │ └── docs │ │ ├── existing-extension-packages │ │ └── +page.md │ │ ├── +layout.svelte │ │ ├── per-routing-mode-data │ │ └── +page.md │ │ ├── electron-support │ │ └── +page.md │ │ └── reactive-data │ │ └── +page.md ├── lib │ ├── testing │ │ ├── LinkContextSpy.svelte │ │ ├── testWithEffect.svelte.ts │ │ ├── TestLinkContextWithContextSpy.svelte │ │ ├── TestActiveBehavior.svelte │ │ └── TestRouteWithRouter.svelte │ ├── logo │ │ ├── logo.d.ts │ │ ├── logo-48.svg │ │ └── logo-64.svg │ ├── kernel │ │ ├── resolveHashValue.ts │ │ ├── LocationState.test.ts │ │ ├── Logger.ts │ │ ├── index.ts │ │ ├── isConformantState.ts │ │ ├── calculateMultiHashFragment.ts │ │ ├── Location.ts │ │ ├── LocationFull.ts │ │ ├── index.test.ts │ │ ├── resolveHashValue.test.ts │ │ ├── Location.test.ts │ │ ├── dissectHrefs.ts │ │ ├── initCore.ts │ │ ├── LocationState.svelte.ts │ │ ├── calculateState.ts │ │ ├── options.ts │ │ ├── trace.svelte.ts │ │ ├── trace.test.ts │ │ ├── StockHistoryApi.svelte.ts │ │ ├── preserveQuery.ts │ │ ├── LocationLite.svelte.ts │ │ └── InterceptedHistoryApi.svelte.ts │ ├── Fallback │ │ ├── README.md │ │ └── Fallback.svelte │ ├── init.ts │ ├── init.test.ts │ ├── index.ts │ ├── public-utils.ts │ ├── buildHref.ts │ ├── index.test.ts │ ├── Route │ │ └── README.md │ ├── utils.ts │ ├── behaviors │ │ └── active.svelte.ts │ ├── LinkContext │ │ ├── README.md │ │ └── LinkContext.svelte │ ├── utils.test.ts │ ├── RouterTrace │ │ └── README.md │ └── Link │ │ └── README.md ├── index.html └── app.d.ts ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── test.yml │ ├── publish.yml │ ├── deploy-pages.yml │ └── preview-pages.yml ├── .prettierrc ├── vite.config.ts ├── svelte.config.js ├── eslint.config.js ├── LICENSE ├── patches └── @sveltepress+theme-default+7.0.2.patch ├── add-component-help.ps1 ├── package.json └── scripts └── generate-sitemap.js /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /playground/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/src/assets/test.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /playground/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WJSoftware/svelte-router/HEAD/static/favicon.png -------------------------------------------------------------------------------- /playground/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /static/_redirects: -------------------------------------------------------------------------------- 1 | # All Sveltekit routes, when deployed with adapter-static, need redirection. 2 | /docs/* / 200 3 | /api/* / 200 4 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | # Algolia-Crawler-Verif: 94605A6E3B552110 2 | 3 | User-agent: * 4 | Allow: / 5 | 6 | # Allow search engines to crawl all pages 7 | Sitemap: /sitemap.xml 8 | -------------------------------------------------------------------------------- /playground/src/app.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/bootstrap'; 2 | 3 | $bootstrap-icons-font-dir: 'bootstrap-icons/font/fonts'; 4 | @import 'bootstrap-icons/font/bootstrap-icons'; 5 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [svelte()] 7 | }); 8 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {@render children()} 9 | -------------------------------------------------------------------------------- /src/routes/api/kit/objects-and-classes/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Objects & Classes 3 | description: API reference for objects and classes in the SvelteKit extension package for Svelte Router 4 | --- 5 | 6 | Currently, no objects or classes are exported by `@svelte-router/kit`. 7 | -------------------------------------------------------------------------------- /playground/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess() 7 | }; 8 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/code-samples/basic-hash-router.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /playground/src/lib/HashRoutes.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {#each Object.entries(location.hashPaths) as [id, path]} 7 |
{id}
8 |
{path}
9 | {/each} 10 |
11 | -------------------------------------------------------------------------------- /playground/src/lib/NotFound.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

This Is Not What You're Looking For

7 |

The URL {location.url} is not mapped to anything.

8 |
9 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/code-samples/basic-hash-router.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /playground/src/lib/views/home/code-samples/path-routing-example.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {#snippet children({ rp })} 7 | 8 | {/snippet} 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/lib/testing/LinkContextSpy.svelte: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /playground/src/lib/FeatureItem.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
  • 12 |   13 | {@render children?.()} 14 |
  • 15 | -------------------------------------------------------------------------------- /playground/src/lib/views/path-routing/code-samples/basic-path-router.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /playground/src/lib/BreadcrumbItem.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
  • 10 | {@render children?.()} 11 |
  • 12 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/lib/logo/logo.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@svelte-router/core/logo' { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module '@svelte-router/core/logo64' { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module '@svelte-router/core/logo48' { 12 | const content: string; 13 | export default content; 14 | } 15 | -------------------------------------------------------------------------------- /playground/src/lib/views/path-routing/code-samples/link-component.txt: -------------------------------------------------------------------------------- 1 | 2 | View Profile 3 | 4 | 5 | View Profile 6 | 7 | 8 | 9 | Account Settings 10 | 11 | -------------------------------------------------------------------------------- /playground/src/lib/views/home/code-samples/hash-routing-example.txt: -------------------------------------------------------------------------------- 1 | // Initialize with hash as default mode 2 | init({ defaultHash: true }); 3 | 4 | // Now you can omit the hash property! 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /playground/src/lib/views/path-routing/code-samples/route-guards.txt: -------------------------------------------------------------------------------- 1 | user.isAdmin} 5 | > 6 | 7 | 8 | 9 | params.userId === currentUser.id || user.canViewProfiles} 13 | > 14 | 15 | 16 | -------------------------------------------------------------------------------- /playground/src/lib/views/home/code-samples/multi-hash-example.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | /dist 11 | 12 | # OS 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Env 17 | .env 18 | .env.* 19 | !.env.example 20 | !.env.test 21 | 22 | # Vite 23 | vite.config.js.timestamp-* 24 | vite.config.ts.timestamp-* 25 | 26 | # Tarballs 27 | *.tgz 28 | 29 | # SveltePress 30 | .sveltepress 31 | -------------------------------------------------------------------------------- /playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { mount } from 'svelte'; 2 | import App from './App.svelte'; 3 | import { init } from '@svelte-router/core'; 4 | import { routingMode } from './lib/hash-routing'; 5 | 6 | init({ 7 | hashMode: routingMode, 8 | trace: { routerHierarchy: true } 9 | }); 10 | const app = mount(App, { 11 | target: document.getElementById('app')! 12 | }); 13 | 14 | export default app; 15 | -------------------------------------------------------------------------------- /playground/src/lib/ListGroupItem.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
  • 12 | {@render children?.()} 13 |
  • 14 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 | 11 |
    %sveltekit.body%
    12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/code-samples/hash-url-examples.ts: -------------------------------------------------------------------------------- 1 | // Single Hash Mode URLs: 2 | // https://example.com#/ 3 | // https://example.com#/profile/123 4 | // https://example.com#/settings/account 5 | 6 | // Multi Hash Mode URLs: 7 | // https://example.com#sidebar=/nav;main=/dashboard 8 | // https://example.com#d1=/demo/routes;d2=/demo/links 9 | // https://example.com#nav=/home;content=/profile/123;modal=/settings 10 | -------------------------------------------------------------------------------- /playground/src/lib/ListGroup.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/code-samples/multi-hash-router.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /playground/src/lib/FeatureList.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/code-samples/multi-hash-router.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/code-samples/links-and-navigation.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | Start Demo 9 | Start Demo (anchor link) 10 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/lib/**' 8 | - 'package.json' 9 | - 'vite.config.ts' 10 | - 'svelte.config.js' 11 | - 'tsconfig.json' 12 | 13 | jobs: 14 | test: 15 | uses: WJSoftware/cicd/.github/workflows/npm-test.yml@main 16 | with: 17 | node-version: 24 18 | build-script: build 19 | test-script: test 20 | build: false 21 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/code-samples/path-and-hash.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | // See https://svelte.dev/docs/kit/types#app.d.ts 4 | // for information about these interfaces 5 | declare global { 6 | namespace App { 7 | // interface Error {} 8 | // interface Locals {} 9 | // interface PageData {} 10 | // interface PageState {} 11 | // interface Platform {} 12 | } 13 | } 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /src/lib/testing/testWithEffect.svelte.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'vitest'; 2 | 3 | export function testWithEffect(name: string, fn: () => void | Promise) { 4 | test(name, () => { 5 | let promise!: void | Promise; 6 | const cleanup = $effect.root(() => { 7 | promise = fn(); 8 | }); 9 | if (promise) { 10 | return promise.finally(cleanup); 11 | } else { 12 | cleanup(); 13 | } 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/kernel/resolveHashValue.ts: -------------------------------------------------------------------------------- 1 | import type { Hash } from '$lib/types.js'; 2 | import { routingOptions } from './options.js'; 3 | 4 | /** 5 | * Resolves the given hash value taking into account the library's routing options. 6 | * @param hash Hash value to resolve. 7 | * @returns The resolved hash value. 8 | */ 9 | export function resolveHashValue(hash: Hash | undefined): Hash { 10 | if (hash === undefined) { 11 | return routingOptions.defaultHash; 12 | } 13 | return hash; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/testing/TestLinkContextWithContextSpy.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/kernel/LocationState.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | import { LocationState } from './LocationState.svelte.js'; 3 | 4 | describe('LocationState', () => { 5 | describe('constructor', () => { 6 | test('Should create a new instance with the expected default values.', () => { 7 | // Act. 8 | const ls = new LocationState(); 9 | 10 | // Assert. 11 | expect(ls.state).toEqual({ hash: {} }); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /playground/src/lib/views/path-routing/code-samples/url-examples.ts: -------------------------------------------------------------------------------- 1 | // Basic routes: 2 | // / → Home page 3 | // /about → About page 4 | // /contact → Contact page 5 | 6 | // Parameterized routes: 7 | // /user/123 → User profile for ID 123 8 | // /blog/tech/svelte-5 → Blog post in tech category 9 | // /product/laptop/123 → Product page 10 | 11 | // Rest parameters: 12 | // /files/docs/readme.md → File explorer 13 | // /admin/users/permissions → Admin section 14 | -------------------------------------------------------------------------------- /playground/src/lib/views/path-routing/code-samples/nested-routing.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "printWidth": 100, 7 | "plugins": ["prettier-plugin-svelte"], 8 | "overrides": [ 9 | { 10 | "files": "*.svelte", 11 | "options": { 12 | "parser": "svelte" 13 | } 14 | }, 15 | { 16 | "files": ["*.json", "*.yml", "*.yaml"], 17 | "options": { 18 | "tabWidth": 2 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /playground/src/lib/Breadcrumb.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { sveltepress } from '@sveltepress/vite'; 3 | import { svelteTesting } from '@testing-library/svelte/vite'; 4 | import { siteConfig, themeConfig } from './sveltepress/config.js'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | sveltepress({ 9 | siteConfig, 10 | theme: themeConfig 11 | }), 12 | svelteTesting() 13 | ], 14 | 15 | test: { 16 | environment: 'jsdom', 17 | globals: true, 18 | include: ['src/**/*.{test,spec}.{js,ts}'] 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /src/lib/testing/TestActiveBehavior.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | Content 17 | 18 | -------------------------------------------------------------------------------- /playground/src/lib/views/home/Hero.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    7 |
    8 |

    webJose's {title}

    9 |
    {subtitle}
    10 |
    11 |
    12 | 13 | 24 | -------------------------------------------------------------------------------- /src/routes/api/kit/kitfallback/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: KitFallback Component 3 | description: API reference for KitFallback component optimized for SvelteKit server-side rendering without content flashes 4 | --- 5 | 6 | :::info[Parent Requirement] 7 | `Router` required. 8 | ::: 9 | 10 | This is a Sveltekit-friendly version of `@svelte-router/core`'s `Fallback` component. The stock component produces unwanted flashes of content because router engines don't get routes registered in the server. This version ensures fallback content is never rendered in the server. 11 | 12 | ## Properties 13 | 14 | Refer to the stock [Fallback](/api/core/fallback) component. 15 | -------------------------------------------------------------------------------- /src/lib/kernel/Logger.ts: -------------------------------------------------------------------------------- 1 | import type { ILogger } from '../types.js'; 2 | 3 | const stockLogger: ILogger = globalThis.console; 4 | 5 | const noop = () => {}; 6 | 7 | const offLogger: ILogger = { 8 | debug: noop, 9 | log: noop, 10 | warn: noop, 11 | error: noop 12 | }; 13 | 14 | export let logger: ILogger = offLogger; 15 | 16 | export function setLogger(newLogger: boolean | ILogger) { 17 | logger = newLogger === true ? stockLogger : newLogger === false ? offLogger : newLogger; 18 | } 19 | 20 | /** 21 | * Resets the logger to the default uninitialized state (offLogger). 22 | */ 23 | export function resetLogger(): void { 24 | logger = offLogger; 25 | } 26 | -------------------------------------------------------------------------------- /playground/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true, 17 | "moduleDetection": "force" 18 | }, 19 | "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] 20 | } 21 | -------------------------------------------------------------------------------- /playground/src/lib/views/path-routing/code-samples/route-parameters.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | {#snippet children({ rp })} 4 |

    User ID: {rp?.id}

    5 | {/snippet} 6 |
    7 | 8 | 9 | 10 | {#snippet children({ rp })} 11 | 15 | {/snippet} 16 | 17 | 18 | 19 | 20 | {#snippet children({ rp })} 21 | 22 | {/snippet} 23 | 24 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/HashRoutingView.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    15 |

    🚀 Hash Routing

    16 |

    17 | Navigate your app using URL fragments! Hash routing uses the portion after the `#` character 18 | to determine routes, perfect for client-side navigation that works everywhere. 19 |

    20 | 21 |
    22 | -------------------------------------------------------------------------------- /playground/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /playground/src/lib/hash-routing.ts: -------------------------------------------------------------------------------- 1 | export type HashRoutingMode = 'single' | 'multi'; 2 | 3 | const defaultRoutingMode = 'single' satisfies HashRoutingMode; 4 | const routingModeKey = 'routingMode'; 5 | 6 | export const routingMode = 7 | (globalThis.window.sessionStorage.getItem(routingModeKey) as HashRoutingMode) || 8 | defaultRoutingMode; 9 | 10 | export function toggleRoutingMode() { 11 | const newMode: HashRoutingMode = routingMode === 'single' ? 'multi' : 'single'; 12 | globalThis.window.sessionStorage.setItem(routingModeKey, newMode); 13 | 14 | // Clear the hash to avoid conflicts when switching modes 15 | globalThis.window.location.hash = ''; 16 | 17 | globalThis.window.location.reload(); 18 | } 19 | -------------------------------------------------------------------------------- /playground/src/lib/views/path-routing/PathRoutingView.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
    24 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-router-playground", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" 11 | }, 12 | "devDependencies": { 13 | "@sveltejs/vite-plugin-svelte": "^6.1.3", 14 | "@tsconfig/svelte": "^5.0.4", 15 | "sass": "^1.83.4", 16 | "svelte": "^5.15.0", 17 | "svelte-check": "^4.1.1", 18 | "typescript": "^5.7.3", 19 | "vite": "^7.1.3" 20 | }, 21 | "dependencies": { 22 | "@floating-ui/dom": "^1.7.4", 23 | "bootstrap": "^5.3.3", 24 | "bootstrap-icons": "^1.11.3", 25 | "svelte-highlight": "^7.8.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/kernel/index.ts: -------------------------------------------------------------------------------- 1 | export { location } from './Location.js'; 2 | export { RouterEngine } from './RouterEngine.svelte.js'; 3 | export { isConformantState } from './isConformantState.js'; 4 | export { calculateHref } from './calculateHref.js'; 5 | export { calculateMultiHashFragment } from './calculateMultiHashFragment.js'; 6 | export { calculateState } from './calculateState.js'; 7 | export { initCore } from './initCore.js'; 8 | export { LocationState } from './LocationState.svelte.js'; 9 | export { StockHistoryApi } from './StockHistoryApi.svelte.js'; 10 | export { InterceptedHistoryApi } from './InterceptedHistoryApi.svelte.js'; 11 | export { LocationLite } from './LocationLite.svelte.js'; 12 | export { LocationFull } from './LocationFull.js'; 13 | export { preserveQueryInUrl } from './preserveQuery.js'; 14 | -------------------------------------------------------------------------------- /playground/src/lib/demo/LinkFeaturesView.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | Link Features 12 | 13 | 14 | 15 | Drop-in replacement of anchor tags 16 | Can set state 17 | Active state based on route key 18 | Replace or push URL's 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/kernel/isConformantState.ts: -------------------------------------------------------------------------------- 1 | import type { State } from '../types.js'; 2 | 3 | /** 4 | * Tests the given state data to see if it conforms to the expected `State` structure. 5 | * 6 | * Use this while in full mode and handling the `beforeNavigate` event, or in code that has to do with directly pushing 7 | * state to the window's History API. 8 | * @param state State data to test. 9 | * @returns `true` if the state conforms to the expected `State` structure, or `false` otherwise. 10 | */ 11 | export function isConformantState(state: unknown): state is State { 12 | return ( 13 | typeof state === 'object' && 14 | state !== null && 15 | 'hash' in state && 16 | typeof state.hash === 'object' && 17 | state.hash !== null && 18 | !Array.isArray(state.hash) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /playground/src/lib/demo/ApiFeaturesView.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | API 11 | 12 | 13 | 14 | init(): Initializes the router library. 15 | 16 | 17 | getRouterContext(): Retrieves the closest router ih the component 18 | hierarchy. 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | npm-tag: 7 | description: 'NPM tag to publish the package (e.g., latest, beta, etc.)' 8 | required: false 9 | default: 'latest' 10 | type: string 11 | dry-run: 12 | description: 'Perform a dry run (true/false)' 13 | required: false 14 | default: false 15 | type: boolean 16 | 17 | permissions: 18 | id-token: write 19 | contents: read 20 | 21 | jobs: 22 | publish: 23 | uses: WJSoftware/cicd/.github/workflows/npm-publish.yml@main 24 | with: 25 | node-version: 24 26 | build-script: build 27 | test-script: test 28 | npm-tag: ${{ inputs.npm-tag || 'latest' }} 29 | dry-run: ${{ inputs.dry-run || false }} 30 | secrets: inherit 31 | -------------------------------------------------------------------------------- /playground/src/lib/ThemeSwitch.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /playground/src/lib/CardBody.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    15 | {@render children?.()} 16 |
    17 | 18 | 31 | -------------------------------------------------------------------------------- /playground/src/lib/state/title.svelte.ts: -------------------------------------------------------------------------------- 1 | import { getContext, setContext } from 'svelte'; 2 | 3 | const defaultTitle = 'Svelte Router: Live Demo'; 4 | 5 | export class TitleState { 6 | #current = $state(defaultTitle); 7 | 8 | constructor() { 9 | $effect(() => { 10 | document.title = this.current ? `${this.current} - ${defaultTitle}` : defaultTitle; 11 | }); 12 | } 13 | 14 | get current() { 15 | return this.#current; 16 | } 17 | 18 | set current(v: string) { 19 | this.#current = v; 20 | } 21 | } 22 | 23 | const titleCtxKey = Symbol(); 24 | 25 | let titleState: TitleState; 26 | 27 | export function initTitleContext() { 28 | titleState = setContext(titleCtxKey, new TitleState()); 29 | } 30 | 31 | export function getTitleContext() { 32 | return getContext(titleCtxKey); 33 | } 34 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/ShowCodeButton.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 | 13 |
    14 | ℹ️ This button uses 15 | shallow routing to open the code dialog. You can close or re-open the dialog window using the browser's back/forward 20 | buttons. 21 |
    22 |
    23 | -------------------------------------------------------------------------------- /playground/src/lib/demo/RouterEngineFeaturesView.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | RouterEngine Features 12 | 13 | 14 | 15 | 16 | Class that powers the Router component 17 | 18 | Allows programatic route definition and evaluation 19 | Can be used to create custom routing solutions 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /playground/src/lib/views/ViewSkeleton.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | {@render children?.()} 18 | 19 |
    20 | 26 |
    27 | -------------------------------------------------------------------------------- /playground/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap color variants used throughout the demo application 3 | */ 4 | export type BootstrapColor = 5 | | 'primary' 6 | | 'secondary' 7 | | 'success' 8 | | 'danger' 9 | | 'warning' 10 | | 'info' 11 | | 'light' 12 | | 'dark'; 13 | 14 | /** 15 | * Bootstrap text color variants 16 | * Includes standard colors plus additional text-specific variants 17 | */ 18 | export type BootstrapTextColor = 19 | | BootstrapColor 20 | | 'white' 21 | | 'muted' 22 | | 'black-50' 23 | | 'white-50' 24 | | 'body' 25 | | 'black'; 26 | 27 | /** 28 | * Bootstrap background color variants 29 | * Includes standard colors plus transparent 30 | */ 31 | export type BootstrapBackgroundColor = BootstrapColor | 'transparent'; 32 | 33 | /** 34 | * Bootstrap size variants used for spacing, buttons, etc. 35 | */ 36 | export type BootstrapSize = 'sm' | 'md' | 'lg' | 'xl'; 37 | -------------------------------------------------------------------------------- /playground/src/lib/demo/RouterFeaturesView.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | Router Features 12 | 13 | 14 | 15 | Multiple route matches 16 | 17 | Non-root serving via Router.basePath 18 | 19 | Nesting routers 20 | 21 | Fallback content via the Fallback component 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | extensions: ['.svelte', '.md'], 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter({ 15 | fallback: 'index.html' 16 | }), 17 | alias: { 18 | $test: 'src/lib/testing' 19 | }, 20 | files: { 21 | appTemplate: 'src/index.html' 22 | } 23 | } 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 9 | 10 | export default ts.config( 11 | includeIgnoreFile(gitignorePath), 12 | js.configs.recommended, 13 | ...ts.configs.recommended, 14 | ...svelte.configs['flat/recommended'], 15 | prettier, 16 | ...svelte.configs['flat/prettier'], 17 | { 18 | languageOptions: { 19 | globals: { 20 | ...globals.browser, 21 | ...globals.node 22 | } 23 | } 24 | }, 25 | { 26 | files: ['**/*.svelte'], 27 | 28 | languageOptions: { 29 | parserOptions: { 30 | parser: ts.parser 31 | } 32 | } 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /playground/src/lib/views/hash-routing/code-samples/live-hash-display.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
    23 |
    24 | Current hash: {currentHash} 25 |
    26 | 27 | {#each Object.entries(routes) as [key, value]} 28 |
    29 | {key}: 30 | {value || '(empty)'} 31 |
    32 | {/each} 33 |
    34 | -------------------------------------------------------------------------------- /playground/src/lib/DemoBc.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | Home 17 | Path Routing 21 | Hash Routing 25 | 26 | 27 | 33 | -------------------------------------------------------------------------------- /src/lib/kernel/calculateMultiHashFragment.ts: -------------------------------------------------------------------------------- 1 | import { location } from './Location.js'; 2 | 3 | /** 4 | * Calculates a new hash fragment with the specified named hash paths while preserving any existing hash paths not 5 | * specified. Paths set to empty string ("") will be completely removed from the hash fragment. 6 | * @param hashPaths The hash paths to include (or remove via empty strings) in the final HREF. 7 | * @returns The calculated hash fragment (without the leading `#`). 8 | */ 9 | export function calculateMultiHashFragment(hashPaths: Record) { 10 | const existingIds = new Set(); 11 | let finalUrl = ''; 12 | for (let [id, path] of Object.entries(location.hashPaths)) { 13 | existingIds.add(id); 14 | path = hashPaths[id] ?? path; 15 | if (path) { 16 | finalUrl += `;${id}=${path}`; 17 | } 18 | } 19 | for (let [hashId, newPath] of Object.entries(hashPaths)) { 20 | if (existingIds.has(hashId) || !newPath) { 21 | continue; 22 | } 23 | finalUrl += `;${hashId}=${newPath}`; 24 | } 25 | return finalUrl.substring(1); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 José Pablo Ramírez Vargas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/kernel/Location.ts: -------------------------------------------------------------------------------- 1 | import type { Location } from '../types.js'; 2 | 3 | /** 4 | * Internal symbol for accessing complete state from Location implementations. 5 | * This provides access to the full state object for internal library operations 6 | * without expanding the public API surface. 7 | */ 8 | export const getCompleteStateKey = Symbol('getCompleteState'); 9 | 10 | /** 11 | * Global location object. Use it to monitor or reactively react to URL or state changes. It also provides the 12 | * `navigate` method to programmatically navigate to a new URL, which allows setting the state as well. 13 | * 14 | * **IMPORTANT**: This object is only available after the application has been initialized with `init`. 15 | */ 16 | export let location: Location; 17 | 18 | export function setLocation(newLocation: Location | null) { 19 | if (newLocation && location) { 20 | throw new Error( 21 | 'Cannot override the current location object. Clean the existing location first.' 22 | ); 23 | } 24 | // @ts-expect-error Purposely not typed as nullable for simplicity of use. 25 | return (location = newLocation); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/kernel/LocationFull.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BeforeNavigateEvent, 3 | NavigationCancelledEvent, 4 | FullModeHistoryApi, 5 | Events 6 | } from '../types.js'; 7 | import { LocationLite } from './LocationLite.svelte.js'; 8 | import { InterceptedHistoryApi } from './InterceptedHistoryApi.svelte.js'; 9 | 10 | /** 11 | * Location implementation of the library's full mode feature. 12 | * Replaces window.history with an InterceptedHistoryApi to capture all navigation events. 13 | */ 14 | export class LocationFull extends LocationLite { 15 | #historyApi: FullModeHistoryApi; 16 | 17 | constructor(historyApi?: FullModeHistoryApi) { 18 | const api = historyApi ?? new InterceptedHistoryApi(); 19 | super(api); 20 | this.#historyApi = api; 21 | } 22 | 23 | on(event: 'beforeNavigate', callback: (event: BeforeNavigateEvent) => void): () => void; 24 | on( 25 | event: 'navigationCancelled', 26 | callback: (event: NavigationCancelledEvent) => void 27 | ): () => void; 28 | on(event: Events, callback: Function): () => void { 29 | return this.#historyApi.on(event as any, callback as any); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /playground/src/lib/demo/RouteFeaturesView.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | Route Features 12 | 13 | 14 | 15 | Exact path matching 16 | Route parameters 17 | Rest parameter 18 | Optional parameters 19 | Path specification via regular expression or text 20 | Additional matching predicate function 21 | Optional path to control matching entirely via other conditions 24 | Share route keys to display disconnected UI pieces 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /playground/src/lib/state/theme.svelte.ts: -------------------------------------------------------------------------------- 1 | const themeOptions = ['system', 'light', 'dark'] as const; 2 | 3 | type ThemeIndex = 0 | 1 | 2; 4 | 5 | function indexOfTheme(theme: 'system' | 'light' | 'dark'): ThemeIndex { 6 | const index = themeOptions.indexOf(theme); 7 | if (index === -1) { 8 | throw new Error(`Invalid theme: ${theme}`); 9 | } 10 | return index as ThemeIndex; 11 | } 12 | 13 | export class ThemeState { 14 | #current: ThemeIndex = $state(0); 15 | 16 | get current() { 17 | return themeOptions[this.#current]; 18 | } 19 | set current(value: 'light' | 'dark' | 'system') { 20 | this.#current = indexOfTheme(value); 21 | this.#updateTheme(); 22 | } 23 | 24 | #updateTheme() { 25 | if (this.#current === 0) { 26 | document.documentElement.removeAttribute('data-bs-theme'); 27 | return; 28 | } 29 | document.documentElement.setAttribute('data-bs-theme', themeOptions[this.#current]); 30 | } 31 | 32 | nextTheme() { 33 | this.#current = ((this.#current + 1) % themeOptions.length) as ThemeIndex; 34 | this.#updateTheme(); 35 | } 36 | } 37 | 38 | export default new ThemeState(); 39 | -------------------------------------------------------------------------------- /src/lib/kernel/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | 3 | describe('index', () => { 4 | test('Should export exactly the expected objects.', async () => { 5 | // Arrange. 6 | const expectedList = [ 7 | 'location', 8 | 'RouterEngine', 9 | 'isConformantState', 10 | 'calculateHref', 11 | 'calculateState', 12 | 'initCore', 13 | 'LocationState', 14 | 'StockHistoryApi', 15 | 'InterceptedHistoryApi', 16 | 'LocationLite', 17 | 'LocationFull', 18 | 'preserveQueryInUrl', 19 | 'calculateMultiHashFragment' 20 | ]; 21 | 22 | // Act. 23 | const lib = await import('./index.js'); 24 | 25 | // Assert. 26 | for (let item of expectedList) { 27 | expect(item in lib, `The expected object ${item} is not exported.`).toEqual(true); 28 | } 29 | for (let key of Object.keys(lib)) { 30 | expect( 31 | expectedList.includes(key), 32 | `The library exports object ${key}, which is not expected.` 33 | ).toEqual(true); 34 | } 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/lib/kernel/resolveHashValue.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect, beforeEach } from 'vitest'; 2 | import { routingOptions } from './options.js'; 3 | import { resolveHashValue } from './resolveHashValue.js'; 4 | import type { Hash } from '$lib/types.js'; 5 | 6 | describe('resolveHashValue', () => { 7 | beforeEach(() => { 8 | // Reset routingOptions to default before each test 9 | routingOptions.defaultHash = false; 10 | }); 11 | 12 | test('Should return the defaultHash routing option when hash is undefined.', () => { 13 | // Arrange 14 | const newDefaultHash = 'abc'; 15 | routingOptions.defaultHash = newDefaultHash; 16 | 17 | // Act 18 | const result = resolveHashValue(undefined); 19 | 20 | // Assert 21 | expect(result).toBe(newDefaultHash); 22 | }); 23 | test.each(['tp', false, true])( 24 | 'Should return the provided hash %s value when defined.', 25 | (hash) => { 26 | // Arrange 27 | routingOptions.defaultHash = !hash; 28 | 29 | // Act 30 | const result = resolveHashValue(hash); 31 | 32 | // Assert 33 | expect(result).toBe(hash); 34 | } 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /src/routes/api/kit/functions/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Functions 3 | description: API reference for functions in the SvelteKit extension for Svelte Router hash routing 4 | --- 5 | 6 | ## `init` 7 | 8 | `(options?: KitInitOptions): () => void` 9 | 10 | Initializes the library. This is a must-do operation before any other functionality can be used. 11 | 12 | ## `kitCalculateHref` 13 | 14 | `(options: KitCalculateHrefOptions, ...hrefs: string[]): string` 15 | 16 | Helper function that combines multiple HREF's into a single HREF using `@svelte-router/core`'s `calculateHref` function **for the path routing universe**. 17 | 18 | It is important to stress the importance of the highlighted phrase in the previous paragraph: This is a function that works like the stock `calculateHref()` function, but only produces URLs for the path routing universe. Its purpose is to assist in the creation of URLs that are coded into regular (or generally speaking, not in `Link` components) HTML anchor elements. 19 | 20 | There is no overload that doesn't take options. If no options are needed, it is because the function is most likely not needed. 21 | 22 | ### `KitCalculateHrefOptions` 23 | 24 | Refer to [CalculateHrefOptions](/api/core/functions#CalculateHrefOptions). All options except `hash` are valid options. 25 | -------------------------------------------------------------------------------- /src/lib/kernel/Location.test.ts: -------------------------------------------------------------------------------- 1 | import { location, setLocation } from './Location.js'; 2 | import { describe, test, expect, afterEach } from 'vitest'; 3 | import { LocationLite } from './LocationLite.svelte.js'; 4 | 5 | describe('Location', () => { 6 | afterEach(() => { 7 | location?.dispose(); 8 | setLocation(null); 9 | }); 10 | test('Should be initially undefined.', () => { 11 | // Assert. 12 | expect(location).toBeUndefined; 13 | }); 14 | test('Should reflect the updated value after being assigned.', () => { 15 | // Act. 16 | setLocation(new LocationLite()); 17 | 18 | // Assert. 19 | expect(location).toBeDefined(); 20 | }); 21 | }); 22 | 23 | describe('setLocation', () => { 24 | afterEach(() => { 25 | location?.dispose(); 26 | setLocation(null); 27 | }); 28 | test('Should throw an error when trying to override the current location object.', () => { 29 | // Arrange. 30 | setLocation(new LocationLite()); 31 | const secondLoc = new LocationLite(); 32 | 33 | // Act. 34 | const act = () => setLocation(secondLoc); 35 | 36 | // Assert. 37 | expect(act).toThrowError(); 38 | 39 | // Cleanup. 40 | secondLoc.dispose(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Cloudflare Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '.github/**' 9 | - 'docs/**' 10 | - 'playground/**' 11 | - 'src/lib/**' 12 | - '.gitignore' 13 | - '.npmrc' 14 | - '.prettierignore' 15 | - '.prettierrc' 16 | - 'add-component-help.ps1' 17 | - 'AGENTS.md' 18 | - 'eslint.config.js' 19 | - 'LICENSE' 20 | - 'package.json' 21 | - 'package-lock.json' 22 | - 'README.md' 23 | - 'tsconfig.json' 24 | workflow_dispatch: 25 | 26 | jobs: 27 | deploy: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: '24' 37 | cache: 'npm' 38 | 39 | - name: Install dependencies 40 | run: npm ci 41 | 42 | - name: Build 43 | run: npm run build 44 | 45 | - name: Deploy to Cloudflare Pages 46 | uses: cloudflare/wrangler-action@v3 47 | with: 48 | apiToken: ${{ secrets.CF_API_TOKEN }} 49 | accountId: ${{ secrets.CF_ACC_ID }} 50 | command: pages deploy build --project-name=svelte-router 51 | -------------------------------------------------------------------------------- /src/lib/kernel/dissectHrefs.ts: -------------------------------------------------------------------------------- 1 | const hrefRegex = /^([^#?]*)?(?:\?([^#]*))?(?:#(.*))?$/; 2 | 3 | /** 4 | * Dissects the given hrefs into its parts, namely the paths, hashes, and search parameters. 5 | * 6 | * Hrefs are expected to be in the form of `path?search#hash`. Hrefs that are falsy will produce empty strings for all 7 | * parts. 8 | * 9 | * The index of the parts in the returned arrays correspond to the index of the hrefs in the given array. 10 | * @param hrefs The hrefs to parse and dissect into its parts. 11 | * @returns A record containing the paths, hashes, and search parameters of the hrefs. 12 | */ 13 | export function dissectHrefs( 14 | ...hrefs: (string | undefined)[] 15 | ): Record<'paths' | 'hashes' | 'searchParams', string[]> { 16 | const paths: string[] = []; 17 | const hashes: string[] = []; 18 | const searchParams: string[] = []; 19 | for (let i = 0; i < hrefs.length; ++i) { 20 | if (!hrefs[i]) { 21 | paths.push(''); 22 | searchParams.push(''); 23 | hashes.push(''); 24 | continue; 25 | } 26 | const match = hrefs[i]!.match(hrefRegex); 27 | paths.push(match![1] || ''); 28 | searchParams.push(match![2] || ''); 29 | hashes.push(match![3] || ''); 30 | } 31 | return { 32 | paths, 33 | searchParams, 34 | hashes 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/Fallback/README.md: -------------------------------------------------------------------------------- 1 | # Fallback 2 | 3 | The `Fallback` component can be thought about as a `Route` component that only render its children if there are no 4 | other routes in the parent router engine that match. 5 | 6 | Internally, it checks the parent router engine's `fallback` value, which is a reactive value calculated when all other 7 | route status data is calculated. 8 | 9 | ## Props 10 | 11 | | Property | Type | Default Value | Bindable | Description | 12 | | ---------- | ---------------------------------- | ------------- | -------- | ------------------------------------------------------------------------------------------ | 13 | | `hash` | `Hash` | `undefined` | | Sets the hash mode of the component. | 14 | | `when` | `WhenPredicate` | `undefined` | | Overrides the default activation conditions for the fallback content inside the component. | 15 | | `children` | `Snippet<[RouterChildrenContext]>` | `undefined` | | Renders the children of the component. | 16 | 17 | [Online Documentation](https://svelte-router.dev/api/core/fallback) 18 | 19 | ## Examples 20 | 21 | See the examples for the `Router` component. 22 | -------------------------------------------------------------------------------- /playground/src/lib/demo/LocationFeaturesView.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | Location Features 12 | 13 | 14 | 15 | Reactive current URL 16 | Reactive query parameters 17 | Reactive hash 18 | Reactive current state 19 | Programatic navigation 20 | 21 |

    Full Mode

    22 | 23 | 24 | Cancellable beforeNavigate event 25 | 26 | 27 | navigationCancelled event 28 | 29 | History API interception 30 | 31 | Suitable when mixing routers from different libraries (micro-frontends) 32 | 33 | 34 |
    35 |
    36 | -------------------------------------------------------------------------------- /src/lib/init.ts: -------------------------------------------------------------------------------- 1 | import { LocationFull } from './kernel/LocationFull.js'; 2 | import { LocationLite } from './kernel/LocationLite.svelte.js'; 3 | import type { InitOptions } from './types.js'; 4 | import { initCore } from './kernel/initCore.js'; 5 | 6 | /** 7 | * Initializes the routing library in normal mode. The following features are available: 8 | * 9 | * - URL and state tracking 10 | * - Navigation 11 | * - Event handling of the `popstate` and `hashchange` events 12 | * - Routers 13 | * - Routes 14 | * - Links 15 | * - Fallbacks 16 | * - Link contexts 17 | * 18 | * Use `initFull()` to enable the following features: 19 | * - Raising the `beforeNavigate` and `navigationCancelled` events 20 | * - Intercepting navigation from other libraries or routers 21 | * 22 | * @returns A cleanup function that reverts the initialization process. 23 | */ 24 | export function init(options?: InitOptions) { 25 | return initCore(new LocationLite(), options); 26 | } 27 | 28 | /** 29 | * Initializes the routing library in full mode. All features of normal mode are available, plus the following: 30 | * 31 | * - Raising the `beforeNavigate` and `navigationCancelled` events 32 | * - Intercepting navigation from other libraries or routers 33 | * 34 | * @returns A cleanup function that reverts the initialization process. 35 | */ 36 | export function initFull(options?: InitOptions) { 37 | return initCore(new LocationFull(), options); 38 | } 39 | -------------------------------------------------------------------------------- /playground/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/src/lib/views/home/Quickstart.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | Quickstart 17 | 18 |

    Install the package:

    19 | 20 |

    Then, in your Svelte app:

    21 | 25 | import { Router, Route } from '@svelte-router/core'; 26 | import Hero from './lib/Hero.svelte'; 27 | import Features from './lib/Features.svelte'; 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | `} 38 | title="Basic Usage" 39 | /> 40 |
    41 |
    42 | -------------------------------------------------------------------------------- /src/routes/api/core/linkcontext/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: LinkContext Component 3 | description: API reference for LinkContext component that provides default properties for nested Link components 4 | --- 5 | 6 | :::info[Parent Requirement] 7 | None. 8 | ::: 9 | 10 | `Link` components have certain properties that affect how they calculate the URL that is pushed to the browser’s history. If the default value for one of these properties does not fit the need of the application, then every `Link` component will have to explicitly specify it. 11 | 12 | While you might create `Link` components inside an `{#each}` block using an array that defines the link data, it might also happen that you specify each link separately. This will force you to repeat over and over the same property or properties on every link. This is not very maintainable and is prone to error. In cases like this, use a `LinkContext` component to set the properties only once. 13 | 14 | ## How To Use 15 | 16 | Refer to the [Navigating with Components](/docs/navigating-with-components) document for a detailed explanation on how to use the `Link` and `LinkContext` components. 17 | 18 | ## Properties 19 | 20 | The following is the list of properties this component supports: 21 | 22 | - `replace` 23 | - `prependBasePath` 24 | - `preserveQuery` 25 | - `activeState` 26 | - `children` 27 | 28 | These properties propagate their values to any child Link components rendered inside its children snippet and they work as described in the [Link Component](/api/core/link) document. 29 | -------------------------------------------------------------------------------- /src/lib/kernel/initCore.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendedInitOptions, Location } from '../types.js'; 2 | import { setLocation } from './Location.js'; 3 | import { resetLogger, setLogger } from './Logger.js'; 4 | import { resetRoutingOptions, setRoutingOptions } from './options.js'; 5 | import { resetTraceOptions, setTraceOptions } from './trace.svelte.js'; 6 | 7 | /** 8 | * Core initialization function used by both the main package and extension packages. 9 | * This ensures consistent initialization logic across different environments. 10 | * 11 | * Extension packages must use this function to provide their own Location implementations 12 | * (e.g., SvelteKit-compatible implementations) while maintaining consistent setup. 13 | * 14 | * @param options Initialization options for the routing library 15 | * @param location The Location implementation to use 16 | * @returns A cleanup function that reverts the initialization process 17 | */ 18 | export function initCore(location: Location, options?: ExtendedInitOptions) { 19 | if (!location) { 20 | throw new Error( 21 | 'A valid location object must be provided to initialize the routing library.' 22 | ); 23 | } 24 | setTraceOptions(options?.trace); 25 | setLogger(options?.logger ?? true); 26 | setRoutingOptions(options); 27 | const newLocation = setLocation(location); 28 | return () => { 29 | newLocation?.dispose(); 30 | setLocation(null); 31 | resetRoutingOptions(); 32 | resetLogger(); 33 | resetTraceOptions(); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /playground/src/lib/Card.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
    23 | {@render children?.()} 24 |
    25 | 26 | 42 | 43 | 55 | -------------------------------------------------------------------------------- /playground/src/lib/CardHeader.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | {@render children?.()} 33 | 34 | 35 | 52 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 43 | 44 | -------------------------------------------------------------------------------- /playground/src/lib/Badge.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | {@render children?.()} 38 | 39 | 40 | 54 | -------------------------------------------------------------------------------- /playground/src/lib/views/redirected/RedirectedView.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    14 | {#if redirected} 15 | 16 | You have been redirected here from a deprecated path. Please update your bookmarks! 17 | 18 | {:else} 19 | 20 | Note how the warning alert is not shown when navigating directly to this URL. This is 21 | because the piece of state indicating a redirection is only set when arriving here via 22 | the redirection defined in this demo's 23 | App.svelte component. 24 | 25 | {/if} 26 |

    Feature With New URL Goes Here

    27 |

    28 | This page is accessible via its new path (/new-path), but can also be reached 29 | by navigating to the old deprecated path (/deprecated-path), which will 30 | redirect to the new URL. 31 |

    32 |

    33 | This demonstrates how to set up URL redirections using the Redirector class 34 | from the @svelte-router/core NPM package. 35 |

    36 |
    37 | -------------------------------------------------------------------------------- /src/lib/testing/TestRouteWithRouter.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | 43 | {#snippet children({ rp, state, rs })} 44 | {#if routeChildren} 45 | {@render routeChildren(rp, state, rs)} 46 | {:else} 47 |
    48 | Route Content - Key: {routeKey} 49 |
    50 | {/if} 51 | {/snippet} 52 |
    53 | {#if children} 54 | {@render children()} 55 | {/if} 56 |
    57 | -------------------------------------------------------------------------------- /src/lib/init.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect, afterEach } from 'vitest'; 2 | import { init, initFull } from './init.js'; 3 | import { location } from './kernel/Location.js'; 4 | import { LocationLite } from './kernel/LocationLite.svelte.js'; 5 | import { LocationFull } from './kernel/LocationFull.js'; 6 | 7 | let cleanup: (() => void) | undefined; 8 | 9 | [ 10 | { 11 | fn: init, 12 | locationClass: LocationLite 13 | }, 14 | { 15 | fn: initFull, 16 | locationClass: LocationFull 17 | } 18 | ].forEach((fnInfo) => { 19 | describe(fnInfo.fn.name, () => { 20 | afterEach(() => { 21 | cleanup?.(); 22 | }); 23 | 24 | test(`Should initialize the global location object to an instance of the ${fnInfo.locationClass.name} class.`, () => { 25 | // Act. 26 | cleanup = fnInfo.fn(); 27 | 28 | // Assert. 29 | expect(location).toBeDefined(); 30 | expect(location).toBeInstanceOf(fnInfo.locationClass); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('init + initFull', () => { 36 | afterEach(() => { 37 | cleanup?.(); 38 | cleanup = undefined; 39 | }); 40 | test.each([ 41 | { 42 | fn1: init, 43 | fn2: initFull 44 | }, 45 | { 46 | fn1: initFull, 47 | fn2: init 48 | } 49 | ])( 50 | 'Should throw an error when calling $fn2.name without prior cleaning of a call to $fn1.name .', 51 | ({ fn1, fn2 }) => { 52 | // Arrange. 53 | cleanup = fn1(); 54 | 55 | // Act. 56 | const act = () => fn2(); 57 | 58 | // Assert. 59 | expect(act).toThrow(); 60 | } 61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /src/routes/docs/existing-extension-packages/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Existing Extension Packages 3 | description: Discover available extensions for Svelte Router including SvelteKit integration and memory routing 4 | --- 5 | 6 | The following table lists the known extension libraries in existence. If you know one or have created one and would like to have it listed here, please open an issue in GitHub with the necessary information to create what you see in the table. 7 | 8 | Thank you! 9 | 10 | | Name | Known Related URL’s | Description | 11 | | -------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 12 | | `@svelte-router/kit` | [Repository](https://github.com/WJSoftware/svelte-router-kit) [Documentation](/docs/sveltekit-support) | **AVAILABLE NOW!** The official extension package for Sveltekit projects. It enables hash routing while leaving path routing to **Sveltekit**’s routing system. It can do single and multi-hash routing. | 13 | | `@svelte-router/mem` | [Upvote issue if interested](https://github.com/WJSoftware/svelte-router/issues/7) | **COMING …?** The official extension package that only routes using an in-memory location URL. The environment’s URL never changes. | 14 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './init.js'; 2 | export * from './Link/Link.svelte'; 3 | export { default as Link } from './Link/Link.svelte'; 4 | export { default as LinkContext } from './LinkContext/LinkContext.svelte'; 5 | export * from './Route/Route.svelte'; 6 | export { default as Route } from './Route/Route.svelte'; 7 | export { getRouterContext, setRouterContext } from './Router/Router.svelte'; 8 | export { default as Router } from './Router/Router.svelte'; 9 | export * from './Fallback/Fallback.svelte'; 10 | export { default as Fallback } from './Fallback/Fallback.svelte'; 11 | export type * from './types.js'; 12 | export { location } from './kernel/Location.js'; 13 | export * from './RouterTrace/RouterTrace.svelte'; 14 | export { default as RouterTrace } from './RouterTrace/RouterTrace.svelte'; 15 | export * from './public-utils.js'; 16 | export * from './behaviors/active.svelte.js'; 17 | export { Redirector } from './Redirector.svelte.js'; 18 | export { buildHref } from './buildHref.js'; 19 | export { RouterEngine } from './kernel/RouterEngine.svelte.js'; 20 | export { isConformantState } from './kernel/isConformantState.js'; 21 | export { calculateHref } from './kernel/calculateHref.js'; 22 | export { calculateMultiHashFragment } from './kernel/calculateMultiHashFragment.js'; 23 | export { calculateState } from './kernel/calculateState.js'; 24 | export { initCore } from './kernel/initCore.js'; 25 | export { LocationState } from './kernel/LocationState.svelte.js'; 26 | export { StockHistoryApi } from './kernel/StockHistoryApi.svelte.js'; 27 | export { InterceptedHistoryApi } from './kernel/InterceptedHistoryApi.svelte.js'; 28 | export { LocationLite } from './kernel/LocationLite.svelte.js'; 29 | export { LocationFull } from './kernel/LocationFull.js'; 30 | export { preserveQueryInUrl } from './kernel/preserveQuery.js'; 31 | -------------------------------------------------------------------------------- /src/routes/api/+layout.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | {@html techArticle} 52 | 53 | 54 | {@render children()} 55 | -------------------------------------------------------------------------------- /src/routes/docs/+layout.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | {@html techArticle} 52 | 53 | 54 | {@render children()} 55 | -------------------------------------------------------------------------------- /patches/@sveltepress+theme-default+7.0.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@sveltepress/theme-default/dist/components/ToggleDark.svelte b/node_modules/@sveltepress/theme-default/dist/components/ToggleDark.svelte 2 | index b640036..b395dad 100644 3 | --- a/node_modules/@sveltepress/theme-default/dist/components/ToggleDark.svelte 4 | +++ b/node_modules/@sveltepress/theme-default/dist/components/ToggleDark.svelte 5 | @@ -17,14 +17,14 @@ 6 | if (themeColor) { 7 | document 8 | .getElementById('theme-color') 9 | - .setAttribute('content', themeColor.dark) 10 | + ?.setAttribute('content', themeColor.dark) 11 | } 12 | } else { 13 | document.querySelector('html').classList.remove('dark') 14 | if (themeColor) { 15 | document 16 | .getElementById('theme-color') 17 | - .setAttribute('content', themeColor.light) 18 | + ?.setAttribute('content', themeColor.light) 19 | } 20 | } 21 | } 22 | @@ -149,11 +149,11 @@ 23 | const storedMode = window.localStorage.getItem('${key}') 24 | if (storedMode === 'dark' || (storedMode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { 25 | document.querySelector('html').classList.add('dark') 26 | - document.getElementById('theme-color').setAttribute('content', themeColor ? themeColor.dark : '#ffffff') 27 | + document.getElementById('theme-color')?.setAttribute('content', themeColor ? themeColor.dark : '#ffffff') 28 | } 29 | else { 30 | document.querySelector('html').classList.remove('dark') 31 | - document.getElementById('theme-color').setAttribute('content', themeColor ? themeColor.light : '#ffffff') 32 | + document.getElementById('theme-color')?.setAttribute('content', themeColor ? themeColor.light : '#ffffff') 33 | } 34 | `} 35 | 36 | -------------------------------------------------------------------------------- /src/lib/public-utils.ts: -------------------------------------------------------------------------------- 1 | import { RouterEngine } from './kernel/RouterEngine.svelte.js'; 2 | import type { RouteStatus } from './types.js'; 3 | import { noTrailingSlash } from './utils.js'; 4 | 5 | /** 6 | * Checks if a specific route is active according to the provided router engine or route status record. 7 | * 8 | * **Note:** `false` is also returned if no router engine is provided or if no route key is specified. 9 | * @param rsOrRouter A router engine or a router engine's route status record. 10 | * @param key The route key to check for activity. 11 | * @returns `true` if the specified route is active; otherwise, `false`. 12 | */ 13 | export function isRouteActive( 14 | rsOrRouter: RouterEngine | Record | null | undefined, 15 | key: string | null | undefined 16 | ): boolean { 17 | const rs = rsOrRouter instanceof RouterEngine ? rsOrRouter.routeStatus : rsOrRouter; 18 | return !!rs?.[key ?? '']?.match; 19 | } 20 | 21 | function hasLeadingSlash(paths: (string | undefined)[]) { 22 | for (let path of paths) { 23 | if (!path) { 24 | continue; 25 | } 26 | return path.startsWith('/'); 27 | } 28 | return false; 29 | } 30 | 31 | /** 32 | * Joins the provided paths into a single path. 33 | * @param paths Paths to join. 34 | * @returns The joined path. 35 | */ 36 | export function joinPaths(...paths: string[]) { 37 | const result = paths.reduce( 38 | (acc, path, index) => { 39 | const trimmedPath = (path ?? '').replace(/^\/|\/$/g, ''); 40 | return ( 41 | acc + 42 | (index > 0 && !acc.endsWith('/') && trimmedPath.length > 0 ? '/' : '') + 43 | trimmedPath 44 | ); 45 | }, 46 | hasLeadingSlash(paths) ? '/' : '' 47 | ); 48 | return noTrailingSlash(result); 49 | } 50 | -------------------------------------------------------------------------------- /playground/src/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/buildHref.ts: -------------------------------------------------------------------------------- 1 | import type { BuildHrefOptions } from '$lib/types.js'; 2 | import { location } from './kernel/Location.js'; 3 | import { mergeQueryParams } from './kernel/preserveQuery.js'; 4 | 5 | /** 6 | * Builds a new HREF by combining the path piece from one HREF and the hash piece from another. 7 | * 8 | * Any query parameters present in either piece are merged and included in the resulting HREF. Furthermore, if the 9 | * `preserveQuery` option is provided, additional query parameters from the current URL are also merged in. 10 | * 11 | * ### When to Use 12 | * 13 | * This is a helper function that came to be when the redirection feature was added to the library. The specific use 14 | * case is cross-routing-universe redirections, where the "source" universe's path is not changed by normal redirection 15 | * because "normal" **cross-universe redirections** don't alter other universes' paths. 16 | * 17 | * This function, in conjunction with the `calculateHref` function, allows relatively easy construction of the desired 18 | * final HREF by combining the results of 2 `calculateHref` calls: One to get the path piece from the source universe, 19 | * and another to get the hash piece for the other universe. 20 | * @param pathPiece HREF value containing the desired path piece. 21 | * @param hashPiece HREF value containing the desired hash piece. 22 | * @param options Optional set of options. 23 | * @returns The built HREF using the provided pieces. 24 | */ 25 | export function buildHref( 26 | pathPiece: string, 27 | hashPiece: string, 28 | options?: BuildHrefOptions 29 | ): string { 30 | const pp = new URL(pathPiece, location.url); 31 | const hp = new URL(hashPiece, location.url); 32 | let sp = mergeQueryParams( 33 | mergeQueryParams(pp.searchParams, hp.searchParams), 34 | options?.preserveQuery 35 | ); 36 | return `${pp.pathname}${sp?.size ? `?${sp.toString()}` : ''}${hp.hash}`; 37 | } 38 | -------------------------------------------------------------------------------- /src/routes/docs/per-routing-mode-data/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Per-Routing Mode Data 3 | description: Learn how Svelte Router manipulates global data to segregate and protect it between the various routing universes 4 | --- 5 | 6 | The concept of multiple parallel universes is convenient to explain how simultaneous and independent routing occurs within the `@svelte-router/core` routing library. However, something critical hasn’t been brought to light: Which data is associated to each universe? 7 | 8 | ## Hash Fragment 9 | 10 | The obvious one: The URL’s hash fragment is handled in a special way as to ensure that `location.navigate()` or the `Link` component’s click action don’t destroy paths that are not meant to be destroyed. Therefore, the value of the environment’s URL’s fragment (a. k. a. the hash) is one piece of data that is carefully split among routing universes. 11 | 12 | ## State Data 13 | 14 | This is the trickiest one: The state data set in the window’s **History API**. This library takes measures to ensure that the saved state data complies with the following data type: 15 | 16 | ```typescript 17 | export type State = { 18 | /** 19 | * Holds the state data associated to path routing. 20 | */ 21 | path: any; 22 | /** 23 | * Holds the state data associated to hash routing. 24 | * 25 | * For single (or traditional) hash routing, the value is stored using the `single` key. For multi-hash routing, 26 | * the value is stored using the hash identifier as the key. 27 | */ 28 | hash: Record; 29 | }; 30 | ``` 31 | 32 | This is the actual type definition exported by the library. If you must meddle with state data outside the confines of this library, always make sure you respect this data structure. 33 | 34 | Just as with the hash data, `location.navigate()` and the `Link` component will respect this data structure automatically, and you don’t have to specify any part of this data structure when using either. 35 | -------------------------------------------------------------------------------- /src/lib/kernel/LocationState.svelte.ts: -------------------------------------------------------------------------------- 1 | import { SvelteURL } from 'svelte/reactivity'; 2 | import { isConformantState } from './isConformantState.js'; 3 | import { logger } from './Logger.js'; 4 | import type { State } from '../types.js'; 5 | 6 | /** 7 | * Helper class used to manage the reactive data of Location implementations. 8 | * This class can serve as a base class for HistoryApi implementations. 9 | */ 10 | export class LocationState { 11 | url; 12 | state; 13 | 14 | constructor(initialUrl?: string, initialState?: State) { 15 | // Initialize URL 16 | this.url = new SvelteURL( 17 | initialUrl ?? globalThis.window?.location?.href ?? 'http://localhost/' 18 | ); 19 | 20 | // Initialize state using normalization 21 | const historyState = initialState ?? globalThis.window?.history?.state; 22 | this.state = $state(this.normalizeState(historyState)); 23 | } 24 | 25 | /** 26 | * Normalizes state data to ensure it conforms to the expected State interface. 27 | * 28 | * **NOTE**: In order to avoid serialization errors of the provided data, which might contain reactive Svelte 29 | * proxies, the returned data is a clean snapshot of the normalized data. 30 | * 31 | * @param state The state to normalize 32 | * @param defaultState Optional default state to use if normalization is needed 33 | * @returns Normalized state that conforms to the State interface 34 | */ 35 | normalizeState(state: any, defaultState?: State): State { 36 | const validState = isConformantState(state); 37 | 38 | if (!validState && state != null) { 39 | const action = defaultState ? 'Using known valid state.' : 'Resetting to clean state.'; 40 | logger.warn(`Non-conformant state data detected. ${action}`); 41 | } 42 | 43 | return $state.snapshot( 44 | validState ? state : (defaultState ?? { path: undefined, hash: {} }) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/kernel/calculateState.ts: -------------------------------------------------------------------------------- 1 | import type { Hash, Location, State } from '../types.js'; 2 | import { location } from './Location.js'; 3 | import { getCompleteStateKey } from './Location.js'; 4 | import { resolveHashValue } from './resolveHashValue.js'; 5 | 6 | type LocationInternal = Location & { [getCompleteStateKey]: () => State }; 7 | 8 | /** 9 | * Calculates the complete state object that should be set in the History API, setting the given state as the state of 10 | * the implicit routing universe, making sure that all states for all other routing universes are preserved. 11 | * @param state The desired state for the given hash. 12 | */ 13 | export function calculateState(state: any): State; 14 | /** 15 | * Calculates the state object that should be set for a given hash value, making sure that all states for all other 16 | * routing universes are preserved. 17 | * @param hash The hash value associated with the state. 18 | * @param state The desired state for the given hash. 19 | * @returns The state object that should be set, accounting for all routing universes. 20 | */ 21 | export function calculateState(hash: Hash | undefined, state: any): State; 22 | export function calculateState(hashOrState: any, state?: any): State { 23 | let hash: Hash; 24 | if (arguments.length === 1) { 25 | state = hashOrState; 26 | hash = resolveHashValue(undefined); 27 | } else { 28 | hash = resolveHashValue(hashOrState); 29 | } 30 | // Get a deep clone of the complete current state using the internal symbol method 31 | const newState = (location as LocationInternal)[getCompleteStateKey](); 32 | 33 | // Set the new state in the appropriate routing universe 34 | if (typeof hash === 'string') { 35 | newState.hash[hash] = state; 36 | } else if (hash) { 37 | newState.hash = { single: state }; 38 | } else { 39 | // For path routing, set the path state 40 | newState.path = state; 41 | } 42 | 43 | return newState; 44 | } 45 | -------------------------------------------------------------------------------- /playground/src/lib/SubNav.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 43 | 44 | 66 | -------------------------------------------------------------------------------- /src/routes/docs/electron-support/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Electron Support 3 | description: Learn the specifics of Svelte Router for Electron applications. 4 | --- 5 | 6 | The `@svelte-router/core` router works in **Electron**. Both path and hash routing works. However, path routing requires one extra step, which is very simple: 7 | 8 | ```typescript 9 | import { init, location } from '@svelte-router/core'; 10 | 11 | init(/* options */); 12 | location.goTo('/'); // <----- HERE. Just perform navigation. 13 | ``` 14 | 15 | By navigating immediately after initializing, path routing will work just fine. 16 | 17 | Again: **This is only needed for path routing mode**. Hash routing (_single_ or _multi_) works without this. 18 | 19 | ## Applications That Also Run in Browser 20 | 21 | If your application also runs in browser, condition this navigation trick to Electron only. One way of doing this is by simply checking if an Electron-only object exists in the global `window` object. 22 | 23 | For example, Electron applications commonly register a global electronAPI object via a `preload.js|ts|cjs|cts` script. If this is your case, you can simply do: 24 | 25 | ```typescript 26 | import { init, location } from '@svelte-router/core'; 27 | 28 | const isElectron = !!window.electronAPI; 29 | 30 | init(/* options */); 31 | if (isElectron) { 32 | location.goTo('/'); 33 | } 34 | ``` 35 | 36 | Now your code works on Electron by immediately navigating to your homepage, while also working in the browser without forcing the homepage to users. 37 | 38 | :::caution[Only Tested on Windows] 39 | I only have a Windows PC available and have therefore not actually tested Electron on MacOS or Linux. If you encounter issues in either of these operating systems, please open an issue in GitHub. 40 | 41 | **COMMUNITY HELP**: To remove this notice from this document and to document compatibility with other environments like **Tauri** or **Neutralino**, please visit [this GitHub discussion](https://github.com/WJSoftware/svelte-router/discussions/57) and provide the necessary information. 42 | ::: 43 | -------------------------------------------------------------------------------- /src/lib/kernel/options.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendedRoutingOptions } from '../types.js'; 2 | 3 | /** 4 | * Default routing options used for rollback. 5 | */ 6 | export const defaultRoutingOptions: Required = { 7 | hashMode: 'single', 8 | defaultHash: false, 9 | disallowPathRouting: false, 10 | disallowHashRouting: false, 11 | disallowMultiHashRouting: false 12 | }; 13 | 14 | /** 15 | * Global routing options. 16 | */ 17 | export const routingOptions: Required = 18 | structuredClone(defaultRoutingOptions); 19 | 20 | /** 21 | * Sets routing options, merging with current values. 22 | * This function is useful for extension packages that need to configure routing options. 23 | * 24 | * @param options Partial routing options to set 25 | */ 26 | export function setRoutingOptions(options?: Partial): void { 27 | routingOptions.hashMode = options?.hashMode ?? routingOptions.hashMode; 28 | routingOptions.defaultHash = options?.defaultHash ?? routingOptions.defaultHash; 29 | routingOptions.disallowPathRouting = 30 | options?.disallowPathRouting ?? routingOptions.disallowPathRouting; 31 | routingOptions.disallowHashRouting = 32 | options?.disallowHashRouting ?? routingOptions.disallowHashRouting; 33 | routingOptions.disallowMultiHashRouting = 34 | options?.disallowMultiHashRouting ?? routingOptions.disallowMultiHashRouting; 35 | if (routingOptions.hashMode === 'single' && typeof routingOptions.defaultHash === 'string') { 36 | throw new Error( 37 | "Using a named hash path as the default path can only be done when 'hashMode' is set to 'multi'." 38 | ); 39 | } else if (routingOptions.hashMode === 'multi' && routingOptions.defaultHash === true) { 40 | throw new Error( 41 | "Using classic hash routing as default can only be done when 'hashMode' is set to 'single'." 42 | ); 43 | } 44 | } 45 | 46 | /** 47 | * Resets routing options to their default values. 48 | */ 49 | export function resetRoutingOptions(): void { 50 | Object.assign(routingOptions, structuredClone(defaultRoutingOptions)); 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { routingOptions } from './kernel/options.js'; 3 | import { location } from './kernel/Location.js'; 4 | 5 | describe('index', () => { 6 | test('Should export exactly the expected objects.', async () => { 7 | // Arrange. 8 | const expectedList = [ 9 | 'Link', 10 | 'LinkContext', 11 | 'Route', 12 | 'Router', 13 | 'Fallback', 14 | 'location', 15 | 'RouterTrace', 16 | 'init', 17 | 'initFull', 18 | 'getRouterContext', 19 | 'setRouterContext', 20 | 'isRouteActive', 21 | 'activeBehavior', 22 | 'Redirector', 23 | 'buildHref', 24 | 'joinPaths', 25 | 'RouterEngine', 26 | 'isConformantState', 27 | 'calculateHref', 28 | 'calculateState', 29 | 'initCore', 30 | 'LocationState', 31 | 'StockHistoryApi', 32 | 'InterceptedHistoryApi', 33 | 'LocationLite', 34 | 'LocationFull', 35 | 'preserveQueryInUrl', 36 | 'calculateMultiHashFragment' 37 | ]; 38 | 39 | // Act. 40 | const lib = await import('./index.js'); 41 | 42 | // Assert. 43 | for (let item of expectedList) { 44 | expect(item in lib, `The expected object ${item} is not exported.`).toEqual(true); 45 | } 46 | for (let key of Object.keys(lib)) { 47 | expect( 48 | expectedList.includes(key), 49 | `The library exports object ${key}, which is not expected.` 50 | ).toEqual(true); 51 | } 52 | }); 53 | 54 | test('Should have default routing options in uninitialized state.', () => { 55 | // Assert. 56 | expect(routingOptions.hashMode).toBe('single'); 57 | expect(routingOptions.defaultHash).toBe(false); 58 | }); 59 | 60 | test('Should have no location in uninitialized state.', () => { 61 | // Assert. 62 | expect(location).toBeUndefined(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | # @svelte-router/core Playground 2 | 3 | This folder contains the tester project the author uses while developing the library. It is a **Vite + Svelte + TS** project created with `npm create vite@latest`. 4 | 5 | ## How to Use 6 | 7 | Install dependencies: 8 | 9 | ```bash 10 | npm ci 11 | ``` 12 | 13 | Then, you can do one of 2 options: 14 | 15 | 1. Install the routing library: `npm i @svelte-router/core` 16 | 2. Build the package and link it with NPM. 17 | 18 | The first option simply pulls the latest version from `npmjs.org`. Not good for library development as it doesn't allow testing whatever code changes were done. 19 | 20 | For the second option, move your terminal's current directory to the repository's root folder, then: 21 | 22 | ```bash 23 | npm pack 24 | ``` 25 | 26 | Once packed: 27 | 28 | ```bash 29 | npm link 30 | ``` 31 | 32 | Now move to the `/playground` folder and complete the link: 33 | 34 | ```bash 35 | npm link @svelte-router/core 36 | ``` 37 | 38 | You are ready to run the development server: 39 | 40 | ```bash 41 | npm run dev 42 | ``` 43 | 44 | Use the second option when developing/manipulating the library's code to see the effects in the playground project. It does require you to run `npm pack` after every change made, though. The link established by the `npm` CLI tool is based on the created `dist/` folder, not the `src/` folder. 45 | 46 | ### Gotcha's 47 | 48 | - NPM can only handle **one** linked library. 49 | - A project using a linked library (the playground project in this case), will require re-linking if you perform any action on the installed packages, such as `npm up`, `npm i`, or `npm remove`. Basically, any command that alters the contents of `node_modules/` will break the link. Re-establish the link after meddling with `node_modules/`. 50 | - Potentially a bug in Vite: HMR will work the first time you repackage, but will not work on subsequent repackage operations. Use the `r` command on the running Vite dev server to restart it. 51 | - Depending on the change you make in the library, even with HMR you'll need to restart the Vite server or refresh the playground page because global state that is set during the call to `init()` might be lost. 52 | -------------------------------------------------------------------------------- /add-component-help.ps1: -------------------------------------------------------------------------------- 1 | $distFolder = Resolve-Path -Path "./dist" 2 | 3 | function Update-SvelteFiles { 4 | param ( 5 | [string]$folder 6 | ) 7 | 8 | $svelteFiles = Get-ChildItem -Path $folder -Filter *.svelte.d.ts 9 | $readmeFiles = Get-ChildItem -Path $folder -Filter README.md 10 | 11 | if ($svelteFiles.Count -eq 1 -and $readmeFiles.Count -eq 1) { 12 | $svelteFile = $svelteFiles[0].FullName 13 | $readmeFile = $readmeFiles[0].FullName 14 | 15 | # Extract component name from filename (e.g., "Link.svelte.d.ts" -> "Link") 16 | $componentName = [System.IO.Path]::GetFileNameWithoutExtension($svelteFiles[0].Name) -replace '\.svelte.*$', '' 17 | 18 | $readmeContent = Get-Content -Path $readmeFile -Raw -Encoding utf8 19 | # Normalize line endings to Unix format 20 | $readmeContent = $readmeContent -replace "`r`n", "`n" -replace "`r", "`n" 21 | 22 | $svelteContent = Get-Content -Path $svelteFile -Raw -Encoding utf8 23 | 24 | # Find JSDoc comment before the specific component's declare statement 25 | $declarePattern = "declare const ${componentName}:" 26 | if ($svelteContent -match "(/\*\*[\s\S]*?\*/)\s*$([regex]::Escape($declarePattern))") { 27 | # Replace existing JSDoc comment for this specific component 28 | $newJSDoc = "/**`n * $($readmeContent -replace "`n", "`n * ")`n */" 29 | $svelteContent = $svelteContent -replace "/\*\*[\s\S]*?\*/\s*(?=$([regex]::Escape($declarePattern)))", "$newJSDoc`n" 30 | } else { 31 | # Add new JSDoc comment before the specific component's declare statement 32 | $newJSDoc = "/**`n * $($readmeContent -replace "`n", "`n * ")`n */`n" 33 | $svelteContent = $svelteContent -replace "($([regex]::Escape($declarePattern)))", "$newJSDoc`$1" 34 | } 35 | 36 | Set-Content -Path $svelteFile -Value $svelteContent -Encoding utf8NoBOM 37 | } 38 | } 39 | 40 | function Traverse-Folders { 41 | param ( 42 | [string]$rootFolder 43 | ) 44 | 45 | $folders = Get-ChildItem -Path $rootFolder -Directory -Recurse 46 | foreach ($folder in $folders) { 47 | Update-SvelteFiles -folder $folder.FullName 48 | } 49 | } 50 | 51 | Traverse-Folders -rootFolder $distFolder 52 | -------------------------------------------------------------------------------- /playground/src/lib/Alert.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | {#if !dismissed} 39 | 49 | {/if} 50 | 51 | 79 | -------------------------------------------------------------------------------- /playground/src/lib/demo/DemoView.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 |
    23 |
    24 |
    25 | params?.rest === '/router' || !params?.rest} 30 | > 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
    49 |
    50 |
    51 | 57 |
    58 |
    59 |
    60 |
    61 |
    62 | -------------------------------------------------------------------------------- /src/lib/Route/README.md: -------------------------------------------------------------------------------- 1 | # Route 2 | 3 | The `Route` component is used to define a route within a `Router` component. It matches the current URL against its 4 | path and renders its children if the path matches. 5 | 6 | `Route` components do not require any special placement. They can be immediate children of `Router` components, or 7 | they can be embedded anywhere down the hierarchy, including being children of other `Route` components. 8 | 9 | ## Props 10 | 11 | | Property | Type | Default Value | Bindable | Description | 12 | | ------------------- | -------------------------------------------------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------ | 13 | | `key` | `string` | (none) | | Sets the route's unique key. | 14 | | `path` | `string \| RegExp` | (none) | | Sets the route's path pattern, or a regular expression used to test and match the browser's URL. | 15 | | `and` | `(params: RouteParamsRecord \| undefined) => boolean` | `undefined` | | Sets a function for additional matching conditions. | 16 | | `ignoreForFallback` | `boolean` | `false` | | Controls whether the matching status of this route affects the visibility of fallback content. | 17 | | `caseSensitive` | `boolean` | `false` | | Sets whether the route's path pattern should be matched case-sensitively. | 18 | | `hash` | `Hash` | `undefined` | | Sets the hash mode of the route. | 19 | | `params` | `RouteParamsRecord` | `undefined` | Yes | Provides a way to obtain a route's parameters through property binding. | 20 | | `children` | `Snippet<[RouteChildrenContext]>` | `undefined` | | Renders the children of the route. | 21 | 22 | [Online Documentation](https://svelte-router.dev/api/core/route) 23 | 24 | ## Examples 25 | 26 | See the examples for the `Router` component. 27 | -------------------------------------------------------------------------------- /src/lib/kernel/trace.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { TraceOptions } from '../types.js'; 2 | import type { RouterEngine } from './RouterEngine.svelte.js'; 3 | 4 | /** 5 | * Weak references to all router engines that are created. 6 | */ 7 | const allRouters = new WeakMap[]>(); 8 | 9 | let version = $state(0); 10 | 11 | /** 12 | * Registers the router engine in the `allRouters` map for tracing purposes. 13 | * @param router Router engine to register. 14 | */ 15 | export function registerRouter(router: RouterEngine) { 16 | if (router.parent) { 17 | ++version; 18 | let parentRefs = allRouters.get(router.parent); 19 | if (!parentRefs) { 20 | allRouters.set(router.parent, [new WeakRef(router)]); 21 | } else { 22 | parentRefs.push(new WeakRef(router)); 23 | } 24 | } 25 | } 26 | 27 | export function unregisterRouter(router: RouterEngine) { 28 | if (router.parent) { 29 | let parentRefs = allRouters.get(router.parent); 30 | if (parentRefs) { 31 | let index = parentRefs.findIndex((ref) => ref.deref() === router); 32 | if (index >= 0) { 33 | parentRefs.splice(index, 1); 34 | ++version; 35 | } 36 | } 37 | if (parentRefs?.length === 0) { 38 | allRouters.delete(router.parent); 39 | ++version; 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * Obtains the list of all child router engines of the specified parent. 46 | * @param parent Router engine in question. 47 | * @returns An array with all child router engines of the specified parent. 48 | */ 49 | export function getAllChildRouters(parent: RouterEngine) { 50 | version; 51 | let refs = allRouters.get(parent); 52 | if (!refs) { 53 | return []; 54 | } 55 | return refs.reduce((acc, ref) => { 56 | const router = ref.deref(); 57 | if (router) { 58 | acc.push(new WeakRef(router)); 59 | } 60 | return acc; 61 | }, [] as WeakRef[]); 62 | } 63 | 64 | /** 65 | * Default tracing options used for rollback. 66 | */ 67 | export const defaultTraceOptions: Required = { 68 | routerHierarchy: false 69 | }; 70 | 71 | /** 72 | * Tracing options that can be set during library initialization. 73 | */ 74 | export const traceOptions: Required = structuredClone(defaultTraceOptions); 75 | 76 | export function setTraceOptions(options?: TraceOptions) { 77 | Object.assign(traceOptions, options); 78 | } 79 | 80 | /** 81 | * Resets tracing options to their default values. 82 | */ 83 | export function resetTraceOptions(): void { 84 | Object.assign(traceOptions, structuredClone(defaultTraceOptions)); 85 | } 86 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ActiveState, ActiveStateAriaAttributes, Hash } from './types.js'; 2 | import { routingOptions } from './kernel/options.js'; 3 | import type { AriaAttributes, HTMLAnchorAttributes } from 'svelte/elements'; 4 | 5 | /** 6 | * Asserts that the specified routing mode is allowed by the current routing options. 7 | * 8 | * @param hash The routing mode to assert. 9 | * @throws If the specified routing mode is disallowed by the current routing options. 10 | */ 11 | export function assertAllowedRoutingMode(hash: Hash) { 12 | if (hash === false && routingOptions.disallowPathRouting) { 13 | throw new Error('Path routing has been disallowed by a library extension.'); 14 | } 15 | if (hash === true && routingOptions.disallowHashRouting) { 16 | throw new Error('Hash routing has been disallowed by a library extension.'); 17 | } 18 | if (typeof hash === 'string' && routingOptions.disallowMultiHashRouting) { 19 | throw new Error('Multi-hash routing has been disallowed by a library extension.'); 20 | } 21 | } 22 | 23 | /** 24 | * Joins two style definitions into a single style definition. 25 | * 26 | * @param startingStyle The base style definition. 27 | * @param addedStyle The style definition to add to the base style definition. 28 | * @returns The combined style definition, or `undefined` if both inputs are empty or `undefined`. 29 | */ 30 | export function joinStyles( 31 | startingStyle: HTMLAnchorAttributes['style'], 32 | addedStyle: ActiveState['style'] 33 | ): string | undefined { 34 | let baseStyle = startingStyle ? startingStyle.trim() : ''; 35 | if (baseStyle && !baseStyle.endsWith(';')) { 36 | baseStyle += ';'; 37 | } 38 | if (!addedStyle) { 39 | return baseStyle || undefined; 40 | } 41 | if (typeof addedStyle === 'string') { 42 | return baseStyle ? `${baseStyle} ${addedStyle}` : addedStyle; 43 | } 44 | const calculatedStyle = Object.entries(addedStyle).reduce( 45 | (acc, [key, value]) => acc + `${key}: ${value}; `, 46 | '' 47 | ); 48 | return baseStyle ? `${baseStyle} ${calculatedStyle}` : calculatedStyle; 49 | } 50 | 51 | /** 52 | * Expands the keys of an `ActiveStateAriaAttributes` object into full `aria-` attributes. 53 | * @param aria Shortcut version of an `AriaAttributes` object. 54 | * @returns An `AriaAttributes` object that can be spread over HTML elements. 55 | */ 56 | export function expandAriaAttributes( 57 | aria: ActiveStateAriaAttributes | undefined 58 | ): AriaAttributes | undefined { 59 | if (!aria) { 60 | return undefined; 61 | } 62 | const result = {} as AriaAttributes; 63 | for (const [k, v] of Object.entries(aria)) { 64 | if (v !== undefined) { 65 | // @ts-expect-error TS7053 - We know this construction is correct. 66 | result[`aria-${k}`] = v; 67 | } 68 | } 69 | return result; 70 | } 71 | 72 | export function noTrailingSlash(path: string) { 73 | return path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path; 74 | } 75 | -------------------------------------------------------------------------------- /playground/src/lib/demo/NavBar.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 79 | -------------------------------------------------------------------------------- /src/lib/behaviors/active.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { Attachment } from 'svelte/attachments'; 2 | import type { ActiveState, RouteStatus } from '../types.js'; 3 | import { joinStyles } from '$lib/utils.js'; 4 | import { isRouteActive } from '$lib/public-utils.js'; 5 | import type { RouterEngine } from '$lib/kernel/RouterEngine.svelte.js'; 6 | import { clsx } from 'clsx'; 7 | 8 | /** 9 | * Svelte attachment factory that creates attachments that apply active styles and `aria-` attributes to an element 10 | * based on the current route status. 11 | * 12 | * This is built-in in the `Link` component, so it is not needed there. Use it anywhere else that is needed. 13 | * For example, the [Bulma Tabs component](https://bulma.io/documentation/components/tabs/) (Bulma is a CSS library) 14 | * requires that the `is-active` class be applied to the `
  • ` element, not the `` element. 15 | * 16 | * @example 17 | * ```svelte 18 | * 21 | * 22 | * {#snippet children(_, rs)} 23 | * 31 | * {/snippet} 32 | * ... 33 | * 34 | * ``` 35 | * 36 | * @param rsOrRouter Router or route status record object. 37 | * @param activeState Desired route and its active state (style/class/aria). 38 | * @param baseStyle Any base style to retain when active style is removed. 39 | * @returns The Svelte attachment function. 40 | */ 41 | export function activeBehavior( 42 | rsOrRouter: Record | RouterEngine | null | undefined, 43 | activeState: ActiveState & { key: string }, 44 | baseStyle: string = '' 45 | ): Attachment { 46 | return function (el: HTMLElement) { 47 | if (isRouteActive(rsOrRouter, activeState.key)) { 48 | el.setAttribute('style', joinStyles(baseStyle, activeState.style) ?? ''); 49 | const activeClass = clsx(activeState.class) 50 | .split(' ') 51 | .filter((c) => c.trim().length > 0); 52 | if (activeClass.length) { 53 | el.classList.add(...activeClass); 54 | } 55 | if (activeState.aria) { 56 | for (let [attr, value] of Object.entries(activeState.aria)) { 57 | el.setAttribute(`aria-${attr}`, (value ?? '').toString()); 58 | } 59 | } 60 | return () => { 61 | el.setAttribute('style', baseStyle ?? ''); 62 | if (activeClass.length) { 63 | el.classList.remove(...activeClass); 64 | } 65 | if (activeState.aria) { 66 | for (let attr of Object.keys(activeState.aria)) { 67 | el.removeAttribute(`aria-${attr}`); 68 | } 69 | } 70 | }; 71 | } 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/logo/logo-48.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | N 71 | E 80 | S 89 | W 98 | -------------------------------------------------------------------------------- /src/lib/kernel/trace.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | import { 3 | getAllChildRouters, 4 | registerRouter, 5 | resetTraceOptions, 6 | setTraceOptions, 7 | traceOptions 8 | } from './trace.svelte.js'; 9 | import { RouterEngine } from './RouterEngine.svelte.js'; 10 | import { init } from '../init.js'; 11 | 12 | vi.mock(import('./trace.svelte.js'), async (importActual) => { 13 | const actual = await importActual(); 14 | return { 15 | ...actual, 16 | registerRouter: vi.fn(actual.registerRouter) 17 | }; 18 | }); 19 | 20 | describe('setTraceOptions', () => { 21 | test.each([true, false])("Should set the 'routerHierarchy' option to %s.", (value) => { 22 | // Act. 23 | setTraceOptions({ routerHierarchy: value }); 24 | 25 | // Assert. 26 | expect(traceOptions.routerHierarchy).toBe(value); 27 | }); 28 | }); 29 | 30 | describe('resetTraceOptions', () => { 31 | test('Should reset trace options to defaults.', () => { 32 | // Arrange - Set to non-default value 33 | setTraceOptions({ routerHierarchy: true }); 34 | expect(traceOptions.routerHierarchy).toBe(true); 35 | 36 | // Act 37 | resetTraceOptions(); 38 | 39 | // Assert - Should be back to default 40 | expect(traceOptions.routerHierarchy).toBe(false); 41 | }); 42 | 43 | test('Should reset trace options when already at defaults.', () => { 44 | // Arrange - Ensure it's already at default 45 | resetTraceOptions(); 46 | expect(traceOptions.routerHierarchy).toBe(false); 47 | 48 | // Act 49 | resetTraceOptions(); 50 | 51 | // Assert - Should still be at default 52 | expect(traceOptions.routerHierarchy).toBe(false); 53 | }); 54 | }); 55 | 56 | describe('registerRouter', () => { 57 | let cleanup: () => void; 58 | beforeAll(() => { 59 | cleanup = init({ trace: { routerHierarchy: true } }); 60 | }); 61 | afterAll(() => { 62 | cleanup(); 63 | }); 64 | beforeEach(() => { 65 | vi.resetAllMocks(); 66 | }); 67 | test('Should be called when a new router engine is created.', () => { 68 | // Act. 69 | new RouterEngine(); 70 | 71 | // Assert. 72 | expect(registerRouter).toHaveBeenCalled(); 73 | }); 74 | }); 75 | 76 | describe('getAllChildRouters', () => { 77 | let cleanup: () => void; 78 | beforeAll(() => { 79 | cleanup = init({ trace: { routerHierarchy: true } }); 80 | }); 81 | afterAll(() => { 82 | cleanup(); 83 | }); 84 | beforeEach(() => { 85 | vi.clearAllMocks(); 86 | }); 87 | test('Should return the children of the specified router.', () => { 88 | // Arrange. 89 | const parent = new RouterEngine(); 90 | const child = new RouterEngine(parent); 91 | 92 | // Act. 93 | const children = getAllChildRouters(parent); 94 | 95 | // Assert. 96 | expect(children.length).toBe(1); 97 | expect(children[0].deref()).toBe(child); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/lib/LinkContext/README.md: -------------------------------------------------------------------------------- 1 | # LinkContext 2 | 3 | The `LinkContext` component is used to create context for `Link` components. This context can be used to set, in mass, the `replace`, `prependBasePath`, `preserveQuery` and `activeState` properties. 4 | 5 | Instead of writing this: 6 | 7 | ```svelte 8 | ... 9 | ... 10 | ... 11 | ... 12 | ``` 13 | 14 | You can do: 15 | 16 | ```svelte 17 | 18 | ... 19 | ... 20 | ... 21 | 22 | ``` 23 | 24 | Unlike the rest of components in this library, this one does not support the `hash` property. The context is 25 | inherited by all links among its children. 26 | 27 | **Note**: The `preserveQuery` option only has an effect on path routing links since hash routing links should not 28 | lose the query string. 29 | 30 | ## Priorities 31 | 32 | The `Link` component will give priority to an explicitly-set value at its property level. If a property-level value is 33 | not found, then the context-provided property value is used. If there is no context, then the default value takes over. 34 | 35 | ### Priorities Between Contexts 36 | 37 | Link contexts inherit from parent link contexts. A context deeper in the document's hierarchy will give priority to the values explicitly set via its component properties. If a component property is `undefined`, the parent context, if any, will be used as source for the value. 38 | 39 | Unlike the `Link` component, the contextual properties of the `LinkContext` component provide no default value. 40 | 41 | ## Props 42 | 43 | | Property | Type | Default Value | Bindable | Description | 44 | | ----------------- | --------------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- | 45 | | `replace` | `boolean` | `undefined` | | Configures the link so it replaces the current URL as opposed to pushing the URL as a new entry in the browser's History API. | 46 | | `prependBasePath` | `boolean` | `undefined` | | Configures the component to prepend the parent router's base path to the `href` property. | 47 | | `preserveQuery` | `PreserveQuery` | `undefined` | | Configures the component to preserve the query string whenever it triggers navigation. | 48 | | `activeState` | `ActiveState` | `undefined` | | Sets the various options that are used to automatically style the anchor tag whenever a particular route becomes active. | 49 | | `children` | `Snippet` | `undefined` | | Renders the children of the component. | 50 | 51 | [Online Documentation](https://svelte-router.dev/api/core/linkcontext) 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@svelte-router/core", 3 | "version": "1.0.7", 4 | "author": { 5 | "name": "José Pablo Ramírez Vargas", 6 | "email": "webJose@gmail.com" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/WJSoftware/svelte-router.git" 11 | }, 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/WJSoftware/svelte-router/issues" 15 | }, 16 | "homepage": "https://svelte-router.dev", 17 | "description": "Next-level routing for Svelte and Sveltekit", 18 | "scripts": { 19 | "dev": "vite dev", 20 | "build": "patch-package && vite build && node scripts/generate-sitemap.js && npm run prepack", 21 | "preview": "vite preview", 22 | "prepack": "svelte-kit sync && svelte-package && publint && pwsh ./add-component-help.ps1", 23 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 24 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 25 | "format": "prettier --write .", 26 | "lint": "prettier --check . && eslint .", 27 | "test:unit": "vitest", 28 | "test": "npm run test:unit -- --run" 29 | }, 30 | "files": [ 31 | "dist", 32 | "!dist/**/*.test.*", 33 | "!dist/**/*.spec.*", 34 | "!dist/testing" 35 | ], 36 | "sideEffects": [ 37 | "**/*.css" 38 | ], 39 | "svelte": "./dist/index.js", 40 | "types": "./dist/index.d.ts", 41 | "type": "module", 42 | "exports": { 43 | ".": { 44 | "types": "./dist/index.d.ts", 45 | "svelte": "./dist/index.js" 46 | }, 47 | "./kernel": { 48 | "types": "./dist/kernel/index.d.ts", 49 | "default": "./dist/kernel/index.js" 50 | }, 51 | "./logo": { 52 | "types": "./dist/logo/logo.d.ts", 53 | "default": "./dist/logo/logo.svg" 54 | }, 55 | "./logo64": { 56 | "types": "./dist/logo/logo.d.ts", 57 | "default": "./dist/logo/logo-64.svg" 58 | }, 59 | "./logo48": { 60 | "types": "./dist/logo/logo.d.ts", 61 | "default": "./dist/logo/logo-48.svg" 62 | } 63 | }, 64 | "keywords": [ 65 | "svelte", 66 | "router", 67 | "svelte-router", 68 | "spa", 69 | "micro-frontend", 70 | "pwa" 71 | ], 72 | "peerDependencies": { 73 | "svelte": "^5.31.0" 74 | }, 75 | "devDependencies": { 76 | "@eslint/compat": "^2.0.0", 77 | "@eslint/js": "^9.18.0", 78 | "@sveltejs/adapter-static": "^3.0.10", 79 | "@sveltejs/kit": "^2.0.0", 80 | "@sveltejs/package": "^2.0.0", 81 | "@sveltejs/vite-plugin-svelte": "^6.1.3", 82 | "@sveltepress/theme-default": "^7.0.2", 83 | "@sveltepress/vite": "^1.3.2", 84 | "@testing-library/svelte": "^5.2.6", 85 | "eslint": "^9.18.0", 86 | "eslint-config-prettier": "^10.0.1", 87 | "eslint-plugin-svelte": "^3.11.0", 88 | "globals": "^16.3.0", 89 | "jsdom": "^27.0.0", 90 | "patch-package": "^8.0.1", 91 | "prettier": "^3.4.2", 92 | "prettier-plugin-svelte": "^3.3.3", 93 | "publint": "^0.3.2", 94 | "sass": "^1.83.4", 95 | "svelte": "^5.0.0", 96 | "svelte-check": "^4.0.0", 97 | "typescript": "^5.0.0", 98 | "typescript-eslint": "^8.20.0", 99 | "vite": "^7.1.3", 100 | "vitest": "^4.0.14" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/lib/kernel/StockHistoryApi.svelte.ts: -------------------------------------------------------------------------------- 1 | import { on } from 'svelte/events'; 2 | import type { HistoryApi, State } from '../types.js'; 3 | import { LocationState } from './LocationState.svelte.js'; 4 | 5 | /** 6 | * Standard implementation of HistoryApi that uses the browser's native History API 7 | * and window.location. This is the default implementation used in normal browser environments. 8 | */ 9 | export class StockHistoryApi extends LocationState implements HistoryApi { 10 | #cleanupFunctions: (() => void)[] = []; 11 | 12 | constructor(initialUrl?: string, initialState?: State) { 13 | super(initialUrl, initialState); 14 | if (typeof globalThis.window !== 'undefined') { 15 | this.#cleanupFunctions.push( 16 | on(globalThis.window, 'popstate', this.#handlePopstateEvent), 17 | on(globalThis.window, 'hashchange', this.#handleHashChangeEvent) 18 | ); 19 | } 20 | } 21 | 22 | #handlePopstateEvent = (event: PopStateEvent): void => { 23 | this.url.href = globalThis.window.location.href; 24 | this.state = this.normalizeState(event.state, this.state); 25 | }; 26 | 27 | #handleHashChangeEvent = (event: HashChangeEvent): void => { 28 | this.url.href = globalThis.window.location.href; 29 | this.state = { 30 | path: this.state.path, 31 | hash: {} 32 | }; 33 | // Synchronize the environment's history state with a replace call. 34 | globalThis.window.history.replaceState($state.snapshot(this.state), '', this.url.href); 35 | }; 36 | 37 | // History API implementation 38 | get length(): number { 39 | return globalThis.window?.history?.length ?? 0; 40 | } 41 | 42 | get scrollRestoration(): ScrollRestoration { 43 | return globalThis.window?.history?.scrollRestoration ?? 'auto'; 44 | } 45 | 46 | set scrollRestoration(value: ScrollRestoration) { 47 | if (globalThis.window?.history) { 48 | globalThis.window.history.scrollRestoration = value; 49 | } 50 | } 51 | 52 | back(): void { 53 | globalThis.window?.history?.back(); 54 | } 55 | 56 | forward(): void { 57 | globalThis.window?.history?.forward(); 58 | } 59 | 60 | go(delta?: number): void { 61 | globalThis.window?.history?.go(delta); 62 | } 63 | 64 | #updateHistory( 65 | historyMethod: 'replaceState' | 'pushState', 66 | data: any, 67 | unused: string, 68 | url?: string | URL | null 69 | ): void { 70 | const normalizedState = this.normalizeState(data); 71 | globalThis.window?.history[historyMethod](normalizedState, unused, url); 72 | this.url.href = globalThis.window?.location?.href ?? new URL(url ?? '', this.url).href; 73 | this.state = normalizedState; 74 | } 75 | 76 | pushState(data: any, unused: string, url?: string | URL | null): void { 77 | this.#updateHistory('pushState', data, unused, url); 78 | } 79 | 80 | replaceState(data: any, unused: string, url?: string | URL | null): void { 81 | this.#updateHistory('replaceState', data, unused, url); 82 | } 83 | 84 | dispose(): void { 85 | this.#cleanupFunctions.forEach((cleanup) => cleanup()); 86 | this.#cleanupFunctions = []; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/lib/Fallback/Fallback.svelte: -------------------------------------------------------------------------------- 1 | 70 | 71 | {#if (router && when?.(router.routeStatus, router.fallback)) || (!when && router?.fallback)} 72 | {@render children?.({ state: router.state, rs: router.routeStatus })} 73 | {/if} 74 | -------------------------------------------------------------------------------- /src/lib/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, expect, test, vi } from 'vitest'; 2 | import { assertAllowedRoutingMode, expandAriaAttributes, noTrailingSlash } from './utils.js'; 3 | import { ALL_HASHES } from '$test/test-utils.js'; 4 | import { resetRoutingOptions, setRoutingOptions } from './kernel/options.js'; 5 | import type { ActiveStateAriaAttributes, ExtendedRoutingOptions, Hash } from './types.js'; 6 | import type { AriaAttributes } from 'svelte/elements'; 7 | 8 | const hashValues = Object.values(ALL_HASHES).filter((x) => x !== undefined); 9 | 10 | describe('assertAllowedRoutingMode', () => { 11 | afterEach(() => { 12 | resetRoutingOptions(); 13 | }); 14 | 15 | test.each(hashValues)( 16 | 'Should not throw when all routing modes are allowed (hash=%s).', 17 | (hash) => { 18 | expect(() => assertAllowedRoutingMode(hash)).not.toThrow(); 19 | } 20 | ); 21 | 22 | test.each<{ 23 | options: Partial; 24 | hash: Hash; 25 | }>([ 26 | { 27 | options: { 28 | disallowHashRouting: true 29 | }, 30 | hash: ALL_HASHES.single 31 | }, 32 | { 33 | options: { 34 | disallowMultiHashRouting: true 35 | }, 36 | hash: ALL_HASHES.multi 37 | }, 38 | { 39 | options: { 40 | disallowPathRouting: true 41 | }, 42 | hash: ALL_HASHES.path 43 | } 44 | ])( 45 | 'Should throw when the specified routing mode is disallowed (hash=$hash).', 46 | ({ options, hash }) => { 47 | setRoutingOptions(options); 48 | expect(() => assertAllowedRoutingMode(hash)).toThrow(); 49 | } 50 | ); 51 | }); 52 | 53 | describe('expandAriaAttributes', () => { 54 | test('Should return undefined when input is undefined.', () => { 55 | // Act. 56 | const result = expandAriaAttributes(undefined); 57 | 58 | // Assert. 59 | expect(result).toBeUndefined(); 60 | }); 61 | test.each<{ 62 | input: ActiveStateAriaAttributes; 63 | expected: AriaAttributes; 64 | }>([ 65 | { 66 | input: { current: 'page' }, 67 | expected: { 'aria-current': 'page' } 68 | }, 69 | { 70 | input: { disabled: true, hidden: false }, 71 | expected: { 'aria-disabled': true, 'aria-hidden': false } 72 | } 73 | ])('Should expand $input as $expected .', ({ input, expected }) => { 74 | // Act. 75 | const result = expandAriaAttributes(input); 76 | 77 | // Assert. 78 | expect(result).toEqual(expected); 79 | }); 80 | }); 81 | 82 | describe('noTrailingSlash', () => { 83 | test.each<{ 84 | input: string; 85 | expected: string; 86 | }>([ 87 | { input: '/path/', expected: '/path' }, 88 | { input: '/path/to/resource/', expected: '/path/to/resource' }, 89 | { input: '/path', expected: '/path' }, 90 | { input: '/', expected: '/' }, 91 | { input: '', expected: '' } 92 | ])('Should convert $input to $expected .', ({ input, expected }) => { 93 | // Act. 94 | const result = noTrailingSlash(input); 95 | 96 | // Assert. 97 | expect(result).toBe(expected); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /scripts/generate-sitemap.js: -------------------------------------------------------------------------------- 1 | import { readdir } from 'fs/promises'; 2 | import { writeFile } from 'fs/promises'; 3 | import { join, resolve } from 'path'; 4 | 5 | /** 6 | * Recursively finds all +page.md and +page.svelte files in the routes directory 7 | */ 8 | async function findRouteFiles(dir, baseDir = dir) { 9 | const routes = []; 10 | 11 | try { 12 | const entries = await readdir(dir, { withFileTypes: true }); 13 | 14 | for (const entry of entries) { 15 | const fullPath = join(dir, entry.name); 16 | 17 | if (entry.isDirectory()) { 18 | // Recursively search subdirectories 19 | const subRoutes = await findRouteFiles(fullPath, baseDir); 20 | routes.push(...subRoutes); 21 | } else if (entry.isFile() && entry.name.match(/^\+page\.(md|svelte)$/)) { 22 | // Convert file path to URL path 23 | const relativePath = fullPath.replace(baseDir, ''); 24 | let urlPath = 25 | relativePath 26 | .replace(/\\/g, '/') // Windows path separator 27 | .replace(/\/\+page\.(md|svelte)$/, '') // Remove +page.md/svelte 28 | .replace(/\/\([^)]+\)/g, '') // Remove SvelteKit route groups (parenthesized folders) 29 | .replace(/\/$/, '') || '/'; // Handle root, remove trailing slash 30 | 31 | // Filter out unwanted routes 32 | if (!urlPath.includes('[') && !urlPath.includes('sitemap')) { 33 | routes.push(urlPath); 34 | } 35 | } 36 | } 37 | } catch (error) { 38 | console.warn(`Could not read directory ${dir}:`, error.message); 39 | } 40 | 41 | return routes; 42 | } 43 | 44 | /** 45 | * Generates XML sitemap content 46 | */ 47 | function generateSitemap(routes, baseUrl = 'https://svelte-router.dev') { 48 | const lastModified = new Date().toISOString(); 49 | 50 | const urlEntries = routes 51 | .sort() 52 | .map((route) => { 53 | const priority = route === '/' ? '1.0' : '0.8'; 54 | return ` 55 | ${baseUrl}${route} 56 | ${lastModified} 57 | weekly 58 | ${priority} 59 | `; 60 | }) 61 | .join('\n'); 62 | 63 | return ` 64 | 65 | ${urlEntries} 66 | `; 67 | } 68 | 69 | /** 70 | * Main function to generate sitemap 71 | */ 72 | async function main() { 73 | const routesDir = resolve('src/routes'); 74 | const buildDir = resolve('build'); 75 | const sitemapPath = join(buildDir, 'sitemap.xml'); 76 | 77 | console.log('🗺️ Generating sitemap...'); 78 | 79 | try { 80 | // Find all route files 81 | const routes = await findRouteFiles(routesDir); 82 | console.log(`Found ${routes.length} routes:`, routes); 83 | 84 | // Generate sitemap XML 85 | const sitemapXml = generateSitemap(routes); 86 | 87 | // Write to build directory 88 | await writeFile(sitemapPath, sitemapXml, 'utf8'); 89 | 90 | console.log(`✅ Sitemap generated successfully at ${sitemapPath}`); 91 | } catch (error) { 92 | console.error('❌ Error generating sitemap:', error); 93 | process.exit(1); 94 | } 95 | } 96 | 97 | main(); 98 | -------------------------------------------------------------------------------- /src/lib/RouterTrace/README.md: -------------------------------------------------------------------------------- 1 | # RouterTrace 2 | 3 | The `RouterTrace` component renders an HTML table with route status data of its parent router. 4 | 5 | The table contains the following information: 6 | 7 | | Column | Description | 8 | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | 9 | | **Route** | Shows the route's key. | 10 | | **Path** | Shows the route's path. | 11 | | **RegEx** | Shows the generated regular expression for paths specified as string patterns. | 12 | | **Matches?** | Shows ✔ or ✘ to signal whether the route is currently matching or not. | 13 | | **Route Params** | Lists all of the route parameters and their inferred values. This only happens if the path’s regular expression was able to match. | 14 | 15 | Furthermore, in the table's caption you'll find the router's assigned ID, its base path, its children count if any, 16 | its parent router, the routing universe the router belongs to, the test path used when testing routes for matches and 17 | the value of the `fallback` property, which is used by `Fallback` components to know when to render content. 18 | 19 | While it's common to rely on the presence of a parent router, a parent router is not actually mandatory. Instead, one 20 | can trace any router that is provided through the `router` property. 21 | 22 | ## Traversing the Router Hierarchy 23 | 24 | The table's caption will present 2 buttons. Use these buttons to go up or down the hierarchy of routers within the 25 | same routing universe. 26 | 27 | ## Props 28 | 29 | | Property | Type | Default | Bindable | Description | 30 | | ---------------------- | ------------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------ | 31 | | `hash` | `Hash` | `undefined` | | Sets the hash mode of the component. | 32 | | `router` | `RouterEngine` | `undefined` | Yes | Sets the router engine to trace. | 33 | | `childrenMenuPosition` | `'top' \| 'bottom'` | `'top'` | | Sets the position of the router engine's children menu. | 34 | | `darkTheme` | `boolean` | `false` | | Enables or disables the dark theme for the component. | 35 | | `themeBtn` | `boolean` | `false` | | Shows or hides a button capable of toggling the component's theme between light and dark themes. | 36 | | `buttonClass` | `ClassValue` | `undefined` | | Overrides the default CSS class of all buttons in the component. | 37 | 38 | [Online Documentation](https://svelte-router.dev/api/core/routertrace) 39 | -------------------------------------------------------------------------------- /src/routes/api/core/fallback/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fallback Component 3 | description: API reference for the Fallback component that renders content when no routes match in your Svelte application 4 | --- 5 | 6 | :::info[Parent Requirement] 7 | `Router` required. 8 | ::: 9 | 10 | This is a very simple component: It renders content whenever its parent router has no matching routes. It is that simple. 11 | 12 | You are free to add as many of these as required to fulfill your user interface requirements. 13 | 14 | ## Properties 15 | 16 | ### `hash` 17 | 18 | Type: `Hash`; Default: `undefined`; Bindable: **No** 19 | 20 | This property controls the universe the `Fallback` component will be a part of. Read the `Router` component’s explanation on this property for detailed information. 21 | 22 | :::warning[Reactivity Warning] 23 | This value cannot be reactively mutated because it directly affects the search for its parent router, which is set in context, and context can only be read or set during component initialization. 24 | 25 | If you need reactive hash values, destroy and re-create the component whenever the value changes using `{#key hash}` or an equivalent approach. 26 | ::: 27 | 28 | ### `when` 29 | 30 | Type: `WhenPredicate`; Default: `undefined`; Bindable: **No** 31 | 32 | This property overrides the default activation logic for the component instance it is applied to. For the record, `Fallback` components render their children whenever the parent router engine’s `fallback` property is `true`. This is the default activation logic. 33 | 34 | However, because this library is a multi-route-matching routing library, this could diminish and even completely shut down the ability to present fallback content. Maybe there are always-on (or almost always-on) routes in the router for layout or navigation purposes, and simply adding the `ignoreForFallback` property to them doesn’t work. 35 | 36 | For cases like this, provide your own fallback condition(s) in the form of a predicate function. This is an (incomplete) example: 37 | 38 | ```svelte 39 | onlyLayoutRoutesRemain(rs)}>... 40 | ``` 41 | 42 | In this example, complex route-matching testing is deferred to the `onlyLayoutRoutesRemain()` function. If the function returns `true`, then fallback content is shown. 43 | 44 | The predicate function receives 2 arguments: The router’s route status information that provides matching status for all routes, and the calculated `fallback` value that represents the original ruling made by the router for fallback content. 45 | 46 | Unlike the `Route` component’s `path` and `and` properties, this property only applies to the instance of the `Fallback` component that got it defined. There’s no “propagation” of the property to other `Fallback` component instances. 47 | 48 | ### `children` 49 | 50 | Type: `Snippet<[RouterChildrenContext]>`; Default: `undefined`; Bindable: **No** 51 | 52 | The component’s default snippet. Children rendering is conditioned to the value of the parent router engine’s `fallback` property, or the ruling of the predicate function specified in the `when` property. 53 | 54 | The snippet provides, via its parameters, the current state data and the router’s route status data in a single, destructurable context object: 55 | 56 | ```svelte 57 | 58 | 59 | {#snippet children({ state, rs })} 60 | ... 61 | {/snippet} 62 | 63 | 64 | ``` 65 | 66 | There are no restrictions as to how or what is a child of a `Fallback` component. 67 | -------------------------------------------------------------------------------- /static/logo-64.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | N 78 | E 87 | S 96 | W 105 | -------------------------------------------------------------------------------- /src/lib/logo/logo-64.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | N 78 | E 87 | S 96 | W 105 | -------------------------------------------------------------------------------- /src/lib/Link/README.md: -------------------------------------------------------------------------------- 1 | # Link 2 | 3 | The `Link` component renders HTML anchor elements that behave in accordance to its `hash` property, allowing 4 | SPA-friendly navigation (navigation without reloading). 5 | 6 | ## Props 7 | 8 | | Property | Type | Default Value | Bindable | Description | 9 | | ----------------- | -------------------------------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- | 10 | | `hash` | `boolean \| string` | `undefined` | | Sets the hash mode of the component. | 11 | | `href` | `string` | (none) | | Sets the URL to navigate to. | 12 | | `replace` | `boolean` | `false` | | Configures the link so it replaces the current URL as opposed to pushing the URL as a new entry in the browser's History API. | 13 | | `state` | `any` | `undefined` | | Sets the state object to pass to the browser's History API when pushing or replacing the URL. | 14 | | `activeFor` | `string` | `undefined` | | Sets the route key that the link will use to determine if it should render as active. | 15 | | `activeState` | `ActiveState` | `undefined` | | Sets the various options that are used to automatically style the anchor tag whenever a particular route becomes active. | 16 | | `prependBasePath` | `boolean` | `false` | | Configures the component to prepend the parent router's base path to the `href` property. | 17 | | `preserveQuery` | `PreserveQuery` | `false` | | Configures the component to preserve the query string whenever it triggers navigation. | 18 | | `children` | `Snippet<[LinkChildrenContext]>` | `undefined` | | Renders the children of the component. | 19 | 20 | [Online Documentation](https://svelte-router.dev/api/core/link) 21 | 22 | ## Examples 23 | 24 | ### Basic Usage 25 | 26 | These don't require a parent router: 27 | 28 | ```svelte 29 | Path Routing => https://example.com/new/path 30 | 31 | Hash Routing => https://example.com/#/new/path 32 | 33 | 34 | Multi Hash Routing => https://example.com/#path1=/new/path Will also preserve any other named 35 | paths 36 | 37 | ``` 38 | 39 | ### Usage Within a Parent Router 40 | 41 | In this example, the `Link` component will take advantage of the parent router to inherit its base path and to 42 | automatically trigger its active appearance based on a specific route becoming active. 43 | 44 | ```svelte 45 | 46 | 53 | Click Me! 54 | 55 | ... 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /src/lib/kernel/preserveQuery.ts: -------------------------------------------------------------------------------- 1 | import type { PreserveQuery } from '../types.js'; 2 | import { location } from './Location.js'; 3 | 4 | /** 5 | * Preserves query parameters from the current URL into the given URL, based on the preservation options. 6 | * @param url The URL to add preserved query parameters to. 7 | * @param preserveQuery The query preservation options. 8 | * @returns The URL with preserved query parameters added. 9 | */ 10 | export function preserveQueryInUrl(url: string, preserveQuery: PreserveQuery): string { 11 | const urlObj = new URL(url, location.url.origin); 12 | mergeQueryParams(urlObj.searchParams, preserveQuery); 13 | return urlObj.toString(); 14 | } 15 | 16 | /** 17 | * Helper that merges query parameters from 2 URL's together. 18 | * 19 | * ### Important Notes 20 | * 21 | * + To preserve system resources, `set1` is modified directly to contain the merged results. 22 | * + If the provided `set1` is `undefined` and all query parameters are to be preserved, then `set2` will be returned 23 | * directly. 24 | * + If `set1` is `undefined`, a new `URLSearchParams` will be created (and returned) to contain the merged results. 25 | * + The return value will be `undefined` whenever `set1` is `undefined` and `set2` is also `undefined` or empty. 26 | * @param set1: First set of query parameters. 27 | * @param set2: Second set of query parameters. 28 | * @returns The merged `URLSearchParams`, or `undefined`. 29 | */ 30 | export function mergeQueryParams( 31 | set1: URLSearchParams | undefined, 32 | set2: URLSearchParams | undefined 33 | ): URLSearchParams | undefined; 34 | /** 35 | * Helper that merges the given search parameters with the ones found in the current environment's URL. 36 | * 37 | * ### Important Notes 38 | * 39 | * + To preserve system resources, `existingParams` is modified directly to contain the merged results. 40 | * + The `URLSearchParams` from the global `location` object will be returned when all query parameters are preserved 41 | * and `existingParams` is `undefined`. 42 | * + If `existingParams` is `undefined`, a new `URLSearchParams` will be created (and returned) to contain the merged 43 | * results. 44 | * + The return value will be `undefined` whenever `existingParams` is `undefined` and the global `location`'s search 45 | * parameters are empty. 46 | * @param existingParams Existing `URLSearchParams` from the new URL. 47 | * @param preserveQuery The query preservation options. 48 | * @returns The merged `URLSearchParams`, or `undefined`. 49 | */ 50 | export function mergeQueryParams( 51 | existingParams: URLSearchParams | undefined, 52 | preserveQuery?: PreserveQuery 53 | ): URLSearchParams | undefined; 54 | export function mergeQueryParams( 55 | set1: URLSearchParams | undefined, 56 | pqOrSet2: PreserveQuery | URLSearchParams | undefined 57 | ): URLSearchParams | undefined { 58 | const set2 = pqOrSet2 instanceof URLSearchParams ? pqOrSet2 : location.url.searchParams; 59 | const preserveQuery = pqOrSet2 instanceof URLSearchParams ? true : pqOrSet2; 60 | if (!pqOrSet2 || !set2.size) { 61 | return set1; 62 | } 63 | 64 | if (!set1 && preserveQuery === true) { 65 | return set2; 66 | } 67 | 68 | const mergedParams = set1 ?? new URLSearchParams(); 69 | 70 | const transferValue = (key: string) => { 71 | const values = set2.getAll(key); 72 | if (values.length) { 73 | values.forEach((v) => mergedParams.append(key, v)); 74 | } 75 | }; 76 | 77 | if (typeof preserveQuery === 'string') { 78 | transferValue(preserveQuery); 79 | } else { 80 | for (let key of Array.isArray(preserveQuery) ? preserveQuery : set2.keys()) { 81 | transferValue(key); 82 | } 83 | } 84 | 85 | return mergedParams; 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/preview-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy PR Preview to Cloudflare Pages 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '.github/**' 9 | - 'docs/**' 10 | - 'playground/**' 11 | - 'src/lib/**' 12 | - '.gitignore' 13 | - '.npmrc' 14 | - '.prettierignore' 15 | - '.prettierrc' 16 | - 'add-component-help.ps1' 17 | - 'AGENTS.md' 18 | - 'eslint.config.js' 19 | - 'LICENSE' 20 | - 'package.json' 21 | - 'package-lock.json' 22 | - 'README.md' 23 | - 'tsconfig.json' 24 | 25 | jobs: 26 | preview: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Setup Node.js 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: '24' 36 | cache: 'npm' 37 | 38 | - name: Install dependencies 39 | run: npm ci 40 | 41 | - name: Build 42 | run: npm run build 43 | 44 | - name: Deploy Preview to Cloudflare Pages 45 | uses: cloudflare/wrangler-action@v3 46 | with: 47 | apiToken: ${{ secrets.CF_API_TOKEN }} 48 | accountId: ${{ secrets.CF_ACC_ID }} 49 | command: pages deploy build --project-name=svelte-router --branch=preview-${{ github.event.number }} --commit-message="PR #${{ github.event.number }}: ${{ github.event.pull_request.title }}" 50 | 51 | - name: Update PR with Preview URL 52 | uses: actions/github-script@v7 53 | with: 54 | script: | 55 | const prNumber = context.issue.number; 56 | const branchName = `preview-${prNumber}`; 57 | const previewUrl = `https://${branchName}.svelte-router.pages.dev`; 58 | const commentTitle = '🚀 **Preview deployment ready!**'; 59 | 60 | // Look for existing preview comment 61 | const comments = await github.rest.issues.listComments({ 62 | owner: context.repo.owner, 63 | repo: context.repo.repo, 64 | issue_number: prNumber 65 | }); 66 | 67 | const botComment = comments.data.find(comment => 68 | comment.user.type === 'Bot' && 69 | comment.body.includes(commentTitle) 70 | ); 71 | 72 | // Extract deployment count from existing comment or start at 1 73 | let deploymentCount = 1; 74 | if (botComment) { 75 | const match = botComment.body.match(/(\d+) deployment/); 76 | deploymentCount = match ? parseInt(match[1]) + 1 : 2; 77 | } 78 | 79 | const commentBody = [ 80 | `${commentTitle} (${deploymentCount} ${deploymentCount === 1 ? 'deployment' : 'deployments'})`, 81 | '', 82 | `📍 **Preview URL:** ${previewUrl}`, 83 | '', 84 | '*This preview updates automatically when you push new commits to this PR.*' 85 | ].join('\n'); 86 | 87 | if (botComment) { 88 | // Update existing comment 89 | await github.rest.issues.updateComment({ 90 | owner: context.repo.owner, 91 | repo: context.repo.repo, 92 | comment_id: botComment.id, 93 | body: commentBody 94 | }); 95 | } else { 96 | // Create new comment 97 | await github.rest.issues.createComment({ 98 | owner: context.repo.owner, 99 | repo: context.repo.repo, 100 | issue_number: prNumber, 101 | body: commentBody 102 | }); 103 | } -------------------------------------------------------------------------------- /src/routes/api/techArticleMetaData.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/core/router": { 3 | "headline": "Router Component API", 4 | "description": "Complete API reference for the Router component that creates routing contexts and manages hierarchical route structures in Svelte applications. Learn about all properties, including the bindable router engine, basePath handling, and context management for building nested routing architectures." 5 | }, 6 | "/api/core/route": { 7 | "headline": "Route Component API", 8 | "description": "Comprehensive API documentation for the Route component that defines path matching conditions and renders content for active routes. Master pattern matching with string templates and regular expressions, parameter extraction, conditional routing with the 'and' property, and fallback behavior configuration." 9 | }, 10 | "/api/core/fallback": { 11 | "headline": "Fallback Component API", 12 | "description": "API reference for the Fallback component that elegantly handles unmatched routes by rendering default content when no routes match. Learn how to implement graceful fallbacks, understand the component's simple yet powerful design, and create user-friendly navigation experiences." 13 | }, 14 | "/api/core/link": { 15 | "headline": "Link Component API", 16 | "description": "Complete API documentation for the Link component providing intelligent navigation with automatic active styling, HREF generation, and seamless History API integration. Master all properties compatible with HTML anchor elements, making it a perfect drop-in replacement for standard hyperlinks." 17 | }, 18 | "/api/core/linkcontext": { 19 | "headline": "LinkContext Component API", 20 | "description": "API reference for the LinkContext component that provides default property inheritance for nested Link components. Simplify Link component configuration by setting common properties once at a higher level, reducing repetition and ensuring consistency across related navigation elements." 21 | }, 22 | "/api/core/routertrace": { 23 | "headline": "RouterTrace Component API", 24 | "description": "Development tool API reference for the RouterTrace debugging component that displays real-time route status information. Learn how to use this powerful debugging aid to visualize route matching, inspect router state, and troubleshoot routing issues during development." 25 | }, 26 | "/api/core/functions": { 27 | "headline": "Core Functions API", 28 | "description": "Complete API reference for utility functions in Svelte Router including navigation helpers, HREF builders, active state behaviors, and programmatic routing tools. Master the JavaScript API that powers component functionality and enables advanced routing scenarios." 29 | }, 30 | "/api/core/objects-and-classes": { 31 | "headline": "Core Objects & Classes API", 32 | "description": "Comprehensive documentation for core objects, classes, and interfaces including RouterEngine, Location implementations, and History API classes. Understand the library's architecture, learn extension points for custom implementations, and master advanced programmatic routing capabilities." 33 | }, 34 | "/api/kit/functions": { 35 | "headline": "SvelteKit Functions API", 36 | "description": "API reference for functions in the @svelte-router/kit extension package that enables hash routing in SvelteKit applications. Learn about the specialized init() function and configuration options tailored for SvelteKit's server-side rendering and file-based routing environment." 37 | }, 38 | "/api/kit/kitfallback": { 39 | "headline": "KitFallback Component API", 40 | "description": "API documentation for the KitFallback component specifically optimized for SvelteKit applications to prevent content flashes during server-side rendering. Understand how this specialized fallback component ensures smooth user experiences by avoiding unwanted content rendering on the server." 41 | }, 42 | "/api/kit/objects-and-classes": { 43 | "headline": "SvelteKit Objects & Classes API", 44 | "description": "API reference documentation for objects and classes in the official extension package for SvelteKit: @svelte-router/kit." 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/routes/docs/reactive-data/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reactive Data 3 | description: Leverage Svelte's reactivity and react naturally to changes in the environment's URL and history state 4 | --- 5 | 6 | All generated data is reactive, which simplifies the library considerably. For example, other routing solutions take the route of providing events for many things, like `beforeRouteMatch` and things like that. 7 | 8 | By contrast, this library provides no events (well, none in **lite** mode anyway). So how does one satisfy the need to react to events like a navigation event? Well, Svelte has made that very simple: _Signals_. 9 | 10 | ## Reacting to Navigation 11 | 12 | The global `location` object provided by the library has the `url`, `path` and `hashPaths` properties which are reactive Svelte signals. Additionally, it returns reactive state data via its `getState()` function. Simply write effects and derived calculations based on these. 13 | 14 | In the following example, it is assumed that path navigation (`false` as hash value means path navigation) sets a state object with the title property, which is meant to be set as the document’s title: 15 | 16 | ```svelte 17 | 20 | 21 | 22 | {location.getState(false)?.title ?? '(no title)'} - Awesome App 23 | 24 | ``` 25 | 26 | This one keeps track of how many times navigation has occurred: 27 | 28 | ```svelte 29 | 39 | ``` 40 | 41 | This one calculates a Boolean control flag to add an extra micro-frontend (using `single-spa`) based on the presence of a particular named path: 42 | 43 | ```svelte 44 | 55 | 56 | {#if showExtraMfe} 57 | 58 | {/if} 59 | ``` 60 | 61 | ## Reacting to Route Data 62 | 63 | Router engines collect route information in their `RouterEngine.routes` property. This is a reactive dictionary where the property name is the route’s key, and the value is the route’s information. Effects and derived calculations can react whenever routes are added or removed as `Route` components come and go to and from the HTML document. 64 | 65 | However, because router engines immediately calculate route status on changes made to this data, maybe it is just simpler and more informative to react to `RouterEngine.routeStatus`, which is a reactive dictionary object containing route matching information for all routes registered in the router. 66 | 67 | The following example shows how to react to routes becoming active: 68 | 69 | ```svelte 70 | 81 | 82 | ... 83 | ``` 84 | 85 | Related to route matching, router engines also provide the `fallback` reactive property. This property is the one that drives the rendering of fallback content: 86 | 87 | ```typescript 88 | $inspect(router.fallback).with((t, v) => { 89 | console.log('(%s) Fallback content is %s.', t, v ? 'visible' : 'not visible'); 90 | }); 91 | ``` 92 | 93 | This example shows how to reactively log a message that describes the visibility state of fallback content. 94 | 95 | --- 96 | 97 | If you believe you have a strong case for something that cannot be done reactively, feel free to drop an issue at the project’s [Issues page](https://github.com/WJSoftware/svelte-router/issues). 98 | -------------------------------------------------------------------------------- /src/routes/api/core/router/+page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Router Component 3 | description: API reference for the Router component that creates routing contexts and manages route hierarchies in Svelte 4 | --- 5 | 6 | :::info[Parent Requirement] 7 | None. 8 | ::: 9 | 10 | `Router` components are the components at the top of the hierarchy. They create router engine objects when not provided with one and register it as context for other components inside to pick up and collaborate. 11 | 12 | Start with a `Router` component, usually at the top of the application hierarchy (in a Vite application, in `App.svelte`), then add navigation such as hyperlinks to “pages” and `Route` components that define said “pages”. 13 | 14 | `Router` components can contain other `Router` components to create a nested hierarchy. Every time a new `Router` component is added, it inherits the parent router’s `basePath` property, which is prepended to its own `basePath` value. 15 | 16 | ## Properties 17 | 18 | ### `router` 19 | 20 | Type: `RouterEngine`; Default: `undefined`; Bindable: **Yes** 21 | 22 | Despite its apparent importance, it is probably the least needed property. This allows you to set or get the underlying router engine object. This, however, is a seldom-needed feature. Use only if you wish to manipulate the router using JavaScript instead of writing markup, or to react to route-matching events via `$derived` or any `$effect`. 23 | 24 | Because `Router` components set the router engine they use as context, this property cannot change reactively like Svelte developers are used to with property values because context can only be set during component initialization. If for any reason you need to reactively set this property, you’ll have to force destruction and recreation of the `Router` component. For example: 25 | 26 | ```svelte 27 | {#key router} 28 | 29 | {/key> 30 | ``` 31 | 32 | ### `basePath` 33 | 34 | Type: `string`; Default: `'/'`; Bindable: **No** 35 | 36 | Use it to add a fixed number of path segments to the route-matching regular expressions. The base path is added as if it had been typed in the route paths. 37 | 38 | This is particularly useful in micro-frontend scenarios because your Svelte micro-frontend can be told at which path it is being mounted at, so all route matching takes into account where the MFE is mounted. 39 | 40 | `Router` components, by virtue of their router engine objects, accumulate base paths as more routers are added. 41 | 42 | This is a reactive property, and can be changed at any point in time, and will reactively trigger recalculations. 43 | 44 | :::note[Only Router Components Add to basePath] 45 | Other routing libraries have their `Route` components collaborate their matched path to the base path of nested `Route` components. This library does not do this. 46 | 47 | `Route` components inside `Route` components do not inherit any matched path from any parent `Route` component, only from `Router` components. If needed, nest a `Router` component. 48 | 49 | _This might change in v2.0_. 50 | ::: 51 | 52 | ### `id` 53 | 54 | Type: `string`; Default: `undefined`; Bindable: **No** 55 | 56 | This is a tracing-only property, and it provides a human-readable identifier for the router. This identifier is visible in the `RouterTrace` component. 57 | 58 | ### `hash` 59 | 60 | Type: `Hash`; Default: `undefined`; Bindable: **No** 61 | 62 | This property controls whether the router matches routes against the current location’s pathname, or against the hash fragment of the current location, or a named path in said hash fragment. 63 | 64 | In short: The `hash` property determines the universe the router will belong to. 65 | 66 | Just like the `router` property, `hash` has a fundamental role in getting and setting the context, so this property cannot be reactive in the normal Svelte sense, and if you find yourself in the need to reactively change its value, you are forced to destroy and recreate the router component with the newly desired property value. 67 | 68 | ### `children` 69 | 70 | Type: `Snippet<[RouterChildrenContext]>`; Default: `undefined`; Bindable: **No** 71 | 72 | The component’s default snippet. The children are always rendered, meaning that any non-routing component/markup renders as per usual. 73 | 74 | The snippet provides, via its parameters, the current state data and the router’s route status data in a single, destructurable context object: 75 | 76 | ```svelte 77 | 78 | {#snippet children({ state, rs })} 79 | ... 80 | {/snippet} 81 | 82 | ``` 83 | 84 | There are no restrictions as to how or what is a child of a `Router` component. 85 | -------------------------------------------------------------------------------- /src/lib/kernel/LocationLite.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BeforeNavigateEvent, 3 | Hash, 4 | Location, 5 | GoToOptions, 6 | NavigateOptions, 7 | NavigationCancelledEvent, 8 | State, 9 | HistoryApi 10 | } from '../types.js'; 11 | import { getCompleteStateKey } from './Location.js'; 12 | import { StockHistoryApi } from './StockHistoryApi.svelte.js'; 13 | import { routingOptions } from './options.js'; 14 | import { resolveHashValue } from './resolveHashValue.js'; 15 | import { calculateHref } from './calculateHref.js'; 16 | import { calculateState } from './calculateState.js'; 17 | import { preserveQueryInUrl } from './preserveQuery.js'; 18 | import { assertAllowedRoutingMode } from '$lib/utils.js'; 19 | 20 | /** 21 | * A lite version of the location object. It does not support event listeners or state-setting call interceptions, 22 | * which are normally only needed when mixing router libraries. 23 | */ 24 | export class LocationLite implements Location { 25 | #historyApi: HistoryApi; 26 | 27 | hashPaths = $derived.by(() => { 28 | if (routingOptions.hashMode === 'single') { 29 | return { single: this.#historyApi.url.hash.substring(1) }; 30 | } 31 | const result = {} as Record; 32 | const paths = this.#historyApi.url.hash.substring(1).split(';'); 33 | for (let rawPath of paths) { 34 | const [id, path] = rawPath.split('='); 35 | if (!id || !path) { 36 | continue; 37 | } 38 | result[id] = path; 39 | } 40 | return result; 41 | }); 42 | 43 | path = $derived.by(() => { 44 | const hasDriveLetter = 45 | this.url.protocol.startsWith('file:') && this.url.pathname[2] === ':'; 46 | return hasDriveLetter ? this.url.pathname.substring(3) : this.url.pathname; 47 | }); 48 | 49 | constructor(historyApi?: HistoryApi) { 50 | this.#historyApi = historyApi ?? new StockHistoryApi(); 51 | } 52 | 53 | on(event: 'beforeNavigate', callback: (event: BeforeNavigateEvent) => void): () => void; 54 | on( 55 | event: 'navigationCancelled', 56 | callback: (event: NavigationCancelledEvent) => void 57 | ): () => void; 58 | on(_event: unknown, _callback: unknown): (() => void) | (() => void) { 59 | throw new Error( 60 | 'This feature is only available when initializing the routing library with the full option.' 61 | ); 62 | } 63 | 64 | get url() { 65 | return this.#historyApi.url; 66 | } 67 | 68 | getState(hash?: Hash | undefined) { 69 | const resolvedHash = resolveHashValue(hash); 70 | if (typeof resolvedHash === 'string') { 71 | return this.#historyApi.state?.hash[resolvedHash]; 72 | } 73 | if (resolvedHash) { 74 | return this.#historyApi.state?.hash.single; 75 | } 76 | return this.#historyApi.state?.path; 77 | } 78 | 79 | back(): void { 80 | this.#historyApi.back(); 81 | } 82 | 83 | forward(): void { 84 | this.#historyApi.forward(); 85 | } 86 | 87 | go(delta: number): void { 88 | this.#historyApi.go(delta); 89 | } 90 | 91 | #goTo(url: string, replace: boolean, state: State | undefined) { 92 | if (url === '') { 93 | // Shallow routing. 94 | url = this.url.href; 95 | } 96 | this.#historyApi[replace ? 'replaceState' : 'pushState'](state, '', url); 97 | } 98 | 99 | goTo(url: string, options?: GoToOptions): void { 100 | if (options?.preserveQuery && url !== '') { 101 | url = preserveQueryInUrl(url, options.preserveQuery); 102 | } 103 | this.#goTo(url, options?.replace ?? false, options?.state); 104 | } 105 | 106 | navigate(url: string, options?: NavigateOptions): void { 107 | const resolvedHash = resolveHashValue(options?.hash); 108 | assertAllowedRoutingMode(resolvedHash); 109 | if (url !== '') { 110 | url = calculateHref( 111 | { 112 | ...options, 113 | hash: resolvedHash 114 | }, 115 | url 116 | ); 117 | } 118 | const newState = calculateState(resolvedHash, options?.state); 119 | this.#goTo(url, options?.replace ?? false, newState); 120 | } 121 | 122 | [getCompleteStateKey](): State { 123 | return $state.snapshot(this.#historyApi.state); 124 | } 125 | 126 | dispose() { 127 | this.#historyApi.dispose(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/lib/LinkContext/LinkContext.svelte: -------------------------------------------------------------------------------- 1 | 78 | 79 | 106 | 107 | {@render children?.()} 108 | -------------------------------------------------------------------------------- /src/lib/kernel/InterceptedHistoryApi.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BeforeNavigateEvent, 3 | NavigationCancelledEvent, 4 | NavigationEvent, 5 | State, 6 | FullModeHistoryApi, 7 | Events 8 | } from '../types.js'; 9 | import { isConformantState } from './isConformantState.js'; 10 | import { StockHistoryApi } from './StockHistoryApi.svelte.js'; 11 | import { logger } from './Logger.js'; 12 | 13 | /** 14 | * HistoryApi implementation that intercepts navigation calls to provide beforeNavigate 15 | * and navigationCancelled events. Used by LocationFull for advanced navigation control. 16 | */ 17 | export class InterceptedHistoryApi extends StockHistoryApi implements FullModeHistoryApi { 18 | #eventSubs: Record> = { 19 | beforeNavigate: {}, 20 | navigationCancelled: {} 21 | }; 22 | #nextSubId = 0; 23 | #origReplaceState; 24 | #origPushState; 25 | 26 | constructor(initialUrl?: string, initialState?: State) { 27 | super(initialUrl, initialState); 28 | if (globalThis.window) { 29 | this.#origReplaceState = globalThis.window.history.replaceState; 30 | this.#origPushState = globalThis.window.history.pushState; 31 | globalThis.window.history.replaceState = this.replaceState.bind(this); 32 | globalThis.window.history.pushState = this.pushState.bind(this); 33 | } 34 | } 35 | 36 | pushState(data: any, unused: string, url?: string | URL | null): void { 37 | this.#navigate('push', data, unused, url); 38 | } 39 | 40 | replaceState(data: any, unused: string, url?: string | URL | null): void { 41 | this.#navigate('replace', data, unused, url); 42 | } 43 | 44 | #navigate( 45 | method: NavigationEvent['method'], 46 | state: any, 47 | unused: string, 48 | url?: string | URL | null 49 | ) { 50 | const urlString = url?.toString() || ''; 51 | const event: BeforeNavigateEvent = { 52 | url: urlString, 53 | state, 54 | method, 55 | wasCancelled: false, 56 | cancelReason: undefined, 57 | cancel: (cause) => { 58 | if (event.wasCancelled) { 59 | return; 60 | } 61 | event.wasCancelled = true; 62 | event.cancelReason = cause; 63 | } 64 | }; 65 | 66 | // Notify beforeNavigate listeners 67 | for (let sub of Object.values(this.#eventSubs.beforeNavigate)) { 68 | sub(event); 69 | } 70 | 71 | if (event.wasCancelled) { 72 | // Notify navigationCancelled listeners 73 | for (let sub of Object.values(this.#eventSubs.navigationCancelled)) { 74 | sub({ 75 | url: urlString, 76 | state: event.state, 77 | method, 78 | cause: event.cancelReason 79 | }); 80 | } 81 | } else { 82 | if (!isConformantState(event.state)) { 83 | logger.warn( 84 | `Warning: Non-conformant state object passed to history.${method}State. Previous state will prevail.` 85 | ); 86 | event.state = this.state; 87 | } 88 | (method === 'push' ? this.#origPushState : this.#origReplaceState)?.bind(globalThis.window.history)(event.state, unused, url); 89 | this.url.href = globalThis.window?.location?.href ?? new URL(url ?? '', this.url).href; 90 | this.state = event.state as State; 91 | } 92 | } 93 | 94 | /** 95 | * Subscribe to navigation events. 96 | */ 97 | on(event: 'beforeNavigate', callback: (event: BeforeNavigateEvent) => void): () => void; 98 | on( 99 | event: 'navigationCancelled', 100 | callback: (event: NavigationCancelledEvent) => void 101 | ): () => void; 102 | on(event: Events, callback: Function): () => void { 103 | const id = ++this.#nextSubId; 104 | this.#eventSubs[event][id] = callback; 105 | return () => delete this.#eventSubs[event][id]; 106 | } 107 | 108 | dispose(): void { 109 | // Clear event subscriptions 110 | this.#eventSubs = { 111 | beforeNavigate: {}, 112 | navigationCancelled: {} 113 | }; 114 | if (globalThis.window) { 115 | globalThis.window.history.replaceState = this.#origReplaceState!; 116 | globalThis.window.history.pushState = this.#origPushState!; 117 | } 118 | super.dispose(); 119 | } 120 | } 121 | --------------------------------------------------------------------------------