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
package/src/watch.ts ADDED
@@ -0,0 +1,261 @@
1
+ import * as fs from 'fs'
2
+ import * as path from 'path'
3
+ import * as ts from 'typescript'
4
+ import { getFilenameForImportDeclaration } from './utils'
5
+
6
+ const KEA_CALL_REGEX = /\bkea\s*[<(]/
7
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mjsx']
8
+ const TYPEGEN_FILE_REGEX = /(^|\.|\/)typegen\.[tj]s$/
9
+ const MAX_INCREMENTAL_CHANGED_FILES = 50
10
+
11
+ export interface WatchFileChange {
12
+ fileName: string
13
+ eventKind: ts.FileWatcherEventKind
14
+ }
15
+
16
+ export type WatchTypegenPlan =
17
+ | { kind: 'full'; reason: string }
18
+ | { kind: 'files'; sourceFilePaths: string[]; reason: string }
19
+ | { kind: 'skip'; reason: string }
20
+
21
+ export interface WatchChangeTracker {
22
+ system: ts.System
23
+ consume(): WatchFileChange[]
24
+ }
25
+
26
+ export function createWatchChangeTracker(baseSystem: ts.System): WatchChangeTracker {
27
+ const changes = new Map<string, WatchFileChange>()
28
+
29
+ const track = (fileName: string, eventKind: ts.FileWatcherEventKind): void => {
30
+ changes.set(path.resolve(fileName), { fileName: path.resolve(fileName), eventKind })
31
+ }
32
+
33
+ const system: ts.System = {
34
+ ...baseSystem,
35
+ watchFile(fileName, callback, pollingInterval, options) {
36
+ return baseSystem.watchFile!(
37
+ fileName,
38
+ (changedFileName, eventKind, modifiedTime) => {
39
+ track(changedFileName, eventKind)
40
+ callback(changedFileName, eventKind, modifiedTime)
41
+ },
42
+ pollingInterval,
43
+ options,
44
+ )
45
+ },
46
+ watchDirectory(fileName, callback, recursive, options) {
47
+ return baseSystem.watchDirectory!(
48
+ fileName,
49
+ (changedFileName) => {
50
+ track(changedFileName, ts.FileWatcherEventKind.Created)
51
+ callback(changedFileName)
52
+ },
53
+ recursive,
54
+ options,
55
+ )
56
+ },
57
+ }
58
+
59
+ return {
60
+ system,
61
+ consume() {
62
+ const currentChanges = [...changes.values()]
63
+ changes.clear()
64
+ return currentChanges
65
+ },
66
+ }
67
+ }
68
+
69
+ export function planWatchTypegenPass(program: ts.Program, changes: WatchFileChange[]): WatchTypegenPlan {
70
+ if (changes.length === 0) {
71
+ return { kind: 'full', reason: 'initial watch pass' }
72
+ }
73
+
74
+ const normalizedChanges = dedupeChanges(changes)
75
+
76
+ if (normalizedChanges.length > MAX_INCREMENTAL_CHANGED_FILES) {
77
+ return { kind: 'full', reason: `${normalizedChanges.length} files changed` }
78
+ }
79
+
80
+ if (normalizedChanges.some((change) => change.eventKind === ts.FileWatcherEventKind.Deleted)) {
81
+ return { kind: 'full', reason: 'deleted file detected' }
82
+ }
83
+
84
+ const sourceFilesByPath = getSourceFilesByPath(program)
85
+ const reverseDependencies = buildReverseDependencyMap(program)
86
+ const queue: string[] = []
87
+
88
+ for (const change of normalizedChanges) {
89
+ const changedPath = path.resolve(change.fileName)
90
+
91
+ if (isTypegenModuleFile(changedPath)) {
92
+ return { kind: 'full', reason: `typegen module changed: ${path.relative(process.cwd(), changedPath)}` }
93
+ }
94
+
95
+ const sourceFile = sourceFilesByPath.get(changedPath)
96
+
97
+ if (sourceFile?.text.includes('resetContext')) {
98
+ return { kind: 'full', reason: `resetContext changed: ${path.relative(process.cwd(), changedPath)}` }
99
+ }
100
+
101
+ if (isGeneratedTypeFile(changedPath)) {
102
+ queue.push(...sourcePathsForTypeFile(changedPath, sourceFilesByPath))
103
+ queue.push(changedPath)
104
+ continue
105
+ }
106
+
107
+ if (sourceFile) {
108
+ if (sourceFile.isDeclarationFile) {
109
+ return {
110
+ kind: 'full',
111
+ reason: `declaration file changed: ${path.relative(process.cwd(), changedPath)}`,
112
+ }
113
+ }
114
+
115
+ queue.push(changedPath)
116
+ continue
117
+ }
118
+
119
+ if (isSourceLikeFile(changedPath) && fs.existsSync(changedPath)) {
120
+ return {
121
+ kind: 'full',
122
+ reason: `new source file outside program: ${path.relative(process.cwd(), changedPath)}`,
123
+ }
124
+ }
125
+ }
126
+
127
+ const affectedLogicFiles = collectAffectedLogicFiles(queue, sourceFilesByPath, reverseDependencies)
128
+
129
+ if (affectedLogicFiles.length === 0) {
130
+ return { kind: 'skip', reason: 'no affected Kea logic files' }
131
+ }
132
+
133
+ return {
134
+ kind: 'files',
135
+ sourceFilePaths: affectedLogicFiles,
136
+ reason: `${affectedLogicFiles.length} affected Kea logic file${affectedLogicFiles.length === 1 ? '' : 's'}`,
137
+ }
138
+ }
139
+
140
+ function dedupeChanges(changes: WatchFileChange[]): WatchFileChange[] {
141
+ const deduped = new Map<string, WatchFileChange>()
142
+
143
+ for (const change of changes) {
144
+ deduped.set(path.resolve(change.fileName), { ...change, fileName: path.resolve(change.fileName) })
145
+ }
146
+
147
+ return [...deduped.values()]
148
+ }
149
+
150
+ function getSourceFilesByPath(program: ts.Program): Map<string, ts.SourceFile> {
151
+ const sourceFilesByPath = new Map<string, ts.SourceFile>()
152
+
153
+ for (const sourceFile of program.getSourceFiles()) {
154
+ sourceFilesByPath.set(path.resolve(sourceFile.fileName), sourceFile)
155
+ }
156
+
157
+ return sourceFilesByPath
158
+ }
159
+
160
+ function buildReverseDependencyMap(program: ts.Program): Map<string, Set<string>> {
161
+ const checker = program.getTypeChecker()
162
+ const reverseDependencies = new Map<string, Set<string>>()
163
+
164
+ for (const sourceFile of program.getSourceFiles()) {
165
+ if (sourceFile.isDeclarationFile) {
166
+ continue
167
+ }
168
+
169
+ const importerPath = path.resolve(sourceFile.fileName)
170
+
171
+ ts.forEachChild(sourceFile, function visit(node) {
172
+ if (ts.isImportDeclaration(node)) {
173
+ const importedFileName = getFilenameForImportDeclaration(checker, node)
174
+
175
+ if (importedFileName) {
176
+ const importedPath = path.resolve(importedFileName)
177
+ let importers = reverseDependencies.get(importedPath)
178
+
179
+ if (!importers) {
180
+ importers = new Set()
181
+ reverseDependencies.set(importedPath, importers)
182
+ }
183
+
184
+ importers.add(importerPath)
185
+ }
186
+ }
187
+
188
+ ts.forEachChild(node, visit)
189
+ })
190
+ }
191
+
192
+ return reverseDependencies
193
+ }
194
+
195
+ function collectAffectedLogicFiles(
196
+ startPaths: string[],
197
+ sourceFilesByPath: Map<string, ts.SourceFile>,
198
+ reverseDependencies: Map<string, Set<string>>,
199
+ ): string[] {
200
+ const visited = new Set<string>()
201
+ const queue = startPaths.map((filePath) => path.resolve(filePath))
202
+ const affectedLogicFiles = new Set<string>()
203
+
204
+ while (queue.length > 0) {
205
+ const currentPath = queue.shift()!
206
+
207
+ if (visited.has(currentPath)) {
208
+ continue
209
+ }
210
+
211
+ visited.add(currentPath)
212
+
213
+ const sourceFile = sourceFilesByPath.get(currentPath)
214
+ if (
215
+ sourceFile &&
216
+ !sourceFile.isDeclarationFile &&
217
+ !isGeneratedTypeFile(currentPath) &&
218
+ sourceFileMightContainKeaCall(sourceFile)
219
+ ) {
220
+ affectedLogicFiles.add(currentPath)
221
+ }
222
+
223
+ if (isGeneratedTypeFile(currentPath)) {
224
+ queue.push(...sourcePathsForTypeFile(currentPath, sourceFilesByPath))
225
+ }
226
+
227
+ for (const importerPath of reverseDependencies.get(currentPath) ?? []) {
228
+ queue.push(importerPath)
229
+ }
230
+ }
231
+
232
+ return [...affectedLogicFiles].sort()
233
+ }
234
+
235
+ function sourcePathsForTypeFile(typeFilePath: string, sourceFilesByPath: Map<string, ts.SourceFile>): string[] {
236
+ if (!isGeneratedTypeFile(typeFilePath)) {
237
+ return []
238
+ }
239
+
240
+ const sourceBasePath = typeFilePath.slice(0, -'Type.ts'.length)
241
+
242
+ return SOURCE_EXTENSIONS.map((extension) => `${sourceBasePath}${extension}`).filter((sourcePath) =>
243
+ sourceFilesByPath.has(path.resolve(sourcePath)),
244
+ )
245
+ }
246
+
247
+ function isGeneratedTypeFile(filePath: string): boolean {
248
+ return filePath.endsWith('Type.ts')
249
+ }
250
+
251
+ function isSourceLikeFile(filePath: string): boolean {
252
+ return SOURCE_EXTENSIONS.some((extension) => filePath.endsWith(extension))
253
+ }
254
+
255
+ function isTypegenModuleFile(filePath: string): boolean {
256
+ return TYPEGEN_FILE_REGEX.test(filePath.split(path.sep).join('/'))
257
+ }
258
+
259
+ function sourceFileMightContainKeaCall(sourceFile: ts.SourceFile): boolean {
260
+ return KEA_CALL_REGEX.test(sourceFile.text)
261
+ }
@@ -0,0 +1,305 @@
1
+ import * as ts from 'typescript'
2
+ import * as fs from 'fs'
3
+ import * as osPath from 'path'
4
+ import { factory, SyntaxKind } from 'typescript'
5
+ import { AppOptions, ParsedLogic } from '../types'
6
+ import { buildTypeImportEntries, nodeToString, runThroughPrettier } from '../print/print'
7
+ import { printValues } from '../print/printValues'
8
+ import { getInlineBlockStatements, isGeneratedInlineBlock } from '../utils'
9
+ import { applyTextEdits, getImportInsertPosition, getTypeArgumentInsertEnd, TextEdit } from './writeTypeImports'
10
+
11
+ export const INLINE_TYPE_MARKER = ' Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.'
12
+
13
+ /**
14
+ * Print a block of interfaces (Values, Actions, and Props when it's a literal), plus an
15
+ * `export type logicType = MakeLogicType<logicValues, logicActions, logicProps>` alias tying them together.
16
+ */
17
+ export function printInlineLogicType(parsedLogic: ParsedLogic): string {
18
+ const statements: ts.Statement[] = []
19
+ const typeArguments: ts.TypeNode[] = []
20
+
21
+ const createExportedInterface = (name: string, members: readonly ts.TypeElement[]) =>
22
+ factory.createInterfaceDeclaration(
23
+ [factory.createModifier(SyntaxKind.ExportKeyword)],
24
+ factory.createIdentifier(name),
25
+ undefined,
26
+ undefined,
27
+ members,
28
+ )
29
+ const addSection = (suffix: string, members: readonly ts.TypeElement[]) => {
30
+ if (members.length > 0) {
31
+ const name = `${parsedLogic.logicName}${suffix}`
32
+ statements.push(createExportedInterface(name, members))
33
+ typeArguments.push(factory.createTypeReferenceNode(factory.createIdentifier(name), undefined))
34
+ } else {
35
+ typeArguments.push(factory.createTypeLiteralNode([]))
36
+ }
37
+ }
38
+
39
+ addSection('Values', printValues(parsedLogic).members)
40
+ addSection(
41
+ '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),
48
+ ),
49
+ ),
50
+ )
51
+ if (parsedLogic.propsType) {
52
+ if (ts.isTypeLiteralNode(parsedLogic.propsType)) {
53
+ addSection('Props', parsedLogic.propsType.members)
54
+ } else {
55
+ // props are already a named type (e.g. `props: {} as MyProps`) - reference it directly
56
+ typeArguments.push(parsedLogic.propsType)
57
+ }
58
+ }
59
+
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
+ ),
67
+ )
68
+ ts.addSyntheticLeadingComment(statements[0], SyntaxKind.SingleLineCommentTrivia, INLINE_TYPE_MARKER, true)
69
+ return statements.map(nodeToString).join('\n\n')
70
+ }
71
+
72
+ /**
73
+ * Write/update generated MakeLogicType blocks above every logic in the file,
74
+ * instead of writing a separate logicType.ts file.
75
+ */
76
+ export async function writeInlineLogicTypes(
77
+ program: ts.Program,
78
+ appOptions: AppOptions,
79
+ filename: string,
80
+ parsedLogics: ParsedLogic[],
81
+ nodeModulesPath: string,
82
+ shouldIgnore: (absolutePath: string) => boolean,
83
+ ): Promise<{ filesToWrite: number; writtenFiles: number }> {
84
+ const { log } = appOptions
85
+ const unchanged = { filesToWrite: 0, writtenFiles: 0 }
86
+
87
+ if (!filename.match(/\.tsx?$/)) {
88
+ return unchanged
89
+ }
90
+ const sourceFile = program.getSourceFile(filename)
91
+ if (!sourceFile) {
92
+ throw new Error(`Could not find source file: ${filename}`)
93
+ }
94
+ const rawCode = sourceFile.text
95
+ const edits: TextEdit[] = []
96
+ const managedLogics: ParsedLogic[] = []
97
+
98
+ for (const parsedLogic of parsedLogics) {
99
+ const existingDeclaration = sourceFile.statements.find(
100
+ (statement): statement is ts.TypeAliasDeclaration | ts.InterfaceDeclaration =>
101
+ (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement)) &&
102
+ statement.name.text === parsedLogic.logicTypeName,
103
+ )
104
+ if (existingDeclaration && !isGeneratedInlineBlock(existingDeclaration, parsedLogic.logicName)) {
105
+ // a manually written type with the same name - leave the logic alone
106
+ continue
107
+ }
108
+ managedLogics.push(parsedLogic)
109
+
110
+ const printedBlock = printInlineLogicType(parsedLogic)
111
+ const block = (
112
+ appOptions.prettier === false ? printedBlock : await runThroughPrettier(printedBlock, filename)
113
+ ).trimEnd()
114
+
115
+ if (existingDeclaration) {
116
+ const blockStatements = getInlineBlockStatements(existingDeclaration, parsedLogic.logicName)
117
+ const start = getBlockStart(blockStatements[0], sourceFile)
118
+ const end = existingDeclaration.getEnd()
119
+ if (rawCode.slice(start, end) !== block) {
120
+ edits.push({ start, end, text: block })
121
+ }
122
+ } else {
123
+ const statement = getTopLevelStatement(parsedLogic.node)
124
+ const insertPos = statement.getStart(sourceFile)
125
+ edits.push({ start: insertPos, end: insertPos, text: `${block}\n\n` })
126
+ }
127
+
128
+ // make sure the kea call is typed with the local logic type: kea<logicType>(...)
129
+ const callExpression = parsedLogic.node.parent
130
+ if (ts.isCallExpression(callExpression)) {
131
+ const start = callExpression.expression.getEnd()
132
+ const end = callExpression.typeArguments ? getTypeArgumentInsertEnd(callExpression, sourceFile) : start
133
+ const text = `<${parsedLogic.logicTypeName}>`
134
+ if (rawCode.slice(start, end) !== text) {
135
+ edits.push({ start, end, text })
136
+ }
137
+ }
138
+ }
139
+
140
+ if (managedLogics.length > 0) {
141
+ const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration)
142
+ const newImportLines: string[] = []
143
+
144
+ // make sure MakeLogicType is imported from kea
145
+ if (!importsMakeLogicType(importDeclarations)) {
146
+ const keaNamedImport = importDeclarations.find(
147
+ (declaration) =>
148
+ ts.isStringLiteralLike(declaration.moduleSpecifier) &&
149
+ declaration.moduleSpecifier.text === 'kea' &&
150
+ declaration.importClause?.namedBindings &&
151
+ ts.isNamedImports(declaration.importClause.namedBindings) &&
152
+ declaration.importClause.namedBindings.elements.length > 0,
153
+ )
154
+ if (keaNamedImport) {
155
+ const firstElement = (keaNamedImport.importClause.namedBindings as ts.NamedImports).elements[0]
156
+ const pos = firstElement.getStart(sourceFile)
157
+ edits.push({ start: pos, end: pos, text: 'MakeLogicType, ' })
158
+ } else {
159
+ newImportLines.push(`import type { MakeLogicType } from 'kea'`)
160
+ }
161
+ }
162
+
163
+ // remove the now obsolete `import type { logicType } from './logicType'`, if any
164
+ const typeFileNameBase = parsedLogics[0].typeFileName.replace(/\.[tj]sx?$/, '')
165
+ for (const declaration of importDeclarations) {
166
+ if (!ts.isStringLiteralLike(declaration.moduleSpecifier)) {
167
+ continue
168
+ }
169
+ const specifier = declaration.moduleSpecifier.text
170
+ if (!specifier.startsWith('.')) {
171
+ continue
172
+ }
173
+ if (osPath.resolve(osPath.dirname(filename), specifier) === typeFileNameBase) {
174
+ const start = declaration.getStart(sourceFile)
175
+ let end = declaration.getEnd()
176
+ if (rawCode[end] === '\n') {
177
+ end += 1
178
+ }
179
+ edits.push({ start, end, text: '' })
180
+ }
181
+ }
182
+
183
+ // import referenced types that are not yet available in this file
184
+ const knownNames = collectKnownTypeNames(sourceFile)
185
+ for (const parsedLogic of parsedLogics) {
186
+ knownNames.add(parsedLogic.logicTypeName)
187
+ }
188
+
189
+ const mergedTypeReferences: Record<string, Set<string>> = {}
190
+ for (const parsedLogic of managedLogics) {
191
+ for (const [file, names] of Object.entries(parsedLogic.typeReferencesToImportFromFiles)) {
192
+ mergedTypeReferences[file] = mergedTypeReferences[file] ?? new Set()
193
+ for (const name of names) {
194
+ mergedTypeReferences[file].add(name)
195
+ }
196
+ }
197
+ }
198
+
199
+ for (const entry of buildTypeImportEntries(mergedTypeReferences, filename, nodeModulesPath, shouldIgnore)) {
200
+ const absolutePath = entry.fullPath.startsWith('.')
201
+ ? osPath.resolve(osPath.dirname(filename), entry.fullPath)
202
+ : entry.fullPath
203
+ if (absolutePath === filename || absolutePath === parsedLogics[0].typeFileName) {
204
+ // types declared in this very file, or in the logicType.ts file we're replacing
205
+ continue
206
+ }
207
+ const missing = entry.list.filter((name) => !knownNames.has(name))
208
+ missing.forEach((name) => knownNames.add(name))
209
+ if (missing.length > 0) {
210
+ newImportLines.push(`import type { ${missing.join(', ')} } from '${entry.finalPath}'`)
211
+ }
212
+ }
213
+
214
+ if (newImportLines.length > 0) {
215
+ const insertPos = getImportInsertPosition(sourceFile, rawCode)
216
+ const text =
217
+ importDeclarations.length > 0
218
+ ? `\n${newImportLines.join('\n')}${rawCode.slice(insertPos, insertPos + 1) === '\n' ? '' : '\n'}`
219
+ : `${newImportLines.join('\n')}\n`
220
+ edits.push({ start: insertPos, end: insertPos, text })
221
+ }
222
+ }
223
+
224
+ if (edits.length === 0) {
225
+ if (appOptions.verbose) {
226
+ log(`🤷 Unchanged: ${osPath.relative(process.cwd(), filename)}`)
227
+ }
228
+ return unchanged
229
+ }
230
+
231
+ if (!appOptions.write) {
232
+ log(`❌ Will not write inline logic types: ${osPath.relative(process.cwd(), filename)}`)
233
+ return { filesToWrite: 1, writtenFiles: 0 }
234
+ }
235
+
236
+ const editedCode = applyTextEdits(rawCode, edits)
237
+ const newText = appOptions.prettier === false ? editedCode : await runThroughPrettier(editedCode, filename)
238
+ fs.writeFileSync(filename, newText)
239
+ log(`🔥 Writing inline logic types: ${osPath.relative(process.cwd(), filename)}`)
240
+ return { filesToWrite: 1, writtenFiles: 1 }
241
+ }
242
+
243
+ /** Start of the generated block: the kea-typegen marker comment if present, otherwise the declaration itself */
244
+ function getBlockStart(declaration: ts.Statement, sourceFile: ts.SourceFile): number {
245
+ const ranges = ts.getLeadingCommentRanges(sourceFile.text, declaration.pos) ?? []
246
+ const marker = ranges.find(({ pos, end }) => sourceFile.text.slice(pos, end).includes('kea-typegen'))
247
+ return marker ? marker.pos : declaration.getStart(sourceFile)
248
+ }
249
+
250
+ function getTopLevelStatement(node: ts.Node): ts.Node {
251
+ let statement = node
252
+ while (statement.parent && !ts.isSourceFile(statement.parent)) {
253
+ statement = statement.parent
254
+ }
255
+ return statement
256
+ }
257
+
258
+ function importsMakeLogicType(importDeclarations: ts.ImportDeclaration[]): boolean {
259
+ return importDeclarations.some(
260
+ (declaration) =>
261
+ ts.isStringLiteralLike(declaration.moduleSpecifier) &&
262
+ declaration.moduleSpecifier.text === 'kea' &&
263
+ declaration.importClause?.namedBindings &&
264
+ ts.isNamedImports(declaration.importClause.namedBindings) &&
265
+ declaration.importClause.namedBindings.elements.some(
266
+ (element) => element.name.text === 'MakeLogicType' && !element.propertyName,
267
+ ),
268
+ )
269
+ }
270
+
271
+ /** All type-ish names already bound at the top level of the file */
272
+ function collectKnownTypeNames(sourceFile: ts.SourceFile): Set<string> {
273
+ const names = new Set<string>()
274
+ for (const statement of sourceFile.statements) {
275
+ if (ts.isImportDeclaration(statement) && statement.importClause) {
276
+ if (statement.importClause.name) {
277
+ names.add(statement.importClause.name.text)
278
+ }
279
+ const namedBindings = statement.importClause.namedBindings
280
+ if (namedBindings) {
281
+ if (ts.isNamedImports(namedBindings)) {
282
+ namedBindings.elements.forEach((element) => names.add(element.name.text))
283
+ } else {
284
+ names.add(namedBindings.name.text)
285
+ }
286
+ }
287
+ } else if (
288
+ (ts.isTypeAliasDeclaration(statement) ||
289
+ ts.isInterfaceDeclaration(statement) ||
290
+ ts.isClassDeclaration(statement) ||
291
+ ts.isEnumDeclaration(statement) ||
292
+ ts.isFunctionDeclaration(statement)) &&
293
+ statement.name
294
+ ) {
295
+ names.add(statement.name.text)
296
+ } else if (ts.isVariableStatement(statement)) {
297
+ for (const declaration of statement.declarationList.declarations) {
298
+ if (ts.isIdentifier(declaration.name)) {
299
+ names.add(declaration.name.text)
300
+ }
301
+ }
302
+ }
303
+ }
304
+ return names
305
+ }
@@ -3,13 +3,13 @@ import * as ts from 'typescript'
3
3
  import * as osPath from 'path'
