@rexeus/typeweaver-server 0.10.1 → 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 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, _rexeus_typeweaver_gen.toPascalCase)(resource.name);
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, _rexeus_typeweaver_gen.toPascalCase)(operationId);
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, toPascalCase } from "@rexeus/typeweaver-gen";
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 = toPascalCase(resource.name);
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 = toPascalCase(operationId);
39
+ const className = pascalCase(operationId);
39
40
  return {
40
41
  operationId,
41
42
  className,
@@ -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, toPascalCase } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n NormalizedOperation,\n NormalizedResource,\n} from \"@rexeus/typeweaver-gen\";\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 = toPascalCase(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 = toPascalCase(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\";\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":";;;;;;;;;;;;;;;;AA8BA,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,aAAa,SAAS,KAAK;CACxD,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,aAAa,YAAY;AAE3C,QAAO;EACL;EACA;EACA,aAAa,SAAS,UAAU;EAChC,QAAQ,UAAU;EAClB,MAAM,UAAU;EACjB;;;;ACpEH,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"}
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
+ }
@@ -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
- } from "./Errors";
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 static readonly DEFAULT_MAX_BODY_SIZE = 1_048_576; // 1 MB
39
-
40
- private readonly maxBodySize: number;
46
+ private readonly bodyLimitPolicy: BodyLimitPolicy;
41
47
 
