@vercube/scan 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.d.mts +338 -0
- package/dist/index.mjs +514 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present - Vercube
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @vercube/scan
|
|
2
|
+
|
|
3
|
+
Shared source-scanning and AST-extraction utilities for the [Vercube](https://github.com/vercube/vercube) framework. It discovers `@Controller` routes, `@Injectable` services and `BaseMiddleware` subclasses by parsing TypeScript/JavaScript with [`oxc-parser`](https://www.npmjs.com/package/oxc-parser).
|
|
4
|
+
|
|
5
|
+
This package is the single source of truth used by both the [`@vercube/nitro`](../nitro) and [`@vercube/vite`](../vite) integrations. You usually don't depend on it directly.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @vercube/scan
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
### Pure extractors
|
|
16
|
+
|
|
17
|
+
Operate on a source string and return discovered classes (with placeholder import specifiers):
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { extractRoutes, extractServices, extractMiddlewares } from '@vercube/scan';
|
|
21
|
+
|
|
22
|
+
extractRoutes(code); // RouteInfo[]
|
|
23
|
+
extractServices(code); // ServiceInfo[]
|
|
24
|
+
extractMiddlewares(code); // MiddlewareInfo[]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Scanning
|
|
28
|
+
|
|
29
|
+
`scanFiles` / `scanDir` walk directories (via [`tinyglobby`](https://www.npmjs.com/package/tinyglobby)); the higher-level helpers read files, extract, and resolve each import to its real path:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { getRoutes, getServices, getMiddlewares, scanProject, scanSource } from '@vercube/scan';
|
|
33
|
+
|
|
34
|
+
// Nitro-style: dedicated api/routes/services subdirectories
|
|
35
|
+
await scanProject({ baseDirs: ['/abs/src'] });
|
|
36
|
+
|
|
37
|
+
// Vercube-style: walk the whole tree, classes can live anywhere
|
|
38
|
+
await scanSource({ dirs: ['/abs/src'] });
|
|
39
|
+
// → { routes, services, middlewares } with imports resolved to real paths
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`scanSource` reads each file once and applies all three extractors, deduplicating services that are already registered as routes.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
//#region src/Ast.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Unwraps export declarations to retrieve the underlying class node,
|
|
4
|
+
* along with metadata about the export style.
|
|
5
|
+
*
|
|
6
|
+
* Handles the following AST patterns:
|
|
7
|
+
* - `ExportDefaultDeclaration` wrapping a `ClassDeclaration` → `isDefault: true`
|
|
8
|
+
* - `ExportNamedDeclaration` wrapping a `ClassDeclaration` → `isDefault: false`
|
|
9
|
+
* - Direct `ClassDeclaration` or `Class` nodes → `isDefault: false`
|
|
10
|
+
*
|
|
11
|
+
* @param node - An AST node from the program body to inspect.
|
|
12
|
+
* @returns An object containing the class AST node and whether it is a default export, or `null` if not a class.
|
|
13
|
+
*/
|
|
14
|
+
declare function getClassNode(node: any): {
|
|
15
|
+
classNode: any;
|
|
16
|
+
isDefault: boolean;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Builds an import statement for a class, choosing default vs named syntax.
|
|
20
|
+
*
|
|
21
|
+
* @param className - The class identifier to import.
|
|
22
|
+
* @param source - The module specifier to import from.
|
|
23
|
+
* @param isDefault - Whether the class is a default export.
|
|
24
|
+
* @returns The generated import statement string.
|
|
25
|
+
*/
|
|
26
|
+
declare function buildImport(className: string, source: string, isDefault: boolean): string;
|
|
27
|
+
/**
|
|
28
|
+
* Searches a list of decorators for a `CallExpression` matching the given name
|
|
29
|
+
* and extracts its first string argument.
|
|
30
|
+
*
|
|
31
|
+
* Used to retrieve the path argument from decorators like `@Controller('/api/foo')`.
|
|
32
|
+
* If the decorator is found but has no string argument, an empty string is returned.
|
|
33
|
+
* If no matching decorator is found, `null` is returned.
|
|
34
|
+
*
|
|
35
|
+
* @param decorators - The array of decorator AST nodes to search, or `undefined` if none exist.
|
|
36
|
+
* @param name - The decorator function name to match against (e.g. 'Controller').
|
|
37
|
+
* @returns The string argument value, an empty string if no argument is provided, or `null` if the decorator is not found.
|
|
38
|
+
*/
|
|
39
|
+
declare function extractDecoratorArg(decorators: any[] | undefined, name: string): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if the given decorators array contains a decorator matching `name`.
|
|
42
|
+
* Supports both `@Name` identifier and `@Name()` call expression styles.
|
|
43
|
+
*
|
|
44
|
+
* @param decorators - The array of decorator AST nodes to search.
|
|
45
|
+
* @param name - The decorator name to match.
|
|
46
|
+
* @returns Whether a matching decorator exists.
|
|
47
|
+
*/
|
|
48
|
+
declare function hasDecorator(decorators: any[] | undefined, name: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Returns true if the class node extends a superclass with the given name.
|
|
51
|
+
*
|
|
52
|
+
* @param classNode - The class AST node to inspect.
|
|
53
|
+
* @param name - The expected superclass identifier name.
|
|
54
|
+
* @returns Whether the class extends the named superclass.
|
|
55
|
+
*/
|
|
56
|
+
declare function extendsSuperClass(classNode: any, name: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Normalizes a route path by removing duplicate slashes and ensuring
|
|
59
|
+
* the path starts with a single leading slash.
|
|
60
|
+
*
|
|
61
|
+
* @param path - The raw concatenated path string to normalize.
|
|
62
|
+
* @returns A cleaned path string with a leading slash and no duplicate separators.
|
|
63
|
+
*/
|
|
64
|
+
declare function normalizePath(path: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Extracts named route parameters from a path string.
|
|
67
|
+
*
|
|
68
|
+
* Scans the path for segments prefixed with ':' and returns an array of
|
|
69
|
+
* parameter names with the ':' prefix stripped.
|
|
70
|
+
*
|
|
71
|
+
* @param route - The normalized route path to scan for parameters.
|
|
72
|
+
* @returns An array of parameter name strings, empty if none are found.
|
|
73
|
+
*/
|
|
74
|
+
declare function extractParams(route: string): string[];
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/Types.d.ts
|
|
77
|
+
/**
|
|
78
|
+
* Basic information about a scanned source file.
|
|
79
|
+
*/
|
|
80
|
+
type FileInfo = {
|
|
81
|
+
/** The path of the file relative to the directory it was scanned from. */path: string; /** The absolute path of the file on disk. */
|
|
82
|
+
fullPath: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Represents a single extracted route from a controller class.
|
|
86
|
+
*/
|
|
87
|
+
interface RouteInfo extends FileInfo {
|
|
88
|
+
/** The import statement required to load the controller class. */
|
|
89
|
+
import: string;
|
|
90
|
+
/** The name of the import statement. */
|
|
91
|
+
importClassName: string;
|
|
92
|
+
/** The full normalized route path, including the base controller path and method path. */
|
|
93
|
+
route: string;
|
|
94
|
+
/** The HTTP method in uppercase (e.g. 'GET', 'POST', 'PUT'). */
|
|
95
|
+
method: string;
|
|
96
|
+
/** Array of route parameter names extracted from path segments prefixed with ':'. */
|
|
97
|
+
params: string[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Represents a single injectable service class discovered during scanning.
|
|
101
|
+
*/
|
|
102
|
+
interface ServiceInfo extends FileInfo {
|
|
103
|
+
/** The import statement required to load the service class. */
|
|
104
|
+
import: string;
|
|
105
|
+
/** The name of the imported class. */
|
|
106
|
+
importClassName: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Represents a single middleware class discovered during scanning.
|
|
110
|
+
*/
|
|
111
|
+
interface MiddlewareInfo extends FileInfo {
|
|
112
|
+
/** The import statement required to load the middleware class. */
|
|
113
|
+
import: string;
|
|
114
|
+
/** The name of the imported class. */
|
|
115
|
+
importClassName: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Minimal logger interface used to report non-fatal scanning issues.
|
|
119
|
+
*/
|
|
120
|
+
interface ScanLogger {
|
|
121
|
+
warn(message: string): void;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/Extract.d.ts
|
|
125
|
+
/**
|
|
126
|
+
* Placeholder module specifier embedded in generated route imports.
|
|
127
|
+
* Callers resolve it to the real file path once known (see {@link Transform}).
|
|
128
|
+
*/
|
|
129
|
+
declare const IMPORT_SOURCE = "#internal/vercube-route-plugin";
|
|
130
|
+
/**
|
|
131
|
+
* Placeholder module specifier embedded in generated service imports.
|
|
132
|
+
*/
|
|
133
|
+
declare const SERVICE_IMPORT_SOURCE = "#internal/vercube-service-source";
|
|
134
|
+
/**
|
|
135
|
+
* Placeholder module specifier embedded in generated middleware imports.
|
|
136
|
+
*/
|
|
137
|
+
declare const MIDDLEWARE_IMPORT_SOURCE = "#internal/vercube-middleware-source";
|
|
138
|
+
/**
|
|
139
|
+
* Extracts all route definitions from the given TypeScript/JavaScript source code.
|
|
140
|
+
*
|
|
141
|
+
* Parses the source using `oxc-parser` and traverses the resulting AST to find
|
|
142
|
+
* classes decorated with `@Controller(path)`. For each such class, it inspects
|
|
143
|
+
* method definitions for HTTP method decorators (`@Get`, `@Post`, `@Put`, `@Delete`,
|
|
144
|
+
* `@Patch`, `@Options`, `@Head`) and constructs full route paths by concatenating
|
|
145
|
+
* the controller base path with the method-level path.
|
|
146
|
+
*
|
|
147
|
+
* @param code - The raw TypeScript or JavaScript source code string to analyze.
|
|
148
|
+
* @returns An array of {@link RouteInfo} objects representing all discovered routes.
|
|
149
|
+
*/
|
|
150
|
+
declare function extractRoutes(code: string): RouteInfo[];
|
|
151
|
+
/**
|
|
152
|
+
* Extracts every `@Controller`-decorated class from the given source code,
|
|
153
|
+
* regardless of whether it declares HTTP method routes.
|
|
154
|
+
*
|
|
155
|
+
* Unlike {@link extractRoutes} (which emits one entry per HTTP route method),
|
|
156
|
+
* this returns one entry per controller class — so controllers that only carry
|
|
157
|
+
* non-HTTP handlers (e.g. WebSocket `@Message` methods) are still discovered for
|
|
158
|
+
* binding into the DI container.
|
|
159
|
+
*
|
|
160
|
+
* @param code - The raw TypeScript or JavaScript source code string to analyze.
|
|
161
|
+
* @returns An array of {@link ServiceInfo} objects for all discovered controller classes.
|
|
162
|
+
*/
|
|
163
|
+
declare function extractControllers(code: string): ServiceInfo[];
|
|
164
|
+
/**
|
|
165
|
+
* Extracts all `@Injectable`-decorated class definitions from the given source code.
|
|
166
|
+
*
|
|
167
|
+
* @param code - The raw TypeScript or JavaScript source code string to analyze.
|
|
168
|
+
* @returns An array of {@link ServiceInfo} objects for all discovered injectable classes.
|
|
169
|
+
*/
|
|
170
|
+
declare function extractServices(code: string): ServiceInfo[];
|
|
171
|
+
/**
|
|
172
|
+
* Extracts all class definitions that extend `BaseMiddleware` from the given source code.
|
|
173
|
+
*
|
|
174
|
+
* @param code - The raw TypeScript or JavaScript source code string to analyze.
|
|
175
|
+
* @returns An array of {@link MiddlewareInfo} objects for all discovered middleware classes.
|
|
176
|
+
*/
|
|
177
|
+
declare function extractMiddlewares(code: string): MiddlewareInfo[];
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/Project.d.ts
|
|
180
|
+
/**
|
|
181
|
+
* Options describing where to scan for controllers, services and middleware.
|
|
182
|
+
*
|
|
183
|
+
* `baseDirs` are the project source roots (e.g. `src/`). The remaining options
|
|
184
|
+
* name the subdirectories scanned within each base root.
|
|
185
|
+
*/
|
|
186
|
+
interface ScanProjectOptions {
|
|
187
|
+
/** Project source roots to scan within. */
|
|
188
|
+
baseDirs: string[];
|
|
189
|
+
/** Subdirectory holding API controllers. Defaults to `api`. */
|
|
190
|
+
apiDir?: string;
|
|
191
|
+
/** Subdirectory holding route controllers. Defaults to `routes`. */
|
|
192
|
+
routesDir?: string;
|
|
193
|
+
/** Subdirectories scanned for `@Injectable` services. Defaults to `['api', 'routes', 'services', 'repositories']`. */
|
|
194
|
+
serviceDirs?: string[];
|
|
195
|
+
/** Subdirectory holding middleware classes. Defaults to `middleware`. */
|
|
196
|
+
middlewareDir?: string;
|
|
197
|
+
/** Optional logger forwarded to the underlying scanner. */
|
|
198
|
+
logger?: ScanLogger;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Scans the API and routes directories for controllers and returns every route
|
|
202
|
+
* with its `import` resolved to the controller's real file path.
|
|
203
|
+
*
|
|
204
|
+
* @param options - Where to scan. See {@link ScanProjectOptions}.
|
|
205
|
+
* @returns The discovered routes with resolved imports.
|
|
206
|
+
*/
|
|
207
|
+
declare function getRoutes(options: ScanProjectOptions): Promise<RouteInfo[]>;
|
|
208
|
+
/**
|
|
209
|
+
* Scans the configured service directories for `@Injectable` classes and returns
|
|
210
|
+
* every service with its `import` resolved to the class's real file path.
|
|
211
|
+
*
|
|
212
|
+
* @param options - Where to scan. See {@link ScanProjectOptions}.
|
|
213
|
+
* @returns The discovered services with resolved imports.
|
|
214
|
+
*/
|
|
215
|
+
declare function getServices(options: ScanProjectOptions): Promise<ServiceInfo[]>;
|
|
216
|
+
/**
|
|
217
|
+
* Scans the middleware directory for `BaseMiddleware` subclasses and returns
|
|
218
|
+
* every middleware with its `import` resolved to the class's real file path.
|
|
219
|
+
*
|
|
220
|
+
* @param options - Where to scan. See {@link ScanProjectOptions}.
|
|
221
|
+
* @returns The discovered middleware with resolved imports.
|
|
222
|
+
*/
|
|
223
|
+
declare function getMiddlewares(options: ScanProjectOptions): Promise<MiddlewareInfo[]>;
|
|
224
|
+
/**
|
|
225
|
+
* Convenience wrapper that scans routes, services and middleware in one call.
|
|
226
|
+
*
|
|
227
|
+
* Services whose class name is already registered as a route are filtered out
|
|
228
|
+
* so a controller is never imported twice.
|
|
229
|
+
*
|
|
230
|
+
* @param options - Where to scan. See {@link ScanProjectOptions}.
|
|
231
|
+
* @returns The discovered routes, deduplicated services, and middleware.
|
|
232
|
+
*/
|
|
233
|
+
declare function scanProject(options: ScanProjectOptions): Promise<{
|
|
234
|
+
routes: RouteInfo[];
|
|
235
|
+
services: ServiceInfo[];
|
|
236
|
+
middlewares: MiddlewareInfo[];
|
|
237
|
+
}>;
|
|
238
|
+
/**
|
|
239
|
+
* Options for {@link scanSource}.
|
|
240
|
+
*/
|
|
241
|
+
interface ScanSourceOptions {
|
|
242
|
+
/** Directories whose entire file tree is scanned for decorated classes. */
|
|
243
|
+
dirs: string[];
|
|
244
|
+
/** Optional logger forwarded to the underlying scanner. */
|
|
245
|
+
logger?: ScanLogger;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Recursively scans the given directories for `@Controller` classes,
|
|
249
|
+
* `@Injectable` services and `BaseMiddleware` subclasses, reading each file only
|
|
250
|
+
* once.
|
|
251
|
+
*
|
|
252
|
+
* Unlike {@link scanProject}, this does not assume the Nitro convention of
|
|
253
|
+
* dedicated `api`/`routes`/`services` subdirectories — decorated classes are
|
|
254
|
+
* discovered wherever they live in the tree, which matches how Vercube apps are
|
|
255
|
+
* typically organised. **Every** `@Controller` class is returned for binding
|
|
256
|
+
* (not just those with HTTP routes), so WebSocket-only controllers are bound
|
|
257
|
+
* too, while `routes` carries the concrete HTTP routes (method + path) for
|
|
258
|
+
* request matching. Services already discovered as controllers are filtered out
|
|
259
|
+
* so a class is never imported twice.
|
|
260
|
+
*
|
|
261
|
+
* @param options - Directories to scan. See {@link ScanSourceOptions}.
|
|
262
|
+
* @returns The discovered controllers, HTTP routes, deduplicated services, and middleware, all with resolved imports.
|
|
263
|
+
*/
|
|
264
|
+
declare function scanSource(options: ScanSourceOptions): Promise<{
|
|
265
|
+
controllers: ServiceInfo[];
|
|
266
|
+
routes: RouteInfo[];
|
|
267
|
+
services: ServiceInfo[];
|
|
268
|
+
middlewares: MiddlewareInfo[];
|
|
269
|
+
}>;
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/Scan.d.ts
|
|
272
|
+
/**
|
|
273
|
+
* Glob pattern matching every source file extension the scanner understands.
|
|
274
|
+
*/
|
|
275
|
+
declare const GLOB_SCAN_PATTERN = "**/*.{js,mjs,cjs,ts,mts,cts,tsx,jsx}";
|
|
276
|
+
/**
|
|
277
|
+
* Scans a single base directory for files inside the named subdirectory.
|
|
278
|
+
*
|
|
279
|
+
* Globs `<name>/**` relative to `dir`, returning each match as a {@link FileInfo}
|
|
280
|
+
* with its absolute path and the path relative to `<dir>/<name>`. Results are
|
|
281
|
+
* sorted by relative path for deterministic output.
|
|
282
|
+
*
|
|
283
|
+
* @param dir - The base directory to scan within.
|
|
284
|
+
* @param name - The subdirectory (e.g. 'api', 'routes', 'services') to look in.
|
|
285
|
+
* @param logger - Optional logger used to warn when `<dir>/<name>` is not a directory.
|
|
286
|
+
* @returns A sorted list of discovered files.
|
|
287
|
+
*/
|
|
288
|
+
declare function scanDir(dir: string, name: string, logger?: ScanLogger): Promise<FileInfo[]>;
|
|
289
|
+
/**
|
|
290
|
+
* Scans every base directory for files inside the named subdirectory.
|
|
291
|
+
*
|
|
292
|
+
* @param baseDirs - The base directories to scan (e.g. the project source roots).
|
|
293
|
+
* @param name - The subdirectory to look in within each base directory.
|
|
294
|
+
* @param logger - Optional logger forwarded to {@link scanDir}.
|
|
295
|
+
* @returns The flattened list of discovered files across all base directories.
|
|
296
|
+
*/
|
|
297
|
+
declare function scanFiles(baseDirs: string[], name: string, logger?: ScanLogger): Promise<FileInfo[]>;
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/Transform.d.ts
|
|
300
|
+
/**
|
|
301
|
+
* Reads a controller file from disk and returns all routes defined in classes
|
|
302
|
+
* decorated with `@Controller` and methods decorated with HTTP decorators.
|
|
303
|
+
*
|
|
304
|
+
* The returned `import` statements still contain the placeholder source; resolve
|
|
305
|
+
* them to the real file path with {@link resolveImports} when generating code.
|
|
306
|
+
*
|
|
307
|
+
* @param file - File info (path and fullPath) to analyze.
|
|
308
|
+
* @returns A list of {@link RouteInfo} for every route in the file.
|
|
309
|
+
*/
|
|
310
|
+
declare function transformRoute(file: FileInfo): Promise<RouteInfo[]>;
|
|
311
|
+
/**
|
|
312
|
+
* Reads a file from disk and returns all `@Injectable`-decorated classes found within it.
|
|
313
|
+
*
|
|
314
|
+
* @param file - File info (path and fullPath) to analyze.
|
|
315
|
+
* @returns A list of {@link ServiceInfo} for every injectable class in the file.
|
|
316
|
+
*/
|
|
317
|
+
declare function transformService(file: FileInfo): Promise<ServiceInfo[]>;
|
|
318
|
+
/**
|
|
319
|
+
* Reads a file from disk and returns all classes extending `BaseMiddleware` found within it.
|
|
320
|
+
*
|
|
321
|
+
* @param file - File info (path and fullPath) to analyze.
|
|
322
|
+
* @returns A list of {@link MiddlewareInfo} for every middleware class in the file.
|
|
323
|
+
*/
|
|
324
|
+
declare function transformMiddleware(file: FileInfo): Promise<MiddlewareInfo[]>;
|
|
325
|
+
/**
|
|
326
|
+
* Replaces the placeholder import source in each entry with the entry's real
|
|
327
|
+
* absolute file path, producing import statements that load the actual module.
|
|
328
|
+
*
|
|
329
|
+
* @param entries - Scanned entries whose `import` still references the placeholder source.
|
|
330
|
+
* @param placeholder - The placeholder module specifier to replace.
|
|
331
|
+
* @returns The entries with resolved `import` statements.
|
|
332
|
+
*/
|
|
333
|
+
declare function resolveImports<T extends {
|
|
334
|
+
import: string;
|
|
335
|
+
fullPath: string;
|
|
336
|
+
}>(entries: T[], placeholder: string): T[];
|
|
337
|
+
//#endregion
|
|
338
|
+
export { FileInfo, GLOB_SCAN_PATTERN, IMPORT_SOURCE, MIDDLEWARE_IMPORT_SOURCE, MiddlewareInfo, RouteInfo, SERVICE_IMPORT_SOURCE, ScanLogger, ScanProjectOptions, ScanSourceOptions, ServiceInfo, buildImport, extendsSuperClass, extractControllers, extractDecoratorArg, extractMiddlewares, extractParams, extractRoutes, extractServices, getClassNode, getMiddlewares, getRoutes, getServices, hasDecorator, normalizePath, resolveImports, scanDir, scanFiles, scanProject, scanSource, transformMiddleware, transformRoute, transformService };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { parseSync } from "oxc-parser";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "pathe";
|
|
4
|
+
import { glob } from "tinyglobby";
|
|
5
|
+
//#region src/Ast.ts
|
|
6
|
+
/**
|
|
7
|
+
* Pre-compiled regular expression for extracting named route parameters.
|
|
8
|
+
* Matches path segments prefixed with ':' (e.g. ':id', ':slug').
|
|
9
|
+
* Uses the global flag for iterative matching via `exec`.
|
|
10
|
+
*/
|
|
11
|
+
const PARAM_RE = /:([^/]+)/g;
|
|
12
|
+
/**
|
|
13
|
+
* Unwraps export declarations to retrieve the underlying class node,
|
|
14
|
+
* along with metadata about the export style.
|
|
15
|
+
*
|
|
16
|
+
* Handles the following AST patterns:
|
|
17
|
+
* - `ExportDefaultDeclaration` wrapping a `ClassDeclaration` → `isDefault: true`
|
|
18
|
+
* - `ExportNamedDeclaration` wrapping a `ClassDeclaration` → `isDefault: false`
|
|
19
|
+
* - Direct `ClassDeclaration` or `Class` nodes → `isDefault: false`
|
|
20
|
+
*
|
|
21
|
+
* @param node - An AST node from the program body to inspect.
|
|
22
|
+
* @returns An object containing the class AST node and whether it is a default export, or `null` if not a class.
|
|
23
|
+
*/
|
|
24
|
+
function getClassNode(node) {
|
|
25
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
26
|
+
const decl = node.declaration;
|
|
27
|
+
if (decl?.type === "ClassDeclaration" || decl?.type === "Class") return {
|
|
28
|
+
classNode: decl,
|
|
29
|
+
isDefault: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
33
|
+
const decl = node.declaration;
|
|
34
|
+
if (decl?.type === "ClassDeclaration" || decl?.type === "Class") return {
|
|
35
|
+
classNode: decl,
|
|
36
|
+
isDefault: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (node?.type === "ClassDeclaration" || node?.type === "Class") return {
|
|
40
|
+
classNode: node,
|
|
41
|
+
isDefault: false
|
|
42
|
+
};
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Builds an import statement for a class, choosing default vs named syntax.
|
|
47
|
+
*
|
|
48
|
+
* @param className - The class identifier to import.
|
|
49
|
+
* @param source - The module specifier to import from.
|
|
50
|
+
* @param isDefault - Whether the class is a default export.
|
|
51
|
+
* @returns The generated import statement string.
|
|
52
|
+
*/
|
|
53
|
+
function buildImport(className, source, isDefault) {
|
|
54
|
+
return isDefault ? `import ${className} from '${source}';` : `import { ${className} } from '${source}';`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Searches a list of decorators for a `CallExpression` matching the given name
|
|
58
|
+
* and extracts its first string argument.
|
|
59
|
+
*
|
|
60
|
+
* Used to retrieve the path argument from decorators like `@Controller('/api/foo')`.
|
|
61
|
+
* If the decorator is found but has no string argument, an empty string is returned.
|
|
62
|
+
* If no matching decorator is found, `null` is returned.
|
|
63
|
+
*
|
|
64
|
+
* @param decorators - The array of decorator AST nodes to search, or `undefined` if none exist.
|
|
65
|
+
* @param name - The decorator function name to match against (e.g. 'Controller').
|
|
66
|
+
* @returns The string argument value, an empty string if no argument is provided, or `null` if the decorator is not found.
|
|
67
|
+
*/
|
|
68
|
+
function extractDecoratorArg(decorators, name) {
|
|
69
|
+
if (!decorators) return null;
|
|
70
|
+
for (const dec of decorators) {
|
|
71
|
+
const expr = dec.expression;
|
|
72
|
+
if (expr?.type === "CallExpression" && expr.callee?.name === name) {
|
|
73
|
+
const arg = expr.arguments?.[0];
|
|
74
|
+
if (arg?.type === "Literal" && typeof arg.value === "string") return arg.value;
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Returns true if the given decorators array contains a decorator matching `name`.
|
|
82
|
+
* Supports both `@Name` identifier and `@Name()` call expression styles.
|
|
83
|
+
*
|
|
84
|
+
* @param decorators - The array of decorator AST nodes to search.
|
|
85
|
+
* @param name - The decorator name to match.
|
|
86
|
+
* @returns Whether a matching decorator exists.
|
|
87
|
+
*/
|
|
88
|
+
function hasDecorator(decorators, name) {
|
|
89
|
+
if (!decorators) return false;
|
|
90
|
+
for (const dec of decorators) {
|
|
91
|
+
const expr = dec.expression;
|
|
92
|
+
if (expr?.type === "CallExpression" && expr.callee?.name === name) return true;
|
|
93
|
+
if (expr?.type === "Identifier" && expr.name === name) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns true if the class node extends a superclass with the given name.
|
|
99
|
+
*
|
|
100
|
+
* @param classNode - The class AST node to inspect.
|
|
101
|
+
* @param name - The expected superclass identifier name.
|
|
102
|
+
* @returns Whether the class extends the named superclass.
|
|
103
|
+
*/
|
|
104
|
+
function extendsSuperClass(classNode, name) {
|
|
105
|
+
return classNode.superClass?.type === "Identifier" && classNode.superClass.name === name;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Normalizes a route path by removing duplicate slashes and ensuring
|
|
109
|
+
* the path starts with a single leading slash.
|
|
110
|
+
*
|
|
111
|
+
* @param path - The raw concatenated path string to normalize.
|
|
112
|
+
* @returns A cleaned path string with a leading slash and no duplicate separators.
|
|
113
|
+
*/
|
|
114
|
+
function normalizePath(path) {
|
|
115
|
+
return "/" + path.split("/").filter(Boolean).join("/");
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Extracts named route parameters from a path string.
|
|
119
|
+
*
|
|
120
|
+
* Scans the path for segments prefixed with ':' and returns an array of
|
|
121
|
+
* parameter names with the ':' prefix stripped.
|
|
122
|
+
*
|
|
123
|
+
* @param route - The normalized route path to scan for parameters.
|
|
124
|
+
* @returns An array of parameter name strings, empty if none are found.
|
|
125
|
+
*/
|
|
126
|
+
function extractParams(route) {
|
|
127
|
+
const params = [];
|
|
128
|
+
let match;
|
|
129
|
+
while (match = PARAM_RE.exec(route)) params.push(match[1]);
|
|
130
|
+
PARAM_RE.lastIndex = 0;
|
|
131
|
+
return params;
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/Extract.ts
|
|
135
|
+
/**
|
|
136
|
+
* Placeholder module specifier embedded in generated route imports.
|
|
137
|
+
* Callers resolve it to the real file path once known (see {@link Transform}).
|
|
138
|
+
*/
|
|
139
|
+
const IMPORT_SOURCE = "#internal/vercube-route-plugin";
|
|
140
|
+
/**
|
|
141
|
+
* Placeholder module specifier embedded in generated service imports.
|
|
142
|
+
*/
|
|
143
|
+
const SERVICE_IMPORT_SOURCE = "#internal/vercube-service-source";
|
|
144
|
+
/**
|
|
145
|
+
* Placeholder module specifier embedded in generated middleware imports.
|
|
146
|
+
*/
|
|
147
|
+
const MIDDLEWARE_IMPORT_SOURCE = "#internal/vercube-middleware-source";
|
|
148
|
+
/**
|
|
149
|
+
* Mapping of decorator names to their corresponding uppercase HTTP method strings.
|
|
150
|
+
* Used for O(1) lookup and simultaneous method name resolution.
|
|
151
|
+
*/
|
|
152
|
+
const HTTP_METHODS = {
|
|
153
|
+
Get: "GET",
|
|
154
|
+
Post: "POST",
|
|
155
|
+
Put: "PUT",
|
|
156
|
+
Delete: "DELETE",
|
|
157
|
+
Patch: "PATCH",
|
|
158
|
+
Options: "OPTIONS",
|
|
159
|
+
Head: "HEAD"
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Extracts all route definitions from the given TypeScript/JavaScript source code.
|
|
163
|
+
*
|
|
164
|
+
* Parses the source using `oxc-parser` and traverses the resulting AST to find
|
|
165
|
+
* classes decorated with `@Controller(path)`. For each such class, it inspects
|
|
166
|
+
* method definitions for HTTP method decorators (`@Get`, `@Post`, `@Put`, `@Delete`,
|
|
167
|
+
* `@Patch`, `@Options`, `@Head`) and constructs full route paths by concatenating
|
|
168
|
+
* the controller base path with the method-level path.
|
|
169
|
+
*
|
|
170
|
+
* @param code - The raw TypeScript or JavaScript source code string to analyze.
|
|
171
|
+
* @returns An array of {@link RouteInfo} objects representing all discovered routes.
|
|
172
|
+
*/
|
|
173
|
+
function extractRoutes(code) {
|
|
174
|
+
const ast = parseSync("file.ts", code).program;
|
|
175
|
+
const routes = [];
|
|
176
|
+
for (const node of ast.body) {
|
|
177
|
+
const classInfo = getClassNode(node);
|
|
178
|
+
if (!classInfo) continue;
|
|
179
|
+
const { classNode, isDefault } = classInfo;
|
|
180
|
+
const basePath = extractDecoratorArg(classNode.decorators, "Controller");
|
|
181
|
+
if (basePath === null) continue;
|
|
182
|
+
const className = classNode.id?.name;
|
|
183
|
+
if (!className) continue;
|
|
184
|
+
const importStatement = buildImport(className, IMPORT_SOURCE, isDefault);
|
|
185
|
+
for (const member of classNode.body?.body ?? []) {
|
|
186
|
+
if (member.type !== "MethodDefinition") continue;
|
|
187
|
+
for (const decorator of member.decorators ?? []) {
|
|
188
|
+
const expr = decorator.expression;
|
|
189
|
+
if (expr?.type !== "CallExpression") continue;
|
|
190
|
+
const method = HTTP_METHODS[expr.callee?.name];
|
|
191
|
+
if (!method) continue;
|
|
192
|
+
const arg = expr.arguments?.[0];
|
|
193
|
+
const fullRoute = normalizePath(basePath + (arg?.type === "Literal" && typeof arg.value === "string" ? arg.value : ""));
|
|
194
|
+
routes.push({
|
|
195
|
+
import: importStatement,
|
|
196
|
+
importClassName: className,
|
|
197
|
+
route: fullRoute,
|
|
198
|
+
method,
|
|
199
|
+
fullPath: "",
|
|
200
|
+
path: "",
|
|
201
|
+
params: extractParams(fullRoute)
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return routes;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Extracts every `@Controller`-decorated class from the given source code,
|
|
210
|
+
* regardless of whether it declares HTTP method routes.
|
|
211
|
+
*
|
|
212
|
+
* Unlike {@link extractRoutes} (which emits one entry per HTTP route method),
|
|
213
|
+
* this returns one entry per controller class — so controllers that only carry
|
|
214
|
+
* non-HTTP handlers (e.g. WebSocket `@Message` methods) are still discovered for
|
|
215
|
+
* binding into the DI container.
|
|
216
|
+
*
|
|
217
|
+
* @param code - The raw TypeScript or JavaScript source code string to analyze.
|
|
218
|
+
* @returns An array of {@link ServiceInfo} objects for all discovered controller classes.
|
|
219
|
+
*/
|
|
220
|
+
function extractControllers(code) {
|
|
221
|
+
const ast = parseSync("file.ts", code).program;
|
|
222
|
+
const controllers = [];
|
|
223
|
+
for (const node of ast.body) {
|
|
224
|
+
const classInfo = getClassNode(node);
|
|
225
|
+
if (!classInfo) continue;
|
|
226
|
+
const { classNode, isDefault } = classInfo;
|
|
227
|
+
if (extractDecoratorArg(classNode.decorators, "Controller") === null) continue;
|
|
228
|
+
const className = classNode.id?.name;
|
|
229
|
+
if (!className) continue;
|
|
230
|
+
controllers.push({
|
|
231
|
+
import: buildImport(className, IMPORT_SOURCE, isDefault),
|
|
232
|
+
importClassName: className,
|
|
233
|
+
fullPath: "",
|
|
234
|
+
path: ""
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return controllers;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Extracts all `@Injectable`-decorated class definitions from the given source code.
|
|
241
|
+
*
|
|
242
|
+
* @param code - The raw TypeScript or JavaScript source code string to analyze.
|
|
243
|
+
* @returns An array of {@link ServiceInfo} objects for all discovered injectable classes.
|
|
244
|
+
*/
|
|
245
|
+
function extractServices(code) {
|
|
246
|
+
const ast = parseSync("file.ts", code).program;
|
|
247
|
+
const services = [];
|
|
248
|
+
for (const node of ast.body) {
|
|
249
|
+
const classInfo = getClassNode(node);
|
|
250
|
+
if (!classInfo) continue;
|
|
251
|
+
const { classNode, isDefault } = classInfo;
|
|
252
|
+
if (!hasDecorator(classNode.decorators, "Injectable")) continue;
|
|
253
|
+
const className = classNode.id?.name;
|
|
254
|
+
if (!className) continue;
|
|
255
|
+
services.push({
|
|
256
|
+
import: buildImport(className, SERVICE_IMPORT_SOURCE, isDefault),
|
|
257
|
+
importClassName: className,
|
|
258
|
+
fullPath: "",
|
|
259
|
+
path: ""
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return services;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Extracts all class definitions that extend `BaseMiddleware` from the given source code.
|
|
266
|
+
*
|
|
267
|
+
* @param code - The raw TypeScript or JavaScript source code string to analyze.
|
|
268
|
+
* @returns An array of {@link MiddlewareInfo} objects for all discovered middleware classes.
|
|
269
|
+
*/
|
|
270
|
+
function extractMiddlewares(code) {
|
|
271
|
+
const ast = parseSync("file.ts", code).program;
|
|
272
|
+
const middlewares = [];
|
|
273
|
+
for (const node of ast.body) {
|
|
274
|
+
const classInfo = getClassNode(node);
|
|
275
|
+
if (!classInfo) continue;
|
|
276
|
+
const { classNode, isDefault } = classInfo;
|
|
277
|
+
if (!extendsSuperClass(classNode, "BaseMiddleware")) continue;
|
|
278
|
+
const className = classNode.id?.name;
|
|
279
|
+
if (!className) continue;
|
|
280
|
+
middlewares.push({
|
|
281
|
+
import: buildImport(className, MIDDLEWARE_IMPORT_SOURCE, isDefault),
|
|
282
|
+
importClassName: className,
|
|
283
|
+
fullPath: "",
|
|
284
|
+
path: ""
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return middlewares;
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/Scan.ts
|
|
291
|
+
/**
|
|
292
|
+
* Glob pattern matching every source file extension the scanner understands.
|
|
293
|
+
*/
|
|
294
|
+
const GLOB_SCAN_PATTERN = "**/*.{js,mjs,cjs,ts,mts,cts,tsx,jsx}";
|
|
295
|
+
/**
|
|
296
|
+
* Scans a single base directory for files inside the named subdirectory.
|
|
297
|
+
*
|
|
298
|
+
* Globs `<name>/**` relative to `dir`, returning each match as a {@link FileInfo}
|
|
299
|
+
* with its absolute path and the path relative to `<dir>/<name>`. Results are
|
|
300
|
+
* sorted by relative path for deterministic output.
|
|
301
|
+
*
|
|
302
|
+
* @param dir - The base directory to scan within.
|
|
303
|
+
* @param name - The subdirectory (e.g. 'api', 'routes', 'services') to look in.
|
|
304
|
+
* @param logger - Optional logger used to warn when `<dir>/<name>` is not a directory.
|
|
305
|
+
* @returns A sorted list of discovered files.
|
|
306
|
+
*/
|
|
307
|
+
async function scanDir(dir, name, logger) {
|
|
308
|
+
return (await glob(join(name, GLOB_SCAN_PATTERN), {
|
|
309
|
+
cwd: dir,
|
|
310
|
+
dot: true,
|
|
311
|
+
absolute: true
|
|
312
|
+
}).catch((error) => {
|
|
313
|
+
if (error?.code === "ENOTDIR") {
|
|
314
|
+
logger?.warn(`Ignoring \`${join(dir, name)}\`. It must be a directory.`);
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
})).map((fullPath) => ({
|
|
319
|
+
fullPath,
|
|
320
|
+
path: relative(join(dir, name), fullPath)
|
|
321
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Scans every base directory for files inside the named subdirectory.
|
|
325
|
+
*
|
|
326
|
+
* @param baseDirs - The base directories to scan (e.g. the project source roots).
|
|
327
|
+
* @param name - The subdirectory to look in within each base directory.
|
|
328
|
+
* @param logger - Optional logger forwarded to {@link scanDir}.
|
|
329
|
+
* @returns The flattened list of discovered files across all base directories.
|
|
330
|
+
*/
|
|
331
|
+
async function scanFiles(baseDirs, name, logger) {
|
|
332
|
+
return Promise.all(baseDirs.map((dir) => scanDir(dir, name, logger))).then((r) => r.flat());
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/Transform.ts
|
|
336
|
+
/**
|
|
337
|
+
* Reads a controller file from disk and returns all routes defined in classes
|
|
338
|
+
* decorated with `@Controller` and methods decorated with HTTP decorators.
|
|
339
|
+
*
|
|
340
|
+
* The returned `import` statements still contain the placeholder source; resolve
|
|
341
|
+
* them to the real file path with {@link resolveImports} when generating code.
|
|
342
|
+
*
|
|
343
|
+
* @param file - File info (path and fullPath) to analyze.
|
|
344
|
+
* @returns A list of {@link RouteInfo} for every route in the file.
|
|
345
|
+
*/
|
|
346
|
+
async function transformRoute(file) {
|
|
347
|
+
return extractRoutes(readFileSync(file.fullPath, "utf8")).map((route) => ({
|
|
348
|
+
...route,
|
|
349
|
+
...file
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Reads a file from disk and returns all `@Injectable`-decorated classes found within it.
|
|
354
|
+
*
|
|
355
|
+
* @param file - File info (path and fullPath) to analyze.
|
|
356
|
+
* @returns A list of {@link ServiceInfo} for every injectable class in the file.
|
|
357
|
+
*/
|
|
358
|
+
async function transformService(file) {
|
|
359
|
+
return extractServices(readFileSync(file.fullPath, "utf8")).map((service) => ({
|
|
360
|
+
...service,
|
|
361
|
+
...file
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Reads a file from disk and returns all classes extending `BaseMiddleware` found within it.
|
|
366
|
+
*
|
|
367
|
+
* @param file - File info (path and fullPath) to analyze.
|
|
368
|
+
* @returns A list of {@link MiddlewareInfo} for every middleware class in the file.
|
|
369
|
+
*/
|
|
370
|
+
async function transformMiddleware(file) {
|
|
371
|
+
return extractMiddlewares(readFileSync(file.fullPath, "utf8")).map((middleware) => ({
|
|
372
|
+
...middleware,
|
|
373
|
+
...file
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Replaces the placeholder import source in each entry with the entry's real
|
|
378
|
+
* absolute file path, producing import statements that load the actual module.
|
|
379
|
+
*
|
|
380
|
+
* @param entries - Scanned entries whose `import` still references the placeholder source.
|
|
381
|
+
* @param placeholder - The placeholder module specifier to replace.
|
|
382
|
+
* @returns The entries with resolved `import` statements.
|
|
383
|
+
*/
|
|
384
|
+
function resolveImports(entries, placeholder) {
|
|
385
|
+
return entries.map((entry) => ({
|
|
386
|
+
...entry,
|
|
387
|
+
import: entry.import.replace(placeholder, entry.fullPath)
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/Project.ts
|
|
392
|
+
const DEFAULT_API_DIR = "api";
|
|
393
|
+
const DEFAULT_ROUTES_DIR = "routes";
|
|
394
|
+
const DEFAULT_SERVICE_DIRS = [
|
|
395
|
+
"api",
|
|
396
|
+
"routes",
|
|
397
|
+
"services",
|
|
398
|
+
"repositories"
|
|
399
|
+
];
|
|
400
|
+
const DEFAULT_MIDDLEWARE_DIR = "middleware";
|
|
401
|
+
/**
|
|
402
|
+
* Scans the API and routes directories for controllers and returns every route
|
|
403
|
+
* with its `import` resolved to the controller's real file path.
|
|
404
|
+
*
|
|
405
|
+
* @param options - Where to scan. See {@link ScanProjectOptions}.
|
|
406
|
+
* @returns The discovered routes with resolved imports.
|
|
407
|
+
*/
|
|
408
|
+
async function getRoutes(options) {
|
|
409
|
+
const { baseDirs, apiDir = DEFAULT_API_DIR, routesDir = DEFAULT_ROUTES_DIR, logger } = options;
|
|
410
|
+
const files = await Promise.all([scanFiles(baseDirs, apiDir, logger), scanFiles(baseDirs, routesDir, logger)]).then((r) => r.flat());
|
|
411
|
+
return resolveImports(await Promise.all(files.map((file) => transformRoute(file))).then((r) => r.flat()), IMPORT_SOURCE);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Scans the configured service directories for `@Injectable` classes and returns
|
|
415
|
+
* every service with its `import` resolved to the class's real file path.
|
|
416
|
+
*
|
|
417
|
+
* @param options - Where to scan. See {@link ScanProjectOptions}.
|
|
418
|
+
* @returns The discovered services with resolved imports.
|
|
419
|
+
*/
|
|
420
|
+
async function getServices(options) {
|
|
421
|
+
const { baseDirs, serviceDirs = DEFAULT_SERVICE_DIRS, logger } = options;
|
|
422
|
+
const files = await Promise.all(serviceDirs.map((dir) => scanFiles(baseDirs, dir, logger))).then((r) => r.flat());
|
|
423
|
+
return resolveImports(await Promise.all(files.map((file) => transformService(file))).then((r) => r.flat()), SERVICE_IMPORT_SOURCE);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Scans the middleware directory for `BaseMiddleware` subclasses and returns
|
|
427
|
+
* every middleware with its `import` resolved to the class's real file path.
|
|
428
|
+
*
|
|
429
|
+
* @param options - Where to scan. See {@link ScanProjectOptions}.
|
|
430
|
+
* @returns The discovered middleware with resolved imports.
|
|
431
|
+
*/
|
|
432
|
+
async function getMiddlewares(options) {
|
|
433
|
+
const { baseDirs, middlewareDir = DEFAULT_MIDDLEWARE_DIR, logger } = options;
|
|
434
|
+
const files = await scanFiles(baseDirs, middlewareDir, logger);
|
|
435
|
+
return resolveImports(await Promise.all(files.map((file) => transformMiddleware(file))).then((r) => r.flat()), MIDDLEWARE_IMPORT_SOURCE);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Convenience wrapper that scans routes, services and middleware in one call.
|
|
439
|
+
*
|
|
440
|
+
* Services whose class name is already registered as a route are filtered out
|
|
441
|
+
* so a controller is never imported twice.
|
|
442
|
+
*
|
|
443
|
+
* @param options - Where to scan. See {@link ScanProjectOptions}.
|
|
444
|
+
* @returns The discovered routes, deduplicated services, and middleware.
|
|
445
|
+
*/
|
|
446
|
+
async function scanProject(options) {
|
|
447
|
+
const [routes, services, middlewares] = await Promise.all([
|
|
448
|
+
getRoutes(options),
|
|
449
|
+
getServices(options),
|
|
450
|
+
getMiddlewares(options)
|
|
451
|
+
]);
|
|
452
|
+
const routeClassNames = new Set(routes.map((r) => r.importClassName));
|
|
453
|
+
return {
|
|
454
|
+
routes,
|
|
455
|
+
services: services.filter((s) => !routeClassNames.has(s.importClassName)),
|
|
456
|
+
middlewares
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Recursively scans the given directories for `@Controller` classes,
|
|
461
|
+
* `@Injectable` services and `BaseMiddleware` subclasses, reading each file only
|
|
462
|
+
* once.
|
|
463
|
+
*
|
|
464
|
+
* Unlike {@link scanProject}, this does not assume the Nitro convention of
|
|
465
|
+
* dedicated `api`/`routes`/`services` subdirectories — decorated classes are
|
|
466
|
+
* discovered wherever they live in the tree, which matches how Vercube apps are
|
|
467
|
+
* typically organised. **Every** `@Controller` class is returned for binding
|
|
468
|
+
* (not just those with HTTP routes), so WebSocket-only controllers are bound
|
|
469
|
+
* too, while `routes` carries the concrete HTTP routes (method + path) for
|
|
470
|
+
* request matching. Services already discovered as controllers are filtered out
|
|
471
|
+
* so a class is never imported twice.
|
|
472
|
+
*
|
|
473
|
+
* @param options - Directories to scan. See {@link ScanSourceOptions}.
|
|
474
|
+
* @returns The discovered controllers, HTTP routes, deduplicated services, and middleware, all with resolved imports.
|
|
475
|
+
*/
|
|
476
|
+
async function scanSource(options) {
|
|
477
|
+
const files = await scanFiles(options.dirs, ".", options.logger);
|
|
478
|
+
const controllers = [];
|
|
479
|
+
const routes = [];
|
|
480
|
+
const services = [];
|
|
481
|
+
const middlewares = [];
|
|
482
|
+
for (const file of files) {
|
|
483
|
+
const code = readFileSync(file.fullPath, "utf8");
|
|
484
|
+
for (const controller of extractControllers(code)) controllers.push({
|
|
485
|
+
...controller,
|
|
486
|
+
...file,
|
|
487
|
+
import: controller.import.replace(IMPORT_SOURCE, file.fullPath)
|
|
488
|
+
});
|
|
489
|
+
for (const route of extractRoutes(code)) routes.push({
|
|
490
|
+
...route,
|
|
491
|
+
...file,
|
|
492
|
+
import: route.import.replace(IMPORT_SOURCE, file.fullPath)
|
|
493
|
+
});
|
|
494
|
+
for (const service of extractServices(code)) services.push({
|
|
495
|
+
...service,
|
|
496
|
+
...file,
|
|
497
|
+
import: service.import.replace(SERVICE_IMPORT_SOURCE, file.fullPath)
|
|
498
|
+
});
|
|
499
|
+
for (const middleware of extractMiddlewares(code)) middlewares.push({
|
|
500
|
+
...middleware,
|
|
501
|
+
...file,
|
|
502
|
+
import: middleware.import.replace(MIDDLEWARE_IMPORT_SOURCE, file.fullPath)
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const controllerClassNames = new Set(controllers.map((c) => c.importClassName));
|
|
506
|
+
return {
|
|
507
|
+
controllers,
|
|
508
|
+
routes,
|
|
509
|
+
services: services.filter((s) => !controllerClassNames.has(s.importClassName)),
|
|
510
|
+
middlewares
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
export { GLOB_SCAN_PATTERN, IMPORT_SOURCE, MIDDLEWARE_IMPORT_SOURCE, SERVICE_IMPORT_SOURCE, buildImport, extendsSuperClass, extractControllers, extractDecoratorArg, extractMiddlewares, extractParams, extractRoutes, extractServices, getClassNode, getMiddlewares, getRoutes, getServices, hasDecorator, normalizePath, resolveImports, scanDir, scanFiles, scanProject, scanSource, transformMiddleware, transformRoute, transformService };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vercube/scan",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Source scanning and AST extraction utilities for the Vercube framework",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/vercube/vercube.git",
|
|
8
|
+
"directory": "packages/scan"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.mjs",
|
|
14
|
+
"module": "./dist/index.mjs",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.mjs",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"vercube",
|
|
26
|
+
"scan",
|
|
27
|
+
"ast",
|
|
28
|
+
"framework"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"oxc-parser": "0.112.0",
|
|
32
|
+
"pathe": "2.0.3",
|
|
33
|
+
"tinyglobby": "0.2.15"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsdown --config ../../tsdown.config.ts --config-loader=unrun"
|
|
40
|
+
}
|
|
41
|
+
}
|