@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,247 @@
1
+ /**
2
+ * @file Engine-independent description of the analyzed SDK
3
+ *
4
+ * Everything in this file is plain data and pure helpers: it must not import any TypeScript
5
+ * compiler at runtime, so the generators and the fast analysis engines can load it cheaply.
6
+ */
7
+
8
+ import type { Route } from './route'
9
+ import type { ResolvedTypeDeps } from './typedeps'
10
+
11
+ export interface SdkContent {
12
+ readonly modules: SdkModules
13
+ readonly types: TypesExtractorContent
14
+ }
15
+
16
+ /** Modules mapped to their controllers (keyed by class name) */
17
+ export type SdkModules = Map<string, Map<string, SdkController>>
18
+
19
+ /**
20
+ * SDK interface of a controller
21
+ */
22
+ export interface SdkController {
23
+ /** Name of the controller's class, camel cased */
24
+ readonly className: string
25
+
26
+ /** Controller's methods */
27
+ readonly methods: SdkMethod[]
28
+
29
+ /** Original controller file's path */
30
+ readonly path: string
31
+
32
+ /** Name the controller is registered under */
33
+ readonly registrationName: string
34
+ }
35
+
36
+ /**
37
+ * SDK interface for a single controller's method
38
+ */
39
+ export interface SdkMethod {
40
+ /** Method's HTTP method (e.g. GET / POST) */
41
+ readonly httpMethod: SdkHttpMethod
42
+
43
+ /** Method's name */
44
+ readonly name: string
45
+
46
+ /** Method's parameters */
47
+ readonly params: SdkMethodParams
48
+
49
+ /** Method's return type with resolved dependencies */
50
+ readonly returnType: ResolvedTypeDeps
51
+
52
+ /** Method's parsed route */
53
+ readonly route: Route
54
+
55
+ /** Method's URI path */
56
+ readonly uriPath: string
57
+ }
58
+
59
+ /**
60
+ * HTTP method of a controller's method
61
+ */
62
+ export enum SdkHttpMethod {
63
+ Delete = 'DELETE',
64
+ Get = 'GET',
65
+ Patch = 'PATCH',
66
+ Post = 'POST',
67
+ Put = 'PUT',
68
+ }
69
+
70
+ /**
71
+ * Type of a parameter that must be passed to an SDK method: Body, Query or Route Param
72
+ * Supports accessing parameters individually by key or as a whole object
73
+ *
74
+ * EXAMPLE
75
+ *
76
+ * Controller Method:
77
+ *
78
+ * getStuff(@Query('email') email: string, @Query('companyId'): CompanyId)
79
+ *
80
+ * SDK Method Param:
81
+ * { email: string, companyId: CompanyId }
82
+ *
83
+ * OR
84
+ *
85
+ * Controller Method:
86
+ * type EmailAndCompanyId = { email: string, companyId: CompanyId }
87
+ * getStuff(@Query(): EmailAndCompanyId)
88
+ *
89
+ * SDK Method Param:
90
+ * EmailAndCompanyId
91
+ */
92
+
93
+ export type SdkMethodParam = SdkMethodParamMap | ResolvedTypeDeps | null
94
+ export type SdkMethodParamMap = { [key: string]: ResolvedTypeDeps }
95
+
96
+ export const isParamResolvedType = (param: SdkMethodParam): param is ResolvedTypeDeps => {
97
+ if (param === null) {
98
+ return false
99
+ }
100
+ return 'rawType' in param && 'resolvedType' in param && 'relativeFilePath' in param && 'dependencies' in param && 'localTypes' in param
101
+ }
102
+
103
+ export const getParamResolvedTypes = (param: SdkMethodParam): ResolvedTypeDeps[] => {
104
+ if (param === null) {
105
+ return []
106
+ }
107
+ if (isParamResolvedType(param)) {
108
+ return [param]
109
+ }
110
+ return Object.values(param)
111
+ }
112
+
113
+ /**
114
+ * SDK interface for a controller's method's parameters
115
+ */
116
+ export interface SdkMethodParams {
117
+ bodyParams: SdkMethodParam
118
+ /** Engine-specific analysis context (compiler nodes); never serialized */
119
+ context: unknown
120
+ queryParams: SdkMethodParam
121
+ routeParams: SdkMethodParam
122
+ }
123
+
124
+ export enum ArgDecorator {
125
+ Body = 'Body',
126
+ Param = 'Param',
127
+ Query = 'Query',
128
+ }
129
+
130
+ /** Valid extensions for TypeScript module files */
131
+ export const MODULE_EXTENSIONS = ['.ts', '.d.ts', '.tsx', '.d.tsx', '.js', '.jsx']
132
+
133
+ /**
134
+ * Location of an imported type
135
+ */
136
+ export interface TypeLocation {
137
+ readonly relativePathNoExt: string
138
+ readonly typename: string
139
+ }
140
+
141
+ /**
142
+ * Location of an imported type after figuring out its extension
143
+ */
144
+ export interface TypeLocationWithExt extends TypeLocation {
145
+ readonly relativePath: string
146
+ }
147
+
148
+ /**
149
+ * Extracted imported type
150
+ */
151
+ export interface ExtractedType extends TypeLocationWithExt {
152
+ /** Type's declaration */
153
+ readonly content: string
154
+
155
+ /** Types this one depends on */
156
+ readonly dependencies: TypeLocationWithExt[]
157
+
158
+ /** Type parameters (e.g. <T>) */
159
+ readonly typeParams: string[]
160
+ }
161
+
162
+ // Maps files to records mapping themselves type names to their declaration code
163
+ export type TypesExtractorContent = Map<string, Map<string, ExtractedType>>
164
+
165
+ /**
166
+ * Locate the files containing a list of a resolved types
167
+ * @param resolvedTypes
168
+ * @returns
169
+ */
170
+ export function locateTypesFile(resolvedTypes: Array<ResolvedTypeDeps>): TypeLocation[] {
171
+ const out = new Array<TypeLocation>()
172
+
173
+ for (const resolved of resolvedTypes) {
174
+ for (const [file, types] of resolved.dependencies) {
175
+ for (const typename of types) {
176
+ if (!out.find((loc) => loc.typename === typename && loc.relativePathNoExt === file)) {
177
+ out.push({ typename, relativePathNoExt: file })
178
+ }
179
+ }
180
+ }
181
+
182
+ for (const typename of resolved.localTypes) {
183
+ if (!out.find((loc) => loc.typename === typename && loc.relativePathNoExt === resolved.relativeFilePath)) {
184
+ out.push({ typename, relativePathNoExt: resolved.relativeFilePath })
185
+ }
186
+ }
187
+ }
188
+
189
+ return out
190
+ }
191
+
192
+ /**
193
+ * Flatten a tree of resolved type dependencies
194
+ * @param sdkModules
195
+ * @returns
196
+ */
197
+ export function flattenSdkResolvedTypes(sdkModules: SdkModules): ResolvedTypeDeps[] {
198
+ const flattened = new Array<ResolvedTypeDeps>()
199
+
200
+ for (const module of sdkModules.values()) {
201
+ for (const controller of module.values()) {
202
+ for (const method of controller.methods.values()) {
203
+ const { bodyParams, queryParams, routeParams } = method.params
204
+
205
+ flattened.push(method.returnType)
206
+ flattened.push(...getParamResolvedTypes(routeParams))
207
+ flattened.push(...getParamResolvedTypes(queryParams))
208
+ flattened.push(...getParamResolvedTypes(bodyParams))
209
+ }
210
+ }
211
+ }
212
+
213
+ return flattened
214
+ }
215
+
216
+ /**
217
+ * Convert a string to camel case
218
+ * @param str
219
+ */
220
+ export function camelcase(str: string): string {
221
+ return str
222
+ .split(/[^a-zA-Z0-9_]/g)
223
+ .map((p, i) => {
224
+ const f = p.substr(0, 1)
225
+ return (i === 0 ? f.toLocaleLowerCase() : f.toLocaleUpperCase()) + p.substr(1)
226
+ })
227
+ .join('')
228
+ }
229
+
230
+ /**
231
+ * Serialize analyzed SDK content to JSON
232
+ * Maps are converted to plain objects, and engine-specific analysis contexts are omitted.
233
+ */
234
+ export function serializeSdkContent(content: SdkContent, pretty: boolean): string {
235
+ return JSON.stringify(
236
+ content,
237
+ function (this: unknown, key, v) {
238
+ if (key === 'context' && isSdkMethodParams(this)) return undefined
239
+ return v instanceof Map ? Object.fromEntries(v) : v
240
+ },
241
+ pretty ? 4 : 0
242
+ )
243
+ }
244
+
245
+ function isSdkMethodParams(value: unknown): value is SdkMethodParams {
246
+ return typeof value === 'object' && value !== null && 'routeParams' in value && 'queryParams' in value && 'bodyParams' in value
247
+ }
@@ -47,9 +47,17 @@ export interface ResolvedTypeDeps {
47
47
  * @returns
48
48
  */
49
49
  export function resolveTypeDependencies(type: Type<ts.Type>, relativeFilePath: string, absoluteSrcPath: string): ResolvedTypeDeps {
50
- /** Raw type's text (e.g. `Array<import("somefile.ts").SomeType>`) */
51
- const rawType = type.getText()
50
+ return resolveTypeTextDependencies(type.getText(), relativeFilePath, absoluteSrcPath)
51
+ }
52
52
 
53
+ /**
54
+ * Resolve the dependencies of a type from its text, as printed by the TypeScript checker
55
+ * @param rawType Raw type's text (e.g. `Array<import("somefile.ts").SomeType>`)
56
+ * @param relativeFilePath
57
+ * @param absoluteSrcPath
58
+ * @returns
59
+ */
60
+ export function resolveTypeTextDependencies(rawType: string, relativeFilePath: string, absoluteSrcPath: string): ResolvedTypeDeps {
53
61
  if (path.isAbsolute(relativeFilePath)) {
54
62
  unreachable(
55
63
  'Internal error: got absolute file path in type dependencies resolver, when expecting a relative one (got {magentaBright})',
@@ -85,7 +93,7 @@ export function resolveTypeDependencies(type: Type<ts.Type>, relativeFilePath: s
85
93
  if (path.isAbsolute(depFile)) {
86
94
  unreachable(
87
95
  'Internal error: resolved absolute file path in type dependencies, when should have resolved a relative one\nIn type: {yellow}\nGot: {magentaBright}',
88
- type.getText(),
96
+ rawType,
89
97
  depFile
90
98
  )
91
99
  }
@@ -107,7 +115,17 @@ export function resolveTypeDependencies(type: Type<ts.Type>, relativeFilePath: s
107
115
  * @returns
108
116
  */
109
117
  export function getImportResolvedType(type: Type<ts.Type>): string {
110
- return type.getText().replace(IMPORTED_TYPE_REGEX, (_, __, typename) => typename)
118
+ return getImportResolvedTypeText(type.getText())
119
+ }
120
+
121
+ /**
122
+ * Extract an import type's name from a type's text
123
+ * @example "import('dir/file').TypeName" => "TypeName"
124
+ * @param typeText
125
+ * @returns
126
+ */
127
+ export function getImportResolvedTypeText(typeText: string): string {
128
+ return typeText.replace(IMPORTED_TYPE_REGEX, (_, __, typename) => typename)
111
129
  }
112
130
 
113
131
  /**
package/src/bin.ts CHANGED
@@ -2,30 +2,55 @@
2
2
 
3
3
  import * as path from 'path'
4
4
 
5
- import { analyzerCli } from './analyzer'
6
5
  import { config, configPath } from './config'
6
+ import { analyze, ENGINES, EngineName, isEngineName } from './engine'
7
7
  import generateAPIClient from './generator'
8
- import { println } from './logging'
8
+ import { log, panic } from './logging'
9
9
  import generateRTKQueryEndpoints from './rtk-query-generator'
10
10
 
11
+ /**
12
+ * Get the analysis engine to use: `--engine <name>` or `--engine=<name>` on the command line,
13
+ * then the configuration file's `engine` option, then the default one
14
+ */
15
+ function getEngine(args: string[]): EngineName {
16
+ let engine: string = config.engine ?? 'tsgo'
17
+
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i] === '--engine') {
20
+ engine = args[++i] ?? ''
21
+ } else if (args[i].startsWith('--engine=')) {
22
+ engine = args[i].substring('--engine='.length)
23
+ }
24
+ }
25
+
26
+ if (!isEngineName(engine)) {
27
+ panic('Unknown analysis engine {yellow} (expected one of: {})', engine, ENGINES.join(', '))
28
+ }
29
+
30
+ return engine
31
+ }
32
+
11
33
  async function main() {
12
34
  const started = Date.now()
13
35
 
14
- process.chdir(path.dirname(path.resolve(configPath)))
36
+ const args = process.argv.slice(3)
37
+ const engine = getEngine(args)
38
+ const outputType = args.find((arg) => arg === '--generate' || arg === '--generate-rtk')
15
39
 
16
- const sdkContent = await analyzerCli(config)
40
+ process.chdir(path.dirname(path.resolve(configPath)))
17
41
 
18
- const outputType = process.argv[3]
42
+ const sdkContent = await analyze(config, engine)
19
43
 
20
44
  if (outputType === '--generate') {
21
45
  await generateAPIClient(config, sdkContent)
22
46
  }
23
47
 
24
48
  if (outputType === '--generate-rtk') {
49
+ log('> Generating RTK Query endpoints for Nest API...')
25
50
  await generateRTKQueryEndpoints(config, sdkContent)
26
51
  }
27
52
 
28
- println('{green}', '@ Done in ' + ((Date.now() - started) / 1000).toFixed(2) + 's')
53
+ log('> Done in ' + ((Date.now() - started) / 1000).toFixed(2) + 's')
29
54
  }
30
55
 
31
56
  main()
package/src/config.ts CHANGED
@@ -26,6 +26,15 @@ export interface Config {
26
26
  }
27
27
  }
28
28
 
29
+ /**
30
+ * Engine used to analyze the API (default: "tsgo")
31
+ * - "tsgo": TypeScript 7's native compiler (fastest)
32
+ * - "typescript": the API's own TypeScript compiler (6.x) running in-process; same output as "tsgo"
33
+ * - "ts-morph": the original analyzer (TypeScript 4.8); same output as previous versions of this tool
34
+ * Can be overridden with the `--engine` command-line option
35
+ */
36
+ readonly engine?: 'tsgo' | 'typescript' | 'ts-morph'
37
+
29
38
  /** If the SDK interface file does not exist yet, create one automatically (enabled by default) */
30
39
  readonly generateDefaultSdkInterface?: boolean
31
40
 
@@ -61,6 +70,12 @@ export interface Config {
61
70
  /** Path to custom tsconfig file */
62
71
  readonly tsconfigFile?: string
63
72
 
73
+ /**
74
+ * Path to the TypeScript package used by the "typescript" engine
75
+ * (default: the "typescript" package installed for the API, resolved from `apiInputPath`)
76
+ */
77
+ readonly typescriptPath?: string
78
+
64
79
  /** Enable verbose mode */
65
80
  readonly verbose?: boolean
66
81
  }