diff --git a/bun.lock b/bun.lock index 6ccdf254cb0b..67a905d7ee32 100644 --- a/bun.lock +++ b/bun.lock @@ -30,6 +30,7 @@ "name": "bun-types", "dependencies": { "@types/node": "*", + "undici-types": "*", }, }, }, diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d8c9402e2871..c67367ab0843 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -4619,7 +4619,7 @@ declare module "bun" { interface WebSocketEventMap { close: CloseEvent; - error: Event; + error: ErrorEvent; message: MessageEvent; open: Event; } @@ -4699,9 +4699,10 @@ declare module "bun" { onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null; /** - * Event handler for error event + * Event handler for error event. Bun dispatches an {@link ErrorEvent}, so + * `ev.message` and `ev.error` describe the failure. */ - onerror: ((this: WebSocket, ev: Event) => any) | null; + onerror: ((this: WebSocket, ev: ErrorEvent) => any) | null; /** * Event handler for close event @@ -9337,10 +9338,10 @@ declare module "bun" { */ resize(width: number, height: number): Promise; - /** Navigate back in session history. */ - back(): Promise; - /** Navigate forward in session history. */ - forward(): Promise; + /** Navigate back in session history. Resolves without navigating when there is no previous entry. */ + goBack(): Promise; + /** Navigate forward in session history. Resolves without navigating when there is no next entry. */ + goForward(): Promise; /** Reload the current page. */ reload(): Promise; diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 6ce65ab7495e..9560173c915d 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -66,10 +66,62 @@ declare module "bun" { type LibEmptyOrReadableStreamBYOBRequest = LibDomIsLoaded extends true ? {} : import("node:stream/web").ReadableStreamBYOBRequest; + + /** + * What the `FormData` iteration methods return. With lib.dom loaded this is + * the `FormDataIterator` that lib.dom.d.ts declares for the same methods, so + * the two declarations of each method agree. Without it, `Request` and + * `Response` come from undici-types, whose `FormData` returns plain + * iterators, so `await request.formData()` must stay assignable to the + * global `FormData`. + */ + type LibFormDataIteratorOrIterableIterator = LibDomIsLoaded extends true + ? FormDataIterator + : IterableIterator; + + /** + * Like a `BodyMixin`, but implemented by more types, such as + * `Blob`, `ReadableStream`, and `Response`. + * + * It has no `blob()` method because it's the lowest common + * denominator of these objects: a `Blob` in Bun does not have a + * `.blob()` method. + */ + interface BunConsumerConvenienceMethods { + /** + * Consume as text + */ + text(): Promise; + + /** + * Consume as a Uint8Array, backed by an ArrayBuffer + */ + bytes(): Promise>; + + /** + * Consume as JSON + */ + json(): Promise; + } + + /** + * The methods Bun adds to `ReadableStream`. Both the global `ReadableStream` + * interface (whether lib.dom.d.ts declares it or not) and the one exported + * by `node:stream/web` (see overrides.d.ts) extend this, so the two stay + * assignable to each other. + */ + interface BunReadableStreamConsumerMethods extends BunConsumerConvenienceMethods { + /** + * Consume as a Blob + */ + blob(): Promise; + } } } -interface ReadableStream extends Bun.__internal.LibEmptyOrNodeReadableStream {} +interface ReadableStream + extends Bun.__internal.LibEmptyOrNodeReadableStream, + Bun.__internal.BunReadableStreamConsumerMethods {} declare var ReadableStream: Bun.__internal.UseLibDomIfAvailable< "ReadableStream", { @@ -1031,6 +1083,20 @@ declare class BuildMessage { readonly level: "error" | "warning" | "info" | "debug" | "verbose"; } +//#region ECMAScript additions +/* + * This region declares ECMAScript features that Bun supports, so that they + * type-check regardless of the `lib` compiler option. TypeScript's own lib + * files, and libraries such as core-js, declare the same members. Interface + * merging turns a signature that differs from the standard one into an extra + * overload, and a library that re-declares the standard signature in an + * interface that extends the global one then fails with TS2430. So every + * signature in this region must stay identical to the one in the TypeScript + * lib file named in the comment above it. + * test/integration/bun-types/fixture/core-js-types.ts checks that it does. + */ + +// lib.es2022.error.d.ts interface ErrorOptions { /** * The cause of the error. @@ -1045,16 +1111,19 @@ interface Error { cause?: unknown; } +// lib.es2022.error.d.ts (constructor), lib.esnext.error.d.ts (`isError`). +// `captureStackTrace` and `stackTraceLimit` are not in a lib file. @types/node +// declares them too, with these exact types. interface ErrorConstructor { new (message?: string, options?: ErrorOptions): Error; /** * Check if a value is an instance of Error * - * @param value - The value to check + * @param error - The value to check * @returns True if the value is an instance of Error, false otherwise */ - isError(value: unknown): value is Error; + isError(error: unknown): error is Error; /** * Create .stack property on a target object @@ -1067,38 +1136,37 @@ interface ErrorConstructor { stackTraceLimit: number; } +// lib.es2024.arraybuffer.d.ts interface ArrayBufferConstructor { - new (byteLength: number, options: { maxByteLength?: number }): ArrayBuffer; + new (byteLength: number, options?: { maxByteLength?: number }): ArrayBuffer; } interface ArrayBuffer { /** - * The length of the ArrayBuffer in bytes. - */ - readonly byteLength: number; - - /** - * Resize an ArrayBuffer in-place. + * Resize an ArrayBuffer in-place. The ArrayBuffer must have been created + * with a `maxByteLength`. */ - resize(byteLength: number): ArrayBuffer; + resize(newByteLength?: number): void; +} - /** - * Returns a section of an ArrayBuffer. - */ - slice(begin: number, end?: number): ArrayBuffer; +// lib.es2024.sharedmemory.d.ts +interface SharedArrayBufferConstructor { + new (byteLength: number, options?: { maxByteLength?: number }): SharedArrayBuffer; } interface SharedArrayBuffer { /** - * Grow the SharedArrayBuffer in-place. + * Grow the SharedArrayBuffer in-place. The SharedArrayBuffer must have been + * created with a `maxByteLength`. */ - grow(size: number): SharedArrayBuffer; + grow(newByteLength?: number): void; } +// lib.esnext.array.d.ts interface ArrayConstructor { /** * Create an array from an iterable or async iterable object. - * Values from the iterable are awaited. + * Values from a sync iterable are awaited. * * ```ts * await Array.fromAsync([1]); // [1] @@ -1106,35 +1174,210 @@ interface ArrayConstructor { * await Array.fromAsync((async function*() { yield 1 })()); // [1] * ``` * - * @param arrayLike - The iterable or async iterable to convert to an array. + * @param iterableOrArrayLike - The iterable or async iterable to convert to an array. * @returns A {@link Promise} that resolves with a new {@link Array} containing the awaited values */ - fromAsync(arrayLike: AsyncIterable | Iterable | ArrayLike): Promise[]>; + fromAsync( + iterableOrArrayLike: AsyncIterable | Iterable> | ArrayLike>, + ): Promise; /** * Create an array from an iterable or async iterable object. - * Values from the iterable are awaited. Results of the map function are also awaited. + * Values from a sync iterable are awaited. Results of the map function are also awaited. * * ```ts - * await Array.fromAsync([1]); // [1] - * await Array.fromAsync([Promise.resolve(1)]); // [1] - * await Array.fromAsync((async function*() { yield 1 })()); // [1] * await Array.fromAsync([1], (n) => n + 1); // [2] * await Array.fromAsync([1], (n) => Promise.resolve(n + 1)); // [2] * ``` * - * @param arrayLike - The iterable or async iterable to convert to an array. - * @param mapFn - A mapper function that transforms each element of `arrayLike` after awaiting them. + * @param iterableOrArrayLike - The iterable or async iterable to convert to an array. + * @param mapFn - A mapper function that transforms each element of `iterableOrArrayLike` after awaiting it. * @param thisArg - The `this` to which `mapFn` is bound. * @returns A {@link Promise} that resolves with a new {@link Array} containing the awaited values */ fromAsync( - arrayLike: AsyncIterable | Iterable | ArrayLike, - mapFn?: (value: T, index: number) => U, + iterableOrArrayLike: AsyncIterable | Iterable | ArrayLike, + mapFn: (value: Awaited, index: number) => U, thisArg?: any, ): Promise[]>; } +// lib.es2024.promise.d.ts +interface PromiseWithResolvers { + promise: Promise; + resolve: (value: T | PromiseLike) => void; + reject: (reason?: any) => void; +} + +// lib.es2024.promise.d.ts (`withResolvers`), lib.es2025.promise.d.ts (`try`) +interface PromiseConstructor { + /** + * Create a deferred promise with its `resolve` and `reject` functions exposed, + * so code outside the promise can settle it. + * + * @example + * ```ts + * const { promise, resolve, reject } = Promise.withResolvers(); + * + * setTimeout(() => { + * resolve("Hello world!"); + * }, 1000); + * + * await promise; // "Hello world!" + * ``` + */ + withResolvers(): PromiseWithResolvers; + + /** + * Run a function and return a promise of its result. If the function throws, + * the returned promise rejects with the thrown error. + * + * @param callbackFn - The function to run + * @param args - The arguments to pass to the function. This is similar to `setTimeout` and avoids the extra closure. + * @returns A promise that resolves with the function's result + */ + try(callbackFn: (...args: U) => T | PromiseLike, ...args: U): Promise>; +} + +// lib.esnext.collection.d.ts +interface Map { + /** + * Returns the value associated with `key`. If there is none, `defaultValue` + * is inserted for `key` and returned. + * + * @example + * ```ts + * const counts = new Map(); + * counts.getOrInsert("a", []).push(1); + * ``` + */ + getOrInsert(key: K, defaultValue: V): V; + /** + * Returns the value associated with `key`. If there is none, `callback` is + * called with `key`, and its result is inserted for `key` and returned. + * + * @example + * ```ts + * const groups = new Map(); + * groups.getOrInsertComputed("a", () => []).push("first"); + * ``` + */ + getOrInsertComputed(key: K, callback: (key: K) => V): V; +} + +interface WeakMap { + /** + * Returns the value associated with `key`. If there is none, `defaultValue` + * is inserted for `key` and returned. + */ + getOrInsert(key: K, defaultValue: V): V; + /** + * Returns the value associated with `key`. If there is none, `callback` is + * called with `key`, and its result is inserted for `key` and returned. + */ + getOrInsertComputed(key: K, callback: (key: K) => V): V; +} + +// lib.es2025.regexp.d.ts +interface RegExpConstructor { + /** + * Escapes any potential regex syntax characters in a string, and returns a + * new string that can be safely used as a literal pattern for the RegExp() + * constructor. + * + * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape) + * + * @example + * ```ts + * const re = new RegExp(RegExp.escape("foo.bar")); + * re.test("foo.bar"); // true + * re.test("foo!bar"); // false + * ``` + */ + escape(string: string): string; +} + +// lib.esnext.typedarrays.d.ts +interface Uint8Array { + /** + * Convert the Uint8Array to a base64 encoded string + */ + toBase64(options?: { alphabet?: "base64" | "base64url" | undefined; omitPadding?: boolean | undefined }): string; + + /** + * Decode a base64 encoded string into this Uint8Array, starting at index 0. + * Decoding stops when the array is full. + * + * @param string The base64 encoded string to decode into the array + * @param options Options for decoding the base64 string + */ + setFromBase64( + string: string, + options?: { + alphabet?: "base64" | "base64url" | undefined; + lastChunkHandling?: "loose" | "strict" | "stop-before-partial" | undefined; + }, + ): { + /** + * The number of characters read from the base64 string + */ + read: number; + /** + * The number of bytes written to the Uint8Array. + * Never greater than the `.byteLength` of this Uint8Array + */ + written: number; + }; + + /** + * Convert the Uint8Array to a hex encoded string + */ + toHex(): string; + + /** + * Decode a hex encoded string into this Uint8Array, starting at index 0. + * Decoding stops when the array is full. + * + * @param string The hex encoded string to decode into the array. The string must have + * an even number of characters, contain only hexadecimal characters, and contain no whitespace. + */ + setFromHex(string: string): { + /** + * The number of characters read from the hex string + */ + read: number; + /** + * The number of bytes written to the Uint8Array. + * Never greater than the `.byteLength` of this Uint8Array + */ + written: number; + }; +} + +interface Uint8ArrayConstructor { + /** + * Create a new Uint8Array from a base64 encoded string + * @param string The base64 encoded string to convert to a Uint8Array + * @param options Options for decoding the base64 string + * @returns A new Uint8Array containing the decoded data + */ + fromBase64( + string: string, + options?: { + alphabet?: "base64" | "base64url" | undefined; + lastChunkHandling?: "loose" | "strict" | "stop-before-partial" | undefined; + }, + ): Uint8Array; + + /** + * Create a new Uint8Array from a hex encoded string + * @param string The hex encoded string to convert to a Uint8Array + * @returns A new Uint8Array containing the decoded data + */ + fromHex(string: string): Uint8Array; +} +//#endregion + interface ConsoleOptions { stdout: import("stream").Writable; stderr?: import("stream").Writable; @@ -1403,39 +1646,6 @@ interface EventSourceInit { withCredentials?: boolean; } -interface PromiseConstructor { - /** - * Create a deferred promise with its `resolve` and `reject` functions exposed, - * so code outside the promise can settle it. - * - * @example - * ```ts - * const { promise, resolve, reject } = Promise.withResolvers(); - * - * setTimeout(() => { - * resolve("Hello world!"); - * }, 1000); - * - * await promise; // "Hello world!" - * ``` - */ - withResolvers(): { - promise: Promise; - resolve: (value?: T | PromiseLike) => void; - reject: (reason?: any) => void; - }; - - /** - * Run a function and return a promise of its result. If the function throws, - * the returned promise rejects with the thrown error. - * - * @param fn - The function to run - * @param args - The arguments to pass to the function. This is similar to `setTimeout` and avoids the extra closure. - * @returns A promise that resolves with the function's result - */ - try(fn: (...args: A) => T | PromiseLike, ...args: A): Promise; -} - interface Navigator { readonly userAgent: string; readonly platform: "MacIntel" | "Win32" | "Linux x86_64"; @@ -1526,78 +1736,6 @@ declare var Blob: Bun.__internal.UseLibDomIfAvailable< } >; -interface Uint8Array { - /** - * Convert the Uint8Array to a base64 encoded string - */ - toBase64(options?: { alphabet?: "base64" | "base64url"; omitPadding?: boolean }): string; - - /** - * Set the contents of the Uint8Array from a base64 encoded string - * @param base64 The base64 encoded string to decode into the array - * @param offset Optional starting index to begin setting the decoded bytes (default: 0) - */ - setFromBase64( - base64: string, - offset?: number, - ): { - /** - * The number of bytes read from the base64 string - */ - read: number; - /** - * The number of bytes written to the Uint8Array. - * Never greater than the `.byteLength` of this Uint8Array - */ - written: number; - }; - - /** - * Convert the Uint8Array to a hex encoded string - */ - toHex(): string; - - /** - * Set the contents of the Uint8Array from a hex encoded string - * @param hex The hex encoded string to decode into the array. The string must have - * an even number of characters, contain only hexadecimal characters, and contain no whitespace. - */ - setFromHex(hex: string): { - /** - * The number of bytes read from the hex string - */ - read: number; - /** - * The number of bytes written to the Uint8Array. - * Never greater than the `.byteLength` of this Uint8Array - */ - written: number; - }; -} - -interface Uint8ArrayConstructor { - /** - * Create a new Uint8Array from a base64 encoded string - * @param base64 The base64 encoded string to convert to a Uint8Array - * @param options Options for decoding the base64 string - * @returns A new Uint8Array containing the decoded data - */ - fromBase64( - base64: string, - options?: { - alphabet?: "base64" | "base64url"; - lastChunkHandling?: "loose" | "strict" | "stop-before-partial"; - }, - ): Uint8Array; - - /** - * Create a new Uint8Array from a hex encoded string - * @param hex The hex encoded string to convert to a Uint8Array - * @returns A new Uint8Array containing the decoded data - */ - fromHex(hex: string): Uint8Array; -} - interface BroadcastChannel extends Bun.__internal.LibEmptyOrBroadcastChannel {} declare var BroadcastChannel: Bun.__internal.UseLibDomIfAvailable< "BroadcastChannel", @@ -1704,12 +1842,23 @@ interface FormData { set(name: string, value: string): void; set(name: string, blobValue: Blob, filename?: string): void; forEach(callbackfn: (value: Bun.FormDataEntryValue, key: string, parent: FormData) => void, thisArg?: any): void; - keys(): IterableIterator; - values(): IterableIterator; - entries(): IterableIterator<[string, string]>; + /** Returns an iterator over the `[name, value]` pairs of every entry in the list. Same as {@link FormData.entries}. */ + [Symbol.iterator](): Bun.__internal.LibFormDataIteratorOrIterableIterator<[string, Bun.FormDataEntryValue]>; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/entries) */ + entries(): Bun.__internal.LibFormDataIteratorOrIterableIterator<[string, Bun.FormDataEntryValue]>; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/keys) */ + keys(): Bun.__internal.LibFormDataIteratorOrIterableIterator; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/values) */ + values(): Bun.__internal.LibFormDataIteratorOrIterableIterator; } declare var FormData: Bun.__internal.UseLibDomIfAvailable<"FormData", { prototype: FormData; new (): FormData }>; +// Declared exactly as in lib.dom.d.ts so that the two declarations merge when lib.dom is loaded. +// Without lib.dom, FormData does not use it (see Bun.__internal.LibFormDataIteratorOrIterableIterator). +interface FormDataIterator extends IteratorObject { + [Symbol.iterator](): FormDataIterator; +} + interface EventSource extends Bun.__internal.LibEmptyOrEventSource {} declare var EventSource: Bun.__internal.UseLibDomIfAvailable< "EventSource", @@ -2153,21 +2302,3 @@ declare namespace fetch { ): void; } //#endregion - -interface RegExpConstructor { - /** - * Escapes any potential regex syntax characters in a string, and returns a - * new string that can be safely used as a literal pattern for the RegExp() - * constructor. - * - * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape) - * - * @example - * ```ts - * const re = new RegExp(RegExp.escape("foo.bar")); - * re.test("foo.bar"); // true - * re.test("foo!bar"); // false - * ``` - */ - escape(string: string): string; -} diff --git a/packages/bun-types/overrides.d.ts b/packages/bun-types/overrides.d.ts index ec4f620289d9..0085556cbed0 100644 --- a/packages/bun-types/overrides.d.ts +++ b/packages/bun-types/overrides.d.ts @@ -1,41 +1,14 @@ export {}; -/** - * Like a `BodyMixin`, but implemented by more types, such as - * `Blob`, `ReadableStream`, and `Response`. - * - * It has no `blob()` method because it's the lowest common - * denominator of these objects: a `Blob` in Bun does not have a - * `.blob()` method. - */ -interface BunConsumerConvenienceMethods { - /** - * Consume as text - */ - text(): Promise; - - /** - * Consume as a Uint8Array, backed by an ArrayBuffer - */ - bytes(): Promise>; - - /** - * Consume as JSON - */ - json(): Promise; -} - declare module "stream/web" { - interface ReadableStream extends BunConsumerConvenienceMethods { - /** - * Consume as a Blob - */ - blob(): Promise; - } + // The global `ReadableStream` interface (globals.d.ts) extends this same + // interface, so streams from `node:stream/web` and global streams stay + // assignable to each other. + interface ReadableStream extends Bun.__internal.BunReadableStreamConsumerMethods {} } declare module "buffer" { - interface Blob extends BunConsumerConvenienceMethods { + interface Blob extends Bun.__internal.BunConsumerConvenienceMethods { // We have to specify bytes again even though it comes from // BunConsumerConvenienceMethods, because inheritance in TypeScript is // slightly different from just "copying in the methods" (the difference is diff --git a/packages/bun-types/package.json b/packages/bun-types/package.json index 7e1e879efdfd..d775855fada6 100644 --- a/packages/bun-types/package.json +++ b/packages/bun-types/package.json @@ -20,7 +20,8 @@ ], "homepage": "https://bun.com", "dependencies": { - "@types/node": "*" + "@types/node": "*", + "undici-types": "*" }, "scripts": { "prebuild": "echo $(pwd)", diff --git a/packages/bun-types/test.d.ts b/packages/bun-types/test.d.ts index 714a2f71d6d1..f0f0c65be333 100644 --- a/packages/bun-types/test.d.ts +++ b/packages/bun-types/test.d.ts @@ -17,8 +17,16 @@ declare module "bun:test" { export type Mock any> = JestMock.Mock; export const mock: { + // Separate overload for the implementation argument: inferring `T` through an + // optional parameter (`T | undefined`) instantiates a generic implementation's + // type parameters as `unknown`, so its generic call signature would be lost. /** - * Creates a mock function. The optional `Function` becomes the mock's implementation. + * Creates a mock function. `Function` becomes the mock's implementation. + */ + any>(Function: T): Mock; + /** + * Creates a mock function with no implementation. Pass a type argument to + * give the mock a call signature. */ any>(Function?: T): Mock; @@ -91,6 +99,8 @@ declare module "bun:test" { function restoreAllMocks(): void; function clearAllMocks(): void; function resetAllMocks(): void; + // Same overload pair as `mock()` above, for the same reason. + function fn any>(func: T): Mock; function fn any>(func?: T): Mock; function setSystemTime(now?: number | Date): void; function setTimeout(milliseconds: number): void; @@ -2017,9 +2027,13 @@ declare module "bun:test" { [K in keyof T as Required[K] extends FunctionLike ? K : never]: T[K]; }; - export interface Mock any> extends MockInstance { - (...args: Parameters): ReturnType; - } + /** + * The mock is callable exactly like `T`. Intersecting with `T` keeps generic + * and overloaded call signatures, which a `(...args: Parameters) => + * ReturnType` call signature would collapse to their `unknown` + * instantiation (or to the last overload). + */ + export type Mock any> = T & MockInstance; /** * All what the internal typings need is to be sure that we have any-function. diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 05fa801f45e5..2dad6b7f2118 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -3,6 +3,7 @@ import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from " import { bunEnv, bunExe, isDebug, makeTree } from "harness"; import { existsSync, readFileSync } from "node:fs"; import { cp, mkdir, mkdtemp, rm } from "node:fs/promises"; +import { builtinModules } from "node:module"; import { tmpdir } from "node:os"; import { basename, dirname, join, relative } from "node:path"; @@ -326,6 +327,46 @@ describe("@types/bun integration test", () => { expect((await claude.text()).length).toBeGreaterThan(0); }); + // A hoisted node_modules resolves a package that bun-types imports but does not depend on + // (it used to import undici-types through @types/node's dependency on it). Layouts that + // only link declared dependencies do not: https://github.com/oven-sh/bun/issues/22805 + test("every package that bun-types imports from is one of its dependencies", () => { + const { dependencies } = JSON.parse(readFileSync(join(BUN_TYPES_PACKAGE_ROOT, "package.json"), "utf8")); + const importedPackages = new Map>(); + + const files = [...new Bun.Glob("**/*.d.ts").scanSync({ cwd: BUN_TYPES_PACKAGE_ROOT })].filter( + file => !/^node_modules[\\/]/.test(file), + ); + + for (const file of files) { + const { importedFiles, typeReferenceDirectives } = ts.preProcessFile( + readFileSync(join(BUN_TYPES_PACKAGE_ROOT, file), "utf8"), + ); + + const specifiers = [ + ...importedFiles.map(({ fileName }) => fileName), + // `/// ` resolves to @types/node + ...typeReferenceDirectives.map(({ fileName }) => `@types/${fileName}`), + ]; + + for (const specifier of specifiers) { + if (specifier.startsWith(".") || specifier === "bun" || specifier.startsWith("bun:")) continue; + if (builtinModules.includes(specifier.replace(/^node:/, ""))) continue; + + const packageName = specifier.split("/", specifier.startsWith("@") ? 2 : 1).join("/"); + importedPackages.set(packageName, (importedPackages.get(packageName) ?? new Set()).add(file)); + } + } + + const undeclared = [...importedPackages] + .filter(([packageName]) => !(packageName in dependencies)) + .map(([packageName, files]) => ({ packageName, files: [...files].sort() })); + + expect(undeclared).toEqual([]); + // Guards the assertion above against the scan silently finding nothing. + expect([...importedPackages.keys()].sort()).toEqual(["@types/node", "undici-types"]); + }); + describe("basic type checks", () => { typeTest("checks without lib.dom.d.ts", { emptyInterfaces: expectedEmptyInterfacesWhenNoDOM, @@ -660,19 +701,19 @@ describe("@types/bun integration test", () => { code: 2322, line: "24154.ts:11:3", message: - "Type 'Blob' is not assignable to type 'import(\"node:buffer\").Blob'.\nThe types returned by 'stream()' are incompatible between these types.\nType 'ReadableStream>' is missing the following properties from type 'ReadableStream': blob, text, bytes, json", + "Type 'Blob' is not assignable to type 'import(\"node:buffer\").Blob'.\nThe types of 'stream().pipeThrough' are incompatible between these types.\nType '(transform: ReadableWritablePair>, options?: StreamPipeOptions | undefined) => ReadableStream<...>' is not assignable to type '(transform: ReadableWritablePair, options?: StreamPipeOptions | undefined) => ReadableStream<...>'.\nTypes of parameters 'transform' and 'transform' are incompatible.\nType 'ReadableWritablePair' is not assignable to type 'ReadableWritablePair>'.\nThe types of 'readable.getReader(...).read' are incompatible between these types.\nType '(view: T, options?: ReadableStreamBYOBReaderReadOptions | undefined) => Promise>' is not assignable to type '>(view: T, options?: ReadableStreamBYOBReaderReadOptions | undefined) => Promise>'.\nTypes of parameters 'view' and 'view' are incompatible.\nType 'T' is not assignable to type 'NonSharedArrayBufferView'.\nType 'ArrayBufferView' is not assignable to type 'NonSharedArrayBufferView'.\nType 'ArrayBufferView' is missing the following properties from type 'DataView': getFloat32, getFloat64, getInt8, getInt16, and 19 more.\nType 'T' is not assignable to type 'DataView'.\nType 'ArrayBufferView' is missing the following properties from type 'DataView': getFloat32, getFloat64, getInt8, getInt16, and 19 more.", }, { code: 2769, line: "fetch.ts:25:32", message: - "No overload matches this call.\nOverload 1 of 3, '(input: string | Request | URL, init?: RequestInit | undefined): Promise', gave the following error.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is not assignable to type 'BodyInit | null | undefined'.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 3 more.\nOverload 2 of 3, '(input: string | Request | URL, init?: BunFetchRequestInit | undefined): Promise', gave the following error.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is not assignable to type 'BodyInit | null | undefined'.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 3 more.\nOverload 3 of 3, '(input: RequestInfo | URL, init?: RequestInit | undefined): Promise', gave the following error.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is not assignable to type 'BodyInit | null | undefined'.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 3 more.", + "No overload matches this call.\nOverload 1 of 3, '(input: string | Request | URL, init?: RequestInit | undefined): Promise', gave the following error.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is not assignable to type 'BodyInit | null | undefined'.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 7 more.\nOverload 2 of 3, '(input: string | Request | URL, init?: BunFetchRequestInit | undefined): Promise', gave the following error.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is not assignable to type 'BodyInit | null | undefined'.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 7 more.\nOverload 3 of 3, '(input: RequestInfo | URL, init?: RequestInit | undefined): Promise', gave the following error.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is not assignable to type 'BodyInit | null | undefined'.\nType 'AsyncGenerator<\"chunk1\" | \"chunk2\", void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 7 more.", }, { code: 2769, line: "fetch.ts:33:32", message: - "No overload matches this call.\nOverload 1 of 3, '(input: string | Request | URL, init?: RequestInit | undefined): Promise', gave the following error.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is not assignable to type 'BodyInit | null | undefined'.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 3 more.\nOverload 2 of 3, '(input: string | Request | URL, init?: BunFetchRequestInit | undefined): Promise', gave the following error.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is not assignable to type 'BodyInit | null | undefined'.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 3 more.\nOverload 3 of 3, '(input: RequestInfo | URL, init?: RequestInit | undefined): Promise', gave the following error.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is not assignable to type 'BodyInit | null | undefined'.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 3 more.", + "No overload matches this call.\nOverload 1 of 3, '(input: string | Request | URL, init?: RequestInit | undefined): Promise', gave the following error.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is not assignable to type 'BodyInit | null | undefined'.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 7 more.\nOverload 2 of 3, '(input: string | Request | URL, init?: BunFetchRequestInit | undefined): Promise', gave the following error.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is not assignable to type 'BodyInit | null | undefined'.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 7 more.\nOverload 3 of 3, '(input: RequestInfo | URL, init?: RequestInit | undefined): Promise', gave the following error.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is not assignable to type 'BodyInit | null | undefined'.\nType '{ [Symbol.asyncIterator](): AsyncGenerator<\"data1\" | \"data2\", void, unknown>; }' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 7 more.", }, { code: 2769, @@ -695,13 +736,13 @@ describe("@types/bun integration test", () => { code: 2345, line: "http.ts:55:24", message: - "Argument of type 'AsyncGenerator | \"it works!\", void, unknown>' is not assignable to parameter of type 'BodyInit | null | undefined'.\nType 'AsyncGenerator | \"it works!\", void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 3 more.", + "Argument of type 'AsyncGenerator | \"it works!\", void, unknown>' is not assignable to parameter of type 'BodyInit | null | undefined'.\nType 'AsyncGenerator | \"it works!\", void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 7 more.", }, { code: 2345, line: "index.ts:196:14", message: - "Argument of type 'AsyncGenerator, void, unknown>' is not assignable to parameter of type 'BodyInit | null | undefined'.\nType 'AsyncGenerator, void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 3 more.", + "Argument of type 'AsyncGenerator, void, unknown>' is not assignable to parameter of type 'BodyInit | null | undefined'.\nType 'AsyncGenerator, void, unknown>' is missing the following properties from type 'ReadableStream': locked, cancel, getReader, pipeThrough, and 7 more.", }, { code: 2345, @@ -709,16 +750,6 @@ describe("@types/bun integration test", () => { message: "Argument of type '{ headers: { \"x-bun\": string; }; }' is not assignable to parameter of type 'number'.", }, - { - code: 2339, - line: "spawn.ts:62:38", - message: "Property 'text' does not exist on type 'ReadableStream>'.", - }, - { - code: 2339, - line: "spawn.ts:107:38", - message: "Property 'text' does not exist on type 'ReadableStream>'.", - }, { code: 2769, line: "streams.ts:18:3", @@ -730,26 +761,6 @@ describe("@types/bun integration test", () => { line: "streams.ts:20:16", message: "Property 'write' does not exist on type 'ReadableByteStreamController'.", }, - { - code: 2339, - line: "streams.ts:46:19", - message: "Property 'json' does not exist on type 'ReadableStream>'.", - }, - { - code: 2339, - line: "streams.ts:47:19", - message: "Property 'bytes' does not exist on type 'ReadableStream>'.", - }, - { - code: 2339, - line: "streams.ts:48:19", - message: "Property 'text' does not exist on type 'ReadableStream>'.", - }, - { - code: 2339, - line: "streams.ts:49:19", - message: "Property 'blob' does not exist on type 'ReadableStream>'.", - }, { code: 2345, line: "streams.ts:63:66", @@ -799,6 +810,13 @@ describe("@types/bun integration test", () => { message: "Object literal may only specify known properties, and 'protocols' does not exist in type 'string[]'.", }, + // lib.dom.d.ts declares the WebSocket "error" event as a plain Event. Bun's + // declaration (an ErrorEvent, which is what Bun dispatches) only applies without it. + { + code: 2554, + line: "websocket.ts:132:23", + message: "Expected 2 arguments, but got 0.", + }, { code: 2551, line: "websocket.ts:192:17", @@ -854,6 +872,33 @@ describe("@types/bun integration test", () => { line: "websocket.ts:270:6", message: "Property 'terminate' does not exist on type 'WebSocket'.", }, + { + code: 2554, + line: "websocket.ts:279:23", + message: "Expected 2 arguments, but got 0.", + }, + { + code: 2339, + line: "websocket.ts:280:22", + message: "Property 'message' does not exist on type 'Event'.", + }, + { + code: 2339, + line: "websocket.ts:281:22", + message: "Property 'error' does not exist on type 'Event'.", + }, + { + code: 2769, + line: "websocket.ts:288:32", + message: + "No overload matches this call.\nOverload 1 of 2, '(type: \"error\", listener: (this: WebSocket, ev: Event) => any, options?: boolean | AddEventListenerOptions | undefined): void', gave the following error.\nArgument of type '(event: ErrorEvent) => void' is not assignable to parameter of type '(this: WebSocket, ev: Event) => any'.\nTypes of parameters 'event' and 'ev' are incompatible.\nType 'Event' is missing the following properties from type 'ErrorEvent': colno, error, filename, lineno, message\nOverload 2 of 2, '(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void', gave the following error.\nArgument of type '(event: ErrorEvent) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'.\nType '(event: ErrorEvent) => void' is not assignable to type 'EventListener'.\nTypes of parameters 'event' and 'evt' are incompatible.\nType 'Event' is missing the following properties from type 'ErrorEvent': colno, error, filename, lineno, message", + }, + { + code: 2322, + line: "websocket.ts:289:3", + message: + "Type '(event: ErrorEvent) => void' is not assignable to type '(this: WebSocket, ev: Event) => any'.\nTypes of parameters 'event' and 'ev' are incompatible.\nType 'Event' is missing the following properties from type 'ErrorEvent': colno, error, filename, lineno, message", + }, { code: 2339, line: "worker.ts:23:11", diff --git a/test/integration/bun-types/fixture/core-js-types.ts b/test/integration/bun-types/fixture/core-js-types.ts new file mode 100644 index 000000000000..ccefcc9aacd2 --- /dev/null +++ b/test/integration/bun-types/fixture/core-js-types.ts @@ -0,0 +1,185 @@ +import { expectType } from "./utilities"; + +// https://github.com/oven-sh/bun/issues/26868 +// +// bun-types declares a few ECMAScript features on the standard global interfaces +// (ArrayBuffer, PromiseConstructor, Map, Uint8Array, ...) so that they type-check +// regardless of the `lib` setting. TypeScript's own lib.*.d.ts files and core-js's +// type definitions declare the same members. Interface merging turns a different +// signature into an extra overload, so the mismatch is silent until a library such +// as core-js re-declares the standard signature in an interface that extends the +// global one: then TS2430 "incorrectly extends" is reported against that library. +// +// core-js-types is not published yet, so this file stands in for it: every +// interface below extends a global interface that bun-types adds members to, and +// re-declares those members with the signatures from TypeScript's lib files (the +// file is named above each one), which is what core-js-types does. If a bun-types +// declaration drifts from the standard one, this file stops compiling. The +// `lib: []` case in bun-types.test.ts is the one that matters most: there the +// bun-types declarations are the only ones, like for a user on an older `lib`. + +// lib.es2024.arraybuffer.d.ts +interface CoreJSArrayBuffer extends ArrayBuffer { + resize(newByteLength?: number): void; +} + +interface CoreJSArrayBufferConstructor extends ArrayBufferConstructor { + new (byteLength: number, options?: { maxByteLength?: number }): ArrayBuffer; +} + +// lib.es2024.sharedmemory.d.ts +interface CoreJSSharedArrayBuffer extends SharedArrayBuffer { + grow(newByteLength?: number): void; +} + +interface CoreJSSharedArrayBufferConstructor extends SharedArrayBufferConstructor { + new (byteLength: number, options?: { maxByteLength?: number }): SharedArrayBuffer; +} + +// lib.es2024.promise.d.ts and lib.es2025.promise.d.ts +declare global { + interface PromiseWithResolvers { + promise: Promise; + resolve: (value: T | PromiseLike) => void; + reject: (reason?: any) => void; + } +} + +interface CoreJSPromiseConstructor extends PromiseConstructor { + withResolvers(): PromiseWithResolvers; + try(callbackFn: (...args: U) => T | PromiseLike, ...args: U): Promise>; +} + +// lib.esnext.array.d.ts +interface CoreJSArrayConstructor extends ArrayConstructor { + fromAsync( + iterableOrArrayLike: AsyncIterable | Iterable> | ArrayLike>, + ): Promise; + fromAsync( + iterableOrArrayLike: AsyncIterable | Iterable | ArrayLike, + mapFn: (value: Awaited, index: number) => U, + thisArg?: any, + ): Promise[]>; +} + +// lib.es2022.error.d.ts and lib.esnext.error.d.ts +interface CoreJSErrorOptions extends ErrorOptions { + cause?: unknown; +} + +interface CoreJSError extends Error { + cause?: unknown; +} + +interface CoreJSErrorConstructor extends ErrorConstructor { + new (message?: string, options?: ErrorOptions): Error; + isError(error: unknown): error is Error; +} + +// lib.esnext.collection.d.ts +interface CoreJSMap extends Map { + getOrInsert(key: K, defaultValue: V): V; + getOrInsertComputed(key: K, callback: (key: K) => V): V; +} + +interface CoreJSWeakMap extends WeakMap { + getOrInsert(key: K, defaultValue: V): V; + getOrInsertComputed(key: K, callback: (key: K) => V): V; +} + +// lib.es2025.regexp.d.ts +interface CoreJSRegExpConstructor extends RegExpConstructor { + escape(string: string): string; +} + +// lib.esnext.typedarrays.d.ts +interface CoreJSUint8Array extends Uint8Array { + toBase64(options?: { alphabet?: "base64" | "base64url" | undefined; omitPadding?: boolean | undefined }): string; + setFromBase64( + string: string, + options?: { + alphabet?: "base64" | "base64url" | undefined; + lastChunkHandling?: "loose" | "strict" | "stop-before-partial" | undefined; + }, + ): { read: number; written: number }; + toHex(): string; + setFromHex(string: string): { read: number; written: number }; +} + +interface CoreJSUint8ArrayConstructor extends Uint8ArrayConstructor { + fromBase64( + string: string, + options?: { + alphabet?: "base64" | "base64url" | undefined; + lastChunkHandling?: "loose" | "strict" | "stop-before-partial" | undefined; + }, + ): Uint8Array; + fromHex(string: string): Uint8Array; +} + +// The merged declarations must also behave like the standard ones at use sites. +declare const buffer: ArrayBuffer; +expectType(buffer.resize(16)).is(); +expectType(buffer.resize()).is(); + +declare const shared: SharedArrayBuffer; +expectType(shared.grow(16)).is(); +expectType(shared.grow()).is(); + +new ArrayBuffer(8); +new ArrayBuffer(8, { maxByteLength: 16 }); +new SharedArrayBuffer(8, { maxByteLength: 16 }); + +const withResolvers = Promise.withResolvers(); +expectType(withResolvers).is>(); +expectType(withResolvers.promise).is>(); +withResolvers.resolve(1); +withResolvers.resolve(Promise.resolve(1)); +// @ts-expect-error the value is required when T is not void +withResolvers.resolve(); +withResolvers.reject(new Error("reason")); +withResolvers.reject(); + +// `resolve()` with no argument is the common case for `void` promises. +const { resolve, reject } = Promise.withResolvers(); +resolve(); +reject(); + +expectType(Promise.try(() => 1)).is>(); +expectType(Promise.try(async () => "a")).is>(); +expectType(Promise.try((a: number, b: string) => `${a}${b}`, 1, "b")).is>(); + +declare const asyncNumbers: AsyncIterable; +expectType(Array.fromAsync([1, Promise.resolve(2)])).is>(); +expectType(Array.fromAsync(asyncNumbers)).is>(); +expectType(Array.fromAsync([1, 2], n => `${n}`)).is>(); +expectType(Array.fromAsync(asyncNumbers, async n => n > 1)).is>(); +Array.fromAsync( + [1, 2], + (n, index) => { + expectType(n).is(); + expectType(index).is(); + return n; + }, + { thisArg: true }, +); + +declare const caught: unknown; +if (Error.isError(caught)) { + expectType(caught).is(); +} + +expectType(RegExp.escape("foo.bar")).is(); + +const bytes = new Uint8Array(8); +expectType(bytes.setFromBase64("aGVsbG8=")).is<{ read: number; written: number }>(); +bytes.setFromBase64("aGVsbG8", { alphabet: "base64", lastChunkHandling: "loose" }); +// @ts-expect-error the second argument is an options object, not an offset +bytes.setFromBase64("aGVsbG8=", 2); +expectType(bytes.setFromHex("68656c6c6f")).is<{ read: number; written: number }>(); +expectType(bytes.toBase64({ alphabet: "base64url", omitPadding: true })).is(); +expectType(bytes.toHex()).is(); +expectType(Uint8Array.fromBase64("aGVsbG8=", { lastChunkHandling: "strict" })).is>(); +expectType(Uint8Array.fromHex("68656c6c6f")).is>(); + +export {}; diff --git a/test/integration/bun-types/fixture/globals.ts b/test/integration/bun-types/fixture/globals.ts index 0ca54f0153e1..4428204cf18e 100644 --- a/test/integration/bun-types/fixture/globals.ts +++ b/test/integration/bun-types/fixture/globals.ts @@ -318,6 +318,39 @@ clearTimeout(1); setImmediate(() => {}); clearImmediate(1); +// FormData iteration yields File | string values, as in lib.dom.d.ts. +// https://github.com/oven-sh/bun/issues/27194 +{ + const form = new FormData(); + + // The iterator type differs with and without lib.dom (it is lib.dom's FormDataIterator + // when that lib is loaded), so only what the iterators yield is checked here. + for (const [name, value] of form) { + expectType(name).is(); + expectType(value).is(); + } + + for (const value of form.values()) { + expectType(value).is(); + } + + expectType([...form.keys()]).is(); + expectType([...form.values()]).is<(File | string)[]>(); + expectType([...form.entries()]).is<[string, File | string][]>(); + expectType(Object.fromEntries(form)).is<{ [k: string]: File | string }>(); +} + +// Without lib.dom, Request and Response come from undici-types, whose FormData has its own +// iterator types. What their formData() resolves to must still be a FormData, like the Blob +// case in 24154.ts. (fetch.ts covers the other direction, a FormData as a request body.) +async function formDataFromRequest(request: Request): Promise { + return await request.formData(); +} + +async function formDataFromResponse(response: Response): Promise { + return await response.formData(); +} + const err = new Error("test"); err.cause = "asdf"; err.cause = new Error("asdf"); @@ -336,7 +369,6 @@ new Error("asdf", { cause: new Error("asdf"), }); -// @ts-expect-error this interface is defined top level in globals.d.ts so we -// are making sure that .d.ts is a module and that anything top level doesn't -// leak to userland +// @ts-expect-error this interface lives in Bun.__internal (globals.d.ts), so we +// are making sure that it does not leak to userland as a global expectType(); diff --git a/test/integration/bun-types/fixture/map.ts b/test/integration/bun-types/fixture/map.ts new file mode 100644 index 000000000000..c803ec836979 --- /dev/null +++ b/test/integration/bun-types/fixture/map.ts @@ -0,0 +1,46 @@ +import { expectType } from "./utilities"; + +// https://github.com/oven-sh/bun/issues/27380 +// Map.prototype.getOrInsert / getOrInsertComputed and the WeakMap equivalents. +// The `lib: []` case in bun-types.test.ts checks these without lib.esnext.collection.d.ts, +// and fixture/core-js-types.ts checks that the declarations match that lib file. +{ + const counts = new Map(); + + expectType(counts.getOrInsert("a", 1)).is(); + expectType(counts.getOrInsertComputed("a", key => key.length)).is(); + + counts.getOrInsertComputed("a", key => { + expectType(key).is(); + return key.length; + }); + + // @ts-expect-error the default value must be a V + counts.getOrInsert("a", "one"); + // @ts-expect-error the callback must return a V + counts.getOrInsertComputed("a", () => "one"); + // @ts-expect-error the key must be a K + counts.getOrInsert(1, 1); +} + +{ + const themes = new WeakMap(); + const key = {}; + + expectType(themes.getOrInsert(key, "light")).is(); + expectType(themes.getOrInsertComputed(key, k => String(k))).is(); + + themes.getOrInsertComputed(key, k => { + expectType(k).is(); + return "dark"; + }); + + // @ts-expect-error the default value must be a V + themes.getOrInsert(key, 1); + // @ts-expect-error the callback must return a V + themes.getOrInsertComputed(key, () => 1); + // @ts-expect-error the key must be a K + themes.getOrInsert("key", "light"); +} + +export {}; diff --git a/test/integration/bun-types/fixture/mocks.ts b/test/integration/bun-types/fixture/mocks.ts index 599dee26feae..346c027138ea 100644 --- a/test/integration/bun-types/fixture/mocks.ts +++ b/test/integration/bun-types/fixture/mocks.ts @@ -1,4 +1,4 @@ -import { jest, mock } from "bun:test"; +import { jest, mock, type Mock, spyOn, vi } from "bun:test"; import { expectType } from "./utilities"; const mock1 = mock((arg: string) => { @@ -29,3 +29,67 @@ jest.fn<() => string>().mockResolvedValue("24"); jest.fn().mockClear(); jest.fn().mockReset(); jest.fn().mockRejectedValueOnce(new Error()); + +// A mock of a generic function keeps the generic call signature. +// https://github.com/oven-sh/bun/issues/38037 +{ + type Run = (callback: () => PromiseLike) => Promise; + const run: Run = async callback => callback(); + + const mocked = mock(run); + const mockedAsRun: Run = mocked; + expectType(mocked(async () => 42)).is>(); + expectType(mocked.mock.calls).is<[callback: () => PromiseLike][]>(); + mocked.mockClear(); + + const jestMocked = jest.fn(run); + const jestMockedAsRun: Run = jestMocked; + expectType(jestMocked(async () => "a")).is>(); + + const viMocked = vi.fn(run); + const viMockedAsRun: Run = viMocked; + expectType(viMocked(async () => true)).is>(); + + const target = { run }; + const spied = spyOn(target, "run"); + const spiedAsRun: Run = spied; + expectType(spied(async () => 1n)).is>(); + spied.mockRestore(); + + void [mockedAsRun, jestMockedAsRun, viMockedAsRun, spiedAsRun]; +} + +// A mock of an overloaded function keeps every overload. +{ + function parse(input: string): number; + function parse(input: number): string; + function parse(input: string | number) { + return typeof input === "string" ? Number(input) : String(input); + } + + const mocked = mock(parse); + expectType(mocked("1")).is(); + expectType(mocked(1)).is(); + // @ts-expect-error no overload accepts a boolean + mocked(true); +} + +// Mocks without an implementation, and with an explicit implementation type. +{ + const untyped = mock(); + untyped(1, "two", { three: 3 }); + + const typed = mock<(input: number) => string>(); + expectType(typed(1)).is(); + // @ts-expect-error the implementation type is (input: number) => string + typed("1"); + + const declared: Mock<(input: string) => number> = mock((input: string) => input.length); + expectType(declared("abc")).is(); + expectType(declared.mock.calls).is<[input: string][]>(); + declared.mockReturnValue(1); + // @ts-expect-error the return type is number + declared.mockReturnValue("1"); + + expectType(vi.fn<(input: number) => string>()(1)).is(); +} diff --git a/test/integration/bun-types/fixture/streams.ts b/test/integration/bun-types/fixture/streams.ts index a1e5aacf045b..9a0b75dbeec3 100644 --- a/test/integration/bun-types/fixture/streams.ts +++ b/test/integration/bun-types/fixture/streams.ts @@ -42,7 +42,7 @@ for await (const chunk of uint8Transform.readable) { } declare const stream: ReadableStream; - +// These exist with and without lib.dom.d.ts: https://github.com/oven-sh/bun/issues/29401 expectType(stream.json()).is>(); expectType(stream.bytes()).is>>(); expectType(stream.text()).is>(); diff --git a/test/integration/bun-types/fixture/websocket.ts b/test/integration/bun-types/fixture/websocket.ts index b8de6edfc556..a0d7f68f2702 100644 --- a/test/integration/bun-types/fixture/websocket.ts +++ b/test/integration/bun-types/fixture/websocket.ts @@ -129,7 +129,7 @@ import { expectType } from "./utilities"; }; ws.onerror = event => { - expectType(event).is(); + expectType(event).is(); }; ws.onclose = event => { @@ -269,3 +269,22 @@ import { expectType } from "./utilities"; // Terminate the connection immediately ws.terminate(); } + +// The "error" event is an ErrorEvent, like the one Bun dispatches at runtime. +// https://github.com/oven-sh/bun/issues/36329 +{ + const ws = new WebSocket("wss://dev.local"); + + ws.addEventListener("error", event => { + expectType(event).is(); + expectType(event.message).is(); + expectType(event.error).is(); + }); + + const handleError = (event: ErrorEvent) => { + expectType(event.error).is(); + }; + + ws.addEventListener("error", handleError); + ws.onerror = handleError; +} diff --git a/test/integration/bun-types/fixture/webview.ts b/test/integration/bun-types/fixture/webview.ts new file mode 100644 index 000000000000..ccbf5cb2352d --- /dev/null +++ b/test/integration/bun-types/fixture/webview.ts @@ -0,0 +1,17 @@ +import { expectType } from "./utilities"; + +// https://github.com/oven-sh/bun/issues/30754 +// The runtime (src/runtime/webview/JSWebViewPrototype.cpp) names the history +// methods goBack() and goForward(). +declare const view: Bun.WebView; + +expectType(view.goBack()).is>(); +expectType(view.goForward()).is>(); +expectType(view.reload()).is>(); + +// @ts-expect-error back() does not exist at runtime +view.back(); +// @ts-expect-error forward() does not exist at runtime +view.forward(); + +export {};