dx-server 0.14.0-alpha.2 → 0.14.0-alpha.4
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/README.md +14 -12
- package/lib/dx.d.ts +21 -8
- package/lib/dx.js +53 -36
- package/lib/dxHelpers.js +17 -10
- package/lib/staticHelpers.d.ts +2 -1
- package/lib/staticHelpers.js +2 -2
- package/lib/vendors/mime.d.ts +1 -1
- package/lib/vendors/mime.js +6 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A modern, unopinionated, and performant Node.js server framework built on AsyncL
|
|
|
7
7
|
|
|
8
8
|
## Important caveats
|
|
9
9
|
|
|
10
|
-
- **ETag is supported.** Static file serving defaults to a weak (mtime/size-based) ETag, while other responses (`setJson`, `setHtml`, `setText`, `setBuffer`, …) use a strong (content-based) ETag. Redirects get no ETag.
|
|
10
|
+
- **ETag is supported.** Static file serving defaults to a weak (mtime/size-based) ETag, while other responses (`setJson`, `setHtml`, `setText`, `setBuffer`, …) use a strong (content-based) ETag. Redirects get no ETag. Disable it globally with `dxServer(req, res, {disableEtag: true})`, or per response with the setter's `disableEtag` option (for `setFile`, use `etag: 'disabled'`).
|
|
11
11
|
- **Request compression is supported.** Request bodies with `Content-Encoding: gzip`, `deflate`, or `br` are decompressed automatically.
|
|
12
12
|
- **Response compression is NOT supported.** dx-server never sets `Content-Encoding` on responses — handle response compression at the reverse proxy / CDN level.
|
|
13
13
|
- **Static file serving supports Range requests.**
|
|
@@ -419,18 +419,18 @@ Options:
|
|
|
419
419
|
|
|
420
420
|
#### Response Setters
|
|
421
421
|
|
|
422
|
-
|
|
423
|
-
headers, use `getRes().setHeader(name, value)` before (or after)
|
|
422
|
+
Each setter's options are inlined to expose only what its response type honors. To set arbitrary
|
|
423
|
+
response headers, use `getRes().setHeader(name, value)` before (or after) a setter.
|
|
424
424
|
|
|
425
|
-
- **`setJson(data, {status?})`** -
|
|
426
|
-
- **`setHtml(html, {status?})`** -
|
|
427
|
-
- **`setText(text, {status?})`** -
|
|
428
|
-
- **`setBuffer(buffer, {status?})`** -
|
|
429
|
-
- **`setFile(path,
|
|
430
|
-
- **`setNodeStream(stream, {status?})`** -
|
|
431
|
-
- **`setWebStream(stream, {status?})`** -
|
|
432
|
-
- **`setRedirect(url, status)`** -
|
|
433
|
-
- **`setEmpty({status?})`** -
|
|
425
|
+
- **`setJson(data, {status?, disableEtag?})`** - JSON response (`application/json`; always UTF-8 per RFC 8259)
|
|
426
|
+
- **`setHtml(html, {status?, charset?, disableEtag?})`** - HTML (`text/html; charset=utf-8`)
|
|
427
|
+
- **`setText(text, {status?, charset?, disableEtag?})`** - plain text (`text/plain; charset=utf-8`)
|
|
428
|
+
- **`setBuffer(buffer, {status?, charset?, disableEtag?})`** - buffer (`application/octet-stream`)
|
|
429
|
+
- **`setFile(path, {status?, ...SendFileOptions})`** - file (charset/ETag via `SendFileOptions`)
|
|
430
|
+
- **`setNodeStream(stream, {status?, charset?})`** - Node.js stream
|
|
431
|
+
- **`setWebStream(stream, {status?, charset?})`** - Web stream
|
|
432
|
+
- **`setRedirect(url, status)`** - redirect; `status` is `301 | 302` (required, positional)
|
|
433
|
+
- **`setEmpty({status?, disableEtag?})`** - empty response
|
|
434
434
|
|
|
435
435
|
#### Context Management
|
|
436
436
|
|
|
@@ -467,6 +467,8 @@ headers, use `getRes().setHeader(name, value)` before (or after) calling a sette
|
|
|
467
467
|
etag: 'weak', // 'weak' (default) | 'strong' | 'disabled'
|
|
468
468
|
disableLastModified: false,
|
|
469
469
|
disableFollowSymlinks: false, // set true to 403 files whose real path escapes root
|
|
470
|
+
charset: undefined, // override the Content-Type charset; default comes from the file extension
|
|
471
|
+
// (text/* -> utf-8). e.g. set 'utf-8' to force a charset on a type that has none.
|
|
470
472
|
})
|
|
471
473
|
```
|
|
472
474
|
|
package/lib/dx.d.ts
CHANGED
|
@@ -13,31 +13,44 @@ export interface Context<T, Params extends any[] = any[], R = any, Next = (...np
|
|
|
13
13
|
}
|
|
14
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>;
|
|
15
15
|
export declare function dxServer(req: IncomingMessage, res: ServerResponse, options?: {
|
|
16
|
+
charset?: BufferEncoding;
|
|
16
17
|
jsonBeautify?: boolean;
|
|
17
18
|
disableEtag?: boolean;
|
|
18
19
|
}): Chainable;
|
|
19
20
|
export declare function getReq(): IncomingMessage;
|
|
20
21
|
export declare function getRes(): ServerResponse<IncomingMessage>;
|
|
21
|
-
export declare function setText(text: string, { status }?: {
|
|
22
|
+
export declare function setText(text: string, { status, charset, disableEtag }?: {
|
|
22
23
|
status?: number;
|
|
24
|
+
charset?: BufferEncoding;
|
|
25
|
+
disableEtag?: boolean;
|
|
23
26
|
}): void;
|
|
24
|
-
export declare function
|
|
27
|
+
export declare function setHtml(html: string, options?: {
|
|
25
28
|
status?: number;
|
|
29
|
+
charset?: BufferEncoding;
|
|
30
|
+
disableEtag?: boolean;
|
|
26
31
|
}): void;
|
|
27
|
-
export declare function
|
|
32
|
+
export declare function setBuffer(buffer: Buffer, { status, charset, disableEtag }?: {
|
|
28
33
|
status?: number;
|
|
34
|
+
charset?: BufferEncoding;
|
|
35
|
+
disableEtag?: boolean;
|
|
29
36
|
}): void;
|
|
30
|
-
export declare function
|
|
31
|
-
export declare function setBuffer(buffer: Buffer, { status }?: {
|
|
37
|
+
export declare function setJson(json: any, { status, disableEtag }?: {
|
|
32
38
|
status?: number;
|
|
39
|
+
disableEtag?: boolean;
|
|
40
|
+
}): void;
|
|
41
|
+
export declare function setEmpty({ status, disableEtag }?: {
|
|
42
|
+
status?: number;
|
|
43
|
+
disableEtag?: boolean;
|
|
33
44
|
}): void;
|
|
34
|
-
export declare function setNodeStream(stream: Readable, { status }?: {
|
|
45
|
+
export declare function setNodeStream(stream: Readable, { status, charset }?: {
|
|
35
46
|
status?: number;
|
|
47
|
+
charset?: BufferEncoding;
|
|
36
48
|
}): void;
|
|
37
|
-
export declare function setWebStream(stream: ReadableStream, { status }?: {
|
|
49
|
+
export declare function setWebStream(stream: ReadableStream, { status, charset }?: {
|
|
38
50
|
status?: number;
|
|
51
|
+
charset?: BufferEncoding;
|
|
39
52
|
}): void;
|
|
40
|
-
export declare function
|
|
53
|
+
export declare function setFile(filePath: string, { status, ...options }?: SendFileOptions & {
|
|
41
54
|
status?: number;
|
|
42
55
|
}): void;
|
|
43
56
|
export declare function setRedirect(url: string, status: 301 | 302): void;
|
package/lib/dx.js
CHANGED
|
@@ -54,69 +54,86 @@ export function getReq() {
|
|
|
54
54
|
export function getRes() {
|
|
55
55
|
return requestStorage.getStore().res;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// Each setter inlines only the options its response type honors:
|
|
58
|
+
// - charset: text/html (body encoding + Content-Type label) and buffer/streams (Content-Type
|
|
59
|
+
// label). not on setJson (always UTF-8, RFC 8259), setEmpty, setRedirect; setFile takes it via
|
|
60
|
+
// SendFileOptions instead (its Content-Type is owned by sendFileTrusted).
|
|
61
|
+
// - disableEtag: the buffer-backed types (text/html/buffer/json/empty). not on streams or redirect
|
|
62
|
+
// (never ETagged) or setFile (use SendFileOptions.etag: 'disabled').
|
|
63
|
+
export function setText(text, { status, charset, disableEtag } = {}) {
|
|
59
64
|
const dx = dxContext.value;
|
|
60
65
|
if (status)
|
|
61
|
-
|
|
66
|
+
getRes().statusCode = status;
|
|
67
|
+
if (charset !== undefined)
|
|
68
|
+
dx.charset = charset;
|
|
69
|
+
if (disableEtag !== undefined)
|
|
70
|
+
dx.disableEtag = disableEtag;
|
|
62
71
|
dx.data = text;
|
|
63
72
|
dx.type = 'text';
|
|
64
73
|
}
|
|
65
|
-
export function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (status)
|
|
69
|
-
res.statusCode = status;
|
|
70
|
-
dx.data = undefined;
|
|
71
|
-
dx.type = 'empty';
|
|
74
|
+
export function setHtml(html, options = {}) {
|
|
75
|
+
setText(html, options);
|
|
76
|
+
dxContext.value.type = 'html';
|
|
72
77
|
}
|
|
73
|
-
export function
|
|
74
|
-
setText(html, opts);
|
|
78
|
+
export function setBuffer(buffer, { status, charset, disableEtag } = {}) {
|
|
75
79
|
const dx = dxContext.value;
|
|
76
|
-
|
|
80
|
+
if (status)
|
|
81
|
+
getRes().statusCode = status;
|
|
82
|
+
if (charset !== undefined)
|
|
83
|
+
dx.charset = charset;
|
|
84
|
+
if (disableEtag !== undefined)
|
|
85
|
+
dx.disableEtag = disableEtag;
|
|
86
|
+
dx.data = buffer;
|
|
87
|
+
dx.type = 'buffer';
|
|
77
88
|
}
|
|
78
|
-
export function
|
|
89
|
+
export function setJson(json, { status, disableEtag } = {}) {
|
|
79
90
|
const dx = dxContext.value;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
if (status)
|
|
92
|
+
getRes().statusCode = status;
|
|
93
|
+
if (disableEtag !== undefined)
|
|
94
|
+
dx.disableEtag = disableEtag;
|
|
95
|
+
dx.data = json;
|
|
96
|
+
dx.type = 'json';
|
|
83
97
|
}
|
|
84
|
-
export function
|
|
85
|
-
const res = getRes();
|
|
98
|
+
export function setEmpty({ status, disableEtag } = {}) {
|
|
86
99
|
const dx = dxContext.value;
|
|
87
100
|
if (status)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
getRes().statusCode = status;
|
|
102
|
+
if (disableEtag !== undefined)
|
|
103
|
+
dx.disableEtag = disableEtag;
|
|
104
|
+
dx.data = undefined;
|
|
105
|
+
dx.type = 'empty';
|
|
91
106
|
}
|
|
92
|
-
export function setNodeStream(stream, { status } = {}) {
|
|
93
|
-
const res = getRes();
|
|
107
|
+
export function setNodeStream(stream, { status, charset } = {}) {
|
|
94
108
|
const dx = dxContext.value;
|
|
95
109
|
if (status)
|
|
96
|
-
|
|
110
|
+
getRes().statusCode = status;
|
|
111
|
+
if (charset !== undefined)
|
|
112
|
+
dx.charset = charset;
|
|
97
113
|
dx.data = stream;
|
|
98
114
|
dx.type = 'nodeStream';
|
|
99
115
|
}
|
|
100
|
-
export function setWebStream(stream, { status } = {}) {
|
|
101
|
-
const res = getRes();
|
|
116
|
+
export function setWebStream(stream, { status, charset } = {}) {
|
|
102
117
|
const dx = dxContext.value;
|
|
103
118
|
if (status)
|
|
104
|
-
|
|
119
|
+
getRes().statusCode = status;
|
|
120
|
+
if (charset !== undefined)
|
|
121
|
+
dx.charset = charset;
|
|
105
122
|
dx.data = stream;
|
|
106
123
|
dx.type = 'webStream';
|
|
107
124
|
}
|
|
108
|
-
export function
|
|
109
|
-
|
|
110
|
-
if (status)
|
|
111
|
-
res.statusCode = status;
|
|
125
|
+
export function setFile(filePath, { status, ...options } = {}) {
|
|
126
|
+
// charset/etag for files come from SendFileOptions (stay in `options`), handled by sendFileTrusted
|
|
112
127
|
const dx = dxContext.value;
|
|
113
|
-
|
|
114
|
-
|
|
128
|
+
if (status)
|
|
129
|
+
getRes().statusCode = status;
|
|
130
|
+
dx.data = filePath;
|
|
131
|
+
dx.type = 'file';
|
|
132
|
+
dx.options = options;
|
|
115
133
|
}
|
|
116
134
|
export function setRedirect(url, status) {
|
|
117
|
-
const res = getRes();
|
|
118
135
|
const dx = dxContext.value;
|
|
119
|
-
|
|
136
|
+
getRes().statusCode = status;
|
|
120
137
|
dx.data = url;
|
|
121
138
|
dx.type = 'redirect';
|
|
122
139
|
}
|
package/lib/dxHelpers.js
CHANGED
|
@@ -15,31 +15,37 @@ export async function writeRes(req, res, { type, data, charset, jsonBeautify, di
|
|
|
15
15
|
break;
|
|
16
16
|
case 'buffer':
|
|
17
17
|
setContentType('application/octet-stream');
|
|
18
|
-
buffer = data ?? Buffer.from(''
|
|
18
|
+
buffer = data ?? Buffer.from('');
|
|
19
19
|
break;
|
|
20
20
|
case 'json':
|
|
21
|
-
|
|
21
|
+
// JSON is always UTF-8 (RFC 8259) and application/json defines no charset parameter, so the
|
|
22
|
+
// charset option is intentionally ignored: no charset label and the body is UTF-8 encoded.
|
|
23
|
+
setContentType('application/json', false);
|
|
22
24
|
buffer =
|
|
23
25
|
data === undefined
|
|
24
|
-
? Buffer.from(''
|
|
25
|
-
: Buffer.from(jsonBeautify ? JSON.stringify(data, null, 2) : JSON.stringify(data)
|
|
26
|
+
? Buffer.from('')
|
|
27
|
+
: Buffer.from(jsonBeautify ? JSON.stringify(data, null, 2) : JSON.stringify(data));
|
|
26
28
|
break;
|
|
27
29
|
case 'redirect':
|
|
28
30
|
case 'empty':
|
|
29
31
|
if (type === 'redirect')
|
|
30
32
|
res.setHeader('location', data);
|
|
31
|
-
buffer = Buffer.from(''
|
|
33
|
+
buffer = Buffer.from('');
|
|
32
34
|
break;
|
|
33
35
|
// Streaming paths own res.end() themselves (via pipeline/sendFileTrusted) and must be
|
|
34
36
|
// awaited to fulfil the "chain resolves after flush" invariant.
|
|
35
37
|
case 'nodeStream':
|
|
36
38
|
case 'webStream':
|
|
37
39
|
if (!data) {
|
|
38
|
-
buffer = Buffer.from(''
|
|
40
|
+
buffer = Buffer.from('');
|
|
39
41
|
break;
|
|
40
42
|
}
|
|
41
43
|
case 'file':
|
|
42
|
-
|
|
44
|
+
// streams have no intrinsic type, so default them to octet-stream. files do NOT get a
|
|
45
|
+
// default here: sendFileTrusted derives Content-Type from the file extension (and falls
|
|
46
|
+
// back to octet-stream itself). pre-setting it would suppress that extension detection.
|
|
47
|
+
if (type !== 'file')
|
|
48
|
+
setContentType('application/octet-stream');
|
|
43
49
|
try {
|
|
44
50
|
if (type === 'file')
|
|
45
51
|
await sendFileTrusted(req, res, data, options);
|
|
@@ -109,11 +115,12 @@ export async function writeRes(req, res, { type, data, charset, jsonBeautify, di
|
|
|
109
115
|
// we do not support content-encoding (gzip, deflate, br) and leave it to reverse proxy or CDN
|
|
110
116
|
await promisify(res.end.bind(res))();
|
|
111
117
|
await awaitResFinished(res);
|
|
112
|
-
function setContentType(contentType) {
|
|
118
|
+
function setContentType(contentType, withCharset = true) {
|
|
113
119
|
if (res.headersSent || res.getHeader('content-type'))
|
|
114
120
|
return;
|
|
115
|
-
//
|
|
116
|
-
|
|
121
|
+
// text/* defaults to utf-8; an explicit charset option applies to any type. withCharset is
|
|
122
|
+
// false for JSON, which is always UTF-8 with no charset parameter (RFC 8259).
|
|
123
|
+
const cs = withCharset ? (charset ?? (contentType.startsWith('text/') ? 'utf-8' : undefined)) : undefined;
|
|
117
124
|
res.setHeader('content-type', `${contentType}${cs ? `; charset=${cs}` : ''}`);
|
|
118
125
|
}
|
|
119
126
|
}
|
package/lib/staticHelpers.d.ts
CHANGED
|
@@ -11,10 +11,11 @@ export interface SendFileOptions {
|
|
|
11
11
|
disableCacheControl?: boolean;
|
|
12
12
|
maxAge?: number;
|
|
13
13
|
immutable?: boolean;
|
|
14
|
+
charset?: BufferEncoding;
|
|
14
15
|
disableFollowSymlinks?: boolean;
|
|
15
16
|
end?: number;
|
|
16
17
|
start?: number;
|
|
17
18
|
}
|
|
18
19
|
export declare function sendFileTrusted(req: IncomingMessage, res: ServerResponse, pathname: string, // plain path, not URI-encoded
|
|
19
20
|
{ root, allowDotfiles, start, end, disableAcceptRanges, disableLastModified, etag, disableCacheControl, maxAge, // 1 year
|
|
20
|
-
immutable, disableFollowSymlinks, }?: SendFileOptions): Promise<undefined>;
|
|
21
|
+
immutable, disableFollowSymlinks, charset, }?: SendFileOptions): Promise<undefined>;
|
package/lib/staticHelpers.js
CHANGED
|
@@ -15,7 +15,7 @@ const bytesRangeRegexp = /^ *bytes=/;
|
|
|
15
15
|
const upPathRegexp = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
16
16
|
export async function sendFileTrusted(req, res, pathname, // plain path, not URI-encoded
|
|
17
17
|
{ root, allowDotfiles, start = 0, end, disableAcceptRanges, disableLastModified, etag = 'weak', disableCacheControl, maxAge = 60 * 60 * 24 * 365 * 1000, // 1 year
|
|
18
|
-
immutable, disableFollowSymlinks, } = {}) {
|
|
18
|
+
immutable, disableFollowSymlinks, charset, } = {}) {
|
|
19
19
|
// null byte(s)
|
|
20
20
|
if (pathname.includes('\0'))
|
|
21
21
|
throw httpError('Forbidden', 403);
|
|
@@ -89,7 +89,7 @@ immutable, disableFollowSymlinks, } = {}) {
|
|
|
89
89
|
//endregion set header fields
|
|
90
90
|
// content-type
|
|
91
91
|
if (!res.getHeader('Content-Type'))
|
|
92
|
-
res.setHeader('Content-Type', contentTypeForExtension(path.extname(pathname).slice(1)
|
|
92
|
+
res.setHeader('Content-Type', contentTypeForExtension(path.extname(pathname).slice(1), charset));
|
|
93
93
|
// conditional GET support
|
|
94
94
|
// isConditionalGET
|
|
95
95
|
if (req.headers['if-match'] ||
|
package/lib/vendors/mime.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function contentTypeForExtension(extension: string): string
|
|
1
|
+
export declare function contentTypeForExtension(extension: string, charset?: string): string;
|
package/lib/vendors/mime.js
CHANGED
|
@@ -10,16 +10,12 @@ function preferredType(ext, type0, type1) {
|
|
|
10
10
|
const score1 = type1 ? mimeScore(type1, mimeDb[type1].source) : 0;
|
|
11
11
|
return score0 > score1 ? type0 : type1;
|
|
12
12
|
}
|
|
13
|
-
export function contentTypeForExtension(extension) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (charset)
|
|
20
|
-
return mimeType + '; charset=' + charset.toLowerCase();
|
|
21
|
-
}
|
|
22
|
-
return mimeType;
|
|
13
|
+
export function contentTypeForExtension(extension, charset) {
|
|
14
|
+
// unknown extension: octet-stream is binary, so add a charset only when one is explicitly given
|
|
15
|
+
const mimeType = extensionToMime[extension.toLowerCase()] ?? 'application/octet-stream';
|
|
16
|
+
// known text/* (and mime-db charset types) default to their charset; an explicit charset overrides it
|
|
17
|
+
charset ??= determineCharset(mimeType);
|
|
18
|
+
return `${mimeType}${charset ? '; charset=' + charset.toLowerCase() : ''}`;
|
|
23
19
|
}
|
|
24
20
|
const extractTypeRegexp = /^\s*([^;\s]*)(?:;|\s|$)/;
|
|
25
21
|
const textTypeRegexp = /^text\//i;
|