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 +1 -1
- package/dist/history.d.ts +5 -0
- package/dist/history.js +30 -19
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/match.d.ts +0 -5
- package/dist/match.js +0 -9
- package/dist/router.d.ts +17 -5
- package/dist/router.js +30 -21
- package/package.json +7 -7
- package/src/history.ts +38 -17
- package/src/index.ts +14 -3
- package/src/match.ts +0 -12
- package/src/router.ts +59 -33
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
|
|
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
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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',
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
|
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:
|
|
48
|
+
data: RouteData<Data>[];
|
|
38
49
|
}
|
|
39
50
|
export declare function createRouter<Data = Record<string, unknown>>(options?: RouterOptions): Router<Data>;
|
|
40
|
-
export declare function
|
|
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 {
|
|
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
|
|
10
|
-
|
|
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
|
-
|
|
13
|
+
matcher = createMatcher(routeMap, { qs });
|
|
15
14
|
let redirects = 0;
|
|
16
15
|
return history.listen((url) => {
|
|
17
|
-
const route =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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": "^
|
|
50
|
+
"ava": "^8.0.1",
|
|
51
51
|
"c8": "^11.0.0",
|
|
52
52
|
"gh-pages": "^6.3.0",
|
|
53
|
-
"oxfmt": "^0.
|
|
54
|
-
"oxlint": "^1.
|
|
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"
|
|
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
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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',
|
|
47
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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:
|
|
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
|
|
55
|
-
const history = createHistory({ mode, sync })
|
|
68
|
+
const history = createHistory({ mode, sync: options.sync, schedule: options.schedule })
|
|
56
69
|
|
|
57
|
-
let
|
|
70
|
+
let matcher = createMatcher<Data>([], { qs })
|
|
58
71
|
|
|
59
72
|
const router: Router<Data> = {
|
|
60
73
|
listen(routeMap, cb) {
|
|
61
|
-
|
|
74
|
+
matcher = createMatcher(routeMap, { qs })
|
|
62
75
|
let redirects = 0
|
|
63
76
|
return history.listen((url) => {
|
|
64
|
-
const route =
|
|
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
|
|
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
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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(
|
|
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
|