@xmachines/play-tanstack-solid-router 1.0.0-beta.7 → 1.0.0-beta.9
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/README.md +130 -295
- package/dist/route-map.d.ts +22 -70
- package/dist/route-map.d.ts.map +1 -1
- package/dist/route-map.js +22 -102
- package/dist/route-map.js.map +1 -1
- package/dist/types.d.ts +15 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -10
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Signals-native integration with TanStack Solid Router enabling logic-driven navi
|
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
`@xmachines/play-tanstack-solid-router` provides seamless integration between TanStack Solid Router and XMachines state machines. Built on Solid's reactive primitives
|
|
9
|
+
`@xmachines/play-tanstack-solid-router` provides seamless integration between TanStack Solid Router and XMachines state machines. Built on Solid's reactive primitives, it implements the `RouterBridgeBase` pattern for bidirectional synchronization while remaining framework-swappable.
|
|
10
10
|
|
|
11
11
|
Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
|
|
12
12
|
|
|
@@ -17,26 +17,26 @@ Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md),
|
|
|
17
17
|
**Key Benefits:**
|
|
18
18
|
|
|
19
19
|
- **Signals-native:** Zero adaptation layer between Solid signals and TC39 Signals
|
|
20
|
-
- **
|
|
21
|
-
- **Automatic tracking:** `createEffect` handles dependency tracking (no manual Watcher)
|
|
20
|
+
- **Bridge-first:** Extends shared `RouterBridgeBase` policy used by other adapters
|
|
21
|
+
- **Automatic tracking:** `createEffect` handles dependency tracking (no manual Watcher setup needed for the bridge)
|
|
22
22
|
- **Logic-driven navigation:** Business logic in state machines, not components
|
|
23
23
|
- **Type-safe parameters:** Route params flow through state machine context
|
|
24
24
|
|
|
25
25
|
**Framework Compatibility:**
|
|
26
26
|
|
|
27
|
-
- TanStack Solid Router 1.
|
|
27
|
+
- TanStack Solid Router 1.100.0+
|
|
28
28
|
- SolidJS 1.8.0+
|
|
29
29
|
- TC39 Signals polyfill integration
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
npm install @tanstack/solid-router@^1.
|
|
34
|
+
npm install @tanstack/solid-router@^1.108.0 solid-js@^1.8.0 @xmachines/play-tanstack-solid-router @xmachines/play-solid
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
**Peer dependencies:**
|
|
38
38
|
|
|
39
|
-
- `@tanstack/solid-router` ^1.
|
|
39
|
+
- `@tanstack/solid-router` ^1.108.0 - TanStack Solid Router library
|
|
40
40
|
- `solid-js` ^1.8.0 - SolidJS runtime
|
|
41
41
|
- `@xmachines/play-solid` - Solid renderer (`PlayRenderer`)
|
|
42
42
|
- `@xmachines/play-actor` - Actor base
|
|
@@ -45,377 +45,212 @@ npm install @tanstack/solid-router@^1.0.0 solid-js@^1.8.0 @xmachines/play-tansta
|
|
|
45
45
|
|
|
46
46
|
## Quick Start
|
|
47
47
|
|
|
48
|
-
```
|
|
49
|
-
import { createRouter } from
|
|
50
|
-
import {
|
|
51
|
-
import { PlayRenderer } from
|
|
52
|
-
import { definePlayer } from
|
|
53
|
-
import {
|
|
54
|
-
import { SolidRouterBridge, createRouteMapFromTree } from '@xmachines/play-tanstack-solid-router';
|
|
48
|
+
```tsx
|
|
49
|
+
import { Router, createRouter } from "@tanstack/solid-router";
|
|
50
|
+
import { PlayTanStackRouterProvider, createRouteMap } from "@xmachines/play-tanstack-solid-router";
|
|
51
|
+
import { PlayRenderer } from "@xmachines/play-solid";
|
|
52
|
+
import { definePlayer } from "@xmachines/play-xstate";
|
|
53
|
+
import { routeTree as routerRouteTree } from "./routeTree.gen"; // from TanStack
|
|
55
54
|
|
|
56
55
|
function App() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
56
|
+
// 1. Create player with state machine
|
|
57
|
+
const createPlayer = definePlayer({
|
|
58
|
+
machine: authMachine,
|
|
59
|
+
catalog: componentCatalog,
|
|
60
|
+
});
|
|
61
|
+
const actor = createPlayer();
|
|
62
|
+
actor.start();
|
|
63
|
+
|
|
64
|
+
// 2. Create TanStack router instance
|
|
65
|
+
const router = createRouter({ routeTree: routerRouteTree });
|
|
66
|
+
|
|
67
|
+
// 3. Create route mapping from machine routes
|
|
68
|
+
const routeMap = createRouteMap(authMachine);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
// 4. Wrap with provider to sync actor and router
|
|
72
|
+
<PlayTanStackRouterProvider
|
|
73
|
+
actor={actor}
|
|
74
|
+
router={router}
|
|
75
|
+
routeMap={routeMap}
|
|
76
|
+
renderer={(currentActor, currentRouter) => (
|
|
77
|
+
<Router router={currentRouter}>
|
|
78
|
+
<PlayRenderer actor={currentActor} components={components} />
|
|
79
|
+
</Router>
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
80
83
|
}
|
|
81
84
|
```
|
|
82
85
|
|
|
83
|
-
**Important:** The bridge must be created inside a component where Solid hooks (`createEffect`, `onCleanup`) are available. TanStack Solid Router requires Solid's ownership context for reactivity to work correctly.
|
|
84
|
-
|
|
85
86
|
## API Reference
|
|
86
87
|
|
|
87
88
|
### `SolidRouterBridge`
|
|
88
89
|
|
|
89
90
|
Router adapter implementing the `RouterBridge` protocol for TanStack Solid Router.
|
|
90
91
|
|
|
91
|
-
**
|
|
92
|
+
**Type Signature:**
|
|
92
93
|
|
|
93
94
|
```typescript
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
95
|
+
class SolidRouterBridge {
|
|
96
|
+
constructor(router: Router, actor: AbstractActor<any>, routeMap: RouteMap);
|
|
97
|
+
dispose(): void;
|
|
98
|
+
}
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
**Parameters:**
|
|
101
|
+
**Constructor Parameters:**
|
|
102
102
|
|
|
103
|
-
- `router
|
|
104
|
-
- `actor
|
|
105
|
-
- `routeMap
|
|
103
|
+
- `router` - TanStack Solid Router instance
|
|
104
|
+
- `actor` - XMachines actor instance
|
|
105
|
+
- `routeMap` - Bidirectional state ID ↔ path mapping
|
|
106
106
|
|
|
107
107
|
**Methods:**
|
|
108
108
|
|
|
109
|
-
- `connect()
|
|
110
|
-
- `disconnect()
|
|
109
|
+
- `connect()` - Start bidirectional synchronization.
|
|
110
|
+
- `disconnect()` - Stop synchronization and cleanup bridge resources.
|
|
111
|
+
- `dispose()` - Alias of `disconnect()`.
|
|
111
112
|
|
|
112
|
-
**
|
|
113
|
+
**Internal Behavior:**
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
- Uses `RouterBridgeBase` TC39 watcher lifecycle for actor→router synchronization
|
|
116
|
+
- Updates TanStack Router via `router.navigate({ to: path })` when actor state changes
|
|
117
|
+
- Uses `router.subscribe` to watch history navigation events
|
|
118
|
+
- Sends `play.route` events to actor when user navigates
|
|
115
119
|
|
|
116
|
-
|
|
120
|
+
### `PlayTanStackRouterProvider`
|
|
117
121
|
|
|
118
|
-
|
|
122
|
+
A Solid component that automatically sets up, connects, and tears down the `SolidRouterBridge` using Solid's lifecycle.
|
|
119
123
|
|
|
120
|
-
```
|
|
121
|
-
|
|
124
|
+
```tsx
|
|
125
|
+
interface PlayTanStackRouterProviderProps {
|
|
126
|
+
actor: AbstractActor<any>;
|
|
127
|
+
router: Router;
|
|
128
|
+
routeMap: RouteMap;
|
|
129
|
+
renderer: (actor: AbstractActor<any>, router: Router) => JSX.Element;
|
|
130
|
+
}
|
|
122
131
|
```
|
|
123
132
|
|
|
124
|
-
**
|
|
133
|
+
**Props:**
|
|
125
134
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
```
|
|
135
|
+
- `actor` - The XMachines player actor
|
|
136
|
+
- `router` - The TanStack Solid Router instance
|
|
137
|
+
- `routeMap` - Mapping between paths and state IDs
|
|
138
|
+
- `renderer` - A render prop function that receives the active actor and router
|
|
132
139
|
|
|
133
|
-
**
|
|
140
|
+
**Behavior:**
|
|
134
141
|
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
1. Instantiates `SolidRouterBridge` on mount
|
|
143
|
+
2. Calls `bridge.connect()`
|
|
144
|
+
3. Renders the content returned by the `renderer` function
|
|
145
|
+
4. Calls `bridge.disconnect()` when the component unmounts via `onCleanup`
|
|
137
146
|
|
|
138
|
-
### `
|
|
147
|
+
### `createRouteMap()`
|
|
139
148
|
|
|
140
|
-
Helper to
|
|
149
|
+
Helper to build a `RouteMap` instance directly from an XState machine.
|
|
141
150
|
|
|
142
151
|
**Signature:**
|
|
143
152
|
|
|
144
153
|
```typescript
|
|
145
|
-
function
|
|
154
|
+
function createRouteMap(machine: AnyStateMachine): RouteMap;
|
|
146
155
|
```
|
|
147
156
|
|
|
148
157
|
**Usage:**
|
|
149
158
|
|
|
150
159
|
```typescript
|
|
151
|
-
import {
|
|
152
|
-
import { createRouteMapFromTree } from "@xmachines/play-tanstack-solid-router";
|
|
160
|
+
import { createRouteMap } from "@xmachines/play-tanstack-solid-router";
|
|
153
161
|
|
|
154
|
-
const
|
|
155
|
-
const routeMap = createRouteMapFromTree(routeTree);
|
|
162
|
+
const routeMap = createRouteMap(machine);
|
|
156
163
|
```
|
|
157
164
|
|
|
158
|
-
This helper traverses the route tree and creates RouteMapping entries for all routable states.
|
|
159
|
-
|
|
160
165
|
## Usage Patterns
|
|
161
166
|
|
|
162
|
-
### Creating RouteMap from Machine
|
|
163
|
-
|
|
164
|
-
**Recommended:** Extract routes from state machine automatically:
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
import { extractMachineRoutes } from "@xmachines/play-router";
|
|
168
|
-
import { createRouteMapFromTree } from "@xmachines/play-tanstack-solid-router";
|
|
169
|
-
|
|
170
|
-
const routeTree = extractMachineRoutes(machine);
|
|
171
|
-
const routeMap = createRouteMapFromTree(routeTree);
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
**Manual:** Define routes explicitly:
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
const routeMap = new RouteMap([
|
|
178
|
-
{ stateId: "#home", path: "/" },
|
|
179
|
-
{ stateId: "#profile", path: "/profile/:userId" },
|
|
180
|
-
]);
|
|
181
|
-
```
|
|
182
|
-
|
|
183
167
|
### Dynamic Routes with Parameters
|
|
184
168
|
|
|
185
|
-
|
|
169
|
+
TanStack Router and URLPattern (used internally) support dynamic route matching syntax:
|
|
186
170
|
|
|
187
171
|
```typescript
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
172
|
+
// Machine configuration
|
|
173
|
+
const machineConfig = {
|
|
174
|
+
states: {
|
|
175
|
+
profile: {
|
|
176
|
+
meta: {
|
|
177
|
+
route: "/profile/:userId",
|
|
178
|
+
view: { component: "Profile" },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
196
183
|
|
|
197
|
-
|
|
184
|
+
// Route mapping will natively support URLPattern parameters
|
|
198
185
|
routeMap.getStateIdByPath("/profile/123"); // → '#profile'
|
|
199
|
-
routeMap.getStateIdByPath("/settings"); // → '#settings'
|
|
200
|
-
routeMap.getStateIdByPath("/settings/privacy"); // → '#settings'
|
|
201
186
|
```
|
|
202
187
|
|
|
203
|
-
###
|
|
188
|
+
### Protected Routes and Guards
|
|
204
189
|
|
|
205
|
-
|
|
190
|
+
With XMachines, auth guards are handled entirely inside the state machine, preventing flashes of unauthorized content.
|
|
206
191
|
|
|
207
192
|
```typescript
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
return <Router>...</Router>;
|
|
193
|
+
// Machine side
|
|
194
|
+
dashboard: {
|
|
195
|
+
meta: { route: "/dashboard", view: { component: "Dashboard" } },
|
|
196
|
+
always: {
|
|
197
|
+
guard: ({ context }) => !context.isAuthenticated,
|
|
198
|
+
target: "login"
|
|
199
|
+
}
|
|
218
200
|
}
|
|
219
201
|
```
|
|
220
202
|
|
|
221
|
-
|
|
203
|
+
When a user navigates to `/dashboard`:
|
|
222
204
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
**How it works:**
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
// Actor state change → Router navigation
|
|
232
|
-
private syncRouterFromActor(route: string | null): void {
|
|
233
|
-
if (this.isProcessingNavigation) return; // Skip if processing router event
|
|
234
|
-
if (route === this.lastSyncedPath) return; // Skip if path unchanged
|
|
235
|
-
|
|
236
|
-
this.lastSyncedPath = route || '/';
|
|
237
|
-
this.router.navigate({ to: this.lastSyncedPath });
|
|
238
|
-
}
|
|
205
|
+
1. TanStack Router updates location
|
|
206
|
+
2. Bridge intercepts and sends `play.route`
|
|
207
|
+
3. Actor evaluates guard -> denies target, transitions to `login` instead
|
|
208
|
+
4. Bridge observes new actor state (`/login`)
|
|
209
|
+
5. Bridge tells TanStack Router to redirect to `/login`
|
|
239
210
|
|
|
240
|
-
|
|
241
|
-
private syncActorFromRouter(location: RouterLocation): void {
|
|
242
|
-
this.isProcessingNavigation = true; // Set flag
|
|
243
|
-
|
|
244
|
-
// Send event to actor...
|
|
211
|
+
## Circular Update Prevention
|
|
245
212
|
|
|
246
|
-
|
|
247
|
-
this.isProcessingNavigation = false; // Clear flag in microtask
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
```
|
|
213
|
+
The `RouterBridgeBase` architecture prevents infinite loops between router and actor using two mechanisms:
|
|
251
214
|
|
|
252
|
-
|
|
215
|
+
1. **`lastSyncedPath` tracking:** Stores the last synchronized path to prevent redundant navigations back to the identical location.
|
|
216
|
+
2. **`isProcessingNavigation` flag:** Set during a router event, preventing the router from immediately reacting to the actor's synchronous state update.
|
|
253
217
|
|
|
254
218
|
## Comparison with @solidjs/router Adapter
|
|
255
219
|
|
|
256
|
-
| Aspect
|
|
257
|
-
|
|
|
258
|
-
| **Router API**
|
|
259
|
-
| **
|
|
260
|
-
| **
|
|
261
|
-
| **
|
|
262
|
-
|
|
263
|
-
**Key Differences:**
|
|
264
|
-
|
|
265
|
-
- **TanStack Solid Router:** Uses `router.navigate({ to })` API (object-based navigation)
|
|
266
|
-
- **SolidJS Router:** Uses `navigate(path)` function (string-based navigation)
|
|
267
|
-
- **TanStack:** Router instance contains state, no separate hooks needed
|
|
268
|
-
- **SolidJS:** Requires hooks (`useNavigate`, `useLocation`) for navigation primitives
|
|
269
|
-
|
|
270
|
-
## Limitations
|
|
271
|
-
|
|
272
|
-
### Solid Ownership Context Required
|
|
273
|
-
|
|
274
|
-
The bridge must be created inside a component where Solid hooks are available:
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
// ✅ CORRECT: Inside component
|
|
278
|
-
function App() {
|
|
279
|
-
const bridge = new SolidRouterBridge(router, actor, routeMap);
|
|
280
|
-
bridge.connect();
|
|
281
|
-
onCleanup(() => bridge.disconnect());
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ❌ WRONG: Outside component (no ownership context)
|
|
285
|
-
const bridge = new SolidRouterBridge(router, actor, routeMap);
|
|
286
|
-
bridge.connect(); // Will fail - no owner for createEffect
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
**Error message:**
|
|
290
|
-
|
|
291
|
-
```
|
|
292
|
-
Error: createEffect can only be used within a component
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
### Router Creation
|
|
296
|
-
|
|
297
|
-
Router must be created with TanStack's `createRouter()`:
|
|
298
|
-
|
|
299
|
-
```typescript
|
|
300
|
-
import { createRouter } from "@tanstack/solid-router";
|
|
301
|
-
|
|
302
|
-
const router = createRouter({
|
|
303
|
-
routes: [
|
|
304
|
-
/* route config */
|
|
305
|
-
],
|
|
306
|
-
});
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Pattern Matching Requirements
|
|
310
|
-
|
|
311
|
-
Route patterns with parameters require URLPattern support:
|
|
312
|
-
|
|
313
|
-
- **Modern browsers:** Safari 17.4+, Chrome 95+, Firefox 106+
|
|
314
|
-
- **Node.js:** Requires `urlpattern-polyfill` for tests
|
|
315
|
-
|
|
316
|
-
**Polyfill setup:**
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
// Top of app entry point
|
|
320
|
-
if (!globalThis.URLPattern) {
|
|
321
|
-
await import("urlpattern-polyfill");
|
|
322
|
-
}
|
|
323
|
-
```
|
|
220
|
+
| Aspect | @solidjs/router Adapter | @tanstack/solid-router Adapter |
|
|
221
|
+
| ----------------- | -------------------------------- | ------------------------------- |
|
|
222
|
+
| **Router API** | `navigate(path)` | `router.navigate({ to })` |
|
|
223
|
+
| **Setup Context** | Must be _inside_ Router context | Can wrap Router instance |
|
|
224
|
+
| **State Source** | Uses Solid hooks (`useLocation`) | Subscribes to `router` directly |
|
|
225
|
+
| **Reacting** | `createEffect` on location | `router.subscribe` callback |
|
|
324
226
|
|
|
325
227
|
## Architecture
|
|
326
228
|
|
|
327
|
-
|
|
229
|
+
This package implements standard Play invariants:
|
|
328
230
|
|
|
329
231
|
### INV-01: Actor Authority
|
|
330
232
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
**Implementation:** Router navigation triggers actor events, but actor guards decide validity:
|
|
334
|
-
|
|
335
|
-
```typescript
|
|
336
|
-
// Router navigation → Actor event
|
|
337
|
-
syncActorFromRouter(location: RouterLocation): void {
|
|
338
|
-
const stateId = this.routeMap.getStateIdByPath(location.pathname);
|
|
339
|
-
this.actor.send({
|
|
340
|
-
type: 'play.route',
|
|
341
|
-
to: stateId,
|
|
342
|
-
params: extractParams(location)
|
|
343
|
-
});
|
|
344
|
-
// Actor guards validate - if rejected, URL reverts to actor's current route
|
|
345
|
-
}
|
|
346
|
-
```
|
|
233
|
+
State machine has final authority over all transitions. TanStack Router navigation triggers actor events (`play.route`), but the actor's guards and transitions ultimately dictate if the view changes.
|
|
347
234
|
|
|
348
235
|
### INV-02: Passive Infrastructure
|
|
349
236
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
**Implementation:** Router observes `actor.currentRoute` signal and updates URL:
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
// Actor state change → Router navigation
|
|
356
|
-
const actorEffect = createEffect(
|
|
357
|
-
on(
|
|
358
|
-
() => this.actor.currentRoute.get(),
|
|
359
|
-
(route) => this.syncRouterFromActor(route),
|
|
360
|
-
),
|
|
361
|
-
);
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
### INV-05: Signal-Only Reactivity
|
|
365
|
-
|
|
366
|
-
**Principle:** All reactivity through signals, never callbacks or promises.
|
|
367
|
-
|
|
368
|
-
**Implementation:** Solid's `createEffect` watches actor signals automatically:
|
|
369
|
-
|
|
370
|
-
```typescript
|
|
371
|
-
// Automatic dependency tracking
|
|
372
|
-
createEffect(
|
|
373
|
-
on(
|
|
374
|
-
() => this.actor.currentRoute.get(), // Signal read
|
|
375
|
-
(route) => {
|
|
376
|
-
// Effect runs when signal changes
|
|
377
|
-
},
|
|
378
|
-
),
|
|
379
|
-
);
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
## Browser Support
|
|
383
|
-
|
|
384
|
-
This package uses the [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) for route pattern matching.
|
|
385
|
-
|
|
386
|
-
**Native Support:**
|
|
387
|
-
|
|
388
|
-
- Safari 17.4+ (Sept 2024)
|
|
389
|
-
- Chrome 95+ (Oct 2021)
|
|
390
|
-
- Firefox 106+ (Oct 2022)
|
|
391
|
-
- **Baseline 2025** status (newly available)
|
|
392
|
-
|
|
393
|
-
**Polyfill Required:**
|
|
394
|
-
|
|
395
|
-
- Node.js (all versions) - required for tests
|
|
396
|
-
- Safari < 17.4
|
|
397
|
-
- Older browsers
|
|
237
|
+
Infrastructure reflects actor state. The router observes `actor.currentRoute` and updates the browser URL, never storing independent business state.
|
|
398
238
|
|
|
399
|
-
|
|
239
|
+
### Cleanup Contract
|
|
400
240
|
|
|
401
|
-
|
|
402
|
-
npm install urlpattern-polyfill
|
|
403
|
-
```
|
|
241
|
+
The bridge implements an explicit `dispose()`/`disconnect()` method. `PlayTanStackRouterProvider` wires this automatically to Solid's `onCleanup` hook to prevent memory leaks and duplicate bridge subscriptions in hot-reloading scenarios.
|
|
404
242
|
|
|
405
|
-
|
|
243
|
+
## URLPattern Support
|
|
406
244
|
|
|
407
|
-
|
|
408
|
-
// Top of your app entry point (e.g., main.tsx)
|
|
409
|
-
if (!globalThis.URLPattern) {
|
|
410
|
-
await import("urlpattern-polyfill");
|
|
411
|
-
}
|
|
412
|
-
```
|
|
245
|
+
This package uses the [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) for robust route pattern matching via `@xmachines/play-router`.
|
|
413
246
|
|
|
414
|
-
|
|
247
|
+
URLPattern is available natively on Node.js 24+ and modern browsers (Chrome 95+, Firefox 117+, Safari 16.4+). On older environments, load a polyfill **before** importing this package — see [`@xmachines/play-router` installation](../play-router/README.md#installation) for details.
|
|
415
248
|
|
|
416
|
-
##
|
|
249
|
+
## Related Packages
|
|
417
250
|
|
|
418
|
-
|
|
251
|
+
- **[@xmachines/play-solid](../play-solid)** - SolidJS renderer
|
|
252
|
+
- **[@xmachines/play-router](../play-router)** - Core router primitives
|
|
253
|
+
- **[@xmachines/play-solid-router](../play-solid-router)** - Native SolidJS Router equivalent
|
|
419
254
|
|
|
420
255
|
## License
|
|
421
256
|
|
package/dist/route-map.d.ts
CHANGED
|
@@ -1,75 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Bidirectional route mapper for TanStack Solid Router.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Extends {@link BaseRouteMap} from `@xmachines/play-router` — all matching logic
|
|
5
|
+
* lives there. This class exists to provide a TanStack Solid Router-specific type
|
|
6
|
+
* name and to allow future adapter-specific extensions without breaking the shared base.
|
|
7
|
+
*
|
|
8
|
+
* **Inherited API:**
|
|
9
|
+
* - `getStateIdByPath(path): string | null` — path → state ID
|
|
10
|
+
* - `getPathByStateId(stateId): string | null` — state ID → path pattern
|
|
11
|
+
*
|
|
12
|
+
* @extends BaseRouteMap
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { createRouteMap } from "@xmachines/play-tanstack-solid-router";
|
|
17
|
+
*
|
|
18
|
+
* const routeMap = createRouteMap(machine);
|
|
19
|
+
*
|
|
20
|
+
* routeMap.getStateIdByPath("/profile/123"); // "#profile"
|
|
21
|
+
* routeMap.getPathByStateId("#settings"); // "/settings/:section?"
|
|
22
|
+
* ```
|
|
6
23
|
*/
|
|
7
|
-
import
|
|
8
|
-
export declare class RouteMap {
|
|
9
|
-
private stateToPath;
|
|
10
|
-
private pathToState;
|
|
11
|
-
/**
|
|
12
|
-
* Create a RouteMap with bidirectional mappings
|
|
13
|
-
*
|
|
14
|
-
* @param mappings - Array of state ID to path mappings
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```typescript
|
|
18
|
-
* const routeMap = new RouteMap([
|
|
19
|
-
* { stateId: '#home', path: '/' },
|
|
20
|
-
* { stateId: '#profile', path: '/profile/:userId' },
|
|
21
|
-
* { stateId: '#settings', path: '/settings/:section?' }
|
|
22
|
-
* ]);
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
constructor(mappings: RouteMapping[]);
|
|
26
|
-
/**
|
|
27
|
-
* Get path pattern for a state ID
|
|
28
|
-
*
|
|
29
|
-
* @param stateId - XMachines state ID (e.g., '#profile')
|
|
30
|
-
* @returns Path pattern or null if not found
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```typescript
|
|
34
|
-
* routeMap.getPathByStateId('#profile'); // '/profile/:userId'
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
getPathByStateId(stateId: string): string | null;
|
|
38
|
-
/**
|
|
39
|
-
* Get state ID for a path, with pattern matching support
|
|
40
|
-
*
|
|
41
|
-
* Performs exact match first, then fuzzy pattern matching for dynamic routes.
|
|
42
|
-
* Supports both required (:param) and optional (:param?) parameters.
|
|
43
|
-
*
|
|
44
|
-
* @param path - Actual URL path (e.g., '/profile/123')
|
|
45
|
-
* @returns State ID or null if no match found
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```typescript
|
|
49
|
-
* routeMap.getStateIdByPath('/profile/123'); // '#profile'
|
|
50
|
-
* routeMap.getStateIdByPath('/settings'); // '#settings'
|
|
51
|
-
* routeMap.getStateIdByPath('/settings/account'); // '#settings'
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
getStateIdByPath(path: string): string | null;
|
|
55
|
-
/**
|
|
56
|
-
* Check if a path matches a pattern
|
|
57
|
-
*
|
|
58
|
-
* Supports:
|
|
59
|
-
* - Required parameters: :param
|
|
60
|
-
* - Optional parameters: :param?
|
|
61
|
-
*
|
|
62
|
-
* @param path - Actual URL path
|
|
63
|
-
* @param pattern - Route pattern with :param syntax
|
|
64
|
-
* @returns true if path matches pattern
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```typescript
|
|
68
|
-
* matchesPattern('/profile/123', '/profile/:userId'); // true
|
|
69
|
-
* matchesPattern('/settings', '/settings/:section?'); // true
|
|
70
|
-
* matchesPattern('/settings/account', '/settings/:section?'); // true
|
|
71
|
-
* ```
|
|
72
|
-
*/
|
|
73
|
-
private matchesPattern;
|
|
24
|
+
import { BaseRouteMap } from "@xmachines/play-router";
|
|
25
|
+
export declare class RouteMap extends BaseRouteMap {
|
|
74
26
|
}
|
|
75
27
|
//# sourceMappingURL=route-map.d.ts.map
|
package/dist/route-map.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-map.d.ts","sourceRoot":"","sources":["../src/route-map.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"route-map.d.ts","sourceRoot":"","sources":["../src/route-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,qBAAa,QAAS,SAAQ,YAAY;CAAG"}
|
package/dist/route-map.js
CHANGED
|
@@ -1,107 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Bidirectional route mapper for TanStack Solid Router.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Extends {@link BaseRouteMap} from `@xmachines/play-router` — all matching logic
|
|
5
|
+
* lives there. This class exists to provide a TanStack Solid Router-specific type
|
|
6
|
+
* name and to allow future adapter-specific extensions without breaking the shared base.
|
|
7
|
+
*
|
|
8
|
+
* **Inherited API:**
|
|
9
|
+
* - `getStateIdByPath(path): string | null` — path → state ID
|
|
10
|
+
* - `getPathByStateId(stateId): string | null` — state ID → path pattern
|
|
11
|
+
*
|
|
12
|
+
* @extends BaseRouteMap
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { createRouteMap } from "@xmachines/play-tanstack-solid-router";
|
|
17
|
+
*
|
|
18
|
+
* const routeMap = createRouteMap(machine);
|
|
19
|
+
*
|
|
20
|
+
* routeMap.getStateIdByPath("/profile/123"); // "#profile"
|
|
21
|
+
* routeMap.getPathByStateId("#settings"); // "/settings/:section?"
|
|
22
|
+
* ```
|
|
6
23
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
pathToState = new Map();
|
|
10
|
-
/**
|
|
11
|
-
* Create a RouteMap with bidirectional mappings
|
|
12
|
-
*
|
|
13
|
-
* @param mappings - Array of state ID to path mappings
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```typescript
|
|
17
|
-
* const routeMap = new RouteMap([
|
|
18
|
-
* { stateId: '#home', path: '/' },
|
|
19
|
-
* { stateId: '#profile', path: '/profile/:userId' },
|
|
20
|
-
* { stateId: '#settings', path: '/settings/:section?' }
|
|
21
|
-
* ]);
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
constructor(mappings) {
|
|
25
|
-
mappings.forEach(({ stateId, path }) => {
|
|
26
|
-
this.stateToPath.set(stateId, path);
|
|
27
|
-
this.pathToState.set(path, stateId);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Get path pattern for a state ID
|
|
32
|
-
*
|
|
33
|
-
* @param stateId - XMachines state ID (e.g., '#profile')
|
|
34
|
-
* @returns Path pattern or null if not found
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```typescript
|
|
38
|
-
* routeMap.getPathByStateId('#profile'); // '/profile/:userId'
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
getPathByStateId(stateId) {
|
|
42
|
-
return this.stateToPath.get(stateId) ?? null;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Get state ID for a path, with pattern matching support
|
|
46
|
-
*
|
|
47
|
-
* Performs exact match first, then fuzzy pattern matching for dynamic routes.
|
|
48
|
-
* Supports both required (:param) and optional (:param?) parameters.
|
|
49
|
-
*
|
|
50
|
-
* @param path - Actual URL path (e.g., '/profile/123')
|
|
51
|
-
* @returns State ID or null if no match found
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* ```typescript
|
|
55
|
-
* routeMap.getStateIdByPath('/profile/123'); // '#profile'
|
|
56
|
-
* routeMap.getStateIdByPath('/settings'); // '#settings'
|
|
57
|
-
* routeMap.getStateIdByPath('/settings/account'); // '#settings'
|
|
58
|
-
* ```
|
|
59
|
-
*/
|
|
60
|
-
getStateIdByPath(path) {
|
|
61
|
-
// Strip query string and hash fragment for matching
|
|
62
|
-
const cleanPath = path.split("?")[0].split("#")[0];
|
|
63
|
-
// Direct lookup first (exact match)
|
|
64
|
-
if (this.pathToState.has(cleanPath)) {
|
|
65
|
-
return this.pathToState.get(cleanPath);
|
|
66
|
-
}
|
|
67
|
-
// Fuzzy match for dynamic routes
|
|
68
|
-
for (const [pattern, stateId] of this.pathToState) {
|
|
69
|
-
if (this.matchesPattern(cleanPath, pattern)) {
|
|
70
|
-
return stateId;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Check if a path matches a pattern
|
|
77
|
-
*
|
|
78
|
-
* Supports:
|
|
79
|
-
* - Required parameters: :param
|
|
80
|
-
* - Optional parameters: :param?
|
|
81
|
-
*
|
|
82
|
-
* @param path - Actual URL path
|
|
83
|
-
* @param pattern - Route pattern with :param syntax
|
|
84
|
-
* @returns true if path matches pattern
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* ```typescript
|
|
88
|
-
* matchesPattern('/profile/123', '/profile/:userId'); // true
|
|
89
|
-
* matchesPattern('/settings', '/settings/:section?'); // true
|
|
90
|
-
* matchesPattern('/settings/account', '/settings/:section?'); // true
|
|
91
|
-
* ```
|
|
92
|
-
*/
|
|
93
|
-
matchesPattern(path, pattern) {
|
|
94
|
-
// Convert route pattern to regex
|
|
95
|
-
// Process replacements in correct order: optional params first, then required
|
|
96
|
-
let regexPattern = pattern
|
|
97
|
-
// Replace /:param? with optional segment (matches both /value and nothing)
|
|
98
|
-
.replace(/\/:(\w+)\?/g, "(?:/([^/]+))?")
|
|
99
|
-
// Replace /:param with required segment
|
|
100
|
-
.replace(/\/:(\w+)/g, "/([^/]+)");
|
|
101
|
-
// Add anchors for exact match
|
|
102
|
-
regexPattern = "^" + regexPattern + "$";
|
|
103
|
-
const regex = new RegExp(regexPattern);
|
|
104
|
-
return regex.test(path);
|
|
105
|
-
}
|
|
24
|
+
import { BaseRouteMap } from "@xmachines/play-router";
|
|
25
|
+
export class RouteMap extends BaseRouteMap {
|
|
106
26
|
}
|
|
107
27
|
//# sourceMappingURL=route-map.js.map
|
package/dist/route-map.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-map.js","sourceRoot":"","sources":["../src/route-map.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"route-map.js","sourceRoot":"","sources":["../src/route-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,MAAM,OAAO,QAAS,SAAQ,YAAY;CAAG"}
|
package/dist/types.d.ts
CHANGED
|
@@ -3,8 +3,21 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export type { RouterBridge, PlayRouteEvent } from "@xmachines/play-router";
|
|
5
5
|
export type { AbstractActor } from "@xmachines/play-actor";
|
|
6
|
+
/**
|
|
7
|
+
* A single state ID ↔ path mapping entry for the TanStack Solid Router adapter.
|
|
8
|
+
*
|
|
9
|
+
* Structurally compatible with `BaseRouteMapping` from `@xmachines/play-router`.
|
|
10
|
+
* Fields are `readonly` — entries are immutable once passed to `RouteMap`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const mapping: RouteMapping = { stateId: "#profile", path: "/profile/:userId" };
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
6
17
|
export interface RouteMapping {
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
/** XMachines state ID (e.g., `"#home"`, `"#profile"`) */
|
|
19
|
+
readonly stateId: string;
|
|
20
|
+
/** TanStack Router path pattern (e.g., `"/"`, `"/profile/:userId"`, `"/settings/:section?"`) */
|
|
21
|
+
readonly path: string;
|
|
9
22
|
}
|
|
10
23
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC3E,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC3E,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmachines/play-tanstack-solid-router",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.9",
|
|
4
4
|
"description": "TanStack Solid Router adapter for XMachines Universal Player Architecture",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "XMachines Contributors",
|
|
@@ -28,27 +28,29 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsc --build",
|
|
31
|
-
"test": "vitest
|
|
31
|
+
"test": "vitest",
|
|
32
32
|
"test:watch": "vitest",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
|
-
"clean": "rm -rf dist *.tsbuildinfo",
|
|
34
|
+
"clean": "rm -rf dist *.tsbuildinfo node_modules/.vite node_modules/.vite-temp",
|
|
35
35
|
"prepublishOnly": "npm run build"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@xmachines/play": "1.0.0-beta.
|
|
39
|
-
"@xmachines/play-actor": "1.0.0-beta.
|
|
40
|
-
"@xmachines/play-router": "1.0.0-beta.
|
|
41
|
-
"@xmachines/play-signals": "1.0.0-beta.
|
|
38
|
+
"@xmachines/play": "1.0.0-beta.9",
|
|
39
|
+
"@xmachines/play-actor": "1.0.0-beta.9",
|
|
40
|
+
"@xmachines/play-router": "1.0.0-beta.9",
|
|
41
|
+
"@xmachines/play-signals": "1.0.0-beta.9"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@solidjs/testing-library": "^0.8.10",
|
|
45
45
|
"@tanstack/solid-router": "^1.167.5",
|
|
46
|
-
"@xmachines/shared": "1.0.0-beta.
|
|
46
|
+
"@xmachines/shared": "1.0.0-beta.9",
|
|
47
47
|
"solid-js": "^1.9.11",
|
|
48
|
-
"vite-plugin-solid": "^2.11.11"
|
|
48
|
+
"vite-plugin-solid": "^2.11.11",
|
|
49
|
+
"vitest": "^4.1.0",
|
|
50
|
+
"xstate": "^5.28.0"
|
|
49
51
|
},
|
|
50
52
|
"peerDependencies": {
|
|
51
|
-
"@tanstack/solid-router": "^1.
|
|
53
|
+
"@tanstack/solid-router": "^1.108.0",
|
|
52
54
|
"solid-js": "^1.8.0"
|
|
53
55
|
}
|
|
54
56
|
}
|