@tanstack/router-plugin 1.167.21 → 1.167.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/cjs/core/code-splitter/compilers.cjs +8 -4
  2. package/dist/cjs/core/code-splitter/compilers.cjs.map +1 -1
  3. package/dist/cjs/core/code-splitter/plugins/framework-plugins.cjs +8 -5
  4. package/dist/cjs/core/code-splitter/plugins/framework-plugins.cjs.map +1 -1
  5. package/dist/cjs/core/code-splitter/plugins/framework-plugins.d.cts +2 -2
  6. package/dist/cjs/core/code-splitter/plugins/react-refresh-ignored-route-exports.cjs +5 -9
  7. package/dist/cjs/core/code-splitter/plugins/react-refresh-ignored-route-exports.cjs.map +1 -1
  8. package/dist/cjs/core/code-splitter/plugins/react-refresh-ignored-route-exports.d.cts +1 -3
  9. package/dist/cjs/core/code-splitter/plugins/react-stable-hmr-split-route-components.cjs +6 -4
  10. package/dist/cjs/core/code-splitter/plugins/react-stable-hmr-split-route-components.cjs.map +1 -1
  11. package/dist/cjs/core/code-splitter/plugins/react-stable-hmr-split-route-components.d.cts +3 -2
  12. package/dist/cjs/core/code-splitter/plugins.d.cts +3 -2
  13. package/dist/cjs/core/config.cjs +1 -1
  14. package/dist/cjs/core/config.cjs.map +1 -1
  15. package/dist/cjs/core/config.d.cts +26 -18
  16. package/dist/cjs/core/{route-hmr-statement.cjs → hmr/handle-route-update.cjs} +39 -25
  17. package/dist/cjs/core/hmr/handle-route-update.cjs.map +1 -0
  18. package/dist/cjs/core/hmr/handle-route-update.d.cts +1 -0
  19. package/dist/cjs/core/hmr/index.d.cts +5 -0
  20. package/dist/cjs/core/hmr/select-adapter.cjs +20 -0
  21. package/dist/cjs/core/hmr/select-adapter.cjs.map +1 -0
  22. package/dist/cjs/core/hmr/select-adapter.d.cts +13 -0
  23. package/dist/cjs/core/hmr/vite-adapter.cjs +36 -0
  24. package/dist/cjs/core/hmr/vite-adapter.cjs.map +1 -0
  25. package/dist/cjs/core/hmr/vite-adapter.d.cts +12 -0
  26. package/dist/cjs/core/hmr/webpack-adapter.cjs +64 -0
  27. package/dist/cjs/core/hmr/webpack-adapter.cjs.map +1 -0
  28. package/dist/cjs/core/hmr/webpack-adapter.d.cts +20 -0
  29. package/dist/cjs/core/router-code-splitter-plugin.cjs +5 -5
  30. package/dist/cjs/core/router-code-splitter-plugin.cjs.map +1 -1
  31. package/dist/cjs/core/router-composed-plugin.cjs +2 -1
  32. package/dist/cjs/core/router-composed-plugin.cjs.map +1 -1
  33. package/dist/cjs/core/router-composed-plugin.d.cts +1 -1
  34. package/dist/cjs/core/router-hmr-plugin.cjs +17 -11
  35. package/dist/cjs/core/router-hmr-plugin.cjs.map +1 -1
  36. package/dist/cjs/esbuild.d.cts +14 -14
  37. package/dist/cjs/rspack.cjs +22 -3
  38. package/dist/cjs/rspack.cjs.map +1 -1
  39. package/dist/cjs/rspack.d.cts +3 -3
  40. package/dist/cjs/vite.d.cts +14 -14
  41. package/dist/cjs/webpack.cjs +19 -3
  42. package/dist/cjs/webpack.cjs.map +1 -1
  43. package/dist/cjs/webpack.d.cts +3 -3
  44. package/dist/esm/core/code-splitter/compilers.js +7 -3
  45. package/dist/esm/core/code-splitter/compilers.js.map +1 -1
  46. package/dist/esm/core/code-splitter/plugins/framework-plugins.d.ts +2 -2
  47. package/dist/esm/core/code-splitter/plugins/framework-plugins.js +8 -5
  48. package/dist/esm/core/code-splitter/plugins/framework-plugins.js.map +1 -1
  49. package/dist/esm/core/code-splitter/plugins/react-refresh-ignored-route-exports.d.ts +1 -3
  50. package/dist/esm/core/code-splitter/plugins/react-refresh-ignored-route-exports.js +4 -8
  51. package/dist/esm/core/code-splitter/plugins/react-refresh-ignored-route-exports.js.map +1 -1
  52. package/dist/esm/core/code-splitter/plugins/react-stable-hmr-split-route-components.d.ts +3 -2
  53. package/dist/esm/core/code-splitter/plugins/react-stable-hmr-split-route-components.js +5 -3
  54. package/dist/esm/core/code-splitter/plugins/react-stable-hmr-split-route-components.js.map +1 -1
  55. package/dist/esm/core/code-splitter/plugins.d.ts +3 -2
  56. package/dist/esm/core/config.d.ts +26 -18
  57. package/dist/esm/core/config.js +1 -1
  58. package/dist/esm/core/config.js.map +1 -1
  59. package/dist/esm/core/hmr/handle-route-update.d.ts +1 -0
  60. package/dist/esm/core/{route-hmr-statement.js → hmr/handle-route-update.js} +39 -23
  61. package/dist/esm/core/hmr/handle-route-update.js.map +1 -0
  62. package/dist/esm/core/hmr/index.d.ts +5 -0
  63. package/dist/esm/core/hmr/select-adapter.d.ts +13 -0
  64. package/dist/esm/core/hmr/select-adapter.js +20 -0
  65. package/dist/esm/core/hmr/select-adapter.js.map +1 -0
  66. package/dist/esm/core/hmr/vite-adapter.d.ts +12 -0
  67. package/dist/esm/core/hmr/vite-adapter.js +34 -0
  68. package/dist/esm/core/hmr/vite-adapter.js.map +1 -0
  69. package/dist/esm/core/hmr/webpack-adapter.d.ts +20 -0
  70. package/dist/esm/core/hmr/webpack-adapter.js +62 -0
  71. package/dist/esm/core/hmr/webpack-adapter.js.map +1 -0
  72. package/dist/esm/core/router-code-splitter-plugin.js +5 -5
  73. package/dist/esm/core/router-code-splitter-plugin.js.map +1 -1
  74. package/dist/esm/core/router-composed-plugin.d.ts +1 -1
  75. package/dist/esm/core/router-composed-plugin.js +2 -1
  76. package/dist/esm/core/router-composed-plugin.js.map +1 -1
  77. package/dist/esm/core/router-hmr-plugin.js +17 -11
  78. package/dist/esm/core/router-hmr-plugin.js.map +1 -1
  79. package/dist/esm/esbuild.d.ts +14 -14
  80. package/dist/esm/rspack.d.ts +3 -3
  81. package/dist/esm/rspack.js +22 -3
  82. package/dist/esm/rspack.js.map +1 -1
  83. package/dist/esm/vite.d.ts +14 -14
  84. package/dist/esm/webpack.d.ts +3 -3
  85. package/dist/esm/webpack.js +19 -3
  86. package/dist/esm/webpack.js.map +1 -1
  87. package/package.json +6 -6
  88. package/src/core/code-splitter/compilers.ts +4 -2
  89. package/src/core/code-splitter/plugins/framework-plugins.ts +7 -8
  90. package/src/core/code-splitter/plugins/react-refresh-ignored-route-exports.ts +2 -8
  91. package/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts +10 -6
  92. package/src/core/code-splitter/plugins.ts +3 -2
  93. package/src/core/config.ts +11 -2
  94. package/src/core/{route-hmr-statement.ts → hmr/handle-route-update.ts} +85 -39
  95. package/src/core/hmr/index.ts +5 -0
  96. package/src/core/hmr/select-adapter.ts +32 -0
  97. package/src/core/hmr/vite-adapter.ts +47 -0
  98. package/src/core/hmr/webpack-adapter.ts +110 -0
  99. package/src/core/router-code-splitter-plugin.ts +5 -7
  100. package/src/core/router-composed-plugin.ts +8 -3
  101. package/src/core/router-hmr-plugin.ts +12 -9
  102. package/src/rspack.ts +37 -9
  103. package/src/webpack.ts +22 -9
  104. package/dist/cjs/core/hmr-hot-expression.cjs +0 -27
  105. package/dist/cjs/core/hmr-hot-expression.cjs.map +0 -1
  106. package/dist/cjs/core/hmr-hot-expression.d.cts +0 -6
  107. package/dist/cjs/core/route-hmr-statement.cjs.map +0 -1
  108. package/dist/cjs/core/route-hmr-statement.d.cts +0 -4
  109. package/dist/esm/core/hmr-hot-expression.d.ts +0 -6
  110. package/dist/esm/core/hmr-hot-expression.js +0 -23
  111. package/dist/esm/core/hmr-hot-expression.js.map +0 -1
  112. package/dist/esm/core/route-hmr-statement.d.ts +0 -4
  113. package/dist/esm/core/route-hmr-statement.js.map +0 -1
  114. package/src/core/hmr-hot-expression.ts +0 -31
