kea-typegen 3.7.1 → 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 +5 -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/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/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/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/cli/typegen.ts +4 -0
- package/src/print/print.ts +102 -57
- package/src/types.ts +2 -0
- package/src/utils.ts +81 -0
- package/src/visit/visit.ts +14 -0
- package/src/write/writeInlineLogicTypes.ts +305 -0
- package/src/write/writeTypeImports.ts +4 -4
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as os from 'os'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import * as ts from 'typescript'
|
|
5
|
+
import { AppOptions } from '../types'
|
|
6
|
+
import { programFromSource } from '../utils'
|
|
7
|
+
import { visitProgram } from '../visit/visit'
|
|
8
|
+
import { printToFiles } from '../print/print'
|
|
9
|
+
|
|
10
|
+
describe('manually typed logics', () => {
|
|
11
|
+
test('skips logics typed with MakeLogicType', () => {
|
|
12
|
+
const logicSource = `
|
|
13
|
+
import { kea, MakeLogicType } from 'kea'
|
|
14
|
+
|
|
15
|
+
interface Values { name: string }
|
|
16
|
+
interface Actions { updateName: (name: string) => { name: string } }
|
|
17
|
+
|
|
18
|
+
const logic = kea<MakeLogicType<Values, Actions>>({
|
|
19
|
+
actions: () => ({
|
|
20
|
+
updateName: (name: string) => ({ name }),
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
23
|
+
`
|
|
24
|
+
expect(visitProgram(programFromSource(logicSource)).length).toBe(0)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('skips logics typed with a custom local type, even when named like a generated one', () => {
|
|
28
|
+
const logicSource = `
|
|
29
|
+
import { kea, MakeLogicType } from 'kea'
|
|
30
|
+
|
|
31
|
+
export type logicType = MakeLogicType<{ name: string }, { updateName: (name: string) => { name: string } }>
|
|
32
|
+
|
|
33
|
+
const logic = kea<logicType>({
|
|
34
|
+
actions: () => ({
|
|
35
|
+
updateName: (name: string) => ({ name }),
|
|
36
|
+
}),
|
|
37
|
+
})
|
|
38
|
+
`
|
|
39
|
+
expect(visitProgram(programFromSource(logicSource)).length).toBe(0)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('does not skip local logic types carrying the kea-typegen marker', () => {
|
|
43
|
+
const logicSource = `
|
|
44
|
+
import { kea, MakeLogicType } from 'kea'
|
|
45
|
+
|
|
46
|
+
// Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.
|
|
47
|
+
export type logicType = MakeLogicType<{ name: string }, { updateName: (name: string) => { name: string } }>
|
|
48
|
+
|
|
49
|
+
const logic = kea<logicType>({
|
|
50
|
+
actions: () => ({
|
|
51
|
+
updateName: (name: string) => ({ name }),
|
|
52
|
+
}),
|
|
53
|
+
})
|
|
54
|
+
`
|
|
55
|
+
const parsedLogics = visitProgram(programFromSource(logicSource))
|
|
56
|
+
expect(parsedLogics.length).toBe(1)
|
|
57
|
+
expect(parsedLogics[0].actions.map((a) => a.name)).toEqual(['updateName'])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('does not skip generated blocks where the marker sits on the values interface', () => {
|
|
61
|
+
const logicSource = `
|
|
62
|
+
import { kea, MakeLogicType } from 'kea'
|
|
63
|
+
|
|
64
|
+
// Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.
|
|
65
|
+
export interface logicValues { name: string }
|
|
66
|
+
|
|
67
|
+
export interface logicActions { updateName: (name: string) => { name: string } }
|
|
68
|
+
|
|
69
|
+
export type logicType = MakeLogicType<logicValues, logicActions>
|
|
70
|
+
|
|
71
|
+
const logic = kea<logicType>({
|
|
72
|
+
actions: () => ({
|
|
73
|
+
updateName: (name: string) => ({ name }),
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
`
|
|
77
|
+
const parsedLogics = visitProgram(programFromSource(logicSource))
|
|
78
|
+
expect(parsedLogics.length).toBe(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('does not skip logics with a generated type that does not resolve yet', () => {
|
|
82
|
+
const logicSource = `
|
|
83
|
+
import { kea } from 'kea'
|
|
84
|
+
|
|
85
|
+
const logic = kea<logicType>({
|
|
86
|
+
actions: () => ({
|
|
87
|
+
updateName: (name: string) => ({ name }),
|
|
88
|
+
}),
|
|
89
|
+
})
|
|
90
|
+
`
|
|
91
|
+
expect(visitProgram(programFromSource(logicSource)).length).toBe(1)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('inline mode', () => {
|
|
96
|
+
const createProgram = (fileNames: string[]) =>
|
|
97
|
+
ts.createProgram(fileNames, {
|
|
98
|
+
module: ts.ModuleKind.CommonJS,
|
|
99
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
100
|
+
target: ts.ScriptTarget.ES2020,
|
|
101
|
+
skipLibCheck: true,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('writes and updates MakeLogicType blocks above the logic', async () => {
|
|
105
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-inline-'))
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const logicDir = path.join(tempDir, 'src')
|
|
109
|
+
const logicPath = path.join(logicDir, 'logic.ts')
|
|
110
|
+
const logicTypePath = path.join(logicDir, 'logicType.ts')
|
|
111
|
+
const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
|
|
112
|
+
|
|
113
|
+
fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
|
|
114
|
+
fs.mkdirSync(logicDir, { recursive: true })
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
keaDtsPath,
|
|
118
|
+
[
|
|
119
|
+
'export function kea<T = any>(input: any): T',
|
|
120
|
+
'export interface MakeLogicType<V = any, A = any, P = any> {}',
|
|
121
|
+
'',
|
|
122
|
+
].join('\n'),
|
|
123
|
+
)
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
logicPath,
|
|
126
|
+
[
|
|
127
|
+
"import { kea } from 'kea'",
|
|
128
|
+
'',
|
|
129
|
+
'export const logic = kea({',
|
|
130
|
+
' props: {} as { id: number },',
|
|
131
|
+
' actions: () => ({',
|
|
132
|
+
' setValue: (value: string) => ({ value }),',
|
|
133
|
+
' }),',
|
|
134
|
+
' reducers: () => ({',
|
|
135
|
+
" value: ['' as string, { setValue: (_, { value }) => value }],",
|
|
136
|
+
' }),',
|
|
137
|
+
'})',
|
|
138
|
+
'',
|
|
139
|
+
].join('\n'),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const appOptions: AppOptions = {
|
|
143
|
+
rootPath: logicDir,
|
|
144
|
+
typesPath: logicDir,
|
|
145
|
+
packageJsonPath: path.join(tempDir, 'package.json'),
|
|
146
|
+
inline: true,
|
|
147
|
+
write: true,
|
|
148
|
+
log: () => {},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const program = createProgram([logicPath])
|
|
152
|
+
const parsedLogics = visitProgram(program, appOptions)
|
|
153
|
+
expect(parsedLogics.length).toBe(1)
|
|
154
|
+
|
|
155
|
+
const response = await printToFiles(program, appOptions, parsedLogics)
|
|
156
|
+
expect(response.writtenFiles).toBe(1)
|
|
157
|
+
|
|
158
|
+
const writtenLogic = fs.readFileSync(logicPath, 'utf8')
|
|
159
|
+
|
|
160
|
+
expect(writtenLogic).toContain("import { MakeLogicType, kea } from 'kea'")
|
|
161
|
+
expect(writtenLogic).toContain('// Generated by kea-typegen. DO NOT EDIT THIS BLOCK MANUALLY.')
|
|
162
|
+
expect(writtenLogic).toContain('export interface logicValues {')
|
|
163
|
+
expect(writtenLogic).toContain('export interface logicActions {')
|
|
164
|
+
expect(writtenLogic).toContain('export interface logicProps {')
|
|
165
|
+
expect(writtenLogic).toContain('export type logicType = MakeLogicType<logicValues, logicActions, logicProps>')
|
|
166
|
+
expect(writtenLogic).toContain('export const logic = kea<logicType>({')
|
|
167
|
+
expect(writtenLogic).toContain('value: string')
|
|
168
|
+
expect(writtenLogic).toContain('setValue: (value: string) =>')
|
|
169
|
+
expect(fs.existsSync(logicTypePath)).toBe(false)
|
|
170
|
+
|
|
171
|
+
// a second pass over the written file changes nothing
|
|
172
|
+
const secondProgram = createProgram([logicPath])
|
|
173
|
+
const secondParsedLogics = visitProgram(secondProgram, appOptions)
|
|
174
|
+
expect(secondParsedLogics.length).toBe(1)
|
|
175
|
+
|
|
176
|
+
const secondResponse = await printToFiles(secondProgram, appOptions, secondParsedLogics)
|
|
177
|
+
expect(secondResponse).toEqual({ filesToWrite: 0, writtenFiles: 0, filesToModify: 0 })
|
|
178
|
+
expect(fs.readFileSync(logicPath, 'utf8')).toBe(writtenLogic)
|
|
179
|
+
|
|
180
|
+
// adding an action updates the existing block in place
|
|
181
|
+
fs.writeFileSync(
|
|
182
|
+
logicPath,
|
|
183
|
+
fs
|
|
184
|
+
.readFileSync(logicPath, 'utf8')
|
|
185
|
+
.replace('setValue: (value: string) => ({ value }),', 'setValue: (value: string) => ({ value }),\n reset: true,'),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
const thirdProgram = createProgram([logicPath])
|
|
189
|
+
const thirdResponse = await printToFiles(thirdProgram, appOptions, visitProgram(thirdProgram, appOptions))
|
|
190
|
+
expect(thirdResponse.writtenFiles).toBe(1)
|
|
191
|
+
|
|
192
|
+
const updatedLogic = fs.readFileSync(logicPath, 'utf8')
|
|
193
|
+
expect(updatedLogic).toContain('reset: ')
|
|
194
|
+
expect(updatedLogic.match(/export type logicType = MakeLogicType</g)?.length).toBe(1)
|
|
195
|
+
expect(updatedLogic.match(/export interface logicValues \{/g)?.length).toBe(1)
|
|
196
|
+
expect(updatedLogic.match(/export interface logicActions \{/g)?.length).toBe(1)
|
|
197
|
+
expect(updatedLogic.match(/DO NOT EDIT THIS BLOCK MANUALLY/g)?.length).toBe(1)
|
|
198
|
+
} finally {
|
|
199
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('replaces the old logicType.ts import when switching to inline mode', async () => {
|
|
204
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-inline-migrate-'))
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const logicDir = path.join(tempDir, 'src')
|
|
208
|
+
const logicPath = path.join(logicDir, 'logic.ts')
|
|
209
|
+
const logicTypePath = path.join(logicDir, 'logicType.ts')
|
|
210
|
+
const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
|
|
211
|
+
|
|
212
|
+
fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
|
|
213
|
+
fs.mkdirSync(logicDir, { recursive: true })
|
|
214
|
+
|
|
215
|
+
fs.writeFileSync(
|
|
216
|
+
keaDtsPath,
|
|
217
|
+
[
|
|
218
|
+
'export function kea<T = any>(input: any): T',
|
|
219
|
+
'export interface MakeLogicType<V = any, A = any, P = any> {}',
|
|
220
|
+
'',
|
|
221
|
+
].join('\n'),
|
|
222
|
+
)
|
|
223
|
+
fs.writeFileSync(logicTypePath, 'export interface logicType {}\n')
|
|
224
|
+
fs.writeFileSync(
|
|
225
|
+
logicPath,
|
|
226
|
+
[
|
|
227
|
+
"import { kea } from 'kea'",
|
|
228
|
+
"import type { logicType } from './logicType'",
|
|
229
|
+
'',
|
|
230
|
+
'export const logic = kea<logicType>({',
|
|
231
|
+
' actions: () => ({',
|
|
232
|
+
' setValue: (value: string) => ({ value }),',
|
|
233
|
+
' }),',
|
|
234
|
+
'})',
|
|
235
|
+
'',
|
|
236
|
+
].join('\n'),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const appOptions: AppOptions = {
|
|
240
|
+
rootPath: logicDir,
|
|
241
|
+
typesPath: logicDir,
|
|
242
|
+
packageJsonPath: path.join(tempDir, 'package.json'),
|
|
243
|
+
inline: true,
|
|
244
|
+
write: true,
|
|
245
|
+
delete: true,
|
|
246
|
+
log: () => {},
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const program = createProgram([logicPath])
|
|
250
|
+
const response = await printToFiles(program, appOptions, visitProgram(program, appOptions))
|
|
251
|
+
expect(response.writtenFiles).toBe(1)
|
|
252
|
+
|
|
253
|
+
const writtenLogic = fs.readFileSync(logicPath, 'utf8')
|
|
254
|
+
|
|
255
|
+
expect(writtenLogic).not.toContain("from './logicType'")
|
|
256
|
+
expect(writtenLogic).toContain("import { MakeLogicType, kea } from 'kea'")
|
|
257
|
+
expect(writtenLogic).toContain('export interface logicActions {')
|
|
258
|
+
// no values or props on this logic, so no interfaces are generated for them
|
|
259
|
+
expect(writtenLogic).toContain('export type logicType = MakeLogicType<{}, logicActions>')
|
|
260
|
+
expect(writtenLogic).toContain('export const logic = kea<logicType>({')
|
|
261
|
+
expect(fs.existsSync(logicTypePath)).toBe(false)
|
|
262
|
+
} finally {
|
|
263
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
})
|
package/src/cli/typegen.ts
CHANGED
|
@@ -39,6 +39,10 @@ yargs
|
|
|
39
39
|
.option('delete', { describe: 'Delete logicType.ts files without a corresponding logic.ts', type: 'boolean' })
|
|
40
40
|
.option('add-ts-nocheck', { describe: 'Add @ts-nocheck to top of logicType.ts files', type: 'boolean' })
|
|
41
41
|
.option('convert-to-builders', { describe: 'Convert Kea 2.0 inputs to Kea 3.0 logic builders', type: 'boolean' })
|
|
42
|
+
.option('inline', {
|
|
43
|
+
describe: 'Write logic types as MakeLogicType blocks above each kea() call, instead of logicType.ts files',
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
})
|
|
42
46
|
.option('import-global-types', {
|
|
43
47
|
describe: 'Add import statements in logicType.ts files for global types (e.g. @types/node)',
|
|
44
48
|
type: 'boolean',
|
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)
|
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
|
|