@typokit/platform-node 0.1.4
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.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +162 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
- package/src/index.test.ts +192 -0
- package/src/index.ts +225 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse, Server } from "node:http";
|
|
2
|
+
import type { ServerHandle, TypoKitRequest, TypoKitResponse } from "@typokit/types";
|
|
3
|
+
/** Runtime platform metadata for diagnostics and inspect commands */
|
|
4
|
+
export interface PlatformInfo {
|
|
5
|
+
runtime: string;
|
|
6
|
+
version: string;
|
|
7
|
+
}
|
|
8
|
+
/** Returns Node.js platform info */
|
|
9
|
+
export declare function getPlatformInfo(): PlatformInfo;
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a Node.js IncomingMessage into a TypoKitRequest.
|
|
12
|
+
* Body is collected asynchronously from the stream.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeRequest(req: IncomingMessage): Promise<TypoKitRequest>;
|
|
15
|
+
/**
|
|
16
|
+
* Write a TypoKitResponse to a Node.js ServerResponse.
|
|
17
|
+
*/
|
|
18
|
+
export declare function writeResponse(res: ServerResponse, response: TypoKitResponse): void;
|
|
19
|
+
/** Handler function that receives a normalized request and returns a response */
|
|
20
|
+
export type NodeRequestHandler = (req: TypoKitRequest) => Promise<TypoKitResponse>;
|
|
21
|
+
export interface NodeServerOptions {
|
|
22
|
+
/** Optional hostname to bind to (default: "0.0.0.0") */
|
|
23
|
+
hostname?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Result of createServer — provides listen/close and access to the raw node:http server */
|
|
26
|
+
export interface NodeServer {
|
|
27
|
+
/** Start listening on the given port. Returns a handle for graceful shutdown. */
|
|
28
|
+
listen(port: number): Promise<ServerHandle>;
|
|
29
|
+
/** The underlying node:http Server instance */
|
|
30
|
+
server: Server;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a Node.js HTTP server that dispatches to a TypoKit request handler.
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* ```ts
|
|
37
|
+
* const srv = createServer(async (req) => ({
|
|
38
|
+
* status: 200,
|
|
39
|
+
* headers: {},
|
|
40
|
+
* body: { ok: true },
|
|
41
|
+
* }));
|
|
42
|
+
* const handle = await srv.listen(3000);
|
|
43
|
+
* // ... later
|
|
44
|
+
* await handle.close();
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function createServer(handler: NodeRequestHandler, options?: NodeServerOptions): NodeServer;
|
|
48
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEzE,OAAO,KAAK,EAEV,YAAY,EACZ,cAAc,EACd,eAAe,EAChB,MAAM,gBAAgB,CAAC;AAIxB,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,oCAAoC;AACpC,wBAAgB,eAAe,IAAI,YAAY,CAK9C;AA2DD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,eAAe,GACxB,IAAI,CA0BN;AAID,iFAAiF;AACjF,MAAM,MAAM,kBAAkB,GAAG,CAC/B,GAAG,EAAE,cAAc,KAChB,OAAO,CAAC,eAAe,CAAC,CAAC;AAI9B,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAID,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,iFAAiF;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5C,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,iBAAsB,GAC9B,UAAU,CAwCZ"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @typokit/platform-node — Node.js Platform Adapter
|
|
2
|
+
import { createServer as nodeCreateServer } from "node:http";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
/** Returns Node.js platform info */
|
|
5
|
+
export function getPlatformInfo() {
|
|
6
|
+
return {
|
|
7
|
+
runtime: "node",
|
|
8
|
+
version: process.version,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
// ─── Request / Response Helpers ──────────────────────────────
|
|
12
|
+
/** Collect the body of an IncomingMessage into a buffer, then parse as JSON or return raw string */
|
|
13
|
+
function collectBody(req) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
17
|
+
req.on("error", reject);
|
|
18
|
+
req.on("end", () => {
|
|
19
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
20
|
+
if (!raw) {
|
|
21
|
+
resolve(undefined);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
25
|
+
if (contentType.includes("application/json")) {
|
|
26
|
+
try {
|
|
27
|
+
resolve(JSON.parse(raw));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
resolve(raw);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
resolve(raw);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Parse query string from a URL into a Record */
|
|
40
|
+
function parseQuery(searchParams) {
|
|
41
|
+
const result = {};
|
|
42
|
+
for (const [key, value] of searchParams.entries()) {
|
|
43
|
+
const existing = result[key];
|
|
44
|
+
if (existing === undefined) {
|
|
45
|
+
result[key] = value;
|
|
46
|
+
}
|
|
47
|
+
else if (Array.isArray(existing)) {
|
|
48
|
+
existing.push(value);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
result[key] = [existing, value];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/** Normalize Node.js headers into a flat Record */
|
|
57
|
+
function normalizeHeaders(raw) {
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
60
|
+
result[key] = value;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Normalize a Node.js IncomingMessage into a TypoKitRequest.
|
|
66
|
+
* Body is collected asynchronously from the stream.
|
|
67
|
+
*/
|
|
68
|
+
export async function normalizeRequest(req) {
|
|
69
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
70
|
+
const body = await collectBody(req);
|
|
71
|
+
return {
|
|
72
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
73
|
+
path: url.pathname,
|
|
74
|
+
headers: normalizeHeaders(req.headers),
|
|
75
|
+
body,
|
|
76
|
+
query: parseQuery(url.searchParams),
|
|
77
|
+
params: {},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Write a TypoKitResponse to a Node.js ServerResponse.
|
|
82
|
+
*/
|
|
83
|
+
export function writeResponse(res, response) {
|
|
84
|
+
// Set headers
|
|
85
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
86
|
+
if (value !== undefined) {
|
|
87
|
+
res.setHeader(key, value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Determine body content before writing head
|
|
91
|
+
let bodyContent;
|
|
92
|
+
if (response.body === null || response.body === undefined) {
|
|
93
|
+
bodyContent = undefined;
|
|
94
|
+
}
|
|
95
|
+
else if (typeof response.body === "string") {
|
|
96
|
+
bodyContent = response.body;
|
|
97
|
+
}
|
|
98
|
+
else if (Buffer.isBuffer(response.body)) {
|
|
99
|
+
bodyContent = response.body;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// JSON serialize objects — set content-type before writeHead
|
|
103
|
+
if (!res.getHeader("content-type")) {
|
|
104
|
+
res.setHeader("content-type", "application/json");
|
|
105
|
+
}
|
|
106
|
+
bodyContent = JSON.stringify(response.body);
|
|
107
|
+
}
|
|
108
|
+
res.writeHead(response.status);
|
|
109
|
+
res.end(bodyContent);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create a Node.js HTTP server that dispatches to a TypoKit request handler.
|
|
113
|
+
*
|
|
114
|
+
* Usage:
|
|
115
|
+
* ```ts
|
|
116
|
+
* const srv = createServer(async (req) => ({
|
|
117
|
+
* status: 200,
|
|
118
|
+
* headers: {},
|
|
119
|
+
* body: { ok: true },
|
|
120
|
+
* }));
|
|
121
|
+
* const handle = await srv.listen(3000);
|
|
122
|
+
* // ... later
|
|
123
|
+
* await handle.close();
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function createServer(handler, options = {}) {
|
|
127
|
+
const hostname = options.hostname ?? "0.0.0.0";
|
|
128
|
+
const server = nodeCreateServer(async (req, res) => {
|
|
129
|
+
try {
|
|
130
|
+
const normalized = await normalizeRequest(req);
|
|
131
|
+
const response = await handler(normalized);
|
|
132
|
+
writeResponse(res, response);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
// Fallback error response
|
|
136
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
137
|
+
res.end(JSON.stringify({
|
|
138
|
+
error: "Internal Server Error",
|
|
139
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
server,
|
|
145
|
+
listen(port) {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
server.on("error", reject);
|
|
148
|
+
server.listen(port, hostname, () => {
|
|
149
|
+
server.removeListener("error", reject);
|
|
150
|
+
resolve({
|
|
151
|
+
async close() {
|
|
152
|
+
return new Promise((res, rej) => {
|
|
153
|
+
server.close((err) => (err ? rej(err) : res()));
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typokit/platform-node",
|
|
3
|
+
"exports": {
|
|
4
|
+
".": {
|
|
5
|
+
"import": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.4",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@typokit/types": "0.1.4"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/KyleBastien/typokit",
|
|
26
|
+
"directory": "packages/platform-node"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "rstest run --passWithNoTests"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// @typokit/platform-node — Tests
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "@rstest/core";
|
|
4
|
+
import {
|
|
5
|
+
createServer,
|
|
6
|
+
normalizeRequest,
|
|
7
|
+
writeResponse,
|
|
8
|
+
getPlatformInfo,
|
|
9
|
+
} from "./index.js";
|
|
10
|
+
import type { TypoKitResponse } from "@typokit/types";
|
|
11
|
+
import { createServer as nodeCreateServer } from "node:http";
|
|
12
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
13
|
+
|
|
14
|
+
// ─── getPlatformInfo ─────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe("getPlatformInfo", () => {
|
|
17
|
+
it("returns node runtime and version", () => {
|
|
18
|
+
const info = getPlatformInfo();
|
|
19
|
+
expect(info.runtime).toBe("node");
|
|
20
|
+
expect(info.version).toMatch(/^v\d+/);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ─── normalizeRequest ────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe("normalizeRequest", () => {
|
|
27
|
+
it("parses method, path, headers, and query from IncomingMessage", async () => {
|
|
28
|
+
const normalized = await new Promise<
|
|
29
|
+
Awaited<ReturnType<typeof normalizeRequest>>
|
|
30
|
+
>((resolve, reject) => {
|
|
31
|
+
const server = nodeCreateServer(async (req: IncomingMessage) => {
|
|
32
|
+
try {
|
|
33
|
+
resolve(await normalizeRequest(req));
|
|
34
|
+
} catch (e) {
|
|
35
|
+
reject(e);
|
|
36
|
+
}
|
|
37
|
+
server.close();
|
|
38
|
+
});
|
|
39
|
+
server.listen(0, "127.0.0.1", () => {
|
|
40
|
+
const addr = server.address();
|
|
41
|
+
if (!addr || typeof addr === "string") return;
|
|
42
|
+
const url = `http://127.0.0.1:${addr.port}/hello?foo=bar`;
|
|
43
|
+
fetch(url, { method: "GET", headers: { "x-test": "yes" } }).catch(
|
|
44
|
+
() => {},
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(normalized.method).toBe("GET");
|
|
50
|
+
expect(normalized.path).toBe("/hello");
|
|
51
|
+
expect(normalized.query["foo"]).toBe("bar");
|
|
52
|
+
expect(normalized.headers["x-test"]).toBe("yes");
|
|
53
|
+
expect(normalized.params).toEqual({});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("collects JSON body when content-type is application/json", async () => {
|
|
57
|
+
const normalized = await new Promise<
|
|
58
|
+
Awaited<ReturnType<typeof normalizeRequest>>
|
|
59
|
+
>((resolve, reject) => {
|
|
60
|
+
const server = nodeCreateServer(
|
|
61
|
+
async (req: IncomingMessage, res: ServerResponse) => {
|
|
62
|
+
try {
|
|
63
|
+
resolve(await normalizeRequest(req));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
reject(e);
|
|
66
|
+
}
|
|
67
|
+
res.end();
|
|
68
|
+
server.close();
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
server.listen(0, "127.0.0.1", () => {
|
|
72
|
+
const addr = server.address();
|
|
73
|
+
if (!addr || typeof addr === "string") return;
|
|
74
|
+
fetch(`http://127.0.0.1:${addr.port}/data`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "content-type": "application/json" },
|
|
77
|
+
body: JSON.stringify({ name: "test" }),
|
|
78
|
+
}).catch(() => {});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(normalized.method).toBe("POST");
|
|
83
|
+
expect(normalized.body).toEqual({ name: "test" });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── writeResponse ───────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
describe("writeResponse", () => {
|
|
90
|
+
it("writes status, headers, and JSON body to ServerResponse", async () => {
|
|
91
|
+
const result = await new Promise<{
|
|
92
|
+
status: number;
|
|
93
|
+
body: string;
|
|
94
|
+
headers: Record<string, string>;
|
|
95
|
+
}>((resolve, reject) => {
|
|
96
|
+
const server = nodeCreateServer(
|
|
97
|
+
(_req: IncomingMessage, res: ServerResponse) => {
|
|
98
|
+
const response: TypoKitResponse = {
|
|
99
|
+
status: 201,
|
|
100
|
+
headers: { "x-custom": "value" },
|
|
101
|
+
body: { created: true },
|
|
102
|
+
};
|
|
103
|
+
writeResponse(res, response);
|
|
104
|
+
server.close();
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
server.listen(0, "127.0.0.1", () => {
|
|
108
|
+
const addr = server.address();
|
|
109
|
+
if (!addr || typeof addr === "string") return;
|
|
110
|
+
fetch(`http://127.0.0.1:${addr.port}/`)
|
|
111
|
+
.then(async (resp) => {
|
|
112
|
+
resolve({
|
|
113
|
+
status: resp.status,
|
|
114
|
+
body: await resp.text(),
|
|
115
|
+
headers: Object.fromEntries(resp.headers.entries()),
|
|
116
|
+
});
|
|
117
|
+
})
|
|
118
|
+
.catch(reject);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(result.status).toBe(201);
|
|
123
|
+
expect(result.headers["x-custom"]).toBe("value");
|
|
124
|
+
expect(JSON.parse(result.body)).toEqual({ created: true });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ─── createServer ────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
describe("createServer", () => {
|
|
131
|
+
it("starts a server, handles a request, and returns a response", async () => {
|
|
132
|
+
const srv = createServer(async (req) => ({
|
|
133
|
+
status: 200,
|
|
134
|
+
headers: { "content-type": "application/json" },
|
|
135
|
+
body: { echo: req.path },
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
const handle = await srv.listen(0);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const addr = srv.server.address();
|
|
142
|
+
if (!addr || typeof addr === "string") throw new Error("No address");
|
|
143
|
+
|
|
144
|
+
const resp = await fetch(`http://127.0.0.1:${addr.port}/test-path`);
|
|
145
|
+
expect(resp.status).toBe(200);
|
|
146
|
+
|
|
147
|
+
const body = await resp.json();
|
|
148
|
+
expect(body).toEqual({ echo: "/test-path" });
|
|
149
|
+
} finally {
|
|
150
|
+
await handle.close();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("returns 500 when handler throws", async () => {
|
|
155
|
+
const srv = createServer(async () => {
|
|
156
|
+
throw new Error("boom");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const handle = await srv.listen(0);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const addr = srv.server.address();
|
|
163
|
+
if (!addr || typeof addr === "string") throw new Error("No address");
|
|
164
|
+
|
|
165
|
+
const resp = await fetch(`http://127.0.0.1:${addr.port}/fail`);
|
|
166
|
+
expect(resp.status).toBe(500);
|
|
167
|
+
|
|
168
|
+
const body = (await resp.json()) as { error: string; message: string };
|
|
169
|
+
expect(body.error).toBe("Internal Server Error");
|
|
170
|
+
expect(body.message).toBe("boom");
|
|
171
|
+
} finally {
|
|
172
|
+
await handle.close();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("close() gracefully shuts down the server", async () => {
|
|
177
|
+
const srv = createServer(async () => ({
|
|
178
|
+
status: 200,
|
|
179
|
+
headers: {},
|
|
180
|
+
body: null,
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
const handle = await srv.listen(0);
|
|
184
|
+
const addr = srv.server.address();
|
|
185
|
+
expect(addr).not.toBeNull();
|
|
186
|
+
|
|
187
|
+
await handle.close();
|
|
188
|
+
|
|
189
|
+
// Server should no longer be listening
|
|
190
|
+
expect(srv.server.listening).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// @typokit/platform-node — Node.js Platform Adapter
|
|
2
|
+
|
|
3
|
+
import { createServer as nodeCreateServer } from "node:http";
|
|
4
|
+
import type { IncomingMessage, ServerResponse, Server } from "node:http";
|
|
5
|
+
import { URL } from "node:url";
|
|
6
|
+
import type {
|
|
7
|
+
HttpMethod,
|
|
8
|
+
ServerHandle,
|
|
9
|
+
TypoKitRequest,
|
|
10
|
+
TypoKitResponse,
|
|
11
|
+
} from "@typokit/types";
|
|
12
|
+
|
|
13
|
+
// ─── Platform Info ───────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** Runtime platform metadata for diagnostics and inspect commands */
|
|
16
|
+
export interface PlatformInfo {
|
|
17
|
+
runtime: string;
|
|
18
|
+
version: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Returns Node.js platform info */
|
|
22
|
+
export function getPlatformInfo(): PlatformInfo {
|
|
23
|
+
return {
|
|
24
|
+
runtime: "node",
|
|
25
|
+
version: process.version,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Request / Response Helpers ──────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Collect the body of an IncomingMessage into a buffer, then parse as JSON or return raw string */
|
|
32
|
+
function collectBody(req: IncomingMessage): Promise<unknown> {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const chunks: Buffer[] = [];
|
|
35
|
+
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
36
|
+
req.on("error", reject);
|
|
37
|
+
req.on("end", () => {
|
|
38
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
39
|
+
if (!raw) {
|
|
40
|
+
resolve(undefined);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
44
|
+
if (contentType.includes("application/json")) {
|
|
45
|
+
try {
|
|
46
|
+
resolve(JSON.parse(raw));
|
|
47
|
+
} catch {
|
|
48
|
+
resolve(raw);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
resolve(raw);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Parse query string from a URL into a Record */
|
|
58
|
+
function parseQuery(
|
|
59
|
+
searchParams: URLSearchParams,
|
|
60
|
+
): Record<string, string | string[] | undefined> {
|
|
61
|
+
const result: Record<string, string | string[] | undefined> = {};
|
|
62
|
+
for (const [key, value] of searchParams.entries()) {
|
|
63
|
+
const existing = result[key];
|
|
64
|
+
if (existing === undefined) {
|
|
65
|
+
result[key] = value;
|
|
66
|
+
} else if (Array.isArray(existing)) {
|
|
67
|
+
existing.push(value);
|
|
68
|
+
} else {
|
|
69
|
+
result[key] = [existing, value];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Normalize Node.js headers into a flat Record */
|
|
76
|
+
function normalizeHeaders(
|
|
77
|
+
raw: IncomingMessage["headers"],
|
|
78
|
+
): Record<string, string | string[] | undefined> {
|
|
79
|
+
const result: Record<string, string | string[] | undefined> = {};
|
|
80
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
81
|
+
result[key] = value;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Normalize a Node.js IncomingMessage into a TypoKitRequest.
|
|
88
|
+
* Body is collected asynchronously from the stream.
|
|
89
|
+
*/
|
|
90
|
+
export async function normalizeRequest(
|
|
91
|
+
req: IncomingMessage,
|
|
92
|
+
): Promise<TypoKitRequest> {
|
|
93
|
+
const url = new URL(
|
|
94
|
+
req.url ?? "/",
|
|
95
|
+
`http://${req.headers.host ?? "localhost"}`,
|
|
96
|
+
);
|
|
97
|
+
const body = await collectBody(req);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
method: (req.method ?? "GET").toUpperCase() as HttpMethod,
|
|
101
|
+
path: url.pathname,
|
|
102
|
+
headers: normalizeHeaders(req.headers),
|
|
103
|
+
body,
|
|
104
|
+
query: parseQuery(url.searchParams),
|
|
105
|
+
params: {},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Write a TypoKitResponse to a Node.js ServerResponse.
|
|
111
|
+
*/
|
|
112
|
+
export function writeResponse(
|
|
113
|
+
res: ServerResponse,
|
|
114
|
+
response: TypoKitResponse,
|
|
115
|
+
): void {
|
|
116
|
+
// Set headers
|
|
117
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
118
|
+
if (value !== undefined) {
|
|
119
|
+
res.setHeader(key, value);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Determine body content before writing head
|
|
124
|
+
let bodyContent: string | Buffer | undefined;
|
|
125
|
+
if (response.body === null || response.body === undefined) {
|
|
126
|
+
bodyContent = undefined;
|
|
127
|
+
} else if (typeof response.body === "string") {
|
|
128
|
+
bodyContent = response.body;
|
|
129
|
+
} else if (Buffer.isBuffer(response.body)) {
|
|
130
|
+
bodyContent = response.body;
|
|
131
|
+
} else {
|
|
132
|
+
// JSON serialize objects — set content-type before writeHead
|
|
133
|
+
if (!res.getHeader("content-type")) {
|
|
134
|
+
res.setHeader("content-type", "application/json");
|
|
135
|
+
}
|
|
136
|
+
bodyContent = JSON.stringify(response.body);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
res.writeHead(response.status);
|
|
140
|
+
res.end(bodyContent);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Request Handler Type ────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/** Handler function that receives a normalized request and returns a response */
|
|
146
|
+
export type NodeRequestHandler = (
|
|
147
|
+
req: TypoKitRequest,
|
|
148
|
+
) => Promise<TypoKitResponse>;
|
|
149
|
+
|
|
150
|
+
// ─── Node Server Options ─────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export interface NodeServerOptions {
|
|
153
|
+
/** Optional hostname to bind to (default: "0.0.0.0") */
|
|
154
|
+
hostname?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Node Server ─────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/** Result of createServer — provides listen/close and access to the raw node:http server */
|
|
160
|
+
export interface NodeServer {
|
|
161
|
+
/** Start listening on the given port. Returns a handle for graceful shutdown. */
|
|
162
|
+
listen(port: number): Promise<ServerHandle>;
|
|
163
|
+
/** The underlying node:http Server instance */
|
|
164
|
+
server: Server;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create a Node.js HTTP server that dispatches to a TypoKit request handler.
|
|
169
|
+
*
|
|
170
|
+
* Usage:
|
|
171
|
+
* ```ts
|
|
172
|
+
* const srv = createServer(async (req) => ({
|
|
173
|
+
* status: 200,
|
|
174
|
+
* headers: {},
|
|
175
|
+
* body: { ok: true },
|
|
176
|
+
* }));
|
|
177
|
+
* const handle = await srv.listen(3000);
|
|
178
|
+
* // ... later
|
|
179
|
+
* await handle.close();
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export function createServer(
|
|
183
|
+
handler: NodeRequestHandler,
|
|
184
|
+
options: NodeServerOptions = {},
|
|
185
|
+
): NodeServer {
|
|
186
|
+
const hostname = options.hostname ?? "0.0.0.0";
|
|
187
|
+
|
|
188
|
+
const server = nodeCreateServer(
|
|
189
|
+
async (req: IncomingMessage, res: ServerResponse) => {
|
|
190
|
+
try {
|
|
191
|
+
const normalized = await normalizeRequest(req);
|
|
192
|
+
const response = await handler(normalized);
|
|
193
|
+
writeResponse(res, response);
|
|
194
|
+
} catch (err: unknown) {
|
|
195
|
+
// Fallback error response
|
|
196
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
197
|
+
res.end(
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
error: "Internal Server Error",
|
|
200
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
server,
|
|
209
|
+
listen(port: number): Promise<ServerHandle> {
|
|
210
|
+
return new Promise((resolve, reject) => {
|
|
211
|
+
server.on("error", reject);
|
|
212
|
+
server.listen(port, hostname, () => {
|
|
213
|
+
server.removeListener("error", reject);
|
|
214
|
+
resolve({
|
|
215
|
+
async close(): Promise<void> {
|
|
216
|
+
return new Promise((res, rej) => {
|
|
217
|
+
server.close((err) => (err ? rej(err) : res()));
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|