space-router 0.9.5 → 1.0.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 -0
- 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 +16 -0
- package/dist/match.js +68 -0
- package/dist/qs.d.ts +5 -0
- package/dist/qs.js +27 -0
- package/dist/router.d.ts +42 -0
- package/dist/router.js +131 -0
- package/package.json +29 -22
- package/src/history.ts +116 -0
- package/src/index.ts +7 -0
- package/src/match.ts +88 -0
- package/src/qs.ts +33 -0
- package/src/router.ts +197 -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
|
@@ -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) {
|
|
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 { createRouter, merge } from './router.ts';
|
|
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';
|
package/dist/index.js
ADDED
package/dist/match.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
interface RouteEntry {
|
|
12
|
+
pattern: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function match(routes: RouteEntry[], url: string | undefined, qs: Qs): MatchedRoute | undefined;
|
|
15
|
+
export declare function matchOne(pattern: string, url: string, qs?: Qs): MatchedRoute | undefined;
|
|
16
|
+
export {};
|
package/dist/match.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
export function matchOne(pattern, url, qs) {
|
|
11
|
+
if (!pattern)
|
|
12
|
+
return;
|
|
13
|
+
const re = /(?:\?([^#]*))?(#.*)?$/;
|
|
14
|
+
const originalUrl = url;
|
|
15
|
+
const originalPattern = pattern;
|
|
16
|
+
const c = url.match(re);
|
|
17
|
+
const params = {};
|
|
18
|
+
let query = {};
|
|
19
|
+
let search = '';
|
|
20
|
+
let hash = '';
|
|
21
|
+
if (c && c[1]) {
|
|
22
|
+
search = '?' + c[1];
|
|
23
|
+
query = qs ? qs.parse(c[1]) : {};
|
|
24
|
+
}
|
|
25
|
+
if (c && c[2]) {
|
|
26
|
+
hash = c[2];
|
|
27
|
+
}
|
|
28
|
+
if (pattern !== '*') {
|
|
29
|
+
const urlSegs = segmentize(url.replace(re, ''));
|
|
30
|
+
const patSegs = segmentize(pattern);
|
|
31
|
+
const max = Math.max(urlSegs.length, patSegs.length);
|
|
32
|
+
for (let i = 0; i < max; i++) {
|
|
33
|
+
const ps = patSegs[i];
|
|
34
|
+
if (ps && ps.charAt(0) === ':') {
|
|
35
|
+
const param = ps.replace(/(^:|[+*?]+$)/g, '');
|
|
36
|
+
const flags = ps.match(/[+*?]+$/)?.[0] ?? '';
|
|
37
|
+
const plus = flags.indexOf('+') > -1;
|
|
38
|
+
const star = flags.indexOf('*') > -1;
|
|
39
|
+
const val = urlSegs[i] || '';
|
|
40
|
+
if (!val && !star && (flags.indexOf('?') < 0 || plus))
|
|
41
|
+
return;
|
|
42
|
+
params[param] = decodeURIComponent(val);
|
|
43
|
+
if (plus || star) {
|
|
44
|
+
params[param] = urlSegs.slice(i).map(decodeURIComponent).join('/');
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (ps !== urlSegs[i]) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
pattern: originalPattern,
|
|
55
|
+
url: originalUrl,
|
|
56
|
+
pathname: originalUrl.replace(re, ''),
|
|
57
|
+
params,
|
|
58
|
+
query,
|
|
59
|
+
search,
|
|
60
|
+
hash,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function segmentize(url) {
|
|
64
|
+
return strip(url).split('/');
|
|
65
|
+
}
|
|
66
|
+
function strip(url) {
|
|
67
|
+
return url.replace(/(^\/+|\/+$)/g, '');
|
|
68
|
+
}
|
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,42 @@
|
|
|
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 Route<Data = Record<string, unknown>> extends MatchedRoute {
|
|
10
|
+
data: Data[];
|
|
11
|
+
}
|
|
12
|
+
export interface NavigateTarget {
|
|
13
|
+
url?: string;
|
|
14
|
+
pathname?: string;
|
|
15
|
+
params?: Record<string, string | number>;
|
|
16
|
+
query?: Record<string, unknown> | null;
|
|
17
|
+
hash?: string | null;
|
|
18
|
+
replace?: boolean;
|
|
19
|
+
merge?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export type To = string | NavigateTarget;
|
|
22
|
+
export type Redirect<Data = Record<string, unknown>> = To | ((route: Route<Data>) => To);
|
|
23
|
+
export type RouteDefinition<Data = Record<string, unknown>> = Data & {
|
|
24
|
+
path?: string;
|
|
25
|
+
redirect?: Redirect<Data>;
|
|
26
|
+
routes?: RouteDefinition<Data>[];
|
|
27
|
+
};
|
|
28
|
+
export interface Router<Data = Record<string, unknown>> {
|
|
29
|
+
listen(routes: RouteDefinition<Data>[], onChange?: (route: Route<Data>) => void): () => void;
|
|
30
|
+
navigate(to: To, curr?: Route<Data>): void;
|
|
31
|
+
href(to: To, curr?: Route<Data>): string;
|
|
32
|
+
match(url: string): Route<Data> | undefined;
|
|
33
|
+
getUrl(): string;
|
|
34
|
+
}
|
|
35
|
+
interface FlatRoute {
|
|
36
|
+
pattern: string;
|
|
37
|
+
data: Array<Record<string, unknown>>;
|
|
38
|
+
}
|
|
39
|
+
export declare function createRouter<Data = Record<string, unknown>>(options?: RouterOptions): Router<Data>;
|
|
40
|
+
export declare function flatten(routeMap: RouteDefinition[]): FlatRoute[];
|
|
41
|
+
export declare function merge(curr: Partial<Route> | NavigateTarget | undefined, to: NavigateTarget): NavigateTarget;
|
|
42
|
+
export {};
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { match as findMatch } 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 routes = [];
|
|
12
|
+
const router = {
|
|
13
|
+
listen(routeMap, cb) {
|
|
14
|
+
routes = flatten(routeMap);
|
|
15
|
+
let redirects = 0;
|
|
16
|
+
return history.listen((url) => {
|
|
17
|
+
const route = router.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
|
+
const route = findMatch(routes, url, qs);
|
|
88
|
+
if (route) {
|
|
89
|
+
return { ...route, data: data(routes, route) };
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
},
|
|
93
|
+
getUrl() {
|
|
94
|
+
return history.getUrl();
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
return router;
|
|
98
|
+
}
|
|
99
|
+
export function flatten(routeMap) {
|
|
100
|
+
const routes = [];
|
|
101
|
+
const parentData = [];
|
|
102
|
+
function addLevel(level) {
|
|
103
|
+
level.forEach((route) => {
|
|
104
|
+
const { path = '', routes: children, ...routeData } = route;
|
|
105
|
+
routes.push({ pattern: path, data: parentData.concat([routeData]) });
|
|
106
|
+
if (children) {
|
|
107
|
+
parentData.push(routeData);
|
|
108
|
+
addLevel(children);
|
|
109
|
+
parentData.pop();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
addLevel(routeMap);
|
|
114
|
+
return routes;
|
|
115
|
+
}
|
|
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
|
+
export function merge(curr, to) {
|
|
125
|
+
const c = (curr || {});
|
|
126
|
+
const pathname = to.pathname || c.pattern || c.pathname;
|
|
127
|
+
const params = Object.assign({}, c.params, to.params);
|
|
128
|
+
const query = to.query === null ? null : Object.assign({}, c.query, to.query);
|
|
129
|
+
const hash = to.hash === null ? null : to.hash || c.hash || '';
|
|
130
|
+
return { pathname, params, query, hash };
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "space-router",
|
|
3
3
|
"description": "All the routing essentials.",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
4
|
+
"version": "1.0.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": "^7.0.0",
|
|
41
51
|
"c8": "^11.0.0",
|
|
42
|
-
"execa": "^9.6.1",
|
|
43
52
|
"gh-pages": "^6.3.0",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
53
|
+
"oxfmt": "^0.46.0",
|
|
54
|
+
"oxlint": "^1.61.0",
|
|
55
|
+
"typescript": "^6.0.3"
|
|
46
56
|
},
|
|
47
57
|
"ava": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
},
|
|
52
|
-
"overrides": {
|
|
53
|
-
"minimatch": ">=10.2.1"
|
|
58
|
+
"extensions": {
|
|
59
|
+
"ts": "module"
|
|
60
|
+
}
|
|
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) {
|
|
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,7 @@
|
|
|
1
|
+
export { qs } from './qs.ts'
|
|
2
|
+
export { createHistory } from './history.ts'
|
|
3
|
+
export { 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 { RouterOptions, Route, NavigateTarget, To, Redirect, RouteDefinition, Router } from './router.ts'
|