Mirror: A Node.js fetch shim using built-in Request, Response, and Headers (but without native fetch)
at v0.2.0 6.3 kB view raw
1import { Readable } from 'node:stream'; 2import { isAnyArrayBuffer } from 'node:util/types'; 3import { randomBytes } from 'node:crypto'; 4import { Blob, FormData, URLSearchParams } from './webstd'; 5 6export type BodyInit = 7 | Exclude<RequestInit['body'], undefined | null> 8 | FormDataPolyfill 9 | Readable; 10 11export interface BodyState { 12 contentLength: number | null; 13 contentType: string | null; 14 body: Readable | ReadableStream | Uint8Array | null; 15} 16 17interface FormDataPolyfill extends Readable { 18 getBoundary(): string; 19 getLengthSync(): number; 20 hasKnownLength(): number; 21} 22 23const CRLF = '\r\n'; 24const CRLF_LENGTH = 2; 25const BOUNDARY = '-'.repeat(2); 26 27const isReadable = (object: any): object is Readable => 28 Readable.isReadable(object); 29 30const isIterable = ( 31 object: any 32): object is AsyncIterable<any> | Iterable<any> => 33 typeof object[Symbol.asyncIterator] === 'function' || 34 typeof object[Symbol.iterator] === 'function'; 35 36const isMultipartFormDataStream = (object: any): object is FormDataPolyfill => 37 typeof object.getBoundary === 'function' && 38 typeof object.hasKnownLength === 'function' && 39 typeof object.getLengthSync === 'function' && 40 Readable.isReadable(object); 41 42const isFormData = (object: any): object is FormData => 43 typeof object === 'object' && 44 typeof object.append === 'function' && 45 typeof object.set === 'function' && 46 typeof object.get === 'function' && 47 typeof object.getAll === 'function' && 48 typeof object.delete === 'function' && 49 typeof object.keys === 'function' && 50 typeof object.values === 'function' && 51 typeof object.entries === 'function' && 52 typeof object.constructor === 'function' && 53 object[Symbol.toStringTag] === 'FormData'; 54 55const isURLSearchParameters = (object: any): object is URLSearchParams => 56 typeof object === 'object' && 57 typeof object.append === 'function' && 58 typeof object.delete === 'function' && 59 typeof object.get === 'function' && 60 typeof object.getAll === 'function' && 61 typeof object.has === 'function' && 62 typeof object.set === 'function' && 63 typeof object.sort === 'function' && 64 object[Symbol.toStringTag] === 'URLSearchParams'; 65 66const isReadableStream = (object: any): object is ReadableStream => 67 typeof object === 'object' && 68 typeof object.getReader === 'function' && 69 typeof object.cancel === 'function' && 70 typeof object.tee === 'function'; 71 72const isBlob = (object: any): object is Blob => { 73 if ( 74 typeof object === 'object' && 75 typeof object.arrayBuffer === 'function' && 76 typeof object.type === 'string' && 77 typeof object.stream === 'function' && 78 typeof object.constructor === 'function' 79 ) { 80 const tag = object[Symbol.toStringTag]; 81 return tag.startsWith('Blob') || tag.startsWith('File'); 82 } else { 83 return false; 84 } 85}; 86 87const makeFormBoundary = (): string => 88 `formdata-${randomBytes(8).toString('hex')}`; 89 90const getFormHeader = ( 91 boundary: string, 92 name: string, 93 field: File | Blob | string 94): string => { 95 let header = `${BOUNDARY}${boundary}${CRLF}`; 96 header += `Content-Disposition: form-data; name="${name}"`; 97 if (isBlob(field)) { 98 header += `; filename="${(field as File).name ?? 'blob'}"${CRLF}`; 99 header += `Content-Type: ${field.type || 'application/octet-stream'}`; 100 } 101 return `${header}${CRLF}${CRLF}`; 102}; 103 104const getFormFooter = (boundary: string) => 105 `${BOUNDARY}${boundary}${BOUNDARY}${CRLF}${CRLF}`; 106 107export const getFormDataLength = (form: FormData, boundary: string) => { 108 let length = Buffer.byteLength(getFormFooter(boundary)); 109 for (const [name, value] of form) 110 length += 111 Buffer.byteLength(getFormHeader(boundary, name, value)) + 112 (isBlob(value) ? value.size : Buffer.byteLength(`${value}`)) + 113 CRLF_LENGTH; 114 return length; 115}; 116 117async function* generatorOfFormData( 118 form: FormData, 119 boundary: string 120): AsyncGenerator<ArrayBufferLike> { 121 const encoder = new TextEncoder(); 122 for (const [name, value] of form) { 123 if (isBlob(value)) { 124 yield encoder.encode(getFormHeader(boundary, name, value)); 125 yield* value.stream(); 126 yield encoder.encode(CRLF); 127 } else { 128 yield encoder.encode(getFormHeader(boundary, name, value) + value + CRLF); 129 } 130 } 131 yield encoder.encode(getFormFooter(boundary)); 132} 133 134const encoder = new TextEncoder(); 135 136export const extractBody = (object: BodyInit | null): BodyState => { 137 let type: string | null = null; 138 let body: Readable | ReadableStream | Uint8Array | null; 139 let size: number | null = null; 140 if (object == null) { 141 body = null; 142 size = 0; 143 } else if (typeof object === 'string') { 144 const bytes = encoder.encode(`${object}`); 145 type = 'text/plain;charset=UTF-8'; 146 size = bytes.byteLength; 147 body = bytes; 148 } else if (isURLSearchParameters(object)) { 149 const bytes = encoder.encode(object.toString()); 150 body = bytes; 151 size = bytes.byteLength; 152 type = 'application/x-www-form-urlencoded;charset=UTF-8'; 153 } else if (isBlob(object)) { 154 size = object.size; 155 type = object.type || null; 156 body = object.stream(); 157 } else if (object instanceof Uint8Array) { 158 body = object; 159 size = object.byteLength; 160 } else if (isAnyArrayBuffer(object)) { 161 const bytes = new Uint8Array(object); 162 body = bytes; 163 size = bytes.byteLength; 164 } else if (ArrayBuffer.isView(object)) { 165 const bytes = new Uint8Array( 166 object.buffer, 167 object.byteOffset, 168 object.byteLength 169 ); 170 body = bytes; 171 size = bytes.byteLength; 172 } else if (isReadableStream(object)) { 173 body = object; 174 } else if (isFormData(object)) { 175 const boundary = makeFormBoundary(); 176 type = `multipart/form-data; boundary=${boundary}`; 177 size = getFormDataLength(object, boundary); 178 body = Readable.from(generatorOfFormData(object, boundary)); 179 } else if (isMultipartFormDataStream(object)) { 180 type = `multipart/form-data; boundary=${object.getBoundary()}`; 181 size = object.hasKnownLength() ? object.getLengthSync() : null; 182 body = object as Readable; 183 } else if (isReadable(object)) { 184 body = object as Readable; 185 } else if (isIterable(object)) { 186 body = Readable.from(object); 187 } else { 188 const bytes = encoder.encode(`${object}`); 189 type = 'text/plain;charset=UTF-8'; 190 body = bytes; 191 size = bytes.byteLength; 192 } 193 return { 194 contentLength: size, 195 contentType: type, 196 body, 197 }; 198};