>()
@@ -159,10 +180,6 @@ export function createRouterCodeSplitterPlugin(
sharedBindingsMap.delete(id)
}
- const addHmr =
- (userConfig.codeSplittingOptions?.addHmr ?? true) && !isProduction
- const hmrStyle = userConfig.plugin?.hmr?.style ?? 'vite'
-
const compiledReferenceRoute = compileCodeSplitReferenceRoute({
code,
codeSplitGroupings: splitGroupings,
@@ -176,14 +193,7 @@ export function createRouterCodeSplitterPlugin(
hmrStyle,
hmrRouteId: generatorNodeInfo.routeId,
sharedBindings: sharedBindings.size > 0 ? sharedBindings : undefined,
- compilerPlugins: [
- ...(getReferenceRouteCompilerPlugins({
- targetFramework: userConfig.target,
- addHmr,
- hmrStyle,
- }) ?? []),
- ...(userConfig.codeSplittingOptions?.compilerPlugins ?? []),
- ],
+ compilerPlugins,
})
if (compiledReferenceRoute === null) {
@@ -232,6 +242,7 @@ export function createRouterCodeSplitterPlugin(
filename: id,
splitTargets: grouping,
sharedBindings: resolvedSharedBindings,
+ compilerPlugins: virtualRouteCompilerPlugins,
})
if (debug) {
@@ -276,6 +287,7 @@ export function createRouterCodeSplitterPlugin(
vite: {
configResolved(config) {
+ isProduction = config.command === 'build'
ROOT = config.root
initUserConfig()
@@ -319,12 +331,14 @@ export function createRouterCodeSplitterPlugin(
},
},
- rspack() {
+ rspack(compiler) {
+ isProduction = compiler.options.mode === 'production'
ROOT = process.cwd()
initUserConfig()
},
- webpack() {
+ webpack(compiler) {
+ isProduction = compiler.options.mode === 'production'
ROOT = process.cwd()
initUserConfig()
},
diff --git a/packages/router-plugin/src/core/router-hmr-plugin.ts b/packages/router-plugin/src/core/router-hmr-plugin.ts
index 75dfde11f8..e1dc01a43b 100644
--- a/packages/router-plugin/src/core/router-hmr-plugin.ts
+++ b/packages/router-plugin/src/core/router-hmr-plugin.ts
@@ -1,6 +1,6 @@
import { generateFromAst, logDiff, parseAst } from '@tanstack/router-utils'
import { compileCodeSplitReferenceRoute } from './code-splitter/compilers'
-import { getReferenceRouteCompilerPlugins } from './code-splitter/plugins/framework-plugins'
+import { getFrameworkHmrCompilerPlugins } from './code-splitter/plugins/framework-plugins'
import { createRouteHmrStatement } from './hmr'
import { debug, normalizePath, routeFactoryCallCodeFilter } from './utils'
import { getConfig } from './config'
@@ -50,9 +50,8 @@ export function createRouterHmrPlugin(
const hmrStyle = userConfig.plugin?.hmr?.style ?? 'vite'
if (userConfig.target === 'react') {
- const compilerPlugins = getReferenceRouteCompilerPlugins({
+ const compilerPlugins = getFrameworkHmrCompilerPlugins({
targetFramework: 'react',
- addHmr: true,
hmrStyle,
})
const compiled = compileCodeSplitReferenceRoute({
diff --git a/packages/router-plugin/src/index.ts b/packages/router-plugin/src/index.ts
index e390c046a2..6a6ad158bb 100644
--- a/packages/router-plugin/src/index.ts
+++ b/packages/router-plugin/src/index.ts
@@ -13,8 +13,10 @@ export type {
export type { RouterPluginContext } from './core/router-plugin-context'
export { getObjectPropertyKeyName } from './core/utils'
export type {
+ CodeSplitCompilerPlugin,
ReferenceRouteCompilerPlugin,
ReferenceRouteCompilerPluginContext,
+ VirtualRouteSplitNodeCompilerPluginContext,
} from './core/code-splitter/plugins'
export {
tsrSplit,
diff --git a/packages/router-plugin/tests/add-hmr.test.ts b/packages/router-plugin/tests/add-hmr.test.ts
index 4430cef74c..52c2645409 100644
--- a/packages/router-plugin/tests/add-hmr.test.ts
+++ b/packages/router-plugin/tests/add-hmr.test.ts
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
import { compileCodeSplitReferenceRoute } from '../src/core/code-splitter/compilers'
import { defaultCodeSplitGroupings } from '../src/core/constants'
-import { getReferenceRouteCompilerPlugins } from '../src/core/code-splitter/plugins/framework-plugins'
+import { getFrameworkHmrCompilerPlugins } from '../src/core/code-splitter/plugins/framework-plugins'
import { createRouteHmrStatement } from '../src/core/hmr'
import { frameworks } from './constants'
@@ -32,9 +32,8 @@ describe('add-hmr works', () => {
addHmr: true,
codeSplitGroupings: defaultCodeSplitGroupings,
targetFramework: framework,
- compilerPlugins: getReferenceRouteCompilerPlugins({
+ compilerPlugins: getFrameworkHmrCompilerPlugins({
targetFramework: framework,
- addHmr: true,
}),
})
@@ -57,10 +56,6 @@ describe('add-hmr works', () => {
addHmr: false,
codeSplitGroupings: defaultCodeSplitGroupings,
targetFramework: framework,
- compilerPlugins: getReferenceRouteCompilerPlugins({
- targetFramework: framework,
- addHmr: false,
- }),
})
await expect(compileResult?.code || code).toMatchFileSnapshot(
@@ -86,9 +81,8 @@ describe('add-hmr works', () => {
hmrStyle: 'webpack',
codeSplitGroupings: defaultCodeSplitGroupings,
targetFramework: framework,
- compilerPlugins: getReferenceRouteCompilerPlugins({
+ compilerPlugins: getFrameworkHmrCompilerPlugins({
targetFramework: framework,
- addHmr: true,
hmrStyle: 'webpack',
}),
})
diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createFileRoute-lowercase-components@false.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createFileRoute-lowercase-components@false.tsx
new file mode 100644
index 0000000000..a2983e31bd
--- /dev/null
+++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createFileRoute-lowercase-components@false.tsx
@@ -0,0 +1,12 @@
+const $$splitComponentImporter = () => import('createFileRoute-lowercase-components.tsx?tsr-split=component');
+const $$splitErrorComponentImporter = () => import('createFileRoute-lowercase-components.tsx?tsr-split=errorComponent');
+import { lazyRouteComponent } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
+export const Route = createFileRoute('/lowercase-components')({
+ pendingComponent: pendingComponent,
+ errorComponent: lazyRouteComponent($$splitErrorComponentImporter, 'errorComponent'),
+ component: lazyRouteComponent($$splitComponentImporter, 'component')
+});
+function pendingComponent() {
+ return Pending
;
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..b04afe83f4
--- /dev/null
+++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createFileRoute-lowercase-components@true.tsx
@@ -0,0 +1,204 @@
+const $$splitComponentImporter = () => import('createFileRoute-lowercase-components.tsx?tsr-split=component');
+const $$splitErrorComponentImporter = () => import('createFileRoute-lowercase-components.tsx?tsr-split=errorComponent');
+import { lazyRouteComponent } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
+const TSRSplitErrorComponent = (() => {
+ const hot = import.meta.hot;
+ const hotData = hot ? hot.data ??= {} : undefined;
+ return hotData?.["tsr-split-component:errorComponent"] ?? lazyRouteComponent($$splitErrorComponentImporter, "errorComponent");
+})();
+if (import.meta.hot) {
+ (import.meta.hot.data ??= {})["tsr-split-component:errorComponent"] = TSRSplitErrorComponent;
+}
+const TSRSplitComponent = (() => {
+ const hot = import.meta.hot;
+ const hotData = hot ? hot.data ??= {} : undefined;
+ return hotData?.["tsr-split-component:component"] ?? lazyRouteComponent($$splitComponentImporter, "component");
+})();
+if (import.meta.hot) {
+ (import.meta.hot.data ??= {})["tsr-split-component:component"] = TSRSplitComponent;
+}
+export const Route = createFileRoute('/lowercase-components')({
+ pendingComponent: TSRPendingComponent,
+ errorComponent: TSRSplitErrorComponent,
+ component: TSRSplitComponent
+});
+function TSRPendingComponent() {
+ return Pending
;
+}
+const hot = import.meta.hot;
+if (hot && typeof window !== 'undefined') {
+ hot.data ??= {};
+ const tsrReactRefresh = window.__TSR_REACT_REFRESH__ ??= (() => {
+ const ignoredExportsById = new Map();
+ const previousGetIgnoredExports = window.__getReactRefreshIgnoredExports;
+ window.__getReactRefreshIgnoredExports = ctx => {
+ const ignoredExports = previousGetIgnoredExports?.(ctx) ?? [];
+ const moduleIgnored = ignoredExportsById.get(ctx.id) ?? [];
+ return [...ignoredExports, ...moduleIgnored];
+ };
+ return {
+ ignoredExportsById
+ };
+ })();
+ tsrReactRefresh.ignoredExportsById.set("createFileRoute-lowercase-components.tsx", ['Route']);
+}
+export function TSRFastRefreshAnchor() {
+ return null;
+}
+if (import.meta.hot) {
+ const hot = import.meta.hot;
+ const hotData = hot.data ??= {};
+ const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) {
+ const router = window.__TSR_ROUTER__;
+ const oldRoute = router.routesById[routeId];
+ if (!oldRoute) {
+ return;
+ }
+ ;
+ const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]);
+ const generatedRouteOptions = {};
+ generatedRouteOptionKeys.forEach(key => {
+ if (key in oldRoute.options) {
+ generatedRouteOptions[key] = oldRoute.options[key];
+ }
+ });
+ const removedKeys = new Set();
+ Object.keys(oldRoute.options).forEach(key => {
+ if (!generatedRouteOptionKeys.has(key) && !(key in newRoute.options)) {
+ removedKeys.add(key);
+ delete oldRoute.options[key];
+ }
+ });
+ const oldHasShellComponent = "shellComponent" in oldRoute.options;
+ const newHasShellComponent = "shellComponent" in newRoute.options;
+ const preserveComponentIdentity = oldHasShellComponent === newHasShellComponent;
+ const componentKeys = ["component", "shellComponent", "pendingComponent", "errorComponent", "notFoundComponent"];
+ if (preserveComponentIdentity) {
+ componentKeys.forEach(key => {
+ if (key in oldRoute.options && key in newRoute.options) {
+ newRoute.options[key] = oldRoute.options[key];
+ }
+ });
+ }
+ ;
+ const nextOptions = {
+ ...newRoute.options,
+ ...generatedRouteOptions
+ };
+ oldRoute.options = nextOptions;
+ oldRoute.update(nextOptions);
+ oldRoute._componentsPromise = undefined;
+ oldRoute._lazyPromise = undefined;
+ router.setRoutes(router.buildRouteTree());
+ syncHotRouteExport(oldRoute);
+ router.resolvePathCache.clear();
+ const filter = m => m.routeId === oldRoute.id;
+ const activeMatch = router.stores.matches.get().find(filter);
+ const pendingMatch = router.stores.pendingMatches.get().find(filter);
+ const cachedMatches = router.stores.cachedMatches.get().filter(filter);
+ if (activeMatch || pendingMatch || cachedMatches.length > 0) {
+ if (removedKeys.has("loader") || removedKeys.has("beforeLoad")) {
+ const matchIds = [activeMatch?.id, pendingMatch?.id, ...cachedMatches.map(match => match.id)].filter(Boolean);
+ router.batch(() => {
+ for (const matchId of matchIds) {
+ const store = router.stores.pendingMatchStores.get(matchId) || router.stores.matchStores.get(matchId) || router.stores.cachedMatchStores.get(matchId);
+ if (store) {
+ store.set(prev => {
+ const next = {
+ ...prev
+ };
+ if (removedKeys.has("loader")) {
+ next.loaderData = undefined;
+ }
+ ;
+ if (removedKeys.has("beforeLoad")) {
+ next.__beforeLoadContext = undefined;
+ next.context = rebuildMatchContextWithoutBeforeLoad(next);
+ }
+ ;
+ return next;
+ });
+ }
+ }
+ });
+ }
+ ;
+ router.invalidate({
+ filter,
+ sync: true
+ });
+ }
+ ;
+ function syncHotRouteExport(liveRoute) {
+ newRoute.options = liveRoute.options;
+ newRoute.parentRoute = liveRoute.parentRoute;
+ newRoute._path = liveRoute._path;
+ newRoute._id = liveRoute._id;
+ newRoute._fullPath = liveRoute._fullPath;
+ newRoute._to = liveRoute._to;
+ }
+ function getStoreMatch(matchId) {
+ return router.stores.pendingMatchStores.get(matchId)?.get() || router.stores.matchStores.get(matchId)?.get() || router.stores.cachedMatchStores.get(matchId)?.get();
+ }
+ function getMatchList(matchId) {
+ const pendingMatches = router.stores.pendingMatches.get();
+ if (pendingMatches.some(match => match.id === matchId)) {
+ return pendingMatches;
+ }
+ ;
+ const activeMatches = router.stores.matches.get();
+ if (activeMatches.some(match => match.id === matchId)) {
+ return activeMatches;
+ }
+ ;
+ const cachedMatches = router.stores.cachedMatches.get();
+ if (cachedMatches.some(match => match.id === matchId)) {
+ return cachedMatches;
+ }
+ ;
+ return [];
+ }
+ function getParentMatch(match) {
+ const matchList = getMatchList(match.id);
+ const matchIndex = matchList.findIndex(item => item.id === match.id);
+ if (matchIndex <= 0) {
+ return undefined;
+ }
+ ;
+ const parentMatch = matchList[matchIndex - 1];
+ return getStoreMatch(parentMatch.id) || parentMatch;
+ }
+ function rebuildMatchContextWithoutBeforeLoad(match) {
+ const parentMatch = getParentMatch(match);
+ const getParentContext = router.getParentContext;
+ const parentContext = getParentContext ? getParentContext.call(router, parentMatch) : parentMatch?.context ?? router.options.context;
+ return {
+ ...(parentContext ?? {}),
+ ...(match.__routeContext ?? {})
+ };
+ }
+ };
+ const initialRouteId = Route.id ?? hotData['tsr-route-id'];
+ if (initialRouteId) {
+ hotData['tsr-route-id'] = initialRouteId;
+ }
+ const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined;
+ if (initialRouteId && existingRoute && existingRoute !== Route) {
+ handleRouteUpdate(initialRouteId, Route);
+ hotData['tsr-route-update-handled'] = Route;
+ }
+ hot.accept(newModule => {
+ if (Route && newModule && newModule.Route) {
+ const routeId = hotData['tsr-route-id'] ?? Route.id;
+ if (routeId) {
+ hotData['tsr-route-id'] = routeId;
+ }
+ if (hotData['tsr-route-update-handled'] === newModule.Route) {
+ delete hotData['tsr-route-update-handled'];
+ return;
+ }
+ handleRouteUpdate(routeId, newModule.Route);
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-lowercase-components@false.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-lowercase-components@false.tsx
new file mode 100644
index 0000000000..20c9a07798
--- /dev/null
+++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-lowercase-components@false.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react'
+import { createRootRoute } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ shellComponent: shellComponent,
+ pendingComponent: pendingComponent,
+ errorComponent: errorComponent,
+ component: component,
+})
+
+function shellComponent({ children }: { children: React.ReactNode }) {
+ return {children}
+}
+
+function pendingComponent() {
+ return Pending
+}
+
+function errorComponent() {
+ return Error
+}
+
+function component() {
+ return Hello
+}
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
new file mode 100644
index 0000000000..d3eae5cb36
--- /dev/null
+++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-lowercase-components@true.tsx
@@ -0,0 +1,200 @@
+import * as React from 'react';
+import { createRootRoute } from '@tanstack/react-router';
+export const Route = createRootRoute({
+ shellComponent: TSRShellComponent,
+ pendingComponent: TSRPendingComponent,
+ errorComponent: TSRErrorComponent,
+ component: TSRComponent
+});
+function TSRShellComponent({
+ children
+}: {
+ children: React.ReactNode;
+}) {
+ return {children};
+}
+function TSRPendingComponent() {
+ return Pending
;
+}
+function TSRErrorComponent() {
+ return Error
;
+}
+function TSRComponent() {
+ return Hello
;
+}
+const hot = import.meta.hot;
+if (hot && typeof window !== 'undefined') {
+ hot.data ??= {};
+ const tsrReactRefresh = window.__TSR_REACT_REFRESH__ ??= (() => {
+ const ignoredExportsById = new Map();
+ const previousGetIgnoredExports = window.__getReactRefreshIgnoredExports;
+ window.__getReactRefreshIgnoredExports = ctx => {
+ const ignoredExports = previousGetIgnoredExports?.(ctx) ?? [];
+ const moduleIgnored = ignoredExportsById.get(ctx.id) ?? [];
+ return [...ignoredExports, ...moduleIgnored];
+ };
+ return {
+ ignoredExportsById
+ };
+ })();
+ tsrReactRefresh.ignoredExportsById.set("createRootRoute-lowercase-components.tsx", ['Route']);
+}
+export function TSRFastRefreshAnchor() {
+ return null;
+}
+if (import.meta.hot) {
+ const hot = import.meta.hot;
+ const hotData = hot.data ??= {};
+ const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) {
+ const router = window.__TSR_ROUTER__;
+ const oldRoute = router.routesById[routeId];
+ if (!oldRoute) {
+ return;
+ }
+ ;
+ const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]);
+ const generatedRouteOptions = {};
+ generatedRouteOptionKeys.forEach(key => {
+ if (key in oldRoute.options) {
+ generatedRouteOptions[key] = oldRoute.options[key];
+ }
+ });
+ const removedKeys = new Set();
+ Object.keys(oldRoute.options).forEach(key => {
+ if (!generatedRouteOptionKeys.has(key) && !(key in newRoute.options)) {
+ removedKeys.add(key);
+ delete oldRoute.options[key];
+ }
+ });
+ const oldHasShellComponent = "shellComponent" in oldRoute.options;
+ const newHasShellComponent = "shellComponent" in newRoute.options;
+ const preserveComponentIdentity = oldHasShellComponent === newHasShellComponent;
+ const componentKeys = ["component", "shellComponent", "pendingComponent", "errorComponent", "notFoundComponent"];
+ if (preserveComponentIdentity) {
+ componentKeys.forEach(key => {
+ if (key in oldRoute.options && key in newRoute.options) {
+ newRoute.options[key] = oldRoute.options[key];
+ }
+ });
+ }
+ ;
+ const nextOptions = {
+ ...newRoute.options,
+ ...generatedRouteOptions
+ };
+ oldRoute.options = nextOptions;
+ oldRoute.update(nextOptions);
+ oldRoute._componentsPromise = undefined;
+ oldRoute._lazyPromise = undefined;
+ router.setRoutes(router.buildRouteTree());
+ syncHotRouteExport(oldRoute);
+ router.resolvePathCache.clear();
+ const filter = m => m.routeId === oldRoute.id;
+ const activeMatch = router.stores.matches.get().find(filter);
+ const pendingMatch = router.stores.pendingMatches.get().find(filter);
+ const cachedMatches = router.stores.cachedMatches.get().filter(filter);
+ if (activeMatch || pendingMatch || cachedMatches.length > 0) {
+ if (removedKeys.has("loader") || removedKeys.has("beforeLoad")) {
+ const matchIds = [activeMatch?.id, pendingMatch?.id, ...cachedMatches.map(match => match.id)].filter(Boolean);
+ router.batch(() => {
+ for (const matchId of matchIds) {
+ const store = router.stores.pendingMatchStores.get(matchId) || router.stores.matchStores.get(matchId) || router.stores.cachedMatchStores.get(matchId);
+ if (store) {
+ store.set(prev => {
+ const next = {
+ ...prev
+ };
+ if (removedKeys.has("loader")) {
+ next.loaderData = undefined;
+ }
+ ;
+ if (removedKeys.has("beforeLoad")) {
+ next.__beforeLoadContext = undefined;
+ next.context = rebuildMatchContextWithoutBeforeLoad(next);
+ }
+ ;
+ return next;
+ });
+ }
+ }
+ });
+ }
+ ;
+ router.invalidate({
+ filter,
+ sync: true
+ });
+ }
+ ;
+ function syncHotRouteExport(liveRoute) {
+ newRoute.options = liveRoute.options;
+ newRoute.parentRoute = liveRoute.parentRoute;
+ newRoute._path = liveRoute._path;
+ newRoute._id = liveRoute._id;
+ newRoute._fullPath = liveRoute._fullPath;
+ newRoute._to = liveRoute._to;
+ }
+ function getStoreMatch(matchId) {
+ return router.stores.pendingMatchStores.get(matchId)?.get() || router.stores.matchStores.get(matchId)?.get() || router.stores.cachedMatchStores.get(matchId)?.get();
+ }
+ function getMatchList(matchId) {
+ const pendingMatches = router.stores.pendingMatches.get();
+ if (pendingMatches.some(match => match.id === matchId)) {
+ return pendingMatches;
+ }
+ ;
+ const activeMatches = router.stores.matches.get();
+ if (activeMatches.some(match => match.id === matchId)) {
+ return activeMatches;
+ }
+ ;
+ const cachedMatches = router.stores.cachedMatches.get();
+ if (cachedMatches.some(match => match.id === matchId)) {
+ return cachedMatches;
+ }
+ ;
+ return [];
+ }
+ function getParentMatch(match) {
+ const matchList = getMatchList(match.id);
+ const matchIndex = matchList.findIndex(item => item.id === match.id);
+ if (matchIndex <= 0) {
+ return undefined;
+ }
+ ;
+ const parentMatch = matchList[matchIndex - 1];
+ return getStoreMatch(parentMatch.id) || parentMatch;
+ }
+ function rebuildMatchContextWithoutBeforeLoad(match) {
+ const parentMatch = getParentMatch(match);
+ const getParentContext = router.getParentContext;
+ const parentContext = getParentContext ? getParentContext.call(router, parentMatch) : parentMatch?.context ?? router.options.context;
+ return {
+ ...(parentContext ?? {}),
+ ...(match.__routeContext ?? {})
+ };
+ }
+ };
+ const initialRouteId = Route.id ?? hotData['tsr-route-id'];
+ if (initialRouteId) {
+ hotData['tsr-route-id'] = initialRouteId;
+ }
+ const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined;
+ if (initialRouteId && existingRoute && existingRoute !== Route) {
+ handleRouteUpdate(initialRouteId, Route);
+ hotData['tsr-route-update-handled'] = Route;
+ }
+ hot.accept(newModule => {
+ if (Route && newModule && newModule.Route) {
+ const routeId = hotData['tsr-route-id'] ?? Route.id;
+ if (routeId) {
+ hotData['tsr-route-id'] = routeId;
+ }
+ if (hotData['tsr-route-update-handled'] === newModule.Route) {
+ delete hotData['tsr-route-update-handled'];
+ return;
+ }
+ handleRouteUpdate(routeId, newModule.Route);
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/router-plugin/tests/add-hmr/test-files/react/createFileRoute-lowercase-components.tsx b/packages/router-plugin/tests/add-hmr/test-files/react/createFileRoute-lowercase-components.tsx
new file mode 100644
index 0000000000..a09f8d24e2
--- /dev/null
+++ b/packages/router-plugin/tests/add-hmr/test-files/react/createFileRoute-lowercase-components.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/lowercase-components')({
+ pendingComponent: pendingComponent,
+ errorComponent: errorComponent,
+ component: component,
+})
+
+function pendingComponent() {
+ return Pending
+}
+
+function errorComponent() {
+ return Error
+}
+
+function component() {
+ return Hello
+}
diff --git a/packages/router-plugin/tests/add-hmr/test-files/react/createRootRoute-lowercase-components.tsx b/packages/router-plugin/tests/add-hmr/test-files/react/createRootRoute-lowercase-components.tsx
new file mode 100644
index 0000000000..20c9a07798
--- /dev/null
+++ b/packages/router-plugin/tests/add-hmr/test-files/react/createRootRoute-lowercase-components.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react'
+import { createRootRoute } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ shellComponent: shellComponent,
+ pendingComponent: pendingComponent,
+ errorComponent: errorComponent,
+ component: component,
+})
+
+function shellComponent({ children }: { children: React.ReactNode }) {
+ return {children}
+}
+
+function pendingComponent() {
+ return Pending
+}
+
+function errorComponent() {
+ return Error
+}
+
+function component() {
+ return Hello
+}
diff --git a/packages/router-plugin/tests/code-splitter.test.ts b/packages/router-plugin/tests/code-splitter.test.ts
index ae64e2778b..967f102e4d 100644
--- a/packages/router-plugin/tests/code-splitter.test.ts
+++ b/packages/router-plugin/tests/code-splitter.test.ts
@@ -20,6 +20,7 @@ import {
} from '../src/core/code-splitter/compilers'
import { createIdentifier } from '@tanstack/router-utils'
import { defaultCodeSplitGroupings } from '../src/core/constants'
+import { getFrameworkHmrCompilerPlugins } from '../src/core/code-splitter/plugins/framework-plugins'
import { frameworks } from './constants'
import type { CodeSplitGroupings } from '../src/core/constants'
@@ -165,6 +166,62 @@ describe('code-splitter works', () => {
})
})
+describe('React Refresh virtual route compiler plugin', () => {
+ const splitComponentCases = [
+ { splitType: 'component', splitIdentifier: 'SplitComponent' },
+ {
+ splitType: 'pendingComponent',
+ splitIdentifier: 'SplitPendingComponent',
+ },
+ { splitType: 'errorComponent', splitIdentifier: 'SplitErrorComponent' },
+ ] as const
+
+ function createRouteCode(splitType: string) {
+ return `
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({ ${splitType} })
+
+function ${splitType}() {
+ return lowercase function declaration
+}
+`
+ }
+
+ it.each(splitComponentCases)(
+ 'renames a lowercase $splitType binding when HMR is enabled',
+ ({ splitType, splitIdentifier }) => {
+ const result = compileCodeSplitVirtualRoute({
+ code: createRouteCode(splitType),
+ filename: `lowercase-function-declaration.tsx?tsr-split=${splitType}`,
+ splitTargets: [splitType],
+ compilerPlugins: getFrameworkHmrCompilerPlugins({
+ targetFramework: 'react',
+ }),
+ })
+
+ expect(result.code).toContain(`function ${splitIdentifier}()`)
+ expect(result.code).toContain(
+ `export { ${splitIdentifier} as ${splitType} }`,
+ )
+ },
+ )
+
+ it.each(splitComponentCases)(
+ 'keeps a lowercase $splitType binding when HMR is disabled',
+ ({ splitType }) => {
+ const result = compileCodeSplitVirtualRoute({
+ code: createRouteCode(splitType),
+ filename: `lowercase-function-declaration.tsx?tsr-split=${splitType}`,
+ splitTargets: [splitType],
+ })
+
+ expect(result.code).toContain(`function ${splitType}()`)
+ expect(result.code).toContain(`export { ${splitType} }`)
+ },
+ )
+})
+
describe('computeSharedBindings fast paths', () => {
it('returns empty when only one split group is present (default groupings)', () => {
const code = `
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration.tsx
new file mode 100644
index 0000000000..b2f908ab45
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration.tsx
@@ -0,0 +1,12 @@
+const $$splitErrorComponentImporter = () => import('lowercase-function-declaration.tsx?tsr-split=errorComponent');
+const $$splitComponentImporter = () => import('lowercase-function-declaration.tsx?tsr-split=component');
+import { lazyRouteComponent } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
+export const Route = createFileRoute('/')({
+ component: lazyRouteComponent($$splitComponentImporter, 'component'),
+ pendingComponent,
+ errorComponent: lazyRouteComponent($$splitErrorComponentImporter, 'errorComponent')
+});
+function pendingComponent() {
+ return lowercase pending function declaration
;
+}
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@component.tsx
new file mode 100644
index 0000000000..65735a0f20
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@component.tsx
@@ -0,0 +1,4 @@
+function component() {
+ return lowercase function declaration
;
+}
+export { component };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@errorComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@errorComponent.tsx
new file mode 100644
index 0000000000..acc1c0be8d
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@errorComponent.tsx
@@ -0,0 +1,4 @@
+function errorComponent() {
+ return lowercase error function declaration
;
+}
+export { errorComponent };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@notFoundComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@notFoundComponent.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@shared.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/1-default/lowercase-function-declaration@shared.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration.tsx
new file mode 100644
index 0000000000..75657cb44a
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration.tsx
@@ -0,0 +1,10 @@
+const $$splitErrorComponentImporter = () => import('lowercase-function-declaration.tsx?tsr-split=component---errorComponent---notFoundComponent---pendingComponent');
+const $$splitPendingComponentImporter = () => import('lowercase-function-declaration.tsx?tsr-split=component---errorComponent---notFoundComponent---pendingComponent');
+const $$splitComponentImporter = () => import('lowercase-function-declaration.tsx?tsr-split=component---errorComponent---notFoundComponent---pendingComponent');
+import { lazyRouteComponent } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
+export const Route = createFileRoute('/')({
+ component: lazyRouteComponent($$splitComponentImporter, 'component'),
+ pendingComponent: lazyRouteComponent($$splitPendingComponentImporter, 'pendingComponent'),
+ errorComponent: lazyRouteComponent($$splitErrorComponentImporter, 'errorComponent')
+});
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration@component---errorComponent---notFoundComponent---pendingComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration@component---errorComponent---notFoundComponent---pendingComponent.tsx
new file mode 100644
index 0000000000..86876d616e
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration@component---errorComponent---notFoundComponent---pendingComponent.tsx
@@ -0,0 +1,12 @@
+function component() {
+ return lowercase function declaration
;
+}
+function pendingComponent() {
+ return lowercase pending function declaration
;
+}
+function errorComponent() {
+ return lowercase error function declaration
;
+}
+export { component };
+export { pendingComponent };
+export { errorComponent };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration@loader.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration@loader.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration@shared.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/2-components-combined-loader-separate/lowercase-function-declaration@shared.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration.tsx
new file mode 100644
index 0000000000..574c6d4c2b
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration.tsx
@@ -0,0 +1,10 @@
+const $$splitErrorComponentImporter = () => import('lowercase-function-declaration.tsx?tsr-split=errorComponent');
+const $$splitPendingComponentImporter = () => import('lowercase-function-declaration.tsx?tsr-split=component---loader---notFoundComponent---pendingComponent');
+const $$splitComponentImporter = () => import('lowercase-function-declaration.tsx?tsr-split=component---loader---notFoundComponent---pendingComponent');
+import { lazyRouteComponent } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
+export const Route = createFileRoute('/')({
+ component: lazyRouteComponent($$splitComponentImporter, 'component'),
+ pendingComponent: lazyRouteComponent($$splitPendingComponentImporter, 'pendingComponent'),
+ errorComponent: lazyRouteComponent($$splitErrorComponentImporter, 'errorComponent')
+});
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration@component---loader---notFoundComponent---pendingComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration@component---loader---notFoundComponent---pendingComponent.tsx
new file mode 100644
index 0000000000..382621ea9a
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration@component---loader---notFoundComponent---pendingComponent.tsx
@@ -0,0 +1,8 @@
+function component() {
+ return lowercase function declaration
;
+}
+function pendingComponent() {
+ return lowercase pending function declaration
;
+}
+export { component };
+export { pendingComponent };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration@errorComponent.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration@errorComponent.tsx
new file mode 100644
index 0000000000..acc1c0be8d
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration@errorComponent.tsx
@@ -0,0 +1,4 @@
+function errorComponent() {
+ return lowercase error function declaration
;
+}
+export { errorComponent };
\ No newline at end of file
diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration@shared.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react/3-all-combined-errorComponent-separate/lowercase-function-declaration@shared.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/router-plugin/tests/code-splitter/test-files/react/lowercase-function-declaration.tsx b/packages/router-plugin/tests/code-splitter/test-files/react/lowercase-function-declaration.tsx
new file mode 100644
index 0000000000..bfc37b1d6a
--- /dev/null
+++ b/packages/router-plugin/tests/code-splitter/test-files/react/lowercase-function-declaration.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component,
+ pendingComponent,
+ errorComponent,
+})
+
+function component() {
+ return lowercase function declaration
+}
+
+function pendingComponent() {
+ return lowercase pending function declaration
+}
+
+function errorComponent() {
+ return lowercase error function declaration
+}
diff --git a/packages/router-plugin/tests/router-plugin-context.test.ts b/packages/router-plugin/tests/router-plugin-context.test.ts
index 40664c50a8..ecb6e7e167 100644
--- a/packages/router-plugin/tests/router-plugin-context.test.ts
+++ b/packages/router-plugin/tests/router-plugin-context.test.ts
@@ -1,8 +1,9 @@
import path from 'node:path'
import * as t from '@babel/types'
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
import { parseAst } from '@tanstack/router-utils'
import { createRouterCodeSplitterPlugin } from '../src/core/router-code-splitter-plugin'
+import { unpluginRouterComposedFactory } from '../src/core/router-composed-plugin'
import { createRouterHmrPlugin } from '../src/core/router-hmr-plugin'
import { createRouterPluginContext } from '../src/core/router-plugin-context'
import { normalizePath } from '../src/core/utils'
@@ -11,19 +12,30 @@ import type { UnpluginOptions, TransformResult } from 'unplugin'
const referencePluginName =
'tanstack-router:code-splitter:compile-reference-file'
+const virtualPluginName = 'tanstack-router:code-splitter:compile-virtual-file'
-function getReferencePlugin(
+function getCodeSplitterPlugin(
plugins: ReturnType,
+ pluginName: string,
): UnpluginOptions {
const pluginArray = Array.isArray(plugins) ? plugins : [plugins]
- const plugin = pluginArray.find((item) => item.name === referencePluginName)
+ const plugin = pluginArray.find((item) => item.name === pluginName)
if (!plugin) {
- throw new Error('Reference code-splitter plugin not found')
+ throw new Error(`Code-splitter plugin "${pluginName}" not found`)
}
return plugin
}
-async function configurePlugin(plugin: UnpluginOptions) {
+function getReferencePlugin(
+ plugins: ReturnType,
+) {
+ return getCodeSplitterPlugin(plugins, referencePluginName)
+}
+
+async function configurePlugin(
+ plugin: UnpluginOptions,
+ command: 'serve' | 'build' = 'serve',
+) {
const hook = plugin.vite?.configResolved
if (!hook) {
return
@@ -31,6 +43,7 @@ async function configurePlugin(plugin: UnpluginOptions) {
const config = {
root: process.cwd(),
+ command,
plugins: [{ name: referencePluginName }],
} as never
@@ -77,6 +90,87 @@ function countProgramHotDeclarations(code: string) {
}
describe('router plugin context', () => {
+ it.each([
+ { mode: 'production', production: true },
+ { mode: 'development with addHmr disabled', production: false },
+ ])(
+ 'does not run React HMR compiler plugins in $mode',
+ async ({ production }) => {
+ const routeFile = normalizePath(
+ path.join(process.cwd(), 'src/routes/lowercase.tsx'),
+ )
+ const routeCode = `
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/lowercase')({ component })
+
+function component() {
+ return Hello
+}
+`
+
+ const context = createRouterPluginContext()
+ context.routesByFile.set(routeFile, { routeId: '/lowercase' })
+
+ if (production) {
+ vi.stubEnv('NODE_ENV', 'production')
+ }
+
+ try {
+ const plugins = createRouterCodeSplitterPlugin(
+ {
+ target: 'react',
+ autoCodeSplitting: true,
+ codeSplittingOptions: production ? undefined : { addHmr: false },
+ },
+ context,
+ )
+ const referencePlugin = getReferencePlugin(plugins)
+ const virtualPlugin = getCodeSplitterPlugin(plugins, virtualPluginName)
+
+ await configurePlugin(referencePlugin, production ? 'build' : 'serve')
+
+ const referenceCode = getCode(
+ await transformReferenceRoute(referencePlugin, routeCode, routeFile),
+ )
+ const virtualCode = getCode(
+ await transformReferenceRoute(
+ virtualPlugin,
+ routeCode,
+ `${routeFile}?tsr-split=component`,
+ ),
+ )
+
+ expect(referenceCode).not.toContain('TSRFastRefreshAnchor')
+ expect(virtualCode).toContain('function component()')
+ expect(virtualCode).toContain('export { component }')
+ expect(virtualCode).not.toContain('SplitComponent')
+ } finally {
+ if (production) {
+ vi.unstubAllEnvs()
+ }
+ }
+ },
+ )
+
+ it('does not install the standalone route HMR plugin in production', () => {
+ vi.stubEnv('NODE_ENV', 'production')
+
+ try {
+ const plugins = unpluginRouterComposedFactory(
+ { target: 'react', autoCodeSplitting: false },
+ { framework: 'vite' },
+ )
+
+ const pluginArray = Array.isArray(plugins) ? plugins : [plugins]
+ expect(
+ pluginArray.some((plugin) => plugin.name === 'tanstack-router:hmr'),
+ ).toBe(false)
+ } finally {
+ vi.unstubAllEnvs()
+ }
+ })
+
it('keeps multiple code-splitter instances isolated by explicit context', async () => {
const routeFile = normalizePath(
path.join(process.cwd(), 'src/routes-a/owned.tsx'),