mikuru 1.0.14 → 1.0.16
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 +38 -1
- package/README.md +38 -7
- package/dist/compiler/analyzeTemplate.js +0 -3
- package/dist/compiler/analyzeTemplate.js.map +1 -1
- package/dist/compiler/generate.js +972 -73
- package/dist/compiler/generate.js.map +1 -1
- package/dist/router/index.d.ts +168 -7
- package/dist/router/index.js +610 -66
- package/dist/router/index.js.map +1 -1
- package/dist/runtime/dom.d.ts +4 -0
- package/dist/runtime/dom.js +39 -0
- package/dist/runtime/dom.js.map +1 -1
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/package.json +26 -26
package/dist/router/index.js
CHANGED
|
@@ -1,50 +1,235 @@
|
|
|
1
|
-
import { effect, ref, unwrap } from "../runtime/index.js";
|
|
1
|
+
import { effect, inject, provide, ref, unwrap } from "../runtime/index.js";
|
|
2
|
+
export const NavigationFailureType = {
|
|
3
|
+
aborted: "aborted",
|
|
4
|
+
cancelled: "cancelled",
|
|
5
|
+
duplicated: "duplicated"
|
|
6
|
+
};
|
|
7
|
+
export function defineRoutes(routes) {
|
|
8
|
+
return routes;
|
|
9
|
+
}
|
|
2
10
|
const notFoundRoute = { path: "/:pathMatch(.*)*", name: "not-found" };
|
|
11
|
+
const routerKey = Symbol.for("mikuru.router");
|
|
12
|
+
const routerErrorNotifiers = new WeakMap();
|
|
13
|
+
const routerReadyTrackers = new WeakMap();
|
|
14
|
+
const maxNavigationRedirects = 20;
|
|
15
|
+
export function provideRouter(router) {
|
|
16
|
+
provide(routerKey, router);
|
|
17
|
+
}
|
|
18
|
+
export function useRouter() {
|
|
19
|
+
const router = inject(routerKey);
|
|
20
|
+
if (!router) {
|
|
21
|
+
throw new Error("No router was provided. Call provideRouter(router) in an ancestor component.");
|
|
22
|
+
}
|
|
23
|
+
return router;
|
|
24
|
+
}
|
|
25
|
+
export function useRoute() {
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
return new Proxy({}, {
|
|
28
|
+
get(_target, key) {
|
|
29
|
+
return router.currentRoute.value[key];
|
|
30
|
+
},
|
|
31
|
+
has(_target, key) {
|
|
32
|
+
return key in router.currentRoute.value;
|
|
33
|
+
},
|
|
34
|
+
ownKeys() {
|
|
35
|
+
return Reflect.ownKeys(router.currentRoute.value);
|
|
36
|
+
},
|
|
37
|
+
getOwnPropertyDescriptor(_target, key) {
|
|
38
|
+
return Object.getOwnPropertyDescriptor(router.currentRoute.value, key);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function isNavigationFailure(value, type) {
|
|
43
|
+
const failure = value;
|
|
44
|
+
const isFailure = !!failure &&
|
|
45
|
+
typeof failure === "object" &&
|
|
46
|
+
typeof failure.type === "string" &&
|
|
47
|
+
typeof failure.message === "string" &&
|
|
48
|
+
!!failure.to &&
|
|
49
|
+
!!failure.from;
|
|
50
|
+
return isFailure && (!type || failure.type === type);
|
|
51
|
+
}
|
|
3
52
|
export function createRouter(options) {
|
|
4
53
|
const history = options.history ?? createWebHashHistory();
|
|
5
|
-
const routes = options.
|
|
6
|
-
const
|
|
54
|
+
const routes = options.routes.slice();
|
|
55
|
+
const fallbackRoute = options.notFound ? { ...notFoundRoute, component: options.notFound } : undefined;
|
|
56
|
+
const loadingComponent = options.loadingComponent;
|
|
57
|
+
const errorComponent = options.errorComponent;
|
|
58
|
+
const queryCodec = {
|
|
59
|
+
parseQuery: options.parseQuery ?? parseRouteQuery,
|
|
60
|
+
stringifyQuery: options.stringifyQuery ?? stringifyRouteQuery
|
|
61
|
+
};
|
|
62
|
+
let matcher = createMatcher(readMatcherRoutes());
|
|
7
63
|
const beforeGuards = [];
|
|
8
64
|
const afterHooks = [];
|
|
9
|
-
const
|
|
65
|
+
const errorHandlers = [];
|
|
66
|
+
const initial = resolveRouteTarget(history.location());
|
|
10
67
|
const currentRoute = ref(initial);
|
|
68
|
+
const pendingReady = new Set();
|
|
11
69
|
let listeningStop;
|
|
12
70
|
let navigationId = 0;
|
|
13
|
-
async function navigate(to, replace) {
|
|
14
|
-
const target = resolveLocation(stringifyLocation(to), matcher);
|
|
71
|
+
async function navigate(to, replace, redirectCount = 0) {
|
|
15
72
|
const from = currentRoute.value;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
73
|
+
let target = from;
|
|
74
|
+
try {
|
|
75
|
+
target = resolveRouteTarget(to, from.path);
|
|
76
|
+
const id = ++navigationId;
|
|
77
|
+
if (target.fullPath === from.fullPath) {
|
|
78
|
+
return createNavigationFailure(NavigationFailureType.duplicated, target, from);
|
|
79
|
+
}
|
|
80
|
+
const guards = [...beforeGuards, ...readBeforeEnterGuards(target.matchedRecords)];
|
|
81
|
+
for (const guard of guards) {
|
|
82
|
+
const result = await guard(target, from);
|
|
83
|
+
if (id !== navigationId)
|
|
84
|
+
return createNavigationFailure(NavigationFailureType.cancelled, target, from);
|
|
85
|
+
if (result === false) {
|
|
86
|
+
const failure = createNavigationFailure(NavigationFailureType.aborted, target, from);
|
|
87
|
+
for (const hook of afterHooks)
|
|
88
|
+
hook(target, from, failure);
|
|
89
|
+
return failure;
|
|
90
|
+
}
|
|
91
|
+
if (typeof result === "string" || (result && typeof result === "object")) {
|
|
92
|
+
if (redirectCount >= maxNavigationRedirects) {
|
|
93
|
+
throw new Error("Too many navigation guard redirects.");
|
|
94
|
+
}
|
|
95
|
+
return navigate(result, true, redirectCount + 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (replace)
|
|
99
|
+
history.replace(target.fullPath);
|
|
100
|
+
else
|
|
101
|
+
history.push(target.fullPath);
|
|
102
|
+
currentRoute.value = target;
|
|
103
|
+
for (const hook of afterHooks) {
|
|
104
|
+
hook(target, from);
|
|
25
105
|
}
|
|
106
|
+
await trackReady(handleScroll(target, from));
|
|
107
|
+
return target;
|
|
26
108
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
109
|
+
catch (error) {
|
|
110
|
+
notifyRouterError(error, target, from);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function preload(to) {
|
|
115
|
+
const from = currentRoute.value;
|
|
116
|
+
let target = from;
|
|
117
|
+
try {
|
|
118
|
+
target = resolveRouteTarget(to, from.path);
|
|
119
|
+
await trackReady(Promise.all(target.matchedRecords.map((record) => record.component ? resolveRouteComponent(record) : Promise.resolve())));
|
|
120
|
+
return target;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
notifyRouterError(error, target, from);
|
|
124
|
+
throw error;
|
|
34
125
|
}
|
|
35
|
-
return target;
|
|
36
126
|
}
|
|
37
127
|
function syncFromHistory() {
|
|
38
|
-
currentRoute.value =
|
|
128
|
+
currentRoute.value = resolveRouteTarget(history.location());
|
|
39
129
|
}
|
|
40
|
-
|
|
130
|
+
function syncCurrentRoute() {
|
|
131
|
+
currentRoute.value = resolveRouteTarget(currentRoute.value.fullPath, currentRoute.value.path);
|
|
132
|
+
}
|
|
133
|
+
function readMatcherRoutes() {
|
|
134
|
+
return fallbackRoute ? [...routes, fallbackRoute] : routes;
|
|
135
|
+
}
|
|
136
|
+
function resolveRouteTarget(to, basePath) {
|
|
137
|
+
let target = resolveLocation(stringifyLocation(to, matcher, basePath, queryCodec.stringifyQuery), matcher, queryCodec.parseQuery);
|
|
138
|
+
for (let redirects = 0; redirects < 10; redirects += 1) {
|
|
139
|
+
const redirect = target.matched?.redirect;
|
|
140
|
+
if (!redirect)
|
|
141
|
+
return target;
|
|
142
|
+
const next = typeof redirect === "function" ? redirect(target) : redirect;
|
|
143
|
+
target = resolveLocation(stringifyLocation(next, matcher, target.path, queryCodec.stringifyQuery), matcher, queryCodec.parseQuery);
|
|
144
|
+
}
|
|
145
|
+
throw new Error("Too many route redirects.");
|
|
146
|
+
}
|
|
147
|
+
function replaceMatcher(nextRoutes = routes) {
|
|
148
|
+
matcher = createMatcher(fallbackRoute ? [...nextRoutes, fallbackRoute] : nextRoutes);
|
|
149
|
+
}
|
|
150
|
+
function addRoute(parentOrRecord, record) {
|
|
151
|
+
if (typeof parentOrRecord !== "string") {
|
|
152
|
+
const nextRoutes = [...routes, parentOrRecord];
|
|
153
|
+
replaceMatcher(nextRoutes);
|
|
154
|
+
routes.push(parentOrRecord);
|
|
155
|
+
syncCurrentRoute();
|
|
156
|
+
return () => {
|
|
157
|
+
if (parentOrRecord.name)
|
|
158
|
+
removeRoute(parentOrRecord.name);
|
|
159
|
+
else if (removeRouteRecord(routes, parentOrRecord)) {
|
|
160
|
+
replaceMatcher();
|
|
161
|
+
syncCurrentRoute();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (!record) {
|
|
166
|
+
throw new Error("router.addRoute(parentName, record) requires a route record.");
|
|
167
|
+
}
|
|
168
|
+
const parent = matcher.byName.get(parentOrRecord)?.record;
|
|
169
|
+
if (!parent) {
|
|
170
|
+
throw new Error(`Unknown route name: ${parentOrRecord}`);
|
|
171
|
+
}
|
|
172
|
+
const nextChildren = [...(parent.children ?? []), record];
|
|
173
|
+
const previousChildren = parent.children;
|
|
174
|
+
parent.children = nextChildren;
|
|
175
|
+
try {
|
|
176
|
+
replaceMatcher();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
parent.children = previousChildren;
|
|
180
|
+
replaceMatcher();
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
syncCurrentRoute();
|
|
184
|
+
return () => {
|
|
185
|
+
if (record.name)
|
|
186
|
+
removeRoute(record.name);
|
|
187
|
+
else if (parent.children && removeRouteRecord(parent.children, record))
|
|
188
|
+
syncCurrentRoute();
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function removeRoute(name) {
|
|
192
|
+
if (!matcher.byName.has(name))
|
|
193
|
+
return false;
|
|
194
|
+
const removed = removeRouteByName(routes, name);
|
|
195
|
+
if (!removed)
|
|
196
|
+
return false;
|
|
197
|
+
replaceMatcher();
|
|
198
|
+
syncCurrentRoute();
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
async function handleScroll(to, from) {
|
|
202
|
+
if (history.mode === "memory")
|
|
203
|
+
return;
|
|
204
|
+
if (options.scrollBehavior) {
|
|
205
|
+
const position = await options.scrollBehavior(to, from);
|
|
206
|
+
if (!position)
|
|
207
|
+
return;
|
|
208
|
+
scrollToPosition(position);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const metaScroll = to.meta.scroll;
|
|
212
|
+
if (metaScroll !== undefined) {
|
|
213
|
+
const position = typeof metaScroll === "function" ? await metaScroll(to, from) : metaScroll;
|
|
214
|
+
if (!position)
|
|
215
|
+
return;
|
|
216
|
+
scrollToPosition(position);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
scrollToDefaultPosition(to);
|
|
220
|
+
}
|
|
221
|
+
function trackReady(promise) {
|
|
222
|
+
pendingReady.add(promise);
|
|
223
|
+
return promise.finally(() => pendingReady.delete(promise));
|
|
224
|
+
}
|
|
225
|
+
const router = {
|
|
41
226
|
currentRoute,
|
|
42
227
|
routes,
|
|
43
228
|
push(to) {
|
|
44
|
-
return navigate(to, false);
|
|
229
|
+
return trackReady(navigate(to, false));
|
|
45
230
|
},
|
|
46
231
|
replace(to) {
|
|
47
|
-
return navigate(to, true);
|
|
232
|
+
return trackReady(navigate(to, true));
|
|
48
233
|
},
|
|
49
234
|
back() {
|
|
50
235
|
history.back();
|
|
@@ -53,7 +238,11 @@ export function createRouter(options) {
|
|
|
53
238
|
history.forward();
|
|
54
239
|
},
|
|
55
240
|
resolve(to) {
|
|
56
|
-
return
|
|
241
|
+
return resolveRouteTarget(to, currentRoute.value.path);
|
|
242
|
+
},
|
|
243
|
+
preload,
|
|
244
|
+
isReady() {
|
|
245
|
+
return Promise.all(Array.from(pendingReady)).then(() => undefined);
|
|
57
246
|
},
|
|
58
247
|
beforeEach(guard) {
|
|
59
248
|
beforeGuards.push(guard);
|
|
@@ -63,6 +252,15 @@ export function createRouter(options) {
|
|
|
63
252
|
afterHooks.push(hook);
|
|
64
253
|
return () => removeItem(afterHooks, hook);
|
|
65
254
|
},
|
|
255
|
+
onError(handler) {
|
|
256
|
+
errorHandlers.push(handler);
|
|
257
|
+
return () => removeItem(errorHandlers, handler);
|
|
258
|
+
},
|
|
259
|
+
addRoute,
|
|
260
|
+
removeRoute,
|
|
261
|
+
hasRoute(name) {
|
|
262
|
+
return matcher.byName.has(name);
|
|
263
|
+
},
|
|
66
264
|
listen() {
|
|
67
265
|
if (listeningStop)
|
|
68
266
|
return listeningStop;
|
|
@@ -74,9 +272,26 @@ export function createRouter(options) {
|
|
|
74
272
|
};
|
|
75
273
|
},
|
|
76
274
|
createHref(to) {
|
|
77
|
-
return history.createHref(stringifyLocation(to));
|
|
78
|
-
}
|
|
275
|
+
return history.createHref(stringifyLocation(to, matcher, currentRoute.value.path, queryCodec.stringifyQuery));
|
|
276
|
+
},
|
|
277
|
+
loadingComponent,
|
|
278
|
+
errorComponent
|
|
79
279
|
};
|
|
280
|
+
routerErrorNotifiers.set(router, notifyRouterError);
|
|
281
|
+
routerReadyTrackers.set(router, trackReady);
|
|
282
|
+
return router;
|
|
283
|
+
function notifyRouterError(error, to, from) {
|
|
284
|
+
for (const handler of errorHandlers) {
|
|
285
|
+
try {
|
|
286
|
+
handler(error, to, from);
|
|
287
|
+
}
|
|
288
|
+
catch (handlerError) {
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
throw handlerError;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
80
295
|
}
|
|
81
296
|
export function createWebHistory(base = "") {
|
|
82
297
|
const normalizedBase = normalizeBase(base);
|
|
@@ -186,18 +401,55 @@ export const RouterView = {
|
|
|
186
401
|
const anchor = document.createComment("mikuru-router-view");
|
|
187
402
|
const cleanup = [];
|
|
188
403
|
let child;
|
|
404
|
+
let renderId = 0;
|
|
189
405
|
target.appendChild(anchor);
|
|
190
406
|
const stop = effect(() => {
|
|
191
407
|
const router = getRouterProp(props);
|
|
192
408
|
const route = router.currentRoute.value;
|
|
193
|
-
const
|
|
409
|
+
const depth = Number(unwrap(props.depth) ?? 0);
|
|
410
|
+
const record = route.matchedRecords[depth];
|
|
411
|
+
const id = ++renderId;
|
|
194
412
|
child?.unmount();
|
|
195
413
|
child = undefined;
|
|
196
|
-
if (!component)
|
|
414
|
+
if (!record?.component)
|
|
197
415
|
return;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
416
|
+
if (isLazyRouteComponent(record.component)) {
|
|
417
|
+
const loadingComponent = record.loadingComponent ?? router.loadingComponent;
|
|
418
|
+
if (loadingComponent) {
|
|
419
|
+
const fragment = anchor.ownerDocument.createDocumentFragment();
|
|
420
|
+
child = loadingComponent.mount(fragment, { route, router, __mikuru_context: props.__mikuru_context });
|
|
421
|
+
anchor.parentNode?.insertBefore(fragment, anchor);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const componentPromise = resolveRouteComponent(record);
|
|
425
|
+
void (routerReadyTrackers.get(router)?.(componentPromise) ?? componentPromise).catch(() => undefined);
|
|
426
|
+
void componentPromise
|
|
427
|
+
.then((component) => {
|
|
428
|
+
if (id !== renderId)
|
|
429
|
+
return;
|
|
430
|
+
child?.unmount();
|
|
431
|
+
child = undefined;
|
|
432
|
+
const fragment = anchor.ownerDocument.createDocumentFragment();
|
|
433
|
+
child = component.mount(fragment, createRouteComponentProps(record, route, router, props.__mikuru_context));
|
|
434
|
+
anchor.parentNode?.insertBefore(fragment, anchor);
|
|
435
|
+
})
|
|
436
|
+
.catch((error) => {
|
|
437
|
+
if (id !== renderId)
|
|
438
|
+
return;
|
|
439
|
+
child?.unmount();
|
|
440
|
+
child = undefined;
|
|
441
|
+
routerErrorNotifiers.get(router)?.(error, route);
|
|
442
|
+
const errorComponent = record.errorComponent ?? router.errorComponent;
|
|
443
|
+
if (!errorComponent) {
|
|
444
|
+
setTimeout(() => {
|
|
445
|
+
throw error;
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const fragment = anchor.ownerDocument.createDocumentFragment();
|
|
450
|
+
child = errorComponent.mount(fragment, { error, route, router, __mikuru_context: props.__mikuru_context });
|
|
451
|
+
anchor.parentNode?.insertBefore(fragment, anchor);
|
|
452
|
+
});
|
|
201
453
|
});
|
|
202
454
|
cleanup.push(stop, () => child?.unmount(), () => anchor.remove());
|
|
203
455
|
return {
|
|
@@ -213,25 +465,51 @@ export const RouterLink = {
|
|
|
213
465
|
mount(target, props = {}) {
|
|
214
466
|
const anchor = document.createElement("a");
|
|
215
467
|
const cleanup = [];
|
|
216
|
-
const
|
|
468
|
+
const hasChildren = typeof props.children === "function";
|
|
469
|
+
const text = () => {
|
|
470
|
+
const label = unwrap(props.label) ?? unwrap(props.childrenText);
|
|
471
|
+
if (label !== undefined)
|
|
472
|
+
return String(label);
|
|
473
|
+
const to = readToProp(props);
|
|
474
|
+
if (typeof to === "string")
|
|
475
|
+
return to;
|
|
476
|
+
if ("name" in to)
|
|
477
|
+
return to.name;
|
|
478
|
+
return to.path;
|
|
479
|
+
};
|
|
217
480
|
const navigate = (event) => {
|
|
218
481
|
const router = getRouterProp(props);
|
|
219
|
-
const to = String(unwrap(props.to) ?? "/");
|
|
220
482
|
event.preventDefault();
|
|
221
|
-
void (unwrap(props.replace) ? router.replace(
|
|
483
|
+
void (unwrap(props.replace) ? router.replace(readToProp(props)) : router.push(readToProp(props)));
|
|
484
|
+
};
|
|
485
|
+
const preload = () => {
|
|
486
|
+
if (!unwrap(props.preload))
|
|
487
|
+
return;
|
|
488
|
+
const router = getRouterProp(props);
|
|
489
|
+
void router.preload(readToProp(props)).catch(() => undefined);
|
|
222
490
|
};
|
|
223
491
|
anchor.addEventListener("click", navigate);
|
|
492
|
+
anchor.addEventListener("mouseenter", preload);
|
|
493
|
+
anchor.addEventListener("focus", preload);
|
|
224
494
|
cleanup.push(() => anchor.removeEventListener("click", navigate));
|
|
495
|
+
cleanup.push(() => anchor.removeEventListener("mouseenter", preload));
|
|
496
|
+
cleanup.push(() => anchor.removeEventListener("focus", preload));
|
|
497
|
+
if (hasChildren) {
|
|
498
|
+
const cleanupResult = props.children(anchor, {});
|
|
499
|
+
if (cleanupResult)
|
|
500
|
+
cleanup.push(cleanupResult);
|
|
501
|
+
}
|
|
225
502
|
const stop = effect(() => {
|
|
226
503
|
const router = getRouterProp(props);
|
|
227
|
-
const to =
|
|
504
|
+
const to = readToProp(props);
|
|
228
505
|
const targetRoute = router.resolve(to);
|
|
229
506
|
const activeClass = String(unwrap(props.activeClass) ?? "router-link-active");
|
|
230
507
|
const exactActiveClass = String(unwrap(props.exactActiveClass) ?? "router-link-exact-active");
|
|
231
508
|
const isExactActive = router.currentRoute.value.fullPath === targetRoute.fullPath;
|
|
232
509
|
const isActive = isExactActive || isPathActive(router.currentRoute.value.path, targetRoute.path);
|
|
233
510
|
anchor.href = router.createHref(to);
|
|
234
|
-
|
|
511
|
+
if (!hasChildren)
|
|
512
|
+
anchor.textContent = text();
|
|
235
513
|
anchor.classList.toggle(activeClass, isActive);
|
|
236
514
|
anchor.classList.toggle(exactActiveClass, isExactActive);
|
|
237
515
|
if (isExactActive) {
|
|
@@ -255,38 +533,191 @@ export const RouterLink = {
|
|
|
255
533
|
function getRouterProp(props) {
|
|
256
534
|
const router = unwrap(props.router);
|
|
257
535
|
if (!router || typeof router !== "object" || !("currentRoute" in router)) {
|
|
258
|
-
|
|
536
|
+
const contextRouter = injectRouterFromContext(props.__mikuru_context);
|
|
537
|
+
if (contextRouter)
|
|
538
|
+
return contextRouter;
|
|
539
|
+
throw new Error("RouterView and RouterLink require a router prop or provided router context.");
|
|
259
540
|
}
|
|
260
541
|
return router;
|
|
261
542
|
}
|
|
543
|
+
function injectRouterFromContext(context) {
|
|
544
|
+
for (let current = context; current; current = current.parent) {
|
|
545
|
+
const router = current.provides?.get(routerKey);
|
|
546
|
+
if (router && typeof router === "object" && "currentRoute" in router) {
|
|
547
|
+
return router;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
async function resolveRouteComponent(record) {
|
|
553
|
+
if (record.__mikuru_resolvedComponent)
|
|
554
|
+
return record.__mikuru_resolvedComponent;
|
|
555
|
+
const component = record.component;
|
|
556
|
+
if (!component) {
|
|
557
|
+
throw new Error(`Route ${record.path} does not have a component.`);
|
|
558
|
+
}
|
|
559
|
+
if (isRouteComponent(component)) {
|
|
560
|
+
record.__mikuru_resolvedComponent = component;
|
|
561
|
+
return component;
|
|
562
|
+
}
|
|
563
|
+
const loaded = await component();
|
|
564
|
+
const resolved = isRouteComponent(loaded) ? loaded : loaded.default;
|
|
565
|
+
if (!isRouteComponent(resolved)) {
|
|
566
|
+
throw new Error(`Lazy route component for ${record.path} did not resolve to a component.`);
|
|
567
|
+
}
|
|
568
|
+
record.__mikuru_resolvedComponent = resolved;
|
|
569
|
+
return resolved;
|
|
570
|
+
}
|
|
571
|
+
function isRouteComponent(component) {
|
|
572
|
+
return !!component && typeof component === "object" && typeof component.mount === "function";
|
|
573
|
+
}
|
|
574
|
+
function isLazyRouteComponent(component) {
|
|
575
|
+
return typeof component === "function";
|
|
576
|
+
}
|
|
577
|
+
function createRouteComponentProps(record, route, router, context) {
|
|
578
|
+
return {
|
|
579
|
+
...resolveRouteProps(record, route),
|
|
580
|
+
route,
|
|
581
|
+
router,
|
|
582
|
+
__mikuru_context: context
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function resolveRouteProps(record, route) {
|
|
586
|
+
if (record.props === true)
|
|
587
|
+
return { ...route.params };
|
|
588
|
+
if (typeof record.props === "function")
|
|
589
|
+
return record.props(route) ?? {};
|
|
590
|
+
if (record.props && typeof record.props === "object")
|
|
591
|
+
return { ...record.props };
|
|
592
|
+
return {};
|
|
593
|
+
}
|
|
594
|
+
function compileRoutePath(path) {
|
|
595
|
+
const keys = [];
|
|
596
|
+
const normalized = normalizePath(path);
|
|
597
|
+
if (normalized === "/")
|
|
598
|
+
return { keys, pattern: /^\/$/ };
|
|
599
|
+
const source = normalized
|
|
600
|
+
.split("/")
|
|
601
|
+
.filter(Boolean)
|
|
602
|
+
.map((segment) => compileRouteSegment(segment, keys))
|
|
603
|
+
.join("");
|
|
604
|
+
return { keys, pattern: new RegExp(`^${source}$`) };
|
|
605
|
+
}
|
|
606
|
+
function compileRouteSegment(segment, keys) {
|
|
607
|
+
const param = parseRouteParamSegment(segment);
|
|
608
|
+
if (!param)
|
|
609
|
+
return `/${escapeRegExp(segment)}`;
|
|
610
|
+
keys.push({ name: param.name, repeat: param.repeat });
|
|
611
|
+
if (param.pattern === ".*") {
|
|
612
|
+
return param.optional ? "(?:/(.*))?" : "/(.*)";
|
|
613
|
+
}
|
|
614
|
+
if (param.repeat) {
|
|
615
|
+
const repeated = "([^/]+(?:/[^/]+)*)";
|
|
616
|
+
return param.optional ? `(?:/${repeated})?` : `/${repeated}`;
|
|
617
|
+
}
|
|
618
|
+
return param.optional ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
619
|
+
}
|
|
620
|
+
function parseRouteParamSegment(segment) {
|
|
621
|
+
const match = /^:([^()+*?]+)(?:\((\.\*)\))?([?+*])?$/.exec(segment);
|
|
622
|
+
if (!match)
|
|
623
|
+
return undefined;
|
|
624
|
+
const modifier = match[3] ?? "";
|
|
625
|
+
return {
|
|
626
|
+
name: match[1],
|
|
627
|
+
pattern: match[2],
|
|
628
|
+
optional: modifier === "?" || modifier === "*",
|
|
629
|
+
repeat: modifier === "+" || modifier === "*" || match[2] === ".*"
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function stringifyRoutePath(path, params) {
|
|
633
|
+
const normalized = normalizePath(path);
|
|
634
|
+
if (normalized === "/")
|
|
635
|
+
return "/";
|
|
636
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
637
|
+
const resolved = [];
|
|
638
|
+
for (const segment of segments) {
|
|
639
|
+
const param = parseRouteParamSegment(segment);
|
|
640
|
+
if (!param) {
|
|
641
|
+
resolved.push(segment);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const value = params[param.name];
|
|
645
|
+
if (value === undefined) {
|
|
646
|
+
if (param.optional)
|
|
647
|
+
continue;
|
|
648
|
+
throw new Error(`Missing route param: ${param.name}`);
|
|
649
|
+
}
|
|
650
|
+
const values = Array.isArray(value) ? value : [value];
|
|
651
|
+
if (values.length === 0) {
|
|
652
|
+
if (param.optional)
|
|
653
|
+
continue;
|
|
654
|
+
throw new Error(`Missing route param: ${param.name}`);
|
|
655
|
+
}
|
|
656
|
+
if (values.length > 1 && !param.repeat) {
|
|
657
|
+
throw new Error(`Route param is not repeatable: ${param.name}`);
|
|
658
|
+
}
|
|
659
|
+
resolved.push(...values.map((item) => encodeURIComponent(String(item))));
|
|
660
|
+
}
|
|
661
|
+
return normalizePath(resolved.join("/"));
|
|
662
|
+
}
|
|
663
|
+
function escapeRegExp(value) {
|
|
664
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
665
|
+
}
|
|
262
666
|
function createMatcher(routes) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return "/(.*)";
|
|
269
|
-
})
|
|
270
|
-
.replace(/:([^/]+)/g, (_match, key) => {
|
|
271
|
-
keys.push(key);
|
|
272
|
-
return "([^/]+)";
|
|
273
|
-
});
|
|
274
|
-
return {
|
|
667
|
+
const compiled = [];
|
|
668
|
+
const byName = new Map();
|
|
669
|
+
function addCompiledRoute(record, path, parents, registerName) {
|
|
670
|
+
const compiledPath = compileRoutePath(path);
|
|
671
|
+
const route = {
|
|
275
672
|
record,
|
|
276
|
-
|
|
277
|
-
|
|
673
|
+
records: [...parents, record],
|
|
674
|
+
path,
|
|
675
|
+
keys: compiledPath.keys,
|
|
676
|
+
pattern: compiledPath.pattern
|
|
278
677
|
};
|
|
678
|
+
compiled.push(route);
|
|
679
|
+
if (registerName && record.name) {
|
|
680
|
+
if (byName.has(record.name)) {
|
|
681
|
+
throw new Error(`Duplicate route name: ${record.name}`);
|
|
682
|
+
}
|
|
683
|
+
byName.set(record.name, route);
|
|
684
|
+
}
|
|
685
|
+
return route;
|
|
686
|
+
}
|
|
687
|
+
function addRoute(record, parentPath, parents, registerNames = true) {
|
|
688
|
+
const path = joinRoutePath(parentPath, record.path);
|
|
689
|
+
const paths = [path, ...readAliases(record).map((alias) => joinRoutePath(parentPath, alias))];
|
|
690
|
+
paths.forEach((routePath, index) => {
|
|
691
|
+
addCompiledRoute(record, routePath, parents, registerNames && index === 0);
|
|
692
|
+
});
|
|
693
|
+
paths.forEach((routePath, index) => {
|
|
694
|
+
for (const child of record.children ?? []) {
|
|
695
|
+
addRoute(child, routePath, [...parents, record], registerNames && index === 0);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
for (const route of routes) {
|
|
700
|
+
addRoute(route, "", []);
|
|
701
|
+
}
|
|
702
|
+
compiled.sort((left, right) => {
|
|
703
|
+
if (left.path !== right.path)
|
|
704
|
+
return 0;
|
|
705
|
+
return right.records.length - left.records.length;
|
|
279
706
|
});
|
|
707
|
+
return { routes: compiled, byName };
|
|
280
708
|
}
|
|
281
|
-
function resolveLocation(raw, matcher) {
|
|
709
|
+
function resolveLocation(raw, matcher, parseQuery = parseRouteQuery) {
|
|
282
710
|
const fullPath = normalizePath(raw);
|
|
283
|
-
const parsed = parsePath(fullPath);
|
|
284
|
-
const matched = matcher.find((route) => route.pattern.test(parsed.path));
|
|
711
|
+
const parsed = parsePath(fullPath, parseQuery);
|
|
712
|
+
const matched = matcher.routes.find((route) => route.pattern.test(parsed.path));
|
|
285
713
|
const params = {};
|
|
286
714
|
if (matched) {
|
|
287
715
|
const match = matched.pattern.exec(parsed.path);
|
|
288
716
|
matched.keys.forEach((key, index) => {
|
|
289
|
-
|
|
717
|
+
const value = match?.[index + 1];
|
|
718
|
+
if (value === undefined || value === "")
|
|
719
|
+
return;
|
|
720
|
+
params[key.name] = key.repeat ? value.split("/").map((part) => decodeURIComponent(part)) : decodeURIComponent(value);
|
|
290
721
|
});
|
|
291
722
|
}
|
|
292
723
|
return {
|
|
@@ -296,11 +727,12 @@ function resolveLocation(raw, matcher) {
|
|
|
296
727
|
hash: parsed.hash,
|
|
297
728
|
params,
|
|
298
729
|
matched: matched?.record,
|
|
730
|
+
matchedRecords: matched?.records ?? [],
|
|
299
731
|
name: matched?.record.name,
|
|
300
|
-
meta: matched?.
|
|
732
|
+
meta: mergeMeta(matched?.records ?? [])
|
|
301
733
|
};
|
|
302
734
|
}
|
|
303
|
-
function parsePath(fullPath) {
|
|
735
|
+
function parsePath(fullPath, parseQuery = parseRouteQuery) {
|
|
304
736
|
const [pathAndQuery, rawHash = ""] = fullPath.split("#", 2);
|
|
305
737
|
const [path = "/", rawQuery = ""] = pathAndQuery.split("?", 2);
|
|
306
738
|
return {
|
|
@@ -309,15 +741,29 @@ function parsePath(fullPath) {
|
|
|
309
741
|
hash: rawHash ? `#${rawHash}` : ""
|
|
310
742
|
};
|
|
311
743
|
}
|
|
312
|
-
function stringifyLocation(to) {
|
|
744
|
+
function stringifyLocation(to, matcher, basePath, stringifyQuery = stringifyRouteQuery) {
|
|
313
745
|
if (typeof to === "string")
|
|
314
|
-
return
|
|
315
|
-
const path =
|
|
746
|
+
return stringifyPathLocation(to, basePath);
|
|
747
|
+
const path = "name" in to ? stringifyNamedLocation(to, matcher) : normalizeRelativePath(to.path, basePath);
|
|
316
748
|
const query = stringifyQuery(to.query ?? {});
|
|
317
749
|
const hash = to.hash ? (to.hash.startsWith("#") ? to.hash : `#${to.hash}`) : "";
|
|
318
750
|
return `${path}${query}${hash}`;
|
|
319
751
|
}
|
|
320
|
-
function
|
|
752
|
+
function stringifyPathLocation(raw, basePath) {
|
|
753
|
+
const [pathAndQuery, rawHash = ""] = raw.split("#", 2);
|
|
754
|
+
const [path = "/", rawQuery = ""] = pathAndQuery.split("?", 2);
|
|
755
|
+
const query = rawQuery ? `?${rawQuery}` : "";
|
|
756
|
+
const hash = rawHash ? `#${rawHash}` : "";
|
|
757
|
+
return `${normalizeRelativePath(path, basePath)}${query}${hash}`;
|
|
758
|
+
}
|
|
759
|
+
function stringifyNamedLocation(to, matcher) {
|
|
760
|
+
const route = matcher?.byName.get(to.name);
|
|
761
|
+
if (!route) {
|
|
762
|
+
throw new Error(`Unknown route name: ${to.name}`);
|
|
763
|
+
}
|
|
764
|
+
return stringifyRoutePath(route.path, to.params ?? {});
|
|
765
|
+
}
|
|
766
|
+
export function parseRouteQuery(raw) {
|
|
321
767
|
const query = {};
|
|
322
768
|
const search = new URLSearchParams(raw);
|
|
323
769
|
for (const [key, value] of search) {
|
|
@@ -331,7 +777,7 @@ function parseQuery(raw) {
|
|
|
331
777
|
}
|
|
332
778
|
return query;
|
|
333
779
|
}
|
|
334
|
-
function
|
|
780
|
+
export function stringifyRouteQuery(query) {
|
|
335
781
|
const search = new URLSearchParams();
|
|
336
782
|
for (const [key, value] of Object.entries(query)) {
|
|
337
783
|
if (Array.isArray(value)) {
|
|
@@ -349,11 +795,109 @@ function normalizePath(path) {
|
|
|
349
795
|
const normalized = path.trim() || "/";
|
|
350
796
|
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
351
797
|
}
|
|
798
|
+
function normalizeRelativePath(path, basePath) {
|
|
799
|
+
const normalized = path.trim() || "/";
|
|
800
|
+
if (!isRelativePath(normalized))
|
|
801
|
+
return normalizePath(normalized);
|
|
802
|
+
const segments = normalizePath(basePath ?? "/")
|
|
803
|
+
.split("/")
|
|
804
|
+
.filter(Boolean);
|
|
805
|
+
for (const segment of normalized.split("/")) {
|
|
806
|
+
if (!segment || segment === ".")
|
|
807
|
+
continue;
|
|
808
|
+
if (segment === "..")
|
|
809
|
+
segments.pop();
|
|
810
|
+
else
|
|
811
|
+
segments.push(segment);
|
|
812
|
+
}
|
|
813
|
+
return normalizePath(segments.map((segment) => encodeURIComponent(decodeURIComponent(segment))).join("/"));
|
|
814
|
+
}
|
|
815
|
+
function isRelativePath(path) {
|
|
816
|
+
return path === "." || path === ".." || path.startsWith("./") || path.startsWith("../");
|
|
817
|
+
}
|
|
352
818
|
function normalizeBase(base) {
|
|
353
819
|
if (!base)
|
|
354
820
|
return "/";
|
|
355
821
|
return base.endsWith("/") ? base : `${base}/`;
|
|
356
822
|
}
|
|
823
|
+
function joinRoutePath(parentPath, path) {
|
|
824
|
+
if (path.startsWith("/"))
|
|
825
|
+
return normalizePath(path);
|
|
826
|
+
if (!parentPath)
|
|
827
|
+
return normalizePath(path);
|
|
828
|
+
if (!path)
|
|
829
|
+
return parentPath;
|
|
830
|
+
return normalizePath(`${parentPath.replace(/\/$/, "")}/${path}`);
|
|
831
|
+
}
|
|
832
|
+
function readToProp(props) {
|
|
833
|
+
return unwrap(props.to) ?? "/";
|
|
834
|
+
}
|
|
835
|
+
function readAliases(record) {
|
|
836
|
+
if (!record.alias)
|
|
837
|
+
return [];
|
|
838
|
+
return Array.isArray(record.alias) ? record.alias : [record.alias];
|
|
839
|
+
}
|
|
840
|
+
function mergeMeta(records) {
|
|
841
|
+
return records.reduce((meta, record) => ({ ...meta, ...(record.meta ?? {}) }), {});
|
|
842
|
+
}
|
|
843
|
+
function readBeforeEnterGuards(records) {
|
|
844
|
+
return records.flatMap((record) => {
|
|
845
|
+
if (!record.beforeEnter)
|
|
846
|
+
return [];
|
|
847
|
+
return Array.isArray(record.beforeEnter) ? record.beforeEnter : [record.beforeEnter];
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
function removeRouteByName(routes, name) {
|
|
851
|
+
for (let index = 0; index < routes.length; index += 1) {
|
|
852
|
+
const route = routes[index];
|
|
853
|
+
if (!route)
|
|
854
|
+
continue;
|
|
855
|
+
if (route.name === name) {
|
|
856
|
+
routes.splice(index, 1);
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
if (route.children && removeRouteByName(route.children, name)) {
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
function removeRouteRecord(routes, record) {
|
|
866
|
+
const index = routes.indexOf(record);
|
|
867
|
+
if (index >= 0) {
|
|
868
|
+
routes.splice(index, 1);
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
for (const route of routes) {
|
|
872
|
+
if (route.children && removeRouteRecord(route.children, record)) {
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
function createNavigationFailure(type, to, from) {
|
|
879
|
+
return {
|
|
880
|
+
type,
|
|
881
|
+
to,
|
|
882
|
+
from,
|
|
883
|
+
message: `Navigation ${type} from ${from.fullPath} to ${to.fullPath}.`
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function scrollToDefaultPosition(to) {
|
|
887
|
+
if (to.hash) {
|
|
888
|
+
const id = decodeURIComponent(to.hash.slice(1));
|
|
889
|
+
const element = globalThis.document?.getElementById?.(id);
|
|
890
|
+
if (element) {
|
|
891
|
+
element.scrollIntoView();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
scrollToPosition({ left: 0, top: 0 });
|
|
896
|
+
}
|
|
897
|
+
function scrollToPosition(position) {
|
|
898
|
+
const scrollTo = globalThis.scrollTo ?? globalThis.window?.scrollTo;
|
|
899
|
+
scrollTo?.call(globalThis.window ?? globalThis, position);
|
|
900
|
+
}
|
|
357
901
|
function stripBase(path, base) {
|
|
358
902
|
if (base === "/")
|
|
359
903
|
return path;
|