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.
- package/README.md +34 -0
- package/dist/package.json +1 -1
- package/dist/src/__tests__/inline.js +489 -2
- 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.d.ts +1 -1
- package/dist/src/write/writeInlineLogicTypes.js +42 -11
- 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 +3 -2
- package/samples-inline/profileLogic.ts +2 -1
- package/samples-inline/searchLogic.ts +3 -1
- package/src/__tests__/inline.ts +607 -2
- 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 +84 -22
package/src/print/print.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
61
|
-
factory.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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) ?? []
|