@adonisjs/assembler 8.0.0-next.13 → 8.0.0-next.15

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.
@@ -184,8 +184,9 @@ function throttle(fn, name) {
184
184
  return;
185
185
  }
186
186
  isBusy = true;
187
- debug_default('executing "%s" function', name);
187
+ debug_default('executing throttled function "%s"', name);
188
188
  await fn(...args);
189
+ debug_default('executed throttled function "%s"', name);
189
190
  isBusy = false;
190
191
  if (hasQueuedCalls) {
191
192
  hasQueuedCalls = false;
@@ -371,8 +372,13 @@ var VirtualFileSystem = class {
371
372
  * @param filePath - Optional file path to clear. If omitted, clears entire cache
372
373
  */
373
374
  invalidate(filePath) {
374
- debug_default('invalidate AST cache "%s"', filePath);
375
- filePath ? this.#astCache.delete(filePath) : this.#astCache.clear();
375
+ if (filePath) {
376
+ debug_default('invalidate AST cache "%s"', filePath);
377
+ this.#astCache.delete(filePath);
378
+ } else {
379
+ debug_default("clear AST cache");
380
+ this.#astCache.clear();
381
+ }
376
382
  }
377
383
  /**
378
384
  * Clear all scanned files from memory
@@ -0,0 +1,474 @@
1
+ import {
2
+ findImport,
3
+ inspectClass,
4
+ inspectClassMethods,
5
+ inspectMethodArguments,
6
+ nodeToPlainText,
7
+ searchValidatorDirectUsage
8
+ } from "./chunk-TIKQQRMX.js";
9
+ import {
10
+ VirtualFileSystem,
11
+ debug_default,
12
+ isRelative
13
+ } from "./chunk-3GYRM5Y2.js";
14
+
15
+ // src/code_scanners/routes_scanner/main.ts
16
+ import { cliui } from "@poppinss/cliui";
17
+ import string2 from "@poppinss/utils/string";
18
+ import { parseImports } from "parse-imports";
19
+ import StringBuilder from "@poppinss/utils/string_builder";
20
+
21
+ // src/paths_resolver.ts
22
+ import { join } from "path";
23
+ import { fileURLToPath, pathToFileURL } from "url";
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) => import.meta.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) {
67
+ if (isRelative(specifier)) {
68
+ throw new Error("Cannot resolve relative paths using PathsResolver");
69
+ }
70
+ const cacheKey = specifier;
71
+ const cached = this.#resolvedPaths[cacheKey];
72
+ if (cached) {
73
+ return cached;
74
+ }
75
+ this.#resolvedPaths[cacheKey] = fileURLToPath(this.#resolver(specifier, this.#appRoot));
76
+ return this.#resolvedPaths[cacheKey];
77
+ }
78
+ };
79
+
80
+ // src/code_scanners/routes_scanner/validator_extractor.ts
81
+ import { relative } from "path";
82
+ import string from "@poppinss/utils/string";
83
+ import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "url";
84
+ async function extractValidators(appRoot, vfs, controller) {
85
+ const root = await vfs.get(controller.path);
86
+ const fileContents = root.text();
87
+ const controllerClass = inspectClass(root);
88
+ if (!controllerClass) {
89
+ debug_default(`No class defined within the "%s"`, controller.import.specifier);
90
+ return;
91
+ }
92
+ const method = inspectClassMethods(controllerClass).find((e) => {
93
+ return e.find({
94
+ rule: { kind: "property_identifier", regex: `\\b${controller.method}\\b` }
95
+ });
96
+ });
97
+ if (!method) {
98
+ debug_default(`Unable to find "%s" method in "%s"`, controller.method, controller.import.specifier);
99
+ return;
100
+ }
101
+ const validationCalls = inspectMethodArguments(method, ["request.validateUsing", "vine.validate"]).map((node) => {
102
+ const firstArg = node.find({
103
+ rule: { any: [{ kind: "identifier" }, { kind: "member_expression" }] }
104
+ });
105
+ if (!firstArg) {
106
+ return;
107
+ }
108
+ return nodeToPlainText(firstArg);
109
+ }).filter((node) => node !== void 0).concat(searchValidatorDirectUsage(method).map((node) => nodeToPlainText(node)));
110
+ if (!validationCalls.length) {
111
+ debug_default(
112
+ `Unable to detect any validation calls in "%s.%s" method`,
113
+ controller.import.specifier,
114
+ controller.method
115
+ );
116
+ return;
117
+ }
118
+ const controllerName = controllerClass.find({ rule: { kind: "type_identifier" } }).text();
119
+ const controllerURL = pathToFileURL2(controller.path);
120
+ const imports = await Promise.all(
121
+ validationCalls.map(async (validationCall) => {
122
+ const validatorNamespace = validationCall.split(".")[0];
123
+ if (validatorNamespace === controllerName) {
124
+ return {
125
+ name: validationCall,
126
+ import: {
127
+ specifier: controller.import.specifier,
128
+ type: "default",
129
+ value: controllerName
130
+ }
131
+ };
132
+ }
133
+ const importCall = await findImport(fileContents, validationCall);
134
+ if (!importCall) {
135
+ debug_default(
136
+ 'Unable to find import for "%s" used by "%s.%s" method',
137
+ validationCall,
138
+ controller.import.specifier,
139
+ controller.method
140
+ );
141
+ return null;
142
+ }
143
+ return {
144
+ name: validationCall,
145
+ import: {
146
+ specifier: isRelative(importCall.specifier) ? string.toUnixSlash(
147
+ relative(appRoot, fileURLToPath2(new URL(importCall.specifier, controllerURL)))
148
+ ) : importCall.specifier,
149
+ type: importCall.clause.type,
150
+ value: importCall.clause.value
151
+ }
152
+ };
153
+ })
154
+ );
155
+ return imports.filter((value) => !!value);
156
+ }
157
+
158
+ // src/code_scanners/routes_scanner/main.ts
159
+ var RoutesScanner = class {
160
+ /**
161
+ * The root of the application from where we will resolve
162
+ * paths.
163
+ */
164
+ #appRoot;
165
+ /**
166
+ * Collection of routes by their controller file path. This helps
167
+ * us re-compute request types when the controller is changed.
168
+ */
169
+ #controllerRoutes = {};
170
+ /**
171
+ * Collection of scanned routes
172
+ */
173
+ #scannedRoutes = [];
174
+ /**
175
+ * A custom method to self compute the response type for a route. Return
176
+ * undefined to fallback to the default behavior
177
+ */
178
+ #computeResponseTypes;
179
+ /**
180
+ * A custom method to self compute the request type for a route. Return
181
+ * undefined to fallback to the default behavior
182
+ */
183
+ #computeRequestTypes;
184
+ /**
185
+ * A custom method to self extract the validators from the route controller.
186
+ * Return undefined to fallback to the default behavior.
187
+ */
188
+ #extractValidators;
189
+ /**
190
+ * CLI UI instance to log colorful messages and progress information
191
+ */
192
+ ui = cliui();
193
+ /**
194
+ * The paths resolver is used to convert subpath and package
195
+ * imports to absolute paths
196
+ */
197
+ pathsResolver;
198
+ /**
199
+ * The rules to apply when scanning routes
200
+ */
201
+ rules = {
202
+ skip: [],
203
+ request: {},
204
+ response: {}
205
+ };
206
+ /**
207
+ * Create a new RoutesScanner instance
208
+ *
209
+ * @param appRoot - The root directory of the application
210
+ * @param rulesCollection - Collection of rules to apply during scanning
211
+ */
212
+ constructor(appRoot, rulesCollection) {
213
+ this.#appRoot = appRoot;
214
+ this.pathsResolver = new PathsResolver(appRoot);
215
+ rulesCollection.forEach((rules) => {
216
+ this.rules.skip = this.rules.skip.concat(rules.skip);
217
+ Object.assign(this.rules.request, rules.request);
218
+ Object.assign(this.rules.response, rules.response);
219
+ });
220
+ this.rules.skip = Array.from(/* @__PURE__ */ new Set([...this.rules.skip]));
221
+ }
222
+ /**
223
+ * Assumes the validators are from VineJS and computes the request types from them
224
+ *
225
+ * This method generates TypeScript type definitions for request validation
226
+ * by analyzing VineJS validators and creating Infer types.
227
+ *
228
+ * @param route - The scanned route with validators
229
+ * @returns Request type definition or undefined
230
+ */
231
+ #prepareRequestTypes(route) {
232
+ if (!route.validators) {
233
+ return;
234
+ }
235
+ const schemas = route.validators.reduce((result, validator) => {
236
+ const validatorExport = validator.import.type === "default" ? ".default" : validator.import.type === "named" ? `.${validator.import.value}` : "";
237
+ const [, ...segments] = validator.name.split(".");
238
+ const namespace = segments.map((segment) => `['${segment}']`).join("");
239
+ result.push(
240
+ `Infer<(typeof import('${validator.import.specifier}')${validatorExport})${namespace}>`
241
+ );
242
+ return result;
243
+ }, []);
244
+ return {
245
+ type: schemas.join("|"),
246
+ imports: [`import { Infer } from '@vinejs/vine/types'`]
247
+ };
248
+ }
249
+ /**
250
+ * Inspects the controller reference and fetches its import specifier
251
+ * and the controller name from it.
252
+ */
253
+ async #inspectControllerSpecifier(importExpression, method) {
254
+ const imports = [...await parseImports(importExpression)];
255
+ const importedModule = imports.find(
256
+ ($import) => $import.isDynamicImport && $import.moduleSpecifier.value
257
+ );
258
+ if (!importedModule || importedModule.moduleSpecifier.type !== "package" || !importedModule.moduleSpecifier.value) {
259
+ return null;
260
+ }
261
+ const specifier = importedModule.moduleSpecifier.value;
262
+ const name = new StringBuilder(specifier.split("/").pop()).removeSuffix("Controller").pascalCase().suffix("Controller").toString();
263
+ return {
264
+ name,
265
+ method: method ?? "handle",
266
+ path: string2.toUnixSlash(this.pathsResolver.resolve(specifier)),
267
+ import: {
268
+ specifier,
269
+ type: "default",
270
+ value: name
271
+ }
272
+ };
273
+ }
274
+ /**
275
+ * Defines the response type for the route
276
+ */
277
+ async #setResponse(route, controller) {
278
+ const responseType = await this.#computeResponseTypes?.(route, controller, this);
279
+ route.response = responseType ?? {
280
+ type: `ReturnType<import('${controller.import.specifier}').default['${controller.method}']>`,
281
+ imports: []
282
+ };
283
+ debug_default('computed route "%s" response %O', route.name, route.response);
284
+ }
285
+ /**
286
+ * Defines the request type for the route
287
+ */
288
+ async #setRequest(route, controller, vfs) {
289
+ route.validators = await this.#extractValidators?.(route, controller, this) ?? await extractValidators(this.#appRoot, vfs, controller);
290
+ debug_default('computed route "%s" validators %O', route.name, route.validators);
291
+ route.request = await this.#computeRequestTypes?.(route, controller, this) ?? this.#prepareRequestTypes(route);
292
+ debug_default('computed route "%s" request input %O', route.name, route.request);
293
+ }
294
+ /**
295
+ * Scans a route that is not using a controller
296
+ */
297
+ #processRouteWithoutController(route) {
298
+ if (!route.name) {
299
+ debug_default(`skipping route "%s" as it does not have a name`, route.name);
300
+ return;
301
+ }
302
+ const scannedRoute = {
303
+ name: route.name,
304
+ domain: route.domain,
305
+ methods: route.methods,
306
+ pattern: route.pattern,
307
+ tokens: route.tokens,
308
+ request: this.rules.request[route.name],
309
+ response: this.rules.response[route.name]
310
+ };
311
+ debug_default("scanned route without controller %O", scannedRoute);
312
+ this.#scannedRoutes.push(scannedRoute);
313
+ }
314
+ /**
315
+ * Scans a route that is using a controller reference
316
+ */
317
+ async #processRouteWithController(route, vfs) {
318
+ if (!route.handler.importExpression) {
319
+ return this.#processRouteWithoutController(route);
320
+ }
321
+ const controller = await this.#inspectControllerSpecifier(
322
+ route.handler.importExpression,
323
+ route.handler.method
324
+ );
325
+ if (!controller) {
326
+ return this.#processRouteWithoutController(route);
327
+ }
328
+ debug_default('processing route "%s" with inspected controller %O', route.name, controller);
329
+ const routeName = route.name ?? new StringBuilder(controller.name).removeSuffix("Controller").snakeCase().suffix(`.${string2.snakeCase(controller.method)}`).toString();
330
+ if (this.rules.skip.includes(routeName)) {
331
+ return;
332
+ }
333
+ const scannedRoute = {
334
+ name: routeName,
335
+ domain: route.domain,
336
+ methods: route.methods,
337
+ pattern: route.pattern,
338
+ tokens: route.tokens,
339
+ request: this.rules.request[routeName],
340
+ response: this.rules.response[routeName],
341
+ controller
342
+ };
343
+ debug_default("scanned route %O", scannedRoute);
344
+ this.#scannedRoutes.push(scannedRoute);
345
+ if (!scannedRoute.request || !scannedRoute.response) {
346
+ debug_default("tracking controller for rescanning %O", scannedRoute);
347
+ this.#controllerRoutes[controller.path] ??= [];
348
+ this.#controllerRoutes[controller.path].push(scannedRoute);
349
+ if (!scannedRoute.response) {
350
+ await this.#setResponse(scannedRoute, controller);
351
+ }
352
+ if (!scannedRoute.request) {
353
+ await this.#setRequest(scannedRoute, controller, vfs);
354
+ }
355
+ }
356
+ }
357
+ /**
358
+ * Processing a given route list item and further scan it to
359
+ * fetch the controller, request and response types.
360
+ */
361
+ async #processRoute(route, vfs) {
362
+ if (route.name && this.rules.skip.includes(route.name)) {
363
+ debug_default("route skipped route: %O, rules: %O", route, this.rules);
364
+ return;
365
+ }
366
+ if (typeof route.handler === "function") {
367
+ this.#processRouteWithoutController(route);
368
+ return;
369
+ }
370
+ await this.#processRouteWithController(
371
+ route,
372
+ vfs
373
+ );
374
+ }
375
+ /**
376
+ * Register a callback to self compute the response types for
377
+ * a given route. The callback will be executed for all
378
+ * the routes and you must return undefined to fallback
379
+ * to the default behavior of detecting types
380
+ *
381
+ * @param callback - Function to compute response types for routes
382
+ * @returns This RoutesScanner instance for method chaining
383
+ */
384
+ defineResponse(callback) {
385
+ this.#computeResponseTypes = callback;
386
+ return this;
387
+ }
388
+ /**
389
+ * Register a callback to self compute the request types for
390
+ * a given route. The callback will be executed for all
391
+ * the routes and you must return undefined to fallback
392
+ * to the default behavior of detecting types
393
+ *
394
+ * @param callback - Function to compute request types for routes
395
+ * @returns This RoutesScanner instance for method chaining
396
+ */
397
+ defineRequest(callback) {
398
+ this.#computeRequestTypes = callback;
399
+ return this;
400
+ }
401
+ /**
402
+ * Register a callback to extract validators from the route controller
403
+ *
404
+ * @param callback - Function to extract validators from controllers
405
+ * @returns This RoutesScanner instance for method chaining
406
+ */
407
+ extractValidators(callback) {
408
+ this.#extractValidators = callback;
409
+ return this;
410
+ }
411
+ /**
412
+ * Returns the scanned routes
413
+ *
414
+ * @returns Array of scanned routes with their metadata
415
+ */
416
+ getScannedRoutes() {
417
+ return this.#scannedRoutes;
418
+ }
419
+ /**
420
+ * Returns an array of controllers bound to the provided
421
+ * routes
422
+ *
423
+ * @returns Array of controller's absolute paths
424
+ */
425
+ getControllers() {
426
+ return Object.keys(this.#controllerRoutes);
427
+ }
428
+ /**
429
+ * Invalidating a controller will trigger computing the validators,
430
+ * request types and the response types.
431
+ *
432
+ * @param controllerPath - Path to the controller file to invalidate
433
+ */
434
+ async invalidate(controllerPath) {
435
+ const controllerRoutes = this.#controllerRoutes[controllerPath];
436
+ if (!controllerRoutes || !controllerRoutes.length) {
437
+ debug_default(
438
+ '"%s" controllers is not part of scanned controllers %O',
439
+ controllerPath,
440
+ this.#controllerRoutes
441
+ );
442
+ return false;
443
+ }
444
+ for (let scannedRoute of controllerRoutes) {
445
+ if (scannedRoute.controller) {
446
+ debug_default("invalidating route %O", scannedRoute);
447
+ const vfs = new VirtualFileSystem(this.#appRoot);
448
+ await this.#setResponse(scannedRoute, scannedRoute.controller);
449
+ await this.#setRequest(scannedRoute, scannedRoute.controller, vfs);
450
+ }
451
+ }
452
+ return true;
453
+ }
454
+ /**
455
+ * Scans an array of Route list items and fetches their validators,
456
+ * controllers, and request/response types.
457
+ *
458
+ * This is the main method that processes all routes and extracts
459
+ * their type information for code generation purposes.
460
+ *
461
+ * @param routes - Array of route list items to scan
462
+ */
463
+ async scan(routes) {
464
+ const vfs = new VirtualFileSystem(this.#appRoot);
465
+ for (const route of routes) {
466
+ await this.#processRoute(route, vfs);
467
+ }
468
+ vfs.invalidate();
469
+ }
470
+ };
471
+
472
+ export {
473
+ RoutesScanner
474
+ };
@@ -3,7 +3,7 @@ import {
3
3
  debug_default,
4
4
  removeExtension,
5
5
  throttle
6
- } from "./chunk-7ANGUQDV.js";
6
+ } from "./chunk-3GYRM5Y2.js";
7
7
 
8
8
  // src/index_generator/source.ts
9
9
  import string from "@poppinss/utils/string";
@@ -325,15 +325,15 @@ var IndexGeneratorSource = class {
325
325
  buffer.dedent().write(`}`);
326
326
  }
327
327
  /**
328
- * Create a log action for tracking file generation progress
328
+ * Displays the log message after generating the index file
329
329
  *
330
330
  * @example
331
- * const action = this.#createLogAction()
331
+ * const startTime = process.hrtime()
332
332
  * // ... perform operations
333
- * action.displayDuration().succeeded()
333
+ * this.#logCreation(startTime)
334
334
  */
335
- #createLogAction() {
336
- return this.#cliLogger.action(`create ${this.#config.output}`);
335
+ #logCreation(startTime) {
336
+ this.#cliLogger.info(`created ${this.#config.output}`, { startTime });
337
337
  }
