@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/cli.mjs +1277 -78
- package/dist/index.d.mts +56 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +150 -55
- package/dist/index.mjs.map +1 -1
- package/dist/typegen-DCnJdqP1.mjs +886 -0
- package/dist/typegen-DCnJdqP1.mjs.map +1 -0
- package/package.json +1 -1
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) */
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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;;
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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,
|
|
2446
|
-
|
|
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:
|
|
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:
|
|
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: [
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
|
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();
|