kea-typegen 3.8.1 → 3.8.3

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.
@@ -5,7 +5,37 @@ import * as ts from 'typescript'
5
5
  import { AppOptions } from '../types'
6
6
  import { programFromSource } from '../utils'
7
7
  import { visitProgram } from '../visit/visit'
8
- import { printToFiles } from '../print/print'
8
+ import { buildTypeImportEntries, printToFiles } from '../print/print'
9
+
10
+ describe('inline type imports', () => {
11
+ test('collapses physical pnpm paths outside the configured node_modules root', () => {
12
+ const workspace = path.join(os.tmpdir(), 'kea-typegen-workspace')
13
+ const physicalType = path.join(
14
+ workspace,
15
+ 'node_modules',
16
+ '.pnpm',
17
+ 'kea-router@3.4.1_kea@4.0.0_patch_hash=abc',
18
+ 'node_modules',
19
+ 'kea-router',
20
+ 'lib',
21
+ 'types.d.ts',
22
+ )
23
+ const entries = buildTypeImportEntries(
24
+ { [physicalType]: new Set(['LocationChangedPayload']) },
25
+ path.join(workspace, 'frontend', 'src', 'logic.ts'),
26
+ path.join(workspace, 'frontend', 'node_modules'),
27
+ () => false,
28
+ )
29
+
30
+ expect(entries).toEqual([
31
+ {
32
+ list: ['LocationChangedPayload'],
33
+ fullPath: physicalType,
34
+ finalPath: 'kea-router/lib/types',
35
+ },
36
+ ])
37
+ })
38
+ })
9
39
 
