@xmachines/play-router 1.0.0-beta.1
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/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +3 -0
- package/README.md +436 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/build-tree.ts.html +316 -0
- package/coverage/connect-router.ts.html +505 -0
- package/coverage/coverage-summary.json +15 -0
- package/coverage/crawl-machine.ts.html +385 -0
- package/coverage/create-browser-history.ts.html +556 -0
- package/coverage/create-route-map.ts.html +400 -0
- package/coverage/create-router.ts.html +328 -0
- package/coverage/extract-route.ts.html +322 -0
- package/coverage/extract-routes.ts.html +286 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +296 -0
- package/coverage/index.ts.html +610 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/query.ts.html +307 -0
- package/coverage/router-bridge-base.ts.html +919 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/types.ts.html +787 -0
- package/coverage/validate-routes.ts.html +319 -0
- package/dist/build-tree.d.ts +13 -0
- package/dist/build-tree.d.ts.map +1 -0
- package/dist/build-tree.js +67 -0
- package/dist/build-tree.js.map +1 -0
- package/dist/connect-router.d.ts +56 -0
- package/dist/connect-router.d.ts.map +1 -0
- package/dist/connect-router.js +119 -0
- package/dist/connect-router.js.map +1 -0
- package/dist/crawl-machine.d.ts +74 -0
- package/dist/crawl-machine.d.ts.map +1 -0
- package/dist/crawl-machine.js +95 -0
- package/dist/crawl-machine.js.map +1 -0
- package/dist/create-browser-history.d.ts +68 -0
- package/dist/create-browser-history.d.ts.map +1 -0
- package/dist/create-browser-history.js +94 -0
- package/dist/create-browser-history.js.map +1 -0
- package/dist/create-route-map.d.ts +46 -0
- package/dist/create-route-map.d.ts.map +1 -0
- package/dist/create-route-map.js +73 -0
- package/dist/create-route-map.js.map +1 -0
- package/dist/create-router.d.ts +73 -0
- package/dist/create-router.d.ts.map +1 -0
- package/dist/create-router.js +63 -0
- package/dist/create-router.js.map +1 -0
- package/dist/extract-route.d.ts +25 -0
- package/dist/extract-route.d.ts.map +1 -0
- package/dist/extract-route.js +63 -0
- package/dist/extract-route.js.map +1 -0
- package/dist/extract-routes.d.ts +41 -0
- package/dist/extract-routes.d.ts.map +1 -0
- package/dist/extract-routes.js +61 -0
- package/dist/extract-routes.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -0
- package/dist/query.d.ts +52 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +69 -0
- package/dist/query.js.map +1 -0
- package/dist/router-bridge-base.d.ts +150 -0
- package/dist/router-bridge-base.d.ts.map +1 -0
- package/dist/router-bridge-base.js +240 -0
- package/dist/router-bridge-base.js.map +1 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-routes.d.ts +39 -0
- package/dist/validate-routes.d.ts.map +1 -0
- package/dist/validate-routes.js +65 -0
- package/dist/validate-routes.js.map +1 -0
- package/examples/demo/README.md +127 -0
- package/examples/demo/index.html +41 -0
- package/examples/demo/package.json +27 -0
- package/examples/demo/src/main.ts +28 -0
- package/examples/demo/src/router.ts +37 -0
- package/examples/demo/src/shell.ts +316 -0
- package/examples/demo/test/browser/auth-flow.browser.test.ts +60 -0
- package/examples/demo/test/browser/startup.browser.test.ts +37 -0
- package/examples/demo/test/library-pattern.test.ts +51 -0
- package/examples/demo/tsconfig.json +17 -0
- package/examples/demo/vite.config.ts +7 -0
- package/examples/demo/vitest.browser.config.ts +20 -0
- package/examples/demo/vitest.config.ts +9 -0
- package/examples/shared/dist/auth-machine.d.ts +20 -0
- package/examples/shared/dist/auth-machine.d.ts.map +1 -0
- package/examples/shared/dist/auth-machine.js +212 -0
- package/examples/shared/dist/auth-machine.js.map +1 -0
- package/examples/shared/dist/catalog.d.ts +85 -0
- package/examples/shared/dist/catalog.d.ts.map +1 -0
- package/examples/shared/dist/catalog.js +86 -0
- package/examples/shared/dist/catalog.js.map +1 -0
- package/examples/shared/dist/index.d.ts +4 -0
- package/examples/shared/dist/index.d.ts.map +1 -0
- package/examples/shared/dist/index.js +3 -0
- package/examples/shared/dist/index.js.map +1 -0
- package/examples/shared/package.json +37 -0
- package/examples/shared/src/auth-machine.ts +234 -0
- package/examples/shared/src/catalog.ts +95 -0
- package/examples/shared/src/index.css +3 -0
- package/examples/shared/src/index.ts +3 -0
- package/examples/shared/src/styles/layout.css +413 -0
- package/examples/shared/src/styles/reset.css +42 -0
- package/examples/shared/src/styles/tokens.css +183 -0
- package/examples/shared/tsconfig.json +14 -0
- package/examples/shared/tsconfig.tsbuildinfo +1 -0
- package/package.json +44 -0
- package/src/build-tree.ts +77 -0
- package/src/connect-router.ts +142 -0
- package/src/crawl-machine.ts +100 -0
- package/src/create-browser-history.ts +157 -0
- package/src/create-route-map.ts +105 -0
- package/src/create-router.ts +87 -0
- package/src/extract-route.ts +79 -0
- package/src/extract-routes.ts +67 -0
- package/src/index.ts +175 -0
- package/src/query.ts +74 -0
- package/src/router-bridge-base.ts +279 -0
- package/src/types.ts +234 -0
- package/src/validate-routes.ts +76 -0
- package/test/connect-route-map.test.ts +320 -0
- package/test/crawl-extract.test.js +473 -0
- package/test/create-browser-history.test.ts +123 -0
- package/test/create-router.test.ts +23 -0
- package/test/extract-routes.test.ts +80 -0
- package/test/find-route-by-path-patterns.test.ts +69 -0
- package/test/integration.test.js +438 -0
- package/test/query.test.ts +56 -0
- package/test/router-bridge-base-edge.test.ts +165 -0
- package/test/router-bridge-base.test.ts +119 -0
- package/test/tree-query.test.js +692 -0
- package/test/validation.test.js +158 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +35 -0
package/.oxfmtrc.json
ADDED
package/.oxlintrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# @xmachines/play-router
|
|
2
|
+
|
|
3
|
+
**Route tree extraction from XState v5 state machines with routing patterns**
|
|
4
|
+
|
|
5
|
+
BFS graph crawling and bidirectional route lookup enabling Actor Authority over navigation.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`@xmachines/play-router` extracts route trees from XState state machines by crawling the state graph using breadth-first traversal. It extracts `meta.route` paths from state machines and builds hierarchical route trees with bidirectional state ID ↔ path mapping.
|
|
10
|
+
|
|
11
|
+
It also exports `RouterBridgeBase`, the shared base class used by framework adapters to implement `RouterBridge` with consistent actor↔router synchronization behavior.
|
|
12
|
+
|
|
13
|
+
`RouterBridgeBase` is the policy point; framework adapters are thin ports that implement only framework-specific navigate/subscribe/unsubscribe behavior.
|
|
14
|
+
|
|
15
|
+
Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
|
|
16
|
+
|
|
17
|
+
- **Actor Authority (INV-01):** Routes derive from machine definitions, not external configuration
|
|
18
|
+
|
|
19
|
+
**Routing:** Supports `meta.route` detection, `play.route` event routing, and pattern matching for dynamic parameters.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install xstate@^5.0.0
|
|
25
|
+
npm install @xmachines/play-router
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Peer dependencies:**
|
|
29
|
+
|
|
30
|
+
- `xstate` ^5.0.0 - State machine runtime
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { createMachine } from "xstate";
|
|
36
|
+
import { extractMachineRoutes } from "@xmachines/play-router";
|
|
37
|
+
|
|
38
|
+
// Route pattern (recommended)
|
|
39
|
+
const machine = createMachine({
|
|
40
|
+
id: "app",
|
|
41
|
+
initial: "home",
|
|
42
|
+
states: {
|
|
43
|
+
home: {
|
|
44
|
+
id: "home",
|
|
45
|
+
meta: { route: "/", view: { component: "Home" } },
|
|
46
|
+
},
|
|
47
|
+
dashboard: {
|
|
48
|
+
id: "dashboard",
|
|
49
|
+
meta: { route: "/dashboard", view: { component: "Dashboard" } },
|
|
50
|
+
initial: "overview",
|
|
51
|
+
states: {
|
|
52
|
+
overview: {
|
|
53
|
+
id: "overview",
|
|
54
|
+
meta: { route: "/overview", view: { component: "Overview" } },
|
|
55
|
+
},
|
|
56
|
+
settings: {
|
|
57
|
+
id: "settings",
|
|
58
|
+
meta: { route: "/settings/:section?", view: { component: "Settings" } }, // Optional parameter
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const tree = extractMachineRoutes(machine);
|
|
66
|
+
|
|
67
|
+
// Bidirectional lookup
|
|
68
|
+
console.log(tree.byPath.get("/dashboard/overview")); // RouteNode
|
|
69
|
+
console.log(tree.byId.get("overview")); // RouteNode
|
|
70
|
+
|
|
71
|
+
// Pattern matching for dynamic routes
|
|
72
|
+
const settingsRoute = tree.byPath.get("/settings/profile");
|
|
73
|
+
console.log(settingsRoute?.id); // "settings"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Vanilla Browser Example
|
|
77
|
+
|
|
78
|
+
See `examples/vanilla-demo/` for a complete example using vanilla TypeScript with the browser History API.
|
|
79
|
+
|
|
80
|
+
The demo demonstrates:
|
|
81
|
+
|
|
82
|
+
- **RouteTree extraction** from XState machine meta.route
|
|
83
|
+
- **History API integration** (pushState, popstate)
|
|
84
|
+
- **Bidirectional synchronization** (actor ↔ URL)
|
|
85
|
+
- **Protected route guards** (authentication redirects)
|
|
86
|
+
- **Dynamic route parameters** (/profile/:userId)
|
|
87
|
+
|
|
88
|
+
**Run the demo:**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd packages/play-router/examples/demo
|
|
92
|
+
npm install
|
|
93
|
+
npm run dev
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Open http://localhost:5174/ and explore:
|
|
97
|
+
|
|
98
|
+
1. Login with any username
|
|
99
|
+
2. Navigate between home and profile
|
|
100
|
+
3. Use browser back/forward buttons
|
|
101
|
+
4. Try accessing protected routes directly
|
|
102
|
+
|
|
103
|
+
**Key implementation patterns:**
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Extract routes from machine
|
|
107
|
+
const routeTree = extractMachineRoutes(authMachine);
|
|
108
|
+
const routeMap = createRouteMap(routeTree);
|
|
109
|
+
|
|
110
|
+
// Actor → URL sync
|
|
111
|
+
const watcher = new Signal.subtle.Watcher(() => {
|
|
112
|
+
queueMicrotask(() => {
|
|
113
|
+
watcher.getPending();
|
|
114
|
+
const route = actor.currentRoute.get();
|
|
115
|
+
if (route) {
|
|
116
|
+
window.history.pushState({}, "", route);
|
|
117
|
+
}
|
|
118
|
+
watcher.watch(actor.currentRoute);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// URL → Actor sync (with pattern matching)
|
|
123
|
+
window.addEventListener("popstate", () => {
|
|
124
|
+
const path = window.location.pathname;
|
|
125
|
+
const { to, params } = routeMap.resolve(path);
|
|
126
|
+
if (to) {
|
|
127
|
+
actor.send({ type: "play.route", to, params });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Initial URL handling
|
|
132
|
+
const initialPath = window.location.pathname;
|
|
133
|
+
if (initialPath !== "/") {
|
|
134
|
+
const { to, params } = routeMap.resolve(initialPath);
|
|
135
|
+
if (to) {
|
|
136
|
+
actor.send({ type: "play.route", to, params });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This example shows the core routing concepts without framework dependencies, making it ideal for understanding how @xmachines/play-router integrates with browser History API.
|
|
142
|
+
|
|
143
|
+
## Canonical Watcher Lifecycle
|
|
144
|
+
|
|
145
|
+
Bridge implementations should use one watcher flow:
|
|
146
|
+
|
|
147
|
+
1. `notify`
|
|
148
|
+
2. `queueMicrotask`
|
|
149
|
+
3. `getPending()`
|
|
150
|
+
4. read actor route and sync infrastructure state
|
|
151
|
+
5. re-arm with `watch(...)` or `watch()`
|
|
152
|
+
|
|
153
|
+
Watcher notification is one-shot; re-arm is required.
|
|
154
|
+
|
|
155
|
+
## Bridge Cleanup Contract
|
|
156
|
+
|
|
157
|
+
Bridge teardown must be explicit and deterministic:
|
|
158
|
+
|
|
159
|
+
- `disconnect`/`dispose` must unwatch signal subscriptions and unhook router listeners.
|
|
160
|
+
- Do not rely on GC-only cleanup guidance.
|
|
161
|
+
- Infrastructure remains passive: bridges observe and forward intents, actors decide validity.
|
|
162
|
+
|
|
163
|
+
## API Reference
|
|
164
|
+
|
|
165
|
+
### extractMachineRoutes()
|
|
166
|
+
|
|
167
|
+
Main entry point — crawls state machine, extracts routes, builds tree:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const tree = extractMachineRoutes(machine: AnyStateMachine): RouteTree;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Detection:**
|
|
174
|
+
|
|
175
|
+
- States with `meta.route` in meta object
|
|
176
|
+
|
|
177
|
+
**Returns:** `RouteTree` with:
|
|
178
|
+
|
|
179
|
+
- `routes: RouteNode[]` - Array of route nodes
|
|
180
|
+
- `byPath: Map<string, RouteNode>` - URL path → route node
|
|
181
|
+
- `byId: Map<string, RouteNode>` - State ID → route node
|
|
182
|
+
|
|
183
|
+
**Throws:** Error if routes are invalid (malformed paths, missing state IDs, duplicates)
|
|
184
|
+
|
|
185
|
+
**Example:**
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { extractMachineRoutes } from "@xmachines/play-router";
|
|
189
|
+
|
|
190
|
+
const tree = extractMachineRoutes(authMachine);
|
|
191
|
+
|
|
192
|
+
// Query routes
|
|
193
|
+
const loginRoute = tree.byId.get("login");
|
|
194
|
+
console.log(loginRoute?.path); // "/login"
|
|
195
|
+
|
|
196
|
+
const dashboardRoute = tree.byPath.get("/dashboard");
|
|
197
|
+
console.log(dashboardRoute?.id); // "dashboard"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### crawlMachine()
|
|
201
|
+
|
|
202
|
+
Low-level BFS traversal of state machine graph:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const visits = crawlMachine(machine: AnyStateMachine): StateVisit[];
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Returns:** Array of state visits in breadth-first order with:
|
|
209
|
+
|
|
210
|
+
- `path: string[]` - State path (e.g., ["dashboard", "settings"])
|
|
211
|
+
- `parent: StateNode | null` - Parent state node
|
|
212
|
+
- `node: StateNode` - Current state node
|
|
213
|
+
|
|
214
|
+
**Example:**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { crawlMachine } from "@xmachines/play-router";
|
|
218
|
+
|
|
219
|
+
const visits = crawlMachine(machine);
|
|
220
|
+
visits.forEach((visit) => {
|
|
221
|
+
console.log("State:", visit.path.join("."));
|
|
222
|
+
console.log("Parent:", visit.parent?.id ?? "root");
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Query Utilities
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Get child routes from state
|
|
230
|
+
const children = getNavigableRoutes(tree, "dashboard");
|
|
231
|
+
|
|
232
|
+
// Check if route exists
|
|
233
|
+
const exists = routeExists(tree, "/profile/:userId");
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Complete API:** See [API Documentation](../../docs/api/@xmachines/play-router)
|
|
237
|
+
|
|
238
|
+
## Examples
|
|
239
|
+
|
|
240
|
+
### Route Detection
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { extractMachineRoutes } from "@xmachines/play-router";
|
|
244
|
+
import { createMachine } from "xstate";
|
|
245
|
+
|
|
246
|
+
const machine = createMachine({
|
|
247
|
+
initial: "home",
|
|
248
|
+
states: {
|
|
249
|
+
home: {
|
|
250
|
+
id: "home",
|
|
251
|
+
meta: { route: "/", view: { component: "Home" } },
|
|
252
|
+
},
|
|
253
|
+
profile: {
|
|
254
|
+
id: "profile",
|
|
255
|
+
meta: { route: "/profile/:userId", view: { component: "Profile" } }, // Parameter pattern
|
|
256
|
+
},
|
|
257
|
+
settings: {
|
|
258
|
+
id: "settings",
|
|
259
|
+
meta: { route: "/settings/:section?", view: { component: "Settings" } }, // Optional parameter
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const tree = extractMachineRoutes(machine);
|
|
265
|
+
|
|
266
|
+
// Bidirectional mapping
|
|
267
|
+
const profileById = tree.byId.get("profile");
|
|
268
|
+
console.log(profileById?.path); // "/profile/:userId"
|
|
269
|
+
|
|
270
|
+
const profileByPath = tree.byPath.get("/profile/user123");
|
|
271
|
+
console.log(profileByPath?.id); // "profile"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Hierarchical Route Tree
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { extractMachineRoutes, getNavigableRoutes } from "@xmachines/play-router";
|
|
278
|
+
|
|
279
|
+
const machine = createMachine({
|
|
280
|
+
initial: "app",
|
|
281
|
+
states: {
|
|
282
|
+
app: {
|
|
283
|
+
id: "app",
|
|
284
|
+
meta: { route: "/", view: { component: "AppShell" } },
|
|
285
|
+
initial: "dashboard",
|
|
286
|
+
states: {
|
|
287
|
+
dashboard: {
|
|
288
|
+
id: "dashboard",
|
|
289
|
+
meta: { route: "/dashboard", view: { component: "Dashboard" } },
|
|
290
|
+
initial: "overview",
|
|
291
|
+
states: {
|
|
292
|
+
overview: {
|
|
293
|
+
id: "overview",
|
|
294
|
+
meta: { route: "/overview", view: { component: "Overview" } },
|
|
295
|
+
},
|
|
296
|
+
analytics: {
|
|
297
|
+
id: "analytics",
|
|
298
|
+
meta: { route: "/analytics", view: { component: "Analytics" } },
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const tree = extractMachineRoutes(machine);
|
|
308
|
+
|
|
309
|
+
// Get child routes
|
|
310
|
+
const dashboardChildren = getNavigableRoutes(tree, "dashboard");
|
|
311
|
+
console.log(dashboardChildren.map((r) => r.id)); // ["overview", "analytics"]
|
|
312
|
+
|
|
313
|
+
// Route inheritance
|
|
314
|
+
const analyticsRoute = tree.byId.get("analytics");
|
|
315
|
+
console.log(analyticsRoute?.path); // "/dashboard/analytics" (inherited parent path)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Pattern Matching
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { extractMachineRoutes } from "@xmachines/play-router";
|
|
322
|
+
|
|
323
|
+
const machine = createMachine({
|
|
324
|
+
states: {
|
|
325
|
+
user: {
|
|
326
|
+
id: "user",
|
|
327
|
+
meta: { route: "/user/:userId", view: { component: "User" } }, // Required parameter
|
|
328
|
+
},
|
|
329
|
+
settings: {
|
|
330
|
+
id: "settings",
|
|
331
|
+
meta: { route: "/settings/:section?", view: { component: "Settings" } }, // Optional parameter
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const tree = extractMachineRoutes(machine);
|
|
337
|
+
|
|
338
|
+
// Pattern matching for actual URLs
|
|
339
|
+
const userRoute = tree.byPath.get("/user/user123");
|
|
340
|
+
console.log(userRoute?.id); // "user"
|
|
341
|
+
|
|
342
|
+
const settingsDefault = tree.byPath.get("/settings");
|
|
343
|
+
console.log(settingsDefault?.id); // "settings" (optional param)
|
|
344
|
+
|
|
345
|
+
const settingsProfile = tree.byPath.get("/settings/profile");
|
|
346
|
+
console.log(settingsProfile?.id); // "settings" (with param)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Route Configuration
|
|
350
|
+
|
|
351
|
+
### Route Pattern (Recommended)
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
states: {
|
|
355
|
+
dashboard: {
|
|
356
|
+
id: "dashboard", // Required for bidirectional lookup
|
|
357
|
+
meta: {
|
|
358
|
+
route: "/dashboard", // URL path - marks state as routable
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Alternative Pattern
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
states: {
|
|
368
|
+
dashboard: {
|
|
369
|
+
id: "dashboard",
|
|
370
|
+
meta: {
|
|
371
|
+
route: "/dashboard",
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Route Inheritance
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
states: {
|
|
381
|
+
parent: {
|
|
382
|
+
id: "parent",
|
|
383
|
+
meta: { route: "/parent", view: { component: "Parent" } },
|
|
384
|
+
states: {
|
|
385
|
+
absolute: {
|
|
386
|
+
id: "absolute",
|
|
387
|
+
meta: { route: "/absolute", view: { component: "Absolute" } }, // Starts with / → doesn't inherit
|
|
388
|
+
},
|
|
389
|
+
relative: {
|
|
390
|
+
id: "relative",
|
|
391
|
+
meta: { route: "relative", view: { component: "Relative" } }, // No leading / → inherits parent
|
|
392
|
+
// Final path: "/parent/relative"
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Dynamic Parameters
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
meta: {
|
|
403
|
+
route: "/profile/:userId", // Required parameter
|
|
404
|
+
route: "/settings/:section?", // Optional parameter
|
|
405
|
+
route: "/docs/:category/:page", // Multiple parameters
|
|
406
|
+
view: { component: "AnyView" },
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Parameter substitution:** Values extracted from context or event params (handled by play-xstate adapter).
|
|
411
|
+
|
|
412
|
+
## Architecture
|
|
413
|
+
|
|
414
|
+
This package enables **Actor Authority (INV-01)**:
|
|
415
|
+
|
|
416
|
+
1. **Routes derive from machine:** Business logic defines routes in state machine, not external config
|
|
417
|
+
2. **BFS traversal:** Systematic state discovery ensures all nested states visited
|
|
418
|
+
3. **Bidirectional mapping:** Fast lookup by path (browser URL) or by ID (state machine)
|
|
419
|
+
4. **Build-time validation:** Invalid routes throw errors during extraction, not runtime
|
|
420
|
+
|
|
421
|
+
**Enhancements:**
|
|
422
|
+
|
|
423
|
+
- `meta.route` detection via state metadata
|
|
424
|
+
- Pattern matching for dynamic routes (`:param` and `:param?`)
|
|
425
|
+
- State ID ↔ path bidirectional maps for `play.route` events
|
|
426
|
+
|
|
427
|
+
## Related Packages
|
|
428
|
+
|
|
429
|
+
- **[@xmachines/play-xstate](../play-xstate)** - XState adapter using route extraction
|
|
430
|
+
- **[@xmachines/play-tanstack-react-router](../play-tanstack-react-router)** - TanStack Router adapter using route trees
|
|
431
|
+
- **[@xmachines/play-react-router](../play-react-router)** - React Router v7 adapter using RouterBridgeBase
|
|
432
|
+
- **[@xmachines/play](../play)** - Protocol types
|
|
433
|
+
|
|
434
|
+
## License
|
|
435
|
+
|
|
436
|
+
MIT
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
body, html {
|
|
2
|
+
margin:0; padding: 0;
|
|
3
|
+
height: 100%;
|
|
4
|
+
}
|
|
5
|
+
body {
|
|
6
|
+
font-family: Helvetica Neue, Helvetica, Arial;
|
|
7
|
+
font-size: 14px;
|
|
8
|
+
color:#333;
|
|
9
|
+
}
|
|
10
|
+
.small { font-size: 12px; }
|
|
11
|
+
*, *:after, *:before {
|
|
12
|
+
-webkit-box-sizing:border-box;
|
|
13
|
+
-moz-box-sizing:border-box;
|
|
14
|
+
box-sizing:border-box;
|
|
15
|
+
}
|
|
16
|
+
h1 { font-size: 20px; margin: 0;}
|
|
17
|
+
h2 { font-size: 14px; }
|
|
18
|
+
pre {
|
|
19
|
+
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
-moz-tab-size: 2;
|
|
23
|
+
-o-tab-size: 2;
|
|
24
|
+
tab-size: 2;
|
|
25
|
+
}
|
|
26
|
+
a { color:#0074D9; text-decoration:none; }
|
|
27
|
+
a:hover { text-decoration:underline; }
|
|
28
|
+
.strong { font-weight: bold; }
|
|
29
|
+
.space-top1 { padding: 10px 0 0 0; }
|
|
30
|
+
.pad2y { padding: 20px 0; }
|
|
31
|
+
.pad1y { padding: 10px 0; }
|
|
32
|
+
.pad2x { padding: 0 20px; }
|
|
33
|
+
.pad2 { padding: 20px; }
|
|
34
|
+
.pad1 { padding: 10px; }
|
|
35
|
+
.space-left2 { padding-left:55px; }
|
|
36
|
+
.space-right2 { padding-right:20px; }
|
|
37
|
+
.center { text-align:center; }
|
|
38
|
+
.clearfix { display:block; }
|
|
39
|
+
.clearfix:after {
|
|
40
|
+
content:'';
|
|
41
|
+
display:block;
|
|
42
|
+
height:0;
|
|
43
|
+
clear:both;
|
|
44
|
+
visibility:hidden;
|
|
45
|
+
}
|
|
46
|
+
.fl { float: left; }
|
|
47
|
+
@media only screen and (max-width:640px) {
|
|
48
|
+
.col3 { width:100%; max-width:100%; }
|
|
49
|
+
.hide-mobile { display:none!important; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.quiet {
|
|
53
|
+
color: #7f7f7f;
|
|
54
|
+
color: rgba(0,0,0,0.5);
|
|
55
|
+
}
|
|
56
|
+
.quiet a { opacity: 0.7; }
|
|
57
|
+
|
|
58
|
+
.fraction {
|
|
59
|
+
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
60
|
+
font-size: 10px;
|
|
61
|
+
color: #555;
|
|
62
|
+
background: #E8E8E8;
|
|
63
|
+
padding: 4px 5px;
|
|
64
|
+
border-radius: 3px;
|
|
65
|
+
vertical-align: middle;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
div.path a:link, div.path a:visited { color: #333; }
|
|
69
|
+
table.coverage {
|
|
70
|
+
border-collapse: collapse;
|
|
71
|
+
margin: 10px 0 0 0;
|
|
72
|
+
padding: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
table.coverage td {
|
|
76
|
+
margin: 0;
|
|
77
|
+
padding: 0;
|
|
78
|
+
vertical-align: top;
|
|
79
|
+
}
|
|
80
|
+
table.coverage td.line-count {
|
|
81
|
+
text-align: right;
|
|
82
|
+
padding: 0 5px 0 20px;
|
|
83
|
+
}
|
|
84
|
+
table.coverage td.line-coverage {
|
|
85
|
+
text-align: right;
|
|
86
|
+
padding-right: 10px;
|
|
87
|
+
min-width:20px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
table.coverage td span.cline-any {
|
|
91
|
+
display: inline-block;
|
|
92
|
+
padding: 0 5px;
|
|
93
|
+
width: 100%;
|
|
94
|
+
}
|
|
95
|
+
.missing-if-branch {
|
|
96
|
+
display: inline-block;
|
|
97
|
+
margin-right: 5px;
|
|
98
|
+
border-radius: 3px;
|
|
99
|
+
position: relative;
|
|
100
|
+
padding: 0 4px;
|
|
101
|
+
background: #333;
|
|
102
|
+
color: yellow;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.skip-if-branch {
|
|
106
|
+
display: none;
|
|
107
|
+
margin-right: 10px;
|
|
108
|
+
position: relative;
|
|
109
|
+
padding: 0 4px;
|
|
110
|
+
background: #ccc;
|
|
111
|
+
color: white;
|
|
112
|
+
}
|
|
113
|
+
.missing-if-branch .typ, .skip-if-branch .typ {
|
|
114
|
+
color: inherit !important;
|
|
115
|
+
}
|
|
116
|
+
.coverage-summary {
|
|
117
|
+
border-collapse: collapse;
|
|
118
|
+
width: 100%;
|
|
119
|
+
}
|
|
120
|
+
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
|
121
|
+
.keyline-all { border: 1px solid #ddd; }
|
|
122
|
+
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
|
123
|
+
.coverage-summary tbody { border: 1px solid #bbb; }
|
|
124
|
+
.coverage-summary td { border-right: 1px solid #bbb; }
|
|
125
|
+
.coverage-summary td:last-child { border-right: none; }
|
|
126
|
+
.coverage-summary th {
|
|
127
|
+
text-align: left;
|
|
128
|
+
font-weight: normal;
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
}
|
|
131
|
+
.coverage-summary th.file { border-right: none !important; }
|
|
132
|
+
.coverage-summary th.pct { }
|
|
133
|
+
.coverage-summary th.pic,
|
|
134
|
+
.coverage-summary th.abs,
|
|
135
|
+
.coverage-summary td.pct,
|
|
136
|
+
.coverage-summary td.abs { text-align: right; }
|
|
137
|
+
.coverage-summary td.file { white-space: nowrap; }
|
|
138
|
+
.coverage-summary td.pic { min-width: 120px !important; }
|
|
139
|
+
.coverage-summary tfoot td { }
|
|
140
|
+
|
|
141
|
+
.coverage-summary .sorter {
|
|
142
|
+
height: 10px;
|
|
143
|
+
width: 7px;
|
|
144
|
+
display: inline-block;
|
|
145
|
+
margin-left: 0.5em;
|
|
146
|
+
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
|
147
|
+
}
|
|
148
|
+
.coverage-summary .sorted .sorter {
|
|
149
|
+
background-position: 0 -20px;
|
|
150
|
+
}
|
|
151
|
+
.coverage-summary .sorted-desc .sorter {
|
|
152
|
+
background-position: 0 -10px;
|
|
153
|
+
}
|
|
154
|
+
.status-line { height: 10px; }
|
|
155
|
+
/* yellow */
|
|
156
|
+
.cbranch-no { background: yellow !important; color: #111; }
|
|
157
|
+
/* dark red */
|
|
158
|
+
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
|
159
|
+
.low .chart { border:1px solid #C21F39 }
|
|
160
|
+
.highlighted,
|
|
161
|
+
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
|
162
|
+
background: #C21F39 !important;
|
|
163
|
+
}
|
|
164
|
+
/* medium red */
|
|
165
|
+
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
|
166
|
+
/* light red */
|
|
167
|
+
.low, .cline-no { background:#FCE1E5 }
|
|
168
|
+
/* light green */
|
|
169
|
+
.high, .cline-yes { background:rgb(230,245,208) }
|
|
170
|
+
/* medium green */
|
|
171
|
+
.cstat-yes { background:rgb(161,215,106) }
|
|
172
|
+
/* dark green */
|
|
173
|
+
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
|
174
|
+
.high .chart { border:1px solid rgb(77,146,33) }
|
|
175
|
+
/* dark yellow (gold) */
|
|
176
|
+
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
|
177
|
+
.medium .chart { border:1px solid #f9cd0b; }
|
|
178
|
+
/* light yellow */
|
|
179
|
+
.medium { background: #fff4c2; }
|
|
180
|
+
|
|
181
|
+
.cstat-skip { background: #ddd; color: #111; }
|
|
182
|
+
.fstat-skip { background: #ddd; color: #111 !important; }
|
|
183
|
+
.cbranch-skip { background: #ddd !important; color: #111; }
|
|
184
|
+
|
|
185
|
+
span.cline-neutral { background: #eaeaea; }
|
|
186
|
+
|
|
187
|
+
.coverage-summary td.empty {
|
|
188
|
+
opacity: .5;
|
|
189
|
+
padding-top: 4px;
|
|
190
|
+
padding-bottom: 4px;
|
|
191
|
+
line-height: 1;
|
|
192
|
+
color: #888;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.cover-fill, .cover-empty {
|
|
196
|
+
display:inline-block;
|
|
197
|
+
height: 12px;
|
|
198
|
+
}
|
|
199
|
+
.chart {
|
|
200
|
+
line-height: 0;
|
|
201
|
+
}
|
|
202
|
+
.cover-empty {
|
|
203
|
+
background: white;
|
|
204
|
+
}
|
|
205
|
+
.cover-full {
|
|
206
|
+
border-right: none !important;
|
|
207
|
+
}
|
|
208
|
+
pre.prettyprint {
|
|
209
|
+
border: none !important;
|
|
210
|
+
padding: 0 !important;
|
|
211
|
+
margin: 0 !important;
|
|
212
|
+
}
|
|
213
|
+
.com { color: #999 !important; }
|
|
214
|
+
.ignore-none { color: #999; font-weight: normal; }
|
|
215
|
+
|
|
216
|
+
.wrapper {
|
|
217
|
+
min-height: 100%;
|
|
218
|
+
height: auto !important;
|
|
219
|
+
height: 100%;
|
|
220
|
+
margin: 0 auto -48px;
|
|
221
|
+
}
|
|
222
|
+
.footer, .push {
|
|
223
|
+
height: 48px;
|
|
224
|
+
}
|