dx-server 0.12.1 → 0.13.0-alpha.1

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 (106) hide show
  1. package/README.md +47 -55
  2. package/{cjs → lib}/body.d.ts +2 -3
  3. package/lib/body.js +10 -0
  4. package/{esm → lib}/bodyHelpers.d.ts +2 -4
  5. package/lib/bodyHelpers.js +89 -0
  6. package/{esm → lib}/dx.d.ts +5 -8
  7. package/lib/dx.js +127 -0
  8. package/{cjs → lib}/dxHelpers.d.ts +0 -4
  9. package/{esm → lib}/dxHelpers.js +0 -2
  10. package/{cjs → lib}/index.d.ts +0 -1
  11. package/{esm/index.d.ts → lib/index.js} +0 -1
  12. package/lib/logger.js +56 -0
  13. package/lib/router.d.ts +42 -0
  14. package/lib/router.js +43 -0
  15. package/lib/static.js +22 -0
  16. package/{cjs → lib}/staticHelpers.d.ts +0 -2
  17. package/lib/staticHelpers.js +186 -0
  18. package/{cjs → lib}/stream.d.ts +2 -7
  19. package/lib/stream.js +90 -0
  20. package/{esm → lib}/vendors/contentType.js +0 -1
  21. package/{cjs → lib}/vendors/etag.d.ts +0 -3
  22. package/lib/vendors/etag.js +104 -0
  23. package/{cjs → lib}/vendors/fresh.d.ts +2 -2
  24. package/lib/vendors/fresh.js +95 -0
  25. package/lib/vendors/mime.d.ts +1 -0
  26. package/lib/vendors/mime.js +35 -0
  27. package/{esm → lib}/vendors/mimeDb.js +0 -1
  28. package/{cjs → lib}/vendors/mimeScore.d.ts +1 -1
  29. package/lib/vendors/mimeScore.js +45 -0
  30. package/{cjs → lib}/vendors/onFinished.d.ts +1 -1
  31. package/lib/vendors/onFinished.js +231 -0
  32. package/lib/vendors/rangeParser.d.ts +20 -0
  33. package/lib/vendors/rangeParser.js +121 -0
  34. package/package.json +10 -19
  35. package/cjs/body.js +0 -14
  36. package/cjs/bodyHelpers.d.ts +0 -16
  37. package/cjs/bodyHelpers.js +0 -101
  38. package/cjs/connect.d.ts +0 -5
  39. package/cjs/connect.js +0 -44
  40. package/cjs/dx.d.ts +0 -46
  41. package/cjs/dx.js +0 -144
  42. package/cjs/dxHelpers.js +0 -123
  43. package/cjs/express.d.ts +0 -4
  44. package/cjs/express.js +0 -43
  45. package/cjs/helpers.js +0 -14
  46. package/cjs/index.js +0 -38
  47. package/cjs/logger.js +0 -61
  48. package/cjs/package.json +0 -3
  49. package/cjs/polyfillWithResolvers.d.ts +0 -1
  50. package/cjs/polyfillWithResolvers.js +0 -17
  51. package/cjs/router.js +0 -47
  52. package/cjs/static.js +0 -27
  53. package/cjs/staticHelpers.js +0 -195
  54. package/cjs/stream.js +0 -97
  55. package/cjs/vendors/contentType.js +0 -92
  56. package/cjs/vendors/etag.js +0 -136
  57. package/cjs/vendors/fresh.js +0 -102
  58. package/cjs/vendors/mime.d.ts +0 -1
  59. package/cjs/vendors/mime.js +0 -42
  60. package/cjs/vendors/mimeDb.js +0 -9417
  61. package/cjs/vendors/mimeScore.js +0 -50
  62. package/cjs/vendors/onFinished.js +0 -245
  63. package/cjs/vendors/rangeParser.d.ts +0 -10
  64. package/cjs/vendors/rangeParser.js +0 -125
  65. package/esm/body.d.ts +0 -8
  66. package/esm/body.js +0 -11
  67. package/esm/bodyHelpers.js +0 -90
  68. package/esm/connect.d.ts +0 -5
  69. package/esm/connect.js +0 -40
  70. package/esm/dx.js +0 -128
  71. package/esm/dxHelpers.d.ts +0 -49
  72. package/esm/express.d.ts +0 -4
  73. package/esm/express.js +0 -35
  74. package/esm/helpers.js +0 -3
  75. package/esm/index.js +0 -9
  76. package/esm/logger.d.ts +0 -3
  77. package/esm/logger.js +0 -57
  78. package/esm/polyfillWithResolvers.d.ts +0 -1
  79. package/esm/polyfillWithResolvers.js +0 -16
  80. package/esm/router.js +0 -44
  81. package/esm/static.d.ts +0 -5
  82. package/esm/static.js +0 -23
  83. package/esm/staticHelpers.d.ts +0 -18
  84. package/esm/staticHelpers.js +0 -188
  85. package/esm/stream.d.ts +0 -12
  86. package/esm/stream.js +0 -92
  87. package/esm/vendors/contentType.d.ts +0 -4
  88. package/esm/vendors/etag.d.ts +0 -10
  89. package/esm/vendors/etag.js +0 -105
  90. package/esm/vendors/fresh.d.ts +0 -23
  91. package/esm/vendors/fresh.js +0 -96
  92. package/esm/vendors/mime.d.ts +0 -1
  93. package/esm/vendors/mime.js +0 -35
  94. package/esm/vendors/mimeDb.d.ts +0 -9413
  95. package/esm/vendors/mimeScore.d.ts +0 -5
  96. package/esm/vendors/mimeScore.js +0 -46
  97. package/esm/vendors/onFinished.d.ts +0 -14
  98. package/esm/vendors/onFinished.js +0 -241
  99. package/esm/vendors/rangeParser.d.ts +0 -10
  100. package/esm/vendors/rangeParser.js +0 -121
  101. /package/{cjs → lib}/helpers.d.ts +0 -0
  102. /package/{esm/helpers.d.ts → lib/helpers.js} +0 -0
  103. /package/{cjs → lib}/logger.d.ts +0 -0
  104. /package/{cjs → lib}/static.d.ts +0 -0
  105. /package/{cjs → lib}/vendors/contentType.d.ts +0 -0
  106. /package/{cjs → lib}/vendors/mimeDb.d.ts +0 -0
