From d9fef110f853ac728430af7d9ca3894f2718f22b Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Thu, 14 Apr 2022 18:27:58 +0200 Subject: [PATCH 01/12] [node] Fix Blob definition in consumers --- types/node/buffer.d.ts | 5 +++-- types/node/stream/consumers.d.ts | 16 ++-------------- types/node/v16/buffer.d.ts | 5 +++-- types/node/v16/stream/consumers.d.ts | 16 ++-------------- 4 files changed, 10 insertions(+), 32 deletions(-) diff --git a/types/node/buffer.d.ts b/types/node/buffer.d.ts index 18be30d2e9e446..5c631eb02837ae 100644 --- a/types/node/buffer.d.ts +++ b/types/node/buffer.d.ts @@ -45,6 +45,7 @@ */ declare module 'buffer' { import { BinaryLike } from 'node:crypto'; + import { ReadableStream as WebReadableStream } from 'node:stream/web'; export const INSPECT_MAX_BYTES: number; export const kMaxLength: number; export const kStringMaxLength: number; @@ -158,10 +159,10 @@ declare module 'buffer' { */ text(): Promise; /** - * Returns a new `ReadableStream` that allows the content of the `Blob` to be read. + * Returns a new (WHATWG) `ReadableStream` that allows the content of the `Blob` to be read. * @since v16.7.0 */ - stream(): unknown; // pending web streams types + stream(): WebReadableStream; } export import atob = globalThis.atob; export import btoa = globalThis.btoa; diff --git a/types/node/stream/consumers.d.ts b/types/node/stream/consumers.d.ts index ce6c9bb7852f27..1ebf12e1fa741b 100644 --- a/types/node/stream/consumers.d.ts +++ b/types/node/stream/consumers.d.ts @@ -1,22 +1,10 @@ -// Duplicates of interface in lib.dom.ts. -// Duplicated here rather than referencing lib.dom.ts because doing so causes lib.dom.ts to be loaded for "test-all" -// Which in turn causes tests to pass that shouldn't pass. -// -// This interface is not, and should not be, exported. -interface Blob { - readonly size: number; - readonly type: string; - arrayBuffer(): Promise; - slice(start?: number, end?: number, contentType?: string): Blob; - stream(): NodeJS.ReadableStream; - text(): Promise; -} declare module 'stream/consumers' { + import { Blob as NodeBlob } from "node:buffer"; import { Readable } from 'node:stream'; function buffer(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; function text(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; function arrayBuffer(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; - function blob(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; + function blob(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; function json(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; } declare module 'node:stream/consumers' { diff --git a/types/node/v16/buffer.d.ts b/types/node/v16/buffer.d.ts index f3f44eebec58cd..1d9dd98e5a4fe1 100644 --- a/types/node/v16/buffer.d.ts +++ b/types/node/v16/buffer.d.ts @@ -45,6 +45,7 @@ */ declare module 'buffer' { import { BinaryLike } from 'node:crypto'; + import { ReadableStream as WebReadableStream } from 'node:stream/web'; export const INSPECT_MAX_BYTES: number; export const kMaxLength: number; export const kStringMaxLength: number; @@ -158,10 +159,10 @@ declare module 'buffer' { */ text(): Promise; /** - * Returns a new `ReadableStream` that allows the content of the `Blob` to be read. + * Returns a new (WHATWG) `ReadableStream` that allows the content of the `Blob` to be read. * @since v16.7.0 */ - stream(): unknown; // pending web streams types + stream(): WebReadableStream; } export import atob = globalThis.atob; export import btoa = globalThis.btoa; diff --git a/types/node/v16/stream/consumers.d.ts b/types/node/v16/stream/consumers.d.ts index ce6c9bb7852f27..1667c24cee72d2 100644 --- a/types/node/v16/stream/consumers.d.ts +++ b/types/node/v16/stream/consumers.d.ts @@ -1,22 +1,10 @@ -// Duplicates of interface in lib.dom.ts. -// Duplicated here rather than referencing lib.dom.ts because doing so causes lib.dom.ts to be loaded for "test-all" -// Which in turn causes tests to pass that shouldn't pass. -// -// This interface is not, and should not be, exported. -interface Blob { - readonly size: number; - readonly type: string; - arrayBuffer(): Promise; - slice(start?: number, end?: number, contentType?: string): Blob; - stream(): NodeJS.ReadableStream; - text(): Promise; -} declare module 'stream/consumers' { import { Readable } from 'node:stream'; + import { Blob as NodeBlob } from "node:buffer"; function buffer(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; function text(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; function arrayBuffer(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; - function blob(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; + function blob(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; function json(stream: NodeJS.ReadableStream | Readable | AsyncIterator): Promise; } declare module 'node:stream/consumers' { From 715df24add7bc95495ff6bfad7c501f3b5059571 Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:39:42 +0200 Subject: [PATCH 02/12] [node] Remove "dom" lib ref, add DOM-like Event Remove global WebAssembly refs from tests, not defined yet Also, test all exports from stream/consumers --- types/node/events.d.ts | 137 ++++++++++++++++++++++++++++++++++++++ types/node/stream.d.ts | 1 + types/node/test/events.ts | 3 +- types/node/test/stream.ts | 23 +++---- types/node/test/wasi.ts | 9 ++- types/node/tsconfig.json | 2 +- 6 files changed, 158 insertions(+), 17 deletions(-) diff --git a/types/node/events.d.ts b/types/node/events.d.ts index c1cef439f84251..3f9bb974c95897 100644 --- a/types/node/events.d.ts +++ b/types/node/events.d.ts @@ -649,3 +649,140 @@ declare module 'node:events' { import events = require('events'); export = events; } + +// NB: The Event / EventTarget / EventListener implementations below were copied +// from lib.dom.d.ts, then edited to reflect Node's documentation at +// https://nodejs.org/api/events.html#class-eventtarget. +// Please read that link to understand important implementation differences. + +// For now, these Events are contained in a temporary module to avoid conflicts +// with their DOM versions in projects that include both `lib.dom.d.ts` and `@types/node` +declare module 'node:dom-events' { + /** An event which takes place in the DOM. */ + interface Event { + /** This is not used in Node.js and is provided purely for completeness. */ + readonly bubbles: false; + /** Alias for event.stopPropagation(). This is not used in Node.js and is provided purely for completeness. */ + cancelBubble(): void; + /** True if the event was created with the cancelable option */ + readonly cancelable: boolean; + /** This is not used in Node.js and is provided purely for completeness. */ + readonly composed: boolean; + /** Returns an array containing the current EventTarget as the only entry or empty if the event is not being dispatched. This is not used in Node.js and is provided purely for completeness. */ + composedPath(): [EventTarget?]; + /** Alias for event.target. */ + readonly currentTarget: EventTarget; + /** Is true if cancelable is true and event.preventDefault() has been called. */ + readonly defaultPrevented: boolean; + /** This is not used in Node.js and is provided purely for completeness. */ + readonly eventPhase: 0 | 2; + /** The `AbortSignal` "abort" event is emitted with `isTrusted` set to `true`. The value is `false` in all other cases. */ + readonly isTrusted: boolean; + /** Sets the `defaultPrevented` property to `true` if `cancelable` is `true`. */ + preventDefault(): void; + /** This is not used in Node.js and is provided purely for completeness. */ + returnValue: boolean; + /** Alias for event.target. */ + readonly srcElement: EventTarget | null; + /** Stops the invocation of event listeners after the current one completes. */ + stopImmediatePropagation(): void; + /** This is not used in Node.js and is provided purely for completeness. */ + stopPropagation(): void; + /** The `EventTarget` dispatching the event */ + readonly target: EventTarget | null; + /** The millisecond timestamp when the Event object was created. */ + readonly timeStamp: number; + /** Returns the type of event, e.g. "click", "hashchange", or "submit". */ + readonly type: string; + } + const Event: { + prototype: Event; + new (type: string, eventInitDict?: EventInit): Event; + }; + + /** EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them. */ + interface EventTarget { + /** + * Adds a new handler for the `type` event. Any given `listener` is added only once per `type` and per `capture` option value. + * + * If the `once` option is true, the `listener` is removed after the next time a `type` event is dispatched. + * + * The `capture` option is not used by Node.js in any functional way other than tracking registered event listeners per the `EventTarget` specification. + * Specifically, the `capture` option is used as part of the key when registering a `listener`. + * Any individual `listener` may be added once with `capture = false`, and once with `capture = true`. + */ + addEventListener( + type: string, + listener: EventListener | EventListenerObject, + options?: AddEventListenerOptions | boolean, + ): void; + /** Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise. */ + dispatchEvent(event: Event): boolean; + /** Removes the event listener in target's event listener list with the same type, callback, and options. */ + removeEventListener( + type: string, + listener: EventListener | EventListenerObject, + options?: EventListenerOptions | boolean, + ): void; + } + const EventTarget: { + prototype: EventTarget; + new (): EventTarget; + }; + + /** The NodeEventTarget is a Node.js-specific extension to EventTarget that emulates a subset of the EventEmitter API. */ + interface NodeEventTarget extends EventTarget { + /** + * Node.js-specific extension to the `EventTarget` class that emulates the equivalent `EventEmitter` API. + * The only difference between `addListener()` and `addEventListener()` is that addListener() will return a reference to the EventTarget. + */ + addListener(type: string, listener: EventListener | EventListenerObject, options?: { once: boolean }): this; + /** Node.js-specific extension to the `EventTarget` class that returns an array of event `type` names for which event listeners are registered. */ + eventNames(): string[]; + /** Node.js-specific extension to the `EventTarget` class that returns the number of event listeners registered for the `type`. */ + listenerCount(type: string): number; + /** Node.js-specific alias for `eventTarget.removeListener()`. */ + off(type: string, listener: EventListener | EventListenerObject): this; + /** Node.js-specific alias for `eventTarget.addListener()`. */ + on(type: string, listener: EventListener | EventListenerObject, options?: { once: boolean }): this; + /** Node.js-specific extension to the `EventTarget` class that adds a `once` listener for the given event `type`. This is equivalent to calling `on` with the `once` option set to `true`. */ + once(type: string, listener: EventListener | EventListenerObject): this; + /** + * Node.js-specific extension to the `EventTarget` class. + * If `type` is specified, removes all registered listeners for `type`, + * otherwise removes all registered listeners. + */ + removeAllListeners(type: string): this; + /** + * Node.js-specific extension to the `EventTarget` class that removes the listener for the given `type`. + * The only difference between `removeListener()` and `removeEventListener()` is that `removeListener()` will return a reference to the `EventTarget`. + */ + removeListener(type: string, listener: EventListener | EventListenerObject): this; + } + + interface EventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + } + + interface EventListenerOptions { + /** Not directly used by Node.js. Added for API completeness. Default: `false`. */ + capture?: boolean; + } + + interface AddEventListenerOptions extends EventListenerOptions { + /** When `true`, the listener is automatically removed when it is first invoked. Default: `false`. */ + once?: boolean; + /** When `true`, serves as a hint that the listener will not call the `Event` object's `preventDefault()` method. Default: false. */ + passive?: boolean; + } + + interface EventListener { + (evt: Event): void; + } + + interface EventListenerObject { + handleEvent(object: Event): void; + } +} diff --git a/types/node/stream.d.ts b/types/node/stream.d.ts index 7edc7bfa8ddace..ab59adf3adf54d 100644 --- a/types/node/stream.d.ts +++ b/types/node/stream.d.ts @@ -18,6 +18,7 @@ */ declare module 'stream' { import { EventEmitter, Abortable } from 'node:events'; + import { Blob } from "node:buffer"; import * as streamPromises from 'node:stream/promises'; import * as streamConsumers from 'node:stream/consumers'; class internal extends EventEmitter { diff --git a/types/node/test/events.ts b/types/node/test/events.ts index 6b502d59299fa0..2757adc75518f3 100644 --- a/types/node/test/events.ts +++ b/types/node/test/events.ts @@ -1,4 +1,5 @@ -import events = require('node:events'); +import * as events from 'node:events'; +import { EventTarget } from 'node:dom-events'; const emitter: events = new events.EventEmitter(); declare const listener: (...args: any[]) => void; diff --git a/types/node/test/stream.ts b/types/node/test/stream.ts index c394d6b9bd4091..84278e9e57c730 100644 --- a/types/node/test/stream.ts +++ b/types/node/test/stream.ts @@ -4,12 +4,17 @@ import { createReadStream, createWriteStream } from 'node:fs'; import { createGzip, constants } from 'node:zlib'; import assert = require('node:assert'); import { Http2ServerResponse } from 'node:http2'; -import { text, json, buffer } from 'node:stream/consumers'; +import { text, json, buffer, arrayBuffer, blob } from 'node:stream/consumers'; import { pipeline as pipelinePromise } from 'node:stream/promises'; import { stdout } from 'node:process'; import { ReadableStream, WritableStream, TransformStream } from 'node:stream/web'; import { setInterval as every } from 'node:timers/promises'; import { MessageChannel } from 'node:worker_threads'; +import { performance } from 'node:perf_hooks'; + +// Ensure there is no global Blob type +// $ExpectError +type ShouldFail = Blob; // Simplified constructors function simplified_stream_ctor_test() { @@ -458,25 +463,19 @@ async function streamPipelineAsyncPromiseAbortTransform() { }); } -async function readableToString() { +async function testConsumers() { const r = createReadStream('file.txt'); // $ExpectType string await text(r); -} - -async function readableToJson() { - const r = createReadStream('file.txt'); - // $ExpectType unknown await json(r); -} - -async function readableToBuffer() { - const r = createReadStream('file.txt'); - // $ExpectType Buffer await buffer(r); + // $ExpectType ArrayBuffer + await arrayBuffer(r); + // $ExpectType Blob + await blob(r); } // https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options diff --git a/types/node/test/wasi.ts b/types/node/test/wasi.ts index e8583c9ab69d19..2af5dbb298f161 100644 --- a/types/node/test/wasi.ts +++ b/types/node/test/wasi.ts @@ -1,5 +1,5 @@ import { WASI } from 'node:wasi'; -import * as fs from 'node:fs'; +// import * as fs from 'node:fs'; { const wasi = new WASI({ @@ -12,8 +12,11 @@ import * as fs from 'node:fs'; const importObject = { wasi_snapshot_preview1: wasi.wasiImport }; (async () => { - const wasm = await WebAssembly.compile(fs.readFileSync('./demo.wasm')); - const instance = await WebAssembly.instantiate(wasm, importObject); + // TODO: Global WebAssembly types are not currently declared.; uncomment below when added. + + // const wasm = await WebAssembly.compile(fs.readFileSync('./demo.wasm')); + // const instance = await WebAssembly.instantiate(wasm, importObject); + const instance = {}; wasi.start(instance); })(); diff --git a/types/node/tsconfig.json b/types/node/tsconfig.json index d33175c96ce3cf..a1530719006190 100644 --- a/types/node/tsconfig.json +++ b/types/node/tsconfig.json @@ -7,7 +7,7 @@ "module": "commonjs", "target": "esnext", "lib": [ - "dom" + "es6" ], "noImplicitAny": true, "noImplicitThis": true, From 783e9dd4b0b1d7a6cdb68a85780240faf0b067fb Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Fri, 15 Apr 2022 16:44:08 +0200 Subject: [PATCH 03/12] Conditionally expose global DOM EventTarget --- types/node/events.d.ts | 18 ++++++++++++++++++ types/node/globals.d.ts | 2 +- types/node/test/events.ts | 1 - 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/types/node/events.d.ts b/types/node/events.d.ts index 3f9bb974c95897..ed55a1e392d3bf 100644 --- a/types/node/events.d.ts +++ b/types/node/events.d.ts @@ -785,4 +785,22 @@ declare module 'node:dom-events' { interface EventListenerObject { handleEvent(object: Event): void; } + + import { Event as _Event, EventTarget as _EventTarget } from 'node:dom-events'; + global { + interface Event extends _Event {} + interface EventTarget extends _EventTarget {} + + // For compatibility with "dom" and "webworker" Event / EventTarget declarations + + var Event: + typeof globalThis extends { onmessage: any, Event: infer Event } + ? Event + : typeof _Event; + + var EventTarget: + typeof globalThis extends { onmessage: any, EventTarget: infer EventTarget } + ? EventTarget + : typeof _EventTarget; + } } diff --git a/types/node/globals.d.ts b/types/node/globals.d.ts index 4533f1cbd7d15f..5b93bee4d55033 100644 --- a/types/node/globals.d.ts +++ b/types/node/globals.d.ts @@ -57,7 +57,7 @@ interface AbortController { } /** A signal object that allows you to communicate with a DOM request (such as a Fetch) and abort it if required via an AbortController object. */ -interface AbortSignal { +interface AbortSignal extends EventTarget { /** * Returns true if this AbortSignal's AbortController has signaled to abort, and false otherwise. */ diff --git a/types/node/test/events.ts b/types/node/test/events.ts index 2757adc75518f3..80884ace1844cc 100644 --- a/types/node/test/events.ts +++ b/types/node/test/events.ts @@ -1,5 +1,4 @@ import * as events from 'node:events'; -import { EventTarget } from 'node:dom-events'; const emitter: events = new events.EventEmitter(); declare const listener: (...args: any[]) => void; From 2770063adfef1b93e8a927934344d6e2825bf6c9 Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Mon, 25 Apr 2022 11:36:57 +0200 Subject: [PATCH 04/12] [node] Take out top level Event declaration Conditional declaration isn't working in TS 4.7 --- types/node/events.d.ts | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/types/node/events.d.ts b/types/node/events.d.ts index ed55a1e392d3bf..26e489088a4c8c 100644 --- a/types/node/events.d.ts +++ b/types/node/events.d.ts @@ -786,21 +786,26 @@ declare module 'node:dom-events' { handleEvent(object: Event): void; } - import { Event as _Event, EventTarget as _EventTarget } from 'node:dom-events'; - global { - interface Event extends _Event {} - interface EventTarget extends _EventTarget {} + // TODO: Event should be a top-level type, but it will conflict with the + // Event in lib.dom.d.ts without the conditional declaration below. It + // works in TS < 4.7, but breaks in the latest release. This can go back + // in once we figure out why. - // For compatibility with "dom" and "webworker" Event / EventTarget declarations + // import { Event as _Event, EventTarget as _EventTarget } from 'node:dom-events'; + // global { + // interface Event extends _Event {} + // interface EventTarget extends _EventTarget {} - var Event: - typeof globalThis extends { onmessage: any, Event: infer Event } - ? Event - : typeof _Event; + // // For compatibility with "dom" and "webworker" Event / EventTarget declarations - var EventTarget: - typeof globalThis extends { onmessage: any, EventTarget: infer EventTarget } - ? EventTarget - : typeof _EventTarget; - } + // var Event: + // typeof globalThis extends { onmessage: any, Event: infer Event } + // ? Event + // : typeof _Event; + + // var EventTarget: + // typeof globalThis extends { onmessage: any, EventTarget: infer EventTarget } + // ? EventTarget + // : typeof _EventTarget; + // } } From 81b61c98ad4d78d80435a7d2e1a674d7ef6db175 Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Mon, 25 Apr 2022 16:43:56 +0200 Subject: [PATCH 05/12] Reconcile Node Event with DOM version --- types/node/events.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/node/events.d.ts b/types/node/events.d.ts index 26e489088a4c8c..430d3bf289c5b1 100644 --- a/types/node/events.d.ts +++ b/types/node/events.d.ts @@ -661,9 +661,9 @@ declare module 'node:dom-events' { /** An event which takes place in the DOM. */ interface Event { /** This is not used in Node.js and is provided purely for completeness. */ - readonly bubbles: false; + readonly bubbles: boolean; /** Alias for event.stopPropagation(). This is not used in Node.js and is provided purely for completeness. */ - cancelBubble(): void; + cancelBubble: unknown; // Should be () => void but would conflict with DOM /** True if the event was created with the cancelable option */ readonly cancelable: boolean; /** This is not used in Node.js and is provided purely for completeness. */ From 00417c04de2845bdcca6a83d88b4c5a6a4925bad Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Tue, 26 Apr 2022 09:03:16 +0200 Subject: [PATCH 06/12] Missed one reference --- types/node/globals.d.ts | 8 +++++++- types/node/test/events.ts | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/types/node/globals.d.ts b/types/node/globals.d.ts index 5b93bee4d55033..8b73e1cb465aca 100644 --- a/types/node/globals.d.ts +++ b/types/node/globals.d.ts @@ -57,7 +57,13 @@ interface AbortController { } /** A signal object that allows you to communicate with a DOM request (such as a Fetch) and abort it if required via an AbortController object. */ -interface AbortSignal extends EventTarget { +interface AbortSignal { +// interface AbortSignal extends EventTarget { + // TODO: see comment in `events.d.ts` -- when EventTarget is exposed globally, + // use the line above instead, since AbortSignal is an EventTarget. + // (Importing the type from `node:dom-events` would require making this file + // non-ambient, which is a hassle if we're just going to change it back later.) + /** * Returns true if this AbortSignal's AbortController has signaled to abort, and false otherwise. */ diff --git a/types/node/test/events.ts b/types/node/test/events.ts index 80884ace1844cc..c4ab9611d820ff 100644 --- a/types/node/test/events.ts +++ b/types/node/test/events.ts @@ -115,6 +115,8 @@ async function test() { captureRejectionSymbol2 = events.captureRejectionSymbol; } +// TODO: remove once global Event works (see events.d.ts) +import { EventTarget } from "node:dom-events"; { events.EventEmitter.setMaxListeners(); events.EventEmitter.setMaxListeners(42); From 9060bc08cba24c7400c3ed9e4a8953628e778960 Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Mon, 1 Aug 2022 17:40:27 +0200 Subject: [PATCH 07/12] Bring tests up to date with latest master branch Expose global Blob, added in v18 Expose global TextEncoder, since we're not getting it from lib-dom --- types/node/buffer.d.ts | 12 ++++++++++++ types/node/test/buffer.ts | 25 +++++++++++++++++-------- types/node/test/crypto.ts | 2 ++ types/node/test/stream.ts | 4 ---- types/node/test/url.ts | 4 ++-- types/node/url.d.ts | 4 ++-- types/node/util.d.ts | 9 +++++++++ 7 files changed, 44 insertions(+), 16 deletions(-) diff --git a/types/node/buffer.d.ts b/types/node/buffer.d.ts index de3531b488530d..a29ef8070757e1 100644 --- a/types/node/buffer.d.ts +++ b/types/node/buffer.d.ts @@ -2232,6 +2232,18 @@ declare module 'buffer' { * @param data An ASCII (Latin1) string. */ function btoa(data: string): string; + + /** + * `Blob` class is a global reference for `require('node:buffer').Blob` + * https://nodejs.org/api/buffer.html#class-blob + * @since v18.0.0 + */ + var Blob: typeof globalThis extends { + onmessage: any; + Blob: infer Blob; + } + ? Blob + : typeof import('node:buffer').Blob; } } declare module 'node:buffer' { diff --git a/types/node/test/buffer.ts b/types/node/test/buffer.ts index aecf8076c6c0f2..6710d4a9f0318d 100644 --- a/types/node/test/buffer.ts +++ b/types/node/test/buffer.ts @@ -1,14 +1,15 @@ // Specifically test buffer module regression. import { + Blob as NodeBlob, + Blob, Buffer as ImportedBuffer, - SlowBuffer as ImportedSlowBuffer, - transcode, - TranscodeEncoding, constants, kMaxLength, kStringMaxLength, - Blob, resolveObjectURL, + SlowBuffer as ImportedSlowBuffer, + transcode, + TranscodeEncoding, } from 'node:buffer'; import { Readable, Writable } from 'node:stream'; @@ -283,7 +284,7 @@ b.fill('a').fill('b'); } async () => { - const blob = new Blob(['asd', Buffer.from('test'), new Blob(['dummy'])], { + const blob = new NodeBlob(['asd', Buffer.from('test'), new NodeBlob(['dummy'])], { type: 'application/javascript', encoding: 'base64', }); @@ -297,6 +298,15 @@ async () => { blob.slice(1); // $ExpectType Blob blob.slice(1, 2); // $ExpectType Blob blob.slice(1, 2, 'other'); // $ExpectType Blob + // ExpectType does not support disambiguating interfaces that have the same + // name but wildly different implementations, like Node native ReadableStream + // vs W3C ReadableStream, so we have to look at properties. + blob.stream().locked; // $ExpectType boolean + + // As above but for global-scoped Blob, which should be an alias for NodeBlob + // as long as `lib-dom` is not included. + const blob2 = new Blob([]); + blob2.stream().locked; // $ExpectType boolean }; { @@ -409,9 +419,8 @@ buff.writeDoubleBE(123.123); buff.writeDoubleBE(123.123, 0); { - // The 'as any' is to make sure the Global DOM Blob does not clash with the - // local "Blob" which comes with node. - resolveObjectURL(URL.createObjectURL(new Blob(['']) as any)); // $ExpectType Blob | undefined + // $ExpectType Blob | undefined + resolveObjectURL(URL.createObjectURL(new NodeBlob(['']))); } { diff --git a/types/node/test/crypto.ts b/types/node/test/crypto.ts index 0b0e06cf53c1ce..e84de411f2807e 100644 --- a/types/node/test/crypto.ts +++ b/types/node/test/crypto.ts @@ -1389,6 +1389,8 @@ import { promisify } from 'node:util'; // The lack of top level await makes it annoying to use generateKey so let's just fake it for typings. const key = null as unknown as crypto.webcrypto.CryptoKey; const buf = new Uint8Array(16); + // Oops, test relied on DOM `globalThis.length` before + const length = 123; subtle.encrypt({ name: 'AES-CBC', iv: new Uint8Array(16) }, key, new TextEncoder().encode('hello')); // $ExpectType Promise subtle.decrypt({ name: 'AES-CBC', iv: new Uint8Array(16) }, key, new ArrayBuffer(8)); // $ExpectType Promise diff --git a/types/node/test/stream.ts b/types/node/test/stream.ts index e0c3a89c2b43d0..46e85af079481f 100644 --- a/types/node/test/stream.ts +++ b/types/node/test/stream.ts @@ -12,10 +12,6 @@ import { setInterval as every } from 'node:timers/promises'; import { MessageChannel } from 'node:worker_threads'; import { performance } from 'node:perf_hooks'; -// Ensure there is no global Blob type -// $ExpectError -type ShouldFail = Blob; - // Simplified constructors function simplified_stream_ctor_test() { new Readable({ diff --git a/types/node/test/url.ts b/types/node/test/url.ts index 28d519386eb19a..db8a89c7a8a532 100644 --- a/types/node/test/url.ts +++ b/types/node/test/url.ts @@ -1,4 +1,4 @@ -import { Blob } from 'node:buffer'; +import { Blob as NodeBlob } from 'node:buffer'; import assert = require('node:assert'); import { RequestOptions } from 'node:http'; import * as url from 'node:url'; @@ -165,7 +165,7 @@ import * as url from 'node:url'; const opts: RequestOptions = url.urlToHttpOptions(new url.URL('test.com')); } { - const dataUrl: string = url.URL.createObjectURL(new Blob([''])); + const dataUrl: string = url.URL.createObjectURL(new NodeBlob([''])); } { const dataUrl1: URL = new url.URL('file://test'); diff --git a/types/node/url.d.ts b/types/node/url.d.ts index 18362c8aa03c7d..4461c53a8d87e1 100644 --- a/types/node/url.d.ts +++ b/types/node/url.d.ts @@ -8,7 +8,7 @@ * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/url.js) */ declare module 'url' { - import { Blob } from 'node:buffer'; + import { Blob as NodeBlob } from 'node:buffer'; import { ClientRequestArgs } from 'node:http'; import { ParsedUrlQuery, ParsedUrlQueryInput } from 'node:querystring'; // Input to `url.format` @@ -395,7 +395,7 @@ declare module 'url' { * @since v16.7.0 * @experimental */ - static createObjectURL(blob: Blob): string; + static createObjectURL(blob: NodeBlob): string; /** * Removes the stored `Blob` identified by the given ID. Attempting to revoke a * ID that isn’t registered will silently fail. diff --git a/types/node/util.d.ts b/types/node/util.d.ts index 88f2a54151f010..af7d495da0c53d 100644 --- a/types/node/util.d.ts +++ b/types/node/util.d.ts @@ -1105,6 +1105,15 @@ declare module 'util' { */ encodeInto(src: string, dest: Uint8Array): EncodeIntoResult; } + + global { + /** + * `TextEncoder` class is a global reference for `require('util').TextEncoder` + * https://nodejs.org/api/globals.html#textencoder + * @since v11.0.0 + */ + const TextEncoder: typeof import('util').TextEncoder; + } } declare module 'util/types' { export * from 'util/types'; From 70a166fe015ce6effe6fe7570aca04af5abf323b Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Tue, 2 Aug 2022 10:15:17 +0200 Subject: [PATCH 08/12] Expose global constructor aliases --- types/node/buffer.d.ts | 4 ++- types/node/perf_hooks.d.ts | 15 +++++++++++ types/node/test/perf_hooks.ts | 5 ++-- types/node/test/stream.ts | 11 ++++++-- types/node/test/util.ts | 7 +++++ types/node/test/worker_threads.ts | 9 +++++++ types/node/util.d.ts | 20 +++++++++++++- types/node/worker_threads.d.ts | 43 +++++++++++++++++++++++++++++++ 8 files changed, 108 insertions(+), 6 deletions(-) diff --git a/types/node/buffer.d.ts b/types/node/buffer.d.ts index a29ef8070757e1..35cdf954f3939c 100644 --- a/types/node/buffer.d.ts +++ b/types/node/buffer.d.ts @@ -165,6 +165,8 @@ declare module 'buffer' { } export import atob = globalThis.atob; export import btoa = globalThis.btoa; + + import { Blob as _Blob } from 'buffer'; global { // Buffer class type BufferEncoding = 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'base64url' | 'latin1' | 'binary' | 'hex'; @@ -2243,7 +2245,7 @@ declare module 'buffer' { Blob: infer Blob; } ? Blob - : typeof import('node:buffer').Blob; + : typeof _Blob; } } declare module 'node:buffer' { diff --git a/types/node/perf_hooks.d.ts b/types/node/perf_hooks.d.ts index 6c956c1eb116f1..044d09f31031f0 100644 --- a/types/node/perf_hooks.d.ts +++ b/types/node/perf_hooks.d.ts @@ -565,6 +565,21 @@ declare module 'perf_hooks' { * @since v15.9.0, v14.18.0 */ function createHistogram(options?: CreateHistogramOptions): RecordableHistogram; + + import { performance as _performance } from 'perf_hooks'; + global { + /** + * `performance` is a global reference for `require('perf_hooks').performance` + * https://nodejs.org/api/globals.html#performance + * @since v16.0.0 + */ + var performance: typeof globalThis extends { + onmessage: any; + performance: infer performance; + } + ? performance + : typeof _performance; + } } declare module 'node:perf_hooks' { export * from 'perf_hooks'; diff --git a/types/node/test/perf_hooks.ts b/types/node/test/perf_hooks.ts index 09d7f1faf346c8..758f28ebce9f72 100644 --- a/types/node/test/perf_hooks.ts +++ b/types/node/test/perf_hooks.ts @@ -1,5 +1,5 @@ import { - performance, + performance as NodePerf, monitorEventLoopDelay, PerformanceObserverCallback, PerformanceObserver, @@ -13,7 +13,8 @@ import { NodeGCPerformanceDetail, } from 'node:perf_hooks'; -performance.mark('start'); +// Test module import once, the rest use global +NodePerf.mark('start'); (() => {})(); performance.mark('end'); diff --git a/types/node/test/stream.ts b/types/node/test/stream.ts index 46e85af079481f..a5614ea2e74620 100644 --- a/types/node/test/stream.ts +++ b/types/node/test/stream.ts @@ -9,7 +9,7 @@ import { pipeline as pipelinePromise } from 'node:stream/promises'; import { stdout } from 'node:process'; import { ReadableStream, WritableStream, TransformStream } from 'node:stream/web'; import { setInterval as every } from 'node:timers/promises'; -import { MessageChannel } from 'node:worker_threads'; +import { MessageChannel as NodeMC } from 'node:worker_threads'; import { performance } from 'node:perf_hooks'; // Simplified constructors @@ -626,7 +626,14 @@ async function testTransformStream() { // https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html#transferring-with-postmessage_2 async function testTransferringStreamWithPostMessage() { const stream = new TransformStream(); - const {port1, port2} = new MessageChannel(); + { + // Global constructor + const {port1, port2} = new MessageChannel(); + } + { + // Constructor from module + const {port1, port2} = new NodeMC(); + } // error: TypeError: port1.postMessage is not a function // port1.onmessage = ({data}) => { diff --git a/types/node/test/util.ts b/types/node/test/util.ts index cd2cbfe4e08e20..a40b849a30e4d1 100644 --- a/types/node/test/util.ts +++ b/types/node/test/util.ts @@ -153,6 +153,10 @@ const td = new util.TextDecoder(); new util.TextDecoder("utf-8"); new util.TextDecoder("utf-8", { fatal: true }); new util.TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + +// Test global alias +const td2 = new TextDecoder(); + const ignoreBom: boolean = td.ignoreBOM; const fatal: boolean = td.fatal; const encoding: string = td.encoding; @@ -177,6 +181,9 @@ const te = new util.TextEncoder(); const teEncoding: string = te.encoding; const teEncodeRes: Uint8Array = te.encode("TextEncoder"); +// Test global alias +const te2 = new TextEncoder(); + const encIntoRes: util.EncodeIntoResult = te.encodeInto('asdf', new Uint8Array(16)); const errorMap: Map = util.getSystemErrorMap(); diff --git a/types/node/test/worker_threads.ts b/types/node/test/worker_threads.ts index 2531ce01f8b53e..c622b24b7fdc33 100644 --- a/types/node/test/worker_threads.ts +++ b/types/node/test/worker_threads.ts @@ -123,6 +123,9 @@ import { EventLoopUtilization } from 'node:perf_hooks'; bc.unref(); bc.onmessage = (msg: unknown) => { }; bc.onmessageerror = (msg: unknown) => { }; + + // Test global alias + const bc2 = new BroadcastChannel('test'); } { @@ -131,3 +134,9 @@ import { EventLoopUtilization } from 'node:perf_hooks'; workerThreads.getEnvironmentData('test'); // $ExpectType Serializable workerThreads.getEnvironmentData(1); // $ExpectType Serializable } + +{ + // Test module constructor, then global alias + const mp1 = new workerThreads.MessagePort(); + const mp2 = new MessagePort(); +} diff --git a/types/node/util.d.ts b/types/node/util.d.ts index af7d495da0c53d..ca4762436d4aee 100644 --- a/types/node/util.d.ts +++ b/types/node/util.d.ts @@ -1106,13 +1106,31 @@ declare module 'util' { encodeInto(src: string, dest: Uint8Array): EncodeIntoResult; } + import { TextDecoder as _TextDecoder, TextEncoder as _TextEncoder } from 'util'; global { + /** + * `TextDecoder` class is a global reference for `require('util').TextDecoder` + * https://nodejs.org/api/globals.html#textdecoder + * @since v11.0.0 + */ + var TextDecoder: typeof globalThis extends { + onmessage: any; + TextDecoder: infer TextDecoder; + } + ? TextDecoder + : typeof _TextDecoder; + /** * `TextEncoder` class is a global reference for `require('util').TextEncoder` * https://nodejs.org/api/globals.html#textencoder * @since v11.0.0 */ - const TextEncoder: typeof import('util').TextEncoder; + var TextEncoder: typeof globalThis extends { + onmessage: any; + TextEncoder: infer TextEncoder; + } + ? TextEncoder + : typeof _TextEncoder; } } declare module 'util/types' { diff --git a/types/node/worker_threads.d.ts b/types/node/worker_threads.d.ts index 792a3bded3a050..e0ec3ee10b2258 100644 --- a/types/node/worker_threads.d.ts +++ b/types/node/worker_threads.d.ts @@ -640,6 +640,49 @@ declare module 'worker_threads' { * for the `key` will be deleted. */ function setEnvironmentData(key: Serializable, value: Serializable): void; + + import { + BroadcastChannel as _BroadcastChannel, + MessageChannel as _MessageChannel, + MessagePort as _MessagePort, + } from 'worker_threads'; + global { + /** + * `BroadcastChannel` class is a global reference for `require('worker_threads').BroadcastChannel` + * https://nodejs.org/api/globals.html#broadcastchannel + * @since v18.0.0 + */ + var BroadcastChannel: typeof globalThis extends { + onmessage: any; + BroadcastChannel: infer BroadcastChannel; + } + ? BroadcastChannel + : typeof _BroadcastChannel; + + /** + * `MessageChannel` class is a global reference for `require('worker_threads').MessageChannel` + * https://nodejs.org/api/globals.html#messagechannel + * @since v15.0.0 + */ + var MessageChannel: typeof globalThis extends { + onmessage: any; + MessageChannel: infer MessageChannel; + } + ? MessageChannel + : typeof _MessageChannel; + + /** + * `MessagePort` class is a global reference for `require('worker_threads').MessagePort` + * https://nodejs.org/api/globals.html#messageport + * @since v15.0.0 + */ + var MessagePort: typeof globalThis extends { + onmessage: any; + MessagePort: infer MessagePort; + } + ? MessagePort + : typeof _MessagePort; + } } declare module 'node:worker_threads' { export * from 'worker_threads'; From 4ef8e7ea59b9395c1082219d13df37c4844ba33c Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Wed, 3 Aug 2022 13:35:21 +0200 Subject: [PATCH 09/12] Expose Event/EventTarget globally Rename fictional module, add note about not importing from it --- types/node/events.d.ts | 57 ++++++++++++++++----------------------- types/node/globals.d.ts | 8 +----- types/node/test/events.ts | 2 -- 3 files changed, 24 insertions(+), 43 deletions(-) diff --git a/types/node/events.d.ts b/types/node/events.d.ts index e3b7890479e409..6c1d3513bb6cde 100644 --- a/types/node/events.d.ts +++ b/types/node/events.d.ts @@ -646,8 +646,9 @@ declare module 'node:events' { // Please read that link to understand important implementation differences. // For now, these Events are contained in a temporary module to avoid conflicts -// with their DOM versions in projects that include both `lib.dom.d.ts` and `@types/node` -declare module 'node:dom-events' { +// with their DOM versions in projects that include both `lib.dom.d.ts` and `@types/node`. +// Library consumers should *not* import from this fictional module! +declare module '__dom-events' { /** An event which takes place in the DOM. */ interface Event { /** This is not used in Node.js and is provided purely for completeness. */ @@ -659,13 +660,13 @@ declare module 'node:dom-events' { /** This is not used in Node.js and is provided purely for completeness. */ readonly composed: boolean; /** Returns an array containing the current EventTarget as the only entry or empty if the event is not being dispatched. This is not used in Node.js and is provided purely for completeness. */ - composedPath(): [EventTarget?]; + composedPath(): EventTarget[]; // should be [EventTarget?] /** Alias for event.target. */ - readonly currentTarget: EventTarget; + readonly currentTarget: EventTarget | null; /** Is true if cancelable is true and event.preventDefault() has been called. */ readonly defaultPrevented: boolean; /** This is not used in Node.js and is provided purely for completeness. */ - readonly eventPhase: 0 | 2; + readonly eventPhase: number; // should be 0 | 2 /** The `AbortSignal` "abort" event is emitted with `isTrusted` set to `true`. The value is `false` in all other cases. */ readonly isTrusted: boolean; /** Sets the `defaultPrevented` property to `true` if `cancelable` is `true`. */ @@ -685,10 +686,6 @@ declare module 'node:dom-events' { /** Returns the type of event, e.g. "click", "hashchange", or "submit". */ readonly type: string; } - const Event: { - prototype: Event; - new (type: string, eventInitDict?: EventInit): Event; - }; /** EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them. */ interface EventTarget { @@ -715,10 +712,6 @@ declare module 'node:dom-events' { options?: EventListenerOptions | boolean, ): void; } - const EventTarget: { - prototype: EventTarget; - new (): EventTarget; - }; /** The NodeEventTarget is a Node.js-specific extension to EventTarget that emulates a subset of the EventEmitter API. */ interface NodeEventTarget extends EventTarget { @@ -776,26 +769,22 @@ declare module 'node:dom-events' { handleEvent(object: Event): void; } - // TODO: Event should be a top-level type, but it will conflict with the - // Event in lib.dom.d.ts without the conditional declaration below. It - // works in TS < 4.7, but breaks in the latest release. This can go back - // in once we figure out why. - - // import { Event as _Event, EventTarget as _EventTarget } from 'node:dom-events'; - // global { - // interface Event extends _Event {} - // interface EventTarget extends _EventTarget {} - - // // For compatibility with "dom" and "webworker" Event / EventTarget declarations - - // var Event: - // typeof globalThis extends { onmessage: any, Event: infer Event } - // ? Event - // : typeof _Event; + import { Event as _Event, EventTarget as _EventTarget } from '__dom-events'; + global { + interface Event extends _Event {} + var Event: typeof globalThis extends { onmessage: any, Event: infer Event } + ? Event + : { + prototype: Event; + new (type: string, eventInitDict?: EventInit): Event; + }; - // var EventTarget: - // typeof globalThis extends { onmessage: any, EventTarget: infer EventTarget } - // ? EventTarget - // : typeof _EventTarget; - // } + interface EventTarget extends _EventTarget {} + var EventTarget: typeof globalThis extends { onmessage: any, EventTarget: infer EventTarget } + ? EventTarget + : { + prototype: EventTarget; + new (): EventTarget; + }; + } } diff --git a/types/node/globals.d.ts b/types/node/globals.d.ts index 9d05468edc319b..f401d95a9e8825 100644 --- a/types/node/globals.d.ts +++ b/types/node/globals.d.ts @@ -57,13 +57,7 @@ interface AbortController { } /** A signal object that allows you to communicate with a DOM request (such as a Fetch) and abort it if required via an AbortController object. */ -interface AbortSignal { -// interface AbortSignal extends EventTarget { - // TODO: see comment in `events.d.ts` -- when EventTarget is exposed globally, - // use the line above instead, since AbortSignal is an EventTarget. - // (Importing the type from `node:dom-events` would require making this file - // non-ambient, which is a hassle if we're just going to change it back later.) - +interface AbortSignal extends EventTarget { /** * Returns true if this AbortSignal's AbortController has signaled to abort, and false otherwise. */ diff --git a/types/node/test/events.ts b/types/node/test/events.ts index c4ab9611d820ff..80884ace1844cc 100644 --- a/types/node/test/events.ts +++ b/types/node/test/events.ts @@ -115,8 +115,6 @@ async function test() { captureRejectionSymbol2 = events.captureRejectionSymbol; } -// TODO: remove once global Event works (see events.d.ts) -import { EventTarget } from "node:dom-events"; { events.EventEmitter.setMaxListeners(); events.EventEmitter.setMaxListeners(42); From 683256dc3f111b1b630ae4653d99da5fd8599115 Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Wed, 3 Aug 2022 14:03:51 +0200 Subject: [PATCH 10/12] Clarify NodeEventTarget --- types/node/events.d.ts | 79 +++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/types/node/events.d.ts b/types/node/events.d.ts index 6c1d3513bb6cde..afbd4e53febdf7 100644 --- a/types/node/events.d.ts +++ b/types/node/events.d.ts @@ -35,16 +35,53 @@ * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/events.js) */ declare module 'events' { + // NOTE: This class is in the docs but is **not actually exported** by Node. + // If https://github.com/nodejs/node/issues/39903 gets resolved and Node + // actually starts exporting the class, uncomment below. + + // import { EventListener, EventListenerObject } from '__dom-events'; + // /** The NodeEventTarget is a Node.js-specific extension to EventTarget that emulates a subset of the EventEmitter API. */ + // interface NodeEventTarget extends EventTarget { + // /** + // * Node.js-specific extension to the `EventTarget` class that emulates the equivalent `EventEmitter` API. + // * The only difference between `addListener()` and `addEventListener()` is that addListener() will return a reference to the EventTarget. + // */ + // addListener(type: string, listener: EventListener | EventListenerObject, options?: { once: boolean }): this; + // /** Node.js-specific extension to the `EventTarget` class that returns an array of event `type` names for which event listeners are registered. */ + // eventNames(): string[]; + // /** Node.js-specific extension to the `EventTarget` class that returns the number of event listeners registered for the `type`. */ + // listenerCount(type: string): number; + // /** Node.js-specific alias for `eventTarget.removeListener()`. */ + // off(type: string, listener: EventListener | EventListenerObject): this; + // /** Node.js-specific alias for `eventTarget.addListener()`. */ + // on(type: string, listener: EventListener | EventListenerObject, options?: { once: boolean }): this; + // /** Node.js-specific extension to the `EventTarget` class that adds a `once` listener for the given event `type`. This is equivalent to calling `on` with the `once` option set to `true`. */ + // once(type: string, listener: EventListener | EventListenerObject): this; + // /** + // * Node.js-specific extension to the `EventTarget` class. + // * If `type` is specified, removes all registered listeners for `type`, + // * otherwise removes all registered listeners. + // */ + // removeAllListeners(type: string): this; + // /** + // * Node.js-specific extension to the `EventTarget` class that removes the listener for the given `type`. + // * The only difference between `removeListener()` and `removeEventListener()` is that `removeListener()` will return a reference to the `EventTarget`. + // */ + // removeListener(type: string, listener: EventListener | EventListenerObject): this; + // } + interface EventEmitterOptions { /** * Enables automatic capturing of promise rejection. */ captureRejections?: boolean | undefined; } - interface NodeEventTarget { + // Any EventTarget with a Node-style `once` function + interface _NodeEventTarget { once(eventName: string | symbol, listener: (...args: any[]) => void): this; } - interface DOMEventTarget { + // Any EventTarget with a DOM-style `addEventListener` + interface _DOMEventTarget { addEventListener( eventName: string, listener: (...args: any[]) => void, @@ -154,8 +191,8 @@ declare module 'events' { * ``` * @since v11.13.0, v10.16.0 */ - static once(emitter: NodeEventTarget, eventName: string | symbol, options?: StaticEventEmitterOptions): Promise; - static once(emitter: DOMEventTarget, eventName: string, options?: StaticEventEmitterOptions): Promise; + static once(emitter: _NodeEventTarget, eventName: string | symbol, options?: StaticEventEmitterOptions): Promise; + static once(emitter: _DOMEventTarget, eventName: string, options?: StaticEventEmitterOptions): Promise; /** * ```js * const { on, EventEmitter } = require('events'); @@ -259,7 +296,7 @@ declare module 'events' { * ``` * @since v15.2.0, v14.17.0 */ - static getEventListeners(emitter: DOMEventTarget | NodeJS.EventEmitter, name: string | symbol): Function[]; + static getEventListeners(emitter: _DOMEventTarget | NodeJS.EventEmitter, name: string | symbol): Function[]; /** * ```js * const { @@ -277,7 +314,7 @@ declare module 'events' { * @param eventsTargets Zero or more {EventTarget} or {EventEmitter} instances. If none are specified, `n` is set as the default max for all newly created {EventTarget} and {EventEmitter} * objects. */ - static setMaxListeners(n?: number, ...eventTargets: Array): void; + static setMaxListeners(n?: number, ...eventTargets: Array<_DOMEventTarget | NodeJS.EventEmitter>): void; /** * This symbol shall be used to install a listener for only monitoring `'error'` * events. Listeners installed using this symbol are called before the regular @@ -713,36 +750,6 @@ declare module '__dom-events' { ): void; } - /** The NodeEventTarget is a Node.js-specific extension to EventTarget that emulates a subset of the EventEmitter API. */ - interface NodeEventTarget extends EventTarget { - /** - * Node.js-specific extension to the `EventTarget` class that emulates the equivalent `EventEmitter` API. - * The only difference between `addListener()` and `addEventListener()` is that addListener() will return a reference to the EventTarget. - */ - addListener(type: string, listener: EventListener | EventListenerObject, options?: { once: boolean }): this; - /** Node.js-specific extension to the `EventTarget` class that returns an array of event `type` names for which event listeners are registered. */ - eventNames(): string[]; - /** Node.js-specific extension to the `EventTarget` class that returns the number of event listeners registered for the `type`. */ - listenerCount(type: string): number; - /** Node.js-specific alias for `eventTarget.removeListener()`. */ - off(type: string, listener: EventListener | EventListenerObject): this; - /** Node.js-specific alias for `eventTarget.addListener()`. */ - on(type: string, listener: EventListener | EventListenerObject, options?: { once: boolean }): this; - /** Node.js-specific extension to the `EventTarget` class that adds a `once` listener for the given event `type`. This is equivalent to calling `on` with the `once` option set to `true`. */ - once(type: string, listener: EventListener | EventListenerObject): this; - /** - * Node.js-specific extension to the `EventTarget` class. - * If `type` is specified, removes all registered listeners for `type`, - * otherwise removes all registered listeners. - */ - removeAllListeners(type: string): this; - /** - * Node.js-specific extension to the `EventTarget` class that removes the listener for the given `type`. - * The only difference between `removeListener()` and `removeEventListener()` is that `removeListener()` will return a reference to the `EventTarget`. - */ - removeListener(type: string, listener: EventListener | EventListenerObject): this; - } - interface EventInit { bubbles?: boolean; cancelable?: boolean; From e5e6b029f7c5e879fa2567cc601adc41aee20b07 Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Wed, 3 Aug 2022 14:35:27 +0200 Subject: [PATCH 11/12] Conditionally expose type side of Event(Target) --- types/node/events.d.ts | 39 +++++++++++++++++++++------------------ types/node/test/events.ts | 10 ++++++++++ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/types/node/events.d.ts b/types/node/events.d.ts index afbd4e53febdf7..eafe51cd49b217 100644 --- a/types/node/events.d.ts +++ b/types/node/events.d.ts @@ -687,23 +687,25 @@ declare module 'node:events' { // Library consumers should *not* import from this fictional module! declare module '__dom-events' { /** An event which takes place in the DOM. */ - interface Event { + type __Event = typeof globalThis extends { onmessage: any, Event: infer T } + ? T + : { /** This is not used in Node.js and is provided purely for completeness. */ readonly bubbles: boolean; /** Alias for event.stopPropagation(). This is not used in Node.js and is provided purely for completeness. */ - cancelBubble: unknown; // Should be () => void but would conflict with DOM + cancelBubble: () => void; /** True if the event was created with the cancelable option */ readonly cancelable: boolean; /** This is not used in Node.js and is provided purely for completeness. */ readonly composed: boolean; /** Returns an array containing the current EventTarget as the only entry or empty if the event is not being dispatched. This is not used in Node.js and is provided purely for completeness. */ - composedPath(): EventTarget[]; // should be [EventTarget?] + composedPath(): [EventTarget?] /** Alias for event.target. */ readonly currentTarget: EventTarget | null; /** Is true if cancelable is true and event.preventDefault() has been called. */ readonly defaultPrevented: boolean; /** This is not used in Node.js and is provided purely for completeness. */ - readonly eventPhase: number; // should be 0 | 2 + readonly eventPhase: 0 | 2; /** The `AbortSignal` "abort" event is emitted with `isTrusted` set to `true`. The value is `false` in all other cases. */ readonly isTrusted: boolean; /** Sets the `defaultPrevented` property to `true` if `cancelable` is `true`. */ @@ -722,10 +724,12 @@ declare module '__dom-events' { readonly timeStamp: number; /** Returns the type of event, e.g. "click", "hashchange", or "submit". */ readonly type: string; - } + }; /** EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them. */ - interface EventTarget { + type __EventTarget = typeof globalThis extends { onmessage: any, EventTarget: infer T } + ? T + : { /** * Adds a new handler for the `type` event. Any given `listener` is added only once per `type` and per `capture` option value. * @@ -748,7 +752,7 @@ declare module '__dom-events' { listener: EventListener | EventListenerObject, options?: EventListenerOptions | boolean, ): void; - } + }; interface EventInit { bubbles?: boolean; @@ -776,22 +780,21 @@ declare module '__dom-events' { handleEvent(object: Event): void; } - import { Event as _Event, EventTarget as _EventTarget } from '__dom-events'; global { - interface Event extends _Event {} - var Event: typeof globalThis extends { onmessage: any, Event: infer Event } - ? Event + interface Event extends __Event {} + var Event: typeof globalThis extends { onmessage: any, Event: infer T } + ? T : { - prototype: Event; - new (type: string, eventInitDict?: EventInit): Event; + prototype: __Event; + new (type: string, eventInitDict?: EventInit): __Event; }; - interface EventTarget extends _EventTarget {} - var EventTarget: typeof globalThis extends { onmessage: any, EventTarget: infer EventTarget } - ? EventTarget + interface EventTarget extends __EventTarget {} + var EventTarget: typeof globalThis extends { onmessage: any, EventTarget: infer T } + ? T : { - prototype: EventTarget; - new (): EventTarget; + prototype: __EventTarget; + new (): __EventTarget; }; } } diff --git a/types/node/test/events.ts b/types/node/test/events.ts index 80884ace1844cc..907831e4bfb4ee 100644 --- a/types/node/test/events.ts +++ b/types/node/test/events.ts @@ -125,3 +125,13 @@ async function test() { const eventEmitter = new events.EventEmitter(); events.EventEmitter.setMaxListeners(42, eventTarget, eventEmitter); } + +{ + // Some event properties differ from DOM types + const evt = new Event("fake"); + evt.cancelBubble(); + // @ts-expect-error + evt.composedPath[2]; + // $ExpectType 0 | 2 + evt.eventPhase; +} From a84fa7a49e56ab5af199fe1c403640bd4b5f7901 Mon Sep 17 00:00:00 2001 From: James Bromwell <943160+thw0rted@users.noreply.github.com> Date: Wed, 10 Aug 2022 09:05:56 +0200 Subject: [PATCH 12/12] Move DOM Event to its own file Avoid identifier name confusion in conditional types --- types/node/buffer.d.ts | 4 +- types/node/dom-events.d.ts | 126 +++++++++++++++++++++++++++++++++ types/node/events.d.ts | 122 ------------------------------- types/node/index.d.ts | 1 + types/node/perf_hooks.d.ts | 4 +- types/node/url.d.ts | 8 +-- types/node/worker_threads.d.ts | 12 ++-- 7 files changed, 141 insertions(+), 136 deletions(-) create mode 100644 types/node/dom-events.d.ts diff --git a/types/node/buffer.d.ts b/types/node/buffer.d.ts index 35cdf954f3939c..5de02f49ff47f4 100644 --- a/types/node/buffer.d.ts +++ b/types/node/buffer.d.ts @@ -2242,9 +2242,9 @@ declare module 'buffer' { */ var Blob: typeof globalThis extends { onmessage: any; - Blob: infer Blob; + Blob: infer T; } - ? Blob + ? T : typeof _Blob; } } diff --git a/types/node/dom-events.d.ts b/types/node/dom-events.d.ts new file mode 100644 index 00000000000000..16e236fafc900c --- /dev/null +++ b/types/node/dom-events.d.ts @@ -0,0 +1,126 @@ +export {}; // Don't export anything! + +//// DOM-like Events +// NB: The Event / EventTarget / EventListener implementations below were copied +// from lib.dom.d.ts, then edited to reflect Node's documentation at +// https://nodejs.org/api/events.html#class-eventtarget. +// Please read that link to understand important implementation differences. + +// This conditional type will be the existing global Event in a browser, or +// the copy below in a Node environment. +type __Event = typeof globalThis extends { onmessage: any, Event: infer T } +? T +: { + /** This is not used in Node.js and is provided purely for completeness. */ + readonly bubbles: boolean; + /** Alias for event.stopPropagation(). This is not used in Node.js and is provided purely for completeness. */ + cancelBubble: () => void; + /** True if the event was created with the cancelable option */ + readonly cancelable: boolean; + /** This is not used in Node.js and is provided purely for completeness. */ + readonly composed: boolean; + /** Returns an array containing the current EventTarget as the only entry or empty if the event is not being dispatched. This is not used in Node.js and is provided purely for completeness. */ + composedPath(): [EventTarget?] + /** Alias for event.target. */ + readonly currentTarget: EventTarget | null; + /** Is true if cancelable is true and event.preventDefault() has been called. */ + readonly defaultPrevented: boolean; + /** This is not used in Node.js and is provided purely for completeness. */ + readonly eventPhase: 0 | 2; + /** The `AbortSignal` "abort" event is emitted with `isTrusted` set to `true`. The value is `false` in all other cases. */ + readonly isTrusted: boolean; + /** Sets the `defaultPrevented` property to `true` if `cancelable` is `true`. */ + preventDefault(): void; + /** This is not used in Node.js and is provided purely for completeness. */ + returnValue: boolean; + /** Alias for event.target. */ + readonly srcElement: EventTarget | null; + /** Stops the invocation of event listeners after the current one completes. */ + stopImmediatePropagation(): void; + /** This is not used in Node.js and is provided purely for completeness. */ + stopPropagation(): void; + /** The `EventTarget` dispatching the event */ + readonly target: EventTarget | null; + /** The millisecond timestamp when the Event object was created. */ + readonly timeStamp: number; + /** Returns the type of event, e.g. "click", "hashchange", or "submit". */ + readonly type: string; +}; + +// See comment above explaining conditional type +type __EventTarget = typeof globalThis extends { onmessage: any, EventTarget: infer T } +? T +: { + /** + * Adds a new handler for the `type` event. Any given `listener` is added only once per `type` and per `capture` option value. + * + * If the `once` option is true, the `listener` is removed after the next time a `type` event is dispatched. + * + * The `capture` option is not used by Node.js in any functional way other than tracking registered event listeners per the `EventTarget` specification. + * Specifically, the `capture` option is used as part of the key when registering a `listener`. + * Any individual `listener` may be added once with `capture = false`, and once with `capture = true`. + */ + addEventListener( + type: string, + listener: EventListener | EventListenerObject, + options?: AddEventListenerOptions | boolean, + ): void; + /** Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise. */ + dispatchEvent(event: Event): boolean; + /** Removes the event listener in target's event listener list with the same type, callback, and options. */ + removeEventListener( + type: string, + listener: EventListener | EventListenerObject, + options?: EventListenerOptions | boolean, + ): void; +}; + +interface EventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; +} + +interface EventListenerOptions { + /** Not directly used by Node.js. Added for API completeness. Default: `false`. */ + capture?: boolean; +} + +interface AddEventListenerOptions extends EventListenerOptions { + /** When `true`, the listener is automatically removed when it is first invoked. Default: `false`. */ + once?: boolean; + /** When `true`, serves as a hint that the listener will not call the `Event` object's `preventDefault()` method. Default: false. */ + passive?: boolean; +} + +interface EventListener { + (evt: Event): void; +} + +interface EventListenerObject { + handleEvent(object: Event): void; +} + +import {} from 'events'; // Make this an ambient declaration +declare global { + /** An event which takes place in the DOM. */ + interface Event extends __Event {} + var Event: typeof globalThis extends { onmessage: any, Event: infer T } + ? T + : { + prototype: __Event; + new (type: string, eventInitDict?: EventInit): __Event; + }; + + /** + * EventTarget is a DOM interface implemented by objects that can + * receive events and may have listeners for them. + */ + interface EventTarget extends __EventTarget {} + var EventTarget: typeof globalThis extends { onmessage: any, EventTarget: infer T } + ? T + : { + prototype: __EventTarget; + new (): __EventTarget; + }; +} diff --git a/types/node/events.d.ts b/types/node/events.d.ts index eafe51cd49b217..87a0b4f9dc8c8f 100644 --- a/types/node/events.d.ts +++ b/types/node/events.d.ts @@ -676,125 +676,3 @@ declare module 'node:events' { import events = require('events'); export = events; } - -// NB: The Event / EventTarget / EventListener implementations below were copied -// from lib.dom.d.ts, then edited to reflect Node's documentation at -// https://nodejs.org/api/events.html#class-eventtarget. -// Please read that link to understand important implementation differences. - -// For now, these Events are contained in a temporary module to avoid conflicts -// with their DOM versions in projects that include both `lib.dom.d.ts` and `@types/node`. -// Library consumers should *not* import from this fictional module! -declare module '__dom-events' { - /** An event which takes place in the DOM. */ - type __Event = typeof globalThis extends { onmessage: any, Event: infer T } - ? T - : { - /** This is not used in Node.js and is provided purely for completeness. */ - readonly bubbles: boolean; - /** Alias for event.stopPropagation(). This is not used in Node.js and is provided purely for completeness. */ - cancelBubble: () => void; - /** True if the event was created with the cancelable option */ - readonly cancelable: boolean; - /** This is not used in Node.js and is provided purely for completeness. */ - readonly composed: boolean; - /** Returns an array containing the current EventTarget as the only entry or empty if the event is not being dispatched. This is not used in Node.js and is provided purely for completeness. */ - composedPath(): [EventTarget?] - /** Alias for event.target. */ - readonly currentTarget: EventTarget | null; - /** Is true if cancelable is true and event.preventDefault() has been called. */ - readonly defaultPrevented: boolean; - /** This is not used in Node.js and is provided purely for completeness. */ - readonly eventPhase: 0 | 2; - /** The `AbortSignal` "abort" event is emitted with `isTrusted` set to `true`. The value is `false` in all other cases. */ - readonly isTrusted: boolean; - /** Sets the `defaultPrevented` property to `true` if `cancelable` is `true`. */ - preventDefault(): void; - /** This is not used in Node.js and is provided purely for completeness. */ - returnValue: boolean; - /** Alias for event.target. */ - readonly srcElement: EventTarget | null; - /** Stops the invocation of event listeners after the current one completes. */ - stopImmediatePropagation(): void; - /** This is not used in Node.js and is provided purely for completeness. */ - stopPropagation(): void; - /** The `EventTarget` dispatching the event */ - readonly target: EventTarget | null; - /** The millisecond timestamp when the Event object was created. */ - readonly timeStamp: number; - /** Returns the type of event, e.g. "click", "hashchange", or "submit". */ - readonly type: string; - }; - - /** EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them. */ - type __EventTarget = typeof globalThis extends { onmessage: any, EventTarget: infer T } - ? T - : { - /** - * Adds a new handler for the `type` event. Any given `listener` is added only once per `type` and per `capture` option value. - * - * If the `once` option is true, the `listener` is removed after the next time a `type` event is dispatched. - * - * The `capture` option is not used by Node.js in any functional way other than tracking registered event listeners per the `EventTarget` specification. - * Specifically, the `capture` option is used as part of the key when registering a `listener`. - * Any individual `listener` may be added once with `capture = false`, and once with `capture = true`. - */ - addEventListener( - type: string, - listener: EventListener | EventListenerObject, - options?: AddEventListenerOptions | boolean, - ): void; - /** Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise. */ - dispatchEvent(event: Event): boolean; - /** Removes the event listener in target's event listener list with the same type, callback, and options. */ - removeEventListener( - type: string, - listener: EventListener | EventListenerObject, - options?: EventListenerOptions | boolean, - ): void; - }; - - interface EventInit { - bubbles?: boolean; - cancelable?: boolean; - composed?: boolean; - } - - interface EventListenerOptions { - /** Not directly used by Node.js. Added for API completeness. Default: `false`. */ - capture?: boolean; - } - - interface AddEventListenerOptions extends EventListenerOptions { - /** When `true`, the listener is automatically removed when it is first invoked. Default: `false`. */ - once?: boolean; - /** When `true`, serves as a hint that the listener will not call the `Event` object's `preventDefault()` method. Default: false. */ - passive?: boolean; - } - - interface EventListener { - (evt: Event): void; - } - - interface EventListenerObject { - handleEvent(object: Event): void; - } - - global { - interface Event extends __Event {} - var Event: typeof globalThis extends { onmessage: any, Event: infer T } - ? T - : { - prototype: __Event; - new (type: string, eventInitDict?: EventInit): __Event; - }; - - interface EventTarget extends __EventTarget {} - var EventTarget: typeof globalThis extends { onmessage: any, EventTarget: infer T } - ? T - : { - prototype: __EventTarget; - new (): __EventTarget; - }; - } -} diff --git a/types/node/index.d.ts b/types/node/index.d.ts index a6346aaf818efe..8009031d626649 100644 --- a/types/node/index.d.ts +++ b/types/node/index.d.ts @@ -92,6 +92,7 @@ /// /// /// +/// /// /// /// diff --git a/types/node/perf_hooks.d.ts b/types/node/perf_hooks.d.ts index 044d09f31031f0..af5f55d365cbb7 100644 --- a/types/node/perf_hooks.d.ts +++ b/types/node/perf_hooks.d.ts @@ -575,9 +575,9 @@ declare module 'perf_hooks' { */ var performance: typeof globalThis extends { onmessage: any; - performance: infer performance; + performance: infer T; } - ? performance + ? T : typeof _performance; } } diff --git a/types/node/url.d.ts b/types/node/url.d.ts index 4461c53a8d87e1..e172acbf543504 100644 --- a/types/node/url.d.ts +++ b/types/node/url.d.ts @@ -875,9 +875,9 @@ declare module 'url' { */ var URL: typeof globalThis extends { onmessage: any; - URL: infer URL; + URL: infer T; } - ? URL + ? T : typeof _URL; /** * `URLSearchParams` class is a global reference for `require('url').URLSearchParams` @@ -886,9 +886,9 @@ declare module 'url' { */ var URLSearchParams: typeof globalThis extends { onmessage: any; - URLSearchParams: infer URLSearchParams; + URLSearchParams: infer T; } - ? URLSearchParams + ? T : typeof _URLSearchParams; } } diff --git a/types/node/worker_threads.d.ts b/types/node/worker_threads.d.ts index e0ec3ee10b2258..72aabbeb90110e 100644 --- a/types/node/worker_threads.d.ts +++ b/types/node/worker_threads.d.ts @@ -654,9 +654,9 @@ declare module 'worker_threads' { */ var BroadcastChannel: typeof globalThis extends { onmessage: any; - BroadcastChannel: infer BroadcastChannel; + BroadcastChannel: infer T; } - ? BroadcastChannel + ? T : typeof _BroadcastChannel; /** @@ -666,9 +666,9 @@ declare module 'worker_threads' { */ var MessageChannel: typeof globalThis extends { onmessage: any; - MessageChannel: infer MessageChannel; + MessageChannel: infer T; } - ? MessageChannel + ? T : typeof _MessageChannel; /** @@ -678,9 +678,9 @@ declare module 'worker_threads' { */ var MessagePort: typeof globalThis extends { onmessage: any; - MessagePort: infer MessagePort; + MessagePort: infer T; } - ? MessagePort + ? T : typeof _MessagePort; } }