diff --git a/.changeset/fix-multi-router-first-import.md b/.changeset/fix-multi-router-first-import.md new file mode 100644 index 0000000000..f07a25b1e2 --- /dev/null +++ b/.changeset/fix-multi-router-first-import.md @@ -0,0 +1,5 @@ +--- +'@tanstack/router-plugin': patch +--- + +Only run the eager HMR route patch on hot re-evaluations of a route module. On a first import, a same-id route on `window.__TSR_ROUTER__` belongs to a different router in the same window (e.g. module federation host/remote), and patching it corrupted both route trees, causing "Invalid hook call" errors. diff --git a/packages/router-plugin/src/core/hmr/vite-adapter.ts b/packages/router-plugin/src/core/hmr/vite-adapter.ts index ba8d42a80a..df742ace3f 100644 --- a/packages/router-plugin/src/core/hmr/vite-adapter.ts +++ b/packages/router-plugin/src/core/hmr/vite-adapter.ts @@ -22,6 +22,14 @@ export function createViteHmrStatement( const routeIdFallback = typeof opts.routeId === 'string' ? JSON.stringify(opts.routeId) : 'Route.id' + // `hot.data` persists across re-evaluations of this module but starts empty + // on the very first import. The eager patch below (which mirrors the live + // route's generated state onto this module's `Route` export, see #4303) + // must therefore only run on a hot re-evaluation: on a first import, a + // same-id route found on `window.__TSR_ROUTER__` belongs to a *different* + // router living in the same window (e.g. a module-federation host/remote + // pair), and patching it would graft foreign route options and components + // across the two apps (#7921). return [ template.statement( ` @@ -33,8 +41,10 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId } + const isHotReevaluation = hotData['tsr-route-initialized'] === true + hotData['tsr-route-initialized'] = true const existingRoute = - typeof window !== 'undefined' && initialRouteId + isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined if (initialRouteId && existingRoute && existingRoute !== Route) { diff --git a/packages/router-plugin/tests/add-hmr.test.ts b/packages/router-plugin/tests/add-hmr.test.ts index b0481bb7f8..11ae98095e 100644 --- a/packages/router-plugin/tests/add-hmr.test.ts +++ b/packages/router-plugin/tests/add-hmr.test.ts @@ -147,6 +147,22 @@ describe('add-hmr works', () => { expect(output).toContain('"object":{"type":"Identifier","name":"hotData"}') }) + it('gates the eager live-route patch behind a persisted hot-data flag', async () => { + const statement = createRouteHmrStatement([], { + hmrStyle: 'vite', + targetFramework: 'react', + routeId: '/posts', + }) + const output = JSON.stringify(statement) + + // On a first import the module must never patch a route owned by a + // different router in the same window (module federation / multiple + // routers, see #7921). The eager patch may only run on hot + // re-evaluations, detected via `hot.data`, which persists across updates. + expect(output).toContain('tsr-route-initialized') + expect(output).toContain('"name":"isHotReevaluation"') + }) + it('normalizes the generated root route id for Vite HMR', async () => { const statement = createRouteHmrStatement([], { hmrStyle: 'vite', diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx index a463d9ab22..a13023e327 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx @@ -172,7 +172,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createFileRoute-lowercase-components@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createFileRoute-lowercase-components@true.tsx index b04afe83f4..0558254c78 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/createFileRoute-lowercase-components@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createFileRoute-lowercase-components@true.tsx @@ -183,7 +183,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-inline-component@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-inline-component@true.tsx index 9ef182f4b7..d9e29eb622 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-inline-component@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-inline-component@true.tsx @@ -161,7 +161,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-lowercase-components@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-lowercase-components@true.tsx index d3eae5cb36..7b3643ee12 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-lowercase-components@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-lowercase-components@true.tsx @@ -179,7 +179,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute@true.tsx index 9789370efc..84432e6ec6 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute@true.tsx @@ -163,7 +163,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRouteWithContext-type-args@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRouteWithContext-type-args@true.tsx index 6c7073a8f3..42a37399f7 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRouteWithContext-type-args@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRouteWithContext-type-args@true.tsx @@ -166,7 +166,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/explicit-undefined-component@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/explicit-undefined-component@true.tsx index bee793d372..f7b934a5fa 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/explicit-undefined-component@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/explicit-undefined-component@true.tsx @@ -160,7 +160,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx index 75b457b778..5fb0dbe939 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx @@ -172,7 +172,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/multi-component@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/multi-component@true.tsx index 7f28b7b3d3..b869e2e3b9 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/multi-component@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/multi-component@true.tsx @@ -182,7 +182,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/string-literal-keys@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/string-literal-keys@true.tsx index 6bee992948..facd66cb96 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/string-literal-keys@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/string-literal-keys@true.tsx @@ -182,7 +182,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route; diff --git a/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx index 2d1be7e32b..04bbb688ac 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx @@ -143,7 +143,9 @@ if (import.meta.hot) { if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } - const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; + const isHotReevaluation = hotData['tsr-route-initialized'] === true; + hotData['tsr-route-initialized'] = true; + const existingRoute = isHotReevaluation && typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { handleRouteUpdate(initialRouteId, Route); hotData['tsr-route-update-handled'] = Route;