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.
- package/CHANGELOG.md +12 -0
- package/dist/package.json +4 -4
- package/dist/src/__tests__/typegen.d.ts +1 -0
- package/dist/src/__tests__/typegen.js +18 -0
- package/dist/src/__tests__/typegen.js.map +1 -0
- package/dist/src/__tests__/watch.d.ts +1 -0
- package/dist/src/__tests__/watch.js +32 -0
- package/dist/src/__tests__/watch.js.map +1 -0
- package/dist/src/__tests__/write.d.ts +1 -0
- package/dist/src/__tests__/write.js +120 -0
- package/dist/src/__tests__/write.js.map +1 -0
- package/dist/src/typegen.d.ts +2 -0
- package/dist/src/typegen.js +47 -10
- package/dist/src/typegen.js.map +1 -1
- package/dist/src/write/writeTypeImports.js +68 -34
- package/dist/src/write/writeTypeImports.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/__tests__/typegen.ts +22 -0
- package/src/__tests__/watch.ts +44 -0
- package/src/__tests__/write.ts +155 -0
- package/src/test-support/watch-mode-smoke.js +140 -0
- package/src/test-support/write-mode-smoke.js +147 -0
- package/src/typegen.ts +66 -16
- package/src/write/writeTypeImports.ts +82 -49
|
@@ -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 =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
const runScheduledTypegen = async () => {
|
|
98
|
+
if (runningTypegen) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
97
101
|
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
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)}`)
|