@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.
@@ -0,0 +1,246 @@
1
+ import { PageDSL, ContainerDSL } from '@streetui/dsl';
2
+ import { ReadonlySignal } from '@streetui/state';
3
+ import { StreetRenderer } from '@streetui/runtime';
4
+
5
+ /**
6
+ * StreetUI Router — public type surface.
7
+ *
8
+ * The router sits ABOVE the DSL/compiler/runtime/renderer and composes them.
9
+ * It introduces no new rendering path and no second reactive system: route
10
+ * state is a StreetUI `signal`, route cleanup reuses the core `CleanupRegistry`,
11
+ * and each route renders as an ordinary compiled StreetUI application tree.
12
+ */
13
+
14
+ /**
15
+ * Context handed to a route builder when its route becomes active.
16
+ *
17
+ * `params` are the values captured from dynamic segments (`/users/:id`),
18
+ * `query` is the parsed query string, and `onCleanup` registers work to run
19
+ * when the router navigates away from this route (subscriptions, effects,
20
+ * timers). It is backed by a route-scoped `CleanupRegistry` — there is no
21
+ * separate cleanup mechanism.
22
+ */
23
+ interface RouteContext {
24
+ /** Full matched pathname, e.g. `/users/123`. */
25
+ readonly path: string;
26
+ /** The route pattern that matched, e.g. `/users/:id` or `*`. */
27
+ readonly pattern: string;
28
+ /** Dynamic segment values captured from the path. */
29
+ readonly params: Readonly<Record<string, string>>;
30
+ /** Parsed query string (everything after `?`). */
31
+ readonly query: URLSearchParams;
32
+ /** Register a callback to run when navigating away from this route. */
33
+ onCleanup(fn: () => void): void;
34
+ }
35
+ /** Builds a route's page tree. Receives the page scope and the route context. */
36
+ type RouteBuilder = (page: PageDSL, ctx: RouteContext) => void;
37
+ /** A single route: a path pattern and the builder that renders it. */
38
+ interface RouteDefinition {
39
+ /**
40
+ * Path pattern. Supports:
41
+ * - static segments: `/`, `/docs`, `/docs/getting-started`
42
+ * - dynamic `:param`: `/users/:id`
43
+ * - catch-all wildcard: `*` (matches anything — use for a 404 route)
44
+ */
45
+ readonly path: string;
46
+ readonly builder: RouteBuilder;
47
+ }
48
+ /** The result of resolving a location against the route table. */
49
+ interface RouteMatch {
50
+ /** The active pathname (no query string). */
51
+ readonly path: string;
52
+ /** The pattern of the matched route. */
53
+ readonly pattern: string;
54
+ /** Captured dynamic params. */
55
+ readonly params: Readonly<Record<string, string>>;
56
+ /** Parsed query string. */
57
+ readonly query: URLSearchParams;
58
+ /** The route definition that produced this match. */
59
+ readonly route: RouteDefinition;
60
+ /** True when this match came from the wildcard catch-all (`*`) — i.e. a 404. */
61
+ readonly isFallback: boolean;
62
+ }
63
+
64
+ /**
65
+ * Route matching — pure functions, no DOM, no reactivity.
66
+ *
67
+ * A pattern is matched segment-by-segment against a pathname:
68
+ * - a literal segment must equal the path segment,
69
+ * - a `:name` segment captures the path segment into `params.name`,
70
+ * - a `*` segment (or a whole-pattern `*`) is a catch-all that matches the
71
+ * remainder of the path and captures it into `params['*']`.
72
+ *
73
+ * Matching is intentionally small: no optional segments, no regex constraints,
74
+ * no nested route trees. Composition of layouts is done in the DSL, not here.
75
+ */
76
+ /** Normalize a pathname: ensure a single leading slash, drop a trailing slash. */
77
+ declare function normalizePath(path: string): string;
78
+ /**
79
+ * Try to match a single pattern against a pathname.
80
+ * Returns the captured params on success, or `null` on no match.
81
+ */
82
+ declare function matchPattern(pattern: string, pathname: string): Record<string, string> | null;
83
+ interface MatchResult<R> {
84
+ readonly route: R;
85
+ readonly params: Record<string, string>;
86
+ }
87
+ /**
88
+ * Match a pathname against an ordered list of routes. The first route whose
89
+ * pattern matches wins (definition order), so more specific routes should be
90
+ * listed before a `*` fallback.
91
+ */
92
+ declare function matchRoutes<R extends {
93
+ path: string;
94
+ }>(routes: readonly R[], pathname: string): MatchResult<R> | null;
95
+ /** Split a `to` target into its pathname and (already-stripped) search string. */
96
+ declare function splitTarget(to: string): {
97
+ pathname: string;
98
+ search: string;
99
+ };
100
+
101
+ /**
102
+ * Router history — a small abstraction over the navigation source so the router
103
+ * can run both in the browser (real `window.history` + `popstate`) and in tests
104
+ * (an in-memory stack, fully deterministic, no globals).
105
+ *
106
+ * Internal navigation never triggers a full-page reload: the browser history
107
+ * uses `pushState`/`replaceState` and notifies listeners synchronously.
108
+ */
109
+ interface RouterLocation {
110
+ /** Pathname, always normalized with a single leading slash. */
111
+ readonly pathname: string;
112
+ /** Query string without the leading `?`. */
113
+ readonly search: string;
114
+ }
115
+ interface RouterHistory {
116
+ /** The current location. */
117
+ location(): RouterLocation;
118
+ /** Push a new entry and notify listeners. */
119
+ push(pathname: string, search: string): void;
120
+ /** Replace the current entry and notify listeners. */
121
+ replace(pathname: string, search: string): void;
122
+ /** Go back one entry. */
123
+ back(): void;
124
+ /** Go forward one entry. */
125
+ forward(): void;
126
+ /** Subscribe to location changes. Returns an unsubscribe function. */
127
+ listen(cb: () => void): () => void;
128
+ /** Detach any global listeners (browser only). */
129
+ dispose(): void;
130
+ }
131
+ /**
132
+ * Browser history backed by `window.history`. `pushState`/`replaceState` do not
133
+ * emit `popstate`, so we notify listeners ourselves after those calls; genuine
134
+ * back/forward navigation arrives via the `popstate` event.
135
+ */
136
+ declare function createBrowserHistory(): RouterHistory;
137
+ /**
138
+ * In-memory history for tests and non-DOM environments. Maintains an explicit
139
+ * stack and cursor so `back()`/`forward()` are deterministic.
140
+ */
141
+ declare function createMemoryHistory(initial?: string): RouterHistory;
142
+
143
+ /**
144
+ * StreetUI Router core — renderer-agnostic.
145
+ *
146
+ * Holds the route table, resolves the current location into a `RouteMatch`,
147
+ * and exposes that match as a StreetUI `signal`. Navigation is delegated to a
148
+ * `RouterHistory`; when the location changes (via `navigate`, `back`, `forward`,
149
+ * or a browser `popstate`) the router recomputes the match and updates the
150
+ * signal, which is how every consumer (`isActive`, the mount integration, any
151
+ * `derived` the app builds) stays in sync. No second reactive system.
152
+ */
153
+
154
+ interface RouterOptions {
155
+ /** The route table. Order matters — the first matching pattern wins. */
156
+ readonly routes: readonly RouteDefinition[];
157
+ /**
158
+ * Navigation source. Defaults to a browser history. Pass a memory history
159
+ * for tests or non-DOM environments.
160
+ */
161
+ readonly history?: RouterHistory;
162
+ /**
163
+ * Fallback route used when nothing else matches and no `*` route is present.
164
+ * Defaults to a built-in 404 page (a normal StreetUI tree — no special path).
165
+ */
166
+ readonly notFound?: RouteDefinition;
167
+ }
168
+ interface NavigateOptions {
169
+ /** Replace the current history entry instead of pushing a new one. */
170
+ readonly replace?: boolean;
171
+ }
172
+ interface IsActiveOptions {
173
+ /** Require an exact pathname match rather than a prefix match. */
174
+ readonly exact?: boolean;
175
+ }
176
+ interface Router {
177
+ /** Reactive current match. Consumers subscribe via StreetUI signals. */
178
+ readonly currentRoute: ReadonlySignal<RouteMatch>;
179
+ /** Navigate to a target path (may include a query string). */
180
+ navigate(to: string, options?: NavigateOptions): void;
181
+ /** Go back one history entry. */
182
+ back(): void;
183
+ /** Go forward one history entry. */
184
+ forward(): void;
185
+ /** Reactive predicate: is `path` the active route (or a prefix of it)? */
186
+ isActive(path: string, options?: IsActiveOptions): ReadonlySignal<boolean>;
187
+ /** Tear down history listeners. */
188
+ destroy(): void;
189
+ }
190
+ declare function createRouter(options: RouterOptions): Router;
191
+
192
+ /**
193
+ * StreetUI Router — DOM integration.
194
+ *
195
+ * `mountRouter` wires a `Router` to real DOM:
196
+ *
197
+ * 1. Mounts an optional persistent shell (layout + navigation) ONCE. The shell
198
+ * declares an outlet element (see `routerOutlet`) into which route content
199
+ * is rendered.
200
+ * 2. Subscribes to `router.currentRoute`. On every change it disposes the
201
+ * previous route (route-scoped `CleanupRegistry.run()` + `runtime.unmount()`)
202
+ * and mounts the new route's compiled application into the outlet.
203
+ * Only the outlet subtree is re-created — the shell persists.
204
+ * 3. Intercepts clicks on internal `<a>` elements for client-side navigation.
205
+ * External links (absolute URLs, `target="_blank"`, `mailto:`/`tel:`) keep
206
+ * their normal browser behaviour, and `link()` is used unchanged.
207
+ *
208
+ * Each route is an ordinary compiled StreetUI application — no special renderer
209
+ * path, no virtual DOM, no full-application rerender on navigation.
210
+ */
211
+
212
+ /** Default id used for the route outlet element inside a shell. */
213
+ declare const ROUTER_OUTLET_ID = "streetui-router-outlet";
214
+ /**
215
+ * Declare the route outlet inside a shell builder. The router replaces this
216
+ * element's contents on every navigation.
217
+ */
218
+ declare function routerOutlet(scope: ContainerDSL, id?: string): void;
219
+ type ShellBuilder = (shell: PageDSL, router: Router) => void;
220
+ interface MountRouterOptions {
221
+ /** Element to mount into. With a shell, the shell fills this; otherwise routes do. */
222
+ readonly container: Element;
223
+ /** Optional persistent layout. Must include a `routerOutlet(...)`. */
224
+ readonly shell?: ShellBuilder;
225
+ /** Id of the outlet element within the shell. Defaults to `ROUTER_OUTLET_ID`. */
226
+ readonly outletId?: string;
227
+ /** Override the renderer (e.g. a custom DOM adapter for tests). */
228
+ readonly renderer?: StreetRenderer;
229
+ /** Intercept internal `<a>` clicks for client-side navigation. Defaults to true. */
230
+ readonly interceptLinks?: boolean;
231
+ /**
232
+ * Hydrate server-rendered HTML already present in the container instead of
233
+ * mounting fresh. The shell and the *initial* route adopt the existing DOM;
234
+ * subsequent client-side navigations mount normally. Defaults to false.
235
+ */
236
+ readonly hydrate?: boolean;
237
+ }
238
+ interface MountedRouter {
239
+ /** The element route content is rendered into. */
240
+ readonly outlet: Element;
241
+ /** Tear down the current route, the shell, link interception and the router. */
242
+ unmount(): void;
243
+ }
244
+ declare function mountRouter(router: Router, options: MountRouterOptions): MountedRouter;
245
+
246
+ export { type IsActiveOptions, type MatchResult, type MountRouterOptions, type MountedRouter, type NavigateOptions, ROUTER_OUTLET_ID, type RouteBuilder, type RouteContext, type RouteDefinition, type RouteMatch, type Router, type RouterHistory, type RouterLocation, type RouterOptions, type ShellBuilder, createBrowserHistory, createMemoryHistory, createRouter, matchPattern, matchRoutes, mountRouter, normalizePath, routerOutlet, splitTarget };
@@ -0,0 +1,246 @@
1
+ import { PageDSL, ContainerDSL } from '@streetui/dsl';
2
+ import { ReadonlySignal } from '@streetui/state';
3
+ import { StreetRenderer } from '@streetui/runtime';
4
+
5
+ /**
6
+ * StreetUI Router — public type surface.
7
+ *
8
+ * The router sits ABOVE the DSL/compiler/runtime/renderer and composes them.
9
+ * It introduces no new rendering path and no second reactive system: route
10
+ * state is a StreetUI `signal`, route cleanup reuses the core `CleanupRegistry`,
11
+ * and each route renders as an ordinary compiled StreetUI application tree.
12
+ */
13
+
14
+ /**
15
+ * Context handed to a route builder when its route becomes active.
16
+ *
17
+ * `params` are the values captured from dynamic segments (`/users/:id`),
18
+ * `query` is the parsed query string, and `onCleanup` registers work to run
19
+ * when the router navigates away from this route (subscriptions, effects,
20
+ * timers). It is backed by a route-scoped `CleanupRegistry` — there is no
21
+ * separate cleanup mechanism.
22
+ */
23
+ interface RouteContext {
24
+ /** Full matched pathname, e.g. `/users/123`. */
25
+ readonly path: string;
26
+ /** The route pattern that matched, e.g. `/users/:id` or `*`. */
27
+ readonly pattern: string;
28
+ /** Dynamic segment values captured from the path. */
29
+ readonly params: Readonly<Record<string, string>>;
30
+ /** Parsed query string (everything after `?`). */
31
+ readonly query: URLSearchParams;
32
+ /** Register a callback to run when navigating away from this route. */
33
+ onCleanup(fn: () => void): void;
34
+ }
35
+ /** Builds a route's page tree. Receives the page scope and the route context. */
36
+ type RouteBuilder = (page: PageDSL, ctx: RouteContext) => void;
37
+ /** A single route: a path pattern and the builder that renders it. */
38
+ interface RouteDefinition {
39
+ /**
40
+ * Path pattern. Supports:
41
+ * - static segments: `/`, `/docs`, `/docs/getting-started`
42
+ * - dynamic `:param`: `/users/:id`
43
+ * - catch-all wildcard: `*` (matches anything — use for a 404 route)
44
+ */
45
+ readonly path: string;
46
+ readonly builder: RouteBuilder;
47
+ }
48
+ /** The result of resolving a location against the route table. */
49
+ interface RouteMatch {
50
+ /** The active pathname (no query string). */
51
+ readonly path: string;
52
+ /** The pattern of the matched route. */
53
+ readonly pattern: string;
54
+ /** Captured dynamic params. */
55
+ readonly params: Readonly<Record<string, string>>;
56
+ /** Parsed query string. */
57
+ readonly query: URLSearchParams;
58
+ /** The route definition that produced this match. */
59
+ readonly route: RouteDefinition;
60
+ /** True when this match came from the wildcard catch-all (`*`) — i.e. a 404. */
61
+ readonly isFallback: boolean;
62
+ }
63
+
64
+ /**
65
+ * Route matching — pure functions, no DOM, no reactivity.
66
+ *
67
+ * A pattern is matched segment-by-segment against a pathname:
68
+ * - a literal segment must equal the path segment,
69
+ * - a `:name` segment captures the path segment into `params.name`,
70
+ * - a `*` segment (or a whole-pattern `*`) is a catch-all that matches the
71
+ * remainder of the path and captures it into `params['*']`.
72
+ *
73
+ * Matching is intentionally small: no optional segments, no regex constraints,
74
+ * no nested route trees. Composition of layouts is done in the DSL, not here.
75
+ */
76
+ /** Normalize a pathname: ensure a single leading slash, drop a trailing slash. */
77
+ declare function normalizePath(path: string): string;
78
+ /**
79
+ * Try to match a single pattern against a pathname.
80
+ * Returns the captured params on success, or `null` on no match.
81
+ */
82
+ declare function matchPattern(pattern: string, pathname: string): Record<string, string> | null;
83
+ interface MatchResult<R> {
84
+ readonly route: R;
85
+ readonly params: Record<string, string>;
86
+ }
87
+ /**
88
+ * Match a pathname against an ordered list of routes. The first route whose
89
+ * pattern matches wins (definition order), so more specific routes should be
90
+ * listed before a `*` fallback.
91
+ */
92
+ declare function matchRoutes<R extends {
93
+ path: string;
94
+ }>(routes: readonly R[], pathname: string): MatchResult<R> | null;
95
+ /** Split a `to` target into its pathname and (already-stripped) search string. */
96
+ declare function splitTarget(to: string): {
97
+ pathname: string;
98
+ search: string;
99
+ };
100
+
101
+ /**
102
+ * Router history — a small abstraction over the navigation source so the router
103
+ * can run both in the browser (real `window.history` + `popstate`) and in tests
104
+ * (an in-memory stack, fully deterministic, no globals).
105
+ *
106
+ * Internal navigation never triggers a full-page reload: the browser history
107
+ * uses `pushState`/`replaceState` and notifies listeners synchronously.
108
+ */
109
+ interface RouterLocation {
110
+ /** Pathname, always normalized with a single leading slash. */
111
+ readonly pathname: string;
112
+ /** Query string without the leading `?`. */
113
+ readonly search: string;
114
+ }
115
+ interface RouterHistory {
116
+ /** The current location. */
117
+ location(): RouterLocation;
118
+ /** Push a new entry and notify listeners. */
119
+ push(pathname: string, search: string): void;
120
+ /** Replace the current entry and notify listeners. */
121
+ replace(pathname: string, search: string): void;
122
+ /** Go back one entry. */
123
+ back(): void;
124
+ /** Go forward one entry. */
125
+ forward(): void;
126
+ /** Subscribe to location changes. Returns an unsubscribe function. */
127
+ listen(cb: () => void): () => void;
128
+ /** Detach any global listeners (browser only). */
129
+ dispose(): void;
130
+ }
131
+ /**
132
+ * Browser history backed by `window.history`. `pushState`/`replaceState` do not
133
+ * emit `popstate`, so we notify listeners ourselves after those calls; genuine
134
+ * back/forward navigation arrives via the `popstate` event.
135
+ */
136
+ declare function createBrowserHistory(): RouterHistory;
137
+ /**
138
+ * In-memory history for tests and non-DOM environments. Maintains an explicit
139
+ * stack and cursor so `back()`/`forward()` are deterministic.
140
+ */
141
+ declare function createMemoryHistory(initial?: string): RouterHistory;
142
+
143
+ /**
144
+ * StreetUI Router core — renderer-agnostic.
145
+ *
146
+ * Holds the route table, resolves the current location into a `RouteMatch`,
147
+ * and exposes that match as a StreetUI `signal`. Navigation is delegated to a
148
+ * `RouterHistory`; when the location changes (via `navigate`, `back`, `forward`,
149
+ * or a browser `popstate`) the router recomputes the match and updates the
150
+ * signal, which is how every consumer (`isActive`, the mount integration, any
151
+ * `derived` the app builds) stays in sync. No second reactive system.
152
+ */
153
+
154
+ interface RouterOptions {
155
+ /** The route table. Order matters — the first matching pattern wins. */
156
+ readonly routes: readonly RouteDefinition[];
157
+ /**
158
+ * Navigation source. Defaults to a browser history. Pass a memory history
159
+ * for tests or non-DOM environments.
160
+ */
161
+ readonly history?: RouterHistory;
162
+ /**
163
+ * Fallback route used when nothing else matches and no `*` route is present.
164
+ * Defaults to a built-in 404 page (a normal StreetUI tree — no special path).
165
+ */
166
+ readonly notFound?: RouteDefinition;
167
+ }
168
+ interface NavigateOptions {
169
+ /** Replace the current history entry instead of pushing a new one. */
170
+ readonly replace?: boolean;
171
+ }
172
+ interface IsActiveOptions {
173
+ /** Require an exact pathname match rather than a prefix match. */
174
+ readonly exact?: boolean;
175
+ }
176
+ interface Router {
177
+ /** Reactive current match. Consumers subscribe via StreetUI signals. */
178
+ readonly currentRoute: ReadonlySignal<RouteMatch>;
179
+ /** Navigate to a target path (may include a query string). */
180
+ navigate(to: string, options?: NavigateOptions): void;
181
+ /** Go back one history entry. */
182
+ back(): void;
183
+ /** Go forward one history entry. */
184
+ forward(): void;
185
+ /** Reactive predicate: is `path` the active route (or a prefix of it)? */
186
+ isActive(path: string, options?: IsActiveOptions): ReadonlySignal<boolean>;
187
+ /** Tear down history listeners. */
188
+ destroy(): void;
189
+ }
190
+ declare function createRouter(options: RouterOptions): Router;
191
+
192
+ /**
193
+ * StreetUI Router — DOM integration.
194
+ *
195
+ * `mountRouter` wires a `Router` to real DOM:
196
+ *
197
+ * 1. Mounts an optional persistent shell (layout + navigation) ONCE. The shell
198
+ * declares an outlet element (see `routerOutlet`) into which route content
199
+ * is rendered.
200
+ * 2. Subscribes to `router.currentRoute`. On every change it disposes the
201
+ * previous route (route-scoped `CleanupRegistry.run()` + `runtime.unmount()`)
202
+ * and mounts the new route's compiled application into the outlet.
203
+ * Only the outlet subtree is re-created — the shell persists.
204
+ * 3. Intercepts clicks on internal `<a>` elements for client-side navigation.
205
+ * External links (absolute URLs, `target="_blank"`, `mailto:`/`tel:`) keep
206
+ * their normal browser behaviour, and `link()` is used unchanged.
207
+ *
208
+ * Each route is an ordinary compiled StreetUI application — no special renderer
209
+ * path, no virtual DOM, no full-application rerender on navigation.
210
+ */
211
+
212
+ /** Default id used for the route outlet element inside a shell. */
213
+ declare const ROUTER_OUTLET_ID = "streetui-router-outlet";
214
+ /**
215
+ * Declare the route outlet inside a shell builder. The router replaces this
216
+ * element's contents on every navigation.
217
+ */
218
+ declare function routerOutlet(scope: ContainerDSL, id?: string): void;
219
+ type ShellBuilder = (shell: PageDSL, router: Router) => void;
220
+ interface MountRouterOptions {
221
+ /** Element to mount into. With a shell, the shell fills this; otherwise routes do. */
222
+ readonly container: Element;
223
+ /** Optional persistent layout. Must include a `routerOutlet(...)`. */
224
+ readonly shell?: ShellBuilder;
225
+ /** Id of the outlet element within the shell. Defaults to `ROUTER_OUTLET_ID`. */
226
+ readonly outletId?: string;
227
+ /** Override the renderer (e.g. a custom DOM adapter for tests). */
228
+ readonly renderer?: StreetRenderer;
229
+ /** Intercept internal `<a>` clicks for client-side navigation. Defaults to true. */
230
+ readonly interceptLinks?: boolean;
231
+ /**
232
+ * Hydrate server-rendered HTML already present in the container instead of
233
+ * mounting fresh. The shell and the *initial* route adopt the existing DOM;
234
+ * subsequent client-side navigations mount normally. Defaults to false.
235
+ */
236
+ readonly hydrate?: boolean;
237
+ }
238
+ interface MountedRouter {
239
+ /** The element route content is rendered into. */
240
+ readonly outlet: Element;
241
+ /** Tear down the current route, the shell, link interception and the router. */
242
+ unmount(): void;
243
+ }
244
+ declare function mountRouter(router: Router, options: MountRouterOptions): MountedRouter;
245
+
246
+ export { type IsActiveOptions, type MatchResult, type MountRouterOptions, type MountedRouter, type NavigateOptions, ROUTER_OUTLET_ID, type RouteBuilder, type RouteContext, type RouteDefinition, type RouteMatch, type Router, type RouterHistory, type RouterLocation, type RouterOptions, type ShellBuilder, createBrowserHistory, createMemoryHistory, createRouter, matchPattern, matchRoutes, mountRouter, normalizePath, routerOutlet, splitTarget };