kea-typegen 3.4.7 → 3.6.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.
@@ -1,4 +1,9 @@
1
- import { sourceToSourceFile, programFromSource, logicSourceToLogicType } from '../utils'
1
+ import { sourceToSourceFile, programFromSource, logicSourceToLogicType, gatherImports } from '../utils'
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 { ParsedLogic } from '../types'
2
7
  import { SyntaxKind } from 'typescript'
3
8
 
4
9
  test('sourceToSourceFile', () => {
@@ -35,3 +40,75 @@ test('logicSourceToLogicType', () => {
35
40
 
36
41
  expect(string).toContain('export interface myRandomLogicType extends Logic {')
37
42
  })
43
+
44
+ test('gatherImports prefers source package path for re-exported npm types', () => {
45
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-imports-'))
46
+ try {
47
+ const logicPath = path.join(tempDir, 'logic.ts')
48
+ const keaPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
49
+ const reactGridLayoutIndexPath = path.join(tempDir, 'node_modules', 'react-grid-layout', 'index.d.ts')
50
+ const reactGridLayoutTypesPath = path.join(
51
+ tempDir,
52
+ 'node_modules',
53
+ 'react-grid-layout',
54
+ 'dist',
55
+ 'types-BiXsdXr7.d.ts',
56
+ )
57
+
58
+ fs.mkdirSync(path.dirname(keaPath), { recursive: true })
59
+ fs.mkdirSync(path.dirname(reactGridLayoutTypesPath), { recursive: true })
60
+
61
+ fs.writeFileSync(
62
+ logicPath,
63
+ [
64
+ "import { kea } from 'kea'",
65
+ "import type { Layout, LayoutItem } from 'react-grid-layout'",
66
+ '',
67
+ 'export const dashboardLogic = kea({',
68
+ ' reducers: () => ({',
69
+ ' currentLayout: [null as Layout | null, {}],',
70
+ ' responsiveLayouts: [null as Record<string, LayoutItem[]> | null, {}],',
71
+ ' }),',
72
+ '})',
73
+ ].join('\n'),
74
+ )
75
+ fs.writeFileSync(keaPath, 'export function kea(input: any): any')
76
+ fs.writeFileSync(
77
+ reactGridLayoutIndexPath,
78
+ "export { L as Layout, a as LayoutItem } from './dist/types-BiXsdXr7'\n",
79
+ )
80
+ fs.writeFileSync(
81
+ reactGridLayoutTypesPath,
82
+ ['export type L = { i: string }', 'export type a = { x: number }'].join('\n'),
83
+ )
84
+
85
+ const program = ts.createProgram([logicPath], {
86
+ module: ts.ModuleKind.CommonJS,
87
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
88
+ target: ts.ScriptTarget.ES2020,
89
+ skipLibCheck: true,
90
+ })
91
+ const sourceFile = program.getSourceFile(logicPath)
92
+ if (!sourceFile) {
93
+ throw new Error('Expected test source file to exist')
94
+ }
95
+
96
+ const parsedLogic = {
97
+ node: sourceFile,
98
+ typeReferencesToImportFromFiles: {},
99
+ } as unknown as ParsedLogic
100
+
101
+ gatherImports(sourceFile, program.getTypeChecker(), parsedLogic)
102
+
103
+ expect(parsedLogic.typeReferencesToImportFromFiles['react-grid-layout']).toEqual(
104
+ new Set(['Layout', 'LayoutItem']),
105
+ )
106
+ expect(
107
+ Object.keys(parsedLogic.typeReferencesToImportFromFiles).some((importPath) =>
108
+ importPath.includes('react-grid-layout/dist/types-BiXsdXr7'),
109
+ ),
110
+ ).toBe(false)
111
+ } finally {
112
+ fs.rmSync(tempDir, { recursive: true, force: true })
113
+ }
114
+ })
@@ -8,30 +8,15 @@ import { AppOptions } from '../types'
8
8
  import { runTypeGen } from '../typegen'
9
9
 
10
10
  yargs
11
- .command(
12
- 'check',
13
- '- check what should be done',
14
- (yargs) => {},
15
- (argv) => {
16
- runTypeGen({ ...includeKeaConfig(parsedToAppOptions(argv)), write: false, watch: false })
17
- },
18
- )
19
- .command(
20
- 'write',
21
- '- write logicType.ts files',
22
- (yargs) => {},
23
- (argv) => {
24
- runTypeGen({ ...includeKeaConfig(parsedToAppOptions(argv)), write: true, watch: false })
25
- },
26
- )
27
- .command(
28
- 'watch',
29
- '- watch for changes and write logicType.ts files',
30
- (yargs) => {},
31
- (argv) => {
32
- runTypeGen({ ...includeKeaConfig(parsedToAppOptions(argv)), write: true, watch: true })
33
- },
34
- )
11
+ .command('check', '- check what should be done', {}, async (argv) => {
12
+ await runTypeGen({ ...includeKeaConfig(parsedToAppOptions(argv)), write: false, watch: false })
13
+ })
14
+ .command('write', '- write logicType.ts files', {}, async (argv) => {
15
+ await runTypeGen({ ...includeKeaConfig(parsedToAppOptions(argv)), write: true, watch: false })
16
+ })
17
+ .command('watch', '- watch for changes and write logicType.ts files', {}, async (argv) => {
18
+ await runTypeGen({ ...includeKeaConfig(parsedToAppOptions(argv)), write: true, watch: true })
19
+ })
35
20
  .option('config', { alias: 'c', describe: 'Path to tsconfig.json (otherwise auto-detected)', type: 'string' })
36
21
  .option('file', { alias: 'f', describe: "Single file to evaluate (can't be used with --config)", type: 'string' })
37
22
  .option('root', {
@@ -69,7 +54,8 @@ yargs
69
54
  .option('verbose', { describe: 'Slightly more verbose output log', type: 'boolean' })
70
55
  .demandCommand()
71
56
  .help()
72
- .wrap(80).argv
57
+ .wrap(80)
58
+ .parse()
73
59
 
74
60
  function parsedToAppOptions(parsedOptions) {
75
61
  const { root, types, config, file, ...rest } = parsedOptions
@@ -110,13 +96,13 @@ function includeKeaConfig(appOptions: AppOptions): AppOptions {
110
96
  const configDirPath = path.dirname(configFilePath)
111
97
  try {
112
98
  rawData = fs.readFileSync(configFilePath)
113
- } catch (e) {
99
+ } catch {
114
100
  console.error(`Error reading Kea config file: ${configFilePath}`)
115
101
  process.exit(1)
116
102
  }
117
103
  try {
118
104
  keaConfig = JSON.parse(rawData)
119
- } catch (e) {
105
+ } catch {
120
106
  console.error(`Error parsing Kea config JSON: ${configFilePath}`)
121
107
  process.exit(1)
122
108
  }
@@ -40,11 +40,11 @@ import { printInternalExtraInput } from './printInternalExtraInput'
40
40
  import { convertToBuilders } from '../write/convertToBuilders'
41
41
  import { cacheWrittenFile } from '../cache'
42
42
 
43
- export function runThroughPrettier(sourceText: string, filePath: string): string {
44
- const options = prettier.resolveConfig.sync(filePath)
43
+ export async function runThroughPrettier(sourceText: string, filePath: string): Promise<string> {
44
+ const options = await prettier.resolveConfig(filePath)
45
45
  if (options) {
46
46
  try {
47
- return prettier.format(sourceText, { ...options, filepath: filePath })
47
+ return await prettier.format(sourceText, { ...options, filepath: filePath })
48
48
  } catch (e) {
49
49
  console.error(`!! Prettier: Error formatting "${filePath}"`)
50
50
  console.error(e.message)
@@ -56,11 +56,11 @@ export function runThroughPrettier(sourceText: string, filePath: string): string
56
56
  }
57
57
 
58
58
  // returns files to write
59
- export function printToFiles(
59
+ export async function printToFiles(
60
60
  program: Program,
61
61
  appOptions: AppOptions,
62
62
  parsedLogics: ParsedLogic[],
63
- ): { filesToWrite: number; writtenFiles: number; filesToModify: number } {
63
+ ): Promise<{ filesToWrite: number; writtenFiles: number; filesToModify: number }> {
64
64
  const { log } = appOptions
65
65
 
66
66
  const groupedByFile: Record<string, ParsedLogic[]> = {}
@@ -101,13 +101,16 @@ export function printToFiles(
101
101
  let filesToWrite = 0
102
102
  let filesToModify = 0
103
103
 
104
- Object.entries(groupedByFile).forEach(([fileName, parsedLogics]) => {
104
+ for (const [fileName, parsedLogics] of Object.entries(groupedByFile)) {
105
105
  const typeFileName = parsedLogics[0].typeFileName
106
106
 
107
107
  const logicStrings = []
108
108
  const requiredKeys = new Set(['Logic'])
109
109
  for (const parsedLogic of parsedLogics) {
110
- const logicTypeStirng = runThroughPrettier(nodeToString(parsedLogic.interfaceDeclaration), typeFileName)
110
+ const logicTypeStirng = await runThroughPrettier(
111
+ nodeToString(parsedLogic.interfaceDeclaration),
112
+ typeFileName,
113
+ )
111
114
  logicStrings.push(logicTypeStirng)
112
115
  for (const string of parsedLogic.importFromKeaInLogicType.values()) {
113
116
  requiredKeys.add(string)
@@ -232,7 +235,7 @@ export function printToFiles(
232
235
  const logicsNeedingImports = parsedLogics.filter(parsedLogicNeedsTypeImport)
233
236
  if (logicsNeedingImports.length > 0) {
234
237
  if (appOptions.write && !appOptions.noImport) {
235
- writeTypeImports(appOptions, program, fileName, logicsNeedingImports, parsedLogics)
238
+ await writeTypeImports(appOptions, program, fileName, logicsNeedingImports, parsedLogics)
236
239
  filesToModify += logicsNeedingImports.length
237
240
  } else {
238
241
  log(
@@ -276,7 +279,7 @@ export function printToFiles(
276
279
  )
277
280
  }
278
281
  }
279
- })
282
+ }
280
283
 
281
284
  if (writtenFiles === 0 && filesToModify === 0) {
282
285
  if (appOptions.write) {
package/src/typegen.ts CHANGED
@@ -16,7 +16,7 @@ if (parseInt(ts.versionMajorMinor.split('.')[0]) < 5) {
16
16
  ;(ts as any).defaultMaximumTruncationLength = Infinity
17
17
  }
18
18
 
19
- export function runTypeGen(appOptions: AppOptions) {
19
+ export async function runTypeGen(appOptions: AppOptions) {
20
20
  let program: Program
21
21
  let resetProgram: () => void
22
22
 
@@ -49,7 +49,7 @@ export function runTypeGen(appOptions: AppOptions) {
49
49
  compilerOptions.options,
50
50
  {
51
51
  ...ts.sys,
52
- writeFile(path: string, data: string, writeByteOrderMark?: boolean) {
52
+ writeFile(_path: string, _data: string, _writeByteOrderMark?: boolean) {
53
53
  // skip emit
54
54
  // https://github.com/microsoft/TypeScript/issues/32385
55
55
  // https://github.com/microsoft/TypeScript/issues/36917
@@ -89,11 +89,11 @@ export function runTypeGen(appOptions: AppOptions) {
89
89
  }
90
90
  const origPostProgramCreate = host.afterProgramCreate
91
91
 
92
- host.afterProgramCreate = (prog) => {
92
+ host.afterProgramCreate = async (prog) => {
93
93
  program = prog.getProgram()
94
94
  origPostProgramCreate!(prog)
95
95
 
96
- goThroughAllTheFiles(program, appOptions)
96
+ await goThroughAllTheFiles(program, appOptions)
97
97
  }
98
98
 
99
99
  ts.createWatchProgram(host)
@@ -108,16 +108,16 @@ export function runTypeGen(appOptions: AppOptions) {
108
108
  log(`⛔ No tsconfig.json found! No source file specified.`)
109
109
  }
110
110
 
111
- function goThroughAllTheFiles(
111
+ async function goThroughAllTheFiles(
112
112
  program,
113
113
  appOptions,
114
- ): { filesToWrite: number; writtenFiles: number; filesToModify: number } {
114
+ ): Promise<{ filesToWrite: number; writtenFiles: number; filesToModify: number }> {
115
115
  const parsedLogics = visitProgram(program, appOptions)
116
116
  if (appOptions.verbose) {
117
117
  log(`🗒️ ${parsedLogics.length} logic${parsedLogics.length === 1 ? '' : 's'} found!`)
118
118
  }
119
119
 
120
- const response = printToFiles(program, appOptions, parsedLogics)
120
+ const response = await printToFiles(program, appOptions, parsedLogics)
121
121
 
122
122
  // running "kea-typegen check" and would write files?
123
123
  // exit with 1
@@ -136,7 +136,7 @@ export function runTypeGen(appOptions: AppOptions) {
136
136
 
137
137
  let round = 0
138
138
  while ((round += 1)) {
139
- const { writtenFiles, filesToModify } = goThroughAllTheFiles(program, appOptions)
139
+ const { writtenFiles, filesToModify } = await goThroughAllTheFiles(program, appOptions)
140
140
 
141
141
  if (writtenFiles === 0 && filesToModify === 0) {
142
142
  log(`👋 Finished writing files! Exiting.`)
package/src/utils.ts CHANGED
@@ -24,7 +24,7 @@ export function programFromSource(sourceCode: string) {
24
24
  }
25
25
 
26
26
  function rejectImportPath(path: string): boolean {
27
- if (path.includes('/node_modules/typescript/')) {
27
+ if (path.includes('/node_modules/typescript/') || path === 'typescript' || path.startsWith('typescript/')) {
28
28
  return true
29
29
  }
30
30
  return false
@@ -274,13 +274,116 @@ export function storeExtractedSymbol(
274
274
  // Also checking isTypeNameAlreadyImported to prevent multiple imports of the same type,
275
275
  // which we were seeing when importing from `export * from ...` files
276
276
  if (typeName && !isTypeNameAlreadyImported(parsedLogic, typeName)) {
277
- const importFilename = getFilenameForNode(declaration, checker)
277
+ const importFilenameFromNode = getFilenameForNode(declaration, checker)
278
+ const sourceFileImportedPackagePath =
279
+ importFilenameFromNode &&
280
+ findImportPathForPackageInSourceFile(
281
+ parsedLogic.node.getSourceFile(),
282
+ importFilenameFromNode,
283
+ typeName.split('.')[0],
284
+ checker,
285
+ )
286
+ const importFilename = sourceFileImportedPackagePath || importFilenameFromNode
278
287
  if (importFilename && !rejectImportPath(importFilename)) {
279
288
  addTypeImport(parsedLogic, importFilename, typeName)
280
289
  }
281
290
  }
282
291
  }
283
292
 
293
+ function findImportPathForPackageInSourceFile(
294
+ sourceFile: ts.SourceFile,
295
+ importFilename: string,
296
+ typeName: string,
297
+ checker: ts.TypeChecker,
298
+ ): string | undefined {
299
+ const packageName = extractNpmPackageName(importFilename)
300
+ if (!packageName) {
301
+ return
302
+ }
303
+
304
+ for (const statement of sourceFile.statements) {
305
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) {
306
+ continue
307
+ }
308
+ const importPath = statement.moduleSpecifier.text
309
+ if (
310
+ extractNpmPackageName(importPath) === packageName &&
311
+ moduleSpecifierExportsTypeName(statement.moduleSpecifier, typeName, checker)
312
+ ) {
313
+ return importPath
314
+ }
315
+ }
316
+ }
317
+
318
+ function moduleSpecifierExportsTypeName(
319
+ moduleSpecifier: ts.Expression,
320
+ typeName: string,
321
+ checker: ts.TypeChecker,
322
+ ): boolean {
323
+ const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier)
324
+ if (!moduleSymbol) {
325
+ return false
326
+ }
327
+
328
+ return checker.getExportsOfModule(moduleSymbol).some((moduleExport) => moduleExport.getName() === typeName)
329
+ }
330
+
331
+ function extractNpmPackageName(input: string): string | undefined {
332
+ const packagePath = extractNodeModulesPackagePath(input)
333
+ if (packagePath) {
334
+ const normalizedPackagePath = packagePath.replace(/\\/g, '/')
335
+ if (normalizedPackagePath === 'typescript' || normalizedPackagePath.startsWith('typescript/')) {
336
+ return
337
+ }
338
+ if (normalizedPackagePath.startsWith('@types/')) {
339
+ return
340
+ }
341
+ if (normalizedPackagePath.startsWith('@')) {
342
+ const [scope, name] = normalizedPackagePath.split('/')
343
+ return scope && name ? `${scope}/${name}` : undefined
344
+ }
345
+ return normalizedPackagePath.split('/')[0]
346
+ }
347
+
348
+ if (input.startsWith('@types/')) {
349
+ return
350
+ }
351
+ if (input.startsWith('@')) {
352
+ const [scope, name] = input.split('/')
353
+ if (!scope || scope.length <= 1 || !name) {
354
+ return
355
+ }
356
+ return `${scope}/${name}`
357
+ }
358
+ if (!input.startsWith('.') && !input.startsWith('/') && !input.startsWith('~')) {
359
+ return input.split('/')[0]
360
+ }
361
+ }
362
+
363
+ function extractNodeModulesPackagePath(input: string): string | undefined {
364
+ const normalizedInput = input.replace(/\\/g, '/')
365
+ const nodeModulesMarker = '/node_modules/'
366
+ if (!normalizedInput.includes(nodeModulesMarker)) {
367
+ return
368
+ }
369
+
370
+ let packagePath = normalizedInput.split(nodeModulesMarker).pop()
371
+ if (!packagePath) {
372
+ return
373
+ }
374
+ if (packagePath.startsWith('.pnpm/')) {
375
+ const pnpmNodeModulesMarker = '/node_modules/'
376
+ if (!packagePath.includes(pnpmNodeModulesMarker)) {
377
+ return
378
+ }
379
+ packagePath = packagePath.split(pnpmNodeModulesMarker).pop()
380
+ if (!packagePath) {
381
+ return
382
+ }
383
+ }
384
+ return packagePath
385
+ }
386
+
284
387
  export function getFilenameForImportDeclaration(checker: ts.TypeChecker, importNode: ts.ImportDeclaration): string {
285
388
  const moduleSymbol = checker.getSymbolAtLocation(importNode.moduleSpecifier)
286
389
  const otherSourceFile = moduleSymbol?.getDeclarations()[0].getSourceFile()
@@ -4,7 +4,7 @@ import { print, visit } from 'recast'
4
4
  import { runThroughPrettier } from '../print/print'
5
5
  import * as fs from 'fs'
6
6
  import * as osPath from 'path'
7
- import { t, b, visitAllKeaCalls, assureImport, getAst } from "./utils";
7
+ import { t, b, visitAllKeaCalls, assureImport, getAst } from './utils'
8
8
 
9
9
  const supportedProperties = {
10
10
  props: 'kea',
@@ -33,7 +33,7 @@ const supportedProperties = {
33
33
  events: 'kea',
34
34
  }
35
35
 
36
- export function convertToBuilders(
36
+ export async function convertToBuilders(
37
37
  appOptions: AppOptions,
38
38
  program: ts.Program,
39
39
  filename: string,
@@ -120,7 +120,7 @@ export function convertToBuilders(
120
120
  }
121
121
  }
122
122
 
123
- const newText = runThroughPrettier(print(ast).code, filename)
123
+ const newText = await runThroughPrettier(print(ast).code, filename)
124
124
  fs.writeFileSync(filename, newText)
125
125
 
126
126
  log(`🔥 Converted to builders: ${osPath.relative(process.cwd(), filename)}`)
@@ -4,9 +4,14 @@ import { print, visit } from 'recast'
4
4
  import { runThroughPrettier } from '../print/print'
5
5
  import * as fs from 'fs'
6
6
  import * as osPath from 'path'
7
- import { t, b, visitAllKeaCalls, assureImport, getAst } from "./utils";
7
+ import { t, b, visitAllKeaCalls, assureImport, getAst } from './utils'
8
8
 
9
- export function writePaths(appOptions: AppOptions, program: ts.Program, filename: string, parsedLogics: ParsedLogic[]) {
9
+ export async function writePaths(
10
+ appOptions: AppOptions,
11
+ program: ts.Program,
12
+ filename: string,
13
+ parsedLogics: ParsedLogic[],
14
+ ) {
10
15
  const { log } = appOptions
11
16
  const sourceFile = program.getSourceFile(filename)
12
17
  const rawCode = sourceFile.getText()
@@ -52,7 +57,7 @@ export function writePaths(appOptions: AppOptions, program: ts.Program, filename
52
57
  visitAllKeaCalls(ast, parsedLogics, filename, ({ path, parsedLogic }) => {
53
58
  const stmt = path.node
54
59
  const arg = stmt.arguments[0]
55
- const logicPath = parsedLogic.path.filter(p => p !== '..')
60
+ const logicPath = parsedLogic.path.filter((p) => p !== '..')
56
61
 
57
62
  if (t.ObjectExpression.check(arg)) {
58
63
  const pathProperty = arg.properties.find(
@@ -80,7 +85,7 @@ export function writePaths(appOptions: AppOptions, program: ts.Program, filename
80
85
  }
81
86
  })
82
87
 
83
- const newText = runThroughPrettier(print(ast).code, filename)
88
+ const newText = await runThroughPrettier(print(ast).code, filename)
84
89
  fs.writeFileSync(filename, newText)
85
90
 
86
91
  log(`🔥 Path added: ${osPath.relative(process.cwd(), filename)}`)
@@ -6,7 +6,7 @@ import { runThroughPrettier } from '../print/print'
6
6
  import * as fs from 'fs'
7
7
  import { t, b, visitAllKeaCalls, getAst } from './utils'
8
8
 
9
- export function writeTypeImports(
9
+ export async function writeTypeImports(
10
10
  appOptions: AppOptions,
11
11
  program: ts.Program,
12
12
  filename: string,
@@ -75,7 +75,7 @@ export function writeTypeImports(
75
75
  ])
76
76
  })
77
77
 
78
- const newText = runThroughPrettier(print(ast).code, filename)
78
+ const newText = await runThroughPrettier(print(ast).code, filename)
79
79
  fs.writeFileSync(filename, newText)
80
80
 
81
81
  log(`🔥 Import added: ${osPath.relative(process.cwd(), filename)}`)