kea-typegen 3.7.1 → 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/CHANGELOG.md +5 -0
- package/README.md +33 -0
- package/dist/package.json +3 -1
- package/dist/src/__tests__/inline.d.ts +1 -0
- package/dist/src/__tests__/inline.js +606 -0
- package/dist/src/__tests__/inline.js.map +1 -0
- 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 +9 -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 +61 -42
- package/dist/src/print/print.js.map +1 -1
- package/dist/src/types.d.ts +9 -0
- package/dist/src/utils.d.ts +6 -0
- package/dist/src/utils.js +82 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/src/visit/visit.js +7 -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 +8 -0
- package/dist/src/write/writeInlineLogicTypes.js +259 -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 +37 -0
- package/samples-inline/manualLogic.ts +20 -0
- package/samples-inline/profileLogic.ts +31 -0
- package/samples-inline/searchLogic.ts +32 -0
- package/samples-inline/tsconfig.json +17 -0
- package/samples-inline/types.ts +5 -0
- package/src/__tests__/inline.ts +747 -0
- package/src/cache.ts +19 -0
- package/src/cli/typegen.ts +10 -0
- package/src/print/print.ts +106 -59
- package/src/types.ts +15 -0
- package/src/utils.ts +124 -1
- package/src/visit/visit.ts +15 -0
- package/src/visit/visitConnect.ts +50 -0
- package/src/visit/visitSelectors.ts +26 -1
- package/src/write/writeInlineLogicTypes.ts +354 -0
- package/src/write/writeTypeImports.ts +4 -4
- package/tsconfig.json +1 -0
package/src/cache.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'path'
|
|
|
3
3
|
import { AppOptions, ParsedLogic } from './types'
|
|
4
4
|
import { Program } from 'typescript'
|
|
5
5
|
import { visitProgram } from './visit/visit'
|
|
6
|
+
import { isInlineFile } from './utils'
|
|
6
7
|
|
|
7
8
|
export function cachePath(appOptions: AppOptions, fileName: string): string {
|
|
8
9
|
return path.join(process.cwd(), '.typegen', path.relative(process.cwd(), fileName))
|
|
@@ -13,8 +14,16 @@ export function restoreCachedTypes(program: Program, appOptions: AppOptions, log
|
|
|
13
14
|
return false
|
|
14
15
|
}
|
|
15
16
|
const parsedLogics = visitProgram(program, appOptions)
|
|
17
|
+
const parsedLogicsByFile: Record<string, ParsedLogic[]> = {}
|
|
18
|
+
for (const pl of parsedLogics) {
|
|
19
|
+
parsedLogicsByFile[pl.fileName] = [...(parsedLogicsByFile[pl.fileName] ?? []), pl]
|
|
20
|
+
}
|
|
16
21
|
let restored = false
|
|
17
22
|
for (const pl of parsedLogics) {
|
|
23
|
+
if (isInlineFile(appOptions, program.getSourceFile(pl.fileName), parsedLogicsByFile[pl.fileName])) {
|
|
24
|
+
// inline files have their types in the logic file itself, there is no logicType.ts to restore
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
18
27
|
if (!fs.existsSync(pl.typeFileName)) {
|
|
19
28
|
const from = cachePath(appOptions, pl.typeFileName)
|
|
20
29
|
if (fs.existsSync(from)) {
|
|
@@ -36,3 +45,13 @@ export function cacheWrittenFile(fileName: string, appOptions: AppOptions) {
|
|
|
36
45
|
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
37
46
|
fs.copyFileSync(fileName, dest)
|
|
38
47
|
}
|
|
48
|
+
|
|
49
|
+
export function deleteCachedFile(fileName: string, appOptions: AppOptions) {
|
|
50
|
+
if (!appOptions.useCache) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
const dest = cachePath(appOptions, fileName)
|
|
54
|
+
if (fs.existsSync(dest)) {
|
|
55
|
+
fs.unlinkSync(dest)
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/cli/typegen.ts
CHANGED
|
@@ -39,6 +39,16 @@ yargs
|
|
|
39
39
|
.option('delete', { describe: 'Delete logicType.ts files without a corresponding logic.ts', type: 'boolean' })
|
|
40
40
|
.option('add-ts-nocheck', { describe: 'Add @ts-nocheck to top of logicType.ts files', type: 'boolean' })
|
|
41
41
|
.option('convert-to-builders', { describe: 'Convert Kea 2.0 inputs to Kea 3.0 logic builders', type: 'boolean' })
|
|
42
|
+
.option('inline', {
|
|
43
|
+
describe: 'Write logic types as MakeLogicType blocks above each kea() call, instead of logicType.ts files',
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
})
|
|
46
|
+
.option('inline-paths', {
|
|
47
|
+
describe:
|
|
48
|
+
'Like --inline, but only for logic files under these paths (relative to --root). ' +
|
|
49
|
+
'Everything else keeps its logicType.ts file.',
|
|
50
|
+
type: 'array',
|
|
51
|
+
})
|
|
42
52
|
.option('import-global-types', {
|
|
43
53
|
describe: 'Add import statements in logicType.ts files for global types (e.g. @types/node)',
|
|
44
54
|
type: 'boolean',
|
package/src/print/print.ts
CHANGED
|
@@ -36,9 +36,11 @@ import { printSharedListeners } from './printSharedListeners'
|
|
|
36
36
|
import { printListeners } from './printListeners'
|
|
37
37
|
import { writePaths } from '../write/writePaths'
|
|
38
38
|
import { writeTypeImports } from '../write/writeTypeImports'
|
|
39
|
+
import { writeInlineLogicTypes } from '../write/writeInlineLogicTypes'
|
|
39
40
|
import { printInternalExtraInput } from './printInternalExtraInput'
|
|
40
41
|
import { convertToBuilders } from '../write/convertToBuilders'
|
|
41
|
-
import { cacheWrittenFile } from '../cache'
|
|
42
|
+
import { cacheWrittenFile, deleteCachedFile } from '../cache'
|
|
43
|
+
import { isInlineFile } from '../utils'
|
|
42
44
|
|
|
43
45
|
const prettierConfigCache = new Map<string, Promise<prettier.Options | null>>()
|
|
44
46
|
|
|
@@ -77,9 +79,6 @@ export async function printToFiles(
|
|
|
77
79
|
groupedByFile[parsedLogic.fileName] = []
|
|
78
80
|
}
|
|
79
81
|
groupedByFile[parsedLogic.fileName].push(parsedLogic)
|
|
80
|
-
|
|
81
|
-
// create the Nodes and gather referenced types
|
|
82
|
-
printLogicType(parsedLogic, appOptions)
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
// Automatically ignore imports from "node_modules/@types/node", if {types: ["node"]} in tsconfig.json
|
|
@@ -105,6 +104,11 @@ export async function printToFiles(
|
|
|
105
104
|
const shouldIgnore = (absolutePath: string) =>
|
|
106
105
|
!!doNotImportFromPaths.find((badPath) => absolutePath.startsWith(badPath))
|
|
107
106
|
|
|
107
|
+
const nodeModulesPath = path.join(
|
|
108
|
+
appOptions.packageJsonPath ? path.dirname(appOptions.packageJsonPath) : appOptions.rootPath,
|
|
109
|
+
'node_modules',
|
|
110
|
+
)
|
|
111
|
+
|
|
108
112
|
let writtenFiles = 0
|
|
109
113
|
let filesToWrite = 0
|
|
110
114
|
let filesToModify = 0
|
|
@@ -112,6 +116,33 @@ export async function printToFiles(
|
|
|
112
116
|
for (const [fileName, parsedLogics] of Object.entries(groupedByFile)) {
|
|
113
117
|
const typeFileName = parsedLogics[0].typeFileName
|
|
114
118
|
|
|
119
|
+
if (isInlineFile(appOptions, program.getSourceFile(fileName), parsedLogics)) {
|
|
120
|
+
// write/update MakeLogicType blocks above the logics instead of writing a logicType.ts file
|
|
121
|
+
const inlineResponse = await writeInlineLogicTypes(
|
|
122
|
+
program,
|
|
123
|
+
appOptions,
|
|
124
|
+
fileName,
|
|
125
|
+
parsedLogics,
|
|
126
|
+
nodeModulesPath,
|
|
127
|
+
shouldIgnore,
|
|
128
|
+
)
|
|
129
|
+
filesToWrite += inlineResponse.filesToWrite
|
|
130
|
+
writtenFiles += inlineResponse.writtenFiles
|
|
131
|
+
|
|
132
|
+
// the logicType.ts file is superseded by the inline block
|
|
133
|
+
if (appOptions.delete && fs.existsSync(typeFileName)) {
|
|
134
|
+
log(`🗑️ Deleting: ${path.relative(process.cwd(), typeFileName)}`)
|
|
135
|
+
fs.unlinkSync(typeFileName)
|
|
136
|
+
deleteCachedFile(typeFileName, appOptions)
|
|
137
|
+
}
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// create the Nodes and gather referenced types
|
|
142
|
+
for (const parsedLogic of parsedLogics) {
|
|
143
|
+
printLogicType(parsedLogic, appOptions)
|
|
144
|
+
}
|
|
145
|
+
|
|
115
146
|
const logicStrings = []
|
|
116
147
|
const requiredKeys = new Set(['Logic'])
|
|
117
148
|
for (const parsedLogic of parsedLogics) {
|
|
@@ -131,62 +162,12 @@ export async function printToFiles(
|
|
|
131
162
|
|
|
132
163
|
const output = logicStrings.join('\n\n')
|
|
133
164
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
165
|
+
const otherimports = buildTypeImportEntries(
|
|
166
|
+
parsedLogics[0].typeReferencesToImportFromFiles,
|
|
167
|
+
parsedLogics[0].typeFileName,
|
|
168
|
+
nodeModulesPath,
|
|
169
|
+
shouldIgnore,
|
|
137
170
|
)
|
|
138
|
-
|
|
139
|
-
const otherimports = Object.entries(parsedLogics[0].typeReferencesToImportFromFiles)
|
|
140
|
-
.filter(([_, list]) => list.size > 0)
|
|
141
|
-
.map(([file, list]) => {
|
|
142
|
-
let finalPath = file
|
|
143
|
-
|
|
144
|
-
// Relative path? Get the absolute.
|
|
145
|
-
if (finalPath.startsWith('.')) {
|
|
146
|
-
finalPath = path.resolve(path.dirname(parsedLogics[0].typeFileName), finalPath)
|
|
147
|
-
}
|
|
148
|
-
if (finalPath.startsWith('node_modules/')) {
|
|
149
|
-
finalPath = path.resolve(path.dirname(nodeModulesPath), finalPath)
|
|
150
|
-
}
|
|
151
|
-
// clean up '../../node_modules/...'
|
|
152
|
-
if (finalPath.startsWith(nodeModulesPath)) {
|
|
153
|
-
finalPath = finalPath.substring(nodeModulesPath.length + 1)
|
|
154
|
-
if (finalPath.startsWith('.pnpm/')) {
|
|
155
|
-
// node_modules/.pnpm/pkg@version/node_modules/* --> *
|
|
156
|
-
const regex = /\.pnpm\/[^/]+@[^\/]+\/node_modules\/(.*)/
|
|
157
|
-
const result = finalPath.match(regex)
|
|
158
|
-
if (result && result.length > 1) {
|
|
159
|
-
finalPath = result[1]
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (finalPath.startsWith('@types/')) {
|
|
163
|
-
finalPath = finalPath.substring(7)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Resolve absolute urls
|
|
168
|
-
if (finalPath.startsWith('/')) {
|
|
169
|
-
finalPath = path.relative(path.dirname(parsedLogics[0].typeFileName), finalPath)
|
|
170
|
-
if (!finalPath.startsWith('.')) {
|
|
171
|
-
finalPath = `./${finalPath}`
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Remove extension
|
|
176
|
-
finalPath = finalPath.replace(/(\.d|)\.tsx?$/, '')
|
|
177
|
-
|
|
178
|
-
// Remove "/index"
|
|
179
|
-
if (finalPath.split('/').length === 2 && finalPath.endsWith('/index')) {
|
|
180
|
-
finalPath = finalPath.substring(0, finalPath.length - 6)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
list: [...list].sort(),
|
|
185
|
-
fullPath: file,
|
|
186
|
-
finalPath,
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
.filter((entry) => !shouldIgnore(entry.fullPath))
|
|
190
171
|
.map(({ list, finalPath }) => `import type { ${list.join(', ')} } from '${finalPath}'`)
|
|
191
172
|
.join('\n')
|
|
192
173
|
|
|
@@ -306,6 +287,72 @@ export async function printToFiles(
|
|
|
306
287
|
return { filesToWrite, writtenFiles, filesToModify }
|
|
307
288
|
}
|
|
308
289
|
|
|
290
|
+
export interface TypeImportEntry {
|
|
291
|
+
list: string[]
|
|
292
|
+
fullPath: string
|
|
293
|
+
finalPath: string
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Resolve gathered type references into import specifiers, relative to the file they'll be imported into */
|
|
297
|
+
export function buildTypeImportEntries(
|
|
298
|
+
typeReferencesToImportFromFiles: Record<string, Set<string>>,
|
|
299
|
+
importIntoFile: string,
|
|
300
|
+
nodeModulesPath: string,
|
|
301
|
+
shouldIgnore: (absolutePath: string) => boolean,
|
|
302
|
+
): TypeImportEntry[] {
|
|
303
|
+
return Object.entries(typeReferencesToImportFromFiles)
|
|
304
|
+
.filter(([_, list]) => list.size > 0)
|
|
305
|
+
.map(([file, list]) => {
|
|
306
|
+
let finalPath = file
|
|
307
|
+
|
|
308
|
+
// Relative path? Get the absolute.
|
|
309
|
+
if (finalPath.startsWith('.')) {
|
|
310
|
+
finalPath = path.resolve(path.dirname(importIntoFile), finalPath)
|
|
311
|
+
}
|
|
312
|
+
if (finalPath.startsWith('node_modules/')) {
|
|
313
|
+
finalPath = path.resolve(path.dirname(nodeModulesPath), finalPath)
|
|
314
|
+
}
|
|
315
|
+
// clean up '../../node_modules/...'
|
|
316
|
+
if (finalPath.startsWith(nodeModulesPath)) {
|
|
317
|
+
finalPath = finalPath.substring(nodeModulesPath.length + 1)
|
|
318
|
+
if (finalPath.startsWith('.pnpm/')) {
|
|
319
|
+
// node_modules/.pnpm/pkg@version/node_modules/* --> *
|
|
320
|
+
const regex = /\.pnpm\/[^/]+@[^\/]+\/node_modules\/(.*)/
|
|
321
|
+
const result = finalPath.match(regex)
|
|
322
|
+
if (result && result.length > 1) {
|
|
323
|
+
finalPath = result[1]
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (finalPath.startsWith('@types/')) {
|
|
327
|
+
finalPath = finalPath.substring(7)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Resolve absolute urls
|
|
332
|
+
if (finalPath.startsWith('/')) {
|
|
333
|
+
finalPath = path.relative(path.dirname(importIntoFile), finalPath)
|
|
334
|
+
if (!finalPath.startsWith('.')) {
|
|
335
|
+
finalPath = `./${finalPath}`
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Remove extension
|
|
340
|
+
finalPath = finalPath.replace(/(\.d|)\.tsx?$/, '')
|
|
341
|
+
|
|
342
|
+
// Remove "/index"
|
|
343
|
+
if (finalPath.split('/').length === 2 && finalPath.endsWith('/index')) {
|
|
344
|
+
finalPath = finalPath.substring(0, finalPath.length - 6)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
list: [...list].sort(),
|
|
349
|
+
fullPath: file,
|
|
350
|
+
finalPath,
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
.filter((entry) => !shouldIgnore(entry.fullPath))
|
|
354
|
+
}
|
|
355
|
+
|
|
309
356
|
export function nodeToString(node: Node): string {
|
|
310
357
|
const printer = createPrinter({ newLine: NewLineKind.LineFeed })
|
|
311
358
|
const sourceFile = createSourceFile('logic.ts', '', ScriptTarget.Latest, false, ScriptKind.TS)
|
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 }>
|
|
@@ -78,6 +89,10 @@ export interface AppOptions {
|
|
|
78
89
|
addTsNocheck?: boolean
|
|
79
90
|
/** Convert kea 2.0 logic input to kea 3.0 builders */
|
|
80
91
|
convertToBuilders?: boolean
|
|
92
|
+
/** Write logic types as MakeLogicType blocks above each kea() call, instead of into logicType.ts files */
|
|
93
|
+
inline?: boolean
|
|
94
|
+
/** Like `inline`, but only for logic files under these paths. Everything else keeps its logicType.ts file. */
|
|
95
|
+
inlinePaths?: string[]
|
|
81
96
|
/** Show TypeScript errors */
|
|
82
97
|
showTsErrors?: boolean
|
|
83
98
|
/** Cache generated logic files into .typegen, use them if generating a logic type for the first time */
|
package/src/utils.ts
CHANGED
|
@@ -53,6 +53,128 @@ export function isKeaCall(node: ts.Node, checker: ts.TypeChecker) {
|
|
|
53
53
|
return ts.isObjectLiteralExpression(input) || ts.isArrayLiteralExpression(input)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** True if the node has a leading "kea-typegen" comment, marking it as generated by us */
|
|
57
|
+
export function hasKeaTypegenMarker(node: ts.Node): boolean {
|
|
58
|
+
const sourceFile = node.getSourceFile()
|
|
59
|
+
const ranges = ts.getLeadingCommentRanges(sourceFile.text, node.pos) ?? []
|
|
60
|
+
return ranges.some(({ pos, end }) => sourceFile.text.slice(pos, end).includes('kea-typegen'))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const INLINE_BLOCK_SUFFIXES = ['Values', 'Actions', 'Props']
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The statements making up an inline logic type block: the `logicType` alias itself, plus any
|
|
67
|
+
* `logicValues` / `logicActions` / `logicProps` interfaces directly above it.
|
|
68
|
+
*/
|
|
69
|
+
export function getInlineBlockStatements(
|
|
70
|
+
declaration: ts.TypeAliasDeclaration | ts.InterfaceDeclaration,
|
|
71
|
+
logicName: string,
|
|
72
|
+
): ts.Statement[] {
|
|
73
|
+
const statements = declaration.getSourceFile().statements
|
|
74
|
+
const index = statements.indexOf(declaration)
|
|
75
|
+
if (index === -1) {
|
|
76
|
+
return [declaration]
|
|
77
|
+
}
|
|
78
|
+
const generatedNames = new Set(INLINE_BLOCK_SUFFIXES.map((suffix) => `${logicName}${suffix}`))
|
|
79
|
+
const block: ts.Statement[] = [declaration]
|
|
80
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
81
|
+
const statement = statements[i]
|
|
82
|
+
if (
|
|
83
|
+
(ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) &&
|
|
84
|
+
generatedNames.has(statement.name.text)
|
|
85
|
+
) {
|
|
86
|
+
block.unshift(statement)
|
|
87
|
+
} else {
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return block
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** True if this local `logicType` declaration is part of a kea-typegen generated inline block */
|
|
95
|
+
export function isGeneratedInlineBlock(
|
|
96
|
+
declaration: ts.TypeAliasDeclaration | ts.InterfaceDeclaration,
|
|
97
|
+
logicName: string,
|
|
98
|
+
): boolean {
|
|
99
|
+
return getInlineBlockStatements(declaration, logicName).some(hasKeaTypegenMarker)
|
|
100
|
+
}
|
|
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
|
+
|
|
143
|
+
/**
|
|
144
|
+
* True if the type argument in `kea<...>()` is a manually written type (e.g. `MakeLogicType<...>`
|
|
145
|
+
* or a custom interface), as opposed to a type that kea-typegen generates and keeps updated.
|
|
146
|
+
* Manually typed logics are skipped entirely.
|
|
147
|
+
*/
|
|
148
|
+
export function isManuallyTypedLogic(
|
|
149
|
+
typeArgument: ts.TypeNode,
|
|
150
|
+
logicTypeName: string,
|
|
151
|
+
checker: ts.TypeChecker,
|
|
152
|
+
): boolean {
|
|
153
|
+
if (!ts.isTypeReferenceNode(typeArgument)) {
|
|
154
|
+
// e.g. kea<{ actions: ... }>(...)
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
if (!ts.isIdentifier(typeArgument.typeName) || typeArgument.typeName.escapedText !== logicTypeName) {
|
|
158
|
+
// e.g. kea<MakeLogicType<Values, Actions>>(...) or kea<MyCustomLogicType>(...)
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
const symbol = checker.getSymbolAtLocation(typeArgument.typeName)
|
|
162
|
+
const declaration = symbol?.getDeclarations()?.[0]
|
|
163
|
+
if (!declaration) {
|
|
164
|
+
// "logicType" that doesn't resolve --> a generated type that doesn't exist yet
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
if (ts.isImportSpecifier(declaration)) {
|
|
168
|
+
// imported logic types are managed by kea-typegen
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
if (ts.isTypeAliasDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) {
|
|
172
|
+
// local declarations are ours only if their block carries the kea-typegen marker (written by --inline mode)
|
|
173
|
+
return !isGeneratedInlineBlock(declaration, logicTypeName.replace(/Type$/, ''))
|
|
174
|
+
}
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
|
|
56
178
|
export function getTypeNodeForNode(node: ts.Node, checker: ts.TypeChecker): ts.TypeNode {
|
|
57
179
|
let typeNode
|
|
58
180
|
if (node) {
|
|
@@ -92,7 +214,8 @@ export function getParameterDeclaration(param: ts.ParameterDeclaration) {
|
|
|
92
214
|
return factory.createParameterDeclaration(
|
|
93
215
|
undefined,
|
|
94
216
|
undefined,
|
|
95
|
-
|
|
217
|
+
// .getText() throws on synthetic nodes (e.g. from signatureToSignatureDeclaration)
|
|
218
|
+
factory.createIdentifier(ts.isIdentifier(param.name) ? param.name.text : param.name.getText()),
|
|
96
219
|
param.initializer || param.questionToken ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
|
|
97
220
|
cloneNodeSorted(param.type || factory.createKeywordTypeNode(SyntaxKind.AnyKeyword)),
|
|
98
221
|
undefined,
|
package/src/visit/visit.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getLogicPathString,
|
|
11
11
|
getTypeNodeForNode,
|
|
12
12
|
isKeaCall,
|
|
13
|
+
isManuallyTypedLogic,
|
|
13
14
|
} from '../utils'
|
|
14
15
|
import { visitActions } from './visitActions'
|
|
15
16
|
import { visitReducers } from './visitReducers'
|
|
@@ -216,6 +217,19 @@ export function visitKeaCalls(
|
|
|
216
217
|
const keaTypeArguments = ts.isCallExpression(node.parent) ? node.parent.typeArguments : []
|
|
217
218
|
const keaTypeArgument = keaTypeArguments?.[0]
|
|
218
219
|
|
|
220
|
+
// if the logic is already manually typed (e.g. kea<MakeLogicType<...>>(...)), leave it alone
|
|
221
|
+
if (keaTypeArgument && isManuallyTypedLogic(keaTypeArgument, logicTypeName, checker)) {
|
|
222
|
+
if (appOptions?.verbose) {
|
|
223
|
+
appOptions.log(
|
|
224
|
+
`🩶 Skipping manually typed logic "${logicName}" in ${path.relative(
|
|
225
|
+
process.cwd(),
|
|
226
|
+
sourceFile.fileName,
|
|
227
|
+
)}`,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
219
233
|
const pathString = getLogicPathString(appOptions, sourceFile.fileName)
|
|
220
234
|
let typeFileName = sourceFile.fileName.replace(/\.[tj]sx?$/, 'Type.ts')
|
|
221
235
|
|
|
@@ -267,6 +281,7 @@ export function visitKeaCalls(
|
|
|
267
281
|
hasKeyInLogic: false,
|
|
268
282
|
hasPathInLogic: false,
|
|
269
283
|
typeReferencesToImportFromFiles: {},
|
|
284
|
+
selectorParamAnnotations: [],
|
|
270
285
|
extraActions: {},
|
|
271
286
|
extraInput: {},
|
|
272
287
|
extraLogicFields: {},
|
|
@@ -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
|
}
|