@tanstack/router-plugin 1.167.1 → 1.167.3

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 (60) hide show
  1. package/dist/cjs/core/code-splitter/compilers.cjs +63 -34
  2. package/dist/cjs/core/code-splitter/compilers.cjs.map +1 -1
  3. package/dist/cjs/core/code-splitter/plugins/framework-plugins.cjs +7 -1
  4. package/dist/cjs/core/code-splitter/plugins/framework-plugins.cjs.map +1 -1
  5. package/dist/cjs/core/code-splitter/plugins/react-refresh-ignored-route-exports.cjs +49 -0
  6. package/dist/cjs/core/code-splitter/plugins/react-refresh-ignored-route-exports.cjs.map +1 -0
  7. package/dist/cjs/core/code-splitter/plugins/react-refresh-ignored-route-exports.d.cts +2 -0
  8. package/dist/cjs/core/code-splitter/plugins/react-refresh-route-components.cjs +24 -12
  9. package/dist/cjs/core/code-splitter/plugins/react-refresh-route-components.cjs.map +1 -1
  10. package/dist/cjs/core/code-splitter/plugins/react-stable-hmr-split-route-components.cjs +41 -0
  11. package/dist/cjs/core/code-splitter/plugins/react-stable-hmr-split-route-components.cjs.map +1 -0
  12. package/dist/cjs/core/code-splitter/plugins/react-stable-hmr-split-route-components.d.cts +2 -0
  13. package/dist/cjs/core/code-splitter/plugins.d.cts +13 -0
  14. package/dist/cjs/core/code-splitter/types.d.cts +9 -0
  15. package/dist/cjs/core/route-hmr-statement.cjs +58 -15
  16. package/dist/cjs/core/route-hmr-statement.cjs.map +1 -1
  17. package/dist/cjs/core/route-hmr-statement.d.cts +1 -1
  18. package/dist/cjs/core/router-code-splitter-plugin.cjs +3 -3
  19. package/dist/cjs/core/router-code-splitter-plugin.cjs.map +1 -1
  20. package/dist/cjs/core/router-hmr-plugin.cjs +2 -2
  21. package/dist/cjs/core/router-hmr-plugin.cjs.map +1 -1
  22. package/dist/cjs/core/utils.cjs +9 -1
  23. package/dist/cjs/core/utils.cjs.map +1 -1
  24. package/dist/cjs/core/utils.d.cts +1 -0
  25. package/dist/esm/core/code-splitter/compilers.js +64 -35
  26. package/dist/esm/core/code-splitter/compilers.js.map +1 -1
  27. package/dist/esm/core/code-splitter/plugins/framework-plugins.js +7 -1
  28. package/dist/esm/core/code-splitter/plugins/framework-plugins.js.map +1 -1
  29. package/dist/esm/core/code-splitter/plugins/react-refresh-ignored-route-exports.d.ts +2 -0
  30. package/dist/esm/core/code-splitter/plugins/react-refresh-ignored-route-exports.js +46 -0
  31. package/dist/esm/core/code-splitter/plugins/react-refresh-ignored-route-exports.js.map +1 -0
  32. package/dist/esm/core/code-splitter/plugins/react-refresh-route-components.js +25 -13
  33. package/dist/esm/core/code-splitter/plugins/react-refresh-route-components.js.map +1 -1
  34. package/dist/esm/core/code-splitter/plugins/react-stable-hmr-split-route-components.d.ts +2 -0
  35. package/dist/esm/core/code-splitter/plugins/react-stable-hmr-split-route-components.js +38 -0
  36. package/dist/esm/core/code-splitter/plugins/react-stable-hmr-split-route-components.js.map +1 -0
  37. package/dist/esm/core/code-splitter/plugins.d.ts +13 -0
  38. package/dist/esm/core/code-splitter/types.d.ts +9 -0
  39. package/dist/esm/core/route-hmr-statement.d.ts +1 -1
  40. package/dist/esm/core/route-hmr-statement.js +58 -15
  41. package/dist/esm/core/route-hmr-statement.js.map +1 -1
  42. package/dist/esm/core/router-code-splitter-plugin.js +3 -3
  43. package/dist/esm/core/router-code-splitter-plugin.js.map +1 -1
  44. package/dist/esm/core/router-hmr-plugin.js +3 -3
  45. package/dist/esm/core/router-hmr-plugin.js.map +1 -1
  46. package/dist/esm/core/utils.d.ts +1 -0
  47. package/dist/esm/core/utils.js +9 -2
  48. package/dist/esm/core/utils.js.map +1 -1
  49. package/package.json +4 -4
  50. package/src/core/code-splitter/compilers.ts +118 -62
  51. package/src/core/code-splitter/plugins/framework-plugins.ts +7 -1
  52. package/src/core/code-splitter/plugins/react-refresh-ignored-route-exports.ts +65 -0
  53. package/src/core/code-splitter/plugins/react-refresh-route-components.ts +68 -39
  54. package/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts +56 -0
  55. package/src/core/code-splitter/plugins.ts +18 -0
  56. package/src/core/code-splitter/types.ts +11 -0
  57. package/src/core/route-hmr-statement.ts +141 -25
  58. package/src/core/router-code-splitter-plugin.ts +2 -2
  59. package/src/core/router-hmr-plugin.ts +7 -6
  60. package/src/core/utils.ts +27 -2
