kea-typegen 3.6.8 → 3.7.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.
package/src/typegen.ts CHANGED
@@ -6,6 +6,7 @@ import { AppOptions } from './types'
6
6
  import { Program } from 'typescript'
7
7
  import { version } from '../package.json'
8
8
  import { restoreCachedTypes } from './cache'
9
+ import { createWatchChangeTracker, planWatchTypegenPass, WatchFileChange, WatchTypegenPlan } from './watch'
9
10
 
10
11
  // The undocumented defaultMaximumTruncationLength setting determines at what point printed types are truncated in versions less than 5.
11
12
  // In kea-typegen output, we NEVER want the types truncated, as that results in a syntax error –
@@ -25,14 +26,18 @@ export async function runTypeGen(appOptions: AppOptions) {
25
26
 
26
27
  if (appOptions.sourceFilePath) {
27
28
  log(`❇️ Loading file: ${appOptions.sourceFilePath}`)
29
+ const compilerOptions = appOptions.tsConfigPath
30
+ ? loadTsConfig(appOptions.tsConfigPath, log).options
31
+ : {
32
+ target: ts.ScriptTarget.ES5,
33
+ module: ts.ModuleKind.CommonJS,
34
+ noEmit: true,
35
+ noErrorTruncation: true,
36
+ }
37
+
28
38
  resetProgram = () => {
29
- program = replaceProgram(() =>
30
- ts.createProgram([appOptions.sourceFilePath], {
31
- target: ts.ScriptTarget.ES5,
32
- module: ts.ModuleKind.CommonJS,
33
- noEmit: true,
34
- noErrorTruncation: true,
35
- }),
39
+ program = replaceProgram(
40
+ () => ts.createProgram([appOptions.sourceFilePath], compilerOptions),
36
41
  (nextProgram) => {
37
42
  program = nextProgram
38
43
  },
@@ -40,22 +45,19 @@ export async function runTypeGen(appOptions: AppOptions) {
40
45
  }
41
46
  resetProgram()
42
47
  } else if (appOptions.tsConfigPath) {
43
- log(`🥚 TypeScript Config: ${appOptions.tsConfigPath}`)
44
-
45
- const configFile = ts.readJsonConfigFile(appOptions.tsConfigPath, ts.sys.readFile)
46
- const rootFolder = path.dirname(appOptions.tsConfigPath)
47
- const compilerOptions = ts.parseJsonSourceFileConfigFileContent(configFile, ts.sys, rootFolder)
48
+ const compilerOptions = loadTsConfig(appOptions.tsConfigPath, log)
48
49
 
49
50
  if (appOptions.watch) {
50
51
  // We don't emit JavaScript files in typegen watch mode, so the semantic-only
51
52
  // builder is enough and avoids extra emit-related work on every rebuild.
52
53
  const createProgram = ts.createSemanticDiagnosticsBuilderProgram
54
+ const watchChangeTracker = createWatchChangeTracker(ts.sys)
53
55
 
54
56
  const host = ts.createWatchCompilerHost(
55
57
  appOptions.tsConfigPath,
56
58
  compilerOptions.options,
57
59
  {
58
- ...ts.sys,
60
+ ...watchChangeTracker.system,
59
61
  writeFile(_path: string, _data: string, _writeByteOrderMark?: boolean) {
60
62
  // skip emit
61
63
  // https://github.com/microsoft/TypeScript/issues/32385
@@ -92,6 +94,7 @@ export async function runTypeGen(appOptions: AppOptions) {
92
94
 
93
95
  const origPostProgramCreate = host.afterProgramCreate
94
96
  let scheduledProgram: Program | undefined
97
+ let scheduledChanges: WatchFileChange[] = []
95
98
  let runningTypegen = false
96
99
 
97
100
  const runScheduledTypegen = async () => {
@@ -104,10 +107,16 @@ export async function runTypeGen(appOptions: AppOptions) {
104
107
  try {
105
108
  while (scheduledProgram) {
106
109
  const nextProgram = scheduledProgram
110
+ const nextChanges = scheduledChanges
107
111
  scheduledProgram = undefined
112
+ scheduledChanges = []
108
113
 
109
114
  try {
110
- await goThroughAllTheFiles(nextProgram, appOptions)
115
+ await goThroughWatchTypegenPlan(
116
+ nextProgram,
117
+ appOptions,
118
+ planWatchTypegenPass(nextProgram, nextChanges),
119
+ )
111
120
  } catch (error) {
112
121
  console.error('⛔ Error running kea-typegen in watch mode')
113
122
  console.error(error)
@@ -124,8 +133,10 @@ export async function runTypeGen(appOptions: AppOptions) {
124
133
 
125
134
  host.afterProgramCreate = (prog) => {
126
135
  program = prog.getProgram()
136
+ const changes = watchChangeTracker.consume()
127
137
  origPostProgramCreate?.(prog)
128
138
  scheduledProgram = program
139
+ scheduledChanges.push(...changes)
129
140
  void runScheduledTypegen()
130
141
  }
131
142
 
@@ -173,7 +184,52 @@ export async function runTypeGen(appOptions: AppOptions) {
173
184
  return response
174
185
  }
175
186
 
176
- if (program && !appOptions.watch && !appOptions.sourceFilePath) {
187
+ async function goThroughWatchTypegenPlan(
188
+ program,
189
+ appOptions,
190
+ plan: WatchTypegenPlan,
191
+ ): Promise<{ filesToWrite: number; writtenFiles: number; filesToModify: number }> {
192
+ if (plan.kind === 'skip') {
193
+ if (appOptions.verbose) {
194
+ appOptions.log(`⏭️ Skipping typegen watch pass: ${plan.reason}`)
195
+ }
196
+ return { filesToWrite: 0, writtenFiles: 0, filesToModify: 0 }
197
+ }
198
+
199
+ if (plan.kind === 'full') {
200
+ if (appOptions.verbose) {
201
+ appOptions.log(`🔎 Running full typegen watch pass: ${plan.reason}`)
202
+ }
203
+ return await goThroughAllTheFiles(program, appOptions)
204
+ }
205
+
206
+ if (appOptions.verbose) {
207
+ appOptions.log(`🎯 Running incremental typegen watch pass: ${plan.reason}`)
208
+ }
209
+
210
+ const response = { filesToWrite: 0, writtenFiles: 0, filesToModify: 0 }
211
+
212
+ for (const sourceFilePath of plan.sourceFilePaths) {
213
+ const fileResponse = await goThroughAllTheFiles(program, {
214
+ ...appOptions,
215
+ delete: false,
216
+ sourceFilePath,
217
+ })
218
+
219
+ response.filesToWrite += fileResponse.filesToWrite
220
+ response.writtenFiles += fileResponse.writtenFiles
221
+ response.filesToModify += fileResponse.filesToModify
222
+ }
223
+
224
+ return response
225
+ }
226
+
227
+ if (program && !appOptions.watch && appOptions.sourceFilePath) {
228
+ await goThroughAllTheFiles(program, appOptions)
229
+ if (appOptions.write) {
230
+ log(`👋 Finished writing files! Exiting.`)
231
+ }
232
+ } else if (program && !appOptions.watch) {
177
233
  if (appOptions.write) {
178
234
  if (restoreCachedTypes(program, appOptions, log)) {
179
235
  resetProgram()
@@ -202,6 +258,13 @@ export async function runTypeGen(appOptions: AppOptions) {
202
258
  }
203
259
  }
204
260
 
261
+ function loadTsConfig(tsConfigPath: string, log: AppOptions['log']): ts.ParsedCommandLine {
262
+ log(`🥚 TypeScript Config: ${tsConfigPath}`)
263
+ const configFile = ts.readJsonConfigFile(tsConfigPath, ts.sys.readFile)
264
+ const rootFolder = path.dirname(tsConfigPath)
265
+ return ts.parseJsonSourceFileConfigFileContent(configFile, ts.sys, rootFolder)
266
+ }
267
+
205
268
  export function replaceProgram(createProgram: () => Program, setProgram: (program?: Program) => void): Program {
206
269
  setProgram(undefined)
207
270
  const nextProgram = createProgram()
package/src/types.ts CHANGED
@@ -82,6 +82,8 @@ export interface AppOptions {
82
82
  showTsErrors?: boolean
83
83
  /** Cache generated logic files into .typegen, use them if generating a logic type for the first time */
84
84
  useCache?: boolean
85
+ /** Skip Prettier formatting while generating logic type files */
86
+ prettier?: boolean
85
87
 
86
88
  log: (message: string) => void
87
89
  }
@@ -25,7 +25,7 @@ import { visitEvents } from './visitEvents'
25
25
  import { visitDefaults } from './visitDefaults'
26
26
  import { visitSharedListeners } from './visitSharedListeners'
27
27
  import { cloneNode } from 'ts-clone-node'
28
- import {NodeBuilderFlags} from "typescript";
28
+ import { NodeBuilderFlags } from 'typescript'
29
29
 
30
30
  const visitFunctions = {
31
31
  actions: visitActions,
@@ -44,20 +44,52 @@ const visitFunctions = {
44
44
  windowValues: visitWindowValues,
45
45
  }
46
46
 
47
+ // Cheap text prefilters so we only do expensive AST + type-checker work on files
48
+ // that can actually contain the thing we're looking for. On large projects most
49
+ // source files contain no Kea logic at all, and walking every node (and resolving
50
+ // a symbol for every identifier via the checker) for them dominates runtime.
51
+ //
52
+ // `kea` is never aliased on import in practice, so a `kea(` / `kea<` text match is a
53
+ // safe-enough gate for the Kea-call pass. If a project does alias `kea`, set
54
+ // KEA_TYPEGEN_NO_PREFILTER=1 to fall back to scanning every file.
55
+ const KEA_CALL_REGEX = /\bkea\s*[<(]/
56
+ const prefilterDisabled = process.env.KEA_TYPEGEN_NO_PREFILTER === '1'
57
+
58
+ function fileMightContainKeaCall(sourceFile: ts.SourceFile): boolean {
59
+ return prefilterDisabled || KEA_CALL_REGEX.test(sourceFile.text)
60
+ }
61
+
62
+ function fileMightContainResetContext(sourceFile: ts.SourceFile): boolean {
63
+ return prefilterDisabled || sourceFile.text.includes('resetContext')
64
+ }
65
+
47
66
  export function visitProgram(program: ts.Program, appOptions?: AppOptions): ParsedLogic[] {
48
67
  const checker = program.getTypeChecker()
49
68
  const parsedLogics: ParsedLogic[] = []
50
69
  const pluginModules: PluginModule[] = []
51
70
  const typeBuilderModules: TypeBuilderModule[] = []
71
+ const sourceFilePath = appOptions?.sourceFilePath ? path.resolve(appOptions.sourceFilePath) : undefined
52
72
 
53
73
  for (const sourceFile of program.getSourceFiles()) {
54
- if (!sourceFile.isDeclarationFile && !sourceFile.fileName.endsWith('Type.ts')) {
74
+ if (
75
+ !sourceFile.isDeclarationFile &&
76
+ !sourceFile.fileName.endsWith('Type.ts') &&
77
+ fileMightContainResetContext(sourceFile)
78
+ ) {
55
79
  ts.forEachChild(sourceFile, visitResetContext(checker, pluginModules))
56
80
  }
57
81
  }
58
82
 
59
83
  for (const sourceFile of program.getSourceFiles()) {
60
- if (!sourceFile.isDeclarationFile && !sourceFile.fileName.endsWith('Type.ts')) {
84
+ if (sourceFilePath && path.resolve(sourceFile.fileName) !== sourceFilePath) {
85
+ continue
86
+ }
87
+
88
+ if (
89
+ !sourceFile.isDeclarationFile &&
90
+ !sourceFile.fileName.endsWith('Type.ts') &&
91
+ fileMightContainKeaCall(sourceFile)
92
+ ) {
61
93
  if (appOptions?.verbose) {
62
94
  appOptions.log(`👀 Visiting: ${path.relative(process.cwd(), sourceFile.fileName)}`)
63
95
  }
@@ -245,7 +277,6 @@ export function visitKeaCalls(
245
277
  const calls: {
246
278
  name: string
247
279
  type: ts.Type
248
- typeNode: ts.TypeNode | null
249
280
  expression: ts.Expression
250
281
  typeBuilders: PluginModule[]
251
282
  }[] = []
@@ -261,8 +292,7 @@ export function visitKeaCalls(
261
292
  }
262
293
  const name = symbol.getName()
263
294
  const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration)
264
- const typeNode = type ? checker.typeToTypeNode(type, undefined, NodeBuilderFlags.NoTruncation) : null
265
- calls.push({ name, type, typeNode, expression: inputProperty.initializer, typeBuilders: [] })
295
+ calls.push({ name, type, expression: inputProperty.initializer, typeBuilders: [] })
266
296
  }
267
297
  } else if (ts.isArrayLiteralExpression(input)) {
268
298
  for (const callExpression of input.elements) {
@@ -273,7 +303,6 @@ export function visitKeaCalls(
273
303
  const builderName = callExpression.expression.getText()
274
304
  const argument = callExpression.arguments[0]
275
305
  const type = checker.getTypeAtLocation(argument)
276
- const typeNode = type ? checker.typeToTypeNode(type, undefined, NodeBuilderFlags.NoTruncation) : null
277
306
 
278
307
  const identifier = callExpression.expression
279
308
  const symbol = checker.getSymbolAtLocation(identifier)
@@ -348,14 +377,13 @@ export function visitKeaCalls(
348
377
  calls.push({
349
378
  name: builderName,
350
379
  type,
351
- typeNode,
352
380
  expression: callExpression.arguments[0],
353
381
  typeBuilders: typeBuilders,
354
382
  })
355
383
  }
356
384
  }
357
385
 
358
- for (let { name, type, typeNode, expression, typeBuilders } of calls) {
386
+ for (let { name, type, expression, typeBuilders } of calls) {
359
387
  if (name === 'path' || name === 'logicPath') {
360
388
  parsedLogic.hasPathInLogic = true
361
389
  }
@@ -363,18 +391,31 @@ export function visitKeaCalls(
363
391
  parsedLogic.hasKeyInLogic = true
364
392
  }
365
393
 
366
- if (typeNode && ts.isFunctionTypeNode(typeNode)) {
367
- type = type.getCallSignatures()[0].getReturnType()
368
- typeNode = type ? checker.typeToTypeNode(type, undefined, NodeBuilderFlags.NoTruncation) : null
394
+ // Equivalent to the previous `ts.isFunctionTypeNode(typeNode)` check, but without
395
+ // eagerly building a typeNode: a pure function type has call signatures and no
396
+ // members, so we unwrap it to its return type before visiting/printing.
397
+ const callSignatures = type.getCallSignatures()
398
+ if (callSignatures.length > 0 && type.getProperties().length === 0) {
399
+ type = callSignatures[0].getReturnType()
369
400
  }
370
401
 
371
402
  visitFunctions[name]?.(parsedLogic, type, expression)
372
403
 
404
+ let typeNode: ts.TypeNode | null | undefined = undefined
405
+ const getTypeNode = (): ts.TypeNode | null => {
406
+ if (typeNode === undefined) {
407
+ typeNode = type ? checker.typeToTypeNode(type, undefined, NodeBuilderFlags.NoTruncation) : null
408
+ }
409
+ return typeNode
410
+ }
411
+
373
412
  const visitKeaPropertyArguments: VisitKeaPropertyArguments = {
374
413
  name,
375
414
  appOptions,
376
415
  type,
377
- typeNode,
416
+ get typeNode() {
417
+ return getTypeNode()
418
+ },
378
419
  parsedLogic,
379
420
  node: expression,
380
421
  checker,
package/src/watch.ts ADDED
@@ -0,0 +1,261 @@
1
+ import * as fs from 'fs'
2
+ import * as path from 'path'
3
+ import * as ts from 'typescript'
4
+ import { getFilenameForImportDeclaration } from './utils'
5
+
6
+ const KEA_CALL_REGEX = /\bkea\s*[<(]/
7
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mjsx']
8
+ const TYPEGEN_FILE_REGEX = /(^|\.|\/)typegen\.[tj]s$/
9
+ const MAX_INCREMENTAL_CHANGED_FILES = 50
10
+
11
+ export interface WatchFileChange {
12
+ fileName: string
13
+ eventKind: ts.FileWatcherEventKind
14
+ }
15
+
16
+ export type WatchTypegenPlan =
17
+ | { kind: 'full'; reason: string }
18
+ | { kind: 'files'; sourceFilePaths: string[]; reason: string }
19
+ | { kind: 'skip'; reason: string }
20
+
21
+ export interface WatchChangeTracker {
22
+ system: ts.System
23
+ consume(): WatchFileChange[]
24
+ }
25
+
26
+ export function createWatchChangeTracker(baseSystem: ts.System): WatchChangeTracker {
27
+ const changes = new Map<string, WatchFileChange>()
28
+
29
+ const track = (fileName: string, eventKind: ts.FileWatcherEventKind): void => {
30
+ changes.set(path.resolve(fileName), { fileName: path.resolve(fileName), eventKind })
31
+ }
32
+
33
+ const system: ts.System = {
34
+ ...baseSystem,
35
+ watchFile(fileName, callback, pollingInterval, options) {
36
+ return baseSystem.watchFile!(
37
+ fileName,
38
+ (changedFileName, eventKind, modifiedTime) => {
39
+ track(changedFileName, eventKind)
40
+ callback(changedFileName, eventKind, modifiedTime)
41
+ },
42
+ pollingInterval,
43
+ options,
44
+ )
45
+ },
46
+ watchDirectory(fileName, callback, recursive, options) {
47
+ return baseSystem.watchDirectory!(
48
+ fileName,
49
+ (changedFileName) => {
50
+ track(changedFileName, ts.FileWatcherEventKind.Created)
51
+ callback(changedFileName)
52
+ },
53
+ recursive,
54
+ options,
55
+ )
56
+ },
57
+ }
58
+
59
+ return {
60
+ system,
61
+ consume() {
62
+ const currentChanges = [...changes.values()]
63
+ changes.clear()
64
+ return currentChanges
65
+ },
66
+ }
67
+ }
68
+
69
+ export function planWatchTypegenPass(program: ts.Program, changes: WatchFileChange[]): WatchTypegenPlan {
70
+ if (changes.length === 0) {
71
+ return { kind: 'full', reason: 'initial watch pass' }
72
+ }
73
+
74
+ const normalizedChanges = dedupeChanges(changes)
75
+
76
+ if (normalizedChanges.length > MAX_INCREMENTAL_CHANGED_FILES) {
77
+ return { kind: 'full', reason: `${normalizedChanges.length} files changed` }
78
+ }
79
+
80
+ if (normalizedChanges.some((change) => change.eventKind === ts.FileWatcherEventKind.Deleted)) {
81
+ return { kind: 'full', reason: 'deleted file detected' }
82
+ }
83
+
84
+ const sourceFilesByPath = getSourceFilesByPath(program)
85
+ const reverseDependencies = buildReverseDependencyMap(program)
86
+ const queue: string[] = []
87
+
88
+ for (const change of normalizedChanges) {
89
+ const changedPath = path.resolve(change.fileName)
90
+
91
+ if (isTypegenModuleFile(changedPath)) {
92
+ return { kind: 'full', reason: `typegen module changed: ${path.relative(process.cwd(), changedPath)}` }
93
+ }
94
+
95
+ const sourceFile = sourceFilesByPath.get(changedPath)
96
+
97
+ if (sourceFile?.text.includes('resetContext')) {
98
+ return { kind: 'full', reason: `resetContext changed: ${path.relative(process.cwd(), changedPath)}` }
99
+ }
100
+
101
+ if (isGeneratedTypeFile(changedPath)) {
102
+ queue.push(...sourcePathsForTypeFile(changedPath, sourceFilesByPath))
103
+ queue.push(changedPath)
104
+ continue
105
+ }
106
+
107
+ if (sourceFile) {
108
+ if (sourceFile.isDeclarationFile) {
109
+ return {
110
+ kind: 'full',
111
+ reason: `declaration file changed: ${path.relative(process.cwd(), changedPath)}`,
112
+ }
113
+ }
114
+
115
+ queue.push(changedPath)
116
+ continue
117
+ }
118
+
119
+ if (isSourceLikeFile(changedPath) && fs.existsSync(changedPath)) {
120
+ return {
121
+ kind: 'full',
122
+ reason: `new source file outside program: ${path.relative(process.cwd(), changedPath)}`,
123
+ }
124
+ }
125
+ }
126
+
127
+ const affectedLogicFiles = collectAffectedLogicFiles(queue, sourceFilesByPath, reverseDependencies)
128
+
129
+ if (affectedLogicFiles.length === 0) {
130
+ return { kind: 'skip', reason: 'no affected Kea logic files' }
131
+ }
132
+
133
+ return {
134
+ kind: 'files',
135
+ sourceFilePaths: affectedLogicFiles,
136
+ reason: `${affectedLogicFiles.length} affected Kea logic file${affectedLogicFiles.length === 1 ? '' : 's'}`,
137
+ }
138
+ }
139
+
140
+ function dedupeChanges(changes: WatchFileChange[]): WatchFileChange[] {
141
+ const deduped = new Map<string, WatchFileChange>()
142
+
143
+ for (const change of changes) {
144
+ deduped.set(path.resolve(change.fileName), { ...change, fileName: path.resolve(change.fileName) })
145
+ }
146
+
147
+ return [...deduped.values()]
148
+ }
149
+
150
+ function getSourceFilesByPath(program: ts.Program): Map<string, ts.SourceFile> {
151
+ const sourceFilesByPath = new Map<string, ts.SourceFile>()
152
+
153
+ for (const sourceFile of program.getSourceFiles()) {
154
+ sourceFilesByPath.set(path.resolve(sourceFile.fileName), sourceFile)
155
+ }
156
+
157
+ return sourceFilesByPath
158
+ }
159
+
160
+ function buildReverseDependencyMap(program: ts.Program): Map<string, Set<string>> {
161
+ const checker = program.getTypeChecker()
162
+ const reverseDependencies = new Map<string, Set<string>>()
163
+
164
+ for (const sourceFile of program.getSourceFiles()) {
165
+ if (sourceFile.isDeclarationFile) {
166
+ continue
167
+ }
168
+
169
+ const importerPath = path.resolve(sourceFile.fileName)
170
+
171
+ ts.forEachChild(sourceFile, function visit(node) {
172
+ if (ts.isImportDeclaration(node)) {
173
+ const importedFileName = getFilenameForImportDeclaration(checker, node)
174
+
175
+ if (importedFileName) {
176
+ const importedPath = path.resolve(importedFileName)
177
+ let importers = reverseDependencies.get(importedPath)
178
+
179
+ if (!importers) {
180
+ importers = new Set()
181
+ reverseDependencies.set(importedPath, importers)
182
+ }
183
+
184
+ importers.add(importerPath)
185
+ }
186
+ }
187
+
188
+ ts.forEachChild(node, visit)
189
+ })
190
+ }
191
+
192
+ return reverseDependencies
193
+ }
194
+
195
+ function collectAffectedLogicFiles(
196
+ startPaths: string[],
197
+ sourceFilesByPath: Map<string, ts.SourceFile>,
198
+ reverseDependencies: Map<string, Set<string>>,
199
+ ): string[] {
200
+ const visited = new Set<string>()
201
+ const queue = startPaths.map((filePath) => path.resolve(filePath))
202
+ const affectedLogicFiles = new Set<string>()
203
+
204
+ while (queue.length > 0) {
205
+ const currentPath = queue.shift()!
206
+
207
+ if (visited.has(currentPath)) {
208
+ continue
209
+ }
210
+
211
+ visited.add(currentPath)
212
+
213
+ const sourceFile = sourceFilesByPath.get(currentPath)
214
+ if (
215
+ sourceFile &&
216
+ !sourceFile.isDeclarationFile &&
217
+ !isGeneratedTypeFile(currentPath) &&
218
+ sourceFileMightContainKeaCall(sourceFile)
219
+ ) {
220
+ affectedLogicFiles.add(currentPath)
221
+ }
222
+
223
+ if (isGeneratedTypeFile(currentPath)) {
224
+ queue.push(...sourcePathsForTypeFile(currentPath, sourceFilesByPath))
225
+ }
226
+
227
+ for (const importerPath of reverseDependencies.get(currentPath) ?? []) {
228
+ queue.push(importerPath)
229
+ }
230
+ }
231
+
232
+ return [...affectedLogicFiles].sort()
233
+ }
234
+
235
+ function sourcePathsForTypeFile(typeFilePath: string, sourceFilesByPath: Map<string, ts.SourceFile>): string[] {
236
+ if (!isGeneratedTypeFile(typeFilePath)) {
237
+ return []
238
+ }
239
+
240
+ const sourceBasePath = typeFilePath.slice(0, -'Type.ts'.length)
241
+
242
+ return SOURCE_EXTENSIONS.map((extension) => `${sourceBasePath}${extension}`).filter((sourcePath) =>
243
+ sourceFilesByPath.has(path.resolve(sourcePath)),
244
+ )
245
+ }
246
+
247
+ function isGeneratedTypeFile(filePath: string): boolean {
248
+ return filePath.endsWith('Type.ts')
249
+ }
250
+
251
+ function isSourceLikeFile(filePath: string): boolean {
252
+ return SOURCE_EXTENSIONS.some((extension) => filePath.endsWith(extension))
253
+ }
254
+
255
+ function isTypegenModuleFile(filePath: string): boolean {
256
+ return TYPEGEN_FILE_REGEX.test(filePath.split(path.sep).join('/'))
257
+ }
258
+
259
+ function sourceFileMightContainKeaCall(sourceFile: ts.SourceFile): boolean {
260
+ return KEA_CALL_REGEX.test(sourceFile.text)
261
+ }