@farhantallei/fetcher 1.0.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.
- package/README.md +15 -0
- package/dist/index.cjs +149 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -0
- package/dist/types.cjs +4 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +5 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var APIError = class _APIError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
url;
|
|
7
|
+
info;
|
|
8
|
+
log;
|
|
9
|
+
constructor(message, url, status, info) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "APIError";
|
|
12
|
+
this.url = url;
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.info = info;
|
|
15
|
+
this.log = this.formatLog(info);
|
|
16
|
+
if (Error.captureStackTrace) {
|
|
17
|
+
Error.captureStackTrace(this, _APIError);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
formatLog(info) {
|
|
21
|
+
if (info === null || info === void 0) return "No additional info";
|
|
22
|
+
if (typeof info === "string") return info;
|
|
23
|
+
if (typeof info === "object") {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(info, null, 2);
|
|
26
|
+
} catch {
|
|
27
|
+
return "[Non-Serializable Object]";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return String(info);
|
|
31
|
+
}
|
|
32
|
+
isClientError() {
|
|
33
|
+
return this.status >= 400 && this.status < 500;
|
|
34
|
+
}
|
|
35
|
+
isServerError() {
|
|
36
|
+
return this.status >= 500;
|
|
37
|
+
}
|
|
38
|
+
isBadRequest() {
|
|
39
|
+
return this.status === 400;
|
|
40
|
+
}
|
|
41
|
+
isUnauthorized() {
|
|
42
|
+
return this.status === 401;
|
|
43
|
+
}
|
|
44
|
+
isForbidden() {
|
|
45
|
+
return this.status === 403;
|
|
46
|
+
}
|
|
47
|
+
isNotFound() {
|
|
48
|
+
return this.status === 404;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/core.ts
|
|
53
|
+
function createBaseFetcher(baseUrl, defaultHeaders = {}, interceptor) {
|
|
54
|
+
return async (pathname, options) => {
|
|
55
|
+
const url = `${baseUrl}${pathname}`;
|
|
56
|
+
let finalOptions = {
|
|
57
|
+
...options,
|
|
58
|
+
headers: {
|
|
59
|
+
...defaultHeaders,
|
|
60
|
+
...options?.headers
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
if (interceptor) {
|
|
64
|
+
finalOptions = await interceptor(finalOptions);
|
|
65
|
+
}
|
|
66
|
+
const res = await fetch(url, finalOptions);
|
|
67
|
+
let data = null;
|
|
68
|
+
const isJson = res.headers.get("content-type")?.includes("application/json");
|
|
69
|
+
if (isJson) {
|
|
70
|
+
data = await res.json();
|
|
71
|
+
} else {
|
|
72
|
+
data = await res.text();
|
|
73
|
+
}
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const message = isJson && data?.message || res.statusText || "Unknown API error";
|
|
76
|
+
throw new APIError(message, url, res.status, data);
|
|
77
|
+
}
|
|
78
|
+
if (res.status === 204) return null;
|
|
79
|
+
return data;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/utils.ts
|
|
84
|
+
function getPathname(...endpoint) {
|
|
85
|
+
const lastEndpoint = endpoint[endpoint.length - 1];
|
|
86
|
+
const hasQueryParams = lastEndpoint?.startsWith("?");
|
|
87
|
+
if (endpoint.length === 1 && hasQueryParams) {
|
|
88
|
+
return lastEndpoint || "";
|
|
89
|
+
}
|
|
90
|
+
const segments = endpoint.flatMap((seg) => seg.split("/")).map((seg) => seg.replace(/^\.+/, "")).filter((seg) => seg !== "" && !seg.startsWith("?"));
|
|
91
|
+
const queryString = hasQueryParams ? lastEndpoint : "";
|
|
92
|
+
const pathname = `/${segments.join("/")}${queryString}`;
|
|
93
|
+
return pathname.replace(/\/$/, "");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/factories.ts
|
|
97
|
+
function createFetcher(baseUrl, interceptor) {
|
|
98
|
+
const baseFetch = createBaseFetcher(
|
|
99
|
+
baseUrl,
|
|
100
|
+
{ "Content-Type": "application/json" },
|
|
101
|
+
interceptor
|
|
102
|
+
);
|
|
103
|
+
return (...endpoint) => {
|
|
104
|
+
const pathname = getPathname(...endpoint);
|
|
105
|
+
return (options) => baseFetch(pathname, options);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function createFormDataFetcher(baseUrl, interceptor) {
|
|
109
|
+
const baseFetch = createBaseFetcher(baseUrl, {}, interceptor);
|
|
110
|
+
return (...endpoint) => {
|
|
111
|
+
const pathname = getPathname(...endpoint);
|
|
112
|
+
return (options) => baseFetch(pathname, options);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/interceptors.ts
|
|
117
|
+
var createInterceptor = (newOptions) => async (options) => {
|
|
118
|
+
return {
|
|
119
|
+
...options,
|
|
120
|
+
...newOptions,
|
|
121
|
+
headers: {
|
|
122
|
+
...options.headers ?? {},
|
|
123
|
+
...newOptions.headers ?? {}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
var loggingInterceptor = async (options) => {
|
|
128
|
+
console.log("[Fetcher] Request:", options);
|
|
129
|
+
return options;
|
|
130
|
+
};
|
|
131
|
+
var composeInterceptors = (...interceptors) => {
|
|
132
|
+
return async (options) => {
|
|
133
|
+
let result = options;
|
|
134
|
+
for (const interceptor of interceptors) {
|
|
135
|
+
result = await interceptor(result);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
exports.APIError = APIError;
|
|
142
|
+
exports.composeInterceptors = composeInterceptors;
|
|
143
|
+
exports.createBaseFetcher = createBaseFetcher;
|
|
144
|
+
exports.createFetcher = createFetcher;
|
|
145
|
+
exports.createFormDataFetcher = createFormDataFetcher;
|
|
146
|
+
exports.createInterceptor = createInterceptor;
|
|
147
|
+
exports.loggingInterceptor = loggingInterceptor;
|
|
148
|
+
//# sourceMappingURL=index.cjs.map
|
|
149
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/core.ts","../src/utils.ts","../src/factories.ts","../src/interceptors.ts"],"names":[],"mappings":";;;AAAO,IAAM,QAAA,GAAN,MAAM,SAAA,SAAiB,KAAA,CAAM;AAAA,EACnC,MAAA;AAAA,EACA,GAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAA;AAAA,EAEA,WAAA,CAAY,OAAA,EAAiB,GAAA,EAAa,MAAA,EAAgB,IAAA,EAAgB;AACzE,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,UAAA;AACZ,IAAA,IAAA,CAAK,GAAA,GAAM,GAAA;AACX,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,IAAA,CAAK,GAAA,GAAM,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAE9B,IAAA,IAAI,MAAM,iBAAA,EAAmB;AAC5B,MAAA,KAAA,CAAM,iBAAA,CAAkB,MAAM,SAAQ,CAAA;AAAA,IACvC;AAAA,EACD;AAAA,EAEQ,UAAU,IAAA,EAAuB;AACxC,IAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,IAAA,KAAS,MAAA,EAAW,OAAO,oBAAA;AAChD,IAAA,IAAI,OAAO,IAAA,KAAS,QAAA,EAAU,OAAO,IAAA;AACrC,IAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC7B,MAAA,IAAI;AACH,QAAA,OAAO,IAAA,CAAK,SAAA,CAAU,IAAA,EAAM,IAAA,EAAM,CAAC,CAAA;AAAA,MACpC,CAAA,CAAA,MAAQ;AACP,QAAA,OAAO,2BAAA;AAAA,MACR;AAAA,IACD;AACA,IAAA,OAAO,OAAO,IAAI,CAAA;AAAA,EACnB;AAAA,EAEA,aAAA,GAAyB;AACxB,IAAA,OAAO,IAAA,CAAK,MAAA,IAAU,GAAA,IAAO,IAAA,CAAK,MAAA,GAAS,GAAA;AAAA,EAC5C;AAAA,EAEA,aAAA,GAAyB;AACxB,IAAA,OAAO,KAAK,MAAA,IAAU,GAAA;AAAA,EACvB;AAAA,EAEA,YAAA,GAAwB;AACvB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACxB;AAAA,EAEA,cAAA,GAA0B;AACzB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACxB;AAAA,EAEA,WAAA,GAAuB;AACtB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACxB;AAAA,EAEA,UAAA,GAAsB;AACrB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACxB;AACD;;;AClDO,SAAS,iBAAA,CACf,OAAA,EACA,cAAA,GAA8B,IAC9B,WAAA,EACC;AACD,EAAA,OAAO,OAAU,UAAkB,OAAA,KAAsC;AACxE,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,EAAG,QAAQ,CAAA,CAAA;AAEjC,IAAA,IAAI,YAAA,GAA4B;AAAA,MAC/B,GAAG,OAAA;AAAA,MACH,OAAA,EAAS;AAAA,QACR,GAAG,cAAA;AAAA,QACH,GAAG,OAAA,EAAS;AAAA;AACb,KACD;AAEA,IAAA,IAAI,WAAA,EAAa;AAChB,MAAA,YAAA,GAAe,MAAM,YAAY,YAAY,CAAA;AAAA,IAC9C;AAKA,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,EAAK,YAAY,CAAA;AAYzC,IAAA,IAAI,IAAA,GAAgB,IAAA;AACpB,IAAA,MAAM,SAAS,GAAA,CAAI,OAAA,CAAQ,IAAI,cAAc,CAAA,EAAG,SAAS,kBAAkB,CAAA;AAE3E,IAAA,IAAI,MAAA,EAAQ;AACX,MAAA,IAAA,GAAO,MAAM,IAAI,IAAA,EAAK;AAAA,IACvB,CAAA,MAAO;AACN,MAAA,IAAA,GAAO,MAAM,IAAI,IAAA,EAAK;AAAA,IACvB;AAEA,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACZ,MAAA,MAAM,OAAA,GACJ,MAAA,IAAW,IAAA,EAA+B,OAAA,IAC3C,IAAI,UAAA,IACJ,mBAAA;AACD,MAAA,MAAM,IAAI,QAAA,CAAS,OAAA,EAAS,GAAA,EAAK,GAAA,CAAI,QAAQ,IAAI,CAAA;AAAA,IAClD;AAEA,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,GAAA,EAAK,OAAO,IAAA;AAC/B,IAAA,OAAO,IAAA;AAAA,EACR,CAAA;AACD;;;AC5DO,SAAS,eAAe,QAAA,EAAoB;AAClD,EAAA,MAAM,YAAA,GAAe,QAAA,CAAS,QAAA,CAAS,MAAA,GAAS,CAAC,CAAA;AACjD,EAAA,MAAM,cAAA,GAAiB,YAAA,EAAc,UAAA,CAAW,GAAG,CAAA;AAEnD,EAAA,IAAI,QAAA,CAAS,MAAA,KAAW,CAAA,IAAK,cAAA,EAAgB;AAC5C,IAAA,OAAO,YAAA,IAAgB,EAAA;AAAA,EACxB;AAEA,EAAA,MAAM,QAAA,GAAW,QAAA,CACf,OAAA,CAAQ,CAAC,GAAA,KAAQ,GAAA,CAAI,KAAA,CAAM,GAAG,CAAC,CAAA,CAC/B,GAAA,CAAI,CAAC,GAAA,KAAQ,GAAA,CAAI,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,CACpC,MAAA,CAAO,CAAC,GAAA,KAAQ,GAAA,KAAQ,EAAA,IAAM,CAAC,GAAA,CAAI,UAAA,CAAW,GAAG,CAAC,CAAA;AAEpD,EAAA,MAAM,WAAA,GAAc,iBAAiB,YAAA,GAAe,EAAA;AAEpD,EAAA,MAAM,WAAW,CAAA,CAAA,EAAI,QAAA,CAAS,KAAK,GAAG,CAAC,GAAG,WAAW,CAAA,CAAA;AAErD,EAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAClC;;;ACdO,SAAS,aAAA,CACf,SACA,WAAA,EACC;AACD,EAAA,MAAM,SAAA,GAAY,iBAAA;AAAA,IACjB,OAAA;AAAA,IACA,EAAE,gBAAgB,kBAAA,EAAmB;AAAA,IACrC;AAAA,GACD;AAEA,EAAA,OAAO,IAAO,QAAA,KAAuB;AACpC,IAAA,MAAM,QAAA,GAAW,WAAA,CAAY,GAAG,QAAQ,CAAA;AACxC,IAAA,OAAO,CAAC,OAAA,KAA0B,SAAA,CAAa,QAAA,EAAU,OAAO,CAAA;AAAA,EACjE,CAAA;AACD;AAEO,SAAS,qBAAA,CACf,SACA,WAAA,EACC;AACD,EAAA,MAAM,SAAA,GAAY,iBAAA,CAAkB,OAAA,EAAS,IAAI,WAAW,CAAA;AAE5D,EAAA,OAAO,IAAO,QAAA,KAAuB;AACpC,IAAA,MAAM,QAAA,GAAW,WAAA,CAAY,GAAG,QAAQ,CAAA;AACxC,IAAA,OAAO,CAAC,OAAA,KAA0B,SAAA,CAAa,QAAA,EAAU,OAAO,CAAA;AAAA,EACjE,CAAA;AACD;;;AC5BO,IAAM,iBAAA,GACZ,CAAC,UAAA,KACD,OAAO,OAAA,KAAY;AAClB,EAAA,OAAO;AAAA,IACN,GAAG,OAAA;AAAA,IACH,GAAG,UAAA;AAAA,IACH,OAAA,EAAS;AAAA,MACR,GAAI,OAAA,CAAQ,OAAA,IAAW,EAAC;AAAA,MACxB,GAAI,UAAA,CAAW,OAAA,IAAW;AAAC;AAC5B,GACD;AACD;AAEM,IAAM,kBAAA,GAAyC,OAAO,OAAA,KAAY;AACxE,EAAA,OAAA,CAAQ,GAAA,CAAI,sBAAsB,OAAO,CAAA;AACzC,EAAA,OAAO,OAAA;AACR;AAEO,IAAM,mBAAA,GAAsB,IAC/B,YAAA,KACqB;AACxB,EAAA,OAAO,OAAO,OAAA,KAAY;AACzB,IAAA,IAAI,MAAA,GAAS,OAAA;AACb,IAAA,KAAA,MAAW,eAAe,YAAA,EAAc;AACvC,MAAA,MAAA,GAAS,MAAM,YAAY,MAAM,CAAA;AAAA,IAClC;AACA,IAAA,OAAO,MAAA;AAAA,EACR,CAAA;AACD","file":"index.cjs","sourcesContent":["export class APIError extends Error {\n\tstatus: number\n\turl: string\n\tinfo?: unknown\n\tlog: string\n\n\tconstructor(message: string, url: string, status: number, info?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = \"APIError\"\n\t\tthis.url = url\n\t\tthis.status = status\n\t\tthis.info = info\n\t\tthis.log = this.formatLog(info)\n\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, APIError)\n\t\t}\n\t}\n\n\tprivate formatLog(info: unknown): string {\n\t\tif (info === null || info === undefined) return \"No additional info\"\n\t\tif (typeof info === \"string\") return info\n\t\tif (typeof info === \"object\") {\n\t\t\ttry {\n\t\t\t\treturn JSON.stringify(info, null, 2)\n\t\t\t} catch {\n\t\t\t\treturn \"[Non-Serializable Object]\"\n\t\t\t}\n\t\t}\n\t\treturn String(info)\n\t}\n\n\tisClientError(): boolean {\n\t\treturn this.status >= 400 && this.status < 500\n\t}\n\n\tisServerError(): boolean {\n\t\treturn this.status >= 500\n\t}\n\n\tisBadRequest(): boolean {\n\t\treturn this.status === 400\n\t}\n\n\tisUnauthorized(): boolean {\n\t\treturn this.status === 401\n\t}\n\n\tisForbidden(): boolean {\n\t\treturn this.status === 403\n\t}\n\n\tisNotFound(): boolean {\n\t\treturn this.status === 404\n\t}\n}\n","import type { HeadersInit } from \"bun\"\nimport { APIError } from \"./errors\"\nimport type { FetcherInterceptor } from \"./types\"\n// import { buildCurl, buildCurlFormData } from \"./utils\";\n\nexport function createBaseFetcher(\n\tbaseUrl: string,\n\tdefaultHeaders: HeadersInit = {},\n\tinterceptor?: FetcherInterceptor,\n) {\n\treturn async <T>(pathname: string, options?: RequestInit): Promise<T> => {\n\t\tconst url = `${baseUrl}${pathname}`\n\n\t\tlet finalOptions: RequestInit = {\n\t\t\t...options,\n\t\t\theaders: {\n\t\t\t\t...defaultHeaders,\n\t\t\t\t...options?.headers,\n\t\t\t},\n\t\t}\n\n\t\tif (interceptor) {\n\t\t\tfinalOptions = await interceptor(finalOptions)\n\t\t}\n\n\t\t// console.log(url);\n\t\t// console.log(finalOptions);\n\n\t\tconst res = await fetch(url, finalOptions)\n\n\t\t// const body = finalOptions.body;\n\t\t//\n\t\t// if (body && body instanceof FormData) {\n\t\t// \tconst curl = buildCurlFormData({ url, options: finalOptions });\n\t\t// \tconsole.log(curl);\n\t\t// } else {\n\t\t// \tconst curl = buildCurl({ url, options: finalOptions });\n\t\t// \tconsole.log(curl);\n\t\t// }\n\n\t\tlet data: unknown = null\n\t\tconst isJson = res.headers.get(\"content-type\")?.includes(\"application/json\")\n\n\t\tif (isJson) {\n\t\t\tdata = await res.json()\n\t\t} else {\n\t\t\tdata = await res.text()\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tconst message =\n\t\t\t\t(isJson && (data as { message?: string })?.message) ||\n\t\t\t\tres.statusText ||\n\t\t\t\t\"Unknown API error\"\n\t\t\tthrow new APIError(message, url, res.status, data)\n\t\t}\n\n\t\tif (res.status === 204) return null as T\n\t\treturn data as T\n\t}\n}\n","export function getPathname(...endpoint: string[]) {\n\tconst lastEndpoint = endpoint[endpoint.length - 1]\n\tconst hasQueryParams = lastEndpoint?.startsWith(\"?\")\n\n\tif (endpoint.length === 1 && hasQueryParams) {\n\t\treturn lastEndpoint || \"\"\n\t}\n\n\tconst segments = endpoint\n\t\t.flatMap((seg) => seg.split(\"/\"))\n\t\t.map((seg) => seg.replace(/^\\.+/, \"\"))\n\t\t.filter((seg) => seg !== \"\" && !seg.startsWith(\"?\"))\n\n\tconst queryString = hasQueryParams ? lastEndpoint : \"\"\n\n\tconst pathname = `/${segments.join(\"/\")}${queryString}`\n\n\treturn pathname.replace(/\\/$/, \"\")\n}\n\nexport function buildCurl({\n\turl,\n\toptions,\n\textraHeaders = {},\n}: {\n\turl: string\n\toptions?: RequestInit\n\textraHeaders?: Record<string, string | undefined | null>\n}): string {\n\tconst method = options?.method || \"GET\"\n\n\tconst headers: Record<string, string> = {\n\t\t\"Content-Type\": \"application/json\",\n\t\t...(options?.headers as Record<string, string>),\n\t\t...Object.fromEntries(\n\t\t\tObject.entries(extraHeaders).filter(\n\t\t\t\t([, value]) => value !== undefined && value !== null,\n\t\t\t),\n\t\t),\n\t}\n\n\tconst body =\n\t\toptions?.body && typeof options.body === \"string\"\n\t\t\t? options.body\n\t\t\t: options?.body\n\t\t\t\t? JSON.stringify(options.body)\n\t\t\t\t: null\n\n\treturn [\n\t\t`curl -X ${method} \"${url}\"`,\n\t\t...Object.entries(headers).map(([key, value]) => `-H \"${key}: ${value}\"`),\n\t\tbody ? `-d '${body.replace(/'/g, `'\\\\''`)}'` : null,\n\t]\n\t\t.filter(Boolean)\n\t\t.join(\" \\\\\\n \")\n}\n\nexport function buildCurlFormData({\n\turl,\n\toptions,\n\textraHeaders = {},\n}: {\n\turl: string\n\toptions?: RequestInit\n\textraHeaders?: Record<string, string | undefined | null>\n}): string {\n\tconst method = options?.method || \"POST\"\n\tconst body = options?.body\n\tconst headers: Record<string, string> = {\n\t\t...(options?.headers as Record<string, string>),\n\t\t...(Object.fromEntries(\n\t\t\tObject.entries(extraHeaders).filter(\n\t\t\t\t([, value]) => value !== undefined && value !== null,\n\t\t\t),\n\t\t) as Record<string, string>),\n\t}\n\n\tconst parts: string[] = [`curl -X ${method} \"${url}\"`]\n\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (key.toLowerCase() === \"content-type\") continue\n\t\tparts.push(`-H \"${key}: ${value}\"`)\n\t}\n\n\tif (body instanceof FormData) {\n\t\tfor (const [key, value] of body.entries()) {\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: can be File\n\t\t\tconst a_value = value as any\n\t\t\tif (a_value instanceof File) {\n\t\t\t\tparts.push(`-F \"${key}=@${a_value.name}\"`)\n\t\t\t} else {\n\t\t\t\tparts.push(`-F \"${key}=${a_value}\"`)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst fallbackBody = typeof body === \"string\" ? body : JSON.stringify(body)\n\t\tparts.push(`-d '${fallbackBody.replace(/'/g, `'\\\\''`)}'`)\n\t}\n\n\treturn parts.join(\" \\\\\\n \")\n}\n","import { createBaseFetcher } from \"./core\"\nimport type { FetcherInterceptor } from \"./types\"\nimport { getPathname } from \"./utils\"\n\nexport function createFetcher(\n\tbaseUrl: string,\n\tinterceptor?: FetcherInterceptor,\n) {\n\tconst baseFetch = createBaseFetcher(\n\t\tbaseUrl,\n\t\t{ \"Content-Type\": \"application/json\" },\n\t\tinterceptor,\n\t)\n\n\treturn <T>(...endpoint: string[]) => {\n\t\tconst pathname = getPathname(...endpoint)\n\t\treturn (options?: RequestInit) => baseFetch<T>(pathname, options)\n\t}\n}\n\nexport function createFormDataFetcher(\n\tbaseUrl: string,\n\tinterceptor?: FetcherInterceptor,\n) {\n\tconst baseFetch = createBaseFetcher(baseUrl, {}, interceptor)\n\n\treturn <T>(...endpoint: string[]) => {\n\t\tconst pathname = getPathname(...endpoint)\n\t\treturn (options?: RequestInit) => baseFetch<T>(pathname, options)\n\t}\n}\n","import type { FetcherInterceptor } from \"./types\"\n\nexport const createInterceptor =\n\t(newOptions: RequestInit): FetcherInterceptor =>\n\tasync (options) => {\n\t\treturn {\n\t\t\t...options,\n\t\t\t...newOptions,\n\t\t\theaders: {\n\t\t\t\t...(options.headers ?? {}),\n\t\t\t\t...(newOptions.headers ?? {}),\n\t\t\t},\n\t\t}\n\t}\n\nexport const loggingInterceptor: FetcherInterceptor = async (options) => {\n\tconsole.log(\"[Fetcher] Request:\", options)\n\treturn options\n}\n\nexport const composeInterceptors = (\n\t...interceptors: FetcherInterceptor[]\n): FetcherInterceptor => {\n\treturn async (options) => {\n\t\tlet result = options\n\t\tfor (const interceptor of interceptors) {\n\t\t\tresult = await interceptor(result)\n\t\t}\n\t\treturn result\n\t}\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HeadersInit } from 'bun';
|
|
2
|
+
import { FetcherInterceptor } from './types.cjs';
|
|
3
|
+
export { FetcherOptions } from './types.cjs';
|
|
4
|
+
|
|
5
|
+
declare function createBaseFetcher(baseUrl: string, defaultHeaders?: HeadersInit, interceptor?: FetcherInterceptor): <T>(pathname: string, options?: RequestInit) => Promise<T>;
|
|
6
|
+
|
|
7
|
+
declare class APIError extends Error {
|
|
8
|
+
status: number;
|
|
9
|
+
url: string;
|
|
10
|
+
info?: unknown;
|
|
11
|
+
log: string;
|
|
12
|
+
constructor(message: string, url: string, status: number, info?: unknown);
|
|
13
|
+
private formatLog;
|
|
14
|
+
isClientError(): boolean;
|
|
15
|
+
isServerError(): boolean;
|
|
16
|
+
isBadRequest(): boolean;
|
|
17
|
+
isUnauthorized(): boolean;
|
|
18
|
+
isForbidden(): boolean;
|
|
19
|
+
isNotFound(): boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare function createFetcher(baseUrl: string, interceptor?: FetcherInterceptor): <T>(...endpoint: string[]) => (options?: RequestInit) => Promise<T>;
|
|
23
|
+
declare function createFormDataFetcher(baseUrl: string, interceptor?: FetcherInterceptor): <T>(...endpoint: string[]) => (options?: RequestInit) => Promise<T>;
|
|
24
|
+
|
|
25
|
+
declare const createInterceptor: (newOptions: RequestInit) => FetcherInterceptor;
|
|
26
|
+
declare const loggingInterceptor: FetcherInterceptor;
|
|
27
|
+
declare const composeInterceptors: (...interceptors: FetcherInterceptor[]) => FetcherInterceptor;
|
|
28
|
+
|
|
29
|
+
export { APIError, FetcherInterceptor, composeInterceptors, createBaseFetcher, createFetcher, createFormDataFetcher, createInterceptor, loggingInterceptor };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HeadersInit } from 'bun';
|
|
2
|
+
import { FetcherInterceptor } from './types.js';
|
|
3
|
+
export { FetcherOptions } from './types.js';
|
|
4
|
+
|
|
5
|
+
declare function createBaseFetcher(baseUrl: string, defaultHeaders?: HeadersInit, interceptor?: FetcherInterceptor): <T>(pathname: string, options?: RequestInit) => Promise<T>;
|
|
6
|
+
|
|
7
|
+
declare class APIError extends Error {
|
|
8
|
+
status: number;
|
|
9
|
+
url: string;
|
|
10
|
+
info?: unknown;
|
|
11
|
+
log: string;
|
|
12
|
+
constructor(message: string, url: string, status: number, info?: unknown);
|
|
13
|
+
private formatLog;
|
|
14
|
+
isClientError(): boolean;
|
|
15
|
+
isServerError(): boolean;
|
|
16
|
+
isBadRequest(): boolean;
|
|
17
|
+
isUnauthorized(): boolean;
|
|
18
|
+
isForbidden(): boolean;
|
|
19
|
+
isNotFound(): boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare function createFetcher(baseUrl: string, interceptor?: FetcherInterceptor): <T>(...endpoint: string[]) => (options?: RequestInit) => Promise<T>;
|
|
23
|
+
declare function createFormDataFetcher(baseUrl: string, interceptor?: FetcherInterceptor): <T>(...endpoint: string[]) => (options?: RequestInit) => Promise<T>;
|
|
24
|
+
|
|
25
|
+
declare const createInterceptor: (newOptions: RequestInit) => FetcherInterceptor;
|
|
26
|
+
declare const loggingInterceptor: FetcherInterceptor;
|
|
27
|
+
declare const composeInterceptors: (...interceptors: FetcherInterceptor[]) => FetcherInterceptor;
|
|
28
|
+
|
|
29
|
+
export { APIError, FetcherInterceptor, composeInterceptors, createBaseFetcher, createFetcher, createFormDataFetcher, createInterceptor, loggingInterceptor };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var APIError = class _APIError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
url;
|
|
5
|
+
info;
|
|
6
|
+
log;
|
|
7
|
+
constructor(message, url, status, info) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "APIError";
|
|
10
|
+
this.url = url;
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.info = info;
|
|
13
|
+
this.log = this.formatLog(info);
|
|
14
|
+
if (Error.captureStackTrace) {
|
|
15
|
+
Error.captureStackTrace(this, _APIError);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
formatLog(info) {
|
|
19
|
+
if (info === null || info === void 0) return "No additional info";
|
|
20
|
+
if (typeof info === "string") return info;
|
|
21
|
+
if (typeof info === "object") {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(info, null, 2);
|
|
24
|
+
} catch {
|
|
25
|
+
return "[Non-Serializable Object]";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return String(info);
|
|
29
|
+
}
|
|
30
|
+
isClientError() {
|
|
31
|
+
return this.status >= 400 && this.status < 500;
|
|
32
|
+
}
|
|
33
|
+
isServerError() {
|
|
34
|
+
return this.status >= 500;
|
|
35
|
+
}
|
|
36
|
+
isBadRequest() {
|
|
37
|
+
return this.status === 400;
|
|
38
|
+
}
|
|
39
|
+
isUnauthorized() {
|
|
40
|
+
return this.status === 401;
|
|
41
|
+
}
|
|
42
|
+
isForbidden() {
|
|
43
|
+
return this.status === 403;
|
|
44
|
+
}
|
|
45
|
+
isNotFound() {
|
|
46
|
+
return this.status === 404;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/core.ts
|
|
51
|
+
function createBaseFetcher(baseUrl, defaultHeaders = {}, interceptor) {
|
|
52
|
+
return async (pathname, options) => {
|
|
53
|
+
const url = `${baseUrl}${pathname}`;
|
|
54
|
+
let finalOptions = {
|
|
55
|
+
...options,
|
|
56
|
+
headers: {
|
|
57
|
+
...defaultHeaders,
|
|
58
|
+
...options?.headers
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
if (interceptor) {
|
|
62
|
+
finalOptions = await interceptor(finalOptions);
|
|
63
|
+
}
|
|
64
|
+
const res = await fetch(url, finalOptions);
|
|
65
|
+
let data = null;
|
|
66
|
+
const isJson = res.headers.get("content-type")?.includes("application/json");
|
|
67
|
+
if (isJson) {
|
|
68
|
+
data = await res.json();
|
|
69
|
+
} else {
|
|
70
|
+
data = await res.text();
|
|
71
|
+
}
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const message = isJson && data?.message || res.statusText || "Unknown API error";
|
|
74
|
+
throw new APIError(message, url, res.status, data);
|
|
75
|
+
}
|
|
76
|
+
if (res.status === 204) return null;
|
|
77
|
+
return data;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/utils.ts
|
|
82
|
+
function getPathname(...endpoint) {
|
|
83
|
+
const lastEndpoint = endpoint[endpoint.length - 1];
|
|
84
|
+
const hasQueryParams = lastEndpoint?.startsWith("?");
|
|
85
|
+
if (endpoint.length === 1 && hasQueryParams) {
|
|
86
|
+
return lastEndpoint || "";
|
|
87
|
+
}
|
|
88
|
+
const segments = endpoint.flatMap((seg) => seg.split("/")).map((seg) => seg.replace(/^\.+/, "")).filter((seg) => seg !== "" && !seg.startsWith("?"));
|
|
89
|
+
const queryString = hasQueryParams ? lastEndpoint : "";
|
|
90
|
+
const pathname = `/${segments.join("/")}${queryString}`;
|
|
91
|
+
return pathname.replace(/\/$/, "");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/factories.ts
|
|
95
|
+
function createFetcher(baseUrl, interceptor) {
|
|
96
|
+
const baseFetch = createBaseFetcher(
|
|
97
|
+
baseUrl,
|
|
98
|
+
{ "Content-Type": "application/json" },
|
|
99
|
+
interceptor
|
|
100
|
+
);
|
|
101
|
+
return (...endpoint) => {
|
|
102
|
+
const pathname = getPathname(...endpoint);
|
|
103
|
+
return (options) => baseFetch(pathname, options);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function createFormDataFetcher(baseUrl, interceptor) {
|
|
107
|
+
const baseFetch = createBaseFetcher(baseUrl, {}, interceptor);
|
|
108
|
+
return (...endpoint) => {
|
|
109
|
+
const pathname = getPathname(...endpoint);
|
|
110
|
+
return (options) => baseFetch(pathname, options);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/interceptors.ts
|
|
115
|
+
var createInterceptor = (newOptions) => async (options) => {
|
|
116
|
+
return {
|
|
117
|
+
...options,
|
|
118
|
+
...newOptions,
|
|
119
|
+
headers: {
|
|
120
|
+
...options.headers ?? {},
|
|
121
|
+
...newOptions.headers ?? {}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
var loggingInterceptor = async (options) => {
|
|
126
|
+
console.log("[Fetcher] Request:", options);
|
|
127
|
+
return options;
|
|
128
|
+
};
|
|
129
|
+
var composeInterceptors = (...interceptors) => {
|
|
130
|
+
return async (options) => {
|
|
131
|
+
let result = options;
|
|
132
|
+
for (const interceptor of interceptors) {
|
|
133
|
+
result = await interceptor(result);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export { APIError, composeInterceptors, createBaseFetcher, createFetcher, createFormDataFetcher, createInterceptor, loggingInterceptor };
|
|
140
|
+
//# sourceMappingURL=index.js.map
|
|
141
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/core.ts","../src/utils.ts","../src/factories.ts","../src/interceptors.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export class APIError extends Error {\n\tstatus: number\n\turl: string\n\tinfo?: unknown\n\tlog: string\n\n\tconstructor(message: string, url: string, status: number, info?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = \"APIError\"\n\t\tthis.url = url\n\t\tthis.status = status\n\t\tthis.info = info\n\t\tthis.log = this.formatLog(info)\n\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, APIError)\n\t\t}\n\t}\n\n\tprivate formatLog(info: unknown): string {\n\t\tif (info === null || info === undefined) return \"No additional info\"\n\t\tif (typeof info === \"string\") return info\n\t\tif (typeof info === \"object\") {\n\t\t\ttry {\n\t\t\t\treturn JSON.stringify(info, null, 2)\n\t\t\t} catch {\n\t\t\t\treturn \"[Non-Serializable Object]\"\n\t\t\t}\n\t\t}\n\t\treturn String(info)\n\t}\n\n\tisClientError(): boolean {\n\t\treturn this.status >= 400 && this.status < 500\n\t}\n\n\tisServerError(): boolean {\n\t\treturn this.status >= 500\n\t}\n\n\tisBadRequest(): boolean {\n\t\treturn this.status === 400\n\t}\n\n\tisUnauthorized(): boolean {\n\t\treturn this.status === 401\n\t}\n\n\tisForbidden(): boolean {\n\t\treturn this.status === 403\n\t}\n\n\tisNotFound(): boolean {\n\t\treturn this.status === 404\n\t}\n}\n","import type { HeadersInit } from \"bun\"\nimport { APIError } from \"./errors\"\nimport type { FetcherInterceptor } from \"./types\"\n// import { buildCurl, buildCurlFormData } from \"./utils\";\n\nexport function createBaseFetcher(\n\tbaseUrl: string,\n\tdefaultHeaders: HeadersInit = {},\n\tinterceptor?: FetcherInterceptor,\n) {\n\treturn async <T>(pathname: string, options?: RequestInit): Promise<T> => {\n\t\tconst url = `${baseUrl}${pathname}`\n\n\t\tlet finalOptions: RequestInit = {\n\t\t\t...options,\n\t\t\theaders: {\n\t\t\t\t...defaultHeaders,\n\t\t\t\t...options?.headers,\n\t\t\t},\n\t\t}\n\n\t\tif (interceptor) {\n\t\t\tfinalOptions = await interceptor(finalOptions)\n\t\t}\n\n\t\t// console.log(url);\n\t\t// console.log(finalOptions);\n\n\t\tconst res = await fetch(url, finalOptions)\n\n\t\t// const body = finalOptions.body;\n\t\t//\n\t\t// if (body && body instanceof FormData) {\n\t\t// \tconst curl = buildCurlFormData({ url, options: finalOptions });\n\t\t// \tconsole.log(curl);\n\t\t// } else {\n\t\t// \tconst curl = buildCurl({ url, options: finalOptions });\n\t\t// \tconsole.log(curl);\n\t\t// }\n\n\t\tlet data: unknown = null\n\t\tconst isJson = res.headers.get(\"content-type\")?.includes(\"application/json\")\n\n\t\tif (isJson) {\n\t\t\tdata = await res.json()\n\t\t} else {\n\t\t\tdata = await res.text()\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tconst message =\n\t\t\t\t(isJson && (data as { message?: string })?.message) ||\n\t\t\t\tres.statusText ||\n\t\t\t\t\"Unknown API error\"\n\t\t\tthrow new APIError(message, url, res.status, data)\n\t\t}\n\n\t\tif (res.status === 204) return null as T\n\t\treturn data as T\n\t}\n}\n","export function getPathname(...endpoint: string[]) {\n\tconst lastEndpoint = endpoint[endpoint.length - 1]\n\tconst hasQueryParams = lastEndpoint?.startsWith(\"?\")\n\n\tif (endpoint.length === 1 && hasQueryParams) {\n\t\treturn lastEndpoint || \"\"\n\t}\n\n\tconst segments = endpoint\n\t\t.flatMap((seg) => seg.split(\"/\"))\n\t\t.map((seg) => seg.replace(/^\\.+/, \"\"))\n\t\t.filter((seg) => seg !== \"\" && !seg.startsWith(\"?\"))\n\n\tconst queryString = hasQueryParams ? lastEndpoint : \"\"\n\n\tconst pathname = `/${segments.join(\"/\")}${queryString}`\n\n\treturn pathname.replace(/\\/$/, \"\")\n}\n\nexport function buildCurl({\n\turl,\n\toptions,\n\textraHeaders = {},\n}: {\n\turl: string\n\toptions?: RequestInit\n\textraHeaders?: Record<string, string | undefined | null>\n}): string {\n\tconst method = options?.method || \"GET\"\n\n\tconst headers: Record<string, string> = {\n\t\t\"Content-Type\": \"application/json\",\n\t\t...(options?.headers as Record<string, string>),\n\t\t...Object.fromEntries(\n\t\t\tObject.entries(extraHeaders).filter(\n\t\t\t\t([, value]) => value !== undefined && value !== null,\n\t\t\t),\n\t\t),\n\t}\n\n\tconst body =\n\t\toptions?.body && typeof options.body === \"string\"\n\t\t\t? options.body\n\t\t\t: options?.body\n\t\t\t\t? JSON.stringify(options.body)\n\t\t\t\t: null\n\n\treturn [\n\t\t`curl -X ${method} \"${url}\"`,\n\t\t...Object.entries(headers).map(([key, value]) => `-H \"${key}: ${value}\"`),\n\t\tbody ? `-d '${body.replace(/'/g, `'\\\\''`)}'` : null,\n\t]\n\t\t.filter(Boolean)\n\t\t.join(\" \\\\\\n \")\n}\n\nexport function buildCurlFormData({\n\turl,\n\toptions,\n\textraHeaders = {},\n}: {\n\turl: string\n\toptions?: RequestInit\n\textraHeaders?: Record<string, string | undefined | null>\n}): string {\n\tconst method = options?.method || \"POST\"\n\tconst body = options?.body\n\tconst headers: Record<string, string> = {\n\t\t...(options?.headers as Record<string, string>),\n\t\t...(Object.fromEntries(\n\t\t\tObject.entries(extraHeaders).filter(\n\t\t\t\t([, value]) => value !== undefined && value !== null,\n\t\t\t),\n\t\t) as Record<string, string>),\n\t}\n\n\tconst parts: string[] = [`curl -X ${method} \"${url}\"`]\n\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (key.toLowerCase() === \"content-type\") continue\n\t\tparts.push(`-H \"${key}: ${value}\"`)\n\t}\n\n\tif (body instanceof FormData) {\n\t\tfor (const [key, value] of body.entries()) {\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: can be File\n\t\t\tconst a_value = value as any\n\t\t\tif (a_value instanceof File) {\n\t\t\t\tparts.push(`-F \"${key}=@${a_value.name}\"`)\n\t\t\t} else {\n\t\t\t\tparts.push(`-F \"${key}=${a_value}\"`)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst fallbackBody = typeof body === \"string\" ? body : JSON.stringify(body)\n\t\tparts.push(`-d '${fallbackBody.replace(/'/g, `'\\\\''`)}'`)\n\t}\n\n\treturn parts.join(\" \\\\\\n \")\n}\n","import { createBaseFetcher } from \"./core\"\nimport type { FetcherInterceptor } from \"./types\"\nimport { getPathname } from \"./utils\"\n\nexport function createFetcher(\n\tbaseUrl: string,\n\tinterceptor?: FetcherInterceptor,\n) {\n\tconst baseFetch = createBaseFetcher(\n\t\tbaseUrl,\n\t\t{ \"Content-Type\": \"application/json\" },\n\t\tinterceptor,\n\t)\n\n\treturn <T>(...endpoint: string[]) => {\n\t\tconst pathname = getPathname(...endpoint)\n\t\treturn (options?: RequestInit) => baseFetch<T>(pathname, options)\n\t}\n}\n\nexport function createFormDataFetcher(\n\tbaseUrl: string,\n\tinterceptor?: FetcherInterceptor,\n) {\n\tconst baseFetch = createBaseFetcher(baseUrl, {}, interceptor)\n\n\treturn <T>(...endpoint: string[]) => {\n\t\tconst pathname = getPathname(...endpoint)\n\t\treturn (options?: RequestInit) => baseFetch<T>(pathname, options)\n\t}\n}\n","import type { FetcherInterceptor } from \"./types\"\n\nexport const createInterceptor =\n\t(newOptions: RequestInit): FetcherInterceptor =>\n\tasync (options) => {\n\t\treturn {\n\t\t\t...options,\n\t\t\t...newOptions,\n\t\t\theaders: {\n\t\t\t\t...(options.headers ?? {}),\n\t\t\t\t...(newOptions.headers ?? {}),\n\t\t\t},\n\t\t}\n\t}\n\nexport const loggingInterceptor: FetcherInterceptor = async (options) => {\n\tconsole.log(\"[Fetcher] Request:\", options)\n\treturn options\n}\n\nexport const composeInterceptors = (\n\t...interceptors: FetcherInterceptor[]\n): FetcherInterceptor => {\n\treturn async (options) => {\n\t\tlet result = options\n\t\tfor (const interceptor of interceptors) {\n\t\t\tresult = await interceptor(result)\n\t\t}\n\t\treturn result\n\t}\n}\n"]}
|
package/dist/types.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"types.cjs"}
|
package/dist/types.d.cts
ADDED
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"types.js"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farhantallei/fetcher",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Lightweight, composable request interceptor system for Fetch API.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"han-kit",
|
|
8
|
+
"fetch",
|
|
9
|
+
"interceptor",
|
|
10
|
+
"middleware",
|
|
11
|
+
"request"
|
|
12
|
+
],
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Farhan Pradana Tallei",
|
|
15
|
+
"email": "farhan.pradana55@gmail.com"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup --config tsup.config.ts",
|
|
19
|
+
"lint": "biome check",
|
|
20
|
+
"format": "biome check --write --unsafe",
|
|
21
|
+
"publint": "publint",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"prepare": "bun run build"
|
|
25
|
+
},
|
|
26
|
+
"module": "src/index.ts",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"registry": "https://registry.npmjs.org/",
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/Han-Kit/fetcher",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "2.3.15",
|
|
39
|
+
"@types/bun": "latest",
|
|
40
|
+
"publint": "^0.3.17",
|
|
41
|
+
"tsup": "^8.5.1"
|
|
42
|
+
},
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"import": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"default": "./dist/index.js"
|
|
48
|
+
},
|
|
49
|
+
"require": {
|
|
50
|
+
"types": "./dist/index.d.cts",
|
|
51
|
+
"default": "./dist/index.cjs"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"./types": {
|
|
55
|
+
"types": "./dist/types.d.ts"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"typescript": "^5"
|
|
60
|
+
}
|
|
61
|
+
}
|