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/print/print.ts
CHANGED
|
@@ -36,6 +36,7 @@ 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
42
|
import { cacheWrittenFile } from '../cache'
|
|
@@ -78,8 +79,10 @@ export async function printToFiles(
|
|
|
78
79
|
}
|
|
79
80
|
groupedByFile[parsedLogic.fileName].push(parsedLogic)
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
if (!appOptions.inline) {
|
|
83
|
+
// create the Nodes and gather referenced types
|
|
84
|
+
printLogicType(parsedLogic, appOptions)
|
|
85
|
+
}
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// Automatically ignore imports from "node_modules/@types/node", if {types: ["node"]} in tsconfig.json
|
|
@@ -105,6 +108,11 @@ export async function printToFiles(
|
|
|
105
108
|
const shouldIgnore = (absolutePath: string) =>
|
|
106
109
|
!!doNotImportFromPaths.find((badPath) => absolutePath.startsWith(badPath))
|
|
107
110
|
|
|
111
|
+
const nodeModulesPath = path.join(
|
|
112
|
+
appOptions.packageJsonPath ? path.dirname(appOptions.packageJsonPath) : appOptions.rootPath,
|
|
113
|
+
'node_modules',
|
|
114
|
+
)
|
|
115
|
+
|
|
108
116
|
let writtenFiles = 0
|
|
109
117
|
let filesToWrite = 0
|
|
110
118
|
let filesToModify = 0
|
|
@@ -112,6 +120,27 @@ export async function printToFiles(
|
|
|
112
120
|
for (const [fileName, parsedLogics] of Object.entries(groupedByFile)) {
|
|
113
121
|
const typeFileName = parsedLogics[0].typeFileName
|
|
114
122
|
|
|
123
|
+
if (appOptions.inline) {
|
|
124
|
+
// write/update MakeLogicType blocks above the logics instead of writing a logicType.ts file
|
|
125
|
+
const inlineResponse = await writeInlineLogicTypes(
|
|
126
|
+
program,
|
|
127
|
+
appOptions,
|
|
128
|
+
fileName,
|
|
129
|
+
parsedLogics,
|
|
130
|
+
nodeModulesPath,
|
|
131
|
+
shouldIgnore,
|
|
132
|
+
)
|
|
133
|
+
filesToWrite += inlineResponse.filesToWrite
|
|
134
|
+
writtenFiles += inlineResponse.writtenFiles
|
|
135
|
+
|
|
136
|
+
// the logicType.ts file is superseded by the inline block
|
|
137
|
+
if (appOptions.delete && fs.existsSync(typeFileName)) {
|
|
138
|
+
log(`🗑️ Deleting: ${path.relative(process.cwd(), typeFileName)}`)
|
|
139
|
+
fs.unlinkSync(typeFileName)
|
|
140
|
+
}
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
115
144
|
const logicStrings = []
|
|
116
145
|
const requiredKeys = new Set(['Logic'])
|
|
117
146
|
for (const parsedLogic of parsedLogics) {
|
|
@@ -131,62 +160,12 @@ export async function printToFiles(
|
|
|
131
160
|
|
|
132
161
|
const output = logicStrings.join('\n\n')
|
|
133
162
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
const otherimports = buildTypeImportEntries(
|
|
164
|
+
parsedLogics[0].typeReferencesToImportFromFiles,
|
|
165
|
+
parsedLogics[0].typeFileName,
|
|
166
|
+
nodeModulesPath,
|
|
167
|
+
shouldIgnore,
|
|
137
168
|
)
|
|
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
169
|
.map(({ list, finalPath }) => `import type { ${list.join(', ')} } from '${finalPath}'`)
|
|
191
170
|
.join('\n')
|
|
192
171
|
|
|
@@ -306,6 +285,72 @@ export async function printToFiles(
|
|
|
306
285
|
return { filesToWrite, writtenFiles, filesToModify }
|
|
307
286
|
}
|
|
308
287
|
|
|
288
|
+
export interface TypeImportEntry {
|
|
289
|
+
list: string[]
|
|
290
|
+
fullPath: string
|
|
291
|
+
finalPath: string
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Resolve gathered type references into import specifiers, relative to the file they'll be imported into */
|
|
295
|
+
export function buildTypeImportEntries(
|
|
296
|
+
typeReferencesToImportFromFiles: Record<string, Set<string>>,
|
|
297
|
+
importIntoFile: string,
|
|
298
|
+
nodeModulesPath: string,
|
|
299
|
+
shouldIgnore: (absolutePath: string) => boolean,
|
|
300
|
+
): TypeImportEntry[] {
|
|
301
|
+
return Object.entries(typeReferencesToImportFromFiles)
|
|
302
|
+
.filter(([_, list]) => list.size > 0)
|
|
303
|
+
.map(([file, list]) => {
|
|
304
|
+
let finalPath = file
|
|
305
|
+
|
|
306
|
+
// Relative path? Get the absolute.
|
|
307
|
+
if (finalPath.startsWith('.')) {
|
|
308
|
+
finalPath = path.resolve(path.dirname(importIntoFile), finalPath)
|
|
309
|
+
}
|
|
310
|
+
if (finalPath.startsWith('node_modules/')) {
|
|
311
|
+
finalPath = path.resolve(path.dirname(nodeModulesPath), finalPath)
|
|
312
|
+
}
|
|
313
|
+
// clean up '../../node_modules/...'
|
|
314
|
+
if (finalPath.startsWith(nodeModulesPath)) {
|
|
315
|
+
finalPath = finalPath.substring(nodeModulesPath.length + 1)
|
|
316
|
+
if (finalPath.startsWith('.pnpm/')) {
|
|
317
|
+
// node_modules/.pnpm/pkg@version/node_modules/* --> *
|
|
318
|
+
const regex = /\.pnpm\/[^/]+@[^\/]+\/node_modules\/(.*)/
|
|
319
|
+
const result = finalPath.match(regex)
|
|
320
|
+
if (result && result.length > 1) {
|
|
321
|
+
finalPath = result[1]
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (finalPath.startsWith('@types/')) {
|
|
325
|
+
finalPath = finalPath.substring(7)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Resolve absolute urls
|
|
330
|
+
if (finalPath.startsWith('/')) {
|
|
331
|
+
finalPath = path.relative(path.dirname(importIntoFile), finalPath)
|
|
332
|
+
if (!finalPath.startsWith('.')) {
|
|
333
|
+
finalPath = `./${finalPath}`
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Remove extension
|
|
338
|
+
finalPath = finalPath.replace(/(\.d|)\.tsx?$/, '')
|
|
339
|
+
|
|
340
|
+
// Remove "/index"
|
|
341
|
+
if (finalPath.split('/').length === 2 && finalPath.endsWith('/index')) {
|
|
342
|
+
finalPath = finalPath.substring(0, finalPath.length - 6)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
list: [...list].sort(),
|
|
347
|
+
fullPath: file,
|
|
348
|
+
finalPath,
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
.filter((entry) => !shouldIgnore(entry.fullPath))
|
|
352
|
+
}
|
|
353
|
+
|
|
309
354
|
export function nodeToString(node: Node): string {
|
|
310
355
|
const printer = createPrinter({ newLine: NewLineKind.LineFeed })
|
|
311
356
|
const sourceFile = createSourceFile('logic.ts', '', ScriptTarget.Latest, false, ScriptKind.TS)
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
2
3
|
const path = require('path')
|
|
3
4
|
|
|
4
5
|
require('ts-node/register/transpile-only')
|
|
5
6
|
|
|
6
7
|
const repoRoot = path.resolve(__dirname, '..', '..')
|
|
7
|
-
const projectDir = fs.mkdtempSync(path.join(
|
|
8
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-watch-smoke-'))
|
|
8
9
|
const tsconfigPath = path.join(projectDir, 'tsconfig.json')
|
|
10
|
+
const keaDtsPath = path.join(projectDir, 'node_modules', 'kea', 'index.d.ts')
|
|
9
11
|
const fileCount = 200
|
|
12
|
+
const logicPaths = []
|
|
10
13
|
|
|
11
14
|
const noop = () => {}
|
|
12
15
|
console.info = noop
|
|
@@ -18,6 +21,10 @@ let completed = 0
|
|
|
18
21
|
let settleTimer
|
|
19
22
|
let timeoutTimer
|
|
20
23
|
let finished = false
|
|
24
|
+
let editTriggered = false
|
|
25
|
+
let incrementalAfterEdit = false
|
|
26
|
+
const parsedLogicCountsAfterEdit = []
|
|
27
|
+
const sourceFilePathsAfterEdit = []
|
|
21
28
|
|
|
22
29
|
function cleanup() {
|
|
23
30
|
try {
|
|
@@ -41,16 +48,31 @@ function finish(code) {
|
|
|
41
48
|
completed,
|
|
42
49
|
active,
|
|
43
50
|
maxActive,
|
|
51
|
+
incrementalAfterEdit,
|
|
52
|
+
parsedLogicCountsAfterEdit,
|
|
53
|
+
sourceFilePathsAfterEdit,
|
|
44
54
|
}) + '\n',
|
|
45
55
|
)
|
|
46
56
|
process.exit(code)
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
function scheduleFinishIfSettled() {
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
settleTimer = setTimeout(() => finish(0), 300)
|
|
60
|
+
if (active !== 0) {
|
|
61
|
+
return
|
|
53
62
|
}
|
|
63
|
+
|
|
64
|
+
clearTimeout(settleTimer)
|
|
65
|
+
settleTimer = setTimeout(() => {
|
|
66
|
+
if (!editTriggered && started > 1) {
|
|
67
|
+
editTriggered = true
|
|
68
|
+
fs.appendFileSync(logicPaths[0], '\n// trigger incremental watch pass\n')
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (editTriggered && incrementalAfterEdit) {
|
|
73
|
+
finish(0)
|
|
74
|
+
}
|
|
75
|
+
}, 300)
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
process.on('uncaughtException', (error) => {
|
|
@@ -82,9 +104,13 @@ fs.writeFileSync(
|
|
|
82
104
|
),
|
|
83
105
|
)
|
|
84
106
|
|
|
107
|
+
fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
|
|
108
|
+
fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
|
|
109
|
+
|
|
85
110
|
for (let i = 0; i < fileCount; i++) {
|
|
86
111
|
const dir = path.join(projectDir, 'src', `group${String(i % 10).padStart(2, '0')}`)
|
|
87
112
|
const filePath = path.join(dir, `logic${i}.ts`)
|
|
113
|
+
logicPaths.push(filePath)
|
|
88
114
|
|
|
89
115
|
fs.mkdirSync(dir, { recursive: true })
|
|
90
116
|
fs.writeFileSync(
|
|
@@ -109,11 +135,20 @@ const printModule = require(path.join(repoRoot, 'src/print/print'))
|
|
|
109
135
|
const originalPrintToFiles = printModule.printToFiles
|
|
110
136
|
|
|
111
137
|
printModule.printToFiles = async function (...args) {
|
|
138
|
+
const appOptions = args[1]
|
|
139
|
+
const parsedLogics = args[2]
|
|
140
|
+
|
|
112
141
|
active += 1
|
|
113
142
|
started += 1
|
|
114
143
|
maxActive = Math.max(maxActive, active)
|
|
115
144
|
clearTimeout(settleTimer)
|
|
116
145
|
|
|
146
|
+
if (editTriggered) {
|
|
147
|
+
parsedLogicCountsAfterEdit.push(parsedLogics.length)
|
|
148
|
+
sourceFilePathsAfterEdit.push(appOptions.sourceFilePath || null)
|
|
149
|
+
incrementalAfterEdit = incrementalAfterEdit || (parsedLogics.length === 1 && !!appOptions.sourceFilePath)
|
|
150
|
+
}
|
|
151
|
+
|
|
117
152
|
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
118
153
|
|
|
119
154
|
try {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const Module = require('module')
|
|
3
|
+
const os = require('os')
|
|
3
4
|
const path = require('path')
|
|
4
5
|
|
|
5
6
|
require('ts-node/register/transpile-only')
|
|
6
7
|
|
|
7
8
|
const repoRoot = path.resolve(__dirname, '..', '..')
|
|
8
|
-
const projectDir = fs.mkdtempSync(path.join(
|
|
9
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-write-smoke-'))
|
|
9
10
|
const tsconfigPath = path.join(projectDir, 'tsconfig.json')
|
|
10
11
|
const logicFilePath = path.join(projectDir, 'src', 'logic.ts')
|
|
12
|
+
const keaDtsPath = path.join(projectDir, 'node_modules', 'kea', 'index.d.ts')
|
|
11
13
|
|
|
12
14
|
const noop = () => {}
|
|
13
15
|
|
|
@@ -83,6 +85,7 @@ process.on('unhandledRejection', (error) => {
|
|
|
83
85
|
})
|
|
84
86
|
|
|
85
87
|
fs.mkdirSync(path.dirname(logicFilePath), { recursive: true })
|
|
88
|
+
fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
|
|
86
89
|
fs.writeFileSync(
|
|
87
90
|
tsconfigPath,
|
|
88
91
|
JSON.stringify(
|
|
@@ -102,6 +105,8 @@ fs.writeFileSync(
|
|
|
102
105
|
),
|
|
103
106
|
)
|
|
104
107
|
|
|
108
|
+
fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
|
|
109
|
+
|
|
105
110
|
fs.writeFileSync(
|
|
106
111
|
logicFilePath,
|
|
107
112
|
[
|
package/src/typegen.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { AppOptions } from './types'
|
|
|
6
6
|
import { Program } from 'typescript'
|
|
7
7
|
import { version } from '../package.json'
|
|
8
8
|
import { restoreCachedTypes } from './cache'
|
|
9
|
+
import { createWatchChangeTracker, planWatchTypegenPass, WatchFileChange, WatchTypegenPlan } from './watch'
|
|
9
10
|
|
|
10
11
|
// The undocumented defaultMaximumTruncationLength setting determines at what point printed types are truncated in versions less than 5.
|
|
11
12
|
// In kea-typegen output, we NEVER want the types truncated, as that results in a syntax error –
|
|
@@ -50,12 +51,13 @@ export async function runTypeGen(appOptions: AppOptions) {
|
|
|
50
51
|
// We don't emit JavaScript files in typegen watch mode, so the semantic-only
|
|
51
52
|
// builder is enough and avoids extra emit-related work on every rebuild.
|
|
52
53
|
const createProgram = ts.createSemanticDiagnosticsBuilderProgram
|
|
54
|
+
const watchChangeTracker = createWatchChangeTracker(ts.sys)
|
|
53
55
|
|
|
54
56
|
const host = ts.createWatchCompilerHost(
|
|
55
57
|
appOptions.tsConfigPath,
|
|
56
58
|
compilerOptions.options,
|
|
57
59
|
{
|
|
58
|
-
...
|
|
60
|
+
...watchChangeTracker.system,
|
|
59
61
|
writeFile(_path: string, _data: string, _writeByteOrderMark?: boolean) {
|
|
60
62
|
// skip emit
|
|
61
63
|
// https://github.com/microsoft/TypeScript/issues/32385
|
|
@@ -92,6 +94,7 @@ export async function runTypeGen(appOptions: AppOptions) {
|
|
|
92
94
|
|
|
93
95
|
const origPostProgramCreate = host.afterProgramCreate
|
|
94
96
|
let scheduledProgram: Program | undefined
|
|
97
|
+
let scheduledChanges: WatchFileChange[] = []
|
|
95
98
|
let runningTypegen = false
|
|
96
99
|
|
|
97
100
|
const runScheduledTypegen = async () => {
|
|
@@ -104,10 +107,16 @@ export async function runTypeGen(appOptions: AppOptions) {
|
|
|
104
107
|
try {
|
|
105
108
|
while (scheduledProgram) {
|
|
106
109
|
const nextProgram = scheduledProgram
|
|
110
|
+
const nextChanges = scheduledChanges
|
|
107
111
|
scheduledProgram = undefined
|
|
112
|
+
scheduledChanges = []
|
|
108
113
|
|
|
109
114
|
try {
|
|
110
|
-
await
|
|
115
|
+
await goThroughWatchTypegenPlan(
|
|
116
|
+
nextProgram,
|
|
117
|
+
appOptions,
|
|
118
|
+
planWatchTypegenPass(nextProgram, nextChanges),
|
|
119
|
+
)
|
|
111
120
|
} catch (error) {
|
|
112
121
|
console.error('⛔ Error running kea-typegen in watch mode')
|
|
113
122
|
console.error(error)
|
|
@@ -124,8 +133,10 @@ export async function runTypeGen(appOptions: AppOptions) {
|
|
|
124
133
|
|
|
125
134
|
host.afterProgramCreate = (prog) => {
|
|
126
135
|
program = prog.getProgram()
|
|
136
|
+
const changes = watchChangeTracker.consume()
|
|
127
137
|
origPostProgramCreate?.(prog)
|
|
128
138
|
scheduledProgram = program
|
|
139
|
+
scheduledChanges.push(...changes)
|
|
129
140
|
void runScheduledTypegen()
|
|
130
141
|
}
|
|
131
142
|
|
|
@@ -173,6 +184,46 @@ export async function runTypeGen(appOptions: AppOptions) {
|
|
|
173
184
|
return response
|
|
174
185
|
}
|
|
175
186
|
|
|
187
|
+
async function goThroughWatchTypegenPlan(
|
|
188
|
+
program,
|
|
189
|
+
appOptions,
|
|
190
|
+
plan: WatchTypegenPlan,
|
|
191
|
+
): Promise<{ filesToWrite: number; writtenFiles: number; filesToModify: number }> {
|
|
192
|
+
if (plan.kind === 'skip') {
|
|
193
|
+
if (appOptions.verbose) {
|
|
194
|
+
appOptions.log(`⏭️ Skipping typegen watch pass: ${plan.reason}`)
|
|
195
|
+
}
|
|
196
|
+
return { filesToWrite: 0, writtenFiles: 0, filesToModify: 0 }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (plan.kind === 'full') {
|
|
200
|
+
if (appOptions.verbose) {
|
|
201
|
+
appOptions.log(`🔎 Running full typegen watch pass: ${plan.reason}`)
|
|
202
|
+
}
|
|
203
|
+
return await goThroughAllTheFiles(program, appOptions)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (appOptions.verbose) {
|
|
207
|
+
appOptions.log(`🎯 Running incremental typegen watch pass: ${plan.reason}`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const response = { filesToWrite: 0, writtenFiles: 0, filesToModify: 0 }
|
|
211
|
+
|
|
212
|
+
for (const sourceFilePath of plan.sourceFilePaths) {
|
|
213
|
+
const fileResponse = await goThroughAllTheFiles(program, {
|
|
214
|
+
...appOptions,
|
|
215
|
+
delete: false,
|
|
216
|
+
sourceFilePath,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
response.filesToWrite += fileResponse.filesToWrite
|
|
220
|
+
response.writtenFiles += fileResponse.writtenFiles
|
|
221
|
+
response.filesToModify += fileResponse.filesToModify
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return response
|
|
225
|
+
}
|
|
226
|
+
|
|
176
227
|
if (program && !appOptions.watch && appOptions.sourceFilePath) {
|
|
177
228
|
await goThroughAllTheFiles(program, appOptions)
|
|
178
229
|
if (appOptions.write) {
|
package/src/types.ts
CHANGED
|
@@ -78,6 +78,8 @@ export interface AppOptions {
|
|
|
78
78
|
addTsNocheck?: boolean
|
|
79
79
|
/** Convert kea 2.0 logic input to kea 3.0 builders */
|
|
80
80
|
convertToBuilders?: boolean
|
|
81
|
+
/** Write logic types as MakeLogicType blocks above each kea() call, instead of into logicType.ts files */
|
|
82
|
+
inline?: boolean
|
|
81
83
|
/** Show TypeScript errors */
|
|
82
84
|
showTsErrors?: boolean
|
|
83
85
|
/** 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,87 @@ 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
|
+
/**
|
|
103
|
+
* True if the type argument in `kea<...>()` is a manually written type (e.g. `MakeLogicType<...>`
|
|
104
|
+
* or a custom interface), as opposed to a type that kea-typegen generates and keeps updated.
|
|
105
|
+
* Manually typed logics are skipped entirely.
|
|
106
|
+
*/
|
|
107
|
+
export function isManuallyTypedLogic(
|
|
108
|
+
typeArgument: ts.TypeNode,
|
|
109
|
+
logicTypeName: string,
|
|
110
|
+
checker: ts.TypeChecker,
|
|
111
|
+
): boolean {
|
|
112
|
+
if (!ts.isTypeReferenceNode(typeArgument)) {
|
|
113
|
+
// e.g. kea<{ actions: ... }>(...)
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
if (!ts.isIdentifier(typeArgument.typeName) || typeArgument.typeName.escapedText !== logicTypeName) {
|
|
117
|
+
// e.g. kea<MakeLogicType<Values, Actions>>(...) or kea<MyCustomLogicType>(...)
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
const symbol = checker.getSymbolAtLocation(typeArgument.typeName)
|
|
121
|
+
const declaration = symbol?.getDeclarations()?.[0]
|
|
122
|
+
if (!declaration) {
|
|
123
|
+
// "logicType" that doesn't resolve --> a generated type that doesn't exist yet
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
if (ts.isImportSpecifier(declaration)) {
|
|
127
|
+
// imported logic types are managed by kea-typegen
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
if (ts.isTypeAliasDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) {
|
|
131
|
+
// local declarations are ours only if their block carries the kea-typegen marker (written by --inline mode)
|
|
132
|
+
return !isGeneratedInlineBlock(declaration, logicTypeName.replace(/Type$/, ''))
|
|
133
|
+
}
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
56
137
|
export function getTypeNodeForNode(node: ts.Node, checker: ts.TypeChecker): ts.TypeNode {
|
|
57
138
|
let typeNode
|
|
58
139
|
if (node) {
|
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
|
|