@zimic/fetch 0.1.0-canary.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/dist/index.mjs ADDED
@@ -0,0 +1,188 @@
1
+ import { HttpSearchParams } from '@zimic/http';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
+
6
+ // src/client/errors/FetchResponseError.ts
7
+ var FetchResponseError = class extends Error {
8
+ constructor(request, response) {
9
+ super(`${request.method} ${request.url} failed with status ${response.status}: ${response.statusText}`);
10
+ this.request = request;
11
+ this.response = response;
12
+ this.name = "FetchResponseError";
13
+ }
14
+ static {
15
+ __name(this, "FetchResponseError");
16
+ }
17
+ get cause() {
18
+ return this.response;
19
+ }
20
+ };
21
+ var FetchResponseError_default = FetchResponseError;
22
+
23
+ // src/utils/urls.ts
24
+ function excludeNonPathParams(url) {
25
+ url.hash = "";
26
+ url.search = "";
27
+ url.username = "";
28
+ url.password = "";
29
+ return url;
30
+ }
31
+ __name(excludeNonPathParams, "excludeNonPathParams");
32
+ function prepareURLForRegex(url) {
33
+ const encodedURL = encodeURI(url);
34
+ return encodedURL.replace(/([.()*?+$\\])/g, "\\$1");
35
+ }
36
+ __name(prepareURLForRegex, "prepareURLForRegex");
37
+ var URL_PATH_PARAM_REGEX = /\/:([^/]+)/g;
38
+ function createRegexFromURL(url) {
39
+ const urlWithReplacedPathParams = prepareURLForRegex(url).replace(URL_PATH_PARAM_REGEX, "/(?<$1>[^/]+)").replace(/(\/+)$/, "(?:/+)?");
40
+ return new RegExp(`^${urlWithReplacedPathParams}$`);
41
+ }
42
+ __name(createRegexFromURL, "createRegexFromURL");
43
+ function joinURL(...parts) {
44
+ return parts.map((part, index) => {
45
+ const isFirstPart = index === 0;
46
+ const isLastPart = index === parts.length - 1;
47
+ let partAsString = part.toString();
48
+ if (!isFirstPart) {
49
+ partAsString = partAsString.replace(/^\//, "");
50
+ }
51
+ if (!isLastPart) {
52
+ partAsString = partAsString.replace(/\/$/, "");
53
+ }
54
+ return partAsString;
55
+ }).filter((part) => part.length > 0).join("/");
56
+ }
57
+ __name(joinURL, "joinURL");
58
+
59
+ // src/client/FetchClient.ts
60
+ var FetchClient = class {
61
+ static {
62
+ __name(this, "FetchClient");
63
+ }
64
+ fetch;
65
+ constructor({ onRequest, onResponse, ...defaults }) {
66
+ this.fetch = this.createFetchFunction();
67
+ this.fetch.defaults = defaults;
68
+ this.fetch.Request = this.createRequestClass(defaults);
69
+ this.fetch.onRequest = onRequest;
70
+ this.fetch.onResponse = onResponse;
71
+ }
72
+ createFetchFunction() {
73
+ const fetch = /* @__PURE__ */ __name(async (input, init) => {
74
+ const request = await this.createFetchRequest(input, init);
75
+ const requestClone = request.clone();
76
+ const rawResponse = await globalThis.fetch(
77
+ // Optimize type checking by narrowing the type of request
78
+ requestClone
79
+ );
80
+ const response = await this.createFetchResponse(request, rawResponse);
81
+ return response;
82
+ }, "fetch");
83
+ Object.setPrototypeOf(fetch, this);
84
+ return fetch;
85
+ }
86
+ async createFetchRequest(input, init) {
87
+ let request = input instanceof Request ? input : new this.fetch.Request(input, init);
88
+ if (this.fetch.onRequest) {
89
+ const requestAfterInterceptor = await this.fetch.onRequest(
90
+ // Optimize type checking by narrowing the type of request
91
+ request,
92
+ this.fetch
93
+ );
94
+ if (requestAfterInterceptor !== request) {
95
+ const isFetchRequest = requestAfterInterceptor instanceof this.fetch.Request;
96
+ request = isFetchRequest ? requestAfterInterceptor : new this.fetch.Request(requestAfterInterceptor, init);
97
+ }
98
+ }
99
+ return request;
100
+ }
101
+ async createFetchResponse(fetchRequest, rawResponse) {
102
+ let response = this.defineFetchResponseProperties(fetchRequest, rawResponse);
103
+ if (this.fetch.onResponse) {
104
+ const responseAfterInterceptor = await this.fetch.onResponse(
105
+ // Optimize type checking by narrowing the type of response
106
+ response,
107
+ this.fetch
108
+ );
109
+ const isFetchResponse = responseAfterInterceptor instanceof Response && "request" in responseAfterInterceptor && responseAfterInterceptor.request instanceof this.fetch.Request;
110
+ response = isFetchResponse ? responseAfterInterceptor : this.defineFetchResponseProperties(fetchRequest, responseAfterInterceptor);
111
+ }
112
+ return response;
113
+ }
114
+ defineFetchResponseProperties(fetchRequest, response) {
115
+ const fetchResponse = response;
116
+ Object.defineProperty(fetchResponse, "request", {
117
+ value: fetchRequest,
118
+ writable: false,
119
+ enumerable: true,
120
+ configurable: false
121
+ });
122
+ const responseError = fetchResponse.ok ? null : new FetchResponseError_default(fetchRequest, fetchResponse);
123
+ Object.defineProperty(fetchResponse, "error", {
124
+ value: responseError,
125
+ writable: false,
126
+ enumerable: true,
127
+ configurable: false
128
+ });
129
+ return fetchResponse;
130
+ }
131
+ createRequestClass(defaults) {
132
+ class Request2 extends globalThis.Request {
133
+ static {
134
+ __name(this, "Request");
135
+ }
136
+ path;
137
+ constructor(input, rawInit) {
138
+ const init = { ...defaults, ...rawInit };
139
+ let url;
140
+ if (input instanceof globalThis.Request) {
141
+ super(
142
+ // Optimize type checking by narrowing the type of input
143
+ input,
144
+ init
145
+ );
146
+ url = new URL(input.url);
147
+ } else {
148
+ url = input instanceof URL ? new URL(input) : new URL(joinURL(init.baseURL, input));
149
+ if (init.searchParams) {
150
+ url.search = new HttpSearchParams(init.searchParams).toString();
151
+ }
152
+ super(url, init);
153
+ }
154
+ this.path = excludeNonPathParams(url).toString().replace(init.baseURL, "");
155
+ }
156
+ clone() {
157
+ const rawClone = super.clone();
158
+ return new Request2(
159
+ rawClone,
160
+ rawClone
161
+ );
162
+ }
163
+ }
164
+ return Request2;
165
+ }
166
+ isRequest(request, path, method) {
167
+ return request instanceof Request && request.method === method && "path" in request && typeof request.path === "string" && createRegexFromURL(path).test(request.path);
168
+ }
169
+ isResponse(response, path, method) {
170
+ return response instanceof Response && "request" in response && "error" in response && this.isRequest(response.request, path, method);
171
+ }
172
+ isResponseError(error, path, method) {
173
+ return error instanceof FetchResponseError_default && error.request.method === method && typeof error.request.path === "string" && createRegexFromURL(path).test(error.request.path);
174
+ }
175
+ };
176
+ var FetchClient_default = FetchClient;
177
+
178
+ // src/client/factory.ts
179
+ function createFetch(options) {
180
+ const { fetch } = new FetchClient_default(options);
181
+ return fetch;
182
+ }
183
+ __name(createFetch, "createFetch");
184
+ var factory_default = createFetch;
185
+
186
+ export { FetchResponseError_default as FetchResponseError, factory_default as createFetch };
187
+ //# sourceMappingURL=index.mjs.map
188
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/errors/FetchResponseError.ts","../src/utils/urls.ts","../src/client/FetchClient.ts","../src/client/factory.ts"],"names":["Request"],"mappings":"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","file":"index.mjs","sourcesContent":["import { HttpMethod, HttpMethodSchema } from '@zimic/http';\n\nimport { FetchRequest, FetchResponse } from '../types/requests';\n\nclass FetchResponseError<\n Path extends string = string,\n Method extends HttpMethod = HttpMethod,\n MethodSchema extends HttpMethodSchema = HttpMethodSchema,\n> extends Error {\n constructor(\n public request: FetchRequest<Path, Method, MethodSchema>,\n public response: FetchResponse<Path, Method, MethodSchema, true>,\n ) {\n super(`${request.method} ${request.url} failed with status ${response.status}: ${response.statusText}`);\n this.name = 'FetchResponseError';\n }\n\n get cause() {\n return this.response;\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyFetchRequestError = FetchResponseError<any, any, any>;\n\nexport default FetchResponseError;\n","export function excludeNonPathParams(url: URL) {\n url.hash = '';\n url.search = '';\n url.username = '';\n url.password = '';\n return url;\n}\n\nfunction prepareURLForRegex(url: string) {\n const encodedURL = encodeURI(url);\n return encodedURL.replace(/([.()*?+$\\\\])/g, '\\\\$1');\n}\n\nconst URL_PATH_PARAM_REGEX = /\\/:([^/]+)/g;\n\nexport function createRegexFromURL(url: string) {\n const urlWithReplacedPathParams = prepareURLForRegex(url)\n .replace(URL_PATH_PARAM_REGEX, '/(?<$1>[^/]+)')\n .replace(/(\\/+)$/, '(?:/+)?');\n\n return new RegExp(`^${urlWithReplacedPathParams}$`);\n}\n\nexport function joinURL(...parts: (string | URL)[]) {\n return parts\n .map((part, index) => {\n const isFirstPart = index === 0;\n const isLastPart = index === parts.length - 1;\n\n let partAsString = part.toString();\n\n if (!isFirstPart) {\n partAsString = partAsString.replace(/^\\//, '');\n }\n if (!isLastPart) {\n partAsString = partAsString.replace(/\\/$/, '');\n }\n\n return partAsString;\n })\n .filter((part) => part.length > 0)\n .join('/');\n}\n","import {\n HttpSchemaPath,\n HttpSchemaMethod,\n HttpSearchParams,\n LiteralHttpSchemaPathFromNonLiteral,\n HttpSchema,\n} from '@zimic/http';\n\nimport { Default } from '@/types/utils';\nimport { createRegexFromURL, excludeNonPathParams, joinURL } from '@/utils/urls';\n\nimport FetchResponseError from './errors/FetchResponseError';\nimport { FetchInput, FetchOptions, Fetch } from './types/public';\nimport { FetchRequestConstructor, FetchRequestInit, FetchRequest, FetchResponse } from './types/requests';\n\nclass FetchClient<Schema extends HttpSchema> {\n fetch: Fetch<Schema>;\n\n constructor({ onRequest, onResponse, ...defaults }: FetchOptions<Schema>) {\n this.fetch = this.createFetchFunction();\n this.fetch.defaults = defaults;\n this.fetch.Request = this.createRequestClass(defaults);\n this.fetch.onRequest = onRequest;\n this.fetch.onResponse = onResponse;\n }\n\n private createFetchFunction() {\n const fetch = async <\n Path extends HttpSchemaPath.NonLiteral<Schema, Method>,\n Method extends HttpSchemaMethod<Schema>,\n >(\n input: FetchInput<Schema, Path, Method>,\n init: FetchRequestInit<Schema, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>, Method>,\n ) => {\n const request = await this.createFetchRequest<Path, Method>(input, init);\n const requestClone = request.clone();\n\n const rawResponse = await globalThis.fetch(\n // Optimize type checking by narrowing the type of request\n requestClone as Request,\n );\n const response = await this.createFetchResponse<\n LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>,\n Method\n >(request, rawResponse);\n\n return response;\n };\n\n Object.setPrototypeOf(fetch, this);\n\n return fetch as Fetch<Schema>;\n }\n\n private async createFetchRequest<\n Path extends HttpSchemaPath.NonLiteral<Schema, Method>,\n Method extends HttpSchemaMethod<Schema>,\n >(\n input: FetchInput<Schema, Path, Method>,\n init: FetchRequestInit<Schema, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>, Method>,\n ) {\n let request = input instanceof Request ? input : new this.fetch.Request(input, init);\n\n if (this.fetch.onRequest) {\n const requestAfterInterceptor = await this.fetch.onRequest(\n // Optimize type checking by narrowing the type of request\n request as FetchRequest.Loose,\n this.fetch,\n );\n\n if (requestAfterInterceptor !== request) {\n const isFetchRequest = requestAfterInterceptor instanceof this.fetch.Request;\n\n request = isFetchRequest\n ? (requestAfterInterceptor as Request as typeof request)\n : new this.fetch.Request(requestAfterInterceptor as FetchInput<Schema, Path, Method>, init);\n }\n }\n\n return request;\n }\n\n private async createFetchResponse<\n Path extends HttpSchemaPath<Schema, Method>,\n Method extends HttpSchemaMethod<Schema>,\n >(fetchRequest: FetchRequest<Path, Method, Default<Schema[Path][Method]>>, rawResponse: Response) {\n let response = this.defineFetchResponseProperties<Path, Method>(fetchRequest, rawResponse);\n\n if (this.fetch.onResponse) {\n const responseAfterInterceptor = await this.fetch.onResponse(\n // Optimize type checking by narrowing the type of response\n response as FetchResponse.Loose,\n this.fetch,\n );\n\n const isFetchResponse =\n responseAfterInterceptor instanceof Response &&\n 'request' in responseAfterInterceptor &&\n responseAfterInterceptor.request instanceof this.fetch.Request;\n\n response = isFetchResponse\n ? (responseAfterInterceptor as typeof response)\n : this.defineFetchResponseProperties<Path, Method>(fetchRequest, responseAfterInterceptor);\n }\n\n return response;\n }\n\n private defineFetchResponseProperties<\n Path extends HttpSchemaPath<Schema, Method>,\n Method extends HttpSchemaMethod<Schema>,\n >(fetchRequest: FetchRequest<Path, Method, Default<Schema[Path][Method]>>, response: Response) {\n const fetchResponse = response as FetchResponse<Path, Method, Default<Schema[Path][Method]>>;\n\n Object.defineProperty(fetchResponse, 'request', {\n value: fetchRequest satisfies FetchResponse.Loose['request'],\n writable: false,\n enumerable: true,\n configurable: false,\n });\n\n const responseError = (\n fetchResponse.ok ? null : new FetchResponseError(fetchRequest, fetchResponse)\n ) satisfies FetchResponse.Loose['error'];\n\n Object.defineProperty(fetchResponse, 'error', {\n value: responseError,\n writable: false,\n enumerable: true,\n configurable: false,\n });\n\n return fetchResponse;\n }\n\n private createRequestClass(defaults: FetchRequestInit.Defaults) {\n class Request<\n Path extends HttpSchemaPath.NonLiteral<Schema, Method>,\n Method extends HttpSchemaMethod<Schema>,\n > extends globalThis.Request {\n path: LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>;\n\n constructor(\n input: FetchInput<Schema, Path, Method>,\n rawInit: FetchRequestInit<Schema, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>, Method>,\n ) {\n const init = { ...defaults, ...rawInit };\n\n let url: URL;\n\n if (input instanceof globalThis.Request) {\n super(\n // Optimize type checking by narrowing the type of input\n input as globalThis.Request,\n init,\n );\n\n url = new URL(input.url);\n } else {\n url = input instanceof URL ? new URL(input) : new URL(joinURL(init.baseURL, input));\n\n if (init.searchParams) {\n url.search = new HttpSearchParams(init.searchParams).toString();\n }\n\n super(url, init);\n }\n\n this.path = excludeNonPathParams(url)\n .toString()\n .replace(init.baseURL, '') as LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>;\n }\n\n clone(): Request<Path, Method> {\n const rawClone = super.clone();\n\n return new Request<Path, Method>(\n rawClone as unknown as FetchInput<Schema, Path, Method>,\n rawClone as unknown as FetchRequestInit<\n Schema,\n LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>,\n Method\n >,\n );\n }\n }\n\n return Request as FetchRequestConstructor<Schema>;\n }\n\n isRequest<Path extends HttpSchemaPath<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(\n request: unknown,\n path: Path,\n method: Method,\n ): request is FetchRequest<Path, Method, Default<Schema[Path][Method]>> {\n return (\n request instanceof Request &&\n request.method === method &&\n 'path' in request &&\n typeof request.path === 'string' &&\n createRegexFromURL(path).test(request.path)\n );\n }\n\n isResponse<Path extends HttpSchemaPath<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(\n response: unknown,\n path: Path,\n method: Method,\n ): response is FetchResponse<Path, Method, Default<Schema[Path][Method]>> {\n return (\n response instanceof Response &&\n 'request' in response &&\n 'error' in response &&\n this.isRequest(response.request, path, method)\n );\n }\n\n isResponseError<Path extends HttpSchemaPath<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(\n error: unknown,\n path: Path,\n method: Method,\n ): error is FetchResponseError<Path, Method, Default<Schema[Path][Method]>> {\n return (\n error instanceof FetchResponseError &&\n error.request.method === method &&\n typeof error.request.path === 'string' &&\n createRegexFromURL(path).test(error.request.path)\n );\n }\n}\n\nexport default FetchClient;\n","import { HttpSchema } from '@zimic/http';\n\nimport FetchClient from './FetchClient';\nimport { FetchOptions, Fetch as PublicFetch } from './types/public';\n\nfunction createFetch<Schema extends HttpSchema>(\n options: FetchOptions<HttpSchema<Schema>>,\n): PublicFetch<HttpSchema<Schema>> {\n const { fetch } = new FetchClient<HttpSchema<Schema>>(options);\n return fetch;\n}\n\nexport default createFetch;\n"]}
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './dist';
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@zimic/fetch",
3
+ "description": "TypeScript-first Fetch Client",
4
+ "keywords": [
5
+ "zimic",
6
+ "fetch",
7
+ "typescript",
8
+ "types",
9
+ "typegen",
10
+ "validation",
11
+ "inference",
12
+ "http",
13
+ "api",
14
+ "static"
15
+ ],
16
+ "version": "0.1.0-canary.0",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/zimicjs/zimic.git",
20
+ "directory": "packages/zimic-fetch"
21
+ },
22
+ "author": {
23
+ "name": "Diego Aquino",
24
+ "url": "https://github.com/diego-aquino"
25
+ },
26
+ "private": false,
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "provenance": true
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "license": "MIT",
35
+ "files": [
36
+ "package.json",
37
+ "README.md",
38
+ "LICENSE.md",
39
+ "src",
40
+ "!src/**/tests",
41
+ "!src/**/__tests__",
42
+ "!src/**/*.test.ts",
43
+ "dist",
44
+ "index.d.ts"
45
+ ],
46
+ "main": "./dist/index.js",
47
+ "module": "./dist/index.mjs",
48
+ "types": "index.d.ts",
49
+ "exports": {
50
+ ".": {
51
+ "types": "./dist/index.d.ts",
52
+ "import": "./dist/index.mjs",
53
+ "require": "./dist/index.js",
54
+ "default": "./dist/index.js"
55
+ },
56
+ "./package.json": "./package.json"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^22.13.5",
60
+ "@vitest/browser": "^3.0.6",
61
+ "@vitest/coverage-istanbul": "^3.0.6",
62
+ "dotenv-cli": "^8.0.0",
63
+ "eslint": "^9.21.0",
64
+ "playwright": "^1.50.1",
65
+ "tsup": "^8.3.6",
66
+ "typescript": "^5.7.3",
67
+ "vitest": "^3.0.6",
68
+ "@zimic/eslint-config-node": "0.0.0",
69
+ "@zimic/lint-staged-config": "0.0.0",
70
+ "@zimic/tsconfig": "0.0.0",
71
+ "zimic": "0.14.0-canary.10"
72
+ },
73
+ "peerDependencies": {
74
+ "@zimic/http": ">=0.1.0-canary.0 <0.2.0",
75
+ "typescript": ">=4.8.0"
76
+ },
77
+ "scripts": {
78
+ "dev": "tsup --watch",
79
+ "cli": "node ./dist/cli.js",
80
+ "build": "tsup",
81
+ "lint": "eslint --cache --no-error-on-unmatched-pattern --no-warn-ignored --fix",
82
+ "lint:turbo": "pnpm lint . --max-warnings 0",
83
+ "style": "prettier --log-level warn --ignore-unknown --no-error-on-unmatched-pattern --cache",
84
+ "style:check": "pnpm style --check",
85
+ "style:format": "pnpm style --write",
86
+ "test": "dotenv -v NODE_ENV=test -- vitest",
87
+ "test:turbo": "dotenv -v CI=true -- pnpm run test run --coverage",
88
+ "types:check": "tsc --noEmit",
89
+ "deps:init": "playwright install chromium",
90
+ "prepublish:patch-relative-paths": "sed -E -i 's/\\]\\(\\.\\/([^\\)]+)\\)/](..\\/..\\/\\1)/g;s/\"\\.\\/([^\"]+)\"/\"..\\/..\\/\\1\"/g'"
91
+ }
92
+ }
@@ -0,0 +1,232 @@
1
+ import {
2
+ HttpSchemaPath,
3
+ HttpSchemaMethod,
4
+ HttpSearchParams,
5
+ LiteralHttpSchemaPathFromNonLiteral,
6
+ HttpSchema,
7
+ } from '@zimic/http';
8
+
9
+ import { Default } from '@/types/utils';
10
+ import { createRegexFromURL, excludeNonPathParams, joinURL } from '@/utils/urls';
11
+
12
+ import FetchResponseError from './errors/FetchResponseError';
13
+ import { FetchInput, FetchOptions, Fetch } from './types/public';
14
+ import { FetchRequestConstructor, FetchRequestInit, FetchRequest, FetchResponse } from './types/requests';
15
+
16
+ class FetchClient<Schema extends HttpSchema> {
17
+ fetch: Fetch<Schema>;
18
+
19
+ constructor({ onRequest, onResponse, ...defaults }: FetchOptions<Schema>) {
20
+ this.fetch = this.createFetchFunction();
21
+ this.fetch.defaults = defaults;
22
+ this.fetch.Request = this.createRequestClass(defaults);
23
+ this.fetch.onRequest = onRequest;
24
+ this.fetch.onResponse = onResponse;
25
+ }
26
+
27
+ private createFetchFunction() {
28
+ const fetch = async <
29
+ Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
30
+ Method extends HttpSchemaMethod<Schema>,
31
+ >(
32
+ input: FetchInput<Schema, Path, Method>,
33
+ init: FetchRequestInit<Schema, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>, Method>,
34
+ ) => {
35
+ const request = await this.createFetchRequest<Path, Method>(input, init);
36
+ const requestClone = request.clone();
37
+
38
+ const rawResponse = await globalThis.fetch(
39
+ // Optimize type checking by narrowing the type of request
40
+ requestClone as Request,
41
+ );
42
+ const response = await this.createFetchResponse<
43
+ LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>,
44
+ Method
45
+ >(request, rawResponse);
46
+
47
+ return response;
48
+ };
49
+
50
+ Object.setPrototypeOf(fetch, this);
51
+
52
+ return fetch as Fetch<Schema>;
53
+ }
54
+
55
+ private async createFetchRequest<
56
+ Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
57
+ Method extends HttpSchemaMethod<Schema>,
58
+ >(
59
+ input: FetchInput<Schema, Path, Method>,
60
+ init: FetchRequestInit<Schema, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>, Method>,
61
+ ) {
62
+ let request = input instanceof Request ? input : new this.fetch.Request(input, init);
63
+
64
+ if (this.fetch.onRequest) {
65
+ const requestAfterInterceptor = await this.fetch.onRequest(
66
+ // Optimize type checking by narrowing the type of request
67
+ request as FetchRequest.Loose,
68
+ this.fetch,
69
+ );
70
+
71
+ if (requestAfterInterceptor !== request) {
72
+ const isFetchRequest = requestAfterInterceptor instanceof this.fetch.Request;
73
+
74
+ request = isFetchRequest
75
+ ? (requestAfterInterceptor as Request as typeof request)
76
+ : new this.fetch.Request(requestAfterInterceptor as FetchInput<Schema, Path, Method>, init);
77
+ }
78
+ }
79
+
80
+ return request;
81
+ }
82
+
83
+ private async createFetchResponse<
84
+ Path extends HttpSchemaPath<Schema, Method>,
85
+ Method extends HttpSchemaMethod<Schema>,
86
+ >(fetchRequest: FetchRequest<Path, Method, Default<Schema[Path][Method]>>, rawResponse: Response) {
87
+ let response = this.defineFetchResponseProperties<Path, Method>(fetchRequest, rawResponse);
88
+
89
+ if (this.fetch.onResponse) {
90
+ const responseAfterInterceptor = await this.fetch.onResponse(
91
+ // Optimize type checking by narrowing the type of response
92
+ response as FetchResponse.Loose,
93
+ this.fetch,
94
+ );
95
+
96
+ const isFetchResponse =
97
+ responseAfterInterceptor instanceof Response &&
98
+ 'request' in responseAfterInterceptor &&
99
+ responseAfterInterceptor.request instanceof this.fetch.Request;
100
+
101
+ response = isFetchResponse
102
+ ? (responseAfterInterceptor as typeof response)
103
+ : this.defineFetchResponseProperties<Path, Method>(fetchRequest, responseAfterInterceptor);
104
+ }
105
+
106
+ return response;
107
+ }
108
+
109
+ private defineFetchResponseProperties<
110
+ Path extends HttpSchemaPath<Schema, Method>,
111
+ Method extends HttpSchemaMethod<Schema>,
112
+ >(fetchRequest: FetchRequest<Path, Method, Default<Schema[Path][Method]>>, response: Response) {
113
+ const fetchResponse = response as FetchResponse<Path, Method, Default<Schema[Path][Method]>>;
114
+
115
+ Object.defineProperty(fetchResponse, 'request', {
116
+ value: fetchRequest satisfies FetchResponse.Loose['request'],
117
+ writable: false,
118
+ enumerable: true,
119
+ configurable: false,
120
+ });
121
+
122
+ const responseError = (
123
+ fetchResponse.ok ? null : new FetchResponseError(fetchRequest, fetchResponse)
124
+ ) satisfies FetchResponse.Loose['error'];
125
+
126
+ Object.defineProperty(fetchResponse, 'error', {
127
+ value: responseError,
128
+ writable: false,
129
+ enumerable: true,
130
+ configurable: false,
131
+ });
132
+
133
+ return fetchResponse;
134
+ }
135
+
136
+ private createRequestClass(defaults: FetchRequestInit.Defaults) {
137
+ class Request<
138
+ Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
139
+ Method extends HttpSchemaMethod<Schema>,
140
+ > extends globalThis.Request {
141
+ path: LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>;
142
+
143
+ constructor(
144
+ input: FetchInput<Schema, Path, Method>,
145
+ rawInit: FetchRequestInit<Schema, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>, Method>,
146
+ ) {
147
+ const init = { ...defaults, ...rawInit };
148
+
149
+ let url: URL;
150
+
151
+ if (input instanceof globalThis.Request) {
152
+ super(
153
+ // Optimize type checking by narrowing the type of input
154
+ input as globalThis.Request,
155
+ init,
156
+ );
157
+
158
+ url = new URL(input.url);
159
+ } else {
160
+ url = input instanceof URL ? new URL(input) : new URL(joinURL(init.baseURL, input));
161
+
162
+ if (init.searchParams) {
163
+ url.search = new HttpSearchParams(init.searchParams).toString();
164
+ }
165
+
166
+ super(url, init);
167
+ }
168
+
169
+ this.path = excludeNonPathParams(url)
170
+ .toString()
171
+ .replace(init.baseURL, '') as LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>;
172
+ }
173
+
174
+ clone(): Request<Path, Method> {
175
+ const rawClone = super.clone();
176
+
177
+ return new Request<Path, Method>(
178
+ rawClone as unknown as FetchInput<Schema, Path, Method>,
179
+ rawClone as unknown as FetchRequestInit<
180
+ Schema,
181
+ LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>,
182
+ Method
183
+ >,
184
+ );
185
+ }
186
+ }
187
+
188
+ return Request as FetchRequestConstructor<Schema>;
189
+ }
190
+
191
+ isRequest<Path extends HttpSchemaPath<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(
192
+ request: unknown,
193
+ path: Path,
194
+ method: Method,
195
+ ): request is FetchRequest<Path, Method, Default<Schema[Path][Method]>> {
196
+ return (
197
+ request instanceof Request &&
198
+ request.method === method &&
199
+ 'path' in request &&
200
+ typeof request.path === 'string' &&
201
+ createRegexFromURL(path).test(request.path)
202
+ );
203
+ }
204
+
205
+ isResponse<Path extends HttpSchemaPath<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(
206
+ response: unknown,
207
+ path: Path,
208
+ method: Method,
209
+ ): response is FetchResponse<Path, Method, Default<Schema[Path][Method]>> {
210
+ return (
211
+ response instanceof Response &&
212
+ 'request' in response &&
213
+ 'error' in response &&
214
+ this.isRequest(response.request, path, method)
215
+ );
216
+ }
217
+
218
+ isResponseError<Path extends HttpSchemaPath<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(
219
+ error: unknown,
220
+ path: Path,
221
+ method: Method,
222
+ ): error is FetchResponseError<Path, Method, Default<Schema[Path][Method]>> {
223
+ return (
224
+ error instanceof FetchResponseError &&
225
+ error.request.method === method &&
226
+ typeof error.request.path === 'string' &&
227
+ createRegexFromURL(path).test(error.request.path)
228
+ );
229
+ }
230
+ }
231
+
232
+ export default FetchClient;
@@ -0,0 +1,26 @@
1
+ import { HttpMethod, HttpMethodSchema } from '@zimic/http';
2
+
3
+ import { FetchRequest, FetchResponse } from '../types/requests';
4
+
5
+ class FetchResponseError<
6
+ Path extends string = string,
7
+ Method extends HttpMethod = HttpMethod,
8
+ MethodSchema extends HttpMethodSchema = HttpMethodSchema,
9
+ > extends Error {
10
+ constructor(
11
+ public request: FetchRequest<Path, Method, MethodSchema>,
12
+ public response: FetchResponse<Path, Method, MethodSchema, true>,
13
+ ) {
14
+ super(`${request.method} ${request.url} failed with status ${response.status}: ${response.statusText}`);
15
+ this.name = 'FetchResponseError';
16
+ }
17
+
18
+ get cause() {
19
+ return this.response;
20
+ }
21
+ }
22
+
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ export type AnyFetchRequestError = FetchResponseError<any, any, any>;
25
+
26
+ export default FetchResponseError;
@@ -0,0 +1,13 @@
1
+ import { HttpSchema } from '@zimic/http';
2
+
3
+ import FetchClient from './FetchClient';
4
+ import { FetchOptions, Fetch as PublicFetch } from './types/public';
5
+
6
+ function createFetch<Schema extends HttpSchema>(
7
+ options: FetchOptions<HttpSchema<Schema>>,
8
+ ): PublicFetch<HttpSchema<Schema>> {
9
+ const { fetch } = new FetchClient<HttpSchema<Schema>>(options);
10
+ return fetch;
11
+ }
12
+
13
+ export default createFetch;
@@ -0,0 +1,15 @@
1
+ const value = Symbol.for('JSONStringified.value');
2
+
3
+ export type JSONStringified<Value> = string & { [value]: Value };
4
+
5
+ declare global {
6
+ interface JSON {
7
+ // eslint-disable-next-line @typescript-eslint/method-signature-style
8
+ stringify<Value>(
9
+ value: Value,
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ replacer?: ((this: any, key: string, value: Value) => any) | (number | string)[] | null,
12
+ space?: string | number,
13
+ ): JSONStringified<Value>;
14
+ }
15
+ }