@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 +260 -0
- package/build/src/bundler.d.ts +2 -0
- package/build/src/code_scanners/routes_scanner/main.d.ts +2 -0
- package/build/src/code_transformer/main.d.ts +5 -1
- package/build/src/code_transformer/main.js +150 -3
- package/build/src/dev_server.d.ts +2 -0
- package/build/src/test_runner.d.ts +2 -0
- package/build/src/types/code_transformer.d.ts +119 -0
- package/build/src/types/common.d.ts +65 -0
- package/package.json +9 -9
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.
|
package/build/src/bundler.d.ts
CHANGED
|
@@ -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 =
|
|
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.
|
|
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.
|
|
45
|
+
"@japa/runner": "^5.3.0",
|
|
46
46
|
"@japa/snapshot": "^2.0.10",
|
|
47
|
-
"@poppinss/ts-exec": "^1.4.
|
|
47
|
+
"@poppinss/ts-exec": "^1.4.2",
|
|
48
48
|
"@release-it/conventional-changelog": "^10.0.4",
|
|
49
|
-
"@types/node": "^25.0.
|
|
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
|
|
58
|
-
"prettier": "^3.
|
|
59
|
-
"release-it": "^19.2.
|
|
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.
|
|
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.
|
|
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",
|