From 8af3adeb260a19a5c925111edf77ce44083b18fb Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 11 Aug 2026 00:48:24 +0000 Subject: [PATCH 1/9] Make jsc::URL a re-export of bun_url with a UrlJsc extension trait --- src/jsc/URL.rs | 99 ++++++------------------------------ src/jsc/lib.rs | 6 +-- src/runtime/webcore/fetch.rs | 2 +- 3 files changed, 19 insertions(+), 88 deletions(-) diff --git a/src/jsc/URL.rs b/src/jsc/URL.rs index 36c2dc07e26d..67fa8981ed48 100644 --- a/src/jsc/URL.rs +++ b/src/jsc/URL.rs @@ -3,102 +3,33 @@ use core::ptr::NonNull; use bun_core::String; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; -bun_opaque::opaque_ffi! { - /// Opaque handle to a WebKit `WTF::URL` allocated on the C++ side. - pub struct URL; -} +// The JSC-agnostic surface (constructors, getters, `destroy`, the +// whole-string conversions) lives in `bun_url::whatwg`; only the entry +// points that need `JSValue`/`JSGlobalObject` stay in this crate, as the +// `UrlJsc` extension trait. +pub use bun_url::whatwg::URL; -// Getters take `&URL` (non-null `*const URL` at the C ABI; BunString.cpp never -// mutates the WTF::URL on read). `&mut String` for the in/out params is -// ABI-identical to non-null `*mut String`. `URL__deinit` consumes the C++ -// allocation, so it keeps a raw pointer and stays `unsafe fn`. unsafe extern "C" { safe fn URL__fromJS(value: JSValue, global: &JSGlobalObject) -> *mut URL; - safe fn URL__fromString(input: &mut String) -> *mut URL; - safe fn URL__protocol(url: &URL) -> String; - safe fn URL__username(url: &URL) -> String; - safe fn URL__password(url: &URL) -> String; - safe fn URL__host(url: &URL) -> String; - safe fn URL__port(url: &URL) -> u32; - fn URL__deinit(url: *mut URL); - safe fn URL__pathname(url: &URL) -> String; safe fn URL__getHrefFromJS(value: JSValue, global: &JSGlobalObject) -> String; - safe fn URL__getFileURLString(input: &mut String) -> String; - safe fn URL__pathFromFileURL(input: &mut String) -> String; } -impl URL { - pub fn file_url_from_string(str: String) -> String { - let mut input = str; - URL__getFileURLString(&mut input) - } - - pub fn path_from_file_url(str: String) -> String { - let mut input = str; - URL__pathFromFileURL(&mut input) - } +pub trait UrlJsc: Sized { + /// This percent-encodes the URL, punycode-encodes the hostname, and returns the result. + /// If it fails, the tag is marked Dead. + fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult; + /// Returns an owned C++ heap pointer that the caller must `destroy()`. + fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>>; +} - /// This percent-encodes the URL, punycode-encodes the hostname, and returns the result - /// If it fails, the tag is marked Dead +impl UrlJsc for URL { #[track_caller] - pub fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult { + fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult { crate::call_check_slow(global, || URL__getHrefFromJS(value, global)) } #[track_caller] - pub fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>> { + fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>> { crate::call_check_slow(global, || URL__fromJS(value, global)).map(NonNull::new) } - - pub fn from_utf8(input: &[u8]) -> Option> { - Self::from_string(String::borrow_utf8(input)) - } - - pub fn from_string(str: String) -> Option> { - let mut input = str; - NonNull::new(URL__fromString(&mut input)) - } - // from_js/from_string/from_utf8 return an owned C++ heap pointer that the - // caller must destroy(). - - pub fn protocol(&self) -> String { - URL__protocol(self) - } - - pub fn username(&self) -> String { - URL__username(self) - } - - pub fn password(&self) -> String { - URL__password(self) - } - - /// Returns the host WITHOUT the port. - /// - /// Note that this does NOT match JS behavior, which returns the host with the port. The - /// with-port form lives on the JSC-free shim as `bun_url::whatwg::URL::hostname`. - /// - /// ```text - /// URL("http://example.com:8080").host() => "example.com" - /// ``` - pub fn host(&self) -> String { - URL__host(self) - } - - /// Returns `u32::MAX` if the port is not set. Otherwise, `port` - /// is guaranteed to be within the `u16` range. - pub fn port(&self) -> u32 { - URL__port(self) - } - - // Kept as explicit destroy (not Drop) — URL is an opaque #[repr(C)] FFI - // handle constructed/destroyed across the C++ boundary. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: `this` is a valid *URL from C++; freed exactly once - unsafe { URL__deinit(this) } - } - - pub fn pathname(&self) -> String { - URL__pathname(self) - } } diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index b59ace1f259f..1bd701a603ca 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -833,7 +833,7 @@ mod __macro_smoke { // newtypes; the real opaque-FFI structs now live in their own files and are // surfaced here at the crate root. pub use self::dom_form_data::DOMFormData; -pub use self::url::URL; +pub use self::url::{URL, UrlJsc}; pub use self::zig_stack_frame::ZigStackFrame; pub use self::zig_stack_trace::ZigStackTrace; pub use abort_signal::{AbortSignal, AbortSignalRef}; @@ -1306,8 +1306,8 @@ impl FromJsEnum for bun_http_types::FetchCacheMode::FetchCacheMode { } } -// `URL::path_from_file_url` / `URL::href_from_js` live in `URL.rs` (the -// dedicated port file); the lib.rs copies were duplicate definitions. +// `URL` is a re-export of `bun_url::whatwg::URL`; the JS-value entry points +// (`UrlJsc::from_js` / `UrlJsc::href_from_js`) live in `URL.rs`. // JSString (real module in JSString.rs). #[path = "JSString.rs"] diff --git a/src/runtime/webcore/fetch.rs b/src/runtime/webcore/fetch.rs index 2a41b66e2cc4..8d000c2ee79a 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -55,7 +55,7 @@ use bun_core::{String as BunString, Tag as BunStringTag, ZigStringSlice}; use bun_http::{self as http, FetchRedirect, Headers, HeadersExt as _, MimeType}; use bun_http_jsc::method_jsc; use bun_http_types::Method::Method; -use bun_jsc::{HTTPHeaderName, StringJsc as _, SysErrorJsc as _}; +use bun_jsc::{HTTPHeaderName, StringJsc as _, SysErrorJsc as _, UrlJsc as _}; use bun_paths::{self, PathBuffer}; use bun_sys::FdExt as _; // `FromJsEnum for FetchRedirect` lives in bun_http_jsc; importing the impl crate From 0458ab64cb1f9d46ff75731483a4f11cefaa1827 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:04:41 +0000 Subject: [PATCH 2/9] Update src/CLAUDE.md URL section for the bun_url re-export --- src/CLAUDE.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/CLAUDE.md b/src/CLAUDE.md index 01e4d563a11d..05f7f13fd69a 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -172,11 +172,13 @@ url.host() // bun_core::String — the hostname WITHOUT the port (opposite url.port() // u32 (u32::MAX = unset; otherwise u16 range) ``` -`URL::href_from_js`, `URL::file_url_from_string`, `URL::path_from_file_url` -do whole-string conversions. The JSC-free shim `bun_url::whatwg::URL` exposes -`hostname()`, which returns the host WITH the port (also the opposite of JS -`hostname`) — so `bun_jsc::URL::host` and `bun_url::whatwg::URL::hostname` -are effectively swapped relative to their JS namesakes. +`bun_jsc::URL` is a re-export of `bun_url::whatwg::URL`; the JS-value entry +points (`href_from_js`, `from_js`) come from the `bun_jsc::UrlJsc` extension +trait. Whole-string conversions are free functions in `bun_url` +(`href_from_string`, `join`, `file_url_from_string`; the file-URL pair also +has by-value `URL::` associated forms). `whatwg::URL::hostname()` returns the +host WITH the port, so `host` and `hostname` are swapped relative to their JS +namesakes. ## MIME Types (`bun_http_types::MimeType`) From c891b85fe0f81a1b3441a2015e0e19a3283683ff Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:15:12 +0000 Subject: [PATCH 3/9] Trim redundant comments flagged in review --- src/jsc/URL.rs | 7 +------ src/jsc/lib.rs | 3 --- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/jsc/URL.rs b/src/jsc/URL.rs index 67fa8981ed48..2ff5229411c0 100644 --- a/src/jsc/URL.rs +++ b/src/jsc/URL.rs @@ -3,10 +3,6 @@ use core::ptr::NonNull; use bun_core::String; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; -// The JSC-agnostic surface (constructors, getters, `destroy`, the -// whole-string conversions) lives in `bun_url::whatwg`; only the entry -// points that need `JSValue`/`JSGlobalObject` stay in this crate, as the -// `UrlJsc` extension trait. pub use bun_url::whatwg::URL; unsafe extern "C" { @@ -15,8 +11,7 @@ unsafe extern "C" { } pub trait UrlJsc: Sized { - /// This percent-encodes the URL, punycode-encodes the hostname, and returns the result. - /// If it fails, the tag is marked Dead. + /// Percent-encoded href; invalid input yields a Dead-tagged string without throwing. fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult; /// Returns an owned C++ heap pointer that the caller must `destroy()`. fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>>; diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 1bd701a603ca..4130ae8da197 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -1306,9 +1306,6 @@ impl FromJsEnum for bun_http_types::FetchCacheMode::FetchCacheMode { } } -// `URL` is a re-export of `bun_url::whatwg::URL`; the JS-value entry points -// (`UrlJsc::from_js` / `UrlJsc::href_from_js`) live in `URL.rs`. - // JSString (real module in JSString.rs). #[path = "JSString.rs"] pub mod js_string; From 7b3793a2cdaddf1c17d17f0278996c369808d53a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:25:34 +0000 Subject: [PATCH 4/9] Add tests covering the fetch URL entry points moved to UrlJsc --- test/js/web/fetch/fetch-args.test.ts | 23 ++++++++++++++++++++++ test/js/web/fetch/fetch-preconnect.test.ts | 14 +++++++++++++ 2 files changed, 37 insertions(+) diff --git a/test/js/web/fetch/fetch-args.test.ts b/test/js/web/fetch/fetch-args.test.ts index bd8a779f0969..59f827423040 100644 --- a/test/js/web/fetch/fetch-args.test.ts +++ b/test/js/web/fetch/fetch-args.test.ts @@ -29,6 +29,11 @@ test("fetch(request subclass with headers)", async () => { expect(headers.get("hello")).toBe("world"); }); +test("fetch(URL object)", async () => { + const response = await fetch(new URL(server!.url)); + expect(response.status).toBe(200); +}); + test("fetch(RequestInit, headers)", async () => { const myRequest = { headers: { @@ -208,6 +213,24 @@ describe("does not send a request when", () => { expect(requestCount).toBe(prevCount); }); + test("Invalid proxy string", async () => { + const prevCount = requestCount; + const target = "http://" + server!.hostname + ":" + server!.port; + expect(async () => await fetch(target, { proxy: "://" })).toThrow("fetch() proxy URL is invalid"); + // Give it a chance to possibly send the request. + await Bun.sleep(2); + expect(requestCount).toBe(prevCount); + }); + + test("Invalid proxy object url", async () => { + const prevCount = requestCount; + const target = "http://" + server!.hostname + ":" + server!.port; + expect(async () => await fetch(target, { proxy: { url: "://" } })).toThrow("fetch() proxy URL is invalid"); + // Give it a chance to possibly send the request. + await Bun.sleep(2); + expect(requestCount).toBe(prevCount); + }); + test("proxy and unix", async () => { const prevCount = requestCount; expect(async () => await fetch(url, { proxy: url, unix: "/tmp/abc.sock" })).toThrow( diff --git a/test/js/web/fetch/fetch-preconnect.test.ts b/test/js/web/fetch/fetch-preconnect.test.ts index fa017e44ce5c..7af680b21a86 100644 --- a/test/js/web/fetch/fetch-preconnect.test.ts +++ b/test/js/web/fetch/fetch-preconnect.test.ts @@ -233,4 +233,18 @@ describe.concurrent.todoIf(isWindows)("fetch.preconnect", () => { expect(() => fetch.preconnect("unix:///tmp/foo")).toThrow(); expect(() => fetch.preconnect("http://:0")).toThrow(); }); + + it("fetch.preconnect stringifies non-string input", () => { + expect(() => fetch.preconnect("notaurl")).toThrow("Invalid URL"); + // A URL object goes through the same stringifier as a string, so it + // reaches the later port validation instead of failing URL parsing. + expect(() => fetch.preconnect(new URL("http://localhost:0"))).toThrow("Invalid port"); + expect(() => + fetch.preconnect({ + toString() { + throw new Error("boom from toString"); + }, + }), + ).toThrow("boom from toString"); + }); }); From d55136e22e358b3fb2b8dd163edadbe0752e3b70 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:06:14 +0000 Subject: [PATCH 5/9] ci: retrigger From 03f04da9a6620aff29595ab442e7c10434627729 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:15:39 +0000 Subject: [PATCH 6/9] Restore the whatwg URL surface the foundations dead-code trim removed, now live again via the jsc re-export --- src/url/lib.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/url/lib.rs b/src/url/lib.rs index 94860e3f1070..534ece178f93 100644 --- a/src/url/lib.rs +++ b/src/url/lib.rs @@ -64,11 +64,16 @@ pub mod whatwg { safe fn URL__fromString(str: &mut String) -> Option>; safe fn URL__protocol(url: &URL) -> String; safe fn URL__href(url: &URL) -> String; + safe fn URL__username(url: &URL) -> String; + safe fn URL__password(url: &URL) -> String; + safe fn URL__host(url: &URL) -> String; safe fn URL__hostname(url: &URL) -> String; + safe fn URL__port(url: &URL) -> u32; safe fn URL__deinit(url: &mut URL); safe fn URL__pathname(url: &URL) -> String; safe fn URL__getHref(input: &mut String) -> String; safe fn URL__getFileURLString(input: &mut String) -> String; + safe fn URL__pathFromFileURL(input: &mut String) -> String; safe fn URL__getHrefJoin(base: &mut String, relative: &mut String) -> String; safe fn URL__fragmentIdentifier(url: &URL) -> String; fn URL__originLength(latin1_slice: *const u8, len: usize) -> u32; @@ -96,6 +101,10 @@ pub mod whatwg { let mut input = *str; URL__getFileURLString(&mut input) } + pub fn path_from_file_url(str: &String) -> String { + let mut input = *str; + URL__pathFromFileURL(&mut input) + } /// Returns the origin (`scheme://host[:port]`) prefix of `slice` as a borrowed /// subslice, or `None` if `slice` does not parse as a valid WHATWG URL. /// @@ -114,12 +123,22 @@ pub mod whatwg { } impl URL { - pub(crate) fn from_string(str: &String) -> Option> { - let mut input = *str; + pub fn from_string(str: String) -> Option> { + let mut input = str; URL__fromString(&mut input) } pub fn from_utf8(input: &[u8]) -> Option> { - Self::from_string(&String::borrow_utf8(input)) + Self::from_string(String::borrow_utf8(input)) + } + /// By-value associated forms of the file-URL conversions, for callers + /// holding a `String` they are done with. + pub fn file_url_from_string(str: String) -> String { + let mut input = str; + URL__getFileURLString(&mut input) + } + pub fn path_from_file_url(str: String) -> String { + let mut input = str; + URL__pathFromFileURL(&mut input) } /// The URL fragment (the part after `#`), excluding the leading '#'. pub fn fragment_identifier(&self) -> String { @@ -142,12 +161,38 @@ pub mod whatwg { pub fn hostname(&self) -> String { URL__hostname(self) } + pub fn username(&self) -> String { + URL__username(self) + } + pub fn password(&self) -> String { + URL__password(self) + } + /// Returns the host WITHOUT the port (the opposite of JS `host`; the + /// with-port form is [`URL::hostname`]). + pub fn host(&self) -> String { + URL__host(self) + } + /// Returns `u32::MAX` if the port is not set. Otherwise, `port` + /// is guaranteed to be within the `u16` range. + pub fn port(&self) -> u32 { + URL__port(self) + } pub fn pathname(&self) -> String { URL__pathname(self) } pub fn deinit(&mut self) { URL__deinit(self) } + /// Raw-pointer form of [`URL::deinit`] for callers releasing an owned + /// `NonNull` (scopeguards). + /// + /// # Safety + /// `this` must be a valid owned pointer from `from_string`/`from_utf8`/ + /// `from_js`, freed exactly once. + pub unsafe fn destroy(this: *mut Self) { + // SAFETY: caller guarantees `this` is valid and owned. + unsafe { URL__deinit(&mut *this) } + } } } // Re-export the free helpers at crate root so lower-tier callers can write From 3cdeb7b851eaf160e3d15aa096614da62882118c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:16:46 +0000 Subject: [PATCH 7/9] Condense the restored URL doc comments --- src/url/lib.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/url/lib.rs b/src/url/lib.rs index 534ece178f93..cc356cf6fd59 100644 --- a/src/url/lib.rs +++ b/src/url/lib.rs @@ -130,8 +130,7 @@ pub mod whatwg { pub fn from_utf8(input: &[u8]) -> Option> { Self::from_string(String::borrow_utf8(input)) } - /// By-value associated forms of the file-URL conversions, for callers - /// holding a `String` they are done with. + /// By-value associated forms of the free file-URL conversions. pub fn file_url_from_string(str: String) -> String { let mut input = str; URL__getFileURLString(&mut input) @@ -167,13 +166,11 @@ pub mod whatwg { pub fn password(&self) -> String { URL__password(self) } - /// Returns the host WITHOUT the port (the opposite of JS `host`; the - /// with-port form is [`URL::hostname`]). + /// The host WITHOUT the port (opposite of JS `host`; with-port form is [`URL::hostname`]). pub fn host(&self) -> String { URL__host(self) } - /// Returns `u32::MAX` if the port is not set. Otherwise, `port` - /// is guaranteed to be within the `u16` range. + /// `u32::MAX` when the port is unset; otherwise within the `u16` range. pub fn port(&self) -> u32 { URL__port(self) } @@ -183,12 +180,10 @@ pub mod whatwg { pub fn deinit(&mut self) { URL__deinit(self) } - /// Raw-pointer form of [`URL::deinit`] for callers releasing an owned - /// `NonNull` (scopeguards). + /// Raw-pointer form of [`URL::deinit`]. /// /// # Safety - /// `this` must be a valid owned pointer from `from_string`/`from_utf8`/ - /// `from_js`, freed exactly once. + /// `this` must be an owned pointer from `from_string`/`from_utf8`/`from_js`, freed once. pub unsafe fn destroy(this: *mut Self) { // SAFETY: caller guarantees `this` is valid and owned. unsafe { URL__deinit(&mut *this) } From b346c211e7527a339f2817f24c29d6acce155c1f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:37:31 +0000 Subject: [PATCH 8/9] Use the describe-scoped url in the proxy tests and drop the uncalled free path_from_file_url --- src/url/lib.rs | 6 +----- test/js/web/fetch/fetch-args.test.ts | 6 ++---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/url/lib.rs b/src/url/lib.rs index cc356cf6fd59..8148d131a086 100644 --- a/src/url/lib.rs +++ b/src/url/lib.rs @@ -101,10 +101,6 @@ pub mod whatwg { let mut input = *str; URL__getFileURLString(&mut input) } - pub fn path_from_file_url(str: &String) -> String { - let mut input = *str; - URL__pathFromFileURL(&mut input) - } /// Returns the origin (`scheme://host[:port]`) prefix of `slice` as a borrowed /// subslice, or `None` if `slice` does not parse as a valid WHATWG URL. /// @@ -130,7 +126,7 @@ pub mod whatwg { pub fn from_utf8(input: &[u8]) -> Option> { Self::from_string(String::borrow_utf8(input)) } - /// By-value associated forms of the free file-URL conversions. + /// By-value whole-string file-URL conversions. pub fn file_url_from_string(str: String) -> String { let mut input = str; URL__getFileURLString(&mut input) diff --git a/test/js/web/fetch/fetch-args.test.ts b/test/js/web/fetch/fetch-args.test.ts index 59f827423040..9acda321a914 100644 --- a/test/js/web/fetch/fetch-args.test.ts +++ b/test/js/web/fetch/fetch-args.test.ts @@ -215,8 +215,7 @@ describe("does not send a request when", () => { test("Invalid proxy string", async () => { const prevCount = requestCount; - const target = "http://" + server!.hostname + ":" + server!.port; - expect(async () => await fetch(target, { proxy: "://" })).toThrow("fetch() proxy URL is invalid"); + expect(async () => await fetch(url, { proxy: "://" })).toThrow("fetch() proxy URL is invalid"); // Give it a chance to possibly send the request. await Bun.sleep(2); expect(requestCount).toBe(prevCount); @@ -224,8 +223,7 @@ describe("does not send a request when", () => { test("Invalid proxy object url", async () => { const prevCount = requestCount; - const target = "http://" + server!.hostname + ":" + server!.port; - expect(async () => await fetch(target, { proxy: { url: "://" } })).toThrow("fetch() proxy URL is invalid"); + expect(async () => await fetch(url, { proxy: { url: "://" } })).toThrow("fetch() proxy URL is invalid"); // Give it a chance to possibly send the request. await Bun.sleep(2); expect(requestCount).toBe(prevCount); From 739eb492cd23aea33426a6cb57baabfb2b7baa95 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:55:38 +0000 Subject: [PATCH 9/9] Declare whatwg::URL with opaque_ffi so the jsc re-export keeps its auto-trait opt-outs --- Cargo.lock | 1 + src/url/Cargo.toml | 1 + src/url/lib.rs | 9 ++++----- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c8ed1083cc9..132bda70c628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2076,6 +2076,7 @@ dependencies = [ "bun_alloc", "bun_collections", "bun_core", + "bun_opaque", "bun_paths", "bun_wyhash", "const_format", diff --git a/src/url/Cargo.toml b/src/url/Cargo.toml index 3ecf09213d22..5b912414efaf 100644 --- a/src/url/Cargo.toml +++ b/src/url/Cargo.toml @@ -21,6 +21,7 @@ libc.workspace = true bitflags.workspace = true bun_alloc.workspace = true bun_core.workspace = true +bun_opaque.workspace = true bun_collections.workspace = true # TODO(b1): bun_io gated — crate does not compile yet; local Write stub in lib.rs # bun_io.workspace = true diff --git a/src/url/lib.rs b/src/url/lib.rs index 8148d131a086..587867dafc7a 100644 --- a/src/url/lib.rs +++ b/src/url/lib.rs @@ -45,11 +45,10 @@ pub mod whatwg { use super::BunString as String; use super::strings; - /// Opaque handle to a heap-allocated WTF::URL (C++). Always behind `*mut URL`. - /// Construct via `from_string`/`from_utf8`; free via `deinit`. - #[repr(C)] - pub struct URL { - _opaque: [u8; 0], + bun_opaque::opaque_ffi! { + /// Opaque handle to a heap-allocated WTF::URL (C++). Always behind `*mut URL`. + /// Construct via `from_string`/`from_utf8`; free via `deinit`. + pub struct URL; } // Getters take `*const URL` — the C++ side (BunString.cpp) never mutates the