kea-typegen 3.6.6 → 3.7.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.
@@ -0,0 +1,211 @@
1
+ import { spawn } from 'child_process'
2
+ import * as fs from 'fs'
3
+ import * as os from 'os'
4
+ import * as path from 'path'
5
+ import * as ts from 'typescript'
6
+ import { AppOptions } from '../types'
7
+ import { visitProgram } from '../visit/visit'
8
+ import { writeTypeImports } from '../write/writeTypeImports'
9
+
10
+ test(
11
+ 'write mode recreates the TypeScript program from scratch between passes',
12
+ async () => {
13
+ const repoRoot = path.resolve(__dirname, '..', '..')
14
+ const scriptPath = path.join(repoRoot, 'src/test-support/write-mode-smoke.js')
15
+
16
+ const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string }>(
17
+ (resolve, reject) => {
18
+ const child = spawn(process.execPath, [scriptPath], {
19
+ cwd: repoRoot,
20
+ env: { ...process.env, FORCE_COLOR: '0' },
21
+ })
22
+
23
+ let stdout = ''
24
+ let stderr = ''
25
+
26
+ child.stdout.on('data', (chunk) => {
27
+ stdout += chunk.toString()
28
+ })
29
+
30
+ child.stderr.on('data', (chunk) => {
31
+ stderr += chunk.toString()
32
+ })
33
+
34
+ child.on('error', reject)
35
+ child.on('close', (code, signal) => resolve({ code, signal, stdout, stderr }))
36
+ },
37
+ )
38
+
39
+ expect(result.signal).toBeNull()
40
+ expect(result.code).toBe(0)
41
+
42
+ const payload = JSON.parse(result.stdout.trim())
43
+
44
+ expect(payload.createProgramCalls).toBeGreaterThan(1)
45
+ expect(payload.reusedOldProgram).toBe(false)
46
+ expect(result.stderr).toBe('')
47
+ },
48
+ 30000,
49
+ )
50
+
51
+ test('writeTypeImports adds missing type import and kea generic', async () => {
52
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-write-imports-'))
53
+
54
+ try {
55
+ const logicDir = path.join(tempDir, 'src')
56
+ const logicPath = path.join(logicDir, 'logic.ts')
57
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
58
+
59
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
60
+ fs.mkdirSync(logicDir, { recursive: true })
61
+
62
+ fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
63
+ fs.writeFileSync(
64
+ logicPath,
65
+ [
66
+ "import { kea } from 'kea'",
67
+ '',
68
+ 'export const logic = kea({',
69
+ ' actions: () => ({',
70
+ ' setValue: (value: string) => ({ value }),',
71
+ ' }),',
72
+ '})',
73
+ '',
74
+ ].join('\n'),
75
+ )
76
+
77
+ const program = ts.createProgram([logicPath], {
78
+ module: ts.ModuleKind.CommonJS,
79
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
80
+ target: ts.ScriptTarget.ES2020,
81
+ skipLibCheck: true,
82
+ })
83
+
84
+ const appOptions: AppOptions = {
85
+ rootPath: logicDir,
86
+ typesPath: logicDir,
87
+ log: () => {},
88
+ }
89
+
90
+ const [parsedLogic] = visitProgram(program, appOptions)
91
+ await writeTypeImports(appOptions, program, logicPath, [parsedLogic], [parsedLogic])
92
+
93
+ const writtenLogic = fs.readFileSync(logicPath, 'utf8')
94
+
95
+ expect(writtenLogic).toMatch(/import type \{ logicType \} from ['"]\.\/logicType['"]/)
96
+ expect(writtenLogic).toContain('export const logic = kea<logicType>({')
97
+ } finally {
98
+ fs.rmSync(tempDir, { recursive: true, force: true })
99
+ }
100
+ })
101
+
102
+ test('writeTypeImports replaces existing kea generic without leaving a trailing >', async () => {
103
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-write-existing-generic-'))
104
+
105
+ try {
106
+ const logicDir = path.join(tempDir, 'src')
107
+ const logicPath = path.join(logicDir, 'logic.ts')
108
+ const logicTypePath = path.join(logicDir, 'logicType.ts')
109
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
110
+
111
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
112
+ fs.mkdirSync(logicDir, { recursive: true })
113
+
114
+ fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
115
+ fs.writeFileSync(logicTypePath, 'export interface logicType {}\n')
116
+ fs.writeFileSync(
117
+ logicPath,
118
+ [
119
+ "import type { logicType } from './logicType'",
120
+ "import { kea, actions } from 'kea'",
121
+ '',
122
+ 'export const logic = kea<logicType<string>>([',
123
+ ' actions({',
124
+ ' setValue: (value: string) => ({ value }),',
125
+ ' }),',
126
+ '])',
127
+ '',
128
+ ].join('\n'),
129
+ )
130
+
131
+ const program = ts.createProgram([logicPath], {
132
+ module: ts.ModuleKind.CommonJS,
133
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
134
+ target: ts.ScriptTarget.ES2020,
135
+ skipLibCheck: true,
136
+ })
137
+
138
+ const appOptions: AppOptions = {
139
+ rootPath: logicDir,
140
+ typesPath: logicDir,
141
+ log: () => {},
142
+ }
143
+
144
+ const [parsedLogic] = visitProgram(program, appOptions)
145
+ await writeTypeImports(appOptions, program, logicPath, [parsedLogic], [parsedLogic])
146
+
147
+ const writtenLogic = fs.readFileSync(logicPath, 'utf8')
148
+
149
+ expect(writtenLogic).toMatch(/import type \{ logicType \} from ['"]\.\/logicType['"]/)
150
+ expect(writtenLogic).toContain('export const logic = kea<logicType>([')
151
+ expect(writtenLogic).not.toContain('kea<logicType>>([')
152
+ } finally {
153
+ fs.rmSync(tempDir, { recursive: true, force: true })
154
+ }
155
+ })
156
+
157
+ test('visitProgram limits project-aware single-file generation to the requested source file', () => {
158
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-single-file-'))
159
+
160
+ try {
161
+ const logicDir = path.join(tempDir, 'src')
162
+ const firstLogicPath = path.join(logicDir, 'firstLogic.ts')
163
+ const secondLogicPath = path.join(logicDir, 'secondLogic.ts')
164
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
165
+
166
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
167
+ fs.mkdirSync(logicDir, { recursive: true })
168
+
169
+ fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
170
+ fs.writeFileSync(
171
+ firstLogicPath,
172
+ [
173
+ "import { kea } from 'kea'",
174
+ '',
175
+ 'export const firstLogic = kea({',
176
+ ' actions: () => ({ first: true }),',
177
+ '})',
178
+ '',
179
+ ].join('\n'),
180
+ )
181
+ fs.writeFileSync(
182
+ secondLogicPath,
183
+ [
184
+ "import { kea } from 'kea'",
185
+ '',
186
+ 'export const secondLogic = kea({',
187
+ ' actions: () => ({ second: true }),',
188
+ '})',
189
+ '',
190
+ ].join('\n'),
191
+ )
192
+
193
+ const program = ts.createProgram([firstLogicPath, secondLogicPath], {
194
+ module: ts.ModuleKind.CommonJS,
195
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
196
+ target: ts.ScriptTarget.ES2020,
197
+ skipLibCheck: true,
198
+ })
199
+
200
+ const parsedLogics = visitProgram(program, {
201
+ rootPath: logicDir,
202
+ typesPath: logicDir,
203
+ sourceFilePath: secondLogicPath,
204
+ log: () => {},
205
+ })
206
+
207
+ expect(parsedLogics.map((logic) => logic.logicName)).toEqual(['secondLogic'])
208
+ } finally {
209
+ fs.rmSync(tempDir, { recursive: true, force: true })
210
+ }
211
+ })
@@ -18,7 +18,11 @@ yargs
18
18
  await runTypeGen({ ...includeKeaConfig(parsedToAppOptions(argv)), write: true, watch: true })
19
19
  })
20
20
  .option('config', { alias: 'c', describe: 'Path to tsconfig.json (otherwise auto-detected)', type: 'string' })
21
- .option('file', { alias: 'f', describe: "Single file to evaluate (can't be used with --config)", type: 'string' })
21
+ .option('file', {
22
+ alias: 'f',
23
+ describe: 'Single file to evaluate. Uses --config/.kearc compiler options when available.',
24
+ type: 'string',
25
+ })
22
26
  .option('root', {
23
27
  alias: 'r',
24
28
  describe: 'Root for logic paths. E.g: ./frontend/src',
@@ -51,6 +55,11 @@ yargs
51
55
  describe: 'Cache generated logic files into .typegen, use them if generating a logic type for the first time',
52
56
  type: 'boolean',
53
57
  })
58
+ .option('prettier', {
59
+ describe: 'Format generated logic type declarations with Prettier (use --no-prettier to skip)',
60
+ type: 'boolean',
61
+ default: true,
62
+ })
54
63
  .option('verbose', { describe: 'Slightly more verbose output log', type: 'boolean' })
55
64
  .demandCommand()
56
65
  .help()
@@ -40,8 +40,16 @@ import { printInternalExtraInput } from './printInternalExtraInput'
40
40
  import { convertToBuilders } from '../write/convertToBuilders'
41
41
  import { cacheWrittenFile } from '../cache'
42
42
 
43
+ const prettierConfigCache = new Map<string, Promise<prettier.Options | null>>()
44
+
43
45
  export async function runThroughPrettier(sourceText: string, filePath: string): Promise<string> {
44
- const options = await prettier.resolveConfig(filePath)
46
+ let configPromise = prettierConfigCache.get(filePath)
47
+ if (!configPromise) {
48
+ configPromise = prettier.resolveConfig(filePath, { useCache: true })
49
+ prettierConfigCache.set(filePath, configPromise)
50
+ }
51
+
52
+ const options = await configPromise
45
53
  if (options) {
46
54
  try {
47
55
  return await prettier.format(sourceText, { ...options, filepath: filePath })
@@ -107,11 +115,12 @@ export async function printToFiles(
107
115
  const logicStrings = []
108
116
  const requiredKeys = new Set(['Logic'])
109
117
  for (const parsedLogic of parsedLogics) {
110
- const logicTypeStirng = await runThroughPrettier(
111
- nodeToString(parsedLogic.interfaceDeclaration),
112
- typeFileName,
118
+ const logicTypeString = nodeToString(parsedLogic.interfaceDeclaration)
119
+ logicStrings.push(
120
+ appOptions.prettier === false
121
+ ? logicTypeString
122
+ : await runThroughPrettier(logicTypeString, typeFileName),
113
123
  )
114
- logicStrings.push(logicTypeStirng)
115
124
  for (const string of parsedLogic.importFromKeaInLogicType.values()) {
116
125
  requiredKeys.add(string)
117
126
  }
@@ -0,0 +1,140 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ require('ts-node/register/transpile-only')
5
+
6
+ const repoRoot = path.resolve(__dirname, '..', '..')
7
+ const projectDir = fs.mkdtempSync(path.join(repoRoot, 'tmp-watch-smoke-'))
8
+ const tsconfigPath = path.join(projectDir, 'tsconfig.json')
9
+ const fileCount = 200
10
+
11
+ const noop = () => {}
12
+ console.info = noop
13
+
14
+ let active = 0
15
+ let maxActive = 0
16
+ let started = 0
17
+ let completed = 0
18
+ let settleTimer
19
+ let timeoutTimer
20
+ let finished = false
21
+
22
+ function cleanup() {
23
+ try {
24
+ fs.rmSync(projectDir, { recursive: true, force: true })
25
+ } catch {}
26
+ }
27
+
28
+ function finish(code) {
29
+ if (finished) {
30
+ return
31
+ }
32
+ finished = true
33
+ clearTimeout(settleTimer)
34
+ clearTimeout(timeoutTimer)
35
+ cleanup()
36
+
37
+ fs.writeFileSync(
38
+ process.stdout.fd,
39
+ JSON.stringify({
40
+ started,
41
+ completed,
42
+ active,
43
+ maxActive,
44
+ }) + '\n',
45
+ )
46
+ process.exit(code)
47
+ }
48
+
49
+ function scheduleFinishIfSettled() {
50
+ if (started > 1 && active === 0) {
51
+ clearTimeout(settleTimer)
52
+ settleTimer = setTimeout(() => finish(0), 300)
53
+ }
54
+ }
55
+
56
+ process.on('uncaughtException', (error) => {
57
+ console.error(error)
58
+ finish(1)
59
+ })
60
+
61
+ process.on('unhandledRejection', (error) => {
62
+ console.error(error)
63
+ finish(1)
64
+ })
65
+
66
+ fs.writeFileSync(
67
+ tsconfigPath,
68
+ JSON.stringify(
69
+ {
70
+ compilerOptions: {
71
+ target: 'ES2020',
72
+ module: 'commonjs',
73
+ moduleResolution: 'node',
74
+ esModuleInterop: true,
75
+ skipLibCheck: true,
76
+ strict: false,
77
+ },
78
+ include: ['src/**/*'],
79
+ },
80
+ null,
81
+ 2,
82
+ ),
83
+ )
84
+
85
+ for (let i = 0; i < fileCount; i++) {
86
+ const dir = path.join(projectDir, 'src', `group${String(i % 10).padStart(2, '0')}`)
87
+ const filePath = path.join(dir, `logic${i}.ts`)
88
+
89
+ fs.mkdirSync(dir, { recursive: true })
90
+ fs.writeFileSync(
91
+ filePath,
92
+ [
93
+ "import { kea } from 'kea'",
94
+ '',
95
+ `export const logic${i} = kea({`,
96
+ ' actions: () => ({',
97
+ ' setValue: (value: string) => ({ value }),',
98
+ ' }),',
99
+ ' reducers: () => ({',
100
+ " value: ['' as string, { setValue: (_, payload) => payload.value }],",
101
+ ' }),',
102
+ '})',
103
+ '',
104
+ ].join('\n'),
105
+ )
106
+ }
107
+
108
+ const printModule = require(path.join(repoRoot, 'src/print/print'))
109
+ const originalPrintToFiles = printModule.printToFiles
110
+
111
+ printModule.printToFiles = async function (...args) {
112
+ active += 1
113
+ started += 1
114
+ maxActive = Math.max(maxActive, active)
115
+ clearTimeout(settleTimer)
116
+
117
+ await new Promise((resolve) => setTimeout(resolve, 25))
118
+
119
+ try {
120
+ return await originalPrintToFiles.apply(this, args)
121
+ } finally {
122
+ await new Promise((resolve) => setTimeout(resolve, 25))
123
+ active -= 1
124
+ completed += 1
125
+ scheduleFinishIfSettled()
126
+ }
127
+ }
128
+
129
+ const { runTypeGen } = require(path.join(repoRoot, 'src/typegen'))
130
+
131
+ timeoutTimer = setTimeout(() => finish(1), 20000)
132
+
133
+ runTypeGen({
134
+ tsConfigPath: tsconfigPath,
135
+ rootPath: projectDir,
136
+ typesPath: projectDir,
137
+ write: true,
138
+ watch: true,
139
+ log: noop,
140
+ })
@@ -0,0 +1,147 @@
1
+ const fs = require('fs')
2
+ const Module = require('module')
3
+ const path = require('path')
4
+
5
+ require('ts-node/register/transpile-only')
6
+
7
+ const repoRoot = path.resolve(__dirname, '..', '..')
8
+ const projectDir = fs.mkdtempSync(path.join(repoRoot, 'tmp-write-smoke-'))
9
+ const tsconfigPath = path.join(projectDir, 'tsconfig.json')
10
+ const logicFilePath = path.join(projectDir, 'src', 'logic.ts')
11
+
12
+ const noop = () => {}
13
+
14
+ let createProgramCalls = 0
15
+ let reusedOldProgram = false
16
+ let finished = false
17
+ const exitSignalKey = '__writeModeSmokeExit'
18
+
19
+ const originalTs = require('typescript')
20
+ const instrumentedTs = new Proxy(originalTs, {
21
+ get(target, property, receiver) {
22
+ if (property === 'createProgram') {
23
+ return function (...args) {
24
+ createProgramCalls += 1
25
+ reusedOldProgram = reusedOldProgram || !!args[3]
26
+ return Reflect.get(target, property, receiver).apply(this, args)
27
+ }
28
+ }
29
+
30
+ return Reflect.get(target, property, receiver)
31
+ },
32
+ })
33
+
34
+ const originalModuleLoad = Module._load
35
+ Module._load = function (request, parent, isMain) {
36
+ if (request === 'typescript') {
37
+ return instrumentedTs
38
+ }
39
+
40
+ return originalModuleLoad.apply(this, arguments)
41
+ }
42
+
43
+ function cleanup() {
44
+ try {
45
+ fs.rmSync(projectDir, { recursive: true, force: true })
46
+ } catch {}
47
+ }
48
+
49
+ function finish(code, error) {
50
+ if (finished) {
51
+ return
52
+ }
53
+ finished = true
54
+
55
+ cleanup()
56
+
57
+ if (error) {
58
+ console.error(error)
59
+ }
60
+
61
+ fs.writeFileSync(
62
+ process.stdout.fd,
63
+ JSON.stringify({
64
+ createProgramCalls,
65
+ reusedOldProgram,
66
+ }) + '\n',
67
+ )
68
+
69
+ process.exitCode = code
70
+ }
71
+
72
+ const originalProcessExit = process.exit
73
+ process.exit = (code) => {
74
+ throw { [exitSignalKey]: true, code: code ?? 0 }
75
+ }
76
+
77
+ process.on('uncaughtException', (error) => {
78
+ finish(1, error)
79
+ })
80
+
81
+ process.on('unhandledRejection', (error) => {
82
+ finish(1, error)
83
+ })
84
+
85
+ fs.mkdirSync(path.dirname(logicFilePath), { recursive: true })
86
+ fs.writeFileSync(
87
+ tsconfigPath,
88
+ JSON.stringify(
89
+ {
90
+ compilerOptions: {
91
+ target: 'ES2020',
92
+ module: 'commonjs',
93
+ moduleResolution: 'node',
94
+ esModuleInterop: true,
95
+ skipLibCheck: true,
96
+ strict: false,
97
+ },
98
+ include: ['src/**/*'],
99
+ },
100
+ null,
101
+ 2,
102
+ ),
103
+ )
104
+
105
+ fs.writeFileSync(
106
+ logicFilePath,
107
+ [
108
+ "import { kea } from 'kea'",
109
+ '',
110
+ 'export const logic = kea({',
111
+ ' actions: () => ({',
112
+ ' setValue: (value: string) => ({ value }),',
113
+ ' }),',
114
+ ' reducers: () => ({',
115
+ " value: ['', { setValue: (_, payload) => payload.value }],",
116
+ ' }),',
117
+ '})',
118
+ '',
119
+ ].join('\n'),
120
+ )
121
+
122
+ const { runTypeGen } = require(path.join(repoRoot, 'src/typegen'))
123
+
124
+ Promise.resolve(
125
+ runTypeGen({
126
+ tsConfigPath: tsconfigPath,
127
+ rootPath: projectDir,
128
+ typesPath: projectDir,
129
+ write: true,
130
+ watch: false,
131
+ log: noop,
132
+ }),
133
+ )
134
+ .then(() => {
135
+ finish(0)
136
+ })
137
+ .catch((error) => {
138
+ if (error && error[exitSignalKey]) {
139
+ finish(error.code)
140
+ } else {
141
+ finish(1, error)
142
+ }
143
+ })
144
+ .finally(() => {
145
+ Module._load = originalModuleLoad
146
+ process.exit = originalProcessExit
147
+ })
package/src/typegen.ts CHANGED
@@ -25,21 +25,26 @@ export async function runTypeGen(appOptions: AppOptions) {
25
25
 
26
26
  if (appOptions.sourceFilePath) {
27
27
  log(`❇️ Loading file: ${appOptions.sourceFilePath}`)
28
+ const compilerOptions = appOptions.tsConfigPath
29
+ ? loadTsConfig(appOptions.tsConfigPath, log).options
30
+ : {
31
+ target: ts.ScriptTarget.ES5,
32
+ module: ts.ModuleKind.CommonJS,
33
+ noEmit: true,
34
+ noErrorTruncation: true,
35
+ }
36
+
28
37
  resetProgram = () => {
29
- program = ts.createProgram([appOptions.sourceFilePath], {
30
- target: ts.ScriptTarget.ES5,
31
- module: ts.ModuleKind.CommonJS,
32
- noEmit: true,
33
- noErrorTruncation: true,
34
- })
38
+ program = replaceProgram(
39
+ () => ts.createProgram([appOptions.sourceFilePath], compilerOptions),
40
+ (nextProgram) => {
41
+ program = nextProgram
42
+ },
43
+ )
35
44
  }
36
45
  resetProgram()
37
46
  } else if (appOptions.tsConfigPath) {
38
- log(`🥚 TypeScript Config: ${appOptions.tsConfigPath}`)
39
-
40
- const configFile = ts.readJsonConfigFile(appOptions.tsConfigPath, ts.sys.readFile)
41
- const rootFolder = path.dirname(appOptions.tsConfigPath)
42
- const compilerOptions = ts.parseJsonSourceFileConfigFileContent(configFile, ts.sys, rootFolder)
47
+ const compilerOptions = loadTsConfig(appOptions.tsConfigPath, log)
43
48
 
44
49
  if (appOptions.watch) {
45
50
  // We don't emit JavaScript files in typegen watch mode, so the semantic-only
@@ -85,24 +90,62 @@ export async function runTypeGen(appOptions: AppOptions) {
85
90
  console.info(codes[diagnostic.code] || `🥚 ${ts.formatDiagnostic(diagnostic, formatHost).trim()}`)
86
91
  }
87
92
 
88
- const origCreateProgram = host.createProgram
89
- host.createProgram = (rootNames: ReadonlyArray<string>, options, host, oldProgram) => {
90
- return origCreateProgram(rootNames, options, host, oldProgram)
91
- }
92
93
  const origPostProgramCreate = host.afterProgramCreate
94
+ let scheduledProgram: Program | undefined
95
+ let runningTypegen = false
93
96
 
94
- host.afterProgramCreate = async (prog) => {
95
- program = prog.getProgram()
96
- origPostProgramCreate!(prog)
97
+ const runScheduledTypegen = async () => {
98
+ if (runningTypegen) {
99
+ return
100
+ }
97
101
 
98
- await goThroughAllTheFiles(program, appOptions)
102
+ runningTypegen = true
103
+
104
+ try {
105
+ while (scheduledProgram) {
106
+ const nextProgram = scheduledProgram
107
+ scheduledProgram = undefined
108
+
109
+ try {
110
+ await goThroughAllTheFiles(nextProgram, appOptions)
111
+ } catch (error) {
112
+ console.error('⛔ Error running kea-typegen in watch mode')
113
+ console.error(error)
114
+ }
115
+ }
116
+ } finally {
117
+ runningTypegen = false
118
+
119
+ if (scheduledProgram) {
120
+ void runScheduledTypegen()
121
+ }
122
+ }
123
+ }
124
+
125
+ host.afterProgramCreate = (prog) => {
126
+ program = prog.getProgram()
127
+ origPostProgramCreate?.(prog)
128
+ scheduledProgram = program
129
+ void runScheduledTypegen()
99
130
  }
100
131
 
101
132
  ts.createWatchProgram(host)
102
133
  } else {
103
- const host = ts.createCompilerHost(compilerOptions.options)
104
134
  resetProgram = () => {
105
- program = ts.createProgram(compilerOptions.fileNames, compilerOptions.options, host, program)
135
+ // Write mode can require multiple passes as generated imports and type files
136
+ // land on disk. Reusing the previous Program retains too much compiler state
137
+ // for large projects, so rebuild from a fresh host each round instead.
138
+ // Clear the previous Program reference before allocating the next one to
139
+ // avoid holding both huge compiler graphs live at the same time.
140
+ program = replaceProgram(
141
+ () => {
142
+ const host = ts.createCompilerHost(compilerOptions.options)
143
+ return ts.createProgram(compilerOptions.fileNames, compilerOptions.options, host)
144
+ },
145
+ (nextProgram) => {
146
+ program = nextProgram
147
+ },
148
+ )
106
149
  }
107
150
  resetProgram()
108
151
  }
@@ -130,7 +173,12 @@ export async function runTypeGen(appOptions: AppOptions) {
130
173
  return response
131
174
  }
132
175
 
133
- if (program && !appOptions.watch && !appOptions.sourceFilePath) {
176
+ if (program && !appOptions.watch && appOptions.sourceFilePath) {
177
+ await goThroughAllTheFiles(program, appOptions)
178
+ if (appOptions.write) {
179
+ log(`👋 Finished writing files! Exiting.`)
180
+ }
181
+ } else if (program && !appOptions.watch) {
134
182
  if (appOptions.write) {
135
183
  if (restoreCachedTypes(program, appOptions, log)) {
136
184
  resetProgram()
@@ -158,3 +206,17 @@ export async function runTypeGen(appOptions: AppOptions) {
158
206
  }
159
207
  }
160
208
  }
209
+
210
+ function loadTsConfig(tsConfigPath: string, log: AppOptions['log']): ts.ParsedCommandLine {
211
+ log(`🥚 TypeScript Config: ${tsConfigPath}`)
212
+ const configFile = ts.readJsonConfigFile(tsConfigPath, ts.sys.readFile)
213
+ const rootFolder = path.dirname(tsConfigPath)
214
+ return ts.parseJsonSourceFileConfigFileContent(configFile, ts.sys, rootFolder)
215
+ }
216
+
217
+ export function replaceProgram(createProgram: () => Program, setProgram: (program?: Program) => void): Program {
218
+ setProgram(undefined)
219
+ const nextProgram = createProgram()
220
+ setProgram(nextProgram)
221
+ return nextProgram
222
+ }