kea-typegen 3.7.0 → 3.7.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 +4 -0
- package/dist/package.json +1 -1
- package/dist/src/__tests__/watch.js +178 -0
- package/dist/src/__tests__/watch.js.map +1 -1
- package/dist/src/typegen.js +38 -2
- package/dist/src/typegen.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/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/watch.ts +259 -32
- 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/watch.ts +261 -0
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/watch.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as ts from 'typescript'
|
|
4
|
+
import { getFilenameForImportDeclaration } from './utils'
|
|
5
|
+
|
|
6
|
+
const KEA_CALL_REGEX = /\bkea\s*[<(]/
|
|
7
|
+
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mjsx']
|
|
8
|
+
const TYPEGEN_FILE_REGEX = /(^|\.|\/)typegen\.[tj]s$/
|
|
9
|
+
const MAX_INCREMENTAL_CHANGED_FILES = 50
|
|
10
|
+
|
|
11
|
+
export interface WatchFileChange {
|
|
12
|
+
fileName: string
|
|
13
|
+
eventKind: ts.FileWatcherEventKind
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type WatchTypegenPlan =
|
|
17
|
+
| { kind: 'full'; reason: string }
|
|
18
|
+
| { kind: 'files'; sourceFilePaths: string[]; reason: string }
|
|
19
|
+
| { kind: 'skip'; reason: string }
|
|
20
|
+
|
|
21
|
+
export interface WatchChangeTracker {
|
|
22
|
+
system: ts.System
|
|
23
|
+
consume(): WatchFileChange[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createWatchChangeTracker(baseSystem: ts.System): WatchChangeTracker {
|
|
27
|
+
const changes = new Map<string, WatchFileChange>()
|
|
28
|
+
|
|
29
|
+
const track = (fileName: string, eventKind: ts.FileWatcherEventKind): void => {
|
|
30
|
+
changes.set(path.resolve(fileName), { fileName: path.resolve(fileName), eventKind })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const system: ts.System = {
|
|
34
|
+
...baseSystem,
|
|
35
|
+
watchFile(fileName, callback, pollingInterval, options) {
|
|
36
|
+
return baseSystem.watchFile!(
|
|
37
|
+
fileName,
|
|
38
|
+
(changedFileName, eventKind, modifiedTime) => {
|
|
39
|
+
track(changedFileName, eventKind)
|
|
40
|
+
callback(changedFileName, eventKind, modifiedTime)
|
|
41
|
+
},
|
|
42
|
+
pollingInterval,
|
|
43
|
+
options,
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
watchDirectory(fileName, callback, recursive, options) {
|
|
47
|
+
return baseSystem.watchDirectory!(
|
|
48
|
+
fileName,
|
|
49
|
+
(changedFileName) => {
|
|
50
|
+
track(changedFileName, ts.FileWatcherEventKind.Created)
|
|
51
|
+
callback(changedFileName)
|
|
52
|
+
},
|
|
53
|
+
recursive,
|
|
54
|
+
options,
|
|
55
|
+
)
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
system,
|
|
61
|
+
consume() {
|
|
62
|
+
const currentChanges = [...changes.values()]
|
|
63
|
+
changes.clear()
|
|
64
|
+
return currentChanges
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function planWatchTypegenPass(program: ts.Program, changes: WatchFileChange[]): WatchTypegenPlan {
|
|
70
|
+
if (changes.length === 0) {
|
|
71
|
+
return { kind: 'full', reason: 'initial watch pass' }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const normalizedChanges = dedupeChanges(changes)
|
|
75
|
+
|
|
76
|
+
if (normalizedChanges.length > MAX_INCREMENTAL_CHANGED_FILES) {
|
|
77
|
+
return { kind: 'full', reason: `${normalizedChanges.length} files changed` }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (normalizedChanges.some((change) => change.eventKind === ts.FileWatcherEventKind.Deleted)) {
|
|
81
|
+
return { kind: 'full', reason: 'deleted file detected' }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const sourceFilesByPath = getSourceFilesByPath(program)
|
|
85
|
+
const reverseDependencies = buildReverseDependencyMap(program)
|
|
86
|
+
const queue: string[] = []
|
|
87
|
+
|
|
88
|
+
for (const change of normalizedChanges) {
|
|
89
|
+
const changedPath = path.resolve(change.fileName)
|
|
90
|
+
|
|
91
|
+
if (isTypegenModuleFile(changedPath)) {
|
|
92
|
+
return { kind: 'full', reason: `typegen module changed: ${path.relative(process.cwd(), changedPath)}` }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sourceFile = sourceFilesByPath.get(changedPath)
|
|
96
|
+
|
|
97
|
+
if (sourceFile?.text.includes('resetContext')) {
|
|
98
|
+
return { kind: 'full', reason: `resetContext changed: ${path.relative(process.cwd(), changedPath)}` }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isGeneratedTypeFile(changedPath)) {
|
|
102
|
+
queue.push(...sourcePathsForTypeFile(changedPath, sourceFilesByPath))
|
|
103
|
+
queue.push(changedPath)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (sourceFile) {
|
|
108
|
+
if (sourceFile.isDeclarationFile) {
|
|
109
|
+
return {
|
|
110
|
+
kind: 'full',
|
|
111
|
+
reason: `declaration file changed: ${path.relative(process.cwd(), changedPath)}`,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
queue.push(changedPath)
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isSourceLikeFile(changedPath) && fs.existsSync(changedPath)) {
|
|
120
|
+
return {
|
|
121
|
+
kind: 'full',
|
|
122
|
+
reason: `new source file outside program: ${path.relative(process.cwd(), changedPath)}`,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const affectedLogicFiles = collectAffectedLogicFiles(queue, sourceFilesByPath, reverseDependencies)
|
|
128
|
+
|
|
129
|
+
if (affectedLogicFiles.length === 0) {
|
|
130
|
+
return { kind: 'skip', reason: 'no affected Kea logic files' }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
kind: 'files',
|
|
135
|
+
sourceFilePaths: affectedLogicFiles,
|
|
136
|
+
reason: `${affectedLogicFiles.length} affected Kea logic file${affectedLogicFiles.length === 1 ? '' : 's'}`,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function dedupeChanges(changes: WatchFileChange[]): WatchFileChange[] {
|
|
141
|
+
const deduped = new Map<string, WatchFileChange>()
|
|
142
|
+
|
|
143
|
+
for (const change of changes) {
|
|
144
|
+
deduped.set(path.resolve(change.fileName), { ...change, fileName: path.resolve(change.fileName) })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return [...deduped.values()]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getSourceFilesByPath(program: ts.Program): Map<string, ts.SourceFile> {
|
|
151
|
+
const sourceFilesByPath = new Map<string, ts.SourceFile>()
|
|
152
|
+
|
|
153
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
154
|
+
sourceFilesByPath.set(path.resolve(sourceFile.fileName), sourceFile)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return sourceFilesByPath
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildReverseDependencyMap(program: ts.Program): Map<string, Set<string>> {
|
|
161
|
+
const checker = program.getTypeChecker()
|
|
162
|
+
const reverseDependencies = new Map<string, Set<string>>()
|
|
163
|
+
|
|
164
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
165
|
+
if (sourceFile.isDeclarationFile) {
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const importerPath = path.resolve(sourceFile.fileName)
|
|
170
|
+
|
|
171
|
+
ts.forEachChild(sourceFile, function visit(node) {
|
|
172
|
+
if (ts.isImportDeclaration(node)) {
|
|
173
|
+
const importedFileName = getFilenameForImportDeclaration(checker, node)
|
|
174
|
+
|
|
175
|
+
if (importedFileName) {
|
|
176
|
+
const importedPath = path.resolve(importedFileName)
|
|
177
|
+
let importers = reverseDependencies.get(importedPath)
|
|
178
|
+
|
|
179
|
+
if (!importers) {
|
|
180
|
+
importers = new Set()
|
|
181
|
+
reverseDependencies.set(importedPath, importers)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
importers.add(importerPath)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
ts.forEachChild(node, visit)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return reverseDependencies
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function collectAffectedLogicFiles(
|
|
196
|
+
startPaths: string[],
|
|
197
|
+
sourceFilesByPath: Map<string, ts.SourceFile>,
|
|
198
|
+
reverseDependencies: Map<string, Set<string>>,
|
|
199
|
+
): string[] {
|
|
200
|
+
const visited = new Set<string>()
|
|
201
|
+
const queue = startPaths.map((filePath) => path.resolve(filePath))
|
|
202
|
+
const affectedLogicFiles = new Set<string>()
|
|
203
|
+
|
|
204
|
+
while (queue.length > 0) {
|
|
205
|
+
const currentPath = queue.shift()!
|
|
206
|
+
|
|
207
|
+
if (visited.has(currentPath)) {
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
visited.add(currentPath)
|
|
212
|
+
|
|
213
|
+
const sourceFile = sourceFilesByPath.get(currentPath)
|
|
214
|
+
if (
|
|
215
|
+
sourceFile &&
|
|
216
|
+
!sourceFile.isDeclarationFile &&
|
|
217
|
+
!isGeneratedTypeFile(currentPath) &&
|
|
218
|
+
sourceFileMightContainKeaCall(sourceFile)
|
|
219
|
+
) {
|
|
220
|
+
affectedLogicFiles.add(currentPath)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (isGeneratedTypeFile(currentPath)) {
|
|
224
|
+
queue.push(...sourcePathsForTypeFile(currentPath, sourceFilesByPath))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const importerPath of reverseDependencies.get(currentPath) ?? []) {
|
|
228
|
+
queue.push(importerPath)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return [...affectedLogicFiles].sort()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function sourcePathsForTypeFile(typeFilePath: string, sourceFilesByPath: Map<string, ts.SourceFile>): string[] {
|
|
236
|
+
if (!isGeneratedTypeFile(typeFilePath)) {
|
|
237
|
+
return []
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const sourceBasePath = typeFilePath.slice(0, -'Type.ts'.length)
|
|
241
|
+
|
|
242
|
+
return SOURCE_EXTENSIONS.map((extension) => `${sourceBasePath}${extension}`).filter((sourcePath) =>
|
|
243
|
+
sourceFilesByPath.has(path.resolve(sourcePath)),
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isGeneratedTypeFile(filePath: string): boolean {
|
|
248
|
+
return filePath.endsWith('Type.ts')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isSourceLikeFile(filePath: string): boolean {
|
|
252
|
+
return SOURCE_EXTENSIONS.some((extension) => filePath.endsWith(extension))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isTypegenModuleFile(filePath: string): boolean {
|
|
256
|
+
return TYPEGEN_FILE_REGEX.test(filePath.split(path.sep).join('/'))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function sourceFileMightContainKeaCall(sourceFile: ts.SourceFile): boolean {
|
|
260
|
+
return KEA_CALL_REGEX.test(sourceFile.text)
|
|
261
|
+
}
|