package/README.md CHANGED
@@ -10,7 +10,7 @@ A modern, unopinionated, and performant Node.js server framework built on AsyncL
10
10
  - 🎯 **Elegant API interface** - No need to pass req/res objects through middleware chains
11
11
  - 🔗 **Chainable middleware** - Elegant middleware composition with [jchain](https://www.npmjs.com/package/jchain)
12
12
  - 🚀 **Context-based architecture** - Access request/response from anywhere using AsyncLocalStorage
13
- - 🔄 **Express compatible** - Use existing Express middleware and applications
13
+ - 🔄 **Express compatible** - Bridge Express/Connect middleware with a small adapter
14
14
  - 📦 **Zero dependencies** - No runtime dependencies, all functionality built-in
15
15
  - 🛡️ **Built-in body parsing** - JSON, text, URL-encoded, and raw body parsing with size limits
16
16
  - 🗂️ **Static file serving** - Efficient static file handling with ETag, Range, and Last-Modified support
@@ -127,9 +127,7 @@ new Server().on('request', (req, res) => chain(
127
127
  )()).listen(3000)
128
128
  ```
129
129
 
130
- ### Production-Ready Server with Express Integration
131
-
132
- This example requires: `npm install express morgan helmet cors`
130
+ ### Production-Ready Server
133
131
 
134
132
  ```javascript
135
133
  import {Server} from 'node:http'
@@ -139,11 +137,9 @@ import dxServer, {
139
137
  getReq, getRes,
140
138
  getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
141
139
  setHtml, setJson, setText, setEmpty, setBuffer, setRedirect, setNodeStream, setWebStream, setFile,
142
- router, connectMiddlewares, chainStatic, makeDxContext
140
+ router, chainStatic, makeDxContext
143
141
  } from 'dx-server'
144
- import {expressApp} from 'dx-server/express'
145
- import express from 'express'
146
- import morgan from 'morgan'
142
+ import {resolve} from 'node:path'
147
143
 
148
144
  // it is best practice to create custom error class for non-system error
149
145
  class ServerError extends Error {
@@ -166,7 +162,6 @@ function requireAuth() {
166
162
 
167
163
  const serverChain = chain(
168
164
  next => {
169
- // this is the difference between express and dx-server
170
165
  // req, res can be accessed from anywhere via context which uses NodeJS's AsyncLocalStorage under the hood
171
166
  getRes().setHeader('Cache-Control', 'no-cache')
172
167
  return next() // must return or await
@@ -182,15 +177,7 @@ const serverChain = chain(
182
177
  }
183
178
  }
184
179
  },
185
- connectMiddlewares(
186
- morgan('common'),
187
- // cors(),
188
- ),
189
- await expressApp(app => {// any express feature can be used. This requires express installed, with for e.g., `yarn add express`
190
- app.set('trust proxy', true)
191
- if (process.env.NODE_ENV !== 'production') app.set('json spaces', 2)
192
- app.use('/public', express.static('public'))
193
- }),
180
+ chainStatic('/public/*', {root: resolve(import.meta.dirname, 'public')}),
194
181
  authContext.chain(), // chain context will set the context value to authContext.value in every request
195
182
  router.post('/api/*', async ({next}) => {// example of catching error for all /api/* routes
196
183
  try {
@@ -318,12 +305,12 @@ import dxServer, {
318
305
  setNodeStream, setWebStream, setFile,
319
306
 
320
307
  // Utilities
321
- router, connectMiddlewares, chainStatic, makeDxContext
308
+ router, chainStatic, makeDxContext,
309
+
310
+ // Logging
311
+ logger, logJson,
322
312
  } from 'dx-server'
323
313
 
324
- // Express integration (requires express installed)
325
- import {expressApp, expressRouter} from 'dx-server/express'
326
-
327
314
  // Low-level helpers
328
315
  import {
329
316
  setBufferBodyDefaultOptions,
@@ -384,7 +371,6 @@ Options:
384
371
  ```
385
372
 
386
373
  #### Middleware Utilities
387
- - **`connectMiddlewares(...middlewares)`** - Use Connect/Express middleware
388
374
  - **`chainStatic(pattern, options)`** - Serve static files
389
375
  ```javascript
390
376
  chainStatic('/public/*', {
@@ -453,30 +439,38 @@ router.get({
453
439
 
454
440
  ### Express Integration
455
441
 
456
- dx-server seamlessly integrates with Express applications and middleware:
442
+ dx-server does not ship a built-in Express adapter, but Express apps and Connect-style middleware slot into a chain with a one-line adapter. A Connect/Express middleware has the signature `(req, res, next) => void`; jchain expects `next => void | Promise`. Bridge them inline:
457
443
 
458
444
  ```javascript
459
- import {expressApp, expressRouter} from 'dx-server/express'
445
+ import chain from 'jchain'
446
+ import {getReq, getRes} from 'dx-server'
460
447
  import express from 'express'
461
- import cors from 'cors'
448
+ import morgan from 'morgan'
462
449
  import helmet from 'helmet'
450
+ import cors from 'cors'
463
451
 
464
- chain(
465
- // Use entire Express app
466
- await expressApp(app => {
467
- app.set('trust proxy', true)
468
- app.set('json spaces', 2)
469
- app.use(helmet())
470
- app.use('/static', express.static('public'))
471
- }),
472
-
473
- // Or use Express router
474
- expressRouter(router => {
475
- router.use(cors())
476
- router.get('/legacy', (req, res) => {
477
- res.json({message: 'Express route'})
478
- })
452
+ // Adapt one Connect/Express-style middleware (or a full Express app, which has the
453
+ // same (req, res, next) shape) into a jchain step. Always calls next() so the rest of
454
+ // the chain runs — dx-server handles the case where the response is already ended.
455
+ const fromConnect = mw => next => new Promise((resolve, reject) => {
456
+ mw(getReq(), getRes(), err => {
457
+ if (err) return reject(err)
458
+ next().then(resolve, reject)
479
459
  })
460
+ })
461
+
462
+ const app = express()
463
+ app.set('trust proxy', true)
464
+ app.use('/public', express.static('public'))
465
+ app.get('/legacy', (req, res) => res.json({message: 'Express route'}))
466
+
467
+ chain(
468
+ fromConnect(app), // mount the entire Express app first
469
+ fromConnect(morgan('common')),
470
+ fromConnect(helmet()),
471
+ fromConnect(cors()),
472
+ // dx-server routes continue here
473
+ router.get('/', () => setHtml('ok')),
480
474
  )
481
475
  ```
482
476
 
@@ -560,20 +554,18 @@ router.post('/api/users', async () => {
560
554
  ```
561
555
 
562
556
  ### Security Headers
563
- Use security middleware:
557
+ Use security middleware via the `fromConnect` adapter shown in [Express Integration](#express-integration):
564
558
 
565
559
  ```javascript
566
560
  import helmet from 'helmet'
567
561
  import cors from 'cors'
568
562
 
569
563
  chain(
570
- connectMiddlewares(
571
- helmet(),
572
- cors({
573
- origin: process.env.ALLOWED_ORIGINS?.split(','),
574
- credentials: true
575
- })
576
- )
564
+ fromConnect(helmet()),
565
+ fromConnect(cors({
566
+ origin: process.env.ALLOWED_ORIGINS?.split(','),
567
+ credentials: true
568
+ })),
577
569
  )
578
570
  ```
579
571
 
@@ -611,16 +603,16 @@ server.on('upgrade', (request, socket, head) => {
611
603
  ```
612
604
 
613
605
  ### Rate Limiting
606
+ Using the `fromConnect` adapter from [Express Integration](#express-integration):
607
+
614
608
  ```javascript
615
609
  import rateLimit from 'express-rate-limit'
616
610
 
617
611
  chain(
618
- connectMiddlewares(
619
- rateLimit({
620
- windowMs: 15 * 60 * 1000, // 15 minutes
621
- max: 100 // limit each IP to 100 requests per windowMs
622
- })
623
- )
612
+ fromConnect(rateLimit({
613
+ windowMs: 15 * 60 * 1000, // 15 minutes
614
+ max: 100 // limit each IP to 100 requests per windowMs
615
+ })),
624
616
  )
625
617
  ```
626
618
 
@@ -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
@@ -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,89 @@
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
21
+ && isNaN(contentLengthParsed))
22
+ return;
23
+ const contentLength = isNaN(contentLengthParsed) ? undefined : contentLengthParsed;
24
+ // read
25
+ const encoding = (req.headers['content-encoding'] ?? 'identity').toLowerCase();
26
+ const stream = getContentStream(req, encoding);
27
+ return await readStream(stream, {
28
+ length: encoding === 'identity' ? contentLength : undefined,
29
+ limit: bodyLimit,
30
+ });
31
+ }
32
+ // if content-type is not as expected, return undefined
33
+ function forceGetContentTypeParams(req, expected) {
34
+ const contentTypeRaw = req.headers['content-type'];
35
+ if (!contentTypeRaw)
36
+ return;
37
+ const { mediaType, parameters } = parseContentType(contentTypeRaw);
38
+ if (mediaType !== expected)
39
+ return;
40
+ return parameters;
41
+ }
42
+ function forceGetCharset(req, expected) {
43
+ const parameters = forceGetContentTypeParams(req, expected);
44
+ if (!parameters)
45
+ return;
46
+ // assert charset per RFC 7159 sec 8.1
47
+ const charset = parameters.charset?.toLowerCase() || 'utf-8';
48
+ if (!charset.startsWith('utf-'))
49
+ throw new Error(`unsupported charset "${charset.toUpperCase()}"`);
50
+ return charset;
51
+ }
52
+ export async function jsonFromReq(req, options) {
53
+ const charset = forceGetCharset(req, 'application/json');
54
+ if (!charset)
55
+ return;
56
+ const buffer = await bufferFromReq(req, options);
57
+ if (buffer) {
58
+ const str = buffer.toString(charset);
59
+ return str ? JSON.parse(str) : undefined;
60
+ }
61
+ }
62
+ export async function rawFromReq(req, options) {
63
+ if (!forceGetContentTypeParams(req, 'application/octet-stream'))
64
+ return;
65
+ return await bufferFromReq(req, options);
66
+ }
67
+ export async function textFromReq(req, options) {
68
+ const charset = forceGetCharset(req, 'text/plain');
69
+ if (!charset)
70
+ return;
71
+ const buffer = await bufferFromReq(req, options);
72
+ if (buffer)
73
+ return buffer.toString(charset);
74
+ }
75
+ export async function urlEncodedFromReq(req, options) {
76
+ const charset = forceGetCharset(req, 'application/x-www-form-urlencoded');
77
+ if (!charset)
78
+ return;
79
+ const buffer = await bufferFromReq(req, options);
80
+ if (buffer) {
81
+ return (bodyDefaultOptions.urlEncodedParser ?? options?.urlEncodedParser ?? defaultQueryParser)(buffer.toString(charset));
82
+ }
83
+ }
84
+ export function urlFromReq(req) {
85
+ return new URL(req.url ?? '', 'https://example.com');
86
+ }
87
+ export function queryFromReq(req, options) {
88
+ return (bodyDefaultOptions.queryParser ?? options?.queryParser ?? defaultQueryParser)(urlFromReq(req).searchParams.toString());
89
+ }
@@ -1,20 +1,17 @@
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;
package/lib/dx.js ADDED
@@ -0,0 +1,127 @@
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() { return valueMap.get(getReq()); },
18
+ set(value) {
19
+ const req = getReq();
20
+ promiseMap.set(req, Promise.resolve(value));
21
+ valueMap.set(req, value);
22
+ }
23
+ });
24
+ context.chain = ((...params) => (async (next) => {
25
+ await context(...params);
26
+ return next();
27
+ }));
28
+ context.set = (req, value) => {
29
+ promiseMap.set(req, Promise.resolve(value));
30
+ valueMap.set(req, value);
31
+ };
32
+ context.get = req => valueMap.get(req);
33
+ return context;
34
+ }
35
+ const requestStorage = new AsyncLocalStorage();
36
+ const dxContext = makeDxContext(options => ({ ...options }));
37
+ export function dxServer(req, res, options = {}) {
38
+ return async (next) => {
39
+ dxContext.set(req, { ...options });
40
+ const result = await requestStorage.run({ req, res }, next);
41
+ await writeRes(req, res, dxContext.get(req));
42
+ return result;
43
+ };
44
+ }
45
+ // method: verb
46
+ // url: full url without server, protocol, port.
47
+ // headers: if headers are repeated, they are joined by comma. Header names are lowercased.
48
+ // rawHeaders: list of header name and value in a flat array. Case is preserved.
49
+ export function getReq() { return requestStorage.getStore().req; }
50
+ export function getRes() { return requestStorage.getStore().res; }
51
+ export function setText(text, { status } = {}) {
52
+ const res = getRes();
53
+ const dx = dxContext.value;
54
+ if (status)
55
+ res.statusCode = status;
56
+ dx.data = text;
57
+ dx.type = 'text';
58
+ }
59
+ export function setEmpty({ status } = {}) {
60
+ const res = getRes();
61
+ const dx = dxContext.value;
62
+ if (status)
63
+ res.statusCode = status;
64
+ dx.data = undefined;
65
+ dx.type = 'empty';
66
+ }
67
+ export function setHtml(html, opts = {}) {
68
+ setText(html, opts);
69
+ const dx = dxContext.value;
70
+ dx.type = 'html';
71
+ }
72
+ export function setFile(filePath, options) {
73
+ const dx = dxContext.value;
74
+ dx.data = filePath;
75
+ dx.type = 'file';
76
+ dx.options = options;
77
+ }
78
+ export function setBuffer(buffer, { status } = {}) {
79
+ const res = getRes();
80
+ const dx = dxContext.value;
81
+ if (status)
82
+ res.statusCode = status;
83
+ dx.data = buffer;
84
+ dx.type = 'buffer';
85
+ }
86
+ export function setNodeStream(stream, { status } = {}) {
87
+ const res = getRes();
88
+ const dx = dxContext.value;
89
+ if (status)
90
+ res.statusCode = status;
91
+ dx.data = stream;
92
+ dx.type = 'nodeStream';
93
+ }
94
+ export function setWebStream(stream, { status } = {}) {
95
+ const res = getRes();
96
+ const dx = dxContext.value;
97
+ if (status)
98
+ res.statusCode = status;
99
+ dx.data = stream;
100
+ dx.type = 'webStream';
101
+ }
102
+ export function setJson(json, { status } = {}) {
103
+ const res = getRes();
104
+ if (status)
105
+ res.statusCode = status;
106
+ const dx = dxContext.value;
107
+ dx.data = json;
108
+ dx.type = 'json';
109
+ }
110
+ export function setRedirect(url, status) {
111
+ const res = getRes();
112
+ const dx = dxContext.value;
113
+ res.statusCode = status;
114
+ dx.data = url;
115
+ dx.type = 'redirect';
116
+ }
117
+ // for download, set content-disposition header
118
+ // res.setHeader('Content-disposition', 'attachment; filename=my-movie.MOV')
119
+ // res.setHeader('Content-type', 'video/quicktime')
120
+ // fileStream.pipe(res)
121
+ // or
122
+ // send(req, filePath, options).pipe(res) // which will set content-type, content-length, and other cache related headers like staticHelpers.sendFile
123
+ // implementing this require a strict validation for the type (attachment) and filename.
124
+ // For example: express relies on this
125
+ // https://github.com/jshttp/content-disposition/blob/1037e24e4790273da96645ad250061f39e77968c/index.js#L186
126
+ // because in most applications, users can specify a simple filename which usually doesn't need to be validated.
127
+ // we leave setDownload() implementation for users, for now.
@@ -1,10 +1,6 @@
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';
6
3
  import { type SendFileOptions } from './staticHelpers.js';
7
- import './polyfillWithResolvers.js';
8
4
  export type DxContext = {
9
5
  charset?: BufferEncoding;
10
6
  jsonBeautify?: boolean;
@@ -2,7 +2,6 @@ import { Readable } from 'node:stream';
2
2
  import { promisify } from 'node:util';
3
3
  import { entityTag, isFreshETag } from './vendors/etag.js';
4
4
  import { sendFileTrusted } from './staticHelpers.js';
5
- import './polyfillWithResolvers.js';
6
5
  export async function writeRes(req, res, { type, data, charset, jsonBeautify, disableEtag, options }) {
7
6
  const setContentType = (contentType) => {
8
7
  if (res.headersSent || res.getHeader('content-type'))
@@ -116,4 +115,3 @@ export async function writeRes(req, res, { type, data, charset, jsonBeautify, di
116
115
  await promisify(res.end.bind(res))();
117
116
  }
118
117
  }
119
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHhIZWxwZXJzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2R4SGVscGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUMsUUFBUSxFQUFDLE1BQU0sYUFBYSxDQUFBO0FBQ3BDLE9BQU8sRUFBQyxTQUFTLEVBQUMsTUFBTSxXQUFXLENBQUE7QUFDbkMsT0FBTyxFQUFDLFNBQVMsRUFBRSxXQUFXLEVBQUMsTUFBTSxtQkFBbUIsQ0FBQTtBQUN4RCxPQUFPLEVBQUMsZUFBZSxFQUF1QixNQUFNLG9CQUFvQixDQUFBO0FBRXhFLE9BQU8sNEJBQTRCLENBQUE7QUFvRG5DLE1BQU0sQ0FBQyxLQUFLLFVBQVUsUUFBUSxDQUFDLEdBQW9CLEVBQUUsR0FBbUIsRUFBRSxFQUFDLElBQUksRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLFlBQVksRUFBRSxXQUFXLEVBQUUsT0FBTyxFQUFZO0lBQzdJLE1BQU0sY0FBYyxHQUFHLENBQUMsV0FBbUIsRUFBRSxFQUFFO1FBQzlDLElBQUksR0FBRyxDQUFDLFdBQVcsSUFBSSxHQUFHLENBQUMsU0FBUyxDQUFDLGNBQWMsQ0FBQztZQUFFLE9BQU07UUFDNUQsR0FBRyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUUsR0FBRyxXQUFXLEdBQUcsT0FBTyxDQUFDLENBQUMsQ0FBQyxhQUFhLE9BQU8sRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFBO0lBQ3hGLENBQUMsQ0FBQTtJQUNELElBQUksY0FBYyxDQUFBO0lBRWxCLFFBQVEsSUFBSSxFQUFFLENBQUM7UUFDZCxLQUFLLE1BQU07WUFDVixjQUFjLENBQUMsWUFBWSxDQUFDLENBQUE7UUFDN0IsS0FBSyxNQUFNO1lBQ1YsY0FBYyxDQUFDLFdBQVcsQ0FBQyxDQUFBO1lBQzNCLG1CQUFtQjtZQUNuQixjQUFjLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLElBQUksRUFBRSxFQUFFLE9BQU8sQ0FBQyxD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
@@ -2,7 +2,6 @@ export { getReq, getRes, setHtml, setNodeStream, setWebStream, setJson, setBuffe
2
2
  import { dxServer } from './dx.js';
3
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;
@@ -2,7 +2,6 @@ export { getReq, getRes, setHtml, setNodeStream, setWebStream, setJson, setBuffe
2
2
  import { dxServer } from './dx.js';
3
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;
package/lib/logger.js ADDED
@@ -0,0 +1,56 @@
1
+ import { getReq, getRes } from './dx.js';
2
+ import { hrtime } from 'node:process';
3
+ export function logJson(json) {
4
+ console.log(JSON.stringify(json));
5
+ }
6
+ let requestCount = 0;
7
+ export default (log = logJson) => function logger(next) {
8
+ const res = getRes();
9
+ const req = getReq();
10
+ const logId = requestCount++;
11
+ const start = hrtime.bigint();
12
+ const now = new Date(Date.now() + 9 * 60 * 60 * 1000); // jst
13
+ log({
14
+ level: 'info',
15
+ id: logId,
16
+ timestamp: [
17
+ [
18
+ now.getUTCFullYear(),
19
+ String(now.getUTCMonth() + 1).padStart(2, '0'),
20
+ String(now.getUTCDate()).padStart(2, '0'),
21
+ ].join('-'),
22
+ [
23
+ String(now.getUTCHours()).padStart(2, '0'),
24
+ String(now.getUTCMinutes()).padStart(2, '0'),
25
+ [String(now.getUTCSeconds()).padStart(2, '0'), String(now.getUTCMilliseconds()).padStart(3, '0')].join('.'),
26
+ ].join(':'),
27
+ ].join('T'),
28
+ remoteAddress: req.socket.remoteAddress,
29
+ method: req.method,
30
+ url: req.url,
31
+ httpVersion: `HTTP/${req.httpVersion}`,
32
+ headers: process.env.NODE_ENV === 'production'
33
+ ? req.headers
34
+ : Object.fromEntries(Object.entries(req.headers).filter(([k]) => [
35
+ 'host',
36
+ 'referer',
37
+ 'referrer',
38
+ 'user-agent',
39
+ 'x-forwarded-proto',
40
+ 'x-forwarded-host',
41
+ 'x-forwarded-for',
42
+ ].includes(k))),
43
+ });
44
+ res.once('finish', end).once('close', end).once('error', end);
45
+ return next();
46
+ function end() {
47
+ res.off('finish', end).off('close', end).off('error', end);
48
+ const durationNs = hrtime.bigint() - start;
49
+ log({
50
+ level: 'info',
51
+ id: logId,
52
+ duration: Number(durationNs) / 1e6, // ms
53
+ headers: res.getHeaders(),
54
+ });
55
+ }
56
+ };
@@ -0,0 +1,42 @@
1
+ import { type Chainable } from './dx.js';
2
+ interface URLPatternOptions {
3
+ }
4
+ interface RouteContext {
5
+ matched: URLPatternResult;
6
+ next(): any;
7
+ }
8
+ interface Route {
9
+ (context: RouteContext): any;
10
+ }
11
+ interface Routes {
12
+ [k: string]: Route;
13
+ }
14
+ interface RouterOptions extends URLPatternOptions {
15
+ prefix?: string;
16
+ }
17
+ type Router = {
18
+ patch(routes: Routes, options?: RouterOptions): Chainable;
19
+ patch(pattern: string, route: Route, options?: RouterOptions): Chainable;
20
+ trace(routes: Routes, options?: RouterOptions): Chainable;
21
+ trace(pattern: string, route: Route, options?: RouterOptions): Chainable;
22
+ options(routes: Routes, options?: RouterOptions): Chainable;
23
+ options(pattern: string, route: Route, options?: RouterOptions): Chainable;
24
+ connect(routes: Routes, options?: RouterOptions): Chainable;
25
+ connect(pattern: string, route: Route, options?: RouterOptions): Chainable;
26
+ delete(routes: Routes, options?: RouterOptions): Chainable;
27
+ delete(pattern: string, route: Route, options?: RouterOptions): Chainable;
28
+ put(routes: Routes, options?: RouterOptions): Chainable;
29
+ put(pattern: string, route: Route, options?: RouterOptions): Chainable;
30
+ post(routes: Routes, options?: RouterOptions): Chainable;
31
+ post(pattern: string, route: Route, options?: RouterOptions): Chainable;
32
+ head(routes: Routes, options?: RouterOptions): Chainable;
33
+ head(pattern: string, route: Route, options?: RouterOptions): Chainable;
34
+ get(routes: Routes, options?: RouterOptions): Chainable;
35
+ get(pattern: string, route: Route, options?: RouterOptions): Chainable;
36
+ all(routes: Routes, options?: RouterOptions): Chainable;
37
+ all(pattern: string, route: Route, options?: RouterOptions): Chainable;
38
+ method(method: string, routes: Routes, options?: RouterOptions): Chainable;
39
+ method(method: string, pattern: string, route: Route, options?: RouterOptions): Chainable;
40
+ };
41
+ export declare const router: Router;
42
+ export {};