fch 3.0.0 → 3.0.3

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/fetch.js +9 -15
  2. package/index.js +1 -0
  3. package/package.json +7 -5
  4. package/readme.md +185 -79
package/fetch.js CHANGED
@@ -118,7 +118,7 @@ const create = (defaults = {}) => {
118
118
  // Merge it, first the global and then the local
119
119
  query = { ...fch.query, ...query };
120
120
  // Absolute URL if possible; Default method; merge the default headers
121
- request.url = createUrl(request.url || "/", query, baseUrl);
121
+ request.url = createUrl(request.url ?? fch.url, query, baseUrl);
122
122
  request.method = (request.method ?? fch.method).toLowerCase();
123
123
  request.headers = createHeaders(request.headers, fch.headers);
124
124
 
@@ -145,8 +145,8 @@ const create = (defaults = {}) => {
145
145
  request = before(request);
146
146
  }
147
147
 
148
- // It should be cached
149
- if (dedupe) {
148
+ // It should be cached and it's not being manually manipulated
149
+ if (dedupe && !request.signal) {
150
150
  // It's already cached! Just return it
151
151
  if (dedupe.get()) return dedupe.get();
152
152
 
@@ -161,6 +161,7 @@ const create = (defaults = {}) => {
161
161
  };
162
162
 
163
163
  // Default values
164
+ fch.url = defaults.url ?? "/";
164
165
  fch.method = defaults.method ?? "get";
165
166
  fch.query = defaults.query ?? {};
166
167
  fch.headers = defaults.headers ?? {};
@@ -189,18 +190,11 @@ const create = (defaults = {}) => {
189
190
  fch.put = put;
190
191
  fch.del = del;
191
192
 
193
+ fch.create = create;
194
+
192
195
  return fch;
193
196
  };
194
197
 
195
- // Need to export it globally with `global`, since if we use export default then
196
- // we cannot load it in the browser as a normal <script>, and if we load it in
197
- // the browser as a <script module> then we cannot run a normal script after it
198
- // since the modules are deferred by default. Basically this is a big mess and
199
- // I wish I could just do if `(typeof window !== 'undefined') window.fch = fch`,
200
- // but unfortunately that's not possible now and I need this as a traditionally
201
- // global definition, and then another file to import it as ESM. UGHHH
202
- let glob = {};
203
- if (typeof global !== "undefined") glob = global;
204
- if (typeof window !== "undefined") glob = window;
205
- glob.fch = create();
206
- glob.fch.create = create;
198
+ const fch = create();
199
+
200
+ export default fch;
package/index.js ADDED
@@ -0,0 +1 @@
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").includes("application/json");return s.body=await(d?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:p=r.error,...l}=o;if(u={...r.query,...u},l.url=createUrl(l.url??r.url,u,n),l.method=(l.method??r.method).toLowerCase(),l.headers=createHeaders(l.headers,r.headers),"get"!==l.method&&(a=!1),a&&(a=createDedupe(t,l.url)),!["body","response"].includes(s)){throw new Error(`options.output needs to be either "body" (default) or "response", not "${s}"`)}return hasPlainBody(l)&&(l.body=JSON.stringify(l.body),l.headers["content-type"]="application/json; charset=utf-8"),c&&(l=c(l)),a&&!l.signal?a.get()?a.get():a.save(createFetch(l,{dedupe:a,output:s,error:p,after:h})):createFetch(l,{output:s,error:p,after:h})};r.url=e.url??"/",r.method=e.method??"get",r.query=e.query??{},r.headers=e.headers??{},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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fch",
3
- "version": "3.0.0",
3
+ "version": "3.0.3",
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,6 +9,7 @@
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' > ./index.js",
12
13
  "start": "npm run watch # Start ~= Start dev",
13
14
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles",
14
15
  "watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles"
@@ -21,19 +22,20 @@
21
22
  "async",
22
23
  "ajax"
23
24
  ],
24
- "main": "./index.js",
25
- "browser": "./fetch.js",
25
+ "main": "./fetch.js",
26
+ "browser": "./index.js",
26
27
  "files": [
28
+ "index.js",
27
29
  "fetch.js"
28
30
  ],
29
31
  "type": "module",
30
32
  "engines": {
31
33
  "node": ">=18.0.0"
32
34
  },
33
- "dependencies": {},
34
35
  "devDependencies": {
35
36
  "jest": "^28.0.1",
36
- "jest-fetch-mock": "^3.0.3"
37
+ "jest-fetch-mock": "^3.0.3",
38
+ "terser": "^5.13.1"
37
39
  },
38
40
  "jest": {
39
41
  "testEnvironment": "jest-environment-node",
package/readme.md CHANGED
@@ -1,47 +1,49 @@
1
- # Fch [![npm install fch](https://img.shields.io/badge/npm%20install-fch-blue.svg)](https://www.npmjs.com/package/fch) [![gzip size](https://img.badgesize.io/franciscop/fetch/master/fetch.js.svg?compression=gzip)](https://github.com/franciscop/fetch/blob/master/fetch.js)
1
+ # Fch [![npm install fch](https://img.shields.io/badge/npm%20install-fch-blue.svg)](https://www.npmjs.com/package/fch) [![gzip size](https://img.badgesize.io/franciscop/fetch/master/index.js.svg?compression=gzip)](https://github.com/franciscop/fetch/blob/master/index.js)
2
2
 
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
6
  import api from "fch";
7
- const mew = await api("https://pokeapi.co/pokemon/150");
7
+ api.baseUrl = "https://pokeapi.co/";
8
+ const mew = await api.get("/pokemon/150");
8
9
  console.log(mew);
9
10
  ```
10
11
 
11
- - Automatically `JSON.stringify()` and `Content-Type: 'application/json'` for objects.
12
- - Automatically parse server response taking into account the headers.
13
- - Works the same way in Node.js and the browser.
14
- - Await/Async Promises. `>= 400 and <= 100` will _reject_ the promise and throw an error.
15
- - No dependencies; include it with a simple `<script>`
16
- - Easily define shared options straight on the root `fch.baseUrl = "https://...";`.
17
- - Interceptors: `before` (the request), `after` (the response) and `error` (it fails).
12
+ - Create instances with shared options across requests.
13
+ - Automatically encode object and array bodies as JSON.
14
+ - Automatically decode JSON responses based on the headers.
15
+ - Await/Async Promises; `>= 400 and <= 100` will _reject_ with an error.
16
+ - Interceptors: `before` the request, `after` the response and catch with `error`.
18
17
  - Deduplicates parallel GET requests.
19
- - Configurable to return either just the body, or the full response.
18
+ - Works the same way in Node.js and the browser.
19
+ - No dependencies; include it with a simple `<script>` on the browser.
20
20
 
21
21
  ```js
22
- // Calls and methods available:
23
- api(url, { method, body, headers, ...options })
22
+ import api from 'fch';
23
+
24
24
  api.get(url, { headers, ...options })
25
25
  api.head(url, { headers, ...options })
26
26
  api.post(url, { body, headers, ...options })
27
27
  api.patch(url, { body, headers, ...options })
28
28
  api.put(url, { body, headers, ...options })
29
29
  api.del(url, { body, headers, ...options })
30
- fch.create({ url, body, headers, ...options})
31
- ```
32
-
33
- |Options/variables |Default |Description |
34
- |------------------|---------------|-------------------------------------------|
35
- |`url` |`null` |The path or full url for the request |
36
- |`api.baseUrl` |`null` |The shared base of the API |
37
- |`api.method` |`"get"` |Default method to use for the call |
38
- |`api.query` |`{}` |Add query parameters to the URL |
39
- |`api.headers` |`{}` |Shared headers across all requests |
40
- |`api.dedupe` |`true` |Reuse GET requests made concurrently |
41
- |`api.output` |`"body"` |The return value of the API call |
42
- |`api.before` |`req => req` |Process the request before sending it |
43
- |`api.after` |`res => res` |Process the response before receiving it |
44
- |`api.error` |`err => reject(err)` |Process errors before returning them |
30
+
31
+ api.create({ url, body, headers, ...options})
32
+ ```
33
+
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 |
45
47
 
46
48
  ## Getting Started
47
49
 
@@ -70,45 +72,78 @@ On the browser you can add it with a script and it will be available as `fch`:
70
72
 
71
73
  ## Options
72
74
 
75
+ These are all available options and their defaults:
76
+
73
77
  ```js
74
78
  import api from 'fch';
75
79
 
76
- // General options with their defaults; most of these are also parameters:
77
- api.baseUrl = null; // Set an API endpoint
80
+ // General options with their defaults; all of these are also parameters:
78
81
  api.method = 'get'; // Default method to use for api()
79
- api.headers = {}; // Is merged with the headers on a per-request basis
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
80
86
 
81
87
  // Control simple variables
82
- api.dedupe = true; // Avoid parallel GET requests to the same path
83
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
84
90
 
85
91
  // Interceptors
86
92
  api.before = req => req;
87
93
  api.after = res => res;
88
94
  api.error = err => Promise.reject(err);
95
+ ```
96
+
97
+ 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).
98
+
99
+ ### Method
100
+
101
+ The HTTP method to make the request. When using the shorthand, it defaults to `GET`. We recommend using the method syntax:
89
102
 
90
- // Similar API to fetch()
91
- api(url, { method, body, headers, ... });
103
+ ```js
104
+ import api from 'fch';
105
+
106
+ api.get('/cats');
107
+ api.post('/cats', { body: { name: 'snowball' } });
108
+ api.put(`/cats/3`, { body: { name: 'snowball' }});
109
+ ```
110
+
111
+ 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:
112
+
113
+ ```js
114
+ // Recommended way of dealing with methods:
115
+ api.get(...);
116
+
117
+ // INVALID; won't work
118
+ api.GET(...);
119
+
120
+ // Both of these are valid:
121
+ api({ method; 'GET' })
122
+ api({ method; 'get'})
123
+ ```
124
+
125
+ Example: adding a new cat and fixing a typo:
126
+
127
+ ```js
128
+ import api from 'fch';
92
129
 
93
- // Our highly recommended style:
94
- api.get(url, { headers, ... });
95
- api.post(url, { body, headers, ... });
96
- api.put(url, { body, headers, ... });
97
- // ...
130
+ const cats = await api.get('/cats');
131
+ console.log(cats);
132
+ const { id } = await api.post('/cats', { body: { name: 'snowbll' } });
133
+ await api.put(`/cats/${id}`, { body: { name: 'snowball' }})
98
134
  ```
99
135
 
100
- ### URL
136
+ ### Url
101
137
 
102
- This is normally the first argument, though technically you can use both styles:
138
+ Specify where to send the request to. It's normally the first argument, though technically you can use both styles:
103
139
 
104
140
  ```js
105
- // All of these methods are valid
106
141
  import api from 'fch';
107
142
 
108
- // We strongly recommend using this style for your normal code:
143
+ // Recommended way of specifying the Url
109
144
  await api.post('/hello', { body: '...', headers: {} })
110
145
 
111
- // Try to avoid these, but they are also valid:
146
+ // These are also valid if you prefer their style; we won't judge
112
147
  await api('/hello', { method: 'post', body: '...', headers: {} });
113
148
  await api({ url: '/hello', method: 'post', headers: {}, body: '...' });
114
149
  await api.post({ url: '/hello', headers: {}, body: '...' });
@@ -123,9 +158,11 @@ api.get('/hello');
123
158
  // Called https//api.filemon.io/hello
124
159
  ```
125
160
 
161
+ > Note: with Node.js you need to either set an absolute baseUrl or make the URL absolute
162
+
126
163
  ### Body
127
164
 
128
- The `body` can be a string, a plain object|array or a FormData instance. If it's an object, it'll be stringified and the header `application/json` will be added. Otherwise it'll be sent as plain text:
165
+ The `body` can be a string, a plain object|array or a FormData instance. If it's an array or object, it'll be stringified and the header `application/json` will be added. Otherwise it'll be sent as plain text:
129
166
 
130
167
  ```js
131
168
  import api from 'api';
@@ -142,6 +179,46 @@ form.onsubmit = e => {
142
179
  };
143
180
  ```
144
181
 
182
+ The methods `GET` and `HEAD` do not accept a body and it'll be ignored.
183
+
184
+ The **response body** will be returned by default as the output of the call:
185
+
186
+ ```js
187
+ const body = await api.get('/cats');
188
+ console.log(body);
189
+ // [{ id: 1, }, ...]
190
+ ```
191
+
192
+ When the server specifies the header `Content-Type` as `application/json`, then we'll attempt to parse the response body and return that as the variable. Otherwise, the plain text will be returned.
193
+
194
+ When the function returns the response (if you set `output: "response"` as an option), then the body can be accessed as `response.body`:
195
+
196
+ ```js
197
+ const response = await api.get('/cats', { output: 'response' });
198
+ console.log(response.body);
199
+ // [{ id: 1, }, ...]
200
+ ```
201
+
202
+
203
+ ### Query
204
+
205
+ You can easily pass GET query parameters by using the option `query`:
206
+
207
+ ```js
208
+ api.get('/cats', { query: { limit: 3 } });
209
+ // /cats?limit=3
210
+ ```
211
+
212
+ 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
+
214
+ ```js
215
+ import api from 'fch';
216
+ api.query.myparam = 'abc';
217
+
218
+ api.get('/cats', { query: { limit: 3 } });
219
+ // /cats?limit=3&myparam=abc
220
+ ```
221
+
145
222
 
146
223
  ### Headers
147
224
 
@@ -150,13 +227,28 @@ You can define headers globally, in which case they'll be added to every request
150
227
 
151
228
  ```js
152
229
  import api from 'fch';
153
- api.headers.abc = 'def';
154
230
 
155
- api.get('/helle', { headers: { ghi: 'jkl' } });
231
+ // Globally, so they are reused across all requests
232
+ api.headers.a = 'b';
233
+
234
+ // With an interceptor, in case you need dynamic headers per-request
235
+ api.before = req => {
236
+ req.headers.c = 'd';
237
+ return req;
238
+ };
239
+
240
+ // Set them for this single request:
241
+ api.get('/hello', { headers: { e: 'f' } });
156
242
  // Total headers on the request:
157
- // { abc: 'def', ghi: 'jkl' }
243
+ // { a: 'b', c: 'd', e: 'f' }
158
244
  ```
159
245
 
246
+ When to use each?
247
+
248
+ - If you need headers shared across all requests, like an API key, then the global one is the best place.
249
+ - 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
+ - When it changes on each request, it's not consistent or it's an one-off, use the option argument.
251
+
160
252
 
161
253
  ### Output
162
254
 
@@ -203,7 +295,7 @@ fch('/a', { dedupe: true }); // [DEFAULT] Dedupes GET requests
203
295
  fch('/a', { dedupe: false }) // All fetch() calls trigger a network call
204
296
  ```
205
297
 
206
- > We do not support deduping other methods right now besides `GET` right now
298
+ > We do not support deduping other methods besides `GET` right now
207
299
 
208
300
  Note that opting out of deduping a request will _also_ make that request not be reusable, see this test for details:
209
301
 
@@ -255,32 +347,6 @@ fch.error = async err => {
255
347
  ```
256
348
 
257
349
 
258
- ### Define shared options
259
-
260
- You can also define values straight away:
261
-
262
- ```js
263
- import api from "fch";
264
-
265
- api.baseUrl = "https://pokeapi.co/";
266
-
267
- const mew = await api.get("/pokemon/150");
268
- console.log(mew);
269
- ```
270
-
271
- If you prefer Axios' style of outputting the whole response, you can do:
272
-
273
- ```js
274
- // Default, already only returns the data on a successful call
275
- api.output = "data";
276
- const name = await api.get("/users/1").name;
277
-
278
- // Axios-like
279
- api.output = "response";
280
- const name = await api.get("/users/1").data.name;
281
- ```
282
-
283
-
284
350
  ## How to
285
351
 
286
352
  ### Stop errors from throwing
@@ -332,7 +398,7 @@ const body = await fch.get('/blabla');
332
398
  ```
333
399
 
334
400
 
335
- ### Set the authorization headers
401
+ ### Set authorization headers
336
402
 
337
403
  You can set that globally as a header:
338
404
 
@@ -387,14 +453,54 @@ fch.get('/hello'); // Gets http://localhost:3000/hello (or wherever you are)
387
453
 
388
454
  Note: for server-side (Node.js) usage, you always want to set `baseUrl`.
389
455
 
390
- ### What are the differences in Node.js vs Browser?
456
+
457
+
458
+ ### Cancel ongoing requests
459
+
460
+ 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
+
462
+ ```js
463
+ import api from 'fch';
464
+
465
+ const controller = new AbortController();
466
+ const signal = controller.signal;
467
+
468
+ abortButton.addEventListener('click', () => {
469
+ controller.abort();
470
+ console.log('Download aborted');
471
+ });
472
+
473
+ api.get(url, { signal });
474
+ ```
475
+
476
+
477
+
478
+ ### Define shared options
479
+
480
+ You can also define values straight away:
481
+
482
+ ```js
483
+ import api from "fch";
484
+
485
+ api.baseUrl = "https://pokeapi.co/";
486
+
487
+ const mew = await api.get("/pokemon/150");
488
+ console.log(mew);
489
+ ```
490
+
491
+ You can also [create an instance](#create-an-instance) that will have the same options for all requests made with that instance.
492
+
493
+
494
+
495
+ ### Node.js vs Browser
391
496
 
392
497
  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.
393
498
 
394
499
  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.
395
500
 
396
501
 
397
- ### What are the differences with Axios?
502
+
503
+ ### Differences with Axios
398
504
 
399
505
  The main difference is that things are simplified with fch:
400
506
 
@@ -412,9 +518,9 @@ axios.interceptors.request.use(fn);
412
518
  fch.before = fn;
413
519
  ```
414
520
 
415
- API size is also strikingly different, with **7.8kb** for Axios and **1.9kb** for fch.
521
+ API size is also strikingly different, with **7.8kb** for Axios and **1.1kb** for fch.
416
522
 
417
523
  As disadvantages, I can think of two major ones for `fch`:
418
524
 
419
525
  - Requires Node.js 18+, which is the version that includes `fetch()` by default.
420
- - Does not support some more advanced options,
526
+ - Does not support many of the more advanced options, like `onUploadProgress` nor `onDownloadProgress`.