@xmachines/play-vue-router-demo
Examples / @xmachines/play-vue-router-demo
Vue Router integration demo for the XMachines Play architecture using Vue Composition API.
What This Demonstrates
- Shared auth machine reused without framework-specific business logic
PlayRouterProviderrenderer-based integration with Vue Router- Shell-driven rendering via
PlayRendererwith actor-authoritative navigation - Vue Composition API mapping to atoms lifecycle
- Non-browser invariant tests plus browser E2E coverage
- Stately inspector attached at creation via
options.inspect— the debug panel’s Show Inspector button opens the live machine in its own window
Running the Demo
From the repository root:
pnpm installpnpm --filter @xmachines/play-vue-router-demo run devThen open http://localhost:3010.
Step-by-Step Code Flow
Use this order to understand how the demo is wired:
src/main.tscreates the Vue app, installs Vue Router, mounts, and wires HMR cleanup.src/runtime.tscreates the actor, starts it, builds the sharedregistryResult, and creates therouteMap.src/router.tsinstalls a single catch-all route;PlayRendererselects the actual view from actor state.src/App.vuerenders<PlayRouterProvider>with the shared actor, routeMap, router instance, and arenderShellrenderer function.- The renderer function creates the shared
Shellcomponent with actor, router, registry, and inspector as props — the last of which drives the debug panel’s Show Inspector button. PlayRouterProviderwaits forrouter.isReady()so direct URL loads are handled correctly.- Browser tests in
test/browser/validate startup and auth route transitions.
// src/main.ts (shape)const app = createApp(App);app.use(router);app.mount("#app");// src/router.ts (shape)export const routes = [{ path: "/:pathMatch(.*)*", name: "xmachines-play", component: RouteHost }];<!-- src/App.vue (shape) --><script setup lang="ts">import { actor, routeMap, registryResult } from "./runtime.js";
const renderShell = (currentActor, currentRouter) => h(SharedShell, { actor: currentActor, router: currentRouter, registryResult, inspector });</script>
<template> <PlayRouterProvider :actor="actor" :route-map="routeMap" :router="router" :renderer="renderShell" /></template>// src/runtime.ts (shape)export const actor = createPlayer();actor.start();
export const routeMap = createRouteMap(authMachine);export const registryResult = defineRegistry(authCatalog, { components, actions });Key Files
src/main.ts- Vue app bootstrap (mount + router install + HMR cleanup)src/runtime.ts- actor startup, routeMap creation, and registry constructionsrc/router.ts- single catch-all route recordsrc/App.vue-PlayRouterProviderwiring and renderer composition using the shared runtimetest/reactivity.test.ts- reactive integration assertionstest/browser/shared-demo.browser.test.ts- browser startup and full auth flow coverage
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants:
- Actor Authority: When a user navigates to a protected route via a link, Vue Router updates the location. The
PlayRouterProviderintercepts this, translates it to aplay.routeevent, and sends it to the actor. The actor evaluates guards (e.g.isAuthenticated) and transitions. - Passive Infrastructure: The router does not execute Vue route guards for business logic. The actor dictates whether navigation is permitted. The Vue application only renders the state.
- Atom-Only Reactivity: The bridge leverages Vue’s
watchandtriggerRefinternally to react precisely when atoms update, without polluting the Vue component tree with reactive refs that hold business state.
Watcher Lifecycle and Cleanup Contract
This demo follows the canonical watcher lifecycle:
- The atom notifies the subscription, synchronously, from the
setthat changed it watchAtomqueues one microtask, and it drops each duplicate notification of that batch- The microtask reads the actor atoms and syncs Vue-local render state
One subscription lives for the whole watch, and no caller arms a watcher again. Teardown is explicit: provider/bridge cleanup must flow through disconnect and watcher unwatch, not GC-only assumptions. The PlayRouterProvider wires this seamlessly into the component’s onUnmounted hook.
Adapter Boundaries
Vue Router integration remains passive infrastructure. RouterBridgeBase owns shared synchronization policy and the Vue adapter is a thin framework port. Route extraction override strategies remain supported when teams need custom route-name mapping.
Available Scripts
These commands are defined in package.json:
| Command | Description |
|---|---|
pnpm --filter @xmachines/play-vue-router-demo run dev | Start Vite dev server (http://localhost:3010/) |
pnpm --filter @xmachines/play-vue-router-demo run build | Build production bundle |
pnpm --filter @xmachines/play-vue-router-demo run preview | Preview built bundle |
pnpm --filter @xmachines/play-vue-router-demo run test | Run Vitest test suite |
pnpm --filter @xmachines/play-vue-router-demo run test:browser | Run browser-focused Vitest suite |