@xmachines/play-router 2.1.0 → 2.2.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/README.md +270 -11
- package/dist/base-path.d.ts +209 -0
- package/dist/base-path.d.ts.map +1 -0
- package/dist/base-path.js +418 -0
- package/dist/base-path.js.map +1 -0
- package/dist/base-route-map.d.ts.map +1 -1
- package/dist/base-route-map.js +5 -0
- package/dist/base-route-map.js.map +1 -1
- package/dist/errors.d.ts +87 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +97 -4
- package/dist/errors.js.map +1 -1
- package/dist/framework-params.d.ts +144 -0
- package/dist/framework-params.d.ts.map +1 -0
- package/dist/framework-params.js +291 -0
- package/dist/framework-params.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/provider-lifecycle.d.ts +179 -0
- package/dist/provider-lifecycle.d.ts.map +1 -0
- package/dist/provider-lifecycle.js +153 -0
- package/dist/provider-lifecycle.js.map +1 -0
- package/dist/query.d.ts +49 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +59 -0
- package/dist/query.js.map +1 -1
- package/dist/router-bridge-base.d.ts +353 -15
- package/dist/router-bridge-base.d.ts.map +1 -1
- package/dist/router-bridge-base.js +998 -83
- package/dist/router-bridge-base.js.map +1 -1
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/url-pattern-utils.d.ts +0 -30
- package/dist/url-pattern-utils.d.ts.map +1 -1
- package/dist/url-pattern-utils.js +52 -1
- package/dist/url-pattern-utils.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Route tree extraction from XState v5 state machines. Part of [@xmachines/play](../play/README.md) Universal Player Architecture.
|
|
4
4
|
|
|
5
|
-
[](https://opensource.org/licenses/MIT) [](https://opensource.org/licenses/MIT) [](https://www.npmjs.com/package/@xmachines/play-router)
|
|
6
6
|
|
|
7
7
|
This package extracts the routes from a machine graph and looks them up in both directions. The Actor therefore keeps the authority over the navigation.
|
|
8
8
|
|
|
@@ -36,6 +36,8 @@ pnpm add urlpattern-polyfill
|
|
|
36
36
|
|
|
37
37
|
`urlpattern-polyfill` is an optional peer dependency. A package manager does not install it for you. Install it and load it yourself when your runtime has no native URLPattern.
|
|
38
38
|
|
|
39
|
+
**When you need it:** `RouteMap` compiles each parameterized route in its CONSTRUCTOR, and it throws a `URLPatternUnavailableError` there when no URLPattern is available. One route that holds a `:param` or a `*` therefore makes the polyfill a startup requirement on such a runtime. A route map whose paths are all static needs URLPattern never.
|
|
40
|
+
|
|
39
41
|
## Usage
|
|
40
42
|
|
|
41
43
|
### Extract routes from a machine
|
|
@@ -123,6 +125,209 @@ actor.send({
|
|
|
123
125
|
});
|
|
124
126
|
```
|
|
125
127
|
|
|
128
|
+
### Sharing a router with the host (`basePath`)
|
|
129
|
+
|
|
130
|
+
A machine owns the complete URL space of its router by default. Give a `basePath`,
|
|
131
|
+
and it owns a prefix only: the host keeps the rest of the same router.
|
|
132
|
+
|
|
133
|
+
The shape that motivates this option is a URL such as `/:machineId/play/dashboard`.
|
|
134
|
+
The host resolves `:machineId` — in a loader of TanStack, in a `useParams()` call, in
|
|
135
|
+
a `load` function of SvelteKit — and the `meta.route` tree of the machine gives
|
|
136
|
+
everything below `/:machineId/play`.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// The route of the host is /$machineId/play/$ and its loader resolved machineId.
|
|
140
|
+
const disconnect = connectRouter({
|
|
141
|
+
actor,
|
|
142
|
+
router,
|
|
143
|
+
routeMap, // unchanged — the route map stays relative to the machine
|
|
144
|
+
basePath: "/:machineId/play",
|
|
145
|
+
basePathParams: { machineId },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// The "/dashboard" of the machine is now the URL "/abc123/play/dashboard".
|
|
149
|
+
// The "/" of the machine is now the URL "/abc123/play".
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The prefix can be a pattern, so that you keep one string that mirrors your route
|
|
153
|
+
config, but every `:param` needs a value in `basePathParams`. The bridge writes a
|
|
154
|
+
real browser URL, so it resolves the prefix in advance, and it refuses each shape
|
|
155
|
+
that resolves to one concrete path never: a `*` wildcard, a `:param?`, a `$param`
|
|
156
|
+
(the spelling of TanStack — write `:param`), a query string, and a hash. A base path
|
|
157
|
+
is a PATHNAME, so it also refuses a URL scheme: give `/admin`, and never
|
|
158
|
+
`https://app.example.com/admin`.
|
|
159
|
+
|
|
160
|
+
It also refuses every segment that a browser REWRITES, and it refuses the resolved
|
|
161
|
+
value of a `:param` on the same rule: a dot segment (`.`, `..`, and their
|
|
162
|
+
percent-encoded forms), a backslash, whitespace, and a character that a URL
|
|
163
|
+
percent-encodes, such as the `é` of `/café`. Each of them makes the prefix that comes
|
|
164
|
+
back differ from the prefix that went out, so every location would read as foreign
|
|
165
|
+
and the machine would go permanently silent. Give the segment in the form that a URL
|
|
166
|
+
carries: `/caf%C3%A9`.
|
|
167
|
+
|
|
168
|
+
**An unresolved `:param` throws** a `MissingBasePathParamError`, at the construction
|
|
169
|
+
and not on the first navigation. There is no "not ready yet" fallback, on purpose: a
|
|
170
|
+
bridge without a prefix would claim the complete router, and it would start to
|
|
171
|
+
correct the URLs of the host, which is worse than a loud failure. Resolve the value
|
|
172
|
+
before you connect or render — a route loader or a `useParams()` call holds it
|
|
173
|
+
already.
|
|
174
|
+
|
|
175
|
+
**A location outside the prefix belongs to the host.** The bridge sends no
|
|
176
|
+
`play.route` event there, it runs no corrective navigation there, and it writes no
|
|
177
|
+
route of its actor there. That silence lets the two halves live together, and it holds
|
|
178
|
+
in both directions: a route change that no URL event caused — an `after` timer, an
|
|
179
|
+
async guard that settles, a restore of a snapshot — would otherwise drag the user off
|
|
180
|
+
the page of the host. The bridge remembers such a move, and it writes it when the
|
|
181
|
+
location comes back under the prefix, so the machine keeps its place. Inside the mount
|
|
182
|
+
nothing changes: a path that the machine does not know is still a 404 of its own URL
|
|
183
|
+
space, and the URL still follows the actor.
|
|
184
|
+
|
|
185
|
+
| Location of the router | `basePath = "/abc123/play"` |
|
|
186
|
+
| ---------------------- | ----------------------------------------------------- |
|
|
187
|
+
| `/abc123/play` | the `/` of the machine → `play.route` |
|
|
188
|
+
| `/abc123/play/about` | the `/about` of the machine → `play.route` |
|
|
189
|
+
| `/abc123/play/nope` | unknown INSIDE the mount → the URL follows the actor |
|
|
190
|
+
| `/account/billing` | a route of the host → the bridge does nothing |
|
|
191
|
+
| `/abc123/playground` | another segment → the host's, the bridge does nothing |
|
|
192
|
+
|
|
193
|
+
**The machine is authoritative over its own params.** `event.params` holds what the
|
|
194
|
+
pattern of the machine declares, and nothing else. The params of the prefix belong to
|
|
195
|
+
the host — the host wrote the prefix and resolved them — so they travel in no
|
|
196
|
+
`play.route` event, and a param of the host that happens to share a name with one of
|
|
197
|
+
the machine cannot shadow it.
|
|
198
|
+
|
|
199
|
+
Read the resolved mount from the bridge instead, where it cannot go stale:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
bridge.basePath; // "/abc123/play"
|
|
203
|
+
bridge.basePathParams; // { machineId: "abc123" }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
A machine that needs the identity of its host — a `machineId`, a tenant — takes it
|
|
207
|
+
through the `input` of the actor, where it belongs: that identity decides WHICH
|
|
208
|
+
machine runs, and it is not a param of a route inside the machine.
|
|
209
|
+
|
|
210
|
+
#### Several machines alive at once
|
|
211
|
+
|
|
212
|
+
More than one machine can be alive at a time, each mounted at its own prefix, all
|
|
213
|
+
sharing one router. Every bridge hears every location change of that router, and each
|
|
214
|
+
one recognises its own half and leaves the rest alone — a location under another
|
|
215
|
+
machine's prefix is foreign in exactly the way a route of the host is. So a
|
|
216
|
+
`play.route` reaches the machine that owns the URL and no other, and a correction of
|
|
217
|
+
an unknown path happens only inside the prefix that owns it.
|
|
218
|
+
|
|
219
|
+
**Each bridge can stay connected, as long as each one has its own prefix.** A bridge
|
|
220
|
+
outside its mount keeps the silence in BOTH directions: it sends no `play.route`, it
|
|
221
|
+
corrects no URL, and it writes no route of its actor. So the machine that the location
|
|
222
|
+
belongs to is the only one that writes, and an actor that nobody is looking at cannot
|
|
223
|
+
take the URL from the one on screen. A route that the hidden machine moved to is not
|
|
224
|
+
lost: the bridge remembers it, and it writes it when the host navigates back under its
|
|
225
|
+
prefix.
|
|
226
|
+
|
|
227
|
+
**Two bridges contend when neither prefix separates their halves of the URL.** That is
|
|
228
|
+
the case for two bridges with no prefix, for two with the same prefix, and also for two
|
|
229
|
+
whose prefixes NEST: a mount at `/a` and a mount at `/a/b` both claim `/a/b/x`, because
|
|
230
|
+
`/a` is a prefix of it. Give sibling prefixes — `/a/one` and `/a/two` — and no location
|
|
231
|
+
belongs to two machines. Where a prefix cannot separate them, connect the bridge that
|
|
232
|
+
owns the address bar and let the others keep their state with no bridge attached. `connect()` refuses a
|
|
233
|
+
second bridge for one actor, but it cannot know which of two different actors should
|
|
234
|
+
own the URL.
|
|
235
|
+
|
|
236
|
+
#### How to load and unload routes
|
|
237
|
+
|
|
238
|
+
The prefix lives on the **bridge**, and not on the `RouteMap`. A route map is static,
|
|
239
|
+
it is shared, and it holds an LRU cache inside, so one map serves every mount without
|
|
240
|
+
a rebuild.
|
|
241
|
+
|
|
242
|
+
`setBasePath()` moves WHERE an actor is mounted, and never WHICH actor is mounted:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// The host moved this actor from one region of its URL space to another.
|
|
246
|
+
bridge.setBasePath("/:region/:machineId/play", { region: "us", machineId });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The call also brings the location in step. Nothing else moves the address bar, so a
|
|
250
|
+
move to a prefix that the URL is not under writes the new mount, and it keeps the
|
|
251
|
+
route of the actor: `/eu/abc123/play/about` becomes `/us/abc123/play/about`. A location
|
|
252
|
+
that already lies under the new prefix drives the actor instead, exactly as it does on
|
|
253
|
+
`connect()`.
|
|
254
|
+
|
|
255
|
+
**An actor never changes identity.** A segment of the prefix that IDENTIFIES the
|
|
256
|
+
actor — a `machineId` that names the document it runs — therefore never moves through
|
|
257
|
+
this method. A new identity is a new ACTOR, and a new actor takes a new bridge,
|
|
258
|
+
because `connect()` permits one bridge for each actor. Give the provider the new actor
|
|
259
|
+
and let it rebuild: that is the correct shape, and it is what the host wants, because
|
|
260
|
+
the new document starts at its own state.
|
|
261
|
+
|
|
262
|
+
The segments that move here are the ones that LOCATE: a region, a locale, a tenant, a
|
|
263
|
+
workspace slug. They say where the same actor lives, and they decide nothing about
|
|
264
|
+
it.
|
|
265
|
+
|
|
266
|
+
Nothing goes away: the actor, the route map, and its cache all stay, and the bridge
|
|
267
|
+
runs the same first-synchronization decision against the new prefix. A call that
|
|
268
|
+
resolves to the same prefix reconciles nothing, and it still takes the new params, so
|
|
269
|
+
a move between `"/abc123/play"` and `"/:machineId/play"` with `{ machineId }` keeps
|
|
270
|
+
`event.params` honest. A call with no argument removes the prefix, and it gives the
|
|
271
|
+
machine the complete router again.
|
|
272
|
+
|
|
273
|
+
Every `PlayRouterProvider` gives `basePath` and `basePathParams` as **reactive
|
|
274
|
+
props**, wired to `setBasePath()`. They need no stable reference, unlike `actor`,
|
|
275
|
+
`router`, and `routeMap`, and they rebuild the bridge never:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
// A render with a new `region` moves the mount, and it rebuilds the bridge never. A
|
|
279
|
+
// render with a new `machineId` gives a different `actor` prop, and THAT rebuilds the
|
|
280
|
+
// bridge, which is correct: one actor takes one bridge.
|
|
281
|
+
<PlayRouterProvider
|
|
282
|
+
actor={actor}
|
|
283
|
+
router={router}
|
|
284
|
+
routeMap={routeMap}
|
|
285
|
+
basePath="/:machineId/play"
|
|
286
|
+
basePathParams={{ machineId }}
|
|
287
|
+
renderer={(a) => <PlayRenderer actor={a} registry={registry} />}
|
|
288
|
+
/>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
To register the routes of the machine in a host router that declares real route
|
|
292
|
+
objects, ask for the list — and drop it again when the machine unloads:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { extractMachineRoutes, getRouteMappings } from "@xmachines/play-router";
|
|
296
|
+
|
|
297
|
+
const tree = extractMachineRoutes(machine);
|
|
298
|
+
|
|
299
|
+
// Concrete, for a route that the host adds after a loader resolved the mount
|
|
300
|
+
getRouteMappings(tree, { basePath: "/:machineId/play", basePathParams: { machineId } });
|
|
301
|
+
// [{ stateId: "home", path: "/abc123/play" },
|
|
302
|
+
// { stateId: "profile", path: "/abc123/play/profile/:userId" }, ...]
|
|
303
|
+
|
|
304
|
+
// The pattern, for a static route declaration of the host
|
|
305
|
+
getRouteMappings(tree, { basePath: "/:machineId/play" });
|
|
306
|
+
// [{ stateId: "home", path: "/:machineId/play" }, ...]
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Each `stateId` comes from the route tree, so it carries NO `#`. A host that keys its
|
|
310
|
+
route table on the target of a `play.route` event adds the `#` itself, because the
|
|
311
|
+
event always carries the prefixed form.
|
|
312
|
+
|
|
313
|
+
> Under a mount, `@xmachines/play-vue-router` and `@xmachines/play-solid-router` read
|
|
314
|
+
> the pre-parsed route params of their framework never: under a prefix the framework
|
|
315
|
+
> matched a route of the HOST by construction, because the machine owns the suffix of
|
|
316
|
+
> the path only. Those params therefore describe the route of the machine never, even
|
|
317
|
+
> when a name collides — a collision carries the value of the HOST. Both adapters
|
|
318
|
+
> resolve each param from the stripped path with `URLPattern` instead, and they
|
|
319
|
+
> therefore need a polyfill on an older runtime when they are mounted.
|
|
320
|
+
>
|
|
321
|
+
> Without a prefix both adapters keep the parse of their framework, with its decoding
|
|
322
|
+
> and with no polyfill, but restricted to the names that the pattern of the machine
|
|
323
|
+
> declares. A splat of a catch-all, and a param of a wrapper route, reach the actor
|
|
324
|
+
> never.
|
|
325
|
+
>
|
|
326
|
+
> A location that fills NO optional segment reaches URLPattern never. `/settings` is the
|
|
327
|
+
> bare form of `/settings/:section?`, so the params are `{}`, and the adapters read that
|
|
328
|
+
> from the path alone rather than from their framework. The route map that HOLDS that
|
|
329
|
+
> pattern still needed URLPattern when it was built.
|
|
330
|
+
|
|
126
331
|
### How to write a `RouterBridgeBase` adapter
|
|
127
332
|
|
|
128
333
|
Extend `RouterBridgeBase`, then implement the three abstract methods for your framework:
|
|
@@ -209,6 +414,7 @@ bridge.disconnect();
|
|
|
209
414
|
| `getRoutableRoutes(tree)` | Returns every routable `RouteNode` in one flat array |
|
|
210
415
|
| `getNavigableRoutes(tree, stateId)` | Returns the child routes that a state can reach, through the hierarchy and through a transition |
|
|
211
416
|
| `routeExists(tree, path)` | Tells you if the tree holds a path |
|
|
417
|
+
| `getRouteMappings(tree, options?)` | The `{ stateId, path }` entries for a route table of a host, with an optional prefix |
|
|
212
418
|
| `getTransitionReachableRoutes(graph, stateId)` | Returns the route paths that a state can reach through an XState transition |
|
|
213
419
|
| `isRouteReachable(graph, fromStateId, toStateId)` | Tells you if a transition path is present between two states |
|
|
214
420
|
|
|
@@ -222,6 +428,56 @@ bridge.disconnect();
|
|
|
222
428
|
| `extractRouteParams(pathname, pattern)` | Reads the path parameters of a URL with URLPattern |
|
|
223
429
|
| `extractQuery(search)` | Reads the query parameters of a URL search string |
|
|
224
430
|
|
|
431
|
+
### Base Path
|
|
432
|
+
|
|
433
|
+
| Export | Description |
|
|
434
|
+
| --------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
435
|
+
| `BasePathOptions` | `{ basePath?, basePathParams? }` — every bridge, provider, and `connectRouter` takes it |
|
|
436
|
+
| `RouterBridgeBase#basePath` | The resolved prefix of the mount, or `""` when the machine owns the complete router |
|
|
437
|
+
| `RouterBridgeBase#basePathParams` | The values of the `:param` segments of the mount — they travel in no `play.route` event |
|
|
438
|
+
| `RouterBridgeBase#setBasePath(p, prm?)` | Moves the mount of a live bridge, with no teardown, and brings the location in step |
|
|
439
|
+
| `normalizeBasePath(basePath?)` | Normalizes a prefix, and it keeps each `:param` segment |
|
|
440
|
+
| `resolveBasePath(basePath?, params?)` | Resolves a prefix to `{ path, params }`, and it substitutes every `:param` |
|
|
441
|
+
| `stripBasePath(pathname, basePath)` | The machine half of a location, or `null` when the location belongs to the host |
|
|
442
|
+
| `joinBasePath(basePath, path)` | Adds a prefix to a machine path, and it keeps a query string or a hash at the end |
|
|
443
|
+
| `NO_BASE_PATH` | The frozen `{ path: "", params: {} }` of a bridge that takes no `basePath` |
|
|
444
|
+
|
|
445
|
+
### Framework Params
|
|
446
|
+
|
|
447
|
+
A bridge whose framework parses the path params itself — Vue Router and SolidJS Router
|
|
448
|
+
both do — keeps that parse instead of running URLPattern again. The decision that makes
|
|
449
|
+
it safe is the same in both, so it lives here.
|
|
450
|
+
|
|
451
|
+
| Export | Description |
|
|
452
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
453
|
+
| `resolveFrameworkParams(source)` | Decides which params describe the route of the machine: `{}`, the parse of the framework, or the fallback |
|
|
454
|
+
| `getPatternParamNames(pattern)` | The names of every `:param` of a route pattern. A `*` wildcard carries none |
|
|
455
|
+
| `getRequiredPatternParamNames(pattern)` | The names the pattern REQUIRES, so an optional `:name?` is left out |
|
|
456
|
+
| `pickOwnParams(params, names, requiredNames?)` | The params that the pattern declares, or `null` when the framework covers them not. `requiredNames` says which names may NOT be absent; it defaults to every name, so a two-argument call treats an optional `:name?` as a gap |
|
|
457
|
+
| `cleanFrameworkParams(params)` | The params of a framework with no absent value, each one a string |
|
|
458
|
+
|
|
459
|
+
### Provider Lifecycle
|
|
460
|
+
|
|
461
|
+
A `PlayRouterProvider` of a framework is two things: the lifecycle of a bridge, and
|
|
462
|
+
about fifteen lines that bind that lifecycle to the effects of the framework. This
|
|
463
|
+
package holds the lifecycle, and it holds every decision in it. An adapter keeps its
|
|
464
|
+
own effects and nothing else.
|
|
465
|
+
|
|
466
|
+
| Export | Description |
|
|
467
|
+
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
468
|
+
| `PlayRouterProviderBaseProps<TRouter, TActor, TNode>` | The documented props. `TNode` is what the framework renders |
|
|
469
|
+
| `PlayRouterBridgeConstructor<TRouter>` | The constructor shape a bridge class must satisfy |
|
|
470
|
+
| `openProviderBridge(BridgeCtor, args)` | Builds the bridge, connects it, and returns it with a `close` |
|
|
471
|
+
| `repointProviderBridge(bridge, basePath, params?)` | Moves the mount of a live bridge. A `null` bridge and a bridge with no mount API are no-ops |
|
|
472
|
+
| `mountKey(basePath, params?)` | A key that changes when the mount changes, compared BY VALUE |
|
|
473
|
+
| `isMountableBridge(bridge)` | The run-time probe for a bridge that can move its mount |
|
|
474
|
+
| `createRouterConnection(bridge)` | Wraps a live bridge in the callable `RouterConnection` that `connectRouter` returns |
|
|
475
|
+
| `RouterConnection` | The callable handle: `disconnect()`, and the mount to read and to move |
|
|
476
|
+
|
|
477
|
+
Nothing here imports a framework, so this package keeps no framework dependency —
|
|
478
|
+
`tests/provider-factory-parity.test.ts` holds that. `TNode` is the only thing in the
|
|
479
|
+
props that a framework decides, which is why it is a type parameter.
|
|
480
|
+
|
|
225
481
|
### Validation
|
|
226
482
|
|
|
227
483
|
| Export | Description |
|
|
@@ -235,6 +491,7 @@ bridge.disconnect();
|
|
|
235
491
|
| Export | Description |
|
|
236
492
|
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
237
493
|
| `RouterBridge` | The interface of the `connect()` and `disconnect()` lifecycle |
|
|
494
|
+
| `MountableRouterBridge` | A `RouterBridge` whose mount can move — it adds `basePath` and `setBasePath()` |
|
|
238
495
|
| `RouteTree` | The hierarchical tree, with `root`, `byStateId`, `byPath`, and an optional `graph` |
|
|
239
496
|
| `RouteNode` | One node of the tree, with `id`, `path`, `fullPath`, `stateId`, `children`, and `parent` |
|
|
240
497
|
| `RouteInfo` | The flat route descriptor that comes from a state node |
|
|
@@ -249,16 +506,18 @@ bridge.disconnect();
|
|
|
249
506
|
|
|
250
507
|
### Errors (subpath `@xmachines/play-router/errors`)
|
|
251
508
|
|
|
252
|
-
| Class | Code | When thrown
|
|
253
|
-
| ---------------------------- | --------------------------------------- |
|
|
254
|
-
| `RouterSyncError` | `PLAY_ROUTER_SYNC_FAILED` | `syncActorFromRouter()` cannot send a `play.route` event
|
|
255
|
-
| `DuplicateBridgeError` | `PLAY_ROUTER_DUPLICATE_BRIDGE` | A second bridge tries to connect to an actor that already has one
|
|
256
|
-
| `URLPatternUnavailableError` | `PLAY_ROUTE_MAP_URLPATTERN_UNAVAILABLE` | The URLPattern API is absent, and no polyfill is loaded
|
|
257
|
-
| `InvalidRoutePatternError` | `PLAY_ROUTE_MAP_INVALID_PATTERN` |
|
|
258
|
-
| `EmptyRoutePathError` | `PLAY_ROUTE_EMPTY_PATH` | A state declares `meta.route: ""`
|
|
259
|
-
| `InvalidStateIdError` | `PLAY_ROUTE_INVALID_STATE_ID` | A route names a state ID that the machine graph does not hold
|
|
260
|
-
| `DuplicateRoutePathError` | `PLAY_ROUTE_DUPLICATE_PATH` | Two or more states share the same URL path
|
|
261
|
-
| `UnknownStateTypeError` | `PLAY_ROUTE_UNKNOWN_STATE_TYPE` | A state node has an XState `.type` value that the package does not know
|
|
509
|
+
| Class | Code | When thrown |
|
|
510
|
+
| ---------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
511
|
+
| `RouterSyncError` | `PLAY_ROUTER_SYNC_FAILED` | `syncActorFromRouter()` cannot send a `play.route` event |
|
|
512
|
+
| `DuplicateBridgeError` | `PLAY_ROUTER_DUPLICATE_BRIDGE` | A second bridge tries to connect to an actor that already has one |
|
|
513
|
+
| `URLPatternUnavailableError` | `PLAY_ROUTE_MAP_URLPATTERN_UNAVAILABLE` | The URLPattern API is absent, and no polyfill is loaded |
|
|
514
|
+
| `InvalidRoutePatternError` | `PLAY_ROUTE_MAP_INVALID_PATTERN` | A route pattern does not compile, or two of its params land on one URLPattern group |
|
|
515
|
+
| `EmptyRoutePathError` | `PLAY_ROUTE_EMPTY_PATH` | A state declares `meta.route: ""` |
|
|
516
|
+
| `InvalidStateIdError` | `PLAY_ROUTE_INVALID_STATE_ID` | A route names a state ID that the machine graph does not hold |
|
|
517
|
+
| `DuplicateRoutePathError` | `PLAY_ROUTE_DUPLICATE_PATH` | Two or more states share the same URL path |
|
|
518
|
+
| `UnknownStateTypeError` | `PLAY_ROUTE_UNKNOWN_STATE_TYPE` | A state node has an XState `.type` value that the package does not know |
|
|
519
|
+
| `InvalidBasePathError` | `PLAY_ROUTER_INVALID_BASE_PATH` | A `basePath` resolves to one concrete prefix never (`*`, `:p?`, `$p`, `?`, `#`, a scheme, `.`/`..`, whitespace) |
|
|
520
|
+
| `MissingBasePathParamError` | `PLAY_ROUTER_MISSING_BASE_PATH_PARAM` | A `:param` of a `basePath` has no value in `basePathParams` |
|
|
262
521
|
|
|
263
522
|
```typescript
|
|
264
523
|
import {
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base-path support — it mounts the routes of a machine under a URL prefix.
|
|
3
|
+
*
|
|
4
|
+
* A host application often owns a part of its own URL space, and it wants a machine
|
|
5
|
+
* to own the rest of that space, inside the SAME router. A URL such as
|
|
6
|
+
* `/:machineId/play/dashboard` has two halves: `/:machineId/play` belongs to the
|
|
7
|
+
* host, which resolves `:machineId` in a loader, and `/dashboard` belongs to the
|
|
8
|
+
* `meta.route` tree of the machine.
|
|
9
|
+
*
|
|
10
|
+
* This module is the boundary between those two halves:
|
|
11
|
+
* - {@link resolveBasePath} makes the concrete prefix that the browser URL carries,
|
|
12
|
+
* from a base-path pattern and its params.
|
|
13
|
+
* - {@link stripBasePath} removes that prefix from an inbound location of the router,
|
|
14
|
+
* and it reports a location OUTSIDE the prefix as foreign, so that the bridge stays
|
|
15
|
+
* out of it.
|
|
16
|
+
* - {@link joinBasePath} adds the prefix again to an outbound path of the machine.
|
|
17
|
+
*
|
|
18
|
+
* The prefix lives on the BRIDGE, and not on the `RouteMap`. A route map is static,
|
|
19
|
+
* it is shared, and it holds an LRU cache inside, while a mount point is dynamic. One
|
|
20
|
+
* route map therefore serves every `machineId` without a rebuild.
|
|
21
|
+
*
|
|
22
|
+
* @internal The package exports these primitives for an adapter and for a host, but
|
|
23
|
+
* a consumer normally uses the `basePath` option of a bridge instead.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The base-path options that every router bridge, `connectRouter` function, and
|
|
27
|
+
* `PlayRouterProvider` component accepts.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // A host that mounts a machine under a route it resolved in a loader
|
|
32
|
+
* connectRouter({
|
|
33
|
+
* actor,
|
|
34
|
+
* router,
|
|
35
|
+
* routeMap,
|
|
36
|
+
* basePath: "/:machineId/play",
|
|
37
|
+
* basePathParams: { machineId },
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export interface BasePathOptions {
|
|
42
|
+
/**
|
|
43
|
+
* The URL prefix that the routes of the machine hang off, for example `"/admin"`
|
|
44
|
+
* or `"/:machineId/play"`.
|
|
45
|
+
*
|
|
46
|
+
* A `:param` segment is a declarative convenience, so that a host keeps ONE string
|
|
47
|
+
* that mirrors its own route config. Every `:param` must have a value in
|
|
48
|
+
* {@link BasePathOptions.basePathParams}, because the bridge writes a real URL. An
|
|
49
|
+
* optional segment (`:param?`) and a wildcard (`*`) are therefore refused: neither
|
|
50
|
+
* of them resolves to one concrete prefix.
|
|
51
|
+
*
|
|
52
|
+
* An absent value, `""`, and `"/"` all mean "no prefix", which is the behaviour of
|
|
53
|
+
* every release before this option existed.
|
|
54
|
+
*/
|
|
55
|
+
readonly basePath?: string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The values of the `:param` segments of {@link BasePathOptions.basePath}.
|
|
58
|
+
*
|
|
59
|
+
* These values belong to the HOST, which wrote the prefix and resolved them, so
|
|
60
|
+
* they travel in NO `play.route` event: the machine is authoritative over its own
|
|
61
|
+
* params, and `event.params` holds what the pattern of the machine declares, and
|
|
62
|
+
* nothing else. A param of the host that shares a name with one of the machine can
|
|
63
|
+
* therefore shadow it never.
|
|
64
|
+
*
|
|
65
|
+
* A host reads them back from `bridge.basePathParams`, where they cannot go stale.
|
|
66
|
+
* A machine that needs the identity of its host takes it through the `input` of the
|
|
67
|
+
* actor, where that identity decides WHICH machine runs.
|
|
68
|
+
*/
|
|
69
|
+
readonly basePathParams?: Record<string, string | number> | undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A base path that is ready for a use in both directions.
|
|
73
|
+
*/
|
|
74
|
+
export interface ResolvedBasePath {
|
|
75
|
+
/**
|
|
76
|
+
* The normalized prefix: it starts with `/`, it carries no trailing `/`, and it
|
|
77
|
+
* holds no duplicate slash. The empty string means "no prefix".
|
|
78
|
+
*/
|
|
79
|
+
readonly path: string;
|
|
80
|
+
/**
|
|
81
|
+
* The values of each `:param` of the prefix, for the `params` of a `play.route`
|
|
82
|
+
* event. The object is empty for a prefix without a param, and for a pattern that
|
|
83
|
+
* nothing resolved.
|
|
84
|
+
*/
|
|
85
|
+
readonly params: Record<string, string>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The base path of a bridge that has no `basePath` option.
|
|
89
|
+
*
|
|
90
|
+
* The object is FROZEN, and both levels of it: {@link resolveBasePath} returns this
|
|
91
|
+
* exact instance for every bridge with no mount, and
|
|
92
|
+
* `RouterBridgeBase.basePathParams` hands its `params` to the caller.
|
|
93
|
+
* `Readonly<Record<string, string>>` is gone at run time, so without the freeze one
|
|
94
|
+
* write in consumer code would leak that key into every other unmounted bridge in
|
|
95
|
+
* the process.
|
|
96
|
+
*/
|
|
97
|
+
export declare const NO_BASE_PATH: ResolvedBasePath;
|
|
98
|
+
/**
|
|
99
|
+
* Normalizes a base path, and it substitutes no `:param` segment of that path.
|
|
100
|
+
*
|
|
101
|
+
* Use this function when you want the form of the pattern, for example to declare
|
|
102
|
+
* the routes of a machine in a route config of a host that substitutes each param
|
|
103
|
+
* itself. {@link resolveBasePath} gives the form that a bridge needs.
|
|
104
|
+
*
|
|
105
|
+
* @param basePath - The prefix, as a pattern or as a concrete path. An absent value,
|
|
106
|
+
* `""`, and `"/"` all give `""`.
|
|
107
|
+
* @returns The normalized prefix. It starts with `/`, and it carries no trailing `/`.
|
|
108
|
+
* @throws {InvalidBasePathError} For a wildcard segment, for an optional `:param?`
|
|
109
|
+
* segment, for a `$param` segment, for a query string or a hash fragment, for a URL
|
|
110
|
+
* scheme, for a dot segment (`.`, `..`, and their percent-encoded forms), and for any
|
|
111
|
+
* segment that a browser rewrites — one that holds whitespace, a backslash, or a
|
|
112
|
+
* character that a URL percent-encodes.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* normalizeBasePath("/:machineId/play/"); // "/:machineId/play"
|
|
117
|
+
* normalizeBasePath("admin"); // "/admin"
|
|
118
|
+
* normalizeBasePath("/"); // ""
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare function normalizeBasePath(basePath?: string | undefined): string;
|
|
122
|
+
/**
|
|
123
|
+
* Resolves a base path and its params to the concrete prefix that a browser URL
|
|
124
|
+
* carries.
|
|
125
|
+
*
|
|
126
|
+
* A bridge calls this function one time, and again on each `setBasePath()` call. The
|
|
127
|
+
* result is always concrete. {@link stripBasePath} therefore compares a literal
|
|
128
|
+
* prefix, and no URLPattern match runs on the hot path of a navigation.
|
|
129
|
+
*
|
|
130
|
+
* @param basePath - The prefix, as a pattern or as a concrete path.
|
|
131
|
+
* @param params - The values of the `:param` segments of `basePath`.
|
|
132
|
+
* @returns The concrete prefix, and the values of each param for a `play.route` event.
|
|
133
|
+
* @throws {InvalidBasePathError} For a wildcard segment, for an optional `:param?`
|
|
134
|
+
* segment, for a `$param` segment, for a query string or a hash fragment, for a URL
|
|
135
|
+
* scheme, for a dot segment (`.`, `..`, and their percent-encoded forms), and for any
|
|
136
|
+
* segment that a browser rewrites — one that holds whitespace, a backslash, or a
|
|
137
|
+
* character that a URL percent-encodes.
|
|
138
|
+
* @throws {MissingBasePathParamError} When a `:param` of the prefix has no value.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* resolveBasePath("/:machineId/play", { machineId: "abc123" });
|
|
143
|
+
* // → { path: "/abc123/play", params: { machineId: "abc123" } }
|
|
144
|
+
*
|
|
145
|
+
* resolveBasePath("/admin");
|
|
146
|
+
* // → { path: "/admin", params: {} }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export declare function resolveBasePath(basePath?: string | undefined, params?: Record<string, string | number> | undefined): ResolvedBasePath;
|
|
150
|
+
/**
|
|
151
|
+
* Removes the base path from an inbound location of the router.
|
|
152
|
+
*
|
|
153
|
+
* The return value separates the two cases that make a shared router work:
|
|
154
|
+
* - A string is the MACHINE half of the location, for example `"/dashboard"` of
|
|
155
|
+
* `"/abc123/play/dashboard"`. The bridge matches it against the route map.
|
|
156
|
+
* - `null` means that the location is FOREIGN: it lies outside the mount, so it
|
|
157
|
+
* belongs to the host. The bridge sends no `play.route` event, AND it runs no
|
|
158
|
+
* corrective navigation. A correction here would drag the user off a page of the
|
|
159
|
+
* host.
|
|
160
|
+
*
|
|
161
|
+
* The match is exact, and it respects each segment boundary. `"/abc/playground"` is
|
|
162
|
+
* therefore foreign to the mount `"/abc/play"`.
|
|
163
|
+
*
|
|
164
|
+
* @param pathname - A sanitized pathname of the router, with no query and no duplicate slash.
|
|
165
|
+
* @param basePath - The concrete prefix of {@link resolveBasePath}. `""` removes nothing.
|
|
166
|
+
* @returns The machine half of the path, or `null` when the location is foreign.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* stripBasePath("/abc123/play/dashboard", "/abc123/play"); // "/dashboard"
|
|
171
|
+
* stripBasePath("/abc123/play", "/abc123/play"); // "/"
|
|
172
|
+
* stripBasePath("/settings", "/abc123/play"); // null — the host owns it
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export declare function stripBasePath(pathname: string, basePath: string): string | null;
|
|
176
|
+
/**
|
|
177
|
+
* Adds the base path to an outbound path of the machine.
|
|
178
|
+
*
|
|
179
|
+
* The function joins the PATHNAME only. A query string and a hash fragment of the
|
|
180
|
+
* path of the machine stay at the end, because the prefix belongs in front of the
|
|
181
|
+
* path.
|
|
182
|
+
*
|
|
183
|
+
* @param basePath - The concrete prefix of {@link resolveBasePath}. `""` adds nothing.
|
|
184
|
+
* @param path - The path of the machine, for example `"/dashboard"` or `"/dates?trip=one-way"`.
|
|
185
|
+
* @returns The location for the router of the host.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* joinBasePath("/abc123/play", "/dashboard"); // "/abc123/play/dashboard"
|
|
190
|
+
* joinBasePath("/abc123/play", "/"); // "/abc123/play"
|
|
191
|
+
* joinBasePath("/abc123/play", "/x?tab=a"); // "/abc123/play/x?tab=a"
|
|
192
|
+
* joinBasePath("", "/dashboard"); // "/dashboard"
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export declare function joinBasePath(basePath: string, path: string): string;
|
|
196
|
+
/**
|
|
197
|
+
* Gives the index of the first query mark or fragment mark of a path, or `-1`.
|
|
198
|
+
*
|
|
199
|
+
* The names say `At`, because both values are an INDEX. A name such as `hash` reads
|
|
200
|
+
* as a digest to a reader and to a scanner both: `gitlab.eslint.detect-possible-timing-attacks`
|
|
201
|
+
* fires on a `===` test against an identifier of that name, and it cannot see that
|
|
202
|
+
* the operands here are two numbers.
|
|
203
|
+
*
|
|
204
|
+
* @internal `RouterBridgeBase` splits a location on the same boundary, to read the
|
|
205
|
+
* pathname half of an actor route. One function, so the two sides cannot disagree
|
|
206
|
+
* about where a pathname ends.
|
|
207
|
+
*/
|
|
208
|
+
export declare function firstMarkIndex(path: string): number;
|
|
209
|
+
//# sourceMappingURL=base-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-path.d.ts","sourceRoot":"","sources":["../src/base-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,eAAe;IAC/B;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC;CACtE;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,EAAE,gBAGzB,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAqBvE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS,GAClD,gBAAgB,CAkElB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMnD"}
|