@iyulab/router 0.7.4 → 0.7.6

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/dist/react.d.ts CHANGED
@@ -1,11 +1,92 @@
1
+ import { CSSResult } from 'lit';
2
+ import { LitElement } from 'lit';
3
+ import { PropertyValues } from 'lit';
4
+ import { ReactWebComponent } from '@lit/react';
5
+ import { TemplateResult } from 'lit-html';
6
+
7
+ /** 렌더링 옵션 */
8
+ declare interface RenderOption {
9
+ /** 교차 렌더링 방지 ID */
10
+ id?: string;
11
+ /** 강제 렌더링 여부 */
12
+ force?: boolean;
13
+ /** 렌더링할 값 */
14
+ value: unknown;
15
+ }
16
+
1
17
  /**
2
18
  * `u-link` 웹 컴포넌트를 React에서 사용할 수 있도록 래핑한 컴포넌트입니다.
3
19
  */
4
- export declare const ULink: any;
20
+ export declare const ULink: ReactWebComponent<ULink_2, {}>;
21
+
22
+ /**
23
+ * - 클라이언트 라우팅을 지원하는 링크 엘리먼트입니다.
24
+ * - 내부 링크는 클라이언트 라우팅을 수행하고, 외부 링크는 브라우저 기본 네비게이션을 사용합니다.
25
+ * - Ctrl/Meta/Shift/Alt, 중클릭/우클릭 등은 브라우저 기본 동작(새 탭, 컨텍스트 메뉴 등)을 그대로 유지합니다.
26
+ */
27
+ declare class ULink_2 extends LitElement {
28
+ /** 외부 링크 여부 */
29
+ private isExternal;
30
+ /**
31
+ * 링크 대상 target 속성
32
+ *
33
+ * - `_self`: 현재 창에서 링크 열기 (기본값)
34
+ * - `_blank`: 새 탭/창에서 링크 열기
35
+ * - `_parent`: 부모 프레임에서 링크 열기
36
+ * - `_top`: 최상위 프레임에서 링크 열기
37
+ */
38
+ target?: string;
39
+ /**
40
+ * 링크 대상 URL, 다음 사항에 따라 SPA 라우팅 또는 브라우저 네비게이션이 결정됩니다.
41
+ *
42
+ * - 속성을 정의하지 않으면 설정에서 지정한 `basepath`로 SPA 라우팅합니다.
43
+ * - http(s)로 시작하면 외부 링크로 간주하고 브라우저 네비게이션을 사용합니다.
44
+ * - 절대경로(/...)의 경우 `basepath`로 시작하면 SPA 라우팅합니다, 이외 브라우저 네비게이션을 사용합니다.
45
+ * - 상대경로는 (basepath + 상대경로)로 결합하여 SPA 라우팅합니다.
46
+ * - ?로 시작하면 현재 경로에 쿼리스트링을 추가하여 SPA 라우팅합니다.
47
+ * - #으로 시작하면 브라우저 기본 동작을 사용합니다.
48
+ */
49
+ href?: string;
50
+ connectedCallback(): void;
51
+ disconnectedCallback(): void;
52
+ protected willUpdate(changedProperties: PropertyValues): void;
53
+ render(): TemplateResult<1>;
54
+ /** a 태그에 주입할 href 값을 계산합니다. */
55
+ private compute;
56
+ /**
57
+ * 클릭 가로채기 핸들러
58
+ * - 좌클릭(0) + 보조키 없음(ctrl/meta/shift/alt 없음) + target이 _self일 때만 SPA 라우팅 고려
59
+ * - 그 외(중클릭/우클릭/보조키/target=_blank 등)는 브라우저 기본 동작 유지
60
+ */
61
+ private handleClick;
62
+ /** 클라이언트 라우팅을 위해 popstate 이벤트를 발생시킵니다. */
63
+ private dispatchPopstate;
64
+ /** basepath를 state에서 꺼내는 헬퍼 */
65
+ private getBasepath;
66
+ static styles: CSSResult;
67
+ }
5
68
 
6
69
  /**
7
70
  * `u-outlet` 웹 컴포넌트를 React에서 사용할 수 있도록 래핑한 컴포넌트입니다.
8
71
  */
