@tanstack/router-plugin 1.20.3-alpha.1

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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/dist/cjs/core/code-splitter/compilers.cjs +755 -0
  4. package/dist/cjs/core/code-splitter/compilers.cjs.map +1 -0
  5. package/dist/cjs/core/code-splitter/compilers.d.cts +22 -0
  6. package/dist/cjs/core/code-splitter/framework-options.cjs +34 -0
  7. package/dist/cjs/core/code-splitter/framework-options.cjs.map +1 -0
  8. package/dist/cjs/core/code-splitter/framework-options.d.cts +10 -0
  9. package/dist/cjs/core/code-splitter/path-ids.cjs +37 -0
  10. package/dist/cjs/core/code-splitter/path-ids.cjs.map +1 -0
  11. package/dist/cjs/core/code-splitter/path-ids.d.cts +2 -0
  12. package/dist/cjs/core/config.cjs +46 -0
  13. package/dist/cjs/core/config.cjs.map +1 -0
  14. package/dist/cjs/core/config.d.cts +160 -0
  15. package/dist/cjs/core/constants.cjs +19 -0
  16. package/dist/cjs/core/constants.cjs.map +1 -0
  17. package/dist/cjs/core/constants.d.cts +5 -0
  18. package/dist/cjs/core/route-autoimport-plugin.cjs +98 -0
  19. package/dist/cjs/core/route-autoimport-plugin.cjs.map +1 -0
  20. package/dist/cjs/core/route-autoimport-plugin.d.cts +6 -0
  21. package/dist/cjs/core/route-hmr-statement.cjs +33 -0
  22. package/dist/cjs/core/route-hmr-statement.cjs.map +1 -0
  23. package/dist/cjs/core/route-hmr-statement.d.cts +1 -0
  24. package/dist/cjs/core/router-code-splitter-plugin.cjs +173 -0
  25. package/dist/cjs/core/router-code-splitter-plugin.cjs.map +1 -0
  26. package/dist/cjs/core/router-code-splitter-plugin.d.cts +3 -0
  27. package/dist/cjs/core/router-composed-plugin.cjs +27 -0
  28. package/dist/cjs/core/router-composed-plugin.cjs.map +1 -0
  29. package/dist/cjs/core/router-composed-plugin.d.cts +3 -0
  30. package/dist/cjs/core/router-generator-plugin.cjs +145 -0
  31. package/dist/cjs/core/router-generator-plugin.cjs.map +1 -0
  32. package/dist/cjs/core/router-generator-plugin.d.cts +3 -0
  33. package/dist/cjs/core/router-hmr-plugin.cjs +51 -0
  34. package/dist/cjs/core/router-hmr-plugin.cjs.map +1 -0
  35. package/dist/cjs/core/router-hmr-plugin.d.cts +8 -0
  36. package/dist/cjs/core/utils.cjs +12 -0
  37. package/dist/cjs/core/utils.cjs.map +1 -0
  38. package/dist/cjs/core/utils.d.cts +2 -0
  39. package/dist/cjs/esbuild.cjs +20 -0
  40. package/dist/cjs/esbuild.cjs.map +1 -0
  41. package/dist/cjs/esbuild.d.cts +127 -0
  42. package/dist/cjs/index.cjs +9 -0
  43. package/dist/cjs/index.cjs.map +1 -0
  44. package/dist/cjs/index.d.cts +4 -0
  45. package/dist/cjs/rspack.cjs +22 -0
  46. package/dist/cjs/rspack.cjs.map +1 -0
  47. package/dist/cjs/rspack.d.cts +139 -0
  48. package/dist/cjs/vite.cjs +22 -0
  49. package/dist/cjs/vite.cjs.map +1 -0
  50. package/dist/cjs/vite.d.cts +159 -0
  51. package/dist/cjs/webpack.cjs +22 -0
  52. package/dist/cjs/webpack.cjs.map +1 -0
  53. package/dist/cjs/webpack.d.cts +127 -0
  54. package/dist/esm/core/code-splitter/compilers.d.ts +22 -0
  55. package/dist/esm/core/code-splitter/compilers.js +737 -0
  56. package/dist/esm/core/code-splitter/compilers.js.map +1 -0
  57. package/dist/esm/core/code-splitter/framework-options.d.ts +10 -0
  58. package/dist/esm/core/code-splitter/framework-options.js +34 -0
  59. package/dist/esm/core/code-splitter/framework-options.js.map +1 -0
  60. package/dist/esm/core/code-splitter/path-ids.d.ts +2 -0
  61. package/dist/esm/core/code-splitter/path-ids.js +37 -0
  62. package/dist/esm/core/code-splitter/path-ids.js.map +1 -0
  63. package/dist/esm/core/config.d.ts +160 -0
  64. package/dist/esm/core/config.js +46 -0
  65. package/dist/esm/core/config.js.map +1 -0
  66. package/dist/esm/core/constants.d.ts +5 -0
  67. package/dist/esm/core/constants.js +19 -0
  68. package/dist/esm/core/constants.js.map +1 -0
  69. package/dist/esm/core/route-autoimport-plugin.d.ts +6 -0
  70. package/dist/esm/core/route-autoimport-plugin.js +81 -0
  71. package/dist/esm/core/route-autoimport-plugin.js.map +1 -0
  72. package/dist/esm/core/route-hmr-statement.d.ts +1 -0
  73. package/dist/esm/core/route-hmr-statement.js +16 -0
  74. package/dist/esm/core/route-hmr-statement.js.map +1 -0
  75. package/dist/esm/core/router-code-splitter-plugin.d.ts +3 -0
  76. package/dist/esm/core/router-code-splitter-plugin.js +173 -0
  77. package/dist/esm/core/router-code-splitter-plugin.js.map +1 -0
  78. package/dist/esm/core/router-composed-plugin.d.ts +3 -0
  79. package/dist/esm/core/router-composed-plugin.js +27 -0
  80. package/dist/esm/core/router-composed-plugin.js.map +1 -0
  81. package/dist/esm/core/router-generator-plugin.d.ts +3 -0
  82. package/dist/esm/core/router-generator-plugin.js +123 -0
  83. package/dist/esm/core/router-generator-plugin.js.map +1 -0
  84. package/dist/esm/core/router-hmr-plugin.d.ts +8 -0
  85. package/dist/esm/core/router-hmr-plugin.js +51 -0
  86. package/dist/esm/core/router-hmr-plugin.js.map +1 -0
  87. package/dist/esm/core/utils.d.ts +2 -0
  88. package/dist/esm/core/utils.js +12 -0
  89. package/dist/esm/core/utils.js.map +1 -0
  90. package/dist/esm/esbuild.d.ts +127 -0
  91. package/dist/esm/esbuild.js +20 -0
  92. package/dist/esm/esbuild.js.map +1 -0
  93. package/dist/esm/index.d.ts +4 -0
  94. package/dist/esm/index.js +9 -0
  95. package/dist/esm/index.js.map +1 -0
  96. package/dist/esm/rspack.d.ts +139 -0
  97. package/dist/esm/rspack.js +22 -0
  98. package/dist/esm/rspack.js.map +1 -0
  99. package/dist/esm/vite.d.ts +159 -0
  100. package/dist/esm/vite.js +22 -0
  101. package/dist/esm/vite.js.map +1 -0
  102. package/dist/esm/webpack.d.ts +127 -0
  103. package/dist/esm/webpack.js +22 -0
  104. package/dist/esm/webpack.js.map +1 -0
  105. package/package.json +133 -0
  106. package/src/core/code-splitter/compilers.ts +1005 -0
  107. package/src/core/code-splitter/framework-options.ts +41 -0
  108. package/src/core/code-splitter/path-ids.ts +39 -0
  109. package/src/core/config.ts +80 -0
  110. package/src/core/constants.ts +17 -0
  111. package/src/core/route-autoimport-plugin.ts +102 -0
  112. package/src/core/route-hmr-statement.ts +13 -0
  113. package/src/core/router-code-splitter-plugin.ts +253 -0
  114. package/src/core/router-composed-plugin.ts +32 -0
  115. package/src/core/router-generator-plugin.ts +172 -0
  116. package/src/core/router-hmr-plugin.ts +65 -0
  117. package/src/core/utils.ts +18 -0
  118. package/src/esbuild.ts +56 -0
  119. package/src/index.ts +4 -0
  120. package/src/rspack.ts +67 -0
  121. package/src/vite.ts +57 -0
  122. package/src/webpack.ts +55 -0
