@forinda/kickjs-cli 2.1.0 → 2.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/index.d.mts CHANGED
@@ -28,6 +28,50 @@ interface CustomRepoType {
28
28
  }
29
29
  /** Repository type — built-in string or custom object */
30
30
  type RepoTypeConfig = BuiltinRepoType$1 | CustomRepoType;
31
+ /**
32
+ * Supported schema validators for `kick typegen` body/query/params
33
+ * type extraction. Only `'zod'` ships built-in for now; other libraries
34
+ * (Joi, Yup, JSON Schema) will be added later as the adapter system
35
+ * grows. Set to `false` (or omit) to disable schema-driven body typing
36
+ * entirely (the route entries will keep `body: unknown`).
37
+ */
38
+ type SchemaValidator = 'zod' | false;
39
+ /** Typegen settings — controls .kickjs/types/* generation */
40
+ interface TypegenConfig {
41
+ /**
42
+ * Source directory to scan for controllers and decorators.
43
+ * Defaults to `'src'`.
44
+ */
45
+ srcDir?: string;
46
+ /**
47
+ * Output directory for generated `.d.ts` files.
48
+ * Defaults to `'.kickjs/types'`.
49
+ */
50
+ outDir?: string;
51
+ /**
52
+ * Schema validator used to derive `body` types from route metadata.
53
+ *
54
+ * - `'zod'` — emit `z.infer<typeof <importedSchema>>` for any schema
55
+ * referenced as a named identifier in `@Get/@Post/...({ body, query, params })`.
56
+ * - `false` — disable schema-driven body typing.
57
+ *
58
+ * Future: `'joi' | 'yup' | 'json-schema'` plus a `{ name; module }`
59
+ * escape hatch for custom adapters.
60
+ *
61
+ * @default 'zod'
62
+ */
63
+ schemaValidator?: SchemaValidator;
64
+ /**
65
+ * Path to the project's env schema file (relative to project root).
66
+ * Must default-export a `defineEnv(...)` schema for typegen to emit
67
+ * the typed `KickEnv` global registry.
68
+ *
69
+ * Set to `false` to disable env typing entirely.
70
+ *
71
+ * @default 'src/env.ts'
72
+ */
73
+ envFile?: string | false;
74
+ }
31
75
  /** Module generation settings — controls how `kick g module` produces code */
