@forinda/kickjs-cli 2.0.1 → 2.2.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.
Files changed (118) hide show
  1. package/bin.js +8 -0
  2. package/dist/cli.mjs +6550 -0
  3. package/dist/index.d.mts +302 -0
  4. package/dist/index.d.mts.map +1 -0
  5. package/dist/index.mjs +3627 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/dist/typegen-DCnJdqP1.mjs +886 -0
  8. package/dist/typegen-DCnJdqP1.mjs.map +1 -0
  9. package/package.json +31 -13
  10. package/dist/cli.d.ts +0 -2
  11. package/dist/cli.d.ts.map +0 -1
  12. package/dist/cli.js +0 -1552
  13. package/dist/commands/add.d.ts +0 -5
  14. package/dist/commands/add.d.ts.map +0 -1
  15. package/dist/commands/custom.d.ts +0 -56
  16. package/dist/commands/custom.d.ts.map +0 -1
  17. package/dist/commands/generate.d.ts +0 -3
  18. package/dist/commands/generate.d.ts.map +0 -1
  19. package/dist/commands/info.d.ts +0 -3
  20. package/dist/commands/info.d.ts.map +0 -1
  21. package/dist/commands/init.d.ts +0 -3
  22. package/dist/commands/init.d.ts.map +0 -1
  23. package/dist/commands/inspect.d.ts +0 -3
  24. package/dist/commands/inspect.d.ts.map +0 -1
  25. package/dist/commands/remove.d.ts +0 -3
  26. package/dist/commands/remove.d.ts.map +0 -1
  27. package/dist/commands/run.d.ts +0 -3
  28. package/dist/commands/run.d.ts.map +0 -1
  29. package/dist/commands/tinker.d.ts +0 -3
  30. package/dist/commands/tinker.d.ts.map +0 -1
  31. package/dist/config-D9faxBLQ.js +0 -3108
  32. package/dist/config.d.ts +0 -131
  33. package/dist/config.d.ts.map +0 -1
  34. package/dist/generators/adapter.d.ts +0 -7
  35. package/dist/generators/adapter.d.ts.map +0 -1
  36. package/dist/generators/config.d.ts +0 -9
  37. package/dist/generators/config.d.ts.map +0 -1
  38. package/dist/generators/controller.d.ts +0 -11
  39. package/dist/generators/controller.d.ts.map +0 -1
  40. package/dist/generators/dto.d.ts +0 -11
  41. package/dist/generators/dto.d.ts.map +0 -1
  42. package/dist/generators/guard.d.ts +0 -11
  43. package/dist/generators/guard.d.ts.map +0 -1
  44. package/dist/generators/job.d.ts +0 -8
  45. package/dist/generators/job.d.ts.map +0 -1
  46. package/dist/generators/middleware.d.ts +0 -11
  47. package/dist/generators/middleware.d.ts.map +0 -1
  48. package/dist/generators/module.d.ts +0 -33
  49. package/dist/generators/module.d.ts.map +0 -1
  50. package/dist/generators/patterns/cqrs.d.ts +0 -3
  51. package/dist/generators/patterns/cqrs.d.ts.map +0 -1
  52. package/dist/generators/patterns/ddd.d.ts +0 -3
  53. package/dist/generators/patterns/ddd.d.ts.map +0 -1
  54. package/dist/generators/patterns/index.d.ts +0 -6
  55. package/dist/generators/patterns/index.d.ts.map +0 -1
  56. package/dist/generators/patterns/minimal.d.ts +0 -3
  57. package/dist/generators/patterns/minimal.d.ts.map +0 -1
  58. package/dist/generators/patterns/rest.d.ts +0 -3
  59. package/dist/generators/patterns/rest.d.ts.map +0 -1
  60. package/dist/generators/patterns/types.d.ts +0 -15
  61. package/dist/generators/patterns/types.d.ts.map +0 -1
  62. package/dist/generators/project.d.ts +0 -14
  63. package/dist/generators/project.d.ts.map +0 -1
  64. package/dist/generators/remove-module.d.ts +0 -12
  65. package/dist/generators/remove-module.d.ts.map +0 -1
  66. package/dist/generators/resolver.d.ts +0 -7
  67. package/dist/generators/resolver.d.ts.map +0 -1
  68. package/dist/generators/scaffold.d.ts +0 -20
  69. package/dist/generators/scaffold.d.ts.map +0 -1
  70. package/dist/generators/service.d.ts +0 -11
  71. package/dist/generators/service.d.ts.map +0 -1
  72. package/dist/generators/templates/constants.d.ts +0 -3
  73. package/dist/generators/templates/constants.d.ts.map +0 -1
  74. package/dist/generators/templates/controller.d.ts +0 -6
  75. package/dist/generators/templates/controller.d.ts.map +0 -1
  76. package/dist/generators/templates/cqrs.d.ts +0 -23
  77. package/dist/generators/templates/cqrs.d.ts.map +0 -1
  78. package/dist/generators/templates/domain.d.ts +0 -5
  79. package/dist/generators/templates/domain.d.ts.map +0 -1
  80. package/dist/generators/templates/drizzle/index.d.ts +0 -4
  81. package/dist/generators/templates/drizzle/index.d.ts.map +0 -1
  82. package/dist/generators/templates/dtos.d.ts +0 -5
  83. package/dist/generators/templates/dtos.d.ts.map +0 -1
  84. package/dist/generators/templates/index.d.ts +0 -14
  85. package/dist/generators/templates/index.d.ts.map +0 -1
  86. package/dist/generators/templates/module-index.d.ts +0 -13
  87. package/dist/generators/templates/module-index.d.ts.map +0 -1
  88. package/dist/generators/templates/prisma/index.d.ts +0 -3
  89. package/dist/generators/templates/prisma/index.d.ts.map +0 -1
  90. package/dist/generators/templates/project-app.d.ts +0 -9
  91. package/dist/generators/templates/project-app.d.ts.map +0 -1
  92. package/dist/generators/templates/project-config.d.ts +0 -23
  93. package/dist/generators/templates/project-config.d.ts.map +0 -1
  94. package/dist/generators/templates/project-docs.d.ts +0 -9
  95. package/dist/generators/templates/project-docs.d.ts.map +0 -1
  96. package/dist/generators/templates/repository.d.ts +0 -5
  97. package/dist/generators/templates/repository.d.ts.map +0 -1
  98. package/dist/generators/templates/rest-service.d.ts +0 -6
  99. package/dist/generators/templates/rest-service.d.ts.map +0 -1
  100. package/dist/generators/templates/tests.d.ts +0 -4
  101. package/dist/generators/templates/tests.d.ts.map +0 -1
  102. package/dist/generators/templates/types.d.ts +0 -20
  103. package/dist/generators/templates/types.d.ts.map +0 -1
  104. package/dist/generators/templates/use-cases.d.ts +0 -6
  105. package/dist/generators/templates/use-cases.d.ts.map +0 -1
  106. package/dist/generators/test.d.ts +0 -9
  107. package/dist/generators/test.d.ts.map +0 -1
  108. package/dist/index.d.ts +0 -12
  109. package/dist/index.d.ts.map +0 -1
  110. package/dist/index.js +0 -17
  111. package/dist/utils/fs.d.ts +0 -11
  112. package/dist/utils/fs.d.ts.map +0 -1
  113. package/dist/utils/naming.d.ts +0 -18
  114. package/dist/utils/naming.d.ts.map +0 -1
  115. package/dist/utils/resolve-out-dir.d.ts +0 -25
  116. package/dist/utils/resolve-out-dir.d.ts.map +0 -1
  117. package/dist/utils/shell.d.ts +0 -3
  118. package/dist/utils/shell.d.ts.map +0 -1
