@xmachines/play-tanstack-react-router 1.0.0-beta.1
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/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +3 -0
- package/README.md +177 -0
- package/dist/extract-params.d.ts +45 -0
- package/dist/extract-params.d.ts.map +1 -0
- package/dist/extract-params.js +70 -0
- package/dist/extract-params.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/play-router-provider.d.ts +33 -0
- package/dist/play-router-provider.d.ts.map +1 -0
- package/dist/play-router-provider.js +31 -0
- package/dist/play-router-provider.js.map +1 -0
- package/dist/route-map.d.ts +101 -0
- package/dist/route-map.d.ts.map +1 -0
- package/dist/route-map.js +139 -0
- package/dist/route-map.js.map +1 -0
- package/dist/tanstack-router-bridge.d.ts +115 -0
- package/dist/tanstack-router-bridge.d.ts.map +1 -0
- package/dist/tanstack-router-bridge.js +112 -0
- package/dist/tanstack-router-bridge.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +10 -0
- package/dist/utils.js.map +1 -0
- package/examples/demo/README.md +100 -0
- package/examples/demo/docs/ARCHITECTURE.md +643 -0
- package/examples/demo/docs/INVARIANTS.md +461 -0
- package/examples/demo/docs/SWAP-REACT.md +635 -0
- package/examples/demo/index.html +16 -0
- package/examples/demo/package.json +39 -0
- package/examples/demo/src/App.tsx +148 -0
- package/examples/demo/src/components/About.tsx +49 -0
- package/examples/demo/src/components/Contact.tsx +43 -0
- package/examples/demo/src/components/Dashboard.tsx +46 -0
- package/examples/demo/src/components/DebugPanel.tsx +68 -0
- package/examples/demo/src/components/HeaderNav.tsx +103 -0
- package/examples/demo/src/components/Home.tsx +41 -0
- package/examples/demo/src/components/Login.tsx +82 -0
- package/examples/demo/src/components/Navigation.tsx +262 -0
- package/examples/demo/src/components/Profile.tsx +46 -0
- package/examples/demo/src/components/Register.tsx +109 -0
- package/examples/demo/src/components/Settings.tsx +92 -0
- package/examples/demo/src/components/index.ts +16 -0
- package/examples/demo/src/main.tsx +20 -0
- package/examples/demo/test/actor-authority.test.ts +50 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/Browser-back-button-navigates-through-unique-history--no-duplicates--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/GAP-12--navigation-via-goto---events-creates-single-history-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/GAP-12--navigation-via-goto---events-creates-single-history-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--After-authentication-flow-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--URL-stays-in-sync-with-actor-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--URL-stays-in-sync-with-actor-state-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--about-loads-about-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--contact-loads-contact-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--home-loads-home-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to-protected-route-while-authenticated-loads-dashboard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to-protected-route-while-unauthenticated-redirects-to-login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/EXACT-USER-SCENARIO--home---about---home---contact---home--then-back-3x-should-land-on-about-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/EXACT-USER-SCENARIO--home---about---home---contact---home--then-back-3x-should-land-on-about-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---unauthenticated-user-stays-on-public-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---unauthenticated-user-stays-on-public-routes-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Rapid-back-forward-navigation-doesn-t-cause-duplicate-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Rapid-back-forward-navigation-doesn-t-cause-duplicate-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-navigates-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-navigates-from-dashboard-to-settings--protected-route--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-navigates-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--account--section-when-navigating-to--settings-account-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--account--section-when-navigating-to--settings-account-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--general--section-when-navigating-to--settings--no-parameter--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--general--section-when-navigating-to--settings--no-parameter--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--profile--section-when-navigating-to--settings-profile-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--profile--section-when-navigating-to--settings-profile-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-update-section-display-when-clicking-section-navigation-buttons-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-update-section-display-when-clicking-section-navigation-buttons-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-query-parameters-works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-section-parameter-works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04--State-Driven-Reset---Browser-back-sends-event-to-actor-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04--State-Driven-Reset---Browser-back-sends-event-to-actor-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/test-multiple-back.browser.test.tsx/Multiple-back--Navigate-forward-3x-then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-1--Opening-with--someinvalidstate-stays-at-current-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2--Opening-with--about-renders-About-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2b--Opening-with--home-renders-Home-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2c--Opening-with--contact-renders-Contact-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-3--Back-forward-navigation---rendering-syncs-with-URL-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-3--Back-forward-navigation---rendering-syncs-with-URL-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4--Auth-state-preserved-when-navigating-between-authenticated-anonymous-states-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4--Auth-state-preserved-when-navigating-between-authenticated-anonymous-states-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4b--Browser-back-forward-preserves-auth-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4b--Browser-back-forward-preserves-auth-state-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-5--Protected-route-with-play-route-respects-authentication-guard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-5--Protected-route-with-play-route-respects-authentication-guard-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/user-reported-scenario.browser.test.tsx/User-scenario--home---about---home---contact---home--then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/user-reported-scenario.browser.test.tsx/User-scenario--login---home---about---home---contact---home--then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-play-route-event-with-correct-state-ID-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-play-route-event-with-correct-state-ID-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-xstate-route-event-with-correct-state-ID-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-play-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-xstate-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-xstate-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/GAP-12-fix-preserved--No-duplicate-history-entries-with-play-route-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/GAP-12-fix-preserved--No-duplicate-history-entries-with-play-route-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Protected-route-sends-xstate-route-with-authentication-guard-1.png +0 -0
- package/examples/demo/test/browser/back-button-duplicate.browser.test.tsx +148 -0
- package/examples/demo/test/browser/back-forward-sync.browser.test.tsx +149 -0
- package/examples/demo/test/browser/direct-navigation.browser.test.ts +146 -0
- package/examples/demo/test/browser/exact-user-scenario.browser.test.tsx +207 -0
- package/examples/demo/test/browser/guard-rejection.browser.test.tsx +52 -0
- package/examples/demo/test/browser/history-investigation.browser.test.tsx +82 -0
- package/examples/demo/test/browser/history-navigation.browser.test.ts +351 -0
- package/examples/demo/test/browser/login-flow.browser.test.tsx +34 -0
- package/examples/demo/test/browser/navigation.browser.test.tsx +34 -0
- package/examples/demo/test/browser/protected-route-navigation.browser.test.tsx +161 -0
- package/examples/demo/test/browser/redirect-url-update.browser.test.tsx +140 -0
- package/examples/demo/test/browser/settings-parameter.browser.test.tsx +164 -0
- package/examples/demo/test/browser/settings-query-freeze.browser.test.ts +141 -0
- package/examples/demo/test/browser/state-driven.browser.test.ts +112 -0
- package/examples/demo/test/browser/tanstack-integration.browser.test.tsx +61 -0
- package/examples/demo/test/browser/uat-xstate-route-regression.browser.test.ts +58 -0
- package/examples/demo/test/browser/xstate-route-events.browser.test.ts +293 -0
- package/examples/demo/test/browser-back-view-rendering.test.ts +104 -0
- package/examples/demo/test/browser-e2e/auth-flow.browser.test.tsx +49 -0
- package/examples/demo/test/invalid-route-redirect.test.ts +40 -0
- package/examples/demo/test/passive-infra.test.ts +35 -0
- package/examples/demo/test/route-parameters.test.ts +539 -0
- package/examples/demo/test/signal-only.test.ts +54 -0
- package/examples/demo/test/strict-separation.test.ts +37 -0
- package/examples/demo/test/test-utils.ts +49 -0
- package/examples/demo/tsconfig.json +21 -0
- package/examples/demo/tsconfig.tsbuildinfo +1 -0
- package/examples/demo/vite.config.ts +13 -0
- package/examples/demo/vitest.browser.config.ts +72 -0
- package/examples/demo/vitest.config.e2e.browser.ts +28 -0
- package/examples/demo/vitest.config.ts +35 -0
- package/package.json +51 -0
- package/src/extract-params.ts +75 -0
- package/src/index.ts +31 -0
- package/src/play-router-provider.tsx +46 -0
- package/src/route-map.ts +158 -0
- package/src/tanstack-router-bridge.ts +135 -0
- package/src/types.ts +26 -0
- package/src/utils.ts +12 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/Browser-back-button-sends-route-navigate-event-to-actor-1.png +0 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-prevents-circular-updates-1.png +0 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-syncs-actor-route-to-browser-URL-1.png +0 -0
- package/test/browser/signal-synced-history.browser.test.ts +95 -0
- package/test/route-map.test.ts +107 -0
- package/test/tanstack-router-bridge.test.ts +318 -0
- package/test/urlpattern-integration.test.ts +145 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
# Architectural Invariants Validation
|
|
2
|
+
|
|
3
|
+
This document explains how the demo application validates each of the 5 architectural invariants that define the Universal Player Architecture.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Actor Authority](#1-actor-authority)
|
|
10
|
+
2. [Strict Separation](#2-strict-separation)
|
|
11
|
+
3. [Signal-Only Reactivity](#3-signal-only-reactivity)
|
|
12
|
+
4. [Passive Infrastructure](#4-passive-infrastructure)
|
|
13
|
+
5. [State-Driven Reset](#5-state-driven-reset)
|
|
14
|
+
6. [Why These Invariants Matter](#why-these-invariants-matter)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Actor Authority
|
|
19
|
+
|
|
20
|
+
### What It Means
|
|
21
|
+
|
|
22
|
+
**The actor controls navigation via guards. Infrastructure obeys actor decisions.**
|
|
23
|
+
|
|
24
|
+
In traditional web apps, routing libraries control navigation - they decide if a route transition is valid. In Universal Player Architecture, the **actor** decides via guard functions, and infrastructure (router, components) obeys.
|
|
25
|
+
|
|
26
|
+
### How Demo Proves It
|
|
27
|
+
|
|
28
|
+
**Code Location:** `src/machines/auth-machine.ts`
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
dashboard: {
|
|
32
|
+
meta: { route: "/dashboard" },
|
|
33
|
+
// Always-guard validates state entry
|
|
34
|
+
always: [
|
|
35
|
+
{
|
|
36
|
+
target: "login",
|
|
37
|
+
guard: ({ context }) => !context.isAuthenticated
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Proof:**
|
|
44
|
+
|
|
45
|
+
1. Try to access `/dashboard` while logged out
|
|
46
|
+
2. Always-guard checks: `!context.isAuthenticated` → `true`
|
|
47
|
+
3. Automatic transition to `login` state
|
|
48
|
+
4. Actor Authority: actor decides navigation validity, not the router
|
|
49
|
+
|
|
50
|
+
**Test Reference:** `test/invariants/actor-authority.test.ts`
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Attempt to access protected route while logged out
|
|
54
|
+
actor.send({ type: "play.route", to: "/dashboard" });
|
|
55
|
+
|
|
56
|
+
// Always-guard validates: !isAuthenticated → redirect to login
|
|
57
|
+
assert.strictEqual(actor.getSnapshot().matches("login"), true);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Run Test:**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cd examples/demo-app
|
|
64
|
+
npm test -- test/invariants/actor-authority.test.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Why This Matters
|
|
68
|
+
|
|
69
|
+
- **Security:** Authentication logic lives in actor, not URL patterns
|
|
70
|
+
- **Consistency:** Same guard logic works across all platforms (web, mobile, desktop)
|
|
71
|
+
- **Testability:** Test guards in isolation without DOM or routing libraries
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 2. Strict Separation
|
|
76
|
+
|
|
77
|
+
### What It Means
|
|
78
|
+
|
|
79
|
+
**Business logic has zero UI framework imports. View structure defined in metadata, not JSX.**
|
|
80
|
+
|
|
81
|
+
Traditional React apps mix business logic with JSX. In Universal Player Architecture, state machines define view structure declaratively in `meta.view`, with zero React imports.
|
|
82
|
+
|
|
83
|
+
### How Demo Proves It
|
|
84
|
+
|
|
85
|
+
**Code Location:** `src/machines/auth-machine.ts` lines 55-63
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
loggedOut: {
|
|
89
|
+
meta: {
|
|
90
|
+
route: "/",
|
|
91
|
+
// View structure in metadata, not JSX
|
|
92
|
+
view: {
|
|
93
|
+
component: "LoginForm", // String reference, not JSX import
|
|
94
|
+
props: { title: "Please Log In" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Proof:**
|
|
101
|
+
|
|
102
|
+
1. `auth-machine.ts` imports: `createMachine`, `assign` from `xstate` - **no React**
|
|
103
|
+
2. `catalog.ts` imports: `z` from `zod`, `defineCatalog` - **no React**
|
|
104
|
+
3. View structure is plain data: `{ component: "LoginForm", props: {...} }`
|
|
105
|
+
4. Renderer resolves component name at runtime via catalog
|
|
106
|
+
|
|
107
|
+
**Test Reference:** `test/invariants/strict-separation.test.ts`
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const authMachineContent = readFileSync("src/machines/auth-machine.ts", "utf-8");
|
|
111
|
+
const catalogContent = readFileSync("src/machines/catalog.ts", "utf-8");
|
|
112
|
+
|
|
113
|
+
// Verify no React imports
|
|
114
|
+
const reactImportPattern = /from ['"]react['"]/;
|
|
115
|
+
assert.strictEqual(reactImportPattern.test(authMachineContent), false);
|
|
116
|
+
assert.strictEqual(reactImportPattern.test(catalogContent), false);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Run Test:**
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
cd examples/demo-app
|
|
123
|
+
npm test -- test/invariants/strict-separation.test.ts
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Manual Verification:**
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
grep -rn "from ['\"']react" src/machines/
|
|
130
|
+
# Expected: no output (zero React imports)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Why This Matters
|
|
134
|
+
|
|
135
|
+
- **Framework Agnostic:** Same state machine works with React, Vue, Svelte, or any renderer
|
|
136
|
+
- **Portability:** Business logic runs in Node.js, browser, mobile, desktop without changes
|
|
137
|
+
- **Longevity:** Framework churn doesn't affect business logic
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 3. Signal-Only Reactivity
|
|
142
|
+
|
|
143
|
+
### What It Means
|
|
144
|
+
|
|
145
|
+
**React rendering driven by signals, not React state for business logic.**
|
|
146
|
+
|
|
147
|
+
Traditional React apps use `useState` for business state. In Universal Player Architecture, business state lives in actor signals. React observes signals via `useSignalEffect` and only uses `useState` for render triggering.
|
|
148
|
+
|
|
149
|
+
### How Demo Proves It
|
|
150
|
+
|
|
151
|
+
**Code Location:** `packages/play-react/src/PlayRenderer.tsx` (simplified)
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
function PlayRenderer({ actor, components }) {
|
|
155
|
+
const [, setTick] = useState(0);
|
|
156
|
+
|
|
157
|
+
// Observe actor.state signal
|
|
158
|
+
useSignalEffect(() => {
|
|
159
|
+
const currentState = actor.state.get();
|
|
160
|
+
setTick((t) => t + 1); // Trigger React re-render
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const currentState = actor.state.get();
|
|
164
|
+
const view = currentState.meta?.view;
|
|
165
|
+
const Component = components[view.component];
|
|
166
|
+
|
|
167
|
+
return <Component send={actor.send.bind(actor)} {...view.props} />;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Proof:**
|
|
172
|
+
|
|
173
|
+
1. `PlayRenderer` uses `useSignalEffect` to observe `actor.state` signal
|
|
174
|
+
2. `useState` is used ONLY for render triggering (`setTick`), not business state
|
|
175
|
+
3. Business state comes from `actor.state.get()` - signal, not React state
|
|
176
|
+
4. Components receive `send` function to forward events to actor
|
|
177
|
+
|
|
178
|
+
**Test Reference:** `test/invariants/signal-only.test.ts`
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const playRendererContent = readFileSync("packages/play-react/src/PlayRenderer.tsx", "utf-8");
|
|
182
|
+
|
|
183
|
+
// PlayRenderer must use useSignalEffect for signal observation
|
|
184
|
+
assert.strictEqual(playRendererContent.includes("useSignalEffect"), true);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Run Test:**
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
cd examples/demo-app
|
|
191
|
+
npm test -- test/invariants/signal-only.test.ts
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Why This Matters
|
|
195
|
+
|
|
196
|
+
- **Single Source of Truth:** Business state lives in actor, not duplicated in React state
|
|
197
|
+
- **Consistency:** Signal updates are atomic and synchronous - no race conditions
|
|
198
|
+
- **Debuggability:** Inspect actor state via XState DevTools, not React DevTools
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 4. Passive Infrastructure
|
|
203
|
+
|
|
204
|
+
### What It Means
|
|
205
|
+
|
|
206
|
+
**Components forward events to actor, don't control navigation. Zero router imports.**
|
|
207
|
+
|
|
208
|
+
Traditional React components import routing libraries and navigate directly (e.g., `navigate("/dashboard")`). In Universal Player Architecture, components forward events to actor via `send()` and actor decides navigation.
|
|
209
|
+
|
|
210
|
+
### How Demo Proves It
|
|
211
|
+
|
|
212
|
+
**Code Location:** `src/components/LoginForm.tsx` lines 54-64
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
|
|
218
|
+
// Forward event to actor - component doesn't navigate
|
|
219
|
+
send({ type: "auth.login", username, password });
|
|
220
|
+
};
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Proof:**
|
|
224
|
+
|
|
225
|
+
1. `LoginForm.tsx` imports: `React`, `useState`, `FormEvent` - **no router imports**
|
|
226
|
+
2. Component receives `send` function via props (bound to actor)
|
|
227
|
+
3. Submit button forwards `auth.login` event to actor
|
|
228
|
+
4. Actor decides if login is valid and transitions to dashboard state
|
|
229
|
+
5. Router observes actor.currentRoute signal (derived from state) and updates browser URL
|
|
230
|
+
|
|
231
|
+
**Test Reference:** `test/invariants/passive-infra.test.ts`
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const componentsDir = "src/components";
|
|
235
|
+
const files = readdirSync(componentsDir).filter((f) => f.endsWith(".tsx"));
|
|
236
|
+
|
|
237
|
+
const routerImportPattern = /from ['"]@tanstack\/react-router['"]/;
|
|
238
|
+
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
const content = readFileSync(join(componentsDir, file), "utf-8");
|
|
241
|
+
|
|
242
|
+
// Verify no TanStack Router imports
|
|
243
|
+
assert.strictEqual(routerImportPattern.test(content), false);
|
|
244
|
+
|
|
245
|
+
// Verify components use send() for event forwarding
|
|
246
|
+
assert.strictEqual(content.includes("send(") || content.includes("send:"), true);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Run Test:**
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
cd examples/demo-app
|
|
254
|
+
npm test -- test/invariants/passive-infra.test.ts
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Manual Verification:**
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
grep -rn "@tanstack/react-router" src/components/
|
|
261
|
+
# Expected: no output (zero router imports)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Why This Matters
|
|
265
|
+
|
|
266
|
+
- **Testability:** Test components without routing libraries - just pass mock `send` function
|
|
267
|
+
- **Flexibility:** Change routing libraries without touching components
|
|
268
|
+
- **Consistency:** Navigation logic centralized in actor, not scattered across components
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 5. State-Driven Reset
|
|
273
|
+
|
|
274
|
+
### What It Means
|
|
275
|
+
|
|
276
|
+
**Browser back button sends event to actor for validation. Infrastructure doesn't decide.**
|
|
277
|
+
|
|
278
|
+
Traditional routers update URL immediately when back button pressed, then components react. In Universal Player Architecture, router sends `play.route` event to actor, actor processes event with always-guards, then state updates.
|
|
279
|
+
|
|
280
|
+
### How Demo Proves It
|
|
281
|
+
|
|
282
|
+
**Code Location:** `packages/play-tanstack-react-router/src/SignalSyncedHistory.ts` (simplified)
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
window.addEventListener("popstate", () => {
|
|
286
|
+
const newPath = window.location.pathname;
|
|
287
|
+
// Send event to actor for validation
|
|
288
|
+
actor.send({ type: "play.route", to: newPath });
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Actor handles event:**
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
dashboard: {
|
|
296
|
+
// Always-guard validates state entry
|
|
297
|
+
always: [
|
|
298
|
+
{
|
|
299
|
+
target: "login",
|
|
300
|
+
guard: ({ context }) => !context.isAuthenticated,
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Proof:**
|
|
307
|
+
|
|
308
|
+
1. User presses browser back button
|
|
309
|
+
2. `popstate` event fires
|
|
310
|
+
3. `SignalSyncedHistory` sends `{ type: "play.route", to: "/" }` to actor
|
|
311
|
+
4. Actor processes event - always-guards validate state entry
|
|
312
|
+
5. If guards allow, state updates
|
|
313
|
+
6. If guards reject, automatic redirect to valid state
|
|
314
|
+
|
|
315
|
+
**Test Reference:** `test/invariants/state-driven.test.ts`
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// Login to dashboard
|
|
319
|
+
actor.send({ type: "auth.login", username: "test", password: "pass" });
|
|
320
|
+
assert.strictEqual(actor.getSnapshot().matches("dashboard"), true);
|
|
321
|
+
|
|
322
|
+
// Simulate browser back button: send play.route event
|
|
323
|
+
actor.send({ type: "play.route", to: "/" });
|
|
324
|
+
|
|
325
|
+
// Actor processes event - always-guards validate state entry
|
|
326
|
+
assert.strictEqual(actor.getSnapshot().matches("home"), true);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Run Test:**
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
cd examples/demo-app
|
|
333
|
+
npm test -- test/invariants/state-driven.test.ts
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Why This Matters
|
|
337
|
+
|
|
338
|
+
- **Security:** Browser back can't bypass authentication guards
|
|
339
|
+
- **Consistency:** Navigation validation logic centralized in actor
|
|
340
|
+
- **Predictability:** Actor state is single source of truth, not URL
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Why These Invariants Matter
|
|
345
|
+
|
|
346
|
+
### Traditional Approach Problems
|
|
347
|
+
|
|
348
|
+
**Example: Traditional React + React Router app**
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// Component controls navigation directly
|
|
352
|
+
function LoginForm() {
|
|
353
|
+
const navigate = useNavigate();
|
|
354
|
+
const [user, setUser] = useState(null);
|
|
355
|
+
|
|
356
|
+
const handleLogin = async () => {
|
|
357
|
+
const result = await api.login(username, password);
|
|
358
|
+
setUser(result.user); // Business state in React state
|
|
359
|
+
navigate("/dashboard"); // Component decides navigation
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return <form onSubmit={handleLogin}>...</form>;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Protected route check in router config
|
|
366
|
+
<Route
|
|
367
|
+
path="/dashboard"
|
|
368
|
+
element={
|
|
369
|
+
<ProtectedRoute>
|
|
370
|
+
<Dashboard />
|
|
371
|
+
</ProtectedRoute>
|
|
372
|
+
}
|
|
373
|
+
/>;
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Problems:**
|
|
377
|
+
|
|
378
|
+
1. **No Actor Authority:** `navigate("/dashboard")` bypasses authentication logic
|
|
379
|
+
2. **No Strict Separation:** Business logic (`api.login`, `setUser`) mixed with React
|
|
380
|
+
3. **No Signal-Only Reactivity:** Business state in `useState` - duplicated across components
|
|
381
|
+
4. **No Passive Infrastructure:** Component imports router and navigates directly
|
|
382
|
+
5. **No State-Driven Reset:** Browser back updates URL immediately - component reacts after
|
|
383
|
+
|
|
384
|
+
### Universal Player Architecture Solution
|
|
385
|
+
|
|
386
|
+
**Example: This demo app**
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// State machine defines behavior
|
|
390
|
+
const authMachine = createMachine({
|
|
391
|
+
states: {
|
|
392
|
+
loggedOut: {
|
|
393
|
+
on: {
|
|
394
|
+
"auth.login": { target: "loggedIn" },
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
loggedIn: {
|
|
398
|
+
// Always-guard validates state entry
|
|
399
|
+
always: [
|
|
400
|
+
{
|
|
401
|
+
target: "loggedOut",
|
|
402
|
+
guard: ({ context }) => !context.isAuthenticated,
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Component forwards events
|
|
410
|
+
function LoginForm({ send }) {
|
|
411
|
+
const [username, setUsername] = useState("");
|
|
412
|
+
const [password, setPassword] = useState("");
|
|
413
|
+
|
|
414
|
+
const handleSubmit = () => {
|
|
415
|
+
send({ type: "auth.login", username, password });
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
return <form onSubmit={handleSubmit}>...</form>;
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Benefits:**
|
|
423
|
+
|
|
424
|
+
1. **Actor Authority:** Always-guard validates state entry - component can't bypass
|
|
425
|
+
2. **Strict Separation:** State machine has zero React imports
|
|
426
|
+
3. **Signal-Only Reactivity:** Business state in actor signals
|
|
427
|
+
4. **Passive Infrastructure:** Component forwards events, doesn't navigate
|
|
428
|
+
5. **State-Driven Reset:** Browser back sends event to actor for validation via always-guards
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Running All Tests
|
|
433
|
+
|
|
434
|
+
Validate all 5 invariants:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
cd examples/demo-app
|
|
438
|
+
npm test
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Expected output:
|
|
442
|
+
|
|
443
|
+
```
|
|
444
|
+
✔ DEMO-02: Actor Authority - Guards reject invalid navigation
|
|
445
|
+
✔ DEMO-03: Strict Separation - Business logic has zero React imports
|
|
446
|
+
✔ DEMO-04: State-Driven Reset - Browser back sends event to actor
|
|
447
|
+
✔ DEMO-05: Passive Infrastructure - Components never import TanStack Router
|
|
448
|
+
✔ DEMO-06: Signal-Only Reactivity - PlayRenderer uses signals not React state
|
|
449
|
+
|
|
450
|
+
ℹ tests 5
|
|
451
|
+
ℹ pass 5
|
|
452
|
+
ℹ fail 0
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Next Steps
|
|
458
|
+
|
|
459
|
+
- **Line-by-Line Walkthrough:** Read [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed code explanations
|
|
460
|
+
- **Swap Frameworks:** Read [SWAP-REACT.md](./SWAP-REACT.md) to see how business logic remains unchanged when swapping React for Vue/Svelte
|
|
461
|
+
- **Build Your Own:** Use invariant tests as regression protection when building your application
|