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.
- package/CHANGELOG.md +9 -0
- package/dist/package.json +3 -1
- package/dist/src/__tests__/inline.d.ts +1 -0
- package/dist/src/__tests__/inline.js +224 -0
- package/dist/src/__tests__/inline.js.map +1 -0
- package/dist/src/__tests__/watch.js +178 -0
- package/dist/src/__tests__/watch.js.map +1 -1
- package/dist/src/cli/typegen.js +4 -0
- package/dist/src/cli/typegen.js.map +1 -1
- package/dist/src/print/print.d.ts +6 -0
- package/dist/src/print/print.js +59 -42
- package/dist/src/print/print.js.map +1 -1
- package/dist/src/typegen.js +38 -2
- package/dist/src/typegen.js.map +1 -1
- package/dist/src/types.d.ts +1 -0
- package/dist/src/utils.d.ts +4 -0
- package/dist/src/utils.js +55 -0
- package/dist/src/utils.js.map +1 -1
- package/dist/src/visit/visit.js +6 -0
- package/dist/src/visit/visit.js.map +1 -1
- package/dist/src/watch.d.ts +22 -0
- package/dist/src/watch.js +183 -0
- package/dist/src/watch.js.map +1 -0
- package/dist/src/write/writeInlineLogicTypes.d.ts +8 -0
- package/dist/src/write/writeInlineLogicTypes.js +236 -0
- package/dist/src/write/writeInlineLogicTypes.js.map +1 -0
- package/dist/src/write/writeTypeImports.d.ts +8 -0
- package/dist/src/write/writeTypeImports.js +3 -0
- package/dist/src/write/writeTypeImports.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/samples/manualTypedLogic.ts +20 -0
- package/samples-inline/counterLogic.ts +36 -0
- package/samples-inline/manualLogic.ts +20 -0
- package/samples-inline/profileLogic.ts +30 -0
- package/samples-inline/searchLogic.ts +30 -0
- package/samples-inline/tsconfig.json +17 -0
- package/samples-inline/types.ts +5 -0
- package/src/__tests__/inline.ts +266 -0
- package/src/__tests__/watch.ts +259 -32
- package/src/cli/typegen.ts +4 -0
- package/src/print/print.ts +102 -57
- package/src/test-support/watch-mode-smoke.js +39 -4
- package/src/test-support/write-mode-smoke.js +6 -1
- package/src/typegen.ts +53 -2
- package/src/types.ts +2 -0
- package/src/utils.ts +81 -0
- package/src/visit/visit.ts +14 -0
- package/src/watch.ts +261 -0
- package/src/write/writeInlineLogicTypes.ts +305 -0
- package/src/write/writeTypeImports.ts +4 -4
- 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)
|