fch 3.0.6 → 4.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.
Files changed (4) hide show
  1. package/package.json +23 -11
  2. package/readme.md +219 -126
  3. package/fetch.js +0 -202
  4. package/fetch.min.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fch",
3
- "version": "3.0.6",
3
+ "version": "4.1.0",
4
4
  "description": "Fetch interface with better promises, deduplication, defaults, etc.",
5
5
  "homepage": "https://github.com/franciscop/fetch",
6
6
  "repository": "https://github.com/franciscop/fetch.git",
@@ -9,7 +9,8 @@
9
9
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
10
10
  "license": "MIT",
11
11
  "scripts": {
12
- "build": "terser --compress --mangle -- ./fetch.js | sed 's/export default fch;/window.fch=fch;/g' > ./fetch.min.js",
12
+ "build": "rollup -c",
13
+ "size": "echo $(gzip -c index.min.js | wc -c) bytes",
13
14
  "start": "npm run watch # Start ~= Start dev",
14
15
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles",
15
16
  "watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles"
@@ -22,24 +23,35 @@
22
23
  "async",
23
24
  "ajax"
24
25
  ],
25
- "main": "./fetch.js",
26
- "module": "./fetch.js",
27
- "browser": "./fetch.min.js",
28
- "files": [
29
- "fetch.js",
30
- "fetch.min.js"
31
- ],
26
+ "main": "./index.min.js",
27
+ "files": [],
32
28
  "type": "module",
33
29
  "engines": {
34
30
  "node": ">=18.0.0"
35
31
  },
36
32
  "devDependencies": {
37
- "jest": "^28.0.1",
33
+ "@babel/core": "^7.15.0",
34
+ "@babel/preset-env": "^7.15.0",
35
+ "@babel/preset-react": "^7.14.5",
36
+ "babel-loader": "^8.2.2",
37
+ "babel-polyfill": "^6.26.0",
38
+ "jest": "^28.1.0",
39
+ "jest-environment-jsdom": "^28.1.0",
38
40
  "jest-fetch-mock": "^3.0.3",
39
- "terser": "^5.13.1"
41
+ "rollup": "^1.32.1",
42
+ "rollup-plugin-babel": "^4.4.0",
43
+ "rollup-plugin-node-resolve": "^5.2.0",
44
+ "rollup-plugin-terser": "^5.2.0",
45
+ "swear": "^1.1.2"
40
46
  },
41
47
  "jest": {
42
48
  "testEnvironment": "jest-environment-node",
43
49
  "transform": {}
50
+ },
51
+ "babel": {
52
+ "presets": [
53
+ "@babel/preset-env",
54
+ "@babel/preset-react"
55
+ ]
44
56
  }
45
57
  }
package/readme.md CHANGED
@@ -3,47 +3,54 @@
3
3
  A tiny library to make API calls easier. Similar to Axios, but tiny size and simpler API:
4
4
 
5
5
  ```js
6
- import api from "fch";
7
- api.baseUrl = "https://pokeapi.co/";
8
- const mew = await api.get("/pokemon/150");
6
+ // Plain usage
7
+ import fch from "fch";
8
+ const mew = await fch("https://pokeapi.co/pokemon/150");
9
9
  console.log(mew);
10
+
11
+ // As an API abstraction
12
+ const api = fch.create({ baseUrl: "https://pokeapi.co/" });
13
+ const mew = await api.get("/pokemon/150");
14
+ await api.patch("/pokemon/150", { body: { type: "psychic" } });
10
15
  ```
11
16
 
12
17
  - Create instances with shared options across requests.
13
18
  - Automatically encode object and array bodies as JSON.
14
19
  - Automatically decode JSON responses based on the headers.
15
20
  - Await/Async Promises; `>= 400 and <= 100` will _reject_ with an error.
21
+ - Credentials: "include" by default
16
22
  - Interceptors: `before` the request, `after` the response and catch with `error`.
17
23
  - Deduplicates parallel GET requests.
18
24
  - Works the same way in Node.js and the browser.
19
25
  - No dependencies; include it with a simple `<script>` on the browser.
20
26
 
21
27
  ```js
22
- import api from 'fch';
23
-
24
- api.get(url, { headers, ...options })
25
- api.head(url, { headers, ...options })
26
- api.post(url, { body, headers, ...options })
27
- api.patch(url, { body, headers, ...options })
28
- api.put(url, { body, headers, ...options })
29
- api.del(url, { body, headers, ...options })
30
-
31
- api.create({ url, body, headers, ...options})
32
- ```
28
+ import api from "fch";
33
29
 
