@tanstack/router-vite-plugin 1.24.1 → 1.26.6

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 (43) hide show
  1. package/dist/cjs/compilers.cjs +335 -0
  2. package/dist/cjs/compilers.cjs.map +1 -0
  3. package/dist/cjs/compilers.d.cts +38 -0
  4. package/dist/cjs/constants.cjs +5 -0
  5. package/dist/cjs/constants.cjs.map +1 -0
  6. package/dist/cjs/constants.d.cts +1 -0
  7. package/dist/cjs/eliminateUnreferencedIdentifiers.cjs +144 -0
  8. package/dist/cjs/eliminateUnreferencedIdentifiers.cjs.map +1 -0
  9. package/dist/cjs/eliminateUnreferencedIdentifiers.d.cts +8 -0
  10. package/dist/cjs/index.cjs +103 -8
  11. package/dist/cjs/index.cjs.map +1 -1
  12. package/dist/cjs/index.d.cts +55 -2
  13. package/dist/esm/compilers.d.ts +38 -0
  14. package/dist/esm/compilers.js +316 -0
  15. package/dist/esm/compilers.js.map +1 -0
  16. package/dist/esm/constants.d.ts +1 -0
  17. package/dist/esm/constants.js +5 -0
  18. package/dist/esm/constants.js.map +1 -0
  19. package/dist/esm/eliminateUnreferencedIdentifiers.d.ts +8 -0
  20. package/dist/esm/eliminateUnreferencedIdentifiers.js +127 -0
  21. package/dist/esm/eliminateUnreferencedIdentifiers.js.map +1 -0
  22. package/dist/esm/index.d.ts +55 -2
  23. package/dist/esm/index.js +103 -8
  24. package/dist/esm/index.js.map +1 -1
  25. package/package.json +18 -1
  26. package/src/compilers.ts +420 -0
  27. package/src/constants.ts +1 -0
  28. package/src/eliminateUnreferencedIdentifiers.ts +287 -0
  29. package/src/index.ts +167 -8
  30. package/src/tests/index.test.ts +60 -0
  31. package/src/tests/shared/imported.tsx +11 -0
  32. package/src/tests/snapshots/function-declaration.tsx +10 -0
  33. package/src/tests/snapshots/function-declaration?split.tsx +29 -0
  34. package/src/tests/snapshots/imported.tsx +10 -0
  35. package/src/tests/snapshots/imported?split.tsx +5 -0
  36. package/src/tests/snapshots/inline.tsx +8 -0
  37. package/src/tests/snapshots/inline?split.tsx +18 -0
  38. package/src/tests/snapshots/random-number.tsx +11 -0
  39. package/src/tests/snapshots/random-number?split.tsx +27 -0
  40. package/src/tests/test-files/function-declaration.tsx +39 -0
  41. package/src/tests/test-files/imported.tsx +8 -0
  42. package/src/tests/test-files/inline.tsx +25 -0
  43. package/src/tests/test-files/random-number.tsx +83 -0
