@forinda/kickjs-cli 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1303 -172
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +88 -67
- package/dist/index.js +1031 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
5
5
|
// src/cli.ts
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { readFileSync as readFileSync2 } from "fs";
|
|
8
|
-
import { dirname as dirname3, join as
|
|
8
|
+
import { dirname as dirname3, join as join18 } from "path";
|
|
9
9
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
@@ -61,13 +61,15 @@ async function initProject(options) {
|
|
|
61
61
|
};
|
|
62
62
|
if (template !== "minimal") {
|
|
63
63
|
baseDeps["@forinda/kickjs-swagger"] = KICKJS_VERSION;
|
|
64
|
+
baseDeps["@forinda/kickjs-devtools"] = KICKJS_VERSION;
|
|
64
65
|
}
|
|
65
66
|
if (template === "graphql") {
|
|
66
67
|
baseDeps["@forinda/kickjs-graphql"] = KICKJS_VERSION;
|
|
67
68
|
baseDeps["graphql"] = "^16.11.0";
|
|
68
69
|
}
|
|
69
|
-
if (template === "
|
|
70
|
+
if (template === "cqrs") {
|
|
70
71
|
baseDeps["@forinda/kickjs-queue"] = KICKJS_VERSION;
|
|
72
|
+
baseDeps["@forinda/kickjs-ws"] = KICKJS_VERSION;
|
|
71
73
|
baseDeps["@forinda/kickjs-otel"] = KICKJS_VERSION;
|
|
72
74
|
}
|
|
73
75
|
if (template === "ddd") {
|
|
@@ -166,12 +168,45 @@ export default defineConfig({
|
|
|
166
168
|
printWidth: 100,
|
|
167
169
|
tabWidth: 2
|
|
168
170
|
}, null, 2));
|
|
171
|
+
await writeFileSafe(join(dir, ".editorconfig"), `# https://editorconfig.org
|
|
172
|
+
root = true
|
|
173
|
+
|
|
174
|
+
[*]
|
|
175
|
+
indent_style = space
|
|
176
|
+
indent_size = 2
|
|
177
|
+
end_of_line = lf
|
|
178
|
+
charset = utf-8
|
|
179
|
+
trim_trailing_whitespace = true
|
|
180
|
+
insert_final_newline = true
|
|
181
|
+
|
|
182
|
+
[*.md]
|
|
183
|
+
trim_trailing_whitespace = false
|
|
184
|
+
`);
|
|
169
185
|
await writeFileSafe(join(dir, ".gitignore"), `node_modules/
|
|
170
186
|
dist/
|
|
171
187
|
.env
|
|
172
188
|
coverage/
|
|
173
189
|
.DS_Store
|
|
174
190
|
*.tsbuildinfo
|
|
191
|
+
`);
|
|
192
|
+
await writeFileSafe(join(dir, ".gitattributes"), `# Auto-detect text files and normalise line endings to LF
|
|
193
|
+
* text=auto eol=lf
|
|
194
|
+
|
|
195
|
+
# Explicitly mark generated / binary files
|
|
196
|
+
*.png binary
|
|
197
|
+
*.jpg binary
|
|
198
|
+
*.jpeg binary
|
|
199
|
+
*.gif binary
|
|
200
|
+
*.ico binary
|
|
201
|
+
*.woff binary
|
|
202
|
+
*.woff2 binary
|
|
203
|
+
*.ttf binary
|
|
204
|
+
*.eot binary
|
|
205
|
+
|
|
206
|
+
# Lock files \u2014 treat as generated
|
|
207
|
+
pnpm-lock.yaml -diff linguist-generated
|
|
208
|
+
yarn.lock -diff linguist-generated
|
|
209
|
+
package-lock.json -diff linguist-generated
|
|
175
210
|
`);
|
|
176
211
|
await writeFileSafe(join(dir, ".env"), `PORT=3000
|
|
177
212
|
NODE_ENV=development
|
|
@@ -275,19 +310,36 @@ export default defineConfig({
|
|
|
275
310
|
rest: "kick g module user",
|
|
276
311
|
graphql: "kick g resolver user",
|
|
277
312
|
ddd: "kick g module user --repo drizzle",
|
|
278
|
-
|
|
313
|
+
cqrs: "kick g module user --pattern cqrs",
|
|
279
314
|
minimal: "# add your routes to src/index.ts"
|
|
280
315
|
};
|
|
281
316
|
console.log(` ${genHint[template] ?? genHint.rest}`);
|
|
282
317
|
console.log(" kick dev");
|
|
283
318
|
console.log();
|
|
284
319
|
console.log(" Commands:");
|
|
285
|
-
console.log(" kick dev
|
|
286
|
-
console.log(" kick build
|
|
287
|
-
console.log(" kick start
|
|
288
|
-
console.log(
|
|
289
|
-
|
|
290
|
-
|
|
320
|
+
console.log(" kick dev Start dev server with Vite HMR");
|
|
321
|
+
console.log(" kick build Production build via Vite");
|
|
322
|
+
console.log(" kick start Run production build");
|
|
323
|
+
console.log();
|
|
324
|
+
console.log(" Generators:");
|
|
325
|
+
console.log(" kick g module <name> Full DDD module (controller, DTOs, use-cases, repo)");
|
|
326
|
+
console.log(" kick g scaffold <n> <f..> CRUD module from field definitions");
|
|
327
|
+
console.log(" kick g controller <name> Standalone controller");
|
|
328
|
+
console.log(" kick g service <name> @Service() class");
|
|
329
|
+
console.log(" kick g middleware <name> Express middleware");
|
|
330
|
+
console.log(" kick g guard <name> Route guard (auth, roles, etc.)");
|
|
331
|
+
console.log(" kick g adapter <name> AppAdapter with lifecycle hooks");
|
|
332
|
+
console.log(" kick g dto <name> Zod DTO schema");
|
|
333
|
+
if (template === "graphql") console.log(" kick g resolver <name> GraphQL resolver");
|
|
334
|
+
if (template === "cqrs") console.log(" kick g job <name> Queue job processor");
|
|
335
|
+
console.log(" kick g config Generate kick.config.ts");
|
|
336
|
+
console.log();
|
|
337
|
+
console.log(" Add packages:");
|
|
338
|
+
console.log(" kick add <pkg> Install a KickJS package + peers");
|
|
339
|
+
console.log(" kick add --list Show all available packages");
|
|
340
|
+
console.log();
|
|
341
|
+
console.log(" Available: auth, swagger, graphql, drizzle, prisma, ws,");
|
|
342
|
+
console.log(" cron, queue, mailer, otel, multi-tenant, notifications, testing");
|
|
291
343
|
console.log();
|
|
292
344
|
}
|
|
293
345
|
__name(initProject, "initProject");
|
|
@@ -315,12 +367,13 @@ bootstrap({
|
|
|
315
367
|
],
|
|
316
368
|
})
|
|
317
369
|
`;
|
|
318
|
-
case "
|
|
370
|
+
case "cqrs":
|
|
319
371
|
return `import 'reflect-metadata'
|
|
320
372
|
import { bootstrap } from '@forinda/kickjs-http'
|
|
321
373
|
import { DevToolsAdapter } from '@forinda/kickjs-devtools'
|
|
322
374
|
import { SwaggerAdapter } from '@forinda/kickjs-swagger'
|
|
323
375
|
import { OtelAdapter } from '@forinda/kickjs-otel'
|
|
376
|
+
// import { WsAdapter } from '@forinda/kickjs-ws'
|
|
324
377
|
// import { QueueAdapter, BullMQProvider } from '@forinda/kickjs-queue'
|
|
325
378
|
import { modules } from './modules'
|
|
326
379
|
|
|
@@ -332,6 +385,8 @@ bootstrap({
|
|
|
332
385
|
new SwaggerAdapter({
|
|
333
386
|
info: { title: '${name}', version: '${cliPkg.version}' },
|
|
334
387
|
}),
|
|
388
|
+
// Uncomment for WebSocket support:
|
|
389
|
+
// new WsAdapter(),
|
|
335
390
|
// Uncomment when Redis is available:
|
|
336
391
|
// new QueueAdapter({
|
|
337
392
|
// provider: new BullMQProvider({ host: 'localhost', port: 6379 }),
|
|
@@ -403,7 +458,7 @@ async function confirm(question, defaultYes = true) {
|
|
|
403
458
|
}
|
|
404
459
|
__name(confirm, "confirm");
|
|
405
460
|
function registerInitCommand(program) {
|
|
406
|
-
program.command("new [name]").alias("init").description('Create a new KickJS project (use "." for current directory)').option("-d, --directory <dir>", "Target directory (defaults to project name)").option("--pm <manager>", "Package manager: pnpm | npm | yarn").option("--git", "Initialize git repository").option("--no-git", "Skip git initialization").option("--install", "Install dependencies after scaffolding").option("--no-install", "Skip dependency installation").option("-f, --force", "Remove existing files without prompting").option("-t, --template <type>", "Project template: rest | graphql | ddd |
|
|
461
|
+
program.command("new [name]").alias("init").description('Create a new KickJS project (use "." for current directory)').option("-d, --directory <dir>", "Target directory (defaults to project name)").option("--pm <manager>", "Package manager: pnpm | npm | yarn").option("--git", "Initialize git repository").option("--no-git", "Skip git initialization").option("--install", "Install dependencies after scaffolding").option("--no-install", "Skip dependency installation").option("-f, --force", "Remove existing files without prompting").option("-t, --template <type>", "Project template: rest | graphql | ddd | cqrs | minimal").action(async (name, opts) => {
|
|
407
462
|
console.log();
|
|
408
463
|
if (!name) {
|
|
409
464
|
name = await ask("Project name", "my-api");
|
|
@@ -451,14 +506,14 @@ function registerInitCommand(program) {
|
|
|
451
506
|
"REST API (Express + Swagger)",
|
|
452
507
|
"GraphQL API (GraphQL + GraphiQL)",
|
|
453
508
|
"DDD (Domain-Driven Design modules)",
|
|
454
|
-
"
|
|
509
|
+
"CQRS (Commands, Queries, Events + WS/Queue)",
|
|
455
510
|
"Minimal (bare Express)"
|
|
456
511
|
], 0);
|
|
457
512
|
const templateMap = {
|
|
458
513
|
"REST API (Express + Swagger)": "rest",
|
|
459
514
|
"GraphQL API (GraphQL + GraphiQL)": "graphql",
|
|
460
515
|
"DDD (Domain-Driven Design modules)": "ddd",
|
|
461
|
-
"
|
|
516
|
+
"CQRS (Commands, Queries, Events + WS/Queue)": "cqrs",
|
|
462
517
|
"Minimal (bare Express)": "minimal"
|
|
463
518
|
};
|
|
464
519
|
template = templateMap[template] ?? "rest";
|
|
@@ -496,10 +551,11 @@ function registerInitCommand(program) {
|
|
|
496
551
|
__name(registerInitCommand, "registerInitCommand");
|
|
497
552
|
|
|
498
553
|
// src/commands/generate.ts
|
|
499
|
-
import { resolve as
|
|
554
|
+
import { resolve as resolve4 } from "path";
|
|
500
555
|
|
|
501
556
|
// src/generators/module.ts
|
|
502
557
|
import { join as join2 } from "path";
|
|
558
|
+
import { createInterface as createInterface2 } from "readline";
|
|
503
559
|
|
|
504
560
|
// src/utils/naming.ts
|
|
505
561
|
function toPascalCase(name) {
|
|
@@ -536,9 +592,25 @@ __name(pluralizePascal, "pluralizePascal");
|
|
|
536
592
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
537
593
|
|
|
538
594
|
// src/generators/templates/module-index.ts
|
|
595
|
+
function repoMaps(pascal, kebab, repo) {
|
|
596
|
+
const repoClassMap = {
|
|
597
|
+
inmemory: `InMemory${pascal}Repository`,
|
|
598
|
+
drizzle: `Drizzle${pascal}Repository`,
|
|
599
|
+
prisma: `Prisma${pascal}Repository`
|
|
600
|
+
};
|
|
601
|
+
const repoFileMap = {
|
|
602
|
+
inmemory: `in-memory-${kebab}`,
|
|
603
|
+
drizzle: `drizzle-${kebab}`,
|
|
604
|
+
prisma: `prisma-${kebab}`
|
|
605
|
+
};
|
|
606
|
+
return {
|
|
607
|
+
repoClass: repoClassMap[repo] ?? repoClassMap.inmemory,
|
|
608
|
+
repoFile: repoFileMap[repo] ?? repoFileMap.inmemory
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
__name(repoMaps, "repoMaps");
|
|
539
612
|
function generateModuleIndex(pascal, kebab, plural, repo) {
|
|
540
|
-
const repoClass
|
|
541
|
-
const repoFile = repo === "inmemory" ? `in-memory-${kebab}` : `drizzle-${kebab}`;
|
|
613
|
+
const { repoClass, repoFile } = repoMaps(pascal, kebab, repo);
|
|
542
614
|
return `/**
|
|
543
615
|
* ${pascal} Module
|
|
544
616
|
*
|
|
@@ -591,6 +663,65 @@ export class ${pascal}Module implements AppModule {
|
|
|
591
663
|
`;
|
|
592
664
|
}
|
|
593
665
|
__name(generateModuleIndex, "generateModuleIndex");
|
|
666
|
+
function generateRestModuleIndex(pascal, kebab, plural, repo) {
|
|
667
|
+
const { repoClass, repoFile } = repoMaps(pascal, kebab, repo);
|
|
668
|
+
return `/**
|
|
669
|
+
* ${pascal} Module
|
|
670
|
+
*
|
|
671
|
+
* REST module with a flat folder structure.
|
|
672
|
+
* Controller delegates to service, service wraps the repository.
|
|
673
|
+
*
|
|
674
|
+
* Structure:
|
|
675
|
+
* ${kebab}.controller.ts \u2014 HTTP routes (CRUD)
|
|
676
|
+
* ${kebab}.service.ts \u2014 Business logic
|
|
677
|
+
* ${kebab}.repository.ts \u2014 Repository interface
|
|
678
|
+
* ${repoFile}.repository.ts \u2014 Repository implementation
|
|
679
|
+
* dtos/ \u2014 Request/response schemas
|
|
680
|
+
*/
|
|
681
|
+
import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
|
|
682
|
+
import { buildRoutes } from '@forinda/kickjs-http'
|
|
683
|
+
import { ${pascal.toUpperCase()}_REPOSITORY } from './${kebab}.repository'
|
|
684
|
+
import { ${repoClass} } from './${repoFile}.repository'
|
|
685
|
+
import { ${pascal}Controller } from './${kebab}.controller'
|
|
686
|
+
|
|
687
|
+
// Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
|
|
688
|
+
import.meta.glob(['./**/*.service.ts', './**/*.repository.ts', '!./**/*.test.ts'], { eager: true })
|
|
689
|
+
|
|
690
|
+
export class ${pascal}Module implements AppModule {
|
|
691
|
+
register(container: Container): void {
|
|
692
|
+
container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
|
|
693
|
+
container.resolve(${repoClass}),
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
routes(): ModuleRoutes {
|
|
698
|
+
return {
|
|
699
|
+
path: '/${plural}',
|
|
700
|
+
router: buildRoutes(${pascal}Controller),
|
|
701
|
+
controller: ${pascal}Controller,
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
`;
|
|
706
|
+
}
|
|
707
|
+
__name(generateRestModuleIndex, "generateRestModuleIndex");
|
|
708
|
+
function generateMinimalModuleIndex(pascal, kebab, plural) {
|
|
709
|
+
return `import { type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
|
|
710
|
+
import { buildRoutes } from '@forinda/kickjs-http'
|
|
711
|
+
import { ${pascal}Controller } from './${kebab}.controller'
|
|
712
|
+
|
|
713
|
+
export class ${pascal}Module implements AppModule {
|
|
714
|
+
routes(): ModuleRoutes {
|
|
715
|
+
return {
|
|
716
|
+
path: '/${plural}',
|
|
717
|
+
router: buildRoutes(${pascal}Controller),
|
|
718
|
+
controller: ${pascal}Controller,
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
`;
|
|
723
|
+
}
|
|
724
|
+
__name(generateMinimalModuleIndex, "generateMinimalModuleIndex");
|
|
594
725
|
|
|
595
726
|
// src/generators/templates/controller.ts
|
|
596
727
|
function generateController(pascal, kebab, plural, pluralPascal) {
|
|
@@ -656,6 +787,62 @@ export class ${pascal}Controller {
|
|
|
656
787
|
`;
|
|
657
788
|
}
|
|
658
789
|
__name(generateController, "generateController");
|
|
790
|
+
function generateRestController(pascal, kebab, plural, pluralPascal) {
|
|
791
|
+
const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
792
|
+
return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs-core'
|
|
793
|
+
import type { RequestContext } from '@forinda/kickjs-http'
|
|
794
|
+
import { ApiTags } from '@forinda/kickjs-swagger'
|
|
795
|
+
import { ${pascal}Service } from './${kebab}.service'
|
|
796
|
+
import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
|
|
797
|
+
import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
|
|
798
|
+
import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
|
|
799
|
+
|
|
800
|
+
@Controller()
|
|
801
|
+
export class ${pascal}Controller {
|
|
802
|
+
@Autowired() private ${camel}Service!: ${pascal}Service
|
|
803
|
+
|
|
804
|
+
@Get('/')
|
|
805
|
+
@ApiTags('${pascal}')
|
|
806
|
+
@ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
|
|
807
|
+
async list(ctx: RequestContext) {
|
|
808
|
+
return ctx.paginate(
|
|
809
|
+
(parsed) => this.${camel}Service.findPaginated(parsed),
|
|
810
|
+
${pascal.toUpperCase()}_QUERY_CONFIG,
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
@Get('/:id')
|
|
815
|
+
@ApiTags('${pascal}')
|
|
816
|
+
async getById(ctx: RequestContext) {
|
|
817
|
+
const result = await this.${camel}Service.findById(ctx.params.id)
|
|
818
|
+
if (!result) return ctx.notFound('${pascal} not found')
|
|
819
|
+
ctx.json(result)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
@Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
|
|
823
|
+
@ApiTags('${pascal}')
|
|
824
|
+
async create(ctx: RequestContext) {
|
|
825
|
+
const result = await this.${camel}Service.create(ctx.body)
|
|
826
|
+
ctx.created(result)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
@Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
|
|
830
|
+
@ApiTags('${pascal}')
|
|
831
|
+
async update(ctx: RequestContext) {
|
|
832
|
+
const result = await this.${camel}Service.update(ctx.params.id, ctx.body)
|
|
833
|
+
ctx.json(result)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
@Delete('/:id')
|
|
837
|
+
@ApiTags('${pascal}')
|
|
838
|
+
async remove(ctx: RequestContext) {
|
|
839
|
+
await this.${camel}Service.delete(ctx.params.id)
|
|
840
|
+
ctx.noContent()
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
`;
|
|
844
|
+
}
|
|
845
|
+
__name(generateRestController, "generateRestController");
|
|
659
846
|
|
|
660
847
|
// src/generators/templates/constants.ts
|
|
661
848
|
function generateConstants(pascal) {
|
|
@@ -819,20 +1006,19 @@ export class Delete${pascal}UseCase {
|
|
|
819
1006
|
__name(generateUseCases, "generateUseCases");
|
|
820
1007
|
|
|
821
1008
|
// src/generators/templates/repository.ts
|
|
822
|
-
function generateRepositoryInterface(pascal, kebab) {
|
|
1009
|
+
function generateRepositoryInterface(pascal, kebab, dtoPrefix = "../../application/dtos") {
|
|
823
1010
|
return `/**
|
|
824
1011
|
* ${pascal} Repository Interface
|
|
825
1012
|
*
|
|
826
|
-
*
|
|
827
|
-
* The interface
|
|
828
|
-
*
|
|
1013
|
+
* Defines the contract for data access.
|
|
1014
|
+
* The interface declares what operations are available;
|
|
1015
|
+
* implementations (in-memory, Drizzle, Prisma) fulfill the contract.
|
|
829
1016
|
*
|
|
830
|
-
* To swap implementations
|
|
831
|
-
* change the factory in the module's register() method.
|
|
1017
|
+
* To swap implementations, change the factory in the module's register() method.
|
|
832
1018
|
*/
|
|
833
|
-
import type { ${pascal}ResponseDTO } from '
|
|
834
|
-
import type { Create${pascal}DTO } from '
|
|
835
|
-
import type { Update${pascal}DTO } from '
|
|
1019
|
+
import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
|
|
1020
|
+
import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
|
|
1021
|
+
import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
|
|
836
1022
|
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
837
1023
|
|
|
838
1024
|
export interface I${pascal}Repository {
|
|
@@ -848,11 +1034,11 @@ export const ${pascal.toUpperCase()}_REPOSITORY = Symbol('I${pascal}Repository')
|
|
|
848
1034
|
`;
|
|
849
1035
|
}
|
|
850
1036
|
__name(generateRepositoryInterface, "generateRepositoryInterface");
|
|
851
|
-
function generateInMemoryRepository(pascal, kebab) {
|
|
1037
|
+
function generateInMemoryRepository(pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos") {
|
|
852
1038
|
return `/**
|
|
853
1039
|
* In-Memory ${pascal} Repository
|
|
854
1040
|
*
|
|
855
|
-
*
|
|
1041
|
+
* Implements the repository interface using a Map.
|
|
856
1042
|
* Useful for prototyping and testing. Replace with a database implementation
|
|
857
1043
|
* (Drizzle, Prisma, etc.) for production use.
|
|
858
1044
|
*
|
|
@@ -861,10 +1047,10 @@ function generateInMemoryRepository(pascal, kebab) {
|
|
|
861
1047
|
import { randomUUID } from 'node:crypto'
|
|
862
1048
|
import { Repository, HttpException } from '@forinda/kickjs-core'
|
|
863
1049
|
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
864
|
-
import type { I${pascal}Repository } from '
|
|
865
|
-
import type { ${pascal}ResponseDTO } from '
|
|
866
|
-
import type { Create${pascal}DTO } from '
|
|
867
|
-
import type { Update${pascal}DTO } from '
|
|
1050
|
+
import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
|
|
1051
|
+
import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
|
|
1052
|
+
import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
|
|
1053
|
+
import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
|
|
868
1054
|
|
|
869
1055
|
@Repository()
|
|
870
1056
|
export class InMemory${pascal}Repository implements I${pascal}Repository {
|
|
@@ -912,6 +1098,162 @@ export class InMemory${pascal}Repository implements I${pascal}Repository {
|
|
|
912
1098
|
`;
|
|
913
1099
|
}
|
|
914
1100
|
__name(generateInMemoryRepository, "generateInMemoryRepository");
|
|
1101
|
+
function generateDrizzleRepository(pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos") {
|
|
1102
|
+
return `/**
|
|
1103
|
+
* Drizzle ${pascal} Repository
|
|
1104
|
+
*
|
|
1105
|
+
* Implements the repository interface using Drizzle ORM.
|
|
1106
|
+
* Requires a Drizzle database instance injected via the DI container.
|
|
1107
|
+
*
|
|
1108
|
+
* TODO: Update the schema import to match your Drizzle schema file.
|
|
1109
|
+
* TODO: Replace 'db' injection token with your actual database token.
|
|
1110
|
+
*
|
|
1111
|
+
* @Repository() registers this class in the DI container as a singleton.
|
|
1112
|
+
*/
|
|
1113
|
+
import { eq, sql } from 'drizzle-orm'
|
|
1114
|
+
import { Repository, HttpException, Autowired } from '@forinda/kickjs-core'
|
|
1115
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
1116
|
+
import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
|
|
1117
|
+
import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
|
|
1118
|
+
import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
|
|
1119
|
+
import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
|
|
1120
|
+
|
|
1121
|
+
// TODO: Import your Drizzle schema table \u2014 e.g.:
|
|
1122
|
+
// import { ${kebab}s } from '@/db/schema'
|
|
1123
|
+
|
|
1124
|
+
// TODO: Import your Drizzle DB injection token \u2014 e.g.:
|
|
1125
|
+
// import { DRIZZLE_DB } from '@/db/drizzle.provider'
|
|
1126
|
+
|
|
1127
|
+
@Repository()
|
|
1128
|
+
export class Drizzle${pascal}Repository implements I${pascal}Repository {
|
|
1129
|
+
// TODO: Uncomment and configure your Drizzle DB injection:
|
|
1130
|
+
// @Autowired(DRIZZLE_DB) private db!: DrizzleDB
|
|
1131
|
+
|
|
1132
|
+
async findById(id: string): Promise<${pascal}ResponseDTO | null> {
|
|
1133
|
+
// TODO: Implement with Drizzle
|
|
1134
|
+
// const [row] = await this.db.select().from(${kebab}s).where(eq(${kebab}s.id, id))
|
|
1135
|
+
// return row ?? null
|
|
1136
|
+
throw new Error('Drizzle ${pascal} repository not yet implemented \u2014 update schema imports and queries')
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async findAll(): Promise<${pascal}ResponseDTO[]> {
|
|
1140
|
+
// TODO: Implement with Drizzle
|
|
1141
|
+
// return this.db.select().from(${kebab}s)
|
|
1142
|
+
throw new Error('Drizzle ${pascal} repository not yet implemented')
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
|
|
1146
|
+
// TODO: Implement with Drizzle
|
|
1147
|
+
// const data = await this.db.select().from(${kebab}s)
|
|
1148
|
+
// .limit(parsed.pagination.limit)
|
|
1149
|
+
// .offset(parsed.pagination.offset)
|
|
1150
|
+
// const [{ count }] = await this.db.select({ count: sql\`count(*)\` }).from(${kebab}s)
|
|
1151
|
+
// return { data, total: Number(count) }
|
|
1152
|
+
throw new Error('Drizzle ${pascal} repository not yet implemented')
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
1156
|
+
// TODO: Implement with Drizzle
|
|
1157
|
+
// const [row] = await this.db.insert(${kebab}s).values(dto).returning()
|
|
1158
|
+
// return row
|
|
1159
|
+
throw new Error('Drizzle ${pascal} repository not yet implemented')
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
1163
|
+
// TODO: Implement with Drizzle
|
|
1164
|
+
// const [row] = await this.db.update(${kebab}s).set(dto).where(eq(${kebab}s.id, id)).returning()
|
|
1165
|
+
// if (!row) throw HttpException.notFound('${pascal} not found')
|
|
1166
|
+
// return row
|
|
1167
|
+
throw new Error('Drizzle ${pascal} repository not yet implemented')
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async delete(id: string): Promise<void> {
|
|
1171
|
+
// TODO: Implement with Drizzle
|
|
1172
|
+
// const result = await this.db.delete(${kebab}s).where(eq(${kebab}s.id, id))
|
|
1173
|
+
// if (!result.rowCount) throw HttpException.notFound('${pascal} not found')
|
|
1174
|
+
throw new Error('Drizzle ${pascal} repository not yet implemented')
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
`;
|
|
1178
|
+
}
|
|
1179
|
+
__name(generateDrizzleRepository, "generateDrizzleRepository");
|
|
1180
|
+
function generatePrismaRepository(pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos") {
|
|
1181
|
+
const camel = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1182
|
+
return `/**
|
|
1183
|
+
* Prisma ${pascal} Repository
|
|
1184
|
+
*
|
|
1185
|
+
* Implements the repository interface using Prisma Client.
|
|
1186
|
+
* Requires a PrismaClient instance injected via the DI container.
|
|
1187
|
+
*
|
|
1188
|
+
* TODO: Ensure your Prisma schema has a '${pascal}' model defined.
|
|
1189
|
+
* TODO: Replace 'PRISMA_CLIENT' with your actual Prisma injection token.
|
|
1190
|
+
*
|
|
1191
|
+
* @Repository() registers this class in the DI container as a singleton.
|
|
1192
|
+
*/
|
|
1193
|
+
import { Repository, HttpException, Autowired } from '@forinda/kickjs-core'
|
|
1194
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
1195
|
+
import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
|
|
1196
|
+
import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
|
|
1197
|
+
import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
|
|
1198
|
+
import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
|
|
1199
|
+
|
|
1200
|
+
// TODO: Import your Prisma injection token \u2014 e.g.:
|
|
1201
|
+
// import { PRISMA_CLIENT } from '@/db/prisma.provider'
|
|
1202
|
+
// import type { PrismaClient } from '@prisma/client'
|
|
1203
|
+
|
|
1204
|
+
@Repository()
|
|
1205
|
+
export class Prisma${pascal}Repository implements I${pascal}Repository {
|
|
1206
|
+
// TODO: Uncomment and configure your Prisma injection:
|
|
1207
|
+
// @Autowired(PRISMA_CLIENT) private prisma!: PrismaClient
|
|
1208
|
+
|
|
1209
|
+
async findById(id: string): Promise<${pascal}ResponseDTO | null> {
|
|
1210
|
+
// TODO: Implement with Prisma
|
|
1211
|
+
// return this.prisma.${camel}.findUnique({ where: { id } })
|
|
1212
|
+
throw new Error('Prisma ${pascal} repository not yet implemented \u2014 update Prisma imports and queries')
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
async findAll(): Promise<${pascal}ResponseDTO[]> {
|
|
1216
|
+
// TODO: Implement with Prisma
|
|
1217
|
+
// return this.prisma.${camel}.findMany()
|
|
1218
|
+
throw new Error('Prisma ${pascal} repository not yet implemented')
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
|
|
1222
|
+
// TODO: Implement with Prisma
|
|
1223
|
+
// const [data, total] = await Promise.all([
|
|
1224
|
+
// this.prisma.${camel}.findMany({
|
|
1225
|
+
// skip: parsed.pagination.offset,
|
|
1226
|
+
// take: parsed.pagination.limit,
|
|
1227
|
+
// }),
|
|
1228
|
+
// this.prisma.${camel}.count(),
|
|
1229
|
+
// ])
|
|
1230
|
+
// return { data, total }
|
|
1231
|
+
throw new Error('Prisma ${pascal} repository not yet implemented')
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
1235
|
+
// TODO: Implement with Prisma
|
|
1236
|
+
// return this.prisma.${camel}.create({ data: dto })
|
|
1237
|
+
throw new Error('Prisma ${pascal} repository not yet implemented')
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
1241
|
+
// TODO: Implement with Prisma
|
|
1242
|
+
// const row = await this.prisma.${camel}.update({ where: { id }, data: dto })
|
|
1243
|
+
// if (!row) throw HttpException.notFound('${pascal} not found')
|
|
1244
|
+
// return row
|
|
1245
|
+
throw new Error('Prisma ${pascal} repository not yet implemented')
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async delete(id: string): Promise<void> {
|
|
1249
|
+
// TODO: Implement with Prisma
|
|
1250
|
+
// await this.prisma.${camel}.delete({ where: { id } })
|
|
1251
|
+
throw new Error('Prisma ${pascal} repository not yet implemented')
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
`;
|
|
1255
|
+
}
|
|
1256
|
+
__name(generatePrismaRepository, "generatePrismaRepository");
|
|
915
1257
|
|
|
916
1258
|
// src/generators/templates/domain.ts
|
|
917
1259
|
function generateDomainService(pascal, kebab) {
|
|
@@ -1110,9 +1452,9 @@ describe('${pascal}Controller', () => {
|
|
|
1110
1452
|
`;
|
|
1111
1453
|
}
|
|
1112
1454
|
__name(generateControllerTest, "generateControllerTest");
|
|
1113
|
-
function generateRepositoryTest(pascal, kebab, plural) {
|
|
1455
|
+
function generateRepositoryTest(pascal, kebab, plural, repoImport = `../infrastructure/repositories/in-memory-${kebab}.repository`) {
|
|
1114
1456
|
return `import { describe, it, expect, beforeEach } from 'vitest'
|
|
1115
|
-
import { InMemory${pascal}Repository } from '
|
|
1457
|
+
import { InMemory${pascal}Repository } from '${repoImport}'
|
|
1116
1458
|
|
|
1117
1459
|
describe('InMemory${pascal}Repository', () => {
|
|
1118
1460
|
let repo: InMemory${pascal}Repository
|
|
@@ -1156,42 +1498,574 @@ describe('InMemory${pascal}Repository', () => {
|
|
|
1156
1498
|
pagination: { page: 1, limit: 2, offset: 0 },
|
|
1157
1499
|
})
|
|
1158
1500
|
|
|
1159
|
-
expect(result.data).toHaveLength(2)
|
|
1160
|
-
expect(result.total).toBe(3)
|
|
1161
|
-
})
|
|
1162
|
-
|
|
1163
|
-
it('should update a ${kebab}', async () => {
|
|
1164
|
-
const created = await repo.create({ name: 'Original' })
|
|
1165
|
-
const updated = await repo.update(created.id, { name: 'Updated' })
|
|
1166
|
-
expect(updated.name).toBe('Updated')
|
|
1167
|
-
})
|
|
1501
|
+
expect(result.data).toHaveLength(2)
|
|
1502
|
+
expect(result.total).toBe(3)
|
|
1503
|
+
})
|
|
1504
|
+
|
|
1505
|
+
it('should update a ${kebab}', async () => {
|
|
1506
|
+
const created = await repo.create({ name: 'Original' })
|
|
1507
|
+
const updated = await repo.update(created.id, { name: 'Updated' })
|
|
1508
|
+
expect(updated.name).toBe('Updated')
|
|
1509
|
+
})
|
|
1510
|
+
|
|
1511
|
+
it('should delete a ${kebab}', async () => {
|
|
1512
|
+
const created = await repo.create({ name: 'To Delete' })
|
|
1513
|
+
await repo.delete(created.id)
|
|
1514
|
+
const found = await repo.findById(created.id)
|
|
1515
|
+
expect(found).toBeNull()
|
|
1516
|
+
})
|
|
1517
|
+
})
|
|
1518
|
+
`;
|
|
1519
|
+
}
|
|
1520
|
+
__name(generateRepositoryTest, "generateRepositoryTest");
|
|
1521
|
+
|
|
1522
|
+
// src/generators/templates/rest-service.ts
|
|
1523
|
+
function generateRestService(pascal, kebab) {
|
|
1524
|
+
return `import { Service, Inject, HttpException } from '@forinda/kickjs-core'
|
|
1525
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
1526
|
+
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from './${kebab}.repository'
|
|
1527
|
+
import type { ${pascal}ResponseDTO } from './dtos/${kebab}-response.dto'
|
|
1528
|
+
import type { Create${pascal}DTO } from './dtos/create-${kebab}.dto'
|
|
1529
|
+
import type { Update${pascal}DTO } from './dtos/update-${kebab}.dto'
|
|
1530
|
+
|
|
1531
|
+
@Service()
|
|
1532
|
+
export class ${pascal}Service {
|
|
1533
|
+
constructor(
|
|
1534
|
+
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
1535
|
+
) {}
|
|
1536
|
+
|
|
1537
|
+
async findById(id: string): Promise<${pascal}ResponseDTO | null> {
|
|
1538
|
+
return this.repo.findById(id)
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
async findAll(): Promise<${pascal}ResponseDTO[]> {
|
|
1542
|
+
return this.repo.findAll()
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
async findPaginated(parsed: ParsedQuery) {
|
|
1546
|
+
return this.repo.findPaginated(parsed)
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
1550
|
+
return this.repo.create(dto)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
1554
|
+
return this.repo.update(id, dto)
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
async delete(id: string): Promise<void> {
|
|
1558
|
+
await this.repo.delete(id)
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
`;
|
|
1562
|
+
}
|
|
1563
|
+
__name(generateRestService, "generateRestService");
|
|
1564
|
+
function generateRestConstants(pascal) {
|
|
1565
|
+
return `import type { QueryFieldConfig } from '@forinda/kickjs-http'
|
|
1566
|
+
|
|
1567
|
+
export const ${pascal.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
|
|
1568
|
+
filterable: ['name'],
|
|
1569
|
+
sortable: ['name', 'createdAt'],
|
|
1570
|
+
searchable: ['name'],
|
|
1571
|
+
}
|
|
1572
|
+
`;
|
|
1573
|
+
}
|
|
1574
|
+
__name(generateRestConstants, "generateRestConstants");
|
|
1575
|
+
|
|
1576
|
+
// src/generators/templates/cqrs.ts
|
|
1577
|
+
function generateCqrsModuleIndex(pascal, kebab, plural, repo) {
|
|
1578
|
+
const repoClassMap = {
|
|
1579
|
+
inmemory: `InMemory${pascal}Repository`,
|
|
1580
|
+
drizzle: `Drizzle${pascal}Repository`,
|
|
1581
|
+
prisma: `Prisma${pascal}Repository`
|
|
1582
|
+
};
|
|
1583
|
+
const repoFileMap = {
|
|
1584
|
+
inmemory: `in-memory-${kebab}`,
|
|
1585
|
+
drizzle: `drizzle-${kebab}`,
|
|
1586
|
+
prisma: `prisma-${kebab}`
|
|
1587
|
+
};
|
|
1588
|
+
const repoClass = repoClassMap[repo] ?? repoClassMap.inmemory;
|
|
1589
|
+
const repoFile = repoFileMap[repo] ?? repoFileMap.inmemory;
|
|
1590
|
+
return `/**
|
|
1591
|
+
* ${pascal} Module \u2014 CQRS Pattern
|
|
1592
|
+
*
|
|
1593
|
+
* Separates read (queries) and write (commands) operations.
|
|
1594
|
+
* Events are emitted after state changes and can be handled via
|
|
1595
|
+
* WebSocket broadcasts, queue jobs, or ETL pipelines.
|
|
1596
|
+
*
|
|
1597
|
+
* Structure:
|
|
1598
|
+
* commands/ \u2014 Write operations (create, update, delete)
|
|
1599
|
+
* queries/ \u2014 Read operations (get, list)
|
|
1600
|
+
* events/ \u2014 Domain events + handlers (WS broadcast, queue dispatch)
|
|
1601
|
+
* dtos/ \u2014 Request/response schemas
|
|
1602
|
+
*/
|
|
1603
|
+
import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
|
|
1604
|
+
import { buildRoutes } from '@forinda/kickjs-http'
|
|
1605
|
+
import { ${pascal.toUpperCase()}_REPOSITORY } from './${kebab}.repository'
|
|
1606
|
+
import { ${repoClass} } from './${repoFile}.repository'
|
|
1607
|
+
import { ${pascal}Controller } from './${kebab}.controller'
|
|
1608
|
+
|
|
1609
|
+
// Eagerly load decorated classes
|
|
1610
|
+
import.meta.glob(
|
|
1611
|
+
[
|
|
1612
|
+
'./commands/**/*.ts',
|
|
1613
|
+
'./queries/**/*.ts',
|
|
1614
|
+
'./events/**/*.ts',
|
|
1615
|
+
'!./**/*.test.ts',
|
|
1616
|
+
],
|
|
1617
|
+
{ eager: true },
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
export class ${pascal}Module implements AppModule {
|
|
1621
|
+
register(container: Container): void {
|
|
1622
|
+
container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
|
|
1623
|
+
container.resolve(${repoClass}),
|
|
1624
|
+
)
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
routes(): ModuleRoutes {
|
|
1628
|
+
return {
|
|
1629
|
+
path: '/${plural}',
|
|
1630
|
+
router: buildRoutes(${pascal}Controller),
|
|
1631
|
+
controller: ${pascal}Controller,
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
`;
|
|
1636
|
+
}
|
|
1637
|
+
__name(generateCqrsModuleIndex, "generateCqrsModuleIndex");
|
|
1638
|
+
function generateCqrsController(pascal, kebab, plural, pluralPascal) {
|
|
1639
|
+
const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
1640
|
+
return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs-core'
|
|
1641
|
+
import type { RequestContext } from '@forinda/kickjs-http'
|
|
1642
|
+
import { ApiTags } from '@forinda/kickjs-swagger'
|
|
1643
|
+
import { Create${pascal}Command } from './commands/create-${kebab}.command'
|
|
1644
|
+
import { Update${pascal}Command } from './commands/update-${kebab}.command'
|
|
1645
|
+
import { Delete${pascal}Command } from './commands/delete-${kebab}.command'
|
|
1646
|
+
import { Get${pascal}Query } from './queries/get-${kebab}.query'
|
|
1647
|
+
import { List${pluralPascal}Query } from './queries/list-${plural}.query'
|
|
1648
|
+
import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
|
|
1649
|
+
import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
|
|
1650
|
+
import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
|
|
1651
|
+
|
|
1652
|
+
@Controller()
|
|
1653
|
+
export class ${pascal}Controller {
|
|
1654
|
+
@Autowired() private create${pascal}Command!: Create${pascal}Command
|
|
1655
|
+
@Autowired() private update${pascal}Command!: Update${pascal}Command
|
|
1656
|
+
@Autowired() private delete${pascal}Command!: Delete${pascal}Command
|
|
1657
|
+
@Autowired() private get${pascal}Query!: Get${pascal}Query
|
|
1658
|
+
@Autowired() private list${pluralPascal}Query!: List${pluralPascal}Query
|
|
1659
|
+
|
|
1660
|
+
@Get('/')
|
|
1661
|
+
@ApiTags('${pascal}')
|
|
1662
|
+
@ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
|
|
1663
|
+
async list(ctx: RequestContext) {
|
|
1664
|
+
return ctx.paginate(
|
|
1665
|
+
(parsed) => this.list${pluralPascal}Query.execute(parsed),
|
|
1666
|
+
${pascal.toUpperCase()}_QUERY_CONFIG,
|
|
1667
|
+
)
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
@Get('/:id')
|
|
1671
|
+
@ApiTags('${pascal}')
|
|
1672
|
+
async getById(ctx: RequestContext) {
|
|
1673
|
+
const result = await this.get${pascal}Query.execute(ctx.params.id)
|
|
1674
|
+
if (!result) return ctx.notFound('${pascal} not found')
|
|
1675
|
+
ctx.json(result)
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
@Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
|
|
1679
|
+
@ApiTags('${pascal}')
|
|
1680
|
+
async create(ctx: RequestContext) {
|
|
1681
|
+
const result = await this.create${pascal}Command.execute(ctx.body)
|
|
1682
|
+
ctx.created(result)
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
@Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
|
|
1686
|
+
@ApiTags('${pascal}')
|
|
1687
|
+
async update(ctx: RequestContext) {
|
|
1688
|
+
const result = await this.update${pascal}Command.execute(ctx.params.id, ctx.body)
|
|
1689
|
+
ctx.json(result)
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
@Delete('/:id')
|
|
1693
|
+
@ApiTags('${pascal}')
|
|
1694
|
+
async remove(ctx: RequestContext) {
|
|
1695
|
+
await this.delete${pascal}Command.execute(ctx.params.id)
|
|
1696
|
+
ctx.noContent()
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
`;
|
|
1700
|
+
}
|
|
1701
|
+
__name(generateCqrsController, "generateCqrsController");
|
|
1702
|
+
function generateCqrsCommands(pascal, kebab) {
|
|
1703
|
+
return [
|
|
1704
|
+
{
|
|
1705
|
+
file: `create-${kebab}.command.ts`,
|
|
1706
|
+
content: `import { Service, Inject } from '@forinda/kickjs-core'
|
|
1707
|
+
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
|
|
1708
|
+
import type { Create${pascal}DTO } from '../dtos/create-${kebab}.dto'
|
|
1709
|
+
import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
|
|
1710
|
+
import { ${pascal}Events } from '../events/${kebab}.events'
|
|
1711
|
+
|
|
1712
|
+
@Service()
|
|
1713
|
+
export class Create${pascal}Command {
|
|
1714
|
+
constructor(
|
|
1715
|
+
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
1716
|
+
@Inject(${pascal}Events) private readonly events: ${pascal}Events,
|
|
1717
|
+
) {}
|
|
1718
|
+
|
|
1719
|
+
async execute(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
1720
|
+
const result = await this.repo.create(dto)
|
|
1721
|
+
this.events.emit('${kebab}.created', result)
|
|
1722
|
+
return result
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
`
|
|
1726
|
+
},
|
|
1727
|
+
{
|
|
1728
|
+
file: `update-${kebab}.command.ts`,
|
|
1729
|
+
content: `import { Service, Inject } from '@forinda/kickjs-core'
|
|
1730
|
+
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
|
|
1731
|
+
import type { Update${pascal}DTO } from '../dtos/update-${kebab}.dto'
|
|
1732
|
+
import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
|
|
1733
|
+
import { ${pascal}Events } from '../events/${kebab}.events'
|
|
1734
|
+
|
|
1735
|
+
@Service()
|
|
1736
|
+
export class Update${pascal}Command {
|
|
1737
|
+
constructor(
|
|
1738
|
+
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
1739
|
+
@Inject(${pascal}Events) private readonly events: ${pascal}Events,
|
|
1740
|
+
) {}
|
|
1741
|
+
|
|
1742
|
+
async execute(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
1743
|
+
const result = await this.repo.update(id, dto)
|
|
1744
|
+
this.events.emit('${kebab}.updated', result)
|
|
1745
|
+
return result
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
`
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
file: `delete-${kebab}.command.ts`,
|
|
1752
|
+
content: `import { Service, Inject } from '@forinda/kickjs-core'
|
|
1753
|
+
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
|
|
1754
|
+
import { ${pascal}Events } from '../events/${kebab}.events'
|
|
1755
|
+
|
|
1756
|
+
@Service()
|
|
1757
|
+
export class Delete${pascal}Command {
|
|
1758
|
+
constructor(
|
|
1759
|
+
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
1760
|
+
@Inject(${pascal}Events) private readonly events: ${pascal}Events,
|
|
1761
|
+
) {}
|
|
1762
|
+
|
|
1763
|
+
async execute(id: string): Promise<void> {
|
|
1764
|
+
await this.repo.delete(id)
|
|
1765
|
+
this.events.emit('${kebab}.deleted', { id })
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
`
|
|
1769
|
+
}
|
|
1770
|
+
];
|
|
1771
|
+
}
|
|
1772
|
+
__name(generateCqrsCommands, "generateCqrsCommands");
|
|
1773
|
+
function generateCqrsQueries(pascal, kebab, plural, pluralPascal) {
|
|
1774
|
+
return [
|
|
1775
|
+
{
|
|
1776
|
+
file: `get-${kebab}.query.ts`,
|
|
1777
|
+
content: `import { Service, Inject } from '@forinda/kickjs-core'
|
|
1778
|
+
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
|
|
1779
|
+
import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
|
|
1780
|
+
|
|
1781
|
+
@Service()
|
|
1782
|
+
export class Get${pascal}Query {
|
|
1783
|
+
constructor(
|
|
1784
|
+
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
1785
|
+
) {}
|
|
1786
|
+
|
|
1787
|
+
async execute(id: string): Promise<${pascal}ResponseDTO | null> {
|
|
1788
|
+
return this.repo.findById(id)
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
`
|
|
1792
|
+
},
|
|
1793
|
+
{
|
|
1794
|
+
file: `list-${plural}.query.ts`,
|
|
1795
|
+
content: `import { Service, Inject } from '@forinda/kickjs-core'
|
|
1796
|
+
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
|
|
1797
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
1798
|
+
|
|
1799
|
+
@Service()
|
|
1800
|
+
export class List${pluralPascal}Query {
|
|
1801
|
+
constructor(
|
|
1802
|
+
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
1803
|
+
) {}
|
|
1804
|
+
|
|
1805
|
+
async execute(parsed: ParsedQuery) {
|
|
1806
|
+
return this.repo.findPaginated(parsed)
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
`
|
|
1810
|
+
}
|
|
1811
|
+
];
|
|
1812
|
+
}
|
|
1813
|
+
__name(generateCqrsQueries, "generateCqrsQueries");
|
|
1814
|
+
function generateCqrsEvents(pascal, kebab) {
|
|
1815
|
+
return [
|
|
1816
|
+
{
|
|
1817
|
+
file: `${kebab}.events.ts`,
|
|
1818
|
+
content: `import { Service } from '@forinda/kickjs-core'
|
|
1819
|
+
import { EventEmitter } from 'node:events'
|
|
1820
|
+
import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* ${pascal} domain event types.
|
|
1824
|
+
*
|
|
1825
|
+
* These events are emitted by commands after state changes.
|
|
1826
|
+
* Subscribe to them in event handlers for side effects:
|
|
1827
|
+
* - WebSocket broadcasts (real-time UI updates)
|
|
1828
|
+
* - Queue jobs (async processing, ETL pipelines)
|
|
1829
|
+
* - Audit logging
|
|
1830
|
+
* - Cache invalidation
|
|
1831
|
+
*/
|
|
1832
|
+
export interface ${pascal}EventMap {
|
|
1833
|
+
'${kebab}.created': ${pascal}ResponseDTO
|
|
1834
|
+
'${kebab}.updated': ${pascal}ResponseDTO
|
|
1835
|
+
'${kebab}.deleted': { id: string }
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
@Service()
|
|
1839
|
+
export class ${pascal}Events {
|
|
1840
|
+
private emitter = new EventEmitter()
|
|
1841
|
+
|
|
1842
|
+
emit<K extends keyof ${pascal}EventMap>(event: K, data: ${pascal}EventMap[K]): void {
|
|
1843
|
+
this.emitter.emit(event, data)
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
on<K extends keyof ${pascal}EventMap>(event: K, handler: (data: ${pascal}EventMap[K]) => void): void {
|
|
1847
|
+
this.emitter.on(event, handler)
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
off<K extends keyof ${pascal}EventMap>(event: K, handler: (data: ${pascal}EventMap[K]) => void): void {
|
|
1851
|
+
this.emitter.off(event, handler)
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
`
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
file: `on-${kebab}-change.handler.ts`,
|
|
1858
|
+
content: `import { Service, Autowired } from '@forinda/kickjs-core'
|
|
1859
|
+
import { ${pascal}Events } from './${kebab}.events'
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* ${pascal} Change Event Handler
|
|
1863
|
+
*
|
|
1864
|
+
* Reacts to domain events emitted by commands.
|
|
1865
|
+
* Wire up side effects here:
|
|
1866
|
+
*
|
|
1867
|
+
* 1. WebSocket broadcast \u2014 notify connected clients in real-time
|
|
1868
|
+
* import { WsGateway } from '@forinda/kickjs-ws'
|
|
1869
|
+
* this.ws.broadcast('${kebab}-channel', { event, data })
|
|
1870
|
+
*
|
|
1871
|
+
* 2. Queue dispatch \u2014 offload heavy processing to background workers
|
|
1872
|
+
* import { QueueService } from '@forinda/kickjs-queue'
|
|
1873
|
+
* this.queue.add('${kebab}-etl', { action: event, payload: data })
|
|
1874
|
+
*
|
|
1875
|
+
* 3. ETL pipeline \u2014 transform and load data to external systems
|
|
1876
|
+
* await this.etlPipeline.process(data)
|
|
1877
|
+
*/
|
|
1878
|
+
@Service()
|
|
1879
|
+
export class On${pascal}ChangeHandler {
|
|
1880
|
+
@Autowired() private events!: ${pascal}Events
|
|
1881
|
+
|
|
1882
|
+
// Uncomment to inject WebSocket and Queue services:
|
|
1883
|
+
// @Autowired() private ws!: WsGateway
|
|
1884
|
+
// @Autowired() private queue!: QueueService
|
|
1885
|
+
|
|
1886
|
+
onInit(): void {
|
|
1887
|
+
this.events.on('${kebab}.created', (data) => {
|
|
1888
|
+
console.log('[${pascal}] Created:', data.id)
|
|
1889
|
+
// TODO: Broadcast via WebSocket
|
|
1890
|
+
// this.ws.broadcast('${kebab}-channel', { event: '${kebab}.created', data })
|
|
1891
|
+
// TODO: Dispatch to queue for async processing / ETL
|
|
1892
|
+
// this.queue.add('${kebab}-etl', { action: 'create', payload: data })
|
|
1893
|
+
})
|
|
1894
|
+
|
|
1895
|
+
this.events.on('${kebab}.updated', (data) => {
|
|
1896
|
+
console.log('[${pascal}] Updated:', data.id)
|
|
1897
|
+
// TODO: Broadcast via WebSocket
|
|
1898
|
+
// this.ws.broadcast('${kebab}-channel', { event: '${kebab}.updated', data })
|
|
1899
|
+
})
|
|
1168
1900
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
}
|
|
1175
|
-
})
|
|
1176
|
-
`;
|
|
1901
|
+
this.events.on('${kebab}.deleted', (data) => {
|
|
1902
|
+
console.log('[${pascal}] Deleted:', data.id)
|
|
1903
|
+
// TODO: Broadcast via WebSocket
|
|
1904
|
+
// this.ws.broadcast('${kebab}-channel', { event: '${kebab}.deleted', data })
|
|
1905
|
+
})
|
|
1906
|
+
}
|
|
1177
1907
|
}
|
|
1178
|
-
|
|
1908
|
+
`
|
|
1909
|
+
}
|
|
1910
|
+
];
|
|
1911
|
+
}
|
|
1912
|
+
__name(generateCqrsEvents, "generateCqrsEvents");
|
|
1179
1913
|
|
|
1180
1914
|
// src/generators/module.ts
|
|
1915
|
+
function promptUser(question) {
|
|
1916
|
+
const rl = createInterface2({
|
|
1917
|
+
input: process.stdin,
|
|
1918
|
+
output: process.stdout
|
|
1919
|
+
});
|
|
1920
|
+
return new Promise((resolve8) => {
|
|
1921
|
+
rl.question(question, (answer) => {
|
|
1922
|
+
rl.close();
|
|
1923
|
+
resolve8(answer.trim().toLowerCase());
|
|
1924
|
+
});
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
__name(promptUser, "promptUser");
|
|
1181
1928
|
async function generateModule(options) {
|
|
1182
|
-
const { name, modulesDir, noEntity, noTests, repo = "inmemory",
|
|
1929
|
+
const { name, modulesDir, noEntity, noTests, repo = "inmemory", force } = options;
|
|
1930
|
+
let pattern = options.pattern ?? "ddd";
|
|
1931
|
+
if (options.minimal) pattern = "minimal";
|
|
1183
1932
|
const kebab = toKebabCase(name);
|
|
1184
1933
|
const pascal = toPascalCase(name);
|
|
1185
|
-
const camel = toCamelCase(name);
|
|
1186
1934
|
const plural = pluralize(kebab);
|
|
1187
1935
|
const pluralPascal = pluralizePascal(pascal);
|
|
1188
1936
|
const moduleDir = join2(modulesDir, plural);
|
|
1189
1937
|
const files = [];
|
|
1938
|
+
let overwriteAll = force ?? false;
|
|
1190
1939
|
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
1191
1940
|
const fullPath = join2(moduleDir, relativePath);
|
|
1941
|
+
if (!overwriteAll && await fileExists(fullPath)) {
|
|
1942
|
+
const answer = await promptUser(` File already exists: ${relativePath}
|
|
1943
|
+
Overwrite? (y/n/a = yes/no/all) `);
|
|
1944
|
+
if (answer === "a") {
|
|
1945
|
+
overwriteAll = true;
|
|
1946
|
+
} else if (answer !== "y") {
|
|
1947
|
+
console.log(` Skipped: ${relativePath}`);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1192
1951
|
await writeFileSafe(fullPath, content);
|
|
1193
1952
|
files.push(fullPath);
|
|
1194
1953
|
}, "write");
|
|
1954
|
+
const ctx = {
|
|
1955
|
+
kebab,
|
|
1956
|
+
pascal,
|
|
1957
|
+
plural,
|
|
1958
|
+
pluralPascal,
|
|
1959
|
+
moduleDir,
|
|
1960
|
+
repo,
|
|
1961
|
+
noEntity: noEntity ?? false,
|
|
1962
|
+
noTests: noTests ?? false,
|
|
1963
|
+
write,
|
|
1964
|
+
files
|
|
1965
|
+
};
|
|
1966
|
+
switch (pattern) {
|
|
1967
|
+
case "minimal":
|
|
1968
|
+
await generateMinimalFiles(ctx);
|
|
1969
|
+
break;
|
|
1970
|
+
case "rest":
|
|
1971
|
+
await generateRestFiles(ctx);
|
|
1972
|
+
break;
|
|
1973
|
+
case "cqrs":
|
|
1974
|
+
await generateCqrsFiles(ctx);
|
|
1975
|
+
break;
|
|
1976
|
+
case "graphql":
|
|
1977
|
+
case "ddd":
|
|
1978
|
+
default:
|
|
1979
|
+
await generateDddFiles(ctx);
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1982
|
+
await autoRegisterModule(modulesDir, pascal, plural);
|
|
1983
|
+
return files;
|
|
1984
|
+
}
|
|
1985
|
+
__name(generateModule, "generateModule");
|
|
1986
|
+
async function generateMinimalFiles(ctx) {
|
|
1987
|
+
const { pascal, kebab, plural, write } = ctx;
|
|
1988
|
+
await write("index.ts", generateMinimalModuleIndex(pascal, kebab, plural));
|
|
1989
|
+
await write(`${kebab}.controller.ts`, `import { Controller, Get } from '@forinda/kickjs-core'
|
|
1990
|
+
import type { RequestContext } from '@forinda/kickjs-http'
|
|
1991
|
+
|
|
1992
|
+
@Controller()
|
|
1993
|
+
export class ${pascal}Controller {
|
|
1994
|
+
@Get('/')
|
|
1995
|
+
async list(ctx: RequestContext) {
|
|
1996
|
+
ctx.json({ message: '${pascal} list' })
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
`);
|
|
2000
|
+
}
|
|
2001
|
+
__name(generateMinimalFiles, "generateMinimalFiles");
|
|
2002
|
+
async function generateRestFiles(ctx) {
|
|
2003
|
+
const { pascal, kebab, plural, pluralPascal, repo, noTests, write } = ctx;
|
|
2004
|
+
await write("index.ts", generateRestModuleIndex(pascal, kebab, plural, repo));
|
|
2005
|
+
await write(`${kebab}.constants.ts`, generateRestConstants(pascal));
|
|
2006
|
+
await write(`${kebab}.controller.ts`, generateRestController(pascal, kebab, plural, pluralPascal));
|
|
2007
|
+
await write(`${kebab}.service.ts`, generateRestService(pascal, kebab));
|
|
2008
|
+
await write(`dtos/create-${kebab}.dto.ts`, generateCreateDTO(pascal, kebab));
|
|
2009
|
+
await write(`dtos/update-${kebab}.dto.ts`, generateUpdateDTO(pascal, kebab));
|
|
2010
|
+
await write(`dtos/${kebab}-response.dto.ts`, generateResponseDTO(pascal, kebab));
|
|
2011
|
+
await write(`${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab, "./dtos"));
|
|
2012
|
+
const repoFileMap = {
|
|
2013
|
+
inmemory: `in-memory-${kebab}`,
|
|
2014
|
+
drizzle: `drizzle-${kebab}`,
|
|
2015
|
+
prisma: `prisma-${kebab}`
|
|
2016
|
+
};
|
|
2017
|
+
const repoGeneratorMap = {
|
|
2018
|
+
inmemory: /* @__PURE__ */ __name(() => generateInMemoryRepository(pascal, kebab, ".", "./dtos"), "inmemory"),
|
|
2019
|
+
drizzle: /* @__PURE__ */ __name(() => generateDrizzleRepository(pascal, kebab, ".", "./dtos"), "drizzle"),
|
|
2020
|
+
prisma: /* @__PURE__ */ __name(() => generatePrismaRepository(pascal, kebab, ".", "./dtos"), "prisma")
|
|
2021
|
+
};
|
|
2022
|
+
await write(`${repoFileMap[repo]}.repository.ts`, repoGeneratorMap[repo]());
|
|
2023
|
+
if (!noTests) {
|
|
2024
|
+
await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
|
|
2025
|
+
await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural, `../${repoFileMap.inmemory}.repository`));
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
__name(generateRestFiles, "generateRestFiles");
|
|
2029
|
+
async function generateCqrsFiles(ctx) {
|
|
2030
|
+
const { pascal, kebab, plural, pluralPascal, repo, noTests, write } = ctx;
|
|
2031
|
+
await write("index.ts", generateCqrsModuleIndex(pascal, kebab, plural, repo));
|
|
2032
|
+
await write(`${kebab}.constants.ts`, generateRestConstants(pascal));
|
|
2033
|
+
await write(`${kebab}.controller.ts`, generateCqrsController(pascal, kebab, plural, pluralPascal));
|
|
2034
|
+
await write(`dtos/create-${kebab}.dto.ts`, generateCreateDTO(pascal, kebab));
|
|
2035
|
+
await write(`dtos/update-${kebab}.dto.ts`, generateUpdateDTO(pascal, kebab));
|
|
2036
|
+
await write(`dtos/${kebab}-response.dto.ts`, generateResponseDTO(pascal, kebab));
|
|
2037
|
+
const commands = generateCqrsCommands(pascal, kebab);
|
|
2038
|
+
for (const cmd of commands) {
|
|
2039
|
+
await write(`commands/${cmd.file}`, cmd.content);
|
|
2040
|
+
}
|
|
2041
|
+
const queries = generateCqrsQueries(pascal, kebab, plural, pluralPascal);
|
|
2042
|
+
for (const q of queries) {
|
|
2043
|
+
await write(`queries/${q.file}`, q.content);
|
|
2044
|
+
}
|
|
2045
|
+
const events = generateCqrsEvents(pascal, kebab);
|
|
2046
|
+
for (const e of events) {
|
|
2047
|
+
await write(`events/${e.file}`, e.content);
|
|
2048
|
+
}
|
|
2049
|
+
await write(`${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab, "./dtos"));
|
|
2050
|
+
const repoFileMap = {
|
|
2051
|
+
inmemory: `in-memory-${kebab}`,
|
|
2052
|
+
drizzle: `drizzle-${kebab}`,
|
|
2053
|
+
prisma: `prisma-${kebab}`
|
|
2054
|
+
};
|
|
2055
|
+
const repoGeneratorMap = {
|
|
2056
|
+
inmemory: /* @__PURE__ */ __name(() => generateInMemoryRepository(pascal, kebab, ".", "./dtos"), "inmemory"),
|
|
2057
|
+
drizzle: /* @__PURE__ */ __name(() => generateDrizzleRepository(pascal, kebab, ".", "./dtos"), "drizzle"),
|
|
2058
|
+
prisma: /* @__PURE__ */ __name(() => generatePrismaRepository(pascal, kebab, ".", "./dtos"), "prisma")
|
|
2059
|
+
};
|
|
2060
|
+
await write(`${repoFileMap[repo]}.repository.ts`, repoGeneratorMap[repo]());
|
|
2061
|
+
if (!noTests) {
|
|
2062
|
+
await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
|
|
2063
|
+
await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural, `../${repoFileMap.inmemory}.repository`));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
__name(generateCqrsFiles, "generateCqrsFiles");
|
|
2067
|
+
async function generateDddFiles(ctx) {
|
|
2068
|
+
const { pascal, kebab, plural, pluralPascal, repo, noEntity, noTests, write } = ctx;
|
|
1195
2069
|
await write("index.ts", generateModuleIndex(pascal, kebab, plural, repo));
|
|
1196
2070
|
await write("constants.ts", generateConstants(pascal));
|
|
1197
2071
|
await write(`presentation/${kebab}.controller.ts`, generateController(pascal, kebab, plural, pluralPascal));
|
|
@@ -1204,10 +2078,18 @@ async function generateModule(options) {
|
|
|
1204
2078
|
}
|
|
1205
2079
|
await write(`domain/repositories/${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab));
|
|
1206
2080
|
await write(`domain/services/${kebab}-domain.service.ts`, generateDomainService(pascal, kebab));
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
2081
|
+
const repoFileMap = {
|
|
2082
|
+
inmemory: `in-memory-${kebab}`,
|
|
2083
|
+
drizzle: `drizzle-${kebab}`,
|
|
2084
|
+
prisma: `prisma-${kebab}`
|
|
2085
|
+
};
|
|
2086
|
+
const repoGeneratorMap = {
|
|
2087
|
+
inmemory: /* @__PURE__ */ __name(() => generateInMemoryRepository(pascal, kebab), "inmemory"),
|
|
2088
|
+
drizzle: /* @__PURE__ */ __name(() => generateDrizzleRepository(pascal, kebab), "drizzle"),
|
|
2089
|
+
prisma: /* @__PURE__ */ __name(() => generatePrismaRepository(pascal, kebab), "prisma")
|
|
2090
|
+
};
|
|
2091
|
+
await write(`infrastructure/repositories/${repoFileMap[repo]}.repository.ts`, repoGeneratorMap[repo]());
|
|
2092
|
+
if (!noEntity) {
|
|
1211
2093
|
await write(`domain/entities/${kebab}.entity.ts`, generateEntity(pascal, kebab));
|
|
1212
2094
|
await write(`domain/value-objects/${kebab}-id.vo.ts`, generateValueObject(pascal, kebab));
|
|
1213
2095
|
}
|
|
@@ -1215,10 +2097,8 @@ async function generateModule(options) {
|
|
|
1215
2097
|
await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
|
|
1216
2098
|
await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural));
|
|
1217
2099
|
}
|
|
1218
|
-
await autoRegisterModule(modulesDir, pascal, plural);
|
|
1219
|
-
return files;
|
|
1220
2100
|
}
|
|
1221
|
-
__name(
|
|
2101
|
+
__name(generateDddFiles, "generateDddFiles");
|
|
1222
2102
|
async function autoRegisterModule(modulesDir, pascal, plural) {
|
|
1223
2103
|
const indexPath = join2(modulesDir, "index.ts");
|
|
1224
2104
|
const exists = await fileExists(indexPath);
|
|
@@ -1354,13 +2234,64 @@ export class ${pascal}Adapter implements AppAdapter {
|
|
|
1354
2234
|
__name(generateAdapter, "generateAdapter");
|
|
1355
2235
|
|
|
1356
2236
|
// src/generators/middleware.ts
|
|
1357
|
-
import { join as
|
|
2237
|
+
import { join as join5 } from "path";
|
|
2238
|
+
|
|
2239
|
+
// src/utils/resolve-out-dir.ts
|
|
2240
|
+
import { resolve as resolve2, join as join4 } from "path";
|
|
2241
|
+
var DDD_FOLDER_MAP = {
|
|
2242
|
+
controller: "presentation",
|
|
2243
|
+
service: "domain/services",
|
|
2244
|
+
dto: "application/dtos",
|
|
2245
|
+
guard: "presentation/guards",
|
|
2246
|
+
middleware: "middleware"
|
|
2247
|
+
};
|
|
2248
|
+
var FLAT_FOLDER_MAP = {
|
|
2249
|
+
controller: "",
|
|
2250
|
+
service: "",
|
|
2251
|
+
dto: "dtos",
|
|
2252
|
+
guard: "guards",
|
|
2253
|
+
middleware: "middleware"
|
|
2254
|
+
};
|
|
2255
|
+
var CQRS_FOLDER_MAP = {
|
|
2256
|
+
controller: "",
|
|
2257
|
+
service: "",
|
|
2258
|
+
dto: "dtos",
|
|
2259
|
+
guard: "guards",
|
|
2260
|
+
middleware: "middleware",
|
|
2261
|
+
command: "commands",
|
|
2262
|
+
query: "queries",
|
|
2263
|
+
event: "events"
|
|
2264
|
+
};
|
|
2265
|
+
function resolveOutDir(options) {
|
|
2266
|
+
const { type, outDir, moduleName, modulesDir = "src/modules", defaultDir, pattern = "ddd" } = options;
|
|
2267
|
+
if (outDir) return resolve2(outDir);
|
|
2268
|
+
if (moduleName) {
|
|
2269
|
+
const folderMap = pattern === "ddd" ? DDD_FOLDER_MAP : pattern === "cqrs" ? CQRS_FOLDER_MAP : FLAT_FOLDER_MAP;
|
|
2270
|
+
const kebab = toKebabCase(moduleName);
|
|
2271
|
+
const plural = pluralize(kebab);
|
|
2272
|
+
const subfolder = folderMap[type] ?? "";
|
|
2273
|
+
const base = join4(modulesDir, plural);
|
|
2274
|
+
return resolve2(subfolder ? join4(base, subfolder) : base);
|
|
2275
|
+
}
|
|
2276
|
+
return resolve2(defaultDir);
|
|
2277
|
+
}
|
|
2278
|
+
__name(resolveOutDir, "resolveOutDir");
|
|
2279
|
+
|
|
2280
|
+
// src/generators/middleware.ts
|
|
1358
2281
|
async function generateMiddleware(options) {
|
|
1359
|
-
const { name,
|
|
2282
|
+
const { name, moduleName, modulesDir, pattern } = options;
|
|
2283
|
+
const outDir = resolveOutDir({
|
|
2284
|
+
type: "middleware",
|
|
2285
|
+
outDir: options.outDir,
|
|
2286
|
+
moduleName,
|
|
2287
|
+
modulesDir,
|
|
2288
|
+
defaultDir: "src/middleware",
|
|
2289
|
+
pattern
|
|
2290
|
+
});
|
|
1360
2291
|
const kebab = toKebabCase(name);
|
|
1361
2292
|
const camel = toCamelCase(name);
|
|
1362
2293
|
const files = [];
|
|
1363
|
-
const filePath =
|
|
2294
|
+
const filePath = join5(outDir, `${kebab}.middleware.ts`);
|
|
1364
2295
|
await writeFileSafe(filePath, `import type { Request, Response, NextFunction } from 'express'
|
|
1365
2296
|
|
|
1366
2297
|
export interface ${toPascalCase(name)}Options {
|
|
@@ -1392,14 +2323,22 @@ export function ${camel}(options: ${toPascalCase(name)}Options = {}) {
|
|
|
1392
2323
|
__name(generateMiddleware, "generateMiddleware");
|
|
1393
2324
|
|
|
1394
2325
|
// src/generators/guard.ts
|
|
1395
|
-
import { join as
|
|
2326
|
+
import { join as join6 } from "path";
|
|
1396
2327
|
async function generateGuard(options) {
|
|
1397
|
-
const { name,
|
|
2328
|
+
const { name, moduleName, modulesDir, pattern } = options;
|
|
2329
|
+
const outDir = resolveOutDir({
|
|
2330
|
+
type: "guard",
|
|
2331
|
+
outDir: options.outDir,
|
|
2332
|
+
moduleName,
|
|
2333
|
+
modulesDir,
|
|
2334
|
+
defaultDir: "src/guards",
|
|
2335
|
+
pattern
|
|
2336
|
+
});
|
|
1398
2337
|
const kebab = toKebabCase(name);
|
|
1399
2338
|
const camel = toCamelCase(name);
|
|
1400
2339
|
const pascal = toPascalCase(name);
|
|
1401
2340
|
const files = [];
|
|
1402
|
-
const filePath =
|
|
2341
|
+
const filePath = join6(outDir, `${kebab}.guard.ts`);
|
|
1403
2342
|
await writeFileSafe(filePath, `import { Container, HttpException } from '@forinda/kickjs-core'
|
|
1404
2343
|
import type { RequestContext } from '@forinda/kickjs-http'
|
|
1405
2344
|
|
|
@@ -1443,13 +2382,21 @@ export async function ${camel}Guard(ctx: RequestContext, next: () => void): Prom
|
|
|
1443
2382
|
__name(generateGuard, "generateGuard");
|
|
1444
2383
|
|
|
1445
2384
|
// src/generators/service.ts
|
|
1446
|
-
import { join as
|
|
2385
|
+
import { join as join7 } from "path";
|
|
1447
2386
|
async function generateService(options) {
|
|
1448
|
-
const { name,
|
|
2387
|
+
const { name, moduleName, modulesDir, pattern } = options;
|
|
2388
|
+
const outDir = resolveOutDir({
|
|
2389
|
+
type: "service",
|
|
2390
|
+
outDir: options.outDir,
|
|
2391
|
+
moduleName,
|
|
2392
|
+
modulesDir,
|
|
2393
|
+
defaultDir: "src/services",
|
|
2394
|
+
pattern
|
|
2395
|
+
});
|
|
1449
2396
|
const kebab = toKebabCase(name);
|
|
1450
2397
|
const pascal = toPascalCase(name);
|
|
1451
2398
|
const files = [];
|
|
1452
|
-
const filePath =
|
|
2399
|
+
const filePath = join7(outDir, `${kebab}.service.ts`);
|
|
1453
2400
|
await writeFileSafe(filePath, `import { Service } from '@forinda/kickjs-core'
|
|
1454
2401
|
|
|
1455
2402
|
@Service()
|
|
@@ -1466,13 +2413,21 @@ export class ${pascal}Service {
|
|
|
1466
2413
|
__name(generateService, "generateService");
|
|
1467
2414
|
|
|
1468
2415
|
// src/generators/controller.ts
|
|
1469
|
-
import { join as
|
|
2416
|
+
import { join as join8 } from "path";
|
|
1470
2417
|
async function generateController2(options) {
|
|
1471
|
-
const { name,
|
|
2418
|
+
const { name, moduleName, modulesDir, pattern } = options;
|
|
2419
|
+
const outDir = resolveOutDir({
|
|
2420
|
+
type: "controller",
|
|
2421
|
+
outDir: options.outDir,
|
|
2422
|
+
moduleName,
|
|
2423
|
+
modulesDir,
|
|
2424
|
+
defaultDir: "src/controllers",
|
|
2425
|
+
pattern
|
|
2426
|
+
});
|
|
1472
2427
|
const kebab = toKebabCase(name);
|
|
1473
2428
|
const pascal = toPascalCase(name);
|
|
1474
2429
|
const files = [];
|
|
1475
|
-
const filePath =
|
|
2430
|
+
const filePath = join8(outDir, `${kebab}.controller.ts`);
|
|
1476
2431
|
await writeFileSafe(filePath, `import { Controller, Get, Post, Autowired } from '@forinda/kickjs-core'
|
|
1477
2432
|
import type { RequestContext } from '@forinda/kickjs-http'
|
|
1478
2433
|
|
|
@@ -1497,14 +2452,22 @@ export class ${pascal}Controller {
|
|
|
1497
2452
|
__name(generateController2, "generateController");
|
|
1498
2453
|
|
|
1499
2454
|
// src/generators/dto.ts
|
|
1500
|
-
import { join as
|
|
2455
|
+
import { join as join9 } from "path";
|
|
1501
2456
|
async function generateDto(options) {
|
|
1502
|
-
const { name,
|
|
2457
|
+
const { name, moduleName, modulesDir, pattern } = options;
|
|
2458
|
+
const outDir = resolveOutDir({
|
|
2459
|
+
type: "dto",
|
|
2460
|
+
outDir: options.outDir,
|
|
2461
|
+
moduleName,
|
|
2462
|
+
modulesDir,
|
|
2463
|
+
defaultDir: "src/dtos",
|
|
2464
|
+
pattern
|
|
2465
|
+
});
|
|
1503
2466
|
const kebab = toKebabCase(name);
|
|
1504
2467
|
const pascal = toPascalCase(name);
|
|
1505
2468
|
const camel = toCamelCase(name);
|
|
1506
2469
|
const files = [];
|
|
1507
|
-
const filePath =
|
|
2470
|
+
const filePath = join9(outDir, `${kebab}.dto.ts`);
|
|
1508
2471
|
await writeFileSafe(filePath, `import { z } from 'zod'
|
|
1509
2472
|
|
|
1510
2473
|
export const ${camel}Schema = z.object({
|
|
@@ -1520,24 +2483,24 @@ export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
|
|
|
1520
2483
|
__name(generateDto, "generateDto");
|
|
1521
2484
|
|
|
1522
2485
|
// src/generators/config.ts
|
|
1523
|
-
import { join as
|
|
2486
|
+
import { join as join10 } from "path";
|
|
1524
2487
|
import { existsSync as existsSync2 } from "fs";
|
|
1525
|
-
import { createInterface as
|
|
2488
|
+
import { createInterface as createInterface3 } from "readline";
|
|
1526
2489
|
async function confirm2(message) {
|
|
1527
|
-
const rl =
|
|
2490
|
+
const rl = createInterface3({
|
|
1528
2491
|
input: process.stdin,
|
|
1529
2492
|
output: process.stdout
|
|
1530
2493
|
});
|
|
1531
|
-
return new Promise((
|
|
2494
|
+
return new Promise((resolve8) => {
|
|
1532
2495
|
rl.question(` ${message} (y/N) `, (answer) => {
|
|
1533
2496
|
rl.close();
|
|
1534
|
-
|
|
2497
|
+
resolve8(answer.trim().toLowerCase() === "y");
|
|
1535
2498
|
});
|
|
1536
2499
|
});
|
|
1537
2500
|
}
|
|
1538
2501
|
__name(confirm2, "confirm");
|
|
1539
2502
|
async function generateConfig(options) {
|
|
1540
|
-
const filePath =
|
|
2503
|
+
const filePath = join10(options.outDir, "kick.config.ts");
|
|
1541
2504
|
const modulesDir = options.modulesDir ?? "src/modules";
|
|
1542
2505
|
const defaultRepo = options.defaultRepo ?? "inmemory";
|
|
1543
2506
|
if (existsSync2(filePath) && !options.force) {
|
|
@@ -1585,7 +2548,7 @@ export default defineConfig({
|
|
|
1585
2548
|
__name(generateConfig, "generateConfig");
|
|
1586
2549
|
|
|
1587
2550
|
// src/generators/resolver.ts
|
|
1588
|
-
import { join as
|
|
2551
|
+
import { join as join11 } from "path";
|
|
1589
2552
|
async function generateResolver(options) {
|
|
1590
2553
|
const { name, outDir } = options;
|
|
1591
2554
|
const pascal = toPascalCase(name);
|
|
@@ -1593,7 +2556,7 @@ async function generateResolver(options) {
|
|
|
1593
2556
|
const camel = toCamelCase(name);
|
|
1594
2557
|
const files = [];
|
|
1595
2558
|
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
1596
|
-
const fullPath =
|
|
2559
|
+
const fullPath = join11(outDir, relativePath);
|
|
1597
2560
|
await writeFileSafe(fullPath, content);
|
|
1598
2561
|
files.push(fullPath);
|
|
1599
2562
|
}, "write");
|
|
@@ -1663,7 +2626,7 @@ export const ${camel}TypeDefs = \`
|
|
|
1663
2626
|
__name(generateResolver, "generateResolver");
|
|
1664
2627
|
|
|
1665
2628
|
// src/generators/job.ts
|
|
1666
|
-
import { join as
|
|
2629
|
+
import { join as join12 } from "path";
|
|
1667
2630
|
async function generateJob(options) {
|
|
1668
2631
|
const { name, outDir } = options;
|
|
1669
2632
|
const pascal = toPascalCase(name);
|
|
@@ -1672,7 +2635,7 @@ async function generateJob(options) {
|
|
|
1672
2635
|
const queueName = options.queue ?? `${kebab}-queue`;
|
|
1673
2636
|
const files = [];
|
|
1674
2637
|
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
1675
|
-
const fullPath =
|
|
2638
|
+
const fullPath = join12(outDir, relativePath);
|
|
1676
2639
|
await writeFileSafe(fullPath, content);
|
|
1677
2640
|
files.push(fullPath);
|
|
1678
2641
|
}, "write");
|
|
@@ -1715,7 +2678,7 @@ export class ${pascal}Job {
|
|
|
1715
2678
|
__name(generateJob, "generateJob");
|
|
1716
2679
|
|
|
1717
2680
|
// src/generators/scaffold.ts
|
|
1718
|
-
import { join as
|
|
2681
|
+
import { join as join13 } from "path";
|
|
1719
2682
|
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1720
2683
|
var TYPE_MAP = {
|
|
1721
2684
|
string: {
|
|
@@ -1811,10 +2774,10 @@ async function generateScaffold(options) {
|
|
|
1811
2774
|
const camel = toCamelCase(name);
|
|
1812
2775
|
const plural = pluralize(kebab);
|
|
1813
2776
|
const pluralPascal = pluralizePascal(pascal);
|
|
1814
|
-
const moduleDir =
|
|
2777
|
+
const moduleDir = join13(modulesDir, plural);
|
|
1815
2778
|
const files = [];
|
|
1816
2779
|
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
1817
|
-
const fullPath =
|
|
2780
|
+
const fullPath = join13(moduleDir, relativePath);
|
|
1818
2781
|
await writeFileSafe(fullPath, content);
|
|
1819
2782
|
files.push(fullPath);
|
|
1820
2783
|
}, "write");
|
|
@@ -2220,7 +3183,7 @@ export class Delete${pascal}UseCase {
|
|
|
2220
3183
|
}
|
|
2221
3184
|
__name(genUseCases, "genUseCases");
|
|
2222
3185
|
async function autoRegisterModule2(modulesDir, pascal, plural) {
|
|
2223
|
-
const indexPath =
|
|
3186
|
+
const indexPath = join13(modulesDir, "index.ts");
|
|
2224
3187
|
const exists = await fileExists(indexPath);
|
|
2225
3188
|
if (!exists) {
|
|
2226
3189
|
await writeFileSafe(indexPath, `import type { AppModuleClass } from '@forinda/kickjs-core'
|
|
@@ -2251,6 +3214,90 @@ export const modules: AppModuleClass[] = [${pascal}Module]
|
|
|
2251
3214
|
}
|
|
2252
3215
|
__name(autoRegisterModule2, "autoRegisterModule");
|
|
2253
3216
|
|
|
3217
|
+
// src/generators/test.ts
|
|
3218
|
+
import { join as join14, resolve as resolve3 } from "path";
|
|
3219
|
+
async function generateTest(options) {
|
|
3220
|
+
const { name, moduleName, modulesDir } = options;
|
|
3221
|
+
const kebab = toKebabCase(name);
|
|
3222
|
+
const pascal = toPascalCase(name);
|
|
3223
|
+
const files = [];
|
|
3224
|
+
let outDir;
|
|
3225
|
+
if (options.outDir) {
|
|
3226
|
+
outDir = resolve3(options.outDir);
|
|
3227
|
+
} else if (moduleName) {
|
|
3228
|
+
const modKebab = toKebabCase(moduleName);
|
|
3229
|
+
const modPlural = pluralize(modKebab);
|
|
3230
|
+
const modDir = modulesDir ?? "src/modules";
|
|
3231
|
+
outDir = resolve3(join14(modDir, modPlural, "__tests__"));
|
|
3232
|
+
} else {
|
|
3233
|
+
outDir = resolve3("src/__tests__");
|
|
3234
|
+
}
|
|
3235
|
+
const filePath = join14(outDir, `${kebab}.test.ts`);
|
|
3236
|
+
await writeFileSafe(filePath, `import { describe, it, expect, beforeEach } from 'vitest'
|
|
3237
|
+
import { Container } from '@forinda/kickjs-core'
|
|
3238
|
+
|
|
3239
|
+
describe('${pascal}', () => {
|
|
3240
|
+
beforeEach(() => {
|
|
3241
|
+
Container.reset()
|
|
3242
|
+
})
|
|
3243
|
+
|
|
3244
|
+
it('should be defined', () => {
|
|
3245
|
+
// TODO: Import and test your class/function here
|
|
3246
|
+
expect(true).toBe(true)
|
|
3247
|
+
})
|
|
3248
|
+
|
|
3249
|
+
it('should handle the happy path', async () => {
|
|
3250
|
+
// TODO: Set up test data and assertions
|
|
3251
|
+
expect(true).toBe(true)
|
|
3252
|
+
})
|
|
3253
|
+
|
|
3254
|
+
it('should handle edge cases', async () => {
|
|
3255
|
+
// TODO: Test error handling, empty inputs, etc.
|
|
3256
|
+
expect(true).toBe(true)
|
|
3257
|
+
})
|
|
3258
|
+
})
|
|
3259
|
+
`);
|
|
3260
|
+
files.push(filePath);
|
|
3261
|
+
return files;
|
|
3262
|
+
}
|
|
3263
|
+
__name(generateTest, "generateTest");
|
|
3264
|
+
|
|
3265
|
+
// src/config.ts
|
|
3266
|
+
import { readFile as readFile4, access as access2 } from "fs/promises";
|
|
3267
|
+
import { join as join15 } from "path";
|
|
3268
|
+
var CONFIG_FILES = [
|
|
3269
|
+
"kick.config.ts",
|
|
3270
|
+
"kick.config.js",
|
|
3271
|
+
"kick.config.mjs",
|
|
3272
|
+
"kick.config.json"
|
|
3273
|
+
];
|
|
3274
|
+
async function loadKickConfig(cwd) {
|
|
3275
|
+
for (const filename of CONFIG_FILES) {
|
|
3276
|
+
const filepath = join15(cwd, filename);
|
|
3277
|
+
try {
|
|
3278
|
+
await access2(filepath);
|
|
3279
|
+
} catch {
|
|
3280
|
+
continue;
|
|
3281
|
+
}
|
|
3282
|
+
if (filename.endsWith(".json")) {
|
|
3283
|
+
const content = await readFile4(filepath, "utf-8");
|
|
3284
|
+
return JSON.parse(content);
|
|
3285
|
+
}
|
|
3286
|
+
try {
|
|
3287
|
+
const { pathToFileURL: pathToFileURL2 } = await import("url");
|
|
3288
|
+
const mod = await import(pathToFileURL2(filepath).href);
|
|
3289
|
+
return mod.default ?? mod;
|
|
3290
|
+
} catch (err) {
|
|
3291
|
+
if (filename.endsWith(".ts")) {
|
|
3292
|
+
console.warn(`Warning: Failed to load ${filename}. TypeScript config files require a runtime loader (e.g. tsx, ts-node) or use kick.config.js/.mjs instead.`);
|
|
3293
|
+
}
|
|
3294
|
+
continue;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
return null;
|
|
3298
|
+
}
|
|
3299
|
+
__name(loadKickConfig, "loadKickConfig");
|
|
3300
|
+
|
|
2254
3301
|
// src/commands/generate.ts
|
|
2255
3302
|
function printGenerated(files) {
|
|
2256
3303
|
const cwd = process.cwd();
|
|
@@ -2262,86 +3309,195 @@ function printGenerated(files) {
|
|
|
2262
3309
|
console.log();
|
|
2263
3310
|
}
|
|
2264
3311
|
__name(printGenerated, "printGenerated");
|
|
3312
|
+
var GENERATORS = [
|
|
3313
|
+
{
|
|
3314
|
+
name: "module <name>",
|
|
3315
|
+
description: "Full DDD module (controller, DTOs, use-cases, repo)"
|
|
3316
|
+
},
|
|
3317
|
+
{
|
|
3318
|
+
name: "scaffold <name> <fields...>",
|
|
3319
|
+
description: "CRUD module from field definitions"
|
|
3320
|
+
},
|
|
3321
|
+
{
|
|
3322
|
+
name: "controller <name>",
|
|
3323
|
+
description: "@Controller() class [-m module]"
|
|
3324
|
+
},
|
|
3325
|
+
{
|
|
3326
|
+
name: "service <name>",
|
|
3327
|
+
description: "@Service() singleton [-m module]"
|
|
3328
|
+
},
|
|
3329
|
+
{
|
|
3330
|
+
name: "middleware <name>",
|
|
3331
|
+
description: "Express middleware function [-m module]"
|
|
3332
|
+
},
|
|
3333
|
+
{
|
|
3334
|
+
name: "guard <name>",
|
|
3335
|
+
description: "Route guard (auth, roles, etc.) [-m module]"
|
|
3336
|
+
},
|
|
3337
|
+
{
|
|
3338
|
+
name: "dto <name>",
|
|
3339
|
+
description: "Zod DTO schema [-m module]"
|
|
3340
|
+
},
|
|
3341
|
+
{
|
|
3342
|
+
name: "adapter <name>",
|
|
3343
|
+
description: "AppAdapter with lifecycle hooks (app-level only)"
|
|
3344
|
+
},
|
|
3345
|
+
{
|
|
3346
|
+
name: "test <name>",
|
|
3347
|
+
description: "Vitest test scaffold [-m module]"
|
|
3348
|
+
},
|
|
3349
|
+
{
|
|
3350
|
+
name: "resolver <name>",
|
|
3351
|
+
description: "GraphQL @Resolver class"
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
name: "job <name>",
|
|
3355
|
+
description: "Queue @Job processor"
|
|
3356
|
+
},
|
|
3357
|
+
{
|
|
3358
|
+
name: "config",
|
|
3359
|
+
description: "Generate kick.config.ts"
|
|
3360
|
+
}
|
|
3361
|
+
];
|
|
3362
|
+
function printGeneratorList() {
|
|
3363
|
+
console.log("\n Available generators:\n");
|
|
3364
|
+
const maxName = Math.max(...GENERATORS.map((g) => g.name.length));
|
|
3365
|
+
for (const g of GENERATORS) {
|
|
3366
|
+
console.log(` kick g ${g.name.padEnd(maxName + 2)} ${g.description}`);
|
|
3367
|
+
}
|
|
3368
|
+
console.log();
|
|
3369
|
+
}
|
|
3370
|
+
__name(printGeneratorList, "printGeneratorList");
|
|
2265
3371
|
function registerGenerateCommand(program) {
|
|
2266
|
-
const gen = program.command("generate").alias("g").description("Generate code scaffolds")
|
|
2267
|
-
|
|
3372
|
+
const gen = program.command("generate").alias("g").description("Generate code scaffolds").option("--list", "List all available generators").action((opts) => {
|
|
3373
|
+
if (opts.list) {
|
|
3374
|
+
printGeneratorList();
|
|
3375
|
+
} else {
|
|
3376
|
+
gen.help();
|
|
3377
|
+
}
|
|
3378
|
+
});
|
|
3379
|
+
gen.command("module <name>").description("Generate a module (structure depends on project pattern)").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--repo <type>", "Repository implementation: inmemory | drizzle | prisma").option("--pattern <pattern>", "Override project pattern: rest | ddd | cqrs | minimal").option("--minimal", "Shorthand for --pattern minimal").option("--modules-dir <dir>", "Modules directory").option("-f, --force", "Overwrite existing files without prompting").action(async (name, opts) => {
|
|
3380
|
+
const config = await loadKickConfig(process.cwd());
|
|
3381
|
+
const modulesDir = opts.modulesDir ?? config?.modulesDir ?? "src/modules";
|
|
3382
|
+
const repo = opts.repo ?? config?.defaultRepo ?? "inmemory";
|
|
3383
|
+
const pattern = opts.pattern ?? config?.pattern ?? "ddd";
|
|
2268
3384
|
const files = await generateModule({
|
|
2269
3385
|
name,
|
|
2270
|
-
modulesDir:
|
|
3386
|
+
modulesDir: resolve4(modulesDir),
|
|
2271
3387
|
noEntity: opts.entity === false,
|
|
2272
3388
|
noTests: opts.tests === false,
|
|
2273
|
-
repo
|
|
2274
|
-
minimal: opts.minimal
|
|
3389
|
+
repo,
|
|
3390
|
+
minimal: opts.minimal,
|
|
3391
|
+
force: opts.force,
|
|
3392
|
+
pattern
|
|
2275
3393
|
});
|
|
2276
3394
|
printGenerated(files);
|
|
2277
3395
|
});
|
|
2278
3396
|
gen.command("adapter <name>").description("Generate an AppAdapter with lifecycle hooks and middleware support").option("-o, --out <dir>", "Output directory", "src/adapters").action(async (name, opts) => {
|
|
2279
3397
|
const files = await generateAdapter({
|
|
2280
3398
|
name,
|
|
2281
|
-
outDir:
|
|
3399
|
+
outDir: resolve4(opts.out)
|
|
2282
3400
|
});
|
|
2283
3401
|
printGenerated(files);
|
|
2284
3402
|
});
|
|
2285
|
-
gen.command("middleware <name>").description("Generate an Express middleware function").option("-o, --out <dir>", "Output directory", "
|
|
3403
|
+
gen.command("middleware <name>").description("Generate an Express middleware function\n Use -m to scope it to a module: kick g middleware auth -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
|
|
3404
|
+
const config = await loadKickConfig(process.cwd());
|
|
3405
|
+
const modulesDir = config?.modulesDir ?? "src/modules";
|
|
2286
3406
|
const files = await generateMiddleware({
|
|
2287
3407
|
name,
|
|
2288
|
-
outDir:
|
|
3408
|
+
outDir: opts.out,
|
|
3409
|
+
moduleName: opts.module,
|
|
3410
|
+
modulesDir,
|
|
3411
|
+
pattern: config?.pattern
|
|
2289
3412
|
});
|
|
2290
3413
|
printGenerated(files);
|
|
2291
3414
|
});
|
|
2292
|
-
gen.command("guard <name>").description("Generate a route guard (auth, roles, etc.)").option("-o, --out <dir>", "Output directory", "
|
|
3415
|
+
gen.command("guard <name>").description("Generate a route guard (auth, roles, etc.)\n Use -m to scope it to a module: kick g guard admin -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
|
|
3416
|
+
const config = await loadKickConfig(process.cwd());
|
|
3417
|
+
const modulesDir = config?.modulesDir ?? "src/modules";
|
|
2293
3418
|
const files = await generateGuard({
|
|
2294
3419
|
name,
|
|
2295
|
-
outDir:
|
|
3420
|
+
outDir: opts.out,
|
|
3421
|
+
moduleName: opts.module,
|
|
3422
|
+
modulesDir,
|
|
3423
|
+
pattern: config?.pattern
|
|
2296
3424
|
});
|
|
2297
3425
|
printGenerated(files);
|
|
2298
3426
|
});
|
|
2299
|
-
gen.command("service <name>").description("Generate a @Service() class").option("-o, --out <dir>", "Output directory", "
|
|
3427
|
+
gen.command("service <name>").description("Generate a @Service() class\n Use -m to scope it to a module: kick g service payment -m orders").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
|
|
3428
|
+
const config = await loadKickConfig(process.cwd());
|
|
3429
|
+
const modulesDir = config?.modulesDir ?? "src/modules";
|
|
2300
3430
|
const files = await generateService({
|
|
2301
3431
|
name,
|
|
2302
|
-
outDir:
|
|
3432
|
+
outDir: opts.out,
|
|
3433
|
+
moduleName: opts.module,
|
|
3434
|
+
modulesDir,
|
|
3435
|
+
pattern: config?.pattern
|
|
2303
3436
|
});
|
|
2304
3437
|
printGenerated(files);
|
|
2305
3438
|
});
|
|
2306
|
-
gen.command("controller <name>").description("Generate a @Controller() class with basic routes").option("-o, --out <dir>", "Output directory", "
|
|
3439
|
+
gen.command("controller <name>").description("Generate a @Controller() class with basic routes\n Use -m to scope it to a module: kick g controller auth -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
|
|
3440
|
+
const config = await loadKickConfig(process.cwd());
|
|
3441
|
+
const modulesDir = config?.modulesDir ?? "src/modules";
|
|
2307
3442
|
const files = await generateController2({
|
|
2308
3443
|
name,
|
|
2309
|
-
outDir:
|
|
3444
|
+
outDir: opts.out,
|
|
3445
|
+
moduleName: opts.module,
|
|
3446
|
+
modulesDir,
|
|
3447
|
+
pattern: config?.pattern
|
|
2310
3448
|
});
|
|
2311
3449
|
printGenerated(files);
|
|
2312
3450
|
});
|
|
2313
|
-
gen.command("dto <name>").description("Generate a Zod DTO schema").option("-o, --out <dir>", "Output directory", "
|
|
3451
|
+
gen.command("dto <name>").description("Generate a Zod DTO schema\n Use -m to scope it to a module: kick g dto create-user -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
|
|
3452
|
+
const config = await loadKickConfig(process.cwd());
|
|
3453
|
+
const modulesDir = config?.modulesDir ?? "src/modules";
|
|
2314
3454
|
const files = await generateDto({
|
|
2315
3455
|
name,
|
|
2316
|
-
outDir:
|
|
3456
|
+
outDir: opts.out,
|
|
3457
|
+
moduleName: opts.module,
|
|
3458
|
+
modulesDir,
|
|
3459
|
+
pattern: config?.pattern
|
|
3460
|
+
});
|
|
3461
|
+
printGenerated(files);
|
|
3462
|
+
});
|
|
3463
|
+
gen.command("test <name>").description("Generate a Vitest test scaffold\n Use -m to scope it to a module: kick g test user-service -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module's __tests__/ folder").action(async (name, opts) => {
|
|
3464
|
+
const config = await loadKickConfig(process.cwd());
|
|
3465
|
+
const modulesDir = config?.modulesDir ?? "src/modules";
|
|
3466
|
+
const files = await generateTest({
|
|
3467
|
+
name,
|
|
3468
|
+
outDir: opts.out,
|
|
3469
|
+
moduleName: opts.module,
|
|
3470
|
+
modulesDir
|
|
2317
3471
|
});
|
|
2318
3472
|
printGenerated(files);
|
|
2319
3473
|
});
|
|
2320
3474
|
gen.command("resolver <name>").description("Generate a GraphQL @Resolver class with @Query and @Mutation methods").option("-o, --out <dir>", "Output directory", "src/resolvers").action(async (name, opts) => {
|
|
2321
3475
|
const files = await generateResolver({
|
|
2322
3476
|
name,
|
|
2323
|
-
outDir:
|
|
3477
|
+
outDir: resolve4(opts.out)
|
|
2324
3478
|
});
|
|
2325
3479
|
printGenerated(files);
|
|
2326
3480
|
});
|
|
2327
3481
|
gen.command("job <name>").description("Generate a @Job queue processor with @Process handlers").option("-o, --out <dir>", "Output directory", "src/jobs").option("-q, --queue <name>", "Queue name (default: <name>-queue)").action(async (name, opts) => {
|
|
2328
3482
|
const files = await generateJob({
|
|
2329
3483
|
name,
|
|
2330
|
-
outDir:
|
|
3484
|
+
outDir: resolve4(opts.out),
|
|
2331
3485
|
queue: opts.queue
|
|
2332
3486
|
});
|
|
2333
3487
|
printGenerated(files);
|
|
2334
3488
|
});
|
|
2335
|
-
gen.command("scaffold <name> [fields...]").description("Generate a full CRUD module from field definitions\n Example: kick g scaffold Post title:string body:text published:boolean?\n Types: string, text, number, int, float, boolean, date, email, url, uuid, json, enum:a,b,c\n Append ? for optional fields: description:text?").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--modules-dir <dir>", "Modules directory"
|
|
3489
|
+
gen.command("scaffold <name> [fields...]").description("Generate a full CRUD module from field definitions\n Example: kick g scaffold Post title:string body:text published:boolean?\n Types: string, text, number, int, float, boolean, date, email, url, uuid, json, enum:a,b,c\n Append ? for optional fields: description:text?").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--modules-dir <dir>", "Modules directory").action(async (name, rawFields, opts) => {
|
|
2336
3490
|
if (rawFields.length === 0) {
|
|
2337
3491
|
console.error("\n Error: At least one field is required.\n Usage: kick g scaffold <name> <field:type> [field:type...]\n Example: kick g scaffold Post title:string body:text published:boolean\n");
|
|
2338
3492
|
process.exit(1);
|
|
2339
3493
|
}
|
|
3494
|
+
const config = await loadKickConfig(process.cwd());
|
|
3495
|
+
const modulesDir = opts.modulesDir ?? config?.modulesDir ?? "src/modules";
|
|
2340
3496
|
const fields = parseFields(rawFields);
|
|
2341
3497
|
const files = await generateScaffold({
|
|
2342
3498
|
name,
|
|
2343
3499
|
fields,
|
|
2344
|
-
modulesDir:
|
|
3500
|
+
modulesDir: resolve4(modulesDir),
|
|
2345
3501
|
noEntity: opts.entity === false,
|
|
2346
3502
|
noTests: opts.tests === false
|
|
2347
3503
|
});
|
|
@@ -2352,9 +3508,9 @@ function registerGenerateCommand(program) {
|
|
|
2352
3508
|
}
|
|
2353
3509
|
printGenerated(files);
|
|
2354
3510
|
});
|
|
2355
|
-
gen.command("config").description("Generate a kick.config.ts at the project root").option("--modules-dir <dir>", "Modules directory path", "src/modules").option("--repo <type>", "Default repository type: inmemory | drizzle", "inmemory").option("-f, --force", "Overwrite existing kick.config.ts without prompting").action(async (opts) => {
|
|
3511
|
+
gen.command("config").description("Generate a kick.config.ts at the project root").option("--modules-dir <dir>", "Modules directory path", "src/modules").option("--repo <type>", "Default repository type: inmemory | drizzle | prisma", "inmemory").option("-f, --force", "Overwrite existing kick.config.ts without prompting").action(async (opts) => {
|
|
2356
3512
|
const files = await generateConfig({
|
|
2357
|
-
outDir:
|
|
3513
|
+
outDir: resolve4("."),
|
|
2358
3514
|
modulesDir: opts.modulesDir,
|
|
2359
3515
|
defaultRepo: opts.repo,
|
|
2360
3516
|
force: opts.force
|
|
@@ -2366,7 +3522,7 @@ __name(registerGenerateCommand, "registerGenerateCommand");
|
|
|
2366
3522
|
|
|
2367
3523
|
// src/commands/run.ts
|
|
2368
3524
|
import { cpSync, existsSync as existsSync3, mkdirSync } from "fs";
|
|
2369
|
-
import { resolve as
|
|
3525
|
+
import { resolve as resolve5, join as join16 } from "path";
|
|
2370
3526
|
|
|
2371
3527
|
// src/utils/shell.ts
|
|
2372
3528
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -2378,42 +3534,6 @@ function runShellCommand(command, cwd) {
|
|
|
2378
3534
|
}
|
|
2379
3535
|
__name(runShellCommand, "runShellCommand");
|
|
2380
3536
|
|
|
2381
|
-
// src/config.ts
|
|
2382
|
-
import { readFile as readFile4, access as access2 } from "fs/promises";
|
|
2383
|
-
import { join as join13 } from "path";
|
|
2384
|
-
var CONFIG_FILES = [
|
|
2385
|
-
"kick.config.ts",
|
|
2386
|
-
"kick.config.js",
|
|
2387
|
-
"kick.config.mjs",
|
|
2388
|
-
"kick.config.json"
|
|
2389
|
-
];
|
|
2390
|
-
async function loadKickConfig(cwd) {
|
|
2391
|
-
for (const filename of CONFIG_FILES) {
|
|
2392
|
-
const filepath = join13(cwd, filename);
|
|
2393
|
-
try {
|
|
2394
|
-
await access2(filepath);
|
|
2395
|
-
} catch {
|
|
2396
|
-
continue;
|
|
2397
|
-
}
|
|
2398
|
-
if (filename.endsWith(".json")) {
|
|
2399
|
-
const content = await readFile4(filepath, "utf-8");
|
|
2400
|
-
return JSON.parse(content);
|
|
2401
|
-
}
|
|
2402
|
-
try {
|
|
2403
|
-
const { pathToFileURL: pathToFileURL2 } = await import("url");
|
|
2404
|
-
const mod = await import(pathToFileURL2(filepath).href);
|
|
2405
|
-
return mod.default ?? mod;
|
|
2406
|
-
} catch (err) {
|
|
2407
|
-
if (filename.endsWith(".ts")) {
|
|
2408
|
-
console.warn(`Warning: Failed to load ${filename}. TypeScript config files require a runtime loader (e.g. tsx, ts-node) or use kick.config.js/.mjs instead.`);
|
|
2409
|
-
}
|
|
2410
|
-
continue;
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
return null;
|
|
2414
|
-
}
|
|
2415
|
-
__name(loadKickConfig, "loadKickConfig");
|
|
2416
|
-
|
|
2417
3537
|
// src/commands/run.ts
|
|
2418
3538
|
function registerRunCommands(program) {
|
|
2419
3539
|
program.command("dev").description("Start development server with Vite HMR (zero-downtime reload)").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").action((opts) => {
|
|
@@ -2440,9 +3560,9 @@ function registerRunCommands(program) {
|
|
|
2440
3560
|
console.log("\n Copying directories to dist...");
|
|
2441
3561
|
for (const entry of copyDirs) {
|
|
2442
3562
|
const src = typeof entry === "string" ? entry : entry.src;
|
|
2443
|
-
const dest = typeof entry === "string" ?
|
|
2444
|
-
const srcPath =
|
|
2445
|
-
const destPath =
|
|
3563
|
+
const dest = typeof entry === "string" ? join16("dist", entry) : entry.dest ?? join16("dist", src);
|
|
3564
|
+
const srcPath = resolve5(src);
|
|
3565
|
+
const destPath = resolve5(dest);
|
|
2446
3566
|
if (!existsSync3(srcPath)) {
|
|
2447
3567
|
console.log(` \u26A0 Skipped ${src} (not found)`);
|
|
2448
3568
|
continue;
|
|
@@ -2691,7 +3811,7 @@ __name(registerInspectCommand, "registerInspectCommand");
|
|
|
2691
3811
|
// src/commands/add.ts
|
|
2692
3812
|
import { execSync as execSync3 } from "child_process";
|
|
2693
3813
|
import { existsSync as existsSync4 } from "fs";
|
|
2694
|
-
import { resolve as
|
|
3814
|
+
import { resolve as resolve6 } from "path";
|
|
2695
3815
|
var PACKAGE_REGISTRY = {
|
|
2696
3816
|
// Core (already installed by kick new)
|
|
2697
3817
|
core: {
|
|
@@ -2841,24 +3961,34 @@ var PACKAGE_REGISTRY = {
|
|
|
2841
3961
|
}
|
|
2842
3962
|
};
|
|
2843
3963
|
function detectPackageManager() {
|
|
2844
|
-
if (existsSync4(
|
|
2845
|
-
if (existsSync4(
|
|
3964
|
+
if (existsSync4(resolve6("pnpm-lock.yaml"))) return "pnpm";
|
|
3965
|
+
if (existsSync4(resolve6("yarn.lock"))) return "yarn";
|
|
2846
3966
|
return "npm";
|
|
2847
3967
|
}
|
|
2848
3968
|
__name(detectPackageManager, "detectPackageManager");
|
|
3969
|
+
function printPackageList() {
|
|
3970
|
+
console.log("\n Available KickJS packages:\n");
|
|
3971
|
+
const maxName = Math.max(...Object.keys(PACKAGE_REGISTRY).map((k) => k.length));
|
|
3972
|
+
for (const [name, info] of Object.entries(PACKAGE_REGISTRY)) {
|
|
3973
|
+
const padded = name.padEnd(maxName + 2);
|
|
3974
|
+
const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
|
|
3975
|
+
console.log(` ${padded} ${info.description}${peers}`);
|
|
3976
|
+
}
|
|
3977
|
+
console.log("\n Usage: kick add graphql drizzle otel");
|
|
3978
|
+
console.log(" kick add queue:bullmq");
|
|
3979
|
+
console.log();
|
|
3980
|
+
}
|
|
3981
|
+
__name(printPackageList, "printPackageList");
|
|
3982
|
+
function registerListCommand(program) {
|
|
3983
|
+
program.command("list").alias("ls").description("List all available KickJS packages").action(() => {
|
|
3984
|
+
printPackageList();
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
__name(registerListCommand, "registerListCommand");
|
|
2849
3988
|
function registerAddCommand(program) {
|
|
2850
3989
|
program.command("add [packages...]").description("Add KickJS packages with their required dependencies").option("--pm <manager>", "Package manager override").option("-D, --dev", "Install as dev dependency").option("--list", "List all available packages").action(async (packages, opts) => {
|
|
2851
3990
|
if (opts.list || packages.length === 0) {
|
|
2852
|
-
|
|
2853
|
-
const maxName = Math.max(...Object.keys(PACKAGE_REGISTRY).map((k) => k.length));
|
|
2854
|
-
for (const [name, info] of Object.entries(PACKAGE_REGISTRY)) {
|
|
2855
|
-
const padded = name.padEnd(maxName + 2);
|
|
2856
|
-
const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
|
|
2857
|
-
console.log(` ${padded} ${info.description}${peers}`);
|
|
2858
|
-
}
|
|
2859
|
-
console.log("\n Usage: kick add graphql drizzle otel");
|
|
2860
|
-
console.log(" kick add queue:bullmq");
|
|
2861
|
-
console.log();
|
|
3991
|
+
printPackageList();
|
|
2862
3992
|
return;
|
|
2863
3993
|
}
|
|
2864
3994
|
const pm = opts.pm ?? detectPackageManager();
|
|
@@ -2926,14 +4056,14 @@ function registerAddCommand(program) {
|
|
|
2926
4056
|
__name(registerAddCommand, "registerAddCommand");
|
|
2927
4057
|
|
|
2928
4058
|
// src/commands/tinker.ts
|
|
2929
|
-
import { resolve as
|
|
4059
|
+
import { resolve as resolve7, join as join17 } from "path";
|
|
2930
4060
|
import { existsSync as existsSync5 } from "fs";
|
|
2931
4061
|
import { pathToFileURL } from "url";
|
|
2932
4062
|
import { fork } from "child_process";
|
|
2933
4063
|
function registerTinkerCommand(program) {
|
|
2934
4064
|
program.command("tinker").description("Interactive REPL with DI container and services loaded").option("-e, --entry <file>", "Entry file to load", "src/index.ts").action(async (opts) => {
|
|
2935
4065
|
const cwd = process.cwd();
|
|
2936
|
-
const entryPath =
|
|
4066
|
+
const entryPath = resolve7(cwd, opts.entry);
|
|
2937
4067
|
if (!existsSync5(entryPath)) {
|
|
2938
4068
|
console.error(`
|
|
2939
4069
|
Error: ${opts.entry} not found.
|
|
@@ -2946,7 +4076,7 @@ function registerTinkerCommand(program) {
|
|
|
2946
4076
|
process.exit(1);
|
|
2947
4077
|
}
|
|
2948
4078
|
const tinkerScript = generateTinkerScript(entryPath, opts.entry);
|
|
2949
|
-
const tmpFile =
|
|
4079
|
+
const tmpFile = join17(cwd, ".kick-tinker.mjs");
|
|
2950
4080
|
const { writeFileSync, unlinkSync } = await import("fs");
|
|
2951
4081
|
writeFileSync(tmpFile, tinkerScript, "utf-8");
|
|
2952
4082
|
try {
|
|
@@ -2955,8 +4085,8 @@ function registerTinkerCommand(program) {
|
|
|
2955
4085
|
execPath: tsxBin,
|
|
2956
4086
|
stdio: "inherit"
|
|
2957
4087
|
});
|
|
2958
|
-
await new Promise((
|
|
2959
|
-
child.on("exit", () =>
|
|
4088
|
+
await new Promise((resolve8) => {
|
|
4089
|
+
child.on("exit", () => resolve8());
|
|
2960
4090
|
});
|
|
2961
4091
|
} finally {
|
|
2962
4092
|
try {
|
|
@@ -3029,9 +4159,9 @@ __name(generateTinkerScript, "generateTinkerScript");
|
|
|
3029
4159
|
function findBin(startDir, name) {
|
|
3030
4160
|
let dir = startDir;
|
|
3031
4161
|
while (true) {
|
|
3032
|
-
const candidate =
|
|
4162
|
+
const candidate = join17(dir, "node_modules", ".bin", name);
|
|
3033
4163
|
if (existsSync5(candidate)) return candidate;
|
|
3034
|
-
const parent =
|
|
4164
|
+
const parent = resolve7(dir, "..");
|
|
3035
4165
|
if (parent === dir) break;
|
|
3036
4166
|
dir = parent;
|
|
3037
4167
|
}
|
|
@@ -3041,7 +4171,7 @@ __name(findBin, "findBin");
|
|
|
3041
4171
|
|
|
3042
4172
|
// src/cli.ts
|
|
3043
4173
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
3044
|
-
var pkg = JSON.parse(readFileSync2(
|
|
4174
|
+
var pkg = JSON.parse(readFileSync2(join18(__dirname2, "..", "package.json"), "utf-8"));
|
|
3045
4175
|
async function main() {
|
|
3046
4176
|
const program = new Command();
|
|
3047
4177
|
program.name("kick").description("KickJS \u2014 A production-grade, decorator-driven Node.js framework").version(pkg.version);
|
|
@@ -3052,6 +4182,7 @@ async function main() {
|
|
|
3052
4182
|
registerInfoCommand(program);
|
|
3053
4183
|
registerInspectCommand(program);
|
|
3054
4184
|
registerAddCommand(program);
|
|
4185
|
+
registerListCommand(program);
|
|
3055
4186
|
registerTinkerCommand(program);
|
|
3056
4187
|
registerCustomCommands(program, config);
|
|
3057
4188
|
program.showHelpAfterError();
|