From 81d4d8f4c4703b9816ed86b31b26d70250cff4ab Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 10 Nov 2026 03:28:02 +0000 Subject: [PATCH] Set the `length` of eleven Web API functions to the value of their Web IDL Web IDL sets the `length` of an operation to the length of its shortest argument list, and the `length` of an interface object to that of its constructor. These functions had another value: Blob.prototype.slice 2 -> 0 Blob.prototype.stream 1 -> 0 Request 0 -> 1 Request.prototype.clone 1 -> 0 Response.prototype.clone 1 -> 0 Response.json 0 -> 1 Response.redirect 0 -> 1 alert, confirm, prompt 1 -> 0 structuredClone 2 -> 1 Three functions hold a copy of the same constants and get the same values: `clone` of the request that a `Bun.serve` route receives, and `slice` and `stream` of a `BuildArtifact`. No call changes. `clearTimeout` and `clearInterval` keep 1: they are the exports of `node:timers`, which have 1 in Node.js. --- src/jsc/bindings/JSBunRequest.cpp | 2 +- src/jsc/bindings/ZigGlobalObject.lut.txt | 8 ++++---- src/runtime/api/JSBundler.bind.ts | 11 +++++++++-- src/runtime/webcore/Blob.bind.ts | 11 +++++++++-- src/runtime/webcore/response.bind.ts | 10 +++++++--- test/js/bun/http/bun-serve-routes.test.ts | 11 +++++++++++ test/js/web/fetch/blob.test.ts | 20 +++++++++++++++++++ test/js/web/fetch/response.test.ts | 24 ++++++++++++++++++++++- test/js/web/request/request.test.ts | 20 +++++++++++++++++++ test/js/web/web-globals.test.js | 9 +++++++++ 10 files changed, 113 insertions(+), 13 deletions(-) diff --git a/src/jsc/bindings/JSBunRequest.cpp b/src/jsc/bindings/JSBunRequest.cpp index 7ed749330fdd..819d2bbbb194 100644 --- a/src/jsc/bindings/JSBunRequest.cpp +++ b/src/jsc/bindings/JSBunRequest.cpp @@ -49,7 +49,7 @@ extern "C" void Bun__JSRequest__calculateEstimatedByteSize(void* requestPtr); static const HashTableValue JSBunRequestPrototypeValues[] = { { "params"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsJSBunRequestGetParams, nullptr } }, { "cookies"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsJSBunRequestGetCookies, nullptr } }, - { "clone"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsJSBunRequestClone, 1 } } + { "clone"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsJSBunRequestClone, 0 } } }; JSBunRequest* JSBunRequest::create(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr, JSObject* params) diff --git a/src/jsc/bindings/ZigGlobalObject.lut.txt b/src/jsc/bindings/ZigGlobalObject.lut.txt index da9881d762ad..cc7789da8b45 100644 --- a/src/jsc/bindings/ZigGlobalObject.lut.txt +++ b/src/jsc/bindings/ZigGlobalObject.lut.txt @@ -3,24 +3,24 @@ /* Source for ZigGlobalObject.lut.h @begin bunGlobalObjectTable addEventListener jsFunctionAddEventListener Function 2 - alert Bun::Bindgen::Generated::Prompt::jsAlert Function 1 + alert Bun::Bindgen::Generated::Prompt::jsAlert Function 0 atob functionATOB Function 1 btoa functionBTOA Function 1 clearImmediate functionClearImmediate Function 1 clearInterval functionClearInterval Function 1 clearTimeout functionClearTimeout Function 1 - confirm Bun::Bindgen::Generated::Prompt::jsConfirm Function 1 + confirm Bun::Bindgen::Generated::Prompt::jsConfirm Function 0 dispatchEvent jsFunctionDispatchEvent Function 1 fetch constructBunFetchObject PropertyCallback postMessage jsFunctionPostMessage Function 1 - prompt Bun::Bindgen::Generated::Prompt::jsPrompt Function 1 + prompt Bun::Bindgen::Generated::Prompt::jsPrompt Function 0 queueMicrotask functionQueueMicrotask Function 1 removeEventListener jsFunctionRemoveEventListener Function 2 reportError functionReportError Function 1 setImmediate createSetImmediateFunction PropertyCallback setInterval createSetIntervalFunction PropertyCallback setTimeout createSetTimeoutFunction PropertyCallback - structuredClone WebCore::jsFunctionStructuredClone Function 2 + structuredClone WebCore::jsFunctionStructuredClone Function 1 global GlobalObject_getGlobalThis PropertyCallback diff --git a/src/runtime/api/JSBundler.bind.ts b/src/runtime/api/JSBundler.bind.ts index 80ad470e6da0..672410d9e0cd 100644 --- a/src/runtime/api/JSBundler.bind.ts +++ b/src/runtime/api/JSBundler.bind.ts @@ -17,15 +17,22 @@ export const BuildArtifact = b.class({ loader: cached, path: { getter: b.string.utf8, cache: true }, size: { getter: b.f64 }, + /** `slice` and `stream` have the `length` that `Blob` gives them. */ slice: { ...js, args: { start: b.any, end: b.any, contentType: b.any }, returns: b.any.returning("Blob"), - length: 2, + length: 0, }, /** `Bun.build` fills the field of an output that has a source map, with `undefined` if nothing was written for the map. */ sourcemap: { getter: b.any.returning("BuildArtifact", "null", "undefined"), cache: true, capability: "heap" }, - stream: { ...js, args: { chunkSize: b.any }, returns: b.any.returning("ReadableStream"), passThis: true }, + stream: { + ...js, + args: { chunkSize: b.any }, + returns: b.any.returning("ReadableStream"), + passThis: true, + length: 0, + }, text: read, type: { getter: b.any.returning("string"), cache: true, capability: "js" }, }, diff --git a/src/runtime/webcore/Blob.bind.ts b/src/runtime/webcore/Blob.bind.ts index 287586f32b11..907c40e0c8bd 100644 --- a/src/runtime/webcore/Blob.bind.ts +++ b/src/runtime/webcore/Blob.bind.ts @@ -42,10 +42,17 @@ export const Blob = b.class({ ...js, args: { start: b.any, end: b.any, contentType: b.any }, returns: b.any.returning("Blob"), - length: 2, + length: 0, }, stat: { returns: b.any.returning("Promise", "undefined"), ...js }, - stream: { ...js, args: { chunkSize: b.any }, returns: b.any.returning("ReadableStream"), passThis: true }, + /** `chunkSize` is Bun's own. The `stream()` of the File API takes no argument. */ + stream: { + ...js, + args: { chunkSize: b.any }, + returns: b.any.returning("ReadableStream"), + passThis: true, + length: 0, + }, text: method, type: { getter: b.any.returning("string"), capability: "js" }, unlink, diff --git a/src/runtime/webcore/response.bind.ts b/src/runtime/webcore/response.bind.ts index f3a51e8c7cb2..82eb68ac3e71 100644 --- a/src/runtime/webcore/response.bind.ts +++ b/src/runtime/webcore/response.bind.ts @@ -22,7 +22,7 @@ const body = { textStream: { returns: b.any.returning("ReadableStream"), ...js }, } as const; const clone = (name: "Request" | "Response") => - ({ returns: b.any.returning(name), ...js, passThis: true, rustName: "do_clone", length: 1 }) as const; + ({ returns: b.any.returning(name), ...js, passThis: true, rustName: "do_clone" }) as const; export const Request = b.class({ rustPath: "crate::webcore::Request", @@ -34,7 +34,7 @@ export const Request = b.class({ estimatedSize: true, types: [table.viewType("Request"), table.exactType("$Bindgen.Request")], lockedMembers: ["prototype"], - construct: { args: { input: b.any, init: b.any }, ...js, passStructure: true, length: 0 }, + construct: { args: { input: b.any, init: b.any }, ...js, passStructure: true, length: 1 }, internalFields: ["stream"], proto: { arrayBuffer: body.arrayBuffer, @@ -62,7 +62,11 @@ export const Request = b.class({ }, }); -const factory = { args: { args: b.any.variadic }, returns: b.any.returning("Response"), ...js } as const; +/** + * The Fetch IDL requires the first argument of both, so `length` is 1. Bun takes a call without it, where the IDL + * and Node throw a TypeError. + */ +const factory = { args: { args: b.any.variadic }, returns: b.any.returning("Response"), ...js, length: 1 } as const; export const Response = b.class({ rustPath: "crate::webcore::Response", diff --git a/test/js/bun/http/bun-serve-routes.test.ts b/test/js/bun/http/bun-serve-routes.test.ts index b704de9915a1..a9a3b44f1cea 100644 --- a/test/js/bun/http/bun-serve-routes.test.ts +++ b/test/js/bun/http/bun-serve-routes.test.ts @@ -1124,3 +1124,14 @@ it("request.clone() copies a param whose name is an array index", async () => { const res = await fetch(`${server.url}users/42`); expect(await res.json()).toEqual({ keys: ["0", "id"], byIndex: "zero" }); }); + +it("request.clone, which is not Request.prototype.clone, has the length of the IDL", async () => { + using server = Bun.serve({ + port: 0, + routes: { + "/users/:id": req => Response.json({ own: req.clone !== Request.prototype.clone, length: req.clone.length }), + }, + }); + const res = await fetch(`${server.url}users/42`); + expect(await res.json()).toEqual({ own: true, length: 0 }); +}); diff --git a/test/js/web/fetch/blob.test.ts b/test/js/web/fetch/blob.test.ts index dc0c772681ef..c7ffab794d09 100644 --- a/test/js/web/fetch/blob.test.ts +++ b/test/js/web/fetch/blob.test.ts @@ -105,6 +105,26 @@ test("new Blob", () => { expect(blob.type).toBe(""); }); +test("Blob and File have the lengths of the IDL", () => { + expect({ + Blob: Blob.length, + File: File.length, + slice: Blob.prototype.slice.length, + stream: Blob.prototype.stream.length, + text: Blob.prototype.text.length, + arrayBuffer: Blob.prototype.arrayBuffer.length, + bytes: Blob.prototype.bytes.length, + }).toEqual({ Blob: 0, File: 2, slice: 0, stream: 0, text: 0, arrayBuffer: 0, bytes: 0 }); +}); + +// A BuildArtifact has a `slice` and a `stream` of its own. +test("the slice and stream of a BuildArtifact have the lengths of those of Blob", async () => { + using dir = tempDir("build-artifact-lengths", { "entry.js": "export default 1;\n" }); + const build = await Bun.build({ entrypoints: [path.join(String(dir), "entry.js")] }); + const [artifact] = build.outputs; + expect({ slice: artifact.slice.length, stream: artifact.stream.length }).toEqual({ slice: 0, stream: 0 }); +}); + test.each(["text/plain", "text/html", "text/css", "application/json", "application/javascript", "image/png"])( "type is the %s that the script gave", type => { diff --git a/test/js/web/fetch/response.test.ts b/test/js/web/fetch/response.test.ts index 82409e6b5f99..b455a6b92d4c 100644 --- a/test/js/web/fetch/response.test.ts +++ b/test/js/web/fetch/response.test.ts @@ -49,7 +49,7 @@ describe("2-arg form", () => { test("print size", () => { expect(normalizeBunSnapshot(Bun.inspect(new Response(Bun.file(import.meta.filename)))), import.meta.dir) .toMatchInlineSnapshot(` - "Response (8.0 KB) { + "Response (8.81 KB) { ok: true, url: "", status: 200, @@ -215,3 +215,25 @@ describe("clone()", () => { expect(clonedText).toBe("Hello, world!"); }); }); + +test("Response has the lengths of the IDL", () => { + const { prototype } = Response; + expect({ + Response: Response.length, + static: { error: Response.error.length, json: Response.json.length, redirect: Response.redirect.length }, + prototype: { + clone: prototype.clone.length, + arrayBuffer: prototype.arrayBuffer.length, + blob: prototype.blob.length, + bytes: prototype.bytes.length, + formData: prototype.formData.length, + json: prototype.json.length, + text: prototype.text.length, + textStream: prototype.textStream.length, + }, + }).toEqual({ + Response: 0, + static: { error: 0, json: 1, redirect: 1 }, + prototype: { clone: 0, arrayBuffer: 0, blob: 0, bytes: 0, formData: 0, json: 0, text: 0, textStream: 0 }, + }); +}); diff --git a/test/js/web/request/request.test.ts b/test/js/web/request/request.test.ts index 85e42bf8e420..bcb6ece43990 100644 --- a/test/js/web/request/request.test.ts +++ b/test/js/web/request/request.test.ts @@ -13,6 +13,26 @@ test("undefined args don't throw", () => { expect(request.method).toBe("GET"); }); +test("Request has the lengths of the IDL", () => { + const { prototype } = Request; + expect({ + Request: Request.length, + prototype: { + clone: prototype.clone.length, + arrayBuffer: prototype.arrayBuffer.length, + blob: prototype.blob.length, + bytes: prototype.bytes.length, + formData: prototype.formData.length, + json: prototype.json.length, + text: prototype.text.length, + textStream: prototype.textStream.length, + }, + }).toEqual({ + Request: 1, + prototype: { clone: 0, arrayBuffer: 0, blob: 0, bytes: 0, formData: 0, json: 0, text: 0, textStream: 0 }, + }); +}); + test("request can receive undefined signal", async () => { const request = new Request("http://example.com/", { method: "POST", diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js index 296c7125971a..bb48023b533c 100644 --- a/test/js/web/web-globals.test.js +++ b/test/js/web/web-globals.test.js @@ -355,6 +355,15 @@ test.concurrent.each(["userAgent", "platform", "hardwareConcurrency"])( }, ); +test("alert, confirm, prompt and structuredClone have the lengths of the IDL", () => { + expect({ + alert: alert.length, + confirm: confirm.length, + prompt: prompt.length, + structuredClone: structuredClone.length, + }).toEqual({ alert: 0, confirm: 0, prompt: 0, structuredClone: 1 }); +}); + test("confirm (yes) unix newline", async () => { const proc = spawn({ cmd: [bunExe(), require("path").join(import.meta.dir, "./confirm-fixture.js")],