@theokit/http 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/LICENSE +201 -0
  2. package/dist/app.d.ts +16 -0
  3. package/dist/app.js +1 -1
  4. package/dist/{chunk-34KOKJ5M.js → chunk-6W4T4DPJ.js} +2 -2
  5. package/dist/{chunk-U46H4CGF.js → chunk-ELCXHPAD.js} +2 -2
  6. package/dist/{chunk-NDD7ANXZ.js → chunk-GQ2UH554.js} +122 -4
  7. package/dist/chunk-GQ2UH554.js.map +1 -0
  8. package/dist/{chunk-LKNI6QEP.js → chunk-MQAJWR3K.js} +1 -1
  9. package/dist/chunk-MQAJWR3K.js.map +1 -0
  10. package/dist/chunk-RC4V75DI.js +257 -0
  11. package/dist/chunk-RC4V75DI.js.map +1 -0
  12. package/dist/{exception-filter-chain-BCSQ3MZ2.js → exception-filter-chain-O45FXGEB.js} +3 -3
  13. package/dist/index.d.ts +280 -5
  14. package/dist/index.js +196 -18
  15. package/dist/index.js.map +1 -1
  16. package/dist/interceptor-chain-ELSL6KZT.js +9 -0
  17. package/dist/{middleware-consumer-ljxK1fU_.d.ts → middleware-consumer-DcaksawH.d.ts} +1 -1
  18. package/dist/theokit-plugin.d.ts +1 -1
  19. package/dist/theokit-plugin.js +5 -165
  20. package/dist/theokit-plugin.js.map +1 -1
  21. package/package.json +19 -17
  22. package/dist/chunk-LKNI6QEP.js.map +0 -1
  23. package/dist/chunk-LWCNTZN6.js +0 -87
  24. package/dist/chunk-LWCNTZN6.js.map +0 -1
  25. package/dist/chunk-NDD7ANXZ.js.map +0 -1
  26. package/dist/interceptor-chain-6S3PUV7J.js +0 -9
  27. /package/dist/{chunk-34KOKJ5M.js.map → chunk-6W4T4DPJ.js.map} +0 -0
  28. /package/dist/{chunk-U46H4CGF.js.map → chunk-ELCXHPAD.js.map} +0 -0
  29. /package/dist/{exception-filter-chain-BCSQ3MZ2.js.map → exception-filter-chain-O45FXGEB.js.map} +0 -0
  30. /package/dist/{interceptor-chain-6S3PUV7J.js.map → interceptor-chain-ELSL6KZT.js.map} +0 -0
