@basaltkit/express 1.2.0 → 1.3.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 +6 -0
- package/dist/index.d.ts +13 -10
- package/dist/index.js +156 -145
- package/package.json +11 -12
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://basaltkit-docs.pages.dev">
|
|
3
|
+
<img src="https://basaltkit-docs.pages.dev/social-card.png" alt="Basalt" width="440">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
1
7
|
# @basaltkit/express
|
|
2
8
|
|
|
3
9
|
Basalt adapter for [Express](https://expressjs.com): the same typed routes, enrichers, and guards you'd use in Fastify or Hono, running on an Express server. You need it when you already use Express (or want its huge middleware ecosystem) and want Basalt's validation, per-request context, and standardized errors.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import * as _basaltkit_core from '@basaltkit/core';
|
|
2
1
|
import { Container } from '@basaltkit/core';
|
|
3
|
-
import { BasaltRoute, RequestEnricher, RouteGuard } from '@basaltkit/http';
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
declare const EXPRESS: _basaltkit_core.Token<express.Express>;
|
|
2
|
+
import { type BasaltRoute, type RequestEnricher, type RouteGuard } from '@basaltkit/http';
|
|
3
|
+
import { type Express } from 'express';
|
|
4
|
+
export declare const EXPRESS: import("@basaltkit/core").Token<Express>;
|
|
7
5
|
/** Mounts Basalt routes on an Express app (usable without the plugin). */
|
|
8
|
-
declare function registerRoutes(app: Express, routes: BasaltRoute[], container?: Container, enrichers?: RequestEnricher[], guards?: RouteGuard[]): void;
|
|
9
|
-
interface ExpressPluginOptions {
|
|
6
|
+
export declare function registerRoutes(app: Express, routes: BasaltRoute[], container?: Container, enrichers?: RequestEnricher[], guards?: RouteGuard[]): void;
|
|
7
|
+
export interface ExpressPluginOptions {
|
|
10
8
|
routes?: BasaltRoute[];
|
|
11
9
|
/** Bring your own Express app; otherwise one is created with `express.json()`. */
|
|
12
10
|
app?: Express;
|
|
11
|
+
/**
|
|
12
|
+
* Serve the neutral JSON body (`NOT_FOUND_RESPONSE` from @basaltkit/http)
|
|
13
|
+
* for unmatched routes, identical across all adapters, instead of Express's
|
|
14
|
+
* HTML default. Default: true. Pass false to keep Express's own handling
|
|
15
|
+
* (e.g. when the app mounts its own catch-all after boot).
|
|
16
|
+
*/
|
|
17
|
+
notFound?: boolean;
|
|
13
18
|
}
|
|
14
19
|
/**
|
|
15
20
|
* Runs Basalt on Express. The same routes, enrichers, guards and edge plugins
|
|
16
21
|
* you register for Fastify work unchanged — resolve `EXPRESS` for the app to
|
|
17
22
|
* `listen()`.
|
|
18
23
|
*/
|
|
19
|
-
declare function expressPlugin(options?: ExpressPluginOptions):
|
|
20
|
-
|
|
21
|
-
export { EXPRESS, type ExpressPluginOptions, expressPlugin, registerRoutes };
|
|
24
|
+
export declare function expressPlugin(options?: ExpressPluginOptions): import("@basaltkit/core").BasaltPlugin<unknown>;
|
package/dist/index.js
CHANGED
|
@@ -1,154 +1,165 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
HTTP_SERVER,
|
|
6
|
-
runRoute,
|
|
7
|
-
toErrorResponse,
|
|
8
|
-
isSseResponse,
|
|
9
|
-
sseProducerOf,
|
|
10
|
-
driveSse,
|
|
11
|
-
SSE_HEADERS
|
|
12
|
-
} from "@basaltkit/http";
|
|
13
|
-
import express from "express";
|
|
14
|
-
var EXPRESS = createToken("express");
|
|
1
|
+
import { Container, createToken, definePlugin, ensureMetadata } from '@basaltkit/core';
|
|
2
|
+
import { NOT_FOUND_RESPONSE, HttpServerCollector, HTTP_SERVER, runRoute, toErrorResponse, isSseResponse, sseProducerOf, driveSse, SSE_HEADERS, } from '@basaltkit/http';
|
|
3
|
+
import express, {} from 'express';
|
|
4
|
+
export const EXPRESS = createToken('express');
|
|
15
5
|
function toNeutralRequest(req) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
6
|
+
return {
|
|
7
|
+
method: req.method,
|
|
8
|
+
url: req.originalUrl,
|
|
9
|
+
headers: req.headers,
|
|
10
|
+
params: req.params,
|
|
11
|
+
query: req.query,
|
|
12
|
+
body: req.body,
|
|
13
|
+
...(req.ip ? { ip: req.ip } : {}),
|
|
14
|
+
...(req.route?.path ? { routePattern: String(req.route.path) } : {}),
|
|
15
|
+
raw: req,
|
|
16
|
+
};
|
|
27
17
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const reply = new ExpressReply(res);
|
|
64
|
-
try {
|
|
65
|
-
const result = await runRoute(definition, toNeutralRequest(req), reply, {
|
|
66
|
-
...container ? { container } : {},
|
|
67
|
-
enrichers,
|
|
68
|
-
guards
|
|
69
|
-
});
|
|
70
|
-
if (isSseResponse(result)) {
|
|
71
|
-
res.writeHead(200, SSE_HEADERS);
|
|
72
|
-
await driveSse(sseProducerOf(result), {
|
|
73
|
-
write: (frame) => void res.write(frame),
|
|
74
|
-
end: () => res.end(),
|
|
75
|
-
onClose: (listener) => req.on("close", listener)
|
|
76
|
-
});
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (!reply.sent) reply.send(result);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
const { status, body } = toErrorResponse(error);
|
|
82
|
-
if (!res.headersSent) res.status(status).json(body);
|
|
18
|
+
/** Neutral reply backed by an Express `res`. */
|
|
19
|
+
class ExpressReply {
|
|
20
|
+
res;
|
|
21
|
+
_status = 200;
|
|
22
|
+
_sent = false;
|
|
23
|
+
constructor(res) {
|
|
24
|
+
this.res = res;
|
|
25
|
+
}
|
|
26
|
+
get sent() {
|
|
27
|
+
return this._sent;
|
|
28
|
+
}
|
|
29
|
+
get statusCode() {
|
|
30
|
+
return this._status;
|
|
31
|
+
}
|
|
32
|
+
get raw() {
|
|
33
|
+
return this.res;
|
|
34
|
+
}
|
|
35
|
+
code(status) {
|
|
36
|
+
this._status = status;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
header(name, value) {
|
|
40
|
+
this.res.setHeader(name, value);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
send(payload) {
|
|
44
|
+
this._sent = true;
|
|
45
|
+
this.res.status(this._status);
|
|
46
|
+
if (payload === undefined || payload === null)
|
|
47
|
+
this.res.end();
|
|
48
|
+
else if (typeof payload === 'string')
|
|
49
|
+
this.res.send(payload);
|
|
50
|
+
else
|
|
51
|
+
this.res.json(payload);
|
|
52
|
+
return this;
|
|
83
53
|
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function registerRoutes(app, routes, container, enrichers = [], guards = []) {
|
|
87
|
-
const router = app;
|
|
88
|
-
for (const definition of routes) {
|
|
89
|
-
router[definition.method.toLowerCase()](definition.url, basaltHandler(definition, container, enrichers, guards));
|
|
90
|
-
}
|
|
91
54
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
app.use(express.urlencoded({ extended: false }));
|
|
101
|
-
return app;
|
|
102
|
-
});
|
|
103
|
-
container.singleton(HTTP_SERVER, () => collector);
|
|
104
|
-
},
|
|
105
|
-
boot({ container, hooks }) {
|
|
106
|
-
const app = container.get(EXPRESS);
|
|
107
|
-
const routes = options.routes ?? [];
|
|
108
|
-
const metadata = ensureMetadata(container);
|
|
109
|
-
const enrichers = metadata.get("http:enrichers");
|
|
110
|
-
const guards = metadata.get("http:guards");
|
|
111
|
-
const router = app;
|
|
112
|
-
hooks.on("app:booted", () => {
|
|
113
|
-
if (collector.afterHooks.length) {
|
|
114
|
-
app.use((req, res, next) => {
|
|
115
|
-
const start = Date.now();
|
|
116
|
-
res.on("finish", () => {
|
|
117
|
-
void collector.runAfter(toNeutralRequest(req), new ExpressReply(res), res.statusCode, Date.now() - start);
|
|
55
|
+
function basaltHandler(definition, container, enrichers, guards) {
|
|
56
|
+
return async (req, res) => {
|
|
57
|
+
const reply = new ExpressReply(res);
|
|
58
|
+
try {
|
|
59
|
+
const result = await runRoute(definition, toNeutralRequest(req), reply, {
|
|
60
|
+
...(container ? { container } : {}),
|
|
61
|
+
enrichers,
|
|
62
|
+
guards,
|
|
118
63
|
});
|
|
119
|
-
|
|
120
|
-
|
|
64
|
+
if (isSseResponse(result)) {
|
|
65
|
+
res.writeHead(200, SSE_HEADERS);
|
|
66
|
+
await driveSse(sseProducerOf(result), {
|
|
67
|
+
write: (frame) => void res.write(frame),
|
|
68
|
+
end: () => res.end(),
|
|
69
|
+
onClose: (listener) => req.on('close', listener),
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!reply.sent)
|
|
74
|
+
reply.send(result);
|
|
121
75
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
127
|
-
registerRoutes(app, routes, container, enrichers, guards);
|
|
128
|
-
for (const { method, url, handler } of collector.extraRoutes) {
|
|
129
|
-
router[method.toLowerCase()](url, async (req, res) => {
|
|
130
|
-
const reply = new ExpressReply(res);
|
|
131
|
-
const result = await handler({ request: toNeutralRequest(req), reply });
|
|
132
|
-
if (!reply.sent) reply.send(result);
|
|
133
|
-
});
|
|
76
|
+
catch (error) {
|
|
77
|
+
const { status, body } = toErrorResponse(error);
|
|
78
|
+
if (!res.headersSent)
|
|
79
|
+
res.status(status).json(body);
|
|
134
80
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
query: definition.query,
|
|
143
|
-
params: definition.params,
|
|
144
|
-
response: definition.response
|
|
145
|
-
});
|
|
146
|
-
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Mounts Basalt routes on an Express app (usable without the plugin). */
|
|
84
|
+
export function registerRoutes(app, routes, container, enrichers = [], guards = []) {
|
|
85
|
+
const router = app;
|
|
86
|
+
for (const definition of routes) {
|
|
87
|
+
router[definition.method.toLowerCase()](definition.url, basaltHandler(definition, container, enrichers, guards));
|
|
147
88
|
}
|
|
148
|
-
});
|
|
149
89
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Runs Basalt on Express. The same routes, enrichers, guards and edge plugins
|
|
92
|
+
* you register for Fastify work unchanged — resolve `EXPRESS` for the app to
|
|
93
|
+
* `listen()`.
|
|
94
|
+
*/
|
|
95
|
+
export function expressPlugin(options = {}) {
|
|
96
|
+
const collector = new HttpServerCollector();
|
|
97
|
+
return definePlugin({
|
|
98
|
+
name: 'basalt:express',
|
|
99
|
+
register({ container }) {
|
|
100
|
+
container.singleton(EXPRESS, () => {
|
|
101
|
+
const app = options.app ?? express();
|
|
102
|
+
app.use(express.json());
|
|
103
|
+
// HTML forms and the SAML ACS binding post application/x-www-form-urlencoded.
|
|
104
|
+
app.use(express.urlencoded({ extended: false }));
|
|
105
|
+
return app;
|
|
106
|
+
});
|
|
107
|
+
container.singleton(HTTP_SERVER, () => collector);
|
|
108
|
+
},
|
|
109
|
+
boot({ container, hooks }) {
|
|
110
|
+
const app = container.get(EXPRESS);
|
|
111
|
+
const routes = options.routes ?? [];
|
|
112
|
+
const metadata = ensureMetadata(container);
|
|
113
|
+
const enrichers = metadata.get('http:enrichers');
|
|
114
|
+
const guards = metadata.get('http:guards');
|
|
115
|
+
const router = app;
|
|
116
|
+
// Mount everything once edge plugins have registered their hooks/routes,
|
|
117
|
+
// in the order Express needs: after-hooks → pre-hooks → routes.
|
|
118
|
+
hooks.on('app:booted', () => {
|
|
119
|
+
if (collector.afterHooks.length) {
|
|
120
|
+
app.use((req, res, next) => {
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
res.on('finish', () => {
|
|
123
|
+
void collector.runAfter(toNeutralRequest(req), new ExpressReply(res), res.statusCode, Date.now() - start);
|
|
124
|
+
});
|
|
125
|
+
next();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
app.use(async (req, res, next) => {
|
|
129
|
+
const reply = new ExpressReply(res);
|
|
130
|
+
if (await collector.runPre(toNeutralRequest(req), reply))
|
|
131
|
+
return;
|
|
132
|
+
next();
|
|
133
|
+
});
|
|
134
|
+
registerRoutes(app, routes, container, enrichers, guards);
|
|
135
|
+
for (const { method, url, handler } of collector.extraRoutes) {
|
|
136
|
+
router[method.toLowerCase()](url, async (req, res) => {
|
|
137
|
+
const reply = new ExpressReply(res);
|
|
138
|
+
const result = await handler({ request: toNeutralRequest(req), reply });
|
|
139
|
+
if (!reply.sent)
|
|
140
|
+
reply.send(result);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Mounted last, so anything unmatched gets the neutral JSON 404
|
|
144
|
+
// instead of Express's HTML default.
|
|
145
|
+
if (options.notFound !== false) {
|
|
146
|
+
app.use((_req, res) => {
|
|
147
|
+
if (!res.headersSent)
|
|
148
|
+
res.status(404).json(NOT_FOUND_RESPONSE);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
for (const definition of routes) {
|
|
153
|
+
metadata.add('http:routes', {
|
|
154
|
+
method: definition.method,
|
|
155
|
+
url: definition.url,
|
|
156
|
+
meta: definition.meta ?? {},
|
|
157
|
+
body: definition.body,
|
|
158
|
+
query: definition.query,
|
|
159
|
+
params: definition.params,
|
|
160
|
+
response: definition.response,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/express",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Express adapter for Basalt: run the same typed routes, enrichers and guards on Express that you would on Fastify or Hono.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,20 +14,19 @@
|
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@basaltkit/core": "^1.1.
|
|
18
|
-
"@basaltkit/http": "^1.
|
|
17
|
+
"@basaltkit/core": "^1.1.2",
|
|
18
|
+
"@basaltkit/http": "^1.10.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"express": "^4.19.0 || ^5.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/express": "^5.0.0",
|
|
25
|
-
"@types/node": "^
|
|
25
|
+
"@types/node": "^26.3.0",
|
|
26
26
|
"express": "^5.1.0",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"zod": "^3.24.0",
|
|
27
|
+
"typescript": "^7.0.2",
|
|
28
|
+
"vitest": "^4.1.11",
|
|
29
|
+
"zod": "^3.24.0 || ^4.0.0",
|
|
31
30
|
"@basaltkit/tsconfig": "^0.24.0"
|
|
32
31
|
},
|
|
33
32
|
"publishConfig": {
|
|
@@ -35,11 +34,11 @@
|
|
|
35
34
|
},
|
|
36
35
|
"repository": {
|
|
37
36
|
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/
|
|
37
|
+
"url": "git+https://github.com/basaltkit/basalt.git",
|
|
39
38
|
"directory": "packages/express"
|
|
40
39
|
},
|
|
41
|
-
"homepage": "https://github.com/
|
|
42
|
-
"bugs": "https://github.com/
|
|
40
|
+
"homepage": "https://github.com/basaltkit/basalt/tree/main/packages/express#readme",
|
|
41
|
+
"bugs": "https://github.com/basaltkit/basalt/issues",
|
|
43
42
|
"keywords": [
|
|
44
43
|
"basalt",
|
|
45
44
|
"typescript",
|
|
@@ -48,7 +47,7 @@
|
|
|
48
47
|
"adapter"
|
|
49
48
|
],
|
|
50
49
|
"scripts": {
|
|
51
|
-
"build": "
|
|
50
|
+
"build": "tsc -p tsconfig.build.json",
|
|
52
51
|
"test": "vitest run",
|
|
53
52
|
"typecheck": "tsc --noEmit"
|
|
54
53
|
}
|