Skip to content

Commit

Permalink
Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
zellwk committed Jul 12, 2023
1 parent f715713 commit 250c3e0
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 286 deletions.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

zlFetch is a wrapper around fetch that provides you with a convenient way to make requests.

Note: From `v4.0.0` onwards, zlFetch is a ESM library. It cannot be used with CommonJS anymore.

It's features are as follows:

- Quality of life improvements over the native `fetch` function
Expand All @@ -21,28 +19,32 @@ It's features are as follows:
- [Generates authorization headers](#authorization-header-helpers) with an `auth` property.
- [Create instances to hold url and options](#creating-a-zlfetch-instance) so you don't have to repeat yourself.

Note: zlFetch is a ESM library since `v4.0.0`.

## Installing zlFetch

You can install zlFetch through npm:
### Through npm (recommended)

```bash
# Installing through npm
npm install zl-fetch --save
```

Then you can use it by importing it in your JavaScript file. It works for both browsers and Node.
Then you can use it by importing it in your JavaScript file.

```js
import zlFetch from 'zl-fetch'
```

Using `zlFetch` without `npm`:
### Using `zlFetch` without `npm`:

You can import zlFetch directly into JavaScript through a CDN.

You can use `zlFetch` without `npm` by importing it directly to your HTML file. To do this, you first need to set your `script`'s type to `module`, then import `zlFetch` from a CDN jsdelivr.
To do this, you first need to set your `script`'s type to `module`, then import `zlFetch`.

```html
<script type="module">
import zlFetch from 'https://cdn.jsdelivr.net/npm/zl-fetch@5.0.1/src/index.js'
import zlFetch from 'https://cdn.jsdelivr.net/npm/zl-fetch@6.0.0/src/index.js'
</script>
```

Expand Down
11 changes: 2 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@
"version": "5.0.1",
"description": "A library that makes the Fetch API easy to use",
"type": "module",
"sideEffects": false,
"files": [
"src"
],
"exports": {
".": {
"import": "./src/index.js"
},
"./*": "./src/*"
},
"exports": "./src/index.js",
"keywords": [
"fetch",
"javascript",
Expand All @@ -28,9 +24,6 @@
},
"author": "Zell Liew <zellwk@gmail.com>",
"license": "MIT",
"dependencies": {
"node-fetch": "^3.2.10"
},
"devDependencies": {
"@vitest/ui": "^0.23.4",
"body-parser": "^1.20.0",
Expand Down
65 changes: 65 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import createRequestOptions, { isFormData } from './createRequestOptions.js'
import { handleError, handleResponse } from './handleResponse.js'

/**
* Main Fetch Function
* @param {string} url - endpoint
* @param {object} options - zlFetch options
* @param {string} options.method - HTTP method
* @param {object} options.headers - HTTP headers
* @param {object} options.body - Body content
* @param {string} options.auth - Authentication information
* @param {string} options.debug - Logs the request options for debugging
* @param {string} options.returnError - Returns the error instead of rejecting it
* @param {string} options.customResponseParser - Use a custome response parser
*/

export function coreFetch(url, options) {
return fetchInstance({ url, ...options })
}
// ========================
// Internal Functions
// ========================
async function fetchInstance(options) {
const requestOptions = createRequestOptions(options)

// Remove options that are not native to a fetch request
delete requestOptions.fetch
delete requestOptions.auth
delete requestOptions.debug
delete requestOptions.returnError

// Performs the fetch request
return fetch(requestOptions.url, requestOptions)
.then(response => handleResponse(response, options))
.then(response => {
if (!options.debug) return response
return { ...response, debug: debugHeaders(requestOptions) }
})
.catch(handleError)
}

function debugHeaders(requestOptions) {
const clone = Object.assign({}, requestOptions)
const headers = {}
for (const [header, value] of clone.headers) {
headers[header] = value
}
clone.headers = headers
return clone
}

export function createShorthandMethods(fn) {
const methods = ['get', 'post', 'put', 'patch', 'delete']

for (const method of methods) {
fn[method] = function (url, options) {
return coreFetch(url, {
...options,
method,
})
}
}

return fn
}
58 changes: 58 additions & 0 deletions src/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { coreFetch } from './core.js'

// Creates an instance of zlFetch to be used later
export function create(baseURL, options) {
const fn = function (...args) {
const { url, newOptions } = normalize(args)
return coreFetch(url, { ...options, ...newOptions })
}

// Create Shorthand methods
const methods = ['get', 'post', 'put', 'patch', 'delete']

for (const method of methods) {
fn[method] = function (...args) {
const { url, newOptions } = normalize(args)
return coreFetch(url, { ...options, ...newOptions, method })
}
}

// Normalize the URL and options
// Allows user to use the created zlFetch item without passing in further URLs.
// Naming can be improved, but can't think of a better name for now
function normalize(args = []) {
const [arg1, arg2] = args

// This means no options are given. So we simply use the baseURL as the URL
if (!arg1) return { url: baseURL }

// If the firs argument is an object, it means there are options but the user didn't pass in a URL.
if (typeof arg1 === 'object') return { url: baseURL, newOptions: arg1 }

// The leftover possibility is that the first argument is a string.
// In this case we need to make a new URL with this argument.
const url = makeURL(baseURL, arg1)

// Wwe need to check whether the second argument is an object or not. If arg2 is undefined, then we simply return the URL since there are no options
if (!arg2) return { url }

// The only possibility left is that arg2 is an object, which means there are new options.
return { url, newOptions: arg2 }
}

return fn
}

// Joins the baseURL and endpoint.
// Uses a simple string concatenation instead of path.join so it works in the browser.
function makeURL(baseURL, url) {
if (baseURL.endsWith('/') && url.startsWith('/')) {
url = url.slice(1)
}

if (!baseURL.endsWith('/') && !url.startsWith('/')) {
url = '/' + url
}

return baseURL + url
}
7 changes: 3 additions & 4 deletions src/createRequestOptions.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export default function createRequestOptions(options = {}) {
const opts = Object.assign({}, options)
const fetchHeaders = options.fetch.Headers

// Note: headers get mutated after setHeaders is called.
// This is why we get the content type here to know what the user originally set.
const headers = new fetchHeaders(options.headers)

const headers = new Headers(options.headers)
const userContentType = headers.get('content-type')

opts.url = setUrl(opts)
Expand Down Expand Up @@ -52,8 +52,7 @@ function setMethod(options) {
// We set the headers depending on the request body and method
// ========================
function setHeaders(options) {
const fetchHeaders = options.fetch.Headers
let headers = new fetchHeaders(options.headers)
let headers = new Headers(options.headers)
headers = contentTypeHeader(options, headers)
headers = authHeader(options, headers)

Expand Down
161 changes: 7 additions & 154 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,158 +1,11 @@
/* globals fetch */
import createRequestOptions, { isFormData } from './createRequestOptions.js'
import { handleError, handleResponse } from './handleResponse.js'
export * from './util.js'

/**
* Main zlFetch Function
* @param {string} url - endpoint
* @param {object} options - zlFetch options
* @param {string} options.method - HTTP method
* @param {object} options.headers - HTTP headers
* @param {object} options.body - Body content
* @param {string} options.auth - Authentication information
* @param {string} options.debug - Logs the request options for debugging
* @param {string} options.returnError - Returns the error instead of rejecting it
* @param {string} options.customResponseParser - Use a custome response parser
*/
export default function zlFetch(url, options) {
return fetchInstance({ url, ...options })
}
import { coreFetch, createShorthandMethods } from './core.js'

// Create Shorthand methods
const methods = ['get', 'post', 'put', 'patch', 'delete']
import { create } from './create.js'

for (const method of methods) {
zlFetch[method] = function (url, options) {
return fetchInstance({ url, method, ...options })
}
}
let fn = coreFetch
fn = createShorthandMethods(fn)

// Creates an instance of zlFetch to be used later
export function createZlFetch(baseURL, options) {
const fn = function (...args) {
const { url, newOptions } = normalize(args)
return fetchInstance({ url, ...options, ...newOptions })
}

// Create Shorthand methods
const methods = ['get', 'post', 'put', 'patch', 'delete']

for (const method of methods) {
fn[method] = function (...args) {
const { url, newOptions } = normalize(args)
return fetchInstance({ url, method, ...options, ...newOptions })
}
}

// Normalize the URL and options
// Allows user to use the created zlFetch item without passing in further URLs.
// Naming can be improved, but can't think of a better name for now
function normalize(args = []) {
const [arg1, arg2] = args

// This means no options are given. So we simply use the baseURL as the URL
if (!arg1) return { url: baseURL }

// If the firs argument is an object, it means there are options but the user didn't pass in a URL.
if (typeof arg1 === 'object') return { url: baseURL, newOptions: arg1 }

// The leftover possibility is that the first argument is a string.
// In this case we need to make a new URL with this argument.
const url = makeURL(baseURL, arg1)

// Wwe need to check whether the second argument is an object or not. If arg2 is undefined, then we simply return the URL since there are no options
if (!arg2) return { url }

// The only possibility left is that arg2 is an object, which means there are new options.
return { url, newOptions: arg2 }
}

return fn
}

// Joins the baseURL and endpoint.
// Uses a simple string concatenation instead of path.join so it works in the browser.
function makeURL(baseURL, url) {
if (baseURL.endsWith('/') && url.startsWith('/')) {
url = url.slice(1)
}

if (!baseURL.endsWith('/') && !url.startsWith('/')) {
url = '/' + url
}

return baseURL + url
}

// ========================
// Internal Functions
// ========================
async function fetchInstance(options) {
const fetch = await getFetch()
const requestOptions = createRequestOptions({ ...options, fetch })

// Remove options that are not native to a fetch request
delete requestOptions.fetch
delete requestOptions.auth
delete requestOptions.debug
delete requestOptions.returnError

// Performs the fetch request
return fetch
.fetch(requestOptions.url, requestOptions)
.then(response => handleResponse(response, options))
.then(response => {
if (!options.debug) return response
return { ...response, debug: debugHeaders(requestOptions) }
})
.catch(handleError)
}

// Normalizes between Browser and Node Fetch
export async function getFetch() {
if (typeof fetch === 'undefined' || typeof window === 'undefined') {
const f = await import('node-fetch')
return {
fetch: f.default,
Headers: f.Headers,
}
} else {
return {
fetch: window.fetch.bind(window),
Headers: window.Headers,
}
}
}

function debugHeaders(requestOptions) {
const clone = Object.assign({}, requestOptions)
const headers = {}
for (const [header, value] of clone.headers) {
headers[header] = value
}
clone.headers = headers
return clone
}

/**
* Converts Form Data into an object
* @param {FormData} formData
* @returns Object
*/
export function toObject(formData) {
const obj = {}
for (const data of formData) {
obj[data[0]] = data[1].trim()
}
return obj
}

/**
* Converts object into a query string
* @param {Object} object
* @returns
*/
export function toQueryString(object) {
const searchParams = new URLSearchParams(object)
return searchParams.toString()
}
export default fn
export const createZlFetch = create

0 comments on commit 250c3e0

Please sign in to comment.