From b9887c6eb25e7828471ed35ed3a2a5349f5f4658 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Thu, 7 Aug 2025 03:41:39 -0400 Subject: [PATCH 1/9] initial work on converting to tanstack router --- .../springboard/core/engine/module_api.ts | 11 +- .../core/module_registry/module_registry.tsx | 3 +- .../platforms/webapp/frontend_routes.tsx | 177 ++++-------------- .../springboard/platforms/webapp/package.json | 6 +- .../platforms/webapp/root_route.tsx | 10 + .../platforms/webapp/test_tanstack_module.tsx | 100 ++++++++++ pnpm-lock.yaml | 105 +++++++++++ 7 files changed, 263 insertions(+), 149 deletions(-) create mode 100644 packages/springboard/platforms/webapp/root_route.tsx create mode 100644 packages/springboard/platforms/webapp/test_tanstack_module.tsx diff --git a/packages/springboard/core/engine/module_api.ts b/packages/springboard/core/engine/module_api.ts index b2d24883..a3adf739 100644 --- a/packages/springboard/core/engine/module_api.ts +++ b/packages/springboard/core/engine/module_api.ts @@ -88,16 +88,7 @@ export class ModuleAPI { * */ registerRoute = (routePath: string, options: RegisterRouteOptions, component: RegisteredRoute['component']) => { - const routes = this.module.routes || {}; - routes[routePath] = { - options, - component, - }; - - this.module.routes = {...routes}; - if (this.modDeps.moduleRegistry.getCustomModule(this.module.moduleId)) { - this.modDeps.moduleRegistry.refreshModules(); - } + console.error('registerRoute is not supported in tanstack router'); }; registerApplicationShell = (component: React.ElementType>) => { diff --git a/packages/springboard/core/module_registry/module_registry.tsx b/packages/springboard/core/module_registry/module_registry.tsx index b47202f1..8c65a743 100644 --- a/packages/springboard/core/module_registry/module_registry.tsx +++ b/packages/springboard/core/module_registry/module_registry.tsx @@ -4,6 +4,7 @@ import {Subject} from 'rxjs'; import type {ModuleAPI} from '../engine/module_api'; import {RegisterRouteOptions} from '../engine/register'; +import type {Route} from '@tanstack/react-router'; type RouteComponentProps = { navigate: (routeName: string) => void; @@ -26,7 +27,7 @@ export type Module = { Provider?: React.ElementType; state?: State; subject?: Subject; - routes?: Record; + routes?: Route[]; applicationShell?: React.ElementType>; }; diff --git a/packages/springboard/platforms/webapp/frontend_routes.tsx b/packages/springboard/platforms/webapp/frontend_routes.tsx index 37078f64..97d0283e 100644 --- a/packages/springboard/platforms/webapp/frontend_routes.tsx +++ b/packages/springboard/platforms/webapp/frontend_routes.tsx @@ -1,157 +1,62 @@ import React from 'react'; import { - createBrowserRouter, - createHashRouter, - Link, - RouteObject, RouterProvider, - useNavigate, -} from 'react-router-dom'; + createRouter, +} from '@tanstack/react-router'; import {useSpringboardEngine} from 'springboard/engine/engine'; -import {Module, RegisteredRoute} from 'springboard/module_registry/module_registry'; - -import {Layout} from './layout'; - -const CustomRoute = (props: {component: RegisteredRoute['component']}) => { - const navigate = useNavigate(); +import {AllModules} from 'springboard/module_registry/module_registry'; +import {rootRoute} from './root_route'; + +// utilities for extracting and typing routes from modules +type ExtractRoutes = T extends {routes: infer R} ? R : + T extends () => Promise<{routes: infer R}> ? R : never; +type Flatten = T extends readonly (infer U)[] ? U : never; +type AllRoutes = { + [K in keyof AllModules]: ExtractRoutes; +}[keyof AllModules]; +type AllRoutesFlat = readonly Flatten[]; + +type x = AllModules['testTanStackModule']['routes']; + +// router factory function that creates a strongly-typed router based on AllModules +function createAppRouter(routes: AllRoutesFlat) { + const routeTree = rootRoute.addChildren(routes); + + return createRouter({ + routeTree, + context: {}, + defaultPreload: 'intent', + scrollRestoration: true, + defaultStructuralSharing: true, + defaultPreloadStaleTime: 0, + }); +} - return ( - - ); -}; +type AppRouter = ReturnType; export const FrontendRoutes = () => { const engine = useSpringboardEngine(); - const mods = engine.moduleRegistry.useModules(); - const moduleRoutes: RouteObject[] = []; - - const rootRouteObjects: RouteObject[] = []; + const allModuleRoutes: any[] = []; for (const mod of mods) { - if (!mod.routes) { - continue; - } - - const routes = mod.routes; - - const thisModRoutes: RouteObject[] = []; - - Object.keys(routes).forEach(path => { - const Component = routes[path].component; - const routeObject: RouteObject = { - path, - element: ( - - - - ), - }; - - if (path.startsWith('/')) { - rootRouteObjects.push(routeObject); - } else { - thisModRoutes.push(routeObject); - } - }); - - if (thisModRoutes.length) { - moduleRoutes.push({ - path: mod.moduleId, - children: thisModRoutes, - }); + if (mod.routes && mod.routes.length > 0) { + allModuleRoutes.push(...mod.routes); } } - moduleRoutes.push({ - path: '*', - element: , - }); + const typedRoutes = allModuleRoutes as unknown as AllRoutesFlat; - const routerContructor = (globalThis as {useHashRouter?: boolean}).useHashRouter ? createHashRouter : createBrowserRouter; + const router = createAppRouter(typedRoutes); - const allRoutes: RouteObject[] = [ - ...rootRouteObjects, - { - path: '/modules', - children: moduleRoutes, - }, - { - path: '/routes', - element: - }, - ]; - - if (!rootRouteObjects.find(r => r.path === '/')) { - allRoutes.push({ - path: '/', - element: - }); - } - - const router = routerContructor(allRoutes, { - future: { - v7_relativeSplatPath: true, - // v7_startTransition: true, - }, - }); - - return ( - - ); -}; - -const RootPath = (props: {modules: Module[]}) => { - return ( -
    - {props.modules.map(mod => ( - - ))} -
- ); + return ; }; -const RenderModuleRoutes = ({mod}: {mod: Module}) => { - return ( -
  • - {mod.moduleId} -
      - {mod.routes && Object.keys(mod.routes).map(path => { - let suffix = ''; - if (path && path !== '/') { - if (!path.startsWith('/')) { - suffix += '/'; - } - - if (path.endsWith('/')) { - suffix += path.substring(0, path.length - 1); - } else { - suffix += path; - } - } - - const href = path.startsWith('/') ? path : `/modules/${mod.moduleId}${suffix}`; - - return ( -
    • - - {path || '/'} - -
    • - ); - })} -
    -
  • - ); -}; +declare module '@tanstack/react-router' { + interface Register { + router: AppRouter; + } +} diff --git a/packages/springboard/platforms/webapp/package.json b/packages/springboard/platforms/webapp/package.json index 02141e5c..6e5130b7 100644 --- a/packages/springboard/platforms/webapp/package.json +++ b/packages/springboard/platforms/webapp/package.json @@ -7,14 +7,16 @@ "fix": "npm run lint -- --fix" }, "peerDependencies": { - "springboard": "workspace:*", - "react-router-dom": "^6" + "@tanstack/react-router": "^1.130.12", + "react-router-dom": "^6", + "springboard": "workspace:*" }, "dependencies": { "json-rpc-2.0": "catalog:", "reconnecting-websocket": "catalog:" }, "devDependencies": { + "@tanstack/react-router": "^1.130.12", "@types/react": "catalog:", "@types/react-dom": "catalog:", "react": "catalog:", diff --git a/packages/springboard/platforms/webapp/root_route.tsx b/packages/springboard/platforms/webapp/root_route.tsx new file mode 100644 index 00000000..7a85b8d3 --- /dev/null +++ b/packages/springboard/platforms/webapp/root_route.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import {createRootRoute, Outlet} from '@tanstack/react-router'; + +export const rootRoute = createRootRoute({ + component: () => ( + <> + + + ), +}); diff --git a/packages/springboard/platforms/webapp/test_tanstack_module.tsx b/packages/springboard/platforms/webapp/test_tanstack_module.tsx new file mode 100644 index 00000000..8f9fb3d4 --- /dev/null +++ b/packages/springboard/platforms/webapp/test_tanstack_module.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import {createRoute, getRouteApi, useRouter} from '@tanstack/react-router'; +import springboard from 'springboard'; +import {rootRoute} from './root_route'; +import {ModuleAPI} from 'springboard/engine/module_api'; + +const makeTestTanStackModule = async (moduleAPI: ModuleAPI) => { + const messageState = await moduleAPI.statesAPI.createPersistentState('testMessage', 'Hello from TanStack Router!'); + + const actions = moduleAPI.createActions({ + updateMessage: async (args: {newMessage: string}) => { + messageState.setState(args.newMessage); + }, + }); + + return { + routes: [ + createRoute({ + getParentRoute: () => rootRoute, + path: '/tanstack-test', + component: () => { + return ( + + ); + }, + }), + createRoute({ + getParentRoute: () => rootRoute, + path: '/tanstack-test-with-search', + component: () => { + const route = getRouteApi('/tanstack-test-with-search'); + const search = route.useSearch(); + + return ( +
    +

    TanStack Test with Search

    +

    Query: {search.query || 'none'}

    +

    Has Discount: {search.hasDiscount ? 'Yes' : 'No'}

    +
    + ); + }, + validateSearch: (search) => ({ + query: (search.query as string) || '', + hasDiscount: search.hasDiscount === 'true', + }), + }) + ], + }; +}; + +type TestTanStackModule = Awaited>; + +springboard.registerModule('TestTanStackModule', {}, makeTestTanStackModule); + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + testTanStackModule: TestTanStackModule; + } +} + +type TestTanStackComponentProps = { + message: string; + updateMessage: (args: {newMessage: string}) => void; +}; + +const TestTanStackComponent = (props: TestTanStackComponentProps) => { + const [inputValue, setInputValue] = React.useState(''); + + const router = useRouter(); + router.navigate({to: '/tanstack-test'}); + + return ( +
    +

    TanStack Router Test Module

    +

    Current message: {props.message}

    + +
    + setInputValue(e.target.value)} + placeholder="Enter new message" + style={{marginRight: '10px', padding: '5px'}} + /> + +
    +
    + ); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e242e13..8695b6bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -647,6 +647,9 @@ importers: specifier: workspace:* version: link:../../core devDependencies: + '@tanstack/react-router': + specifier: ^1.130.12 + version: 1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/react': specifier: 'catalog:' version: 19.1.5 @@ -1497,6 +1500,30 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@tanstack/history@1.130.12': + resolution: {integrity: sha512-2VO1nNFDWojgZ7Uqv/OJfH6LphZQ1kE6l8sI3YBgSPtj3qN6I/rsoTHW9rGjwiDO8sQoDRXod2hpH6HMs5NDsw==} + engines: {node: '>=12'} + + '@tanstack/react-router@1.130.12': + resolution: {integrity: sha512-7BYgOpGc1vK8MH1LIFLLBudGpH46GQy+hewnP7dNQJ4KHmkwPHv958L1IMA9jU/rs5g1ZH5n1f33BAMOBXUMYQ==} + engines: {node: '>=12'} + peerDependencies: + react: ^19.1.0 + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.7.3': + resolution: {integrity: sha512-3Dnqtbw9P2P0gw8uUM8WP2fFfg8XMDSZCTsywRPZe/XqqYW8PGkXKZTvP0AHkE4mpqP9Y43GpOg9vwO44azu6Q==} + peerDependencies: + react: ^19.1.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.130.12': + resolution: {integrity: sha512-emq3cRU9Na1hnIToojzkfJcOZm/MG2bv9M+Kr/elUxEf83enGEwQXC1EKezTuwNgeJrOv8vPJdEhWM7IQodnHQ==} + engines: {node: '>=12'} + + '@tanstack/store@0.7.2': + resolution: {integrity: sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==} + '@tauri-apps/api@2.5.0': resolution: {integrity: sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==} @@ -2131,6 +2158,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -2873,6 +2903,10 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbot@5.1.29: + resolution: {integrity: sha512-DelDWWoa3mBoyWTq3wjp+GIWx/yZdN7zLUE7NFhKjAiJ+uJVRkbLlwykdduCE4sPUUy8mlTYTmdhBUYu91F+sw==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3634,6 +3668,16 @@ packages: engines: {node: '>=10'} hasBin: true + seroval-plugins@1.3.2: + resolution: {integrity: sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.3.2: + resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} + engines: {node: '>=10'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -3843,6 +3887,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4108,6 +4158,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^19.1.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5124,6 +5179,38 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@tanstack/history@1.130.12': {} + + '@tanstack/react-router@1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/history': 1.130.12 + '@tanstack/react-store': 0.7.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/router-core': 1.130.12 + isbot: 5.1.29 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/react-store@0.7.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/store': 0.7.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + use-sync-external-store: 1.5.0(react@19.1.0) + + '@tanstack/router-core@1.130.12': + dependencies: + '@tanstack/history': 1.130.12 + '@tanstack/store': 0.7.2 + cookie-es: 1.2.2 + seroval: 1.3.2 + seroval-plugins: 1.3.2(seroval@1.3.2) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/store@0.7.2': {} + '@tauri-apps/api@2.5.0': {} '@tauri-apps/plugin-shell@2.2.1': @@ -5908,6 +5995,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} + cookie@0.7.2: {} cosmiconfig@7.1.0: @@ -6788,6 +6877,8 @@ snapshots: isarray@2.0.5: {} + isbot@5.1.29: {} + isexe@2.0.0: {} isomorphic-ws@4.0.1(ws@8.18.2): @@ -7647,6 +7738,12 @@ snapshots: semver@7.7.2: {} + seroval-plugins@1.3.2(seroval@1.3.2): + dependencies: + seroval: 1.3.2 + + seroval@1.3.2: {} + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -7893,6 +7990,10 @@ snapshots: dependencies: any-promise: 1.3.0 + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -8165,6 +8266,10 @@ snapshots: optionalDependencies: '@types/react': 19.1.5 + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + util-deprecate@1.0.2: {} vite-node@2.1.9(@types/node@22.15.21): From 1f1afa4051b22433a7277ee741012c572df840af Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Thu, 7 Aug 2025 03:46:51 -0400 Subject: [PATCH 2/9] add test tanstack app --- .../test_tanstack_app/test_tanstack_app.tsx | 1 + package.json | 3 ++- .../platforms/webapp/test_tanstack_module.tsx | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 apps/small_apps/test_tanstack_app/test_tanstack_app.tsx diff --git a/apps/small_apps/test_tanstack_app/test_tanstack_app.tsx b/apps/small_apps/test_tanstack_app/test_tanstack_app.tsx new file mode 100644 index 00000000..64aa47ca --- /dev/null +++ b/apps/small_apps/test_tanstack_app/test_tanstack_app.tsx @@ -0,0 +1 @@ +import '@springboardjs/platforms-browser/test_tanstack_module'; diff --git a/package.json b/package.json index 0ca2fee8..69d2f74e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "heroku-postbuild": "NODE_ENV=production npm run build-saas", "build-desktop": "RUN_SIDECAR_FROM_WEBVIEW=true npx tsx packages/springboard/cli/src/cli.ts build ./apps/small_apps/empty_jamtools_app/index.ts --platforms desktop", "build-all": "RUN_SIDECAR_FROM_WEBVIEW=true npx tsx packages/springboard/cli/src/cli.ts build ./apps/small_apps/empty_jamtools_app/index.ts --platforms all", - "splash-screen-app": "npx tsx packages/springboard/cli/src/cli.ts dev ./apps/small_apps/app_with_splash_screen/app_with_splash_screen.tsx" + "splash-screen-app": "npx tsx packages/springboard/cli/src/cli.ts dev ./apps/small_apps/app_with_splash_screen/app_with_splash_screen.tsx", + "test-tanstack-app": "npx tsx packages/springboard/cli/src/cli.ts dev ./apps/small_apps/test_tanstack_app/test_tanstack_app.tsx" }, "keywords": [], "author": "", diff --git a/packages/springboard/platforms/webapp/test_tanstack_module.tsx b/packages/springboard/platforms/webapp/test_tanstack_module.tsx index 8f9fb3d4..79a4cb12 100644 --- a/packages/springboard/platforms/webapp/test_tanstack_module.tsx +++ b/packages/springboard/platforms/webapp/test_tanstack_module.tsx @@ -15,6 +15,18 @@ const makeTestTanStackModule = async (moduleAPI: ModuleAPI) => { return { routes: [ + createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => { + return ( + + ); + }, + }), createRoute({ getParentRoute: () => rootRoute, path: '/tanstack-test', From a2c68069c341ba91c899a02364a746e72c44130a Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Thu, 7 Aug 2025 03:49:39 -0400 Subject: [PATCH 3/9] avoid navigating during render --- packages/springboard/platforms/webapp/test_tanstack_module.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/springboard/platforms/webapp/test_tanstack_module.tsx b/packages/springboard/platforms/webapp/test_tanstack_module.tsx index 79a4cb12..e76cc94e 100644 --- a/packages/springboard/platforms/webapp/test_tanstack_module.tsx +++ b/packages/springboard/platforms/webapp/test_tanstack_module.tsx @@ -82,13 +82,14 @@ const TestTanStackComponent = (props: TestTanStackComponentProps) => { const [inputValue, setInputValue] = React.useState(''); const router = useRouter(); - router.navigate({to: '/tanstack-test'}); return (

    TanStack Router Test Module

    Current message: {props.message}

    + +
    Date: Thu, 7 Aug 2025 03:50:12 -0400 Subject: [PATCH 4/9] don't run calude review on additional pr commits --- .github/workflows/claude-code-review.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 5bf8ce59..62a7f213 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -2,7 +2,7 @@ name: Claude Code Review on: pull_request: - types: [opened, synchronize] + types: [opened] # Optional: Only run on specific file changes # paths: # - "src/**/*.ts" @@ -17,14 +17,14 @@ jobs: # github.event.pull_request.user.login == 'external-contributor' || # github.event.pull_request.user.login == 'new-developer' || # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - + runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read id-token: write - + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -39,7 +39,7 @@ jobs: # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # model: "claude-opus-4-20250514" - + # Direct prompt for automated review (no @claude mention needed) direct_prompt: | Please review this pull request and provide feedback on: @@ -48,12 +48,12 @@ jobs: - Performance considerations - Security concerns - Test coverage - + Be constructive and helpful in your feedback. # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR # use_sticky_comment: true - + # Optional: Customize review based on file types # direct_prompt: | # Review this PR focusing on: @@ -61,18 +61,17 @@ jobs: # - For API endpoints: Security, input validation, and error handling # - For React components: Performance, accessibility, and best practices # - For tests: Coverage, edge cases, and test quality - + # Optional: Different prompts for different authors # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && + # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} - + # Optional: Add specific tools for running tests or linting # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" - + # Optional: Skip review for certain conditions # if: | # !contains(github.event.pull_request.title, '[skip-review]') && # !contains(github.event.pull_request.title, '[WIP]') - From a64c143f82c333511008401764ef27a0eed01f6e Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Thu, 7 Aug 2025 04:09:08 -0400 Subject: [PATCH 5/9] remove more references to react-router routes assumptions --- .../modules/macro_module/macro_module.tsx | 30 +++++-- packages/jamtools/core/package.json | 2 + .../modules/lighting/wled/wled_module.tsx | 26 +++--- packages/springboard/core/package.json | 2 + .../platforms/webapp/frontend_routes.tsx | 4 +- .../springboard/platforms/webapp/layout.tsx | 88 ++++++++++--------- .../springboard/platforms/webapp/package.json | 4 +- pnpm-lock.yaml | 11 ++- pnpm-workspace.yaml | 1 + 9 files changed, 98 insertions(+), 70 deletions(-) diff --git a/packages/jamtools/core/modules/macro_module/macro_module.tsx b/packages/jamtools/core/modules/macro_module/macro_module.tsx index a8d0f9ed..dbadc381 100644 --- a/packages/jamtools/core/modules/macro_module/macro_module.tsx +++ b/packages/jamtools/core/modules/macro_module/macro_module.tsx @@ -12,8 +12,11 @@ import springboard from 'springboard'; import {CapturedRegisterMacroTypeCall, MacroAPI, MacroCallback} from '@jamtools/core/modules/macro_module/registered_macro_types'; import {ModuleAPI} from 'springboard/engine/module_api'; +import {createRoute} from '@tanstack/react-router'; + import './macro_handlers'; import {macroTypeRegistry} from './registered_macro_types'; +import {rootRoute} from '@springboardjs/platforms-browser/root_route'; type ModuleId = string; @@ -52,14 +55,25 @@ export class MacroModule implements Module { constructor(private coreDeps: CoreDependencies, private moduleDeps: ModuleDependencies) { } - routes = { - '': { - component: () => { - const mod = MacroModule.use(); - return ; - }, - }, - }; + // routes = [ + // createRoute({ + // getParentRoute: () => rootRoute, + // path: '/modules/macro', + // component: () => { + // const mod = MacroModule.use(); + // return ; + // }, + // }), + // ]; + + // routes = { + // '': { + // component: () => { + // const mod = MacroModule.use(); + // return ; + // }, + // }, + // }; state: MacroConfigState = { configs: {}, diff --git a/packages/jamtools/core/package.json b/packages/jamtools/core/package.json index 9e01d557..5f054024 100644 --- a/packages/jamtools/core/package.json +++ b/packages/jamtools/core/package.json @@ -13,6 +13,7 @@ "module": "./src/index.ts", "peerDependencies": { "@springboardjs/platforms-browser": "workspace:*", + "@tanstack/react-router": "^1", "@tonejs/midi": "^2.0.0", "springboard": "workspace:*" }, @@ -25,6 +26,7 @@ }, "devDependencies": { "@springboardjs/platforms-browser": "workspace:*", + "@tanstack/react-router": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", "react": "catalog:", diff --git a/packages/jamtools/features/modules/lighting/wled/wled_module.tsx b/packages/jamtools/features/modules/lighting/wled/wled_module.tsx index a50ad5e0..d5d25695 100644 --- a/packages/jamtools/features/modules/lighting/wled/wled_module.tsx +++ b/packages/jamtools/features/modules/lighting/wled/wled_module.tsx @@ -47,19 +47,19 @@ export class WledModule implements Module { cleanup: (() => void)[] = []; - routes = { - '': { - component: () => { - const mod = WledModule.use(); - - return ( -
    -                        {JSON.stringify(mod.state)}
    -                    
    - ); - }, - }, - }; + // routes = { + // '': { + // component: () => { + // const mod = WledModule.use(); + + // return ( + //
    +    //                     {JSON.stringify(mod.state)}
    +    //                 
    + // ); + // }, + // }, + // }; // wled controllers need to be stored as hostnames, // so they are readable and stay consistent for that controller diff --git a/packages/springboard/core/package.json b/packages/springboard/core/package.json index 2a4aba16..93e3d540 100644 --- a/packages/springboard/core/package.json +++ b/packages/springboard/core/package.json @@ -31,6 +31,7 @@ "utils" ], "peerDependencies": { + "@tanstack/react-router": "^1", "@trpc/client": "^10.45.2", "json-rpc-2.0": "catalog:", "immer": "catalog:", @@ -45,6 +46,7 @@ "ws": "^8.18.0" }, "devDependencies": { + "@tanstack/react-router": "catalog:", "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", diff --git a/packages/springboard/platforms/webapp/frontend_routes.tsx b/packages/springboard/platforms/webapp/frontend_routes.tsx index 97d0283e..498dc703 100644 --- a/packages/springboard/platforms/webapp/frontend_routes.tsx +++ b/packages/springboard/platforms/webapp/frontend_routes.tsx @@ -11,15 +11,13 @@ import {rootRoute} from './root_route'; // utilities for extracting and typing routes from modules type ExtractRoutes = T extends {routes: infer R} ? R : - T extends () => Promise<{routes: infer R}> ? R : never; + T extends () => Promise<{routes: infer R}> ? R : never; type Flatten = T extends readonly (infer U)[] ? U : never; type AllRoutes = { [K in keyof AllModules]: ExtractRoutes; }[keyof AllModules]; type AllRoutesFlat = readonly Flatten[]; -type x = AllModules['testTanStackModule']['routes']; - // router factory function that creates a strongly-typed router based on AllModules function createAppRouter(routes: AllRoutesFlat) { const routeTree = rootRoute.addChildren(routes); diff --git a/packages/springboard/platforms/webapp/layout.tsx b/packages/springboard/platforms/webapp/layout.tsx index 26890a7d..d3b003d2 100644 --- a/packages/springboard/platforms/webapp/layout.tsx +++ b/packages/springboard/platforms/webapp/layout.tsx @@ -9,59 +9,61 @@ type Props = React.PropsWithChildren<{ }>; const useApplicationShell = (modules: Module[]) => { - const loc = useLocation(); - let pathname = loc.pathname; - if (!pathname.endsWith('/')) { - pathname += '/'; - } + // const loc = useLocation(); + // let pathname = loc.pathname; + // if (!pathname.endsWith('/')) { + // pathname += '/'; + // } - for (const mod of modules) { - if (!mod.routes) { - continue; - } + // for (const mod of modules) { + // if (!mod.routes) { + // continue; + // } - for (const route of Object.keys(mod.routes)) { - if (route.startsWith('/')) { - if (matchPath(route, loc.pathname)) { - const options = mod.routes[route].options; - if (options?.hideApplicationShell) { - return null; - } - } + // for (const route of Object.keys(mod.routes)) { + // if (route.startsWith('/')) { + // if (matchPath(route, loc.pathname)) { + // const options = mod.routes[route].options; + // if (options?.hideApplicationShell) { + // return null; + // } + // } - continue; - } + // continue; + // } - if (matchPath(`/modules/${mod.moduleId}/${route}`, loc.pathname)) { - const options = mod.routes[route].options; - if (options?.hideApplicationShell) { - return null; - } - } - } - } + // if (matchPath(`/modules/${mod.moduleId}/${route}`, loc.pathname)) { + // const options = mod.routes[route].options; + // if (options?.hideApplicationShell) { + // return null; + // } + // } + // } + // } - for (const mod of modules) { - if (mod.applicationShell) { - return mod.applicationShell; - } - } + // for (const mod of modules) { + // if (mod.applicationShell) { + // return mod.applicationShell; + // } + // } return null; }; export const Layout = (props: Props) => { - const ApplicationShell = useApplicationShell(props.modules); + return props.children; - if (!ApplicationShell) { - return props.children; - } + // const ApplicationShell = useApplicationShell(props.modules); - return ( - - {props.children} - - ); + // if (!ApplicationShell) { + // return props.children; + // } + + // return ( + // + // {props.children} + // + // ); }; diff --git a/packages/springboard/platforms/webapp/package.json b/packages/springboard/platforms/webapp/package.json index 6e5130b7..99cd5c01 100644 --- a/packages/springboard/platforms/webapp/package.json +++ b/packages/springboard/platforms/webapp/package.json @@ -7,7 +7,7 @@ "fix": "npm run lint -- --fix" }, "peerDependencies": { - "@tanstack/react-router": "^1.130.12", + "@tanstack/react-router": "^1", "react-router-dom": "^6", "springboard": "workspace:*" }, @@ -16,7 +16,7 @@ "reconnecting-websocket": "catalog:" }, "devDependencies": { - "@tanstack/react-router": "^1.130.12", + "@tanstack/react-router": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", "react": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8695b6bc..40064135 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,9 @@ catalogs: '@hono/trpc-server': specifier: ^0.3.2 version: 0.3.4 + '@tanstack/react-router': + specifier: ^1.130.12 + version: 1.130.12 '@trpc/client': specifier: ^10.45.2 version: 10.45.2 @@ -284,6 +287,9 @@ importers: '@springboardjs/platforms-browser': specifier: workspace:* version: link:../../springboard/platforms/webapp + '@tanstack/react-router': + specifier: 'catalog:' + version: 1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/react': specifier: 'catalog:' version: 19.1.5 @@ -421,6 +427,9 @@ importers: specifier: ^8.18.0 version: 8.18.2 devDependencies: + '@tanstack/react-router': + specifier: 'catalog:' + version: 1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/node': specifier: 'catalog:' version: 20.17.48 @@ -648,7 +657,7 @@ importers: version: link:../../core devDependencies: '@tanstack/react-router': - specifier: ^1.130.12 + specifier: 'catalog:' version: 1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/react': specifier: 'catalog:' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d6944fea..380a67e5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -31,3 +31,4 @@ catalog: "@hono/trpc-server": "^0.3.2" "react-router-dom": "^6.28.1" esbuild: "^0.25.0" + "@tanstack/react-router": "^1.130.12" From c37026603b6ef68e9984a60eb405e99d7694450d Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Thu, 7 Aug 2025 13:15:17 -0400 Subject: [PATCH 6/9] fix a bunch of tests and types --- .../app_with_splash_screen.tsx | 2 +- apps/small_apps/tic_tac_toe/tic_tac_toe.tsx | 2 +- doks/content/docs/introduction/quickstart.md | 2 +- doks/content/docs/jamtools/macro-module.md | 2 +- doks/content/docs/jamtools/midi-io-module.md | 2 +- .../guides/registering-ui-routes.md | 2 +- .../chord_families/chord_families_module.tsx | 2 +- .../core/modules/io/io_module.spec.ts | 2 +- .../inputs/macro_input_test_helpers.tsx | 13 +++++--- ...ical_keyboard_input_macro_handler.spec.tsx | 4 +-- .../modules/macro_module/macro_module.tsx | 31 +++++++------------ .../modules/dashboards/dashboards_module.tsx | 2 +- .../keytar_and_foot_dashboard.tsx | 4 +-- .../module_or_snack_template.tsx | 2 +- .../modules/daw_interaction_module.tsx | 2 +- .../modules/eventide/eventide_module.tsx | 2 +- .../features/modules/hand_raiser_module.tsx | 2 +- .../modules/lighting/wled/wled_module.tsx | 28 +++++++++-------- .../midi_playback/midi_playback_module.tsx | 2 +- .../modules/phone_jam/phone_jam_module.tsx | 2 +- .../song_structures_dashboards_module.tsx | 4 +-- .../ultimate_guitar_module.tsx | 6 ++-- packages/jamtools/features/package.json | 4 ++- .../features/snacks/midi_thru_snack.tsx | 2 +- .../root_mode_snack/root_mode_snack.tsx | 2 +- packages/springboard/core/engine/engine.tsx | 2 +- .../springboard/core/engine/module_api.ts | 11 +++++-- packages/springboard/core/engine/register.ts | 27 ++++++++++++++-- .../core/module_registry/module_registry.tsx | 4 +-- .../webapp => core/src}/root_route.tsx | 0 .../create-springboard-app/example/index.tsx | 2 +- .../src/example/index-as-string.ts | 2 +- .../services/kv/kv_rn_and_webview.spec.tsx | 4 ++- .../platforms/webapp/frontend_routes.tsx | 7 ++++- .../platforms/webapp/test_tanstack_module.tsx | 4 ++- pnpm-lock.yaml | 3 ++ 36 files changed, 117 insertions(+), 77 deletions(-) rename packages/springboard/{platforms/webapp => core/src}/root_route.tsx (100%) diff --git a/apps/small_apps/app_with_splash_screen/app_with_splash_screen.tsx b/apps/small_apps/app_with_splash_screen/app_with_splash_screen.tsx index e0a46761..26089249 100644 --- a/apps/small_apps/app_with_splash_screen/app_with_splash_screen.tsx +++ b/apps/small_apps/app_with_splash_screen/app_with_splash_screen.tsx @@ -60,7 +60,7 @@ springboard.registerModule('AppWithSplashScreen', {}, async (moduleAPI) => { }, }); - moduleAPI.registerRoute('/', {}, () => { + moduleAPI.registerRoute('/', () => { return ( { }, }); - moduleAPI.registerRoute('/', {}, () => { + moduleAPI.registerRoute('/', () => { return ( { // }); }); - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { return (
    diff --git a/doks/content/docs/jamtools/macro-module.md b/doks/content/docs/jamtools/macro-module.md index 3110e49f..7f2de0e3 100644 --- a/doks/content/docs/jamtools/macro-module.md +++ b/doks/content/docs/jamtools/macro-module.md @@ -51,7 +51,7 @@ springboard.registerModule('midi_thru', {}, async (moduleAPI) => { myOutput.send(evt.event); }); - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { return (
    diff --git a/doks/content/docs/jamtools/midi-io-module.md b/doks/content/docs/jamtools/midi-io-module.md index b3cc28fc..6f76e52a 100644 --- a/doks/content/docs/jamtools/midi-io-module.md +++ b/doks/content/docs/jamtools/midi-io-module.md @@ -35,7 +35,7 @@ springboard.registerModule('Main', {}, async (moduleAPI) => { mostRecentMidiEvent.setState(event); }); - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { const midiEvent = mostRecentMidiEvent.useState(); return ( diff --git a/doks/content/docs/springboard/guides/registering-ui-routes.md b/doks/content/docs/springboard/guides/registering-ui-routes.md index aec02c9a..b232f601 100644 --- a/doks/content/docs/springboard/guides/registering-ui-routes.md +++ b/doks/content/docs/springboard/guides/registering-ui-routes.md @@ -48,7 +48,7 @@ springboard.registerModule('MyModule', async (moduleAPI) => { }); // matches "/users/1" - moduleAPI.registerRoute('/users/:userId', {}, () => { + moduleAPI.registerRoute('/users/:userId', () => { const params = useParams(); const userId = params.userId; diff --git a/packages/jamtools/core/modules/chord_families/chord_families_module.tsx b/packages/jamtools/core/modules/chord_families/chord_families_module.tsx index 4dc21386..f096b567 100644 --- a/packages/jamtools/core/modules/chord_families/chord_families_module.tsx +++ b/packages/jamtools/core/modules/chord_families/chord_families_module.tsx @@ -131,7 +131,7 @@ springboard.registerModule('chord_families', {}, async (moduleAPI) => { }); }; - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { const state = rootModeState.useState(); const onClick = () => { diff --git a/packages/jamtools/core/modules/io/io_module.spec.ts b/packages/jamtools/core/modules/io/io_module.spec.ts index d0b8e0f8..d4d20aa2 100644 --- a/packages/jamtools/core/modules/io/io_module.spec.ts +++ b/packages/jamtools/core/modules/io/io_module.spec.ts @@ -1,4 +1,4 @@ -import '@jamtools/core/modules'; +import '@jamtools/core/modules/io/io_module'; import {Springboard} from 'springboard/engine/engine'; import {makeMockCoreDependencies, makeMockExtraDependences} from 'springboard/test/mock_core_dependencies'; diff --git a/packages/jamtools/core/modules/macro_module/macro_handlers/inputs/macro_input_test_helpers.tsx b/packages/jamtools/core/modules/macro_module/macro_handlers/inputs/macro_input_test_helpers.tsx index 407785c8..70db4f63 100644 --- a/packages/jamtools/core/modules/macro_module/macro_handlers/inputs/macro_input_test_helpers.tsx +++ b/packages/jamtools/core/modules/macro_module/macro_handlers/inputs/macro_input_test_helpers.tsx @@ -53,12 +53,17 @@ export const getMacroInputTestHelpers = () => { }; const gotoMacroPage = async () => { - const macroPageLink = screen.getByTestId('link-to-/modules/macro'); - // const macroPageLink = container.querySelector('a[href="/modules/macro/"]'); - expect(macroPageLink).toBeInTheDocument(); + const router = (window as any).tsRouter; + if (!router) { + throw new Error('Router not found on window.tsRouter'); + } await act(async () => { - fireEvent.click(macroPageLink!); + await router.navigate({ to: '/modules/macro' }); + }); + + await act(async () => { + await new Promise(r => setTimeout(r, 10)); }); }; diff --git a/packages/jamtools/core/modules/macro_module/macro_handlers/inputs/musical_keyboard_input_macro_handler.spec.tsx b/packages/jamtools/core/modules/macro_module/macro_handlers/inputs/musical_keyboard_input_macro_handler.spec.tsx index ece6d79d..a25958c9 100644 --- a/packages/jamtools/core/modules/macro_module/macro_handlers/inputs/musical_keyboard_input_macro_handler.spec.tsx +++ b/packages/jamtools/core/modules/macro_module/macro_handlers/inputs/musical_keyboard_input_macro_handler.spec.tsx @@ -3,7 +3,7 @@ import {act} from 'react'; import { screen } from 'shadow-dom-testing-library'; import '@testing-library/jest-dom'; -import '@jamtools/core/modules'; +import '@jamtools/core/modules/macro_module/macro_module'; import {Springboard} from 'springboard/engine/engine'; import springboard from 'springboard'; @@ -20,7 +20,7 @@ import {getMacroInputTestHelpers} from './macro_input_test_helpers'; describe('MusicalKeyboardInputMacroHandler', () => { beforeEach(() => { - springboard.reset(); + springboard.reset({keepCalls: true}); macroTypeRegistry.reset(); }); diff --git a/packages/jamtools/core/modules/macro_module/macro_module.tsx b/packages/jamtools/core/modules/macro_module/macro_module.tsx index dbadc381..42e7aa2f 100644 --- a/packages/jamtools/core/modules/macro_module/macro_module.tsx +++ b/packages/jamtools/core/modules/macro_module/macro_module.tsx @@ -16,7 +16,7 @@ import {createRoute} from '@tanstack/react-router'; import './macro_handlers'; import {macroTypeRegistry} from './registered_macro_types'; -import {rootRoute} from '@springboardjs/platforms-browser/root_route'; +import {rootRoute} from 'springboard/src/root_route'; type ModuleId = string; @@ -55,25 +55,16 @@ export class MacroModule implements Module { constructor(private coreDeps: CoreDependencies, private moduleDeps: ModuleDependencies) { } - // routes = [ - // createRoute({ - // getParentRoute: () => rootRoute, - // path: '/modules/macro', - // component: () => { - // const mod = MacroModule.use(); - // return ; - // }, - // }), - // ]; - - // routes = { - // '': { - // component: () => { - // const mod = MacroModule.use(); - // return ; - // }, - // }, - // }; + routes = [ + createRoute({ + getParentRoute: () => rootRoute, + path: '/modules/macro', + component: () => { + const mod = MacroModule.use(); + return ; + }, + }), + ]; state: MacroConfigState = { configs: {}, diff --git a/packages/jamtools/features/modules/dashboards/dashboards_module.tsx b/packages/jamtools/features/modules/dashboards/dashboards_module.tsx index 541cba7a..0ae530d2 100644 --- a/packages/jamtools/features/modules/dashboards/dashboards_module.tsx +++ b/packages/jamtools/features/modules/dashboards/dashboards_module.tsx @@ -28,7 +28,7 @@ springboard.registerModule('Dashboards', {}, async (moduleAPI): Promise d.dashboard(moduleAPI, d.id)); await Promise.all(promises); - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { return (

    Dashboards:

    diff --git a/packages/jamtools/features/modules/dashboards/keytar_and_foot_dashboard/keytar_and_foot_dashboard.tsx b/packages/jamtools/features/modules/dashboards/keytar_and_foot_dashboard/keytar_and_foot_dashboard.tsx index 8010497d..9433794e 100644 --- a/packages/jamtools/features/modules/dashboards/keytar_and_foot_dashboard/keytar_and_foot_dashboard.tsx +++ b/packages/jamtools/features/modules/dashboards/keytar_and_foot_dashboard/keytar_and_foot_dashboard.tsx @@ -21,11 +21,11 @@ const KeytarAndFootDashboard = async (moduleAPI: ModuleAPI, dashboardName: strin singleOctaveSupervisor.initialize(), ]); - moduleAPI.registerRoute('kiosk', {hideApplicationShell: true}, () => ( + moduleAPI.registerRoute('kiosk', () => ( )); - moduleAPI.registerRoute(dashboardName, {}, () => ( + moduleAPI.registerRoute(dashboardName, () => (

    Keytar and Foot Dashboard diff --git a/packages/jamtools/features/modules/dashboards/keytar_and_foot_dashboard/module_or_snack_template.tsx b/packages/jamtools/features/modules/dashboards/keytar_and_foot_dashboard/module_or_snack_template.tsx index 42052f83..76e8e4b1 100644 --- a/packages/jamtools/features/modules/dashboards/keytar_and_foot_dashboard/module_or_snack_template.tsx +++ b/packages/jamtools/features/modules/dashboards/keytar_and_foot_dashboard/module_or_snack_template.tsx @@ -56,7 +56,7 @@ type States = Awaited>; type Macros = Awaited>; const registerRoutes = (moduleAPI: ModuleAPI, states: States, macros: Macros, actions: Actions) => { - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { const myState = states.myState.useState(); return ( diff --git a/packages/jamtools/features/modules/daw_interaction_module.tsx b/packages/jamtools/features/modules/daw_interaction_module.tsx index 8aab897e..70327d27 100644 --- a/packages/jamtools/features/modules/daw_interaction_module.tsx +++ b/packages/jamtools/features/modules/daw_interaction_module.tsx @@ -39,7 +39,7 @@ springboard.registerModule('daw_interaction', {}, async (moduleAPI) => { state.setState(args.value); }); - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { const sliderPosition1 = sliderPositionState1.useState(); const sliderPosition2 = sliderPositionState2.useState(); diff --git a/packages/jamtools/features/modules/eventide/eventide_module.tsx b/packages/jamtools/features/modules/eventide/eventide_module.tsx index 666c4eb2..d411afaa 100644 --- a/packages/jamtools/features/modules/eventide/eventide_module.tsx +++ b/packages/jamtools/features/modules/eventide/eventide_module.tsx @@ -69,7 +69,7 @@ springbord.registerModule('Eventide', {}, async (moduleAPI) => { }; // hideNavbar should really be "hideApplicationShell", and also be a global option - moduleAPI.registerRoute('', {hideApplicationShell: false}, () => { + moduleAPI.registerRoute('', () => { const currentPreset = currentPresetState.useState(); const favoritedPresets = favoritedPresetsState.useState(); diff --git a/packages/jamtools/features/modules/hand_raiser_module.tsx b/packages/jamtools/features/modules/hand_raiser_module.tsx index 072e780d..f61b9c45 100644 --- a/packages/jamtools/features/modules/hand_raiser_module.tsx +++ b/packages/jamtools/features/modules/hand_raiser_module.tsx @@ -44,7 +44,7 @@ springboard.registerModule('HandRaiser', {}, async (m) => { }, }); - m.registerRoute('/', {}, () => { + m.registerRoute('/', () => { const positions = states.handPositions.useState(); return ( diff --git a/packages/jamtools/features/modules/lighting/wled/wled_module.tsx b/packages/jamtools/features/modules/lighting/wled/wled_module.tsx index d5d25695..29218893 100644 --- a/packages/jamtools/features/modules/lighting/wled/wled_module.tsx +++ b/packages/jamtools/features/modules/lighting/wled/wled_module.tsx @@ -10,6 +10,8 @@ import {BaseModule, ModuleHookValue} from 'springboard/modules/base_module/base_ import {Module} from 'springboard/module_registry/module_registry'; import springboard from 'springboard'; +import {createRoute} from '@tanstack/react-router'; +import {rootRoute} from 'springboard/src/root_route'; type WledClientStatus = { url: string; @@ -47,19 +49,19 @@ export class WledModule implements Module { cleanup: (() => void)[] = []; - // routes = { - // '': { - // component: () => { - // const mod = WledModule.use(); - - // return ( - //
    -    //                     {JSON.stringify(mod.state)}
    -    //                 
    - // ); - // }, - // }, - // }; + routes = [ + createRoute({ + getParentRoute: () => rootRoute, + path: '/wled', + component: () => { + return ( +
    +                        {JSON.stringify(this.state)}
    +                    
    + ); + }, + }), + ]; // wled controllers need to be stored as hostnames, // so they are readable and stay consistent for that controller diff --git a/packages/jamtools/features/modules/midi_playback/midi_playback_module.tsx b/packages/jamtools/features/modules/midi_playback/midi_playback_module.tsx index 4a9799f3..ee456682 100644 --- a/packages/jamtools/features/modules/midi_playback/midi_playback_module.tsx +++ b/packages/jamtools/features/modules/midi_playback/midi_playback_module.tsx @@ -62,7 +62,7 @@ springboard.registerModule('MidiPlayback', {}, async (moduleAPI): Promise { + moduleAPI.registerRoute('', () => { const savedState = savedMidiFileData.useState(); return ( diff --git a/packages/jamtools/features/modules/phone_jam/phone_jam_module.tsx b/packages/jamtools/features/modules/phone_jam/phone_jam_module.tsx index c410fa86..1c3b6e97 100644 --- a/packages/jamtools/features/modules/phone_jam/phone_jam_module.tsx +++ b/packages/jamtools/features/modules/phone_jam/phone_jam_module.tsx @@ -13,7 +13,7 @@ springboard.registerModule('phone_jam', {}, async (moduleAPI) => { }, 1000); }; - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { return ( { + moduleAPI.registerRoute('', () => { return (
    @@ -149,7 +149,7 @@ springboard.registerModule('song_structures_dashboards', {}, async (moduleAPI): ); }); - moduleAPI.registerRoute('bass_guitar', {}, () => { + moduleAPI.registerRoute('bass_guitar', () => { const props: React.ComponentProps = { numberOfStrings: 4, chosenFrets: [ diff --git a/packages/jamtools/features/modules/ultimate_guitar/ultimate_guitar_module.tsx b/packages/jamtools/features/modules/ultimate_guitar/ultimate_guitar_module.tsx index b4775264..9805e84d 100644 --- a/packages/jamtools/features/modules/ultimate_guitar/ultimate_guitar_module.tsx +++ b/packages/jamtools/features/modules/ultimate_guitar/ultimate_guitar_module.tsx @@ -41,7 +41,7 @@ springboard.registerModule('Ultimate_Guitar', {}, async (moduleAPI): Promise ( + moduleAPI.registerRoute('', () => ( )); - moduleAPI.registerRoute('manage', {}, () => ( + moduleAPI.registerRoute('manage', () => ( )); - moduleAPI.registerRoute('qrcode', {}, () => ( + moduleAPI.registerRoute('qrcode', () => ( )); diff --git a/packages/jamtools/features/package.json b/packages/jamtools/features/package.json index 313068f3..d70c3b6e 100644 --- a/packages/jamtools/features/package.json +++ b/packages/jamtools/features/package.json @@ -17,6 +17,7 @@ "devDependencies": { "@jamtools/core": "workspace:*", "@springboardjs/shoelace": "workspace:*", + "@tanstack/react-router": "catalog:", "@types/qrcode": "^1.5.5", "@types/react": "catalog:", "@types/react-dom": "catalog:", @@ -29,6 +30,7 @@ }, "peerDependencies": { "@jamtools/core": "workspace:*", - "@springboardjs/shoelace": "workspace:*" + "@springboardjs/shoelace": "workspace:*", + "@tanstack/react-router": "^1" } } diff --git a/packages/jamtools/features/snacks/midi_thru_snack.tsx b/packages/jamtools/features/snacks/midi_thru_snack.tsx index 88d7c3e4..eef2f7a6 100644 --- a/packages/jamtools/features/snacks/midi_thru_snack.tsx +++ b/packages/jamtools/features/snacks/midi_thru_snack.tsx @@ -15,7 +15,7 @@ springboard.registerModule('midi_thru', {}, async (moduleAPI) => { myOutput.send(evt.event); }); - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { return (
    diff --git a/packages/jamtools/features/snacks/root_mode_snack/root_mode_snack.tsx b/packages/jamtools/features/snacks/root_mode_snack/root_mode_snack.tsx index cc418039..24c3af4b 100644 --- a/packages/jamtools/features/snacks/root_mode_snack/root_mode_snack.tsx +++ b/packages/jamtools/features/snacks/root_mode_snack/root_mode_snack.tsx @@ -28,7 +28,7 @@ springboard.registerModule('Main', {}, async (moduleAPI) => { }); }; - moduleAPI.registerRoute('', {}, () => { + moduleAPI.registerRoute('', () => { const state = rootModeState.useState(); const onClick = () => { diff --git a/packages/springboard/core/engine/engine.tsx b/packages/springboard/core/engine/engine.tsx index 1782a7d4..e04f3c60 100644 --- a/packages/springboard/core/engine/engine.tsx +++ b/packages/springboard/core/engine/engine.tsx @@ -114,7 +114,7 @@ export class Springboard { logPerformance(start, end, `${id} module initialized`); }; - // TODO: this is not good that classes are unconditionally all registered first. Let's use performance.now() to determine the order of when things were called + // TODO: this is not good that classes are unconditionally all registered first before the non-class ones. Let's use performance.now() to determine the order of when things were called // or put them all in the same array instead of different arrays like they currently are for (const modClassCallback of registeredClassModuleCallbacks) { const start = now(); // would be great to use `using` here to time this diff --git a/packages/springboard/core/engine/module_api.ts b/packages/springboard/core/engine/module_api.ts index a3adf739..d7caa701 100644 --- a/packages/springboard/core/engine/module_api.ts +++ b/packages/springboard/core/engine/module_api.ts @@ -2,6 +2,8 @@ import {SharedStateSupervisor, StateSupervisor, UserAgentStateSupervisor} from ' import {ExtraModuleDependencies, Module, NavigationItemConfig, RegisteredRoute} from 'springboard/module_registry/module_registry'; import {CoreDependencies, ModuleDependencies} from '../types/module_types'; import {RegisterRouteOptions} from './register'; +import {createRoute, RouteComponent} from '@tanstack/react-router'; +import {rootRoute} from 'springboard/src/root_route'; type ActionConfigOptions = object; @@ -87,8 +89,13 @@ export class ModuleAPI { * ``` * */ - registerRoute = (routePath: string, options: RegisterRouteOptions, component: RegisteredRoute['component']) => { - console.error('registerRoute is not supported in tanstack router'); + registerRoute = (routePath: string, component: RouteComponent) => { + this.module.routes ||= []; + this.module.routes.push(createRoute({ + path: routePath, + getParentRoute: () => rootRoute, + component, + })); }; registerApplicationShell = (component: React.ElementType>) => { diff --git a/packages/springboard/core/engine/register.ts b/packages/springboard/core/engine/register.ts index 729870de..4740960a 100644 --- a/packages/springboard/core/engine/register.ts +++ b/packages/springboard/core/engine/register.ts @@ -14,6 +14,19 @@ export type ClassModuleCallback = (coreDeps: CoreDependencies, Promise> | Module; export type SpringboardRegistry = { + /** + * Register a Springboard module + * + * After registering, you'll need to declare the module using interface merging: + * + * ```ts + * declare module 'springboard/module_registry/module_registry' { + * interface AllModules { + * MyModuleId: MyModule; + * } + * } + * ``` + */ registerModule: ( moduleId: string, options: ModuleOptions, @@ -21,7 +34,7 @@ export type SpringboardRegistry = { ) => void; registerClassModule: (cb: ClassModuleCallback) => void; registerSplashScreen: (component: React.ComponentType) => void; - reset: () => void; + reset: (options?: {keepCalls?: boolean}) => void; }; export type RegisterModuleOptions = { @@ -62,9 +75,17 @@ export const springboard: SpringboardRegistry = { registerModule, registerClassModule, registerSplashScreen, - reset: () => { + reset: (options?: {keepCalls?: boolean}) => { springboard.registerModule = registerModule; + if (!options?.keepCalls) { + (registerModule as any).calls = []; + } + springboard.registerClassModule = registerClassModule; - springboard.registerSplashScreen = registerSplashScreen; + if (!options?.keepCalls) { + (registerClassModule as any).calls = []; + } + + registeredSplashScreen = null; }, }; diff --git a/packages/springboard/core/module_registry/module_registry.tsx b/packages/springboard/core/module_registry/module_registry.tsx index 8c65a743..e3d1c11e 100644 --- a/packages/springboard/core/module_registry/module_registry.tsx +++ b/packages/springboard/core/module_registry/module_registry.tsx @@ -4,7 +4,7 @@ import {Subject} from 'rxjs'; import type {ModuleAPI} from '../engine/module_api'; import {RegisterRouteOptions} from '../engine/register'; -import type {Route} from '@tanstack/react-router'; +import type {AnyRoute} from '@tanstack/react-router'; type RouteComponentProps = { navigate: (routeName: string) => void; @@ -27,7 +27,7 @@ export type Module = { Provider?: React.ElementType; state?: State; subject?: Subject; - routes?: Route[]; + routes?: AnyRoute[]; applicationShell?: React.ElementType>; }; diff --git a/packages/springboard/platforms/webapp/root_route.tsx b/packages/springboard/core/src/root_route.tsx similarity index 100% rename from packages/springboard/platforms/webapp/root_route.tsx rename to packages/springboard/core/src/root_route.tsx diff --git a/packages/springboard/create-springboard-app/example/index.tsx b/packages/springboard/create-springboard-app/example/index.tsx index 15c384b3..10028a09 100644 --- a/packages/springboard/create-springboard-app/example/index.tsx +++ b/packages/springboard/create-springboard-app/example/index.tsx @@ -3,7 +3,7 @@ import React from 'react'; import springboard from 'springboard'; springboard.registerModule('example', {}, async (app) => { - app.registerRoute('/', {}, () => { + app.registerRoute('/', () => { return

    Example

    ; }); diff --git a/packages/springboard/create-springboard-app/src/example/index-as-string.ts b/packages/springboard/create-springboard-app/src/example/index-as-string.ts index 3de73b61..18abfa6d 100644 --- a/packages/springboard/create-springboard-app/src/example/index-as-string.ts +++ b/packages/springboard/create-springboard-app/src/example/index-as-string.ts @@ -4,7 +4,7 @@ import React from 'react'; import springboard from 'springboard'; springboard.registerModule('example', {}, async (app) => { - app.registerRoute('/', {}, () => { + app.registerRoute('/', () => { return

    Example

    ; }); diff --git a/packages/springboard/platforms/react-native/services/kv/kv_rn_and_webview.spec.tsx b/packages/springboard/platforms/react-native/services/kv/kv_rn_and_webview.spec.tsx index 341c1523..e299a5f5 100644 --- a/packages/springboard/platforms/react-native/services/kv/kv_rn_and_webview.spec.tsx +++ b/packages/springboard/platforms/react-native/services/kv/kv_rn_and_webview.spec.tsx @@ -14,6 +14,8 @@ import {createRNWebviewEngine} from '../../entrypoints/platform_react_native_bro import {Main} from '@springboardjs/platforms-browser/entrypoints/main'; import {createRNMainEngine} from '../../entrypoints/rn_app_springboard_entrypoint'; +window.scrollTo = vitest.fn(); + describe('KvRnWebview', () => { beforeEach(() => { springboard.reset(); @@ -37,7 +39,7 @@ describe('KvRnWebview', () => { }, }); - m.registerRoute('/', {}, () => { + m.registerRoute('/', () => { const myState = myUserAgentState.useState(); const [localState, setLocalState] = useState(''); diff --git a/packages/springboard/platforms/webapp/frontend_routes.tsx b/packages/springboard/platforms/webapp/frontend_routes.tsx index 498dc703..6f1975a7 100644 --- a/packages/springboard/platforms/webapp/frontend_routes.tsx +++ b/packages/springboard/platforms/webapp/frontend_routes.tsx @@ -7,7 +7,7 @@ import { import {useSpringboardEngine} from 'springboard/engine/engine'; import {AllModules} from 'springboard/module_registry/module_registry'; -import {rootRoute} from './root_route'; +import {rootRoute} from 'springboard/src/root_route'; // utilities for extracting and typing routes from modules type ExtractRoutes = T extends {routes: infer R} ? R : @@ -50,6 +50,11 @@ export const FrontendRoutes = () => { const router = createAppRouter(typedRoutes); + // Expose router globally for testing + if (typeof window !== 'undefined') { + (window as any).tsRouter = router; + } + return ; }; diff --git a/packages/springboard/platforms/webapp/test_tanstack_module.tsx b/packages/springboard/platforms/webapp/test_tanstack_module.tsx index e76cc94e..80d7aee5 100644 --- a/packages/springboard/platforms/webapp/test_tanstack_module.tsx +++ b/packages/springboard/platforms/webapp/test_tanstack_module.tsx @@ -1,9 +1,11 @@ import React from 'react'; import {createRoute, getRouteApi, useRouter} from '@tanstack/react-router'; import springboard from 'springboard'; -import {rootRoute} from './root_route'; +import {rootRoute} from 'springboard/src/root_route'; import {ModuleAPI} from 'springboard/engine/module_api'; +import '@jamtools/core/modules/macro_module/macro_module'; + const makeTestTanStackModule = async (moduleAPI: ModuleAPI) => { const messageState = await moduleAPI.statesAPI.createPersistentState('testMessage', 'Hello from TanStack Router!'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40064135..b68b42ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -333,6 +333,9 @@ importers: '@springboardjs/shoelace': specifier: workspace:* version: link:../../springboard/external/shoelace + '@tanstack/react-router': + specifier: 'catalog:' + version: 1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/qrcode': specifier: ^1.5.5 version: 1.5.5 From c59c7cee56e9ba6688a1bb465b65e15badc7a313 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Sat, 16 Aug 2025 04:02:58 -0400 Subject: [PATCH 7/9] improved registerRoute to have support for some tanstack props out of the box --- .../modules/macro_module/macro_module.tsx | 2 +- .../modules/lighting/wled/wled_module.tsx | 2 +- .../springboard/core/engine/module_api.ts | 43 ++++++++++++++----- .../core/{src => ui}/root_route.tsx | 0 .../platforms/webapp/frontend_routes.tsx | 6 +-- .../platforms/webapp/test_tanstack_module.tsx | 29 +++++++++---- 6 files changed, 58 insertions(+), 24 deletions(-) rename packages/springboard/core/{src => ui}/root_route.tsx (100%) diff --git a/packages/jamtools/core/modules/macro_module/macro_module.tsx b/packages/jamtools/core/modules/macro_module/macro_module.tsx index 42e7aa2f..8c2efaed 100644 --- a/packages/jamtools/core/modules/macro_module/macro_module.tsx +++ b/packages/jamtools/core/modules/macro_module/macro_module.tsx @@ -16,7 +16,7 @@ import {createRoute} from '@tanstack/react-router'; import './macro_handlers'; import {macroTypeRegistry} from './registered_macro_types'; -import {rootRoute} from 'springboard/src/root_route'; +import {rootRoute} from 'springboard/ui/root_route'; type ModuleId = string; diff --git a/packages/jamtools/features/modules/lighting/wled/wled_module.tsx b/packages/jamtools/features/modules/lighting/wled/wled_module.tsx index 29218893..bf4b257e 100644 --- a/packages/jamtools/features/modules/lighting/wled/wled_module.tsx +++ b/packages/jamtools/features/modules/lighting/wled/wled_module.tsx @@ -11,7 +11,7 @@ import {Module} from 'springboard/module_registry/module_registry'; import springboard from 'springboard'; import {createRoute} from '@tanstack/react-router'; -import {rootRoute} from 'springboard/src/root_route'; +import {rootRoute} from 'springboard/ui/root_route'; type WledClientStatus = { url: string; diff --git a/packages/springboard/core/engine/module_api.ts b/packages/springboard/core/engine/module_api.ts index d7caa701..6de62429 100644 --- a/packages/springboard/core/engine/module_api.ts +++ b/packages/springboard/core/engine/module_api.ts @@ -1,9 +1,11 @@ +import React, {ComponentProps} from 'react'; +import {createRoute, ResolveParams} from '@tanstack/react-router'; + import {SharedStateSupervisor, StateSupervisor, UserAgentStateSupervisor} from '../services/states/shared_state_service'; import {ExtraModuleDependencies, Module, NavigationItemConfig, RegisteredRoute} from 'springboard/module_registry/module_registry'; import {CoreDependencies, ModuleDependencies} from '../types/module_types'; -import {RegisterRouteOptions} from './register'; -import {createRoute, RouteComponent} from '@tanstack/react-router'; -import {rootRoute} from 'springboard/src/root_route'; + +import {rootRoute} from 'springboard/ui/root_route'; type ActionConfigOptions = object; @@ -89,19 +91,38 @@ export class ModuleAPI { * ``` * */ - registerRoute = (routePath: string, component: RouteComponent) => { - this.module.routes ||= []; - this.module.routes.push(createRoute({ + registerRoute = (routePath: TRoutePath, Component: React.ElementType<{ + navigate: (route: string) => void; + pathParams: ResolveParams; + searchParams: Record; + }>) => { + const route = createRoute({ path: routePath, getParentRoute: () => rootRoute, - component, - })); - }; + component: () => { + const navigate = route.useNavigate(); + const pathParams = route.useParams() as ResolveParams; + const searchParams = route.useSearch() as Record; + + return React.createElement(Component, { + navigate: route => navigate({to: route}), + pathParams, + searchParams, + } satisfies ComponentProps); + }, + }); - registerApplicationShell = (component: React.ElementType>) => { - this.module.applicationShell = component; + this.module.routes ||= []; + this.module.routes.push(route); }; + rootRoute = rootRoute; + + // TODO: remove traces of this before merge + // registerApplicationShell = (component: React.ElementType>) => { + // this.module.applicationShell = component; + // }; + createStates = async >(states: States): Promise<{[K in keyof States]: StateSupervisor}> => { const keys = Object.keys(states); const promises = keys.map(async key => { diff --git a/packages/springboard/core/src/root_route.tsx b/packages/springboard/core/ui/root_route.tsx similarity index 100% rename from packages/springboard/core/src/root_route.tsx rename to packages/springboard/core/ui/root_route.tsx diff --git a/packages/springboard/platforms/webapp/frontend_routes.tsx b/packages/springboard/platforms/webapp/frontend_routes.tsx index 6f1975a7..3c61b63e 100644 --- a/packages/springboard/platforms/webapp/frontend_routes.tsx +++ b/packages/springboard/platforms/webapp/frontend_routes.tsx @@ -7,9 +7,9 @@ import { import {useSpringboardEngine} from 'springboard/engine/engine'; import {AllModules} from 'springboard/module_registry/module_registry'; -import {rootRoute} from 'springboard/src/root_route'; +import {rootRoute} from 'springboard/ui/root_route'; -// utilities for extracting and typing routes from modules +// utilities for extracting and typing routes from registered modules type ExtractRoutes = T extends {routes: infer R} ? R : T extends () => Promise<{routes: infer R}> ? R : never; type Flatten = T extends readonly (infer U)[] ? U : never; @@ -18,7 +18,7 @@ type AllRoutes = { }[keyof AllModules]; type AllRoutesFlat = readonly Flatten[]; -// router factory function that creates a strongly-typed router based on AllModules +// creates a strongly-typed router based on all registered modules that return a `routes` property function createAppRouter(routes: AllRoutesFlat) { const routeTree = rootRoute.addChildren(routes); diff --git a/packages/springboard/platforms/webapp/test_tanstack_module.tsx b/packages/springboard/platforms/webapp/test_tanstack_module.tsx index 80d7aee5..f557c9b4 100644 --- a/packages/springboard/platforms/webapp/test_tanstack_module.tsx +++ b/packages/springboard/platforms/webapp/test_tanstack_module.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import {createRoute, getRouteApi, useRouter} from '@tanstack/react-router'; +import {createRoute, getRouteApi, useParams, useRouter, useSearch} from '@tanstack/react-router'; import springboard from 'springboard'; -import {rootRoute} from 'springboard/src/root_route'; import {ModuleAPI} from 'springboard/engine/module_api'; import '@jamtools/core/modules/macro_module/macro_module'; @@ -18,7 +17,7 @@ const makeTestTanStackModule = async (moduleAPI: ModuleAPI) => { return { routes: [ createRoute({ - getParentRoute: () => rootRoute, + getParentRoute: () => moduleAPI.rootRoute, path: '/', component: () => { return ( @@ -30,9 +29,21 @@ const makeTestTanStackModule = async (moduleAPI: ModuleAPI) => { }, }), createRoute({ - getParentRoute: () => rootRoute, - path: '/tanstack-test', + getParentRoute: () => moduleAPI.rootRoute, + path: '/tanstack-test/$id', + params: { + parse: (params) => ({ + id: params.id, + }), + }, + validateSearch: (search) => { + return { + other_id: search.other_id as string | undefined, + }; + }, component: () => { + const { id } = useParams({from: '/tanstack-test/$id'}); + const { other_id } = useSearch({from: '/tanstack-test/$id'}); return ( { }, }), createRoute({ - getParentRoute: () => rootRoute, + getParentRoute: () => moduleAPI.rootRoute, path: '/tanstack-test-with-search', component: () => { const route = getRouteApi('/tanstack-test-with-search'); const search = route.useSearch(); + const search2 = useSearch({from: '/tanstack-test-with-search'}); return (

    TanStack Test with Search

    Query: {search.query || 'none'}

    -

    Has Discount: {search.hasDiscount ? 'Yes' : 'No'}

    +

    Has Discount: {search2.hasDiscount ? 'Yes' : 'No'}

    ); }, @@ -90,7 +102,8 @@ const TestTanStackComponent = (props: TestTanStackComponentProps) => {

    TanStack Router Test Module

    Current message: {props.message}

    - + +
    Date: Wed, 24 Sep 2025 20:08:08 -0400 Subject: [PATCH 8/9] remove react-router dependency --- apps/jamtools/package.json | 1 - .../modules/dashboards/dashboards_module.tsx | 6 +- .../song_structures_dashboards_module.tsx | 6 +- packages/jamtools/features/package.json | 3 +- packages/springboard/core/package.json | 1 + packages/springboard/core/src/index.ts | 2 + .../components/shoelace_application_shell.tsx | 6 +- .../external/shoelace/package.json | 3 +- .../springboard/platforms/webapp/layout.tsx | 2 - .../springboard/platforms/webapp/package.json | 1 - .../webapp/tanstack_router_scratch_notes.tsx | 112 ++++++++++++++++++ .../platforms/webapp/tanstack_types_test.tsx | 15 +++ pnpm-lock.yaml | 50 +------- pnpm-workspace.yaml | 1 - 14 files changed, 141 insertions(+), 68 deletions(-) create mode 100644 packages/springboard/platforms/webapp/tanstack_router_scratch_notes.tsx create mode 100644 packages/springboard/platforms/webapp/tanstack_types_test.tsx diff --git a/apps/jamtools/package.json b/apps/jamtools/package.json index e31e77f2..724569c0 100644 --- a/apps/jamtools/package.json +++ b/apps/jamtools/package.json @@ -8,7 +8,6 @@ "license": "ISC", "description": "", "dependencies": { - "react-router-dom": "catalog:", "springboard": "workspace:*", "@jamtools/core": "workspace:*", "@jamtools/features": "workspace:*", diff --git a/packages/jamtools/features/modules/dashboards/dashboards_module.tsx b/packages/jamtools/features/modules/dashboards/dashboards_module.tsx index 0ae530d2..d571761e 100644 --- a/packages/jamtools/features/modules/dashboards/dashboards_module.tsx +++ b/packages/jamtools/features/modules/dashboards/dashboards_module.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import {Link} from 'react-router-dom'; - import springboard from 'springboard'; import {ModuleAPI} from 'springboard/engine/module_api'; @@ -35,9 +33,9 @@ springboard.registerModule('Dashboards', {}, async (moduleAPI): Promise {allDashboards.map(d => (
  • - + {d.label} - +
  • ))} diff --git a/packages/jamtools/features/modules/song_structures_dashboards/song_structures_dashboards_module.tsx b/packages/jamtools/features/modules/song_structures_dashboards/song_structures_dashboards_module.tsx index d9dbd558..07b5d19c 100644 --- a/packages/jamtools/features/modules/song_structures_dashboards/song_structures_dashboards_module.tsx +++ b/packages/jamtools/features/modules/song_structures_dashboards/song_structures_dashboards_module.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import {Link} from 'react-router-dom'; - import springboard from 'springboard'; import {MidiEvent} from '@jamtools/core/modules/macro_module/macro_module_types'; @@ -140,11 +138,11 @@ springboard.registerModule('song_structures_dashboards', {}, async (moduleAPI): moduleAPI.registerRoute('', () => { return ( ); }); diff --git a/packages/jamtools/features/package.json b/packages/jamtools/features/package.json index d70c3b6e..bdac35f5 100644 --- a/packages/jamtools/features/package.json +++ b/packages/jamtools/features/package.json @@ -22,8 +22,7 @@ "@types/react": "catalog:", "@types/react-dom": "catalog:", "react": "catalog:", - "react-dom": "catalog:", - "react-router-dom": "catalog:" + "react-dom": "catalog:" }, "config": { "dir": "../../../configs" diff --git a/packages/springboard/core/package.json b/packages/springboard/core/package.json index 06d81dc4..1c2624db 100644 --- a/packages/springboard/core/package.json +++ b/packages/springboard/core/package.json @@ -28,6 +28,7 @@ "src", "test", "types", + "ui", "utils" ], "peerDependencies": { diff --git a/packages/springboard/core/src/index.ts b/packages/springboard/core/src/index.ts index 70e27568..0a129ccf 100644 --- a/packages/springboard/core/src/index.ts +++ b/packages/springboard/core/src/index.ts @@ -5,3 +5,5 @@ import {springboard} from '../engine/register'; // export const SpringboardProvider = engine.SpringboardProvider; export default springboard; + +export {AllModules} from '../module_registry/module_registry'; diff --git a/packages/springboard/external/shoelace/components/shoelace_application_shell.tsx b/packages/springboard/external/shoelace/components/shoelace_application_shell.tsx index c876ca4a..6612e14d 100644 --- a/packages/springboard/external/shoelace/components/shoelace_application_shell.tsx +++ b/packages/springboard/external/shoelace/components/shoelace_application_shell.tsx @@ -1,6 +1,6 @@ import React, {useState} from 'react'; -import {useLocation, useNavigate} from 'react-router-dom'; +import {useLocation, useNavigate} from '@tanstack/react-router'; import SlTab from '@shoelace-style/shoelace/dist/react/tab/index.js'; import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group/index.js'; @@ -79,10 +79,10 @@ const Tabs = (props: TabsProps) => { const showRoute = (modId: string, route: string) => { if (route.startsWith('/')) { - navigate(route); + navigate({to: route}); return; } - navigate(`/modules/${modId}/${route}`); + navigate({to: `/modules/${modId}/${route}`}); }; const modulesWithRoutes = props.modules.filter(m => m.routes).map(m => ( diff --git a/packages/springboard/external/shoelace/package.json b/packages/springboard/external/shoelace/package.json index 8c729203..69488bd0 100644 --- a/packages/springboard/external/shoelace/package.json +++ b/packages/springboard/external/shoelace/package.json @@ -15,7 +15,6 @@ "springboard": "workspace:*" }, "devDependencies": { - "react": "catalog:", - "react-router-dom": "catalog:" + "@tanstack/react-router": "catalog:" } } diff --git a/packages/springboard/platforms/webapp/layout.tsx b/packages/springboard/platforms/webapp/layout.tsx index d3b003d2..f94fd388 100644 --- a/packages/springboard/platforms/webapp/layout.tsx +++ b/packages/springboard/platforms/webapp/layout.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import {useLocation, matchPath} from 'react-router-dom'; - import {Module} from 'springboard/module_registry/module_registry'; type Props = React.PropsWithChildren<{ diff --git a/packages/springboard/platforms/webapp/package.json b/packages/springboard/platforms/webapp/package.json index 1cdaa219..6b124104 100644 --- a/packages/springboard/platforms/webapp/package.json +++ b/packages/springboard/platforms/webapp/package.json @@ -8,7 +8,6 @@ }, "peerDependencies": { "@tanstack/react-router": "^1", - "react-router-dom": "^6", "springboard": "workspace:*" }, "dependencies": { diff --git a/packages/springboard/platforms/webapp/tanstack_router_scratch_notes.tsx b/packages/springboard/platforms/webapp/tanstack_router_scratch_notes.tsx new file mode 100644 index 00000000..276efdf6 --- /dev/null +++ b/packages/springboard/platforms/webapp/tanstack_router_scratch_notes.tsx @@ -0,0 +1,112 @@ +// import React from 'react'; + +// import { +// Outlet, +// RouterProvider, +// createRootRoute, +// createRoute, +// createRouter, +// getRouteApi, +// useRouter, +// type RouteOptions, +// } from '@tanstack/react-router'; + + +// const root = createRootRoute({ +// component: () => ( +// <> +// +// +// ), +// }); + + +// const ui = { +// enableRouterDevTools: () => { }, +// root: () => root, +// routeProps: { +// getParentRoute: () => root, +// }, +// }; + +// interface AllModules { } + +// const module1 = () => { +// const routes = [ +// createRoute({ +// ...ui.routeProps, +// path: '/module1', +// component: () => { +// return 'hey'; +// }, +// }) +// ]; + +// return { +// routes, +// }; +// }; + +// interface AllModules { +// module1: typeof module1; +// } + +// const module2 = () => { +// return { +// routes: [ +// createRoute({ +// getParentRoute: ui.root, +// path: '/', +// component: () => { +// const route = getRouteApi('/'); +// const search = route.useSearch(); +// search.hasDiscount; +// return null; +// }, +// validateSearch: search => ({ +// query: (search.query as string) || '', +// hasDiscount: search.hasDiscount === 'true', +// }), +// }), +// ] +// }; +// }; + +// interface AllModules { +// module2: typeof module2; +// } + +// const allModules = { +// module1, +// module2, +// };// as const satisfies AllModules; + +// type ExtractRoutes = T extends () => {routes: infer R} ? R : never; +// type Flatten = T extends readonly (infer U)[] ? U : never; +// type AllRoutes = { +// [K in keyof AllModules]: ExtractRoutes; +// }[keyof AllModules]; +// type AllRoutesFlat = readonly Flatten[]; + +// const allRoutes = [...allModules.module1().routes, ...allModules.module2().routes] as AllRoutesFlat; + +// const routeTree = root.addChildren(allRoutes); + +// export const router = createRouter({ +// routeTree, +// context: {}, +// defaultPreload: 'intent', +// scrollRestoration: true, +// defaultStructuralSharing: true, +// defaultPreloadStaleTime: 0, +// }); + +// export type Router = typeof router; + +// // declare module '@tanstack/react-router' { +// // interface Register { +// // router: typeof router +// // } +// // } + +// // router.navigate({to: '/', search: {hasDiscount: true, query: ''}}) diff --git a/packages/springboard/platforms/webapp/tanstack_types_test.tsx b/packages/springboard/platforms/webapp/tanstack_types_test.tsx new file mode 100644 index 00000000..cc91d217 --- /dev/null +++ b/packages/springboard/platforms/webapp/tanstack_types_test.tsx @@ -0,0 +1,15 @@ +import {AnyRoute, createRoute} from '@tanstack/react-router'; +import {rootRoute} from 'springboard/ui/root_route'; +import React from 'react'; + +const routes: AnyRoute[] = [ + createRoute({ + getParentRoute: () => rootRoute, + path: '/my-test', + component: () => { + return ( +
    + ); + }, + }), +]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96dd9519..b834f3b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,9 +33,6 @@ catalogs: react-dom: specifier: ^19.1.0 version: 19.1.0 - react-router-dom: - specifier: ^6.28.1 - version: 6.30.0 reconnecting-websocket: specifier: ^4.4.0 version: 4.4.0 @@ -155,9 +152,6 @@ importers: '@springboardjs/shoelace': specifier: workspace:* version: link:../../packages/springboard/external/shoelace - react-router-dom: - specifier: 'catalog:' - version: 6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) springboard: specifier: workspace:* version: link:../../packages/springboard/core @@ -349,9 +343,6 @@ importers: react-dom: specifier: 'catalog:' version: 19.1.0(react@19.1.0) - react-router-dom: - specifier: 'catalog:' - version: 6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) packages/observability: dependencies: @@ -509,12 +500,9 @@ importers: specifier: workspace:* version: link:../../core devDependencies: - react: - specifier: ^19.1.0 - version: 19.1.0 - react-router-dom: + '@tanstack/react-router': specifier: 'catalog:' - version: 6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 1.132.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) packages/springboard/platforms/node: dependencies: @@ -632,9 +620,6 @@ importers: json-rpc-2.0: specifier: 'catalog:' version: 1.7.0 - react-router-dom: - specifier: ^6 - version: 6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) reconnecting-websocket: specifier: 'catalog:' version: 4.4.0 @@ -1411,10 +1396,6 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@remix-run/router@1.23.0': - resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} - engines: {node: '>=14.0.0'} - '@rollup/rollup-android-arm-eabi@4.41.0': resolution: {integrity: sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==} cpu: [arm] @@ -3563,19 +3544,6 @@ packages: '@types/react': optional: true - react-router-dom@6.30.0: - resolution: {integrity: sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: ^19.1.0 - react-dom: '>=16.8' - - react-router@6.30.0: - resolution: {integrity: sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: ^19.1.0 - react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -5182,8 +5150,6 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@remix-run/router@1.23.0': {} - '@rollup/rollup-android-arm-eabi@4.41.0': optional: true @@ -7657,18 +7623,6 @@ snapshots: optionalDependencies: '@types/react': 19.1.5 - react-router-dom@6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@remix-run/router': 1.23.0 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-router: 6.30.0(react@19.1.0) - - react-router@6.30.0(react@19.1.0): - dependencies: - '@remix-run/router': 1.23.0 - react: 19.1.0 - react-style-singleton@2.2.3(@types/react@19.1.5)(react@19.1.0): dependencies: get-nonce: 1.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3c53460d..1869f8ef 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -27,7 +27,6 @@ catalog: "@types/node": "^20.14.2" hono: "^4.6.7" zod: "^3.23.8" - "react-router-dom": "^6.28.1" esbuild: "^0.25.0" "@tanstack/react-router": "^1.130.12" estree-walker: "^2" From 8a240c5764d5e6cb94c77a167dfbefc3bf173df0 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:04:45 -0400 Subject: [PATCH 9/9] first go at updating docs to include tanstack --- .../guides/registering-ui-routes.md | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/doks/content/docs/springboard/guides/registering-ui-routes.md b/doks/content/docs/springboard/guides/registering-ui-routes.md index b232f601..6929e9a0 100644 --- a/doks/content/docs/springboard/guides/registering-ui-routes.md +++ b/doks/content/docs/springboard/guides/registering-ui-routes.md @@ -15,12 +15,15 @@ seo: --- -Springboard currently uses React Router to register UI routes for the application. The plan is to move to [TanStack Router](https://tanstack.com/router) in the future for better type safety and more features. +Springboard currently uses [TanStack Router](https://tanstack.com/router) to register UI routes for the application. -The `moduleAPI.registerRoute` function allows modules to register their own routes. If the specified route begins with a `/`, it's assumed the route starts at the beginning of the URL. Otherwise, the URL is assumed to be relative to the URL `/modules/(module id)`. +There are two ways to register routes: + +- The module can return a `routes` array of tanstack router routes. These are assumed to be relative to the root route. +- The `moduleAPI.registerRoute` function allows modules to register their own routes. This circumvents tanstack's type safety. ```jsx -import {useParams} from 'react-router'; +import {createRoute} from '@tanstack/react-router'; springboard.registerModule('MyModule', async (moduleAPI) => { // matches "" and "/" @@ -38,9 +41,8 @@ springboard.registerModule('MyModule', async (moduleAPI) => { }); // matches "/modules/MyModule/things/1" - moduleAPI.registerRoute('things/:thingId', () => { - const params = useParams(); - const thingId = params.thingId; + moduleAPI.registerRoute('things/:thingId', ({pathParams}) => { + const thingId = pathParams.thingId; return (
    @@ -48,14 +50,24 @@ springboard.registerModule('MyModule', async (moduleAPI) => { }); // matches "/users/1" - moduleAPI.registerRoute('/users/:userId', () => { - const params = useParams(); - const userId = params.userId; + moduleAPI.registerRoute('/users/:userId', ({pathParams}) => { + const userId = pathParams.userId; return (
    ); }); + + return { + routes: [ + createRoute({ + path: '/my-typed-route', + element: () => ( +
    + ), + }), + ], + }; }); ```