UNPKG

@zimic/fetch

Version:

Next-gen TypeScript-first Fetch client

458 lines (448 loc) 16.9 kB
'use strict'; var http = require('@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 http.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 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 !== void 0) { actualInit.headers.assign(new http.HttpHeaders(init.headers)); } let url; const baseURL = new URL(actualInit.baseURL); if (input instanceof Request) { const request2 = input; actualInit.headers.assign(new http.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 http.HttpSearchParams(url.searchParams) ); if (init?.searchParams !== void 0) { actualInit.searchParams.assign(new http.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: http.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: http.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; exports.FetchRequest = FetchRequest; exports.FetchResponse = FetchResponse; exports.FetchResponseError = FetchResponseError_default; exports.createFetch = factory_default; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map