Skip to content

Commit 7464afd

Browse files
committed
✨ Create core module with http client build on fetch API
1 parent fc6dbab commit 7464afd

File tree

13 files changed

+351
-11
lines changed

13 files changed

+351
-11
lines changed

.eslintrc

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
{
2-
"extends": ["@ackee/eslint-config", "eslint-config-prettier"]
2+
"extends": ["@ackee/eslint-config", "eslint-config-prettier"],
3+
"settings": {
4+
"import/resolver": {
5+
"node": {
6+
"paths": ["src"]
7+
}
8+
}
9+
}
310
}

babel.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const config = {
55
plugins: [
66
'@babel/proposal-class-properties',
77
'@babel/proposal-object-rest-spread',
8+
'@babel/plugin-proposal-nullish-coalescing-operator',
89
[
910
'babel-plugin-custom-import-path-transform',
1011
{

jsconfig.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"module": "commonjs",
5+
"jsx": "preserve",
6+
"baseUrl": "src"
7+
},
8+
"include": ["src"]
9+
}

package.json

+9-7
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,25 @@
22
"name": "@ackee/antonio",
33
"version": "3.0.0",
44
"description": "A HTTP client built on fetch with axios-like API.",
5-
"main": "lib/index.js",
5+
"main": "es/index.js",
66
"module": "es/index.js",
77
"sideEffects": false,
88
"scripts": {
9-
"build:lib": "BABEL_ENV=lib babel src --out-dir lib --extensions \".js,.jsx\" --source-maps inline",
10-
"build:es": "BABEL_ENV=es babel src --out-dir es --extensions \".js,.jsx\" --source-maps inline",
11-
"build:js": "yarn build:es && yarn build:lib",
9+
"build:lib": "BABEL_ENV=lib babel src --out-dir lib --extensions \".js\" --source-maps inline",
10+
"build:es": "BABEL_ENV=es babel src --out-dir es --extensions \".js\" --source-maps inline",
11+
"build:js": "yarn build:es",
1212
"clean": "rm -rf lib es",
1313
"build": "yarn clean && yarn build:js",
1414
"prepare": "yarn build",
15-
"lint": "eslint \"src/**/*.{js,jsx}\"",
15+
"lint": "eslint \"src/**/*.js\"",
1616
"test": "BABEL_ENV=test jest",
1717
"push": "yarn build && yalc push",
18-
"start": "yarn build && onchange 'src/**/*.{js,jsx}' -- yarn build",
18+
"start": "yarn build && onchange 'src/**/*.js' -- yarn rebuild",
19+
"rebuild": "yarn clean && yarn build:es && yalc push",
1920
"changelog": "gitmoji-changelog",
2021
"version": "yarn changelog && code --wait CHANGELOG.md && git add CHANGELOG.md",
2122
"release": "yarn version",
22-
"prettier": "prettier --config ./prettier.config.js --write './src/**/*.{js,jsx}'",
23+
"prettier": "prettier --config ./prettier.config.js --write './src/**/*.js'",
2324
"size": "package-size ./es --no-cache"
2425
},
2526
"author": "Jiří Čermák <[email protected]>",
@@ -33,6 +34,7 @@
3334
"@babel/cli": "7.x",
3435
"@babel/core": "7.x",
3536
"@babel/plugin-proposal-class-properties": "^7.8.3",
37+
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
3638
"@babel/plugin-proposal-object-rest-spread": "7.x",
3739
"@babel/plugin-transform-runtime": "7.x",
3840
"@babel/preset-env": "7.x",

src/index.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
export default function () {
2-
console.log('hello world');
3-
}
1+
export { create } from './modules/core';

src/modules/core/config/index.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ResponseType, RequestConfigFields } from '../constants';
2+
3+
export const RequestConfig = {
4+
// `baseURL` will be prepended to `url` unless `url` is absolute.
5+
// It can be convenient to set `baseURL` for an instance of axios to pass relative URLs
6+
// to methods of that instance.
7+
[RequestConfigFields.BASE_URL]: undefined,
8+
9+
[RequestConfigFields.RESPONSE_TYPE]: ResponseType.JSON,
10+
11+
[RequestConfigFields.URI_PARAMS]: undefined,
12+
13+
// `headers` are custom headers to be sent
14+
[RequestConfigFields.HEADERS]: undefined,
15+
16+
// `params` are the URL parameters to be sent with the request
17+
// Must be a plain object or a URLSearchParams object
18+
[RequestConfigFields.SEARCH_PARAMS]: undefined,
19+
20+
[RequestConfigFields.MODE]: undefined,
21+
[RequestConfigFields.CREDENTIALS]: undefined,
22+
[RequestConfigFields.CACHE]: undefined,
23+
[RequestConfigFields.REDIRECT]: undefined,
24+
[RequestConfigFields.REFERRER]: undefined,
25+
[RequestConfigFields.REFERRER_POLICY]: undefined,
26+
[RequestConfigFields.INTEGRITY]: undefined,
27+
[RequestConfigFields.KEEPALIVE]: undefined,
28+
[RequestConfigFields.SIGNAL]: undefined,
29+
};

src/modules/core/constants/index.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export const Methods = {
2+
GET: 'get',
3+
POST: 'post',
4+
PUT: 'put',
5+
PATCH: 'patch',
6+
DELETE: 'delete',
7+
OPTIONS: 'options',
8+
HEAD: 'head',
9+
};
10+
11+
export const ResponseType = {
12+
JSON: 'json',
13+
BLOB: 'blob',
14+
FORM_DATA: 'formData',
15+
TEXT: 'text',
16+
};
17+
18+
export const ResponseTypes = {
19+
[ResponseType.JSON]: 'application/json',
20+
[ResponseType.TEXT]: 'text/*',
21+
[ResponseType.FORM_DATA]: 'multipart/form-data',
22+
[ResponseType.BLOB]: '*/*',
23+
};
24+
25+
export const Header = {
26+
CONTENT_TYPE: 'Content-Type',
27+
};
28+
29+
export const RequestConfigFields = {
30+
BASE_URL: 'baseURL',
31+
RESPONSE_TYPE: 'responseType',
32+
URI_PARAMS: 'uriParams',
33+
HEADERS: 'headers',
34+
SEARCH_PARAMS: 'searchParams',
35+
MODE: 'mode',
36+
CREDENTIALS: 'credentials',
37+
CACHE: 'cache',
38+
REDIRECT: 'redirect',
39+
REFERRER: 'referrer',
40+
REFERRER_POLICY: 'referrerPolicy',
41+
INTEGRITY: 'integrity',
42+
KEEPALIVE: 'keepalive',
43+
SIGNAL: 'signal',
44+
};

src/modules/core/errors/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export class HTTPError extends Error {
2+
constructor(response) {
3+
// Set the message to the status text, such as Unauthorized,
4+
// with some fallbacks. This message should never be undefined.
5+
super(
6+
response.statusText ||
7+
String(response.status === 0 || response.status ? response.status : 'Unknown response error'),
8+
);
9+
this.name = 'HTTPError';
10+
this.response = response;
11+
}
12+
}

src/modules/core/index.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Methods } from './constants';
2+
import * as DefaultConfig from './config';
3+
import { HTTPError } from './errors';
4+
import { createRequestUrl, formatRequestBody, setRequestHeaders, parseResponse, mergeConfig } from './utils';
5+
6+
async function request(method, requestUrl, body, requestConfig, defaultRequestConfig) {
7+
const config = mergeConfig(defaultRequestConfig, requestConfig);
8+
9+
const url = createRequestUrl(requestUrl, config);
10+
11+
const { mode, credentials, cache, redirect, referrer, referrerPolicy, integrity, keepalive, signal } = config;
12+
13+
const request = new Request(url, {
14+
method,
15+
body: formatRequestBody(body, config),
16+
headers: setRequestHeaders(method, config),
17+
mode,
18+
credentials,
19+
cache,
20+
redirect,
21+
referrer,
22+
referrerPolicy,
23+
integrity,
24+
keepalive,
25+
signal,
26+
});
27+
28+
const response = await fetch(request);
29+
30+
if (!response.ok) {
31+
// TODO: try response.error instead
32+
throw new HTTPError('Fetch error:', response.statusText);
33+
}
34+
35+
const data = await parseResponse(config.responseType, response);
36+
37+
return {
38+
request,
39+
response,
40+
data,
41+
};
42+
}
43+
44+
export function create(customRequestConfig) {
45+
const customDefaultRequestConfig = mergeConfig(DefaultConfig.RequestConfig, customRequestConfig);
46+
47+
const instance = {
48+
defaults: customDefaultRequestConfig,
49+
50+
post(url, body, requestConfig) {
51+
return request(Methods.POST, url, body, requestConfig, customDefaultRequestConfig);
52+
},
53+
put(url, body, requestConfig) {
54+
return request(Methods.PUT, url, body, requestConfig, customDefaultRequestConfig);
55+
},
56+
patch(url, body, requestConfig) {
57+
return request(Methods.PATCH, url, body, requestConfig, customDefaultRequestConfig);
58+
},
59+
60+
get(url, requestConfig) {
61+
return request(Methods.GET, url, undefined, requestConfig, customDefaultRequestConfig);
62+
},
63+
delete(url, requestConfig) {
64+
return request(Methods.DELETE, url, undefined, requestConfig, customDefaultRequestConfig);
65+
},
66+
head(url, requestConfig) {
67+
return request(Methods.HEAD, url, undefined, requestConfig, customDefaultRequestConfig);
68+
},
69+
options(url, requestConfig) {
70+
return request(Methods.OPTIONS, url, undefined, requestConfig, customDefaultRequestConfig);
71+
},
72+
};
73+
74+
return Object.freeze(instance);
75+
}

