@forinda/kickjs-cli 3.2.0 → 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 +940 -47
- package/dist/index.d.mts +252 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +148 -26
- package/dist/index.mjs.map +1 -1
- package/dist/{typegen-C30frihW.mjs → typegen-vI1eqGLK.mjs} +446 -11
- package/dist/typegen-vI1eqGLK.mjs.map +1 -0
- package/package.json +9 -14
- package/dist/typegen-C30frihW.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
|
|
@@ -769,7 +771,7 @@ generated \`KickRoutes\` namespace (refreshed on \`kick dev\` and \`kick typegen
|
|
|
769
771
|
\`\`\`ts
|
|
770
772
|
import { Controller, Get, Post, type Ctx } from '@forinda/kickjs'
|
|
771
773
|
|
|
772
|
-
@Controller(
|
|
774
|
+
@Controller()
|
|
773
775
|
export class UserController {
|
|
774
776
|
@Get('/')
|
|
775
777
|
async findAll(ctx: Ctx<KickRoutes.UserController['findAll']>) {
|
|
@@ -1050,7 +1052,7 @@ Run tests:
|
|
|
1050
1052
|
## Decorators Reference
|
|
1051
1053
|
|
|
1052
1054
|
### Route Decorators
|
|
1053
|
-
- \`@Controller(
|
|
1055
|
+
- \`@Controller()\` — mark a class as an HTTP controller (path comes from \`routes().path\`)
|
|
1054
1056
|
- \`@Get('/'), @Post('/'), @Put('/'), @Delete('/'), @Patch('/')\` — HTTP methods
|
|
1055
1057
|
- \`@Middleware(fn)\` — attach middleware
|
|
1056
1058
|
- \`@Public()\` — skip authentication (requires @forinda/kickjs-auth)
|
|
@@ -1197,7 +1199,7 @@ Then:
|
|
|
1197
1199
|
If not using generators:
|
|
1198
1200
|
|
|
1199
1201
|
- [ ] Create \`src/modules/<name>/<name>.controller.ts\`
|
|
1200
|
-
- [ ] Add \`@Controller(
|
|
1202
|
+
- [ ] Add \`@Controller()\` decorator
|
|
1201
1203
|
- [ ] Add route handlers with \`@Get()\`, \`@Post()\`, etc.
|
|
1202
1204
|
- [ ] Create module file implementing \`AppModule\` with \`routes()\` returning \`{ path, router: buildRoutes(Controller), controller }\`
|
|
1203
1205
|
- [ ] Register module in \`src/modules/index.ts\` (\`AppModuleClass[]\` array)
|
|
@@ -1259,8 +1261,8 @@ import { AuthAdapter, JwtStrategy } from '@forinda/kickjs-auth'
|
|
|
1259
1261
|
bootstrap({
|
|
1260
1262
|
modules,
|
|
1261
1263
|
adapters: [
|
|
1262
|
-
|
|
1263
|
-
strategies: [
|
|
1264
|
+
AuthAdapter({
|
|
1265
|
+
strategies: [JwtStrategy({ secret: process.env.JWT_SECRET! })],
|
|
1264
1266
|
}),
|
|
1265
1267
|
],
|
|
1266
1268
|
})
|
|
@@ -1290,7 +1292,7 @@ import { WsAdapter } from '@forinda/kickjs-ws'
|
|
|
1290
1292
|
|
|
1291
1293
|
bootstrap({
|
|
1292
1294
|
modules,
|
|
1293
|
-
adapters: [
|
|
1295
|
+
adapters: [WsAdapter()],
|
|
1294
1296
|
})
|
|
1295
1297
|
\`\`\`
|
|
1296
1298
|
|
|
@@ -1394,7 +1396,7 @@ These work anywhere — scripts, plain files, outside \`@Service\`/\`@Controller
|
|
|
1394
1396
|
### HTTP Routes
|
|
1395
1397
|
| Decorator | Purpose |
|
|
1396
1398
|
|-----------|---------|
|
|
1397
|
-
| \`@Controller(
|
|
1399
|
+
| \`@Controller()\` | Define route prefix |
|
|
1398
1400
|
| \`@Get('/'), @Post('/')\` | HTTP method handlers |
|
|
1399
1401
|
| \`@Middleware(fn)\` | Attach middleware |
|
|
1400
1402
|
| \`@Public()\` | Skip auth (requires auth adapter) |
|
|
@@ -1915,6 +1917,228 @@ function pluralizePascal(name) {
|
|
|
1915
1917
|
return pkg.plural(name);
|
|
1916
1918
|
}
|
|
1917
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
|
|
1918
2142
|
//#region src/generators/templates/module-index.ts
|
|
1919
2143
|
const repoLabelMap = {
|
|
1920
2144
|
inmemory: "in-memory",
|
|
@@ -4441,7 +4665,7 @@ import type { RequestContext } from '@forinda/kickjs'
|
|
|
4441
4665
|
import { Autowired } from '@forinda/kickjs'
|
|
4442
4666
|
import { AuthService } from './auth.service'
|
|
4443
4667
|
|
|
4444
|
-
@Controller(
|
|
4668
|
+
@Controller()
|
|
4445
4669
|
@Authenticated()
|
|
4446
4670
|
export class AuthController {
|
|
4447
4671
|
@Autowired() private authService!: AuthService
|
|
@@ -4523,7 +4747,7 @@ import type { RequestContext } from '@forinda/kickjs'
|
|
|
4523
4747
|
import { Autowired } from '@forinda/kickjs'
|
|
4524
4748
|
import { AuthService } from './auth.service'
|
|
4525
4749
|
|
|
4526
|
-
@Controller(
|
|
4750
|
+
@Controller()
|
|
4527
4751
|
@Authenticated()
|
|
4528
4752
|
export class AuthController {
|
|
4529
4753
|
@Autowired() private authService!: AuthService
|
|
@@ -5330,14 +5554,14 @@ const BUILTIN_REPO_TYPES = [
|
|
|
5330
5554
|
"inmemory",
|
|
5331
5555
|
"prisma"
|
|
5332
5556
|
];
|
|
5333
|
-
/** Resolve module config
|
|
5557
|
+
/** Resolve module config from `modules.*` block. */
|
|
5334
5558
|
function resolveModuleConfig(config) {
|
|
5335
5559
|
if (!config) return {};
|
|
5336
5560
|
const mc = {
|
|
5337
|
-
dir: config.modules?.dir
|
|
5338
|
-
repo: config.modules?.repo
|
|
5339
|
-
schemaDir: config.modules?.schemaDir
|
|
5340
|
-
pluralize: config.modules?.pluralize
|
|
5561
|
+
dir: config.modules?.dir,
|
|
5562
|
+
repo: config.modules?.repo,
|
|
5563
|
+
schemaDir: config.modules?.schemaDir,
|
|
5564
|
+
pluralize: config.modules?.pluralize,
|
|
5341
5565
|
prismaClientPath: config.modules?.prismaClientPath
|
|
5342
5566
|
};
|
|
5343
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.`);
|
|
@@ -5365,7 +5589,10 @@ async function loadKickConfig(cwd) {
|
|
|
5365
5589
|
try {
|
|
5366
5590
|
const { pathToFileURL } = await import("node:url");
|
|
5367
5591
|
const mod = await import(pathToFileURL(filepath).href);
|
|
5368
|
-
|
|
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;
|
|
5369
5596
|
} catch (err) {
|
|
5370
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.`);
|
|
5371
5598
|
continue;
|
|
@@ -5373,6 +5600,59 @@ async function loadKickConfig(cwd) {
|
|
|
5373
5600
|
}
|
|
5374
5601
|
return null;
|
|
5375
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
|
+
}
|
|
5376
5656
|
//#endregion
|
|
5377
5657
|
//#region src/typegen/scanner.ts
|
|
5378
5658
|
/** Decorators that mark a class as DI-managed */
|
|
@@ -5428,6 +5708,28 @@ const BARE_CREATE_TOKEN_REGEX = /createToken\s*(?:<[^>]*>)?\s*\(\s*['"`]([^'"`]+
|
|
|
5428
5708
|
/** Match `@Inject('literal')` — only literals; computed args are skipped */
|
|
5429
5709
|
const INJECT_LITERAL_REGEX = /@Inject\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
5430
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
|
+
/**
|
|
5431
5733
|
* Locate the start of a route decorator: `@Get(`, `@Post(`, etc.
|
|
5432
5734
|
* Used by `extractRoutesFromSource`; the rest of the route declaration
|
|
5433
5735
|
* (balanced parens, stacked decorators, method name) is parsed by walking
|
|
@@ -5774,6 +6076,120 @@ function extractInjectsFromSource(source, filePath, cwd) {
|
|
|
5774
6076
|
return out;
|
|
5775
6077
|
}
|
|
5776
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
|
+
/**
|
|
5777
6193
|
* Default search order for the env schema file. Newer projects keep
|
|
5778
6194
|
* the schema under `src/config/` so the framework's "config" concept
|
|
5779
6195
|
* has a single home; older scaffolds dropped it at `src/env.ts` (kept
|
|
@@ -5844,6 +6260,8 @@ async function scanProject(opts) {
|
|
|
5844
6260
|
const routes = [];
|
|
5845
6261
|
const tokens = [];
|
|
5846
6262
|
const injects = [];
|
|
6263
|
+
const pluginsAndAdapters = [];
|
|
6264
|
+
const augmentations = [];
|
|
5847
6265
|
const sources = /* @__PURE__ */ new Map();
|
|
5848
6266
|
for (const file of files) {
|
|
5849
6267
|
let source;
|
|
@@ -5856,6 +6274,8 @@ async function scanProject(opts) {
|
|
|
5856
6274
|
classes.push(...extractClassesFromSource(source, file, opts.cwd));
|
|
5857
6275
|
tokens.push(...extractTokensFromSource(source, file, opts.cwd));
|
|
5858
6276
|
injects.push(...extractInjectsFromSource(source, file, opts.cwd));
|
|
6277
|
+
pluginsAndAdapters.push(...extractPluginsAndAdaptersFromSource(source, file, opts.cwd));
|
|
6278
|
+
augmentations.push(...extractAugmentationsFromSource(source, file, opts.cwd));
|
|
5859
6279
|
}
|
|
5860
6280
|
for (const [file, source] of sources) {
|
|
5861
6281
|
const classesInFile = classes.filter((c) => c.filePath === file);
|
|
@@ -5868,16 +6288,149 @@ async function scanProject(opts) {
|
|
|
5868
6288
|
tokens.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
5869
6289
|
injects.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
5870
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));
|
|
5871
6293
|
return {
|
|
5872
6294
|
classes,
|
|
5873
6295
|
routes,
|
|
5874
6296
|
tokens,
|
|
5875
6297
|
injects,
|
|
5876
6298
|
collisions: findCollisions(classes),
|
|
5877
|
-
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
|
|
5878
6302
|
};
|
|
5879
6303
|
}
|
|
5880
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
|
|
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;
|
|
6432
|
+
}
|
|
6433
|
+
//#endregion
|
|
5881
6434
|
//#region src/typegen/generator.ts
|
|
5882
6435
|
/**
|
|
5883
6436
|
* Generates `.d.ts` files inside `.kickjs/types/` from the discovered
|
|
@@ -6013,12 +6566,17 @@ function renderIndex(includeEnv) {
|
|
|
6013
6566
|
export type { ServiceToken } from './services'
|
|
6014
6567
|
export type { ModuleToken } from './modules'
|
|
6015
6568
|
|
|
6016
|
-
// The registry, routes, and env augmentations are
|
|
6017
|
-
// importing this file (or having it on
|
|
6018
|
-
// \`container.resolve()\`,
|
|
6019
|
-
//
|
|
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.
|
|
6020
6575
|
import './registry'
|
|
6021
6576
|
import './routes'
|
|
6577
|
+
import './plugins'
|
|
6578
|
+
import './augmentations'
|
|
6579
|
+
import './assets'
|
|
6022
6580
|
${includeEnv ? "import './env'\n" : ""}`;
|
|
6023
6581
|
}
|
|
6024
6582
|
/**
|
|
@@ -6213,9 +6771,90 @@ ${interfaces.join("\n")}
|
|
|
6213
6771
|
export {}
|
|
6214
6772
|
`;
|
|
6215
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
|
+
}
|
|
6216
6852
|
/** Write all generated `.d.ts` files to `outDir` */
|
|
6217
6853
|
async function generateTypes(opts) {
|
|
6218
|
-
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;
|
|
6219
6858
|
if (collisions.length > 0 && !allowDuplicates) throw new TokenCollisionError(collisions);
|
|
6220
6859
|
await mkdir(outDir, { recursive: true });
|
|
6221
6860
|
const registryFile = join(outDir, "registry.d.ts");
|
|
@@ -6223,6 +6862,9 @@ async function generateTypes(opts) {
|
|
|
6223
6862
|
const modulesFile = join(outDir, "modules.d.ts");
|
|
6224
6863
|
const routesFile = join(outDir, "routes.ts");
|
|
6225
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");
|
|
6226
6868
|
const indexFile = join(outDir, "index.d.ts");
|
|
6227
6869
|
const collidingNames = new Set(collisions.map((c) => c.className));
|
|
6228
6870
|
const registryContent = renderRegistry(classes, registryFile, collidingNames);
|
|
@@ -6239,17 +6881,26 @@ async function generateTypes(opts) {
|
|
|
6239
6881
|
const modulesContent = renderUnion("ModuleToken", modules, "(no @Module classes discovered — `kick g module <name>` to add one)");
|
|
6240
6882
|
const routesContent = renderRoutes(routes, routesFile, schemaValidator);
|
|
6241
6883
|
const envContent = renderEnv(env, envFile);
|
|
6884
|
+
const pluginsContent = renderPlugins(pluginsAndAdapters);
|
|
6885
|
+
const augmentationsContent = renderAugmentations(augmentations);
|
|
6886
|
+
const assetsContent = renderAssetTypes(assets);
|
|
6242
6887
|
const indexContent = renderIndex(envContent !== null);
|
|
6243
6888
|
await writeFile(registryFile, registryContent, "utf-8");
|
|
6244
6889
|
await writeFile(servicesFile, servicesContent, "utf-8");
|
|
6245
6890
|
await writeFile(modulesFile, modulesContent, "utf-8");
|
|
6246
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");
|
|
6247
6895
|
await writeFile(indexFile, indexContent, "utf-8");
|
|
6248
6896
|
const written = [
|
|
6249
6897
|
registryFile,
|
|
6250
6898
|
servicesFile,
|
|
6251
6899
|
modulesFile,
|
|
6252
6900
|
routesFile,
|
|
6901
|
+
pluginsFile,
|
|
6902
|
+
augmentationsFile,
|
|
6903
|
+
assetsFile,
|
|
6253
6904
|
indexFile
|
|
6254
6905
|
];
|
|
6255
6906
|
if (envContent) {
|
|
@@ -6257,17 +6908,62 @@ async function generateTypes(opts) {
|
|
|
6257
6908
|
written.push(envFile);
|
|
6258
6909
|
}
|
|
6259
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;
|
|
6260
6913
|
return {
|
|
6261
6914
|
registryEntries: classTokens.length,
|
|
6262
6915
|
serviceTokens: new Set(allServices).size,
|
|
6263
6916
|
moduleTokens: modules.length,
|
|
6264
6917
|
routeEntries: routes.length,
|
|
6918
|
+
pluginEntries: uniquePluginNames,
|
|
6919
|
+
augmentationEntries: uniqueAugmentations,
|
|
6920
|
+
assetEntries: assets.count,
|
|
6265
6921
|
envWritten: envContent !== null,
|
|
6266
6922
|
written,
|
|
6267
6923
|
resolvedCollisions: collisions.length
|
|
6268
6924
|
};
|
|
6269
6925
|
}
|
|
6270
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
|
|
6271
6967
|
//#region src/typegen/index.ts
|
|
6272
6968
|
/**
|
|
6273
6969
|
* Public entry point for the KickJS typegen module.
|
|
@@ -6312,6 +7008,7 @@ async function runTypegen(opts = {}) {
|
|
|
6312
7008
|
cwd,
|
|
6313
7009
|
envFile: envFile === false ? void 0 : envFile
|
|
6314
7010
|
});
|
|
7011
|
+
const assets = discoverAssets(opts.assetMap, cwd);
|
|
6315
7012
|
const result = await generateTypes({
|
|
6316
7013
|
classes: scan.classes,
|
|
6317
7014
|
routes: scan.routes,
|
|
@@ -6319,20 +7016,36 @@ async function runTypegen(opts = {}) {
|
|
|
6319
7016
|
injects: scan.injects,
|
|
6320
7017
|
collisions: scan.collisions,
|
|
6321
7018
|
env: envFile === false ? null : scan.env,
|
|
7019
|
+
pluginsAndAdapters: scan.pluginsAndAdapters,
|
|
7020
|
+
augmentations: scan.augmentations,
|
|
7021
|
+
assets,
|
|
6322
7022
|
outDir,
|
|
6323
7023
|
allowDuplicates,
|
|
6324
7024
|
schemaValidator
|
|
6325
7025
|
});
|
|
7026
|
+
const tokenWarnings = validateTokenConventions(scan.tokens);
|
|
6326
7027
|
const elapsed = Date.now() - start;
|
|
6327
7028
|
if (!silent) {
|
|
6328
7029
|
const where = outDir.replace(cwd + "/", "");
|
|
6329
7030
|
const collisionNote = result.resolvedCollisions > 0 ? `, ${result.resolvedCollisions} collisions namespaced` : "";
|
|
6330
7031
|
const envNote = result.envWritten ? ", env typed" : "";
|
|
6331
|
-
|
|
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
|
+
}
|
|
6332
7044
|
}
|
|
6333
7045
|
return {
|
|
6334
7046
|
scan,
|
|
6335
|
-
result
|
|
7047
|
+
result,
|
|
7048
|
+
tokenWarnings
|
|
6336
7049
|
};
|
|
6337
7050
|
}
|
|
6338
7051
|
/**
|
|
@@ -6489,10 +7202,23 @@ const GENERATORS = [
|
|
|
6489
7202
|
description: "Generate kick.config.ts"
|
|
6490
7203
|
}
|
|
6491
7204
|
];
|
|
6492
|
-
function printGeneratorList() {
|
|
6493
|
-
console.log("\n
|
|
7205
|
+
async function printGeneratorList() {
|
|
7206
|
+
console.log("\n Built-in generators:\n");
|
|
6494
7207
|
const maxName = Math.max(...GENERATORS.map((g) => g.name.length));
|
|
6495
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
|
+
}
|
|
6496
7222
|
console.log();
|
|
6497
7223
|
}
|
|
6498
7224
|
/**
|
|
@@ -6529,7 +7255,7 @@ async function runModuleGeneration(names, opts, dryRun) {
|
|
|
6529
7255
|
function registerGenerateCommand(program) {
|
|
6530
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) => {
|
|
6531
7257
|
if (opts.list) {
|
|
6532
|
-
printGeneratorList();
|
|
7258
|
+
await printGeneratorList();
|
|
6533
7259
|
return;
|
|
6534
7260
|
}
|
|
6535
7261
|
if (!names || names.length === 0) {
|
|
@@ -6538,6 +7264,20 @@ function registerGenerateCommand(program) {
|
|
|
6538
7264
|
}
|
|
6539
7265
|
const dryRun = isDryRun(cmd);
|
|
6540
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
|
+
}
|
|
6541
7281
|
await runModuleGeneration(names, opts, dryRun);
|
|
6542
7282
|
});
|
|
6543
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) => {
|
|
@@ -6767,6 +7507,132 @@ function runNodeWithEnv(entry, env, cwd) {
|
|
|
6767
7507
|
});
|
|
6768
7508
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
6769
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
|
+
}
|
|
6770
7636
|
//#endregion
|
|
6771
7637
|
//#region src/commands/run.ts
|
|
6772
7638
|
/**
|
|
@@ -6795,7 +7661,8 @@ async function startDevServer(_entry, port) {
|
|
|
6795
7661
|
schemaValidator,
|
|
6796
7662
|
envFile,
|
|
6797
7663
|
srcDir: devConfig?.typegen?.srcDir,
|
|
6798
|
-
outDir: devConfig?.typegen?.outDir
|
|
7664
|
+
outDir: devConfig?.typegen?.outDir,
|
|
7665
|
+
assetMap: devConfig?.assetMap
|
|
6799
7666
|
});
|
|
6800
7667
|
} catch (err) {
|
|
6801
7668
|
console.warn(` kick typegen: skipped (${err?.message ?? err})`);
|
|
@@ -6820,7 +7687,8 @@ async function startDevServer(_entry, port) {
|
|
|
6820
7687
|
schemaValidator,
|
|
6821
7688
|
envFile,
|
|
6822
7689
|
srcDir: devConfig?.typegen?.srcDir,
|
|
6823
|
-
outDir: devConfig?.typegen?.outDir
|
|
7690
|
+
outDir: devConfig?.typegen?.outDir,
|
|
7691
|
+
assetMap: devConfig?.assetMap
|
|
6824
7692
|
}).catch(() => {});
|
|
6825
7693
|
}, 100);
|
|
6826
7694
|
};
|
|
@@ -6853,7 +7721,8 @@ function registerRunCommands(program) {
|
|
|
6853
7721
|
const { createRequire } = await import("node:module");
|
|
6854
7722
|
const { build } = await import(pathToFileURL(createRequire(resolve("package.json")).resolve("vite")).href);
|
|
6855
7723
|
await build({ configFile: resolve("vite.config.ts") });
|
|
6856
|
-
const
|
|
7724
|
+
const config = await loadKickConfig(process.cwd());
|
|
7725
|
+
const copyDirs = config?.copyDirs ?? [];
|
|
6857
7726
|
if (copyDirs.length > 0) {
|
|
6858
7727
|
console.log("\n Copying directories to dist...");
|
|
6859
7728
|
for (const entry of copyDirs) {
|
|
@@ -6870,8 +7739,32 @@ function registerRunCommands(program) {
|
|
|
6870
7739
|
console.log(` ✓ ${src} → ${dest}`);
|
|
6871
7740
|
}
|
|
6872
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
|
+
}
|
|
6873
7751
|
console.log("\n Build complete.\n");
|
|
6874
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
|
+
});
|
|
6875
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) => {
|
|
6876
7769
|
const env = { NODE_ENV: "production" };
|
|
6877
7770
|
if (opts.port) env.PORT = String(opts.port);
|
|
@@ -6903,7 +7796,6 @@ function registerInfoCommand(program) {
|
|
|
6903
7796
|
Packages:
|
|
6904
7797
|
@forinda/kickjs workspace
|
|
6905
7798
|
@forinda/kickjs-vite workspace
|
|
6906
|
-
@forinda/kickjs-config workspace
|
|
6907
7799
|
@forinda/kickjs-cli workspace
|
|
6908
7800
|
`);
|
|
6909
7801
|
});
|
|
@@ -8141,7 +9033,8 @@ function registerTypegenCommand(program) {
|
|
|
8141
9033
|
silent: opts.silent,
|
|
8142
9034
|
allowDuplicates: opts.allowDuplicates,
|
|
8143
9035
|
schemaValidator,
|
|
8144
|
-
envFile
|
|
9036
|
+
envFile,
|
|
9037
|
+
assetMap: config?.assetMap
|
|
8145
9038
|
};
|
|
8146
9039
|
try {
|
|
8147
9040
|
if (opts.watch) {
|