@forinda/kickjs-cli 2.2.2 → 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 +133 -39
- package/dist/index.mjs +96 -20
- package/dist/index.mjs.map +1 -1
- package/dist/{typegen-CDS6VbOd.mjs → typegen-BncsvEr-.mjs} +40 -22
- package/dist/typegen-BncsvEr-.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/typegen-CDS6VbOd.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
|
*
|
|
@@ -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/config'
|
|
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 */
|
|
@@ -771,7 +815,20 @@ kick add --list # Show all available packages
|
|
|
771
815
|
|
|
772
816
|
## Environment Configuration
|
|
773
817
|
|
|
774
|
-
|
|
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()\`:
|
|
775
832
|
|
|
776
833
|
\`\`\`ts
|
|
777
834
|
import { Value } from '@forinda/kickjs'
|
|
@@ -789,7 +846,7 @@ export class ApiService {
|
|
|
789
846
|
Or use \`ConfigService\`:
|
|
790
847
|
|
|
791
848
|
\`\`\`ts
|
|
792
|
-
import { ConfigService } from '@forinda/kickjs'
|
|
849
|
+
import { Service, Autowired, ConfigService } from '@forinda/kickjs'
|
|
793
850
|
|
|
794
851
|
@Service()
|
|
795
852
|
export class AppService {
|
|
@@ -797,11 +854,16 @@ export class AppService {
|
|
|
797
854
|
private config!: ConfigService
|
|
798
855
|
|
|
799
856
|
getPort() {
|
|
800
|
-
|
|
857
|
+
// typed: number, Zod-coerced from baseEnvSchema
|
|
858
|
+
return this.config.get('PORT')
|
|
801
859
|
}
|
|
802
860
|
}
|
|
803
861
|
\`\`\`
|
|
804
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
|
+
|
|
805
867
|
## Testing
|
|
806
868
|
|
|
807
869
|
Tests live in \`src/**/*.test.ts\`:
|
|
@@ -863,6 +925,7 @@ ${template === "cqrs" ? `### CQRS/Event Decorators
|
|
|
863
925
|
3. **Always use \`ctx.body\`** — never \`req.body\` directly
|
|
864
926
|
4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
|
|
865
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).
|
|
866
929
|
|
|
867
930
|
## Learn More
|
|
868
931
|
|
|
@@ -894,7 +957,8 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
|
|
|
894
957
|
| Entry point | \`src/index.ts\` |
|
|
895
958
|
| Module registry | \`src/modules/index.ts\` |
|
|
896
959
|
| Feature modules | \`src/modules/<module-name>/\` |
|
|
897
|
-
${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\` |
|
|
898
962
|
| TypeScript config | \`tsconfig.json\` |
|
|
899
963
|
| Vite config (HMR) | \`vite.config.ts\` |
|
|
900
964
|
| Vitest config | \`vitest.config.ts\` |
|
|
@@ -1104,26 +1168,37 @@ Run tests:
|
|
|
1104
1168
|
|
|
1105
1169
|
## Environment Variables
|
|
1106
1170
|
|
|
1107
|
-
|
|
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.
|
|
1177
|
+
|
|
1178
|
+
Access patterns:
|
|
1108
1179
|
|
|
1109
|
-
1. **@Value() decorator** (recommended):
|
|
1180
|
+
1. **@Value() decorator** (recommended for known-at-construction keys):
|
|
1110
1181
|
\`\`\`ts
|
|
1111
1182
|
@Value('DATABASE_URL')
|
|
1112
1183
|
private dbUrl!: string
|
|
1113
1184
|
\`\`\`
|
|
1114
1185
|
|
|
1115
|
-
2. **ConfigService** (for dynamic access):
|
|
1186
|
+
2. **ConfigService** (recommended for dynamic / method-scoped access):
|
|
1116
1187
|
\`\`\`ts
|
|
1117
1188
|
@Autowired()
|
|
1118
1189
|
private config!: ConfigService
|
|
1119
1190
|
|
|
1120
|
-
const port = this.config.get('PORT'
|
|
1191
|
+
const port = this.config.get('PORT') // typed: number
|
|
1121
1192
|
\`\`\`
|
|
1122
1193
|
|
|
1123
|
-
3. **Direct
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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.
|
|
1127
1202
|
|
|
1128
1203
|
## Key Decorators
|
|
1129
1204
|
|
|
@@ -1170,6 +1245,7 @@ ${template === "graphql" ? `### GraphQL
|
|
|
1170
1245
|
4. **Routes not found** — Check controller path and module registration
|
|
1171
1246
|
5. **HMR not working** — Verify \`vite.config.ts\` has \`hmr: true\`
|
|
1172
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.
|
|
1173
1249
|
|
|
1174
1250
|
## CLI Commands Reference
|
|
1175
1251
|
|
|
@@ -1222,7 +1298,7 @@ async function initProject(options) {
|
|
|
1222
1298
|
await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
|
|
1223
1299
|
await writeFileSafe(join(dir, ".env"), generateEnv());
|
|
1224
1300
|
await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
|
|
1225
|
-
await writeFileSafe(join(dir, "src/
|
|
1301
|
+
await writeFileSafe(join(dir, "src/config/index.ts"), generateEnvFile());
|
|
1226
1302
|
await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
|
|
1227
1303
|
await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
|
|
1228
1304
|
await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
|
|
@@ -4860,31 +4936,49 @@ function extractInjectsFromSource(source, filePath, cwd) {
|
|
|
4860
4936
|
return out;
|
|
4861
4937
|
}
|
|
4862
4938
|
/**
|
|
4863
|
-
*
|
|
4864
|
-
*
|
|
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
|
|
4865
4958
|
* `defineEnv(...)` call and a default export — the two markers we
|
|
4866
4959
|
* need before it's safe to emit `import type schema from '...'` in
|
|
4867
|
-
* the generator.
|
|
4868
|
-
*
|
|
4869
|
-
*
|
|
4870
|
-
* default export) so the generator skips env typing silently. Users
|
|
4871
|
-
* who want env typing must opt in by writing `src/env.ts` to the
|
|
4872
|
-
* 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.
|
|
4873
4963
|
*/
|
|
4874
4964
|
async function detectEnvFile(cwd, envFile) {
|
|
4875
|
-
const
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
source
|
|
4879
|
-
|
|
4880
|
-
|
|
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
|
+
};
|
|
4881
4980
|
}
|
|
4882
|
-
|
|
4883
|
-
if (!/export\s+default\b/.test(source)) return null;
|
|
4884
|
-
return {
|
|
4885
|
-
filePath: abs,
|
|
4886
|
-
relativePath: toRelative(abs, cwd)
|
|
4887
|
-
};
|
|
4981
|
+
return null;
|
|
4888
4982
|
}
|
|
4889
4983
|
/** Detect duplicate class names across files */
|
|
4890
4984
|
function findCollisions(classes) {
|
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/config'
|
|
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 */
|
|
@@ -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: {
|
|
@@ -3035,7 +3079,20 @@ kick add --list # Show all available packages
|
|
|
3035
3079
|
|
|
3036
3080
|
## Environment Configuration
|
|
3037
3081
|
|
|
3038
|
-
|
|
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()\`:
|
|
3039
3096
|
|
|
3040
3097
|
\`\`\`ts
|
|
3041
3098
|
import { Value } from '@forinda/kickjs'
|
|
@@ -3053,7 +3110,7 @@ export class ApiService {
|
|
|
3053
3110
|
Or use \`ConfigService\`:
|
|
3054
3111
|
|
|
3055
3112
|
\`\`\`ts
|
|
3056
|
-
import { ConfigService } from '@forinda/kickjs'
|
|
3113
|
+
import { Service, Autowired, ConfigService } from '@forinda/kickjs'
|
|
3057
3114
|
|
|
3058
3115
|
@Service()
|
|
3059
3116
|
export class AppService {
|
|
@@ -3061,11 +3118,16 @@ export class AppService {
|
|
|
3061
3118
|
private config!: ConfigService
|
|
3062
3119
|
|
|
3063
3120
|
getPort() {
|
|
3064
|
-
|
|
3121
|
+
// typed: number, Zod-coerced from baseEnvSchema
|
|
3122
|
+
return this.config.get('PORT')
|
|
3065
3123
|
}
|
|
3066
3124
|
}
|
|
3067
3125
|
\`\`\`
|
|
3068
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
|
+
|
|
3069
3131
|
## Testing
|
|
3070
3132
|
|
|
3071
3133
|
Tests live in \`src/**/*.test.ts\`:
|
|
@@ -3127,6 +3189,7 @@ ${template === "cqrs" ? `### CQRS/Event Decorators
|
|
|
3127
3189
|
3. **Always use \`ctx.body\`** — never \`req.body\` directly
|
|
3128
3190
|
4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
|
|
3129
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).
|
|
3130
3193
|
|
|
3131
3194
|
## Learn More
|
|
3132
3195
|
|
|
@@ -3158,7 +3221,8 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
|
|
|
3158
3221
|
| Entry point | \`src/index.ts\` |
|
|
3159
3222
|
| Module registry | \`src/modules/index.ts\` |
|
|
3160
3223
|
| Feature modules | \`src/modules/<module-name>/\` |
|
|
3161
|
-
${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\` |
|
|
3162
3226
|
| TypeScript config | \`tsconfig.json\` |
|
|
3163
3227
|
| Vite config (HMR) | \`vite.config.ts\` |
|
|
3164
3228
|
| Vitest config | \`vitest.config.ts\` |
|
|
@@ -3368,26 +3432,37 @@ Run tests:
|
|
|
3368
3432
|
|
|
3369
3433
|
## Environment Variables
|
|
3370
3434
|
|
|
3371
|
-
|
|
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:
|
|
3372
3443
|
|
|
3373
|
-
1. **@Value() decorator** (recommended):
|
|
3444
|
+
1. **@Value() decorator** (recommended for known-at-construction keys):
|
|
3374
3445
|
\`\`\`ts
|
|
3375
3446
|
@Value('DATABASE_URL')
|
|
3376
3447
|
private dbUrl!: string
|
|
3377
3448
|
\`\`\`
|
|
3378
3449
|
|
|
3379
|
-
2. **ConfigService** (for dynamic access):
|
|
3450
|
+
2. **ConfigService** (recommended for dynamic / method-scoped access):
|
|
3380
3451
|
\`\`\`ts
|
|
3381
3452
|
@Autowired()
|
|
3382
3453
|
private config!: ConfigService
|
|
3383
3454
|
|
|
3384
|
-
const port = this.config.get('PORT'
|
|
3455
|
+
const port = this.config.get('PORT') // typed: number
|
|
3385
3456
|
\`\`\`
|
|
3386
3457
|
|
|
3387
|
-
3. **Direct
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
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.
|
|
3391
3466
|
|
|
3392
3467
|
## Key Decorators
|
|
3393
3468
|
|
|
@@ -3434,6 +3509,7 @@ ${template === "graphql" ? `### GraphQL
|
|
|
3434
3509
|
4. **Routes not found** — Check controller path and module registration
|
|
3435
3510
|
5. **HMR not working** — Verify \`vite.config.ts\` has \`hmr: true\`
|
|
3436
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.
|
|
3437
3513
|
|
|
3438
3514
|
## CLI Commands Reference
|
|
3439
3515
|
|
|
@@ -3486,7 +3562,7 @@ async function initProject(options) {
|
|
|
3486
3562
|
await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
|
|
3487
3563
|
await writeFileSafe(join(dir, ".env"), generateEnv());
|
|
3488
3564
|
await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
|
|
3489
|
-
await writeFileSafe(join(dir, "src/
|
|
3565
|
+
await writeFileSafe(join(dir, "src/config/index.ts"), generateEnvFile());
|
|
3490
3566
|
await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
|
|
3491
3567
|
await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
|
|
3492
3568
|
await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
|
|
@@ -3532,7 +3608,7 @@ async function initProject(options) {
|
|
|
3532
3608
|
}
|
|
3533
3609
|
}
|
|
3534
3610
|
try {
|
|
3535
|
-
const { runTypegen } = await import("./typegen-
|
|
3611
|
+
const { runTypegen } = await import("./typegen-BncsvEr-.mjs");
|
|
3536
3612
|
await runTypegen({
|
|
3537
3613
|
cwd: dir,
|
|
3538
3614
|
allowDuplicates: true,
|