@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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameter extraction utilities using URLPattern API
|
|
3
|
+
*
|
|
4
|
+
* Provides robust parameter extraction from URL paths and query strings.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract route parameters from URL path based on route pattern using URLPattern API
|
|
11
|
+
*
|
|
12
|
+
* @param pathname - Actual URL path (e.g., "/profile/123")
|
|
13
|
+
* @param pattern - Route pattern with params (e.g., "/profile/:userId")
|
|
14
|
+
* @returns Object with extracted parameters
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* extractParams("/profile/123", "/profile/:userId")
|
|
19
|
+
* // Returns: { userId: "123" }
|
|
20
|
+
*
|
|
21
|
+
* extractParams("/api/123", "/api/:id(\\d+)")
|
|
22
|
+
* // Returns: { id: "123" }
|
|
23
|
+
* // Validates numeric constraint via URLPattern
|
|
24
|
+
*
|
|
25
|
+
* extractParams("/docs/intro", "/docs/*")
|
|
26
|
+
* // Returns: { "0": "intro" }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function extractParams(pathname: string, pattern: string): Record<string, string> {
|
|
30
|
+
// Reuse existing implementation from signal-synced-history.ts
|
|
31
|
+
// URLPattern-based extraction with :param syntax
|
|
32
|
+
const params: Record<string, string> = {};
|
|
33
|
+
|
|
34
|
+
// Parse pattern and pathname to extract named segments
|
|
35
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
36
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
39
|
+
const patternPart = patternParts[i];
|
|
40
|
+
const pathPart = pathParts[i];
|
|
41
|
+
|
|
42
|
+
if (patternPart.startsWith(":")) {
|
|
43
|
+
const paramName = patternPart.slice(1).replace("?", "");
|
|
44
|
+
if (pathPart) {
|
|
45
|
+
params[paramName] = decodeURIComponent(pathPart);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return params;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract query parameters from search string
|
|
55
|
+
*
|
|
56
|
+
* @param search - Query string with or without leading ? (e.g., "?tab=settings" or "tab=settings")
|
|
57
|
+
* @returns Object mapping query parameter names to values
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* extractQueryParams("?tab=settings&theme=dark")
|
|
62
|
+
* // Returns: { tab: "settings", theme: "dark" }
|
|
63
|
+
*
|
|
64
|
+
* extractQueryParams("q=hello%20world")
|
|
65
|
+
* // Returns: { q: "hello world" } (auto-decoded)
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function extractQueryParams(search: string): Record<string, string> {
|
|
69
|
+
const params = new URLSearchParams(search);
|
|
70
|
+
const result: Record<string, string> = {};
|
|
71
|
+
params.forEach((value, key) => {
|
|
72
|
+
result[key] = value;
|
|
73
|
+
});
|
|
74
|
+
return result;
|
|
75
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xmachines/play-tanstack-react-router
|
|
3
|
+
*
|
|
4
|
+
* TanStack Router adapter for XMachines Play architecture.
|
|
5
|
+
* Synchronizes browser URL with actor state through passive infrastructure.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// New exports (primary API - RouterBridge pattern)
|
|
11
|
+
export { TanStackReactRouterBridge } from "./tanstack-router-bridge.js";
|
|
12
|
+
export { RouteMap, createRouteMapFromTree } from "./route-map.js";
|
|
13
|
+
export { extractParams, extractQueryParams } from "./extract-params.js";
|
|
14
|
+
export type { RouteMapping } from "./route-map.js";
|
|
15
|
+
export type { TanStackRouterLike } from "./tanstack-router-bridge.js";
|
|
16
|
+
|
|
17
|
+
// TanStack Router integration (React-only)
|
|
18
|
+
export {
|
|
19
|
+
PlayRouterProvider,
|
|
20
|
+
type PlayRouterProviderProps,
|
|
21
|
+
type TanStackRouterInstance,
|
|
22
|
+
} from "./play-router-provider.js";
|
|
23
|
+
|
|
24
|
+
// Re-export shared utilities for convenience
|
|
25
|
+
export { extractMachineRoutes, createRouteMap } from "@xmachines/play-router";
|
|
26
|
+
|
|
27
|
+
// Core routing utilities
|
|
28
|
+
export type { RouteNavigateEvent } from "./types.js";
|
|
29
|
+
|
|
30
|
+
// Re-exports from protocols
|
|
31
|
+
export type { RouterBridge, PlayRouteEvent } from "@xmachines/play-router";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlayRouterProvider — React convenience wrapper for TanStackReactRouterBridge
|
|
3
|
+
*
|
|
4
|
+
* Creates and connects a TanStackReactRouterBridge in a React component lifecycle.
|
|
5
|
+
* Actor state drives router navigation; router events send play.route to actor.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <PlayRouterProvider
|
|
10
|
+
* actor={actor}
|
|
11
|
+
* router={router}
|
|
12
|
+
* routeMap={routeMap}
|
|
13
|
+
* renderer={(currentActor, currentRouter) => {
|
|
14
|
+
* void currentRouter;
|
|
15
|
+
* return <PlayRenderer actor={currentActor} components={components} />;
|
|
16
|
+
* }}
|
|
17
|
+
* />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, type ReactNode } from "react";
|
|
21
|
+
import type { AbstractActor, Routable, Viewable } from "@xmachines/play-actor";
|
|
22
|
+
import type { AnyActorLogic } from "xstate";
|
|
23
|
+
import { TanStackReactRouterBridge } from "./tanstack-router-bridge.js";
|
|
24
|
+
import type { RouteMap } from "./route-map.js";
|
|
25
|
+
|
|
26
|
+
export type TanStackRouterInstance = ConstructorParameters<typeof TanStackReactRouterBridge>[0];
|
|
27
|
+
|
|
28
|
+
export interface PlayRouterProviderProps {
|
|
29
|
+
actor: AbstractActor<AnyActorLogic> & Routable & Viewable;
|
|
30
|
+
router: TanStackRouterInstance;
|
|
31
|
+
routeMap: RouteMap;
|
|
32
|
+
renderer: (
|
|
33
|
+
actor: AbstractActor<AnyActorLogic> & Routable & Viewable,
|
|
34
|
+
router: TanStackRouterInstance,
|
|
35
|
+
) => ReactNode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function PlayRouterProvider({ actor, router, routeMap, renderer }: PlayRouterProviderProps) {
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const bridge = new TanStackReactRouterBridge(router, actor, routeMap);
|
|
41
|
+
bridge.connect();
|
|
42
|
+
return () => bridge.disconnect();
|
|
43
|
+
}, [actor, router, routeMap]);
|
|
44
|
+
|
|
45
|
+
return <>{renderer(actor, router)}</>;
|
|
46
|
+
}
|
package/src/route-map.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouteMap - Bidirectional mapping between state IDs and route paths
|
|
3
|
+
*
|
|
4
|
+
* Provides efficient lookup for:
|
|
5
|
+
* - State ID → Path (actor state changes → router navigation)
|
|
6
|
+
* - Path → State ID (router navigation → actor events)
|
|
7
|
+
*
|
|
8
|
+
* Supports both static paths and parameterized paths with :param syntax.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { RouteTree, RouteNode } from "@xmachines/play-router";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mapping between state machine state ID and router path
|
|
17
|
+
*/
|
|
18
|
+
export interface RouteMapping {
|
|
19
|
+
/** State ID from state machine (e.g., "settings.profile") */
|
|
20
|
+
stateId: string;
|
|
21
|
+
/** Router path with optional parameters (e.g., "/settings/:section?") */
|
|
22
|
+
path: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Bidirectional route mapper with pattern matching support
|
|
27
|
+
*
|
|
28
|
+
* Maps between state IDs (XMachines actor) and paths (router).
|
|
29
|
+
* Handles both exact matches (static paths) and pattern matches (parameterized paths).
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const routes: RouteMapping[] = [
|
|
34
|
+
* { stateId: "home", path: "/" },
|
|
35
|
+
* { stateId: "profile", path: "/profile/:userId" },
|
|
36
|
+
* { stateId: "settings", path: "/settings/:section?" }
|
|
37
|
+
* ];
|
|
38
|
+
*
|
|
39
|
+
* const routeMap = new RouteMap(routes);
|
|
40
|
+
*
|
|
41
|
+
* routeMap.getStateIdByPath("/profile/123"); // "profile"
|
|
42
|
+
* routeMap.getPathByStateId("home"); // "/"
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class RouteMap {
|
|
46
|
+
private stateIdToPath: Map<string, string>;
|
|
47
|
+
private pathToStateId: Map<string, string>;
|
|
48
|
+
private patterns: Array<{ pattern: RegExp; stateId: string; path: string }>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create RouteMap from route mappings
|
|
52
|
+
*
|
|
53
|
+
* @param routes - Array of state ID → path mappings
|
|
54
|
+
*/
|
|
55
|
+
constructor(routes: RouteMapping[]) {
|
|
56
|
+
this.stateIdToPath = new Map();
|
|
57
|
+
this.pathToStateId = new Map();
|
|
58
|
+
this.patterns = [];
|
|
59
|
+
|
|
60
|
+
for (const { stateId, path } of routes) {
|
|
61
|
+
this.stateIdToPath.set(stateId, path);
|
|
62
|
+
|
|
63
|
+
// Check if path has parameters
|
|
64
|
+
if (path.includes(":")) {
|
|
65
|
+
// Convert :param and :param? to regex
|
|
66
|
+
const pattern = path
|
|
67
|
+
.replace(/\/:[^/]+\?/g, "(?:/([^/]+))?") // Optional params with optional slash
|
|
68
|
+
.replace(/:[^/]+/g, "([^/]+)"); // Required params
|
|
69
|
+
this.patterns.push({
|
|
70
|
+
pattern: new RegExp(`^${pattern}$`),
|
|
71
|
+
stateId,
|
|
72
|
+
path,
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
// Exact match for static paths
|
|
76
|
+
this.pathToStateId.set(path, stateId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get state ID from router path
|
|
83
|
+
*
|
|
84
|
+
* Tries exact match first, then pattern match for parameterized paths.
|
|
85
|
+
*
|
|
86
|
+
* @param path - Router path (e.g., "/profile/123")
|
|
87
|
+
* @returns State ID or null if no match
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* routeMap.getStateIdByPath("/"); // "home" (exact match)
|
|
92
|
+
* routeMap.getStateIdByPath("/profile/123"); // "profile" (pattern match)
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
getStateIdByPath(path: string): string | null {
|
|
96
|
+
// Try exact match first
|
|
97
|
+
const exactMatch = this.pathToStateId.get(path);
|
|
98
|
+
if (exactMatch) return exactMatch;
|
|
99
|
+
|
|
100
|
+
// Try pattern match
|
|
101
|
+
for (const { pattern, stateId } of this.patterns) {
|
|
102
|
+
if (pattern.test(path)) return stateId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get router path from state ID
|
|
110
|
+
*
|
|
111
|
+
* Returns the route pattern with parameter placeholders (not substituted).
|
|
112
|
+
*
|
|
113
|
+
* @param stateId - State machine state ID
|
|
114
|
+
* @returns Router path or null if no match
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* routeMap.getPathByStateId("home"); // "/"
|
|
119
|
+
* routeMap.getPathByStateId("profile"); // "/profile/:userId"
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
getPathByStateId(stateId: string): string | null {
|
|
123
|
+
return this.stateIdToPath.get(stateId) || null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create RouteMap from RouteTree
|
|
129
|
+
*
|
|
130
|
+
* Converts XMachines route tree to RouteMap for use with router adapters.
|
|
131
|
+
*
|
|
132
|
+
* @param routeTree - Route tree from extractMachineRoutes()
|
|
133
|
+
* @returns RouteMap instance
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* import { extractMachineRoutes } from '@xmachines/play-router';
|
|
138
|
+
* import { createRouteMapFromTree } from '@xmachines/play-tanstack-react-router';
|
|
139
|
+
*
|
|
140
|
+
* const routeTree = extractMachineRoutes(machine);
|
|
141
|
+
* const routeMap = createRouteMapFromTree(routeTree);
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export function createRouteMapFromTree(routeTree: RouteTree): RouteMap {
|
|
145
|
+
const routes: RouteMapping[] = [];
|
|
146
|
+
|
|
147
|
+
function traverse(node: RouteNode) {
|
|
148
|
+
if (node.id && node.path) {
|
|
149
|
+
routes.push({ stateId: node.id, path: node.path });
|
|
150
|
+
}
|
|
151
|
+
if (node.children) {
|
|
152
|
+
node.children.forEach(traverse);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
traverse(routeTree.root);
|
|
157
|
+
return new RouteMap(routes);
|
|
158
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStackReactRouterBridge - TanStack React Router adapter implementing RouterBridge protocol
|
|
3
|
+
*
|
|
4
|
+
* Provides bidirectional synchronization between XMachines actor state
|
|
5
|
+
* and TanStack React Router. Extends RouterBridgeBase which handles all
|
|
6
|
+
* common lifecycle, sync, and circular-update prevention logic.
|
|
7
|
+
*
|
|
8
|
+
* Only the 3 TanStack-specific methods need to be implemented:
|
|
9
|
+
* - navigateRouter: calls router.navigate({ to: path })
|
|
10
|
+
* - watchRouterChanges: subscribes to router.history for ALL navigation types
|
|
11
|
+
* - unwatchRouterChanges: calls the unsubscribe function
|
|
12
|
+
*
|
|
13
|
+
* Architectural Invariants:
|
|
14
|
+
* - INV-02 (Passive Infrastructure): Router reflects actor state, never decides
|
|
15
|
+
* - Actor validates all navigation via guards before URL changes
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { createRouter } from '@tanstack/react-router';
|
|
22
|
+
* import { definePlayer } from '@xmachines/play-xstate';
|
|
23
|
+
* import { TanStackReactRouterBridge, createRouteMapFromTree } from '@xmachines/play-tanstack-react-router';
|
|
24
|
+
*
|
|
25
|
+
* const router = createRouter({ routes });
|
|
26
|
+
* const actor = definePlayer({ machine, catalog })();
|
|
27
|
+
* const routeMap = createRouteMapFromTree(extractMachineRoutes(machine));
|
|
28
|
+
*
|
|
29
|
+
* const bridge = new TanStackReactRouterBridge(router, actor, routeMap);
|
|
30
|
+
* bridge.connect();
|
|
31
|
+
*
|
|
32
|
+
* // Cleanup on unmount
|
|
33
|
+
* return () => bridge.disconnect();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { RouterBridgeBase } from "@xmachines/play-router";
|
|
38
|
+
import type { AbstractActor, Routable } from "@xmachines/play-actor";
|
|
39
|
+
import type { AnyActorLogic } from "xstate";
|
|
40
|
+
import type { RouteMap } from "./route-map.js";
|
|
41
|
+
|
|
42
|
+
export type TanStackRouterLike = {
|
|
43
|
+
navigate(args: { to: string }): void;
|
|
44
|
+
state?: { location?: { pathname?: string } };
|
|
45
|
+
history: {
|
|
46
|
+
subscribe(
|
|
47
|
+
handler: (event: { location: { pathname: string; search?: string } }) => void,
|
|
48
|
+
): () => void;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* TanStack React Router adapter implementing RouterBridge protocol via RouterBridgeBase
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* Extends RouterBridgeBase to handle all common lifecycle and sync logic.
|
|
57
|
+
* Only 3 TanStack-specific methods are implemented here.
|
|
58
|
+
*
|
|
59
|
+
* Subscribes to router.history (not router.subscribe("onBeforeLoad")) so that
|
|
60
|
+
* back/forward browser navigation (popstate events) are also captured. The
|
|
61
|
+
* router.subscribe("onBeforeLoad") approach only works when TanStack's
|
|
62
|
+
* Transitioner component is mounted (i.e. inside a full <RouterProvider>).
|
|
63
|
+
* Since this bridge is used without <RouterProvider>, we must subscribe to
|
|
64
|
+
* the underlying history object directly — which is exactly what Transitioner
|
|
65
|
+
* does internally: `router.history.subscribe(router.load)`.
|
|
66
|
+
*
|
|
67
|
+
* Architectural Invariants:
|
|
68
|
+
* - INV-02 (Passive Infrastructure): Router reflects actor state, never decides
|
|
69
|
+
* - Actor validates all navigation via guards before URL changes
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const bridge = new TanStackReactRouterBridge(router, actor, routeMap);
|
|
74
|
+
* bridge.connect();
|
|
75
|
+
* return () => bridge.disconnect();
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export class TanStackReactRouterBridge extends RouterBridgeBase {
|
|
79
|
+
private routerUnsubscribe: (() => void) | null = null;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create TanStack React Router bridge
|
|
83
|
+
*
|
|
84
|
+
* @param router - TanStack React Router instance
|
|
85
|
+
* @param actor - XMachines actor instance
|
|
86
|
+
* @param routeMap - Bidirectional mapping between state IDs and paths
|
|
87
|
+
*/
|
|
88
|
+
constructor(
|
|
89
|
+
private readonly router: TanStackRouterLike,
|
|
90
|
+
actor: AbstractActor<AnyActorLogic> & Routable,
|
|
91
|
+
routeMap: RouteMap,
|
|
92
|
+
) {
|
|
93
|
+
// Adapt RouteMap's null returns to undefined for RouterBridgeBase compatibility
|
|
94
|
+
super(actor, {
|
|
95
|
+
getStateIdByPath: (path: string) => routeMap.getStateIdByPath(path) ?? undefined,
|
|
96
|
+
getPathByStateId: (id: string) => routeMap.getPathByStateId(id) ?? undefined,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
protected navigateRouter(path: string): void {
|
|
101
|
+
this.router.navigate({ to: path });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read the router's current pathname for initial sync.
|
|
106
|
+
*
|
|
107
|
+
* Called once in connect() to handle cold-load / deep-link scenarios where
|
|
108
|
+
* the URL differs from the actor's initial state.
|
|
109
|
+
*/
|
|
110
|
+
protected override getInitialRouterPath(): string | null {
|
|
111
|
+
return this.router.state?.location?.pathname ?? null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Subscribe to ALL navigation events via router.history.
|
|
116
|
+
*
|
|
117
|
+
* router.history.subscribe fires for PUSH, POP, BACK, FORWARD, REPLACE, and GO —
|
|
118
|
+
* covering both link clicks and browser back/forward button presses.
|
|
119
|
+
*
|
|
120
|
+
* The subscriber callback receives { location, action } where location is the
|
|
121
|
+
* new history location with pathname and search already updated.
|
|
122
|
+
*/
|
|
123
|
+
protected watchRouterChanges(): void {
|
|
124
|
+
this.routerUnsubscribe = this.router.history.subscribe(
|
|
125
|
+
({ location }: { location: { pathname: string; search?: string } }) => {
|
|
126
|
+
this.syncActorFromRouter(location.pathname, location.search ?? "");
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
protected unwatchRouterChanges(): void {
|
|
132
|
+
this.routerUnsubscribe?.();
|
|
133
|
+
this.routerUnsubscribe = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for TanStack Router integration with Play Architecture.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Event sent to actor when browser navigates.
|
|
9
|
+
*
|
|
10
|
+
* Browser navigation (user clicks link, back/forward button, direct URL entry)
|
|
11
|
+
* sends this event to the actor. The actor's guards determine if navigation
|
|
12
|
+
* is allowed based on business logic.
|
|
13
|
+
*
|
|
14
|
+
* Invariant: Passive Infrastructure - Router suggests, actor decides validity.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* actor.send({ type: 'route.navigate', path: '/dashboard' });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export interface RouteNavigateEvent {
|
|
22
|
+
/** Event type discriminant (extends XState event pattern) */
|
|
23
|
+
readonly type: "route.navigate";
|
|
24
|
+
/** Target route path (may include parameters, e.g., /posts/123) */
|
|
25
|
+
readonly path: string;
|
|
26
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility barrel exports
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Parameter extraction utilities
|
|
8
|
+
export { extractParams, extractQueryParams } from "./extract-params.js";
|
|
9
|
+
|
|
10
|
+
// RouteMap utilities
|
|
11
|
+
export { RouteMap, createRouteMapFromTree } from "./route-map.js";
|
|
12
|
+
export type { RouteMapping } from "./route-map.js";
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import { Signal } from "@xmachines/play-signals";
|
|
3
|
+
import { SignalSyncedHistory } from "../../src/signal-synced-history.js";
|
|
4
|
+
|
|
5
|
+
test("SignalSyncedHistory syncs actor route to browser URL", async () => {
|
|
6
|
+
const currentRoute = new Signal.State("/home");
|
|
7
|
+
|
|
8
|
+
const mockActor = {
|
|
9
|
+
currentRoute,
|
|
10
|
+
send: () => {},
|
|
11
|
+
} as any;
|
|
12
|
+
|
|
13
|
+
const history = new SignalSyncedHistory(mockActor, "/home");
|
|
14
|
+
|
|
15
|
+
// Initial URL should match signal (via memory history)
|
|
16
|
+
const memoryHistory = history.getHistory();
|
|
17
|
+
expect(memoryHistory.location.pathname).toBe("/home");
|
|
18
|
+
|
|
19
|
+
// Update signal
|
|
20
|
+
currentRoute.set("/dashboard");
|
|
21
|
+
|
|
22
|
+
// Wait for microtask batch
|
|
23
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
24
|
+
|
|
25
|
+
// Memory history should update
|
|
26
|
+
expect(memoryHistory.location.pathname).toBe("/dashboard");
|
|
27
|
+
|
|
28
|
+
history.dispose();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("Browser back button sends route.navigate event to actor", async () => {
|
|
32
|
+
const currentRoute = new Signal.State("/page1");
|
|
33
|
+
const sentEvents: any[] = [];
|
|
34
|
+
|
|
35
|
+
const mockActor = {
|
|
36
|
+
currentRoute,
|
|
37
|
+
send: (event: any) => {
|
|
38
|
+
sentEvents.push(event);
|
|
39
|
+
},
|
|
40
|
+
} as any;
|
|
41
|
+
|
|
42
|
+
// Initialize with matching path to avoid initial sync
|
|
43
|
+
const history = new SignalSyncedHistory(mockActor, "/page1");
|
|
44
|
+
const memoryHistory = history.getHistory();
|
|
45
|
+
|
|
46
|
+
// Clear any initialization events
|
|
47
|
+
sentEvents.length = 0;
|
|
48
|
+
|
|
49
|
+
// Navigate to page2 through memory history (simulating user navigation)
|
|
50
|
+
memoryHistory.push("/page2");
|
|
51
|
+
|
|
52
|
+
// Wait for navigation
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
54
|
+
|
|
55
|
+
// Verify event sent to actor
|
|
56
|
+
const navEvents = sentEvents.filter((e) => e.type === "route.navigate");
|
|
57
|
+
expect(navEvents.length).toBeGreaterThan(0);
|
|
58
|
+
expect(navEvents[0].path).toBe("/page2");
|
|
59
|
+
|
|
60
|
+
// Navigate back
|
|
61
|
+
memoryHistory.back();
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
63
|
+
|
|
64
|
+
// Should have sent another event for going back to /page1
|
|
65
|
+
expect(sentEvents.length).toBeGreaterThan(1);
|
|
66
|
+
|
|
67
|
+
history.dispose();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("SignalSyncedHistory prevents circular updates", async () => {
|
|
71
|
+
const currentRoute = new Signal.State("/initial");
|
|
72
|
+
const sentEvents: any[] = [];
|
|
73
|
+
|
|
74
|
+
const mockActor = {
|
|
75
|
+
currentRoute,
|
|
76
|
+
send: (event: any) => {
|
|
77
|
+
sentEvents.push(event);
|
|
78
|
+
},
|
|
79
|
+
} as any;
|
|
80
|
+
|
|
81
|
+
// Initialize with matching path to avoid initial sync
|
|
82
|
+
const history = new SignalSyncedHistory(mockActor, "/initial");
|
|
83
|
+
|
|
84
|
+
// Clear any initialization events
|
|
85
|
+
sentEvents.length = 0;
|
|
86
|
+
|
|
87
|
+
// Update signal directly (actor decision)
|
|
88
|
+
currentRoute.set("/new-path");
|
|
89
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
90
|
+
|
|
91
|
+
// Should not trigger send events (actor initiated the change)
|
|
92
|
+
expect(sentEvents.length).toBe(0); // No circular events
|
|
93
|
+
|
|
94
|
+
history.dispose();
|
|
95
|
+
});
|