@forinda/kickjs-cli 5.2.0 → 5.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtins-BdvmVAJ1.mjs +3740 -0
- package/dist/builtins-Du70nybS.mjs +1066 -0
- package/dist/builtins-Du70nybS.mjs.map +1 -0
- package/dist/cli.mjs +2 -120
- package/dist/config-Dzw8Ws4d.mjs +11 -0
- package/dist/config-lCKbrRnt.mjs +12 -0
- package/dist/{config-DDrgs-I3.mjs.map → config-lCKbrRnt.mjs.map} +1 -1
- package/dist/generator-extension-Cp5FUUAw.mjs +2687 -0
- package/dist/generator-extension-Cp5FUUAw.mjs.map +1 -0
- package/dist/index.mjs +2 -5
- package/dist/plugin-Dv2gKsuC.mjs +11 -0
- package/dist/plugin-VPl_QQGb.mjs +12 -0
- package/dist/{plugin-6_YlK-JG.mjs.map → plugin-VPl_QQGb.mjs.map} +1 -1
- package/dist/rolldown-runtime-B6QC8dMY.mjs +11 -0
- package/dist/{run-plugins-B1R0HG0g.mjs → run-plugins-CM1Af-4B.mjs} +2 -3
- package/dist/typegen-C6ZfoYTC.mjs +114 -0
- package/dist/typegen-CBI7dNXr.mjs +115 -0
- package/dist/{typegen-DugZmi-0.mjs.map → typegen-CBI7dNXr.mjs.map} +1 -1
- package/dist/types-n4LRUF_c.mjs +12 -0
- package/dist/{types-CGB8BiQh.mjs.map → types-n4LRUF_c.mjs.map} +1 -1
- package/package.json +5 -5
- package/dist/builtins-BW3g09hP.mjs +0 -8538
- package/dist/builtins-C_VfEGdg.mjs +0 -4182
- package/dist/builtins-C_VfEGdg.mjs.map +0 -1
- package/dist/config-DDrgs-I3.mjs +0 -171
- package/dist/config-DsQe2yzy.mjs +0 -169
- package/dist/generator-extension-DRNQpoZP.mjs +0 -4380
- package/dist/generator-extension-DRNQpoZP.mjs.map +0 -1
- package/dist/plugin-6_YlK-JG.mjs +0 -71
- package/dist/plugin-CQ0yYXyr.mjs +0 -80
- package/dist/rolldown-runtime-CYBbkZNy.mjs +0 -24
- package/dist/typegen-CYCsmCRF.mjs +0 -1351
- package/dist/typegen-DugZmi-0.mjs +0 -1353
- package/dist/types-CGB8BiQh.mjs +0 -25
|
@@ -1,1351 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @forinda/kickjs-cli v5.2.0
|
|
3
|
-
*
|
|
4
|
-
* Copyright (c) Felix Orinda
|
|
5
|
-
*
|
|
6
|
-
* This source code is licensed under the MIT license found in the
|
|
7
|
-
* LICENSE file in the root directory of this source tree.
|
|
8
|
-
*
|
|
9
|
-
* @license MIT
|
|
10
|
-
*/
|
|
11
|
-
import { t as __exportAll } from "./rolldown-runtime-CYBbkZNy.mjs";
|
|
12
|
-
import { statSync } from "node:fs";
|
|
13
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
14
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
15
|
-
import { globSync } from "glob";
|
|
16
|
-
import { groupAssetKeys } from "@forinda/kickjs";
|
|
17
|
-
//#region src/typegen/scanner.ts
|
|
18
|
-
/** Decorators that mark a class as DI-managed */
|
|
19
|
-
const DECORATOR_NAMES = [
|
|
20
|
-
"Service",
|
|
21
|
-
"Controller",
|
|
22
|
-
"Repository",
|
|
23
|
-
"Injectable",
|
|
24
|
-
"Component",
|
|
25
|
-
"Module"
|
|
26
|
-
];
|
|
27
|
-
const DEFAULT_EXTENSIONS = [
|
|
28
|
-
".ts",
|
|
29
|
-
".tsx",
|
|
30
|
-
".mts",
|
|
31
|
-
".cts"
|
|
32
|
-
];
|
|
33
|
-
const DEFAULT_EXCLUDES = [
|
|
34
|
-
"node_modules",
|
|
35
|
-
".kickjs",
|
|
36
|
-
"dist",
|
|
37
|
-
"build",
|
|
38
|
-
".test.",
|
|
39
|
-
".spec.",
|
|
40
|
-
".d.ts"
|
|
41
|
-
];
|
|
42
|
-
/**
|
|
43
|
-
* Match a class-level decorator immediately followed by an exported
|
|
44
|
-
* class declaration. Captures decorator name and class name.
|
|
45
|
-
*/
|
|
46
|
-
const DECORATED_CLASS_REGEX = new RegExp(String.raw`@(${DECORATOR_NAMES.join("|")})\s*\([^)]*\)` + String.raw`(?:\s*@[A-Z]\w*(?:\s*\([^)]*\))?)*` + String.raw`\s*export\s+(default\s+)?(?:abstract\s+)?class\s+(\w+)`, "g");
|
|
47
|
-
/**
|
|
48
|
-
* Match an exported class declaration that implements `AppModule`.
|
|
49
|
-
* KickJS modules are not decorated — they implement the `AppModule`
|
|
50
|
-
* interface — so the decorated-class scanner never picks them up. This
|
|
51
|
-
* regex captures them by name so `ModuleToken` can be populated.
|
|
52
|
-
*
|
|
53
|
-
* Tolerates an `extends BaseClass` clause before `implements`, multiple
|
|
54
|
-
* implements clauses (`implements Foo, AppModule`), and `default` exports.
|
|
55
|
-
*/
|
|
56
|
-
const APP_MODULE_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+[^{]*\bAppModule\b`, "g");
|
|
57
|
-
/**
|
|
58
|
-
* Match a `createToken<T>('name')` call with optional `export const X =`
|
|
59
|
-
* or `const X =` prefix. Tolerates whitespace and the type parameter
|
|
60
|
-
* being absent (`createToken('name')`).
|
|
61
|
-
*/
|
|
62
|
-
const CREATE_TOKEN_REGEX = /(?:export\s+)?const\s+(\w+)\s*(?::\s*[^=]+)?=\s*createToken\s*(?:<[^>]*>)?\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
63
|
-
/**
|
|
64
|
-
* Match a bare `createToken<T>('name')` call (no const assignment) so
|
|
65
|
-
* we still pick up dynamically-used tokens.
|
|
66
|
-
*/
|
|
67
|
-
const BARE_CREATE_TOKEN_REGEX = /createToken\s*(?:<[^>]*>)?\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
68
|
-
/** Match `@Inject('literal')` — only literals; computed args are skipped */
|
|
69
|
-
const INJECT_LITERAL_REGEX = /@Inject\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
70
|
-
/**
|
|
71
|
-
* Match the start of a `defineAdapter(...)` or `definePlugin(...)` call,
|
|
72
|
-
* tolerating optional `<TConfig, TExtra>` generics. Captures the helper
|
|
73
|
-
* name. The callsite's first-arg object is parsed forward via
|
|
74
|
-
* `findBalancedClose` so nested objects/parens don't confuse us.
|
|
75
|
-
*/
|
|
76
|
-
const DEFINE_HELPER_START = /\b(defineAdapter|definePlugin)\s*(?:<[^>]*>)?\s*\(/g;
|
|
77
|
-
/**
|
|
78
|
-
* Match a class declaration whose `implements` clause includes `AppAdapter`.
|
|
79
|
-
* Captures the class name. Used to pick up the (rare, post-defineAdapter)
|
|
80
|
-
* legacy class-style adapters so their literal `name = '...'` field can
|
|
81
|
-
* still feed `KickJsPluginRegistry`.
|
|
82
|
-
*/
|
|
83
|
-
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");
|
|
84
|
-
/** Match a string-literal `name = '...'` field on a class body. */
|
|
85
|
-
const CLASS_NAME_FIELD_REGEX = /\bname\s*(?::\s*[^=]+)?=\s*['"`]([^'"`]+)['"`]/;
|
|
86
|
-
/**
|
|
87
|
-
* Match the start of a `defineAugmentation('Name', ...)` call. Captures
|
|
88
|
-
* the literal name. The optional second-arg object is parsed forward so
|
|
89
|
-
* `description` / `example` can be pulled out.
|
|
90
|
-
*/
|
|
91
|
-
const DEFINE_AUGMENTATION_START = /\bdefineAugmentation\s*\(\s*['"`]([^'"`]+)['"`]\s*(,\s*\{)?/g;
|
|
92
|
-
/**
|
|
93
|
-
* Locate the start of a route decorator: `@Get(`, `@Post(`, etc.
|
|
94
|
-
* Used by `extractRoutesFromSource`; the rest of the route declaration
|
|
95
|
-
* (balanced parens, stacked decorators, method name) is parsed by walking
|
|
96
|
-
* the source forward from this match. The previous all-in-one regex
|
|
97
|
-
* couldn't handle nested parens in stacked decorator args (e.g.
|
|
98
|
-
* `@ApiResponse(201, { schema: z.object({ id: z.string() }) })`) — see
|
|
99
|
-
* forinda/kick-js#108.
|
|
100
|
-
*/
|
|
101
|
-
const ROUTE_DECORATOR_START = new RegExp(String.raw`@(${[
|
|
102
|
-
"Get",
|
|
103
|
-
"Post",
|
|
104
|
-
"Put",
|
|
105
|
-
"Delete",
|
|
106
|
-
"Patch"
|
|
107
|
-
].join("|")})\s*\(`, "g");
|
|
108
|
-
/**
|
|
109
|
-
* Find the index of the `)` that balances the `(` at `openPos`.
|
|
110
|
-
* Returns -1 if no matching `)` exists. Counts balanced parens only;
|
|
111
|
-
* does not understand string literals, so a `(` or `)` inside a string
|
|
112
|
-
* inside the args will skew the depth counter (matches the limitation
|
|
113
|
-
* of `extractRouteOptionsArg`).
|
|
114
|
-
*/
|
|
115
|
-
function findBalancedClose(text, openPos) {
|
|
116
|
-
let depth = 1;
|
|
117
|
-
for (let i = openPos + 1; i < text.length; i++) {
|
|
118
|
-
const ch = text[i];
|
|
119
|
-
if (ch === "(") depth++;
|
|
120
|
-
else if (ch === ")") {
|
|
121
|
-
depth--;
|
|
122
|
-
if (depth === 0) return i;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return -1;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Walk forward from the end of a route decorator past any stacked
|
|
129
|
-
* decorators (`@ApiOperation(...)`, `@ApiResponse(...)`, `@Middleware(fn)`,
|
|
130
|
-
* etc.), then past optional `public`/`private`/`protected` and `async`,
|
|
131
|
-
* and capture the method name + opening `(`.
|
|
132
|
-
*
|
|
133
|
-
* Returns the method name and the position immediately after the method's
|
|
134
|
-
* opening `(`, or `null` if the source between the route decorator and
|
|
135
|
-
* the method body doesn't fit the expected shape.
|
|
136
|
-
*/
|
|
137
|
-
function readMethodAfterDecorators(block, startPos) {
|
|
138
|
-
let pos = startPos;
|
|
139
|
-
while (pos < block.length) {
|
|
140
|
-
while (pos < block.length && /\s/.test(block[pos])) pos++;
|
|
141
|
-
if (block[pos] !== "@") break;
|
|
142
|
-
const decMatch = block.slice(pos).match(/^@([A-Z]\w*)/);
|
|
143
|
-
if (!decMatch) break;
|
|
144
|
-
pos += decMatch[0].length;
|
|
145
|
-
while (pos < block.length && /\s/.test(block[pos])) pos++;
|
|
146
|
-
if (block[pos] === "(") {
|
|
147
|
-
const close = findBalancedClose(block, pos);
|
|
148
|
-
if (close < 0) return null;
|
|
149
|
-
pos = close + 1;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
while (pos < block.length && /\s/.test(block[pos])) pos++;
|
|
153
|
-
for (const mod of [
|
|
154
|
-
"public",
|
|
155
|
-
"private",
|
|
156
|
-
"protected"
|
|
157
|
-
]) if (block.slice(pos, pos + mod.length) === mod && /\s/.test(block.charAt(pos + mod.length))) {
|
|
158
|
-
pos += mod.length;
|
|
159
|
-
while (pos < block.length && /\s/.test(block[pos])) pos++;
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
if (block.slice(pos, pos + 5) === "async" && /\s/.test(block.charAt(pos + 5))) {
|
|
163
|
-
pos += 5;
|
|
164
|
-
while (pos < block.length && /\s/.test(block[pos])) pos++;
|
|
165
|
-
}
|
|
166
|
-
const methodMatch = block.slice(pos).match(/^([a-zA-Z_]\w*)\s*\(/);
|
|
167
|
-
if (!methodMatch) return null;
|
|
168
|
-
return {
|
|
169
|
-
methodName: methodMatch[1],
|
|
170
|
-
endPos: pos + methodMatch[0].length
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
/** Extract `:placeholder` segments from an Express route path */
|
|
174
|
-
function extractPathParams(path) {
|
|
175
|
-
return (path.match(/:([a-zA-Z_]\w*)/g) ?? []).map((m) => m.slice(1));
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Extract a bare identifier value from a single field in an object literal
|
|
179
|
-
* embedded in a string. Returns `null` if the field is missing or its value
|
|
180
|
-
* isn't a bare identifier (e.g. an inline object, function call, etc.).
|
|
181
|
-
*
|
|
182
|
-
* Example: `extractObjectFieldIdentifier("'/' , { body: createTaskSchema }", 'body')`
|
|
183
|
-
* returns `'createTaskSchema'`.
|
|
184
|
-
*/
|
|
185
|
-
function extractObjectFieldIdentifier(text, field) {
|
|
186
|
-
const m = new RegExp(String.raw`\b${field}\s*:\s*([A-Za-z_$][\w$]*)`, "g").exec(text);
|
|
187
|
-
if (!m) return null;
|
|
188
|
-
return m[1];
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Resolve a bare identifier to its module source by inspecting the file's
|
|
192
|
-
* top-level imports and same-file `const` declarations.
|
|
193
|
-
*
|
|
194
|
-
* - `import { X } from './path'` → returns `'./path'`
|
|
195
|
-
* - `import X from './path'` (default import) → returns `'./path'`
|
|
196
|
-
* - `import * as X from './path'` → returns `'./path'`
|
|
197
|
-
* - `const X = z.object(...)` (same file) → returns `null` (caller emits a self-import)
|
|
198
|
-
*
|
|
199
|
-
* Returns `null` when the identifier cannot be resolved.
|
|
200
|
-
*/
|
|
201
|
-
function resolveImportSource(source, identifier) {
|
|
202
|
-
const named = new RegExp(String.raw`import\s*(?:type\s+)?\{[^}]*\b${identifier}\b[^}]*\}\s*from\s*['"\`]([^'"\`]+)['"\`]`).exec(source);
|
|
203
|
-
if (named) return named[1];
|
|
204
|
-
const def = new RegExp(String.raw`import\s+(?:type\s+)?${identifier}\s+from\s*['"\`]([^'"\`]+)['"\`]`).exec(source);
|
|
205
|
-
if (def) return def[1];
|
|
206
|
-
const ns = new RegExp(String.raw`import\s*\*\s*as\s+${identifier}\s+from\s*['"\`]([^'"\`]+)['"\`]`).exec(source);
|
|
207
|
-
if (ns) return ns[1];
|
|
208
|
-
if (new RegExp(String.raw`(?:^|\n)\s*(?:export\s+)?const\s+${identifier}\b`).test(source)) return "";
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Extract whitelist arrays from an `@ApiQueryParams(...)` decorator
|
|
213
|
-
* within `decoratorBlock`. Handles two forms:
|
|
214
|
-
*
|
|
215
|
-
* - Inline literal: `@ApiQueryParams({ filterable: ['a', 'b'], ... })`
|
|
216
|
-
* - Const reference: `@ApiQueryParams(SOME_CONFIG)` — looks up
|
|
217
|
-
* `const SOME_CONFIG = { ... }` in the same file (`fullSource`).
|
|
218
|
-
*
|
|
219
|
-
* Returns `null` if no `@ApiQueryParams` is present. Returns
|
|
220
|
-
* `{ filterable: [], sortable: [], searchable: [] }` if the decorator
|
|
221
|
-
* is present but no fields could be statically extracted (opaque
|
|
222
|
-
* imports, column-object configs, function calls, etc.).
|
|
223
|
-
*/
|
|
224
|
-
function extractApiQueryParams(decoratorBlock, fullSource) {
|
|
225
|
-
const apiMatch = /@ApiQueryParams\s*\(\s*([\s\S]*?)\s*\)\s*$/.exec(decoratorBlock);
|
|
226
|
-
if (!apiMatch) {
|
|
227
|
-
const loose = /@ApiQueryParams\s*\(([\s\S]*?)\)/.exec(decoratorBlock);
|
|
228
|
-
if (!loose) return null;
|
|
229
|
-
return parseApiQueryParamsArg(loose[1].trim(), fullSource);
|
|
230
|
-
}
|
|
231
|
-
return parseApiQueryParamsArg(apiMatch[1].trim(), fullSource);
|
|
232
|
-
}
|
|
233
|
-
function parseApiQueryParamsArg(arg, fullSource) {
|
|
234
|
-
if (arg.startsWith("{")) return parseInlineConfigLiteral(arg);
|
|
235
|
-
const idMatch = /^([A-Za-z_]\w*)/.exec(arg);
|
|
236
|
-
if (idMatch) {
|
|
237
|
-
const ident = idMatch[1];
|
|
238
|
-
const constMatch = new RegExp(String.raw`const\s+${ident}\s*(?::\s*[^=]+)?=\s*(\{[\s\S]*?\n\})`, "m").exec(fullSource);
|
|
239
|
-
if (constMatch) return parseInlineConfigLiteral(constMatch[1]);
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
filterable: [],
|
|
243
|
-
sortable: [],
|
|
244
|
-
searchable: []
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
/** Extract a string array literal for one config key from an inline object literal */
|
|
248
|
-
function extractStringArray(literal, key) {
|
|
249
|
-
const m = new RegExp(String.raw`${key}\s*:\s*\[([\s\S]*?)\]`).exec(literal);
|
|
250
|
-
if (!m) return [];
|
|
251
|
-
return Array.from(m[1].matchAll(/['"`]([^'"`]+)['"`]/g)).map((x) => x[1]);
|
|
252
|
-
}
|
|
253
|
-
/** Parse an inline `{ filterable: [...], sortable: [...], searchable: [...] }` literal */
|
|
254
|
-
function parseInlineConfigLiteral(literal) {
|
|
255
|
-
return {
|
|
256
|
-
filterable: extractStringArray(literal, "filterable"),
|
|
257
|
-
sortable: extractStringArray(literal, "sortable"),
|
|
258
|
-
searchable: extractStringArray(literal, "searchable")
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
/** Recursively walk a directory and yield matching file paths */
|
|
262
|
-
async function walk(dir, opts) {
|
|
263
|
-
const exts = opts.extensions ?? DEFAULT_EXTENSIONS;
|
|
264
|
-
const excludes = opts.exclude ?? DEFAULT_EXCLUDES;
|
|
265
|
-
const out = [];
|
|
266
|
-
let entries;
|
|
267
|
-
try {
|
|
268
|
-
entries = await readdir(dir, {
|
|
269
|
-
withFileTypes: true,
|
|
270
|
-
encoding: "utf-8"
|
|
271
|
-
});
|
|
272
|
-
} catch {
|
|
273
|
-
return out;
|
|
274
|
-
}
|
|
275
|
-
for (const entry of entries) {
|
|
276
|
-
const full = join(dir, entry.name);
|
|
277
|
-
const rel = relative(opts.cwd, full);
|
|
278
|
-
if (excludes.some((ex) => rel.includes(ex))) continue;
|
|
279
|
-
if (entry.isDirectory()) out.push(...await walk(full, opts));
|
|
280
|
-
else if (entry.isFile()) {
|
|
281
|
-
if (exts.some((ext) => entry.name.endsWith(ext))) out.push(full);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return out;
|
|
285
|
-
}
|
|
286
|
-
/** Compute the forward-slash relative path used in scanner output */
|
|
287
|
-
function toRelative(filePath, cwd) {
|
|
288
|
-
return relative(cwd, filePath).split(sep).join("/");
|
|
289
|
-
}
|
|
290
|
-
/** Extract decorated classes from a single source file */
|
|
291
|
-
function extractClassesFromSource(source, filePath, cwd) {
|
|
292
|
-
const out = [];
|
|
293
|
-
const relPath = toRelative(filePath, cwd);
|
|
294
|
-
DECORATED_CLASS_REGEX.lastIndex = 0;
|
|
295
|
-
let match;
|
|
296
|
-
while ((match = DECORATED_CLASS_REGEX.exec(source)) !== null) {
|
|
297
|
-
const [, decorator, defaultMarker, className] = match;
|
|
298
|
-
out.push({
|
|
299
|
-
className,
|
|
300
|
-
decorator,
|
|
301
|
-
filePath,
|
|
302
|
-
relativePath: relPath,
|
|
303
|
-
isDefault: Boolean(defaultMarker)
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
APP_MODULE_CLASS_REGEX.lastIndex = 0;
|
|
307
|
-
let modMatch;
|
|
308
|
-
while ((modMatch = APP_MODULE_CLASS_REGEX.exec(source)) !== null) {
|
|
309
|
-
const [, defaultMarker, className] = modMatch;
|
|
310
|
-
if (out.some((c) => c.className === className && c.filePath === filePath)) continue;
|
|
311
|
-
out.push({
|
|
312
|
-
className,
|
|
313
|
-
decorator: "Module",
|
|
314
|
-
filePath,
|
|
315
|
-
relativePath: relPath,
|
|
316
|
-
isDefault: Boolean(defaultMarker)
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
return out;
|
|
320
|
-
}
|
|
321
|
-
/** Extract `createToken('name')` definitions from a single source file */
|
|
322
|
-
function extractTokensFromSource(source, filePath, cwd) {
|
|
323
|
-
const out = [];
|
|
324
|
-
const relPath = toRelative(filePath, cwd);
|
|
325
|
-
const seen = /* @__PURE__ */ new Set();
|
|
326
|
-
CREATE_TOKEN_REGEX.lastIndex = 0;
|
|
327
|
-
let match;
|
|
328
|
-
while ((match = CREATE_TOKEN_REGEX.exec(source)) !== null) {
|
|
329
|
-
const [full, variable, name] = match;
|
|
330
|
-
seen.add(full);
|
|
331
|
-
out.push({
|
|
332
|
-
name,
|
|
333
|
-
variable,
|
|
334
|
-
filePath,
|
|
335
|
-
relativePath: relPath
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
BARE_CREATE_TOKEN_REGEX.lastIndex = 0;
|
|
339
|
-
while ((match = BARE_CREATE_TOKEN_REGEX.exec(source)) !== null) {
|
|
340
|
-
if (seen.has(match[0])) continue;
|
|
341
|
-
out.push({
|
|
342
|
-
name: match[1],
|
|
343
|
-
variable: null,
|
|
344
|
-
filePath,
|
|
345
|
-
relativePath: relPath
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
return out;
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Extract route handlers from a source file.
|
|
352
|
-
*
|
|
353
|
-
* For each decorated class in `classesInFile`, slices the source from
|
|
354
|
-
* the class declaration to the next class (or EOF) and runs the route
|
|
355
|
-
* decorator regex within that slice. The result is a list of routes
|
|
356
|
-
* tagged with their owning controller.
|
|
357
|
-
*
|
|
358
|
-
* Heuristic note: this assumes classes are not nested. KickJS controllers
|
|
359
|
-
* are top-level by convention so this holds in practice.
|
|
360
|
-
*/
|
|
361
|
-
function extractRoutesFromSource(source, filePath, cwd, classesInFile) {
|
|
362
|
-
const out = [];
|
|
363
|
-
if (classesInFile.length === 0) return out;
|
|
364
|
-
const relPath = toRelative(filePath, cwd);
|
|
365
|
-
const positions = [];
|
|
366
|
-
for (const cls of classesInFile) {
|
|
367
|
-
const m = new RegExp(String.raw`class\s+${cls.className}\b`).exec(source);
|
|
368
|
-
if (m?.index !== void 0) positions.push({
|
|
369
|
-
cls,
|
|
370
|
-
start: m.index
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
positions.sort((a, b) => a.start - b.start);
|
|
374
|
-
for (let i = 0; i < positions.length; i++) {
|
|
375
|
-
const { cls, start } = positions[i];
|
|
376
|
-
const end = i + 1 < positions.length ? positions[i + 1].start : source.length;
|
|
377
|
-
const block = source.slice(start, end);
|
|
378
|
-
ROUTE_DECORATOR_START.lastIndex = 0;
|
|
379
|
-
let startMatch;
|
|
380
|
-
while ((startMatch = ROUTE_DECORATOR_START.exec(block)) !== null) {
|
|
381
|
-
const verb = startMatch[1];
|
|
382
|
-
const decoratorStart = startMatch.index;
|
|
383
|
-
const openParen = ROUTE_DECORATOR_START.lastIndex - 1;
|
|
384
|
-
const closeParen = findBalancedClose(block, openParen);
|
|
385
|
-
if (closeParen < 0) continue;
|
|
386
|
-
const routeArgs = block.slice(openParen + 1, closeParen);
|
|
387
|
-
const pathLiteralMatch = routeArgs.match(/^\s*['"`]([^'"`]*)['"`]/);
|
|
388
|
-
const path = pathLiteralMatch && pathLiteralMatch[1].length > 0 ? pathLiteralMatch[1] : "/";
|
|
389
|
-
const methodInfo = readMethodAfterDecorators(block, closeParen + 1);
|
|
390
|
-
if (!methodInfo) continue;
|
|
391
|
-
const { methodName, endPos } = methodInfo;
|
|
392
|
-
ROUTE_DECORATOR_START.lastIndex = endPos;
|
|
393
|
-
const apiQp = extractApiQueryParams(block.slice(decoratorStart, endPos), source);
|
|
394
|
-
const bodyId = extractObjectFieldIdentifier(routeArgs, "body");
|
|
395
|
-
const queryId = extractObjectFieldIdentifier(routeArgs, "query");
|
|
396
|
-
const paramsId = extractObjectFieldIdentifier(routeArgs, "params");
|
|
397
|
-
out.push({
|
|
398
|
-
controller: cls.className,
|
|
399
|
-
method: methodName,
|
|
400
|
-
httpMethod: verb.toUpperCase(),
|
|
401
|
-
path,
|
|
402
|
-
pathParams: extractPathParams(path),
|
|
403
|
-
queryFilterable: apiQp?.filterable ?? null,
|
|
404
|
-
querySortable: apiQp?.sortable ?? null,
|
|
405
|
-
querySearchable: apiQp?.searchable ?? null,
|
|
406
|
-
bodySchema: bodyId ? {
|
|
407
|
-
identifier: bodyId,
|
|
408
|
-
source: resolveImportSource(source, bodyId)
|
|
409
|
-
} : null,
|
|
410
|
-
querySchema: queryId ? {
|
|
411
|
-
identifier: queryId,
|
|
412
|
-
source: resolveImportSource(source, queryId)
|
|
413
|
-
} : null,
|
|
414
|
-
paramsSchema: paramsId ? {
|
|
415
|
-
identifier: paramsId,
|
|
416
|
-
source: resolveImportSource(source, paramsId)
|
|
417
|
-
} : null,
|
|
418
|
-
filePath,
|
|
419
|
-
relativePath: relPath
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return out;
|
|
424
|
-
}
|
|
425
|
-
/** Extract `@Inject('literal')` calls from a single source file */
|
|
426
|
-
function extractInjectsFromSource(source, filePath, cwd) {
|
|
427
|
-
const out = [];
|
|
428
|
-
const relPath = toRelative(filePath, cwd);
|
|
429
|
-
INJECT_LITERAL_REGEX.lastIndex = 0;
|
|
430
|
-
let match;
|
|
431
|
-
while ((match = INJECT_LITERAL_REGEX.exec(source)) !== null) out.push({
|
|
432
|
-
name: match[1],
|
|
433
|
-
filePath,
|
|
434
|
-
relativePath: relPath
|
|
435
|
-
});
|
|
436
|
-
return out;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Extract the bounds of an object literal that begins at `openBracePos`
|
|
440
|
-
* (the index of the `{` character). Returns the index of the matching `}`
|
|
441
|
-
* or -1 if no match is found. Counts balanced braces only — does not
|
|
442
|
-
* understand string literals so a `{` or `}` inside a string inside the
|
|
443
|
-
* object will skew the depth counter (matches `findBalancedClose`).
|
|
444
|
-
*/
|
|
445
|
-
function findBalancedBrace(text, openBracePos) {
|
|
446
|
-
let depth = 1;
|
|
447
|
-
for (let i = openBracePos + 1; i < text.length; i++) {
|
|
448
|
-
const ch = text[i];
|
|
449
|
-
if (ch === "{") depth++;
|
|
450
|
-
else if (ch === "}") {
|
|
451
|
-
depth--;
|
|
452
|
-
if (depth === 0) return i;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return -1;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Extract plugins/adapters declared via `defineAdapter({ name: '...' })`
|
|
459
|
-
* or `definePlugin({ name: '...' })` calls and via class-style adapters
|
|
460
|
-
* (`class XxxAdapter implements AppAdapter` with a string-literal `name`
|
|
461
|
-
* field).
|
|
462
|
-
*
|
|
463
|
-
* Only the literal `name:` field feeds the result — the symbol on the LHS
|
|
464
|
-
* is irrelevant since `dependsOn` references the runtime name.
|
|
465
|
-
*/
|
|
466
|
-
function extractPluginsAndAdaptersFromSource(source, filePath, cwd) {
|
|
467
|
-
const out = [];
|
|
468
|
-
const relPath = toRelative(filePath, cwd);
|
|
469
|
-
const seen = /* @__PURE__ */ new Set();
|
|
470
|
-
DEFINE_HELPER_START.lastIndex = 0;
|
|
471
|
-
let helperMatch;
|
|
472
|
-
while ((helperMatch = DEFINE_HELPER_START.exec(source)) !== null) {
|
|
473
|
-
const helper = helperMatch[1];
|
|
474
|
-
const openParen = DEFINE_HELPER_START.lastIndex - 1;
|
|
475
|
-
const closeParen = findBalancedClose(source, openParen);
|
|
476
|
-
if (closeParen < 0) continue;
|
|
477
|
-
const callArgs = source.slice(openParen + 1, closeParen);
|
|
478
|
-
const nameMatch = /\bname\s*:\s*['"`]([^'"`]+)['"`]/.exec(callArgs);
|
|
479
|
-
if (!nameMatch) continue;
|
|
480
|
-
const name = nameMatch[1];
|
|
481
|
-
const dedupeKey = `${helper}::${name}::${filePath}`;
|
|
482
|
-
if (seen.has(dedupeKey)) continue;
|
|
483
|
-
seen.add(dedupeKey);
|
|
484
|
-
out.push({
|
|
485
|
-
kind: helper === "definePlugin" ? "plugin" : "adapter",
|
|
486
|
-
name,
|
|
487
|
-
filePath,
|
|
488
|
-
relativePath: relPath
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
APP_ADAPTER_CLASS_REGEX.lastIndex = 0;
|
|
492
|
-
let classMatch;
|
|
493
|
-
while ((classMatch = APP_ADAPTER_CLASS_REGEX.exec(source)) !== null) {
|
|
494
|
-
const classStart = classMatch.index;
|
|
495
|
-
const bracePos = source.indexOf("{", classStart);
|
|
496
|
-
if (bracePos < 0) continue;
|
|
497
|
-
const closeBrace = findBalancedBrace(source, bracePos);
|
|
498
|
-
if (closeBrace < 0) continue;
|
|
499
|
-
const body = source.slice(bracePos + 1, closeBrace);
|
|
500
|
-
const nameMatch = CLASS_NAME_FIELD_REGEX.exec(body);
|
|
501
|
-
if (!nameMatch) continue;
|
|
502
|
-
const name = nameMatch[1];
|
|
503
|
-
const dedupeKey = `class::${name}::${filePath}`;
|
|
504
|
-
if (seen.has(dedupeKey)) continue;
|
|
505
|
-
seen.add(dedupeKey);
|
|
506
|
-
out.push({
|
|
507
|
-
kind: "adapter",
|
|
508
|
-
name,
|
|
509
|
-
filePath,
|
|
510
|
-
relativePath: relPath
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
return out;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Extract `defineAugmentation('Name', { description, example })` calls
|
|
517
|
-
* from a source file. The metadata object is optional — when absent both
|
|
518
|
-
* `description` and `example` resolve to `null`.
|
|
519
|
-
*/
|
|
520
|
-
function extractAugmentationsFromSource(source, filePath, cwd) {
|
|
521
|
-
const out = [];
|
|
522
|
-
const relPath = toRelative(filePath, cwd);
|
|
523
|
-
DEFINE_AUGMENTATION_START.lastIndex = 0;
|
|
524
|
-
let match;
|
|
525
|
-
while ((match = DEFINE_AUGMENTATION_START.exec(source)) !== null) {
|
|
526
|
-
const name = match[1];
|
|
527
|
-
let description = null;
|
|
528
|
-
let example = null;
|
|
529
|
-
if (match[2]) {
|
|
530
|
-
const bracePos = source.indexOf("{", match.index + match[0].length - 1);
|
|
531
|
-
if (bracePos >= 0) {
|
|
532
|
-
const closeBrace = findBalancedBrace(source, bracePos);
|
|
533
|
-
if (closeBrace >= 0) {
|
|
534
|
-
const body = source.slice(bracePos + 1, closeBrace);
|
|
535
|
-
description = readStringField(body, "description");
|
|
536
|
-
example = readStringField(body, "example");
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
out.push({
|
|
541
|
-
name,
|
|
542
|
-
description,
|
|
543
|
-
example,
|
|
544
|
-
filePath,
|
|
545
|
-
relativePath: relPath
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
return out;
|
|
549
|
-
}
|
|
550
|
-
/**
|
|
551
|
-
* Pull a string-valued field out of a JS object-literal body, respecting
|
|
552
|
-
* the opening quote so the value isn't truncated at the first foreign
|
|
553
|
-
* quote character. Handles backslash escapes inside the literal.
|
|
554
|
-
*
|
|
555
|
-
* Why a custom parser instead of one regex per delimiter: real-world
|
|
556
|
-
* `defineAugmentation` calls embed all three quote characters at once
|
|
557
|
-
* — backtick template literals carrying TS shapes like
|
|
558
|
-
* `'free' | 'pro'` (single quotes) AND `\`ctx.get(...)\`` (escaped
|
|
559
|
-
* backticks). A character-class regex like `[^'"`]+` truncates on the
|
|
560
|
-
* first foreign quote it sees. This walker scans char-by-char from
|
|
561
|
-
* the matched delimiter and only stops on the matching one.
|
|
562
|
-
*/
|
|
563
|
-
function readStringField(body, field) {
|
|
564
|
-
const m = new RegExp(`\\b${field}\\s*:\\s*(['"\`])`, "g").exec(body);
|
|
565
|
-
if (!m) return null;
|
|
566
|
-
const quote = m[1];
|
|
567
|
-
const start = m.index + m[0].length;
|
|
568
|
-
let i = start;
|
|
569
|
-
let raw = null;
|
|
570
|
-
while (i < body.length) {
|
|
571
|
-
const ch = body[i];
|
|
572
|
-
if (ch === "\\") {
|
|
573
|
-
i += 2;
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
if (ch === quote) {
|
|
577
|
-
raw = body.slice(start, i);
|
|
578
|
-
break;
|
|
579
|
-
}
|
|
580
|
-
i++;
|
|
581
|
-
}
|
|
582
|
-
if (raw === null) return null;
|
|
583
|
-
return raw.replace(/\\(.)/g, (_m, c) => {
|
|
584
|
-
if (c === "n") return "\n";
|
|
585
|
-
if (c === "t") return " ";
|
|
586
|
-
if (c === "r") return "\r";
|
|
587
|
-
return c;
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Default search order for the env schema file. Newer projects keep
|
|
592
|
-
* the schema under `src/config/` so the framework's "config" concept
|
|
593
|
-
* has a single home; older scaffolds dropped it at `src/env.ts` (kept
|
|
594
|
-
* here for back-compat). The first match wins.
|
|
595
|
-
*/
|
|
596
|
-
const DEFAULT_ENV_FILE_CANDIDATES = [
|
|
597
|
-
"src/config/index.ts",
|
|
598
|
-
"src/config/env.ts",
|
|
599
|
-
"src/config.ts",
|
|
600
|
-
"src/env.ts"
|
|
601
|
-
];
|
|
602
|
-
/**
|
|
603
|
-
* Look for an env schema file. When `envFile` is the string default
|
|
604
|
-
* (`'src/env.ts'`) or omitted, every entry in `DEFAULT_ENV_FILE_CANDIDATES`
|
|
605
|
-
* is tried in order. When the caller passes an explicit path, only that
|
|
606
|
-
* path is tried (so projects can opt out of the search by setting
|
|
607
|
-
* `kick.config.ts → typegen.envFile`).
|
|
608
|
-
*
|
|
609
|
-
* Returns a `DiscoveredEnv` if the file exists and contains both a
|
|
610
|
-
* `defineEnv(...)` call and a default export — the two markers we
|
|
611
|
-
* need before it's safe to emit `import type schema from '...'` in
|
|
612
|
-
* the generator. Returns `null` for any other state (no candidate
|
|
613
|
-
* found, no defineEnv, no default export) so the generator skips env
|
|
614
|
-
* typing silently.
|
|
615
|
-
*/
|
|
616
|
-
async function detectEnvFile(cwd, envFile) {
|
|
617
|
-
const candidates = envFile === "src/env.ts" ? DEFAULT_ENV_FILE_CANDIDATES : [envFile];
|
|
618
|
-
for (const candidate of candidates) {
|
|
619
|
-
const abs = resolve(cwd, candidate);
|
|
620
|
-
let source;
|
|
621
|
-
try {
|
|
622
|
-
source = await readFile(abs, "utf-8");
|
|
623
|
-
} catch {
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
if (!/\bdefineEnv\s*\(/.test(source)) continue;
|
|
627
|
-
if (!/export\s+default\b/.test(source)) continue;
|
|
628
|
-
return {
|
|
629
|
-
filePath: abs,
|
|
630
|
-
relativePath: toRelative(abs, cwd)
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
/** Detect duplicate class names across files */
|
|
636
|
-
function findCollisions(classes) {
|
|
637
|
-
const groups = /* @__PURE__ */ new Map();
|
|
638
|
-
for (const cls of classes) {
|
|
639
|
-
const arr = groups.get(cls.className) ?? [];
|
|
640
|
-
arr.push(cls);
|
|
641
|
-
groups.set(cls.className, arr);
|
|
642
|
-
}
|
|
643
|
-
const collisions = [];
|
|
644
|
-
for (const [className, group] of groups) if (new Set(group.map((c) => c.filePath)).size > 1) collisions.push({
|
|
645
|
-
className,
|
|
646
|
-
classes: group
|
|
647
|
-
});
|
|
648
|
-
collisions.sort((a, b) => a.className.localeCompare(b.className));
|
|
649
|
-
return collisions;
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* Scan a project for decorated classes, createToken definitions, and
|
|
653
|
-
* `@Inject` literal usages.
|
|
654
|
-
*/
|
|
655
|
-
async function scanProject(opts) {
|
|
656
|
-
const files = await walk(resolve(opts.root), opts);
|
|
657
|
-
const classes = [];
|
|
658
|
-
const routes = [];
|
|
659
|
-
const tokens = [];
|
|
660
|
-
const injects = [];
|
|
661
|
-
const pluginsAndAdapters = [];
|
|
662
|
-
const augmentations = [];
|
|
663
|
-
const sources = /* @__PURE__ */ new Map();
|
|
664
|
-
for (const file of files) {
|
|
665
|
-
let source;
|
|
666
|
-
try {
|
|
667
|
-
source = await readFile(file, "utf-8");
|
|
668
|
-
} catch {
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
sources.set(file, source);
|
|
672
|
-
classes.push(...extractClassesFromSource(source, file, opts.cwd));
|
|
673
|
-
tokens.push(...extractTokensFromSource(source, file, opts.cwd));
|
|
674
|
-
injects.push(...extractInjectsFromSource(source, file, opts.cwd));
|
|
675
|
-
pluginsAndAdapters.push(...extractPluginsAndAdaptersFromSource(source, file, opts.cwd));
|
|
676
|
-
augmentations.push(...extractAugmentationsFromSource(source, file, opts.cwd));
|
|
677
|
-
}
|
|
678
|
-
for (const [file, source] of sources) {
|
|
679
|
-
const classesInFile = classes.filter((c) => c.filePath === file);
|
|
680
|
-
routes.push(...extractRoutesFromSource(source, file, opts.cwd, classesInFile));
|
|
681
|
-
}
|
|
682
|
-
classes.sort((a, b) => {
|
|
683
|
-
if (a.className !== b.className) return a.className.localeCompare(b.className);
|
|
684
|
-
return a.relativePath.localeCompare(b.relativePath);
|
|
685
|
-
});
|
|
686
|
-
tokens.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
687
|
-
injects.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
688
|
-
routes.sort((a, b) => a.controller.localeCompare(b.controller) || a.method.localeCompare(b.method));
|
|
689
|
-
pluginsAndAdapters.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
690
|
-
augmentations.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
|
|
691
|
-
return {
|
|
692
|
-
classes,
|
|
693
|
-
routes,
|
|
694
|
-
tokens,
|
|
695
|
-
injects,
|
|
696
|
-
collisions: findCollisions(classes),
|
|
697
|
-
env: await detectEnvFile(opts.cwd, opts.envFile ?? "src/env.ts"),
|
|
698
|
-
pluginsAndAdapters,
|
|
699
|
-
augmentations
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
//#endregion
|
|
703
|
-
//#region src/typegen/asset-types.ts
|
|
704
|
-
/**
|
|
705
|
-
* Walks every `assetMap` entry's source directory + emits a typed
|
|
706
|
-
* `KickAssets` ambient augmentation (assets-plan.md PR 4). Generates
|
|
707
|
-
* `.kickjs/types/assets.d.ts` so adopters get autocomplete on
|
|
708
|
-
* `assets.<namespace>.<key>` and `@Asset('<namespace>/<key>')`.
|
|
709
|
-
*
|
|
710
|
-
* Pure module — no side effects beyond what the caller does with the
|
|
711
|
-
* returned content. Mirrors the shape of `renderPlugins` /
|
|
712
|
-
* `renderRegistry` in the generator so the typegen output stays
|
|
713
|
-
* consistent across surfaces.
|
|
714
|
-
*
|
|
715
|
-
* @module @forinda/kickjs-cli/typegen/asset-types
|
|
716
|
-
*/
|
|
717
|
-
function discoverAssets(assetMap, cwd) {
|
|
718
|
-
if (!assetMap) return {
|
|
719
|
-
entries: [],
|
|
720
|
-
count: 0
|
|
721
|
-
};
|
|
722
|
-
const seen = /* @__PURE__ */ new Map();
|
|
723
|
-
for (const [namespace, entry] of Object.entries(assetMap)) {
|
|
724
|
-
if (!entry || typeof entry.src !== "string") continue;
|
|
725
|
-
const srcAbs = resolve(cwd, entry.src);
|
|
726
|
-
if (!isDir(srcAbs)) continue;
|
|
727
|
-
const matches = globSync(entry.glob ?? "**/*", {
|
|
728
|
-
cwd: srcAbs,
|
|
729
|
-
nodir: true,
|
|
730
|
-
dot: false,
|
|
731
|
-
posix: true
|
|
732
|
-
});
|
|
733
|
-
matches.sort();
|
|
734
|
-
const { pairs } = groupAssetKeys(namespace, matches, { strategy: entry.keys ?? "auto" });
|
|
735
|
-
for (const { key: logical } of pairs) {
|
|
736
|
-
const subKey = logical.slice(namespace.length + 1);
|
|
737
|
-
seen.set(logical, {
|
|
738
|
-
namespace,
|
|
739
|
-
key: subKey
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
return {
|
|
744
|
-
entries: [...seen.values()],
|
|
745
|
-
count: seen.size
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
function renderAssetTypes(discovered) {
|
|
749
|
-
const HEADER = `/* eslint-disable */
|
|
750
|
-
// AUTO-GENERATED by \`kick typegen\`. DO NOT EDIT.
|
|
751
|
-
// Re-run with \`kick typegen\` or rely on \`kick dev\` to refresh.
|
|
752
|
-
`;
|
|
753
|
-
if (discovered.entries.length === 0) return `${HEADER}
|
|
754
|
-
declare module '@forinda/kickjs' {
|
|
755
|
-
/**
|
|
756
|
-
* Map of every typed asset discovered in the project's assetMap.
|
|
757
|
-
* (No assetMap entries discovered yet — declare with
|
|
758
|
-
* \`assetMap: { name: { src: 'src/...' } }\` in kick.config.ts.)
|
|
759
|
-
*/
|
|
760
|
-
interface KickAssets {}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
export {}
|
|
764
|
-
`;
|
|
765
|
-
const tree = {};
|
|
766
|
-
for (const entry of discovered.entries) {
|
|
767
|
-
const path = `${entry.namespace}/${entry.key}`.split("/");
|
|
768
|
-
let node = tree;
|
|
769
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
770
|
-
const part = path[i];
|
|
771
|
-
const existing = node[part];
|
|
772
|
-
if (existing === LEAF) {
|
|
773
|
-
const promoted = {};
|
|
774
|
-
node[part] = promoted;
|
|
775
|
-
node = promoted;
|
|
776
|
-
} else {
|
|
777
|
-
if (!existing) node[part] = {};
|
|
778
|
-
node = node[part];
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
const leaf = path[path.length - 1];
|
|
782
|
-
if (typeof node[leaf] === "object") continue;
|
|
783
|
-
node[leaf] = LEAF;
|
|
784
|
-
}
|
|
785
|
-
return `${HEADER}
|
|
786
|
-
declare module '@forinda/kickjs' {
|
|
787
|
-
/**
|
|
788
|
-
* Map of every typed asset discovered in the project's assetMap.
|
|
789
|
-
* Each leaf is a \`() => string\` thunk that returns the resolved
|
|
790
|
-
* absolute path for the file in the current run mode (dev → src,
|
|
791
|
-
* prod → dist).
|
|
792
|
-
*/
|
|
793
|
-
interface KickAssets {
|
|
794
|
-
${renderTree(tree, " ")}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
export {}
|
|
799
|
-
`;
|
|
800
|
-
}
|
|
801
|
-
const LEAF = Symbol("asset-leaf");
|
|
802
|
-
function renderTree(node, indent) {
|
|
803
|
-
const keys = Object.keys(node).toSorted();
|
|
804
|
-
const lines = [];
|
|
805
|
-
for (const key of keys) {
|
|
806
|
-
const child = node[key];
|
|
807
|
-
const safeKey = isIdentifier(key) ? key : JSON.stringify(key);
|
|
808
|
-
if (child === LEAF) lines.push(`${indent}${safeKey}: () => string`);
|
|
809
|
-
else {
|
|
810
|
-
lines.push(`${indent}${safeKey}: {`);
|
|
811
|
-
lines.push(renderTree(child, `${indent} `));
|
|
812
|
-
lines.push(`${indent}}`);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
return lines.join("\n");
|
|
816
|
-
}
|
|
817
|
-
function isIdentifier(str) {
|
|
818
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(str);
|
|
819
|
-
}
|
|
820
|
-
function isDir(path) {
|
|
821
|
-
try {
|
|
822
|
-
return statSync(path).isDirectory();
|
|
823
|
-
} catch {
|
|
824
|
-
return false;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
//#endregion
|
|
828
|
-
//#region src/typegen/generator.ts
|
|
829
|
-
/**
|
|
830
|
-
* Generates `.d.ts` files inside `.kickjs/types/` from the discovered
|
|
831
|
-
* decorated classes and DI tokens. Pattern modeled on React Router's
|
|
832
|
-
* `.react-router/types/` directory.
|
|
833
|
-
*
|
|
834
|
-
* Outputs:
|
|
835
|
-
* - `.kickjs/types/registry.d.ts` — module augmentation for `KickJsRegistry`
|
|
836
|
-
* that gives `container.resolve('UserService')` the right return type.
|
|
837
|
-
* - `.kickjs/types/services.d.ts` — string-literal union of all known
|
|
838
|
-
* service-style tokens for tooling autocomplete.
|
|
839
|
-
* - `.kickjs/types/modules.d.ts` — string-literal union of discovered
|
|
840
|
-
* module class names.
|
|
841
|
-
* - `.kickjs/types/index.d.ts` — re-exports the above (single import target).
|
|
842
|
-
* - `.kickjs/.gitignore` — gitignores the whole folder so generated files
|
|
843
|
-
* never get committed.
|
|
844
|
-
*
|
|
845
|
-
* ## Collision behaviour
|
|
846
|
-
*
|
|
847
|
-
* If `findCollisions()` returns any duplicate class names:
|
|
848
|
-
* - **Default (`allowDuplicates: false`)** — `generateTypes` throws a
|
|
849
|
-
* `TokenCollisionError` with a clear message listing every conflicting
|
|
850
|
-
* file. The caller (CLI) prints it and exits non-zero. Nothing is
|
|
851
|
-
* written to disk.
|
|
852
|
-
* - **`allowDuplicates: true`** — colliding classes are auto-namespaced
|
|
853
|
-
* by their relative file path so the registry keys become e.g.
|
|
854
|
-
* `'modules/users/UserService'` instead of `'UserService'`. Non-colliding
|
|
855
|
-
* classes still get bare `'ClassName'` keys (smart default).
|
|
856
|
-
*
|
|
857
|
-
* @module @forinda/kickjs-cli/typegen/generator
|
|
858
|
-
*/
|
|
859
|
-
/** Header written to every generated file */
|
|
860
|
-
const HEADER = `/* eslint-disable */
|
|
861
|
-
// AUTO-GENERATED by \`kick typegen\`. DO NOT EDIT.
|
|
862
|
-
// Re-run with \`kick typegen\` or rely on \`kick dev\` to refresh.
|
|
863
|
-
`;
|
|
864
|
-
/** Decorators whose classes participate in the DI registry augmentation */
|
|
865
|
-
const REGISTRY_DECORATORS = new Set([
|
|
866
|
-
"Service",
|
|
867
|
-
"Repository",
|
|
868
|
-
"Injectable",
|
|
869
|
-
"Component"
|
|
870
|
-
]);
|
|
871
|
-
/** Thrown by `generateTypes` when collisions are found and not allowed */
|
|
872
|
-
var TokenCollisionError = class extends Error {
|
|
873
|
-
collisions;
|
|
874
|
-
constructor(collisions) {
|
|
875
|
-
super(formatCollisionMessage(collisions));
|
|
876
|
-
this.name = "TokenCollisionError";
|
|
877
|
-
this.collisions = collisions;
|
|
878
|
-
}
|
|
879
|
-
};
|
|
880
|
-
/** Build a human-readable message describing every collision */
|
|
881
|
-
function formatCollisionMessage(collisions) {
|
|
882
|
-
const lines = ["kick typegen: token collision detected"];
|
|
883
|
-
for (const c of collisions) {
|
|
884
|
-
lines.push("");
|
|
885
|
-
lines.push(` ${c.classes.length} classes named '${c.className}':`);
|
|
886
|
-
for (const cls of c.classes) lines.push(` - ${cls.relativePath}`);
|
|
887
|
-
}
|
|
888
|
-
lines.push("");
|
|
889
|
-
lines.push("Resolutions:");
|
|
890
|
-
lines.push(" (a) Rename one of the classes");
|
|
891
|
-
lines.push(" (b) Use createToken<T>('namespaced/Name') and import the token explicitly — see @forinda/kickjs");
|
|
892
|
-
lines.push(" (c) Pass --allow-duplicates to namespace the registry keys automatically");
|
|
893
|
-
lines.push(" (e.g. 'modules/users/UserService' instead of 'UserService')");
|
|
894
|
-
return lines.join("\n");
|
|
895
|
-
}
|
|
896
|
-
/** Compute the module specifier (without extension) used inside `import('...')` */
|
|
897
|
-
function importSpecifierFor(targetFile, fromFile) {
|
|
898
|
-
let rel = relative(dirname(fromFile), targetFile).split(sep).join("/");
|
|
899
|
-
rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
|
|
900
|
-
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
901
|
-
return rel;
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* Build the namespaced registry key for a colliding class.
|
|
905
|
-
* Strips the `src/` prefix and the file extension, then appends the
|
|
906
|
-
* class name. Example: `src/modules/users/user.service.ts` + `UserService`
|
|
907
|
-
* → `modules/users/UserService`.
|
|
908
|
-
*/
|
|
909
|
-
function namespacedKeyFor(cls) {
|
|
910
|
-
const parts = cls.relativePath.replace(/^src\//, "").replace(/\.(ts|tsx|mts|cts)$/i, "").split("/");
|
|
911
|
-
parts.pop();
|
|
912
|
-
const ns = parts.join("/");
|
|
913
|
-
return ns ? `${ns}/${cls.className}` : cls.className;
|
|
914
|
-
}
|
|
915
|
-
/**
|
|
916
|
-
* Render the `KickJsRegistry` module augmentation. Each entry maps a
|
|
917
|
-
* string token to the imported class type.
|
|
918
|
-
*
|
|
919
|
-
* Default-exported classes are imported as `import('...').default`.
|
|
920
|
-
*
|
|
921
|
-
* `collidingNames` lists class names that should be auto-namespaced;
|
|
922
|
-
* everything else gets a bare key.
|
|
923
|
-
*/
|
|
924
|
-
function renderRegistry(classes, outFile, collidingNames) {
|
|
925
|
-
const seen = /* @__PURE__ */ new Set();
|
|
926
|
-
const entries = [];
|
|
927
|
-
for (const c of classes) {
|
|
928
|
-
if (!REGISTRY_DECORATORS.has(c.decorator)) continue;
|
|
929
|
-
const key = collidingNames.has(c.className) ? namespacedKeyFor(c) : c.className;
|
|
930
|
-
if (seen.has(key)) continue;
|
|
931
|
-
seen.add(key);
|
|
932
|
-
const spec = importSpecifierFor(c.filePath, outFile);
|
|
933
|
-
const ref = c.isDefault ? `import('${spec}').default` : `import('${spec}').${c.className}`;
|
|
934
|
-
entries.push(` '${key}': ${ref}`);
|
|
935
|
-
}
|
|
936
|
-
return `${HEADER}
|
|
937
|
-
declare module '@forinda/kickjs' {
|
|
938
|
-
interface KickJsRegistry {
|
|
939
|
-
${entries.length ? entries.join("\n") : " // (no services discovered yet — run `kick g service <name>` to add one)"}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
export {}
|
|
944
|
-
`;
|
|
945
|
-
}
|
|
946
|
-
/** Render a string-literal union type containing the given names */
|
|
947
|
-
function renderUnion(typeName, names, emptyComment) {
|
|
948
|
-
if (names.length === 0) return `${HEADER}
|
|
949
|
-
// ${emptyComment}
|
|
950
|
-
export type ${typeName} = never
|
|
951
|
-
`;
|
|
952
|
-
return `${HEADER}
|
|
953
|
-
export type ${typeName} =
|
|
954
|
-
${[...new Set(names)].toSorted().map((n) => ` | '${n}'`).join("\n")}
|
|
955
|
-
`;
|
|
956
|
-
}
|
|
957
|
-
/** Render the barrel index that re-exports the union types */
|
|
958
|
-
function renderIndex(includeEnv) {
|
|
959
|
-
return `${HEADER}
|
|
960
|
-
export type { ServiceToken } from './services'
|
|
961
|
-
export type { ModuleToken } from './modules'
|
|
962
|
-
|
|
963
|
-
// The registry, routes, plugins, assets, and env augmentations are
|
|
964
|
-
// loaded as side-effects — importing this file (or having it on
|
|
965
|
-
// tsconfig include) is enough for \`container.resolve()\`,
|
|
966
|
-
// \`Ctx<KickRoutes.UserController['getUser']>\`,
|
|
967
|
-
// \`dependsOn: ['TenantAdapter']\`, \`assets.mails.welcome()\`, and
|
|
968
|
-
// \`@Value('PORT')\` to resolve.
|
|
969
|
-
import './registry'
|
|
970
|
-
import './kick__routes'
|
|
971
|
-
import './plugins'
|
|
972
|
-
import './augmentations'
|
|
973
|
-
import './assets'
|
|
974
|
-
${includeEnv ? "import './kick__env'\n" : ""}`;
|
|
975
|
-
}
|
|
976
|
-
/**
|
|
977
|
-
* Render the `KickJsPluginRegistry` augmentation. Each entry maps the
|
|
978
|
-
* literal `name` field of a plugin/adapter to a marker type (the
|
|
979
|
-
* registry value isn't load-bearing at runtime — `dependsOn` only cares
|
|
980
|
-
* about `keyof`, so any non-`never` type works). We emit `'plugin'` /
|
|
981
|
-
* `'adapter'` strings so DevTools can later read the registry to tell
|
|
982
|
-
* the kinds apart without a second source of truth.
|
|
983
|
-
*
|
|
984
|
-
* When the project has no discoverable plugins/adapters, the augmentation
|
|
985
|
-
* is intentionally empty rather than skipped so the `keyof` constraint
|
|
986
|
-
* resolves to `never` (which is harmless — `dependsOn: []` still works).
|
|
987
|
-
*/
|
|
988
|
-
function renderPlugins(items) {
|
|
989
|
-
const byName = /* @__PURE__ */ new Map();
|
|
990
|
-
for (const item of items) if (!byName.has(item.name)) byName.set(item.name, item);
|
|
991
|
-
const entries = [...byName.values()].toSorted((a, b) => a.name.localeCompare(b.name)).map((item) => ` '${item.name}': '${item.kind}'`).join("\n");
|
|
992
|
-
return `${HEADER}
|
|
993
|
-
declare module '@forinda/kickjs' {
|
|
994
|
-
/**
|
|
995
|
-
* Map of every plugin/adapter \`name\` discovered in the project. The
|
|
996
|
-
* value type is the kind tag (\`'plugin'\` or \`'adapter'\`); the
|
|
997
|
-
* \`keyof\` of this interface narrows \`dependsOn\` so misspelled deps
|
|
998
|
-
* become compile errors instead of boot-time \`MissingMountDepError\`.
|
|
999
|
-
*/
|
|
1000
|
-
interface KickJsPluginRegistry {
|
|
1001
|
-
${entries ? entries : " // (no plugins/adapters discovered yet — `defineAdapter`/`definePlugin` calls feed this)"}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
export {}
|
|
1006
|
-
`;
|
|
1007
|
-
}
|
|
1008
|
-
/**
|
|
1009
|
-
* Render the augmentation manifest — one block per `defineAugmentation`
|
|
1010
|
-
* call discovered in the project. The output is a `.d.ts` file that
|
|
1011
|
-
* does nothing at runtime but acts as in-IDE documentation: adopters
|
|
1012
|
-
* jumping into it see every interface their plugins offer for
|
|
1013
|
-
* augmentation, alongside any `description` / `example` the plugin
|
|
1014
|
-
* authors provided.
|
|
1015
|
-
*/
|
|
1016
|
-
function renderAugmentations(items) {
|
|
1017
|
-
if (items.length === 0) return `${HEADER}
|
|
1018
|
-
// No augmentations discovered.
|
|
1019
|
-
//
|
|
1020
|
-
// Plugins advertise augmentable interfaces via:
|
|
1021
|
-
//
|
|
1022
|
-
// import { defineAugmentation } from '@forinda/kickjs'
|
|
1023
|
-
// defineAugmentation('FeatureFlags', {
|
|
1024
|
-
// description: 'Feature flag shape consumed by FlagsPlugin',
|
|
1025
|
-
// example: '{ beta: boolean; rolloutPercentage: number }',
|
|
1026
|
-
// })
|
|
1027
|
-
//
|
|
1028
|
-
// See \`docs/guide/typegen.md#augmentations\` for the full pattern.
|
|
1029
|
-
export {}
|
|
1030
|
-
`;
|
|
1031
|
-
const byName = /* @__PURE__ */ new Map();
|
|
1032
|
-
for (const item of items) if (!byName.has(item.name)) byName.set(item.name, item);
|
|
1033
|
-
const blocks = [];
|
|
1034
|
-
for (const item of [...byName.values()].toSorted((a, b) => a.name.localeCompare(b.name))) {
|
|
1035
|
-
const docLines = [];
|
|
1036
|
-
if (item.description) for (const line of item.description.split("\n")) docLines.push(` * ${line}`);
|
|
1037
|
-
if (item.example) {
|
|
1038
|
-
docLines.push(` * @example`, ` * \`\`\`ts`);
|
|
1039
|
-
for (const line of item.example.split("\n")) docLines.push(` * ${line}`);
|
|
1040
|
-
docLines.push(` * \`\`\``);
|
|
1041
|
-
}
|
|
1042
|
-
docLines.push(` * @see ${item.relativePath}`);
|
|
1043
|
-
blocks.push([
|
|
1044
|
-
"/**",
|
|
1045
|
-
...docLines,
|
|
1046
|
-
" */",
|
|
1047
|
-
`export interface ${item.name}Augmentation {}`
|
|
1048
|
-
].join("\n"));
|
|
1049
|
-
}
|
|
1050
|
-
return `${HEADER}
|
|
1051
|
-
// Catalogue of augmentable interfaces in this project. The interfaces
|
|
1052
|
-
// below are documentation only — augment the source-of-truth interfaces
|
|
1053
|
-
// in your own \`d.ts\` files (the framework declares the actual types).
|
|
1054
|
-
|
|
1055
|
-
${blocks.join("\n\n")}
|
|
1056
|
-
`;
|
|
1057
|
-
}
|
|
1058
|
-
/** Write all generated `.d.ts` files to `outDir` */
|
|
1059
|
-
async function generateTypes(opts) {
|
|
1060
|
-
const { classes, routes = [], tokens = [], injects = [], collisions = [], env = null, pluginsAndAdapters = [], augmentations = [], assets = {
|
|
1061
|
-
entries: [],
|
|
1062
|
-
count: 0
|
|
1063
|
-
}, outDir, allowDuplicates = false, schemaValidator: _schemaValidator = false } = opts;
|
|
1064
|
-
if (collisions.length > 0 && !allowDuplicates) throw new TokenCollisionError(collisions);
|
|
1065
|
-
await mkdir(outDir, { recursive: true });
|
|
1066
|
-
const registryFile = join(outDir, "registry.d.ts");
|
|
1067
|
-
const servicesFile = join(outDir, "services.d.ts");
|
|
1068
|
-
const modulesFile = join(outDir, "modules.d.ts");
|
|
1069
|
-
const pluginsFile = join(outDir, "plugins.d.ts");
|
|
1070
|
-
const augmentationsFile = join(outDir, "augmentations.d.ts");
|
|
1071
|
-
const assetsFile = join(outDir, "assets.d.ts");
|
|
1072
|
-
const indexFile = join(outDir, "index.d.ts");
|
|
1073
|
-
const collidingNames = new Set(collisions.map((c) => c.className));
|
|
1074
|
-
const registryContent = renderRegistry(classes, registryFile, collidingNames);
|
|
1075
|
-
const classTokens = classes.filter((c) => REGISTRY_DECORATORS.has(c.decorator)).map((c) => collidingNames.has(c.className) ? namespacedKeyFor(c) : c.className);
|
|
1076
|
-
const tokenLiterals = tokens.map((t) => t.name);
|
|
1077
|
-
const injectLiterals = injects.map((i) => i.name);
|
|
1078
|
-
const allServices = [
|
|
1079
|
-
...classTokens,
|
|
1080
|
-
...tokenLiterals,
|
|
1081
|
-
...injectLiterals
|
|
1082
|
-
];
|
|
1083
|
-
const modules = classes.filter((c) => c.decorator === "Module").map((c) => c.className);
|
|
1084
|
-
const servicesContent = renderUnion("ServiceToken", allServices, "(no tokens discovered — declare with createToken<T>() or `kick g service <name>`)");
|
|
1085
|
-
const modulesContent = renderUnion("ModuleToken", modules, "(no @Module classes discovered — `kick g module <name>` to add one)");
|
|
1086
|
-
const pluginsContent = renderPlugins(pluginsAndAdapters);
|
|
1087
|
-
const augmentationsContent = renderAugmentations(augmentations);
|
|
1088
|
-
const assetsContent = renderAssetTypes(assets);
|
|
1089
|
-
const indexContent = renderIndex(env !== null);
|
|
1090
|
-
await writeFile(registryFile, registryContent, "utf-8");
|
|
1091
|
-
await writeFile(servicesFile, servicesContent, "utf-8");
|
|
1092
|
-
await writeFile(modulesFile, modulesContent, "utf-8");
|
|
1093
|
-
await writeFile(pluginsFile, pluginsContent, "utf-8");
|
|
1094
|
-
await writeFile(augmentationsFile, augmentationsContent, "utf-8");
|
|
1095
|
-
await writeFile(assetsFile, assetsContent, "utf-8");
|
|
1096
|
-
await writeFile(indexFile, indexContent, "utf-8");
|
|
1097
|
-
const written = [
|
|
1098
|
-
registryFile,
|
|
1099
|
-
servicesFile,
|
|
1100
|
-
modulesFile,
|
|
1101
|
-
pluginsFile,
|
|
1102
|
-
augmentationsFile,
|
|
1103
|
-
assetsFile,
|
|
1104
|
-
indexFile
|
|
1105
|
-
];
|
|
1106
|
-
await writeFile(join(dirname(outDir), ".gitignore"), "# Auto-generated by kick typegen\n*\n", "utf-8");
|
|
1107
|
-
const uniquePluginNames = new Set(pluginsAndAdapters.map((p) => p.name)).size;
|
|
1108
|
-
const uniqueAugmentations = new Set(augmentations.map((a) => a.name)).size;
|
|
1109
|
-
return {
|
|
1110
|
-
registryEntries: classTokens.length,
|
|
1111
|
-
serviceTokens: new Set(allServices).size,
|
|
1112
|
-
moduleTokens: modules.length,
|
|
1113
|
-
routeEntries: routes.length,
|
|
1114
|
-
pluginEntries: uniquePluginNames,
|
|
1115
|
-
augmentationEntries: uniqueAugmentations,
|
|
1116
|
-
assetEntries: assets.count,
|
|
1117
|
-
envWritten: env !== null,
|
|
1118
|
-
written,
|
|
1119
|
-
resolvedCollisions: collisions.length
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
//#endregion
|
|
1123
|
-
//#region src/typegen/token-conventions.ts
|
|
1124
|
-
/**
|
|
1125
|
-
* Regex for the §22.2 token shape. Breakdown:
|
|
1126
|
-
*
|
|
1127
|
-
* - `^(kick\/)?` — optional reserved framework prefix.
|
|
1128
|
-
* - `([a-z][\w-]*\/[A-Z]\w*)` — `<scope>/<PascalKey>`. Scope is
|
|
1129
|
-
* lowercase, key is PascalCase.
|
|
1130
|
-
* - `(\/.+)?` — optional `/suffix` for sub-flavours
|
|
1131
|
-
* (e.g. `mycorp/Cache/redis`).
|
|
1132
|
-
* - `(:[a-z][\w-]+(:[a-z][\w-]+)*)?` — optional `:instance` (and
|
|
1133
|
-
* further `:extra` colon-sections) for `.scoped()` shards.
|
|
1134
|
-
*/
|
|
1135
|
-
const TOKEN_CONVENTION_REGEX = /^(kick\/)?([a-z][\w-]*\/[A-Z]\w*)(\/.+)?(:[a-z][\w-]+(:[a-z][\w-]+)*)?$/;
|
|
1136
|
-
const LEGACY_PREFIX = "kickjs.";
|
|
1137
|
-
function validateTokenConventions(tokens) {
|
|
1138
|
-
const warnings = [];
|
|
1139
|
-
for (const token of tokens) {
|
|
1140
|
-
const name = token.name;
|
|
1141
|
-
if (name.startsWith(LEGACY_PREFIX)) continue;
|
|
1142
|
-
if (TOKEN_CONVENTION_REGEX.test(name)) continue;
|
|
1143
|
-
warnings.push({
|
|
1144
|
-
token: name,
|
|
1145
|
-
variable: token.variable,
|
|
1146
|
-
filePath: token.relativePath,
|
|
1147
|
-
reason: "does not match `<scope>/<PascalKey>[/<suffix>][:<instance>]`",
|
|
1148
|
-
suggestion: suggestRename(name)
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
return warnings;
|
|
1152
|
-
}
|
|
1153
|
-
function suggestRename(name) {
|
|
1154
|
-
if (/^[A-Z]\w*$/.test(name)) return `'<scope>/${name}' (e.g. 'mycorp/${name}')`;
|
|
1155
|
-
if (name.includes(".")) return `consider '<scope>/PascalKey' instead of dotted form`;
|
|
1156
|
-
const slashLower = /^([a-z][\w-]*)\/([a-z]\w*)$/.exec(name);
|
|
1157
|
-
if (slashLower) {
|
|
1158
|
-
const [, scope, key] = slashLower;
|
|
1159
|
-
return `'${scope}/${key.charAt(0).toUpperCase()}${key.slice(1)}'`;
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
//#endregion
|
|
1163
|
-
//#region src/typegen/index.ts
|
|
1164
|
-
/**
|
|
1165
|
-
* Public entry point for the KickJS typegen module.
|
|
1166
|
-
*
|
|
1167
|
-
* Used by:
|
|
1168
|
-
* - `kick typegen` (one-shot or watch mode)
|
|
1169
|
-
* - `kick dev` (auto-runs once before Vite starts; refreshes when files change)
|
|
1170
|
-
*
|
|
1171
|
-
* @module @forinda/kickjs-cli/typegen
|
|
1172
|
-
*/
|
|
1173
|
-
var typegen_exports = /* @__PURE__ */ __exportAll({
|
|
1174
|
-
runTypegen: () => runTypegen,
|
|
1175
|
-
watchTypegen: () => watchTypegen
|
|
1176
|
-
});
|
|
1177
|
-
/** Resolve options to absolute paths */
|
|
1178
|
-
function resolveOptions(opts) {
|
|
1179
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
1180
|
-
return {
|
|
1181
|
-
cwd,
|
|
1182
|
-
srcDir: resolve(cwd, opts.srcDir ?? "src"),
|
|
1183
|
-
outDir: resolve(cwd, opts.outDir ?? ".kickjs/types"),
|
|
1184
|
-
silent: opts.silent ?? false,
|
|
1185
|
-
allowDuplicates: opts.allowDuplicates ?? false,
|
|
1186
|
-
schemaValidator: opts.schemaValidator ?? false,
|
|
1187
|
-
envFile: opts.envFile ?? "src/env.ts"
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Run a single typegen pass: scan source files, generate `.d.ts` files.
|
|
1192
|
-
*
|
|
1193
|
-
* Returns the discovered scan result alongside the generation result so
|
|
1194
|
-
* callers (`kick dev`, devtools) can log them or feed them to other tools.
|
|
1195
|
-
*
|
|
1196
|
-
* Throws `TokenCollisionError` if duplicate class names are found and
|
|
1197
|
-
* `allowDuplicates` is false.
|
|
1198
|
-
*/
|
|
1199
|
-
async function runTypegen(opts = {}) {
|
|
1200
|
-
const { cwd, srcDir, outDir, silent, allowDuplicates, schemaValidator, envFile } = resolveOptions(opts);
|
|
1201
|
-
const start = Date.now();
|
|
1202
|
-
const scan = await scanProject({
|
|
1203
|
-
root: srcDir,
|
|
1204
|
-
cwd,
|
|
1205
|
-
envFile: envFile === false ? void 0 : envFile
|
|
1206
|
-
});
|
|
1207
|
-
const assets = discoverAssets(opts.assetMap, cwd);
|
|
1208
|
-
const result = await generateTypes({
|
|
1209
|
-
classes: scan.classes,
|
|
1210
|
-
routes: scan.routes,
|
|
1211
|
-
tokens: scan.tokens,
|
|
1212
|
-
injects: scan.injects,
|
|
1213
|
-
collisions: scan.collisions,
|
|
1214
|
-
env: envFile === false ? null : scan.env,
|
|
1215
|
-
pluginsAndAdapters: scan.pluginsAndAdapters,
|
|
1216
|
-
augmentations: scan.augmentations,
|
|
1217
|
-
assets,
|
|
1218
|
-
outDir,
|
|
1219
|
-
allowDuplicates,
|
|
1220
|
-
schemaValidator
|
|
1221
|
-
});
|
|
1222
|
-
if (opts.runPlugins !== false) try {
|
|
1223
|
-
const { runAllPluginTypegens } = await import("./builtins-BW3g09hP.mjs").then((n) => n.r);
|
|
1224
|
-
const { loadKickConfig } = await import("./config-DsQe2yzy.mjs").then((n) => n.n);
|
|
1225
|
-
await runAllPluginTypegens({
|
|
1226
|
-
cwd,
|
|
1227
|
-
config: await loadKickConfig(cwd),
|
|
1228
|
-
silent: true
|
|
1229
|
-
});
|
|
1230
|
-
} catch (err) {
|
|
1231
|
-
if (!silent) {
|
|
1232
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1233
|
-
console.warn(` kick typegen: plugin pipeline failed (${msg}) — continuing`);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
const tokenWarnings = validateTokenConventions(scan.tokens);
|
|
1237
|
-
const elapsed = Date.now() - start;
|
|
1238
|
-
if (!silent) {
|
|
1239
|
-
const where = outDir.replace(cwd + "/", "");
|
|
1240
|
-
const collisionNote = result.resolvedCollisions > 0 ? `, ${result.resolvedCollisions} collisions namespaced` : "";
|
|
1241
|
-
const envNote = result.envWritten ? ", env typed" : "";
|
|
1242
|
-
const pluginNote = result.pluginEntries > 0 ? `, ${result.pluginEntries} plugins/adapters` : "";
|
|
1243
|
-
const augNote = result.augmentationEntries > 0 ? `, ${result.augmentationEntries} augmentations` : "";
|
|
1244
|
-
const assetNote = result.assetEntries > 0 ? `, ${result.assetEntries} assets` : "";
|
|
1245
|
-
console.log(` kick typegen → ${result.serviceTokens} services, ${result.routeEntries} routes, ${result.moduleTokens} modules${pluginNote}${augNote}${assetNote}${envNote}${collisionNote} → ${where} (${elapsed}ms)`);
|
|
1246
|
-
if (tokenWarnings.length > 0) {
|
|
1247
|
-
console.warn(` kick typegen: ${tokenWarnings.length} token(s) don't match the §22.2 convention:`);
|
|
1248
|
-
for (const warning of tokenWarnings) {
|
|
1249
|
-
const variableNote = warning.variable ? ` [${warning.variable}]` : "";
|
|
1250
|
-
console.warn(` '${warning.token}' (${warning.filePath})${variableNote} — ${warning.reason}`);
|
|
1251
|
-
if (warning.suggestion) console.warn(` → suggestion: ${warning.suggestion}`);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
return {
|
|
1256
|
-
scan,
|
|
1257
|
-
result,
|
|
1258
|
-
tokenWarnings
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
/**
|
|
1262
|
-
* Watch mode for `kick typegen --watch`.
|
|
1263
|
-
*
|
|
1264
|
-
* Uses Node's built-in `fs.watch` (recursive, available on Linux 22+ and
|
|
1265
|
-
* macOS 19+). Falls back gracefully if recursive watch is not supported.
|
|
1266
|
-
*
|
|
1267
|
-
* Debounces re-runs by 100ms so a bulk file change (e.g. `kick g module`
|
|
1268
|
-
* creating 5 files at once) emits one regen, not five.
|
|
1269
|
-
*
|
|
1270
|
-
* In watch mode collisions are reported but never thrown — the watcher
|
|
1271
|
-
* keeps running so the user can fix the rename and the next scan
|
|
1272
|
-
* recovers automatically.
|
|
1273
|
-
*
|
|
1274
|
-
* Returns a `stop()` function that closes the watcher.
|
|
1275
|
-
*/
|
|
1276
|
-
async function watchTypegen(opts = {}) {
|
|
1277
|
-
const resolved = resolveOptions(opts);
|
|
1278
|
-
const { srcDir, silent, cwd } = resolved;
|
|
1279
|
-
const runOpts = {
|
|
1280
|
-
...resolved,
|
|
1281
|
-
allowDuplicates: true,
|
|
1282
|
-
runPlugins: false
|
|
1283
|
-
};
|
|
1284
|
-
const forcePolling = process.env.KICKJS_WATCH_POLLING === "1" || process.env.KICKJS_WATCH_POLLING === "true";
|
|
1285
|
-
const [{ runAllPluginTypegens }, { loadKickConfig }] = await Promise.all([import("./builtins-BW3g09hP.mjs").then((n) => n.r), import("./config-DsQe2yzy.mjs").then((n) => n.n)]);
|
|
1286
|
-
const pluginConfig = await loadKickConfig(cwd);
|
|
1287
|
-
const runPlugins = () => runAllPluginTypegens({
|
|
1288
|
-
cwd,
|
|
1289
|
-
config: pluginConfig,
|
|
1290
|
-
silent: true
|
|
1291
|
-
}).catch(() => {});
|
|
1292
|
-
await safeRun(runOpts, silent);
|
|
1293
|
-
await runPlugins();
|
|
1294
|
-
const { watch } = await import("node:fs");
|
|
1295
|
-
let timer = null;
|
|
1296
|
-
const trigger = (filename) => {
|
|
1297
|
-
if (!filename) return;
|
|
1298
|
-
if (!/\.(ts|tsx|mts|cts)$/.test(filename)) return;
|
|
1299
|
-
if (filename.includes(".kickjs")) return;
|
|
1300
|
-
if (filename.endsWith(".d.ts")) return;
|
|
1301
|
-
if (timer) clearTimeout(timer);
|
|
1302
|
-
timer = setTimeout(() => {
|
|
1303
|
-
safeRun(runOpts, silent);
|
|
1304
|
-
runPlugins();
|
|
1305
|
-
}, 100);
|
|
1306
|
-
};
|
|
1307
|
-
if (forcePolling) {
|
|
1308
|
-
if (!silent) console.log(" kick typegen: polling mode (KICKJS_WATCH_POLLING)");
|
|
1309
|
-
const interval = setInterval(() => {
|
|
1310
|
-
safeRun({
|
|
1311
|
-
...runOpts,
|
|
1312
|
-
silent: true
|
|
1313
|
-
}, true);
|
|
1314
|
-
}, 2e3);
|
|
1315
|
-
return () => clearInterval(interval);
|
|
1316
|
-
}
|
|
1317
|
-
let watcher;
|
|
1318
|
-
try {
|
|
1319
|
-
watcher = watch(srcDir, { recursive: true }, (_event, filename) => {
|
|
1320
|
-
trigger(filename);
|
|
1321
|
-
});
|
|
1322
|
-
} catch (err) {
|
|
1323
|
-
if (!silent) console.warn(` kick typegen: watch mode unavailable (${err?.message ?? err}). Falling back to polling.`);
|
|
1324
|
-
const interval = setInterval(() => {
|
|
1325
|
-
safeRun({
|
|
1326
|
-
...runOpts,
|
|
1327
|
-
silent: true
|
|
1328
|
-
}, true);
|
|
1329
|
-
}, 2e3);
|
|
1330
|
-
return () => clearInterval(interval);
|
|
1331
|
-
}
|
|
1332
|
-
return () => {
|
|
1333
|
-
if (timer) clearTimeout(timer);
|
|
1334
|
-
watcher.close();
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
/** Run typegen swallowing errors so the watcher loop never dies */
|
|
1338
|
-
async function safeRun(opts, silent) {
|
|
1339
|
-
try {
|
|
1340
|
-
await runTypegen(opts);
|
|
1341
|
-
} catch (err) {
|
|
1342
|
-
if (silent) return;
|
|
1343
|
-
if (err instanceof TokenCollisionError) console.error("\n" + err.message + "\n");
|
|
1344
|
-
else {
|
|
1345
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1346
|
-
console.error(` kick typegen failed: ${msg}`);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
//#endregion
|
|
1351
|
-
export { discoverAssets as a, TokenCollisionError as i, typegen_exports as n, renderAssetTypes as o, watchTypegen as r, scanProject as s, runTypegen as t };
|