@@ -0,0 +1,257 @@
1
+ import {
2
+ HttpDecoratorsConfigError
3
+ } from "./chunk-QGB5YC4T.js";
4
+ import {
5
+ resolveOrNew
6
+ } from "./chunk-MQAJWR3K.js";
7
+ import {
8
+ __name
9
+ } from "./chunk-7QVYU63E.js";
10
+
11
+ // src/bridge/middleware-consumer.ts
12
+ var MiddlewareConsumerImpl = class {
13
+ static {
14
+ __name(this, "MiddlewareConsumerImpl");
15
+ }
16
+ entries = [];
17
+ container;
18
+ constructor(container) {
19
+ this.container = container;
20
+ }
21
+ apply(...middleware) {
22
+ const resolved = middleware.map((m) => this.resolveMiddleware(m));
23
+ const excludePatterns = [];
24
+ const proxy = {
25
+ exclude: /* @__PURE__ */ __name((...routes) => {
26
+ for (const r of routes) excludePatterns.push(typeof r === "string" ? r : r.path);
27
+ return proxy;
28
+ }, "exclude"),
29
+ forRoutes: /* @__PURE__ */ __name((...routes) => {
30
+ const routePatterns = routes.map((r) => typeof r === "string" ? r : r.path);
31
+ for (const handler of resolved) {
32
+ this.entries.push({
33
+ handler,
34
+ routePatterns: [
35
+ ...routePatterns
36
+ ],
37
+ excludePatterns: [
38
+ ...excludePatterns
39
+ ]
40
+ });
41
+ }
42
+ return this;
43
+ }, "forRoutes")
44
+ };
45
+ return proxy;
46
+ }
47
+ getEntries() {
48
+ return this.entries;
49
+ }
50
+ resolveMiddleware(m) {
51
+ if (typeof m === "function" && !m.prototype?.use) {
52
+ return m;
53
+ }
54
+ const instance = resolveOrNew(m, this.container);
55
+ return (request, next) => instance.use(request, next);
56
+ }
57
+ };
58
+ function middlewareMatchesPath(entry, requestPath) {
59
+ for (const pattern of entry.excludePatterns) {
60
+ if (pathMatches(requestPath, pattern)) return false;
61
+ }
62
+ for (const pattern of entry.routePatterns) {
63
+ if (pathMatches(requestPath, pattern)) return true;
64
+ }
65
+ return false;
66
+ }
67
+ __name(middlewareMatchesPath, "middlewareMatchesPath");
68
+ function pathMatches(requestPath, pattern) {
69
+ if (pattern === "*") return true;
70
+ const normPath = ("/" + requestPath).replace(/\/+/g, "/").replace(/\/$/, "");
71
+ const normPattern = ("/" + pattern).replace(/\/+/g, "/").replace(/\/$/, "");
72
+ return normPath === normPattern || normPath.startsWith(normPattern + "/");
73
+ }
74
+ __name(pathMatches, "pathMatches");
75
+ async function runMiddleware(entries, request, requestPath) {
76
+ for (const entry of entries) {
77
+ if (!middlewareMatchesPath(entry, requestPath)) continue;
78
+ const response = await entry.handler(request, () => Promise.resolve(null));
79
+ if (response) return response;
80
+ }
81
+ return null;
82
+ }
83
+ __name(runMiddleware, "runMiddleware");
84
+
85
+ // src/bridge/swc-loader.ts
86
+ import "reflect-metadata";
87
+ import { readFileSync, writeFileSync, unlinkSync, readdirSync } from "fs";
88
+ import { createRequire } from "module";
89
+ import { resolve, dirname, join, relative } from "path";
90
+ import { pathToFileURL, fileURLToPath } from "url";
91
+ function getSwcrcTarget(swcrc) {
92
+ if (!swcrc) return "es2022";
93
+ const jsc = swcrc.jsc;
94
+ if (typeof jsc !== "object" || jsc === null) return "es2022";
95
+ const target = jsc.target;
96
+ return typeof target === "string" ? target : "es2022";
97
+ }
98
+ __name(getSwcrcTarget, "getSwcrcTarget");
99
+ var consumerSwcrcCache;
100
+ function readConsumerSwcrc(startDir) {
101
+ if (consumerSwcrcCache !== void 0) return consumerSwcrcCache;
102
+ let dir = startDir;
103
+ const root = dirname(dir);
104
+ while (dir !== root) {
105
+ const candidate = join(dir, ".swcrc");
106
+ try {
107
+ const raw = readFileSync(candidate, "utf-8");
108
+ const parsed = JSON.parse(raw);
109
+ delete parsed.$schema;
110
+ consumerSwcrcCache = parsed;
111
+ return parsed;
112
+ } catch {
113
+ }
114
+ const parent = dirname(dir);
115
+ if (parent === dir) break;
116
+ dir = parent;
117
+ }
118
+ consumerSwcrcCache = null;
119
+ return null;
120
+ }
121
+ __name(readConsumerSwcrc, "readConsumerSwcrc");
122
+ var swcCoreCache;
123
+ async function loadSwcCore() {
124
+ if (swcCoreCache !== void 0) return swcCoreCache;
125
+ try {
126
+ swcCoreCache = await import("@swc/core");
127
+ } catch {
128
+ try {
129
+ const thisDir = dirname(fileURLToPath(import.meta.url));
130
+ const req = createRequire(resolve(thisDir, "index.js"));
131
+ const resolved = req.resolve("@swc/core");
132
+ swcCoreCache = await import(pathToFileURL(resolved).href);
133
+ } catch {
134
+ swcCoreCache = null;
135
+ }
136
+ }
137
+ return swcCoreCache;
138
+ }
139
+ __name(loadSwcCore, "loadSwcCore");
140
+ async function transformControllerSource(source, filename, loadSwc = loadSwcCore) {
141
+ const swc = await loadSwc();
142
+ if (!swc) {
143
+ throw new HttpDecoratorsConfigError(`@swc/core is required for parameter decorators (@Body, @Param, @Query). Install it:
144
+
145
+ pnpm add -D @swc/core
146
+
147
+ esbuild (used by tsx/Vite) cannot parse parameter decorators. SWC handles them correctly with full metadata emission.`);
148
+ }
149
+ const consumerSwcrc = readConsumerSwcrc(dirname(filename));
150
+ const consumerJsc = consumerSwcrc?.jsc && typeof consumerSwcrc.jsc === "object" ? consumerSwcrc.jsc : null;
151
+ const { code } = swc.transformSync(source, {
152
+ filename,
153
+ jsc: {
154
+ parser: {
155
+ syntax: "typescript",
156
+ decorators: true,
157
+ ...consumerJsc?.parser
158
+ },
159
+ transform: {
160
+ ...consumerJsc?.transform,
161
+ // NON-NEGOTIABLE — always enforce decorator metadata emission
162
+ // regardless of consumer .swcrc overrides
163
+ legacyDecorator: true,
164
+ decoratorMetadata: true
165
+ },
166
+ target: getSwcrcTarget(consumerSwcrc)
167
+ },
168
+ module: {
169
+ type: "es6"
170
+ },
171
+ sourceMaps: false
172
+ });
173
+ return code;
174
+ }
175
+ __name(transformControllerSource, "transformControllerSource");
176
+ async function loadControllerWithSwc(absoluteFilePath) {
177
+ const source = readFileSync(absoluteFilePath, "utf-8");
178
+ const code = await transformControllerSource(source, absoluteFilePath);
179
+ const tmpPath = absoluteFilePath.replace(/\.ts$/, ".__decorated__.mjs");
180
+ writeFileSync(tmpPath, code, "utf-8");
181
+ try {
182
+ const url = pathToFileURL(tmpPath).href + `?t=${Date.now()}`;
183
+ return await import(url);
184
+ } finally {
185
+ try {
186
+ unlinkSync(tmpPath);
187
+ } catch {
188
+ }
189
+ }
190
+ }
191
+ __name(loadControllerWithSwc, "loadControllerWithSwc");
192
+ async function loadControllersFromGlob(rootDir, pattern) {
193
+ const files = scanControllerFiles(rootDir, pattern);
194
+ if (files.length === 0) {
195
+ console.warn(`[@theokit/http] No controller files found matching "${pattern}" in ${rootDir}. Ensure files match the pattern and export @Controller classes.`);
196
+ return [];
197
+ }
198
+ const controllers = [];
199
+ for (const file of files) {
200
+ const absPath = resolve(rootDir, file);
201
+ const mod = await loadControllerWithSwc(absPath);
202
+ for (const exported of Object.values(mod)) {
203
+ if (typeof exported === "function" && isControllerClass(exported)) {
204
+ controllers.push(exported);
205
+ }
206
+ }
207
+ }
208
+ return controllers;
209
+ }
210
+ __name(loadControllersFromGlob, "loadControllersFromGlob");
211
+ function isControllerClass(fn) {
212
+ try {
213
+ return Reflect.hasMetadata(/* @__PURE__ */ Symbol.for("theokit:http-decorators:controller-prefix"), fn);
214
+ } catch {
215
+ return false;
216
+ }
217
+ }
218
+ __name(isControllerClass, "isControllerClass");
219
+ function scanControllerFiles(rootDir, pattern) {
220
+ const parts = pattern.split("/");
221
+ const dirParts = [];
222
+ for (const part of parts) {
223
+ if (part.includes("*")) break;
224
+ dirParts.push(part);
225
+ }
226
+ const baseDir = join(rootDir, ...dirParts);
227
+ const lastPart = parts[parts.length - 1];
228
+ const suffix = lastPart.replace(/\*/g, "");
229
+ const files = [];
230
+ function walk(dir) {
231
+ try {
232
+ for (const entry of readdirSync(dir, {
233
+ withFileTypes: true
234
+ })) {
235
+ const full = join(dir, entry.name);
236
+ if (entry.isDirectory()) walk(full);
237
+ else if (entry.name.endsWith(suffix)) files.push(relative(rootDir, full));
238
+ }
239
+ } catch {
240
+ }
241
+ }
242
+ __name(walk, "walk");
243
+ walk(baseDir);
244
+ return files;
245
+ }
246
+ __name(scanControllerFiles, "scanControllerFiles");
247
+
248
+ export {
249
+ MiddlewareConsumerImpl,
250
+ middlewareMatchesPath,
251
+ runMiddleware,
252
+ transformControllerSource,
253
+ loadControllerWithSwc,
254
+ loadControllersFromGlob,
255
+ isControllerClass
256
+ };
257
+ //# sourceMappingURL=chunk-RC4V75DI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bridge/middleware-consumer.ts","../src/bridge/swc-loader.ts"],"sourcesContent":["/**\n * NestJS-style Middleware support — Web Standard Request/Response.\n *\n * Middleware runs BEFORE guards: middleware → guards → interceptors → handler\n *\n * Supports class middleware (NestMiddleware) and functional middleware.\n * Returns a Response to short-circuit, or null to continue.\n */\nimport { resolveOrNew, type DiContainer } from './di-resolve.js'\n\n/** NestJS-compatible middleware interface — Web Standard. */\nexport interface NestMiddleware {\n use(\n request: Request,\n next: () => Promise<Response | null>,\n ): Response | null | Promise<Response | null>\n}\n\n/** Functional middleware. */\nexport type MiddlewareFn = (\n request: Request,\n next: () => Promise<Response | null>,\n) => Response | null | Promise<Response | null>\n\nexport interface ResolvedMiddleware {\n handler: MiddlewareFn\n routePatterns: string[]\n excludePatterns: string[]\n}\n\nexport type RouteInfo = string | { path: string; method?: string }\n\nexport interface MiddlewareConfigProxy {\n forRoutes(...routes: RouteInfo[]): MiddlewareConsumerImpl\n exclude(...routes: RouteInfo[]): MiddlewareConfigProxy\n}\n\nexport class MiddlewareConsumerImpl {\n private entries: ResolvedMiddleware[] = []\n private container?: DiContainer\n\n constructor(container?: DiContainer) {\n this.container = container\n }\n\n apply(...middleware: (Function | MiddlewareFn)[]): MiddlewareConfigProxy {\n const resolved = middleware.map((m) => this.resolveMiddleware(m))\n const excludePatterns: string[] = []\n\n const proxy: MiddlewareConfigProxy = {\n exclude: (...routes: RouteInfo[]) => {\n for (const r of routes) excludePatterns.push(typeof r === 'string' ? r : r.path)\n return proxy\n },\n forRoutes: (...routes: RouteInfo[]) => {\n const routePatterns = routes.map((r) => (typeof r === 'string' ? r : r.path))\n for (const handler of resolved) {\n this.entries.push({ handler, routePatterns: [...routePatterns], excludePatterns: [...excludePatterns] })\n }\n return this\n },\n }\n return proxy\n }\n\n getEntries(): ResolvedMiddleware[] {\n return this.entries\n }\n\n private resolveMiddleware(m: Function | MiddlewareFn): MiddlewareFn {\n if (typeof m === 'function' && !m.prototype?.use) {\n return m as MiddlewareFn\n }\n const instance = resolveOrNew(m, this.container) as NestMiddleware\n return (request, next) => instance.use(request, next)\n }\n}\n\nexport function middlewareMatchesPath(entry: ResolvedMiddleware, requestPath: string): boolean {\n for (const pattern of entry.excludePatterns) {\n if (pathMatches(requestPath, pattern)) return false\n }\n for (const pattern of entry.routePatterns) {\n if (pathMatches(requestPath, pattern)) return true\n }\n return false\n}\n\nfunction pathMatches(requestPath: string, pattern: string): boolean {\n if (pattern === '*') return true\n const normPath = ('/' + requestPath).replace(/\\/+/g, '/').replace(/\\/$/, '')\n const normPattern = ('/' + pattern).replace(/\\/+/g, '/').replace(/\\/$/, '')\n return normPath === normPattern || normPath.startsWith(normPattern + '/')\n}\n\n/**\n * Run all matching middleware. Returns a Response if any short-circuited, null otherwise.\n */\nexport async function runMiddleware(\n entries: ResolvedMiddleware[],\n request: Request,\n requestPath: string,\n): Promise<Response | null> {\n for (const entry of entries) {\n if (!middlewareMatchesPath(entry, requestPath)) continue\n const response = await entry.handler(request, () => Promise.resolve(null))\n if (response) return response // short-circuit\n }\n return null\n}\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\nexport interface 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 * Transform TypeScript controller SOURCE (with parameter decorators) into\n * ESM code, emitting the `design:paramtypes` / decorator metadata that esbuild\n * cannot produce. Pure code→code — no file I/O, no module load — so it is\n * reusable both by {@link loadControllerWithSwc} (which then temp-writes +\n * imports) and by a build-tool transform hook that returns `{ code }` directly.\n *\n * The `@swc/core` loader is injectable (`loadSwc`) for testability; it defaults\n * to the cached singleton.\n *\n * @throws HttpDecoratorsConfigError when @swc/core is unavailable.\n */\nexport async function transformControllerSource(\n source: string,\n filename: string,\n loadSwc: () => Promise<SwcCore | null> = loadSwcCore,\n): Promise<string> {\n const swc = await loadSwc()\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 consumerSwcrc = readConsumerSwcrc(dirname(filename))\n const consumerJsc =\n consumerSwcrc?.jsc && typeof consumerSwcrc.jsc === 'object'\n ? (consumerSwcrc.jsc as Record<string, unknown>)\n : null\n const { code } = swc.transformSync(source, {\n filename,\n jsc: {\n parser: {\n syntax: 'typescript',\n decorators: true,\n ...(consumerJsc?.parser as Record<string, unknown> | undefined),\n },\n transform: {\n ...(consumerJsc?.transform as Record<string, unknown> | undefined),\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 return code\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 {@link transformControllerSource} (legacyDecorator + metadata)\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 source = readFileSync(absoluteFilePath, 'utf-8')\n const code = await transformControllerSource(source, absoluteFilePath)\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 */\nexport function 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":["MiddlewareConsumerImpl","entries","container","apply","middleware","resolved","map","m","resolveMiddleware","excludePatterns","proxy","exclude","routes","r","push","path","forRoutes","routePatterns","handler","getEntries","prototype","use","instance","resolveOrNew","request","next","middlewareMatchesPath","entry","requestPath","pattern","pathMatches","normPath","replace","normPattern","startsWith","runMiddleware","response","Promise","resolve","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","transformControllerSource","source","filename","loadSwc","swc","HttpDecoratorsConfigError","consumerSwcrc","consumerJsc","code","transformSync","parser","syntax","decorators","transform","legacyDecorator","decoratorMetadata","module","type","sourceMaps","loadControllerWithSwc","absoluteFilePath","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"]}
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runExceptionFilters
3
- } from "./chunk-34KOKJ5M.js";
3
+ } from "./chunk-6W4T4DPJ.js";
4
4
  import "./chunk-3PGQVQWG.js";
