@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Fluixi
3
+ Copyright (c) 2026 Ibrahima Touré
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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-next`](../core)) render the match tree **top-down**, which makes
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 — framework-agnostic router core.
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';
@@ -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 — framework-agnostic router core.
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
+ };