@fougere/http 0.3.0-alpha.0 → 0.5.0-alpha.0
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 +1 -1
- package/dist/express.d.ts.map +1 -1
- package/dist/express.js +5 -16
- package/dist/express.js.map +1 -1
- package/dist/fastify.d.ts +0 -3
- package/dist/fastify.d.ts.map +1 -1
- package/dist/fastify.js +5 -18
- package/dist/fastify.js.map +1 -1
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +6 -5
- package/dist/hono.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +26 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +31 -0
- package/dist/router.js.map +1 -1
- package/package.json +3 -2
- package/src/express.ts +191 -0
- package/src/fastify.ts +102 -0
- package/src/hono.ts +135 -0
- package/src/index.ts +15 -0
- package/src/logger.ts +41 -0
- package/src/router.ts +101 -0
package/README.md
CHANGED
|
@@ -11,4 +11,4 @@ pnpm add @fougere/http
|
|
|
11
11
|
|
|
12
12
|
Part of [Fougere](https://github.com/chok/fougere) — one schema, a gradient from
|
|
13
13
|
monolith to distributed, the same user code.
|
|
14
|
-
Reference documentation: [the site](https://
|
|
14
|
+
Reference documentation: [the site](https://fougere.dev/) (en/fr).
|
package/dist/express.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAA6B,KAAK,UAAU,EAA4F,MAAM,aAAa,CAAC;AAEnK,UAAU,WAAW;IACnB,GAAG,CAAC,GAAG,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACnC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC7C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC/C;AAeD,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAqB1D;AAoDD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,GAAG,UAAU,CAuChE"}
|
package/dist/express.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* works when the host app is configured a particular way is a footgun, and this
|
|
12
12
|
* one is meant to be dropped into an app that already exists.
|
|
13
13
|
*/
|
|
14
|
-
import { MalformedJsonError } from './router.js';
|
|
14
|
+
import { MalformedJsonError, chain } from './router.js';
|
|
15
15
|
const METHOD_MAP = {
|
|
16
16
|
GET: 'get',
|
|
17
17
|
POST: 'post',
|
|
@@ -19,6 +19,9 @@ const METHOD_MAP = {
|
|
|
19
19
|
PATCH: 'patch',
|
|
20
20
|
DELETE: 'delete',
|
|
21
21
|
};
|
|
22
|
+
// Core declares this cap (`wire/call.ts`) and three doors read it there. This package
|
|
23
|
+
// declares NO Fougere dependency — a leaf port does not take one on the kernel for a
|
|
24
|
+
// number — so it keeps the fourth copy, deliberately.
|
|
22
25
|
const MAX_BODY_BYTES = 1024 * 1024;
|
|
23
26
|
/** Drain the Node stream. Only reached when no body parser ran before us. */
|
|
24
27
|
export function readRawBody(req) {
|
|
@@ -137,20 +140,6 @@ function sendResponse(res, result) {
|
|
|
137
140
|
export function createExpressRouter(app) {
|
|
138
141
|
const globalMiddlewares = [];
|
|
139
142
|
const scopedMiddlewares = [];
|
|
140
|
-
// Run matching middlewares as an onion chain around the handler
|
|
141
|
-
function runMiddlewares(ctx, handler) {
|
|
142
|
-
const matching = [
|
|
143
|
-
...globalMiddlewares,
|
|
144
|
-
...scopedMiddlewares.filter((s) => ctx.path.startsWith(s.path)).map((s) => s.mw),
|
|
145
|
-
];
|
|
146
|
-
let index = 0;
|
|
147
|
-
const next = () => {
|
|
148
|
-
if (index < matching.length)
|
|
149
|
-
return matching[index++](ctx, next);
|
|
150
|
-
return handler(ctx);
|
|
151
|
-
};
|
|
152
|
-
return next();
|
|
153
|
-
}
|
|
154
143
|
return {
|
|
155
144
|
use(...args) {
|
|
156
145
|
const [pathOrMw, maybeMw] = args;
|
|
@@ -166,7 +155,7 @@ export function createExpressRouter(app) {
|
|
|
166
155
|
on(method, path, handler) {
|
|
167
156
|
app[METHOD_MAP[method]](path, async (req, res, next) => {
|
|
168
157
|
try {
|
|
169
|
-
sendResponse(res, await
|
|
158
|
+
sendResponse(res, await chain(globalMiddlewares, scopedMiddlewares, buildContext(req), handler));
|
|
170
159
|
}
|
|
171
160
|
catch (err) {
|
|
172
161
|
// This adapter is the one that parses JSON, so it is the one that answers
|
package/dist/express.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,kBAAkB,EAA6G,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"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"}
|
package/dist/fastify.d.ts
CHANGED
package/dist/fastify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../src/fastify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../src/fastify.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAmE,MAAM,aAAa,CAAC;AAE/G,UAAU,WAAW;IACnB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC/C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC7C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC/C;AAoDD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,CAyBnE"}
|
package/dist/fastify.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fastify adapter — bridges a Fastify server to the HttpRouter interface.
|
|
3
|
+
*/
|
|
4
|
+
import { chain } from './router.js';
|
|
1
5
|
const METHOD_MAP = {
|
|
2
6
|
GET: 'get',
|
|
3
7
|
POST: 'post',
|
|
@@ -58,23 +62,6 @@ function sendResponse(reply, result) {
|
|
|
58
62
|
export function createFastifyRouter(server) {
|
|
59
63
|
const globalMiddlewares = [];
|
|
60
64
|
const scopedMiddlewares = [];
|
|
61
|
-
// Run matching middlewares as an onion chain around the handler
|
|
62
|
-
function runMiddlewares(ctx, handler) {
|
|
63
|
-
const matching = [
|
|
64
|
-
...globalMiddlewares,
|
|
65
|
-
...scopedMiddlewares
|
|
66
|
-
.filter((s) => ctx.path.startsWith(s.path))
|
|
67
|
-
.map((s) => s.mw),
|
|
68
|
-
];
|
|
69
|
-
let index = 0;
|
|
70
|
-
const next = () => {
|
|
71
|
-
if (index < matching.length) {
|
|
72
|
-
return matching[index++](ctx, next);
|
|
73
|
-
}
|
|
74
|
-
return handler(ctx);
|
|
75
|
-
};
|
|
76
|
-
return next();
|
|
77
|
-
}
|
|
78
65
|
return {
|
|
79
66
|
use(...args) {
|
|
80
67
|
const [pathOrMw, maybeMw] = args;
|
|
@@ -91,7 +78,7 @@ export function createFastifyRouter(server) {
|
|
|
91
78
|
const m = METHOD_MAP[method];
|
|
92
79
|
server[m](path, async (req, reply) => {
|
|
93
80
|
const ctx = buildContext(req);
|
|
94
|
-
const result = await
|
|
81
|
+
const result = await chain(globalMiddlewares, scopedMiddlewares, ctx, handler);
|
|
95
82
|
sendResponse(reply, result);
|
|
96
83
|
});
|
|
97
84
|
},
|
package/dist/fastify.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.js","sourceRoot":"","sources":["../src/fastify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fastify.js","sourceRoot":"","sources":["../src/fastify.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAYpC,MAAM,UAAU,GAAoE;IAClF,GAAG,EAAE,KAAK;IACV,IAAI,EAAE,MAAM;IACZ,GAAG,EAAE,KAAK;IACV,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;CACjB,CAAC;AAEF,SAAS,YAAY,CAAC,GAAQ;IAC5B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,OAA4B,CAAC;IAEjC,OAAO;QACL,8EAA8E;QAC9E,yEAAyE;QACzE,IAAI,OAAO;YACT,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,QAAQ,IAAI,MAAM,MAAM,GAAG,CAAC,QAAQ,IAAI,WAAW,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC;gBACnF,MAAM,OAAO,GAAG,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,CAAC;gBACtD,OAAO,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;oBACzB,MAAM;oBACN,OAAO,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,OAAiC,CAAC;oBAC3D,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACjF,CAAC,CAAC;YACL,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,MAAM,EAAE,MAAoB;QAC5B,IAAI,EAAE,GAAG,CAAC,GAAG;QACb,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE;QACxB,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE;QACtB,qFAAqF;QACrF,wDAAwD;QACxD,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;QACxD,KAAK,EAAE,EAAE;KACV,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,KAAU,EAAE,MAAsB;IACtD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACpD,qDAAqD;YACrD,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,uFAAuF;IACvF,qFAAqF;IACrF,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAmB;IACrD,MAAM,iBAAiB,GAAiB,EAAE,CAAC;IAC3C,MAAM,iBAAiB,GAAuC,EAAE,CAAC;IAEjE,OAAO;QACL,GAAG,CAAC,GAAG,IAAyC;YAC9C,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;YACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACjC,0EAA0E;gBAC1E,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBACrD,iBAAiB,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,OAAqB,EAAE,CAAC,CAAC;YAC9E,CAAC;iBAAM,CAAC;gBACN,iBAAiB,CAAC,IAAI,CAAC,QAAsB,CAAC,CAAC;YACjD,CAAC;QACH,CAAC;QAED,EAAE,CAAC,MAAkB,EAAE,IAAY,EAAE,OAAgB;YACnD,MAAM,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;YAC7B,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,GAAQ,EAAE,KAAU,EAAE,EAAE;gBAC7C,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;gBAC9B,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;gBAC/E,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC9B,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/hono.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../src/hono.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../src/hono.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAErB,UAAU,QAAQ;IAChB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACjD,GAAG,CAAC,GAAG,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACnC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC7C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC/C;AAyDD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,GAAG,UAAU,CAiD1D"}
|
package/dist/hono.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hono adapter — bridges a Hono app to the HttpRouter interface.
|
|
3
3
|
*/
|
|
4
|
-
import { MalformedJsonError } from './router.js';
|
|
4
|
+
import { MalformedJsonError, PASSTHROUGH, } from './router.js';
|
|
5
5
|
const METHOD_MAP = {
|
|
6
6
|
GET: 'get',
|
|
7
7
|
POST: 'post',
|
|
@@ -79,11 +79,12 @@ export function createHonoRouter(app) {
|
|
|
79
79
|
const ctx = buildContext(c);
|
|
80
80
|
const result = await mw(ctx, async () => {
|
|
81
81
|
await next();
|
|
82
|
-
// After next(), Hono has already set the response
|
|
83
|
-
|
|
82
|
+
// After next(), Hono has already set the response. The sentinel says so, and it
|
|
83
|
+
// is a symbol on purpose — see PASSTHROUGH.
|
|
84
|
+
return { status: c.res.status, data: PASSTHROUGH };
|
|
84
85
|
});
|
|
85
|
-
//
|
|
86
|
-
if (result.data !==
|
|
86
|
+
// Anything else is the middleware's own response, empty body included.
|
|
87
|
+
if (result.data !== PASSTHROUGH) {
|
|
87
88
|
return sendResponse(c, result);
|
|
88
89
|
}
|
|
89
90
|
}
|
package/dist/hono.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.js","sourceRoot":"","sources":["../src/hono.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"hono.js","sourceRoot":"","sources":["../src/hono.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { HttpMethod, RequestContext, ResponseResult, Handler, Next, Middleware, HttpRouter, } from './router.js';
|
|
2
|
-
export { MalformedJsonError } from './router.js';
|
|
2
|
+
export { MalformedJsonError, PASSTHROUGH } from './router.js';
|
|
3
3
|
export { createHonoRouter } from './hono.js';
|
|
4
4
|
export { createFastifyRouter } from './fastify.js';
|
|
5
5
|
export { createExpressRouter, readExpressBody } from './express.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,UAAU,EACV,cAAc,EACd,cAAc,EACd,OAAO,EACP,IAAI,EACJ,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,UAAU,EACV,cAAc,EACd,cAAc,EACd,OAAO,EACP,IAAI,EACJ,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { MalformedJsonError } from './router.js';
|
|
1
|
+
export { MalformedJsonError, PASSTHROUGH } from './router.js';
|
|
2
2
|
export { createHonoRouter } from './hono.js';
|
|
3
3
|
export { createFastifyRouter } from './fastify.js';
|
|
4
4
|
export { createExpressRouter, readExpressBody } from './express.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/router.d.ts
CHANGED
|
@@ -40,6 +40,21 @@ export declare class MalformedJsonError extends Error {
|
|
|
40
40
|
}
|
|
41
41
|
export type Handler = (ctx: RequestContext) => Promise<ResponseResult>;
|
|
42
42
|
export type Next = () => Promise<ResponseResult>;
|
|
43
|
+
/**
|
|
44
|
+
* What `next()` answers when the host framework — not us — owns the chain.
|
|
45
|
+
*
|
|
46
|
+
* Express and Fastify build their own middleware chain here, so a middleware's return IS
|
|
47
|
+
* the response and there is nothing to distinguish. Hono owns its chain, so its adapter has
|
|
48
|
+
* to tell "the middleware answered" from "the middleware delegated", and it used
|
|
49
|
+
* `data === null` for the second. `null` is a legal body — `{ status: 403, data: null }` is
|
|
50
|
+
* the ordinary spelling of a deny — so a middleware that REFUSED with no body was read as
|
|
51
|
+
* having delegated: it was silently bypassed on Hono and honoured on Fastify. One contract,
|
|
52
|
+
* two verdicts, on a value inside the value space.
|
|
53
|
+
*
|
|
54
|
+
* A symbol cannot be produced by accident, and no middleware author ever writes it: only an
|
|
55
|
+
* adapter that does not own its chain returns it from the `next` it hands out.
|
|
56
|
+
*/
|
|
57
|
+
export declare const PASSTHROUGH: unique symbol;
|
|
43
58
|
export type Middleware = (ctx: RequestContext, next: Next) => Promise<ResponseResult>;
|
|
44
59
|
export interface HttpRouter {
|
|
45
60
|
/** Register a global middleware. */
|
|
@@ -49,4 +64,15 @@ export interface HttpRouter {
|
|
|
49
64
|
/** Register a route handler. */
|
|
50
65
|
on(method: HttpMethod, path: string, handler: Handler): void;
|
|
51
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The onion chain both adapters run: matching middlewares around the handler.
|
|
69
|
+
*
|
|
70
|
+
* Stated once because it was stated twice, identically, in `express.ts` and `fastify.ts` —
|
|
71
|
+
* the same package, and the same place `PASSTHROUGH` already lives. A scoped middleware
|
|
72
|
+
* matches by `startsWith`, which is what makes the two adapters agree on a mounted path.
|
|
73
|
+
*/
|
|
74
|
+
export declare function chain(global: Middleware[], scoped: {
|
|
75
|
+
path: string;
|
|
76
|
+
mw: Middleware;
|
|
77
|
+
}[], ctx: RequestContext, handler: Handler): Promise<ResponseResult>;
|
|
52
78
|
//# sourceMappingURL=router.d.ts.map
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC5C,4FAA4F;IAC5F,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,4EAA4E;AAC5E,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,CAAC,EAAE,YAAY,EAGjC;CACF;AAED,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AACvE,MAAM,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;AACjD,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAEtF,MAAM,WAAW,UAAU;IACzB,oCAAoC;IACpC,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAClC,qDAAqD;IACrD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAChD,gCAAgC;IAChC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9D"}
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC5C,4FAA4F;IAC5F,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,4EAA4E;AAC5E,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,CAAC,EAAE,YAAY,EAGjC;CACF;AAED,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AACvE,MAAM,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;AACjD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,EAAE,OAAO,MAA2C,CAAC;AAE7E,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAEtF,MAAM,WAAW,UAAU;IACzB,oCAAoC;IACpC,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAClC,qDAAqD;IACrD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAChD,gCAAgC;IAChC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9D;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CACnB,MAAM,EAAE,UAAU,EAAE,EACpB,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,UAAU,CAAA;CAAE,EAAE,EAC1C,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,cAAc,CAAC,CAWzB"}
|
package/dist/router.js
CHANGED
|
@@ -11,4 +11,35 @@ export class MalformedJsonError extends Error {
|
|
|
11
11
|
this.name = 'MalformedJsonError';
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* What `next()` answers when the host framework — not us — owns the chain.
|
|
16
|
+
*
|
|
17
|
+
* Express and Fastify build their own middleware chain here, so a middleware's return IS
|
|
18
|
+
* the response and there is nothing to distinguish. Hono owns its chain, so its adapter has
|
|
19
|
+
* to tell "the middleware answered" from "the middleware delegated", and it used
|
|
20
|
+
* `data === null` for the second. `null` is a legal body — `{ status: 403, data: null }` is
|
|
21
|
+
* the ordinary spelling of a deny — so a middleware that REFUSED with no body was read as
|
|
22
|
+
* having delegated: it was silently bypassed on Hono and honoured on Fastify. One contract,
|
|
23
|
+
* two verdicts, on a value inside the value space.
|
|
24
|
+
*
|
|
25
|
+
* A symbol cannot be produced by accident, and no middleware author ever writes it: only an
|
|
26
|
+
* adapter that does not own its chain returns it from the `next` it hands out.
|
|
27
|
+
*/
|
|
28
|
+
export const PASSTHROUGH = Symbol('fougere.http.passthrough');
|
|
29
|
+
/**
|
|
30
|
+
* The onion chain both adapters run: matching middlewares around the handler.
|
|
31
|
+
*
|
|
32
|
+
* Stated once because it was stated twice, identically, in `express.ts` and `fastify.ts` —
|
|
33
|
+
* the same package, and the same place `PASSTHROUGH` already lives. A scoped middleware
|
|
34
|
+
* matches by `startsWith`, which is what makes the two adapters agree on a mounted path.
|
|
35
|
+
*/
|
|
36
|
+
export function chain(global, scoped, ctx, handler) {
|
|
37
|
+
const matching = [
|
|
38
|
+
...global,
|
|
39
|
+
...scoped.filter((s) => ctx.path.startsWith(s.path)).map((s) => s.mw),
|
|
40
|
+
];
|
|
41
|
+
let index = 0;
|
|
42
|
+
const next = () => index < matching.length ? matching[index++](ctx, next) : handler(ctx);
|
|
43
|
+
return next();
|
|
44
|
+
}
|
|
14
45
|
//# sourceMappingURL=router.js.map
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAmCH,4EAA4E;AAC5E,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAsB;QAChC,KAAK,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF"}
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAmCH,4EAA4E;AAC5E,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAsB;QAChC,KAAK,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAID;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,WAAW,GAAkB,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAa7E;;;;;;GAMG;AACH,MAAM,UAAU,KAAK,CACnB,MAAoB,EACpB,MAA0C,EAC1C,GAAmB,EACnB,OAAgB;IAEhB,MAAM,QAAQ,GAAG;QACf,GAAG,MAAM;QACT,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtE,CAAC;IAEF,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,GAA4B,EAAE,CACzC,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAExE,OAAO,IAAI,EAAE,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-alpha.0",
|
|
4
4
|
"description": "HTTP router and middleware.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist"
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
29
30
|
],
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/node": "^22.19.15",
|
package/src/express.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express adapter — bridges an Express app to the HttpRouter interface.
|
|
3
|
+
*
|
|
4
|
+
* The one that is not Web-standard, and that is the whole substance of this file.
|
|
5
|
+
* Hono hands over `c.req.raw`, Fastify parses the body for us; Express hands a
|
|
6
|
+
* Node `IncomingMessage` and parses nothing unless the app happens to have mounted
|
|
7
|
+
* `express.json()`. So the conversion lives here — read the stream when nobody
|
|
8
|
+
* else did, and build the `Request` the interface promises.
|
|
9
|
+
*
|
|
10
|
+
* Working with or without `express.json()` is deliberate: an adapter that only
|
|
11
|
+
* works when the host app is configured a particular way is a footgun, and this
|
|
12
|
+
* one is meant to be dropped into an app that already exists.
|
|
13
|
+
*/
|
|
14
|
+
import { MalformedJsonError, chain, type HttpRouter, type HttpMethod, type RequestContext, type ResponseResult, type Middleware, type Handler } from './router.js';
|
|
15
|
+
|
|
16
|
+
interface ExpressLike {
|
|
17
|
+
use(...handlers: Function[]): void;
|
|
18
|
+
get(path: string, handler: Function): void;
|
|
19
|
+
post(path: string, handler: Function): void;
|
|
20
|
+
put(path: string, handler: Function): void;
|
|
21
|
+
patch(path: string, handler: Function): void;
|
|
22
|
+
delete(path: string, handler: Function): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const METHOD_MAP: Record<HttpMethod, 'get' | 'post' | 'put' | 'patch' | 'delete'> = {
|
|
26
|
+
GET: 'get',
|
|
27
|
+
POST: 'post',
|
|
28
|
+
PUT: 'put',
|
|
29
|
+
PATCH: 'patch',
|
|
30
|
+
DELETE: 'delete',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Core declares this cap (`wire/call.ts`) and three doors read it there. This package
|
|
34
|
+
// declares NO Fougere dependency — a leaf port does not take one on the kernel for a
|
|
35
|
+
// number — so it keeps the fourth copy, deliberately.
|
|
36
|
+
const MAX_BODY_BYTES = 1024 * 1024;
|
|
37
|
+
|
|
38
|
+
/** Drain the Node stream. Only reached when no body parser ran before us. */
|
|
39
|
+
export function readRawBody(req: any): Promise<string> {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const chunks: Buffer[] = [];
|
|
42
|
+
let size = 0;
|
|
43
|
+
let exceeded = false;
|
|
44
|
+
req.on('data', (chunk: Buffer) => {
|
|
45
|
+
if (exceeded) return;
|
|
46
|
+
size += chunk.length;
|
|
47
|
+
if (size > MAX_BODY_BYTES) {
|
|
48
|
+
exceeded = true;
|
|
49
|
+
reject(Object.assign(new Error('Payload too large'), { statusCode: 413 }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
chunks.push(Buffer.from(chunk));
|
|
53
|
+
});
|
|
54
|
+
req.on('end', () => { if (!exceeded) resolve(Buffer.concat(chunks).toString('utf8')); });
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The JSON body of an Express request, whoever parsed it.
|
|
61
|
+
*
|
|
62
|
+
* Exported because two consumers need the same Express fact: this adapter, and the
|
|
63
|
+
* middlewares in `@fougere/app/express`. `express.json()` may or may not have run —
|
|
64
|
+
* an adapter that only works when the host app is configured a particular way is a
|
|
65
|
+
* footgun — so this trusts `req.body` when it is there and drains the stream when it
|
|
66
|
+
* is not. Parsed once per request: a drained stream answers empty the second time.
|
|
67
|
+
*/
|
|
68
|
+
export function readExpressBody(req: any): Promise<unknown> {
|
|
69
|
+
if (req.__fougereBody) return req.__fougereBody;
|
|
70
|
+
|
|
71
|
+
const verb = String(req.method ?? 'GET').toUpperCase();
|
|
72
|
+
req.__fougereBody = (async () => {
|
|
73
|
+
if (verb === 'GET' || verb === 'HEAD') return {};
|
|
74
|
+
// `null` is parsed JSON, not evidence that no parser ran. Falling through here
|
|
75
|
+
// turned an explicit domain value into `{}` (or tried to drain an already consumed
|
|
76
|
+
// stream), while every other door kept it.
|
|
77
|
+
if (req.body !== undefined) return req.body;
|
|
78
|
+
const contentType = String(req.headers?.['content-type'] ?? '');
|
|
79
|
+
if (!contentType.toLowerCase().includes('json')) return {};
|
|
80
|
+
const raw = await readRawBody(req);
|
|
81
|
+
if (!raw) return {};
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(raw);
|
|
84
|
+
} catch (cause) {
|
|
85
|
+
throw new MalformedJsonError({ cause });
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
return req.__fougereBody;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildContext(req: any): RequestContext {
|
|
92
|
+
// Kept as a plain string: a real request may be HEAD or OPTIONS, which the
|
|
93
|
+
// interface's `HttpMethod` does not name. Casting here would have made the
|
|
94
|
+
// "does this verb carry a body" question unaskable — it is asked below.
|
|
95
|
+
const verb: string = req.method.toUpperCase();
|
|
96
|
+
let request: Request | undefined;
|
|
97
|
+
|
|
98
|
+
// One reader for both consumers — see `readExpressBody`.
|
|
99
|
+
const body = () => readExpressBody(req);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
// The Request carries no body: Express may have consumed the stream already,
|
|
103
|
+
// and a Request built around a drained one lies. `body()` is the honest reader,
|
|
104
|
+
// and it is what every consumer in this repo calls.
|
|
105
|
+
get request(): Request {
|
|
106
|
+
if (!request) {
|
|
107
|
+
const host = req.headers?.host ?? 'localhost';
|
|
108
|
+
const url = `${req.protocol ?? 'http'}://${host}${req.originalUrl ?? req.url}`;
|
|
109
|
+
request = new Request(url, {
|
|
110
|
+
method: verb,
|
|
111
|
+
headers: new Headers(
|
|
112
|
+
Object.entries(req.headers ?? {})
|
|
113
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string'),
|
|
114
|
+
),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return request;
|
|
118
|
+
},
|
|
119
|
+
method: verb as HttpMethod,
|
|
120
|
+
path: req.path ?? (req.originalUrl ?? req.url ?? '').split('?')[0],
|
|
121
|
+
params: req.params ?? {},
|
|
122
|
+
query: Object.fromEntries(
|
|
123
|
+
Object.entries(req.query ?? {}).map(([key, value]) => [key, String(Array.isArray(value) ? value[0] : value)]),
|
|
124
|
+
),
|
|
125
|
+
body,
|
|
126
|
+
state: {},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sendResponse(res: any, result: ResponseResult): void {
|
|
131
|
+
if (result.headers) {
|
|
132
|
+
for (const [key, value] of Object.entries(result.headers)) res.set(key, value);
|
|
133
|
+
}
|
|
134
|
+
res.status(result.status);
|
|
135
|
+
// `raw` means "do not JSON-serialize" — `send` on a string writes it as-is, and
|
|
136
|
+
// the content-type rides in `headers`, stated by the producer rather than guessed.
|
|
137
|
+
if (result.raw) res.send(result.data);
|
|
138
|
+
else res.json(result.data);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create an HttpRouter backed by an Express app.
|
|
143
|
+
*
|
|
144
|
+
* ```ts
|
|
145
|
+
* import express from 'express'
|
|
146
|
+
* import { createExpressRouter } from '@fougere/http'
|
|
147
|
+
*
|
|
148
|
+
* const app = express()
|
|
149
|
+
* const router = createExpressRouter(app)
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function createExpressRouter(app: ExpressLike): HttpRouter {
|
|
153
|
+
const globalMiddlewares: Middleware[] = [];
|
|
154
|
+
const scopedMiddlewares: { path: string; mw: Middleware }[] = [];
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
use(...args: [Middleware] | [string, Middleware]): void {
|
|
158
|
+
const [pathOrMw, maybeMw] = args;
|
|
159
|
+
if (typeof pathOrMw === 'string') {
|
|
160
|
+
// Strip a trailing /* — prefix matching here is `startsWith`, as in the
|
|
161
|
+
// Fastify adapter, and Express 5 spells its own splat differently anyway.
|
|
162
|
+
scopedMiddlewares.push({ path: pathOrMw.replace(/\/\*$/, ''), mw: maybeMw as Middleware });
|
|
163
|
+
} else {
|
|
164
|
+
globalMiddlewares.push(pathOrMw as Middleware);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
on(method: HttpMethod, path: string, handler: Handler): void {
|
|
169
|
+
app[METHOD_MAP[method]](path, async (req: any, res: any, next: Function) => {
|
|
170
|
+
try {
|
|
171
|
+
sendResponse(res, await chain(globalMiddlewares, scopedMiddlewares, buildContext(req), handler));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
// This adapter is the one that parses JSON, so it is the one that answers
|
|
174
|
+
// for it — same 400 the Hono adapter gives, which is the other self-parsing
|
|
175
|
+
// one. Fastify's own parser answers before we ever see the request.
|
|
176
|
+
if (err instanceof MalformedJsonError) {
|
|
177
|
+
sendResponse(res, { status: 400, data: { code: 'BAD_REQUEST', message: 'Malformed JSON body' } });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if ((err as { statusCode?: number })?.statusCode === 413) {
|
|
181
|
+
sendResponse(res, { status: 413, data: { code: 'PAYLOAD_TOO_LARGE', message: 'Payload too large' } });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Anything else goes to Express's error pipeline rather than crashing the
|
|
185
|
+
// process — an app that already has an error handler keeps using it.
|
|
186
|
+
next(err);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
package/src/fastify.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fastify adapter — bridges a Fastify server to the HttpRouter interface.
|
|
3
|
+
*/
|
|
4
|
+
import { chain } from './router.js';
|
|
5
|
+
import type { HttpRouter, HttpMethod, RequestContext, ResponseResult, Middleware, Handler } from './router.js';
|
|
6
|
+
|
|
7
|
+
interface FastifyLike {
|
|
8
|
+
addHook(hook: string, handler: Function): void;
|
|
9
|
+
get(path: string, handler: Function): void;
|
|
10
|
+
post(path: string, handler: Function): void;
|
|
11
|
+
put(path: string, handler: Function): void;
|
|
12
|
+
patch(path: string, handler: Function): void;
|
|
13
|
+
delete(path: string, handler: Function): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const METHOD_MAP: Record<HttpMethod, 'get' | 'post' | 'put' | 'patch' | 'delete'> = {
|
|
17
|
+
GET: 'get',
|
|
18
|
+
POST: 'post',
|
|
19
|
+
PUT: 'put',
|
|
20
|
+
PATCH: 'patch',
|
|
21
|
+
DELETE: 'delete',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function buildContext(req: any): RequestContext {
|
|
25
|
+
const method = req.method.toUpperCase();
|
|
26
|
+
let request: Request | undefined;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
// Deferred, and it re-serializes a body fastify already parsed — which is why
|
|
30
|
+
// paying for it on every call was worth removing rather than optimising.
|
|
31
|
+
get request(): Request {
|
|
32
|
+
if (!request) {
|
|
33
|
+
const url = `${req.protocol ?? 'http'}://${req.hostname ?? 'localhost'}${req.url}`;
|
|
34
|
+
const hasBody = method !== 'GET' && method !== 'HEAD';
|
|
35
|
+
request = new Request(url, {
|
|
36
|
+
method,
|
|
37
|
+
headers: new Headers(req.headers as Record<string, string>),
|
|
38
|
+
...(hasBody && req.body !== undefined ? { body: JSON.stringify(req.body) } : {}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return request;
|
|
42
|
+
},
|
|
43
|
+
method: method as HttpMethod,
|
|
44
|
+
path: req.url,
|
|
45
|
+
params: req.params ?? {},
|
|
46
|
+
query: req.query ?? {},
|
|
47
|
+
// Absence alone gets the empty protocol object. `null` is valid parsed JSON and must
|
|
48
|
+
// reach the core unchanged when the contract admits it.
|
|
49
|
+
body: async () => req.body === undefined ? {} : req.body,
|
|
50
|
+
state: {},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sendResponse(reply: any, result: ResponseResult): void {
|
|
55
|
+
if (result.headers) {
|
|
56
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
57
|
+
// Fastify handles arrays natively (multiple headers)
|
|
58
|
+
reply.header(k, v);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// `raw` needs no branch here: fastify serializes a string as-is and an object as JSON,
|
|
62
|
+
// and the content-type rides in `headers` — the producer states it, we do not guess.
|
|
63
|
+
reply.status(result.status).send(result.data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create an HttpRouter backed by a Fastify server.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* import Fastify from 'fastify'
|
|
71
|
+
* import { createFastifyRouter } from '@fougere/http'
|
|
72
|
+
*
|
|
73
|
+
* const fastify = Fastify()
|
|
74
|
+
* const router = createFastifyRouter(fastify)
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function createFastifyRouter(server: FastifyLike): HttpRouter {
|
|
78
|
+
const globalMiddlewares: Middleware[] = [];
|
|
79
|
+
const scopedMiddlewares: { path: string; mw: Middleware }[] = [];
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
use(...args: [Middleware] | [string, Middleware]): void {
|
|
83
|
+
const [pathOrMw, maybeMw] = args;
|
|
84
|
+
if (typeof pathOrMw === 'string') {
|
|
85
|
+
// Strip trailing /* — Fastify adapter uses startsWith for prefix matching
|
|
86
|
+
const normalizedPath = pathOrMw.replace(/\/\*$/, '');
|
|
87
|
+
scopedMiddlewares.push({ path: normalizedPath, mw: maybeMw as Middleware });
|
|
88
|
+
} else {
|
|
89
|
+
globalMiddlewares.push(pathOrMw as Middleware);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
on(method: HttpMethod, path: string, handler: Handler): void {
|
|
94
|
+
const m = METHOD_MAP[method];
|
|
95
|
+
server[m](path, async (req: any, reply: any) => {
|
|
96
|
+
const ctx = buildContext(req);
|
|
97
|
+
const result = await chain(globalMiddlewares, scopedMiddlewares, ctx, handler);
|
|
98
|
+
sendResponse(reply, result);
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
package/src/hono.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono adapter — bridges a Hono app to the HttpRouter interface.
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
MalformedJsonError, PASSTHROUGH,
|
|
6
|
+
type HttpRouter, type HttpMethod, type RequestContext, type ResponseResult,
|
|
7
|
+
type Middleware, type Handler,
|
|
8
|
+
} from './router.js';
|
|
9
|
+
|
|
10
|
+
interface HonoLike {
|
|
11
|
+
use(path: string, ...handlers: Function[]): void;
|
|
12
|
+
use(...handlers: Function[]): void;
|
|
13
|
+
get(path: string, handler: Function): void;
|
|
14
|
+
post(path: string, handler: Function): void;
|
|
15
|
+
put(path: string, handler: Function): void;
|
|
16
|
+
patch(path: string, handler: Function): void;
|
|
17
|
+
delete(path: string, handler: Function): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const METHOD_MAP: Record<HttpMethod, 'get' | 'post' | 'put' | 'patch' | 'delete'> = {
|
|
21
|
+
GET: 'get',
|
|
22
|
+
POST: 'post',
|
|
23
|
+
PUT: 'put',
|
|
24
|
+
PATCH: 'patch',
|
|
25
|
+
DELETE: 'delete',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function buildContext(c: any): RequestContext {
|
|
29
|
+
// Reuse state across middleware/handler calls within the same request
|
|
30
|
+
const state = c.get('_fougereState') ?? {};
|
|
31
|
+
c.set('_fougereState', state);
|
|
32
|
+
return {
|
|
33
|
+
request: c.req.raw,
|
|
34
|
+
method: c.req.method.toUpperCase() as HttpMethod,
|
|
35
|
+
path: c.req.path,
|
|
36
|
+
params: c.req.param() ?? {},
|
|
37
|
+
query: c.req.query() ?? {},
|
|
38
|
+
body: async () => {
|
|
39
|
+
const method = c.req.method.toUpperCase();
|
|
40
|
+
const contentType = c.req.header?.('content-type') ?? c.req.raw?.headers?.get?.('content-type') ?? '';
|
|
41
|
+
if (method === 'GET' || method === 'HEAD' || !contentType.toLowerCase().includes('json')) return {};
|
|
42
|
+
try {
|
|
43
|
+
return await c.req.json();
|
|
44
|
+
} catch (cause) {
|
|
45
|
+
throw new MalformedJsonError({ cause });
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
state,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sendResponse(c: any, result: ResponseResult): Response {
|
|
53
|
+
if (result.headers) {
|
|
54
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
55
|
+
if (Array.isArray(v)) {
|
|
56
|
+
for (const item of v) c.header(k, item, { append: true });
|
|
57
|
+
} else {
|
|
58
|
+
c.header(k, v);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (result.data === undefined || result.data === null) {
|
|
63
|
+
return c.body(null, result.status);
|
|
64
|
+
}
|
|
65
|
+
if (result.raw) {
|
|
66
|
+
return c.body(result.data, result.status);
|
|
67
|
+
}
|
|
68
|
+
return c.json(result.data, result.status);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function malformedJsonResponse(c: any): Response {
|
|
72
|
+
return sendResponse(c, { status: 400, data: { code: 'BAD_REQUEST', message: 'Malformed JSON body' } });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create an HttpRouter backed by a Hono app.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* import { Hono } from 'hono'
|
|
80
|
+
* import { createHonoRouter } from '@fougere/http'
|
|
81
|
+
*
|
|
82
|
+
* const hono = new Hono()
|
|
83
|
+
* const router = createHonoRouter(hono)
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function createHonoRouter(app: HonoLike): HttpRouter {
|
|
87
|
+
return {
|
|
88
|
+
use(...args: [Middleware] | [string, Middleware]): void {
|
|
89
|
+
const [pathOrMw, maybeMw] = args;
|
|
90
|
+
const path = typeof pathOrMw === 'string' ? pathOrMw : undefined;
|
|
91
|
+
const mw: Middleware = (typeof pathOrMw === 'function' ? pathOrMw : maybeMw) as Middleware;
|
|
92
|
+
|
|
93
|
+
const honoMiddleware = async (c: any, next: Function) => {
|
|
94
|
+
try {
|
|
95
|
+
const ctx = buildContext(c);
|
|
96
|
+
const result = await mw(ctx, async () => {
|
|
97
|
+
await next();
|
|
98
|
+
// After next(), Hono has already set the response. The sentinel says so, and it
|
|
99
|
+
// is a symbol on purpose — see PASSTHROUGH.
|
|
100
|
+
return { status: c.res.status, data: PASSTHROUGH };
|
|
101
|
+
});
|
|
102
|
+
// Anything else is the middleware's own response, empty body included.
|
|
103
|
+
if (result.data !== PASSTHROUGH) {
|
|
104
|
+
return sendResponse(c, result);
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err instanceof MalformedJsonError) return malformedJsonResponse(c);
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (path) {
|
|
113
|
+
// Hono requires /* suffix for prefix matching on middleware
|
|
114
|
+
const honoPath = path.endsWith('/*') ? path : `${path}/*`;
|
|
115
|
+
app.use(honoPath, honoMiddleware);
|
|
116
|
+
} else {
|
|
117
|
+
app.use(honoMiddleware);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
on(method: HttpMethod, path: string, handler: Handler): void {
|
|
122
|
+
const m = METHOD_MAP[method];
|
|
123
|
+
app[m](path, async (c: any) => {
|
|
124
|
+
const ctx = buildContext(c);
|
|
125
|
+
try {
|
|
126
|
+
const result = await handler(ctx);
|
|
127
|
+
return sendResponse(c, result);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err instanceof MalformedJsonError) return malformedJsonResponse(c);
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
HttpMethod,
|
|
3
|
+
RequestContext,
|
|
4
|
+
ResponseResult,
|
|
5
|
+
Handler,
|
|
6
|
+
Next,
|
|
7
|
+
Middleware,
|
|
8
|
+
HttpRouter,
|
|
9
|
+
} from './router.js';
|
|
10
|
+
export { MalformedJsonError, PASSTHROUGH } from './router.js';
|
|
11
|
+
|
|
12
|
+
export { createHonoRouter } from './hono.js';
|
|
13
|
+
export { createFastifyRouter } from './fastify.js';
|
|
14
|
+
export { createExpressRouter, readExpressBody } from './express.js';
|
|
15
|
+
export { httpLogger } from './logger.js';
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP request logger middleware.
|
|
3
|
+
*/
|
|
4
|
+
import type { Middleware } from './router.js';
|
|
5
|
+
|
|
6
|
+
interface LoggerLike {
|
|
7
|
+
info(msg: string, ...args: unknown[]): void;
|
|
8
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
12
|
+
'2': '\x1b[32m', // green
|
|
13
|
+
'3': '\x1b[36m', // cyan
|
|
14
|
+
'4': '\x1b[33m', // yellow
|
|
15
|
+
'5': '\x1b[31m', // red
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* HTTP request logger — logs method, path, status, and duration.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { httpLogger } from '@fougere/http'
|
|
23
|
+
*
|
|
24
|
+
* router.use(httpLogger(logger))
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function httpLogger(logger: LoggerLike): Middleware {
|
|
28
|
+
return async (ctx, next) => {
|
|
29
|
+
const start = performance.now();
|
|
30
|
+
const result = await next();
|
|
31
|
+
const ms = (performance.now() - start).toFixed(1);
|
|
32
|
+
|
|
33
|
+
const method = ctx.method.padEnd(7);
|
|
34
|
+
const status = result.status;
|
|
35
|
+
const log = status >= 400 ? 'warn' : 'info';
|
|
36
|
+
|
|
37
|
+
logger[log](`${method} ${ctx.path} ${status} ${ms}ms`);
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fougere/http — framework-agnostic HTTP router interface.
|
|
3
|
+
*
|
|
4
|
+
* Adapters (Hono, Fastify, etc.) implement HttpRouter.
|
|
5
|
+
* Consumers (schema-rest, schema-graphql) program against it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
9
|
+
|
|
10
|
+
export interface RequestContext {
|
|
11
|
+
/**
|
|
12
|
+
* The request as a Web Standard `Request`.
|
|
13
|
+
*
|
|
14
|
+
* Built on FIRST ACCESS. Every adapter used to build one eagerly, and no consumer
|
|
15
|
+
* in this repo reads it — measured, zero call sites outside the adapters and their
|
|
16
|
+
* tests, while the construction cost a fifth of the port's throughput on the engines
|
|
17
|
+
* that have no `Request` of their own (express, fastify). Hono hands over the one it
|
|
18
|
+
* already has, so nothing is deferred there.
|
|
19
|
+
*
|
|
20
|
+
* The fields beside it — `method`, `path`, `params`, `query`, `body` — are what the
|
|
21
|
+
* projections read, and they are not derived from this one.
|
|
22
|
+
*/
|
|
23
|
+
readonly request: Request;
|
|
24
|
+
method: HttpMethod;
|
|
25
|
+
path: string;
|
|
26
|
+
params: Record<string, string>;
|
|
27
|
+
query: Record<string, string>;
|
|
28
|
+
body: () => Promise<unknown>;
|
|
29
|
+
/** Extensible bag — middlewares deposit data here (auth, session, etc.). */
|
|
30
|
+
state: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResponseResult {
|
|
34
|
+
status: number;
|
|
35
|
+
data: unknown;
|
|
36
|
+
headers?: Record<string, string | string[]>;
|
|
37
|
+
/** Send data as raw body (not JSON-serialized). Use for HTML, XML, CSV, plain text, etc. */
|
|
38
|
+
raw?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Raised by an adapter when a request claims JSON but cannot be parsed. */
|
|
42
|
+
export class MalformedJsonError extends Error {
|
|
43
|
+
constructor(options?: ErrorOptions) {
|
|
44
|
+
super('Malformed JSON body', options);
|
|
45
|
+
this.name = 'MalformedJsonError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type Handler = (ctx: RequestContext) => Promise<ResponseResult>;
|
|
50
|
+
export type Next = () => Promise<ResponseResult>;
|
|
51
|
+
/**
|
|
52
|
+
* What `next()` answers when the host framework — not us — owns the chain.
|
|
53
|
+
*
|
|
54
|
+
* Express and Fastify build their own middleware chain here, so a middleware's return IS
|
|
55
|
+
* the response and there is nothing to distinguish. Hono owns its chain, so its adapter has
|
|
56
|
+
* to tell "the middleware answered" from "the middleware delegated", and it used
|
|
57
|
+
* `data === null` for the second. `null` is a legal body — `{ status: 403, data: null }` is
|
|
58
|
+
* the ordinary spelling of a deny — so a middleware that REFUSED with no body was read as
|
|
59
|
+
* having delegated: it was silently bypassed on Hono and honoured on Fastify. One contract,
|
|
60
|
+
* two verdicts, on a value inside the value space.
|
|
61
|
+
*
|
|
62
|
+
* A symbol cannot be produced by accident, and no middleware author ever writes it: only an
|
|
63
|
+
* adapter that does not own its chain returns it from the `next` it hands out.
|
|
64
|
+
*/
|
|
65
|
+
export const PASSTHROUGH: unique symbol = Symbol('fougere.http.passthrough');
|
|
66
|
+
|
|
67
|
+
export type Middleware = (ctx: RequestContext, next: Next) => Promise<ResponseResult>;
|
|
68
|
+
|
|
69
|
+
export interface HttpRouter {
|
|
70
|
+
/** Register a global middleware. */
|
|
71
|
+
use(middleware: Middleware): void;
|
|
72
|
+
/** Register a middleware scoped to a path prefix. */
|
|
73
|
+
use(path: string, middleware: Middleware): void;
|
|
74
|
+
/** Register a route handler. */
|
|
75
|
+
on(method: HttpMethod, path: string, handler: Handler): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The onion chain both adapters run: matching middlewares around the handler.
|
|
80
|
+
*
|
|
81
|
+
* Stated once because it was stated twice, identically, in `express.ts` and `fastify.ts` —
|
|
82
|
+
* the same package, and the same place `PASSTHROUGH` already lives. A scoped middleware
|
|
83
|
+
* matches by `startsWith`, which is what makes the two adapters agree on a mounted path.
|
|
84
|
+
*/
|
|
85
|
+
export function chain(
|
|
86
|
+
global: Middleware[],
|
|
87
|
+
scoped: { path: string; mw: Middleware }[],
|
|
88
|
+
ctx: RequestContext,
|
|
89
|
+
handler: Handler,
|
|
90
|
+
): Promise<ResponseResult> {
|
|
91
|
+
const matching = [
|
|
92
|
+
...global,
|
|
93
|
+
...scoped.filter((s) => ctx.path.startsWith(s.path)).map((s) => s.mw),
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
let index = 0;
|
|
97
|
+
const next = (): Promise<ResponseResult> =>
|
|
98
|
+
index < matching.length ? matching[index++](ctx, next) : handler(ctx);
|
|
99
|
+
|
|
100
|
+
return next();
|
|
101
|
+
}
|