@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 ADDED
@@ -0,0 +1,15 @@
1
+ # fetcher
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.7. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
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":"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","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"]}
@@ -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 };
@@ -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,4 @@
1
+ 'use strict';
2
+
3
+ //# sourceMappingURL=types.cjs.map
4
+ //# sourceMappingURL=types.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"types.cjs"}
@@ -0,0 +1,5 @@
1
+ type FetcherInterceptor = (options: RequestInit) => Promise<RequestInit> | RequestInit;
2
+ interface FetcherOptions extends RequestInit {
3
+ }
4
+
5
+ export type { FetcherInterceptor, FetcherOptions };
@@ -0,0 +1,5 @@
1
+ type FetcherInterceptor = (options: RequestInit) => Promise<RequestInit> | RequestInit;
2
+ interface FetcherOptions extends RequestInit {
3
+ }
4
+
5
+ export type { FetcherInterceptor, FetcherOptions };
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+
2
+ //# sourceMappingURL=types.js.map
3
+ //# sourceMappingURL=types.js.map
@@ -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
+ }