@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.
- package/LICENSE +201 -0
- package/dist/app.d.ts +16 -0
- package/dist/app.js +1 -1
- package/dist/{chunk-34KOKJ5M.js → chunk-6W4T4DPJ.js} +2 -2
- package/dist/{chunk-U46H4CGF.js → chunk-ELCXHPAD.js} +2 -2
- package/dist/{chunk-NDD7ANXZ.js → chunk-GQ2UH554.js} +122 -4
- package/dist/chunk-GQ2UH554.js.map +1 -0
- package/dist/{chunk-LKNI6QEP.js → chunk-MQAJWR3K.js} +1 -1
- package/dist/chunk-MQAJWR3K.js.map +1 -0
- package/dist/chunk-RC4V75DI.js +257 -0
- package/dist/chunk-RC4V75DI.js.map +1 -0
- package/dist/{exception-filter-chain-BCSQ3MZ2.js → exception-filter-chain-O45FXGEB.js} +3 -3
- package/dist/index.d.ts +280 -5
- package/dist/index.js +196 -18
- package/dist/index.js.map +1 -1
- package/dist/interceptor-chain-ELSL6KZT.js +9 -0
- package/dist/{middleware-consumer-ljxK1fU_.d.ts → middleware-consumer-DcaksawH.d.ts} +1 -1
- package/dist/theokit-plugin.d.ts +1 -1
- package/dist/theokit-plugin.js +5 -165
- package/dist/theokit-plugin.js.map +1 -1
- package/package.json +19 -17
- package/dist/chunk-LKNI6QEP.js.map +0 -1
- package/dist/chunk-LWCNTZN6.js +0 -87
- package/dist/chunk-LWCNTZN6.js.map +0 -1
- package/dist/chunk-NDD7ANXZ.js.map +0 -1
- package/dist/interceptor-chain-6S3PUV7J.js +0 -9
- /package/dist/{chunk-34KOKJ5M.js.map → chunk-6W4T4DPJ.js.map} +0 -0
- /package/dist/{chunk-U46H4CGF.js.map → chunk-ELCXHPAD.js.map} +0 -0
- /package/dist/{exception-filter-chain-BCSQ3MZ2.js.map → exception-filter-chain-O45FXGEB.js.map} +0 -0
- /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-
|
|
3
|
+
} from "./chunk-6W4T4DPJ.js";
|
|
4
4
|
import "./chunk-3PGQVQWG.js";
|
|
5
|
-
import "./chunk-
|
|
5
|
+
import "./chunk-MQAJWR3K.js";
|
|
6
6
|
import "./chunk-7QVYU63E.js";
|
|
7
7
|
export {
|
|
8
8
|
runExceptionFilters
|
|
9
9
|
};
|
|
10
|
-
//# sourceMappingURL=exception-filter-chain-
|
|
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-
|
|
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-
|
|
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
|
|
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}`]
|
|
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
|
-
|
|
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 };
|