@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 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
- A modern, lightweight client-side router for web applications with support for both Lit and React components.
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
- index: true,
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
- // HTML element
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
- const routes: RouteConfig[] = [
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 to give your AI agent knowledge of this package's API and patterns.
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
- ## Usage Examples
44
+ ## Route Guards
106
45
 
107
- ### Using with Lit Elements
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
- basepath: '/',
162
- routes: [...],
163
- fallback: {
164
- title: 'Error',
165
- render: (ctx) => {
166
- // ctx.error contains RouteError with code, message, and original error
167
- const { code, message, original } = ctx.error;
168
-
169
- if (code === 'NOT_FOUND') {
170
- return html`<not-found-page .path=${ctx.pathname}></not-found-page>`;
171
- }
172
- if (code === 'CONTENT_LOAD_ERROR') {
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
- Error types:
182
- - `NotFoundError` — No matching route found (code: `NOT_FOUND`)
183
- - `ContentLoadError` Route render function threw an error (code: `CONTENT_LOAD_ERROR`)
184
- - `ContentRenderError` Outlet rendering failed (code: `CONTENT_RENDER_ERROR`)
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
- Routes can carry arbitrary metadata via the `meta` field. When a route matches, metadata from the entire matched route chain is merged (parent child order, child overrides parent):
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 router = new Router({
192
- root: document.body,
193
- basepath: '/',
194
- routes: [
195
- {
196
- path: '/admin',
197
- meta: { requiresAuth: true, layout: 'admin' },
198
- render: (ctx) => {
199
- // ctx.meta === { requiresAuth: true, layout: 'admin' }
200
- return html`<admin-layout><u-outlet></u-outlet></admin-layout>`;
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
- children: [
203
- {
204
- path: 'settings',
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
- Use cases: authentication guards, SEO tags, analytics tracking, layout selection, and more.
92
+ ## Nested Routes
218
93
 
219
- ## Route Events
94
+ Parent routes must render `<u-outlet>` to host child route content.
220
95
 
221
- The router dispatches events on the `window` object during navigation:
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
- | Event | Type | Description |
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
- ```typescript
231
- // Track navigation progress
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
- // Log navigation events
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
- window.addEventListener('route-done', (e: RouteDoneEvent) => {
242
- analytics.trackPageView(e.context.pathname);
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
- window.addEventListener('route-error', (e: RouteErrorEvent) => {
246
- errorTracker.report(e.error);
247
- });
122
+ React wrappers:
123
+
124
+ ```tsx
125
+ import { ULink, UOutlet } from '@iyulab/router/react';
248
126
  ```
249
127
 
250
- ## URL Parameters
128
+ ## Route Events
129
+
130
+ The router dispatches events on `window`:
251
131
 
252
- The router supports URLPattern-based parameter matching:
132
+ - `route-begin`
133
+ - `route-progress`
134
+ - `route-done`
135
+ - `route-error`
253
136
 
254
137
  ```typescript
255
- const routes: RouteConfig[] = [
256
- // Required parameter
257
- { path: '/user/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
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 - see [LICENSE](LICENSE) file for details.
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', meta: { requiresAuth: true, role: 'admin' } }
77
+ * { path: '/admin', metadata: { requiresAuth: true, role: 'admin' } }
67
78
  * ```
68
79
  */
69
- meta?: Record<string, unknown>;
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
- meta: Record<string, unknown>;
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<void>;
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({ id, value, force }: RenderOption): Promise<void>;
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-sbAElOI7.js";
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.tagName === "U-OUTLET") return 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.meta) Object.assign(mergedMeta, route.meta);
406
- context.meta = mergedMeta;
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({ id, value, force }: RenderOption): Promise<void>;
93
+ render(value: unknown, options?: RenderOption): Promise<void>;
86
94
  /**
87
95
  * 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
88
96
  */
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { s as UOutlet$1, t as ULink$1 } from "./share-sbAElOI7.js";
1
+ import { s as UOutlet$1, t as ULink$1 } from "./share-CUGwxZKa.js";
2
2
  import React from "react";
3
3
  import { createComponent } from "@lit/react";
4
4
  //#region src/react.ts
@@ -9,9 +9,9 @@ var UOutlet = class extends HTMLElement {
9
9
  /**
10
10
  * 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
11
11
  */
12
- async render({ id, value, force }) {
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
- meta: {}
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.122.0/helpers/decorateMetadata.js
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.122.0/helpers/decorate.js
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 target=${ifDefined(this.target)} href=${this.compute(this.href)}>
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.7.5",
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
+ ```