@buildvision/nest-sdk-generator 2.3.0 → 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.
Files changed (94) hide show
  1. package/.eslintrc.js +1 -1
  2. package/.github/workflows/test.yml +35 -0
  3. package/README.md +402 -239
  4. package/build/analyzer/classdeps.js +36 -4
  5. package/build/analyzer/controller.d.ts +2 -14
  6. package/build/analyzer/controller.js +38 -18
  7. package/build/analyzer/controllers.d.ts +2 -2
  8. package/build/analyzer/controllers.js +37 -5
  9. package/build/analyzer/extractor.d.ts +2 -41
  10. package/build/analyzer/extractor.js +63 -66
  11. package/build/analyzer/index.d.ts +2 -6
  12. package/build/analyzer/index.js +107 -86
  13. package/build/analyzer/methods.d.ts +2 -30
  14. package/build/analyzer/methods.js +7 -16
  15. package/build/analyzer/module.js +36 -4
  16. package/build/analyzer/params.d.ts +3 -39
  17. package/build/analyzer/params.js +17 -37
  18. package/build/analyzer/route.js +8 -9
  19. package/build/analyzer/sdk.d.ts +146 -0
  20. package/build/analyzer/sdk.js +120 -0
  21. package/build/analyzer/typedeps.d.ts +15 -0
  22. package/build/analyzer/typedeps.js +62 -10
  23. package/build/bin.js +72 -25
  24. package/build/config.d.ts +13 -0
  25. package/build/config.js +36 -3
  26. package/build/engine/analyze.d.ts +16 -0
  27. package/build/engine/analyze.js +786 -0
  28. package/build/engine/ast.d.ts +24 -0
  29. package/build/engine/ast.js +100 -0
  30. package/build/engine/file-order.d.ts +17 -0
  31. package/build/engine/file-order.js +97 -0
  32. package/build/engine/host.d.ts +63 -0
  33. package/build/engine/host.js +16 -0
  34. package/build/engine/index.d.ts +15 -0
  35. package/build/engine/index.js +93 -0
  36. package/build/engine/ts-morph-compat-host.d.ts +8 -0
  37. package/build/engine/ts-morph-compat-host.js +18 -0
  38. package/build/engine/tsgo-host.d.ts +9 -0
  39. package/build/engine/tsgo-host.js +136 -0
  40. package/build/engine/typescript-host.d.ts +26 -0
  41. package/build/engine/typescript-host.js +142 -0
  42. package/build/generator/genmodules.d.ts +1 -4
  43. package/build/generator/genmodules.js +23 -27
  44. package/build/generator/gentypes.d.ts +1 -1
  45. package/build/generator/gentypes.js +36 -4
  46. package/build/generator/index.d.ts +1 -1
  47. package/build/generator/index.js +91 -69
  48. package/build/generator/prettier.js +43 -9
  49. package/build/generator/sdk-interface.d.ts +1 -1
  50. package/build/generator/sdk-interface.js +5 -5
  51. package/build/logging.d.ts +3 -0
  52. package/build/logging.js +17 -10
  53. package/build/rtk-query-generator/genmodules.d.ts +1 -4
  54. package/build/rtk-query-generator/genmodules.js +31 -33
  55. package/build/rtk-query-generator/index.d.ts +1 -1
  56. package/build/rtk-query-generator/index.js +87 -72
  57. package/build/rtk-query-generator/prettier.js +43 -9
  58. package/build/rtk-query-generator/sdk-interface.js +3 -4
  59. package/build/utils.js +38 -6
  60. package/package.json +29 -11
  61. package/src/analyzer/controller.ts +3 -31
  62. package/src/analyzer/controllers.ts +3 -2
  63. package/src/analyzer/extractor.ts +17 -86
  64. package/src/analyzer/index.ts +5 -11
  65. package/src/analyzer/methods.ts +5 -36
  66. package/src/analyzer/params.ts +13 -55
  67. package/src/analyzer/sdk.ts +247 -0
  68. package/src/analyzer/typedeps.ts +22 -4
  69. package/src/bin.ts +31 -6
  70. package/src/config.ts +15 -0
  71. package/src/engine/analyze.ts +1110 -0
  72. package/src/engine/ast.ts +105 -0
  73. package/src/engine/file-order.ts +104 -0
  74. package/src/engine/host.ts +79 -0
  75. package/src/engine/index.ts +79 -0
  76. package/src/engine/ts-morph-compat-host.ts +20 -0
  77. package/src/engine/tsgo-host.ts +119 -0
  78. package/src/engine/typescript-host.ts +159 -0
  79. package/src/generator/genmodules.ts +9 -4
  80. package/src/generator/gentypes.ts +1 -1
  81. package/src/generator/index.ts +1 -1
  82. package/src/logging.ts +10 -1
  83. package/src/rtk-query-generator/genmodules.ts +17 -9
  84. package/src/rtk-query-generator/index.ts +2 -12
  85. package/src/rtk-query-generator/sdk-interface.ts +2 -3
  86. package/tsconfig.json +7 -6
  87. package/build/analyzer/decorator.d.ts +0 -10
  88. package/build/analyzer/decorator.js +0 -29
  89. package/build/rtk-query-generator/gentypes.d.ts +0 -10
  90. package/build/rtk-query-generator/gentypes.js +0 -62
  91. package/build/tsconfig.tsbuildinfo +0 -1
  92. package/bun.lockb +0 -0
  93. package/src/rtk-query-generator/gentypes.ts +0 -74
  94. 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
+ }