Skip to content

Commit d4f26be

Browse files
authored
feat: formData support (#17)
1 parent a9768ed commit d4f26be

18 files changed

+530
-183
lines changed

.github/workflows/fetch.yml

+29-28
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@ on:
1414
- ".github/workflows/fetch.yml"
1515

1616
jobs:
17+
check:
18+
name: Typecheck
19+
runs-on: ubuntu-latest
20+
strategy:
21+
matrix:
22+
node-version:
23+
- 16
24+
project:
25+
- fetch
26+
steps:
27+
- uses: actions/checkout@v2
28+
29+
- name: Setup node ${{ matrix.node-version }}
30+
uses: actions/setup-node@v2
31+
with:
32+
node-version: ${{ matrix.node-version }}
33+
34+
- name: Install
35+
uses: bahmutov/npm-install@v1
36+
37+
- name: Typecheck
38+
uses: gozala/[email protected]
39+
with:
40+
project: ${{matrix.project}}/tsconfig.json
41+
1742
test:
1843
name: Test
1944
runs-on: ${{ matrix.os }}
@@ -22,7 +47,7 @@ jobs:
2247
matrix:
2348
node-version:
2449
- 14
25-
- 15
50+
- 16
2651
os:
2752
- ubuntu-latest
2853
- windows-latest
@@ -44,20 +69,8 @@ jobs:
4469
with:
4570
node-version: ${{ matrix.node-version }}
4671

47-
- name: Restore yarn cache
48-
id: yarn-cache-dir-path
49-
run: echo "::set-output name=dir::$(yarn cache dir)"
50-
51-
- uses: actions/cache@v2
52-
id: yarn-cache
53-
with:
54-
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
55-
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
56-
restore-keys: |
57-
${{ runner.os }}-yarn-
58-
59-
- name: Install dependencies
60-
run: yarn install
72+
- name: Install
73+
uses: bahmutov/npm-install@v1
6174

6275
- name: Test
6376
run: yarn --cwd ${{matrix.project}} test -- --colors
@@ -81,20 +94,8 @@ jobs:
8194
steps:
8295
- uses: actions/checkout@v2
8396

84-
- name: Restore yarn cache
85-
id: yarn-cache-dir-path
86-
run: echo "::set-output name=dir::$(yarn cache dir)"
87-
88-
- uses: actions/cache@v2
89-
id: yarn-cache
90-
with:
91-
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
92-
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
93-
restore-keys: |
94-
${{ runner.os }}-yarn-
95-
9697
- name: Install
97-
run: yarn install
98+
uses: bahmutov/npm-install@v1
9899

99100
- name: Build
100101
run: yarn --cwd ${{matrix.project}} build

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fetch

fetch/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
# Generated files
55
dist/
6+
@types/
7+
*.tsbuildinfo
68

79
# Logs
810
logs

fetch/package.json

+9-6
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
"exports": {
1010
".": {
1111
"import": "./src/index.js",
12-
"require": "./dist/index.cjs"
12+
"require": "./dist/index.cjs",
13+
"types": "./@types/index.d.ts"
1314
},
1415
"./package.json": "./package.json"
1516
},
1617
"files": [
1718
"src",
1819
"dist",
19-
"@types/index.d.ts"
20+
"@types"
2021
],
2122
"types": "./@types/index.d.ts",
2223
"engines": {
@@ -26,9 +27,9 @@
2627
"build": "rollup -c",
2728
"test": "node --experimental-modules ../node_modules/c8/bin/c8 --reporter=html --reporter=lcov --reporter=text --check-coverage node --experimental-modules ../node_modules/mocha/bin/mocha",
2829
"coverage": "c8 report --reporter=text-lcov | coveralls",
29-
"test-types": "tsd",
30+
"typecheck": "tsc --build",
3031
"lint": "xo",
31-
"prepublishOnly": "node ./test/commonjs/test-artifact.js"
32+
"prepublishOnly": "node ./test/commonjs/test-artifact.js && tsc --build"
3233
},
3334
"repository": {
3435
"type": "git",
@@ -67,12 +68,14 @@
6768
"p-timeout": "^3.2.0",
6869
"rollup": "^2.26.10",
6970
"tsd": "^0.13.1",
70-
"xo": "^0.33.1"
71+
"xo": "^0.33.1",
72+
"typescript": "^4.4.4"
7173
},
7274
"dependencies": {
7375
"@web-std/blob": "^2.1.0",
7476
"data-uri-to-buffer": "^3.0.1",
75-
"web-streams-polyfill": "^3.0.2"
77+
"web-streams-polyfill": "^3.1.1",
78+
"@ssttevee/multipart-parser": "^0.1.9"
7679
},
7780
"esm": {
7881
"sourceMap": true,

fetch/src/body.js

+46-17
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ import WebStreams from 'web-streams-polyfill';
1313

1414
import {FetchError} from './errors/fetch-error.js';
1515
import {FetchBaseError} from './errors/base.js';
16-
import {formDataIterator, getBoundary, getFormDataLength} from './utils/form-data.js';
16+
import {formDataIterator, getBoundary, getFormDataLength, toFormData} from './utils/form-data.js';
1717
import {isBlob, isURLSearchParameters, isFormData, isMultipartFormDataStream, isReadableStream} from './utils/is.js';
1818
import * as utf8 from './utils/utf8.js';
19-
2019
const {readableHighWaterMark} = new Stream.Readable();
2120

2221
const {ReadableStream} = WebStreams;
@@ -26,15 +25,12 @@ const INTERNALS = Symbol('Body internals');
2625
* Body mixin
2726
*
2827
* Ref: https://fetch.spec.whatwg.org/#body
29-
*
30-
* @param {BodyInit} body Readable stream
31-
* @param Object opts Response options
32-
* @return Void
28+
* @implements {globalThis.Body}
3329
*/
3430

3531
export default class Body {
3632
/**
37-
* @param {BodyInit|Stream} body
33+
* @param {BodyInit|Stream|null} body
3834
* @param {{size?:number}} options
3935
*/
4036
constructor(body, {
@@ -123,7 +119,7 @@ export default class Body {
123119
/** @type {Headers|undefined} */
124120
/* c8 ignore next 3 */
125121
get headers() {
126-
return null;
122+
return undefined;
127123
}
128124

129125
get body() {
@@ -176,6 +172,14 @@ export default class Body {
176172
const buffer = await consumeBody(this);
177173
return utf8.decode(buffer);
178174
}
175+
176+
/**
177+
* @returns {Promise<FormData>}
178+
*/
179+
180+
async formData() {
181+
return toFormData(this)
182+
}
179183
}
180184

181185
// In browsers, all properties are enumerable.
@@ -185,7 +189,8 @@ Object.defineProperties(Body.prototype, {
185189
arrayBuffer: {enumerable: true},
186190
blob: {enumerable: true},
187191
json: {enumerable: true},
188-
text: {enumerable: true}
192+
text: {enumerable: true},
193+
formData: {enumerable: true}
189194
});
190195

191196
/**
@@ -217,8 +222,9 @@ async function consumeBody(data) {
217222

218223
// Body is stream
219224
// get ready to actually consume the body
225+
/** @type {[Uint8Array|null, Uint8Array[], number]} */
220226
const [buffer, chunks, limit] = data.size > 0 ?
221-
[new Uint8Array(data.size), null, data.size] :
227+
[new Uint8Array(data.size), [], data.size] :
222228
[null, [], Infinity];
223229
let offset = 0;
224230

@@ -244,7 +250,7 @@ async function consumeBody(data) {
244250

245251
if (buffer) {
246252
if (offset < buffer.byteLength) {
247-
throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`);
253+
throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`, 'premature-close');
248254
} else {
249255
return buffer;
250256
}
@@ -254,11 +260,13 @@ async function consumeBody(data) {
254260
} catch (error) {
255261
if (error instanceof FetchBaseError) {
256262
throw error;
263+
// @ts-expect-error - we know it will have a name
257264
} else if (error && error.name === 'AbortError') {
258265
throw error;
259266
} else {
267+
const e = /** @type {import('./errors/fetch-error').SystemError} */(error)
260268
// Other errors, such as incorrect content-encoding
261-
throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error);
269+
throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${e.message}`, 'system', e);
262270
}
263271
}
264272
}
@@ -277,6 +285,7 @@ export const clone = instance => {
277285
throw new Error('cannot clone body after it is used');
278286
}
279287

288+
// @ts-expect-error - could be null
280289
const [left, right] = body.tee();
281290
instance[INTERNALS].body = left;
282291
return right;
@@ -332,7 +341,6 @@ class StreamIterableIterator {
332341
constructor(stream) {
333342
this.stream = stream;
334343
this.reader = null;
335-
this.state = null;
336344
}
337345

338346
/**
@@ -359,6 +367,9 @@ class StreamIterableIterator {
359367
return /** @type {Promise<IteratorResult<T, void>>} */ (this.getReader().read());
360368
}
361369

370+
/**
371+
* @returns {Promise<IteratorResult<T, void>>}
372+
*/
362373
async return() {
363374
if (this.reader) {
364375
await this.reader.cancel();
@@ -367,6 +378,11 @@ class StreamIterableIterator {
367378
return {done: true, value: undefined};
368379
}
369380

381+
/**
382+
*
383+
* @param {any} error
384+
* @returns {Promise<IteratorResult<T, void>>}
385+
*/
370386
async throw(error) {
371387
await this.getReader().cancel(error);
372388
return {done: true, value: undefined};
@@ -429,7 +445,7 @@ class AsyncIterablePump {
429445
*/
430446
async pull(controller) {
431447
try {
432-
while (controller.desiredSize > 0) {
448+
while (controller.desiredSize || 0 > 0) {
433449
// eslint-disable-next-line no-await-in-loop
434450
const next = await this.source.next();
435451
if (next.done) {
@@ -444,6 +460,9 @@ class AsyncIterablePump {
444460
}
445461
}
446462

463+
/**
464+
* @param {any} [reason]
465+
*/
447466
cancel(reason) {
448467
if (reason) {
449468
if (typeof this.source.throw === 'function') {
@@ -463,8 +482,8 @@ class AsyncIterablePump {
463482
*/
464483
export const fromStream = source => {
465484
const pump = new StreamPump(source);
466-
const stream =
467-
/** @type {ReadableStream<Uint8Array>} */(new ReadableStream(pump, pump));
485+
const stream = new ReadableStream(pump, pump);
486+
// @ts-ignore - web-streams-polyfill API is incompatible
468487
return stream;
469488
};
470489

@@ -490,6 +509,10 @@ class StreamPump {
490509
this.close = this.close.bind(this);
491510
}
492511

512+
/**
513+
* @param {Uint8Array} chunk
514+
* @returns
515+
*/
493516
size(chunk) {
494517
return chunk.byteLength;
495518
}
@@ -509,6 +532,9 @@ class StreamPump {
509532
this.resume();
510533
}
511534

535+
/**
536+
* @param {any} [reason]
537+
*/
512538
cancel(reason) {
513539
if (this.stream.destroy) {
514540
this.stream.destroy(reason);
@@ -530,7 +556,7 @@ class StreamPump {
530556
chunk :
531557
Buffer.from(chunk);
532558

533-
const available = this.controller.desiredSize - bytes.byteLength;
559+
const available = (this.controller.desiredSize || 0) - bytes.byteLength;
534560
this.controller.enqueue(bytes);
535561
if (available <= 0) {
536562
this.pause();
@@ -561,6 +587,9 @@ class StreamPump {
561587
}
562588
}
563589

590+
/**
591+
* @param {Error} error
592+
*/
564593
error(error) {
565594
if (this.controller) {
566595
this.controller.error(error);

fetch/src/errors/abort-error.js

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import {FetchBaseError} from './base.js';
44
* AbortError interface for cancelled requests
55
*/
66
export class AbortError extends FetchBaseError {
7+
/**
8+
* @param {string} message
9+
* @param {string} [type]
10+
*/
711
constructor(message, type = 'aborted') {
812
super(message, type);
913
}

fetch/src/errors/base.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
'use strict';
22

33
export class FetchBaseError extends Error {
4+
/**
5+
* @param {string} message
6+
* @param {string} type
7+
*/
48
constructor(message, type) {
59
super(message);
610
// Hide custom error implementation details from end-users

0 commit comments

Comments
 (0)