@keenmate/svelte-spa-router 5.1.0 → 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 +129 -2
- package/README.md +102 -8
- package/package.json +6 -1
- package/src/lib/Router.svelte +18 -11
- package/src/lib/helpers/ErrorDisplay.svelte.d.ts +44 -0
- package/src/lib/helpers/GlobalErrorHandler.svelte.d.ts +44 -0
- package/src/lib/helpers/route-metadata.d.ts +3 -3
- package/src/lib/helpers/route-metadata.svelte.js +56 -20
- package/src/lib/index.d.ts +0 -1
- package/src/lib/logger.ts +0 -2
- package/src/lib/routes.d.ts +101 -0
- package/src/lib/routes.svelte.js +76 -1
- package/src/lib/utils.d.ts +45 -0
- package/src/lib/utils.svelte.js +2 -25
- package/src/lib/vendor/loglevel/index.d.ts +24 -0
- package/src/lib/vendor/loglevel/prefix.d.ts +16 -0
- package/src/lib/wrap.js +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,136 @@ 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
|
-
## [
|
|
8
|
+
## [5.2.0-rc01] - 2026-02-18
|
|
9
9
|
|
|
10
|
-
|
|
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
|
+
|
|
107
|
+
## [5.1.1] - 2025-11-30
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
- **Breadcrumbs preserved on querystring changes** - Fixed breadcrumbs resetting to "Loading..." when only the querystring changes (e.g., tab navigation)
|
|
111
|
+
- Issue: Changing tabs via `replace('/items/1', {}, { tab: 'settings' })` would reset dynamically updated breadcrumbs back to their initial "Loading..." state
|
|
112
|
+
- Root cause: `updateRouteMetadata()` was resetting the entire route context on any navigation, including querystring-only changes
|
|
113
|
+
- Solution: Detect when only querystring changed (same location + params) and preserve current breadcrumbs instead of resetting
|
|
114
|
+
- Also applies cached breadcrumb updates when the route context is updated
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
- **TabsDemo example** - New example demonstrating correct pattern for tabs with querystring state
|
|
118
|
+
- Shows how to use `replace()` instead of `push()` for tab changes
|
|
119
|
+
- Demonstrates dynamic breadcrumbs that persist across tab switches
|
|
120
|
+
- Located at `example/src/routes/TabsDemo.svelte`
|
|
121
|
+
|
|
122
|
+
### Documentation
|
|
123
|
+
- **AI Documentation Index** - Added comprehensive `ai/INDEX.txt` file for quick keyword lookup
|
|
124
|
+
- Organized by topic sections: Getting Started, Imports, Navigation, Named Routes, etc.
|
|
125
|
+
- Includes file descriptions, reading order recommendations, and cross-references
|
|
126
|
+
- Quick problem solving guide mapping common errors to solutions
|
|
127
|
+
- Examples organized by use case (Simple SPA, Admin Dashboard, E-commerce, etc.)
|
|
128
|
+
- **Breadcrumbs with Tabs** - Added "TABS WITH QUERY STRING" section to `ai/breadcrumbs.txt`
|
|
129
|
+
- Documents the pattern for tabs that use querystring for state
|
|
130
|
+
- Explains why `replace()` is necessary to preserve breadcrumbs
|
|
131
|
+
- Includes complete code example
|
|
132
|
+
- **Named Routes in Basic Setup** - Added "ENABLING NAMED ROUTES" section to `ai/basic-setup.txt`
|
|
133
|
+
- Documents that `registerRoutes()` must be called for named route navigation to work
|
|
134
|
+
- Shows common pattern with page definitions array
|
|
135
|
+
- Explains the "Route X not found in registry" error and how to fix it
|
|
136
|
+
|
|
137
|
+
## [5.1.0] - 2025-11-20 ✅ Published
|
|
11
138
|
|
|
12
139
|
### Added
|
|
13
140
|
- **Global Window API:** Added runtime debugging and introspection via `window.components['svelte-spa-router']`
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|
package/src/lib/Router.svelte
CHANGED
|
@@ -595,11 +595,6 @@ function commitToReactiveState(ctx) {
|
|
|
595
595
|
}
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
-
// Debug log AFTER navigationContext is set
|
|
599
|
-
const finalReferrer = ctx.updatedNavigationContext?.referrer?.location || 'none'
|
|
600
|
-
const finalSeq = typeof window !== 'undefined' && window.history.state?.__navigationSequence || 0
|
|
601
|
-
console.log(`🔍 NAV: route="${ctx.location}" referrer="${finalReferrer}" seq=${finalSeq}`)
|
|
602
|
-
|
|
603
598
|
// Update current route tracking (unless catch-all)
|
|
604
599
|
if (!ctx.isCatchAll) {
|
|
605
600
|
currentRoute = ctx.location
|
|
@@ -1298,11 +1293,11 @@ $effect(() => {
|
|
|
1298
1293
|
{@const Comp = zoneComponentData.component}
|
|
1299
1294
|
{@const zoneParams = zoneComponentData.params}
|
|
1300
1295
|
{@const zoneProps = zoneComponentData.props}
|
|
1301
|
-
{@const
|
|
1296
|
+
{@const zoneRouteContext = zoneComponentData.routeContext}
|
|
1302
1297
|
{#if zoneParams}
|
|
1303
|
-
<Comp routeParams={zoneParams} routeContext={
|
|
1298
|
+
<Comp routeParams={zoneParams} routeContext={zoneRouteContext} {...zoneProps} />
|
|
1304
1299
|
{:else}
|
|
1305
|
-
<Comp routeContext={
|
|
1300
|
+
<Comp routeContext={zoneRouteContext} {...zoneProps} />
|
|
1306
1301
|
{/if}
|
|
1307
1302
|
{/if}
|
|
1308
1303
|
{:else if component}
|
|
@@ -1317,8 +1312,20 @@ $effect(() => {
|
|
|
1317
1312
|
{/if}
|
|
1318
1313
|
{/if}
|
|
1319
1314
|
|
|
1320
|
-
<!-- Real component
|
|
1321
|
-
|
|
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 -->
|
|
1322
1329
|
{#if componentParams}
|
|
1323
1330
|
{@const Comp = component}
|
|
1324
1331
|
<Comp routeParams={componentParams} {...componentProps} />
|
|
@@ -1326,5 +1333,5 @@ $effect(() => {
|
|
|
1326
1333
|
{@const Comp = component}
|
|
1327
1334
|
<Comp {...componentProps} />
|
|
1328
1335
|
{/if}
|
|
1329
|
-
|
|
1336
|
+
{/if}
|
|
1330
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 {
|
|
71
|
+
* import { routeContext } from '@keenmate/svelte-spa-router/helpers/route-metadata'
|
|
72
72
|
*
|
|
73
73
|
* // In a component
|
|
74
|
-
* const routeContext = $derived(
|
|
74
|
+
* const routeContext = $derived(routeContext())
|
|
75
75
|
* const customField = routeContext.myCustomField
|
|
76
76
|
* ```
|
|
77
77
|
*/
|
|
78
|
-
export function
|
|
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
|
|
15
|
-
let currentRouteTitle = $derived(
|
|
16
|
-
let currentRouteBreadcrumbs = $derived(
|
|
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
|
-
|
|
35
|
+
const updatedBreadcrumbsCache = new Map()
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Update route metadata (called by Router or user code)
|
|
@@ -42,8 +42,11 @@ let updatedBreadcrumbsCache = new Map()
|
|
|
42
42
|
* @param {Object} params - route params
|
|
43
43
|
*/
|
|
44
44
|
export function updateRouteMetadata(routeContext = {}, location = '', querystring = '', params = {}) {
|
|
45
|
-
// Create a
|
|
46
|
-
const
|
|
45
|
+
// Create a key for the route path (without querystring) to detect actual route changes
|
|
46
|
+
const locationKey = `${location}|${JSON.stringify(params)}`
|
|
47
|
+
|
|
48
|
+
// Create full key including querystring for logging
|
|
49
|
+
const fullRouteKey = `${location}|${querystring}|${JSON.stringify(params)}`
|
|
47
50
|
|
|
48
51
|
// Extract base path (e.g., /documents/1 → /documents, /documents/1/logs → /documents/1)
|
|
49
52
|
// This is a simple heuristic: get path up to the last segment
|
|
@@ -56,12 +59,45 @@ export function updateRouteMetadata(routeContext = {}, location = '', querystrin
|
|
|
56
59
|
clearBreadcrumbCache()
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
currentRouteKey
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Check if only querystring changed (same location and params)
|
|
63
|
+
const onlyQuerystringChanged = currentRouteKey && currentRouteKey.startsWith(locationKey.split('|')[0] + '|')
|
|
64
|
+
&& currentRouteKey.includes('|' + JSON.stringify(params))
|
|
65
|
+
&& currentRouteKey !== fullRouteKey
|
|
66
|
+
|
|
67
|
+
// Only update context if the actual route (location + params) changed, not just querystring
|
|
68
|
+
if (currentRouteKey !== fullRouteKey) {
|
|
69
|
+
if (onlyQuerystringChanged) {
|
|
70
|
+
// Querystring-only change: preserve breadcrumbs, just update the key
|
|
71
|
+
metadataLogger.debug('[updateRouteMetadata] Querystring changed, preserving breadcrumbs')
|
|
72
|
+
currentRouteKey = fullRouteKey
|
|
73
|
+
} else {
|
|
74
|
+
// Actual route change: update context but apply cached breadcrumb updates
|
|
75
|
+
let finalContext = { ...routeContext }
|
|
76
|
+
|
|
77
|
+
// Apply any cached breadcrumb updates to the new context
|
|
78
|
+
if (routeContext.breadcrumbs && updatedBreadcrumbsCache.size > 0) {
|
|
79
|
+
const breadcrumbs = [...routeContext.breadcrumbs]
|
|
80
|
+
let appliedUpdates = false
|
|
81
|
+
|
|
82
|
+
for (const [id, updates] of updatedBreadcrumbsCache) {
|
|
83
|
+
const index = breadcrumbs.findIndex(crumb => crumb.id === id)
|
|
84
|
+
if (index !== -1) {
|
|
85
|
+
breadcrumbs[index] = { ...breadcrumbs[index], ...updates }
|
|
86
|
+
appliedUpdates = true
|
|
87
|
+
metadataLogger.debug('[updateRouteMetadata] Applied cached update for:', id)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (appliedUpdates) {
|
|
92
|
+
finalContext = { ...finalContext, breadcrumbs }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
currentRouteContext = finalContext
|
|
97
|
+
currentRouteKey = fullRouteKey
|
|
98
|
+
currentBasePath = basePath
|
|
99
|
+
metadataLogger.debug('[updateRouteMetadata] Route changed to:', fullRouteKey)
|
|
100
|
+
}
|
|
65
101
|
} else {
|
|
66
102
|
metadataLogger.debug('[updateRouteMetadata] Same route, ignoring update')
|
|
67
103
|
}
|
|
@@ -103,14 +139,14 @@ export function routeBreadcrumbs() {
|
|
|
103
139
|
*
|
|
104
140
|
* @example
|
|
105
141
|
* ```javascript
|
|
106
|
-
* import {
|
|
142
|
+
* import { routeContext } from '@keenmate/svelte-spa-router/helpers/route-metadata'
|
|
107
143
|
*
|
|
108
|
-
* const routeContext = $derived(
|
|
144
|
+
* const routeContext = $derived(routeContext())
|
|
109
145
|
* const customData = $derived(routeContext.myCustomField)
|
|
110
146
|
* ```
|
|
111
147
|
*/
|
|
112
|
-
export function
|
|
113
|
-
return
|
|
148
|
+
export function routeContext() {
|
|
149
|
+
return currentRouteContext
|
|
114
150
|
}
|
|
115
151
|
|
|
116
152
|
/**
|
|
@@ -145,7 +181,7 @@ export function updateBreadcrumb(id, updates) {
|
|
|
145
181
|
metadataLogger.debug('[updateBreadcrumb] Cached update for id:', id, 'updates:', updates)
|
|
146
182
|
|
|
147
183
|
// Get snapshot to work with plain values (not proxies)
|
|
148
|
-
const currentContext = $state.snapshot(
|
|
184
|
+
const currentContext = $state.snapshot(currentRouteContext)
|
|
149
185
|
const breadcrumbs = [...(currentContext.breadcrumbs || [])]
|
|
150
186
|
const index = breadcrumbs.findIndex(crumb => crumb.id === id)
|
|
151
187
|
metadataLogger.debug('[updateBreadcrumb] Found at index:', index)
|
|
@@ -156,7 +192,7 @@ export function updateBreadcrumb(id, updates) {
|
|
|
156
192
|
...updates
|
|
157
193
|
}
|
|
158
194
|
metadataLogger.debug('[updateBreadcrumb] Updated breadcrumb to:', breadcrumbs[index])
|
|
159
|
-
|
|
195
|
+
currentRouteContext = {
|
|
160
196
|
...currentContext,
|
|
161
197
|
breadcrumbs
|
|
162
198
|
}
|
|
@@ -199,8 +235,8 @@ export function clearBreadcrumbCache() {
|
|
|
199
235
|
* ```
|
|
200
236
|
*/
|
|
201
237
|
export function updateTitle(title) {
|
|
202
|
-
|
|
203
|
-
...
|
|
238
|
+
currentRouteContext = {
|
|
239
|
+
...currentRouteContext,
|
|
204
240
|
title
|
|
205
241
|
}
|
|
206
242
|
// Title updates don't affect breadcrumbs flag
|
package/src/lib/index.d.ts
CHANGED
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
|
package/src/lib/routes.d.ts
CHANGED
|
@@ -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>;
|
package/src/lib/routes.svelte.js
CHANGED
|
@@ -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
|
+
}
|
package/src/lib/utils.d.ts
CHANGED
|
@@ -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
|
*
|
package/src/lib/utils.svelte.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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,
|