32
76
  interface ModuleConfig {
33
77
  /** Where modules live (default: 'src/modules') */
@@ -113,6 +157,18 @@ interface KickConfig {
113
157
  src: string;
114
158
  dest?: string;
115
159
  }>;
160
+ /**
161
+ * Typegen settings — controls `.kickjs/types/*` generation including
162
+ * the schema validator used for body type extraction.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * typegen: {
167
+ * schemaValidator: 'zod',
168
+ * }
169
+ * ```
170
+ */
171
+ typegen?: TypegenConfig;
116
172
  /** Custom commands that extend the CLI */
117
173
  commands?: KickCommandDefinition[];
118
174
  /** Code style overrides (auto-detected from prettier when possible) */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/generators/module.ts","../src/generators/adapter.ts","../src/generators/middleware.ts","../src/generators/guard.ts","../src/generators/service.ts","../src/generators/controller.ts","../src/generators/dto.ts","../src/generators/project.ts","../src/utils/naming.ts"],"mappings":";;;UAIiB,qBAAA;EAAqB;EAEpC,IAAA;EAFoC;EAIpC,WAAA;EAAA;;;;;AAeF;;;EANE,KAAA;EAMwB;EAJxB,OAAA;AAAA;;KAIU,cAAA;;KAGA,iBAAA;;UAKK,cAAA;EACf,IAAA;AAAA;;KAIU,cAAA,GAAiB,iBAAA,GAAkB,cAAA;;UAG9B,YAAA;EAAA;EAEf,GAAA;;;;;;;;;;;AAoCF;;;EAtBE,IAAA,GAAO,cAAA;EA2CG;EAzCV,SAAA;EAiEW;;;;;EA3DX,SAAA;EAmCA;;;;;;;;;EAzBA,gBAAA;AAAA;;UAIe,UAAA;EAiDf;;;;;;;AASF;EAjDE,OAAA,GAAU,cAAA;;;;;;;;AA8EZ;;;;EAlEE,OAAA,GAAU,YAAA;EAkEuC;EA9DjD,UAAA;EA8DmE;EA5DnE,WAAA,GAAc,cAAA;;EAEd,SAAA;;EAEA,SAAA;EC9FyB;;;;AAC3B;;;;;AAOC;;;;EDoGC,QAAA,GAAW,KAAA;IAAiB,GAAA;IAAa,IAAA;EAAA;EC7FzC;ED+FA,QAAA,GAAW,qBAAA;EC9FX;EDgGA,KAAA;IACE,UAAA;IACA,MAAA;IACA,aAAA;IACA,MAAA;EAAA;AAAA;;iBAKY,YAAA,CAAa,MAAA,EAAQ,UAAA,GAAa,UAAA;;iBA6B5B,cAAA,CAAe,GAAA,WAAc,OAAA,CAAQ,UAAA;;;KCtJ/C,eAAA;AAAA,KACA,QAAA,GAAW,eAAA;AAAA,UASb,qBAAA;EACR,IAAA;EACA,UAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA,GAAO,QAAA;EACP,OAAA;EACA,KAAA;EACA,OAAA,GAAU,cAAA;EACV,MAAA;EDVwB;ECYxB,SAAA;EDTyB;ECWzB,gBAAA;AAAA;;ADNF;;;;;AAKA;;;;iBCyBsB,cAAA,CAAe,OAAA,EAAS,qBAAA,GAAwB,OAAA;;;UCzD5D,sBAAA;EACR,IAAA;EACA,MAAA;AAAA;AAAA,iBAGoB,eAAA,CAAgB,OAAA,EAAS,sBAAA,GAAyB,OAAA;;;UCH9D,yBAAA;EACR,IAAA;EACA,MAAA;EACA,UAAA;EACA,UAAA;EACA,OAAA,GAAU,cAAA;AAAA;AAAA,iBAGU,kBAAA,CAAmB,OAAA,EAAS,yBAAA,GAA4B,OAAA;;;UCRpE,oBAAA;EACR,IAAA;EACA,MAAA;EACA,UAAA;EACA,UAAA;EACA,OAAA,GAAU,cAAA;AAAA;AAAA,iBAGU,aAAA,CAAc,OAAA,EAAS,oBAAA,GAAuB,OAAA;;;UCR1D,sBAAA;EACR,IAAA;EACA,MAAA;EACA,UAAA;EACA,UAAA;EACA,OAAA,GAAU,cAAA;AAAA;AAAA,iBAGU,eAAA,CAAgB,OAAA,EAAS,sBAAA,GAAyB,OAAA;;;UCR9D,yBAAA;EACR,IAAA;EACA,MAAA;EACA,UAAA;EACA,UAAA;EACA,OAAA,GAAU,cAAA;AAAA;AAAA,iBAGU,kBAAA,CAAmB,OAAA,EAAS,yBAAA,GAA4B,OAAA;;;UCRpE,kBAAA;EACR,IAAA;EACA,MAAA;EACA,UAAA;EACA,UAAA;EACA,OAAA,GAAU,cAAA;AAAA;AAAA,iBAGU,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,OAAA;;;KCiB3D,eAAA;AAAA,UAEK,kBAAA;EACR,IAAA;EACA,SAAA;EACA,cAAA;EACA,OAAA;EACA,WAAA;EACA,QAAA,GAAW,eAAA;EACX,WAAA;AAAA;;iBAIoB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,OAAA;;;;iBC3ChD,YAAA,CAAa,IAAA;;iBAOb,WAAA,CAAY,IAAA;;iBAMZ,WAAA,CAAY,IAAA;;;;;iBAWZ,SAAA,CAAU,IAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/generators/module.ts","../src/generators/adapter.ts","../src/generators/middleware.ts","../src/generators/guard.ts","../src/generators/service.ts","../src/generators/controller.ts","../src/generators/dto.ts","../src/generators/project.ts","../src/utils/naming.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v2.1.0
2
+ * @forinda/kickjs-cli v2.2.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -219,8 +219,7 @@ export class ${pascal}Module implements AppModule {
219
219
  /** DDD controller — injects use-cases, nested import paths */
220
220
  function generateController$1(ctx) {
221
221
  const { pascal, kebab, plural = "", pluralPascal = "" } = ctx;
222
- return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs'
223
- import type { RequestContext } from '@forinda/kickjs'
222
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams, type Ctx } from '@forinda/kickjs'
224
223
  import { ApiTags } from '@forinda/kickjs-swagger'
225
224
  import { Create${pascal}UseCase } from '../application/use-cases/create-${kebab}.use-case'
226
225
  import { Get${pascal}UseCase } from '../application/use-cases/get-${kebab}.use-case'
@@ -231,18 +230,23 @@ import { create${pascal}Schema } from '../application/dtos/create-${kebab}.dto'
231
230
  import { update${pascal}Schema } from '../application/dtos/update-${kebab}.dto'
232
231
  import { ${pascal.toUpperCase()}_QUERY_CONFIG } from '../constants'
233
232
 
233
+ // Each handler annotates its \`ctx\` with \`Ctx<KickRoutes.${pascal}Controller['<method>']>\`
234
+ // so \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` are typed end-to-end.
235
+ // The \`KickRoutes\` namespace is generated by \`kick typegen\` (auto-run on
236
+ // \`kick dev\`) — see https://forinda.github.io/kick-js/guide/typegen.
237
+
234
238
  @Controller()
235
239
  export class ${pascal}Controller {
236
- @Autowired() private create${pascal}UseCase!: Create${pascal}UseCase
237
- @Autowired() private get${pascal}UseCase!: Get${pascal}UseCase
238
- @Autowired() private list${pluralPascal}UseCase!: List${pluralPascal}UseCase
239
- @Autowired() private update${pascal}UseCase!: Update${pascal}UseCase
240
- @Autowired() private delete${pascal}UseCase!: Delete${pascal}UseCase
240
+ @Autowired() private readonly create${pascal}UseCase!: Create${pascal}UseCase
241
+ @Autowired() private readonly get${pascal}UseCase!: Get${pascal}UseCase
242
+ @Autowired() private readonly list${pluralPascal}UseCase!: List${pluralPascal}UseCase
243
+ @Autowired() private readonly update${pascal}UseCase!: Update${pascal}UseCase
244
+ @Autowired() private readonly delete${pascal}UseCase!: Delete${pascal}UseCase
241
245
 
242
246
  @Get('/')
243
247
  @ApiTags('${pascal}')
244
248
  @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
245
- async list(ctx: RequestContext) {
249
+ async list(ctx: Ctx<KickRoutes.${pascal}Controller['list']>) {
246
250
  return ctx.paginate(
247
251
  (parsed) => this.list${pluralPascal}UseCase.execute(parsed),
248
252
  ${pascal.toUpperCase()}_QUERY_CONFIG,
@@ -251,7 +255,7 @@ export class ${pascal}Controller {
251
255
 
252
256
  @Get('/:id')
253
257
  @ApiTags('${pascal}')
254
- async getById(ctx: RequestContext) {
258
+ async getById(ctx: Ctx<KickRoutes.${pascal}Controller['getById']>) {
255
259
  const result = await this.get${pascal}UseCase.execute(ctx.params.id)
256
260
  if (!result) return ctx.notFound('${pascal} not found')
257
261
  ctx.json(result)
@@ -259,21 +263,21 @@ export class ${pascal}Controller {
259
263
 
260
264
  @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
261
265
  @ApiTags('${pascal}')
262
- async create(ctx: RequestContext) {
266
+ async create(ctx: Ctx<KickRoutes.${pascal}Controller['create']>) {
263
267
  const result = await this.create${pascal}UseCase.execute(ctx.body)
264
268
  ctx.created(result)
265
269
  }
266
270
 
267
271
  @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
268
272
  @ApiTags('${pascal}')
269
- async update(ctx: RequestContext) {
273
+ async update(ctx: Ctx<KickRoutes.${pascal}Controller['update']>) {
270
274
  const result = await this.update${pascal}UseCase.execute(ctx.params.id, ctx.body)
271
275
  ctx.json(result)
272
276
  }
273
277
 
274
278
  @Delete('/:id')
275
279
  @ApiTags('${pascal}')
276
- async remove(ctx: RequestContext) {
280
+ async remove(ctx: Ctx<KickRoutes.${pascal}Controller['remove']>) {
277
281
  await this.delete${pascal}UseCase.execute(ctx.params.id)
278
282
  ctx.noContent()
279
283
  }
@@ -282,24 +286,28 @@ export class ${pascal}Controller {
282
286
  }
283
287
  /** REST controller — injects service directly, flat import paths */
284
288
  function generateRestController(ctx) {
285
- const { pascal, kebab, plural = "", pluralPascal = "" } = ctx;
289
+ const { pascal, kebab } = ctx;
286
290
  const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
287
- return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs'
288
- import type { RequestContext } from '@forinda/kickjs'
291
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams, type Ctx } from '@forinda/kickjs'
289
292
  import { ApiTags } from '@forinda/kickjs-swagger'
290
293
  import { ${pascal}Service } from './${kebab}.service'
291
294
  import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
292
295
  import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
293
296
  import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
294
297
 
298
+ // Each handler annotates its \`ctx\` with \`Ctx<KickRoutes.${pascal}Controller['<method>']>\`
299
+ // so \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` are typed end-to-end.
300
+ // The \`KickRoutes\` namespace is generated by \`kick typegen\` (auto-run on
301
+ // \`kick dev\`) — see https://forinda.github.io/kick-js/guide/typegen.
302
+
295
303
  @Controller()
296
304
  export class ${pascal}Controller {
297
- @Autowired() private ${camel}Service!: ${pascal}Service
305
+ @Autowired() private readonly ${camel}Service!: ${pascal}Service
298
306
 
299
307
  @Get('/')
300
308
  @ApiTags('${pascal}')
301
309
  @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
302
- async list(ctx: RequestContext) {
310
+ async list(ctx: Ctx<KickRoutes.${pascal}Controller['list']>) {
303
311
  return ctx.paginate(
304
312
  (parsed) => this.${camel}Service.findPaginated(parsed),
305
313
  ${pascal.toUpperCase()}_QUERY_CONFIG,
@@ -308,7 +316,7 @@ export class ${pascal}Controller {
308
316
 
309
317
  @Get('/:id')
310
318
  @ApiTags('${pascal}')
311
- async getById(ctx: RequestContext) {
319
+ async getById(ctx: Ctx<KickRoutes.${pascal}Controller['getById']>) {
312
320
  const result = await this.${camel}Service.findById(ctx.params.id)
313
321
  if (!result) return ctx.notFound('${pascal} not found')
314
322
  ctx.json(result)
@@ -316,21 +324,21 @@ export class ${pascal}Controller {
316
324
 
317
325
  @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
318
326
  @ApiTags('${pascal}')
319
- async create(ctx: RequestContext) {
327
+ async create(ctx: Ctx<KickRoutes.${pascal}Controller['create']>) {
320
328
  const result = await this.${camel}Service.create(ctx.body)
321
329
  ctx.created(result)
322
330
  }
323
331
 
324
332
  @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
325
333
  @ApiTags('${pascal}')
326
- async update(ctx: RequestContext) {
334
+ async update(ctx: Ctx<KickRoutes.${pascal}Controller['update']>) {
327
335
  const result = await this.${camel}Service.update(ctx.params.id, ctx.body)
328
336
  ctx.json(result)
329
337
  }
330
338
 
331
339
  @Delete('/:id')
332
340
  @ApiTags('${pascal}')
333
- async remove(ctx: RequestContext) {
341
+ async remove(ctx: Ctx<KickRoutes.${pascal}Controller['remove']>) {
334
342
  await this.${camel}Service.delete(ctx.params.id)
335
343
  ctx.noContent()
336
344
  }
@@ -511,6 +519,7 @@ function generateRepositoryInterface(ctx) {
511
519
  *
512
520
  * To swap implementations, change the factory in the module's register() method.
513
521
  */
522
+ import { createToken } from '@forinda/kickjs'
514
523
  import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
515
524
  import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
516
525
  import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
@@ -525,7 +534,13 @@ export interface I${pascal}Repository {
525
534
  delete(id: string): Promise<void>
526
535
  }
527
536
 
528
- export const ${pascal.toUpperCase()}_REPOSITORY = Symbol('I${pascal}Repository')
537
+ /**
538
+ * Collision-safe DI token bound to \`I${pascal}Repository\`.
539
+ * \`container.resolve(${pascal.toUpperCase()}_REPOSITORY)\` and
540
+ * \`@Inject(${pascal.toUpperCase()}_REPOSITORY)\` both return the typed
541
+ * interface — no manual generic, no \`any\` cast.
542
+ */
543
+ export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('${pascal}/Repository')
529
544
  `;
530
545
  }
531
546
  function generateInMemoryRepository(ctx) {
@@ -1053,8 +1068,7 @@ export class ${pascal}Module implements AppModule {
1053
1068
  /** CQRS controller — dispatches to command/query handlers */
1054
1069
  function generateCqrsController(ctx) {
1055
1070
  const { pascal, kebab, plural = "", pluralPascal = "" } = ctx;
1056
- return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs'
1057
- import type { RequestContext } from '@forinda/kickjs'
1071
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams, type Ctx } from '@forinda/kickjs'
1058
1072
  import { ApiTags } from '@forinda/kickjs-swagger'
1059
1073
  import { Create${pascal}Command } from './commands/create-${kebab}.command'
1060
1074
  import { Update${pascal}Command } from './commands/update-${kebab}.command'
@@ -1065,18 +1079,23 @@ import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
1065
1079
  import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
1066
1080
  import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
1067
1081
 
1082
+ // Each handler annotates its \`ctx\` with \`Ctx<KickRoutes.${pascal}Controller['<method>']>\`
1083
+ // so \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` are typed end-to-end.
1084
+ // The \`KickRoutes\` namespace is generated by \`kick typegen\` (auto-run on
1085
+ // \`kick dev\`) — see https://forinda.github.io/kick-js/guide/typegen.
1086
+
1068
1087
  @Controller()
1069
1088
  export class ${pascal}Controller {
1070
- @Autowired() private create${pascal}Command!: Create${pascal}Command
1071
- @Autowired() private update${pascal}Command!: Update${pascal}Command
1072
- @Autowired() private delete${pascal}Command!: Delete${pascal}Command
1073
- @Autowired() private get${pascal}Query!: Get${pascal}Query
1074
- @Autowired() private list${pluralPascal}Query!: List${pluralPascal}Query
1089
+ @Autowired() private readonly create${pascal}Command!: Create${pascal}Command
1090
+ @Autowired() private readonly update${pascal}Command!: Update${pascal}Command
1091
+ @Autowired() private readonly delete${pascal}Command!: Delete${pascal}Command
1092
+ @Autowired() private readonly get${pascal}Query!: Get${pascal}Query
1093
+ @Autowired() private readonly list${pluralPascal}Query!: List${pluralPascal}Query
1075
1094
 
1076
1095
  @Get('/')
1077
1096
  @ApiTags('${pascal}')
1078
1097
  @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
1079
- async list(ctx: RequestContext) {
1098
+ async list(ctx: Ctx<KickRoutes.${pascal}Controller['list']>) {
1080
1099
  return ctx.paginate(
1081
1100
  (parsed) => this.list${pluralPascal}Query.execute(parsed),
1082
1101
  ${pascal.toUpperCase()}_QUERY_CONFIG,
@@ -1085,7 +1104,7 @@ export class ${pascal}Controller {
1085
1104
 
1086
1105
  @Get('/:id')
1087
1106
  @ApiTags('${pascal}')
1088
- async getById(ctx: RequestContext) {
1107
+ async getById(ctx: Ctx<KickRoutes.${pascal}Controller['getById']>) {
1089
1108
  const result = await this.get${pascal}Query.execute(ctx.params.id)
1090
1109
  if (!result) return ctx.notFound('${pascal} not found')
1091
1110
  ctx.json(result)
@@ -1093,21 +1112,21 @@ export class ${pascal}Controller {
1093
1112
 
1094
1113
  @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
1095
1114
  @ApiTags('${pascal}')
1096
- async create(ctx: RequestContext) {
1115
+ async create(ctx: Ctx<KickRoutes.${pascal}Controller['create']>) {
1097
1116
  const result = await this.create${pascal}Command.execute(ctx.body)
1098
1117
  ctx.created(result)
1099
1118
  }
1100
1119
 
1101
1120
  @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
1102
1121
  @ApiTags('${pascal}')
1103
- async update(ctx: RequestContext) {
1122
+ async update(ctx: Ctx<KickRoutes.${pascal}Controller['update']>) {
1104
1123
  const result = await this.update${pascal}Command.execute(ctx.params.id, ctx.body)
1105
1124
  ctx.json(result)
1106
1125
  }
1107
1126
 
1108
1127
  @Delete('/:id')
1109
1128
  @ApiTags('${pascal}')
1110
- async remove(ctx: RequestContext) {
1129
+ async remove(ctx: Ctx<KickRoutes.${pascal}Controller['remove']>) {
1111
1130
  await this.delete${pascal}Command.execute(ctx.params.id)
1112
1131
  ctx.noContent()
1113
1132
  }
@@ -1606,6 +1625,41 @@ import { HelloModule } from './hello/hello.module'
1606
1625
  export const modules: AppModuleClass[] = [HelloModule]
1607
1626
  `;
1608
1627
  }
1628
+ /**
1629
+ * Generate `src/env.ts` — the project's typed env schema.
1630
+ *
1631
+ * Default-exports a `defineEnv(...)` schema so `kick typegen` can
1632
+ * infer it into the global `KickEnv` registry. After typegen runs:
1633
+ *
1634
+ * @Value('DATABASE_URL') private url!: Env<'DATABASE_URL'>
1635
+ * process.env.DATABASE_URL // typed as string
1636
+ *
1637
+ * Both autocomplete and type-check at compile time.
1638
+ */
1639
+ function generateEnvFile() {
1640
+ return `import { defineEnv } from '@forinda/kickjs-config'
1641
+ import { z } from 'zod'
1642
+
1643
+ /**
1644
+ * Project environment schema.
1645
+ *
1646
+ * Extend the base schema with your application's variables. The
1647
+ * default export is the contract \`kick typegen\` reads to populate
1648
+ * the global \`KickEnv\` registry — that's what makes \`@Value('FOO')\`
1649
+ * autocomplete and \`process.env.FOO\` typed.
1650
+ *
1651
+ * @example
1652
+ * DATABASE_URL: z.string().url(),
1653
+ * JWT_SECRET: z.string().min(32),
1654
+ * REDIS_URL: z.string().url().optional(),
1655
+ */
1656
+ export default defineEnv((base) =>
1657
+ base.extend({
1658
+ // DATABASE_URL: z.string().url(),
1659
+ }),
1660
+ )
1661
+ `;
1662
+ }
1609
1663
  /** Generate src/modules/hello/hello.service.ts */
1610
1664
  function generateHelloService() {
1611
1665
  return `import { Service } from '@forinda/kickjs'
@@ -1624,20 +1678,25 @@ export class HelloService {
1624
1678
  }
1625
1679
  /** Generate src/modules/hello/hello.controller.ts */
1626
1680
  function generateHelloController() {
1627
- return `import { Controller, Get, Autowired, type RequestContext } from '@forinda/kickjs'
1681
+ return `import { Controller, Get, Autowired, type Ctx } from '@forinda/kickjs'
1628
1682
  import { HelloService } from './hello.service'
1629
1683
 
1684
+ // \`Ctx<KickRoutes.HelloController['<method>']>\` is generated by
1685
+ // \`kick typegen\` (auto-run on \`kick dev\`). The first run after a fresh
1686
+ // scaffold creates \`.kickjs/types/routes.ts\` so this file typechecks.
1687
+ // See https://forinda.github.io/kick-js/guide/typegen.
1688
+
1630
1689
  @Controller()
1631
1690
  export class HelloController {
1632
- @Autowired() private helloService!: HelloService
1691
+ @Autowired() private readonly helloService!: HelloService
1633
1692
 
1634
1693
  @Get('/')
1635
- index(ctx: RequestContext) {
1694
+ index(ctx: Ctx<KickRoutes.HelloController['index']>) {
1636
1695
  ctx.json(this.helloService.greet('World'))
1637
1696
  }
1638
1697
 
1639
1698
  @Get('/health')
1640
- health(ctx: RequestContext) {
1699
+ health(ctx: Ctx<KickRoutes.HelloController['health']>) {
1641
1700
  ctx.json(this.helloService.healthCheck())
1642
1701
  }
1643
1702
  }
@@ -1649,6 +1708,13 @@ function generateHelloModule() {
1649
1708
  import { HelloController } from './hello.controller'
1650
1709
 
1651
1710
  export class HelloModule implements AppModule {
1711
+ // \`register(container)\` is optional — only implement it when you need
1712
+ // to bind a token to a concrete implementation, e.g.
1713
+ // register(container) {
1714
+ // container.registerFactory(USER_REPOSITORY, () => container.resolve(InMemoryUserRepository))
1715
+ // }
1716
+ // The HelloService uses @Service() so the decorator handles registration.
1717
+
1652
1718
  routes(): ModuleRoutes {
1653
1719
  return {
1654
1720
  path: '/hello',
@@ -1675,6 +1741,13 @@ export default defineConfig({
1675
1741
  pluralize: true,
1676
1742
  },
1677
1743
 
1744
+ // \`kick typegen\` populates \`.kickjs/types/\` so \`Ctx<KickRoutes.X['method']>\`
1745
+ // resolves to fully-typed params/body/query. Auto-runs on \`kick dev\`.
1746
+ // Set \`schemaValidator: false\` to skip schema-driven body typing entirely.
1747
+ typegen: {
1748
+ schemaValidator: 'zod',
1749
+ },
1750
+
1678
1751
  commands: [
1679
1752
  {
1680
1753
  name: 'test',
@@ -1710,13 +1783,15 @@ async function generateMinimalFiles(ctx) {
1710
1783
  kebab,
1711
1784
  plural
1712
1785
  }));
1713
- await write(`${kebab}.controller.ts`, `import { Controller, Get } from '@forinda/kickjs'
1714
- import type { RequestContext } from '@forinda/kickjs'
1786
+ await write(`${kebab}.controller.ts`, `import { Controller, Get, type Ctx } from '@forinda/kickjs'
1787
+
1788
+ // \`Ctx<KickRoutes.${pascal}Controller['<method>']>\` is generated by
1789
+ // \`kick typegen\` (auto-run on \`kick dev\`).
1715
1790
 
1716
1791
  @Controller()
1717
1792
  export class ${pascal}Controller {
1718
1793
  @Get('/')
1719
- async list(ctx: RequestContext) {
1794
+ async list(ctx: Ctx<KickRoutes.${pascal}Controller['list']>) {
1720
1795
  ctx.json({ message: '${pascal} list' })
1721
1796
  }
1722
1797
  }
@@ -2442,20 +2517,24 @@ async function generateController(options) {
2442
2517
  const pascal = toPascalCase(name);
2443
2518
  const files = [];
2444
2519
  const filePath = join(outDir, `${kebab}.controller.ts`);
2445
- await writeFileSafe(filePath, `import { Controller, Get, Post, Autowired } from '@forinda/kickjs'
2446
- import type { RequestContext } from '@forinda/kickjs'
2520
+ await writeFileSafe(filePath, `import { Controller, Get, Post, type Ctx } from '@forinda/kickjs'
2521
+
2522
+ // \`Ctx<KickRoutes.${pascal}Controller['<method>']>\` is generated by
2523
+ // \`kick typegen\` (auto-run on \`kick dev\`). After the first run, your IDE
2524
+ // will autocomplete \`ctx.params\`, \`ctx.body\`, and \`ctx.query\`.
2525
+ // See https://forinda.github.io/kick-js/guide/typegen for details.
2447
2526
 
2448
2527
  @Controller()
2449
2528
  export class ${pascal}Controller {
2450
- // @Autowired() private myService!: MyService
2529
+ // @Autowired() private readonly myService!: MyService
2451
2530
 
2452
2531
  @Get('/')
2453
- async list(ctx: RequestContext) {
2532
+ async list(ctx: Ctx<KickRoutes.${pascal}Controller['list']>) {
2454
2533
  ctx.json({ message: '${pascal} list' })
2455
2534
  }
2456
2535
 
2457
2536
  @Post('/')
2458
- async create(ctx: RequestContext) {
2537
+ async create(ctx: Ctx<KickRoutes.${pascal}Controller['create']>) {
2459
2538
  ctx.created({ message: '${pascal} created', data: ctx.body })
2460
2539
  }
2461
2540
  }
@@ -2563,7 +2642,7 @@ function generatePackageJson(name, template, kickjsVersion) {
2563
2642
  */
2564
2643
  function generateViteConfig() {
2565
2644
  return `import { defineConfig } from 'vite'
2566
- import { resolve } from 'path'
2645
+ import { resolve } from 'node:path'
2567
2646
  import swc from 'unplugin-swc'
2568
2647
  import { kickjsVitePlugin } from '@forinda/kickjs-vite'
2569
2648
 
@@ -2613,10 +2692,13 @@ function generateTsConfig() {
2613
2692
  experimentalDecorators: true,
2614
2693
  emitDecoratorMetadata: true,
2615
2694
  outDir: "dist",
2616
- rootDir: "src",
2617
2695
  paths: { "@/*": ["./src/*"] }
2618
2696
  },
2619
- include: ["src"]
2697
+ include: [
2698
+ "src",
2699
+ ".kickjs/types/**/*.d.ts",
2700
+ ".kickjs/types/**/*.ts"
2701
+ ]
2620
2702
  }, null, 2);
2621
2703
  }
2622
2704
  /** Generate .prettierrc with project formatting rules */
@@ -2654,6 +2736,7 @@ dist/
2654
2736
  coverage/
2655
2737
  .DS_Store
2656
2738
  *.tsbuildinfo
2739
+ .kickjs/
2657
2740
  `;
2658
2741
  }
2659
2742
  /** Generate .gitattributes for consistent line endings */
@@ -2841,20 +2924,22 @@ ${template === "graphql" ? "├── resolvers/ # GraphQL resolvers\n"
2841
2924
 
2842
2925
  ### Controllers
2843
2926
 
2844
- Use decorators to define routes:
2927
+ Use decorators to define routes. Annotate \`ctx\` with \`Ctx<KickRoutes.X['method']>\`
2928
+ to get fully-typed \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` from the
2929
+ generated \`KickRoutes\` namespace (refreshed on \`kick dev\` and \`kick typegen\`).
2845
2930
 
2846
2931
  \`\`\`ts
2847
- import { Controller, Get, Post, RequestContext } from '@forinda/kickjs'
2932
+ import { Controller, Get, Post, type Ctx } from '@forinda/kickjs'
2848
2933
 
2849
2934
  @Controller('/users')
2850
2935
  export class UserController {
2851
2936
  @Get('/')
2852
- async findAll(ctx: RequestContext) {
2937
+ async findAll(ctx: Ctx<KickRoutes.UserController['findAll']>) {
2853
2938
  return ctx.json({ users: [] })
2854
2939
  }
2855
2940
 
2856
2941
  @Post('/')
2857
- async create(ctx: RequestContext) {
2942
+ async create(ctx: Ctx<KickRoutes.UserController['create']>) {
2858
2943
  const data = ctx.body
2859
2944
  return ctx.created({ user: data })
2860
2945
  }
@@ -2897,7 +2982,8 @@ export class UserModule {}
2897
2982
 
2898
2983
  ### RequestContext
2899
2984
 
2900
- Every controller method receives \`ctx: RequestContext\`:
2985
+ Every controller method receives a \`ctx\` (alias \`Ctx<TRoute>\` or the
2986
+ loose \`RequestContext\`):
2901
2987
 
2902
2988
  \`\`\`ts
2903
2989
  ctx.body // Request body (parsed JSON)
@@ -3404,6 +3490,7 @@ async function initProject(options) {
3404
3490
  await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
3405
3491
  await writeFileSafe(join(dir, ".env"), generateEnv());
3406
3492
  await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
3493
+ await writeFileSafe(join(dir, "src/env.ts"), generateEnvFile());
3407
3494
  await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
3408
3495
  await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
3409
3496
  await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
@@ -3448,6 +3535,14 @@ async function initProject(options) {
3448
3535
  console.log(`\n Warning: ${packageManager} install failed. Run it manually.`);
3449
3536
  }
3450
3537
  }
3538
+ try {
3539
+ const { runTypegen } = await import("./typegen-DCnJdqP1.mjs");
3540
+ await runTypegen({
3541
+ cwd: dir,
3542
+ allowDuplicates: true,
3543
+ silent: true
3544
+ });
3545
+ } catch {}
3451
3546
  console.log("\n Project scaffolded successfully!");
3452
3547
  console.log();
3453
3548
  const needsCd = dir !== process.cwd();