diff --git a/.changeset/popular-lions-sell.md b/.changeset/popular-lions-sell.md new file mode 100644 index 00000000000..f0111e67cfa --- /dev/null +++ b/.changeset/popular-lions-sell.md @@ -0,0 +1,5 @@ +--- +'@whatwg-node/server': patch +--- + +Respect given fetchAPI diff --git a/.changeset/red-vans-rest.md b/.changeset/red-vans-rest.md new file mode 100644 index 00000000000..e48814fb21e --- /dev/null +++ b/.changeset/red-vans-rest.md @@ -0,0 +1,5 @@ +--- +'@whatwg-node/node-fetch': minor +--- + +Support `IteratorObject` diff --git a/e2e/aws-lambda/package.json b/e2e/aws-lambda/package.json index bf9ffc2bc9b..6235df9717a 100644 --- a/e2e/aws-lambda/package.json +++ b/e2e/aws-lambda/package.json @@ -19,6 +19,6 @@ "esbuild": "0.24.0", "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", - "typescript": "5.5.4" + "typescript": "5.6.3" } } diff --git a/e2e/azure-function/package.json b/e2e/azure-function/package.json index 70a4f1cda9d..b5d7c59b30c 100644 --- a/e2e/azure-function/package.json +++ b/e2e/azure-function/package.json @@ -20,6 +20,6 @@ "esbuild": "0.24.0", "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", - "typescript": "5.5.4" + "typescript": "5.6.3" } } diff --git a/e2e/bun/package.json b/e2e/bun/package.json index 58c4bb4613d..fdb26bb7278 100644 --- a/e2e/bun/package.json +++ b/e2e/bun/package.json @@ -12,6 +12,6 @@ "bun-types": "1.1.34" }, "devDependencies": { - "typescript": "5.5.4" + "typescript": "5.6.3" } } diff --git a/e2e/cloudflare-modules/package.json b/e2e/cloudflare-modules/package.json index a4e861197c9..b7c3070707c 100644 --- a/e2e/cloudflare-modules/package.json +++ b/e2e/cloudflare-modules/package.json @@ -15,7 +15,7 @@ "@pulumi/pulumi": "3.137.0", "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", - "typescript": "5.5.4", + "typescript": "5.6.3", "wrangler": "3.84.1" } } diff --git a/e2e/cloudflare-workers/package.json b/e2e/cloudflare-workers/package.json index 8f0ff0d75c2..ddf37c59ea9 100644 --- a/e2e/cloudflare-workers/package.json +++ b/e2e/cloudflare-workers/package.json @@ -15,7 +15,7 @@ "@pulumi/pulumi": "3.137.0", "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", - "typescript": "5.5.4", + "typescript": "5.6.3", "wrangler": "3.84.1" } } diff --git a/e2e/vercel/package.json b/e2e/vercel/package.json index b5c5625b368..0f0b5689c1a 100644 --- a/e2e/vercel/package.json +++ b/e2e/vercel/package.json @@ -26,6 +26,6 @@ "eslint-config-next": "15.0.2", "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", - "typescript": "5.5.4" + "typescript": "5.6.3" } } diff --git a/package.json b/package.json index 086524fdb3f..8b16eb7e4ff 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "prettier": "3.3.3", "rimraf": "6.0.1", "ts-jest": "29.2.5", - "typescript": "5.5.4" + "typescript": "5.6.3" }, "resolutions": { "@pulumi/pulumi": "3.137.0" diff --git a/packages/node-fetch/src/FormData.ts b/packages/node-fetch/src/FormData.ts index 9cdb7443be4..88b8a4de715 100644 --- a/packages/node-fetch/src/FormData.ts +++ b/packages/node-fetch/src/FormData.ts @@ -1,5 +1,6 @@ import { PonyfillBlob } from './Blob.js'; import { PonyfillFile } from './File.js'; +import { PonyfillIteratorObject } from './IteratorObject.js'; import { PonyfillReadableStream } from './ReadableStream.js'; export class PonyfillFormData implements FormData { @@ -45,7 +46,11 @@ export class PonyfillFormData implements FormData { this.map.set(name, [entry]); } - *[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]> { + [Symbol.iterator](): FormDataIterator<[string, FormDataEntryValue]> { + return this._entries(); + } + + *_entries(): FormDataIterator<[string, FormDataEntryValue]> { for (const [key, values] of this.map) { for (const value of values) { yield [key, value]; @@ -53,15 +58,19 @@ export class PonyfillFormData implements FormData { } } - entries(): IterableIterator<[string, FormDataEntryValue]> { - return this[Symbol.iterator](); + entries(): FormDataIterator<[string, FormDataEntryValue]> { + return new PonyfillIteratorObject(this._entries(), 'FormDataIterator'); } - keys(): IterableIterator { + _keys(): IterableIterator { return this.map.keys(); } - *values(): IterableIterator { + keys(): FormDataIterator { + return new PonyfillIteratorObject(this._keys(), 'FormDataIterator'); + } + + *_values(): IterableIterator { for (const values of this.map.values()) { for (const value of values) { yield value; @@ -69,6 +78,10 @@ export class PonyfillFormData implements FormData { } } + values(): FormDataIterator { + return new PonyfillIteratorObject(this._values(), 'FormDataIterator'); + } + forEach(callback: (value: FormDataEntryValue, key: string, parent: this) => void): void { for (const [key, value] of this) { callback(value, key, this); diff --git a/packages/node-fetch/src/Headers.ts b/packages/node-fetch/src/Headers.ts index 670f88bac80..a7b5a616fa4 100644 --- a/packages/node-fetch/src/Headers.ts +++ b/packages/node-fetch/src/Headers.ts @@ -1,4 +1,5 @@ import { inspect } from 'util'; +import { PonyfillIteratorObject } from './IteratorObject.js'; export type PonyfillHeadersInit = [string, string][] | Record | Headers; @@ -177,7 +178,7 @@ export class PonyfillHeaders implements Headers { }); } - *keys(): IterableIterator { + *_keys(): IterableIterator { if (this._setCookies.length) { yield 'set-cookie'; } @@ -198,7 +199,11 @@ export class PonyfillHeaders implements Headers { yield* this.getMap().keys(); } - *values(): IterableIterator { + keys(): HeadersIterator { + return new PonyfillIteratorObject(this._keys(), 'HeadersIterator'); + } + + *_values(): IterableIterator { yield* this._setCookies; if (!this._map) { if (this.headersInit) { @@ -217,7 +222,11 @@ export class PonyfillHeaders implements Headers { yield* this.getMap().values(); } - *entries(): IterableIterator<[string, string]> { + values(): HeadersIterator { + return new PonyfillIteratorObject(this._values(), 'HeadersIterator'); + } + + *_entries(): IterableIterator<[string, string]> { yield* this._setCookies.map(cookie => ['set-cookie', cookie] as [string, string]); if (!this._map) { if (this.headersInit) { @@ -236,11 +245,15 @@ export class PonyfillHeaders implements Headers { yield* this.getMap().entries(); } + entries(): HeadersIterator<[string, string]> { + return new PonyfillIteratorObject(this._entries(), 'HeadersIterator'); + } + getSetCookie() { return this._setCookies; } - [Symbol.iterator](): IterableIterator<[string, string]> { + [Symbol.iterator](): HeadersIterator<[string, string]> { return this.entries(); } diff --git a/packages/node-fetch/src/IteratorObject.ts b/packages/node-fetch/src/IteratorObject.ts new file mode 100644 index 00000000000..9fbe6f3c283 --- /dev/null +++ b/packages/node-fetch/src/IteratorObject.ts @@ -0,0 +1,155 @@ +import { inspect } from 'node:util'; +import { isIterable } from './utils.js'; + +export class PonyfillIteratorObject implements IteratorObject { + [Symbol.toStringTag] = 'IteratorObject'; + constructor( + private iterableIterator: IterableIterator, + className: string, + ) { + this[Symbol.toStringTag] = className; + } + + *map(callbackfn: (value: T, index: number) => U) { + let index = 0; + for (const value of this.iterableIterator) { + yield callbackfn(value, index++); + } + return undefined; + } + + *filter(callbackfn: (value: T, index: number) => boolean) { + let index = 0; + for (const value of this.iterableIterator) { + if (callbackfn(value, index++)) { + yield value; + } + } + return undefined; + } + + reduce( + callbackfn: (previousValue: U, currentValue: T, currentIndex: number) => U, + initialValue?: U, + ) { + let index = 0; + let accumulator = initialValue as U; + for (const value of this.iterableIterator) { + accumulator = callbackfn(accumulator, value, index++); + } + return accumulator; + } + + forEach(callbackfn: (value: T, index: number) => void): void { + let index = 0; + for (const value of this.iterableIterator) { + callbackfn(value, index++); + } + } + + *take(limit: number) { + let index = 0; + for (const value of this.iterableIterator) { + if (index >= limit) { + break; + } + yield value; + index++; + } + return undefined; + } + + *drop(count: number): IteratorObject { + let index = 0; + for (const value of this.iterableIterator) { + if (index >= count) { + yield value; + } + index++; + } + return undefined; + } + + *flatMap( + callback: ( + value: T, + index: number, + ) => Iterator | Iterable, + ): IteratorObject { + let index = 0; + for (const value of this.iterableIterator) { + const iteratorOrIterable = callback(value, index++); + if (isIterable(iteratorOrIterable)) { + for (const innerValue of iteratorOrIterable) { + yield innerValue; + } + } else { + for (const innerValue of { + [Symbol.iterator]: () => iteratorOrIterable, + }) { + yield innerValue; + } + } + } + return undefined; + } + + some(predicate: (value: T, index: number) => unknown): boolean { + let index = 0; + for (const value of this.iterableIterator) { + if (predicate(value, index++)) { + return true; + } + } + return false; + } + + every(predicate: (value: T, index: number) => unknown): boolean { + let index = 0; + for (const value of this.iterableIterator) { + if (!predicate(value, index++)) { + return false; + } + } + return true; + } + + find(predicate: (value: T, index: number) => unknown): T | undefined { + let index = 0; + for (const value of this.iterableIterator) { + if (predicate(value, index++)) { + return value; + } + } + return undefined; + } + + toArray(): T[] { + return Array.from(this.iterableIterator); + } + + [Symbol.dispose](): void { + if (typeof (this.iterableIterator as any).return === 'function') { + (this.iterableIterator as any).return(); + } + } + + next(...[value]: [] | [unknown]): IteratorResult { + return this.iterableIterator.next(value); + } + + [Symbol.iterator](): URLSearchParamsIterator { + return this; + } + + [Symbol.for('nodejs.util.inspect.custom')]() { + const record: Record = {}; + this.forEach((value, key) => { + const inspectedValue = inspect(value); + record[key] = inspectedValue.includes(',') + ? inspectedValue.split(',').map(el => el.trim()) + : inspectedValue; + }); + return `${this[Symbol.toStringTag]} ${inspect(record)}`; + } +} diff --git a/packages/node-fetch/src/URLSearchParams.ts b/packages/node-fetch/src/URLSearchParams.ts index 72c6e866a34..68ba74704e2 100644 --- a/packages/node-fetch/src/URLSearchParams.ts +++ b/packages/node-fetch/src/URLSearchParams.ts @@ -1,4 +1,5 @@ import FastQuerystring from 'fast-querystring'; +import { PonyfillIteratorObject } from './IteratorObject.js'; function isURLSearchParams(value: any): value is URLSearchParams { return value?.entries != null; @@ -75,13 +76,17 @@ export class PonyfillURLSearchParams implements URLSearchParams { return FastQuerystring.stringify(this.params); } - *keys(): IterableIterator { + *_keys(): IterableIterator { for (const key in this.params) { yield key; } } - *entries(): IterableIterator<[string, string]> { + keys(): URLSearchParamsIterator { + return new PonyfillIteratorObject(this._keys(), 'URLSearchParamsIterator'); + } + + *_entries(): IterableIterator<[string, string]> { for (const key of this.keys()) { const value = this.params[key]; if (Array.isArray(value)) { @@ -94,13 +99,21 @@ export class PonyfillURLSearchParams implements URLSearchParams { } } - *values(): IterableIterator { + entries(): URLSearchParamsIterator<[string, string]> { + return new PonyfillIteratorObject(this._entries(), 'URLSearchParamsIterator'); + } + + *_values(): IterableIterator { for (const [, value] of this) { yield value; } } - [Symbol.iterator](): IterableIterator<[string, string]> { + values(): URLSearchParamsIterator { + return new PonyfillIteratorObject(this._values(), 'URLSearchParamsIterator'); + } + + [Symbol.iterator](): URLSearchParamsIterator<[string, string]> { return this.entries(); } diff --git a/packages/node-fetch/src/index.ts b/packages/node-fetch/src/index.ts index de504e5a94d..a7c2f522eef 100644 --- a/packages/node-fetch/src/index.ts +++ b/packages/node-fetch/src/index.ts @@ -18,3 +18,4 @@ export { PonyfillWritableStream as WritableStream } from './WritableStream.js'; export { PonyfillTransformStream as TransformStream } from './TransformStream.js'; export { PonyfillCompressionStream as CompressionStream } from './CompressionStream.js'; export { PonyfillDecompressionStream as DecompressionStream } from './DecompressionStream.js'; +export { PonyfillIteratorObject as IteratorObject } from './IteratorObject.js'; diff --git a/packages/node-fetch/src/utils.ts b/packages/node-fetch/src/utils.ts index 811f41bf0e2..04f872d192e 100644 --- a/packages/node-fetch/src/utils.ts +++ b/packages/node-fetch/src/utils.ts @@ -98,3 +98,7 @@ export function createDeferredPromise(): DeferredPromise { }, }; } + +export function isIterable(value: any): value is Iterable { + return value?.[Symbol.iterator] != null; +} diff --git a/packages/server/src/createServerAdapter.ts b/packages/server/src/createServerAdapter.ts index a034f246200..39aa43363b3 100644 --- a/packages/server/src/createServerAdapter.ts +++ b/packages/server/src/createServerAdapter.ts @@ -193,7 +193,7 @@ function createServerAdapter< // TODO: Remove this on the next major version function handleNodeRequest(nodeRequest: NodeRequest, ...ctx: Partial[]) { const serverContext = ctx.length > 1 ? completeAssign(...ctx) : ctx[0] || {}; - const request = normalizeNodeRequest(nodeRequest, fetchAPI.Request); + const request = normalizeNodeRequest(nodeRequest, fetchAPI); return handleRequest(request, serverContext); } diff --git a/packages/server/src/plugins/useCors.ts b/packages/server/src/plugins/useCors.ts index 07c191d902a..fe35ca21568 100644 --- a/packages/server/src/plugins/useCors.ts +++ b/packages/server/src/plugins/useCors.ts @@ -1,4 +1,4 @@ -import { ServerAdapterPlugin } from './types.js'; +import type { ServerAdapterPlugin } from './types.js'; export type CORSOptions = | { diff --git a/packages/server/src/plugins/useErrorHandling.ts b/packages/server/src/plugins/useErrorHandling.ts index 5f49c51a6e2..20d35a8a462 100644 --- a/packages/server/src/plugins/useErrorHandling.ts +++ b/packages/server/src/plugins/useErrorHandling.ts @@ -1,6 +1,6 @@ import { Response as DefaultResponseCtor } from '@whatwg-node/fetch'; import { isPromise } from '../utils.js'; -import { ServerAdapterPlugin } from './types.js'; +import type { ServerAdapterPlugin } from './types.js'; export function createDefaultErrorHandler( ResponseCtor: typeof Response = DefaultResponseCtor, diff --git a/packages/server/src/utils.ts b/packages/server/src/utils.ts index f70cd7ddc13..763b5ea9c11 100644 --- a/packages/server/src/utils.ts +++ b/packages/server/src/utils.ts @@ -2,7 +2,6 @@ import type { IncomingMessage, ServerResponse } from 'http'; import type { Http2ServerRequest, Http2ServerResponse } from 'http2'; import type { Socket } from 'net'; import type { Readable } from 'stream'; -import { URL } from '@whatwg-node/fetch'; import type { FetchAPI, FetchEvent } from './types.js'; export function isAsyncIterable(body: any): body is AsyncIterable { @@ -122,14 +121,11 @@ let bunNodeCompatModeWarned = false; export const nodeRequestResponseMap = new WeakMap(); -export function normalizeNodeRequest( - nodeRequest: NodeRequest, - RequestCtor: typeof Request, -): Request { +export function normalizeNodeRequest(nodeRequest: NodeRequest, fetchAPI: FetchAPI): Request { const rawRequest = nodeRequest.raw || nodeRequest.req || nodeRequest; let fullUrl = buildFullUrl(rawRequest); if (nodeRequest.query) { - const url = new URL(fullUrl); + const url = new fetchAPI.URL(fullUrl); for (const key in nodeRequest.query) { url.searchParams.set(key, nodeRequest.query[key]); } @@ -153,7 +149,7 @@ export function normalizeNodeRequest( let sendAbortSignal: VoidFunction; // If ponyfilled - if (RequestCtor !== globalThis.Request) { + if (fetchAPI.Request !== globalThis.Request) { signal = new ServerAdapterRequestAbortSignal(); sendAbortSignal = () => (signal as ServerAdapterRequestAbortSignal).sendAbort(); } else { @@ -178,7 +174,7 @@ export function normalizeNodeRequest( } if (nodeRequest.method === 'GET' || nodeRequest.method === 'HEAD') { - return new RequestCtor(fullUrl, { + return new fetchAPI.Request(fullUrl, { method: nodeRequest.method, headers: normalizedHeaders, signal, @@ -194,14 +190,14 @@ export function normalizeNodeRequest( const maybeParsedBody = nodeRequest.body; if (maybeParsedBody != null && Object.keys(maybeParsedBody).length > 0) { if (isRequestBody(maybeParsedBody)) { - return new RequestCtor(fullUrl, { + return new fetchAPI.Request(fullUrl, { method: nodeRequest.method, headers: normalizedHeaders, body: maybeParsedBody, signal, }); } - const request = new RequestCtor(fullUrl, { + const request = new fetchAPI.Request(fullUrl, { method: nodeRequest.method, headers: normalizedHeaders, signal, @@ -232,7 +228,7 @@ export function normalizeNodeRequest( It will affect your performance. Please check our Bun integration recipe, and avoid using 'http' for your server implementation.`, ); } - return new RequestCtor(fullUrl, { + return new fetchAPI.Request(fullUrl, { method: nodeRequest.method, headers: normalizedHeaders, duplex: 'half', @@ -257,7 +253,7 @@ It will affect your performance. Please check our Bun integration recipe, and av } // perf: instead of spreading the object, we can just pass it as is and it performs better - return new RequestCtor(fullUrl, { + return new fetchAPI.Request(fullUrl, { method: nodeRequest.method, headers: normalizedHeaders, body: rawRequest as any, diff --git a/yarn.lock b/yarn.lock index 28eb2e4cc5c..da8451163dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9491,10 +9491,10 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" -typescript@5.5.4: - version "5.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" - integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== +typescript@5.6.3: + version "5.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== uWebSockets.js@uNetworking/uWebSockets.js#v20.49.0: version "20.49.0"