@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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Analyzer for the source API's routes in controller methods
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A single URI part
|
|
7
|
+
*/
|
|
8
|
+
export type RoutePart = { readonly segment: string } | { readonly param: string }
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A parsed URI path
|
|
12
|
+
*/
|
|
13
|
+
export interface Route {
|
|
14
|
+
/** Is this an absolute route? (routes starting with a '/') */
|
|
15
|
+
readonly isRoot: boolean
|
|
16
|
+
|
|
17
|
+
/** The route's parts */
|
|
18
|
+
readonly parts: RoutePart[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Analyze an URI path
|
|
23
|
+
* @param uriPath The URI path to analyze
|
|
24
|
+
*/
|
|
25
|
+
export function analyzeUri(uriPath: string): Route | Error {
|
|
26
|
+
// Split the URI path into "parts"
|
|
27
|
+
const rawParts = uriPath.split('/')
|
|
28
|
+
|
|
29
|
+
// The parsed parts we'll return
|
|
30
|
+
const parts: RoutePart[] = []
|
|
31
|
+
|
|
32
|
+
// The current string offset in the URI path
|
|
33
|
+
// This variable is used for error display
|
|
34
|
+
let offset = 0
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Find if a specific part of the URI path contains an invalid character
|
|
38
|
+
* @param part The part to check
|
|
39
|
+
* @param pattern The invalid characters to look for, as a regular expression
|
|
40
|
+
* @returns An error message if an invalid character is found, nothing else
|
|
41
|
+
*/
|
|
42
|
+
function _findInvalid(part: string, pattern: RegExp): string | null {
|
|
43
|
+
return part.match(pattern) ? uriPath + '\n' + ' '.repeat(offset) + '^' : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Treat all parts of the URI path
|
|
47
|
+
for (let i = 0; i < rawParts.length; i++) {
|
|
48
|
+
const part = rawParts[i]
|
|
49
|
+
|
|
50
|
+
// Ignore empty parts (e.g. "/a///b" will be treated as "/a/b")
|
|
51
|
+
if (part === '') continue
|
|
52
|
+
|
|
53
|
+
// Ensure there is no generic character in the path as we don't support them
|
|
54
|
+
const genericErr = _findInvalid(part, /[\*\+\?]/)
|
|
55
|
+
|
|
56
|
+
if (genericErr) {
|
|
57
|
+
return new Error(
|
|
58
|
+
'Generic symbols (* + ?) are not supported as they prevent from determining the right route to use. Found in URI:\n' + genericErr
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check if this part is an URI parameter
|
|
63
|
+
if (part.startsWith(':')) {
|
|
64
|
+
// URI parameters must follow a strict naming
|
|
65
|
+
const paramErr = _findInvalid(part, /[^a-zA-Z0-9_:]/)
|
|
66
|
+
|
|
67
|
+
if (paramErr) {
|
|
68
|
+
return new Error('Invalid character detected in named parameter in URI:\n' + paramErr)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// We got a parameter
|
|
72
|
+
parts.push({ param: part.substr(1) })
|
|
73
|
+
} else {
|
|
74
|
+
// We got a literal part
|
|
75
|
+
parts.push({ segment: part })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Update the offset for error display
|
|
79
|
+
offset += part.length + 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Success!
|
|
83
|
+
return {
|
|
84
|
+
isRoot: uriPath.startsWith('/'),
|
|
85
|
+
parts,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the named parameters of a parsed route
|
|
91
|
+
* @param route
|
|
92
|
+
*/
|
|
93
|
+
export function paramsOfRoute(route: Route): string[] {
|
|
94
|
+
return route.parts.map((part) => ('param' in part ? part.param : null)).filter((e) => e !== null) as string[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Convert a route back to its original string
|
|
99
|
+
* @param route
|
|
100
|
+
*/
|
|
101
|
+
export function unparseRoute(route: Route): string {
|
|
102
|
+
return (route.isRoot ? '/' : '') + route.parts.map((part) => ('segment' in part ? part.segment : ':' + part.param)).join('/')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pretty-print a route
|
|
107
|
+
*/
|
|
108
|
+
export function debugUri(route: Route, color: (str: string) => string): string {
|
|
109
|
+
return (route.isRoot ? '/' : '') + route.parts.map((part) => ('segment' in part ? part.segment : color(':' + part.param))).join('/')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a route by providing its required parameters
|
|
114
|
+
* @param route
|
|
115
|
+
* @param params
|
|
116
|
+
*/
|
|
117
|
+
export function resolveRoute(route: Route, params: { [name: string]: string }): string | Error {
|
|
118
|
+
let uri: string[] = []
|
|
119
|
+
|
|
120
|
+
for (const part of route.parts) {
|
|
121
|
+
if ('segment' in part) {
|
|
122
|
+
uri.push(part.segment)
|
|
123
|
+
} else if (!params.hasOwnProperty(part.param)) {
|
|
124
|
+
return new Error('Missing route parameter ' + part.param)
|
|
125
|
+
} else {
|
|
126
|
+
uri.push(params[part.param])
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (route.isRoot ? '/' : '') + uri.join('/')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a route by providing its required parameters through a callback
|
|
135
|
+
* @param route
|
|
136
|
+
* @param paramsProvider
|
|
137
|
+
*/
|
|
138
|
+
export function resolveRouteWith(route: Route, paramsProvider: (param: string) => string | null): string | Error {
|
|
139
|
+
let uri: string[] = []
|
|
140
|
+
|
|
141
|
+
for (const part of route.parts) {
|
|
142
|
+
if ('segment' in part) {
|
|
143
|
+
uri.push(part.segment)
|
|
144
|
+
} else {
|
|
145
|
+
const param = paramsProvider(part.param)
|
|
146
|
+
|
|
147
|
+
if (param === null) {
|
|
148
|
+
return new Error('Missing route parameter ' + part.param)
|
|
149
|
+
} else {
|
|
150
|
+
uri.push(param)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return (route.isRoot ? '/' : '') + uri.join('/')
|
|
156
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Type dependencies builders from the source API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path'
|
|
6
|
+
import { ts, Type } from 'ts-morph'
|
|
7
|
+
import { unreachable } from '../logging'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regex to match or replace imported types
|
|
11
|
+
*/
|
|
12
|
+
export const IMPORTED_TYPE_REGEX = /\bimport\(['"]([^'"]+)['"]\)\.([a-zA-Z0-9_]+)\b/g
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Type with resolved level 1 dependencies (does not look for dependencies' own dependencies!)
|
|
16
|
+
*/
|
|
17
|
+
export interface ResolvedTypeDeps {
|
|
18
|
+
/** The original raw type, with import("...") expressions */
|
|
19
|
+
readonly rawType: string
|
|
20
|
+
|
|
21
|
+
/** The resolved type, without imports */
|
|
22
|
+
readonly resolvedType: string
|
|
23
|
+
|
|
24
|
+
/** File from which the type originates */
|
|
25
|
+
readonly relativeFilePath: string
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The type dependencies used by the resolved type
|
|
29
|
+
* A collection of scripts' relative paths mapped with the list of types imported from them
|
|
30
|
+
*/
|
|
31
|
+
readonly dependencies: Map<string, string[]>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Non-native types that are not imported
|
|
35
|
+
* They may be either local types (declared in the same file than the one analyzed) or globally-defined types
|
|
36
|
+
*/
|
|
37
|
+
readonly localTypes: string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the dependencies of a TS-Morph analyzed type
|
|
42
|
+
* @param type
|
|
43
|
+
* @param relativeFilePath
|
|
44
|
+
* @param absoluteSrcPath
|
|
45
|
+
* @returns
|
|
46
|
+
*/
|
|
47
|
+
export function resolveTypeDependencies(type: Type<ts.Type>, relativeFilePath: string, absoluteSrcPath: string): ResolvedTypeDeps {
|
|
48
|
+
/** Raw type's text (e.g. `Array<import("somefile.ts").SomeType>`) */
|
|
49
|
+
const rawType = type.getText()
|
|
50
|
+
|
|
51
|
+
if (path.isAbsolute(relativeFilePath)) {
|
|
52
|
+
unreachable(
|
|
53
|
+
'Internal error: got absolute file path in type dependencies resolver, when expecting a relative one (got {magentaBright})',
|
|
54
|
+
relativeFilePath
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let dependencies: ResolvedTypeDeps['dependencies'] = new Map()
|
|
59
|
+
let localTypes: ResolvedTypeDeps['localTypes'] = []
|
|
60
|
+
|
|
61
|
+
/** Resolved type (without import statements) */
|
|
62
|
+
const resolvedType: ResolvedTypeDeps['resolvedType'] = rawType.replace(IMPORTED_TYPE_REGEX, (_, matchedFilePath, type) => {
|
|
63
|
+
const filePath = path.isAbsolute(matchedFilePath) ? path.relative(absoluteSrcPath, matchedFilePath) : matchedFilePath
|
|
64
|
+
|
|
65
|
+
const deps = dependencies.get(filePath)
|
|
66
|
+
|
|
67
|
+
if (deps) {
|
|
68
|
+
if (!deps.includes(type)) {
|
|
69
|
+
deps.push(type)
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
dependencies.set(filePath, [type])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return type
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (resolvedType.includes('import(')) {
|
|
79
|
+
unreachable('Internal error: resolved still contains an {magenta} statement: {green}', 'import(...)', resolvedType)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const depFile of dependencies.keys()) {
|
|
83
|
+
if (path.isAbsolute(depFile)) {
|
|
84
|
+
unreachable(
|
|
85
|
+
'Internal error: resolved absolute file path in type dependencies, when should have resolved a relative one\nIn type: {yellow}\nGot: {magentaBright}',
|
|
86
|
+
type.getText(),
|
|
87
|
+
depFile
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
rawType,
|
|
94
|
+
relativeFilePath,
|
|
95
|
+
resolvedType,
|
|
96
|
+
dependencies,
|
|
97
|
+
localTypes,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extract an import type's name from a TS-Morph type
|
|
103
|
+
* @example "import('dir/file').TypeName" => "TypeName"
|
|
104
|
+
* @param type
|
|
105
|
+
* @returns
|
|
106
|
+
*/
|
|
107
|
+
export function getImportResolvedType(type: Type<ts.Type>): string {
|
|
108
|
+
return type.getText().replace(IMPORTED_TYPE_REGEX, (_, __, typename) => typename)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Convert paths for external files
|
|
113
|
+
* @param importedFilePath
|
|
114
|
+
*/
|
|
115
|
+
export function normalizeExternalFilePath(importedFilePath: string): string {
|
|
116
|
+
importedFilePath = path.normalize(importedFilePath)
|
|
117
|
+
|
|
118
|
+
if (!importedFilePath.startsWith('../')) {
|
|
119
|
+
return importedFilePath
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let level = 0
|
|
123
|
+
|
|
124
|
+
while (importedFilePath.startsWith('../')) {
|
|
125
|
+
level++
|
|
126
|
+
importedFilePath = importedFilePath.substr(3)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return `_external${level}/${importedFilePath}`
|
|
130
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { analyzerCli } from './analyzer'
|
|
5
|
+
import { config, configPath } from './config'
|
|
6
|
+
import generatorCli from './generator'
|
|
7
|
+
import { println } from './logging'
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
const started = Date.now()
|
|
11
|
+
|
|
12
|
+
process.chdir(path.dirname(path.resolve(configPath)))
|
|
13
|
+
|
|
14
|
+
switch (process.argv[3]) {
|
|
15
|
+
case '--analyze':
|
|
16
|
+
await analyzerCli(config)
|
|
17
|
+
break
|
|
18
|
+
|
|
19
|
+
case '--generate':
|
|
20
|
+
case undefined:
|
|
21
|
+
const sdkContent = await analyzerCli(config)
|
|
22
|
+
await generatorCli(config, sdkContent)
|
|
23
|
+
break
|
|
24
|
+
|
|
25
|
+
default:
|
|
26
|
+
console.error('ERROR: Unknown action provided (must be either "--analyze" or "--generate")')
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
println('{green}', '@ Done in ' + ((Date.now() - started) / 1000).toFixed(2) + 's')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main()
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chalk = require('chalk')
|
|
2
|
+
import * as fs from 'fs'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The configuration file's content
|
|
7
|
+
* For details on what these options do, see the project's README
|
|
8
|
+
*/
|
|
9
|
+
export interface Config {
|
|
10
|
+
/** Enable verbose mode */
|
|
11
|
+
readonly verbose?: boolean
|
|
12
|
+
|
|
13
|
+
/** Disable colored output */
|
|
14
|
+
readonly noColor?: boolean
|
|
15
|
+
|
|
16
|
+
/** Path to the API's source directory */
|
|
17
|
+
readonly apiInputPath: string
|
|
18
|
+
|
|
19
|
+
/** Path to generate the SDK at */
|
|
20
|
+
readonly sdkOutput: string
|
|
21
|
+
|
|
22
|
+
/** Path to the SDK interface file */
|
|
23
|
+
readonly sdkInterfacePath: string
|
|
24
|
+
|
|
25
|
+
/** List of magic types */
|
|
26
|
+
readonly magicTypes?: MagicType[]
|
|
27
|
+
|
|
28
|
+
/** Show a JSON output */
|
|
29
|
+
readonly jsonOutput?: string
|
|
30
|
+
|
|
31
|
+
/** Prettify the JSON output */
|
|
32
|
+
readonly jsonPrettyOutput?: boolean
|
|
33
|
+
|
|
34
|
+
/** Prettify the generated files (enabled by default) */
|
|
35
|
+
readonly prettify?: boolean
|
|
36
|
+
|
|
37
|
+
/** Path to Prettier's configuration file */
|
|
38
|
+
readonly prettierConfig?: string
|
|
39
|
+
|
|
40
|
+
/** Path to custom tsconfig file */
|
|
41
|
+
readonly tsconfigFile?: string
|
|
42
|
+
|
|
43
|
+
/** If the output directory already exists, overwrite it (enabled by default) */
|
|
44
|
+
readonly overwriteOldOutputDir?: boolean
|
|
45
|
+
|
|
46
|
+
/** If the SDK interface file does not exist yet, create one automatically (enabled by default) */
|
|
47
|
+
readonly generateDefaultSdkInterface?: boolean
|
|
48
|
+
|
|
49
|
+
/** Write generation timestamp in each TypeScript file (enabled by default) */
|
|
50
|
+
readonly generateTimestamps?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Magic type used to replace a non-compatible type in the generated SDK
|
|
55
|
+
*/
|
|
56
|
+
export interface MagicType {
|
|
57
|
+
readonly nodeModuleFilePath: string
|
|
58
|
+
readonly typeName: string
|
|
59
|
+
readonly placeholderContent: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load an existing configuration file and decode it
|
|
64
|
+
* @param configPath
|
|
65
|
+
*/
|
|
66
|
+
function loadConfigFile(configPath: string): Config {
|
|
67
|
+
if (!fs.existsSync(configPath)) {
|
|
68
|
+
console.error(chalk.red('Config file was not found at path: ' + chalk.yellow(path.resolve(configPath))))
|
|
69
|
+
process.exit(4)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const text = fs.readFileSync(configPath, 'utf8')
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(text)
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error(chalk.red('Failed to parse configuration file: ' + e))
|
|
78
|
+
process.exit(3)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const configPath = process.argv[2]
|
|
83
|
+
|
|
84
|
+
if (!configPath) {
|
|
85
|
+
console.error(chalk.red('Please provide a path to the configuration file'))
|
|
86
|
+
process.exit(2)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const config = loadConfigFile(configPath)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Generate SDK modules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path'
|
|
6
|
+
import { SdkModules } from '../analyzer/controllers'
|
|
7
|
+
import { SdkMethod } from '../analyzer/methods'
|
|
8
|
+
import { SdkMethodParams } from '../analyzer/params'
|
|
9
|
+
import { resolveRouteWith, unparseRoute } from '../analyzer/route'
|
|
10
|
+
import { normalizeExternalFilePath, ResolvedTypeDeps } from '../analyzer/typedeps'
|
|
11
|
+
import { panic } from '../logging'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate the SDK's module and controllers files
|
|
15
|
+
* @param modules
|
|
16
|
+
* @returns
|
|
17
|
+
*/
|
|
18
|
+
export function generateSdkModules(modules: SdkModules): Map<string, string> {
|
|
19
|
+
/** Generated module files */
|
|
20
|
+
const genFiles = new Map<string, string>()
|
|
21
|
+
|
|
22
|
+
// Iterate over each module
|
|
23
|
+
for (const [moduleName, controllers] of modules) {
|
|
24
|
+
// Iterate over each of the module's controllers
|
|
25
|
+
for (const [controllerName, controller] of controllers) {
|
|
26
|
+
/** Generated controller's content */
|
|
27
|
+
const out: string[] = []
|
|
28
|
+
|
|
29
|
+
out.push('/// Parent module: ' + moduleName)
|
|
30
|
+
out.push(`/// Controller: "${controllerName}" registered as "${controller.registrationName}" (${controller.methods.length} routes)`)
|
|
31
|
+
out.push('')
|
|
32
|
+
out.push('import { request } from "../central";')
|
|
33
|
+
|
|
34
|
+
const imports = new Map<string, string[]>()
|
|
35
|
+
|
|
36
|
+
const depsToImport = new Array<ResolvedTypeDeps>()
|
|
37
|
+
|
|
38
|
+
// Iterate over each controller
|
|
39
|
+
for (const method of controller.methods.values()) {
|
|
40
|
+
const { parameters: args, query, body } = method.params
|
|
41
|
+
|
|
42
|
+
depsToImport.push(method.returnType)
|
|
43
|
+
|
|
44
|
+
if (args) {
|
|
45
|
+
depsToImport.push(...args.values())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (query) {
|
|
49
|
+
depsToImport.push(...query.values())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (body) {
|
|
53
|
+
if (body.full) {
|
|
54
|
+
depsToImport.push(body.type)
|
|
55
|
+
} else {
|
|
56
|
+
depsToImport.push(...body.fields.values())
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build the imports list
|
|
62
|
+
for (const dep of depsToImport) {
|
|
63
|
+
for (const [file, types] of dep.dependencies) {
|
|
64
|
+
let imported = imports.get(file)
|
|
65
|
+
|
|
66
|
+
if (!imported) {
|
|
67
|
+
imported = []
|
|
68
|
+
imports.set(file, imported)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const typ of types) {
|
|
72
|
+
if (!imported.includes(typ)) {
|
|
73
|
+
imported.push(typ)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const [file, types] of imports) {
|
|
80
|
+
out.push(
|
|
81
|
+
`import type { ${types.join(', ')} } from "../_types/${normalizeExternalFilePath(file.replace(/\\/g, '/')).replace(/\\/g, '/')}";`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
out.push('')
|
|
86
|
+
out.push(`export default {`)
|
|
87
|
+
|
|
88
|
+
for (const method of controller.methods) {
|
|
89
|
+
const ret = method.returnType.resolvedType
|
|
90
|
+
const promised = ret.startsWith('Promise<') ? ret : `Promise<${ret}>`
|
|
91
|
+
|
|
92
|
+
out.push('')
|
|
93
|
+
out.push(` // ${method.httpMethod} @ ${unparseRoute(method.route)}`)
|
|
94
|
+
out.push(` ${method.name}(${generateSdkMethodParams(method.params)}): ${promised} {`)
|
|
95
|
+
out.push(generateCentralRequest(method).replace(/^/gm, ' '))
|
|
96
|
+
out.push(' },')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
out.push('')
|
|
100
|
+
out.push('};')
|
|
101
|
+
|
|
102
|
+
genFiles.set(path.join(moduleName, controller.camelClassName + '.ts'), out.join('\n'))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Generated module's content */
|
|
106
|
+
const moduleContent: string[] = []
|
|
107
|
+
|
|
108
|
+
moduleContent.push('/// Module name: ' + moduleName)
|
|
109
|
+
moduleContent.push('')
|
|
110
|
+
|
|
111
|
+
for (const controller of controllers.keys()) {
|
|
112
|
+
moduleContent.push(`export { default as ${controller} } from "./${controller}";`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Generate the SDK module file
|
|
116
|
+
genFiles.set(path.join(moduleName, 'index.ts'), moduleContent.join('\n'))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return genFiles
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Generate the method parameters for a given SDK method
|
|
124
|
+
* @param params
|
|
125
|
+
* @returns
|
|
126
|
+
*/
|
|
127
|
+
export function generateSdkMethodParams(params: SdkMethodParams): string {
|
|
128
|
+
// List of parameters (e.g. `id` in `/get/:id`, analyzed from the usages of the `@Param` decorator)
|
|
129
|
+
const parameters = params.parameters ? [...params.parameters].map(([name, type]) => `${name}: ${type.resolvedType}`) : []
|
|
130
|
+
|
|
131
|
+
// List of query values (e.g. `id` in `?id=xxx`, analyzed from the usages of the `@Query` decorator)
|
|
132
|
+
const query = params.query ? [...params.query].map(([name, type]) => `${name}: ${type.resolvedType}`) : []
|
|
133
|
+
|
|
134
|
+
// Body's content (type used with the `@Body` decorator)
|
|
135
|
+
const body = params.body
|
|
136
|
+
? params.body.full
|
|
137
|
+
? params.body.type.resolvedType
|
|
138
|
+
: '{ ' + [...params.body.fields].map(([name, type]) => `${name}: ${type.resolvedType}`).join(', ') + ' }'
|
|
139
|
+
: null
|
|
140
|
+
|
|
141
|
+
// The ternary conditions below are made to eclipse useless parameters
|
|
142
|
+
// For instance, if we're not expecting any query nor body, these two parameters can be omitted when calling the method
|
|
143
|
+
return [
|
|
144
|
+
`params: {${' ' + parameters.join(', ') + ' '}}${parameters.length === 0 && !body && query.length === 0 ? ' = {}' : ''}`,
|
|
145
|
+
`body: ${body ?? '{}'}${!body && query.length === 0 ? ' = {}' : ''}`,
|
|
146
|
+
`query: {${' ' + query.join(', ') + ' '}}${query.length === 0 ? ' = {}' : ''}`,
|
|
147
|
+
].join(', ')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate a request call to Central for the generated files
|
|
152
|
+
* @param method
|
|
153
|
+
* @returns
|
|
154
|
+
*/
|
|
155
|
+
export function generateCentralRequest(method: SdkMethod): string {
|
|
156
|
+
const resolvedRoute = resolveRouteWith(method.route, (param) => '${params.' + param + '}')
|
|
157
|
+
|
|
158
|
+
if (resolvedRoute instanceof Error) {
|
|
159
|
+
panic('Internal error: failed to resolve route: ' + resolvedRoute.message)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return `return request('${method.httpMethod}', \`${resolvedRoute}\`, body, query) as any`
|
|
163
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Generate type files for the SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path'
|
|
6
|
+
import { TypesExtractorContent } from '../analyzer/extractor'
|
|
7
|
+
import { normalizeExternalFilePath } from '../analyzer/typedeps'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate the non-formatted type files which will be used by the SDK's route functions
|
|
11
|
+
* @param sdkTypes
|
|
12
|
+
* @returns
|
|
13
|
+
*/
|
|
14
|
+
export function generateSdkTypeFiles(sdkTypes: TypesExtractorContent): Map<string, string> {
|
|
15
|
+
/** Generated type files */
|
|
16
|
+
const genFiles = new Map<string, string>()
|
|
17
|
+
|
|
18
|
+
// While analyzing the controllers' content, all types used by their methods have been extracted
|
|
19
|
+
// The list of these types is then provided to this function as the `sdkTypes` argument, allowing us
|
|
20
|
+
// to establish the list of all dependencies.
|
|
21
|
+
for (const [file, types] of sdkTypes) {
|
|
22
|
+
const out = []
|
|
23
|
+
|
|
24
|
+
/** List of imports in the current file */
|
|
25
|
+
const imports = new Map<string, string[]>()
|
|
26
|
+
|
|
27
|
+
// Iterate over all types in the current `file`
|
|
28
|
+
for (const extracted of types.values()) {
|
|
29
|
+
// Iterate over all of the extracted type's dependencies
|
|
30
|
+
for (const dep of extracted.dependencies) {
|
|
31
|
+
// If the dependency is from the same file, then we have nothing to do
|
|
32
|
+
if (dep.relativePath === file) {
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Push the typename to the list of imports
|
|
37
|
+
let imported = imports.get(dep.relativePathNoExt)
|
|
38
|
+
|
|
39
|
+
if (!imported) {
|
|
40
|
+
imported = [dep.typename]
|
|
41
|
+
imports.set(dep.relativePathNoExt, imported)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!imported.includes(dep.typename)) {
|
|
45
|
+
imported.push(dep.typename)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate an import statement for each imported type
|
|
51
|
+
out.push(
|
|
52
|
+
[...imports]
|
|
53
|
+
.map(([depFile, types]) => {
|
|
54
|
+
let depPath = path.relative(path.dirname(file), normalizeExternalFilePath(depFile)).replace(/\\/g, '/')
|
|
55
|
+
if (!depPath.includes('/')) depPath = './' + depPath
|
|
56
|
+
return `import type { ${types.join(', ')} } from "${
|
|
57
|
+
depPath.startsWith('./') || depPath.startsWith('../') ? depPath : './' + depPath
|
|
58
|
+
}"`
|
|
59
|
+
})
|
|
60
|
+
.join('\n')
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Add the extracted types' declaration, indented
|
|
64
|
+
for (const extracted of types.values()) {
|
|
65
|
+
out.push(extracted.content.replace(/^/gm, ' '))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Generate the type file
|
|
69
|
+
genFiles.set(file, out.join('\n'))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return genFiles
|
|
73
|
+
}
|