kea-typegen 3.6.6 → 3.7.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 +12 -0
- package/dist/package.json +2 -2
- package/dist/src/__tests__/typegen.d.ts +1 -0
- package/dist/src/__tests__/typegen.js +18 -0
- package/dist/src/__tests__/typegen.js.map +1 -0
- package/dist/src/__tests__/watch.d.ts +1 -0
- package/dist/src/__tests__/watch.js +32 -0
- package/dist/src/__tests__/watch.js.map +1 -0
- package/dist/src/__tests__/write.d.ts +1 -0
- package/dist/src/__tests__/write.js +164 -0
- package/dist/src/__tests__/write.js.map +1 -0
- package/dist/src/cli/typegen.js +10 -1
- package/dist/src/cli/typegen.js.map +1 -1
- package/dist/src/print/print.js +11 -3
- package/dist/src/print/print.js.map +1 -1
- package/dist/src/typegen.d.ts +2 -0
- package/dist/src/typegen.js +65 -16
- package/dist/src/typegen.js.map +1 -1
- package/dist/src/types.d.ts +1 -0
- package/dist/src/visit/visit.js +33 -11
- package/dist/src/visit/visit.js.map +1 -1
- package/dist/src/write/writeTypeImports.js +68 -34
- package/dist/src/write/writeTypeImports.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/__tests__/typegen.ts +22 -0
- package/src/__tests__/watch.ts +44 -0
- package/src/__tests__/write.ts +211 -0
- package/src/cli/typegen.ts +10 -1
- package/src/print/print.ts +14 -5
- package/src/test-support/watch-mode-smoke.js +140 -0
- package/src/test-support/write-mode-smoke.js +147 -0
- package/src/typegen.ts +84 -22
- package/src/types.ts +2 -0
- package/src/visit/visit.ts +54 -13
- package/src/write/writeTypeImports.ts +82 -49
package/src/types.ts
CHANGED
|
@@ -82,6 +82,8 @@ export interface AppOptions {
|
|
|
82
82
|
showTsErrors?: boolean
|
|
83
83
|
/** Cache generated logic files into .typegen, use them if generating a logic type for the first time */
|
|
84
84
|
useCache?: boolean
|
|
85
|
+
/** Skip Prettier formatting while generating logic type files */
|
|
86
|
+
prettier?: boolean
|
|
85
87
|
|
|
86
88
|
log: (message: string) => void
|
|
87
89
|
}
|
package/src/visit/visit.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { visitEvents } from './visitEvents'
|
|
|
25
25
|
import { visitDefaults } from './visitDefaults'
|
|
26
26
|
import { visitSharedListeners } from './visitSharedListeners'
|
|
27
27
|
import { cloneNode } from 'ts-clone-node'
|
|
28
|
-
import {NodeBuilderFlags} from
|
|
28
|
+
import { NodeBuilderFlags } from 'typescript'
|
|
29
29
|
|
|
30
30
|
const visitFunctions = {
|
|
31
31
|
actions: visitActions,
|
|
@@ -44,20 +44,52 @@ const visitFunctions = {
|
|
|
44
44
|
windowValues: visitWindowValues,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Cheap text prefilters so we only do expensive AST + type-checker work on files
|
|
48
|
+
// that can actually contain the thing we're looking for. On large projects most
|
|
49
|
+
// source files contain no Kea logic at all, and walking every node (and resolving
|
|
50
|
+
// a symbol for every identifier via the checker) for them dominates runtime.
|
|
51
|
+
//
|
|
52
|
+
// `kea` is never aliased on import in practice, so a `kea(` / `kea<` text match is a
|
|
53
|
+
// safe-enough gate for the Kea-call pass. If a project does alias `kea`, set
|
|
54
|
+
// KEA_TYPEGEN_NO_PREFILTER=1 to fall back to scanning every file.
|
|
55
|
+
const KEA_CALL_REGEX = /\bkea\s*[<(]/
|
|
56
|
+
const prefilterDisabled = process.env.KEA_TYPEGEN_NO_PREFILTER === '1'
|
|
57
|
+
|
|
58
|
+
function fileMightContainKeaCall(sourceFile: ts.SourceFile): boolean {
|
|
59
|
+
return prefilterDisabled || KEA_CALL_REGEX.test(sourceFile.text)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fileMightContainResetContext(sourceFile: ts.SourceFile): boolean {
|
|
63
|
+
return prefilterDisabled || sourceFile.text.includes('resetContext')
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
export function visitProgram(program: ts.Program, appOptions?: AppOptions): ParsedLogic[] {
|
|
48
67
|
const checker = program.getTypeChecker()
|
|
49
68
|
const parsedLogics: ParsedLogic[] = []
|
|
50
69
|
const pluginModules: PluginModule[] = []
|
|
51
70
|
const typeBuilderModules: TypeBuilderModule[] = []
|
|
71
|
+
const sourceFilePath = appOptions?.sourceFilePath ? path.resolve(appOptions.sourceFilePath) : undefined
|
|
52
72
|
|
|
53
73
|
for (const sourceFile of program.getSourceFiles()) {
|
|
54
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
!sourceFile.isDeclarationFile &&
|
|
76
|
+
!sourceFile.fileName.endsWith('Type.ts') &&
|
|
77
|
+
fileMightContainResetContext(sourceFile)
|
|
78
|
+
) {
|
|
55
79
|
ts.forEachChild(sourceFile, visitResetContext(checker, pluginModules))
|
|
56
80
|
}
|
|
57
81
|
}
|
|
58
82
|
|
|
59
83
|
for (const sourceFile of program.getSourceFiles()) {
|
|
60
|
-
if (
|
|
84
|
+
if (sourceFilePath && path.resolve(sourceFile.fileName) !== sourceFilePath) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
!sourceFile.isDeclarationFile &&
|
|
90
|
+
!sourceFile.fileName.endsWith('Type.ts') &&
|
|
91
|
+
fileMightContainKeaCall(sourceFile)
|
|
92
|
+
) {
|
|
61
93
|
if (appOptions?.verbose) {
|
|
62
94
|
appOptions.log(`👀 Visiting: ${path.relative(process.cwd(), sourceFile.fileName)}`)
|
|
63
95
|
}
|
|
@@ -245,7 +277,6 @@ export function visitKeaCalls(
|
|
|
245
277
|
const calls: {
|
|
246
278
|
name: string
|
|
247
279
|
type: ts.Type
|
|
248
|
-
typeNode: ts.TypeNode | null
|
|
249
280
|
expression: ts.Expression
|
|
250
281
|
typeBuilders: PluginModule[]
|
|
251
282
|
}[] = []
|
|
@@ -261,8 +292,7 @@ export function visitKeaCalls(
|
|
|
261
292
|
}
|
|
262
293
|
const name = symbol.getName()
|
|
263
294
|
const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration)
|
|
264
|
-
|
|
265
|
-
calls.push({ name, type, typeNode, expression: inputProperty.initializer, typeBuilders: [] })
|
|
295
|
+
calls.push({ name, type, expression: inputProperty.initializer, typeBuilders: [] })
|
|
266
296
|
}
|
|
267
297
|
} else if (ts.isArrayLiteralExpression(input)) {
|
|
268
298
|
for (const callExpression of input.elements) {
|
|
@@ -273,7 +303,6 @@ export function visitKeaCalls(
|
|
|
273
303
|
const builderName = callExpression.expression.getText()
|
|
274
304
|
const argument = callExpression.arguments[0]
|
|
275
305
|
const type = checker.getTypeAtLocation(argument)
|
|
276
|
-
const typeNode = type ? checker.typeToTypeNode(type, undefined, NodeBuilderFlags.NoTruncation) : null
|
|
277
306
|
|
|
278
307
|
const identifier = callExpression.expression
|
|
279
308
|
const symbol = checker.getSymbolAtLocation(identifier)
|
|
@@ -348,14 +377,13 @@ export function visitKeaCalls(
|
|
|
348
377
|
calls.push({
|
|
349
378
|
name: builderName,
|
|
350
379
|
type,
|
|
351
|
-
typeNode,
|
|
352
380
|
expression: callExpression.arguments[0],
|
|
353
381
|
typeBuilders: typeBuilders,
|
|
354
382
|
})
|
|
355
383
|
}
|
|
356
384
|
}
|
|
357
385
|
|
|
358
|
-
for (let { name, type,
|
|
386
|
+
for (let { name, type, expression, typeBuilders } of calls) {
|
|
359
387
|
if (name === 'path' || name === 'logicPath') {
|
|
360
388
|
parsedLogic.hasPathInLogic = true
|
|
361
389
|
}
|
|
@@ -363,18 +391,31 @@ export function visitKeaCalls(
|
|
|
363
391
|
parsedLogic.hasKeyInLogic = true
|
|
364
392
|
}
|
|
365
393
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
394
|
+
// Equivalent to the previous `ts.isFunctionTypeNode(typeNode)` check, but without
|
|
395
|
+
// eagerly building a typeNode: a pure function type has call signatures and no
|
|
396
|
+
// members, so we unwrap it to its return type before visiting/printing.
|
|
397
|
+
const callSignatures = type.getCallSignatures()
|
|
398
|
+
if (callSignatures.length > 0 && type.getProperties().length === 0) {
|
|
399
|
+
type = callSignatures[0].getReturnType()
|
|
369
400
|
}
|
|
370
401
|
|
|
371
402
|
visitFunctions[name]?.(parsedLogic, type, expression)
|
|
372
403
|
|
|
404
|
+
let typeNode: ts.TypeNode | null | undefined = undefined
|
|
405
|
+
const getTypeNode = (): ts.TypeNode | null => {
|
|
406
|
+
if (typeNode === undefined) {
|
|
407
|
+
typeNode = type ? checker.typeToTypeNode(type, undefined, NodeBuilderFlags.NoTruncation) : null
|
|
408
|
+
}
|
|
409
|
+
return typeNode
|
|
410
|
+
}
|
|
411
|
+
|
|
373
412
|
const visitKeaPropertyArguments: VisitKeaPropertyArguments = {
|
|
374
413
|
name,
|
|
375
414
|
appOptions,
|
|
376
415
|
type,
|
|
377
|
-
typeNode
|
|
416
|
+
get typeNode() {
|
|
417
|
+
return getTypeNode()
|
|
418
|
+
},
|
|
378
419
|
parsedLogic,
|
|
379
420
|
node: expression,
|
|
380
421
|
checker,
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import { AppOptions, ParsedLogic } from '../types'
|
|
2
2
|
import * as ts from 'typescript'
|
|
3
|
-
import { print, visit } from 'recast'
|
|
4
3
|
import * as osPath from 'path'
|
|
5
4
|
import { runThroughPrettier } from '../print/print'
|
|
6
5
|
import * as fs from 'fs'
|
|
7
|
-
|
|
6
|
+
interface TextEdit {
|
|
7
|
+
start: number
|
|
8
|
+
end: number
|
|
9
|
+
text: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function applyTextEdits(source: string, edits: TextEdit[]): string {
|
|
13
|
+
return edits
|
|
14
|
+
.sort((a, b) => b.start - a.start || b.end - a.end)
|
|
15
|
+
.reduce((output, { start, end, text }) => output.slice(0, start) + text + output.slice(end), source)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getImportPath(importDeclaration: ts.ImportDeclaration): string | null {
|
|
19
|
+
return ts.isStringLiteralLike(importDeclaration.moduleSpecifier) ? importDeclaration.moduleSpecifier.text : null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getImportInsertPosition(sourceFile: ts.SourceFile, rawCode: string): number {
|
|
23
|
+
const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration)
|
|
24
|
+
if (importDeclarations.length > 0) {
|
|
25
|
+
return importDeclarations[importDeclarations.length - 1].getEnd()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const shebangMatch = rawCode.match(/^#!.*(?:\r?\n|$)/)
|
|
29
|
+
return shebangMatch ? shebangMatch[0].length : 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getTypeArgumentInsertEnd(callExpression: ts.CallExpression, sourceFile: ts.SourceFile): number {
|
|
33
|
+
const openParenToken = callExpression
|
|
34
|
+
.getChildren(sourceFile)
|
|
35
|
+
.find((child) => child.kind === ts.SyntaxKind.OpenParenToken)
|
|
36
|
+
|
|
37
|
+
return openParenToken ? openParenToken.getStart(sourceFile) : callExpression.expression.getEnd()
|
|
38
|
+
}
|
|
8
39
|
|
|
9
40
|
export async function writeTypeImports(
|
|
10
41
|
appOptions: AppOptions,
|
|
@@ -15,9 +46,10 @@ export async function writeTypeImports(
|
|
|
15
46
|
) {
|
|
16
47
|
const { log } = appOptions
|
|
17
48
|
const sourceFile = program.getSourceFile(filename)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
49
|
+
if (!sourceFile) {
|
|
50
|
+
throw new Error(`Could not find source file: ${filename}`)
|
|
51
|
+
}
|
|
52
|
+
const rawCode = fs.readFileSync(filename, 'utf8')
|
|
21
53
|
|
|
22
54
|
let importLocation = osPath
|
|
23
55
|
.relative(osPath.dirname(filename), logicsNeedingImports[0].typeFileName)
|
|
@@ -26,56 +58,57 @@ export async function writeTypeImports(
|
|
|
26
58
|
importLocation = `./${importLocation}`
|
|
27
59
|
}
|
|
28
60
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
importPath &&
|
|
39
|
-
osPath.resolve(osPath.dirname(filename), importPath) ===
|
|
40
|
-
osPath.resolve(osPath.dirname(filename), importLocation)
|
|
41
|
-
) {
|
|
42
|
-
foundImport = true
|
|
43
|
-
path.value.importKind = 'type'
|
|
44
|
-
path.value.specifiers = parsedLogics.map((l) =>
|
|
45
|
-
b.importSpecifier(b.identifier(l.logicTypeName), b.identifier(l.logicTypeName)),
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
return false
|
|
49
|
-
},
|
|
61
|
+
const desiredImport = `import type { ${parsedLogics.map((l) => l.logicTypeName).join(', ')} } from '${importLocation}'`
|
|
62
|
+
const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration)
|
|
63
|
+
const matchingImport = importDeclarations.find((importDeclaration) => {
|
|
64
|
+
const importPath = getImportPath(importDeclaration)
|
|
65
|
+
return (
|
|
66
|
+
importPath !== null &&
|
|
67
|
+
osPath.resolve(osPath.dirname(filename), importPath) ===
|
|
68
|
+
osPath.resolve(osPath.dirname(filename), importLocation)
|
|
69
|
+
)
|
|
50
70
|
})
|
|
51
71
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
const edits: TextEdit[] = []
|
|
73
|
+
if (matchingImport) {
|
|
74
|
+
edits.push({
|
|
75
|
+
start: matchingImport.getStart(sourceFile),
|
|
76
|
+
end: matchingImport.getEnd(),
|
|
77
|
+
text: desiredImport,
|
|
78
|
+
})
|
|
79
|
+
} else {
|
|
80
|
+
const insertPos = getImportInsertPosition(sourceFile, rawCode)
|
|
81
|
+
const hasExistingImports = importDeclarations.length > 0
|
|
82
|
+
const importText = hasExistingImports
|
|
83
|
+
? `\n${desiredImport}${rawCode.slice(insertPos, insertPos + 1) === '\n' ? '' : '\n'}`
|
|
84
|
+
: `${desiredImport}\n`
|
|
85
|
+
|
|
86
|
+
edits.push({
|
|
87
|
+
start: insertPos,
|
|
88
|
+
end: insertPos,
|
|
89
|
+
text: importText,
|
|
68
90
|
})
|
|
69
91
|
}
|
|
70
92
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
for (const parsedLogic of logicsNeedingImports) {
|
|
94
|
+
const callExpression = parsedLogic.node.parent
|
|
95
|
+
if (!ts.isCallExpression(callExpression)) {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const typeArgumentStart = callExpression.expression.getEnd()
|
|
100
|
+
const typeArgumentEnd = callExpression.typeArguments
|
|
101
|
+
? getTypeArgumentInsertEnd(callExpression, sourceFile)
|
|
102
|
+
: typeArgumentStart
|
|
103
|
+
|
|
104
|
+
edits.push({
|
|
105
|
+
start: typeArgumentStart,
|
|
106
|
+
end: typeArgumentEnd,
|
|
107
|
+
text: `<${parsedLogic.logicTypeName}>`,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
77
110
|
|
|
78
|
-
const newText = await runThroughPrettier(
|
|
111
|
+
const newText = await runThroughPrettier(applyTextEdits(rawCode, edits), filename)
|
|
79
112
|
fs.writeFileSync(filename, newText)
|
|
80
113
|
|
|
81
114
|
log(`🔥 Import added: ${osPath.relative(process.cwd(), filename)}`)
|