@buildvision/nest-sdk-generator 2.0.0 → 2.2.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 (73) hide show
  1. package/.eslintrc.js +63 -0
  2. package/README.md +2 -2
  3. package/build/analyzer/classdeps.js +1 -1
  4. package/build/analyzer/controller.d.ts +4 -4
  5. package/build/analyzer/controller.js +3 -3
  6. package/build/analyzer/controllers.d.ts +1 -1
  7. package/build/analyzer/controllers.js +4 -4
  8. package/build/analyzer/decorator.d.ts +2 -2
  9. package/build/analyzer/decorator.js +19 -19
  10. package/build/analyzer/extractor.d.ts +4 -4
  11. package/build/analyzer/extractor.js +2 -2
  12. package/build/analyzer/index.d.ts +1 -1
  13. package/build/analyzer/index.js +2 -2
  14. package/build/analyzer/methods.d.ts +7 -7
  15. package/build/analyzer/methods.js +5 -6
  16. package/build/analyzer/module.js +2 -2
  17. package/build/analyzer/params.d.ts +12 -12
  18. package/build/analyzer/params.js +31 -15
  19. package/build/analyzer/route.d.ts +1 -1
  20. package/build/analyzer/route.js +6 -4
  21. package/build/analyzer/typedeps.d.ts +6 -6
  22. package/build/analyzer/typedeps.js +3 -3
  23. package/build/bin.js +9 -13
  24. package/build/config.d.ts +33 -19
  25. package/build/config.js +2 -2
  26. package/build/generator/genmodules.d.ts +29 -5
  27. package/build/generator/genmodules.js +58 -31
  28. package/build/generator/gentypes.js +1 -1
  29. package/build/generator/index.d.ts +1 -1
  30. package/build/generator/index.js +23 -17
  31. package/build/generator/prettier.d.ts +1 -1
  32. package/build/generator/prettier.js +2 -2
  33. package/build/generator/sdk-interface.d.ts +1 -1
  34. package/build/generator/sdk-interface.js +5 -5
  35. package/build/logging.js +1 -1
  36. package/build/rtk-query-generator/genmodules.d.ts +69 -0
  37. package/build/rtk-query-generator/genmodules.js +295 -0
  38. package/build/rtk-query-generator/gentypes.d.ts +10 -0
  39. package/build/rtk-query-generator/gentypes.js +62 -0
  40. package/build/rtk-query-generator/index.d.ts +6 -0
  41. package/build/rtk-query-generator/index.js +84 -0
  42. package/build/rtk-query-generator/prettier.d.ts +16 -0
  43. package/build/rtk-query-generator/prettier.js +43 -0
  44. package/build/rtk-query-generator/sdk-interface.d.ts +1 -0
  45. package/build/rtk-query-generator/sdk-interface.js +36 -0
  46. package/build/tsconfig.tsbuildinfo +1 -1
  47. package/build/utils.js +1 -1
  48. package/bun.lockb +0 -0
  49. package/package.json +15 -5
  50. package/src/analyzer/classdeps.ts +3 -0
  51. package/src/analyzer/controller.ts +12 -6
  52. package/src/analyzer/controllers.ts +7 -4
  53. package/src/analyzer/extractor.ts +9 -6
  54. package/src/analyzer/index.ts +5 -2
  55. package/src/analyzer/methods.ts +13 -12
  56. package/src/analyzer/module.ts +4 -10
  57. package/src/analyzer/params.ts +78 -64
  58. package/src/analyzer/route.ts +5 -3
  59. package/src/analyzer/typedeps.ts +13 -11
  60. package/src/bin.ts +11 -13
  61. package/src/config.ts +39 -24
  62. package/src/generator/genmodules.ts +90 -37
  63. package/src/generator/gentypes.ts +1 -0
  64. package/src/generator/index.ts +23 -19
  65. package/src/generator/prettier.ts +4 -2
  66. package/src/logging.ts +2 -1
  67. package/src/rtk-query-generator/genmodules.ts +407 -0
  68. package/src/rtk-query-generator/gentypes.ts +74 -0
  69. package/src/rtk-query-generator/index.ts +92 -0
  70. package/src/rtk-query-generator/prettier.ts +48 -0
  71. package/src/rtk-query-generator/sdk-interface.ts +32 -0
  72. package/src/utils.ts +2 -2
  73. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,407 @@
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
+ const NAMES = {
17
+ buildRTKEndpoints: 'buildRTKEndpoints',
18
+ groupRTKEndpoints: 'groupRTKEndpoints',
19
+ } as const
20
+
21
+ /**
22
+ * Generate the SDK's module and controllers files
23
+ * @param modules
24
+ * @returns
25
+ */
26
+ export function generateSdkModules({
27
+ inputRelativePath,
28
+ modules,
29
+ }: {
30
+ inputRelativePath: string
31
+ modules: SdkModules
32
+ }): Map<string, string> {
33
+ /** Generated module files */
34
+ const genFiles = new Map<string, string>()
35
+
36
+ /** Index file content */
37
+ const imports = [
38
+ 'import type { Api, EndpointBuilder, reactHooksModuleName } from "@reduxjs/toolkit/query/react";',
39
+ 'import { BaseRequest } from "./baseRequest";',
40
+ ]
41
+ const exports = []
42
+ const controllerEndpointBuilders = []
43
+ const controllerEndpointGroupers = []
44
+
45
+ // Iterate over each module
46
+ for (const [moduleName, controllers] of modules) {
47
+ // Iterate over each of the module's controllers
48
+ for (const [_, controller] of controllers) {
49
+ const { ext: fileExt, name: fileName } = controllerFileName(controller)
50
+ genFiles.set(fileName + fileExt, generateController({ moduleName, controller, inputRelativePath }))
51
+
52
+ const controllerExport = controllerExportName(controller)
53
+ imports.push(`import ${controllerExport} from "./${fileName}";`)
54
+ exports.push(`${controllerExport}`)
55
+ controllerEndpointBuilders.push(`...${controllerExport}.${NAMES.buildRTKEndpoints}(builder)`)
56
+
57
+ const endpointGroupName = controllerEndpointGroupName(controller)
58
+ controllerEndpointGroupers.push(`${endpointGroupName}: ${controllerExport}.${NAMES.groupRTKEndpoints}(api)`)
59
+ }
60
+ }
61
+
62
+ const endpointBuilder = [
63
+ `const ${NAMES.buildRTKEndpoints} = (builder: EndpointBuilder<typeof BaseRequest, any, any>) => ({`,
64
+ controllerEndpointBuilders.join(',\n'),
65
+ '})',
66
+ ].join('\n')
67
+
68
+ const endpointGrouper = [
69
+ `const ${NAMES.groupRTKEndpoints} = (api: Api<typeof BaseRequest, ReturnType<typeof ${NAMES.buildRTKEndpoints}>, string, string, typeof reactHooksModuleName>) => ({`,
70
+ controllerEndpointGroupers.join(',\n'),
71
+ '})',
72
+ ].join('\n')
73
+
74
+ exports.push(NAMES.buildRTKEndpoints)
75
+ exports.push(NAMES.groupRTKEndpoints)
76
+
77
+ const defaultExport = ['export default {', exports.join(',\n'), '}'].join('\n')
78
+
79
+ const indexContent = [...imports, endpointBuilder, endpointGrouper, defaultExport].join('\n\n')
80
+
81
+ genFiles.set('index.ts', indexContent)
82
+
83
+ return genFiles
84
+ }
85
+
86
+ export function generateController({
87
+ controller,
88
+ inputRelativePath,
89
+ moduleName,
90
+ }: {
91
+ controller: SdkController
92
+ inputRelativePath: string
93
+ moduleName: string
94
+ }): string {
95
+ const controllerName = controller.className
96
+ /** Generated controller's content */
97
+ const out: string[] = []
98
+
99
+ out.push('/// Module: ' + moduleName)
100
+ out.push(`/// Controller: ${controllerName}`)
101
+ out.push(`/// File Path: ${generateControllerFileLink({ controller, inputRelativePath })}`)
102
+ out.push('')
103
+ out.push('import type { Api, EndpointBuilder, reactHooksModuleName } from "@reduxjs/toolkit/query/react";')
104
+ out.push('import { BaseRequest } from "./baseRequest";')
105
+ out.push('import type { BaseRequestArgs } from "./baseRequest";')
106
+
107
+ const imports = new Map<string, string[]>()
108
+
109
+ const depsToImport = new Array<ResolvedTypeDeps>()
110
+
111
+ // Iterate over each controller
112
+ for (const method of controller.methods.values()) {
113
+ const { bodyParams, queryParams, routeParams } = method.params
114
+
115
+ depsToImport.push(method.returnType)
116
+ depsToImport.push(...getParamResolvedTypes(routeParams))
117
+ depsToImport.push(...getParamResolvedTypes(queryParams))
118
+ depsToImport.push(...getParamResolvedTypes(bodyParams))
119
+ }
120
+
121
+ // Build the imports list
122
+ for (const dep of depsToImport) {
123
+ for (const [file, types] of dep.dependencies) {
124
+ let imported = imports.get(file)
125
+
126
+ if (!imported) {
127
+ imported = []
128
+ imports.set(file, imported)
129
+ }
130
+
131
+ for (const typ of types) {
132
+ if (!imported.includes(typ)) {
133
+ imported.push(typ)
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ for (const [file, types] of imports) {
140
+ out.push(
141
+ `import type { ${types.join(', ')} } from "../_types/${normalizeExternalFilePath(file.replace(/\\/g, '/')).replace(/\\/g, '/')}";`
142
+ )
143
+ }
144
+
145
+ out.push('')
146
+ out.push(`export default class ${controllerExportName(controller)} {`)
147
+
148
+ const { mutations, queries } = controller.methods.reduce(
149
+ (acc, method) => {
150
+ if (method.httpMethod === SdkHttpMethod.Get) {
151
+ acc.queries.push(method)
152
+ } else {
153
+ acc.mutations.push(method)
154
+ }
155
+
156
+ return acc
157
+ },
158
+ { mutations: [] as SdkMethod[], queries: [] as SdkMethod[] }
159
+ )
160
+
161
+ out.push(`static readonly queries = {`)
162
+
163
+ for (const method of queries) {
164
+ out.push(generateSdkMethod({ method, controller, inputRelativePath }))
165
+ }
166
+
167
+ out.push(' }')
168
+ out.push('')
169
+
170
+ out.push(`static readonly mutations = {`)
171
+
172
+ for (const method of mutations) {
173
+ out.push(generateSdkMethod({ method, controller, inputRelativePath }))
174
+ }
175
+
176
+ out.push(' }')
177
+ out.push('')
178
+
179
+ out.push(generateBuildRTKEndpoints({ controller, mutations, queries, inputRelativePath }))
180
+ out.push('')
181
+
182
+ out.push(generateGroupRTKEndpoints({ controller, inputRelativePath }))
183
+ out.push('')
184
+
185
+ out.push('};')
186
+
187
+ return out.join('\n')
188
+ }
189
+
190
+ export function generateSdkMethod({
191
+ controller,
192
+ inputRelativePath,
193
+ method,
194
+ }: {
195
+ controller: SdkController
196
+ inputRelativePath: string
197
+ method: SdkMethod
198
+ }): string {
199
+ const out = []
200
+
201
+ out.push('')
202
+ out.push(generateEndpointComment({ controller, inputRelativePath, method }))
203
+ out.push(`${generateSdkMethodSignature(method)} {`)
204
+ out.push(generateSdkMethodBody(method))
205
+ out.push(' },')
206
+ return out.join('\n')
207
+ }
208
+
209
+ export function generateSdkMethodSignature(method: SdkMethod): string {
210
+ const mergedArgsType = generateMergedArgsType(method)
211
+ return `${method.name}(args: ${mergedArgsType}): BaseRequestArgs`
212
+ }
213
+
214
+ export function generateMergedArgsType(method: SdkMethod): string {
215
+ const { params } = method
216
+
217
+ const inputTypes = []
218
+
219
+ if (params.routeParams) {
220
+ inputTypes.push(methodParamsToString(params.routeParams))
221
+ }
222
+
223
+ if (params.queryParams) {
224
+ inputTypes.push(methodParamsToString(params.queryParams))
225
+ }
226
+
227
+ if (params.bodyParams) {
228
+ inputTypes.push(methodParamsToString(params.bodyParams))
229
+ }
230
+
231
+ return inputTypes.join(' &\n') || 'void'
232
+ }
233
+
234
+ /**
235
+ * Generate a request call to Central for the generated files
236
+ * @param method
237
+ * @returns
238
+ */
239
+ export function generateSdkMethodBody(method: SdkMethod): string {
240
+ const output = []
241
+ const { httpMethod, route } = method
242
+ const routeParams = paramsFromRoute(route)
243
+ const resolvedRoute = resolveRouteWith(route, (param) => '${' + param + '}')
244
+ const hasAnyParams = method.params.routeParams || method.params.queryParams || method.params.bodyParams
245
+
246
+ if (resolvedRoute instanceof Error) {
247
+ panic('Internal error: failed to resolve route: ' + resolvedRoute.message)
248
+ }
249
+ const routeParamsSpread = routeParams.length > 0 ? `${routeParams.join(', ')},` : ''
250
+
251
+ // Destructure parameters if they exist
252
+ if (hasAnyParams) {
253
+ output.push(`const { ${routeParamsSpread} ...rest } = args`)
254
+ }
255
+
256
+ const isGet = httpMethod === SdkHttpMethod.Get
257
+ const body = isGet || !hasAnyParams ? 'null' : 'rest'
258
+ const query = isGet && hasAnyParams ? 'rest' : '{}'
259
+
260
+ output.push(`return { body: ${body}, method: '${httpMethod}', query: ${query}, route: \`${resolvedRoute}\` }`)
261
+ return output.join('\n')
262
+ }
263
+
264
+ export function generateBuildRTKEndpoints({
265
+ controller,
266
+ inputRelativePath,
267
+ mutations,
268
+ queries,
269
+ }: {
270
+ controller: SdkController
271
+ inputRelativePath: string
272
+ mutations: SdkMethod[]
273
+ queries: SdkMethod[]
274
+ }): string {
275
+ const out = []
276
+ out.push(`static readonly ${NAMES.buildRTKEndpoints} = (builder: EndpointBuilder<typeof BaseRequest, any, any>) => ({`)
277
+
278
+ for (const method of queries) {
279
+ out.push(generateEndpointComment({ controller, inputRelativePath, method }))
280
+
281
+ const resultType = awaitedResolvedType(method.returnType.resolvedType)
282
+ const argsType = generateMergedArgsType(method)
283
+
284
+ out.push(` ${method.name}: builder.query<${resultType}, ${argsType}>({`)
285
+ out.push(` queryFn: (args, api, extraOptions) => {`)
286
+ out.push(` return BaseRequest(this.queries.${method.name}(args), api, extraOptions)`)
287
+ out.push(` },`)
288
+ out.push(` }),`)
289
+ }
290
+
291
+ for (const method of mutations) {
292
+ out.push(generateEndpointComment({ controller, inputRelativePath, method }))
293
+
294
+ const resultType = awaitedResolvedType(method.returnType.resolvedType)
295
+ const argsType = generateMergedArgsType(method)
296
+
297
+ out.push(` ${method.name}: builder.mutation<${resultType}, ${argsType}>({`)
298
+ out.push(` queryFn: (args, api, extraOptions) => {`)
299
+ out.push(` return BaseRequest(this.mutations.${method.name}(args), api, extraOptions)`)
300
+ out.push(` },`)
301
+ out.push(` }),`)
302
+ }
303
+
304
+ out.push('})')
305
+
306
+ return out.join('\n')
307
+ }
308
+
309
+ export function generateGroupRTKEndpoints({
310
+ controller,
311
+ inputRelativePath,
312
+ }: {
313
+ controller: SdkController
314
+ inputRelativePath: string
315
+ }): string {
316
+ const out = []
317
+ out.push(
318
+ `static readonly ${NAMES.groupRTKEndpoints} = (api: Api<typeof BaseRequest, ReturnType<typeof this.${NAMES.buildRTKEndpoints}>, string, string, typeof reactHooksModuleName>) => ({`
319
+ )
320
+
321
+ for (const method of controller.methods) {
322
+ out.push(generateEndpointComment({ controller, inputRelativePath, method }))
323
+ out.push(` ${method.name}: api.endpoints.${method.name},`)
324
+ }
325
+
326
+ out.push('})')
327
+
328
+ return out.join('\n')
329
+ }
330
+
331
+ export function methodParamsToString(params: SdkMethodParam): string {
332
+ if (params === null) {
333
+ return ''
334
+ }
335
+
336
+ if (isParamResolvedType(params)) {
337
+ return params.resolvedType
338
+ }
339
+
340
+ const output = ['{']
341
+ for (const [name, type] of Object.entries(params)) {
342
+ output.push(`${name}: ${type.resolvedType},`)
343
+ }
344
+ output.push('}')
345
+ return output.join('\n')
346
+ }
347
+
348
+ export const generateEndpointComment = ({
349
+ controller,
350
+ inputRelativePath,
351
+ method,
352
+ }: {
353
+ controller: SdkController
354
+ inputRelativePath: string
355
+ method: SdkMethod
356
+ }) => {
357
+ const out = []
358
+ out.push(`/**`)
359
+ out.push(`* ${method.httpMethod} ${unparseRoute(method.route)} <=> ${controller.className}.${method.name}`)
360
+ out.push(`*`)
361
+ out.push(`* ${generateControllerFileLink({ controller, inputRelativePath })}`)
362
+ out.push(`*/`)
363
+ return out.join('\n')
364
+ }
365
+
366
+ export const generateControllerFileLink = ({ controller, inputRelativePath }: { controller: SdkController; inputRelativePath: string }) => {
367
+ return `file:///./${controllerRelativePath({ controller, inputRelativePath })} (cmd + click)`
368
+ }
369
+
370
+ export function replaceSuffix(str: string, { addSuffix = '', removeSuffix = '' }: { addSuffix?: string; removeSuffix?: string }) {
371
+ return str.replace(new RegExp(removeSuffix + '$'), '') + addSuffix
372
+ }
373
+
374
+ export function controllerEndpointGroupName(controller: SdkController): string {
375
+ const suffixReplacement = config.controllerOutput?.endpointGroupName ?? {}
376
+ return replaceSuffix(controller.className, suffixReplacement)
377
+ }
378
+
379
+ export function controllerExportName(controller: SdkController): string {
380
+ const suffixReplacement = config.controllerOutput?.exportName ?? {}
381
+ return replaceSuffix(controller.className, suffixReplacement)
382
+ }
383
+
384
+ export function controllerFileName(controller: SdkController): { ext: string; name: string } {
385
+ const { ext, name } = path.parse(controller.path)
386
+ const suffixReplacement = config.controllerOutput?.fileName ?? {}
387
+ const fileName = replaceSuffix(name, suffixReplacement)
388
+ return {
389
+ name: fileName,
390
+ ext: ext || '.ts',
391
+ }
392
+ }
393
+
394
+ export function controllerRelativePath({
395
+ controller,
396
+ inputRelativePath,
397
+ }: {
398
+ controller: SdkController
399
+ inputRelativePath: string
400
+ }): string {
401
+ return path.join(inputRelativePath, controller.path)
402
+ }
403
+
404
+ export function awaitedResolvedType(type: string): string {
405
+ const prefix = 'Promise<'
406
+ return type.startsWith(prefix) ? type.substring(prefix.length, type.length - 1) : type
407
+ }
@@ -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
- ? [relative ? path.relative(rootDir, fullPath) : path.resolve(cwd, currDir, item)]
55
- : []
54
+ ? [relative ? path.relative(rootDir, fullPath) : path.resolve(cwd, currDir, item)]
55
+ : []
56
56
  })
57
57
  .flat()
58
58
  }