10
40
  describe('manually typed logics', () => {
11
41
  test('skips logics typed with MakeLogicType', () => {
@@ -117,7 +147,8 @@ describe('inline mode', () => {
117
147
  keaDtsPath,
118
148
  [
119
149
  'export function kea<T = any>(input: any): T',
120
- 'export interface MakeLogicType<V = any, A = any, P = any> {}',
150
+ 'export interface MakeLogicType<V = any, A = any, P = any, M = any> {}',
151
+ 'export type BreakPointFunction = (() => void) & ((ms: number) => Promise<void>)',
121
152
  '',
122
153
  ].join('\n'),
123
154
  )
@@ -128,12 +159,16 @@ describe('inline mode', () => {
128
159
  '',
129
160
  'export const logic = kea({',
130
161
  ' props: {} as { id: number },',
162
+ ' key: (props: { id: number }) => props.id,',
131
163
  ' actions: () => ({',
132
164
  ' setValue: (value: string) => ({ value }),',
133
165
  ' }),',
134
166
  ' reducers: () => ({',
135
167
  " value: ['' as string, { setValue: (_, { value }) => value }],",
136
168
  ' }),',
169
+ ' sharedListeners: () => ({',
170
+ ' syncValue: () => {},',
171
+ ' }),',
137
172
  '})',
138
173
  '',
139
174
  ].join('\n'),
@@ -158,31 +193,44 @@ describe('inline mode', () => {
158
193
  const writtenLogic = fs.readFileSync(logicPath, 'utf8')
159
194
 
160
195
  expect(writtenLogic).toContain("import { MakeLogicType, kea } from 'kea'")
161
- expect(writtenLogic).toContain('// Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.')
196
+ expect(writtenLogic).toContain(
197
+ "// Generated by kea-typegen. Update if you're an agent, ignore if you're human.",
198
+ )
162
199
  expect(writtenLogic).toContain('export interface logicValues {')
163
200
  expect(writtenLogic).toContain('export interface logicActions {')
164
201
  expect(writtenLogic).toContain('export interface logicProps {')
165
- expect(writtenLogic).toContain('export type logicType = MakeLogicType<logicValues, logicActions, logicProps>')
202
+ expect(writtenLogic).toContain('export interface logicMeta {')
203
+ expect(writtenLogic).toContain('key: number')
204
+ expect(writtenLogic).toContain('sharedListeners: {')
205
+ expect(writtenLogic).toContain(
206
+ 'export type logicType = MakeLogicType<logicValues, logicActions, logicProps, logicMeta>',
207
+ )
208
+ expect(writtenLogic).toContain("import type { BreakPointFunction } from 'kea'")
166
209
  expect(writtenLogic).toContain('export const logic = kea<logicType>({')
167
210
  expect(writtenLogic).toContain('value: string')
168
211
  expect(writtenLogic).toContain('setValue: (value: string) =>')
169
212
  expect(fs.existsSync(logicTypePath)).toBe(false)
170
213
 
171
- // a second pass over the written file changes nothing
214
+ // a second pass leaves formatter-unquoted property names alone
215
+ const formattedLogic = writtenLogic.replace('"syncValue":', 'syncValue:')
216
+ fs.writeFileSync(logicPath, formattedLogic)
172
217
  const secondProgram = createProgram([logicPath])
173
218
  const secondParsedLogics = visitProgram(secondProgram, appOptions)
174
219
  expect(secondParsedLogics.length).toBe(1)
175
220
 
176
221
  const secondResponse = await printToFiles(secondProgram, appOptions, secondParsedLogics)
177
222
  expect(secondResponse).toEqual({ filesToWrite: 0, writtenFiles: 0, filesToModify: 0 })
178
- expect(fs.readFileSync(logicPath, 'utf8')).toBe(writtenLogic)
223
+ expect(fs.readFileSync(logicPath, 'utf8')).toBe(formattedLogic)
179
224
 
180
225
  // adding an action updates the existing block in place
181
226
  fs.writeFileSync(
182
227
  logicPath,
183
228
  fs
184
229
  .readFileSync(logicPath, 'utf8')
185
- .replace('setValue: (value: string) => ({ value }),', 'setValue: (value: string) => ({ value }),\n reset: true,'),
230
+ .replace(
231
+ 'setValue: (value: string) => ({ value }),',
232
+ 'setValue: (value: string) => ({ value }),\n reset: true,',
233
+ ),
186
234
  )
187
235
 
188
236
  const thirdProgram = createProgram([logicPath])
@@ -194,8 +242,8 @@ describe('inline mode', () => {
194
242
  expect(updatedLogic.match(/export type logicType = MakeLogicType</g)?.length).toBe(1)
195
243
  expect(updatedLogic.match(/export interface logicValues \{/g)?.length).toBe(1)
196
244
  expect(updatedLogic.match(/export interface logicActions \{/g)?.length).toBe(1)
197
- // one marker per generated interface (values, actions, props)
198
- expect(updatedLogic.match(/DO NOT EDIT THIS BLOCK MANUALLY/g)?.length).toBe(3)
245
+ // one marker per generated interface (values, actions, props, meta)
246
+ expect(updatedLogic.match(/Update if you're an agent, ignore if you're human/g)?.length).toBe(4)
199
247
  } finally {
200
248
  fs.rmSync(tempDir, { recursive: true, force: true })
201
249
  }
@@ -310,15 +358,31 @@ describe('inline mode', () => {
310
358
  const program = createProgram([logicPath])
311
359
  await printToFiles(program, appOptions, visitProgram(program, appOptions))
312
360
 
313
- // simulate a formatter pass: strip semicolons and reflow unions with a leading pipe
361
+ // simulate an oxfmt pass: strip semicolons and reflow nested unions and objects
314
362
  const formatted = fs
315
363
  .readFileSync(logicPath, 'utf8')
316
364
  .replace(/;$/gm, '')
317
- .replace(/value: string \| number\) =>/g, 'value:\n | string\n | number\n ) =>')
365
+ .replace(
366
+ /setValue: \(value: string \| number\) => \{\n value: string \| number\n \}/g,
367
+ [
368
+ 'setValue: (',
369
+ ' value:',
370
+ ' | string',
371
+ ' | number,',
372
+ ' ) => {',
373
+ ' value: string | number',
374
+ ' }',
375
+ ].join('\n'),
376
+ )
377
+ .replace(' value: string | number\n }', ' value: (string | number)\n }')
318
378
  fs.writeFileSync(logicPath, formatted)
319
379
 
320
380
  const secondProgram = createProgram([logicPath])
321
- const secondResponse = await printToFiles(secondProgram, appOptions, visitProgram(secondProgram, appOptions))
381
+ const secondResponse = await printToFiles(
382
+ secondProgram,
383
+ appOptions,
384
+ visitProgram(secondProgram, appOptions),
385
+ )
322
386
  expect(secondResponse).toEqual({ filesToWrite: 0, writtenFiles: 0, filesToModify: 0 })
323
387
  expect(fs.readFileSync(logicPath, 'utf8')).toBe(formatted)
324
388
  } finally {
@@ -381,7 +445,7 @@ describe('inline mode', () => {
381
445
  ].join('\n'),
382
446
  )
383
447
  // the generated block sits above the comment
384
- expect(writtenLogic.indexOf('DO NOT EDIT THIS BLOCK MANUALLY')).toBeLessThan(
448
+ expect(writtenLogic.indexOf("Update if you're an agent, ignore if you're human")).toBeLessThan(
385
449
  writtenLogic.indexOf('// this comment describes the logic'),
386
450
  )
387
451
  } finally {
@@ -449,7 +513,11 @@ describe('inline mode', () => {
449
513
 
450
514
  // a second pass changes nothing
451
515
  const secondProgram = createProgram([logicPath])
452
- const secondResponse = await printToFiles(secondProgram, appOptions, visitProgram(secondProgram, appOptions))
516
+ const secondResponse = await printToFiles(
517
+ secondProgram,
518
+ appOptions,
519
+ visitProgram(secondProgram, appOptions),
520
+ )
453
521
  expect(secondResponse).toEqual({ filesToWrite: 0, writtenFiles: 0, filesToModify: 0 })
454
522
  } finally {
455
523
  fs.rmSync(tempDir, { recursive: true, force: true })
@@ -503,8 +571,14 @@ describe('mixed mode with inlinePaths', () => {
503
571
  writeKeaDts(tempDir)
504
572
  fs.mkdirSync(inlineDir, { recursive: true })
505
573
  fs.mkdirSync(classicDir, { recursive: true })
506
- fs.writeFileSync(inlineLogicPath, simpleLogicSource.replace('export const logic', 'export const inlinedLogic'))
507
- fs.writeFileSync(classicLogicPath, simpleLogicSource.replace('export const logic', 'export const classicLogic'))
574
+ fs.writeFileSync(
575
+ inlineLogicPath,
576
+ simpleLogicSource.replace('export const logic', 'export const inlinedLogic'),
577
+ )
578
+ fs.writeFileSync(
579
+ classicLogicPath,
580
+ simpleLogicSource.replace('export const logic', 'export const classicLogic'),
581
+ )
508
582
 
509
583
  const appOptions: AppOptions = {
510
584
  rootPath: rootDir,
@@ -520,8 +594,12 @@ describe('mixed mode with inlinePaths', () => {
520
594
  await printToFiles(program, appOptions, visitProgram(program, appOptions))
521
595
 
522
596
  const writtenInlineLogic = fs.readFileSync(inlineLogicPath, 'utf8')
523
- expect(writtenInlineLogic).toContain('// Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.')
524
- expect(writtenInlineLogic).toContain('export type inlinedLogicType = MakeLogicType<{}, inlinedLogicActions>')
597
+ expect(writtenInlineLogic).toContain(
598
+ "// Generated by kea-typegen. Update if you're an agent, ignore if you're human.",
599
+ )
600
+ expect(writtenInlineLogic).toContain(
601
+ 'export type inlinedLogicType = MakeLogicType<{}, inlinedLogicActions>',
602
+ )
525
603
  expect(fs.existsSync(path.join(inlineDir, 'inlinedLogicType.ts'))).toBe(false)
526
604
 
527
605
  const writtenClassicLogic = fs.readFileSync(classicLogicPath, 'utf8')
@@ -542,7 +620,10 @@ describe('mixed mode with inlinePaths', () => {
542
620
 
543
621
  writeKeaDts(tempDir)
544
622
  fs.mkdirSync(inlineDir, { recursive: true })
545
- fs.writeFileSync(inlineLogicPath, simpleLogicSource.replace('export const logic', 'export const inlinedLogic'))
623
+ fs.writeFileSync(
624
+ inlineLogicPath,
625
+ simpleLogicSource.replace('export const logic', 'export const inlinedLogic'),
626
+ )
546
627
 
547
628
  const appOptions: AppOptions = {
548
629
  rootPath: rootDir,
@@ -557,7 +638,7 @@ describe('mixed mode with inlinePaths', () => {
557
638
  await printToFiles(program, appOptions, visitProgram(program, appOptions))
558
639
 
559
640
  expect(fs.readFileSync(inlineLogicPath, 'utf8')).toContain(
560
- '// Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.',
641
+ "// Generated by kea-typegen. Update if you're an agent, ignore if you're human.",
561
642
  )
562
643
  expect(fs.existsSync(path.join(inlineDir, 'inlinedLogicType.ts'))).toBe(false)
563
644
  } finally {
@@ -683,13 +764,141 @@ describe('mixed mode with inlinePaths', () => {
683
764
 
684
765
  // a second pass over the annotated block changes nothing
685
766
  const secondProgram = createProgram([inlineLogicPath, consumerLogicPath, inlineConsumerLogicPath])
686
- const secondResponse = await printToFiles(secondProgram, appOptions, visitProgram(secondProgram, appOptions))
767
+ const secondResponse = await printToFiles(
768
+ secondProgram,
769
+ appOptions,
770
+ visitProgram(secondProgram, appOptions),
771
+ )
687
772
  expect(secondResponse).toEqual({ filesToWrite: 0, writtenFiles: 0, filesToModify: 0 })
688
773
  } finally {
689
774
  fs.rmSync(tempDir, { recursive: true, force: true })
690
775
  }
691
776
  })
692
777
 
778
+ test('sorts values and actions: connected first grouped by source logic, then own, all alphabetical', async () => {
779
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-inline-sort-'))
780
+
781
+ try {
782
+ const rootDir = path.join(tempDir, 'src')
783
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
784
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
785
+ fs.mkdirSync(rootDir, { recursive: true })
786
+ fs.writeFileSync(
787
+ keaDtsPath,
788
+ [
789
+ 'export function kea<T = any>(input: any): T',
790
+ 'export type AnyFunction = (...args: any) => any',
791
+ 'export interface MakeLogicType<V = any, A = any, P = any> {',
792
+ ' actionCreators: {',
793
+ ' [K in keyof A]: A[K] extends AnyFunction',
794
+ ' ? (...args: Parameters<A[K]>) => { type: string; payload: ReturnType<A[K]> }',
795
+ ' : never',
796
+ ' }',
797
+ ' values: V',
798
+ '}',
799
+ '',
800
+ ].join('\n'),
801
+ )
802
+
803
+ const makeProvider = (name: string, entries: [string, string][]): string =>
804
+ [
805
+ "import { MakeLogicType, kea } from 'kea'",
806
+ '',
807
+ '// Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.',
808
+ `export interface ${name}Values {`,
809
+ ...entries.map(([value]) => ` ${value}: string`),
810
+ '}',
811
+ '',
812
+ `export interface ${name}Actions {`,
813
+ ...entries.map(([value, action]) => ` ${action}: (${value}: string) => { ${value}: string }`),
814
+ '}',
815
+ '',
816
+ `export type ${name}Type = MakeLogicType<${name}Values, ${name}Actions>`,
817
+ '',
818
+ `export const ${name} = kea<${name}Type>({`,
819
+ ' actions: () => ({',
820
+ ...entries.map(([value, action]) => ` ${action}: (${value}: string) => ({ ${value} }),`),
821
+ ' }),',
822
+ ' reducers: () => ({',
823
+ ...entries.map(([value, action]) => ` ${value}: ['', { ${action}: (_, p) => p.${value} }],`),
824
+ ' }),',
825
+ '})',
826
+ '',
827
+ ].join('\n')
828
+
829
+ fs.writeFileSync(
830
+ path.join(rootDir, 'alphaLogic.ts'),
831
+ makeProvider('alphaLogic', [
832
+ ['bravo', 'setBravo'],
833
+ ['alpha', 'setAlpha'],
834
+ ]),
835
+ )
836
+ fs.writeFileSync(path.join(rootDir, 'betaLogic.ts'), makeProvider('betaLogic', [['charlie', 'setCharlie']]))
837
+
838
+ const consumerDir = path.join(rootDir, 'consumer')
839
+ const consumerLogicPath = path.join(consumerDir, 'sortedLogic.ts')
840
+ fs.mkdirSync(consumerDir, { recursive: true })
841
+ fs.writeFileSync(
842
+ consumerLogicPath,
843
+ [
844
+ "import { kea } from 'kea'",
845
+ '',
846
+ "import { alphaLogic } from '../alphaLogic'",
847
+ "import { betaLogic } from '../betaLogic'",
848
+ '',
849
+ 'export const sortedLogic = kea({',
850
+ ' connect: {',
851
+ " actions: [betaLogic, ['setCharlie'], alphaLogic, ['setBravo', 'setAlpha']],",
852
+ " values: [betaLogic, ['charlie'], alphaLogic, ['bravo', 'alpha']],",
853
+ ' },',
854
+ ' actions: () => ({',
855
+ ' zulu: (z: number) => ({ z }),',
856
+ ' aardvark: (a: number) => ({ a }),',
857
+ ' }),',
858
+ ' reducers: () => ({',
859
+ ' zebra: [0, { zulu: (_, { z }) => z }],',
860
+ ' anteater: [0, { aardvark: (_, { a }) => a }],',
861
+ ' }),',
862
+ '})',
863
+ '',
864
+ ].join('\n'),
865
+ )
866
+
867
+ const appOptions: AppOptions = {
868
+ rootPath: rootDir,
869
+ typesPath: rootDir,
870
+ packageJsonPath: path.join(tempDir, 'package.json'),
871
+ inlinePaths: [consumerDir],
872
+ write: true,
873
+ log: () => {},
874
+ }
875
+
876
+ const program = createProgram([
877
+ path.join(rootDir, 'alphaLogic.ts'),
878
+ path.join(rootDir, 'betaLogic.ts'),
879
+ consumerLogicPath,
880
+ ])
881
+ await printToFiles(program, appOptions, visitProgram(program, appOptions))
882
+
883
+ const written = fs.readFileSync(consumerLogicPath, 'utf8')
884
+ const orderOf = (names: string[]): void => {
885
+ const positions = names.map((name) => written.indexOf(name))
886
+ positions.forEach((position, index) => {
887
+ expect(position).toBeGreaterThan(-1)
888
+ if (index > 0) {
889
+ expect(position).toBeGreaterThan(positions[index - 1])
890
+ }
891
+ })
892
+ }
893
+ // values: alphaLogic group (alphabetical), betaLogic group, then own values alphabetically
894
+ orderOf(['alpha: string', 'bravo: string', 'charlie: string', 'anteater: number', 'zebra: number'])
895
+ // actions: same grouping
896
+ orderOf(['setAlpha:', 'setBravo:', 'setCharlie:', 'aardvark:', 'zulu:'])
897
+ } finally {
898
+ fs.rmSync(tempDir, { recursive: true, force: true })
899
+ }
900
+ })
901
+
693
902
  test('files that already carry an inline block stay inline without any inline options', async () => {
694
903
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-inline-sticky-'))
695
904
 
@@ -738,7 +947,7 @@ describe('mixed mode with inlinePaths', () => {
738
947
 
739
948
  const writtenLogic = fs.readFileSync(logicPath, 'utf8')
740
949
  expect(writtenLogic).toContain('setOther: (other: number) =>')
741
- expect(writtenLogic.match(/DO NOT EDIT THIS BLOCK MANUALLY/g)?.length).toBe(1)
950
+ expect(writtenLogic.match(/Update if you're an agent, ignore if you're human/g)?.length).toBe(1)
742
951
  expect(fs.existsSync(path.join(rootDir, 'stickyLogicType.ts'))).toBe(false)
743
952
  } finally {
744
953
  fs.rmSync(tempDir, { recursive: true, force: true })
@@ -323,9 +323,17 @@ export function buildTypeImportEntries(
323
323
  finalPath = result[1]
324
324
  }
325
325
  }
326
- if (finalPath.startsWith('@types/')) {
327
- finalPath = finalPath.substring(7)
328
- }
326
+ }
327
+
328
+ // A workspace package.json can point at a different node_modules root than the physical
329
+ // dependency path. Collapse the innermost package path instead of writing pnpm internals.
330
+ const nestedNodeModules = `${path.sep}node_modules${path.sep}`
331
+ const nestedNodeModulesIndex = finalPath.lastIndexOf(nestedNodeModules)
332
+ if (nestedNodeModulesIndex >= 0) {
333
+ finalPath = finalPath.substring(nestedNodeModulesIndex + nestedNodeModules.length)
334
+ }
335
+ if (finalPath.startsWith('@types/')) {
336
+ finalPath = finalPath.substring(7)
329
337
  }
330
338
 
331
339
  // Resolve absolute urls
package/src/utils.ts CHANGED
@@ -60,11 +60,11 @@ export function hasKeaTypegenMarker(node: ts.Node): boolean {
60
60
  return ranges.some(({ pos, end }) => sourceFile.text.slice(pos, end).includes('kea-typegen'))
61
61
  }
62
62
 
63
- const INLINE_BLOCK_SUFFIXES = ['Values', 'Actions', 'Props']
63
+ const INLINE_BLOCK_SUFFIXES = ['Values', 'Actions', 'Props', 'Meta']
64
64
 
65
65
  /**
66
66
  * The statements making up an inline logic type block: the `logicType` alias itself, plus any
67
- * `logicValues` / `logicActions` / `logicProps` interfaces directly above it.
67
+ * `logicValues` / `logicActions` / `logicProps` / `logicMeta` interfaces directly above it.
68
68
  */
69
69
  export function getInlineBlockStatements(
70
70
  declaration: ts.TypeAliasDeclaration | ts.InterfaceDeclaration,
@@ -4,11 +4,15 @@ import * as osPath from 'path'
4
4
  import { factory, SyntaxKind } from 'typescript'
5
5
  import { AppOptions, ParsedLogic } from '../types'
6
6
  import { buildTypeImportEntries, nodeToString, runThroughPrettier } from '../print/print'
7
- import { printValues } from '../print/printValues'
8
- import { getInlineBlockStatements, isGeneratedInlineBlock } from '../utils'
7
+ import { printInternalExtraInput } from '../print/printInternalExtraInput'
8
+ import { printInternalReducerActions } from '../print/printInternalReducerActions'
9
+ import { printInternalSelectorTypes } from '../print/printInternalSelectorTypes'
10
+ import { printKey } from '../print/printKey'
11
+ import { printSharedListeners } from '../print/printSharedListeners'
12
+ import { cleanDuplicateAnyNodes, getInlineBlockStatements, isGeneratedInlineBlock } from '../utils'
9
13
  import { applyTextEdits, getImportInsertPosition, getTypeArgumentInsertEnd, TextEdit } from './writeTypeImports'
10
14
 
11
- export const INLINE_TYPE_MARKER = ' Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.'
15
+ export const INLINE_TYPE_MARKER = " Generated by kea-typegen. Update if you're an agent, ignore if you're human."
12
16
 
13
17
  /**
14
18
  * Print a block of interfaces (Values, Actions, and Props when it's a literal), plus an
@@ -40,35 +44,51 @@ export function printInlineLogicType(parsedLogic: ParsedLogic): string {
40
44
 
41
45
  // mark connected values/actions with the logic they come from, e.g. `user: UserType | null // userLogic`
42
46
  const withSourceLogicComment = <T extends ts.TypeElement>(member: T, sourceLogic: string | undefined): T =>
43
- sourceLogic ? ts.addSyntheticTrailingComment(member, SyntaxKind.SingleLineCommentTrivia, ` ${sourceLogic}`, false) : member
47
+ sourceLogic
48
+ ? ts.addSyntheticTrailingComment(member, SyntaxKind.SingleLineCommentTrivia, ` ${sourceLogic}`, false)
49
+ : member
44
50
 
45
- const valueSources = new Map<string, string | undefined>()
46
- for (const value of [...parsedLogic.reducers, ...parsedLogic.selectors]) {
47
- valueSources.set(value.name, value.sourceLogic)
51
+ // connected entries first, grouped by source logic (groups alphabetical, entries alphabetical
52
+ // within each group), then the logic's own entries alphabetically
53
+ const bySourceLogicThenName = (
54
+ a: { name: string; sourceLogic?: string },
55
+ b: { name: string; sourceLogic?: string },
56
+ ): number => {
57
+ if (!!a.sourceLogic !== !!b.sourceLogic) {
58
+ return a.sourceLogic ? -1 : 1
59
+ }
60
+ if (a.sourceLogic && b.sourceLogic && a.sourceLogic !== b.sourceLogic) {
61
+ return a.sourceLogic < b.sourceLogic ? -1 : 1
62
+ }
63
+ return a.name === b.name ? 0 : a.name < b.name ? -1 : 1
48
64
  }
49
65
 
50
66
  addSection(
51
67
  'Values',
52
- printValues(parsedLogic).members.map((member) =>
53
- withSourceLogicComment(
54
- member,
55
- member.name && ts.isIdentifier(member.name) ? valueSources.get(member.name.text) : undefined,
68
+ [...cleanDuplicateAnyNodes(parsedLogic.reducers.concat(parsedLogic.selectors))]
69
+ .sort(bySourceLogicThenName)
70
+ .map(({ name, typeNode, sourceLogic }) =>
71
+ withSourceLogicComment(
72
+ factory.createPropertySignature(undefined, factory.createIdentifier(name), undefined, typeNode),
73
+ sourceLogic,
74
+ ),
56
75
  ),
57
- ),
58
76
  )
59
77
  addSection(
60
78
  'Actions',
61
- parsedLogic.actions.map(({ name, parameters, returnTypeNode, sourceLogic }) =>
62
- withSourceLogicComment(
63
- factory.createPropertySignature(
64
- undefined,
65
- factory.createIdentifier(name),
66
- undefined,
67
- factory.createFunctionTypeNode(undefined, parameters, returnTypeNode),
79
+ [...parsedLogic.actions]
80
+ .sort(bySourceLogicThenName)
81
+ .map(({ name, parameters, returnTypeNode, sourceLogic }) =>
82
+ withSourceLogicComment(
83
+ factory.createPropertySignature(
84
+ undefined,
85
+ factory.createIdentifier(name),
86
+ undefined,
87
+ factory.createFunctionTypeNode(undefined, parameters, returnTypeNode),
88
+ ),
89
+ sourceLogic,
68
90
  ),
69
- sourceLogic,
70
91
  ),
71
- ),
72
92
  )
73
93
  if (parsedLogic.propsType) {
74
94
  if (ts.isTypeLiteralNode(parsedLogic.propsType)) {
@@ -79,6 +99,39 @@ export function printInlineLogicType(parsedLogic: ParsedLogic): string {
79
99
  }
80
100
  }
81
101
 
102
+ const metaMembers: ts.TypeElement[] = []
103
+ const addMetaProperty = (name: string, typeNode: ts.TypeNode): void => {
104
+ metaMembers.push(
105
+ factory.createPropertySignature(undefined, factory.createIdentifier(name), undefined, typeNode),
106
+ )
107
+ }
108
+ if (parsedLogic.keyType) {
109
+ addMetaProperty('key', printKey(parsedLogic))
110
+ }
111
+ if (parsedLogic.sharedListeners.length > 0) {
112
+ addMetaProperty('sharedListeners', printSharedListeners(parsedLogic))
113
+ }
114
+ if (parsedLogic.selectors.some((selector) => (selector.functionTypes?.length ?? 0) > 0)) {
115
+ addMetaProperty('__keaTypeGenInternalSelectorTypes', printInternalSelectorTypes(parsedLogic))
116
+ }
117
+ if (Object.keys(parsedLogic.extraActions).length > 0) {
118
+ addMetaProperty('__keaTypeGenInternalReducerActions', printInternalReducerActions(parsedLogic))
119
+ }
120
+ if (Object.keys(parsedLogic.extraInput).length > 0) {
121
+ addMetaProperty('__keaTypeGenInternalExtraInput', printInternalExtraInput(parsedLogic))
122
+ }
123
+ if (metaMembers.length > 0) {
124
+ if (!parsedLogic.propsType) {
125
+ typeArguments.push(
126
+ factory.createTypeReferenceNode(factory.createIdentifier('Record'), [
127
+ factory.createKeywordTypeNode(SyntaxKind.StringKeyword),
128
+ factory.createKeywordTypeNode(SyntaxKind.AnyKeyword),
129
+ ]),
130
+ )
131
+ }
132
+ addSection('Meta', metaMembers)
133
+ }
134
+
82
135
  const typeAlias = factory.createTypeAliasDeclaration(
83
136
  [factory.createModifier(SyntaxKind.ExportKeyword)],
84
137
  factory.createIdentifier(parsedLogic.logicTypeName),
@@ -193,6 +246,12 @@ export async function writeInlineLogicTypes(
193
246
  newImportLines.push(`import type { MakeLogicType } from 'kea'`)
194
247
  }
195
248
  }
249
+ if (
250
+ managedLogics.some((parsedLogic) => parsedLogic.sharedListeners.length > 0) &&
251
+ !importsNamedKeaType(importDeclarations, 'BreakPointFunction')
252
+ ) {
253
+ newImportLines.push(`import type { BreakPointFunction } from 'kea'`)
254
+ }
196
255
 
197
256
  // remove the now obsolete `import type { logicType } from './logicType'`, if any
198
257
  const typeFileNameBase = parsedLogics[0].typeFileName.replace(/\.[tj]sx?$/, '')
@@ -279,13 +338,32 @@ export async function writeInlineLogicTypes(
279
338
  * block - semicolons, trailing commas, quotes, line breaks - does not make us rewrite it forever.
280
339
  */
281
340
  function sameGeneratedCode(existingCode: string, generatedCode: string): boolean {
282
- const normalize = (code: string): string =>
283
- code
341
+ const normalize = (code: string): string => {
342
+ const sourceFile = ts.createSourceFile(
343
+ 'inlineLogicType.ts',
344
+ code,
345
+ ts.ScriptTarget.Latest,
346
+ true,
347
+ ts.ScriptKind.TS,
348
+ )
349
+ const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
350
+ const visit: ts.Visitor = (node) =>
351
+ ts.isParenthesizedTypeNode(node)
352
+ ? ts.visitNode(node.type, visit)
353
+ : ts.visitEachChild(node, visit, context)
354
+ return (root) => ts.visitNode(root, visit) as ts.SourceFile
355
+ }
356
+ const transformed = ts.transform(sourceFile, [transformer])
357
+ const normalized = ts
358
+ .createPrinter({ removeComments: true })
359
+ .printFile(transformed.transformed[0])
284
360
  .replace(/"/g, "'")
285
- .replace(/[;,]/g, '')
286
361
  .replace(/\s+/g, '')
287
- // leading pipe of a formatter-reflowed union type, e.g. `selection: | number | EditorRange`
288
- .replace(/([:(<=[])\|/g, '$1')
362
+ .replace(/,(?=[}\])])/g, '')
363
+ .replace(/'([A-Za-z_$][\w$]*)':/g, '$1:')
364
+ transformed.dispose()
365
+ return normalized
366
+ }
289
367
  return normalize(existingCode) === normalize(generatedCode)
290
368
  }
291
369
 
@@ -305,6 +383,10 @@ function getTopLevelStatement(node: ts.Node): ts.Node {
305
383
  }
306
384
 
307
385
  function importsMakeLogicType(importDeclarations: ts.ImportDeclaration[]): boolean {
386
+ return importsNamedKeaType(importDeclarations, 'MakeLogicType')
387
+ }
388
+
389
+ function importsNamedKeaType(importDeclarations: ts.ImportDeclaration[], name: string): boolean {
308
390
  return importDeclarations.some(
309
391
  (declaration) =>
310
392
  ts.isStringLiteralLike(declaration.moduleSpecifier) &&
@@ -312,7 +394,7 @@ function importsMakeLogicType(importDeclarations: ts.ImportDeclaration[]): boole
312
394
  declaration.importClause?.namedBindings &&
313
395
  ts.isNamedImports(declaration.importClause.namedBindings) &&
314
396
  declaration.importClause.namedBindings.elements.some(
315
- (element) => element.name.text === 'MakeLogicType' && !element.propertyName,
397
+ (element) => element.name.text === name && !element.propertyName,
316
398
  ),
317
399
  )
318
400
  }