@forinda/kickjs-cli 5.0.2 → 5.1.0

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