@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 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 express, { Express } from 'express';
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): _basaltkit_core.BasaltPlugin<unknown>;
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
- // src/index.ts
2
- import { createToken, definePlugin, ensureMetadata } from "@basaltkit/core";
3
- import {
4
- HttpServerCollector,
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
- return {
17
- method: req.method,
18
- url: req.originalUrl,
19
- headers: req.headers,
20
- params: req.params,
21
- query: req.query,
22
- body: req.body,
23
- ...req.ip ? { ip: req.ip } : {},
24
- ...req.route?.path ? { routePattern: String(req.route.path) } : {},
25
- raw: req
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
- var ExpressReply = class {
29
- constructor(res) {
30
- this.res = res;
31
- }
32
- res;
33
- _status = 200;
34
- _sent = false;
35
- get sent() {
36
- return this._sent;
37
- }
38
- get statusCode() {
39
- return this._status;
40
- }
41
- get raw() {
42
- return this.res;
43
- }
44
- code(status) {
45
- this._status = status;
46
- return this;
47
- }
48
- header(name, value) {
49
- this.res.setHeader(name, value);
50
- return this;
51
- }
52
- send(payload) {
53
- this._sent = true;
54
- this.res.status(this._status);
55
- if (payload === void 0 || payload === null) this.res.end();
56
- else if (typeof payload === "string") this.res.send(payload);
57
- else this.res.json(payload);
58
- return this;
59
- }
60
- };
61
- function basaltHandler(definition, container, enrichers, guards) {
62
- return async (req, res) => {
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 expressPlugin(options = {}) {
93
- const collector = new HttpServerCollector();
94
- return definePlugin({
95
- name: "basalt:express",
96
- register({ container }) {
97
- container.singleton(EXPRESS, () => {
98
- const app = options.app ?? express();
99
- app.use(express.json());
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
- next();
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
- app.use(async (req, res, next) => {
123
- const reply = new ExpressReply(res);
124
- if (await collector.runPre(toNeutralRequest(req), reply)) return;
125
- next();
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
- for (const definition of routes) {
137
- metadata.add("http:routes", {
138
- method: definition.method,
139
- url: definition.url,
140
- meta: definition.meta ?? {},
141
- body: definition.body,
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
- export {
151
- EXPRESS,
152
- expressPlugin,
153
- registerRoutes
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.2.0",
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.0",
18
- "@basaltkit/http": "^1.7.0"
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": "^22.15.0",
25
+ "@types/node": "^26.3.0",
26
26
  "express": "^5.1.0",
27
- "tsup": "^8.4.0",
28
- "typescript": "^5.8.0",
29
- "vitest": "^3.1.0",
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/Zebedeu/basalt.git",
37
+ "url": "git+https://github.com/basaltkit/basalt.git",
39
38
  "directory": "packages/express"
40
39
  },
41
- "homepage": "https://github.com/Zebedeu/basalt/tree/main/packages/express#readme",
42
- "bugs": "https://github.com/Zebedeu/basalt/issues",
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": "tsup src/index.ts --format esm --dts --clean",
50
+ "build": "tsc -p tsconfig.build.json",
52
51
  "test": "vitest run",
53
52
  "typecheck": "tsc --noEmit"
54
53
  }