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.
@@ -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.notFound ? [...options.routes, { ...notFoundRoute, component: options.notFound }] : options.routes.slice();
6
- const matcher = createMatcher(routes);
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 initial = resolveLocation(history.location(), matcher);
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
- const id = ++navigationId;
17
- for (const guard of beforeGuards) {
18
- const result = await guard(target, from);
19
- if (id !== navigationId)
20
- return currentRoute.value;
21
- if (result === false)
22
- return from;
23
- if (typeof result === "string" || (result && typeof result === "object")) {
24
- return navigate(result, true);
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
- if (replace)
28
- history.replace(target.fullPath);
29
- else
30
- history.push(target.fullPath);
31
- currentRoute.value = target;
32
- for (const hook of afterHooks) {
33
- hook(target, from);
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 = resolveLocation(history.location(), matcher);
128
+ currentRoute.value = resolveRouteTarget(history.location());
39
129
  }
40
- return {
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 resolveLocation(stringifyLocation(to), matcher);
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 component = route.matched?.component;
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
- const fragment = document.createDocumentFragment();
199
- child = component.mount(fragment, { route, router, __mikuru_context: props.__mikuru_context });
200
- anchor.parentNode?.insertBefore(fragment, anchor);
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 text = () => String(unwrap(props.label) ?? unwrap(props.childrenText) ?? unwrap(props.to) ?? "");
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(to) : router.push(to));
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 = stringifyLocation(String(unwrap(props.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
- anchor.textContent = text();
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
- throw new Error("RouterView and RouterLink require a router prop.");
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
- return routes.map((record) => {
264
- const keys = [];
265
- const source = record.path
266
- .replace(/\/:([^/(]+)\(\.\*\)\*/g, (_match, key) => {
267
- keys.push(key);
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
- keys,
277
- pattern: new RegExp(`^${source.replace(/\//g, "\\/")}$`)
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
- params[key] = decodeURIComponent(match?.[index + 1] ?? "");
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?.record.meta ?? {}
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 normalizePath(to);
315
- const path = normalizePath(to.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 parseQuery(raw) {
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 stringifyQuery(query) {
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;