42
48
  public constructor(options?: FetchApiAdapterOptions) {
43
- this.maxBodySize =
44
- options?.maxBodySize ?? FetchApiAdapter.DEFAULT_MAX_BODY_SIZE;
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
- const contentLengthHeader = request.headers.get("content-length");
238
- if (contentLengthHeader === null) {
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
- if (contentLength > this.maxBodySize) {
248
- throw new PayloadTooLargeError(contentLength, this.maxBodySize);
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 > this.maxBodySize) {
268
- await reader.cancel();
269
- throw new PayloadTooLargeError(totalBytes, this.maxBodySize);
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
- reader.releaseLock();
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 Blob || body instanceof ArrayBuffer) return body;
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
 
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
9
- import type { ServerContext } from "./ServerContext";
9
+ import type { ServerContext } from "./ServerContext.js";
10
10
 
11
11
  /**
12
12
  * A middleware function that processes requests in the pipeline.
@@ -10,10 +10,36 @@ import {
10
10
  internalServerErrorDefaultError,
11
11
  payloadTooLargeDefaultError,
12
12
  } from "@rexeus/typeweaver-core";
13
- import { PayloadTooLargeError } from "./Errors";
14
- import type { TypeweaverApp } from "./TypeweaverApp";
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";
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 maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
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, maxBodySize);
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
- maxBodySize: number
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 isBodyless = req.method === "GET" || req.method === "HEAD";
57
- const body = isBodyless ? undefined : await collectBody(req, maxBodySize);
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
- if (error instanceof PayloadTooLargeError) {
80
- if (!res.headersSent) {
81
- res.writeHead(payloadTooLargeDefaultError.statusCode, {
82
- "content-type": "application/json",
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
- console.error(error);
92
- if (!res.headersSent) {
93
- res.writeHead(internalServerErrorDefaultError.statusCode, {
94
- "content-type": "application/json",
95
- });
96
- }
97
- res.end(
98
- JSON.stringify(createDefaultErrorBody(internalServerErrorDefaultError))
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 > maxBodySize) {
114
- req.destroy();
115
- reject(new PayloadTooLargeError(totalBytes, maxBodySize));
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
- req.on("end", () => {
324
+ const handleEnd = (): void => {
122
325
  const combined = Buffer.concat(chunks, totalBytes);
123
- resolve(
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
- req.on("error", reject);
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
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { IHttpRequest, IHttpResponse } from "@rexeus/typeweaver-core";
9
- import type { ServerContext } from "./ServerContext";
9
+ import type { ServerContext } from "./ServerContext.js";
10
10
 
11
11
  /**
12
12
  * A type-safe request handler function.
@@ -14,8 +14,8 @@ import type {
14
14
  RequestValidationError,
15
15
  ResponseValidationError,
16
16
  } from "@rexeus/typeweaver-core";
17
- import type { RequestHandler } from "./RequestHandler";
18
- import type { ServerContext } from "./ServerContext";
17
+ import type { RequestHandler } from "./RequestHandler.js";
18
+ import type { ServerContext } from "./ServerContext.js";
19
19
 
20
20
  /**
21
21
  * Metadata about a matched route, available in middleware and handlers via `ctx.route`.
@@ -6,8 +6,8 @@
6
6
  */
7
7
 
8
8
  import type { IHttpRequest } from "@rexeus/typeweaver-core";
9
- import type { RouteMetadata } from "./Router";
10
- import type { StateMap } from "./StateMap";
9
+ import type { RouteMetadata } from "./Router.js";
10
+ import type { StateMap } from "./StateMap.js";
11
11
 
12
12
  /**
13
13
  * Context object passed through the middleware pipeline and to request handlers.
@@ -6,9 +6,9 @@
6
6
  */
7
7
 
8
8
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
9
- import type { Middleware } from "./Middleware";
10
- import type { ServerContext } from "./ServerContext";
11
- import type { TypeweaverApp } from "./TypeweaverApp";
9
+ import type { Middleware } from "./Middleware.js";
10
+ import type { ServerContext } from "./ServerContext.js";
11
+ import type { TypeweaverApp } from "./TypeweaverApp.js";
12
12
 
13
13
  /**
14
14
  * A middleware descriptor carrying compile-time metadata about what state
@@ -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,
@@ -18,24 +19,28 @@ import {
18
19
  validationDefaultError,
19
20
  } from "@rexeus/typeweaver-core";
20
21
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
21
- import { BodyParseError, PayloadTooLargeError } from "./Errors";
22
- import { FetchApiAdapter } from "./FetchApiAdapter";
23
- import { executeMiddlewarePipeline } from "./Middleware";
24
- import { Router } from "./Router";
25
- import { StateMap } from "./StateMap";
26
- import type { Middleware } from "./Middleware";
27
- import type { RequestHandler } from "./RequestHandler";
22
+ import { BodyParseError, PayloadTooLargeError } from "./Errors.js";
23
+ import { executeMiddlewarePipeline } from "./Middleware.js";
24
+ import { Router } from "./Router.js";
25
+ import { StateMap } from "./StateMap.js";
26
+ import { initializeTypeweaverAppRuntime } from "./TypeweaverAppRuntime.js";
27
+ import type { FetchApiAdapter } from "./FetchApiAdapter.js";
28
+ import type { Middleware } from "./Middleware.js";
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
- } from "./Router";
36
- import type { ServerContext } from "./ServerContext";
37
- import type { StateRequirementError, TypedMiddleware } from "./TypedMiddleware";
38
- import type { TypeweaverRouter } from "./TypeweaverRouter";
37
+ } from "./Router.js";
38
+ import type { ServerContext } from "./ServerContext.js";
39
+ import type {
40
+ StateRequirementError,
41
+ TypedMiddleware,
42
+ } from "./TypedMiddleware.js";
43
+ import type { TypeweaverRouter } from "./TypeweaverRouter.js";
39
44
 
40
45
  /**
41
46
  * The main application class that provides routing, middleware, and
@@ -79,8 +84,12 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
79
84
  private readonly onError: (error: unknown) => void;
80
85
 
81
86
  public constructor(options?: TypeweaverAppOptions) {
82
- this.adapter = new FetchApiAdapter({ maxBodySize: options?.maxBodySize });
83
- this.onError = options?.onError ?? console.error;
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
+ });
84
93
  }
85
94
 
86
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
+ }
@@ -12,7 +12,7 @@ import type {
12
12
  IRequestValidator,
13
13
  IResponseValidator,
14
14
  } from "@rexeus/typeweaver-core";
15
- import type { RequestHandler } from "./RequestHandler";
15
+ import type { RequestHandler } from "./RequestHandler.js";
16
16
  import type {
17
17
  HttpResponseErrorHandler,
18
18
  ResponseValidationErrorHandler,
@@ -20,7 +20,7 @@ import type {
20
20
  RouterErrorConfig,
21
21
  UnknownErrorHandler,
22
22
  RequestValidationErrorHandler,
23
- } from "./Router";
23
+ } from "./Router.js";
24
24
 
25
25
  /**
26
26
  * Configuration options for TypeweaverRouter instances.
package/dist/lib/index.ts CHANGED
@@ -7,11 +7,11 @@
7
7
 
8
8
  /* oxlint-disable import/max-dependencies */
9
9
 
10
- export { TypeweaverApp, type TypeweaverAppOptions } from "./TypeweaverApp";
10
+ export { TypeweaverApp, type TypeweaverAppOptions } from "./TypeweaverApp.js";
11
11
  export {
12
12
  TypeweaverRouter,
13
13
  type TypeweaverRouterOptions,
14
- } from "./TypeweaverRouter";
14
+ } from "./TypeweaverRouter.js";
15
15
  export { HttpMethod } from "@rexeus/typeweaver-core";
16
16
  export type {
17
17
  HttpResponseErrorHandler,
@@ -19,25 +19,26 @@ export type {
19
19
  ResponseValidationErrorHandler,
20
20
  RouteMetadata,
21
21
  UnknownErrorHandler,
22
- } from "./Router";
23
- export type { ServerContext } from "./ServerContext";
24
- export type { RequestHandler } from "./RequestHandler";
25
- export { StateMap } from "./StateMap";
22
+ } from "./Router.js";
23
+ export type { ServerContext } from "./ServerContext.js";
24
+ export type { RequestHandler } from "./RequestHandler.js";
25
+ export { StateMap } from "./StateMap.js";
26
26
  export {
27
27
  defineMiddleware,
28
28
  type InferState,
29
29
  type NextFn,
30
30
  type StateRequirementError,
31
31
  type TypedMiddleware,
32
- } from "./TypedMiddleware";
32
+ } from "./TypedMiddleware.js";
33
33
  export {
34
34
  BodyParseError,
35
35
  PayloadTooLargeError,
36
+ RequestBodyDrainTimeoutError,
36
37
  ResponseSerializationError,
37
- } from "./Errors";
38
- export { FetchApiAdapter } from "./FetchApiAdapter";
39
- export { nodeAdapter, type NodeAdapterOptions } from "./NodeAdapter";
40
- export { pathMatcher } from "./PathMatcher";
38
+ } from "./Errors.js";
39
+ export { FetchApiAdapter } from "./FetchApiAdapter.js";
40
+ export { nodeAdapter, type NodeAdapterOptions } from "./NodeAdapter.js";
41
+ export { pathMatcher } from "./PathMatcher.js";
41
42
  export {
42
43
  basicAuth,
43
44
  type BasicAuthOptions,
@@ -56,4 +57,4 @@ export {
56
57
  scoped,
57
58
  secureHeaders,
58
59
  type SecureHeadersOptions,
59
- } from "./middleware/index";
60
+ } from "./middleware/index.js";
@@ -3,8 +3,8 @@ import {
3
3
  unauthorizedDefaultError,
4
4
  } from "@rexeus/typeweaver-core";
