@fluixi/router 0.1.2-alpha.3 → 0.1.2-alpha.5
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/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/index.cjs +299 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +277 -0
- package/dist/lib/history.cjs +122 -0
- package/dist/lib/history.d.ts +4 -2
- package/dist/lib/history.d.ts.map +1 -1
- package/dist/lib/history.js +41 -10
- package/dist/lib/history.mjs +101 -0
- package/dist/lib/match.cjs +122 -0
- package/dist/lib/match.d.ts +1 -1
- package/dist/lib/match.js +3 -3
- package/dist/lib/match.mjs +101 -0
- package/dist/lib/router.cjs +213 -0
- package/dist/lib/router.d.ts +1 -1
- package/dist/lib/router.d.ts.map +1 -1
- package/dist/lib/router.js +10 -6
- package/dist/lib/router.mjs +190 -0
- package/dist/lib/types.cjs +18 -0
- package/dist/lib/types.d.ts +12 -2
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/types.mjs +0 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +11 -3
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
`@fluixi/router` is the routing **core** — pure state and a `subscribe` API, with **no
|
|
19
19
|
reactivity, no DOM, no view framework**. View adapters (the Fluixi adapter lives in
|
|
20
|
-
[`@fluixi/core/router
|
|
20
|
+
[`@fluixi/core/router`](../core)) render the match tree **top-down**, which makes
|
|
21
21
|
deeply nested layout/outlet rendering work by construction. See [DESIGN.md](./DESIGN.md).
|
|
22
22
|
|
|
23
23
|
## 📦 Installation
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/*! @fluixi/router v0.1.2-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
createBrowserHistory: () => createBrowserHistory,
|
|
25
|
+
createMemoryHistory: () => createMemoryHistory,
|
|
26
|
+
createRouter: () => createRouter,
|
|
27
|
+
matchPath: () => matchPath,
|
|
28
|
+
matchRoutes: () => matchRoutes,
|
|
29
|
+
rankRoutes: () => rankRoutes,
|
|
30
|
+
scoreRoute: () => scoreRoute,
|
|
31
|
+
splitPath: () => splitPath
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/lib/match.ts
|
|
36
|
+
function normalizePath(p) {
|
|
37
|
+
return p.startsWith("//") ? p.replace(/^\/+/, "/") : p;
|
|
38
|
+
}
|
|
39
|
+
function matchPath(pattern, path, exact = false) {
|
|
40
|
+
const keys2 = [];
|
|
41
|
+
if (pattern === path) return { params: {}, path };
|
|
42
|
+
if (!exact && pattern === "/" && path.startsWith("/"))
|
|
43
|
+
return { params: {}, path: "/" };
|
|
44
|
+
let regexStr = pattern.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
45
|
+
keys2.push(key);
|
|
46
|
+
return "([^/]+)";
|
|
47
|
+
}).replace(/\*([a-zA-Z0-9_]*)/g, (_, name) => {
|
|
48
|
+
if (name) keys2.push(name);
|
|
49
|
+
return "(.*)";
|
|
50
|
+
});
|
|
51
|
+
if (!regexStr.startsWith("/")) regexStr = "/" + regexStr;
|
|
52
|
+
if (regexStr === "") regexStr = "/";
|
|
53
|
+
const regex = new RegExp(`^${regexStr}${exact ? "$" : "(?=/|$)"}`);
|
|
54
|
+
const match = path.match(regex);
|
|
55
|
+
if (!match) return null;
|
|
56
|
+
const params = {};
|
|
57
|
+
keys2.forEach((key, i) => {
|
|
58
|
+
params[key] = match[i + 1];
|
|
59
|
+
});
|
|
60
|
+
return { params, path: match[0] };
|
|
61
|
+
}
|
|
62
|
+
function scoreRoute(def) {
|
|
63
|
+
const segments = def.path.split("/").filter(Boolean);
|
|
64
|
+
if (segments.length === 0) return 5;
|
|
65
|
+
let score = 0;
|
|
66
|
+
for (const seg of segments) {
|
|
67
|
+
if (seg.startsWith("*")) score += 1;
|
|
68
|
+
else if (seg.startsWith(":")) score += 10;
|
|
69
|
+
else score += 100;
|
|
70
|
+
}
|
|
71
|
+
return score;
|
|
72
|
+
}
|
|
73
|
+
function rankRoutes(defs) {
|
|
74
|
+
return [...defs].sort((a, b) => scoreRoute(b) - scoreRoute(a));
|
|
75
|
+
}
|
|
76
|
+
function matchRoutes(routes, pathname) {
|
|
77
|
+
const path = pathname.length > 1 ? pathname.replace(/\/+$/, "") || "/" : pathname;
|
|
78
|
+
const result = matchInto(routes, path);
|
|
79
|
+
if (!result) return null;
|
|
80
|
+
const matched = [];
|
|
81
|
+
let cumulative = "";
|
|
82
|
+
const params = {};
|
|
83
|
+
for (const layer of result.layers) {
|
|
84
|
+
cumulative = normalizePath(joinMatched(cumulative, layer.consumed));
|
|
85
|
+
const layerParams = {};
|
|
86
|
+
for (const [k, v] of Object.entries(layer.params)) {
|
|
87
|
+
if (!(k in params)) layerParams[k] = v;
|
|
88
|
+
params[k] = v;
|
|
89
|
+
}
|
|
90
|
+
matched.push({
|
|
91
|
+
route: layer.route,
|
|
92
|
+
component: layer.route.component,
|
|
93
|
+
params: layerParams,
|
|
94
|
+
pathname: cumulative || "/"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return { matched, params, pathname };
|
|
98
|
+
}
|
|
99
|
+
function matchInto(defs, currentPath) {
|
|
100
|
+
if (!defs || defs.length === 0) return null;
|
|
101
|
+
const ranked = rankRoutes(defs);
|
|
102
|
+
for (const def of ranked) {
|
|
103
|
+
const hasChildren = !!def.children && def.children.length > 0;
|
|
104
|
+
const seg = matchPath(def.path, currentPath, !hasChildren);
|
|
105
|
+
if (!seg) continue;
|
|
106
|
+
if (hasChildren) {
|
|
107
|
+
const childMatch = matchInto(def.children, currentPath);
|
|
108
|
+
if (childMatch) {
|
|
109
|
+
return {
|
|
110
|
+
layers: [
|
|
111
|
+
{ route: def, params: seg.params, consumed: seg.path },
|
|
112
|
+
...childMatch.layers
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
return { layers: [{ route: def, params: seg.params, consumed: seg.path }] };
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function joinMatched(cumulative, consumed) {
|
|
123
|
+
if (!cumulative) return consumed;
|
|
124
|
+
if (consumed === cumulative) return cumulative;
|
|
125
|
+
if (consumed.startsWith(cumulative)) return consumed;
|
|
126
|
+
const a = cumulative.replace(/\/$/, "");
|
|
127
|
+
const b = consumed.replace(/^\//, "");
|
|
128
|
+
return `${a}/${b}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/lib/history.ts
|
|
132
|
+
var keys = 0;
|
|
133
|
+
var nextKey = () => `${Date.now().toString(36)}-${(keys++).toString(36)}`;
|
|
134
|
+
function splitPath(to) {
|
|
135
|
+
let rest = to;
|
|
136
|
+
let hash = "";
|
|
137
|
+
const hashIdx = rest.indexOf("#");
|
|
138
|
+
if (hashIdx !== -1) {
|
|
139
|
+
hash = rest.slice(hashIdx);
|
|
140
|
+
rest = rest.slice(0, hashIdx);
|
|
141
|
+
}
|
|
142
|
+
let search = "";
|
|
143
|
+
const qIdx = rest.indexOf("?");
|
|
144
|
+
if (qIdx !== -1) {
|
|
145
|
+
search = rest.slice(qIdx);
|
|
146
|
+
rest = rest.slice(0, qIdx);
|
|
147
|
+
}
|
|
148
|
+
return { pathname: rest || "/", search, hash };
|
|
149
|
+
}
|
|
150
|
+
function createMemoryHistory(initial = "/") {
|
|
151
|
+
let entries = [
|
|
152
|
+
{ path: initial, state: null, key: nextKey() }
|
|
153
|
+
];
|
|
154
|
+
let index = 0;
|
|
155
|
+
let action = "pop";
|
|
156
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
157
|
+
const notify = () => listeners.forEach((l) => l());
|
|
158
|
+
return {
|
|
159
|
+
get() {
|
|
160
|
+
const { path, state, key } = entries[index];
|
|
161
|
+
return { ...splitPath(path), state, key, action };
|
|
162
|
+
},
|
|
163
|
+
push(to, state = null) {
|
|
164
|
+
entries = entries.slice(0, index + 1);
|
|
165
|
+
entries.push({ path: to, state, key: nextKey() });
|
|
166
|
+
index = entries.length - 1;
|
|
167
|
+
action = "push";
|
|
168
|
+
notify();
|
|
169
|
+
},
|
|
170
|
+
replace(to, state = null) {
|
|
171
|
+
entries[index] = { path: to, state, key: nextKey() };
|
|
172
|
+
action = "replace";
|
|
173
|
+
notify();
|
|
174
|
+
},
|
|
175
|
+
/** Back (`-1`) or forward (`1`), as the browser's buttons would. */
|
|
176
|
+
go(delta) {
|
|
177
|
+
const next = index + delta;
|
|
178
|
+
if (next < 0 || next >= entries.length) return;
|
|
179
|
+
index = next;
|
|
180
|
+
action = "pop";
|
|
181
|
+
notify();
|
|
182
|
+
},
|
|
183
|
+
subscribe(onChange) {
|
|
184
|
+
listeners.add(onChange);
|
|
185
|
+
return () => listeners.delete(onChange);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function createBrowserHistory() {
|
|
190
|
+
const w = typeof window !== "undefined" ? window : void 0;
|
|
191
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
192
|
+
let action = "pop";
|
|
193
|
+
const notify = () => listeners.forEach((l) => l());
|
|
194
|
+
if (w) w.addEventListener("popstate", () => (action = "pop", notify()));
|
|
195
|
+
const wrapped = (value) => typeof value === "object" && value !== null && typeof value.fx === "string";
|
|
196
|
+
const entry = () => {
|
|
197
|
+
const raw = w.history.state;
|
|
198
|
+
if (wrapped(raw)) return raw;
|
|
199
|
+
const keyed = { fx: nextKey(), usr: raw ?? null };
|
|
200
|
+
w.history.replaceState(keyed, "");
|
|
201
|
+
return keyed;
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
window: w !== void 0,
|
|
205
|
+
get() {
|
|
206
|
+
if (!w) return { pathname: "/", search: "", hash: "", state: null };
|
|
207
|
+
const { pathname, search, hash } = w.location;
|
|
208
|
+
const { fx, usr } = entry();
|
|
209
|
+
return { pathname, search, hash, state: usr, key: fx, action };
|
|
210
|
+
},
|
|
211
|
+
push(to, state = null) {
|
|
212
|
+
w?.history.pushState({ fx: nextKey(), usr: state }, "", to);
|
|
213
|
+
action = "push";
|
|
214
|
+
notify();
|
|
215
|
+
},
|
|
216
|
+
replace(to, state = null) {
|
|
217
|
+
w?.history.replaceState({ fx: nextKey(), usr: state }, "", to);
|
|
218
|
+
action = "replace";
|
|
219
|
+
notify();
|
|
220
|
+
},
|
|
221
|
+
subscribe(onChange) {
|
|
222
|
+
listeners.add(onChange);
|
|
223
|
+
return () => listeners.delete(onChange);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/lib/router.ts
|
|
229
|
+
function parseQuery(search) {
|
|
230
|
+
const q = {};
|
|
231
|
+
if (search) {
|
|
232
|
+
const usp = new URLSearchParams(
|
|
233
|
+
search.startsWith("?") ? search.slice(1) : search
|
|
234
|
+
);
|
|
235
|
+
usp.forEach((v, k) => q[k] = v);
|
|
236
|
+
}
|
|
237
|
+
return q;
|
|
238
|
+
}
|
|
239
|
+
function stripBase(pathname, base) {
|
|
240
|
+
if (!base) return pathname || "/";
|
|
241
|
+
const b = base.replace(/\/$/, "");
|
|
242
|
+
if (pathname === b) return "/";
|
|
243
|
+
if (pathname.startsWith(b + "/")) return pathname.slice(b.length) || "/";
|
|
244
|
+
return pathname || "/";
|
|
245
|
+
}
|
|
246
|
+
function withBase(to, base) {
|
|
247
|
+
if (!base) return to;
|
|
248
|
+
if (/^https?:\/\//.test(to)) return to;
|
|
249
|
+
const b = base.replace(/\/$/, "");
|
|
250
|
+
return `${b}${to.startsWith("/") ? to : "/" + to}`;
|
|
251
|
+
}
|
|
252
|
+
function createRouter(config) {
|
|
253
|
+
const { routes, history, base } = config;
|
|
254
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
255
|
+
const readLocation = () => {
|
|
256
|
+
const raw = history.get();
|
|
257
|
+
return {
|
|
258
|
+
pathname: raw.pathname,
|
|
259
|
+
search: raw.search,
|
|
260
|
+
hash: raw.hash,
|
|
261
|
+
state: raw.state,
|
|
262
|
+
query: parseQuery(raw.search),
|
|
263
|
+
...raw.key !== void 0 ? { key: raw.key } : {},
|
|
264
|
+
...raw.action !== void 0 ? { action: raw.action } : {}
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
const computeMatch = (pathname) => matchRoutes(routes, stripBase(pathname, base));
|
|
268
|
+
let current = (() => {
|
|
269
|
+
const location = readLocation();
|
|
270
|
+
return { location, match: computeMatch(location.pathname) };
|
|
271
|
+
})();
|
|
272
|
+
const recompute = () => {
|
|
273
|
+
const location = readLocation();
|
|
274
|
+
current = { location, match: computeMatch(location.pathname) };
|
|
275
|
+
listeners.forEach((l) => l(current));
|
|
276
|
+
};
|
|
277
|
+
const unsubscribeHistory = history.subscribe(recompute);
|
|
278
|
+
return {
|
|
279
|
+
state() {
|
|
280
|
+
return current;
|
|
281
|
+
},
|
|
282
|
+
subscribe(listener) {
|
|
283
|
+
listeners.add(listener);
|
|
284
|
+
return () => listeners.delete(listener);
|
|
285
|
+
},
|
|
286
|
+
navigate(to, options = {}) {
|
|
287
|
+
const target = withBase(to, base);
|
|
288
|
+
if (options.replace) history.replace(target, options.state ?? null);
|
|
289
|
+
else history.push(target, options.state ?? null);
|
|
290
|
+
},
|
|
291
|
+
match(pathname) {
|
|
292
|
+
return computeMatch(pathname);
|
|
293
|
+
},
|
|
294
|
+
destroy() {
|
|
295
|
+
unsubscribeHistory();
|
|
296
|
+
listeners.clear();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fluixi/router
|
|
2
|
+
* @fluixi/router: framework-agnostic router core.
|
|
3
3
|
*
|
|
4
4
|
* Routing as pure state: URL → resolved match tree + navigation. No reactivity,
|
|
5
5
|
* no DOM, no view framework. View adapters subscribe and render the match tree
|
|
6
6
|
* top-down (see DESIGN.md).
|
|
7
7
|
*/
|
|
8
|
-
export type { RouteDefinition, MatchedRoute, RouteMatch, RouterLocation, NavigateOptions, RouterState, HistoryAdapter, Router, RouterConfig, RouteData, } from './lib/types.js';
|
|
8
|
+
export type { RouteDefinition, MatchedRoute, RouteMatch, RouterLocation, HistoryAction, NavigateOptions, RouterState, HistoryAdapter, Router, RouterConfig, RouteData, } from './lib/types.js';
|
|
9
9
|
export { matchRoutes, matchPath, scoreRoute, rankRoutes } from './lib/match.js';
|
|
10
10
|
export { createMemoryHistory, createBrowserHistory, splitPath, } from './lib/history.js';
|
|
11
11
|
export { createRouter } from './lib/router.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EACV,eAAe,EACf,YAAY,EACZ,UAAU,EACV,cAAc,EACd,eAAe,EACf,WAAW,EACX,cAAc,EACd,MAAM,EACN,YAAY,EACZ,SAAS,GACV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAChF,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EACV,eAAe,EACf,YAAY,EACZ,UAAU,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,WAAW,EACX,cAAc,EACd,MAAM,EACN,YAAY,EACZ,SAAS,GACV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAChF,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fluixi/router
|
|
2
|
+
* @fluixi/router: framework-agnostic router core.
|
|
3
3
|
*
|
|
4
4
|
* Routing as pure state: URL → resolved match tree + navigation. No reactivity,
|
|
5
5
|
* no DOM, no view framework. View adapters subscribe and render the match tree
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/*! @fluixi/router v0.1.2-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
|
|
3
|
+
// src/lib/match.ts
|
|
4
|
+
function normalizePath(p) {
|
|
5
|
+
return p.startsWith("//") ? p.replace(/^\/+/, "/") : p;
|
|
6
|
+
}
|
|
7
|
+
function matchPath(pattern, path, exact = false) {
|
|
8
|
+
const keys2 = [];
|
|
9
|
+
if (pattern === path) return { params: {}, path };
|
|
10
|
+
if (!exact && pattern === "/" && path.startsWith("/"))
|
|
11
|
+
return { params: {}, path: "/" };
|
|
12
|
+
let regexStr = pattern.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
13
|
+
keys2.push(key);
|
|
14
|
+
return "([^/]+)";
|
|
15
|
+
}).replace(/\*([a-zA-Z0-9_]*)/g, (_, name) => {
|
|
16
|
+
if (name) keys2.push(name);
|
|
17
|
+
return "(.*)";
|
|
18
|
+
});
|
|
19
|
+
if (!regexStr.startsWith("/")) regexStr = "/" + regexStr;
|
|
20
|
+
if (regexStr === "") regexStr = "/";
|
|
21
|
+
const regex = new RegExp(`^${regexStr}${exact ? "$" : "(?=/|$)"}`);
|
|
22
|
+
const match = path.match(regex);
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
const params = {};
|
|
25
|
+
keys2.forEach((key, i) => {
|
|
26
|
+
params[key] = match[i + 1];
|
|
27
|
+
});
|
|
28
|
+
return { params, path: match[0] };
|
|
29
|
+
}
|
|
30
|
+
function scoreRoute(def) {
|
|
31
|
+
const segments = def.path.split("/").filter(Boolean);
|
|
32
|
+
if (segments.length === 0) return 5;
|
|
33
|
+
let score = 0;
|
|
34
|
+
for (const seg of segments) {
|
|
35
|
+
if (seg.startsWith("*")) score += 1;
|
|
36
|
+
else if (seg.startsWith(":")) score += 10;
|
|
37
|
+
else score += 100;
|
|
38
|
+
}
|
|
39
|
+
return score;
|
|
40
|
+
}
|
|
41
|
+
function rankRoutes(defs) {
|
|
42
|
+
return [...defs].sort((a, b) => scoreRoute(b) - scoreRoute(a));
|
|
43
|
+
}
|
|
44
|
+
function matchRoutes(routes, pathname) {
|
|
45
|
+
const path = pathname.length > 1 ? pathname.replace(/\/+$/, "") || "/" : pathname;
|
|
46
|
+
const result = matchInto(routes, path);
|
|
47
|
+
if (!result) return null;
|
|
48
|
+
const matched = [];
|
|
49
|
+
let cumulative = "";
|
|
50
|
+
const params = {};
|
|
51
|
+
for (const layer of result.layers) {
|
|
52
|
+
cumulative = normalizePath(joinMatched(cumulative, layer.consumed));
|
|
53
|
+
const layerParams = {};
|
|
54
|
+
for (const [k, v] of Object.entries(layer.params)) {
|
|
55
|
+
if (!(k in params)) layerParams[k] = v;
|
|
56
|
+
params[k] = v;
|
|
57
|
+
}
|
|
58
|
+
matched.push({
|
|
59
|
+
route: layer.route,
|
|
60
|
+
component: layer.route.component,
|
|
61
|
+
params: layerParams,
|
|
62
|
+
pathname: cumulative || "/"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return { matched, params, pathname };
|
|
66
|
+
}
|
|
67
|
+
function matchInto(defs, currentPath) {
|
|
68
|
+
if (!defs || defs.length === 0) return null;
|
|
69
|
+
const ranked = rankRoutes(defs);
|
|
70
|
+
for (const def of ranked) {
|
|
71
|
+
const hasChildren = !!def.children && def.children.length > 0;
|
|
72
|
+
const seg = matchPath(def.path, currentPath, !hasChildren);
|
|
73
|
+
if (!seg) continue;
|
|
74
|
+
if (hasChildren) {
|
|
75
|
+
const childMatch = matchInto(def.children, currentPath);
|
|
76
|
+
if (childMatch) {
|
|
77
|
+
return {
|
|
78
|
+
layers: [
|
|
79
|
+
{ route: def, params: seg.params, consumed: seg.path },
|
|
80
|
+
...childMatch.layers
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
return { layers: [{ route: def, params: seg.params, consumed: seg.path }] };
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function joinMatched(cumulative, consumed) {
|
|
91
|
+
if (!cumulative) return consumed;
|
|
92
|
+
if (consumed === cumulative) return cumulative;
|
|
93
|
+
if (consumed.startsWith(cumulative)) return consumed;
|
|
94
|
+
const a = cumulative.replace(/\/$/, "");
|
|
95
|
+
const b = consumed.replace(/^\//, "");
|
|
96
|
+
return `${a}/${b}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/lib/history.ts
|
|
100
|
+
var keys = 0;
|
|
101
|
+
var nextKey = () => `${Date.now().toString(36)}-${(keys++).toString(36)}`;
|
|
102
|
+
function splitPath(to) {
|
|
103
|
+
let rest = to;
|
|
104
|
+
let hash = "";
|
|
105
|
+
const hashIdx = rest.indexOf("#");
|
|
106
|
+
if (hashIdx !== -1) {
|
|
107
|
+
hash = rest.slice(hashIdx);
|
|
108
|
+
rest = rest.slice(0, hashIdx);
|
|
109
|
+
}
|
|
110
|
+
let search = "";
|
|
111
|
+
const qIdx = rest.indexOf("?");
|
|
112
|
+
if (qIdx !== -1) {
|
|
113
|
+
search = rest.slice(qIdx);
|
|
114
|
+
rest = rest.slice(0, qIdx);
|
|
115
|
+
}
|
|
116
|
+
return { pathname: rest || "/", search, hash };
|
|
117
|
+
}
|
|
118
|
+
function createMemoryHistory(initial = "/") {
|
|
119
|
+
let entries = [
|
|
120
|
+
{ path: initial, state: null, key: nextKey() }
|
|
121
|
+
];
|
|
122
|
+
let index = 0;
|
|
123
|
+
let action = "pop";
|
|
124
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
125
|
+
const notify = () => listeners.forEach((l) => l());
|
|
126
|
+
return {
|
|
127
|
+
get() {
|
|
128
|
+
const { path, state, key } = entries[index];
|
|
129
|
+
return { ...splitPath(path), state, key, action };
|
|
130
|
+
},
|
|
131
|
+
push(to, state = null) {
|
|
132
|
+
entries = entries.slice(0, index + 1);
|
|
133
|
+
entries.push({ path: to, state, key: nextKey() });
|
|
134
|
+
index = entries.length - 1;
|
|
135
|
+
action = "push";
|
|
136
|
+
notify();
|
|
137
|
+
},
|
|
138
|
+
replace(to, state = null) {
|
|
139
|
+
entries[index] = { path: to, state, key: nextKey() };
|
|
140
|
+
action = "replace";
|
|
141
|
+
notify();
|
|
142
|
+
},
|
|
143
|
+
/** Back (`-1`) or forward (`1`), as the browser's buttons would. */
|
|
144
|
+
go(delta) {
|
|
145
|
+
const next = index + delta;
|
|
146
|
+
if (next < 0 || next >= entries.length) return;
|
|
147
|
+
index = next;
|
|
148
|
+
action = "pop";
|
|
149
|
+
notify();
|
|
150
|
+
},
|
|
151
|
+
subscribe(onChange) {
|
|
152
|
+
listeners.add(onChange);
|
|
153
|
+
return () => listeners.delete(onChange);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function createBrowserHistory() {
|
|
158
|
+
const w = typeof window !== "undefined" ? window : void 0;
|
|
159
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
160
|
+
let action = "pop";
|
|
161
|
+
const notify = () => listeners.forEach((l) => l());
|
|
162
|
+
if (w) w.addEventListener("popstate", () => (action = "pop", notify()));
|
|
163
|
+
const wrapped = (value) => typeof value === "object" && value !== null && typeof value.fx === "string";
|
|
164
|
+
const entry = () => {
|
|
165
|
+
const raw = w.history.state;
|
|
166
|
+
if (wrapped(raw)) return raw;
|
|
167
|
+
const keyed = { fx: nextKey(), usr: raw ?? null };
|
|
168
|
+
w.history.replaceState(keyed, "");
|
|
169
|
+
return keyed;
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
window: w !== void 0,
|
|
173
|
+
get() {
|
|
174
|
+
if (!w) return { pathname: "/", search: "", hash: "", state: null };
|
|
175
|
+
const { pathname, search, hash } = w.location;
|
|
176
|
+
const { fx, usr } = entry();
|
|
177
|
+
return { pathname, search, hash, state: usr, key: fx, action };
|
|
178
|
+
},
|
|
179
|
+
push(to, state = null) {
|
|
180
|
+
w?.history.pushState({ fx: nextKey(), usr: state }, "", to);
|
|
181
|
+
action = "push";
|
|
182
|
+
notify();
|
|
183
|
+
},
|
|
184
|
+
replace(to, state = null) {
|
|
185
|
+
w?.history.replaceState({ fx: nextKey(), usr: state }, "", to);
|
|
186
|
+
action = "replace";
|
|
187
|
+
notify();
|
|
188
|
+
},
|
|
189
|
+
subscribe(onChange) {
|
|
190
|
+
listeners.add(onChange);
|
|
191
|
+
return () => listeners.delete(onChange);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/lib/router.ts
|
|
197
|
+
function parseQuery(search) {
|
|
198
|
+
const q = {};
|
|
199
|
+
if (search) {
|
|
200
|
+
const usp = new URLSearchParams(
|
|
201
|
+
search.startsWith("?") ? search.slice(1) : search
|
|
202
|
+
);
|
|
203
|
+
usp.forEach((v, k) => q[k] = v);
|
|
204
|
+
}
|
|
205
|
+
return q;
|
|
206
|
+
}
|
|
207
|
+
function stripBase(pathname, base) {
|
|
208
|
+
if (!base) return pathname || "/";
|
|
209
|
+
const b = base.replace(/\/$/, "");
|
|
210
|
+
if (pathname === b) return "/";
|
|
211
|
+
if (pathname.startsWith(b + "/")) return pathname.slice(b.length) || "/";
|
|
212
|
+
return pathname || "/";
|
|
213
|
+
}
|
|
214
|
+
function withBase(to, base) {
|
|
215
|
+
if (!base) return to;
|
|
216
|
+
if (/^https?:\/\//.test(to)) return to;
|
|
217
|
+
const b = base.replace(/\/$/, "");
|
|
218
|
+
return `${b}${to.startsWith("/") ? to : "/" + to}`;
|
|
219
|
+
}
|
|
220
|
+
function createRouter(config) {
|
|
221
|
+
const { routes, history, base } = config;
|
|
222
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
223
|
+
const readLocation = () => {
|
|
224
|
+
const raw = history.get();
|
|
225
|
+
return {
|
|
226
|
+
pathname: raw.pathname,
|
|
227
|
+
search: raw.search,
|
|
228
|
+
hash: raw.hash,
|
|
229
|
+
state: raw.state,
|
|
230
|
+
query: parseQuery(raw.search),
|
|
231
|
+
...raw.key !== void 0 ? { key: raw.key } : {},
|
|
232
|
+
...raw.action !== void 0 ? { action: raw.action } : {}
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
const computeMatch = (pathname) => matchRoutes(routes, stripBase(pathname, base));
|
|
236
|
+
let current = (() => {
|
|
237
|
+
const location = readLocation();
|
|
238
|
+
return { location, match: computeMatch(location.pathname) };
|
|
239
|
+
})();
|
|
240
|
+
const recompute = () => {
|
|
241
|
+
const location = readLocation();
|
|
242
|
+
current = { location, match: computeMatch(location.pathname) };
|
|
243
|
+
listeners.forEach((l) => l(current));
|
|
244
|
+
};
|
|
245
|
+
const unsubscribeHistory = history.subscribe(recompute);
|
|
246
|
+
return {
|
|
247
|
+
state() {
|
|
248
|
+
return current;
|
|
249
|
+
},
|
|
250
|
+
subscribe(listener) {
|
|
251
|
+
listeners.add(listener);
|
|
252
|
+
return () => listeners.delete(listener);
|
|
253
|
+
},
|
|
254
|
+
navigate(to, options = {}) {
|
|
255
|
+
const target = withBase(to, base);
|
|
256
|
+
if (options.replace) history.replace(target, options.state ?? null);
|
|
257
|
+
else history.push(target, options.state ?? null);
|
|
258
|
+
},
|
|
259
|
+
match(pathname) {
|
|
260
|
+
return computeMatch(pathname);
|
|
261
|
+
},
|
|
262
|
+
destroy() {
|
|
263
|
+
unsubscribeHistory();
|
|
264
|
+
listeners.clear();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
export {
|
|
269
|
+
createBrowserHistory,
|
|
270
|
+
createMemoryHistory,
|
|
271
|
+
createRouter,
|
|
272
|
+
matchPath,
|
|
273
|
+
matchRoutes,
|
|
274
|
+
rankRoutes,
|
|
275
|
+
scoreRoute,
|
|
276
|
+
splitPath
|
|
277
|
+
};
|