Mirror: A Node.js fetch shim using built-in Request, Response, and Headers (but without native fetch)

feat: Add web standard type/global re-exports and set `File` global (#1)

+5
.changeset/pink-flowers-wait.md
···
···
+
---
+
'fetch-nodeshim': minor
+
---
+
+
Add web standard type/globals re-exports and polyfill `File` from `node:buffer`.
+1 -1
package.json
···
"rollup-plugin-cjs-check": "^1.0.3",
"rollup-plugin-dts": "^6.1.1",
"typescript": "^5.7.3",
-
"undici-types": "^7.3.0",
"vitest": "^3.0.4"
}
}
···
"rollup-plugin-cjs-check": "^1.0.3",
"rollup-plugin-dts": "^6.1.1",
"typescript": "^5.7.3",
+
"undici-types": "^6.20.0",
"vitest": "^3.0.4"
}
}
+2 -7
pnpm-lock.yaml
···
specifier: ^5.7.3
version: 5.7.3
undici-types:
-
specifier: ^7.3.0
-
version: 7.3.0
vitest:
specifier: ^3.0.4
version: 3.0.4(@types/node@22.12.0)(terser@5.37.0)(yaml@2.7.0)
···
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
-
-
undici-types@7.3.0:
-
resolution: {integrity: sha512-z2pHpkN2BEJl3QlQo0GtfGCyuhuBbWX60vzGwyn7ex/seM2UkvyGEfEV0Qb9pXc5StNfcJpsstgaf2YTEJa63Q==}
universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
···
which-boxed-primitive: 1.1.1
undici-types@6.20.0: {}
-
-
undici-types@7.3.0: {}
universalify@0.1.2: {}
···
specifier: ^5.7.3
version: 5.7.3
undici-types:
+
specifier: ^6.20.0
+
version: 6.20.0
vitest:
specifier: ^3.0.4
version: 3.0.4(@types/node@22.12.0)(terser@5.37.0)(yaml@2.7.0)
···
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
···
which-boxed-primitive: 1.1.1
undici-types@6.20.0: {}
universalify@0.1.2: {}
+7 -4
src/fetch.ts
···
import { extractBody } from './body';
import { createContentDecoder } from './encoding';
/** Maximum allowed redirects (matching Chromium's limit) */
const MAX_REDIRECTS = 20;
···
return response;
}
-
export async function fetch(
input: string | URL | Request,
requestInit?: RequestInit
): Promise<Response> {
···
signal,
} satisfies http.RequestOptions;
-
function _fetch(
resolve: (response: Response | Promise<Response>) => void,
reject: (reason?: any) => void
) {
···
requestOptions,
urlToHttpOptions((requestUrl = locationURL))
);
-
return _fetch(resolve, reject);
}
}
···
}
}
-
return await new Promise(_fetch);
}
···
import { extractBody } from './body';
import { createContentDecoder } from './encoding';
+
import { URL, Request, RequestInit, Response } from './webstd';
/** Maximum allowed redirects (matching Chromium's limit) */
const MAX_REDIRECTS = 20;
···
return response;
}
+
async function _fetch(
input: string | URL | Request,
requestInit?: RequestInit
): Promise<Response> {
···
signal,
} satisfies http.RequestOptions;
+
function _call(
resolve: (response: Response | Promise<Response>) => void,
reject: (reason?: any) => void
) {
···
requestOptions,
urlToHttpOptions((requestUrl = locationURL))
);
+
return _call(resolve, reject);
}
}
···
}
}
+
return await new Promise(_call);
}
+
+
export { _fetch as fetch };
+1
src/index.ts
···
export { fetch, fetch as default } from './fetch';
···
export { fetch, fetch as default } from './fetch';
+
export * from './webstd';
+81
src/webstd.ts
···
···
+
/// <reference types="@types/node" />
+
+
import * as buffer from 'node:buffer';
+
+
type Or<T, U> = void extends T ? U : T;
+
+
export type BodyInit =
+
| ArrayBuffer
+
| AsyncIterable<Uint8Array>
+
| Blob
+
| FormData
+
| Iterable<Uint8Array>
+
| NodeJS.ArrayBufferView
+
| URLSearchParams
+
| null
+
| string;
+
+
// See: https://nodejs.org/docs/latest-v20.x/api/globals.html#class-file
+
// The `File` global was only added in Node.js 20
+
interface _File extends Or<File, globalThis.File> {}
+
const _File: Or<typeof File, typeof buffer.File> = buffer.File;
+
if (typeof globalThis.File === 'undefined') {
+
globalThis.File = _File;
+
}
+
+
declare global {
+
var File: typeof _File;
+
+
// NOTE: In case undici was used, but its types aren't applied, this needs to be added
+
interface RequestInit {
+
duplex?: 'half';
+
}
+
}
+
+
// There be dragons here.
+
// This is complex because of overlapping definitions in lib.dom, @types/node, and undici-types
+
// Some types define and overload constructor interfaces with type interfaces
+
// Here, we have to account for global differences and split the overloads apart
+
+
interface _RequestInit extends Or<RequestInit, globalThis.RequestInit> {}
+
interface _ResponseInit extends Or<ResponseInit, globalThis.ResponseInit> {}
+
+
interface _URLSearchParams
+
extends Or<URLSearchParams, globalThis.URLSearchParams> {}
+
interface URLSearchParamsClass
+
extends Or<typeof URLSearchParams, typeof globalThis.URLSearchParams> {}
+
const _URLSearchParams: URLSearchParamsClass = URLSearchParams as any;
+
+
interface _URL extends Or<URL, globalThis.URL> {}
+
interface URLClass extends Or<typeof URL, typeof globalThis.URL> {}
+
const _URL: URLClass = URL;
+
+
interface _Request extends Or<Request, globalThis.Request> {}
+
interface RequestClass extends Or<typeof Request, typeof globalThis.Request> {}
+
const _Request: RequestClass = Request;
+
+
interface _Response extends Or<Response, globalThis.Response> {}
+
interface ResponseClass
+
extends Or<typeof Response, typeof globalThis.Response> {}
+
const _Response: ResponseClass = Response;
+
+
interface _Headers extends Or<Headers, globalThis.Headers> {}
+
interface HeadersClass extends Or<typeof Headers, typeof globalThis.Headers> {}
+
const _Headers: HeadersClass = Headers;
+
+
interface _FormData extends Or<FormData, globalThis.FormData> {}
+
interface FormDataClass
+
extends Or<typeof FormData, typeof globalThis.FormData> {}
+
const _FormData: FormDataClass = FormData;
+
+
export {
+
type _RequestInit as RequestInit,
+
type _ResponseInit as ResponseInit,
+
_File as File,
+
_URL as URL,
+
_URLSearchParams as URLSearchParams,
+
_Request as Request,
+
_Response as Response,
+
_Headers as Headers,
+
_FormData as FormData,
+
};