kea-typegen 3.6.5 → 3.6.8

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,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
@@ -26,12 +26,17 @@ export async function runTypeGen(appOptions: AppOptions) {
26
26
  if (appOptions.sourceFilePath) {
27
27
  log(`❇️ Loading file: ${appOptions.sourceFilePath}`)
28
28
  resetProgram = () => {
29
- program = ts.createProgram([appOptions.sourceFilePath], {
30
- target: ts.ScriptTarget.ES5,
31
- module: ts.ModuleKind.CommonJS,
32
- noEmit: true,
33
- noErrorTruncation: true,
34
- })
29
+ program = replaceProgram(() =>
30
+ ts.createProgram([appOptions.sourceFilePath], {
31
+ target: ts.ScriptTarget.ES5,
32
+ module: ts.ModuleKind.CommonJS,
33
+ noEmit: true,
34
+ noErrorTruncation: true,
35
+ }),
36
+ (nextProgram) => {
37
+ program = nextProgram
38
+ },
39
+ )
35
40
  }
36
41
  resetProgram()
37
42
  } else if (appOptions.tsConfigPath) {
@@ -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
  }
@@ -158,3 +201,10 @@ export async function runTypeGen(appOptions: AppOptions) {
158
201
  }
159
202
  }
160
203
  }
204
+
205
+ export function replaceProgram(createProgram: () => Program, setProgram: (program?: Program) => void): Program {
206
+ setProgram(undefined)
207
+ const nextProgram = createProgram()
208
+ setProgram(nextProgram)
209
+ return nextProgram
210
+ }
@@ -1,10 +1,41 @@
1
1
  import { AppOptions, ParsedLogic } from '../types'
2
2
  import * as ts from 'typescript'
3
- import { print, visit } from 'recast'
4
3
  import * as osPath from 'path'
5
4
  import { runThroughPrettier } from '../print/print'
6
5
  import * as fs from 'fs'
7
- import { t, b, visitAllKeaCalls, getAst } from './utils'
6
+ interface TextEdit {
7
+ start: number
8
+ end: number
9
+ text: string
10
+ }
11
+
12
+ function applyTextEdits(source: string, edits: TextEdit[]): string {
13
+ return edits
14
+ .sort((a, b) => b.start - a.start || b.end - a.end)
15
+ .reduce((output, { start, end, text }) => output.slice(0, start) + text + output.slice(end), source)
16
+ }
17
+
18
+ function getImportPath(importDeclaration: ts.ImportDeclaration): string | null {
19
+ return ts.isStringLiteralLike(importDeclaration.moduleSpecifier) ? importDeclaration.moduleSpecifier.text : null
20
+ }
21
+
22
+ function getImportInsertPosition(sourceFile: ts.SourceFile, rawCode: string): number {
23
+ const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration)
24
+ if (importDeclarations.length > 0) {
25
+ return importDeclarations[importDeclarations.length - 1].getEnd()
26
+ }
27
+
28
+ const shebangMatch = rawCode.match(/^#!.*(?:\r?\n|$)/)
29
+ return shebangMatch ? shebangMatch[0].length : 0
30
+ }
31
+
32
+ function getTypeArgumentInsertEnd(callExpression: ts.CallExpression, sourceFile: ts.SourceFile): number {
33
+ const openParenToken = callExpression
34
+ .getChildren(sourceFile)
35
+ .find((child) => child.kind === ts.SyntaxKind.OpenParenToken)
36
+
37
+ return openParenToken ? openParenToken.getStart(sourceFile) : callExpression.expression.getEnd()
38
+ }
8
39
 
