@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.
Files changed (34) hide show
  1. package/dist/builtins-BdvmVAJ1.mjs +3740 -0
  2. package/dist/builtins-Du70nybS.mjs +1066 -0
  3. package/dist/builtins-Du70nybS.mjs.map +1 -0
  4. package/dist/cli.mjs +2 -120
  5. package/dist/config-Dzw8Ws4d.mjs +11 -0
  6. package/dist/config-lCKbrRnt.mjs +12 -0
  7. package/dist/{config-DDrgs-I3.mjs.map → config-lCKbrRnt.mjs.map} +1 -1
  8. package/dist/generator-extension-Cp5FUUAw.mjs +2687 -0
  9. package/dist/generator-extension-Cp5FUUAw.mjs.map +1 -0
  10. package/dist/index.mjs +2 -5
  11. package/dist/plugin-Dv2gKsuC.mjs +11 -0
  12. package/dist/plugin-VPl_QQGb.mjs +12 -0
  13. package/dist/{plugin-6_YlK-JG.mjs.map → plugin-VPl_QQGb.mjs.map} +1 -1
  14. package/dist/rolldown-runtime-B6QC8dMY.mjs +11 -0
  15. package/dist/{run-plugins-B1R0HG0g.mjs → run-plugins-CM1Af-4B.mjs} +2 -3
  16. package/dist/typegen-C6ZfoYTC.mjs +114 -0
  17. package/dist/typegen-CBI7dNXr.mjs +115 -0
  18. package/dist/{typegen-DugZmi-0.mjs.map → typegen-CBI7dNXr.mjs.map} +1 -1
  19. package/dist/types-n4LRUF_c.mjs +12 -0
  20. package/dist/{types-CGB8BiQh.mjs.map → types-n4LRUF_c.mjs.map} +1 -1
  21. package/package.json +5 -5
  22. package/dist/builtins-BW3g09hP.mjs +0 -8538
  23. package/dist/builtins-C_VfEGdg.mjs +0 -4182
  24. package/dist/builtins-C_VfEGdg.mjs.map +0 -1
  25. package/dist/config-DDrgs-I3.mjs +0 -171
  26. package/dist/config-DsQe2yzy.mjs +0 -169
  27. package/dist/generator-extension-DRNQpoZP.mjs +0 -4380
  28. package/dist/generator-extension-DRNQpoZP.mjs.map +0 -1
  29. package/dist/plugin-6_YlK-JG.mjs +0 -71
  30. package/dist/plugin-CQ0yYXyr.mjs +0 -80
  31. package/dist/rolldown-runtime-CYBbkZNy.mjs +0 -24
  32. package/dist/typegen-CYCsmCRF.mjs +0 -1351
  33. package/dist/typegen-DugZmi-0.mjs +0 -1353
  34. package/dist/types-CGB8BiQh.mjs +0 -25
@@ -1,1353 +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 { dirname, join, relative, resolve, sep } from "node:path";
13
- import { statSync } from "node:fs";
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("./run-plugins-B1R0HG0g.mjs");
1224
- const { loadKickConfig } = await import("./config-DDrgs-I3.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("./run-plugins-B1R0HG0g.mjs"), import("./config-DDrgs-I3.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 };
1352
-
1353
- //# sourceMappingURL=typegen-DugZmi-0.mjs.map