@forinda/kickjs-cli 3.1.3 → 4.0.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/README.md +12 -89
- package/dist/cli.mjs +974 -49
- package/dist/index.d.mts +252 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +182 -28
- package/dist/index.mjs.map +1 -1
- package/dist/{typegen-0X9tc3wa.mjs → typegen-vI1eqGLK.mjs} +446 -11
- package/dist/typegen-vI1eqGLK.mjs.map +1 -0
- package/package.json +9 -14
- package/dist/typegen-0X9tc3wa.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli
|
|
2
|
+
* @forinda/kickjs-cli v4.0.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
+
import { createRequire } from "node:module";
|
|
11
12
|
import { Command } from "commander";
|
|
12
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
13
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
15
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
16
|
import { execSync, fork, spawn, spawnSync } from "node:child_process";
|
|
16
17
|
import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
17
18
|
import * as clack from "@clack/prompts";
|
|
18
19
|
import pc from "picocolors";
|
|
19
20
|
import pkg from "pluralize";
|
|
21
|
+
import { glob, globSync } from "glob";
|
|
20
22
|
import { arch, platform, release } from "node:os";
|
|
21
23
|
//#region \0rolldown/runtime.js
|
|
22
24
|
var __defProp = Object.defineProperty;
|
|
@@ -296,15 +298,15 @@ function generateEntryFile(name, template, version, packages = []) {
|
|
|
296
298
|
const gqlAdapters = [];
|
|
297
299
|
if (packages.includes("devtools")) {
|
|
298
300
|
gqlImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
|
|
299
|
-
gqlAdapters.push(`
|
|
301
|
+
gqlAdapters.push(` DevToolsAdapter(),`);
|
|
300
302
|
}
|
|
301
303
|
if (packages.includes("otel")) {
|
|
302
304
|
gqlImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
|
|
303
|
-
gqlAdapters.push(`
|
|
305
|
+
gqlAdapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
|
|
304
306
|
}
|
|
305
307
|
if (packages.includes("swagger")) {
|
|
306
308
|
gqlImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
|
|
307
|
-
gqlAdapters.push(`
|
|
309
|
+
gqlAdapters.push(` SwaggerAdapter({ info: { title: '${name}', version: '${version}' } }),`);
|
|
308
310
|
}
|
|
309
311
|
return `import 'reflect-metadata'
|
|
310
312
|
// Side-effect import — registers the extended env schema with kickjs
|
|
@@ -337,15 +339,15 @@ ${gqlAdapters.length ? gqlAdapters.join("\n") + "\n" : ""} new GraphQLAdapter
|
|
|
337
339
|
const cqrsAdapters = [];
|
|
338
340
|
if (packages.includes("otel")) {
|
|
339
341
|
cqrsImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
|
|
340
|
-
cqrsAdapters.push(`
|
|
342
|
+
cqrsAdapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
|
|
341
343
|
}
|
|
342
344
|
if (packages.includes("devtools")) {
|
|
343
345
|
cqrsImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
|
|
344
|
-
cqrsAdapters.push(`
|
|
346
|
+
cqrsAdapters.push(` DevToolsAdapter(),`);
|
|
345
347
|
}
|
|
346
348
|
if (packages.includes("swagger")) {
|
|
347
349
|
cqrsImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
|
|
348
|
-
cqrsAdapters.push(`
|
|
350
|
+
cqrsAdapters.push(` SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
|
|
349
351
|
}
|
|
350
352
|
if (packages.includes("graphql")) {
|
|
351
353
|
cqrsImports.push(`import { GraphQLAdapter } from '@forinda/kickjs-graphql'`);
|
|
@@ -364,7 +366,7 @@ ${cqrsImports.length ? cqrsImports.join("\n") + "\n" : ""}import { modules } fro
|
|
|
364
366
|
|
|
365
367
|
// Export the app for the Vite plugin (dev mode)
|
|
366
368
|
export const app = await bootstrap({
|
|
367
|
-
modules,${cqrsImports.length ? `\n adapters: [\n${cqrsAdapters.join("\n")}\n // Uncomment for WebSocket support:\n //
|
|
369
|
+
modules,${cqrsImports.length ? `\n adapters: [\n${cqrsAdapters.join("\n")}\n // Uncomment for WebSocket support:\n // WsAdapter(),\n // Uncomment when Redis is available:\n // QueueAdapter({\n // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),\n // }),\n ],` : `\n adapters: [\n // Uncomment for WebSocket support:\n // WsAdapter(),\n // Uncomment when Redis is available:\n // QueueAdapter({\n // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),\n // }),\n ],`}
|
|
368
370
|
})
|
|
369
371
|
`;
|
|
370
372
|
}
|
|
@@ -373,15 +375,15 @@ export const app = await bootstrap({
|
|
|
373
375
|
const adapters = [];
|
|
374
376
|
if (packages.includes("swagger")) {
|
|
375
377
|
imports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
|
|
376
|
-
adapters.push(`
|
|
378
|
+
adapters.push(` SwaggerAdapter({ info: { title: '${name}', version: '${version}' } }),`);
|
|
377
379
|
}
|
|
378
380
|
if (packages.includes("devtools")) {
|
|
379
381
|
imports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
|
|
380
|
-
adapters.push(`
|
|
382
|
+
adapters.push(` DevToolsAdapter(),`);
|
|
381
383
|
}
|
|
382
384
|
if (packages.includes("otel")) {
|
|
383
385
|
imports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
|
|
384
|
-
adapters.push(`
|
|
386
|
+
adapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
|
|
385
387
|
}
|
|
386
388
|
if (packages.includes("graphql")) {
|
|
387
389
|
imports.push(`import { GraphQLAdapter } from '@forinda/kickjs-graphql'`);
|
|
@@ -405,15 +407,15 @@ export const app = await bootstrap({ modules${adapters.length ? `,\n adapters:
|
|
|
405
407
|
const restAdapters = [];
|
|
406
408
|
if (packages.includes("devtools")) {
|
|
407
409
|
restImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
|
|
408
|
-
restAdapters.push(`
|
|
410
|
+
restAdapters.push(` DevToolsAdapter(),`);
|
|
409
411
|
}
|
|
410
412
|
if (packages.includes("swagger")) {
|
|
411
413
|
restImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
|
|
412
|
-
restAdapters.push(`
|
|
414
|
+
restAdapters.push(` SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
|
|
413
415
|
}
|
|
414
416
|
if (packages.includes("otel")) {
|
|
415
417
|
restImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
|
|
416
|
-
restAdapters.push(`
|
|
418
|
+
restAdapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
|
|
417
419
|
}
|
|
418
420
|
return `import 'reflect-metadata'
|
|
419
421
|
// Side-effect import — registers the extended env schema with kickjs
|
|
@@ -711,6 +713,8 @@ Copy \`.env.example\` to \`.env\` and configure:
|
|
|
711
713
|
function generateClaude(name, template, pm) {
|
|
712
714
|
return `# CLAUDE.md — ${name} Development Guide
|
|
713
715
|
|
|
716
|
+
> **Read \`AGENTS.md\` first.** It is the canonical, multi-agent reference for this project (Claude, Copilot, Codex, Gemini, etc.). This file contains the same project context distilled for Claude, plus Claude-specific notes. When the two disagree on anything substantive, treat \`AGENTS.md\` as authoritative and flag the discrepancy.
|
|
717
|
+
|
|
714
718
|
## Project Overview
|
|
715
719
|
|
|
716
720
|
This is a **${{
|
|
@@ -767,7 +771,7 @@ generated \`KickRoutes\` namespace (refreshed on \`kick dev\` and \`kick typegen
|
|
|
767
771
|
\`\`\`ts
|
|
768
772
|
import { Controller, Get, Post, type Ctx } from '@forinda/kickjs'
|
|
769
773
|
|
|
770
|
-
@Controller(
|
|
774
|
+
@Controller()
|
|
771
775
|
export class UserController {
|
|
772
776
|
@Get('/')
|
|
773
777
|
async findAll(ctx: Ctx<KickRoutes.UserController['findAll']>) {
|
|
@@ -802,9 +806,12 @@ export class UserService {
|
|
|
802
806
|
|
|
803
807
|
### Modules
|
|
804
808
|
|
|
805
|
-
Modules implement \`AppModule\` and wire controllers via \`buildRoutes()
|
|
809
|
+
Modules implement \`AppModule\` and wire controllers via \`buildRoutes()\`.
|
|
810
|
+
|
|
811
|
+
> **Naming matters.** Module files **must** be named \`<name>.module.ts\` and live under \`src/modules/\`. The Vite plugin auto-discovers files matching \`*.module.[tj]sx?\` for HMR — a misnamed file (e.g., \`projects.ts\`) won't trigger a graceful module rebuild on save and will require a full server restart. The CLI generator (\`kick g module <name>\`) follows this convention automatically.
|
|
806
812
|
|
|
807
813
|
\`\`\`ts
|
|
814
|
+
// src/modules/users/users.module.ts (named <feature>.module.ts)
|
|
808
815
|
import { type AppModule, type ModuleRoutes, buildRoutes } from '@forinda/kickjs'
|
|
809
816
|
import { UserController } from './user.controller'
|
|
810
817
|
|
|
@@ -855,6 +862,8 @@ ctx.notFound() // 404 Not Found
|
|
|
855
862
|
ctx.badRequest(msg) // 400 Bad Request
|
|
856
863
|
\`\`\`
|
|
857
864
|
|
|
865
|
+
> **Context decorators** — when a middleware's only job is to populate \`ctx.set/get\` for the handler to read, prefer \`defineContextDecorator()\` over \`@Middleware()\`. Typed via \`ContextMeta\`, supports \`dependsOn\` ordering, validates the pipeline at boot. Full pattern reference in \`AGENTS.md\` and at <https://forinda.github.io/kick-js/guide/context-decorators>.
|
|
866
|
+
|
|
858
867
|
## CLI Generators
|
|
859
868
|
|
|
860
869
|
Generate code with the \`kick\` CLI:
|
|
@@ -1043,7 +1052,7 @@ Run tests:
|
|
|
1043
1052
|
## Decorators Reference
|
|
1044
1053
|
|
|
1045
1054
|
### Route Decorators
|
|
1046
|
-
- \`@Controller(
|
|
1055
|
+
- \`@Controller()\` — mark a class as an HTTP controller (path comes from \`routes().path\`)
|
|
1047
1056
|
- \`@Get('/'), @Post('/'), @Put('/'), @Delete('/'), @Patch('/')\` — HTTP methods
|
|
1048
1057
|
- \`@Middleware(fn)\` — attach middleware
|
|
1049
1058
|
- \`@Public()\` — skip authentication (requires @forinda/kickjs-auth)
|
|
@@ -1118,6 +1127,8 @@ ${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| E
|
|
|
1118
1127
|
|
|
1119
1128
|
### Module Pattern (${template.toUpperCase()})
|
|
1120
1129
|
|
|
1130
|
+
> **Vite HMR auto-discovery contract:** module files **must** be named \`<name>.module.ts\` (or \`.tsx\`/\`.js\`/\`.jsx\`) and live under \`src/modules/\`. The Vite plugin scans for \`*.module.[tj]sx?\` to drive graceful HMR rebuilds; renaming a file to \`projects.ts\` (no \`.module\`) silently breaks HMR — saves trigger a full restart instead of a swap. The CLI generator (\`kick g module <name>\`) follows the convention; manual files must too.
|
|
1131
|
+
|
|
1121
1132
|
Each module in \`src/modules/<name>/\` typically contains:
|
|
1122
1133
|
|
|
1123
1134
|
${template === "ddd" ? `\`\`\`
|
|
@@ -1188,7 +1199,7 @@ Then:
|
|
|
1188
1199
|
If not using generators:
|
|
1189
1200
|
|
|
1190
1201
|
- [ ] Create \`src/modules/<name>/<name>.controller.ts\`
|
|
1191
|
-
- [ ] Add \`@Controller(
|
|
1202
|
+
- [ ] Add \`@Controller()\` decorator
|
|
1192
1203
|
- [ ] Add route handlers with \`@Get()\`, \`@Post()\`, etc.
|
|
1193
1204
|
- [ ] Create module file implementing \`AppModule\` with \`routes()\` returning \`{ path, router: buildRoutes(Controller), controller }\`
|
|
1194
1205
|
- [ ] Register module in \`src/modules/index.ts\` (\`AppModuleClass[]\` array)
|
|
@@ -1250,8 +1261,8 @@ import { AuthAdapter, JwtStrategy } from '@forinda/kickjs-auth'
|
|
|
1250
1261
|
bootstrap({
|
|
1251
1262
|
modules,
|
|
1252
1263
|
adapters: [
|
|
1253
|
-
|
|
1254
|
-
strategies: [
|
|
1264
|
+
AuthAdapter({
|
|
1265
|
+
strategies: [JwtStrategy({ secret: process.env.JWT_SECRET! })],
|
|
1255
1266
|
}),
|
|
1256
1267
|
],
|
|
1257
1268
|
})
|
|
@@ -1281,7 +1292,7 @@ import { WsAdapter } from '@forinda/kickjs-ws'
|
|
|
1281
1292
|
|
|
1282
1293
|
bootstrap({
|
|
1283
1294
|
modules,
|
|
1284
|
-
adapters: [
|
|
1295
|
+
adapters: [WsAdapter()],
|
|
1285
1296
|
})
|
|
1286
1297
|
\`\`\`
|
|
1287
1298
|
|
|
@@ -1385,7 +1396,7 @@ These work anywhere — scripts, plain files, outside \`@Service\`/\`@Controller
|
|
|
1385
1396
|
### HTTP Routes
|
|
1386
1397
|
| Decorator | Purpose |
|
|
1387
1398
|
|-----------|---------|
|
|
1388
|
-
| \`@Controller(
|
|
1399
|
+
| \`@Controller()\` | Define route prefix |
|
|
1389
1400
|
| \`@Get('/'), @Post('/')\` | HTTP method handlers |
|
|
1390
1401
|
| \`@Middleware(fn)\` | Attach middleware |
|
|
1391
1402
|
| \`@Public()\` | Skip auth (requires auth adapter) |
|
|
@@ -1401,6 +1412,27 @@ These work anywhere — scripts, plain files, outside \`@Service\`/\`@Controller
|
|
|
1401
1412
|
| \`@Inject('token')\` | Token-based injection |
|
|
1402
1413
|
| \`@Value('VAR')\` | Inject env variable |
|
|
1403
1414
|
|
|
1415
|
+
### Context Decorators
|
|
1416
|
+
|
|
1417
|
+
Typed, ordered way to populate \`ctx.set/get\` keys before the handler runs.
|
|
1418
|
+
Use this **instead of \`@Middleware()\`** when the middleware's only output
|
|
1419
|
+
is a value other code reads off \`ctx\`.
|
|
1420
|
+
|
|
1421
|
+
| Concept | Where it lives |
|
|
1422
|
+
|---------|----------------|
|
|
1423
|
+
| \`defineContextDecorator({ key, deps, dependsOn, optional, onError, resolve })\` | \`@forinda/kickjs\` |
|
|
1424
|
+
| Method/class decorator | \`@LoadX\` on a controller method/class |
|
|
1425
|
+
| Module hook | \`AppModule.contributors?(): ContributorRegistration[]\` |
|
|
1426
|
+
| Adapter hook | \`AppAdapter.contributors?(): ContributorRegistration[]\` |
|
|
1427
|
+
| Global registration | \`bootstrap({ contributors: [LoadX.registration] })\` |
|
|
1428
|
+
| Type augmentation | \`declare module '@forinda/kickjs' { interface ContextMeta { ... } }\` |
|
|
1429
|
+
|
|
1430
|
+
Precedence high → low: **method > class > module > adapter > global**.
|
|
1431
|
+
Cycles and missing \`dependsOn\` keys throw at \`app.setup()\` (boot fails
|
|
1432
|
+
fast). The \`onError\` hook is async-permitted.
|
|
1433
|
+
|
|
1434
|
+
Full guide: <https://forinda.github.io/kick-js/guide/context-decorators>.
|
|
1435
|
+
|
|
1404
1436
|
${template === "graphql" ? `### GraphQL
|
|
1405
1437
|
| Decorator | Purpose |
|
|
1406
1438
|
|-----------|---------|
|
|
@@ -1423,9 +1455,11 @@ ${template === "graphql" ? `### GraphQL
|
|
|
1423
1455
|
2. **DI not working** — Ensure \`reflect-metadata\` is imported in \`src/index.ts\`
|
|
1424
1456
|
3. **Tests failing randomly** — Missing \`Container.reset()\` in \`beforeEach\`
|
|
1425
1457
|
4. **Routes not found** — Check controller path and module registration
|
|
1426
|
-
5. **HMR not working** —
|
|
1458
|
+
5. **HMR not working** — Two checks: (a) \`vite.config.ts\` has \`hmr: true\`; (b) module file is named \`<name>.module.ts\` (or \`.tsx\`/\`.js\`/\`.jsx\`) and lives under \`src/modules/\`. The Vite plugin auto-discovers \`*.module.[tj]sx?\` for graceful HMR — a misnamed module file (e.g., \`projects.ts\`) silently degrades to a full restart on every save.
|
|
1427
1459
|
6. **Decorators not working** — Check \`tsconfig.json\` has \`experimentalDecorators: true\`
|
|
1428
1460
|
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.
|
|
1461
|
+
8. **Used \`@Middleware()\` to compute a value for \`ctx\`** — prefer \`defineContextDecorator()\` (see Context Decorators above). It's typed via \`ContextMeta\`, supports \`dependsOn\` for ordering, and validates the pipeline at boot. \`@Middleware()\` is for response short-circuiting, stream mutation, and pre-route-matching work.
|
|
1462
|
+
9. **Context contributor's \`dependsOn\` key not produced anywhere** — boot throws \`MissingContributorError\` naming the dependent and the route. Either remove the dep or register a contributor that produces the key (at any precedence level: method/class/module/adapter/global).
|
|
1429
1463
|
|
|
1430
1464
|
## CLI Commands Reference
|
|
1431
1465
|
|
|
@@ -1883,6 +1917,228 @@ function pluralizePascal(name) {
|
|
|
1883
1917
|
return pkg.plural(name);
|
|
1884
1918
|
}
|
|
1885
1919
|
//#endregion
|
|
1920
|
+
//#region src/generator-extension/context.ts
|
|
1921
|
+
/** Convert any string to snake_case (`UserPost` / `user-post` → `user_post`). */
|
|
1922
|
+
function toSnakeCase(name) {
|
|
1923
|
+
return toKebabCase(name).replace(/-/g, "_");
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Build a {@link GeneratorContext} from the raw name + invocation
|
|
1927
|
+
* arguments. Centralises the case-transformation logic so every plugin
|
|
1928
|
+
* generator sees the same shape regardless of how the name was typed
|
|
1929
|
+
* on the command line (`Post` vs `post` vs `user_post`).
|
|
1930
|
+
*/
|
|
1931
|
+
function buildGeneratorContext(input) {
|
|
1932
|
+
const cwd = input.cwd ?? process.cwd();
|
|
1933
|
+
const usePlural = input.pluralize ?? true;
|
|
1934
|
+
const pascal = toPascalCase(input.name);
|
|
1935
|
+
const camel = toCamelCase(input.name);
|
|
1936
|
+
const kebab = toKebabCase(input.name);
|
|
1937
|
+
const snake = toSnakeCase(input.name);
|
|
1938
|
+
const ctx = {
|
|
1939
|
+
name: input.name,
|
|
1940
|
+
pascal,
|
|
1941
|
+
camel,
|
|
1942
|
+
kebab,
|
|
1943
|
+
snake,
|
|
1944
|
+
modulesDir: input.modulesDir ?? "src/modules",
|
|
1945
|
+
cwd,
|
|
1946
|
+
args: input.args ?? [],
|
|
1947
|
+
flags: input.flags ?? {}
|
|
1948
|
+
};
|
|
1949
|
+
if (usePlural) {
|
|
1950
|
+
const pluralKebab = pluralize(kebab);
|
|
1951
|
+
ctx.pluralKebab = pluralKebab;
|
|
1952
|
+
ctx.pluralPascal = toPascalCase(pluralKebab);
|
|
1953
|
+
ctx.pluralCamel = toCamelCase(pluralKebab);
|
|
1954
|
+
}
|
|
1955
|
+
return ctx;
|
|
1956
|
+
}
|
|
1957
|
+
/** Resolve a generator output path against the context's cwd. */
|
|
1958
|
+
function resolveGeneratorPath(ctx, path) {
|
|
1959
|
+
return resolve(ctx.cwd, path);
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Dynamic-import a generator manifest file. Wraps `pathToFileURL` so
|
|
1963
|
+
* callers don't have to think about Windows/Unix path quirks.
|
|
1964
|
+
*/
|
|
1965
|
+
async function importManifest(absPath) {
|
|
1966
|
+
return import(pathToFileURL(absPath).href);
|
|
1967
|
+
}
|
|
1968
|
+
//#endregion
|
|
1969
|
+
//#region src/generator-extension/discover.ts
|
|
1970
|
+
/**
|
|
1971
|
+
* Discover generator manifests shipped by every kickjs plugin in the
|
|
1972
|
+
* project's direct deps. Spec rationale: walking the
|
|
1973
|
+
* `node_modules/@scope/kickjs-name/` tree is one option, but reading
|
|
1974
|
+
* the project's own `package.json` and resolving each dep through
|
|
1975
|
+
* Node's module resolver gives:
|
|
1976
|
+
*
|
|
1977
|
+
* 1. Predictable scoping — only deps the project actually declared
|
|
1978
|
+
* get scanned, no surprises from transitive packages
|
|
1979
|
+
* 2. pnpm `.pnpm` store compatibility — `createRequire().resolve()`
|
|
1980
|
+
* handles the symlinked layout correctly
|
|
1981
|
+
* 3. Clear error attribution — the source package name is always
|
|
1982
|
+
* known before the import happens
|
|
1983
|
+
*
|
|
1984
|
+
* The walk is shallow (direct deps only). Transitive plugins that want
|
|
1985
|
+
* to expose generators must be re-exported by a direct dep.
|
|
1986
|
+
*
|
|
1987
|
+
* Caches per-cwd inside one CLI invocation so a single `kick g` call
|
|
1988
|
+
* does the disk + import work exactly once even when multiple
|
|
1989
|
+
* generators dispatch through the same registry.
|
|
1990
|
+
*/
|
|
1991
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1992
|
+
async function discoverPluginGenerators(cwd) {
|
|
1993
|
+
const cached = cache.get(cwd);
|
|
1994
|
+
if (cached) return cached;
|
|
1995
|
+
const promise = doDiscover(cwd);
|
|
1996
|
+
cache.set(cwd, promise);
|
|
1997
|
+
return promise;
|
|
1998
|
+
}
|
|
1999
|
+
async function doDiscover(cwd) {
|
|
2000
|
+
const projectPkgPath = resolve(cwd, "package.json");
|
|
2001
|
+
if (!existsSync(projectPkgPath)) return {
|
|
2002
|
+
generators: [],
|
|
2003
|
+
loaded: [],
|
|
2004
|
+
failed: []
|
|
2005
|
+
};
|
|
2006
|
+
const depNames = collectDepNames(JSON.parse(await readFile(projectPkgPath, "utf-8")));
|
|
2007
|
+
const require = createRequire(resolve(cwd, "package.json"));
|
|
2008
|
+
const generators = [];
|
|
2009
|
+
const loaded = [];
|
|
2010
|
+
const failed = [];
|
|
2011
|
+
for (const depName of depNames) {
|
|
2012
|
+
let depPkgPath;
|
|
2013
|
+
try {
|
|
2014
|
+
depPkgPath = require.resolve(`${depName}/package.json`);
|
|
2015
|
+
} catch {
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
let depPkg;
|
|
2019
|
+
try {
|
|
2020
|
+
depPkg = JSON.parse(await readFile(depPkgPath, "utf-8"));
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
failed.push({
|
|
2023
|
+
source: depName,
|
|
2024
|
+
reason: `failed to parse package.json: ${err}`
|
|
2025
|
+
});
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
if (!depPkg.kickjs?.generators) continue;
|
|
2029
|
+
const entryRel = depPkg.kickjs.generators;
|
|
2030
|
+
const entryAbs = resolve(dirname(depPkgPath), entryRel);
|
|
2031
|
+
if (!existsSync(entryAbs)) {
|
|
2032
|
+
failed.push({
|
|
2033
|
+
source: depName,
|
|
2034
|
+
reason: `kickjs.generators points to missing file: ${entryRel}`
|
|
2035
|
+
});
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
let mod;
|
|
2039
|
+
try {
|
|
2040
|
+
mod = await importManifest(entryAbs);
|
|
2041
|
+
} catch (err) {
|
|
2042
|
+
failed.push({
|
|
2043
|
+
source: depName,
|
|
2044
|
+
reason: `failed to import manifest: ${err}`
|
|
2045
|
+
});
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
const manifest = mod.default;
|
|
2049
|
+
if (!Array.isArray(manifest)) {
|
|
2050
|
+
failed.push({
|
|
2051
|
+
source: depName,
|
|
2052
|
+
reason: `manifest's default export is not an array of GeneratorSpec`
|
|
2053
|
+
});
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
for (const entry of manifest) {
|
|
2057
|
+
if (!isGeneratorSpec(entry)) {
|
|
2058
|
+
failed.push({
|
|
2059
|
+
source: depName,
|
|
2060
|
+
reason: `manifest entry is not a valid GeneratorSpec (missing name/files)`
|
|
2061
|
+
});
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
generators.push({
|
|
2065
|
+
source: depName,
|
|
2066
|
+
spec: entry
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
loaded.push(depName);
|
|
2070
|
+
}
|
|
2071
|
+
return {
|
|
2072
|
+
generators,
|
|
2073
|
+
loaded,
|
|
2074
|
+
failed
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
function collectDepNames(pkg) {
|
|
2078
|
+
const set = /* @__PURE__ */ new Set();
|
|
2079
|
+
for (const block of [
|
|
2080
|
+
pkg.dependencies,
|
|
2081
|
+
pkg.devDependencies,
|
|
2082
|
+
pkg.peerDependencies
|
|
2083
|
+
]) {
|
|
2084
|
+
if (!block) continue;
|
|
2085
|
+
for (const name of Object.keys(block)) set.add(name);
|
|
2086
|
+
}
|
|
2087
|
+
return Array.from(set);
|
|
2088
|
+
}
|
|
2089
|
+
function isGeneratorSpec(entry) {
|
|
2090
|
+
if (!entry || typeof entry !== "object") return false;
|
|
2091
|
+
const e = entry;
|
|
2092
|
+
return typeof e.name === "string" && typeof e.files === "function";
|
|
2093
|
+
}
|
|
2094
|
+
//#endregion
|
|
2095
|
+
//#region src/generator-extension/dispatch.ts
|
|
2096
|
+
/**
|
|
2097
|
+
* Look up a plugin generator by name and run it. Returns `null` when
|
|
2098
|
+
* no plugin generator matches — callers can then fall through to the
|
|
2099
|
+
* built-in dispatch (module / scaffold / etc.).
|
|
2100
|
+
*
|
|
2101
|
+
* The lookup is FIRST-MATCH-WINS in dependency declaration order: if
|
|
2102
|
+
* two plugins claim the same generator name, the one whose package was
|
|
2103
|
+
* resolved first wins. Adopters with conflicts should rename the
|
|
2104
|
+
* generator on their side or pin one of the plugins to a different
|
|
2105
|
+
* version.
|
|
2106
|
+
*/
|
|
2107
|
+
async function tryDispatchPluginGenerator(input) {
|
|
2108
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2109
|
+
const match = findGenerator(await discoverPluginGenerators(cwd), input.generatorName);
|
|
2110
|
+
if (!match) return null;
|
|
2111
|
+
return runGenerator(match.spec, match.source, input, cwd);
|
|
2112
|
+
}
|
|
2113
|
+
/** Public helper for `kick g --list` — returns every discovered plugin generator. */
|
|
2114
|
+
async function listPluginGenerators(cwd) {
|
|
2115
|
+
return discoverPluginGenerators(cwd);
|
|
2116
|
+
}
|
|
2117
|
+
function findGenerator(discovery, name) {
|
|
2118
|
+
return discovery.generators.find((g) => g.spec.name === name);
|
|
2119
|
+
}
|
|
2120
|
+
async function runGenerator(spec, source, input, cwd) {
|
|
2121
|
+
const ctx = buildGeneratorContext({
|
|
2122
|
+
name: input.itemName,
|
|
2123
|
+
args: input.args,
|
|
2124
|
+
flags: input.flags,
|
|
2125
|
+
modulesDir: input.modulesDir,
|
|
2126
|
+
pluralize: input.pluralize,
|
|
2127
|
+
cwd
|
|
2128
|
+
});
|
|
2129
|
+
const files = await spec.files(ctx);
|
|
2130
|
+
const written = [];
|
|
2131
|
+
for (const file of files) {
|
|
2132
|
+
const absPath = resolveGeneratorPath(ctx, file.path);
|
|
2133
|
+
await writeFileSafe(absPath, file.content);
|
|
2134
|
+
written.push(absPath);
|
|
2135
|
+
}
|
|
2136
|
+
return {
|
|
2137
|
+
files: written,
|
|
2138
|
+
source
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
//#endregion
|
|
1886
2142
|
//#region src/generators/templates/module-index.ts
|
|
1887
2143
|
const repoLabelMap = {
|
|
1888
2144
|
inmemory: "in-memory",
|
|
@@ -4409,7 +4665,7 @@ import type { RequestContext } from '@forinda/kickjs'
|
|
|
4409
4665
|
import { Autowired } from '@forinda/kickjs'
|
|
4410
4666
|
import { AuthService } from './auth.service'
|
|
4411
4667
|
|
|
4412
|
-
@Controller(
|
|
4668
|
+
@Controller()
|
|
4413
4669
|
@Authenticated()
|
|
4414
4670
|
export class AuthController {
|
|
4415
4671
|
@Autowired() private authService!: AuthService
|
|
@@ -4491,7 +4747,7 @@ import type { RequestContext } from '@forinda/kickjs'
|
|
|
4491
4747
|
import { Autowired } from '@forinda/kickjs'
|
|
4492
4748
|
import { AuthService } from './auth.service'
|
|
4493
4749
|
|
|
4494
|
-
@Controller(
|
|
4750
|
+
@Controller()
|
|
4495
4751
|
@Authenticated()
|
|
4496
4752
|
export class AuthController {
|
|
4497
4753
|
@Autowired() private authService!: AuthService
|
|
@@ -5298,14 +5554,14 @@ const BUILTIN_REPO_TYPES = [
|
|
|
5298
5554
|
"inmemory",
|
|
5299
5555
|
"prisma"
|
|
5300
5556
|
];
|
|
5301
|
-
/** Resolve module config
|
|
5557
|
+
/** Resolve module config from `modules.*` block. */
|
|
5302
5558
|
function resolveModuleConfig(config) {
|
|
5303
5559
|
if (!config) return {};
|
|
5304
5560
|
const mc = {
|
|
5305
|
-
dir: config.modules?.dir
|
|
5306
|
-
repo: config.modules?.repo
|
|
5307
|
-
schemaDir: config.modules?.schemaDir
|
|
5308
|
-
pluralize: config.modules?.pluralize
|
|
5561
|
+
dir: config.modules?.dir,
|
|
5562
|
+
repo: config.modules?.repo,
|
|
5563
|
+
schemaDir: config.modules?.schemaDir,
|
|
5564
|
+
pluralize: config.modules?.pluralize,
|
|
5309
5565
|
prismaClientPath: config.modules?.prismaClientPath
|
|
5310
5566
|
};
|
|
5311
5567
|
if (mc.repo && typeof mc.repo === "string" && !BUILTIN_REPO_TYPES.includes(mc.repo)) console.warn(` Warning: modules.repo '${mc.repo}' is not a built-in type (${BUILTIN_REPO_TYPES.join(", ")}). It will generate a stub repository. Use { name: '${mc.repo}' } to silence this warning.`);
|
|
@@ -5333,7 +5589,10 @@ async function loadKickConfig(cwd) {
|
|
|
5333
5589
|
try {
|
|
5334
5590
|
const { pathToFileURL } = await import("node:url");
|
|
5335
5591
|
const mod = await import(pathToFileURL(filepath).href);
|
|
5336
|
-
|
|
5592
|
+
const config = mod.default ?? mod;
|
|
5593
|
+
const warnings = validateAssetMap(config, cwd);
|
|
5594
|
+
for (const warning of warnings) console.warn(` Warning: ${warning}`);
|
|
5595
|
+
return config;
|
|
5337
5596
|
} catch (err) {
|
|
5338
5597
|
if (filename.endsWith(".ts")) console.warn(`Warning: Failed to load ${filename}. TypeScript config files require a runtime loader (e.g. tsx, ts-node) or use kick.config.js/.mjs instead.`);
|
|
5339
5598
|
continue;
|
|
@@ -5341,6 +5600,59 @@ async function loadKickConfig(cwd) {
|
|
|
5341
5600
|
}
|
|
5342
5601
|
return null;
|
|
5343
5602
|
}
|
|
5603
|
+
/**
|
|
5604
|
+
* Validate `assetMap` entries on a loaded config. Returns a list of
|
|
5605
|
+
* human-readable warnings; the caller decides how to surface them
|
|
5606
|
+
* (typically `console.warn`). Never throws — `kick g` and other
|
|
5607
|
+
* unrelated commands should keep working even when the assetMap is
|
|
5608
|
+
* misconfigured.
|
|
5609
|
+
*
|
|
5610
|
+
* Checks:
|
|
5611
|
+
*
|
|
5612
|
+
* - Each entry's `src` is a non-empty string.
|
|
5613
|
+
* - The `src` directory exists on disk (otherwise the typegen + build
|
|
5614
|
+
* steps will fail later with cryptic errors).
|
|
5615
|
+
* - `dest` doesn't escape the project root (defensive — a `dest:
|
|
5616
|
+
* '../../etc'` typo could write files outside the workspace).
|
|
5617
|
+
* - The namespace key is a non-empty string and doesn't include a
|
|
5618
|
+
* `/` (would conflict with the `<namespace>/<key>` manifest format).
|
|
5619
|
+
*/
|
|
5620
|
+
function validateAssetMap(config, cwd) {
|
|
5621
|
+
const warnings = [];
|
|
5622
|
+
if (!config?.assetMap) return warnings;
|
|
5623
|
+
const root = resolve(cwd);
|
|
5624
|
+
for (const [namespace, entry] of Object.entries(config.assetMap)) {
|
|
5625
|
+
if (!namespace || namespace.includes("/")) {
|
|
5626
|
+
warnings.push(`assetMap key '${namespace}' is invalid — must be a non-empty string without '/'`);
|
|
5627
|
+
continue;
|
|
5628
|
+
}
|
|
5629
|
+
if (typeof entry?.src !== "string" || entry.src.length === 0) {
|
|
5630
|
+
warnings.push(`assetMap.${namespace} is missing a non-empty 'src' field`);
|
|
5631
|
+
continue;
|
|
5632
|
+
}
|
|
5633
|
+
if (!existsSync(resolve(cwd, entry.src))) warnings.push(`assetMap.${namespace}.src ('${entry.src}') does not exist — typegen + build will fail`);
|
|
5634
|
+
if (entry.dest) {
|
|
5635
|
+
if (escapesRoot$1(resolve(cwd, entry.dest), root)) warnings.push(`assetMap.${namespace}.dest ('${entry.dest}') resolves outside the project root — refusing to copy`);
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
return warnings;
|
|
5639
|
+
}
|
|
5640
|
+
/**
|
|
5641
|
+
* Returns true when `path` (absolute) resolves outside of `root`
|
|
5642
|
+
* (also absolute). Uses `path.relative` for accuracy:
|
|
5643
|
+
*
|
|
5644
|
+
* - The result is empty when paths are identical (inside).
|
|
5645
|
+
* - It starts with `..` when the path traverses outside the root.
|
|
5646
|
+
* - It's absolute (Windows: cross-drive) when there's no relative
|
|
5647
|
+
* path between them.
|
|
5648
|
+
*
|
|
5649
|
+
* Avoids the prefix-match pitfalls of `startsWith` (e.g. `/app`
|
|
5650
|
+
* matching `/app2/...`, or case-mismatches on macOS / Windows).
|
|
5651
|
+
*/
|
|
5652
|
+
function escapesRoot$1(path, root) {
|
|
5653
|
+
const rel = relative(root, path);
|
|
5654
|
+
return rel === "" ? false : rel.startsWith("..") || isAbsolute(rel);
|
|
5655
|
+
}
|
|
5344
5656
|
//#endregion
|
|
5345
5657
|
//#region src/typegen/scanner.ts
|
|
5346
5658
|
/** Decorators that mark a class as DI-managed */
|
|
@@ -5396,6 +5708,28 @@ const BARE_CREATE_TOKEN_REGEX = /createToken\s*(?:<[^>]*>)?\s*\(\s*['"`]([^'"`]+
|
|
|
5396
5708
|
/** Match `@Inject('literal')` — only literals; computed args are skipped */
|
|
5397
5709
|
const INJECT_LITERAL_REGEX = /@Inject\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
5398
5710
|
/**
|
|
5711
|
+
* Match the start of a `defineAdapter(...)` or `definePlugin(...)` call,
|
|
5712
|
+
* tolerating optional `<TConfig, TExtra>` generics. Captures the helper
|
|
5713
|
+
* name. The callsite's first-arg object is parsed forward via
|
|
5714
|
+
* `findBalancedClose` so nested objects/parens don't confuse us.
|
|
5715
|
+
*/
|
|
5716
|
+
const DEFINE_HELPER_START = /\b(defineAdapter|definePlugin)\s*(?:<[^>]*>)?\s*\(/g;
|
|
5717
|
+
/**
|
|
5718
|
+
* Match a class declaration whose `implements` clause includes `AppAdapter`.
|
|
5719
|
+
* Captures the class name. Used to pick up the (rare, post-defineAdapter)
|
|
5720
|
+
* legacy class-style adapters so their literal `name = '...'` field can
|
|
5721
|
+
* still feed `KickJsPluginRegistry`.
|
|
5722
|
+
*/
|
|
5723
|
+
const APP_ADAPTER_CLASS_REGEX = new RegExp(String.raw`export\s+(?:default\s+)?(?:abstract\s+)?class\s+(\w+)` + String.raw`(?:\s+extends\s+\w+(?:<[^>]*>)?)?` + String.raw`\s+implements\s+[^{]*\bAppAdapter\b`, "g");
|
|
5724
|
+
/** Match a string-literal `name = '...'` field on a class body. */
|
|
5725
|
+
const CLASS_NAME_FIELD_REGEX = /\bname\s*(?::\s*[^=]+)?=\s*['"`]([^'"`]+)['"`]/;
|
|
5726
|
+
/**
|
|
5727
|
+
* Match the start of a `defineAugmentation('Name', ...)` call. Captures
|
|
5728
|
+
* the literal name. The optional second-arg object is parsed forward so
|
|
5729
|
+
* `description` / `example` can be pulled out.
|
|
5730
|
+
*/
|
|
5731
|
+
const DEFINE_AUGMENTATION_START = /\bdefineAugmentation\s*\(\s*['"`]([^'"`]+)['"`]\s*(,\s*\{)?/g;
|
|
5732
|
+
/**
|
|
5399
5733
|
* Locate the start of a route decorator: `@Get(`, `@Post(`, etc.
|
|
5400
5734
|
* Used by `extractRoutesFromSource`; the rest of the route declaration
|
|
5401
5735
|
* (balanced parens, stacked decorators, method name) is parsed by walking
|
|
@@ -5742,6 +6076,120 @@ function extractInjectsFromSource(source, filePath, cwd) {
|
|
|
5742
6076
|
return out;
|
|
5743
6077
|
}
|
|
5744
6078
|
/**
|
|
6079
|
+
* Extract the bounds of an object literal that begins at `openBracePos`
|
|
6080
|
+
* (the index of the `{` character). Returns the index of the matching `}`
|
|
6081
|
+
* or -1 if no match is found. Counts balanced braces only — does not
|
|
6082
|
+
* understand string literals so a `{` or `}` inside a string inside the
|
|
6083
|
+
* object will skew the depth counter (matches `findBalancedClose`).
|
|
6084
|
+
*/
|
|
6085
|
+
function findBalancedBrace(text, openBracePos) {
|
|
6086
|
+
let depth = 1;
|
|
6087
|
+
for (let i = openBracePos + 1; i < text.length; i++) {
|
|
6088
|
+
const ch = text[i];
|
|
6089
|
+
if (ch === "{") depth++;
|
|
6090
|
+
else if (ch === "}") {
|
|
6091
|
+
depth--;
|
|
6092
|
+
if (depth === 0) return i;
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
return -1;
|
|
6096
|
+
}
|
|
6097
|
+
/**
|
|
6098
|
+
* Extract plugins/adapters declared via `defineAdapter({ name: '...' })`
|
|
6099
|
+
* or `definePlugin({ name: '...' })` calls and via class-style adapters
|
|
6100
|
+
* (`class XxxAdapter implements AppAdapter` with a string-literal `name`
|
|
6101
|
+
* field).
|
|
6102
|
+
*
|
|
6103
|
+
* Only the literal `name:` field feeds the result — the symbol on the LHS
|
|
6104
|
+
* is irrelevant since `dependsOn` references the runtime name.
|
|
6105
|
+
*/
|
|
6106
|
+
function extractPluginsAndAdaptersFromSource(source, filePath, cwd) {
|
|
6107
|
+
const out = [];
|
|
6108
|
+
const relPath = toRelative(filePath, cwd);
|
|
6109
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6110
|
+
DEFINE_HELPER_START.lastIndex = 0;
|
|
6111
|
+
let helperMatch;
|
|
6112
|
+
while ((helperMatch = DEFINE_HELPER_START.exec(source)) !== null) {
|
|
6113
|
+
const helper = helperMatch[1];
|
|
6114
|
+
const openParen = DEFINE_HELPER_START.lastIndex - 1;
|
|
6115
|
+
const closeParen = findBalancedClose(source, openParen);
|
|
6116
|
+
if (closeParen < 0) continue;
|
|
6117
|
+
const callArgs = source.slice(openParen + 1, closeParen);
|
|
6118
|
+
const nameMatch = /\bname\s*:\s*['"`]([^'"`]+)['"`]/.exec(callArgs);
|
|
6119
|
+
if (!nameMatch) continue;
|
|
6120
|
+
const name = nameMatch[1];
|
|
6121
|
+
const dedupeKey = `${helper}::${name}::${filePath}`;
|
|
6122
|
+
if (seen.has(dedupeKey)) continue;
|
|
6123
|
+
seen.add(dedupeKey);
|
|
6124
|
+
out.push({
|
|
6125
|
+
kind: helper === "definePlugin" ? "plugin" : "adapter",
|
|
6126
|
+
name,
|
|
6127
|
+
filePath,
|
|
6128
|
+
relativePath: relPath
|
|
6129
|
+
});
|
|
6130
|
+
}
|
|
6131
|
+
APP_ADAPTER_CLASS_REGEX.lastIndex = 0;
|
|
6132
|
+
let classMatch;
|
|
6133
|
+
while ((classMatch = APP_ADAPTER_CLASS_REGEX.exec(source)) !== null) {
|
|
6134
|
+
const classStart = classMatch.index;
|
|
6135
|
+
const bracePos = source.indexOf("{", classStart);
|
|
6136
|
+
if (bracePos < 0) continue;
|
|
6137
|
+
const closeBrace = findBalancedBrace(source, bracePos);
|
|
6138
|
+
if (closeBrace < 0) continue;
|
|
6139
|
+
const body = source.slice(bracePos + 1, closeBrace);
|
|
6140
|
+
const nameMatch = CLASS_NAME_FIELD_REGEX.exec(body);
|
|
6141
|
+
if (!nameMatch) continue;
|
|
6142
|
+
const name = nameMatch[1];
|
|
6143
|
+
const dedupeKey = `class::${name}::${filePath}`;
|
|
6144
|
+
if (seen.has(dedupeKey)) continue;
|
|
6145
|
+
seen.add(dedupeKey);
|
|
6146
|
+
out.push({
|
|
6147
|
+
kind: "adapter",
|
|
6148
|
+
name,
|
|
6149
|
+
filePath,
|
|
6150
|
+
relativePath: relPath
|
|
6151
|
+
});
|
|
6152
|
+
}
|
|
6153
|
+
return out;
|
|
6154
|
+
}
|
|
6155
|
+
/**
|
|
6156
|
+
* Extract `defineAugmentation('Name', { description, example })` calls
|
|
6157
|
+
* from a source file. The metadata object is optional — when absent both
|
|
6158
|
+
* `description` and `example` resolve to `null`.
|
|
6159
|
+
*/
|
|
6160
|
+
function extractAugmentationsFromSource(source, filePath, cwd) {
|
|
6161
|
+
const out = [];
|
|
6162
|
+
const relPath = toRelative(filePath, cwd);
|
|
6163
|
+
DEFINE_AUGMENTATION_START.lastIndex = 0;
|
|
6164
|
+
let match;
|
|
6165
|
+
while ((match = DEFINE_AUGMENTATION_START.exec(source)) !== null) {
|
|
6166
|
+
const name = match[1];
|
|
6167
|
+
let description = null;
|
|
6168
|
+
let example = null;
|
|
6169
|
+
if (match[2]) {
|
|
6170
|
+
const bracePos = source.indexOf("{", match.index + match[0].length - 1);
|
|
6171
|
+
if (bracePos >= 0) {
|
|
6172
|
+
const closeBrace = findBalancedBrace(source, bracePos);
|
|
6173
|
+
if (closeBrace >= 0) {
|
|
6174
|
+
const body = source.slice(bracePos + 1, closeBrace);
|
|
6175
|
+
const descMatch = /\bdescription\s*:\s*['"`]([^'"`]+)['"`]/.exec(body);
|
|
6176
|
+
const exampleMatch = /\bexample\s*:\s*['"`]([^'"`]+)['"`]/.exec(body);
|
|
6177
|
+
description = descMatch ? descMatch[1] : null;
|
|
6178
|
+
example = exampleMatch ? exampleMatch[1] : null;
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
6182
|
+
out.push({
|
|
6183
|
+
name,
|
|
6184
|
+
description,
|
|
6185
|
+
example,
|
|
6186
|
+
filePath,
|
|
6187
|
+
relativePath: relPath
|
|
6188
|
+
});
|
|
6189
|
+
}
|
|
6190
|
+
return out;
|
|
6191
|
+
}
|
|
6192
|
+
/**
|
|
5745
6193
|
* Default search order for the env schema file. Newer projects keep
|
|
5746
6194
|
* the schema under `src/config/` so the framework's "config" concept
|
|
5747
6195
|
* has a single home; older scaffolds dropped it at `src/env.ts` (kept
|
|
@@ -5812,6 +6260,8 @@ async function scanProject(opts) {
|
|
|
5812
6260
|
const routes = [];
|
|
5813
6261
|
const tokens = [];
|
|
5814
6262
|
const injects = [];
|
|
6263
|
+
const pluginsAndAdapters = [];
|
|
6264
|
+
const augmentations = [];
|
|
5815
6265
|
const sources = /* @__PURE__ */ new Map();
|
|
5816
6266
|
for (const file of files) {
|
|
5817
6267
|
let source;
|
|
@@ -5824,6 +6274,8 @@ async function scanProject(opts) {
|
|
|
5824
6274
|
classes.push(...extractClassesFromSource(source, file, opts.cwd));
|
|
5825
6275
|
tokens.push(...extractTokensFromSource(source, file, opts.cwd));
|
|
5826
6276
|
injects.push(...extractInjectsFromSource(source, file, opts.cwd));
|
|
6277
|
+
pluginsAndAdapters.push(...extractPluginsAndAdaptersFromSource(source, file, opts.cwd));
|
|
6278
|
+
augmentations.push(...extractAugmentationsFromSource(source, file, opts.cwd));
|
|
5827
6279
|
}
|
|
5828
6280
|
for (const [file, source] of sources) {
|
|
5829
6281
|
const classesInFile = classes.filter((c) => c.filePath === file);
|
|
@@ -5836,14 +6288,147 @@ async function scanProject(opts) {
|
|
|
5836
6288
|
tokens.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
5837
6289
|
injects.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
5838
6290
|
routes.sort((a, b) => a.controller.localeCompare(b.controller) || a.method.localeCompare(b.method));
|
|
6291
|
+
pluginsAndAdapters.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
6292
|
+
augmentations.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
5839
6293
|
return {
|
|
5840
6294
|
classes,
|
|
5841
6295
|
routes,
|
|
5842
6296
|
tokens,
|
|
5843
6297
|
injects,
|
|
5844
6298
|
collisions: findCollisions(classes),
|
|
5845
|
-
env: await detectEnvFile(opts.cwd, opts.envFile ?? "src/env.ts")
|
|
6299
|
+
env: await detectEnvFile(opts.cwd, opts.envFile ?? "src/env.ts"),
|
|
6300
|
+
pluginsAndAdapters,
|
|
6301
|
+
augmentations
|
|
6302
|
+
};
|
|
6303
|
+
}
|
|
6304
|
+
//#endregion
|
|
6305
|
+
//#region src/typegen/asset-types.ts
|
|
6306
|
+
/**
|
|
6307
|
+
* Walks every `assetMap` entry's source directory + emits a typed
|
|
6308
|
+
* `KickAssets` ambient augmentation (assets-plan.md PR 4). Generates
|
|
6309
|
+
* `.kickjs/types/assets.d.ts` so adopters get autocomplete on
|
|
6310
|
+
* `assets.<namespace>.<key>` and `@Asset('<namespace>/<key>')`.
|
|
6311
|
+
*
|
|
6312
|
+
* Pure module — no side effects beyond what the caller does with the
|
|
6313
|
+
* returned content. Mirrors the shape of `renderPlugins` /
|
|
6314
|
+
* `renderRegistry` in the generator so the typegen output stays
|
|
6315
|
+
* consistent across surfaces.
|
|
6316
|
+
*
|
|
6317
|
+
* @module @forinda/kickjs-cli/typegen/asset-types
|
|
6318
|
+
*/
|
|
6319
|
+
function discoverAssets(assetMap, cwd) {
|
|
6320
|
+
if (!assetMap) return {
|
|
6321
|
+
entries: [],
|
|
6322
|
+
count: 0
|
|
5846
6323
|
};
|
|
6324
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6325
|
+
for (const [namespace, entry] of Object.entries(assetMap)) {
|
|
6326
|
+
if (!entry || typeof entry.src !== "string") continue;
|
|
6327
|
+
const srcAbs = resolve(cwd, entry.src);
|
|
6328
|
+
if (!isDir(srcAbs)) continue;
|
|
6329
|
+
const matches = globSync(entry.glob ?? "**/*", {
|
|
6330
|
+
cwd: srcAbs,
|
|
6331
|
+
nodir: true,
|
|
6332
|
+
dot: false,
|
|
6333
|
+
posix: true
|
|
6334
|
+
});
|
|
6335
|
+
matches.sort();
|
|
6336
|
+
for (const rel of matches) {
|
|
6337
|
+
const key = stripExt$1(rel);
|
|
6338
|
+
const logical = `${namespace}/${key}`;
|
|
6339
|
+
seen.set(logical, {
|
|
6340
|
+
namespace,
|
|
6341
|
+
key
|
|
6342
|
+
});
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
return {
|
|
6346
|
+
entries: [...seen.values()],
|
|
6347
|
+
count: seen.size
|
|
6348
|
+
};
|
|
6349
|
+
}
|
|
6350
|
+
function renderAssetTypes(discovered) {
|
|
6351
|
+
const HEADER = `/* eslint-disable */
|
|
6352
|
+
// AUTO-GENERATED by \`kick typegen\`. DO NOT EDIT.
|
|
6353
|
+
// Re-run with \`kick typegen\` or rely on \`kick dev\` to refresh.
|
|
6354
|
+
`;
|
|
6355
|
+
if (discovered.entries.length === 0) return `${HEADER}
|
|
6356
|
+
declare module '@forinda/kickjs' {
|
|
6357
|
+
/**
|
|
6358
|
+
* Map of every typed asset discovered in the project's assetMap.
|
|
6359
|
+
* (No assetMap entries discovered yet — declare with
|
|
6360
|
+
* \`assetMap: { name: { src: 'src/...' } }\` in kick.config.ts.)
|
|
6361
|
+
*/
|
|
6362
|
+
interface KickAssets {}
|
|
6363
|
+
}
|
|
6364
|
+
|
|
6365
|
+
export {}
|
|
6366
|
+
`;
|
|
6367
|
+
const tree = {};
|
|
6368
|
+
for (const entry of discovered.entries) {
|
|
6369
|
+
const path = `${entry.namespace}/${entry.key}`.split("/");
|
|
6370
|
+
let node = tree;
|
|
6371
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
6372
|
+
const part = path[i];
|
|
6373
|
+
const existing = node[part];
|
|
6374
|
+
if (existing === LEAF) {
|
|
6375
|
+
const promoted = {};
|
|
6376
|
+
node[part] = promoted;
|
|
6377
|
+
node = promoted;
|
|
6378
|
+
} else {
|
|
6379
|
+
if (!existing) node[part] = {};
|
|
6380
|
+
node = node[part];
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
const leaf = path[path.length - 1];
|
|
6384
|
+
if (typeof node[leaf] === "object") continue;
|
|
6385
|
+
node[leaf] = LEAF;
|
|
6386
|
+
}
|
|
6387
|
+
return `${HEADER}
|
|
6388
|
+
declare module '@forinda/kickjs' {
|
|
6389
|
+
/**
|
|
6390
|
+
* Map of every typed asset discovered in the project's assetMap.
|
|
6391
|
+
* Each leaf is a \`() => string\` thunk that returns the resolved
|
|
6392
|
+
* absolute path for the file in the current run mode (dev → src,
|
|
6393
|
+
* prod → dist).
|
|
6394
|
+
*/
|
|
6395
|
+
interface KickAssets {
|
|
6396
|
+
${renderTree(tree, " ")}
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
export {}
|
|
6401
|
+
`;
|
|
6402
|
+
}
|
|
6403
|
+
const LEAF = Symbol("asset-leaf");
|
|
6404
|
+
function renderTree(node, indent) {
|
|
6405
|
+
const keys = Object.keys(node).sort();
|
|
6406
|
+
const lines = [];
|
|
6407
|
+
for (const key of keys) {
|
|
6408
|
+
const child = node[key];
|
|
6409
|
+
const safeKey = isIdentifier(key) ? key : JSON.stringify(key);
|
|
6410
|
+
if (child === LEAF) lines.push(`${indent}${safeKey}: () => string`);
|
|
6411
|
+
else {
|
|
6412
|
+
lines.push(`${indent}${safeKey}: {`);
|
|
6413
|
+
lines.push(renderTree(child, `${indent} `));
|
|
6414
|
+
lines.push(`${indent}}`);
|
|
6415
|
+
}
|
|
6416
|
+
}
|
|
6417
|
+
return lines.join("\n");
|
|
6418
|
+
}
|
|
6419
|
+
function isIdentifier(str) {
|
|
6420
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(str);
|
|
6421
|
+
}
|
|
6422
|
+
function isDir(path) {
|
|
6423
|
+
try {
|
|
6424
|
+
return statSync(path).isDirectory();
|
|
6425
|
+
} catch {
|
|
6426
|
+
return false;
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
function stripExt$1(path) {
|
|
6430
|
+
const ext = extname(path);
|
|
6431
|
+
return ext ? path.slice(0, -ext.length) : path;
|
|
5847
6432
|
}
|
|
5848
6433
|
//#endregion
|
|
5849
6434
|
//#region src/typegen/generator.ts
|
|
@@ -5981,12 +6566,17 @@ function renderIndex(includeEnv) {
|
|
|
5981
6566
|
export type { ServiceToken } from './services'
|
|
5982
6567
|
export type { ModuleToken } from './modules'
|
|
5983
6568
|
|
|
5984
|
-
// The registry, routes, and env augmentations are
|
|
5985
|
-
// importing this file (or having it on
|
|
5986
|
-
// \`container.resolve()\`,
|
|
5987
|
-
//
|
|
6569
|
+
// The registry, routes, plugins, assets, and env augmentations are
|
|
6570
|
+
// loaded as side-effects — importing this file (or having it on
|
|
6571
|
+
// tsconfig include) is enough for \`container.resolve()\`,
|
|
6572
|
+
// \`Ctx<KickRoutes.UserController['getUser']>\`,
|
|
6573
|
+
// \`dependsOn: ['TenantAdapter']\`, \`assets.mails.welcome()\`, and
|
|
6574
|
+
// \`@Value('PORT')\` to resolve.
|
|
5988
6575
|
import './registry'
|
|
5989
6576
|
import './routes'
|
|
6577
|
+
import './plugins'
|
|
6578
|
+
import './augmentations'
|
|
6579
|
+
import './assets'
|
|
5990
6580
|
${includeEnv ? "import './env'\n" : ""}`;
|
|
5991
6581
|
}
|
|
5992
6582
|
/**
|
|
@@ -6181,9 +6771,90 @@ ${interfaces.join("\n")}
|
|
|
6181
6771
|
export {}
|
|
6182
6772
|
`;
|
|
6183
6773
|
}
|
|
6774
|
+
/**
|
|
6775
|
+
* Render the `KickJsPluginRegistry` augmentation. Each entry maps the
|
|
6776
|
+
* literal `name` field of a plugin/adapter to a marker type (the
|
|
6777
|
+
* registry value isn't load-bearing at runtime — `dependsOn` only cares
|
|
6778
|
+
* about `keyof`, so any non-`never` type works). We emit `'plugin'` /
|
|
6779
|
+
* `'adapter'` strings so DevTools can later read the registry to tell
|
|
6780
|
+
* the kinds apart without a second source of truth.
|
|
6781
|
+
*
|
|
6782
|
+
* When the project has no discoverable plugins/adapters, the augmentation
|
|
6783
|
+
* is intentionally empty rather than skipped so the `keyof` constraint
|
|
6784
|
+
* resolves to `never` (which is harmless — `dependsOn: []` still works).
|
|
6785
|
+
*/
|
|
6786
|
+
function renderPlugins(items) {
|
|
6787
|
+
const byName = /* @__PURE__ */ new Map();
|
|
6788
|
+
for (const item of items) if (!byName.has(item.name)) byName.set(item.name, item);
|
|
6789
|
+
const entries = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((item) => ` '${item.name}': '${item.kind}'`).join("\n");
|
|
6790
|
+
return `${HEADER}
|
|
6791
|
+
declare module '@forinda/kickjs' {
|
|
6792
|
+
/**
|
|
6793
|
+
* Map of every plugin/adapter \`name\` discovered in the project. The
|
|
6794
|
+
* value type is the kind tag (\`'plugin'\` or \`'adapter'\`); the
|
|
6795
|
+
* \`keyof\` of this interface narrows \`dependsOn\` so misspelled deps
|
|
6796
|
+
* become compile errors instead of boot-time \`MissingMountDepError\`.
|
|
6797
|
+
*/
|
|
6798
|
+
interface KickJsPluginRegistry {
|
|
6799
|
+
${entries ? entries : " // (no plugins/adapters discovered yet — `defineAdapter`/`definePlugin` calls feed this)"}
|
|
6800
|
+
}
|
|
6801
|
+
}
|
|
6802
|
+
|
|
6803
|
+
export {}
|
|
6804
|
+
`;
|
|
6805
|
+
}
|
|
6806
|
+
/**
|
|
6807
|
+
* Render the augmentation manifest — one block per `defineAugmentation`
|
|
6808
|
+
* call discovered in the project. The output is a `.d.ts` file that
|
|
6809
|
+
* does nothing at runtime but acts as in-IDE documentation: adopters
|
|
6810
|
+
* jumping into it see every interface their plugins offer for
|
|
6811
|
+
* augmentation, alongside any `description` / `example` the plugin
|
|
6812
|
+
* authors provided.
|
|
6813
|
+
*/
|
|
6814
|
+
function renderAugmentations(items) {
|
|
6815
|
+
if (items.length === 0) return `${HEADER}
|
|
6816
|
+
// No augmentations discovered.
|
|
6817
|
+
//
|
|
6818
|
+
// Plugins advertise augmentable interfaces via:
|
|
6819
|
+
//
|
|
6820
|
+
// import { defineAugmentation } from '@forinda/kickjs'
|
|
6821
|
+
// defineAugmentation('FeatureFlags', {
|
|
6822
|
+
// description: 'Feature flag shape consumed by FlagsPlugin',
|
|
6823
|
+
// example: '{ beta: boolean; rolloutPercentage: number }',
|
|
6824
|
+
// })
|
|
6825
|
+
//
|
|
6826
|
+
// See \`docs/guide/typegen.md#augmentations\` for the full pattern.
|
|
6827
|
+
export {}
|
|
6828
|
+
`;
|
|
6829
|
+
const byName = /* @__PURE__ */ new Map();
|
|
6830
|
+
for (const item of items) if (!byName.has(item.name)) byName.set(item.name, item);
|
|
6831
|
+
const blocks = [];
|
|
6832
|
+
for (const item of [...byName.values()].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6833
|
+
const docLines = [];
|
|
6834
|
+
if (item.description) docLines.push(` * ${item.description}`);
|
|
6835
|
+
if (item.example) docLines.push(` * @example`, ` * \`\`\`ts`, ` * ${item.example}`, ` * \`\`\``);
|
|
6836
|
+
docLines.push(` * @see ${item.relativePath}`);
|
|
6837
|
+
blocks.push([
|
|
6838
|
+
"/**",
|
|
6839
|
+
...docLines,
|
|
6840
|
+
" */",
|
|
6841
|
+
`export interface ${item.name}Augmentation {}`
|
|
6842
|
+
].join("\n"));
|
|
6843
|
+
}
|
|
6844
|
+
return `${HEADER}
|
|
6845
|
+
// Catalogue of augmentable interfaces in this project. The interfaces
|
|
6846
|
+
// below are documentation only — augment the source-of-truth interfaces
|
|
6847
|
+
// in your own \`d.ts\` files (the framework declares the actual types).
|
|
6848
|
+
|
|
6849
|
+
${blocks.join("\n\n")}
|
|
6850
|
+
`;
|
|
6851
|
+
}
|
|
6184
6852
|
/** Write all generated `.d.ts` files to `outDir` */
|
|
6185
6853
|
async function generateTypes(opts) {
|
|
6186
|
-
const { classes, routes = [], tokens = [], injects = [], collisions = [], env = null,
|
|
6854
|
+
const { classes, routes = [], tokens = [], injects = [], collisions = [], env = null, pluginsAndAdapters = [], augmentations = [], assets = {
|
|
6855
|
+
entries: [],
|
|
6856
|
+
count: 0
|
|
6857
|
+
}, outDir, allowDuplicates = false, schemaValidator = false } = opts;
|
|
6187
6858
|
if (collisions.length > 0 && !allowDuplicates) throw new TokenCollisionError(collisions);
|
|
6188
6859
|
await mkdir(outDir, { recursive: true });
|
|
6189
6860
|
const registryFile = join(outDir, "registry.d.ts");
|
|
@@ -6191,6 +6862,9 @@ async function generateTypes(opts) {
|
|
|
6191
6862
|
const modulesFile = join(outDir, "modules.d.ts");
|
|
6192
6863
|
const routesFile = join(outDir, "routes.ts");
|
|
6193
6864
|
const envFile = join(outDir, "env.ts");
|
|
6865
|
+
const pluginsFile = join(outDir, "plugins.d.ts");
|
|
6866
|
+
const augmentationsFile = join(outDir, "augmentations.d.ts");
|
|
6867
|
+
const assetsFile = join(outDir, "assets.d.ts");
|
|
6194
6868
|
const indexFile = join(outDir, "index.d.ts");
|
|
6195
6869
|
const collidingNames = new Set(collisions.map((c) => c.className));
|
|
6196
6870
|
const registryContent = renderRegistry(classes, registryFile, collidingNames);
|
|
@@ -6207,17 +6881,26 @@ async function generateTypes(opts) {
|
|
|
6207
6881
|
const modulesContent = renderUnion("ModuleToken", modules, "(no @Module classes discovered — `kick g module <name>` to add one)");
|
|
6208
6882
|
const routesContent = renderRoutes(routes, routesFile, schemaValidator);
|
|
6209
6883
|
const envContent = renderEnv(env, envFile);
|
|
6884
|
+
const pluginsContent = renderPlugins(pluginsAndAdapters);
|
|
6885
|
+
const augmentationsContent = renderAugmentations(augmentations);
|
|
6886
|
+
const assetsContent = renderAssetTypes(assets);
|
|
6210
6887
|
const indexContent = renderIndex(envContent !== null);
|
|
6211
6888
|
await writeFile(registryFile, registryContent, "utf-8");
|
|
6212
6889
|
await writeFile(servicesFile, servicesContent, "utf-8");
|
|
6213
6890
|
await writeFile(modulesFile, modulesContent, "utf-8");
|
|
6214
6891
|
await writeFile(routesFile, routesContent, "utf-8");
|
|
6892
|
+
await writeFile(pluginsFile, pluginsContent, "utf-8");
|
|
6893
|
+
await writeFile(augmentationsFile, augmentationsContent, "utf-8");
|
|
6894
|
+
await writeFile(assetsFile, assetsContent, "utf-8");
|
|
6215
6895
|
await writeFile(indexFile, indexContent, "utf-8");
|
|
6216
6896
|
const written = [
|
|
6217
6897
|
registryFile,
|
|
6218
6898
|
servicesFile,
|
|
6219
6899
|
modulesFile,
|
|
6220
6900
|
routesFile,
|
|
6901
|
+
pluginsFile,
|
|
6902
|
+
augmentationsFile,
|
|
6903
|
+
assetsFile,
|
|
6221
6904
|
indexFile
|
|
6222
6905
|
];
|
|
6223
6906
|
if (envContent) {
|
|
@@ -6225,17 +6908,62 @@ async function generateTypes(opts) {
|
|
|
6225
6908
|
written.push(envFile);
|
|
6226
6909
|
}
|
|
6227
6910
|
await writeFile(join(dirname(outDir), ".gitignore"), "# Auto-generated by kick typegen\n*\n", "utf-8");
|
|
6911
|
+
const uniquePluginNames = new Set(pluginsAndAdapters.map((p) => p.name)).size;
|
|
6912
|
+
const uniqueAugmentations = new Set(augmentations.map((a) => a.name)).size;
|
|
6228
6913
|
return {
|
|
6229
6914
|
registryEntries: classTokens.length,
|
|
6230
6915
|
serviceTokens: new Set(allServices).size,
|
|
6231
6916
|
moduleTokens: modules.length,
|
|
6232
6917
|
routeEntries: routes.length,
|
|
6918
|
+
pluginEntries: uniquePluginNames,
|
|
6919
|
+
augmentationEntries: uniqueAugmentations,
|
|
6920
|
+
assetEntries: assets.count,
|
|
6233
6921
|
envWritten: envContent !== null,
|
|
6234
6922
|
written,
|
|
6235
6923
|
resolvedCollisions: collisions.length
|
|
6236
6924
|
};
|
|
6237
6925
|
}
|
|
6238
6926
|
//#endregion
|
|
6927
|
+
//#region src/typegen/token-conventions.ts
|
|
6928
|
+
/**
|
|
6929
|
+
* Regex for the §22.2 token shape. Breakdown:
|
|
6930
|
+
*
|
|
6931
|
+
* - `^(kick\/)?` — optional reserved framework prefix.
|
|
6932
|
+
* - `([a-z][\w-]*\/[A-Z]\w*)` — `<scope>/<PascalKey>`. Scope is
|
|
6933
|
+
* lowercase, key is PascalCase.
|
|
6934
|
+
* - `(\/.+)?` — optional `/suffix` for sub-flavours
|
|
6935
|
+
* (e.g. `mycorp/Cache/redis`).
|
|
6936
|
+
* - `(:[a-z][\w-]+(:[a-z][\w-]+)*)?` — optional `:instance` (and
|
|
6937
|
+
* further `:extra` colon-sections) for `.scoped()` shards.
|
|
6938
|
+
*/
|
|
6939
|
+
const TOKEN_CONVENTION_REGEX = /^(kick\/)?([a-z][\w-]*\/[A-Z]\w*)(\/.+)?(:[a-z][\w-]+(:[a-z][\w-]+)*)?$/;
|
|
6940
|
+
const LEGACY_PREFIX = "kickjs.";
|
|
6941
|
+
function validateTokenConventions(tokens) {
|
|
6942
|
+
const warnings = [];
|
|
6943
|
+
for (const token of tokens) {
|
|
6944
|
+
const name = token.name;
|
|
6945
|
+
if (name.startsWith(LEGACY_PREFIX)) continue;
|
|
6946
|
+
if (TOKEN_CONVENTION_REGEX.test(name)) continue;
|
|
6947
|
+
warnings.push({
|
|
6948
|
+
token: name,
|
|
6949
|
+
variable: token.variable,
|
|
6950
|
+
filePath: token.relativePath,
|
|
6951
|
+
reason: "does not match `<scope>/<PascalKey>[/<suffix>][:<instance>]`",
|
|
6952
|
+
suggestion: suggestRename(name)
|
|
6953
|
+
});
|
|
6954
|
+
}
|
|
6955
|
+
return warnings;
|
|
6956
|
+
}
|
|
6957
|
+
function suggestRename(name) {
|
|
6958
|
+
if (/^[A-Z]\w*$/.test(name)) return `'<scope>/${name}' (e.g. 'mycorp/${name}')`;
|
|
6959
|
+
if (name.includes(".")) return `consider '<scope>/PascalKey' instead of dotted form`;
|
|
6960
|
+
const slashLower = /^([a-z][\w-]*)\/([a-z]\w*)$/.exec(name);
|
|
6961
|
+
if (slashLower) {
|
|
6962
|
+
const [, scope, key] = slashLower;
|
|
6963
|
+
return `'${scope}/${key.charAt(0).toUpperCase()}${key.slice(1)}'`;
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
//#endregion
|
|
6239
6967
|
//#region src/typegen/index.ts
|
|
6240
6968
|
/**
|
|
6241
6969
|
* Public entry point for the KickJS typegen module.
|
|
@@ -6280,6 +7008,7 @@ async function runTypegen(opts = {}) {
|
|
|
6280
7008
|
cwd,
|
|
6281
7009
|
envFile: envFile === false ? void 0 : envFile
|
|
6282
7010
|
});
|
|
7011
|
+
const assets = discoverAssets(opts.assetMap, cwd);
|
|
6283
7012
|
const result = await generateTypes({
|
|
6284
7013
|
classes: scan.classes,
|
|
6285
7014
|
routes: scan.routes,
|
|
@@ -6287,20 +7016,36 @@ async function runTypegen(opts = {}) {
|
|
|
6287
7016
|
injects: scan.injects,
|
|
6288
7017
|
collisions: scan.collisions,
|
|
6289
7018
|
env: envFile === false ? null : scan.env,
|
|
7019
|
+
pluginsAndAdapters: scan.pluginsAndAdapters,
|
|
7020
|
+
augmentations: scan.augmentations,
|
|
7021
|
+
assets,
|
|
6290
7022
|
outDir,
|
|
6291
7023
|
allowDuplicates,
|
|
6292
7024
|
schemaValidator
|
|
6293
7025
|
});
|
|
7026
|
+
const tokenWarnings = validateTokenConventions(scan.tokens);
|
|
6294
7027
|
const elapsed = Date.now() - start;
|
|
6295
7028
|
if (!silent) {
|
|
6296
7029
|
const where = outDir.replace(cwd + "/", "");
|
|
6297
7030
|
const collisionNote = result.resolvedCollisions > 0 ? `, ${result.resolvedCollisions} collisions namespaced` : "";
|
|
6298
7031
|
const envNote = result.envWritten ? ", env typed" : "";
|
|
6299
|
-
|
|
7032
|
+
const pluginNote = result.pluginEntries > 0 ? `, ${result.pluginEntries} plugins/adapters` : "";
|
|
7033
|
+
const augNote = result.augmentationEntries > 0 ? `, ${result.augmentationEntries} augmentations` : "";
|
|
7034
|
+
const assetNote = result.assetEntries > 0 ? `, ${result.assetEntries} assets` : "";
|
|
7035
|
+
console.log(` kick typegen → ${result.serviceTokens} services, ${result.routeEntries} routes, ${result.moduleTokens} modules${pluginNote}${augNote}${assetNote}${envNote}${collisionNote} → ${where} (${elapsed}ms)`);
|
|
7036
|
+
if (tokenWarnings.length > 0) {
|
|
7037
|
+
console.warn(` kick typegen: ${tokenWarnings.length} token(s) don't match the §22.2 convention:`);
|
|
7038
|
+
for (const warning of tokenWarnings) {
|
|
7039
|
+
const variableNote = warning.variable ? ` [${warning.variable}]` : "";
|
|
7040
|
+
console.warn(` '${warning.token}' (${warning.filePath})${variableNote} — ${warning.reason}`);
|
|
7041
|
+
if (warning.suggestion) console.warn(` → suggestion: ${warning.suggestion}`);
|
|
7042
|
+
}
|
|
7043
|
+
}
|
|
6300
7044
|
}
|
|
6301
7045
|
return {
|
|
6302
7046
|
scan,
|
|
6303
|
-
result
|
|
7047
|
+
result,
|
|
7048
|
+
tokenWarnings
|
|
6304
7049
|
};
|
|
6305
7050
|
}
|
|
6306
7051
|
/**
|
|
@@ -6457,10 +7202,23 @@ const GENERATORS = [
|
|
|
6457
7202
|
description: "Generate kick.config.ts"
|
|
6458
7203
|
}
|
|
6459
7204
|
];
|
|
6460
|
-
function printGeneratorList() {
|
|
6461
|
-
console.log("\n
|
|
7205
|
+
async function printGeneratorList() {
|
|
7206
|
+
console.log("\n Built-in generators:\n");
|
|
6462
7207
|
const maxName = Math.max(...GENERATORS.map((g) => g.name.length));
|
|
6463
7208
|
for (const g of GENERATORS) console.log(` kick g ${g.name.padEnd(maxName + 2)} ${g.description}`);
|
|
7209
|
+
const discovery = await listPluginGenerators(process.cwd());
|
|
7210
|
+
if (discovery.generators.length > 0) {
|
|
7211
|
+
console.log("\n Plugin generators:\n");
|
|
7212
|
+
const pluginMax = Math.max(...discovery.generators.map((g) => `${g.spec.name} <name>`.length));
|
|
7213
|
+
for (const { source, spec } of discovery.generators) {
|
|
7214
|
+
const usage = `${spec.name} <name>`;
|
|
7215
|
+
console.log(` kick g ${usage.padEnd(pluginMax + 2)} ${spec.description} [${source}]`);
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
if (discovery.failed.length > 0) {
|
|
7219
|
+
console.log("\n Failed to load:\n");
|
|
7220
|
+
for (const { source, reason } of discovery.failed) console.log(` ${source} — ${reason}`);
|
|
7221
|
+
}
|
|
6464
7222
|
console.log();
|
|
6465
7223
|
}
|
|
6466
7224
|
/**
|
|
@@ -6497,7 +7255,7 @@ async function runModuleGeneration(names, opts, dryRun) {
|
|
|
6497
7255
|
function registerGenerateCommand(program) {
|
|
6498
7256
|
const gen = program.command("generate [names...]").alias("g").description("Generate code scaffolds — bare form `kick g <name>` is shorthand for `kick g module <name>`").option("--list", "List all available generators").option("--dry-run", "Preview files that would be generated without writing them").option("--no-entity", "Skip entity and value object generation (module shortcut)").option("--no-tests", "Skip test file generation (module shortcut)").option("--repo <type>", "Repository implementation: inmemory | drizzle | prisma").option("--pattern <pattern>", "Override project pattern: rest | ddd | cqrs | minimal").option("--minimal", "Shorthand for --pattern minimal").option("--modules-dir <dir>", "Modules directory").option("--no-pluralize", "Use singular names (skip auto-pluralization)").option("-f, --force", "Overwrite existing files without prompting").action(async (names, opts, cmd) => {
|
|
6499
7257
|
if (opts.list) {
|
|
6500
|
-
printGeneratorList();
|
|
7258
|
+
await printGeneratorList();
|
|
6501
7259
|
return;
|
|
6502
7260
|
}
|
|
6503
7261
|
if (!names || names.length === 0) {
|
|
@@ -6506,6 +7264,20 @@ function registerGenerateCommand(program) {
|
|
|
6506
7264
|
}
|
|
6507
7265
|
const dryRun = isDryRun(cmd);
|
|
6508
7266
|
setDryRun(dryRun);
|
|
7267
|
+
if (names.length >= 2) {
|
|
7268
|
+
const [generatorName, itemName, ...rest] = names;
|
|
7269
|
+
const result = await tryDispatchPluginGenerator({
|
|
7270
|
+
generatorName,
|
|
7271
|
+
itemName,
|
|
7272
|
+
args: rest,
|
|
7273
|
+
flags: opts,
|
|
7274
|
+
cwd: process.cwd()
|
|
7275
|
+
});
|
|
7276
|
+
if (result) {
|
|
7277
|
+
printGenerated(result.files, dryRun);
|
|
7278
|
+
return;
|
|
7279
|
+
}
|
|
7280
|
+
}
|
|
6509
7281
|
await runModuleGeneration(names, opts, dryRun);
|
|
6510
7282
|
});
|
|
6511
7283
|
gen.command("module <names...>").description("Generate one or more modules (e.g. kick g module user task project)").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--repo <type>", "Repository implementation: inmemory | drizzle | prisma").option("--pattern <pattern>", "Override project pattern: rest | ddd | cqrs | minimal").option("--minimal", "Shorthand for --pattern minimal").option("--modules-dir <dir>", "Modules directory").option("--no-pluralize", "Use singular names (skip auto-pluralization)").option("-f, --force", "Overwrite existing files without prompting").action(async (names, opts, cmd) => {
|
|
@@ -6735,6 +7507,132 @@ function runNodeWithEnv(entry, env, cwd) {
|
|
|
6735
7507
|
});
|
|
6736
7508
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
6737
7509
|
}
|
|
7510
|
+
/**
|
|
7511
|
+
* Run the full asset build for a loaded config:
|
|
7512
|
+
*
|
|
7513
|
+
* 1. For each `assetMap` entry, glob → copy → manifest stub.
|
|
7514
|
+
* 2. Write `dist/.kickjs-assets.json`.
|
|
7515
|
+
*
|
|
7516
|
+
* Returns a summary including the manifest contents. No-op (and no
|
|
7517
|
+
* manifest written) when `assetMap` is empty / missing — the build
|
|
7518
|
+
* pipeline shouldn't litter `dist/` with empty manifests for
|
|
7519
|
+
* adopters who don't use the feature.
|
|
7520
|
+
*/
|
|
7521
|
+
async function buildAssets(config, opts) {
|
|
7522
|
+
const { cwd, silent = false } = opts;
|
|
7523
|
+
const distDir = opts.distDir ?? config?.build?.outDir ?? "dist";
|
|
7524
|
+
const map = config?.assetMap;
|
|
7525
|
+
if (!map || Object.keys(map).length === 0) return null;
|
|
7526
|
+
const log = silent ? () => {} : console.log;
|
|
7527
|
+
const distAbs = resolve(cwd, distDir);
|
|
7528
|
+
mkdirSync(distAbs, { recursive: true });
|
|
7529
|
+
const summary = [];
|
|
7530
|
+
const manifestEntries = {};
|
|
7531
|
+
for (const [namespace, entry] of Object.entries(map)) {
|
|
7532
|
+
const result = await processEntry(namespace, entry, cwd, distAbs);
|
|
7533
|
+
summary.push(result.entrySummary);
|
|
7534
|
+
Object.assign(manifestEntries, result.manifestSlice);
|
|
7535
|
+
log(` ✓ ${namespace}: ${result.entrySummary.filesCopied} file(s) → ${result.entrySummary.dest}`);
|
|
7536
|
+
}
|
|
7537
|
+
const manifest = {
|
|
7538
|
+
version: 1,
|
|
7539
|
+
entries: manifestEntries
|
|
7540
|
+
};
|
|
7541
|
+
const manifestPath = join(distAbs, ".kickjs-assets.json");
|
|
7542
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
7543
|
+
log(` ✓ wrote manifest → ${relative(cwd, manifestPath)} (${Object.keys(manifestEntries).length} entries)`);
|
|
7544
|
+
return {
|
|
7545
|
+
manifestPath,
|
|
7546
|
+
entries: summary,
|
|
7547
|
+
manifest
|
|
7548
|
+
};
|
|
7549
|
+
}
|
|
7550
|
+
/** Per-entry inner pipeline — extracted for unit-test reuse. */
|
|
7551
|
+
async function processEntry(namespace, entry, cwd, distAbs) {
|
|
7552
|
+
const srcAbs = resolve(cwd, entry.src);
|
|
7553
|
+
const destAbs = entry.dest ? resolve(cwd, entry.dest) : join(distAbs, namespace);
|
|
7554
|
+
if (escapesRoot(destAbs, cwd)) {
|
|
7555
|
+
console.warn(` ⚠ assetMap.${namespace}.dest ('${entry.dest}') resolves outside the project root — skipping copy`);
|
|
7556
|
+
return {
|
|
7557
|
+
entrySummary: {
|
|
7558
|
+
namespace,
|
|
7559
|
+
src: entry.src,
|
|
7560
|
+
dest: relative(cwd, destAbs),
|
|
7561
|
+
filesCopied: 0
|
|
7562
|
+
},
|
|
7563
|
+
manifestSlice: {}
|
|
7564
|
+
};
|
|
7565
|
+
}
|
|
7566
|
+
if (!existsSync(srcAbs) || !isDirectorySync(srcAbs)) return {
|
|
7567
|
+
entrySummary: {
|
|
7568
|
+
namespace,
|
|
7569
|
+
src: entry.src,
|
|
7570
|
+
dest: relative(cwd, destAbs),
|
|
7571
|
+
filesCopied: 0
|
|
7572
|
+
},
|
|
7573
|
+
manifestSlice: {}
|
|
7574
|
+
};
|
|
7575
|
+
const matches = await glob(entry.glob ?? "**/*", {
|
|
7576
|
+
cwd: srcAbs,
|
|
7577
|
+
nodir: true,
|
|
7578
|
+
dot: false,
|
|
7579
|
+
posix: true
|
|
7580
|
+
});
|
|
7581
|
+
mkdirSync(destAbs, { recursive: true });
|
|
7582
|
+
const manifestSlice = {};
|
|
7583
|
+
const keyOwner = /* @__PURE__ */ new Map();
|
|
7584
|
+
for (const relPath of matches.sort()) {
|
|
7585
|
+
const srcFile = join(srcAbs, relPath);
|
|
7586
|
+
const destFile = join(destAbs, relPath);
|
|
7587
|
+
mkdirSync(dirname(destFile), { recursive: true });
|
|
7588
|
+
cpSync(srcFile, destFile);
|
|
7589
|
+
const logicalKey = `${namespace}/${stripExt(relPath)}`;
|
|
7590
|
+
const previous = keyOwner.get(logicalKey);
|
|
7591
|
+
if (previous) console.warn(` ⚠ assetMap collision in '${namespace}': '${previous}' and '${relPath}' both flatten to key '${logicalKey}'. Last-alphabetical wins ('${relPath}'). Rename one of them or set assetMap.${namespace}.glob to filter by extension.`);
|
|
7592
|
+
keyOwner.set(logicalKey, relPath);
|
|
7593
|
+
manifestSlice[logicalKey] = toManifestRelative(distAbs, destFile);
|
|
7594
|
+
}
|
|
7595
|
+
return {
|
|
7596
|
+
entrySummary: {
|
|
7597
|
+
namespace,
|
|
7598
|
+
src: entry.src,
|
|
7599
|
+
dest: relative(cwd, destAbs),
|
|
7600
|
+
filesCopied: matches.length
|
|
7601
|
+
},
|
|
7602
|
+
manifestSlice
|
|
7603
|
+
};
|
|
7604
|
+
}
|
|
7605
|
+
/** Strip the final extension from a file path (`mails/welcome.ejs` → `mails/welcome`). */
|
|
7606
|
+
function stripExt(path) {
|
|
7607
|
+
const ext = extname(path);
|
|
7608
|
+
return ext ? path.slice(0, -ext.length) : path;
|
|
7609
|
+
}
|
|
7610
|
+
/**
|
|
7611
|
+
* Make `destFile` relative to the manifest's directory + force POSIX
|
|
7612
|
+
* separators so the manifest is byte-stable across platforms.
|
|
7613
|
+
*/
|
|
7614
|
+
function toManifestRelative(manifestDir, destFile) {
|
|
7615
|
+
return relative(manifestDir, destFile).split(/[\\/]/).filter(Boolean).join("/");
|
|
7616
|
+
}
|
|
7617
|
+
/**
|
|
7618
|
+
* Project-root escape check that's safe across symlinks + drive letters.
|
|
7619
|
+
* `path.relative` returns `..` segments when the target sits above root,
|
|
7620
|
+
* and an absolute path when the two live on different roots (Windows).
|
|
7621
|
+
* `startsWith(root)` would miss both cases.
|
|
7622
|
+
*/
|
|
7623
|
+
function escapesRoot(path, root) {
|
|
7624
|
+
const rel = relative(root, path);
|
|
7625
|
+
if (rel === "") return false;
|
|
7626
|
+
return rel.startsWith("..") || isAbsolute(rel);
|
|
7627
|
+
}
|
|
7628
|
+
/** Pure helper — `false` for missing, non-dir, or unreadable paths. */
|
|
7629
|
+
function isDirectorySync(path) {
|
|
7630
|
+
try {
|
|
7631
|
+
return statSync(path).isDirectory();
|
|
7632
|
+
} catch {
|
|
7633
|
+
return false;
|
|
7634
|
+
}
|
|
7635
|
+
}
|
|
6738
7636
|
//#endregion
|
|
6739
7637
|
//#region src/commands/run.ts
|
|
6740
7638
|
/**
|
|
@@ -6763,7 +7661,8 @@ async function startDevServer(_entry, port) {
|
|
|
6763
7661
|
schemaValidator,
|
|
6764
7662
|
envFile,
|
|
6765
7663
|
srcDir: devConfig?.typegen?.srcDir,
|
|
6766
|
-
outDir: devConfig?.typegen?.outDir
|
|
7664
|
+
outDir: devConfig?.typegen?.outDir,
|
|
7665
|
+
assetMap: devConfig?.assetMap
|
|
6767
7666
|
});
|
|
6768
7667
|
} catch (err) {
|
|
6769
7668
|
console.warn(` kick typegen: skipped (${err?.message ?? err})`);
|
|
@@ -6788,7 +7687,8 @@ async function startDevServer(_entry, port) {
|
|
|
6788
7687
|
schemaValidator,
|
|
6789
7688
|
envFile,
|
|
6790
7689
|
srcDir: devConfig?.typegen?.srcDir,
|
|
6791
|
-
outDir: devConfig?.typegen?.outDir
|
|
7690
|
+
outDir: devConfig?.typegen?.outDir,
|
|
7691
|
+
assetMap: devConfig?.assetMap
|
|
6792
7692
|
}).catch(() => {});
|
|
6793
7693
|
}, 100);
|
|
6794
7694
|
};
|
|
@@ -6821,7 +7721,8 @@ function registerRunCommands(program) {
|
|
|
6821
7721
|
const { createRequire } = await import("node:module");
|
|
6822
7722
|
const { build } = await import(pathToFileURL(createRequire(resolve("package.json")).resolve("vite")).href);
|
|
6823
7723
|
await build({ configFile: resolve("vite.config.ts") });
|
|
6824
|
-
const
|
|
7724
|
+
const config = await loadKickConfig(process.cwd());
|
|
7725
|
+
const copyDirs = config?.copyDirs ?? [];
|
|
6825
7726
|
if (copyDirs.length > 0) {
|
|
6826
7727
|
console.log("\n Copying directories to dist...");
|
|
6827
7728
|
for (const entry of copyDirs) {
|
|
@@ -6838,8 +7739,32 @@ function registerRunCommands(program) {
|
|
|
6838
7739
|
console.log(` ✓ ${src} → ${dest}`);
|
|
6839
7740
|
}
|
|
6840
7741
|
}
|
|
7742
|
+
if (config?.assetMap && Object.keys(config.assetMap).length > 0) {
|
|
7743
|
+
console.log("\n Building asset map...");
|
|
7744
|
+
try {
|
|
7745
|
+
await buildAssets(config, { cwd: process.cwd() });
|
|
7746
|
+
} catch (err) {
|
|
7747
|
+
console.error(` ✗ asset build failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
7748
|
+
process.exit(1);
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
6841
7751
|
console.log("\n Build complete.\n");
|
|
6842
7752
|
});
|
|
7753
|
+
program.command("build:assets").description("Rebuild the .kickjs-assets.json manifest under the configured outDir (no JS rebuild)").action(async () => {
|
|
7754
|
+
const config = await loadKickConfig(process.cwd());
|
|
7755
|
+
if (!config?.assetMap || Object.keys(config.assetMap).length === 0) {
|
|
7756
|
+
console.log(" No assetMap entries — nothing to build.");
|
|
7757
|
+
return;
|
|
7758
|
+
}
|
|
7759
|
+
console.log("\n Building asset map...");
|
|
7760
|
+
try {
|
|
7761
|
+
await buildAssets(config, { cwd: process.cwd() });
|
|
7762
|
+
console.log("\n Asset build complete.\n");
|
|
7763
|
+
} catch (err) {
|
|
7764
|
+
console.error(` ✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
7765
|
+
process.exit(1);
|
|
7766
|
+
}
|
|
7767
|
+
});
|
|
6843
7768
|
program.command("start").description("Start production server").option("-e, --entry <file>", "Entry file", "dist/index.js").option("-p, --port <port>", "Port number").action((opts) => {
|
|
6844
7769
|
const env = { NODE_ENV: "production" };
|
|
6845
7770
|
if (opts.port) env.PORT = String(opts.port);
|
|
@@ -6871,7 +7796,6 @@ function registerInfoCommand(program) {
|
|
|
6871
7796
|
Packages:
|
|
6872
7797
|
@forinda/kickjs workspace
|
|
6873
7798
|
@forinda/kickjs-vite workspace
|
|
6874
|
-
@forinda/kickjs-config workspace
|
|
6875
7799
|
@forinda/kickjs-cli workspace
|
|
6876
7800
|
`);
|
|
6877
7801
|
});
|
|
@@ -8109,7 +9033,8 @@ function registerTypegenCommand(program) {
|
|
|
8109
9033
|
silent: opts.silent,
|
|
8110
9034
|
allowDuplicates: opts.allowDuplicates,
|
|
8111
9035
|
schemaValidator,
|
|
8112
|
-
envFile
|
|
9036
|
+
envFile,
|
|
9037
|
+
assetMap: config?.assetMap
|
|
8113
9038
|
};
|
|
8114
9039
|
try {
|
|
8115
9040
|
if (opts.watch) {
|