space-router 0.9.5 → 1.1.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 +2 -1
- package/dist/history.d.ts +12 -0
- package/dist/history.js +98 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/match.d.ts +11 -0
- package/dist/match.js +59 -0
- package/dist/qs.d.ts +5 -0
- package/dist/qs.js +27 -0
- package/dist/router.d.ts +53 -0
- package/dist/router.js +141 -0
- package/package.json +28 -21
- package/src/history.ts +116 -0
- package/src/index.ts +18 -0
- package/src/match.ts +76 -0
- package/src/qs.ts +33 -0
- package/src/router.ts +223 -0
- package/.prettierignore +0 -5
- package/.prettierrc +0 -6
- package/.swc-cjs +0 -11
- package/.swc-esm +0 -1
- package/CHANGELOG.md +0 -68
- package/dist/cjs/history.js +0 -100
- package/dist/cjs/index.js +0 -27
- package/dist/cjs/match.js +0 -94
- package/dist/cjs/qs.js +0 -27
- package/dist/cjs/router.js +0 -279
- package/dist/esm/history.js +0 -90
- package/dist/esm/index.js +0 -3
- package/dist/esm/match.js +0 -76
- package/dist/esm/qs.js +0 -17
- package/dist/esm/router.js +0 -258
- package/docs/archetypes/default.md +0 -7
- package/docs/assets/js/bg.js +0 -742
- package/docs/assets/styles/base.scss +0 -2816
- package/docs/assets/styles/main.scss +0 -1368
- package/docs/assets/styles/syntax-m.scss +0 -264
- package/docs/assets/styles/syntax.scss +0 -240
- package/docs/config.toml +0 -9
- package/docs/content/_index.md +0 -189
- package/docs/layouts/404.html +0 -0
- package/docs/layouts/_default/baseof.html +0 -68
- package/docs/layouts/index.html +0 -31
- package/docs/layouts/partials/favicon.html +0 -5
- package/docs/layouts/partials/header.html +0 -0
- package/docs/layouts/partials/meta/name-author.html +0 -6
- package/docs/layouts/partials/meta/ogimage.html +0 -8
- package/docs/layouts/partials/site-verification.html +0 -12
- package/docs/layouts/shortcodes/callout.html +0 -1
- package/docs/static/js/highlightjs-9.15.10.min.js +0 -2
- package/docs/static/js/master.js +0 -0
- package/docs/static/js/tocbot.min.js +0 -18
- package/docs/static/space.png +0 -0
- package/oxlintrc.json +0 -6
- package/src/history.js +0 -97
- package/src/index.js +0 -3
- package/src/match.js +0 -87
- package/src/qs.js +0 -20
- package/src/router.js +0 -147
- package/tasks/build.js +0 -16
- package/test/flatten.test.js +0 -58
- package/test/match.test.js +0 -68
- package/test/router.test.js +0 -157
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
|
|
|
@@ -14,6 +14,7 @@ In summary, Space Router:
|
|
|
14
14
|
- extracts url parameters and parses query strings
|
|
15
15
|
- supports nested routes and arbitrary route metadata
|
|
16
16
|
- fits into a wide range of application architectures and frameworks
|
|
17
|
+
- ships TypeScript types
|
|
17
18
|
- has no dependencies and weighs less than 2kb
|
|
18
19
|
|
|
19
20
|
## Install
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type Mode = 'history' | 'hash' | 'memory';
|
|
2
|
+
export interface History {
|
|
3
|
+
listen(onChange: (url: string) => void): () => void;
|
|
4
|
+
getUrl(): string;
|
|
5
|
+
push(url: string): void;
|
|
6
|
+
replace(url: string): void;
|
|
7
|
+
}
|
|
8
|
+
export interface CreateHistoryOptions {
|
|
9
|
+
mode?: Mode;
|
|
10
|
+
sync?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function createHistory(options?: CreateHistoryOptions): History;
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export function createHistory(options = {}) {
|
|
2
|
+
const sync = !!options.sync;
|
|
3
|
+
let mode = options.mode || 'history';
|
|
4
|
+
let listener = null;
|
|
5
|
+
let pending = false;
|
|
6
|
+
const memory = [];
|
|
7
|
+
if (typeof window === 'undefined') {
|
|
8
|
+
mode = 'memory';
|
|
9
|
+
}
|
|
10
|
+
function emit() {
|
|
11
|
+
if (listener)
|
|
12
|
+
listener(getUrl());
|
|
13
|
+
}
|
|
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
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function listen(onChange) {
|
|
26
|
+
if (listener)
|
|
27
|
+
throw new Error('Already listening');
|
|
28
|
+
listener = onChange;
|
|
29
|
+
let off;
|
|
30
|
+
if (mode !== 'memory') {
|
|
31
|
+
off = on(window, mode === 'history' ? 'popstate' : 'hashchange', schedule);
|
|
32
|
+
schedule();
|
|
33
|
+
}
|
|
34
|
+
return () => {
|
|
35
|
+
listener = null;
|
|
36
|
+
if (off)
|
|
37
|
+
off();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function go(url, replace) {
|
|
41
|
+
url = url.replace(/^\/?#?\/?/, '/').replace(/\/$/, '') || '/';
|
|
42
|
+
if (mode === 'history') {
|
|
43
|
+
history[replace ? 'replaceState' : 'pushState']({}, '', url);
|
|
44
|
+
schedule();
|
|
45
|
+
}
|
|
46
|
+
else if (mode === 'hash') {
|
|
47
|
+
// hashchange only fires when the URL actually changes; if it doesn't,
|
|
48
|
+
// we schedule the emit manually so navigation stays consistent with
|
|
49
|
+
// history mode (where pushState is silent and we always schedule).
|
|
50
|
+
const same = url === getUrl();
|
|
51
|
+
location[replace ? 'replace' : 'assign']('#' + url);
|
|
52
|
+
if (same)
|
|
53
|
+
schedule();
|
|
54
|
+
}
|
|
55
|
+
else if (mode === 'memory') {
|
|
56
|
+
if (replace && memory.length) {
|
|
57
|
+
memory[memory.length - 1] = url;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
memory.push(url);
|
|
61
|
+
}
|
|
62
|
+
schedule();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function getUrl() {
|
|
66
|
+
if (mode === 'memory') {
|
|
67
|
+
return memory[memory.length - 1] ?? '';
|
|
68
|
+
}
|
|
69
|
+
const hash = getHash();
|
|
70
|
+
if (mode === 'hash') {
|
|
71
|
+
return hash === '' ? '/' : hash;
|
|
72
|
+
}
|
|
73
|
+
let url = location.pathname + location.search;
|
|
74
|
+
if (hash !== '') {
|
|
75
|
+
url += '#' + hash;
|
|
76
|
+
}
|
|
77
|
+
return url;
|
|
78
|
+
}
|
|
79
|
+
function getHash() {
|
|
80
|
+
return location.hash.slice(1);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
listen,
|
|
84
|
+
getUrl,
|
|
85
|
+
push(url) {
|
|
86
|
+
go(url);
|
|
87
|
+
},
|
|
88
|
+
replace(url) {
|
|
89
|
+
go(url, true);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function on(el, type, fn) {
|
|
94
|
+
el.addEventListener(type, fn, false);
|
|
95
|
+
return function off() {
|
|
96
|
+
el.removeEventListener(type, fn, false);
|
|
97
|
+
};
|
|
98
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { qs } from './qs.ts';
|
|
2
|
+
export { createHistory } from './history.ts';
|
|
3
|
+
export { createMatcher, createRouter, merge } from './router.ts';
|
|
4
|
+
export type { Qs } from './qs.ts';
|
|
5
|
+
export type { Mode, History, CreateHistoryOptions } from './history.ts';
|
|
6
|
+
export type { MatcherOptions, RouteData, RouterOptions, Route, NavigateTarget, To, Redirect, RouteDefinition, Matcher, Router, } from './router.ts';
|
package/dist/index.js
ADDED
package/dist/match.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Qs } from './qs.ts';
|
|
2
|
+
export interface MatchedRoute {
|
|
3
|
+
pattern: string;
|
|
4
|
+
url: string;
|
|
5
|
+
pathname: string;
|
|
6
|
+
params: Record<string, string>;
|
|
7
|
+
query: Record<string, string>;
|
|
8
|
+
search: string;
|
|
9
|
+
hash: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function matchOne(pattern: string, url: string, qs?: Qs): MatchedRoute | undefined;
|
package/dist/match.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function matchOne(pattern, url, qs) {
|
|
2
|
+
if (!pattern)
|
|
3
|
+
return;
|
|
4
|
+
const re = /(?:\?([^#]*))?(#.*)?$/;
|
|
5
|
+
const originalUrl = url;
|
|
6
|
+
const originalPattern = pattern;
|
|
7
|
+
const c = url.match(re);
|
|
8
|
+
const params = {};
|
|
9
|
+
let query = {};
|
|
10
|
+
let search = '';
|
|
11
|
+
let hash = '';
|
|
12
|
+
if (c && c[1]) {
|
|
13
|
+
search = '?' + c[1];
|
|
14
|
+
query = qs ? qs.parse(c[1]) : {};
|
|
15
|
+
}
|
|
16
|
+
if (c && c[2]) {
|
|
17
|
+
hash = c[2];
|
|
18
|
+
}
|
|
19
|
+
if (pattern !== '*') {
|
|
20
|
+
const urlSegs = segmentize(url.replace(re, ''));
|
|
21
|
+
const patSegs = segmentize(pattern);
|
|
22
|
+
const max = Math.max(urlSegs.length, patSegs.length);
|
|
23
|
+
for (let i = 0; i < max; i++) {
|
|
24
|
+
const ps = patSegs[i];
|
|
25
|
+
if (ps && ps.charAt(0) === ':') {
|
|
26
|
+
const param = ps.replace(/(^:|[+*?]+$)/g, '');
|
|
27
|
+
const flags = ps.match(/[+*?]+$/)?.[0] ?? '';
|
|
28
|
+
const plus = flags.indexOf('+') > -1;
|
|
29
|
+
const star = flags.indexOf('*') > -1;
|
|
30
|
+
const val = urlSegs[i] || '';
|
|
31
|
+
if (!val && !star && (flags.indexOf('?') < 0 || plus))
|
|
32
|
+
return;
|
|
33
|
+
params[param] = decodeURIComponent(val);
|
|
34
|
+
if (plus || star) {
|
|
35
|
+
params[param] = urlSegs.slice(i).map(decodeURIComponent).join('/');
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (ps !== urlSegs[i]) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
pattern: originalPattern,
|
|
46
|
+
url: originalUrl,
|
|
47
|
+
pathname: originalUrl.replace(re, ''),
|
|
48
|
+
params,
|
|
49
|
+
query,
|
|
50
|
+
search,
|
|
51
|
+
hash,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function segmentize(url) {
|
|
55
|
+
return strip(url).split('/');
|
|
56
|
+
}
|
|
57
|
+
function strip(url) {
|
|
58
|
+
return url.replace(/(^\/+|\/+$)/g, '');
|
|
59
|
+
}
|
package/dist/qs.d.ts
ADDED
package/dist/qs.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const qs = {
|
|
2
|
+
parse(queryString) {
|
|
3
|
+
return queryString.split('&').reduce((acc, pair) => {
|
|
4
|
+
if (!pair)
|
|
5
|
+
return acc;
|
|
6
|
+
const i = pair.indexOf('=');
|
|
7
|
+
const key = decode(i < 0 ? pair : pair.slice(0, i));
|
|
8
|
+
const val = i < 0 ? '' : decode(pair.slice(i + 1));
|
|
9
|
+
acc[key] = val;
|
|
10
|
+
return acc;
|
|
11
|
+
}, {});
|
|
12
|
+
},
|
|
13
|
+
stringify(query) {
|
|
14
|
+
return Object.keys(query)
|
|
15
|
+
.reduce((acc, key) => {
|
|
16
|
+
const value = query[key];
|
|
17
|
+
if (value !== undefined) {
|
|
18
|
+
acc.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
|
|
19
|
+
}
|
|
20
|
+
return acc;
|
|
21
|
+
}, [])
|
|
22
|
+
.join('&');
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
function decode(s) {
|
|
26
|
+
return decodeURIComponent(s.replace(/\+/g, ' '));
|
|
27
|
+
}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type MatchedRoute } from './match.ts';
|
|
2
|
+
import { type Mode } from './history.ts';
|
|
3
|
+
import { type Qs } from './qs.ts';
|
|
4
|
+
export interface RouterOptions {
|
|
5
|
+
mode?: Mode;
|
|
6
|
+
qs?: Qs;
|
|
7
|
+
sync?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface MatcherOptions {
|
|
10
|
+
qs?: Qs;
|
|
11
|
+
}
|
|
12
|
+
export interface Route<Data = Record<string, unknown>> extends MatchedRoute {
|
|
13
|
+
data: RouteData<Data>[];
|
|
14
|
+
}
|
|
15
|
+
export interface NavigateTarget {
|
|
16
|
+
url?: string;
|
|
17
|
+
pathname?: string;
|
|
18
|
+
params?: Record<string, string | number>;
|
|
19
|
+
query?: Record<string, unknown> | null;
|
|
20
|
+
hash?: string | null;
|
|
21
|
+
replace?: boolean;
|
|
22
|
+
merge?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export type To = string | NavigateTarget;
|
|
25
|
+
export type Redirect<Data = Record<string, unknown>> = To | ((route: Route<Data>) => To);
|
|
26
|
+
export type RouteData<Data = Record<string, unknown>> = Data & {
|
|
27
|
+
path: string;
|
|
28
|
+
redirect?: Redirect<Data>;
|
|
29
|
+
};
|
|
30
|
+
export type RouteDefinition<Data = Record<string, unknown>> = Data & {
|
|
31
|
+
path?: string;
|
|
32
|
+
redirect?: Redirect<Data>;
|
|
33
|
+
routes?: RouteDefinition<Data>[];
|
|
34
|
+
};
|
|
35
|
+
export interface Router<Data = Record<string, unknown>> {
|
|
36
|
+
listen(routes: RouteDefinition<Data>[], onChange?: (route: Route<Data>) => void): () => void;
|
|
37
|
+
navigate(to: To, curr?: Route<Data>): void;
|
|
38
|
+
href(to: To, curr?: Route<Data>): string;
|
|
39
|
+
match(url: string): Route<Data> | undefined;
|
|
40
|
+
getUrl(): string;
|
|
41
|
+
}
|
|
42
|
+
export interface Matcher<Data = Record<string, unknown>> {
|
|
43
|
+
match(url: string): Route<Data> | undefined;
|
|
44
|
+
}
|
|
45
|
+
interface FlatRoute<Data = Record<string, unknown>> {
|
|
46
|
+
pattern: string;
|
|
47
|
+
data: RouteData<Data>[];
|
|
48
|
+
}
|
|
49
|
+
export declare function createRouter<Data = Record<string, unknown>>(options?: RouterOptions): Router<Data>;
|
|
50
|
+
export declare function createMatcher<Data = Record<string, unknown>>(routeMap: RouteDefinition<Data>[], options?: MatcherOptions): Matcher<Data>;
|
|
51
|
+
export declare function flatten<Data = Record<string, unknown>>(routeMap: RouteDefinition<Data>[]): FlatRoute<Data>[];
|
|
52
|
+
export declare function merge(curr: Partial<Route> | NavigateTarget | undefined, to: NavigateTarget): NavigateTarget;
|
|
53
|
+
export {};
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { matchOne } from "./match.js";
|
|
2
|
+
import { createHistory } from "./history.js";
|
|
3
|
+
import { qs as defaultQs } from "./qs.js";
|
|
4
|
+
const PARAM_RE = /:([A-Za-z0-9_]+)([+*?])?/g;
|
|
5
|
+
const MAX_REDIRECTS = 10;
|
|
6
|
+
export function createRouter(options = {}) {
|
|
7
|
+
const mode = options.mode || 'history';
|
|
8
|
+
const qs = options.qs || defaultQs;
|
|
9
|
+
const sync = options.sync || false;
|
|
10
|
+
const history = createHistory({ mode, sync });
|
|
11
|
+
let matcher = createMatcher([], { qs });
|
|
12
|
+
const router = {
|
|
13
|
+
listen(routeMap, cb) {
|
|
14
|
+
matcher = createMatcher(routeMap, { qs });
|
|
15
|
+
let redirects = 0;
|
|
16
|
+
return history.listen((url) => {
|
|
17
|
+
const route = matcher.match(url);
|
|
18
|
+
if (!route) {
|
|
19
|
+
redirects = 0;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const r of route.data) {
|
|
23
|
+
if (r.redirect) {
|
|
24
|
+
if (++redirects > MAX_REDIRECTS) {
|
|
25
|
+
redirects = 0;
|
|
26
|
+
throw new Error('space-router: too many redirects');
|
|
27
|
+
}
|
|
28
|
+
const target = typeof r.redirect === 'function' ? r.redirect(route) : r.redirect;
|
|
29
|
+
return router.navigate({ url: router.href(target), replace: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
redirects = 0;
|
|
33
|
+
if (cb)
|
|
34
|
+
cb(route);
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
navigate(to, curr) {
|
|
38
|
+
const target = typeof to === 'string' ? { url: to } : to;
|
|
39
|
+
const url = router.href(target, curr);
|
|
40
|
+
if (target.replace) {
|
|
41
|
+
history.replace(url);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
history.push(url);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
href(to, curr) {
|
|
48
|
+
// already a url
|
|
49
|
+
if (typeof to === 'string') {
|
|
50
|
+
return to;
|
|
51
|
+
}
|
|
52
|
+
// align with navigate API
|
|
53
|
+
if (to.url) {
|
|
54
|
+
return to.url;
|
|
55
|
+
}
|
|
56
|
+
let target = to;
|
|
57
|
+
if (target.merge) {
|
|
58
|
+
const c = curr || router.match(router.getUrl());
|
|
59
|
+
target = merge(c, target);
|
|
60
|
+
}
|
|
61
|
+
let url = target.pathname || '/';
|
|
62
|
+
if (target.params) {
|
|
63
|
+
const params = target.params;
|
|
64
|
+
url = url.replace(PARAM_RE, (m, name, flag) => {
|
|
65
|
+
const v = params[name];
|
|
66
|
+
if (v == null)
|
|
67
|
+
return m;
|
|
68
|
+
if (flag === '+' || flag === '*') {
|
|
69
|
+
return String(v).split('/').map(encodeURIComponent).join('/');
|
|
70
|
+
}
|
|
71
|
+
return encodeURIComponent(String(v));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (target.query && Object.keys(target.query).length) {
|
|
75
|
+
const query = qs.stringify(target.query);
|
|
76
|
+
if (query) {
|
|
77
|
+
url = url + '?' + query;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (target.hash) {
|
|
81
|
+
const prefix = target.hash.startsWith('#') ? '' : '#';
|
|
82
|
+
url = url + prefix + target.hash;
|
|
83
|
+
}
|
|
84
|
+
return url;
|
|
85
|
+
},
|
|
86
|
+
match(url) {
|
|
87
|
+
return matcher.match(url);
|
|
88
|
+
},
|
|
89
|
+
getUrl() {
|
|
90
|
+
return history.getUrl();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
return router;
|
|
94
|
+
}
|
|
95
|
+
export function createMatcher(routeMap, options = {}) {
|
|
96
|
+
const routes = flatten(routeMap);
|
|
97
|
+
const qs = options.qs || defaultQs;
|
|
98
|
+
return {
|
|
99
|
+
match(url) {
|
|
100
|
+
if (!url)
|
|
101
|
+
return undefined;
|
|
102
|
+
for (const route of routes) {
|
|
103
|
+
const m = matchOne(route.pattern, url, qs);
|
|
104
|
+
if (m) {
|
|
105
|
+
return { ...m, data: route.data };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function flatten(routeMap) {
|
|
113
|
+
const routes = [];
|
|
114
|
+
const parentData = [];
|
|
115
|
+
function addLevel(level) {
|
|
116
|
+
level.forEach((route) => {
|
|
117
|
+
const { path = '', routes: children, ...routeData } = route;
|
|
118
|
+
const segment = { path, ...routeData };
|
|
119
|
+
// pathless routes only contribute data to their children — they have
|
|
120
|
+
// no pattern of their own to match
|
|
121
|
+
if (path) {
|
|
122
|
+
routes.push({ pattern: path, data: parentData.concat([segment]) });
|
|
123
|
+
}
|
|
124
|
+
if (children) {
|
|
125
|
+
parentData.push(segment);
|
|
126
|
+
addLevel(children);
|
|
127
|
+
parentData.pop();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
addLevel(routeMap);
|
|
132
|
+
return routes;
|
|
133
|
+
}
|
|
134
|
+
export function merge(curr, to) {
|
|
135
|
+
const c = (curr || {});
|
|
136
|
+
const pathname = to.pathname || c.pattern || c.pathname;
|
|
137
|
+
const params = Object.assign({}, c.params, to.params);
|
|
138
|
+
const query = to.query === null ? null : Object.assign({}, c.query, to.query);
|
|
139
|
+
const hash = to.hash === null ? null : to.hash || c.hash || '';
|
|
140
|
+
return { pathname, params, query, hash };
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "space-router",
|
|
3
3
|
"description": "All the routing essentials.",
|
|
4
|
-
"version": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"sideEffects": false,
|
|
8
21
|
"repository": {
|
|
9
22
|
"type": "git",
|
|
10
23
|
"url": "git://github.com/KidkArolis/space-router.git"
|
|
@@ -20,36 +33,30 @@
|
|
|
20
33
|
"url"
|
|
21
34
|
],
|
|
22
35
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
36
|
+
"node": ">=18"
|
|
24
37
|
},
|
|
25
38
|
"author": "Karolis Narkevicius",
|
|
26
39
|
"scripts": {
|
|
27
|
-
"test": "npm run build && oxlint -c oxlintrc.json &&
|
|
28
|
-
"format": "
|
|
40
|
+
"test": "npm run build && oxlint -c oxlintrc.json && oxfmt --check '**/*.{ts,js,css,yml}' && c8 ava",
|
|
41
|
+
"format": "oxfmt --write '**/*.{ts,js,css,yml}'",
|
|
29
42
|
"coverage": "c8 --reporter=html ava",
|
|
30
|
-
"build": "
|
|
31
|
-
"watch": "
|
|
43
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
44
|
+
"watch": "tsc -p tsconfig.build.json --watch",
|
|
32
45
|
"release": "np --no-release-draft",
|
|
33
46
|
"release:beta": "np --tag=beta --no-release-draft",
|
|
34
47
|
"release:docs": "hugo -s docs && gh-pages -d docs/public"
|
|
35
48
|
},
|
|
36
49
|
"devDependencies": {
|
|
37
|
-
"
|
|
38
|
-
"@swc/cli": "^0.8.0",
|
|
39
|
-
"@swc/core": "^1.15.13",
|
|
40
|
-
"ava": "^6.4.1",
|
|
50
|
+
"ava": "^8.0.1",
|
|
41
51
|
"c8": "^11.0.0",
|
|
42
|
-
"execa": "^9.6.1",
|
|
43
52
|
"gh-pages": "^6.3.0",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
53
|
+
"oxfmt": "^0.57.0",
|
|
54
|
+
"oxlint": "^1.72.0",
|
|
55
|
+
"typescript": "^6.0.3"
|
|
46
56
|
},
|
|
47
57
|
"ava": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
58
|
+
"extensions": [
|
|
59
|
+
"ts"
|
|
50
60
|
]
|
|
51
|
-
},
|
|
52
|
-
"overrides": {
|
|
53
|
-
"minimatch": ">=10.2.1"
|
|
54
61
|
}
|
|
55
62
|
}
|
package/src/history.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export type Mode = 'history' | 'hash' | 'memory'
|
|
2
|
+
|
|
3
|
+
export interface History {
|
|
4
|
+
listen(onChange: (url: string) => void): () => void
|
|
5
|
+
getUrl(): string
|
|
6
|
+
push(url: string): void
|
|
7
|
+
replace(url: string): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CreateHistoryOptions {
|
|
11
|
+
mode?: Mode
|
|
12
|
+
sync?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createHistory(options: CreateHistoryOptions = {}): History {
|
|
16
|
+
const sync = !!options.sync
|
|
17
|
+
let mode: Mode = options.mode || 'history'
|
|
18
|
+
let listener: ((url: string) => void) | null = null
|
|
19
|
+
let pending = false
|
|
20
|
+
|
|
21
|
+
const memory: string[] = []
|
|
22
|
+
|
|
23
|
+
if (typeof window === 'undefined') {
|
|
24
|
+
mode = 'memory'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function emit() {
|
|
28
|
+
if (listener) listener(getUrl())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function schedule() {
|
|
32
|
+
if (sync) return emit()
|
|
33
|
+
if (pending) return
|
|
34
|
+
pending = true
|
|
35
|
+
queueMicrotask(() => {
|
|
36
|
+
pending = false
|
|
37
|
+
emit()
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listen(onChange: (url: string) => void) {
|
|
42
|
+
if (listener) throw new Error('Already listening')
|
|
43
|
+
listener = onChange
|
|
44
|
+
let off: (() => void) | undefined
|
|
45
|
+
if (mode !== 'memory') {
|
|
46
|
+
off = on(window, mode === 'history' ? 'popstate' : 'hashchange', schedule)
|
|
47
|
+
schedule()
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
listener = null
|
|
51
|
+
if (off) off()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function go(url: string, replace?: boolean) {
|
|
56
|
+
url = url.replace(/^\/?#?\/?/, '/').replace(/\/$/, '') || '/'
|
|
57
|
+
if (mode === 'history') {
|
|
58
|
+
history[replace ? 'replaceState' : 'pushState']({}, '', url)
|
|
59
|
+
schedule()
|
|
60
|
+
} else if (mode === 'hash') {
|
|
61
|
+
// hashchange only fires when the URL actually changes; if it doesn't,
|
|
62
|
+
// we schedule the emit manually so navigation stays consistent with
|
|
63
|
+
// history mode (where pushState is silent and we always schedule).
|
|
64
|
+
const same = url === getUrl()
|
|
65
|
+
location[replace ? 'replace' : 'assign']('#' + url)
|
|
66
|
+
if (same) schedule()
|
|
67
|
+
} else if (mode === 'memory') {
|
|
68
|
+
if (replace && memory.length) {
|
|
69
|
+
memory[memory.length - 1] = url
|
|
70
|
+
} else {
|
|
71
|
+
memory.push(url)
|
|
72
|
+
}
|
|
73
|
+
schedule()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getUrl(): string {
|
|
78
|
+
if (mode === 'memory') {
|
|
79
|
+
return memory[memory.length - 1] ?? ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hash = getHash()
|
|
83
|
+
if (mode === 'hash') {
|
|
84
|
+
return hash === '' ? '/' : hash
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let url = location.pathname + location.search
|
|
88
|
+
if (hash !== '') {
|
|
89
|
+
url += '#' + hash
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return url
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getHash() {
|
|
96
|
+
return location.hash.slice(1)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
listen,
|
|
101
|
+
getUrl,
|
|
102
|
+
push(url) {
|
|
103
|
+
go(url)
|
|
104
|
+
},
|
|
105
|
+
replace(url) {
|
|
106
|
+
go(url, true)
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function on(el: Window, type: string, fn: () => void) {
|
|
112
|
+
el.addEventListener(type, fn, false)
|
|
113
|
+
return function off() {
|
|
114
|
+
el.removeEventListener(type, fn, false)
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { qs } from './qs.ts'
|
|
2
|
+
export { createHistory } from './history.ts'
|
|
3
|
+
export { createMatcher, createRouter, merge } from './router.ts'
|
|
4
|
+
|
|
5
|
+
export type { Qs } from './qs.ts'
|
|
6
|
+
export type { Mode, History, CreateHistoryOptions } 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'
|