@iyulab/router 0.7.5 → 0.8.0
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 +165 -0
- package/README.md +79 -213
- package/dist/index.d.ts +40 -7
- package/dist/index.js +23 -12
- package/dist/react.d.ts +11 -3
- package/dist/react.js +1 -1
- package/dist/{share-sbAElOI7.js → share-CUGwxZKa.js} +12 -7
- package/package.json +3 -1
- package/skills/iyulab-router/SKILL.md +145 -0
- package/skills/iyulab-router/references/components.md +43 -0
- package/skills/iyulab-router/references/events-and-errors.md +39 -0
- package/skills/iyulab-router/references/guards-and-metadata.md +50 -0
- package/skills/iyulab-router/references/routing-basics.md +40 -0
- package/skills/iyulab-router/references/url-pattern.md +32 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.8.0] - 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added navigation guards via `enter` hooks at both router level (`RouterConfig.enter`) and route level (`RouteConfig.enter`) with redirect/cancel flow support
|
|
7
|
+
- Added `rel` attribute support to `<u-link>` for secure external navigation patterns (for example `noopener noreferrer`)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Breaking:** Renamed route metadata fields from `meta` to `metadata` (`RouteConfig.metadata`, `RouteContext.metadata`)
|
|
11
|
+
- Updated nested outlet resolution to prefer child outlet discovery inside the current outlet, improving deep nested route rendering behavior
|
|
12
|
+
|
|
13
|
+
## [0.7.6] - 2026-04-02
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Added `skills/` and `CHANGELOG.md` to npm `files` field — both were missing from the published package, making `npx skills add ./node_modules/@iyulab/router` non-functional
|
|
17
|
+
|
|
18
|
+
## [0.7.5] - 2026-04-02
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `@lit/react` promoted from optional peer dependency to direct dependency — React integration now works without a separate `@lit/react` install
|
|
22
|
+
- Added Agent Skills definition (`skills/iyulab-router`) for AI agent tooling support
|
|
23
|
+
|
|
24
|
+
## [0.7.4] - 2026-03-05
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- `findOutlet()` now searches both shadow DOM and light DOM simultaneously
|
|
28
|
+
|
|
29
|
+
## [0.7.3] - 2026-03-05
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **Breaking:** Removed `RenderResult` and `FallbackRenderResult` types — `render` function return type is now `unknown`
|
|
33
|
+
- **Breaking:** Moved React dependencies (`react`, `react-dom`, `@lit/react`) to optional peer dependencies — Lit-only projects no longer require a React install
|
|
34
|
+
- Simplified `findOutlet` — removed shadow/light DOM branching and redundant `querySelector` traversal
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- `UOutlet` now dynamically imports `react-dom/client` — prevents import failure in React-free environments
|
|
38
|
+
- Corrected `UErrorPage` error code string mismatches (`OUTLET_NOT_FOUND` → `OUTLET_MISSING`, `RENDER_FAILED` → `CONTENT_RENDER_FAILED`)
|
|
39
|
+
- Fixed edge case in `catchBasepath` where an empty `restPath` was treated as falsy, causing trailing slashes to be dropped
|
|
40
|
+
- Changed `ULink.getBasepath()` fallback from `""` to `"/"` to prevent incorrect path generation on initial access
|
|
41
|
+
- Applied optional chaining in `Router.go()` catch block to prevent property access errors when a primitive value is thrown
|
|
42
|
+
|
|
43
|
+
## [0.7.2] - 2026-02-09
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- Updated dependencies to latest versions
|
|
47
|
+
|
|
48
|
+
## [0.7.1] - 2026-02-09
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- Fixed silent failure when passing `<u-outlet>` element directly as `Router` root (#1)
|
|
52
|
+
- `findOutlet()` now recognizes the root element itself as a valid outlet
|
|
53
|
+
- Improved `waitOutlet()` timeout error message with root element context for easier debugging
|
|
54
|
+
|
|
55
|
+
## [0.7.0] - 2026-02-09
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
- `meta` field on `RouteConfig` — attach arbitrary key-value data to any route
|
|
59
|
+
- `RouteContext.meta` — populated at navigation time with metadata merged from the full matched route chain (parent → child order, child overrides parent)
|
|
60
|
+
|
|
61
|
+
## [0.6.2] - 2026-01-21
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- **Breaking:** Refactored `UOutlet` from `LitElement` to native `HTMLElement` for improved performance and reduced bundle size
|
|
65
|
+
- **Breaking:** `UErrorPage` CSS custom properties renamed: `--route-icon-color` → `--error-icon-color`, `--route-code-color` → `--error-code-color`, `--route-message-color` → `--error-message-color`
|
|
66
|
+
- `ULink` click event handling moved to host element level for better encapsulation
|
|
67
|
+
- `@lit/react` moved from devDependency to dependency for proper React integration
|
|
68
|
+
- `UErrorPage` replaced inline SVG icon imports with emoji icons; SVG asset files removed
|
|
69
|
+
|
|
70
|
+
## [0.6.1] - 2026-01-20
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
- Added `global.d.ts` import to main entry point
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
- Moved click event listener from root element to document level for more reliable event handling
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
- Fixed `UErrorPage` CSS syntax error (trailing semicolon in CSS block)
|
|
80
|
+
- Fixed initial route loading to wait for outlet element readiness before navigation
|
|
81
|
+
|
|
82
|
+
### Removed
|
|
83
|
+
- Removed unused `computedHref` reactive state from `ULink`
|
|
84
|
+
|
|
85
|
+
## [0.6.0] - 2026-01-15
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
- Dedicated `react.ts` export entry providing React-compatible `UOutlet` and `ULink` wrappers
|
|
89
|
+
- `ULink` now supports the `target` attribute, enabling standard browser behavior (e.g. `_blank`)
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
- Renamed custom elements: `ErrorPage` → `UErrorPage`, `Link` → `ULink`, `Outlet` → `UOutlet`
|
|
93
|
+
- Refactored internal module organization
|
|
94
|
+
|
|
95
|
+
## [0.5.3] - 2025-12-04
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
- Removed restriction that prevented re-navigation to the current URL
|
|
99
|
+
|
|
100
|
+
## [0.5.2] - 2025-11-17
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
- `initialLoad` option on `RouterConfig` — controls whether the router navigates to the current URL on initialization
|
|
104
|
+
- `fallback` option on `RouterConfig` — defines a custom render function for error and not-found states
|
|
105
|
+
- `RouteProgressEvent` dispatched during async route loading
|
|
106
|
+
|
|
107
|
+
### Changed
|
|
108
|
+
- Renamed `RouteInfo` to `RouteContext`; added `progress` callback to context for reporting load progress
|
|
109
|
+
- Improved error handling and display in the built-in error page component
|
|
110
|
+
|
|
111
|
+
### Removed
|
|
112
|
+
- Removed `children` from `NonIndexRouteConfig` to simplify the type interface
|
|
113
|
+
- Removed `route` property from the `window` object to reduce global namespace pollution
|
|
114
|
+
|
|
115
|
+
## [0.5.1] - 2025-11-13
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
- Improved route error handling
|
|
119
|
+
|
|
120
|
+
## [0.5.0] - 2025-11-12
|
|
121
|
+
|
|
122
|
+
### Changed
|
|
123
|
+
- Route `render` functions now support both synchronous and asynchronous return values
|
|
124
|
+
|
|
125
|
+
### Removed
|
|
126
|
+
- Dropped CommonJS build output — ESM only
|
|
127
|
+
|
|
128
|
+
## [0.4.0] - 2025-11-11
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
- `destroy()` method on `Router` to cleanly remove event listeners
|
|
132
|
+
- `useIntercept` option on `RouterConfig` — controls whether anchor tag clicks are intercepted for client-side routing
|
|
133
|
+
- Global type declarations (`global.d.ts`)
|
|
134
|
+
|
|
135
|
+
## [0.3.0] - 2025-10-28
|
|
136
|
+
|
|
137
|
+
### Changed
|
|
138
|
+
- **Breaking:** Complete router architecture overhaul
|
|
139
|
+
- **Breaking:** `RouteConfig` split into `IndexRouteConfig` and `PathRouteConfig`
|
|
140
|
+
- **Breaking:** `RouteError` converted to a class; added `NotFoundRouteError`
|
|
141
|
+
- **Breaking:** Route event names changed: `route-start` → `route-begin`, `route-end` → `route-done`
|
|
142
|
+
- Simplified `Outlet` rendering via a unified `renderContent` method
|
|
143
|
+
|
|
144
|
+
### Added
|
|
145
|
+
- Improved `ErrorPage` component styling and usability
|
|
146
|
+
- Unified route rendering using render functions
|
|
147
|
+
|
|
148
|
+
## [0.2.1] - 2025-10-27
|
|
149
|
+
|
|
150
|
+
### Changed
|
|
151
|
+
- Refactored routing mechanism for improved performance
|
|
152
|
+
- Enhanced error handling with custom error pages
|
|
153
|
+
|
|
154
|
+
### Removed
|
|
155
|
+
- Removed `connect()` and `disconnect()` methods from `Router`
|
|
156
|
+
- Removed `notfound` configuration option
|
|
157
|
+
- Removed legacy route progress events
|
|
158
|
+
- added ErrorPage component for error handling
|
|
159
|
+
- added Route events: `route-start`, `route-end`, `route-error`
|
|
160
|
+
- improved TypeScript types and interfaces
|
|
161
|
+
|
|
162
|
+
## [0.1.0] - 2025-04-25
|
|
163
|
+
|
|
164
|
+
### Added
|
|
165
|
+
- Initial release
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @iyulab/router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Client-side SPA router for Lit and React with URLPattern matching, nested routes, and route lifecycle events.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,270 +10,136 @@ npm install @iyulab/router
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
-
### Basic Setup
|
|
14
|
-
|
|
15
13
|
```typescript
|
|
16
14
|
import { Router } from '@iyulab/router';
|
|
17
15
|
import { html } from 'lit';
|
|
18
16
|
|
|
19
17
|
const router = new Router({
|
|
18
|
+
root: document.body,
|
|
20
19
|
basepath: '/',
|
|
21
20
|
routes: [
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
render: () => html`<home-page></home-page>`
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
path: '/user/:id', // URLPattern route
|
|
28
|
-
render: (routeInfo) => html`<user-page .userId=${routeInfo.params.id}></user-page>`
|
|
29
|
-
}
|
|
21
|
+
{ index: true, render: () => html`<home-page></home-page>` },
|
|
22
|
+
{ path: '/users/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
|
|
30
23
|
],
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### Mixed Framework Support
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
import React from 'react';
|
|
38
|
-
|
|
39
|
-
const routes = [
|
|
40
|
-
// Lit component
|
|
41
|
-
{
|
|
42
|
-
path: '/lit-page',
|
|
43
|
-
render: (routeInfo) => {
|
|
44
|
-
return html`<my-lit-component .routeInfo=${routeInfo}></my-lit-component>`
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
// React component
|
|
48
|
-
{
|
|
49
|
-
path: '/react-page',
|
|
50
|
-
render: (routeInfo) => {
|
|
51
|
-
return ( <MyComponent></MyComponent> )
|
|
52
|
-
}
|
|
24
|
+
fallback: {
|
|
25
|
+
render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`,
|
|
53
26
|
},
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
path: '/element-page',
|
|
57
|
-
render: (routeInfo) => {
|
|
58
|
-
const element = document.createElement('my-element');
|
|
59
|
-
element.data = routeInfo.params;
|
|
60
|
-
return element;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
];
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Nested Routes
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
import { RouteConfig } from '@iyulab/router';
|
|
27
|
+
});
|
|
70
28
|
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
path: '/dashboard',
|
|
74
|
-
render: () => html`<dashboard-layout><u-outlet></u-outlet></dashboard-layout>`,
|
|
75
|
-
children: [
|
|
76
|
-
{
|
|
77
|
-
index: true, // Matches '/dashboard'
|
|
78
|
-
render: () => html`<dashboard-home></dashboard-home>`
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
path: 'settings', // Matches '/dashboard/settings'
|
|
82
|
-
render: () => html`<dashboard-settings></dashboard-settings>`
|
|
83
|
-
}
|
|
84
|
-
]
|
|
85
|
-
}
|
|
86
|
-
];
|
|
29
|
+
router.go('/users/1');
|
|
87
30
|
```
|
|
88
31
|
|
|
89
32
|
## Skills Usage
|
|
90
33
|
|
|
91
|
-
Install the `iyulab-router` skill
|
|
92
|
-
|
|
93
|
-
**Using GitHub shorthand:**
|
|
34
|
+
Install the `iyulab-router` skill for agent-friendly package guidance.
|
|
94
35
|
|
|
95
36
|
```bash
|
|
96
37
|
npx skills add iyulab/node-router
|
|
97
38
|
```
|
|
98
39
|
|
|
99
|
-
**Using local path (after `npm install`):**
|
|
100
|
-
|
|
101
40
|
```bash
|
|
102
41
|
npx skills add ./node_modules/@iyulab/router
|
|
103
42
|
```
|
|
104
43
|
|
|
105
|
-
##
|
|
44
|
+
## Route Guards
|
|
106
45
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
import { LitElement, html } from 'lit';
|
|
111
|
-
import { customElement } from 'lit/decorators.js';
|
|
112
|
-
|
|
113
|
-
import "@iyulab/router";
|
|
114
|
-
|
|
115
|
-
@customElement('app-root')
|
|
116
|
-
export class AppRoot extends LitElement {
|
|
117
|
-
render() {
|
|
118
|
-
return html`
|
|
119
|
-
<nav>
|
|
120
|
-
<u-link href="/">Home</u-link>
|
|
121
|
-
<u-link href="/about">About</u-link>
|
|
122
|
-
<u-link href="/user/123">User Profile</u-link>
|
|
123
|
-
</nav>
|
|
124
|
-
<main>
|
|
125
|
-
<u-outlet></u-outlet>
|
|
126
|
-
</main>
|
|
127
|
-
`;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Using with React Components
|
|
133
|
-
|
|
134
|
-
```tsx
|
|
135
|
-
import React from 'react';
|
|
136
|
-
import { UOutlet, ULink } from '@iyulab/router/react';
|
|
137
|
-
|
|
138
|
-
export function AppRoot() {
|
|
139
|
-
return (
|
|
140
|
-
<div>
|
|
141
|
-
<nav>
|
|
142
|
-
<ULink href="/">Home</ULink>
|
|
143
|
-
<ULink href="/about">About</ULink>
|
|
144
|
-
<ULink href="/user/123">User Profile</ULink>
|
|
145
|
-
</nav>
|
|
146
|
-
<main>
|
|
147
|
-
<UOutlet />
|
|
148
|
-
</main>
|
|
149
|
-
</div>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Error Handling
|
|
155
|
-
|
|
156
|
-
The router provides comprehensive error handling through `FallbackRouteContext`. When a routing error occurs, the fallback render function receives a context with full error information:
|
|
46
|
+
Use `enter` to run guard logic before rendering.
|
|
157
47
|
|
|
158
48
|
```typescript
|
|
159
49
|
const router = new Router({
|
|
160
50
|
root: document.body,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return html`<error-page .message=${message}></error-page>`;
|
|
174
|
-
}
|
|
175
|
-
return html`<error-page .error=${ctx.error}></error-page>`;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
51
|
+
enter: (ctx) => {
|
|
52
|
+
if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
routes: [
|
|
56
|
+
{ path: '/login', render: () => html`<login-page></login-page>` },
|
|
57
|
+
{
|
|
58
|
+
path: '/admin',
|
|
59
|
+
enter: () => hasRole('admin') || '/forbidden',
|
|
60
|
+
render: () => html`<admin-page></admin-page>`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
178
63
|
});
|
|
179
64
|
```
|
|
180
65
|
|
|
181
|
-
|
|
182
|
-
- `
|
|
183
|
-
- `
|
|
184
|
-
- `
|
|
66
|
+
`enter` return values:
|
|
67
|
+
- `true` (or `undefined`): continue
|
|
68
|
+
- `false`: cancel navigation
|
|
69
|
+
- `string`: redirect to that path
|
|
185
70
|
|
|
186
71
|
## Route Metadata
|
|
187
72
|
|
|
188
|
-
|
|
73
|
+
Attach metadata to routes using `metadata`. The router merges metadata from parent to child and exposes it on `ctx.metadata`.
|
|
189
74
|
|
|
190
75
|
```typescript
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
76
|
+
const routes = [
|
|
77
|
+
{
|
|
78
|
+
path: '/dashboard',
|
|
79
|
+
metadata: { requiresAuth: true, section: 'dashboard' },
|
|
80
|
+
render: () => html`<dashboard-layout><u-outlet></u-outlet></dashboard-layout>`,
|
|
81
|
+
children: [
|
|
82
|
+
{
|
|
83
|
+
path: 'settings',
|
|
84
|
+
metadata: { tab: 'settings' },
|
|
85
|
+
render: (ctx) => html`<settings-page .metadata=${ctx.metadata}></settings-page>`,
|
|
201
86
|
},
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
meta: { requiresAuth: true, role: 'superadmin' },
|
|
206
|
-
render: (ctx) => {
|
|
207
|
-
// ctx.meta === { requiresAuth: true, layout: 'admin', role: 'superadmin' }
|
|
208
|
-
return html`<admin-settings></admin-settings>`;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
]
|
|
212
|
-
}
|
|
213
|
-
]
|
|
214
|
-
});
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
];
|
|
215
90
|
```
|
|
216
91
|
|
|
217
|
-
|
|
92
|
+
## Nested Routes
|
|
218
93
|
|
|
219
|
-
|
|
94
|
+
Parent routes must render `<u-outlet>` to host child route content.
|
|
220
95
|
|
|
221
|
-
|
|
96
|
+
```typescript
|
|
97
|
+
const routes = [
|
|
98
|
+
{
|
|
99
|
+
path: '/nested',
|
|
100
|
+
render: () => html`<nested-layout><u-outlet></u-outlet></nested-layout>`,
|
|
101
|
+
children: [
|
|
102
|
+
{ index: true, render: () => html`<nested-home></nested-home>` },
|
|
103
|
+
{ path: 'lit', render: () => html`<nested-lit></nested-lit>` },
|
|
104
|
+
{ path: 'react', render: () => <NestedReact /> },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
```
|
|
222
109
|
|
|
223
|
-
|
|
224
|
-
|-------|------|-------------|
|
|
225
|
-
| `route-begin` | `RouteBeginEvent` | Fired when navigation starts |
|
|
226
|
-
| `route-progress` | `RouteProgressEvent` | Fired during async loading (0–100) |
|
|
227
|
-
| `route-done` | `RouteDoneEvent` | Fired when navigation completes successfully |
|
|
228
|
-
| `route-error` | `RouteErrorEvent` | Fired when a routing error occurs |
|
|
110
|
+
## Link and Outlet Components
|
|
229
111
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
window.addEventListener('route-progress', (e: RouteProgressEvent) => {
|
|
233
|
-
progressBar.value = e.progress;
|
|
234
|
-
});
|
|
112
|
+
- `<u-link>`: SPA-aware anchor element
|
|
113
|
+
- `<u-outlet>`: render target for matched route output
|
|
235
114
|
|
|
236
|
-
|
|
237
|
-
window.addEventListener('route-begin', (e: RouteBeginEvent) => {
|
|
238
|
-
console.log('Navigating to:', e.context.pathname);
|
|
239
|
-
});
|
|
115
|
+
`<u-link>` supports `href`, `target`, and `rel`.
|
|
240
116
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
117
|
+
```html
|
|
118
|
+
<u-link href="/docs">Docs</u-link>
|
|
119
|
+
<u-link href="https://example.com" target="_blank" rel="noopener noreferrer">External</u-link>
|
|
120
|
+
```
|
|
244
121
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
122
|
+
React wrappers:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { ULink, UOutlet } from '@iyulab/router/react';
|
|
248
126
|
```
|
|
249
127
|
|
|
250
|
-
##
|
|
128
|
+
## Route Events
|
|
129
|
+
|
|
130
|
+
The router dispatches events on `window`:
|
|
251
131
|
|
|
252
|
-
|
|
132
|
+
- `route-begin`
|
|
133
|
+
- `route-progress`
|
|
134
|
+
- `route-done`
|
|
135
|
+
- `route-error`
|
|
253
136
|
|
|
254
137
|
```typescript
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// Optional parameter
|
|
260
|
-
{ path: '/posts/:category?', render: (ctx) => {
|
|
261
|
-
const category = ctx.params.category || 'all';
|
|
262
|
-
return html`<posts-page .category=${category}></posts-page>`;
|
|
263
|
-
}},
|
|
264
|
-
|
|
265
|
-
// Wildcard (catch-all)
|
|
266
|
-
{ path: '/docs/:path*', render: (ctx) => html`<docs-page .path=${ctx.params.path}></docs-page>` },
|
|
267
|
-
|
|
268
|
-
// Multiple parameters
|
|
269
|
-
{ path: '/org/:orgId/repo/:repoId', render: (ctx) => {
|
|
270
|
-
return html`<repo-page .orgId=${ctx.params.orgId} .repoId=${ctx.params.repoId}></repo-page>`;
|
|
271
|
-
}}
|
|
272
|
-
];
|
|
138
|
+
window.addEventListener('route-progress', (e) => {
|
|
139
|
+
console.log(e.progress);
|
|
140
|
+
});
|
|
273
141
|
```
|
|
274
142
|
|
|
275
|
-
When URL parameters change (e.g., navigating from `/user/1` to `/user/2`), leaf routes (without children) automatically re-render since `force` defaults to `true`. For parent routes with children, set `force: true` explicitly if re-rendering is needed on parameter changes.
|
|
276
|
-
|
|
277
143
|
## License
|
|
278
144
|
|
|
279
|
-
MIT License
|
|
145
|
+
MIT License. See [LICENSE](LICENSE).
|
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,17 @@ declare interface BaseRouteConfig {
|
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
49
|
render?: (ctx: RouteContext) => Promise<unknown> | unknown;
|
|
50
|
+
/**
|
|
51
|
+
* 이 라우트 진입 전에 호출되는 enter 함수입니다.
|
|
52
|
+
* - `string` 반환: 해당 경로로 redirect
|
|
53
|
+
* - `false` 반환: 네비게이션 취소
|
|
54
|
+
* - `true` 반환: 통과
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* { path: '/admin', enter: (ctx) => ctx.metadata.role === 'admin' || '/forbidden' }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
enter?: (ctx: RouteContext) => Promise<string | boolean> | string | boolean;
|
|
50
61
|
/**
|
|
51
62
|
* 라우터 URL 변경시 렌더링을 강제할지 여부
|
|
52
63
|
* - 기본값으로 children을 가질때 false로 설정되며, children이 없을 경우 true로 설정됩니다.
|
|
@@ -63,10 +74,10 @@ declare interface BaseRouteConfig {
|
|
|
63
74
|
* - 인증, SEO, 분석 등의 용도로 사용할 수 있습니다.
|
|
64
75
|
* @example
|
|
65
76
|
* ```typescript
|
|
66
|
-
* { path: '/admin',
|
|
77
|
+
* { path: '/admin', metadata: { requiresAuth: true, role: 'admin' } }
|
|
67
78
|
* ```
|
|
68
79
|
*/
|
|
69
|
-
|
|
80
|
+
metadata?: Record<string, unknown>;
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
/**
|
|
@@ -156,8 +167,6 @@ declare interface RenderOption {
|
|
|
156
167
|
id?: string;
|
|
157
168
|
/** 강제 렌더링 여부 */
|
|
158
169
|
force?: boolean;
|
|
159
|
-
/** 렌더링할 값 */
|
|
160
|
-
value: unknown;
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
/**
|
|
@@ -244,7 +253,7 @@ export declare interface RouteContext {
|
|
|
244
253
|
* 매칭된 라우트 체인의 병합된 메타데이터
|
|
245
254
|
* - 부모 라우트에서 자식 라우트 순서로 병합됩니다.
|
|
246
255
|
*/
|
|
247
|
-
|
|
256
|
+
metadata: Record<string, unknown>;
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
/**
|
|
@@ -316,6 +325,7 @@ export declare class Router {
|
|
|
316
325
|
private readonly _basepath;
|
|
317
326
|
private readonly _routes;
|
|
318
327
|
private readonly _fallback?;
|
|
328
|
+
private readonly _enter?;
|
|
319
329
|
/** 현재 라우팅 요청 ID */
|
|
320
330
|
private _requestID?;
|
|
321
331
|
/** 현재 라우팅 정보 */
|
|
@@ -333,7 +343,7 @@ export declare class Router {
|
|
|
333
343
|
* 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
|
|
334
344
|
* @param href 이동할 경로
|
|
335
345
|
*/
|
|
336
|
-
go(href: string): Promise<
|
|
346
|
+
go(href: string): Promise<undefined>;
|
|
337
347
|
/** 브라우저 히스토리 이벤트가 발생시 라우팅 처리 */
|
|
338
348
|
private handleWindowPopstate;
|
|
339
349
|
/** 클릭 이벤트에서 라우터로 처리할 앵커를 찾아 클라이언트 라우팅 수행 */
|
|
@@ -361,6 +371,19 @@ export declare interface RouterConfig {
|
|
|
361
371
|
* - 라우트는 렌더링할 엘리먼트 또는 컴포넌트를 지정합니다.
|
|
362
372
|
*/
|
|
363
373
|
routes?: RouteConfig[];
|
|
374
|
+
/**
|
|
375
|
+
* 모든 라우트 전환 전에 호출되는 글로벌 enter 함수입니다.
|
|
376
|
+
* - `string` 반환: 해당 경로로 redirect
|
|
377
|
+
* - `false` 반환: 네비게이션 취소
|
|
378
|
+
* - `true` 반환: 통과
|
|
379
|
+
* @example
|
|
380
|
+
* ```typescript
|
|
381
|
+
* enter: async (ctx) => {
|
|
382
|
+
* if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
|
|
383
|
+
* }
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
enter?: (ctx: RouteContext) => Promise<string | boolean> | string | boolean;
|
|
364
387
|
/**
|
|
365
388
|
* 라우트 매칭 실패 또는 오류 발생 시 대체 라우트 설정
|
|
366
389
|
* - 지정된 설정이 없을 경우, 기본 오류 페이지가 렌더링됩니다.
|
|
@@ -395,6 +418,16 @@ export declare class ULink extends LitElement {
|
|
|
395
418
|
* - `_top`: 최상위 프레임에서 링크 열기
|
|
396
419
|
*/
|
|
397
420
|
target?: string;
|
|
421
|
+
/**
|
|
422
|
+
* 링크 관계 rel 속성
|
|
423
|
+
*
|
|
424
|
+
* - `noopener`: target이 _blank인 경우 보안 강화 (window.opener 차단)
|
|
425
|
+
* - `noreferrer`: target이 _blank인 경우 보안 강화 + Referer 헤더 제거
|
|
426
|
+
* - `external`: 외부 링크임을 명시 (SEO/접근성에 도움)
|
|
427
|
+
* - `nofollow`: 검색 엔진이 링크를 따라가지 않도록 지시 (SEO에 영향)
|
|
428
|
+
* - 그 외 rel 값도 그대로 전달됩니다.
|
|
429
|
+
*/
|
|
430
|
+
rel?: string;
|
|
398
431
|
/**
|
|
399
432
|
* 링크 대상 URL, 다음 사항에 따라 SPA 라우팅 또는 브라우저 네비게이션이 결정됩니다.
|
|
400
433
|
*
|
|
@@ -436,7 +469,7 @@ export declare class UOutlet extends HTMLElement {
|
|
|
436
469
|
/**
|
|
437
470
|
* 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
|
|
438
471
|
*/
|
|
439
|
-
render(
|
|
472
|
+
render(value: unknown, options?: RenderOption): Promise<void>;
|
|
440
473
|
/**
|
|
441
474
|
* 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
|
|
442
475
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as isExternalUrl, i as absolutePath, n as __decorate, o as parseUrl, r as __decorateMetadata, s as UOutlet, t as ULink } from "./share-
|
|
1
|
+
import { a as isExternalUrl, i as absolutePath, n as __decorate, o as parseUrl, r as __decorateMetadata, s as UOutlet, t as ULink } from "./share-CUGwxZKa.js";
|
|
2
2
|
import { LitElement, css, html } from "lit";
|
|
3
3
|
import { customElement, property } from "lit/decorators.js";
|
|
4
4
|
//#region src/types/RouteError.ts
|
|
@@ -214,10 +214,11 @@ function getRandomID() {
|
|
|
214
214
|
* `u-outlet` 엘리먼트를 찾아 반환합니다.
|
|
215
215
|
*
|
|
216
216
|
* @param element 검색을 시작할 HTMLElement
|
|
217
|
+
* @param skip element 자신을 검사에서 제외할지 여부 (기본값: false)
|
|
217
218
|
* @returns 찾은 UOutlet 엘리먼트 또는 undefined
|
|
218
219
|
*/
|
|
219
|
-
function findOutlet(element) {
|
|
220
|
-
if (element
|
|
220
|
+
function findOutlet(element, skip = false) {
|
|
221
|
+
if (!skip && element instanceof UOutlet) return element;
|
|
221
222
|
const roots = element.shadowRoot ? [element.shadowRoot, element] : [element];
|
|
222
223
|
for (const root of roots) for (const child of Array.from(root.children)) {
|
|
223
224
|
const result = findOutlet(child);
|
|
@@ -353,6 +354,7 @@ var Router = class {
|
|
|
353
354
|
this._basepath = absolutePath(config.basepath || "/");
|
|
354
355
|
this._routes = setRoutes(config.routes || [], this._basepath);
|
|
355
356
|
this._fallback = config.fallback;
|
|
357
|
+
this._enter = config.enter;
|
|
356
358
|
window.addEventListener("popstate", this.handleWindowPopstate);
|
|
357
359
|
if (config.useIntercept !== false) document.addEventListener("click", this.handleDocumentClick);
|
|
358
360
|
if (config.initialLoad !== false) waitOutlet(this._rootElement).then(() => {
|
|
@@ -396,14 +398,20 @@ var Router = class {
|
|
|
396
398
|
context.progress = progressCallback;
|
|
397
399
|
let outlet = void 0;
|
|
398
400
|
try {
|
|
401
|
+
if (this._enter) {
|
|
402
|
+
const result = await this._enter(context);
|
|
403
|
+
if (this._requestID !== requestID) return;
|
|
404
|
+
if (typeof result === "string") return void this.go(result);
|
|
405
|
+
if (result === false) return;
|
|
406
|
+
}
|
|
399
407
|
if (this._requestID !== requestID) return;
|
|
400
408
|
window.dispatchEvent(new RouteBeginEvent(context));
|
|
401
409
|
const routes = getRoutes(this._routes, context.pathname);
|
|
402
410
|
const lastRoute = routes[routes.length - 1];
|
|
403
411
|
if (lastRoute && lastRoute.path instanceof URLPattern) context.params = lastRoute.path.exec({ pathname: context.pathname })?.pathname.groups || {};
|
|
404
412
|
const mergedMeta = {};
|
|
405
|
-
for (const route of routes) if (route.
|
|
406
|
-
context.
|
|
413
|
+
for (const route of routes) if (route.metadata) Object.assign(mergedMeta, route.metadata);
|
|
414
|
+
context.metadata = mergedMeta;
|
|
407
415
|
this._context = context;
|
|
408
416
|
outlet = findOutletOrThrow(this._rootElement);
|
|
409
417
|
let title = void 0;
|
|
@@ -411,6 +419,12 @@ var Router = class {
|
|
|
411
419
|
if (routes.length === 0) throw new NotFoundError(context.href);
|
|
412
420
|
for (const route of routes) {
|
|
413
421
|
if (this._requestID !== requestID) return;
|
|
422
|
+
if (route.enter) {
|
|
423
|
+
const result = await route.enter(context);
|
|
424
|
+
if (this._requestID !== requestID) return;
|
|
425
|
+
if (typeof result === "string") return void this.go(result);
|
|
426
|
+
if (result === false) return;
|
|
427
|
+
}
|
|
414
428
|
if (!route.render) continue;
|
|
415
429
|
try {
|
|
416
430
|
content = await route.render(context);
|
|
@@ -419,15 +433,14 @@ var Router = class {
|
|
|
419
433
|
throw new ContentLoadError(LoadError);
|
|
420
434
|
}
|
|
421
435
|
try {
|
|
422
|
-
outlet.render({
|
|
436
|
+
outlet.render(content, {
|
|
423
437
|
id: route.id,
|
|
424
|
-
value: content,
|
|
425
438
|
force: route.force
|
|
426
439
|
});
|
|
427
440
|
} catch (renderError) {
|
|
428
441
|
throw new ContentRenderError(renderError);
|
|
429
442
|
}
|
|
430
|
-
outlet = findOutlet(outlet) || outlet;
|
|
443
|
+
outlet = findOutlet(outlet, true) || outlet;
|
|
431
444
|
title = route.title || title;
|
|
432
445
|
}
|
|
433
446
|
document.title = title || document.title;
|
|
@@ -442,18 +455,16 @@ var Router = class {
|
|
|
442
455
|
...context,
|
|
443
456
|
error: routeError
|
|
444
457
|
});
|
|
445
|
-
outlet.render({
|
|
458
|
+
outlet.render(fallbackContent, {
|
|
446
459
|
id: "#fallback",
|
|
447
|
-
value: fallbackContent,
|
|
448
460
|
force: true
|
|
449
461
|
});
|
|
450
462
|
document.title = this._fallback.title || document.title;
|
|
451
463
|
} else {
|
|
452
464
|
const errorContent = new UErrorPage();
|
|
453
465
|
errorContent.error = error;
|
|
454
|
-
if (outlet) outlet.render({
|
|
466
|
+
if (outlet) outlet.render(errorContent, {
|
|
455
467
|
id: "#error",
|
|
456
|
-
value: errorContent,
|
|
457
468
|
force: true
|
|
458
469
|
});
|
|
459
470
|
else {
|
package/dist/react.d.ts
CHANGED
|
@@ -10,8 +10,6 @@ declare interface RenderOption {
|
|
|
10
10
|
id?: string;
|
|
11
11
|
/** 강제 렌더링 여부 */
|
|
12
12
|
force?: boolean;
|
|
13
|
-
/** 렌더링할 값 */
|
|
14
|
-
value: unknown;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
/**
|
|
@@ -36,6 +34,16 @@ declare class ULink_2 extends LitElement {
|
|
|
36
34
|
* - `_top`: 최상위 프레임에서 링크 열기
|
|
37
35
|
*/
|
|
38
36
|
target?: string;
|
|
37
|
+
/**
|
|
38
|
+
* 링크 관계 rel 속성
|
|
39
|
+
*
|
|
40
|
+
* - `noopener`: target이 _blank인 경우 보안 강화 (window.opener 차단)
|
|
41
|
+
* - `noreferrer`: target이 _blank인 경우 보안 강화 + Referer 헤더 제거
|
|
42
|
+
* - `external`: 외부 링크임을 명시 (SEO/접근성에 도움)
|
|
43
|
+
* - `nofollow`: 검색 엔진이 링크를 따라가지 않도록 지시 (SEO에 영향)
|
|
44
|
+
* - 그 외 rel 값도 그대로 전달됩니다.
|
|
45
|
+
*/
|
|
46
|
+
rel?: string;
|
|
39
47
|
/**
|
|
40
48
|
* 링크 대상 URL, 다음 사항에 따라 SPA 라우팅 또는 브라우저 네비게이션이 결정됩니다.
|
|
41
49
|
*
|
|
@@ -82,7 +90,7 @@ declare class UOutlet_2 extends HTMLElement {
|
|
|
82
90
|
/**
|
|
83
91
|
* 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
|
|
84
92
|
*/
|
|
85
|
-
render(
|
|
93
|
+
render(value: unknown, options?: RenderOption): Promise<void>;
|
|
86
94
|
/**
|
|
87
95
|
* 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
|
|
88
96
|
*/
|
package/dist/react.js
CHANGED
|
@@ -9,9 +9,9 @@ var UOutlet = class extends HTMLElement {
|
|
|
9
9
|
/**
|
|
10
10
|
* 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
|
|
11
11
|
*/
|
|
12
|
-
async render(
|
|
13
|
-
if (this.routeId === id && force === false) return;
|
|
14
|
-
this.routeId = id;
|
|
12
|
+
async render(value, options) {
|
|
13
|
+
if (this.routeId === options?.id && options?.force === false) return;
|
|
14
|
+
this.routeId = options?.id;
|
|
15
15
|
this.reset();
|
|
16
16
|
if (value === null) throw new Error("Content is null and cannot be rendered.");
|
|
17
17
|
if (typeof value !== "object") throw new Error("Content is not a valid renderable object.");
|
|
@@ -88,7 +88,7 @@ function parseUrl(url, basepath) {
|
|
|
88
88
|
hash: urlObj.hash,
|
|
89
89
|
params: {},
|
|
90
90
|
progress: () => {},
|
|
91
|
-
|
|
91
|
+
metadata: {}
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
@@ -126,12 +126,12 @@ function catchBasepath(basepath) {
|
|
|
126
126
|
return basepath;
|
|
127
127
|
}
|
|
128
128
|
//#endregion
|
|
129
|
-
//#region \0@oxc-project+runtime@0.
|
|
129
|
+
//#region \0@oxc-project+runtime@0.123.0/helpers/decorateMetadata.js
|
|
130
130
|
function __decorateMetadata(k, v) {
|
|
131
131
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
132
132
|
}
|
|
133
133
|
//#endregion
|
|
134
|
-
//#region \0@oxc-project+runtime@0.
|
|
134
|
+
//#region \0@oxc-project+runtime@0.123.0/helpers/decorate.js
|
|
135
135
|
function __decorate(decorators, target, key, desc) {
|
|
136
136
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
137
137
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -189,7 +189,11 @@ var ULink = class ULink extends LitElement {
|
|
|
189
189
|
}
|
|
190
190
|
render() {
|
|
191
191
|
return html`
|
|
192
|
-
<a
|
|
192
|
+
<a
|
|
193
|
+
href=${this.compute(this.href)}
|
|
194
|
+
target=${ifDefined(this.target)}
|
|
195
|
+
rel=${ifDefined(this.rel)}
|
|
196
|
+
>
|
|
193
197
|
<slot></slot>
|
|
194
198
|
</a>
|
|
195
199
|
`;
|
|
@@ -231,6 +235,7 @@ var ULink = class ULink extends LitElement {
|
|
|
231
235
|
}
|
|
232
236
|
};
|
|
233
237
|
__decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "target", void 0);
|
|
238
|
+
__decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "rel", void 0);
|
|
234
239
|
__decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "href", void 0);
|
|
235
240
|
ULink = __decorate([customElement("u-link")], ULink);
|
|
236
241
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A modern client-side router for web applications with support for Lit and React components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lit",
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
+
"skills",
|
|
22
23
|
"package.json",
|
|
23
24
|
"README.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
24
26
|
"LICENSE"
|
|
25
27
|
],
|
|
26
28
|
"type": "module",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: iyulab-router
|
|
3
|
+
description: Client-side SPA router for Lit and React with URLPattern matching, nested routes, route guards, metadata merging, fallback handling, and route events. Use when working with @iyulab/router to define routes, add guards, handle navigation, or integrate <u-outlet>/<u-link>.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Browser environments only (requires URLPattern and History API)
|
|
6
|
+
metadata:
|
|
7
|
+
author: iyulab
|
|
8
|
+
version: "0.8.0"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# @iyulab/router
|
|
12
|
+
|
|
13
|
+
Client-side router supporting Lit and React renders, nested routes, route guards, and URLPattern-based matching.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @iyulab/router
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Core API
|
|
22
|
+
|
|
23
|
+
| Export | Purpose |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `Router` | Main router class |
|
|
26
|
+
| `RouteConfig` | Route definition type |
|
|
27
|
+
| `RouterConfig` | Constructor config type |
|
|
28
|
+
| `RouteContext` | Passed to every `render()` call |
|
|
29
|
+
| `FallbackRouteConfig` | Error/404 fallback definition |
|
|
30
|
+
| `<u-outlet>` | Renders the matched route output |
|
|
31
|
+
| `<u-link>` | Client-side navigation anchor |
|
|
32
|
+
| `UOutlet`, `ULink` | React wrappers (from `@iyulab/router/react`) |
|
|
33
|
+
|
|
34
|
+
## Router Setup
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Router } from '@iyulab/router';
|
|
38
|
+
import { html } from 'lit';
|
|
39
|
+
|
|
40
|
+
const router = new Router({
|
|
41
|
+
root: document.body, // required — mount element containing <u-outlet>
|
|
42
|
+
basepath: '/', // optional
|
|
43
|
+
enter: (ctx) => {
|
|
44
|
+
if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
routes: [
|
|
48
|
+
{ index: true, render: () => html`<home-page></home-page>` },
|
|
49
|
+
{ path: '/user/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
|
|
50
|
+
{
|
|
51
|
+
path: '/admin',
|
|
52
|
+
metadata: { role: 'admin' },
|
|
53
|
+
enter: (ctx) => ctx.metadata.role === 'admin' || '/forbidden',
|
|
54
|
+
render: () => html`<admin-page></admin-page>`,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
fallback: {
|
|
58
|
+
render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## RouteConfig Fields
|
|
64
|
+
|
|
65
|
+
| Field | Type | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `path` | `string \| URLPattern` | URLPattern path; omit when `index: true` |
|
|
68
|
+
| `index` | `true` | Marks route as index of its parent path |
|
|
69
|
+
| `render` | `(ctx) => unknown` | Returns Lit `TemplateResult`, React element, or `HTMLElement` |
|
|
70
|
+
| `enter` | `(ctx) => string \| boolean \| Promise<string \| boolean>` | Guard before route render (`false` cancel, `string` redirect) |
|
|
71
|
+
| `children` | `RouteConfig[]` | Nested routes; parent must render `<u-outlet>` |
|
|
72
|
+
| `title` | `string` | Sets `document.title` on match |
|
|
73
|
+
| `metadata` | `Record<string, unknown>` | Arbitrary metadata (auth, layout, analytics) |
|
|
74
|
+
| `force` | `boolean` | Force re-render on URL change (default `true` for leaf routes) |
|
|
75
|
+
|
|
76
|
+
## RouteContext Fields
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
ctx.params // URLPattern captured params
|
|
80
|
+
ctx.pathname // path without query/hash
|
|
81
|
+
ctx.path // full path including query + hash
|
|
82
|
+
ctx.query // URLSearchParams
|
|
83
|
+
ctx.metadata // merged metadata from matched route chain
|
|
84
|
+
ctx.progress // (value: number) => void — report 0–100 loading progress
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Nested Routes
|
|
88
|
+
|
|
89
|
+
Parent must include `<u-outlet>` in its render output:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
{
|
|
93
|
+
path: '/dashboard',
|
|
94
|
+
render: () => html`<dashboard-layout><u-outlet></u-outlet></dashboard-layout>`,
|
|
95
|
+
children: [
|
|
96
|
+
{ index: true, render: () => html`<dashboard-home></dashboard-home>` },
|
|
97
|
+
{ path: 'settings', render: () => html`<dashboard-settings></dashboard-settings>` }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Navigation
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
router.go('/path'); // programmatic navigation
|
|
106
|
+
router.go('relative-path'); // relative to basepath
|
|
107
|
+
router.destroy(); // remove event listeners
|
|
108
|
+
|
|
109
|
+
// From Lit template
|
|
110
|
+
html`<u-link href="/about">About</u-link>`
|
|
111
|
+
|
|
112
|
+
// From React
|
|
113
|
+
import { ULink } from '@iyulab/router/react';
|
|
114
|
+
<ULink href="/about">About</ULink>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`<u-link>` supports `href`, `target`, and `rel`.
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<u-link href="https://example.com" target="_blank" rel="noopener noreferrer">External</u-link>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Route Events (window)
|
|
124
|
+
|
|
125
|
+
| Event | Fired when |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `route-begin` | Navigation starts |
|
|
128
|
+
| `route-progress` | Async progress update (0–100) |
|
|
129
|
+
| `route-done` | Navigation completes |
|
|
130
|
+
| `route-error` | Routing error occurs |
|
|
131
|
+
|
|
132
|
+
## Error Types (fallback ctx.error)
|
|
133
|
+
|
|
134
|
+
| Code | Class |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `NOT_FOUND` | `NotFoundError` |
|
|
137
|
+
| `CONTENT_LOAD_ERROR` | `ContentLoadError` |
|
|
138
|
+
| `CONTENT_RENDER_ERROR` | `ContentRenderError` |
|
|
139
|
+
|
|
140
|
+
References:
|
|
141
|
+
- [references/routing-basics.md](references/routing-basics.md)
|
|
142
|
+
- [references/url-pattern.md](references/url-pattern.md)
|
|
143
|
+
- [references/guards-and-metadata.md](references/guards-and-metadata.md)
|
|
144
|
+
- [references/components.md](references/components.md)
|
|
145
|
+
- [references/events-and-errors.md](references/events-and-errors.md)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
## Lit Usage
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import '@iyulab/router';
|
|
7
|
+
import { html } from 'lit';
|
|
8
|
+
|
|
9
|
+
html`
|
|
10
|
+
<nav>
|
|
11
|
+
<u-link href="/">Home</u-link>
|
|
12
|
+
<u-link href="/docs">Docs</u-link>
|
|
13
|
+
<u-link href="https://example.com" target="_blank" rel="noopener noreferrer">External</u-link>
|
|
14
|
+
</nav>
|
|
15
|
+
<main>
|
|
16
|
+
<u-outlet></u-outlet>
|
|
17
|
+
</main>
|
|
18
|
+
`;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## React Wrappers
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { ULink, UOutlet } from '@iyulab/router/react';
|
|
25
|
+
|
|
26
|
+
export function AppRoot() {
|
|
27
|
+
return (
|
|
28
|
+
<div>
|
|
29
|
+
<nav>
|
|
30
|
+
<ULink href="/">Home</ULink>
|
|
31
|
+
<ULink href="/about">About</ULink>
|
|
32
|
+
</nav>
|
|
33
|
+
<main>
|
|
34
|
+
<UOutlet />
|
|
35
|
+
</main>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Nested Outlet Rule
|
|
42
|
+
|
|
43
|
+
A parent route must render `<u-outlet>` to host child route content.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Events and Errors
|
|
2
|
+
|
|
3
|
+
## Route Events
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
window.addEventListener('route-begin', (e) => {
|
|
7
|
+
console.log(e.context.pathname);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
window.addEventListener('route-progress', (e) => {
|
|
11
|
+
progressBar.value = e.progress;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
window.addEventListener('route-done', (e) => {
|
|
15
|
+
analytics.track(e.context.pathname);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
window.addEventListener('route-error', (e) => {
|
|
19
|
+
errorTracker.report(e.error);
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Fallback
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
fallback: {
|
|
27
|
+
render: (ctx) => {
|
|
28
|
+
const { code, message } = ctx.error;
|
|
29
|
+
if (code === 'NOT_FOUND') return html`<not-found-page></not-found-page>`;
|
|
30
|
+
return html`<error-page .message=${message}></error-page>`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Error Codes
|
|
36
|
+
|
|
37
|
+
- `NOT_FOUND`
|
|
38
|
+
- `CONTENT_LOAD_ERROR`
|
|
39
|
+
- `CONTENT_RENDER_ERROR`
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Guards and Metadata
|
|
2
|
+
|
|
3
|
+
## Global Guard
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const router = new Router({
|
|
7
|
+
root: document.body,
|
|
8
|
+
enter: (ctx) => {
|
|
9
|
+
if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
|
|
10
|
+
return true;
|
|
11
|
+
},
|
|
12
|
+
routes: [...],
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Route Guard
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
{
|
|
20
|
+
path: '/admin',
|
|
21
|
+
enter: () => hasRole('admin') || '/forbidden',
|
|
22
|
+
render: () => html`<admin-page></admin-page>`
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Guard return values:
|
|
27
|
+
- `true` or `undefined`: continue
|
|
28
|
+
- `false`: cancel navigation
|
|
29
|
+
- `string`: redirect
|
|
30
|
+
|
|
31
|
+
## Route Metadata
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
{
|
|
35
|
+
path: '/admin',
|
|
36
|
+
metadata: { requiresAuth: true, section: 'admin' },
|
|
37
|
+
render: (ctx) => {
|
|
38
|
+
// merged metadata from matched chain
|
|
39
|
+
console.log(ctx.metadata);
|
|
40
|
+
return html`<admin-layout><u-outlet></u-outlet></admin-layout>`;
|
|
41
|
+
},
|
|
42
|
+
children: [
|
|
43
|
+
{
|
|
44
|
+
path: 'settings',
|
|
45
|
+
metadata: { tab: 'settings' },
|
|
46
|
+
render: (ctx) => html`<settings-page .metadata=${ctx.metadata}></settings-page>`
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Routing Basics
|
|
2
|
+
|
|
3
|
+
## Minimal Setup
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Router } from '@iyulab/router';
|
|
7
|
+
import { html } from 'lit';
|
|
8
|
+
|
|
9
|
+
const router = new Router({
|
|
10
|
+
root: document.body,
|
|
11
|
+
basepath: '/',
|
|
12
|
+
routes: [
|
|
13
|
+
{ index: true, render: () => html`<home-page></home-page>` },
|
|
14
|
+
{ path: '/users/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
|
|
15
|
+
],
|
|
16
|
+
fallback: {
|
|
17
|
+
render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## RouterConfig Options
|
|
23
|
+
|
|
24
|
+
| Option | Default | Description |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `root` | - | Mount element (required) |
|
|
27
|
+
| `basepath` | `'/'` | URL base path |
|
|
28
|
+
| `routes` | `[]` | Route definitions |
|
|
29
|
+
| `enter` | - | Global guard before navigation |
|
|
30
|
+
| `fallback` | built-in error page | Error/404 handler |
|
|
31
|
+
| `useIntercept` | `true` | Intercept `<a>` clicks for client routing |
|
|
32
|
+
| `initialLoad` | `true` | Auto-navigate on initialization |
|
|
33
|
+
|
|
34
|
+
## Navigation
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
router.go('/dashboard');
|
|
38
|
+
router.go('settings');
|
|
39
|
+
router.destroy();
|
|
40
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# URL Pattern
|
|
2
|
+
|
|
3
|
+
## Supported Patterns
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// Required
|
|
7
|
+
{ path: '/user/:id' }
|
|
8
|
+
|
|
9
|
+
// Optional
|
|
10
|
+
{ path: '/posts/:category?' }
|
|
11
|
+
|
|
12
|
+
// Wildcard
|
|
13
|
+
{ path: '/docs/:path*' }
|
|
14
|
+
|
|
15
|
+
// Multiple params
|
|
16
|
+
{ path: '/org/:orgId/repo/:repoId' }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Access params via `ctx.params`.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
ctx.params.id;
|
|
23
|
+
ctx.params.category;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Query and Hash
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
ctx.query.get('q');
|
|
30
|
+
ctx.query.get('page');
|
|
31
|
+
ctx.hash;
|
|
32
|
+
```
|