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/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..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: () => ( +
+ ), + }), + ], + }; }); ``` diff --git a/package.json b/package.json index 36c9f686..98b9b3b3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "fix": "TURBO_NO_UPDATE_NOTIFIER=true turbo run fix", "ci": "NODE_MODULES_PARENT_FOLDER=$PWD TURBO_NO_UPDATE_NOTIFIER=true turbo run lint check-types build test", "heroku-postbuild": "NODE_ENV=production npm run build-saas", + "test-tanstack-app": "npx tsx packages/springboard/cli/src/cli.ts dev ./apps/small_apps/test_tanstack_app/test_tanstack_app.tsx", "build-desktop": "RUN_SIDECAR_FROM_WEBVIEW=true npx tsx packages/springboard/cli/src/cli.ts build ./apps/jamtools/modules/index.ts --platforms desktop", "build-all": "RUN_SIDECAR_FROM_WEBVIEW=true npx tsx packages/springboard/cli/src/cli.ts build ./apps/small_apps/empty_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" 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 a8d0f9ed..8c2efaed 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 'springboard/ui/root_route'; type ModuleId = string; @@ -52,14 +55,16 @@ export class MacroModule implements Module { constructor(private coreDeps: CoreDependencies, private moduleDeps: ModuleDependencies) { } - routes = { - '': { + routes = [ + createRoute({ + getParentRoute: () => rootRoute, + path: '/modules/macro', component: () => { const mod = MacroModule.use(); return ; }, - }, - }; + }), + ]; state: MacroConfigState = { configs: {}, diff --git a/packages/jamtools/core/package.json b/packages/jamtools/core/package.json index bcf03a60..78f0731a 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:*", "svelte": ">= 5" @@ -31,6 +32,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/dashboards/dashboards_module.tsx b/packages/jamtools/features/modules/dashboards/dashboards_module.tsx index 541cba7a..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'; @@ -28,16 +26,16 @@ 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 111895fa..4374d615 100644 --- a/packages/jamtools/features/modules/hand_raiser_module.tsx +++ b/packages/jamtools/features/modules/hand_raiser_module.tsx @@ -46,7 +46,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 a50ad5e0..bf4b257e 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/ui/root_route'; type WledClientStatus = { url: string; @@ -47,19 +49,19 @@ export class WledModule implements Module { cleanup: (() => void)[] = []; - routes = { - '': { + routes = [ + createRoute({ + getParentRoute: () => rootRoute, + path: '/wled', component: () => { - const mod = WledModule.use(); - return (
-                        {JSON.stringify(mod.state)}
+                        {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 ( ); }); - 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..bdac35f5 100644 --- a/packages/jamtools/features/package.json +++ b/packages/jamtools/features/package.json @@ -17,18 +17,19 @@ "devDependencies": { "@jamtools/core": "workspace:*", "@springboardjs/shoelace": "workspace:*", + "@tanstack/react-router": "catalog:", "@types/qrcode": "^1.5.5", "@types/react": "catalog:", "@types/react-dom": "catalog:", "react": "catalog:", - "react-dom": "catalog:", - "react-router-dom": "catalog:" + "react-dom": "catalog:" }, "config": { "dir": "../../../configs" }, "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 b2d24883..6de62429 100644 --- a/packages/springboard/core/engine/module_api.ts +++ b/packages/springboard/core/engine/module_api.ts @@ -1,7 +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 {rootRoute} from 'springboard/ui/root_route'; type ActionConfigOptions = object; @@ -87,23 +91,38 @@ 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(); - } - }; + registerRoute = (routePath: TRoutePath, Component: React.ElementType<{ + navigate: (route: string) => void; + pathParams: ResolveParams; + searchParams: Record; + }>) => { + const route = createRoute({ + path: routePath, + getParentRoute: () => rootRoute, + 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/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 b47202f1..e3d1c11e 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 {AnyRoute} 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?: AnyRoute[]; applicationShell?: React.ElementType>; }; diff --git a/packages/springboard/core/package.json b/packages/springboard/core/package.json index 4d412650..1c2624db 100644 --- a/packages/springboard/core/package.json +++ b/packages/springboard/core/package.json @@ -28,9 +28,11 @@ "src", "test", "types", + "ui", "utils" ], "peerDependencies": { + "@tanstack/react-router": "^1", "json-rpc-2.0": "catalog:", "immer": "catalog:", "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -43,6 +45,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/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/core/ui/root_route.tsx b/packages/springboard/core/ui/root_route.tsx new file mode 100644 index 00000000..7a85b8d3 --- /dev/null +++ b/packages/springboard/core/ui/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/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/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/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 37078f64..3c61b63e 100644 --- a/packages/springboard/platforms/webapp/frontend_routes.tsx +++ b/packages/springboard/platforms/webapp/frontend_routes.tsx @@ -1,157 +1,65 @@ 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 'springboard/ui/root_route'; + +// 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; +type AllRoutes = { + [K in keyof AllModules]: ExtractRoutes; +}[keyof AllModules]; +type AllRoutesFlat = readonly Flatten[]; + +// creates a strongly-typed router based on all registered modules that return a `routes` property +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 routerContructor = (globalThis as {useHashRouter?: boolean}).useHashRouter ? createHashRouter : createBrowserRouter; + const typedRoutes = allModuleRoutes as unknown as AllRoutesFlat; - const allRoutes: RouteObject[] = [ - ...rootRouteObjects, - { - path: '/modules', - children: moduleRoutes, - }, - { - path: '/routes', - element: - }, - ]; + const router = createAppRouter(typedRoutes); - if (!rootRouteObjects.find(r => r.path === '/')) { - allRoutes.push({ - path: '/', - element: - }); + // Expose router globally for testing + if (typeof window !== 'undefined') { + (window as any).tsRouter = router; } - 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/layout.tsx b/packages/springboard/platforms/webapp/layout.tsx index 26890a7d..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<{ @@ -9,59 +7,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; + + // const ApplicationShell = useApplicationShell(props.modules); - if (!ApplicationShell) { - return props.children; - } + // if (!ApplicationShell) { + // return props.children; + // } - return ( - - {props.children} - - ); + // return ( + // + // {props.children} + // + // ); }; diff --git a/packages/springboard/platforms/webapp/package.json b/packages/springboard/platforms/webapp/package.json index e1c3ae29..6b124104 100644 --- a/packages/springboard/platforms/webapp/package.json +++ b/packages/springboard/platforms/webapp/package.json @@ -7,14 +7,15 @@ "fix": "npm run lint -- --fix" }, "peerDependencies": { - "springboard": "workspace:*", - "react-router-dom": "^6" + "@tanstack/react-router": "^1", + "springboard": "workspace:*" }, "dependencies": { "json-rpc-2.0": "catalog:", "reconnecting-websocket": "catalog:" }, "devDependencies": { + "@tanstack/react-router": "catalog:", "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", 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/packages/springboard/platforms/webapp/test_tanstack_module.tsx b/packages/springboard/platforms/webapp/test_tanstack_module.tsx new file mode 100644 index 00000000..f557c9b4 --- /dev/null +++ b/packages/springboard/platforms/webapp/test_tanstack_module.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import {createRoute, getRouteApi, useParams, useRouter, useSearch} from '@tanstack/react-router'; +import springboard from 'springboard'; +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!'); + + const actions = moduleAPI.createActions({ + updateMessage: async (args: {newMessage: string}) => { + messageState.setState(args.newMessage); + }, + }); + + return { + routes: [ + createRoute({ + getParentRoute: () => moduleAPI.rootRoute, + path: '/', + component: () => { + return ( + + ); + }, + }), + createRoute({ + 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: () => 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: {search2.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(); + + 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 0d68f62d..b834f3b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,9 @@ settings: catalogs: default: + '@tanstack/react-router': + specifier: ^1.130.12 + version: 1.132.2 '@types/node': specifier: ^20.14.2 version: 20.17.48 @@ -30,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 @@ -152,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 @@ -279,6 +276,9 @@ importers: '@springboardjs/platforms-browser': specifier: workspace:* version: link:../../springboard/platforms/webapp + '@tanstack/react-router': + specifier: 'catalog:' + version: 1.132.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/react': specifier: 'catalog:' version: 19.1.5 @@ -325,6 +325,9 @@ importers: '@springboardjs/shoelace': specifier: workspace:* version: link:../../springboard/external/shoelace + '@tanstack/react-router': + specifier: 'catalog:' + version: 1.132.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/qrcode': specifier: ^1.5.5 version: 1.5.5 @@ -340,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: @@ -413,6 +413,9 @@ importers: specifier: ^8.18.0 version: 8.18.2 devDependencies: + '@tanstack/react-router': + specifier: 'catalog:' + version: 1.132.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/node': specifier: 'catalog:' version: 20.17.48 @@ -497,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: @@ -620,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 @@ -630,6 +627,9 @@ importers: specifier: workspace:* version: link:../../core devDependencies: + '@tanstack/react-router': + specifier: 'catalog:' + version: 1.132.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/node': specifier: 'catalog:' version: 20.17.48 @@ -1396,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] @@ -1524,6 +1520,30 @@ packages: peerDependencies: acorn: ^8.9.0 + '@tanstack/history@1.132.0': + resolution: {integrity: sha512-GG2R9I6QSlbNR9fEuX2sQCigY6K28w51h2634TWmkaHXlzQw+rWuIWr4nAGM9doA+kWRi1LFSFMvAiG3cOqjXQ==} + engines: {node: '>=12'} + + '@tanstack/react-router@1.132.2': + resolution: {integrity: sha512-667txdisNZVLS8jZnu8HNe8fhQAvayMobUBAGsLjMN7YWOT9F9YwGA1tHugtm0PkfS6k4aevBcNpCKZbJsHc5w==} + engines: {node: '>=12'} + peerDependencies: + react: ^19.1.0 + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.7.7': + resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} + peerDependencies: + react: ^19.1.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.132.2': + resolution: {integrity: sha512-PDaEp1tmBirGaNDtrV6AS7awbO42GCegNTxMi0H1mwgWccDwp6RUS7nOF33jPzfBJXhDJ0xFBNdRI3ItakZgug==} + engines: {node: '>=12'} + + '@tanstack/store@0.7.7': + resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@tauri-apps/api@2.5.0': resolution: {integrity: sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==} @@ -2154,6 +2174,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -2912,6 +2935,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==} @@ -3517,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'} @@ -3676,6 +3690,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==} @@ -3926,6 +3950,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==} @@ -4191,6 +4221,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==} @@ -5115,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 @@ -5209,6 +5242,38 @@ snapshots: dependencies: acorn: 8.14.1 + '@tanstack/history@1.132.0': {} + + '@tanstack/react-router@1.132.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/history': 1.132.0 + '@tanstack/react-store': 0.7.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/router-core': 1.132.2 + 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.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/store': 0.7.7 + 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.132.2': + dependencies: + '@tanstack/history': 1.132.0 + '@tanstack/store': 0.7.7 + cookie-es: 2.0.0 + 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.7': {} + '@tauri-apps/api@2.5.0': {} '@tauri-apps/plugin-shell@2.2.1': @@ -5989,6 +6054,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@2.0.0: {} + cookie@0.7.2: {} cosmiconfig@7.1.0: @@ -6883,6 +6950,8 @@ snapshots: isarray@2.0.5: {} + isbot@5.1.29: {} + isexe@2.0.0: {} isomorphic-ws@4.0.1(ws@8.18.2): @@ -7554,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 @@ -7744,6 +7801,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: @@ -8016,6 +8079,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: {} @@ -8288,6 +8355,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): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0f81152e..1869f8ef 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -27,6 +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"