@adonisjs/assembler 8.0.0-next.14 → 8.0.0-next.16

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.
@@ -26,6 +26,7 @@ import { importDefault } from "@poppinss/utils";
26
26
  import { copyFile, mkdir } from "fs/promises";
27
27
  import { EnvLoader, EnvParser } from "@adonisjs/env";
28
28
  import chokidar from "chokidar";
29
+ import { parseTsconfig } from "get-tsconfig";
29
30
  import { basename, dirname, isAbsolute, join, relative } from "path";
30
31
  var DEFAULT_NODE_ARGS = ["--import=@poppinss/ts-exec", "--enable-source-maps"];
31
32
  function parseConfig(cwd, ts) {
@@ -65,6 +66,29 @@ function parseConfig(cwd, ts) {
65
66
  }
66
67
  return parsedConfig;
67
68
  }
69
+ function readTsConfig(cwd) {
70
+ const tsConfigPath = join(cwd, "tsconfig.json");
71
+ debug_default('reading config file from location "%s"', tsConfigPath);
72
+ try {
73
+ const tsConfig = parseTsconfig(tsConfigPath);
74
+ if (tsConfig.include) {
75
+ tsConfig.include = tsConfig.include.map((resolvedPath) => {
76
+ return resolvedPath.replace(`${cwd}/`, "");
77
+ });
78
+ }
79
+ if (tsConfig.exclude) {
80
+ tsConfig.exclude = tsConfig.exclude.map((resolvedPath) => {
81
+ return resolvedPath.replace(`${cwd}/`, "");
82
+ });
83
+ }
84
+ return {
85
+ path: tsConfigPath,
86
+ config: tsConfig
87
+ };
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
68
92
  function runNode(cwd, options) {
69
93
  const childProcess = execaNode(options.script, options.scriptArgs, {
70
94
  nodeOptions: DEFAULT_NODE_ARGS.concat(options.nodeArgs),
@@ -184,8 +208,9 @@ function throttle(fn, name) {
184
208
  return;
185
209
  }
186
210
  isBusy = true;
187
- debug_default('executing "%s" function', name);
211
+ debug_default('executing throttled function "%s"', name);
188
212
  await fn(...args);
213
+ debug_default('executed throttled function "%s"', name);
189
214
  isBusy = false;
190
215
  if (hasQueuedCalls) {
191
216
  hasQueuedCalls = false;
@@ -243,6 +268,30 @@ var VirtualFileSystem = class {
243
268
  };
244
269
  this.#matcher = picomatch(this.#options.glob ?? DEFAULT_GLOB, this.#picoMatchOptions);
245
270
  }
271
+ /**
272
+ * Currently the PathsResolver relies on a non-standard way of resolving
273
+ * absolute paths or paths with subpath imports. Because of which, the
274
+ * on-disk file could be TypeScript, while the resolved filepath is
275
+ * JavaScript.
276
+ *
277
+ * To overcome this limitation, we start by first looking for a `.ts` file
278
+ * (because of high probability) and then look for `.js` file
279
+ */
280
+ async #readTSOrJSFile(filePath) {
281
+ if (filePath.endsWith(".js")) {
282
+ try {
283
+ const contents = await readFile(filePath.replace(/\.js$/, ".ts"), "utf-8");
284
+ debug_default('read as TypeScript file "%s"', filePath);
285
+ return contents;
286
+ } catch (error) {
287
+ if (error.code === "ENOENT") {
288
+ return readFile(filePath, "utf-8");
289
+ }
290
+ throw error;
291
+ }
292
+ }
293
+ return readFile(filePath, "utf-8");
294
+ }
246
295
  /**
247
296
  * Scans the filesystem to collect the files. Newly files must
248
297
  * be added via the ".add" method.
@@ -357,7 +406,7 @@ var VirtualFileSystem = class {
357
406
  debug_default('returning AST nodes from cache "%s"', filePath);
358
407
  return cached;
359
408
  }
360
- const fileContents = await readFile(filePath, "utf-8");
409
+ const fileContents = await this.#readTSOrJSFile(filePath);
361
410
  debug_default('parsing "%s" file to AST', filePath);
362
411
  this.#astCache.set(filePath, parse(Lang.TypeScript, fileContents).root());
363
412
  return this.#astCache.get(filePath);
@@ -371,8 +420,13 @@ var VirtualFileSystem = class {
371
420
  * @param filePath - Optional file path to clear. If omitted, clears entire cache
372
421
  */
373
422
  invalidate(filePath) {
374
- debug_default('invalidate AST cache "%s"', filePath);
375
- filePath ? this.#astCache.delete(filePath) : this.#astCache.clear();
423
+ if (filePath) {
424
+ debug_default('invalidate AST cache "%s"', filePath);
425
+ this.#astCache.delete(filePath);
426
+ } else {
427
+ debug_default("clear AST cache");
428
+ this.#astCache.clear();
429
+ }
376
430
  }
377
431
  /**
378
432
  * Clear all scanned files from memory
@@ -388,6 +442,7 @@ var VirtualFileSystem = class {
388
442
  export {
389
443
  debug_default,
390
444
  parseConfig,
445
+ readTsConfig,
391
446
  runNode,
392
447
  run,
393
448
  watch,
@@ -0,0 +1,475 @@
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-HRE5L24F.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 { resolve } from "import-meta-resolve";
24
+ import { fileURLToPath, pathToFileURL } from "url";
25
+ var PathsResolver = class {
26
+ /**
27
+ * The root of the application from where we will resolve
28
+ * paths.
29
+ */
30
+ #appRoot;
31
+ /**
32
+ * Cache of resolved paths to avoid resolving the same specifier multiple times
33
+ */
34
+ #resolvedPaths = {};
35
+ /**
36
+ * The resolver function used to resolve import specifiers
37
+ */
38
+ #resolver = (specifier, parentPath) => resolve(specifier, parentPath);
39
+ constructor(appRoot) {
40
+ this.#appRoot = pathToFileURL(join(appRoot, "index.js")).href;
41
+ }
42
+ /**
43
+ * Define a custom resolver that resolves a path
44
+ *
45
+ * The custom resolver function will be used instead of the default
46
+ * import.meta.resolve() for resolving import specifiers.
47
+ *
48
+ * @param resolver - Function that takes a specifier and returns resolved path
49
+ */
50
+ use(resolver) {
51
+ this.#resolver = resolver;
52
+ }
53
+ /**
54
+ * Resolve import specifier to an absolute file path
55
+ *
56
+ * This method caches resolved paths to improve performance on repeated
57
+ * resolutions. Relative paths are not supported and will throw an error.
58
+ *
59
+ * @param specifier - The import specifier to resolve (must not be relative)
60
+ * @returns The resolved absolute file path
61
+ * @throws Error when attempting to resolve relative paths
62
+ *
63
+ * @example
64
+ * const path = resolver.resolve('#app/models/user')
65
+ * const path2 = resolver.resolve('@/utils/helper')
66
+ */
67
+ resolve(specifier) {
68
+ if (isRelative(specifier)) {
69
+ throw new Error("Cannot resolve relative paths using PathsResolver");
70
+ }
71
+ const cacheKey = specifier;
72
+ const cached = this.#resolvedPaths[cacheKey];
73
+ if (cached) {
74
+ return cached;
75
+ }
76
+ this.#resolvedPaths[cacheKey] = fileURLToPath(this.#resolver(specifier, this.#appRoot));
77
+ return this.#resolvedPaths[cacheKey];
78
+ }
79
+ };
80
+
81
+ // src/code_scanners/routes_scanner/validator_extractor.ts
82
+ import { relative } from "path";
83
+ import string from "@poppinss/utils/string";
84
+ import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "url";
85
+ async function extractValidators(appRoot, vfs, controller) {
86
+ const root = await vfs.get(controller.path);
87
+ const fileContents = root.text();
88
+ const controllerClass = inspectClass(root);
89
+ if (!controllerClass) {
90
+ debug_default(`No class defined within the "%s"`, controller.import.specifier);
91
+ return;
92
+ }
93
+ const method = inspectClassMethods(controllerClass).find((e) => {
94
+ return e.find({
95
+ rule: { kind: "property_identifier", regex: `\\b${controller.method}\\b` }
96
+ });
97
+ });
98
+ if (!method) {
99
+ debug_default(`Unable to find "%s" method in "%s"`, controller.method, controller.import.specifier);
100
+ return;
101
+ }
102
+ const validationCalls = inspectMethodArguments(method, ["request.validateUsing", "vine.validate"]).map((node) => {
103
+ const firstArg = node.find({
104
+ rule: { any: [{ kind: "identifier" }, { kind: "member_expression" }] }
105
+ });
106
+ if (!firstArg) {
107
+ return;
108
+ }
109
+ return nodeToPlainText(firstArg);
110
+ }).filter((node) => node !== void 0).concat(searchValidatorDirectUsage(method).map((node) => nodeToPlainText(node)));
111
+ if (!validationCalls.length) {
112
+ debug_default(
113
+ `Unable to detect any validation calls in "%s.%s" method`,
114
+ controller.import.specifier,
115
+ controller.method
116
+ );
117
+ return;
118
+ }
119
+ const controllerName = controllerClass.find({ rule: { kind: "type_identifier" } }).text();
120
+ const controllerURL = pathToFileURL2(controller.path);
121
+ const imports = await Promise.all(
122
+ validationCalls.map(async (validationCall) => {
123
+ const validatorNamespace = validationCall.split(".")[0];
124
+ if (validatorNamespace === controllerName) {
125
+ return {
126
+ name: validationCall,
127
+ import: {
128
+ specifier: controller.import.specifier,
129
+ type: "default",
130
+ value: controllerName
131
+ }
132
+ };
133
+ }
134
+ const importCall = await findImport(fileContents, validationCall);
135
+ if (!importCall) {
136
+ debug_default(
137
+ 'Unable to find import for "%s" used by "%s.%s" method',
138
+ validationCall,
139
+ controller.import.specifier,
140
+ controller.method
141
+ );
142
+ return null;
143
+ }
144
+ return {
145
+ name: validationCall,
146
+ import: {
147
+ specifier: isRelative(importCall.specifier) ? string.toUnixSlash(
148
+ relative(appRoot, fileURLToPath2(new URL(importCall.specifier, controllerURL)))
149
+ ) : importCall.specifier,
150
+ type: importCall.clause.type,
151
+ value: importCall.clause.value
152
+ }
153
+ };
154
+ })
155
+ );
156
+ return imports.filter((value) => !!value);
157
+ }
158
+
159
+ // src/code_scanners/routes_scanner/main.ts
160
+ var RoutesScanner = class {
161
+ /**
162
+ * The root of the application from where we will resolve
163
+ * paths.
164
+ */
165
+ #appRoot;
166
+ /**
167
+ * Collection of routes by their controller file path. This helps
168
+ * us re-compute request types when the controller is changed.
169
+ */
170
+ #controllerRoutes = {};
171
+ /**
172
+ * Collection of scanned routes
173
+ */
174
+ #scannedRoutes = [];
175
+ /**
176
+ * A custom method to self compute the response type for a route. Return
177
+ * undefined to fallback to the default behavior
178
+ */
179
+ #computeResponseTypes;
180
+ /**
181
+ * A custom method to self compute the request type for a route. Return
182
+ * undefined to fallback to the default behavior
183
+ */
184
+ #computeRequestTypes;
185
+ /**
186
+ * A custom method to self extract the validators from the route controller.
187
+ * Return undefined to fallback to the default behavior.
188
+ */
189
+ #extractValidators;
190
+ /**
191
+ * CLI UI instance to log colorful messages and progress information
192
+ */
193
+ ui = cliui();
194
+ /**
195
+ * The paths resolver is used to convert subpath and package
196
+ * imports to absolute paths
197
+ */
198
+ pathsResolver;
199
+ /**
200
+ * The rules to apply when scanning routes
201
+ */
202
+ rules = {
203
+ skip: [],
204
+ request: {},
205
+ response: {}
206
+ };
207
+ /**
208
+ * Create a new RoutesScanner instance
209
+ *
210
+ * @param appRoot - The root directory of the application
211
+ * @param rulesCollection - Collection of rules to apply during scanning
212
+ */
213
+ constructor(appRoot, rulesCollection) {
214
+ this.#appRoot = appRoot;
215
+ this.pathsResolver = new PathsResolver(appRoot);
216
+ rulesCollection.forEach((rules) => {
217
+ this.rules.skip = this.rules.skip.concat(rules.skip);
218
+ Object.assign(this.rules.request, rules.request);
219
+ Object.assign(this.rules.response, rules.response);
220
+ });
221
+ this.rules.skip = Array.from(/* @__PURE__ */ new Set([...this.rules.skip]));
222
+ }
223
+ /**
224
+ * Assumes the validators are from VineJS and computes the request types from them
225
+ *
226
+ * This method generates TypeScript type definitions for request validation
227
+ * by analyzing VineJS validators and creating Infer types.
228
+ *
229
+ * @param route - The scanned route with validators
230
+ * @returns Request type definition or undefined
231
+ */
232
+ #prepareRequestTypes(route) {
233
+ if (!route.validators) {
234
+ return;
235
+ }
236
+ const schemas = route.validators.reduce((result, validator) => {
237
+ const validatorExport = validator.import.type === "default" ? ".default" : validator.import.type === "named" ? `.${validator.import.value}` : "";
238
+ const [, ...segments] = validator.name.split(".");
239
+ const namespace = segments.map((segment) => `['${segment}']`).join("");
240
+ result.push(
241
+ `Infer<(typeof import('${validator.import.specifier}')${validatorExport})${namespace}>`
242
+ );
243
+ return result;
244
+ }, []);
245
+ return {
246
+ type: schemas.join("|"),
247
+ imports: [`import { Infer } from '@vinejs/vine/types'`]
248
+ };
249
+ }
250
+ /**
251
+ * Inspects the controller reference and fetches its import specifier
252
+ * and the controller name from it.
253
+ */
254
+ async #inspectControllerSpecifier(importExpression, method) {
255
+ const imports = [...await parseImports(importExpression)];
256
+ const importedModule = imports.find(
257
+ ($import) => $import.isDynamicImport && $import.moduleSpecifier.value
258
+ );
259
+ if (!importedModule || importedModule.moduleSpecifier.type !== "package" || !importedModule.moduleSpecifier.value) {
260
+ return null;
261
+ }
262
+ const specifier = importedModule.moduleSpecifier.value;
263
+ const name = new StringBuilder(specifier.split("/").pop()).removeSuffix("Controller").pascalCase().suffix("Controller").toString();
264
+ return {
265
+ name,
266
+ method: method ?? "handle",
267
+ path: string2.toUnixSlash(this.pathsResolver.resolve(specifier)),
268
+ import: {
269
+ specifier,
270
+ type: "default",
271
+ value: name
272
+ }
273
+ };
274
+ }
275
+ /**
276
+ * Defines the response type for the route
277
+ */
278
+ async #setResponse(route, controller) {
279
+ const responseType = await this.#computeResponseTypes?.(route, controller, this);
280
+ route.response = responseType ?? {
281
+ type: `ReturnType<import('${controller.import.specifier}').default['${controller.method}']>`,
282
+ imports: []
283
+ };
284
+ debug_default('computed route "%s" response %O', route.name, route.response);
285
+ }
286
+ /**
287
+ * Defines the request type for the route
288
+ */
289
+ async #setRequest(route, controller, vfs) {
290
+ route.validators = await this.#extractValidators?.(route, controller, this) ?? await extractValidators(this.#appRoot, vfs, controller);
291
+ debug_default('computed route "%s" validators %O', route.name, route.validators);
292
+ route.request = await this.#computeRequestTypes?.(route, controller, this) ?? this.#prepareRequestTypes(route);
293
+ debug_default('computed route "%s" request input %O', route.name, route.request);
294
+ }
295
+ /**
296
+ * Scans a route that is not using a controller
297
+ */
298
+ #processRouteWithoutController(route) {
299
+ if (!route.name) {
300
+ debug_default(`skipping route "%s" as it does not have a name`, route.name);
301
+ return;
302
+ }
303
+ const scannedRoute = {
304
+ name: route.name,
305
+ domain: route.domain,
306
+ methods: route.methods,
307
+ pattern: route.pattern,
308
+ tokens: route.tokens,
309
+ request: this.rules.request[route.name],
310
+ response: this.rules.response[route.name]
311
+ };
312
+ debug_default("scanned route without controller %O", scannedRoute);
313
+ this.#scannedRoutes.push(scannedRoute);
314
+ }
315
+ /**
316
+ * Scans a route that is using a controller reference
317
+ */
318
+ async #processRouteWithController(route, vfs) {
319
+ if (!route.handler.importExpression) {
320
+ return this.#processRouteWithoutController(route);
321
+ }
322
+ const controller = await this.#inspectControllerSpecifier(
323
+ route.handler.importExpression,
324
+ route.handler.method
325
+ );
326
+ if (!controller) {
327
+ return this.#processRouteWithoutController(route);
328
+ }
329
+ debug_default('processing route "%s" with inspected controller %O', route.name, controller);
330
+ const routeName = route.name ?? new StringBuilder(controller.name).removeSuffix("Controller").snakeCase().suffix(`.${string2.snakeCase(controller.method)}`).toString();
331
+ if (this.rules.skip.includes(routeName)) {
332
+ return;
333
+ }
334
+ const scannedRoute = {
335
+ name: routeName,
336
+ domain: route.domain,
337
+ methods: route.methods,
338
+ pattern: route.pattern,
339
+ tokens: route.tokens,
340
+ request: this.rules.request[routeName],
341
+ response: this.rules.response[routeName],
342
+ controller
343
+ };
344
+ debug_default("scanned route %O", scannedRoute);
345
+ this.#scannedRoutes.push(scannedRoute);
346
+ if (!scannedRoute.request || !scannedRoute.response) {
347
+ debug_default("tracking controller for rescanning %O", scannedRoute);
348
+ this.#controllerRoutes[controller.path] ??= [];
349
+ this.#controllerRoutes[controller.path].push(scannedRoute);
350
+ if (!scannedRoute.response) {
351
+ await this.#setResponse(scannedRoute, controller);
352
+ }
353
+ if (!scannedRoute.request) {
354
+ await this.#setRequest(scannedRoute, controller, vfs);
355
+ }
356
+ }
357
+ }
358
+ /**
359
+ * Processing a given route list item and further scan it to
360
+ * fetch the controller, request and response types.
361
+ */
362
+ async #processRoute(route, vfs) {
363
+ if (route.name && this.rules.skip.includes(route.name)) {
364
+ debug_default("route skipped route: %O, rules: %O", route, this.rules);
365
+ return;
366
+ }
367
+ if (typeof route.handler === "function") {
368
+ this.#processRouteWithoutController(route);
369
+ return;
370
+ }
371
+ await this.#processRouteWithController(
372
+ route,
373
+ vfs
374
+ );
375
+ }
376
+ /**
377
+ * Register a callback to self compute the response types for
378
+ * a given route. The callback will be executed for all
379
+ * the routes and you must return undefined to fallback
380
+ * to the default behavior of detecting types
381
+ *
382
+ * @param callback - Function to compute response types for routes
383
+ * @returns This RoutesScanner instance for method chaining
384
+ */
385
+ defineResponse(callback) {
386
+ this.#computeResponseTypes = callback;
387
+ return this;
388
+ }
389
+ /**
390
+ * Register a callback to self compute the request types for
391
+ * a given route. The callback will be executed for all
392
+ * the routes and you must return undefined to fallback
393
+ * to the default behavior of detecting types
394
+ *
395
+ * @param callback - Function to compute request types for routes
396
+ * @returns This RoutesScanner instance for method chaining
397
+ */
398
+ defineRequest(callback) {
399
+ this.#computeRequestTypes = callback;
400
+ return this;
401
+ }
402
+ /**
403
+ * Register a callback to extract validators from the route controller
404
+ *
405
+ * @param callback - Function to extract validators from controllers
406
+ * @returns This RoutesScanner instance for method chaining
407
+ */
408
+ extractValidators(callback) {
409
+ this.#extractValidators = callback;
410
+ return this;
411
+ }
412
+ /**
413
+ * Returns the scanned routes
414
+ *
415
+ * @returns Array of scanned routes with their metadata
416
+ */
417
+ getScannedRoutes() {
418
+ return this.#scannedRoutes;
419
+ }
420
+ /**
421
+ * Returns an array of controllers bound to the provided
422
+ * routes
423
+ *
424
+ * @returns Array of controller's absolute paths
425
+ */
426
+ getControllers() {
427
+ return Object.keys(this.#controllerRoutes);
428
+ }
429
+ /**
430
+ * Invalidating a controller will trigger computing the validators,
431
+ * request types and the response types.
432
+ *
433
+ * @param controllerPath - Path to the controller file to invalidate
434
+ */
435
+ async invalidate(controllerPath) {
436
+ const controllerRoutes = this.#controllerRoutes[controllerPath];
437
+ if (!controllerRoutes || !controllerRoutes.length) {
438
+ debug_default(
439
+ '"%s" controllers is not part of scanned controllers %O',
440
+ controllerPath,
441
+ this.#controllerRoutes
442
+ );
443
+ return false;
444
+ }
445
+ for (let scannedRoute of controllerRoutes) {
446
+ if (scannedRoute.controller) {
447
+ debug_default("invalidating route %O", scannedRoute);
448
+ const vfs = new VirtualFileSystem(this.#appRoot);
449
+ await this.#setResponse(scannedRoute, scannedRoute.controller);
450
+ await this.#setRequest(scannedRoute, scannedRoute.controller, vfs);
451
+ }
452
+ }
453
+ return true;
454
+ }
455
+ /**
456
+ * Scans an array of Route list items and fetches their validators,
457
+ * controllers, and request/response types.
458
+ *
459
+ * This is the main method that processes all routes and extracts
460
+ * their type information for code generation purposes.
461
+ *
462
+ * @param routes - Array of route list items to scan
463
+ */
464
+ async scan(routes) {
465
+ const vfs = new VirtualFileSystem(this.#appRoot);
466
+ for (const route of routes) {
467
+ await this.#processRoute(route, vfs);
468
+ }
469
+ vfs.invalidate();
470
+ }
471
+ };
472
+
473
+ export {
474
+ RoutesScanner
475
+ };
@@ -3,7 +3,7 @@ import {
3
3
  debug_default,
4
4
  removeExtension,
5
5
  throttle
6
- } from "./chunk-7ANGUQDV.js";
6
+ } from "./chunk-HRE5L24F.js";
7
7
 
8
8
  // src/index_generator/source.ts
9
9
  import string from "@poppinss/utils/string";