kea-typegen 3.7.1 → 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 (40) hide show
  1. package/CHANGELOG.md +5 -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/cli/typegen.js +4 -0
  7. package/dist/src/cli/typegen.js.map +1 -1
  8. package/dist/src/print/print.d.ts +6 -0
  9. package/dist/src/print/print.js +59 -42
  10. package/dist/src/print/print.js.map +1 -1
  11. package/dist/src/types.d.ts +1 -0
  12. package/dist/src/utils.d.ts +4 -0
  13. package/dist/src/utils.js +55 -0
  14. package/dist/src/utils.js.map +1 -1
  15. package/dist/src/visit/visit.js +6 -0
  16. package/dist/src/visit/visit.js.map +1 -1
  17. package/dist/src/write/writeInlineLogicTypes.d.ts +8 -0
  18. package/dist/src/write/writeInlineLogicTypes.js +236 -0
  19. package/dist/src/write/writeInlineLogicTypes.js.map +1 -0
  20. package/dist/src/write/writeTypeImports.d.ts +8 -0
  21. package/dist/src/write/writeTypeImports.js +3 -0
  22. package/dist/src/write/writeTypeImports.js.map +1 -1
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +3 -1
  25. package/samples/manualTypedLogic.ts +20 -0
  26. package/samples-inline/counterLogic.ts +36 -0
  27. package/samples-inline/manualLogic.ts +20 -0
  28. package/samples-inline/profileLogic.ts +30 -0
  29. package/samples-inline/searchLogic.ts +30 -0
  30. package/samples-inline/tsconfig.json +17 -0
  31. package/samples-inline/types.ts +5 -0
  32. package/src/__tests__/inline.ts +266 -0
  33. package/src/cli/typegen.ts +4 -0
  34. package/src/print/print.ts +102 -57
  35. package/src/types.ts +2 -0
  36. package/src/utils.ts +81 -0
  37. package/src/visit/visit.ts +14 -0
  38. package/src/write/writeInlineLogicTypes.ts +305 -0
  39. package/src/write/writeTypeImports.ts +4 -4
  40. package/tsconfig.json +1 -0