@@ -2,39 +2,145 @@ import * as template from '@babel/template'
2
2
  import type { AnyRoute, AnyRouteMatch, AnyRouter } from '@tanstack/router-core'
3
3
 
4
4
  type AnyRouteWithPrivateProps = AnyRoute & {
5
+ options: Record<string, unknown>
6
+ _componentsPromise?: Promise<void>
7
+ _lazyPromise?: Promise<void>
8
+ update: (options: Record<string, unknown>) => unknown
5
9
  _path: string
6
10
  _id: string
7
11
  _fullPath: string
8
12
  _to: string
9
13
  }
10
14
 
15
+ type AnyRouterWithPrivateMaps = AnyRouter & {
16
+ routesById: Record<string, AnyRoute>
17
+ routesByPath: Record<string, AnyRoute>
18
+ stores: AnyRouter['stores'] & {
19
+ cachedMatchStoresById: Map<
20
+ string,
21
+ {
22
+ setState: (updater: (prev: AnyRouteMatch) => AnyRouteMatch) => void
23
+ }
24
+ >
25
+ pendingMatchStoresById: Map<
26
+ string,
27
+ {
28
+ setState: (updater: (prev: AnyRouteMatch) => AnyRouteMatch) => void
29
+ }
30
+ >
31
+ activeMatchStoresById: Map<
32
+ string,
33
+ {
34
+ setState: (updater: (prev: AnyRouteMatch) => AnyRouteMatch) => void
35
+ }
36
+ >
37
+ }
38
+ }
39
+
40
+ type AnyRouteMatchWithPrivateProps = AnyRouteMatch & {
41
+ __beforeLoadContext?: unknown
42
+ }
43
+
11
44
  function handleRouteUpdate(
12
- oldRoute: AnyRouteWithPrivateProps,
45
+ routeId: string,
13
46
  newRoute: AnyRouteWithPrivateProps,
14
47
  ) {
15
- newRoute._path = oldRoute._path
16
- newRoute._id = oldRoute._id
17
- newRoute._fullPath = oldRoute._fullPath
18
- newRoute._to = oldRoute._to
19
- newRoute.children = oldRoute.children
20
- newRoute.parentRoute = oldRoute.parentRoute
21
-
22
- const router = window.__TSR_ROUTER__!
23
- router.routesById[newRoute.id] = newRoute
24
- router.routesByPath[newRoute.fullPath] = newRoute
48
+ const router = window.__TSR_ROUTER__ as AnyRouterWithPrivateMaps
49
+ const oldRoute = router.routesById[routeId] as
50
+ | AnyRouteWithPrivateProps
51
+ | undefined
52
+
53
+ if (!oldRoute) {
54
+ return
55
+ }
56
+
57
+ // Keys whose identity must remain stable to prevent React from
58
+ // unmounting/remounting the component tree. React Fast Refresh already
59
+ // handles hot-updating the function bodies of these components — our job
60
+ // is only to update non-component route options (loader, head, etc.).
61
+ // For code-split (splittable) routes, the lazyRouteComponent wrapper is
62
+ // already cached in import.meta.hot.data so its identity is stable.
63
+ // For unsplittable routes (e.g. root routes), the component is a plain
64
+ // function reference that gets recreated on every module re-execution,
65
+ // so we must explicitly preserve the old reference.
66
+ const removedKeys = new Set<string>()
67
+ Object.keys(oldRoute.options).forEach((key) => {
68
+ if (!(key in newRoute.options)) {
69
+ removedKeys.add(key)
70
+ delete oldRoute.options[key]
71
+ }
72
+ })
73
+
74
+ // Preserve component identity so React doesn't remount.
75
+ // React Fast Refresh patches the function bodies in-place.
76
+ const componentKeys = '__TSR_COMPONENT_TYPES__' as unknown as Array<string>
77
+ componentKeys.forEach((key) => {
78
+ if (key in oldRoute.options && key in newRoute.options) {
79
+ newRoute.options[key] = oldRoute.options[key]
80
+ }
81
+ })
82
+
83
+ oldRoute.options = newRoute.options
84
+ oldRoute.update(newRoute.options)
85
+ oldRoute._componentsPromise = undefined
86
+ oldRoute._lazyPromise = undefined
87
+
88
+ router.routesById[oldRoute.id] = oldRoute
89
+ router.routesByPath[oldRoute.fullPath] = oldRoute
90
+
25
91
  router.processedTree.matchCache.clear()
26
92
  router.processedTree.flatCache?.clear()
27
93
  router.processedTree.singleCache.clear()
28
94
  router.resolvePathCache.clear()
29
- // TODO: how to rebuild the tree if we add a new route?
30
- walkReplaceSegmentTree(newRoute, router.processedTree.segmentTree)
95
+ walkReplaceSegmentTree(oldRoute, router.processedTree.segmentTree)
96
+
31
97
  const filter = (m: AnyRouteMatch) => m.routeId === oldRoute.id
32
- if (
33
- router.stores.activeMatchesSnapshot.state.find(filter) ||
34
- router.stores.pendingMatchesSnapshot.state.find(filter)
35
- ) {
36
- router.invalidate({ filter })
98
+ const activeMatch = router.stores.activeMatchesSnapshot.state.find(filter)
99
+ const pendingMatch = router.stores.pendingMatchesSnapshot.state.find(filter)
100
+ const cachedMatches = router.stores.cachedMatchesSnapshot.state.filter(filter)
101
+
102
+ if (activeMatch || pendingMatch || cachedMatches.length > 0) {
103
+ // Clear stale match data for removed route options BEFORE invalidating.
104
+ // Without this, router.invalidate() -> matchRoutes() reuses the existing
105
+ // match from the store (via ...existingMatch spread) and the stale
106
+ // loaderData / __beforeLoadContext survives the reload cycle.
107
+ //
108
+ // We must update the store directly (not via router.updateMatch) because
109
+ // updateMatch wraps in startTransition which may defer the state update,
110
+ // and we need the clear to be visible before invalidate reads the store.
111
+ if (removedKeys.has('loader') || removedKeys.has('beforeLoad')) {
112
+ const matchIds = [
113
+ activeMatch?.id,
114
+ pendingMatch?.id,
115
+ ...cachedMatches.map((match) => match.id),
116
+ ].filter(Boolean) as Array<string>
117
+ router.batch(() => {
118
+ for (const matchId of matchIds) {
119
+ const store =
120
+ router.stores.pendingMatchStoresById.get(matchId) ||
121
+ router.stores.activeMatchStoresById.get(matchId) ||
122
+ router.stores.cachedMatchStoresById.get(matchId)
123
+ if (store) {
124
+ store.setState((prev) => {
125
+ const next: AnyRouteMatchWithPrivateProps = { ...prev }
126
+
127
+ if (removedKeys.has('loader')) {
128
+ next.loaderData = undefined
129
+ }
130
+ if (removedKeys.has('beforeLoad')) {
131
+ next.__beforeLoadContext = undefined
132
+ }
133
+
134
+ return next
135
+ })
136
+ }
137
+ }
138
+ })
139
+ }
140
+
141
+ router.invalidate({ filter, sync: true })
37
142
  }
143
+
38
144
  function walkReplaceSegmentTree(
39
145
  route: AnyRouteWithPrivateProps,
40
146
  node: AnyRouter['processedTree']['segmentTree'],
@@ -51,16 +157,26 @@ function handleRouteUpdate(
51
157
  }
52
158
  }
53
159
 
54
- export const routeHmrStatement = template.statement(
55
- `
160
+ const handleRouteUpdateStr = handleRouteUpdate.toString()
161
+
162
+ export function createRouteHmrStatement(stableRouteOptionKeys: Array<string>) {
163
+ return template.statement(
164
+ `
56
165
  if (import.meta.hot) {
57
166
  import.meta.hot.accept((newModule) => {
58
167
  if (Route && newModule && newModule.Route) {
59
- (${handleRouteUpdate.toString()})(Route, newModule.Route)
168
+ const routeId = import.meta.hot.data['tsr-route-id'] ?? Route.id
169
+ if (routeId) {
170
+ import.meta.hot.data['tsr-route-id'] = routeId
171
+ }
172
+ (${handleRouteUpdateStr.replace(
173
+ /['"]__TSR_COMPONENT_TYPES__['"]/,
174
+ JSON.stringify(stableRouteOptionKeys),
175
+ )})(routeId, newModule.Route)
60
176
  }
61
- })
177
+ })
62
178
  }
63
179
  `,
64
- // Disable placeholder parsing so identifiers like __TSR_ROUTER__ are treated as normal identifiers instead of template placeholders
65
- { placeholderPattern: false },
66
- )()
180
+ { placeholderPattern: false },
181
+ )()
182
+ }
@@ -116,7 +116,7 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory<
116
116
  code,
117
117
  })
118
118
 
119
- if (fromCode.groupings) {
119
+ if (fromCode.groupings !== undefined) {
120
120
  const res = splitGroupingsSchema.safeParse(fromCode.groupings)
121
121
  if (!res.success) {
122
122
  const message = res.error.errors.map((e) => e.message).join('. ')
@@ -143,7 +143,7 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory<
143
143
  }
144
144
 
145
145
  const splitGroupings: CodeSplitGroupings =
146
- fromCode.groupings || pluginSplitBehavior || getGlobalCodeSplitGroupings()
146
+ fromCode.groupings ?? pluginSplitBehavior ?? getGlobalCodeSplitGroupings()
147
147
 
148
148
  // Compute shared bindings before compiling the reference route
149
149
  const sharedBindings = computeSharedBindings({
@@ -1,7 +1,7 @@
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 { routeHmrStatement } from './route-hmr-statement'
4
+ import { createRouteHmrStatement } from './route-hmr-statement'
5
5
  import { debug, normalizePath } from './utils'
6
6
  import { getConfig } from './config'
7
7
  import type { UnpluginFactory } from 'unplugin'
@@ -44,6 +44,10 @@ export const unpluginRouterHmrFactory: UnpluginFactory<
44
44
  if (debug) console.info('Adding HMR handling to route ', normalizedId)
45
45
 
46
46
  if (userConfig.target === 'react') {
47
+ const compilerPlugins = getReferenceRouteCompilerPlugins({
48
+ targetFramework: 'react',
49
+ addHmr: true,
50
+ })
47
51
  const compiled = compileCodeSplitReferenceRoute({
48
52
  code,
49
53
  filename: normalizedId,
@@ -51,10 +55,7 @@ export const unpluginRouterHmrFactory: UnpluginFactory<
51
55
  addHmr: true,
52
56
  codeSplitGroupings: [],
53
57
  targetFramework: 'react',
54
- compilerPlugins: getReferenceRouteCompilerPlugins({
55
- targetFramework: 'react',
56
- addHmr: true,
57
- }),
58
+ compilerPlugins,
58
59
  })
59
60
 
60
61
  if (compiled) {
@@ -68,7 +69,7 @@ export const unpluginRouterHmrFactory: UnpluginFactory<
68
69
  }
69
70
 
70
71
  const ast = parseAst({ code })
71
- ast.program.body.push(routeHmrStatement)
72
+ ast.program.body.push(createRouteHmrStatement([]))
72
73
  const result = generateFromAst(ast, {
73
74
  sourceMaps: true,
74
75
  filename: normalizedId,
package/src/core/utils.ts CHANGED
@@ -16,6 +16,24 @@ export function normalizePath(path: string): string {
16
16
  return path.replace(/\\/g, '/')
17
17
  }
18
18
 
19
+ export function getObjectPropertyKeyName(
20
+ prop: t.ObjectProperty,
21
+ ): string | undefined {
22
+ if (prop.computed) {
23
+ return undefined
24
+ }
25
+
26
+ if (t.isIdentifier(prop.key)) {
27
+ return prop.key.name
28
+ }
29
+
30
+ if (t.isStringLiteral(prop.key)) {
31
+ return prop.key.value
32
+ }
33
+
34
+ return undefined
35
+ }
36
+
19
37
  export function getUniqueProgramIdentifier(
20
38
  programPath: babel.NodePath<t.Program>,
21
39
  baseName: string,
@@ -23,13 +41,20 @@ export function getUniqueProgramIdentifier(
23
41
  let name = baseName
24
42
  let suffix = 2
25
43
 
44
+ const programScope = programPath.scope.getProgramParent()
45
+
26
46
  while (
27
- programPath.scope.hasBinding(name) ||
28
- programPath.scope.hasGlobal(name)
47
+ programScope.hasBinding(name) ||
48
+ programScope.hasGlobal(name) ||
49
+ programScope.hasReference(name)
29
50
  ) {
30
51
  name = `${baseName}${suffix}`
31
52
  suffix++
32
53
  }
33
54
 
55
+ // Register the name so subsequent calls within the same traversal
56
+ // see it and avoid collisions
57
+ programScope.references[name] = true
58
+
34
59
  return t.identifier(name)
35
60
  }