diff --git a/package.json b/package.json index d8ce9ef0d..6b560cd90 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "create-test-server": "^3.0.1", "del-cli": "^3.0.0", "delay": "^4.3.0", - "eslint-config-xo-typescript": "^0.19.0", + "eslint-config-xo-typescript": "^0.21.0", "form-data": "^3.0.0", "get-port": "^5.0.0", "keyv": "^3.1.0", @@ -115,9 +115,7 @@ ], "rules": { "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/promise-function-async": "off", - "@typescript-eslint/strict-boolean-expressions": "off", - "@typescript-eslint/no-unnecessary-condition": "off" + "@typescript-eslint/promise-function-async": "off" }, "ignores": [ "documentation/examples/*" diff --git a/source/as-promise.ts b/source/as-promise.ts index e965cca8b..68be175c7 100644 --- a/source/as-promise.ts +++ b/source/as-promise.ts @@ -11,7 +11,7 @@ import requestAsEventEmitter from './request-as-event-emitter'; type ResponseReturn = Response | Buffer | string | any; -export const isProxiedSymbol = Symbol('proxied'); +export const isProxiedSymbol: unique symbol = Symbol('proxied'); export default function asPromise(options: NormalizedOptions): CancelableRequest { const proxy = new EventEmitter(); @@ -56,8 +56,8 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest return; } - if (response.req && response.req.aborted) { - // Canceled while downloading - will throw a CancelError or TimeoutError + if (response.req?.aborted) { + // Canceled while downloading - will throw a `CancelError` or `TimeoutError` error return; } @@ -123,14 +123,19 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest }); emitter.once('error', reject); - [ + + const events = [ 'request', 'redirect', 'uploadProgress', 'downloadProgress' - ].forEach(event => emitter.on(event, (...args: unknown[]) => { - proxy.emit(event, ...args); - })); + ]; + + for (const event of events) { + emitter.on(event, (...args: unknown[]) => { + proxy.emit(event, ...args); + }); + } }) as CancelableRequest; promise[isProxiedSymbol] = true; diff --git a/source/as-stream.ts b/source/as-stream.ts index 38cd94780..b03f69f8c 100644 --- a/source/as-stream.ts +++ b/source/as-stream.ts @@ -82,8 +82,8 @@ export default function asStream(options: NormalizedOptions): ProxyStream { for (const [key, value] of Object.entries(response.headers)) { // Got gives *decompressed* data. Overriding `content-encoding` header would result in an error. // It's not possible to decompress already decompressed data, is it? - const allowed = options.decompress ? key !== 'content-encoding' : true; - if (allowed) { + const isAllowed = options.decompress ? key !== 'content-encoding' : true; + if (isAllowed) { destination.setHeader(key, value); } } diff --git a/source/calculate-retry-delay.ts b/source/calculate-retry-delay.ts index 9fc2ecdd4..7f4bf3874 100644 --- a/source/calculate-retry-delay.ts +++ b/source/calculate-retry-delay.ts @@ -16,6 +16,7 @@ const calculateRetryDelay: RetryFunction = ({attemptCount, retryOptions, error}) return 0; } + // TODO: This type coercion is not entirely correct as it makes `response` a guaranteed property, when it's in fact not. const {response} = error as HTTPError | ParseError | MaxRedirectsError; if (response && Reflect.has(response.headers, 'retry-after') && retryAfterStatusCodes.has(response.statusCode)) { let after = Number(response.headers['retry-after']); @@ -32,7 +33,7 @@ const calculateRetryDelay: RetryFunction = ({attemptCount, retryOptions, error}) return after; } - if (response && response.statusCode === 413) { + if (response?.statusCode === 413) { return 0; } diff --git a/source/create.ts b/source/create.ts index 55687f6e3..8ca80ebf0 100644 --- a/source/create.ts +++ b/source/create.ts @@ -18,10 +18,16 @@ import asStream, {ProxyStream} from './as-stream'; import {preNormalizeArguments, normalizeArguments} from './normalize-arguments'; import {Hooks} from './known-hook-events'; -export type HTTPAlias = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete'; - -export type ReturnResponse = (url: URLArgument | Options & { stream?: false; url: URLArgument }, options?: Options & { stream?: false }) => CancelableRequest; -export type ReturnStream = (url: URLArgument | Options & { stream: true; url: URLArgument }, options?: Options & { stream: true }) => ProxyStream; +export type HTTPAlias = + | 'get' + | 'post' + | 'put' + | 'patch' + | 'head' + | 'delete'; + +export type ReturnResponse = (url: URLArgument | Options & {stream?: false; url: URLArgument}, options?: Options & {stream?: false}) => CancelableRequest; +export type ReturnStream = (url: URLArgument | Options & {stream: true; url: URLArgument}, options?: Options & {stream: true}) => ProxyStream; export type GotReturn = ProxyStream | CancelableRequest; const getPromiseOrStream = (options: NormalizedOptions): GotReturn => options.stream ? asStream(options) : asPromise(options); @@ -40,13 +46,13 @@ export interface Got extends Record { TimeoutError: typeof errors.TimeoutError; CancelError: typeof errors.CancelError; - (url: URLArgument | Options & { stream?: false; url: URLArgument }, options?: Options & { stream?: false }): CancelableRequest; - (url: URLArgument | Options & { stream: true; url: URLArgument }, options?: Options & { stream: true }): ProxyStream; + (url: URLArgument | Options & {stream?: false; url: URLArgument}, options?: Options & {stream?: false}): CancelableRequest; + (url: URLArgument | Options & {stream: true; url: URLArgument}, options?: Options & {stream: true}): ProxyStream; (url: URLOrOptions, options?: Options): CancelableRequest | ProxyStream; create(defaults: Defaults): Got; extend(...instancesOrOptions: Array): Got; mergeInstances(parent: Got, ...instances: Got[]): Got; - mergeOptions(...sources: T[]): T & { hooks: Partial }; + mergeOptions(...sources: T[]): T & {hooks: Partial}; } export interface GotStream extends Record { @@ -76,7 +82,7 @@ const create = (nonNormalizedDefaults: Defaults): Got => { // @ts-ignore Because the for loop handles it for us, as well as the other Object.defines const got: Got = (url: URLOrOptions, options?: Options): GotReturn => { - const isStream = options && options.stream; + const isStream = options?.stream ?? false; let iteration = 0; const iterateHandlers = (newOptions: NormalizedOptions): GotReturn => { @@ -97,9 +103,7 @@ const create = (nonNormalizedDefaults: Defaults): Got => { if (!isStream && !Reflect.has(result, isProxiedSymbol)) { for (const key of Object.keys(nextPromise)) { Object.defineProperty(result, key, { - get: () => { - return nextPromise[key]; - }, + get: () => nextPromise[key], set: (value: unknown) => { nextPromise[key] = value; } diff --git a/source/errors.ts b/source/errors.ts index 483ecdae1..68248c1b0 100644 --- a/source/errors.ts +++ b/source/errors.ts @@ -6,8 +6,7 @@ import {TimeoutError as TimedOutError} from './utils/timed-out'; export class GotError extends Error { code?: string; - - readonly options!: NormalizedOptions; + readonly options: NormalizedOptions; constructor(message: string, error: (Error & {code?: string}) | {code?: string}, options: NormalizedOptions) { super(message); @@ -46,7 +45,7 @@ export class ReadError extends GotError { } export class ParseError extends GotError { - readonly response!: Response; + readonly response: Response; constructor(error: Error, response: Response, options: NormalizedOptions) { super(`${error.message} in "${format(options as unknown as URL)}"`, error, options); @@ -59,7 +58,7 @@ export class ParseError extends GotError { } export class HTTPError extends GotError { - readonly response!: Response; + readonly response: Response; constructor(response: Response, options: NormalizedOptions) { const {statusCode, statusMessage} = response; @@ -74,7 +73,7 @@ export class HTTPError extends GotError { } export class MaxRedirectsError extends GotError { - readonly response!: Response; + readonly response: Response; constructor(response: Response, maxRedirects: number, options: NormalizedOptions) { super(`Redirected ${maxRedirects} times. Aborting.`, {}, options); @@ -95,7 +94,6 @@ export class UnsupportedProtocolError extends GotError { export class TimeoutError extends GotError { timings: Timings; - event: string; constructor(error: TimedOutError, timings: Timings, options: NormalizedOptions) { diff --git a/source/get-response.ts b/source/get-response.ts index 3c0ef515c..57707963d 100644 --- a/source/get-response.ts +++ b/source/get-response.ts @@ -19,7 +19,7 @@ export default (response: IncomingMessage, options: NormalizedOptions, emitter: options.method !== 'HEAD' ? decompressResponse(progressStream as unknown as IncomingMessage) : progressStream ) as Response; - if (!options.decompress && ['gzip', 'deflate', 'br'].includes(response.headers['content-encoding'] || '')) { + if (!options.decompress && ['gzip', 'deflate', 'br'].includes(response.headers['content-encoding'] ?? '')) { options.encoding = null; } diff --git a/source/merge.ts b/source/merge.ts index 048bf65f5..df6fb4d7f 100644 --- a/source/merge.ts +++ b/source/merge.ts @@ -67,34 +67,36 @@ export function mergeOptions(...sources: Array>): Partial.+?):(?.+)/.exec(options.path); - if (matches && matches.groups) { + if (matches?.groups) { const {socketPath, path} = matches.groups; options = { ...options, diff --git a/source/progress.ts b/source/progress.ts index 08b6f70ca..8c98db75e 100644 --- a/source/progress.ts +++ b/source/progress.ts @@ -4,19 +4,19 @@ import {Socket} from 'net'; import EventEmitter = require('events'); export function downloadProgress(_response: IncomingMessage, emitter: EventEmitter, downloadBodySize?: number): TransformStream { - let downloaded = 0; + let downloadedBytes = 0; return new TransformStream({ transform(chunk, _encoding, callback) { - downloaded += chunk.length; + downloadedBytes += chunk.length; - const percent = downloadBodySize ? downloaded / downloadBodySize : 0; + const percent = downloadBodySize ? downloadedBytes / downloadBodySize : 0; // Let `flush()` be responsible for emitting the last event if (percent < 1) { emitter.emit('downloadProgress', { percent, - transferred: downloaded, + transferred: downloadedBytes, total: downloadBodySize }); } @@ -27,7 +27,7 @@ export function downloadProgress(_response: IncomingMessage, emitter: EventEmitt flush(callback) { emitter.emit('downloadProgress', { percent: 1, - transferred: downloaded, + transferred: downloadedBytes, total: downloadBodySize }); @@ -38,7 +38,7 @@ export function downloadProgress(_response: IncomingMessage, emitter: EventEmitt export function uploadProgress(request: ClientRequest, emitter: EventEmitter, uploadBodySize?: number): void { const uploadEventFrequency = 150; - let uploaded = 0; + let uploadedBytes = 0; let progressInterval: NodeJS.Timeout; emitter.emit('uploadProgress', { @@ -56,7 +56,7 @@ export function uploadProgress(request: ClientRequest, emitter: EventEmitter, up emitter.emit('uploadProgress', { percent: 1, - transferred: uploaded, + transferred: uploadedBytes, total: uploadBodySize }); }); @@ -64,21 +64,21 @@ export function uploadProgress(request: ClientRequest, emitter: EventEmitter, up request.once('socket', (socket: Socket) => { const onSocketConnect = (): void => { progressInterval = setInterval(() => { - const lastUploaded = uploaded; + const lastUploadedBytes = uploadedBytes; /* istanbul ignore next: see #490 (occurs randomly!) */ const headersSize = (request as any)._header ? Buffer.byteLength((request as any)._header) : 0; - uploaded = socket.bytesWritten - headersSize; + uploadedBytes = socket.bytesWritten - headersSize; // Don't emit events with unchanged progress and // prevent last event from being emitted, because // it's emitted when `response` is emitted - if (uploaded === lastUploaded || uploaded === uploadBodySize) { + if (uploadedBytes === lastUploadedBytes || uploadedBytes === uploadBodySize) { return; } emitter.emit('uploadProgress', { - percent: uploadBodySize ? uploaded / uploadBodySize : 0, - transferred: uploaded, + percent: uploadBodySize ? uploadedBytes / uploadBodySize : 0, + transferred: uploadedBytes, total: uploadBodySize }); }, uploadEventFrequency); diff --git a/source/request-as-event-emitter.ts b/source/request-as-event-emitter.ts index 2c07ef715..aa572e5cc 100644 --- a/source/request-as-event-emitter.ts +++ b/source/request-as-event-emitter.ts @@ -30,17 +30,17 @@ export interface RequestAsEventEmitter extends EventEmitter { export default (options: NormalizedOptions, input?: TransformStream) => { const emitter = new EventEmitter() as RequestAsEventEmitter; - const redirects = [] as string[]; + const redirects: string[] = []; let currentRequest: http.ClientRequest; let requestUrl: string; - let redirectString: string; + let redirectString: string | undefined; let uploadBodySize: number | undefined; let retryCount = 0; let shouldAbort = false; - const setCookie = options.cookieJar ? promisify(options.cookieJar.setCookie.bind(options.cookieJar)) : null; - const getCookieString = options.cookieJar ? promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null; - const agents = is.object(options.agent) ? options.agent : null; + const setCookie = options.cookieJar && promisify(options.cookieJar.setCookie.bind(options.cookieJar)); + const getCookieString = options.cookieJar && promisify(options.cookieJar.getCookieString.bind(options.cookieJar)); + const agents = is.object(options.agent) && options.agent; const emitError = async (error: Error): Promise => { try { @@ -56,12 +56,13 @@ export default (options: NormalizedOptions, input?: TransformStream) => { }; const get = async (options: NormalizedOptions): Promise => { - const currentUrl = redirectString || requestUrl; + const currentUrl = redirectString ?? requestUrl; if (options.protocol !== 'http:' && options.protocol !== 'https:') { throw new UnsupportedProtocolError(options); } + // Validate the URL decodeURI(currentUrl); let requestFn: RequestFunction; @@ -73,15 +74,15 @@ export default (options: NormalizedOptions, input?: TransformStream) => { if (agents) { const protocolName = options.protocol === 'https:' ? 'https' : 'http'; - options.agent = (agents as AgentByProtocol)[protocolName] || options.agent; + options.agent = (agents as AgentByProtocol)[protocolName] ?? options.agent; } /* istanbul ignore next: electron.net is broken */ // No point in typing process.versions correctly, as - // process.version.electron is used only once, right here. + // `process.version.electron` is used only once, right here. if (options.useElectronNet && (process.versions as any).electron) { - const electron = dynamicRequire(module, 'electron'); // Trick webpack - requestFn = (electron as any).net.request || (electron as any).remote.net.request; + const electron = dynamicRequire(module, 'electron') as any; // Trick Webpack + requestFn = electron.net.request ?? electron.remote.net.request; } if (options.cookieJar) { @@ -113,6 +114,7 @@ export default (options: NormalizedOptions, input?: TransformStream) => { const {statusCode} = response; const typedResponse = response as Response; + // This is intentionally using `||` over `??` so it can also catch empty status message. typedResponse.statusMessage = typedResponse.statusMessage || http.STATUS_CODES[statusCode]; typedResponse.url = currentUrl; typedResponse.requestUrl = requestUrl; @@ -120,11 +122,11 @@ export default (options: NormalizedOptions, input?: TransformStream) => { typedResponse.timings = timings; typedResponse.redirectUrls = redirects; typedResponse.request = {options}; - typedResponse.isFromCache = typedResponse.fromCache || false; + typedResponse.isFromCache = typedResponse.fromCache ?? false; delete typedResponse.fromCache; if (!typedResponse.isFromCache) { - // @ts-ignore Node typings haven't been updated yet + // @ts-ignore Node.js typings haven't been updated yet typedResponse.ip = response.socket.remoteAddress; } @@ -225,7 +227,7 @@ export default (options: NormalizedOptions, input?: TransformStream) => { } // No need to attach an error handler here, - // as `stream.pipeline(...)` doesn't remove this handler + // as `stream.pipeline(…)` doesn't remove this handler // to allow stream reuse. request.emit('upload-complete'); @@ -248,8 +250,9 @@ export default (options: NormalizedOptions, input?: TransformStream) => { const {body} = options; delete options.body; - // `stream.pipeline(...)` does it for us. + // `stream.pipeline(…)` handles `error` for us. request.removeListener('error', onError); + stream.pipeline( body, request, @@ -292,7 +295,7 @@ export default (options: NormalizedOptions, input?: TransformStream) => { cacheRequest.once('request', handleRequest); } else { - // Catches errors thrown by calling requestFn(...) + // Catches errors thrown by calling `requestFn(…)` try { // @ts-ignore TS complains that URLSearchParams is not the same as URLSearchParams handleRequest(requestFn(options as unknown as URL, handleResponse)); @@ -376,7 +379,7 @@ export default (options: NormalizedOptions, input?: TransformStream) => { if (is.object(body) && isFormData(body)) { // Special case for https://github.com/form-data/form-data - headers['content-type'] = headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`; + headers['content-type'] = headers['content-type'] ?? `multipart/form-data; boundary=${body.getBoundary()}`; } else if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body)) { throw new TypeError('The `body` option must be a stream.Readable, string or Buffer'); } @@ -385,10 +388,10 @@ export default (options: NormalizedOptions, input?: TransformStream) => { throw new TypeError('The `form` option must be an Object'); } - headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded'; + headers['content-type'] = headers['content-type'] ?? 'application/x-www-form-urlencoded'; options.body = (new URLSearchParams(options.form as Record)).toString(); } else if (isJSON) { - headers['content-type'] = headers['content-type'] || 'application/json'; + headers['content-type'] = headers['content-type'] ?? 'application/json'; options.body = JSON.stringify(options.json); } @@ -410,7 +413,7 @@ export default (options: NormalizedOptions, input?: TransformStream) => { options.headers.accept = 'application/json'; } - requestUrl = options.href || (new URL(options.path, format(options as UrlObject))).toString(); + requestUrl = options.href ?? (new URL(options.path, format(options as UrlObject))).toString(); await get(options); } catch (error) { diff --git a/source/utils/dynamic-require.ts b/source/utils/dynamic-require.ts index 11887dfc8..515f71995 100644 --- a/source/utils/dynamic-require.ts +++ b/source/utils/dynamic-require.ts @@ -1,5 +1,3 @@ /* istanbul ignore file: used for webpack */ -export default (moduleObject: NodeModule, moduleId: string): unknown => { - return moduleObject.require(moduleId); -}; +export default (moduleObject: NodeModule, moduleId: string): unknown => moduleObject.require(moduleId); diff --git a/source/utils/timed-out.ts b/source/utils/timed-out.ts index 5d05d78d2..27701e676 100644 --- a/source/utils/timed-out.ts +++ b/source/utils/timed-out.ts @@ -3,7 +3,7 @@ import {ClientRequest, IncomingMessage} from 'http'; import {Delays} from './types'; import unhandler from './unhandle'; -const reentry = Symbol('reentry'); +const reentry: unique symbol = Symbol('reentry'); const noop = (): void => {}; interface TimedOutOptions { @@ -43,10 +43,7 @@ export default (request: ClientRequest, delays: Delays, options: TimedOutOptions immediate.unref(); }, delay); - /* istanbul ignore next: in order to support electron renderer */ - if (timeout.unref) { - timeout.unref(); - } + timeout.unref?.(); const cancel = (): void => { clearTimeout(timeout); @@ -103,12 +100,14 @@ export default (request: ClientRequest, delays: Delays, options: TimedOutOptions } once(request, 'socket', (socket: net.Socket): void => { - // TODO: There seems to not be a 'socketPath' on the request, but there IS a socket.remoteAddress + // TODO: There seems to not be a `socketPath` on the request, but there *is* a `socket.remoteAddress`. const {socketPath} = request as any; /* istanbul ignore next: hard to test */ if (socket.connecting) { - if (typeof delays.lookup !== 'undefined' && !socketPath && !net.isIP(hostname || host || '') && typeof (socket.address() as net.AddressInfo).address === 'undefined') { + const hasPath = Boolean(socketPath ?? net.isIP(hostname ?? host ?? '') !== 0); + + if (typeof delays.lookup !== 'undefined' && !hasPath && typeof (socket.address() as net.AddressInfo).address === 'undefined') { const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup'); once(socket, 'lookup', cancelTimeout); } @@ -116,7 +115,7 @@ export default (request: ClientRequest, delays: Delays, options: TimedOutOptions if (typeof delays.connect !== 'undefined') { const timeConnect = (): (() => void) => addTimeout(delays.connect, timeoutHandler, 'connect'); - if (socketPath || net.isIP(hostname || host || '')) { + if (hasPath) { once(socket, 'connect', timeConnect()); } else { once(socket, 'lookup', (error: Error): void => { diff --git a/source/utils/validate-search-params.ts b/source/utils/validate-search-params.ts index f02f9c3bd..561d703c6 100644 --- a/source/utils/validate-search-params.ts +++ b/source/utils/validate-search-params.ts @@ -1,6 +1,6 @@ import is from '@sindresorhus/is'; -export default (searchParams: Record): void => { +export default (searchParams: Record): asserts searchParams is Record => { for (const value of Object.values(searchParams)) { if (!is.string(value) && !is.number(value) && !is.boolean(value) && !is.null_(value)) { throw new TypeError(`The \`searchParams\` value '${value}' must be a string, number, boolean or null`); diff --git a/test/cookies.ts b/test/cookies.ts index 71a305f23..4773df871 100644 --- a/test/cookies.ts +++ b/test/cookies.ts @@ -50,7 +50,7 @@ test('cookies doesn\'t break on redirects', withServer, async (t, server, got) = }); server.get('/', (request, response) => { - response.end(request.headers.cookie || ''); + response.end(request.headers.cookie ?? ''); }); const cookieJar = new toughCookie.CookieJar(); @@ -108,7 +108,7 @@ test('overrides options.headers.cookie', withServer, async (t, server, got) => { }); server.get('/', (request, response) => { - response.end(request.headers.cookie || ''); + response.end(request.headers.cookie ?? ''); }); const cookieJar = new toughCookie.CookieJar(); diff --git a/test/stream.ts b/test/stream.ts index 88add5b28..848d92f12 100644 --- a/test/stream.ts +++ b/test/stream.ts @@ -113,7 +113,7 @@ test('has error event', withServer, async (t, server, got) => { }); test('has error event #2', withServer, async (t, _server, got) => { - const stream = got.stream('http://doesntexist', {prefixUrl: null}); + const stream = got.stream('http://doesntexist', {prefixUrl: ''}); await t.throwsAsync(pEvent(stream, 'response'), {code: 'ENOTFOUND'}); });