@forinda/kickjs-cli 5.1.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{builtins-caRjFvKz.mjs → builtins-B0dptoXq.mjs} +626 -241
- package/dist/builtins-B0dptoXq.mjs.map +1 -0
- package/dist/{builtins-Cb_d-b1S.mjs → builtins-N3mDa6bM.mjs} +695 -273
- package/dist/cli.mjs +9 -9
- package/dist/{config-8bAt-mLl.mjs → config-Bc6ERRTE.mjs} +35 -5
- package/dist/{config-C_LQNClP.mjs → config-CRi3zCxk.mjs} +36 -6
- package/dist/config-CRi3zCxk.mjs.map +1 -0
- package/dist/{generator-extension-CYY-RI21.mjs → generator-extension-C-HwKvFf.mjs} +76 -39
- package/dist/generator-extension-C-HwKvFf.mjs.map +1 -0
- package/dist/index.d.mts +282 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{plugin-D8K5fG-O.mjs → plugin-DfomEcef.mjs} +4 -4
- package/dist/{plugin-D8K5fG-O.mjs.map → plugin-DfomEcef.mjs.map} +1 -1
- package/dist/{plugin-Bgfg7qMk.mjs → plugin-b7ig7Uxv.mjs} +2 -2
- package/dist/{rolldown-runtime-BM29JyaJ.mjs → rolldown-runtime-CV_zlh2d.mjs} +1 -1
- package/dist/{run-plugins-BXvMFPhJ.mjs → run-plugins-D9abb5Nx.mjs} +2 -2
- package/dist/{typegen-BNz_RQTb.mjs → typegen-B9S81bOx.mjs} +48 -225
- package/dist/typegen-B9S81bOx.mjs.map +1 -0
- package/dist/{typegen-8ZeA1B-X.mjs → typegen-BKUAdp_3.mjs} +47 -228
- package/dist/{types-BBUo1vXh.mjs → types-CU89yUxU.mjs} +2 -2
- package/dist/{types-BBUo1vXh.mjs.map → types-CU89yUxU.mjs.map} +1 -1
- package/package.json +6 -4
- package/dist/builtins-caRjFvKz.mjs.map +0 -1
- package/dist/config-C_LQNClP.mjs.map +0 -1
- package/dist/generator-extension-CYY-RI21.mjs.map +0 -1
- package/dist/typegen-8ZeA1B-X.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli v5.1
|
|
2
|
+
* @forinda/kickjs-cli v5.2.1
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
12
|
-
import { i as resolveModuleConfig, r as loadKickConfig, t as PACKAGE_MANAGERS } from "./config-
|
|
13
|
-
import { n as mergeCliPlugins, r as defineCliPlugin } from "./plugin-
|
|
14
|
-
import { a as discoverAssets, i as TokenCollisionError, o as renderAssetTypes, r as watchTypegen, t as runTypegen$1 } from "./typegen-
|
|
11
|
+
import { t as __exportAll } from "./rolldown-runtime-CV_zlh2d.mjs";
|
|
12
|
+
import { a as resolveTokenScope, i as resolveModuleConfig, r as loadKickConfig, t as PACKAGE_MANAGERS } from "./config-Bc6ERRTE.mjs";
|
|
13
|
+
import { n as mergeCliPlugins, r as defineCliPlugin } from "./plugin-b7ig7Uxv.mjs";
|
|
14
|
+
import { a as discoverAssets, i as TokenCollisionError, o as renderAssetTypes, r as watchTypegen, s as scanProject, t as runTypegen$1 } from "./typegen-BKUAdp_3.mjs";
|
|
15
15
|
import { createRequire } from "node:module";
|
|
16
16
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
-
import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
17
|
+
import path, { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
19
|
import { execSync, fork, spawn, spawnSync } from "node:child_process";
|
|
20
20
|
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -22,6 +22,7 @@ import * as clack from "@clack/prompts";
|
|
|
22
22
|
import pc from "picocolors";
|
|
23
23
|
import pkg from "pluralize";
|
|
24
24
|
import { glob } from "glob";
|
|
25
|
+
import { groupAssetKeys } from "@forinda/kickjs";
|
|
25
26
|
import { arch, platform, release } from "node:os";
|
|
26
27
|
import { generate, migrateDown, migrateLatest, migrateRollback, migrateStatus, migrateUp, renderSchemaSource, resolveDbConfig } from "@forinda/kickjs-db";
|
|
27
28
|
//#region src/utils/shell.ts
|
|
@@ -69,7 +70,7 @@ let _dryRun = false;
|
|
|
69
70
|
function setDryRun(enabled) {
|
|
70
71
|
_dryRun = enabled;
|
|
71
72
|
}
|
|
72
|
-
/** Extensions
|
|
73
|
+
/** Extensions oxfmt can format. Anything else is written verbatim. */
|
|
73
74
|
const FORMATTABLE = new Set([
|
|
74
75
|
".ts",
|
|
75
76
|
".tsx",
|
|
@@ -83,15 +84,14 @@ const FORMATTABLE = new Set([
|
|
|
83
84
|
/**
|
|
84
85
|
* Write a file, creating parent directories if needed.
|
|
85
86
|
*
|
|
86
|
-
* After write, runs
|
|
87
|
+
* After write, runs oxfmt against the file when:
|
|
87
88
|
* - format-on-write is enabled (default)
|
|
88
89
|
* - the extension is in {@link FORMATTABLE}
|
|
89
|
-
* -
|
|
90
|
+
* - oxfmt resolves from the user's project (or our own cwd)
|
|
90
91
|
*
|
|
91
|
-
* Failures (missing
|
|
92
|
+
* Failures (missing oxfmt, unparseable source, formatter crash) are
|
|
92
93
|
* swallowed silently — formatting is a polish step, not a correctness
|
|
93
|
-
* gate. The pre-
|
|
94
|
-
* couldn't format.
|
|
94
|
+
* gate. The pre-commit hook still catches anything we couldn't format.
|
|
95
95
|
*
|
|
96
96
|
* Skips writing entirely in dry run mode.
|
|
97
97
|
*/
|
|
@@ -101,28 +101,58 @@ async function writeFileSafe(filePath, content) {
|
|
|
101
101
|
await writeFile(filePath, content, "utf-8");
|
|
102
102
|
if (FORMATTABLE.has(extname(filePath))) await formatFile(filePath, content).catch(() => {});
|
|
103
103
|
}
|
|
104
|
-
let
|
|
105
|
-
/** Resolve
|
|
106
|
-
function
|
|
107
|
-
if (
|
|
104
|
+
let _oxfmt = void 0;
|
|
105
|
+
/** Resolve oxfmt from the user's project; cache the result (or null) for the process. */
|
|
106
|
+
async function resolveOxfmt(cwd) {
|
|
107
|
+
if (_oxfmt !== void 0) return _oxfmt;
|
|
108
108
|
try {
|
|
109
|
-
|
|
109
|
+
_oxfmt = await import(createRequire(join(cwd, "package.json")).resolve("oxfmt"));
|
|
110
110
|
} catch {
|
|
111
|
-
|
|
111
|
+
_oxfmt = null;
|
|
112
112
|
}
|
|
113
|
-
return
|
|
113
|
+
return _oxfmt;
|
|
114
114
|
}
|
|
115
115
|
async function formatFile(filePath, content) {
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
const oxfmt = await resolveOxfmt(process.cwd());
|
|
117
|
+
if (!oxfmt) return;
|
|
118
|
+
const options = await loadOxfmtConfig(filePath);
|
|
119
|
+
if (options === null) return;
|
|
120
|
+
const result = await oxfmt.format(filePath, content, options);
|
|
121
|
+
if (result.code === content) return;
|
|
122
|
+
await writeFile(filePath, result.code, "utf-8");
|
|
123
|
+
}
|
|
124
|
+
const _oxfmtConfigCache = /* @__PURE__ */ new Map();
|
|
125
|
+
/**
|
|
126
|
+
* Walk up from `filePath`'s directory looking for `.oxfmtrc.json`.
|
|
127
|
+
* Returns `null` when no config is found anywhere on the path —
|
|
128
|
+
* generators then leave the raw template alone (which already
|
|
129
|
+
* follows project conventions). Cached per starting directory so
|
|
130
|
+
* the walk is one-shot per generator run.
|
|
131
|
+
*/
|
|
132
|
+
async function loadOxfmtConfig(filePath) {
|
|
133
|
+
let dir = dirname(filePath);
|
|
134
|
+
const startDir = dir;
|
|
135
|
+
if (_oxfmtConfigCache.has(startDir)) return _oxfmtConfigCache.get(startDir);
|
|
136
|
+
while (true) {
|
|
137
|
+
const configPath = join(dir, ".oxfmtrc.json");
|
|
138
|
+
if (existsSync(configPath)) try {
|
|
139
|
+
const raw = await readFile(configPath, "utf-8");
|
|
140
|
+
const parsed = JSON.parse(raw);
|
|
141
|
+
delete parsed["$schema"];
|
|
142
|
+
delete parsed.ignorePatterns;
|
|
143
|
+
_oxfmtConfigCache.set(startDir, parsed);
|
|
144
|
+
return parsed;
|
|
145
|
+
} catch {
|
|
146
|
+
_oxfmtConfigCache.set(startDir, null);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const parent = dirname(dir);
|
|
150
|
+
if (parent === dir) {
|
|
151
|
+
_oxfmtConfigCache.set(startDir, null);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
dir = parent;
|
|
155
|
+
}
|
|
126
156
|
}
|
|
127
157
|
/** Check if a file exists */
|
|
128
158
|
async function fileExists(filePath) {
|
|
@@ -1606,7 +1636,7 @@ async function initProject(options) {
|
|
|
1606
1636
|
}
|
|
1607
1637
|
}
|
|
1608
1638
|
try {
|
|
1609
|
-
const { runTypegen } = await import("./typegen-
|
|
1639
|
+
const { runTypegen } = await import("./typegen-BKUAdp_3.mjs").then((n) => n.n);
|
|
1610
1640
|
await runTypegen({
|
|
1611
1641
|
cwd: dir,
|
|
1612
1642
|
allowDuplicates: true,
|
|
@@ -1743,6 +1773,278 @@ function spinner() {
|
|
|
1743
1773
|
/** Log utilities for styled messages inside clack flow */
|
|
1744
1774
|
const log = clack.log;
|
|
1745
1775
|
//#endregion
|
|
1776
|
+
//#region src/commands/add.ts
|
|
1777
|
+
/** Registry of KickJS packages and their required peer dependencies */
|
|
1778
|
+
const PACKAGE_REGISTRY = {
|
|
1779
|
+
kickjs: {
|
|
1780
|
+
pkg: "@forinda/kickjs",
|
|
1781
|
+
peers: ["express"],
|
|
1782
|
+
description: "Unified framework: DI, decorators, routing, middleware",
|
|
1783
|
+
core: true
|
|
1784
|
+
},
|
|
1785
|
+
vite: {
|
|
1786
|
+
pkg: "@forinda/kickjs-vite",
|
|
1787
|
+
peers: ["vite"],
|
|
1788
|
+
description: "Vite plugin: dev server, HMR, module discovery",
|
|
1789
|
+
dev: true,
|
|
1790
|
+
core: true
|
|
1791
|
+
},
|
|
1792
|
+
cli: {
|
|
1793
|
+
pkg: "@forinda/kickjs-cli",
|
|
1794
|
+
peers: [],
|
|
1795
|
+
description: "CLI tool and code generators",
|
|
1796
|
+
dev: true,
|
|
1797
|
+
core: true
|
|
1798
|
+
},
|
|
1799
|
+
swagger: {
|
|
1800
|
+
pkg: "@forinda/kickjs-swagger",
|
|
1801
|
+
peers: [],
|
|
1802
|
+
description: "OpenAPI spec + Swagger UI + ReDoc"
|
|
1803
|
+
},
|
|
1804
|
+
db: {
|
|
1805
|
+
pkg: "@forinda/kickjs-db",
|
|
1806
|
+
peers: [],
|
|
1807
|
+
description: "kick/db core — schema DSL, migrations, KickDbClient, customType"
|
|
1808
|
+
},
|
|
1809
|
+
"db-pg": {
|
|
1810
|
+
pkg: "@forinda/kickjs-db-pg",
|
|
1811
|
+
peers: ["pg"],
|
|
1812
|
+
description: "kick/db PostgreSQL dialect + adapter (pgDialect, pgAdapter)"
|
|
1813
|
+
},
|
|
1814
|
+
drizzle: {
|
|
1815
|
+
pkg: "@forinda/kickjs-drizzle",
|
|
1816
|
+
peers: ["drizzle-orm"],
|
|
1817
|
+
description: "Drizzle ORM adapter + query builder"
|
|
1818
|
+
},
|
|
1819
|
+
prisma: {
|
|
1820
|
+
pkg: "@forinda/kickjs-prisma",
|
|
1821
|
+
peers: ["@prisma/client"],
|
|
1822
|
+
description: "Prisma adapter + query builder"
|
|
1823
|
+
},
|
|
1824
|
+
ws: {
|
|
1825
|
+
pkg: "@forinda/kickjs-ws",
|
|
1826
|
+
peers: ["socket.io"],
|
|
1827
|
+
description: "WebSocket with @WsController decorators"
|
|
1828
|
+
},
|
|
1829
|
+
devtools: {
|
|
1830
|
+
pkg: "@forinda/kickjs-devtools",
|
|
1831
|
+
peers: [],
|
|
1832
|
+
description: "Development dashboard — routes, DI, metrics, health",
|
|
1833
|
+
dev: true
|
|
1834
|
+
},
|
|
1835
|
+
auth: {
|
|
1836
|
+
pkg: "@forinda/kickjs-auth",
|
|
1837
|
+
peers: ["jsonwebtoken"],
|
|
1838
|
+
description: "Authentication — JWT, API key, and custom strategies"
|
|
1839
|
+
},
|
|
1840
|
+
queue: {
|
|
1841
|
+
pkg: "@forinda/kickjs-queue",
|
|
1842
|
+
peers: [],
|
|
1843
|
+
description: "Queue adapter (BullMQ/RabbitMQ/Kafka)"
|
|
1844
|
+
},
|
|
1845
|
+
"queue:bullmq": {
|
|
1846
|
+
pkg: "@forinda/kickjs-queue",
|
|
1847
|
+
peers: ["bullmq", "ioredis"],
|
|
1848
|
+
description: "Queue with BullMQ + Redis"
|
|
1849
|
+
},
|
|
1850
|
+
"queue:rabbitmq": {
|
|
1851
|
+
pkg: "@forinda/kickjs-queue",
|
|
1852
|
+
peers: ["amqplib"],
|
|
1853
|
+
description: "Queue with RabbitMQ"
|
|
1854
|
+
},
|
|
1855
|
+
"queue:kafka": {
|
|
1856
|
+
pkg: "@forinda/kickjs-queue",
|
|
1857
|
+
peers: ["kafkajs"],
|
|
1858
|
+
description: "Queue with Kafka"
|
|
1859
|
+
},
|
|
1860
|
+
mcp: {
|
|
1861
|
+
pkg: "@forinda/kickjs-mcp",
|
|
1862
|
+
peers: ["@modelcontextprotocol/sdk"],
|
|
1863
|
+
description: "Model Context Protocol server — expose @Controller endpoints as AI tools"
|
|
1864
|
+
},
|
|
1865
|
+
testing: {
|
|
1866
|
+
pkg: "@forinda/kickjs-testing",
|
|
1867
|
+
peers: [],
|
|
1868
|
+
description: "Test utilities and TestModule builder",
|
|
1869
|
+
dev: true
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
/**
|
|
1873
|
+
* Walk up from `fromDir` to filesystem root, returning the first
|
|
1874
|
+
* directory that contains `name`. Lets monorepo sub-packages pick up
|
|
1875
|
+
* lockfiles and `packageManager` fields living at the workspace root.
|
|
1876
|
+
*/
|
|
1877
|
+
function findUp(name, fromDir = process.cwd()) {
|
|
1878
|
+
let current = fromDir;
|
|
1879
|
+
while (true) {
|
|
1880
|
+
if (existsSync(resolve(current, name))) return current;
|
|
1881
|
+
const parent = dirname(current);
|
|
1882
|
+
if (parent === current) return null;
|
|
1883
|
+
current = parent;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
function detectFromLockfile() {
|
|
1887
|
+
if (findUp("pnpm-lock.yaml")) return "pnpm";
|
|
1888
|
+
if (findUp("yarn.lock")) return "yarn";
|
|
1889
|
+
if (findUp("bun.lockb") || findUp("bun.lock")) return "bun";
|
|
1890
|
+
if (findUp("package-lock.json")) return "npm";
|
|
1891
|
+
return null;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Read `packageManager` from the nearest ancestor `package.json` that
|
|
1895
|
+
* declares the field (corepack convention: `"pnpm@10.0.0"`). Climbs so
|
|
1896
|
+
* monorepo sub-packages inherit the workspace pm even when their own
|
|
1897
|
+
* package.json omits the field.
|
|
1898
|
+
*/
|
|
1899
|
+
function packageManagerFromPackageJson() {
|
|
1900
|
+
let dir = process.cwd();
|
|
1901
|
+
while (dir) {
|
|
1902
|
+
const pkgPath = resolve(dir, "package.json");
|
|
1903
|
+
if (existsSync(pkgPath)) try {
|
|
1904
|
+
const field = JSON.parse(readFileSync(pkgPath, "utf-8")).packageManager;
|
|
1905
|
+
if (typeof field === "string") {
|
|
1906
|
+
const name = field.split("@")[0];
|
|
1907
|
+
if (PACKAGE_MANAGERS.includes(name)) return name;
|
|
1908
|
+
}
|
|
1909
|
+
} catch {}
|
|
1910
|
+
const parent = dirname(dir);
|
|
1911
|
+
if (parent === dir) return null;
|
|
1912
|
+
dir = parent;
|
|
1913
|
+
}
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Resolve which package manager to use, in priority order:
|
|
1918
|
+
* 1. `--pm` CLI flag
|
|
1919
|
+
* 2. `packageManager` in kick.config
|
|
1920
|
+
* 3. `packageManager` in nearest ancestor package.json (corepack)
|
|
1921
|
+
* 4. Nearest ancestor lockfile (pnpm-lock.yaml → yarn.lock → bun.lock → package-lock.json)
|
|
1922
|
+
* 5. `'npm'` fallback
|
|
1923
|
+
*
|
|
1924
|
+
* Returns the chosen pm plus the source for callers that want to log
|
|
1925
|
+
* the resolution path.
|
|
1926
|
+
*/
|
|
1927
|
+
async function resolvePackageManagerWithSource(flagPm) {
|
|
1928
|
+
if (flagPm && PACKAGE_MANAGERS.includes(flagPm)) return {
|
|
1929
|
+
pm: flagPm,
|
|
1930
|
+
source: "flag"
|
|
1931
|
+
};
|
|
1932
|
+
const config = await loadKickConfig(process.cwd());
|
|
1933
|
+
if (config?.packageManager && PACKAGE_MANAGERS.includes(config.packageManager)) return {
|
|
1934
|
+
pm: config.packageManager,
|
|
1935
|
+
source: "config"
|
|
1936
|
+
};
|
|
1937
|
+
const fromPkg = packageManagerFromPackageJson();
|
|
1938
|
+
if (fromPkg) return {
|
|
1939
|
+
pm: fromPkg,
|
|
1940
|
+
source: "package.json"
|
|
1941
|
+
};
|
|
1942
|
+
const fromLock = detectFromLockfile();
|
|
1943
|
+
if (fromLock) return {
|
|
1944
|
+
pm: fromLock,
|
|
1945
|
+
source: "lockfile"
|
|
1946
|
+
};
|
|
1947
|
+
return {
|
|
1948
|
+
pm: "npm",
|
|
1949
|
+
source: "default"
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
/** Convenience wrapper for callers that don't care about the source. */
|
|
1953
|
+
async function resolvePackageManager(flagPm) {
|
|
1954
|
+
const { pm } = await resolvePackageManagerWithSource(flagPm);
|
|
1955
|
+
return pm;
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Print the package catalog. By default shows just the three core
|
|
1959
|
+
* packages every project always has — the optional list churns
|
|
1960
|
+
* (packages added, deprecated, removed) and a long enumeration in CLI
|
|
1961
|
+
* output / docs goes stale within a release. Pass `all = true` to dump
|
|
1962
|
+
* everything; that's what `kick add --list --all` triggers when an
|
|
1963
|
+
* adopter genuinely wants the live catalog.
|
|
1964
|
+
*/
|
|
1965
|
+
function printPackageList(all = false) {
|
|
1966
|
+
const entries = Object.entries(PACKAGE_REGISTRY);
|
|
1967
|
+
const maxName = Math.max(...entries.map(([k]) => k.length));
|
|
1968
|
+
const core = entries.filter(([, info]) => info.core);
|
|
1969
|
+
const optional = entries.filter(([, info]) => !info.core);
|
|
1970
|
+
const formatRow = ([name, info]) => {
|
|
1971
|
+
const padded = name.padEnd(maxName + 2);
|
|
1972
|
+
const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
|
|
1973
|
+
return ` ${padded} ${info.description}${peers}`;
|
|
1974
|
+
};
|
|
1975
|
+
console.log("\n Core packages (always installed by `kick new`):\n");
|
|
1976
|
+
for (const row of core) console.log(formatRow(row));
|
|
1977
|
+
if (all) {
|
|
1978
|
+
console.log("\n Optional packages (add as needed):\n");
|
|
1979
|
+
for (const row of optional) console.log(formatRow(row));
|
|
1980
|
+
} else {
|
|
1981
|
+
console.log(`\n Plus ${optional.length} optional packages (auth, swagger, db, queue, …).`);
|
|
1982
|
+
console.log(" Run `kick add --list --all` for the full catalog.");
|
|
1983
|
+
}
|
|
1984
|
+
console.log("\n Usage: kick add auth drizzle swagger");
|
|
1985
|
+
console.log(" kick add queue:bullmq");
|
|
1986
|
+
console.log();
|
|
1987
|
+
}
|
|
1988
|
+
function registerListCommand(program) {
|
|
1989
|
+
program.command("list").alias("ls").description("List KickJS packages (core only; pair with --all for the full catalog)").option("--all", "Include the full optional catalog").action((opts) => {
|
|
1990
|
+
printPackageList(Boolean(opts.all));
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
function registerAddCommand(program) {
|
|
1994
|
+
program.command("add [packages...]").description("Add KickJS packages with their required dependencies").option("--pm <manager>", "Package manager override").option("-D, --dev", "Install as dev dependency").option("--list", "List packages (core only by default; pair with --all)").option("--all", "When listing, include the full optional catalog").action(async (packages, opts) => {
|
|
1995
|
+
if (opts.list || packages.length === 0) {
|
|
1996
|
+
printPackageList(Boolean(opts.all));
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const { pm, source } = await resolvePackageManagerWithSource(opts.pm);
|
|
2000
|
+
console.log(`\n Using ${pm} (resolved from ${source})`);
|
|
2001
|
+
const forceDevFlag = opts.dev;
|
|
2002
|
+
const prodDeps = /* @__PURE__ */ new Set();
|
|
2003
|
+
const devDeps = /* @__PURE__ */ new Set();
|
|
2004
|
+
const unknown = [];
|
|
2005
|
+
for (const name of packages) {
|
|
2006
|
+
const entry = PACKAGE_REGISTRY[name];
|
|
2007
|
+
if (!entry) {
|
|
2008
|
+
unknown.push(name);
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
const target = forceDevFlag || entry.dev ? devDeps : prodDeps;
|
|
2012
|
+
target.add(entry.pkg);
|
|
2013
|
+
for (const peer of entry.peers) target.add(peer);
|
|
2014
|
+
}
|
|
2015
|
+
if (unknown.length > 0) {
|
|
2016
|
+
console.log(`\n Unknown packages: ${unknown.join(", ")}`);
|
|
2017
|
+
console.log(" Run \"kick add --list\" to see available packages.\n");
|
|
2018
|
+
if (prodDeps.size === 0 && devDeps.size === 0) return;
|
|
2019
|
+
}
|
|
2020
|
+
if (prodDeps.size > 0) {
|
|
2021
|
+
const deps = Array.from(prodDeps);
|
|
2022
|
+
const cmd = `${pm} add ${deps.join(" ")}`;
|
|
2023
|
+
console.log(`\n Installing ${deps.length} dependency(ies):`);
|
|
2024
|
+
for (const dep of deps) console.log(` + ${dep}`);
|
|
2025
|
+
console.log();
|
|
2026
|
+
try {
|
|
2027
|
+
execSync(cmd, { stdio: "inherit" });
|
|
2028
|
+
} catch {
|
|
2029
|
+
console.log(`\n Installation failed. Run manually:\n ${cmd}\n`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
if (devDeps.size > 0) {
|
|
2033
|
+
const deps = Array.from(devDeps);
|
|
2034
|
+
const cmd = `${pm} add -D ${deps.join(" ")}`;
|
|
2035
|
+
console.log(`\n Installing ${deps.length} dev dependency(ies):`);
|
|
2036
|
+
for (const dep of deps) console.log(` + ${dep} (dev)`);
|
|
2037
|
+
console.log();
|
|
2038
|
+
try {
|
|
2039
|
+
execSync(cmd, { stdio: "inherit" });
|
|
2040
|
+
} catch {
|
|
2041
|
+
console.log(`\n Installation failed. Run manually:\n ${cmd}\n`);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
console.log(" Done!\n");
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
//#endregion
|
|
1746
2048
|
//#region src/commands/init.ts
|
|
1747
2049
|
/** All optional packages available for selection */
|
|
1748
2050
|
const OPTIONAL_PACKAGES = [
|
|
@@ -1773,9 +2075,11 @@ const OPTIONAL_PACKAGES = [
|
|
|
1773
2075
|
}
|
|
1774
2076
|
];
|
|
1775
2077
|
function registerInitCommand(program) {
|
|
1776
|
-
program.command("new [name]").alias("init").description("Create a new KickJS project (use \".\" for current directory)").option("-d, --directory <dir>", "Target directory (defaults to project name)").option("--pm <manager>", "Package manager: pnpm | npm | yarn | bun").option("--git", "Initialize git repository").option("--no-git", "Skip git initialization").option("--install", "Install dependencies after scaffolding").option("--no-install", "Skip dependency installation").option("-f, --force", "Remove existing files without prompting").option("-t, --template <type>", "Project template: rest | ddd | cqrs | minimal").option("-r, --repo <type>", "Default repository: prisma | drizzle | inmemory | custom").option("--packages <packages>", "Comma-separated packages to include (e.g. auth,swagger,ws,queue)").action(async (name, opts) => {
|
|
2078
|
+
program.command("new [name]").alias("init").description("Create a new KickJS project (use \".\" for current directory)").option("-d, --directory <dir>", "Target directory (defaults to project name)").option("--pm <manager>", "Package manager: pnpm | npm | yarn | bun").option("--git", "Initialize git repository").option("--no-git", "Skip git initialization").option("--install", "Install dependencies after scaffolding").option("--no-install", "Skip dependency installation").option("-f, --force", "Remove existing files without prompting").option("-t, --template <type>", "Project template: rest | ddd | cqrs | minimal").option("-r, --repo <type>", "Default repository: prisma | drizzle | inmemory | custom").option("--packages <packages>", "Comma-separated packages to include (e.g. auth,swagger,ws,queue)").option("-y, --yes", "Pick safe defaults for every prompt (template=minimal, repo=inmemory, no extras, git+install on)").option("--non-interactive", "alias for --yes").action(async (name, opts) => {
|
|
1777
2079
|
intro("KickJS — Create a new project");
|
|
1778
|
-
|
|
2080
|
+
const yes = Boolean(opts.yes || opts.nonInteractive);
|
|
2081
|
+
if (!name) if (yes) name = "my-api";
|
|
2082
|
+
else name = await text({
|
|
1779
2083
|
message: "Project name",
|
|
1780
2084
|
placeholder: "my-api",
|
|
1781
2085
|
defaultValue: "my-api"
|
|
@@ -1789,7 +2093,11 @@ function registerInitCommand(program) {
|
|
|
1789
2093
|
const entries = readdirSync(directory);
|
|
1790
2094
|
if (entries.length > 0) {
|
|
1791
2095
|
if (opts.force) log.warn(`Clearing existing files in ${directory}`);
|
|
1792
|
-
else {
|
|
2096
|
+
else if (yes) {
|
|
2097
|
+
log.warn(`Directory "${name}" is not empty. Pass --force to clear it.`);
|
|
2098
|
+
outro("Aborted.");
|
|
2099
|
+
return;
|
|
2100
|
+
} else {
|
|
1793
2101
|
log.warn(`Directory "${name}" is not empty:`);
|
|
1794
2102
|
const shown = entries.slice(0, 5);
|
|
1795
2103
|
for (const entry of shown) log.message(` - ${entry}`);
|
|
@@ -1809,7 +2117,8 @@ function registerInitCommand(program) {
|
|
|
1809
2117
|
}
|
|
1810
2118
|
}
|
|
1811
2119
|
let template = opts.template;
|
|
1812
|
-
if (!template) template =
|
|
2120
|
+
if (!template) if (yes) template = "minimal";
|
|
2121
|
+
else template = await select({
|
|
1813
2122
|
message: "Project template",
|
|
1814
2123
|
options: [
|
|
1815
2124
|
{
|
|
@@ -1835,7 +2144,8 @@ function registerInitCommand(program) {
|
|
|
1835
2144
|
]
|
|
1836
2145
|
});
|
|
1837
2146
|
let packageManager = opts.pm;
|
|
1838
|
-
if (!packageManager) packageManager = await
|
|
2147
|
+
if (!packageManager) if (yes) packageManager = await resolvePackageManager(void 0);
|
|
2148
|
+
else packageManager = await select({
|
|
1839
2149
|
message: "Package manager",
|
|
1840
2150
|
options: [
|
|
1841
2151
|
{
|
|
@@ -1857,7 +2167,8 @@ function registerInitCommand(program) {
|
|
|
1857
2167
|
]
|
|
1858
2168
|
});
|
|
1859
2169
|
let defaultRepo = opts.repo;
|
|
1860
|
-
if (!defaultRepo)
|
|
2170
|
+
if (!defaultRepo) if (yes) defaultRepo = "inmemory";
|
|
2171
|
+
else {
|
|
1861
2172
|
defaultRepo = await select({
|
|
1862
2173
|
message: "Default repository/ORM",
|
|
1863
2174
|
options: [
|
|
@@ -1890,19 +2201,20 @@ function registerInitCommand(program) {
|
|
|
1890
2201
|
const raw = opts.packages.trim().toLowerCase();
|
|
1891
2202
|
if (raw === "" || raw === "none" || raw === "false") selectedPackages = [];
|
|
1892
2203
|
else selectedPackages = opts.packages.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1893
|
-
} else selectedPackages =
|
|
2204
|
+
} else if (yes) selectedPackages = [];
|
|
2205
|
+
else selectedPackages = await multiSelect({
|
|
1894
2206
|
message: "Select packages to include",
|
|
1895
2207
|
options: [...OPTIONAL_PACKAGES],
|
|
1896
2208
|
required: false
|
|
1897
2209
|
});
|
|
1898
2210
|
let initGit;
|
|
1899
|
-
if (opts.git === void 0) initGit = await confirm({
|
|
2211
|
+
if (opts.git === void 0) initGit = yes ? true : await confirm({
|
|
1900
2212
|
message: "Initialize git repository?",
|
|
1901
2213
|
initialValue: true
|
|
1902
2214
|
});
|
|
1903
2215
|
else initGit = opts.git;
|
|
1904
2216
|
let installDeps;
|
|
1905
|
-
if (opts.install === void 0) installDeps = await confirm({
|
|
2217
|
+
if (opts.install === void 0) installDeps = yes ? true : await confirm({
|
|
1906
2218
|
message: "Install dependencies?",
|
|
1907
2219
|
initialValue: true
|
|
1908
2220
|
});
|
|
@@ -2485,7 +2797,7 @@ export const ${pascal.toUpperCase()}_QUERY_CONFIG: QueryParamsConfig = {
|
|
|
2485
2797
|
//#endregion
|
|
2486
2798
|
//#region src/generators/templates/dtos.ts
|
|
2487
2799
|
function generateCreateDTO(ctx) {
|
|
2488
|
-
const { pascal
|
|
2800
|
+
const { pascal } = ctx;
|
|
2489
2801
|
return `import { z } from 'zod'
|
|
2490
2802
|
|
|
2491
2803
|
/**
|
|
@@ -2505,7 +2817,7 @@ export type Create${pascal}DTO = z.infer<typeof create${pascal}Schema>
|
|
|
2505
2817
|
`;
|
|
2506
2818
|
}
|
|
2507
2819
|
function generateUpdateDTO(ctx) {
|
|
2508
|
-
const { pascal
|
|
2820
|
+
const { pascal } = ctx;
|
|
2509
2821
|
return `import { z } from 'zod'
|
|
2510
2822
|
|
|
2511
2823
|
export const update${pascal}Schema = z.object({
|
|
@@ -2516,7 +2828,7 @@ export type Update${pascal}DTO = z.infer<typeof update${pascal}Schema>
|
|
|
2516
2828
|
`;
|
|
2517
2829
|
}
|
|
2518
2830
|
function generateResponseDTO(ctx) {
|
|
2519
|
-
const { pascal
|
|
2831
|
+
const { pascal } = ctx;
|
|
2520
2832
|
return `export interface ${pascal}ResponseDTO {
|
|
2521
2833
|
id: string
|
|
2522
2834
|
name: string
|
|
@@ -2633,7 +2945,7 @@ export class Delete${pascal}UseCase {
|
|
|
2633
2945
|
//#endregion
|
|
2634
2946
|
//#region src/generators/templates/repository.ts
|
|
2635
2947
|
function generateRepositoryInterface(ctx) {
|
|
2636
|
-
const { pascal, kebab, dtoPrefix = "../../application/dtos" } = ctx;
|
|
2948
|
+
const { pascal, kebab, dtoPrefix = "../../application/dtos", tokenScope = "app" } = ctx;
|
|
2637
2949
|
return `/**
|
|
2638
2950
|
* ${pascal} Repository Interface
|
|
2639
2951
|
*
|
|
@@ -2663,8 +2975,12 @@ export interface I${pascal}Repository {
|
|
|
2663
2975
|
* \`container.resolve(${pascal.toUpperCase()}_REPOSITORY)\` and
|
|
2664
2976
|
* \`@Inject(${pascal.toUpperCase()}_REPOSITORY)\` both return the typed
|
|
2665
2977
|
* interface — no manual generic, no \`any\` cast.
|
|
2978
|
+
*
|
|
2979
|
+
* The \`'${tokenScope}/'\` prefix matches the project scope so
|
|
2980
|
+
* \`kick-lint\`'s \`token-reserved-prefix\` rule never fires —
|
|
2981
|
+
* adopters must NOT use the reserved \`'kick/'\` namespace.
|
|
2666
2982
|
*/
|
|
2667
|
-
export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('
|
|
2983
|
+
export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('${tokenScope}/${kebab}/repository')
|
|
2668
2984
|
`;
|
|
2669
2985
|
}
|
|
2670
2986
|
function generateInMemoryRepository(ctx) {
|
|
@@ -2908,7 +3224,7 @@ export class ${pascal} {
|
|
|
2908
3224
|
`;
|
|
2909
3225
|
}
|
|
2910
3226
|
function generateValueObject(ctx) {
|
|
2911
|
-
const { pascal
|
|
3227
|
+
const { pascal } = ctx;
|
|
2912
3228
|
return `/**
|
|
2913
3229
|
* ${pascal} ID Value Object
|
|
2914
3230
|
*
|
|
@@ -3666,7 +3982,7 @@ export class ${pascal}Controller {
|
|
|
3666
3982
|
//#endregion
|
|
3667
3983
|
//#region src/generators/patterns/rest.ts
|
|
3668
3984
|
async function generateRestFiles(ctx) {
|
|
3669
|
-
const { pascal, kebab, plural, pluralPascal, repo, noTests, prismaClientPath, write } = ctx;
|
|
3985
|
+
const { pascal, kebab, plural, pluralPascal, repo, noTests, prismaClientPath, tokenScope, write } = ctx;
|
|
3670
3986
|
await write(`${kebab}.module.ts`, generateRestModuleIndex({
|
|
3671
3987
|
pascal,
|
|
3672
3988
|
kebab,
|
|
@@ -3702,7 +4018,8 @@ async function generateRestFiles(ctx) {
|
|
|
3702
4018
|
await write(`${kebab}.repository.ts`, generateRepositoryInterface({
|
|
3703
4019
|
pascal,
|
|
3704
4020
|
kebab,
|
|
3705
|
-
dtoPrefix: "./dtos"
|
|
4021
|
+
dtoPrefix: "./dtos",
|
|
4022
|
+
tokenScope
|
|
3706
4023
|
}));
|
|
3707
4024
|
const builtinRepoFileMap = {
|
|
3708
4025
|
inmemory: `in-memory-${kebab}`,
|
|
@@ -3762,7 +4079,7 @@ async function generateRestFiles(ctx) {
|
|
|
3762
4079
|
//#endregion
|
|
3763
4080
|
//#region src/generators/patterns/cqrs.ts
|
|
3764
4081
|
async function generateCqrsFiles(ctx) {
|
|
3765
|
-
const { pascal, kebab, plural, pluralPascal, repo, noTests, prismaClientPath, write } = ctx;
|
|
4082
|
+
const { pascal, kebab, plural, pluralPascal, repo, noTests, prismaClientPath, tokenScope, write } = ctx;
|
|
3766
4083
|
await write(`${kebab}.module.ts`, generateCqrsModuleIndex({
|
|
3767
4084
|
pascal,
|
|
3768
4085
|
kebab,
|
|
@@ -3811,7 +4128,8 @@ async function generateCqrsFiles(ctx) {
|
|
|
3811
4128
|
await write(`${kebab}.repository.ts`, generateRepositoryInterface({
|
|
3812
4129
|
pascal,
|
|
3813
4130
|
kebab,
|
|
3814
|
-
dtoPrefix: "./dtos"
|
|
4131
|
+
dtoPrefix: "./dtos",
|
|
4132
|
+
tokenScope
|
|
3815
4133
|
}));
|
|
3816
4134
|
const builtinRepoFileMap = {
|
|
3817
4135
|
inmemory: `in-memory-${kebab}`,
|
|
@@ -3871,7 +4189,7 @@ async function generateCqrsFiles(ctx) {
|
|
|
3871
4189
|
//#endregion
|
|
3872
4190
|
//#region src/generators/patterns/ddd.ts
|
|
3873
4191
|
async function generateDddFiles(ctx) {
|
|
3874
|
-
const { pascal, kebab, plural, pluralPascal, repo, noEntity, noTests, prismaClientPath, write } = ctx;
|
|
4192
|
+
const { pascal, kebab, plural, pluralPascal, repo, noEntity, noTests, prismaClientPath, tokenScope, write } = ctx;
|
|
3875
4193
|
await write(`${kebab}.module.ts`, generateModuleIndex({
|
|
3876
4194
|
pascal,
|
|
3877
4195
|
kebab,
|
|
@@ -3912,7 +4230,8 @@ async function generateDddFiles(ctx) {
|
|
|
3912
4230
|
for (const uc of useCases) await write(`application/use-cases/${uc.file}`, uc.content);
|
|
3913
4231
|
await write(`domain/repositories/${kebab}.repository.ts`, generateRepositoryInterface({
|
|
3914
4232
|
pascal,
|
|
3915
|
-
kebab
|
|
4233
|
+
kebab,
|
|
4234
|
+
tokenScope
|
|
3916
4235
|
}));
|
|
3917
4236
|
await write(`domain/services/${kebab}-domain.service.ts`, generateDomainService({
|
|
3918
4237
|
pascal,
|
|
@@ -4029,6 +4348,7 @@ async function generateModule(options) {
|
|
|
4029
4348
|
noEntity: noEntity ?? false,
|
|
4030
4349
|
noTests: noTests ?? false,
|
|
4031
4350
|
prismaClientPath: options.prismaClientPath ?? "@prisma/client",
|
|
4351
|
+
tokenScope: options.tokenScope ?? "app",
|
|
4032
4352
|
write,
|
|
4033
4353
|
files
|
|
4034
4354
|
};
|
|
@@ -4763,7 +5083,7 @@ function detectName(outDir, override) {
|
|
|
4763
5083
|
const pkg = JSON.parse(readFileSync(join(outDir, "package.json"), "utf-8"));
|
|
4764
5084
|
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
|
|
4765
5085
|
} catch {}
|
|
4766
|
-
return outDir.split("/").
|
|
5086
|
+
return outDir.split("/").findLast(Boolean) ?? "app";
|
|
4767
5087
|
}
|
|
4768
5088
|
function detectPm(outDir, override) {
|
|
4769
5089
|
if (override) return override;
|
|
@@ -5227,7 +5547,7 @@ function parseFields(raw) {
|
|
|
5227
5547
|
});
|
|
5228
5548
|
}
|
|
5229
5549
|
async function generateScaffold(options) {
|
|
5230
|
-
const { name, fields, modulesDir, noEntity, noTests, repo = "inmemory" } = options;
|
|
5550
|
+
const { name, fields, modulesDir, noEntity, noTests: _noTests, repo = "inmemory", tokenScope = "app" } = options;
|
|
5231
5551
|
const shouldPluralize = options.pluralize !== false;
|
|
5232
5552
|
const kebab = toKebabCase(name);
|
|
5233
5553
|
const pascal = toPascalCase(name);
|
|
@@ -5249,7 +5569,7 @@ async function generateScaffold(options) {
|
|
|
5249
5569
|
await write(`application/dtos/${kebab}-response.dto.ts`, genResponseDTO(pascal, fields));
|
|
5250
5570
|
const useCases = genUseCases(pascal, kebab, plural, pluralPascal);
|
|
5251
5571
|
for (const uc of useCases) await write(`application/use-cases/${uc.file}`, uc.content);
|
|
5252
|
-
await write(`domain/repositories/${kebab}.repository.ts`, genRepositoryInterface(pascal, kebab));
|
|
5572
|
+
await write(`domain/repositories/${kebab}.repository.ts`, genRepositoryInterface(pascal, kebab, tokenScope));
|
|
5253
5573
|
await write(`domain/services/${kebab}-domain.service.ts`, genDomainService(pascal, kebab));
|
|
5254
5574
|
if (repo === "inmemory") await write(`infrastructure/repositories/in-memory-${kebab}.repository.ts`, genInMemoryRepository(pascal, kebab, fields));
|
|
5255
5575
|
if (!noEntity) {
|
|
@@ -5530,7 +5850,7 @@ export class ${pascal}Controller {
|
|
|
5530
5850
|
}
|
|
5531
5851
|
`;
|
|
5532
5852
|
}
|
|
5533
|
-
function genRepositoryInterface(pascal, kebab) {
|
|
5853
|
+
function genRepositoryInterface(pascal, kebab, tokenScope) {
|
|
5534
5854
|
return `import { createToken } from '@forinda/kickjs'
|
|
5535
5855
|
import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
|
|
5536
5856
|
import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
|
|
@@ -5551,8 +5871,12 @@ export interface I${pascal}Repository {
|
|
|
5551
5871
|
* \`container.resolve(${pascal.toUpperCase()}_REPOSITORY)\` and
|
|
5552
5872
|
* \`@Inject(${pascal.toUpperCase()}_REPOSITORY)\` both return the typed
|
|
5553
5873
|
* interface — no manual generic, no \`any\` cast.
|
|
5874
|
+
*
|
|
5875
|
+
* The \`'${tokenScope}/'\` prefix matches the project scope so
|
|
5876
|
+
* \`kick-lint\`'s \`token-reserved-prefix\` rule never fires —
|
|
5877
|
+
* adopters must NOT use the reserved \`'kick/'\` namespace.
|
|
5554
5878
|
*/
|
|
5555
|
-
export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('
|
|
5879
|
+
export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('${tokenScope}/${kebab}/repository')
|
|
5556
5880
|
`;
|
|
5557
5881
|
}
|
|
5558
5882
|
function genDomainService(pascal, kebab) {
|
|
@@ -5832,6 +6156,7 @@ async function runModuleGeneration(names, opts, dryRun) {
|
|
|
5832
6156
|
const repo = opts.repo ?? resolveRepoType(mc.repo);
|
|
5833
6157
|
const pattern = opts.pattern ?? config?.pattern ?? "ddd";
|
|
5834
6158
|
const shouldPluralize = opts.pluralize === false ? false : mc.pluralize ?? true;
|
|
6159
|
+
const tokenScope = resolveTokenScope(config, process.cwd());
|
|
5835
6160
|
const allFiles = [];
|
|
5836
6161
|
for (const name of names) {
|
|
5837
6162
|
const files = await generateModule({
|
|
@@ -5845,7 +6170,8 @@ async function runModuleGeneration(names, opts, dryRun) {
|
|
|
5845
6170
|
pattern,
|
|
5846
6171
|
dryRun,
|
|
5847
6172
|
pluralize: shouldPluralize,
|
|
5848
|
-
prismaClientPath: mc.prismaClientPath
|
|
6173
|
+
prismaClientPath: mc.prismaClientPath,
|
|
6174
|
+
tokenScope
|
|
5849
6175
|
});
|
|
5850
6176
|
allFiles.push(...files);
|
|
5851
6177
|
}
|
|
@@ -6008,16 +6334,19 @@ function registerGenerateCommand(program) {
|
|
|
6008
6334
|
console.error("\n Error: At least one field is required.\n Usage: kick g scaffold <name> <field:type> [field:type...]\n Example: kick g scaffold Post title:string body:text:optional published:boolean:optional\n Optional: append :optional (shell-safe, no quoting needed)\n");
|
|
6009
6335
|
process.exit(1);
|
|
6010
6336
|
}
|
|
6011
|
-
const
|
|
6337
|
+
const config = await loadKickConfig(process.cwd());
|
|
6338
|
+
const mc = resolveModuleConfig(config);
|
|
6012
6339
|
const modulesDir = opts.modulesDir ?? mc.dir ?? "src/modules";
|
|
6013
6340
|
const fields = parseFields(rawFields);
|
|
6341
|
+
const tokenScope = resolveTokenScope(config, process.cwd());
|
|
6014
6342
|
const files = await generateScaffold({
|
|
6015
6343
|
name,
|
|
6016
6344
|
fields,
|
|
6017
6345
|
modulesDir: resolve(modulesDir),
|
|
6018
6346
|
noEntity: opts.entity === false,
|
|
6019
6347
|
noTests: opts.tests === false,
|
|
6020
|
-
pluralize: opts.pluralize === false ? false : mc.pluralize ?? true
|
|
6348
|
+
pluralize: opts.pluralize === false ? false : mc.pluralize ?? true,
|
|
6349
|
+
tokenScope
|
|
6021
6350
|
});
|
|
6022
6351
|
console.log(`\n Scaffolded ${name} with ${fields.length} field(s):`);
|
|
6023
6352
|
for (const f of fields) console.log(` ${f.name}: ${f.type}${f.optional ? " (optional)" : ""}`);
|
|
@@ -6087,6 +6416,17 @@ const BANNER_PREFIX = "/* AUTO-GENERATED by kick typegen — do not edit. Plugin
|
|
|
6087
6416
|
async function runTypegen(opts) {
|
|
6088
6417
|
const typesDirAbs = path.resolve(opts.cwd, TYPES_DIR);
|
|
6089
6418
|
await mkdir(typesDirAbs, { recursive: true });
|
|
6419
|
+
const scanCache = /* @__PURE__ */ new Map();
|
|
6420
|
+
const scanFn = opts.scan ?? scanProject;
|
|
6421
|
+
const getScanResult = (scanOpts) => {
|
|
6422
|
+
const key = stableScanKey(scanOpts);
|
|
6423
|
+
let pending = scanCache.get(key);
|
|
6424
|
+
if (!pending) {
|
|
6425
|
+
pending = scanFn(scanOpts);
|
|
6426
|
+
scanCache.set(key, pending);
|
|
6427
|
+
}
|
|
6428
|
+
return pending;
|
|
6429
|
+
};
|
|
6090
6430
|
const ctx = {
|
|
6091
6431
|
cwd: opts.cwd,
|
|
6092
6432
|
config: opts.config,
|
|
@@ -6098,6 +6438,7 @@ async function runTypegen(opts) {
|
|
|
6098
6438
|
await mkdir(path.dirname(abs), { recursive: true });
|
|
6099
6439
|
await writeFile(abs, contents, "utf8");
|
|
6100
6440
|
},
|
|
6441
|
+
getScanResult,
|
|
6101
6442
|
log: console
|
|
6102
6443
|
};
|
|
6103
6444
|
const results = [];
|
|
@@ -6110,7 +6451,8 @@ async function runTypegen(opts) {
|
|
|
6110
6451
|
});
|
|
6111
6452
|
continue;
|
|
6112
6453
|
}
|
|
6113
|
-
const
|
|
6454
|
+
const ext = plugin.outExtension ?? ".d.ts";
|
|
6455
|
+
const file = path.join(typesDirAbs, `${plugin.id.replace(/\//g, "__")}${ext}`);
|
|
6114
6456
|
const next = `${BANNER_PREFIX}${plugin.id} */\n\n` + out + "\n";
|
|
6115
6457
|
let prev = "";
|
|
6116
6458
|
if (existsSync(file)) prev = await readFile(file, "utf8");
|
|
@@ -6132,6 +6474,24 @@ async function runTypegen(opts) {
|
|
|
6132
6474
|
}
|
|
6133
6475
|
return results;
|
|
6134
6476
|
}
|
|
6477
|
+
/**
|
|
6478
|
+
* Order-independent cache key for `ScanOptions`. Builds the key from
|
|
6479
|
+
* the known fields in a fixed order so semantically equal options
|
|
6480
|
+
* always produce the same key regardless of how the caller built the
|
|
6481
|
+
* object literal. Arrays (extensions / exclude) are sorted before
|
|
6482
|
+
* joining so `['.ts', '.tsx']` and `['.tsx', '.ts']` collide.
|
|
6483
|
+
*/
|
|
6484
|
+
function stableScanKey(opts) {
|
|
6485
|
+
const extensions = (opts.extensions ?? []).slice().toSorted().join(",");
|
|
6486
|
+
const exclude = (opts.exclude ?? []).slice().toSorted().join(",");
|
|
6487
|
+
return [
|
|
6488
|
+
`root=${opts.root}`,
|
|
6489
|
+
`cwd=${opts.cwd}`,
|
|
6490
|
+
`extensions=${extensions}`,
|
|
6491
|
+
`exclude=${exclude}`,
|
|
6492
|
+
`envFile=${opts.envFile ?? ""}`
|
|
6493
|
+
].join("|");
|
|
6494
|
+
}
|
|
6135
6495
|
//#endregion
|
|
6136
6496
|
//#region src/typegen/disable-filter.ts
|
|
6137
6497
|
/**
|
|
@@ -6257,18 +6617,15 @@ async function processEntry(namespace, entry, cwd, distAbs) {
|
|
|
6257
6617
|
});
|
|
6258
6618
|
mkdirSync(destAbs, { recursive: true });
|
|
6259
6619
|
const manifestSlice = {};
|
|
6260
|
-
const
|
|
6261
|
-
for (const relPath of
|
|
6620
|
+
const { pairs, collisionGroupsResolved } = groupAssetKeys(namespace, [...matches].toSorted(), { strategy: entry.keys ?? "auto" });
|
|
6621
|
+
for (const { rel: relPath, key } of pairs) {
|
|
6262
6622
|
const srcFile = join(srcAbs, relPath);
|
|
6263
6623
|
const destFile = join(destAbs, relPath);
|
|
6264
6624
|
mkdirSync(dirname(destFile), { recursive: true });
|
|
6265
6625
|
cpSync(srcFile, destFile);
|
|
6266
|
-
|
|
6267
|
-
const previous = keyOwner.get(logicalKey);
|
|
6268
|
-
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.`);
|
|
6269
|
-
keyOwner.set(logicalKey, relPath);
|
|
6270
|
-
manifestSlice[logicalKey] = toManifestRelative(distAbs, destFile);
|
|
6626
|
+
manifestSlice[key] = toManifestRelative(distAbs, destFile);
|
|
6271
6627
|
}
|
|
6628
|
+
if (collisionGroupsResolved > 0) console.log(` ℹ assetMap.${namespace}: auto-resolved ${collisionGroupsResolved} basename collision(s) by keeping extensions (set 'keys: "strip"' to opt back into legacy last-write-wins behaviour, or 'keys: "with-extension"' to keep all keys verbose).`);
|
|
6272
6629
|
return {
|
|
6273
6630
|
entrySummary: {
|
|
6274
6631
|
namespace,
|
|
@@ -6279,11 +6636,6 @@ async function processEntry(namespace, entry, cwd, distAbs) {
|
|
|
6279
6636
|
manifestSlice
|
|
6280
6637
|
};
|
|
6281
6638
|
}
|
|
6282
|
-
/** Strip the final extension from a file path (`mails/welcome.ejs` → `mails/welcome`). */
|
|
6283
|
-
function stripExt(path) {
|
|
6284
|
-
const ext = extname(path);
|
|
6285
|
-
return ext ? path.slice(0, -ext.length) : path;
|
|
6286
|
-
}
|
|
6287
6639
|
/**
|
|
6288
6640
|
* Make `destFile` relative to the manifest's directory + force POSIX
|
|
6289
6641
|
* separators so the manifest is byte-stable across platforms.
|
|
@@ -6325,8 +6677,21 @@ function isDirectorySync(path) {
|
|
|
6325
6677
|
* This function just creates the Vite server, listens, and handles shutdown.
|
|
6326
6678
|
* Vite owns the HTTP port — Express runs as post-middleware on Vite's server.
|
|
6327
6679
|
*/
|
|
6328
|
-
|
|
6680
|
+
/**
|
|
6681
|
+
* Resolve whether the dev server's chokidar should poll instead of
|
|
6682
|
+
* relying on `fs.watch` events. CLI flag wins over env var; default
|
|
6683
|
+
* is event-based (faster, lower CPU). Polling is the right choice in
|
|
6684
|
+
* Docker bind mounts, WSL crossing the WSL/Windows boundary, NFS,
|
|
6685
|
+
* and some old Linux kernels where new-file events get dropped.
|
|
6686
|
+
*/
|
|
6687
|
+
function resolvePolling(flag) {
|
|
6688
|
+
if (typeof flag === "boolean") return flag;
|
|
6689
|
+
const env = process.env.KICKJS_WATCH_POLLING;
|
|
6690
|
+
return env === "1" || env === "true";
|
|
6691
|
+
}
|
|
6692
|
+
async function startDevServer(_entry, port, opts = {}) {
|
|
6329
6693
|
if (port) process.env.PORT = port;
|
|
6694
|
+
const polling = resolvePolling(opts.polling);
|
|
6330
6695
|
const cwd = process.cwd();
|
|
6331
6696
|
const devConfig = await loadKickConfig(cwd);
|
|
6332
6697
|
const schemaValidator = devConfig?.typegen?.schemaValidator ?? "zod";
|
|
@@ -6339,7 +6704,8 @@ async function startDevServer(_entry, port) {
|
|
|
6339
6704
|
envFile,
|
|
6340
6705
|
srcDir: devConfig?.typegen?.srcDir,
|
|
6341
6706
|
outDir: devConfig?.typegen?.outDir,
|
|
6342
|
-
assetMap: devConfig?.assetMap
|
|
6707
|
+
assetMap: devConfig?.assetMap,
|
|
6708
|
+
runPlugins: false
|
|
6343
6709
|
});
|
|
6344
6710
|
} catch (err) {
|
|
6345
6711
|
console.warn(` kick typegen: skipped (${err?.message ?? err})`);
|
|
@@ -6352,7 +6718,13 @@ async function startDevServer(_entry, port) {
|
|
|
6352
6718
|
const { createServer } = await import(pathToFileURL(createRequire(resolve("package.json")).resolve("vite")).href);
|
|
6353
6719
|
const server = await createServer({
|
|
6354
6720
|
configFile: resolve("vite.config.ts"),
|
|
6355
|
-
server: {
|
|
6721
|
+
server: {
|
|
6722
|
+
port: port ? parseInt(port, 10) : void 0,
|
|
6723
|
+
...polling ? { watch: {
|
|
6724
|
+
usePolling: true,
|
|
6725
|
+
interval: 100
|
|
6726
|
+
} } : {}
|
|
6727
|
+
}
|
|
6356
6728
|
});
|
|
6357
6729
|
const assetSrcRoots = devConfig?.assetMap ? Object.values(devConfig.assetMap).map((entry) => entry?.src).filter((src) => typeof src === "string" && src.length > 0).map((src) => resolve(cwd, src)) : [];
|
|
6358
6730
|
const isAssetFile = (file) => assetSrcRoots.some((root) => file === root || file.startsWith(`${root}/`));
|
|
@@ -6373,7 +6745,8 @@ async function startDevServer(_entry, port) {
|
|
|
6373
6745
|
envFile,
|
|
6374
6746
|
srcDir: devConfig?.typegen?.srcDir,
|
|
6375
6747
|
outDir: devConfig?.typegen?.outDir,
|
|
6376
|
-
assetMap: devConfig?.assetMap
|
|
6748
|
+
assetMap: devConfig?.assetMap,
|
|
6749
|
+
runPlugins: false
|
|
6377
6750
|
}).catch(() => {});
|
|
6378
6751
|
runAllPluginTypegens({
|
|
6379
6752
|
cwd,
|
|
@@ -6398,9 +6771,9 @@ async function startDevServer(_entry, port) {
|
|
|
6398
6771
|
process.on("SIGTERM", shutdown);
|
|
6399
6772
|
}
|
|
6400
6773
|
function registerRunCommands(program) {
|
|
6401
|
-
program.command("dev").description("Start development server with Vite HMR (zero-downtime reload)").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").action(async (opts) => {
|
|
6774
|
+
program.command("dev").description("Start development server with Vite HMR (zero-downtime reload)").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").option("--polling", "Force chokidar to poll for file changes (Docker / WSL / NFS / older kernels)").action(async (opts) => {
|
|
6402
6775
|
try {
|
|
6403
|
-
await startDevServer(opts.entry, opts.port);
|
|
6776
|
+
await startDevServer(opts.entry, opts.port, { polling: opts.polling });
|
|
6404
6777
|
} catch (err) {
|
|
6405
6778
|
if (err.code === "ERR_MODULE_NOT_FOUND" && err.message?.includes("vite")) console.error("\n Error: vite is not installed.\n Run: pnpm add -D vite unplugin-swc\n");
|
|
6406
6779
|
else console.error("\n Dev server failed:", err.message ?? err);
|
|
@@ -6493,7 +6866,7 @@ function registerInfoCommand(program) {
|
|
|
6493
6866
|
}
|
|
6494
6867
|
//#endregion
|
|
6495
6868
|
//#region src/commands/inspect.ts
|
|
6496
|
-
const { bold, dim, green, red, yellow,
|
|
6869
|
+
const { bold, dim, green, red, yellow, blue } = pc;
|
|
6497
6870
|
function formatUptime(seconds) {
|
|
6498
6871
|
const d = Math.floor(seconds / 86400);
|
|
6499
6872
|
const h = Math.floor(seconds % 86400 / 3600);
|
|
@@ -6602,198 +6975,6 @@ function registerInspectCommand(program) {
|
|
|
6602
6975
|
});
|
|
6603
6976
|
}
|
|
6604
6977
|
//#endregion
|
|
6605
|
-
//#region src/commands/add.ts
|
|
6606
|
-
/** Registry of KickJS packages and their required peer dependencies */
|
|
6607
|
-
const PACKAGE_REGISTRY = {
|
|
6608
|
-
kickjs: {
|
|
6609
|
-
pkg: "@forinda/kickjs",
|
|
6610
|
-
peers: ["express"],
|
|
6611
|
-
description: "Unified framework: DI, decorators, routing, middleware"
|
|
6612
|
-
},
|
|
6613
|
-
vite: {
|
|
6614
|
-
pkg: "@forinda/kickjs-vite",
|
|
6615
|
-
peers: ["vite"],
|
|
6616
|
-
description: "Vite plugin: dev server, HMR, module discovery",
|
|
6617
|
-
dev: true
|
|
6618
|
-
},
|
|
6619
|
-
config: {
|
|
6620
|
-
pkg: "dotenv",
|
|
6621
|
-
peers: [],
|
|
6622
|
-
description: "Optional .env file loader (kickjs ConfigService now ships in @forinda/kickjs)"
|
|
6623
|
-
},
|
|
6624
|
-
cli: {
|
|
6625
|
-
pkg: "@forinda/kickjs-cli",
|
|
6626
|
-
peers: [],
|
|
6627
|
-
description: "CLI tool and code generators",
|
|
6628
|
-
dev: true
|
|
6629
|
-
},
|
|
6630
|
-
swagger: {
|
|
6631
|
-
pkg: "@forinda/kickjs-swagger",
|
|
6632
|
-
peers: [],
|
|
6633
|
-
description: "OpenAPI spec + Swagger UI + ReDoc"
|
|
6634
|
-
},
|
|
6635
|
-
drizzle: {
|
|
6636
|
-
pkg: "@forinda/kickjs-drizzle",
|
|
6637
|
-
peers: ["drizzle-orm"],
|
|
6638
|
-
description: "Drizzle ORM adapter + query builder"
|
|
6639
|
-
},
|
|
6640
|
-
prisma: {
|
|
6641
|
-
pkg: "@forinda/kickjs-prisma",
|
|
6642
|
-
peers: ["@prisma/client"],
|
|
6643
|
-
description: "Prisma adapter + query builder"
|
|
6644
|
-
},
|
|
6645
|
-
ws: {
|
|
6646
|
-
pkg: "@forinda/kickjs-ws",
|
|
6647
|
-
peers: ["socket.io"],
|
|
6648
|
-
description: "WebSocket with @WsController decorators"
|
|
6649
|
-
},
|
|
6650
|
-
devtools: {
|
|
6651
|
-
pkg: "@forinda/kickjs-devtools",
|
|
6652
|
-
peers: [],
|
|
6653
|
-
description: "Development dashboard — routes, DI, metrics, health",
|
|
6654
|
-
dev: true
|
|
6655
|
-
},
|
|
6656
|
-
auth: {
|
|
6657
|
-
pkg: "@forinda/kickjs-auth",
|
|
6658
|
-
peers: ["jsonwebtoken"],
|
|
6659
|
-
description: "Authentication — JWT, API key, and custom strategies"
|
|
6660
|
-
},
|
|
6661
|
-
queue: {
|
|
6662
|
-
pkg: "@forinda/kickjs-queue",
|
|
6663
|
-
peers: [],
|
|
6664
|
-
description: "Queue adapter (BullMQ/RabbitMQ/Kafka)"
|
|
6665
|
-
},
|
|
6666
|
-
"queue:bullmq": {
|
|
6667
|
-
pkg: "@forinda/kickjs-queue",
|
|
6668
|
-
peers: ["bullmq", "ioredis"],
|
|
6669
|
-
description: "Queue with BullMQ + Redis"
|
|
6670
|
-
},
|
|
6671
|
-
"queue:rabbitmq": {
|
|
6672
|
-
pkg: "@forinda/kickjs-queue",
|
|
6673
|
-
peers: ["amqplib"],
|
|
6674
|
-
description: "Queue with RabbitMQ"
|
|
6675
|
-
},
|
|
6676
|
-
"queue:kafka": {
|
|
6677
|
-
pkg: "@forinda/kickjs-queue",
|
|
6678
|
-
peers: ["kafkajs"],
|
|
6679
|
-
description: "Queue with Kafka"
|
|
6680
|
-
},
|
|
6681
|
-
mcp: {
|
|
6682
|
-
pkg: "@forinda/kickjs-mcp",
|
|
6683
|
-
peers: ["@modelcontextprotocol/sdk"],
|
|
6684
|
-
description: "Model Context Protocol server — expose @Controller endpoints as AI tools"
|
|
6685
|
-
},
|
|
6686
|
-
testing: {
|
|
6687
|
-
pkg: "@forinda/kickjs-testing",
|
|
6688
|
-
peers: [],
|
|
6689
|
-
description: "Test utilities and TestModule builder",
|
|
6690
|
-
dev: true
|
|
6691
|
-
}
|
|
6692
|
-
};
|
|
6693
|
-
function detectPackageManager() {
|
|
6694
|
-
if (existsSync(resolve("pnpm-lock.yaml"))) return "pnpm";
|
|
6695
|
-
if (existsSync(resolve("yarn.lock"))) return "yarn";
|
|
6696
|
-
if (existsSync(resolve("bun.lockb")) || existsSync(resolve("bun.lock"))) return "bun";
|
|
6697
|
-
return "npm";
|
|
6698
|
-
}
|
|
6699
|
-
/** Read `packageManager` from package.json (corepack convention: "pnpm@10.0.0") */
|
|
6700
|
-
function packageManagerFromPackageJson() {
|
|
6701
|
-
try {
|
|
6702
|
-
const field = JSON.parse(readFileSync(resolve("package.json"), "utf-8")).packageManager;
|
|
6703
|
-
if (typeof field !== "string") return null;
|
|
6704
|
-
const name = field.split("@")[0];
|
|
6705
|
-
return PACKAGE_MANAGERS.includes(name) ? name : null;
|
|
6706
|
-
} catch {
|
|
6707
|
-
return null;
|
|
6708
|
-
}
|
|
6709
|
-
}
|
|
6710
|
-
/**
|
|
6711
|
-
* Resolve which package manager to use, in priority order:
|
|
6712
|
-
* 1. `--pm` CLI flag
|
|
6713
|
-
* 2. `packageManager` in kick.config
|
|
6714
|
-
* 3. `packageManager` in package.json (corepack)
|
|
6715
|
-
* 4. Lockfile detection
|
|
6716
|
-
* 5. `'npm'`
|
|
6717
|
-
*/
|
|
6718
|
-
async function resolvePackageManager(flagPm) {
|
|
6719
|
-
if (flagPm && PACKAGE_MANAGERS.includes(flagPm)) return flagPm;
|
|
6720
|
-
const config = await loadKickConfig(process.cwd());
|
|
6721
|
-
if (config?.packageManager && PACKAGE_MANAGERS.includes(config.packageManager)) return config.packageManager;
|
|
6722
|
-
const fromPkg = packageManagerFromPackageJson();
|
|
6723
|
-
if (fromPkg) return fromPkg;
|
|
6724
|
-
return detectPackageManager();
|
|
6725
|
-
}
|
|
6726
|
-
function printPackageList() {
|
|
6727
|
-
console.log("\n Available KickJS packages:\n");
|
|
6728
|
-
const maxName = Math.max(...Object.keys(PACKAGE_REGISTRY).map((k) => k.length));
|
|
6729
|
-
for (const [name, info] of Object.entries(PACKAGE_REGISTRY)) {
|
|
6730
|
-
const padded = name.padEnd(maxName + 2);
|
|
6731
|
-
const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
|
|
6732
|
-
console.log(` ${padded} ${info.description}${peers}`);
|
|
6733
|
-
}
|
|
6734
|
-
console.log("\n Usage: kick add auth drizzle swagger");
|
|
6735
|
-
console.log(" kick add queue:bullmq");
|
|
6736
|
-
console.log();
|
|
6737
|
-
}
|
|
6738
|
-
function registerListCommand(program) {
|
|
6739
|
-
program.command("list").alias("ls").description("List all available KickJS packages").action(() => {
|
|
6740
|
-
printPackageList();
|
|
6741
|
-
});
|
|
6742
|
-
}
|
|
6743
|
-
function registerAddCommand(program) {
|
|
6744
|
-
program.command("add [packages...]").description("Add KickJS packages with their required dependencies").option("--pm <manager>", "Package manager override").option("-D, --dev", "Install as dev dependency").option("--list", "List all available packages").action(async (packages, opts) => {
|
|
6745
|
-
if (opts.list || packages.length === 0) {
|
|
6746
|
-
printPackageList();
|
|
6747
|
-
return;
|
|
6748
|
-
}
|
|
6749
|
-
const pm = await resolvePackageManager(opts.pm);
|
|
6750
|
-
const forceDevFlag = opts.dev;
|
|
6751
|
-
const prodDeps = /* @__PURE__ */ new Set();
|
|
6752
|
-
const devDeps = /* @__PURE__ */ new Set();
|
|
6753
|
-
const unknown = [];
|
|
6754
|
-
for (const name of packages) {
|
|
6755
|
-
const entry = PACKAGE_REGISTRY[name];
|
|
6756
|
-
if (!entry) {
|
|
6757
|
-
unknown.push(name);
|
|
6758
|
-
continue;
|
|
6759
|
-
}
|
|
6760
|
-
const target = forceDevFlag || entry.dev ? devDeps : prodDeps;
|
|
6761
|
-
target.add(entry.pkg);
|
|
6762
|
-
for (const peer of entry.peers) target.add(peer);
|
|
6763
|
-
}
|
|
6764
|
-
if (unknown.length > 0) {
|
|
6765
|
-
console.log(`\n Unknown packages: ${unknown.join(", ")}`);
|
|
6766
|
-
console.log(" Run \"kick add --list\" to see available packages.\n");
|
|
6767
|
-
if (prodDeps.size === 0 && devDeps.size === 0) return;
|
|
6768
|
-
}
|
|
6769
|
-
if (prodDeps.size > 0) {
|
|
6770
|
-
const deps = Array.from(prodDeps);
|
|
6771
|
-
const cmd = `${pm} add ${deps.join(" ")}`;
|
|
6772
|
-
console.log(`\n Installing ${deps.length} dependency(ies):`);
|
|
6773
|
-
for (const dep of deps) console.log(` + ${dep}`);
|
|
6774
|
-
console.log();
|
|
6775
|
-
try {
|
|
6776
|
-
execSync(cmd, { stdio: "inherit" });
|
|
6777
|
-
} catch {
|
|
6778
|
-
console.log(`\n Installation failed. Run manually:\n ${cmd}\n`);
|
|
6779
|
-
}
|
|
6780
|
-
}
|
|
6781
|
-
if (devDeps.size > 0) {
|
|
6782
|
-
const deps = Array.from(devDeps);
|
|
6783
|
-
const cmd = `${pm} add -D ${deps.join(" ")}`;
|
|
6784
|
-
console.log(`\n Installing ${deps.length} dev dependency(ies):`);
|
|
6785
|
-
for (const dep of deps) console.log(` + ${dep} (dev)`);
|
|
6786
|
-
console.log();
|
|
6787
|
-
try {
|
|
6788
|
-
execSync(cmd, { stdio: "inherit" });
|
|
6789
|
-
} catch {
|
|
6790
|
-
console.log(`\n Installation failed. Run manually:\n ${cmd}\n`);
|
|
6791
|
-
}
|
|
6792
|
-
}
|
|
6793
|
-
console.log(" Done!\n");
|
|
6794
|
-
});
|
|
6795
|
-
}
|
|
6796
|
-
//#endregion
|
|
6797
6978
|
//#region src/explain/known-issues.ts
|
|
6798
6979
|
function includesAll(haystack, needles) {
|
|
6799
6980
|
const lower = haystack.toLowerCase();
|
|
@@ -7600,7 +7781,7 @@ function registerTypegenCommand(program) {
|
|
|
7600
7781
|
const cwd = process.cwd();
|
|
7601
7782
|
const config = await loadKickConfig(cwd);
|
|
7602
7783
|
if (opts.list) {
|
|
7603
|
-
const { mergeCliPlugins } = await import("./plugin-
|
|
7784
|
+
const { mergeCliPlugins } = await import("./plugin-b7ig7Uxv.mjs").then((n) => n.t);
|
|
7604
7785
|
const { builtinCliPlugins } = await Promise.resolve().then(() => builtins_exports);
|
|
7605
7786
|
const merged = mergeCliPlugins([...builtinCliPlugins, ...config?.plugins ?? []], config?.commands ?? []);
|
|
7606
7787
|
const disabled = new Set(config?.typegen?.disable ?? []);
|
|
@@ -7627,7 +7808,8 @@ function registerTypegenCommand(program) {
|
|
|
7627
7808
|
allowDuplicates: opts.allowDuplicates,
|
|
7628
7809
|
schemaValidator,
|
|
7629
7810
|
envFile,
|
|
7630
|
-
assetMap: config?.assetMap
|
|
7811
|
+
assetMap: config?.assetMap,
|
|
7812
|
+
runPlugins: false
|
|
7631
7813
|
};
|
|
7632
7814
|
try {
|
|
7633
7815
|
if (opts.watch) {
|
|
@@ -8047,6 +8229,238 @@ const kickAssetsTypegen = () => ({
|
|
|
8047
8229
|
}
|
|
8048
8230
|
});
|
|
8049
8231
|
//#endregion
|
|
8232
|
+
//#region src/typegen/render/routes.ts
|
|
8233
|
+
const ROUTES_HEADER = `/* eslint-disable */
|
|
8234
|
+
// AUTO-GENERATED by \`kick typegen\`. DO NOT EDIT.
|
|
8235
|
+
// Re-run with \`kick typegen\` or rely on \`kick dev\` to refresh.
|
|
8236
|
+
`;
|
|
8237
|
+
/**
|
|
8238
|
+
* Render the `KickRoutes` global namespace augmentation. Each interface
|
|
8239
|
+
* inside corresponds to a controller class; each property is a single
|
|
8240
|
+
* route method on that controller, conforming to `RouteShape`.
|
|
8241
|
+
*
|
|
8242
|
+
* Fills `params` from URL patterns, `query` from `@ApiQueryParams`, and
|
|
8243
|
+
* `body`/`query`/`params` (when schema-validated) from the configured
|
|
8244
|
+
* schema validator. `response` is emitted as `unknown`.
|
|
8245
|
+
*/
|
|
8246
|
+
function renderRoutes(routes, routesOutFile, schemaValidator) {
|
|
8247
|
+
if (routes.length === 0) return `${ROUTES_HEADER}
|
|
8248
|
+
// (no routes discovered yet — annotate a controller method with
|
|
8249
|
+
// @Get/@Post/@Put/@Delete/@Patch and re-run \`kick typegen\`)
|
|
8250
|
+
declare global {
|
|
8251
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
8252
|
+
namespace KickRoutes {}
|
|
8253
|
+
}
|
|
8254
|
+
|
|
8255
|
+
export {}
|
|
8256
|
+
`;
|
|
8257
|
+
const byController = /* @__PURE__ */ new Map();
|
|
8258
|
+
for (const r of routes) {
|
|
8259
|
+
const arr = byController.get(r.controller) ?? [];
|
|
8260
|
+
arr.push(r);
|
|
8261
|
+
byController.set(r.controller, arr);
|
|
8262
|
+
}
|
|
8263
|
+
const schemaImports = /* @__PURE__ */ new Map();
|
|
8264
|
+
const renderField = (schema, routeFilePath) => {
|
|
8265
|
+
const alias = planSchemaImport(schema, routeFilePath, routesOutFile, schemaValidator, schemaImports);
|
|
8266
|
+
return alias ? `import('zod').infer<typeof ${alias}>` : null;
|
|
8267
|
+
};
|
|
8268
|
+
const interfaces = [];
|
|
8269
|
+
for (const [controller, methods] of byController) {
|
|
8270
|
+
const lines = [` interface ${controller} {`];
|
|
8271
|
+
for (const m of methods) {
|
|
8272
|
+
const urlParamsType = m.pathParams.length > 0 ? `{ ${m.pathParams.map((p) => `${p}: string`).join("; ")} }` : "{}";
|
|
8273
|
+
const bodySchemaType = renderField(m.bodySchema, m.filePath);
|
|
8274
|
+
const querySchemaType = renderField(m.querySchema, m.filePath);
|
|
8275
|
+
const paramsType = renderField(m.paramsSchema, m.filePath) ?? urlParamsType;
|
|
8276
|
+
const bodyType = bodySchemaType ?? "unknown";
|
|
8277
|
+
const queryType = querySchemaType ?? renderQueryShape(m);
|
|
8278
|
+
const docLines = renderQueryDocLines(m);
|
|
8279
|
+
lines.push(` /**`, ` * ${m.httpMethod} ${m.path}`, ...docLines.map((d) => ` * ${d}`), ` */`, ` ${m.method}: {`, ` params: ${paramsType}`, ` body: ${bodyType}`, ` query: ${queryType}`, ` response: unknown`, ` }`);
|
|
8280
|
+
}
|
|
8281
|
+
lines.push(" }");
|
|
8282
|
+
interfaces.push(lines.join("\n"));
|
|
8283
|
+
}
|
|
8284
|
+
return `${ROUTES_HEADER}${renderSchemaImports(schemaImports)}
|
|
8285
|
+
declare global {
|
|
8286
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
8287
|
+
namespace KickRoutes {
|
|
8288
|
+
${interfaces.join("\n")}
|
|
8289
|
+
}
|
|
8290
|
+
}
|
|
8291
|
+
|
|
8292
|
+
export {}
|
|
8293
|
+
`;
|
|
8294
|
+
}
|
|
8295
|
+
function renderQueryShape(m) {
|
|
8296
|
+
if (m.queryFilterable === null) return "unknown";
|
|
8297
|
+
const sortable = m.querySortable ?? [];
|
|
8298
|
+
return `{ filter?: string | string[]; sort?: ${sortable.length > 0 ? sortable.flatMap((f) => [`'${f}'`, `'-${f}'`]).join(" | ") : "string"}; q?: string; page?: string; limit?: string }`;
|
|
8299
|
+
}
|
|
8300
|
+
function renderQueryDocLines(m) {
|
|
8301
|
+
const lines = [];
|
|
8302
|
+
if (m.queryFilterable && m.queryFilterable.length > 0) lines.push(`Filterable: ${m.queryFilterable.join(", ")}`);
|
|
8303
|
+
if (m.querySortable && m.querySortable.length > 0) lines.push(`Sortable: ${m.querySortable.join(", ")}`);
|
|
8304
|
+
if (m.querySearchable && m.querySearchable.length > 0) lines.push(`Searchable: ${m.querySearchable.join(", ")}`);
|
|
8305
|
+
return lines;
|
|
8306
|
+
}
|
|
8307
|
+
/**
|
|
8308
|
+
* Plan a schema import for hoisting at the top of `routes.ts`. Returns
|
|
8309
|
+
* the alias the in-namespace code should use, or `null` if the schema
|
|
8310
|
+
* cannot be referenced (no validator configured, or source unresolvable).
|
|
8311
|
+
*/
|
|
8312
|
+
function planSchemaImport(schema, routeFilePath, routesOutFile, schemaValidator, imports) {
|
|
8313
|
+
if (!schema || schemaValidator !== "zod") return null;
|
|
8314
|
+
if (schema.source === null) return null;
|
|
8315
|
+
const specifier = resolveSchemaImportSpecifier(schema.source, routeFilePath, routesOutFile);
|
|
8316
|
+
if (specifier === "unknown") return null;
|
|
8317
|
+
const key = `${specifier}::${schema.identifier}`;
|
|
8318
|
+
let alias = imports.get(key)?.specifier;
|
|
8319
|
+
if (!alias) {
|
|
8320
|
+
alias = `_S${imports.size}`;
|
|
8321
|
+
imports.set(key, {
|
|
8322
|
+
identifier: schema.identifier,
|
|
8323
|
+
specifier: alias
|
|
8324
|
+
});
|
|
8325
|
+
} else alias = imports.get(key).specifier;
|
|
8326
|
+
return alias;
|
|
8327
|
+
}
|
|
8328
|
+
function renderSchemaImports(imports) {
|
|
8329
|
+
if (imports.size === 0) return "";
|
|
8330
|
+
const lines = [];
|
|
8331
|
+
for (const [key, value] of imports) {
|
|
8332
|
+
const [path] = key.split("::");
|
|
8333
|
+
lines.push(`import type { ${value.identifier} as ${value.specifier} } from '${path}'`);
|
|
8334
|
+
}
|
|
8335
|
+
return lines.join("\n") + "\n";
|
|
8336
|
+
}
|
|
8337
|
+
/**
|
|
8338
|
+
* Compute the import specifier the generated `routes.d.ts` should use
|
|
8339
|
+
* to reach a schema declared either in the controller file (empty
|
|
8340
|
+
* string) or imported from elsewhere (relative path or bare module).
|
|
8341
|
+
*/
|
|
8342
|
+
function resolveSchemaImportSpecifier(source, routeFilePath, routesOutFile) {
|
|
8343
|
+
if (source === null) return "unknown";
|
|
8344
|
+
const routesDir = dirname(routesOutFile);
|
|
8345
|
+
if (source === "") {
|
|
8346
|
+
let rel = relative(routesDir, routeFilePath).split(sep).join("/");
|
|
8347
|
+
rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
|
|
8348
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
8349
|
+
return rel;
|
|
8350
|
+
}
|
|
8351
|
+
if (!source.startsWith(".") && !source.startsWith("/")) return source;
|
|
8352
|
+
let rel = relative(routesDir, resolve(dirname(routeFilePath), source)).split(sep).join("/");
|
|
8353
|
+
rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
|
|
8354
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
8355
|
+
return rel;
|
|
8356
|
+
}
|
|
8357
|
+
//#endregion
|
|
8358
|
+
//#region src/typegen/builtin/routes.ts
|
|
8359
|
+
const kickRoutesTypegen = () => ({
|
|
8360
|
+
id: "kick/routes",
|
|
8361
|
+
outExtension: ".ts",
|
|
8362
|
+
inputs: ["src/**/*.controller.ts", "src/**/*.module.ts"],
|
|
8363
|
+
async generate(ctx) {
|
|
8364
|
+
const scan = await ctx.getScanResult({
|
|
8365
|
+
root: resolveSrcDir$1(ctx),
|
|
8366
|
+
cwd: ctx.cwd,
|
|
8367
|
+
envFile: resolveEnvFile$1(ctx)
|
|
8368
|
+
});
|
|
8369
|
+
const schemaValidator = ctx.config?.typegen?.schemaValidator ?? "zod";
|
|
8370
|
+
const outFile = path.resolve(ctx.cwd, ".kickjs/types/kick__routes.ts");
|
|
8371
|
+
return renderRoutes(scan.routes, outFile, schemaValidator);
|
|
8372
|
+
}
|
|
8373
|
+
});
|
|
8374
|
+
function resolveSrcDir$1(ctx) {
|
|
8375
|
+
return path.resolve(ctx.cwd, ctx.config?.typegen?.srcDir ?? "src");
|
|
8376
|
+
}
|
|
8377
|
+
function resolveEnvFile$1(ctx) {
|
|
8378
|
+
const cfg = ctx.config?.typegen?.envFile;
|
|
8379
|
+
if (cfg === false) return void 0;
|
|
8380
|
+
return cfg;
|
|
8381
|
+
}
|
|
8382
|
+
//#endregion
|
|
8383
|
+
//#region src/typegen/render/env.ts
|
|
8384
|
+
const ENV_HEADER = `/* eslint-disable */
|
|
8385
|
+
// AUTO-GENERATED by \`kick typegen\`. DO NOT EDIT.
|
|
8386
|
+
// Re-run with \`kick typegen\` or rely on \`kick dev\` to refresh.
|
|
8387
|
+
`;
|
|
8388
|
+
/**
|
|
8389
|
+
* Render the `KickEnv` + `NodeJS.ProcessEnv` augmentation file from a
|
|
8390
|
+
* detected env schema. Returns `null` when no env file was discovered,
|
|
8391
|
+
* so the caller can skip emission entirely (rather than emitting an
|
|
8392
|
+
* empty augmentation that would shadow `KickEnv` to a useless `{}`).
|
|
8393
|
+
*/
|
|
8394
|
+
function renderEnv(env, envOutFile) {
|
|
8395
|
+
if (!env) return null;
|
|
8396
|
+
let rel = relative(dirname(envOutFile), env.filePath).split(sep).join("/");
|
|
8397
|
+
rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
|
|
8398
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
8399
|
+
return `${ENV_HEADER}
|
|
8400
|
+
// Importing the schema as a type lets us infer its shape without
|
|
8401
|
+
// pulling in any runtime code. \`Awaited<>\` strips an accidental
|
|
8402
|
+
// Promise wrap on dynamic-imported defaults.
|
|
8403
|
+
import type _envSchema from '${rel}'
|
|
8404
|
+
|
|
8405
|
+
// Local type alias — interfaces can only \`extend\` an identifier,
|
|
8406
|
+
// not an inline import expression, so we resolve the schema's
|
|
8407
|
+
// inferred shape into a named type first.
|
|
8408
|
+
type _KickEnvShape = import('zod').infer<typeof _envSchema>
|
|
8409
|
+
|
|
8410
|
+
declare global {
|
|
8411
|
+
/**
|
|
8412
|
+
* Typed environment registry. Augmented from \`${env.relativePath}\`
|
|
8413
|
+
* so \`@Value('PORT')\`, \`Env<'PORT'>\`, and \`process.env.PORT\` are
|
|
8414
|
+
* all type-safe and autocomplete.
|
|
8415
|
+
*/
|
|
8416
|
+
interface KickEnv extends _KickEnvShape {}
|
|
8417
|
+
|
|
8418
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
8419
|
+
namespace NodeJS {
|
|
8420
|
+
/**
|
|
8421
|
+
* Narrow \`process.env\` so known keys exist as \`string\` (the raw
|
|
8422
|
+
* pre-Zod-coercion form). \`@Value\` and the \`ConfigService\` apply
|
|
8423
|
+
* the schema's transforms internally; access \`process.env\` directly
|
|
8424
|
+
* only when you need the raw string. Unknown keys still resolve to
|
|
8425
|
+
* \`string | undefined\` via the base @types/node declaration.
|
|
8426
|
+
*/
|
|
8427
|
+
interface ProcessEnv extends Record<keyof KickEnv, string> {}
|
|
8428
|
+
}
|
|
8429
|
+
}
|
|
8430
|
+
|
|
8431
|
+
export {}
|
|
8432
|
+
`;
|
|
8433
|
+
}
|
|
8434
|
+
//#endregion
|
|
8435
|
+
//#region src/typegen/builtin/env.ts
|
|
8436
|
+
const kickEnvTypegen = () => ({
|
|
8437
|
+
id: "kick/env",
|
|
8438
|
+
outExtension: ".ts",
|
|
8439
|
+
inputs: [
|
|
8440
|
+
"src/env.ts",
|
|
8441
|
+
"src/**/env.ts",
|
|
8442
|
+
"src/**/*.env.ts"
|
|
8443
|
+
],
|
|
8444
|
+
async generate(ctx) {
|
|
8445
|
+
const envFile = resolveEnvFile(ctx);
|
|
8446
|
+
if (envFile === false) return null;
|
|
8447
|
+
const scan = await ctx.getScanResult({
|
|
8448
|
+
root: resolveSrcDir(ctx),
|
|
8449
|
+
cwd: ctx.cwd,
|
|
8450
|
+
envFile
|
|
8451
|
+
});
|
|
8452
|
+
if (!scan.env) return null;
|
|
8453
|
+
const outFile = path.resolve(ctx.cwd, ".kickjs/types/kick__env.ts");
|
|
8454
|
+
return renderEnv(scan.env, outFile);
|
|
8455
|
+
}
|
|
8456
|
+
});
|
|
8457
|
+
function resolveSrcDir(ctx) {
|
|
8458
|
+
return path.resolve(ctx.cwd, ctx.config?.typegen?.srcDir ?? "src");
|
|
8459
|
+
}
|
|
8460
|
+
function resolveEnvFile(ctx) {
|
|
8461
|
+
return ctx.config?.typegen?.envFile;
|
|
8462
|
+
}
|
|
8463
|
+
//#endregion
|
|
8050
8464
|
//#region src/plugin/builtins.ts
|
|
8051
8465
|
var builtins_exports = /* @__PURE__ */ __exportAll({ builtinCliPlugins: () => builtinCliPlugins });
|
|
8052
8466
|
const builtinCliPlugins = [
|
|
@@ -8110,6 +8524,14 @@ const builtinCliPlugins = [
|
|
|
8110
8524
|
defineCliPlugin({
|
|
8111
8525
|
name: "kick/assets",
|
|
8112
8526
|
typegens: [kickAssetsTypegen()]
|
|
8527
|
+
}),
|
|
8528
|
+
defineCliPlugin({
|
|
8529
|
+
name: "kick/routes",
|
|
8530
|
+
typegens: [kickRoutesTypegen()]
|
|
8531
|
+
}),
|
|
8532
|
+
defineCliPlugin({
|
|
8533
|
+
name: "kick/env",
|
|
8534
|
+
typegens: [kickEnvTypegen()]
|
|
8113
8535
|
})
|
|
8114
8536
|
];
|
|
8115
8537
|
//#endregion
|