@rexeus/typeweaver-server 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +3 -2
- package/dist/index.mjs +4 -3
- package/dist/index.mjs.map +1 -1
- package/dist/lib/BodyLimitPolicy.ts +111 -0
- package/dist/lib/Errors.ts +16 -0
- package/dist/lib/FetchApiAdapter.ts +49 -21
- package/dist/lib/NodeAdapter.ts +256 -34
- package/dist/lib/TypeweaverApp.ts +10 -4
- package/dist/lib/TypeweaverAppRuntime.ts +37 -0
- package/dist/lib/TypeweaverInternals.ts +45 -0
- package/dist/lib/index.ts +1 -0
- package/package.json +8 -5
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,7 @@ node_path = __toESM(node_path);
|
|
|
26
26
|
let node_url = require("node:url");
|
|
27
27
|
let _rexeus_typeweaver_gen = require("@rexeus/typeweaver-gen");
|
|
28
28
|
let _rexeus_typeweaver_core = require("@rexeus/typeweaver-core");
|
|
29
|
+
let polycase = require("polycase");
|
|
29
30
|
//#region src/routerGenerator.ts
|
|
30
31
|
/**
|
|
31
32
|
* Generates TypeweaverRouter subclasses from API definitions.
|
|
@@ -44,7 +45,7 @@ function generate(context) {
|
|
|
44
45
|
for (const resource of context.normalizedSpec.resources) writeRouter(resource, templateFile, context);
|
|
45
46
|
}
|
|
46
47
|
function writeRouter(resource, templateFile, context) {
|
|
47
|
-
const pascalCaseEntityName = (0,
|
|
48
|
+
const pascalCaseEntityName = (0, polycase.pascalCase)(resource.name);
|
|
48
49
|
const outputDir = context.getResourceOutputDir(resource.name);
|
|
49
50
|
const outputPath = node_path.default.join(outputDir, `${pascalCaseEntityName}Router.ts`);
|
|
50
51
|
const operations = resource.operations.filter((operation) => operation.method !== _rexeus_typeweaver_core.HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => (0, _rexeus_typeweaver_gen.compareRoutes)(a, b));
|
|
@@ -59,7 +60,7 @@ function writeRouter(resource, templateFile, context) {
|
|
|
59
60
|
}
|
|
60
61
|
function createOperationData(operation) {
|
|
61
62
|
const operationId = operation.operationId;
|
|
62
|
-
const className = (0,
|
|
63
|
+
const className = (0, polycase.pascalCase)(operationId);
|
|
63
64
|
return {
|
|
64
65
|
operationId,
|
|
65
66
|
className,
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { BasePlugin, compareRoutes, relative
|
|
3
|
+
import { BasePlugin, compareRoutes, relative } from "@rexeus/typeweaver-gen";
|
|
4
4
|
import { HttpMethod } from "@rexeus/typeweaver-core";
|
|
5
|
+
import { pascalCase } from "polycase";
|
|
5
6
|
//#region src/routerGenerator.ts
|
|
6
7
|
/**
|
|
7
8
|
* Generates TypeweaverRouter subclasses from API definitions.
|
|
@@ -20,7 +21,7 @@ function generate(context) {
|
|
|
20
21
|
for (const resource of context.normalizedSpec.resources) writeRouter(resource, templateFile, context);
|
|
21
22
|
}
|
|
22
23
|
function writeRouter(resource, templateFile, context) {
|
|
23
|
-
const pascalCaseEntityName =
|
|
24
|
+
const pascalCaseEntityName = pascalCase(resource.name);
|
|
24
25
|
const outputDir = context.getResourceOutputDir(resource.name);
|
|
25
26
|
const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);
|
|
26
27
|
const operations = resource.operations.filter((operation) => operation.method !== HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => compareRoutes(a, b));
|
|
@@ -35,7 +36,7 @@ function writeRouter(resource, templateFile, context) {
|
|
|
35
36
|
}
|
|
36
37
|
function createOperationData(operation) {
|
|
37
38
|
const operationId = operation.operationId;
|
|
38
|
-
const className =
|
|
39
|
+
const className = pascalCase(operationId);
|
|
39
40
|
return {
|
|
40
41
|
operationId,
|
|
41
42
|
className,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/routerGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { compareRoutes, relative
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/routerGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { compareRoutes, relative } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n NormalizedOperation,\n NormalizedResource,\n} from \"@rexeus/typeweaver-gen\";\nimport { pascalCase } from \"polycase\";\n\ntype OperationData = {\n readonly operationId: string;\n readonly className: string;\n readonly handlerName: string;\n readonly method: string;\n readonly path: string;\n};\n\n/**\n * Generates TypeweaverRouter subclasses from API definitions.\n *\n * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`\n * file that extends `TypeweaverRouter` and registers all operations as routes.\n */\n\n/**\n * Generates router files for all resources in the given context.\n *\n * @param context - The generator context containing resources, templates, and output configuration\n */\nexport function generate(context: GeneratorContext): void {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n const templateFile = path.join(moduleDir, \"templates\", \"Router.ejs\");\n\n for (const resource of context.normalizedSpec.resources) {\n writeRouter(resource, templateFile, context);\n }\n}\n\nfunction writeRouter(\n resource: NormalizedResource,\n templateFile: string,\n context: GeneratorContext\n): void {\n const pascalCaseEntityName = pascalCase(resource.name);\n const outputDir = context.getResourceOutputDir(resource.name);\n const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);\n\n const operations = resource.operations\n .filter(operation => operation.method !== HttpMethod.HEAD)\n .map(operation => createOperationData(operation))\n .sort((a, b) => compareRoutes(a, b));\n\n const content = context.renderTemplate(templateFile, {\n coreDir: relative(outputDir, context.outputDir),\n entityName: resource.name,\n pascalCaseEntityName,\n operations,\n });\n\n const relativePath = path.relative(context.outputDir, outputPath);\n context.writeFile(relativePath, content);\n}\n\nfunction createOperationData(operation: NormalizedOperation): OperationData {\n const operationId = operation.operationId;\n const className = pascalCase(operationId);\n\n return {\n operationId,\n className,\n handlerName: `handle${className}Request`,\n method: operation.method,\n path: operation.path,\n };\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { BasePlugin } from \"@rexeus/typeweaver-gen\";\nimport type { GeneratorContext } from \"@rexeus/typeweaver-gen\";\nimport { generate as generateRouters } from \"./routerGenerator.js\";\n\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Typeweaver plugin that generates a lightweight, dependency-free server\n * with built-in routing and middleware support.\n *\n * Copies the runtime library files (`TypeweaverApp`, `TypeweaverRouter`, `Router`,\n * `Middleware`, etc.) and generates typed router classes for each resource.\n */\nexport class ServerPlugin extends BasePlugin {\n public name = \"server\";\n public override depends = [\"types\"];\n\n /**\n * Generates the server runtime and typed routers for all resources.\n *\n * @param context - The generator context\n */\n public override generate(context: GeneratorContext): void {\n const libSourceDir = path.join(moduleDir, \"lib\");\n this.copyLibFiles(context, libSourceDir, this.name);\n\n generateRouters(context);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA+BA,SAAgB,SAAS,SAAiC;CACxD,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CAC9D,MAAM,eAAe,KAAK,KAAK,WAAW,aAAa,aAAa;AAEpE,MAAK,MAAM,YAAY,QAAQ,eAAe,UAC5C,aAAY,UAAU,cAAc,QAAQ;;AAIhD,SAAS,YACP,UACA,cACA,SACM;CACN,MAAM,uBAAuB,WAAW,SAAS,KAAK;CACtD,MAAM,YAAY,QAAQ,qBAAqB,SAAS,KAAK;CAC7D,MAAM,aAAa,KAAK,KAAK,WAAW,GAAG,qBAAqB,WAAW;CAE3E,MAAM,aAAa,SAAS,WACzB,QAAO,cAAa,UAAU,WAAW,WAAW,KAAK,CACzD,KAAI,cAAa,oBAAoB,UAAU,CAAC,CAChD,MAAM,GAAG,MAAM,cAAc,GAAG,EAAE,CAAC;CAEtC,MAAM,UAAU,QAAQ,eAAe,cAAc;EACnD,SAAS,SAAS,WAAW,QAAQ,UAAU;EAC/C,YAAY,SAAS;EACrB;EACA;EACD,CAAC;CAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,WAAW,WAAW;AACjE,SAAQ,UAAU,cAAc,QAAQ;;AAG1C,SAAS,oBAAoB,WAA+C;CAC1E,MAAM,cAAc,UAAU;CAC9B,MAAM,YAAY,WAAW,YAAY;AAEzC,QAAO;EACL;EACA;EACA,aAAa,SAAS,UAAU;EAChC,QAAQ,UAAU;EAClB,MAAM,UAAU;EACjB;;;;ACrEH,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;;;;;;;;AAS9D,IAAa,eAAb,cAAkC,WAAW;CAC3C,OAAc;CACd,UAA0B,CAAC,QAAQ;;;;;;CAOnC,SAAyB,SAAiC;EACxD,MAAM,eAAe,KAAK,KAAK,WAAW,MAAM;AAChD,OAAK,aAAa,SAAS,cAAc,KAAK,KAAK;AAEnD,WAAgB,QAAQ"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_MAX_BODY_SIZE = 1_048_576; // 1 MB
|
|
9
|
+
|
|
10
|
+
export type BodyLimitCapability =
|
|
11
|
+
| "unvalidated-request-body"
|
|
12
|
+
| "prevalidated-request-body";
|
|
13
|
+
|
|
14
|
+
export type BodyLimitPolicy = {
|
|
15
|
+
readonly maxBodySize: number;
|
|
16
|
+
readonly capability: BodyLimitCapability;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type RequestBodyPrevalidation = {
|
|
20
|
+
readonly maxBodySize: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// NodeAdapter reads and caps IncomingMessage bodies before creating Fetch
|
|
24
|
+
// Requests. This WeakMap records that Node→Fetch handoff so FetchApiAdapter
|
|
25
|
+
// skips re-reading only when the Node policy was at least as strict.
|
|
26
|
+
const requestBodyPrevalidations = new WeakMap<
|
|
27
|
+
Request,
|
|
28
|
+
RequestBodyPrevalidation
|
|
29
|
+
>();
|
|
30
|
+
|
|
31
|
+
const FETCH_BODY_LIMIT_CAPABILITY: BodyLimitCapability =
|
|
32
|
+
"unvalidated-request-body";
|
|
33
|
+
|
|
34
|
+
const NODE_BODY_LIMIT_CAPABILITY: BodyLimitCapability =
|
|
35
|
+
"prevalidated-request-body";
|
|
36
|
+
|
|
37
|
+
export function createFetchBodyLimitPolicy(
|
|
38
|
+
maxBodySize?: number
|
|
39
|
+
): BodyLimitPolicy {
|
|
40
|
+
return {
|
|
41
|
+
maxBodySize: resolveMaxBodySize(maxBodySize),
|
|
42
|
+
capability: FETCH_BODY_LIMIT_CAPABILITY,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createNodeBodyLimitPolicy(
|
|
47
|
+
maxBodySize?: number
|
|
48
|
+
): BodyLimitPolicy {
|
|
49
|
+
return {
|
|
50
|
+
maxBodySize: resolveMaxBodySize(maxBodySize),
|
|
51
|
+
capability: NODE_BODY_LIMIT_CAPABILITY,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveMaxBodySize(maxBodySize?: number): number {
|
|
56
|
+
return maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function parseContentLength(
|
|
60
|
+
value: string | string[] | null | undefined
|
|
61
|
+
): number | undefined {
|
|
62
|
+
const rawValue = Array.isArray(value) ? value[0] : value;
|
|
63
|
+
if (rawValue === undefined || rawValue === null) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const contentLength = Number(rawValue);
|
|
68
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return contentLength;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isBodySizeOverLimit(
|
|
76
|
+
bodySize: number,
|
|
77
|
+
maxBodySize: number
|
|
78
|
+
): boolean {
|
|
79
|
+
return bodySize > maxBodySize;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function markRequestBodyPrevalidated(
|
|
83
|
+
request: Request,
|
|
84
|
+
policy: BodyLimitPolicy
|
|
85
|
+
): void {
|
|
86
|
+
if (policy.capability !== "prevalidated-request-body") {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
requestBodyPrevalidations.set(request, {
|
|
91
|
+
maxBodySize: policy.maxBodySize,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getRequestBodyPrevalidation(
|
|
96
|
+
request: Request
|
|
97
|
+
): RequestBodyPrevalidation | undefined {
|
|
98
|
+
return requestBodyPrevalidations.get(request);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function hasSatisfiedBodyLimitPolicy(
|
|
102
|
+
request: Request,
|
|
103
|
+
policy: BodyLimitPolicy
|
|
104
|
+
): boolean {
|
|
105
|
+
const prevalidation = getRequestBodyPrevalidation(request);
|
|
106
|
+
if (!prevalidation) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return prevalidation.maxBodySize <= policy.maxBodySize;
|
|
111
|
+
}
|
package/dist/lib/Errors.ts
CHANGED
|
@@ -29,6 +29,22 @@ export class PayloadTooLargeError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Error thrown when a skipped request body cannot be drained before timeout.
|
|
34
|
+
* Caught by NodeAdapter to return a 413 Payload Too Large response.
|
|
35
|
+
*/
|
|
36
|
+
export class RequestBodyDrainTimeoutError extends Error {
|
|
37
|
+
public override readonly name = "RequestBodyDrainTimeoutError";
|
|
38
|
+
public constructor(
|
|
39
|
+
public readonly maxBodySize: number,
|
|
40
|
+
public readonly timeoutMs: number
|
|
41
|
+
) {
|
|
42
|
+
super(
|
|
43
|
+
`Request body drain timed out after ${timeoutMs}ms while enforcing ${maxBodySize} byte limit`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
* Error thrown when the response body cannot be serialized to JSON.
|
|
34
50
|
* Typically caused by circular references or non-serializable values.
|
|
@@ -13,14 +13,22 @@ import type {
|
|
|
13
13
|
IHttpRequest,
|
|
14
14
|
IHttpResponse,
|
|
15
15
|
} from "@rexeus/typeweaver-core";
|
|
16
|
+
import {
|
|
17
|
+
createFetchBodyLimitPolicy,
|
|
18
|
+
hasSatisfiedBodyLimitPolicy,
|
|
19
|
+
isBodySizeOverLimit,
|
|
20
|
+
parseContentLength,
|
|
21
|
+
} from "./BodyLimitPolicy.js";
|
|
16
22
|
import {
|
|
17
23
|
BodyParseError,
|
|
18
24
|
PayloadTooLargeError,
|
|
19
25
|
ResponseSerializationError,
|
|
20
26
|
} from "./Errors.js";
|
|
27
|
+
import type { BodyLimitPolicy } from "./BodyLimitPolicy.js";
|
|
21
28
|
|
|
22
29
|
export type FetchApiAdapterOptions = {
|
|
23
30
|
readonly maxBodySize?: number;
|
|
31
|
+
readonly bodyLimitPolicy?: BodyLimitPolicy;
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
/**
|
|
@@ -35,13 +43,12 @@ export type FetchApiAdapterOptions = {
|
|
|
35
43
|
* Bun, Deno, Node.js (>=18), Cloudflare Workers.
|
|
36
44
|
*/
|
|
37
45
|
export class FetchApiAdapter {
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
private readonly maxBodySize: number;
|
|
46
|
+
private readonly bodyLimitPolicy: BodyLimitPolicy;
|
|
41
47
|
|
|
42
48
|
public constructor(options?: FetchApiAdapterOptions) {
|
|
43
|
-
this.
|
|
44
|
-
options?.
|
|
49
|
+
this.bodyLimitPolicy =
|
|
50
|
+
options?.bodyLimitPolicy ??
|
|
51
|
+
createFetchBodyLimitPolicy(options?.maxBodySize);
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
/**
|
|
@@ -234,21 +241,24 @@ export class FetchApiAdapter {
|
|
|
234
241
|
}
|
|
235
242
|
|
|
236
243
|
private async enforceBodySizeLimit(request: Request): Promise<Request> {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return this.readBodyWithLimit(request);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const contentLength = Number(contentLengthHeader);
|
|
243
|
-
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
244
|
-
return this.readBodyWithLimit(request);
|
|
244
|
+
if (hasSatisfiedBodyLimitPolicy(request, this.bodyLimitPolicy)) {
|
|
245
|
+
return request;
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
const contentLength = parseContentLength(
|
|
249
|
+
request.headers.get("content-length")
|
|
250
|
+
);
|
|
251
|
+
if (
|
|
252
|
+
contentLength !== undefined &&
|
|
253
|
+
isBodySizeOverLimit(contentLength, this.bodyLimitPolicy.maxBodySize)
|
|
254
|
+
) {
|
|
255
|
+
throw new PayloadTooLargeError(
|
|
256
|
+
contentLength,
|
|
257
|
+
this.bodyLimitPolicy.maxBodySize
|
|
258
|
+
);
|
|
249
259
|
}
|
|
250
260
|
|
|
251
|
-
return request;
|
|
261
|
+
return this.readBodyWithLimit(request);
|
|
252
262
|
}
|
|
253
263
|
|
|
254
264
|
private async readBodyWithLimit(request: Request): Promise<Request> {
|
|
@@ -264,14 +274,27 @@ export class FetchApiAdapter {
|
|
|
264
274
|
if (done) break;
|
|
265
275
|
|
|
266
276
|
totalBytes += value.byteLength;
|
|
267
|
-
if (totalBytes
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
if (isBodySizeOverLimit(totalBytes, this.bodyLimitPolicy.maxBodySize)) {
|
|
278
|
+
throw new PayloadTooLargeError(
|
|
279
|
+
totalBytes,
|
|
280
|
+
this.bodyLimitPolicy.maxBodySize
|
|
281
|
+
);
|
|
270
282
|
}
|
|
271
283
|
chunks.push(value);
|
|
272
284
|
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
try {
|
|
287
|
+
await reader.cancel();
|
|
288
|
+
} catch {
|
|
289
|
+
// Preserve the original read failure if stream cleanup also fails.
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
273
292
|
} finally {
|
|
274
|
-
|
|
293
|
+
try {
|
|
294
|
+
reader.releaseLock();
|
|
295
|
+
} catch {
|
|
296
|
+
// Some runtimes may report release errors after stream termination.
|
|
297
|
+
}
|
|
275
298
|
}
|
|
276
299
|
|
|
277
300
|
return new Request(request.url, {
|
|
@@ -299,7 +322,8 @@ export class FetchApiAdapter {
|
|
|
299
322
|
): string | ArrayBuffer | Blob | null {
|
|
300
323
|
if (body === undefined || body === null) return null;
|
|
301
324
|
if (typeof body === "string") return body;
|
|
302
|
-
if (body instanceof
|
|
325
|
+
if (body instanceof ArrayBuffer) return body;
|
|
326
|
+
if (body instanceof Blob) return body;
|
|
303
327
|
|
|
304
328
|
try {
|
|
305
329
|
return JSON.stringify(body);
|
|
@@ -332,6 +356,10 @@ export class FetchApiAdapter {
|
|
|
332
356
|
headers.set("content-type", "application/json");
|
|
333
357
|
}
|
|
334
358
|
|
|
359
|
+
if (!headers.has("content-type") && body instanceof Blob && body.type) {
|
|
360
|
+
headers.set("content-type", body.type);
|
|
361
|
+
}
|
|
362
|
+
|
|
335
363
|
return headers;
|
|
336
364
|
}
|
|
337
365
|
|
package/dist/lib/NodeAdapter.ts
CHANGED
|
@@ -10,10 +10,36 @@ import {
|
|
|
10
10
|
internalServerErrorDefaultError,
|
|
11
11
|
payloadTooLargeDefaultError,
|
|
12
12
|
} from "@rexeus/typeweaver-core";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
createNodeBodyLimitPolicy,
|
|
15
|
+
isBodySizeOverLimit,
|
|
16
|
+
markRequestBodyPrevalidated,
|
|
17
|
+
parseContentLength,
|
|
18
|
+
} from "./BodyLimitPolicy.js";
|
|
19
|
+
import {
|
|
20
|
+
PayloadTooLargeError,
|
|
21
|
+
RequestBodyDrainTimeoutError,
|
|
22
|
+
} from "./Errors.js";
|
|
23
|
+
import {
|
|
24
|
+
getTypeweaverAppErrorReporter,
|
|
25
|
+
getTypeweaverAppRuntimeContext,
|
|
26
|
+
} from "./TypeweaverInternals.js";
|
|
14
27
|
import type { TypeweaverApp } from "./TypeweaverApp.js";
|
|
15
28
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
16
29
|
|
|
30
|
+
type DrainRequestResult = {
|
|
31
|
+
readonly exceededLimit: boolean;
|
|
32
|
+
readonly timedOut: boolean;
|
|
33
|
+
readonly totalBytes: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type DrainRequestOptions = {
|
|
37
|
+
readonly destroyOnLimitExceeded?: boolean;
|
|
38
|
+
readonly timeoutMs?: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const REQUEST_DRAIN_TIMEOUT_MS = 5_000;
|
|
42
|
+
|
|
17
43
|
/**
|
|
18
44
|
* Adapts a `TypeweaverApp` to Node.js `http.createServer`.
|
|
19
45
|
*
|
|
@@ -29,8 +55,6 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
29
55
|
* createServer(nodeAdapter(app)).listen(3000);
|
|
30
56
|
* ```
|
|
31
57
|
*/
|
|
32
|
-
const DEFAULT_MAX_BODY_SIZE = 1_048_576; // 1 MB
|
|
33
|
-
|
|
34
58
|
export type NodeAdapterOptions = {
|
|
35
59
|
readonly maxBodySize?: number;
|
|
36
60
|
};
|
|
@@ -39,9 +63,14 @@ export function nodeAdapter(
|
|
|
39
63
|
app: TypeweaverApp<any>,
|
|
40
64
|
options?: NodeAdapterOptions
|
|
41
65
|
): (req: IncomingMessage, res: ServerResponse) => void {
|
|
42
|
-
const
|
|
66
|
+
const appRuntimeContext = getTypeweaverAppRuntimeContext(app);
|
|
67
|
+
const maxBodySize =
|
|
68
|
+
options?.maxBodySize ?? appRuntimeContext?.bodyLimitPolicy.maxBodySize;
|
|
69
|
+
const bodyLimitPolicy = createNodeBodyLimitPolicy(maxBodySize);
|
|
70
|
+
const reportError = getTypeweaverAppErrorReporter(app);
|
|
71
|
+
|
|
43
72
|
return (req, res) => {
|
|
44
|
-
void handleRequest(app, req, res,
|
|
73
|
+
void handleRequest(app, req, res, bodyLimitPolicy, reportError);
|
|
45
74
|
};
|
|
46
75
|
}
|
|
47
76
|
|
|
@@ -49,18 +78,45 @@ async function handleRequest(
|
|
|
49
78
|
app: TypeweaverApp<any>,
|
|
50
79
|
req: IncomingMessage,
|
|
51
80
|
res: ServerResponse,
|
|
52
|
-
|
|
81
|
+
bodyLimitPolicy: ReturnType<typeof createNodeBodyLimitPolicy>,
|
|
82
|
+
reportError: (error: unknown) => void
|
|
53
83
|
): Promise<void> {
|
|
54
84
|
try {
|
|
55
85
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
56
|
-
const
|
|
57
|
-
|
|
86
|
+
const shouldValidateBody = shouldValidateRequestBody(req.method);
|
|
87
|
+
|
|
88
|
+
enforceContentLengthLimit(req, bodyLimitPolicy.maxBodySize);
|
|
89
|
+
|
|
90
|
+
if (!shouldValidateBody) {
|
|
91
|
+
const drainResult = await drainRequest(req, bodyLimitPolicy.maxBodySize, {
|
|
92
|
+
destroyOnLimitExceeded: false,
|
|
93
|
+
});
|
|
94
|
+
if (drainResult.exceededLimit) {
|
|
95
|
+
throw new PayloadTooLargeError(
|
|
96
|
+
drainResult.totalBytes,
|
|
97
|
+
bodyLimitPolicy.maxBodySize
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (drainResult.timedOut) {
|
|
101
|
+
throw new RequestBodyDrainTimeoutError(
|
|
102
|
+
bodyLimitPolicy.maxBodySize,
|
|
103
|
+
REQUEST_DRAIN_TIMEOUT_MS
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const body = !shouldValidateBody
|
|
109
|
+
? undefined
|
|
110
|
+
: await collectBody(req, bodyLimitPolicy.maxBodySize);
|
|
58
111
|
|
|
59
112
|
const request = new Request(url, {
|
|
60
113
|
method: req.method,
|
|
61
114
|
headers: req.headers as Record<string, string>,
|
|
62
115
|
body,
|
|
63
116
|
});
|
|
117
|
+
if (shouldValidateBody) {
|
|
118
|
+
markRequestBodyPrevalidated(request, bodyLimitPolicy);
|
|
119
|
+
}
|
|
64
120
|
|
|
65
121
|
const response = await app.fetch(request);
|
|
66
122
|
|
|
@@ -76,28 +132,148 @@ async function handleRequest(
|
|
|
76
132
|
res.writeHead(response.status);
|
|
77
133
|
res.end(Buffer.from(await response.arrayBuffer()));
|
|
78
134
|
} catch (error) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
135
|
+
reportError(error);
|
|
136
|
+
|
|
137
|
+
if (isRequestBodyLimitError(error)) {
|
|
138
|
+
writeDefaultErrorResponse(res, payloadTooLargeDefaultError, () => {
|
|
139
|
+
void drainRequest(req, bodyLimitPolicy.maxBodySize, {
|
|
140
|
+
destroyOnLimitExceeded: true,
|
|
83
141
|
});
|
|
84
|
-
}
|
|
85
|
-
res.end(
|
|
86
|
-
JSON.stringify(createDefaultErrorBody(payloadTooLargeDefaultError))
|
|
87
|
-
);
|
|
142
|
+
});
|
|
88
143
|
return;
|
|
89
144
|
}
|
|
90
145
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
146
|
+
writeDefaultErrorResponse(res, internalServerErrorDefaultError);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function shouldValidateRequestBody(method?: string): boolean {
|
|
151
|
+
return method !== "GET" && method !== "HEAD";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isRequestBodyLimitError(
|
|
155
|
+
error: unknown
|
|
156
|
+
): error is PayloadTooLargeError | RequestBodyDrainTimeoutError {
|
|
157
|
+
return (
|
|
158
|
+
error instanceof PayloadTooLargeError ||
|
|
159
|
+
error instanceof RequestBodyDrainTimeoutError
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function enforceContentLengthLimit(
|
|
164
|
+
req: IncomingMessage,
|
|
165
|
+
maxBodySize: number
|
|
166
|
+
): void {
|
|
167
|
+
const contentLength = parseContentLength(req.headers["content-length"]);
|
|
168
|
+
if (contentLength === undefined) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (isBodySizeOverLimit(contentLength, maxBodySize)) {
|
|
173
|
+
throw new PayloadTooLargeError(contentLength, maxBodySize);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function writeDefaultErrorResponse(
|
|
178
|
+
res: ServerResponse,
|
|
179
|
+
error:
|
|
180
|
+
| typeof payloadTooLargeDefaultError
|
|
181
|
+
| typeof internalServerErrorDefaultError,
|
|
182
|
+
onFinished?: () => void
|
|
183
|
+
): void {
|
|
184
|
+
if (!res.headersSent) {
|
|
185
|
+
res.writeHead(error.statusCode, {
|
|
186
|
+
"content-type": "application/json",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (onFinished !== undefined) {
|
|
191
|
+
res.once("finish", onFinished);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
res.end(JSON.stringify(createDefaultErrorBody(error)));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function drainRequest(
|
|
198
|
+
req: IncomingMessage,
|
|
199
|
+
maxBodySize: number,
|
|
200
|
+
options: DrainRequestOptions = {}
|
|
201
|
+
): Promise<DrainRequestResult> {
|
|
202
|
+
if (req.readableEnded || req.destroyed) {
|
|
203
|
+
return { exceededLimit: false, timedOut: false, totalBytes: 0 };
|
|
100
204
|
}
|
|
205
|
+
|
|
206
|
+
return await new Promise<DrainRequestResult>(resolve => {
|
|
207
|
+
const destroyOnLimitExceeded = options.destroyOnLimitExceeded ?? true;
|
|
208
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_DRAIN_TIMEOUT_MS;
|
|
209
|
+
let drainedBytes = 0;
|
|
210
|
+
let isSettled = false;
|
|
211
|
+
|
|
212
|
+
const settle = (result: Omit<DrainRequestResult, "totalBytes">): void => {
|
|
213
|
+
if (isSettled) return;
|
|
214
|
+
isSettled = true;
|
|
215
|
+
cleanup();
|
|
216
|
+
resolve({ ...result, totalBytes: drainedBytes });
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const stopReading = (
|
|
220
|
+
result: Omit<DrainRequestResult, "totalBytes">
|
|
221
|
+
): void => {
|
|
222
|
+
if (isSettled) return;
|
|
223
|
+
isSettled = true;
|
|
224
|
+
cleanup();
|
|
225
|
+
if (destroyOnLimitExceeded) {
|
|
226
|
+
req.destroy();
|
|
227
|
+
}
|
|
228
|
+
resolve({ ...result, totalBytes: drainedBytes });
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const drainTimeout = setTimeout(() => {
|
|
232
|
+
stopReading({ exceededLimit: false, timedOut: true });
|
|
233
|
+
}, timeoutMs);
|
|
234
|
+
drainTimeout.unref();
|
|
235
|
+
|
|
236
|
+
const handleData = (chunk: Buffer | string): void => {
|
|
237
|
+
drainedBytes +=
|
|
238
|
+
typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
|
|
239
|
+
|
|
240
|
+
if (isBodySizeOverLimit(drainedBytes, maxBodySize)) {
|
|
241
|
+
stopReading({ exceededLimit: true, timedOut: false });
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const handleEnd = (): void => {
|
|
246
|
+
settle({ exceededLimit: false, timedOut: false });
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const handleClose = (): void => {
|
|
250
|
+
settle({ exceededLimit: false, timedOut: false });
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const handleAborted = (): void => {
|
|
254
|
+
settle({ exceededLimit: false, timedOut: false });
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const handleError = (): void => {
|
|
258
|
+
settle({ exceededLimit: false, timedOut: false });
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const cleanup = (): void => {
|
|
262
|
+
clearTimeout(drainTimeout);
|
|
263
|
+
req.off("data", handleData);
|
|
264
|
+
req.off("end", handleEnd);
|
|
265
|
+
req.off("error", handleError);
|
|
266
|
+
req.off("aborted", handleAborted);
|
|
267
|
+
req.off("close", handleClose);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
req.on("data", handleData);
|
|
271
|
+
req.on("end", handleEnd);
|
|
272
|
+
req.on("error", handleError);
|
|
273
|
+
req.on("aborted", handleAborted);
|
|
274
|
+
req.on("close", handleClose);
|
|
275
|
+
req.resume();
|
|
276
|
+
});
|
|
101
277
|
}
|
|
102
278
|
|
|
103
279
|
function collectBody(
|
|
@@ -107,26 +283,72 @@ function collectBody(
|
|
|
107
283
|
return new Promise<ArrayBuffer>((resolve, reject) => {
|
|
108
284
|
const chunks: Buffer[] = [];
|
|
109
285
|
let totalBytes = 0;
|
|
286
|
+
let isSettled = false;
|
|
287
|
+
|
|
288
|
+
const cleanup = (): void => {
|
|
289
|
+
req.off("data", handleData);
|
|
290
|
+
req.off("end", handleEnd);
|
|
291
|
+
req.off("error", handleError);
|
|
292
|
+
req.off("aborted", handleAborted);
|
|
293
|
+
req.off("close", handleClose);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const rejectOnce = (error: unknown): void => {
|
|
297
|
+
if (isSettled) return;
|
|
298
|
+
isSettled = true;
|
|
299
|
+
cleanup();
|
|
300
|
+
reject(error);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const resolveOnce = (body: ArrayBuffer): void => {
|
|
304
|
+
if (isSettled) return;
|
|
305
|
+
isSettled = true;
|
|
306
|
+
cleanup();
|
|
307
|
+
resolve(body);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const handleData = (chunk: Buffer): void => {
|
|
311
|
+
if (isSettled) return;
|
|
110
312
|
|
|
111
|
-
req.on("data", (chunk: Buffer) => {
|
|
112
313
|
totalBytes += chunk.byteLength;
|
|
113
|
-
if (totalBytes
|
|
114
|
-
req.
|
|
115
|
-
|
|
314
|
+
if (isBodySizeOverLimit(totalBytes, maxBodySize)) {
|
|
315
|
+
req.pause();
|
|
316
|
+
cleanup();
|
|
317
|
+
req.resume();
|
|
318
|
+
rejectOnce(new PayloadTooLargeError(totalBytes, maxBodySize));
|
|
116
319
|
return;
|
|
117
320
|
}
|
|
118
321
|
chunks.push(chunk);
|
|
119
|
-
}
|
|
322
|
+
};
|
|
120
323
|
|
|
121
|
-
|
|
324
|
+
const handleEnd = (): void => {
|
|
122
325
|
const combined = Buffer.concat(chunks, totalBytes);
|
|
123
|
-
|
|
326
|
+
resolveOnce(
|
|
124
327
|
combined.buffer.slice(
|
|
125
328
|
combined.byteOffset,
|
|
126
329
|
combined.byteOffset + combined.byteLength
|
|
127
330
|
) as ArrayBuffer
|
|
128
331
|
);
|
|
129
|
-
}
|
|
130
|
-
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const handleError = (error: Error): void => {
|
|
335
|
+
rejectOnce(error);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const handleAborted = (): void => {
|
|
339
|
+
rejectOnce(new Error("Request aborted while reading body"));
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const handleClose = (): void => {
|
|
343
|
+
if (!req.readableEnded) {
|
|
344
|
+
rejectOnce(new Error("Request closed before body was fully read"));
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
req.on("data", handleData);
|
|
349
|
+
req.on("end", handleEnd);
|
|
350
|
+
req.on("error", handleError);
|
|
351
|
+
req.on("aborted", handleAborted);
|
|
352
|
+
req.on("close", handleClose);
|
|
131
353
|
});
|
|
132
354
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @generated by @rexeus/typeweaver
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
// oxlint-disable import/max-dependencies
|
|
8
9
|
import {
|
|
9
10
|
badRequestDefaultError,
|
|
10
11
|
createDefaultErrorBody,
|
|
@@ -19,19 +20,20 @@ import {
|
|
|
19
20
|
} from "@rexeus/typeweaver-core";
|
|
20
21
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
21
22
|
import { BodyParseError, PayloadTooLargeError } from "./Errors.js";
|
|
22
|
-
import { FetchApiAdapter } from "./FetchApiAdapter.js";
|
|
23
23
|
import { executeMiddlewarePipeline } from "./Middleware.js";
|
|
24
24
|
import { Router } from "./Router.js";
|
|
25
25
|
import { StateMap } from "./StateMap.js";
|
|
26
|
+
import { initializeTypeweaverAppRuntime } from "./TypeweaverAppRuntime.js";
|
|
27
|
+
import type { FetchApiAdapter } from "./FetchApiAdapter.js";
|
|
26
28
|
import type { Middleware } from "./Middleware.js";
|
|
27
29
|
import type { RequestHandler } from "./RequestHandler.js";
|
|
28
30
|
import type {
|
|
29
31
|
HttpResponseErrorHandler,
|
|
32
|
+
RequestValidationErrorHandler,
|
|
30
33
|
ResponseValidationErrorHandler,
|
|
31
34
|
RouteDefinition,
|
|
32
35
|
RouteMatch,
|
|
33
36
|
UnknownErrorHandler,
|
|
34
|
-
RequestValidationErrorHandler,
|
|
35
37
|
} from "./Router.js";
|
|
36
38
|
import type { ServerContext } from "./ServerContext.js";
|
|
37
39
|
import type {
|
|
@@ -82,8 +84,12 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
82
84
|
private readonly onError: (error: unknown) => void;
|
|
83
85
|
|
|
84
86
|
public constructor(options?: TypeweaverAppOptions) {
|
|
85
|
-
this.
|
|
86
|
-
this.
|
|
87
|
+
this.onError = options?.onError ?? (error => console.error(error));
|
|
88
|
+
this.adapter = initializeTypeweaverAppRuntime({
|
|
89
|
+
app: this,
|
|
90
|
+
options,
|
|
91
|
+
reportError: error => this.safeOnError(error),
|
|
92
|
+
});
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
private safeOnError(error: unknown): void {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createFetchBodyLimitPolicy } from "./BodyLimitPolicy.js";
|
|
9
|
+
import { FetchApiAdapter } from "./FetchApiAdapter.js";
|
|
10
|
+
import { setTypeweaverAppRuntimeContext } from "./TypeweaverInternals.js";
|
|
11
|
+
import type { TypeweaverApp } from "./TypeweaverApp.js";
|
|
12
|
+
|
|
13
|
+
type InitializeTypeweaverAppRuntimeOptions = {
|
|
14
|
+
readonly maxBodySize?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type InitializeTypeweaverAppRuntimeParameters = {
|
|
18
|
+
readonly app: TypeweaverApp<any>;
|
|
19
|
+
readonly options?: InitializeTypeweaverAppRuntimeOptions;
|
|
20
|
+
readonly reportError: (error: unknown) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function initializeTypeweaverAppRuntime({
|
|
24
|
+
app,
|
|
25
|
+
options,
|
|
26
|
+
reportError,
|
|
27
|
+
}: InitializeTypeweaverAppRuntimeParameters): FetchApiAdapter {
|
|
28
|
+
const bodyLimitPolicy = createFetchBodyLimitPolicy(options?.maxBodySize);
|
|
29
|
+
const adapter = new FetchApiAdapter({ bodyLimitPolicy });
|
|
30
|
+
|
|
31
|
+
setTypeweaverAppRuntimeContext(app, {
|
|
32
|
+
bodyLimitPolicy,
|
|
33
|
+
reportError,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return adapter;
|
|
37
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BodyLimitPolicy } from "./BodyLimitPolicy.js";
|
|
9
|
+
import type { TypeweaverApp } from "./TypeweaverApp.js";
|
|
10
|
+
|
|
11
|
+
/** Runtime context shared privately between TypeweaverApp and adapters. */
|
|
12
|
+
export type TypeweaverRuntimeContext = {
|
|
13
|
+
readonly bodyLimitPolicy: BodyLimitPolicy;
|
|
14
|
+
readonly reportError: (error: unknown) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const appRuntimeContextRegistry = new WeakMap<
|
|
18
|
+
TypeweaverApp<any>,
|
|
19
|
+
TypeweaverRuntimeContext
|
|
20
|
+
>();
|
|
21
|
+
|
|
22
|
+
function fallbackReportError(error: unknown): void {
|
|
23
|
+
console.error(error);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setTypeweaverAppRuntimeContext(
|
|
27
|
+
app: TypeweaverApp<any>,
|
|
28
|
+
runtimeContext: TypeweaverRuntimeContext
|
|
29
|
+
): void {
|
|
30
|
+
appRuntimeContextRegistry.set(app, runtimeContext);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getTypeweaverAppRuntimeContext(
|
|
34
|
+
app: TypeweaverApp<any>
|
|
35
|
+
): TypeweaverRuntimeContext | undefined {
|
|
36
|
+
return appRuntimeContextRegistry.get(app);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getTypeweaverAppErrorReporter(
|
|
40
|
+
app: TypeweaverApp<any>
|
|
41
|
+
): (error: unknown) => void {
|
|
42
|
+
return (
|
|
43
|
+
getTypeweaverAppRuntimeContext(app)?.reportError ?? fallbackReportError
|
|
44
|
+
);
|
|
45
|
+
}
|
package/dist/lib/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexeus/typeweaver-server",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Generates a lightweight, dependency-free server with built-in routing and middleware from your API definitions. Powered by Typeweaver.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -47,15 +47,18 @@
|
|
|
47
47
|
},
|
|
48
48
|
"homepage": "https://github.com/rexeus/typeweaver#readme",
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@rexeus/typeweaver-core": "^0.10.
|
|
51
|
-
"@rexeus/typeweaver-gen": "^0.10.
|
|
50
|
+
"@rexeus/typeweaver-core": "^0.10.3",
|
|
51
|
+
"@rexeus/typeweaver-gen": "^0.10.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"get-port": "^7.2.0",
|
|
55
55
|
"test-utils": "file:../test-utils",
|
|
56
56
|
"tsx": "^4.21.0",
|
|
57
|
-
"@rexeus/typeweaver-core": "^0.10.
|
|
58
|
-
"@rexeus/typeweaver-gen": "^0.10.
|
|
57
|
+
"@rexeus/typeweaver-core": "^0.10.3",
|
|
58
|
+
"@rexeus/typeweaver-gen": "^0.10.3"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"polycase": "^1.1.0"
|
|
59
62
|
},
|
|
60
63
|
"scripts": {
|
|
61
64
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|