@buildvision/nest-sdk-generator 2.3.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) 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 -26
  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 +2 -0
  52. package/build/logging.js +14 -11
  53. package/build/rtk-query-generator/genmodules.d.ts +1 -4
  54. package/build/rtk-query-generator/genmodules.js +27 -31
  55. package/build/rtk-query-generator/index.d.ts +1 -1
  56. package/build/rtk-query-generator/index.js +87 -65
  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 +29 -5
  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 +6 -1
  83. package/src/rtk-query-generator/genmodules.ts +9 -4
  84. package/src/rtk-query-generator/index.ts +1 -2
  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/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,1110 @@
1
+ /**
2
+ * @file Source API analyzer running on top of an `AnalysisHost` (tsgo or in-process TypeScript)
3
+ *
4
+ * This is a port of the legacy ts-morph analyzer (`src/analyzer`). It performs the same operations, in the same
5
+ * order, with the same messages, so that given the same type checker it produces the same SDK content.
6
+ * The parity tests enforce this by running it against the TypeScript program ts-morph builds.
7
+ */
8
+
9
+ import * as fs from 'fs'
10
+ import * as os from 'os'
11
+ import * as path from 'path'
12
+
13
+ import chalk = require('chalk')
14
+
15
+ import { builtinMagicTypes } from '../analyzer/builtin'
16
+ import { analyzeUri, debugUri, paramsFromRoute, Route } from '../analyzer/route'
17
+ import {
18
+ ArgDecorator,
19
+ camelcase,
20
+ ExtractedType,
21
+ flattenSdkResolvedTypes,
22
+ locateTypesFile,
23
+ MODULE_EXTENSIONS,
24
+ SdkContent,
25
+ SdkController,
26
+ SdkHttpMethod,
27
+ SdkMethod,
28
+ SdkMethodParam,
29
+ SdkMethodParamMap,
30
+ SdkMethodParams,
31
+ SdkModules,
32
+ serializeSdkContent,
33
+ TypeLocation,
34
+ TypeLocationWithExt,
35
+ TypesExtractorContent,
36
+ } from '../analyzer/sdk'
37
+ import { getImportResolvedTypeText, ResolvedTypeDeps, resolveTypeTextDependencies } from '../analyzer/typedeps'
38
+ import { Config, MagicType } from '../config'
39
+ import { debug, format, panic, unreachable, warn } from '../logging'
40
+ import { findFileAbove } from '../utils'
41
+
42
+ import {
43
+ decoratorArguments,
44
+ decoratorCallExpression,
45
+ decoratorName,
46
+ getClassMembers,
47
+ getDecorators,
48
+ getDeclarationProperties,
49
+ getExtendsTypeNodes,
50
+ isStringLiteral,
51
+ nodeName,
52
+ nodeText,
53
+ } from './ast'
54
+ import { AnalysisHost, TsNode, TsType } from './host'
55
+
56
+ /**
57
+ * Analyze the source API
58
+ * @param config Generator configuration
59
+ * @param createHost Creates the engine for a given tsconfig file
60
+ */
61
+ export async function analyzeWithHost(config: Config, createHost: (tsConfigFilePath: string) => AnalysisHost): Promise<SdkContent> {
62
+ const started = Date.now()
63
+
64
+ const sourcePath = path.resolve(process.cwd(), config.apiInputPath)
65
+
66
+ if (!sourcePath) panic('Please provide a source directory')
67
+
68
+ if (!fs.existsSync(sourcePath)) panic('Provided source path {magentaBright} does not exist', sourcePath)
69
+ if (!fs.lstatSync(sourcePath).isDirectory()) panic('Provided source path is not a directory')
70
+
71
+ debug(`Analyzing from source directory {yellow}`, sourcePath)
72
+
73
+ if (config.jsonOutput) {
74
+ const jsonOutputParentDir = path.dirname(path.resolve(process.cwd(), config.jsonOutput))
75
+
76
+ if (!fs.existsSync(jsonOutputParentDir)) {
77
+ panic("Output file's parent directory {magentaBright} does not exist.", jsonOutputParentDir)
78
+ }
79
+
80
+ debug('Writing output to {yellow}', config.jsonOutput, config.jsonPrettyOutput ? 'beautified' : 'minified')
81
+ }
82
+
83
+ // ====== Find & parse 'tsconfig.json' ====== //
84
+ if (config.tsconfigFile && config.tsconfigFile.includes('/')) {
85
+ panic('Provided tsconfig file name contains slashes')
86
+ }
87
+
88
+ const tsConfigFileName = config.tsconfigFile ?? 'tsconfig.json'
89
+ const tsConfigFilePath = path.join(sourcePath, tsConfigFileName)
90
+
91
+ if (!fs.existsSync(tsConfigFilePath)) {
92
+ panic('No {yellow} file found in provided source path {yellow}', tsConfigFileName, sourcePath)
93
+ }
94
+
95
+ const host = createHost(tsConfigFilePath)
96
+
97
+ try {
98
+ // Get the list of all TypeScript files in the source directory
99
+ const sourceTSFiles = host.getProjectFilePaths().map((file) => path.relative(sourcePath, file))
100
+ debug(`Found {magentaBright} source files.`, sourceTSFiles.length)
101
+
102
+ // Add them
103
+ debug('\nAdding them to the source project...')
104
+
105
+ let progressByTenth = 0
106
+ const strLen = sourceTSFiles.length.toString().length
107
+
108
+ const hasProgress = (filesTreated: number) => filesTreated / sourceTSFiles.length >= (progressByTenth + 1) / 10
109
+
110
+ const controllers = []
111
+
112
+ for (let i = 0; i < sourceTSFiles.length; i++) {
113
+ const file = sourceTSFiles[i]
114
+
115
+ if (file.endsWith('.controller.ts')) {
116
+ controllers.push(file)
117
+ }
118
+
119
+ if (hasProgress(i + 1)) {
120
+ while (hasProgress(i + 1)) progressByTenth++
121
+
122
+ debug(
123
+ '| Progress: {yellow} ({magentaBright} files) - {green} controller{} found',
124
+ (progressByTenth * 10).toString().padStart(3, ' ') + '%',
125
+ `${(i + 1).toString().padStart(strLen, ' ')} / ${sourceTSFiles.length}`,
126
+ controllers.length.toString().padStart(strLen, ''),
127
+ controllers.length > 1 ? 's' : ''
128
+ )
129
+ }
130
+ }
131
+
132
+ debug('All files were added successfully.\n')
133
+
134
+ const modules = analyzeControllers(host, controllers, sourcePath)
135
+
136
+ // Builtin magic types are concatenated **after** the configuration's ones, as this allows users to
137
+ // override the builtin ones if they want. Do **not** change the concatenation order!
138
+ const magicTypes = (config.magicTypes ?? []).concat(builtinMagicTypes)
139
+
140
+ const typesCache = new TypesExtractor(host, sourcePath, magicTypes)
141
+
142
+ const typesToExtract = locateTypesFile(flattenSdkResolvedTypes(modules))
143
+
144
+ debug('\n==== Extracting {} type' + (typesToExtract.length > 1 ? 's' : '') + ' ====\n', typesToExtract.length)
145
+
146
+ for (const loc of typesToExtract) {
147
+ const result = typesCache.extractType(loc)
148
+
149
+ if (result instanceof Error) {
150
+ panic(result.message)
151
+ }
152
+ }
153
+
154
+ const content: SdkContent = {
155
+ modules,
156
+ types: typesCache.extracted,
157
+ }
158
+
159
+ if (config.jsonOutput) {
160
+ fs.writeFileSync(config.jsonOutput, serializeSdkContent(content, !!config.jsonPrettyOutput), 'utf8')
161
+ }
162
+
163
+ debug('\n===== Done in {green}! ====', ((Date.now() - started) / 1000).toFixed(2) + 's')
164
+
165
+ return content
166
+ } finally {
167
+ host.dispose()
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get a source file from the program, throwing like ts-morph's `project.getSourceFileOrThrow()`
173
+ */
174
+ function getSourceFileOrThrow(host: AnalysisHost, absolutePath: string): TsNode {
175
+ const file = host.getSourceFile(absolutePath)
176
+
177
+ if (!file) {
178
+ throw new Error(`Could not find source file in project at the provided path: ${absolutePath}`)
179
+ }
180
+
181
+ return file
182
+ }
183
+
184
+ // =============================================================================================================== //
185
+ // Controllers & modules (see src/analyzer/controllers.ts, src/analyzer/controller.ts, src/analyzer/module.ts) //
186
+ // =============================================================================================================== //
187
+
188
+ function analyzeControllers(host: AnalysisHost, controllers: string[], absoluteSrcPath: string): SdkModules {
189
+ /** Hierarchised SDK informations */
190
+ const collected = new Map<string, Map<string, SdkController>>()
191
+ const usedControllerNames = new Set<string>()
192
+
193
+ /**
194
+ * Modules cache: contains for a given directory the nearest module file's path and name
195
+ * This allows to avoid having to analyze the whole directory structure for each controller
196
+ */
197
+ const modulesCache = new Map<string, string>()
198
+
199
+ /**
200
+ * Path of the declared modules
201
+ * When a module is detected and put in the cache, its name is registered here along with its path
202
+ * This allows to ensure there is no name clash between two different modules
203
+ */
204
+ const declaredModulesPath = new Map<string, string>()
205
+
206
+ debug(`Analyzing {yellow} controllers...`, controllers.length)
207
+
208
+ controllers.forEach((relativeControllerPath, i) => {
209
+ const absoluteControllerPath = path.resolve(absoluteSrcPath, relativeControllerPath)
210
+
211
+ debug(
212
+ '\n{cyan} {yellow}: {magentaBright} {cyan}\n',
213
+ '===== Analyzing controller',
214
+ `${i + 1}/${controllers.length}`,
215
+ relativeControllerPath,
216
+ '====='
217
+ )
218
+
219
+ const basePath = path.dirname(absoluteControllerPath)
220
+
221
+ let moduleName = modulesCache.get(basePath)
222
+
223
+ // Check if the module's name is in cache
224
+ if (!moduleName) {
225
+ // Else, find the nearest module file
226
+ const absoluteModulePath = findFileAbove(/^.*\.module\.ts$/, path.resolve(absoluteSrcPath, basePath))
227
+
228
+ if (absoluteModulePath === null) {
229
+ panic('No module file was found for controller at path: {yellow}', absoluteControllerPath)
230
+ }
231
+
232
+ const relativeModulePath = path.relative(absoluteSrcPath, absoluteModulePath)
233
+
234
+ // Get the module's name
235
+ moduleName = getModuleName(host, relativeModulePath, absoluteSrcPath)
236
+
237
+ debug('Discovered module: {yellow}', moduleName)
238
+
239
+ // Ensure this module is unique
240
+ const cachedModulePath = declaredModulesPath.get(moduleName)
241
+
242
+ if (cachedModulePath) {
243
+ panic(
244
+ `Two modules were declared with the same name {yellow}:\n` + `- One in {yellow}\n` + `- One in {yellow}`,
245
+ moduleName,
246
+ cachedModulePath,
247
+ relativeModulePath
248
+ )
249
+ }
250
+
251
+ modulesCache.set(basePath, moduleName)
252
+ }
253
+
254
+ if (moduleName in {}) {
255
+ panic(`Detected module whose name {yellow} collides with a JavaScript's native object property`, moduleName)
256
+ }
257
+
258
+ let moduleSdkInfos = collected.get(moduleName)
259
+
260
+ if (!moduleSdkInfos) {
261
+ moduleSdkInfos = new Map()
262
+ collected.set(moduleName, moduleSdkInfos)
263
+ }
264
+
265
+ if (i === 0) {
266
+ if (process.platform === 'linux' && os.release().toLocaleLowerCase().includes('microsoft') && absoluteSrcPath.startsWith('/mnt/')) {
267
+ warn("NOTE: On WSL, the first type analysis on a project located in Windows's filesystem may take a long time to complete.")
268
+ warn('Please consider moving your project to WSL, or running this tool directly from Windows')
269
+ }
270
+ }
271
+
272
+ const metadata = analyzeController(host, relativeControllerPath, absoluteSrcPath)
273
+
274
+ if (metadata instanceof Error) {
275
+ throw new Error(format('Failed to analyze controller at path {magenta}:\n{}', relativeControllerPath, metadata.message))
276
+ }
277
+
278
+ if (metadata) {
279
+ if (metadata.registrationName in {}) {
280
+ panic(
281
+ `Detected controller whose registration name {yellow} collides with a JavaScript's native object property`,
282
+ metadata.registrationName
283
+ )
284
+ }
285
+
286
+ // Ensure the each controller has a unique name across modules
287
+ if (usedControllerNames.has(metadata.className)) {
288
+ panic(
289
+ `Detected two controllers with the same name {yellow}. Second occurrence found in {yellow}`,
290
+ metadata.registrationName,
291
+ metadata.path
292
+ )
293
+ } else {
294
+ usedControllerNames.add(metadata.className)
295
+ }
296
+
297
+ moduleSdkInfos.set(metadata.className, metadata)
298
+ }
299
+ })
300
+
301
+ return collected
302
+ }
303
+
304
+ function getModuleName(host: AnalysisHost, modulePath: string, sourcePath: string): string {
305
+ const SK = host.SyntaxKind
306
+
307
+ // Prepare the source file to analyze
308
+ const file = getSourceFileOrThrow(host, path.resolve(sourcePath, modulePath))
309
+
310
+ // Find the module class declaration
311
+ const classDecl = (file.statements as TsNode[]).find((node) => node.kind === SK.ClassDeclaration && getDecorators(host, node).length > 0)
312
+
313
+ if (!classDecl) {
314
+ panic('No class declaration found in module at {yellow}', modulePath)
315
+ }
316
+
317
+ const moduleName = nodeName(classDecl, file)
318
+
319
+ if (moduleName === undefined) {
320
+ panic('Internal error: failed to retrieve name of declared class')
321
+ }
322
+
323
+ const decorators = getDecorators(host, classDecl)
324
+ const moduleDecorator = decorators.find((dec) => decoratorName(host, dec, file) === 'Module')
325
+
326
+ if (!moduleDecorator) {
327
+ panic(format(`Module class {yellow} is missing {yellow} decorator\nModule path is: {yellow}`, moduleName, '@Module', modulePath))
328
+ }
329
+
330
+ return moduleName
331
+ }
332
+
333
+ function analyzeController(host: AnalysisHost, controllerPath: string, absoluteSrcPath: string): SdkController | null | Error {
334
+ const SK = host.SyntaxKind
335
+
336
+ debug('Analyzing: {yellow}', controllerPath)
337
+
338
+ // Prepare the source file to analyze
339
+ const file = getSourceFileOrThrow(host, path.resolve(absoluteSrcPath, controllerPath))
340
+
341
+ // Find the controller class declaration
342
+ const classDecl = (file.statements as TsNode[]).find((node) => node.kind === SK.ClassDeclaration)
343
+
344
+ if (!classDecl) {
345
+ warn('No controller found in this file.')
346
+ return null
347
+ }
348
+
349
+ const className = nodeName(classDecl, file)
350
+
351
+ if (className === undefined) {
352
+ return new Error('Internal error: failed to retrieve name of declared class')
353
+ }
354
+
355
+ // By default, a controller is registered under its class name
356
+ // This is unless it provides an argument to its @Controller() decorator
357
+ let registrationName = camelcase(className)
358
+ let controllerUriPrefix: string | null = null
359
+
360
+ debug('Found class declaration: {yellow}', className)
361
+
362
+ // Get the @Controller() decorator
363
+ const decorator = getDecorators(host, classDecl).find((dec) => decoratorName(host, dec, file) === 'Controller')
364
+
365
+ if (!decorator) {
366
+ warn('Skipping this controller as it does not have a @Controller() decorator')
367
+ return null
368
+ }
369
+
370
+ // Get the decorator's call expression
371
+ const decCallExpr = decoratorCallExpression(host, decorator)
372
+
373
+ if (!decCallExpr) {
374
+ warn('Skipping this controller as its @Controller() decorator is not called')
375
+ return null
376
+ }
377
+
378
+ // Get the decorator's arguments
379
+ const decExpr: TsNode[] = decCallExpr.arguments
380
+
381
+ if (decExpr.length > 1) {
382
+ warn('Skipping this controller as its @Controller() decorator is called with more than 1 argument')
383
+ return null
384
+ }
385
+
386
+ // Get the first argument, which is expected to be the controller's registration name
387
+ // Example: `@Controller("SuperName")` will register the controller under the name "SuperName"
388
+ if (decExpr[0]) {
389
+ const nameArg = decExpr[0]
390
+
391
+ // Variables are not supported
392
+ if (!isStringLiteral(host, nameArg)) {
393
+ warn("Skipping this controller as its @Controller() decorator's argument is not a string literal")
394
+ return null
395
+ }
396
+
397
+ // Update the registration name
398
+ registrationName = nameArg.text
399
+ controllerUriPrefix = registrationName
400
+ debug('Registering controller {yellow} as {yellow} (as specified in @Controller())', className, registrationName)
401
+ } else {
402
+ // No argument was provided to the @Controller() decorator, so we stick with the original controller's name
403
+ debug('@Controller() was called without argument, registering controller under name {yellow}', registrationName)
404
+ }
405
+
406
+ // Generate a SDK interface for the controller's methods
407
+ const methods = analyzeMethods(host, file, classDecl, controllerUriPrefix, controllerPath, absoluteSrcPath)
408
+
409
+ if (methods instanceof Error) {
410
+ return methods
411
+ }
412
+
413
+ // Success!
414
+ debug(`└─ Done for controller {yellow}`, controllerPath)
415
+
416
+ return {
417
+ path: controllerPath,
418
+ className,
419
+ registrationName,
420
+ methods,
421
+ }
422
+ }
423
+
424
+ // =============================================================================================================== //
425
+ // Methods & parameters (see src/analyzer/methods.ts, src/analyzer/params.ts) //
426
+ // =============================================================================================================== //
427
+
428
+ function analyzeMethods(
429
+ host: AnalysisHost,
430
+ sourceFile: TsNode,
431
+ controllerClass: TsNode,
432
+ controllerUriPrefix: string | null,
433
+ filePath: string,
434
+ absoluteSrcPath: string
435
+ ): SdkMethod[] | Error {
436
+ // Output variable
437
+ const collected = new Array<SdkMethod>()
438
+
439
+ // Get the list of all methods
440
+ const methods = (controllerClass.members as TsNode[]).filter((node) => node.kind === host.SyntaxKind.MethodDeclaration)
441
+
442
+ for (const method of methods) {
443
+ const methodName = nodeName(method, sourceFile)!
444
+
445
+ debug('├─ Found method: {yellow}', methodName)
446
+
447
+ // Get the HTTP decorator(s) of the method
448
+ const decorators = getDecorators(host, method).filter((dec) =>
449
+ Object.keys(SdkHttpMethod).includes(decoratorName(host, dec, sourceFile))
450
+ )
451
+
452
+ // We expect to have exactly one HTTP decorator
453
+ if (decorators.length > 1) {
454
+ // If there is more than one decorator, that's invalid, so we can't analyze the method
455
+ return new Error(
456
+ format(
457
+ '├─── Detected multiple HTTP decorators on method: {yellow}' +
458
+ decorators.map((dec) => decoratorName(host, dec, sourceFile)).join(',')
459
+ )
460
+ )
461
+ } else if (decorators.length === 0) {
462
+ // If there isn't any HTTP decorator, this is simply not a method available from the outside and so we won't generate an interface for it
463
+ debug('├─── Skipping this method as it does not have an HTTP decorator')
464
+ continue
465
+ }
466
+
467
+ // Get the HTTP decorator
468
+ const dec = decorators[0]
469
+
470
+ const httpMethod = SdkHttpMethod[decoratorName(host, dec, sourceFile) as keyof typeof SdkHttpMethod]
471
+
472
+ debug('├─── Detected HTTP method: {magentaBright}', httpMethod.toLocaleUpperCase())
473
+
474
+ // Get the arguments provided to the HTTP decorator (we expect one, the URI path)
475
+ const decArgs = decoratorArguments(host, dec)
476
+
477
+ // The method's URI path
478
+ let uriPath: string
479
+
480
+ // We expect the decorator to have exactly one argument
481
+ if (decArgs.length > 1) {
482
+ // If we have more than one argument, that's invalid (or at least not supported here), so we can't analyze the method
483
+ return new Error(`Multiple (${decArgs.length}) arguments were provided to the HTTP decorator`)
484
+ } else if (decArgs.length === 0) {
485
+ // If there is no argument, we take the method's name as the URI path
486
+ debug('├─── No argument found for decorator, using base URI path.')
487
+ uriPath = ''
488
+ } else {
489
+ // If we have exactly one argument, hurray! That's our URI path.
490
+ const uriNameDec = decArgs[0]
491
+
492
+ // Variables are not supported
493
+ if (!isStringLiteral(host, uriNameDec)) {
494
+ return new Error(
495
+ format('├─── The argument provided to the HTTP decorator is not a string literal:\n>> {cyan}', nodeText(uriNameDec, sourceFile))
496
+ )
497
+ }
498
+
499
+ // Update the method's URI path
500
+ uriPath = uriNameDec.text
501
+
502
+ debug('├─── Detected argument in HTTP decorator, mapping this method to custom URI name')
503
+ }
504
+
505
+ debug('├─── Detected URI name: {yellow}', uriPath)
506
+
507
+ // Analyze the method's URI
508
+ const route = analyzeUri(controllerUriPrefix ? (uriPath ? `/${controllerUriPrefix}/${uriPath}` : `/` + controllerUriPrefix) : uriPath)
509
+
510
+ if (route instanceof Error) {
511
+ return new Error(
512
+ '├─── Detected unsupported URI format:\n' +
513
+ route.message
514
+ .split('\n')
515
+ .map((line) => '├───── ' + line)
516
+ .join('\n')
517
+ )
518
+ }
519
+
520
+ debug('├─── Parsed URI name to route: {yellow}', debugUri(route, chalk.blue))
521
+
522
+ // Analyze the method's arguments
523
+ debug('├─── Analyzing arguments...')
524
+ const params = extractParams({ host, sourceFile, controllerClass, httpMethod, route, method, filePath, absoluteSrcPath })
525
+
526
+ // Get the method's return type
527
+ debug('├─── Resolving return type...')
528
+ const returnType = resolveTypeTextDependencies(host.typeToString(host.getReturnTypeOfDeclaration(method)), filePath, absoluteSrcPath)
529
+
530
+ debug('├─── Detected return type: {cyan}', returnType.resolvedType)
531
+
532
+ // Success!
533
+ collected.push({
534
+ name: methodName,
535
+ httpMethod,
536
+ returnType,
537
+ route,
538
+ uriPath,
539
+ params,
540
+ })
541
+ }
542
+
543
+ return collected
544
+ }
545
+
546
+ type MethodContext = {
547
+ absoluteSrcPath: string
548
+ controllerClass: TsNode
549
+ filePath: string
550
+ host: AnalysisHost
551
+ httpMethod: SdkHttpMethod
552
+ method: TsNode
553
+ route: Route
554
+ sourceFile: TsNode
555
+ }
556
+
557
+ type DecoratedArg = {
558
+ arg: TsNode
559
+ argParamKey: string | null
560
+ context: MethodContext
561
+ decorator: TsNode
562
+ decoratorType: ArgDecorator
563
+ }
564
+
565
+ function extractParams(context: MethodContext): SdkMethodParams {
566
+ const { httpMethod, route } = context
567
+ const { Body, Param, Query } = extractDecoratedArgs(context)
568
+ const resolvedParams: SdkMethodParams = {
569
+ routeParams: mergeDecoratedArgs(Param),
570
+ queryParams: mergeDecoratedArgs(Query),
571
+ bodyParams: mergeDecoratedArgs(Body),
572
+ context,
573
+ }
574
+
575
+ /**
576
+ * Validate Body Params are not used in GET requests
577
+ */
578
+ if (httpMethod === SdkHttpMethod.Get && resolvedParams.bodyParams) {
579
+ panicWithContext(`${httpMethod} request should not have @Body params, use @Query params instead`, context)
580
+ }
581
+
582
+ /**
583
+ * Validate Query Params are not used in non-GET requests
584
+ */
585
+ if (httpMethod !== SdkHttpMethod.Get && resolvedParams.queryParams) {
586
+ panicWithContext(`${httpMethod} request should not have @Query params, use @Body params instead`, context)
587
+ }
588
+
589
+ /**
590
+ * Validate Route Params exist in route URL
591
+ */
592
+ const { routeParams } = resolvedParams
593
+ if (routeParams) {
594
+ const allowedRouteParams = new Set(paramsFromRoute(route))
595
+ // Like the legacy analyzer, this checks the keys of the resolved params object. For a generic `@Param()` argument,
596
+ // these are the keys of the resolved type's description (rawType, resolvedType...), which are rejected.
597
+ const usedRouteParams = Object.keys(routeParams)
598
+
599
+ for (const usedParam of usedRouteParams) {
600
+ if (!allowedRouteParams.has(usedParam)) {
601
+ panicWithContext(`Route param ${usedParam} does not appear in route URL`, context)
602
+ }
603
+ }
604
+ }
605
+
606
+ return resolvedParams
607
+ }
608
+
609
+ function extractDecoratedArgs(context: MethodContext): Record<ArgDecorator, DecoratedArg[]> {
610
+ const decoratedArgs = {
611
+ [ArgDecorator.Param]: [] as DecoratedArg[],
612
+ [ArgDecorator.Query]: [] as DecoratedArg[],
613
+ [ArgDecorator.Body]: [] as DecoratedArg[],
614
+ }
615
+
616
+ const { controllerClass, host, method, sourceFile } = context
617
+ const args: TsNode[] = method.parameters
618
+
619
+ for (const arg of args) {
620
+ const argName = nodeName(arg, sourceFile)
621
+
622
+ debug('├───── Detected argument: {yellow}', argName!)
623
+
624
+ // Arguments are collected as soon as they have a decorator like @Query() or @Body()
625
+ const argDecorators = getDecorators(host, arg).filter((dec) =>
626
+ (Object.values(ArgDecorator) as string[]).includes(decoratorName(host, dec, sourceFile))
627
+ )
628
+
629
+ if (argDecorators.length === 0) {
630
+ // If we have no argument, this is not an argument we are interested in, so we just skip it
631
+ debug('├───── Skipping this argument as it does not have a decorator')
632
+ continue
633
+ } else if (argDecorators.length > 1) {
634
+ panic(
635
+ `${nodeName(controllerClass, sourceFile)} ${nodeName(method, sourceFile)} has multiple decorators on argument ${argName} ${argDecorators
636
+ .map((dec) => decoratorName(host, dec, sourceFile))
637
+ .join(', ')}`
638
+ )
639
+ }
640
+ // Get the only decrator
641
+ const decorator = argDecorators[0]
642
+ const decoratorType = decoratorName(host, decorator, sourceFile) as ArgDecorator
643
+ const argParamKey = extractArgParamKey({ ...context, arg, decorator })
644
+ const argType = host.getTypeAtLocation(arg)
645
+
646
+ if (!argParamKey && !host.isObjectType(argType)) {
647
+ panic(
648
+ `${nodeName(controllerClass, sourceFile)} ${nodeName(method, sourceFile)} generic controller argument ${decoratorType}() ${argName} must be an object type`
649
+ )
650
+ }
651
+
652
+ decoratedArgs[decoratorType].push({
653
+ arg,
654
+ argParamKey,
655
+ decorator,
656
+ decoratorType,
657
+ context,
658
+ })
659
+ }
660
+
661
+ return decoratedArgs
662
+ }
663
+
664
+ function extractArgParamKey(
665
+ context: MethodContext & {
666
+ arg: TsNode
667
+ decorator: TsNode
668
+ }
669
+ ): string | null {
670
+ const { arg, controllerClass, decorator, host, method, sourceFile } = context
671
+ const decoratorArgs = decoratorArguments(host, decorator)
672
+
673
+ if (decoratorArgs.length > 1) {
674
+ panic(
675
+ `${nodeName(controllerClass, sourceFile)} ${nodeName(method, sourceFile)} ${decoratorName(host, decorator, sourceFile)}() ${nodeName(
676
+ arg,
677
+ sourceFile
678
+ )} argument decorator has multiple parameters`
679
+ )
680
+ } else if (decoratorArgs.length === 0) {
681
+ return null
682
+ }
683
+
684
+ const decoratorArg = decoratorArgs[0]
685
+
686
+ if (!isStringLiteral(host, decoratorArg)) {
687
+ panic('The argument provided to the decorator is not a string literal:\n>>> {cyan}', nodeText(arg, sourceFile))
688
+ }
689
+
690
+ const paramKey: string = decoratorArg.text
691
+ if (paramKey in {}) {
692
+ panicWithContext(
693
+ `${decoratorName(host, decorator, sourceFile)}('${paramKey}') param name collides with JavaScript native object property`,
694
+ context
695
+ )
696
+ }
697
+
698
+ return paramKey
699
+ }
700
+
701
+ function mergeDecoratedArgs(decoratedArgs: DecoratedArg[]): SdkMethodParam {
702
+ let genericParam: SdkMethodParam | null = null
703
+ const paramMap: SdkMethodParamMap = {}
704
+
705
+ for (const decoratedArg of decoratedArgs) {
706
+ const { arg, argParamKey, context, decoratorType } = decoratedArg
707
+ const resolvedType = resolveTypeTextDependencies(
708
+ context.host.typeToString(context.host.getTypeAtLocation(arg)),
709
+ context.filePath,
710
+ context.absoluteSrcPath
711
+ )
712
+
713
+ if (argParamKey) {
714
+ if (paramMap[argParamKey]) {
715
+ panicWithContext(`${decoratorType}('${argParamKey}') used twice in controller method`, decoratedArg.context)
716
+ }
717
+
718
+ paramMap[argParamKey] = resolvedType
719
+ } else {
720
+ if (genericParam) {
721
+ panicWithContext(`${decoratorType}() used twice in controller method`, decoratedArg.context)
722
+ }
723
+
724
+ genericParam = resolvedType
725
+ }
726
+ }
727
+
728
+ const hasArgsByKey = Object.keys(paramMap).length > 0
729
+
730
+ if (genericParam && hasArgsByKey) {
731
+ panicWithContext(`Cannot mix generic and specific ${decoratedArgs[0].decoratorType}()`, decoratedArgs[0].context)
732
+ }
733
+
734
+ return hasArgsByKey ? paramMap : genericParam
735
+ }
736
+
737
+ const panicWithContext = (message: string, context: MethodContext) => {
738
+ const { controllerClass, filePath, httpMethod, method, sourceFile } = context
739
+ const ctx = {
740
+ controller: nodeName(controllerClass, sourceFile),
741
+ method: nodeName(method, sourceFile),
742
+ httpMethod,
743
+ filePath,
744
+ }
745
+ panic(`${message}\n${JSON.stringify(ctx, null, 2)}`)
746
+ }
747
+
748
+ // =============================================================================================================== //
749
+ // Types extraction (see src/analyzer/extractor.ts, src/analyzer/classdeps.ts) //
750
+ // =============================================================================================================== //
751
+
752
+ function analyzeClassDeps(
753
+ host: AnalysisHost,
754
+ decl: TsNode,
755
+ sourceFile: TsNode,
756
+ relativeFilePath: string,
757
+ absoluteSrcPath: string
758
+ ): ResolvedTypeDeps[] {
759
+ if (path.isAbsolute(relativeFilePath)) {
760
+ unreachable(
761
+ 'Internal error: got absolute file path in class dependencies analyzer, when expecting a relative one (got {magentaBright})',
762
+ relativeFilePath
763
+ )
764
+ }
765
+
766
+ const toLookup = new Array<TsType>()
767
+
768
+ for (const sup of getExtendsTypeNodes(host, decl)) {
769
+ toLookup.push(host.getTypeAtLocation(sup))
770
+ }
771
+
772
+ for (const prop of getDeclarationProperties(host, decl, sourceFile)) {
773
+ toLookup.push(host.getTypeAtLocation(prop))
774
+ }
775
+
776
+ return toLookup.map((type) => resolveTypeTextDependencies(host.typeToString(type), relativeFilePath, absoluteSrcPath))
777
+ }
778
+
779
+ /**
780
+ * Types extractor
781
+ */
782
+ class TypesExtractor {
783
+ constructor(
784
+ /** Analysis engine */
785
+ public readonly host: AnalysisHost,
786
+
787
+ /** Absolute source path */
788
+ public readonly absoluteSrcPath: string,
789
+
790
+ /** Magic types to replace non-portable types */
791
+ public readonly magicTypes: Array<MagicType>,
792
+
793
+ /** Extracted types */
794
+ public readonly extracted: TypesExtractorContent = new Map()
795
+ ) {}
796
+
797
+ /**
798
+ * Find if a type has previously been extracted, without providing its extension
799
+ * @param loc
800
+ * @returns
801
+ */
802
+ findExtractedTypeWithoutExt(loc: TypeLocation): ExtractedType | null {
803
+ for (const ext of MODULE_EXTENSIONS) {
804
+ const typ = this.extracted.get(loc.relativePathNoExt + ext)?.get(loc.typename)
805
+
806
+ if (typ) {
807
+ return typ
808
+ }
809
+ }
810
+
811
+ return null
812
+ }
813
+
814
+ /**
815
+ * Find the relative file location of a type
816
+ * @param loc
817
+ * @returns
818
+ */
819
+ findTypeRelativeFilePath(loc: TypeLocation): string | Error {
820
+ if (path.isAbsolute(loc.relativePathNoExt)) {
821
+ unreachable(
822
+ 'Internal error: got absolute file path in types extractor, when expecting a relative one (got {magentaBright})',
823
+ loc.relativePathNoExt
824
+ )
825
+ }
826
+
827
+ const absolutePathNoExt = path.resolve(this.absoluteSrcPath, loc.relativePathNoExt)
828
+
829
+ const cached = this.findExtractedTypeWithoutExt(loc)
830
+
831
+ if (cached) {
832
+ return cached.relativePath
833
+ }
834
+
835
+ let relativeFilePath: string | null = null
836
+
837
+ for (const ext of MODULE_EXTENSIONS) {
838
+ if (this.host.getSourceFile(absolutePathNoExt + ext)) {
839
+ relativeFilePath = loc.relativePathNoExt + ext
840
+ }
841
+ }
842
+
843
+ return (
844
+ relativeFilePath ??
845
+ new Error(
846
+ format('File {magenta} was not found (was expected to contain dependency type {yellow})', loc.relativePathNoExt, loc.typename)
847
+ )
848
+ )
849
+ }
850
+
851
+ /**
852
+ * Extract a type
853
+ * @param loc
854
+ * @param typesPath
855
+ * @returns
856
+ */
857
+ extractType(loc: TypeLocation, typesPath: string[] = []): ExtractedType | Error {
858
+ const { host } = this
859
+ const SK = host.SyntaxKind
860
+
861
+ if (path.isAbsolute(loc.relativePathNoExt)) {
862
+ unreachable(
863
+ 'Internal error: got absolute file path in types extractor, when expecting a relative one (got {magentaBright})',
864
+ loc.relativePathNoExt
865
+ )
866
+ }
867
+
868
+ // Get the absolute path of the type's parent file
869
+ // We don't know its extension yet as imported file names in import statements don't have an extension
870
+ const absolutePathNoExt = path.resolve(this.absoluteSrcPath, loc.relativePathNoExt)
871
+
872
+ // If the type is already in cache, return it directly
873
+ const cached = this.findExtractedTypeWithoutExt(loc)
874
+
875
+ if (cached) {
876
+ return cached
877
+ }
878
+
879
+ // Try to find the path with extension of the file and get it as a source file
880
+ // (like the legacy analyzer, the last matching extension wins)
881
+ let fileAndPath: [TsNode, string] | null = null
882
+
883
+ for (const ext of MODULE_EXTENSIONS) {
884
+ const sourceFile = host.getSourceFile(absolutePathNoExt + ext)
885
+
886
+ if (sourceFile) {
887
+ fileAndPath = [sourceFile, loc.relativePathNoExt + ext]
888
+ }
889
+ }
890
+
891
+ if (!fileAndPath) {
892
+ return new Error(
893
+ format('File {magenta} was not found (was expected to contain dependency type {yellow})', loc.relativePathNoExt, loc.typename)
894
+ )
895
+ }
896
+
897
+ const [file, relativeFilePath] = fileAndPath
898
+
899
+ // Use magic types to replace non-portable types
900
+ for (const magicType of this.magicTypes) {
901
+ if (relativeFilePath.endsWith(`node_modules/${magicType.nodeModuleFilePath}`) && loc.typename === magicType.typeName) {
902
+ debug(
903
+ '-> '.repeat(typesPath.length + 1) +
904
+ 'Found magic type {yellow} from external module file {magentaBright}, using provided placeholder.',
905
+ loc.typename,
906
+ relativeFilePath
907
+ )
908
+
909
+ const extracted: ExtractedType = {
910
+ content: '/** @file Magic placeholder from configuration file */\n\n' + magicType.placeholderContent,
911
+ relativePath: relativeFilePath,
912
+ relativePathNoExt: loc.relativePathNoExt,
913
+ typename: loc.typename,
914
+ typeParams: [],
915
+ dependencies: [],
916
+ }
917
+
918
+ typesPath.pop()
919
+
920
+ let types = this.extracted.get(relativeFilePath)
921
+
922
+ if (!types) {
923
+ types = new Map()
924
+ this.extracted.set(relativeFilePath, types)
925
+ }
926
+
927
+ types.set(loc.typename, extracted)
928
+
929
+ return extracted
930
+ }
931
+ }
932
+
933
+ debug('-> '.repeat(typesPath.length + 1) + 'Extracting type {yellow} from file {magentaBright}...', loc.typename, relativeFilePath)
934
+
935
+ // Analyze the type's declaration
936
+ const decl = (file.statements as TsNode[]).find((node) => {
937
+ return (
938
+ (node.kind === SK.EnumDeclaration ||
939
+ node.kind === SK.TypeAliasDeclaration ||
940
+ node.kind === SK.InterfaceDeclaration ||
941
+ node.kind === SK.ClassDeclaration ||
942
+ node.kind === SK.FunctionDeclaration) &&
943
+ nodeName(node, file) === loc.typename
944
+ )
945
+ })
946
+
947
+ if (!decl) {
948
+ return new Error(format(`Type {yellow} was not found in file {magenta}`, loc.typename, relativeFilePath))
949
+ }
950
+
951
+ // Handle a limitation of the tool: you can't import two types from two files at the same path with just two different extensions
952
+ // Example: importing a type named "User" from two files in the same directory called "user.entity.ts" and "user.entity.js"
953
+ const typ = this.findExtractedTypeWithoutExt(loc)
954
+
955
+ if (typ && typ.relativePath !== relativeFilePath) {
956
+ panic(
957
+ 'Found two conflicting files at same path but with different extensions:\n> {magentaBright}\n> {magentaBright}',
958
+ typ.relativePath,
959
+ relativeFilePath
960
+ )
961
+ }
962
+
963
+ /** Resolved type's dependencies */
964
+ let resolvedDeps: ResolvedTypeDeps[]
965
+
966
+ /** Type's parameters (e.g. <T>) */
967
+ let typeParams: string[]
968
+
969
+ /** Type declaration */
970
+ let extractedDecl = nodeText(decl, file)
971
+
972
+ // Handle enumerations
973
+ if (decl.kind === SK.EnumDeclaration) {
974
+ resolvedDeps = []
975
+ typeParams = []
976
+ }
977
+
978
+ // Handle type aliases
979
+ else if (decl.kind === SK.TypeAliasDeclaration) {
980
+ resolvedDeps = [resolveTypeTextDependencies(host.typeToString(host.getTypeAtLocation(decl)), relativeFilePath, this.absoluteSrcPath)]
981
+ typeParams = []
982
+ }
983
+
984
+ // Handle interfaces
985
+ else if (decl.kind === SK.InterfaceDeclaration) {
986
+ resolvedDeps = analyzeClassDeps(host, decl, file, relativeFilePath, this.absoluteSrcPath)
987
+ typeParams = (decl.typeParameters ?? []).map((tp: TsNode) => nodeText(tp, file))
988
+ }
989
+
990
+ // Handle classes
991
+ // Methods are not handled because they shouldn't be used as DTOs and won't be decodable from JSON in all cases
992
+ // This part is tricky as we remake the class from scratch using the informations we have on it, given we have to get rid
993
+ // of methods as well as removing decorators
994
+ else if (decl.kind === SK.ClassDeclaration) {
995
+ const classHead = nodeText(decl, file).match(/\b(export[^{]+class[^{]+{)/)
996
+
997
+ if (!classHead) {
998
+ unreachable('Internal error: failed to match class head in declaration: {yellow}', nodeText(decl, file))
999
+ }
1000
+
1001
+ extractedDecl = classHead[1]
1002
+
1003
+ const index = host.getIndexType(host.getTypeAtLocation(decl), 'string') ?? host.getIndexType(host.getTypeAtLocation(decl), 'number')
1004
+
1005
+ if (index) {
1006
+ extractedDecl += '\npublic [input: string | number]: ' + getImportResolvedTypeText(host.typeToString(index))
1007
+ }
1008
+
1009
+ // Export all members
1010
+ for (const member of getClassMembers(host, decl, file)) {
1011
+ if (member.kind !== SK.PropertyDeclaration) {
1012
+ warn('Found non-property member in class {cyan}: {magenta}', nodeName(decl, file) ?? '<anonymous>', nodeText(member, file))
1013
+ continue
1014
+ }
1015
+
1016
+ const memberType = host.getTypeAtLocation(member)
1017
+ const memberText = nodeText(member, file)
1018
+
1019
+ extractedDecl += `\npublic ${nodeName(member, file)}${
1020
+ memberText.includes('?') ? '?' : memberText.includes('!:') ? '!' : ''
1021
+ }: ${getImportResolvedTypeText(host.typeToString(memberType))};`
1022
+ }
1023
+
1024
+ extractedDecl += '\n}'
1025
+
1026
+ resolvedDeps = analyzeClassDeps(host, decl, file, relativeFilePath, this.absoluteSrcPath)
1027
+ typeParams = (decl.typeParameters ?? []).map((tp: TsNode) => nodeText(tp, file))
1028
+ }
1029
+
1030
+ // Handle functions
1031
+ else if (decl.kind === SK.FunctionDeclaration) {
1032
+ resolvedDeps = []
1033
+ typeParams = []
1034
+ }
1035
+
1036
+ // Handle unknown types
1037
+ else {
1038
+ panic('Unknown node type when extracting types: ' + decl.kind)
1039
+ }
1040
+
1041
+ /** Normalized dependencies */
1042
+ const dependencies: TypeLocationWithExt[] = []
1043
+
1044
+ // Ensure we're not stuck in an infinite loop where we analyze a type A, then its dependency B, which itself depends on A, and so on
1045
+ if (typesPath.includes(loc.typename)) {
1046
+ unreachable(`Internal error: infinite loop detected in types extracted: {yellow}`, typesPath.join(' -> '))
1047
+ }
1048
+
1049
+ typesPath.push(loc.typename)
1050
+
1051
+ // Analyze all dependencies
1052
+ for (const dependencyLoc of locateTypesFile(resolvedDeps)) {
1053
+ // If the "dependency" is one of the type's parameters (e.g. "T"), ignore this part
1054
+ if (typeParams.includes(dependencyLoc.typename)) {
1055
+ continue
1056
+ }
1057
+
1058
+ // Find the dependency's relative path
1059
+ let relativePath: string | Error
1060
+
1061
+ const cached = this.findExtractedTypeWithoutExt(dependencyLoc)
1062
+
1063
+ if (cached) {
1064
+ relativePath = cached.relativePath
1065
+ } else if (typesPath.includes(dependencyLoc.typename)) {
1066
+ relativePath = this.findTypeRelativeFilePath(dependencyLoc)
1067
+ } else {
1068
+ const extracted = this.extractType(dependencyLoc, typesPath)
1069
+ relativePath = extracted instanceof Error ? extracted : extracted.relativePath
1070
+ }
1071
+
1072
+ if (relativePath instanceof Error) {
1073
+ return new Error(
1074
+ format(
1075
+ '├─ Failed to extract type {yellow} due to an error in dependency type {yellow}\nfrom file {magenta} :\n{}',
1076
+ loc.typename,
1077
+ dependencyLoc.typename,
1078
+ relativeFilePath,
1079
+ relativePath.message.replace(/^/gm, ' ')
1080
+ )
1081
+ )
1082
+ }
1083
+
1084
+ // Update dependencies
1085
+ dependencies.push({ ...dependencyLoc, relativePath })
1086
+ }
1087
+
1088
+ const extracted: ExtractedType = {
1089
+ ...loc,
1090
+ relativePath: relativeFilePath,
1091
+ typeParams,
1092
+ content: extractedDecl,
1093
+ dependencies,
1094
+ }
1095
+
1096
+ typesPath.pop()
1097
+
1098
+ let types = this.extracted.get(relativeFilePath)
1099
+
1100
+ if (!types) {
1101
+ types = new Map()
1102
+ this.extracted.set(relativeFilePath, types)
1103
+ }
1104
+
1105
+ // Save the dependency
1106
+ types.set(loc.typename, extracted)
1107
+
1108
+ return extracted
1109
+ }
1110
+ }