5
5
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
6
- import { defineMiddleware } from "../TypedMiddleware";
7
- import type { ServerContext } from "../ServerContext";
6
+ import { defineMiddleware } from "../TypedMiddleware.js";
7
+ import type { ServerContext } from "../ServerContext.js";
8
8
 
9
9
  export type BasicAuthOptions = {
10
10
  readonly verifyCredentials: (
@@ -3,8 +3,8 @@ import {
3
3
  unauthorizedDefaultError,
4
4
  } from "@rexeus/typeweaver-core";
5
5
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
6
- import { defineMiddleware } from "../TypedMiddleware";
7
- import type { ServerContext } from "../ServerContext";
6
+ import { defineMiddleware } from "../TypedMiddleware.js";
7
+ import type { ServerContext } from "../ServerContext.js";
8
8
 
9
9
  export type BearerAuthOptions = {
10
10
  readonly verifyToken: (
@@ -1,5 +1,5 @@
1
1
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
- import { defineMiddleware } from "../TypedMiddleware";
2
+ import { defineMiddleware } from "../TypedMiddleware.js";
3
3
 
4
4
  export type CorsOptions = {
5
5
  readonly origin?:
@@ -1,8 +1,8 @@
1
- export { basicAuth, type BasicAuthOptions } from "./basicAuth";
2
- export { bearerAuth, type BearerAuthOptions } from "./bearerAuth";
3
- export { cors, type CorsOptions } from "./cors";
4
- export { logger, type LogData, type LoggerOptions } from "./logger";
5
- export { poweredBy, type PoweredByOptions } from "./poweredBy";
6
- export { requestId, type RequestIdOptions } from "./requestId";
7
- export { scoped, except } from "./scoped";
8
- export { secureHeaders, type SecureHeadersOptions } from "./secureHeaders";
1
+ export { basicAuth, type BasicAuthOptions } from "./basicAuth.js";
2
+ export { bearerAuth, type BearerAuthOptions } from "./bearerAuth.js";
3
+ export { cors, type CorsOptions } from "./cors.js";
4
+ export { logger, type LogData, type LoggerOptions } from "./logger.js";
5
+ export { poweredBy, type PoweredByOptions } from "./poweredBy.js";
6
+ export { requestId, type RequestIdOptions } from "./requestId.js";
7
+ export { scoped, except } from "./scoped.js";
8
+ export { secureHeaders, type SecureHeadersOptions } from "./secureHeaders.js";
@@ -1,4 +1,4 @@
1
- import { defineMiddleware } from "../TypedMiddleware";
1
+ import { defineMiddleware } from "../TypedMiddleware.js";
2
2
 
3
3
  export type LogData = {
4
4
  readonly method: string;
@@ -1,5 +1,5 @@
1
1
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
- import { defineMiddleware } from "../TypedMiddleware";
2
+ import { defineMiddleware } from "../TypedMiddleware.js";
3
3
 
4
4
  export type PoweredByOptions = {
5
5
  readonly name?: string;
@@ -1,5 +1,5 @@
1
1
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
- import { defineMiddleware } from "../TypedMiddleware";
2
+ import { defineMiddleware } from "../TypedMiddleware.js";
3
3
 
4
4
  export type RequestIdOptions = {
5
5
  readonly headerName?: string;
@@ -1,6 +1,6 @@
1
- import { pathMatcher } from "../PathMatcher";
2
- import { defineMiddleware } from "../TypedMiddleware";
3
- import type { TypedMiddleware } from "../TypedMiddleware";
1
+ import { pathMatcher } from "../PathMatcher.js";
2
+ import { defineMiddleware } from "../TypedMiddleware.js";
3
+ import type { TypedMiddleware } from "../TypedMiddleware.js";
4
4
 
5
5
  /**
6
6
  * Restricts a middleware to only run on paths matching the given patterns.
@@ -1,5 +1,5 @@
1
1
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
- import { defineMiddleware } from "../TypedMiddleware";
2
+ import { defineMiddleware } from "../TypedMiddleware.js";
3
3
 
4
4
  export type SecureHeadersOptions = {
5
5
  readonly contentTypeOptions?: string | false;
@@ -6,12 +6,12 @@
6
6
  * @generated by @rexeus/typeweaver
7
7
  */
8
8
 
9
- import { HttpMethod, TypeweaverRouter, type RequestHandler, type TypeweaverRouterOptions } from "<%- coreDir %>/lib/server";
9
+ import { HttpMethod, TypeweaverRouter, type RequestHandler, type TypeweaverRouterOptions } from "<%- coreDir %>/lib/server/index.js";
10
10
  <% for (const operation of operations) { %>
11
- import type { I<%- operation.className %>Request } from "./<%- operation.className %>Request";
12
- import { <%- operation.className %>RequestValidator } from "./<%- operation.className %>RequestValidator";
13
- import type { <%- operation.className %>Response } from "./<%- operation.className %>Response";
14
- import { <%- operation.className %>ResponseValidator } from "./<%- operation.className %>ResponseValidator";
11
+ import type { I<%- operation.className %>Request } from "./<%- operation.className %>Request.js";
12
+ import { <%- operation.className %>RequestValidator } from "./<%- operation.className %>RequestValidator.js";
13
+ import type { <%- operation.className %>Response } from "./<%- operation.className %>Response.js";
14
+ import { <%- operation.className %>ResponseValidator } from "./<%- operation.className %>ResponseValidator.js";
15
15
  <% } %>
16
16
 
17
17
  export type Server<%- pascalCaseEntityName %>ApiHandler<
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rexeus/typeweaver-server",
3
- "version": "0.10.1",
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.1",
51
- "@rexeus/typeweaver-gen": "^0.10.1"
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.1",
58
- "@rexeus/typeweaver-gen": "^0.10.1"
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",