kea-typegen 3.8.0 → 3.8.2

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 (39) hide show
  1. package/README.md +34 -0
  2. package/dist/package.json +1 -1
  3. package/dist/src/__tests__/inline.js +489 -2
  4. package/dist/src/__tests__/inline.js.map +1 -1
  5. package/dist/src/cache.d.ts +1 -0
  6. package/dist/src/cache.js +19 -0
  7. package/dist/src/cache.js.map +1 -1
  8. package/dist/src/cli/typegen.js +5 -0
  9. package/dist/src/cli/typegen.js.map +1 -1
  10. package/dist/src/print/print.js +6 -4
  11. package/dist/src/print/print.js.map +1 -1
  12. package/dist/src/types.d.ts +8 -0
  13. package/dist/src/utils.d.ts +2 -0
  14. package/dist/src/utils.js +27 -1
  15. package/dist/src/utils.js.map +1 -1
  16. package/dist/src/visit/visit.js +1 -0
  17. package/dist/src/visit/visit.js.map +1 -1
  18. package/dist/src/visit/visitConnect.js +40 -0
  19. package/dist/src/visit/visitConnect.js.map +1 -1
  20. package/dist/src/visit/visitSelectors.js +14 -1
  21. package/dist/src/visit/visitSelectors.js.map +1 -1
  22. package/dist/src/write/writeInlineLogicTypes.d.ts +1 -1
  23. package/dist/src/write/writeInlineLogicTypes.js +42 -11
  24. package/dist/src/write/writeInlineLogicTypes.js.map +1 -1
  25. package/dist/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +1 -1
  27. package/samples-inline/counterLogic.ts +3 -2
  28. package/samples-inline/profileLogic.ts +2 -1
  29. package/samples-inline/searchLogic.ts +3 -1
  30. package/src/__tests__/inline.ts +607 -2
  31. package/src/cache.ts +19 -0
  32. package/src/cli/typegen.ts +6 -0
  33. package/src/print/print.ts +9 -7
  34. package/src/types.ts +13 -0
  35. package/src/utils.ts +43 -1
  36. package/src/visit/visit.ts +1 -0
  37. package/src/visit/visitConnect.ts +50 -0
  38. package/src/visit/visitSelectors.ts +26 -1
  39. package/src/write/writeInlineLogicTypes.ts +84 -22
@@ -39,7 +39,8 @@ import { writeTypeImports } from '../write/writeTypeImports'
39
39
  import { writeInlineLogicTypes } from '../write/writeInlineLogicTypes'
40
40
  import { printInternalExtraInput } from './printInternalExtraInput'
41
41
  import { convertToBuilders } from '../write/convertToBuilders'
42
- import { cacheWrittenFile } from '../cache'
42
+ import { cacheWrittenFile, deleteCachedFile } from '../cache'
43
+ import { isInlineFile } from '../utils'
43
44
 
44
45
  const prettierConfigCache = new Map<string, Promise<prettier.Options | null>>()
45
46
 
@@ -78,11 +79,6 @@ export async function printToFiles(
78
79
  groupedByFile[parsedLogic.fileName] = []
79
80
  }
80
81
  groupedByFile[parsedLogic.fileName].push(parsedLogic)
81
-
82
- if (!appOptions.inline) {
83
- // create the Nodes and gather referenced types
84
- printLogicType(parsedLogic, appOptions)
85
- }
86
82
  }
87
83
 
88
84
  // Automatically ignore imports from "node_modules/@types/node", if {types: ["node"]} in tsconfig.json
