Something went wrong. Try again.
Mirror: A Node.js fetch shim using built-in Request, Response, and Headers (but without native fetch)
Something went wrong. Try again.
7.9 kB · 257 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258import { Readable } from 'node:stream';import { arrayBuffer, blob, text } from 'node:stream/consumers';import { isAnyArrayBuffer } from 'node:util/types';import { randomBytes } from 'node:crypto';import { Response, Blob, FormData, URLSearchParams } from './webstd';
export type BodyInit = | Exclude<RequestInit['body'], undefined | null> | FormDataPolyfill | Readable;
export interface BodyState { contentLength: number | null; contentType: string | null; body: Readable | ReadableStream | Uint8Array | null;}
interface FormDataPolyfill extends Readable { getBoundary(): string; getLengthSync(): number; hasKnownLength(): number;}
const CRLF = '\r\n';const CRLF_LENGTH = 2;const BOUNDARY = '-'.repeat(2);
const isReadable = (object: any): object is Readable => Readable.isReadable(object);
const isIterable = ( object: any): object is AsyncIterable<any> | Iterable<any> => typeof object[Symbol.asyncIterator] === 'function' || typeof object[Symbol.iterator] === 'function';
const isMultipartFormDataStream = (object: any): object is FormDataPolyfill => typeof object.getBoundary === 'function' && typeof object.hasKnownLength === 'function' && typeof object.getLengthSync === 'function' && Readable.isReadable(object);
const isFormData = (object: any): object is FormData => typeof object === 'object' && typeof object.append === 'function' && typeof object.set === 'function' && typeof object.get === 'function' && typeof object.getAll === 'function' && typeof object.delete === 'function' && typeof object.keys === 'function' && typeof object.values === 'function' && typeof object.entries === 'function' && typeof object.constructor === 'function' && object[Symbol.toStringTag] === 'FormData';
const isURLSearchParameters = (object: any): object is URLSearchParams => typeof object === 'object' && typeof object.append === 'function' && typeof object.delete === 'function' && typeof object.get === 'function' && typeof object.getAll === 'function' && typeof object.has === 'function' && typeof object.set === 'function' && typeof object.sort === 'function' && object[Symbol.toStringTag] === 'URLSearchParams';
const isReadableStream = (object: any): object is ReadableStream => typeof object === 'object' && typeof object.getReader === 'function' && typeof object.cancel === 'function' && typeof object.tee === 'function';
const isBlob = (object: any): object is Blob => { if ( typeof object === 'object' && typeof object.arrayBuffer === 'function' && typeof object.type === 'string' && typeof object.stream === 'function' && typeof object.constructor === 'function' ) { const tag = object[Symbol.toStringTag] as string | undefined; return !!tag && (tag.startsWith('Blob') || tag.startsWith('File')); } else { return false; }};
const makeFormBoundary = (): string => `formdata-${randomBytes(8).toString('hex')}`;
const getFormHeader = ( boundary: string, name: string, field: File | Blob | string): string => { let header = `${BOUNDARY}${boundary}${CRLF}`; header += `Content-Disposition: form-data; name="${name}"`; if (isBlob(field)) { header += `; filename="${(field as File).name ?? 'blob'}"${CRLF}`; header += `Content-Type: ${field.type || 'application/octet-stream'}`; } return `${header}${CRLF}${CRLF}`;};
const getFormFooter = (boundary: string) => `${BOUNDARY}${boundary}${BOUNDARY}${CRLF}${CRLF}`;
export const getFormDataLength = (form: FormData, boundary: string) => { let length = Buffer.byteLength(getFormFooter(boundary)); for (const [name, value] of form) length += Buffer.byteLength(getFormHeader(boundary, name, value)) + (isBlob(value) ? value.size : Buffer.byteLength(`${value}`)) + CRLF_LENGTH; return length;};
async function* generatorOfFormData( form: FormData, boundary: string): AsyncGenerator<ArrayBufferLike> { const encoder = new TextEncoder(); for (const [name, value] of form) { if (isBlob(value)) { yield encoder.encode(getFormHeader(boundary, name, value)); yield* value.stream(); yield encoder.encode(CRLF); } else { yield encoder.encode(getFormHeader(boundary, name, value) + value + CRLF); } } yield encoder.encode(getFormFooter(boundary));}
const encoder = new TextEncoder();
export const extractBody = (object: BodyInit | null): BodyState => { let type: string | null = null; let body: Readable | ReadableStream | Uint8Array | null; let size: number | null = null; if (object == null) { body = null; size = 0; } else if (typeof object === 'string') { const bytes = encoder.encode(`${object}`); type = 'text/plain;charset=UTF-8'; size = bytes.byteLength; body = bytes; } else if (isURLSearchParameters(object)) { const bytes = encoder.encode(object.toString()); body = bytes; size = bytes.byteLength; type = 'application/x-www-form-urlencoded;charset=UTF-8'; } else if (isBlob(object)) { size = object.size; type = object.type || null; body = object.stream(); } else if (object instanceof Uint8Array) { body = object; size = object.byteLength; } else if (isAnyArrayBuffer(object)) { const bytes = new Uint8Array(object); body = bytes; size = bytes.byteLength; } else if (ArrayBuffer.isView(object)) { const bytes = new Uint8Array( object.buffer, object.byteOffset, object.byteLength ); body = bytes; size = bytes.byteLength; } else if (isReadableStream(object)) { body = object; } else if (isFormData(object)) { const boundary = makeFormBoundary(); type = `multipart/form-data; boundary=${boundary}`; size = getFormDataLength(object, boundary); body = Readable.from(generatorOfFormData(object, boundary)); } else if (isMultipartFormDataStream(object)) { type = `multipart/form-data; boundary=${object.getBoundary()}`; size = object.hasKnownLength() ? object.getLengthSync() : null; body = object as Readable; } else if (isReadable(object)) { body = object as Readable; } else if (isIterable(object)) { body = Readable.from(object); } else { const bytes = encoder.encode(`${object}`); type = 'text/plain;charset=UTF-8'; body = bytes; size = bytes.byteLength; } return { contentLength: size, contentType: type, body, };};
const kBodyInternals = Symbol('kBodyInternals');
export class Body { private [kBodyInternals]: BodyState;
constructor(init: BodyInit | null) { this[kBodyInternals] = extractBody(init); }
get body() { return this[kBodyInternals].body; }
get bodyUsed() { const { body } = this[kBodyInternals]; if (isReadable(body)) { return Readable.isDisturbed(body); } else if (isReadableStream(body)) { return body.locked; } else { return false; } }
async arrayBuffer() { const { body } = this[kBodyInternals]; return body != null && !isAnyArrayBuffer(body) ? arrayBuffer(body) : body; }
async formData() { const { body, contentLength, contentType } = this[kBodyInternals]; const headers = {}; if (contentLength) headers['Content-Length'] = contentLength; if (contentType) headers['Content-Type'] = contentType; return new Response(body, { headers }).formData(); }
async blob() { const { body, contentType } = this[kBodyInternals]; const chunks = body !== null ? [!isAnyArrayBuffer(body) ? await blob(body) : body] : []; return new Blob(chunks, { type: contentType ?? undefined, }); }
async json() { return JSON.parse(await this.text()); }
async text() { const { body } = this[kBodyInternals]; return body == null || isAnyArrayBuffer(body) ? new TextDecoder().decode(await this.arrayBuffer()) : text(body); }}