@@ -0,0 +1,886 @@
1
+ /**
2
+ * @forinda/kickjs-cli v2.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 { dirname, join, relative, resolve, sep } from "node:path";
12
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
13
+ //#region src/typegen/scanner.ts
14
+ /** Decorators that mark a class as DI-managed */
15
+ const DECORATOR_NAMES = [
16
+ "Service",
17
+ "Controller",
18
+ "Repository",
19
+ "Injectable",
20
+ "Component",
21
+ "Module"
22
+ ];
23
+ const DEFAULT_EXTENSIONS = [
24
+ ".ts",
25
+ ".tsx",
26
+ ".mts",
27
+ ".cts"
28
+ ];
29
+ const DEFAULT_EXCLUDES = [
30
+ "node_modules",
31
+ ".kickjs",
32
+ "dist",
33
+ "build",
34
+ ".test.",
35
+ ".spec.",
36
+ ".d.ts"
37
+ ];
38
+ /**
39
+ * Match a class-level decorator immediately followed by an exported
40
+ * class declaration. Captures decorator name and class name.
41
+ */
42
+ 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");
43
+ /**
44
+ * Match a `createToken<T>('name')` call with optional `export const X =`
45
+ * or `const X =` prefix. Tolerates whitespace and the type parameter
46
+ * being absent (`createToken('name')`).
47
+ */
48
+ const CREATE_TOKEN_REGEX = /(?:export\s+)?const\s+(\w+)\s*(?::\s*[^=]+)?=\s*createToken\s*(?:<[^>]*>)?\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
49
+ /**
50
+ * Match a bare `createToken<T>('name')` call (no const assignment) so
51
+ * we still pick up dynamically-used tokens.
52
+ */
53
+ const BARE_CREATE_TOKEN_REGEX = /createToken\s*(?:<[^>]*>)?\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
54
+ /** Match `@Inject('literal')` — only literals; computed args are skipped */
55
+ const INJECT_LITERAL_REGEX = /@Inject\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
56
+ /**
57
+ * Match a route decorator immediately followed by a method declaration.
58
+ * Captures the HTTP verb, path literal (or empty), and method name.
59
+ *
60
+ * Tolerates:
61
+ * - Optional second arg to the route decorator (`@Get('/path', { ... })`)
62
+ * - Stacked decorators between the route and the method (`@Get('/') @Use(...)`)
63
+ * - Path-less decorators (`@Get()` → defaults to `/`)
64
+ * - `async` modifier on the method
65
+ *
66
+ * Run within a class body slice (see extractRoutesFromSource) so the
67
+ * captured method name is unambiguously a method on that class.
68
+ */
69
+ const ROUTE_METHOD_REGEX = new RegExp(String.raw`@(${[
70
+ "Get",
71
+ "Post",
72
+ "Put",
73
+ "Delete",
74
+ "Patch"
75
+ ].join("|")})\s*\(` + String.raw`(?:\s*['"\`]([^'"\`]*)['"\`])?[^)]*\)` + String.raw`(?:\s*@[A-Z]\w*(?:\s*\([^)]*\))?)*` + String.raw`\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?` + String.raw`([a-zA-Z_]\w*)\s*\(`, "g");
76
+ /** Extract `:placeholder` segments from an Express route path */
77
+ function extractPathParams(path) {
78
+ return (path.match(/:([a-zA-Z_]\w*)/g) ?? []).map((m) => m.slice(1));
79
+ }
80
+ /**
81
+ * Given the matched text of a route decorator + method declaration, return
82
+ * the substring inside the route decorator's argument list (between the
83
+ * outermost `(` and `)`). Returns `null` if no parens are found.
84
+ *
85
+ * Example input:
86
+ * `@Post('/', { body: createTaskSchema, name: 'CreateTask' }) async create(`
87
+ * Returns:
88
+ * `'/', { body: createTaskSchema, name: 'CreateTask' }`
89
+ */
90
+ function extractRouteOptionsArg(matchedText) {
91
+ const open = matchedText.indexOf("(");
92
+ if (open < 0) return null;
93
+ let depth = 1;
94
+ for (let i = open + 1; i < matchedText.length; i++) {
95
+ const ch = matchedText[i];
96
+ if (ch === "(") depth++;
97
+ else if (ch === ")") {
98
+ depth--;
99
+ if (depth === 0) return matchedText.slice(open + 1, i);
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ /**
105
+ * Extract a bare identifier value from a single field in an object literal
106
+ * embedded in a string. Returns `null` if the field is missing or its value
107
+ * isn't a bare identifier (e.g. an inline object, function call, etc.).
108
+ *
109
+ * Example: `extractObjectFieldIdentifier("'/' , { body: createTaskSchema }", 'body')`
110
+ * returns `'createTaskSchema'`.
111
+ */
112
+ function extractObjectFieldIdentifier(text, field) {
113
+ const m = new RegExp(String.raw`\b${field}\s*:\s*([A-Za-z_$][\w$]*)`, "g").exec(text);
114
+ if (!m) return null;
115
+ return m[1];
116
+ }
117
+ /**
118
+ * Resolve a bare identifier to its module source by inspecting the file's
119
+ * top-level imports and same-file `const` declarations.
120
+ *
121
+ * - `import { X } from './path'` → returns `'./path'`
122
+ * - `import X from './path'` (default import) → returns `'./path'`
123
+ * - `import * as X from './path'` → returns `'./path'`
124
+ * - `const X = z.object(...)` (same file) → returns `null` (caller emits a self-import)
125
+ *
126
+ * Returns `null` when the identifier cannot be resolved.
127
+ */
128
+ function resolveImportSource(source, identifier) {
129
+ const named = new RegExp(String.raw`import\s*(?:type\s+)?\{[^}]*\b${identifier}\b[^}]*\}\s*from\s*['"\`]([^'"\`]+)['"\`]`).exec(source);
130
+ if (named) return named[1];
131
+ const def = new RegExp(String.raw`import\s+(?:type\s+)?${identifier}\s+from\s*['"\`]([^'"\`]+)['"\`]`).exec(source);
132
+ if (def) return def[1];
133
+ const ns = new RegExp(String.raw`import\s*\*\s*as\s+${identifier}\s+from\s*['"\`]([^'"\`]+)['"\`]`).exec(source);
134
+ if (ns) return ns[1];
135
+ if (new RegExp(String.raw`(?:^|\n)\s*(?:export\s+)?const\s+${identifier}\b`).test(source)) return "";
136
+ return null;
137
+ }
138
+ /**
139
+ * Extract whitelist arrays from an `@ApiQueryParams(...)` decorator
140
+ * within `decoratorBlock`. Handles two forms:
141
+ *
142
+ * - Inline literal: `@ApiQueryParams({ filterable: ['a', 'b'], ... })`
143
+ * - Const reference: `@ApiQueryParams(SOME_CONFIG)` — looks up
144
+ * `const SOME_CONFIG = { ... }` in the same file (`fullSource`).
145
+ *
146
+ * Returns `null` if no `@ApiQueryParams` is present. Returns
147
+ * `{ filterable: [], sortable: [], searchable: [] }` if the decorator
148
+ * is present but no fields could be statically extracted (opaque
149
+ * imports, column-object configs, function calls, etc.).
150
+ */
151
+ function extractApiQueryParams(decoratorBlock, fullSource) {
152
+ const apiMatch = /@ApiQueryParams\s*\(\s*([\s\S]*?)\s*\)\s*$/.exec(decoratorBlock);
153
+ if (!apiMatch) {
154
+ const loose = /@ApiQueryParams\s*\(([\s\S]*?)\)/.exec(decoratorBlock);
155
+ if (!loose) return null;
156
+ return parseApiQueryParamsArg(loose[1].trim(), fullSource);
157
+ }
158
+ return parseApiQueryParamsArg(apiMatch[1].trim(), fullSource);
159
+ }
160
+ function parseApiQueryParamsArg(arg, fullSource) {
161
+ if (arg.startsWith("{")) return parseInlineConfigLiteral(arg);
162
+ const idMatch = /^([A-Za-z_]\w*)/.exec(arg);
163
+ if (idMatch) {
164
+ const ident = idMatch[1];
165
+ const constMatch = new RegExp(String.raw`const\s+${ident}\s*(?::\s*[^=]+)?=\s*(\{[\s\S]*?\n\})`, "m").exec(fullSource);
166
+ if (constMatch) return parseInlineConfigLiteral(constMatch[1]);
167
+ }
168
+ return {
169
+ filterable: [],
170
+ sortable: [],
171
+ searchable: []
172
+ };
173
+ }
174
+ /** Extract a string array literal for one config key from an inline object literal */
175
+ function extractStringArray(literal, key) {
176
+ const m = new RegExp(String.raw`${key}\s*:\s*\[([\s\S]*?)\]`).exec(literal);
177
+ if (!m) return [];
178
+ return Array.from(m[1].matchAll(/['"`]([^'"`]+)['"`]/g)).map((x) => x[1]);
179
+ }
180
+ /** Parse an inline `{ filterable: [...], sortable: [...], searchable: [...] }` literal */
181
+ function parseInlineConfigLiteral(literal) {
182
+ return {
183
+ filterable: extractStringArray(literal, "filterable"),
184
+ sortable: extractStringArray(literal, "sortable"),
185
+ searchable: extractStringArray(literal, "searchable")
186
+ };
187
+ }
188
+ /** Recursively walk a directory and yield matching file paths */
189
+ async function walk(dir, opts) {
190
+ const exts = opts.extensions ?? DEFAULT_EXTENSIONS;
191
+ const excludes = opts.exclude ?? DEFAULT_EXCLUDES;
192
+ const out = [];
193
+ let entries;
194
+ try {
195
+ entries = await readdir(dir, {
196
+ withFileTypes: true,
197
+ encoding: "utf-8"
198
+ });
199
+ } catch {
200
+ return out;
201
+ }
202
+ for (const entry of entries) {
203
+ const full = join(dir, entry.name);
204
+ const rel = relative(opts.cwd, full);
205
+ if (excludes.some((ex) => rel.includes(ex))) continue;
206
+ if (entry.isDirectory()) out.push(...await walk(full, opts));
207
+ else if (entry.isFile()) {
208
+ if (exts.some((ext) => entry.name.endsWith(ext))) out.push(full);
209
+ }
210
+ }
211
+ return out;
212
+ }
213
+ /** Compute the forward-slash relative path used in scanner output */
214
+ function toRelative(filePath, cwd) {
215
+ return relative(cwd, filePath).split(sep).join("/");
216
+ }
217
+ /** Extract decorated classes from a single source file */
218
+ function extractClassesFromSource(source, filePath, cwd) {
219
+ const out = [];
220
+ const relPath = toRelative(filePath, cwd);
221
+ DECORATED_CLASS_REGEX.lastIndex = 0;
222
+ let match;
223
+ while ((match = DECORATED_CLASS_REGEX.exec(source)) !== null) {
224
+ const [, decorator, defaultMarker, className] = match;
225
+ out.push({
226
+ className,
227
+ decorator,
228
+ filePath,
229
+ relativePath: relPath,
230
+ isDefault: Boolean(defaultMarker)
231
+ });
232
+ }
233
+ return out;
234
+ }
235
+ /** Extract `createToken('name')` definitions from a single source file */
236
+ function extractTokensFromSource(source, filePath, cwd) {
237
+ const out = [];
238
+ const relPath = toRelative(filePath, cwd);
239
+ const seen = /* @__PURE__ */ new Set();
240
+ CREATE_TOKEN_REGEX.lastIndex = 0;
241
+ let match;
242
+ while ((match = CREATE_TOKEN_REGEX.exec(source)) !== null) {
243
+ const [full, variable, name] = match;
244
+ seen.add(full);
245
+ out.push({
246
+ name,
247
+ variable,
248
+ filePath,
249
+ relativePath: relPath
250
+ });
251
+ }
252
+ BARE_CREATE_TOKEN_REGEX.lastIndex = 0;
253
+ while ((match = BARE_CREATE_TOKEN_REGEX.exec(source)) !== null) {
254
+ if (seen.has(match[0])) continue;
255
+ out.push({
256
+ name: match[1],
257
+ variable: null,
258
+ filePath,
259
+ relativePath: relPath
260
+ });
261
+ }
262
+ return out;
263
+ }
264
+ /**
265
+ * Extract route handlers from a source file.
266
+ *
267
+ * For each decorated class in `classesInFile`, slices the source from
268
+ * the class declaration to the next class (or EOF) and runs the route
269
+ * decorator regex within that slice. The result is a list of routes
270
+ * tagged with their owning controller.
271
+ *
272
+ * Heuristic note: this assumes classes are not nested. KickJS controllers
273
+ * are top-level by convention so this holds in practice.
274
+ */
275
+ function extractRoutesFromSource(source, filePath, cwd, classesInFile) {
276
+ const out = [];
277
+ if (classesInFile.length === 0) return out;
278
+ const relPath = toRelative(filePath, cwd);
279
+ const positions = [];
280
+ for (const cls of classesInFile) {
281
+ const m = new RegExp(String.raw`class\s+${cls.className}\b`).exec(source);
282
+ if (m?.index !== void 0) positions.push({
283
+ cls,
284
+ start: m.index
285
+ });
286
+ }
287
+ positions.sort((a, b) => a.start - b.start);
288
+ for (let i = 0; i < positions.length; i++) {
289
+ const { cls, start } = positions[i];
290
+ const end = i + 1 < positions.length ? positions[i + 1].start : source.length;
291
+ const block = source.slice(start, end);
292
+ ROUTE_METHOD_REGEX.lastIndex = 0;
293
+ let match;
294
+ while ((match = ROUTE_METHOD_REGEX.exec(block)) !== null) {
295
+ const [matchedText, verb, pathLiteral, methodName] = match;
296
+ const path = pathLiteral && pathLiteral.length > 0 ? pathLiteral : "/";
297
+ const apiQp = extractApiQueryParams(matchedText, source);
298
+ const routeArgs = extractRouteOptionsArg(matchedText);
299
+ const bodyId = routeArgs ? extractObjectFieldIdentifier(routeArgs, "body") : null;
300
+ const queryId = routeArgs ? extractObjectFieldIdentifier(routeArgs, "query") : null;
301
+ const paramsId = routeArgs ? extractObjectFieldIdentifier(routeArgs, "params") : null;
302
+ out.push({
303
+ controller: cls.className,
304
+ method: methodName,
305
+ httpMethod: verb.toUpperCase(),
306
+ path,
307
+ pathParams: extractPathParams(path),
308
+ queryFilterable: apiQp?.filterable ?? null,
309
+ querySortable: apiQp?.sortable ?? null,
310
+ querySearchable: apiQp?.searchable ?? null,
311
+ bodySchema: bodyId ? {
312
+ identifier: bodyId,
313
+ source: resolveImportSource(source, bodyId)
314
+ } : null,
315
+ querySchema: queryId ? {
316
+ identifier: queryId,
317
+ source: resolveImportSource(source, queryId)
318
+ } : null,
319
+ paramsSchema: paramsId ? {
320
+ identifier: paramsId,
321
+ source: resolveImportSource(source, paramsId)
322
+ } : null,
323
+ filePath,
324
+ relativePath: relPath
325
+ });
326
+ }
327
+ }
328
+ return out;
329
+ }
330
+ /** Extract `@Inject('literal')` calls from a single source file */
331
+ function extractInjectsFromSource(source, filePath, cwd) {
332
+ const out = [];
333
+ const relPath = toRelative(filePath, cwd);
334
+ INJECT_LITERAL_REGEX.lastIndex = 0;
335
+ let match;
336
+ while ((match = INJECT_LITERAL_REGEX.exec(source)) !== null) out.push({
337
+ name: match[1],
338
+ filePath,
339
+ relativePath: relPath
340
+ });
341
+ return out;
342
+ }
343
+ /**
344
+ * Look for an env schema file at `<cwd>/<envFile>`. Returns a
345
+ * `DiscoveredEnv` if the file exists and contains both a
346
+ * `defineEnv(...)` call and a default export — the two markers we
347
+ * need before it's safe to emit `import type schema from '...'` in
348
+ * the generator.
349
+ *
350
+ * Returns `null` for any other state (file missing, no defineEnv, no
351
+ * default export) so the generator skips env typing silently. Users
352
+ * who want env typing must opt in by writing `src/env.ts` to the
353
+ * documented shape.
354
+ */
355
+ async function detectEnvFile(cwd, envFile) {
356
+ const abs = resolve(cwd, envFile);
357
+ let source;
358
+ try {
359
+ source = await readFile(abs, "utf-8");
360
+ } catch {
361
+ return null;
362
+ }
363
+ if (!/\bdefineEnv\s*\(/.test(source)) return null;
364
+ if (!/export\s+default\b/.test(source)) return null;
365
+ return {
366
+ filePath: abs,
367
+ relativePath: toRelative(abs, cwd)
368
+ };
369
+ }
370
+ /** Detect duplicate class names across files */
371
+ function findCollisions(classes) {
372
+ const groups = /* @__PURE__ */ new Map();
373
+ for (const cls of classes) {
374
+ const arr = groups.get(cls.className) ?? [];
375
+ arr.push(cls);
376
+ groups.set(cls.className, arr);
377
+ }
378
+ const collisions = [];
379
+ for (const [className, group] of groups) if (new Set(group.map((c) => c.filePath)).size > 1) collisions.push({
380
+ className,
381
+ classes: group
382
+ });
383
+ collisions.sort((a, b) => a.className.localeCompare(b.className));
384
+ return collisions;
385
+ }
386
+ /**
387
+ * Scan a project for decorated classes, createToken definitions, and
388
+ * `@Inject` literal usages.
389
+ */
390
+ async function scanProject(opts) {
391
+ const files = await walk(resolve(opts.root), opts);
392
+ const classes = [];
393
+ const routes = [];
394
+ const tokens = [];
395
+ const injects = [];
396
+ const sources = /* @__PURE__ */ new Map();
397
+ for (const file of files) {
398
+ let source;
399
+ try {
400
+ source = await readFile(file, "utf-8");
401
+ } catch {
402
+ continue;
403
+ }
404
+ sources.set(file, source);
405
+ classes.push(...extractClassesFromSource(source, file, opts.cwd));
406
+ tokens.push(...extractTokensFromSource(source, file, opts.cwd));
407
+ injects.push(...extractInjectsFromSource(source, file, opts.cwd));
408
+ }
409
+ for (const [file, source] of sources) {
410
+ const classesInFile = classes.filter((c) => c.filePath === file);
411
+ routes.push(...extractRoutesFromSource(source, file, opts.cwd, classesInFile));
412
+ }
413
+ classes.sort((a, b) => {
414
+ if (a.className !== b.className) return a.className.localeCompare(b.className);
415
+ return a.relativePath.localeCompare(b.relativePath);
416
+ });
417
+ tokens.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
418
+ injects.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
419
+ routes.sort((a, b) => a.controller.localeCompare(b.controller) || a.method.localeCompare(b.method));
420
+ return {
421
+ classes,
422
+ routes,
423
+ tokens,
424
+ injects,
425
+ collisions: findCollisions(classes),
426
+ env: await detectEnvFile(opts.cwd, opts.envFile ?? "src/env.ts")
427
+ };
428
+ }
429
+ //#endregion
430
+ //#region src/typegen/generator.ts
431
+ /**
432
+ * Generates `.d.ts` files inside `.kickjs/types/` from the discovered
433
+ * decorated classes and DI tokens. Pattern modeled on React Router's
434
+ * `.react-router/types/` directory.
435
+ *
436
+ * Outputs:
437
+ * - `.kickjs/types/registry.d.ts` — module augmentation for `KickJsRegistry`
438
+ * that gives `container.resolve('UserService')` the right return type.
439
+ * - `.kickjs/types/services.d.ts` — string-literal union of all known
440
+ * service-style tokens for tooling autocomplete.
441
+ * - `.kickjs/types/modules.d.ts` — string-literal union of discovered
442
+ * module class names.
443
+ * - `.kickjs/types/index.d.ts` — re-exports the above (single import target).
444
+ * - `.kickjs/.gitignore` — gitignores the whole folder so generated files
445
+ * never get committed.
446
+ *
447
+ * ## Collision behaviour
448
+ *
449
+ * If `findCollisions()` returns any duplicate class names:
450
+ * - **Default (`allowDuplicates: false`)** — `generateTypes` throws a
451
+ * `TokenCollisionError` with a clear message listing every conflicting
452
+ * file. The caller (CLI) prints it and exits non-zero. Nothing is
453
+ * written to disk.
454
+ * - **`allowDuplicates: true`** — colliding classes are auto-namespaced
455
+ * by their relative file path so the registry keys become e.g.
456
+ * `'modules/users/UserService'` instead of `'UserService'`. Non-colliding
457
+ * classes still get bare `'ClassName'` keys (smart default).
458
+ *
459
+ * @module @forinda/kickjs-cli/typegen/generator
460
+ */
461
+ /** Header written to every generated file */
462
+ const HEADER = `/* eslint-disable */
463
+ // AUTO-GENERATED by \`kick typegen\`. DO NOT EDIT.
464
+ // Re-run with \`kick typegen\` or rely on \`kick dev\` to refresh.
465
+ `;
466
+ /** Decorators whose classes participate in the DI registry augmentation */
467
+ const REGISTRY_DECORATORS = new Set([
468
+ "Service",
469
+ "Repository",
470
+ "Injectable",
471
+ "Component"
472
+ ]);
473
+ /** Thrown by `generateTypes` when collisions are found and not allowed */
474
+ var TokenCollisionError = class extends Error {
475
+ collisions;
476
+ constructor(collisions) {
477
+ super(formatCollisionMessage(collisions));
478
+ this.name = "TokenCollisionError";
479
+ this.collisions = collisions;
480
+ }
481
+ };
482
+ /** Build a human-readable message describing every collision */
483
+ function formatCollisionMessage(collisions) {
484
+ const lines = ["kick typegen: token collision detected"];
485
+ for (const c of collisions) {
486
+ lines.push("");
487
+ lines.push(` ${c.classes.length} classes named '${c.className}':`);
488
+ for (const cls of c.classes) lines.push(` - ${cls.relativePath}`);
489
+ }
490
+ lines.push("");
491
+ lines.push("Resolutions:");
492
+ lines.push(" (a) Rename one of the classes");
493
+ lines.push(" (b) Use createToken<T>('namespaced/Name') and import the token explicitly — see @forinda/kickjs");
494
+ lines.push(" (c) Pass --allow-duplicates to namespace the registry keys automatically");
495
+ lines.push(" (e.g. 'modules/users/UserService' instead of 'UserService')");
496
+ return lines.join("\n");
497
+ }
498
+ /** Compute the module specifier (without extension) used inside `import('...')` */
499
+ function importSpecifierFor(targetFile, fromFile) {
500
+ let rel = relative(dirname(fromFile), targetFile).split(sep).join("/");
501
+ rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
502
+ if (!rel.startsWith(".")) rel = "./" + rel;
503
+ return rel;
504
+ }
505
+ /**
506
+ * Build the namespaced registry key for a colliding class.
507
+ * Strips the `src/` prefix and the file extension, then appends the
508
+ * class name. Example: `src/modules/users/user.service.ts` + `UserService`
509
+ * → `modules/users/UserService`.
510
+ */
511
+ function namespacedKeyFor(cls) {
512
+ const parts = cls.relativePath.replace(/^src\//, "").replace(/\.(ts|tsx|mts|cts)$/i, "").split("/");
513
+ parts.pop();
514
+ const ns = parts.join("/");
515
+ return ns ? `${ns}/${cls.className}` : cls.className;
516
+ }
517
+ /**
518
+ * Render the `KickJsRegistry` module augmentation. Each entry maps a
519
+ * string token to the imported class type.
520
+ *
521
+ * Default-exported classes are imported as `import('...').default`.
522
+ *
523
+ * `collidingNames` lists class names that should be auto-namespaced;
524
+ * everything else gets a bare key.
525
+ */
526
+ function renderRegistry(classes, outFile, collidingNames) {
527
+ const seen = /* @__PURE__ */ new Set();
528
+ const entries = [];
529
+ for (const c of classes) {
530
+ if (!REGISTRY_DECORATORS.has(c.decorator)) continue;
531
+ const key = collidingNames.has(c.className) ? namespacedKeyFor(c) : c.className;
532
+ if (seen.has(key)) continue;
533
+ seen.add(key);
534
+ const spec = importSpecifierFor(c.filePath, outFile);
535
+ const ref = c.isDefault ? `import('${spec}').default` : `import('${spec}').${c.className}`;
536
+ entries.push(` '${key}': ${ref}`);
537
+ }
538
+ return `${HEADER}
539
+ declare module '@forinda/kickjs' {
540
+ interface KickJsRegistry {
541
+ ${entries.length ? entries.join("\n") : " // (no services discovered yet — run `kick g service <name>` to add one)"}
542
+ }
543
+ }
544
+
545
+ export {}
546
+ `;
547
+ }
548
+ /** Render a string-literal union type containing the given names */
549
+ function renderUnion(typeName, names, emptyComment) {
550
+ if (names.length === 0) return `${HEADER}
551
+ // ${emptyComment}
552
+ export type ${typeName} = never
553
+ `;
554
+ return `${HEADER}
555
+ export type ${typeName} =
556
+ ${[...new Set(names)].sort().map((n) => ` | '${n}'`).join("\n")}
557
+ `;
558
+ }
559
+ /** Render the barrel index that re-exports the union types */
560
+ function renderIndex(includeEnv) {
561
+ return `${HEADER}
562
+ export type { ServiceToken } from './services'
563
+ export type { ModuleToken } from './modules'
564
+
565
+ // The registry, routes, and env augmentations are loaded as side-effects —
566
+ // importing this file (or having it on tsconfig include) is enough for
567
+ // \`container.resolve()\`, \`Ctx<KickRoutes.UserController['getUser']>\`,
568
+ // and \`@Value('PORT')\` to resolve.
569
+ import './registry'
570
+ import './routes'
571
+ ${includeEnv ? "import './env'\n" : ""}`;
572
+ }
573
+ /**
574
+ * Render the `query` field's TypeScript type for a single route.
575
+ *
576
+ * - When `@ApiQueryParams` is absent (`queryFilterable === null`), emits
577
+ * `unknown` so the user gets nothing extra.
578
+ * - When the decorator is present, emits an object literal whose keys
579
+ * are the standard query string keys (`filter`, `sort`, `q`, `page`,
580
+ * `limit`). `sort` is narrowed to a string-literal union of allowed
581
+ * field names with optional `-` direction prefix.
582
+ */
583
+ function renderQueryShape(m) {
584
+ if (m.queryFilterable === null) return "unknown";
585
+ const sortable = m.querySortable ?? [];
586
+ return `{ filter?: string | string[]; sort?: ${sortable.length > 0 ? sortable.flatMap((f) => [`'${f}'`, `'-${f}'`]).join(" | ") : "string"}; q?: string; page?: string; limit?: string }`;
587
+ }
588
+ /** Render JSDoc lines summarising the @ApiQueryParams whitelist */
589
+ function renderQueryDocLines(m) {
590
+ const lines = [];
591
+ if (m.queryFilterable && m.queryFilterable.length > 0) lines.push(`Filterable: ${m.queryFilterable.join(", ")}`);
592
+ if (m.querySortable && m.querySortable.length > 0) lines.push(`Sortable: ${m.querySortable.join(", ")}`);
593
+ if (m.querySearchable && m.querySearchable.length > 0) lines.push(`Searchable: ${m.querySearchable.join(", ")}`);
594
+ return lines;
595
+ }
596
+ /**
597
+ * Plan a schema import for hoisting at the top of `routes.ts`. Returns
598
+ * the alias the in-namespace code should use, or `null` if the schema
599
+ * cannot be referenced (no validator configured, or source unresolvable).
600
+ *
601
+ * Aliases are unique per (alias-counter) so two schemas named
602
+ * `createTaskSchema` from different modules don't collide.
603
+ */
604
+ function planSchemaImport(schema, routeFilePath, routesOutFile, schemaValidator, imports) {
605
+ if (!schema || schemaValidator !== "zod") return null;
606
+ if (schema.source === null) return null;
607
+ const specifier = resolveSchemaImportSpecifier(schema.source, routeFilePath, routesOutFile);
608
+ if (specifier === "unknown") return null;
609
+ const key = `${specifier}::${schema.identifier}`;
610
+ let alias = imports.get(key)?.specifier;
611
+ if (!alias) {
612
+ alias = `_S${imports.size}`;
613
+ imports.set(key, {
614
+ identifier: schema.identifier,
615
+ specifier: alias
616
+ });
617
+ } else alias = imports.get(key).specifier;
618
+ return alias;
619
+ }
620
+ /** Build the `import type { ... } from '...'` lines for hoisted schema imports */
621
+ function renderSchemaImports(imports) {
622
+ if (imports.size === 0) return "";
623
+ const lines = [];
624
+ for (const [key, value] of imports) {
625
+ const [path] = key.split("::");
626
+ lines.push(`import type { ${value.identifier} as ${value.specifier} } from '${path}'`);
627
+ }
628
+ return lines.join("\n") + "\n";
629
+ }
630
+ /**
631
+ * Compute the import specifier the generated `routes.d.ts` should use to
632
+ * reach a schema declared either in the controller file (empty string)
633
+ * or imported from elsewhere (relative path or bare module name).
634
+ *
635
+ * - Bare module names (`zod`, `@scope/pkg`) are returned as-is.
636
+ * - Relative paths (`./users.dto`, `../shared/schema`) are resolved
637
+ * against the controller's file path, then re-relativised against the
638
+ * directory containing `routes.d.ts`.
639
+ * - Empty string (same-file schema) becomes a relative path from the
640
+ * `routes.d.ts` directory back to the controller file.
641
+ */
642
+ function resolveSchemaImportSpecifier(source, routeFilePath, routesOutFile) {
643
+ if (source === null) return "unknown";
644
+ const routesDir = dirname(routesOutFile);
645
+ if (source === "") {
646
+ let rel = relative(routesDir, routeFilePath).split(sep).join("/");
647
+ rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
648
+ if (!rel.startsWith(".")) rel = "./" + rel;
649
+ return rel;
650
+ }
651
+ if (!source.startsWith(".") && !source.startsWith("/")) return source;
652
+ let rel = relative(routesDir, resolve(dirname(routeFilePath), source)).split(sep).join("/");
653
+ rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
654
+ if (!rel.startsWith(".")) rel = "./" + rel;
655
+ return rel;
656
+ }
657
+ /**
658
+ * Render the `KickEnv` + `NodeJS.ProcessEnv` augmentation file from a
659
+ * detected env schema. Mirrors the routes.ts pattern: emits as a `.ts`
660
+ * file (not `.d.ts`) so the top-level `import type schema from '...'`
661
+ * actually resolves under `moduleResolution: 'bundler'`.
662
+ *
663
+ * Returns `null` when no env file was discovered, so the caller can
664
+ * skip writing the file altogether (rather than emitting an empty
665
+ * augmentation that would shadow `KickEnv` to a useless `{}`).
666
+ */
667
+ function renderEnv(env, envOutFile) {
668
+ if (!env) return null;
669
+ let rel = relative(dirname(envOutFile), env.filePath).split(sep).join("/");
670
+ rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
671
+ if (!rel.startsWith(".")) rel = "./" + rel;
672
+ return `${HEADER}
673
+ // Importing the schema as a type lets us infer its shape without
674
+ // pulling in any runtime code. \`Awaited<>\` strips an accidental
675
+ // Promise wrap on dynamic-imported defaults.
676
+ import type _envSchema from '${rel}'
677
+
678
+ // Local type alias — interfaces can only \`extend\` an identifier,
679
+ // not an inline import expression, so we resolve the schema's
680
+ // inferred shape into a named type first.
681
+ type _KickEnvShape = import('zod').infer<typeof _envSchema>
682
+
683
+ declare global {
684
+ /**
685
+ * Typed environment registry. Augmented from \`${env.relativePath}\`
686
+ * so \`@Value('PORT')\`, \`Env<'PORT'>\`, and \`process.env.PORT\` are
687
+ * all type-safe and autocomplete.
688
+ */
689
+ interface KickEnv extends _KickEnvShape {}
690
+
691
+ // eslint-disable-next-line @typescript-eslint/no-namespace
692
+ namespace NodeJS {
693
+ /**
694
+ * Narrow \`process.env\` so known keys exist as \`string\` (the raw
695
+ * pre-Zod-coercion form). \`@Value\` and the \`ConfigService\` apply
696
+ * the schema's transforms internally; access \`process.env\` directly
697
+ * only when you need the raw string. Unknown keys still resolve to
698
+ * \`string | undefined\` via the base @types/node declaration.
699
+ */
700
+ interface ProcessEnv extends Record<keyof KickEnv, string> {}
701
+ }
702
+ }
703
+
704
+ export {}
705
+ `;
706
+ }
707
+ /**
708
+ * Render the `KickRoutes` global namespace augmentation. Each interface
709
+ * inside corresponds to a controller class; each property is a single
710
+ * route method on that controller, conforming to `RouteShape`.
711
+ *
712
+ * Fills `params` from URL patterns, `query` from `@ApiQueryParams`, and
713
+ * `body`/`query`/`params` (when schema-validated) from the configured
714
+ * schema validator. `response` is emitted as `unknown`.
715
+ */
716
+ function renderRoutes(routes, routesOutFile, schemaValidator) {
717
+ if (routes.length === 0) return `${HEADER}
718
+ // (no routes discovered yet — annotate a controller method with
719
+ // @Get/@Post/@Put/@Delete/@Patch and re-run \`kick typegen\`)
720
+ declare global {
721
+ // eslint-disable-next-line @typescript-eslint/no-namespace
722
+ namespace KickRoutes {}
723
+ }
724
+
725
+ export {}
726
+ `;
727
+ const byController = /* @__PURE__ */ new Map();
728
+ for (const r of routes) {
729
+ const arr = byController.get(r.controller) ?? [];
730
+ arr.push(r);
731
+ byController.set(r.controller, arr);
732
+ }
733
+ const schemaImports = /* @__PURE__ */ new Map();
734
+ const renderField = (schema, routeFilePath) => {
735
+ const alias = planSchemaImport(schema, routeFilePath, routesOutFile, schemaValidator, schemaImports);
736
+ return alias ? `import('zod').infer<typeof ${alias}>` : null;
737
+ };
738
+ const interfaces = [];
739
+ for (const [controller, methods] of byController) {
740
+ const lines = [` interface ${controller} {`];
741
+ for (const m of methods) {
742
+ const urlParamsType = m.pathParams.length > 0 ? `{ ${m.pathParams.map((p) => `${p}: string`).join("; ")} }` : "{}";
743
+ const bodySchemaType = renderField(m.bodySchema, m.filePath);
744
+ const querySchemaType = renderField(m.querySchema, m.filePath);
745
+ const paramsType = renderField(m.paramsSchema, m.filePath) ?? urlParamsType;
746
+ const bodyType = bodySchemaType ?? "unknown";
747
+ const queryType = querySchemaType ?? renderQueryShape(m);
748
+ const docLines = renderQueryDocLines(m);
749
+ lines.push(` /**`, ` * ${m.httpMethod} ${m.path}`, ...docLines.map((d) => ` * ${d}`), ` */`, ` ${m.method}: {`, ` params: ${paramsType}`, ` body: ${bodyType}`, ` query: ${queryType}`, ` response: unknown`, ` }`);
750
+ }
751
+ lines.push(" }");
752
+ interfaces.push(lines.join("\n"));
753
+ }
754
+ return `${HEADER}${renderSchemaImports(schemaImports)}
755
+ declare global {
756
+ // eslint-disable-next-line @typescript-eslint/no-namespace
757
+ namespace KickRoutes {
758
+ ${interfaces.join("\n")}
759
+ }
760
+ }
761
+
762
+ export {}
763
+ `;
764
+ }
765
+ /** Write all generated `.d.ts` files to `outDir` */
766
+ async function generateTypes(opts) {
767
+ const { classes, routes = [], tokens = [], injects = [], collisions = [], env = null, outDir, allowDuplicates = false, schemaValidator = false } = opts;
768
+ if (collisions.length > 0 && !allowDuplicates) throw new TokenCollisionError(collisions);
769
+ await mkdir(outDir, { recursive: true });
770
+ const registryFile = join(outDir, "registry.d.ts");
771
+ const servicesFile = join(outDir, "services.d.ts");
772
+ const modulesFile = join(outDir, "modules.d.ts");
773
+ const routesFile = join(outDir, "routes.ts");
774
+ const envFile = join(outDir, "env.ts");
775
+ const indexFile = join(outDir, "index.d.ts");
776
+ const collidingNames = new Set(collisions.map((c) => c.className));
777
+ const registryContent = renderRegistry(classes, registryFile, collidingNames);
778
+ const classTokens = classes.filter((c) => REGISTRY_DECORATORS.has(c.decorator)).map((c) => collidingNames.has(c.className) ? namespacedKeyFor(c) : c.className);
779
+ const tokenLiterals = tokens.map((t) => t.name);
780
+ const injectLiterals = injects.map((i) => i.name);
781
+ const allServices = [
782
+ ...classTokens,
783
+ ...tokenLiterals,
784
+ ...injectLiterals
785
+ ];
786
+ const modules = classes.filter((c) => c.decorator === "Module").map((c) => c.className);
787
+ const servicesContent = renderUnion("ServiceToken", allServices, "(no tokens discovered — declare with createToken<T>() or `kick g service <name>`)");
788
+ const modulesContent = renderUnion("ModuleToken", modules, "(no @Module classes discovered — `kick g module <name>` to add one)");
789
+ const routesContent = renderRoutes(routes, routesFile, schemaValidator);
790
+ const envContent = renderEnv(env, envFile);
791
+ const indexContent = renderIndex(envContent !== null);
792
+ await writeFile(registryFile, registryContent, "utf-8");
793
+ await writeFile(servicesFile, servicesContent, "utf-8");
794
+ await writeFile(modulesFile, modulesContent, "utf-8");
795
+ await writeFile(routesFile, routesContent, "utf-8");
796
+ await writeFile(indexFile, indexContent, "utf-8");
797
+ const written = [
798
+ registryFile,
799
+ servicesFile,
800
+ modulesFile,
801
+ routesFile,
802
+ indexFile
803
+ ];
804
+ if (envContent) {
805
+ await writeFile(envFile, envContent, "utf-8");
806
+ written.push(envFile);
807
+ }
808
+ await writeFile(join(dirname(outDir), ".gitignore"), "# Auto-generated by kick typegen\n*\n", "utf-8");
809
+ return {
810
+ registryEntries: classTokens.length,
811
+ serviceTokens: new Set(allServices).size,
812
+ moduleTokens: modules.length,
813
+ routeEntries: routes.length,
814
+ envWritten: envContent !== null,
815
+ written,
816
+ resolvedCollisions: collisions.length
817
+ };
818
+ }
819
+ //#endregion
820
+ //#region src/typegen/index.ts
821
+ /**
822
+ * Public entry point for the KickJS typegen module.
823
+ *
824
+ * Used by:
825
+ * - `kick typegen` (one-shot or watch mode)
826
+ * - `kick dev` (auto-runs once before Vite starts; refreshes when files change)
827
+ *
828
+ * @module @forinda/kickjs-cli/typegen
829
+ */
830
+ /** Resolve options to absolute paths */
831
+ function resolveOptions(opts) {
832
+ const cwd = opts.cwd ?? process.cwd();
833
+ return {
834
+ cwd,
835
+ srcDir: resolve(cwd, opts.srcDir ?? "src"),
836
+ outDir: resolve(cwd, opts.outDir ?? ".kickjs/types"),
837
+ silent: opts.silent ?? false,
838
+ allowDuplicates: opts.allowDuplicates ?? false,
839
+ schemaValidator: opts.schemaValidator ?? false,
840
+ envFile: opts.envFile ?? "src/env.ts"
841
+ };
842
+ }
843
+ /**
844
+ * Run a single typegen pass: scan source files, generate `.d.ts` files.
845
+ *
846
+ * Returns the discovered scan result alongside the generation result so
847
+ * callers (`kick dev`, devtools) can log them or feed them to other tools.
848
+ *
849
+ * Throws `TokenCollisionError` if duplicate class names are found and
850
+ * `allowDuplicates` is false.
851
+ */
852
+ async function runTypegen(opts = {}) {
853
+ const { cwd, srcDir, outDir, silent, allowDuplicates, schemaValidator, envFile } = resolveOptions(opts);
854
+ const start = Date.now();
855
+ const scan = await scanProject({
856
+ root: srcDir,
857
+ cwd,
858
+ envFile: envFile === false ? void 0 : envFile
859
+ });
860
+ const result = await generateTypes({
861
+ classes: scan.classes,
862
+ routes: scan.routes,
863
+ tokens: scan.tokens,
864
+ injects: scan.injects,
865
+ collisions: scan.collisions,
866
+ env: envFile === false ? null : scan.env,
867
+ outDir,
868
+ allowDuplicates,
869
+ schemaValidator
870
+ });
871
+ const elapsed = Date.now() - start;
872
+ if (!silent) {
873
+ const where = outDir.replace(cwd + "/", "");
874
+ const collisionNote = result.resolvedCollisions > 0 ? `, ${result.resolvedCollisions} collisions namespaced` : "";
875
+ const envNote = result.envWritten ? ", env typed" : "";
876
+ console.log(` kick typegen → ${result.serviceTokens} services, ${result.routeEntries} routes, ${result.moduleTokens} modules${envNote}${collisionNote} → ${where} (${elapsed}ms)`);
877
+ }
878
+ return {
879
+ scan,
880
+ result
881
+ };
882
+ }
883
+ //#endregion
884
+ export { runTypegen };
885
+
886
+ //# sourceMappingURL=typegen-DCnJdqP1.mjs.map