src/modules/core/utils/config.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { RequestConfigFields } from '../constants';
2+
3+
export function mergeUrlSearchParams(searchParamsA, searchParamsB) {
4+
const result = new URLSearchParams();
5+
const getEntries = value => (value instanceof URLSearchParams ? value.entries() : Object.entries(value ?? {}));
6+
7+
for (const [key, value] of getEntries(searchParamsA)) {
8+
result.set(key, value);
9+
}
10+
11+
for (const [key, value] of getEntries(searchParamsB)) {
12+
result.set(key, value);
13+
}
14+
15+
return result;
16+
}
17+
18+
function mergeHeaders(searchParamsA, searchParamsB) {
19+
const result = new Headers();
20+
const getEntries = value => (value instanceof Headers ? value.entries() : Object.entries(value ?? {}));
21+
22+
for (const [key, value] of getEntries(searchParamsA)) {
23+
result.set(key, value);
24+
}
25+
26+
for (const [key, value] of getEntries(searchParamsB)) {
27+
result.set(key, value);
28+
}
29+
30+
return result;
31+
}
32+
33+
export function mergeConfig(requestConfigA, requestConfigB = {}) {
34+
const result = {};
35+
36+
for (const [key, value] of Object.entries(requestConfigA || {})) {
37+
const newValue = requestConfigB[key];
38+
39+
switch (key) {
40+
case RequestConfigFields.HEADERS:
41+
result[key] = mergeHeaders(value, newValue);
42+
break;
43+
44+
case RequestConfigFields.URI_PARAMS:
45+
result[key] = {
46+
...value,
47+
...newValue,
48+
};
49+
break;
50+
51+
case RequestConfigFields.SEARCH_PARAMS:
52+
result[key] = mergeUrlSearchParams(value, newValue);
53+
break;
54+
55+
default:
56+
result[key] = newValue ?? value;
57+
}
58+
}
59+
60+
return result;
61+
}

