@forinda/kickjs-cli 8.10.2 → 8.11.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/{agent-docs-ZgfnGL-j.mjs → agent-docs-DGE_9641.mjs} +23 -19
- package/dist/agent-docs-DGE_9641.mjs.map +1 -0
- package/dist/{agent-docs-As_KjQBC.mjs → agent-docs-DUxCvCBc.mjs} +30 -26
- package/dist/{build-BuBCsnzr.mjs → build-BQiF63pu.mjs} +3 -3
- package/dist/{build-BuBCsnzr.mjs.map → build-BQiF63pu.mjs.map} +1 -1
- package/dist/{build-BfC9Kkt-.mjs → build-BUFgk6Ss.mjs} +2 -2
- package/dist/{builtins-DXASTS7y.mjs → builtins-CaOnJmm8.mjs} +2 -2
- package/dist/{builtins-397XZB9_.mjs → builtins-d9YrHyb1.mjs} +378 -171
- package/dist/cli.mjs +2 -2
- package/dist/config-CcsYA02N.mjs +13 -0
- package/dist/config-CcsYA02N.mjs.map +1 -0
- package/dist/config-DhzyaY1S.mjs +12 -0
- package/dist/{doctor-DGWvBOvj.mjs → doctor-CYnItgYt.mjs} +278 -71
- package/dist/doctor-CYnItgYt.mjs.map +1 -0
- package/dist/{fullstack-BqYdhTDI.mjs → fullstack--utMEADY.mjs} +8 -8
- package/dist/{fullstack-BqYdhTDI.mjs.map → fullstack--utMEADY.mjs.map} +1 -1
- package/dist/{fullstack-x16HfCRz.mjs → fullstack-D6ikgPKw.mjs} +7 -7
- package/dist/index.d.mts +21 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{package-manager-DBmD5nSr.mjs → package-manager-BaXE6gY_.mjs} +3 -3
- package/dist/package-manager-BaXE6gY_.mjs.map +1 -0
- package/dist/{package-manager--IUl91nq.mjs → package-manager-Ogog_AHE.mjs} +2 -2
- package/dist/{plugin-HazJu53p.mjs → plugin-C7ybMh21.mjs} +2 -2
- package/dist/{plugin-Dh0LYWl5.mjs → plugin-DS5g6A5U.mjs} +2 -2
- package/dist/project-CDUJMeIr.mjs +133 -0
- package/dist/project-CGCAZsQV.mjs +134 -0
- package/dist/project-CGCAZsQV.mjs.map +1 -0
- package/dist/{project-root-DGKTJDH3.mjs → project-root-Da2aMneY.mjs} +2 -2
- package/dist/{project-root-DlgCWwc_.mjs → project-root-ThZdP1t_.mjs} +3 -3
- package/dist/{project-root-DlgCWwc_.mjs.map → project-root-ThZdP1t_.mjs.map} +1 -1
- package/dist/{prompts-D4nwf5mP.mjs → prompts-Pzk_Seut.mjs} +2 -2
- package/dist/{prompts-D4nwf5mP.mjs.map → prompts-Pzk_Seut.mjs.map} +1 -1
- package/dist/{rolldown-runtime-7c5UUG6D.mjs → rolldown-runtime-DAPeToCs.mjs} +1 -1
- package/dist/{run-plugins-Dx-QENlm.mjs → run-plugins-Du6FlB7_.mjs} +86 -86
- package/dist/run-plugins-Du6FlB7_.mjs.map +1 -0
- package/dist/typegen-9tepCLIZ.mjs +107 -0
- package/dist/typegen-9tepCLIZ.mjs.map +1 -0
- package/dist/typegen-BkpCnSQ3.mjs +106 -0
- package/dist/{types-CtrnAiNl.mjs → types-DJXxhn43.mjs} +1 -1
- package/package.json +3 -3
- package/templates/base/files/README.md +2 -2
- package/templates/base/files/kick.config.ts +1 -1
- package/templates/base/files/src/modules/hello/hello.controller.ts +0 -5
- package/templates/base/files/src/modules/hello/hello.service.ts +0 -4
- package/templates/feature-devtools/feature.json +2 -6
- package/templates/feature-devtools/files/src/devtools.d.ts +3 -0
- package/templates/feature-queue/feature.json +39 -3
- package/templates/feature-queue/files/_dot_env.example.append +4 -2
- package/templates/feature-queue/files/src/adapters/queue.ts +16 -0
- package/templates/feature-storage/feature.json +1 -1
- package/templates/schema-valibot/files/src/config/index.ts +5 -2
- package/templates/schema-yup/files/src/config/index.ts +5 -2
- package/templates/schema-zod/files/src/config/index.ts +5 -2
- package/templates/template-inertia/feature.json +9 -1
- package/templates/template-inertia/files/_dot_env.append +2 -0
- package/templates/template-inertia/files/_dot_env.example.append +6 -2
- package/templates/template-inertia/files/_dot_env.test.append +1 -0
- package/templates/template-inertia/files/_dot_env.test.example.append +1 -0
- package/templates/template-inertia/files/src/client/Layout.tsx +2 -0
- package/templates/template-inertia/files/src/client/Seo.tsx +35 -0
- package/templates/template-inertia/files/src/index.ts +11 -5
- package/templates/template-inertia/files/src/modules/index.ts +2 -1
- package/templates/template-inertia/files/src/modules/pages/pages.controller.ts +18 -6
- package/templates/template-inertia/files/src/modules/pages/pages.test.ts +34 -0
- package/templates/template-inertia/files/src/modules/seo/seo.controller.ts +19 -0
- package/templates/template-inertia/files/src/modules/seo/seo.module.ts +12 -0
- package/templates/web-kick/files/web/_dot_oxfmtrc.json +9 -0
- package/templates/web-kick/files/web/src/App.tsx +1 -7
- package/dist/agent-docs-ZgfnGL-j.mjs.map +0 -1
- package/dist/config-Ck9aQVpE.mjs +0 -12
- package/dist/config-CsOfUFfq.mjs +0 -13
- package/dist/config-CsOfUFfq.mjs.map +0 -1
- package/dist/doctor-DGWvBOvj.mjs.map +0 -1
- package/dist/package-manager-DBmD5nSr.mjs.map +0 -1
- package/dist/project-C98JovY4.mjs +0 -115
- package/dist/project-C98JovY4.mjs.map +0 -1
- package/dist/project-TbTVQCnh.mjs +0 -114
- package/dist/run-plugins-Dx-QENlm.mjs.map +0 -1
- package/dist/typegen-DJQ6WBwr.mjs +0 -106
- package/dist/typegen-DSaawXaQ.mjs +0 -107
- package/dist/typegen-DSaawXaQ.mjs.map +0 -1
- package/templates/feature-queue/files/_dot_env.append +0 -2
- package/templates/template-inertia/files/public/robots.txt +0 -4
- /package/templates/feature-storage/files/src/{storage.ts → adapters/storage.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli v8.
|
|
2
|
+
* @forinda/kickjs-cli v8.11.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{i as e,o as t,t as n}from"./package-manager-
|
|
11
|
+
import{i as e,o as t,t as n}from"./package-manager-BaXE6gY_.mjs";import{a as r,n as i,r as a,t as o,u as s}from"./prompts-Pzk_Seut.mjs";import{s as c}from"./config-CcsYA02N.mjs";import{c as l,i as u}from"./project-CGCAZsQV.mjs";import{t as d}from"./project-root-ThZdP1t_.mjs";import{createRequire as f}from"node:module";import p,{dirname as m,join as h,relative as g,resolve as _}from"node:path";import{existsSync as v,readFileSync as y,readdirSync as b,statSync as x}from"node:fs";import{readFile as S}from"node:fs/promises";import C from"pluralize";import{pathToFileURL as w}from"node:url";import{defineGenerator as ee}from"@forinda/kickjs-cli-kit";C.addUncountableRule(`auth`);function T(e){return e.replace(/[-_\s]+(.)?/g,(e,t)=>t?t.toUpperCase():``).replace(/^(.)/,e=>e.toUpperCase())}function E(e){let t=T(e);return t.charAt(0).toLowerCase()+t.slice(1)}function D(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).replace(/[\s_]+/g,`-`).toLowerCase()}function O(e){return C.plural(e)}function k(e){return C.plural(e)}function A(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}const te={inmemory:`in-memory`,drizzle:`Drizzle`,prisma:`Prisma`};function ne(e){return e.charAt(0).toUpperCase()+e.slice(1).replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}function re(e){return te[e]??ne(e)}function ie(e){return`create${e}Repository`}function j(e){return e??`define`}function M(e){let{pascal:t,kebab:n,plural:r=``,repo:i,style:a}=e,o=e.routePath??r,s=ie(t),c=j(a),l=`/**
|
|
12
12
|
* ${t} Module
|
|
13
13
|
*
|
|
14
14
|
* REST module with a flat folder structure.
|
|
@@ -21,10 +21,10 @@ import{i as e,o as t,t as n}from"./package-manager-DBmD5nSr.mjs";import{a as r,n
|
|
|
21
21
|
* dtos/ — Request/response schemas
|
|
22
22
|
*
|
|
23
23
|
* The repository is backed by an in-memory Map so this module works as
|
|
24
|
-
* generated. Swap in ${i===`inmemory`?`a database`:
|
|
24
|
+
* generated. Swap in ${i===`inmemory`?`a database`:re(i)} by replacing the factory body in
|
|
25
25
|
* ${n}.repository.ts — the contract is whatever that factory returns, so
|
|
26
26
|
* nothing else has to change.
|
|
27
|
-
*/`,
|
|
27
|
+
*/`,u=`import { ${t.toUpperCase()}_REPOSITORY, ${s} } from './${n}.repository'
|
|
28
28
|
import { ${t}Controller } from './${n}.controller'
|
|
29
29
|
|
|
30
30
|
// Eagerly load every module file so decorators (@Controller / @Service /
|
|
@@ -33,7 +33,7 @@ import { ${t}Controller } from './${n}.controller'
|
|
|
33
33
|
// to emit, so a hand-written \`*.usecase.ts\` or \`*.policy.ts\` silently never
|
|
34
34
|
// registered and failed later as \`No provider for X\` (#609). Recursive (./**/)
|
|
35
35
|
// so nesting keeps working.
|
|
36
|
-
import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./**/*.d.ts'], { eager: true })`,
|
|
36
|
+
import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./**/*.d.ts'], { eager: true })`,d=` /**
|
|
37
37
|
* Declare HTTP routes for this module. Return value shape:
|
|
38
38
|
*
|
|
39
39
|
* - \`path\` — URL prefix for this route set.
|
|
@@ -44,49 +44,49 @@ import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./**/*.d.ts'], { eager: true
|
|
|
44
44
|
* surfaces, side-by-side v1 + v2 controllers, etc:
|
|
45
45
|
*
|
|
46
46
|
* return [
|
|
47
|
-
* { path: '/${
|
|
48
|
-
* { path: '/${
|
|
47
|
+
* { path: '/${o}', version: 1, controller: ${t}V1Controller },
|
|
48
|
+
* { path: '/${o}', version: 2, controller: ${t}V2Controller },
|
|
49
49
|
* ]
|
|
50
|
-
*/`;return
|
|
50
|
+
*/`;return c===`class`?`${l}
|
|
51
51
|
import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs'
|
|
52
|
-
${
|
|
52
|
+
${u}
|
|
53
53
|
|
|
54
54
|
export class ${t}Module implements AppModule {
|
|
55
55
|
register(container: Container): void {
|
|
56
|
-
container.registerFactory(${t.toUpperCase()}_REPOSITORY, () => ${
|
|
56
|
+
container.registerFactory(${t.toUpperCase()}_REPOSITORY, () => ${s}())
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
${
|
|
59
|
+
${d.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
|
|
60
60
|
routes(): ModuleRoutes {
|
|
61
61
|
return {
|
|
62
|
-
path: '/${
|
|
62
|
+
path: '/${o}',
|
|
63
63
|
controller: ${t}Controller,
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
`:`${
|
|
67
|
+
`:`${l}
|
|
68
68
|
import { defineModule } from '@forinda/kickjs'
|
|
69
|
-
${
|
|
69
|
+
${u}
|
|
70
70
|
|
|
71
71
|
export const ${t}Module = defineModule({
|
|
72
72
|
name: '${t}Module',
|
|
73
73
|
build: () => ({
|
|
74
74
|
register(container) {
|
|
75
75
|
container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
|
|
76
|
-
${
|
|
76
|
+
${s}(),
|
|
77
77
|
)
|
|
78
78
|
},
|
|
79
79
|
|
|
80
|
-
${
|
|
80
|
+
${d}
|
|
81
81
|
routes() {
|
|
82
82
|
return {
|
|
83
|
-
path: '/${
|
|
83
|
+
path: '/${o}',
|
|
84
84
|
controller: ${t}Controller,
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
}),
|
|
88
88
|
})
|
|
89
|
-
`}function
|
|
89
|
+
`}function ae(e){let{pascal:t,kebab:n,plural:r=``,style:i}=e,a=e.routePath??r,o=j(i),s=` /**
|
|
90
90
|
* Declare HTTP routes. Return value shape:
|
|
91
91
|
*
|
|
92
92
|
* - \`path\` — URL prefix for this route set.
|
|
@@ -96,17 +96,17 @@ ${u}
|
|
|
96
96
|
* Return an array to mount multiple route sets:
|
|
97
97
|
*
|
|
98
98
|
* return [
|
|
99
|
-
* { path: '/${
|
|
100
|
-
* { path: '/${
|
|
99
|
+
* { path: '/${a}', version: 1, controller: ${t}V1Controller },
|
|
100
|
+
* { path: '/${a}', version: 2, controller: ${t}V2Controller },
|
|
101
101
|
* ]
|
|
102
|
-
*/`;return
|
|
102
|
+
*/`;return o===`class`?`import { type AppModule, type ModuleRoutes } from '@forinda/kickjs'
|
|
103
103
|
import { ${t}Controller } from './${n}.controller'
|
|
104
104
|
|
|
105
105
|
export class ${t}Module implements AppModule {
|
|
106
|
-
${
|
|
106
|
+
${s.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
|
|
107
107
|
routes(): ModuleRoutes {
|
|
108
108
|
return {
|
|
109
|
-
path: '/${
|
|
109
|
+
path: '/${a}',
|
|
110
110
|
controller: ${t}Controller,
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -117,10 +117,10 @@ import { ${t}Controller } from './${n}.controller'
|
|
|
117
117
|
export const ${t}Module = defineModule({
|
|
118
118
|
name: '${t}Module',
|
|
119
119
|
build: () => ({
|
|
120
|
-
${
|
|
120
|
+
${s}
|
|
121
121
|
routes() {
|
|
122
122
|
return {
|
|
123
|
-
path: '/${
|
|
123
|
+
path: '/${a}',
|
|
124
124
|
controller: ${t}Controller,
|
|
125
125
|
}
|
|
126
126
|
},
|
|
@@ -131,7 +131,7 @@ ${o}
|
|
|
131
131
|
Controller,
|
|
132
132
|
Get,
|
|
133
133
|
Post,
|
|
134
|
-
|
|
134
|
+
Patch,
|
|
135
135
|
Delete,
|
|
136
136
|
Autowired,
|
|
137
137
|
ApiQueryParams,
|
|
@@ -157,10 +157,18 @@ import { ${t.toUpperCase()}_QUERY_CONFIG } from './${n}.constants'
|
|
|
157
157
|
// \`ctx.problem.*\` for error branches — those send immediately and return
|
|
158
158
|
// nothing, so they stay out of the success type.
|
|
159
159
|
|
|
160
|
+
// Require a signed-in user for these routes: https://kickjs.app/guide/authentication
|
|
160
161
|
@Controller()
|
|
161
162
|
export class ${t}Controller {
|
|
162
163
|
@Autowired() private readonly ${a}Service!: ${t}Service
|
|
163
164
|
|
|
165
|
+
// Offset pages (?page=&limit=). For keyset pages (?cursor=&limit=) — stable
|
|
166
|
+
// under concurrent inserts, no deep OFFSET scans — swap the body for:
|
|
167
|
+
// return ctx.paginateCursor(
|
|
168
|
+
// (q) => this.${a}Service.findAfter({ ...q, limit: q.limit + 1 }),
|
|
169
|
+
// ${t.toUpperCase()}_QUERY_CONFIG,
|
|
170
|
+
// )
|
|
171
|
+
// https://kickjs.app/guide/query-parsing#cursor-pagination
|
|
164
172
|
@Get('/')
|
|
165
173
|
${i} @ApiQueryParams(${t.toUpperCase()}_QUERY_CONFIG)
|
|
166
174
|
async list(ctx: Ctx<KickRoutes.${t}Controller['list']>) {
|
|
@@ -184,10 +192,14 @@ ${i} async getById(ctx: Ctx<KickRoutes.${t}Controller['getById']>) {
|
|
|
184
192
|
|
|
185
193
|
@Post('/', { body: create${t}Schema, name: 'Create${t}' })
|
|
186
194
|
${i} async create(ctx: Ctx<KickRoutes.${t}Controller['create']>) {
|
|
187
|
-
|
|
195
|
+
const created = await this.${a}Service.create(ctx.body)
|
|
196
|
+
// 201 + Location: where the new ${n} lives, built from the getById route.
|
|
197
|
+
return reply.created(created, {
|
|
198
|
+
location: ctx.urlFor('${t}Controller.getById', { id: created.id }),
|
|
199
|
+
})
|
|
188
200
|
}
|
|
189
201
|
|
|
190
|
-
@
|
|
202
|
+
@Patch('/:id', { body: update${t}Schema, name: 'Update${t}' })
|
|
191
203
|
${i} async update(ctx: Ctx<KickRoutes.${t}Controller['update']>) {
|
|
192
204
|
return this.${a}Service.update(ctx.params.id, ctx.body)
|
|
193
205
|
}
|
|
@@ -201,7 +213,7 @@ ${i} async remove(ctx: Ctx<KickRoutes.${t}Controller['remove']>) {
|
|
|
201
213
|
`}const P={zod:{import:`import { z } from 'zod'`,required:`z.string().min(1, { error: 'Name is required' }).max(200)`,optional:`z.string().min(1).max(200).optional()`,infer:e=>`z.infer<typeof ${e}>`,hint:[` * z.string(), z.number(), z.boolean(), z.enum([...]),`,` * z.array(), z.object(), .optional(), .default(), .transform()`].join(`
|
|
202
214
|
`)},valibot:{import:`import * as v from 'valibot'`,required:`v.pipe(v.string(), v.minLength(1, 'Name is required'), v.maxLength(200))`,optional:`v.optional(v.pipe(v.string(), v.minLength(1), v.maxLength(200)))`,infer:e=>`v.InferOutput<typeof ${e}>`,hint:[` * v.string(), v.number(), v.boolean(), v.picklist([...]),`,` * v.array(), v.object(), v.optional(), and v.pipe(...) to chain checks`].join(`
|
|
203
215
|
`)},yup:{import:`import * as yup from 'yup'`,required:`yup.string().required('Name is required').min(1).max(200)`,optional:`yup.string().min(1).max(200).optional()`,infer:e=>`yup.InferType<typeof ${e}>`,hint:[` * yup.string(), yup.number(), yup.boolean(), .oneOf([...]),`,` * yup.array(), yup.object(), .required(), .optional(), .default()`].join(`
|
|
204
|
-
`)}};function F(e,t){return`${e===`zod`?`z.object`:e===`valibot`?`v.object`:`yup.object`}({\n ${t},\n})`}function
|
|
216
|
+
`)}};function F(e,t){return`${e===`zod`?`z.object`:e===`valibot`?`v.object`:`yup.object`}({\n ${t},\n})`}function oe(e){let{pascal:t}=e,n=e.schemaLib??`zod`,r=P[n],i=`create${t}Schema`,a=n===`zod`?`Zod`:n===`valibot`?`Valibot`:`Yup`;return`${r.import}
|
|
205
217
|
|
|
206
218
|
/**
|
|
207
219
|
* Create ${t} DTO — ${a} schema for validating POST request bodies.
|
|
@@ -214,12 +226,12 @@ ${r.hint}
|
|
|
214
226
|
export const ${i} = ${F(n,`name: ${r.required}`)}
|
|
215
227
|
|
|
216
228
|
export type Create${t}DTO = ${r.infer(i)}
|
|
217
|
-
`}function
|
|
229
|
+
`}function se(e){let{pascal:t}=e,n=e.schemaLib??`zod`,r=P[n],i=`update${t}Schema`;return`${r.import}
|
|
218
230
|
|
|
219
231
|
export const ${i} = ${F(n,`name: ${r.optional}`)}
|
|
220
232
|
|
|
221
233
|
export type Update${t}DTO = ${r.infer(i)}
|
|
222
|
-
`}function
|
|
234
|
+
`}function ce(e){let{pascal:t}=e;return`export interface ${t}ResponseDTO {
|
|
223
235
|
id: string
|
|
224
236
|
name: string
|
|
225
237
|
createdAt: string
|
|
@@ -227,6 +239,22 @@ export type Update${t}DTO = ${r.infer(i)}
|
|
|
227
239
|
}
|
|
228
240
|
`}function I(e){let{pascal:t,kebab:n,repoType:r,dtoPrefix:i=`./dtos`,tokenScope:a=`app`}=e,o=r&&r!==`inmemory`?r:``,s=`create${t}Repository`,c=` const store = new Map<string, ${t}ResponseDTO>()
|
|
229
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Filtered, searched and sorted. \`byId\` breaks sort-key ties by id — what a
|
|
244
|
+
* cursor needs; without it ties (and an unsorted list) keep insertion order.
|
|
245
|
+
*/
|
|
246
|
+
function select(parsed: Omit<ParsedQuery, 'pagination'>, byId = false): ${t}ResponseDTO[] {
|
|
247
|
+
// The parser already limited fields to ${t.toUpperCase()}_QUERY_CONFIG.
|
|
248
|
+
const q = parsed.search.toLowerCase()
|
|
249
|
+
return [...store.values()]
|
|
250
|
+
.filter(
|
|
251
|
+
(row) =>
|
|
252
|
+
parsed.filters.every((f) => matches(field(row, f.field), f)) &&
|
|
253
|
+
(!q || SEARCHABLE.some((name) => String(field(row, name) ?? '').toLowerCase().includes(q))),
|
|
254
|
+
)
|
|
255
|
+
.sort((a, b) => order(a, b, parsed.sort, byId))
|
|
256
|
+
}
|
|
257
|
+
|
|
230
258
|
return {
|
|
231
259
|
async findById(id: string): Promise<${t}ResponseDTO | null> {
|
|
232
260
|
return store.get(id) ?? null
|
|
@@ -239,9 +267,28 @@ export type Update${t}DTO = ${r.infer(i)}
|
|
|
239
267
|
async findPaginated(
|
|
240
268
|
parsed: ParsedQuery,
|
|
241
269
|
): Promise<{ data: ${t}ResponseDTO[]; total: number }> {
|
|
242
|
-
const
|
|
270
|
+
const rows = select(parsed)
|
|
243
271
|
const { offset, limit } = parsed.pagination
|
|
244
|
-
return { data:
|
|
272
|
+
return { data: rows.slice(offset, offset + limit), total: rows.length }
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Keyset page for \`ctx.paginateCursor\`: the rows strictly after
|
|
277
|
+
* \`cursor\` in sort order (id breaks ties), at most \`limit\` of them —
|
|
278
|
+
* the caller asks for one more than it shows, to learn whether a next
|
|
279
|
+
* page exists. A database does the same with
|
|
280
|
+
* \`WHERE (sortField, id) > (cursor.sortField, cursor.id)\`.
|
|
281
|
+
*/
|
|
282
|
+
async findAfter(
|
|
283
|
+
query: Omit<ParsedQuery, 'pagination'> & {
|
|
284
|
+
cursor: Readonly<Record<string, unknown>> | null
|
|
285
|
+
limit: number
|
|
286
|
+
},
|
|
287
|
+
): Promise<${t}ResponseDTO[]> {
|
|
288
|
+
const { cursor, limit, sort } = query
|
|
289
|
+
const rows = select(query, true)
|
|
290
|
+
const after = cursor ? rows.filter((row) => order(row, cursor, sort, true) > 0) : rows
|
|
291
|
+
return after.slice(0, limit)
|
|
245
292
|
},
|
|
246
293
|
|
|
247
294
|
async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
|
|
@@ -277,11 +324,71 @@ export type Update${t}DTO = ${r.infer(i)}
|
|
|
277
324
|
*/
|
|
278
325
|
import { randomUUID } from 'node:crypto'
|
|
279
326
|
import { createToken, HttpException } from '@forinda/kickjs'
|
|
280
|
-
import type { ParsedQuery } from '@forinda/kickjs'
|
|
327
|
+
import type { FilterItem, ParsedQuery, SortItem } from '@forinda/kickjs'
|
|
328
|
+
import { ${t.toUpperCase()}_QUERY_CONFIG } from './${n}.constants'
|
|
281
329
|
import type { ${t}ResponseDTO } from '${i}/${n}-response.dto'
|
|
282
330
|
import type { Create${t}DTO } from '${i}/create-${n}.dto'
|
|
283
331
|
import type { Update${t}DTO } from '${i}/update-${n}.dto'
|
|
284
332
|
|
|
333
|
+
const SEARCHABLE = (${t.toUpperCase()}_QUERY_CONFIG.searchable ?? []) as string[]
|
|
334
|
+
|
|
335
|
+
function field(row: object, name: string): unknown {
|
|
336
|
+
return (row as Record<string, unknown>)[name]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* A stored number compares by value (the query's value is text); everything
|
|
341
|
+
* else, ISO dates included, as text — so a string field keeps '001' ≠ '1'.
|
|
342
|
+
*/
|
|
343
|
+
function compare(stored: unknown, other: unknown): number {
|
|
344
|
+
if (typeof stored === 'number') return stored - Number(other)
|
|
345
|
+
return String(stored ?? '').localeCompare(String(other ?? ''))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Sort order of two rows (or a row and a cursor position): each sort field in
|
|
350
|
+
* turn, then — with \`byId\` — id, in the direction of the last sort field.
|
|
351
|
+
*/
|
|
352
|
+
function order(a: object, b: object, sort: readonly SortItem[], byId: boolean): number {
|
|
353
|
+
for (const { field: name, direction } of sort) {
|
|
354
|
+
const result = compare(field(a, name), field(b, name))
|
|
355
|
+
if (result) return direction === 'desc' ? -result : result
|
|
356
|
+
}
|
|
357
|
+
if (!byId) return 0
|
|
358
|
+
const result = compare(field(a, 'id'), field(b, 'id'))
|
|
359
|
+
return sort.at(-1)?.direction === 'desc' ? -result : result
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** One \`?filter=field:op:value\` — \`in\` and \`between\` take comma-separated values. */
|
|
363
|
+
function matches(value: unknown, { operator, value: raw }: FilterItem): boolean {
|
|
364
|
+
const text = String(value ?? '').toLowerCase()
|
|
365
|
+
const list = raw.split(',')
|
|
366
|
+
switch (operator) {
|
|
367
|
+
case 'eq':
|
|
368
|
+
return compare(value, raw) === 0
|
|
369
|
+
case 'neq':
|
|
370
|
+
return compare(value, raw) !== 0
|
|
371
|
+
case 'gt':
|
|
372
|
+
return compare(value, raw) > 0
|
|
373
|
+
case 'gte':
|
|
374
|
+
return compare(value, raw) >= 0
|
|
375
|
+
case 'lt':
|
|
376
|
+
return compare(value, raw) < 0
|
|
377
|
+
case 'lte':
|
|
378
|
+
return compare(value, raw) <= 0
|
|
379
|
+
case 'between':
|
|
380
|
+
return compare(value, list[0]) >= 0 && compare(value, list[1]) <= 0
|
|
381
|
+
case 'in':
|
|
382
|
+
return list.some((v) => compare(value, v) === 0)
|
|
383
|
+
case 'contains':
|
|
384
|
+
return text.includes(raw.toLowerCase())
|
|
385
|
+
case 'starts':
|
|
386
|
+
return text.startsWith(raw.toLowerCase())
|
|
387
|
+
case 'ends':
|
|
388
|
+
return text.endsWith(raw.toLowerCase())
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
285
392
|
export function ${s}() {
|
|
286
393
|
${c}
|
|
287
394
|
}
|
|
@@ -300,7 +407,7 @@ export type ${t}Repository = ReturnType<typeof ${s}>
|
|
|
300
407
|
* adopters must NOT use the reserved \`'kick/'\` namespace.
|
|
301
408
|
*/
|
|
302
409
|
export const ${t.toUpperCase()}_REPOSITORY = createToken<${t}Repository>('${a}/${t}/repository')
|
|
303
|
-
`}function
|
|
410
|
+
`}function le(e){let{pascal:t,kebab:n,plural:r=``,style:i=`define`,testHarness:a=!1}=e,o=e.routePath??r,s=i===`define`?`${t}Module()`:`${t}Module`;return`${a?`import { describe, it, expect, beforeEach } from 'vitest'
|
|
304
411
|
import request from 'supertest'
|
|
305
412
|
import { Container } from '@forinda/kickjs'
|
|
306
413
|
import { createTestApp } from '@forinda/kickjs-testing'
|
|
@@ -316,10 +423,10 @@ import { ${t}Module } from '../${n}.module'
|
|
|
316
423
|
* pass the same values in \`boot()\` below and update this to match, so the test
|
|
317
424
|
* exercises the paths production actually serves:
|
|
318
425
|
*
|
|
319
|
-
* defaultVersion: false → '/api/${
|
|
320
|
-
* apiPrefix: '/v1' → '/v1/v1/${
|
|
426
|
+
* defaultVersion: false → '/api/${o}'
|
|
427
|
+
* apiPrefix: '/v1' → '/v1/v1/${o}'
|
|
321
428
|
*/
|
|
322
|
-
const BASE = '/api/v1/${
|
|
429
|
+
const BASE = '/api/v1/${o}'
|
|
323
430
|
`:`import { describe, it, beforeEach } from 'vitest'
|
|
324
431
|
import { Container } from '@forinda/kickjs'
|
|
325
432
|
|
|
@@ -342,7 +449,7 @@ ${a?` /**
|
|
|
342
449
|
*/
|
|
343
450
|
async function boot() {
|
|
344
451
|
const { app, container } = await createTestApp({
|
|
345
|
-
modules: [${
|
|
452
|
+
modules: [${s}],
|
|
346
453
|
// Mirror your bootstrap() here if it overrides either, and update BASE:
|
|
347
454
|
// apiPrefix: '/api',
|
|
348
455
|
// defaultVersion: 1,
|
|
@@ -350,7 +457,7 @@ ${a?` /**
|
|
|
350
457
|
return { app, container, agent: request(app.handle.bind(app)) }
|
|
351
458
|
}
|
|
352
459
|
|
|
353
|
-
describe('GET /${
|
|
460
|
+
describe('GET /${o}', () => {
|
|
354
461
|
it('returns an empty page before anything is created', async () => {
|
|
355
462
|
const { agent } = await boot()
|
|
356
463
|
const res = await agent.get(BASE)
|
|
@@ -359,8 +466,18 @@ ${a?` /**
|
|
|
359
466
|
expect(res.body).toMatchObject({ data: [], meta: { page: 1 } })
|
|
360
467
|
})
|
|
361
468
|
|
|
469
|
+
it('filters and sorts through the query string', async () => {
|
|
470
|
+
const { agent } = await boot()
|
|
471
|
+
for (const name of ['Alpha', 'Beta', 'Gamma']) await agent.post(BASE).send({ name })
|
|
472
|
+
const res = await agent.get(\`\${BASE}?filter=name:neq:Beta&sort=name:desc\`)
|
|
473
|
+
|
|
474
|
+
expect(res.status).toBe(200)
|
|
475
|
+
expect(res.body.data.map((row: { name: string }) => row.name)).toEqual(['Gamma', 'Alpha'])
|
|
476
|
+
expect(res.body.meta.total).toBe(2)
|
|
477
|
+
})
|
|
478
|
+
|
|
362
479
|
it.todo('returns created ${r}, paginated')
|
|
363
|
-
})`:` describe('GET /${
|
|
480
|
+
})`:` describe('GET /${o}', () => {
|
|
364
481
|
// Boot the module with createTestApp and assert { data, meta }.
|
|
365
482
|
it.todo('returns an empty page before anything is created')
|
|
366
483
|
it.todo('returns created ${r}, paginated')
|
|
@@ -370,31 +487,38 @@ ${a?` /**
|
|
|
370
487
|
// reporter lists it as outstanding instead of counting it as a pass. Replace
|
|
371
488
|
// the todo with a real test as you implement each endpoint.
|
|
372
489
|
|
|
373
|
-
describe('POST /${
|
|
374
|
-
|
|
375
|
-
|
|
490
|
+
describe('POST /${o}', () => {
|
|
491
|
+
${a?` it('creates a ${n} and points Location at it', async () => {
|
|
492
|
+
const { agent } = await boot()
|
|
493
|
+
const res = await agent.post(BASE).send({ name: 'Test ${t}' })
|
|
494
|
+
|
|
495
|
+
expect(res.status).toBe(201)
|
|
496
|
+
expect(res.body).toMatchObject({ name: 'Test ${t}' })
|
|
497
|
+
expect(res.headers.location).toBe(\`\${BASE}/\${res.body.id}\`)
|
|
498
|
+
})`:` // Post a valid body, assert 201, the created ${n}, and its Location header.
|
|
499
|
+
it.todo('creates a ${n}')`}
|
|
376
500
|
// Post an invalid body, assert 422 and the validation detail.
|
|
377
501
|
it.todo('rejects an invalid body')
|
|
378
502
|
})
|
|
379
503
|
|
|
380
|
-
describe('GET /${
|
|
504
|
+
describe('GET /${o}/:id', () => {
|
|
381
505
|
// Create one, fetch it by id, assert the payload matches.
|
|
382
506
|
it.todo('returns a ${n} by id')
|
|
383
507
|
// Fetch an id that does not exist, assert 404.
|
|
384
508
|
it.todo('returns 404 for an unknown id')
|
|
385
509
|
})
|
|
386
510
|
|
|
387
|
-
describe('
|
|
511
|
+
describe('PATCH /${o}/:id', () => {
|
|
388
512
|
// Create, update, assert the change is reflected on a subsequent read.
|
|
389
513
|
it.todo('updates an existing ${n}')
|
|
390
514
|
})
|
|
391
515
|
|
|
392
|
-
describe('DELETE /${
|
|
516
|
+
describe('DELETE /${o}/:id', () => {
|
|
393
517
|
// Create, delete, assert a subsequent read is 404.
|
|
394
518
|
it.todo('deletes a ${n}')
|
|
395
519
|
})
|
|
396
520
|
})
|
|
397
|
-
`}function
|
|
521
|
+
`}function ue(e){let{pascal:t,kebab:n,plural:r=``,repoPrefix:i=`../${n}.repository`}=e,a=`create${t}Repository`;return`import { describe, it, expect, beforeEach } from 'vitest'
|
|
398
522
|
import { ${a}, type ${t}Repository } from '${i}'
|
|
399
523
|
|
|
400
524
|
describe('${t} repository', () => {
|
|
@@ -443,6 +567,83 @@ describe('${t} repository', () => {
|
|
|
443
567
|
expect(result.total).toBe(3)
|
|
444
568
|
})
|
|
445
569
|
|
|
570
|
+
it('should filter, search and sort before paginating', async () => {
|
|
571
|
+
await repo.create({ name: 'Banana' })
|
|
572
|
+
await repo.create({ name: 'apple' })
|
|
573
|
+
await repo.create({ name: 'Cherry' })
|
|
574
|
+
const pagination = { page: 1, limit: 10, offset: 0 }
|
|
575
|
+
|
|
576
|
+
const sorted = await repo.findPaginated({
|
|
577
|
+
filters: [],
|
|
578
|
+
sort: [{ field: 'name', direction: 'desc' }],
|
|
579
|
+
search: '',
|
|
580
|
+
pagination,
|
|
581
|
+
})
|
|
582
|
+
expect(sorted.data.map((row) => row.name)).toEqual(['Cherry', 'Banana', 'apple'])
|
|
583
|
+
|
|
584
|
+
const filtered = await repo.findPaginated({
|
|
585
|
+
filters: [{ field: 'name', operator: 'neq', value: 'apple' }],
|
|
586
|
+
sort: [],
|
|
587
|
+
search: '',
|
|
588
|
+
pagination: { page: 1, limit: 1, offset: 0 },
|
|
589
|
+
})
|
|
590
|
+
expect(filtered.data).toHaveLength(1)
|
|
591
|
+
expect(filtered.total).toBe(2)
|
|
592
|
+
|
|
593
|
+
const searched = await repo.findPaginated({ filters: [], sort: [], search: 'AN', pagination })
|
|
594
|
+
expect(searched.data.map((row) => row.name)).toEqual(['Banana'])
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
it('compares a text field as text, not as a number', async () => {
|
|
598
|
+
await repo.create({ name: '001' })
|
|
599
|
+
await repo.create({ name: '10' })
|
|
600
|
+
await repo.create({ name: '9' })
|
|
601
|
+
const pagination = { page: 1, limit: 10, offset: 0 }
|
|
602
|
+
|
|
603
|
+
const exact = await repo.findPaginated({
|
|
604
|
+
filters: [{ field: 'name', operator: 'eq', value: '1' }],
|
|
605
|
+
sort: [],
|
|
606
|
+
search: '',
|
|
607
|
+
pagination,
|
|
608
|
+
})
|
|
609
|
+
expect(exact.total).toBe(0)
|
|
610
|
+
|
|
611
|
+
const sorted = await repo.findPaginated({
|
|
612
|
+
filters: [],
|
|
613
|
+
sort: [{ field: 'name', direction: 'asc' }],
|
|
614
|
+
search: '',
|
|
615
|
+
pagination,
|
|
616
|
+
})
|
|
617
|
+
expect(sorted.data.map((row) => row.name)).toEqual(['001', '10', '9'])
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it('pages through rows with findAfter, stable when sort keys tie', async () => {
|
|
621
|
+
// Three rows share 'a', so a page boundary falls inside the tie: only the
|
|
622
|
+
// id tie-breaker keeps the third 'a' from being skipped or repeated.
|
|
623
|
+
for (const name of ['b', 'a', 'b', 'a', 'a']) await repo.create({ name })
|
|
624
|
+
const query = { filters: [], sort: [{ field: 'name', direction: 'asc' as const }], search: '' }
|
|
625
|
+
const limit = 2
|
|
626
|
+
|
|
627
|
+
const pages: string[][] = []
|
|
628
|
+
const ids: string[] = []
|
|
629
|
+
let cursor: Record<string, unknown> | null = null
|
|
630
|
+
let hasNext = true
|
|
631
|
+
while (hasNext) {
|
|
632
|
+
// Ask for one more than a page, as ctx.paginateCursor's fetcher does.
|
|
633
|
+
const rows = await repo.findAfter({ ...query, cursor, limit: limit + 1 })
|
|
634
|
+
hasNext = rows.length > limit
|
|
635
|
+
const page = rows.slice(0, limit)
|
|
636
|
+
pages.push(page.map((row) => row.name))
|
|
637
|
+
ids.push(...page.map((row) => row.id))
|
|
638
|
+
const last = page.at(-1)
|
|
639
|
+
cursor = last ? { name: last.name, id: last.id } : null
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
expect(pages.map((page) => page.length)).toEqual([2, 2, 1])
|
|
643
|
+
expect(pages.flat()).toEqual(['a', 'a', 'a', 'b', 'b'])
|
|
644
|
+
expect(new Set(ids).size).toBe(5)
|
|
645
|
+
})
|
|
646
|
+
|
|
446
647
|
it('should update a ${n}', async () => {
|
|
447
648
|
const created = await repo.create({ name: 'Original' })
|
|
448
649
|
const updated = await repo.update(created.id, { name: 'Updated' })
|
|
@@ -481,6 +682,10 @@ export class ${t}Service {
|
|
|
481
682
|
return this.repo.findPaginated(parsed)
|
|
482
683
|
}
|
|
483
684
|
|
|
685
|
+
async findAfter(query: Parameters<${t}Repository['findAfter']>[0]) {
|
|
686
|
+
return this.repo.findAfter(query)
|
|
687
|
+
}
|
|
688
|
+
|
|
484
689
|
async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
|
|
485
690
|
return this.repo.create(dto)
|
|
486
691
|
}
|
|
@@ -493,14 +698,14 @@ export class ${t}Service {
|
|
|
493
698
|
await this.repo.delete(id)
|
|
494
699
|
}
|
|
495
700
|
}
|
|
496
|
-
`}function R(e){let{pascal:t}=e
|
|
701
|
+
`}const de=/* @__PURE__ */ new Set([`string`,`text`,`email`,`url`]),fe=/* @__PURE__ */ new Set([`json`]);function R(e){let{pascal:t,fields:n=[{name:`name`,type:`string`}]}=e,r=n.filter(e=>!fe.has(e.type)).map(e=>e.name),i=n.filter(e=>de.has(e.type)).map(e=>e.name),a=e=>`[${e.map(e=>`'${e}'`).join(`, `)}]`;return`import type { QueryFieldConfig } from '@forinda/kickjs'
|
|
497
702
|
|
|
498
703
|
export const ${t.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
|
|
499
|
-
filterable:
|
|
500
|
-
sortable: [
|
|
501
|
-
searchable:
|
|
704
|
+
filterable: ${a(r)},
|
|
705
|
+
sortable: ${a([...r.filter(e=>e!==`createdAt`),`createdAt`])},
|
|
706
|
+
searchable: ${a(i)},
|
|
502
707
|
}
|
|
503
|
-
`}async function
|
|
708
|
+
`}async function pe(e){let{pascal:t,kebab:n,plural:r,routePath:i,style:a,write:o}=e;await o(`${n}.module.ts`,ae({pascal:t,kebab:n,plural:r,routePath:i,style:a})),await o(`${n}.controller.ts`,`import { Controller, Get, Post, Patch, Delete, reply, type Ctx } from '@forinda/kickjs'
|
|
504
709
|
|
|
505
710
|
// \`Ctx<KickRoutes.${t}Controller['<method>']>\` is generated by
|
|
506
711
|
// \`kick typegen\` (auto-run on \`kick dev\`).
|
|
@@ -513,6 +718,7 @@ export const ${t.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
|
|
|
513
718
|
// "Minimal" is about the file count, not the route surface — you get the whole
|
|
514
719
|
// CRUD shape here in one file.
|
|
515
720
|
|
|
721
|
+
// Require a signed-in user for these routes: https://kickjs.app/guide/authentication
|
|
516
722
|
@Controller()
|
|
517
723
|
export class ${t}Controller {
|
|
518
724
|
@Get('/')
|
|
@@ -530,7 +736,7 @@ export class ${t}Controller {
|
|
|
530
736
|
return reply.created({ data: ctx.body })
|
|
531
737
|
}
|
|
532
738
|
|
|
533
|
-
@
|
|
739
|
+
@Patch('/:id')
|
|
534
740
|
async update(ctx: Ctx<KickRoutes.${t}Controller['update']>) {
|
|
535
741
|
return { id: ctx.params.id, data: ctx.body }
|
|
536
742
|
}
|
|
@@ -540,7 +746,7 @@ export class ${t}Controller {
|
|
|
540
746
|
return reply.noContent()
|
|
541
747
|
}
|
|
542
748
|
}
|
|
543
|
-
`)}async function
|
|
749
|
+
`)}async function me(e){let{pascal:t,kebab:n,plural:r,pluralPascal:i,routePath:a,repo:o,noTests:s,tokenScope:c,style:l,testHarness:u,write:d}=e,f=e.swagger??!1,p=e.schemaLib??`zod`;await d(`${n}.module.ts`,M({pascal:t,kebab:n,plural:r,routePath:a,repo:o,style:l})),await d(`${n}.constants.ts`,R({pascal:t,kebab:n})),await d(`${n}.controller.ts`,N({pascal:t,kebab:n,plural:r,pluralPascal:i,swagger:f})),await d(`${n}.service.ts`,L({pascal:t,kebab:n})),await d(`dtos/create-${n}.dto.ts`,oe({pascal:t,kebab:n,schemaLib:p})),await d(`dtos/update-${n}.dto.ts`,se({pascal:t,kebab:n,schemaLib:p})),await d(`dtos/${n}-response.dto.ts`,ce({pascal:t,kebab:n})),await d(`${n}.repository.ts`,I({pascal:t,kebab:n,repoType:o,dtoPrefix:`./dtos`,tokenScope:c})),s||(await d(`__tests__/${n}.controller.test.ts`,le({pascal:t,kebab:n,plural:r,routePath:a,style:l,testHarness:u})),await d(`__tests__/${n}.repository.test.ts`,ue({pascal:t,kebab:n,plural:r,repoPrefix:`../${n}.repository`})))}function he(e){return e?typeof e==`string`?e:e.name:`inmemory`}async function ge(n){let{name:r,modulesDir:i,noEntity:c,noTests:l,repo:u=`inmemory`,force:d,dryRun:f}=n,p=n.pluralize===!0,m=n.pattern??`rest`;n.minimal&&(m=`minimal`);let g=D(r),_=T(r),v=p?O(g):g,y=p?k(_):_,b=n.pluralizeRoutes===!1?g:O(g),x=h(i,v),S=[],C=d??!1,w={kebab:g,pascal:_,plural:v,pluralPascal:y,routePath:b,moduleDir:x,repo:u,noEntity:c??!1,noTests:l??!1,tokenScope:n.tokenScope??`app`,swagger:n.swagger??!1,schemaLib:n.schemaLib??`zod`,testHarness:n.testHarness??!1,style:n.style??`define`,write:async(n,r)=>{let i=h(x,n);f?S.push(i):!C&&await e(i)&&!await o({message:`File exists: ${s.dim(n)}. Overwrite?`,initialValue:!1})?a.warn(`Skipped: ${n}`):(await t(i,r),S.push(i))},files:S};switch(m){case`minimal`:await pe(w);break;default:await me(w)}return f||await z(i,_,v,g,w.style),S}async function z(n,r,i,a,o=`define`){let s=h(n,`index.ts`),c=await e(s),l=`./${i}/${a}.module`,u=o===`class`?`${r}Module`:`${r}Module()`;if(!c){let e=o===`class`?`import type { AppModuleEntry } from '@forinda/kickjs'
|
|
544
750
|
import { ${r}Module } from '${l}'
|
|
545
751
|
|
|
546
752
|
export const modules: AppModuleEntry[] = [${u}]
|
|
@@ -551,8 +757,8 @@ export const modules = defineModules().mount(${u})
|
|
|
551
757
|
`;await t(s,e);return}let d=await S(s,`utf-8`),f=`import { ${r}Module } from '${l}'`,p=A(l);if(!RegExp(`^import\\s*\\{[^}]*\\b${A(r)}Module\\b[^}]*\\}\\s*from\\s*['"]${p}['"]`,`m`).test(d)){let e=d.lastIndexOf(`import `);if(e!==-1){let t=d.indexOf(`
|
|
552
758
|
`,e);d=d.slice(0,t+1)+f+`
|
|
553
759
|
`+d.slice(t+1)}else d=f+`
|
|
554
|
-
`+d}let m=V(d);if(m){let e=d.slice(m.rhsStart,m.rhsEnd+1);RegExp(`\\b${A(r)}Module\\b`).test(e)||(d=B(d,u))}else d=B(d,u);await
|
|
555
|
-
`;)t++;return t}if(n===`/*`){for(t+=2;t+1<e.length&&(e[t]!==`*`||e[t+1]!==`/`);)t++;return t+2}return t}function
|
|
760
|
+
`+d}let m=V(d);if(m){let e=d.slice(m.rhsStart,m.rhsEnd+1);RegExp(`\\b${A(r)}Module\\b`).test(e)||(d=B(d,u))}else d=B(d,u);await t(s,d)}function B(e,t){let n=V(e);if(!n)return e;if(n.shape===`array`){let r=e.slice(n.rhsStart+1,n.rhsEnd),i=r.trim(),a;if(!i)a=`[${t}]`;else{let e=i.endsWith(`,`)?``:`,`;a=`[${r.trimEnd()}${e} ${t}]`}return e.slice(0,n.rhsStart)+a+e.slice(n.rhsEnd+1)}return`${e.slice(0,n.chainEnd)}\n .mount(${t})${e.slice(n.chainEnd)}`}function V(e){let t=/export\s+const\s+modules\b[^=]*=/.exec(e);if(!t)return null;let n=t.index+t[0].length;for(;n<e.length&&/\s/.test(e[n]??``);)n++;if(e[n]===`[`){let t=ve(e,n);return t===-1?null:{shape:`array`,rhsStart:n,rhsEnd:t}}if(e.slice(n,n+13)===`defineModules`){let t=_e(e,n);return t===-1?null:{shape:`chain`,rhsStart:n,rhsEnd:t-1,chainEnd:t}}return null}function _e(e,t=0){let n=/defineModules\s*\(/g;n.lastIndex=t;let r=n.exec(e);if(!r)return-1;let i=r.index+r[0].length-1;if(e[i]!==`(`||(i=U(e,i),i===-1))return-1;for(i++;;){let t=i;for(;t<e.length&&/\s/.test(e[t]??``);)t++;if(e[t]!==`.`||e.slice(t,t+6)!==`.mount`)break;for(t+=6;t<e.length&&/\s/.test(e[t]??``);)t++;if(e[t]!==`(`)break;let n=U(e,t);if(n===-1)break;i=n+1}return i}function H(e,t){let n=e.slice(t,t+2);if(n===`//`){for(t+=2;t<e.length&&e[t]!==`
|
|
761
|
+
`;)t++;return t}if(n===`/*`){for(t+=2;t+1<e.length&&(e[t]!==`*`||e[t+1]!==`/`);)t++;return t+2}return t}function ve(e,t){if(e[t]!==`[`)return-1;let n=1,r=t+1;for(;r<e.length;){let t=e.slice(r,r+2);if(t===`//`||t===`/*`){r=H(e,r);continue}let i=e[r]??``;if(i===`'`||i===`"`||i==="`"){let t=i;for(r++;r<e.length&&e[r]!==t;)e[r]===`\\`&&r++,r++;r<e.length&&r++}else{if(i===`[`)n++;else if(i===`]`&&(n--,n===0))return r;r++}}return-1}function U(e,t){if(e[t]!==`(`)return-1;let n=1,r=t+1;for(;r<e.length;){let t=e.slice(r,r+2);if(t===`//`||t===`/*`){r=H(e,r);continue}let i=e[r]??``;if(i===`'`||i===`"`||i==="`"){let t=i;for(r++;r<e.length&&e[r]!==t;)e[r]===`\\`&&r++,r++;r<e.length&&r++}else{if(i===`(`)n++;else if(i===`)`&&(n--,n===0))return r;r++}}return-1}function W(e,t,n,r){return`/**
|
|
556
762
|
* Snapshot for the DevTools topology view (\`/_debug\`).
|
|
557
763
|
*
|
|
558
764
|
* Must be CHEAP — the topology endpoint polls on a short interval, so
|
|
@@ -592,7 +798,7 @@ export const modules = defineModules().mount(${u})
|
|
|
592
798
|
// ]
|
|
593
799
|
// },`.split(`
|
|
594
800
|
`).map(e=>r+e).join(`
|
|
595
|
-
`)}async function
|
|
801
|
+
`)}async function ye(e){let{name:n,outDir:r}=e,i=D(n),a=T(n),o=[],s=h(r,`${i}.adapter.ts`);return await t(s,`import {
|
|
596
802
|
defineAdapter,
|
|
597
803
|
type AdapterContext,
|
|
598
804
|
type AdapterMiddleware,
|
|
@@ -801,8 +1007,8 @@ ${W(i,a,`adapter`,` `)}
|
|
|
801
1007
|
}
|
|
802
1008
|
},
|
|
803
1009
|
})
|
|
804
|
-
`),o.push(s),o}const
|
|
805
|
-
// \`express\``:"this project declares no `runtime` in kick.config.ts, so it\n // may have no `express`",c=G({type:`middleware`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/middleware`,pattern:a,shouldPluralize:e.pluralize??!
|
|
1010
|
+
`),o.push(s),o}const be={controller:``,service:``,dto:`dtos`,guard:`guards`,middleware:`middleware`,contributor:`contributors`};function G(e){let{type:t,outDir:n,moduleName:r,modulesDir:i=`src/modules`,defaultDir:a,shouldPluralize:o=!1}=e;if(n)return _(n);if(r){let e=be,n=D(r),a=o?O(n):n,s=e[t]??``,c=h(i,a);return _(s?h(c,s):c)}return _(a)}async function xe(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=e.runtime===`express`,s=e.runtime?`this project runs on ${e.runtime} and has no
|
|
1011
|
+
// \`express\``:"this project declares no `runtime` in kick.config.ts, so it\n // may have no `express`",c=G({type:`middleware`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/middleware`,pattern:a,shouldPluralize:e.pluralize??!1}),l=D(n),u=E(n),d=[],f=h(c,`${l}.middleware.ts`);return await t(f,`${o?`import type { Request, Response, NextFunction } from 'express'`:`import type { IncomingMessage, ServerResponse } from 'node:http'`}
|
|
806
1012
|
|
|
807
1013
|
export interface ${T(n)}Options {
|
|
808
1014
|
// Add configuration options here. The factory below closes over the
|
|
@@ -861,7 +1067,7 @@ ${o?" // Typed from `express` because that is this project's runtime\n // (`ru
|
|
|
861
1067
|
next()
|
|
862
1068
|
}
|
|
863
1069
|
}
|
|
864
|
-
`),d.push(f),d}async function
|
|
1070
|
+
`),d.push(f),d}async function Se(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`guard`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/guards`,pattern:a,shouldPluralize:e.pluralize??!1}),s=D(n),c=E(n),l=T(n),u=[],d=h(o,`${s}.guard.ts`);return await t(d,`import { Container, HttpException } from '@forinda/kickjs'
|
|
865
1071
|
import type { RequestContext } from '@forinda/kickjs'
|
|
866
1072
|
|
|
867
1073
|
/**
|
|
@@ -901,7 +1107,7 @@ export async function ${c}Guard(ctx: RequestContext, next: () => void): Promise<
|
|
|
901
1107
|
ctx.problem.unauthorized({ detail: 'Invalid or expired token' })
|
|
902
1108
|
}
|
|
903
1109
|
}
|
|
904
|
-
`),u.push(d),u}async function
|
|
1110
|
+
`),u.push(d),u}async function Ce(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`service`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/services`,pattern:a,shouldPluralize:e.pluralize??!1}),s=D(n),c=T(n),l=[],u=h(o,`${s}.service.ts`);return await t(u,`import { Service } from '@forinda/kickjs'
|
|
905
1111
|
|
|
906
1112
|
@Service()
|
|
907
1113
|
export class ${c}Service {
|
|
@@ -910,7 +1116,7 @@ export class ${c}Service {
|
|
|
910
1116
|
// @Inject(MY_REPO) private readonly repo: IMyRepository,
|
|
911
1117
|
// ) {}
|
|
912
1118
|
}
|
|
913
|
-
`),l.push(u),l}async function
|
|
1119
|
+
`),l.push(u),l}async function we(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`controller`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/controllers`,pattern:a,shouldPluralize:e.pluralize??!1}),s=D(n),c=T(n),l=[],u=h(o,`${s}.controller.ts`);return await t(u,`import { Controller, Get, Post, Patch, Delete, reply, type Ctx } from '@forinda/kickjs'
|
|
914
1120
|
|
|
915
1121
|
// \`Ctx<KickRoutes.${c}Controller['<method>']>\` is generated by
|
|
916
1122
|
// \`kick typegen\` (auto-run on \`kick dev\`). After the first run, your IDE
|
|
@@ -923,6 +1129,7 @@ export class ${c}Service {
|
|
|
923
1129
|
//
|
|
924
1130
|
// The bodies below are placeholders: wire them to a service or repository.
|
|
925
1131
|
|
|
1132
|
+
// Require a signed-in user for these routes: https://kickjs.app/guide/authentication
|
|
926
1133
|
@Controller()
|
|
927
1134
|
export class ${c}Controller {
|
|
928
1135
|
// @Autowired() private readonly myService!: MyService
|
|
@@ -942,7 +1149,7 @@ export class ${c}Controller {
|
|
|
942
1149
|
return reply.created({ data: ctx.body })
|
|
943
1150
|
}
|
|
944
1151
|
|
|
945
|
-
@
|
|
1152
|
+
@Patch('/:id')
|
|
946
1153
|
async update(ctx: Ctx<KickRoutes.${c}Controller['update']>) {
|
|
947
1154
|
return { id: ctx.params.id, data: ctx.body }
|
|
948
1155
|
}
|
|
@@ -952,21 +1159,21 @@ export class ${c}Controller {
|
|
|
952
1159
|
return reply.noContent()
|
|
953
1160
|
}
|
|
954
1161
|
}
|
|
955
|
-
`),l.push(u),l}async function
|
|
1162
|
+
`),l.push(u),l}async function Te(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`dto`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/dtos`,pattern:a,shouldPluralize:e.pluralize??!1}),s=D(n),c=T(n),l=E(n),u=e.schemaLib??`zod`,d=P[u],f=[],p=h(o,`${s}.dto.ts`);return await t(p,`${d.import}
|
|
956
1163
|
|
|
957
1164
|
export const ${l}Schema = ${F(u,`// Define your schema fields here\n name: ${d.required}`)}
|
|
958
1165
|
|
|
959
1166
|
export type ${c}DTO = ${d.infer(`${l}Schema`)}
|
|
960
|
-
`),f.push(p),f}function
|
|
961
|
-
`);super(`kick typegen --check: ${e.length} generated file(s) are out of date:\n${t}\n Run \`kick typegen\` and commit the result.`),this.name=`TypegenDriftError`,this.drifted=e}};function
|
|
962
|
-
Install a supported version via nvm / fnm / volta.`}:{name:`Node version`,status:`pass`,message:e}}function
|
|
1167
|
+
`),f.push(p),f}function Ee(){let e=(process.env.LOG_LEVEL??process.env.KICKJS_LOG_LEVEL??``).toLowerCase();return e===`debug`||e===`trace`}var De=class extends Error{drifted;constructor(e){let t=e.map(e=>` ${e.id} → ${e.outFile}`).join(`
|
|
1168
|
+
`);super(`kick typegen --check: ${e.length} generated file(s) are out of date:\n${t}\n Run \`kick typegen\` and commit the result.`),this.name=`TypegenDriftError`,this.drifted=e}};function Oe(e){return e}function ke(e){let t=/* @__PURE__ */ new Map;return{report(n,r){let i=r instanceof Error?r.message:String(r);t.get(n)!==i&&(t.set(n,i),e(` kick typegen: ${n} pass failed (${i}) — types in .kickjs/types may be stale`))},clear(e){t.delete(e)}}}const Ae=`__kickjs_typegen_owner`;function je(e){let{cwd:t,config:n}=e,r=e.debounceMs??100,i=e.pipeline??{runTypegen:async e=>(await import(`./typegen-9tepCLIZ.mjs`).then(e=>e.n)).runTypegen(e),runAllPluginTypegens:async e=>(await import(`./run-plugins-Du6FlB7_.mjs`).then(e=>e.n)).runAllPluginTypegens(e),writeTypegenArtifacts:async(e,t,n)=>(await import(`./typegen-9tepCLIZ.mjs`).then(e=>e.n)).writeTypegenArtifacts(e,t,n),buildAssets:async(e,t)=>(await import(`./build-BQiF63pu.mjs`).then(e=>e.n)).buildAssets(e,t)},a=n?.typegen?.schemaValidator??`zod`,o=n?.typegen?.envFile,s=p.resolve(t,n?.typegen?.outDir??`.kickjs/types`),c=n?.assetMap?Object.values(n.assetMap).map(e=>e?.src).filter(e=>typeof e==`string`&&e.length>0).map(e=>p.resolve(t,e)):[],l=!!n?.assetMap&&Object.keys(n.assetMap).length>0,u=e=>e.replaceAll(`\\`,`/`),d=c.map(u),f=e=>{let t=u(e);return d.some(e=>t===e||t.startsWith(`${e}/`))},m=ke(e.emitWarning),h=null,g=!1,_=/* @__PURE__ */ new Set,v=/* @__PURE__ */ new Set,y=!1,b=!1;function x(r,c){i.runTypegen({cwd:t,silent:!0,allowDuplicates:!0,schemaValidator:a,envFile:o,srcDir:n?.typegen?.srcDir,outDir:n?.typegen?.outDir,assetMap:n?.assetMap,changedFiles:r,runPlugins:!1}).then(()=>m.clear(`scan`)).catch(e=>m.report(`scan`,e)),i.runAllPluginTypegens({cwd:t,config:n,silent:!0,changedFiles:r,watch:!0}).then(e=>i.writeTypegenArtifacts(s,e,!0)).then(()=>m.clear(`plugins`)).catch(e=>m.report(`plugins`,e)).finally(()=>e.onPassComplete?.()),c&&n&&i.buildAssets(n,{cwd:t,silent:!0}).catch(()=>{})}function S(){let e=y?void 0:{changed:[..._],removed:[...v]},t=b;_.clear(),v.clear(),y=!1,b=!1,x(e,t)}return{assetSrcRoots:c,handleWatchEvent(e,t){if(!g&&!u(t).includes(`/.kickjs/`)){if(e===`unlinkDir`)y=!0,l&&(b=!0);else{if(t.endsWith(`.d.ts`))return;let n=/\.(ts|tsx|mts|cts)$/.test(t),r=f(t);if(!n&&!r)return;r&&l&&(b=!0),n&&(e===`unlink`?(v.add(t),_.delete(t)):(_.add(t),v.delete(t)))}h&&clearTimeout(h),h=setTimeout(S,r)}},runOnce(){g||x(void 0,l)},dispose(){g=!0,h&&clearTimeout(h),h=null}}}function Me(e){return D(e).replace(/-/g,`_`)}function K(e){let t=e.cwd??process.cwd(),n=e.projectRoot??d(t),r=e.pluralize??!1,i=T(e.name),a=E(e.name),o=D(e.name),s=Me(e.name),c={name:e.name,pascal:i,camel:a,kebab:o,snake:s,modulesDir:e.modulesDir??`src/modules`,cwd:t,projectRoot:n,args:e.args??[],flags:e.flags??{}};if(r){let e=O(o);c.pluralKebab=e,c.pluralPascal=T(e),c.pluralCamel=E(e)}return c}function Ne(e,t){return _(e.cwd,t)}async function Pe(e){return import(w(e).href)}const q=/* @__PURE__ */ new Map;async function J(e){let t=q.get(e);if(t)return t;let n=Fe(e);return q.set(e,n),n}async function Fe(e){let t=_(e,`package.json`);if(!v(t))return{generators:[],loaded:[],failed:[]};let n=Ie(JSON.parse(await S(t,`utf-8`))),r=f(_(e,`package.json`)),i=[],a=[],o=[];for(let e of n){let t;try{t=r.resolve(`${e}/package.json`)}catch{continue}let n;try{n=JSON.parse(await S(t,`utf-8`))}catch(t){o.push({source:e,reason:`failed to parse package.json: ${t}`});continue}if(!n.kickjs?.generators)continue;let s=n.kickjs.generators,c=_(m(t),s);if(!v(c)){o.push({source:e,reason:`kickjs.generators points to missing file: ${s}`});continue}let l;try{l=await Pe(c)}catch(t){o.push({source:e,reason:`failed to import manifest: ${t}`});continue}let u=l.default;if(!Array.isArray(u))o.push({source:e,reason:`manifest's default export is not an array of GeneratorSpec`});else{for(let t of u)Le(t)?i.push({source:e,spec:t}):o.push({source:e,reason:`manifest entry is not a valid GeneratorSpec (missing name/files)`});a.push(e)}}return{generators:i,loaded:a,failed:o}}function Ie(e){let t=/* @__PURE__ */ new Set;for(let n of[e.dependencies,e.devDependencies,e.peerDependencies])if(n)for(let e of Object.keys(n))t.add(e);return Array.from(t)}function Le(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.name==`string`&&typeof t.files==`function`}async function Re(e,t=[]){let n=e.cwd??process.cwd(),r=t.find(t=>t.spec.name===e.generatorName);if(r)return Y(r.spec,r.source,e,n);let i=Be(await J(n),e.generatorName);return i?Y(i.spec,i.source,e,n):null}async function ze(e,t=[]){let n=await J(e),r=new Set(t.map(e=>e.spec.name)),i=n.generators.filter(e=>!r.has(e.spec.name));return{generators:[...t,...i],loaded:n.loaded,failed:n.failed}}function Be(e,t){return e.generators.find(e=>e.spec.name===t)}async function Y(e,n,r,i){let a=K({name:r.itemName,args:r.args,flags:r.flags,modulesDir:r.modulesDir,pluralize:r.pluralize,cwd:i,projectRoot:r.projectRoot}),o=await e.files(a),s=[];for(let e of o){let n=Ne(a,e.path);await t(n,e.content),s.push(n)}return{files:s,source:n}}function Ve(e){return e}function He(e){return e}function Ue(e){try{return JSON.parse(y(e,`utf-8`))}catch{return null}}function X(e){try{return y(e,`utf-8`)}catch{return null}}function We(e){let t=e.replace(/\/\*[\s\S]*?\*\//g,``).replace(/(^|[^:"'\\])\/\/.*$/gm,`$1`).replace(/,(\s*[}\]])/g,`$1`);try{return JSON.parse(t)}catch{return null}}function Ge(e){let t=h(e,`tsconfig.json`);if(v(t))return Z(t,/* @__PURE__ */ new Set)}function Z(e,t){if(t.has(e)||t.size>=16)return null;t.add(e);let n=X(e);if(n===null)return null;let r=We(n);if(typeof r!=`object`||!r)return null;let i=Array.isArray(r.extends)?r.extends:typeof r.extends==`string`?[r.extends]:[],a={};for(let n of i){if(typeof n!=`string`)continue;let r=Ke(m(e),n);if(!r)continue;let i=Z(r,t);i&&Object.assign(a,i.compilerOptions)}return r.compilerOptions={...a,...r.compilerOptions},r}function Ke(e,t){if(t.startsWith(`.`)||t.startsWith(`/`))return Q(_(e,t));let n=e;for(;;){let e=Q(h(n,`node_modules`,t));if(e)return e;let r=m(n);if(r===n)break;n=r}return null}function Q(e){if(v(e)){try{if(x(e).isDirectory()){let t=h(e,`tsconfig.json`);return v(t)?t:null}}catch{return null}return e}let t=`${e}.json`;return v(t)?t:null}function qe(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function Je(){let e=process.version,t=Number.parseInt(e.replace(/^v/,``).split(`.`)[0],10);return Number.isNaN(t)||t<20?{name:`Node version`,status:`fail`,message:e,fix:`KickJS requires Node 20 or newer.
|
|
1169
|
+
Install a supported version via nvm / fnm / volta.`}:{name:`Node version`,status:`pass`,message:e}}function Ye(e){if(!e.pkg)return{name:`@forinda/kickjs installed`,status:`warn`,message:`no package.json`};let t={...e.pkg.dependencies,...e.pkg.peerDependencies};return t[`@forinda/kickjs`]?{name:`@forinda/kickjs installed`,status:`pass`,message:t[`@forinda/kickjs`]}:{name:`@forinda/kickjs installed`,status:`fail`,fix:"This directory does not look like a KickJS project — `@forinda/kickjs` is not in your package.json. Run `kick doctor` from the project root, or scaffold a fresh project with `kick new <name>`."}}function Xe(e){if(!e.pkg)return null;let t={...e.pkg.dependencies,...e.pkg.peerDependencies};return t[`@forinda/kickjs`]&&!t.express?{name:`express installed`,status:`fail`,fix:`\`@forinda/kickjs\` declares \`express\` as a required peer dependency, but your package.json does not include it. Install: ${n(e.cwd)} add express`}:t.express?{name:`express installed`,status:`pass`,message:t.express}:null}const Ze={express:[],fastify:[`fastify`,`@fastify/middie`],h3:[`h3`]};function Qe(e){if(!e.pkg||e.runtime===`express`)return null;let t={...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies},r=Ze[e.runtime].filter(e=>!t[e]),i=`runtime engine (${e.runtime})`;return r.length>0?{name:i,status:`fail`,fix:`Resolved runtime '${e.runtime}' is missing engine peer(s): ${r.join(`, `)}.\nInstall: ${n(e.cwd)} add ${r.join(` `)}`}:e.runtime===`h3`&&$e(e.cwd,t.h3)===1?{name:i,status:`fail`,message:`h3 v1`,fix:`This version of @forinda/kickjs runs on h3 v2 only. Upgrade: ${n(e.cwd)} add h3@^2 — no code change for apps that only use KickJS APIs.`}:{name:i,status:`pass`}}function $e(e,t){let n=t;for(let t=_(e);;t=m(t)){try{n=JSON.parse(y(h(t,`node_modules`,`h3`,`package.json`),`utf8`)).version;break}catch{}if(m(t)===t)break}let r=/(\d+)/.exec(n)?.[1];return r===void 0?void 0:Number(r)}function et(e){if(!e.pkg||!tt(e.cwd))return null;let t=u[e.runtime],r=`upload driver (${e.runtime})`;return t.prod?{...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies}[t.prod]?{name:r,status:`pass`,message:t.prod}:{name:r,status:`fail`,fix:`This project uses file uploads on the '${e.runtime}' runtime, which needs '${t.prod}'.\nInstall it: kick add upload (or ${n(e.cwd)} add ${t.prod})`}:{name:r,status:`pass`,message:`native multipart`}}const $=2e3;function tt(e){let t=h(e,`src`);if(!v(t))return!1;let n=/@FileUpload\b|\bupload\.(single|array|none)\s*\(/,r=[t],i=0;for(;r.length>0&&i<$;){let e=r.pop(),t;try{t=b(e,{withFileTypes:!0})}catch{continue}for(let a of t){if(i>=$)break;let t=h(e,a.name);if(a.isDirectory())a.name!==`node_modules`&&r.push(t);else if(/\.(ts|tsx|mts|cts)$/.test(a.name)&&(i++,n.test(X(t)??``)))return!0}}return!1}function nt(e){if(!e.pkg)return{name:`reflect-metadata installed`,status:`warn`,message:`no package.json`};let t={...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies};return t[`reflect-metadata`]?{name:`reflect-metadata installed`,status:`pass`,message:t[`reflect-metadata`]}:{name:`reflect-metadata installed`,status:`fail`,fix:`KickJS decorators require the reflect-metadata polyfill.\nInstall it: ${n(e.cwd)} add reflect-metadata\nThen import it at the top of src/index.ts:\n\n import 'reflect-metadata'\n // ... rest of bootstrap`}}function rt(e){if(e.tsconfig===void 0)return[{name:`tsconfig.json present`,status:`fail`,fix:"Create a tsconfig.json with `experimentalDecorators: true` and `emitDecoratorMetadata: true`. `kick new` scaffolds one automatically."}];if(e.tsconfig===null)return[{name:`tsconfig.json readable`,status:`warn`,message:`could not parse tsconfig.json (or a config it extends)`,fix:"Check tsconfig.json for a syntax error, and that every path in its `extends` chain resolves. Decorator options could not be verified."}];let t=e.tsconfig.compilerOptions??{},n=[];return n.push(t.experimentalDecorators===!0?{name:`tsconfig: experimentalDecorators`,status:`pass`}:{name:`tsconfig: experimentalDecorators`,status:`fail`,fix:'Add `"experimentalDecorators": true` to compilerOptions in tsconfig.json. Without it, @Service / @Controller / @Get etc. don\'t register any metadata at compile time.'}),n.push(t.emitDecoratorMetadata===!0?{name:`tsconfig: emitDecoratorMetadata`,status:`pass`}:{name:`tsconfig: emitDecoratorMetadata`,status:`fail`,fix:'Add `"emitDecoratorMetadata": true` to compilerOptions in tsconfig.json. The DI container uses this metadata for constructor-parameter injection.'}),n}function it(e){let t=[`src/env.ts`,`src/env/index.ts`,`src/config/env.ts`,`src/config/index.ts`].map(t=>h(e.cwd,t)).filter(e=>v(e)).filter(e=>/\bloadEnv\s*\(/.test(X(e)??``));if(t.length===0)return null;let n=[`src/index.ts`,`src/main.ts`].map(t=>h(e.cwd,t)).find(e=>v(e));if(!n)return{name:`env wiring`,status:`warn`,message:`env-init file exists but no src/index.ts or src/main.ts found`};let r=X(n)??``,i=m(n),a=[];for(let e of t){let t=g(i,e).replace(/\\/g,`/`).replace(/\.ts$/,``),n=t.startsWith(`.`)?t:`./`+t,r=n.replace(/\/index$/,``);a.push(n,r);let o=e.replace(/\\/g,`/`).match(/\/src\/(.+?)(?:\.ts)?$/);if(o){let e=`@/`+o[1],t=e.replace(/\/index$/,``);a.push(e,t)}}let o=-1;for(let e of new Set(a)){let t=RegExp(`^import\\s+(?:.*?from\\s+)?['"]${qe(e)}['"]`,`m`),n=r.match(t);n&&n.index!==void 0&&(o===-1||n.index<o)&&(o=n.index)}let s=r.search(/\bbootstrap\s*\(/),c=t.map(t=>g(e.cwd,t).replace(/\\/g,`/`)).join(`, `);return o===-1?{name:`env wiring`,status:`fail`,message:c,fix:`An env-init file (${c}) calls \`loadEnv(...)\` but \`${g(e.cwd,n).replace(/\\/g,`/`)}\` doesn't import it.\nWithout this, ConfigService.get('X') returns undefined while @Value('X') works via process.env fallback — a half-broken config you won't notice until something is missing.\n\nFix: add a side-effect import at the top of ${g(e.cwd,n).replace(/\\/g,`/`)} (above bootstrap()), pointing at one of the detected files. For example:\n\n import './env'\n // or\n import './config'\n // or, with the @/ alias:\n import '@/config/env'`}:s!==-1&&o>s?{name:`env wiring`,status:`warn`,message:`env-init imported AFTER bootstrap() — should be before`,fix:`Move the env import above the bootstrap() call so the schema runs before any service reads from ConfigService.`}:{name:`env wiring`,status:`pass`}}function at(e,t=ot){let n=0,r=0,i=[e];for(;i.length>0&&r<t;){let e=i.pop(),a;try{a=b(e,{withFileTypes:!0})}catch{continue}for(let o of a){if(r>=t)break;r++;let a=h(e,o.name);if(o.isDirectory())i.push(a);else try{let e=x(a).mtimeMs;e>n&&(n=e)}catch{}}}return n}const ot=2e3;function st(e){let t=h(e.cwd,`.kickjs`,`types`);if(!v(t))return null;let n=at(t);if(n===0)return null;let r=Date.now()-n,i=Math.floor(r/6e4);return i>60?{name:`typegen freshness`,status:`warn`,message:`last updated ${i} minutes ago`,fix:"Re-run `kick typegen` (or `kick dev`, which runs it on every reload) so generated types match the current code."}:{name:`typegen freshness`,status:`pass`,message:i===0?`just now`:`${i}m ago`}}const ct=[`vitest.config.ts`,`vitest.config.js`,`vitest.config.mts`,`vite.config.ts`,`jest.config.ts`,`jest.config.js`],lt=[`.env.test`,`.env.test.local`],ut=[`.env`,`.env.local`];function dt(e){let t=e.pkg?.scripts;if(t&&typeof t==`object`)for(let[e,n]of Object.entries(t)){if(!e.startsWith(`test`)||typeof n!=`string`)continue;let t=/KICKJS_ENV_FILE=(\S+)/.exec(n);if(t?.[1])return t[1]}return process.env.KICKJS_ENV_FILE??null}function ft(e){if(!ct.some(t=>v(h(e.cwd,t))))return null;let t=ut.filter(t=>v(h(e.cwd,t)));if(t.length===0)return null;let n=lt.filter(t=>v(h(e.cwd,t)));if(n.length>0){let t=dt(e);return t&&t!==`off`&&t!==`none`?{name:`test env isolation`,status:`warn`,message:`${n.join(`, `)} present, but KICKJS_ENV_FILE=${t} overrides it`,fix:`KICKJS_ENV_FILE is checked before test-mode isolation, so ${n[0]} is\nignored and "${t}" is read instead. Drop the override to use the\ntest files, or set KICKJS_ENV_FILE=off if env comes from the runner.`}:{name:`test env isolation`,status:`pass`,message:`${n.join(`, `)} present`}}return{name:`test env isolation`,status:`warn`,message:`${t.join(`, `)} present, no .env.test`,fix:`Under NODE_ENV=test (or vitest's VITEST) KickJS reads .env.test / .env.test.local
|
|
963
1170
|
INSTEAD of ${t.join(` and `)} — no layering, no fallback. Without one of\nthose files, every var your test config does not pin resolves from your
|
|
964
1171
|
development env — including DATABASE_URL and any third-party credentials.
|
|
965
1172
|
|
|
966
1173
|
Create .env.test (cp .env.test.example .env.test, if the project has one)
|
|
967
1174
|
holding the whole environment the suite should run against
|
|
968
1175
|
(a var you leave out is then missing, not inherited), or set KICKJS_ENV_FILE=off
|
|
969
|
-
and supply env from the test runner.`}}const
|
|
1176
|
+
and supply env from the test runner.`}}const pt=[()=>Je(),Ye,Xe,Qe,et,nt,rt,it,ft,st];async function mt(e,t={}){let n={cwd:e,pkg:Ue(h(e,`package.json`)),tsconfig:Ge(e),runtime:t.runtime??`express`},r=[...pt,...t.extraChecks??[]],i=[];for(let e of r){let t;try{t=await e(n)}catch(t){i.push({name:e.name||`doctor check`,status:`fail`,message:t instanceof Error?t.message:String(t)});continue}t!=null&&(Array.isArray(t)?i.push(...t):i.push(t))}return i}function ht(e){switch(e){case`pass`:return s.green(`✔`);case`warn`:return s.yellow(`⚠`);case`fail`:return s.red(`✖`)}}function gt(e){let t=ht(e.status),n=e.message?` ${s.dim(`(${e.message})`)}`:``;return`${t} ${e.name}${n}`}function _t(e){return e.split(`
|
|
970
1177
|
`).map(e=>` ${s.dim(`→`)} ${e}`).join(`
|
|
971
|
-
`)}function
|
|
972
|
-
//# sourceMappingURL=doctor-
|
|
1178
|
+
`)}function vt(e){return e?.doctor?.checks??[]}function yt(e){e.command(`doctor`).description(`Pre-flight checks for your KickJS project (dev environment health)`).action(async()=>{let e=process.cwd(),t=vt(await c(e)),n=await l(e);i(`KickJS Doctor`);let o=await mt(e,{extraChecks:t,runtime:n});for(let e of o)a.message(gt(e)),e.fix&&e.status!==`pass`&&a.message(_t(e.fix));let u=o.filter(e=>e.status===`pass`).length,d=o.filter(e=>e.status===`warn`).length,f=o.filter(e=>e.status===`fail`).length,p=[s.green(`${u} passed`),d>0?s.yellow(`${d} warning${d===1?``:`s`}`):`${d} warnings`,f>0?s.red(`${f} error${f===1?``:`s`}`):`${f} errors`].join(`, `);f>0?(r(`${p} — fix the errors above before running the app`),process.exit(1)):r(d>0?`${p} — review the warnings`:s.green(`${p} — your environment looks good`))})}export{A,ge as C,I as D,L as E,T as F,k as M,E as N,N as O,D as P,V as S,R as T,xe as _,Re as a,W as b,Ae as c,Oe as d,Ee as f,Se as g,Ce as h,ze as i,O as j,M as k,je as l,we as m,Ve as n,K as o,Te as p,yt as r,ee as s,He as t,De as u,G as v,he as w,z as x,ye as y};
|
|
1179
|
+
//# sourceMappingURL=doctor-CYnItgYt.mjs.map
|