theokit 0.36.1 → 0.38.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 (81) hide show
  1. package/dist/{actions-virtual-module-SYGQE6JQ.js → actions-virtual-module-H3LY5Q22.js} +3 -3
  2. package/dist/{actions-virtual-module-3KSP3WPJ.js → actions-virtual-module-WLOT2SPR.js} +3 -3
  3. package/dist/{agent-DX5LHD6X.js → agent-VQMWM5BK.js} +2 -2
  4. package/dist/{app-typed-client-N4TRX5FI.js → app-typed-client-67GBLYD6.js} +113 -24
  5. package/dist/app-typed-client-67GBLYD6.js.map +1 -0
  6. package/dist/{app-typed-client-ESMMLPYK.js → app-typed-client-SPFNGVZL.js} +113 -24
  7. package/dist/app-typed-client-SPFNGVZL.js.map +1 -0
  8. package/dist/{build-3C2KQPME.js → build-3BFIACHN.js} +6 -6
  9. package/dist/{chunk-RVJAM77M.js → chunk-2HKRQUWU.js} +2 -2
  10. package/dist/{chunk-3KD2O6DC.js → chunk-BKYFOEBK.js} +58 -54
  11. package/dist/chunk-BKYFOEBK.js.map +1 -0
  12. package/dist/{chunk-V3X5FWJA.js → chunk-EV33DOG6.js} +1 -1
  13. package/dist/{chunk-V3X5FWJA.js.map → chunk-EV33DOG6.js.map} +1 -1
  14. package/dist/{chunk-UDG6JPVP.js → chunk-GW5H4KB4.js} +2 -2
  15. package/dist/{chunk-DISADTFZ.js → chunk-IPSFSNEJ.js} +2 -2
  16. package/dist/{chunk-RPMKF6BC.js → chunk-OLZLYLOE.js} +109 -7
  17. package/dist/chunk-OLZLYLOE.js.map +1 -0
  18. package/dist/{chunk-CEHXC5NI.js → chunk-Q3C3KH3C.js} +24 -5
  19. package/dist/chunk-Q3C3KH3C.js.map +1 -0
  20. package/dist/{chunk-MNQC5XEJ.js → chunk-QCB7ZNR2.js} +94 -37
  21. package/dist/chunk-QCB7ZNR2.js.map +1 -0
  22. package/dist/{chunk-T4TCLEFB.js → chunk-QMV3R6D5.js} +91 -36
  23. package/dist/chunk-QMV3R6D5.js.map +1 -0
  24. package/dist/chunk-QNFVYST4.js +110 -0
  25. package/dist/chunk-QNFVYST4.js.map +1 -0
  26. package/dist/{chunk-WL4AX5FI.js → chunk-QWBXUZAX.js} +57 -54
  27. package/dist/chunk-QWBXUZAX.js.map +1 -0
  28. package/dist/cli/index.js +5 -5
  29. package/dist/client/core.d.ts +12 -1
  30. package/dist/client/core.js +1 -1
  31. package/dist/client/index.d.ts +7 -1
  32. package/dist/client/index.js +2 -1
  33. package/dist/client/index.js.map +1 -1
  34. package/dist/controller-swc-transform-URELVCBM.js +27 -0
  35. package/dist/controller-swc-transform-URELVCBM.js.map +1 -0
  36. package/dist/controller-swc-transform-XV3ETSE3.js +28 -0
  37. package/dist/controller-swc-transform-XV3ETSE3.js.map +1 -0
  38. package/dist/{dev-CNPT7COE.js → dev-IBCZYFZZ.js} +8 -8
  39. package/dist/{index-C3ged4mn.d.ts → index-BeAsvfPr.d.ts} +2 -2
  40. package/dist/index.js +4 -4
  41. package/dist/{internal-api-4IXBITVV.js → internal-api-MDCX7A7H.js} +10 -3
  42. package/dist/{internal-api-5JKDYRGM.js → internal-api-OLNM25HS.js} +9 -3
  43. package/dist/{mcp-INKDONQS.js → mcp-DKRODZAL.js} +2 -2
  44. package/dist/{plugin-runner-CMprWWHZ.d.ts → plugin-runner-BFEcgtmJ.d.ts} +1 -1
  45. package/dist/{plugin-types-L49QYMb5.d.ts → plugin-types-D69-lJZ9.d.ts} +9 -2
  46. package/dist/{registry-3NB7KOUI.js → registry-FKPH7XEL.js} +1 -1
  47. package/dist/server/define/index.d.ts +1 -1
  48. package/dist/server/define/index.js +1 -1
  49. package/dist/server/http/index.d.ts +3 -3
  50. package/dist/server/http/index.js +1 -1
  51. package/dist/server/index.d.ts +4 -4
  52. package/dist/server/index.js +3 -3
  53. package/dist/server/plugins/index.d.ts +3 -3
  54. package/dist/{start-LNJZNKF6.js → start-WB7GYXEW.js} +3 -3
  55. package/dist/vite-plugin/index.js +4 -4
  56. package/dist/{vite-plugin-NC5ETNMV.js → vite-plugin-5Y3LDTXI.js} +5 -5
  57. package/package.json +2 -2
  58. package/dist/app-typed-client-ESMMLPYK.js.map +0 -1
  59. package/dist/app-typed-client-N4TRX5FI.js.map +0 -1
  60. package/dist/chunk-3KD2O6DC.js.map +0 -1
  61. package/dist/chunk-CEHXC5NI.js.map +0 -1
  62. package/dist/chunk-MNQC5XEJ.js.map +0 -1
  63. package/dist/chunk-RPMKF6BC.js.map +0 -1
  64. package/dist/chunk-RSVN727G.js +0 -1
  65. package/dist/chunk-T4TCLEFB.js.map +0 -1
  66. package/dist/chunk-WL4AX5FI.js.map +0 -1
  67. package/dist/vite-plugin-NC5ETNMV.js.map +0 -1
  68. /package/dist/{actions-virtual-module-SYGQE6JQ.js.map → actions-virtual-module-H3LY5Q22.js.map} +0 -0
  69. /package/dist/{actions-virtual-module-3KSP3WPJ.js.map → actions-virtual-module-WLOT2SPR.js.map} +0 -0
  70. /package/dist/{agent-DX5LHD6X.js.map → agent-VQMWM5BK.js.map} +0 -0
  71. /package/dist/{build-3C2KQPME.js.map → build-3BFIACHN.js.map} +0 -0
  72. /package/dist/{chunk-RVJAM77M.js.map → chunk-2HKRQUWU.js.map} +0 -0
  73. /package/dist/{chunk-UDG6JPVP.js.map → chunk-GW5H4KB4.js.map} +0 -0
  74. /package/dist/{chunk-DISADTFZ.js.map → chunk-IPSFSNEJ.js.map} +0 -0
  75. /package/dist/{dev-CNPT7COE.js.map → dev-IBCZYFZZ.js.map} +0 -0
  76. /package/dist/{chunk-RSVN727G.js.map → internal-api-MDCX7A7H.js.map} +0 -0
  77. /package/dist/{internal-api-4IXBITVV.js.map → internal-api-OLNM25HS.js.map} +0 -0
  78. /package/dist/{mcp-INKDONQS.js.map → mcp-DKRODZAL.js.map} +0 -0
  79. /package/dist/{registry-3NB7KOUI.js.map → registry-FKPH7XEL.js.map} +0 -0
  80. /package/dist/{start-LNJZNKF6.js.map → start-WB7GYXEW.js.map} +0 -0
  81. /package/dist/{internal-api-5JKDYRGM.js.map → vite-plugin-5Y3LDTXI.js.map} +0 -0
