space-router 1.0.0 → 1.2.0

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  <h4 align="center">Framework agnostic router for single page apps</h4>
7
7
  <br />
8
8
 
9
- Space Router packs all the features you need to keep your app in sync with the url. It's distinct from many other routers in that there is only **a single callback**. This callback can be used to re-render your applocation, update a store and perform other actions on each url change. Space Router is **stateless**, it doesn't store the current route leaving state completely up to you to handle.
9
+ Space Router packs all the features you need to keep your app in sync with the url. It's distinct from many other routers in that there is only **a single callback**. This callback can be used to re-render your application, update a store and perform other actions on each url change. Space Router is **stateless**, it doesn't store the current route leaving state completely up to you to handle.
10
10
 
11
11
  In summary, Space Router:
12
12
 
package/dist/history.d.ts CHANGED
@@ -5,8 +5,13 @@ export interface History {
5
5
  push(url: string): void;
6
6
  replace(url: string): void;
7
7
  }
8
+ export interface ScheduleInfo {
9
+ traversal: boolean;
10
+ }
11
+ export type Schedule = (fire: () => void, info: ScheduleInfo) => void;
8
12
  export interface CreateHistoryOptions {
9
13
  mode?: Mode;
10
14
  sync?: boolean;
15
+ schedule?: Schedule;
11
16
  }
12
17
  export declare function createHistory(options?: CreateHistoryOptions): History;
