dx-server 0.12.2 → 0.13.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +417 -293
  2. package/{cjs → lib}/body.d.ts +2 -3
  3. package/lib/body.js +10 -0
  4. package/lib/body.js.map +1 -0
  5. package/{esm → lib}/bodyHelpers.d.ts +2 -4
  6. package/lib/bodyHelpers.js +102 -0
  7. package/lib/bodyHelpers.js.map +1 -0
  8. package/{esm → lib}/dx.d.ts +6 -9
  9. package/lib/dx.js +133 -0
  10. package/lib/dx.js.map +1 -0
  11. package/{cjs → lib}/dxHelpers.d.ts +2 -5
  12. package/lib/dxHelpers.js +135 -0
  13. package/lib/dxHelpers.js.map +1 -0
  14. package/lib/helpers.js.map +1 -0
  15. package/{cjs → lib}/index.d.ts +1 -2
  16. package/{esm/index.d.ts → lib/index.js} +1 -2
  17. package/lib/index.js.map +1 -0
  18. package/lib/logger.d.ts +4 -0
  19. package/lib/logger.js +64 -0
  20. package/lib/logger.js.map +1 -0
  21. package/lib/router.d.ts +42 -0
  22. package/lib/router.js +43 -0
  23. package/lib/router.js.map +1 -0
  24. package/lib/static.js +23 -0
  25. package/lib/static.js.map +1 -0
  26. package/{cjs → lib}/staticHelpers.d.ts +5 -3
  27. package/lib/staticHelpers.js +202 -0
  28. package/lib/staticHelpers.js.map +1 -0
  29. package/{cjs → lib}/stream.d.ts +3 -8
  30. package/lib/stream.js +96 -0
  31. package/lib/stream.js.map +1 -0
  32. package/lib/vendors/contentType.js +64 -0
  33. package/lib/vendors/contentType.js.map +1 -0
  34. package/{cjs → lib}/vendors/etag.d.ts +2 -5
  35. package/lib/vendors/etag.js +94 -0
  36. package/lib/vendors/etag.js.map +1 -0
  37. package/{cjs → lib}/vendors/fresh.d.ts +2 -2
  38. package/lib/vendors/fresh.js +88 -0
  39. package/lib/vendors/fresh.js.map +1 -0
  40. package/lib/vendors/mime.d.ts +1 -0
  41. package/lib/vendors/mime.js +35 -0
  42. package/lib/vendors/mime.js.map +1 -0
  43. package/{cjs → lib}/vendors/mimeDb.d.ts +2544 -2544
  44. package/lib/vendors/mimeDb.js +9435 -0
  45. package/lib/vendors/mimeDb.js.map +1 -0
  46. package/{cjs → lib}/vendors/mimeScore.d.ts +1 -1
  47. package/lib/vendors/mimeScore.js +44 -0
  48. package/lib/vendors/mimeScore.js.map +1 -0
  49. package/{cjs → lib}/vendors/onFinished.d.ts +1 -1
  50. package/lib/vendors/onFinished.js +231 -0
  51. package/lib/vendors/rangeParser.d.ts +12 -0
  52. package/lib/vendors/rangeParser.js +108 -0
  53. package/lib/vendors/rangeParser.js.map +1 -0
  54. package/package.json +32 -36
  55. package/cjs/body.js +0 -14
  56. package/cjs/bodyHelpers.d.ts +0 -16
  57. package/cjs/bodyHelpers.js +0 -101
  58. package/cjs/connect.d.ts +0 -5
  59. package/cjs/connect.js +0 -44
  60. package/cjs/dx.d.ts +0 -46
  61. package/cjs/dx.js +0 -144
  62. package/cjs/dxHelpers.js +0 -123
  63. package/cjs/express.d.ts +0 -4
  64. package/cjs/express.js +0 -43
  65. package/cjs/helpers.js +0 -14
  66. package/cjs/index.js +0 -38
  67. package/cjs/logger.d.ts +0 -3
  68. package/cjs/logger.js +0 -61
  69. package/cjs/package.json +0 -3
  70. package/cjs/polyfillWithResolvers.d.ts +0 -1
  71. package/cjs/polyfillWithResolvers.js +0 -17
  72. package/cjs/router.js +0 -47
  73. package/cjs/static.js +0 -27
  74. package/cjs/staticHelpers.js +0 -195
  75. package/cjs/stream.js +0 -97
  76. package/cjs/vendors/contentType.js +0 -92
  77. package/cjs/vendors/etag.js +0 -136
  78. package/cjs/vendors/fresh.js +0 -102
  79. package/cjs/vendors/mime.d.ts +0 -1
  80. package/cjs/vendors/mime.js +0 -42
  81. package/cjs/vendors/mimeDb.js +0 -9417
  82. package/cjs/vendors/mimeScore.js +0 -50
  83. package/cjs/vendors/onFinished.js +0 -245
  84. package/cjs/vendors/rangeParser.d.ts +0 -10
  85. package/cjs/vendors/rangeParser.js +0 -126
  86. package/esm/body.d.ts +0 -8
  87. package/esm/body.js +0 -11
  88. package/esm/bodyHelpers.js +0 -90
  89. package/esm/connect.d.ts +0 -5
  90. package/esm/connect.js +0 -40
  91. package/esm/dx.js +0 -128
  92. package/esm/dxHelpers.d.ts +0 -49
  93. package/esm/dxHelpers.js +0 -119
  94. package/esm/express.d.ts +0 -4
  95. package/esm/express.js +0 -35
  96. package/esm/helpers.js +0 -3
  97. package/esm/index.js +0 -9
  98. package/esm/logger.d.ts +0 -3
  99. package/esm/logger.js +0 -57
  100. package/esm/polyfillWithResolvers.d.ts +0 -1
  101. package/esm/polyfillWithResolvers.js +0 -16
  102. package/esm/router.js +0 -44
  103. package/esm/static.d.ts +0 -5
  104. package/esm/static.js +0 -23
  105. package/esm/staticHelpers.d.ts +0 -18
  106. package/esm/staticHelpers.js +0 -188
  107. package/esm/stream.d.ts +0 -12
  108. package/esm/stream.js +0 -92
  109. package/esm/vendors/contentType.d.ts +0 -4
  110. package/esm/vendors/contentType.js +0 -88
  111. package/esm/vendors/etag.d.ts +0 -10
  112. package/esm/vendors/etag.js +0 -105
  113. package/esm/vendors/fresh.d.ts +0 -23
  114. package/esm/vendors/fresh.js +0 -96
  115. package/esm/vendors/mime.d.ts +0 -1
  116. package/esm/vendors/mime.js +0 -35
  117. package/esm/vendors/mimeDb.d.ts +0 -9413
  118. package/esm/vendors/mimeDb.js +0 -9415
  119. package/esm/vendors/mimeScore.d.ts +0 -5
  120. package/esm/vendors/mimeScore.js +0 -46
  121. package/esm/vendors/onFinished.d.ts +0 -14
  122. package/esm/vendors/onFinished.js +0 -241
  123. package/esm/vendors/rangeParser.d.ts +0 -10
  124. package/esm/vendors/rangeParser.js +0 -122
  125. /package/{cjs → lib}/helpers.d.ts +0 -0
  126. /package/{esm/helpers.d.ts → lib/helpers.js} +0 -0
  127. /package/{cjs → lib}/static.d.ts +0 -0
  128. /package/{cjs → lib}/vendors/contentType.d.ts +0 -0
