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.
Files changed (63) hide show
  1. package/README.md +2 -1
  2. package/dist/history.d.ts +12 -0
  3. package/dist/history.js +98 -0
  4. package/dist/index.d.ts +6 -0
  5. package/dist/index.js +3 -0
  6. package/dist/match.d.ts +11 -0
  7. package/dist/match.js +59 -0
  8. package/dist/qs.d.ts +5 -0
  9. package/dist/qs.js +27 -0
  10. package/dist/router.d.ts +53 -0
  11. package/dist/router.js +141 -0
  12. package/package.json +28 -21
  13. package/src/history.ts +116 -0
  14. package/src/index.ts +18 -0
  15. package/src/match.ts +76 -0
  16. package/src/qs.ts +33 -0
  17. package/src/router.ts +223 -0
  18. package/.prettierignore +0 -5
  19. package/.prettierrc +0 -6
  20. package/.swc-cjs +0 -11
  21. package/.swc-esm +0 -1
  22. package/CHANGELOG.md +0 -68
  23. package/dist/cjs/history.js +0 -100
  24. package/dist/cjs/index.js +0 -27
  25. package/dist/cjs/match.js +0 -94
  26. package/dist/cjs/qs.js +0 -27
  27. package/dist/cjs/router.js +0 -279
  28. package/dist/esm/history.js +0 -90
  29. package/dist/esm/index.js +0 -3
  30. package/dist/esm/match.js +0 -76
  31. package/dist/esm/qs.js +0 -17
  32. package/dist/esm/router.js +0 -258
  33. package/docs/archetypes/default.md +0 -7
  34. package/docs/assets/js/bg.js +0 -742
  35. package/docs/assets/styles/base.scss +0 -2816
  36. package/docs/assets/styles/main.scss +0 -1368
  37. package/docs/assets/styles/syntax-m.scss +0 -264
  38. package/docs/assets/styles/syntax.scss +0 -240
  39. package/docs/config.toml +0 -9
  40. package/docs/content/_index.md +0 -189
  41. package/docs/layouts/404.html +0 -0
  42. package/docs/layouts/_default/baseof.html +0 -68
  43. package/docs/layouts/index.html +0 -31
  44. package/docs/layouts/partials/favicon.html +0 -5
  45. package/docs/layouts/partials/header.html +0 -0
  46. package/docs/layouts/partials/meta/name-author.html +0 -6
  47. package/docs/layouts/partials/meta/ogimage.html +0 -8
  48. package/docs/layouts/partials/site-verification.html +0 -12
  49. package/docs/layouts/shortcodes/callout.html +0 -1
  50. package/docs/static/js/highlightjs-9.15.10.min.js +0 -2
  51. package/docs/static/js/master.js +0 -0
  52. package/docs/static/js/tocbot.min.js +0 -18
  53. package/docs/static/space.png +0 -0
  54. package/oxlintrc.json +0 -6
  55. package/src/history.js +0 -97
  56. package/src/index.js +0 -3
  57. package/src/match.js +0 -87
  58. package/src/qs.js +0 -20
  59. package/src/router.js +0 -147
  60. package/tasks/build.js +0 -16
  61. package/test/flatten.test.js +0 -58
  62. package/test/match.test.js +0 -68
  63. 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 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
 
@@ -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;
@@ -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
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ export { qs } from "./qs.js";
2
+ export { createHistory } from "./history.js";
3
+ export { createMatcher, createRouter, merge } from "./router.js";
@@ -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
@@ -0,0 +1,5 @@
1
+ export interface Qs {
2
+ parse(queryString: string): Record<string, string>;
3
+ stringify(query: Record<string, unknown>): string;
4
+ }
5
+ export declare const qs: Qs;
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
+ }
@@ -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": "0.9.5",
5
- "main": "dist/cjs",
6
- "module": "dist/esm",
7
- "license": "ICS",
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": ">=14"
36
+ "node": ">=18"
24
37
  },
25
38
  "author": "Karolis Narkevicius",
26
39
  "scripts": {
27
- "test": "npm run build && oxlint -c oxlintrc.json && prettier --check '**/*.{js,css,yml}' && c8 ava",
28
- "format": "prettier --write '**/*.{js,css,yml}'",
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": "node ./tasks/build.js",
31
- "watch": "node ./tasks/build.js -w",
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
- "@swc-node/register": "^1.11.1",
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
- "oxlint": "^1.50.0",
45
- "prettier": "^3.8.1"
53
+ "oxfmt": "^0.57.0",
54
+ "oxlint": "^1.72.0",
55
+ "typescript": "^6.0.3"
46
56
  },
47
57
  "ava": {
48
- "require": [
49
- "@swc-node/register"
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'