diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index c02e1080e7a1..82a59a7295ca 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -152,10 +152,18 @@ declare var ReadableStream: Bun.__internal.UseLibDomIfAvailable< prototype: ReadableStream; new (underlyingSource?: Bun.UnderlyingSource, strategy?: QueuingStrategy): ReadableStream; new (underlyingSource?: Bun.DirectUnderlyingSource, strategy?: QueuingStrategy): ReadableStream; - new >( - underlyingSource: import("node:stream/web").UnderlyingByteSource, + // A call without type arguments infers `K` as `"bytes"`. Else `K` is `never`, and this returns `ReadableStream`. + new < + R = any, + K extends "bytes" = never, + B extends ([K] extends [never] ? unknown : Uint8Array) = [K] extends [never] ? R : Uint8Array, + >( + // Always `UnderlyingByteSource`. The other branch is where a call infers `K`. + underlyingSource: [K] extends ["bytes"] + ? import("node:stream/web").UnderlyingByteSource + : import("node:stream/web").UnderlyingByteSource & { type: K }, strategy?: { highWaterMark?: number }, - ): ReadableStream; + ): ReadableStream; } >; @@ -1746,7 +1754,7 @@ declare var URL: Bun.__internal.UseLibDomIfAvailable< * @param url - The URL to check. * @param base - The base URL to use. */ - canParse(url: string, base?: string): boolean; + canParse(url: string | URL, base?: string | URL): boolean; /** * Create a `blob:` URL for a {@link Blob}. * @@ -1766,7 +1774,7 @@ declare var URL: Bun.__internal.UseLibDomIfAvailable< * @param url - The URL to parse. * @param base - The base URL to use. */ - parse(url: string, base?: string): URL | null; + parse(url: string | URL, base?: string | URL): URL | null; } >; @@ -1990,8 +1998,8 @@ declare var Request: Bun.__internal.UseLibDomIfAvailable< { prototype: Request; new (requestInfo: string, init?: RequestInit): Request; - new (requestInfo: RequestInit & { url: string }): Request; - new (requestInfo: Request, init?: RequestInit): Request; + new (requestInfo: RequestInit & { url: string | URL }): Request; + new (requestInfo: string | URL | Request, init?: RequestInit): Request; } >; @@ -2029,7 +2037,7 @@ declare var Response: Bun.__internal.UseLibDomIfAvailable< * @param url The URL to redirect to * @param status The HTTP status code to use for the redirect */ - redirect(url: string, status?: number): Response; + redirect(url: string | URL, status?: number): Response; /** * Create a new {@link Response} that redirects to `url`. @@ -2037,7 +2045,7 @@ declare var Response: Bun.__internal.UseLibDomIfAvailable< * @param url The URL to redirect to * @param init Options for the response */ - redirect(url: string, init?: ResponseInit): Response; + redirect(url: string | URL, init?: ResponseInit): Response; /** * Create a new {@link Response} that represents a network error. diff --git a/packages/bun-types/wasm.d.ts b/packages/bun-types/wasm.d.ts index 8b11b43174be..aba6f5c01a69 100644 --- a/packages/bun-types/wasm.d.ts +++ b/packages/bun-types/wasm.d.ts @@ -1,8 +1,24 @@ declare module "bun" { + namespace __internal { + interface FallbackWebAssemblyTag { + readonly [Symbol.toStringTag]: "WebAssembly.Tag"; + } + // An empty interface, not `{}`: an interface without members is read as its base, and `{}` is an object literal type. + interface LibWebAssemblyTag {} + // lib.dom's `Tag` is empty. It stays empty with lib.dom, so that a cast of an export to `WebAssembly.Tag` is legal. + type LibOrFallbackWebAssemblyTag = LibDomIsLoaded extends true ? LibWebAssemblyTag : FallbackWebAssemblyTag; + } + namespace WebAssembly { type ImportExportKind = "function" | "global" | "memory" | "table" | "tag"; type TableKind = "anyfunc" | "externref"; - type ExportValue = Function | Global | WebAssembly.Memory | WebAssembly.Table; + // Every value fits lib.dom's empty `Tag`, so with lib.dom a tag is no export value, as in lib.dom itself. + type ExportValue = + | Function + | Global + | WebAssembly.Memory + | WebAssembly.Table + | (__internal.LibDomIsLoaded extends true ? never : Tag); type Exports = Record; type ImportValue = ExportValue | number; type Imports = Record; @@ -95,6 +111,55 @@ declare module "bun" { instance: Instance; module: Module; } + + interface TagType { + /** The value types of the values that an exception with this tag carries. */ + parameters: ValueType[]; + } + + interface ExceptionOptions { + /** Give the exception a `stack` property with a stack trace. Defaults to `false`. */ + traceStack?: boolean | undefined; + } + + /** + * The type of a WebAssembly exception: the value types of the values that + * the exception carries. + * + * [MDN Reference](https://developer.mozilla.org/docs/WebAssembly/Reference/JavaScript_interface/Tag) + */ + interface Tag extends __internal.LibOrFallbackWebAssemblyTag {} + + /** + * An exception that WebAssembly code throws to JavaScript, or that + * JavaScript throws to WebAssembly code. It is not an instance of `Error`. + * + * [MDN Reference](https://developer.mozilla.org/docs/WebAssembly/Reference/JavaScript_interface/Exception) + */ + interface Exception { + /** + * A stack trace, if the exception was created with `traceStack: true`. + * Otherwise `undefined`. + * + * [MDN Reference](https://developer.mozilla.org/docs/WebAssembly/Reference/JavaScript_interface/Exception/stack) + */ + readonly stack: string | undefined; + /** + * Returns the value at `index` among the values that the exception carries. + * + * Throws a `TypeError` if `exceptionTag` is not the tag of the exception, + * and a `RangeError` if `index` is out of range. + * + * [MDN Reference](https://developer.mozilla.org/docs/WebAssembly/Reference/JavaScript_interface/Exception/getArg) + */ + getArg(exceptionTag: Tag, index: number): any; + /** + * Returns `true` if `exceptionTag` is the tag of the exception. + * + * [MDN Reference](https://developer.mozilla.org/docs/WebAssembly/Reference/JavaScript_interface/Exception/is) + */ + is(exceptionTag: Tag): boolean; + } } } @@ -172,6 +237,31 @@ declare namespace WebAssembly { } >["Module"]; + interface TagType extends Bun.WebAssembly.TagType {} + interface ExceptionOptions extends Bun.WebAssembly.ExceptionOptions {} + + interface Tag extends Bun.WebAssembly.Tag {} + var Tag: { + prototype: Tag; + new (type: TagType): Tag; + }; + + interface Exception extends Bun.WebAssembly.Exception {} + var Exception: { + prototype: Exception; + /** + * @param exceptionTag The tag of the exception. `WebAssembly.JSTag` is not accepted. + * @param payload One value for each of the `parameters` of the tag + */ + new (exceptionTag: Tag, payload: any[], options?: ExceptionOptions): Exception; + }; + + /** + * The tag of a value that JavaScript throws. WebAssembly code that imports + * this tag can catch such a value as an `externref`. + */ + var JSTag: Tag; + interface Table extends Bun.WebAssembly.Table {} var Table: { prototype: Table; diff --git a/test/aot/what-rests-on-a-brought-declaration-stops-no-build.test.ts b/test/aot/what-rests-on-a-brought-declaration-stops-no-build.test.ts index 538d9581c035..ecd046dfefd2 100644 --- a/test/aot/what-rests-on-a-brought-declaration-stops-no-build.test.ts +++ b/test/aot/what-rests-on-a-brought-declaration-stops-no-build.test.ts @@ -170,6 +170,12 @@ const builds: Record; stdout: string }> }, stdout: "string\nother\nrang\n", }, + "classes without a constructor below a stream of the web, with its type and without": { + files: { + "main.ts": `export {};\nclass Lines extends ReadableStream {}\nclass Pipe extends ReadableStream {}\nconsole.log(new Lines().locked, new Pipe({}).locked);\n`, + }, + stdout: "false false\n", + }, "a number of Bun's through a generic function, a generic class, a map, a promise of all and a pattern": { files: { "main.ts": `export {};\nfunction same(value: T): T {\n return value;\n}\nclass Box {\n constructor(public held: T) {}\n}\nconst size = () => Bun.file(import.meta.path).size;\nconst one: string = same(size());\nconst two: string = new Box(size()).held;\nconst three: string = new Map([["a", size()]]).get("a")!;\nconst [four] = await Promise.all([size()]);\nconst five: string = four;\nconst { held } = { ...new Box(size()) };\nconst six: string = held;\nconsole.log(typeof one, typeof two, typeof three, typeof five, typeof six);\n`, @@ -182,8 +188,8 @@ const builds: Record; stdout: string }> const stops: Record; errors: string[] }> = { // TypeScript's own library for browsers declares these with a type parameter, and tsc says the same of them. "classes without a constructor below classes of the web that take a type": { - text: `export {};\nclass Told extends CustomEvent {}\nclass Sent extends MessageEvent {}\nclass Pipe extends ReadableStream {}\nconsole.log(new Told("e", { detail: 1 }).detail, new Sent("m", { data: 2 }).data, new Pipe({}).locked);\n`, - errors: ["TS2508", "TS2508", "TS2510", "TS2351", "TS2351"], + text: `export {};\nclass Told extends CustomEvent {}\nclass Sent extends MessageEvent {}\nconsole.log(new Told("e", { detail: 1 }).detail, new Sent("m", { data: 2 }).data);\n`, + errors: ["TS2508", "TS2508", "TS2351", "TS2351"], }, "a number out of a field in another file, held against a text in the result of a function and at the top": { text: `import { n } from "./other";\nconst r = inner();\nfunction inner() {\n return n === "one";\n}\nif (n === "two") console.log(r);\nconsole.log("ran");\n`, diff --git a/test/cli/check/web-globals-take-what-the-runtime-takes.test.ts b/test/cli/check/web-globals-take-what-the-runtime-takes.test.ts new file mode 100644 index 000000000000..394ee28d4e86 --- /dev/null +++ b/test/cli/check/web-globals-take-what-the-runtime-takes.test.ts @@ -0,0 +1,75 @@ +// `bun compile` type checks what it builds, so a statement that runs on Bun and that the declarations of Bun refuse stops the build. +// The program below runs on Bun. The project is the one that `bun init` writes: no library for browsers, and the types of Bun. +import { expect, test } from "bun:test"; +import { bunExe, tempDirWithBunTypes } from "harness"; +import { run } from "../../aot/harness/run"; + +const tsconfig = JSON.stringify({ + compilerOptions: { + lib: ["ESNext"], + target: "ESNext", + module: "Preserve", + moduleDetection: "force", + moduleResolution: "bundler", + types: ["bun"], + strict: true, + skipLibCheck: true, + noEmit: true, + }, +}); + +const program = `const url = new URL("http://localhost/a/b"); +const request = new Request(url, { method: "POST" }); +const redirect = Response.redirect(url, 302); +console.log(request.method, new Request({ url }).url, redirect.headers.get("location")); +console.log(Response.redirect(url, { status: 307 }).status, URL.canParse(url), URL.parse(url)?.href); +console.log(URL.canParse("/c", url), URL.parse("c", url)?.href); + +class Lines extends ReadableStream {} +class Chunks extends ReadableStream {} +class Bytes extends ReadableStream { + constructor() { + super({ + type: "bytes", + pull(controller) { + controller.enqueue(new Uint8Array([104, 105])); + controller.close(); + }, + }); + } +} +const bytes: ReadableStream> = new ReadableStream({ type: "bytes" }); +const lines = new Lines({ + start(controller) { + controller.enqueue("line"); + controller.close(); + }, +}); +console.log(await lines.text(), new Chunks({ type: "bytes" }) instanceof Chunks, bytes.locked); +const { value: chunk } = await new Bytes().getReader().read(); +console.log(chunk instanceof Uint8Array, Buffer.isBuffer(chunk), chunk?.length); + +const tag = new WebAssembly.Tag({ parameters: ["i32"] }); +const exception = new WebAssembly.Exception(tag, [7]); +console.log(exception.is(tag), exception.is(WebAssembly.JSTag), exception.getArg(tag, 0)); +`; + +const printed = `POST http://localhost/a/b http://localhost/a/b +307 true http://localhost/a/b +true http://localhost/a/c +line true false +true false 2 +true false 7 +`; + +// One check reads every declaration of Bun and of Node.js, which takes a debug build several seconds. +test("web globals take what the runtime takes", async () => { + using dir = tempDirWithBunTypes("check-web-globals", { "tsconfig.json": tsconfig, "index.ts": program }); + expect(await run([bunExe(), "index.ts"], String(dir))).toEqual({ stdout: printed, stderr: "", exitCode: 0 }); + const checked = await run([bunExe(), "check", "--no-pretty"], String(dir)); + // The summary on stderr has a time in it. Where the check fails, the message shows all three. + expect(checked.exitCode === 0 ? { stdout: checked.stdout, exitCode: 0 } : checked).toEqual({ + stdout: "", + exitCode: 0, + }); +}, 60_000); diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 47e6ab29c5fc..ad7ba5014048 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -902,37 +902,128 @@ describe("@types/bun integration test", () => { }); }); - // Runs on debug builds too, same as the Bun.mmap block above. - describe("Event and EventTarget", () => { - async function checkEventFixture(name: string, lib: string[], source: string) { - const checkDir = join(TEMP_DIR, name); - const tsconfig = structuredClone(sourceTsconfig); - tsconfig.include = ["event-check.ts"]; - tsconfig.compilerOptions.lib = lib; - tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")]; - await mkdir(checkDir, { recursive: true }); - await makeTree(checkDir, { - "tsconfig.json": JSON.stringify(tsconfig, null, 2), - "event-check.ts": source, - }); + async function checkSource(name: string, lib: string[], source: string, options: Partial = {}) { + const checkDir = join(TEMP_DIR, name); + const tsconfig = structuredClone(sourceTsconfig); + tsconfig.include = ["check.ts"]; + Object.assign(tsconfig.compilerOptions, options); + tsconfig.compilerOptions.lib = lib; + tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")]; + await mkdir(checkDir, { recursive: true }); + await makeTree(checkDir, { + "tsconfig.json": JSON.stringify(tsconfig, null, 2), + "check.ts": source, + }); - await using proc = Bun.spawn({ - cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."], - env: bunEnv, - cwd: checkDir, - stdout: "pipe", - stderr: "pipe", - }); + await using proc = Bun.spawn({ + cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."], + env: bunEnv, + cwd: checkDir, + stdout: "pipe", + stderr: "pipe", + }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr.trim()).toBe(""); - expect(stdout.trim()).toBe(""); - expect(exitCode).toBe(0); - } + expect(stderr.trim()).toBe(""); + expect(stdout.trim()).toBe(""); + expect(exitCode).toBe(0); + } + + // Runs on debug builds too, same as the Bun.mmap block above. These forms run on Bun, + // and lib.dom rejects them, so they cannot go into the fixture that both configurations check. + describe("web globals without lib.dom", () => { + test("follow the runtime", async () => { + await checkSource( + "web-globals-no-lib-dom-check", + ["ESNext"], + `const url = new URL("https://example.com"); + new Request({ url, method: "POST" }); + Response.redirect(url, { status: 302 }); + + class BareStream extends ReadableStream {} + new BareStream({ type: "bytes" }); + // A program that picks between the class and a subclass of it constructs what it picked. + class ByteStream extends ReadableStream {} + declare const subclass: boolean; + new (subclass ? ByteStream : ReadableStream)({ type: "bytes" }); + + // A source of type \`any\` gives \`ReadableStream\`, and no byte stream (#44614). + declare const source: any; + export const fromAny: ReadableStream = new ReadableStream(source); + // The expected type sets the chunk type of a byte stream. + export function byteStream(): ReadableStream { + return new ReadableStream({ type: "bytes" }); + } + + // What a program reads off the constructor type stays: three signatures, and the last one is the byte source. + type EverySource = typeof ReadableStream extends { + new (...args: infer A1): any; + new (...args: infer A2): any; + new (...args: infer A3): any; + } + ? NonNullable + : never; + export const defaultSource: EverySource = { start() {} }; + export const byteSource: ConstructorParameters[0] = { type: "bytes" }; + declare const instance: InstanceType; + instance satisfies ReadableStream; + + // lib.dom has an empty \`WebAssembly.Tag\`, and no tag among the values that a module imports. + declare const compiled: WebAssembly.Module; + declare const settings: { trace?: boolean }; + const tag = new WebAssembly.Tag({ parameters: ["i32"] }); + const exception = new WebAssembly.Exception(tag, [1], { traceStack: settings.trace }); + WebAssembly.instantiate(compiled, { env: { thrown: tag, thrownByJavaScript: WebAssembly.JSTag } }); + // @ts-expect-error - a string is not a value that a module imports + WebAssembly.instantiate(compiled, { env: { thrown: "tag" } }); + // @ts-expect-error - a number is not a tag + exception.is(42); + // @ts-expect-error - an object is not a tag + new WebAssembly.Exception({}, []); + + // @ts-expect-error - the runtime ignores \`endings\` (#11796), so the option is not declared + new Blob(["a"], { endings: "native" });`, + { exactOptionalPropertyTypes: true }, + ); + }); + // TypeScript before 7 reads every construct signature of the base class for a class in a JavaScript file. + typeTest("a class in a checked JavaScript file extends ReadableStream", { + options: { checkJs: true }, + files: { + "streams-in-javascript.js": ` + /** @extends {ReadableStream} */ + export class Lines extends ReadableStream {} + /** @extends {ReadableStream} */ + export class Bytes extends ReadableStream { + constructor() { + super({ type: "bytes" }); + } + } + /** + * @template {Uint8Array} T + * @extends {ReadableStream} + */ + export class Chunks extends ReadableStream {} + export class Bare extends ReadableStream {} + + /** @type {ReadableStream>} */ + export const bytes = new ReadableStream({ type: "bytes" }); + /** @type {ReadableStream} */ + // @ts-expect-error - a byte source does not produce strings + export const text = new ReadableStream({ type: "bytes" }); + `, + }, + emptyInterfaces: expectedEmptyInterfacesWhenNoDOM, + diagnostics: [], + }); + }); + + // Runs on debug builds too, same as the Bun.mmap block above. + describe("Event and EventTarget", () => { test("lib.dom's composedPath() declaration wins when lib.dom is loaded", async () => { - await checkEventFixture( + await checkSource( "event-lib-dom-check", ["ESNext", "DOM"], `// lib.dom declares composedPath(): EventTarget[]. The Node-style tuple @@ -943,7 +1034,7 @@ describe("@types/bun integration test", () => { }); test("the Node-style composedPath() tuple applies without lib.dom", async () => { - await checkEventFixture( + await checkSource( "event-no-lib-dom-check", ["ESNext"], `declare const e: Event; diff --git a/test/integration/bun-types/fixture/fetch.ts b/test/integration/bun-types/fixture/fetch.ts index 86a8abbf7444..2678fe3ac842 100644 --- a/test/integration/bun-types/fixture/fetch.ts +++ b/test/integration/bun-types/fixture/fetch.ts @@ -387,3 +387,17 @@ if (typeof process !== "undefined") { // @ts-expect-error crl takes PEM contents fetch("https://example.com", { tls: { crl: 1 } }); } + +{ + // `new Request`, `Response.redirect`, `URL.canParse` and `URL.parse` take a URL object + const url = new URL("https://example.com"); + new Request(url); + new Request(url, { method: "POST" }); + new Request(url as string | URL | Request); + Response.redirect(url); + Response.redirect(url, 302); + URL.canParse(url); + URL.canParse("/path", url); + URL.parse(url); + URL.parse("/path", url); +} diff --git a/test/integration/bun-types/fixture/streams.ts b/test/integration/bun-types/fixture/streams.ts index 3ef5d426cf2c..2e749689d42d 100644 --- a/test/integration/bun-types/fixture/streams.ts +++ b/test/integration/bun-types/fixture/streams.ts @@ -101,3 +101,56 @@ new ReadableStream({ expectType(new ReadableStream({ type: "bytes" })).is>>(); expectType(new ReadableStream({ type: "bytes" })).is>(); expectType(new ReadableStream({} as any)).is>(); + +class StringStream extends ReadableStream {} +class ChunkStream extends ReadableStream {} +expectType(new StringStream()).extends>(); +expectType(new ChunkStream()).extends>(); + +class LineStream extends ReadableStream { + constructor(lines: string[]) { + super( + { + start(controller) { + expectType(controller).is>(); + for (const line of lines) controller.enqueue(line); + // @ts-expect-error - the chunks are strings + controller.enqueue(1); + controller.close(); + }, + }, + { highWaterMark: 1 }, + ); + } +} +new LineStream(["a"]); + +// @ts-expect-error - a byte source does not produce strings +const byteSourceAsStrings: ReadableStream = new ReadableStream({ type: "bytes" }); +// The chunk type of a byte stream can be a type parameter. +function byteStreamOf() { + return new ReadableStream({ type: "bytes" }); +} +class ByteStream extends ReadableStream { + constructor() { + super({ type: "bytes" }); + } +} +expectType(byteStreamOf>()).is>>(); +expectType(new ByteStream>()).extends>>(); +expectType(new ReadableStream({ type: "bytes" }, { highWaterMark: 1024 })).is>>(); +expectType(new ReadableStream({ type: "bytes" }, { highWaterMark: 1024 })).is>(); + +// An expected type that also allows other chunks, or other streams, takes a byte stream. +declare function readTextOrBytes(stream: ReadableStream>): void; +readTextOrBytes(new ReadableStream({ type: "bytes" })); +const textOrBytes: ReadableStream> = new ReadableStream({ type: "bytes" }); +const textStreamOrByteStream: ReadableStream | ReadableStream> = new ReadableStream({ + type: "bytes", +}); +function textOrByteStream(text: boolean): ReadableStream | ReadableStream> { + return text ? new ReadableStream() : new ReadableStream({ type: "bytes" }); +} +const byteStreams: Array> | ReadableStream> = [ + new ReadableStream({ type: "bytes" }), +]; diff --git a/test/integration/bun-types/fixture/wasm.ts b/test/integration/bun-types/fixture/wasm.ts index 3b8785ec7327..d1c54980a101 100644 --- a/test/integration/bun-types/fixture/wasm.ts +++ b/test/integration/bun-types/fixture/wasm.ts @@ -40,3 +40,27 @@ async () => { // Additional operations with instance, memory, and table can be performed here }; + +(error: unknown, instance: WebAssembly.Instance, module: WebAssembly.Module) => { + const tagType: WebAssembly.TagType = { parameters: ["i32", "f64"] }; + const tag: WebAssembly.Tag = new WebAssembly.Tag(tagType); + const options: WebAssembly.ExceptionOptions = { traceStack: true }; + const exception: WebAssembly.Exception = new WebAssembly.Exception(tag, [1, 2.5], options); + exception.is(tag) satisfies boolean; + exception.getArg(tag, 0); + exception.stack satisfies string | undefined; + WebAssembly.JSTag satisfies WebAssembly.Tag; + tag satisfies Bun.WebAssembly.Tag; + exception satisfies Bun.WebAssembly.Exception; + // A tag that a module exports. With lib.dom the type of an export has no tag, so the program casts. + exception.is(instance.exports.thrown as WebAssembly.Tag); + + if (error instanceof WebAssembly.Exception && error.is(WebAssembly.JSTag)) { + error.getArg(WebAssembly.JSTag, 0) satisfies number; + } + + // @ts-expect-error - a string is not a value that a module imports + WebAssembly.instantiate(module, { env: { thrown: "tag" } }); + // @ts-expect-error - "int" is not a value type + new WebAssembly.Tag({ parameters: ["int"] }); +};