@buildvision/nest-sdk-generator 1.2.2 → 2.1.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 +63 -0
- package/README.md +2 -2
- package/build/analyzer/classdeps.js +1 -1
- package/build/analyzer/controller.d.ts +4 -4
- package/build/analyzer/controller.js +3 -3
- package/build/analyzer/controllers.d.ts +1 -1
- package/build/analyzer/controllers.js +10 -2
- package/build/analyzer/decorator.d.ts +2 -2
- package/build/analyzer/decorator.js +19 -19
- package/build/analyzer/extractor.d.ts +4 -4
- package/build/analyzer/extractor.js +6 -17
- package/build/analyzer/index.d.ts +1 -1
- package/build/analyzer/index.js +2 -2
- package/build/analyzer/methods.d.ts +7 -7
- package/build/analyzer/methods.js +6 -9
- package/build/analyzer/module.js +2 -2
- package/build/analyzer/params.d.ts +59 -17
- package/build/analyzer/params.js +150 -129
- package/build/analyzer/route.d.ts +5 -2
- package/build/analyzer/route.js +13 -7
- package/build/analyzer/typedeps.d.ts +6 -6
- package/build/analyzer/typedeps.js +3 -3
- package/build/bin.js +9 -13
- package/build/config.d.ts +29 -19
- package/build/config.js +2 -2
- package/build/generator/genmodules.d.ts +33 -10
- package/build/generator/genmodules.js +165 -113
- package/build/generator/gentypes.js +1 -1
- package/build/generator/index.d.ts +1 -1
- package/build/generator/index.js +23 -22
- package/build/generator/prettier.d.ts +1 -1
- package/build/generator/prettier.js +2 -2
- package/build/generator/sdk-interface.d.ts +1 -1
- package/build/generator/sdk-interface.js +5 -5
- package/build/logging.js +4 -2
- package/build/rtk-query-generator/genmodules.d.ts +54 -0
- package/build/rtk-query-generator/genmodules.js +252 -0
- package/build/rtk-query-generator/gentypes.d.ts +10 -0
- package/build/rtk-query-generator/gentypes.js +62 -0
- package/build/rtk-query-generator/index.d.ts +6 -0
- package/build/rtk-query-generator/index.js +84 -0
- package/build/rtk-query-generator/prettier.d.ts +16 -0
- package/build/rtk-query-generator/prettier.js +43 -0
- package/build/rtk-query-generator/sdk-interface.d.ts +1 -0
- package/build/rtk-query-generator/sdk-interface.js +36 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils.js +1 -1
- package/bun.lockb +0 -0
- package/package.json +16 -5
- package/src/analyzer/classdeps.ts +3 -0
- package/src/analyzer/controller.ts +12 -6
- package/src/analyzer/controllers.ts +16 -1
- package/src/analyzer/extractor.ts +12 -23
- package/src/analyzer/index.ts +5 -2
- package/src/analyzer/methods.ts +12 -13
- package/src/analyzer/module.ts +4 -10
- package/src/analyzer/params.ts +212 -182
- package/src/analyzer/route.ts +10 -4
- package/src/analyzer/typedeps.ts +13 -11
- package/src/bin.ts +11 -13
- package/src/config.ts +35 -24
- package/src/generator/genmodules.ts +212 -129
- package/src/generator/gentypes.ts +1 -0
- package/src/generator/index.ts +23 -23
- package/src/generator/prettier.ts +4 -2
- package/src/logging.ts +4 -1
- package/src/rtk-query-generator/genmodules.ts +335 -0
- package/src/rtk-query-generator/gentypes.ts +74 -0
- package/src/rtk-query-generator/index.ts +92 -0
- package/src/rtk-query-generator/prettier.ts +48 -0
- package/src/rtk-query-generator/sdk-interface.ts +32 -0
- package/src/utils.ts +2 -2
- package/tsconfig.tsbuildinfo +1 -0
- package/src/analyzer/decorator.ts +0 -29
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as fs from 'fs'
|
|
6
|
+
|
|
6
7
|
import * as prettier from 'prettier'
|
|
8
|
+
|
|
7
9
|
import { Config } from '../config'
|
|
8
10
|
import { panic } from '../logging'
|
|
9
11
|
import { findFileAbove } from '../utils'
|
|
@@ -12,7 +14,7 @@ import { findFileAbove } from '../utils'
|
|
|
12
14
|
* Find a .prettierrc configuration file in the current directory or above
|
|
13
15
|
*/
|
|
14
16
|
export function findPrettierConfig(config: Config): object {
|
|
15
|
-
|
|
17
|
+
const prettierConfigPath = config.prettierConfig ?? findFileAbove('.prettierrc', config.sdkOutputPath)
|
|
16
18
|
|
|
17
19
|
if (!prettierConfigPath) {
|
|
18
20
|
return {}
|
|
@@ -38,7 +40,7 @@ export function findPrettierConfig(config: Config): object {
|
|
|
38
40
|
* @param parser
|
|
39
41
|
* @returns
|
|
40
42
|
*/
|
|
41
|
-
export function prettify(source: string, config: object, parser: 'typescript' | 'json'): string {
|
|
43
|
+
export function prettify(source: string, config: object, parser: 'typescript' | 'json'): Promise<string> {
|
|
42
44
|
return prettier.format(source, {
|
|
43
45
|
parser,
|
|
44
46
|
...config,
|
package/src/logging.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as chalk from 'chalk'
|
|
2
|
+
|
|
2
3
|
import { config } from './config'
|
|
3
4
|
|
|
4
5
|
export function format(message: string, ...params: Array<number | string>): string {
|
|
@@ -21,7 +22,9 @@ export function unreachable(message: string, ...params: Array<number | string>):
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export function warn(message: string, ...params: Array<number | string>) {
|
|
24
|
-
|
|
25
|
+
if (config.verbose) {
|
|
26
|
+
console.warn(chalk.yellow(format(message, ...params)))
|
|
27
|
+
}
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export function println(message: string, ...params: Array<number | string>) {
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Generate SDK modules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import path = require('path')
|
|
6
|
+
|
|
7
|
+
import { SdkController } from '../analyzer/controller'
|
|
8
|
+
import { SdkModules } from '../analyzer/controllers'
|
|
9
|
+
import { SdkHttpMethod, SdkMethod } from '../analyzer/methods'
|
|
10
|
+
import { SdkMethodParam, getParamResolvedTypes, isParamResolvedType } from '../analyzer/params'
|
|
11
|
+
import { paramsFromRoute, resolveRouteWith, unparseRoute } from '../analyzer/route'
|
|
12
|
+
import { ResolvedTypeDeps, normalizeExternalFilePath } from '../analyzer/typedeps'
|
|
13
|
+
import { config } from '../config'
|
|
14
|
+
import { panic } from '../logging'
|
|
15
|
+
|
|
16
|
+
export function replaceSuffix(str: string, { addSuffix = '', removeSuffix = '' }: { addSuffix?: string; removeSuffix?: string }) {
|
|
17
|
+
return str.replace(new RegExp(removeSuffix + '$'), '') + addSuffix
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function controllerExportName(controller: SdkController): string {
|
|
21
|
+
const suffixReplacement = config.controllerOutput?.exportName ?? {}
|
|
22
|
+
return replaceSuffix(controller.className, suffixReplacement)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function controllerFileName(controller: SdkController): { ext: string; name: string } {
|
|
26
|
+
const { ext, name } = path.parse(controller.path)
|
|
27
|
+
const suffixReplacement = config.controllerOutput?.fileName ?? {}
|
|
28
|
+
const fileName = replaceSuffix(name, suffixReplacement)
|
|
29
|
+
return {
|
|
30
|
+
name: fileName,
|
|
31
|
+
ext: ext || '.ts',
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function controllerRelativePath({
|
|
36
|
+
controller,
|
|
37
|
+
inputRelativePath,
|
|
38
|
+
}: {
|
|
39
|
+
controller: SdkController
|
|
40
|
+
inputRelativePath: string
|
|
41
|
+
}): string {
|
|
42
|
+
return path.join(inputRelativePath, controller.path)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generate the SDK's module and controllers files
|
|
47
|
+
* @param modules
|
|
48
|
+
* @returns
|
|
49
|
+
*/
|
|
50
|
+
export function generateSdkModules({
|
|
51
|
+
inputRelativePath,
|
|
52
|
+
modules,
|
|
53
|
+
}: {
|
|
54
|
+
inputRelativePath: string
|
|
55
|
+
modules: SdkModules
|
|
56
|
+
}): Map<string, string> {
|
|
57
|
+
/** Generated module files */
|
|
58
|
+
const genFiles = new Map<string, string>()
|
|
59
|
+
|
|
60
|
+
/** Index file content */
|
|
61
|
+
const indexImports = [
|
|
62
|
+
'import type { EndpointBuilder } from "@reduxjs/toolkit/query/react";',
|
|
63
|
+
'import { BaseRequest } from "./baseRequest";',
|
|
64
|
+
]
|
|
65
|
+
const indexExports = []
|
|
66
|
+
const indexEndpoints = []
|
|
67
|
+
|
|
68
|
+
// Iterate over each module
|
|
69
|
+
for (const [moduleName, controllers] of modules) {
|
|
70
|
+
// Iterate over each of the module's controllers
|
|
71
|
+
for (const [_, controller] of controllers) {
|
|
72
|
+
const { ext: fileExt, name: fileName } = controllerFileName(controller)
|
|
73
|
+
genFiles.set(fileName + fileExt, generateController({ moduleName, controller, inputRelativePath }))
|
|
74
|
+
|
|
75
|
+
const controllerExport = controllerExportName(controller)
|
|
76
|
+
indexImports.push(`import ${controllerExport} from "./${fileName}";`)
|
|
77
|
+
indexExports.push(`${controllerExport}`)
|
|
78
|
+
indexEndpoints.push(`...${controllerExport}.build(builder)`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const indexEndpointBuilder = [
|
|
83
|
+
'const build = (builder: EndpointBuilder<typeof BaseRequest, any, any>) => ({',
|
|
84
|
+
indexEndpoints.join(',\n'),
|
|
85
|
+
'})',
|
|
86
|
+
].join('\n')
|
|
87
|
+
|
|
88
|
+
indexExports.push('build')
|
|
89
|
+
|
|
90
|
+
const indexDefaultExport = ['export default {', indexExports.join(',\n'), '}'].join('\n')
|
|
91
|
+
|
|
92
|
+
const indexContent = [...indexImports, '', indexEndpointBuilder, '', indexDefaultExport, ''].join('\n')
|
|
93
|
+
|
|
94
|
+
genFiles.set('index.ts', indexContent)
|
|
95
|
+
|
|
96
|
+
return genFiles
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function generateController({
|
|
100
|
+
controller,
|
|
101
|
+
inputRelativePath,
|
|
102
|
+
moduleName,
|
|
103
|
+
}: {
|
|
104
|
+
controller: SdkController
|
|
105
|
+
inputRelativePath: string
|
|
106
|
+
moduleName: string
|
|
107
|
+
}): string {
|
|
108
|
+
const controllerName = controller.className
|
|
109
|
+
/** Generated controller's content */
|
|
110
|
+
const out: string[] = []
|
|
111
|
+
|
|
112
|
+
out.push('/// Module: ' + moduleName)
|
|
113
|
+
out.push(`/// Controller: ${controllerName}`)
|
|
114
|
+
out.push(`/// File Path: file:///./${controllerRelativePath({ controller, inputRelativePath })}`)
|
|
115
|
+
out.push('')
|
|
116
|
+
out.push('import type { EndpointBuilder } from "@reduxjs/toolkit/query/react";')
|
|
117
|
+
out.push('import { BaseRequest } from "./baseRequest";')
|
|
118
|
+
out.push('import type { BaseRequestArgs } from "./baseRequest";')
|
|
119
|
+
|
|
120
|
+
const imports = new Map<string, string[]>()
|
|
121
|
+
|
|
122
|
+
const depsToImport = new Array<ResolvedTypeDeps>()
|
|
123
|
+
|
|
124
|
+
// Iterate over each controller
|
|
125
|
+
for (const method of controller.methods.values()) {
|
|
126
|
+
const { bodyParams, queryParams, routeParams } = method.params
|
|
127
|
+
|
|
128
|
+
depsToImport.push(method.returnType)
|
|
129
|
+
depsToImport.push(...getParamResolvedTypes(routeParams))
|
|
130
|
+
depsToImport.push(...getParamResolvedTypes(queryParams))
|
|
131
|
+
depsToImport.push(...getParamResolvedTypes(bodyParams))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build the imports list
|
|
135
|
+
for (const dep of depsToImport) {
|
|
136
|
+
for (const [file, types] of dep.dependencies) {
|
|
137
|
+
let imported = imports.get(file)
|
|
138
|
+
|
|
139
|
+
if (!imported) {
|
|
140
|
+
imported = []
|
|
141
|
+
imports.set(file, imported)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const typ of types) {
|
|
145
|
+
if (!imported.includes(typ)) {
|
|
146
|
+
imported.push(typ)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const [file, types] of imports) {
|
|
153
|
+
out.push(
|
|
154
|
+
`import type { ${types.join(', ')} } from "../_types/${normalizeExternalFilePath(file.replace(/\\/g, '/')).replace(/\\/g, '/')}";`
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
out.push('')
|
|
159
|
+
out.push(`export default class ${controllerExportName(controller)} {`)
|
|
160
|
+
|
|
161
|
+
const { mutations, queries } = controller.methods.reduce(
|
|
162
|
+
(acc, method) => {
|
|
163
|
+
if (method.httpMethod === SdkHttpMethod.Get) {
|
|
164
|
+
acc.queries.push(method)
|
|
165
|
+
} else {
|
|
166
|
+
acc.mutations.push(method)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return acc
|
|
170
|
+
},
|
|
171
|
+
{ mutations: [] as SdkMethod[], queries: [] as SdkMethod[] }
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
out.push(`static readonly queries = {`)
|
|
175
|
+
|
|
176
|
+
for (const method of queries) {
|
|
177
|
+
out.push(generateSdkMethod({ method, controller, inputRelativePath }))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
out.push(' }')
|
|
181
|
+
out.push('')
|
|
182
|
+
|
|
183
|
+
out.push(`static readonly mutations = {`)
|
|
184
|
+
|
|
185
|
+
for (const method of mutations) {
|
|
186
|
+
out.push(generateSdkMethod({ method, controller, inputRelativePath }))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
out.push(' }')
|
|
190
|
+
out.push('')
|
|
191
|
+
|
|
192
|
+
out.push(generateEndpointBuilder({ controller, mutations, queries }))
|
|
193
|
+
out.push('};')
|
|
194
|
+
|
|
195
|
+
return out.join('\n')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function generateEndpointBuilder({
|
|
199
|
+
mutations,
|
|
200
|
+
queries,
|
|
201
|
+
}: {
|
|
202
|
+
controller: SdkController
|
|
203
|
+
mutations: SdkMethod[]
|
|
204
|
+
queries: SdkMethod[]
|
|
205
|
+
}): string {
|
|
206
|
+
const out = []
|
|
207
|
+
out.push('static readonly build = (builder: EndpointBuilder<typeof BaseRequest, any, any>) => ({')
|
|
208
|
+
|
|
209
|
+
for (const method of queries) {
|
|
210
|
+
const resultType = awaitedResolvedType(method.returnType.resolvedType)
|
|
211
|
+
const argsType = generateMergedArgsType(method)
|
|
212
|
+
out.push(` ${method.name}: builder.query<${resultType}, ${argsType}>({`)
|
|
213
|
+
out.push(` queryFn: (args, api, extraOptions) => {`)
|
|
214
|
+
out.push(` return BaseRequest(this.queries.${method.name}(args), api, extraOptions)`)
|
|
215
|
+
out.push(` },`)
|
|
216
|
+
out.push(` }),`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const method of mutations) {
|
|
220
|
+
const resultType = awaitedResolvedType(method.returnType.resolvedType)
|
|
221
|
+
const argsType = generateMergedArgsType(method)
|
|
222
|
+
out.push(` ${method.name}: builder.mutation<${resultType}, ${argsType}>({`)
|
|
223
|
+
out.push(` queryFn: (args, api, extraOptions) => {`)
|
|
224
|
+
out.push(` return BaseRequest(this.mutations.${method.name}(args), api, extraOptions)`)
|
|
225
|
+
out.push(` },`)
|
|
226
|
+
out.push(` }),`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
out.push('})')
|
|
230
|
+
|
|
231
|
+
return out.join('\n')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function awaitedResolvedType(type: string): string {
|
|
235
|
+
const prefix = 'Promise<'
|
|
236
|
+
return type.startsWith(prefix) ? type.substring(prefix.length, type.length - 1) : type
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function generateSdkMethod({
|
|
240
|
+
controller,
|
|
241
|
+
inputRelativePath,
|
|
242
|
+
method,
|
|
243
|
+
}: {
|
|
244
|
+
controller: SdkController
|
|
245
|
+
inputRelativePath: string
|
|
246
|
+
method: SdkMethod
|
|
247
|
+
}): string {
|
|
248
|
+
const out = []
|
|
249
|
+
|
|
250
|
+
out.push('')
|
|
251
|
+
out.push(` /**`)
|
|
252
|
+
out.push(` * ${method.httpMethod} ${unparseRoute(method.route)}`)
|
|
253
|
+
out.push(` * ${controller.className}.${method.name}`)
|
|
254
|
+
out.push(` * file:///./${controllerRelativePath({ controller, inputRelativePath })}`)
|
|
255
|
+
out.push(` */`)
|
|
256
|
+
out.push(`${generateSdkMethodSignature(method)} {`)
|
|
257
|
+
out.push(generateSdkMethodBody(method))
|
|
258
|
+
out.push(' },')
|
|
259
|
+
return out.join('\n')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function generateSdkMethodSignature(method: SdkMethod): string {
|
|
263
|
+
const mergedArgsType = generateMergedArgsType(method)
|
|
264
|
+
return `${method.name}(args: ${mergedArgsType}): BaseRequestArgs`
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function generateMergedArgsType(method: SdkMethod): string {
|
|
268
|
+
const { params } = method
|
|
269
|
+
|
|
270
|
+
const inputTypes = []
|
|
271
|
+
|
|
272
|
+
if (params.routeParams) {
|
|
273
|
+
inputTypes.push(methodParamsToString(params.routeParams))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (params.queryParams) {
|
|
277
|
+
inputTypes.push(methodParamsToString(params.queryParams))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (params.bodyParams) {
|
|
281
|
+
inputTypes.push(methodParamsToString(params.bodyParams))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return inputTypes.join(' &\n') || 'void'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Generate a request call to Central for the generated files
|
|
289
|
+
* @param method
|
|
290
|
+
* @returns
|
|
291
|
+
*/
|
|
292
|
+
export function generateSdkMethodBody(method: SdkMethod): string {
|
|
293
|
+
const output = []
|
|
294
|
+
const { httpMethod, route } = method
|
|
295
|
+
const routeParams = paramsFromRoute(route)
|
|
296
|
+
const resolvedRoute = resolveRouteWith(route, (param) => '${' + param + '}')
|
|
297
|
+
const hasAnyParams = method.params.routeParams || method.params.queryParams || method.params.bodyParams
|
|
298
|
+
|
|
299
|
+
if (resolvedRoute instanceof Error) {
|
|
300
|
+
panic('Internal error: failed to resolve route: ' + resolvedRoute.message)
|
|
301
|
+
}
|
|
302
|
+
const routeParamsSpread = routeParams.length > 0 ? `${routeParams.join(', ')},` : ''
|
|
303
|
+
|
|
304
|
+
// Destructure parameters if they exist
|
|
305
|
+
if (hasAnyParams) {
|
|
306
|
+
output.push(`const { ${routeParamsSpread} ...rest } = args`)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const isGet = httpMethod === SdkHttpMethod.Get
|
|
310
|
+
const body = isGet || !hasAnyParams ? 'null' : 'rest'
|
|
311
|
+
const query = isGet && hasAnyParams ? 'rest' : '{}'
|
|
312
|
+
|
|
313
|
+
output.push(`return { body: ${body}, method: '${httpMethod}', query: ${query}, route: \`${resolvedRoute}\` }`)
|
|
314
|
+
return output.join('\n')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function methodParamsToString(params: SdkMethodParam): string {
|
|
318
|
+
if (params === null) {
|
|
319
|
+
return ''
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (isParamResolvedType(params)) {
|
|
323
|
+
return params.resolvedType
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const output = ['{']
|
|
327
|
+
for (const [name, type] of Object.entries(params)) {
|
|
328
|
+
output.push(`${name}: ${type.resolvedType},`)
|
|
329
|
+
}
|
|
330
|
+
output.push('}')
|
|
331
|
+
return output.join('\n')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// const ret = method.returnType.resolvedType
|
|
335
|
+
// const promised = ret.startsWith('Promise<') ? ret : `Promise<${ret}>`
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Generate type files for the SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path'
|
|
6
|
+
|
|
7
|
+
import { TypesExtractorContent } from '../analyzer/extractor'
|
|
8
|
+
import { normalizeExternalFilePath } from '../analyzer/typedeps'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate the non-formatted type files which will be used by the SDK's route functions
|
|
12
|
+
* @param sdkTypes
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
export function generateSdkTypeFiles(sdkTypes: TypesExtractorContent): Map<string, string> {
|
|
16
|
+
/** Generated type files */
|
|
17
|
+
const genFiles = new Map<string, string>()
|
|
18
|
+
|
|
19
|
+
// While analyzing the controllers' content, all types used by their methods have been extracted
|
|
20
|
+
// The list of these types is then provided to this function as the `sdkTypes` argument, allowing us
|
|
21
|
+
// to establish the list of all dependencies.
|
|
22
|
+
for (const [file, types] of sdkTypes) {
|
|
23
|
+
const out = []
|
|
24
|
+
|
|
25
|
+
/** List of imports in the current file */
|
|
26
|
+
const imports = new Map<string, string[]>()
|
|
27
|
+
|
|
28
|
+
// Iterate over all types in the current `file`
|
|
29
|
+
for (const extracted of types.values()) {
|
|
30
|
+
// Iterate over all of the extracted type's dependencies
|
|
31
|
+
for (const dep of extracted.dependencies) {
|
|
32
|
+
// If the dependency is from the same file, then we have nothing to do
|
|
33
|
+
if (dep.relativePath === file) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Push the typename to the list of imports
|
|
38
|
+
let imported = imports.get(dep.relativePathNoExt)
|
|
39
|
+
|
|
40
|
+
if (!imported) {
|
|
41
|
+
imported = [dep.typename]
|
|
42
|
+
imports.set(dep.relativePathNoExt, imported)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!imported.includes(dep.typename)) {
|
|
46
|
+
imported.push(dep.typename)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Generate an import statement for each imported type
|
|
52
|
+
out.push(
|
|
53
|
+
[...imports]
|
|
54
|
+
.map(([depFile, types]) => {
|
|
55
|
+
let depPath = path.relative(path.dirname(file), normalizeExternalFilePath(depFile)).replace(/\\/g, '/')
|
|
56
|
+
if (!depPath.includes('/')) depPath = './' + depPath
|
|
57
|
+
return `import type { ${types.join(', ')} } from "${
|
|
58
|
+
depPath.startsWith('./') || depPath.startsWith('../') ? depPath : './' + depPath
|
|
59
|
+
}"`
|
|
60
|
+
})
|
|
61
|
+
.join('\n')
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// Add the extracted types' declaration, indented
|
|
65
|
+
for (const extracted of types.values()) {
|
|
66
|
+
out.push(extracted.content.replace(/^/gm, ' '))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generate the type file
|
|
70
|
+
genFiles.set(file, out.join('\n'))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return genFiles
|
|
74
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Entrypoint of the SDK generator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs'
|
|
6
|
+
import * as path from 'path'
|
|
7
|
+
|
|
8
|
+
import { SdkContent } from '../analyzer'
|
|
9
|
+
import { normalizeExternalFilePath } from '../analyzer/typedeps'
|
|
10
|
+
import { Config } from '../config'
|
|
11
|
+
import { debug, panic, println } from '../logging'
|
|
12
|
+
|
|
13
|
+
import { generateSdkModules } from './genmodules'
|
|
14
|
+
import { generateSdkTypeFiles } from './gentypes'
|
|
15
|
+
import { findPrettierConfig, prettify } from './prettier'
|
|
16
|
+
import { defaultSdkInterface } from './sdk-interface'
|
|
17
|
+
|
|
18
|
+
export default async function generateRTKQueryEndpoints(config: Config, sdkContent: SdkContent): Promise<void> {
|
|
19
|
+
const prettifyOutput = config.prettify !== false
|
|
20
|
+
|
|
21
|
+
if (!prettifyOutput) {
|
|
22
|
+
debug('NOTE: files will not be prettified with Prettier')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const outputPath = path.resolve(process.cwd(), config.sdkOutputPath)
|
|
26
|
+
const inputPath = path.resolve(process.cwd(), config.apiInputPath)
|
|
27
|
+
const inputRelativePath = path.relative(outputPath, inputPath)
|
|
28
|
+
|
|
29
|
+
if (fs.existsSync(outputPath)) {
|
|
30
|
+
if (config.overwriteOldOutputDir === false) {
|
|
31
|
+
panic("Please provide an output directory that doesn't exist yet")
|
|
32
|
+
} else {
|
|
33
|
+
fs.rmSync(outputPath, { recursive: true })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const outputParentDir = path.dirname(outputPath)
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(outputParentDir)) {
|
|
40
|
+
panic("Output directory's parent {magentaBright} does not exist.", outputParentDir)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fs.mkdirSync(outputPath)
|
|
44
|
+
|
|
45
|
+
const prettierConfig = prettifyOutput ? findPrettierConfig(config) : {}
|
|
46
|
+
|
|
47
|
+
const writeScriptTo = async (parentDir: null | string, file: string, utf8Content: string) => {
|
|
48
|
+
if (file.endsWith('.ts')) {
|
|
49
|
+
utf8Content =
|
|
50
|
+
'/// Auto-generated file (nest-sdk-generator)\n' +
|
|
51
|
+
'/// Please do not edit this file - re-generate the SDK using the generator instead.\n' +
|
|
52
|
+
(config.generateTimestamps !== false ? '/// Generated on: ' + new Date().toUTCString() + '\n' : '') +
|
|
53
|
+
'///\n' +
|
|
54
|
+
utf8Content
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fullPath = path.resolve(outputPath, parentDir ?? '', file)
|
|
58
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true })
|
|
59
|
+
|
|
60
|
+
const fileContent = prettifyOutput
|
|
61
|
+
? await prettify(utf8Content, prettierConfig, file.endsWith('.json') ? 'json' : 'typescript')
|
|
62
|
+
: utf8Content
|
|
63
|
+
fs.writeFileSync(fullPath, fileContent, 'utf8')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
println('> Generating type files...')
|
|
67
|
+
|
|
68
|
+
for (const [file, content] of generateSdkTypeFiles(sdkContent.types)) {
|
|
69
|
+
await writeScriptTo('_types', normalizeExternalFilePath(file), content)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
println('> Generating modules...')
|
|
73
|
+
const modules = sdkContent.modules
|
|
74
|
+
for (const [file, content] of generateSdkModules({ modules, inputRelativePath })) {
|
|
75
|
+
await writeScriptTo(null, file, content)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sdkInterfacePath = path.resolve(process.cwd(), config.sdkInterfacePath)
|
|
79
|
+
|
|
80
|
+
const relativeSdkInterfacePath = path
|
|
81
|
+
.relative(outputPath, sdkInterfacePath)
|
|
82
|
+
.replace(/\\/g, '/')
|
|
83
|
+
.replace(/\.([jt]sx?)$/, '')
|
|
84
|
+
|
|
85
|
+
await writeScriptTo(null, 'baseRequest.ts', `export { BaseRequest, type BaseRequestArgs } from "${relativeSdkInterfacePath}"`)
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(sdkInterfacePath) && config.generateDefaultSdkInterface !== false) {
|
|
88
|
+
println('├─ Generating default SDK interface...')
|
|
89
|
+
|
|
90
|
+
fs.writeFileSync(sdkInterfacePath, defaultSdkInterface, 'utf8')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Interface for prettifying generated files
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs'
|
|
6
|
+
|
|
7
|
+
import * as prettier from 'prettier'
|
|
8
|
+
|
|
9
|
+
import { Config } from '../config'
|
|
10
|
+
import { panic } from '../logging'
|
|
11
|
+
import { findFileAbove } from '../utils'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find a .prettierrc configuration file in the current directory or above
|
|
15
|
+
*/
|
|
16
|
+
export function findPrettierConfig(config: Config): object {
|
|
17
|
+
const prettierConfigPath = config.prettierConfig ?? findFileAbove('.prettierrc', config.sdkOutputPath)
|
|
18
|
+
|
|
19
|
+
if (!prettierConfigPath) {
|
|
20
|
+
return {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(prettierConfigPath)) {
|
|
24
|
+
panic('Prettier configuration was not found at specified path {magenta}', prettierConfigPath)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const text = fs.readFileSync(prettierConfigPath, 'utf8')
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(text)
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw new Error('Failed to parse Prettier configuration: ' + e)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Prettify a TypeScript or JSON input
|
|
38
|
+
* @param source
|
|
39
|
+
* @param config
|
|
40
|
+
* @param parser
|
|
41
|
+
* @returns
|
|
42
|
+
*/
|
|
43
|
+
export function prettify(source: string, config: object, parser: 'typescript' | 'json'): Promise<string> {
|
|
44
|
+
return prettier.format(source, {
|
|
45
|
+
parser,
|
|
46
|
+
...config,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const defaultSdkInterface = `
|
|
2
|
+
export async function request(
|
|
3
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
4
|
+
uri: string,
|
|
5
|
+
body: unknown,
|
|
6
|
+
query: Record<string, string>
|
|
7
|
+
): Promise<unknown> {
|
|
8
|
+
const url = new URL('http://localhost:3000' + uri)
|
|
9
|
+
url.search = new URLSearchParams(query).toString()
|
|
10
|
+
|
|
11
|
+
const params: RequestInit = {
|
|
12
|
+
method,
|
|
13
|
+
// Required for content to be correctly parsed by NestJS
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Setting a body is forbidden on GET requests
|
|
18
|
+
if (method !== 'GET') {
|
|
19
|
+
params.body = JSON.stringify(body)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return fetch(url.toString(), params).then((res) => {
|
|
23
|
+
// Handle failed requests
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
throw Error(res.statusText)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return res.json()
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
`.trim()
|
package/src/utils.ts
CHANGED
|
@@ -51,8 +51,8 @@ export function findFilesRecursive(pattern: string | RegExp, dir: string, relati
|
|
|
51
51
|
return fs.lstatSync(fullPath).isDirectory()
|
|
52
52
|
? find(pattern, rootDir, fullPath)
|
|
53
53
|
: (typeof pattern === 'string' ? item === pattern : pattern.exec(item))
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
? [relative ? path.relative(rootDir, fullPath) : path.resolve(cwd, currDir, item)]
|
|
55
|
+
: []
|
|
56
56
|
})
|
|
57
57
|
.flat()
|
|
58
58
|
}
|