@forinda/kickjs-cli 2.2.1 → 2.2.3
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 +138 -48
- package/dist/index.mjs +99 -27
- package/dist/index.mjs.map +1 -1
- package/dist/{typegen-UejiKdXA.mjs → typegen-BncsvEr-.mjs} +40 -22
- package/dist/typegen-BncsvEr-.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/typegen-UejiKdXA.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli v2.2.
|
|
2
|
+
* @forinda/kickjs-cli v2.2.3
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -55,7 +55,7 @@ async function fileExists(filePath) {
|
|
|
55
55
|
function generatePackageJson(name, template, kickjsVersion) {
|
|
56
56
|
const baseDeps = {
|
|
57
57
|
"@forinda/kickjs": kickjsVersion,
|
|
58
|
-
"
|
|
58
|
+
dotenv: "^17.3.1",
|
|
59
59
|
express: "^5.1.0",
|
|
60
60
|
"reflect-metadata": "^0.2.2",
|
|
61
61
|
zod: "^4.3.6",
|
|
@@ -122,13 +122,16 @@ function generateViteConfig() {
|
|
|
122
122
|
return `import { defineConfig } from 'vite'
|
|
123
123
|
import { resolve } from 'node:path'
|
|
124
124
|
import swc from 'unplugin-swc'
|
|
125
|
-
import { kickjsVitePlugin } from '@forinda/kickjs-vite'
|
|
125
|
+
import { kickjsVitePlugin, envWatchPlugin } from '@forinda/kickjs-vite'
|
|
126
126
|
|
|
127
127
|
export default defineConfig({
|
|
128
128
|
oxc: false,
|
|
129
129
|
plugins: [
|
|
130
130
|
swc.vite(),
|
|
131
131
|
kickjsVitePlugin({ entry: 'src/index.ts' }),
|
|
132
|
+
// Watches .env files and triggers a full reload on change so the
|
|
133
|
+
// dev server picks up env tweaks without a manual restart.
|
|
134
|
+
envWatchPlugin(),
|
|
132
135
|
],
|
|
133
136
|
resolve: {
|
|
134
137
|
alias: {
|
|
@@ -278,6 +281,11 @@ export default defineConfig({
|
|
|
278
281
|
function generateEntryFile(name, template, version) {
|
|
279
282
|
switch (template) {
|
|
280
283
|
case "graphql": return `import 'reflect-metadata'
|
|
284
|
+
// Side-effect import — registers the extended env schema with kickjs
|
|
285
|
+
// **before** any controller / service / @Value gets resolved. Without
|
|
286
|
+
// this line ConfigService.get('YOUR_KEY') returns undefined because the
|
|
287
|
+
// cached schema would still be the base shape. See guide/configuration.
|
|
288
|
+
import './config'
|
|
281
289
|
import { bootstrap } from '@forinda/kickjs'
|
|
282
290
|
import { DevToolsAdapter } from '@forinda/kickjs-devtools'
|
|
283
291
|
import { GraphQLAdapter } from '@forinda/kickjs-graphql'
|
|
@@ -300,6 +308,11 @@ export const app = await bootstrap({
|
|
|
300
308
|
})
|
|
301
309
|
`;
|
|
302
310
|
case "cqrs": return `import 'reflect-metadata'
|
|
311
|
+
// Side-effect import — registers the extended env schema with kickjs
|
|
312
|
+
// **before** any controller / service / @Value gets resolved. Without
|
|
313
|
+
// this line ConfigService.get('YOUR_KEY') returns undefined because the
|
|
314
|
+
// cached schema would still be the base shape. See guide/configuration.
|
|
315
|
+
import './config'
|
|
303
316
|
import { bootstrap } from '@forinda/kickjs'
|
|
304
317
|
import { DevToolsAdapter } from '@forinda/kickjs-devtools'
|
|
305
318
|
import { SwaggerAdapter } from '@forinda/kickjs-swagger'
|
|
@@ -327,6 +340,11 @@ export const app = await bootstrap({
|
|
|
327
340
|
})
|
|
328
341
|
`;
|
|
329
342
|
case "minimal": return `import 'reflect-metadata'
|
|
343
|
+
// Side-effect import — registers the extended env schema with kickjs
|
|
344
|
+
// **before** any controller / service / @Value gets resolved. Without
|
|
345
|
+
// this line ConfigService.get('YOUR_KEY') returns undefined because the
|
|
346
|
+
// cached schema would still be the base shape. See guide/configuration.
|
|
347
|
+
import './config'
|
|
330
348
|
import { bootstrap } from '@forinda/kickjs'
|
|
331
349
|
import { modules } from './modules'
|
|
332
350
|
|
|
@@ -334,6 +352,11 @@ import { modules } from './modules'
|
|
|
334
352
|
export const app = await bootstrap({ modules })
|
|
335
353
|
`;
|
|
336
354
|
default: return `import 'reflect-metadata'
|
|
355
|
+
// Side-effect import — registers the extended env schema with kickjs
|
|
356
|
+
// **before** any controller / service / @Value gets resolved. Without
|
|
357
|
+
// this line ConfigService.get('YOUR_KEY') returns undefined because the
|
|
358
|
+
// cached schema would still be the base shape. See guide/configuration.
|
|
359
|
+
import './config'
|
|
337
360
|
import express from 'express'
|
|
338
361
|
import {
|
|
339
362
|
bootstrap,
|
|
@@ -376,10 +399,17 @@ export const modules: AppModuleClass[] = [HelloModule]
|
|
|
376
399
|
`;
|
|
377
400
|
}
|
|
378
401
|
/**
|
|
379
|
-
* Generate `src/
|
|
402
|
+
* Generate `src/config/index.ts` — the project's typed env schema.
|
|
380
403
|
*
|
|
381
404
|
* Default-exports a `defineEnv(...)` schema so `kick typegen` can
|
|
382
|
-
* infer it into the global `KickEnv` registry
|
|
405
|
+
* infer it into the global `KickEnv` registry, and *also* calls
|
|
406
|
+
* `loadEnv(envSchema)` as a module-load side effect so `ConfigService`
|
|
407
|
+
* and `@Value()` see the extended shape from the very first DI
|
|
408
|
+
* resolution. The companion `src/index.ts` template adds
|
|
409
|
+
* `import './config'` immediately after `reflect-metadata` so the
|
|
410
|
+
* registration runs before `bootstrap()` constructs anything.
|
|
411
|
+
*
|
|
412
|
+
* After typegen runs:
|
|
383
413
|
*
|
|
384
414
|
* @Value('DATABASE_URL') private url!: Env<'DATABASE_URL'>
|
|
385
415
|
* process.env.DATABASE_URL // typed as string
|
|
@@ -387,7 +417,7 @@ export const modules: AppModuleClass[] = [HelloModule]
|
|
|
387
417
|
* Both autocomplete and type-check at compile time.
|
|
388
418
|
*/
|
|
389
419
|
function generateEnvFile() {
|
|
390
|
-
return `import { defineEnv } from '@forinda/kickjs
|
|
420
|
+
return `import { defineEnv, loadEnv } from '@forinda/kickjs/config'
|
|
391
421
|
import { z } from 'zod'
|
|
392
422
|
|
|
393
423
|
/**
|
|
@@ -403,11 +433,25 @@ import { z } from 'zod'
|
|
|
403
433
|
* JWT_SECRET: z.string().min(32),
|
|
404
434
|
* REDIS_URL: z.string().url().optional(),
|
|
405
435
|
*/
|
|
406
|
-
|
|
436
|
+
const envSchema = defineEnv((base) =>
|
|
407
437
|
base.extend({
|
|
408
438
|
// DATABASE_URL: z.string().url(),
|
|
409
439
|
}),
|
|
410
440
|
)
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* IMPORTANT — side effect: register the schema with kickjs's env cache
|
|
444
|
+
* **at module-load time**. \`ConfigService\` and \`@Value()\` both consume
|
|
445
|
+
* this cache, and they will fall back to the base schema (or undefined)
|
|
446
|
+
* if no extended schema has been registered before they're resolved.
|
|
447
|
+
*
|
|
448
|
+
* As long as \`src/index.ts\` imports this file (\`import './env'\`) at the
|
|
449
|
+
* top — before \`bootstrap()\` runs — every controller and service in the
|
|
450
|
+
* app sees the typed extended values.
|
|
451
|
+
*/
|
|
452
|
+
export const env = loadEnv(envSchema)
|
|
453
|
+
|
|
454
|
+
export default envSchema
|
|
411
455
|
`;
|
|
412
456
|
}
|
|
413
457
|
/** Generate src/modules/hello/hello.service.ts */
|
|
@@ -535,11 +579,7 @@ function generateReadme(name, template, pm) {
|
|
|
535
579
|
cqrs: "CQRS + Event-Driven",
|
|
536
580
|
minimal: "Minimal"
|
|
537
581
|
};
|
|
538
|
-
const packages = [
|
|
539
|
-
"@forinda/kickjs",
|
|
540
|
-
"@forinda/kickjs-vite",
|
|
541
|
-
"@forinda/kickjs-config"
|
|
542
|
-
];
|
|
582
|
+
const packages = ["@forinda/kickjs", "@forinda/kickjs-vite"];
|
|
543
583
|
if (template !== "minimal") packages.push("@forinda/kickjs-swagger", "@forinda/kickjs-devtools");
|
|
544
584
|
if (template === "graphql") packages.push("@forinda/kickjs-graphql");
|
|
545
585
|
if (template === "cqrs") packages.push("@forinda/kickjs-queue", "@forinda/kickjs-ws", "@forinda/kickjs-otel");
|
|
@@ -775,10 +815,23 @@ kick add --list # Show all available packages
|
|
|
775
815
|
|
|
776
816
|
## Environment Configuration
|
|
777
817
|
|
|
778
|
-
|
|
818
|
+
The project's typed env schema lives in **\`src/config/index.ts\`** —
|
|
819
|
+
extend the base schema there with your application-specific keys, and
|
|
820
|
+
the schema is auto-registered with kickjs at module load. The companion
|
|
821
|
+
\`src/index.ts\` imports it as a side effect (\`import './config'\`) **before**
|
|
822
|
+
\`bootstrap()\` runs, so every \`@Service\`, \`@Controller\`, \`@Value\`, and
|
|
823
|
+
\`ConfigService\` resolution sees the validated extended values.
|
|
824
|
+
|
|
825
|
+
> **Do not delete \`import './config'\` from \`src/index.ts\`.** It is the
|
|
826
|
+
> registration step that wires \`ConfigService\` to your env schema.
|
|
827
|
+
> Without it, \`config.get('YOUR_KEY')\` returns \`undefined\` for every
|
|
828
|
+
> user-defined key and \`@Value('YOUR_KEY')\` only works because of a
|
|
829
|
+
> raw \`process.env\` fallback (Zod coercion + defaults are skipped).
|
|
830
|
+
|
|
831
|
+
Edit \`.env\` for variable values. Access them with \`@Value()\`:
|
|
779
832
|
|
|
780
833
|
\`\`\`ts
|
|
781
|
-
import { Value } from '@forinda/kickjs
|
|
834
|
+
import { Value } from '@forinda/kickjs'
|
|
782
835
|
|
|
783
836
|
@Service()
|
|
784
837
|
export class ApiService {
|
|
@@ -793,7 +846,7 @@ export class ApiService {
|
|
|
793
846
|
Or use \`ConfigService\`:
|
|
794
847
|
|
|
795
848
|
\`\`\`ts
|
|
796
|
-
import { ConfigService } from '@forinda/kickjs
|
|
849
|
+
import { Service, Autowired, ConfigService } from '@forinda/kickjs'
|
|
797
850
|
|
|
798
851
|
@Service()
|
|
799
852
|
export class AppService {
|
|
@@ -801,11 +854,16 @@ export class AppService {
|
|
|
801
854
|
private config!: ConfigService
|
|
802
855
|
|
|
803
856
|
getPort() {
|
|
804
|
-
|
|
857
|
+
// typed: number, Zod-coerced from baseEnvSchema
|
|
858
|
+
return this.config.get('PORT')
|
|
805
859
|
}
|
|
806
860
|
}
|
|
807
861
|
\`\`\`
|
|
808
862
|
|
|
863
|
+
Hot-reload of \`.env\` changes during dev is wired up automatically via
|
|
864
|
+
\`envWatchPlugin()\` in \`vite.config.ts\` — edit \`.env\`, the dev server
|
|
865
|
+
reloads, and the next \`config.get()\` re-parses with the new values.
|
|
866
|
+
|
|
809
867
|
## Testing
|
|
810
868
|
|
|
811
869
|
Tests live in \`src/**/*.test.ts\`:
|
|
@@ -867,6 +925,7 @@ ${template === "cqrs" ? `### CQRS/Event Decorators
|
|
|
867
925
|
3. **Always use \`ctx.body\`** — never \`req.body\` directly
|
|
868
926
|
4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
|
|
869
927
|
5. **Vite HMR requires proper cleanup** — adapters should implement \`shutdown()\`
|
|
928
|
+
6. **Never delete \`import './config'\` from \`src/index.ts\`** — that side-effect import registers the env schema with kickjs. Without it \`ConfigService.get('YOUR_KEY')\` returns \`undefined\` for every user-defined key. \`@Value('YOUR_KEY')\` *appears* to keep working but only via a raw \`process.env\` fallback (Zod coercion + schema defaults are silently skipped).
|
|
870
929
|
|
|
871
930
|
## Learn More
|
|
872
931
|
|
|
@@ -898,7 +957,8 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
|
|
|
898
957
|
| Entry point | \`src/index.ts\` |
|
|
899
958
|
| Module registry | \`src/modules/index.ts\` |
|
|
900
959
|
| Feature modules | \`src/modules/<module-name>/\` |
|
|
901
|
-
${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}|
|
|
960
|
+
${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Env values | \`.env\` |
|
|
961
|
+
| Env schema (Zod) | \`src/config/index.ts\` |
|
|
902
962
|
| TypeScript config | \`tsconfig.json\` |
|
|
903
963
|
| Vite config (HMR) | \`vite.config.ts\` |
|
|
904
964
|
| Vitest config | \`vitest.config.ts\` |
|
|
@@ -1108,26 +1168,37 @@ Run tests:
|
|
|
1108
1168
|
|
|
1109
1169
|
## Environment Variables
|
|
1110
1170
|
|
|
1111
|
-
|
|
1171
|
+
Schema is declared in \`src/config/index.ts\` (extends the base
|
|
1172
|
+
\`PORT\`/\`NODE_ENV\`/\`LOG_LEVEL\` shape via \`defineEnv\`) and registered
|
|
1173
|
+
with kickjs at module load. \`src/index.ts\` imports it via
|
|
1174
|
+
\`import './config'\` **before** \`bootstrap()\` so the cache is populated
|
|
1175
|
+
in time for DI. Add new keys to the schema, drop their values into
|
|
1176
|
+
\`.env\`, and they're typed everywhere.
|
|
1112
1177
|
|
|
1113
|
-
|
|
1178
|
+
Access patterns:
|
|
1179
|
+
|
|
1180
|
+
1. **@Value() decorator** (recommended for known-at-construction keys):
|
|
1114
1181
|
\`\`\`ts
|
|
1115
1182
|
@Value('DATABASE_URL')
|
|
1116
1183
|
private dbUrl!: string
|
|
1117
1184
|
\`\`\`
|
|
1118
1185
|
|
|
1119
|
-
2. **ConfigService** (for dynamic access):
|
|
1186
|
+
2. **ConfigService** (recommended for dynamic / method-scoped access):
|
|
1120
1187
|
\`\`\`ts
|
|
1121
1188
|
@Autowired()
|
|
1122
1189
|
private config!: ConfigService
|
|
1123
1190
|
|
|
1124
|
-
const port = this.config.get('PORT'
|
|
1191
|
+
const port = this.config.get('PORT') // typed: number
|
|
1125
1192
|
\`\`\`
|
|
1126
1193
|
|
|
1127
|
-
3. **Direct
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1194
|
+
3. **Direct \`process.env\`** — avoid in app code; bypasses Zod
|
|
1195
|
+
coercion and the typed \`KickEnv\` registry.
|
|
1196
|
+
|
|
1197
|
+
> **Pitfall**: never delete \`import './config'\` from \`src/index.ts\`.
|
|
1198
|
+
> If the schema is not registered before DI runs, \`config.get()\`
|
|
1199
|
+
> returns \`undefined\` for user keys (the base shape only) and
|
|
1200
|
+
> \`@Value()\` only works because of its raw \`process.env\` fallback —
|
|
1201
|
+
> Zod coercion + schema defaults are silently skipped.
|
|
1131
1202
|
|
|
1132
1203
|
## Key Decorators
|
|
1133
1204
|
|
|
@@ -1174,6 +1245,7 @@ ${template === "graphql" ? `### GraphQL
|
|
|
1174
1245
|
4. **Routes not found** — Check controller path and module registration
|
|
1175
1246
|
5. **HMR not working** — Verify \`vite.config.ts\` has \`hmr: true\`
|
|
1176
1247
|
6. **Decorators not working** — Check \`tsconfig.json\` has \`experimentalDecorators: true\`
|
|
1248
|
+
7. **\`config.get('YOUR_KEY')\` returns \`undefined\`** — \`src/index.ts\` is missing \`import './config'\`. That side-effect import registers the env schema with kickjs (\`loadEnv(envSchema)\` runs at module load). Without it, \`ConfigService\` falls back to the base schema (\`PORT\`/\`NODE_ENV\`/\`LOG_LEVEL\` only) and every user-defined key reads as \`undefined\`. \`@Value()\` may *appear* to work because of a raw \`process.env\` fallback, but Zod coercion and schema defaults are silently skipped — investigate \`src/index.ts\` and \`src/config/index.ts\` first.
|
|
1177
1249
|
|
|
1178
1250
|
## CLI Commands Reference
|
|
1179
1251
|
|
|
@@ -1226,7 +1298,7 @@ async function initProject(options) {
|
|
|
1226
1298
|
await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
|
|
1227
1299
|
await writeFileSafe(join(dir, ".env"), generateEnv());
|
|
1228
1300
|
await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
|
|
1229
|
-
await writeFileSafe(join(dir, "src/
|
|
1301
|
+
await writeFileSafe(join(dir, "src/config/index.ts"), generateEnvFile());
|
|
1230
1302
|
await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
|
|
1231
1303
|
await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
|
|
1232
1304
|
await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
|
|
@@ -4864,31 +4936,49 @@ function extractInjectsFromSource(source, filePath, cwd) {
|
|
|
4864
4936
|
return out;
|
|
4865
4937
|
}
|
|
4866
4938
|
/**
|
|
4867
|
-
*
|
|
4868
|
-
*
|
|
4939
|
+
* Default search order for the env schema file. Newer projects keep
|
|
4940
|
+
* the schema under `src/config/` so the framework's "config" concept
|
|
4941
|
+
* has a single home; older scaffolds dropped it at `src/env.ts` (kept
|
|
4942
|
+
* here for back-compat). The first match wins.
|
|
4943
|
+
*/
|
|
4944
|
+
const DEFAULT_ENV_FILE_CANDIDATES = [
|
|
4945
|
+
"src/config/index.ts",
|
|
4946
|
+
"src/config/env.ts",
|
|
4947
|
+
"src/config.ts",
|
|
4948
|
+
"src/env.ts"
|
|
4949
|
+
];
|
|
4950
|
+
/**
|
|
4951
|
+
* Look for an env schema file. When `envFile` is the string default
|
|
4952
|
+
* (`'src/env.ts'`) or omitted, every entry in `DEFAULT_ENV_FILE_CANDIDATES`
|
|
4953
|
+
* is tried in order. When the caller passes an explicit path, only that
|
|
4954
|
+
* path is tried (so projects can opt out of the search by setting
|
|
4955
|
+
* `kick.config.ts → typegen.envFile`).
|
|
4956
|
+
*
|
|
4957
|
+
* Returns a `DiscoveredEnv` if the file exists and contains both a
|
|
4869
4958
|
* `defineEnv(...)` call and a default export — the two markers we
|
|
4870
4959
|
* need before it's safe to emit `import type schema from '...'` in
|
|
4871
|
-
* the generator.
|
|
4872
|
-
*
|
|
4873
|
-
*
|
|
4874
|
-
* default export) so the generator skips env typing silently. Users
|
|
4875
|
-
* who want env typing must opt in by writing `src/env.ts` to the
|
|
4876
|
-
* documented shape.
|
|
4960
|
+
* the generator. Returns `null` for any other state (no candidate
|
|
4961
|
+
* found, no defineEnv, no default export) so the generator skips env
|
|
4962
|
+
* typing silently.
|
|
4877
4963
|
*/
|
|
4878
4964
|
async function detectEnvFile(cwd, envFile) {
|
|
4879
|
-
const
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
source
|
|
4883
|
-
|
|
4884
|
-
|
|
4965
|
+
const candidates = envFile === "src/env.ts" ? DEFAULT_ENV_FILE_CANDIDATES : [envFile];
|
|
4966
|
+
for (const candidate of candidates) {
|
|
4967
|
+
const abs = resolve(cwd, candidate);
|
|
4968
|
+
let source;
|
|
4969
|
+
try {
|
|
4970
|
+
source = await readFile(abs, "utf-8");
|
|
4971
|
+
} catch {
|
|
4972
|
+
continue;
|
|
4973
|
+
}
|
|
4974
|
+
if (!/\bdefineEnv\s*\(/.test(source)) continue;
|
|
4975
|
+
if (!/export\s+default\b/.test(source)) continue;
|
|
4976
|
+
return {
|
|
4977
|
+
filePath: abs,
|
|
4978
|
+
relativePath: toRelative(abs, cwd)
|
|
4979
|
+
};
|
|
4885
4980
|
}
|
|
4886
|
-
|
|
4887
|
-
if (!/export\s+default\b/.test(source)) return null;
|
|
4888
|
-
return {
|
|
4889
|
-
filePath: abs,
|
|
4890
|
-
relativePath: toRelative(abs, cwd)
|
|
4891
|
-
};
|
|
4981
|
+
return null;
|
|
4892
4982
|
}
|
|
4893
4983
|
/** Detect duplicate class names across files */
|
|
4894
4984
|
function findCollisions(classes) {
|
|
@@ -6111,9 +6201,9 @@ const PACKAGE_REGISTRY = {
|
|
|
6111
6201
|
dev: true
|
|
6112
6202
|
},
|
|
6113
6203
|
config: {
|
|
6114
|
-
pkg: "
|
|
6204
|
+
pkg: "dotenv",
|
|
6115
6205
|
peers: [],
|
|
6116
|
-
description: "
|
|
6206
|
+
description: "Optional .env file loader (kickjs ConfigService now ships in @forinda/kickjs)"
|
|
6117
6207
|
},
|
|
6118
6208
|
cli: {
|
|
6119
6209
|
pkg: "@forinda/kickjs-cli",
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli v2.2.
|
|
2
|
+
* @forinda/kickjs-cli v2.2.3
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -1528,6 +1528,11 @@ export class Prisma${pascal}Repository implements I${pascal}Repository {
|
|
|
1528
1528
|
function generateEntryFile(name, template, version) {
|
|
1529
1529
|
switch (template) {
|
|
1530
1530
|
case "graphql": return `import 'reflect-metadata'
|
|
1531
|
+
// Side-effect import — registers the extended env schema with kickjs
|
|
1532
|
+
// **before** any controller / service / @Value gets resolved. Without
|
|
1533
|
+
// this line ConfigService.get('YOUR_KEY') returns undefined because the
|
|
1534
|
+
// cached schema would still be the base shape. See guide/configuration.
|
|
1535
|
+
import './config'
|
|
1531
1536
|
import { bootstrap } from '@forinda/kickjs'
|
|
1532
1537
|
import { DevToolsAdapter } from '@forinda/kickjs-devtools'
|
|
1533
1538
|
import { GraphQLAdapter } from '@forinda/kickjs-graphql'
|
|
@@ -1550,6 +1555,11 @@ export const app = await bootstrap({
|
|
|
1550
1555
|
})
|
|
1551
1556
|
`;
|
|
1552
1557
|
case "cqrs": return `import 'reflect-metadata'
|
|
1558
|
+
// Side-effect import — registers the extended env schema with kickjs
|
|
1559
|
+
// **before** any controller / service / @Value gets resolved. Without
|
|
1560
|
+
// this line ConfigService.get('YOUR_KEY') returns undefined because the
|
|
1561
|
+
// cached schema would still be the base shape. See guide/configuration.
|
|
1562
|
+
import './config'
|
|
1553
1563
|
import { bootstrap } from '@forinda/kickjs'
|
|
1554
1564
|
import { DevToolsAdapter } from '@forinda/kickjs-devtools'
|
|
1555
1565
|
import { SwaggerAdapter } from '@forinda/kickjs-swagger'
|
|
@@ -1577,6 +1587,11 @@ export const app = await bootstrap({
|
|
|
1577
1587
|
})
|
|
1578
1588
|
`;
|
|
1579
1589
|
case "minimal": return `import 'reflect-metadata'
|
|
1590
|
+
// Side-effect import — registers the extended env schema with kickjs
|
|
1591
|
+
// **before** any controller / service / @Value gets resolved. Without
|
|
1592
|
+
// this line ConfigService.get('YOUR_KEY') returns undefined because the
|
|
1593
|
+
// cached schema would still be the base shape. See guide/configuration.
|
|
1594
|
+
import './config'
|
|
1580
1595
|
import { bootstrap } from '@forinda/kickjs'
|
|
1581
1596
|
import { modules } from './modules'
|
|
1582
1597
|
|
|
@@ -1584,6 +1599,11 @@ import { modules } from './modules'
|
|
|
1584
1599
|
export const app = await bootstrap({ modules })
|
|
1585
1600
|
`;
|
|
1586
1601
|
default: return `import 'reflect-metadata'
|
|
1602
|
+
// Side-effect import — registers the extended env schema with kickjs
|
|
1603
|
+
// **before** any controller / service / @Value gets resolved. Without
|
|
1604
|
+
// this line ConfigService.get('YOUR_KEY') returns undefined because the
|
|
1605
|
+
// cached schema would still be the base shape. See guide/configuration.
|
|
1606
|
+
import './config'
|
|
1587
1607
|
import express from 'express'
|
|
1588
1608
|
import {
|
|
1589
1609
|
bootstrap,
|
|
@@ -1626,10 +1646,17 @@ export const modules: AppModuleClass[] = [HelloModule]
|
|
|
1626
1646
|
`;
|
|
1627
1647
|
}
|
|
1628
1648
|
/**
|
|
1629
|
-
* Generate `src/
|
|
1649
|
+
* Generate `src/config/index.ts` — the project's typed env schema.
|
|
1630
1650
|
*
|
|
1631
1651
|
* Default-exports a `defineEnv(...)` schema so `kick typegen` can
|
|
1632
|
-
* infer it into the global `KickEnv` registry
|
|
1652
|
+
* infer it into the global `KickEnv` registry, and *also* calls
|
|
1653
|
+
* `loadEnv(envSchema)` as a module-load side effect so `ConfigService`
|
|
1654
|
+
* and `@Value()` see the extended shape from the very first DI
|
|
1655
|
+
* resolution. The companion `src/index.ts` template adds
|
|
1656
|
+
* `import './config'` immediately after `reflect-metadata` so the
|
|
1657
|
+
* registration runs before `bootstrap()` constructs anything.
|
|
1658
|
+
*
|
|
1659
|
+
* After typegen runs:
|
|
1633
1660
|
*
|
|
1634
1661
|
* @Value('DATABASE_URL') private url!: Env<'DATABASE_URL'>
|
|
1635
1662
|
* process.env.DATABASE_URL // typed as string
|
|
@@ -1637,7 +1664,7 @@ export const modules: AppModuleClass[] = [HelloModule]
|
|
|
1637
1664
|
* Both autocomplete and type-check at compile time.
|
|
1638
1665
|
*/
|
|
1639
1666
|
function generateEnvFile() {
|
|
1640
|
-
return `import { defineEnv } from '@forinda/kickjs
|
|
1667
|
+
return `import { defineEnv, loadEnv } from '@forinda/kickjs/config'
|
|
1641
1668
|
import { z } from 'zod'
|
|
1642
1669
|
|
|
1643
1670
|
/**
|
|
@@ -1653,11 +1680,25 @@ import { z } from 'zod'
|
|
|
1653
1680
|
* JWT_SECRET: z.string().min(32),
|
|
1654
1681
|
* REDIS_URL: z.string().url().optional(),
|
|
1655
1682
|
*/
|
|
1656
|
-
|
|
1683
|
+
const envSchema = defineEnv((base) =>
|
|
1657
1684
|
base.extend({
|
|
1658
1685
|
// DATABASE_URL: z.string().url(),
|
|
1659
1686
|
}),
|
|
1660
1687
|
)
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* IMPORTANT — side effect: register the schema with kickjs's env cache
|
|
1691
|
+
* **at module-load time**. \`ConfigService\` and \`@Value()\` both consume
|
|
1692
|
+
* this cache, and they will fall back to the base schema (or undefined)
|
|
1693
|
+
* if no extended schema has been registered before they're resolved.
|
|
1694
|
+
*
|
|
1695
|
+
* As long as \`src/index.ts\` imports this file (\`import './env'\`) at the
|
|
1696
|
+
* top — before \`bootstrap()\` runs — every controller and service in the
|
|
1697
|
+
* app sees the typed extended values.
|
|
1698
|
+
*/
|
|
1699
|
+
export const env = loadEnv(envSchema)
|
|
1700
|
+
|
|
1701
|
+
export default envSchema
|
|
1661
1702
|
`;
|
|
1662
1703
|
}
|
|
1663
1704
|
/** Generate src/modules/hello/hello.service.ts */
|
|
@@ -2577,7 +2618,7 @@ export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
|
|
|
2577
2618
|
function generatePackageJson(name, template, kickjsVersion) {
|
|
2578
2619
|
const baseDeps = {
|
|
2579
2620
|
"@forinda/kickjs": kickjsVersion,
|
|
2580
|
-
"
|
|
2621
|
+
dotenv: "^17.3.1",
|
|
2581
2622
|
express: "^5.1.0",
|
|
2582
2623
|
"reflect-metadata": "^0.2.2",
|
|
2583
2624
|
zod: "^4.3.6",
|
|
@@ -2644,13 +2685,16 @@ function generateViteConfig() {
|
|
|
2644
2685
|
return `import { defineConfig } from 'vite'
|
|
2645
2686
|
import { resolve } from 'node:path'
|
|
2646
2687
|
import swc from 'unplugin-swc'
|
|
2647
|
-
import { kickjsVitePlugin } from '@forinda/kickjs-vite'
|
|
2688
|
+
import { kickjsVitePlugin, envWatchPlugin } from '@forinda/kickjs-vite'
|
|
2648
2689
|
|
|
2649
2690
|
export default defineConfig({
|
|
2650
2691
|
oxc: false,
|
|
2651
2692
|
plugins: [
|
|
2652
2693
|
swc.vite(),
|
|
2653
2694
|
kickjsVitePlugin({ entry: 'src/index.ts' }),
|
|
2695
|
+
// Watches .env files and triggers a full reload on change so the
|
|
2696
|
+
// dev server picks up env tweaks without a manual restart.
|
|
2697
|
+
envWatchPlugin(),
|
|
2654
2698
|
],
|
|
2655
2699
|
resolve: {
|
|
2656
2700
|
alias: {
|
|
@@ -2799,11 +2843,7 @@ function generateReadme(name, template, pm) {
|
|
|
2799
2843
|
cqrs: "CQRS + Event-Driven",
|
|
2800
2844
|
minimal: "Minimal"
|
|
2801
2845
|
};
|
|
2802
|
-
const packages = [
|
|
2803
|
-
"@forinda/kickjs",
|
|
2804
|
-
"@forinda/kickjs-vite",
|
|
2805
|
-
"@forinda/kickjs-config"
|
|
2806
|
-
];
|
|
2846
|
+
const packages = ["@forinda/kickjs", "@forinda/kickjs-vite"];
|
|
2807
2847
|
if (template !== "minimal") packages.push("@forinda/kickjs-swagger", "@forinda/kickjs-devtools");
|
|
2808
2848
|
if (template === "graphql") packages.push("@forinda/kickjs-graphql");
|
|
2809
2849
|
if (template === "cqrs") packages.push("@forinda/kickjs-queue", "@forinda/kickjs-ws", "@forinda/kickjs-otel");
|
|
@@ -3039,10 +3079,23 @@ kick add --list # Show all available packages
|
|
|
3039
3079
|
|
|
3040
3080
|
## Environment Configuration
|
|
3041
3081
|
|
|
3042
|
-
|
|
3082
|
+
The project's typed env schema lives in **\`src/config/index.ts\`** —
|
|
3083
|
+
extend the base schema there with your application-specific keys, and
|
|
3084
|
+
the schema is auto-registered with kickjs at module load. The companion
|
|
3085
|
+
\`src/index.ts\` imports it as a side effect (\`import './config'\`) **before**
|
|
3086
|
+
\`bootstrap()\` runs, so every \`@Service\`, \`@Controller\`, \`@Value\`, and
|
|
3087
|
+
\`ConfigService\` resolution sees the validated extended values.
|
|
3088
|
+
|
|
3089
|
+
> **Do not delete \`import './config'\` from \`src/index.ts\`.** It is the
|
|
3090
|
+
> registration step that wires \`ConfigService\` to your env schema.
|
|
3091
|
+
> Without it, \`config.get('YOUR_KEY')\` returns \`undefined\` for every
|
|
3092
|
+
> user-defined key and \`@Value('YOUR_KEY')\` only works because of a
|
|
3093
|
+
> raw \`process.env\` fallback (Zod coercion + defaults are skipped).
|
|
3094
|
+
|
|
3095
|
+
Edit \`.env\` for variable values. Access them with \`@Value()\`:
|
|
3043
3096
|
|
|
3044
3097
|
\`\`\`ts
|
|
3045
|
-
import { Value } from '@forinda/kickjs
|
|
3098
|
+
import { Value } from '@forinda/kickjs'
|
|
3046
3099
|
|
|
3047
3100
|
@Service()
|
|
3048
3101
|
export class ApiService {
|
|
@@ -3057,7 +3110,7 @@ export class ApiService {
|
|
|
3057
3110
|
Or use \`ConfigService\`:
|
|
3058
3111
|
|
|
3059
3112
|
\`\`\`ts
|
|
3060
|
-
import { ConfigService } from '@forinda/kickjs
|
|
3113
|
+
import { Service, Autowired, ConfigService } from '@forinda/kickjs'
|
|
3061
3114
|
|
|
3062
3115
|
@Service()
|
|
3063
3116
|
export class AppService {
|
|
@@ -3065,11 +3118,16 @@ export class AppService {
|
|
|
3065
3118
|
private config!: ConfigService
|
|
3066
3119
|
|
|
3067
3120
|
getPort() {
|
|
3068
|
-
|
|
3121
|
+
// typed: number, Zod-coerced from baseEnvSchema
|
|
3122
|
+
return this.config.get('PORT')
|
|
3069
3123
|
}
|
|
3070
3124
|
}
|
|
3071
3125
|
\`\`\`
|
|
3072
3126
|
|
|
3127
|
+
Hot-reload of \`.env\` changes during dev is wired up automatically via
|
|
3128
|
+
\`envWatchPlugin()\` in \`vite.config.ts\` — edit \`.env\`, the dev server
|
|
3129
|
+
reloads, and the next \`config.get()\` re-parses with the new values.
|
|
3130
|
+
|
|
3073
3131
|
## Testing
|
|
3074
3132
|
|
|
3075
3133
|
Tests live in \`src/**/*.test.ts\`:
|
|
@@ -3131,6 +3189,7 @@ ${template === "cqrs" ? `### CQRS/Event Decorators
|
|
|
3131
3189
|
3. **Always use \`ctx.body\`** — never \`req.body\` directly
|
|
3132
3190
|
4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
|
|
3133
3191
|
5. **Vite HMR requires proper cleanup** — adapters should implement \`shutdown()\`
|
|
3192
|
+
6. **Never delete \`import './config'\` from \`src/index.ts\`** — that side-effect import registers the env schema with kickjs. Without it \`ConfigService.get('YOUR_KEY')\` returns \`undefined\` for every user-defined key. \`@Value('YOUR_KEY')\` *appears* to keep working but only via a raw \`process.env\` fallback (Zod coercion + schema defaults are silently skipped).
|
|
3134
3193
|
|
|
3135
3194
|
## Learn More
|
|
3136
3195
|
|
|
@@ -3162,7 +3221,8 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
|
|
|
3162
3221
|
| Entry point | \`src/index.ts\` |
|
|
3163
3222
|
| Module registry | \`src/modules/index.ts\` |
|
|
3164
3223
|
| Feature modules | \`src/modules/<module-name>/\` |
|
|
3165
|
-
${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}|
|
|
3224
|
+
${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Env values | \`.env\` |
|
|
3225
|
+
| Env schema (Zod) | \`src/config/index.ts\` |
|
|
3166
3226
|
| TypeScript config | \`tsconfig.json\` |
|
|
3167
3227
|
| Vite config (HMR) | \`vite.config.ts\` |
|
|
3168
3228
|
| Vitest config | \`vitest.config.ts\` |
|
|
@@ -3372,26 +3432,37 @@ Run tests:
|
|
|
3372
3432
|
|
|
3373
3433
|
## Environment Variables
|
|
3374
3434
|
|
|
3375
|
-
|
|
3435
|
+
Schema is declared in \`src/config/index.ts\` (extends the base
|
|
3436
|
+
\`PORT\`/\`NODE_ENV\`/\`LOG_LEVEL\` shape via \`defineEnv\`) and registered
|
|
3437
|
+
with kickjs at module load. \`src/index.ts\` imports it via
|
|
3438
|
+
\`import './config'\` **before** \`bootstrap()\` so the cache is populated
|
|
3439
|
+
in time for DI. Add new keys to the schema, drop their values into
|
|
3440
|
+
\`.env\`, and they're typed everywhere.
|
|
3441
|
+
|
|
3442
|
+
Access patterns:
|
|
3376
3443
|
|
|
3377
|
-
1. **@Value() decorator** (recommended):
|
|
3444
|
+
1. **@Value() decorator** (recommended for known-at-construction keys):
|
|
3378
3445
|
\`\`\`ts
|
|
3379
3446
|
@Value('DATABASE_URL')
|
|
3380
3447
|
private dbUrl!: string
|
|
3381
3448
|
\`\`\`
|
|
3382
3449
|
|
|
3383
|
-
2. **ConfigService** (for dynamic access):
|
|
3450
|
+
2. **ConfigService** (recommended for dynamic / method-scoped access):
|
|
3384
3451
|
\`\`\`ts
|
|
3385
3452
|
@Autowired()
|
|
3386
3453
|
private config!: ConfigService
|
|
3387
3454
|
|
|
3388
|
-
const port = this.config.get('PORT'
|
|
3455
|
+
const port = this.config.get('PORT') // typed: number
|
|
3389
3456
|
\`\`\`
|
|
3390
3457
|
|
|
3391
|
-
3. **Direct
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3458
|
+
3. **Direct \`process.env\`** — avoid in app code; bypasses Zod
|
|
3459
|
+
coercion and the typed \`KickEnv\` registry.
|
|
3460
|
+
|
|
3461
|
+
> **Pitfall**: never delete \`import './config'\` from \`src/index.ts\`.
|
|
3462
|
+
> If the schema is not registered before DI runs, \`config.get()\`
|
|
3463
|
+
> returns \`undefined\` for user keys (the base shape only) and
|
|
3464
|
+
> \`@Value()\` only works because of its raw \`process.env\` fallback —
|
|
3465
|
+
> Zod coercion + schema defaults are silently skipped.
|
|
3395
3466
|
|
|
3396
3467
|
## Key Decorators
|
|
3397
3468
|
|
|
@@ -3438,6 +3509,7 @@ ${template === "graphql" ? `### GraphQL
|
|
|
3438
3509
|
4. **Routes not found** — Check controller path and module registration
|
|
3439
3510
|
5. **HMR not working** — Verify \`vite.config.ts\` has \`hmr: true\`
|
|
3440
3511
|
6. **Decorators not working** — Check \`tsconfig.json\` has \`experimentalDecorators: true\`
|
|
3512
|
+
7. **\`config.get('YOUR_KEY')\` returns \`undefined\`** — \`src/index.ts\` is missing \`import './config'\`. That side-effect import registers the env schema with kickjs (\`loadEnv(envSchema)\` runs at module load). Without it, \`ConfigService\` falls back to the base schema (\`PORT\`/\`NODE_ENV\`/\`LOG_LEVEL\` only) and every user-defined key reads as \`undefined\`. \`@Value()\` may *appear* to work because of a raw \`process.env\` fallback, but Zod coercion and schema defaults are silently skipped — investigate \`src/index.ts\` and \`src/config/index.ts\` first.
|
|
3441
3513
|
|
|
3442
3514
|
## CLI Commands Reference
|
|
3443
3515
|
|
|
@@ -3490,7 +3562,7 @@ async function initProject(options) {
|
|
|
3490
3562
|
await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
|
|
3491
3563
|
await writeFileSafe(join(dir, ".env"), generateEnv());
|
|
3492
3564
|
await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
|
|
3493
|
-
await writeFileSafe(join(dir, "src/
|
|
3565
|
+
await writeFileSafe(join(dir, "src/config/index.ts"), generateEnvFile());
|
|
3494
3566
|
await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
|
|
3495
3567
|
await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
|
|
3496
3568
|
await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
|
|
@@ -3536,7 +3608,7 @@ async function initProject(options) {
|
|
|
3536
3608
|
}
|
|
3537
3609
|
}
|
|
3538
3610
|
try {
|
|
3539
|
-
const { runTypegen } = await import("./typegen-
|
|
3611
|
+
const { runTypegen } = await import("./typegen-BncsvEr-.mjs");
|
|
3540
3612
|
await runTypegen({
|
|
3541
3613
|
cwd: dir,
|
|
3542
3614
|
allowDuplicates: true,
|