@typokit/platform-deno 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 +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +167 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -0
- package/src/env.d.ts +50 -0
- package/src/index.test.ts +307 -0
- package/src/index.ts +250 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ServerHandle, TypoKitRequest, TypoKitResponse } from "@typokit/types";
|
|
2
|
+
/** Subset of Deno's HttpServer type we rely on */
|
|
3
|
+
interface DenoHttpServer {
|
|
4
|
+
shutdown(): Promise<void>;
|
|
5
|
+
finished: Promise<void>;
|
|
6
|
+
addr: {
|
|
7
|
+
port: number;
|
|
8
|
+
hostname: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Runtime platform metadata for diagnostics and inspect commands */
|
|
12
|
+
export interface PlatformInfo {
|
|
13
|
+
runtime: string;
|
|
14
|
+
version: string;
|
|
15
|
+
}
|
|
16
|
+
/** Returns Deno platform info */
|
|
17
|
+
export declare function getPlatformInfo(): PlatformInfo;
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a Web API Request (used by Deno.serve) into a TypoKitRequest.
|
|
20
|
+
*/
|
|
21
|
+
export declare function normalizeRequest(req: Request): Promise<TypoKitRequest>;
|
|
22
|
+
/**
|
|
23
|
+
* Convert a TypoKitResponse into a Web API Response for Deno.serve().
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildResponse(response: TypoKitResponse): Response;
|
|
26
|
+
/** Handler function that receives a normalized request and returns a response */
|
|
27
|
+
export type DenoRequestHandler = (req: TypoKitRequest) => Promise<TypoKitResponse>;
|
|
28
|
+
export interface DenoServerOptions {
|
|
29
|
+
/** Optional hostname to bind to (default: "0.0.0.0") */
|
|
30
|
+
hostname?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Result of createServer — provides listen/close and access to the underlying Deno server */
|
|
33
|
+
export interface DenoServerInstance {
|
|
34
|
+
/** Start listening on the given port. Returns a handle for graceful shutdown. */
|
|
35
|
+
listen(port: number): Promise<ServerHandle>;
|
|
36
|
+
/** The underlying Deno HTTP server instance (available after listen()) */
|
|
37
|
+
server: DenoHttpServer | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a Deno HTTP server that dispatches to a TypoKit request handler.
|
|
41
|
+
*
|
|
42
|
+
* Usage:
|
|
43
|
+
* ```ts
|
|
44
|
+
* const srv = createServer(async (req) => ({
|
|
45
|
+
* status: 200,
|
|
46
|
+
* headers: {},
|
|
47
|
+
* body: { ok: true },
|
|
48
|
+
* }));
|
|
49
|
+
* const handle = await srv.listen(3000);
|
|
50
|
+
* // ... later
|
|
51
|
+
* await handle.close();
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function createServer(handler: DenoRequestHandler, options?: DenoServerOptions): DenoServerInstance;
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,YAAY,EACZ,cAAc,EACd,eAAe,EAChB,MAAM,gBAAgB,CAAC;AAMxB,kDAAkD;AAClD,UAAU,cAAc;IACtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAoBD,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,iCAAiC;AACjC,wBAAgB,eAAe,IAAI,YAAY,CAM9C;AAiCD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CA4B5E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,eAAe,GAAG,QAAQ,CA8BjE;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,8FAA8F;AAC9F,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5C,0EAA0E;IAC1E,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,iBAAsB,GAC9B,kBAAkB,CAuDpB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// @typokit/platform-deno — Deno Platform Adapter
|
|
2
|
+
/** Returns Deno platform info */
|
|
3
|
+
export function getPlatformInfo() {
|
|
4
|
+
const deno = globalThis.Deno;
|
|
5
|
+
return {
|
|
6
|
+
runtime: "deno",
|
|
7
|
+
version: deno?.version?.deno ?? "unknown",
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
// ─── Request / Response Helpers ──────────────────────────────
|
|
11
|
+
/** Parse query string from a URL into a Record */
|
|
12
|
+
function parseQuery(searchParams) {
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const [key, value] of searchParams.entries()) {
|
|
15
|
+
const existing = result[key];
|
|
16
|
+
if (existing === undefined) {
|
|
17
|
+
result[key] = value;
|
|
18
|
+
}
|
|
19
|
+
else if (Array.isArray(existing)) {
|
|
20
|
+
existing.push(value);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
result[key] = [existing, value];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
/** Normalize Web API headers into a flat Record */
|
|
29
|
+
function normalizeHeaders(headers) {
|
|
30
|
+
const result = {};
|
|
31
|
+
headers.forEach((value, key) => {
|
|
32
|
+
result[key] = value;
|
|
33
|
+
});
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a Web API Request (used by Deno.serve) into a TypoKitRequest.
|
|
38
|
+
*/
|
|
39
|
+
export async function normalizeRequest(req) {
|
|
40
|
+
const url = new URL(req.url);
|
|
41
|
+
let body = undefined;
|
|
42
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
43
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
44
|
+
const raw = await req.text();
|
|
45
|
+
if (raw) {
|
|
46
|
+
if (contentType.includes("application/json")) {
|
|
47
|
+
try {
|
|
48
|
+
body = JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
body = raw;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
body = raw;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
method: req.method.toUpperCase(),
|
|
61
|
+
path: url.pathname,
|
|
62
|
+
headers: normalizeHeaders(req.headers),
|
|
63
|
+
body,
|
|
64
|
+
query: parseQuery(url.searchParams),
|
|
65
|
+
params: {},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Convert a TypoKitResponse into a Web API Response for Deno.serve().
|
|
70
|
+
*/
|
|
71
|
+
export function buildResponse(response) {
|
|
72
|
+
const headers = new Headers();
|
|
73
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
74
|
+
if (value !== undefined) {
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
for (const v of value) {
|
|
77
|
+
headers.append(key, v);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
headers.set(key, value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let bodyContent = null;
|
|
86
|
+
if (response.body === null || response.body === undefined) {
|
|
87
|
+
bodyContent = null;
|
|
88
|
+
}
|
|
89
|
+
else if (typeof response.body === "string") {
|
|
90
|
+
bodyContent = response.body;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
if (!headers.has("content-type")) {
|
|
94
|
+
headers.set("content-type", "application/json");
|
|
95
|
+
}
|
|
96
|
+
bodyContent = JSON.stringify(response.body);
|
|
97
|
+
}
|
|
98
|
+
return new Response(bodyContent, {
|
|
99
|
+
status: response.status,
|
|
100
|
+
headers,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Create a Deno HTTP server that dispatches to a TypoKit request handler.
|
|
105
|
+
*
|
|
106
|
+
* Usage:
|
|
107
|
+
* ```ts
|
|
108
|
+
* const srv = createServer(async (req) => ({
|
|
109
|
+
* status: 200,
|
|
110
|
+
* headers: {},
|
|
111
|
+
* body: { ok: true },
|
|
112
|
+
* }));
|
|
113
|
+
* const handle = await srv.listen(3000);
|
|
114
|
+
* // ... later
|
|
115
|
+
* await handle.close();
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export function createServer(handler, options = {}) {
|
|
119
|
+
const hostname = options.hostname ?? "0.0.0.0";
|
|
120
|
+
let denoServer = null;
|
|
121
|
+
const instance = {
|
|
122
|
+
get server() {
|
|
123
|
+
return denoServer;
|
|
124
|
+
},
|
|
125
|
+
listen(port) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
try {
|
|
128
|
+
const deno = globalThis.Deno;
|
|
129
|
+
denoServer = deno.serve({
|
|
130
|
+
port,
|
|
131
|
+
hostname,
|
|
132
|
+
onListen() {
|
|
133
|
+
resolve({
|
|
134
|
+
async close() {
|
|
135
|
+
if (denoServer) {
|
|
136
|
+
await denoServer.shutdown();
|
|
137
|
+
denoServer = null;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
}, async (req) => {
|
|
143
|
+
try {
|
|
144
|
+
const normalized = await normalizeRequest(req);
|
|
145
|
+
const response = await handler(normalized);
|
|
146
|
+
return buildResponse(response);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
return new Response(JSON.stringify({
|
|
150
|
+
error: "Internal Server Error",
|
|
151
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
|
152
|
+
}), {
|
|
153
|
+
status: 500,
|
|
154
|
+
headers: { "content-type": "application/json" },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
reject(err);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
return instance;
|
|
166
|
+
}
|
|
167
|
+
//# 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,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typokit/platform-deno",
|
|
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
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/KyleBastien/typokit",
|
|
23
|
+
"directory": "packages/platform-deno"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "rstest run --passWithNoTests"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Minimal Web API type declarations for Deno platform adapter.
|
|
2
|
+
// These types are available in Deno natively, but we declare them here
|
|
3
|
+
// so the package compiles under Node16 moduleResolution without DOM lib.
|
|
4
|
+
|
|
5
|
+
declare class URL {
|
|
6
|
+
constructor(url: string, base?: string);
|
|
7
|
+
readonly pathname: string;
|
|
8
|
+
readonly searchParams: URLSearchParams;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare class URLSearchParams {
|
|
12
|
+
entries(): IterableIterator<[string, string]>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare class Headers {
|
|
16
|
+
constructor();
|
|
17
|
+
get(name: string): string | null;
|
|
18
|
+
has(name: string): boolean;
|
|
19
|
+
set(name: string, value: string): void;
|
|
20
|
+
append(name: string, value: string): void;
|
|
21
|
+
forEach(callback: (value: string, key: string) => void): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare class Request {
|
|
25
|
+
constructor(input: string, init?: RequestInit);
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly method: string;
|
|
28
|
+
readonly headers: Headers;
|
|
29
|
+
text(): Promise<string>;
|
|
30
|
+
json(): Promise<unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RequestInit {
|
|
34
|
+
method?: string;
|
|
35
|
+
headers?: Record<string, string> | Headers;
|
|
36
|
+
body?: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare class Response {
|
|
40
|
+
constructor(body?: string | null, init?: ResponseInit);
|
|
41
|
+
readonly status: number;
|
|
42
|
+
readonly headers: Headers;
|
|
43
|
+
text(): Promise<string>;
|
|
44
|
+
json(): Promise<unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ResponseInit {
|
|
48
|
+
status?: number;
|
|
49
|
+
headers?: Record<string, string> | Headers;
|
|
50
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// @typokit/platform-deno — Tests
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "@rstest/core";
|
|
4
|
+
import {
|
|
5
|
+
normalizeRequest,
|
|
6
|
+
buildResponse,
|
|
7
|
+
getPlatformInfo,
|
|
8
|
+
createServer,
|
|
9
|
+
} from "./index.js";
|
|
10
|
+
import type { TypoKitResponse } from "@typokit/types";
|
|
11
|
+
|
|
12
|
+
// ─── getPlatformInfo ─────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
describe("getPlatformInfo", () => {
|
|
15
|
+
it("returns deno runtime", () => {
|
|
16
|
+
const g = globalThis as unknown as Record<string, unknown>;
|
|
17
|
+
g["Deno"] = { version: { deno: "2.0.0", v8: "12.0", typescript: "5.0" } };
|
|
18
|
+
try {
|
|
19
|
+
const info = getPlatformInfo();
|
|
20
|
+
expect(info.runtime).toBe("deno");
|
|
21
|
+
expect(info.version).toBe("2.0.0");
|
|
22
|
+
} finally {
|
|
23
|
+
delete g["Deno"];
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns unknown version when Deno global is missing", () => {
|
|
28
|
+
const info = getPlatformInfo();
|
|
29
|
+
expect(info.runtime).toBe("deno");
|
|
30
|
+
expect(info.version).toBe("unknown");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── normalizeRequest ────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
describe("normalizeRequest", () => {
|
|
37
|
+
it("parses method, path, headers, and query from Request", async () => {
|
|
38
|
+
const req = new Request("http://localhost:3000/hello?foo=bar", {
|
|
39
|
+
method: "GET",
|
|
40
|
+
headers: { "x-test": "yes" },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const normalized = await normalizeRequest(req);
|
|
44
|
+
|
|
45
|
+
expect(normalized.method).toBe("GET");
|
|
46
|
+
expect(normalized.path).toBe("/hello");
|
|
47
|
+
expect(normalized.query["foo"]).toBe("bar");
|
|
48
|
+
expect(normalized.headers["x-test"]).toBe("yes");
|
|
49
|
+
expect(normalized.params).toEqual({});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("collects JSON body when content-type is application/json", async () => {
|
|
53
|
+
const req = new Request("http://localhost:3000/data", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "content-type": "application/json" },
|
|
56
|
+
body: JSON.stringify({ name: "test" }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const normalized = await normalizeRequest(req);
|
|
60
|
+
|
|
61
|
+
expect(normalized.method).toBe("POST");
|
|
62
|
+
expect(normalized.body).toEqual({ name: "test" });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns raw string body when content-type is not JSON", async () => {
|
|
66
|
+
const req = new Request("http://localhost:3000/text", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "content-type": "text/plain" },
|
|
69
|
+
body: "hello world",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const normalized = await normalizeRequest(req);
|
|
73
|
+
expect(normalized.body).toBe("hello world");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns undefined body for GET requests", async () => {
|
|
77
|
+
const req = new Request("http://localhost:3000/empty", {
|
|
78
|
+
method: "GET",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const normalized = await normalizeRequest(req);
|
|
82
|
+
expect(normalized.body).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("handles multiple query params with the same key", async () => {
|
|
86
|
+
const req = new Request("http://localhost:3000/multi?tag=a&tag=b", {
|
|
87
|
+
method: "GET",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const normalized = await normalizeRequest(req);
|
|
91
|
+
expect(normalized.query["tag"]).toEqual(["a", "b"]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ─── buildResponse ───────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe("buildResponse", () => {
|
|
98
|
+
it("builds a Response with status, headers, and JSON body", () => {
|
|
99
|
+
const typoResponse: TypoKitResponse = {
|
|
100
|
+
status: 201,
|
|
101
|
+
headers: { "x-custom": "value" },
|
|
102
|
+
body: { created: true },
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const response = buildResponse(typoResponse);
|
|
106
|
+
|
|
107
|
+
expect(response.status).toBe(201);
|
|
108
|
+
expect(response.headers.get("x-custom")).toBe("value");
|
|
109
|
+
expect(response.headers.get("content-type")).toBe("application/json");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("builds a Response with string body", async () => {
|
|
113
|
+
const typoResponse: TypoKitResponse = {
|
|
114
|
+
status: 200,
|
|
115
|
+
headers: { "content-type": "text/plain" },
|
|
116
|
+
body: "hello",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const response = buildResponse(typoResponse);
|
|
120
|
+
const text = await response.text();
|
|
121
|
+
|
|
122
|
+
expect(response.status).toBe(200);
|
|
123
|
+
expect(text).toBe("hello");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("builds a Response with null body", () => {
|
|
127
|
+
const typoResponse: TypoKitResponse = {
|
|
128
|
+
status: 204,
|
|
129
|
+
headers: {},
|
|
130
|
+
body: null,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const response = buildResponse(typoResponse);
|
|
134
|
+
expect(response.status).toBe(204);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("handles array header values", () => {
|
|
138
|
+
const typoResponse: TypoKitResponse = {
|
|
139
|
+
status: 200,
|
|
140
|
+
headers: { "set-cookie": ["a=1", "b=2"] },
|
|
141
|
+
body: null,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const response = buildResponse(typoResponse);
|
|
145
|
+
expect(response.headers.get("set-cookie")).toContain("a=1");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─── createServer ────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
describe("createServer", () => {
|
|
152
|
+
it("creates a server instance with listen method", () => {
|
|
153
|
+
const srv = createServer(async () => ({
|
|
154
|
+
status: 200,
|
|
155
|
+
headers: {},
|
|
156
|
+
body: null,
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
expect(typeof srv.listen).toBe("function");
|
|
160
|
+
expect(srv.server).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("calls Deno.serve when listen is invoked", async () => {
|
|
164
|
+
const mockServer = {
|
|
165
|
+
shutdown: async () => {},
|
|
166
|
+
finished: Promise.resolve(),
|
|
167
|
+
addr: { port: 3000, hostname: "0.0.0.0" },
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const g = globalThis as unknown as Record<string, unknown>;
|
|
171
|
+
g["Deno"] = {
|
|
172
|
+
version: { deno: "2.0.0", v8: "12.0", typescript: "5.0" },
|
|
173
|
+
serve: (opts: {
|
|
174
|
+
onListen?: (addr: { port: number; hostname: string }) => void;
|
|
175
|
+
}) => {
|
|
176
|
+
if (opts.onListen) {
|
|
177
|
+
opts.onListen(mockServer.addr);
|
|
178
|
+
}
|
|
179
|
+
return mockServer;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const srv = createServer(async () => ({
|
|
185
|
+
status: 200,
|
|
186
|
+
headers: {},
|
|
187
|
+
body: { ok: true },
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
const handle = await srv.listen(3000);
|
|
191
|
+
expect(srv.server).not.toBeNull();
|
|
192
|
+
|
|
193
|
+
await handle.close();
|
|
194
|
+
expect(srv.server).toBeNull();
|
|
195
|
+
} finally {
|
|
196
|
+
delete g["Deno"];
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("rejects when Deno global is not available", async () => {
|
|
201
|
+
const srv = createServer(async () => ({
|
|
202
|
+
status: 200,
|
|
203
|
+
headers: {},
|
|
204
|
+
body: null,
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
let error: Error | null = null;
|
|
208
|
+
try {
|
|
209
|
+
await srv.listen(3000);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
error = err as Error;
|
|
212
|
+
}
|
|
213
|
+
expect(error).not.toBeNull();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("fetch handler converts request and returns response", async () => {
|
|
217
|
+
let capturedHandler: ((req: Request) => Promise<Response>) | null = null;
|
|
218
|
+
const mockServer = {
|
|
219
|
+
shutdown: async () => {},
|
|
220
|
+
finished: Promise.resolve(),
|
|
221
|
+
addr: { port: 3001, hostname: "0.0.0.0" },
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const g = globalThis as unknown as Record<string, unknown>;
|
|
225
|
+
g["Deno"] = {
|
|
226
|
+
version: { deno: "2.0.0", v8: "12.0", typescript: "5.0" },
|
|
227
|
+
serve: (
|
|
228
|
+
opts: { onListen?: (addr: { port: number; hostname: string }) => void },
|
|
229
|
+
handler: (req: Request) => Promise<Response>,
|
|
230
|
+
) => {
|
|
231
|
+
capturedHandler = handler;
|
|
232
|
+
if (opts.onListen) {
|
|
233
|
+
opts.onListen(mockServer.addr);
|
|
234
|
+
}
|
|
235
|
+
return mockServer;
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const srv = createServer(async (req) => ({
|
|
241
|
+
status: 200,
|
|
242
|
+
headers: { "content-type": "application/json" },
|
|
243
|
+
body: { echo: req.path },
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
await srv.listen(3001);
|
|
247
|
+
|
|
248
|
+
// Simulate a request through the captured handler
|
|
249
|
+
const webReq = new Request("http://localhost:3001/test-path");
|
|
250
|
+
const webResp = await capturedHandler!(webReq);
|
|
251
|
+
|
|
252
|
+
expect(webResp.status).toBe(200);
|
|
253
|
+
const body = await webResp.json();
|
|
254
|
+
expect(body).toEqual({ echo: "/test-path" });
|
|
255
|
+
|
|
256
|
+
const handle = await srv.listen(3001);
|
|
257
|
+
await handle.close();
|
|
258
|
+
} finally {
|
|
259
|
+
delete g["Deno"];
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("fetch handler returns 500 when handler throws", async () => {
|
|
264
|
+
let capturedHandler: ((req: Request) => Promise<Response>) | null = null;
|
|
265
|
+
const mockServer = {
|
|
266
|
+
shutdown: async () => {},
|
|
267
|
+
finished: Promise.resolve(),
|
|
268
|
+
addr: { port: 3002, hostname: "0.0.0.0" },
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const g = globalThis as unknown as Record<string, unknown>;
|
|
272
|
+
g["Deno"] = {
|
|
273
|
+
version: { deno: "2.0.0", v8: "12.0", typescript: "5.0" },
|
|
274
|
+
serve: (
|
|
275
|
+
opts: { onListen?: (addr: { port: number; hostname: string }) => void },
|
|
276
|
+
handler: (req: Request) => Promise<Response>,
|
|
277
|
+
) => {
|
|
278
|
+
capturedHandler = handler;
|
|
279
|
+
if (opts.onListen) {
|
|
280
|
+
opts.onListen(mockServer.addr);
|
|
281
|
+
}
|
|
282
|
+
return mockServer;
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const srv = createServer(async () => {
|
|
288
|
+
throw new Error("boom");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await srv.listen(3002);
|
|
292
|
+
|
|
293
|
+
const webReq = new Request("http://localhost:3002/fail");
|
|
294
|
+
const webResp = await capturedHandler!(webReq);
|
|
295
|
+
|
|
296
|
+
expect(webResp.status).toBe(500);
|
|
297
|
+
const body = (await webResp.json()) as { error: string; message: string };
|
|
298
|
+
expect(body.error).toBe("Internal Server Error");
|
|
299
|
+
expect(body.message).toBe("boom");
|
|
300
|
+
|
|
301
|
+
const handle = await srv.listen(3002);
|
|
302
|
+
await handle.close();
|
|
303
|
+
} finally {
|
|
304
|
+
delete g["Deno"];
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// @typokit/platform-deno — Deno Platform Adapter
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
HttpMethod,
|
|
5
|
+
ServerHandle,
|
|
6
|
+
TypoKitRequest,
|
|
7
|
+
TypoKitResponse,
|
|
8
|
+
} from "@typokit/types";
|
|
9
|
+
|
|
10
|
+
// ─── Deno Type Declarations ─────────────────────────────────
|
|
11
|
+
// Minimal type declarations for Deno APIs so this package compiles
|
|
12
|
+
// without Deno types installed (they're only available in Deno runtimes).
|
|
13
|
+
|
|
14
|
+
/** Subset of Deno's HttpServer type we rely on */
|
|
15
|
+
interface DenoHttpServer {
|
|
16
|
+
shutdown(): Promise<void>;
|
|
17
|
+
finished: Promise<void>;
|
|
18
|
+
addr: { port: number; hostname: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Options passed to Deno.serve() */
|
|
22
|
+
interface DenoServeOptions {
|
|
23
|
+
port: number;
|
|
24
|
+
hostname: string;
|
|
25
|
+
onListen?: (addr: { port: number; hostname: string }) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Minimal shape of the global Deno object */
|
|
29
|
+
interface DenoGlobal {
|
|
30
|
+
version: { deno: string; v8: string; typescript: string };
|
|
31
|
+
serve(
|
|
32
|
+
options: DenoServeOptions,
|
|
33
|
+
handler: (req: Request) => Promise<Response> | Response,
|
|
34
|
+
): DenoHttpServer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Platform Info ───────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Runtime platform metadata for diagnostics and inspect commands */
|
|
40
|
+
export interface PlatformInfo {
|
|
41
|
+
runtime: string;
|
|
42
|
+
version: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Returns Deno platform info */
|
|
46
|
+
export function getPlatformInfo(): PlatformInfo {
|
|
47
|
+
const deno = (globalThis as unknown as { Deno: DenoGlobal }).Deno;
|
|
48
|
+
return {
|
|
49
|
+
runtime: "deno",
|
|
50
|
+
version: deno?.version?.deno ?? "unknown",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Request / Response Helpers ──────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** Parse query string from a URL into a Record */
|
|
57
|
+
function parseQuery(
|
|
58
|
+
searchParams: URLSearchParams,
|
|
59
|
+
): Record<string, string | string[] | undefined> {
|
|
60
|
+
const result: Record<string, string | string[] | undefined> = {};
|
|
61
|
+
for (const [key, value] of searchParams.entries()) {
|
|
62
|
+
const existing = result[key];
|
|
63
|
+
if (existing === undefined) {
|
|
64
|
+
result[key] = value;
|
|
65
|
+
} else if (Array.isArray(existing)) {
|
|
66
|
+
existing.push(value);
|
|
67
|
+
} else {
|
|
68
|
+
result[key] = [existing, value];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Normalize Web API headers into a flat Record */
|
|
75
|
+
function normalizeHeaders(
|
|
76
|
+
headers: Headers,
|
|
77
|
+
): Record<string, string | string[] | undefined> {
|
|
78
|
+
const result: Record<string, string | string[] | undefined> = {};
|
|
79
|
+
headers.forEach((value, key) => {
|
|
80
|
+
result[key] = value;
|
|
81
|
+
});
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Normalize a Web API Request (used by Deno.serve) into a TypoKitRequest.
|
|
87
|
+
*/
|
|
88
|
+
export async function normalizeRequest(req: Request): Promise<TypoKitRequest> {
|
|
89
|
+
const url = new URL(req.url);
|
|
90
|
+
|
|
91
|
+
let body: unknown = undefined;
|
|
92
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
93
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
94
|
+
const raw = await req.text();
|
|
95
|
+
if (raw) {
|
|
96
|
+
if (contentType.includes("application/json")) {
|
|
97
|
+
try {
|
|
98
|
+
body = JSON.parse(raw);
|
|
99
|
+
} catch {
|
|
100
|
+
body = raw;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
body = raw;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
method: req.method.toUpperCase() as HttpMethod,
|
|
110
|
+
path: url.pathname,
|
|
111
|
+
headers: normalizeHeaders(req.headers),
|
|
112
|
+
body,
|
|
113
|
+
query: parseQuery(url.searchParams),
|
|
114
|
+
params: {},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convert a TypoKitResponse into a Web API Response for Deno.serve().
|
|
120
|
+
*/
|
|
121
|
+
export function buildResponse(response: TypoKitResponse): Response {
|
|
122
|
+
const headers = new Headers();
|
|
123
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
124
|
+
if (value !== undefined) {
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
for (const v of value) {
|
|
127
|
+
headers.append(key, v);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
headers.set(key, value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let bodyContent: string | null = null;
|
|
136
|
+
if (response.body === null || response.body === undefined) {
|
|
137
|
+
bodyContent = null;
|
|
138
|
+
} else if (typeof response.body === "string") {
|
|
139
|
+
bodyContent = response.body;
|
|
140
|
+
} else {
|
|
141
|
+
if (!headers.has("content-type")) {
|
|
142
|
+
headers.set("content-type", "application/json");
|
|
143
|
+
}
|
|
144
|
+
bodyContent = JSON.stringify(response.body);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return new Response(bodyContent, {
|
|
148
|
+
status: response.status,
|
|
149
|
+
headers,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Request Handler Type ────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/** Handler function that receives a normalized request and returns a response */
|
|
156
|
+
export type DenoRequestHandler = (
|
|
157
|
+
req: TypoKitRequest,
|
|
158
|
+
) => Promise<TypoKitResponse>;
|
|
159
|
+
|
|
160
|
+
// ─── Deno Server Options ─────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
export interface DenoServerOptions {
|
|
163
|
+
/** Optional hostname to bind to (default: "0.0.0.0") */
|
|
164
|
+
hostname?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Deno Server ─────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/** Result of createServer — provides listen/close and access to the underlying Deno server */
|
|
170
|
+
export interface DenoServerInstance {
|
|
171
|
+
/** Start listening on the given port. Returns a handle for graceful shutdown. */
|
|
172
|
+
listen(port: number): Promise<ServerHandle>;
|
|
173
|
+
/** The underlying Deno HTTP server instance (available after listen()) */
|
|
174
|
+
server: DenoHttpServer | null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a Deno HTTP server that dispatches to a TypoKit request handler.
|
|
179
|
+
*
|
|
180
|
+
* Usage:
|
|
181
|
+
* ```ts
|
|
182
|
+
* const srv = createServer(async (req) => ({
|
|
183
|
+
* status: 200,
|
|
184
|
+
* headers: {},
|
|
185
|
+
* body: { ok: true },
|
|
186
|
+
* }));
|
|
187
|
+
* const handle = await srv.listen(3000);
|
|
188
|
+
* // ... later
|
|
189
|
+
* await handle.close();
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export function createServer(
|
|
193
|
+
handler: DenoRequestHandler,
|
|
194
|
+
options: DenoServerOptions = {},
|
|
195
|
+
): DenoServerInstance {
|
|
196
|
+
const hostname = options.hostname ?? "0.0.0.0";
|
|
197
|
+
let denoServer: DenoHttpServer | null = null;
|
|
198
|
+
|
|
199
|
+
const instance: DenoServerInstance = {
|
|
200
|
+
get server(): DenoHttpServer | null {
|
|
201
|
+
return denoServer;
|
|
202
|
+
},
|
|
203
|
+
listen(port: number): Promise<ServerHandle> {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
try {
|
|
206
|
+
const deno = (globalThis as unknown as { Deno: DenoGlobal }).Deno;
|
|
207
|
+
denoServer = deno.serve(
|
|
208
|
+
{
|
|
209
|
+
port,
|
|
210
|
+
hostname,
|
|
211
|
+
onListen() {
|
|
212
|
+
resolve({
|
|
213
|
+
async close(): Promise<void> {
|
|
214
|
+
if (denoServer) {
|
|
215
|
+
await denoServer.shutdown();
|
|
216
|
+
denoServer = null;
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
async (req: Request): Promise<Response> => {
|
|
223
|
+
try {
|
|
224
|
+
const normalized = await normalizeRequest(req);
|
|
225
|
+
const response = await handler(normalized);
|
|
226
|
+
return buildResponse(response);
|
|
227
|
+
} catch (err: unknown) {
|
|
228
|
+
return new Response(
|
|
229
|
+
JSON.stringify({
|
|
230
|
+
error: "Internal Server Error",
|
|
231
|
+
message:
|
|
232
|
+
err instanceof Error ? err.message : "Unknown error",
|
|
233
|
+
}),
|
|
234
|
+
{
|
|
235
|
+
status: 500,
|
|
236
|
+
headers: { "content-type": "application/json" },
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
reject(err);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return instance;
|
|
250
|
+
}
|