@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 +99 -21
- package/README.md +102 -8
- package/package.json +6 -1
- package/src/lib/Router.svelte +18 -6
- 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 +13 -13
- 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,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,
|
|
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
|
@@ -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
|
|
1296
|
+
{@const zoneRouteContext = zoneComponentData.routeContext}
|
|
1297
1297
|
{#if zoneParams}
|
|
1298
|
-
<Comp routeParams={zoneParams} routeContext={
|
|
1298
|
+
<Comp routeParams={zoneParams} routeContext={zoneRouteContext} {...zoneProps} />
|
|
1299
1299
|
{:else}
|
|
1300
|
-
<Comp routeContext={
|
|
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
|
|
1316
|
-
|
|
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
|
-
|
|
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 {
|
|
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)
|
|
@@ -93,7 +93,7 @@ export function updateRouteMetadata(routeContext = {}, location = '', querystrin
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
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 {
|
|
142
|
+
* import { routeContext } from '@keenmate/svelte-spa-router/helpers/route-metadata'
|
|
143
143
|
*
|
|
144
|
-
* const routeContext = $derived(
|
|
144
|
+
* const routeContext = $derived(routeContext())
|
|
145
145
|
* const customData = $derived(routeContext.myCustomField)
|
|
146
146
|
* ```
|
|
147
147
|
*/
|
|
148
|
-
export function
|
|
149
|
-
return
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
...
|
|
238
|
+
currentRouteContext = {
|
|
239
|
+
...currentRouteContext,
|
|
240
240
|
title
|
|
241
241
|
}
|
|
242
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,
|