@theokit/http 0.4.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/README.md +172 -0
- package/dist/app.d.ts +67 -0
- package/dist/app.js +11 -0
- package/dist/app.js.map +1 -0
- package/dist/chunk-34KOKJ5M.js +71 -0
- package/dist/chunk-34KOKJ5M.js.map +1 -0
- package/dist/chunk-3PGQVQWG.js +276 -0
- package/dist/chunk-3PGQVQWG.js.map +1 -0
- package/dist/chunk-7QVYU63E.js +7 -0
- package/dist/chunk-7QVYU63E.js.map +1 -0
- package/dist/chunk-HLW7YKZE.js +99 -0
- package/dist/chunk-HLW7YKZE.js.map +1 -0
- package/dist/chunk-LKNI6QEP.js +20 -0
- package/dist/chunk-LKNI6QEP.js.map +1 -0
- package/dist/chunk-LWCNTZN6.js +87 -0
- package/dist/chunk-LWCNTZN6.js.map +1 -0
- package/dist/chunk-SMWUPP2C.js +125 -0
- package/dist/chunk-SMWUPP2C.js.map +1 -0
- package/dist/chunk-TBMGRXH5.js +477 -0
- package/dist/chunk-TBMGRXH5.js.map +1 -0
- package/dist/chunk-U46H4CGF.js +34 -0
- package/dist/chunk-U46H4CGF.js.map +1 -0
- package/dist/exception-filter-chain-BCSQ3MZ2.js +10 -0
- package/dist/exception-filter-chain-BCSQ3MZ2.js.map +1 -0
- package/dist/index.d.ts +1047 -0
- package/dist/index.js +761 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptor-chain-6S3PUV7J.js +9 -0
- package/dist/interceptor-chain-6S3PUV7J.js.map +1 -0
- package/dist/middleware-consumer-ljxK1fU_.d.ts +58 -0
- package/dist/runtime-node.d.ts +21 -0
- package/dist/runtime-node.js +12 -0
- package/dist/runtime-node.js.map +1 -0
- package/dist/theokit-plugin.d.ts +65 -0
- package/dist/theokit-plugin.js +432 -0
- package/dist/theokit-plugin.js.map +1 -0
- package/dist/types-CGthbcon.d.ts +19 -0
- package/package.json +58 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MiddlewareConsumerImpl,
|
|
3
|
+
runMiddleware
|
|
4
|
+
} from "./chunk-LWCNTZN6.js";
|
|
5
|
+
import {
|
|
6
|
+
HttpDecoratorsConfigError,
|
|
7
|
+
createExecutionContext,
|
|
8
|
+
walkControllerMetadata
|
|
9
|
+
} from "./chunk-SMWUPP2C.js";
|
|
10
|
+
import {
|
|
11
|
+
nodeIncomingToRequest,
|
|
12
|
+
writeResponseToNode
|
|
13
|
+
} from "./chunk-HLW7YKZE.js";
|
|
14
|
+
import {
|
|
15
|
+
runInterceptors
|
|
16
|
+
} from "./chunk-U46H4CGF.js";
|
|
17
|
+
import {
|
|
18
|
+
runExceptionFilters
|
|
19
|
+
} from "./chunk-34KOKJ5M.js";
|
|
20
|
+
import {
|
|
21
|
+
ForbiddenException
|
|
22
|
+
} from "./chunk-3PGQVQWG.js";
|
|
23
|
+
import {
|
|
24
|
+
resolveOrNew
|
|
25
|
+
} from "./chunk-LKNI6QEP.js";
|
|
26
|
+
import {
|
|
27
|
+
__name
|
|
28
|
+
} from "./chunk-7QVYU63E.js";
|
|
29
|
+
|
|
30
|
+
// src/theokit-plugin.ts
|
|
31
|
+
import "reflect-metadata";
|
|
32
|
+
|
|
33
|
+
// src/bridge/swc-loader.ts
|
|
34
|
+
import "reflect-metadata";
|
|
35
|
+
import { readFileSync, writeFileSync, unlinkSync, readdirSync } from "fs";
|
|
36
|
+
import { createRequire } from "module";
|
|
37
|
+
import { resolve, dirname, join, relative } from "path";
|
|
38
|
+
import { pathToFileURL, fileURLToPath } from "url";
|
|
39
|
+
function getSwcrcTarget(swcrc) {
|
|
40
|
+
if (!swcrc) return "es2022";
|
|
41
|
+
const jsc = swcrc.jsc;
|
|
42
|
+
if (typeof jsc !== "object" || jsc === null) return "es2022";
|
|
43
|
+
const target = jsc.target;
|
|
44
|
+
return typeof target === "string" ? target : "es2022";
|
|
45
|
+
}
|
|
46
|
+
__name(getSwcrcTarget, "getSwcrcTarget");
|
|
47
|
+
var consumerSwcrcCache;
|
|
48
|
+
function readConsumerSwcrc(startDir) {
|
|
49
|
+
if (consumerSwcrcCache !== void 0) return consumerSwcrcCache;
|
|
50
|
+
let dir = startDir;
|
|
51
|
+
const root = dirname(dir);
|
|
52
|
+
while (dir !== root) {
|
|
53
|
+
const candidate = join(dir, ".swcrc");
|
|
54
|
+
try {
|
|
55
|
+
const raw = readFileSync(candidate, "utf-8");
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
delete parsed.$schema;
|
|
58
|
+
consumerSwcrcCache = parsed;
|
|
59
|
+
return parsed;
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
const parent = dirname(dir);
|
|
63
|
+
if (parent === dir) break;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
consumerSwcrcCache = null;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
__name(readConsumerSwcrc, "readConsumerSwcrc");
|
|
70
|
+
var swcCoreCache;
|
|
71
|
+
async function loadSwcCore() {
|
|
72
|
+
if (swcCoreCache !== void 0) return swcCoreCache;
|
|
73
|
+
try {
|
|
74
|
+
swcCoreCache = await import("@swc/core");
|
|
75
|
+
} catch {
|
|
76
|
+
try {
|
|
77
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
78
|
+
const req = createRequire(resolve(thisDir, "index.js"));
|
|
79
|
+
const resolved = req.resolve("@swc/core");
|
|
80
|
+
swcCoreCache = await import(pathToFileURL(resolved).href);
|
|
81
|
+
} catch {
|
|
82
|
+
swcCoreCache = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return swcCoreCache;
|
|
86
|
+
}
|
|
87
|
+
__name(loadSwcCore, "loadSwcCore");
|
|
88
|
+
async function loadControllerWithSwc(absoluteFilePath) {
|
|
89
|
+
const swc = await loadSwcCore();
|
|
90
|
+
if (!swc) {
|
|
91
|
+
throw new HttpDecoratorsConfigError(`@swc/core is required for parameter decorators (@Body, @Param, @Query). Install it:
|
|
92
|
+
|
|
93
|
+
pnpm add -D @swc/core
|
|
94
|
+
|
|
95
|
+
esbuild (used by tsx/Vite) cannot parse parameter decorators. SWC handles them correctly with full metadata emission.`);
|
|
96
|
+
}
|
|
97
|
+
const source = readFileSync(absoluteFilePath, "utf-8");
|
|
98
|
+
const consumerSwcrc = readConsumerSwcrc(dirname(absoluteFilePath));
|
|
99
|
+
const { code } = swc.transformSync(source, {
|
|
100
|
+
filename: absoluteFilePath,
|
|
101
|
+
jsc: {
|
|
102
|
+
parser: {
|
|
103
|
+
syntax: "typescript",
|
|
104
|
+
decorators: true,
|
|
105
|
+
...consumerSwcrc?.jsc?.parser
|
|
106
|
+
},
|
|
107
|
+
transform: {
|
|
108
|
+
...consumerSwcrc?.jsc?.transform,
|
|
109
|
+
// NON-NEGOTIABLE — always enforce decorator metadata emission
|
|
110
|
+
// regardless of consumer .swcrc overrides
|
|
111
|
+
legacyDecorator: true,
|
|
112
|
+
decoratorMetadata: true
|
|
113
|
+
},
|
|
114
|
+
target: getSwcrcTarget(consumerSwcrc)
|
|
115
|
+
},
|
|
116
|
+
module: {
|
|
117
|
+
type: "es6"
|
|
118
|
+
},
|
|
119
|
+
sourceMaps: false
|
|
120
|
+
});
|
|
121
|
+
const tmpPath = absoluteFilePath.replace(/\.ts$/, ".__decorated__.mjs");
|
|
122
|
+
writeFileSync(tmpPath, code, "utf-8");
|
|
123
|
+
try {
|
|
124
|
+
const url = pathToFileURL(tmpPath).href + `?t=${Date.now()}`;
|
|
125
|
+
return await import(url);
|
|
126
|
+
} finally {
|
|
127
|
+
try {
|
|
128
|
+
unlinkSync(tmpPath);
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
__name(loadControllerWithSwc, "loadControllerWithSwc");
|
|
134
|
+
async function loadControllersFromGlob(rootDir, pattern) {
|
|
135
|
+
const files = scanControllerFiles(rootDir, pattern);
|
|
136
|
+
if (files.length === 0) {
|
|
137
|
+
console.warn(`[@theokit/http] No controller files found matching "${pattern}" in ${rootDir}. Ensure files match the pattern and export @Controller classes.`);
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const controllers = [];
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
const absPath = resolve(rootDir, file);
|
|
143
|
+
const mod = await loadControllerWithSwc(absPath);
|
|
144
|
+
for (const exported of Object.values(mod)) {
|
|
145
|
+
if (typeof exported === "function" && isControllerClass(exported)) {
|
|
146
|
+
controllers.push(exported);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return controllers;
|
|
151
|
+
}
|
|
152
|
+
__name(loadControllersFromGlob, "loadControllersFromGlob");
|
|
153
|
+
function isControllerClass(fn) {
|
|
154
|
+
try {
|
|
155
|
+
return Reflect.hasMetadata(/* @__PURE__ */ Symbol.for("theokit:http-decorators:controller-prefix"), fn);
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
__name(isControllerClass, "isControllerClass");
|
|
161
|
+
function scanControllerFiles(rootDir, pattern) {
|
|
162
|
+
const parts = pattern.split("/");
|
|
163
|
+
const dirParts = [];
|
|
164
|
+
for (const part of parts) {
|
|
165
|
+
if (part.includes("*")) break;
|
|
166
|
+
dirParts.push(part);
|
|
167
|
+
}
|
|
168
|
+
const baseDir = join(rootDir, ...dirParts);
|
|
169
|
+
const lastPart = parts[parts.length - 1];
|
|
170
|
+
const suffix = lastPart.replace(/\*/g, "");
|
|
171
|
+
const files = [];
|
|
172
|
+
function walk(dir) {
|
|
173
|
+
try {
|
|
174
|
+
for (const entry of readdirSync(dir, {
|
|
175
|
+
withFileTypes: true
|
|
176
|
+
})) {
|
|
177
|
+
const full = join(dir, entry.name);
|
|
178
|
+
if (entry.isDirectory()) walk(full);
|
|
179
|
+
else if (entry.name.endsWith(suffix)) files.push(relative(rootDir, full));
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
__name(walk, "walk");
|
|
185
|
+
walk(baseDir);
|
|
186
|
+
return files;
|
|
187
|
+
}
|
|
188
|
+
__name(scanControllerFiles, "scanControllerFiles");
|
|
189
|
+
|
|
190
|
+
// src/theokit-plugin.ts
|
|
191
|
+
function httpDecoratorsPlugin(opts) {
|
|
192
|
+
const routes = [];
|
|
193
|
+
let initialized = false;
|
|
194
|
+
let initPromise = null;
|
|
195
|
+
const middlewareConsumer = new MiddlewareConsumerImpl(opts.container);
|
|
196
|
+
if (opts.configure) opts.configure(middlewareConsumer);
|
|
197
|
+
const middlewareEntries = middlewareConsumer.getEntries();
|
|
198
|
+
if (opts.controllers && opts.controllers.length > 0) {
|
|
199
|
+
buildRouteTable(routes, opts.controllers, opts.container);
|
|
200
|
+
initialized = true;
|
|
201
|
+
}
|
|
202
|
+
if (opts.controllersGlob && !initialized) {
|
|
203
|
+
const glob = opts.controllersGlob;
|
|
204
|
+
initPromise = (async () => {
|
|
205
|
+
const rootDir = process.cwd();
|
|
206
|
+
const controllers = await loadControllersFromGlob(rootDir, glob);
|
|
207
|
+
buildRouteTable(routes, controllers, opts.container);
|
|
208
|
+
initialized = true;
|
|
209
|
+
console.log(`[@theokit/http] Loaded ${controllers.length} controller(s) with ${routes.length} route(s) via SWC.`);
|
|
210
|
+
})();
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
name: "@theokit/http",
|
|
214
|
+
register(app) {
|
|
215
|
+
app.addHook("onRequest", async (pluginCtx) => {
|
|
216
|
+
if (initPromise && !initialized) await initPromise;
|
|
217
|
+
const _nodeHttp = await import("http");
|
|
218
|
+
const request = nodeIncomingToRequest(pluginCtx.request);
|
|
219
|
+
const response = await handleDecoratorRoute(routes, request, opts.container, middlewareEntries);
|
|
220
|
+
if (response) {
|
|
221
|
+
await writeResponseToNode(response, pluginCtx.response);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
__name(httpDecoratorsPlugin, "httpDecoratorsPlugin");
|
|
228
|
+
function buildRouteTable(routes, controllers, container) {
|
|
229
|
+
const seen = /* @__PURE__ */ new Set();
|
|
230
|
+
const unique = [];
|
|
231
|
+
for (const Ctor of controllers) {
|
|
232
|
+
if (seen.has(Ctor)) continue;
|
|
233
|
+
seen.add(Ctor);
|
|
234
|
+
unique.push(Ctor);
|
|
235
|
+
}
|
|
236
|
+
for (const Ctor of unique) {
|
|
237
|
+
const instance = resolveOrNew(Ctor, container);
|
|
238
|
+
const walks = walkControllerMetadata(Ctor);
|
|
239
|
+
for (const w of walks) {
|
|
240
|
+
const { regex, paramNames } = compilePattern(w.fullPath);
|
|
241
|
+
routes.push({
|
|
242
|
+
walk: w,
|
|
243
|
+
instance,
|
|
244
|
+
regex,
|
|
245
|
+
paramNames
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
routes.sort((a, b) => {
|
|
250
|
+
const aHasParam = a.walk.fullPath.includes(":");
|
|
251
|
+
const bHasParam = b.walk.fullPath.includes(":");
|
|
252
|
+
if (aHasParam !== bHasParam) return aHasParam ? 1 : -1;
|
|
253
|
+
return 0;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
__name(buildRouteTable, "buildRouteTable");
|
|
257
|
+
async function handleDecoratorRoute(routes, request, container, mwEntries = []) {
|
|
258
|
+
const url = new URL(request.url);
|
|
259
|
+
const method = request.method.toUpperCase();
|
|
260
|
+
const match = findMatch(routes, method, url.pathname);
|
|
261
|
+
if (!match) return null;
|
|
262
|
+
const { entry, params } = match;
|
|
263
|
+
const { walk, instance } = entry;
|
|
264
|
+
try {
|
|
265
|
+
const mwResponse = await runMiddleware(mwEntries, request, url.pathname);
|
|
266
|
+
if (mwResponse) return mwResponse;
|
|
267
|
+
const ctx = createExecutionContext(request, instance.constructor, walk.propertyKey);
|
|
268
|
+
const guardResponse = await runGuards(walk.guards, ctx, container);
|
|
269
|
+
if (guardResponse) return guardResponse;
|
|
270
|
+
const body = await resolveBody(method, request, walk);
|
|
271
|
+
if (body instanceof Response) return body;
|
|
272
|
+
const args = buildArgs(walk.paramEntries, {
|
|
273
|
+
request,
|
|
274
|
+
body,
|
|
275
|
+
params,
|
|
276
|
+
query: Object.fromEntries(url.searchParams)
|
|
277
|
+
});
|
|
278
|
+
if (walk.redirect) {
|
|
279
|
+
return new Response(null, {
|
|
280
|
+
status: walk.redirect.status,
|
|
281
|
+
headers: {
|
|
282
|
+
location: walk.redirect.url
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const handlerFn = instance[walk.propertyKey];
|
|
287
|
+
const result = await runInterceptors(walk.interceptors, () => handlerFn.apply(instance, args), request, container);
|
|
288
|
+
return buildResponse(result, walk, method);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
return runExceptionFilters(err, walk.filters, request, container);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
__name(handleDecoratorRoute, "handleDecoratorRoute");
|
|
294
|
+
async function runGuards(guards, context, container) {
|
|
295
|
+
for (const GuardCtor of guards) {
|
|
296
|
+
const guard = resolveOrNew(GuardCtor, container);
|
|
297
|
+
if (!await guard.canActivate(context)) {
|
|
298
|
+
const ex = new ForbiddenException("Forbidden resource");
|
|
299
|
+
return jsonResponse(ex.statusCode, ex.toJSON());
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
__name(runGuards, "runGuards");
|
|
305
|
+
async function resolveBody(method, request, walk) {
|
|
306
|
+
if (![
|
|
307
|
+
"POST",
|
|
308
|
+
"PUT",
|
|
309
|
+
"PATCH"
|
|
310
|
+
].includes(method)) return void 0;
|
|
311
|
+
let body;
|
|
312
|
+
try {
|
|
313
|
+
const text = await request.text();
|
|
314
|
+
body = text ? JSON.parse(text) : void 0;
|
|
315
|
+
} catch {
|
|
316
|
+
body = void 0;
|
|
317
|
+
}
|
|
318
|
+
if (walk.bodySchema && body !== void 0) {
|
|
319
|
+
const result = walk.bodySchema.safeParse(body);
|
|
320
|
+
if (!result.success) {
|
|
321
|
+
return jsonResponse(422, {
|
|
322
|
+
error: {
|
|
323
|
+
code: "VALIDATION_ERROR",
|
|
324
|
+
issues: result.error.issues
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
body = result.data;
|
|
329
|
+
}
|
|
330
|
+
return body;
|
|
331
|
+
}
|
|
332
|
+
__name(resolveBody, "resolveBody");
|
|
333
|
+
function buildResponse(result, walk, method) {
|
|
334
|
+
const status = walk.status ?? (method === "POST" ? 201 : 200);
|
|
335
|
+
const headers = {
|
|
336
|
+
"content-type": "application/json"
|
|
337
|
+
};
|
|
338
|
+
for (const [n, v] of walk.headers) headers[n.toLowerCase()] = v;
|
|
339
|
+
if (result === void 0 || result === null) {
|
|
340
|
+
return new Response(null, {
|
|
341
|
+
status: status === 200 ? 204 : status,
|
|
342
|
+
headers
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (typeof result === "string") {
|
|
346
|
+
headers["content-type"] = "text/plain";
|
|
347
|
+
return new Response(result, {
|
|
348
|
+
status,
|
|
349
|
+
headers
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return new Response(JSON.stringify(result), {
|
|
353
|
+
status,
|
|
354
|
+
headers
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
__name(buildResponse, "buildResponse");
|
|
358
|
+
function jsonResponse(status, body) {
|
|
359
|
+
return new Response(JSON.stringify(body), {
|
|
360
|
+
status,
|
|
361
|
+
headers: {
|
|
362
|
+
"content-type": "application/json"
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
__name(jsonResponse, "jsonResponse");
|
|
367
|
+
function findMatch(routes, method, pathname) {
|
|
368
|
+
for (const entry of routes) {
|
|
369
|
+
if (entry.walk.verb !== "ALL" && entry.walk.verb !== method) continue;
|
|
370
|
+
const match = pathname.match(entry.regex);
|
|
371
|
+
if (!match) continue;
|
|
372
|
+
const params = {};
|
|
373
|
+
entry.paramNames.forEach((name, i) => {
|
|
374
|
+
params[name] = match[i + 1];
|
|
375
|
+
});
|
|
376
|
+
return {
|
|
377
|
+
entry,
|
|
378
|
+
params
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
__name(findMatch, "findMatch");
|
|
384
|
+
function compilePattern(pattern) {
|
|
385
|
+
const paramNames = [];
|
|
386
|
+
const regexStr = pattern.replace(/:(\w+)/g, (_m, name) => {
|
|
387
|
+
paramNames.push(name);
|
|
388
|
+
return "([^/]+)";
|
|
389
|
+
});
|
|
390
|
+
return {
|
|
391
|
+
regex: new RegExp(`^${regexStr}$`),
|
|
392
|
+
paramNames
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
__name(compilePattern, "compilePattern");
|
|
396
|
+
function buildArgs(paramEntries, ctx) {
|
|
397
|
+
if (paramEntries.length === 0) return [];
|
|
398
|
+
const maxIndex = Math.max(...paramEntries.map((p) => p.index));
|
|
399
|
+
const args = Array.from({
|
|
400
|
+
length: maxIndex + 1
|
|
401
|
+
}, () => void 0);
|
|
402
|
+
for (const p of paramEntries) {
|
|
403
|
+
switch (p.source) {
|
|
404
|
+
case "req":
|
|
405
|
+
args[p.index] = ctx.request;
|
|
406
|
+
break;
|
|
407
|
+
case "body":
|
|
408
|
+
args[p.index] = p.key ? ctx.body[p.key] : ctx.body;
|
|
409
|
+
break;
|
|
410
|
+
case "param":
|
|
411
|
+
args[p.index] = p.key ? ctx.params[p.key] : ctx.params;
|
|
412
|
+
break;
|
|
413
|
+
case "query":
|
|
414
|
+
args[p.index] = p.key ? ctx.query[p.key] : ctx.query;
|
|
415
|
+
break;
|
|
416
|
+
case "headers":
|
|
417
|
+
args[p.index] = p.key ? ctx.request.headers.get(p.key.toLowerCase()) : Object.fromEntries(ctx.request.headers.entries());
|
|
418
|
+
break;
|
|
419
|
+
case "ip":
|
|
420
|
+
args[p.index] = ctx.request.headers.get("x-forwarded-for") ?? "127.0.0.1";
|
|
421
|
+
break;
|
|
422
|
+
default:
|
|
423
|
+
args[p.index] = void 0;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return args;
|
|
427
|
+
}
|
|
428
|
+
__name(buildArgs, "buildArgs");
|
|
429
|
+
export {
|
|
430
|
+
httpDecoratorsPlugin
|
|
431
|
+
};
|
|
432
|
+
//# sourceMappingURL=theokit-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theokit-plugin.ts","../src/bridge/swc-loader.ts"],"sourcesContent":["/* eslint-disable security/detect-non-literal-regexp */\n/**\n * TheoKit integration plugin for @theokit/http.\n *\n * Two modes:\n *\n * **Mode 1 — controllersGlob (RECOMMENDED):**\n * Controllers are discovered by glob pattern and loaded via @swc/core at\n * first request. This bypasses the esbuild parameter-decorator limitation\n * because controller files are never imported through tsx/esbuild.\n *\n * ```ts\n * // theo.config.ts — NO controller import needed\n * import { defineConfig } from 'theokit/server'\n * import { httpDecoratorsPlugin } from '@theokit/http/theokit-plugin'\n *\n * export default defineConfig({\n * plugins: [\n * httpDecoratorsPlugin({\n * controllersGlob: 'server/controllers/**\\/*.controller.ts'\n * })\n * ]\n * })\n * ```\n *\n * **Mode 2 — direct controllers array (tests, pre-compiled):**\n * Pass controller classes directly. Only works when the transpiler\n * supports parameter decorators (e.g., SWC in Vitest, or manual\n * Reflect.defineMetadata in tests).\n *\n * ```ts\n * httpDecoratorsPlugin({ controllers: [CatsController] })\n * ```\n */\nimport 'reflect-metadata'\n\nimport { resolveOrNew, type DiContainer } from './bridge/di-resolve.js'\nimport { runExceptionFilters } from './bridge/exception-filter-chain.js'\nimport { createExecutionContext, type CanActivate, type ExecutionContext } from './bridge/execution-context.js'\nimport { runInterceptors } from './bridge/interceptor-chain.js'\nimport {\n MiddlewareConsumerImpl,\n runMiddleware,\n type ResolvedMiddleware,\n} from './bridge/middleware-consumer.js'\nimport { nodeIncomingToRequest, writeResponseToNode } from './bridge/runtime/node.js'\nimport { loadControllersFromGlob } from './bridge/swc-loader.js'\nimport { walkControllerMetadata, type WalkResult } from './bridge/walk-metadata.js'\nimport type { ParamEntry } from './decorators/params.js'\nimport { ForbiddenException } from './exceptions/http-exception.js'\n\nexport interface HttpDecoratorsPluginOptions {\n /** Direct controller class references (Mode 2 — tests, pre-compiled). */\n controllers?: Function[]\n /** Glob pattern relative to project root (Mode 1 — RECOMMENDED).\n * Example: 'server/controllers/**\\/*.controller.ts'\n * Requires @swc/core as devDependency. */\n controllersGlob?: string\n /** Optional DI container (e.g., @theokit/di Container). */\n container?: DiContainer\n /** NestJS-style middleware configuration callback. */\n configure?: (consumer: MiddlewareConsumerImpl) => void\n}\n\ninterface RouteEntry {\n walk: WalkResult\n instance: object\n regex: RegExp\n paramNames: string[]\n}\n\n/**\n * TheoKit plugin that mounts decorated controllers into the `theokit dev`\n * / `theokit start` request pipeline via the `onRequest` hook.\n *\n * Returns a plain `{ name, register }` object compatible with\n * `TheoPlugin` from `theokit/server` (Pattern D6 — we don't import the\n * type to avoid adding theokit as a compile-time dep; the structural\n * shape is sufficient).\n */\nexport function httpDecoratorsPlugin(opts: HttpDecoratorsPluginOptions) {\n const routes: RouteEntry[] = []\n let initialized = false\n let initPromise: Promise<void> | null = null\n\n // Collect middleware via configure() callback (NestJS pattern)\n const middlewareConsumer = new MiddlewareConsumerImpl(opts.container)\n if (opts.configure) opts.configure(middlewareConsumer)\n const middlewareEntries = middlewareConsumer.getEntries()\n\n // Mode 2: Direct class references — initialize eagerly\n if (opts.controllers && opts.controllers.length > 0) {\n buildRouteTable(routes, opts.controllers, opts.container)\n initialized = true\n }\n\n // Mode 1: controllersGlob — defer initialization to first request\n if (opts.controllersGlob && !initialized) {\n const glob = opts.controllersGlob\n initPromise = (async () => {\n // Resolve root from process.cwd() — the TheoKit project root\n const rootDir = process.cwd()\n const controllers = await loadControllersFromGlob(rootDir, glob)\n buildRouteTable(routes, controllers, opts.container)\n initialized = true\n console.log(\n `[@theokit/http] Loaded ${controllers.length} controller(s) ` +\n `with ${routes.length} route(s) via SWC.`,\n )\n })()\n }\n\n return {\n name: '@theokit/http',\n register(app: {\n addHook: (\n name: string,\n fn: (ctx: Record<string, unknown>) => Promise<void>,\n ) => void\n }) {\n app.addHook('onRequest', async (pluginCtx) => {\n if (initPromise && !initialized) await initPromise\n\n // Convert Node types → Web Standard at the boundary\n const _nodeHttp = await import('node:http')\n const request = nodeIncomingToRequest(pluginCtx.request as InstanceType<typeof _nodeHttp.IncomingMessage>)\n const response = await handleDecoratorRoute(routes, request, opts.container, middlewareEntries)\n if (response) {\n await writeResponseToNode(response, pluginCtx.response as InstanceType<typeof _nodeHttp.ServerResponse>)\n }\n // null = not our route, fall through to TheoKit scanner\n })\n },\n }\n}\n\n// ─── Route table builder ─────────────────────────────────\n\nfunction buildRouteTable(\n routes: RouteEntry[],\n controllers: Function[],\n container?: DiContainer,\n): void {\n // Dedupe controllers (EC-5)\n const seen = new Set<Function>()\n const unique: Function[] = []\n for (const Ctor of controllers) {\n if (seen.has(Ctor)) continue\n seen.add(Ctor)\n unique.push(Ctor)\n }\n\n for (const Ctor of unique) {\n const instance = resolveOrNew(Ctor, container)\n const walks = walkControllerMetadata(Ctor)\n for (const w of walks) {\n const { regex, paramNames } = compilePattern(w.fullPath)\n routes.push({ walk: w, instance, regex, paramNames })\n }\n }\n\n // Sort: static routes first, parameterized last (prevents /cats/:id matching \"admin\")\n routes.sort((a, b) => {\n const aHasParam = a.walk.fullPath.includes(':')\n const bHasParam = b.walk.fullPath.includes(':')\n if (aHasParam !== bHasParam) return aHasParam ? 1 : -1\n return 0\n })\n}\n\n// ─── Request handler (extracted for complexity budget) ──\n\n/** Returns Response if handled, null if not our route. */\nasync function handleDecoratorRoute(\n routes: RouteEntry[],\n request: Request,\n container?: DiContainer,\n mwEntries: ResolvedMiddleware[] = [],\n): Promise<Response | null> {\n const url = new URL(request.url)\n const method = request.method.toUpperCase()\n const match = findMatch(routes, method, url.pathname)\n if (!match) return null\n\n const { entry, params } = match\n const { walk, instance } = entry\n\n try {\n const mwResponse = await runMiddleware(mwEntries, request, url.pathname)\n if (mwResponse) return mwResponse\n\n const ctx = createExecutionContext(request, instance.constructor, walk.propertyKey)\n const guardResponse = await runGuards(walk.guards, ctx, container)\n if (guardResponse) return guardResponse\n\n const body = await resolveBody(method, request, walk)\n if (body instanceof Response) return body\n\n const args = buildArgs(walk.paramEntries, {\n request,\n body,\n params,\n query: Object.fromEntries(url.searchParams),\n })\n\n if (walk.redirect) {\n return new Response(null, { status: walk.redirect.status, headers: { location: walk.redirect.url } })\n }\n\n const handlerFn = (instance as Record<string | symbol, Function>)[walk.propertyKey]\n const result = await runInterceptors(\n walk.interceptors,\n () => handlerFn.apply(instance, args) as Promise<unknown>,\n request,\n container,\n )\n\n return buildResponse(result, walk, method)\n } catch (err) {\n return runExceptionFilters(err, walk.filters, request, container)\n }\n}\n\nasync function runGuards(\n guards: Function[],\n context: ExecutionContext,\n container?: DiContainer,\n): Promise<Response | null> {\n for (const GuardCtor of guards) {\n const guard = resolveOrNew(GuardCtor, container) as CanActivate\n if (!(await guard.canActivate(context))) {\n const ex = new ForbiddenException('Forbidden resource')\n return jsonResponse(ex.statusCode, ex.toJSON())\n }\n }\n return null\n}\n\nasync function resolveBody(method: string, request: Request, walk: WalkResult): Promise<unknown> {\n if (!['POST', 'PUT', 'PATCH'].includes(method)) return undefined\n let body: unknown\n try {\n const text = await request.text()\n body = text ? JSON.parse(text) : undefined\n } catch { body = undefined }\n if (walk.bodySchema && body !== undefined) {\n const result = walk.bodySchema.safeParse(body)\n if (!result.success) {\n return jsonResponse(422, { error: { code: 'VALIDATION_ERROR', issues: result.error.issues } })\n }\n body = result.data\n }\n return body\n}\n\nfunction buildResponse(result: unknown, walk: WalkResult, method: string): Response {\n const status = walk.status ?? (method === 'POST' ? 201 : 200)\n const headers: Record<string, string> = { 'content-type': 'application/json' }\n for (const [n, v] of walk.headers) headers[n.toLowerCase()] = v\n if (result === undefined || result === null) {\n return new Response(null, { status: status === 200 ? 204 : status, headers })\n }\n if (typeof result === 'string') {\n headers['content-type'] = 'text/plain'\n return new Response(result, { status, headers })\n }\n return new Response(JSON.stringify(result), { status, headers })\n}\n\nfunction jsonResponse(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n}\n\nfunction findMatch(\n routes: RouteEntry[],\n method: string,\n pathname: string,\n): { entry: RouteEntry; params: Record<string, string> } | null {\n for (const entry of routes) {\n if (entry.walk.verb !== 'ALL' && entry.walk.verb !== method) continue\n const match = pathname.match(entry.regex)\n if (!match) continue\n const params: Record<string, string> = {}\n entry.paramNames.forEach((name, i) => {\n params[name] = match[i + 1]\n })\n return { entry, params }\n }\n return null\n}\n\nfunction compilePattern(pattern: string): { regex: RegExp; paramNames: string[] } {\n const paramNames: string[] = []\n const regexStr = pattern.replace(/:(\\w+)/g, (_m, name: string) => {\n paramNames.push(name)\n return '([^/]+)'\n })\n return { regex: new RegExp(`^${regexStr}$`), paramNames }\n}\n\ninterface ArgContext {\n request: Request\n body: unknown\n params: Record<string, string>\n query: Record<string, string>\n}\n\nfunction buildArgs(paramEntries: ParamEntry[], ctx: ArgContext): unknown[] {\n if (paramEntries.length === 0) return []\n const maxIndex = Math.max(...paramEntries.map((p) => p.index))\n const args: unknown[] = Array.from({ length: maxIndex + 1 }, () => undefined)\n for (const p of paramEntries) {\n switch (p.source) {\n case 'req':\n args[p.index] = ctx.request\n break\n case 'body':\n args[p.index] = p.key ? (ctx.body as Record<string, unknown>)[p.key] : ctx.body\n break\n case 'param':\n args[p.index] = p.key ? ctx.params[p.key] : ctx.params\n break\n case 'query':\n args[p.index] = p.key ? ctx.query[p.key] : ctx.query\n break\n case 'headers':\n args[p.index] = p.key ? ctx.request.headers.get(p.key.toLowerCase()) : Object.fromEntries(ctx.request.headers.entries())\n break\n case 'ip':\n args[p.index] = ctx.request.headers.get('x-forwarded-for') ?? '127.0.0.1'\n break\n default:\n args[p.index] = undefined\n }\n }\n return args\n}\n\n// resolveOrNew imported from ./bridge/di-resolve.ts (DRY — EC-3)\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * The SWC loader reads/writes controller files by absolute path derived\n * from developer-authored glob patterns in theo.config.ts, not from HTTP\n * input. The file paths come from scanControllerFiles() which walks the\n * project's own source tree. No injection vector.\n */\n/**\n * SWC-powered module loader for controller files with parameter decorators.\n *\n * esbuild (used by tsx/Vite SSR) fundamentally cannot parse TypeScript\n * parameter decorators (`@Body()`, `@Param()`, `@Query()`). This loader\n * uses @swc/core to transform controller files with full decorator support\n * (legacyDecorator + decoratorMetadata), then imports them via a temp .mjs\n * file written in the SAME directory (preserving relative import resolution).\n *\n * Pattern: follows Next.js's approach (read tsconfig → configure SWC)\n * but scoped to the http-decorators package, not the framework core.\n *\n * @see references/next.js/packages/next/src/build/swc/options.ts\n */\nimport 'reflect-metadata'\nimport { readFileSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { resolve, dirname, join, relative } from 'node:path'\nimport { pathToFileURL, fileURLToPath } from 'node:url'\n\nimport { HttpDecoratorsConfigError } from './errors.js'\n\n/** Extract jsc.target from parsed .swcrc or fall back to es2022. */\nfunction getSwcrcTarget(swcrc: Record<string, unknown> | null): string {\n if (!swcrc) return 'es2022'\n const jsc = swcrc.jsc\n if (typeof jsc !== 'object' || jsc === null) return 'es2022'\n const target = (jsc as Record<string, unknown>).target\n return typeof target === 'string' ? target : 'es2022'\n}\n\n/** Cached consumer .swcrc — read once per process. */\nlet consumerSwcrcCache: Record<string, unknown> | null | undefined\n\n/**\n * Walk up from the controller file's directory to find a .swcrc.\n * Returns the parsed JSON or null if none found.\n * Caches the result — .swcrc doesn't change at runtime.\n */\nfunction readConsumerSwcrc(startDir: string): Record<string, unknown> | null {\n if (consumerSwcrcCache !== undefined) return consumerSwcrcCache\n\n let dir = startDir\n const root = dirname(dir) // stop at filesystem root\n while (dir !== root) {\n const candidate = join(dir, '.swcrc')\n try {\n const raw = readFileSync(candidate, 'utf-8')\n const parsed = JSON.parse(raw) as Record<string, unknown>\n // Strip $schema — not an SWC transform option\n delete parsed.$schema\n consumerSwcrcCache = parsed\n return parsed\n } catch {\n // File doesn't exist or invalid JSON — walk up\n }\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n\n consumerSwcrcCache = null\n return null\n}\n\ninterface SwcCore {\n transformSync: (src: string, opts: unknown) => { code: string }\n}\n\n/** Cached @swc/core instance — loaded once, reused across all controller files. */\nlet swcCoreCache: SwcCore | null | undefined\n\n/**\n * Dynamically load @swc/core with singleton cache.\n *\n * Handles pnpm strict node_modules: @swc/core is only directly importable\n * from the package that declares it as a dependency. We use createRequire\n * rooted at THIS package's directory to resolve correctly.\n *\n * The cache ensures the ~50ms dynamic import() cost is paid ONCE,\n * not per-controller-file.\n */\nasync function loadSwcCore(): Promise<SwcCore | null> {\n if (swcCoreCache !== undefined) return swcCoreCache\n\n try {\n swcCoreCache = (await import('@swc/core')) as SwcCore\n } catch {\n try {\n const thisDir = dirname(fileURLToPath(import.meta.url))\n const req = createRequire(resolve(thisDir, 'index.js'))\n const resolved = req.resolve('@swc/core')\n swcCoreCache = (await import(pathToFileURL(resolved).href)) as SwcCore\n } catch {\n swcCoreCache = null\n }\n }\n return swcCoreCache\n}\n\n/**\n * Load a TypeScript controller file using @swc/core for decorator support.\n *\n * Strategy:\n * 1. Read source .ts file\n * 2. Transform via @swc/core with legacyDecorator + decoratorMetadata\n * 3. Write temp .mjs in SAME directory (relative imports resolve correctly)\n * 4. Dynamic import() the .mjs — transitive .ts imports go through\n * tsx/Vite's global hook (they don't have parameter decorators)\n * 5. Cleanup temp file\n */\nexport async function loadControllerWithSwc(\n absoluteFilePath: string,\n): Promise<Record<string, unknown>> {\n const swc = await loadSwcCore()\n if (!swc) {\n throw new HttpDecoratorsConfigError(\n `@swc/core is required for parameter decorators (@Body, @Param, @Query). ` +\n `Install it:\\n\\n pnpm add -D @swc/core\\n\\n` +\n `esbuild (used by tsx/Vite) cannot parse parameter decorators. ` +\n `SWC handles them correctly with full metadata emission.`,\n )\n }\n\n const source = readFileSync(absoluteFilePath, 'utf-8')\n const consumerSwcrc = readConsumerSwcrc(dirname(absoluteFilePath))\n const { code } = swc.transformSync(source, {\n filename: absoluteFilePath,\n jsc: {\n parser: {\n syntax: 'typescript',\n decorators: true,\n ...(consumerSwcrc?.jsc?.parser as Record<string, unknown>),\n },\n transform: {\n ...(consumerSwcrc?.jsc?.transform as Record<string, unknown>),\n // NON-NEGOTIABLE — always enforce decorator metadata emission\n // regardless of consumer .swcrc overrides\n legacyDecorator: true,\n decoratorMetadata: true,\n },\n target: getSwcrcTarget(consumerSwcrc),\n },\n module: { type: 'es6' },\n sourceMaps: false,\n })\n\n // Write temp .mjs in SAME directory so relative imports resolve identically.\n // .mjs = Node treats as ESM regardless of package.json type field.\n // Transitive .ts imports go through tsx's global hook (no param decorators there).\n const tmpPath = absoluteFilePath.replace(/\\.ts$/, '.__decorated__.mjs')\n writeFileSync(tmpPath, code, 'utf-8')\n try {\n const url = pathToFileURL(tmpPath).href + `?t=${Date.now()}`\n return (await import(url)) as Record<string, unknown>\n } finally {\n try {\n unlinkSync(tmpPath)\n } catch {\n // Best-effort cleanup\n }\n }\n}\n\n/**\n * Scan a glob pattern for controller files and load them all via SWC.\n * Returns an array of controller class constructors found.\n */\nexport async function loadControllersFromGlob(\n rootDir: string,\n pattern: string,\n): Promise<Function[]> {\n const files = scanControllerFiles(rootDir, pattern)\n if (files.length === 0) {\n console.warn(\n `[@theokit/http] No controller files found matching \"${pattern}\" ` +\n `in ${rootDir}. Ensure files match the pattern and export @Controller classes.`,\n )\n return []\n }\n\n const controllers: Function[] = []\n for (const file of files) {\n const absPath = resolve(rootDir, file)\n const mod = await loadControllerWithSwc(absPath)\n for (const exported of Object.values(mod)) {\n if (typeof exported === 'function' && isControllerClass(exported)) {\n controllers.push(exported)\n }\n }\n }\n\n return controllers\n}\n\n/**\n * Check if a function has @Controller metadata.\n * Uses Symbol.for() global registry key — same Symbol instance across\n * module boundaries (SWC-loaded controllers share the global registry).\n */\nfunction isControllerClass(fn: Function): boolean {\n try {\n return Reflect.hasMetadata(Symbol.for('theokit:http-decorators:controller-prefix'), fn)\n } catch {\n return false\n }\n}\n\n/**\n * Scan for files matching a controller glob pattern.\n * Extracts the static directory prefix and file suffix from the pattern,\n * then recursively walks the directory.\n */\nfunction scanControllerFiles(rootDir: string, pattern: string): string[] {\n const parts = pattern.split('/')\n const dirParts: string[] = []\n for (const part of parts) {\n if (part.includes('*')) break\n dirParts.push(part)\n }\n const baseDir = join(rootDir, ...dirParts)\n\n // Extract file suffix (e.g., '*.controller.ts' → '.controller.ts')\n const lastPart = parts[parts.length - 1]\n const suffix = lastPart.replace(/\\*/g, '')\n\n const files: string[] = []\n function walk(dir: string) {\n try {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) walk(full)\n else if (entry.name.endsWith(suffix)) files.push(relative(rootDir, full))\n }\n } catch {\n // Directory doesn't exist\n }\n }\n walk(baseDir)\n return files\n}\n"],"mappings":"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","names":["readFileSync","writeFileSync","unlinkSync","readdirSync","createRequire","resolve","dirname","join","relative","pathToFileURL","fileURLToPath","getSwcrcTarget","swcrc","jsc","target","consumerSwcrcCache","readConsumerSwcrc","startDir","undefined","dir","root","dirname","candidate","join","raw","readFileSync","parsed","JSON","parse","$schema","parent","swcCoreCache","loadSwcCore","thisDir","fileURLToPath","url","req","createRequire","resolve","resolved","pathToFileURL","href","loadControllerWithSwc","absoluteFilePath","swc","HttpDecoratorsConfigError","source","consumerSwcrc","code","transformSync","filename","parser","syntax","decorators","transform","legacyDecorator","decoratorMetadata","module","type","sourceMaps","tmpPath","replace","writeFileSync","Date","now","unlinkSync","loadControllersFromGlob","rootDir","pattern","files","scanControllerFiles","length","console","warn","controllers","file","absPath","mod","exported","Object","values","isControllerClass","push","fn","Reflect","hasMetadata","Symbol","for","parts","split","dirParts","part","includes","baseDir","lastPart","suffix","walk","entry","readdirSync","withFileTypes","full","name","isDirectory","endsWith","relative","httpDecoratorsPlugin","opts","routes","initialized","initPromise","middlewareConsumer","MiddlewareConsumerImpl","container","configure","middlewareEntries","getEntries","controllers","length","buildRouteTable","controllersGlob","glob","rootDir","process","cwd","loadControllersFromGlob","console","log","name","register","app","addHook","pluginCtx","_nodeHttp","request","nodeIncomingToRequest","response","handleDecoratorRoute","writeResponseToNode","seen","Set","unique","Ctor","has","add","push","instance","resolveOrNew","walks","walkControllerMetadata","w","regex","paramNames","compilePattern","fullPath","walk","sort","a","b","aHasParam","includes","bHasParam","mwEntries","url","URL","method","toUpperCase","match","findMatch","pathname","entry","params","mwResponse","runMiddleware","ctx","createExecutionContext","propertyKey","guardResponse","runGuards","guards","body","resolveBody","Response","args","buildArgs","paramEntries","query","Object","fromEntries","searchParams","redirect","status","headers","location","handlerFn","result","runInterceptors","interceptors","apply","buildResponse","err","runExceptionFilters","filters","context","GuardCtor","guard","canActivate","ex","ForbiddenException","jsonResponse","statusCode","toJSON","undefined","text","JSON","parse","bodySchema","safeParse","success","error","code","issues","data","n","v","toLowerCase","stringify","verb","forEach","i","pattern","regexStr","replace","_m","RegExp","maxIndex","Math","max","map","p","index","Array","from","source","key","get","entries"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuntimeAdapter — abstraction layer between Web Standard pipeline and runtime server APIs.
|
|
3
|
+
*
|
|
4
|
+
* Per ADR D460: the pipeline operates on Web Standard Request/Response.
|
|
5
|
+
* Each runtime adapter converts its native server API to/from these types.
|
|
6
|
+
*/
|
|
7
|
+
interface RuntimeAdapter {
|
|
8
|
+
/** Create an HTTP server that delegates to a Web Standard handler. */
|
|
9
|
+
createServer(handler: (request: Request) => Response | Promise<Response>): ServerHandle;
|
|
10
|
+
}
|
|
11
|
+
interface ServerHandle {
|
|
12
|
+
listen(port: number, callback?: () => void): void;
|
|
13
|
+
close(callback?: () => void): void;
|
|
14
|
+
address(): {
|
|
15
|
+
port: number;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type { RuntimeAdapter as R, ServerHandle as S };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@theokit/http",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "NestJS-style decorators (@Controller, @Get, @Post, @Body, @UseGuards) bridging to TheoKit's defineRoute + defineMiddleware",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./theokit-plugin": {
|
|
15
|
+
"import": "./dist/theokit-plugin.js",
|
|
16
|
+
"types": "./dist/theokit-plugin.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./app": {
|
|
19
|
+
"import": "./dist/app.js",
|
|
20
|
+
"types": "./dist/app.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"./runtime/node": {
|
|
23
|
+
"import": "./dist/runtime-node.js",
|
|
24
|
+
"types": "./dist/runtime-node.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest",
|
|
36
|
+
"typecheck": "tsc --noEmit"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@swc/core": ">=1.3.0",
|
|
40
|
+
"reflect-metadata": "^0.2.2",
|
|
41
|
+
"theokit": ">=0.2.0",
|
|
42
|
+
"zod": "^4.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"@swc/core": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@swc/core": "^1.15.41",
|
|
51
|
+
"reflect-metadata": "^0.2.2",
|
|
52
|
+
"tsup": "^8.0.0",
|
|
53
|
+
"typescript": "^5.7.0",
|
|
54
|
+
"vitest": "^3.2.0",
|
|
55
|
+
"zod": "^4.0.0"
|
|
56
|
+
},
|
|
57
|
+
"license": "MIT"
|
|
58
|
+
}
|