4
4
  import { runThroughPrettier } from '../print/print'
5
5
  import * as fs from 'fs'
6
- interface TextEdit {
6
+ export interface TextEdit {
7
7
  start: number
8
8
  end: number
9
9
  text: string
10
10
  }
11
11
 
12
- function applyTextEdits(source: string, edits: TextEdit[]): string {
12
+ export function applyTextEdits(source: string, edits: TextEdit[]): string {
13
13
  return edits
14
14
  .sort((a, b) => b.start - a.start || b.end - a.end)
15
15
  .reduce((output, { start, end, text }) => output.slice(0, start) + text + output.slice(end), source)
@@ -19,7 +19,7 @@ function getImportPath(importDeclaration: ts.ImportDeclaration): string | null {
19
19
  return ts.isStringLiteralLike(importDeclaration.moduleSpecifier) ? importDeclaration.moduleSpecifier.text : null
20
20
  }
21
21
 
22
- function getImportInsertPosition(sourceFile: ts.SourceFile, rawCode: string): number {
22
+ export function getImportInsertPosition(sourceFile: ts.SourceFile, rawCode: string): number {
23
23
  const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration)
24
24
  if (importDeclarations.length > 0) {
25
25
  return importDeclarations[importDeclarations.length - 1].getEnd()
@@ -29,7 +29,7 @@ function getImportInsertPosition(sourceFile: ts.SourceFile, rawCode: string): nu
29
29
  return shebangMatch ? shebangMatch[0].length : 0
30
30
  }
31
31
 
32
- function getTypeArgumentInsertEnd(callExpression: ts.CallExpression, sourceFile: ts.SourceFile): number {
32
+ export function getTypeArgumentInsertEnd(callExpression: ts.CallExpression, sourceFile: ts.SourceFile): number {
33
33
  const openParenToken = callExpression
34
34
  .getChildren(sourceFile)
35
35
  .find((child) => child.kind === ts.SyntaxKind.OpenParenToken)
package/tsconfig.json CHANGED
@@ -17,6 +17,7 @@
17
17
  "node_modules",
18
18
  "dist",
19
19
  "samples",
20
+ "samples-inline",
20
21
  "form-plugin"
21
22
  ]
22
23
  }