src/modules/core/utils/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './request';
2+
export * from './response';
3+
export * from './config';

src/modules/core/utils/request.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ResponseType, Header, ResponseTypes, Methods } from '../constants';
2+
import { mergeUrlSearchParams } from '../utils';
3+
4+
export function formatRequestBody(body, config) {
5+
if (config.responseType === ResponseType.JSON) {
6+
return JSON.stringify(body);
7+
}
8+
9+
return body;
10+
}
11+
12+
const isTokenTemplate = token => token.startsWith(':');
13+
14+
function setUriParams(templateUrl, uriParams) {
15+
const templateToValue = token => {
16+
if (!isTokenTemplate(token)) {
17+
return token;
18+
}
19+
20+
const uriParamKey = token.slice(1);
21+
const value = uriParams[uriParamKey];
22+
23+
if (value === undefined) {
24+
throw new TypeError(
25+
`No URI param found for '${token}' template token among URI params: \n${JSON.stringify(
26+
uriParams,
27+
null,
28+
2,
29+
)}\n for template URL: '${templateUrl}'.`,
30+
);
31+
}
32+
33+
// replace template (:itemId) with actual value
34+
return value;
35+
};
36+
37+
return templateUrl.split('/').map(templateToValue).join('/');
38+
}
39+
40+
export function createRequestUrl(requestUrl, requestConfig) {
41+
try {
42+
if (requestConfig.uriParams) {
43+
requestUrl = setUriParams(requestUrl, requestConfig.uriParams);
44+
}
45+
46+
const url = new URL(requestUrl, requestConfig.baseURL);
47+
48+
url.search = mergeUrlSearchParams(new URLSearchParams(url.search), requestConfig.searchParams);
49+
50+
return url;
51+
} catch (e) {
52+
// TODO: use log-level
53+
console.error(e);
54+
throw new TypeError(
55+
`Could not compose request url from: requestUrl: '${requestUrl}' and baseURL: '${requestConfig.baseURL}'.`,
56+
);
57+
}
58+
}
59+
60+
export function setRequestHeaders(method, config) {
61+
const headers = new Headers(config.headers);
62+
63+
if (!headers.has(Header.CONTENT_TYPE) && method !== Methods.HEAD) {
64+
headers.set(Header.CONTENT_TYPE, ResponseTypes[config.responseType]);
65+
}
66+
67+
return headers;
68+
}

0 commit comments

Comments
 (0)