@iyulab/router 0.7.4 → 0.7.5
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/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 +5 -8
- package/dist/share-CG-3Tbuy.js +0 -224
package/dist/index.js
CHANGED
|
@@ -1,131 +1,144 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
constructor(context, error) {
|
|
72
|
-
super("route-error", context, false);
|
|
73
|
-
this.error = error;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
var __defProp = Object.defineProperty;
|
|
77
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
78
|
-
var __decorateClass = (decorators, target, key, kind) => {
|
|
79
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
80
|
-
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
81
|
-
if (decorator = decorators[i])
|
|
82
|
-
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
83
|
-
if (kind && result) __defProp(target, key, result);
|
|
84
|
-
return result;
|
|
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";
|
|
2
|
+
import { LitElement, css, html } from "lit";
|
|
3
|
+
import { customElement, property } from "lit/decorators.js";
|
|
4
|
+
//#region src/types/RouteError.ts
|
|
5
|
+
/**
|
|
6
|
+
* 라우팅 에러 정보
|
|
7
|
+
*/
|
|
8
|
+
var RouteError = class RouteError extends Error {
|
|
9
|
+
constructor(code, message, original) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "RouteError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.original = original;
|
|
14
|
+
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
15
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, RouteError);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* 페이지를 찾을 수 없을 때 발생하는 에러
|
|
20
|
+
*/
|
|
21
|
+
var NotFoundError = class extends RouteError {
|
|
22
|
+
constructor(path, original) {
|
|
23
|
+
super(404, `Page not found: ${path}`, original);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* u-outlet 요소를 찾을 수 없을 때 발생하는 에러
|
|
28
|
+
*/
|
|
29
|
+
var OutletMissingError = class extends RouteError {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("OUTLET_MISSING", "Router outlet element not found. Add <u-outlet> to your template.");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* 컨텐츠 로드시 나타나는 에러
|
|
36
|
+
*/
|
|
37
|
+
var ContentLoadError = class extends RouteError {
|
|
38
|
+
constructor(original) {
|
|
39
|
+
super("CONTENT_LOAD_FAILED", "Failed to load route content. Check browser console for details.", original);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* 컨텐츠 렌더링시 발생하는 에러
|
|
44
|
+
*/
|
|
45
|
+
var ContentRenderError = class extends RouteError {
|
|
46
|
+
constructor(original) {
|
|
47
|
+
super("CONTENT_RENDER_FAILED", "Failed to render route component. Check browser console for details.", original);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/types/RouteEvent.ts
|
|
52
|
+
/** 라우터 이벤트 기본 클래스 */
|
|
53
|
+
var RouteEvent = class extends Event {
|
|
54
|
+
constructor(type, context, cancelable = false) {
|
|
55
|
+
super(type, {
|
|
56
|
+
bubbles: true,
|
|
57
|
+
composed: true,
|
|
58
|
+
cancelable
|
|
59
|
+
});
|
|
60
|
+
this.context = context;
|
|
61
|
+
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
62
|
+
}
|
|
63
|
+
/** 이벤트가 취소되었는지 확인 */
|
|
64
|
+
get cancelled() {
|
|
65
|
+
return this.defaultPrevented;
|
|
66
|
+
}
|
|
67
|
+
/** 이벤트 취소 */
|
|
68
|
+
cancel() {
|
|
69
|
+
if (this.cancelable) this.preventDefault();
|
|
70
|
+
}
|
|
85
71
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
72
|
+
/**
|
|
73
|
+
* 라우트 시작 이벤트
|
|
74
|
+
*/
|
|
75
|
+
var RouteBeginEvent = class extends RouteEvent {
|
|
76
|
+
constructor(context) {
|
|
77
|
+
super("route-begin", context, false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* 라우트 진행 이벤트
|
|
82
|
+
*/
|
|
83
|
+
var RouteProgressEvent = class extends RouteEvent {
|
|
84
|
+
constructor(context, progress) {
|
|
85
|
+
super("route-progress", context, false);
|
|
86
|
+
this.progress = progress;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* 라우트 완료 이벤트
|
|
91
|
+
*/
|
|
92
|
+
var RouteDoneEvent = class extends RouteEvent {
|
|
93
|
+
constructor(context) {
|
|
94
|
+
super("route-done", context, false);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* 라우트 에러 이벤트
|
|
99
|
+
*/
|
|
100
|
+
var RouteErrorEvent = class extends RouteEvent {
|
|
101
|
+
constructor(context, error) {
|
|
102
|
+
super("route-error", context, false);
|
|
103
|
+
this.error = error;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/components/UErrorPage.ts
|
|
108
|
+
var _ref;
|
|
109
|
+
var UErrorPage = class UErrorPage extends LitElement {
|
|
110
|
+
render() {
|
|
111
|
+
const error = this.error || this.getDefaultError();
|
|
112
|
+
return html`
|
|
113
|
+
<div class="icon">${this.getErrorIcon(error.code)}</div>
|
|
92
114
|
<div class="code">${error.code}</div>
|
|
93
115
|
<div class="message">${error.message}</div>
|
|
94
116
|
`;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return "⏱️";
|
|
121
|
-
case 503:
|
|
122
|
-
return "🛠️";
|
|
123
|
-
default:
|
|
124
|
-
return "⚠️";
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
UErrorPage.styles = css`
|
|
117
|
+
}
|
|
118
|
+
/** 기본 에러 정보 반환 */
|
|
119
|
+
getDefaultError() {
|
|
120
|
+
return new RouteError(500, "Something went wrong. Please try again or contact support if the problem persists.");
|
|
121
|
+
}
|
|
122
|
+
/** 에러 코드에 따른 기본 아이콘 반환 */
|
|
123
|
+
getErrorIcon(code) {
|
|
124
|
+
const codeStr = String(code);
|
|
125
|
+
const numericCode = typeof code === "string" ? parseInt(code) : code;
|
|
126
|
+
switch (codeStr) {
|
|
127
|
+
case "OUTLET_MISSING": return "📦";
|
|
128
|
+
case "CONTENT_LOAD_FAILED": return "📡";
|
|
129
|
+
case "CONTENT_RENDER_FAILED": return "🎨";
|
|
130
|
+
}
|
|
131
|
+
switch (numericCode) {
|
|
132
|
+
case 404: return "🔍";
|
|
133
|
+
case 403: return "🚫";
|
|
134
|
+
case 401: return "🔐";
|
|
135
|
+
case 429: return "⏱️";
|
|
136
|
+
case 503: return "🛠️";
|
|
137
|
+
default: return "⚠️";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
static {
|
|
141
|
+
this.styles = css`
|
|
129
142
|
:host {
|
|
130
143
|
--error-icon-color: #4a5568;
|
|
131
144
|
--error-code-color: #1a202c;
|
|
@@ -179,264 +192,281 @@ UErrorPage.styles = css`
|
|
|
179
192
|
max-width: 600px;
|
|
180
193
|
}
|
|
181
194
|
`;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
], UErrorPage.prototype, "error",
|
|
185
|
-
UErrorPage =
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
__decorate([property({ type: Object }), __decorateMetadata("design:type", typeof (_ref = typeof RouteError !== "undefined" && RouteError) === "function" ? _ref : Object)], UErrorPage.prototype, "error", void 0);
|
|
198
|
+
UErrorPage = __decorate([customElement("u-error-page")], UErrorPage);
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/internals/crypto-helpers.ts
|
|
201
|
+
/**
|
|
202
|
+
* 브라우저에서 랜덤한 문자열ID를 생성합니다.
|
|
203
|
+
* - https일 경우 crypto.randomUUID()를 사용합니다.
|
|
204
|
+
* - 그 외의 경우 crypto.getRandomValues()를 사용합니다.
|
|
205
|
+
*
|
|
206
|
+
* @return 랜덤한 ID 문자열
|
|
207
|
+
*/
|
|
188
208
|
function getRandomID() {
|
|
189
|
-
|
|
209
|
+
return window.isSecureContext ? window.crypto.randomUUID() : window.crypto.getRandomValues(new Uint32Array(1))[0].toString(16);
|
|
190
210
|
}
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/internals/element-helpers.ts
|
|
213
|
+
/**
|
|
214
|
+
* `u-outlet` 엘리먼트를 찾아 반환합니다.
|
|
215
|
+
*
|
|
216
|
+
* @param element 검색을 시작할 HTMLElement
|
|
217
|
+
* @returns 찾은 UOutlet 엘리먼트 또는 undefined
|
|
218
|
+
*/
|
|
191
219
|
function findOutlet(element) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return void 0;
|
|
220
|
+
if (element.tagName === "U-OUTLET") return element;
|
|
221
|
+
const roots = element.shadowRoot ? [element.shadowRoot, element] : [element];
|
|
222
|
+
for (const root of roots) for (const child of Array.from(root.children)) {
|
|
223
|
+
const result = findOutlet(child);
|
|
224
|
+
if (result) return result;
|
|
225
|
+
}
|
|
201
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* `u-outlet` 엘리먼트를 찾아 반환합니다. 없으면 에러를 던집니다.
|
|
229
|
+
*
|
|
230
|
+
* @param element 검색을 시작할 HTMLElement
|
|
231
|
+
* @returns 찾은 UOutlet 엘리먼트
|
|
232
|
+
* @throws OutletMissingError `u-outlet` 엘리먼트를 찾지 못한 경우
|
|
233
|
+
*/
|
|
202
234
|
function findOutletOrThrow(element) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
return outlet;
|
|
235
|
+
const outlet = findOutlet(element);
|
|
236
|
+
if (!outlet) throw new OutletMissingError();
|
|
237
|
+
return outlet;
|
|
208
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* 주어진 엘리먼트 내에서 `u-outlet`이 준비될 때까지 대기합니다.
|
|
241
|
+
*
|
|
242
|
+
* @param element 대기할 엘리먼트
|
|
243
|
+
* @param timeout 타임아웃 시간(밀리초, 기본값: 10_000ms)
|
|
244
|
+
* @returns 준비된 `u-outlet` 엘리먼트
|
|
245
|
+
*/
|
|
209
246
|
async function waitOutlet(element, timeout = 1e4) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`Timed out waiting for <u-outlet> inside <${element.tagName.toLowerCase()}>. Ensure that the router root element contains a <u-outlet> child.`
|
|
218
|
-
);
|
|
247
|
+
const start = performance.now();
|
|
248
|
+
while (performance.now() - start < timeout) {
|
|
249
|
+
const outlet = findOutlet(element);
|
|
250
|
+
if (outlet) return outlet;
|
|
251
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`Timed out waiting for <u-outlet> inside <${element.tagName.toLowerCase()}>. Ensure that the router root element contains a <u-outlet> child.`);
|
|
219
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* 이벤트에서 composedPath()/closest를 사용하여 A 태그를 찾아 반환합니다.
|
|
257
|
+
*
|
|
258
|
+
* @param event 이벤트 객체
|
|
259
|
+
* @returns 찾은 A 태그 엘리먼트 또는 null
|
|
260
|
+
*/
|
|
220
261
|
function findAnchorFrom(event) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (!tgt) return null;
|
|
230
|
-
return tgt.closest("a");
|
|
262
|
+
const targets = event.composedPath() || [];
|
|
263
|
+
if (targets && targets.length) for (const node of targets) {
|
|
264
|
+
if (!(node instanceof Element)) continue;
|
|
265
|
+
if (node.tagName === "A") return node;
|
|
266
|
+
}
|
|
267
|
+
const tgt = event.target;
|
|
268
|
+
if (!tgt) return null;
|
|
269
|
+
return tgt.closest("a");
|
|
231
270
|
}
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/internals/route-helpers.ts
|
|
273
|
+
/**
|
|
274
|
+
* 라우트들을 다음 사항에 따라 재귀적으로 재설정합니다.
|
|
275
|
+
* - 각 라우트에 고유 `id`를 랜덤하게 부여합니다.
|
|
276
|
+
* - `path`를 URLPattern 객체로 변환합니다.
|
|
277
|
+
* - 자식 라우트가 있으면 재귀적으로 재설정합니다.
|
|
278
|
+
*
|
|
279
|
+
* @param routes 설정할 라우트 배열
|
|
280
|
+
* @param basepath 기준이 되는 basepath 문자열
|
|
281
|
+
* @returns 재설정된 라우트 배열
|
|
282
|
+
*/
|
|
232
283
|
function setRoutes(routes, basepath) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
if (route.children && route.children.length > 0) {
|
|
254
|
-
const childBasepath = route.path.pathname.replace("{/}?", "");
|
|
255
|
-
route.children = setRoutes(route.children, childBasepath);
|
|
256
|
-
route.force ||= false;
|
|
257
|
-
} else {
|
|
258
|
-
route.force ||= true;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return routes;
|
|
284
|
+
for (const route of routes) {
|
|
285
|
+
route.id ||= getRandomID();
|
|
286
|
+
route.ignoreCase ||= false;
|
|
287
|
+
if (route.index === true) {
|
|
288
|
+
route.path = new URLPattern({ pathname: `${basepath}{/}?` }, { ignoreCase: route.ignoreCase });
|
|
289
|
+
route.force ||= true;
|
|
290
|
+
} else {
|
|
291
|
+
if (typeof route.path === "string") {
|
|
292
|
+
const absolutePathStr = absolutePath(basepath, route.path);
|
|
293
|
+
route.path = new URLPattern({ pathname: `${absolutePathStr}{/}?` }, { ignoreCase: route.ignoreCase });
|
|
294
|
+
} else if (route.path instanceof URLPattern) {} else route.path = new URLPattern({ pathname: `${basepath}{/}?` }, { ignoreCase: route.ignoreCase });
|
|
295
|
+
if (route.children && route.children.length > 0) {
|
|
296
|
+
const childBasepath = route.path.pathname.replace("{/}?", "");
|
|
297
|
+
route.children = setRoutes(route.children, childBasepath);
|
|
298
|
+
route.force ||= false;
|
|
299
|
+
} else route.force ||= true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return routes;
|
|
263
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* URLPattern을 사용하여 경로와 일치하는 라우트들을 자식 라우트까지 포함하여 반환합니다.
|
|
306
|
+
* 반환된 배열은 상위 라우트부터 하위 라우트 순서로 정렬됩니다.
|
|
307
|
+
*
|
|
308
|
+
* @param routes 검사할 라우트 배열
|
|
309
|
+
* @param pathname 검사할 경로 이름
|
|
310
|
+
* @returns 일치하는 라우트 배열
|
|
311
|
+
*/
|
|
264
312
|
function getRoutes(routes, pathname) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
} else {
|
|
276
|
-
throw new Error("Route path must be an instance of URLPattern, Something wrong in setRoutes function.");
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return [];
|
|
280
|
-
}
|
|
281
|
-
class Router {
|
|
282
|
-
constructor(config) {
|
|
283
|
-
this.handleWindowPopstate = async (_) => {
|
|
284
|
-
const href = window.location.href;
|
|
285
|
-
await this.go(href);
|
|
286
|
-
};
|
|
287
|
-
this.handleDocumentClick = async (e) => {
|
|
288
|
-
try {
|
|
289
|
-
if (e.defaultPrevented) return;
|
|
290
|
-
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey) return;
|
|
291
|
-
const anchor = findAnchorFrom(e);
|
|
292
|
-
if (!anchor) return;
|
|
293
|
-
const href = anchor.getAttribute("href") || anchor.href;
|
|
294
|
-
if (!href) return;
|
|
295
|
-
if (isExternalUrl(href)) return;
|
|
296
|
-
if (anchor.hasAttribute("download")) return;
|
|
297
|
-
if (anchor.getAttribute("rel") === "external") return;
|
|
298
|
-
if (anchor.target && anchor.target !== "") return;
|
|
299
|
-
e.preventDefault();
|
|
300
|
-
await this.go(anchor.href);
|
|
301
|
-
} catch {
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
this.destroy();
|
|
305
|
-
this._rootElement = config.root;
|
|
306
|
-
this._basepath = absolutePath(config.basepath || "/");
|
|
307
|
-
this._routes = setRoutes(config.routes || [], this._basepath);
|
|
308
|
-
this._fallback = config.fallback;
|
|
309
|
-
window.addEventListener("popstate", this.handleWindowPopstate);
|
|
310
|
-
if (config.useIntercept !== false) {
|
|
311
|
-
document.addEventListener("click", this.handleDocumentClick);
|
|
312
|
-
}
|
|
313
|
-
if (config.initialLoad !== false) {
|
|
314
|
-
void waitOutlet(this._rootElement).then(() => {
|
|
315
|
-
this.go(window.location.href);
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
/** 객체를 정리하고 이벤트 리스너를 제거합니다. */
|
|
320
|
-
destroy() {
|
|
321
|
-
window.removeEventListener("popstate", this.handleWindowPopstate);
|
|
322
|
-
document.removeEventListener("click", this.handleDocumentClick);
|
|
323
|
-
this._requestID = void 0;
|
|
324
|
-
this._context = void 0;
|
|
325
|
-
}
|
|
326
|
-
/** 라우터의 기본 경로 반환 */
|
|
327
|
-
get basepath() {
|
|
328
|
-
return this._basepath;
|
|
329
|
-
}
|
|
330
|
-
/** 등록된 라우트 정보 반환 */
|
|
331
|
-
get routes() {
|
|
332
|
-
return this._routes;
|
|
333
|
-
}
|
|
334
|
-
/** 현재 라우팅 정보 반환 */
|
|
335
|
-
get context() {
|
|
336
|
-
return this._context;
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
|
|
340
|
-
* @param href 이동할 경로
|
|
341
|
-
*/
|
|
342
|
-
async go(href) {
|
|
343
|
-
const requestID = getRandomID();
|
|
344
|
-
this._requestID = requestID;
|
|
345
|
-
const context = parseUrl(href, this._basepath);
|
|
346
|
-
if (context.href !== window.location.href) {
|
|
347
|
-
window.history.pushState({ basepath: context.basepath }, "", context.href);
|
|
348
|
-
} else {
|
|
349
|
-
window.history.replaceState({ basepath: context.basepath }, "", context.href);
|
|
350
|
-
}
|
|
351
|
-
const progressCallback = (value) => {
|
|
352
|
-
if (this._requestID !== requestID) return;
|
|
353
|
-
const progress = Math.max(0, Math.min(100, Math.round(value)));
|
|
354
|
-
window.dispatchEvent(new RouteProgressEvent(context, progress));
|
|
355
|
-
};
|
|
356
|
-
context.progress = progressCallback;
|
|
357
|
-
let outlet = void 0;
|
|
358
|
-
try {
|
|
359
|
-
if (this._requestID !== requestID) return;
|
|
360
|
-
window.dispatchEvent(new RouteBeginEvent(context));
|
|
361
|
-
const routes = getRoutes(this._routes, context.pathname);
|
|
362
|
-
const lastRoute = routes[routes.length - 1];
|
|
363
|
-
if (lastRoute && lastRoute.path instanceof URLPattern) {
|
|
364
|
-
context.params = lastRoute.path.exec({ pathname: context.pathname })?.pathname.groups || {};
|
|
365
|
-
}
|
|
366
|
-
const mergedMeta = {};
|
|
367
|
-
for (const route of routes) {
|
|
368
|
-
if (route.meta) Object.assign(mergedMeta, route.meta);
|
|
369
|
-
}
|
|
370
|
-
context.meta = mergedMeta;
|
|
371
|
-
this._context = context;
|
|
372
|
-
outlet = findOutletOrThrow(this._rootElement);
|
|
373
|
-
let title = void 0;
|
|
374
|
-
let content = null;
|
|
375
|
-
if (routes.length === 0) {
|
|
376
|
-
throw new NotFoundError(context.href);
|
|
377
|
-
}
|
|
378
|
-
for (const route of routes) {
|
|
379
|
-
if (this._requestID !== requestID) return;
|
|
380
|
-
if (!route.render) continue;
|
|
381
|
-
try {
|
|
382
|
-
content = await route.render(context);
|
|
383
|
-
if (content === false || content === void 0 || content === null) {
|
|
384
|
-
throw new Error("Failed to load content for the route.");
|
|
385
|
-
}
|
|
386
|
-
} catch (LoadError) {
|
|
387
|
-
throw new ContentLoadError(LoadError);
|
|
388
|
-
}
|
|
389
|
-
try {
|
|
390
|
-
outlet.render({ id: route.id, value: content, force: route.force });
|
|
391
|
-
} catch (renderError) {
|
|
392
|
-
throw new ContentRenderError(renderError);
|
|
393
|
-
}
|
|
394
|
-
outlet = findOutlet(outlet) || outlet;
|
|
395
|
-
title = route.title || title;
|
|
396
|
-
}
|
|
397
|
-
document.title = title || document.title;
|
|
398
|
-
window.dispatchEvent(new RouteDoneEvent(context));
|
|
399
|
-
} catch (error) {
|
|
400
|
-
const routeError = error instanceof RouteError ? error : new RouteError(
|
|
401
|
-
error?.status || error?.code || "UNKNOWN_ERROR",
|
|
402
|
-
error?.message || "An unexpected error occurred",
|
|
403
|
-
error
|
|
404
|
-
);
|
|
405
|
-
window.dispatchEvent(new RouteErrorEvent(context, routeError));
|
|
406
|
-
console.error("Routing error:", routeError.original);
|
|
407
|
-
try {
|
|
408
|
-
if (this._fallback && this._fallback.render && outlet) {
|
|
409
|
-
const fallbackContent = await this._fallback.render({ ...context, error: routeError });
|
|
410
|
-
outlet.render({ id: "#fallback", value: fallbackContent, force: true });
|
|
411
|
-
document.title = this._fallback.title || document.title;
|
|
412
|
-
} else {
|
|
413
|
-
const errorContent = new UErrorPage();
|
|
414
|
-
errorContent.error = error;
|
|
415
|
-
if (outlet) {
|
|
416
|
-
outlet.render({ id: "#error", value: errorContent, force: true });
|
|
417
|
-
} else {
|
|
418
|
-
document.body.innerHTML = "";
|
|
419
|
-
document.body.appendChild(errorContent);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
} catch (pageError) {
|
|
423
|
-
console.error("Failed to render error component:", pageError);
|
|
424
|
-
console.error("Original error:", routeError.original || routeError);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
313
|
+
for (const route of routes) {
|
|
314
|
+
if (route.index !== true && route.children && route.children.length > 0) {
|
|
315
|
+
const childRoutes = getRoutes(route.children, pathname);
|
|
316
|
+
if (childRoutes.length > 0) return [route, ...childRoutes];
|
|
317
|
+
}
|
|
318
|
+
if (route.path instanceof URLPattern) {
|
|
319
|
+
if (route.path.test({ pathname })) return [route];
|
|
320
|
+
} else throw new Error("Route path must be an instance of URLPattern, Something wrong in setRoutes function.");
|
|
321
|
+
}
|
|
322
|
+
return [];
|
|
428
323
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/Router.ts
|
|
326
|
+
/**
|
|
327
|
+
* `lit-element`, `react`를 지원하는 SPA 클라이언트 라우터 객체입니다.
|
|
328
|
+
*/
|
|
329
|
+
var Router = class {
|
|
330
|
+
constructor(config) {
|
|
331
|
+
this.handleWindowPopstate = async (_) => {
|
|
332
|
+
const href = window.location.href;
|
|
333
|
+
await this.go(href);
|
|
334
|
+
};
|
|
335
|
+
this.handleDocumentClick = async (e) => {
|
|
336
|
+
try {
|
|
337
|
+
if (e.defaultPrevented) return;
|
|
338
|
+
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey) return;
|
|
339
|
+
const anchor = findAnchorFrom(e);
|
|
340
|
+
if (!anchor) return;
|
|
341
|
+
const href = anchor.getAttribute("href") || anchor.href;
|
|
342
|
+
if (!href) return;
|
|
343
|
+
if (isExternalUrl(href)) return;
|
|
344
|
+
if (anchor.hasAttribute("download")) return;
|
|
345
|
+
if (anchor.getAttribute("rel") === "external") return;
|
|
346
|
+
if (anchor.target && anchor.target !== "") return;
|
|
347
|
+
e.preventDefault();
|
|
348
|
+
await this.go(anchor.href);
|
|
349
|
+
} catch {}
|
|
350
|
+
};
|
|
351
|
+
this.destroy();
|
|
352
|
+
this._rootElement = config.root;
|
|
353
|
+
this._basepath = absolutePath(config.basepath || "/");
|
|
354
|
+
this._routes = setRoutes(config.routes || [], this._basepath);
|
|
355
|
+
this._fallback = config.fallback;
|
|
356
|
+
window.addEventListener("popstate", this.handleWindowPopstate);
|
|
357
|
+
if (config.useIntercept !== false) document.addEventListener("click", this.handleDocumentClick);
|
|
358
|
+
if (config.initialLoad !== false) waitOutlet(this._rootElement).then(() => {
|
|
359
|
+
this.go(window.location.href);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** 객체를 정리하고 이벤트 리스너를 제거합니다. */
|
|
363
|
+
destroy() {
|
|
364
|
+
window.removeEventListener("popstate", this.handleWindowPopstate);
|
|
365
|
+
document.removeEventListener("click", this.handleDocumentClick);
|
|
366
|
+
this._requestID = void 0;
|
|
367
|
+
this._context = void 0;
|
|
368
|
+
}
|
|
369
|
+
/** 라우터의 기본 경로 반환 */
|
|
370
|
+
get basepath() {
|
|
371
|
+
return this._basepath;
|
|
372
|
+
}
|
|
373
|
+
/** 등록된 라우트 정보 반환 */
|
|
374
|
+
get routes() {
|
|
375
|
+
return this._routes;
|
|
376
|
+
}
|
|
377
|
+
/** 현재 라우팅 정보 반환 */
|
|
378
|
+
get context() {
|
|
379
|
+
return this._context;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
|
|
383
|
+
* @param href 이동할 경로
|
|
384
|
+
*/
|
|
385
|
+
async go(href) {
|
|
386
|
+
const requestID = getRandomID();
|
|
387
|
+
this._requestID = requestID;
|
|
388
|
+
const context = parseUrl(href, this._basepath);
|
|
389
|
+
if (context.href !== window.location.href) window.history.pushState({ basepath: context.basepath }, "", context.href);
|
|
390
|
+
else window.history.replaceState({ basepath: context.basepath }, "", context.href);
|
|
391
|
+
const progressCallback = (value) => {
|
|
392
|
+
if (this._requestID !== requestID) return;
|
|
393
|
+
const progress = Math.max(0, Math.min(100, Math.round(value)));
|
|
394
|
+
window.dispatchEvent(new RouteProgressEvent(context, progress));
|
|
395
|
+
};
|
|
396
|
+
context.progress = progressCallback;
|
|
397
|
+
let outlet = void 0;
|
|
398
|
+
try {
|
|
399
|
+
if (this._requestID !== requestID) return;
|
|
400
|
+
window.dispatchEvent(new RouteBeginEvent(context));
|
|
401
|
+
const routes = getRoutes(this._routes, context.pathname);
|
|
402
|
+
const lastRoute = routes[routes.length - 1];
|
|
403
|
+
if (lastRoute && lastRoute.path instanceof URLPattern) context.params = lastRoute.path.exec({ pathname: context.pathname })?.pathname.groups || {};
|
|
404
|
+
const mergedMeta = {};
|
|
405
|
+
for (const route of routes) if (route.meta) Object.assign(mergedMeta, route.meta);
|
|
406
|
+
context.meta = mergedMeta;
|
|
407
|
+
this._context = context;
|
|
408
|
+
outlet = findOutletOrThrow(this._rootElement);
|
|
409
|
+
let title = void 0;
|
|
410
|
+
let content = null;
|
|
411
|
+
if (routes.length === 0) throw new NotFoundError(context.href);
|
|
412
|
+
for (const route of routes) {
|
|
413
|
+
if (this._requestID !== requestID) return;
|
|
414
|
+
if (!route.render) continue;
|
|
415
|
+
try {
|
|
416
|
+
content = await route.render(context);
|
|
417
|
+
if (content === false || content === void 0 || content === null) throw new Error("Failed to load content for the route.");
|
|
418
|
+
} catch (LoadError) {
|
|
419
|
+
throw new ContentLoadError(LoadError);
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
outlet.render({
|
|
423
|
+
id: route.id,
|
|
424
|
+
value: content,
|
|
425
|
+
force: route.force
|
|
426
|
+
});
|
|
427
|
+
} catch (renderError) {
|
|
428
|
+
throw new ContentRenderError(renderError);
|
|
429
|
+
}
|
|
430
|
+
outlet = findOutlet(outlet) || outlet;
|
|
431
|
+
title = route.title || title;
|
|
432
|
+
}
|
|
433
|
+
document.title = title || document.title;
|
|
434
|
+
window.dispatchEvent(new RouteDoneEvent(context));
|
|
435
|
+
} catch (error) {
|
|
436
|
+
const routeError = error instanceof RouteError ? error : new RouteError(error?.status || error?.code || "UNKNOWN_ERROR", error?.message || "An unexpected error occurred", error);
|
|
437
|
+
window.dispatchEvent(new RouteErrorEvent(context, routeError));
|
|
438
|
+
console.error("Routing error:", routeError.original);
|
|
439
|
+
try {
|
|
440
|
+
if (this._fallback && this._fallback.render && outlet) {
|
|
441
|
+
const fallbackContent = await this._fallback.render({
|
|
442
|
+
...context,
|
|
443
|
+
error: routeError
|
|
444
|
+
});
|
|
445
|
+
outlet.render({
|
|
446
|
+
id: "#fallback",
|
|
447
|
+
value: fallbackContent,
|
|
448
|
+
force: true
|
|
449
|
+
});
|
|
450
|
+
document.title = this._fallback.title || document.title;
|
|
451
|
+
} else {
|
|
452
|
+
const errorContent = new UErrorPage();
|
|
453
|
+
errorContent.error = error;
|
|
454
|
+
if (outlet) outlet.render({
|
|
455
|
+
id: "#error",
|
|
456
|
+
value: errorContent,
|
|
457
|
+
force: true
|
|
458
|
+
});
|
|
459
|
+
else {
|
|
460
|
+
document.body.innerHTML = "";
|
|
461
|
+
document.body.appendChild(errorContent);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} catch (pageError) {
|
|
465
|
+
console.error("Failed to render error component:", pageError);
|
|
466
|
+
console.error("Original error:", routeError.original || routeError);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
442
470
|
};
|
|
471
|
+
//#endregion
|
|
472
|
+
export { ContentLoadError, ContentRenderError, NotFoundError, OutletMissingError, RouteBeginEvent, RouteDoneEvent, RouteError, RouteErrorEvent, RouteProgressEvent, Router, ULink, UOutlet };
|