@@ -0,0 +1,420 @@
1
+ import * as t from '@babel/types'
2
+ import * as template from '@babel/template'
3
+ import * as babel from '@babel/core'
4
+ import { splitPrefix } from './constants'
5
+ import { eliminateUnreferencedIdentifiers } from './eliminateUnreferencedIdentifiers'
6
+
7
+ type SplitModulesById = Record<
8
+ string,
9
+ { id: string; node: t.FunctionExpression }
10
+ >
11
+
12
+ interface State {
13
+ filename: string
14
+ opts: {
15
+ minify: boolean
16
+ root: string
17
+ }
18
+ imported: Record<string, boolean>
19
+ refs: Set<any>
20
+ serverIndex: number
21
+ splitIndex: number
22
+ splitModulesById: SplitModulesById
23
+ }
24
+
25
+ export type CompileFn = (compileOpts: {
26
+ code: string
27
+ filename: string
28
+ getBabelConfig: () => { plugins: Array<any> }
29
+ }) => Promise<{
30
+ code: string
31
+ map: any
32
+ }>
33
+
34
+ export function makeCompile(makeOpts: { root: string }) {
35
+ return async (opts: {
36
+ code: string
37
+ filename: string
38
+ getBabelConfig: () => { plugins: Array<any> }
39
+ }): Promise<{
40
+ code: string
41
+ map: any
42
+ }> => {
43
+ const res = await babel.transform(opts.code, {
44
+ plugins: [
45
+ ['@babel/plugin-syntax-jsx', {}],
46
+ [
47
+ '@babel/plugin-syntax-typescript',
48
+ {
49
+ isTSX: true,
50
+ },
51
+ ],
52
+ ...opts.getBabelConfig().plugins,
53
+ ],
54
+ root: makeOpts.root,
55
+ filename: opts.filename,
56
+ sourceMaps: true,
57
+ })
58
+
59
+ if (res?.code) {
60
+ return {
61
+ code: res.code,
62
+ map: res.map,
63
+ }
64
+ }
65
+
66
+ return {
67
+ code: opts.code,
68
+ map: null,
69
+ }
70
+ }
71
+ }
72
+
73
+ export async function compileFile(opts: {
74
+ code: string
75
+ compile: CompileFn
76
+ filename: string
77
+ }) {
78
+ return await opts.compile({
79
+ code: opts.code,
80
+ filename: opts.filename,
81
+ getBabelConfig: () => ({
82
+ plugins: [
83
+ [
84
+ {
85
+ visitor: {
86
+ Program: {
87
+ enter(programPath: babel.NodePath<t.Program>, state: State) {
88
+ const splitUrl = `${splitPrefix}:${state.filename}?${splitPrefix}`
89
+
90
+ programPath.traverse(
91
+ {
92
+ CallExpression: (path) => {
93
+ if (path.node.callee.type === 'Identifier') {
94
+ if (
95
+ path.node.callee.name === 'createRoute' ||
96
+ path.node.callee.name === 'createFileRoute'
97
+ ) {
98
+ if (
99
+ path.parentPath.node.type === 'CallExpression'
100
+ ) {
101
+ const options = resolveIdentifier(
102
+ path,
103
+ path.parentPath.node.arguments[0],
104
+ )
105
+
106
+ let found = false
107
+
108
+ if (t.isObjectExpression(options)) {
109
+ options.properties.forEach((prop) => {
110
+ if (t.isObjectProperty(prop)) {
111
+ if (t.isIdentifier(prop.key)) {
112
+ if (prop.key.name === 'component') {
113
+ const value = prop.value
114
+
115
+ if (t.isIdentifier(value)) {
116
+ removeIdentifierLiteral(path, value)
117
+ }
118
+
119
+ // Prepend the import statement to the program along with the importer function
120
+
121
+ programPath.unshiftContainer('body', [
122
+ template.smart(
123
+ `import { lazyRouteComponent } from '@tanstack/react-router'`,
124
+ )() as t.Statement,
125
+ template.smart(
126
+ `const $$splitComponentImporter = () => import('${splitUrl}')`,
127
+ )() as t.Statement,
128
+ ])
129
+
130
+ prop.value = template.expression(
131
+ `lazyRouteComponent($$splitComponentImporter, 'component')`,
132
+ )() as any
133
+
134
+ programPath.pushContainer('body', [
135
+ template.smart(
136
+ `function DummyComponent() { return null }`,
137
+ )() as t.Statement,
138
+ ])
139
+
140
+ found = true
141
+ } else if (prop.key.name === 'loader') {
142
+ const value = prop.value
143
+
144
+ if (t.isIdentifier(value)) {
145
+ removeIdentifierLiteral(path, value)
146
+ }
147
+
148
+ // Prepend the import statement to the program along with the importer function
149
+
150
+ programPath.unshiftContainer('body', [
151
+ template.smart(
152
+ `import { lazyFn } from '@tanstack/react-router'`,
153
+ )() as t.Statement,
154
+ template.smart(
155
+ `const $$splitLoaderImporter = () => import('${splitUrl}')`,
156
+ )() as t.Statement,
157
+ ])
158
+
159
+ prop.value = template.expression(
160
+ `lazyFn($$splitLoaderImporter, 'loader')`,
161
+ )() as any
162
+
163
+ found = true
164
+ }
165
+ }
166
+ }
167
+ })
168
+ }
169
+
170
+ if (found as boolean) {
171
+ programPath.pushContainer('body', [
172
+ template.smart(
173
+ `function TSR_Dummy_Component() {}`,
174
+ )() as t.Statement,
175
+ ])
176
+ }
177
+ }
178
+ }
179
+ }
180
+ },
181
+ },
182
+ state,
183
+ )
184
+
185
+ eliminateUnreferencedIdentifiers(programPath)
186
+ },
187
+ },
188
+ },
189
+ },
190
+ {
191
+ root: process.cwd(),
192
+ minify: process.env.NODE_ENV === 'production',
193
+ },
194
+ ],
195
+ ].filter(Boolean),
196
+ }),
197
+ })
198
+ }
199
+
200
+ // Reusable function to get literal value or resolve variable to literal
201
+ function resolveIdentifier(path: any, node: any) {
202
+ if (t.isIdentifier(node)) {
203
+ const binding = path.scope.getBinding(node.name)
204
+ if (
205
+ binding
206
+ // && binding.kind === 'const'
207
+ ) {
208
+ const declarator = binding.path.node
209
+ if (t.isObjectExpression(declarator.init)) {
210
+ return declarator.init
211
+ } else if (t.isFunctionDeclaration(declarator.init)) {
212
+ return declarator.init
213
+ }
214
+ }
215
+ return undefined
216
+ }
217
+
218
+ return node
219
+ }
220
+
221
+ function removeIdentifierLiteral(path: any, node: any) {
222
+ if (t.isIdentifier(node)) {
223
+ const binding = path.scope.getBinding(node.name)
224
+ if (binding) {
225
+ binding.path.remove()
226
+ }
227
+ }
228
+ }
229
+
230
+ const splitNodeTypes = ['component', 'loader'] as const
231
+ type SplitNodeType = (typeof splitNodeTypes)[number]
232
+
233
+ export async function splitFile(opts: {
234
+ code: string
235
+ compile: CompileFn
236
+ filename: string
237
+ // ref: string
238
+ }) {
239
+ return await opts.compile({
240
+ code: opts.code,
241
+ filename: opts.filename,
242
+ getBabelConfig: () => ({
243
+ plugins: [
244
+ [
245
+ {
246
+ visitor: {
247
+ Program: {
248
+ enter(programPath: babel.NodePath<t.Program>, state: State) {
249
+ const splitNodesByType: Record<
250
+ SplitNodeType,
251
+ t.Node | undefined
252
+ > = {
253
+ component: undefined,
254
+ loader: undefined,
255
+ }
256
+
257
+ // Find the node
258
+ programPath.traverse(
259
+ {
260
+ CallExpression: (path) => {
261
+ if (path.node.callee.type === 'Identifier') {
262
+ if (path.node.callee.name === 'createFileRoute') {
263
+ if (
264
+ path.parentPath.node.type === 'CallExpression'
265
+ ) {
266
+ const options = resolveIdentifier(
267
+ path,
268
+ path.parentPath.node.arguments[0],
269
+ )
270
+
271
+ if (t.isObjectExpression(options)) {
272
+ options.properties.forEach((prop) => {
273
+ if (t.isObjectProperty(prop)) {
274
+ splitNodeTypes.forEach((type) => {
275
+ if (t.isIdentifier(prop.key)) {
276
+ if (prop.key.name === type) {
277
+ splitNodesByType[type] = prop.value
278
+ }
279
+ }
280
+ })
281
+ }
282
+ })
283
+
284
+ // Remove all of the options
285
+ options.properties = []
286
+ }
287
+ }
288
+ }
289
+ }
290
+ },
291
+ },
292
+ state,
293
+ )
294
+
295
+ splitNodeTypes.forEach((splitType) => {
296
+ let splitNode = splitNodesByType[splitType]
297
+
298
+ if (!splitNode) {
299
+ return
300
+ }
301
+
302
+ while (t.isIdentifier(splitNode)) {
303
+ const binding = programPath.scope.getBinding(
304
+ splitNode.name,
305
+ )
306
+ splitNode = binding?.path.node
307
+ }
308
+
309
+ // Add the node to the program
310
+ if (splitNode) {
311
+ if (t.isFunctionDeclaration(splitNode)) {
312
+ programPath.pushContainer(
313
+ 'body',
314
+ t.variableDeclaration('const', [
315
+ t.variableDeclarator(
316
+ t.identifier(splitType),
317
+ t.functionExpression(
318
+ splitNode.id || null, // Anonymize the function expression
319
+ splitNode.params,
320
+ splitNode.body,
321
+ splitNode.generator,
322
+ splitNode.async,
323
+ ),
324
+ ),
325
+ ]),
326
+ )
327
+ } else if (
328
+ t.isFunctionExpression(splitNode) ||
329
+ t.isArrowFunctionExpression(splitNode)
330
+ ) {
331
+ programPath.pushContainer(
332
+ 'body',
333
+ t.variableDeclaration('const', [
334
+ t.variableDeclarator(
335
+ t.identifier(splitType),
336
+ splitNode as any,
337
+ ),
338
+ ]),
339
+ )
340
+ } else if (t.isImportSpecifier(splitNode)) {
341
+ programPath.pushContainer(
342
+ 'body',
343
+ t.variableDeclaration('const', [
344
+ t.variableDeclarator(
345
+ t.identifier(splitType),
346
+ splitNode.local,
347
+ ),
348
+ ]),
349
+ )
350
+ } else {
351
+ console.log(splitNode)
352
+ throw new Error(
353
+ `Unexpected splitNode type: ${splitNode.type}`,
354
+ )
355
+ }
356
+ }
357
+
358
+ // If the splitNode exists at the top of the program
359
+ // then we need to remove that copy
360
+ programPath.node.body = programPath.node.body.filter(
361
+ (node) => {
362
+ // console.log(node)
363
+ return node !== splitNode
364
+ },
365
+ )
366
+
367
+ // Export the node
368
+ programPath.pushContainer('body', [
369
+ t.exportNamedDeclaration(null, [
370
+ t.exportSpecifier(
371
+ t.identifier(splitType),
372
+ t.identifier(splitType),
373
+ ),
374
+ ]),
375
+ ])
376
+ })
377
+
378
+ // convert exports to imports from the original file
379
+ programPath.traverse({
380
+ ExportNamedDeclaration(path) {
381
+ // e.g. export const x = 1 or export { x }
382
+ // becomes
383
+ // import { x } from '${opts.id}'
384
+
385
+ if (path.node.declaration) {
386
+ if (t.isVariableDeclaration(path.node.declaration)) {
387
+ path.replaceWith(
388
+ t.importDeclaration(
389
+ path.node.declaration.declarations.map((decl) =>
390
+ t.importSpecifier(
391
+ t.identifier((decl.id as any).name),
392
+ t.identifier((decl.id as any).name),
393
+ ),
394
+ ),
395
+ t.stringLiteral(
396
+ opts.filename.split(
397
+ `?${splitPrefix}`,
398
+ )[0] as string,
399
+ ),
400
+ ),
401
+ )
402
+ }
403
+ }
404
+ },
405
+ })
406
+
407
+ eliminateUnreferencedIdentifiers(programPath)
408
+ },
409
+ },
410
+ },
411
+ },
412
+ {
413
+ root: process.cwd(),
414
+ minify: process.env.NODE_ENV === 'production',
415
+ },
416
+ ],
417
+ ].filter(Boolean),
418
+ }),
419
+ })
420
+ }
@@ -0,0 +1 @@
1
+ export const splitPrefix = 'tsr-split'
@@ -0,0 +1,287 @@
1
+ // Copied from https://github.com/pcattori/vite-env-only/blob/main/src/dce.ts
2
+ // Adapted with some minor changes for the purpose of this project
3
+
4
+ import * as t from '@babel/types'
5
+ import type { types as BabelTypes } from '@babel/core'
6
+ import type { NodePath } from '@babel/traverse'
7
+
8
+ type IdentifierPath = NodePath<BabelTypes.Identifier>
9
+
10
+ // export function findReferencedIdentifiers(
11
+ // programPath: NodePath<BabelTypes.Program>,
12
+ // ): Set<IdentifierPath> {
13
+ // const refs = new Set<IdentifierPath>()
14
+
15
+ // function markFunction(
16
+ // path: NodePath<
17
+ // | BabelTypes.FunctionDeclaration
18
+ // | BabelTypes.FunctionExpression
19
+ // | BabelTypes.ArrowFunctionExpression
20
+ // >,
21
+ // ) {
22
+ // const ident = getIdentifier(path)
23
+ // if (ident?.node && isIdentifierReferenced(ident)) {
24
+ // refs.add(ident)
25
+ // }
26
+ // }
27
+
28
+ // function markImport(
29
+ // path: NodePath<
30
+ // | BabelTypes.ImportSpecifier
31
+ // | BabelTypes.ImportDefaultSpecifier
32
+ // | BabelTypes.ImportNamespaceSpecifier
33
+ // >,
34
+ // ) {
35
+ // const local = path.get('local')
36
+ // if (isIdentifierReferenced(local)) {
37
+ // refs.add(local)
38
+ // }
39
+ // }
40
+
41
+ // programPath.traverse({
42
+ // VariableDeclarator(path) {
43
+ // if (path.node.id.type === 'Identifier') {
44
+ // const local = path.get('id') as NodePath<BabelTypes.Identifier>
45
+ // if (isIdentifierReferenced(local)) {
46
+ // refs.add(local)
47
+ // }
48
+ // } else if (path.node.id.type === 'ObjectPattern') {
49
+ // const pattern = path.get('id') as NodePath<BabelTypes.ObjectPattern>
50
+
51
+ // const properties = pattern.get('properties')
52
+ // properties.forEach((p) => {
53
+ // const local = p.get(
54
+ // p.node.type === 'ObjectProperty'
55
+ // ? 'value'
56
+ // : p.node.type === 'RestElement'
57
+ // ? 'argument'
58
+ // : (function () {
59
+ // throw new Error('invariant')
60
+ // })(),
61
+ // ) as NodePath<BabelTypes.Identifier>
62
+ // if (isIdentifierReferenced(local)) {
63
+ // refs.add(local)
64
+ // }
65
+ // })
66
+ // } else if (path.node.id.type === 'ArrayPattern') {
67
+ // const pattern = path.get('id') as NodePath<BabelTypes.ArrayPattern>
68
+
69
+ // const elements = pattern.get('elements')
70
+ // elements.forEach((e) => {
71
+ // let local: NodePath<BabelTypes.Identifier>
72
+ // if (e.node?.type === 'Identifier') {
73
+ // local = e as NodePath<BabelTypes.Identifier>
74
+ // } else if (e.node?.type === 'RestElement') {
75
+ // local = e.get('argument') as NodePath<BabelTypes.Identifier>
76
+ // } else {
77
+ // return
78
+ // }
79
+
80
+ // if (isIdentifierReferenced(local)) {
81
+ // refs.add(local)
82
+ // }
83
+ // })
84
+ // }
85
+ // },
86
+
87
+ // FunctionDeclaration: markFunction,
88
+ // FunctionExpression: markFunction,
89
+ // ArrowFunctionExpression: markFunction,
90
+ // ImportSpecifier: markImport,
91
+ // ImportDefaultSpecifier: markImport,
92
+ // ImportNamespaceSpecifier: markImport,
93
+ // })
94
+ // return refs
95
+ // }
96
+
97
+ /**
98
+ * @param refs - If provided, only these identifiers will be considered for removal.
99
+ */
100
+ export const eliminateUnreferencedIdentifiers = (
101
+ programPath: NodePath<BabelTypes.Program>,
102
+ refs?: Set<IdentifierPath>,
103
+ ) => {
104
+ let referencesRemovedInThisPass: number
105
+
106
+ const shouldBeRemoved = (ident: IdentifierPath) => {
107
+ if (isIdentifierReferenced(ident)) return false
108
+ if (!refs) return true
109
+ return refs.has(ident)
110
+ }
111
+
112
+ const sweepFunction = (
113
+ path: NodePath<
114
+ | BabelTypes.FunctionDeclaration
115
+ | BabelTypes.FunctionExpression
116
+ | BabelTypes.ArrowFunctionExpression
117
+ >,
118
+ ) => {
119
+ const identifier = getIdentifier(path)
120
+ if (identifier?.node && shouldBeRemoved(identifier)) {
121
+ ++referencesRemovedInThisPass
122
+
123
+ if (
124
+ t.isAssignmentExpression(path.parentPath.node) ||
125
+ t.isVariableDeclarator(path.parentPath.node)
126
+ ) {
127
+ path.parentPath.remove()
128
+ } else {
129
+ path.remove()
130
+ }
131
+ }
132
+ }
133
+
134
+ const sweepImport = (
135
+ path: NodePath<
136
+ | BabelTypes.ImportSpecifier
137
+ | BabelTypes.ImportDefaultSpecifier
138
+ | BabelTypes.ImportNamespaceSpecifier
139
+ >,
140
+ ) => {
141
+ const local = path.get('local')
142
+ if (shouldBeRemoved(local)) {
143
+ ++referencesRemovedInThisPass
144
+ path.remove()
145
+ if (
146
+ (path.parent as BabelTypes.ImportDeclaration).specifiers.length === 0
147
+ ) {
148
+ path.parentPath.remove()
149
+ }
150
+ }
151
+ }
152
+
153
+ // Traverse again to remove unused references. This happens at least once,
154
+ // then repeats until no more references are removed.
155
+ do {
156
+ referencesRemovedInThisPass = 0
157
+
158
+ programPath.scope.crawl()
159
+
160
+ programPath.traverse({
161
+ VariableDeclarator(path) {
162
+ if (path.node.id.type === 'Identifier') {
163
+ const local = path.get('id') as NodePath<BabelTypes.Identifier>
164
+ if (shouldBeRemoved(local)) {
165
+ ++referencesRemovedInThisPass
166
+ path.remove()
167
+ }
168
+ } else if (path.node.id.type === 'ObjectPattern') {
169
+ const pattern = path.get('id') as NodePath<BabelTypes.ObjectPattern>
170
+
171
+ const beforeCount = referencesRemovedInThisPass
172
+ const properties = pattern.get('properties')
173
+ properties.forEach((property) => {
174
+ const local = property.get(
175
+ property.node.type === 'ObjectProperty'
176
+ ? 'value'
177
+ : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
178
+ property.node.type === 'RestElement'
179
+ ? 'argument'
180
+ : (function () {
181
+ throw new Error('invariant')
182
+ })(),
183
+ ) as NodePath<BabelTypes.Identifier>
184
+
185
+ if (shouldBeRemoved(local)) {
186
+ ++referencesRemovedInThisPass
187
+ property.remove()
188
+ }
189
+ })
190
+
191
+ if (
192
+ beforeCount !== referencesRemovedInThisPass &&
193
+ pattern.get('properties').length < 1
194
+ ) {
195
+ path.remove()
196
+ }
197
+ } else if (path.node.id.type === 'ArrayPattern') {
198
+ const pattern = path.get('id') as NodePath<BabelTypes.ArrayPattern>
199
+
200
+ const beforeCount = referencesRemovedInThisPass
201
+ const elements = pattern.get('elements')
202
+ elements.forEach((e) => {
203
+ let local: NodePath<BabelTypes.Identifier>
204
+ if (e.node?.type === 'Identifier') {
205
+ local = e as NodePath<BabelTypes.Identifier>
206
+ } else if (e.node?.type === 'RestElement') {
207
+ local = e.get('argument') as NodePath<BabelTypes.Identifier>
208
+ } else {
209
+ return
210
+ }
211
+
212
+ if (shouldBeRemoved(local)) {
213
+ ++referencesRemovedInThisPass
214
+ e.remove()
215
+ }
216
+ })
217
+
218
+ if (
219
+ beforeCount !== referencesRemovedInThisPass &&
220
+ pattern.get('elements').length < 1
221
+ ) {
222
+ path.remove()
223
+ }
224
+ }
225
+ },
226
+ FunctionDeclaration: sweepFunction,
227
+ FunctionExpression: sweepFunction,
228
+ ArrowFunctionExpression: sweepFunction,
229
+ ImportSpecifier: sweepImport,
230
+ ImportDefaultSpecifier: sweepImport,
231
+ ImportNamespaceSpecifier: sweepImport,
232
+ })
233
+ } while (referencesRemovedInThisPass)
234
+ }
235
+
236
+ function getIdentifier(
237
+ path: NodePath<
238
+ | BabelTypes.FunctionDeclaration
239
+ | BabelTypes.FunctionExpression
240
+ | BabelTypes.ArrowFunctionExpression
241
+ >,
242
+ ): NodePath<BabelTypes.Identifier> | null {
243
+ const parentPath = path.parentPath
244
+ if (parentPath.type === 'VariableDeclarator') {
245
+ const variablePath = parentPath as NodePath<BabelTypes.VariableDeclarator>
246
+ const name = variablePath.get('id')
247
+ return name.node.type === 'Identifier'
248
+ ? (name as NodePath<BabelTypes.Identifier>)
249
+ : null
250
+ }
251
+
252
+ if (parentPath.type === 'AssignmentExpression') {
253
+ const variablePath = parentPath as NodePath<BabelTypes.AssignmentExpression>
254
+ const name = variablePath.get('left')
255
+ return name.node.type === 'Identifier'
256
+ ? (name as NodePath<BabelTypes.Identifier>)
257
+ : null
258
+ }
259
+
260
+ if (path.node.type === 'ArrowFunctionExpression') {
261
+ return null
262
+ }
263
+
264
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
265
+ return path.node.id && path.node.id.type === 'Identifier'
266
+ ? (path.get('id') as NodePath<BabelTypes.Identifier>)
267
+ : null
268
+ }
269
+
270
+ function isIdentifierReferenced(
271
+ ident: NodePath<BabelTypes.Identifier>,
272
+ ): boolean {
273
+ const binding = ident.scope.getBinding(ident.node.name)
274
+ if (binding?.referenced) {
275
+ // Functions can reference themselves, so we need to check if there's a
276
+ // binding outside the function scope or not.
277
+ if (binding.path.type === 'FunctionDeclaration') {
278
+ return !binding.constantViolations
279
+ .concat(binding.referencePaths)
280
+ // Check that every reference is contained within the function:
281
+ .every((ref) => ref.findParent((parent) => parent === binding.path))
282
+ }
283
+
284
+ return true
285
+ }
286
+ return false
287
+ }