@sdxc/http 2026.9.28 → 2026.9.30
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/README.md +53 -4
- package/dist/response.d.ts +20 -6
- package/dist/response.js +50 -7
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -189,19 +189,68 @@ and returns a plain
|
|
|
189
189
|
import { csv, markdown, xml } from "@sdxc/http/response";
|
|
190
190
|
|
|
191
191
|
xml("<root><item>Hello</item></root>"); // text/xml
|
|
192
|
-
csv("name,age\nAda,36"); // text/csv
|
|
192
|
+
csv("name,age\nAda,36"); // text/csv; charset=utf-8
|
|
193
193
|
markdown("# Hello"); // text/markdown
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
`json(body, init?)` serializes any value through `Response.json`. `text`, `html`, `css`,
|
|
197
|
-
`javascript`, `xml
|
|
197
|
+
`javascript`, `xml` and `markdown` each take a string and write their own type.
|
|
198
198
|
`pdf(body, init?)` writes `application/pdf` and takes a `Blob`, `ArrayBuffer` or
|
|
199
199
|
`ReadableStream`.
|
|
200
200
|
|
|
201
|
+
#### `csv(body: string | ReadableStream<Uint8Array>, init?: ResponseInit): Response`
|
|
202
|
+
|
|
203
|
+
Writes `text/csv; charset=utf-8`, so a client decodes accented and non-Latin cells as UTF-8.
|
|
204
|
+
The body is a whole document or a stream of its UTF-8 bytes, which sends each row as it is
|
|
205
|
+
written instead of holding the file in memory:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { csv } from "@sdxc/http/response";
|
|
209
|
+
|
|
210
|
+
let encoder = new TextEncoder();
|
|
211
|
+
|
|
212
|
+
return csv(
|
|
213
|
+
new ReadableStream<Uint8Array>({
|
|
214
|
+
async start(controller) {
|
|
215
|
+
controller.enqueue(encoder.encode("name,city\r\n"));
|
|
216
|
+
for await (let row of readRows()) {
|
|
217
|
+
controller.enqueue(encoder.encode(`${row.name},${row.city}\r\n`));
|
|
218
|
+
}
|
|
219
|
+
controller.close();
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### `attachment(filename: string): string`
|
|
226
|
+
|
|
227
|
+
A [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Disposition)
|
|
228
|
+
value that makes a response a download saved under `filename`. The quoted `filename` always
|
|
229
|
+
holds an ASCII version of the name, with quotes and backslashes escaped and every other
|
|
230
|
+
character outside printable ASCII shown as `_`. A name that needed such a replacement also
|
|
231
|
+
gets an RFC 8187 `filename*`, which browsers prefer, carrying the exact name in UTF-8.
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { attachment } from "@sdxc/http/response";
|
|
235
|
+
|
|
236
|
+
attachment("report.csv");
|
|
237
|
+
// 'attachment; filename="report.csv"'
|
|
238
|
+
attachment("Café report.csv");
|
|
239
|
+
// "attachment; filename=\"Caf_ report.csv\"; filename*=UTF-8''Caf%C3%A9%20report.csv"
|
|
240
|
+
attachment("報告.csv");
|
|
241
|
+
// "attachment; filename=\"__.csv\"; filename*=UTF-8''%E5%A0%B1%E5%91%8A.csv"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Pass it in the `headers` of any builder, so a CSV or a PDF download keeps its own type:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
return csv(report, { headers: { "Content-Disposition": attachment("report-2026-08.csv") } });
|
|
248
|
+
```
|
|
249
|
+
|
|
201
250
|
#### `file(body: Blob | ArrayBuffer | ReadableStream, filename: string, init?: ResponseInit): Response`
|
|
202
251
|
|
|
203
|
-
A download: `application/octet-stream` plus
|
|
204
|
-
`
|
|
252
|
+
A download of opaque bytes: `application/octet-stream` plus the `Content-Disposition` that
|
|
253
|
+
`attachment(filename)` writes.
|
|
205
254
|
|
|
206
255
|
#### `stream(body: ReadableStream, init?: ResponseInit): Response`
|
|
207
256
|
|
package/dist/response.d.ts
CHANGED
|
@@ -73,16 +73,18 @@ export declare function javascript(body: string, init?: ResponseInit): Response;
|
|
|
73
73
|
*/
|
|
74
74
|
export declare function xml(body: string, init?: ResponseInit): Response;
|
|
75
75
|
/**
|
|
76
|
-
* Creates a Response with
|
|
77
|
-
*
|
|
76
|
+
* Creates a Response with `text/csv; charset=utf-8`, for a whole document or one streamed
|
|
77
|
+
* as it is written. The charset parameter is what makes a client decode non-ASCII cells as
|
|
78
|
+
* UTF-8 instead of its locale's legacy encoding.
|
|
79
|
+
* @param body - The CSV document, or a stream of its UTF-8 bytes
|
|
78
80
|
* @param init - Optional response options (status, headers, etc.)
|
|
79
81
|
* @returns A new Response instance with CSV content
|
|
80
82
|
* @example
|
|
81
83
|
* return csv("name,age\nJohn,30\nJane,25");
|
|
82
84
|
* @example
|
|
83
|
-
* return csv(
|
|
85
|
+
* return csv(rows, { headers: { "Content-Disposition": attachment("report.csv") } });
|
|
84
86
|
*/
|
|
85
|
-
export declare function csv(body: string
|
|
87
|
+
export declare function csv(body: string | ReadableStream<Uint8Array>, init?: ResponseInit): Response;
|
|
86
88
|
/**
|
|
87
89
|
* Creates a Response with Markdown content and appropriate Content-Type header.
|
|
88
90
|
* @param body - The Markdown string to send
|
|
@@ -107,8 +109,20 @@ export declare function markdown(body: string, init?: ResponseInit): Response;
|
|
|
107
109
|
*/
|
|
108
110
|
export declare function pdf(body: Blob | ArrayBuffer | ReadableStream, init?: ResponseInit): Response;
|
|
109
111
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
+
* An RFC 6266 `Content-Disposition: attachment` value for a download's file name. Every
|
|
113
|
+
* name gets a quoted ASCII `filename`, with characters outside printable ASCII shown as `_`;
|
|
114
|
+
* such a name also gets an RFC 8187 `filename*`, which clients prefer, carrying it exactly.
|
|
115
|
+
* @param filename - The name the download is saved as
|
|
116
|
+
* @returns The header value
|
|
117
|
+
* @example
|
|
118
|
+
* attachment("report.csv"); // 'attachment; filename="report.csv"'
|
|
119
|
+
* @example
|
|
120
|
+
* attachment("Café.csv"); // "attachment; filename=\"Caf_.csv\"; filename*=UTF-8''Caf%C3%A9.csv"
|
|
121
|
+
*/
|
|
122
|
+
export declare function attachment(filename: string): string;
|
|
123
|
+
/**
|
|
124
|
+
* Creates a Response for a file download, with the name written by `attachment` so a quote
|
|
125
|
+
* or a non-ASCII character in it reaches the client intact.
|
|
112
126
|
* @param body - The file content as Blob, ArrayBuffer, or ReadableStream
|
|
113
127
|
* @param filename - The filename to suggest for the download
|
|
114
128
|
* @param init - Optional response options (status, headers, etc.)
|
package/dist/response.js
CHANGED
|
@@ -96,18 +96,20 @@ export function xml(body, init) {
|
|
|
96
96
|
return new Response(body, { ...init, headers });
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
|
-
* Creates a Response with
|
|
100
|
-
*
|
|
99
|
+
* Creates a Response with `text/csv; charset=utf-8`, for a whole document or one streamed
|
|
100
|
+
* as it is written. The charset parameter is what makes a client decode non-ASCII cells as
|
|
101
|
+
* UTF-8 instead of its locale's legacy encoding.
|
|
102
|
+
* @param body - The CSV document, or a stream of its UTF-8 bytes
|
|
101
103
|
* @param init - Optional response options (status, headers, etc.)
|
|
102
104
|
* @returns A new Response instance with CSV content
|
|
103
105
|
* @example
|
|
104
106
|
* return csv("name,age\nJohn,30\nJane,25");
|
|
105
107
|
* @example
|
|
106
|
-
* return csv(
|
|
108
|
+
* return csv(rows, { headers: { "Content-Disposition": attachment("report.csv") } });
|
|
107
109
|
*/
|
|
108
110
|
export function csv(body, init) {
|
|
109
111
|
let headers = new Headers(init?.headers);
|
|
110
|
-
headers.set("Content-Type", ContentType.CSV);
|
|
112
|
+
headers.set("Content-Type", `${ContentType.CSV}; charset=utf-8`);
|
|
111
113
|
return new Response(body, { ...init, headers });
|
|
112
114
|
}
|
|
113
115
|
/**
|
|
@@ -142,8 +144,49 @@ export function pdf(body, init) {
|
|
|
142
144
|
return new Response(body, { ...init, headers });
|
|
143
145
|
}
|
|
144
146
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
+
* Characters outside printable ASCII, which a quoted `filename` cannot carry. Matched per
|
|
148
|
+
* code point, so an astral character becomes one fallback character.
|
|
149
|
+
*/
|
|
150
|
+
const NON_PRINTABLE_ASCII = /[^\x20-\x7E]/gu;
|
|
151
|
+
/** The two characters a quoted-string must escape with a backslash. */
|
|
152
|
+
const QUOTED_STRING_SPECIALS = /["\\]/g;
|
|
153
|
+
/** RFC 8187 `attr-char`: the bytes an `ext-value` may carry without percent-encoding. */
|
|
154
|
+
const ATTR_CHAR = /^[A-Za-z0-9!#$&+\-.^_`|~]$/;
|
|
155
|
+
/**
|
|
156
|
+
* An RFC 8187 `ext-value` for a name, percent-encoding every UTF-8 byte outside
|
|
157
|
+
* `attr-char`. A lone surrogate encodes as U+FFFD, so every string yields a valid value.
|
|
158
|
+
* @param value - The name to encode
|
|
159
|
+
* @returns The value with its `UTF-8''` prefix
|
|
160
|
+
*/
|
|
161
|
+
function extValue(value) {
|
|
162
|
+
let encoded = "UTF-8''";
|
|
163
|
+
for (let byte of new TextEncoder().encode(value)) {
|
|
164
|
+
let char = String.fromCharCode(byte);
|
|
165
|
+
encoded += ATTR_CHAR.test(char) ? char : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
166
|
+
}
|
|
167
|
+
return encoded;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* An RFC 6266 `Content-Disposition: attachment` value for a download's file name. Every
|
|
171
|
+
* name gets a quoted ASCII `filename`, with characters outside printable ASCII shown as `_`;
|
|
172
|
+
* such a name also gets an RFC 8187 `filename*`, which clients prefer, carrying it exactly.
|
|
173
|
+
* @param filename - The name the download is saved as
|
|
174
|
+
* @returns The header value
|
|
175
|
+
* @example
|
|
176
|
+
* attachment("report.csv"); // 'attachment; filename="report.csv"'
|
|
177
|
+
* @example
|
|
178
|
+
* attachment("Café.csv"); // "attachment; filename=\"Caf_.csv\"; filename*=UTF-8''Caf%C3%A9.csv"
|
|
179
|
+
*/
|
|
180
|
+
export function attachment(filename) {
|
|
181
|
+
let fallback = filename.replaceAll(NON_PRINTABLE_ASCII, "_");
|
|
182
|
+
let value = `attachment; filename="${fallback.replaceAll(QUOTED_STRING_SPECIALS, "\\$&")}"`;
|
|
183
|
+
if (fallback !== filename)
|
|
184
|
+
value += `; filename*=${extValue(filename)}`;
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Creates a Response for a file download, with the name written by `attachment` so a quote
|
|
189
|
+
* or a non-ASCII character in it reaches the client intact.
|
|
147
190
|
* @param body - The file content as Blob, ArrayBuffer, or ReadableStream
|
|
148
191
|
* @param filename - The filename to suggest for the download
|
|
149
192
|
* @param init - Optional response options (status, headers, etc.)
|
|
@@ -156,7 +199,7 @@ export function pdf(body, init) {
|
|
|
156
199
|
export function file(body, filename, init) {
|
|
157
200
|
let headers = new Headers(init?.headers);
|
|
158
201
|
headers.set("Content-Type", ContentType.OctetStream);
|
|
159
|
-
headers.set("Content-Disposition",
|
|
202
|
+
headers.set("Content-Disposition", attachment(filename));
|
|
160
203
|
return new Response(body, { ...init, headers });
|
|
161
204
|
}
|
|
162
205
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/http",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.30",
|
|
4
4
|
"description": "Response builders, content negotiation, and HTTP caching for the Fetch API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"@sdxc/duration": "2026.9.15",
|
|
20
20
|
"@sdxc/location": "2026.9.11",
|
|
21
21
|
"@sdxc/result": "2026.9.15",
|
|
22
|
-
"remix": "3.0.0-rc.
|
|
22
|
+
"remix": "3.0.0-rc.4"
|
|
23
23
|
},
|
|
24
|
-
"gitHead": "
|
|
24
|
+
"gitHead": "81432d2333817dfd7ee61634baff813432ecee9c",
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|