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 all 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 T;
}
? T
: typeof _Blob;
}
}
declare module 'node:buffer' {
Expand Down
126 changes: 126 additions & 0 deletions 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;
};
}
49 changes: 43 additions & 6 deletions types/node/events.d.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -154,8 +191,8 @@ declare module 'events' {
* ```
* @since v11.13.0, v10.16.0
*/
static once(emitter: NodeEventTarget, eventName: string | symbol, options?: StaticEventEmitterOptions): Promise<any[]>;
static once(emitter: DOMEventTarget, eventName: string, options?: StaticEventEmitterOptions): Promise<any[]>;
static once(emitter: _NodeEventTarget, eventName: string | symbol, options?: StaticEventEmitterOptions): Promise<any[]>;
static once(emitter: _DOMEventTarget, eventName: string, options?: StaticEventEmitterOptions): Promise<any[]>;
/**
* ```js
* const { on, EventEmitter } = require('events');
Expand Down Expand Up @@ -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 {
Expand All @@ -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<DOMEventTarget | NodeJS.EventEmitter>): 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
Expand Down
2 changes: 1 addition & 1 deletion types/node/globals.d.ts
Expand Up @@ -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.
*/
Expand Down
1 change: 1 addition & 0 deletions types/node/index.d.ts
Expand Up @@ -92,6 +92,7 @@
/// <reference path="dns/promises.d.ts" />
/// <reference path="dns/promises.d.ts" />
/// <reference path="domain.d.ts" />
/// <reference path="dom-events.d.ts" />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised this passes tests given that dom-events should be parsed as a module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, well, I may be showing a lot of ignorance in this PR but at least I'm learning a lot too.

Is the opposite of "parsed as a module" supposed to be "parsed as a script"? And all the other referenced files parse as a script because at the top level they only declare module 'xyz'? (Or in the case of globals.d.ts, declare types that will be added to the global scope.)

Should I refactor the DOM event declarations to parse as a script? (Is that possible without exporting all the intermediate types?) If not, would it be more idiomatically correct to use a side-effect import statement instead? I was just aping the syntax used to incorporate the other files, not realizing how they were different.

/// <reference path="events.d.ts" />
/// <reference path="fs.d.ts" />
/// <reference path="fs/promises.d.ts" />
Expand Down
15 changes: 15 additions & 0 deletions types/node/perf_hooks.d.ts
Expand Up @@ -604,6 +604,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 T;
}
? T
: 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 @@ -1425,6 +1425,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