338
338
  /**
339
339
  * Add a file to the virtual file system and regenerate index if needed
@@ -347,9 +347,9 @@ var IndexGeneratorSource = class {
347
347
  const added = this.#vfs.add(filePath);
348
348
  if (added) {
349
349
  debug_default('file added, re-generating "%s" index', this.name);
350
- const action = this.#createLogAction();
350
+ const startTime = process.hrtime();
351
351
  await this.#generateOutput();
352
- action.displayDuration().succeeded();
352
+ this.#logCreation(startTime);
353
353
  }
354
354
  }
355
355
  /**
@@ -364,9 +364,9 @@ var IndexGeneratorSource = class {
364
364
  const removed = this.#vfs.remove(filePath);
365
365
  if (removed) {
366
366
  debug_default('file removed, re-generating "%s" index', this.name);
367
- const action = this.#createLogAction();
367
+ const startTime = process.hrtime();
368
368
  await this.#generateOutput();
369
- action.displayDuration().succeeded();
369
+ this.#logCreation(startTime);
370
370
  }
371
371
  }
372
372
  /**
@@ -376,10 +376,10 @@ var IndexGeneratorSource = class {
376
376
  * the configuration, and writes the generated index file to disk.
377
377
  */
378
378
  async generate() {
379
- const action = this.#createLogAction();
379
+ const startTime = process.hrtime();
380
380
  await this.#vfs.scan();
381
381
  await this.#generateOutput();
382
- action.displayDuration().succeeded();
382
+ this.#logCreation(startTime);
383
383
  }
384
384
  };
385
385