@adonisjs/assembler 8.0.0 → 8.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/build/chunk-DF48asd8.js +9 -0
- package/build/{codemod_exception-CzQgXAAf.js → codemod_exception-BMNJZ0i1.js} +143 -0
- package/build/index.js +934 -7
- package/build/main-DEUzrVBq.js +565 -0
- package/build/main-INOi9swJ.js +471 -0
- package/build/src/code_scanners/routes_scanner/main.js +3 -171
- package/build/src/code_transformer/main.js +481 -2
- package/build/src/file_system.d.ts +1 -1
- package/build/src/helpers.js +133 -0
- package/build/src/index_generator/main.js +3 -28
- package/build/src/types/main.js +1 -0
- package/build/src/utils.d.ts +0 -2
- package/build/{virtual_file_system-bGeoWsK-.js → virtual_file_system-dzfXNwEp.js} +287 -0
- package/package.json +9 -9
- package/build/source-dVeugJ0e.js +0 -166
- package/build/validator_extractor-Ccio_Ndi.js +0 -82
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { i as isRelative, m as debug_default, t as VirtualFileSystem } from "./virtual_file_system-dzfXNwEp.js";
|
|
2
|
+
import { findImport, inspectClass, inspectClassMethods, inspectMethodArguments, nodeToPlainText, searchValidatorDirectUsage } from "./src/helpers.js";
|
|
3
|
+
import { cliui } from "@poppinss/cliui";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import string from "@poppinss/utils/string";
|
|
6
|
+
import { join, relative } from "node:path";
|
|
7
|
+
import StringBuilder from "@poppinss/utils/string_builder";
|
|
8
|
+
import { parseImports } from "parse-imports";
|
|
9
|
+
import { resolve } from "import-meta-resolve";
|
|
10
|
+
//#region src/paths_resolver.ts
|
|
11
|
+
/**
|
|
12
|
+
* Encapsulates the API to resolve import specifiers with the ability
|
|
13
|
+
* to define custom resolver functions.
|
|
14
|
+
*
|
|
15
|
+
* The PathsResolver provides a caching mechanism to avoid resolving the same
|
|
16
|
+
* specifier multiple times and supports custom resolvers for handling
|
|
17
|
+
* special import patterns like path aliases.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const resolver = new PathsResolver()
|
|
21
|
+
* resolver.use((specifier) => '/custom/path/' + specifier)
|
|
22
|
+
* const resolved = resolver.resolve('#app/models/user')
|
|
23
|
+
*/
|
|
24
|
+
var PathsResolver = class {
|
|
25
|
+
/**
|
|
26
|
+
* The root of the application from where we will resolve
|
|
27
|
+
* paths.
|
|
28
|
+
*/
|
|
29
|
+
#appRoot;
|
|
30
|
+
/**
|
|
31
|
+
* Cache of resolved paths to avoid resolving the same specifier multiple times
|
|
32
|
+
*/
|
|
33
|
+
#resolvedPaths = {};
|
|
34
|
+
/**
|
|
35
|
+
* The resolver function used to resolve import specifiers
|
|
36
|
+
*/
|
|
37
|
+
#resolver = (specifier, parentPath) => resolve(specifier, parentPath);
|
|
38
|
+
constructor(appRoot) {
|
|
39
|
+
this.#appRoot = pathToFileURL(join(appRoot, "index.js")).href;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Define a custom resolver that resolves a path
|
|
43
|
+
*
|
|
44
|
+
* The custom resolver function will be used instead of the default
|
|
45
|
+
* import.meta.resolve() for resolving import specifiers.
|
|
46
|
+
*
|
|
47
|
+
* @param resolver - Function that takes a specifier and returns resolved path
|
|
48
|
+
*/
|
|
49
|
+
use(resolver) {
|
|
50
|
+
this.#resolver = resolver;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve import specifier to an absolute file path
|
|
54
|
+
*
|
|
55
|
+
* This method caches resolved paths to improve performance on repeated
|
|
56
|
+
* resolutions. Relative paths are not supported and will throw an error.
|
|
57
|
+
*
|
|
58
|
+
* @param specifier - The import specifier to resolve (must not be relative)
|
|
59
|
+
* @returns The resolved absolute file path
|
|
60
|
+
* @throws Error when attempting to resolve relative paths
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* const path = resolver.resolve('#app/models/user')
|
|
64
|
+
* const path2 = resolver.resolve('@/utils/helper')
|
|
65
|
+
*/
|
|
66
|
+
resolve(specifier, rewriteAliasImportExtension = false) {
|
|
67
|
+
if (isRelative(specifier)) throw new Error("Cannot resolve relative paths using PathsResolver");
|
|
68
|
+
const cacheKey = specifier;
|
|
69
|
+
const cached = this.#resolvedPaths[cacheKey];
|
|
70
|
+
if (cached) return cached;
|
|
71
|
+
/**
|
|
72
|
+
* Currently the PathsResolver relies on a non-standard way of resolving
|
|
73
|
+
* absolute paths or paths with subpath imports. Because of which, the
|
|
74
|
+
* on-disk file could be TypeScript, while the resolved filepath is
|
|
75
|
+
* JavaScript.
|
|
76
|
+
*
|
|
77
|
+
* To overcome this limitation, we rewrite the file extension to ".ts" when
|
|
78
|
+
* the import specifier starts with a "#".
|
|
79
|
+
*/
|
|
80
|
+
let resolvedPath = fileURLToPath(this.#resolver(specifier, this.#appRoot));
|
|
81
|
+
if (rewriteAliasImportExtension && specifier.startsWith("#") && resolvedPath.endsWith(".js")) resolvedPath = resolvedPath.replace(/\.js$/, ".ts");
|
|
82
|
+
this.#resolvedPaths[cacheKey] = resolvedPath;
|
|
83
|
+
return this.#resolvedPaths[cacheKey];
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/code_scanners/routes_scanner/validator_extractor.ts
|
|
88
|
+
/**
|
|
89
|
+
* Extracts the VineJS validator usage from within a controller method.
|
|
90
|
+
*
|
|
91
|
+
* This function analyzes controller method code to detect validator usage patterns
|
|
92
|
+
* and extracts the validator references along with their import information.
|
|
93
|
+
* The following syntaxes are supported:
|
|
94
|
+
*
|
|
95
|
+
* - `request.validateUsing(validatorReference)`
|
|
96
|
+
* - `vine.validate(validatorReference)`
|
|
97
|
+
* - `validatorReference.validate(request.all())`
|
|
98
|
+
*
|
|
99
|
+
* - `request.validateUsing(ControllerReference.validator)`
|
|
100
|
+
* - `vine.validate(ControllerReference.validator)`
|
|
101
|
+
* - `ControllerReference.validator.validate(request.all())`
|
|
102
|
+
*
|
|
103
|
+
* The app root is needed to create relative validator imports in case
|
|
104
|
+
* a relative import was used within the controller file.
|
|
105
|
+
*
|
|
106
|
+
* @param appRoot - The root directory of the application
|
|
107
|
+
* @param vfs - Virtual file system instance for code analysis
|
|
108
|
+
* @param controller - The controller to analyze for validator usage
|
|
109
|
+
* @returns Promise resolving to array of validator information or undefined
|
|
110
|
+
*/
|
|
111
|
+
async function extractValidators(appRoot, vfs, controller) {
|
|
112
|
+
const root = await vfs.get(controller.path);
|
|
113
|
+
const fileContents = root.text();
|
|
114
|
+
const controllerClass = inspectClass(root);
|
|
115
|
+
if (!controllerClass) {
|
|
116
|
+
debug_default(`No class defined within the "%s"`, controller.import.specifier);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Inspect class methods and find the scope down to the method
|
|
121
|
+
* we are currently inspecting
|
|
122
|
+
*/
|
|
123
|
+
const method = inspectClassMethods(controllerClass).find((methodNode) => {
|
|
124
|
+
return methodNode.find({ rule: { kind: "property_identifier" } })?.text() === controller.method;
|
|
125
|
+
});
|
|
126
|
+
if (!method) {
|
|
127
|
+
debug_default(`Unable to find "%s" method in "%s"`, controller.method, controller.import.specifier);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Inspect validators via "request.validateUsing" and "vine.validate"
|
|
132
|
+
* method calls.
|
|
133
|
+
*/
|
|
134
|
+
const validationCalls = inspectMethodArguments(method, [
|
|
135
|
+
"request.validateUsing",
|
|
136
|
+
"request.tryValidateUsing",
|
|
137
|
+
"$CTX.request.validateUsing",
|
|
138
|
+
"$CTX.request.tryValidateUsing",
|
|
139
|
+
"vine.validate",
|
|
140
|
+
"vine.tryValidate"
|
|
141
|
+
]).map((node) => {
|
|
142
|
+
const firstArg = node.find({ rule: { any: [{ kind: "identifier" }, { kind: "member_expression" }] } });
|
|
143
|
+
if (!firstArg) return;
|
|
144
|
+
return nodeToPlainText(firstArg);
|
|
145
|
+
}).filter((node) => node !== void 0).concat(searchValidatorDirectUsage(method).map((node) => nodeToPlainText(node)));
|
|
146
|
+
/**
|
|
147
|
+
* Unable to find any validation calls, or the first argument of the
|
|
148
|
+
* validation call was not a member_expression or an identifier.
|
|
149
|
+
*/
|
|
150
|
+
if (!validationCalls.length) {
|
|
151
|
+
debug_default(`Unable to detect any validation calls in "%s.%s" method`, controller.import.specifier, controller.method);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const controllerName = controllerClass.find({ rule: { kind: "type_identifier" } }).text();
|
|
155
|
+
const controllerURL = pathToFileURL(controller.path);
|
|
156
|
+
return (await Promise.all(validationCalls.map(async (validationCall) => {
|
|
157
|
+
if (validationCall.split(".")[0] === controllerName) return {
|
|
158
|
+
name: validationCall,
|
|
159
|
+
import: {
|
|
160
|
+
specifier: controller.import.specifier,
|
|
161
|
+
type: "default",
|
|
162
|
+
value: controllerName
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const importCall = await findImport(fileContents, validationCall);
|
|
166
|
+
if (!importCall) {
|
|
167
|
+
debug_default("Unable to find import for \"%s\" used by \"%s.%s\" method", validationCall, controller.import.specifier, controller.method);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
name: validationCall,
|
|
172
|
+
import: {
|
|
173
|
+
specifier: isRelative(importCall.specifier) ? string.toUnixSlash(relative(appRoot, fileURLToPath(new URL(importCall.specifier, controllerURL)))) : importCall.specifier,
|
|
174
|
+
type: importCall.clause.type,
|
|
175
|
+
value: importCall.clause.value
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}))).filter((value) => !!value);
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/code_scanners/routes_scanner/main.ts
|
|
182
|
+
/**
|
|
183
|
+
* RoutesScanner is responsible for scanning application routes,
|
|
184
|
+
* extracting their controllers, validators, request and response types.
|
|
185
|
+
*
|
|
186
|
+
* The RoutesScanner analyzes route definitions to extract TypeScript type information
|
|
187
|
+
* for request validation, response types, and controller methods. It supports custom
|
|
188
|
+
* rules for overriding default behavior and provides hooks for extending functionality.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* const scanner = new RoutesScanner(appRoot, [rules])
|
|
192
|
+
* scanner.defineResponse((route, controller) => {
|
|
193
|
+
* return { type: 'CustomResponseType', imports: [] }
|
|
194
|
+
* })
|
|
195
|
+
* await scanner.scan(routes)
|
|
196
|
+
* const scannedRoutes = scanner.getScannedRoutes()
|
|
197
|
+
*/
|
|
198
|
+
var RoutesScanner = class {
|
|
199
|
+
/**
|
|
200
|
+
* Optional filter function to selectively include routes during scanning.
|
|
201
|
+
* When defined, only routes for which this function returns true will be processed.
|
|
202
|
+
*/
|
|
203
|
+
#filter;
|
|
204
|
+
/**
|
|
205
|
+
* The root of the application from where we will resolve
|
|
206
|
+
* paths.
|
|
207
|
+
*/
|
|
208
|
+
#appRoot;
|
|
209
|
+
/**
|
|
210
|
+
* Collection of routes by their controller file path. This helps
|
|
211
|
+
* us re-compute request types when the controller is changed.
|
|
212
|
+
*/
|
|
213
|
+
#controllerRoutes = {};
|
|
214
|
+
/**
|
|
215
|
+
* Collection of scanned routes
|
|
216
|
+
*/
|
|
217
|
+
#scannedRoutes = [];
|
|
218
|
+
/**
|
|
219
|
+
* A custom method to self compute the response type for a route. Return
|
|
220
|
+
* undefined to fallback to the default behavior
|
|
221
|
+
*/
|
|
222
|
+
#computeResponseTypes;
|
|
223
|
+
/**
|
|
224
|
+
* A custom method to self compute the request type for a route. Return
|
|
225
|
+
* undefined to fallback to the default behavior
|
|
226
|
+
*/
|
|
227
|
+
#computeRequestTypes;
|
|
228
|
+
/**
|
|
229
|
+
* A custom method to self extract the validators from the route controller.
|
|
230
|
+
* Return undefined to fallback to the default behavior.
|
|
231
|
+
*/
|
|
232
|
+
#extractValidators;
|
|
233
|
+
/**
|
|
234
|
+
* CLI UI instance to log colorful messages and progress information
|
|
235
|
+
*/
|
|
236
|
+
ui = cliui();
|
|
237
|
+
/**
|
|
238
|
+
* The paths resolver is used to convert subpath and package
|
|
239
|
+
* imports to absolute paths
|
|
240
|
+
*/
|
|
241
|
+
pathsResolver;
|
|
242
|
+
/**
|
|
243
|
+
* The rules to apply when scanning routes
|
|
244
|
+
*/
|
|
245
|
+
rules = {
|
|
246
|
+
request: {},
|
|
247
|
+
response: {}
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Create a new RoutesScanner instance
|
|
251
|
+
*
|
|
252
|
+
* @param appRoot - The root directory of the application
|
|
253
|
+
* @param rulesCollection - Collection of rules to apply during scanning
|
|
254
|
+
*/
|
|
255
|
+
constructor(appRoot, rulesCollection) {
|
|
256
|
+
this.#appRoot = appRoot;
|
|
257
|
+
this.pathsResolver = new PathsResolver(appRoot);
|
|
258
|
+
rulesCollection.forEach((rules) => {
|
|
259
|
+
Object.assign(this.rules.request, rules.request);
|
|
260
|
+
Object.assign(this.rules.response, rules.response);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Determines if a route should be skipped based on the filter function.
|
|
265
|
+
*
|
|
266
|
+
* @param route - The route to check
|
|
267
|
+
*/
|
|
268
|
+
#shouldSkipRoute(route) {
|
|
269
|
+
if (this.#filter) return !this.#filter(route);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Assumes the validators are from VineJS and computes the request types from them
|
|
274
|
+
*
|
|
275
|
+
* This method generates TypeScript type definitions for request validation
|
|
276
|
+
* by analyzing VineJS validators and creating Infer types.
|
|
277
|
+
*
|
|
278
|
+
* @param route - The scanned route with validators
|
|
279
|
+
* @returns Request type definition or undefined
|
|
280
|
+
*/
|
|
281
|
+
#prepareRequestTypes(route) {
|
|
282
|
+
if (!route.validators) return;
|
|
283
|
+
return {
|
|
284
|
+
type: route.validators.reduce((result, validator) => {
|
|
285
|
+
const validatorExport = validator.import.type === "default" ? ".default" : validator.import.type === "named" ? `.${validator.import.value}` : "";
|
|
286
|
+
const [, ...segments] = validator.name.split(".");
|
|
287
|
+
const namespace = segments.map((segment) => `['${segment}']`).join("");
|
|
288
|
+
result.push(`InferInput<(typeof import('${validator.import.specifier}')${validatorExport})${namespace}>`);
|
|
289
|
+
return result;
|
|
290
|
+
}, []).join("|"),
|
|
291
|
+
imports: [`import { InferInput } from '@vinejs/vine/types'`]
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Inspects the controller reference and fetches its import specifier
|
|
296
|
+
* and the controller name from it.
|
|
297
|
+
*
|
|
298
|
+
* @param importExpression - The import expression to parse (e.g., "import('#controllers/users_controller')")
|
|
299
|
+
* @param method - The controller method name (defaults to 'handle' if not provided)
|
|
300
|
+
*/
|
|
301
|
+
async #inspectControllerSpecifier(importExpression, method) {
|
|
302
|
+
const importedModule = [...await parseImports(importExpression)].find(($import) => $import.isDynamicImport && $import.moduleSpecifier.value);
|
|
303
|
+
/**
|
|
304
|
+
* The provided expression was not a lazy load import.
|
|
305
|
+
*/
|
|
306
|
+
if (!importedModule || importedModule.moduleSpecifier.type !== "package" || !importedModule.moduleSpecifier.value) return null;
|
|
307
|
+
const specifier = importedModule.moduleSpecifier.value;
|
|
308
|
+
const name = new StringBuilder(specifier.split("/").pop()).removeSuffix("Controller").pascalCase().suffix("Controller").toString();
|
|
309
|
+
return {
|
|
310
|
+
name,
|
|
311
|
+
method: method ?? "handle",
|
|
312
|
+
path: string.toUnixSlash(this.pathsResolver.resolve(specifier, true)),
|
|
313
|
+
import: {
|
|
314
|
+
specifier,
|
|
315
|
+
type: "default",
|
|
316
|
+
value: name
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Defines the response type for the route by calling the custom compute function
|
|
322
|
+
* or falling back to inferring the return type from the controller method.
|
|
323
|
+
*
|
|
324
|
+
* @param route - The scanned route to set response type for
|
|
325
|
+
* @param controller - The controller containing the route handler
|
|
326
|
+
*/
|
|
327
|
+
async #setResponse(route, controller) {
|
|
328
|
+
route.response = await this.#computeResponseTypes?.(route, controller, this) ?? {
|
|
329
|
+
type: `ReturnType<import('${controller.import.specifier}').default['${controller.method}']>`,
|
|
330
|
+
imports: []
|
|
331
|
+
};
|
|
332
|
+
debug_default("computed route \"%s\" response %O", route.name, route.response);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Defines the request type for the route by extracting validators and computing
|
|
336
|
+
* the request type from them or using a custom compute function.
|
|
337
|
+
*
|
|
338
|
+
* @param route - The scanned route to set request type for
|
|
339
|
+
* @param controller - The controller containing the route handler
|
|
340
|
+
* @param vfs - Virtual file system for analyzing controller code
|
|
341
|
+
*/
|
|
342
|
+
async #setRequest(route, controller, vfs) {
|
|
343
|
+
route.validators = await this.#extractValidators?.(route, controller, this) ?? await extractValidators(this.#appRoot, vfs, controller);
|
|
344
|
+
debug_default("computed route \"%s\" validators %O", route.name, route.validators);
|
|
345
|
+
route.request = await this.#computeRequestTypes?.(route, controller, this) ?? this.#prepareRequestTypes(route);
|
|
346
|
+
debug_default("computed route \"%s\" request input %O", route.name, route.request);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Scans a route that is not using a controller (inline route handler).
|
|
350
|
+
* For routes without controllers, only rules-based request/response types are available.
|
|
351
|
+
*
|
|
352
|
+
* @param route - The route to process
|
|
353
|
+
*/
|
|
354
|
+
#processRouteWithoutController(route) {
|
|
355
|
+
if (!route.name) {
|
|
356
|
+
debug_default(`skipping route "%s" as it does not have a name`, route.pattern);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const scannedRoute = {
|
|
360
|
+
name: route.name,
|
|
361
|
+
domain: route.domain,
|
|
362
|
+
methods: route.methods,
|
|
363
|
+
pattern: route.pattern,
|
|
364
|
+
tokens: route.tokens,
|
|
365
|
+
request: this.rules.request[route.name],
|
|
366
|
+
response: this.rules.response[route.name]
|
|
367
|
+
};
|
|
368
|
+
debug_default("scanned route without controller %O", scannedRoute);
|
|
369
|
+
this.#scannedRoutes.push(scannedRoute);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Scans a route that is using a controller reference.
|
|
373
|
+
* Extracts controller information, validators, and type information.
|
|
374
|
+
*
|
|
375
|
+
* @param route - The route with a controller handler
|
|
376
|
+
* @param vfs - Virtual file system for analyzing controller code
|
|
377
|
+
*/
|
|
378
|
+
async #processRouteWithController(route, vfs) {
|
|
379
|
+
/**
|
|
380
|
+
* Process without controller, when importExpression is missing. This is
|
|
381
|
+
* the case where someone imports the controllers and uses it by
|
|
382
|
+
* reference
|
|
383
|
+
*/
|
|
384
|
+
if (!route.handler || !route.handler.importExpression) return this.#processRouteWithoutController(route);
|
|
385
|
+
/**
|
|
386
|
+
* Inspect controller by parsing its import expression
|
|
387
|
+
*/
|
|
388
|
+
const controller = await this.#inspectControllerSpecifier(route.handler.importExpression, route.handler.method);
|
|
389
|
+
/**
|
|
390
|
+
* Process route without a controller when we are not able to extract the import
|
|
391
|
+
* path of the controller, because the import path is needed to further extract
|
|
392
|
+
* request and response types.
|
|
393
|
+
*/
|
|
394
|
+
if (!controller) return this.#processRouteWithoutController(route);
|
|
395
|
+
debug_default("processing route \"%s\" with inspected controller %O", route.name, controller);
|
|
396
|
+
/**
|
|
397
|
+
* Converting controller name and its method to snake_case to create a unique
|
|
398
|
+
* name for the route. There could be chances where one controller+method
|
|
399
|
+
* combination is bound to multiple routes.
|
|
400
|
+
*/
|
|
401
|
+
route.name = route.name ?? new StringBuilder(controller.name).removeSuffix("Controller").snakeCase().suffix(`.${string.snakeCase(controller.method)}`).toString();
|
|
402
|
+
/**
|
|
403
|
+
* Skip route when its name is within the array of
|
|
404
|
+
* skip routes
|
|
405
|
+
*/
|
|
406
|
+
if (this.#shouldSkipRoute(route)) return;
|
|
407
|
+
/**
|
|
408
|
+
* Creating a scanned route and tracking it. We will inspect the response
|
|
409
|
+
* and request if these values are not provided via rules.
|
|
410
|
+
*/
|
|
411
|
+
const scannedRoute = {
|
|
412
|
+
name: route.name,
|
|
413
|
+
domain: route.domain,
|
|
414
|
+
methods: route.methods,
|
|
415
|
+
pattern: route.pattern,
|
|
416
|
+
tokens: route.tokens,
|
|
417
|
+
request: this.rules.request[route.name],
|
|
418
|
+
response: this.rules.response[route.name],
|
|
419
|
+
controller
|
|
420
|
+
};
|
|
421
|
+
debug_default("scanned route %O", scannedRoute);
|
|
422
|
+
this.#scannedRoutes.push(scannedRoute);
|
|
423
|
+
/**
|
|
424
|
+
* Track route next to the controller absolute path. This way we will be
|
|
425
|
+
* able to invalidate request types everytime the controller is modified.
|
|
426
|
+
*/
|
|
427
|
+
if (!scannedRoute.request || !scannedRoute.response) {
|
|
428
|
+
debug_default("tracking controller for rescanning %O", scannedRoute);
|
|
429
|
+
this.#controllerRoutes[controller.path] ??= [];
|
|
430
|
+
this.#controllerRoutes[controller.path].push(scannedRoute);
|
|
431
|
+
if (!scannedRoute.response) await this.#setResponse(scannedRoute, controller);
|
|
432
|
+
if (!scannedRoute.request) await this.#setRequest(scannedRoute, controller, vfs);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Processes a given route list item and scans it to extract
|
|
437
|
+
* the controller, request and response types.
|
|
438
|
+
*
|
|
439
|
+
* @param route - The route to process
|
|
440
|
+
* @param vfs - Virtual file system for analyzing controller code
|
|
441
|
+
*/
|
|
442
|
+
async #processRoute(route, vfs) {
|
|
443
|
+
/**
|
|
444
|
+
* Skip route when it has a name and also part of
|
|
445
|
+
* skip array
|
|
446
|
+
*/
|
|
447
|
+
if (route.name && this.#shouldSkipRoute(route)) {
|
|
448
|
+
debug_default("route skipped route: %O, rules: %O", route, this.rules);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Routes without a controller reference cannot have types for the request
|
|
453
|
+
* and response (unless provided via rules)
|
|
454
|
+
*/
|
|
455
|
+
if (typeof route.handler === "function") {
|
|
456
|
+
this.#processRouteWithoutController(route);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
await this.#processRouteWithController(route, vfs);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Register a callback to self compute the response types for
|
|
463
|
+
* a given route. The callback will be executed for all
|
|
464
|
+
* the routes and you must return undefined to fallback
|
|
465
|
+
* to the default behavior of detecting types
|
|
466
|
+
*
|
|
467
|
+
* @param callback - Function to compute response types for routes
|
|
468
|
+
* @returns This RoutesScanner instance for method chaining
|
|
469
|
+
*/
|
|
470
|
+
defineResponse(callback) {
|
|
471
|
+
this.#computeResponseTypes = callback;
|
|
472
|
+
return this;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Register a callback to self compute the request types for
|
|
476
|
+
* a given route. The callback will be executed for all
|
|
477
|
+
* the routes and you must return undefined to fallback
|
|
478
|
+
* to the default behavior of detecting types
|
|
479
|
+
*
|
|
480
|
+
* @param callback - Function to compute request types for routes
|
|
481
|
+
* @returns This RoutesScanner instance for method chaining
|
|
482
|
+
*/
|
|
483
|
+
defineRequest(callback) {
|
|
484
|
+
this.#computeRequestTypes = callback;
|
|
485
|
+
return this;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Register a callback to extract validators from the route controller
|
|
489
|
+
*
|
|
490
|
+
* @param callback - Function to extract validators from controllers
|
|
491
|
+
* @returns This RoutesScanner instance for method chaining
|
|
492
|
+
*/
|
|
493
|
+
extractValidators(callback) {
|
|
494
|
+
this.#extractValidators = callback;
|
|
495
|
+
return this;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Returns the scanned routes
|
|
499
|
+
*
|
|
500
|
+
* @returns Array of scanned routes with their metadata
|
|
501
|
+
*/
|
|
502
|
+
getScannedRoutes() {
|
|
503
|
+
return this.#scannedRoutes;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Returns an array of controllers bound to the provided
|
|
507
|
+
* routes
|
|
508
|
+
*
|
|
509
|
+
* @returns Array of controller's absolute paths
|
|
510
|
+
*/
|
|
511
|
+
getControllers() {
|
|
512
|
+
return Object.keys(this.#controllerRoutes);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Invalidating a controller will trigger computing the validators,
|
|
516
|
+
* request types and the response types.
|
|
517
|
+
*
|
|
518
|
+
* @param controllerPath - Path to the controller file to invalidate
|
|
519
|
+
*/
|
|
520
|
+
async invalidate(controllerPath) {
|
|
521
|
+
const controllerRoutes = this.#controllerRoutes[controllerPath];
|
|
522
|
+
if (!controllerRoutes || !controllerRoutes.length) {
|
|
523
|
+
debug_default("\"%s\" controllers is not part of scanned controllers %O", controllerPath, this.#controllerRoutes);
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
for (let scannedRoute of controllerRoutes) if (scannedRoute.controller) {
|
|
527
|
+
debug_default("invalidating route %O", scannedRoute);
|
|
528
|
+
const vfs = new VirtualFileSystem(this.#appRoot);
|
|
529
|
+
await this.#setResponse(scannedRoute, scannedRoute.controller);
|
|
530
|
+
await this.#setRequest(scannedRoute, scannedRoute.controller, vfs);
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Sets a filter function to selectively include routes during scanning.
|
|
536
|
+
*
|
|
537
|
+
* @param filterFn - Function that returns true for routes to include
|
|
538
|
+
* @returns This RoutesScanner instance for method chaining
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* scanner.filter((route) => {
|
|
542
|
+
* return route.pattern.startsWith('/api')
|
|
543
|
+
* })
|
|
544
|
+
*/
|
|
545
|
+
filter(filterFn) {
|
|
546
|
+
this.#filter = filterFn;
|
|
547
|
+
return this;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Scans an array of Route list items and fetches their validators,
|
|
551
|
+
* controllers, and request/response types.
|
|
552
|
+
*
|
|
553
|
+
* This is the main method that processes all routes and extracts
|
|
554
|
+
* their type information for code generation purposes.
|
|
555
|
+
*
|
|
556
|
+
* @param routes - Array of route list items to scan
|
|
557
|
+
*/
|
|
558
|
+
async scan(routes) {
|
|
559
|
+
const vfs = new VirtualFileSystem(this.#appRoot);
|
|
560
|
+
for (const route of routes) await this.#processRoute(route, vfs);
|
|
561
|
+
vfs.invalidate();
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
//#endregion
|
|
565
|
+
export { RoutesScanner as t };
|