@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/CHANGELOG.md +155 -0
- package/README.md +16 -9
- package/dist/index.js +400 -370
- package/dist/react.d.ts +83 -2
- package/dist/react.js +20 -15
- package/dist/share-sbAElOI7.js +237 -0
- package/package.json +7 -8
- package/skills/iyulab-router/SKILL.md +123 -0
- package/skills/iyulab-router/references/REFERENCE.md +175 -0
- package/dist/share-CG-3Tbuy.js +0 -224
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# @iyulab/router — Reference
|
|
2
|
+
|
|
3
|
+
## URL Parameter Patterns
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// Required
|
|
7
|
+
{ path: '/user/:id' }
|
|
8
|
+
|
|
9
|
+
// Optional
|
|
10
|
+
{ path: '/posts/:category?' }
|
|
11
|
+
|
|
12
|
+
// Wildcard (catch-all)
|
|
13
|
+
{ path: '/docs/:path*' }
|
|
14
|
+
|
|
15
|
+
// Multiple
|
|
16
|
+
{ path: '/org/:orgId/repo/:repoId' }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Access via `ctx.params.id`, `ctx.params.category`, etc.
|
|
20
|
+
|
|
21
|
+
## Query String
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// URL: /search?q=hello&page=2
|
|
25
|
+
ctx.query.get('q') // 'hello'
|
|
26
|
+
ctx.query.get('page') // '2'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Route Meta
|
|
30
|
+
|
|
31
|
+
Meta from the full matched chain is merged (parent → child, child overrides):
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
{
|
|
35
|
+
path: '/admin',
|
|
36
|
+
meta: { requiresAuth: true, layout: 'admin' },
|
|
37
|
+
render: (ctx) => {
|
|
38
|
+
// ctx.meta === { requiresAuth: true, layout: 'admin' }
|
|
39
|
+
return html`<admin-layout><u-outlet></u-outlet></admin-layout>`;
|
|
40
|
+
},
|
|
41
|
+
children: [
|
|
42
|
+
{
|
|
43
|
+
path: 'settings',
|
|
44
|
+
meta: { role: 'superadmin' },
|
|
45
|
+
render: (ctx) => {
|
|
46
|
+
// ctx.meta === { requiresAuth: true, layout: 'admin', role: 'superadmin' }
|
|
47
|
+
return html`<admin-settings></admin-settings>`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Async Render with Progress
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
{
|
|
58
|
+
path: '/user/:id',
|
|
59
|
+
render: async (ctx) => {
|
|
60
|
+
ctx.progress(20);
|
|
61
|
+
const user = await fetchUser(ctx.params.id);
|
|
62
|
+
ctx.progress(80);
|
|
63
|
+
return html`<user-profile .data=${user}></user-profile>`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Route Events
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
window.addEventListener('route-begin', (e) => {
|
|
72
|
+
console.log('navigating to:', e.context.pathname);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
window.addEventListener('route-progress', (e) => {
|
|
76
|
+
progressBar.value = e.progress; // 0–100
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
window.addEventListener('route-done', (e) => {
|
|
80
|
+
analytics.track(e.context.pathname);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
window.addEventListener('route-error', (e) => {
|
|
84
|
+
errorTracker.report(e.error);
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Error Fallback
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
fallback: {
|
|
92
|
+
render: (ctx) => {
|
|
93
|
+
const { code, message } = ctx.error;
|
|
94
|
+
if (code === 'NOT_FOUND') return html`<not-found-page></not-found-page>`;
|
|
95
|
+
return html`<error-page .message=${message}></error-page>`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## React Usage
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { UOutlet, ULink } from '@iyulab/router/react';
|
|
104
|
+
|
|
105
|
+
export function AppRoot() {
|
|
106
|
+
return (
|
|
107
|
+
<div>
|
|
108
|
+
<nav>
|
|
109
|
+
<ULink href="/">Home</ULink>
|
|
110
|
+
<ULink href="/about">About</ULink>
|
|
111
|
+
</nav>
|
|
112
|
+
<main>
|
|
113
|
+
<UOutlet />
|
|
114
|
+
</main>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Mixed Lit + React routes:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const routes = [
|
|
124
|
+
{
|
|
125
|
+
path: '/lit-page',
|
|
126
|
+
render: () => html`<my-lit-component></my-lit-component>`
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
path: '/react-page',
|
|
130
|
+
render: () => <MyReactComponent />
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
path: '/raw-element',
|
|
134
|
+
render: (ctx) => {
|
|
135
|
+
const el = document.createElement('my-element');
|
|
136
|
+
el.data = ctx.params;
|
|
137
|
+
return el;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
];
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## RouterConfig Options
|
|
144
|
+
|
|
145
|
+
| Option | Default | Description |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `root` | — | Mount element (required) |
|
|
148
|
+
| `basepath` | `'/'` | URL base path |
|
|
149
|
+
| `routes` | `[]` | Route definitions |
|
|
150
|
+
| `fallback` | built-in error page | Error/404 handler |
|
|
151
|
+
| `useIntercept` | `true` | Intercept `<a>` clicks for client routing |
|
|
152
|
+
| `initialLoad` | `true` | Auto-navigate to current URL on init |
|
|
153
|
+
|
|
154
|
+
## Lit Element Integration
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import "@iyulab/router"; // registers <u-outlet> and <u-link>
|
|
158
|
+
import { LitElement, html } from 'lit';
|
|
159
|
+
import { customElement } from 'lit/decorators.js';
|
|
160
|
+
|
|
161
|
+
@customElement('app-root')
|
|
162
|
+
export class AppRoot extends LitElement {
|
|
163
|
+
render() {
|
|
164
|
+
return html`
|
|
165
|
+
<nav>
|
|
166
|
+
<u-link href="/">Home</u-link>
|
|
167
|
+
<u-link href="/about">About</u-link>
|
|
168
|
+
</nav>
|
|
169
|
+
<main>
|
|
170
|
+
<u-outlet></u-outlet>
|
|
171
|
+
</main>
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
package/dist/share-CG-3Tbuy.js
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { render, css, LitElement, html } from "lit";
|
|
2
|
-
import { property, customElement } from "lit/decorators.js";
|
|
3
|
-
import { ifDefined } from "lit/directives/if-defined.js";
|
|
4
|
-
class UOutlet extends HTMLElement {
|
|
5
|
-
/**
|
|
6
|
-
* 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
|
|
7
|
-
*/
|
|
8
|
-
async render({ id, value, force }) {
|
|
9
|
-
if (this.routeId === id && force === false) return;
|
|
10
|
-
this.routeId = id;
|
|
11
|
-
this.reset();
|
|
12
|
-
if (value === null) {
|
|
13
|
-
throw new Error("Content is null and cannot be rendered.");
|
|
14
|
-
}
|
|
15
|
-
if (typeof value !== "object") {
|
|
16
|
-
throw new Error("Content is not a valid renderable object.");
|
|
17
|
-
}
|
|
18
|
-
if (value instanceof HTMLElement) {
|
|
19
|
-
this.replaceChildren(value);
|
|
20
|
-
this.root = void 0;
|
|
21
|
-
} else if ("_$litType$" in value) {
|
|
22
|
-
this.root = render(value, this);
|
|
23
|
-
} else if ("$$typeof" in value) {
|
|
24
|
-
const { createRoot } = await import("react-dom/client");
|
|
25
|
-
this.root = createRoot(this);
|
|
26
|
-
this.root.render(value);
|
|
27
|
-
} else {
|
|
28
|
-
throw new Error("not supported content type for Outlet rendering.");
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
|
|
33
|
-
*/
|
|
34
|
-
reset() {
|
|
35
|
-
if (this.root && "_$litPart$" in this) {
|
|
36
|
-
delete this._$litPart$;
|
|
37
|
-
}
|
|
38
|
-
if (this.root && "unmount" in this.root) {
|
|
39
|
-
this.root.unmount();
|
|
40
|
-
}
|
|
41
|
-
this.root = void 0;
|
|
42
|
-
this.innerHTML = "";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
customElements.define("u-outlet", UOutlet);
|
|
46
|
-
function isExternalUrl(url) {
|
|
47
|
-
if (!url) return false;
|
|
48
|
-
url = url.trim();
|
|
49
|
-
if (/^(?:mailto:|tel:|javascript:)/i.test(url)) return true;
|
|
50
|
-
if (url.startsWith("//")) return true;
|
|
51
|
-
try {
|
|
52
|
-
const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
53
|
-
const parsed = new URL(url, base);
|
|
54
|
-
if (/^(?:ftp:|ftps:|ws:|wss:)/i.test(parsed.protocol)) return true;
|
|
55
|
-
return parsed.origin !== new URL(base).origin;
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function parseUrl(url, basepath) {
|
|
61
|
-
let urlObj;
|
|
62
|
-
basepath = catchBasepath(basepath);
|
|
63
|
-
if (url.startsWith("http")) {
|
|
64
|
-
urlObj = new URL(url);
|
|
65
|
-
} else if (url.startsWith("/")) {
|
|
66
|
-
urlObj = new URL(url, window.location.origin);
|
|
67
|
-
} else if (url.startsWith("?")) {
|
|
68
|
-
urlObj = new URL(window.location.pathname + url, window.location.origin);
|
|
69
|
-
} else if (url.startsWith("#")) {
|
|
70
|
-
urlObj = new URL(window.location.pathname + window.location.search + url, window.location.origin);
|
|
71
|
-
} else {
|
|
72
|
-
urlObj = new URL(absolutePath(basepath, url), window.location.origin);
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
href: urlObj.href,
|
|
76
|
-
origin: urlObj.origin,
|
|
77
|
-
basepath,
|
|
78
|
-
path: urlObj.href.replace(urlObj.origin, ""),
|
|
79
|
-
pathname: urlObj.pathname,
|
|
80
|
-
query: new URLSearchParams(urlObj.search),
|
|
81
|
-
hash: urlObj.hash,
|
|
82
|
-
params: {},
|
|
83
|
-
progress: () => {
|
|
84
|
-
},
|
|
85
|
-
meta: {}
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function absolutePath(...paths) {
|
|
89
|
-
paths = paths.map((p) => p.replace(/^\/|\/$/g, "")).filter((p) => p.length > 0);
|
|
90
|
-
if (paths.length === 0) return "/";
|
|
91
|
-
return "/" + paths.join("/");
|
|
92
|
-
}
|
|
93
|
-
function catchBasepath(basepath) {
|
|
94
|
-
if (basepath === "/") return basepath;
|
|
95
|
-
let pattern = new URLPattern({ pathname: basepath + "/*" });
|
|
96
|
-
let match = pattern.exec({ pathname: window.location.pathname });
|
|
97
|
-
if (match) {
|
|
98
|
-
const rawPath = match.pathname.input;
|
|
99
|
-
const restPath = match.pathname.groups?.["0"];
|
|
100
|
-
return restPath !== void 0 && restPath !== "" ? rawPath.replace("/" + restPath, "") : rawPath.replace(/\/$/, "");
|
|
101
|
-
}
|
|
102
|
-
pattern = new URLPattern({ pathname: `${basepath}{/}?` });
|
|
103
|
-
match = pattern.exec({ pathname: window.location.pathname });
|
|
104
|
-
if (match) {
|
|
105
|
-
return match.pathname.input;
|
|
106
|
-
}
|
|
107
|
-
return basepath;
|
|
108
|
-
}
|
|
109
|
-
var __defProp = Object.defineProperty;
|
|
110
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
111
|
-
var __decorateClass = (decorators, target, key, kind) => {
|
|
112
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
113
|
-
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
114
|
-
if (decorator = decorators[i])
|
|
115
|
-
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
116
|
-
if (kind && result) __defProp(target, key, result);
|
|
117
|
-
return result;
|
|
118
|
-
};
|
|
119
|
-
let ULink = class extends LitElement {
|
|
120
|
-
constructor() {
|
|
121
|
-
super(...arguments);
|
|
122
|
-
this.isExternal = false;
|
|
123
|
-
this.handleClick = (event) => {
|
|
124
|
-
if (event.defaultPrevented) return;
|
|
125
|
-
if (event.button !== 0) return;
|
|
126
|
-
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
127
|
-
if (this.target && this.target.toLowerCase() !== "_self") return;
|
|
128
|
-
const basepath = this.getBasepath();
|
|
129
|
-
if (!this.href) {
|
|
130
|
-
event.preventDefault();
|
|
131
|
-
this.dispatchPopstate(basepath, basepath);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (this.isExternal) return;
|
|
135
|
-
if (this.href.startsWith("#")) return;
|
|
136
|
-
event.preventDefault();
|
|
137
|
-
if (this.href.startsWith("?")) {
|
|
138
|
-
const url2 = window.location.pathname + this.href;
|
|
139
|
-
this.dispatchPopstate(basepath, url2);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (this.href.startsWith("/")) {
|
|
143
|
-
if (!this.href.startsWith(basepath)) {
|
|
144
|
-
window.location.assign(this.href);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
this.dispatchPopstate(basepath, this.href);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const url = absolutePath(basepath, this.href);
|
|
151
|
-
this.dispatchPopstate(basepath, url);
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
connectedCallback() {
|
|
155
|
-
super.connectedCallback();
|
|
156
|
-
this.addEventListener("click", this.handleClick);
|
|
157
|
-
}
|
|
158
|
-
disconnectedCallback() {
|
|
159
|
-
this.removeEventListener("click", this.handleClick);
|
|
160
|
-
super.disconnectedCallback();
|
|
161
|
-
}
|
|
162
|
-
willUpdate(changedProperties) {
|
|
163
|
-
super.willUpdate(changedProperties);
|
|
164
|
-
if (changedProperties.has("href")) {
|
|
165
|
-
this.isExternal = isExternalUrl(this.href || "");
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
render() {
|
|
169
|
-
return html`
|
|
170
|
-
<a target=${ifDefined(this.target)} href=${this.compute(this.href)}>
|
|
171
|
-
<slot></slot>
|
|
172
|
-
</a>
|
|
173
|
-
`;
|
|
174
|
-
}
|
|
175
|
-
/** a 태그에 주입할 href 값을 계산합니다. */
|
|
176
|
-
compute(href) {
|
|
177
|
-
const basepath = this.getBasepath();
|
|
178
|
-
if (!href) return window.location.origin + basepath;
|
|
179
|
-
if (this.isExternal) return href;
|
|
180
|
-
if (href.startsWith("/")) return href;
|
|
181
|
-
if (href.startsWith("#") || href.startsWith("?")) return href;
|
|
182
|
-
return absolutePath(basepath, href);
|
|
183
|
-
}
|
|
184
|
-
/** 클라이언트 라우팅을 위해 popstate 이벤트를 발생시킵니다. */
|
|
185
|
-
dispatchPopstate(basepath, url) {
|
|
186
|
-
window.history.pushState({ basepath }, "", url);
|
|
187
|
-
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
188
|
-
}
|
|
189
|
-
/** basepath를 state에서 꺼내는 헬퍼 */
|
|
190
|
-
getBasepath() {
|
|
191
|
-
return window.history.state?.basepath || "/";
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
ULink.styles = css`
|
|
195
|
-
:host {
|
|
196
|
-
cursor: pointer;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
a {
|
|
200
|
-
text-decoration: none;
|
|
201
|
-
|
|
202
|
-
font-size: inherit;
|
|
203
|
-
font-weight: inherit;
|
|
204
|
-
font-family: inherit;
|
|
205
|
-
color: inherit;
|
|
206
|
-
cursor: inherit;
|
|
207
|
-
}
|
|
208
|
-
`;
|
|
209
|
-
__decorateClass([
|
|
210
|
-
property({ type: String })
|
|
211
|
-
], ULink.prototype, "target", 2);
|
|
212
|
-
__decorateClass([
|
|
213
|
-
property({ type: String })
|
|
214
|
-
], ULink.prototype, "href", 2);
|
|
215
|
-
ULink = __decorateClass([
|
|
216
|
-
customElement("u-link")
|
|
217
|
-
], ULink);
|
|
218
|
-
export {
|
|
219
|
-
ULink as U,
|
|
220
|
-
absolutePath as a,
|
|
221
|
-
UOutlet as b,
|
|
222
|
-
isExternalUrl as i,
|
|
223
|
-
parseUrl as p
|
|
224
|
-
};
|