9
40
  export async function writeTypeImports(
10
41
  appOptions: AppOptions,
@@ -15,9 +46,10 @@ export async function writeTypeImports(
15
46
  ) {
16
47
  const { log } = appOptions
17
48
  const sourceFile = program.getSourceFile(filename)
18
- const rawCode = sourceFile.getText()
19
-
20
- const ast = getAst(filename, rawCode)
49
+ if (!sourceFile) {
50
+ throw new Error(`Could not find source file: ${filename}`)
51
+ }
52
+ const rawCode = fs.readFileSync(filename, 'utf8')
21
53
 
22
54
  let importLocation = osPath
23
55
  .relative(osPath.dirname(filename), logicsNeedingImports[0].typeFileName)
@@ -26,56 +58,57 @@ export async function writeTypeImports(
26
58
  importLocation = `./${importLocation}`
27
59
  }
28
60
 
29
- // add import if missing
30
- let foundImport = false
31
- visit(ast, {
32
- visitImportDeclaration(path) {
33
- const importPath =
34
- path.value.source && t.StringLiteral.check(path.value.source) ? path.value.source.value : null
35
-
36
- if (
37
- t.ImportDeclaration.check(path.value) &&
38
- importPath &&
39
- osPath.resolve(osPath.dirname(filename), importPath) ===
40
- osPath.resolve(osPath.dirname(filename), importLocation)
41
- ) {
42
- foundImport = true
43
- path.value.importKind = 'type'
44
- path.value.specifiers = parsedLogics.map((l) =>
45
- b.importSpecifier(b.identifier(l.logicTypeName), b.identifier(l.logicTypeName)),
46
- )
47
- }
48
- return false
49
- },
61
+ const desiredImport = `import type { ${parsedLogics.map((l) => l.logicTypeName).join(', ')} } from '${importLocation}'`
62
+ const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration)
63
+ const matchingImport = importDeclarations.find((importDeclaration) => {
64
+ const importPath = getImportPath(importDeclaration)
65
+ return (
66
+ importPath !== null &&
67
+ osPath.resolve(osPath.dirname(filename), importPath) ===
68
+ osPath.resolve(osPath.dirname(filename), importLocation)
69
+ )
50
70
  })
51
71
 
52
- if (!foundImport) {
53
- visit(ast, {
54
- visitProgram(path) {
55
- path.value.body = [
56
- ...path.value.body.filter((n) => t.ImportDeclaration.check(n)),
57
- b.importDeclaration(
58
- parsedLogics.map((l) =>
59
- b.importSpecifier(b.identifier(l.logicTypeName), b.identifier(l.logicTypeName)),
60
- ),
61
- b.stringLiteral(importLocation),
62
- 'type',
63
- ),
64
- ...path.value.body.filter((n) => !t.ImportDeclaration.check(n)),
65
- ]
66
- return false
67
- },
72
+ const edits: TextEdit[] = []
73
+ if (matchingImport) {
74
+ edits.push({
75
+ start: matchingImport.getStart(sourceFile),
76
+ end: matchingImport.getEnd(),
77
+ text: desiredImport,
78
+ })
79
+ } else {
80
+ const insertPos = getImportInsertPosition(sourceFile, rawCode)
81
+ const hasExistingImports = importDeclarations.length > 0
82
+ const importText = hasExistingImports
83
+ ? `\n${desiredImport}${rawCode.slice(insertPos, insertPos + 1) === '\n' ? '' : '\n'}`
84
+ : `${desiredImport}\n`
85
+
86
+ edits.push({
87
+ start: insertPos,
88
+ end: insertPos,
89
+ text: importText,
68
90
  })
69
91
  }
70
92
 
71
- // find all kea calls, add `<logicType>` type parameters if needed
72
- visitAllKeaCalls(ast, logicsNeedingImports, filename, ({ path, parsedLogic }) => {
73
- path.node.typeParameters = b.tsTypeParameterInstantiation([
74
- b.tsTypeReference(b.identifier(parsedLogic.logicTypeName)),
75
- ])
76
- })
93
+ for (const parsedLogic of logicsNeedingImports) {
94
+ const callExpression = parsedLogic.node.parent
95
+ if (!ts.isCallExpression(callExpression)) {
96
+ continue
97
+ }
98
+
99
+ const typeArgumentStart = callExpression.expression.getEnd()
100
+ const typeArgumentEnd = callExpression.typeArguments
101
+ ? getTypeArgumentInsertEnd(callExpression, sourceFile)
102
+ : typeArgumentStart
103
+
104
+ edits.push({
105
+ start: typeArgumentStart,
106
+ end: typeArgumentEnd,
107
+ text: `<${parsedLogic.logicTypeName}>`,
108
+ })
109
+ }
77
110
 
78
- const newText = await runThroughPrettier(print(ast).code, filename)
111
+ const newText = await runThroughPrettier(applyTextEdits(rawCode, edits), filename)
79
112
  fs.writeFileSync(filename, newText)
80
113
 
81
114
  log(`🔥 Import added: ${osPath.relative(process.cwd(), filename)}`)