Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-multi-router-first-import.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 11 additions & 1 deletion packages/router-plugin/src/core/hmr/vite-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
`
Expand All @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions packages/router-plugin/tests/add-hmr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down