@xmachines/play-router
v3.0.0
Published
Route tree extraction from XState v5 state machines. Part of @xmachines/play Universal Player Architecture.
Maintainers
Readme
@xmachines/play-router
Route tree extraction from XState v5 state machines. Part of @xmachines/play Universal Player Architecture.
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.
Browser floor: Chrome 110, Firefox 115, Safari 16.4. This package calls the ES2023 change-by-copy array methods, so a browser below that floor throws
TypeError: ... is not a functionat the extraction of the routes. Vite 8 resolves its defaultbaseline-widely-availabletarget to Firefox 114, which is below it — raisebuild.targetwhen you bundle for the browser. The root README carries the table.A parameterized route raises Firefox to 117, which is the version that has the URLPattern API. Load the
urlpattern-polyfillfor an older target, as the Installation section below says.
Installation
pnpm add xstate@^5.31.0
pnpm add @xmachines/play-routerPeer dependencies:
xstate^5.31.0 — XState v5 state machine runtime
URLPattern polyfill (Node.js < 24 / older browsers):
@xmachines/play-router matches each dynamic route with the URLPattern API. URLPattern is native in Node.js 24+ and in a modern browser (Chrome 95+, Firefox 117+, Safari 16.4+).
In an environment without the native API, load a polyfill before you import this package:
// Entry point — must run before any @xmachines/play-router import
import "urlpattern-polyfill";Install the polyfill:
pnpm add urlpattern-polyfillurlpattern-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.
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.
Usage
Extract routes from a machine
import { createMachine } from "xstate";
import { extractMachineRoutes, createRouteMap } from "@xmachines/play-router";
const machine = createMachine({
id: "app",
initial: "home",
states: {
home: {
id: "home",
meta: { route: "/" },
},
dashboard: {
id: "dashboard",
meta: { route: "/dashboard" },
initial: "overview",
states: {
overview: {
id: "overview",
meta: { route: "/overview" },
},
settings: {
id: "settings",
meta: { route: "/settings/:section?" }, // optional parameter
},
},
},
profile: {
id: "profile",
meta: { route: "/profile/:userId" }, // required parameter
},
},
});
// Build hierarchical route tree with bidirectional maps
const tree = extractMachineRoutes(machine);
// Path → RouteNode
const node = tree.byPath.get("/dashboard"); // RouteNode for "dashboard"
// State ID → RouteNode
const overview = tree.byStateId.get("overview");
console.log(overview?.fullPath); // "/overview"
// Build a RouteMap for framework adapters
const routeMap = createRouteMap(machine);
routeMap.getStateIdByPath("/profile/123"); // "profile"
routeMap.getPathByStateId("profile"); // "/profile/:userId"Sending play.route events
import { definePlayer } from "@xmachines/play-xstate";
import type { PlayRouteEvent } from "@xmachines/play-router";
// machine: your routable machine (states carry meta.route)
const actor = definePlayer({ machine })();
actor.start();
// Navigate to a state by ID
const event: PlayRouteEvent = {
type: "play.route",
to: "#dashboard",
};
actor.send(event);
// Navigate with route parameters
actor.send({
type: "play.route",
to: "#profile",
params: { userId: "123" },
});
// Navigate with query parameters
actor.send({
type: "play.route",
to: "#settings",
params: { section: "billing" },
query: { tab: "invoices" },
});Sharing a router with the host (basePath)
A machine owns the complete URL space of its router by default. Give a basePath,
and it owns a prefix only: the host keeps the rest of the same router.
The shape that motivates this option is a URL such as /:machineId/play/dashboard.
The host resolves :machineId — in a loader of TanStack, in a useParams() call, in
a load function of SvelteKit — and the meta.route tree of the machine gives
everything below /:machineId/play.
// The route of the host is /$machineId/play/$ and its loader resolved machineId.
const disconnect = connectRouter({
actor,
router,
routeMap, // unchanged — the route map stays relative to the machine
basePath: "/:machineId/play",
basePathParams: { machineId },
});
// The "/dashboard" of the machine is now the URL "/abc123/play/dashboard".
// The "/" of the machine is now the URL "/abc123/play".The prefix can be a pattern, so that you keep one string that mirrors your route
config, but every :param needs a value in basePathParams. The bridge writes a
real browser URL, so it resolves the prefix in advance, and it refuses each shape
that resolves to one concrete path never: a * wildcard, a :param?, a $param
(the spelling of TanStack — write :param), a query string, and a hash. A base path
is a PATHNAME, so it also refuses a URL scheme: give /admin, and never
https://app.example.com/admin.
It also refuses every segment that a browser REWRITES, and it refuses the resolved
value of a :param on the same rule: a dot segment (., .., and their
percent-encoded forms), a backslash, whitespace, and a character that a URL
percent-encodes, such as the é of /café. Each of them makes the prefix that comes
back differ from the prefix that went out, so every location would read as foreign
and the machine would go permanently silent. Give the segment in the form that a URL
carries: /caf%C3%A9.
An unresolved :param throws a MissingBasePathParamError, at the construction
and not on the first navigation. There is no "not ready yet" fallback, on purpose: a
bridge without a prefix would claim the complete router, and it would start to
correct the URLs of the host, which is worse than a loud failure. Resolve the value
before you connect or render — a route loader or a useParams() call holds it
already.
A location outside the prefix belongs to the host. The bridge sends no
play.route event there, it runs no corrective navigation there, and it writes no
route of its actor there. That silence lets the two halves live together, and it holds
in both directions: a route change that no URL event caused — an after timer, an
async guard that settles, a restore of a snapshot — would otherwise drag the user off
the page of the host. The bridge remembers such a move, and it writes it when the
location comes back under the prefix, so the machine keeps its place. Inside the mount
nothing changes: a path that the machine does not know is still a 404 of its own URL
space, and the URL still follows the actor.
| Location of the router | basePath = "/abc123/play" |
| ---------------------- | ----------------------------------------------------- |
| /abc123/play | the / of the machine → play.route |
| /abc123/play/about | the /about of the machine → play.route |
| /abc123/play/nope | unknown INSIDE the mount → the URL follows the actor |
| /account/billing | a route of the host → the bridge does nothing |
| /abc123/playground | another segment → the host's, the bridge does nothing |
The machine is authoritative over its own params. event.params holds what the
pattern of the machine declares, and nothing else. The params of the prefix belong to
the host — the host wrote the prefix and resolved them — so they travel in no
play.route event, and a param of the host that happens to share a name with one of
the machine cannot shadow it.
Read the resolved mount from the bridge instead, where it cannot go stale:
bridge.basePath; // "/abc123/play"
bridge.basePathParams; // { machineId: "abc123" }A machine that needs the identity of its host — a machineId, a tenant — takes it
through the input of the actor, where it belongs: that identity decides WHICH
machine runs, and it is not a param of a route inside the machine.
Several machines alive at once
More than one machine can be alive at a time, each mounted at its own prefix, all
sharing one router. Every bridge hears every location change of that router, and each
one recognises its own half and leaves the rest alone — a location under another
machine's prefix is foreign in exactly the way a route of the host is. So a
play.route reaches the machine that owns the URL and no other, and a correction of
an unknown path happens only inside the prefix that owns it.
Each bridge can stay connected, as long as each one has its own prefix. A bridge
outside its mount keeps the silence in BOTH directions: it sends no play.route, it
corrects no URL, and it writes no route of its actor. So the machine that the location
belongs to is the only one that writes, and an actor that nobody is looking at cannot
take the URL from the one on screen. A route that the hidden machine moved to is not
lost: the bridge remembers it, and it writes it when the host navigates back under its
prefix.
Two bridges contend when neither prefix separates their halves of the URL. That is
the case for two bridges with no prefix, for two with the same prefix, and also for two
whose prefixes NEST: a mount at /a and a mount at /a/b both claim /a/b/x, because
/a is a prefix of it. Give sibling prefixes — /a/one and /a/two — and no location
belongs to two machines. Where a prefix cannot separate them, connect the bridge that
owns the address bar and let the others keep their state with no bridge attached. connect() refuses a
second bridge for one actor, but it cannot know which of two different actors should
own the URL.
How to load and unload routes
The prefix lives on the bridge, and not on the RouteMap. A route map is static,
it is shared, and it holds an LRU cache inside, so one map serves every mount without
a rebuild.
setBasePath() moves WHERE an actor is mounted, and never WHICH actor is mounted:
// The host moved this actor from one region of its URL space to another.
bridge.setBasePath("/:region/:machineId/play", { region: "us", machineId });The call also brings the location in step. Nothing else moves the address bar, so a
move to a prefix that the URL is not under writes the new mount, and it keeps the
route of the actor: /eu/abc123/play/about becomes /us/abc123/play/about. A location
that already lies under the new prefix drives the actor instead, exactly as it does on
connect().
An actor never changes identity. A segment of the prefix that IDENTIFIES the
actor — a machineId that names the document it runs — therefore never moves through
this method. A new identity is a new ACTOR, and a new actor takes a new bridge,
because connect() permits one bridge for each actor. Give the provider the new actor
and let it rebuild: that is the correct shape, and it is what the host wants, because
the new document starts at its own state.
The segments that move here are the ones that LOCATE: a region, a locale, a tenant, a workspace slug. They say where the same actor lives, and they decide nothing about it.
Nothing goes away: the actor, the route map, and its cache all stay, and the bridge
runs the same first-synchronization decision against the new prefix. A call that
resolves to the same prefix reconciles nothing, and it still takes the new params, so
a move between "/abc123/play" and "/:machineId/play" with { machineId } keeps
event.params honest. A call with no argument removes the prefix, and it gives the
machine the complete router again.
Every PlayRouterProvider gives basePath and basePathParams as reactive
props, wired to setBasePath(). They need no stable reference, unlike actor,
router, and routeMap, and they rebuild the bridge never:
// A render with a new `region` moves the mount, and it rebuilds the bridge never. A
// render with a new `machineId` gives a different `actor` prop, and THAT rebuilds the
// bridge, which is correct: one actor takes one bridge.
<PlayRouterProvider
actor={actor}
router={router}
routeMap={routeMap}
basePath="/:machineId/play"
basePathParams={{ machineId }}
renderer={(a) => <PlayRenderer actor={a} registry={registry} />}
/>To register the routes of the machine in a host router that declares real route objects, ask for the list — and drop it again when the machine unloads:
import { extractMachineRoutes, getRouteMappings } from "@xmachines/play-router";
const tree = extractMachineRoutes(machine);
// Concrete, for a route that the host adds after a loader resolved the mount
getRouteMappings(tree, { basePath: "/:machineId/play", basePathParams: { machineId } });
// [{ stateId: "home", path: "/abc123/play" },
// { stateId: "profile", path: "/abc123/play/profile/:userId" }, ...]
// The pattern, for a static route declaration of the host
getRouteMappings(tree, { basePath: "/:machineId/play" });
// [{ stateId: "home", path: "/:machineId/play" }, ...]Each stateId comes from the route tree, so it carries NO #. A host that keys its
route table on the target of a play.route event adds the # itself, because the
event always carries the prefixed form.
Under a mount,
@xmachines/play-vue-routerand@xmachines/play-solid-routerread the pre-parsed route params of their framework never: under a prefix the framework matched a route of the HOST by construction, because the machine owns the suffix of the path only. Those params therefore describe the route of the machine never, even when a name collides — a collision carries the value of the HOST. Both adapters resolve each param from the stripped path withURLPatterninstead, and they therefore need a polyfill on an older runtime when they are mounted.Without a prefix both adapters keep the parse of their framework, with its decoding and with no polyfill, but restricted to the names that the pattern of the machine declares. A splat of a catch-all, and a param of a wrapper route, reach the actor never.
A location that fills NO optional segment reaches URLPattern never.
/settingsis the bare form of/settings/:section?, so the params are{}, and the adapters read that from the path alone rather than from their framework. The route map that HOLDS that pattern still needed URLPattern when it was built.
How to write a RouterBridgeBase adapter
Extend RouterBridgeBase, then implement the three abstract methods for your framework:
import { RouterBridgeBase, createRouteMap } from "@xmachines/play-router";
import type { RoutableActor } from "@xmachines/play-router";
// Shape of your framework's router — adjust to its real API
type MyRouter = {
navigate(path: string): void;
subscribe(handler: (location: { pathname: string; search: string }) => void): () => void;
state: { location: { pathname: string } };
};
export class MyRouterBridge extends RouterBridgeBase {
private unsubscribe: (() => void) | null = null;
constructor(
private readonly myRouter: MyRouter,
actor: RoutableActor,
routeMap: ReturnType<typeof createRouteMap>,
) {
super(actor, routeMap);
}
// Tell the framework router to navigate to a path
protected navigateRouter(path: string): void {
this.myRouter.navigate(path);
}
// Subscribe to router location changes, call syncActorFromRouter on each
protected watchRouterChanges(): void {
this.unsubscribe = this.myRouter.subscribe((location) => {
this.syncActorFromRouter(location.pathname, location.search);
});
}
// Unsubscribe from router location changes
protected unwatchRouterChanges(): void {
this.unsubscribe?.();
this.unsubscribe = null;
}
// Provide the router's current path for initial deep-link sync
protected override getInitialRouterPath(): string {
return this.myRouter.state.location.pathname;
}
}
// Usage — myRouter: your framework's router instance;
// machine/actor: your routable machine and its started actor
const routeMap = createRouteMap(machine);
const bridge = new MyRouterBridge(myRouter, actor, routeMap);
bridge.connect();
// ...
bridge.disconnect();API Summary
Route Extraction
| Export | Description |
| ---------------------------------------- | --------------------------------------------------------------------------------- |
| extractMachineRoutes(machine) | Converts an XState machine into a RouteTree with the state ID ↔ path maps |
| createRouteMap(machine, options?) | Builds a RouteMap directly from a machine. An adapter uses this form |
| createRouteMapFromTree(tree, options?) | Builds a RouteMap from a RouteTree that you extracted before |
| buildRouteTree(routes) | Builds a RouteTree from an array of RouteInfo objects |
| machineToGraph(machine) | Converts a machine into a typed @statelyai/graph Graph, for a graph algorithm |
Route Matching
| Export | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
| RouteMap | The stateId ↔ path lookup class for both directions. It matches an exact path in O(1) and a pattern in O(k) |
| findRouteById(tree, id) | Finds a RouteNode by its state ID |
| findRouteByPath(tree, path) | Finds a RouteNode by its URL path. It also matches a dynamic pattern |
Query Utilities
| Export | Description |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| getRoutableRoutes(tree) | Returns every routable RouteNode in one flat array |
| getNavigableRoutes(tree, stateId) | Returns the child routes that a state can reach, through the hierarchy and through a transition |
| routeExists(tree, path) | Tells you if the tree holds a path |
| getRouteMappings(tree, options?) | The { stateId, path } entries for a route table of a host, with an optional prefix |
| getTransitionReachableRoutes(graph, stateId) | Returns the route paths that a state can reach through an XState transition |
| isRouteReachable(graph, fromStateId, toStateId) | Tells you if a transition path is present between two states |
Router Bridge
| Export | Description |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| RouterBridgeBase | The abstract base class of each framework router adapter. It implements the RouterBridge protocol |
| sanitizePathname(path) | Normalizes a raw pathname. It returns null for a path of more than 2048 characters, and for malformed input |
| buildPlayRouteEvent(options) | Builds a PlayRouteEvent from a pathname and a route-map match result |
| extractRouteParams(pathname, pattern) | Reads the path parameters of a URL with URLPattern |
| extractQuery(search) | Reads the query parameters of a URL search string |
Base Path
| Export | Description |
| --------------------------------------- | --------------------------------------------------------------------------------------- |
| BasePathOptions | { basePath?, basePathParams? } — every bridge, provider, and connectRouter takes it |
| RouterBridgeBase#basePath | The resolved prefix of the mount, or "" when the machine owns the complete router |
| RouterBridgeBase#basePathParams | The values of the :param segments of the mount — they travel in no play.route event |
| RouterBridgeBase#setBasePath(p, prm?) | Moves the mount of a live bridge, with no teardown, and brings the location in step |
| normalizeBasePath(basePath?) | Normalizes a prefix, and it keeps each :param segment |
| resolveBasePath(basePath?, params?) | Resolves a prefix to { path, params }, and it substitutes every :param |
| stripBasePath(pathname, basePath) | The machine half of a location, or null when the location belongs to the host |
| joinBasePath(basePath, path) | Adds a prefix to a machine path, and it keeps a query string or a hash at the end |
| NO_BASE_PATH | The frozen { path: "", params: {} } of a bridge that takes no basePath |
Framework Params
A bridge whose framework parses the path params itself — Vue Router and SolidJS Router both do — keeps that parse instead of running URLPattern again. The decision that makes it safe is the same in both, so it lives here.
| Export | Description |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| resolveFrameworkParams(source) | Decides which params describe the route of the machine: {}, the parse of the framework, or the fallback |
| getPatternParamNames(pattern) | The names of every :param of a route pattern. A * wildcard carries none |
| getRequiredPatternParamNames(pattern) | The names the pattern REQUIRES, so an optional :name? is left out |
| 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 |
| cleanFrameworkParams(params) | The params of a framework with no absent value, each one a string |
Provider Lifecycle
A PlayRouterProvider of a framework is two things: the lifecycle of a bridge, and
about fifteen lines that bind that lifecycle to the effects of the framework. This
package holds the lifecycle, and it holds every decision in it. An adapter keeps its
own effects and nothing else.
| Export | Description |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| PlayRouterProviderBaseProps<TRouter, TActor, TNode> | The documented props. TNode is what the framework renders |
| PlayRouterBridgeConstructor<TRouter> | The constructor shape a bridge class must satisfy |
| openProviderBridge(BridgeCtor, args) | Builds the bridge, connects it, and returns it with a close |
| repointProviderBridge(bridge, basePath, params?) | Moves the mount of a live bridge. A null bridge and a bridge with no mount API are no-ops |
| mountKey(basePath, params?) | A key that changes when the mount changes, compared BY VALUE |
| isMountableBridge(bridge) | The run-time probe for a bridge that can move its mount |
| createRouterConnection(bridge) | Wraps a live bridge in the callable RouterConnection that connectRouter returns |
| RouterConnection | The callable handle: disconnect(), the mount to read and to move, and a Disposable |
Nothing here imports a framework, so this package keeps no framework dependency —
tests/provider-factory-parity.test.ts holds that. TNode is the only thing in the
props that a framework decides, which is why it is a type parameter.
Validation
| Export | Description |
| ---------------------------------------- | ---------------------------------------------------- |
| validateRouteFormat(route, stateId) | Asserts that the route path is not empty |
| validateStateExists(stateId, stateIds) | Asserts that the machine graph holds the state ID |
| detectDuplicateRoutes(routes) | Throws when two states resolve to the same full path |
Key Types
| Export | Description |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| RouterBridge | The interface of the connect() and disconnect() lifecycle |
| MountableRouterBridge | A RouterBridge whose mount can move — it adds basePath and setBasePath() |
| RouteTree | The hierarchical tree, with root, byStateId, byPath, and an optional graph |
| RouteNode | One node of the tree, with id, path, fullPath, stateId, children, and parent |
| RouteInfo | The flat route descriptor that comes from a state node |
| PlayRouteEvent | Routing event { type: "play.route", to, params?, query? } |
| RoutableActor | The minimal actor interface that RouterBridgeBase requires: currentRoute, initialRoute, and send(PlayRouteEvent) |
| PlayActor | The complete actor interface that PlayRouterProvider uses. It extends RoutableActor with currentView (Routable + Viewable) |
| RouteMapping | The { stateId, path } pair that builds a RouteMap |
| RouteMapping as BaseRouteMapping | The alias of RouteMapping, for compatibility with an earlier version |
| MachineGraph | The typed @statelyai/graph Graph, with MachineNodeData and MachineEdgeData |
| WindowLike | The minimal window interface that you can inject for SSR and for a test |
| LocationLike | The minimal location interface that you can inject for SSR and for a test |
Errors (subpath @xmachines/play-router/errors)
| Class | Code | When thrown |
| ---------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| RouterSyncError | PLAY_ROUTER_SYNC_FAILED | syncActorFromRouter() cannot send a play.route event |
| DuplicateBridgeError | PLAY_ROUTER_DUPLICATE_BRIDGE | A second bridge tries to connect to an actor that already has one |
| URLPatternUnavailableError | PLAY_ROUTE_MAP_URLPATTERN_UNAVAILABLE | The URLPattern API is absent, and no polyfill is loaded |
| InvalidRoutePatternError | PLAY_ROUTE_MAP_INVALID_PATTERN | A route pattern does not compile, or two of its params land on one URLPattern group |
| EmptyRoutePathError | PLAY_ROUTE_EMPTY_PATH | A state declares meta.route: "" |
| InvalidStateIdError | PLAY_ROUTE_INVALID_STATE_ID | A route names a state ID that the machine graph does not hold |
| DuplicateRoutePathError | PLAY_ROUTE_DUPLICATE_PATH | Two or more states share the same URL path |
| UnknownStateTypeError | PLAY_ROUTE_UNKNOWN_STATE_TYPE | A state node has an XState .type value that the package does not know |
| InvalidBasePathError | PLAY_ROUTER_INVALID_BASE_PATH | A basePath resolves to one concrete prefix never (*, :p?, $p, ?, #, a scheme, ./.., whitespace) |
| MissingBasePathParamError | PLAY_ROUTER_MISSING_BASE_PATH_PARAM | A :param of a basePath has no value in basePathParams |
import {
RouterSyncError,
DuplicateBridgeError,
URLPatternUnavailableError,
} from "@xmachines/play-router/errors";
// bridge from the adapter example above
try {
bridge.connect();
} catch (err) {
if (err instanceof DuplicateBridgeError) {
// Actor already bridged — call disconnect() first
} else if (err instanceof RouterSyncError) {
console.error("Router sync failed:", err.message, err.cause);
}
}Route Configuration
meta.route patterns
Declare the route of an XState state node in its meta.route field:
states: {
home: {
id: "home",
meta: { route: "/" }, // static route
},
profile: {
id: "profile",
meta: { route: "/profile/:userId" }, // required parameter
},
settings: {
id: "settings",
meta: { route: "/settings/:section?" }, // optional parameter
},
docs: {
id: "docs",
meta: { route: { path: "/docs", title: "Documentation" } }, // object form
},
}Relative vs absolute paths
A child route that starts with / is absolute, and it does not inherit the path of its parent. A child route without the first / is relative to its nearest routable ancestor:
states: {
dashboard: {
id: "dashboard",
meta: { route: "/dashboard" },
states: {
overview: {
id: "overview",
meta: { route: "/overview" }, // absolute → fullPath: "/overview"
},
stats: {
id: "stats",
meta: { route: "stats" }, // relative → fullPath: "/dashboard/stats"
},
},
},
}Always use node.fullPath to match a browser URL and to build a route map. Never use node.path for this.
Testing
# Run tests for this package
pnpm --filter @xmachines/play-router test
# Watch mode
pnpm --filter @xmachines/play-router run test:watch@xmachines/play-router-shared holds a contract test suite of the router bridge, for the
author of an adapter. That suite drives a real actor. Therefore it is one layer above this
package, and @xmachines/play-router keeps no dependency on an actor runtime.
@xmachines/play-router-shared is a private workspace package. Thus only an adapter author
in this repository can use the suite:
import { runBridgeContractTests } from "@xmachines/play-router-shared/test/router-bridge-contract.js";
runBridgeContractTests({
name: "MyRouterBridge",
createHarness(initialPath) {
// return ContractHarness with bridge, actor, simulateNavigation, getLastNavigatedPath
},
createRestoredHarness(routedPath) {
// return ContractHarness whose actor is restored to routedPath
// while the mock router starts at the machine's initial route
},
});Related Packages
- @xmachines/play — Core protocol types (
PlayEvent,PlayError) - @xmachines/play-actor — the abstract actor base class (
AbstractActor,Routable). EveryAbstractActorsubclass satisfiesRoutableActorstructurally - @xmachines/play-signals — the TC39 Signals polyfill that observes the actor route
- @xmachines/play-xstate — the XState v5 logic adapter, which works with a route tree
- @xmachines/play-tanstack-router — Shared TanStack Router bridge base (framework-agnostic)
- @xmachines/play-tanstack-react-router — TanStack Router adapter (React)
- @xmachines/play-tanstack-solid-router — TanStack Router adapter (SolidJS)
- @xmachines/play-react-router — React Router v7 adapter
- @xmachines/play-vue-router — Vue Router adapter
- @xmachines/play-solid-router — SolidJS Router adapter
License
MIT — see LICENSE.
