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.
- package/dist/{actions-virtual-module-SYGQE6JQ.js → actions-virtual-module-H3LY5Q22.js} +3 -3
- package/dist/{actions-virtual-module-3KSP3WPJ.js → actions-virtual-module-WLOT2SPR.js} +3 -3
- package/dist/{agent-DX5LHD6X.js → agent-VQMWM5BK.js} +2 -2
- package/dist/{app-typed-client-N4TRX5FI.js → app-typed-client-67GBLYD6.js} +113 -24
- package/dist/app-typed-client-67GBLYD6.js.map +1 -0
- package/dist/{app-typed-client-ESMMLPYK.js → app-typed-client-SPFNGVZL.js} +113 -24
- package/dist/app-typed-client-SPFNGVZL.js.map +1 -0
- package/dist/{build-3C2KQPME.js → build-3BFIACHN.js} +6 -6
- package/dist/{chunk-RVJAM77M.js → chunk-2HKRQUWU.js} +2 -2
- package/dist/{chunk-3KD2O6DC.js → chunk-BKYFOEBK.js} +58 -54
- package/dist/chunk-BKYFOEBK.js.map +1 -0
- package/dist/{chunk-V3X5FWJA.js → chunk-EV33DOG6.js} +1 -1
- package/dist/{chunk-V3X5FWJA.js.map → chunk-EV33DOG6.js.map} +1 -1
- package/dist/{chunk-UDG6JPVP.js → chunk-GW5H4KB4.js} +2 -2
- package/dist/{chunk-DISADTFZ.js → chunk-IPSFSNEJ.js} +2 -2
- package/dist/{chunk-RPMKF6BC.js → chunk-OLZLYLOE.js} +109 -7
- package/dist/chunk-OLZLYLOE.js.map +1 -0
- package/dist/{chunk-CEHXC5NI.js → chunk-Q3C3KH3C.js} +24 -5
- package/dist/chunk-Q3C3KH3C.js.map +1 -0
- package/dist/{chunk-MNQC5XEJ.js → chunk-QCB7ZNR2.js} +94 -37
- package/dist/chunk-QCB7ZNR2.js.map +1 -0
- package/dist/{chunk-T4TCLEFB.js → chunk-QMV3R6D5.js} +91 -36
- package/dist/chunk-QMV3R6D5.js.map +1 -0
- package/dist/chunk-QNFVYST4.js +110 -0
- package/dist/chunk-QNFVYST4.js.map +1 -0
- package/dist/{chunk-WL4AX5FI.js → chunk-QWBXUZAX.js} +57 -54
- package/dist/chunk-QWBXUZAX.js.map +1 -0
- package/dist/cli/index.js +5 -5
- package/dist/client/core.d.ts +12 -1
- package/dist/client/core.js +1 -1
- package/dist/client/index.d.ts +7 -1
- package/dist/client/index.js +2 -1
- package/dist/client/index.js.map +1 -1
- package/dist/controller-swc-transform-URELVCBM.js +27 -0
- package/dist/controller-swc-transform-URELVCBM.js.map +1 -0
- package/dist/controller-swc-transform-XV3ETSE3.js +28 -0
- package/dist/controller-swc-transform-XV3ETSE3.js.map +1 -0
- package/dist/{dev-CNPT7COE.js → dev-IBCZYFZZ.js} +8 -8
- package/dist/{index-C3ged4mn.d.ts → index-BeAsvfPr.d.ts} +2 -2
- package/dist/index.js +4 -4
- package/dist/{internal-api-4IXBITVV.js → internal-api-MDCX7A7H.js} +10 -3
- package/dist/{internal-api-5JKDYRGM.js → internal-api-OLNM25HS.js} +9 -3
- package/dist/{mcp-INKDONQS.js → mcp-DKRODZAL.js} +2 -2
- package/dist/{plugin-runner-CMprWWHZ.d.ts → plugin-runner-BFEcgtmJ.d.ts} +1 -1
- package/dist/{plugin-types-L49QYMb5.d.ts → plugin-types-D69-lJZ9.d.ts} +9 -2
- package/dist/{registry-3NB7KOUI.js → registry-FKPH7XEL.js} +1 -1
- package/dist/server/define/index.d.ts +1 -1
- package/dist/server/define/index.js +1 -1
- package/dist/server/http/index.d.ts +3 -3
- package/dist/server/http/index.js +1 -1
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/plugins/index.d.ts +3 -3
- package/dist/{start-LNJZNKF6.js → start-WB7GYXEW.js} +3 -3
- package/dist/vite-plugin/index.js +4 -4
- package/dist/{vite-plugin-NC5ETNMV.js → vite-plugin-5Y3LDTXI.js} +5 -5
- package/package.json +2 -2
- package/dist/app-typed-client-ESMMLPYK.js.map +0 -1
- package/dist/app-typed-client-N4TRX5FI.js.map +0 -1
- package/dist/chunk-3KD2O6DC.js.map +0 -1
- package/dist/chunk-CEHXC5NI.js.map +0 -1
- package/dist/chunk-MNQC5XEJ.js.map +0 -1
- package/dist/chunk-RPMKF6BC.js.map +0 -1
- package/dist/chunk-RSVN727G.js +0 -1
- package/dist/chunk-T4TCLEFB.js.map +0 -1
- package/dist/chunk-WL4AX5FI.js.map +0 -1
- package/dist/vite-plugin-NC5ETNMV.js.map +0 -1
- /package/dist/{actions-virtual-module-SYGQE6JQ.js.map → actions-virtual-module-H3LY5Q22.js.map} +0 -0
- /package/dist/{actions-virtual-module-3KSP3WPJ.js.map → actions-virtual-module-WLOT2SPR.js.map} +0 -0
- /package/dist/{agent-DX5LHD6X.js.map → agent-VQMWM5BK.js.map} +0 -0
- /package/dist/{build-3C2KQPME.js.map → build-3BFIACHN.js.map} +0 -0
- /package/dist/{chunk-RVJAM77M.js.map → chunk-2HKRQUWU.js.map} +0 -0
- /package/dist/{chunk-UDG6JPVP.js.map → chunk-GW5H4KB4.js.map} +0 -0
- /package/dist/{chunk-DISADTFZ.js.map → chunk-IPSFSNEJ.js.map} +0 -0
- /package/dist/{dev-CNPT7COE.js.map → dev-IBCZYFZZ.js.map} +0 -0
- /package/dist/{chunk-RSVN727G.js.map → internal-api-MDCX7A7H.js.map} +0 -0
- /package/dist/{internal-api-4IXBITVV.js.map → internal-api-OLNM25HS.js.map} +0 -0
- /package/dist/{mcp-INKDONQS.js.map → mcp-DKRODZAL.js.map} +0 -0
- /package/dist/{registry-3NB7KOUI.js.map → registry-FKPH7XEL.js.map} +0 -0
- /package/dist/{start-LNJZNKF6.js.map → start-WB7GYXEW.js.map} +0 -0
- /package/dist/{internal-api-5JKDYRGM.js.map → vite-plugin-5Y3LDTXI.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import "./chunk-
|
|
2
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
4
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
15
|
+
//# sourceMappingURL=agent-VQMWM5BK.js.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "tsx/esm";
|
|
3
|
-
import
|
|
4
|
-
|
|
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 {
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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-
|
|
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
|
|
2
|
-
|
|
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 {
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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-
|
|
388
|
+
//# sourceMappingURL=app-typed-client-SPFNGVZL.js.map
|