@@ -0,0 +1,1005 @@
1
+ import * as t from '@babel/types'
2
+ import babel from '@babel/core'
3
+ import * as template from '@babel/template'
4
+ import {
5
+ deadCodeElimination,
6
+ findReferencedIdentifiers,
7
+ } from 'babel-dead-code-elimination'
8
+ import { generateFromAst, parseAst } from '@tanstack/router-utils'
9
+ import { tsrSplit } from '../constants'
10
+ import { routeHmrStatement } from '../route-hmr-statement'
11
+ import { createIdentifier } from './path-ids'
12
+ import { getFrameworkOptions } from './framework-options'
13
+ import type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils'
14
+ import type { CodeSplitGroupings, SplitRouteIdentNodes } from '../constants'
15
+ import type { Config } from '../config'
16
+
17
+ // eslint-disable-next-line unused-imports/no-unused-vars
18
+ const debug = process.env.TSR_VITE_DEBUG
19
+
20
+ type SplitModulesById = Record<
21
+ string,
22
+ { id: string; node: t.FunctionExpression }
23
+ >
24
+
25
+ interface State {
26
+ filename: string
27
+ opts: {
28
+ minify: boolean
29
+ root: string
30
+ }
31
+ imported: Record<string, boolean>
32
+ refs: Set<any>
33
+ serverIndex: number
34
+ splitIndex: number
35
+ splitModulesById: SplitModulesById
36
+ }
37
+
38
+ type SplitNodeMeta = {
39
+ routeIdent: SplitRouteIdentNodes
40
+ splitStrategy: 'lazyFn' | 'lazyRouteComponent'
41
+ localImporterIdent: string
42
+ exporterIdent: string
43
+ localExporterIdent: string
44
+ }
45
+ const SPLIT_NODES_CONFIG = new Map<SplitRouteIdentNodes, SplitNodeMeta>([
46
+ [
47
+ 'loader',
48
+ {
49
+ routeIdent: 'loader',
50
+ localImporterIdent: '$$splitLoaderImporter', // const $$splitLoaderImporter = () => import('...')
51
+ splitStrategy: 'lazyFn',
52
+ localExporterIdent: 'SplitLoader', // const SplitLoader = ...
53
+ exporterIdent: 'loader', // export { SplitLoader as loader }
54
+ },
55
+ ],
56
+ [
57
+ 'component',
58
+ {
59
+ routeIdent: 'component',
60
+ localImporterIdent: '$$splitComponentImporter', // const $$splitComponentImporter = () => import('...')
61
+ splitStrategy: 'lazyRouteComponent',
62
+ localExporterIdent: 'SplitComponent', // const SplitComponent = ...
63
+ exporterIdent: 'component', // export { SplitComponent as component }
64
+ },
65
+ ],
66
+ [
67
+ 'pendingComponent',
68
+ {
69
+ routeIdent: 'pendingComponent',
70
+ localImporterIdent: '$$splitPendingComponentImporter', // const $$splitPendingComponentImporter = () => import('...')
71
+ splitStrategy: 'lazyRouteComponent',
72
+ localExporterIdent: 'SplitPendingComponent', // const SplitPendingComponent = ...
73
+ exporterIdent: 'pendingComponent', // export { SplitPendingComponent as pendingComponent }
74
+ },
75
+ ],
76
+ [
77
+ 'errorComponent',
78
+ {
79
+ routeIdent: 'errorComponent',
80
+ localImporterIdent: '$$splitErrorComponentImporter', // const $$splitErrorComponentImporter = () => import('...')
81
+ splitStrategy: 'lazyRouteComponent',
82
+ localExporterIdent: 'SplitErrorComponent', // const SplitErrorComponent = ...
83
+ exporterIdent: 'errorComponent', // export { SplitErrorComponent as errorComponent }
84
+ },
85
+ ],
86
+ [
87
+ 'notFoundComponent',
88
+ {
89
+ routeIdent: 'notFoundComponent',
90
+ localImporterIdent: '$$splitNotFoundComponentImporter', // const $$splitNotFoundComponentImporter = () => import('...')
91
+ splitStrategy: 'lazyRouteComponent',
92
+ localExporterIdent: 'SplitNotFoundComponent', // const SplitNotFoundComponent = ...
93
+ exporterIdent: 'notFoundComponent', // export { SplitNotFoundComponent as notFoundComponent }
94
+ },
95
+ ],
96
+ ])
97
+ const KNOWN_SPLIT_ROUTE_IDENTS = [...SPLIT_NODES_CONFIG.keys()] as const
98
+
99
+ function addSplitSearchParamToFilename(
100
+ filename: string,
101
+ grouping: Array<string>,
102
+ ) {
103
+ const [bareFilename] = filename.split('?')
104
+
105
+ const params = new URLSearchParams()
106
+ params.append(tsrSplit, createIdentifier(grouping))
107
+
108
+ return `${bareFilename}?${params.toString()}`
109
+ }
110
+
111
+ function removeSplitSearchParamFromFilename(filename: string) {
112
+ const [bareFilename] = filename.split('?')
113
+ return bareFilename!
114
+ }
115
+
116
+ export function compileCodeSplitReferenceRoute(
117
+ opts: ParseAstOptions & {
118
+ runtimeEnv: 'dev' | 'prod'
119
+ codeSplitGroupings: CodeSplitGroupings
120
+ targetFramework: Config['target']
121
+ filename: string
122
+ id: string
123
+ },
124
+ ): GeneratorResult {
125
+ const ast = parseAst(opts)
126
+
127
+ const refIdents = findReferencedIdentifiers(ast)
128
+
129
+ function findIndexForSplitNode(str: string) {
130
+ return opts.codeSplitGroupings.findIndex((group) =>
131
+ group.includes(str as any),
132
+ )
133
+ }
134
+
135
+ const frameworkOptions = getFrameworkOptions(opts.targetFramework)
136
+ const PACKAGE = frameworkOptions.package
137
+ const LAZY_ROUTE_COMPONENT_IDENT = frameworkOptions.idents.lazyRouteComponent
138
+ const LAZY_FN_IDENT = frameworkOptions.idents.lazyFn
139
+
140
+ babel.traverse(ast, {
141
+ Program: {
142
+ enter(programPath, programState) {
143
+ const state = programState as unknown as State
144
+
145
+ /**
146
+ * If the component for the route is being imported from
147
+ * another file, this is to track the path to that file
148
+ * the path itself doesn't matter, we just need to keep
149
+ * track of it so that we can remove it from the imports
150
+ * list if it's not being used like:
151
+ *
152
+ * `import '../shared/imported'`
153
+ */
154
+ const removableImportPaths = new Set<string>([])
155
+
156
+ programPath.traverse(
157
+ {
158
+ CallExpression: (path) => {
159
+ if (!t.isIdentifier(path.node.callee)) {
160
+ return
161
+ }
162
+
163
+ if (
164
+ !(
165
+ path.node.callee.name === 'createRoute' ||
166
+ path.node.callee.name === 'createFileRoute'
167
+ )
168
+ ) {
169
+ return
170
+ }
171
+
172
+ function babelHandleReference(routeOptions: t.Node | undefined) {
173
+ const hasImportedOrDefinedIdentifier = (name: string) => {
174
+ return programPath.scope.hasBinding(name)
175
+ }
176
+
177
+ if (t.isObjectExpression(routeOptions)) {
178
+ routeOptions.properties.forEach((prop) => {
179
+ if (t.isObjectProperty(prop)) {
180
+ if (t.isIdentifier(prop.key)) {
181
+ // If the user has not specified a split grouping for this key
182
+ // then we should not split it
183
+ const codeSplitGroupingByKey = findIndexForSplitNode(
184
+ prop.key.name,
185
+ )
186
+ if (codeSplitGroupingByKey === -1) {
187
+ return
188
+ }
189
+ const codeSplitGroup = [
190
+ ...new Set(
191
+ opts.codeSplitGroupings[codeSplitGroupingByKey],
192
+ ),
193
+ ]
194
+
195
+ const key = prop.key.name
196
+ // find key in nodeSplitConfig
197
+ const isNodeConfigAvailable = SPLIT_NODES_CONFIG.has(
198
+ key as any,
199
+ )
200
+
201
+ if (!isNodeConfigAvailable) {
202
+ return
203
+ }
204
+
205
+ const splitNodeMeta = SPLIT_NODES_CONFIG.get(
206
+ key as any,
207
+ )!
208
+
209
+ // We need to extract the existing search params from the filename, if any
210
+ // and add the relevant codesplitPrefix to them, then write them back to the filename
211
+ const splitUrl = addSplitSearchParamToFilename(
212
+ opts.filename,
213
+ codeSplitGroup,
214
+ )
215
+
216
+ if (
217
+ splitNodeMeta.splitStrategy === 'lazyRouteComponent'
218
+ ) {
219
+ const value = prop.value
220
+
221
+ let shouldSplit = true
222
+
223
+ if (t.isIdentifier(value)) {
224
+ const existingImportPath =
225
+ getImportSpecifierAndPathFromLocalName(
226
+ programPath,
227
+ value.name,
228
+ ).path
229
+ if (existingImportPath) {
230
+ removableImportPaths.add(existingImportPath)
231
+ }
232
+
233
+ // exported identifiers should not be split
234
+ // since they are already being imported
235
+ // and need to be retained in the compiled file
236
+ const isExported = hasExport(ast, value)
237
+ shouldSplit = !isExported
238
+
239
+ if (shouldSplit) {
240
+ removeIdentifierLiteral(path, value)
241
+ }
242
+ }
243
+
244
+ if (!shouldSplit) {
245
+ return
246
+ }
247
+
248
+ // Prepend the import statement to the program along with the importer function
249
+ // Check to see if lazyRouteComponent is already imported before attempting
250
+ // to import it again
251
+
252
+ if (
253
+ !hasImportedOrDefinedIdentifier(
254
+ LAZY_ROUTE_COMPONENT_IDENT,
255
+ )
256
+ ) {
257
+ programPath.unshiftContainer('body', [
258
+ template.statement(
259
+ `import { ${LAZY_ROUTE_COMPONENT_IDENT} } from '${PACKAGE}'`,
260
+ )(),
261
+ ])
262
+ }
263
+
264
+ // Check to see if the importer function is already defined
265
+ // If not, define it with the dynamic import statement
266
+ if (
267
+ !hasImportedOrDefinedIdentifier(
268
+ splitNodeMeta.localImporterIdent,
269
+ )
270
+ ) {
271
+ programPath.unshiftContainer('body', [
272
+ template.statement(
273
+ `const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`,
274
+ )(),
275
+ ])
276
+ }
277
+
278
+ // If it's a component, we need to pass the function to check the Route.ssr value
279
+ if (key === 'component') {
280
+ prop.value = template.expression(
281
+ `${LAZY_ROUTE_COMPONENT_IDENT}(${splitNodeMeta.localImporterIdent}, '${splitNodeMeta.exporterIdent}', () => Route.ssr)`,
282
+ )()
283
+ } else {
284
+ prop.value = template.expression(
285
+ `${LAZY_ROUTE_COMPONENT_IDENT}(${splitNodeMeta.localImporterIdent}, '${splitNodeMeta.exporterIdent}')`,
286
+ )()
287
+ }
288
+
289
+ // add HMR handling
290
+ if (opts.runtimeEnv !== 'prod') {
291
+ programPath.pushContainer('body', routeHmrStatement)
292
+ }
293
+ }
294
+
295
+ if (splitNodeMeta.splitStrategy === 'lazyFn') {
296
+ const value = prop.value
297
+
298
+ let shouldSplit = true
299
+
300
+ if (t.isIdentifier(value)) {
301
+ const existingImportPath =
302
+ getImportSpecifierAndPathFromLocalName(
303
+ programPath,
304
+ value.name,
305
+ ).path
306
+ if (existingImportPath) {
307
+ removableImportPaths.add(existingImportPath)
308
+ }
309
+
310
+ // exported identifiers should not be split
311
+ // since they are already being imported
312
+ // and need to be retained in the compiled file
313
+ const isExported = hasExport(ast, value)
314
+ shouldSplit = !isExported
315
+
316
+ if (shouldSplit) {
317
+ removeIdentifierLiteral(path, value)
318
+ }
319
+ }
320
+
321
+ if (!shouldSplit) {
322
+ return
323
+ }
324
+
325
+ // Prepend the import statement to the program along with the importer function
326
+ if (!hasImportedOrDefinedIdentifier(LAZY_FN_IDENT)) {
327
+ programPath.unshiftContainer(
328
+ 'body',
329
+ template.smart(
330
+ `import { ${LAZY_FN_IDENT} } from '${PACKAGE}'`,
331
+ )(),
332
+ )
333
+ }
334
+
335
+ // Check to see if the importer function is already defined
336
+ // If not, define it with the dynamic import statement
337
+ if (
338
+ !hasImportedOrDefinedIdentifier(
339
+ splitNodeMeta.localImporterIdent,
340
+ )
341
+ ) {
342
+ programPath.unshiftContainer('body', [
343
+ template.statement(
344
+ `const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`,
345
+ )(),
346
+ ])
347
+ }
348
+
349
+ // Add the lazyFn call with the dynamic import to the prop value
350
+ prop.value = template.expression(
351
+ `${LAZY_FN_IDENT}(${splitNodeMeta.localImporterIdent}, '${splitNodeMeta.exporterIdent}')`,
352
+ )()
353
+ }
354
+ }
355
+ }
356
+
357
+ programPath.scope.crawl()
358
+ })
359
+ }
360
+ }
361
+
362
+ if (t.isCallExpression(path.parentPath.node)) {
363
+ // createFileRoute('/')({ ... })
364
+ const options = resolveIdentifier(
365
+ path,
366
+ path.parentPath.node.arguments[0],
367
+ )
368
+
369
+ babelHandleReference(options)
370
+ } else if (t.isVariableDeclarator(path.parentPath.node)) {
371
+ // createFileRoute({ ... })
372
+ const caller = resolveIdentifier(
373
+ path,
374
+ path.parentPath.node.init,
375
+ )
376
+
377
+ if (t.isCallExpression(caller)) {
378
+ const options = resolveIdentifier(path, caller.arguments[0])
379
+ babelHandleReference(options)
380
+ }
381
+ }
382
+ },
383
+ },
384
+ state,
385
+ )
386
+
387
+ /**
388
+ * If the component for the route is being imported,
389
+ * and it's not being used, remove the import statement
390
+ * from the program, by checking that the import has no
391
+ * specifiers
392
+ */
393
+ if (removableImportPaths.size > 0) {
394
+ programPath.traverse({
395
+ ImportDeclaration(path) {
396
+ if (path.node.specifiers.length > 0) return
397
+ if (removableImportPaths.has(path.node.source.value)) {
398
+ path.remove()
399
+ }
400
+ },
401
+ })
402
+ }
403
+ },
404
+ },
405
+ })
406
+
407
+ deadCodeElimination(ast, refIdents)
408
+
409
+ return generateFromAst(ast, {
410
+ sourceMaps: true,
411
+ sourceFileName: opts.filename,
412
+ filename: opts.filename,
413
+ })
414
+ }
415
+
416
+ export function compileCodeSplitVirtualRoute(
417
+ opts: ParseAstOptions & {
418
+ splitTargets: Array<SplitRouteIdentNodes>
419
+ filename: string
420
+ },
421
+ ): GeneratorResult {
422
+ const ast = parseAst(opts)
423
+ const refIdents = findReferencedIdentifiers(ast)
424
+
425
+ const intendedSplitNodes = new Set(opts.splitTargets)
426
+
427
+ const knownExportedIdents = new Set<string>()
428
+
429
+ babel.traverse(ast, {
430
+ Program: {
431
+ enter(programPath, programState) {
432
+ const state = programState as unknown as State
433
+
434
+ const trackedNodesToSplitByType: Record<
435
+ SplitRouteIdentNodes,
436
+ { node: t.Node | undefined; meta: SplitNodeMeta } | undefined
437
+ > = {
438
+ component: undefined,
439
+ loader: undefined,
440
+ pendingComponent: undefined,
441
+ errorComponent: undefined,
442
+ notFoundComponent: undefined,
443
+ }
444
+
445
+ // Find and track all the known split-able nodes
446
+ programPath.traverse(
447
+ {
448
+ CallExpression: (path) => {
449
+ if (!t.isIdentifier(path.node.callee)) {
450
+ return
451
+ }
452
+
453
+ if (
454
+ !(
455
+ path.node.callee.name === 'createRoute' ||
456
+ path.node.callee.name === 'createFileRoute'
457
+ )
458
+ ) {
459
+ return
460
+ }
461
+
462
+ function babelHandleVirtual(options: t.Node | undefined) {
463
+ if (t.isObjectExpression(options)) {
464
+ options.properties.forEach((prop) => {
465
+ if (t.isObjectProperty(prop)) {
466
+ // do not use `intendedSplitNodes` here
467
+ // since we have special considerations that need
468
+ // to be accounted for like (not splitting exported identifiers)
469
+ KNOWN_SPLIT_ROUTE_IDENTS.forEach((splitType) => {
470
+ if (
471
+ !t.isIdentifier(prop.key) ||
472
+ prop.key.name !== splitType
473
+ ) {
474
+ return
475
+ }
476
+
477
+ const value = prop.value
478
+
479
+ let isExported = false
480
+ if (t.isIdentifier(value)) {
481
+ isExported = hasExport(ast, value)
482
+ if (isExported) {
483
+ knownExportedIdents.add(value.name)
484
+ }
485
+ }
486
+
487
+ // If the node is exported, we need to remove
488
+ // the export from the split file
489
+ if (isExported && t.isIdentifier(value)) {
490
+ removeExports(ast, value)
491
+ } else {
492
+ const meta = SPLIT_NODES_CONFIG.get(splitType)!
493
+ trackedNodesToSplitByType[splitType] = {
494
+ node: prop.value,
495
+ meta,
496
+ }
497
+ }
498
+ })
499
+ }
500
+ })
501
+
502
+ // Remove all of the options
503
+ options.properties = []
504
+ }
505
+ }
506
+
507
+ if (t.isCallExpression(path.parentPath.node)) {
508
+ // createFileRoute('/')({ ... })
509
+ const options = resolveIdentifier(
510
+ path,
511
+ path.parentPath.node.arguments[0],
512
+ )
513
+
514
+ babelHandleVirtual(options)
515
+ } else if (t.isVariableDeclarator(path.parentPath.node)) {
516
+ // createFileRoute({ ... })
517
+ const caller = resolveIdentifier(
518
+ path,
519
+ path.parentPath.node.init,
520
+ )
521
+
522
+ if (t.isCallExpression(caller)) {
523
+ const options = resolveIdentifier(path, caller.arguments[0])
524
+ babelHandleVirtual(options)
525
+ }
526
+ }
527
+ },
528
+ },
529
+ state,
530
+ )
531
+
532
+ // Start the transformation to only exported the intended split nodes
533
+ intendedSplitNodes.forEach((SPLIT_TYPE) => {
534
+ const splitKey = trackedNodesToSplitByType[SPLIT_TYPE]
535
+
536
+ if (!splitKey) {
537
+ return
538
+ }
539
+
540
+ let splitNode = splitKey.node
541
+ const splitMeta = splitKey.meta
542
+
543
+ while (t.isIdentifier(splitNode)) {
544
+ const binding = programPath.scope.getBinding(splitNode.name)
545
+ splitNode = binding?.path.node
546
+ }
547
+
548
+ // Add the node to the program
549
+ if (splitNode) {
550
+ if (t.isFunctionDeclaration(splitNode)) {
551
+ programPath.pushContainer(
552
+ 'body',
553
+ t.variableDeclaration('const', [
554
+ t.variableDeclarator(
555
+ t.identifier(splitMeta.localExporterIdent),
556
+ t.functionExpression(
557
+ splitNode.id || null, // Anonymize the function expression
558
+ splitNode.params,
559
+ splitNode.body,
560
+ splitNode.generator,
561
+ splitNode.async,
562
+ ),
563
+ ),
564
+ ]),
565
+ )
566
+ } else if (
567
+ t.isFunctionExpression(splitNode) ||
568
+ t.isArrowFunctionExpression(splitNode)
569
+ ) {
570
+ programPath.pushContainer(
571
+ 'body',
572
+ t.variableDeclaration('const', [
573
+ t.variableDeclarator(
574
+ t.identifier(splitMeta.localExporterIdent),
575
+ splitNode as any,
576
+ ),
577
+ ]),
578
+ )
579
+ } else if (
580
+ t.isImportSpecifier(splitNode) ||
581
+ t.isImportDefaultSpecifier(splitNode)
582
+ ) {
583
+ programPath.pushContainer(
584
+ 'body',
585
+ t.variableDeclaration('const', [
586
+ t.variableDeclarator(
587
+ t.identifier(splitMeta.localExporterIdent),
588
+ splitNode.local,
589
+ ),
590
+ ]),
591
+ )
592
+ } else if (t.isVariableDeclarator(splitNode)) {
593
+ programPath.pushContainer(
594
+ 'body',
595
+ t.variableDeclaration('const', [
596
+ t.variableDeclarator(
597
+ t.identifier(splitMeta.localExporterIdent),
598
+ splitNode.init,
599
+ ),
600
+ ]),
601
+ )
602
+ } else if (t.isCallExpression(splitNode)) {
603
+ const outputSplitNodeCode = generateFromAst(splitNode).code
604
+ const splitNodeAst = babel.parse(outputSplitNodeCode)
605
+
606
+ if (!splitNodeAst) {
607
+ throw new Error(
608
+ `Failed to parse the generated code for "${SPLIT_TYPE}" in the node type "${splitNode.type}"`,
609
+ )
610
+ }
611
+
612
+ const statement = splitNodeAst.program.body[0]
613
+
614
+ if (!statement) {
615
+ throw new Error(
616
+ `Failed to parse the generated code for "${SPLIT_TYPE}" in the node type "${splitNode.type}" as no statement was found in the program body`,
617
+ )
618
+ }
619
+
620
+ if (t.isExpressionStatement(statement)) {
621
+ const expression = statement.expression
622
+ programPath.pushContainer(
623
+ 'body',
624
+ t.variableDeclaration('const', [
625
+ t.variableDeclarator(
626
+ t.identifier(splitMeta.localExporterIdent),
627
+ expression,
628
+ ),
629
+ ]),
630
+ )
631
+ } else {
632
+ throw new Error(
633
+ `Unexpected expression type encounter for "${SPLIT_TYPE}" in the node type "${splitNode.type}"`,
634
+ )
635
+ }
636
+ } else if (t.isConditionalExpression(splitNode)) {
637
+ programPath.pushContainer(
638
+ 'body',
639
+ t.variableDeclaration('const', [
640
+ t.variableDeclarator(
641
+ t.identifier(splitMeta.localExporterIdent),
642
+ splitNode,
643
+ ),
644
+ ]),
645
+ )
646
+ } else if (t.isTSAsExpression(splitNode)) {
647
+ // remove the type assertion
648
+ splitNode = splitNode.expression
649
+ programPath.pushContainer(
650
+ 'body',
651
+ t.variableDeclaration('const', [
652
+ t.variableDeclarator(
653
+ t.identifier(splitMeta.localExporterIdent),
654
+ splitNode,
655
+ ),
656
+ ]),
657
+ )
658
+ } else {
659
+ console.info('Unexpected splitNode type:', splitNode)
660
+ throw new Error(`Unexpected splitNode type ☝️: ${splitNode.type}`)
661
+ }
662
+ }
663
+
664
+ // If the splitNode exists at the top of the program
665
+ // then we need to remove that copy
666
+ programPath.node.body = programPath.node.body.filter((node) => {
667
+ return node !== splitNode
668
+ })
669
+
670
+ // Export the node
671
+ programPath.pushContainer('body', [
672
+ t.exportNamedDeclaration(null, [
673
+ t.exportSpecifier(
674
+ t.identifier(splitMeta.localExporterIdent), // local variable name
675
+ t.identifier(splitMeta.exporterIdent), // as what name it should be exported as
676
+ ),
677
+ ]),
678
+ ])
679
+ })
680
+
681
+ // convert exports to imports from the original file
682
+ programPath.traverse({
683
+ ExportNamedDeclaration(path) {
684
+ // e.g. export const x = 1 or export { x }
685
+ // becomes
686
+ // import { x } from '${opts.id}'
687
+
688
+ if (path.node.declaration) {
689
+ if (t.isVariableDeclaration(path.node.declaration)) {
690
+ path.replaceWith(
691
+ t.importDeclaration(
692
+ path.node.declaration.declarations.map((decl) =>
693
+ t.importSpecifier(
694
+ t.identifier((decl.id as any).name),
695
+ t.identifier((decl.id as any).name),
696
+ ),
697
+ ),
698
+ t.stringLiteral(
699
+ removeSplitSearchParamFromFilename(opts.filename),
700
+ ),
701
+ ),
702
+ )
703
+ }
704
+ }
705
+ },
706
+ })
707
+ },
708
+ },
709
+ })
710
+
711
+ deadCodeElimination(ast, refIdents)
712
+
713
+ // if there are exported identifiers, then we need to add a warning
714
+ // to the file to let the user know that the exported identifiers
715
+ // will not in the split file but in the original file, therefore
716
+ // increasing the bundle size
717
+ if (knownExportedIdents.size > 0) {
718
+ const list = Array.from(knownExportedIdents).reduce((str, ident) => {
719
+ str += `\n- ${ident}`
720
+ return str
721
+ }, '')
722
+
723
+ const warningMessage = `These exports from "${opts.filename}" are not being code-split and will increase your bundle size: ${list}\nThese should either have their export statements removed or be imported from another file that is not a route.`
724
+ console.warn(warningMessage)
725
+
726
+ // append this warning to the file using a template
727
+ if (process.env.NODE_ENV !== 'production') {
728
+ const warningTemplate = template.statement(
729
+ `console.warn(${JSON.stringify(warningMessage)})`,
730
+ )()
731
+ ast.program.body.unshift(warningTemplate)
732
+ }
733
+ }
734
+
735
+ return generateFromAst(ast, {
736
+ sourceMaps: true,
737
+ sourceFileName: opts.filename,
738
+ filename: opts.filename,
739
+ })
740
+ }
741
+
742
+ /**
743
+ * This function should read get the options from by searching for the key `codeSplitGroupings`
744
+ * on createFileRoute and return it's values if it exists, else return undefined
745
+ */
746
+ export function detectCodeSplitGroupingsFromRoute(opts: ParseAstOptions): {
747
+ groupings: CodeSplitGroupings | undefined
748
+ routeId: string
749
+ } {
750
+ const ast = parseAst(opts)
751
+
752
+ let routeId = ''
753
+
754
+ let codeSplitGroupings: CodeSplitGroupings | undefined = undefined
755
+
756
+ babel.traverse(ast, {
757
+ Program: {
758
+ enter(programPath) {
759
+ programPath.traverse({
760
+ CallExpression(path) {
761
+ if (!t.isIdentifier(path.node.callee)) {
762
+ return
763
+ }
764
+
765
+ if (
766
+ !(
767
+ path.node.callee.name === 'createRoute' ||
768
+ path.node.callee.name === 'createFileRoute'
769
+ )
770
+ ) {
771
+ return
772
+ }
773
+
774
+ if (t.isCallExpression(path.parentPath.node)) {
775
+ // Extract out the routeId
776
+ if (t.isCallExpression(path.parentPath.node.callee)) {
777
+ const callee = path.parentPath.node.callee
778
+
779
+ if (t.isIdentifier(callee.callee)) {
780
+ const firstArg = callee.arguments[0]
781
+ if (t.isStringLiteral(firstArg)) {
782
+ routeId = firstArg.value
783
+ }
784
+ }
785
+ }
786
+
787
+ // Extracting the codeSplitGroupings
788
+ const options = resolveIdentifier(
789
+ path,
790
+ path.parentPath.node.arguments[0],
791
+ )
792
+ if (t.isObjectExpression(options)) {
793
+ options.properties.forEach((prop) => {
794
+ if (t.isObjectProperty(prop)) {
795
+ if (t.isIdentifier(prop.key)) {
796
+ if (prop.key.name === 'codeSplitGroupings') {
797
+ const value = prop.value
798
+
799
+ if (t.isArrayExpression(value)) {
800
+ codeSplitGroupings = value.elements.map((group) => {
801
+ if (t.isArrayExpression(group)) {
802
+ return group.elements.map((node) => {
803
+ if (!t.isStringLiteral(node)) {
804
+ throw new Error(
805
+ 'You must provide a string literal for the codeSplitGroupings',
806
+ )
807
+ }
808
+
809
+ return node.value
810
+ }) as Array<SplitRouteIdentNodes>
811
+ }
812
+
813
+ throw new Error(
814
+ 'You must provide arrays with codeSplitGroupings options.',
815
+ )
816
+ })
817
+ } else {
818
+ throw new Error(
819
+ 'You must provide an array of arrays for the codeSplitGroupings.',
820
+ )
821
+ }
822
+ }
823
+ }
824
+ }
825
+ })
826
+ }
827
+ }
828
+ },
829
+ })
830
+ },
831
+ },
832
+ })
833
+
834
+ return { groupings: codeSplitGroupings, routeId }
835
+ }
836
+
837
+ function getImportSpecifierAndPathFromLocalName(
838
+ programPath: babel.NodePath<t.Program>,
839
+ name: string,
840
+ ): {
841
+ specifier:
842
+ | t.ImportSpecifier
843
+ | t.ImportDefaultSpecifier
844
+ | t.ImportNamespaceSpecifier
845
+ | null
846
+ path: string | null
847
+ } {
848
+ let specifier:
849
+ | t.ImportSpecifier
850
+ | t.ImportDefaultSpecifier
851
+ | t.ImportNamespaceSpecifier
852
+ | null = null
853
+ let path: string | null = null
854
+
855
+ programPath.traverse({
856
+ ImportDeclaration(importPath) {
857
+ const found = importPath.node.specifiers.find(
858
+ (targetSpecifier) => targetSpecifier.local.name === name,
859
+ )
860
+ if (found) {
861
+ specifier = found
862
+ path = importPath.node.source.value
863
+ }
864
+ },
865
+ })
866
+
867
+ return { specifier, path }
868
+ }
869
+
870
+ // Reusable function to get literal value or resolve variable to literal
871
+ function resolveIdentifier(path: any, node: any): t.Node | undefined {
872
+ if (t.isIdentifier(node)) {
873
+ const binding = path.scope.getBinding(node.name)
874
+ if (
875
+ binding
876
+ // && binding.kind === 'const'
877
+ ) {
878
+ const declarator = binding.path.node
879
+ if (t.isObjectExpression(declarator.init)) {
880
+ return declarator.init
881
+ } else if (t.isFunctionDeclaration(declarator.init)) {
882
+ return declarator.init
883
+ }
884
+ }
885
+ return undefined
886
+ }
887
+
888
+ return node
889
+ }
890
+
891
+ function removeIdentifierLiteral(path: any, node: any) {
892
+ if (t.isIdentifier(node)) {
893
+ const binding = path.scope.getBinding(node.name)
894
+ if (binding) {
895
+ binding.path.remove()
896
+ }
897
+ }
898
+ }
899
+
900
+ function hasExport(ast: t.File, node: t.Identifier): boolean {
901
+ let found = false
902
+
903
+ babel.traverse(ast, {
904
+ ExportNamedDeclaration(path) {
905
+ if (path.node.declaration) {
906
+ // declared as `const loaderFn = () => {}`
907
+ if (t.isVariableDeclaration(path.node.declaration)) {
908
+ path.node.declaration.declarations.forEach((decl) => {
909
+ if (t.isVariableDeclarator(decl)) {
910
+ if (t.isIdentifier(decl.id)) {
911
+ if (decl.id.name === node.name) {
912
+ found = true
913
+ }
914
+ }
915
+ }
916
+ })
917
+ }
918
+
919
+ // declared as `function loaderFn() {}`
920
+ if (t.isFunctionDeclaration(path.node.declaration)) {
921
+ if (t.isIdentifier(path.node.declaration.id)) {
922
+ if (path.node.declaration.id.name === node.name) {
923
+ found = true
924
+ }
925
+ }
926
+ }
927
+ }
928
+ },
929
+ ExportDefaultDeclaration(path) {
930
+ // declared as `export default loaderFn`
931
+ if (t.isIdentifier(path.node.declaration)) {
932
+ if (path.node.declaration.name === node.name) {
933
+ found = true
934
+ }
935
+ }
936
+
937
+ // declared as `export default function loaderFn() {}`
938
+ if (t.isFunctionDeclaration(path.node.declaration)) {
939
+ if (t.isIdentifier(path.node.declaration.id)) {
940
+ if (path.node.declaration.id.name === node.name) {
941
+ found = true
942
+ }
943
+ }
944
+ }
945
+ },
946
+ })
947
+
948
+ return found
949
+ }
950
+
951
+ function removeExports(ast: t.File, node: t.Identifier): boolean {
952
+ let removed = false
953
+
954
+ // The checks use sequential if/else if statements since it
955
+ // directly mutates the AST and as such doing normal checks
956
+ // (using only if statements) could lead to a situation where
957
+ // `path.node` is null since it has been already removed from
958
+ // the program tree but typescript doesn't know that.
959
+ babel.traverse(ast, {
960
+ ExportNamedDeclaration(path) {
961
+ if (path.node.declaration) {
962
+ if (t.isVariableDeclaration(path.node.declaration)) {
963
+ // declared as `const loaderFn = () => {}`
964
+ path.node.declaration.declarations.forEach((decl) => {
965
+ if (t.isVariableDeclarator(decl)) {
966
+ if (t.isIdentifier(decl.id)) {
967
+ if (decl.id.name === node.name) {
968
+ path.remove()
969
+ removed = true
970
+ }
971
+ }
972
+ }
973
+ })
974
+ } else if (t.isFunctionDeclaration(path.node.declaration)) {
975
+ // declared as `export const loaderFn = () => {}`
976
+ if (t.isIdentifier(path.node.declaration.id)) {
977
+ if (path.node.declaration.id.name === node.name) {
978
+ path.remove()
979
+ removed = true
980
+ }
981
+ }
982
+ }
983
+ }
984
+ },
985
+ ExportDefaultDeclaration(path) {
986
+ // declared as `export default loaderFn`
987
+ if (t.isIdentifier(path.node.declaration)) {
988
+ if (path.node.declaration.name === node.name) {
989
+ path.remove()
990
+ removed = true
991
+ }
992
+ } else if (t.isFunctionDeclaration(path.node.declaration)) {
993
+ // declared as `export default function loaderFn() {}`
994
+ if (t.isIdentifier(path.node.declaration.id)) {
995
+ if (path.node.declaration.id.name === node.name) {
996
+ path.remove()
997
+ removed = true
998
+ }
999
+ }
1000
+ }
1001
+ },
1002
+ })
1003
+
1004
+ return removed
1005
+ }