@@ -1,8 +1,7 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import { type BufferBodyOptions } from './bodyHelpers.js';
3
- export declare const getBuffer: import("./dx.js").Context<Buffer | undefined, [options?: Partial<BufferBodyOptions> | undefined], any, (...np: any[]) => any>;
2
+ export declare const getBuffer: import("./dx.js").Context<Buffer<ArrayBufferLike> | undefined, [options?: Partial<BufferBodyOptions> | undefined], any, (...np: any[]) => any>;
4
3
  export declare const getJson: import("./dx.js").Context<any, [options?: Partial<BufferBodyOptions> | undefined], any, (...np: any[]) => any>;
5
- export declare const getRaw: import("./dx.js").Context<Buffer | undefined, [options?: Partial<BufferBodyOptions> | undefined], any, (...np: any[]) => any>;
4
+ export declare const getRaw: import("./dx.js").Context<Buffer<ArrayBufferLike> | undefined, [options?: Partial<BufferBodyOptions> | undefined], any, (...np: any[]) => any>;
6
5
  export declare const getText: import("./dx.js").Context<string | undefined, [options?: Partial<BufferBodyOptions> | undefined], any, (...np: any[]) => any>;
7
6
  export declare const getUrlEncoded: import("./dx.js").Context<any, [options?: Partial<BufferBodyOptions> | undefined], any, (...np: any[]) => any>;
8
7
  export declare const getQuery: import("./dx.js").Context<any, [options?: Partial<BufferBodyOptions> | undefined], any, (...np: any[]) => any>;
package/lib/body.js ADDED
@@ -0,0 +1,10 @@
1
+ import { getReq, makeDxContext } from './dx.js';
2
+ import { bufferFromReq, jsonFromReq, queryFromReq, rawFromReq, textFromReq, urlEncodedFromReq, } from './bodyHelpers.js';
3
+ export const getBuffer = makeDxContext((options) => bufferFromReq(getReq(), options));
4
+ export const getJson = makeDxContext((options) => jsonFromReq(getReq(), options));
5
+ export const getRaw = makeDxContext((options) => rawFromReq(getReq(), options));
6
+ export const getText = makeDxContext((options) => textFromReq(getReq(), options));
7
+ export const getUrlEncoded = makeDxContext((options) => urlEncodedFromReq(getReq(), options));
8
+ export const getQuery = makeDxContext((options) => queryFromReq(getReq(), options));
9
+ // to getFile use busboy
10
+ // https://github.com/mscdex/busboy
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.js","sourceRoot":"","sources":["../src/body.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAE,aAAa,EAAC,MAAM,SAAS,CAAA;AAC7C,OAAO,EAEN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,iBAAiB,GACjB,MAAM,kBAAkB,CAAA;AAEzB,MAAM,CAAC,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAClH,MAAM,CAAC,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAC9G,MAAM,CAAC,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAC5G,MAAM,CAAC,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAC9G,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CACnF,iBAAiB,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CACpC,CAAA;AACD,MAAM,CAAC,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAEhH,wBAAwB;AACxB,mCAAmC","sourcesContent":["import {getReq, makeDxContext} from './dx.js'\nimport {\n\ttype BufferBodyOptions,\n\tbufferFromReq,\n\tjsonFromReq,\n\tqueryFromReq,\n\trawFromReq,\n\ttextFromReq,\n\turlEncodedFromReq,\n} from './bodyHelpers.js'\n\nexport const getBuffer = makeDxContext((options?: Partial<BufferBodyOptions>) => bufferFromReq(getReq(), options))\nexport const getJson = makeDxContext((options?: Partial<BufferBodyOptions>) => jsonFromReq(getReq(), options))\nexport const getRaw = makeDxContext((options?: Partial<BufferBodyOptions>) => rawFromReq(getReq(), options))\nexport const getText = makeDxContext((options?: Partial<BufferBodyOptions>) => textFromReq(getReq(), options))\nexport const getUrlEncoded = makeDxContext((options?: Partial<BufferBodyOptions>) =>\n\turlEncodedFromReq(getReq(), options),\n)\nexport const getQuery = makeDxContext((options?: Partial<BufferBodyOptions>) => queryFromReq(getReq(), options))\n\n// to getFile use busboy\n// https://github.com/mscdex/busboy\n"]}
@@ -1,5 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node" resolution-mode="require"/>
3
1
  import { IncomingMessage } from 'node:http';
4
2
  export interface BufferBodyOptions {
5
3
  bodyLimit: number;
@@ -7,9 +5,9 @@ export interface BufferBodyOptions {
7
5
  queryParser?(search: string): any;
8
6
  }
9
7
  export declare function setBufferBodyDefaultOptions(options: Partial<BufferBodyOptions>): void;
10
- export declare function bufferFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>): Promise<Buffer | undefined>;
8
+ export declare function bufferFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>): Promise<Buffer<ArrayBufferLike> | undefined>;
11
9
  export declare function jsonFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>): Promise<any>;
12
- export declare function rawFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>): Promise<Buffer | undefined>;
10
+ export declare function rawFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>): Promise<Buffer<ArrayBufferLike> | undefined>;
13
11
  export declare function textFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>): Promise<string | undefined>;
14
12
  export declare function urlEncodedFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>): Promise<any>;
15
13
  export declare function urlFromReq(req: IncomingMessage): URL;