5
- import "./chunk-LKNI6QEP.js";
5
+ import "./chunk-MQAJWR3K.js";
6
6
  import "./chunk-7QVYU63E.js";
7
7
  export {
8
8
  runExceptionFilters
9
9
  };
10
- //# sourceMappingURL=exception-filter-chain-BCSQ3MZ2.js.map
10
+ //# sourceMappingURL=exception-filter-chain-O45FXGEB.js.map
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { z } from 'zod';
2
- import { D as DiContainer, M as MiddlewareConsumerImpl } from './middleware-consumer-ljxK1fU_.js';
3
- export { a as MiddlewareConfigProxy, b as MiddlewareFn, N as NestMiddleware, R as ResolvedMiddleware, m as middlewareMatchesPath, r as resolveOrNew, c as runMiddleware } from './middleware-consumer-ljxK1fU_.js';
2
+ import { D as DiContainer, M as MiddlewareConsumerImpl } from './middleware-consumer-DcaksawH.js';
3
+ export { a as MiddlewareConfigProxy, b as MiddlewareFn, N as NestMiddleware, R as ResolvedMiddleware, m as middlewareMatchesPath, r as resolveOrNew, c as runMiddleware } from './middleware-consumer-DcaksawH.js';
4
4
  import { S as ServerHandle } from './types-CGthbcon.js';
