Skip to content

@xmachines/play-vue-router-demo

Examples / @xmachines/play-vue-router-demo

Vue Router integration demo for the XMachines Play architecture using Vue Composition API.

License: MIT

What This Demonstrates

  • Shared auth machine reused without framework-specific business logic
  • PlayRouterProvider renderer-based integration with Vue Router
  • Shell-driven rendering via PlayRenderer with 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:

Terminal window
pnpm install
pnpm --filter @xmachines/play-vue-router-demo run dev

Then open http://localhost:3010.

Step-by-Step Code Flow

Use this order to understand how the demo is wired:

  1. src/main.ts creates the Vue app, installs Vue Router, mounts, and wires HMR cleanup.
  2. src/runtime.ts creates the actor, starts it, builds the shared registryResult, and creates the routeMap.
  3. src/router.ts installs a single catch-all route; PlayRenderer selects the actual view from actor state.
  4. src/App.vue renders <PlayRouterProvider> with the shared actor, routeMap, router instance, and a renderShell renderer function.
  5. The renderer function creates the shared Shell component with actor, router, registry, and inspector as props — the last of which drives the debug panel’s Show Inspector button.
  6. PlayRouterProvider waits for router.isReady() so direct URL loads are handled correctly.
  7. 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 construction
  • src/router.ts - single catch-all route record
  • src/App.vue - PlayRouterProvider wiring and renderer composition using the shared runtime
  • test/reactivity.test.ts - reactive integration assertions
  • test/browser/shared-demo.browser.test.ts - browser startup and full auth flow coverage

State Machine & Architecture Details

The demo utilizes XMachines architectural invariants:

  1. Actor Authority: When a user navigates to a protected route via a link, Vue Router updates the location. The PlayRouterProvider intercepts this, translates it to a play.route event, and sends it to the actor. The actor evaluates guards (e.g. isAuthenticated) and transitions.
  2. 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.
  3. Atom-Only Reactivity: The bridge leverages Vue’s watch and triggerRef internally 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:

  1. The atom notifies the subscription, synchronously, from the set that changed it
  2. watchAtom queues one microtask, and it drops each duplicate notification of that batch
  3. 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:

CommandDescription
pnpm --filter @xmachines/play-vue-router-demo run devStart Vite dev server (http://localhost:3010/)
pnpm --filter @xmachines/play-vue-router-demo run buildBuild production bundle
pnpm --filter @xmachines/play-vue-router-demo run previewPreview built bundle
pnpm --filter @xmachines/play-vue-router-demo run testRun Vitest test suite
pnpm --filter @xmachines/play-vue-router-demo run test:browserRun browser-focused Vitest suite

Learn More