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

fix: Add missing constructor overloads and `Blob` re-export (#2)

+5
.changeset/pink-papayas-doubt.md
···
+
---
+
'fetch-nodeshim': patch
+
---
+
+
Add missing constructor type overloads and add missing `Blob` re-export
+3 -3
.github/workflows/ci.yml
···
- name: Install Dependencies
run: pnpm install --frozen-lockfile --prefer-offline
-
- name: TypeScript
-
run: pnpm run check
-
- name: Unit Tests
run: pnpm run test
+
+
- name: Type checks
+
run: pnpm run check:all
- name: Build
run: pnpm run build
+8 -2
package.json
···
"scripts": {
"test": "vitest test",
"test:run": "vitest test --run",
-
"check": "tsc --noEmit",
"build": "rollup -c ./scripts/rollup.config.mjs",
+
"postbuild": "tsc --noEmit ./dist/minifetch.d.ts",
+
"check": "tsc --noEmit",
+
"check:node18": "tsc --noEmit -p ./tsconfig-node18.json",
+
"check:node20": "tsc --noEmit -p ./tsconfig-node20.json",
+
"check:all": "run-s check check:node18 check:node20",
"clean": "rimraf dist node_modules/.cache",
-
"prepublishOnly": "run-s clean build check test:run",
+
"prepublishOnly": "run-s clean build check:all test:run",
"prepare": "node ./scripts/prepare.js || true",
"changeset:version": "changeset version && pnpm install --lockfile-only",
"changeset:publish": "changeset publish"
···
"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-terser": "^0.4.4",
+
"@types-internal/node-18": "npm:@types/node@^18.19.0",
+
"@types-internal/node-20": "npm:@types/node@^20.17.0",
"@types/node": "^22.12.0",
"busboy": "^0.3.1",
"dotenv": "^16.4.7",
+30
pnpm-lock.yaml
···
'@rollup/plugin-terser':
specifier: ^0.4.4
version: 0.4.4(rollup@4.32.1)
+
'@types-internal/node-18':
+
specifier: npm:@types/node@^18.19.0
+
version: '@types/node@18.19.74'
+
'@types-internal/node-20':
+
specifier: npm:@types/node@^20.17.0
+
version: '@types/node@20.17.16'
'@types/node':
specifier: ^22.12.0
version: 22.12.0
···
'@types/node@12.20.55':
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
+
+
'@types/node@18.19.74':
+
resolution: {integrity: sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==}
+
+
'@types/node@20.17.16':
+
resolution: {integrity: sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==}
'@types/node@22.12.0':
resolution: {integrity: sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==}
···
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
+
undici-types@5.26.5:
+
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
+
undici-types@6.19.8:
+
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
+
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
···
'@types/estree@1.0.6': {}
'@types/node@12.20.55': {}
+
+
'@types/node@18.19.74':
+
dependencies:
+
undici-types: 5.26.5
+
+
'@types/node@20.17.16':
+
dependencies:
+
undici-types: 6.19.8
'@types/node@22.12.0':
dependencies:
···
has-bigints: 1.1.0
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
+
+
undici-types@5.26.5: {}
+
+
undici-types@6.19.8: {}
undici-types@6.20.0: {}
+1
src/body.ts
···
import { Readable } from 'node:stream';
import { isAnyArrayBuffer } from 'node:util/types';
import { randomBytes } from 'node:crypto';
+
import { Blob, FormData, URLSearchParams } from './webstd';
export type BodyInit =
| Exclude<RequestInit['body'], undefined | null>
+61 -23
src/webstd.ts
···
-
/// <reference types="@types/node" />
-
import * as buffer from 'node:buffer';
type Or<T, U> = void extends T ? U : T;
+
export type HeadersInit =
+
| string[][]
+
| Record<string, string | ReadonlyArray<string>>
+
| _Headers;
+
+
export type FormDataEntryValue = string | _File;
+
+
export type RequestInfo = string | _URL | _Request;
+
+
interface _Iterable<T, TReturn = any, TNext = any>
+
extends Or<
+
Iterable<T, TReturn, TNext>,
+
globalThis.Iterable<T, TReturn, TNext>
+
> {}
+
interface _AsyncIterable<T, TReturn = any, TNext = any>
+
extends Or<
+
AsyncIterable<T, TReturn, TNext>,
+
globalThis.AsyncIterable<T, TReturn, TNext>
+
> {}
+
interface _ReadableStream<T = any>
+
extends Or<ReadableStream<T>, globalThis.ReadableStream<T>> {}
+
+
// NOTE: AsyncIterable<Uint8Array> is left out
export type BodyInit =
| ArrayBuffer
-
| AsyncIterable<Uint8Array>
-
| Blob
-
| FormData
-
| Iterable<Uint8Array>
+
| _Blob
| NodeJS.ArrayBufferView
-
| URLSearchParams
+
| _URLSearchParams
+
| _ReadableStream
+
| _AsyncIterable<Uint8Array>
+
| _FormData
+
| _Iterable<Uint8Array>
| 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;
+
interface _File extends _Blob, Or<File, globalThis.File> {
+
readonly name: string;
+
readonly lastModified: number;
+
}
+
interface _File extends Or<globalThis.File, buffer.File> {}
+
interface FileClass extends Or<typeof globalThis.File, typeof buffer.File> {}
+
const _File: FileClass = globalThis.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 _RequestInit extends Or<RequestInit, globalThis.RequestInit> {
+
duplex?: 'half';
+
}
interface _ResponseInit extends Or<ResponseInit, globalThis.ResponseInit> {}
+
interface _Blob extends Or<Blob, globalThis.Blob> {}
+
interface BlobClass extends Or<typeof Blob, typeof globalThis.Blob> {}
+
const _Blob: BlobClass = Blob;
+
interface _URLSearchParams
extends Or<URLSearchParams, globalThis.URLSearchParams> {}
interface URLSearchParamsClass
···
const _URL: URLClass = URL;
interface _Request extends Or<Request, globalThis.Request> {}
-
interface RequestClass extends Or<typeof Request, typeof globalThis.Request> {}
+
interface RequestClass extends Or<typeof Request, typeof globalThis.Request> {
+
new (
+
input: RequestInfo,
+
init?: _RequestInit | Or<RequestInit, globalThis.RequestInit>
+
): _Request;
+
}
const _Request: RequestClass = Request;
interface _Response extends Or<Response, globalThis.Response> {}
interface ResponseClass
-
extends Or<typeof Response, typeof globalThis.Response> {}
+
extends Or<typeof Response, typeof globalThis.Response> {
+
new (body?: BodyInit, init?: _ResponseInit): _Response;
+
}
const _Response: ResponseClass = Response;
interface _Headers extends Or<Headers, globalThis.Headers> {}
-
interface HeadersClass extends Or<typeof Headers, typeof globalThis.Headers> {}
+
interface HeadersClass extends Or<typeof Headers, typeof globalThis.Headers> {
+
new (init?: HeadersInit): _Headers;
+
}
const _Headers: HeadersClass = Headers;
-
interface _FormData extends Or<FormData, globalThis.FormData> {}
+
interface _FormData
+
extends Or<
+
FormData & _Iterable<[string, FormDataEntryValue]>,
+
globalThis.FormData
+
> {}
interface FormDataClass
extends Or<typeof FormData, typeof globalThis.FormData> {}
const _FormData: FormDataClass = FormData;
···
export {
type _RequestInit as RequestInit,
type _ResponseInit as ResponseInit,
+
_Blob as Blob,
_File as File,
_URL as URL,
_URLSearchParams as URLSearchParams,
+11
tsconfig-node18.json
···
+
{
+
"extends": "./tsconfig.json",
+
"compilerOptions": {
+
"types": ["@types-internal/node-18"],
+
"paths": {
+
"@types/node": ["node_modules/@types-internal/node-18"],
+
"@types/node/*": ["node_modules/@types-internal/node-18/*"]
+
}
+
},
+
"include": ["src", "packages"]
+
}
+11
tsconfig-node20.json
···
+
{
+
"extends": "./tsconfig.json",
+
"compilerOptions": {
+
"types": ["@types-internal/node-20"],
+
"paths": {
+
"@types/node": ["node_modules/@types-internal/node-20"],
+
"@types/node/*": ["node_modules/@types-internal/node-20/*"]
+
}
+
},
+
"include": ["src", "packages"]
+
}
+1
tsconfig.json
···
{
"compilerOptions": {
+
"types": ["@types/node"],
"baseUrl": "./",
"rootDir": ".",
"forceConsistentCasingInFileNames": true,