From 4824abe41a63a67bc055ff9104f682513fe05244 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Sun, 24 May 2020 11:58:51 -0400 Subject: [PATCH] Breaking: Revamp TypeScript declarations (#810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * revamp types * add GitHub Action for TypeScript check Co-authored-by: Linus Unnebäck Co-authored-by: Antoni Kepinski --- .github/workflows/types.yml | 20 +++ .npmrc | 1 + @types/index.d.ts | 184 +++++++++++++++++++++++++++ @types/index.test-d.ts | 66 ++++++++++ index.d.ts | 220 -------------------------------- package.json | 241 +++++++++++++++++++----------------- 6 files changed, 397 insertions(+), 335 deletions(-) create mode 100644 .github/workflows/types.yml create mode 100644 @types/index.d.ts create mode 100644 @types/index.test-d.ts delete mode 100644 index.d.ts diff --git a/.github/workflows/types.yml b/.github/workflows/types.yml new file mode 100644 index 0000000..d26f2c9 --- /dev/null +++ b/.github/workflows/types.yml @@ -0,0 +1,20 @@ +name: CI + +on: + pull_request: + paths: + - '**.ts' + - package.json + - .github/workflows/types.yml + +jobs: + typescript: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + + - run: npm install + + - name: Check typings file + run: npm run test-types diff --git a/.npmrc b/.npmrc index 43c97e7..5c69597 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ package-lock=false +save-exact=false diff --git a/@types/index.d.ts b/@types/index.d.ts new file mode 100644 index 0000000..2f94ad6 --- /dev/null +++ b/@types/index.d.ts @@ -0,0 +1,184 @@ +/// + +/* eslint-disable no-var, import/no-mutable-exports */ + +import {Agent} from 'http'; +import {AbortSignal} from 'abort-controller'; +import Blob from 'fetch-blob'; + +type HeadersInit = Headers | string[][] | Record; + +/** + * This Fetch API interface allows you to perform various actions on HTTP request and response headers. + * These actions include retrieving, setting, adding to, and removing. + * A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs. + * You can add to this using methods like append() (see Examples.) + * In all methods of this interface, header names are matched by case-insensitive byte sequence. + * */ +interface Headers { + append: (name: string, value: string) => void; + delete: (name: string) => void; + get: (name: string) => string | null; + has: (name: string) => boolean; + set: (name: string, value: string) => void; + forEach: ( + callbackfn: (value: string, key: string, parent: Headers) => void, + thisArg?: any + ) => void; + + [Symbol.iterator]: () => IterableIterator<[string, string]>; + /** + * Returns an iterator allowing to go through all key/value pairs contained in this object. + */ + entries: () => IterableIterator<[string, string]>; + /** + * Returns an iterator allowing to go through all keys of the key/value pairs contained in this object. + */ + keys: () => IterableIterator; + /** + * Returns an iterator allowing to go through all values of the key/value pairs contained in this object. + */ + values: () => IterableIterator; + + /** Node-fetch extension */ + raw: () => Record; +} +declare var Headers: { + prototype: Headers; + new (init?: HeadersInit): Headers; +}; + +interface RequestInit { + /** + * A BodyInit object or null to set request's body. + */ + body?: BodyInit | null; + /** + * A Headers object, an object literal, or an array of two-item arrays to set request's headers. + */ + headers?: HeadersInit; + /** + * A string to set request's method. + */ + method?: string; + /** + * A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. + */ + redirect?: RequestRedirect; + /** + * An AbortSignal to set request's signal. + */ + signal?: AbortSignal | null; + + // Node-fetch extensions to the whatwg/fetch spec + agent?: Agent | ((parsedUrl: URL) => Agent); + compress?: boolean; + counter?: number; + follow?: number; + hostname?: string; + port?: number; + protocol?: string; + size?: number; + timeout?: number; + highWaterMark?: number; +} + +interface ResponseInit { + headers?: HeadersInit; + status?: number; + statusText?: string; +} + +type BodyInit = + | Blob + | Buffer + | URLSearchParams + | NodeJS.ReadableStream + | string; +interface Body { + readonly body: NodeJS.ReadableStream | null; + readonly bodyUsed: boolean; + readonly size: number; + readonly timeout: number; + buffer: () => Promise; + arrayBuffer: () => Promise; + blob: () => Promise; + json: () => Promise; + text: () => Promise; +} +declare var Body: { + prototype: Body; + new (body?: BodyInit, opts?: {size?: number; timeout?: number}): Body; +}; + +type RequestRedirect = 'error' | 'follow' | 'manual'; +interface Request extends Body { + /** + * Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. + */ + readonly headers: Headers; + /** + * Returns request's HTTP method, which is "GET" by default. + */ + readonly method: string; + /** + * Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. + */ + readonly redirect: RequestRedirect; + /** + * Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. + */ + readonly signal: AbortSignal; + /** + * Returns the URL of request as a string. + */ + readonly url: string; + clone: () => Request; +} +type RequestInfo = string | Body; +declare var Request: { + prototype: Request; + new (input: RequestInfo, init?: RequestInit): Request; +}; + +interface Response extends Body { + readonly headers: Headers; + readonly ok: boolean; + readonly redirected: boolean; + readonly status: number; + readonly statusText: string; + readonly url: string; + clone: () => Response; +} + +declare var Response: { + prototype: Response; + new (body?: BodyInit | null, init?: ResponseInit): Response; +}; + +declare function fetch(url: RequestInfo, init?: RequestInit): Promise; + +declare namespace fetch { + function isRedirect(code: number): boolean; +} + +interface FetchError extends Error { + name: 'FetchError'; + [Symbol.toStringTag]: 'FetchError'; + type: string; + code?: string; + errno?: string; +} +declare var FetchError: { + prototype: FetchError; + new (message: string, type: string, systemError?: object): FetchError; +}; + +export class AbortError extends Error { + type: string; + name: 'AbortError'; + [Symbol.toStringTag]: 'AbortError'; +} + +export {Headers, Request, Response, FetchError}; +export default fetch; diff --git a/@types/index.test-d.ts b/@types/index.test-d.ts new file mode 100644 index 0000000..094a0a8 --- /dev/null +++ b/@types/index.test-d.ts @@ -0,0 +1,66 @@ +import {expectType} from 'tsd'; +import fetch, {Request, Response, Headers, FetchError, AbortError} from '.'; + +async function run() { + const getRes = await fetch('https://bigfile.com/test.zip'); + expectType(getRes.ok); + expectType(getRes.size); + expectType(getRes.status); + expectType(getRes.statusText); + expectType<() => Response>(getRes.clone); + + // Test async iterator over body + expectType(getRes.body); + if (getRes.body) { + for await (const data of getRes.body) { + expectType(data); + } + } + + // Test Buffer + expectType(await getRes.buffer()); + + // Test arrayBuffer + expectType(await getRes.arrayBuffer()); + + // Test JSON, returns unknown + expectType(await getRes.json()); + + // Headers iterable + expectType(getRes.headers); + + // Post + try { + const request = new Request('http://byjka.com/buka'); + expectType(request.url); + expectType(request.headers); + expectType(request.timeout); + + const headers = new Headers({byaka: 'buke'}); + expectType<(a: string, b: string) => void>(headers.append); + expectType<(a: string) => string | null>(headers.get); + expectType<(name: string, value: string) => void>(headers.set); + expectType<(name: string) => void>(headers.delete); + expectType<() => IterableIterator>(headers.keys); + expectType<() => IterableIterator<[string, string]>>(headers.entries); + expectType<() => IterableIterator<[string, string]>>(headers[Symbol.iterator]); + + const postRes = await fetch(request, {method: 'POST', headers}); + expectType(await postRes.blob()); + } catch (error) { + if (error instanceof FetchError) { + throw new TypeError(error.errno); + } + + if (error instanceof AbortError) { + throw error; + } + } + + const response = new Response(); + expectType(response.url); +} + +run().finally(() => { + console.log('✅'); +}); diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index bcef0f4..0000000 --- a/index.d.ts +++ /dev/null @@ -1,220 +0,0 @@ -// Prior contributors: Torsten Werner -// Niklas Lindgren -// Vinay Bedre -// Antonio Román -// Andrew Leedham -// Jason Li -// Brandon Wilson -// Steve Faulkner - -/// - -import {URL, URLSearchParams} from 'url'; -import {Agent} from 'http'; - -export class Request extends Body { - method: string; - redirect: RequestRedirect; - referrer: string; - url: string; - - // Node-fetch extensions to the whatwg/fetch spec - agent?: Agent | ((parsedUrl: URL) => Agent); - compress: boolean; - counter: number; - follow: number; - hostname: string; - port?: number; - protocol: string; - size: number; - timeout: number; - highWaterMark?: number; - - context: RequestContext; - headers: Headers; - constructor(input: string | { href: string } | Request, init?: RequestInit); - static redirect(url: string, status?: number): Response; - clone(): Request; -} - -export interface RequestInit { - // Whatwg/fetch standard options - body?: BodyInit; - headers?: HeadersInit; - method?: string; - redirect?: RequestRedirect; - signal?: AbortSignal | null; - - // Node-fetch extensions - agent?: Agent | ((parsedUrl: URL) => Agent); // =null http.Agent instance, allows custom proxy, certificate etc. - compress?: boolean; // =true support gzip/deflate content encoding. false to disable - follow?: number; // =20 maximum redirect count. 0 to not follow redirect - size?: number; // =0 maximum response body size in bytes. 0 to disable - timeout?: number; // =0 req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies) - highWaterMark?: number; // =16384 the maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. - - // node-fetch does not support mode, cache or credentials options -} - -export type RequestContext = - 'audio' - | 'beacon' - | 'cspreport' - | 'download' - | 'embed' - | 'eventsource' - | 'favicon' - | 'fetch' - | 'font' - | 'form' - | 'frame' - | 'hyperlink' - | 'iframe' - | 'image' - | 'imageset' - | 'import' - | 'internal' - | 'location' - | 'manifest' - | 'object' - | 'ping' - | 'plugin' - | 'prefetch' - | 'script' - | 'serviceworker' - | 'sharedworker' - | 'style' - | 'subresource' - | 'track' - | 'video' - | 'worker' - | 'xmlhttprequest' - | 'xslt'; -export type RequestMode = 'cors' | 'no-cors' | 'same-origin'; -export type RequestRedirect = 'error' | 'follow' | 'manual'; -export type RequestCredentials = 'omit' | 'include' | 'same-origin'; - -export type RequestCache = - 'default' - | 'force-cache' - | 'no-cache' - | 'no-store' - | 'only-if-cached' - | 'reload'; - -export class Headers implements Iterable<[string, string]> { - constructor(init?: HeadersInit); - forEach(callback: (value: string, name: string) => void): void; - append(name: string, value: string): void; - delete(name: string): void; - get(name: string): string | null; - getAll(name: string): string[]; - has(name: string): boolean; - raw(): { [k: string]: string[] }; - set(name: string, value: string): void; - - // Iterator methods - entries(): Iterator<[string, string]>; - keys(): Iterator; - values(): Iterator<[string]>; - [Symbol.iterator](): Iterator<[string, string]>; -} - -type BlobPart = ArrayBuffer | ArrayBufferView | Blob | string; - -interface BlobOptions { - type?: string; - endings?: 'transparent' | 'native'; -} - -export class Blob { - readonly type: string; - readonly size: number; - constructor(blobParts?: BlobPart[], options?: BlobOptions); - slice(start?: number, end?: number): Blob; -} - -export class Body { - body: NodeJS.ReadableStream; - bodyUsed: boolean; - size: number; - timeout: number; - constructor(body?: any, opts?: { size?: number; timeout?: number }); - arrayBuffer(): Promise; - blob(): Promise; - buffer(): Promise; - json(): Promise; - text(): Promise; -} - -export class FetchError extends Error { - name: 'FetchError'; - [Symbol.toStringTag]: 'FetchError'; - type: string; - code?: string; - errno?: string; - constructor(message: string, type: string, systemError?: object); -} - -export class AbortError extends Error { - type: string; - message: string; - name: 'AbortError'; - [Symbol.toStringTag]: 'AbortError'; - constructor(message: string); -} - -export class Response extends Body { - headers: Headers; - ok: boolean; - redirected: boolean; - status: number; - statusText: string; - type: ResponseType; - url: string; - size: number; - timeout: number; - constructor(body?: BodyInit, init?: ResponseInit); - static error(): Response; - static redirect(url: string, status: number): Response; - clone(): Response; -} - -export type ResponseType = - 'basic' - | 'cors' - | 'default' - | 'error' - | 'opaque' - | 'opaqueredirect'; - -export interface ResponseInit { - headers?: HeadersInit; - size?: number; - status?: number; - statusText?: string; - timeout?: number; - url?: string; -} - -export type HeadersInit = Headers | string[][] | { [key: string]: string }; -// HeaderInit is exported to support backwards compatibility. See PR #34382 -export type HeaderInit = HeadersInit; -export type BodyInit = - ArrayBuffer - | ArrayBufferView - | NodeJS.ReadableStream - | string - | URLSearchParams; -export type RequestInfo = string | Request; - -declare function fetch( - url: RequestInfo, - init?: RequestInit -): Promise; - -declare namespace fetch { - function isRedirect(code: number): boolean; -} - -export default fetch; diff --git a/package.json b/package.json index b029149..f963fd6 100644 --- a/package.json +++ b/package.json @@ -1,117 +1,128 @@ { - "name": "node-fetch", - "version": "3.0.0-beta.5", - "description": "A light-weight module that brings window.fetch to node.js", - "main": "./dist/index.cjs", - "module": "./src/index.js", - "sideEffects": false, - "type": "module", - "exports": { - "import": "./src/index.js", - "require": "./dist/index.cjs" - }, - "files": [ - "src", - "dist", - "*.d.ts" - ], - "engines": { - "node": ">=10.16" - }, - "scripts": { - "build": "rollup -c", - "test": "node --experimental-modules node_modules/c8/bin/c8 --reporter=html --reporter=lcov --reporter=text --check-coverage node --experimental-modules node_modules/mocha/bin/mocha", - "coverage": "c8 report --reporter=text-lcov | coveralls", - "lint": "xo" - }, - "repository": { - "type": "git", - "url": "https://github.com/node-fetch/node-fetch.git" - }, - "keywords": [ - "fetch", - "http", - "promise" - ], - "author": "David Frank", - "license": "MIT", - "bugs": { - "url": "https://github.com/node-fetch/node-fetch/issues" - }, - "homepage": "https://github.com/node-fetch/node-fetch", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - }, - "devDependencies": { - "abort-controller": "^3.0.0", - "abortcontroller-polyfill": "^1.4.0", - "c8": "^7.1.2", - "chai": "^4.2.0", - "chai-as-promised": "^7.1.1", - "chai-iterator": "^3.0.2", - "chai-string": "^1.5.0", - "coveralls": "^3.1.0", - "delay": "^4.3.0", - "form-data": "^3.0.0", - "mocha": "^7.2.0", - "p-timeout": "^3.2.0", - "parted": "^0.1.1", - "promise": "^8.1.0", - "resumer": "0.0.0", - "rollup": "^2.10.8", - "string-to-arraybuffer": "^1.0.2", - "xo": "^0.30.0" - }, - "dependencies": { - "data-uri-to-buffer": "^3.0.0", - "fetch-blob": "^1.0.6" - }, - "esm": { - "sourceMap": true - }, - "xo": { - "envs": [ - "node", - "browser" - ], - "rules": { - "complexity": 0, - "promise/prefer-await-to-then": 0, - "no-mixed-operators": 0, - "no-negated-condition": 0, - "unicorn/prevent-abbreviations": 0, - "@typescript-eslint/prefer-readonly-parameter-types": 0, - "import/extensions": 0, - "import/no-useless-path-segments": 0, - "unicorn/import-index": 0, - "capitalized-comments": 0 - }, - "ignores": [ - "dist", - "index.d.ts" - ], - "overrides": [ - { - "files": "test/**/*.js", - "envs": [ - "node", - "mocha" - ], - "rules": { - "max-nested-callbacks": 0, - "no-unused-expressions": 0, - "new-cap": 0, - "guard-for-in": 0 - } - }, - { - "files": "example.js", - "rules": { - "import/no-extraneous-dependencies": 0 - } - } - ] - }, - "runkitExampleFilename": "example.js" + "name": "node-fetch", + "version": "3.0.0-beta.5", + "description": "A light-weight module that brings window.fetch to node.js", + "main": "./dist/index.cjs", + "module": "./src/index.js", + "sideEffects": false, + "type": "module", + "exports": { + "import": "./src/index.js", + "require": "./dist/index.cjs" + }, + "files": [ + "src", + "dist", + "@types/index.d.ts" + ], + "types": "./@types/index.d.ts", + "engines": { + "node": ">=10.16" + }, + "scripts": { + "build": "rollup -c", + "test": "node --experimental-modules node_modules/c8/bin/c8 --reporter=html --reporter=lcov --reporter=text --check-coverage node --experimental-modules node_modules/mocha/bin/mocha", + "coverage": "c8 report --reporter=text-lcov | coveralls", + "test-types": "tsd", + "lint": "xo" + }, + "repository": { + "type": "git", + "url": "https://github.com/node-fetch/node-fetch.git" + }, + "keywords": [ + "fetch", + "http", + "promise" + ], + "author": "David Frank", + "license": "MIT", + "bugs": { + "url": "https://github.com/node-fetch/node-fetch/issues" + }, + "homepage": "https://github.com/node-fetch/node-fetch", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + }, + "devDependencies": { + "abort-controller": "^3.0.0", + "abortcontroller-polyfill": "^1.4.0", + "c8": "^7.1.2", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "chai-iterator": "^3.0.2", + "chai-string": "^1.5.0", + "coveralls": "^3.1.0", + "delay": "^4.3.0", + "form-data": "^3.0.0", + "mocha": "^7.1.2", + "p-timeout": "^3.2.0", + "parted": "^0.1.1", + "promise": "^8.1.0", + "resumer": "0.0.0", + "rollup": "^2.10.8", + "string-to-arraybuffer": "^1.0.2", + "tsc": "^1.20150623.0", + "tsd": "^0.11.0", + "xo": "^0.30.0" + }, + "dependencies": { + "data-uri-to-buffer": "^3.0.0", + "fetch-blob": "^1.0.6" + }, + "tsd": { + "cwd": "@types", + "compilerOptions": { + "target": "esnext", + "lib": [ + "es2018" + ], + "allowSyntheticDefaultImports": true + } + }, + "xo": { + "envs": [ + "node", + "browser" + ], + "rules": { + "complexity": 0, + "promise/prefer-await-to-then": 0, + "no-mixed-operators": 0, + "no-negated-condition": 0, + "unicorn/prevent-abbreviations": 0, + "@typescript-eslint/prefer-readonly-parameter-types": 0, + "import/extensions": 0, + "import/no-useless-path-segments": 0, + "unicorn/import-index": 0, + "capitalized-comments": 0 + }, + "ignores": [ + "dist", + "@types" + ], + "overrides": [ + { + "files": "test/**/*.js", + "envs": [ + "node", + "mocha" + ], + "rules": { + "max-nested-callbacks": 0, + "no-unused-expressions": 0, + "new-cap": 0, + "guard-for-in": 0 + } + }, + { + "files": "example.js", + "rules": { + "import/no-extraneous-dependencies": 0 + } + } + ] + }, + "runkitExampleFilename": "example.js" }