@zimic/fetch
Version:
Next-gen TypeScript-first Fetch client
453 lines (444 loc) • 16.9 kB
JavaScript
import { HttpSearchParams, HttpHeaders, parseHttpBody } from '@zimic/http';
// src/client/request/FetchRequest.ts
// ../zimic-utils/dist/url.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;
function excludeNonPathParams(url) {
url.hash = "";
url.search = "";
url.username = "";
url.password = "";
return url;
}
var excludeNonPathParams_default = excludeNonPathParams;
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;
var BODY_METHOD = ["json", "formData", "text", "arrayBuffer", "blob", "bytes"];
function isBodyMethod(property, value) {
return BODY_METHOD.includes(property) && typeof value === "function";
}
function getOrSetBoundBodyMethod(resource, property, value) {
const isValueAlreadyBound = Object.prototype.hasOwnProperty.call(resource, property);
if (isValueAlreadyBound) {
return value;
}
const boundValue = value.bind(resource);
Object.defineProperty(resource, property, {
value: boundValue,
configurable: true,
enumerable: false,
writable: true
});
return boundValue;
}
function withIncludedBodyIfAvailable(resource, resourceObject) {
const resourceType = resource instanceof Request ? "request" : "response";
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 \`toObject()\`, consider reading it from a cloned ${resourceType}.
Learn more: https://zimic.dev/docs/fetch/api/fetch-response-error#errortoobject`
);
return resourceObject;
}
return parseHttpBody(resource.clone()).then((body) => {
resourceObject.body = body;
return resourceObject;
}).catch((error) => {
console.error("[@zimic/fetch]", `Failed to parse ${resourceType} body:`, error);
return resourceObject;
});
}
// src/client/request/FetchRequest.ts
var FETCH_REQUEST_BRAND = /* @__PURE__ */ Symbol.for("FetchRequest");
var FETCH_REQUEST_EXTRA_PROPERTIES = [FETCH_REQUEST_BRAND, "raw", "path", "toObject"];
function createFetchRequestClass() {
const FetchRequestClass = function FetchRequest2(fetch, input, init) {
let actualInput;
const actualInit = {
baseURL: init?.baseURL ?? fetch.baseURL,
method: init?.method ?? fetch.method,
headers: new HttpHeaders(fetch.headers),
searchParams: new 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 !== void 0) {
actualInit.headers.assign(new HttpHeaders(init.headers));
}
let url;
const baseURL = new URL(actualInit.baseURL);
if (input instanceof Request) {
const request2 = input;
actualInit.headers.assign(new HttpHeaders(request2.headers));
url = new URL(input.url);
actualInput = request2 instanceof FetchRequestClass ? request2.raw : request2;
} else {
url = new URL(input instanceof URL ? input : joinURL_default(baseURL, input));
actualInit.searchParams.assign(
new HttpSearchParams(url.searchParams)
);
if (init?.searchParams !== void 0) {
actualInit.searchParams.assign(new HttpSearchParams(init.searchParams));
}
url.search = actualInit.searchParams.toString();
actualInput = url;
}
const request = new Request(actualInput, actualInit);
const baseURLWithoutTrailingSlash = baseURL.toString().replace(/\/$/, "");
const path = excludeNonPathParams_default(url).toString().replace(baseURLWithoutTrailingSlash, "");
function clone() {
return new FetchRequestClass(fetch, request.clone());
}
function toObject(options) {
const requestObject = {
url: request.url,
path,
method: request.method,
headers: HttpHeaders.prototype.toObject.call(request.headers),
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 withIncludedBodyIfAvailable(request, requestObject);
}
const fetchRequest = new Proxy(request, {
get(target, property) {
if (property === FETCH_REQUEST_BRAND) {
return true;
}
if (property === "raw") {
return request;
}
if (property === "path") {
return path;
}
if (property === "clone") {
return clone;
}
if (property === "toObject") {
return toObject;
}
const value = Reflect.get(target, property, target);
if (isBodyMethod(property, value)) {
return getOrSetBoundBodyMethod(request, property, value);
}
return value;
},
has(target, property) {
return FETCH_REQUEST_EXTRA_PROPERTIES.includes(property) || Reflect.has(target, property);
}
});
return fetchRequest;
};
Object.defineProperty(FetchRequestClass, Symbol.hasInstance, {
value(instance) {
return instance instanceof Request && FETCH_REQUEST_BRAND in instance && instance[FETCH_REQUEST_BRAND] === true;
},
writable: false,
enumerable: false,
configurable: false
});
Object.setPrototypeOf(FetchRequestClass.prototype, Request.prototype);
return FetchRequestClass;
}
var FetchRequest = createFetchRequestClass();
// src/client/response/error/FetchResponseError.ts
var FetchResponseError = class extends Error {
constructor(request, response) {
super(`${request.method} ${request.url} failed with status ${response.status}.`);
this.request = request;
this.response = response;
this.name = "FetchResponseError";
}
request;
response;
toObject(options) {
const partialObject = {
name: this.name,
message: this.message
};
if (!options?.includeRequestBody && !options?.includeResponseBody) {
return {
...partialObject,
request: this.request.toObject({ includeBody: false }),
response: this.response.toObject({ includeBody: false })
};
}
return Promise.all([
Promise.resolve(this.request.toObject({ includeBody: options.includeRequestBody })),
Promise.resolve(this.response.toObject({ includeBody: options.includeResponseBody }))
]).then(([request, response]) => ({ ...partialObject, request, response }));
}
};
var FetchResponseError_default = FetchResponseError;
// src/client/response/FetchResponse.ts
var FETCH_RESPONSE_BRAND = /* @__PURE__ */ Symbol.for("FetchResponse");
var FETCH_RESPONSE_EXTRA_PROPERTIES = [FETCH_RESPONSE_BRAND, "raw", "request", "error", "toObject"];
function createFetchResponseClass() {
const FetchResponseClass = function FetchResponse2(fetchRequest, responseOrBody, init) {
const response = responseOrBody instanceof Response ? responseOrBody : new Response(responseOrBody, init);
let error = null;
function clone() {
return new FetchResponseClass(fetchRequest, response.clone());
}
function toObject(options) {
const responseObject = {
url: response.url,
type: response.type,
status: response.status,
statusText: response.statusText,
ok: response.ok,
headers: HttpHeaders.prototype.toObject.call(response.headers),
redirected: response.redirected
};
if (!options?.includeBody) {
return responseObject;
}
return withIncludedBodyIfAvailable(response, responseObject);
}
const fetchResponse = new Proxy(response, {
get(target, property, receiver) {
if (property === FETCH_RESPONSE_BRAND) {
return true;
}
if (property === "raw") {
return response;
}
if (property === "request") {
return fetchRequest;
}
if (property === "error") {
error ??= new FetchResponseError_default(fetchRequest, receiver);
return error;
}
if (property === "clone") {
return clone;
}
if (property === "toObject") {
return toObject;
}
const value = Reflect.get(target, property, target);
if (isBodyMethod(property, value)) {
return getOrSetBoundBodyMethod(response, property, value);
}
return value;
},
has(target, property) {
return FETCH_RESPONSE_EXTRA_PROPERTIES.includes(property) || Reflect.has(target, property);
}
});
return fetchResponse;
};
Object.defineProperty(FetchResponseClass, Symbol.hasInstance, {
value(instance) {
return instance instanceof Response && FETCH_RESPONSE_BRAND in instance && instance[FETCH_RESPONSE_BRAND] === true;
},
writable: false,
enumerable: false,
configurable: false
});
Object.setPrototypeOf(FetchResponseClass.prototype, Response.prototype);
return FetchResponseClass;
}
var FetchResponse = createFetchResponseClass();
Object.setPrototypeOf(FetchResponse.prototype, Response.prototype);
// src/client/FetchClient.ts
var FETCH_OPTIONS_DEFAULT_PROPERTIES = [
"baseURL",
"onRequest",
"onResponse",
"body",
"cache",
"credentials",
"integrity",
"keepalive",
"mode",
"priority",
"redirect",
"referrer",
"referrerPolicy",
"signal",
"window",
"duplex"
];
var FetchClient = class {
fetch;
constructor(options) {
this.fetch = this.createFetchFunction();
this.assignDefaults(this.fetch, options);
this.fetch.loose = this.fetch;
this.fetch.Request = this.createFetchRequestConstructor(this.fetch);
}
assignDefaults(fetch, { headers = {}, searchParams = {}, ...otherOptions }) {
fetch.headers = headers;
fetch.searchParams = searchParams;
for (const property of FETCH_OPTIONS_DEFAULT_PROPERTIES) {
const propertyValue = otherOptions[property];
if (propertyValue !== void 0) {
fetch[property] = propertyValue;
}
}
}
get defaults() {
return this.fetch;
}
createFetchFunction() {
const fetch = async (input, init) => {
const fetchRequest = await this.createFetchRequest(input, init);
const response = await globalThis.fetch(fetchRequest.raw.clone());
const fetchResponse = await this.createFetchResponse(fetchRequest, response);
return fetchResponse;
};
return Object.setPrototypeOf(fetch, this);
}
createFetchRequestConstructor(fetch) {
function Request2(input, init) {
return new FetchRequest(fetch, input, init);
}
Object.setPrototypeOf(Request2.prototype, FetchRequest.prototype);
return Request2;
}
async createFetchRequest(input, init) {
let fetchRequest = new this.fetch.Request(input, init);
if (this.fetch.onRequest) {
const newRequest = await this.fetch.onRequest(fetchRequest);
if (newRequest !== fetchRequest) {
if (newRequest instanceof FetchRequest) {
fetchRequest = newRequest;
} else {
fetchRequest = new this.fetch.Request(newRequest, init);
}
}
}
return fetchRequest;
}
async createFetchResponse(fetchRequest, response) {
let fetchResponse = new FetchResponse(fetchRequest, response);
if (this.fetch.onResponse) {
const newResponse = await this.fetch.onResponse(fetchResponse);
fetchResponse = newResponse instanceof FetchResponse ? newResponse : new FetchResponse(fetchRequest, newResponse);
}
return fetchResponse;
}
isRequest(request, method, path) {
return request instanceof FetchRequest && request.method === method && typeof request.path === "string" && createRegexFromPath_default(path).test(request.path);
}
isResponse(response, method, path) {
return response instanceof FetchResponse && this.isRequest(response.request, method, path);
}
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;
export { FetchRequest, FetchResponse, FetchResponseError_default as FetchResponseError, factory_default as createFetch };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map