@@ -1,5 +1,5 @@
1
- import "./chunk-RSVN727G.js";
2
- import "./chunk-WL4AX5FI.js";
1
+ import "./chunk-QNFVYST4.js";
2
+ import "./chunk-QWBXUZAX.js";
3
3
  import "./chunk-GY5Q27BJ.js";
4
4
  import "./chunk-5QW7IQQU.js";
5
5
  import "./chunk-WFNLNIJX.js";
@@ -221,4 +221,4 @@ export const actions = manifest
221
221
  export {
222
222
  actionsVirtualModule
223
223
  };
224
- //# sourceMappingURL=actions-virtual-module-SYGQE6JQ.js.map
224
+ //# sourceMappingURL=actions-virtual-module-H3LY5Q22.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import "tsx/esm";
3
- import "./chunk-RPMKF6BC.js";
4
- import "./chunk-3KD2O6DC.js";
3
+ import "./chunk-OLZLYLOE.js";
4
+ import "./chunk-BKYFOEBK.js";
5
5
  import "./chunk-NQRNDPZJ.js";
6
6
  import "./chunk-L4GCEMNW.js";
7
7
  import {
@@ -211,4 +211,4 @@ export const actions = manifest
211
211
  export {
212
212
  actionsVirtualModule
213
213
  };
214
- //# sourceMappingURL=actions-virtual-module-3KSP3WPJ.js.map
214
+ //# sourceMappingURL=actions-virtual-module-WLOT2SPR.js.map
@@ -3,7 +3,7 @@ import "tsx/esm";
3
3
  import {
4
4
  agentCommand,
5
5
  createAgentSsrLoader
6
- } from "./chunk-DISADTFZ.js";
6
+ } from "./chunk-IPSFSNEJ.js";
7
7
  import "./chunk-M6NMS5LR.js";
8
8
  import "./chunk-GDN3PXFH.js";
9
9
  import "./chunk-L4GCEMNW.js";
@@ -12,4 +12,4 @@ export {
12
12
  agentCommand,
13
13
  createAgentSsrLoader
14
14
  };
15
- //# sourceMappingURL=agent-DX5LHD6X.js.map
15
+ //# sourceMappingURL=agent-VQMWM5BK.js.map
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import "tsx/esm";
3
- import "./chunk-RPMKF6BC.js";
4
- import "./chunk-3KD2O6DC.js";
3
+ import {
4
+ scanControllerModules
5
+ } from "./chunk-OLZLYLOE.js";
6
+ import "./chunk-BKYFOEBK.js";
5
7
  import {
6
8
  generateManifest
7
9
  } from "./chunk-NQRNDPZJ.js";
@@ -15,6 +17,29 @@ import "./chunk-HGZL5EOI.js";
15
17
  // src/vite-plugin/app-typed-client.ts
16
18
  import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from "fs";
17
19
  import { dirname, isAbsolute, posix, relative, resolve } from "path";
