kea-typegen 3.7.0 → 3.8.0

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 (52) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/package.json +3 -1
  3. package/dist/src/__tests__/inline.d.ts +1 -0
  4. package/dist/src/__tests__/inline.js +224 -0
  5. package/dist/src/__tests__/inline.js.map +1 -0
  6. package/dist/src/__tests__/watch.js +178 -0
  7. package/dist/src/__tests__/watch.js.map +1 -1
  8. package/dist/src/cli/typegen.js +4 -0
  9. package/dist/src/cli/typegen.js.map +1 -1
  10. package/dist/src/print/print.d.ts +6 -0
  11. package/dist/src/print/print.js +59 -42
  12. package/dist/src/print/print.js.map +1 -1
  13. package/dist/src/typegen.js +38 -2
  14. package/dist/src/typegen.js.map +1 -1
  15. package/dist/src/types.d.ts +1 -0
  16. package/dist/src/utils.d.ts +4 -0
  17. package/dist/src/utils.js +55 -0
  18. package/dist/src/utils.js.map +1 -1
  19. package/dist/src/visit/visit.js +6 -0
  20. package/dist/src/visit/visit.js.map +1 -1
  21. package/dist/src/watch.d.ts +22 -0
  22. package/dist/src/watch.js +183 -0
  23. package/dist/src/watch.js.map +1 -0
  24. package/dist/src/write/writeInlineLogicTypes.d.ts +8 -0
  25. package/dist/src/write/writeInlineLogicTypes.js +236 -0
  26. package/dist/src/write/writeInlineLogicTypes.js.map +1 -0
  27. package/dist/src/write/writeTypeImports.d.ts +8 -0
  28. package/dist/src/write/writeTypeImports.js +3 -0
  29. package/dist/src/write/writeTypeImports.js.map +1 -1
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +3 -1
  32. package/samples/manualTypedLogic.ts +20 -0
  33. package/samples-inline/counterLogic.ts +36 -0
  34. package/samples-inline/manualLogic.ts +20 -0
  35. package/samples-inline/profileLogic.ts +30 -0
  36. package/samples-inline/searchLogic.ts +30 -0
  37. package/samples-inline/tsconfig.json +17 -0
  38. package/samples-inline/types.ts +5 -0
  39. package/src/__tests__/inline.ts +266 -0
  40. package/src/__tests__/watch.ts +259 -32
  41. package/src/cli/typegen.ts +4 -0
  42. package/src/print/print.ts +102 -57
  43. package/src/test-support/watch-mode-smoke.js +39 -4
  44. package/src/test-support/write-mode-smoke.js +6 -1
  45. package/src/typegen.ts +53 -2
  46. package/src/types.ts +2 -0
  47. package/src/utils.ts +81 -0
  48. package/src/visit/visit.ts +14 -0
  49. package/src/watch.ts +261 -0
  50. package/src/write/writeInlineLogicTypes.ts +305 -0
  51. package/src/write/writeTypeImports.ts +4 -4
  52. package/tsconfig.json +1 -0
@@ -36,6 +36,7 @@ import { printSharedListeners } from './printSharedListeners'
36
36
  import { printListeners } from './printListeners'
37
37
  import { writePaths } from '../write/writePaths'
38
38
  import { writeTypeImports } from '../write/writeTypeImports'
39
+ import { writeInlineLogicTypes } from '../write/writeInlineLogicTypes'
39
40
  import { printInternalExtraInput } from './printInternalExtraInput'
40
41
  import { convertToBuilders } from '../write/convertToBuilders'
41
42
  import { cacheWrittenFile } from '../cache'
@@ -78,8 +79,10 @@ export async function printToFiles(
78
79
  }
79
80
  groupedByFile[parsedLogic.fileName].push(parsedLogic)
80
81
 
81
- // create the Nodes and gather referenced types
82
- printLogicType(parsedLogic, appOptions)
82
+ if (!appOptions.inline) {
83
+ // create the Nodes and gather referenced types
84
+ printLogicType(parsedLogic, appOptions)
85
+ }
83
86
  }
