@streetui/router 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/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/index.cjs +373 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +246 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +337 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// src/matching.ts
|
|
2
|
+
function segments(path) {
|
|
3
|
+
return path.split("/").filter((s) => s.length > 0);
|
|
4
|
+
}
|
|
5
|
+
function normalizePath(path) {
|
|
6
|
+
let p = path.trim();
|
|
7
|
+
if (p === "") return "/";
|
|
8
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
9
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
10
|
+
return p;
|
|
11
|
+
}
|
|
12
|
+
function matchPattern(pattern, pathname) {
|
|
13
|
+
if (pattern === "*") {
|
|
14
|
+
return { "*": normalizePath(pathname).slice(1) };
|
|
15
|
+
}
|
|
16
|
+
const patSegs = segments(pattern);
|
|
17
|
+
const pathSegs = segments(normalizePath(pathname));
|
|
18
|
+
const params = {};
|
|
19
|
+
for (let i = 0; i < patSegs.length; i++) {
|
|
20
|
+
const patSeg = patSegs[i];
|
|
21
|
+
if (patSeg === "*") {
|
|
22
|
+
params["*"] = pathSegs.slice(i).map((s) => decodeURIComponent(s)).join("/");
|
|
23
|
+
return params;
|
|
24
|
+
}
|
|
25
|
+
const pathSeg = pathSegs[i];
|
|
26
|
+
if (pathSeg === void 0) return null;
|
|
27
|
+
if (patSeg.startsWith(":")) {
|
|
28
|
+
const name = patSeg.slice(1);
|
|
29
|
+
if (name === "") return null;
|
|
30
|
+
params[name] = decodeURIComponent(pathSeg);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (patSeg !== pathSeg) return null;
|
|
34
|
+
}
|
|
35
|
+
if (pathSegs.length !== patSegs.length) return null;
|
|
36
|
+
return params;
|
|
37
|
+
}
|
|
38
|
+
function matchRoutes(routes, pathname) {
|
|
39
|
+
for (const route of routes) {
|
|
40
|
+
const params = matchPattern(route.path, pathname);
|
|
41
|
+
if (params !== null) return { route, params };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function splitTarget(to) {
|
|
46
|
+
const hashIndex = to.indexOf("#");
|
|
47
|
+
const withoutHash = hashIndex >= 0 ? to.slice(0, hashIndex) : to;
|
|
48
|
+
const qIndex = withoutHash.indexOf("?");
|
|
49
|
+
if (qIndex < 0) return { pathname: normalizePath(withoutHash), search: "" };
|
|
50
|
+
return {
|
|
51
|
+
pathname: normalizePath(withoutHash.slice(0, qIndex)),
|
|
52
|
+
search: withoutHash.slice(qIndex + 1)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/history.ts
|
|
57
|
+
function buildLocation(pathname, search) {
|
|
58
|
+
return { pathname, search };
|
|
59
|
+
}
|
|
60
|
+
function toUrl(pathname, search) {
|
|
61
|
+
return search.length > 0 ? `${pathname}?${search}` : pathname;
|
|
62
|
+
}
|
|
63
|
+
function createBrowserHistory() {
|
|
64
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
65
|
+
const notify = () => {
|
|
66
|
+
for (const cb of listeners) cb();
|
|
67
|
+
};
|
|
68
|
+
const onPopState = () => notify();
|
|
69
|
+
window.addEventListener("popstate", onPopState);
|
|
70
|
+
const current = () => {
|
|
71
|
+
const loc = window.location;
|
|
72
|
+
return buildLocation(loc.pathname, loc.search.replace(/^\?/, ""));
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
location: current,
|
|
76
|
+
push(pathname, search) {
|
|
77
|
+
window.history.pushState({}, "", toUrl(pathname, search));
|
|
78
|
+
notify();
|
|
79
|
+
},
|
|
80
|
+
replace(pathname, search) {
|
|
81
|
+
window.history.replaceState({}, "", toUrl(pathname, search));
|
|
82
|
+
notify();
|
|
83
|
+
},
|
|
84
|
+
back() {
|
|
85
|
+
window.history.back();
|
|
86
|
+
},
|
|
87
|
+
forward() {
|
|
88
|
+
window.history.forward();
|
|
89
|
+
},
|
|
90
|
+
listen(cb) {
|
|
91
|
+
listeners.add(cb);
|
|
92
|
+
return () => listeners.delete(cb);
|
|
93
|
+
},
|
|
94
|
+
dispose() {
|
|
95
|
+
window.removeEventListener("popstate", onPopState);
|
|
96
|
+
listeners.clear();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function createMemoryHistory(initial = "/") {
|
|
101
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
102
|
+
const notify = () => {
|
|
103
|
+
for (const cb of listeners) cb();
|
|
104
|
+
};
|
|
105
|
+
const parse = (entry) => {
|
|
106
|
+
const qIndex = entry.indexOf("?");
|
|
107
|
+
if (qIndex < 0) return buildLocation(entry, "");
|
|
108
|
+
return buildLocation(entry.slice(0, qIndex), entry.slice(qIndex + 1));
|
|
109
|
+
};
|
|
110
|
+
const stack = [initial];
|
|
111
|
+
let index = 0;
|
|
112
|
+
return {
|
|
113
|
+
location() {
|
|
114
|
+
return parse(stack[index]);
|
|
115
|
+
},
|
|
116
|
+
push(pathname, search) {
|
|
117
|
+
stack.splice(index + 1);
|
|
118
|
+
stack.push(toUrl(pathname, search));
|
|
119
|
+
index = stack.length - 1;
|
|
120
|
+
notify();
|
|
121
|
+
},
|
|
122
|
+
replace(pathname, search) {
|
|
123
|
+
stack[index] = toUrl(pathname, search);
|
|
124
|
+
notify();
|
|
125
|
+
},
|
|
126
|
+
back() {
|
|
127
|
+
if (index > 0) {
|
|
128
|
+
index--;
|
|
129
|
+
notify();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
forward() {
|
|
133
|
+
if (index < stack.length - 1) {
|
|
134
|
+
index++;
|
|
135
|
+
notify();
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
listen(cb) {
|
|
139
|
+
listeners.add(cb);
|
|
140
|
+
return () => listeners.delete(cb);
|
|
141
|
+
},
|
|
142
|
+
dispose() {
|
|
143
|
+
listeners.clear();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/router.ts
|
|
149
|
+
import { signal, derived } from "@streetui/state";
|
|
150
|
+
var DEFAULT_NOT_FOUND = {
|
|
151
|
+
path: "*",
|
|
152
|
+
builder: (page) => {
|
|
153
|
+
page.section("not-found", (s) => {
|
|
154
|
+
s.heading("404 \u2014 Page not found", { level: 1, id: "not-found-title" });
|
|
155
|
+
s.text("The page you were looking for does not exist.", { id: "not-found-text" });
|
|
156
|
+
s.link("Go home", { href: "/", id: "not-found-home" });
|
|
157
|
+
}, { id: "not-found" });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function createRouter(options) {
|
|
161
|
+
const routes = options.routes;
|
|
162
|
+
const history = options.history ?? createBrowserHistory();
|
|
163
|
+
const fallback = options.notFound ?? DEFAULT_NOT_FOUND;
|
|
164
|
+
const resolve = () => {
|
|
165
|
+
const loc = history.location();
|
|
166
|
+
const pathname = normalizePath(loc.pathname);
|
|
167
|
+
const query = new URLSearchParams(loc.search);
|
|
168
|
+
const matched = matchRoutes(routes, pathname);
|
|
169
|
+
if (matched !== null) {
|
|
170
|
+
return {
|
|
171
|
+
path: pathname,
|
|
172
|
+
pattern: matched.route.path,
|
|
173
|
+
params: matched.params,
|
|
174
|
+
query,
|
|
175
|
+
route: matched.route,
|
|
176
|
+
// A catch-all `*` match is the 404 route whether user-supplied or built-in.
|
|
177
|
+
isFallback: matched.route.path === "*"
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const fallbackParams = matchRoutes([fallback], pathname)?.params ?? {};
|
|
181
|
+
return {
|
|
182
|
+
path: pathname,
|
|
183
|
+
pattern: fallback.path,
|
|
184
|
+
params: fallbackParams,
|
|
185
|
+
query,
|
|
186
|
+
route: fallback,
|
|
187
|
+
isFallback: true
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
const current = signal(resolve());
|
|
191
|
+
const stopListening = history.listen(() => {
|
|
192
|
+
current.set(resolve());
|
|
193
|
+
});
|
|
194
|
+
const navigate = (to, opts = {}) => {
|
|
195
|
+
const { pathname, search } = splitTarget(to);
|
|
196
|
+
if (opts.replace === true) history.replace(pathname, search);
|
|
197
|
+
else history.push(pathname, search);
|
|
198
|
+
};
|
|
199
|
+
const isActive = (path, opts = {}) => {
|
|
200
|
+
const target = normalizePath(path);
|
|
201
|
+
const exact = opts.exact === true;
|
|
202
|
+
return derived(() => {
|
|
203
|
+
const activePath = current.get().path;
|
|
204
|
+
if (activePath === target) return true;
|
|
205
|
+
if (exact || target === "/") return false;
|
|
206
|
+
return activePath.startsWith(`${target}/`);
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
currentRoute: current,
|
|
211
|
+
navigate,
|
|
212
|
+
back: () => history.back(),
|
|
213
|
+
forward: () => history.forward(),
|
|
214
|
+
isActive,
|
|
215
|
+
destroy: () => {
|
|
216
|
+
stopListening();
|
|
217
|
+
history.dispose();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/mount-router.ts
|
|
223
|
+
import { streetui } from "@streetui/dsl";
|
|
224
|
+
import { compile } from "@streetui/compiler";
|
|
225
|
+
import { createRuntime } from "@streetui/runtime";
|
|
226
|
+
import { createRenderer } from "@streetui/renderer";
|
|
227
|
+
import { CleanupRegistry } from "@streetui/core";
|
|
228
|
+
var ROUTER_OUTLET_ID = "streetui-router-outlet";
|
|
229
|
+
function routerOutlet(scope, id = ROUTER_OUTLET_ID) {
|
|
230
|
+
scope.container("router-outlet", () => {
|
|
231
|
+
}, { id });
|
|
232
|
+
}
|
|
233
|
+
function isExternalHref(href) {
|
|
234
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href) || // scheme: http:, https:, mailto:, tel:
|
|
235
|
+
href.startsWith("//");
|
|
236
|
+
}
|
|
237
|
+
function mountRouter(router, options) {
|
|
238
|
+
const { container } = options;
|
|
239
|
+
const renderer = options.renderer ?? createRenderer();
|
|
240
|
+
const outletId = options.outletId ?? ROUTER_OUTLET_ID;
|
|
241
|
+
const interceptLinks = options.interceptLinks ?? true;
|
|
242
|
+
const hydrateMode = options.hydrate ?? false;
|
|
243
|
+
let shellMounted = null;
|
|
244
|
+
let outlet;
|
|
245
|
+
if (options.shell !== void 0) {
|
|
246
|
+
const shellApp = streetui.app({ name: "router-shell" });
|
|
247
|
+
const shellBuilder = options.shell;
|
|
248
|
+
shellApp.page("shell", (page) => shellBuilder(page, router));
|
|
249
|
+
const shellRuntime = createRuntime({ renderer });
|
|
250
|
+
const shellCompiled = compile(shellApp);
|
|
251
|
+
if (hydrateMode) {
|
|
252
|
+
for (const node of shellCompiled.graph.findAll((n) => n.getProp("id") === outletId)) {
|
|
253
|
+
node.setProp("_hydrationBoundary", true);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
shellMounted = hydrateMode ? shellRuntime.hydrate(shellCompiled, container) : shellRuntime.mount(shellCompiled, container);
|
|
257
|
+
const found = container.querySelector(`[id="${outletId}"]`);
|
|
258
|
+
if (found === null) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`[Router] The shell must contain a route outlet. Call routerOutlet(scope) (or add a container with id="${outletId}") inside your shell builder.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
outlet = found;
|
|
264
|
+
} else {
|
|
265
|
+
outlet = container;
|
|
266
|
+
}
|
|
267
|
+
let active = null;
|
|
268
|
+
let firstRender = hydrateMode;
|
|
269
|
+
const disposeActive = () => {
|
|
270
|
+
if (active === null) return;
|
|
271
|
+
active.registry.run();
|
|
272
|
+
active.mounted.unmount();
|
|
273
|
+
active = null;
|
|
274
|
+
};
|
|
275
|
+
const renderRoute = (match) => {
|
|
276
|
+
disposeActive();
|
|
277
|
+
const registry = new CleanupRegistry();
|
|
278
|
+
const ctx = {
|
|
279
|
+
path: match.path,
|
|
280
|
+
pattern: match.pattern,
|
|
281
|
+
params: match.params,
|
|
282
|
+
query: match.query,
|
|
283
|
+
onCleanup: (fn) => registry.add(fn)
|
|
284
|
+
};
|
|
285
|
+
const routeApp = streetui.app({ name: `route:${match.pattern}` });
|
|
286
|
+
routeApp.page("route", (page) => match.route.builder(page, ctx));
|
|
287
|
+
const runtime = createRuntime({ renderer });
|
|
288
|
+
const routeCompiled = compile(routeApp);
|
|
289
|
+
const mounted = firstRender ? runtime.hydrate(routeCompiled, outlet) : runtime.mount(routeCompiled, outlet);
|
|
290
|
+
firstRender = false;
|
|
291
|
+
active = { registry, mounted };
|
|
292
|
+
};
|
|
293
|
+
renderRoute(router.currentRoute.peek());
|
|
294
|
+
const stopRouteSub = router.currentRoute.subscribe((match) => renderRoute(match));
|
|
295
|
+
const onClick = (event) => {
|
|
296
|
+
if (event.defaultPrevented) return;
|
|
297
|
+
const mouse = event;
|
|
298
|
+
if (typeof mouse.button === "number" && mouse.button !== 0) return;
|
|
299
|
+
if (mouse.metaKey || mouse.ctrlKey || mouse.shiftKey || mouse.altKey) return;
|
|
300
|
+
const target = event.target;
|
|
301
|
+
const anchor = target?.closest?.("a") ?? null;
|
|
302
|
+
if (anchor === null) return;
|
|
303
|
+
const targetAttr = anchor.getAttribute("target");
|
|
304
|
+
if (targetAttr !== null && targetAttr !== "_self") return;
|
|
305
|
+
const href = anchor.getAttribute("href");
|
|
306
|
+
if (href === null || href === "" || href.startsWith("#")) return;
|
|
307
|
+
if (isExternalHref(href)) return;
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
router.navigate(href);
|
|
310
|
+
};
|
|
311
|
+
if (interceptLinks) {
|
|
312
|
+
container.addEventListener("click", onClick);
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
outlet,
|
|
316
|
+
unmount() {
|
|
317
|
+
if (interceptLinks) container.removeEventListener("click", onClick);
|
|
318
|
+
stopRouteSub();
|
|
319
|
+
disposeActive();
|
|
320
|
+
shellMounted?.unmount();
|
|
321
|
+
router.destroy();
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
export {
|
|
326
|
+
ROUTER_OUTLET_ID,
|
|
327
|
+
createBrowserHistory,
|
|
328
|
+
createMemoryHistory,
|
|
329
|
+
createRouter,
|
|
330
|
+
matchPattern,
|
|
331
|
+
matchRoutes,
|
|
332
|
+
mountRouter,
|
|
333
|
+
normalizePath,
|
|
334
|
+
routerOutlet,
|
|
335
|
+
splitTarget
|
|
336
|
+
};
|
|
337
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/matching.ts","../src/history.ts","../src/router.ts","../src/mount-router.ts"],"sourcesContent":["/**\n * Route matching — pure functions, no DOM, no reactivity.\n *\n * A pattern is matched segment-by-segment against a pathname:\n * - a literal segment must equal the path segment,\n * - a `:name` segment captures the path segment into `params.name`,\n * - a `*` segment (or a whole-pattern `*`) is a catch-all that matches the\n * remainder of the path and captures it into `params['*']`.\n *\n * Matching is intentionally small: no optional segments, no regex constraints,\n * no nested route trees. Composition of layouts is done in the DSL, not here.\n */\n\n/** Split a path into non-empty segments. `/` → [], `/a/b` → ['a','b']. */\nfunction segments(path: string): string[] {\n return path.split('/').filter((s) => s.length > 0);\n}\n\n/** Normalize a pathname: ensure a single leading slash, drop a trailing slash. */\nexport function normalizePath(path: string): string {\n let p = path.trim();\n if (p === '') return '/';\n if (!p.startsWith('/')) p = `/${p}`;\n if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);\n return p;\n}\n\n/**\n * Try to match a single pattern against a pathname.\n * Returns the captured params on success, or `null` on no match.\n */\nexport function matchPattern(\n pattern: string,\n pathname: string,\n): Record<string, string> | null {\n // Whole-pattern wildcard: matches everything.\n if (pattern === '*') {\n return { '*': normalizePath(pathname).slice(1) };\n }\n\n const patSegs = segments(pattern);\n const pathSegs = segments(normalizePath(pathname));\n const params: Record<string, string> = {};\n\n for (let i = 0; i < patSegs.length; i++) {\n const patSeg = patSegs[i]!;\n\n // Trailing catch-all: capture the rest of the path.\n if (patSeg === '*') {\n params['*'] = pathSegs.slice(i).map((s) => decodeURIComponent(s)).join('/');\n return params;\n }\n\n const pathSeg = pathSegs[i];\n if (pathSeg === undefined) return null; // path ran out of segments\n\n if (patSeg.startsWith(':')) {\n const name = patSeg.slice(1);\n if (name === '') return null; // malformed `:` with no name\n params[name] = decodeURIComponent(pathSeg);\n continue;\n }\n\n if (patSeg !== pathSeg) return null; // literal mismatch\n }\n\n // All pattern segments consumed — the path must be fully consumed too.\n if (pathSegs.length !== patSegs.length) return null;\n return params;\n}\n\nexport interface MatchResult<R> {\n readonly route: R;\n readonly params: Record<string, string>;\n}\n\n/**\n * Match a pathname against an ordered list of routes. The first route whose\n * pattern matches wins (definition order), so more specific routes should be\n * listed before a `*` fallback.\n */\nexport function matchRoutes<R extends { path: string }>(\n routes: readonly R[],\n pathname: string,\n): MatchResult<R> | null {\n for (const route of routes) {\n const params = matchPattern(route.path, pathname);\n if (params !== null) return { route, params };\n }\n return null;\n}\n\n/** Split a `to` target into its pathname and (already-stripped) search string. */\nexport function splitTarget(to: string): { pathname: string; search: string } {\n const hashIndex = to.indexOf('#');\n const withoutHash = hashIndex >= 0 ? to.slice(0, hashIndex) : to;\n const qIndex = withoutHash.indexOf('?');\n if (qIndex < 0) return { pathname: normalizePath(withoutHash), search: '' };\n return {\n pathname: normalizePath(withoutHash.slice(0, qIndex)),\n search: withoutHash.slice(qIndex + 1),\n };\n}\n","/**\n * Router history — a small abstraction over the navigation source so the router\n * can run both in the browser (real `window.history` + `popstate`) and in tests\n * (an in-memory stack, fully deterministic, no globals).\n *\n * Internal navigation never triggers a full-page reload: the browser history\n * uses `pushState`/`replaceState` and notifies listeners synchronously.\n */\n\nexport interface RouterLocation {\n /** Pathname, always normalized with a single leading slash. */\n readonly pathname: string;\n /** Query string without the leading `?`. */\n readonly search: string;\n}\n\nexport interface RouterHistory {\n /** The current location. */\n location(): RouterLocation;\n /** Push a new entry and notify listeners. */\n push(pathname: string, search: string): void;\n /** Replace the current entry and notify listeners. */\n replace(pathname: string, search: string): void;\n /** Go back one entry. */\n back(): void;\n /** Go forward one entry. */\n forward(): void;\n /** Subscribe to location changes. Returns an unsubscribe function. */\n listen(cb: () => void): () => void;\n /** Detach any global listeners (browser only). */\n dispose(): void;\n}\n\nfunction buildLocation(pathname: string, search: string): RouterLocation {\n return { pathname, search };\n}\n\nfunction toUrl(pathname: string, search: string): string {\n return search.length > 0 ? `${pathname}?${search}` : pathname;\n}\n\n/**\n * Browser history backed by `window.history`. `pushState`/`replaceState` do not\n * emit `popstate`, so we notify listeners ourselves after those calls; genuine\n * back/forward navigation arrives via the `popstate` event.\n */\nexport function createBrowserHistory(): RouterHistory {\n const listeners = new Set<() => void>();\n const notify = (): void => {\n for (const cb of listeners) cb();\n };\n const onPopState = (): void => notify();\n window.addEventListener('popstate', onPopState);\n\n const current = (): RouterLocation => {\n const loc = window.location;\n return buildLocation(loc.pathname, loc.search.replace(/^\\?/, ''));\n };\n\n return {\n location: current,\n push(pathname, search) {\n window.history.pushState({}, '', toUrl(pathname, search));\n notify();\n },\n replace(pathname, search) {\n window.history.replaceState({}, '', toUrl(pathname, search));\n notify();\n },\n back() {\n window.history.back(); // async → emits popstate\n },\n forward() {\n window.history.forward(); // async → emits popstate\n },\n listen(cb) {\n listeners.add(cb);\n return () => listeners.delete(cb);\n },\n dispose() {\n window.removeEventListener('popstate', onPopState);\n listeners.clear();\n },\n };\n}\n\n/**\n * In-memory history for tests and non-DOM environments. Maintains an explicit\n * stack and cursor so `back()`/`forward()` are deterministic.\n */\nexport function createMemoryHistory(initial = '/'): RouterHistory {\n const listeners = new Set<() => void>();\n const notify = (): void => {\n for (const cb of listeners) cb();\n };\n\n const parse = (entry: string): RouterLocation => {\n const qIndex = entry.indexOf('?');\n if (qIndex < 0) return buildLocation(entry, '');\n return buildLocation(entry.slice(0, qIndex), entry.slice(qIndex + 1));\n };\n\n const stack: string[] = [initial];\n let index = 0;\n\n return {\n location() {\n return parse(stack[index]!);\n },\n push(pathname, search) {\n // Drop any forward entries, then append.\n stack.splice(index + 1);\n stack.push(toUrl(pathname, search));\n index = stack.length - 1;\n notify();\n },\n replace(pathname, search) {\n stack[index] = toUrl(pathname, search);\n notify();\n },\n back() {\n if (index > 0) {\n index--;\n notify();\n }\n },\n forward() {\n if (index < stack.length - 1) {\n index++;\n notify();\n }\n },\n listen(cb) {\n listeners.add(cb);\n return () => listeners.delete(cb);\n },\n dispose() {\n listeners.clear();\n },\n };\n}\n","/**\n * StreetUI Router core — renderer-agnostic.\n *\n * Holds the route table, resolves the current location into a `RouteMatch`,\n * and exposes that match as a StreetUI `signal`. Navigation is delegated to a\n * `RouterHistory`; when the location changes (via `navigate`, `back`, `forward`,\n * or a browser `popstate`) the router recomputes the match and updates the\n * signal, which is how every consumer (`isActive`, the mount integration, any\n * `derived` the app builds) stays in sync. No second reactive system.\n */\n\nimport { signal, derived, type ReadonlySignal } from '@streetui/state';\nimport type { RouterHistory } from './history.js';\nimport { createBrowserHistory } from './history.js';\nimport { matchRoutes, normalizePath, splitTarget } from './matching.js';\nimport type { RouteDefinition, RouteMatch } from './types.js';\n\nexport interface RouterOptions {\n /** The route table. Order matters — the first matching pattern wins. */\n readonly routes: readonly RouteDefinition[];\n /**\n * Navigation source. Defaults to a browser history. Pass a memory history\n * for tests or non-DOM environments.\n */\n readonly history?: RouterHistory;\n /**\n * Fallback route used when nothing else matches and no `*` route is present.\n * Defaults to a built-in 404 page (a normal StreetUI tree — no special path).\n */\n readonly notFound?: RouteDefinition;\n}\n\nexport interface NavigateOptions {\n /** Replace the current history entry instead of pushing a new one. */\n readonly replace?: boolean;\n}\n\nexport interface IsActiveOptions {\n /** Require an exact pathname match rather than a prefix match. */\n readonly exact?: boolean;\n}\n\nexport interface Router {\n /** Reactive current match. Consumers subscribe via StreetUI signals. */\n readonly currentRoute: ReadonlySignal<RouteMatch>;\n /** Navigate to a target path (may include a query string). */\n navigate(to: string, options?: NavigateOptions): void;\n /** Go back one history entry. */\n back(): void;\n /** Go forward one history entry. */\n forward(): void;\n /** Reactive predicate: is `path` the active route (or a prefix of it)? */\n isActive(path: string, options?: IsActiveOptions): ReadonlySignal<boolean>;\n /** Tear down history listeners. */\n destroy(): void;\n}\n\n/** Built-in 404 route — a normal StreetUI page tree, overridable via a `*` route. */\nconst DEFAULT_NOT_FOUND: RouteDefinition = {\n path: '*',\n builder: (page) => {\n page.section('not-found', (s) => {\n s.heading('404 — Page not found', { level: 1, id: 'not-found-title' });\n s.text('The page you were looking for does not exist.', { id: 'not-found-text' });\n s.link('Go home', { href: '/', id: 'not-found-home' });\n }, { id: 'not-found' });\n },\n};\n\nexport function createRouter(options: RouterOptions): Router {\n const routes = options.routes;\n const history = options.history ?? createBrowserHistory();\n const fallback = options.notFound ?? DEFAULT_NOT_FOUND;\n\n const resolve = (): RouteMatch => {\n const loc = history.location();\n const pathname = normalizePath(loc.pathname);\n const query = new URLSearchParams(loc.search);\n const matched = matchRoutes(routes, pathname);\n if (matched !== null) {\n return {\n path: pathname,\n pattern: matched.route.path,\n params: matched.params,\n query,\n route: matched.route,\n // A catch-all `*` match is the 404 route whether user-supplied or built-in.\n isFallback: matched.route.path === '*',\n };\n }\n // No explicit route matched — use the fallback (a `*` route if the table\n // has one, otherwise the built-in 404).\n const fallbackParams =\n matchRoutes([fallback], pathname)?.params ?? {};\n return {\n path: pathname,\n pattern: fallback.path,\n params: fallbackParams,\n query,\n route: fallback,\n isFallback: true,\n };\n };\n\n const current = signal<RouteMatch>(resolve());\n const stopListening = history.listen(() => {\n current.set(resolve());\n });\n\n const navigate = (to: string, opts: NavigateOptions = {}): void => {\n const { pathname, search } = splitTarget(to);\n if (opts.replace === true) history.replace(pathname, search);\n else history.push(pathname, search);\n };\n\n const isActive = (path: string, opts: IsActiveOptions = {}): ReadonlySignal<boolean> => {\n const target = normalizePath(path);\n const exact = opts.exact === true;\n return derived(() => {\n const activePath = current.get().path;\n if (activePath === target) return true;\n if (exact || target === '/') return false;\n return activePath.startsWith(`${target}/`);\n });\n };\n\n return {\n currentRoute: current,\n navigate,\n back: () => history.back(),\n forward: () => history.forward(),\n isActive,\n destroy: () => {\n stopListening();\n history.dispose();\n },\n };\n}\n","/**\n * StreetUI Router — DOM integration.\n *\n * `mountRouter` wires a `Router` to real DOM:\n *\n * 1. Mounts an optional persistent shell (layout + navigation) ONCE. The shell\n * declares an outlet element (see `routerOutlet`) into which route content\n * is rendered.\n * 2. Subscribes to `router.currentRoute`. On every change it disposes the\n * previous route (route-scoped `CleanupRegistry.run()` + `runtime.unmount()`)\n * and mounts the new route's compiled application into the outlet.\n * Only the outlet subtree is re-created — the shell persists.\n * 3. Intercepts clicks on internal `<a>` elements for client-side navigation.\n * External links (absolute URLs, `target=\"_blank\"`, `mailto:`/`tel:`) keep\n * their normal browser behaviour, and `link()` is used unchanged.\n *\n * Each route is an ordinary compiled StreetUI application — no special renderer\n * path, no virtual DOM, no full-application rerender on navigation.\n */\n\nimport { streetui, type PageDSL, type ContainerDSL } from '@streetui/dsl';\nimport { compile } from '@streetui/compiler';\nimport { createRuntime, type MountedApplication, type StreetRenderer } from '@streetui/runtime';\nimport { createRenderer } from '@streetui/renderer';\nimport { CleanupRegistry } from '@streetui/core';\nimport type { Router } from './router.js';\nimport type { RouteContext, RouteMatch } from './types.js';\n\n/** Default id used for the route outlet element inside a shell. */\nexport const ROUTER_OUTLET_ID = 'streetui-router-outlet';\n\n/**\n * Declare the route outlet inside a shell builder. The router replaces this\n * element's contents on every navigation.\n */\nexport function routerOutlet(scope: ContainerDSL, id: string = ROUTER_OUTLET_ID): void {\n scope.container('router-outlet', () => { /* filled by the router at runtime */ }, { id });\n}\n\nexport type ShellBuilder = (shell: PageDSL, router: Router) => void;\n\nexport interface MountRouterOptions {\n /** Element to mount into. With a shell, the shell fills this; otherwise routes do. */\n readonly container: Element;\n /** Optional persistent layout. Must include a `routerOutlet(...)`. */\n readonly shell?: ShellBuilder;\n /** Id of the outlet element within the shell. Defaults to `ROUTER_OUTLET_ID`. */\n readonly outletId?: string;\n /** Override the renderer (e.g. a custom DOM adapter for tests). */\n readonly renderer?: StreetRenderer;\n /** Intercept internal `<a>` clicks for client-side navigation. Defaults to true. */\n readonly interceptLinks?: boolean;\n /**\n * Hydrate server-rendered HTML already present in the container instead of\n * mounting fresh. The shell and the *initial* route adopt the existing DOM;\n * subsequent client-side navigations mount normally. Defaults to false.\n */\n readonly hydrate?: boolean;\n}\n\nexport interface MountedRouter {\n /** The element route content is rendered into. */\n readonly outlet: Element;\n /** Tear down the current route, the shell, link interception and the router. */\n unmount(): void;\n}\n\n/** A URL is external when it targets another origin or a non-navigational scheme. */\nfunction isExternalHref(href: string): boolean {\n return (\n /^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(href) || // scheme: http:, https:, mailto:, tel:\n href.startsWith('//') // protocol-relative\n );\n}\n\nexport function mountRouter(router: Router, options: MountRouterOptions): MountedRouter {\n const { container } = options;\n const renderer = options.renderer ?? createRenderer();\n const outletId = options.outletId ?? ROUTER_OUTLET_ID;\n const interceptLinks = options.interceptLinks ?? true;\n const hydrateMode = options.hydrate ?? false;\n\n // ── 1. Mount the shell once (if any) and resolve the outlet ──────────────────\n let shellMounted: MountedApplication | null = null;\n let outlet: Element;\n\n if (options.shell !== undefined) {\n const shellApp = streetui.app({ name: 'router-shell' });\n const shellBuilder = options.shell;\n shellApp.page('shell', (page) => shellBuilder(page, router));\n const shellRuntime = createRuntime({ renderer });\n const shellCompiled = compile(shellApp);\n if (hydrateMode) {\n // The outlet is a slot the router fills. When the shell hydrates, mark the\n // outlet node as a hydration boundary so shell hydration adopts the outlet\n // element itself but preserves the server-rendered route content inside it\n // (instead of stripping it as surplus). The initial route then hydrates\n // that content node-for-node.\n for (const node of shellCompiled.graph.findAll((n) => n.getProp('id') === outletId)) {\n node.setProp('_hydrationBoundary', true);\n }\n }\n shellMounted = hydrateMode\n ? shellRuntime.hydrate(shellCompiled, container)\n : shellRuntime.mount(shellCompiled, container);\n\n const found = container.querySelector(`[id=\"${outletId}\"]`);\n if (found === null) {\n throw new Error(\n `[Router] The shell must contain a route outlet. Call routerOutlet(scope) ` +\n `(or add a container with id=\"${outletId}\") inside your shell builder.`,\n );\n }\n outlet = found;\n } else {\n outlet = container;\n }\n\n // ── 2. Route mounting / disposal ─────────────────────────────────────────────\n interface ActiveRoute {\n readonly registry: CleanupRegistry;\n readonly mounted: MountedApplication;\n }\n let active: ActiveRoute | null = null;\n // Only the very first route render hydrates the server HTML in the outlet;\n // client-side navigations after that mount fresh.\n let firstRender = hydrateMode;\n\n const disposeActive = (): void => {\n if (active === null) return;\n // Run user-registered cleanup (effects, subscriptions) FIRST, then tear down\n // the DOM + runtime signal bindings. Both reuse existing machinery.\n active.registry.run();\n active.mounted.unmount();\n active = null;\n };\n\n const renderRoute = (match: RouteMatch): void => {\n disposeActive();\n\n const registry = new CleanupRegistry();\n const ctx: RouteContext = {\n path: match.path,\n pattern: match.pattern,\n params: match.params,\n query: match.query,\n onCleanup: (fn) => registry.add(fn),\n };\n\n const routeApp = streetui.app({ name: `route:${match.pattern}` });\n routeApp.page('route', (page) => match.route.builder(page, ctx));\n const runtime = createRuntime({ renderer });\n const routeCompiled = compile(routeApp);\n const mounted = firstRender\n ? runtime.hydrate(routeCompiled, outlet)\n : runtime.mount(routeCompiled, outlet);\n firstRender = false;\n\n active = { registry, mounted };\n };\n\n // Initial render, then react to every route change.\n renderRoute(router.currentRoute.peek());\n const stopRouteSub = router.currentRoute.subscribe((match) => renderRoute(match));\n\n // ── 3. Client-side link interception ─────────────────────────────────────────\n const onClick = (event: Event): void => {\n if (event.defaultPrevented) return;\n const mouse = event as MouseEvent;\n if (typeof mouse.button === 'number' && mouse.button !== 0) return;\n if (mouse.metaKey || mouse.ctrlKey || mouse.shiftKey || mouse.altKey) return;\n\n const target = event.target as Element | null;\n const anchor = target?.closest?.('a') ?? null;\n if (anchor === null) return;\n\n const targetAttr = anchor.getAttribute('target');\n if (targetAttr !== null && targetAttr !== '_self') return; // _blank etc.\n\n const href = anchor.getAttribute('href');\n if (href === null || href === '' || href.startsWith('#')) return;\n if (isExternalHref(href)) return;\n\n event.preventDefault();\n router.navigate(href);\n };\n\n if (interceptLinks) {\n container.addEventListener('click', onClick);\n }\n\n // ── Teardown ─────────────────────────────────────────────────────────────────\n return {\n outlet,\n unmount() {\n if (interceptLinks) container.removeEventListener('click', onClick);\n stopRouteSub();\n disposeActive();\n shellMounted?.unmount();\n router.destroy();\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@streetui/router",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "StreetUI router — client-side routing, navigation, route lifecycle and layout composition for multi-page StreetUI applications",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@streetui/core": "1.0.0",
|
|
29
|
+
"@streetui/state": "1.0.0",
|
|
30
|
+
"@streetui/dsl": "1.0.0",
|
|
31
|
+
"@streetui/compiler": "1.0.0",
|
|
32
|
+
"@streetui/runtime": "1.0.0",
|
|
33
|
+
"@streetui/renderer": "1.0.0",
|
|
34
|
+
"@streetui/dom": "1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "*",
|
|
38
|
+
"tsup": "*",
|
|
39
|
+
"vitest": "*",
|
|
40
|
+
"happy-dom": "*"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
]
|
|
52
|
+
}
|