@@ -0,0 +1,266 @@
1
+ import * as fs from 'fs'
2
+ import * as os from 'os'
3
+ import * as path from 'path'
4
+ import * as ts from 'typescript'
5
+ import { AppOptions } from '../types'
6
+ import { programFromSource } from '../utils'
7
+ import { visitProgram } from '../visit/visit'
8
+ import { printToFiles } from '../print/print'
9
+
10
+ describe('manually typed logics', () => {
11
+ test('skips logics typed with MakeLogicType', () => {
12
+ const logicSource = `
13
+ import { kea, MakeLogicType } from 'kea'
14
+
15
+ interface Values { name: string }
16
+ interface Actions { updateName: (name: string) => { name: string } }
17
+
18
+ const logic = kea<MakeLogicType<Values, Actions>>({
19
+ actions: () => ({
20
+ updateName: (name: string) => ({ name }),
21
+ }),
22
+ })
23
+ `
24
+ expect(visitProgram(programFromSource(logicSource)).length).toBe(0)
25
+ })
26
+
27
+ test('skips logics typed with a custom local type, even when named like a generated one', () => {
28
+ const logicSource = `
29
+ import { kea, MakeLogicType } from 'kea'
30
+
31
+ export type logicType = MakeLogicType<{ name: string }, { updateName: (name: string) => { name: string } }>
32
+
33
+ const logic = kea<logicType>({
34
+ actions: () => ({
35
+ updateName: (name: string) => ({ name }),
36
+ }),
37
+ })
38
+ `
39
+ expect(visitProgram(programFromSource(logicSource)).length).toBe(0)
40
+ })
41
+
42
+ test('does not skip local logic types carrying the kea-typegen marker', () => {
43
+ const logicSource = `
44
+ import { kea, MakeLogicType } from 'kea'
45
+
46
+ // Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.
47
+ export type logicType = MakeLogicType<{ name: string }, { updateName: (name: string) => { name: string } }>
48
+
49
+ const logic = kea<logicType>({
50
+ actions: () => ({
51
+ updateName: (name: string) => ({ name }),
52
+ }),
53
+ })
54
+ `
55
+ const parsedLogics = visitProgram(programFromSource(logicSource))
56
+ expect(parsedLogics.length).toBe(1)
57
+ expect(parsedLogics[0].actions.map((a) => a.name)).toEqual(['updateName'])
58
+ })
59
+
60
+ test('does not skip generated blocks where the marker sits on the values interface', () => {
61
+ const logicSource = `
62
+ import { kea, MakeLogicType } from 'kea'
63
+
64
+ // Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.
65
+ export interface logicValues { name: string }
66
+
67
+ export interface logicActions { updateName: (name: string) => { name: string } }
68
+
69
+ export type logicType = MakeLogicType<logicValues, logicActions>
70
+
71
+ const logic = kea<logicType>({
72
+ actions: () => ({
73
+ updateName: (name: string) => ({ name }),
74
+ }),
75
+ })
76
+ `
77
+ const parsedLogics = visitProgram(programFromSource(logicSource))
78
+ expect(parsedLogics.length).toBe(1)
79
+ })
80
+
81
+ test('does not skip logics with a generated type that does not resolve yet', () => {
82
+ const logicSource = `
83
+ import { kea } from 'kea'
84
+
85
+ const logic = kea<logicType>({
86
+ actions: () => ({
87
+ updateName: (name: string) => ({ name }),
88
+ }),
89
+ })
90
+ `
91
+ expect(visitProgram(programFromSource(logicSource)).length).toBe(1)
92
+ })
93
+ })
94
+
95
+ describe('inline mode', () => {
96
+ const createProgram = (fileNames: string[]) =>
97
+ ts.createProgram(fileNames, {
98
+ module: ts.ModuleKind.CommonJS,
99
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
100
+ target: ts.ScriptTarget.ES2020,
101
+ skipLibCheck: true,
102
+ })
103
+
104
+ test('writes and updates MakeLogicType blocks above the logic', async () => {
105
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-inline-'))
106
+
107
+ try {
108
+ const logicDir = path.join(tempDir, 'src')
109
+ const logicPath = path.join(logicDir, 'logic.ts')
110
+ const logicTypePath = path.join(logicDir, 'logicType.ts')
111
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
112
+
113
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
114
+ fs.mkdirSync(logicDir, { recursive: true })
115
+
116
+ fs.writeFileSync(
117
+ keaDtsPath,
118
+ [
119
+ 'export function kea<T = any>(input: any): T',
120
+ 'export interface MakeLogicType<V = any, A = any, P = any> {}',
121
+ '',
122
+ ].join('\n'),
123
+ )
124
+ fs.writeFileSync(
125
+ logicPath,
126
+ [
127
+ "import { kea } from 'kea'",
128
+ '',
129
+ 'export const logic = kea({',
130
+ ' props: {} as { id: number },',
131
+ ' actions: () => ({',
132
+ ' setValue: (value: string) => ({ value }),',
133
+ ' }),',
134
+ ' reducers: () => ({',
135
+ " value: ['' as string, { setValue: (_, { value }) => value }],",
136
+ ' }),',
137
+ '})',
138
+ '',
139
+ ].join('\n'),
140
+ )
141
+
142
+ const appOptions: AppOptions = {
143
+ rootPath: logicDir,
144
+ typesPath: logicDir,
145
+ packageJsonPath: path.join(tempDir, 'package.json'),
146
+ inline: true,
147
+ write: true,
148
+ log: () => {},
149
+ }
150
+
151
+ const program = createProgram([logicPath])
152
+ const parsedLogics = visitProgram(program, appOptions)
153
+ expect(parsedLogics.length).toBe(1)
154
+
155
+ const response = await printToFiles(program, appOptions, parsedLogics)
156
+ expect(response.writtenFiles).toBe(1)
157
+
158
+ const writtenLogic = fs.readFileSync(logicPath, 'utf8')
159
+
160
+ expect(writtenLogic).toContain("import { MakeLogicType, kea } from 'kea'")
161
+ expect(writtenLogic).toContain('// Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.')
162
+ expect(writtenLogic).toContain('export interface logicValues {')
163
+ expect(writtenLogic).toContain('export interface logicActions {')
164
+ expect(writtenLogic).toContain('export interface logicProps {')
165
+ expect(writtenLogic).toContain('export type logicType = MakeLogicType<logicValues, logicActions, logicProps>')
166
+ expect(writtenLogic).toContain('export const logic = kea<logicType>({')
167
+ expect(writtenLogic).toContain('value: string')
168
+ expect(writtenLogic).toContain('setValue: (value: string) =>')
169
+ expect(fs.existsSync(logicTypePath)).toBe(false)
170
+
171
+ // a second pass over the written file changes nothing
172
+ const secondProgram = createProgram([logicPath])
173
+ const secondParsedLogics = visitProgram(secondProgram, appOptions)
174
+ expect(secondParsedLogics.length).toBe(1)
175
+
176
+ const secondResponse = await printToFiles(secondProgram, appOptions, secondParsedLogics)
177
+ expect(secondResponse).toEqual({ filesToWrite: 0, writtenFiles: 0, filesToModify: 0 })
178
+ expect(fs.readFileSync(logicPath, 'utf8')).toBe(writtenLogic)
179
+
180
+ // adding an action updates the existing block in place
181
+ fs.writeFileSync(
182
+ logicPath,
183
+ fs
184
+ .readFileSync(logicPath, 'utf8')
185
+ .replace('setValue: (value: string) => ({ value }),', 'setValue: (value: string) => ({ value }),\n reset: true,'),
186
+ )
187
+
188
+ const thirdProgram = createProgram([logicPath])
189
+ const thirdResponse = await printToFiles(thirdProgram, appOptions, visitProgram(thirdProgram, appOptions))
190
+ expect(thirdResponse.writtenFiles).toBe(1)
191
+
192
+ const updatedLogic = fs.readFileSync(logicPath, 'utf8')
193
+ expect(updatedLogic).toContain('reset: ')
194
+ expect(updatedLogic.match(/export type logicType = MakeLogicType</g)?.length).toBe(1)
195
+ expect(updatedLogic.match(/export interface logicValues \{/g)?.length).toBe(1)
196
+ expect(updatedLogic.match(/export interface logicActions \{/g)?.length).toBe(1)
197
+ expect(updatedLogic.match(/DO NOT EDIT THIS BLOCK MANUALLY/g)?.length).toBe(1)
198
+ } finally {
199
+ fs.rmSync(tempDir, { recursive: true, force: true })
200
+ }
201
+ })
202
+
203
+ test('replaces the old logicType.ts import when switching to inline mode', async () => {
204
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-inline-migrate-'))
205
+
206
+ try {
207
+ const logicDir = path.join(tempDir, 'src')
208
+ const logicPath = path.join(logicDir, 'logic.ts')
209
+ const logicTypePath = path.join(logicDir, 'logicType.ts')
210
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
211
+
212
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
213
+ fs.mkdirSync(logicDir, { recursive: true })
214
+
215
+ fs.writeFileSync(
216
+ keaDtsPath,
217
+ [
218
+ 'export function kea<T = any>(input: any): T',
219
+ 'export interface MakeLogicType<V = any, A = any, P = any> {}',
220
+ '',
221
+ ].join('\n'),
222
+ )
223
+ fs.writeFileSync(logicTypePath, 'export interface logicType {}\n')
224
+ fs.writeFileSync(
225
+ logicPath,
226
+ [
227
+ "import { kea } from 'kea'",
228
+ "import type { logicType } from './logicType'",
229
+ '',
230
+ 'export const logic = kea<logicType>({',
231
+ ' actions: () => ({',
232
+ ' setValue: (value: string) => ({ value }),',
233
+ ' }),',
234
+ '})',
235
+ '',
236
+ ].join('\n'),
237
+ )
238
+
239
+ const appOptions: AppOptions = {
240
+ rootPath: logicDir,
241
+ typesPath: logicDir,
242
+ packageJsonPath: path.join(tempDir, 'package.json'),
243
+ inline: true,
244
+ write: true,
245
+ delete: true,
246
+ log: () => {},
247
+ }
248
+
249
+ const program = createProgram([logicPath])
250
+ const response = await printToFiles(program, appOptions, visitProgram(program, appOptions))
251
+ expect(response.writtenFiles).toBe(1)
252
+
253
+ const writtenLogic = fs.readFileSync(logicPath, 'utf8')
254
+
255
+ expect(writtenLogic).not.toContain("from './logicType'")
256
+ expect(writtenLogic).toContain("import { MakeLogicType, kea } from 'kea'")
257
+ expect(writtenLogic).toContain('export interface logicActions {')
258
+ // no values or props on this logic, so no interfaces are generated for them
259
+ expect(writtenLogic).toContain('export type logicType = MakeLogicType<{}, logicActions>')
260
+ expect(writtenLogic).toContain('export const logic = kea<logicType>({')
261
+ expect(fs.existsSync(logicTypePath)).toBe(false)
262
+ } finally {
263
+ fs.rmSync(tempDir, { recursive: true, force: true })
264
+ }
265
+ })
266
+ })
@@ -39,6 +39,10 @@ yargs
39
39
  .option('delete', { describe: 'Delete logicType.ts files without a corresponding logic.ts', type: 'boolean' })
40
40
  .option('add-ts-nocheck', { describe: 'Add @ts-nocheck to top of logicType.ts files', type: 'boolean' })
41
41
  .option('convert-to-builders', { describe: 'Convert Kea 2.0 inputs to Kea 3.0 logic builders', type: 'boolean' })
42
+ .option('inline', {
43
+ describe: 'Write logic types as MakeLogicType blocks above each kea() call, instead of logicType.ts files',
44
+ type: 'boolean',
45
+ })
42
46
  .option('import-global-types', {
43
47
  describe: 'Add import statements in logicType.ts files for global types (e.g. @types/node)',
44
48
  type: 'boolean',
@@ -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)
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