Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[node] remove lib: "dom"; add global Event and EventTarget, fix other global types #59905

Merged
merged 16 commits into from Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 17 additions & 2 deletions types/node/buffer.d.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -157,13 +158,15 @@ declare module 'buffer' {
*/
text(): Promise<string>;
/**
* 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;

import { Blob as _Blob } from 'buffer';
global {
// Buffer class
type BufferEncoding = 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'base64url' | 'latin1' | 'binary' | 'hex';
Expand Down Expand Up @@ -2231,6 +2234,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 _Blob;
}
}
declare module 'node:buffer' {
Expand Down
160 changes: 160 additions & 0 deletions types/node/events.d.ts
Expand Up @@ -639,3 +639,163 @@ 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' {
peterblazejewicz marked this conversation as resolved.
Show resolved Hide resolved
/** 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: 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
/** 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;
}

// 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;

// var EventTarget:
// typeof globalThis extends { onmessage: any, EventTarget: infer EventTarget }
// ? EventTarget
// : typeof _EventTarget;
// }
}
6 changes: 6 additions & 0 deletions types/node/globals.d.ts
Expand Up @@ -58,6 +58,12 @@ 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.)

/**
* Returns true if this AbortSignal's AbortController has signaled to abort, and false otherwise.
*/
Expand Down
15 changes: 15 additions & 0 deletions types/node/perf_hooks.d.ts
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions types/node/stream.d.ts
Expand Up @@ -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';
import * as streamWeb from 'node:stream/web';
Expand Down
16 changes: 2 additions & 14 deletions 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<ArrayBuffer>;
slice(start?: number, end?: number, contentType?: string): Blob;
stream(): NodeJS.ReadableStream;
text(): Promise<string>;
}
declare module 'stream/consumers' {
import { Blob as NodeBlob } from "node:buffer";
rbuckton marked this conversation as resolved.
Show resolved Hide resolved
import { Readable } from 'node:stream';
function buffer(stream: NodeJS.ReadableStream | Readable | AsyncIterator<any>): Promise<Buffer>;
function text(stream: NodeJS.ReadableStream | Readable | AsyncIterator<any>): Promise<string>;
function arrayBuffer(stream: NodeJS.ReadableStream | Readable | AsyncIterator<any>): Promise<ArrayBuffer>;
function blob(stream: NodeJS.ReadableStream | Readable | AsyncIterator<any>): Promise<Blob>;
function blob(stream: NodeJS.ReadableStream | Readable | AsyncIterator<any>): Promise<NodeBlob>;
function json(stream: NodeJS.ReadableStream | Readable | AsyncIterator<any>): Promise<unknown>;
}
declare module 'node:stream/consumers' {
Expand Down
25 changes: 17 additions & 8 deletions 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';

Expand Down Expand Up @@ -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',
});
Expand All @@ -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
};

{
Expand Down Expand Up @@ -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([''])));
}

{
Expand Down
2 changes: 2 additions & 0 deletions types/node/test/crypto.ts
Expand Up @@ -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
peterblazejewicz marked this conversation as resolved.
Show resolved Hide resolved
const length = 123;

subtle.encrypt({ name: 'AES-CBC', iv: new Uint8Array(16) }, key, new TextEncoder().encode('hello')); // $ExpectType Promise<ArrayBuffer>
subtle.decrypt({ name: 'AES-CBC', iv: new Uint8Array(16) }, key, new ArrayBuffer(8)); // $ExpectType Promise<ArrayBuffer>
Expand Down
4 changes: 3 additions & 1 deletion types/node/test/events.ts
@@ -1,4 +1,4 @@
import events = require('node:events');
import * as events from 'node:events';

const emitter: events = new events.EventEmitter();
declare const listener: (...args: any[]) => void;
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions types/node/test/perf_hooks.ts
@@ -1,5 +1,5 @@
import {
performance,
performance as NodePerf,
monitorEventLoopDelay,
PerformanceObserverCallback,
PerformanceObserver,
Expand All @@ -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');

Expand Down