@zimic/fetch
Version:
Next-gen TypeScript-first Fetch client
349 lines (340 loc) • 13.6 kB
JavaScript
;
var http = require('@zimic/http');
// src/client/errors/FetchResponseError.ts
var FetchResponseError = class extends Error {
constructor(request, response) {
super(`${request.method} ${request.url} failed with status ${response.status}: ${response.statusText}`);
this.request = request;
this.response = response;
this.name = "FetchResponseError";
}
toObject({
includeRequestBody = false,
includeResponseBody = false
} = {}) {
const partialObject = {
name: this.name,
message: this.message
};
if (!includeRequestBody && !includeResponseBody) {
return {
...partialObject,
request: this.requestToObject({ includeBody: false }),
response: this.responseToObject({ includeBody: false })
};
}
return Promise.all([
Promise.resolve(this.requestToObject({ includeBody: includeRequestBody })),
Promise.resolve(this.responseToObject({ includeBody: includeResponseBody }))
]).then(([request, response]) => ({ ...partialObject, request, response }));
}
requestToObject(options) {
const request = this.request;
const requestObject = {
url: request.url,
path: request.path,
method: request.method,
headers: this.convertHeadersToObject(request),
cache: request.cache,
destination: request.destination,
credentials: request.credentials,
integrity: request.integrity,
keepalive: request.keepalive,
mode: request.mode,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy
};
if (!options.includeBody) {
return requestObject;
}
return this.withIncludedBodyIfAvailable("request", requestObject);
}
responseToObject(options) {
const response = this.response;
const responseObject = {
url: response.url,
type: response.type,
status: response.status,
statusText: response.statusText,
ok: response.ok,
headers: this.convertHeadersToObject(response),
redirected: response.redirected
};
if (!options.includeBody) {
return responseObject;
}
return this.withIncludedBodyIfAvailable("response", responseObject);
}
convertHeadersToObject(resource) {
return http.HttpHeaders.prototype.toObject.call(resource.headers);
}
withIncludedBodyIfAvailable(resourceType, resourceObject) {
const resource = this[resourceType];
if (resource.bodyUsed) {
console.warn(
"[@zimic/fetch]",
`Could not include the ${resourceType} body because it is already used. If you access the body before calling \`error.toObject()\`, consider reading it from a cloned ${resourceType}.
Learn more: https://zimic.dev/docs/fetch/api/fetch-response-error#errortoobject`
);
return resourceObject;
}
return http.parseHttpBody(resource).then((body) => {
resourceObject.body = body;
return resourceObject;
}).catch((error) => {
console.error("[@zimic/fetch]", `Failed to parse ${resourceType} body:`, error);
return resourceObject;
});
}
};
var FetchResponseError_default = FetchResponseError;
// ../zimic-utils/dist/chunk-VPHA4ZCK.mjs
function createPathCharactersToEscapeRegex() {
return /([.(){}+$])/g;
}
function preparePathForRegex(path) {
const pathURLPrefix = `data:${path.startsWith("/") ? "" : "/"}`;
const pathAsURL = new URL(`${pathURLPrefix}${path}`);
const encodedPath = pathAsURL.href.replace(pathURLPrefix, "");
return encodedPath.replace(/^\/+/g, "").replace(/\/+$/g, "").replace(createPathCharactersToEscapeRegex(), "\\$1");
}
function createPathParamRegex() {
return /(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)(?!\\[*+?])/gu;
}
function createRepeatingPathParamRegex() {
return /(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)\\\+/gu;
}
function createOptionalPathParamRegex() {
return /(?<leadingSlash>\/)?(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)\?(?<trailingSlash>\/)?/gu;
}
function createOptionalRepeatingPathParamRegex() {
return /(?<leadingSlash>\/)?(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)\*(?<trailingSlash>\/)?/gu;
}
function createRegexFromPath(path) {
const pathRegexContent = preparePathForRegex(path).replace(
createOptionalRepeatingPathParamRegex(),
(_match, leadingSlash, escape, identifier, trailingSlash) => {
if (escape) {
return `${leadingSlash ?? ""}:${identifier}\\*${trailingSlash ?? ""}`;
}
const hasSegmentBeforePrefix = leadingSlash === "/";
const prefixExpression = hasSegmentBeforePrefix ? "/?" : leadingSlash;
const hasSegmentAfterSuffix = trailingSlash === "/";
const suffixExpression = hasSegmentAfterSuffix ? "/?" : trailingSlash;
if (prefixExpression && suffixExpression) {
return `(?:${prefixExpression}(?<${identifier}>.+?)?${suffixExpression})?`;
} else if (prefixExpression) {
return `(?:${prefixExpression}(?<${identifier}>.+?))?`;
} else if (suffixExpression) {
return `(?:(?<${identifier}>.+?)${suffixExpression})?`;
} else {
return `(?<${identifier}>.+?)?`;
}
}
).replace(createRepeatingPathParamRegex(), (_match, escape, identifier) => {
return escape ? `:${identifier}\\+` : `(?<${identifier}>.+)`;
}).replace(
createOptionalPathParamRegex(),
(_match, leadingSlash, escape, identifier, trailingSlash) => {
if (escape) {
return `${leadingSlash ?? ""}:${identifier}\\?${trailingSlash ?? ""}`;
}
const hasSegmentBeforePrefix = leadingSlash === "/";
const prefixExpression = hasSegmentBeforePrefix ? "/?" : leadingSlash;
const hasSegmentAfterSuffix = trailingSlash === "/";
const suffixExpression = hasSegmentAfterSuffix ? "/?" : trailingSlash;
if (prefixExpression && suffixExpression) {
return `(?:${prefixExpression}(?<${identifier}>[^\\/]+?)?${suffixExpression})`;
} else if (prefixExpression) {
return `(?:${prefixExpression}(?<${identifier}>[^\\/]+?))?`;
} else if (suffixExpression) {
return `(?:(?<${identifier}>[^\\/]+?)${suffixExpression})?`;
} else {
return `(?<${identifier}>[^\\/]+?)?`;
}
}
).replace(createPathParamRegex(), (_match, escape, identifier) => {
return escape ? `:${identifier}` : `(?<${identifier}>[^\\/]+?)`;
});
return new RegExp(`^/?${pathRegexContent}/?$`);
}
var createRegexFromPath_default = createRegexFromPath;
// ../zimic-utils/dist/url/excludeNonPathParams.mjs
function excludeNonPathParams(url) {
url.hash = "";
url.search = "";
url.username = "";
url.password = "";
return url;
}
var excludeNonPathParams_default = excludeNonPathParams;
// ../zimic-utils/dist/url/joinURL.mjs
function joinURL(...parts) {
return parts.map((part, index) => {
const isFirstPart = index === 0;
const isLastPart = index === parts.length - 1;
let partAsString = part.toString();
if (!isFirstPart) {
partAsString = partAsString.replace(/^\//, "");
}
if (!isLastPart) {
partAsString = partAsString.replace(/\/$/, "");
}
return partAsString;
}).filter((part) => part.length > 0).join("/");
}
var joinURL_default = joinURL;
// src/client/FetchClient.ts
var FetchClient = class {
fetch;
constructor({ headers = {}, searchParams = {}, ...otherOptions }) {
this.fetch = this.createFetchFunction();
this.fetch.headers = headers;
this.fetch.searchParams = searchParams;
Object.assign(this.fetch, otherOptions);
this.fetch.loose = this.fetch;
this.fetch.Request = this.createRequestClass(this.fetch);
}
get defaults() {
return this.fetch;
}
createFetchFunction() {
const fetch = async (input, init) => {
const request = await this.createFetchRequest(input, init);
const requestClone = request.clone();
const rawResponse = await globalThis.fetch(
// Optimize type checking by narrowing the type of request
requestClone
);
const response = await this.createFetchResponse(request, rawResponse);
return response;
};
Object.setPrototypeOf(fetch, this);
return fetch;
}
async createFetchRequest(input, init) {
let request = input instanceof Request ? input : new this.fetch.Request(input, init);
if (this.fetch.onRequest) {
const requestAfterInterceptor = await this.fetch.onRequest(
// Optimize type checking by narrowing the type of request
request
);
if (requestAfterInterceptor !== request) {
const isFetchRequest = requestAfterInterceptor instanceof this.fetch.Request;
request = isFetchRequest ? requestAfterInterceptor : new this.fetch.Request(requestAfterInterceptor, init);
}
}
return request;
}
async createFetchResponse(fetchRequest, rawResponse) {
let response = this.defineFetchResponseProperties(fetchRequest, rawResponse);
if (this.fetch.onResponse) {
const responseAfterInterceptor = await this.fetch.onResponse(
// Optimize type checking by narrowing the type of response
response
);
const isFetchResponse = responseAfterInterceptor instanceof Response && "request" in responseAfterInterceptor && responseAfterInterceptor.request instanceof this.fetch.Request;
response = isFetchResponse ? responseAfterInterceptor : this.defineFetchResponseProperties(fetchRequest, responseAfterInterceptor);
}
return response;
}
defineFetchResponseProperties(fetchRequest, response) {
const fetchResponse = response;
Object.defineProperty(fetchResponse, "request", {
value: fetchRequest,
writable: false,
enumerable: true,
configurable: false
});
let responseError;
Object.defineProperty(fetchResponse, "error", {
get() {
if (responseError === void 0) {
responseError = fetchResponse.ok ? null : new FetchResponseError_default(
fetchRequest,
fetchResponse
);
}
return responseError;
},
enumerable: true,
configurable: false
});
return fetchResponse;
}
createRequestClass(fetch) {
class Request2 extends globalThis.Request {
path;
constructor(input, init) {
let actualInput;
const actualInit = {
baseURL: init?.baseURL ?? fetch.baseURL,
method: init?.method ?? fetch.method,
headers: new http.HttpHeaders(fetch.headers),
searchParams: new http.HttpSearchParams(fetch.searchParams),
body: init?.body ?? fetch.body,
mode: init?.mode ?? fetch.mode,
cache: init?.cache ?? fetch.cache,
credentials: init?.credentials ?? fetch.credentials,
integrity: init?.integrity ?? fetch.integrity,
keepalive: init?.keepalive ?? fetch.keepalive,
priority: init?.priority ?? fetch.priority,
redirect: init?.redirect ?? fetch.redirect,
referrer: init?.referrer ?? fetch.referrer,
referrerPolicy: init?.referrerPolicy ?? fetch.referrerPolicy,
signal: init?.signal ?? fetch.signal,
window: init?.window === void 0 ? fetch.window : init.window,
duplex: init?.duplex ?? fetch.duplex
};
if (init?.headers) {
actualInit.headers.assign(new http.HttpHeaders(init.headers));
}
let url;
const baseURL = new URL(actualInit.baseURL);
if (input instanceof globalThis.Request) {
const request = input;
actualInit.headers.assign(new http.HttpHeaders(request.headers));
url = new URL(input.url);
actualInput = request;
} else {
url = new URL(input instanceof URL ? input : joinURL_default(baseURL, input));
actualInit.searchParams.assign(new http.HttpSearchParams(url.searchParams));
if (init?.searchParams) {
actualInit.searchParams.assign(new http.HttpSearchParams(init.searchParams));
}
url.search = actualInit.searchParams.toString();
actualInput = url;
}
super(actualInput, actualInit);
const baseURLWithoutTrailingSlash = baseURL.toString().replace(/\/$/, "");
this.path = excludeNonPathParams_default(url).toString().replace(baseURLWithoutTrailingSlash, "");
}
clone() {
const rawClone = super.clone();
return new Request2(rawClone);
}
}
return Request2;
}
isRequest(request, method, path) {
return request instanceof Request && request.method === method && "path" in request && typeof request.path === "string" && createRegexFromPath_default(path).test(request.path);
}
isResponse(response, method, path) {
return response instanceof Response && "request" in response && this.isRequest(response.request, method, path) && "error" in response && (response.error === null || response.error instanceof FetchResponseError_default);
}
isResponseError(error, method, path) {
return error instanceof FetchResponseError_default && this.isRequest(error.request, method, path) && this.isResponse(error.response, method, path);
}
};
var FetchClient_default = FetchClient;
// src/client/factory.ts
function createFetch(options) {
const { fetch } = new FetchClient_default(options);
return fetch;
}
var factory_default = createFetch;
exports.FetchResponseError = FetchResponseError_default;
exports.createFetch = factory_default;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map