UNPKG

@zimic/fetch

Version:

Next-gen TypeScript-first Fetch client

349 lines (340 loc) 13.6 kB
'use strict'; 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