@keenmate/svelte-spa-router 5.1.1 → 5.2.0-rc01

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,105 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.2.0-rc01] - 2026-02-18
9
+
10
+ ### Fixed
11
+ - **`routeContext()` function missing / mangled name** (Issue #3) — The exported function was named `routerouteContext()` instead of `routeContext()` due to a find-replace accident during the `userData` → `routeContext` rename. The README also referenced the old name `routeUserData()`.
12
+ - Renamed `routerouteContext()` → `routeContext()` in `route-metadata.svelte.js` (function + all internal variable references)
13
+ - Updated `route-metadata.d.ts` type declaration to match
14
+ - Fixed README.md: `routeUserData` → `routeContext` in all import examples and API reference
15
+
16
+ - **`wrap()` not merging title/breadcrumbs into routeContext** (Issue #3) — `routeTitle()` and `routeBreadcrumbs()` returned empty values for routes defined with `wrap({ title, breadcrumbs })` because `wrap()` never merged these into `routeContext`. The Router's `pipelineComputeMetadata()` only reads from `routeItem.routeContext`, so title and breadcrumbs were silently lost.
17
+ - Fixed in both single-component and zones code paths in `wrap.js`
18
+ - `createRouteDefinition()` already had the merge logic — only `wrap()` was missing it
19
+
20
+ - **Logger TypeScript errors** — Added `.d.ts` type declarations for vendored loglevel libraries
21
+ - Created `src/lib/vendor/loglevel/index.d.ts` and `prefix.d.ts`
22
+ - Removed `@ts-ignore` comments from `logger.ts`
23
+ - `svelte-check` now passes with 0 errors and 0 warnings
24
+
25
+ - **Missing TypeScript declarations for GlobalErrorHandler and ErrorDisplay** (Issue #4) — `Cannot find module '@keenmate/svelte-spa-router/helpers/GlobalErrorHandler' or its corresponding type declarations`
26
+ - Created `GlobalErrorHandler.svelte.d.ts` with `GlobalErrorHandlerProps` and `ErrorComponentProps` interfaces
27
+ - Created `ErrorDisplay.svelte.d.ts` with `ErrorDisplayProps` interface
28
+ - Added `types` field to `./helpers/GlobalErrorHandler` export in `package.json`
29
+ - Added new `./helpers/ErrorDisplay` export to `package.json` (was not exported at all)
30
+
31
+ - **Missing TypeScript declarations for `setHierarchicalRoutesEnabled` and `setIncludeReferrer`** (Issue #5) — `Module has no exported member 'setHierarchicalRoutesEnabled'`
32
+ - Added `setHierarchicalRoutesEnabled()`, `getHierarchicalRoutesEnabled()`, `setIncludeReferrer()`, and `getIncludeReferrer()` declarations to `utils.d.ts`
33
+ - Removed phantom `active.svelte.js` re-export from `index.d.ts` (type declared an export that didn't exist at runtime)
34
+
35
+ ### Added
36
+ - **`defineRoutes()` — Type-safe route definitions** (Issue #2) - Single source of truth for routes, navigation, and URL building
37
+ - Returns `routes` (for `<Router>`), `nav` (navigation helpers), and `paths` (URL builders)
38
+ - Full TypeScript support with IDE autocomplete on route names and parameters
39
+ - Extracts `:param` names from path patterns at the type level — catches typos at compile time
40
+ - `nav.X.push(params)` / `nav.X.replace(params)` — programmatic navigation with autocomplete
41
+ - `nav.X.link(params)` — returns object for `use:link` action
42
+ - `paths.X(params)` — builds URL string for `href` attributes
43
+ - Smart optimization: sync components without options skip `wrap()` overhead
44
+ - Async components and routes with options automatically use `createRoute()`
45
+ - Automatically calls `registerRoutes()` — no separate registration step needed
46
+ - Supports all existing route options: `conditions`, `breadcrumbs`, `permissions`, `loadingComponent`, `props`, `title`, etc.
47
+ - Example:
48
+ ```javascript
49
+ import { defineRoutes } from '@keenmate/svelte-spa-router/routes'
50
+
51
+ const { routes, nav, paths } = defineRoutes({
52
+ home: { path: '/', component: Home },
53
+ user: { path: '/user/:id', component: () => import('./User.svelte') }
54
+ })
55
+
56
+ // <Router {routes} />
57
+ // nav.user.push({ id: 123 }) — autocomplete on 'id'
58
+ // <a href={paths.user({ id: 123 })} use:link>
59
+ ```
60
+
61
+ - **Route Context Demo pages** — Example pages demonstrating `routeContext()`, `routeTitle()`, and `routeBreadcrumbs()` with live output
62
+ - `example/src/routes/RouteContextDemo.svelte` — explains routeContext, shows live values, code examples
63
+ - `example/src/routes/RouteContextTarget.svelte` — target page reached via button, displays its own routeContext
64
+ - Both wired into App.svelte with nav link in Routing dropdown
65
+
66
+ - **Comprehensive test suite** — Expanded from ~156 to 347 passing tests across 16 test files (0 skipped)
67
+ - **New test files:**
68
+ - `route-metadata.test.js` (26 tests) — `updateRouteMetadata`, `routeContext()`, `routeTitle()`, `routeBreadcrumbs()`, `updateBreadcrumb()`, `updateTitle()`, `clearBreadcrumbCache()`, loading state functions
69
+ - `navigation-guard.test.js` (22 tests) — `NavigationCancelledError`, `registerBeforeLeave()`, `runBeforeLeaveGuards()`, `createDirtyCheckGuard()`
70
+ - `error-handler.test.js` (23 tests) — `configureGlobalErrorHandler()`, error state, `shouldIgnoreError()`, restart loop prevention, `createErrorInfo()`, `createRecoveryHelpers()`
71
+ - `filters.test.js` (18 tests) — `configureFilters()`, `filters()` flat/structured modes, `updateFilters()`, custom parse/stringify round-trip
72
+ - `querystring-shared.test.js` (7 tests) — `configureQuerystring()`, `query()` with arrayFormat/arrays config
73
+ - `zones-and-scroll.test.js` (12 tests) — `getZoneComponent()`, `setZoneComponents()`, `restoreScroll()`
74
+ - `logger.test.js` (13 tests) — `enableLogging()`, `disableLogging()`, `setLogLevel()`, `setCategoryLevel()` for all 12 categories, `logStructured()`
75
+ - **Extended test files:**
76
+ - `wrap.test.js` (9 → 42 tests) — zones mode, inheritance flags, `createRouteDefinition()`, `createRoute()`, sync component wrapping, condition normalization, validation errors
77
+ - `navigation.test.js` (10 → 27 tests) — `goBack()`, `loc()`, `routeParams()`/`setParams()`, `navigationContext()`/`setNavigationContext()`, array/object/multi-param push formats, `setIncludeReferrer()`, `setParamReplacementPlaceholder()`
78
+ - `permissions.test.js` (13 → 33 tests) — `createProtectedRouteDefinition()`, `authorizationCallback` execution order/fail-fast, `getUnauthorizedBehavior/Route/Component/Handler()`, `hasExplicitHandler()`, `all:` permission requirement
79
+ - **Removed 34 `it.skip` stubs** that required Svelte component rendering or real browser DOM (deleted 3 empty test files: Router.test.js, hierarchical-routes.test.js, link-action.test.js; trimmed active-action.test.js and querystring-helpers.test.js)
80
+
81
+ ### Documentation
82
+ - **defineRoutes() example page** — Added interactive demo page to example app (`example/src/routes/DefineRoutesDemo.svelte`)
83
+ - Covers basic usage, navigation helpers, path builders, and supported route options
84
+ - Includes interactive playground with real-time output
85
+ - Shows before/after comparison with manual route definitions
86
+ - **Example app navbar rework** — Replaced flat navigation with grouped dropdown menus
87
+ - 5 dropdown groups: Navigation, URL & Data, Routing, Errors, Security
88
+ - CSS hover-based dropdowns (no JavaScript state management)
89
+ - **AI Assistant Documentation** - Added 15 concise text files in `./ai` folder optimized for AI assistants
90
+ - Plain text format (no markdown) with bullet-style structure for efficient AI parsing
91
+ - Files organized by feature: basic-setup, navigation, named-routes, route-params, permissions, guards-conditions, hierarchical-routes, tree-structure, link-actions, error-handling, referrer-tracking, debug-logging, import-patterns, utilities, breadcrumbs
92
+ - Includes correct/incorrect usage patterns (✅/❌) for common mistakes
93
+ - Code examples designed for copy-paste usage
94
+ - Complements CLAUDE.md by providing quick-reference documentation
95
+ - Aimed at helping AI coding assistants (like Claude, Cursor, Copilot) quickly understand router functionality
96
+ - **Breadcrumbs Documentation** - Added comprehensive `ai/breadcrumbs.txt` covering breadcrumb navigation system
97
+ - Basic breadcrumb definition and structure
98
+ - Accessing breadcrumbs in components via `routeBreadcrumbs()` helper
99
+ - Breadcrumb component examples with navigation and styling
100
+ - Dynamic breadcrumb updates using `updateBreadcrumb(id, updates)` after data loads
101
+ - Integration with route parameters for dynamic segments
102
+ - Hierarchical breadcrumb inheritance with automatic concatenation
103
+ - Tree structure support with `createHierarchy()`
104
+ - Best practices and common patterns
105
+ - Debugging with ROUTER:METADATA logging category
106
+
8
107
  ## [5.1.1] - 2025-11-30
9
108
 
10
109
  ### Fixed
@@ -35,27 +134,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
35
134
  - Shows common pattern with page definitions array
36
135
  - Explains the "Route X not found in registry" error and how to fix it
37
136
 
38
- ## [Unreleased]
39
-
40
- ### Documentation
41
- - **AI Assistant Documentation** - Added 15 concise text files in `./ai` folder optimized for AI assistants
42
- - Plain text format (no markdown) with bullet-style structure for efficient AI parsing
43
- - Files organized by feature: basic-setup, navigation, named-routes, route-params, permissions, guards-conditions, hierarchical-routes, tree-structure, link-actions, error-handling, referrer-tracking, debug-logging, import-patterns, utilities, breadcrumbs
44
- - Includes correct/incorrect usage patterns (✅/❌) for common mistakes
45
- - Code examples designed for copy-paste usage
46
- - Complements CLAUDE.md by providing quick-reference documentation
47
- - Aimed at helping AI coding assistants (like Claude, Cursor, Copilot) quickly understand router functionality
48
- - **Breadcrumbs Documentation** - Added comprehensive `ai/breadcrumbs.txt` covering breadcrumb navigation system
49
- - Basic breadcrumb definition and structure
50
- - Accessing breadcrumbs in components via `routeBreadcrumbs()` helper
51
- - Breadcrumb component examples with navigation and styling
52
- - Dynamic breadcrumb updates using `updateBreadcrumb(id, updates)` after data loads
53
- - Integration with route parameters for dynamic segments
54
- - Hierarchical breadcrumb inheritance with automatic concatenation
55
- - Tree structure support with `createHierarchy()`
56
- - Best practices and common patterns
57
- - Debugging with ROUTER:METADATA logging category
58
-
59
137
  ## [5.1.0] - 2025-11-20 ✅ Published
60
138
 
61
139
  ### Added
package/README.md CHANGED
@@ -11,6 +11,7 @@ Main features:
11
11
 
12
12
  - **Dual-mode routing**: Supports both hash-based (`#/path`) and history API (`/path`) routing
13
13
  - Built with **Svelte 5 runes** for better reactivity and performance
14
+ - **Type-safe routes**: `defineRoutes()` — single source of truth with IDE autocomplete on route names and params
14
15
  - **TypeScript-first**: Full generic support for `routeParams()`, `query()`, and `filters()` with intellisense
15
16
  - **Flexible Navigation**: Multi-parameter signatures, named routes, navigation context (WinForms-like data passing)
16
17
  - **Referrer Tracking**: Automatic previous route tracking with configurable modes ('never', 'notfound', 'always')
@@ -84,6 +85,9 @@ let { routeParams = {} } = $props()
84
85
  ### Route Configuration
85
86
 
86
87
  ```javascript
88
+ // Type-safe route definitions (recommended!)
89
+ import { defineRoutes } from '@keenmate/svelte-spa-router/routes'
90
+
87
91
  // Wrap routes with loading/conditions
88
92
  import { wrap } from '@keenmate/svelte-spa-router/wrap'
89
93
 
@@ -451,6 +455,96 @@ const routes = {
451
455
  }
452
456
  ```
453
457
 
458
+ ### Define routes with type safety (Recommended)
459
+
460
+ Use `defineRoutes()` for a single source of truth that gives you IDE autocomplete on route names and parameters, preventing typos at compile time:
461
+
462
+ ```javascript
463
+ // src/routes.js (or routes.ts for TypeScript)
464
+ import { defineRoutes } from '@keenmate/svelte-spa-router/routes'
465
+ import Home from './routes/Home.svelte'
466
+
467
+ const { routes, nav, paths } = defineRoutes({
468
+ home: {
469
+ path: '/',
470
+ component: Home
471
+ },
472
+ about: {
473
+ path: '/about',
474
+ component: () => import('./routes/About.svelte')
475
+ },
476
+ user: {
477
+ path: '/user/:id',
478
+ component: () => import('./routes/User.svelte'),
479
+ conditions: [checkAuth],
480
+ breadcrumbs: [{ label: 'Users' }, { id: 'user', label: 'User' }]
481
+ },
482
+ settings: {
483
+ path: '/settings',
484
+ component: () => import('./routes/Settings.svelte'),
485
+ permissions: { any: ['settings.read'] }
486
+ }
487
+ })
488
+
489
+ export { routes, nav, paths }
490
+ ```
491
+
492
+ **Use in App.svelte:**
493
+
494
+ ```svelte
495
+ <script>
496
+ import Router from '@keenmate/svelte-spa-router'
497
+ import { link } from '@keenmate/svelte-spa-router'
498
+ import { routes, nav, paths } from './routes'
499
+ </script>
500
+
501
+ <!-- Pass routes to Router -->
502
+ <Router {routes} />
503
+
504
+ <!-- Links with autocomplete on route names + params -->
505
+ <a href={paths.user({ id: 123 })} use:link>User 123</a>
506
+ <a href={paths.about()} use:link>About</a>
507
+
508
+ <!-- Programmatic navigation -->
509
+ <button onclick={() => nav.user.push({ id: 42 })}>Go to User 42</button>
510
+ <button onclick={() => nav.settings.replace()}>Settings</button>
511
+
512
+ <!-- For use:link action -->
513
+ <a use:link={nav.user.link({ id: 99 })}>User 99</a>
514
+ ```
515
+
516
+ **What `defineRoutes()` returns:**
517
+
518
+ | Property | Description |
519
+ |----------|-------------|
520
+ | `routes` | Standard routes object for `<Router {routes} />` |
521
+ | `nav.X.push(params?, query?, ctx?)` | Navigate to route X (calls `push()` internally) |
522
+ | `nav.X.replace(params?, query?, ctx?)` | Replace with route X (calls `replace()` internally) |
523
+ | `nav.X.link(params?, query?)` | Returns object for `use:link` action |
524
+ | `nav.X.path` | Raw path pattern (e.g. `'/user/:id'`) |
525
+ | `paths.X(params?, query?)` | Build URL string for `href` attributes |
526
+
527
+ **TypeScript support:**
528
+
529
+ In TypeScript, `defineRoutes()` extracts `:param` names from path patterns at the type level:
530
+
531
+ ```typescript
532
+ const { nav, paths } = defineRoutes({
533
+ user: { path: '/user/:id', component: UserPage }
534
+ })
535
+
536
+ nav.user.push({ id: 123 }) // ✅ TypeScript knows 'id' is required
537
+ nav.user.push({ userId: 123 }) // ❌ Type error — 'userId' doesn't exist
538
+ nav.user.push() // ✅ OK — params are optional at runtime
539
+ paths.user({ id: 123 }) // ✅ Returns '/user/123'
540
+ ```
541
+
542
+ **Supported route options:**
543
+
544
+ Each route in `defineRoutes()` accepts `path`, `component`, and all existing `createRoute()` / `wrap()` options: `loadingComponent`, `loadingParams`, `conditions`, `props`, `routeContext`, `title`, `breadcrumbs`, `shouldDisplayLoadingOnRouteLoad`, `permissions`, `authorizationCallback`, and inheritance flags (`inheritBreadcrumbs`, `inheritPermissions`, etc.).
545
+
546
+ > **Note:** `defineRoutes()` automatically calls `registerRoutes()` internally — no separate registration step is needed. Named routes work immediately with `push()`, `replace()`, and `buildUrl()`.
547
+
454
548
  ### Include the router
455
549
 
456
550
  In your main component (usually `App.svelte`):
@@ -1077,11 +1171,11 @@ Access current route metadata reactively:
1077
1171
 
1078
1172
  ```svelte
1079
1173
  <script>
1080
- import { routeTitle, routeBreadcrumbs, routeUserData } from '@keenmate/svelte-spa-router/helpers/route-metadata'
1174
+ import { routeTitle, routeBreadcrumbs, routeContext } from '@keenmate/svelte-spa-router/helpers/route-metadata'
1081
1175
 
1082
1176
  const title = $derived(routeTitle())
1083
1177
  const breadcrumbs = $derived(routeBreadcrumbs())
1084
- const userData = $derived(routeUserData())
1178
+ const context = $derived(routeContext())
1085
1179
  </script>
1086
1180
 
1087
1181
  <h1>{title || 'Default Title'}</h1>
@@ -1115,7 +1209,7 @@ import {
1115
1209
  // Reactive metadata access
1116
1210
  routeTitle, // Get current title
1117
1211
  routeBreadcrumbs, // Get current breadcrumbs
1118
- routeUserData // Get full userData object
1212
+ routeContext // Get full route context object
1119
1213
  } from '@keenmate/svelte-spa-router/helpers/route-metadata'
1120
1214
  ```
1121
1215
 
@@ -1716,12 +1810,12 @@ const routes = {
1716
1810
 
1717
1811
  Define routes in a hierarchical tree structure as an alternative to flat definitions. Child paths are automatically concatenated to parent paths, and routes inherit metadata from parents.
1718
1812
 
1719
- **Enable hierarchical mode first:**
1813
+ **Enable hierarchical mode first** (disabled by default — routes are flat with no inheritance):
1720
1814
  ```javascript
1721
1815
  // main.js - before mounting app
1722
1816
  import { setHierarchicalRoutesEnabled } from '@keenmate/svelte-spa-router'
1723
1817
 
1724
- setHierarchicalRoutesEnabled(true)
1818
+ setHierarchicalRoutesEnabled(true) // default: false
1725
1819
  ```
1726
1820
 
1727
1821
  **Define routes using tree structure:**
@@ -1800,7 +1894,7 @@ import Router from '@keenmate/svelte-spa-router'
1800
1894
  import { push, replace, pop, goBack, location, querystring, routeParams, navigationContext } from '@keenmate/svelte-spa-router'
1801
1895
 
1802
1896
  // Named routes (for use with push/replace/link)
1803
- import { registerRoutes, buildUrl } from '@keenmate/svelte-spa-router/routes'
1897
+ import { registerRoutes, buildUrl, defineRoutes } from '@keenmate/svelte-spa-router/routes'
1804
1898
 
1805
1899
  // Route creation (recommended - no wrap() needed!)
1806
1900
  import { createRoute, createRouteDefinition } from '@keenmate/svelte-spa-router/wrap'
@@ -1815,7 +1909,7 @@ import { createHierarchy } from '@keenmate/svelte-spa-router/helpers/hierarchy'
1815
1909
  import active from '@keenmate/svelte-spa-router/active'
1816
1910
 
1817
1911
  // Configuration
1818
- import { setHashRoutingEnabled, setBasePath, setParamReplacementPlaceholder, setHierarchicalRoutesEnabled } from '@keenmate/svelte-spa-router'
1912
+ import { setHashRoutingEnabled, setBasePath, setParamReplacementPlaceholder, setHierarchicalRoutesEnabled, setIncludeReferrer } from '@keenmate/svelte-spa-router'
1819
1913
 
1820
1914
  // Querystring helpers (shared reactive state)
1821
1915
  import { configureQuerystring, query } from '@keenmate/svelte-spa-router/helpers/querystring'
@@ -1865,7 +1959,7 @@ import {
1865
1959
  updateRouteMetadata,
1866
1960
  routeTitle,
1867
1961
  routeBreadcrumbs,
1868
- routeUserData
1962
+ routeContext
1869
1963
  } from '@keenmate/svelte-spa-router/helpers/route-metadata'
1870
1964
  ```
1871
1965
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/svelte-spa-router",
3
- "version": "5.1.1",
3
+ "version": "5.2.0-rc01",
4
4
  "description": "Router for SPAs using Svelte 5 with runes, dual-mode routing, permissions, and error handling",
5
5
  "main": "./src/lib/index.js",
6
6
  "svelte": "./src/lib/Router.svelte",
@@ -72,8 +72,13 @@
72
72
  "import": "./src/lib/helpers/error-handler.svelte.js"
73
73
  },
74
74
  "./helpers/GlobalErrorHandler": {
75
+ "types": "./src/lib/helpers/GlobalErrorHandler.svelte.d.ts",
75
76
  "svelte": "./src/lib/helpers/GlobalErrorHandler.svelte"
76
77
  },
78
+ "./helpers/ErrorDisplay": {
79
+ "types": "./src/lib/helpers/ErrorDisplay.svelte.d.ts",
80
+ "svelte": "./src/lib/helpers/ErrorDisplay.svelte"
81
+ },
77
82
  "./helpers/hierarchy": {
78
83
  "types": "./src/lib/helpers/hierarchy.d.ts",
79
84
  "import": "./src/lib/helpers/hierarchy.svelte.js"
@@ -1293,11 +1293,11 @@ $effect(() => {
1293
1293
  {@const Comp = zoneComponentData.component}
1294
1294
  {@const zoneParams = zoneComponentData.params}
1295
1295
  {@const zoneProps = zoneComponentData.props}
1296
- {@const zonerouteContext = zoneComponentData.routeContext}
1296
+ {@const zoneRouteContext = zoneComponentData.routeContext}
1297
1297
  {#if zoneParams}
1298
- <Comp routeParams={zoneParams} routeContext={zonerouteContext} {...zoneProps} />
1298
+ <Comp routeParams={zoneParams} routeContext={zoneRouteContext} {...zoneProps} />
1299
1299
  {:else}
1300
- <Comp routeContext={zonerouteContext} {...zoneProps} />
1300
+ <Comp routeContext={zoneRouteContext} {...zoneProps} />
1301
1301
  {/if}
1302
1302
  {/if}
1303
1303
  {:else if component}
@@ -1312,8 +1312,20 @@ $effect(() => {
1312
1312
  {/if}
1313
1313
  {/if}
1314
1314
 
1315
- <!-- Real component (hidden while loading, visible after hideLoading() called) -->
1316
- <div style:display={isWaitingForData ? 'none' : 'block'}>
1315
+ <!-- Real component -->
1316
+ {#if loadingComponent}
1317
+ <!-- Routes with loading: wrapper needed to hide component while loading spinner shows -->
1318
+ <div style:display={isWaitingForData ? 'none' : 'contents'}>
1319
+ {#if componentParams}
1320
+ {@const Comp = component}
1321
+ <Comp routeParams={componentParams} {...componentProps} />
1322
+ {:else}
1323
+ {@const Comp = component}
1324
+ <Comp {...componentProps} />
1325
+ {/if}
1326
+ </div>
1327
+ {:else}
1328
+ <!-- Routes without loading: render directly, no wrapper div -->
1317
1329
  {#if componentParams}
1318
1330
  {@const Comp = component}
1319
1331
  <Comp routeParams={componentParams} {...componentProps} />
@@ -1321,5 +1333,5 @@ $effect(() => {
1321
1333
  {@const Comp = component}
1322
1334
  <Comp {...componentProps} />
1323
1335
  {/if}
1324
- </div>
1336
+ {/if}
1325
1337
  {/if}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Type definitions for ErrorDisplay.svelte component
3
+ */
4
+
5
+ import type { Component } from 'svelte'
6
+ import type { ErrorInfo } from './error-handler.js'
7
+
8
+ /**
9
+ * ErrorDisplay component props
10
+ */
11
+ export interface ErrorDisplayProps {
12
+ /** The error to display */
13
+ error: Error
14
+
15
+ /** Additional error context */
16
+ errorInfo: ErrorInfo | null
17
+
18
+ /** Restart the application */
19
+ onRestart: () => void
20
+
21
+ /** Navigate to the safe route */
22
+ onNavigateSafe: () => void
23
+
24
+ /** Dismiss the error and continue */
25
+ onContinue: () => void
26
+
27
+ /** Whether restart is allowed (not in a restart loop) */
28
+ canRestart: boolean
29
+
30
+ /**
31
+ * Route to navigate to when clicking "Go to Home Page"
32
+ * @default '/'
33
+ */
34
+ safeRoute?: string
35
+
36
+ /**
37
+ * Show detailed technical information (stack trace, error context)
38
+ * @default false
39
+ */
40
+ isDevelopment?: boolean
41
+ }
42
+
43
+ declare const ErrorDisplay: Component<ErrorDisplayProps>
44
+ export default ErrorDisplay
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Type definitions for GlobalErrorHandler.svelte component
3
+ */
4
+
5
+ import type { Component, Snippet } from 'svelte'
6
+ import type { ErrorInfo } from './error-handler.js'
7
+
8
+ /**
9
+ * Props passed to a custom errorComponent snippet
10
+ */
11
+ export interface ErrorComponentProps {
12
+ /** The caught error */
13
+ error: Error
14
+ /** Additional error context */
15
+ errorInfo: ErrorInfo | null
16
+ /** Restart the application */
17
+ onRestart: () => void
18
+ /** Navigate to the safe route */
19
+ onNavigateSafe: () => void
20
+ /** Dismiss the error and continue */
21
+ onContinue: () => void
22
+ /** Whether restart is allowed (not in a restart loop) */
23
+ canRestart: boolean
24
+ }
25
+
26
+ /**
27
+ * GlobalErrorHandler component props
28
+ */
29
+ export interface GlobalErrorHandlerProps {
30
+ /**
31
+ * App content rendered inside the error boundary.
32
+ */
33
+ children: Snippet
34
+
35
+ /**
36
+ * Custom error UI snippet. When provided, replaces the default ErrorDisplay.
37
+ * Receives ErrorComponentProps as its argument.
38
+ * @default null
39
+ */
40
+ errorComponent?: Snippet<[ErrorComponentProps]> | null
41
+ }
42
+
43
+ declare const GlobalErrorHandler: Component<GlobalErrorHandlerProps>
44
+ export default GlobalErrorHandler
@@ -68,14 +68,14 @@ export function routeBreadcrumbs(): BreadcrumbItem[];
68
68
  *
69
69
  * @example
70
70
  * ```typescript
71
- * import { routerouteContext } from '@keenmate/svelte-spa-router/helpers/route-metadata'
71
+ * import { routeContext } from '@keenmate/svelte-spa-router/helpers/route-metadata'
72
72
  *
73
73
  * // In a component
74
- * const routeContext = $derived(routerouteContext())
74
+ * const routeContext = $derived(routeContext())
75
75
  * const customField = routeContext.myCustomField
76
76
  * ```
77
77
  */
78
- export function routerouteContext(): Record<string, any>;
78
+ export function routeContext(): Record<string, any>;
79
79
 
80
80
  /**
81
81
  * Hide the loading screen
@@ -11,9 +11,9 @@ import { metadataLogger } from '../logger.ts'
11
11
  * Current route metadata state
12
12
  * Single source of truth - other values derive from this
13
13
  */
14
- let currentRouterouteContext = $state({})
15
- let currentRouteTitle = $derived(currentRouterouteContext.title || '')
16
- let currentRouteBreadcrumbs = $derived(currentRouterouteContext.breadcrumbs || [])
14
+ let currentRouteContext = $state({})
15
+ let currentRouteTitle = $derived(currentRouteContext.title || '')
16
+ let currentRouteBreadcrumbs = $derived(currentRouteContext.breadcrumbs || [])
17
17
 
18
18
  /**
19
19
  * Loading control state
@@ -32,7 +32,7 @@ let currentBasePath = null // Track base path to clear cache on major route chan
32
32
  * Cache for manually updated breadcrumbs
33
33
  * Maps breadcrumb ID to updated breadcrumb data
34
34
  */
35
- let updatedBreadcrumbsCache = new Map()
35
+ const updatedBreadcrumbsCache = new Map()
36
36
 
37
37
  /**
38
38
  * Update route metadata (called by Router or user code)
@@ -93,7 +93,7 @@ export function updateRouteMetadata(routeContext = {}, location = '', querystrin
93
93
  }
94
94
  }
95
95
 
96
- currentRouterouteContext = finalContext
96
+ currentRouteContext = finalContext
97
97
  currentRouteKey = fullRouteKey
98
98
  currentBasePath = basePath
99
99
  metadataLogger.debug('[updateRouteMetadata] Route changed to:', fullRouteKey)
@@ -139,14 +139,14 @@ export function routeBreadcrumbs() {
139
139
  *
140
140
  * @example
141
141
  * ```javascript
142
- * import { routerouteContext } from '@keenmate/svelte-spa-router/helpers/route-metadata'
142
+ * import { routeContext } from '@keenmate/svelte-spa-router/helpers/route-metadata'
143
143
  *
144
- * const routeContext = $derived(routerouteContext())
144
+ * const routeContext = $derived(routeContext())
145
145
  * const customData = $derived(routeContext.myCustomField)
146
146
  * ```
147
147
  */
148
- export function routerouteContext() {
149
- return currentRouterouteContext
148
+ export function routeContext() {
149
+ return currentRouteContext
150
150
  }
151
151
 
152
152
  /**
@@ -181,7 +181,7 @@ export function updateBreadcrumb(id, updates) {
181
181
  metadataLogger.debug('[updateBreadcrumb] Cached update for id:', id, 'updates:', updates)
182
182
 
183
183
  // Get snapshot to work with plain values (not proxies)
184
- const currentContext = $state.snapshot(currentRouterouteContext)
184
+ const currentContext = $state.snapshot(currentRouteContext)
185
185
  const breadcrumbs = [...(currentContext.breadcrumbs || [])]
186
186
  const index = breadcrumbs.findIndex(crumb => crumb.id === id)
187
187
  metadataLogger.debug('[updateBreadcrumb] Found at index:', index)
@@ -192,7 +192,7 @@ export function updateBreadcrumb(id, updates) {
192
192
  ...updates
193
193
  }
194
194
  metadataLogger.debug('[updateBreadcrumb] Updated breadcrumb to:', breadcrumbs[index])
195
- currentRouterouteContext = {
195
+ currentRouteContext = {
196
196
  ...currentContext,
197
197
  breadcrumbs
198
198
  }
@@ -235,8 +235,8 @@ export function clearBreadcrumbCache() {
235
235
  * ```
236
236
  */
237
237
  export function updateTitle(title) {
238
- currentRouterouteContext = {
239
- ...currentRouterouteContext,
238
+ currentRouteContext = {
239
+ ...currentRouteContext,
240
240
  title
241
241
  }
242
242
  // Title updates don't affect breadcrumbs flag
@@ -6,7 +6,6 @@
6
6
  export { default as Router } from './Router.svelte';
7
7
 
8
8
  // Core utilities
9
- export * from './active.svelte.js';
10
9
  export * from './utils';
11
10
  export * from './constants';
12
11
  export { default as wrap } from './wrap';
package/src/lib/logger.ts CHANGED
@@ -36,9 +36,7 @@
36
36
  */
37
37
 
38
38
  // Import vendored libraries via ES module wrappers
39
- // @ts-ignore - Vendored library without type definitions
40
39
  import log from './vendor/loglevel/index.js';
41
- // @ts-ignore - Vendored library without type definitions
42
40
  import prefix from './vendor/loglevel/prefix.js';
43
41
 
44
42
  // Define color scheme
@@ -74,3 +74,104 @@ export function buildUrl(
74
74
  * @returns True if route is registered
75
75
  */
76
76
  export function hasRoute(name: string): boolean;
77
+
78
+ /**
79
+ * Get the pattern for a registered route by name
80
+ *
81
+ * @param name - Route name
82
+ * @returns Route pattern or undefined if not registered
83
+ */
84
+ export function getRouteByName(name: string): string | undefined;
85
+
86
+ // --- defineRoutes types ---
87
+
88
+ /** Extract :param names from a route path pattern */
89
+ type ExtractParams<T extends string> =
90
+ T extends `${string}:${infer Param}/${infer Rest}`
91
+ ? { [K in Param]: string | number } & ExtractParams<`/${Rest}`>
92
+ : T extends `${string}:${infer Param}`
93
+ ? { [K in Param]: string | number }
94
+ : Record<string, never>;
95
+
96
+ /** Route definition for defineRoutes() */
97
+ interface RouteDefinition {
98
+ path: string;
99
+ component: any;
100
+ loadingComponent?: any;
101
+ loadingParams?: Record<string, any>;
102
+ conditions?: Function | Function[];
103
+ props?: Record<string, any>;
104
+ routeContext?: Record<string, any>;
105
+ title?: string;
106
+ breadcrumbs?: Array<{ label: string; path?: string; id?: string }>;
107
+ shouldDisplayLoadingOnRouteLoad?: boolean;
108
+ permissions?: { any?: string[]; all?: string[] };
109
+ authorizationCallback?: Function;
110
+ inheritBreadcrumbs?: boolean;
111
+ inheritPermissions?: boolean;
112
+ inheritConditions?: boolean;
113
+ inheritAuthorization?: boolean;
114
+ }
115
+
116
+ /** Navigation helper for a single route */
117
+ interface RouteNav<Path extends string> {
118
+ push(
119
+ params?: ExtractParams<Path>,
120
+ query?: Record<string, any>,
121
+ navigationContext?: any
122
+ ): Promise<void>;
123
+ replace(
124
+ params?: ExtractParams<Path>,
125
+ query?: Record<string, any>,
126
+ navigationContext?: any
127
+ ): Promise<void>;
128
+ link(
129
+ params?: ExtractParams<Path>,
130
+ query?: Record<string, any>
131
+ ): { route: string; params?: any; query?: any };
132
+ readonly path: Path;
133
+ }
134
+
135
+ /** Return type of defineRoutes() */
136
+ interface DefineRoutesResult<T extends Record<string, RouteDefinition>> {
137
+ routes: Record<string, any>;
138
+ nav: {
139
+ [K in keyof T]: RouteNav<T[K]['path']>;
140
+ };
141
+ paths: {
142
+ [K in keyof T]: (
143
+ params?: ExtractParams<T[K]['path']>,
144
+ query?: Record<string, any>
145
+ ) => string;
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Define routes as a single source of truth, returning the routes object
151
+ * for <Router>, navigation helpers with autocomplete, and path builders.
152
+ *
153
+ * @param definitions - Route definitions keyed by name
154
+ * @returns Routes object, navigation helpers, and path builders
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * const { routes, nav, paths } = defineRoutes({
159
+ * home: { path: '/', component: Home },
160
+ * user: { path: '/user/:id', component: () => import('./User.svelte') },
161
+ * about: { path: '/about', component: () => import('./About.svelte') }
162
+ * })
163
+ *
164
+ * // Navigate with autocomplete on route names and params
165
+ * nav.user.push({ id: 123 })
166
+ * nav.home.replace()
167
+ *
168
+ * // Build URLs for links
169
+ * paths.user({ id: 123 }) // '/user/123'
170
+ *
171
+ * // For use:link action
172
+ * nav.user.link({ id: 123 }) // { route: 'user', params: { id: 123 } }
173
+ * ```
174
+ */
175
+ export function defineRoutes<const T extends Record<string, RouteDefinition>>(
176
+ definitions: T
177
+ ): DefineRoutesResult<T>;
@@ -7,7 +7,8 @@
7
7
  * 3. Use named routes in the link action
8
8
  */
9
9
 
10
- import { getParamReplacementPlaceholder } from './utils.svelte.js'
10
+ import { getParamReplacementPlaceholder, push as navPush, replace as navReplace } from './utils.svelte.js'
11
+ import { createRoute } from './wrap.js'
11
12
 
12
13
  // Route registry - maps route names to path patterns
13
14
  let routeRegistry = $state({})
@@ -130,3 +131,77 @@ export function hasRoute(name) {
130
131
  export function getRouteByName(name) {
131
132
  return routeRegistry[name]
132
133
  }
134
+
135
+ /**
136
+ * Define routes as a single source of truth, returning the routes object
137
+ * for <Router>, navigation helpers with autocomplete, and path builders.
138
+ *
139
+ * @param {Object.<string, {path: string, component: any, [key: string]: any}>} definitions - Route definitions keyed by name
140
+ * @returns {{routes: Object, nav: Object, paths: Object}} Routes object, navigation helpers, and path builders
141
+ *
142
+ * @example
143
+ * ```javascript
144
+ * const { routes, nav, paths } = defineRoutes({
145
+ * home: { path: '/', component: Home },
146
+ * user: { path: '/user/:id', component: () => import('./User.svelte') },
147
+ * about: { path: '/about', component: () => import('./About.svelte'), conditions: [checkAuth] }
148
+ * })
149
+ *
150
+ * // Use routes with Router
151
+ * <Router {routes} />
152
+ *
153
+ * // Navigate with autocomplete
154
+ * nav.user.push({ id: 123 })
155
+ * nav.home.replace()
156
+ *
157
+ * // Build URLs for links
158
+ * <a href={paths.user({ id: 123 })} use:link>User 123</a>
159
+ *
160
+ * // For use:link action
161
+ * <a use:link={nav.user.link({ id: 123 })}>User 123</a>
162
+ * ```
163
+ */
164
+ export function defineRoutes(definitions) {
165
+ const routes = {}
166
+ const routeMap = {}
167
+ const nav = {}
168
+ const paths = {}
169
+
170
+ for (const [name, config] of Object.entries(definitions)) {
171
+ const { path, component, ...options } = config
172
+
173
+ // Build routes object for <Router>
174
+ const hasOptions = Object.keys(options).length > 0
175
+ const isAsync = typeof component === 'function' && component.length === 0
176
+
177
+ if (!hasOptions && !isAsync) {
178
+ // Simple sync component — use directly (no wrap overhead)
179
+ routes[path] = component
180
+ } else {
181
+ // Has options or async component — use createRoute()
182
+ routes[path] = createRoute({ component, ...options })
183
+ }
184
+
185
+ // Track for named route registration
186
+ routeMap[name] = path
187
+
188
+ // Build nav helper
189
+ nav[name] = {
190
+ push: (params, query, navigationContext) =>
191
+ navPush(name, params, query, navigationContext),
192
+ replace: (params, query, navigationContext) =>
193
+ navReplace(name, params, query, navigationContext),
194
+ link: (params, query) =>
195
+ ({ route: name, params, query }),
196
+ path
197
+ }
198
+
199
+ // Build path helper
200
+ paths[name] = (params, query) => buildUrl(name, params, query)
201
+ }
202
+
203
+ // Register all named routes
204
+ registerRoutes(routeMap)
205
+
206
+ return { routes, nav, paths }
207
+ }
@@ -96,6 +96,51 @@ export function setParamReplacementPlaceholder(value: string): void;
96
96
  */
97
97
  export function getParamReplacementPlaceholder(): string;
98
98
 
99
+ /**
100
+ * Enable or disable hierarchical route inheritance
101
+ * Must be called before app initialization
102
+ *
103
+ * When enabled, child routes automatically inherit breadcrumbs, permissions,
104
+ * conditions, and authorization callbacks from parent routes.
105
+ *
106
+ * @param value - true to enable hierarchical mode, false for flat mode (default: false)
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * import { setHierarchicalRoutesEnabled } from '@keenmate/svelte-spa-router'
111
+ * setHierarchicalRoutesEnabled(true)
112
+ * ```
113
+ */
114
+ export function setHierarchicalRoutesEnabled(value: boolean): void;
115
+
116
+ /**
117
+ * Get current hierarchical routes mode
118
+ *
119
+ * @returns true if hierarchical mode is enabled
120
+ */
121
+ export function getHierarchicalRoutesEnabled(): boolean;
122
+
123
+ /**
124
+ * Configure automatic referrer tracking in navigationContext
125
+ * Must be called before app initialization
126
+ *
127
+ * @param value - 'never' (default), 'notfound' (404 only), or 'always' (all routes)
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * import { setIncludeReferrer } from '@keenmate/svelte-spa-router'
132
+ * setIncludeReferrer('always')
133
+ * ```
134
+ */
135
+ export function setIncludeReferrer(value: 'never' | 'notfound' | 'always'): void;
136
+
137
+ /**
138
+ * Get current referrer tracking mode
139
+ *
140
+ * @returns Current mode: 'never', 'notfound', or 'always'
141
+ */
142
+ export function getIncludeReferrer(): 'never' | 'notfound' | 'always';
143
+
99
144
  /**
100
145
  * Get the current location path
101
146
  *
@@ -380,29 +380,6 @@ function navigate(location, shouldReplace = false, context = null) {
380
380
  // Manually trigger hashchange event
381
381
  window.dispatchEvent(new HashChangeEvent('hashchange'))
382
382
  } else {
383
- // Try to save context in history state for back/forward support
384
- let processedNavigationContext = context
385
- let shouldSaveContext = true
386
-
387
- try {
388
- // First try structured clone
389
- if (context !== null) {
390
- structuredClone(context)
391
- }
392
- } catch {
393
- // If structured clone fails, try JSON serialization
394
- try {
395
- if (context !== null) {
396
- const jsonString = JSON.stringify(context)
397
- processedNavigationContext = JSON.parse(jsonString)
398
- }
399
- } catch (jsonError) {
400
- // If JSON also fails, don't save context
401
- console.warn('Navigation context data cannot be stored in history (not serializable). Navigation context will not persist on back/forward navigation.', jsonError)
402
- shouldSaveContext = false
403
- }
404
- }
405
-
406
383
  try {
407
384
  // Save CURRENT context (with referrer) to current history entry before navigating
408
385
  const currentContext = navigationContextState
@@ -616,7 +593,7 @@ export async function push(location, param2, param3, param4, param5) {
616
593
  }
617
594
 
618
595
  // Build URL from route if needed
619
- let href = opts.route
596
+ const href = opts.route
620
597
  ? buildUrl(opts.route, opts.params, opts.query)
621
598
  : opts.href
622
599
 
@@ -729,7 +706,7 @@ export async function replace(location, param2, param3, param4, param5) {
729
706
  }
730
707
 
731
708
  // Build URL from route if needed
732
- let href = opts.route
709
+ const href = opts.route
733
710
  ? buildUrl(opts.route, opts.params, opts.query)
734
711
  : opts.href
735
712
 
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Type definitions for vendored loglevel library
3
+ */
4
+
5
+ type LogLevelNames = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';
6
+
7
+ interface MethodFactory {
8
+ (methodName: string, logLevel: number, loggerName: string): (...args: any[]) => void;
9
+ }
10
+
11
+ interface Logger {
12
+ trace(...args: any[]): void;
13
+ debug(...args: any[]): void;
14
+ info(...args: any[]): void;
15
+ warn(...args: any[]): void;
16
+ error(...args: any[]): void;
17
+ setLevel(level: LogLevelNames | number): void;
18
+ getLevel(): number;
19
+ methodFactory: MethodFactory;
20
+ getLogger(name: string): Logger;
21
+ }
22
+
23
+ declare const log: Logger;
24
+ export default log;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Type definitions for vendored loglevel-plugin-prefix
3
+ */
4
+
5
+ interface PrefixOptions {
6
+ format?(level: string, name: string | undefined, timestamp: string): string;
7
+ timestampFormatter?(date: Date): string;
8
+ }
9
+
10
+ interface PrefixPlugin {
11
+ reg(logger: any): void;
12
+ apply(logger: any, options?: PrefixOptions): void;
13
+ }
14
+
15
+ declare const prefix: PrefixPlugin;
16
+ export default prefix;
package/src/lib/wrap.js CHANGED
@@ -101,10 +101,15 @@ export function wrap(args) {
101
101
  }
102
102
  }
103
103
 
104
+ // Merge title and breadcrumbs into routeContext
105
+ const zoneRouteContext = { ...(args.routeContext || {}) }
106
+ if (args.title) zoneRouteContext.title = args.title
107
+ if (args.breadcrumbs) zoneRouteContext.breadcrumbs = args.breadcrumbs
108
+
104
109
  // Return zone-based route object
105
110
  return {
106
111
  zones: asyncZones,
107
- routeContext: args.routeContext,
112
+ routeContext: Object.keys(zoneRouteContext).length > 0 ? zoneRouteContext : undefined,
108
113
  conditions: (args.conditions && args.conditions.length) ? args.conditions : undefined,
109
114
  props: (args.props && Object.keys(args.props).length) ? args.props : {},
110
115
  shouldDisplayLoadingOnRouteLoad: args.shouldDisplayLoadingOnRouteLoad || false,
@@ -151,11 +156,16 @@ export function wrap(args) {
151
156
  args.asyncComponent.loadingParams = args.loadingParams || undefined
152
157
  }
153
158
 
159
+ // Merge title and breadcrumbs into routeContext
160
+ const mergedRouteContext = { ...(args.routeContext || {}) }
161
+ if (args.title) mergedRouteContext.title = args.title
162
+ if (args.breadcrumbs) mergedRouteContext.breadcrumbs = args.breadcrumbs
163
+
154
164
  // Returns an object that contains all the functions to execute too
155
165
  // The _sveltesparouter flag is to confirm the object was created by this router
156
166
  const obj = {
157
167
  component: args.asyncComponent,
158
- routeContext: args.routeContext,
168
+ routeContext: Object.keys(mergedRouteContext).length > 0 ? mergedRouteContext : undefined,
159
169
  conditions: (args.conditions && args.conditions.length) ? args.conditions : undefined,
160
170
  props: (args.props && Object.keys(args.props).length) ? args.props : {},
161
171
  shouldDisplayLoadingOnRouteLoad: args.shouldDisplayLoadingOnRouteLoad || false,