5
5
  export { ReadinessCheck, TheoApp, TheoAppOptions } from './app.js';
6
+ import * as ReactTypes from 'react';
6
7
 
7
8
  interface ControllerOptions {
8
9
  host?: string;
@@ -354,6 +355,63 @@ declare function joinPath(prefix: string, path: string): string;
354
355
  */
355
356
  declare function walkControllerMetadata(ControllerClass: Function): WalkResult[];
356
357
 
358
+ /**
359
+ * SWC-powered module loader for controller files with parameter decorators.
360
+ *
361
+ * esbuild (used by tsx/Vite SSR) fundamentally cannot parse TypeScript
362
+ * parameter decorators (`@Body()`, `@Param()`, `@Query()`). This loader
363
+ * uses @swc/core to transform controller files with full decorator support
364
+ * (legacyDecorator + decoratorMetadata), then imports them via a temp .mjs
365
+ * file written in the SAME directory (preserving relative import resolution).
366
+ *
367
+ * Pattern: follows Next.js's approach (read tsconfig → configure SWC)
368
+ * but scoped to the http-decorators package, not the framework core.
369
+ *
370
+ * @see references/next.js/packages/next/src/build/swc/options.ts
371
+ */
372
+
373
+ interface SwcCore {
374
+ transformSync: (src: string, opts: unknown) => {
375
+ code: string;
376
+ };
377
+ }
378
+ /**
379
+ * Transform TypeScript controller SOURCE (with parameter decorators) into
380
+ * ESM code, emitting the `design:paramtypes` / decorator metadata that esbuild
381
+ * cannot produce. Pure code→code — no file I/O, no module load — so it is
382
+ * reusable both by {@link loadControllerWithSwc} (which then temp-writes +
383
+ * imports) and by a build-tool transform hook that returns `{ code }` directly.
384
+ *
385
+ * The `@swc/core` loader is injectable (`loadSwc`) for testability; it defaults
386
+ * to the cached singleton.
387
+ *
388
+ * @throws HttpDecoratorsConfigError when @swc/core is unavailable.
389
+ */
390
+ declare function transformControllerSource(source: string, filename: string, loadSwc?: () => Promise<SwcCore | null>): Promise<string>;
391
+ /**
392
+ * Load a TypeScript controller file using @swc/core for decorator support.
393
+ *
394
+ * Strategy:
395
+ * 1. Read source .ts file
396
+ * 2. Transform via {@link transformControllerSource} (legacyDecorator + metadata)
397
+ * 3. Write temp .mjs in SAME directory (relative imports resolve correctly)
398
+ * 4. Dynamic import() the .mjs — transitive .ts imports go through
399
+ * tsx/Vite's global hook (they don't have parameter decorators)
400
+ * 5. Cleanup temp file
401
+ */
402
+ declare function loadControllerWithSwc(absoluteFilePath: string): Promise<Record<string, unknown>>;
403
+ /**
404
+ * Scan a glob pattern for controller files and load them all via SWC.
405
+ * Returns an array of controller class constructors found.
406
+ */
407
+ declare function loadControllersFromGlob(rootDir: string, pattern: string): Promise<Function[]>;
408
+ /**
409
+ * Check if a function has @Controller metadata.
410
+ * Uses Symbol.for() global registry key — same Symbol instance across
411
+ * module boundaries (SWC-loaded controllers share the global registry).
412
+ */
413
+ declare function isControllerClass(fn: Function): boolean;
414
+
357
415
  interface RouteRegistration {
358
416
  verb: HttpVerb;
359
417
  fullPath: string;
@@ -377,6 +435,25 @@ interface CreateDecoratorServerOptions {
377
435
  container?: DiContainer;
378
436
  configure?: (consumer: MiddlewareConsumerImpl) => void;
379
437
  }
438
+ /**
439
+ * A pure Web-Standard controller handler: callable as `(request) => Response | null`
440
+ * plus a non-executing `matches(method, pathname)` route probe (so a host can gate
441
+ * — e.g. CSRF — before dispatch runs a handler).
442
+ */
443
+ interface DecoratorHandler {
444
+ (request: Request): Promise<Response | null>;
445
+ /** True when a controller route owns `method` + `pathname` (no handler executed). */
446
+ matches(method: string, pathname: string): boolean;
447
+ }
448
+ /**
449
+ * Build a pure Web-Standard request handler from decorated controller classes,
450
+ * WITHOUT binding a network listener. Returns a {@link DecoratorHandler} whose
451
+ * call returns `null` when no controller route matched — the caller decides the
452
+ * miss (a standalone server answers 404; a host middleware falls through to its
453
+ * own routing). This is the reusable dispatch seam consumed by the framework's
454
+ * controller dispatch (#122) so it never re-implements match/bind/validate.
455
+ */
456
+ declare function createDecoratorHandler(controllersOrOpts: Function[] | CreateDecoratorServerOptions): DecoratorHandler;
380
457
  declare function createDecoratorServer(controllersOrOpts: Function[] | CreateDecoratorServerOptions): ServerHandle;
381
458
 
382
459
  /**
@@ -998,10 +1075,10 @@ type InferResponse<D> = D extends {
998
1075
  response: infer R;
999
1076
  } ? R : unknown;
1000
1077
  interface TypedClient<M extends RouteMap> {
1001
- get<P extends string & keyof M>(path: P, opts?: {
1078
+ get<P extends string>(path: P, opts?: {
1002
1079
  query?: Record<string, string>;
1003
1080
  headers?: Record<string, string>;
1004
- }): Promise<InferResponse<M[`GET ${P}`] extends never ? M[P] : M[`GET ${P}`]>>;
1081
+ }): Promise<InferResponse<M[`GET ${P}`]>>;
1005
1082
  post<P extends string>(path: P, body?: InferBody<M[`POST ${P}`]>, opts?: {
1006
1083
  headers?: Record<string, string>;
1007
1084
  }): Promise<InferResponse<M[`POST ${P}`]>>;
@@ -1091,4 +1168,202 @@ declare function isSafePath(pathname: string): boolean;
1091
1168
  */
1092
1169
  declare function createStaticHandler(options?: StaticOptions): (request: Request) => Promise<Response | null>;
1093
1170
 
1094
- export { All, type ArgumentsHost, BadGatewayException, BadRequestException, Body, CATCH_EXCEPTIONS, CONTROLLER_PREFIX, type CanActivate, Catch, ConflictException, Controller, type ControllerMeta, type ControllerOptions, Delete, DiContainer, type ExceptionFilter, type ExecutionContext, ForbiddenException, GatewayTimeoutException, Get, GoneException, Head, Header, Headers, HostParam, HttpCode, HttpDecoratorsConfigError, HttpException, type HttpExceptionOptions, HttpStatus, type HttpStatusCode, type HttpVerb, HttpVersionNotSupportedException, ImATeapotException, type Interceptor, InternalServerErrorException, Ip, type MetadataKey, MethodNotAllowedException, MiddlewareConsumerImpl, NotAcceptableException, NotFoundException, NotImplementedException, Options, Param, type ParamEntry, type ParamSource, Patch, PayloadTooLargeException, Post, PreconditionFailedException, Put, Query, ROUTE_HEADERS, ROUTE_METHODS, ROUTE_PARAMS, ROUTE_REDIRECT, ROUTE_STATUS, Redirect, type RedirectMeta, Reflector, Req, RequestTimeoutException, Res, type RouteDefinition, type RouteMap, type RouteMethodEntry, type RouteRegistration, ServiceUnavailableException, Session, SetMetadata, SkipThrottle, type StaticOptions, Throttle, type ThrottleOptions, TooManyRequestsException, type TypedClient, TypedClientError, USE_FILTERS, USE_GUARDS, USE_INTERCEPTORS, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, UseFilters, UseGuards, UseInterceptors, type WalkResult, contract, createDecorator, createDecoratorServer, createExecutionContext, createStaticHandler, createTypedClient, getMeta, getMimeType, getThrottleOptions, isSafePath, isThrottleSkipped, joinPath, registerControllers, resolveDtoSchema, runExceptionFilters, runInterceptors, setMeta, walkControllerMetadata };
1171
+ interface TheoRequestContext {
1172
+ /** The raw Web Standard Request object. */
1173
+ request: Request;
1174
+ /** URL pathname (e.g., '/api/tasks'). */
1175
+ pathname: string;
1176
+ /** HTTP method (e.g., 'GET', 'POST'). */
1177
+ method: string;
1178
+ /** Route params extracted from URL (e.g., { id: '42' }). */
1179
+ params: Record<string, string>;
1180
+ /** Matched controller or agent class name (if resolved). */
1181
+ handler?: string;
1182
+ /** Request start time (ms). */
1183
+ startedAt: number;
1184
+ }
1185
+ /**
1186
+ * Get the current request context.
1187
+ *
1188
+ * @throws Error if called outside a request (e.g., at module load time).
1189
+ *
1190
+ * @example
1191
+ * ```typescript
1192
+ * import { getRequestContext } from '@theokit/http'
1193
+ *
1194
+ * class AuthGuard {
1195
+ * canActivate() {
1196
+ * const { request } = getRequestContext()
1197
+ * return request.headers.get('authorization') !== null
1198
+ * }
1199
+ * }
1200
+ * ```
1201
+ */
1202
+ declare function getRequestContext(): TheoRequestContext;
1203
+ /**
1204
+ * Try to get the current request context, or null if not in a request.
1205
+ * Useful for code that may run both inside and outside requests.
1206
+ */
1207
+ declare function tryGetRequestContext(): TheoRequestContext | null;
1208
+
1209
+ /**
1210
+ * Error digestion — converts any thrown value into a stable hash + context.
1211
+ *
1212
+ * Inspired by Next.js `create-error-handler.tsx`. Produces a deterministic
1213
+ * digest ID suitable for logging and client-safe error references without
1214
+ * leaking stack traces in production.
1215
+ *
1216
+ * Uses djb2 hash (sync, no crypto dependency) per ADR D3.
1217
+ */
1218
+ interface ErrorContext {
1219
+ route?: string;
1220
+ phase?: 'guard' | 'interceptor' | 'handler' | 'filter' | 'agent';
1221
+ source?: string;
1222
+ }
1223
+ interface DigestedError {
1224
+ digest: string;
1225
+ message: string;
1226
+ status: number;
1227
+ context: ErrorContext;
1228
+ stack?: string;
1229
+ }
1230
+ /**
1231
+ * Converts any thrown value into a structured {@link DigestedError}.
1232
+ *
1233
+ * - Sync (never async) — safe to call inside catch blocks.
1234
+ * - Stack trace stripped when `process.env.NODE_ENV === 'production'`.
1235
+ * - Preserves {@link HttpException} status codes.
1236
+ * - Handles non-Error throws (string, number, object).
1237
+ */
1238
+ declare function digestError(err: unknown, context?: ErrorContext): DigestedError;
1239
+
1240
+ /**
1241
+ * Component tree composition — recursive wrapping of file-convention
1242
+ * components (layout, page, loading, error, not-found) into a React
1243
+ * element tree with Suspense and error boundaries.
1244
+ *
1245
+ * Inspired by Next.js `create-component-tree.tsx`.
1246
+ *
1247
+ * React is loaded via dynamic `import('react')` because it is an
1248
+ * optional peerDep of @theokit/http (EC-2).
1249
+ */
1250
+
1251
+ interface RouteTree {
1252
+ layout?: ReactTypes.ComponentType<{
1253
+ children: ReactTypes.ReactNode;
1254
+ }>;
1255
+ page?: ReactTypes.ComponentType;
1256
+ loading?: ReactTypes.ComponentType;
1257
+ error?: ReactTypes.ComponentType;
1258
+ notFound?: ReactTypes.ComponentType;
1259
+ children?: Record<string, RouteTree>;
1260
+ }
1261
+ /**
1262
+ * Composes a {@link RouteTree} into a nested React element tree.
1263
+ *
1264
+ * Wrapping order (outermost → innermost):
1265
+ * layout → ErrorBoundary(error) → Suspense(loading) → page
1266
+ *
1267
+ * Returns `null` when no `page` component is found in the tree.
1268
+ *
1269
+ * @param tree - The route tree describing file conventions found.
1270
+ * @returns A React element or `null`.
1271
+ */
1272
+ declare function composeComponentTree(tree: RouteTree): Promise<ReactTypes.ReactElement | null>;
1273
+
1274
+ /**
1275
+ * Streaming SSR — renders a React element tree to a `ReadableStream<Uint8Array>`.
1276
+ *
1277
+ * Inspired by Next.js `stream-ops.ts`. Uses Web Standard `renderToReadableStream`
1278
+ * (works on Node 18+, Bun, Deno). Falls back to `renderToString` wrapped in a
1279
+ * ReadableStream when `renderToReadableStream` is not available (React 17 — EC-4).
1280
+ *
1281
+ * React is loaded via dynamic `import('react-dom/server')` because react-dom
1282
+ * is an optional peerDep of @theokit/http (ADR D2).
1283
+ */
1284
+
1285
+ interface StreamRenderOptions {
1286
+ /** React element to render */
1287
+ root: ReactTypes.ReactElement;
1288
+ /** Whether to wait for all Suspense to resolve (default: false — stream immediately) */
1289
+ waitForAll?: boolean;
1290
+ }
1291
+ interface StreamRenderResult {
1292
+ /** The HTML stream */
1293
+ stream: ReadableStream<Uint8Array>;
1294
+ /** Promise that resolves when all content has been flushed */
1295
+ allReady: Promise<void>;
1296
+ }
1297
+ /**
1298
+ * Renders a React element to a `ReadableStream<Uint8Array>` (Web Standard).
1299
+ *
1300
+ * 1. Tries `renderToReadableStream` first (React 18+ — Web Standard API).
1301
+ * 2. Falls back to `renderToString` wrapped in a ReadableStream when
1302
+ * `renderToReadableStream` is not available (React 17 compat — EC-4).
1303
+ * 3. Prepends `<!DOCTYPE html>` to the stream.
1304
+ *
1305
+ * **EC-7 — Streaming error handling:** In streaming mode, errors thrown inside
1306
+ * Suspense boundaries are caught by React's streaming error handler and result
1307
+ * in a client-side error boundary activation (the shell is already sent). In
1308
+ * string mode (`renderToString`), errors throw synchronously before any bytes
1309
+ * are sent, allowing a full 500 error page. Choose streaming when you want
1310
+ * progressive rendering; choose string mode when you want atomic error handling.
1311
+ */
1312
+ declare function renderToStream(options: StreamRenderOptions): Promise<StreamRenderResult>;
1313
+ /**
1314
+ * Converts a {@link StreamRenderResult} into a Web Standard `Response`.
1315
+ *
1316
+ * Convenience wrapper for use in request handlers:
1317
+ * ```ts
1318
+ * const result = await renderToStream({ root: <App /> })
1319
+ * return streamToResponse(result)
1320
+ * ```
1321
+ */
1322
+ declare function streamToResponse(result: StreamRenderResult): Response;
1323
+
1324
+ interface RevalidationSignal {
1325
+ kind: 'tag' | 'path';
1326
+ value: string;
1327
+ timestamp: number;
1328
+ }
1329
+ /**
1330
+ * Signal that a cache tag should be revalidated.
1331
+ *
1332
+ * Safe to call from any request handler (controller, agent, action).
1333
+ * Signals are accumulated per-request and consumed by the cache engine.
1334
+ *
1335
+ * @example
1336
+ * ```typescript
1337
+ * import { revalidateTag } from '@theokit/http'
1338
+ *
1339
+ * @Post()
1340
+ * async createTask(@Body(schema) body) {
1341
+ * const task = await db.tasks.create(body)
1342
+ * revalidateTag('tasks') // invalidate cached task lists
1343
+ * return task
1344
+ * }
1345
+ * ```
1346
+ */
1347
+ declare function revalidateTag(tag: string): void;
1348
+ /**
1349
+ * Signal that a cached path should be revalidated.
1350
+ *
1351
+ * @example
1352
+ * ```typescript
1353
+ * import { revalidatePath } from '@theokit/http'
1354
+ *
1355
+ * @Delete(':id')
1356
+ * async removeTask(@Param('id') id: string) {
1357
+ * await db.tasks.delete(id)
1358
+ * revalidatePath('/api/tasks')
1359
+ * }
1360
+ * ```
1361
+ */
1362
+ declare function revalidatePath(path: string): void;
1363
+ /**
1364
+ * Get all revalidation signals collected during the current request.
1365
+ * Called by the cache engine after the handler completes.
1366
+ */
1367
+ declare function getRevalidationSignals(): RevalidationSignal[];
1368
+
1369
+ export { All, type ArgumentsHost, BadGatewayException, BadRequestException, Body, CATCH_EXCEPTIONS, CONTROLLER_PREFIX, type CanActivate, Catch, ConflictException, Controller, type ControllerMeta, type ControllerOptions, type DecoratorHandler, Delete, DiContainer, type DigestedError, type ErrorContext, type ExceptionFilter, type ExecutionContext, ForbiddenException, GatewayTimeoutException, Get, GoneException, Head, Header, Headers, HostParam, HttpCode, HttpDecoratorsConfigError, HttpException, type HttpExceptionOptions, HttpStatus, type HttpStatusCode, type HttpVerb, HttpVersionNotSupportedException, ImATeapotException, type Interceptor, InternalServerErrorException, Ip, type MetadataKey, MethodNotAllowedException, MiddlewareConsumerImpl, NotAcceptableException, NotFoundException, NotImplementedException, Options, Param, type ParamEntry, type ParamSource, Patch, PayloadTooLargeException, Post, PreconditionFailedException, Put, Query, ROUTE_HEADERS, ROUTE_METHODS, ROUTE_PARAMS, ROUTE_REDIRECT, ROUTE_STATUS, Redirect, type RedirectMeta, Reflector, Req, RequestTimeoutException, Res, type RevalidationSignal, type RouteDefinition, type RouteMap, type RouteMethodEntry, type RouteRegistration, type RouteTree, ServiceUnavailableException, Session, SetMetadata, SkipThrottle, type StaticOptions, type StreamRenderOptions, type StreamRenderResult, type SwcCore, type TheoRequestContext, Throttle, type ThrottleOptions, TooManyRequestsException, type TypedClient, TypedClientError, USE_FILTERS, USE_GUARDS, USE_INTERCEPTORS, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, UseFilters, UseGuards, UseInterceptors, type WalkResult, composeComponentTree, contract, createDecorator, createDecoratorHandler, createDecoratorServer, createExecutionContext, createStaticHandler, createTypedClient, digestError, getMeta, getMimeType, getRequestContext, getRevalidationSignals, getThrottleOptions, isControllerClass, isSafePath, isThrottleSkipped, joinPath, loadControllerWithSwc, loadControllersFromGlob, registerControllers, renderToStream, resolveDtoSchema, revalidatePath, revalidateTag, runExceptionFilters, runInterceptors, setMeta, streamToResponse, transformControllerSource, tryGetRequestContext, walkControllerMetadata };