kea-typegen 3.8.0 → 3.8.1
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/README.md +33 -0
- package/dist/package.json +1 -1
- package/dist/src/__tests__/inline.js +383 -1
- package/dist/src/__tests__/inline.js.map +1 -1
- package/dist/src/cache.d.ts +1 -0
- package/dist/src/cache.js +19 -0
- package/dist/src/cache.js.map +1 -1
- package/dist/src/cli/typegen.js +5 -0
- package/dist/src/cli/typegen.js.map +1 -1
- package/dist/src/print/print.js +6 -4
- package/dist/src/print/print.js.map +1 -1
- package/dist/src/types.d.ts +8 -0
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +27 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/src/visit/visit.js +1 -0
- package/dist/src/visit/visit.js.map +1 -1
- package/dist/src/visit/visitConnect.js +40 -0
- package/dist/src/visit/visitConnect.js.map +1 -1
- package/dist/src/visit/visitSelectors.js +14 -1
- package/dist/src/visit/visitSelectors.js.map +1 -1
- package/dist/src/write/writeInlineLogicTypes.js +32 -9
- package/dist/src/write/writeInlineLogicTypes.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/samples-inline/counterLogic.ts +2 -1
- package/samples-inline/profileLogic.ts +1 -0
- package/samples-inline/searchLogic.ts +2 -0
- package/src/__tests__/inline.ts +482 -1
- package/src/cache.ts +19 -0
- package/src/cli/typegen.ts +6 -0
- package/src/print/print.ts +9 -7
- package/src/types.ts +13 -0
- package/src/utils.ts +43 -1
- package/src/visit/visit.ts +1 -0
- package/src/visit/visitConnect.ts +50 -0
- package/src/visit/visitSelectors.ts +26 -1
- package/src/write/writeInlineLogicTypes.ts +67 -18
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
|
-
|
|
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,
|
package/src/visit/visit.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
|
@@ -29,22 +29,44 @@ export function printInlineLogicType(parsedLogic: ParsedLogic): string {
|
|
|
29
29
|
const addSection = (suffix: string, members: readonly ts.TypeElement[]) => {
|
|
30
30
|
if (members.length > 0) {
|
|
31
31
|
const name = `${parsedLogic.logicName}${suffix}`
|
|
32
|
-
|
|
32
|
+
const declaration = createExportedInterface(name, members)
|
|
33
|
+
ts.addSyntheticLeadingComment(declaration, SyntaxKind.SingleLineCommentTrivia, INLINE_TYPE_MARKER, true)
|
|
34
|
+
statements.push(declaration)
|
|
33
35
|
typeArguments.push(factory.createTypeReferenceNode(factory.createIdentifier(name), undefined))
|
|
34
36
|
} else {
|
|
35
37
|
typeArguments.push(factory.createTypeLiteralNode([]))
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
// mark connected values/actions with the logic they come from, e.g. `user: UserType | null // userLogic`
|
|
42
|
+
const withSourceLogicComment = <T extends ts.TypeElement>(member: T, sourceLogic: string | undefined): T =>
|
|
43
|
+
sourceLogic ? ts.addSyntheticTrailingComment(member, SyntaxKind.SingleLineCommentTrivia, ` ${sourceLogic}`, false) : member
|
|
44
|
+
|
|
45
|
+
const valueSources = new Map<string, string | undefined>()
|
|
46
|
+
for (const value of [...parsedLogic.reducers, ...parsedLogic.selectors]) {
|
|
47
|
+
valueSources.set(value.name, value.sourceLogic)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
addSection(
|
|
51
|
+
'Values',
|
|
52
|
+
printValues(parsedLogic).members.map((member) =>
|
|
53
|
+
withSourceLogicComment(
|
|
54
|
+
member,
|
|
55
|
+
member.name && ts.isIdentifier(member.name) ? valueSources.get(member.name.text) : undefined,
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
40
59
|
addSection(
|
|
41
60
|
'Actions',
|
|
42
|
-
parsedLogic.actions.map(({ name, parameters, returnTypeNode }) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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),
|
|
68
|
+
),
|
|
69
|
+
sourceLogic,
|
|
48
70
|
),
|
|
49
71
|
),
|
|
50
72
|
)
|
|
@@ -57,15 +79,17 @@ export function printInlineLogicType(parsedLogic: ParsedLogic): string {
|
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
81
|
|
|
60
|
-
|
|
61
|
-
factory.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
factory.createTypeReferenceNode(factory.createIdentifier('MakeLogicType'), typeArguments),
|
|
66
|
-
),
|
|
82
|
+
const typeAlias = factory.createTypeAliasDeclaration(
|
|
83
|
+
[factory.createModifier(SyntaxKind.ExportKeyword)],
|
|
84
|
+
factory.createIdentifier(parsedLogic.logicTypeName),
|
|
85
|
+
undefined,
|
|
86
|
+
factory.createTypeReferenceNode(factory.createIdentifier('MakeLogicType'), typeArguments),
|
|
67
87
|
)
|
|
68
|
-
|
|
88
|
+
if (statements.length === 0) {
|
|
89
|
+
// no interfaces at all - the alias itself carries the marker
|
|
90
|
+
ts.addSyntheticLeadingComment(typeAlias, SyntaxKind.SingleLineCommentTrivia, INLINE_TYPE_MARKER, true)
|
|
91
|
+
}
|
|
92
|
+
statements.push(typeAlias)
|
|
69
93
|
return statements.map(nodeToString).join('\n\n')
|
|
70
94
|
}
|
|
71
95
|
|
|
@@ -116,12 +140,14 @@ export async function writeInlineLogicTypes(
|
|
|
116
140
|
const blockStatements = getInlineBlockStatements(existingDeclaration, parsedLogic.logicName)
|
|
117
141
|
const start = getBlockStart(blockStatements[0], sourceFile)
|
|
118
142
|
const end = existingDeclaration.getEnd()
|
|
119
|
-
if (rawCode.slice(start, end)
|
|
143
|
+
if (!sameGeneratedCode(rawCode.slice(start, end), block)) {
|
|
120
144
|
edits.push({ start, end, text: block })
|
|
121
145
|
}
|
|
122
146
|
} else {
|
|
123
147
|
const statement = getTopLevelStatement(parsedLogic.node)
|
|
124
|
-
|
|
148
|
+
// insert above the statement's leading comments, so they stay attached to the logic
|
|
149
|
+
const leadingComments = ts.getLeadingCommentRanges(rawCode, statement.pos) ?? []
|
|
150
|
+
const insertPos = leadingComments.length > 0 ? leadingComments[0].pos : statement.getStart(sourceFile)
|
|
125
151
|
edits.push({ start: insertPos, end: insertPos, text: `${block}\n\n` })
|
|
126
152
|
}
|
|
127
153
|
|
|
@@ -135,6 +161,14 @@ export async function writeInlineLogicTypes(
|
|
|
135
161
|
edits.push({ start, end, text })
|
|
136
162
|
}
|
|
137
163
|
}
|
|
164
|
+
|
|
165
|
+
// annotate untyped selector combiner parameters, e.g. `(counter) => ...` -> `(counter: number) => ...`.
|
|
166
|
+
// MakeLogicType types these as `any`, so without an explicit annotation the inferred
|
|
167
|
+
// value types would degrade to `any` on the next typegen run.
|
|
168
|
+
for (const { parameter, typeNode } of parsedLogic.selectorParamAnnotations) {
|
|
169
|
+
const insertPos = (parameter.questionToken ?? parameter.name).getEnd()
|
|
170
|
+
edits.push({ start: insertPos, end: insertPos, text: `: ${nodeToString(typeNode)}` })
|
|
171
|
+
}
|
|
138
172
|
}
|
|
139
173
|
|
|
140
174
|
if (managedLogics.length > 0) {
|
|
@@ -240,6 +274,21 @@ export async function writeInlineLogicTypes(
|
|
|
240
274
|
return { filesToWrite: 1, writtenFiles: 1 }
|
|
241
275
|
}
|
|
242
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Compare generated code ignoring formatting, so a formatter (prettier, oxfmt, ...) reflowing the
|
|
279
|
+
* block - semicolons, trailing commas, quotes, line breaks - does not make us rewrite it forever.
|
|
280
|
+
*/
|
|
281
|
+
function sameGeneratedCode(existingCode: string, generatedCode: string): boolean {
|
|
282
|
+
const normalize = (code: string): string =>
|
|
283
|
+
code
|
|
284
|
+
.replace(/"/g, "'")
|
|
285
|
+
.replace(/[;,]/g, '')
|
|
286
|
+
.replace(/\s+/g, '')
|
|
287
|
+
// leading pipe of a formatter-reflowed union type, e.g. `selection: | number | EditorRange`
|
|
288
|
+
.replace(/([:(<=[])\|/g, '$1')
|
|
289
|
+
return normalize(existingCode) === normalize(generatedCode)
|
|
290
|
+
}
|
|
291
|
+
|
|
243
292
|
/** Start of the generated block: the kea-typegen marker comment if present, otherwise the declaration itself */
|
|
244
293
|
function getBlockStart(declaration: ts.Statement, sourceFile: ts.SourceFile): number {
|
|
245
294
|
const ranges = ts.getLeadingCommentRanges(sourceFile.text, declaration.pos) ?? []
|