@@ -120,7 +116,7 @@ export async function printToFiles(
120
116
  for (const [fileName, parsedLogics] of Object.entries(groupedByFile)) {
121
117
  const typeFileName = parsedLogics[0].typeFileName
122
118
 
123
- if (appOptions.inline) {
119
+ if (isInlineFile(appOptions, program.getSourceFile(fileName), parsedLogics)) {
124
120
  // write/update MakeLogicType blocks above the logics instead of writing a logicType.ts file
125
121
  const inlineResponse = await writeInlineLogicTypes(
126
122
  program,
@@ -137,10 +133,16 @@ export async function printToFiles(
137
133
  if (appOptions.delete && fs.existsSync(typeFileName)) {
138
134
  log(`🗑️ Deleting: ${path.relative(process.cwd(), typeFileName)}`)
139
135
  fs.unlinkSync(typeFileName)
136
+ deleteCachedFile(typeFileName, appOptions)
140
137
  }
141
138
  continue
142
139
  }
143
140
 
141
+ // create the Nodes and gather referenced types
142
+ for (const parsedLogic of parsedLogics) {
143
+ printLogicType(parsedLogic, appOptions)
144
+ }
145
+
144
146
  const logicStrings = []
145
147
  const requiredKeys = new Set(['Logic'])
146
148
  for (const parsedLogic of parsedLogics) {
package/src/types.ts CHANGED
@@ -5,11 +5,15 @@ export interface ActionTransform {
5
5
  name: string
6
6
  parameters: ts.ParameterDeclaration[]
7
7
  returnTypeNode: ts.TypeNode
8
+ /** Name of the logic this action was connected from, e.g. "userLogic" */
9
+ sourceLogic?: string
8
10
  }
9
11
 
10
12
  export interface NameType {
11
13
  name: string
12
14
  typeNode: ts.TypeNode | ts.KeywordTypeNode | ts.ParenthesizedTypeNode
15
+ /** Name of the logic this value was connected from, e.g. "userLogic" */
16
+ sourceLogic?: string
13
17
  }
14
18
 
15
19
  export interface ReducerTransform extends NameType {}
@@ -18,6 +22,12 @@ export interface SelectorTransform extends NameType {
18
22
  functionTypes?: { name: string; type: ts.TypeNode }[]
19
23
  }
20
24
 
25
+ /** A selector combiner parameter without a type annotation, plus the inferred type to write into the source */
26
+ export interface SelectorParamAnnotation {
27
+ parameter: ts.ParameterDeclaration
28
+ typeNode: ts.TypeNode
29
+ }
30
+
21
31
  export interface ListenerTransform {
22
32
  name: string
23
33
  action: ts.TypeNode | ts.KeywordTypeNode | ts.ParenthesizedTypeNode
@@ -46,6 +56,7 @@ export interface ParsedLogic {
46
56
  propsType?: ts.TypeNode
47
57
  keyType?: ts.TypeNode
48
58
  typeReferencesToImportFromFiles: Record<string, Set<string>>
59
+ selectorParamAnnotations: SelectorParamAnnotation[]
49
60
  interfaceDeclaration?: ts.InterfaceDeclaration
50
61
  extraActions: Record<string, ts.TypeNode>
51
62
  extraInput: Record<string, { typeNode: ts.TypeNode; withLogicFunction: boolean }>
@@ -80,6 +91,8 @@ export interface AppOptions {
80
91
  convertToBuilders?: boolean
81
92
  /** Write logic types as MakeLogicType blocks above each kea() call, instead of into logicType.ts files */
82
93
  inline?: boolean
94
+ /** Like `inline`, but only for logic files under these paths. Everything else keeps its logicType.ts file. */
95
+ inlinePaths?: string[]
83
96
  /** Show TypeScript errors */
84
97
  showTsErrors?: boolean
85
98
  /** Cache generated logic files into .typegen, use them if generating a logic type for the first time */
package/src/utils.ts CHANGED
@@ -99,6 +99,47 @@ export function isGeneratedInlineBlock(
99
99
  return getInlineBlockStatements(declaration, logicName).some(hasKeaTypegenMarker)
100
100
  }
101
101
 
102
+ /** Find a top-level `type logicType = ...` or `interface logicType {...}` declaration in the file */
103
+ export function findLogicTypeDeclaration(
104
+ sourceFile: ts.SourceFile,
105
+ logicTypeName: string,
106
+ ): ts.TypeAliasDeclaration | ts.InterfaceDeclaration | undefined {
107
+ return sourceFile.statements.find(
108
+ (statement): statement is ts.TypeAliasDeclaration | ts.InterfaceDeclaration =>
109
+ (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement)) &&
110
+ statement.name.text === logicTypeName,
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Should this logic file get inline MakeLogicType blocks instead of a logicType.ts file?
116
+ * True when running with --inline, when the file sits under one of `inlinePaths`, or when the
117
+ * file already carries a generated inline block (so converted files stay inline, whatever the config).
118
+ */
119
+ export function isInlineFile(
120
+ appOptions: AppOptions | undefined,
121
+ sourceFile: ts.SourceFile | undefined,
122
+ parsedLogics: ParsedLogic[],
123
+ ): boolean {
124
+ if (appOptions?.inline) {
125
+ return true
126
+ }
127
+ if (!sourceFile) {
128
+ return false
129
+ }
130
+ for (const inlinePath of appOptions?.inlinePaths ?? []) {
131
+ const resolvedPath = path.resolve(appOptions?.rootPath ?? '.', inlinePath)
132
+ const resolvedFileName = path.resolve(sourceFile.fileName)
133
+ if (resolvedFileName === resolvedPath || resolvedFileName.startsWith(resolvedPath + path.sep)) {
134
+ return true
135
+ }
136
+ }
137
+ return parsedLogics.some((parsedLogic) => {
138
+ const declaration = findLogicTypeDeclaration(sourceFile, parsedLogic.logicTypeName)
139
+ return declaration ? isGeneratedInlineBlock(declaration, parsedLogic.logicName) : false
140
+ })
141
+ }
142
+
102
143
  /**
103
144
  * True if the type argument in `kea<...>()` is a manually written type (e.g. `MakeLogicType<...>`
104
145
  * or a custom interface), as opposed to a type that kea-typegen generates and keeps updated.
@@ -173,7 +214,8 @@ export function getParameterDeclaration(param: ts.ParameterDeclaration) {
173
214
  return factory.createParameterDeclaration(
174
215
  undefined,
175
216
  undefined,
176
- factory.createIdentifier(param.name.getText()),
217
+ // .getText() throws on synthetic nodes (e.g. from signatureToSignatureDeclaration)
218
+ factory.createIdentifier(ts.isIdentifier(param.name) ? param.name.text : param.name.getText()),
177
219
  param.initializer || param.questionToken ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
178
220
  cloneNodeSorted(param.type || factory.createKeywordTypeNode(SyntaxKind.AnyKeyword)),
179
221
  undefined,
@@ -281,6 +281,7 @@ export function visitKeaCalls(
281
281
  hasKeyInLogic: false,
282
282
  hasPathInLogic: false,
283
283
  typeReferencesToImportFromFiles: {},
284
+ selectorParamAnnotations: [],
284
285
  extraActions: {},
285
286
  extraInput: {},
286
287
  extraLogicFields: {},
@@ -1,5 +1,6 @@
1
1
  import { ParsedLogic } from '../types'
2
2
  import * as ts from 'typescript'
3
+ import { NodeBuilderFlags } from 'typescript'
3
4
  import { cloneNodeSorted, gatherImports, getParameterDeclaration } from '../utils'
4
5
  import { Expression, Type } from 'typescript'
5
6
 
@@ -47,6 +48,7 @@ export function visitConnect(parsedLogic: ParsedLogic, type: Type, expression: E
47
48
 
48
49
  const symbol = checker.getSymbolAtLocation(logicReference)
49
50
  const otherLogicType = checker.getTypeOfSymbolAtLocation(symbol, logicReference)
51
+ const sourceLogic = ts.isIdentifier(logicReference) ? logicReference.text : logicReference.getText()
50
52
 
51
53
  if (loaderName === 'actions') {
52
54
  const actionsForLogic = otherLogicType
@@ -85,12 +87,59 @@ export function visitConnect(parsedLogic: ParsedLogic, type: Type, expression: E
85
87
  name: lookup[name],
86
88
  returnTypeNode,
87
89
  parameters,
90
+ sourceLogic,
88
91
  })
89
92
  }
90
93
  }
91
94
  }
92
95
  }
93
96
  }
97
+ } else if (actionsForLogic) {
98
+ // `actionCreators` is not a literal type node we can walk syntactically - e.g. the other
99
+ // logic is typed with MakeLogicType, where actionCreators is a mapped type. Resolve the
100
+ // connected action types semantically instead.
101
+ const actionCreatorsType = checker.getTypeOfSymbolAtLocation(actionsForLogic, logicReference)
102
+ for (const actionProperty of actionCreatorsType.getProperties()) {
103
+ const name = actionProperty.getName()
104
+ if (!lookup[name]) {
105
+ continue
106
+ }
107
+ const actionCreatorType = checker.getTypeOfSymbolAtLocation(actionProperty, logicReference)
108
+ const signature = actionCreatorType.getCallSignatures()[0]
109
+ if (!signature) {
110
+ continue
111
+ }
112
+ const signatureNode = checker.signatureToSignatureDeclaration(
113
+ signature,
114
+ ts.SyntaxKind.FunctionType,
115
+ logicReference,
116
+ NodeBuilderFlags.NoTruncation | NodeBuilderFlags.IgnoreErrors,
117
+ ) as ts.FunctionTypeNode | undefined
118
+ if (!signatureNode) {
119
+ continue
120
+ }
121
+ const parameters = signatureNode.parameters.map((param) => getParameterDeclaration(param))
122
+ let returnType: ts.TypeNode = signatureNode.type
123
+ if (ts.isParenthesizedTypeNode(returnType)) {
124
+ returnType = returnType.type
125
+ }
126
+ if (ts.isTypeLiteralNode(returnType)) {
127
+ const payload = returnType.members.find(
128
+ (m) => m.name && ts.isIdentifier(m.name) && m.name.text === 'payload',
129
+ )
130
+ if (payload && ts.isPropertySignature(payload) && payload.type) {
131
+ const returnTypeNode = cloneNodeSorted(payload.type)
132
+ gatherImports(signatureNode, checker, parsedLogic)
133
+
134
+ parsedLogic.actions.push({
135
+ name: lookup[name],
136
+ returnTypeNode,
137
+ parameters,
138
+ sourceLogic,
139
+ })
140
+ }
141
+ }
142
+ }
94
143
  }
95
144
  }
96
145
 
@@ -109,6 +158,7 @@ export function visitConnect(parsedLogic: ParsedLogic, type: Type, expression: E
109
158
  name: lookup[name],
110
159
  typeNode,
111
160
  functionTypes: [],
161
+ sourceLogic,
112
162
  })
113
163
  }
114
164
  }
@@ -14,8 +14,14 @@ export function visitSelectors(parsedLogic: ParsedLogic, type: Type, expression:
14
14
  const inputFunctionTypeNode = checker.getTypeAtLocation(inputFunction)
15
15
 
16
16
  const selectorInputFunctionType = inputFunctionTypeNode.getCallSignatures()[0]?.getReturnType() as ts.Type
17
+ // IgnoreErrors: the tuple may mention types not referenceable from the logic file
18
+ // (e.g. kea's internal Props); we only consume the elements' return types anyway
17
19
  const selectorInputTypeNode = selectorInputFunctionType
18
- ? checker.typeToTypeNode(selectorInputFunctionType, inputFunction, NodeBuilderFlags.NoTruncation)
20
+ ? checker.typeToTypeNode(
21
+ selectorInputFunctionType,
22
+ inputFunction,
23
+ NodeBuilderFlags.NoTruncation | NodeBuilderFlags.IgnoreErrors,
24
+ )
19
25
  : null
20
26
 
21
27
  let functionNames = []
@@ -75,6 +81,25 @@ export function visitSelectors(parsedLogic: ParsedLogic, type: Type, expression:
75
81
  typeNode,
76
82
  functionTypes,
77
83
  })
84
+
85
+ // combiner parameters without a type annotation, e.g. `(counter) => counter * 2`.
86
+ // In inline mode these get their inferred types written into the source, since
87
+ // MakeLogicType's loose selector typing can no longer infer them contextually.
88
+ computedFunction.parameters.forEach((parameter, index) => {
89
+ const functionType = functionTypes[index]
90
+ if (
91
+ !parameter.type &&
92
+ !parameter.dotDotDotToken &&
93
+ ts.isIdentifier(parameter.name) &&
94
+ functionType &&
95
+ functionType.type.kind !== ts.SyntaxKind.AnyKeyword
96
+ ) {
97
+ parsedLogic.selectorParamAnnotations.push({
98
+ parameter,
99
+ typeNode: functionType.type,
100
+ })
101
+ }
102
+ })
78
103
  }
79
104
  }
80
105
  }
@@ -4,11 +4,10 @@ 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 { cleanDuplicateAnyNodes, getInlineBlockStatements, isGeneratedInlineBlock } from '../utils'
9
8
  import { applyTextEdits, getImportInsertPosition, getTypeArgumentInsertEnd, TextEdit } from './writeTypeImports'
10
9
 
11
- export const INLINE_TYPE_MARKER = ' Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.'
10
+ export const INLINE_TYPE_MARKER = " Generated by kea-typegen. Update if you're an agent, ignore if you're human."
12
11
 
13
12
  /**
14
13
  * Print a block of interfaces (Values, Actions, and Props when it's a literal), plus an
@@ -29,24 +28,60 @@ export function printInlineLogicType(parsedLogic: ParsedLogic): string {
29
28
  const addSection = (suffix: string, members: readonly ts.TypeElement[]) => {
30
29
  if (members.length > 0) {
31
30
  const name = `${parsedLogic.logicName}${suffix}`
32
- statements.push(createExportedInterface(name, members))
31
+ const declaration = createExportedInterface(name, members)
32
+ ts.addSyntheticLeadingComment(declaration, SyntaxKind.SingleLineCommentTrivia, INLINE_TYPE_MARKER, true)
33
+ statements.push(declaration)
33
34
  typeArguments.push(factory.createTypeReferenceNode(factory.createIdentifier(name), undefined))
34
35
  } else {
35
36
  typeArguments.push(factory.createTypeLiteralNode([]))
36
37
  }
37
38
  }
38
39
 
39
- addSection('Values', printValues(parsedLogic).members)
40
+ // mark connected values/actions with the logic they come from, e.g. `user: UserType | null // userLogic`
41
+ const withSourceLogicComment = <T extends ts.TypeElement>(member: T, sourceLogic: string | undefined): T =>
42
+ sourceLogic ? ts.addSyntheticTrailingComment(member, SyntaxKind.SingleLineCommentTrivia, ` ${sourceLogic}`, false) : member
43
+
44
+ // connected entries first, grouped by source logic (groups alphabetical, entries alphabetical
45
+ // within each group), then the logic's own entries alphabetically
46
+ const bySourceLogicThenName = (
47
+ a: { name: string; sourceLogic?: string },
48
+ b: { name: string; sourceLogic?: string },
49
+ ): number => {
50
+ if (!!a.sourceLogic !== !!b.sourceLogic) {
51
+ return a.sourceLogic ? -1 : 1
52
+ }
53
+ if (a.sourceLogic && b.sourceLogic && a.sourceLogic !== b.sourceLogic) {
54
+ return a.sourceLogic < b.sourceLogic ? -1 : 1
55
+ }
56
+ return a.name === b.name ? 0 : a.name < b.name ? -1 : 1
57
+ }
58
+
59
+ addSection(
60
+ 'Values',
61
+ [...cleanDuplicateAnyNodes(parsedLogic.reducers.concat(parsedLogic.selectors))]
62
+ .sort(bySourceLogicThenName)
63
+ .map(({ name, typeNode, sourceLogic }) =>
64
+ withSourceLogicComment(
65
+ factory.createPropertySignature(undefined, factory.createIdentifier(name), undefined, typeNode),
66
+ sourceLogic,
67
+ ),
68
+ ),
69
+ )
40
70
  addSection(
41
71
  'Actions',
42
- parsedLogic.actions.map(({ name, parameters, returnTypeNode }) =>
43
- factory.createPropertySignature(
44
- undefined,
45
- factory.createIdentifier(name),
46
- undefined,
47
- factory.createFunctionTypeNode(undefined, parameters, returnTypeNode),
72
+ [...parsedLogic.actions]
73
+ .sort(bySourceLogicThenName)
74
+ .map(({ name, parameters, returnTypeNode, sourceLogic }) =>
75
+ withSourceLogicComment(
76
+ factory.createPropertySignature(
77
+ undefined,
78
+ factory.createIdentifier(name),
79
+ undefined,
80
+ factory.createFunctionTypeNode(undefined, parameters, returnTypeNode),
81
+ ),
82
+ sourceLogic,
83
+ ),
48
84
  ),
49
- ),
50
85
  )
51
86
  if (parsedLogic.propsType) {
52
87
  if (ts.isTypeLiteralNode(parsedLogic.propsType)) {
@@ -57,15 +92,17 @@ export function printInlineLogicType(parsedLogic: ParsedLogic): string {
57
92
  }
58
93
  }
59
94
 
60
- statements.push(
61
- factory.createTypeAliasDeclaration(
62
- [factory.createModifier(SyntaxKind.ExportKeyword)],
63
- factory.createIdentifier(parsedLogic.logicTypeName),
64
- undefined,
65
- factory.createTypeReferenceNode(factory.createIdentifier('MakeLogicType'), typeArguments),
66
- ),
95
+ const typeAlias = factory.createTypeAliasDeclaration(
96
+ [factory.createModifier(SyntaxKind.ExportKeyword)],
97
+ factory.createIdentifier(parsedLogic.logicTypeName),
98
+ undefined,
99
+ factory.createTypeReferenceNode(factory.createIdentifier('MakeLogicType'), typeArguments),
67
100
  )
68
- ts.addSyntheticLeadingComment(statements[0], SyntaxKind.SingleLineCommentTrivia, INLINE_TYPE_MARKER, true)
101
+ if (statements.length === 0) {
102
+ // no interfaces at all - the alias itself carries the marker
103
+ ts.addSyntheticLeadingComment(typeAlias, SyntaxKind.SingleLineCommentTrivia, INLINE_TYPE_MARKER, true)
104
+ }
105
+ statements.push(typeAlias)
69
106
  return statements.map(nodeToString).join('\n\n')
70
107
  }
71
108
 
@@ -116,12 +153,14 @@ export async function writeInlineLogicTypes(
116
153
  const blockStatements = getInlineBlockStatements(existingDeclaration, parsedLogic.logicName)
117
154
  const start = getBlockStart(blockStatements[0], sourceFile)
118
155
  const end = existingDeclaration.getEnd()
119
- if (rawCode.slice(start, end) !== block) {
156
+ if (!sameGeneratedCode(rawCode.slice(start, end), block)) {
120
157
  edits.push({ start, end, text: block })
121
158
  }
122
159
  } else {
123
160
  const statement = getTopLevelStatement(parsedLogic.node)
124
- const insertPos = statement.getStart(sourceFile)
161
+ // insert above the statement's leading comments, so they stay attached to the logic
162
+ const leadingComments = ts.getLeadingCommentRanges(rawCode, statement.pos) ?? []
163
+ const insertPos = leadingComments.length > 0 ? leadingComments[0].pos : statement.getStart(sourceFile)
125
164
  edits.push({ start: insertPos, end: insertPos, text: `${block}\n\n` })
126
165
  }
127
166
 
@@ -135,6 +174,14 @@ export async function writeInlineLogicTypes(
135
174
  edits.push({ start, end, text })
136
175
  }
137
176
  }
177
+
178
+ // annotate untyped selector combiner parameters, e.g. `(counter) => ...` -> `(counter: number) => ...`.
179
+ // MakeLogicType types these as `any`, so without an explicit annotation the inferred
180
+ // value types would degrade to `any` on the next typegen run.
181
+ for (const { parameter, typeNode } of parsedLogic.selectorParamAnnotations) {
182
+ const insertPos = (parameter.questionToken ?? parameter.name).getEnd()
183
+ edits.push({ start: insertPos, end: insertPos, text: `: ${nodeToString(typeNode)}` })
184
+ }
138
185
  }
139
186
 
140
187
  if (managedLogics.length > 0) {
@@ -240,6 +287,21 @@ export async function writeInlineLogicTypes(
240
287
  return { filesToWrite: 1, writtenFiles: 1 }
241
288
  }
242
289
 
290
+ /**
291
+ * Compare generated code ignoring formatting, so a formatter (prettier, oxfmt, ...) reflowing the
292
+ * block - semicolons, trailing commas, quotes, line breaks - does not make us rewrite it forever.
293
+ */
294
+ function sameGeneratedCode(existingCode: string, generatedCode: string): boolean {
295
+ const normalize = (code: string): string =>
296
+ code
297
+ .replace(/"/g, "'")
298
+ .replace(/[;,]/g, '')
299
+ .replace(/\s+/g, '')
300
+ // leading pipe of a formatter-reflowed union type, e.g. `selection: | number | EditorRange`
301
+ .replace(/([:(<=[])\|/g, '$1')
302
+ return normalize(existingCode) === normalize(generatedCode)
303
+ }
304
+
243
305
  /** Start of the generated block: the kea-typegen marker comment if present, otherwise the declaration itself */
244
306
  function getBlockStart(declaration: ts.Statement, sourceFile: ts.SourceFile): number {
245
307
  const ranges = ts.getLeadingCommentRanges(sourceFile.text, declaration.pos) ?? []