@buildvision/nest-sdk-generator 2.3.1 → 3.0.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/.eslintrc.js +1 -1
- package/.github/workflows/test.yml +35 -0
- package/README.md +402 -239
- package/build/analyzer/classdeps.js +36 -4
- package/build/analyzer/controller.d.ts +2 -14
- package/build/analyzer/controller.js +38 -18
- package/build/analyzer/controllers.d.ts +2 -2
- package/build/analyzer/controllers.js +37 -5
- package/build/analyzer/extractor.d.ts +2 -41
- package/build/analyzer/extractor.js +63 -66
- package/build/analyzer/index.d.ts +2 -6
- package/build/analyzer/index.js +107 -86
- package/build/analyzer/methods.d.ts +2 -30
- package/build/analyzer/methods.js +7 -16
- package/build/analyzer/module.js +36 -4
- package/build/analyzer/params.d.ts +3 -39
- package/build/analyzer/params.js +17 -37
- package/build/analyzer/route.js +8 -9
- package/build/analyzer/sdk.d.ts +146 -0
- package/build/analyzer/sdk.js +120 -0
- package/build/analyzer/typedeps.d.ts +15 -0
- package/build/analyzer/typedeps.js +62 -10
- package/build/bin.js +72 -26
- package/build/config.d.ts +13 -0
- package/build/config.js +36 -3
- package/build/engine/analyze.d.ts +16 -0
- package/build/engine/analyze.js +786 -0
- package/build/engine/ast.d.ts +24 -0
- package/build/engine/ast.js +100 -0
- package/build/engine/file-order.d.ts +17 -0
- package/build/engine/file-order.js +97 -0
- package/build/engine/host.d.ts +63 -0
- package/build/engine/host.js +16 -0
- package/build/engine/index.d.ts +15 -0
- package/build/engine/index.js +93 -0
- package/build/engine/ts-morph-compat-host.d.ts +8 -0
- package/build/engine/ts-morph-compat-host.js +18 -0
- package/build/engine/tsgo-host.d.ts +9 -0
- package/build/engine/tsgo-host.js +136 -0
- package/build/engine/typescript-host.d.ts +26 -0
- package/build/engine/typescript-host.js +142 -0
- package/build/generator/genmodules.d.ts +1 -4
- package/build/generator/genmodules.js +23 -27
- package/build/generator/gentypes.d.ts +1 -1
- package/build/generator/gentypes.js +36 -4
- package/build/generator/index.d.ts +1 -1
- package/build/generator/index.js +91 -69
- package/build/generator/prettier.js +43 -9
- package/build/generator/sdk-interface.d.ts +1 -1
- package/build/generator/sdk-interface.js +5 -5
- package/build/logging.d.ts +2 -0
- package/build/logging.js +14 -11
- package/build/rtk-query-generator/genmodules.d.ts +1 -4
- package/build/rtk-query-generator/genmodules.js +27 -31
- package/build/rtk-query-generator/index.d.ts +1 -1
- package/build/rtk-query-generator/index.js +87 -65
- package/build/rtk-query-generator/prettier.js +43 -9
- package/build/rtk-query-generator/sdk-interface.js +3 -4
- package/build/utils.js +38 -6
- package/package.json +29 -11
- package/src/analyzer/controller.ts +3 -31
- package/src/analyzer/controllers.ts +3 -2
- package/src/analyzer/extractor.ts +17 -86
- package/src/analyzer/index.ts +5 -11
- package/src/analyzer/methods.ts +5 -36
- package/src/analyzer/params.ts +13 -55
- package/src/analyzer/sdk.ts +247 -0
- package/src/analyzer/typedeps.ts +22 -4
- package/src/bin.ts +29 -5
- package/src/config.ts +15 -0
- package/src/engine/analyze.ts +1110 -0
- package/src/engine/ast.ts +105 -0
- package/src/engine/file-order.ts +104 -0
- package/src/engine/host.ts +79 -0
- package/src/engine/index.ts +79 -0
- package/src/engine/ts-morph-compat-host.ts +20 -0
- package/src/engine/tsgo-host.ts +119 -0
- package/src/engine/typescript-host.ts +159 -0
- package/src/generator/genmodules.ts +9 -4
- package/src/generator/gentypes.ts +1 -1
- package/src/generator/index.ts +1 -1
- package/src/logging.ts +6 -1
- package/src/rtk-query-generator/genmodules.ts +9 -4
- package/src/rtk-query-generator/index.ts +1 -2
- package/src/rtk-query-generator/sdk-interface.ts +2 -3
- package/tsconfig.json +7 -6
- package/build/analyzer/decorator.d.ts +0 -10
- package/build/analyzer/decorator.js +0 -29
- package/build/rtk-query-generator/gentypes.d.ts +0 -10
- package/build/rtk-query-generator/gentypes.js +0 -62
- package/build/tsconfig.tsbuildinfo +0 -1
- package/bun.lockb +0 -0
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AST helpers reproducing the semantics of the ts-morph methods used by the legacy analyzer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AnalysisHost, TsNode } from './host'
|
|
6
|
+
|
|
7
|
+
/** ts-morph's `node.getText()`: the node's source text, without leading trivia */
|
|
8
|
+
export function nodeText(node: TsNode, sourceFile: TsNode): string {
|
|
9
|
+
return sourceFile.text.substring(node.getStart(sourceFile), node.end)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** ts-morph's `node.getName()` for named and nameable declarations (class, function, method, parameter...) */
|
|
13
|
+
export function nodeName(node: TsNode, sourceFile: TsNode): string | undefined {
|
|
14
|
+
return node.name ? nodeText(node.name, sourceFile) : undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** ts-morph's `node.getDecorators()` */
|
|
18
|
+
export function getDecorators(host: AnalysisHost, node: TsNode): TsNode[] {
|
|
19
|
+
return (node.modifiers ?? []).filter((m: TsNode) => m.kind === host.SyntaxKind.Decorator)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** ts-morph's `decorator._getInnerExpression()` */
|
|
23
|
+
function decoratorInnerExpression(host: AnalysisHost, decorator: TsNode): TsNode {
|
|
24
|
+
let expr = decorator.expression
|
|
25
|
+
while (expr.kind === host.SyntaxKind.ParenthesizedExpression) expr = expr.expression
|
|
26
|
+
return expr
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** ts-morph's `decorator.getCallExpression()` */
|
|
30
|
+
export function decoratorCallExpression(host: AnalysisHost, decorator: TsNode): TsNode | undefined {
|
|
31
|
+
const expr = decoratorInnerExpression(host, decorator)
|
|
32
|
+
return expr.kind === host.SyntaxKind.CallExpression ? expr : undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** ts-morph's `decorator.getArguments()` */
|
|
36
|
+
export function decoratorArguments(host: AnalysisHost, decorator: TsNode): TsNode[] {
|
|
37
|
+
return decoratorCallExpression(host, decorator)?.arguments ?? []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** ts-morph's `decorator.getName()` */
|
|
41
|
+
export function decoratorName(host: AnalysisHost, decorator: TsNode, sourceFile: TsNode): string {
|
|
42
|
+
const callExpression = decoratorCallExpression(host, decorator)
|
|
43
|
+
let identifier = callExpression ? callExpression.expression : decoratorInnerExpression(host, decorator)
|
|
44
|
+
|
|
45
|
+
if (identifier.kind === host.SyntaxKind.PropertyAccessExpression) {
|
|
46
|
+
identifier = identifier.name
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (identifier.kind !== host.SyntaxKind.Identifier) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Expected the decorator expression '${nodeText(identifier, sourceFile)}' to be an identifier. ` +
|
|
52
|
+
`Please deal directly with 'getExpression()' on the decorator to handle more complex scenarios.`
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return nodeText(identifier, sourceFile)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** ts-morph's `Node.isStringLiteral()` */
|
|
60
|
+
export function isStringLiteral(host: AnalysisHost, node: TsNode): boolean {
|
|
61
|
+
return node.kind === host.SyntaxKind.StringLiteral
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasModifier(node: TsNode, kind: number): boolean {
|
|
65
|
+
return (node.modifiers ?? []).some((m: TsNode) => m.kind === kind)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** ts-morph's `classDeclaration.getMembers()` */
|
|
69
|
+
export function getClassMembers(host: AnalysisHost, decl: TsNode, sourceFile: TsNode): TsNode[] {
|
|
70
|
+
const SK = host.SyntaxKind
|
|
71
|
+
const isAmbient = hasModifier(decl, SK.DeclareKeyword) || sourceFile.isDeclarationFile
|
|
72
|
+
|
|
73
|
+
return (decl.members as TsNode[])
|
|
74
|
+
.filter((m) => {
|
|
75
|
+
if (isAmbient) return true
|
|
76
|
+
if (m.kind !== SK.Constructor && m.kind !== SK.MethodDeclaration) return true
|
|
77
|
+
if (m.kind === SK.MethodDeclaration && hasModifier(m, SK.AbstractKeyword)) return true
|
|
78
|
+
return m.body != null
|
|
79
|
+
})
|
|
80
|
+
.filter((m) =>
|
|
81
|
+
[
|
|
82
|
+
SK.MethodDeclaration,
|
|
83
|
+
SK.PropertyDeclaration,
|
|
84
|
+
SK.GetAccessor,
|
|
85
|
+
SK.SetAccessor,
|
|
86
|
+
SK.Constructor,
|
|
87
|
+
SK.ClassStaticBlockDeclaration,
|
|
88
|
+
].includes(m.kind)
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** ts-morph's `classDeclaration.getProperties()` / `interfaceDeclaration.getProperties()` */
|
|
93
|
+
export function getDeclarationProperties(host: AnalysisHost, decl: TsNode, sourceFile: TsNode): TsNode[] {
|
|
94
|
+
const SK = host.SyntaxKind
|
|
95
|
+
return decl.kind === SK.ClassDeclaration
|
|
96
|
+
? getClassMembers(host, decl, sourceFile).filter((m) => m.kind === SK.PropertyDeclaration)
|
|
97
|
+
: (decl.members as TsNode[]).filter((m) => m.kind === SK.PropertySignature)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** ts-morph's `classDeclaration.getExtends()` (first type only) / `interfaceDeclaration.getExtends()` (all types) */
|
|
101
|
+
export function getExtendsTypeNodes(host: AnalysisHost, decl: TsNode): TsNode[] {
|
|
102
|
+
const clause = (decl.heritageClauses ?? []).find((c: TsNode) => c.token === host.SyntaxKind.ExtendsKeyword)
|
|
103
|
+
const types: TsNode[] = clause?.types ?? []
|
|
104
|
+
return decl.kind === host.SyntaxKind.ClassDeclaration ? types.slice(0, 1) : types
|
|
105
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Order project files the way ts-morph's `project.getSourceFiles()` does
|
|
3
|
+
*
|
|
4
|
+
* The order of the source files determines the order controllers are analyzed in, which in turn determines the order
|
|
5
|
+
* of the generated modules and types.
|
|
6
|
+
*
|
|
7
|
+
* ts-morph keeps a tree of the directories containing source files. Directories without a parent in the tree are
|
|
8
|
+
* "roots", kept in the order they were created (i.e. the order files were added: the tsconfig's files, then the files
|
|
9
|
+
* they import). The tree is walked breadth-first by depth: at each depth, the roots at that depth come first, then the
|
|
10
|
+
* children of the previous depth's directories, sorted by name. The files of each directory are sorted by name.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function compareNames(a: string, b: string): number {
|
|
14
|
+
// Same comparison as ts-morph's `LocaleStringComparer`
|
|
15
|
+
const result = a.localeCompare(b, 'en-us-u-kf-upper')
|
|
16
|
+
return result < 0 ? -1 : result === 0 ? 0 : 1
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dirname(file: string): string {
|
|
20
|
+
const index = file.lastIndexOf('/')
|
|
21
|
+
return index <= 0 ? '/' : file.substring(0, index)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function depth(dir: string): number {
|
|
25
|
+
return dir.split('/').filter((part) => part.length > 0).length
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isProjectFile(file: string): boolean {
|
|
29
|
+
// Files ts-morph considers part of the project: anything outside of `node_modules` (and outside of TypeScript's
|
|
30
|
+
// bundled libraries, which tsgo exposes with non-absolute paths)
|
|
31
|
+
return (file.startsWith('/') || /^[a-zA-Z]:\//.test(file)) && file.toLowerCase().lastIndexOf('/node_modules/') === -1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the project's files, in ts-morph's order
|
|
36
|
+
* @param rootFiles The tsconfig's files, in the tsconfig's order (absolute paths, forward slashes)
|
|
37
|
+
* @param programFiles All files of the program (absolute paths, forward slashes)
|
|
38
|
+
*/
|
|
39
|
+
export function orderProjectFiles(rootFiles: string[], programFiles: string[]): string[] {
|
|
40
|
+
// ts-morph adds the tsconfig's files first, then the files the compiler requested while building the program.
|
|
41
|
+
// The latter only affect the order of directories outside of the tsconfig's ones: sort them for determinism.
|
|
42
|
+
const rootFilesSet = new Set(rootFiles)
|
|
43
|
+
const files = [...new Set([...rootFiles, ...programFiles.filter((file) => !rootFilesSet.has(file)).sort()])].filter(isProjectFile)
|
|
44
|
+
|
|
45
|
+
const filesByDir = new Map<string, string[]>()
|
|
46
|
+
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const dir = dirname(file)
|
|
49
|
+
const dirFiles = filesByDir.get(dir)
|
|
50
|
+
if (dirFiles) dirFiles.push(file)
|
|
51
|
+
else filesByDir.set(dir, [file])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Directories, in creation order
|
|
55
|
+
const dirs = [...filesByDir.keys()]
|
|
56
|
+
const dirsSet = new Set(dirs)
|
|
57
|
+
|
|
58
|
+
const hasAncestorInTree = (dir: string) => {
|
|
59
|
+
for (let parent = dirname(dir); ; parent = dirname(parent)) {
|
|
60
|
+
if (dirsSet.has(parent)) return true
|
|
61
|
+
if (parent === '/' || parent === dirname(parent)) return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const roots = dirs.filter((dir) => !hasAncestorInTree(dir))
|
|
66
|
+
|
|
67
|
+
// Link every directory to its parent, creating the intermediate directories up to the nearest directory in the tree
|
|
68
|
+
const children = new Map<string, Set<string>>()
|
|
69
|
+
|
|
70
|
+
for (const dir of dirs) {
|
|
71
|
+
if (roots.includes(dir)) continue
|
|
72
|
+
|
|
73
|
+
for (let current = dir; !roots.includes(current) && !children.get(dirname(current))?.has(current); current = dirname(current)) {
|
|
74
|
+
const parent = dirname(current)
|
|
75
|
+
const siblings = children.get(parent)
|
|
76
|
+
if (siblings) siblings.add(current)
|
|
77
|
+
else children.set(parent, new Set([current]))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Breadth-first walk, like ts-morph's `DirectoryCache.getAllByDepth()`
|
|
82
|
+
const levels = new Map<number, string[]>()
|
|
83
|
+
|
|
84
|
+
const addToLevel = (dir: string) => {
|
|
85
|
+
const level = levels.get(depth(dir))
|
|
86
|
+
if (level) level.push(dir)
|
|
87
|
+
else levels.set(depth(dir), [dir])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
roots.forEach(addToLevel)
|
|
91
|
+
|
|
92
|
+
const ordered: string[] = []
|
|
93
|
+
|
|
94
|
+
for (let currentDepth = Math.min(...levels.keys()); levels.size > 0; currentDepth++) {
|
|
95
|
+
for (const dir of levels.get(currentDepth) ?? []) {
|
|
96
|
+
ordered.push(...(filesByDir.get(dir) ?? []).sort((a, b) => compareNames(a.substring(dir.length), b.substring(dir.length))))
|
|
97
|
+
;[...(children.get(dir) ?? [])].sort((a, b) => compareNames(a.substring(dir.length), b.substring(dir.length))).forEach(addToLevel)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
levels.delete(currentDepth)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return ordered
|
|
104
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Minimal compiler interface the analyzer needs from a TypeScript engine
|
|
3
|
+
*
|
|
4
|
+
* Both the in-process TypeScript compiler API and the TypeScript 7 native (tsgo) API expose the same AST shapes
|
|
5
|
+
* (`kind`, `statements`, `members`, `modifiers`...) and the same checker operations, so the analyzer is written once
|
|
6
|
+
* against this interface. Syntax kinds differ between TypeScript versions, so they are provided by the host.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
10
|
+
|
|
11
|
+
/** A compiler AST node (TypeScript JS API or TypeScript 7 API) */
|
|
12
|
+
export type TsNode = any
|
|
13
|
+
|
|
14
|
+
/** A compiler type (TypeScript JS API or TypeScript 7 API) */
|
|
15
|
+
export type TsType = any
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Flags ts-morph uses by default when printing a type with `Type.getText()`:
|
|
19
|
+
* UseTypeOfFunction (4096) | NoTruncation (1) | UseFullyQualifiedType (64) | WriteTypeArgumentsOfSignature (32)
|
|
20
|
+
*/
|
|
21
|
+
export const TYPE_FORMAT_FLAGS = 4193
|
|
22
|
+
|
|
23
|
+
export interface SyntaxKinds {
|
|
24
|
+
readonly AbstractKeyword: number
|
|
25
|
+
readonly CallExpression: number
|
|
26
|
+
readonly ClassDeclaration: number
|
|
27
|
+
readonly ClassStaticBlockDeclaration: number
|
|
28
|
+
readonly Constructor: number
|
|
29
|
+
readonly DeclareKeyword: number
|
|
30
|
+
readonly Decorator: number
|
|
31
|
+
readonly EnumDeclaration: number
|
|
32
|
+
readonly ExtendsKeyword: number
|
|
33
|
+
readonly FunctionDeclaration: number
|
|
34
|
+
readonly GetAccessor: number
|
|
35
|
+
readonly Identifier: number
|
|
36
|
+
readonly InterfaceDeclaration: number
|
|
37
|
+
readonly MethodDeclaration: number
|
|
38
|
+
readonly ParenthesizedExpression: number
|
|
39
|
+
readonly PropertyAccessExpression: number
|
|
40
|
+
readonly PropertyDeclaration: number
|
|
41
|
+
readonly PropertySignature: number
|
|
42
|
+
readonly SetAccessor: number
|
|
43
|
+
readonly StringLiteral: number
|
|
44
|
+
readonly TypeAliasDeclaration: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AnalysisHost {
|
|
48
|
+
readonly SyntaxKind: SyntaxKinds
|
|
49
|
+
|
|
50
|
+
/** Release the engine's resources */
|
|
51
|
+
dispose(): void
|
|
52
|
+
|
|
53
|
+
/** Equivalent of ts-morph's `type.getStringIndexType()` / `type.getNumberIndexType()` */
|
|
54
|
+
getIndexType(type: TsType, keyKind: 'string' | 'number'): TsType | undefined
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Absolute paths of the project's source files: the files ts-morph would consider part of the project (the tsconfig's
|
|
58
|
+
* files and the non-`node_modules` files they import), in the order of ts-morph's `project.getSourceFiles()`
|
|
59
|
+
*/
|
|
60
|
+
getProjectFilePaths(): string[]
|
|
61
|
+
|
|
62
|
+
/** Equivalent of ts-morph's `method.getReturnType()` */
|
|
63
|
+
getReturnTypeOfDeclaration(node: TsNode): TsType
|
|
64
|
+
|
|
65
|
+
/** Get a source file of the program, if it exists */
|
|
66
|
+
getSourceFile(absolutePath: string): TsNode | undefined
|
|
67
|
+
|
|
68
|
+
/** Equivalent of ts-morph's `node.getType()` */
|
|
69
|
+
getTypeAtLocation(node: TsNode): TsType
|
|
70
|
+
|
|
71
|
+
/** Equivalent of ts-morph's `type.isObject()` */
|
|
72
|
+
isObjectType(type: TsType): boolean
|
|
73
|
+
|
|
74
|
+
/** Engine name, for logging */
|
|
75
|
+
readonly name: string
|
|
76
|
+
|
|
77
|
+
/** Equivalent of ts-morph's `type.getText()` */
|
|
78
|
+
typeToString(type: TsType): string
|
|
79
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Analysis engines
|
|
3
|
+
*
|
|
4
|
+
* - `tsgo` (default): TypeScript 7's native compiler, through its API. Fastest.
|
|
5
|
+
* - `typescript`: the TypeScript compiler installed for the analyzed API (6.x), running in-process. Generates the same SDK
|
|
6
|
+
* as `tsgo`, and is used automatically if `tsgo` can't be started.
|
|
7
|
+
* - `ts-morph`: the original analyzer, running on the TypeScript 4.8 compiler bundled with ts-morph. Slowest; generates
|
|
8
|
+
* the same SDK as previous versions of this tool (which may differ from TypeScript 6+ on some inferred types).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* eslint-disable @typescript-eslint/no-var-requires -- engines are loaded lazily, as loading a compiler is expensive */
|
|
12
|
+
|
|
13
|
+
import * as path from 'path'
|
|
14
|
+
|
|
15
|
+
import type { SdkContent } from '../analyzer/sdk'
|
|
16
|
+
import { Config } from '../config'
|
|
17
|
+
import { debug, notice, panic } from '../logging'
|
|
18
|
+
|
|
19
|
+
import { analyzeWithHost } from './analyze'
|
|
20
|
+
|
|
21
|
+
export const ENGINES = ['tsgo', 'typescript', 'ts-morph'] as const
|
|
22
|
+
|
|
23
|
+
export type EngineName = (typeof ENGINES)[number]
|
|
24
|
+
|
|
25
|
+
/** Internal engine running this analyzer on ts-morph's program, used to verify it matches the legacy analyzer */
|
|
26
|
+
const TS_MORPH_COMPAT_ENGINE = 'ts-morph-compat'
|
|
27
|
+
|
|
28
|
+
export function isEngineName(name: string): name is EngineName {
|
|
29
|
+
return (ENGINES as readonly string[]).includes(name) || name === TS_MORPH_COMPAT_ENGINE
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function analyze(config: Config, engine: EngineName): Promise<SdkContent> {
|
|
33
|
+
debug('Analysis engine: {yellow}', engine)
|
|
34
|
+
|
|
35
|
+
const sourcePath = path.resolve(process.cwd(), config.apiInputPath)
|
|
36
|
+
|
|
37
|
+
switch (engine as string) {
|
|
38
|
+
// Engines are loaded lazily: loading a compiler is expensive
|
|
39
|
+
case 'ts-morph': {
|
|
40
|
+
const { analyzerCli }: typeof import('../analyzer') = require('../analyzer')
|
|
41
|
+
return analyzerCli(config)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case TS_MORPH_COMPAT_ENGINE: {
|
|
45
|
+
const { createTsMorphCompatHost }: typeof import('./ts-morph-compat-host') = require('./ts-morph-compat-host')
|
|
46
|
+
return analyzeWithHost(config, createTsMorphCompatHost)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'typescript': {
|
|
50
|
+
const { createTypeScriptHost, loadTypeScript }: typeof import('./typescript-host') = require('./typescript-host')
|
|
51
|
+
return analyzeWithHost(config, (tsConfigFilePath) =>
|
|
52
|
+
createTypeScriptHost(loadTypeScript(sourcePath, config.typescriptPath), tsConfigFilePath)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'tsgo': {
|
|
57
|
+
const { createTsgoHost }: typeof import('./tsgo-host') = require('./tsgo-host')
|
|
58
|
+
const { createTypeScriptHost, loadTypeScript }: typeof import('./typescript-host') = require('./typescript-host')
|
|
59
|
+
|
|
60
|
+
return analyzeWithHost(config, (tsConfigFilePath) => {
|
|
61
|
+
try {
|
|
62
|
+
return createTsgoHost(tsConfigFilePath)
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// Only startup failures end up here (e.g. no native binary for this platform): analysis errors happen later on
|
|
65
|
+
notice(
|
|
66
|
+
'Could not start the {yellow} engine, falling back to the {yellow} engine (same output, slower):\n{}',
|
|
67
|
+
'tsgo',
|
|
68
|
+
'typescript',
|
|
69
|
+
e instanceof Error ? e.message : String(e)
|
|
70
|
+
)
|
|
71
|
+
return createTypeScriptHost(loadTypeScript(sourcePath, config.typescriptPath), tsConfigFilePath)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
default:
|
|
77
|
+
panic('Unknown analysis engine {yellow} (expected one of: {})', engine, ENGINES.join(', '))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Analysis host running on the TypeScript program built by ts-morph (TypeScript 4.8)
|
|
3
|
+
*
|
|
4
|
+
* This is not meant for users: it runs the engine-independent analyzer against the exact same program, type checker
|
|
5
|
+
* and file order as the legacy ts-morph analyzer, which lets the parity tests verify both analyzers are equivalent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Project, ts } from 'ts-morph'
|
|
9
|
+
|
|
10
|
+
import { AnalysisHost } from './host'
|
|
11
|
+
import { createProgramHost } from './typescript-host'
|
|
12
|
+
|
|
13
|
+
export function createTsMorphCompatHost(tsConfigFilePath: string): AnalysisHost {
|
|
14
|
+
// Same project setup as the legacy analyzer (src/analyzer/index.ts)
|
|
15
|
+
const project = new Project({ tsConfigFilePath })
|
|
16
|
+
|
|
17
|
+
const projectFilePaths = project.getSourceFiles().map((file) => file.getFilePath() as string)
|
|
18
|
+
|
|
19
|
+
return createProgramHost('ts-morph-compat', ts, project.getProgram().compilerObject, projectFilePaths)
|
|
20
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Analysis host backed by the TypeScript 7 native compiler (tsgo), through its synchronous API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
|
+
/* eslint-disable @typescript-eslint/no-var-requires -- the TypeScript 7 API is loaded lazily, and can fail to load */
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs'
|
|
9
|
+
import * as path from 'path'
|
|
10
|
+
|
|
11
|
+
import { debug } from '../logging'
|
|
12
|
+
|
|
13
|
+
import { orderProjectFiles } from './file-order'
|
|
14
|
+
import { AnalysisHost, SyntaxKinds, TsNode, TsType, TYPE_FORMAT_FLAGS } from './host'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The API throws synchronously when the native executable is missing, but if it exists and can't be executed, the
|
|
18
|
+
* failure surfaces as an unhandled 'error' event of the child process, which would crash instead of falling back
|
|
19
|
+
*/
|
|
20
|
+
function assertTsgoIsExecutable() {
|
|
21
|
+
if (process.platform === 'win32') return
|
|
22
|
+
|
|
23
|
+
// Same resolution as TypeScript's lib/getExePath.js
|
|
24
|
+
const platformPackage = `@typescript/typescript-${process.platform}-${process.arch}`
|
|
25
|
+
const packageJson = require.resolve(`${platformPackage}/package.json`, {
|
|
26
|
+
paths: [path.dirname(require.resolve('typescript/package.json'))],
|
|
27
|
+
})
|
|
28
|
+
fs.accessSync(path.join(path.dirname(packageJson), 'lib', 'tsc'), fs.constants.X_OK)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Stop tsgo
|
|
33
|
+
* The API's `close()` sends SIGTERM, to which a busy tsgo process responds by logging "context canceled" to the stderr
|
|
34
|
+
* it shares with this process. tsgo holds no state worth a graceful shutdown, so it is killed with SIGKILL first.
|
|
35
|
+
*/
|
|
36
|
+
function terminate(api: any) {
|
|
37
|
+
// Private fields of TypeScript 7.0's synchronous API (the dependency's version is pinned)
|
|
38
|
+
const client = api.client
|
|
39
|
+
const child = client?.channel?.child
|
|
40
|
+
|
|
41
|
+
if (!child) {
|
|
42
|
+
api.close()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
47
|
+
child.kill('SIGKILL')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Only close the channel: `api.close()` would first ask the (now dead) process to release its snapshots
|
|
51
|
+
client.close()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load the TypeScript 7 API and start a tsgo process
|
|
56
|
+
* Throws if the API can't be loaded or tsgo can't be started (e.g. unsupported platform)
|
|
57
|
+
*/
|
|
58
|
+
export function createTsgoHost(tsConfigFilePath: string): AnalysisHost {
|
|
59
|
+
// The synchronous API relies on Node.js internals (and the tsgo process it spawns would then never exit)
|
|
60
|
+
if (process.versions['bun']) {
|
|
61
|
+
throw new Error("TypeScript 7's API requires the Node.js runtime (run the generator with `bun x` or `npx`, without `--bun`)")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The API is published as ES modules: this relies on `require(esm)` (Node.js 20.19+ / 22.12+, Bun)
|
|
65
|
+
const { API, TypeFlags } = require('typescript/unstable/sync')
|
|
66
|
+
const { SyntaxKind } = require('typescript/unstable/ast')
|
|
67
|
+
|
|
68
|
+
assertTsgoIsExecutable()
|
|
69
|
+
|
|
70
|
+
const api = new API({ cwd: path.dirname(tsConfigFilePath) })
|
|
71
|
+
|
|
72
|
+
// Errors exit the process right away: stop tsgo then too. Prepended, to run before the API's own exit handler.
|
|
73
|
+
const onExit = () => terminate(api)
|
|
74
|
+
process.prependListener('exit', onExit)
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const snapshot = api.updateSnapshot({ openProjects: [tsConfigFilePath] })
|
|
78
|
+
const project = snapshot.getProject(tsConfigFilePath)
|
|
79
|
+
|
|
80
|
+
if (!project) {
|
|
81
|
+
throw new Error(`tsgo did not load the project for ${tsConfigFilePath}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { checker, program } = project
|
|
85
|
+
|
|
86
|
+
debug('Using TypeScript {yellow} (native)', String(require('typescript/package.json').version))
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
name: 'tsgo',
|
|
90
|
+
SyntaxKind: SyntaxKind as SyntaxKinds,
|
|
91
|
+
getProjectFilePaths: () => orderProjectFiles(project.rootFiles, program.getSourceFileNames()),
|
|
92
|
+
getSourceFile: (absolutePath) => program.getSourceFile(absolutePath),
|
|
93
|
+
getTypeAtLocation: (node: TsNode) => checker.getTypeAtLocation(node),
|
|
94
|
+
getReturnTypeOfDeclaration(node: TsNode) {
|
|
95
|
+
const signature = checker.getSignatureFromDeclaration(node)
|
|
96
|
+
|
|
97
|
+
if (!signature) {
|
|
98
|
+
throw new Error('Expected the node to have a signature.')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return checker.getReturnTypeOfSignature(signature)
|
|
102
|
+
},
|
|
103
|
+
typeToString: (type: TsType) => checker.typeToString(type, undefined, TYPE_FORMAT_FLAGS),
|
|
104
|
+
isObjectType: (type: TsType) => (type.flags & TypeFlags.Object) !== 0,
|
|
105
|
+
getIndexType(type: TsType, keyKind) {
|
|
106
|
+
const keyFlag = keyKind === 'string' ? TypeFlags.String : TypeFlags.Number
|
|
107
|
+
return checker.getIndexInfosOfType(type).find((info: any) => (info.keyType.flags & keyFlag) !== 0)?.valueType
|
|
108
|
+
},
|
|
109
|
+
dispose() {
|
|
110
|
+
process.removeListener('exit', onExit)
|
|
111
|
+
terminate(api)
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
process.removeListener('exit', onExit)
|
|
116
|
+
terminate(api)
|
|
117
|
+
throw e
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Analysis host backed by the in-process TypeScript compiler API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
|
+
/* eslint-disable @typescript-eslint/no-var-requires -- the TypeScript compiler is resolved at runtime */
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs'
|
|
9
|
+
import * as path from 'path'
|
|
10
|
+
|
|
11
|
+
import { debug, notice, panic } from '../logging'
|
|
12
|
+
|
|
13
|
+
import { orderProjectFiles } from './file-order'
|
|
14
|
+
import { AnalysisHost, SyntaxKinds, TsNode, TsType, TYPE_FORMAT_FLAGS } from './host'
|
|
15
|
+
|
|
16
|
+
/** The TypeScript compiler module (`typeof import('typescript')` for TypeScript 6 and below) */
|
|
17
|
+
export type TypeScriptModule = any
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolution mode attributes of import types, e.g. `import("pkg", { with: { "resolution-mode": "import" } }).Type`
|
|
21
|
+
* TypeScript 5.3+ prints them for ESM-only modules referenced from CommonJS files; TypeScript 7 and 4.8 print the
|
|
22
|
+
* same type as `import("pkg").Type`.
|
|
23
|
+
*/
|
|
24
|
+
const IMPORT_TYPE_ATTRIBUTES_REGEX = /\bimport\(("[^"]+"|'[^']+'), \{ (?:with|assert): \{ "resolution-mode": "(?:import|require)" \} \}\)/g
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a host from an existing program
|
|
28
|
+
* @param name Engine name
|
|
29
|
+
* @param ts The TypeScript module the program was created with
|
|
30
|
+
* @param program The TypeScript program
|
|
31
|
+
* @param projectFilePaths Files that are part of the project
|
|
32
|
+
*/
|
|
33
|
+
export function createProgramHost(name: string, ts: TypeScriptModule, program: any, projectFilePaths: string[]): AnalysisHost {
|
|
34
|
+
const checker = program.getTypeChecker()
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
SyntaxKind: ts.SyntaxKind as SyntaxKinds,
|
|
39
|
+
getProjectFilePaths: () => projectFilePaths,
|
|
40
|
+
getSourceFile: (absolutePath) => program.getSourceFile(absolutePath),
|
|
41
|
+
getTypeAtLocation: (node: TsNode) => checker.getTypeAtLocation(node),
|
|
42
|
+
getReturnTypeOfDeclaration(node: TsNode) {
|
|
43
|
+
const signature = checker.getSignatureFromDeclaration(node)
|
|
44
|
+
|
|
45
|
+
if (!signature) {
|
|
46
|
+
throw new Error('Expected the node to have a signature.')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return checker.getReturnTypeOfSignature(signature)
|
|
50
|
+
},
|
|
51
|
+
typeToString: (type: TsType) =>
|
|
52
|
+
checker.typeToString(type, undefined, TYPE_FORMAT_FLAGS).replace(IMPORT_TYPE_ATTRIBUTES_REGEX, 'import($1)'),
|
|
53
|
+
isObjectType: (type: TsType) => (type.flags & ts.TypeFlags.Object) !== 0,
|
|
54
|
+
getIndexType: (type: TsType, keyKind) => (keyKind === 'string' ? type.getStringIndexType() : type.getNumberIndexType()),
|
|
55
|
+
dispose() {},
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load the TypeScript compiler used by the `typescript` engine
|
|
61
|
+
* By default, this is the TypeScript version installed for the analyzed API (resolved from its source directory).
|
|
62
|
+
* @param sourcePath Absolute path of the API's source directory
|
|
63
|
+
* @param typescriptPath Explicit path to a TypeScript package (from the configuration file)
|
|
64
|
+
*/
|
|
65
|
+
export function loadTypeScript(sourcePath: string, typescriptPath: string | undefined): TypeScriptModule {
|
|
66
|
+
let modulePath: string
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
modulePath = typescriptPath
|
|
70
|
+
? require.resolve(path.resolve(process.cwd(), typescriptPath))
|
|
71
|
+
: require.resolve('typescript', { paths: [sourcePath] })
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// TypeScript 7 packages have no main entry point
|
|
74
|
+
const packageJsonPath = typescriptPath && path.resolve(process.cwd(), typescriptPath, 'package.json')
|
|
75
|
+
|
|
76
|
+
if (packageJsonPath && fs.existsSync(packageJsonPath)) {
|
|
77
|
+
panic(
|
|
78
|
+
'The TypeScript package at {magentaBright} (version {yellow}) does not provide the JavaScript compiler API required by the {yellow} engine.\nUse TypeScript 6 or below, or the {yellow} engine.',
|
|
79
|
+
path.dirname(packageJsonPath),
|
|
80
|
+
String(JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version),
|
|
81
|
+
'typescript',
|
|
82
|
+
'tsgo'
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
panic(
|
|
87
|
+
typescriptPath
|
|
88
|
+
? 'Could not load TypeScript from {magentaBright}'
|
|
89
|
+
: 'The {yellow} engine uses the TypeScript compiler installed for the analyzed API, but none was found from {magentaBright}.\nInstall {yellow} or set the {yellow} option.',
|
|
90
|
+
...(typescriptPath ? [typescriptPath] : ['typescript', sourcePath, 'typescript', 'typescriptPath'])
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ts = require(modulePath)
|
|
95
|
+
|
|
96
|
+
if (typeof ts.createProgram !== 'function') {
|
|
97
|
+
panic(
|
|
98
|
+
'The TypeScript package at {magentaBright} (version {yellow}) does not provide the JavaScript compiler API required by the {yellow} engine.\nUse TypeScript 6 or below, or the {yellow} engine.',
|
|
99
|
+
modulePath,
|
|
100
|
+
String(ts.version),
|
|
101
|
+
'typescript',
|
|
102
|
+
'tsgo'
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const [major] = String(ts.version).split('.').map(Number)
|
|
107
|
+
|
|
108
|
+
if (major < 6) {
|
|
109
|
+
notice(
|
|
110
|
+
'TypeScript {yellow} does not support stable type ordering: generated types may be ordered differently than with the {yellow} engine.',
|
|
111
|
+
String(ts.version),
|
|
112
|
+
'tsgo'
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
debug('Using TypeScript {yellow} from {magentaBright}', String(ts.version), modulePath)
|
|
117
|
+
|
|
118
|
+
return ts
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a host running the in-process TypeScript compiler on a tsconfig file
|
|
123
|
+
* Uses the same settings as the TypeScript 7 native compiler, so both engines generate the same SDK.
|
|
124
|
+
*/
|
|
125
|
+
export function createTypeScriptHost(ts: TypeScriptModule, tsConfigFilePath: string): AnalysisHost {
|
|
126
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(
|
|
127
|
+
tsConfigFilePath,
|
|
128
|
+
{},
|
|
129
|
+
{
|
|
130
|
+
...ts.sys,
|
|
131
|
+
onUnRecoverableConfigFileDiagnostic: (diagnostic: any) =>
|
|
132
|
+
panic('Failed to parse {yellow}: {}', tsConfigFilePath, ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')),
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Order union constituents deterministically, like TypeScript 7 does
|
|
137
|
+
const options = { ...parsed.options, stableTypeOrdering: true }
|
|
138
|
+
|
|
139
|
+
const compilerHost = ts.createCompilerHost(options)
|
|
140
|
+
|
|
141
|
+
// JSDoc comments don't affect types in TypeScript files: skip parsing them (TypeScript 5.3+)
|
|
142
|
+
if (ts.JSDocParsingMode) {
|
|
143
|
+
compilerHost.jsDocParsingMode = ts.JSDocParsingMode.ParseForTypeErrors
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const program = ts.createProgram({
|
|
147
|
+
rootNames: parsed.fileNames,
|
|
148
|
+
options,
|
|
149
|
+
host: compilerHost,
|
|
150
|
+
projectReferences: parsed.projectReferences,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const projectFilePaths = orderProjectFiles(
|
|
154
|
+
parsed.fileNames,
|
|
155
|
+
program.getSourceFiles().map((file: any) => file.fileName)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return createProgramHost('typescript', ts, program, projectFilePaths)
|
|
159
|
+
}
|