20
+
21
+ // src/vite-plugin/controller-client-emit.ts
22
+ import { loadControllerWithSwc, walkControllerMetadata } from "@theokit/http";
23
+ async function collectControllerRouteData(opts) {
24
+ const loader = opts.loadModule ?? loadControllerWithSwc;
25
+ const modules = await scanControllerModules(opts.controllersDir, loader);
26
+ const out = [];
27
+ for (const { filePath, cls } of modules) {
28
+ const className = cls.name;
29
+ for (const walk of walkControllerMetadata(cls)) {
30
+ out.push({
31
+ filePath,
32
+ className,
33
+ fullPath: walk.fullPath,
34
+ verb: walk.verb,
35
+ methodName: String(walk.propertyKey)
36
+ });
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
42
+ // src/vite-plugin/app-typed-client.ts
18
43
  var FILE_HEADER = `// AUTO-GENERATED by theokit \u2014 do not edit.
19
44
  // Source: theokit/packages/theo/src/vite-plugin/app-typed-client.ts (G1 plan).
20
45
  // To refresh: \`pnpm dev\` or \`pnpm build\` regenerates this file on every route change.
@@ -22,7 +47,12 @@ var FILE_HEADER = `// AUTO-GENERATED by theokit \u2014 do not edit.
22
47
  /* eslint-disable */
23
48
  `;
24
49
  function makeNode() {
25
- return { children: /* @__PURE__ */ new Map(), methods: /* @__PURE__ */ new Map(), paramNamesAccumulated: [] };
50
+ return {
51
+ children: /* @__PURE__ */ new Map(),
52
+ methods: /* @__PURE__ */ new Map(),
53
+ controllerMethods: /* @__PURE__ */ new Map(),
54
+ paramNamesAccumulated: []
55
+ };
26
56
  }
27
57
  function normalizeSegment(raw) {
28
58
  let s = raw;
@@ -90,11 +120,38 @@ function buildOneRoute(route, ctx) {
90
120
  node.methods.set(method, alias);
91
121
  }
92
122
  }
123
+ function buildOneController(cr, classAlias, ctx) {
124
+ const verb = cr.verb.toUpperCase();
125
+ if (!HTTP_METHODS.includes(verb)) return;
126
+ const method = verb;
127
+ const segments = routePathSegments(cr.fullPath);
128
+ let node = ctx.root;
129
+ for (const seg of segments) {
130
+ const norm = normalizeSegment(seg);
131
+ let child = node.children.get(norm.identifier);
132
+ if (child === void 0) {
133
+ child = makeNode();
134
+ node.children.set(norm.identifier, child);
135
+ }
136
+ node = child;
137
+ }
138
+ node.paramNamesAccumulated = paramNamesFromRoute(cr.fullPath);
139
+ if (node.methods.has(method)) {
140
+ ctx.warnings.push(
141
+ `WARNING (#122): controller ${cr.className}.${cr.methodName} (${method} /${segments.join("/")}) collides with a file-based route at the same path; file route wins.`
142
+ );
143
+ return;
144
+ }
145
+ node.controllerMethods.set(method, { classAlias, methodName: cr.methodName });
146
+ }
93
147
  function resolveCollisions(node, pathPrefix, warnings) {
94
148
  const methodLowerNames = /* @__PURE__ */ new Set();
95
149
  for (const method of node.methods.keys()) {
96
150
  methodLowerNames.add(method.toLowerCase());
97
151
  }
152
+ for (const method of node.controllerMethods.keys()) {
153
+ methodLowerNames.add(method.toLowerCase());
154
+ }
98
155
  const renames = [];
99
156
  for (const childName of node.children.keys()) {
100
157
  if (methodLowerNames.has(childName)) {
@@ -126,6 +183,15 @@ function renderTreeNode(node, indent, _isRoot) {
126
183
  `${indent} ${lower}: (opts${optionalMark}: ${paramsField}import('theokit/client').TheoFetchOptions<typeof ${alias}>) => Promise<import('theokit/client').InferResponse<typeof ${alias}>>`
127
184
  );
128
185
  }
186
+ for (const [method, leaf] of node.controllerMethods) {
187
+ const lower = method.toLowerCase();
188
+ const paramsList = node.paramNamesAccumulated.map((p) => `${p}: string`).join("; ");
189
+ const paramsField = node.paramNamesAccumulated.length > 0 ? `{ params: { ${paramsList} } } & ` : "";
190
+ const optionalMark = node.paramNamesAccumulated.length === 0 ? "?" : "";
191
+ lines.push(
192
+ `${indent} ${lower}: (opts${optionalMark}: ${paramsField}{ body?: unknown; query?: Record<string, string> }) => Promise<Awaited<ReturnType<InstanceType<typeof ${leaf.classAlias}>['${leaf.methodName}']>>>`
193
+ );
194
+ }
129
195
  for (const [name, child] of node.children) {
130
196
  const propKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
131
197
  lines.push(`${indent} ${propKey}: ${renderTreeNode(child, indent + " ", false)}`);
@@ -136,7 +202,8 @@ function renderTreeNode(node, indent, _isRoot) {
136
202
  function generateClientDts({
137
203
  manifest,
138
204
  dtsOutPath,
139
- serverDir
205
+ serverDir,
206
+ controllerRoutes
140
207
  }) {
141
208
  const ctx = {
142
209
  root: makeNode(),
@@ -149,10 +216,23 @@ function generateClientDts({
149
216
  for (const route of manifest.routes) {
150
217
  buildOneRoute(route, ctx);
151
218
  }
219
+ if (controllerRoutes?.length) {
220
+ const classAliases = /* @__PURE__ */ new Map();
221
+ for (const cr of controllerRoutes) {
222
+ let alias = classAliases.get(cr.filePath);
223
+ if (alias === void 0) {
224
+ alias = `_c${classAliases.size}`;
225
+ classAliases.set(cr.filePath, alias);
226
+ const importPath = importPathFor(dtsOutPath, cr.filePath);
227
+ ctx.importLines.push(` import type { ${cr.className} as ${alias} } from '${importPath}'`);
228
+ }
229
+ buildOneController(cr, alias, ctx);
230
+ }
231
+ }
152
232
  resolveCollisions(ctx.root, "", ctx.warnings);
153
233
  const importsBlock = ctx.importLines.length > 0 ? ctx.importLines.join("\n") + "\n\n" : "\n";
154
234
  const warningsBlock = ctx.warnings.length > 0 ? " // " + ctx.warnings.join("\n // ") + "\n\n" : "";
155
- const treeBody = ctx.root.children.size === 0 && ctx.root.methods.size === 0 ? "{\n // No routes detected \u2014 add `server/routes/*.ts` to populate this client.\n }" : renderTreeNode(
235
+ const treeBody = ctx.root.children.size === 0 && ctx.root.methods.size === 0 && ctx.root.controllerMethods.size === 0 ? "{\n // No routes detected \u2014 add `server/routes/*.ts` to populate this client.\n }" : renderTreeNode(
156
236
  ctx.root,
157
237
  " ",
158
238
  /* isRoot */
@@ -167,6 +247,11 @@ ${importsBlock}${warningsBlock} export interface AppClient ${treeBody}
167
247
  }
168
248
  var VIRTUAL_APP_CLIENT_ID = "@theo/client";
169
249
  var RESOLVED_APP_CLIENT_ID = "\0@theo/client";
250
+ function shouldReemitForFile(file, routesGlob, controllersGlob) {
251
+ const normalized = file.replace(/\\/g, "/");
252
+ if (normalized.includes(".__decorated__.")) return false;
253
+ return normalized.startsWith(routesGlob) || normalized.startsWith(controllersGlob);
254
+ }
170
255
  function writeFileIfChanged(filePath, content) {
171
256
  if (existsSync(filePath)) {
172
257
  try {
@@ -181,7 +266,7 @@ function writeFileIfChanged(filePath, content) {
181
266
  renameSync(tmp, filePath);
182
267
  return { changed: true };
183
268
  }
184
- function emitClientDts(opts) {
269
+ async function emitClientDts(opts) {
185
270
  if (!existsSync(opts.serverDir)) {
186
271
  const stubPath = posix.join(opts.distDir.replace(/\\/g, "/"), "client.d.ts");
187
272
  return {
@@ -206,11 +291,14 @@ function emitClientDts(opts) {
206
291
  if (opts.extraRoutes?.length) {
207
292
  manifest.routes.push(...opts.extraRoutes);
208
293
  }
294
+ const controllersDir = resolve(opts.serverDir, "controllers");
295
+ const controllerRoutes = existsSync(controllersDir) ? await collectControllerRouteData({ controllersDir }) : [];
209
296
  const dtsOutPath = posix.join(opts.distDir.replace(/\\/g, "/"), "client.d.ts");
210
297
  const content = generateClientDts({
211
298
  manifest,
212
299
  dtsOutPath,
213
- serverDir: opts.serverDir
300
+ serverDir: opts.serverDir,
301
+ controllerRoutes
214
302
  });
215
303
  const { changed } = writeFileIfChanged(dtsOutPath, content);
216
304
  return { changed, path: dtsOutPath };
@@ -221,30 +309,32 @@ function appTypedClientPlugin(opts) {
221
309
  function scheduleEmit() {
222
310
  if (debounceTimer) clearTimeout(debounceTimer);
223
311
  debounceTimer = setTimeout(() => {
224
- try {
225
- emitClientDts(opts);
226
- if (viteServer) {
227
- const mod = viteServer.moduleGraph.getModuleById(RESOLVED_APP_CLIENT_ID);
228
- if (mod) viteServer.moduleGraph.invalidateModule(mod);
312
+ void (async () => {
313
+ try {
314
+ await emitClientDts(opts);
315
+ if (viteServer) {
316
+ const mod = viteServer.moduleGraph.getModuleById(RESOLVED_APP_CLIENT_ID);
317
+ if (mod) viteServer.moduleGraph.invalidateModule(mod);
318
+ }
319
+ } catch (err) {
320
+ console.error("[theokit:app-typed-client] codegen error:", err);
229
321
  }
230
- } catch (err) {
231
- console.error("[theokit:app-typed-client] codegen error:", err);
232
- }
322
+ })();
233
323
  }, 100);
234
324
  }
235
325
  return {
236
326
  name: "theokit:app-typed-client",
237
327
  enforce: "post",
238
- configResolved() {
328
+ async configResolved() {
239
329
  try {
240
- emitClientDts(opts);
330
+ await emitClientDts(opts);
241
331
  } catch (err) {
242
332
  console.error("[theokit:app-typed-client] initial codegen error:", err);
243
333
  }
244
334
  },
245
- buildEnd() {
335
+ async buildEnd() {
246
336
  try {
247
- emitClientDts(opts);
337
+ await emitClientDts(opts);
248
338
  } catch (err) {
249
339
  console.error("[theokit:app-typed-client] buildEnd codegen error:", err);
250
340
  }
@@ -254,9 +344,7 @@ function appTypedClientPlugin(opts) {
254
344
  const routesGlob = posix.join(opts.serverDir.replace(/\\/g, "/"), "routes");
255
345
  const controllersGlob = posix.join(opts.serverDir.replace(/\\/g, "/"), "controllers");
256
346
  const onFile = (file) => {
257
- const normalized = file.replace(/\\/g, "/");
258
- if (normalized.startsWith(routesGlob) || normalized.startsWith(controllersGlob))
259
- scheduleEmit();
347
+ if (shouldReemitForFile(file, routesGlob, controllersGlob)) scheduleEmit();
260
348
  };
261
349
  server.watcher.on("add", onFile);
262
350
  server.watcher.on("change", onFile);
@@ -284,6 +372,7 @@ export {
284
372
  VIRTUAL_APP_CLIENT_ID,
285
373
  appTypedClientPlugin,
286
374
  emitClientDts,
287
- generateClientDts
375
+ generateClientDts,
376
+ shouldReemitForFile
288
377
  };
289
- //# sourceMappingURL=app-typed-client-N4TRX5FI.js.map
378
+ //# sourceMappingURL=app-typed-client-67GBLYD6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite-plugin/app-typed-client.ts","../src/vite-plugin/controller-client-emit.ts"],"sourcesContent":["/* eslint-disable security/detect-non-literal-fs-filename --\n * Codegen-time fs ops: all paths derive from `opts.serverDir` / `opts.distDir`\n * (vite plugin opts — trusted, set by the framework's CLI). The plugin only\n * runs at dev/build time, never serves user HTTP input. No path traversal\n * surface here.\n */\n/**\n * Phase 2 + Phase 4 of G1 (g1-client-codegen-plan.md):\n *\n * 1. `generateClientDts(manifest, dtsOutPath)` — pure function emitting the\n * `.theokit/client.d.ts` shape (declared module `@theo/client`).\n * 2. `appTypedClientPlugin(opts)` — Vite plugin wrapper that calls (1),\n * writes the file atomically, and rewrites on route file changes.\n *\n * Implementation notes:\n * - Pure (1) is unit-testable without Vite or a filesystem (see G1-P2 tests).\n * - EC-2: kebab-case segments → camelCase; unsafe chars → bracket-access.\n * - EC-3: collision between HTTP method name and sub-segment name → method\n * wins; sub-segment ganha suffix `_` + warning comment.\n * - EC-6: write é `writeFileSync(tmp) + renameSync(tmp, final)` (POSIX atomic).\n * - EC-8: imports normalizam `\\\\` → `/`.\n */\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from 'node:fs'\nimport { dirname, isAbsolute, posix, relative, resolve } from 'node:path'\n\nimport type { Plugin, ViteDevServer } from 'vite'\n\nimport { HTTP_METHODS, type HttpMethod } from '../core/contracts/http-methods.js'\nimport { generateManifest, type ManifestRoute, type TheoManifest } from '../server/internal-api.js'\n\nimport { collectControllerRouteData, type ControllerRouteData } from './controller-client-emit.js'\n\nconst FILE_HEADER = `// AUTO-GENERATED by theokit — do not edit.\n// Source: theokit/packages/theo/src/vite-plugin/app-typed-client.ts (G1 plan).\n// To refresh: \\`pnpm dev\\` or \\`pnpm build\\` regenerates this file on every route change.\n\n/* eslint-disable */\n`\n\ninterface ControllerLeaf {\n /** Import alias of the controller class (`_c0`). */\n classAlias: string\n /** Controller method name backing the route (`findById`). */\n methodName: string\n}\n\ninterface TreeNode {\n /** sub-namespaces — keyed by segment name (already normalized). */\n children: Map<string, TreeNode>\n /** File-based routes: HTTP method → import alias (`InferResponse<typeof alias>`). */\n methods: Map<HttpMethod, string>\n /** Decorator controllers: HTTP method → class alias + method (`Awaited<ReturnType<...>>`). */\n controllerMethods: Map<HttpMethod, ControllerLeaf>\n /** Whether any descendant declares dynamic params (for params type construction). */\n paramNamesAccumulated: string[]\n}\n\nfunction makeNode(): TreeNode {\n return {\n children: new Map(),\n methods: new Map(),\n controllerMethods: new Map(),\n paramNamesAccumulated: [],\n }\n}\n\n/**\n * Normalize a path segment to a valid TS identifier when possible.\n * EC-2: kebab-case → camelCase; pure dynamic (`:name`) → identifier `name`;\n * starts with digit or unsafe char → emit as bracket-access key.\n */\nfunction normalizeSegment(raw: string): { identifier: string; bracket: boolean } {\n // Dynamic segment: leading `:` (e.g. `:id` or `:...slug`).\n let s = raw\n if (s.startsWith(':...')) s = s.slice(4)\n else if (s.startsWith(':')) s = s.slice(1)\n\n // kebab → camel\n const camel = s.replace(/-([a-z0-9])/gi, (_, c: string) => c.toUpperCase())\n\n // Valid TS identifier (cannot start with digit, must be [a-zA-Z_$][a-zA-Z0-9_$]*)\n if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {\n return { identifier: camel, bracket: false }\n }\n // Fallback: bracket-access (keep raw form intact for JSON-string key)\n return { identifier: s, bracket: true }\n}\n\nfunction importPathFor(dtsOutPath: string, routeFileAbs: string): string {\n let rel = relative(dirname(dtsOutPath), routeFileAbs)\n // EC-8: normalize Windows path separators.\n rel = rel.replace(/\\\\/g, '/')\n // Strip extension (`.ts` / `.tsx`); TS module resolution likes extensionless.\n rel = rel.replace(/\\.[jt]sx?$/, '')\n if (!rel.startsWith('.') && !rel.startsWith('/')) {\n rel = './' + rel\n }\n return rel\n}\n\n/**\n * Convert a route path (e.g. `/api/users/:id/posts`) into the tree segments\n * we want under the `client` root. Strips the `/api/` prefix and splits.\n */\nfunction routePathSegments(routePath: string): string[] {\n let p = routePath\n if (p.startsWith('/api/')) p = p.slice(5)\n else if (p.startsWith('/api')) p = p.slice(4)\n if (p.startsWith('/')) p = p.slice(1)\n if (p === '') return []\n return p.split('/').filter(Boolean)\n}\n\nfunction paramNamesFromRoute(routePath: string): string[] {\n const out: string[] = []\n const re = /:(?:\\.\\.\\.)?([^/]+)/g\n let m: RegExpExecArray | null\n while ((m = re.exec(routePath)) !== null) {\n out.push(m[1])\n }\n return out\n}\n\ninterface BuildContext {\n root: TreeNode\n /** Sequential index per route file for aliasing imports. */\n aliasCounter: { value: number }\n /** Lines emitted at the top of the declared module (one per route file). */\n importLines: string[]\n /** Diagnostic warnings rendered as block comments inside the declared module. */\n warnings: string[]\n dtsOutPath: string\n serverDir: string\n}\n\nfunction buildOneRoute(route: ManifestRoute, ctx: BuildContext): void {\n if (!route.methods || route.methods.length === 0) return\n // Build alias prefix once per route file (`_r0`, `_r1`, ...).\n const idx = ctx.aliasCounter.value++\n // Resolve absolute path for the route file (manifest stores it relative to serverDir).\n const absRouteFile = isAbsolute(route.filePath)\n ? route.filePath\n : resolve(ctx.serverDir, route.filePath)\n const importPath = importPathFor(ctx.dtsOutPath, absRouteFile)\n const aliases = route.methods\n .filter((m): m is HttpMethod => (HTTP_METHODS as readonly string[]).includes(m))\n .map((method) => ({ method, alias: `_r${idx}_${method}` }))\n if (aliases.length === 0) return\n const importClause = aliases.map((a) => `${a.method} as ${a.alias}`).join(', ')\n ctx.importLines.push(` import type { ${importClause} } from '${importPath}'`)\n\n // Walk segments + register methods at the leaf node.\n const segments = routePathSegments(route.routePath)\n let node = ctx.root\n for (const seg of segments) {\n const norm = normalizeSegment(seg)\n let child = node.children.get(norm.identifier)\n if (child === undefined) {\n child = makeNode()\n node.children.set(norm.identifier, child)\n }\n node = child\n }\n // Accumulated param names for typed params (e.g. `{ id: string }`).\n node.paramNamesAccumulated = paramNamesFromRoute(route.routePath)\n\n // Register each HTTP method (collision detection runs in a post-pass).\n for (const { method, alias } of aliases) {\n if (node.methods.has(method)) {\n ctx.warnings.push(\n `// WARNING: duplicate ${method} export at /${segments.join('/')} (route file declared twice). Keeping last definition.`,\n )\n }\n node.methods.set(method, alias)\n }\n}\n\n/**\n * #122 (T2.1) — register one decorator-controller route into the tree. The class\n * import alias is shared across the class's methods (resolved by the caller). A\n * file-based route at the same node+verb WINS (file routes are the default); the\n * controller entry is skipped with a warning to keep the emitted `.d.ts` valid.\n */\nfunction buildOneController(cr: ControllerRouteData, classAlias: string, ctx: BuildContext): void {\n const verb = cr.verb.toUpperCase()\n if (!(HTTP_METHODS as readonly string[]).includes(verb)) return\n const method = verb as HttpMethod\n\n const segments = routePathSegments(cr.fullPath)\n let node = ctx.root\n for (const seg of segments) {\n const norm = normalizeSegment(seg)\n let child = node.children.get(norm.identifier)\n if (child === undefined) {\n child = makeNode()\n node.children.set(norm.identifier, child)\n }\n node = child\n }\n node.paramNamesAccumulated = paramNamesFromRoute(cr.fullPath)\n\n if (node.methods.has(method)) {\n ctx.warnings.push(\n `WARNING (#122): controller ${cr.className}.${cr.methodName} (${method} /${segments.join('/')}) collides with a file-based route at the same path; file route wins.`,\n )\n return\n }\n node.controllerMethods.set(method, { classAlias, methodName: cr.methodName })\n}\n\n/**\n * EC-3: walk the tree post-build and resolve any node where a method's\n * lowercase name clashes with a child segment. HTTP method wins (more common);\n * the offending child gets a `_` suffix + a warning entry.\n */\nfunction resolveCollisions(node: TreeNode, pathPrefix: string, warnings: string[]): void {\n const methodLowerNames = new Set<string>()\n for (const method of node.methods.keys()) {\n methodLowerNames.add(method.toLowerCase())\n }\n for (const method of node.controllerMethods.keys()) {\n methodLowerNames.add(method.toLowerCase())\n }\n const renames: { from: string; to: string }[] = []\n for (const childName of node.children.keys()) {\n if (methodLowerNames.has(childName)) {\n renames.push({ from: childName, to: `${childName}_` })\n }\n }\n for (const { from, to } of renames) {\n const old = node.children.get(from)\n if (old !== undefined) {\n node.children.delete(from)\n node.children.set(to, old)\n warnings.push(\n `WARNING (EC-3): path collision — method '${from}' clashed with sub-namespace at ${pathPrefix}/${from}; sub-namespace renamed to '${to}'.`,\n )\n }\n }\n // Recurse into all children (including the renamed ones).\n for (const [name, child] of node.children) {\n resolveCollisions(child, `${pathPrefix}/${name}`, warnings)\n }\n}\n\nfunction renderTreeNode(node: TreeNode, indent: string, _isRoot: boolean): string {\n // _isRoot is reserved for callers; current rendering is uniform across roots\n // and children (both open with `{`). Kept in the signature for future overrides.\n const lines: string[] = ['{']\n // Methods first\n for (const [method, alias] of node.methods) {\n const lower = method.toLowerCase()\n // Routes with path params (`:id`, `:name`) emit a `{ params: {...} }` object\n // intersected with TheoFetchOptions. Without the outer `{...}` wrap the\n // generated signature `(opts: params: {...} & ...)` is invalid TS syntax\n // (parser reads `params:` as a parameter label, then `{...}` as type — invalid).\n const paramsList = node.paramNamesAccumulated.map((p) => `${p}: string`).join('; ')\n const paramsField =\n node.paramNamesAccumulated.length > 0 ? `{ params: { ${paramsList} } } & ` : ''\n const optionalMark = node.paramNamesAccumulated.length === 0 ? '?' : ''\n lines.push(\n `${indent} ${lower}: (opts${optionalMark}: ${paramsField}import('theokit/client').TheoFetchOptions<typeof ${alias}>) => Promise<import('theokit/client').InferResponse<typeof ${alias}>>`,\n )\n }\n // #122 (T2.1) — decorator-controller methods. Response inferred via\n // `Awaited<ReturnType<...>>`; body is `unknown` (ADR-2 checkpoint — @Body types\n // are not recoverable from the class type). params typed from the route pattern.\n for (const [method, leaf] of node.controllerMethods) {\n const lower = method.toLowerCase()\n const paramsList = node.paramNamesAccumulated.map((p) => `${p}: string`).join('; ')\n const paramsField =\n node.paramNamesAccumulated.length > 0 ? `{ params: { ${paramsList} } } & ` : ''\n const optionalMark = node.paramNamesAccumulated.length === 0 ? '?' : ''\n lines.push(\n `${indent} ${lower}: (opts${optionalMark}: ${paramsField}{ body?: unknown; query?: Record<string, string> }) => Promise<Awaited<ReturnType<InstanceType<typeof ${leaf.classAlias}>['${leaf.methodName}']>>>`,\n )\n }\n // Children\n for (const [name, child] of node.children) {\n const propKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)\n ? name\n : `'${name.replace(/'/g, \"\\\\'\")}'`\n lines.push(`${indent} ${propKey}: ${renderTreeNode(child, indent + ' ', false)}`)\n }\n lines.push(`${indent}}`)\n return lines.join('\\n')\n}\n\nexport interface GenerateClientDtsOptions {\n manifest: TheoManifest\n /** Absolute path to the .d.ts that will be emitted. Used to compute import paths. */\n dtsOutPath: string\n /** Absolute serverDir; manifest's filePath entries are relative to this. */\n serverDir: string\n /**\n * #122 (T2.1) — decorator-controller routes (from `collectControllerRouteData`),\n * merged into the same `client.<ns>.<method>()` tree as file routes. Absent/empty\n * → zero controller output (ADR-5 routes-only byte-identity).\n */\n controllerRoutes?: ControllerRouteData[]\n}\n\nexport function generateClientDts({\n manifest,\n dtsOutPath,\n serverDir,\n controllerRoutes,\n}: GenerateClientDtsOptions): string {\n const ctx: BuildContext = {\n root: makeNode(),\n aliasCounter: { value: 0 },\n importLines: [],\n warnings: [],\n dtsOutPath,\n serverDir,\n }\n for (const route of manifest.routes) {\n buildOneRoute(route, ctx)\n }\n // #122 (T2.1) — merge decorator controllers into the same tree. One import\n // alias per class (shared across its methods), keyed by source file.\n if (controllerRoutes?.length) {\n const classAliases = new Map<string, string>()\n for (const cr of controllerRoutes) {\n let alias = classAliases.get(cr.filePath)\n if (alias === undefined) {\n alias = `_c${classAliases.size}`\n classAliases.set(cr.filePath, alias)\n const importPath = importPathFor(dtsOutPath, cr.filePath)\n ctx.importLines.push(` import type { ${cr.className} as ${alias} } from '${importPath}'`)\n }\n buildOneController(cr, alias, ctx)\n }\n }\n // EC-3: resolve collisions in a post-build pass (collisions materialize\n // when two routes are both processed, not during a single route's build).\n resolveCollisions(ctx.root, '', ctx.warnings)\n\n const importsBlock = ctx.importLines.length > 0 ? ctx.importLines.join('\\n') + '\\n\\n' : '\\n'\n const warningsBlock =\n ctx.warnings.length > 0 ? ' // ' + ctx.warnings.join('\\n // ') + '\\n\\n' : ''\n\n // Empty manifest (no routes with methods) → emit a stub interface so consumers\n // still get `client.X` autocomplete-friendly errors instead of \"Module not found\".\n const treeBody =\n ctx.root.children.size === 0 &&\n ctx.root.methods.size === 0 &&\n ctx.root.controllerMethods.size === 0\n ? '{\\n // No routes detected — add `server/routes/*.ts` to populate this client.\\n }'\n : renderTreeNode(ctx.root, ' ', /* isRoot */ true)\n\n return `${FILE_HEADER}\ndeclare module '@theo/client' {\n${importsBlock}${warningsBlock} export interface AppClient ${treeBody}\n export const client: AppClient\n}\n`\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Phase 4 — Vite plugin\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AppTypedClientPluginOptions {\n /** Project root (cwd). */\n cwd: string\n /** Absolute path to the user's `server/` dir. */\n serverDir: string\n /** Absolute path to the `.theokit/` output dir (where `client.d.ts` lands). */\n distDir: string\n /**\n * Additional routes to merge into the typed client alongside file-system routes.\n * Each entry follows ManifestRoute shape (filePath, routePath, methods, paramNames).\n *\n * Used by `@theokit/http` `httpDecoratorsPlugin` to inject\n * decorator-defined routes — zero coupling between theokit core and the\n * decorator package (the walk happens in http-decorators; only data arrives here).\n */\n extraRoutes?: ManifestRoute[]\n}\n\nconst VIRTUAL_APP_CLIENT_ID = '@theo/client'\nconst RESOLVED_APP_CLIENT_ID = '\\0@theo/client'\n\n/**\n * Should a watcher file event under `routes/` or `controllers/` trigger a client\n * re-emit? Ignores the `.__decorated__.mjs` temp files that `loadControllerWithSwc`\n * writes+unlinks during controller codegen — reacting to them would create a\n * perpetual emit→temp-write→watcher→emit loop (#122 T2.1).\n */\nexport function shouldReemitForFile(\n file: string,\n routesGlob: string,\n controllersGlob: string,\n): boolean {\n const normalized = file.replace(/\\\\/g, '/')\n if (normalized.includes('.__decorated__.')) return false\n return normalized.startsWith(routesGlob) || normalized.startsWith(controllersGlob)\n}\n\n/** Atomic write via tmp + rename (EC-6). Skips when content is unchanged. */\nfunction writeFileIfChanged(filePath: string, content: string): { changed: boolean } {\n if (existsSync(filePath)) {\n try {\n const current = readFileSync(filePath, 'utf-8')\n if (current === content) return { changed: false }\n } catch {\n // fall through to write\n }\n }\n mkdirSync(dirname(filePath), { recursive: true })\n const tmp = `${filePath}.${process.pid}.tmp`\n writeFileSync(tmp, content)\n renameSync(tmp, filePath)\n return { changed: true }\n}\n\nasync function emitClientDts(\n opts: AppTypedClientPluginOptions,\n): Promise<{ changed: boolean; path: string }> {\n if (!existsSync(opts.serverDir)) {\n // No server dir — emit empty stub to avoid \"module not found\".\n const stubPath = posix.join(opts.distDir.replace(/\\\\/g, '/'), 'client.d.ts')\n return {\n changed: writeFileIfChanged(\n stubPath,\n generateClientDts({\n manifest: {\n version: 1,\n generatedAt: new Date().toISOString(),\n routes: [],\n actions: [],\n websockets: [],\n },\n dtsOutPath: stubPath,\n serverDir: opts.serverDir,\n }),\n ).changed,\n path: stubPath,\n }\n }\n const manifest = generateManifest(opts.serverDir)\n\n // Decorator-client-bridge: merge extra routes (from @theokit/http\n // or any other source) into the manifest so generateClientDts() produces a\n // SINGLE typed client covering both file-system routes AND injected routes.\n if (opts.extraRoutes?.length) {\n manifest.routes.push(...opts.extraRoutes)\n }\n\n // #122 (T2.1) — merge decorator controllers under `<serverDir>/controllers/`\n // into the same typed client. Zero output when the dir is absent/empty (ADR-5).\n const controllersDir = resolve(opts.serverDir, 'controllers')\n const controllerRoutes = existsSync(controllersDir)\n ? await collectControllerRouteData({ controllersDir })\n : []\n\n const dtsOutPath = posix.join(opts.distDir.replace(/\\\\/g, '/'), 'client.d.ts')\n const content = generateClientDts({\n manifest,\n dtsOutPath,\n serverDir: opts.serverDir,\n controllerRoutes,\n })\n const { changed } = writeFileIfChanged(dtsOutPath, content)\n return { changed, path: dtsOutPath }\n}\n\nexport function appTypedClientPlugin(opts: AppTypedClientPluginOptions): Plugin {\n let debounceTimer: NodeJS.Timeout | undefined\n let viteServer: ViteDevServer | undefined\n\n function scheduleEmit(): void {\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n void (async () => {\n try {\n await emitClientDts(opts)\n // Invalidate the virtual module so re-imports pick up potential new shape.\n if (viteServer) {\n const mod = viteServer.moduleGraph.getModuleById(RESOLVED_APP_CLIENT_ID)\n if (mod) viteServer.moduleGraph.invalidateModule(mod)\n }\n } catch (err) {\n // Surface but don't crash dev server.\n console.error('[theokit:app-typed-client] codegen error:', err)\n }\n })()\n }, 100)\n }\n\n return {\n name: 'theokit:app-typed-client',\n enforce: 'post',\n async configResolved() {\n try {\n await emitClientDts(opts)\n } catch (err) {\n console.error('[theokit:app-typed-client] initial codegen error:', err)\n }\n },\n async buildEnd() {\n try {\n await emitClientDts(opts)\n } catch (err) {\n console.error('[theokit:app-typed-client] buildEnd codegen error:', err)\n }\n },\n configureServer(server) {\n viteServer = server\n const routesGlob = posix.join(opts.serverDir.replace(/\\\\/g, '/'), 'routes')\n const controllersGlob = posix.join(opts.serverDir.replace(/\\\\/g, '/'), 'controllers')\n const onFile = (file: string): void => {\n if (shouldReemitForFile(file, routesGlob, controllersGlob)) scheduleEmit()\n }\n server.watcher.on('add', onFile)\n server.watcher.on('change', onFile)\n server.watcher.on('unlink', onFile)\n },\n resolveId(id) {\n if (id === VIRTUAL_APP_CLIENT_ID) return RESOLVED_APP_CLIENT_ID\n return null\n },\n load(id) {\n if (id === RESOLVED_APP_CLIENT_ID) {\n return [\n `import { createAppClient } from 'theokit/client'`,\n `export const client = createAppClient()`,\n `export default client`,\n ``,\n ].join('\\n')\n }\n return null\n },\n }\n}\n\nexport { VIRTUAL_APP_CLIENT_ID, RESOLVED_APP_CLIENT_ID, emitClientDts }\n","/**\n * #122 (T2.1) — typed-client codegen for decorator controllers.\n *\n * Collects the routing metadata of every `@Controller` under `controllersDir`\n * so the app-typed-client codegen can emit `client.<ns>.<method>()` entries.\n * Codegen loads controllers via `loadControllerWithSwc` (swc-compile + import,\n * no Vite dev server needed) — distinct from the dev SERVING path, which loads\n * through Vite's `ssrLoadModule`.\n *\n * ADR-2 checkpoint (LOCKED — see the plan's Unresolved Questions): only the\n * RESPONSE type is inferred, via `Awaited<ReturnType<InstanceType<typeof C>['m']>>`.\n * Request `@Body` types are NOT inferred from the class — parameter decorators are\n * erased runtime metadata, invisible to the type system, so `Parameters<...>[N]` is\n * positional-only and cannot tell body from param from query. Body falls back to\n * `unknown` (runtime `@Body` Zod validation from Phase 1 still applies). Full body\n * inference is tracked in the follow-up issue.\n */\nimport { loadControllerWithSwc, walkControllerMetadata } from '@theokit/http'\n\nimport {\n scanControllerModules,\n type ControllerModuleLoader,\n} from '../server/http/controller-dispatch.js'\n\n/** One controller route, flattened for the client codegen. */\nexport interface ControllerRouteData {\n /** Absolute path to the `*.controller.ts` source (for the `import type` line). */\n filePath: string\n /** The exported class name (e.g. `TasksController`). */\n className: string\n /** Full route path, e.g. `/api/v2/tasks/:id`. */\n fullPath: string\n /** HTTP verb (`GET`, `POST`, …). */\n verb: string\n /** The controller method name backing this route (e.g. `findById`). */\n methodName: string\n}\n\n/**\n * Scan `controllersDir` and flatten every controller's routes into\n * {@link ControllerRouteData}. Returns `[]` when the directory has no controllers\n * (so the codegen contributes zero bytes — ADR-5 routes-only byte-identity).\n */\nexport async function collectControllerRouteData(opts: {\n controllersDir: string\n loadModule?: ControllerModuleLoader\n}): Promise<ControllerRouteData[]> {\n const loader = opts.loadModule ?? loadControllerWithSwc\n const modules = await scanControllerModules(opts.controllersDir, loader)\n const out: ControllerRouteData[] = []\n for (const { filePath, cls } of modules) {\n const className = (cls as { name: string }).name\n for (const walk of walkControllerMetadata(cls)) {\n out.push({\n filePath,\n className,\n fullPath: walk.fullPath,\n verb: walk.verb,\n methodName: String(walk.propertyKey),\n })\n }\n }\n return out\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,7 @@
1
- import "./chunk-RSVN727G.js";
2
- import "./chunk-WL4AX5FI.js";
1
+ import {
2
+ scanControllerModules
3
+ } from "./chunk-QNFVYST4.js";
4
+ import "./chunk-QWBXUZAX.js";
3
5
  import "./chunk-GY5Q27BJ.js";
4
6
  import "./chunk-5QW7IQQU.js";
5
7
  import "./chunk-WFNLNIJX.js";
@@ -25,6 +27,29 @@ import "./chunk-DGUM43GV.js";
25
27
  // src/vite-plugin/app-typed-client.ts
26
28
  import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from "fs";
27
29
  import { dirname, isAbsolute, posix, relative, resolve } from "path";
30
+
31
+ // src/vite-plugin/controller-client-emit.ts
32
+ import { loadControllerWithSwc, walkControllerMetadata } from "@theokit/http";
33
+ async function collectControllerRouteData(opts) {
34
+ const loader = opts.loadModule ?? loadControllerWithSwc;
35
+ const modules = await scanControllerModules(opts.controllersDir, loader);
36
+ const out = [];
37
+ for (const { filePath, cls } of modules) {
38
+ const className = cls.name;
39
+ for (const walk of walkControllerMetadata(cls)) {
40
+ out.push({
41
+ filePath,
42
+ className,
43
+ fullPath: walk.fullPath,
44
+ verb: walk.verb,
45
+ methodName: String(walk.propertyKey)
46
+ });
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+
52
+ // src/vite-plugin/app-typed-client.ts
28
53
  var FILE_HEADER = `// AUTO-GENERATED by theokit \u2014 do not edit.
29
54
  // Source: theokit/packages/theo/src/vite-plugin/app-typed-client.ts (G1 plan).
30
55
  // To refresh: \`pnpm dev\` or \`pnpm build\` regenerates this file on every route change.
@@ -32,7 +57,12 @@ var FILE_HEADER = `// AUTO-GENERATED by theokit \u2014 do not edit.
32
57
  /* eslint-disable */
33
58
  `;
34
59
  function makeNode() {
35
- return { children: /* @__PURE__ */ new Map(), methods: /* @__PURE__ */ new Map(), paramNamesAccumulated: [] };
60
+ return {
61
+ children: /* @__PURE__ */ new Map(),
62
+ methods: /* @__PURE__ */ new Map(),
63
+ controllerMethods: /* @__PURE__ */ new Map(),
64
+ paramNamesAccumulated: []
65
+ };
36
66
  }
37
67
  function normalizeSegment(raw) {
38
68
  let s = raw;
@@ -100,11 +130,38 @@ function buildOneRoute(route, ctx) {
100
130
  node.methods.set(method, alias);
101
131
  }
102
132
  }
133
+ function buildOneController(cr, classAlias, ctx) {
134
+ const verb = cr.verb.toUpperCase();
135
+ if (!HTTP_METHODS.includes(verb)) return;
136
+ const method = verb;
137
+ const segments = routePathSegments(cr.fullPath);
138
+ let node = ctx.root;
139
+ for (const seg of segments) {
140
+ const norm = normalizeSegment(seg);
141
+ let child = node.children.get(norm.identifier);
142
+ if (child === void 0) {
143
+ child = makeNode();
144
+ node.children.set(norm.identifier, child);
145
+ }
146
+ node = child;
147
+ }
148
+ node.paramNamesAccumulated = paramNamesFromRoute(cr.fullPath);
149
+ if (node.methods.has(method)) {
150
+ ctx.warnings.push(
151
+ `WARNING (#122): controller ${cr.className}.${cr.methodName} (${method} /${segments.join("/")}) collides with a file-based route at the same path; file route wins.`
152
+ );
153
+ return;
154
+ }
155
+ node.controllerMethods.set(method, { classAlias, methodName: cr.methodName });
156
+ }
103
157
  function resolveCollisions(node, pathPrefix, warnings) {
104
158
  const methodLowerNames = /* @__PURE__ */ new Set();
105
159
  for (const method of node.methods.keys()) {
106
160
  methodLowerNames.add(method.toLowerCase());
107
161
  }
162
+ for (const method of node.controllerMethods.keys()) {
163
+ methodLowerNames.add(method.toLowerCase());
164
+ }
108
165
  const renames = [];
109
166
  for (const childName of node.children.keys()) {
110
167
  if (methodLowerNames.has(childName)) {
@@ -136,6 +193,15 @@ function renderTreeNode(node, indent, _isRoot) {
136
193
  `${indent} ${lower}: (opts${optionalMark}: ${paramsField}import('theokit/client').TheoFetchOptions<typeof ${alias}>) => Promise<import('theokit/client').InferResponse<typeof ${alias}>>`
137
194
  );
138
195
  }
196
+ for (const [method, leaf] of node.controllerMethods) {
197
+ const lower = method.toLowerCase();
198
+ const paramsList = node.paramNamesAccumulated.map((p) => `${p}: string`).join("; ");
199
+ const paramsField = node.paramNamesAccumulated.length > 0 ? `{ params: { ${paramsList} } } & ` : "";
200
+ const optionalMark = node.paramNamesAccumulated.length === 0 ? "?" : "";
201
+ lines.push(
202
+ `${indent} ${lower}: (opts${optionalMark}: ${paramsField}{ body?: unknown; query?: Record<string, string> }) => Promise<Awaited<ReturnType<InstanceType<typeof ${leaf.classAlias}>['${leaf.methodName}']>>>`
203
+ );
204
+ }
139
205
  for (const [name, child] of node.children) {
140
206
  const propKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
141
207
  lines.push(`${indent} ${propKey}: ${renderTreeNode(child, indent + " ", false)}`);
@@ -146,7 +212,8 @@ function renderTreeNode(node, indent, _isRoot) {
146
212
  function generateClientDts({
147
213
  manifest,
148
214
  dtsOutPath,
149
- serverDir
215
+ serverDir,
216
+ controllerRoutes
150
217
  }) {
151
218
  const ctx = {
152
219
  root: makeNode(),
@@ -159,10 +226,23 @@ function generateClientDts({
159
226
  for (const route of manifest.routes) {
160
227
  buildOneRoute(route, ctx);
161
228
  }
229
+ if (controllerRoutes?.length) {
230
+ const classAliases = /* @__PURE__ */ new Map();
231
+ for (const cr of controllerRoutes) {
232
+ let alias = classAliases.get(cr.filePath);
233
+ if (alias === void 0) {
234
+ alias = `_c${classAliases.size}`;
235
+ classAliases.set(cr.filePath, alias);
236
+ const importPath = importPathFor(dtsOutPath, cr.filePath);
237
+ ctx.importLines.push(` import type { ${cr.className} as ${alias} } from '${importPath}'`);
238
+ }
239
+ buildOneController(cr, alias, ctx);
240
+ }
241
+ }
162
242
  resolveCollisions(ctx.root, "", ctx.warnings);
163
243
  const importsBlock = ctx.importLines.length > 0 ? ctx.importLines.join("\n") + "\n\n" : "\n";
164
244
  const warningsBlock = ctx.warnings.length > 0 ? " // " + ctx.warnings.join("\n // ") + "\n\n" : "";
165
- const treeBody = ctx.root.children.size === 0 && ctx.root.methods.size === 0 ? "{\n // No routes detected \u2014 add `server/routes/*.ts` to populate this client.\n }" : renderTreeNode(
245
+ const treeBody = ctx.root.children.size === 0 && ctx.root.methods.size === 0 && ctx.root.controllerMethods.size === 0 ? "{\n // No routes detected \u2014 add `server/routes/*.ts` to populate this client.\n }" : renderTreeNode(
166
246
  ctx.root,
167
247
  " ",
168
248
  /* isRoot */
@@ -177,6 +257,11 @@ ${importsBlock}${warningsBlock} export interface AppClient ${treeBody}
177
257
  }
178
258
  var VIRTUAL_APP_CLIENT_ID = "@theo/client";
179
259
  var RESOLVED_APP_CLIENT_ID = "\0@theo/client";
260
+ function shouldReemitForFile(file, routesGlob, controllersGlob) {
261
+ const normalized = file.replace(/\\/g, "/");
262
+ if (normalized.includes(".__decorated__.")) return false;
263
+ return normalized.startsWith(routesGlob) || normalized.startsWith(controllersGlob);
264
+ }
180
265
  function writeFileIfChanged(filePath, content) {
181
266
  if (existsSync(filePath)) {
182
267
  try {
@@ -191,7 +276,7 @@ function writeFileIfChanged(filePath, content) {
191
276
  renameSync(tmp, filePath);
192
277
  return { changed: true };
193
278
  }
194
- function emitClientDts(opts) {
279
+ async function emitClientDts(opts) {
195
280
  if (!existsSync(opts.serverDir)) {
196
281
  const stubPath = posix.join(opts.distDir.replace(/\\/g, "/"), "client.d.ts");
197
282
  return {
@@ -216,11 +301,14 @@ function emitClientDts(opts) {
216
301
  if (opts.extraRoutes?.length) {
217
302
  manifest.routes.push(...opts.extraRoutes);
218
303
  }
304
+ const controllersDir = resolve(opts.serverDir, "controllers");
305
+ const controllerRoutes = existsSync(controllersDir) ? await collectControllerRouteData({ controllersDir }) : [];
219
306
  const dtsOutPath = posix.join(opts.distDir.replace(/\\/g, "/"), "client.d.ts");
220
307
  const content = generateClientDts({
221
308
  manifest,
222
309
  dtsOutPath,
223
- serverDir: opts.serverDir
310
+ serverDir: opts.serverDir,
311
+ controllerRoutes
224
312
  });
225
313
  const { changed } = writeFileIfChanged(dtsOutPath, content);
226
314
  return { changed, path: dtsOutPath };
@@ -231,30 +319,32 @@ function appTypedClientPlugin(opts) {
231
319
  function scheduleEmit() {
232
320
  if (debounceTimer) clearTimeout(debounceTimer);
233
321
  debounceTimer = setTimeout(() => {
234
- try {
235
- emitClientDts(opts);
236
- if (viteServer) {
237
- const mod = viteServer.moduleGraph.getModuleById(RESOLVED_APP_CLIENT_ID);
238
- if (mod) viteServer.moduleGraph.invalidateModule(mod);
322
+ void (async () => {
323
+ try {
324
+ await emitClientDts(opts);
325
+ if (viteServer) {
326
+ const mod = viteServer.moduleGraph.getModuleById(RESOLVED_APP_CLIENT_ID);
327
+ if (mod) viteServer.moduleGraph.invalidateModule(mod);
328
+ }
329
+ } catch (err) {
330
+ console.error("[theokit:app-typed-client] codegen error:", err);
239
331
  }
240
- } catch (err) {
241
- console.error("[theokit:app-typed-client] codegen error:", err);
242
- }
332
+ })();
243
333
  }, 100);
244
334
  }
245
335
  return {
246
336
  name: "theokit:app-typed-client",
247
337
  enforce: "post",
248
- configResolved() {
338
+ async configResolved() {
249
339
  try {
250
- emitClientDts(opts);
340
+ await emitClientDts(opts);
251
341
  } catch (err) {
252
342
  console.error("[theokit:app-typed-client] initial codegen error:", err);
253
343
  }
254
344
  },
255
- buildEnd() {
345
+ async buildEnd() {
256
346
  try {
257
- emitClientDts(opts);
347
+ await emitClientDts(opts);
258
348
  } catch (err) {
259
349
  console.error("[theokit:app-typed-client] buildEnd codegen error:", err);
260
350
  }
@@ -264,9 +354,7 @@ function appTypedClientPlugin(opts) {
264
354
  const routesGlob = posix.join(opts.serverDir.replace(/\\/g, "/"), "routes");
265
355
  const controllersGlob = posix.join(opts.serverDir.replace(/\\/g, "/"), "controllers");
266
356
  const onFile = (file) => {
267
- const normalized = file.replace(/\\/g, "/");
268
- if (normalized.startsWith(routesGlob) || normalized.startsWith(controllersGlob))
269
- scheduleEmit();
357
+ if (shouldReemitForFile(file, routesGlob, controllersGlob)) scheduleEmit();
270
358
  };
271
359
  server.watcher.on("add", onFile);
272
360
  server.watcher.on("change", onFile);
@@ -294,6 +382,7 @@ export {
294
382
  VIRTUAL_APP_CLIENT_ID,
295
383
  appTypedClientPlugin,
296
384
  emitClientDts,
297
- generateClientDts
385
+ generateClientDts,
386
+ shouldReemitForFile
298
387
  };
299
- //# sourceMappingURL=app-typed-client-ESMMLPYK.js.map
388
+ //# sourceMappingURL=app-typed-client-SPFNGVZL.js.map