9
- export declare const UOutlet: any;
72
+ export declare const UOutlet: ReactWebComponent<UOutlet_2, {}>;
73
+
74
+ /**
75
+ * LitElement 또는 React 컴포넌트를 렌더링해주는 웹컴포넌트 입니다.
76
+ */
77
+ declare class UOutlet_2 extends HTMLElement {
78
+ /** 교차 렌더링 방지 id */
79
+ private routeId?;
80
+ /** 실제 렌더링 컨텐츠 */
81
+ private root?;
82
+ /**
83
+ * 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
84
+ */
85
+ render({ id, value, force }: RenderOption): Promise<void>;
86
+ /**
87
+ * 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
88
+ */
89
+ reset(): void;
90
+ }
10
91
 
11
92
  export { }
package/dist/react.js CHANGED
@@ -1,19 +1,24 @@
1
+ import { s as UOutlet$1, t as ULink$1 } from "./share-sbAElOI7.js";
1
2
  import React from "react";
2
3
  import { createComponent } from "@lit/react";
3
- import { U as ULink$1, b as UOutlet$1 } from "./share-CG-3Tbuy.js";
4
- const ULink = createComponent({
5
- react: React,
6
- tagName: "u-link",
7
- elementClass: ULink$1,
8
- events: {}
4
+ //#region src/react.ts
5
+ /**
6
+ * `u-link` 웹 컴포넌트를 React에서 사용할 수 있도록 래핑한 컴포넌트입니다.
7
+ */
8
+ var ULink = createComponent({
9
+ react: React,
10
+ tagName: "u-link",
11
+ elementClass: ULink$1,
12
+ events: {}
9
13
  });
10
- const UOutlet = createComponent({
11
- react: React,
12
- tagName: "u-outlet",
13
- elementClass: UOutlet$1,
14
- events: {}
14
+ /**
15
+ * `u-outlet` 웹 컴포넌트를 React에서 사용할 수 있도록 래핑한 컴포넌트입니다.
16
+ */
17
+ var UOutlet = createComponent({
18
+ react: React,
19
+ tagName: "u-outlet",
20
+ elementClass: UOutlet$1,
21
+ events: {}
15
22
  });
16
- export {
17
- ULink,
18
- UOutlet
19
- };
23
+ //#endregion
24
+ export { ULink, UOutlet };
@@ -0,0 +1,237 @@
1
+ import { LitElement, css, html, render } from "lit";
2
+ import { customElement, property } from "lit/decorators.js";
3
+ import { ifDefined } from "lit/directives/if-defined.js";
4
+ //#region src/components/UOutlet.ts
5
+ /**
6
+ * LitElement 또는 React 컴포넌트를 렌더링해주는 웹컴포넌트 입니다.
7
+ */
8
+ var UOutlet = class extends HTMLElement {
9
+ /**
10
+ * 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
11
+ */
12
+ async render({ id, value, force }) {
13
+ if (this.routeId === id && force === false) return;
14
+ this.routeId = id;
15
+ this.reset();
16
+ if (value === null) throw new Error("Content is null and cannot be rendered.");
17
+ if (typeof value !== "object") throw new Error("Content is not a valid renderable object.");
18
+ if (value instanceof HTMLElement) {
19
+ this.replaceChildren(value);
20
+ this.root = void 0;
21
+ } else if ("_$litType$" in value) this.root = render(value, this);
22
+ else if ("$$typeof" in value) {
23
+ const { createRoot } = await import("react-dom/client");
24
+ this.root = createRoot(this);
25
+ this.root.render(value);
26
+ } else throw new Error("not supported content type for Outlet rendering.");
27
+ }
28
+ /**
29
+ * 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
30
+ */
31
+ reset() {
32
+ if (this.root && "_$litPart$" in this) delete this._$litPart$;
33
+ if (this.root && "unmount" in this.root) this.root.unmount();
34
+ this.root = void 0;
35
+ this.innerHTML = "";
36
+ }
37
+ };
38
+ customElements.define("u-outlet", UOutlet);
39
+ //#endregion
40
+ //#region src/internals/url-helpers.ts
41
+ /**
42
+ * 주어진 URL이 외부 링크인지 확인합니다.
43
+ *
44
+ * @param url 확인할 URL 문자열
45
+ * @return 외부 링크인 경우 true, 내부 링크인 경우 false
46
+ */
47
+ function isExternalUrl(url) {
48
+ if (!url) return false;
49
+ url = url.trim();
50
+ if (/^(?:mailto:|tel:|javascript:)/i.test(url)) return true;
51
+ if (url.startsWith("//")) return true;
52
+ try {
53
+ const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
54
+ const parsed = new URL(url, base);
55
+ if (/^(?:ftp:|ftps:|ws:|wss:)/i.test(parsed.protocol)) return true;
56
+ return parsed.origin !== new URL(base).origin;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+ /**
62
+ * URL 문자열을 파싱하여 RouteContext 객체로 반환합니다.
63
+ * - http(s)로 시작하는 절대 URL은 외부 링크로 간주됩니다.
64
+ * - 절대경로(/...)는 그대로 사용됩니다.
65
+ * - 상대경로는 basepath를 기준으로 절대경로로 변환됩니다.
66
+ * - 쿼리스트링(?)로 시작하는 쿼리는 현재 경로와 추가됩니다.
67
+ * - 해시(#)로 시작하는 해시는 현재 경로와 추가됩니다.
68
+ *
69
+ * @param url 파싱할 URL 문자열
70
+ * @param basepath 기준이 되는 basepath 문자열
71
+ * @returns 파싱된 RouteContext 객체
72
+ */
73
+ function parseUrl(url, basepath) {
74
+ let urlObj;
75
+ basepath = catchBasepath(basepath);
76
+ if (url.startsWith("http")) urlObj = new URL(url);
77
+ else if (url.startsWith("/")) urlObj = new URL(url, window.location.origin);
78
+ else if (url.startsWith("?")) urlObj = new URL(window.location.pathname + url, window.location.origin);
79
+ else if (url.startsWith("#")) urlObj = new URL(window.location.pathname + window.location.search + url, window.location.origin);
80
+ else urlObj = new URL(absolutePath(basepath, url), window.location.origin);
81
+ return {
82
+ href: urlObj.href,
83
+ origin: urlObj.origin,
84
+ basepath,
85
+ path: urlObj.href.replace(urlObj.origin, ""),
86
+ pathname: urlObj.pathname,
87
+ query: new URLSearchParams(urlObj.search),
88
+ hash: urlObj.hash,
89
+ params: {},
90
+ progress: () => {},
91
+ meta: {}
92
+ };
93
+ }
94
+ /**
95
+ * pathname 경로를 조합하여 절대경로를 반환합니다.
96
+ *
97
+ * @param paths 조합할 경로 문자열들
98
+ * @returns 조합된 절대경로 문자열
99
+ */
100
+ function absolutePath(...paths) {
101
+ paths = paths.map((p) => p.replace(/^\/|\/$/g, "")).filter((p) => p.length > 0);
102
+ if (paths.length === 0) return "/";
103
+ return "/" + paths.join("/");
104
+ }
105
+ /**
106
+ * basepath가 동적 패턴일 경우(RouteConfig에서 basepath가 :id 등으로 정의된 경우),
107
+ * 현재 경로에서 해당되는 패턴의 basepath를 추출하여 반환합니다.
108
+ *
109
+ * @param basepath 동적 패턴이 포함된 basepath 문자열
110
+ * @return 현재 경로에 매칭되는 basepath 문자열
111
+ * @example
112
+ * catchBasePath('/app/:id') => '/app/123'
113
+ */
114
+ function catchBasepath(basepath) {
115
+ if (basepath === "/") return basepath;
116
+ let pattern = new URLPattern({ pathname: basepath + "/*" });
117
+ let match = pattern.exec({ pathname: window.location.pathname });
118
+ if (match) {
119
+ const rawPath = match.pathname.input;
120
+ const restPath = match.pathname.groups?.["0"];
121
+ return restPath !== void 0 && restPath !== "" ? rawPath.replace("/" + restPath, "") : rawPath.replace(/\/$/, "");
122
+ }
123
+ pattern = new URLPattern({ pathname: `${basepath}{/}?` });
124
+ match = pattern.exec({ pathname: window.location.pathname });
125
+ if (match) return match.pathname.input;
126
+ return basepath;
127
+ }
128
+ //#endregion
129
+ //#region \0@oxc-project+runtime@0.122.0/helpers/decorateMetadata.js
130
+ function __decorateMetadata(k, v) {
131
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
132
+ }
133
+ //#endregion
134
+ //#region \0@oxc-project+runtime@0.122.0/helpers/decorate.js
135
+ function __decorate(decorators, target, key, desc) {
136
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
137
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
138
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
139
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
140
+ }
141
+ //#endregion
142
+ //#region src/components/ULink.ts
143
+ var ULink = class ULink extends LitElement {
144
+ constructor(..._args) {
145
+ super(..._args);
146
+ this.isExternal = false;
147
+ this.handleClick = (event) => {
148
+ if (event.defaultPrevented) return;
149
+ if (event.button !== 0) return;
150
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
151
+ if (this.target && this.target.toLowerCase() !== "_self") return;
152
+ const basepath = this.getBasepath();
153
+ if (!this.href) {
154
+ event.preventDefault();
155
+ this.dispatchPopstate(basepath, basepath);
156
+ return;
157
+ }
158
+ if (this.isExternal) return;
159
+ if (this.href.startsWith("#")) return;
160
+ event.preventDefault();
161
+ if (this.href.startsWith("?")) {
162
+ const url = window.location.pathname + this.href;
163
+ this.dispatchPopstate(basepath, url);
164
+ return;
165
+ }
166
+ if (this.href.startsWith("/")) {
167
+ if (!this.href.startsWith(basepath)) {
168
+ window.location.assign(this.href);
169
+ return;
170
+ }
171
+ this.dispatchPopstate(basepath, this.href);
172
+ return;
173
+ }
174
+ const url = absolutePath(basepath, this.href);
175
+ this.dispatchPopstate(basepath, url);
176
+ };
177
+ }
178
+ connectedCallback() {
179
+ super.connectedCallback();
180
+ this.addEventListener("click", this.handleClick);
181
+ }
182
+ disconnectedCallback() {
183
+ this.removeEventListener("click", this.handleClick);
184
+ super.disconnectedCallback();
185
+ }
186
+ willUpdate(changedProperties) {
187
+ super.willUpdate(changedProperties);
188
+ if (changedProperties.has("href")) this.isExternal = isExternalUrl(this.href || "");
189
+ }
190
+ render() {
191
+ return html`
192
+ <a target=${ifDefined(this.target)} href=${this.compute(this.href)}>
193
+ <slot></slot>
194
+ </a>
195
+ `;
196
+ }
197
+ /** a 태그에 주입할 href 값을 계산합니다. */
198
+ compute(href) {
199
+ const basepath = this.getBasepath();
200
+ if (!href) return window.location.origin + basepath;
201
+ if (this.isExternal) return href;
202
+ if (href.startsWith("/")) return href;
203
+ if (href.startsWith("#") || href.startsWith("?")) return href;
204
+ return absolutePath(basepath, href);
205
+ }
206
+ /** 클라이언트 라우팅을 위해 popstate 이벤트를 발생시킵니다. */
207
+ dispatchPopstate(basepath, url) {
208
+ window.history.pushState({ basepath }, "", url);
209
+ window.dispatchEvent(new PopStateEvent("popstate"));
210
+ }
211
+ /** basepath를 state에서 꺼내는 헬퍼 */
212
+ getBasepath() {
213
+ return window.history.state?.basepath || "/";
214
+ }
215
+ static {
216
+ this.styles = css`
217
+ :host {
218
+ cursor: pointer;
219
+ }
220
+
221
+ a {
222
+ text-decoration: none;
223
+
224
+ font-size: inherit;
225
+ font-weight: inherit;
226
+ font-family: inherit;
227
+ color: inherit;
228
+ cursor: inherit;
229
+ }
230
+ `;
231
+ }
232
+ };
233
+ __decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "target", void 0);
234
+ __decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "href", void 0);
235
+ ULink = __decorate([customElement("u-link")], ULink);
236
+ //#endregion
237
+ export { isExternalUrl as a, absolutePath as i, __decorate as n, parseUrl as o, __decorateMetadata as r, UOutlet as s, ULink as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/router",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
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",
@@ -40,10 +42,10 @@
40
42
  "build": "vite build"
41
43
  },
42
44
  "dependencies": {
43
- "lit": "^3.3.2"
45
+ "lit": "^3.3.2",
46
+ "@lit/react": "^1.0.8"
44
47
  },
45
48
  "peerDependencies": {
46
- "@lit/react": ">=1.0.0",
47
49
  "react": ">=18.0.0",
48
50
  "react-dom": ">=18.0.0"
49
51
  },
@@ -53,17 +55,14 @@
53
55
  },
54
56
  "react-dom": {
55
57
  "optional": true
56
- },
57
- "@lit/react": {
58
- "optional": true
59
58
  }
60
59
  },
61
60
  "devDependencies": {
62
- "@types/node": "^25.3.2",
61
+ "@types/node": "^25.5.0",
63
62
  "@types/react": "^19.2.14",
64
63
  "@types/react-dom": "^19.2.3",
65
64
  "typescript": "^5.9.3",
66
- "vite": "^7.3.1",
65
+ "vite": "^8.0.3",
67
66
  "vite-plugin-dts": "^4.5.4"
68
67
  }
69
68
  }
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: iyulab-router
3
+ description: Client-side SPA router for Lit and React with URLPattern-based matching, nested routes, fallback handling, and route events. Use when working with @iyulab/router — setting up routing, defining routes, handling navigation, nested layouts with <u-outlet>, or listening to route lifecycle events.
4
+ license: MIT
5
+ compatibility: Browser environments only (requires URLPattern and History API)
6
+ metadata:
7
+ author: iyulab
8
+ version: "0.7.4"
9
+ ---
10
+
11
+ # @iyulab/router
12
+
13
+ Client-side router supporting Lit and React renders, nested routes, 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
+ routes: [
44
+ { index: true, render: () => html`<home-page></home-page>` },
45
+ { path: '/user/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
46
+ ],
47
+ fallback: {
48
+ render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`
49
+ }
50
+ });
51
+ ```
52
+
53
+ ## RouteConfig Fields
54
+
55
+ | Field | Type | Description |
56
+ |---|---|---|
57
+ | `path` | `string \| URLPattern` | URLPattern path; omit when `index: true` |
58
+ | `index` | `true` | Marks route as index of its parent path |
59
+ | `render` | `(ctx) => unknown` | Returns Lit `TemplateResult`, React element, or `HTMLElement` |
60
+ | `children` | `RouteConfig[]` | Nested routes; parent must render `<u-outlet>` |
61
+ | `title` | `string` | Sets `document.title` on match |
62
+ | `meta` | `Record<string, unknown>` | Arbitrary metadata (auth, layout, analytics) |
63
+ | `force` | `boolean` | Force re-render on URL change (default `true` for leaf routes) |
64
+
65
+ ## RouteContext Fields
66
+
67
+ ```ts
68
+ ctx.params // URLPattern captured params
69
+ ctx.pathname // path without query/hash
70
+ ctx.path // full path including query + hash
71
+ ctx.query // URLSearchParams
72
+ ctx.meta // merged meta from matched route chain
73
+ ctx.progress // (value: number) => void — report 0–100 loading progress
74
+ ```
75
+
76
+ ## Nested Routes
77
+
78
+ Parent must include `<u-outlet>` in its render output:
79
+
80
+ ```ts
81
+ {
82
+ path: '/dashboard',
83
+ render: () => html`<dashboard-layout><u-outlet></u-outlet></dashboard-layout>`,
84
+ children: [
85
+ { index: true, render: () => html`<dashboard-home></dashboard-home>` },
86
+ { path: 'settings', render: () => html`<dashboard-settings></dashboard-settings>` }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ ## Navigation
92
+
93
+ ```ts
94
+ router.go('/path'); // programmatic navigation
95
+ router.go('relative-path'); // relative to basepath
96
+ router.destroy(); // remove event listeners
97
+
98
+ // From Lit template
99
+ html`<u-link href="/about">About</u-link>`
100
+
101
+ // From React
102
+ import { ULink } from '@iyulab/router/react';
103
+ <ULink href="/about">About</ULink>
104
+ ```
105
+
106
+ ## Route Events (window)
107
+
108
+ | Event | Fired when |
109
+ |---|---|
110
+ | `route-begin` | Navigation starts |
111
+ | `route-progress` | Async progress update (0–100) |
112
+ | `route-done` | Navigation completes |
113
+ | `route-error` | Routing error occurs |
114
+
115
+ ## Error Types (fallback ctx.error)
116
+
117
+ | Code | Class |
118
+ |---|---|
119
+ | `NOT_FOUND` | `NotFoundError` |
120
+ | `CONTENT_LOAD_ERROR` | `ContentLoadError` |
121
+ | `CONTENT_RENDER_ERROR` | `ContentRenderError` |
122
+
123
+ See [references/REFERENCE.md](references/REFERENCE.md) for URL parameter patterns, React usage, and advanced examples.