@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 StreetUI contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # @streetui/router
2
+
3
+ Client-side routing for StreetUI applications: route matching, navigation,
4
+ active-link state, 404 handling, and route lifecycle cleanup — built entirely on
5
+ StreetUI's own primitives (signals, the compiler, the runtime, the renderer).
6
+
7
+ It introduces **no** virtual DOM, **no** second reactive system, and **no**
8
+ third-party dependencies. The router sits *above* the existing pipeline and
9
+ composes it:
10
+
11
+ ```
12
+ DSL → Compiler → Semantic Application Graph → Runtime → Renderer → DOM
13
+ ▲
14
+ @streetui/router (drives which page is mounted)
15
+ ```
16
+
17
+ Route state is a StreetUI `signal`; route cleanup reuses the core
18
+ `CleanupRegistry`; each route is compiled and mounted as an ordinary StreetUI
19
+ application tree. Only the affected route subtree is re-created on navigation —
20
+ the shell persists.
21
+
22
+ ## Quick start
23
+
24
+ ```ts
25
+ import { createRouter, mountRouter, routerOutlet } from '@streetui/router';
26
+
27
+ const router = createRouter({
28
+ routes: [
29
+ { path: '/', builder: (page) => page.section('home', s => s.heading('Home')) },
30
+ { path: '/docs', builder: (page) => page.section('docs', s => s.heading('Docs')) },
31
+ { path: '/docs/:section', builder: (page, ctx) =>
32
+ page.section('doc', s => s.heading(ctx.params.section ?? '')) },
33
+ { path: '/products', builder: (page, ctx) =>
34
+ page.section('p', s => s.text(`page ${ctx.query.get('page') ?? '1'}`)) },
35
+ { path: '*', builder: (page) => page.section('nf', s => s.heading('404')) },
36
+ ],
37
+ });
38
+
39
+ mountRouter(router, {
40
+ container: document.getElementById('app')!,
41
+ shell: (shell) => {
42
+ shell.section('nav', (n) => {
43
+ n.link('Home', { href: '/' });
44
+ n.link('Docs', { href: '/docs' });
45
+ });
46
+ routerOutlet(shell); // route content renders here
47
+ },
48
+ });
49
+ ```
50
+
51
+ ## Routes
52
+
53
+ A route is `{ path, builder }`. The builder receives the page scope and a
54
+ `RouteContext`, and builds the page with the normal StreetUI DSL.
55
+
56
+ Patterns support:
57
+
58
+ | Pattern | Matches | Captures |
59
+ | ------------------ | -------------------------------- | ------------------- |
60
+ | `/`, `/docs` | that exact path | — |
61
+ | `/users/:id` | `/users/123` | `params.id = "123"` |
62
+ | `/files/*` | `/files/a/b/c` | `params['*'] = "a/b/c"` |
63
+ | `*` | anything (use as the 404 route) | `params['*']` |
64
+
65
+ Routes are matched in definition order — the first match wins, so list a `*`
66
+ route last.
67
+
68
+ ## Dynamic parameters
69
+
70
+ `:name` segments are captured into `ctx.params` (percent-decoded):
71
+
72
+ ```ts
73
+ { path: '/users/:id', builder: (page, ctx) => {
74
+ page.section('u', s => s.heading(`User ${ctx.params.id}`));
75
+ } }
76
+ ```
77
+
78
+ ## Query parameters
79
+
80
+ The query string is parsed into a standard `URLSearchParams` on `ctx.query`:
81
+
82
+ ```ts
83
+ { path: '/products', builder: (page, ctx) => {
84
+ const page$ = ctx.query.get('page') ?? '1'; // /products?page=2 → "2"
85
+ } }
86
+ ```
87
+
88
+ ## Navigation
89
+
90
+ ```ts
91
+ router.navigate('/docs'); // push a new history entry
92
+ router.navigate('/docs', { replace: true }); // replace the current entry
93
+ router.navigate('/products?page=2'); // query strings are carried through
94
+ router.back();
95
+ router.forward();
96
+ ```
97
+
98
+ Internal navigation uses `history.pushState`/`replaceState` — **no full-page
99
+ reload**. Genuine browser back/forward is handled via `popstate`.
100
+
101
+ Clicks on internal `<a>` elements (rendered by the existing `link()` DSL) are
102
+ intercepted for client-side navigation. External links are left untouched:
103
+ absolute URLs (`https://…`), `target="_blank"` (i.e. `link(…, { external: true })`),
104
+ `mailto:`/`tel:`, in-page `#anchors`, and modified clicks (⌘/Ctrl/Shift/Alt or
105
+ non-left button) all behave normally. Disable interception with
106
+ `mountRouter(router, { …, interceptLinks: false })`.
107
+
108
+ ## Active links
109
+
110
+ `isActive` returns a reactive `ReadonlySignal<boolean>` you can consume like any
111
+ StreetUI signal (e.g. inside `when()`):
112
+
113
+ ```ts
114
+ router.isActive('/docs'); // true on /docs and /docs/anything (prefix)
115
+ router.isActive('/docs', { exact: true }); // true only on exactly /docs
116
+ ```
117
+
118
+ ## 404 routes
119
+
120
+ Add a `*` route as the last entry; it renders as a normal StreetUI page tree
121
+ (no special renderer path). Its match reports `currentRoute.get().isFallback === true`.
122
+ If you omit a `*` route, a minimal built-in 404 page is used; override it with
123
+ `createRouter({ routes, notFound })`.
124
+
125
+ ## Route lifecycle & cleanup
126
+
127
+ On navigation A → B, the router disposes route A completely before mounting B:
128
+
129
+ 1. runs A's route-scoped `CleanupRegistry` (everything registered via
130
+ `ctx.onCleanup`), then
131
+ 2. unmounts A's runtime — which disposes DOM nodes, event listeners and signal
132
+ subscriptions via the existing `NodeInstance.dispose()` / renderer teardown.
133
+
134
+ Register any manually-created resources (effects, timers, subscriptions) with
135
+ `ctx.onCleanup` so they are torn down on navigation:
136
+
137
+ ```ts
138
+ { path: '/live', builder: (page, ctx) => {
139
+ const stop = effect(() => console.log(count.get()));
140
+ ctx.onCleanup(stop); // disposed when leaving /live
141
+ const t = setInterval(tick, 1000);
142
+ ctx.onCleanup(() => clearInterval(t));
143
+ } }
144
+ ```
145
+
146
+ There is no second cleanup system — this is the same `CleanupRegistry` the
147
+ runtime and renderer already use.
148
+
149
+ ## Hydration (SSR)
150
+
151
+ When the shell + initial route were rendered on the server, hydrate instead of
152
+ mounting cold:
153
+
154
+ ```ts
155
+ mountRouter(router, { container, hydrate: true, shell });
156
+ ```
157
+
158
+ With `hydrate: true`, `mountRouter` adopts the server-rendered shell and the
159
+ initial route's DOM in place (same element objects, no rebuild), resolving
160
+ dynamic params and query identically on both sides. Client-side navigation then
161
+ takes over — subsequent route changes render fresh into the outlet while the
162
+ shell persists. Use `createMemoryHistory(path)` with the same initial path on
163
+ the server so the initial match agrees. See `packages/renderer/README.md` for
164
+ `renderToString` and the state island.
165
+
166
+ ## History adapters
167
+
168
+ `createRouter` uses a browser history by default. For tests or non-DOM
169
+ environments, pass an in-memory history (deterministic `back()`/`forward()`):
170
+
171
+ ```ts
172
+ import { createMemoryHistory } from '@streetui/router';
173
+ const router = createRouter({ routes, history: createMemoryHistory('/docs') });
174
+ ```
175
+
176
+ ## API reference
177
+
178
+ - `createRouter({ routes, history?, notFound? }): Router`
179
+ - `router.currentRoute: ReadonlySignal<RouteMatch>`
180
+ - `router.navigate(to, { replace? })`, `router.back()`, `router.forward()`
181
+ - `router.isActive(path, { exact? }): ReadonlySignal<boolean>`
182
+ - `router.destroy()`
183
+ - `mountRouter(router, { container, shell?, outletId?, renderer?, interceptLinks?, hydrate? }): MountedRouter`
184
+ - `routerOutlet(scope, id?)` — declare the outlet inside a shell
185
+ - `createBrowserHistory()`, `createMemoryHistory(initial?)`
186
+ - Matching helpers: `matchPattern`, `matchRoutes`, `normalizePath`, `splitTarget`
187
+ - Types: `RouteDefinition`, `RouteContext`, `RouteMatch`, `Router`, `RouterHistory`
package/dist/index.cjs ADDED
@@ -0,0 +1,373 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ROUTER_OUTLET_ID: () => ROUTER_OUTLET_ID,
24
+ createBrowserHistory: () => createBrowserHistory,
25
+ createMemoryHistory: () => createMemoryHistory,
26
+ createRouter: () => createRouter,
27
+ matchPattern: () => matchPattern,
28
+ matchRoutes: () => matchRoutes,
29
+ mountRouter: () => mountRouter,
30
+ normalizePath: () => normalizePath,
31
+ routerOutlet: () => routerOutlet,
32
+ splitTarget: () => splitTarget
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/matching.ts
37
+ function segments(path) {
38
+ return path.split("/").filter((s) => s.length > 0);
39
+ }
40
+ function normalizePath(path) {
41
+ let p = path.trim();
42
+ if (p === "") return "/";
43
+ if (!p.startsWith("/")) p = `/${p}`;
44
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
45
+ return p;
46
+ }
47
+ function matchPattern(pattern, pathname) {
48
+ if (pattern === "*") {
49
+ return { "*": normalizePath(pathname).slice(1) };
50
+ }
51
+ const patSegs = segments(pattern);
52
+ const pathSegs = segments(normalizePath(pathname));
53
+ const params = {};
54
+ for (let i = 0; i < patSegs.length; i++) {
55
+ const patSeg = patSegs[i];
56
+ if (patSeg === "*") {
57
+ params["*"] = pathSegs.slice(i).map((s) => decodeURIComponent(s)).join("/");
58
+ return params;
59
+ }
60
+ const pathSeg = pathSegs[i];
61
+ if (pathSeg === void 0) return null;
62
+ if (patSeg.startsWith(":")) {
63
+ const name = patSeg.slice(1);
64
+ if (name === "") return null;
65
+ params[name] = decodeURIComponent(pathSeg);
66
+ continue;
67
+ }
68
+ if (patSeg !== pathSeg) return null;
69
+ }
70
+ if (pathSegs.length !== patSegs.length) return null;
71
+ return params;
72
+ }
73
+ function matchRoutes(routes, pathname) {
74
+ for (const route of routes) {
75
+ const params = matchPattern(route.path, pathname);
76
+ if (params !== null) return { route, params };
77
+ }
78
+ return null;
79
+ }
80
+ function splitTarget(to) {
81
+ const hashIndex = to.indexOf("#");
82
+ const withoutHash = hashIndex >= 0 ? to.slice(0, hashIndex) : to;
83
+ const qIndex = withoutHash.indexOf("?");
84
+ if (qIndex < 0) return { pathname: normalizePath(withoutHash), search: "" };
85
+ return {
86
+ pathname: normalizePath(withoutHash.slice(0, qIndex)),
87
+ search: withoutHash.slice(qIndex + 1)
88
+ };
89
+ }
90
+
91
+ // src/history.ts
92
+ function buildLocation(pathname, search) {
93
+ return { pathname, search };
94
+ }
95
+ function toUrl(pathname, search) {
96
+ return search.length > 0 ? `${pathname}?${search}` : pathname;
97
+ }
98
+ function createBrowserHistory() {
99
+ const listeners = /* @__PURE__ */ new Set();
100
+ const notify = () => {
101
+ for (const cb of listeners) cb();
102
+ };
103
+ const onPopState = () => notify();
104
+ window.addEventListener("popstate", onPopState);
105
+ const current = () => {
106
+ const loc = window.location;
107
+ return buildLocation(loc.pathname, loc.search.replace(/^\?/, ""));
108
+ };
109
+ return {
110
+ location: current,
111
+ push(pathname, search) {
112
+ window.history.pushState({}, "", toUrl(pathname, search));
113
+ notify();
114
+ },
115
+ replace(pathname, search) {
116
+ window.history.replaceState({}, "", toUrl(pathname, search));
117
+ notify();
118
+ },
119
+ back() {
120
+ window.history.back();
121
+ },
122
+ forward() {
123
+ window.history.forward();
124
+ },
125
+ listen(cb) {
126
+ listeners.add(cb);
127
+ return () => listeners.delete(cb);
128
+ },
129
+ dispose() {
130
+ window.removeEventListener("popstate", onPopState);
131
+ listeners.clear();
132
+ }
133
+ };
134
+ }
135
+ function createMemoryHistory(initial = "/") {
136
+ const listeners = /* @__PURE__ */ new Set();
137
+ const notify = () => {
138
+ for (const cb of listeners) cb();
139
+ };
140
+ const parse = (entry) => {
141
+ const qIndex = entry.indexOf("?");
142
+ if (qIndex < 0) return buildLocation(entry, "");
143
+ return buildLocation(entry.slice(0, qIndex), entry.slice(qIndex + 1));
144
+ };
145
+ const stack = [initial];
146
+ let index = 0;
147
+ return {
148
+ location() {
149
+ return parse(stack[index]);
150
+ },
151
+ push(pathname, search) {
152
+ stack.splice(index + 1);
153
+ stack.push(toUrl(pathname, search));
154
+ index = stack.length - 1;
155
+ notify();
156
+ },
157
+ replace(pathname, search) {
158
+ stack[index] = toUrl(pathname, search);
159
+ notify();
160
+ },
161
+ back() {
162
+ if (index > 0) {
163
+ index--;
164
+ notify();
165
+ }
166
+ },
167
+ forward() {
168
+ if (index < stack.length - 1) {
169
+ index++;
170
+ notify();
171
+ }
172
+ },
173
+ listen(cb) {
174
+ listeners.add(cb);
175
+ return () => listeners.delete(cb);
176
+ },
177
+ dispose() {
178
+ listeners.clear();
179
+ }
180
+ };
181
+ }
182
+
183
+ // src/router.ts
184
+ var import_state = require("@streetui/state");
185
+ var DEFAULT_NOT_FOUND = {
186
+ path: "*",
187
+ builder: (page) => {
188
+ page.section("not-found", (s) => {
189
+ s.heading("404 \u2014 Page not found", { level: 1, id: "not-found-title" });
190
+ s.text("The page you were looking for does not exist.", { id: "not-found-text" });
191
+ s.link("Go home", { href: "/", id: "not-found-home" });
192
+ }, { id: "not-found" });
193
+ }
194
+ };
195
+ function createRouter(options) {
196
+ const routes = options.routes;
197
+ const history = options.history ?? createBrowserHistory();
198
+ const fallback = options.notFound ?? DEFAULT_NOT_FOUND;
199
+ const resolve = () => {
200
+ const loc = history.location();
201
+ const pathname = normalizePath(loc.pathname);
202
+ const query = new URLSearchParams(loc.search);
203
+ const matched = matchRoutes(routes, pathname);
204
+ if (matched !== null) {
205
+ return {
206
+ path: pathname,
207
+ pattern: matched.route.path,
208
+ params: matched.params,
209
+ query,
210
+ route: matched.route,
211
+ // A catch-all `*` match is the 404 route whether user-supplied or built-in.
212
+ isFallback: matched.route.path === "*"
213
+ };
214
+ }
215
+ const fallbackParams = matchRoutes([fallback], pathname)?.params ?? {};
216
+ return {
217
+ path: pathname,
218
+ pattern: fallback.path,
219
+ params: fallbackParams,
220
+ query,
221
+ route: fallback,
222
+ isFallback: true
223
+ };
224
+ };
225
+ const current = (0, import_state.signal)(resolve());
226
+ const stopListening = history.listen(() => {
227
+ current.set(resolve());
228
+ });
229
+ const navigate = (to, opts = {}) => {
230
+ const { pathname, search } = splitTarget(to);
231
+ if (opts.replace === true) history.replace(pathname, search);
232
+ else history.push(pathname, search);
233
+ };
234
+ const isActive = (path, opts = {}) => {
235
+ const target = normalizePath(path);
236
+ const exact = opts.exact === true;
237
+ return (0, import_state.derived)(() => {
238
+ const activePath = current.get().path;
239
+ if (activePath === target) return true;
240
+ if (exact || target === "/") return false;
241
+ return activePath.startsWith(`${target}/`);
242
+ });
243
+ };
244
+ return {
245
+ currentRoute: current,
246
+ navigate,
247
+ back: () => history.back(),
248
+ forward: () => history.forward(),
249
+ isActive,
250
+ destroy: () => {
251
+ stopListening();
252
+ history.dispose();
253
+ }
254
+ };
255
+ }
256
+
257
+ // src/mount-router.ts
258
+ var import_dsl = require("@streetui/dsl");
259
+ var import_compiler = require("@streetui/compiler");
260
+ var import_runtime = require("@streetui/runtime");
261
+ var import_renderer = require("@streetui/renderer");
262
+ var import_core = require("@streetui/core");
263
+ var ROUTER_OUTLET_ID = "streetui-router-outlet";
264
+ function routerOutlet(scope, id = ROUTER_OUTLET_ID) {
265
+ scope.container("router-outlet", () => {
266
+ }, { id });
267
+ }
268
+ function isExternalHref(href) {
269
+ return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href) || // scheme: http:, https:, mailto:, tel:
270
+ href.startsWith("//");
271
+ }
272
+ function mountRouter(router, options) {
273
+ const { container } = options;
274
+ const renderer = options.renderer ?? (0, import_renderer.createRenderer)();
275
+ const outletId = options.outletId ?? ROUTER_OUTLET_ID;
276
+ const interceptLinks = options.interceptLinks ?? true;
277
+ const hydrateMode = options.hydrate ?? false;
278
+ let shellMounted = null;
279
+ let outlet;
280
+ if (options.shell !== void 0) {
281
+ const shellApp = import_dsl.streetui.app({ name: "router-shell" });
282
+ const shellBuilder = options.shell;
283
+ shellApp.page("shell", (page) => shellBuilder(page, router));
284
+ const shellRuntime = (0, import_runtime.createRuntime)({ renderer });
285
+ const shellCompiled = (0, import_compiler.compile)(shellApp);
286
+ if (hydrateMode) {
287
+ for (const node of shellCompiled.graph.findAll((n) => n.getProp("id") === outletId)) {
288
+ node.setProp("_hydrationBoundary", true);
289
+ }
290
+ }
291
+ shellMounted = hydrateMode ? shellRuntime.hydrate(shellCompiled, container) : shellRuntime.mount(shellCompiled, container);
292
+ const found = container.querySelector(`[id="${outletId}"]`);
293
+ if (found === null) {
294
+ throw new Error(
295
+ `[Router] The shell must contain a route outlet. Call routerOutlet(scope) (or add a container with id="${outletId}") inside your shell builder.`
296
+ );
297
+ }
298
+ outlet = found;
299
+ } else {
300
+ outlet = container;
301
+ }
302
+ let active = null;
303
+ let firstRender = hydrateMode;
304
+ const disposeActive = () => {
305
+ if (active === null) return;
306
+ active.registry.run();
307
+ active.mounted.unmount();
308
+ active = null;
309
+ };
310
+ const renderRoute = (match) => {
311
+ disposeActive();
312
+ const registry = new import_core.CleanupRegistry();
313
+ const ctx = {
314
+ path: match.path,
315
+ pattern: match.pattern,
316
+ params: match.params,
317
+ query: match.query,
318
+ onCleanup: (fn) => registry.add(fn)
319
+ };
320
+ const routeApp = import_dsl.streetui.app({ name: `route:${match.pattern}` });
321
+ routeApp.page("route", (page) => match.route.builder(page, ctx));
322
+ const runtime = (0, import_runtime.createRuntime)({ renderer });
323
+ const routeCompiled = (0, import_compiler.compile)(routeApp);
324
+ const mounted = firstRender ? runtime.hydrate(routeCompiled, outlet) : runtime.mount(routeCompiled, outlet);
325
+ firstRender = false;
326
+ active = { registry, mounted };
327
+ };
328
+ renderRoute(router.currentRoute.peek());
329
+ const stopRouteSub = router.currentRoute.subscribe((match) => renderRoute(match));
330
+ const onClick = (event) => {
331
+ if (event.defaultPrevented) return;
332
+ const mouse = event;
333
+ if (typeof mouse.button === "number" && mouse.button !== 0) return;
334
+ if (mouse.metaKey || mouse.ctrlKey || mouse.shiftKey || mouse.altKey) return;
335
+ const target = event.target;
336
+ const anchor = target?.closest?.("a") ?? null;
337
+ if (anchor === null) return;
338
+ const targetAttr = anchor.getAttribute("target");
339
+ if (targetAttr !== null && targetAttr !== "_self") return;
340
+ const href = anchor.getAttribute("href");
341
+ if (href === null || href === "" || href.startsWith("#")) return;
342
+ if (isExternalHref(href)) return;
343
+ event.preventDefault();
344
+ router.navigate(href);
345
+ };
346
+ if (interceptLinks) {
347
+ container.addEventListener("click", onClick);
348
+ }
349
+ return {
350
+ outlet,
351
+ unmount() {
352
+ if (interceptLinks) container.removeEventListener("click", onClick);
353
+ stopRouteSub();
354
+ disposeActive();
355
+ shellMounted?.unmount();
356
+ router.destroy();
357
+ }
358
+ };
359
+ }
360
+ // Annotate the CommonJS export names for ESM import in node:
361
+ 0 && (module.exports = {
362
+ ROUTER_OUTLET_ID,
363
+ createBrowserHistory,
364
+ createMemoryHistory,
365
+ createRouter,
366
+ matchPattern,
367
+ matchRoutes,
368
+ mountRouter,
369
+ normalizePath,
370
+ routerOutlet,
371
+ splitTarget
372
+ });
373
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/matching.ts","../src/history.ts","../src/router.ts","../src/mount-router.ts"],"sourcesContent":["/**\n * @streetui/router — client-side routing for StreetUI applications.\n *\n * Public surface:\n * - createRouter route table + reactive currentRoute + navigation\n * - mountRouter DOM integration (shell + outlet + link interception)\n * - routerOutlet declare the outlet inside a shell\n * - createBrowserHistory window.history-backed navigation source\n * - createMemoryHistory in-memory navigation source (tests / non-DOM)\n * - matching helpers matchPattern / matchRoutes / normalizePath\n * - all router types\n */\n\nexport * from './types.js';\nexport * from './matching.js';\nexport * from './history.js';\nexport * from './router.js';\nexport * from './mount-router.js';\n","/**\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":[]}