84
87
 
85
88
  // Automatically ignore imports from "node_modules/@types/node", if {types: ["node"]} in tsconfig.json
@@ -105,6 +108,11 @@ export async function printToFiles(
105
108
  const shouldIgnore = (absolutePath: string) =>
106
109
  !!doNotImportFromPaths.find((badPath) => absolutePath.startsWith(badPath))
107
110
 
111
+ const nodeModulesPath = path.join(
112
+ appOptions.packageJsonPath ? path.dirname(appOptions.packageJsonPath) : appOptions.rootPath,
113
+ 'node_modules',
114
+ )
115
+
108
116
  let writtenFiles = 0
109
117
  let filesToWrite = 0
110
118
  let filesToModify = 0
@@ -112,6 +120,27 @@ export async function printToFiles(
112
120
  for (const [fileName, parsedLogics] of Object.entries(groupedByFile)) {
113
121
  const typeFileName = parsedLogics[0].typeFileName
114
122
 
123
+ if (appOptions.inline) {
124
+ // write/update MakeLogicType blocks above the logics instead of writing a logicType.ts file
125
+ const inlineResponse = await writeInlineLogicTypes(
126
+ program,
127
+ appOptions,
128
+ fileName,
129
+ parsedLogics,
130
+ nodeModulesPath,
131
+ shouldIgnore,
132
+ )
133
+ filesToWrite += inlineResponse.filesToWrite
134
+ writtenFiles += inlineResponse.writtenFiles
135
+
136
+ // the logicType.ts file is superseded by the inline block
137
+ if (appOptions.delete && fs.existsSync(typeFileName)) {
138
+ log(`🗑️ Deleting: ${path.relative(process.cwd(), typeFileName)}`)
139
+ fs.unlinkSync(typeFileName)
140
+ }
141
+ continue
142
+ }
143
+
115
144
  const logicStrings = []
116
145
  const requiredKeys = new Set(['Logic'])
117
146
  for (const parsedLogic of parsedLogics) {
@@ -131,62 +160,12 @@ export async function printToFiles(
131
160
 
132
161
  const output = logicStrings.join('\n\n')
133
162
 
134
- const nodeModulesPath = path.join(
135
- appOptions.packageJsonPath ? path.dirname(appOptions.packageJsonPath) : appOptions.rootPath,
136
- 'node_modules',
163
+ const otherimports = buildTypeImportEntries(
164
+ parsedLogics[0].typeReferencesToImportFromFiles,
165
+ parsedLogics[0].typeFileName,
166
+ nodeModulesPath,
167
+ shouldIgnore,
137
168
  )
138
-
139
- const otherimports = Object.entries(parsedLogics[0].typeReferencesToImportFromFiles)
140
- .filter(([_, list]) => list.size > 0)
141
- .map(([file, list]) => {
142
- let finalPath = file
143
-
144
- // Relative path? Get the absolute.
145
- if (finalPath.startsWith('.')) {
146
- finalPath = path.resolve(path.dirname(parsedLogics[0].typeFileName), finalPath)
147
- }
148
- if (finalPath.startsWith('node_modules/')) {
149
- finalPath = path.resolve(path.dirname(nodeModulesPath), finalPath)
150
- }
151
- // clean up '../../node_modules/...'
152
- if (finalPath.startsWith(nodeModulesPath)) {
153
- finalPath = finalPath.substring(nodeModulesPath.length + 1)
154
- if (finalPath.startsWith('.pnpm/')) {
155
- // node_modules/.pnpm/pkg@version/node_modules/* --> *
156
- const regex = /\.pnpm\/[^/]+@[^\/]+\/node_modules\/(.*)/
157
- const result = finalPath.match(regex)
158
- if (result && result.length > 1) {
159
- finalPath = result[1]
160
- }
161
- }
162
- if (finalPath.startsWith('@types/')) {
163
- finalPath = finalPath.substring(7)
164
- }
165
- }
166
-
167
- // Resolve absolute urls
168
- if (finalPath.startsWith('/')) {
169
- finalPath = path.relative(path.dirname(parsedLogics[0].typeFileName), finalPath)
170
- if (!finalPath.startsWith('.')) {
171
- finalPath = `./${finalPath}`
172
- }
173
- }
174
-
175
- // Remove extension
176
- finalPath = finalPath.replace(/(\.d|)\.tsx?$/, '')
177
-
178
- // Remove "/index"
179
- if (finalPath.split('/').length === 2 && finalPath.endsWith('/index')) {
180
- finalPath = finalPath.substring(0, finalPath.length - 6)
181
- }
182
-
183
- return {
184
- list: [...list].sort(),
185
- fullPath: file,
186
- finalPath,
187
- }
188
- })
189
- .filter((entry) => !shouldIgnore(entry.fullPath))
190
169
  .map(({ list, finalPath }) => `import type { ${list.join(', ')} } from '${finalPath}'`)
191
170
  .join('\n')
192
171
 
@@ -306,6 +285,72 @@ export async function printToFiles(
306
285
  return { filesToWrite, writtenFiles, filesToModify }
307
286
  }
308
287
 
288
+ export interface TypeImportEntry {
289
+ list: string[]
290
+ fullPath: string
291
+ finalPath: string
292
+ }
293
+
294
+ /** Resolve gathered type references into import specifiers, relative to the file they'll be imported into */
295
+ export function buildTypeImportEntries(
296
+ typeReferencesToImportFromFiles: Record<string, Set<string>>,
297
+ importIntoFile: string,
298
+ nodeModulesPath: string,
299
+ shouldIgnore: (absolutePath: string) => boolean,
300
+ ): TypeImportEntry[] {
301
+ return Object.entries(typeReferencesToImportFromFiles)
302
+ .filter(([_, list]) => list.size > 0)
303
+ .map(([file, list]) => {
304
+ let finalPath = file
305
+
306
+ // Relative path? Get the absolute.
307
+ if (finalPath.startsWith('.')) {
308
+ finalPath = path.resolve(path.dirname(importIntoFile), finalPath)
309
+ }
310
+ if (finalPath.startsWith('node_modules/')) {
311
+ finalPath = path.resolve(path.dirname(nodeModulesPath), finalPath)
312
+ }
313
+ // clean up '../../node_modules/...'
314
+ if (finalPath.startsWith(nodeModulesPath)) {
315
+ finalPath = finalPath.substring(nodeModulesPath.length + 1)
316
+ if (finalPath.startsWith('.pnpm/')) {
317
+ // node_modules/.pnpm/pkg@version/node_modules/* --> *
318
+ const regex = /\.pnpm\/[^/]+@[^\/]+\/node_modules\/(.*)/
319
+ const result = finalPath.match(regex)
320
+ if (result && result.length > 1) {
321
+ finalPath = result[1]
322
+ }
323
+ }
324
+ if (finalPath.startsWith('@types/')) {
325
+ finalPath = finalPath.substring(7)
326
+ }
327
+ }
328
+
329
+ // Resolve absolute urls
330
+ if (finalPath.startsWith('/')) {
331
+ finalPath = path.relative(path.dirname(importIntoFile), finalPath)
332
+ if (!finalPath.startsWith('.')) {
333
+ finalPath = `./${finalPath}`
334
+ }
335
+ }
336
+
337
+ // Remove extension
338
+ finalPath = finalPath.replace(/(\.d|)\.tsx?$/, '')
339
+
340
+ // Remove "/index"
341
+ if (finalPath.split('/').length === 2 && finalPath.endsWith('/index')) {
342
+ finalPath = finalPath.substring(0, finalPath.length - 6)
343
+ }
344
+
345
+ return {
346
+ list: [...list].sort(),
347
+ fullPath: file,
348
+ finalPath,
349
+ }
350
+ })
351
+ .filter((entry) => !shouldIgnore(entry.fullPath))
352
+ }
353
+
309
354
  export function nodeToString(node: Node): string {
310
355
  const printer = createPrinter({ newLine: NewLineKind.LineFeed })
311
356
  const sourceFile = createSourceFile('logic.ts', '', ScriptTarget.Latest, false, ScriptKind.TS)
@@ -1,12 +1,15 @@
1
1
  const fs = require('fs')
2
+ const os = require('os')
2
3
  const path = require('path')
3
4
 
4
5
  require('ts-node/register/transpile-only')
5
6
 
6
7
  const repoRoot = path.resolve(__dirname, '..', '..')
7
- const projectDir = fs.mkdtempSync(path.join(repoRoot, 'tmp-watch-smoke-'))
8
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-watch-smoke-'))
8
9
  const tsconfigPath = path.join(projectDir, 'tsconfig.json')
10
+ const keaDtsPath = path.join(projectDir, 'node_modules', 'kea', 'index.d.ts')
9
11
  const fileCount = 200
12
+ const logicPaths = []
10
13
 
11
14
  const noop = () => {}
12
15
  console.info = noop
@@ -18,6 +21,10 @@ let completed = 0
18
21
  let settleTimer
19
22
  let timeoutTimer
20
23
  let finished = false
24
+ let editTriggered = false
25
+ let incrementalAfterEdit = false
26
+ const parsedLogicCountsAfterEdit = []
27
+ const sourceFilePathsAfterEdit = []
21
28
 
22
29
  function cleanup() {
23
30
  try {
@@ -41,16 +48,31 @@ function finish(code) {
41
48
  completed,
42
49
  active,
43
50
  maxActive,
51
+ incrementalAfterEdit,
52
+ parsedLogicCountsAfterEdit,
53
+ sourceFilePathsAfterEdit,
44
54
  }) + '\n',
45
55
  )
46
56
  process.exit(code)
47
57
  }
48
58
 
49
59
  function scheduleFinishIfSettled() {
50
- if (started > 1 && active === 0) {
51
- clearTimeout(settleTimer)
52
- settleTimer = setTimeout(() => finish(0), 300)
60
+ if (active !== 0) {
61
+ return
53
62
  }
63
+
64
+ clearTimeout(settleTimer)
65
+ settleTimer = setTimeout(() => {
66
+ if (!editTriggered && started > 1) {
67
+ editTriggered = true
68
+ fs.appendFileSync(logicPaths[0], '\n// trigger incremental watch pass\n')
69
+ return
70
+ }
71
+
72
+ if (editTriggered && incrementalAfterEdit) {
73
+ finish(0)
74
+ }
75
+ }, 300)
54
76
  }
55
77
 
56
78
  process.on('uncaughtException', (error) => {
@@ -82,9 +104,13 @@ fs.writeFileSync(
82
104
  ),
83
105
  )
84
106
 
107
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
108
+ fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
109
+
85
110
  for (let i = 0; i < fileCount; i++) {
86
111
  const dir = path.join(projectDir, 'src', `group${String(i % 10).padStart(2, '0')}`)
87
112
  const filePath = path.join(dir, `logic${i}.ts`)
113
+ logicPaths.push(filePath)
88
114
 
89
115
  fs.mkdirSync(dir, { recursive: true })
90
116
  fs.writeFileSync(
@@ -109,11 +135,20 @@ const printModule = require(path.join(repoRoot, 'src/print/print'))
109
135
  const originalPrintToFiles = printModule.printToFiles
110
136
 
111
137
  printModule.printToFiles = async function (...args) {
138
+ const appOptions = args[1]
139
+ const parsedLogics = args[2]
140
+
112
141
  active += 1
113
142
  started += 1
114
143
  maxActive = Math.max(maxActive, active)
115
144
  clearTimeout(settleTimer)
116
145
 
146
+ if (editTriggered) {
147
+ parsedLogicCountsAfterEdit.push(parsedLogics.length)
148
+ sourceFilePathsAfterEdit.push(appOptions.sourceFilePath || null)
149
+ incrementalAfterEdit = incrementalAfterEdit || (parsedLogics.length === 1 && !!appOptions.sourceFilePath)
150
+ }
151
+
117
152
  await new Promise((resolve) => setTimeout(resolve, 25))
118
153
 
119
154
  try {
@@ -1,13 +1,15 @@
1
1
  const fs = require('fs')
2
2
  const Module = require('module')
3
+ const os = require('os')
3
4
  const path = require('path')
4
5
 
5
6
  require('ts-node/register/transpile-only')
6
7
 
7
8
  const repoRoot = path.resolve(__dirname, '..', '..')
8
- const projectDir = fs.mkdtempSync(path.join(repoRoot, 'tmp-write-smoke-'))
9
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-write-smoke-'))
9
10
  const tsconfigPath = path.join(projectDir, 'tsconfig.json')
10
11
  const logicFilePath = path.join(projectDir, 'src', 'logic.ts')
12
+ const keaDtsPath = path.join(projectDir, 'node_modules', 'kea', 'index.d.ts')
11
13
 
12
14
  const noop = () => {}
13
15
 
@@ -83,6 +85,7 @@ process.on('unhandledRejection', (error) => {
83
85
  })
84
86
 
85
87
  fs.mkdirSync(path.dirname(logicFilePath), { recursive: true })
88
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
86
89
  fs.writeFileSync(
87
90
  tsconfigPath,
88
91
  JSON.stringify(
@@ -102,6 +105,8 @@ fs.writeFileSync(
102
105
  ),
103
106
  )
104
107
 
108
+ fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
109
+
105
110
  fs.writeFileSync(
106
111
  logicFilePath,
107
112
  [
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 –
@@ -50,12 +51,13 @@ export async function runTypeGen(appOptions: AppOptions) {
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,6 +184,46 @@ export async function runTypeGen(appOptions: AppOptions) {
173
184
  return response
174
185
  }
175
186
 
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
+
176
227
  if (program && !appOptions.watch && appOptions.sourceFilePath) {
177
228
  await goThroughAllTheFiles(program, appOptions)
178
229
  if (appOptions.write) {
package/src/types.ts CHANGED
@@ -78,6 +78,8 @@ export interface AppOptions {
78
78
  addTsNocheck?: boolean
79
79
  /** Convert kea 2.0 logic input to kea 3.0 builders */
80
80
  convertToBuilders?: boolean
81
+ /** Write logic types as MakeLogicType blocks above each kea() call, instead of into logicType.ts files */
82
+ inline?: boolean
81
83
  /** Show TypeScript errors */
82
84
  showTsErrors?: boolean
83
85
  /** Cache generated logic files into .typegen, use them if generating a logic type for the first time */
package/src/utils.ts CHANGED
@@ -53,6 +53,87 @@ export function isKeaCall(node: ts.Node, checker: ts.TypeChecker) {
53
53
  return ts.isObjectLiteralExpression(input) || ts.isArrayLiteralExpression(input)
54
54
  }
55
55
 
56
+ /** True if the node has a leading "kea-typegen" comment, marking it as generated by us */
57
+ export function hasKeaTypegenMarker(node: ts.Node): boolean {
58
+ const sourceFile = node.getSourceFile()
59
+ const ranges = ts.getLeadingCommentRanges(sourceFile.text, node.pos) ?? []
60
+ return ranges.some(({ pos, end }) => sourceFile.text.slice(pos, end).includes('kea-typegen'))
61
+ }
62
+
63
+ const INLINE_BLOCK_SUFFIXES = ['Values', 'Actions', 'Props']
64
+
65
+ /**
66
+ * The statements making up an inline logic type block: the `logicType` alias itself, plus any
67
+ * `logicValues` / `logicActions` / `logicProps` interfaces directly above it.
68
+ */
69
+ export function getInlineBlockStatements(
70
+ declaration: ts.TypeAliasDeclaration | ts.InterfaceDeclaration,
71
+ logicName: string,
72
+ ): ts.Statement[] {
73
+ const statements = declaration.getSourceFile().statements
74
+ const index = statements.indexOf(declaration)
75
+ if (index === -1) {
76
+ return [declaration]
77
+ }
78
+ const generatedNames = new Set(INLINE_BLOCK_SUFFIXES.map((suffix) => `${logicName}${suffix}`))
79
+ const block: ts.Statement[] = [declaration]
80
+ for (let i = index - 1; i >= 0; i--) {
81
+ const statement = statements[i]
82
+ if (
83
+ (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) &&
84
+ generatedNames.has(statement.name.text)
85
+ ) {
86
+ block.unshift(statement)
87
+ } else {
88
+ break
89
+ }
90
+ }
91
+ return block
92
+ }
93
+
94
+ /** True if this local `logicType` declaration is part of a kea-typegen generated inline block */
95
+ export function isGeneratedInlineBlock(
96
+ declaration: ts.TypeAliasDeclaration | ts.InterfaceDeclaration,
97
+ logicName: string,
98
+ ): boolean {
99
+ return getInlineBlockStatements(declaration, logicName).some(hasKeaTypegenMarker)
100
+ }
101
+
102
+ /**
103
+ * True if the type argument in `kea<...>()` is a manually written type (e.g. `MakeLogicType<...>`
104
+ * or a custom interface), as opposed to a type that kea-typegen generates and keeps updated.
105
+ * Manually typed logics are skipped entirely.
106
+ */
107
+ export function isManuallyTypedLogic(
108
+ typeArgument: ts.TypeNode,
109
+ logicTypeName: string,
110
+ checker: ts.TypeChecker,
111
+ ): boolean {
112
+ if (!ts.isTypeReferenceNode(typeArgument)) {
113
+ // e.g. kea<{ actions: ... }>(...)
114
+ return true
115
+ }
116
+ if (!ts.isIdentifier(typeArgument.typeName) || typeArgument.typeName.escapedText !== logicTypeName) {
117
+ // e.g. kea<MakeLogicType<Values, Actions>>(...) or kea<MyCustomLogicType>(...)
118
+ return true
119
+ }
120
+ const symbol = checker.getSymbolAtLocation(typeArgument.typeName)
121
+ const declaration = symbol?.getDeclarations()?.[0]
122
+ if (!declaration) {
123
+ // "logicType" that doesn't resolve --> a generated type that doesn't exist yet
124
+ return false
125
+ }
126
+ if (ts.isImportSpecifier(declaration)) {
127
+ // imported logic types are managed by kea-typegen
128
+ return false
129
+ }
130
+ if (ts.isTypeAliasDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) {
131
+ // local declarations are ours only if their block carries the kea-typegen marker (written by --inline mode)
132
+ return !isGeneratedInlineBlock(declaration, logicTypeName.replace(/Type$/, ''))
133
+ }
134
+ return true
135
+ }
136
+
56
137
  export function getTypeNodeForNode(node: ts.Node, checker: ts.TypeChecker): ts.TypeNode {
57
138
  let typeNode
58
139
  if (node) {
@@ -10,6 +10,7 @@ import {
10
10
  getLogicPathString,
11
11
  getTypeNodeForNode,
12
12
  isKeaCall,
13
+ isManuallyTypedLogic,
13
14
  } from '../utils'
14
15
  import { visitActions } from './visitActions'
15
16
  import { visitReducers } from './visitReducers'
@@ -216,6 +217,19 @@ export function visitKeaCalls(
216
217
  const keaTypeArguments = ts.isCallExpression(node.parent) ? node.parent.typeArguments : []
217
218
  const keaTypeArgument = keaTypeArguments?.[0]
218
219
 
220
+ // if the logic is already manually typed (e.g. kea<MakeLogicType<...>>(...)), leave it alone
221
+ if (keaTypeArgument && isManuallyTypedLogic(keaTypeArgument, logicTypeName, checker)) {
222
+ if (appOptions?.verbose) {
223
+ appOptions.log(
224
+ `🩶 Skipping manually typed logic "${logicName}" in ${path.relative(
225
+ process.cwd(),
226
+ sourceFile.fileName,
227
+ )}`,
228
+ )
229
+ }
230
+ return
231
+ }
232
+
219
233
  const pathString = getLogicPathString(appOptions, sourceFile.fileName)
220
234
  let typeFileName = sourceFile.fileName.replace(/\.[tj]sx?$/, 'Type.ts')
221
235