@open-nest/node 0.0.0-stage → 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.
- package/dist/index.d.mts +52 -0
- package/dist/index.mjs +189 -0
- package/package.json +41 -4
- package/readme.md +12 -0
- package/README.md +0 -3
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FetchAdapter, Fetcher, GatewayClient, WsGateway } from "open-nest";
|
|
2
|
+
import { INestApplication, NestApplicationOptions, Type } from "@nestjs/common";
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
interface NodeSocket extends GatewayClient {
|
|
5
|
+
data: {
|
|
6
|
+
path: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Records Nest gateways during `init`. `node:http` performs the upgrade.
|
|
11
|
+
*/
|
|
12
|
+
export declare class NodeWsAdapter extends WsGateway<NodeSocket> {
|
|
13
|
+
constructor();
|
|
14
|
+
dispose(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Fetch adapter plus static files via `node:fs`.
|
|
18
|
+
*/
|
|
19
|
+
export declare class NodeAdapter extends FetchAdapter {
|
|
20
|
+
useStaticAssets(root?: string, options?: {
|
|
21
|
+
prefix?: string;
|
|
22
|
+
index?: string | false;
|
|
23
|
+
}): this;
|
|
24
|
+
}
|
|
25
|
+
export interface CreateNodeServerOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Root Nest module.
|
|
28
|
+
*/
|
|
29
|
+
module: Type<unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Port passed to `node:http`. `0` asks the OS for a free port.
|
|
32
|
+
*/
|
|
33
|
+
port?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Runs after the app is created and before init.
|
|
36
|
+
*/
|
|
37
|
+
configure?: (app: INestApplication) => void | Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Forwarded to `NestFactory.create`.
|
|
40
|
+
*/
|
|
41
|
+
opts?: NestApplicationOptions;
|
|
42
|
+
}
|
|
43
|
+
export interface NodeServer {
|
|
44
|
+
port: number;
|
|
45
|
+
fetch: Fetcher;
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Boots a Nest app and serves it with `node:http`, including WebSocket upgrades.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createNodeServer({ module, port, configure, opts }: CreateNodeServerOptions): Promise<NodeServer>;
|
|
52
|
+
//#endregion
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { openAsBlob } from "node:fs";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { extname, resolve, sep } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { FetchAdapter, WsGateway, createFetcher, gatewayPath } from "open-nest";
|
|
7
|
+
import { WebSocketServer } from "ws";
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
const adapters = [];
|
|
10
|
+
/**
|
|
11
|
+
* Records Nest gateways during `init`. `node:http` performs the upgrade.
|
|
12
|
+
*/
|
|
13
|
+
var NodeWsAdapter = class extends WsGateway {
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
adapters.push(this);
|
|
17
|
+
}
|
|
18
|
+
async dispose() {
|
|
19
|
+
const index = adapters.indexOf(this);
|
|
20
|
+
if (index !== -1) adapters.splice(index, 1);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Fetch adapter plus static files via `node:fs`.
|
|
25
|
+
*/
|
|
26
|
+
var NodeAdapter = class extends FetchAdapter {
|
|
27
|
+
useStaticAssets(root, options) {
|
|
28
|
+
if (!root) return this;
|
|
29
|
+
const prefix = options?.prefix ?? "/";
|
|
30
|
+
const index = options?.index === void 0 ? "index.html" : options.index;
|
|
31
|
+
const rootResolved = resolve(root);
|
|
32
|
+
this.router.use(prefix, async (req, res, next) => {
|
|
33
|
+
if (req.method !== "GET" && req.method !== "HEAD") return next();
|
|
34
|
+
let relative;
|
|
35
|
+
try {
|
|
36
|
+
relative = decodeURIComponent(req.path);
|
|
37
|
+
} catch {
|
|
38
|
+
return next();
|
|
39
|
+
}
|
|
40
|
+
if (relative === "/") {
|
|
41
|
+
if (index === false) return next();
|
|
42
|
+
relative = index;
|
|
43
|
+
} else relative = relative.replace(/^\//, "");
|
|
44
|
+
const target = resolve(rootResolved, relative);
|
|
45
|
+
if (target !== rootResolved && !target.startsWith(rootResolved + sep)) return next();
|
|
46
|
+
let size;
|
|
47
|
+
try {
|
|
48
|
+
const info = await stat(target);
|
|
49
|
+
if (!info.isFile()) return next();
|
|
50
|
+
size = info.size;
|
|
51
|
+
} catch {
|
|
52
|
+
return next();
|
|
53
|
+
}
|
|
54
|
+
const type = contentType(target);
|
|
55
|
+
const blob = await openAsBlob(target);
|
|
56
|
+
res.setHeader("content-type", type);
|
|
57
|
+
res.setHeader("content-length", String(size));
|
|
58
|
+
if (req.method === "HEAD") {
|
|
59
|
+
res.end();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const reader = blob.stream().getReader();
|
|
63
|
+
for (;;) {
|
|
64
|
+
const { done, value } = await reader.read();
|
|
65
|
+
if (done) break;
|
|
66
|
+
res.write(value);
|
|
67
|
+
}
|
|
68
|
+
res.end();
|
|
69
|
+
});
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function contentType(file) {
|
|
74
|
+
switch (extname(file).toLowerCase()) {
|
|
75
|
+
case ".html": return "text/html; charset=utf-8";
|
|
76
|
+
case ".txt": return "text/plain; charset=utf-8";
|
|
77
|
+
case ".json": return "application/json";
|
|
78
|
+
case ".css": return "text/css; charset=utf-8";
|
|
79
|
+
case ".js": return "text/javascript; charset=utf-8";
|
|
80
|
+
case ".svg": return "image/svg+xml";
|
|
81
|
+
case ".png": return "image/png";
|
|
82
|
+
case ".jpg":
|
|
83
|
+
case ".jpeg": return "image/jpeg";
|
|
84
|
+
case ".webp": return "image/webp";
|
|
85
|
+
case ".pdf": return "application/pdf";
|
|
86
|
+
case ".wasm": return "application/wasm";
|
|
87
|
+
default: return "application/octet-stream";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Boots a Nest app and serves it with `node:http`, including WebSocket upgrades.
|
|
92
|
+
*/
|
|
93
|
+
async function createNodeServer({ module, port, configure, opts }) {
|
|
94
|
+
const fallback = new NodeWsAdapter();
|
|
95
|
+
const seen = new Set(adapters);
|
|
96
|
+
const fetcher = createFetcher({
|
|
97
|
+
module,
|
|
98
|
+
opts,
|
|
99
|
+
configure: async (app) => {
|
|
100
|
+
app.useWebSocketAdapter(fallback);
|
|
101
|
+
await configure?.(app);
|
|
102
|
+
}
|
|
103
|
+
}, new NodeAdapter());
|
|
104
|
+
await fetcher.app();
|
|
105
|
+
const active = [...adapters].reverse().find((adapter) => !seen.has(adapter) && adapter.hasGateways()) ?? fallback;
|
|
106
|
+
const sockets = new WebSocketServer({ noServer: true });
|
|
107
|
+
const server = createServer((req, res) => {
|
|
108
|
+
writeFetch(fetcher, req, res);
|
|
109
|
+
});
|
|
110
|
+
server.on("upgrade", (req, socket, head) => {
|
|
111
|
+
const pathname = gatewayPath(new URL(req.url ?? "/", "http://localhost").pathname);
|
|
112
|
+
if (!active.matches(pathname)) {
|
|
113
|
+
socket.destroy();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
sockets.handleUpgrade(req, socket, head, (ws) => {
|
|
117
|
+
const client = {
|
|
118
|
+
data: { path: pathname },
|
|
119
|
+
send(data) {
|
|
120
|
+
ws.send(data);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
active.open(client);
|
|
124
|
+
ws.on("message", (data) => {
|
|
125
|
+
const raw = typeof data === "string" ? data : Array.isArray(data) ? Buffer.concat(data) : data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
126
|
+
active.receive(client, raw);
|
|
127
|
+
});
|
|
128
|
+
ws.on("close", () => {
|
|
129
|
+
active.closed(client);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
await new Promise((resolve) => {
|
|
134
|
+
server.listen(port ?? 3e3, "127.0.0.1", () => resolve());
|
|
135
|
+
});
|
|
136
|
+
const address = server.address();
|
|
137
|
+
return {
|
|
138
|
+
port: typeof address === "object" && address ? address.port : port ?? 3e3,
|
|
139
|
+
fetch: fetcher,
|
|
140
|
+
close: async () => {
|
|
141
|
+
for (const client of sockets.clients) client.terminate();
|
|
142
|
+
await new Promise((resolve, reject) => {
|
|
143
|
+
sockets.close((error) => error ? reject(error) : resolve());
|
|
144
|
+
});
|
|
145
|
+
server.closeAllConnections();
|
|
146
|
+
await new Promise((resolve, reject) => {
|
|
147
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
148
|
+
});
|
|
149
|
+
await fetcher.close();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function writeFetch(fetcher, req, res) {
|
|
154
|
+
try {
|
|
155
|
+
const response = await fetcher(toRequest(req));
|
|
156
|
+
const headers = Object.fromEntries(response.headers.entries());
|
|
157
|
+
res.writeHead(response.status, headers);
|
|
158
|
+
if (!response.body) {
|
|
159
|
+
res.end();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
Readable.fromWeb(response.body).pipe(res).on("finish", () => resolve()).on("error", reject);
|
|
164
|
+
});
|
|
165
|
+
} catch {
|
|
166
|
+
if (!res.headersSent) res.writeHead(500);
|
|
167
|
+
res.end();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function toRequest(req) {
|
|
171
|
+
const host = req.headers.host ?? "127.0.0.1";
|
|
172
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
173
|
+
const headers = new Headers();
|
|
174
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
175
|
+
if (value == null) continue;
|
|
176
|
+
if (Array.isArray(value)) for (const item of value) headers.append(name, item);
|
|
177
|
+
else headers.set(name, value);
|
|
178
|
+
}
|
|
179
|
+
const method = req.method ?? "GET";
|
|
180
|
+
const bodyless = method === "GET" || method === "HEAD";
|
|
181
|
+
return new Request(url.href, {
|
|
182
|
+
method,
|
|
183
|
+
headers,
|
|
184
|
+
body: bodyless ? void 0 : Readable.toWeb(req),
|
|
185
|
+
duplex: "half"
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
export { NodeAdapter, NodeWsAdapter, createNodeServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-nest/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run an open-nest application with node:http.",
|
|
5
|
+
"files": [
|
|
6
|
+
"package.json",
|
|
7
|
+
"readme.md",
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"bun": "./src/index.ts",
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"import": "./dist/index.mjs"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsdown --clean",
|
|
22
|
+
"dev": "tsdown --watch",
|
|
23
|
+
"typecheck": "bun check",
|
|
24
|
+
"test": "bun test"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"open-nest": "0.1.0",
|
|
28
|
+
"ws": "8.22.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@nestjs/websockets": "12.1.3",
|
|
32
|
+
"@types/ws": "8.18.2"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@nestjs/common": "^12.0.0",
|
|
36
|
+
"@nestjs/websockets": "^12.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@nestjs/websockets": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# `@open-nest/node`
|
|
2
|
+
|
|
3
|
+
Serve an [`open-nest`](https://github.com/juriadams/open-nest) handler with `node:http`.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bun add @open-nest/node
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`createNodeServer` starts an `http.Server`. `NodeWsAdapter` upgrades WebSocket gateways through `ws`. Static files use `node:fs`.
|
|
10
|
+
|
|
11
|
+
- [HTTP](../../../examples/node)
|
|
12
|
+
- [WebSockets](../../../examples/node-websockets)
|
package/README.md
DELETED