@adonisjs/assembler 8.0.0-next.30 → 8.0.0-next.31

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/README.md CHANGED
@@ -420,6 +420,266 @@ export const policies = {
420
420
  }
421
421
  ```
422
422
 
423
+ ### addValidator
424
+ Create a new validator file or add a validator to an existing file. If the file does not exist, it will be created with the provided contents. If it exists and the export name is not already defined, the validator will be appended to the file.
425
+
426
+ > [!IMPORTANT]
427
+ > This codemod respects the `validators` directory configured in `adonisrc.ts` and defaults to `app/validators`.
428
+
429
+ ```ts
430
+ const transformer = new CodeTransformer(appRoot)
431
+
432
+ try {
433
+ await transformer.addValidator({
434
+ validatorFileName: 'user.ts',
435
+ exportName: 'loginValidator',
436
+ contents: `export const loginValidator = vine.compile(
437
+ vine.object({
438
+ email: vine.string().email(),
439
+ password: vine.string().minLength(8)
440
+ })
441
+ )`
442
+ })
443
+ } catch (error) {
444
+ console.error('Unable to add validator')
445
+ console.error(error)
446
+ }
447
+ ```
448
+
449
+ Output (app/validators/user.ts)
450
+
451
+ ```ts
452
+ export const loginValidator = vine.compile(
453
+ vine.object({
454
+ email: vine.string().email(),
455
+ password: vine.string().minLength(8)
456
+ })
457
+ )
458
+ ```
459
+
460
+ ### addLimiter
461
+ Create a new rate limiter file or add a limiter to an existing file. If the file does not exist, it will be created with the provided contents. If it exists and the export name is not already defined, the limiter will be appended to the file.
462
+
463
+ > [!IMPORTANT]
464
+ > Limiters are created in the `start` directory configured in `adonisrc.ts` and defaults to `start`.
465
+
466
+ ```ts
467
+ const transformer = new CodeTransformer(appRoot)
468
+
469
+ try {
470
+ await transformer.addLimiter({
471
+ limiterFileName: 'limiters.ts',
472
+ exportName: 'apiThrottle',
473
+ contents: `export const apiThrottle = limiter.define('api', () => {
474
+ return limiter.allowRequests(10).every('1 minute')
475
+ })`
476
+ })
477
+ } catch (error) {
478
+ console.error('Unable to add limiter')
479
+ console.error(error)
480
+ }
481
+ ```
482
+
483
+ Output (start/limiters.ts)
484
+
485
+ ```ts
486
+ export const apiThrottle = limiter.define('api', () => {
487
+ return limiter.allowRequests(10).every('1 minute')
488
+ })
489
+ ```
490
+
491
+ ### addModelMixins
492
+ Apply one or more mixins to a model class. This wraps the model's extends clause with the `compose` helper and applies the specified mixins.
493
+
494
+ > [!IMPORTANT]
495
+ > This codemod expects the model file to exist with a default exported class that extends a base class.
496
+
497
+ ```ts
498
+ const transformer = new CodeTransformer(appRoot)
499
+
500
+ try {
501
+ await transformer.addModelMixins('user.ts', [
502
+ {
503
+ name: 'SoftDeletes',
504
+ importPath: '@adonisjs/lucid/orm/mixins/soft_deletes',
505
+ importType: 'named'
506
+ },
507
+ {
508
+ name: 'Sluggable',
509
+ importPath: '#mixins/sluggable',
510
+ importType: 'default',
511
+ args: ['title', '{ strategy: "dbIncrement" }']
512
+ }
513
+ ])
514
+ } catch (error) {
515
+ console.error('Unable to add mixins to model')
516
+ console.error(error)
517
+ }
518
+ ```
519
+
520
+ Input (app/models/user.ts)
521
+
522
+ ```ts
523
+ import { BaseModel } from '@adonisjs/lucid/orm'
524
+
525
+ export default class User extends BaseModel {
526
+ // ...
527
+ }
528
+ ```
529
+
530
+ Output (app/models/user.ts)
531
+
532
+ ```ts
533
+ import { BaseModel } from '@adonisjs/lucid/orm'
534
+ import { compose } from '@adonisjs/core/helpers'
535
+ import { SoftDeletes } from '@adonisjs/lucid/orm/mixins/soft_deletes'
536
+ import Sluggable from '#mixins/sluggable'
537
+
538
+ export default class User extends compose(BaseModel, SoftDeletes(), Sluggable(title, { strategy: "dbIncrement" })) {
539
+ // ...
540
+ }
541
+ ```
542
+
543
+ ### addControllerMethod
544
+ Create a new controller file or add a method to an existing controller class. If the controller file does not exist, it will be created with the class and method. If it exists and the method is not already defined, the method will be added to the class.
545
+
546
+ > [!IMPORTANT]
547
+ > This codemod respects the `controllers` directory configured in `adonisrc.ts` and defaults to `app/controllers`.
548
+
549
+ ```ts
550
+ const transformer = new CodeTransformer(appRoot)
551
+
552
+ try {
553
+ await transformer.addControllerMethod({
554
+ controllerFileName: 'users_controller.ts',
555
+ className: 'UsersController',
556
+ name: 'destroy',
557
+ contents: `async destroy({ params, response }: HttpContext) {
558
+ const user = await User.findOrFail(params.id)
559
+ await user.delete()
560
+ return response.noContent()
561
+ }`,
562
+ imports: [
563
+ { isType: false, isNamed: true, name: 'HttpContext', path: '@adonisjs/core/http' },
564
+ { isType: false, isNamed: false, name: 'User', path: '#models/user' }
565
+ ]
566
+ })
567
+ } catch (error) {
568
+ console.error('Unable to add controller method')
569
+ console.error(error)
570
+ }
571
+ ```
572
+
573
+ Output (app/controllers/users_controller.ts)
574
+
575
+ ```ts
576
+ import type { HttpContext } from '@adonisjs/core/http'
577
+ import User from '#models/user'
578
+
579
+ export default class UsersController {
580
+ async destroy({ params, response }: HttpContext) {
581
+ const user = await User.findOrFail(params.id)
582
+ await user.delete()
583
+ return response.noContent()
584
+ }
585
+ }
586
+ ```
587
+
588
+ ### RcFileTransformer additional methods
589
+
590
+ The `RcFileTransformer` class (accessible via `updateRcFile` callback) now supports additional methods for managing imports and hooks.
591
+
592
+ #### addNamedImport
593
+ Add a named import to the `adonisrc.ts` file.
594
+
595
+ ```ts
596
+ const transformer = new CodeTransformer(appRoot)
597
+
598
+ try {
599
+ await transformer.updateRcFile((rcFile) => {
600
+ rcFile.addNamedImport('@adonisjs/core/types', ['Middleware', 'Provider'])
601
+ })
602
+ } catch (error) {
603
+ console.error('Unable to add named import')
604
+ console.error(error)
605
+ }
606
+ ```
607
+
608
+ Output
609
+
610
+ ```ts
611
+ import { defineConfig } from '@adonisjs/core/app'
612
+ import { Middleware, Provider } from '@adonisjs/core/types'
613
+
614
+ export default defineConfig({
615
+ // ...
616
+ })
617
+ ```
618
+
619
+ #### addDefaultImport
620
+ Add a default import to the `adonisrc.ts` file.
621
+
622
+ ```ts
623
+ const transformer = new CodeTransformer(appRoot)
624
+
625
+ try {
626
+ await transformer.updateRcFile((rcFile) => {
627
+ rcFile.addDefaultImport('#config/database', 'databaseConfig')
628
+ })
629
+ } catch (error) {
630
+ console.error('Unable to add default import')
631
+ console.error(error)
632
+ }
633
+ ```
634
+
635
+ Output
636
+
637
+ ```ts
638
+ import { defineConfig } from '@adonisjs/core/app'
639
+ import databaseConfig from '#config/database'
640
+
641
+ export default defineConfig({
642
+ // ...
643
+ })
644
+ ```
645
+
646
+ #### addAssemblerHook
647
+ Add assembler hooks to the `adonisrc.ts` file. Hooks can be added as thunk imports (lazy loaded) or as raw values for direct import references.
648
+
649
+ ```ts
650
+ const transformer = new CodeTransformer(appRoot)
651
+
652
+ try {
653
+ await transformer.updateRcFile((rcFile) => {
654
+ // Add a thunk-style hook (lazy import)
655
+ rcFile.addAssemblerHook('onBuildStarting', './commands/build_hook.js')
656
+
657
+ // Add a raw hook (direct import reference)
658
+ rcFile.addAssemblerHook('onBuildCompleted', 'buildCompletedHook', true)
659
+ })
660
+ } catch (error) {
661
+ console.error('Unable to add assembler hook')
662
+ console.error(error)
663
+ }
664
+ ```
665
+
666
+ Output
667
+
668
+ ```ts
669
+ import { defineConfig } from '@adonisjs/core/app'
670
+
671
+ export default defineConfig({
672
+ hooks: {
673
+ onBuildStarting: [
674
+ () => import('./commands/build_hook.js')
675
+ ],
676
+ onBuildCompleted: [
677
+ buildCompletedHook
678
+ ]
679
+ }
680
+ })
681
+ ```
682
+
423
683
  ## Index generator
424
684
 
425
685
  The `IndexGenerator` is a core concept in Assembler that is used to watch the filesystem and create barrel files or types from a source directory.
@@ -28,6 +28,7 @@ export declare class Bundler {
28
28
  logger: import("@poppinss/cliui").Logger;
29
29
  table: (tableOptions?: Partial<import("@poppinss/cliui/types").TableOptions>) => import("@poppinss/cliui").Table;
30
30
  tasks: (tasksOptions?: Partial<import("@poppinss/cliui/types").TaskManagerOptions>) => import("@poppinss/cliui").TaskManager;
31
+ steps: () => import("@poppinss/cliui").Steps;
31
32
  icons: {
32
33
  tick: string;
33
34
  cross: string;
@@ -37,6 +38,7 @@ export declare class Bundler {
37
38
  info: string;
38
39
  warning: string;
39
40
  squareSmallFilled: string;
41
+ borderVertical: string;
40
42
  };
41
43
  sticker: () => import("@poppinss/cliui").Instructions;
42
44
  instructions: () => import("@poppinss/cliui").Instructions;
@@ -27,6 +27,7 @@ export declare class RoutesScanner {
27
27
  logger: import("@poppinss/cliui").Logger;
28
28
  table: (tableOptions?: Partial<import("@poppinss/cliui/types").TableOptions>) => import("@poppinss/cliui").Table;
29
29
  tasks: (tasksOptions?: Partial<import("@poppinss/cliui/types").TaskManagerOptions>) => import("@poppinss/cliui").TaskManager;
30
+ steps: () => import("@poppinss/cliui").Steps;
30
31
  icons: {
31
32
  tick: string;
32
33
  cross: string;
@@ -36,6 +37,7 @@ export declare class RoutesScanner {
36
37
  info: string;
37
38
  warning: string;
38
39
  squareSmallFilled: string;
40
+ borderVertical: string;
39
41
  };
40
42
  sticker: () => import("@poppinss/cliui").Instructions;
41
43
  instructions: () => import("@poppinss/cliui").Instructions;
@@ -1,7 +1,7 @@
1
1
  import { installPackage, detectPackageManager } from '@antfu/install-pkg';
2
2
  import { Project } from 'ts-morph';
3
3
  import { RcFileTransformer } from './rc_file_transformer.ts';
4
- import type { MiddlewareNode, EnvValidationNode, BouncerPolicyNode } from '../types/code_transformer.ts';
4
+ import type { MiddlewareNode, EnvValidationNode, BouncerPolicyNode, ValidatorNode, LimiterNode, MixinDefinition, ControllerMethodNode } from '../types/code_transformer.ts';
5
5
  /**
6
6
  * This class is responsible for transforming AdonisJS project code,
7
7
  * including updating middleware, environment validations, and other
@@ -100,4 +100,8 @@ export declare class CodeTransformer {
100
100
  * @param policies - Array of bouncer policy entries to add
101
101
  */
102
102
  addPolicies(policies: BouncerPolicyNode[]): Promise<void>;
103
+ addValidator(definition: ValidatorNode): Promise<void>;
104
+ addLimiter(definition: LimiterNode): Promise<void>;
105
+ addModelMixins(modelFileName: string, mixins: MixinDefinition[]): Promise<void>;
106
+ addControllerMethod(definition: ControllerMethodNode): Promise<void>;
103
107
  }
@@ -1,6 +1,7 @@
1
1
  import { t as CodemodException } from "../../codemod_exception-vyN1VXuX.js";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { detectPackageManager, installPackage } from "@antfu/install-pkg";
4
+ import { ImportsBag } from "@poppinss/utils";
4
5
  import { join } from "node:path";
5
6
  import { Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
6
7
  const ALLOWED_ENVIRONMENTS = [
@@ -221,7 +222,10 @@ var CodeTransformer = class {
221
222
  return {
222
223
  start: rcFileTransformer.getDirectory("start", "start"),
223
224
  tests: rcFileTransformer.getDirectory("tests", "tests"),
224
- policies: rcFileTransformer.getDirectory("policies", "app/policies")
225
+ policies: rcFileTransformer.getDirectory("policies", "app/policies"),
226
+ validators: rcFileTransformer.getDirectory("validators", "app/validators"),
227
+ models: rcFileTransformer.getDirectory("models", "app/models"),
228
+ controllers: rcFileTransformer.getDirectory("controllers", "app/controllers")
225
229
  };
226
230
  }
227
231
  #addToMiddlewareArray(file, target, middlewareEntry) {
@@ -252,9 +256,8 @@ var CodeTransformer = class {
252
256
  }
253
257
  }
254
258
  #addImportDeclarations(file, importDeclarations) {
255
- const existingImports = file.getImportDeclarations();
256
259
  importDeclarations.forEach((importDeclaration) => {
257
- const existingImport = existingImports.find((mod) => mod.getModuleSpecifierValue() === importDeclaration.module);
260
+ const existingImport = file.getImportDeclarations().find((mod) => mod.getModuleSpecifierValue() === importDeclaration.module);
258
261
  if (existingImport && importDeclaration.isNamed) {
259
262
  if (!existingImport.getNamedImports().find((namedImport) => namedImport.getName() === importDeclaration.identifier)) existingImport.addNamedImport(importDeclaration.identifier);
260
263
  return;
@@ -266,6 +269,24 @@ var CodeTransformer = class {
266
269
  });
267
270
  });
268
271
  }
272
+ #addImportsFromImportInfo(file, imports) {
273
+ const importsBag = new ImportsBag();
274
+ for (const importInfo of imports) importsBag.add(importInfo);
275
+ const importDeclarations = importsBag.toArray().flatMap((moduleImport) => {
276
+ return (moduleImport.namedImports ?? []).map((symbol) => {
277
+ return {
278
+ isNamed: true,
279
+ module: moduleImport.source,
280
+ identifier: symbol
281
+ };
282
+ }).concat(moduleImport.defaultImport ? [{
283
+ isNamed: false,
284
+ module: moduleImport.source,
285
+ identifier: moduleImport.defaultImport
286
+ }] : []);
287
+ });
288
+ this.#addImportDeclarations(file, importDeclarations);
289
+ }
269
290
  #addLeadingComment(writer, comment) {
270
291
  if (!comment) return writer.blankLine();
271
292
  return writer.blankLine().writeLine("/*").writeLine(`|----------------------------------------------------------`).writeLine(`| ${comment}`).writeLine(`|----------------------------------------------------------`).writeLine(`*/`);
@@ -363,5 +384,131 @@ var CodeTransformer = class {
363
384
  file.formatText(this.#editorSettings);
364
385
  await file.save();
365
386
  }
387
+ async addValidator(definition) {
388
+ const filePath = `${this.getDirectories().validators}/${definition.validatorFileName}`;
389
+ const validatorFileUrl = join(this.#cwdPath, `./${filePath}`);
390
+ let file = this.project.getSourceFile(validatorFileUrl);
391
+ if (!file) try {
392
+ file = this.project.addSourceFileAtPath(validatorFileUrl);
393
+ } catch {}
394
+ if (!file) {
395
+ file = this.project.createSourceFile(validatorFileUrl, definition.contents);
396
+ file.formatText(this.#editorSettings);
397
+ await file.save();
398
+ return;
399
+ }
400
+ if (file.getVariableDeclaration(definition.exportName)) return;
401
+ file.addStatements(`\n${definition.contents}`);
402
+ file.formatText(this.#editorSettings);
403
+ await file.save();
404
+ }
405
+ async addLimiter(definition) {
406
+ const filePath = `${this.getDirectories().start}/${definition.limiterFileName}`;
407
+ const limiterFileUrl = join(this.#cwdPath, `./${filePath}`);
408
+ let file = this.project.getSourceFile(limiterFileUrl);
409
+ if (!file) try {
410
+ file = this.project.addSourceFileAtPath(limiterFileUrl);
411
+ } catch {}
412
+ if (!file) {
413
+ file = this.project.createSourceFile(limiterFileUrl, definition.contents);
414
+ file.formatText(this.#editorSettings);
415
+ await file.save();
416
+ return;
417
+ }
418
+ if (file.getVariableDeclaration(definition.exportName)) return;
419
+ file.addStatements(`\n${definition.contents}`);
420
+ file.formatText(this.#editorSettings);
421
+ await file.save();
422
+ }
423
+ async addModelMixins(modelFileName, mixins) {
424
+ const filePath = `${this.getDirectories().models}/${modelFileName}`;
425
+ const modelFileUrl = join(this.#cwdPath, `./${filePath}`);
426
+ let file = this.project.getSourceFile(modelFileUrl);
427
+ if (!file) try {
428
+ file = this.project.addSourceFileAtPath(modelFileUrl);
429
+ } catch {
430
+ throw new Error(`Could not find source file at path: "${filePath}"`);
431
+ }
432
+ const defaultExportSymbol = file.getDefaultExportSymbol();
433
+ if (!defaultExportSymbol) throw new Error(`Could not find default export in "${filePath}". The model must have a default export class.`);
434
+ const declarations = defaultExportSymbol.getDeclarations();
435
+ if (declarations.length === 0) throw new Error(`Could not find default export declaration in "${filePath}".`);
436
+ const declaration = declarations[0];
437
+ if (!Node.isClassDeclaration(declaration)) throw new Error(`Default export in "${filePath}" is not a class. The model must be exported as a class.`);
438
+ const mixinImports = mixins.map((mixin) => {
439
+ if (mixin.importType === "named") return {
440
+ source: mixin.importPath,
441
+ namedImports: [mixin.name]
442
+ };
443
+ else return {
444
+ source: mixin.importPath,
445
+ defaultImport: mixin.name
446
+ };
447
+ });
448
+ this.#addImportsFromImportInfo(file, mixinImports);
449
+ const heritageClause = declaration.getHeritageClauseByKind(SyntaxKind.ExtendsKeyword);
450
+ if (!heritageClause) throw new Error(`Could not find extends clause in "${filePath}".`);
451
+ const extendsExpression = heritageClause.getTypeNodes()[0];
452
+ if (!extendsExpression) throw new Error(`Could not find extends expression in "${filePath}".`);
453
+ const extendsExpressionNode = extendsExpression.getExpression();
454
+ let composeCall;
455
+ if (Node.isCallExpression(extendsExpressionNode)) {
456
+ if (extendsExpressionNode.getExpression().getText() === "compose") composeCall = extendsExpressionNode;
457
+ }
458
+ const mixinCalls = mixins.map((mixin) => {
459
+ const args = mixin.args && mixin.args.length > 0 ? mixin.args.join(", ") : "";
460
+ return `${mixin.name}(${args})`;
461
+ });
462
+ if (composeCall && Node.isCallExpression(composeCall)) {
463
+ const existingArgsText = composeCall.getArguments().map((arg) => arg.getText());
464
+ const newMixinCalls = mixinCalls.filter((mixinCall) => {
465
+ const mixinFunctionName = mixinCall.split("(")[0];
466
+ return !existingArgsText.some((existingArg) => {
467
+ return existingArg.includes(`${mixinFunctionName}(`);
468
+ });
469
+ });
470
+ const newArgs = [...existingArgsText, ...newMixinCalls];
471
+ composeCall.replaceWithText(`compose(${newArgs.join(", ")})`);
472
+ } else {
473
+ this.#addImportDeclarations(file, [{
474
+ isNamed: true,
475
+ module: "@adonisjs/core/helpers",
476
+ identifier: "compose"
477
+ }]);
478
+ const newExtends = `compose(${extendsExpressionNode.getText()}, ${mixinCalls.join(", ")})`;
479
+ extendsExpression.replaceWithText(newExtends);
480
+ }
481
+ file.formatText(this.#editorSettings);
482
+ await file.save();
483
+ }
484
+ async addControllerMethod(definition) {
485
+ const filePath = `${this.getDirectories().controllers}/${definition.controllerFileName}`;
486
+ const controllerFileUrl = join(this.#cwdPath, `./${filePath}`);
487
+ let file = this.project.getSourceFile(controllerFileUrl);
488
+ if (!file) try {
489
+ file = this.project.addSourceFileAtPath(controllerFileUrl);
490
+ } catch {}
491
+ if (!file) {
492
+ const contents = `export default class ${definition.className} {
493
+ ${definition.contents}
494
+ }`;
495
+ file = this.project.createSourceFile(controllerFileUrl, contents);
496
+ if (definition.imports) this.#addImportsFromImportInfo(file, definition.imports);
497
+ file.formatText(this.#editorSettings);
498
+ await file.save();
499
+ return;
500
+ }
501
+ const defaultExportSymbol = file.getDefaultExportSymbol();
502
+ if (!defaultExportSymbol) throw new Error(`Could not find default export in "${filePath}". The controller must have a default export class.`);
503
+ const declarations = defaultExportSymbol.getDeclarations();
504
+ if (declarations.length === 0) throw new Error(`Could not find default export declaration in "${filePath}".`);
505
+ const declaration = declarations[0];
506
+ if (!Node.isClassDeclaration(declaration)) throw new Error(`Default export in "${filePath}" is not a class. The controller must be exported as a class.`);
507
+ if (declaration.getMethod(definition.name)) return;
508
+ if (definition.imports) this.#addImportsFromImportInfo(file, definition.imports);
509
+ declaration.addMember(definition.contents);
510
+ file.formatText(this.#editorSettings);
511
+ await file.save();
512
+ }
366
513
  };
367
514
  export { CodeTransformer };
@@ -25,6 +25,7 @@ export declare class DevServer {
25
25
  logger: import("@poppinss/cliui").Logger;
26
26
  table: (tableOptions?: Partial<import("@poppinss/cliui/types").TableOptions>) => import("@poppinss/cliui").Table;
27
27
  tasks: (tasksOptions?: Partial<import("@poppinss/cliui/types").TaskManagerOptions>) => import("@poppinss/cliui").TaskManager;
28
+ steps: () => import("@poppinss/cliui").Steps;
28
29
  icons: {
29
30
  tick: string;
30
31
  cross: string;
@@ -34,6 +35,7 @@ export declare class DevServer {
34
35
  info: string;
35
36
  warning: string;
36
37
  squareSmallFilled: string;
38
+ borderVertical: string;
37
39
  };
38
40
  sticker: () => import("@poppinss/cliui").Instructions;
39
41
  instructions: () => import("@poppinss/cliui").Instructions;
@@ -32,6 +32,7 @@ export declare class TestRunner {
32
32
  logger: import("@poppinss/cliui").Logger;
33
33
  table: (tableOptions?: Partial<import("@poppinss/cliui/types").TableOptions>) => import("@poppinss/cliui").Table;
34
34
  tasks: (tasksOptions?: Partial<import("@poppinss/cliui/types").TaskManagerOptions>) => import("@poppinss/cliui").TaskManager;
35
+ steps: () => import("@poppinss/cliui").Steps;
35
36
  icons: {
36
37
  tick: string;
37
38
  cross: string;
@@ -41,6 +42,7 @@ export declare class TestRunner {
41
42
  info: string;
42
43
  warning: string;
43
44
  squareSmallFilled: string;
45
+ borderVertical: string;
44
46
  };
45
47
  sticker: () => import("@poppinss/cliui").Instructions;
46
48
  instructions: () => import("@poppinss/cliui").Instructions;
@@ -1,3 +1,4 @@
1
+ import { type ImportInfo } from '@poppinss/utils';
1
2
  /**
2
3
  * Entry to add a middleware to a given middleware stack via the CodeTransformer.
3
4
  * Represents middleware configuration for server, router, or named middleware stacks.
@@ -90,12 +91,130 @@ export type EnvValidationNode = {
90
91
  */
91
92
  variables: Record<string, string>;
92
93
  };
94
+ /**
95
+ * Configuration for creating a new validator file via CodeTransformer.
96
+ * Represents the structure needed to generate validator files.
97
+ *
98
+ * @example
99
+ * const validator: ValidatorNode = {
100
+ * validatorFileName: 'create_user',
101
+ * exportName: 'createUserValidator',
102
+ * contents: 'export const createUserValidator = vine.compile(...)'
103
+ * }
104
+ */
105
+ export type ValidatorNode = {
106
+ /** The filename for the validator (without extension) */
107
+ validatorFileName: string;
108
+ /** The name of the exported validator constant or function */
109
+ exportName: string;
110
+ /** The complete file contents including the validator definition */
111
+ contents: string;
112
+ };
113
+ /**
114
+ * Configuration for creating a new rate limiter file via CodeTransformer.
115
+ * Represents the structure needed to generate limiter files.
116
+ *
117
+ * @example
118
+ * const limiter: LimiterNode = {
119
+ * limiterFileName: 'api_throttle',
120
+ * exportName: 'apiThrottleLimiter',
121
+ * contents: 'export const apiThrottleLimiter = limiter.define(...)'
122
+ * }
123
+ */
124
+ export type LimiterNode = {
125
+ /** The filename for the limiter (without extension) */
126
+ limiterFileName: string;
127
+ /** The name of the exported limiter constant or function */
128
+ exportName: string;
129
+ /** The complete file contents including the limiter definition */
130
+ contents: string;
131
+ };
132
+ /**
133
+ * Definition for applying a mixin to a model class.
134
+ * Mixins extend model functionality by adding methods, properties, or behaviors.
135
+ *
136
+ * @example
137
+ * const mixin: MixinDefinition = {
138
+ * name: 'SoftDeletes',
139
+ * importPath: '@adonisjs/lucid/mixins/soft_deletes',
140
+ * importType: 'named'
141
+ * }
142
+ *
143
+ * @example
144
+ * const mixinWithArgs: MixinDefinition = {
145
+ * name: 'Sluggable',
146
+ * args: ['title', '{ strategy: "dbIncrement" }'],
147
+ * importPath: '#mixins/sluggable',
148
+ * importType: 'default'
149
+ * }
150
+ */
151
+ export type MixinDefinition = {
152
+ /** The name of the mixin function or class */
153
+ name: string;
154
+ /** Optional arguments to pass to the mixin function */
155
+ args?: string[];
156
+ /** The import path to the mixin module */
157
+ importPath: string;
158
+ /** Whether the mixin is exported as named or default export */
159
+ importType: 'named' | 'default';
160
+ };
161
+ /**
162
+ * Configuration for adding a new method to an existing controller class.
163
+ * Used by CodeTransformer to inject methods into controller files.
164
+ *
165
+ * @example
166
+ * const method: ControllerMethodNode = {
167
+ * controllerFileName: 'users_controller',
168
+ * className: 'UsersController',
169
+ * name: 'destroy',
170
+ * contents: 'async destroy({ params, response }: HttpContext) { ... }',
171
+ * imports: [
172
+ * { isType: false, isNamed: true, name: 'HttpContext', path: '@adonisjs/core/http' }
173
+ * ]
174
+ * }
175
+ */
176
+ export type ControllerMethodNode = {
177
+ /** The controller filename (without extension) */
178
+ controllerFileName: string;
179
+ /** The name of the controller class */
180
+ className: string;
181
+ /** The name of the method to add */
182
+ name: string;
183
+ /** The complete method implementation including signature and body */
184
+ contents: string;
185
+ /** Optional imports needed by the method */
186
+ imports?: ImportInfo[];
187
+ };
188
+ /**
189
+ * Configuration for adding hooks to adonisrc.ts file.
190
+ * Hooks can be defined as thunks (lazy imports) or direct imports.
191
+ *
192
+ * @example
193
+ * // Thunk style (lazy import)
194
+ * const thunkHook: HookNode = {
195
+ * type: 'thunk',
196
+ * path: './commands/migrate.js'
197
+ * }
198
+ *
199
+ * @example
200
+ * // Import style with named export
201
+ * const importHook: HookNode = {
202
+ * type: 'import',
203
+ * path: '#hooks/after_build',
204
+ * name: 'afterBuildHook'
205
+ * }
206
+ */
93
207
  export type HookNode = {
208
+ /** Hook type: thunk creates a lazy import function */
94
209
  type: 'thunk';
210
+ /** Path to the hook module */
95
211
  path: string;
96
212
  } | {
213
+ /** Hook type: import directly imports the hook */
97
214
  type: 'import';
215
+ /** Path to the hook module */
98
216
  path: string;
217
+ /** Optional name of the exported hook (for named exports) */
99
218
  name?: string;
100
219
  };
101
220
  /**
@@ -345,26 +345,91 @@ export interface ShortcutsManagerOptions {
345
345
  /** Callback functions for different shortcut actions */
346
346
  callbacks: KeyboardShortcutsCallbacks;
347
347
  }
348
+ /**
349
+ * Message sent from the dev server child process to the parent when the server is ready.
350
+ * Used for IPC communication to notify when the AdonisJS HTTP server has started.
351
+ *
352
+ * @example
353
+ * const message: AdonisJSServerReadyMessage = {
354
+ * isAdonisJS: true,
355
+ * environment: 'web',
356
+ * port: 3333,
357
+ * host: 'localhost',
358
+ * duration: [0, 150000000] // [seconds, nanoseconds]
359
+ * }
360
+ */
348
361
  export type AdonisJSServerReadyMessage = {
362
+ /** Marker to identify AdonisJS-specific messages */
349
363
  isAdonisJS: true;
364
+ /** The environment type (always 'web' for HTTP servers) */
350
365
  environment: 'web';
366
+ /** The port number the server is listening on */
351
367
  port: number;
368
+ /** The host address the server is bound to */
352
369
  host: string;
370
+ /** Optional server startup duration as [seconds, nanoseconds] tuple */
353
371
  duration?: [number, number];
354
372
  };
373
+ /**
374
+ * Message sent from the dev server child process when routes are committed.
375
+ * Contains the file location where routes are defined.
376
+ *
377
+ * @example
378
+ * const message: AdonisJSRoutesSharedMessage = {
379
+ * isAdonisJS: true,
380
+ * routesFileLocation: '/project/start/routes.ts'
381
+ * }
382
+ */
355
383
  export type AdonisJSRoutesSharedMessage = {
384
+ /** Marker to identify AdonisJS-specific messages */
356
385
  isAdonisJS: true;
386
+ /** Absolute path to the routes definition file */
357
387
  routesFileLocation: string;
358
388
  };
389
+ /**
390
+ * Messages sent by the hot-hook module for HMR (Hot Module Replacement) communication.
391
+ * These messages notify the dev server about file system changes and module invalidations.
392
+ *
393
+ * @example
394
+ * // Full reload required
395
+ * const fullReload: HotHookMessage = {
396
+ * type: 'hot-hook:full-reload',
397
+ * path: 'app/middleware/auth.ts',
398
+ * shouldBeReloadable: false
399
+ * }
400
+ *
401
+ * @example
402
+ * // Modules invalidated (can be hot-reloaded)
403
+ * const invalidated: HotHookMessage = {
404
+ * type: 'hot-hook:invalidated',
405
+ * paths: ['app/controllers/users_controller.ts', 'app/models/user.ts']
406
+ * }
407
+ *
408
+ * @example
409
+ * // File changed notification
410
+ * const fileChanged: HotHookMessage = {
411
+ * type: 'hot-hook:file-changed',
412
+ * path: 'config/database.ts',
413
+ * action: 'change'
414
+ * }
415
+ */
359
416
  export type HotHookMessage = {
417
+ /** Message type indicating a full server reload is required */
360
418
  type: 'hot-hook:full-reload';
419
+ /** Path to the file that triggered the full reload */
361
420
  path: string;
421
+ /** Whether the file should be hot-reloadable but isn't */
362
422
  shouldBeReloadable?: boolean;
363
423
  } | {
424
+ /** Message type indicating modules have been invalidated */
364
425
  type: 'hot-hook:invalidated';
426
+ /** Array of module paths that were invalidated */
365
427
  paths: string[];
366
428
  } | {
429
+ /** Message type indicating a file has changed */
367
430
  type: 'hot-hook:file-changed';
431
+ /** Path to the file that changed */
368
432
  path: string;
433
+ /** The type of file system change */
369
434
  action: 'change' | 'add' | 'unlink';
370
435
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adonisjs/assembler",
3
3
  "description": "Provides utilities to run AdonisJS development server and build project for production",
4
- "version": "8.0.0-next.30",
4
+ "version": "8.0.0-next.31",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },
@@ -42,11 +42,11 @@
42
42
  "@adonisjs/tsconfig": "^2.0.0-next.3",
43
43
  "@japa/assert": "^4.2.0",
44
44
  "@japa/file-system": "^3.0.0",
45
- "@japa/runner": "^5.0.0",
45
+ "@japa/runner": "^5.3.0",
46
46
  "@japa/snapshot": "^2.0.10",
47
- "@poppinss/ts-exec": "^1.4.1",
47
+ "@poppinss/ts-exec": "^1.4.2",
48
48
  "@release-it/conventional-changelog": "^10.0.4",
49
- "@types/node": "^25.0.5",
49
+ "@types/node": "^25.0.10",
50
50
  "@types/picomatch": "^4.0.2",
51
51
  "@types/pretty-hrtime": "^1.0.3",
52
52
  "c8": "^10.1.3",
@@ -54,17 +54,17 @@
54
54
  "del-cli": "^7.0.0",
55
55
  "eslint": "^9.39.2",
56
56
  "hot-hook": "^0.4.1-next.2",
57
- "p-event": "^7.0.2",
58
- "prettier": "^3.7.4",
59
- "release-it": "^19.2.3",
57
+ "p-event": "^7.1.0",
58
+ "prettier": "^3.8.1",
59
+ "release-it": "^19.2.4",
60
60
  "tsdown": "^0.19.0",
61
- "typedoc": "^0.28.15",
61
+ "typedoc": "^0.28.16",
62
62
  "typescript": "^5.9.3"
63
63
  },
64
64
  "dependencies": {
65
65
  "@adonisjs/env": "^7.0.0-next.3",
66
66
  "@antfu/install-pkg": "^1.1.0",
67
- "@ast-grep/napi": "^0.40.4",
67
+ "@ast-grep/napi": "^0.40.5",
68
68
  "@poppinss/cliui": "^6.6.0",
69
69
  "@poppinss/hooks": "^7.3.0",
70
70
  "@poppinss/utils": "^7.0.0-next.5",