@@ -1,12 +1,11 @@
1
1
  import * as template from '@babel/template'
2
2
  import * as t from '@babel/types'
3
- import { createHmrHotExpressionAst } from '../../hmr-hot-expression'
4
3
  import { getUniqueProgramIdentifier } from '../../utils'
5
4
  import type { ReferenceRouteCompilerPlugin } from '../plugins'
6
5
 
7
6
  const buildReactRefreshIgnoredRouteExportsStatements = template.statements(
8
7
  `
9
- const hot = %%hotExpression%%
8
+ const hot = import.meta.hot
10
9
  if (hot && typeof window !== 'undefined') {
11
10
  ;(hot.data ??= {})
12
11
  const tsrReactRefresh = window.__TSR_REACT_REFRESH__ ??= (() => {
@@ -41,9 +40,7 @@ const buildRefreshAnchorStatement = template.statement(
41
40
  { syntacticPlaceholders: true },
42
41
  )
43
42
 
44
- export function createReactRefreshIgnoredRouteExportsPlugin(opts?: {
45
- hotExpression?: string
46
- }): ReferenceRouteCompilerPlugin {
43
+ export function createReactRefreshIgnoredRouteExportsPlugin(): ReferenceRouteCompilerPlugin {
47
44
  return {
48
45
  name: 'react-refresh-ignored-route-exports',
49
46
  onAddHmr(ctx) {
@@ -55,9 +52,6 @@ export function createReactRefreshIgnoredRouteExportsPlugin(opts?: {
55
52
  ctx.programPath.pushContainer(
56
53
  'body',
57
54
  buildReactRefreshIgnoredRouteExportsStatements({
58
- hotExpression: createHmrHotExpressionAst(
59
- opts?.hotExpression ?? ctx.opts.hmrHotExpression,
60
- ),
61
55
  moduleId: t.stringLiteral(ctx.opts.id),
62
56
  }),
63
57
  )
@@ -1,7 +1,7 @@
1
1
  import * as template from '@babel/template'
2
2
  import * as t from '@babel/types'
3
- import { createHmrHotExpressionAst } from '../../hmr-hot-expression'
4
3
  import { getUniqueProgramIdentifier } from '../../utils'
4
+ import type { HmrStyle } from '../../config'
5
5
  import type { ReferenceRouteCompilerPlugin } from '../plugins'
6
6
 
7
7
  function capitalizeIdentifier(str: string) {
@@ -28,8 +28,14 @@ const buildStableSplitComponentStatements = template.statements(
28
28
  },
29
29
  )
30
30
 
31
- export function createReactStableHmrSplitRouteComponentsPlugin(opts?: {
32
- hotExpression?: string
31
+ function hotExpressionAstFor(hmrStyle: HmrStyle): t.Expression {
32
+ return template.expression.ast(
33
+ hmrStyle === 'webpack' ? 'import.meta.webpackHot' : 'import.meta.hot',
34
+ )
35
+ }
36
+
37
+ export function createReactStableHmrSplitRouteComponentsPlugin(opts: {
38
+ hmrStyle: HmrStyle
33
39
  }): ReferenceRouteCompilerPlugin {
34
40
  return {
35
41
  name: 'react-stable-hmr-split-route-components',
@@ -49,9 +55,7 @@ export function createReactStableHmrSplitRouteComponentsPlugin(opts?: {
49
55
  buildStableSplitComponentStatements({
50
56
  stableComponentIdent,
51
57
  hotDataKey: t.stringLiteral(hotDataKey),
52
- hotExpression: createHmrHotExpressionAst(
53
- opts?.hotExpression ?? ctx.opts.hmrHotExpression,
54
- ),
58
+ hotExpression: hotExpressionAstFor(opts.hmrStyle),
55
59
  lazyRouteComponentIdent: t.identifier(ctx.lazyRouteComponentIdent),
56
60
  localImporterIdent: t.identifier(
57
61
  ctx.splitNodeMeta.localImporterIdent,
@@ -1,6 +1,6 @@
1
1
  import type babel from '@babel/core'
2
2
  import type * as t from '@babel/types'
3
- import type { Config, DeletableNodes } from '../config'
3
+ import type { Config, DeletableNodes, HmrStyle } from '../config'
4
4
  import type { CodeSplitGroupings } from '../constants'
5
5
  import type { SplitNodeMeta } from './types'
6
6
 
@@ -11,7 +11,8 @@ export type CompileCodeSplitReferenceRouteOptions = {
11
11
  filename: string
12
12
  id: string
13
13
  addHmr?: boolean
14
- hmrHotExpression?: string
14
+ hmrStyle?: HmrStyle
15
+ hmrRouteId?: string
15
16
  sharedBindings?: Set<string>
16
17
  }
17
18
 
@@ -72,8 +72,17 @@ export type CodeSplittingOptions = {
72
72
  addHmr?: boolean
73
73
  }
74
74
 
75
+ export type HmrStyle = 'vite' | 'webpack'
76
+
75
77
  export type HmrOptions = {
76
- hotExpression?: string
78
+ /**
79
+ * Selects the HMR runtime style to emit code for.
80
+ * - `'vite'` (default): ESM `import.meta.hot` with Vite accept-callback semantics.
81
+ * - `'webpack'`: `import.meta.webpackHot` with webpack / Rspack `module.hot` re-execution semantics.
82
+ *
83
+ * Bundler-specific plugin entries (e.g. `rspack.ts`, `webpack.ts`) set this explicitly.
84
+ */
85
+ style?: HmrStyle
77
86
  }
78
87
 
79
88
  const codeSplittingOptionsSchema = z.object({
@@ -99,7 +108,7 @@ export const configSchema = generatorConfigSchema.extend({
99
108
  .object({
100
109
  hmr: z
101
110
  .object({
102
- hotExpression: z.string().optional(),
111
+ style: z.enum(['vite', 'webpack']).optional(),
103
112
  })
104
113
  .optional(),
105
114
  vite: z
@@ -1,6 +1,3 @@
1
- import * as template from '@babel/template'
2
- import { createHmrHotExpressionAst } from './hmr-hot-expression'
3
- import type * as t from '@babel/types'
4
1
  import type {
5
2
  AnyRoute,
6
3
  AnyRouteMatch,
@@ -25,18 +22,23 @@ type AnyRouterWithPrivateMaps = AnyRouter & {
25
22
  stores: AnyRouter['stores'] & {
26
23
  cachedMatchStores: Map<
27
24
  string,
28
- Pick<RouterWritableStore<AnyRouteMatch>, 'set'>
25
+ Pick<RouterWritableStore<AnyRouteMatch>, 'get' | 'set'>
29
26
  >
30
27
  pendingMatchStores: Map<
31
28
  string,
32
- Pick<RouterWritableStore<AnyRouteMatch>, 'set'>
29
+ Pick<RouterWritableStore<AnyRouteMatch>, 'get' | 'set'>
30
+ >
31
+ matchStores: Map<
32
+ string,
33
+ Pick<RouterWritableStore<AnyRouteMatch>, 'get' | 'set'>
33
34
  >
34
- matchStores: Map<string, Pick<RouterWritableStore<AnyRouteMatch>, 'set'>>
35
35
  }
36
36
  }
37
37
 
38
38
  type AnyRouteMatchWithPrivateProps = AnyRouteMatch & {
39
39
  __beforeLoadContext?: unknown
40
+ __routeContext?: Record<string, unknown>
41
+ context?: Record<string, unknown>
40
42
  }
41
43
 
42
44
  function handleRouteUpdate(
@@ -69,14 +71,21 @@ function handleRouteUpdate(
69
71
  }
70
72
  })
71
73
 
74
+ const oldHasShellComponent = 'shellComponent' in oldRoute.options
75
+ const newHasShellComponent = 'shellComponent' in newRoute.options
76
+ const preserveComponentIdentity =
77
+ oldHasShellComponent === newHasShellComponent
78
+
72
79
  // Preserve component identity so React doesn't remount.
73
80
  // React Fast Refresh patches the function bodies in-place.
74
81
  const componentKeys = '__TSR_COMPONENT_TYPES__' as unknown as Array<string>
75
- componentKeys.forEach((key) => {
76
- if (key in oldRoute.options && key in newRoute.options) {
77
- newRoute.options[key] = oldRoute.options[key]
78
- }
79
- })
82
+ if (preserveComponentIdentity) {
83
+ componentKeys.forEach((key) => {
84
+ if (key in oldRoute.options && key in newRoute.options) {
85
+ newRoute.options[key] = oldRoute.options[key]
86
+ }
87
+ })
88
+ }
80
89
 
81
90
  oldRoute.options = newRoute.options
82
91
  oldRoute.update(newRoute.options)
@@ -127,6 +136,7 @@ function handleRouteUpdate(
127
136
  }
128
137
  if (removedKeys.has('beforeLoad')) {
129
138
  next.__beforeLoadContext = undefined
139
+ next.context = rebuildMatchContextWithoutBeforeLoad(next)
130
140
  }
131
141
 
132
142
  return next
@@ -153,37 +163,73 @@ function handleRouteUpdate(
153
163
  node.optional?.forEach((child) => walkReplaceSegmentTree(route, child))
154
164
  node.wildcard?.forEach((child) => walkReplaceSegmentTree(route, child))
155
165
  }
156
- }
157
166
 
158
- const handleRouteUpdateStr = handleRouteUpdate.toString()
167
+ function getStoreMatch(matchId: string) {
168
+ return (
169
+ router.stores.pendingMatchStores.get(matchId)?.get() ||
170
+ router.stores.matchStores.get(matchId)?.get() ||
171
+ router.stores.cachedMatchStores.get(matchId)?.get()
172
+ )
173
+ }
174
+
175
+ function getMatchList(matchId: string) {
176
+ const pendingMatches = router.stores.pendingMatches.get()
177
+ if (pendingMatches.some((match) => match.id === matchId)) {
178
+ return pendingMatches
179
+ }
180
+
181
+ const activeMatches = router.stores.matches.get()
182
+ if (activeMatches.some((match) => match.id === matchId)) {
183
+ return activeMatches
184
+ }
159
185
 
160
- export function createRouteHmrStatement(
161
- stableRouteOptionKeys: Array<string>,
162
- opts?: { hotExpression?: string },
163
- ): t.Statement {
164
- return template.statement(
165
- `
166
- if (%%hotExpression%%) {
167
- const hot = %%hotExpression%%
168
- const hotData = hot.data ??= {}
169
- hot.accept((newModule) => {
170
- if (Route && newModule && newModule.Route) {
171
- const routeId = hotData['tsr-route-id'] ?? Route.id
172
- if (routeId) {
173
- hotData['tsr-route-id'] = routeId
186
+ const cachedMatches = router.stores.cachedMatches.get()
187
+ if (cachedMatches.some((match) => match.id === matchId)) {
188
+ return cachedMatches
189
+ }
190
+
191
+ return []
192
+ }
193
+
194
+ function getParentMatch(match: AnyRouteMatch) {
195
+ const matchList = getMatchList(match.id)
196
+ const matchIndex = matchList.findIndex((item) => item.id === match.id)
197
+
198
+ if (matchIndex <= 0) {
199
+ return undefined
200
+ }
201
+
202
+ const parentMatch = matchList[matchIndex - 1]!
203
+ return getStoreMatch(parentMatch.id) || parentMatch
204
+ }
205
+
206
+ function rebuildMatchContextWithoutBeforeLoad(
207
+ match: AnyRouteMatchWithPrivateProps,
208
+ ) {
209
+ const parentMatch = getParentMatch(match)
210
+ const getParentContext = (
211
+ router as unknown as {
212
+ getParentContext?: (
213
+ parentMatch?: AnyRouteMatch,
214
+ ) => Record<string, unknown> | undefined
174
215
  }
175
- (${handleRouteUpdateStr.replace(
176
- /['"]__TSR_COMPONENT_TYPES__['"]/,
177
- JSON.stringify(stableRouteOptionKeys),
178
- )})(routeId, newModule.Route)
216
+ ).getParentContext
217
+ const parentContext = getParentContext
218
+ ? getParentContext.call(router, parentMatch)
219
+ : (parentMatch?.context ?? router.options.context)
220
+
221
+ return {
222
+ ...(parentContext ?? {}),
223
+ ...(match.__routeContext ?? {}),
179
224
  }
180
- })
225
+ }
181
226
  }
182
- `,
183
- {
184
- syntacticPlaceholders: true,
185
- },
186
- )({
187
- hotExpression: createHmrHotExpressionAst(opts?.hotExpression),
188
- })
227
+
228
+ const handleRouteUpdateStr = handleRouteUpdate.toString()
229
+
230
+ export function getHandleRouteUpdateCode(stableRouteOptionKeys: Array<string>) {
231
+ return handleRouteUpdateStr.replace(
232
+ /['"]__TSR_COMPONENT_TYPES__['"]/,
233
+ JSON.stringify(stableRouteOptionKeys),
234
+ )
189
235
  }
@@ -0,0 +1,5 @@
1
+ export { createRouteHmrStatement } from './select-adapter'
2
+ export type { CreateRouteHmrStatementOpts } from './select-adapter'
3
+ export { createViteHmrStatement } from './vite-adapter'
4
+ export { createWebpackHmrStatement } from './webpack-adapter'
5
+ export { getHandleRouteUpdateCode } from './handle-route-update'
@@ -0,0 +1,32 @@
1
+ import { createViteHmrStatement } from './vite-adapter'
2
+ import { createWebpackHmrStatement } from './webpack-adapter'
3
+ import type { Config, HmrStyle } from '../config'
4
+ import type * as t from '@babel/types'
5
+
6
+ export type CreateRouteHmrStatementOpts = {
7
+ hmrStyle: HmrStyle
8
+ targetFramework: Config['target']
9
+ routeId?: string
10
+ }
11
+
12
+ /**
13
+ * Dispatches to the configured HMR adapter. `hmrStyle` is set explicitly by
14
+ * the bundler-specific plugin entry (e.g. `rspack.ts` → `'webpack'`), so there
15
+ * is no runtime inference based on config string shapes.
16
+ */
17
+ export function createRouteHmrStatement(
18
+ stableRouteOptionKeys: Array<string>,
19
+ opts: CreateRouteHmrStatementOpts,
20
+ ): Array<t.Statement> {
21
+ const routeId = opts.routeId === '/__root' ? '__root__' : opts.routeId
22
+
23
+ if (opts.hmrStyle === 'webpack') {
24
+ return createWebpackHmrStatement(stableRouteOptionKeys, {
25
+ targetFramework: opts.targetFramework,
26
+ routeId,
27
+ })
28
+ }
29
+ return createViteHmrStatement(stableRouteOptionKeys, {
30
+ routeId,
31
+ })
32
+ }
@@ -0,0 +1,47 @@
1
+ import * as template from '@babel/template'
2
+ import { getHandleRouteUpdateCode } from './handle-route-update'
3
+ import type * as t from '@babel/types'
4
+
5
+ /**
6
+ * Emits HMR accept code for Vite / native ESM HMR: `import.meta.hot.accept`
7
+ * with a callback that receives the freshly re-imported module.
8
+ *
9
+ * `targetFramework` is currently unused — Vite's framework-specific fast-refresh
10
+ * plugins handle component body patching via their own accept boundaries — but
11
+ * we take it for API symmetry with `createWebpackHmrStatement`.
12
+ */
13
+ export function createViteHmrStatement(
14
+ stableRouteOptionKeys: Array<string>,
15
+ opts: {
16
+ routeId?: string
17
+ } = {},
18
+ ): Array<t.Statement> {
19
+ const handleRouteUpdateCode = getHandleRouteUpdateCode(stableRouteOptionKeys)
20
+ // The replacement Route object can be uninitialized; keep a generated id as
21
+ // fallback for the existing router route we need to patch.
22
+ const routeIdFallback =
23
+ typeof opts.routeId === 'string' ? JSON.stringify(opts.routeId) : 'Route.id'
24
+
25
+ return [
26
+ template.statement(
27
+ `
28
+ if (import.meta.hot) {
29
+ const hot = import.meta.hot
30
+ const hotData = hot.data ??= {}
31
+ hot.accept((newModule) => {
32
+ if (Route && newModule && newModule.Route) {
33
+ const routeId = hotData['tsr-route-id'] ?? ${routeIdFallback}
34
+ if (routeId) {
35
+ hotData['tsr-route-id'] = routeId
36
+ }
37
+ (${handleRouteUpdateCode})(routeId, newModule.Route)
38
+ }
39
+ })
40
+ }
41
+ `,
42
+ {
43
+ syntacticPlaceholders: true,
44
+ },
45
+ )(),
46
+ ]
47
+ }
@@ -0,0 +1,110 @@
1
+ import * as template from '@babel/template'
2
+ import { getHandleRouteUpdateCode } from './handle-route-update'
3
+ import type { Config } from '../config'
4
+ import type * as t from '@babel/types'
5
+
6
+ /**
7
+ * Emits HMR accept code for bundlers with webpack-compatible `module.hot`
8
+ * semantics (classic webpack via `import.meta.webpackHot`, and Rspack).
9
+ *
10
+ * Unlike Vite's `hot.accept((newModule) => {...})` — where the callback receives
11
+ * the freshly re-imported module — webpack re-executes the module factory on
12
+ * accept, so our HMR logic must live at module top level and read the previous
13
+ * `routeId` out of `hot.data`. `hot.dispose` stashes it for the next run, and
14
+ * `hot.accept()` (no callback) enrolls us as a self-accepting boundary.
15
+ *
16
+ * Returns an array of statements so that for React we can prepend an
17
+ * `import { performReactRefresh } from 'react-refresh/runtime'` hoisted to the
18
+ * top of the module.
19
+ */
20
+ export function createWebpackHmrStatement(
21
+ stableRouteOptionKeys: Array<string>,
22
+ opts: {
23
+ targetFramework: Config['target']
24
+ routeId?: string
25
+ },
26
+ ): Array<t.Statement> {
27
+ const handleRouteUpdateCode = getHandleRouteUpdateCode(stableRouteOptionKeys)
28
+ const staticRouteIdLiteral =
29
+ typeof opts.routeId === 'string'
30
+ ? JSON.stringify(opts.routeId)
31
+ : 'undefined'
32
+
33
+ const statements: Array<t.Statement> = []
34
+
35
+ // React-only: route modules aren't React Refresh "boundaries" (they export
36
+ // a non-component `Route`), so the bundler's react-refresh runtime won't
37
+ // call `performReactRefresh` for us. We kick it manually after swapping
38
+ // route options so newly-registered component bodies get patched into live
39
+ // fibers.
40
+ //
41
+ // We import `performReactRefresh` directly from `react-refresh/runtime` —
42
+ // the canonical public API — rather than relying on the ProvidePlugin-
43
+ // injected `__react_refresh_utils__` global, whose name is an internal
44
+ // detail of `@rspack/plugin-react-refresh`. The rspack plugin aliases
45
+ // `react-refresh` → its bundled runtime (getRefreshRuntimeDirPath), so this
46
+ // resolves to the same singleton the plugin itself uses and shares the
47
+ // registry React was patched against.
48
+ //
49
+ // Use the same delayed refresh style as Rspack's React Refresh runtime.
50
+ // Route modules and their split component chunks can arrive in separate HMR
51
+ // steps under CI load; a microtask can run before the split chunk registers
52
+ // its new component family, causing the refresh to no-op or remount.
53
+ //
54
+ // For non-React frameworks we skip this entirely.
55
+ const reactRefreshCall =
56
+ opts.targetFramework === 'react'
57
+ ? `
58
+ const tsrRefreshState = globalThis.__TSR_HMR__ ??= {}
59
+ try {
60
+ if (!tsrRefreshState.refreshScheduled) {
61
+ tsrRefreshState.refreshScheduled = true
62
+ setTimeout(() => {
63
+ tsrRefreshState.refreshScheduled = false
64
+ try { __tsr_performReactRefresh() } catch (_e) { /* noop */ }
65
+ }, 30)
66
+ }
67
+ } catch (_err) { /* noop */ }`
68
+ : ''
69
+
70
+ if (opts.targetFramework === 'react') {
71
+ statements.push(
72
+ template.statement(
73
+ `import { performReactRefresh as __tsr_performReactRefresh } from 'react-refresh/runtime'`,
74
+ )(),
75
+ )
76
+ }
77
+
78
+ statements.push(
79
+ template.statement(
80
+ `
81
+ if (import.meta.webpackHot) {
82
+ const hot = import.meta.webpackHot
83
+ const hotData = hot.data ??= {}
84
+ const routeId = hotData['tsr-route-id'] ?? Route.id ?? (Route.isRoot ? '__root__' : ${staticRouteIdLiteral})
85
+ if (routeId) {
86
+ hotData['tsr-route-id'] = routeId
87
+ }
88
+ const existingRoute =
89
+ typeof window !== 'undefined' && routeId
90
+ ? window.__TSR_ROUTER__?.routesById?.[routeId]
91
+ : undefined
92
+ if (routeId && existingRoute && existingRoute !== Route) {
93
+ (${handleRouteUpdateCode})(routeId, Route)${reactRefreshCall}
94
+ }
95
+ hot.dispose((data) => {
96
+ if (routeId) {
97
+ data['tsr-route-id'] = routeId
98
+ }
99
+ })
100
+ hot.accept()
101
+ }
102
+ `,
103
+ {
104
+ syntacticPlaceholders: true,
105
+ },
106
+ )(),
107
+ )
108
+
109
+ return statements
110
+ }
@@ -6,7 +6,6 @@
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url'
7
7
  import { logDiff } from '@tanstack/router-utils'
8
8
  import { getConfig, splitGroupingsSchema } from './config'
9
- import { resolveHmrHotExpression } from './hmr-hot-expression'
10
9
  import {
11
10
  compileCodeSplitReferenceRoute,
12
11
  compileCodeSplitSharedRoute,
@@ -129,7 +128,7 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory<
129
128
  const userShouldSplitFn = getShouldSplitFn()
130
129
 
131
130
  const pluginSplitBehavior = userShouldSplitFn?.({
132
- routeId: generatorNodeInfo.routePath,
131
+ routeId: generatorNodeInfo.routeId,
133
132
  }) as CodeSplitGroupings | undefined
134
133
 
135
134
  if (pluginSplitBehavior) {
@@ -158,9 +157,7 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory<
158
157
 
159
158
  const addHmr =
160
159
  (userConfig.codeSplittingOptions?.addHmr ?? true) && !isProduction
161
- const hmrHotExpression = resolveHmrHotExpression(
162
- userConfig.plugin?.hmr?.hotExpression,
163
- )
160
+ const hmrStyle = userConfig.plugin?.hmr?.style ?? 'vite'
164
161
 
165
162
  const compiledReferenceRoute = compileCodeSplitReferenceRoute({
166
163
  code,
@@ -172,12 +169,13 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory<
172
169
  ? new Set(userConfig.codeSplittingOptions.deleteNodes)
173
170
  : undefined,
174
171
  addHmr,
175
- hmrHotExpression,
172
+ hmrStyle,
173
+ hmrRouteId: generatorNodeInfo.routeId,
176
174
  sharedBindings: sharedBindings.size > 0 ? sharedBindings : undefined,
177
175
  compilerPlugins: getReferenceRouteCompilerPlugins({
178
176
  targetFramework: userConfig.target,
179
177
  addHmr,
180
- hmrHotExpression,
178
+ hmrStyle,
181
179
  }),
182
180
  })
183
181
 
@@ -6,12 +6,17 @@ import type { Config } from './config'
6
6
  import type { UnpluginFactory } from 'unplugin'
7
7
 
8
8
  export const unpluginRouterComposedFactory: UnpluginFactory<
9
- Partial<Config> | undefined
9
+ Partial<Config | (() => Config)> | undefined
10
10
  > = (options = {}, meta) => {
11
11
  const ROOT: string = process.cwd()
12
- const userConfig = getConfig(options, ROOT)
12
+ const userConfig = getConfig(
13
+ (typeof options === 'function' ? options() : options) as Partial<Config>,
14
+ ROOT,
15
+ )
13
16
 
14
- const getPlugin = (pluginFactory: UnpluginFactory<Partial<Config>>) => {
17
+ const getPlugin = (
18
+ pluginFactory: UnpluginFactory<Partial<Config | (() => Config)>>,
19
+ ) => {
15
20
  const plugin = pluginFactory(options, meta)
16
21
  if (!Array.isArray(plugin)) {
17
22
  return [plugin]
@@ -1,10 +1,9 @@
1
1
  import { generateFromAst, logDiff, parseAst } from '@tanstack/router-utils'
2
2
  import { compileCodeSplitReferenceRoute } from './code-splitter/compilers'
3
3
  import { getReferenceRouteCompilerPlugins } from './code-splitter/plugins/framework-plugins'
4
- import { createRouteHmrStatement } from './route-hmr-statement'
4
+ import { createRouteHmrStatement } from './hmr'
5
5
  import { debug, normalizePath } from './utils'
6
6
  import { getConfig } from './config'
7
- import { resolveHmrHotExpression } from './hmr-hot-expression'
8
7
  import type { UnpluginFactory } from 'unplugin'
9
8
  import type { Config } from './config'
10
9
 
@@ -38,28 +37,28 @@ export const unpluginRouterHmrFactory: UnpluginFactory<
38
37
  },
39
38
  handler(code, id) {
40
39
  const normalizedId = normalizePath(id)
41
- if (!globalThis.TSR_ROUTES_BY_ID_MAP?.has(normalizedId)) {
40
+ const routeEntry = globalThis.TSR_ROUTES_BY_ID_MAP?.get(normalizedId)
41
+ if (!routeEntry) {
42
42
  return null
43
43
  }
44
44
 
45
45
  if (debug) console.info('Adding HMR handling to route ', normalizedId)
46
46
 
47
- const hmrHotExpression = resolveHmrHotExpression(
48
- userConfig.plugin?.hmr?.hotExpression,
49
- )
47
+ const hmrStyle = userConfig.plugin?.hmr?.style ?? 'vite'
50
48
 
51
49
  if (userConfig.target === 'react') {
52
50
  const compilerPlugins = getReferenceRouteCompilerPlugins({
53
51
  targetFramework: 'react',
54
52
  addHmr: true,
55
- hmrHotExpression,
53
+ hmrStyle,
56
54
  })
57
55
  const compiled = compileCodeSplitReferenceRoute({
58
56
  code,
59
57
  filename: normalizedId,
60
58
  id: normalizedId,
61
59
  addHmr: true,
62
- hmrHotExpression,
60
+ hmrStyle,
61
+ hmrRouteId: routeEntry.routeId,
63
62
  codeSplitGroupings: [],
64
63
  targetFramework: 'react',
65
64
  compilerPlugins,
@@ -77,7 +76,11 @@ export const unpluginRouterHmrFactory: UnpluginFactory<
77
76
 
78
77
  const ast = parseAst({ code })
79
78
  ast.program.body.push(
80
- createRouteHmrStatement([], { hotExpression: hmrHotExpression }),
79
+ ...createRouteHmrStatement([], {
80
+ hmrStyle,
81
+ targetFramework: userConfig.target,
82
+ routeId: routeEntry.routeId,
83
+ }),
81
84
  )
82
85
  const result = generateFromAst(ast, {
83
86
  sourceMaps: true,