@voltts/serializer 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/package.json +15 -4
- package/src/index.ts +2 -0
- package/src/serializer.ts +122 -0
- package/src/types.ts +11 -0
- package/test/serializer.test.ts +47 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltts/serializer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ultra-fast response serialization and content-type handling for Volt",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.ts",
|
|
11
|
+
"types": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ResponseInitOptions } from './types';
|
|
2
|
+
|
|
3
|
+
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
4
|
+
const TEXT_CONTENT_TYPE = 'text/plain; charset=utf-8';
|
|
5
|
+
const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
6
|
+
const OCTET_CONTENT_TYPE = 'application/octet-stream';
|
|
7
|
+
|
|
8
|
+
export function json(data: unknown, init?: number | ResponseInitOptions): Response {
|
|
9
|
+
if ((init === undefined || init === 200) && typeof Response.json === 'function') {
|
|
10
|
+
return Response.json(data);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const options = typeof init === 'number' ? { status: init } : init || {};
|
|
14
|
+
const status = options.status ?? 200;
|
|
15
|
+
|
|
16
|
+
// Use Bun's highly optimized Response.json when available
|
|
17
|
+
if (typeof Response.json === 'function') {
|
|
18
|
+
return Response.json(data, {
|
|
19
|
+
...options,
|
|
20
|
+
status,
|
|
21
|
+
headers: {
|
|
22
|
+
'content-type': JSON_CONTENT_TYPE,
|
|
23
|
+
...options.headers,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return new Response(JSON.stringify(data), {
|
|
29
|
+
...options,
|
|
30
|
+
status,
|
|
31
|
+
headers: {
|
|
32
|
+
'content-type': JSON_CONTENT_TYPE,
|
|
33
|
+
...options.headers,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function text(text: string, init?: number | ResponseInitOptions): Response {
|
|
39
|
+
const options = typeof init === 'number' ? { status: init } : init || {};
|
|
40
|
+
return new Response(text, {
|
|
41
|
+
...options,
|
|
42
|
+
status: options.status ?? 200,
|
|
43
|
+
headers: {
|
|
44
|
+
'content-type': TEXT_CONTENT_TYPE,
|
|
45
|
+
...options.headers,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function html(html: string, init?: number | ResponseInitOptions): Response {
|
|
51
|
+
const options = typeof init === 'number' ? { status: init } : init || {};
|
|
52
|
+
return new Response(html, {
|
|
53
|
+
...options,
|
|
54
|
+
status: options.status ?? 200,
|
|
55
|
+
headers: {
|
|
56
|
+
'content-type': HTML_CONTENT_TYPE,
|
|
57
|
+
...options.headers,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function binary(buffer: BodyInit, contentType = OCTET_CONTENT_TYPE, init?: number | ResponseInitOptions): Response {
|
|
63
|
+
const options = typeof init === 'number' ? { status: init } : init || {};
|
|
64
|
+
return new Response(buffer, {
|
|
65
|
+
...options,
|
|
66
|
+
status: options.status ?? 200,
|
|
67
|
+
headers: {
|
|
68
|
+
'content-type': contentType,
|
|
69
|
+
...options.headers,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function stream(readable: ReadableStream, contentType = OCTET_CONTENT_TYPE, init?: number | ResponseInitOptions): Response {
|
|
75
|
+
const options = typeof init === 'number' ? { status: init } : init || {};
|
|
76
|
+
return new Response(readable, {
|
|
77
|
+
...options,
|
|
78
|
+
status: options.status ?? 200,
|
|
79
|
+
headers: {
|
|
80
|
+
'content-type': contentType,
|
|
81
|
+
...options.headers,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function serializeResponse(value: unknown, defaultStatus = 200): Response {
|
|
87
|
+
if (value instanceof Response) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof value === 'object') {
|
|
92
|
+
if (value === null) {
|
|
93
|
+
return new Response(null, { status: 204 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fast-path: 99% of API responses are plain objects/arrays with 200 status
|
|
97
|
+
if (defaultStatus === 200 && typeof Response.json === 'function' && ((value as any).constructor === Object || Array.isArray(value))) {
|
|
98
|
+
return Response.json(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (value instanceof Blob || value instanceof ArrayBuffer || value instanceof Uint8Array) {
|
|
102
|
+
return binary(value as BodyInit, OCTET_CONTENT_TYPE, defaultStatus);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (value instanceof ReadableStream) {
|
|
106
|
+
return stream(value, OCTET_CONTENT_TYPE, defaultStatus);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return json(value, defaultStatus);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof value === 'string') {
|
|
113
|
+
return text(value, defaultStatus);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (value === undefined) {
|
|
117
|
+
return new Response(null, { status: 204 });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return text(String(value), defaultStatus);
|
|
121
|
+
}
|
|
122
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { html, json, serializeResponse, text } from '../src/index';
|
|
3
|
+
|
|
4
|
+
describe('@voltts/serializer', () => {
|
|
5
|
+
it('serializes JSON responses with correct headers', async () => {
|
|
6
|
+
const res = json({ hello: 'volt' }, 201);
|
|
7
|
+
expect(res.status).toBe(201);
|
|
8
|
+
expect(res.headers.get('content-type')).toContain('application/json');
|
|
9
|
+
const data = await res.json();
|
|
10
|
+
expect(data).toEqual({ hello: 'volt' });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('serializes text responses', async () => {
|
|
14
|
+
const res = text('Plain text response', 200);
|
|
15
|
+
expect(res.status).toBe(200);
|
|
16
|
+
expect(res.headers.get('content-type')).toContain('text/plain');
|
|
17
|
+
expect(await res.text()).toBe('Plain text response');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('serializes HTML responses', async () => {
|
|
21
|
+
const res = html('<h1>Volt</h1>', 200);
|
|
22
|
+
expect(res.status).toBe(200);
|
|
23
|
+
expect(res.headers.get('content-type')).toContain('text/html');
|
|
24
|
+
expect(await res.text()).toBe('<h1>Volt</h1>');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('automatically detects and serializes arbitrary handler return values', async () => {
|
|
28
|
+
// 1. Primitive string
|
|
29
|
+
const resStr = serializeResponse('hello');
|
|
30
|
+
expect(resStr.headers.get('content-type')).toContain('text/plain');
|
|
31
|
+
expect(await resStr.text()).toBe('hello');
|
|
32
|
+
|
|
33
|
+
// 2. Object
|
|
34
|
+
const resObj = serializeResponse({ a: 1 });
|
|
35
|
+
expect(resObj.headers.get('content-type')).toContain('application/json');
|
|
36
|
+
expect(await resObj.json()).toEqual({ a: 1 });
|
|
37
|
+
|
|
38
|
+
// 3. Null / Undefined -> 204 No Content
|
|
39
|
+
const resNull = serializeResponse(null);
|
|
40
|
+
expect(resNull.status).toBe(204);
|
|
41
|
+
|
|
42
|
+
// 4. Pass-through Response
|
|
43
|
+
const raw = new Response('raw', { status: 418 });
|
|
44
|
+
const resRaw = serializeResponse(raw);
|
|
45
|
+
expect(resRaw.status).toBe(418);
|
|
46
|
+
});
|
|
47
|
+
});
|
package/README.md
DELETED