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 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
- Setters only take a `{status?}` option (except `setRedirect`/`setFile`). To set response
423
- headers, use `getRes().setHeader(name, value)` before (or after) calling a setter.
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?})`** - Send JSON response (`application/json; charset=utf-8`)
426
- - **`setHtml(html, {status?})`** - Send HTML response (`text/html; charset=utf-8`)
427
- - **`setText(text, {status?})`** - Send plain text (`text/plain; charset=utf-8`)
428
- - **`setBuffer(buffer, {status?})`** - Send buffer (`application/octet-stream`)
429
- - **`setFile(path, options?)`** - Send file (see `SendFileOptions`)
430
- - **`setNodeStream(stream, {status?})`** - Send Node.js stream
431
- - **`setWebStream(stream, {status?})`** - Send Web stream
432
- - **`setRedirect(url, status)`** - Redirect response; `status` is `301 | 302` (required, positional)
433
- - **`setEmpty({status?})`** - Send empty response
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 setEmpty({ status }?: {
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 setHtml(html: string, opts?: {
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 setFile(filePath: string, options?: SendFileOptions): void;
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 setJson(json: any, { status }?: {
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
- export function setText(text, { status } = {}) {
58
- const res = getRes();
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
- res.statusCode = status;
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 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';
74
+ export function setHtml(html, options = {}) {
75
+ setText(html, options);
76
+ dxContext.value.type = 'html';
72
77
  }
73
- export function setHtml(html, opts = {}) {
74
- setText(html, opts);
78
+ export function setBuffer(buffer, { status, charset, disableEtag } = {}) {
75
79
  const dx = dxContext.value;
76
- dx.type = 'html';
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 setFile(filePath, options) {
89
+ export function setJson(json, { status, disableEtag } = {}) {
79
90
  const dx = dxContext.value;
80
- dx.data = filePath;
81
- dx.type = 'file';
82
- dx.options = options;
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 setBuffer(buffer, { status } = {}) {
85
- const res = getRes();
98
+ export function setEmpty({ status, disableEtag } = {}) {
86
99
  const dx = dxContext.value;
87
100
  if (status)
88
- res.statusCode = status;
89
- dx.data = buffer;
90
- dx.type = 'buffer';
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
- res.statusCode = status;
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
- res.statusCode = status;
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 setJson(json, { status } = {}) {
109
- const res = getRes();
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
- dx.data = json;
114
- dx.type = 'json';
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
- res.statusCode = status;
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('', charset);
18
+ buffer = data ?? Buffer.from('');
19
19
  break;
20
20
  case 'json':
21
- setContentType('application/json');
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('', charset)
25
- : Buffer.from(jsonBeautify ? JSON.stringify(data, null, 2) : JSON.stringify(data), charset);
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('', charset);
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('', charset);
40
+ buffer = Buffer.from('');
39
41
  break;
40
42
  }
41
43
  case 'file':
42
- setContentType('application/octet-stream');
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
- // 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);
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
  }
@@ -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>;
@@ -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)) || 'application/octet-stream');
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'] ||
@@ -1 +1 @@
1
- export declare function contentTypeForExtension(extension: string): string | undefined;
1
+ export declare function contentTypeForExtension(extension: string, charset?: string): string;
@@ -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
- const mimeType = extensionToMime[extension.toLowerCase()];
15
- if (!mimeType)
16
- return;
17
- if (!mimeType.includes('charset')) {
18
- const charset = determineCharset(mimeType);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dx-server",
3
- "version": "0.14.0-alpha.2",
3
+ "version": "0.14.0-alpha.4",
4
4
  "main": "./lib/index.js",
5
5
  "repository": {
6
6
  "type": "git",