open-nest 0.0.2 → 0.1.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.
@@ -128,12 +128,57 @@ interface MultipartLimits {
128
128
  interface MultipartOptions {
129
129
  limits?: MultipartLimits;
130
130
  }
131
- declare function FileInterceptor(field: string, options?: MultipartOptions): Type<NestInterceptor>;
132
- declare function FilesInterceptor(field: string, maxCount?: number, options?: MultipartOptions): Type<NestInterceptor>;
133
- declare function AnyFilesInterceptor(options?: MultipartOptions): Type<NestInterceptor>;
131
+ export declare function FileInterceptor(field: string, options?: MultipartOptions): Type<NestInterceptor>;
132
+ export declare function FilesInterceptor(field: string, maxCount?: number, options?: MultipartOptions): Type<NestInterceptor>;
133
+ export declare function AnyFilesInterceptor(options?: MultipartOptions): Type<NestInterceptor>;
134
+ //#endregion
135
+ //#region src/lib/gateway.d.ts
136
+ interface GatewayClient {
137
+ send(data: string): void;
138
+ data: {
139
+ path: string;
140
+ };
141
+ }
142
+ interface Gateway {
143
+ path: string;
144
+ connect?: (client: GatewayClient) => void;
145
+ }
146
+ interface MessageHandler {
147
+ message: string;
148
+ callback: (data: unknown) => unknown;
149
+ }
150
+ interface Subscription {
151
+ unsubscribe(): void;
152
+ }
153
+ interface ResultStream {
154
+ subscribe(observer: {
155
+ next: (value: unknown) => void;
156
+ error?: (error: unknown) => void;
157
+ }): Subscription;
158
+ }
159
+ export declare function gatewayPath(path: string): string;
160
+ /**
161
+ * Records Nest gateways during `init`. The runtime server performs the upgrade.
162
+ */
163
+ export declare class WsGateway<T extends GatewayClient = GatewayClient> {
164
+ private readonly gateways;
165
+ private readonly clients;
166
+ create(_port: number, options?: {
167
+ path?: string;
168
+ }): Gateway;
169
+ bindClientConnect(server: Gateway, callback: (client: T) => void): void;
170
+ bindMessageHandlers(client: T, handlers: MessageHandler[], transform: (result: unknown) => ResultStream): void;
171
+ bindClientDisconnect(client: T, callback: () => void): void;
172
+ close(): void;
173
+ matches(pathname: string): boolean;
174
+ hasGateways(): boolean;
175
+ open(client: T): void;
176
+ receive(client: T, raw: string | Uint8Array): void;
177
+ closed(client: T): void;
178
+ }
134
179
  //#endregion
135
180
  //#region src/fetch/index.d.ts
136
- interface CorsOptions {
181
+ export interface CorsOptions {
137
182
  origin?: boolean | string | RegExp | Array<string | RegExp> | ((origin: string) => boolean);
138
183
  methods?: string | string[];
139
184
  allowedHeaders?: string | string[];
@@ -144,7 +189,7 @@ interface CorsOptions {
144
189
  /**
145
190
  * Options for {@link createFetcher}.
146
191
  */
147
- interface CreateHandlerOptions {
192
+ export interface CreateHandlerOptions {
148
193
  /**
149
194
  * Root Nest module.
150
195
  */
@@ -162,7 +207,7 @@ interface CreateHandlerOptions {
162
207
  */
163
208
  bodyLimit?: number;
164
209
  }
165
- interface Fetcher {
210
+ export interface Fetcher {
166
211
  (request: Request): Promise<Response>;
167
212
  /**
168
213
  * Resolves the underlying Nest app instance.
@@ -173,7 +218,7 @@ interface Fetcher {
173
218
  */
174
219
  close(): Promise<void>;
175
220
  }
176
- declare class FetchAdapter extends AbstractHttpAdapter<undefined, FetchRequest, FetchResponse> {
221
+ export declare class FetchAdapter extends AbstractHttpAdapter<undefined, FetchRequest, FetchResponse> {
177
222
  private bodyLimit;
178
223
  private rawBody;
179
224
  private shuttingDown;
@@ -212,6 +257,6 @@ declare class FetchAdapter extends AbstractHttpAdapter<undefined, FetchRequest,
212
257
  enableCors(options?: CorsOptions): void;
213
258
  setBodyLimit(limit: number): void;
214
259
  }
215
- declare function createFetcher({ module, configure, opts, bodyLimit }: CreateHandlerOptions, adapter?: FetchAdapter): Fetcher;
260
+ export declare function createFetcher({ module, configure, opts, bodyLimit }: CreateHandlerOptions, adapter?: FetchAdapter): Fetcher;
216
261
  //#endregion
217
- export { createFetcher as a, FilesInterceptor as c, Fetcher as i, MultipartLimits as l, CreateHandlerOptions as n, AnyFilesInterceptor as o, FetchAdapter as r, FileInterceptor as s, CorsOptions as t, MultipartOptions as u };
262
+ export type { GatewayClient, MultipartLimits, MultipartOptions };
@@ -664,6 +664,94 @@ function AnyFilesInterceptor(options) {
664
664
  return mixin(MixinInterceptor);
665
665
  }
666
666
  //#endregion
667
+ //#region src/lib/gateway.ts
668
+ function gatewayPath(path) {
669
+ if (!path.startsWith("/")) path = `/${path}`;
670
+ if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
671
+ return path;
672
+ }
673
+ /**
674
+ * Records Nest gateways during `init`. The runtime server performs the upgrade.
675
+ */
676
+ var WsGateway = class {
677
+ gateways = [];
678
+ clients = /* @__PURE__ */ new WeakMap();
679
+ create(_port, options) {
680
+ const gateway = { path: gatewayPath(options?.path ?? "/") };
681
+ this.gateways.push(gateway);
682
+ return gateway;
683
+ }
684
+ bindClientConnect(server, callback) {
685
+ server.connect = callback;
686
+ }
687
+ bindMessageHandlers(client, handlers, transform) {
688
+ const prev = this.clients.get(client);
689
+ this.clients.set(client, {
690
+ handlers,
691
+ transform,
692
+ subscriptions: prev?.subscriptions ?? [],
693
+ onClose: prev?.onClose
694
+ });
695
+ }
696
+ bindClientDisconnect(client, callback) {
697
+ const state = this.clients.get(client);
698
+ if (!state) return;
699
+ state.onClose = callback;
700
+ }
701
+ close() {}
702
+ matches(pathname) {
703
+ const path = gatewayPath(pathname);
704
+ return this.gateways.some((gateway) => gateway.path === path);
705
+ }
706
+ hasGateways() {
707
+ return this.gateways.length > 0;
708
+ }
709
+ open(client) {
710
+ this.gateways.find((item) => item.path === client.data.path)?.connect?.(client);
711
+ }
712
+ receive(client, raw) {
713
+ const state = this.clients.get(client);
714
+ if (!state) return;
715
+ let event;
716
+ let data;
717
+ try {
718
+ const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
719
+ const parsed = JSON.parse(text);
720
+ if (typeof parsed.event !== "string") return;
721
+ event = parsed.event;
722
+ data = parsed.data;
723
+ } catch {
724
+ return;
725
+ }
726
+ const handler = state.handlers.find((item) => item.message === event);
727
+ if (!handler) return;
728
+ let stream;
729
+ try {
730
+ stream = state.transform(handler.callback(data));
731
+ } catch {
732
+ return;
733
+ }
734
+ const subscription = stream.subscribe({
735
+ next: (value) => {
736
+ if (value === void 0) return;
737
+ client.send(JSON.stringify({
738
+ event,
739
+ data: value
740
+ }));
741
+ },
742
+ error: () => {}
743
+ });
744
+ state.subscriptions.push(subscription);
745
+ }
746
+ closed(client) {
747
+ const state = this.clients.get(client);
748
+ if (!state) return;
749
+ for (const subscription of state.subscriptions) subscription.unsubscribe();
750
+ state.onClose?.();
751
+ this.clients.delete(client);
752
+ }
753
+ };
754
+ //#endregion
667
755
  //#region src/fetch/index.ts
668
756
  const BODY_LIMIT = 1048576;
669
757
  var FetchAdapter = class extends AbstractHttpAdapter {
@@ -955,4 +1043,4 @@ function createFetcher({ module, configure, opts, bodyLimit }, adapter = new Fet
955
1043
  return handler;
956
1044
  }
957
1045
  //#endregion
958
- export { FilesInterceptor as a, FileInterceptor as i, createFetcher as n, AnyFilesInterceptor as r, FetchAdapter as t };
1046
+ export { AnyFilesInterceptor, FetchAdapter, FileInterceptor, FilesInterceptor, WsGateway, createFetcher, gatewayPath };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "open-nest",
3
- "version": "0.0.2",
4
- "description": "Run any Nest application as a Web-Standard Request/Response handler. First class support for Bun, Node, and Cloudflare Workers.",
3
+ "version": "0.1.0",
4
+ "description": "Run any Nest application as a Web-Standard Request/Response handler.",
5
5
  "files": [
6
6
  "package.json",
7
7
  "readme.md",
@@ -11,71 +11,23 @@
11
11
  "sideEffects": false,
12
12
  "exports": {
13
13
  ".": {
14
- "types": "./dist/fetch.d.mts",
15
- "import": "./dist/fetch.mjs"
16
- },
17
- "./bun": {
18
- "types": "./dist/bun.d.mts",
19
- "import": "./dist/bun.mjs"
20
- },
21
- "./node": {
22
- "types": "./dist/node.d.mts",
23
- "import": "./dist/node.mjs"
24
- },
25
- "./workers": {
26
- "types": "./dist/workers.d.mts",
27
- "import": "./dist/workers.mjs"
14
+ "bun": "./src/fetch/index.ts",
15
+ "types": "./dist/index.d.mts",
16
+ "import": "./dist/index.mjs"
28
17
  },
29
18
  "./package.json": "./package.json"
30
19
  },
31
20
  "scripts": {
32
- "dev": "tsdown --watch",
33
21
  "build": "tsdown --clean",
22
+ "dev": "tsdown --watch",
34
23
  "typecheck": "bun check",
35
- "test": "bun test",
36
- "lint": "oxlint --type-aware && oxfmt --check",
37
- "format": "oxlint --fix --fix-suggestions && oxfmt",
38
- "example:bun": "bun examples/bun/index.ts",
39
- "example:bun-websockets": "bun examples/bun-websockets/index.ts",
40
- "example:node": "node --no-strip-types --import tsx examples/node/index.ts",
41
- "example:node-websockets": "node --no-strip-types --import tsx examples/node-websockets/index.ts",
42
- "example:workers": "bunx wrangler dev --config examples/workers/wrangler.jsonc --tsconfig ../../tsconfig.json"
43
- },
44
- "dependencies": {
45
- "ws": "8.22.0"
24
+ "test": "bun test"
46
25
  },
47
26
  "devDependencies": {
48
- "@arethetypeswrong/core": "0.18.5",
49
- "@nats-io/transport-node": "3.4.0",
50
- "@nestjs/common": "12.1.3",
51
- "@nestjs/core": "12.1.3",
52
- "@nestjs/microservices": "12.1.3",
53
27
  "@nestjs/platform-express": "12.1.3",
54
- "@nestjs/platform-socket.io": "12.1.3",
55
- "@nestjs/websockets": "12.1.3",
56
- "@types/bun": "latest",
57
28
  "@types/express": "5.0.6",
58
- "@types/node": "26.6.5",
59
- "@types/ws": "8.18.2",
60
- "amqp-connection-manager": "5.0.0",
61
- "amqplib": "2.2.0",
62
- "class-transformer": "0.5.1",
63
- "class-validator": "0.15.1",
64
29
  "express": "5.3.0",
65
- "helmet": "8.3.0",
66
- "ioredis": "6.0.0",
67
- "kafkajs": "2.2.4",
68
- "mqtt": "5.16.0",
69
- "oxfmt": "0.72.0",
70
- "oxlint": "1.87.0",
71
- "oxlint-tsgolint": "7.0.2003",
72
- "publint": "0.3.25",
73
- "reflect-metadata": "0.2.2",
74
- "rxjs": "7.8.2",
75
- "tsdown": "0.23.0",
76
- "tsup": "8.5.1",
77
- "tsx": "4.23.15",
78
- "typescript": "7.0.2"
30
+ "helmet": "8.3.0"
79
31
  },
80
32
  "peerDependencies": {
81
33
  "@nestjs/common": "^12.0.0",
package/readme.md CHANGED
@@ -7,37 +7,62 @@
7
7
  [![npm version](https://img.shields.io/npm/v/open-nest)](https://www.npmjs.com/package/open-nest)
8
8
  [![bundle size](https://img.shields.io/bundlejs/size/open-nest)](https://bundlejs.com/?q=open-nest)
9
9
 
10
- Run any Nest application as a Web-Standard `Request`/`Response` handler. First class support for **Bun**, **Node**, and **Cloudflare Workers**.
10
+ ---
11
11
 
12
- ## Installation
12
+ **open-nest** (`open-nest`) runs a Nest application as a Web-standard `Request`/`Response` handler.
13
13
 
14
- ```bash
15
- bun add open-nest
16
- ```
14
+ The same Nest module serves HTTP on Bun, Node, and Cloudflare Workers. Each runtime is its own package. The handler is created once and reused for every request.
17
15
 
18
- Peer dependencies are `@nestjs/common`, `@nestjs/core`, `reflect-metadata`, and `rxjs`. WebSocket gateways also need `@nestjs/websockets`. These should already ship with your Nest installation.
16
+ ### Features:
19
17
 
20
- ## Examples
18
+ - **One handler**
19
+ `createFetcher` turns a Nest module into `(request) => Response`.
20
+
21
+ - **Bun, Node, and Workers**
22
+ `@open-nest/bun` uses `Bun.serve`. `@open-nest/node` uses `node:http`. `@open-nest/workers` uses the Worker `fetch` handler.
23
+
24
+ - **WebSockets**
25
+ `BunWsAdapter` lets `Bun.serve` upgrade the connection. `NodeWsAdapter` upgrades through [`ws`](https://github.com/websockets/ws).
26
+
27
+ - **Static files**
28
+ `BunAdapter` reads files with `Bun.file()`. `NodeAdapter` reads them with `node:fs`.
29
+
30
+ - **Worker bindings**
31
+ `workerContext()` returns `env` and `ctx` for the request in flight, including `ctx.waitUntil`.
32
+
33
+ ## Contents
34
+
35
+ - [Installation](#installation)
36
+ Add `open-nest` and a runtime package.
21
37
 
22
- - [`examples/workers`](/examples/workers)<br />
23
- <sup>[readme](/examples/workers/readme.md) ⋅ [code](/examples/workers/)</sup><br />
24
- Run a Nest application on Cloudflare Workers.
38
+ - [Usage](#usage)
39
+ Create a handler and serve it.
25
40
 
26
- - [`examples/bun`](/examples/bun)<br />
27
- <sup>[readme](/examples/bun/readme.md) ⋅ [code](/examples/bun/)</sup><br />
28
- Run a Nest application via `Bun.serve`.
41
+ - [Examples](#examples)
42
+ Small apps for each runtime.
29
43
 
30
- - [`examples/bun-websockets`](/examples/bun-websockets)<br />
31
- <sup>[readme](/examples/bun-websockets/readme.md) ⋅ [code](/examples/bun-websockets/)</sup><br />
32
- Run a Nest application via `Bun.serve` with WebSockets.
44
+ - [Bun](#bun)
45
+ - [Node](#node)
46
+ - [Workers](#workers)
33
47
 
34
- - [`examples/node`](/examples/node)<br />
35
- <sup>[readme](/examples/node/readme.md) ⋅ [code](/examples/node/)</sup><br />
36
- Run a Nest application via `http.Server`.
48
+ - [Packages](#packages)
49
+ What each package is for.
37
50
 
38
- - [`examples/node-websockets`](/examples/node-websockets)<br />
39
- <sup>[readme](/examples/node-websockets/readme.md) ⋅ [code](/examples/node-websockets/)</sup><br />
40
- Run a Nest application via `http.Server` with WebSockets.
51
+ ## Installation
52
+
53
+ Peer dependencies are `@nestjs/common`, `@nestjs/core`, `reflect-metadata`, and `rxjs`. WebSocket gateways also need `@nestjs/websockets`. These already ship with a Nest installation.
54
+
55
+ ```sh
56
+ bun add open-nest
57
+ ```
58
+
59
+ Add the runtime you serve with:
60
+
61
+ ```sh
62
+ bun add @open-nest/bun
63
+ bun add @open-nest/node
64
+ bun add @open-nest/workers
65
+ ```
41
66
 
42
67
  ## Usage
43
68
 
@@ -48,24 +73,37 @@ import { createFetcher } from "open-nest";
48
73
 
49
74
  import { AppModule } from "@/app/module";
50
75
 
76
+ // 1. Create a handler from a Nest module.
51
77
  const fetcher = createFetcher({ module: AppModule });
52
78
 
79
+ // 2. Serve it anywhere that speaks Request and Response.
53
80
  await Bun.serve({
54
81
  fetch: fetcher,
55
82
  port: 3000,
56
83
  });
57
84
  ```
58
85
 
59
- `createFetcher` from `open-nest` is the HTTP entry. It returns a function from `Request` to `Response`. WebSocket gateways and static files are available for the respective runtime under `open-nest/bun` and `open-nest/node`.
86
+ `createFetcher` initializes the Nest application once and returns a function from `Request` to `Response`.
60
87
 
61
- ### Bun
88
+ `configure` runs after the app is created and before `init`. `opts` is forwarded to `NestFactory.create`. `app()` resolves the Nest application.
62
89
 
63
90
  ```ts
64
- import "reflect-metadata";
91
+ const fetcher = createFetcher({
92
+ module: AppModule,
93
+ configure: (app) => {
94
+ app.enableCors();
95
+ app.setGlobalPrefix("api");
96
+ },
97
+ opts: { logger: ["error", "warn"] },
98
+ });
65
99
 
66
- import { BunWsAdapter, createBunServer } from "open-nest/bun";
100
+ const app = await fetcher.app();
101
+ ```
67
102
 
68
- import { AppModule } from "@/app/module";
103
+ ### Bun
104
+
105
+ ```ts
106
+ import { BunWsAdapter, createBunServer } from "@open-nest/bun";
69
107
 
70
108
  await createBunServer({
71
109
  module: AppModule,
@@ -77,20 +115,12 @@ await createBunServer({
77
115
  });
78
116
  ```
79
117
 
80
- `createBunServer` from `open-nest/bun` serves that handler with `Bun.serve`. It upgrades gateway paths and serves files with `Bun.file()`.
81
-
82
- - `createBunServer` initializes the Nest application once and starts and returns a `Bun.serve` instance.
83
- - `BunWsAdapter` is the Nest WebSocket adapter. `Bun.serve` performs the upgrade.
84
- - `BunAdapter` extends `FetchAdapter` and implements `useStaticAssets` with `Bun.file()`.
118
+ `createBunServer` starts `Bun.serve` and returns it. `BunWsAdapter` records gateway paths so `Bun.serve` can upgrade them. Static files are served with `Bun.file()`.
85
119
 
86
120
  ### Node
87
121
 
88
122
  ```ts
89
- import "reflect-metadata";
90
-
91
- import { createNodeServer, NodeWsAdapter } from "open-nest/node";
92
-
93
- import { AppModule } from "@/app/module";
123
+ import { createNodeServer, NodeWsAdapter } from "@open-nest/node";
94
124
 
95
125
  await createNodeServer({
96
126
  module: AppModule,
@@ -102,30 +132,29 @@ await createNodeServer({
102
132
  });
103
133
  ```
104
134
 
105
- `createNodeServer` from `open-nest/node` initializes the Nest application and starts and returns a `http.Server` instance. HTTP uses `node:http`, `node:stream`, and `node:fs` (`openAsBlob`).
106
-
107
- Gateway upgrades go through [`ws`](https://github.com/websockets/ws). The WebSocket client is the global `WebSocket` from Node 22 onward.
108
-
109
- - `createNodeServer` initializes the Nest application once and starts and returns a `http.Server` instance.
110
- - `NodeWsAdapter` is the Nest WebSocket adapter. Upgrades go through `ws`.
111
- - `NodeAdapter` extends `FetchAdapter` and implements `useStaticAssets` with `node:fs`.
135
+ `createNodeServer` starts an `http.Server` and returns it. Gateway upgrades go through `ws`. The WebSocket client is the global `WebSocket` from Node 22 onward. Static files are served with `node:fs`.
112
136
 
113
137
  ### Workers
114
138
 
115
139
  ```ts
116
- import "reflect-metadata";
117
-
118
140
  import {
119
141
  createWorkerFetcher,
120
142
  workerContext,
121
143
  type WorkerExecutionContext,
122
- } from "open-nest/workers";
144
+ } from "@open-nest/workers";
123
145
 
124
146
  interface Env {
125
147
  APP_NAME: string;
126
148
  CACHE: KVNamespace;
127
149
  }
128
150
 
151
+ const fetcher = createWorkerFetcher<Env>({ module: AppModule });
152
+
153
+ export default {
154
+ fetch: (request: Request, env: Env, ctx: WorkerExecutionContext) =>
155
+ fetcher(request, env, ctx),
156
+ };
157
+
129
158
  @Controller()
130
159
  class AppController {
131
160
  @Get()
@@ -138,55 +167,48 @@ class AppController {
138
167
  return { data: { app: env.APP_NAME, lastOpenedAt }, error: null };
139
168
  }
140
169
  }
141
-
142
- @Module({ controllers: [AppController] })
143
- class AppModule {}
144
-
145
- const fetcher = createWorkerFetcher<Env>({ module: AppModule });
146
-
147
- export default {
148
- fetch: (request: Request, env: Env, ctx: WorkerExecutionContext) =>
149
- fetcher(request, env, ctx),
150
- };
151
170
  ```
152
171
 
153
- `createWorkerFetcher` from `open-nest/workers` initializes the Nest application and returns a handler of `(request, env, ctx)`.
172
+ `createWorkerFetcher` returns a handler of `(request, env, ctx)`. `workerContext()` reads those bindings for the request in flight. The Worker needs the `nodejs_compat` compatibility flag.
154
173
 
155
- Pass bindings and the execution context through, then read them with `workerContext()` from a controller or provider. The Worker needs the `nodejs_compat` compatibility flag.
174
+ ## Examples
156
175
 
157
- - `createWorkerFetcher` initializes the Nest application once and returns a handler of `(request, env, ctx)`.
158
- - `workerContext()` reads the bindings and the raw execution context for the request in flight.
159
- - `WorkerExecutionContext` is the `ctx` argument: `waitUntil`, `passThroughOnException`, and `props`.
176
+ - [`examples/bun`](/examples/bun)
177
+ <sup>[readme](/examples/bun/readme.md) ⋅ [code](/examples/bun/)</sup>
178
+ Nest on `Bun.serve`. `GET /health` returns `{ data: null, error: null }`.
160
179
 
161
- ### App Configuration
180
+ - [`examples/bun-websockets`](/examples/bun-websockets)
181
+ <sup>[readme](/examples/bun-websockets/readme.md) ⋅ [code](/examples/bun-websockets/)</sup>
182
+ A gateway at `/ws` that echoes `events`.
162
183
 
163
- ```ts
164
- const fetcher = createFetcher({
165
- module: AppModule,
166
- configure: (app) => {
167
- app.enableCors();
168
- app.setGlobalPrefix("api");
169
- },
170
- });
171
- ```
184
+ - [`examples/node`](/examples/node)
185
+ <sup>[readme](/examples/node/readme.md) ⋅ [code](/examples/node/)</sup>
186
+ Nest on `http.Server`. `GET /` returns `{ data: null, error: null }`.
172
187
 
173
- `configure` runs after the app is created and before `init`. Use it for pipes, a global prefix, CORS, and the other app methods.
188
+ - [`examples/node-websockets`](/examples/node-websockets)
189
+ <sup>[readme](/examples/node-websockets/readme.md) ⋅ [code](/examples/node-websockets/)</sup>
190
+ The same gateway, upgraded through `ws`.
174
191
 
175
- ### App Instance
192
+ - [`examples/workers`](/examples/workers)
193
+ <sup>[readme](/examples/workers/readme.md) ⋅ [code](/examples/workers/)</sup>
194
+ A Worker with an `APP_NAME` variable and a `CACHE` KV binding.
176
195
 
177
- ```ts
178
- const app = await fetcher.app();
179
- ```
196
+ From the repository root, `bun run dev` watches the packages and starts every example. Bun listens on port 4001, Bun websockets on 4002, Node on 4003, Node websockets on 4004, and Workers on 8787.
180
197
 
181
- `app()` resolves the underlying Nest application.
198
+ ## Packages
182
199
 
183
- #### Nest Options
200
+ - [`open-nest`](/packages/open-nest)
201
+ <sup>[readme](/packages/open-nest/readme.md)</sup>
202
+ The HTTP handler: `createFetcher`, `FetchAdapter`, and multipart interceptors.
184
203
 
185
- ```ts
186
- const fetcher = createFetcher({
187
- module: AppModule,
188
- opts: { logger: ["error", "warn"] },
189
- });
190
- ```
204
+ - [`@open-nest/bun`](/packages/@open-nest/bun)
205
+ <sup>[readme](/packages/@open-nest/bun/readme.md) ⋅ [examples](/examples/bun)</sup>
206
+ `Bun.serve`, WebSocket upgrades, and static files.
207
+
208
+ - [`@open-nest/node`](/packages/@open-nest/node)
209
+ <sup>[readme](/packages/@open-nest/node/readme.md) ⋅ [examples](/examples/node)</sup>
210
+ `node:http`, WebSocket upgrades through `ws`, and static files.
191
211
 
192
- `opts` is forwarded to `NestFactory.create` as `NestApplicationOptions`. When omitted, Nest keeps its own defaults.
212
+ - [`@open-nest/workers`](/packages/@open-nest/workers)
213
+ <sup>[readme](/packages/@open-nest/workers/readme.md) ⋅ [examples](/examples/workers)</sup>
214
+ Worker `fetch`, plus `env` and `ctx` for the request in flight.
package/dist/bun.d.mts DELETED
@@ -1,48 +0,0 @@
1
- import { i as Fetcher, r as FetchAdapter } from "./index-DCLeOkkF.mjs";
2
- import { n as WsGateway, t as GatewayClient } from "./gateway-CrNeg78D.mjs";
3
- import { INestApplication, NestApplicationOptions, Type } from "@nestjs/common";
4
- //#region src/adapters/bun/index.d.ts
5
- /**
6
- * Records Nest gateways during `init`. `Bun.serve` performs the upgrade.
7
- */
8
- export declare class BunWsAdapter extends WsGateway<GatewayClient> {
9
- constructor();
10
- dispose(): Promise<void>;
11
- }
12
- /**
13
- * Fetch adapter plus static files. Workers stay on {@link FetchAdapter}.
14
- */
15
- export declare class BunAdapter extends FetchAdapter {
16
- useStaticAssets(root?: string, options?: {
17
- prefix?: string;
18
- index?: string | false;
19
- }): this;
20
- }
21
- export interface CreateBunServerOptions {
22
- /**
23
- * Root Nest module.
24
- */
25
- module: Type<unknown>;
26
- /**
27
- * Port passed to `Bun.serve`. `0` asks the OS for a free port.
28
- */
29
- port?: number;
30
- /**
31
- * Runs after the app is created and before init.
32
- */
33
- configure?: (app: INestApplication) => void | Promise<void>;
34
- /**
35
- * Forwarded to `NestFactory.create`.
36
- */
37
- opts?: NestApplicationOptions;
38
- }
39
- export interface BunServer {
40
- port: number;
41
- fetch: Fetcher;
42
- close(): Promise<void>;
43
- }
44
- /**
45
- * Boots a Nest app and serves it with `Bun.serve`, including WebSocket upgrades.
46
- */
47
- export declare function createBunServer({ module, port, configure, opts }: CreateBunServerOptions): Promise<BunServer>;
48
- //#endregion
package/dist/bun.mjs DELETED
@@ -1,100 +0,0 @@
1
- import { n as createFetcher, t as FetchAdapter } from "./fetch-Bmr9TEe3.mjs";
2
- import { n as gatewayPath, t as WsGateway } from "./gateway-D7PGV2DW.mjs";
3
- import { resolve, sep } from "node:path";
4
- //#region src/adapters/bun/index.ts
5
- const adapters = [];
6
- /**
7
- * Records Nest gateways during `init`. `Bun.serve` performs the upgrade.
8
- */
9
- var BunWsAdapter = class extends WsGateway {
10
- constructor() {
11
- super();
12
- adapters.push(this);
13
- }
14
- async dispose() {
15
- const index = adapters.indexOf(this);
16
- if (index !== -1) adapters.splice(index, 1);
17
- }
18
- };
19
- /**
20
- * Fetch adapter plus static files. Workers stay on {@link FetchAdapter}.
21
- */
22
- var BunAdapter = class extends FetchAdapter {
23
- useStaticAssets(root, options) {
24
- if (!root) return this;
25
- const prefix = options?.prefix ?? "/";
26
- const index = options?.index === void 0 ? "index.html" : options.index;
27
- const rootResolved = resolve(root);
28
- this.router.use(prefix, async (req, res, next) => {
29
- if (req.method !== "GET" && req.method !== "HEAD") return next();
30
- let relative;
31
- try {
32
- relative = decodeURIComponent(req.path);
33
- } catch {
34
- return next();
35
- }
36
- if (relative === "/") {
37
- if (index === false) return next();
38
- relative = index;
39
- } else relative = relative.replace(/^\//, "");
40
- const target = resolve(rootResolved, relative);
41
- if (target !== rootResolved && !target.startsWith(rootResolved + sep)) return next();
42
- const file = Bun.file(target);
43
- if (!await file.exists()) return next();
44
- res.setHeader("content-type", file.type || "application/octet-stream");
45
- res.setHeader("content-length", String(file.size));
46
- if (req.method === "HEAD") {
47
- res.end();
48
- return;
49
- }
50
- res.end(new Uint8Array(await file.arrayBuffer()));
51
- });
52
- return this;
53
- }
54
- };
55
- /**
56
- * Boots a Nest app and serves it with `Bun.serve`, including WebSocket upgrades.
57
- */
58
- async function createBunServer({ module, port, configure, opts }) {
59
- const fallback = new BunWsAdapter();
60
- const seen = new Set(adapters);
61
- const fetcher = createFetcher({
62
- module,
63
- opts,
64
- configure: async (app) => {
65
- app.useWebSocketAdapter(fallback);
66
- await configure?.(app);
67
- }
68
- }, new BunAdapter());
69
- await fetcher.app();
70
- const active = [...adapters].reverse().find((adapter) => !seen.has(adapter) && adapter.hasGateways()) ?? fallback;
71
- const server = Bun.serve({
72
- port: port ?? 3e3,
73
- fetch: (request, bun) => {
74
- const pathname = new URL(request.url).pathname;
75
- if (active.matches(pathname) && bun.upgrade(request, { data: { path: gatewayPath(pathname) } })) return;
76
- return fetcher(request);
77
- },
78
- websocket: {
79
- open: (client) => {
80
- active.open(client);
81
- },
82
- message: (client, message) => {
83
- active.receive(client, message);
84
- },
85
- close: (client) => {
86
- active.closed(client);
87
- }
88
- }
89
- });
90
- return {
91
- port: server.port ?? port ?? 3e3,
92
- fetch: fetcher,
93
- close: async () => {
94
- await server.stop(true);
95
- await fetcher.close();
96
- }
97
- };
98
- }
99
- //#endregion
100
- export { BunAdapter, BunWsAdapter, createBunServer };
package/dist/fetch.d.mts DELETED
@@ -1,2 +0,0 @@
1
- import { a as createFetcher, c as FilesInterceptor, i as Fetcher, l as MultipartLimits, n as CreateHandlerOptions, o as AnyFilesInterceptor, r as FetchAdapter, s as FileInterceptor, t as CorsOptions, u as MultipartOptions } from "./index-DCLeOkkF.mjs";
2
- export { AnyFilesInterceptor, CorsOptions, CreateHandlerOptions, FetchAdapter, Fetcher, FileInterceptor, FilesInterceptor, type MultipartLimits, type MultipartOptions, createFetcher };
package/dist/fetch.mjs DELETED
@@ -1,2 +0,0 @@
1
- import { a as FilesInterceptor, i as FileInterceptor, n as createFetcher, r as AnyFilesInterceptor, t as FetchAdapter } from "./fetch-Bmr9TEe3.mjs";
2
- export { AnyFilesInterceptor, FetchAdapter, FileInterceptor, FilesInterceptor, createFetcher };
@@ -1,45 +0,0 @@
1
- //#region src/lib/gateway.d.ts
2
- interface GatewayClient {
3
- send(data: string): void;
4
- data: {
5
- path: string;
6
- };
7
- }
8
- interface Gateway {
9
- path: string;
10
- connect?: (client: GatewayClient) => void;
11
- }
12
- interface MessageHandler {
13
- message: string;
14
- callback: (data: unknown) => unknown;
15
- }
16
- interface Subscription {
17
- unsubscribe(): void;
18
- }
19
- interface ResultStream {
20
- subscribe(observer: {
21
- next: (value: unknown) => void;
22
- error?: (error: unknown) => void;
23
- }): Subscription;
24
- }
25
- /**
26
- * Records Nest gateways during `init`. The runtime server performs the upgrade.
27
- */
28
- declare class WsGateway<T extends GatewayClient = GatewayClient> {
29
- private readonly gateways;
30
- private readonly clients;
31
- create(_port: number, options?: {
32
- path?: string;
33
- }): Gateway;
34
- bindClientConnect(server: Gateway, callback: (client: T) => void): void;
35
- bindMessageHandlers(client: T, handlers: MessageHandler[], transform: (result: unknown) => ResultStream): void;
36
- bindClientDisconnect(client: T, callback: () => void): void;
37
- close(): void;
38
- matches(pathname: string): boolean;
39
- hasGateways(): boolean;
40
- open(client: T): void;
41
- receive(client: T, raw: string | Uint8Array): void;
42
- closed(client: T): void;
43
- }
44
- //#endregion
45
- export { WsGateway as n, GatewayClient as t };
@@ -1,89 +0,0 @@
1
- //#region src/lib/gateway.ts
2
- function gatewayPath(path) {
3
- if (!path.startsWith("/")) path = `/${path}`;
4
- if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
5
- return path;
6
- }
7
- /**
8
- * Records Nest gateways during `init`. The runtime server performs the upgrade.
9
- */
10
- var WsGateway = class {
11
- gateways = [];
12
- clients = /* @__PURE__ */ new WeakMap();
13
- create(_port, options) {
14
- const gateway = { path: gatewayPath(options?.path ?? "/") };
15
- this.gateways.push(gateway);
16
- return gateway;
17
- }
18
- bindClientConnect(server, callback) {
19
- server.connect = callback;
20
- }
21
- bindMessageHandlers(client, handlers, transform) {
22
- const prev = this.clients.get(client);
23
- this.clients.set(client, {
24
- handlers,
25
- transform,
26
- subscriptions: prev?.subscriptions ?? [],
27
- onClose: prev?.onClose
28
- });
29
- }
30
- bindClientDisconnect(client, callback) {
31
- const state = this.clients.get(client);
32
- if (!state) return;
33
- state.onClose = callback;
34
- }
35
- close() {}
36
- matches(pathname) {
37
- const path = gatewayPath(pathname);
38
- return this.gateways.some((gateway) => gateway.path === path);
39
- }
40
- hasGateways() {
41
- return this.gateways.length > 0;
42
- }
43
- open(client) {
44
- this.gateways.find((item) => item.path === client.data.path)?.connect?.(client);
45
- }
46
- receive(client, raw) {
47
- const state = this.clients.get(client);
48
- if (!state) return;
49
- let event;
50
- let data;
51
- try {
52
- const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
53
- const parsed = JSON.parse(text);
54
- if (typeof parsed.event !== "string") return;
55
- event = parsed.event;
56
- data = parsed.data;
57
- } catch {
58
- return;
59
- }
60
- const handler = state.handlers.find((item) => item.message === event);
61
- if (!handler) return;
62
- let stream;
63
- try {
64
- stream = state.transform(handler.callback(data));
65
- } catch {
66
- return;
67
- }
68
- const subscription = stream.subscribe({
69
- next: (value) => {
70
- if (value === void 0) return;
71
- client.send(JSON.stringify({
72
- event,
73
- data: value
74
- }));
75
- },
76
- error: () => {}
77
- });
78
- state.subscriptions.push(subscription);
79
- }
80
- closed(client) {
81
- const state = this.clients.get(client);
82
- if (!state) return;
83
- for (const subscription of state.subscriptions) subscription.unsubscribe();
84
- state.onClose?.();
85
- this.clients.delete(client);
86
- }
87
- };
88
- //#endregion
89
- export { gatewayPath as n, WsGateway as t };
package/dist/node.d.mts DELETED
@@ -1,53 +0,0 @@
1
- import { i as Fetcher, r as FetchAdapter } from "./index-DCLeOkkF.mjs";
2
- import { n as WsGateway, t as GatewayClient } from "./gateway-CrNeg78D.mjs";
3
- import { INestApplication, NestApplicationOptions, Type } from "@nestjs/common";
4
- //#region src/adapters/node/index.d.ts
5
- interface NodeSocket extends GatewayClient {
6
- data: {
7
- path: string;
8
- };
9
- }
10
- /**
11
- * Records Nest gateways during `init`. `node:http` performs the upgrade.
12
- */
13
- export declare class NodeWsAdapter extends WsGateway<NodeSocket> {
14
- constructor();
15
- dispose(): Promise<void>;
16
- }
17
- /**
18
- * Fetch adapter plus static files via `node:fs`.
19
- */
20
- export declare class NodeAdapter extends FetchAdapter {
21
- useStaticAssets(root?: string, options?: {
22
- prefix?: string;
23
- index?: string | false;
24
- }): this;
25
- }
26
- export interface CreateNodeServerOptions {
27
- /**
28
- * Root Nest module.
29
- */
30
- module: Type<unknown>;
31
- /**
32
- * Port passed to `node:http`. `0` asks the OS for a free port.
33
- */
34
- port?: number;
35
- /**
36
- * Runs after the app is created and before init.
37
- */
38
- configure?: (app: INestApplication) => void | Promise<void>;
39
- /**
40
- * Forwarded to `NestFactory.create`.
41
- */
42
- opts?: NestApplicationOptions;
43
- }
44
- export interface NodeServer {
45
- port: number;
46
- fetch: Fetcher;
47
- close(): Promise<void>;
48
- }
49
- /**
50
- * Boots a Nest app and serves it with `node:http`, including WebSocket upgrades.
51
- */
52
- export declare function createNodeServer({ module, port, configure, opts }: CreateNodeServerOptions): Promise<NodeServer>;
53
- //#endregion
package/dist/node.mjs DELETED
@@ -1,190 +0,0 @@
1
- import { n as createFetcher, t as FetchAdapter } from "./fetch-Bmr9TEe3.mjs";
2
- import { n as gatewayPath, t as WsGateway } from "./gateway-D7PGV2DW.mjs";
3
- import { Readable } from "node:stream";
4
- import { extname, resolve, sep } from "node:path";
5
- import { openAsBlob } from "node:fs";
6
- import { stat } from "node:fs/promises";
7
- import { createServer } from "node:http";
8
- import { WebSocketServer } from "ws";
9
- //#region src/adapters/node/index.ts
10
- const adapters = [];
11
- /**
12
- * Records Nest gateways during `init`. `node:http` performs the upgrade.
13
- */
14
- var NodeWsAdapter = class extends WsGateway {
15
- constructor() {
16
- super();
17
- adapters.push(this);
18
- }
19
- async dispose() {
20
- const index = adapters.indexOf(this);
21
- if (index !== -1) adapters.splice(index, 1);
22
- }
23
- };
24
- /**
25
- * Fetch adapter plus static files via `node:fs`.
26
- */
27
- var NodeAdapter = class extends FetchAdapter {
28
- useStaticAssets(root, options) {
29
- if (!root) return this;
30
- const prefix = options?.prefix ?? "/";
31
- const index = options?.index === void 0 ? "index.html" : options.index;
32
- const rootResolved = resolve(root);
33
- this.router.use(prefix, async (req, res, next) => {
34
- if (req.method !== "GET" && req.method !== "HEAD") return next();
35
- let relative;
36
- try {
37
- relative = decodeURIComponent(req.path);
38
- } catch {
39
- return next();
40
- }
41
- if (relative === "/") {
42
- if (index === false) return next();
43
- relative = index;
44
- } else relative = relative.replace(/^\//, "");
45
- const target = resolve(rootResolved, relative);
46
- if (target !== rootResolved && !target.startsWith(rootResolved + sep)) return next();
47
- let size;
48
- try {
49
- const info = await stat(target);
50
- if (!info.isFile()) return next();
51
- size = info.size;
52
- } catch {
53
- return next();
54
- }
55
- const type = contentType(target);
56
- const blob = await openAsBlob(target);
57
- res.setHeader("content-type", type);
58
- res.setHeader("content-length", String(size));
59
- if (req.method === "HEAD") {
60
- res.end();
61
- return;
62
- }
63
- const reader = blob.stream().getReader();
64
- for (;;) {
65
- const { done, value } = await reader.read();
66
- if (done) break;
67
- res.write(value);
68
- }
69
- res.end();
70
- });
71
- return this;
72
- }
73
- };
74
- function contentType(file) {
75
- switch (extname(file).toLowerCase()) {
76
- case ".html": return "text/html; charset=utf-8";
77
- case ".txt": return "text/plain; charset=utf-8";
78
- case ".json": return "application/json";
79
- case ".css": return "text/css; charset=utf-8";
80
- case ".js": return "text/javascript; charset=utf-8";
81
- case ".svg": return "image/svg+xml";
82
- case ".png": return "image/png";
83
- case ".jpg":
84
- case ".jpeg": return "image/jpeg";
85
- case ".webp": return "image/webp";
86
- case ".pdf": return "application/pdf";
87
- case ".wasm": return "application/wasm";
88
- default: return "application/octet-stream";
89
- }
90
- }
91
- /**
92
- * Boots a Nest app and serves it with `node:http`, including WebSocket upgrades.
93
- */
94
- async function createNodeServer({ module, port, configure, opts }) {
95
- const fallback = new NodeWsAdapter();
96
- const seen = new Set(adapters);
97
- const fetcher = createFetcher({
98
- module,
99
- opts,
100
- configure: async (app) => {
101
- app.useWebSocketAdapter(fallback);
102
- await configure?.(app);
103
- }
104
- }, new NodeAdapter());
105
- await fetcher.app();
106
- const active = [...adapters].reverse().find((adapter) => !seen.has(adapter) && adapter.hasGateways()) ?? fallback;
107
- const sockets = new WebSocketServer({ noServer: true });
108
- const server = createServer((req, res) => {
109
- writeFetch(fetcher, req, res);
110
- });
111
- server.on("upgrade", (req, socket, head) => {
112
- const pathname = gatewayPath(new URL(req.url ?? "/", "http://localhost").pathname);
113
- if (!active.matches(pathname)) {
114
- socket.destroy();
115
- return;
116
- }
117
- sockets.handleUpgrade(req, socket, head, (ws) => {
118
- const client = {
119
- data: { path: pathname },
120
- send(data) {
121
- ws.send(data);
122
- }
123
- };
124
- active.open(client);
125
- ws.on("message", (data) => {
126
- const raw = typeof data === "string" ? data : Array.isArray(data) ? Buffer.concat(data) : data instanceof ArrayBuffer ? new Uint8Array(data) : data;
127
- active.receive(client, raw);
128
- });
129
- ws.on("close", () => {
130
- active.closed(client);
131
- });
132
- });
133
- });
134
- await new Promise((resolve) => {
135
- server.listen(port ?? 3e3, "127.0.0.1", () => resolve());
136
- });
137
- const address = server.address();
138
- return {
139
- port: typeof address === "object" && address ? address.port : port ?? 3e3,
140
- fetch: fetcher,
141
- close: async () => {
142
- for (const client of sockets.clients) client.terminate();
143
- await new Promise((resolve, reject) => {
144
- sockets.close((error) => error ? reject(error) : resolve());
145
- });
146
- server.closeAllConnections();
147
- await new Promise((resolve, reject) => {
148
- server.close((error) => error ? reject(error) : resolve());
149
- });
150
- await fetcher.close();
151
- }
152
- };
153
- }
154
- async function writeFetch(fetcher, req, res) {
155
- try {
156
- const response = await fetcher(toRequest(req));
157
- const headers = Object.fromEntries(response.headers.entries());
158
- res.writeHead(response.status, headers);
159
- if (!response.body) {
160
- res.end();
161
- return;
162
- }
163
- await new Promise((resolve, reject) => {
164
- Readable.fromWeb(response.body).pipe(res).on("finish", () => resolve()).on("error", reject);
165
- });
166
- } catch {
167
- if (!res.headersSent) res.writeHead(500);
168
- res.end();
169
- }
170
- }
171
- function toRequest(req) {
172
- const host = req.headers.host ?? "127.0.0.1";
173
- const url = new URL(req.url ?? "/", `http://${host}`);
174
- const headers = new Headers();
175
- for (const [name, value] of Object.entries(req.headers)) {
176
- if (value == null) continue;
177
- if (Array.isArray(value)) for (const item of value) headers.append(name, item);
178
- else headers.set(name, value);
179
- }
180
- const method = req.method ?? "GET";
181
- const bodyless = method === "GET" || method === "HEAD";
182
- return new Request(url.href, {
183
- method,
184
- headers,
185
- body: bodyless ? void 0 : Readable.toWeb(req),
186
- duplex: "half"
187
- });
188
- }
189
- //#endregion
190
- export { NodeAdapter, NodeWsAdapter, createNodeServer };
@@ -1,38 +0,0 @@
1
- import { i as Fetcher, n as CreateHandlerOptions } from "./index-DCLeOkkF.mjs";
2
- //#region src/adapters/workers/index.d.ts
3
- /**
4
- * The `ctx` argument from a Worker `fetch` handler.
5
- */
6
- export interface WorkerExecutionContext {
7
- waitUntil(promise: Promise<unknown>): void;
8
- passThroughOnException(): void;
9
- readonly props?: unknown;
10
- }
11
- /**
12
- * Bindings and the raw execution context for the request in flight.
13
- */
14
- export interface WorkerContext<Env> {
15
- env: Env;
16
- ctx: WorkerExecutionContext;
17
- }
18
- /**
19
- * Bindings and execution context for the current Worker request.
20
- */
21
- export declare function workerContext<Env>(): WorkerContext<Env>;
22
- export interface WorkerFetcher<Env> {
23
- (request: Request, env: Env, ctx: WorkerExecutionContext): Promise<Response>;
24
- /**
25
- * Resolves the underlying Nest app instance.
26
- */
27
- app: Fetcher["app"];
28
- /**
29
- * Closes the underlying Nest app.
30
- */
31
- close: Fetcher["close"];
32
- }
33
- /**
34
- * `createFetcher` whose handler also receives Worker bindings and `ctx`.
35
- * `workerContext()` reads them for the duration of the request.
36
- */
37
- export declare function createWorkerFetcher<Env>(options: CreateHandlerOptions): WorkerFetcher<Env>;
38
- //#endregion
package/dist/workers.mjs DELETED
@@ -1,28 +0,0 @@
1
- import { n as createFetcher } from "./fetch-Bmr9TEe3.mjs";
2
- import { AsyncLocalStorage } from "node:async_hooks";
3
- //#region src/adapters/workers/index.ts
4
- const storage = new AsyncLocalStorage();
5
- /**
6
- * Bindings and execution context for the current Worker request.
7
- */
8
- function workerContext() {
9
- const context = storage.getStore();
10
- if (!context) throw new Error("Called outside a worker request.");
11
- return context;
12
- }
13
- /**
14
- * `createFetcher` whose handler also receives Worker bindings and `ctx`.
15
- * `workerContext()` reads them for the duration of the request.
16
- */
17
- function createWorkerFetcher(options) {
18
- const fetcher = createFetcher(options);
19
- const handler = (async (request, env, ctx) => storage.run({
20
- env,
21
- ctx
22
- }, () => fetcher(request)));
23
- handler.app = () => fetcher.app();
24
- handler.close = () => fetcher.close();
25
- return handler;
26
- }
27
- //#endregion
28
- export { createWorkerFetcher, workerContext };