@@ -0,0 +1,102 @@
1
+ import { getContentStream, readStream } from './stream.js';
2
+ import { parseContentType } from './vendors/contentType.js';
3
+ function defaultQueryParser(search) {
4
+ return Object.fromEntries(new URLSearchParams(search)); // support both leading ? and not
5
+ }
6
+ let bodyDefaultOptions = { bodyLimit: 100 << 10 }; // 100kb
7
+ export function setBufferBodyDefaultOptions(options) {
8
+ bodyDefaultOptions = { ...bodyDefaultOptions, ...options };
9
+ }
10
+ export async function bufferFromReq(req, options) {
11
+ const { bodyLimit } = { ...bodyDefaultOptions, ...options };
12
+ /**
13
+ * Check if a request has a request body.
14
+ * A request with a body __must__ either have `transfer-encoding`
15
+ * or `content-length` headers set.
16
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
17
+ */
18
+ // https://github.com/jshttp/type-is/blob/cdcfe23e9833872e425b0aaf71ca0311373b6116/index.js#L92
19
+ const contentLengthParsed = parseInt(req.headers['content-length'] ?? '', 10);
20
+ if (req.headers['transfer-encoding'] === undefined && isNaN(contentLengthParsed))
21
+ return;
22
+ const contentLength = isNaN(contentLengthParsed) ? undefined : contentLengthParsed;
23
+ // read
24
+ const encoding = (req.headers['content-encoding'] ?? 'identity').toLowerCase();
25
+ const stream = getContentStream(req, encoding);
26
+ try {
27
+ return await readStream(stream, {
28
+ length: encoding === 'identity' ? contentLength : undefined,
29
+ limit: bodyLimit,
30
+ });
31
+ }
32
+ catch (e) {
33
+ // On rejection (e.g. body limit exceeded) tear down the decompressor and unpipe it
34
+ // from req. Otherwise req keeps feeding the decompressor and a zip-bomb keeps inflating
35
+ // long after the limit fired. req itself is left alive so error middleware can respond.
36
+ if (stream !== req) {
37
+ req.unpipe(stream);
38
+ stream.destroy();
39
+ }
40
+ throw e;
41
+ }
42
+ }
43
+ // if content-type is not as expected, return undefined
44
+ function forceGetContentTypeParams(req, expected) {
45
+ const contentTypeRaw = req.headers['content-type'];
46
+ if (!contentTypeRaw)
47
+ return;
48
+ const { mediaType, parameters } = parseContentType(contentTypeRaw);
49
+ if (mediaType !== expected)
50
+ return;
51
+ return parameters;
52
+ }
53
+ function forceGetCharset(req, expected) {
54
+ const parameters = forceGetContentTypeParams(req, expected);
55
+ if (!parameters)
56
+ return;
57
+ // assert charset per RFC 7159 sec 8.1
58
+ const charset = parameters.charset?.toLowerCase() || 'utf-8';
59
+ // positive allowlist: utf-7 etc. would pass a `startsWith('utf-')` check but is not a valid
60
+ // Buffer encoding (and utf-7 is an XSS smuggling vector)
61
+ if (!['utf-8', 'utf8', 'utf-16le', 'utf16le'].includes(charset))
62
+ throw new Error(`unsupported charset "${charset.toUpperCase()}"`);
63
+ return charset;
64
+ }
65
+ export async function jsonFromReq(req, options) {
66
+ const charset = forceGetCharset(req, 'application/json');
67
+ if (!charset)
68
+ return;
69
+ const buffer = await bufferFromReq(req, options);
70
+ if (buffer) {
71
+ const str = buffer.toString(charset);
72
+ return str ? JSON.parse(str) : undefined;
73
+ }
74
+ }
75
+ export async function rawFromReq(req, options) {
76
+ if (!forceGetContentTypeParams(req, 'application/octet-stream'))
77
+ return;
78
+ return await bufferFromReq(req, options);
79
+ }
80
+ export async function textFromReq(req, options) {
81
+ const charset = forceGetCharset(req, 'text/plain');
82
+ if (!charset)
83
+ return;
84
+ const buffer = await bufferFromReq(req, options);
85
+ if (buffer)
86
+ return buffer.toString(charset);
87
+ }
88
+ export async function urlEncodedFromReq(req, options) {
89
+ const charset = forceGetCharset(req, 'application/x-www-form-urlencoded');
90
+ if (!charset)
91
+ return;
92
+ const buffer = await bufferFromReq(req, options);
93
+ if (buffer) {
94
+ return (options?.urlEncodedParser ?? bodyDefaultOptions.urlEncodedParser ?? defaultQueryParser)(buffer.toString(charset));
95
+ }
96
+ }
97
+ export function urlFromReq(req) {
98
+ return new URL(req.url ?? '', 'https://example.com');
99
+ }
100
+ export function queryFromReq(req, options) {
101
+ return (options?.queryParser ?? bodyDefaultOptions.queryParser ?? defaultQueryParser)(urlFromReq(req).searchParams.toString());
102
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bodyHelpers.js","sourceRoot":"","sources":["../src/bodyHelpers.ts"],"names":[],"mappings":"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","sourcesContent":["import {IncomingMessage} from 'node:http'\nimport {getContentStream, readStream} from './stream.js'\nimport {parseContentType} from './vendors/contentType.js'\n\nexport interface BufferBodyOptions {\n\tbodyLimit: number\n\turlEncodedParser?(search: string): any\n\tqueryParser?(search: string): any\n}\n\nfunction defaultQueryParser(search: string) {\n\treturn Object.fromEntries(new URLSearchParams(search)) // support both leading ? and not\n}\n\nlet bodyDefaultOptions: BufferBodyOptions = {bodyLimit: 100 << 10} // 100kb\nexport function setBufferBodyDefaultOptions(options: Partial<BufferBodyOptions>) {\n\tbodyDefaultOptions = {...bodyDefaultOptions, ...options}\n}\n\nexport async function bufferFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tconst {bodyLimit} = {...bodyDefaultOptions, ...options}\n\t/**\n\t * Check if a request has a request body.\n\t * A request with a body __must__ either have `transfer-encoding`\n\t * or `content-length` headers set.\n\t * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3\n\t */\n\t// https://github.com/jshttp/type-is/blob/cdcfe23e9833872e425b0aaf71ca0311373b6116/index.js#L92\n\tconst contentLengthParsed = parseInt(req.headers['content-length'] ?? '', 10)\n\tif (req.headers['transfer-encoding'] === undefined && isNaN(contentLengthParsed)) return\n\tconst contentLength = isNaN(contentLengthParsed) ? undefined : contentLengthParsed\n\n\t// read\n\tconst encoding = (req.headers['content-encoding'] ?? 'identity').toLowerCase()\n\tconst stream = getContentStream(req, encoding)\n\ttry {\n\t\treturn await readStream(stream, {\n\t\t\tlength: encoding === 'identity' ? contentLength : undefined,\n\t\t\tlimit: bodyLimit,\n\t\t})\n\t} catch (e) {\n\t\t// On rejection (e.g. body limit exceeded) tear down the decompressor and unpipe it\n\t\t// from req. Otherwise req keeps feeding the decompressor and a zip-bomb keeps inflating\n\t\t// long after the limit fired. req itself is left alive so error middleware can respond.\n\t\tif (stream !== req) {\n\t\t\treq.unpipe(stream as NodeJS.WritableStream)\n\t\t\tstream.destroy()\n\t\t}\n\t\tthrow e\n\t}\n}\n\n// if content-type is not as expected, return undefined\nfunction forceGetContentTypeParams(req: IncomingMessage, expected: string) {\n\tconst contentTypeRaw = req.headers['content-type']\n\tif (!contentTypeRaw) return\n\tconst {mediaType, parameters} = parseContentType(contentTypeRaw)\n\tif (mediaType !== expected) return\n\n\treturn parameters\n}\nfunction forceGetCharset(req: IncomingMessage, expected: string) {\n\tconst parameters = forceGetContentTypeParams(req, expected)\n\tif (!parameters) return\n\t// assert charset per RFC 7159 sec 8.1\n\tconst charset = (parameters.charset?.toLowerCase() as BufferEncoding) || 'utf-8'\n\t// positive allowlist: utf-7 etc. would pass a `startsWith('utf-')` check but is not a valid\n\t// Buffer encoding (and utf-7 is an XSS smuggling vector)\n\tif (!['utf-8', 'utf8', 'utf-16le', 'utf16le'].includes(charset))\n\t\tthrow new Error(`unsupported charset \"${charset.toUpperCase()}\"`)\n\n\treturn charset\n}\n\nexport async function jsonFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tconst charset = forceGetCharset(req, 'application/json')\n\tif (!charset) return\n\tconst buffer = await bufferFromReq(req, options)\n\tif (buffer) {\n\t\tconst str = buffer.toString(charset)\n\t\treturn str ? JSON.parse(str) : undefined\n\t}\n}\n\nexport async function rawFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tif (!forceGetContentTypeParams(req, 'application/octet-stream')) return\n\treturn await bufferFromReq(req, options)\n}\n\nexport async function textFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tconst charset = forceGetCharset(req, 'text/plain')\n\tif (!charset) return\n\tconst buffer = await bufferFromReq(req, options)\n\tif (buffer) return buffer.toString(charset)\n}\n\nexport async function urlEncodedFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tconst charset = forceGetCharset(req, 'application/x-www-form-urlencoded')\n\tif (!charset) return\n\tconst buffer = await bufferFromReq(req, options)\n\tif (buffer) {\n\t\treturn (bodyDefaultOptions.urlEncodedParser ?? options?.urlEncodedParser ?? defaultQueryParser)(\n\t\t\tbuffer.toString(charset),\n\t\t)\n\t}\n}\n\nexport function urlFromReq(req: IncomingMessage) {\n\treturn new URL(req.url ?? '', 'https://example.com')\n}\n\nexport function queryFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\treturn (bodyDefaultOptions.queryParser ?? options?.queryParser ?? defaultQueryParser)(\n\t\turlFromReq(req).searchParams.toString(),\n\t)\n}\n"]}
@@ -1,26 +1,23 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node" resolution-mode="require"/>
3
- /// <reference types="node" resolution-mode="require"/>
4
1
  import { Readable } from 'node:stream';
5
2
  import type { IncomingMessage, ServerResponse } from 'node:http';
6
3
  import type { SendFileOptions } from './staticHelpers.js';
7
- export interface Chainable<P extends any[] = any[], R = any, Next = (...np: any[]) => any> {
8
- (next: Next, ...p: P): R;
4
+ export interface Chainable<R = any, Next = (...np: any[]) => any> {
5
+ (next: Next): R;
9
6
  }
10
- export interface Context<T, Params extends any[], R = any, Next = (...np: any[]) => any> {
7
+ export interface Context<T, Params extends any[] = any[], R = any, Next = (...np: any[]) => any> {
11
8
  value: Awaited<T>;
12
9
  get(req: IncomingMessage): T;
13
10
  set(req: IncomingMessage, value: T): void;
14
11
  (...params: Params): Promise<T>;
15
- chain(...params: Params): Chainable<Params, R, Next>;
12
+ chain(...params: Params): Chainable<R, Next>;
16
13
  }
17
- export declare function makeDxContext<T, Params extends any[], R = any, Next = (...np: any[]) => any>(maker: (...params: Params) => T | Promise<T>): Context<T, Params, R, Next>;
14
+ export declare function makeDxContext<T, Params extends any[] = any[], R = any, Next = (...np: any[]) => any>(maker: (...params: Params) => T | Promise<T>): Context<T, Params, R, Next>;
18
15
  export declare function dxServer(req: IncomingMessage, res: ServerResponse, options?: {
19
16
  jsonBeautify?: boolean;
20
17
  disableEtag?: boolean;
21
18
  }): Chainable;
22
19
  export declare function getReq(): IncomingMessage;
23
- export declare function getRes(): ServerResponse;
20
+ export declare function getRes(): ServerResponse<IncomingMessage>;
24
21
  export declare function setText(text: string, { status }?: {
25
22
  status?: number;
26
23
  }): void;
package/lib/dx.js ADDED
@@ -0,0 +1,133 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { writeRes } from './dxHelpers.js';
3
+ export function makeDxContext(maker) {
4
+ const promiseMap = new WeakMap();
5
+ const valueMap = new WeakMap();
6
+ const context = ((...params) => {
7
+ const req = getReq();
8
+ if (!promiseMap.has(req))
9
+ promiseMap.set(req, (async () => {
10
+ const value = await maker(...params);
11
+ valueMap.set(req, value);
12
+ return value;
13
+ })());
14
+ return promiseMap.get(req);
15
+ });
16
+ Object.defineProperty(context, 'value', {
17
+ get() {
18
+ return valueMap.get(getReq());
19
+ },
20
+ set(value) {
21
+ const req = getReq();
22
+ promiseMap.set(req, Promise.resolve(value));
23
+ valueMap.set(req, value);
24
+ },
25
+ });
26
+ context.chain = ((...params) => async (next) => {
27
+ await context(...params);
28
+ return next();
29
+ });
30
+ context.set = (req, value) => {
31
+ promiseMap.set(req, Promise.resolve(value));
32
+ valueMap.set(req, value);
33
+ };
34
+ context.get = req => valueMap.get(req);
35
+ return context;
36
+ }
37
+ const requestStorage = new AsyncLocalStorage();
38
+ const dxContext = makeDxContext(options => ({ ...options }));
39
+ export function dxServer(req, res, options = {}) {
40
+ return async (next) => {
41
+ dxContext.set(req, { ...options });
42
+ const result = await requestStorage.run({ req, res }, next);
43
+ await writeRes(req, res, dxContext.get(req));
44
+ return result;
45
+ };
46
+ }
47
+ // method: verb
48
+ // url: full url without server, protocol, port.
49
+ // headers: if headers are repeated, they are joined by comma. Header names are lowercased.
50
+ // rawHeaders: list of header name and value in a flat array. Case is preserved.
51
+ export function getReq() {
52
+ return requestStorage.getStore().req;
53
+ }
54
+ export function getRes() {
55
+ return requestStorage.getStore().res;
56
+ }
57
+ export function setText(text, { status } = {}) {
58
+ const res = getRes();
59
+ const dx = dxContext.value;
60
+ if (status)
61
+ res.statusCode = status;
62
+ dx.data = text;
63
+ dx.type = 'text';
64
+ }
65
+ export function setEmpty({ status } = {}) {
66
+ const res = getRes();
67
+ const dx = dxContext.value;
68
+ if (status)
69
+ res.statusCode = status;
70
+ dx.data = undefined;
71
+ dx.type = 'empty';
72
+ }
73
+ export function setHtml(html, opts = {}) {
74
+ setText(html, opts);
75
+ const dx = dxContext.value;
76
+ dx.type = 'html';
77
+ }
78
+ export function setFile(filePath, options) {
79
+ const dx = dxContext.value;
80
+ dx.data = filePath;
81
+ dx.type = 'file';
82
+ dx.options = options;
83
+ }
84
+ export function setBuffer(buffer, { status } = {}) {
85
+ const res = getRes();
86
+ const dx = dxContext.value;
87
+ if (status)
88
+ res.statusCode = status;
89
+ dx.data = buffer;
90
+ dx.type = 'buffer';
91
+ }
92
+ export function setNodeStream(stream, { status } = {}) {
93
+ const res = getRes();
94
+ const dx = dxContext.value;
95
+ if (status)
96
+ res.statusCode = status;
97
+ dx.data = stream;
98
+ dx.type = 'nodeStream';
99
+ }
100
+ export function setWebStream(stream, { status } = {}) {
101
+ const res = getRes();
102
+ const dx = dxContext.value;
103
+ if (status)
104
+ res.statusCode = status;
105
+ dx.data = stream;
106
+ dx.type = 'webStream';
107
+ }
108
+ export function setJson(json, { status } = {}) {
109
+ const res = getRes();
110
+ if (status)
111
+ res.statusCode = status;
112
+ const dx = dxContext.value;
113
+ dx.data = json;
114
+ dx.type = 'json';
115
+ }
116
+ export function setRedirect(url, status) {
117
+ const res = getRes();
118
+ const dx = dxContext.value;
119
+ res.statusCode = status;
120
+ dx.data = url;
121
+ dx.type = 'redirect';
122
+ }
123
+ // for download, set content-disposition header
124
+ // res.setHeader('Content-disposition', 'attachment; filename=my-movie.MOV')
125
+ // res.setHeader('Content-type', 'video/quicktime')
126
+ // fileStream.pipe(res)
127
+ // or
128
+ // send(req, filePath, options).pipe(res) // which will set content-type, content-length, and other cache related headers like staticHelpers.sendFile
129
+ // implementing this require a strict validation for the type (attachment) and filename.
130
+ // For example: express relies on this
131
+ // https://github.com/jshttp/content-disposition/blob/1037e24e4790273da96645ad250061f39e77968c/index.js#L186
132
+ // because in most applications, users can specify a simple filename which usually doesn't need to be validated.
133
+ // we leave setDownload() implementation for users, for now.
package/lib/dx.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dx.js","sourceRoot":"","sources":["../src/dx.ts"],"names":[],"mappings":"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","sourcesContent":["import {Readable} from 'node:stream'\nimport type {IncomingMessage, ServerResponse} from 'node:http'\nimport {AsyncLocalStorage} from 'node:async_hooks'\nimport {type DxContext, writeRes} from './dxHelpers.js'\nimport type {SendFileOptions} from './staticHelpers.js'\n\nexport interface Chainable<R = any, Next = (...np: any[]) => any> {\n\t(next: Next): R\n}\n\nexport interface Context<T, Params extends any[] = any[], R = any, Next = (...np: any[]) => any> {\n\tvalue: Awaited<T> // can be undefined\n\tget(req: IncomingMessage): T\n\tset(req: IncomingMessage, value: T): void\n\t(...params: Params): Promise<T>\n\tchain(...params: Params): Chainable<R, Next>\n}\nexport function makeDxContext<T, Params extends any[] = any[], R = any, Next = (...np: any[]) => any>(\n\tmaker: (...params: Params) => T | Promise<T>,\n): Context<T, Params, R, Next> {\n\tconst promiseMap = new WeakMap<IncomingMessage, Promise<T>>()\n\tconst valueMap = new WeakMap<IncomingMessage, T>()\n\tconst context = ((...params: Params) => {\n\t\tconst req = getReq()\n\t\tif (!promiseMap.has(req))\n\t\t\tpromiseMap.set(\n\t\t\t\treq,\n\t\t\t\t(async () => {\n\t\t\t\t\tconst value = await maker(...params)\n\t\t\t\t\tvalueMap.set(req, value)\n\t\t\t\t\treturn value\n\t\t\t\t})(),\n\t\t\t)\n\t\treturn promiseMap.get(req)!\n\t}) as Context<T, Params, R, Next>\n\tObject.defineProperty(context, 'value', {\n\t\tget() {\n\t\t\treturn valueMap.get(getReq())\n\t\t},\n\t\tset(value) {\n\t\t\tconst req = getReq()\n\t\t\tpromiseMap.set(req, Promise.resolve(value))\n\t\t\tvalueMap.set(req, value)\n\t\t},\n\t})\n\tcontext.chain = ((...params: Params) =>\n\t\tasync (next: Next) => {\n\t\t\tawait context(...params)\n\t\t\treturn (next as (...args: any[]) => any)()\n\t\t}) as Context<T, Params, R, Next>['chain']\n\tcontext.set = (req, value) => {\n\t\tpromiseMap.set(req, Promise.resolve(value))\n\t\tvalueMap.set(req, value)\n\t}\n\tcontext.get = req => valueMap.get(req) as T\n\treturn context\n}\n\nconst requestStorage = new AsyncLocalStorage<{\n\treq: IncomingMessage\n\tres: ServerResponse\n}>()\nconst dxContext = makeDxContext<DxContext>(options => ({...options}) as DxContext)\nexport function dxServer(\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\toptions: {\n\t\tjsonBeautify?: boolean\n\t\tdisableEtag?: boolean\n\t} = {},\n): Chainable {\n\treturn async next => {\n\t\tdxContext.set(req, {...options} as DxContext)\n\t\tconst result = await requestStorage.run({req, res}, next)\n\t\tawait writeRes(req, res, dxContext.get(req))\n\t\treturn result\n\t}\n}\n\n// method: verb\n// url: full url without server, protocol, port.\n// headers: if headers are repeated, they are joined by comma. Header names are lowercased.\n// rawHeaders: list of header name and value in a flat array. Case is preserved.\nexport function getReq() {\n\treturn requestStorage.getStore()!.req\n}\nexport function getRes() {\n\treturn requestStorage.getStore()!.res\n}\n\nexport function setText(text: string, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = text\n\tdx.type = 'text'\n}\n\nexport function setEmpty({status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = undefined\n\tdx.type = 'empty'\n}\n\nexport function setHtml(html: string, opts: {status?: number} = {}) {\n\tsetText(html, opts)\n\tconst dx = dxContext.value\n\tdx.type = 'html'\n}\n\nexport function setFile(filePath: string, options?: SendFileOptions) {\n\tconst dx = dxContext.value\n\tdx.data = filePath\n\tdx.type = 'file'\n\tdx.options = options\n}\n\nexport function setBuffer(buffer: Buffer, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = buffer\n\tdx.type = 'buffer'\n}\n\nexport function setNodeStream(stream: Readable, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = stream\n\tdx.type = 'nodeStream'\n}\n\nexport function setWebStream(stream: ReadableStream, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = stream\n\tdx.type = 'webStream'\n}\n\nexport function setJson(json: any, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tif (status) res.statusCode = status\n\n\tconst dx = dxContext.value\n\tdx.data = json\n\tdx.type = 'json'\n}\n\nexport function setRedirect(url: string, status: 301 | 302) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tres.statusCode = status\n\tdx.data = url\n\tdx.type = 'redirect'\n}\n\n// for download, set content-disposition header\n// res.setHeader('Content-disposition', 'attachment; filename=my-movie.MOV')\n\n// res.setHeader('Content-type', 'video/quicktime')\n// fileStream.pipe(res)\n// or\n// send(req, filePath, options).pipe(res) // which will set content-type, content-length, and other cache related headers like staticHelpers.sendFile\n\n// implementing this require a strict validation for the type (attachment) and filename.\n// For example: express relies on this\n// https://github.com/jshttp/content-disposition/blob/1037e24e4790273da96645ad250061f39e77968c/index.js#L186\n// because in most applications, users can specify a simple filename which usually doesn't need to be validated.\n// we leave setDownload() implementation for users, for now.\n"]}
@@ -1,10 +1,7 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node" resolution-mode="require"/>
3
- /// <reference types="node" resolution-mode="require"/>
4
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
5
2
  import { Readable } from 'node:stream';
3
+ import type { ReadableStream as WebReadableStream } from 'node:stream/web';
6
4
  import { type SendFileOptions } from './staticHelpers.js';
7
- import './polyfillWithResolvers.js';
8
5
  export type DxContext = {
9
6
  charset?: BufferEncoding;
10
7
  jsonBeautify?: boolean;
@@ -39,7 +36,7 @@ export type DxContext = {
39
36
  options: undefined;
40
37
  } | {
41
38
  type: 'webStream';
42
- data: ReadableStream;
39
+ data: WebReadableStream;
43
40
  options: undefined;
44
41
  } | {
45
42
  type: 'file';
@@ -0,0 +1,135 @@
1
+ import { Readable } from 'node:stream';
2
+ import { pipeline } from 'node:stream/promises';
3
+ import { promisify } from 'node:util';
4
+ import { entityTag, isFreshETag } from './vendors/etag.js';
5
+ import { sendFileTrusted } from './staticHelpers.js';
6
+ export async function writeRes(req, res, { type, data, charset, jsonBeautify, disableEtag, options }) {
7
+ if (res.headersSent)
8
+ return;
9
+ let buffer;
10
+ switch (type) {
11
+ case 'text':
12
+ case 'html':
13
+ setContentType(type === 'html' ? 'text/html' : 'text/plain');
14
+ buffer = Buffer.from(data ?? '', charset);
15
+ break;
16
+ case 'buffer':
17
+ setContentType('application/octet-stream');
18
+ buffer = data ?? Buffer.from('', charset);
19
+ break;
20
+ case 'json':
21
+ setContentType('application/json');
22
+ buffer =
23
+ data === undefined
24
+ ? Buffer.from('', charset)
25
+ : Buffer.from(jsonBeautify ? JSON.stringify(data, null, 2) : JSON.stringify(data), charset);
26
+ break;
27
+ case 'redirect':
28
+ case 'empty':
29
+ if (type === 'redirect')
30
+ res.setHeader('location', data);
31
+ buffer = Buffer.from('', charset);
32
+ break;
33
+ // Streaming paths own res.end() themselves (via pipeline/sendFileTrusted) and must be
34
+ // awaited to fulfil the "chain resolves after flush" invariant.
35
+ case 'nodeStream':
36
+ case 'webStream':
37
+ if (!data) {
38
+ buffer = Buffer.from('', charset);
39
+ break;
40
+ }
41
+ case 'file':
42
+ setContentType('application/octet-stream');
43
+ try {
44
+ if (type === 'file')
45
+ await sendFileTrusted(req, res, data, options);
46
+ else if (type === 'nodeStream')
47
+ await pipeline(data, res);
48
+ else if (type === 'webStream')
49
+ await pipeline(Readable.fromWeb(data), res);
50
+ }
51
+ catch (e) {
52
+ // A streaming helper (pipeline/sendFileTrusted) may already have destroyed res on
53
+ // error (e.g. fs open EACCES mid-stream). Calling res.end() on a destroyed response
54
+ // never resolves, so skip it — otherwise the chain hangs forever.
55
+ if (res.destroyed) {
56
+ // nothing to flush; res is already torn down
57
+ }
58
+ else if (!res.headersSent) {
59
+ res.statusCode = e?.statusCode ?? 500;
60
+ await promisify(res.end.bind(res))();
61
+ }
62
+ else if (!res.writableEnded)
63
+ res.destroy(e);
64
+ }
65
+ await awaitResFinished(res);
66
+ return;
67
+ case undefined:
68
+ // No setter was called. End the response with 404 instead of leaving it hung.
69
+ if (!res.headersSent)
70
+ res.statusCode = 404;
71
+ if (!res.writableEnded)
72
+ await promisify(res.end.bind(res))();
73
+ await awaitResFinished(res);
74
+ return;
75
+ default:
76
+ // Unknown type: programming error. Surface it via console.error but still finish
77
+ // res so the invariant holds (chain resolves only after flush).
78
+ console.error(new Error(`unsupported response type ${type}`));
79
+ if (!res.headersSent)
80
+ res.statusCode = 500;
81
+ if (!res.writableEnded)
82
+ await promisify(res.end.bind(res))();
83
+ await awaitResFinished(res);
84
+ return;
85
+ }
86
+ // 204 No Content and 304 Not Modified must not carry a body or Content-Length.
87
+ if (res.statusCode !== 204 && res.statusCode !== 304) {
88
+ // Content-Length and ETag mirror what a GET would send, so a HEAD reports them too.
89
+ res.setHeader('content-length', buffer.length);
90
+ // no ETag for redirects: the empty body would share the empty-body tag with other empty
91
+ // responses, so a cached If-None-Match could wrongly 304 a redirect (dropping Location)
92
+ if (!disableEtag && type !== 'redirect') {
93
+ const etag = entityTag(buffer);
94
+ res.setHeader('ETag', etag);
95
+ if (isFreshETag(req, etag))
96
+ res.statusCode = 304;
97
+ }
98
+ }
99
+ // 204/304 carry no body or representation/framing metadata (ETag is a validator, so it stays).
100
+ // This catches both an explicitly-set 204/304 and the freshETag -> 304 transition above.
101
+ if (res.statusCode === 204 || res.statusCode === 304) {
102
+ // https://github.com/expressjs/express/blob/980d881e3b023db079de60477a2588a91f046ca5/lib/response.js#L210
103
+ res.removeHeader('content-type');
104
+ res.removeHeader('content-length');
105
+ res.removeHeader('transfer-encoding');
106
+ }
107
+ else if (req.method !== 'HEAD')
108
+ res.write(buffer);
109
+ // we do not support content-encoding (gzip, deflate, br) and leave it to reverse proxy or CDN
110
+ await promisify(res.end.bind(res))();
111
+ await awaitResFinished(res);
112
+ function setContentType(contentType) {
113
+ if (res.headersSent || res.getHeader('content-type'))
114
+ return;
115
+ // only text/* carries a charset; binary (octet-stream) and JSON (always UTF-8 per RFC 8259) do not
116
+ const cs = charset ?? (contentType.startsWith('text/') ? 'utf-8' : undefined);
117
+ res.setHeader('content-type', `${contentType}${cs ? `; charset=${cs}` : ''}`);
118
+ }
119
+ }
120
+ // Resolves when res is fully flushed (finish) or the socket is gone (close).
121
+ // Used as the universal "we're done with this response" signal so every code path
122
+ // in writeRes can guarantee the chain doesn't unwind before bytes hit the wire.
123
+ function awaitResFinished(res) {
124
+ if (res.writableFinished || res.destroyed)
125
+ return Promise.resolve();
126
+ return new Promise(resolve => {
127
+ res.once('finish', done);
128
+ res.once('close', done);
129
+ function done() {
130
+ res.off('finish', done);
131
+ res.off('close', done);
132
+ resolve();
133
+ }
134
+ });
135
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dxHelpers.js","sourceRoot":"","sources":["../src/dxHelpers.ts"],"names":[],"mappings":"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","sourcesContent":["import type {IncomingMessage, ServerResponse} from 'node:http'\nimport {Readable} from 'node:stream'\nimport type {ReadableStream as WebReadableStream} from 'node:stream/web'\nimport {pipeline} from 'node:stream/promises'\nimport {promisify} from 'node:util'\nimport {entityTag, isFreshETag} from './vendors/etag.js'\nimport {sendFileTrusted, type SendFileOptions, type HttpError} from './staticHelpers.js'\n\nexport type DxContext = {\n\tcharset?: BufferEncoding // not for redirect\n\tjsonBeautify?: boolean // json only\n\tdisableEtag?: boolean\n} & (\n\t| {\n\t\t\ttype: 'empty'\n\t\t\tdata: undefined\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'text'\n\t\t\tdata: string\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'html'\n\t\t\tdata: string\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'buffer'\n\t\t\tdata: Buffer\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'json'\n\t\t\tdata: any\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'redirect'\n\t\t\tdata: string\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'nodeStream'\n\t\t\tdata: Readable\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'webStream'\n\t\t\tdata: WebReadableStream\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'file'\n\t\t\tdata: string\n\t\t\toptions?: SendFileOptions\n\t }\n)\n\nexport async function writeRes(\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\t{type, data, charset, jsonBeautify, disableEtag, options}: DxContext,\n) {\n\tif (res.headersSent) return\n\n\tlet buffer: Buffer | undefined\n\n\tswitch (type) {\n\t\tcase 'text':\n\t\tcase 'html':\n\t\t\tsetContentType(type === 'html' ? 'text/html' : 'text/plain')\n\t\t\tbuffer = Buffer.from(data ?? '', charset)\n\t\t\tbreak\n\t\tcase 'buffer':\n\t\t\tsetContentType('application/octet-stream')\n\t\t\tbuffer = data ?? Buffer.from('', charset)\n\t\t\tbreak\n\t\tcase 'json':\n\t\t\tsetContentType('application/json')\n\t\t\tbuffer =\n\t\t\t\tdata === undefined\n\t\t\t\t\t? Buffer.from('', charset)\n\t\t\t\t\t: Buffer.from(jsonBeautify ? JSON.stringify(data, null, 2) : JSON.stringify(data), charset)\n\t\t\tbreak\n\t\tcase 'redirect':\n\t\tcase 'empty':\n\t\t\tif (type === 'redirect') res.setHeader('location', data)\n\t\t\tbuffer = Buffer.from('', charset)\n\t\t\tbreak\n\t\t// Streaming paths own res.end() themselves (via pipeline/sendFileTrusted) and must be\n\t\t// awaited to fulfil the \"chain resolves after flush\" invariant.\n\t\tcase 'nodeStream':\n\t\tcase 'webStream':\n\t\t\tif (!data) {\n\t\t\t\tbuffer = Buffer.from('', charset)\n\t\t\t\tbreak\n\t\t\t}\n\t\tcase 'file':\n\t\t\tsetContentType('application/octet-stream')\n\t\t\ttry {\n\t\t\t\tif (type === 'file') await sendFileTrusted(req, res, data, options)\n\t\t\t\telse if (type === 'nodeStream') await pipeline(data, res)\n\t\t\t\telse if (type === 'webStream') await pipeline(Readable.fromWeb(data), res)\n\t\t\t} catch (e) {\n\t\t\t\t// A streaming helper (pipeline/sendFileTrusted) may already have destroyed res on\n\t\t\t\t// error (e.g. fs open EACCES mid-stream). Calling res.end() on a destroyed response\n\t\t\t\t// never resolves, so skip it — otherwise the chain hangs forever.\n\t\t\t\tif (res.destroyed) {\n\t\t\t\t\t// nothing to flush; res is already torn down\n\t\t\t\t} else if (!res.headersSent) {\n\t\t\t\t\tres.statusCode = (e as Partial<HttpError>)?.statusCode ?? 500\n\t\t\t\t\tawait promisify(res.end.bind(res))()\n\t\t\t\t} else if (!res.writableEnded) res.destroy(e as Error)\n\t\t\t\tconsole.error(e)\n\t\t\t}\n\t\t\tawait awaitResFinished(res)\n\t\t\treturn\n\t\tcase undefined:\n\t\t\t// No setter was called. End the response with 404 instead of leaving it hung.\n\t\t\tif (!res.headersSent) res.statusCode = 404\n\t\t\tif (!res.writableEnded) await promisify(res.end.bind(res))()\n\t\t\tawait awaitResFinished(res)\n\t\t\treturn\n\t\tdefault:\n\t\t\t// Unknown type: programming error. Surface it via console.error but still finish\n\t\t\t// res so the invariant holds (chain resolves only after flush).\n\t\t\tconsole.error(new Error(`unsupported response type ${type satisfies never}`))\n\t\t\tif (!res.headersSent) res.statusCode = 500\n\t\t\tif (!res.writableEnded) await promisify(res.end.bind(res))()\n\t\t\tawait awaitResFinished(res)\n\t\t\treturn\n\t}\n\n\t// https://github.com/expressjs/express/blob/980d881e3b023db079de60477a2588a91f046ca5/lib/response.js#L210\n\t// if (res.statusCode === 204) { // No Content\n\t// \tres.removeHeader('content-type')\n\t// \tres.removeHeader('content-length')\n\t// \tres.removeHeader('transfer-encoding')\n\t// \t// write nothing\n\t// }\n\t// if (res.statusCode === 205) { // reset content. Tell client to clear the form, etc.\n\t// \tres.setHeader('content-length', 0)\n\t// \tres.removeHeader('transfer-encoding')\n\t// } else\n\tif (req.method !== 'HEAD') {\n\t\tres.setHeader('content-length', buffer.length)\n\t\t// no ETag for redirects: the empty body would share the empty-body tag with other empty\n\t\t// responses, so a cached If-None-Match could wrongly 304 a redirect (dropping Location)\n\t\tif (!disableEtag && type !== 'redirect') {\n\t\t\tconst etag = entityTag(buffer)\n\t\t\tres.setHeader('ETag', etag)\n\t\t\tif (isFreshETag(req, etag)) {\n\t\t\t\tres.removeHeader('content-type')\n\t\t\t\tres.removeHeader('content-length')\n\t\t\t\tres.removeHeader('transfer-encoding')\n\t\t\t\tres.statusCode = 304\n\t\t\t} else res.write(buffer)\n\t\t} else res.write(buffer)\n\t\tawait promisify(res.end.bind(res))()\n\t}\n\t// we do not support content-encoding (gzip, deflate, br) and leave it to reverse proxy or CDN\n\n\tawait promisify(res.end.bind(res))()\n\n\tawait awaitResFinished(res)\n\n\tfunction setContentType(contentType: string) {\n\t\tif (res.headersSent || res.getHeader('content-type')) return\n\t\tres.setHeader('content-type', `${contentType}${charset ? `; charset=${charset}` : ''}`)\n\t}\n}\n\n// Resolves when res is fully flushed (finish) or the socket is gone (close).\n// Used as the universal \"we're done with this response\" signal so every code path\n// in writeRes can guarantee the chain doesn't unwind before bytes hit the wire.\nfunction awaitResFinished(res: ServerResponse) {\n\tif (res.writableFinished || res.destroyed) return Promise.resolve()\n\treturn new Promise<void>(resolve => {\n\t\tres.once('finish', done)\n\t\tres.once('close', done)\n\t\tfunction done() {\n\t\t\tres.off('finish', done)\n\t\t\tres.off('close', done)\n\t\t\tresolve()\n\t\t}\n\t})\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,gBAAgB,CAAA;AACvC,OAAO,EACN,2BAA2B,EAC3B,aAAa,EACb,WAAW,EACX,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,YAAY,GACZ,MAAM,kBAAkB,CAAA","sourcesContent":["export {writeRes} from './dxHelpers.js'\nexport {\n\tsetBufferBodyDefaultOptions,\n\tbufferFromReq,\n\tjsonFromReq,\n\trawFromReq,\n\ttextFromReq,\n\turlEncodedFromReq,\n\tqueryFromReq,\n} from './bodyHelpers.js'\n"]}
@@ -1,8 +1,7 @@
1
1
  export { getReq, getRes, setHtml, setNodeStream, setWebStream, setJson, setBuffer, setRedirect, setText, setEmpty, setFile, makeDxContext, } from './dx.js';
2
2
  import { dxServer } from './dx.js';
3
- export { getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery, } from './body.js';
3
+ export { getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery } from './body.js';
4
4
  export { router } from './router.js';
5
- export { connectMiddlewares } from './connect.js';
6
5
  export { chainStatic } from './static.js';
7
6
  export { logJson, default as logger } from './logger.js';
8
7
  export default dxServer;
@@ -1,8 +1,7 @@
1
1
  export { getReq, getRes, setHtml, setNodeStream, setWebStream, setJson, setBuffer, setRedirect, setText, setEmpty, setFile, makeDxContext, } from './dx.js';
2
2
  import { dxServer } from './dx.js';
3
- export { getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery, } from './body.js';
3
+ export { getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery } from './body.js';
4
4
  export { router } from './router.js';
5
- export { connectMiddlewares } from './connect.js';
6
5
  export { chainStatic } from './static.js';
7
6
  export { logJson, default as logger } from './logger.js';
8
7
  export default dxServer;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,MAAM,EACN,MAAM,EACN,OAAO,EACP,aAAa,EACb,YAAY,EACZ,OAAO,EACP,SAAS,EACT,WAAW,EACX,OAAO,EACP,QAAQ,EACR,OAAO,EACP,aAAa,GACb,MAAM,SAAS,CAAA;AAChB,OAAO,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAA;AAChC,OAAO,EAAC,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAC,MAAM,WAAW,CAAA;AACtF,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAA;AAClC,OAAO,EAAC,WAAW,EAAC,MAAM,aAAa,CAAA;AACvC,OAAO,EAAC,OAAO,EAAE,OAAO,IAAI,MAAM,EAAC,MAAM,aAAa,CAAA;AAEtD,eAAe,QAAQ,CAAA","sourcesContent":["export {\n\tgetReq,\n\tgetRes,\n\tsetHtml,\n\tsetNodeStream,\n\tsetWebStream,\n\tsetJson,\n\tsetBuffer,\n\tsetRedirect,\n\tsetText,\n\tsetEmpty,\n\tsetFile,\n\tmakeDxContext,\n} from './dx.js'\nimport {dxServer} from './dx.js'\nexport {getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery} from './body.js'\nexport {router} from './router.js'\nexport {chainStatic} from './static.js'\nexport {logJson, default as logger} from './logger.js'\n\nexport default dxServer\n"]}
@@ -0,0 +1,4 @@
1
+ export declare function logJson(json: any): void;
2
+ export default function makeLogger(log?: typeof logJson, { timezoneOffset }?: {
3
+ timezoneOffset?: number | undefined;
4
+ }): (next: () => any) => any;