@xmachines/play-router 2.1.1 → 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.
Files changed (39) hide show
  1. package/README.md +270 -11
  2. package/dist/base-path.d.ts +209 -0
  3. package/dist/base-path.d.ts.map +1 -0
  4. package/dist/base-path.js +418 -0
  5. package/dist/base-path.js.map +1 -0
  6. package/dist/base-route-map.d.ts.map +1 -1
  7. package/dist/base-route-map.js +5 -0
  8. package/dist/base-route-map.js.map +1 -1
  9. package/dist/errors.d.ts +87 -4
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/errors.js +97 -4
  12. package/dist/errors.js.map +1 -1
  13. package/dist/framework-params.d.ts +144 -0
  14. package/dist/framework-params.d.ts.map +1 -0
  15. package/dist/framework-params.js +291 -0
  16. package/dist/framework-params.js.map +1 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +13 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/provider-lifecycle.d.ts +179 -0
  22. package/dist/provider-lifecycle.d.ts.map +1 -0
  23. package/dist/provider-lifecycle.js +153 -0
  24. package/dist/provider-lifecycle.js.map +1 -0
  25. package/dist/query.d.ts +49 -0
  26. package/dist/query.d.ts.map +1 -1
  27. package/dist/query.js +59 -0
  28. package/dist/query.js.map +1 -1
  29. package/dist/router-bridge-base.d.ts +320 -14
  30. package/dist/router-bridge-base.d.ts.map +1 -1
  31. package/dist/router-bridge-base.js +919 -102
  32. package/dist/router-bridge-base.js.map +1 -1
  33. package/dist/types.d.ts +44 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/url-pattern-utils.d.ts +0 -30
  36. package/dist/url-pattern-utils.d.ts.map +1 -1
  37. package/dist/url-pattern-utils.js +52 -1
  38. package/dist/url-pattern-utils.js.map +1 -1
  39. 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
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Version](https://img.shields.io/badge/version-2.1.1-blue)](https://www.npmjs.com/package/@xmachines/play-router)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Version](https://img.shields.io/badge/version-2.2.0-blue)](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` | The URLPattern constructor refuses a route pattern string |
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"}