@covenant-rpc/request-serializer 1.1.0 → 1.2.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.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/package.json +9 -5
- package/CLAUDE.md +0 -107
- package/index.test.ts +0 -187
- package/index.ts +0 -85
- package/tsconfig.json +0 -29
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const serializedRequestSchema: z.ZodObject<{
|
|
3
|
+
url: z.ZodString;
|
|
4
|
+
method: z.ZodString;
|
|
5
|
+
headers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
6
|
+
body: z.ZodOptional<z.ZodString>;
|
|
7
|
+
bodyType: z.ZodOptional<z.ZodEnum<{
|
|
8
|
+
text: "text";
|
|
9
|
+
json: "json";
|
|
10
|
+
formdata: "formdata";
|
|
11
|
+
arraybuffer: "arraybuffer";
|
|
12
|
+
blob: "blob";
|
|
13
|
+
}>>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export type SerializedRequest = z.infer<typeof serializedRequestSchema>;
|
|
16
|
+
export declare function serializeRequest(request: Request): Promise<SerializedRequest>;
|
|
17
|
+
export declare function deserializeRequest(serialized: SerializedRequest): Request;
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,uBAAuB;;;;;;;;;;;;iBAMlC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoCnF;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,iBAAiB,GAAG,OAAO,CAkCzE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const serializedRequestSchema = z.object({
|
|
3
|
+
url: z.string(),
|
|
4
|
+
method: z.string(),
|
|
5
|
+
headers: z.record(z.string(), z.string()),
|
|
6
|
+
body: z.string().optional(),
|
|
7
|
+
bodyType: z.enum(['text', 'json', 'formdata', 'arraybuffer', 'blob']).optional()
|
|
8
|
+
});
|
|
9
|
+
export async function serializeRequest(request) {
|
|
10
|
+
const headers = {};
|
|
11
|
+
for (const [key, value] of request.headers.entries()) {
|
|
12
|
+
headers[key] = value;
|
|
13
|
+
}
|
|
14
|
+
let body;
|
|
15
|
+
let bodyType;
|
|
16
|
+
if (request.body) {
|
|
17
|
+
const clonedRequest = request.clone();
|
|
18
|
+
const contentType = request.headers.get('content-type');
|
|
19
|
+
if (contentType?.includes('application/json')) {
|
|
20
|
+
body = await clonedRequest.text();
|
|
21
|
+
bodyType = 'json';
|
|
22
|
+
}
|
|
23
|
+
else if (contentType?.includes('multipart/form-data') || contentType?.includes('application/x-www-form-urlencoded')) {
|
|
24
|
+
body = await clonedRequest.text();
|
|
25
|
+
bodyType = 'formdata';
|
|
26
|
+
}
|
|
27
|
+
else if (contentType?.includes('application/octet-stream')) {
|
|
28
|
+
const arrayBuffer = await clonedRequest.arrayBuffer();
|
|
29
|
+
body = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
|
30
|
+
bodyType = 'arraybuffer';
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
body = await clonedRequest.text();
|
|
34
|
+
bodyType = 'text';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
url: request.url,
|
|
39
|
+
method: request.method,
|
|
40
|
+
headers,
|
|
41
|
+
body,
|
|
42
|
+
bodyType
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function deserializeRequest(serialized) {
|
|
46
|
+
const validated = serializedRequestSchema.parse(serialized);
|
|
47
|
+
const { url, method, headers, body, bodyType } = validated;
|
|
48
|
+
let requestBody;
|
|
49
|
+
if (body) {
|
|
50
|
+
switch (bodyType) {
|
|
51
|
+
case 'json':
|
|
52
|
+
requestBody = body;
|
|
53
|
+
break;
|
|
54
|
+
case 'formdata':
|
|
55
|
+
requestBody = body;
|
|
56
|
+
break;
|
|
57
|
+
case 'arraybuffer':
|
|
58
|
+
const binaryString = atob(body);
|
|
59
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
60
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
61
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
62
|
+
}
|
|
63
|
+
requestBody = bytes.buffer;
|
|
64
|
+
break;
|
|
65
|
+
case 'text':
|
|
66
|
+
default:
|
|
67
|
+
requestBody = body;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return new Request(url, {
|
|
72
|
+
method,
|
|
73
|
+
headers,
|
|
74
|
+
body: requestBody
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@covenant-rpc/request-serializer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Serialize and deserialize JavaScript Request objects to/from JSON",
|
|
5
|
-
"main": "index.
|
|
6
|
-
"module": "index.
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "bun test",
|
|
13
|
-
"build": "bun build
|
|
13
|
+
"build": "bun ../../scripts/build-package.ts .",
|
|
14
14
|
"dev": "bun --watch index.ts"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
@@ -21,5 +21,9 @@
|
|
|
21
21
|
],
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"typescript": "^5"
|
|
24
|
-
}
|
|
24
|
+
},
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
]
|
|
25
29
|
}
|
package/CLAUDE.md
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
|
|
3
|
-
Default to using Bun instead of Node.js.
|
|
4
|
-
|
|
5
|
-
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
6
|
-
- Use `bun test` instead of `jest` or `vitest`
|
|
7
|
-
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
8
|
-
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
9
|
-
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
10
|
-
- Bun automatically loads .env, so don't use dotenv.
|
|
11
|
-
|
|
12
|
-
## APIs
|
|
13
|
-
|
|
14
|
-
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
15
|
-
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
16
|
-
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
17
|
-
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
18
|
-
- `WebSocket` is built-in. Don't use `ws`.
|
|
19
|
-
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
20
|
-
- Bun.$`ls` instead of execa.
|
|
21
|
-
|
|
22
|
-
## Testing
|
|
23
|
-
|
|
24
|
-
Use `bun test` to run tests.
|
|
25
|
-
|
|
26
|
-
```ts#index.test.ts
|
|
27
|
-
import { test, expect } from "bun:test";
|
|
28
|
-
|
|
29
|
-
test("hello world", () => {
|
|
30
|
-
expect(1).toBe(1);
|
|
31
|
-
});
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Frontend
|
|
35
|
-
|
|
36
|
-
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
37
|
-
|
|
38
|
-
Server:
|
|
39
|
-
|
|
40
|
-
```ts#index.ts
|
|
41
|
-
import index from "./index.html"
|
|
42
|
-
|
|
43
|
-
Bun.serve({
|
|
44
|
-
routes: {
|
|
45
|
-
"/": index,
|
|
46
|
-
"/api/users/:id": {
|
|
47
|
-
GET: (req) => {
|
|
48
|
-
return new Response(JSON.stringify({ id: req.params.id }));
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
// optional websocket support
|
|
53
|
-
websocket: {
|
|
54
|
-
open: (ws) => {
|
|
55
|
-
ws.send("Hello, world!");
|
|
56
|
-
},
|
|
57
|
-
message: (ws, message) => {
|
|
58
|
-
ws.send(message);
|
|
59
|
-
},
|
|
60
|
-
close: (ws) => {
|
|
61
|
-
// handle close
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
development: {
|
|
65
|
-
hmr: true,
|
|
66
|
-
console: true,
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
72
|
-
|
|
73
|
-
```html#index.html
|
|
74
|
-
<html>
|
|
75
|
-
<body>
|
|
76
|
-
<h1>Hello, world!</h1>
|
|
77
|
-
<script type="module" src="./frontend.tsx"></script>
|
|
78
|
-
</body>
|
|
79
|
-
</html>
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
With the following `frontend.tsx`:
|
|
83
|
-
|
|
84
|
-
```tsx#frontend.tsx
|
|
85
|
-
import React from "react";
|
|
86
|
-
|
|
87
|
-
// import .css files directly and it works
|
|
88
|
-
import './index.css';
|
|
89
|
-
|
|
90
|
-
import { createRoot } from "react-dom/client";
|
|
91
|
-
|
|
92
|
-
const root = createRoot(document.body);
|
|
93
|
-
|
|
94
|
-
export default function Frontend() {
|
|
95
|
-
return <h1>Hello, world!</h1>;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
root.render(<Frontend />);
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Then, run index.ts
|
|
102
|
-
|
|
103
|
-
```sh
|
|
104
|
-
bun --hot ./index.ts
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
package/index.test.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { test, expect } from "bun:test";
|
|
2
|
-
import { serializeRequest, deserializeRequest, serializedRequestSchema, type SerializedRequest } from "./index";
|
|
3
|
-
|
|
4
|
-
test("serialize and deserialize GET request without body", async () => {
|
|
5
|
-
const originalRequest = new Request("https://example.com/api/users", {
|
|
6
|
-
method: "GET",
|
|
7
|
-
headers: {
|
|
8
|
-
"Authorization": "Bearer token123",
|
|
9
|
-
"Content-Type": "application/json"
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const serialized = await serializeRequest(originalRequest);
|
|
14
|
-
const deserialized = deserializeRequest(serialized);
|
|
15
|
-
|
|
16
|
-
expect(serialized.url).toBe("https://example.com/api/users");
|
|
17
|
-
expect(serialized.method).toBe("GET");
|
|
18
|
-
expect(serialized.headers["authorization"]).toBe("Bearer token123");
|
|
19
|
-
expect(serialized.headers["content-type"]).toBe("application/json");
|
|
20
|
-
expect(serialized.body).toBeUndefined();
|
|
21
|
-
expect(serialized.bodyType).toBeUndefined();
|
|
22
|
-
|
|
23
|
-
expect(deserialized.url).toBe(originalRequest.url);
|
|
24
|
-
expect(deserialized.method).toBe(originalRequest.method);
|
|
25
|
-
expect(deserialized.headers.get("Authorization")).toBe("Bearer token123");
|
|
26
|
-
expect(deserialized.headers.get("Content-Type")).toBe("application/json");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("serialize and deserialize POST request with JSON body", async () => {
|
|
30
|
-
const jsonData = { name: "John", age: 30 };
|
|
31
|
-
const originalRequest = new Request("https://example.com/api/users", {
|
|
32
|
-
method: "POST",
|
|
33
|
-
headers: {
|
|
34
|
-
"Content-Type": "application/json"
|
|
35
|
-
},
|
|
36
|
-
body: JSON.stringify(jsonData)
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const serialized = await serializeRequest(originalRequest);
|
|
40
|
-
const deserialized = deserializeRequest(serialized);
|
|
41
|
-
|
|
42
|
-
expect(serialized.method).toBe("POST");
|
|
43
|
-
expect(serialized.body).toBe(JSON.stringify(jsonData));
|
|
44
|
-
expect(serialized.bodyType).toBe("json");
|
|
45
|
-
|
|
46
|
-
const deserializedBody = await deserialized.text();
|
|
47
|
-
expect(JSON.parse(deserializedBody)).toEqual(jsonData);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("serialize and deserialize POST request with form data", async () => {
|
|
51
|
-
const formData = "name=John&age=30";
|
|
52
|
-
const originalRequest = new Request("https://example.com/api/users", {
|
|
53
|
-
method: "POST",
|
|
54
|
-
headers: {
|
|
55
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
56
|
-
},
|
|
57
|
-
body: formData
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const serialized = await serializeRequest(originalRequest);
|
|
61
|
-
const deserialized = deserializeRequest(serialized);
|
|
62
|
-
|
|
63
|
-
expect(serialized.body).toBe(formData);
|
|
64
|
-
expect(serialized.bodyType).toBe("formdata");
|
|
65
|
-
|
|
66
|
-
const deserializedBody = await deserialized.text();
|
|
67
|
-
expect(deserializedBody).toBe(formData);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("serialize and deserialize request with binary data", async () => {
|
|
71
|
-
const binaryData = new Uint8Array([1, 2, 3, 4, 5]);
|
|
72
|
-
const originalRequest = new Request("https://example.com/api/upload", {
|
|
73
|
-
method: "POST",
|
|
74
|
-
headers: {
|
|
75
|
-
"Content-Type": "application/octet-stream"
|
|
76
|
-
},
|
|
77
|
-
body: binaryData
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const serialized = await serializeRequest(originalRequest);
|
|
81
|
-
const deserialized = deserializeRequest(serialized);
|
|
82
|
-
|
|
83
|
-
expect(serialized.bodyType).toBe("arraybuffer");
|
|
84
|
-
expect(serialized.body).toBe(btoa(String.fromCharCode(...binaryData)));
|
|
85
|
-
|
|
86
|
-
const deserializedBuffer = await deserialized.arrayBuffer();
|
|
87
|
-
const deserializedArray = new Uint8Array(deserializedBuffer);
|
|
88
|
-
expect(deserializedArray).toEqual(binaryData);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("serialize and deserialize request with text body", async () => {
|
|
92
|
-
const textData = "Hello, world!";
|
|
93
|
-
const originalRequest = new Request("https://example.com/api/message", {
|
|
94
|
-
method: "POST",
|
|
95
|
-
headers: {
|
|
96
|
-
"Content-Type": "text/plain"
|
|
97
|
-
},
|
|
98
|
-
body: textData
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const serialized = await serializeRequest(originalRequest);
|
|
102
|
-
const deserialized = deserializeRequest(serialized);
|
|
103
|
-
|
|
104
|
-
expect(serialized.body).toBe(textData);
|
|
105
|
-
expect(serialized.bodyType).toBe("text");
|
|
106
|
-
|
|
107
|
-
const deserializedBody = await deserialized.text();
|
|
108
|
-
expect(deserializedBody).toBe(textData);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("handle request with no content-type header", async () => {
|
|
112
|
-
const textData = "Raw data";
|
|
113
|
-
const originalRequest = new Request("https://example.com/api/raw", {
|
|
114
|
-
method: "POST",
|
|
115
|
-
body: textData
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const serialized = await serializeRequest(originalRequest);
|
|
119
|
-
const deserialized = deserializeRequest(serialized);
|
|
120
|
-
|
|
121
|
-
expect(serialized.body).toBe(textData);
|
|
122
|
-
expect(serialized.bodyType).toBe("text");
|
|
123
|
-
|
|
124
|
-
const deserializedBody = await deserialized.text();
|
|
125
|
-
expect(deserializedBody).toBe(textData);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("roundtrip serialization preserves all request properties", async () => {
|
|
129
|
-
const originalRequest = new Request("https://example.com/api/complex?param=value", {
|
|
130
|
-
method: "PATCH",
|
|
131
|
-
headers: {
|
|
132
|
-
"Authorization": "Bearer abc123",
|
|
133
|
-
"X-Custom-Header": "custom-value",
|
|
134
|
-
"Content-Type": "application/json"
|
|
135
|
-
},
|
|
136
|
-
body: JSON.stringify({ update: "data" })
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const serialized = await serializeRequest(originalRequest);
|
|
140
|
-
const json = JSON.stringify(serialized);
|
|
141
|
-
const parsed: SerializedRequest = JSON.parse(json);
|
|
142
|
-
const deserialized = deserializeRequest(parsed);
|
|
143
|
-
|
|
144
|
-
expect(deserialized.url).toBe(originalRequest.url);
|
|
145
|
-
expect(deserialized.method).toBe(originalRequest.method);
|
|
146
|
-
expect(deserialized.headers.get("Authorization")).toBe("Bearer abc123");
|
|
147
|
-
expect(deserialized.headers.get("X-Custom-Header")).toBe("custom-value");
|
|
148
|
-
expect(deserialized.headers.get("Content-Type")).toBe("application/json");
|
|
149
|
-
|
|
150
|
-
const originalBody = await originalRequest.text();
|
|
151
|
-
const deserializedBody = await deserialized.text();
|
|
152
|
-
expect(deserializedBody).toBe(originalBody);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("zod schema validation works correctly", () => {
|
|
156
|
-
const validData = {
|
|
157
|
-
url: "https://example.com",
|
|
158
|
-
method: "GET",
|
|
159
|
-
headers: { "content-type": "application/json" },
|
|
160
|
-
body: "test",
|
|
161
|
-
bodyType: "text" as const
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const result = serializedRequestSchema.safeParse(validData);
|
|
165
|
-
expect(result.success).toBe(true);
|
|
166
|
-
|
|
167
|
-
const invalidData = {
|
|
168
|
-
url: "https://example.com",
|
|
169
|
-
method: "GET",
|
|
170
|
-
headers: { "content-type": "application/json" },
|
|
171
|
-
bodyType: "invalid-type"
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const invalidResult = serializedRequestSchema.safeParse(invalidData);
|
|
175
|
-
expect(invalidResult.success).toBe(false);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("deserializeRequest validates input with zod", () => {
|
|
179
|
-
const invalidData = {
|
|
180
|
-
url: "https://example.com",
|
|
181
|
-
method: "GET",
|
|
182
|
-
headers: { "content-type": "application/json" },
|
|
183
|
-
bodyType: "invalid-type"
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
expect(() => deserializeRequest(invalidData as any)).toThrow();
|
|
187
|
-
});
|
package/index.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
export const serializedRequestSchema = z.object({
|
|
4
|
-
url: z.string(),
|
|
5
|
-
method: z.string(),
|
|
6
|
-
headers: z.record(z.string(), z.string()),
|
|
7
|
-
body: z.string().optional(),
|
|
8
|
-
bodyType: z.enum(['text', 'json', 'formdata', 'arraybuffer', 'blob']).optional()
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
export type SerializedRequest = z.infer<typeof serializedRequestSchema>;
|
|
12
|
-
|
|
13
|
-
export async function serializeRequest(request: Request): Promise<SerializedRequest> {
|
|
14
|
-
const headers: Record<string, string> = {};
|
|
15
|
-
for (const [key, value] of request.headers.entries()) {
|
|
16
|
-
headers[key] = value;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let body: string | undefined;
|
|
20
|
-
let bodyType: SerializedRequest['bodyType'];
|
|
21
|
-
|
|
22
|
-
if (request.body) {
|
|
23
|
-
const clonedRequest = request.clone();
|
|
24
|
-
const contentType = request.headers.get('content-type');
|
|
25
|
-
|
|
26
|
-
if (contentType?.includes('application/json')) {
|
|
27
|
-
body = await clonedRequest.text();
|
|
28
|
-
bodyType = 'json';
|
|
29
|
-
} else if (contentType?.includes('multipart/form-data') || contentType?.includes('application/x-www-form-urlencoded')) {
|
|
30
|
-
body = await clonedRequest.text();
|
|
31
|
-
bodyType = 'formdata';
|
|
32
|
-
} else if (contentType?.includes('application/octet-stream')) {
|
|
33
|
-
const arrayBuffer = await clonedRequest.arrayBuffer();
|
|
34
|
-
body = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
|
35
|
-
bodyType = 'arraybuffer';
|
|
36
|
-
} else {
|
|
37
|
-
body = await clonedRequest.text();
|
|
38
|
-
bodyType = 'text';
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
url: request.url,
|
|
44
|
-
method: request.method,
|
|
45
|
-
headers,
|
|
46
|
-
body,
|
|
47
|
-
bodyType
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function deserializeRequest(serialized: SerializedRequest): Request {
|
|
52
|
-
const validated = serializedRequestSchema.parse(serialized);
|
|
53
|
-
const { url, method, headers, body, bodyType } = validated;
|
|
54
|
-
|
|
55
|
-
let requestBody: Bun.BodyInit | undefined;
|
|
56
|
-
|
|
57
|
-
if (body) {
|
|
58
|
-
switch (bodyType) {
|
|
59
|
-
case 'json':
|
|
60
|
-
requestBody = body;
|
|
61
|
-
break;
|
|
62
|
-
case 'formdata':
|
|
63
|
-
requestBody = body;
|
|
64
|
-
break;
|
|
65
|
-
case 'arraybuffer':
|
|
66
|
-
const binaryString = atob(body);
|
|
67
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
68
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
69
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
70
|
-
}
|
|
71
|
-
requestBody = bytes.buffer;
|
|
72
|
-
break;
|
|
73
|
-
case 'text':
|
|
74
|
-
default:
|
|
75
|
-
requestBody = body;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return new Request(url, {
|
|
81
|
-
method,
|
|
82
|
-
headers,
|
|
83
|
-
body: requestBody
|
|
84
|
-
});
|
|
85
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
// Environment setup & latest features
|
|
4
|
-
"lib": ["ESNext"],
|
|
5
|
-
"target": "ESNext",
|
|
6
|
-
"module": "Preserve",
|
|
7
|
-
"moduleDetection": "force",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"allowJs": true,
|
|
10
|
-
|
|
11
|
-
// Bundler mode
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"allowImportingTsExtensions": true,
|
|
14
|
-
"verbatimModuleSyntax": true,
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
|
|
17
|
-
// Best practices
|
|
18
|
-
"strict": true,
|
|
19
|
-
"skipLibCheck": true,
|
|
20
|
-
"noFallthroughCasesInSwitch": true,
|
|
21
|
-
"noUncheckedIndexedAccess": true,
|
|
22
|
-
"noImplicitOverride": true,
|
|
23
|
-
|
|
24
|
-
// Some stricter flags (disabled by default)
|
|
25
|
-
"noUnusedLocals": false,
|
|
26
|
-
"noUnusedParameters": false,
|
|
27
|
-
"noPropertyAccessFromIndexSignature": false
|
|
28
|
-
}
|
|
29
|
-
}
|