@buildvision/nest-sdk-generator 1.1.3
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/.prettierrc +6 -0
- package/LICENSE.md +9 -0
- package/README.md +353 -0
- package/build/analyzer/builtin.d.ts +2 -0
- package/build/analyzer/builtin.js +11 -0
- package/build/analyzer/classdeps.d.ts +6 -0
- package/build/analyzer/classdeps.js +27 -0
- package/build/analyzer/controller.d.ts +26 -0
- package/build/analyzer/controller.js +103 -0
- package/build/analyzer/controllers.d.ts +7 -0
- package/build/analyzer/controllers.js +79 -0
- package/build/analyzer/decorator.d.ts +10 -0
- package/build/analyzer/decorator.js +29 -0
- package/build/analyzer/extractor.d.ts +107 -0
- package/build/analyzer/extractor.js +349 -0
- package/build/analyzer/index.d.ts +16 -0
- package/build/analyzer/index.js +101 -0
- package/build/analyzer/methods.d.ts +42 -0
- package/build/analyzer/methods.js +115 -0
- package/build/analyzer/module.d.ts +11 -0
- package/build/analyzer/module.js +41 -0
- package/build/analyzer/params.d.ts +38 -0
- package/build/analyzer/params.js +163 -0
- package/build/analyzer/route.d.ts +53 -0
- package/build/analyzer/route.js +131 -0
- package/build/analyzer/typedeps.d.ts +49 -0
- package/build/analyzer/typedeps.js +86 -0
- package/build/bin.d.ts +2 -0
- package/build/bin.js +39 -0
- package/build/config.d.ts +44 -0
- package/build/config.js +31 -0
- package/build/generator/genmodules.d.ts +24 -0
- package/build/generator/genmodules.js +134 -0
- package/build/generator/gentypes.d.ts +10 -0
- package/build/generator/gentypes.js +62 -0
- package/build/generator/index.d.ts +6 -0
- package/build/generator/index.js +83 -0
- package/build/generator/prettier.d.ts +16 -0
- package/build/generator/prettier.js +43 -0
- package/build/generator/sdk-interface.d.ts +1 -0
- package/build/generator/sdk-interface.js +36 -0
- package/build/logging.d.ts +6 -0
- package/build/logging.js +37 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/build/utils.d.ts +9 -0
- package/build/utils.js +56 -0
- package/package.json +34 -0
- package/src/analyzer/builtin.ts +9 -0
- package/src/analyzer/classdeps.ts +37 -0
- package/src/analyzer/controller.ts +135 -0
- package/src/analyzer/controllers.ts +117 -0
- package/src/analyzer/decorator.ts +29 -0
- package/src/analyzer/extractor.ts +491 -0
- package/src/analyzer/index.ts +136 -0
- package/src/analyzer/methods.ts +166 -0
- package/src/analyzer/module.ts +56 -0
- package/src/analyzer/params.ts +241 -0
- package/src/analyzer/route.ts +156 -0
- package/src/analyzer/typedeps.ts +130 -0
- package/src/bin.ts +33 -0
- package/src/config.ts +89 -0
- package/src/generator/genmodules.ts +163 -0
- package/src/generator/gentypes.ts +73 -0
- package/src/generator/index.ts +92 -0
- package/src/generator/prettier.ts +46 -0
- package/src/generator/sdk-interface.ts +32 -0
- package/src/logging.ts +35 -0
- package/src/utils.ts +61 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Analyzer for the source API's controllers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as os from 'os'
|
|
6
|
+
import * as path from 'path'
|
|
7
|
+
import { Project } from 'ts-morph'
|
|
8
|
+
import { debug, format, panic, warn } from '../logging'
|
|
9
|
+
import { findFileAbove } from '../utils'
|
|
10
|
+
import { analyzeController, SdkController } from './controller'
|
|
11
|
+
import { getModuleName } from './module'
|
|
12
|
+
|
|
13
|
+
export type SdkModules = Map<string, Map<string, SdkController>>
|
|
14
|
+
|
|
15
|
+
export function analyzeControllers(controllers: string[], absoluteSrcPath: string, project: Project): SdkModules {
|
|
16
|
+
/** Hierarchised SDK informations */
|
|
17
|
+
const collected = new Map<string, Map<string, SdkController>>()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Modules cache: contains for a given directory the nearest module file's path and name
|
|
21
|
+
* This allows to avoid having to analyze the whole directory structure for each controller
|
|
22
|
+
*/
|
|
23
|
+
const modulesCache = new Map<string, string>()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Path of the declared modules
|
|
27
|
+
* When a module is detected and put in the cache, its name is registered here along with its path
|
|
28
|
+
* This allows to ensure there is no name clash between two different modules
|
|
29
|
+
*/
|
|
30
|
+
const declaredModulesPath = new Map<string, string>()
|
|
31
|
+
|
|
32
|
+
debug(`Analyzing {yellow} controllers...`, controllers.length)
|
|
33
|
+
|
|
34
|
+
controllers.forEach((relativeControllerPath, i) => {
|
|
35
|
+
const absoluteControllerPath = path.resolve(absoluteSrcPath, relativeControllerPath)
|
|
36
|
+
|
|
37
|
+
debug(
|
|
38
|
+
'\n{cyan} {yellow}: {magentaBright} {cyan}\n',
|
|
39
|
+
'===== Analyzing controller',
|
|
40
|
+
`${i + 1}/${controllers.length}`,
|
|
41
|
+
relativeControllerPath,
|
|
42
|
+
'====='
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const basePath = path.dirname(absoluteControllerPath)
|
|
46
|
+
|
|
47
|
+
let moduleName = modulesCache.get(basePath)
|
|
48
|
+
|
|
49
|
+
// Check if the module's name is in cache
|
|
50
|
+
if (!moduleName) {
|
|
51
|
+
// Else, find the nearest module file
|
|
52
|
+
const absoluteModulePath = findFileAbove(/^.*\.module\.ts$/, path.resolve(absoluteSrcPath, basePath))
|
|
53
|
+
|
|
54
|
+
if (absoluteModulePath === null) {
|
|
55
|
+
panic('No module file was found for controller at path: {yellow}', absoluteControllerPath)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const relativeModulePath = path.relative(absoluteSrcPath, absoluteModulePath)
|
|
59
|
+
|
|
60
|
+
// Get the module's name
|
|
61
|
+
moduleName = getModuleName(project, relativeModulePath, absoluteSrcPath)
|
|
62
|
+
|
|
63
|
+
debug('Discovered module: {yellow}', moduleName)
|
|
64
|
+
|
|
65
|
+
// Ensure this module is unique
|
|
66
|
+
const cachedModulePath = declaredModulesPath.get(moduleName)
|
|
67
|
+
|
|
68
|
+
if (cachedModulePath) {
|
|
69
|
+
panic(
|
|
70
|
+
`Two modules were declared with the same name {yellow}:\n` + `- One in {yellow}\n` + `- One in {yellow}`,
|
|
71
|
+
moduleName,
|
|
72
|
+
cachedModulePath,
|
|
73
|
+
relativeModulePath
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
modulesCache.set(basePath, moduleName)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (moduleName in {}) {
|
|
81
|
+
panic(`Detected module whose name {yellow} collides with a JavaScript's native object property`, moduleName)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let moduleSdkInfos = collected.get(moduleName)
|
|
85
|
+
|
|
86
|
+
if (!moduleSdkInfos) {
|
|
87
|
+
moduleSdkInfos = new Map()
|
|
88
|
+
collected.set(moduleName, moduleSdkInfos)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (i === 0) {
|
|
92
|
+
if (process.platform === 'linux' && os.release().toLocaleLowerCase().includes('microsoft') && absoluteSrcPath.startsWith('/mnt/')) {
|
|
93
|
+
warn("NOTE: On WSL, the first type analysis on a project located in Windows's filesystem may take a long time to complete.")
|
|
94
|
+
warn('Please consider moving your project to WSL, or running this tool directly from Windows')
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const metadata = analyzeController(project, relativeControllerPath, absoluteSrcPath)
|
|
99
|
+
|
|
100
|
+
if (metadata instanceof Error) {
|
|
101
|
+
throw new Error(format('Failed to analyze controller at path {magenta}:\n{}', relativeControllerPath, metadata.message))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (metadata) {
|
|
105
|
+
if (metadata.registrationName in {}) {
|
|
106
|
+
panic(
|
|
107
|
+
`Detected controller whose registration name {yellow} collides with a JavaScript's native object property`,
|
|
108
|
+
metadata.registrationName
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
moduleSdkInfos.set(metadata.camelClassName, metadata)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return collected
|
|
117
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Utilities for analyzing the source API's decorators
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Decorator, Node } from 'ts-morph'
|
|
6
|
+
import { format } from '../logging'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Expect a decorator to have a single, string literal argument
|
|
10
|
+
* @param dec The decorator
|
|
11
|
+
* @returns Nothing if the decorator has no argument, a { lit: string } if the decorator has a string literal, an Error else
|
|
12
|
+
*/
|
|
13
|
+
export function expectSingleStrLitDecorator(dec: Decorator): string | null | Error {
|
|
14
|
+
const args = dec.getArguments()
|
|
15
|
+
|
|
16
|
+
if (args.length > 1) {
|
|
17
|
+
return new Error(`Multiple (${args.length}) arguments were provided to the decorator`)
|
|
18
|
+
} else if (args.length === 0) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const [arg] = args
|
|
23
|
+
|
|
24
|
+
if (!Node.isStringLiteral(arg)) {
|
|
25
|
+
return new Error(format('The argument provided to the decorator is not a string literal:\n>>> {cyan}', arg.getText()))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return arg.getLiteralText()
|
|
29
|
+
}
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Types extractor to use in the generated SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path'
|
|
6
|
+
import { Node, Project, SourceFile } from 'ts-morph'
|
|
7
|
+
import { MagicType } from '../config'
|
|
8
|
+
import { debug, format, panic, unreachable, warn } from '../logging'
|
|
9
|
+
import { analyzeClassDeps } from './classdeps'
|
|
10
|
+
import { SdkModules } from './controllers'
|
|
11
|
+
import { getImportResolvedType, ResolvedTypeDeps, resolveTypeDependencies } from './typedeps'
|
|
12
|
+
|
|
13
|
+
/** Valid extensions for TypeScript module files */
|
|
14
|
+
export const MODULE_EXTENSIONS = ['.ts', '.d.ts', '.tsx', '.d.tsx', '.js', '.jsx']
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Location of an imported type
|
|
18
|
+
*/
|
|
19
|
+
export interface TypeLocation {
|
|
20
|
+
readonly typename: string
|
|
21
|
+
readonly relativePathNoExt: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Location of an imported type after figuring out its extension
|
|
26
|
+
*/
|
|
27
|
+
export interface TypeLocationWithExt extends TypeLocation {
|
|
28
|
+
readonly relativePath: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extracted imported type
|
|
33
|
+
*/
|
|
34
|
+
export interface ExtractedType extends TypeLocationWithExt {
|
|
35
|
+
/** Type's declaration */
|
|
36
|
+
readonly content: string
|
|
37
|
+
|
|
38
|
+
/** Type parameters (e.g. <T>) */
|
|
39
|
+
readonly typeParams: string[]
|
|
40
|
+
|
|
41
|
+
/** Types this one depends on */
|
|
42
|
+
readonly dependencies: TypeLocationWithExt[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Maps files to records mapping themselves type names to their declaration code
|
|
46
|
+
export type TypesExtractorContent = Map<string, Map<string, ExtractedType>>
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Types extractor
|
|
50
|
+
*/
|
|
51
|
+
export class TypesExtractor {
|
|
52
|
+
constructor(
|
|
53
|
+
/** TS-Morph project */
|
|
54
|
+
public readonly project: Project,
|
|
55
|
+
|
|
56
|
+
/** Absolute source path */
|
|
57
|
+
public readonly absoluteSrcPath: string,
|
|
58
|
+
|
|
59
|
+
/** Magic types to replace non-portable types */
|
|
60
|
+
public readonly magicTypes: Array<MagicType>,
|
|
61
|
+
|
|
62
|
+
/** Extracted types */
|
|
63
|
+
public readonly extracted: TypesExtractorContent = new Map()
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a type has already been extracted
|
|
68
|
+
*/
|
|
69
|
+
hasExtractedType(loc: TypeLocationWithExt): boolean {
|
|
70
|
+
const files = this.extracted.get(loc.relativePath)
|
|
71
|
+
return files ? files.has(loc.typename) : false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get a type that was previously extracted
|
|
76
|
+
*/
|
|
77
|
+
getExtractedType(loc: TypeLocationWithExt): ExtractedType | null {
|
|
78
|
+
return this.extracted.get(loc.relativePath)?.get(loc.typename) ?? null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Find the extension of a TypeScript module
|
|
83
|
+
* @param loc
|
|
84
|
+
* @returns
|
|
85
|
+
*/
|
|
86
|
+
guessExtractedTypeModuleFileExt(loc: TypeLocation): string | null {
|
|
87
|
+
for (const ext of MODULE_EXTENSIONS) {
|
|
88
|
+
if (this.extracted.has(loc.relativePathNoExt + ext)) {
|
|
89
|
+
return loc.relativePathNoExt + ext
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Find if a type has previously been extracted, without providing its extension
|
|
98
|
+
* @param loc
|
|
99
|
+
* @returns
|
|
100
|
+
*/
|
|
101
|
+
findExtractedTypeWithoutExt(loc: TypeLocation): ExtractedType | null {
|
|
102
|
+
for (const ext of MODULE_EXTENSIONS) {
|
|
103
|
+
const typ = this.extracted.get(loc.relativePathNoExt + ext)?.get(loc.typename)
|
|
104
|
+
|
|
105
|
+
if (typ) {
|
|
106
|
+
return typ
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Memorize an extracted type so it can be reused later on
|
|
115
|
+
* @param loc
|
|
116
|
+
* @param extracted
|
|
117
|
+
*/
|
|
118
|
+
memorizeExtractedType(loc: TypeLocationWithExt, extracted: ExtractedType) {
|
|
119
|
+
let files = this.extracted.get(loc.relativePath)
|
|
120
|
+
|
|
121
|
+
if (!files) {
|
|
122
|
+
files = new Map()
|
|
123
|
+
this.extracted.set(loc.relativePath, files)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!files.has(loc.typename)) {
|
|
127
|
+
files.set(loc.typename, extracted)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find the relative file location of a type
|
|
133
|
+
* @param loc
|
|
134
|
+
* @returns
|
|
135
|
+
*/
|
|
136
|
+
findTypeRelativeFilePath(loc: TypeLocation): string | Error {
|
|
137
|
+
if (path.isAbsolute(loc.relativePathNoExt)) {
|
|
138
|
+
unreachable(
|
|
139
|
+
'Internal error: got absolute file path in types extractor, when expecting a relative one (got {magentaBright})',
|
|
140
|
+
loc.relativePathNoExt
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const absolutePathNoExt = path.resolve(this.absoluteSrcPath, loc.relativePathNoExt)
|
|
145
|
+
|
|
146
|
+
const cached = this.findExtractedTypeWithoutExt(loc)
|
|
147
|
+
|
|
148
|
+
if (cached) {
|
|
149
|
+
return cached.relativePath
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let relativeFilePath: string | null = null
|
|
153
|
+
|
|
154
|
+
for (const ext of MODULE_EXTENSIONS) {
|
|
155
|
+
try {
|
|
156
|
+
this.project.getSourceFileOrThrow(absolutePathNoExt + ext)
|
|
157
|
+
relativeFilePath = loc.relativePathNoExt + ext
|
|
158
|
+
} catch (e) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
relativeFilePath ??
|
|
165
|
+
new Error(
|
|
166
|
+
format('File {magenta} was not found (was expected to contain dependency type {yellow})', loc.relativePathNoExt, loc.typename)
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extract a type
|
|
173
|
+
* @param loc
|
|
174
|
+
* @param typesPath
|
|
175
|
+
* @returns
|
|
176
|
+
*/
|
|
177
|
+
extractType(loc: TypeLocation, typesPath: string[] = []): ExtractedType | Error {
|
|
178
|
+
if (path.isAbsolute(loc.relativePathNoExt)) {
|
|
179
|
+
unreachable(
|
|
180
|
+
'Internal error: got absolute file path in types extractor, when expecting a relative one (got {magentaBright})',
|
|
181
|
+
loc.relativePathNoExt
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Get the absolute path of the type's parent file
|
|
186
|
+
// We don't know its extension yet as imported file names in import statements don't have an extension
|
|
187
|
+
const absolutePathNoExt = path.resolve(this.absoluteSrcPath, loc.relativePathNoExt)
|
|
188
|
+
|
|
189
|
+
// If the type is already in cache, return it directly
|
|
190
|
+
const cached = this.findExtractedTypeWithoutExt(loc)
|
|
191
|
+
|
|
192
|
+
if (cached) {
|
|
193
|
+
return cached
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Try to find the path with extension of the file and get it as a TS-Morph file
|
|
197
|
+
let fileAndPath: [SourceFile, string] | null = null
|
|
198
|
+
|
|
199
|
+
for (const ext of MODULE_EXTENSIONS) {
|
|
200
|
+
try {
|
|
201
|
+
fileAndPath = [this.project.getSourceFileOrThrow(absolutePathNoExt + ext), loc.relativePathNoExt + ext]
|
|
202
|
+
} catch (e) {
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!fileAndPath) {
|
|
208
|
+
return new Error(
|
|
209
|
+
format('File {magenta} was not found (was expected to contain dependency type {yellow})', loc.relativePathNoExt, loc.typename)
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const [file, relativeFilePath] = fileAndPath
|
|
214
|
+
|
|
215
|
+
// Use magic types to replace non-portable types
|
|
216
|
+
for (const magicType of this.magicTypes) {
|
|
217
|
+
if (relativeFilePath.endsWith(`node_modules/${magicType.nodeModuleFilePath}`) && loc.typename === magicType.typeName) {
|
|
218
|
+
debug(
|
|
219
|
+
'-> '.repeat(typesPath.length + 1) +
|
|
220
|
+
'Found magic type {yellow} from external module file {magentaBright}, using provided placeholder.',
|
|
221
|
+
loc.typename,
|
|
222
|
+
relativeFilePath
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const extracted: ExtractedType = {
|
|
226
|
+
content: '/** @file Magic placeholder from configuration file */\n\n' + magicType.placeholderContent,
|
|
227
|
+
relativePath: relativeFilePath,
|
|
228
|
+
relativePathNoExt: loc.relativePathNoExt,
|
|
229
|
+
typename: loc.typename,
|
|
230
|
+
typeParams: [],
|
|
231
|
+
dependencies: [],
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
typesPath.pop()
|
|
235
|
+
|
|
236
|
+
let types = this.extracted.get(relativeFilePath)
|
|
237
|
+
|
|
238
|
+
if (!types) {
|
|
239
|
+
types = new Map()
|
|
240
|
+
this.extracted.set(relativeFilePath, types)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
types.set(loc.typename, extracted)
|
|
244
|
+
|
|
245
|
+
return extracted
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
debug('-> '.repeat(typesPath.length + 1) + 'Extracting type {yellow} from file {magentaBright}...', loc.typename, relativeFilePath)
|
|
250
|
+
|
|
251
|
+
// Analyze the type's declaration
|
|
252
|
+
const decl = file.forEachChildAsArray().find((node) => {
|
|
253
|
+
return (
|
|
254
|
+
(Node.isEnumDeclaration(node) ||
|
|
255
|
+
Node.isTypeAliasDeclaration(node) ||
|
|
256
|
+
Node.isInterfaceDeclaration(node) ||
|
|
257
|
+
Node.isClassDeclaration(node) ||
|
|
258
|
+
Node.isFunctionDeclaration(node)) &&
|
|
259
|
+
node.getName() === loc.typename
|
|
260
|
+
)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
if (!decl) {
|
|
264
|
+
return new Error(format(`Type {yellow} was not found in file {magenta}`, loc.typename, relativeFilePath))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Handle a limitation of the tool: you can't import two types from two files at the same path with just two different extensions
|
|
268
|
+
// Example: importing a type named "User" from two files in the same directory called "user.entity.ts" and "user.entity.js"
|
|
269
|
+
const typ = this.findExtractedTypeWithoutExt(loc)
|
|
270
|
+
|
|
271
|
+
if (typ && typ.relativePath !== relativeFilePath) {
|
|
272
|
+
panic(
|
|
273
|
+
'Found two conflicting files at same path but with different extensions:\n> {magentaBright}\n> {magentaBright}',
|
|
274
|
+
typ.relativePath,
|
|
275
|
+
relativeFilePath
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Resolved type's dependencies */
|
|
280
|
+
let resolvedDeps: ResolvedTypeDeps[]
|
|
281
|
+
|
|
282
|
+
/** Type's parameters (e.g. <T>) */
|
|
283
|
+
let typeParams: string[]
|
|
284
|
+
|
|
285
|
+
/** Type declaration */
|
|
286
|
+
let extractedDecl = decl.getText()
|
|
287
|
+
|
|
288
|
+
// Handle enumerations
|
|
289
|
+
if (Node.isEnumDeclaration(decl)) {
|
|
290
|
+
resolvedDeps = []
|
|
291
|
+
typeParams = []
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Handle type aliases
|
|
295
|
+
else if (Node.isTypeAliasDeclaration(decl)) {
|
|
296
|
+
resolvedDeps = [resolveTypeDependencies(decl.getType(), relativeFilePath, this.absoluteSrcPath)]
|
|
297
|
+
typeParams = []
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle interfaces
|
|
301
|
+
else if (Node.isInterfaceDeclaration(decl)) {
|
|
302
|
+
resolvedDeps = analyzeClassDeps(decl, relativeFilePath, this.absoluteSrcPath)
|
|
303
|
+
typeParams = decl.getTypeParameters().map((tp) => tp.getText())
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Handle classes
|
|
307
|
+
// Methods are not handled because they shouldn't be used as DTOs and won't be decodable from JSON in all cases
|
|
308
|
+
// This part is tricky as we remake the class from scratch using the informations we have on it, given we have to get rid
|
|
309
|
+
// of methods as well as removing decorators
|
|
310
|
+
else if (Node.isClassDeclaration(decl)) {
|
|
311
|
+
const classHead = decl.getText().match(/\b(export[^{]+class[^{]+{)/)
|
|
312
|
+
|
|
313
|
+
if (!classHead) {
|
|
314
|
+
unreachable('Internal error: failed to match class head in declaration: {yellow}', decl.getText())
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
extractedDecl = classHead[1]
|
|
318
|
+
|
|
319
|
+
const index = decl.getType().getStringIndexType() ?? decl.getType().getNumberIndexType()
|
|
320
|
+
|
|
321
|
+
if (index) {
|
|
322
|
+
extractedDecl += '\npublic [input: string | number]: ' + getImportResolvedType(index)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Export all members
|
|
326
|
+
for (const member of decl.getMembers()) {
|
|
327
|
+
if (!Node.isPropertyDeclaration(member)) {
|
|
328
|
+
warn('Found non-property member in class {cyan}: {magenta}', decl.getName() ?? '<anonymous>', member.getText())
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const memberType = member.getType()
|
|
333
|
+
|
|
334
|
+
extractedDecl += `\npublic ${member.getName()}${
|
|
335
|
+
member.getText().includes('?') ? '?' : member.getText().includes('!:') ? '!' : ''
|
|
336
|
+
}: ${getImportResolvedType(memberType)};`
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
extractedDecl += '\n}'
|
|
340
|
+
|
|
341
|
+
resolvedDeps = analyzeClassDeps(decl, relativeFilePath, this.absoluteSrcPath)
|
|
342
|
+
typeParams = decl.getTypeParameters().map((tp) => tp.getText())
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Handle functions
|
|
346
|
+
else if (Node.isFunctionDeclaration(decl)) {
|
|
347
|
+
resolvedDeps = []
|
|
348
|
+
typeParams = []
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Handle unknown types
|
|
352
|
+
else {
|
|
353
|
+
panic('Unknown node type when extracting types: ' + decl.getKindName())
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Normalized dependencies */
|
|
357
|
+
const dependencies: TypeLocationWithExt[] = []
|
|
358
|
+
|
|
359
|
+
// 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
|
|
360
|
+
if (typesPath.includes(loc.typename)) {
|
|
361
|
+
unreachable(`Internal error: infinite loop detected in types extracted: {yellow}`, typesPath.join(' -> '))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
typesPath.push(loc.typename)
|
|
365
|
+
|
|
366
|
+
// Analyze all dependencies
|
|
367
|
+
for (const dependencyLoc of locateTypesFile(resolvedDeps)) {
|
|
368
|
+
// If the "dependency" is one of the type's parameters (e.g. "T"), ignore this part
|
|
369
|
+
if (typeParams.includes(dependencyLoc.typename)) {
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Find the dependency's relative path
|
|
374
|
+
let relativePath: string | Error
|
|
375
|
+
|
|
376
|
+
const cached = this.findExtractedTypeWithoutExt(dependencyLoc)
|
|
377
|
+
|
|
378
|
+
if (cached) {
|
|
379
|
+
relativePath = cached.relativePath
|
|
380
|
+
} else if (typesPath.includes(dependencyLoc.typename)) {
|
|
381
|
+
relativePath = this.findTypeRelativeFilePath(dependencyLoc)
|
|
382
|
+
} else {
|
|
383
|
+
const extracted = this.extractType(dependencyLoc, typesPath)
|
|
384
|
+
relativePath = extracted instanceof Error ? extracted : extracted.relativePath
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (relativePath instanceof Error) {
|
|
388
|
+
return new Error(
|
|
389
|
+
format(
|
|
390
|
+
'├─ Failed to extract type {yellow} due to an error in dependency type {yellow}\nfrom file {magenta} :\n{}',
|
|
391
|
+
loc.typename,
|
|
392
|
+
dependencyLoc.typename,
|
|
393
|
+
relativeFilePath,
|
|
394
|
+
relativePath.message.replace(/^/gm, ' ')
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Update dependencies
|
|
400
|
+
dependencies.push({ ...dependencyLoc, relativePath })
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const extracted: ExtractedType = {
|
|
404
|
+
...loc,
|
|
405
|
+
relativePath: relativeFilePath,
|
|
406
|
+
typeParams,
|
|
407
|
+
content: extractedDecl,
|
|
408
|
+
dependencies,
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
typesPath.pop()
|
|
412
|
+
|
|
413
|
+
let types = this.extracted.get(relativeFilePath)
|
|
414
|
+
|
|
415
|
+
if (!types) {
|
|
416
|
+
types = new Map()
|
|
417
|
+
this.extracted.set(relativeFilePath, types)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Save the dependency
|
|
421
|
+
types.set(loc.typename, extracted)
|
|
422
|
+
|
|
423
|
+
return extracted
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Locate the files containing a list of a resolved types
|
|
429
|
+
* @param resolvedTypes
|
|
430
|
+
* @returns
|
|
431
|
+
*/
|
|
432
|
+
export function locateTypesFile(resolvedTypes: Array<ResolvedTypeDeps>): TypeLocation[] {
|
|
433
|
+
const out = new Array<TypeLocation>()
|
|
434
|
+
|
|
435
|
+
for (const resolved of resolvedTypes) {
|
|
436
|
+
for (const [file, types] of resolved.dependencies) {
|
|
437
|
+
for (const typename of types) {
|
|
438
|
+
if (!out.find((loc) => loc.typename === typename && loc.relativePathNoExt === file)) {
|
|
439
|
+
out.push({ typename, relativePathNoExt: file })
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
for (const typename of resolved.localTypes) {
|
|
445
|
+
if (!out.find((loc) => loc.typename === typename && loc.relativePathNoExt === resolved.relativeFilePath)) {
|
|
446
|
+
out.push({ typename, relativePathNoExt: resolved.relativeFilePath })
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return out
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Flatten a tree of resolved type dependencies
|
|
456
|
+
* @param sdkModules
|
|
457
|
+
* @returns
|
|
458
|
+
*/
|
|
459
|
+
export function flattenSdkResolvedTypes(sdkModules: SdkModules): ResolvedTypeDeps[] {
|
|
460
|
+
const flattened = new Array<ResolvedTypeDeps>()
|
|
461
|
+
|
|
462
|
+
for (const module of sdkModules.values()) {
|
|
463
|
+
for (const controller of module.values()) {
|
|
464
|
+
for (const method of controller.methods.values()) {
|
|
465
|
+
const { parameters: args, query, body } = method.params
|
|
466
|
+
|
|
467
|
+
flattened.push(method.returnType)
|
|
468
|
+
|
|
469
|
+
if (args) {
|
|
470
|
+
flattened.push(...args.values())
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (query) {
|
|
474
|
+
flattened.push(...query.values())
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (!body) {
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (body.full) {
|
|
482
|
+
flattened.push(body.type)
|
|
483
|
+
} else {
|
|
484
|
+
flattened.push(...body.fields.values())
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return flattened
|
|
491
|
+
}
|