34
- | Options | Default | Description |
35
- | ----------------------| ----------| -----------------------------------|
36
- | [`method`](#method) | `"get"` | Default method to use for the call |
37
- | [`url`](#url) | `null` | The path or url for the request |
38
- | [`baseUrl`](#url) | `null` | The shared base of the API |
39
- | [`body`](#body) | `null` | The body to send with the request |
40
- | [`query`](#query) | `{}` | Add query parameters to the URL |
41
- | [`headers`](#headers) | `{}` | Shared headers across all requests |
42
- | [`output`](#output) | `"body"` | The return value of the API call |
43
- | [`dedupe`](#dedupe) | `true` | Reuse concurrently GET requests |
44
- | [`before`](#interceptors) |`req => req` |Process the request before sending it |
45
- | [`after`](#interceptors) |`res => res` |Process the response before returning it |
46
- | [`error`](#interceptors) |`err => throw err` |Process errors before returning them |
30
+ api.get(url, { headers, ...options });
31
+ api.head(url, { headers, ...options });
32
+ api.post(url, { body, headers, ...options });
33
+ api.patch(url, { body, headers, ...options });
34
+ api.put(url, { body, headers, ...options });
35
+ api.del(url, { body, headers, ...options });
36
+ api.delete(url, { body, headers, ...options });
37
+
38
+ api.create({ url, body, headers, ...options });
39
+ ```
40
+
41
+ | Options | Default | Description |
42
+ | ------------------------- | ------------------ | ---------------------------------------- |
43
+ | [`method`](#method) | `"get"` | Default method to use for the call |
44
+ | [`url`](#url) | `"/"` | The path or url for the request |
45
+ | [`baseUrl`](#url) | `null` | The shared base of the API |
46
+ | [`body`](#body) | `null` | The body to send with the request |
47
+ | [`query`](#query) | `{}` | Add query parameters to the URL |
48
+ | [`headers`](#headers) | `{}` | Shared headers across all requests |
49
+ | [`output`](#output) | `"body"` | The return value of the API call |
50
+ | [`dedupe`](#dedupe) | `true` | Reuse concurrently GET requests |
51
+ | [`before`](#interceptors) | `req => req` | Process the request before sending it |
52
+ | [`after`](#interceptors) | `res => res` | Process the response before returning it |
53
+ | [`error`](#interceptors) | `err => throw err` | Process errors before returning them |
47
54
 
48
55
  ## Getting Started
49
56
 
@@ -56,17 +63,17 @@ npm install fch
56
63
  Then import it to be able to use it in your code:
57
64
 
58
65
  ```js
59
- import fch from 'fch';
60
- const body = await fch.get('/');
66
+ import fch from "fch";
67
+ const body = await fch.get("/");
61
68
  ```
62
69
 
63
70
  On the browser you can add it with a script and it will be available as `fch`:
64
71
 
65
72
  ```html
66
73
  <!-- Import it as usual -->
67
- <script src="https://cdn.jsdelivr.net/npm/fch"></script>
68
- <script>
69
- fch('/hello');
74
+ <script src="https://cdn.jsdelivr.net/npm/fch" type="module"></script>
75
+ <script type="module">
76
+ fch("/hello");
70
77
  </script>
71
78
  ```
72
79
 
@@ -75,23 +82,23 @@ On the browser you can add it with a script and it will be available as `fch`:
75
82
  These are all available options and their defaults:
76
83
 
77
84
  ```js
78
- import api from 'fch';
85
+ import api from "fch";
79
86
 
80
87
  // General options with their defaults; all of these are also parameters:
81
- api.method = 'get'; // Default method to use for api()
82
- api.url = '/'; // Relative or absolute url where the request is sent
83
- api.baseUrl = null; // Set an API base URL reused all across requests
84
- api.query = {}; // Merged with the query parameters passed manually
85
- api.headers = {}; // Merged with the headers on a per-request basis
88
+ api.method = "get"; // Default method to use for api()
89
+ api.url = "/"; // Relative or absolute url where the request is sent
90
+ api.baseUrl = null; // Set an API base URL reused all across requests
91
+ api.query = {}; // Merged with the query parameters passed manually
92
+ api.headers = {}; // Merged with the headers on a per-request basis
86
93
 
87
94
  // Control simple variables
88
- api.output = 'body'; // Return the body; use 'response' for the full response
89
- api.dedupe = true; // Avoid sending concurrent GET requests to the same path
95
+ api.output = "body"; // Return the parsed body; use 'response' or 'stream' otherwise
96
+ api.dedupe = true; // Avoid sending concurrent GET requests to the same path
90
97
 
91
98
  // Interceptors
92
- api.before = req => req;
93
- api.after = res => res;
94
- api.error = err => Promise.reject(err);
99
+ api.before = (req) => req;
100
+ api.after = (res) => res;
101
+ api.error = (err) => Promise.reject(err);
95
102
  ```
96
103
 
97
104
  They can all be defined globally as shown above, passed manually as the options argument, or be used when [creating a new instance](#create-an-instance).
@@ -101,11 +108,11 @@ They can all be defined globally as shown above, passed manually as the options
101
108
  The HTTP method to make the request. When using the shorthand, it defaults to `GET`. We recommend using the method syntax:
102
109
 
103
110
  ```js
104
- import api from 'fch';
111
+ import api from "fch";
105
112
 
106
- api.get('/cats');
107
- api.post('/cats', { body: { name: 'snowball' } });
108
- api.put(`/cats/3`, { body: { name: 'snowball' }});
113
+ api.get("/cats");
114
+ api.post("/cats", { body: { name: "snowball" } });
115
+ api.put(`/cats/3`, { body: { name: "snowball" } });
109
116
  ```
110
117
 
111
118
  You can use it with the plain function as an option parameter. The methods are all lowercase but the option as a parameter is case insensitive; it can be either uppercase or lowercase:
@@ -125,12 +132,12 @@ api({ method; 'get'})
125
132
  Example: adding a new cat and fixing a typo:
126
133
 
127
134
  ```js
128
- import api from 'fch';
135
+ import api from "fch";
129
136
 
130
- const cats = await api.get('/cats');
137
+ const cats = await api.get("/cats");
131
138
  console.log(cats);
132
- const { id } = await api.post('/cats', { body: { name: 'snowbll' } });
133
- await api.put(`/cats/${id}`, { body: { name: 'snowball' }})
139
+ const { id } = await api.post("/cats", { body: { name: "snowbll" } });
140
+ await api.put(`/cats/${id}`, { body: { name: "snowball" } });
134
141
  ```
135
142
 
136
143
  ### Url
@@ -138,23 +145,23 @@ await api.put(`/cats/${id}`, { body: { name: 'snowball' }})
138
145
  Specify where to send the request to. It's normally the first argument, though technically you can use both styles:
139
146
 
140
147
  ```js
141
- import api from 'fch';
148
+ import api from "fch";
142
149
 
143
150
  // Recommended way of specifying the Url
144
- await api.post('/hello', { body: '...', headers: {} })
151
+ await api.post("/hello", { body: "...", headers: {} });
145
152
 
146
153
  // These are also valid if you prefer their style; we won't judge
147
- await api('/hello', { method: 'post', body: '...', headers: {} });
148
- await api({ url: '/hello', method: 'post', headers: {}, body: '...' });
149
- await api.post({ url: '/hello', headers: {}, body: '...' });
154
+ await api("/hello", { method: "post", body: "...", headers: {} });
155
+ await api({ url: "/hello", method: "post", headers: {}, body: "..." });
156
+ await api.post({ url: "/hello", headers: {}, body: "..." });
150
157
  ```
151
158
 
152
159
  It can be either absolute or relative, in which case it'll use the local one in the page. It's recommending to set `baseUrl`:
153
160
 
154
161
  ```js
155
- import api from 'fch';
156
- api.baseUrl = 'https//api.filemon.io/';
157
- api.get('/hello');
162
+ import api from "fch";
163
+ api.baseUrl = "https//api.filemon.io/";
164
+ api.get("/hello");
158
165
  // Called https//api.filemon.io/hello
159
166
  ```
160
167
 
@@ -183,8 +190,10 @@ The methods `GET` and `HEAD` do not accept a body and it'll be ignored.
183
190
 
184
191
  The **response body** will be returned by default as the output of the call:
185
192
 
193
+ > See more info in [**Output**](#output)
194
+
186
195
  ```js
187
- const body = await api.get('/cats');
196
+ const body = await api.get("/cats");
188
197
  console.log(body);
189
198
  // [{ id: 1, }, ...]
190
199
  ```
@@ -194,51 +203,48 @@ When the server specifies the header `Content-Type` as `application/json`, then
194
203
  When the function returns the response (if you set `output: "response"` as an option), then the body can be accessed as `response.body`:
195
204
 
196
205
  ```js
197
- const response = await api.get('/cats', { output: 'response' });
206
+ const response = await api.get("/cats", { output: "response" });
198
207
  console.log(response.body);
199
208
  // [{ id: 1, }, ...]
200
209
  ```
201
210
 
202
-
203
211
  ### Query
204
212
 
205
213
  You can easily pass GET query parameters by using the option `query`:
206
214
 
207
215
  ```js
208
- api.get('/cats', { query: { limit: 3 } });
216
+ api.get("/cats", { query: { limit: 3 } });
209
217
  // /cats?limit=3
210
218
  ```
211
219
 
212
220
  While rare, some times you might want to persist a query parameter across requests and always include it; in that case, you can define it globally and it'll be added to every request:
213
221
 
214
222
  ```js
215
- import api from 'fch';
216
- api.query.myparam = 'abc';
223
+ import api from "fch";
224
+ api.query.myparam = "abc";
217
225
 
218
- api.get('/cats', { query: { limit: 3 } });
226
+ api.get("/cats", { query: { limit: 3 } });
219
227
  // /cats?limit=3&myparam=abc
220
228
  ```
221
229
 
222
-
223
230
  ### Headers
224
231
 
225
232
  You can define headers globally, in which case they'll be added to every request, or locally, so that they are only added to the current request. You can also add them in the `before` callback:
226
233
 
227
-
228
234
  ```js
229
- import api from 'fch';
235
+ import api from "fch";
230
236
 
231
237
  // Globally, so they are reused across all requests
232
- api.headers.a = 'b';
238
+ api.headers.a = "b";
233
239
 
234
240
  // With an interceptor, in case you need dynamic headers per-request
235
- api.before = req => {
236
- req.headers.c = 'd';
241
+ api.before = (req) => {
242
+ req.headers.c = "d";
237
243
  return req;
238
244
  };
239
245
 
240
246
  // Set them for this single request:
241
- api.get('/hello', { headers: { e: 'f' } });
247
+ api.get("/hello", { headers: { e: "f" } });
242
248
  // Total headers on the request:
243
249
  // { a: 'b', c: 'd', e: 'f' }
244
250
  ```
@@ -249,31 +255,75 @@ When to use each?
249
255
  - When you need to extract them dynamically from somewhere it's better to use the .before() interceptor. An example would be the user Authorization token.
250
256
  - When it changes on each request, it's not consistent or it's an one-off, use the option argument.
251
257
 
252
-
253
258
  ### Output
254
259
 
255
- This controls whether the call returns just the body (default), or the whole response. It can be controlled globally or on a per-request basis:
260
+ The default output manipulation is to expect either plan `TEXT` as `plain/text` or `JSON` as `application/json` from the `Content-Type`. If your API works with these (the vast majority of APIs do) then you should be fine out of the box!
261
+
262
+ ```js
263
+ const cats = await api.get("/cats");
264
+ console.log(cats); // [{ id: 1, name: 'Whiskers', ... }, ...]
265
+ ```
266
+
267
+ For more expressive control, you can use the **`output` option** (either as a default when [creating an instance](#create-an-instance) or with each call), or using a method:
256
268
 
257
269
  ```js
258
- import api from 'fch';
270
+ const api = fch.create({ output: "json" }); // JSON by default
271
+ const streamImg = await api.get("/cats/123/image", { output: "stream" }); // Stream the image
272
+ const streamImg2 = await api.get("/cats/123/image").stream(); // Shortcut for the one above
273
+ ```
259
274
 
260
- // "body" (default) or "response"
261
- api.output = 'body';
275
+ This controls whether the call returns just the body (default), or the whole response. It can be controlled globally or on a per-request basis:
262
276
 
263
- // Return only the body, this is the default
264
- const body = await api.get('/data');
277
+ ```js
278
+ // Return only the body (default)
279
+ const body = await api.get("/data");
265
280
 
266
281
  // Return the whole response (with .body):
267
- const response = await api.get('/data', { output: 'response' });
282
+ const response = await api.get("/data", { output: "response" });
283
+
284
+ // Return a plain body stream
285
+ const stream = await api.get("/data", { output: "stream" });
286
+ stream.pipeTo(outStream);
268
287
 
269
- // Throws error
270
- const invalid = await api.get('/data', { output: 'invalid' });
288
+ // Return a blob, since `response.blob()` is available:
289
+ const blob = await api.get("/data", { output: "blob" });
271
290
  ```
272
291
 
292
+ There are few options that can be specified:
293
+
294
+ - `output: "body"` (default): returns the body, parsed as JSON or plain TEXT depending on the headers.
295
+ - `output: "response"`: return the full response with the properties `body`, `headers`, `status`. The body will be parsed as JSON or plain TEXT depending on the headers. If you want the raw `response`, use `raw` or `clone` instead (see below in "raw" or "clone").
296
+ - `output: "stream"`: return a [web ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) of the body as the result of the promise.
297
+ - `output: "arrayBuffer"`\*: returns an arrayBuffer of the response body.
298
+ - `output: "blob"`\*: returns an arrayBuffer of the response body.
299
+ - `output: "clone"`\*: returns the raw Response, with the raw body. See also `raw` below.
300
+ - `output: "formData"`\* (might be unavailable): returns an instance of FormData with all the parsed data.
301
+ - `output: "json"`\*: attempts to parse the response as JSON.
302
+ - `output: "text"`\*: puts the response body as plain text.
303
+ - `output: "raw"`\*: an alias for `clone`, returning the raw response (after passing through `after`).
304
+
305
+ \* Standard [MDN methods](https://developer.mozilla.org/en-US/docs/Web/API/Response#methods)
306
+
307
+ The `output` values can all be used as a method as well. So all of these are equivalent:
308
+
309
+ ```js
310
+ const text = await api.get("/cats", { output: "text" });
311
+ const text = await api.get("/cats").text();
312
+
313
+ const raw = await api.get("/cats", { output: "raw" });
314
+ const raw = await api.get("/cats").raw();
315
+ ```
316
+
317
+ For example, return the raw body as a `ReadableStream` with the option `stream`:
318
+
319
+ ```js
320
+ const stream = await api.get('/cats', { output: 'stream' });
321
+ stream.pipeTo(...);
322
+ ```
273
323
 
274
324
  ### Dedupe
275
325
 
276
- When you perform a GET request to a given URL, but another GET request *to the same* URL is ongoing, it'll **not** create a new request and instead reuse the response when the first one is finished:
326
+ When you perform a GET request to a given URL, but another GET request _to the same_ URL is ongoing, it'll **not** create a new request and instead reuse the response when the first one is finished:
277
327
 
278
328
  ```js
279
329
  fetch.mockOnce("a").mockOnce("b");
@@ -287,12 +337,12 @@ You can disable this by setting either the global `fch.dedupe` option to `false`
287
337
 
288
338
  ```js
289
339
  // Globally set it for all calls
290
- fch.dedupe = true; // [DEFAULT] Dedupes GET requests
340
+ fch.dedupe = true; // [DEFAULT] Dedupes GET requests
291
341
  fch.dedupe = false; // All fetch() calls trigger a network call
292
342
 
293
343
  // Set it on a per-call basis
294
- fch('/a', { dedupe: true }); // [DEFAULT] Dedupes GET requests
295
- fch('/a', { dedupe: false }) // All fetch() calls trigger a network call
344
+ fch("/a", { dedupe: true }); // [DEFAULT] Dedupes GET requests
345
+ fch("/a", { dedupe: false }); // All fetch() calls trigger a network call
296
346
  ```
297
347
 
298
348
  > We do not support deduping other methods besides `GET` right now
@@ -305,7 +355,7 @@ it("can opt out locally", async () => {
305
355
  const res = await Promise.all([
306
356
  fch("/a"),
307
357
  fch("/a", { dedupe: false }),
308
- fch("/a"), // Reuses the 1st response, not the 2nd one
358
+ fch("/a"), // Reuses the 1st response, not the 2nd one
309
359
  ]);
310
360
 
311
361
  expect(res).toEqual(["a", "b", "a"]);
@@ -317,6 +367,12 @@ it("can opt out locally", async () => {
317
367
 
318
368
  You can also easily add the interceptors `before`, `after` and `error`:
319
369
 
370
+ - `before`: Called when the request is fully formed, but before actually launching it.
371
+ - `after`: Called just after the response is created and if there was no error, but before parsing anything else.
372
+ - `error`: When the response is not okay, if possible it'll include the `response` object.
373
+
374
+ > Note: interceptors are never deduped/cached and always execute once per call, even if the main async fetch() has been deduped.
375
+
320
376
  ```js
321
377
  // Perform an action or request transformation before the request is sent
322
378
  fch.before = async req => {
@@ -346,7 +402,6 @@ fch.error = async err => {
346
402
  };
347
403
  ```
348
404
 
349
-
350
405
  ## How to
351
406
 
352
407
  ### Stop errors from throwing
@@ -354,10 +409,11 @@ fch.error = async err => {
354
409
  While you can handle this on a per-request basis, if you want to overwrite the global behavior you can write a interceptor:
355
410
 
356
411
  ```js
357
- import fch from 'fch';
358
- fch.error = error => error.response;
412
+ import fch from "fch";
413
+ fch.output = "response";
414
+ fch.error = (error) => error.response;
359
415
 
360
- const res = await fch('/notfound');
416
+ const res = await fch("/notfound");
361
417
  expect(res.status).toBe(404);
362
418
  ```
363
419
 
@@ -366,75 +422,83 @@ expect(res.status).toBe(404);
366
422
  By default a successful request will just return the data. However this one is configurable on a global level:
367
423
 
368
424
  ```js
369
- import fch from 'fch';
370
- fch.output = 'response'; // Valid values are 'body' (default) or 'response'
425
+ import fch from "fch";
426
+ fch.output = "response";
371
427
 
372
- const res = await fch('/hello');
428
+ const res = await fch("/hello");
429
+ console.log(res.status);
373
430
  ```
374
431
 
375
432
  Or on a per-request level:
376
433
 
377
434
  ```js
378
- import fch from 'fch';
435
+ import fch from "fch";
379
436
 
380
437
  // Valid values are 'body' (default) or 'response'
381
- const res = await fch('/hello', { output: 'response' });
438
+ const res = await fch("/hello", { output: "response" });
382
439
 
383
440
  // Does not affect others
384
- const body = await fch('/hello');
441
+ const body = await fch("/hello");
385
442
  ```
386
443
 
444
+ It does perform some basic parsing of the `body`, if you don't want any of that you can retrieve the very raw response:
445
+
446
+ ```js
447
+ import fch from "fch";
448
+
449
+ // Valid values are 'body' (default) or 'response'
450
+ const res = await fch("/hello", { output: "raw" });
451
+ console.log(res.body); // ReadableStream
452
+ ```
387
453
 
388
454
  ### Set a base URL
389
455
 
390
456
  There's a configuration parameter for that:
391
457
 
392
458
  ```js
393
- import fch from 'fch';
394
- fch.baseUrl = 'https://api.filemon.io/';
459
+ import fch from "fch";
460
+ fch.baseUrl = "https://api.filemon.io/";
395
461
 
396
462
  // Calls "https://api.filemon.io/blabla"
397
- const body = await fch.get('/blabla');
463
+ const body = await fch.get("/blabla");
398
464
  ```
399
465
 
400
-
401
466
  ### Set authorization headers
402
467
 
403
468
  You can set that globally as a header:
404
469
 
405
470
  ```js
406
- import fch from 'fch';
407
- fch.headers.Authorization = 'bearer abc';
471
+ import fch from "fch";
472
+ fch.headers.Authorization = "bearer abc";
408
473
 
409
- const me = await fch('/users/me');
474
+ const me = await fch("/users/me");
410
475
  ```
411
476
 
412
477
  Or globally on a per-request basis, for example if you take the value from localStorage:
413
478
 
414
479
  ```js
415
- import fch from 'fch';
480
+ import fch from "fch";
416
481
 
417
482
  // All the requests will add the Authorization header when the token is
418
483
  // in localStorage
419
- fch.before = req => {
420
- if (localStorage.get('token')) {
421
- req.headers.Authorization = 'bearer ' + localStorage.get('token');
484
+ fch.before = (req) => {
485
+ if (localStorage.get("token")) {
486
+ req.headers.Authorization = "bearer " + localStorage.get("token");
422
487
  }
423
488
  return req;
424
489
  };
425
490
 
426
- const me = await fch('/users/me');
491
+ const me = await fch("/users/me");
427
492
  ```
428
493
 
429
494
  Or on a per-request basis, though we wouldn't recommend this:
430
495
 
431
496
  ```js
432
- import fch from 'fch';
497
+ import fch from "fch";
433
498
 
434
- const me = await fch('/users/me', { headers: { Authorization: 'bearer abc' } });
499
+ const me = await fch("/users/me", { headers: { Authorization: "bearer abc" } });
435
500
  ```
436
501
 
437
-
438
502
  ### Create an instance
439
503
 
440
504
  You can create an instance with its own defaults and global options easily. It's common when writing an API that you want to encapsulate away:
@@ -453,28 +517,47 @@ fch.get('/hello'); // Gets http://localhost:3000/hello (or wherever you are)
453
517
 
454
518
  Note: for server-side (Node.js) usage, you always want to set `baseUrl`.
455
519
 
520
+ ### Streaming a response body
456
521
 
522
+ To stream the body, you need to use the `output: "stream"` option so that it returns a [WebStream ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream):
523
+
524
+ ```js
525
+ import fch from "fch";
526
+
527
+ // Valid values are 'body' (default) or 'response'
528
+ const stream = await fch("/data", { output: "stream" });
529
+ stream.pipeTo(outStream);
530
+ ```
531
+
532
+ You might want to convert it to a Node.js ReadStream:
533
+
534
+ ```js
535
+ import fch from "fch";
536
+ import { Readable } from "node:stream";
537
+
538
+ const stream = await fch("/data", { output: "stream" });
539
+ const readableNodeStream = Readable.fromWeb(stream);
540
+ // ...
541
+ ```
457
542
 
458
543
  ### Cancel ongoing requests
459
544
 
460
545
  You can cancel ongoing requests [similarly to native fetch()](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort#examples), by passing it a signal:
461
546
 
462
547
  ```js
463
- import api from 'fch';
548
+ import api from "fch";
464
549
 
465
550
  const controller = new AbortController();
466
551
  const signal = controller.signal;
467
552
 
468
- abortButton.addEventListener('click', () => {
553
+ abortButton.addEventListener("click", () => {
469
554
  controller.abort();
470
- console.log('Download aborted');
555
+ console.log("Download aborted");
471
556
  });
472
557
 
473
558
  api.get(url, { signal });
474
559
  ```
475
560
 
476
-
477
-
478
561
  ### Define shared options
479
562
 
480
563
  You can also define values straight away:
@@ -490,28 +573,24 @@ console.log(mew);
490
573
 
491
574
  You can also [create an instance](#create-an-instance) that will have the same options for all requests made with that instance.
492
575
 
493
-
494
-
495
576
  ### Node.js vs Browser
496
577
 
497
578
  First, we use the native Node.js' fetch() and the browser's native fetch(), so any difference between those also applies to this library. For example, if you were to call `"/"` in the browser it'd refer to the current URL, while in Node.js it'd fail since you need to specify the full URL. Some other places where you might find differences: CORS, cache, etc.
498
579
 
499
580
  In the library itself there's nothing different between the browser and Node.js, but it might be interesting to note that (if/when implemented) things like cache, etc. in Node.js are normally long-lived and shared, while in a browser request it'd bound to the request itself.
500
581
 
501
-
502
-
503
582
  ### Differences with Axios
504
583
 
505
584
  The main difference is that things are simplified with fch:
506
585
 
507
586
  ```js
508
587
  // Modify headers
509
- axios.defaults.headers.Authorization = '...';
510
- fch.headers.Authorization = '...';
588
+ axios.defaults.headers.Authorization = "...";
589
+ fch.headers.Authorization = "...";
511
590
 
512
591
  // Set a base URL
513
- axios.defaults.baseURL = '...';
514
- fch.baseUrl = '...';
592
+ axios.defaults.baseURL = "...";
593
+ fch.baseUrl = "...";
515
594
 
516
595
  // Add an interceptor
517
596
  axios.interceptors.request.use(fn);
@@ -524,3 +603,17 @@ As disadvantages, I can think of two major ones for `fch`:
524
603
 
525
604
  - Requires Node.js 18+, which is the version that includes `fetch()` by default.
526
605
  - Does not support many of the more advanced options, like `onUploadProgress` nor `onDownloadProgress`.
606
+
607
+ ## Releases
608
+
609
+ ### V4
610
+
611
+ Breaking changes:
612
+
613
+ - Only ESM exports. Meaning, if you use it in a browser you'll need the `<script type="module">`.
614
+ - The method `fch.del()` (and derivates with fch.create()) have been renamed to `fch.delete()`.
615
+
616
+ Changes:
617
+
618
+ - Added `output` options: `raw`, `stream`, `arrayBuffer`, `blob`, `clone`, `formData`, `json`, `text`
619
+ - Gone from 1.2kb down to 1.0kb
package/fetch.js DELETED
@@ -1,202 +0,0 @@
1
- // Plain-ish object
2
- const hasPlainBody = (options) => {
3
- if (options.headers["content-type"]) return;
4
- if (!["object", "array"].includes(typeof options.body)) return;
5
- if (options.body instanceof FormData) return;
6
- return true;
7
- };
8
-
9
- const createUrl = (url, query, base) => {
10
- let [path, urlQuery = {}] = url.split("?");
11
-
12
- // Merge global params with passed params with url params
13
- const entries = new URLSearchParams({
14
- ...Object.fromEntries(new URLSearchParams(query)),
15
- ...Object.fromEntries(new URLSearchParams(urlQuery)),
16
- }).toString();
17
- if (entries) {
18
- path = path + "?" + entries;
19
- }
20
-
21
- if (!base) return path;
22
- const fullUrl = new URL(path, base);
23
- return fullUrl.href;
24
- };
25
-
26
- const createHeaders = (user, base) => {
27
- // User-set headers overwrite the base headers
28
- const headers = { ...base, ...user };
29
-
30
- // Make the headers lowercase
31
- for (let key in headers) {
32
- const value = headers[key];
33
- delete headers[key];
34
- headers[key.toLowerCase()] = value;
35
- }
36
-
37
- return headers;
38
- };
39
-
40
- const createDedupe = (ongoing, url) => ({
41
- save: (prom) => {
42
- ongoing.set(url, prom);
43
- return prom;
44
- },
45
- get: () => ongoing.get(url),
46
- clear: () => ongoing.delete(url),
47
- });
48
-
49
- const createFetch = (request, { after, dedupe, error, output }) => {
50
- return fetch(request.url, request).then(async (res) => {
51
- // No longer ongoing at this point
52
- if (dedupe) dedupe.clear();
53
-
54
- // Need to manually create it to set some things like the proper response
55
- let response = {
56
- status: res.status,
57
- statusText: res.statusText,
58
- headers: {},
59
- };
60
- for (let key of res.headers.keys()) {
61
- response.headers[key.toLowerCase()] = res.headers.get(key);
62
- }
63
-
64
- // Oops, throw it
65
- if (!res.ok) {
66
- const err = new Error(res.statusText);
67
- err.response = response;
68
- return error(err);
69
- }
70
-
71
- // Automatically parse the response
72
- const type = res.headers.get("content-type");
73
- const isJson = type && type.includes("application/json");
74
- response.body = await (isJson ? res.json() : res.text());
75
-
76
- // Hijack the response and modify it
77
- if (after) {
78
- response = after(response);
79
- }
80
-
81
- if (output === "body") {
82
- return response.body;
83
- } else {
84
- return response;
85
- }
86
- });
87
- };
88
-
89
- const create = (defaults = {}) => {
90
- // DEDUPLICATION is created on a per-instance basis
91
- // To avoid making parallel requests to the same url if one is ongoing
92
- const ongoing = new Map();
93
-
94
- const fch = async (url, options = {}) => {
95
- // Second parameter always has to be an object, even when it defaults
96
- if (typeof options !== "object") options = {};
97
-
98
- // Accept either fch(options) or fch(url, options)
99
- options = typeof url === "string" ? { url, ...options } : url || {};
100
-
101
- // Exctract the options
102
- let {
103
- dedupe = fch.dedupe,
104
- output = fch.output,
105
- baseURL = fch.baseURL, // DO NOT USE; it's here only for user friendliness
106
- baseUrl = baseURL || fch.baseUrl,
107
-
108
- // Extract it since it should not be part of fetch()
109
- query = {},
110
-
111
- // Interceptors can also be passed as parameters
112
- before = fch.before,
113
- after = fch.after,
114
- error = fch.error,
115
-
116
- ...request
117
- } = options; // Local option OR global value (including defaults)
118
-
119
- // Merge it, first the global and then the local
120
- query = { ...fch.query, ...query };
121
- // Absolute URL if possible; Default method; merge the default headers
122
- request.url = createUrl(request.url ?? fch.url, query, baseUrl);
123
- request.method = (request.method ?? fch.method).toLowerCase();
124
- request.headers = createHeaders(request.headers, fch.headers);
125
-
126
- if (request.method !== "get") {
127
- dedupe = false;
128
- }
129
- if (dedupe) {
130
- dedupe = createDedupe(ongoing, request.url);
131
- }
132
-
133
- if (!["body", "response"].includes(output)) {
134
- const msg = `options.output needs to be either "body" (default) or "response", not "${output}"`;
135
- throw new Error(msg);
136
- }
137
-
138
- // JSON-encode plain objects
139
- if (hasPlainBody(request)) {
140
- request.body = JSON.stringify(request.body);
141
- request.headers["content-type"] = "application/json; charset=utf-8";
142
- }
143
-
144
- // Hijack the requeset and modify it
145
- if (before) {
146
- request = before(request);
147
- }
148
-
149
- // It should be cached and it's not being manually manipulated
150
- if (dedupe && !request.signal) {
151
- // It's already cached! Just return it
152
- if (dedupe.get()) return dedupe.get();
153
-
154
- // Otherwise, save it in the cache and return the promise
155
- return dedupe.save(
156
- createFetch(request, { dedupe, output, error, after })
157
- );
158
- } else {
159
- // PUT, POST, etc should never dedupe and just return the plain request
160
- return createFetch(request, { output, error, after });
161
- }
162
- };
163
-
164
- // Default values
165
- fch.url = defaults.url ?? "/";
166
- fch.method = defaults.method ?? "get";
167
- fch.query = defaults.query ?? {};
168
- fch.headers = defaults.headers ?? {};
169
- fch.baseUrl = defaults.baseUrl ?? defaults.baseURL ?? null;
170
-
171
- // Default options
172
- fch.dedupe = defaults.dedupe ?? true;
173
- fch.output = defaults.output ?? "body";
174
- fch.credentials = defaults.credentials ?? "include";
175
-
176
- // Interceptors
177
- fch.before = defaults.before ?? ((request) => request);
178
- fch.after = defaults.after ?? ((response) => response);
179
- fch.error = defaults.error ?? ((error) => Promise.reject(error));
180
-
181
- const get = (url, opts = {}) => fch(url, { ...opts });
182
- const head = (url, opts = {}) => fch(url, { ...opts, method: "head" });
183
- const post = (url, opts = {}) => fch(url, { ...opts, method: "post" });
184
- const patch = (url, opts = {}) => fch(url, { ...opts, method: "patch" });
185
- const put = (url, opts = {}) => fch(url, { ...opts, method: "put" });
186
- const del = (url, opts = {}) => fch(url, { ...opts, method: "delete" });
187
-
188
- fch.get = get;
189
- fch.head = head;
190
- fch.post = post;
191
- fch.patch = patch;
192
- fch.put = put;
193
- fch.del = del;
194
-
195
- fch.create = create;
196
-
197
- return fch;
198
- };
199
-
200
- const fch = create();
201
-
202
- export default fch;
package/fetch.min.js DELETED
@@ -1 +0,0 @@
1
- const hasPlainBody=e=>{if(!e.headers["content-type"]&&["object","array"].includes(typeof e.body)&&!(e.body instanceof FormData))return!0},createUrl=(e,t,r)=>{let[o,a={}]=e.split("?");const s=new URLSearchParams({...Object.fromEntries(new URLSearchParams(t)),...Object.fromEntries(new URLSearchParams(a))}).toString();if(s&&(o=o+"?"+s),!r)return o;return new URL(o,r).href},createHeaders=(e,t)=>{const r={...t,...e};for(let e in r){const t=r[e];delete r[e],r[e.toLowerCase()]=t}return r},createDedupe=(e,t)=>({save:r=>(e.set(t,r),r),get:()=>e.get(t),clear:()=>e.delete(t)}),createFetch=(e,{after:t,dedupe:r,error:o,output:a})=>fetch(e.url,e).then((async e=>{r&&r.clear();let s={status:e.status,statusText:e.statusText,headers:{}};for(let t of e.headers.keys())s.headers[t.toLowerCase()]=e.headers.get(t);if(!e.ok){const t=new Error(e.statusText);return t.response=s,o(t)}const d=e.headers.get("content-type"),n=d&&d.includes("application/json");return s.body=await(n?e.json():e.text()),t&&(s=t(s)),"body"===a?s.body:s})),create=(e={})=>{const t=new Map,r=async(e,o={})=>{"object"!=typeof o&&(o={}),o="string"==typeof e?{url:e,...o}:e||{};let{dedupe:a=r.dedupe,output:s=r.output,baseURL:d=r.baseURL,baseUrl:n=d||r.baseUrl,query:u={},before:c=r.before,after:h=r.after,error:l=r.error,...p}=o;if(u={...r.query,...u},p.url=createUrl(p.url??r.url,u,n),p.method=(p.method??r.method).toLowerCase(),p.headers=createHeaders(p.headers,r.headers),"get"!==p.method&&(a=!1),a&&(a=createDedupe(t,p.url)),!["body","response"].includes(s)){throw new Error(`options.output needs to be either "body" (default) or "response", not "${s}"`)}return hasPlainBody(p)&&(p.body=JSON.stringify(p.body),p.headers["content-type"]="application/json; charset=utf-8"),c&&(p=c(p)),a&&!p.signal?a.get()?a.get():a.save(createFetch(p,{dedupe:a,output:s,error:l,after:h})):createFetch(p,{output:s,error:l,after:h})};r.url=e.url??"/",r.method=e.method??"get",r.query=e.query??{},r.headers=e.headers??{},r.baseUrl=e.baseUrl??e.baseURL??null,r.dedupe=e.dedupe??!0,r.output=e.output??"body",r.credentials=e.credentials??"include",r.before=e.before??(e=>e),r.after=e.after??(e=>e),r.error=e.error??(e=>Promise.reject(e));return r.get=(e,t={})=>r(e,{...t}),r.head=(e,t={})=>r(e,{...t,method:"head"}),r.post=(e,t={})=>r(e,{...t,method:"post"}),r.patch=(e,t={})=>r(e,{...t,method:"patch"}),r.put=(e,t={})=>r(e,{...t,method:"put"}),r.del=(e,t={})=>r(e,{...t,method:"delete"}),r.create=create,r},fch=create();window.fch=fch;