package/dist/history.js CHANGED
@@ -1,8 +1,9 @@
1
1
  export function createHistory(options = {}) {
2
- const sync = !!options.sync;
2
+ const schedule = options.schedule || (options.sync ? (fire) => fire() : (fire) => queueMicrotask(fire));
3
3
  let mode = options.mode || 'history';
4
4
  let listener = null;
5
- let pending = false;
5
+ let seq = 0;
6
+ let selfNavs = 0;
6
7
  const memory = [];
7
8
  if (typeof window === 'undefined') {
8
9
  mode = 'memory';
@@ -11,16 +12,24 @@ export function createHistory(options = {}) {
11
12
  if (listener)
12
13
  listener(getUrl());
13
14
  }
14
- function schedule() {
15
- if (sync)
16
- return emit();
17
- if (pending)
18
- return;
19
- pending = true;
20
- queueMicrotask(() => {
21
- pending = false;
22
- emit();
23
- });
15
+ // each scheduled fire captures its seq: superseded fires no-op, and the
16
+ // surviving one reads the url fresh in emit() — so racing schedules of any
17
+ // mix (e.g. a deferred traversal emit overtaken by a push's microtask emit)
18
+ // coalesce into a single emit of the final url
19
+ function scheduleEmit(traversal) {
20
+ const s = ++seq;
21
+ schedule(() => {
22
+ if (s === seq)
23
+ emit();
24
+ }, { traversal });
25
+ }
26
+ function onTraversal() {
27
+ // a hashchange caused by our own push/replace is a navigation,
28
+ // not a back/forward traversal
29
+ const traversal = selfNavs === 0;
30
+ if (selfNavs > 0)
31
+ selfNavs--;
32
+ scheduleEmit(traversal);
24
33
  }
25
34
  function listen(onChange) {
26
35
  if (listener)
@@ -28,8 +37,8 @@ export function createHistory(options = {}) {
28
37
  listener = onChange;
29
38
  let off;
30
39
  if (mode !== 'memory') {
31
- off = on(window, mode === 'history' ? 'popstate' : 'hashchange', schedule);
32
- schedule();
40
+ off = on(window, mode === 'history' ? 'popstate' : 'hashchange', onTraversal);
41
+ scheduleEmit(false);
33
42
  }
34
43
  return () => {
35
44
  listener = null;
@@ -41,30 +50,32 @@ export function createHistory(options = {}) {
41
50
  url = url.replace(/^\/?#?\/?/, '/').replace(/\/$/, '') || '/';
42
51
  if (mode === 'history') {
43
52
  history[replace ? 'replaceState' : 'pushState']({}, '', url);
44
- schedule();
53
+ scheduleEmit(false);
45
54
  }
46
55
  else if (mode === 'hash') {
47
56
  // hashchange only fires when the URL actually changes; if it doesn't,
48
57
  // we schedule the emit manually so navigation stays consistent with
49
58
  // history mode (where pushState is silent and we always schedule).
50
59
  const same = url === getUrl();
60
+ if (!same)
61
+ selfNavs++;
51
62
  location[replace ? 'replace' : 'assign']('#' + url);
52
63
  if (same)
53
- schedule();
64
+ scheduleEmit(false);
54
65
  }
55
66
  else if (mode === 'memory') {
56
- if (replace) {
67
+ if (replace && memory.length) {
57
68
  memory[memory.length - 1] = url;
58
69
  }
59
70
  else {
60
71
  memory.push(url);
61
72
  }
62
- schedule();
73
+ scheduleEmit(false);
63
74
  }
64
75
  }
65
76
  function getUrl() {
66
77
  if (mode === 'memory') {
67
- return memory[memory.length - 1];
78
+ return memory[memory.length - 1] ?? '';
68
79
  }
69
80
  const hash = getHash();
70
81
  if (mode === 'hash') {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { qs } from './qs.ts';
2
2
  export { createHistory } from './history.ts';
3
- export { createRouter, merge } from './router.ts';
3
+ export { createMatcher, createRouter, merge } from './router.ts';
4
4
  export type { Qs } from './qs.ts';
5
- export type { Mode, History, CreateHistoryOptions } from './history.ts';
6
- export type { RouterOptions, Route, NavigateTarget, To, Redirect, RouteDefinition, Router } from './router.ts';
5
+ export type { Mode, History, CreateHistoryOptions, Schedule, ScheduleInfo } from './history.ts';
6
+ export type { MatcherOptions, RouteData, RouterOptions, Route, NavigateTarget, To, Redirect, RouteDefinition, Matcher, Router, } from './router.ts';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { qs } from "./qs.js";
2
2
  export { createHistory } from "./history.js";
3
- export { createRouter, merge } from "./router.js";
3
+ export { createMatcher, createRouter, merge } from "./router.js";
package/dist/match.d.ts CHANGED
@@ -8,9 +8,4 @@ export interface MatchedRoute {
8
8
  search: string;
9
9
  hash: string;
10
10
  }
11
- interface RouteEntry {
12
- pattern: string;
13
- }
14
- export declare function match(routes: RouteEntry[], url: string | undefined, qs: Qs): MatchedRoute | undefined;
15
11
  export declare function matchOne(pattern: string, url: string, qs?: Qs): MatchedRoute | undefined;
16
- export {};
package/dist/match.js CHANGED
@@ -1,12 +1,3 @@
1
- export function match(routes, url, qs) {
2
- if (!url)
3
- return;
4
- for (let i = 0; i < routes.length; i++) {
5
- const m = matchOne(routes[i].pattern, url, qs);
6
- if (m)
7
- return m;
8
- }
9
- }
10
1
  export function matchOne(pattern, url, qs) {
11
2
  if (!pattern)
12
3
  return;
package/dist/router.d.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import { type MatchedRoute } from './match.ts';
2
- import { type Mode } from './history.ts';
2
+ import { type Mode, type Schedule } from './history.ts';
3
3
  import { type Qs } from './qs.ts';
4
4
  export interface RouterOptions {
5
5
  mode?: Mode;
6
6
  qs?: Qs;
7
7
  sync?: boolean;
8
+ schedule?: Schedule;
9
+ }
10
+ export interface MatcherOptions {
11
+ qs?: Qs;
8
12
  }
9
13
  export interface Route<Data = Record<string, unknown>> extends MatchedRoute {
10
- data: Data[];
14
+ data: RouteData<Data>[];
11
15
  }
12
16
  export interface NavigateTarget {
13
17
  url?: string;
@@ -20,6 +24,10 @@ export interface NavigateTarget {
20
24
  }
21
25
  export type To = string | NavigateTarget;
22
26
  export type Redirect<Data = Record<string, unknown>> = To | ((route: Route<Data>) => To);
27
+ export type RouteData<Data = Record<string, unknown>> = Data & {
28
+ path: string;
29
+ redirect?: Redirect<Data>;
30
+ };
23
31
  export type RouteDefinition<Data = Record<string, unknown>> = Data & {
24
32
  path?: string;
25
33
  redirect?: Redirect<Data>;
@@ -32,11 +40,15 @@ export interface Router<Data = Record<string, unknown>> {
32
40
  match(url: string): Route<Data> | undefined;
33
41
  getUrl(): string;
34
42
  }
35
- interface FlatRoute {
43
+ export interface Matcher<Data = Record<string, unknown>> {
44
+ match(url: string): Route<Data> | undefined;
45
+ }
46
+ interface FlatRoute<Data = Record<string, unknown>> {
36
47
  pattern: string;
37
- data: Array<Record<string, unknown>>;
48
+ data: RouteData<Data>[];
38
49
  }
39
50
  export declare function createRouter<Data = Record<string, unknown>>(options?: RouterOptions): Router<Data>;
40
- export declare function flatten(routeMap: RouteDefinition[]): FlatRoute[];
51
+ export declare function createMatcher<Data = Record<string, unknown>>(routeMap: RouteDefinition<Data>[], options?: MatcherOptions): Matcher<Data>;
52
+ export declare function flatten<Data = Record<string, unknown>>(routeMap: RouteDefinition<Data>[]): FlatRoute<Data>[];
41
53
  export declare function merge(curr: Partial<Route> | NavigateTarget | undefined, to: NavigateTarget): NavigateTarget;
42
54
  export {};
package/dist/router.js CHANGED
@@ -1,4 +1,4 @@
1
- import { match as findMatch } from "./match.js";
1
+ import { matchOne } from "./match.js";
2
2
  import { createHistory } from "./history.js";
3
3
  import { qs as defaultQs } from "./qs.js";
4
4
  const PARAM_RE = /:([A-Za-z0-9_]+)([+*?])?/g;
@@ -6,15 +6,14 @@ const MAX_REDIRECTS = 10;
6
6
  export function createRouter(options = {}) {
7
7
  const mode = options.mode || 'history';
8
8
  const qs = options.qs || defaultQs;
9
- const sync = options.sync || false;
10
- const history = createHistory({ mode, sync });
11
- let routes = [];
9
+ const history = createHistory({ mode, sync: options.sync, schedule: options.schedule });
10
+ let matcher = createMatcher([], { qs });
12
11
  const router = {
13
12
  listen(routeMap, cb) {
14
- routes = flatten(routeMap);
13
+ matcher = createMatcher(routeMap, { qs });
15
14
  let redirects = 0;
16
15
  return history.listen((url) => {
17
- const route = router.match(url);
16
+ const route = matcher.match(url);
18
17
  if (!route) {
19
18
  redirects = 0;
20
19
  return;
@@ -84,11 +83,7 @@ export function createRouter(options = {}) {
84
83
  return url;
85
84
  },
86
85
  match(url) {
87
- const route = findMatch(routes, url, qs);
88
- if (route) {
89
- return { ...route, data: data(routes, route) };
90
- }
91
- return undefined;
86
+ return matcher.match(url);
92
87
  },
93
88
  getUrl() {
94
89
  return history.getUrl();
@@ -96,15 +91,37 @@ export function createRouter(options = {}) {
96
91
  };
97
92
  return router;
98
93
  }
94
+ export function createMatcher(routeMap, options = {}) {
95
+ const routes = flatten(routeMap);
96
+ const qs = options.qs || defaultQs;
97
+ return {
98
+ match(url) {
99
+ if (!url)
100
+ return undefined;
101
+ for (const route of routes) {
102
+ const m = matchOne(route.pattern, url, qs);
103
+ if (m) {
104
+ return { ...m, data: route.data };
105
+ }
106
+ }
107
+ return undefined;
108
+ },
109
+ };
110
+ }
99
111
  export function flatten(routeMap) {
100
112
  const routes = [];
101
113
  const parentData = [];
102
114
  function addLevel(level) {
103
115
  level.forEach((route) => {
104
116
  const { path = '', routes: children, ...routeData } = route;
105
- routes.push({ pattern: path, data: parentData.concat([routeData]) });
117
+ const segment = { path, ...routeData };
118
+ // pathless routes only contribute data to their children — they have
119
+ // no pattern of their own to match
120
+ if (path) {
121
+ routes.push({ pattern: path, data: parentData.concat([segment]) });
122
+ }
106
123
  if (children) {
107
- parentData.push(routeData);
124
+ parentData.push(segment);
108
125
  addLevel(children);
109
126
  parentData.pop();
110
127
  }
@@ -113,14 +130,6 @@ export function flatten(routeMap) {
113
130
  addLevel(routeMap);
114
131
  return routes;
115
132
  }
116
- function data(routes, matchingRoute) {
117
- for (let i = 0; i < routes.length; i++) {
118
- if (routes[i].pattern === matchingRoute.pattern) {
119
- return routes[i].data;
120
- }
121
- }
122
- return [];
123
- }
124
133
  export function merge(curr, to) {
125
134
  const c = (curr || {});
126
135
  const pathname = to.pathname || c.pattern || c.pathname;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "space-router",
3
3
  "description": "All the routing essentials.",
4
- "version": "1.0.0",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -47,16 +47,16 @@
47
47
  "release:docs": "hugo -s docs && gh-pages -d docs/public"
48
48
  },
49
49
  "devDependencies": {
50
- "ava": "^7.0.0",
50
+ "ava": "^8.0.1",
51
51
  "c8": "^11.0.0",
52
52
  "gh-pages": "^6.3.0",
53
- "oxfmt": "^0.46.0",
54
- "oxlint": "^1.61.0",
53
+ "oxfmt": "^0.57.0",
54
+ "oxlint": "^1.72.0",
55
55
  "typescript": "^6.0.3"
56
56
  },
57
57
  "ava": {
58
- "extensions": {
59
- "ts": "module"
60
- }
58
+ "extensions": [
59
+ "ts"
60
+ ]
61
61
  }
62
62
  }
package/src/history.ts CHANGED
@@ -7,16 +7,24 @@ export interface History {
7
7
  replace(url: string): void
8
8
  }
9
9
 
10
+ export interface ScheduleInfo {
11
+ traversal: boolean
12
+ }
13
+
14
+ export type Schedule = (fire: () => void, info: ScheduleInfo) => void
15
+
10
16
  export interface CreateHistoryOptions {
11
17
  mode?: Mode
12
18
  sync?: boolean
19
+ schedule?: Schedule
13
20
  }
14
21
 
15
22
  export function createHistory(options: CreateHistoryOptions = {}): History {
16
- const sync = !!options.sync
23
+ const schedule: Schedule = options.schedule || (options.sync ? (fire) => fire() : (fire) => queueMicrotask(fire))
17
24
  let mode: Mode = options.mode || 'history'
18
25
  let listener: ((url: string) => void) | null = null
19
- let pending = false
26
+ let seq = 0
27
+ let selfNavs = 0
20
28
 
21
29
  const memory: string[] = []
22
30
 
@@ -28,14 +36,26 @@ export function createHistory(options: CreateHistoryOptions = {}): History {
28
36
  if (listener) listener(getUrl())
29
37
  }
30
38
 
31
- function schedule() {
32
- if (sync) return emit()
33
- if (pending) return
34
- pending = true
35
- queueMicrotask(() => {
36
- pending = false
37
- emit()
38
- })
39
+ // each scheduled fire captures its seq: superseded fires no-op, and the
40
+ // surviving one reads the url fresh in emit() — so racing schedules of any
41
+ // mix (e.g. a deferred traversal emit overtaken by a push's microtask emit)
42
+ // coalesce into a single emit of the final url
43
+ function scheduleEmit(traversal: boolean) {
44
+ const s = ++seq
45
+ schedule(
46
+ () => {
47
+ if (s === seq) emit()
48
+ },
49
+ { traversal },
50
+ )
51
+ }
52
+
53
+ function onTraversal() {
54
+ // a hashchange caused by our own push/replace is a navigation,
55
+ // not a back/forward traversal
56
+ const traversal = selfNavs === 0
57
+ if (selfNavs > 0) selfNavs--
58
+ scheduleEmit(traversal)
39
59
  }
40
60
 
41
61
  function listen(onChange: (url: string) => void) {
@@ -43,8 +63,8 @@ export function createHistory(options: CreateHistoryOptions = {}): History {
43
63
  listener = onChange
44
64
  let off: (() => void) | undefined
45
65
  if (mode !== 'memory') {
46
- off = on(window, mode === 'history' ? 'popstate' : 'hashchange', schedule)
47
- schedule()
66
+ off = on(window, mode === 'history' ? 'popstate' : 'hashchange', onTraversal)
67
+ scheduleEmit(false)
48
68
  }
49
69
  return () => {
50
70
  listener = null
@@ -56,27 +76,28 @@ export function createHistory(options: CreateHistoryOptions = {}): History {
56
76
  url = url.replace(/^\/?#?\/?/, '/').replace(/\/$/, '') || '/'
57
77
  if (mode === 'history') {
58
78
  history[replace ? 'replaceState' : 'pushState']({}, '', url)
59
- schedule()
79
+ scheduleEmit(false)
60
80
  } else if (mode === 'hash') {
61
81
  // hashchange only fires when the URL actually changes; if it doesn't,
62
82
  // we schedule the emit manually so navigation stays consistent with
63
83
  // history mode (where pushState is silent and we always schedule).
64
84
  const same = url === getUrl()
85
+ if (!same) selfNavs++
65
86
  location[replace ? 'replace' : 'assign']('#' + url)
66
- if (same) schedule()
87
+ if (same) scheduleEmit(false)
67
88
  } else if (mode === 'memory') {
68
- if (replace) {
89
+ if (replace && memory.length) {
69
90
  memory[memory.length - 1] = url
70
91
  } else {
71
92
  memory.push(url)
72
93
  }
73
- schedule()
94
+ scheduleEmit(false)
74
95
  }
75
96
  }
76
97
 
77
98
  function getUrl(): string {
78
99
  if (mode === 'memory') {
79
- return memory[memory.length - 1]
100
+ return memory[memory.length - 1] ?? ''
80
101
  }
81
102
 
82
103
  const hash = getHash()
package/src/index.ts CHANGED
@@ -1,7 +1,18 @@
1
1
  export { qs } from './qs.ts'
2
2
  export { createHistory } from './history.ts'
3
- export { createRouter, merge } from './router.ts'
3
+ export { createMatcher, createRouter, merge } from './router.ts'
4
4
 
5
5
  export type { Qs } from './qs.ts'
6
- export type { Mode, History, CreateHistoryOptions } from './history.ts'
7
- export type { RouterOptions, Route, NavigateTarget, To, Redirect, RouteDefinition, Router } from './router.ts'
6
+ export type { Mode, History, CreateHistoryOptions, Schedule, ScheduleInfo } from './history.ts'
7
+ export type {
8
+ MatcherOptions,
9
+ RouteData,
10
+ RouterOptions,
11
+ Route,
12
+ NavigateTarget,
13
+ To,
14
+ Redirect,
15
+ RouteDefinition,
16
+ Matcher,
17
+ Router,
18
+ } from './router.ts'
package/src/match.ts CHANGED
@@ -10,18 +10,6 @@ export interface MatchedRoute {
10
10
  hash: string
11
11
  }
12
12
 
13
- interface RouteEntry {
14
- pattern: string
15
- }
16
-
17
- export function match(routes: RouteEntry[], url: string | undefined, qs: Qs): MatchedRoute | undefined {
18
- if (!url) return
19
- for (let i = 0; i < routes.length; i++) {
20
- const m = matchOne(routes[i].pattern, url, qs)
21
- if (m) return m
22
- }
23
- }
24
-
25
13
  export function matchOne(pattern: string, url: string, qs?: Qs): MatchedRoute | undefined {
26
14
  if (!pattern) return
27
15
 
package/src/router.ts CHANGED
@@ -1,15 +1,20 @@
1
- import { match as findMatch, type MatchedRoute } from './match.ts'
2
- import { createHistory, type Mode } from './history.ts'
1
+ import { matchOne, type MatchedRoute } from './match.ts'
2
+ import { createHistory, type Mode, type Schedule } from './history.ts'
3
3
  import { qs as defaultQs, type Qs } from './qs.ts'
4
4
 
5
5
  export interface RouterOptions {
6
6
  mode?: Mode
7
7
  qs?: Qs
8
8
  sync?: boolean
9
+ schedule?: Schedule
10
+ }
11
+
12
+ export interface MatcherOptions {
13
+ qs?: Qs
9
14
  }
10
15
 
11
16
  export interface Route<Data = Record<string, unknown>> extends MatchedRoute {
12
- data: Data[]
17
+ data: RouteData<Data>[]
13
18
  }
14
19
 
15
20
  export interface NavigateTarget {
@@ -26,6 +31,11 @@ export type To = string | NavigateTarget
26
31
 
27
32
  export type Redirect<Data = Record<string, unknown>> = To | ((route: Route<Data>) => To)
28
33
 
34
+ export type RouteData<Data = Record<string, unknown>> = Data & {
35
+ path: string
36
+ redirect?: Redirect<Data>
37
+ }
38
+
29
39
  export type RouteDefinition<Data = Record<string, unknown>> = Data & {
30
40
  path?: string
31
41
  redirect?: Redirect<Data>
@@ -40,9 +50,13 @@ export interface Router<Data = Record<string, unknown>> {
40
50
  getUrl(): string
41
51
  }
42
52
 
43
- interface FlatRoute {
53
+ export interface Matcher<Data = Record<string, unknown>> {
54
+ match(url: string): Route<Data> | undefined
55
+ }
56
+
57
+ interface FlatRoute<Data = Record<string, unknown>> {
44
58
  pattern: string
45
- data: Array<Record<string, unknown>>
59
+ data: RouteData<Data>[]
46
60
  }
47
61
 
48
62
  const PARAM_RE = /:([A-Za-z0-9_]+)([+*?])?/g
@@ -51,22 +65,21 @@ const MAX_REDIRECTS = 10
51
65
  export function createRouter<Data = Record<string, unknown>>(options: RouterOptions = {}): Router<Data> {
52
66
  const mode = options.mode || 'history'
53
67
  const qs = options.qs || defaultQs
54
- const sync = options.sync || false
55
- const history = createHistory({ mode, sync })
68
+ const history = createHistory({ mode, sync: options.sync, schedule: options.schedule })
56
69
 
57
- let routes: FlatRoute[] = []
70
+ let matcher = createMatcher<Data>([], { qs })
58
71
 
59
72
  const router: Router<Data> = {
60
73
  listen(routeMap, cb) {
61
- routes = flatten(routeMap as RouteDefinition[])
74
+ matcher = createMatcher(routeMap, { qs })
62
75
  let redirects = 0
63
76
  return history.listen((url) => {
64
- const route = router.match(url)
77
+ const route = matcher.match(url)
65
78
  if (!route) {
66
79
  redirects = 0
67
80
  return
68
81
  }
69
- for (const r of route.data as Array<{ redirect?: Redirect<Data> }>) {
82
+ for (const r of route.data) {
70
83
  if (r.redirect) {
71
84
  if (++redirects > MAX_REDIRECTS) {
72
85
  redirects = 0
@@ -138,11 +151,7 @@ export function createRouter<Data = Record<string, unknown>>(options: RouterOpti
138
151
  },
139
152
 
140
153
  match(url) {
141
- const route = findMatch(routes, url, qs)
142
- if (route) {
143
- return { ...route, data: data(routes, route) } as Route<Data>
144
- }
145
- return undefined
154
+ return matcher.match(url)
146
155
  },
147
156
 
148
157
  getUrl() {
@@ -153,22 +162,48 @@ export function createRouter<Data = Record<string, unknown>>(options: RouterOpti
153
162
  return router
154
163
  }
155
164
 
156
- export function flatten(routeMap: RouteDefinition[]): FlatRoute[] {
157
- const routes: FlatRoute[] = []
158
- const parentData: Array<Record<string, unknown>> = []
159
- function addLevel(level: RouteDefinition[]) {
165
+ export function createMatcher<Data = Record<string, unknown>>(
166
+ routeMap: RouteDefinition<Data>[],
167
+ options: MatcherOptions = {},
168
+ ): Matcher<Data> {
169
+ const routes = flatten(routeMap)
170
+ const qs = options.qs || defaultQs
171
+
172
+ return {
173
+ match(url) {
174
+ if (!url) return undefined
175
+ for (const route of routes) {
176
+ const m = matchOne(route.pattern, url, qs)
177
+ if (m) {
178
+ return { ...m, data: route.data } as Route<Data>
179
+ }
180
+ }
181
+ return undefined
182
+ },
183
+ }
184
+ }
185
+
186
+ export function flatten<Data = Record<string, unknown>>(routeMap: RouteDefinition<Data>[]): FlatRoute<Data>[] {
187
+ const routes: FlatRoute<Data>[] = []
188
+ const parentData: RouteData<Data>[] = []
189
+ function addLevel(level: RouteDefinition<Data>[]) {
160
190
  level.forEach((route) => {
161
191
  const {
162
192
  path = '',
163
193
  routes: children,
164
194
  ...routeData
165
- } = route as RouteDefinition & {
195
+ } = route as RouteDefinition<Data> & {
166
196
  path?: string
167
- routes?: RouteDefinition[]
197
+ routes?: RouteDefinition<Data>[]
198
+ }
199
+ const segment = { path, ...routeData } as RouteData<Data>
200
+ // pathless routes only contribute data to their children — they have
201
+ // no pattern of their own to match
202
+ if (path) {
203
+ routes.push({ pattern: path, data: parentData.concat([segment]) })
168
204
  }
169
- routes.push({ pattern: path, data: parentData.concat([routeData]) })
170
205
  if (children) {
171
- parentData.push(routeData)
206
+ parentData.push(segment)
172
207
  addLevel(children)
173
208
  parentData.pop()
174
209
  }
@@ -178,15 +213,6 @@ export function flatten(routeMap: RouteDefinition[]): FlatRoute[] {
178
213
  return routes
179
214
  }
180
215
 
181
- function data(routes: FlatRoute[], matchingRoute: { pattern: string }) {
182
- for (let i = 0; i < routes.length; i++) {
183
- if (routes[i].pattern === matchingRoute.pattern) {
184
- return routes[i].data
185
- }
186
- }
187
- return []
188
- }
189
-
190
216
  export function merge(curr: Partial<Route> | NavigateTarget | undefined, to: NavigateTarget): NavigateTarget {
191
217
  const c = (curr || {}) as Partial<Route> & NavigateTarget
192
218
  const pathname = to.pathname || c.pattern || c.pathname