From 64efcaf6d5ebe4628d2cae5ed78d1c31f1fdc7f8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:16:05 +0000 Subject: [PATCH 1/5] Bun.build: stop blocking the event loop on an async plugin setup() Config::from_js ran each plugin's setup() during synchronous config parsing and, when runSetupFunction returned a promise that was still pending, called waitForPromise to spin the event loop until it settled. That re-enters the event loop from whatever callback called Bun.build (the bug class tracked in #33261) and also deadlocks deterministically when the code that would settle the promise is later on the same stack. Bun.build already returns a promise, and the runSetupFunction builtin already folds a pending async setup() together with any pending onStart() promises into the one promise it hands back to Rust. So: split Config::from_js at the plugin loop, and when a step's promise is still pending, park the parse state in a heap DeferredBuild and finish the chain, the rest of the config parse, and the bundle scheduling from that promise's .then continuation. The synchronous path (no plugins, or every setup() and onStart() settled before returning) is unchanged. The only observable difference is on the path that previously blocked: a config-parse error raised after an async setup() settles now rejects Bun.build's promise instead of throwing synchronously, and a rejecting async setup() does the same. --- src/jsc/bindings/ZigGlobalObject.cpp | 4 + src/jsc/bindings/ZigGlobalObject.h | 4 +- src/jsc/bindings/headers.h | 3 + src/runtime/api/JSBundler.rs | 455 +++++++++++++++--- .../bun-build-plugin-async-setup.test.ts | 198 ++++++++ 5 files changed, 584 insertions(+), 80 deletions(-) create mode 100644 test/bundler/bun-build-plugin-async-setup.test.ts diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index a45cf0016dfe..c5a93f71bce0 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -4080,6 +4080,10 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h return GlobalObject::PromiseFunctions::Bun__HTTPRequestContextDebugH3__onResolve; } else if (handler == Bun__HTTPRequestContextDebugH3__onResolveStream) { return GlobalObject::PromiseFunctions::Bun__HTTPRequestContextDebugH3__onResolveStream; + } else if (handler == Bun__JSBundler__onResolvePluginSetup) { + return GlobalObject::PromiseFunctions::Bun__JSBundler__onResolvePluginSetup; + } else if (handler == Bun__JSBundler__onRejectPluginSetup) { + return GlobalObject::PromiseFunctions::Bun__JSBundler__onRejectPluginSetup; } else { RELEASE_ASSERT_NOT_REACHED(); } diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index ca759a74da9e..1c61279bb4d4 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -412,8 +412,10 @@ class GlobalObject : public Bun::GlobalScope { Bun__HTTPRequestContextDebugH3__onRejectStream, Bun__HTTPRequestContextDebugH3__onResolve, Bun__HTTPRequestContextDebugH3__onResolveStream, + Bun__JSBundler__onResolvePluginSetup, + Bun__JSBundler__onRejectPluginSetup, }; - static constexpr size_t promiseFunctionsSize = 42; + static constexpr size_t promiseFunctionsSize = 44; static PromiseFunctions promiseHandlerID(SYSV_ABI EncodedJSValue (*handler)(JSC::JSGlobalObject* arg0, JSC::CallFrame* arg1)); diff --git a/src/jsc/bindings/headers.h b/src/jsc/bindings/headers.h index ab056ebc60bd..3356b5dc1b3d 100644 --- a/src/jsc/bindings/headers.h +++ b/src/jsc/bindings/headers.h @@ -719,6 +719,9 @@ ZIG_DECL JSC::EncodedJSValue Bun__Timer__setImmediate(JSC::JSGlobalObject* globa BUN_DECLARE_HOST_FUNCTION(BunServe__onResolvePlugins); BUN_DECLARE_HOST_FUNCTION(BunServe__onRejectPlugins); +BUN_DECLARE_HOST_FUNCTION(Bun__JSBundler__onResolvePluginSetup); +BUN_DECLARE_HOST_FUNCTION(Bun__JSBundler__onRejectPluginSetup); + #endif #ifdef __cplusplus diff --git a/src/runtime/api/JSBundler.rs b/src/runtime/api/JSBundler.rs index cfbd22382da3..cadc5083f15b 100644 --- a/src/runtime/api/JSBundler.rs +++ b/src/runtime/api/JSBundler.rs @@ -434,12 +434,56 @@ pub mod js_bundler { } } + /// One resolved step of the plugin `setup()` chain. + enum SetupStep { + /// `runSetupFunction` (and, for the last plugin, its `onStart()` + /// promises) settled synchronously; carries the updated promise array + /// to thread into the next plugin's `runSetupFunction` call. + Settled(JSValue), + /// `runSetupFunction` returned a promise that is still pending: an + /// async `setup()` that suspended on an `await`, or the last plugin's + /// `Promise.all` over pending `onStart()` promises. + Pending(JSValue), + } + + /// Where [`Config::from_js`] stopped when a plugin `setup()` yielded a + /// promise that had not settled yet. `build` turns this into a + /// [`DeferredBuild`] and continues from the promise's `.then` + /// continuation. + pub struct SuspendedPluginSetup { + /// The still-pending promise `runSetupFunction` returned. + pending: JSValue, + /// `config.plugins`, snapshotted before any `setup()` ran so a getter + /// is not re-invoked on resume. + plugins_array: jsc::Strong, + /// Plugin index to resume the chain from. + next_index: u32, + /// Snapshot of the plugin array length. + length: u32, + plugin_target: jsc::BunPluginTarget, + /// The partially-parsed config: only the pre-plugin `target` fields + /// are set. The rest is parsed by `finish_from_js` once every + /// `setup()` has settled, because plugins may mutate the config + /// object before then. + config: Config, + did_set_target: bool, + } + impl Config { + /// Parses `Bun.build`'s config object. + /// + /// Returns `Ok(None)` when a plugin's `setup()` (or the last plugin's + /// pending `onStart()` promises) yielded a promise that is still + /// pending. `*suspended` then records where the chain stopped and + /// owns the partially-parsed config; the caller must continue from + /// that promise's `.then` continuation instead of re-entering the + /// event loop (#33261). pub fn from_js( global_this: &JSGlobalObject, config: JSValue, plugins: &mut Option<*mut Plugin>, - ) -> JsResult { + suspended: &mut Option, + ) -> JsResult> { // Config implements Drop, so functional-record-update from Default::default() // is rejected by rustc (E0509). Construct default then mutate instead. let mut this = Config::default(); @@ -481,94 +525,163 @@ pub mod js_bundler { // Plugins must be resolved first as they are allowed to mutate the config JSValue if let Some(array) = config.get_array(global_this, "plugins")? { - let length = array.get_length(global_this)?; - let mut iter = array.array_iterator(global_this)?; + let length = array.get_length(global_this)? as u32; + let plugin_target = match this.target { + Target::Bun | Target::BunMacro => jsc::BunPluginTarget::Bun, + Target::Node => jsc::BunPluginTarget::Node, + _ => jsc::BunPluginTarget::Browser, + }; let mut onstart_promise_array = JSValue::UNDEFINED; - let mut i: usize = 0; - while let Some(plugin) = iter.next()? { - if !plugin.is_object() { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected plugin to be an object" - ))); - } - - if let Some(slice) = plugin.get_optional_slice(global_this, b"name")? { - if slice.slice().is_empty() { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected plugin to have a non-empty name" - ))); + for index in 0..length { + match Self::run_one_plugin_setup( + global_this, + config, + &mut **plugins, + plugin_target, + array, + index, + length, + onstart_promise_array, + )? { + SetupStep::Settled(value) => onstart_promise_array = value, + SetupStep::Pending(pending) => { + // An async `setup()` (or the last plugin's pending + // `onStart()` promises) has not settled. Never + // re-enter the event loop from here (#33261): + // record where the chain stopped so `build` can + // run the remaining plugins, and the rest of the + // config parse, from this promise's `.then` + // continuation. + *suspended = Some(SuspendedPluginSetup { + pending, + plugins_array: jsc::Strong::create(array, global_this), + next_index: index + 1, + length, + plugin_target, + config: this, + did_set_target, + }); + // Ownership of the plugin registry transfers to + // the continuation; disarm the error cleanup. + scopeguard::ScopeGuard::into_inner(plugins); + return Ok(None); } - drop(slice); - } else { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected plugin to have a name" - ))); } + } + } - let Some(function) = plugin.get_function(global_this, b"setup")? else { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected plugin to have a setup() function" - ))); - }; + let this = Self::finish_from_js(global_this, config, this, did_set_target)?; + scopeguard::ScopeGuard::into_inner(plugins); + Ok(Some(this)) + } - let bun_plugins: *mut Plugin = match **plugins { - Some(p) => p, - None => { - let p = Plugin::create( - global_this, - match this.target { - Target::Bun | Target::BunMacro => jsc::BunPluginTarget::Bun, - Target::Node => jsc::BunPluginTarget::Node, - _ => jsc::BunPluginTarget::Browser, - }, - ); - **plugins = Some(p); - p - } - }; + /// Validate plugin `index` of the `plugins` array, lazily create the + /// shared plugin registry, and invoke the plugin's `setup()` through + /// the `runSetupFunction` builtin. + /// + /// `runSetupFunction` stores the `onStart()` promise array it is + /// given on the plugin registry before calling `setup()`, so the + /// chain has to advance one plugin at a time with the previous + /// plugin's settled result. + #[allow(clippy::too_many_arguments)] + fn run_one_plugin_setup( + global_this: &JSGlobalObject, + config: JSValue, + plugins: &mut Option<*mut Plugin>, + plugin_target: jsc::BunPluginTarget, + array: JSValue, + index: u32, + length: u32, + onstart_promise_array: JSValue, + ) -> JsResult { + let plugin = array.get_index(global_this, index)?; + if !plugin.is_object() { + return Err(global_this.throw_invalid_arguments(format_args!( + "Expected plugin to be an object" + ))); + } - let is_last = i == (length as usize).saturating_sub(1); - // SAFETY: bun_plugins is a valid pointer created/stored above - let mut plugin_result = unsafe { - (*bun_plugins).add_plugin( - function, - config, - onstart_promise_array, - is_last, - false, - )? - }; + if let Some(slice) = plugin.get_optional_slice(global_this, b"name")? { + if slice.slice().is_empty() { + return Err(global_this.throw_invalid_arguments(format_args!( + "Expected plugin to have a non-empty name" + ))); + } + drop(slice); + } else { + return Err(global_this.throw_invalid_arguments(format_args!( + "Expected plugin to have a name" + ))); + } - if !plugin_result.is_empty_or_undefined_or_null() { - if let Some(promise) = plugin_result.as_any_promise() { - promise.set_handled(global_this.vm()); - // SAFETY: bun_vm() returns the live process VirtualMachine pointer. - global_this.bun_vm().as_mut().wait_for_promise(promise); - match promise - .unwrap(global_this.vm(), jsc::PromiseUnwrapMode::MarkHandled) - { - jsc::PromiseResult::Pending => unreachable!(), - jsc::PromiseResult::Fulfilled(val) => { - plugin_result = val; - } - jsc::PromiseResult::Rejected(err) => { - return Err(global_this.throw_value(err)); - } - } - } - } + let Some(function) = plugin.get_function(global_this, b"setup")? else { + return Err(global_this.throw_invalid_arguments(format_args!( + "Expected plugin to have a setup() function" + ))); + }; - if let Some(err) = plugin_result.to_error() { - return Err(global_this.throw_value(err)); - } else if global_this.has_exception() { - return Err(JsError::Thrown); - } + let bun_plugins: *mut Plugin = match *plugins { + Some(p) => p, + None => { + let p = Plugin::create(global_this, plugin_target); + *plugins = Some(p); + p + } + }; - onstart_promise_array = plugin_result; - i += 1; + let is_last = index + 1 == length; + // SAFETY: bun_plugins is a valid pointer created/stored above + let mut plugin_result = unsafe { + (*bun_plugins).add_plugin( + function, + config, + onstart_promise_array, + is_last, + false, + )? + }; + + if !plugin_result.is_empty_or_undefined_or_null() { + if let Some(promise) = plugin_result.as_any_promise() { + promise.set_handled(global_this.vm()); + if matches!(promise.status(), jsc::js_promise::Status::Pending) { + // Never block the event loop waiting for this promise + // (#33261): the caller attaches `.then` reactions and + // continues the chain from there. + return Ok(SetupStep::Pending(plugin_result)); + } + match promise.unwrap(global_this.vm(), jsc::PromiseUnwrapMode::MarkHandled) { + jsc::PromiseResult::Pending => unreachable!(), + jsc::PromiseResult::Fulfilled(val) => { + plugin_result = val; + } + jsc::PromiseResult::Rejected(err) => { + return Err(global_this.throw_value(err)); + } + } } } + if let Some(err) = plugin_result.to_error() { + return Err(global_this.throw_value(err)); + } else if global_this.has_exception() { + return Err(JsError::Thrown); + } + + Ok(SetupStep::Settled(plugin_result)) + } + + /// Everything `from_js` parses after the plugin `setup()` chain has + /// settled. Split out so that, when a `setup()` suspends, the + /// remainder can run in its `.then` continuation: plugins are + /// allowed to mutate the config object before their promise settles, + /// so none of these fields may be read earlier. + fn finish_from_js( + global_this: &JSGlobalObject, + config: JSValue, + mut this: Config, + did_set_target: bool, + ) -> JsResult { if let Some(macros_flag) = config.get_boolean_loose(global_this, "macros")? { this.no_macros = !macros_flag; } @@ -1278,7 +1391,6 @@ pub mod js_bundler { } } - scopeguard::ScopeGuard::into_inner(plugins); Ok(this) } } @@ -1337,7 +1449,39 @@ pub mod js_bundler { } let mut plugins: Option<*mut Plugin> = None; - let config = Config::from_js(global_this, arguments[0], &mut plugins)?; + let mut suspended: Option = None; + let Some(config) = + Config::from_js(global_this, arguments[0], &mut plugins, &mut suspended)? + else { + // A plugin `setup()` (or the last plugin's `onStart()` promises) + // suspended on a promise that has not settled. `Bun.build` + // already returns a promise, so run the remaining plugins, finish + // the config parse, and schedule the bundle from that promise's + // `.then` continuation instead of re-entering the event loop + // (#33261). + let suspended = suspended.expect("from_js returned None without a suspension"); + let result = jsc::JSPromiseStrong::init(global_this); + let return_value = result.value(); + let pending = suspended.pending; + let deferred = bun_core::heap::into_raw(Box::new(DeferredBuild { + config_js: jsc::Strong::create(arguments[0], global_this), + plugins_array: suspended.plugins_array, + next_index: suspended.next_index, + length: suspended.length, + plugin_target: suspended.plugin_target, + plugin: plugins, + partial: Some(suspended.config), + did_set_target: suspended.did_set_target, + result, + })); + pending.then( + global_this, + deferred, + __jsc_host_on_deferred_build_resolve, + __jsc_host_on_deferred_build_reject, + ); + return Ok(return_value); + }; let event_loop = vm.event_loop(); @@ -1370,6 +1514,159 @@ pub mod js_bundler { build(global_this, callframe.arguments()) } + /// A `Bun.build` whose plugin `setup()` chain suspended on a pending + /// promise. + /// + /// Leaked with `heap::into_raw` and handed to the promise's `.then` + /// reactions as their context pointer; exactly one of the two reactions + /// runs and reclaims it (or leaks it again if the chain suspends on a + /// later plugin). + struct DeferredBuild { + /// `Bun.build(config)`'s config argument; the rest of the parse reads + /// it after the chain settles. + config_js: jsc::Strong, + /// See [`SuspendedPluginSetup::plugins_array`]. + plugins_array: jsc::Strong, + next_index: u32, + length: u32, + plugin_target: jsc::BunPluginTarget, + /// The plugin registry, owned here until it either transfers to the + /// completion task (success) or is destroyed (failure). + plugin: Option<*mut Plugin>, + /// The partially-parsed config (pre-plugin `target` fields only); + /// `None` once it has moved into the completion task. + partial: Option, + did_set_target: bool, + /// `Bun.build`'s result promise. Moved into the completion task on + /// success, rejected here on failure. + result: jsc::JSPromiseStrong, + } + + impl DeferredBuild { + /// Continue the plugin `setup()` chain from `next_index` with the + /// value the last pending step settled to, then, once every plugin + /// has settled, finish the config parse and schedule the bundle. + /// + /// Returns `Some(pending)` when the chain suspended again on a later + /// plugin's promise. + fn resume( + &mut self, + global_this: &JSGlobalObject, + mut onstart_promise_array: JSValue, + ) -> JsResult> { + // The synchronous chain rejects when a step settles to an Error; + // preserve that for a step that settled through `.then`. + if let Some(err) = onstart_promise_array.to_error() { + return Err(global_this.throw_value(err)); + } + + let array = self.plugins_array.get(); + while self.next_index < self.length { + let index = self.next_index; + self.next_index += 1; + match Config::run_one_plugin_setup( + global_this, + self.config_js.get(), + &mut self.plugin, + self.plugin_target, + array, + index, + self.length, + onstart_promise_array, + )? { + SetupStep::Settled(value) => onstart_promise_array = value, + SetupStep::Pending(pending) => return Ok(Some(pending)), + } + } + + let partial = self.partial.take().expect("DeferredBuild resumed after completion"); + let config = + Config::finish_from_js(global_this, self.config_js.get(), partial, self.did_set_target)?; + + let event_loop = global_this.bun_vm().event_loop(); + let completion = + crate::api::js_bundle_completion_task::create_and_schedule_completion_task( + config, + self.plugin.take().and_then(core::ptr::NonNull::new), + global_this, + event_loop, + ) + .map_err(|_| JsError::OutOfMemory)?; + // SAFETY: `completion` is the freshly-boxed allocation returned + // above; sole owner on the JS thread until the enqueued task runs. + unsafe { + (*completion).promise = self.result.take(); + } + Ok(None) + } + + /// Reject `Bun.build`'s result promise with the pending exception and + /// destroy the plugin registry. + fn fail(&mut self, global_this: &JSGlobalObject, err: JsError) { + if let Some(plugin) = self.plugin.take() { + Plugin::destroy(plugin); + } + if matches!(err, JsError::Terminated) { + // The VM is terminating; leave the termination exception in + // place and don't settle the promise. + return; + } + let exception = global_this.take_exception(err); + let _ = self.result.reject(global_this, Ok(exception)); + } + } + + /// `.then` resolve reaction for a suspended plugin `setup()` chain: the + /// step's settled value (the `onStart()` promise array) arrives as the + /// first argument. + #[bun_jsc::host_fn(export = "Bun__JSBundler__onResolvePluginSetup")] + fn on_deferred_build_resolve( + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + let [onstart_promise_array, ctx] = callframe.arguments_as_array::<2>(); + let deferred: *mut DeferredBuild = ctx.as_promise_ptr::(); + // SAFETY: `deferred` was leaked via `heap::into_raw` by `build` (or a + // previous re-suspension) and exactly one of the two `.then` + // reactions runs; reclaiming it here is the unique owner transition. + let mut this: Box = unsafe { bun_core::heap::take(deferred) }; + match this.resume(global_this, onstart_promise_array) { + Ok(Some(pending)) => { + // Another plugin suspended; hand the allocation to the next + // pair of reactions. + let deferred = bun_core::heap::into_raw(this); + pending.then( + global_this, + deferred, + __jsc_host_on_deferred_build_resolve, + __jsc_host_on_deferred_build_reject, + ); + } + Ok(None) => {} + Err(err) => this.fail(global_this, err), + } + Ok(JSValue::UNDEFINED) + } + + /// `.then` reject reaction for a suspended plugin `setup()` chain: a + /// plugin's async `setup()` (or one of its `onStart()` callbacks) + /// rejected. + #[bun_jsc::host_fn(export = "Bun__JSBundler__onRejectPluginSetup")] + fn on_deferred_build_reject( + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + let [error, ctx] = callframe.arguments_as_array::<2>(); + let deferred: *mut DeferredBuild = ctx.as_promise_ptr::(); + // SAFETY: see `on_deferred_build_resolve`. + let mut this: Box = unsafe { bun_core::heap::take(deferred) }; + if let Some(plugin) = this.plugin.take() { + Plugin::destroy(plugin); + } + let _ = this.result.reject(global_this, Ok(error)); + Ok(JSValue::UNDEFINED) + } + // NOTE: `Resolve`/`Load`/`MiniImportRecord`/etc. are owned by // `bun_bundler::bundle_v2::api::JSBundler` so that `BundleV2` can operate // on them directly (`on_resolve_async`/`on_load_async`). `dispatch()` and diff --git a/test/bundler/bun-build-plugin-async-setup.test.ts b/test/bundler/bun-build-plugin-async-setup.test.ts new file mode 100644 index 000000000000..4451742b37f0 --- /dev/null +++ b/test/bundler/bun-build-plugin-async-setup.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +// `Bun.build({ plugins })` used to run each plugin's `setup()` during +// synchronous config parsing and, when `setup()` returned a promise that was +// still pending, spin the whole event loop (`waitForPromise`) until it +// settled. Synchronously re-entering the event loop from inside whatever +// callback called `Bun.build` is the bug class tracked in +// https://github.com/oven-sh/bun/issues/33261: a nested tick run from inside +// a ready-poll dispatch can clobber the shared ready-poll batch and lose +// one-shot events. It was also a deterministic hang on its own, because the +// nested loop blocks the very JS frame that would have settled the promise. +// +// `Bun.build` now returns without blocking: the remaining plugins and the +// rest of the config parse run in the pending promise's `.then` continuation. +describe("Bun.build with an async plugin setup()", () => { + const entry = `export const hello = "world";\n`; + + test( + "returns before a pending setup() promise settles", + async () => { + using dir = tempDir("bun-build-async-setup", { + "entry.js": entry, + "build-fixture.ts": /* ts */ ` + let release!: () => void; + const gate = new Promise(resolve => (release = resolve)); + const order: string[] = []; + + const built = Bun.build({ + entrypoints: ["./entry.js"], + plugins: [ + { + name: "gated", + async setup(build) { + order.push("setup:start"); + await gate; + order.push("setup:resume"); + // A filter registered after the await must still be + // installed: they only take effect once setup() settles. + build.onLoad({ filter: /entry\\.js$/ }, () => ({ + contents: "export const hello = 'patched';", + loader: "js", + })); + }, + }, + ], + }); + + // Unreachable when Bun.build spins the event loop: release() is the + // only thing that can settle the promise it would be waiting on. + order.push("returned"); + release(); + + const result = await built; + order.push("built"); + if (!result.success) throw new AggregateError(result.logs, "build failed"); + const text = await result.outputs[0].text(); + if (!text.includes("patched")) { + throw new Error("onLoad registered after an await was ignored:\\n" + text); + } + console.log(JSON.stringify(order)); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "build-fixture.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + // Only reached when Bun.build blocks; the non-blocking path exits in + // well under a second. + timeout: 15_000, + killSignal: "SIGKILL", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + proc.stdout.text(), + proc.stderr.text(), + proc.exited, + ]); + expect(exitCode === 0 ? JSON.parse(stdout.trim()) : { exitCode, stdout, stderr }).toEqual([ + "setup:start", + "returned", + "setup:resume", + "built", + ]); + }, + 30_000, + ); + + test("still waits for a setup() that suspends on I/O", async () => { + using dir = tempDir("bun-build-async-setup-io", { "entry.js": entry }); + let setupResumed = false; + const result = await Bun.build({ + entrypoints: [`${dir}/entry.js`], + plugins: [ + { + name: "sleepy", + async setup() { + await Bun.sleep(10); + setupResumed = true; + }, + }, + ], + }); + expect(setupResumed).toBe(true); + expect(result.success).toBe(true); + }); + + test("still waits for pending onStart() promises before loading any file", async () => { + using dir = tempDir("bun-build-async-setup-onstart", { "entry.js": entry }); + const order: string[] = []; + const result = await Bun.build({ + entrypoints: [`${dir}/entry.js`], + plugins: [ + { + name: "starter", + setup(build) { + build.onStart(async () => { + await Bun.sleep(10); + order.push("onStart"); + }); + build.onLoad({ filter: /entry\.js$/ }, () => { + order.push("onLoad"); + return { contents: entry, loader: "js" }; + }); + }, + }, + ], + }); + expect(result.success).toBe(true); + expect(order).toEqual(["onStart", "onLoad"]); + }); + + test("runs the remaining plugins after an earlier async setup() settles", async () => { + using dir = tempDir("bun-build-async-setup-chain", { "entry.js": entry }); + const order: string[] = []; + const result = await Bun.build({ + entrypoints: [`${dir}/entry.js`], + plugins: [ + { + name: "first", + async setup() { + order.push("first:start"); + await Bun.sleep(10); + order.push("first:done"); + }, + }, + { + name: "second", + setup() { + order.push("second"); + }, + }, + ], + }); + expect(result.success).toBe(true); + expect(order).toEqual(["first:start", "first:done", "second"]); + }); + + test("config mutated after an await in setup() is respected", async () => { + using dir = tempDir("bun-build-async-setup-mutate", { + "entry.js": `export const flag = BUILD_FLAG;\n`, + }); + const result = await Bun.build({ + entrypoints: [`${dir}/entry.js`], + plugins: [ + { + name: "definer", + async setup(build) { + await Bun.sleep(10); + build.config.define = { BUILD_FLAG: JSON.stringify("after-await") }; + }, + }, + ], + }); + expect(result.success).toBe(true); + expect(await result.outputs[0].text()).toContain("after-await"); + }); + + test("rejects the build promise when an async setup() rejects", async () => { + using dir = tempDir("bun-build-async-setup-reject", { "entry.js": entry }); + await expect( + Bun.build({ + entrypoints: [`${dir}/entry.js`], + plugins: [ + { + name: "explosive", + async setup() { + await Bun.sleep(10); + throw new Error("setup exploded"); + }, + }, + ], + }), + ).rejects.toThrow("setup exploded"); + }); +}); From a3c00d1937ae9b71fcf82a9de250eba71a922b5b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:43:16 +0000 Subject: [PATCH 2/5] [autofix.ci] apply automated fixes --- src/runtime/api/JSBundler.rs | 23 +++++--- .../bun-build-plugin-async-setup.test.ts | 56 ++++++++----------- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/runtime/api/JSBundler.rs b/src/runtime/api/JSBundler.rs index cadc5083f15b..f4c262d176a1 100644 --- a/src/runtime/api/JSBundler.rs +++ b/src/runtime/api/JSBundler.rs @@ -596,9 +596,8 @@ pub mod js_bundler { ) -> JsResult { let plugin = array.get_index(global_this, index)?; if !plugin.is_object() { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected plugin to be an object" - ))); + return Err(global_this + .throw_invalid_arguments(format_args!("Expected plugin to be an object"))); } if let Some(slice) = plugin.get_optional_slice(global_this, b"name")? { @@ -609,9 +608,8 @@ pub mod js_bundler { } drop(slice); } else { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected plugin to have a name" - ))); + return Err(global_this + .throw_invalid_arguments(format_args!("Expected plugin to have a name"))); } let Some(function) = plugin.get_function(global_this, b"setup")? else { @@ -1579,9 +1577,16 @@ pub mod js_bundler { } } - let partial = self.partial.take().expect("DeferredBuild resumed after completion"); - let config = - Config::finish_from_js(global_this, self.config_js.get(), partial, self.did_set_target)?; + let partial = self + .partial + .take() + .expect("DeferredBuild resumed after completion"); + let config = Config::finish_from_js( + global_this, + self.config_js.get(), + partial, + self.did_set_target, + )?; let event_loop = global_this.bun_vm().event_loop(); let completion = diff --git a/test/bundler/bun-build-plugin-async-setup.test.ts b/test/bundler/bun-build-plugin-async-setup.test.ts index 4451742b37f0..a8731eb33978 100644 --- a/test/bundler/bun-build-plugin-async-setup.test.ts +++ b/test/bundler/bun-build-plugin-async-setup.test.ts @@ -16,12 +16,10 @@ import { bunEnv, bunExe, tempDir } from "harness"; describe("Bun.build with an async plugin setup()", () => { const entry = `export const hello = "world";\n`; - test( - "returns before a pending setup() promise settles", - async () => { - using dir = tempDir("bun-build-async-setup", { - "entry.js": entry, - "build-fixture.ts": /* ts */ ` + test("returns before a pending setup() promise settles", async () => { + using dir = tempDir("bun-build-async-setup", { + "entry.js": entry, + "build-fixture.ts": /* ts */ ` let release!: () => void; const gate = new Promise(resolve => (release = resolve)); const order: string[] = []; @@ -60,33 +58,27 @@ describe("Bun.build with an async plugin setup()", () => { } console.log(JSON.stringify(order)); `, - }); + }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "build-fixture.ts"], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - // Only reached when Bun.build blocks; the non-blocking path exits in - // well under a second. - timeout: 15_000, - killSignal: "SIGKILL", - }); - const [stdout, stderr, exitCode] = await Promise.all([ - proc.stdout.text(), - proc.stderr.text(), - proc.exited, - ]); - expect(exitCode === 0 ? JSON.parse(stdout.trim()) : { exitCode, stdout, stderr }).toEqual([ - "setup:start", - "returned", - "setup:resume", - "built", - ]); - }, - 30_000, - ); + await using proc = Bun.spawn({ + cmd: [bunExe(), "build-fixture.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + // Only reached when Bun.build blocks; the non-blocking path exits in + // well under a second. + timeout: 15_000, + killSignal: "SIGKILL", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(exitCode === 0 ? JSON.parse(stdout.trim()) : { exitCode, stdout, stderr }).toEqual([ + "setup:start", + "returned", + "setup:resume", + "built", + ]); + }, 30_000); test("still waits for a setup() that suspends on I/O", async () => { using dir = tempDir("bun-build-async-setup-io", { "entry.js": entry }); From 037d3fa7f0bdcb9201c4a44521a561f6e1982a2a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:22:28 +0000 Subject: [PATCH 3/5] Bun.build: GC-root the suspended setup() promise until .then attaches SuspendedPluginSetup held the still-pending promise runSetupFunction returned as a bare JSValue across the gap between Config::from_js recording the suspension and build() attaching the .then reactions. Hold it in a jsc::Strong instead so the rooting is explicit rather than relying on the JS settling chain and the conservative stack scan; this is also what ServePlugins does for the analogous promise. Also make the new test suite concurrent and trim its header comment. --- src/runtime/api/JSBundler.rs | 12 ++++++++---- .../bun-build-plugin-async-setup.test.ts | 17 ++++------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/runtime/api/JSBundler.rs b/src/runtime/api/JSBundler.rs index f4c262d176a1..351a4c27de7c 100644 --- a/src/runtime/api/JSBundler.rs +++ b/src/runtime/api/JSBundler.rs @@ -451,8 +451,9 @@ pub mod js_bundler { /// [`DeferredBuild`] and continues from the promise's `.then` /// continuation. pub struct SuspendedPluginSetup { - /// The still-pending promise `runSetupFunction` returned. - pending: JSValue, + /// The still-pending promise `runSetupFunction` returned, kept + /// GC-rooted until `build` attaches the `.then` reactions to it. + pending: jsc::Strong, /// `config.plugins`, snapshotted before any `setup()` ran so a getter /// is not re-invoked on resume. plugins_array: jsc::Strong, @@ -553,7 +554,7 @@ pub mod js_bundler { // config parse, from this promise's `.then` // continuation. *suspended = Some(SuspendedPluginSetup { - pending, + pending: jsc::Strong::create(pending, global_this), plugins_array: jsc::Strong::create(array, global_this), next_index: index + 1, length, @@ -1460,7 +1461,10 @@ pub mod js_bundler { let suspended = suspended.expect("from_js returned None without a suspension"); let result = jsc::JSPromiseStrong::init(global_this); let return_value = result.value(); - let pending = suspended.pending; + // `suspended.pending` (the Strong that is GC-rooting the promise) + // drops at the end of this block, after `.then` has attached its + // reactions. + let pending = suspended.pending.get(); let deferred = bun_core::heap::into_raw(Box::new(DeferredBuild { config_js: jsc::Strong::create(arguments[0], global_this), plugins_array: suspended.plugins_array, diff --git a/test/bundler/bun-build-plugin-async-setup.test.ts b/test/bundler/bun-build-plugin-async-setup.test.ts index a8731eb33978..7fab60548308 100644 --- a/test/bundler/bun-build-plugin-async-setup.test.ts +++ b/test/bundler/bun-build-plugin-async-setup.test.ts @@ -1,19 +1,10 @@ import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, tempDir } from "harness"; -// `Bun.build({ plugins })` used to run each plugin's `setup()` during -// synchronous config parsing and, when `setup()` returned a promise that was -// still pending, spin the whole event loop (`waitForPromise`) until it -// settled. Synchronously re-entering the event loop from inside whatever -// callback called `Bun.build` is the bug class tracked in -// https://github.com/oven-sh/bun/issues/33261: a nested tick run from inside -// a ready-poll dispatch can clobber the shared ready-poll batch and lose -// one-shot events. It was also a deterministic hang on its own, because the -// nested loop blocks the very JS frame that would have settled the promise. -// -// `Bun.build` now returns without blocking: the remaining plugins and the -// rest of the config parse run in the pending promise's `.then` continuation. -describe("Bun.build with an async plugin setup()", () => { +// https://github.com/oven-sh/bun/issues/33261 +// `Bun.build` must not block the event loop on a pending plugin setup() +// promise; it resumes config parsing in that promise's `.then` continuation. +describe.concurrent("Bun.build with an async plugin setup()", () => { const entry = `export const hello = "world";\n`; test("returns before a pending setup() promise settles", async () => { From 14ca1fc5c829ce403a0cdea298bcc844b7da85f0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:50:29 +0000 Subject: [PATCH 4/5] test: add Promise.race escape coverage for never-settling setup() --- .../bun-build-plugin-async-setup.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/bundler/bun-build-plugin-async-setup.test.ts b/test/bundler/bun-build-plugin-async-setup.test.ts index 7fab60548308..585fee5fe57e 100644 --- a/test/bundler/bun-build-plugin-async-setup.test.ts +++ b/test/bundler/bun-build-plugin-async-setup.test.ts @@ -161,6 +161,44 @@ describe.concurrent("Bun.build with an async plugin setup()", () => { expect(await result.outputs[0].text()).toContain("after-await"); }); + test("lets a caller escape a never-settling setup() with Promise.race", async () => { + using dir = tempDir("bun-build-async-setup-race", { + "entry.js": entry, + "race-fixture.ts": /* ts */ ` + const cfg = { + entrypoints: ["./entry.js"], + plugins: [ + { + name: "never-settles", + async setup() { + await new Promise(() => {}); + }, + }, + ], + }; + const buildPromise = Bun.build(cfg); + const winner = await Promise.race([ + buildPromise.then(() => "build"), + Bun.sleep(200).then(() => "timeout"), + ]); + console.log(JSON.stringify({ winner })); + process.exit(winner === "timeout" ? 0 : 1); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "race-fixture.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + timeout: 15_000, + killSignal: "SIGKILL", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(exitCode === 0 ? JSON.parse(stdout.trim()) : { exitCode, stdout, stderr }).toEqual({ winner: "timeout" }); + }, 30_000); + test("rejects the build promise when an async setup() rejects", async () => { using dir = tempDir("bun-build-async-setup-reject", { "entry.js": entry }); await expect( From 112f49c3a458f46f25eea462f17bacc2d2febc55 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:56:20 +0000 Subject: [PATCH 5/5] JSBundler: trim added doc comments to one line each comment-cop flags every added multi-line comment on claude-labeled PRs. The flagged blocks were ordinary doc comments, not workaround justifications, but they were also more verbose than they needed to be: several restated the same #33261 invariant. Each block is now a single line; the SAFETY comments on unsafe blocks are exempt and unchanged. --- src/runtime/api/JSBundler.rs | 116 +++++++---------------------------- 1 file changed, 21 insertions(+), 95 deletions(-) diff --git a/src/runtime/api/JSBundler.rs b/src/runtime/api/JSBundler.rs index 351a4c27de7c..8c8ad2113d74 100644 --- a/src/runtime/api/JSBundler.rs +++ b/src/runtime/api/JSBundler.rs @@ -436,49 +436,30 @@ pub mod js_bundler { /// One resolved step of the plugin `setup()` chain. enum SetupStep { - /// `runSetupFunction` (and, for the last plugin, its `onStart()` - /// promises) settled synchronously; carries the updated promise array - /// to thread into the next plugin's `runSetupFunction` call. + /// The updated `onStart()` promise array, threaded into the next step. Settled(JSValue), - /// `runSetupFunction` returned a promise that is still pending: an - /// async `setup()` that suspended on an `await`, or the last plugin's - /// `Promise.all` over pending `onStart()` promises. + /// A still-pending async `setup()` or last-plugin `onStart()` `Promise.all`. Pending(JSValue), } - /// Where [`Config::from_js`] stopped when a plugin `setup()` yielded a - /// promise that had not settled yet. `build` turns this into a - /// [`DeferredBuild`] and continues from the promise's `.then` - /// continuation. + /// Where [`Config::from_js`] stopped when a plugin `setup()` suspended; see [`DeferredBuild`]. pub struct SuspendedPluginSetup { - /// The still-pending promise `runSetupFunction` returned, kept - /// GC-rooted until `build` attaches the `.then` reactions to it. + /// GC-roots the `runSetupFunction` promise until `build` attaches `.then` reactions. pending: jsc::Strong, - /// `config.plugins`, snapshotted before any `setup()` ran so a getter - /// is not re-invoked on resume. + /// `config.plugins`, snapshotted so getters aren't re-invoked on resume. plugins_array: jsc::Strong, /// Plugin index to resume the chain from. next_index: u32, /// Snapshot of the plugin array length. length: u32, plugin_target: jsc::BunPluginTarget, - /// The partially-parsed config: only the pre-plugin `target` fields - /// are set. The rest is parsed by `finish_from_js` once every - /// `setup()` has settled, because plugins may mutate the config - /// object before then. + /// Only `target` is set; the rest is parsed by `finish_from_js` once the chain settles. config: Config, did_set_target: bool, } impl Config { - /// Parses `Bun.build`'s config object. - /// - /// Returns `Ok(None)` when a plugin's `setup()` (or the last plugin's - /// pending `onStart()` promises) yielded a promise that is still - /// pending. `*suspended` then records where the chain stopped and - /// owns the partially-parsed config; the caller must continue from - /// that promise's `.then` continuation instead of re-entering the - /// event loop (#33261). + /// `Ok(None)` means a plugin `setup()` suspended; `*suspended` records where the chain stopped (#33261). pub fn from_js( global_this: &JSGlobalObject, config: JSValue, @@ -546,13 +527,6 @@ pub mod js_bundler { )? { SetupStep::Settled(value) => onstart_promise_array = value, SetupStep::Pending(pending) => { - // An async `setup()` (or the last plugin's pending - // `onStart()` promises) has not settled. Never - // re-enter the event loop from here (#33261): - // record where the chain stopped so `build` can - // run the remaining plugins, and the rest of the - // config parse, from this promise's `.then` - // continuation. *suspended = Some(SuspendedPluginSetup { pending: jsc::Strong::create(pending, global_this), plugins_array: jsc::Strong::create(array, global_this), @@ -562,8 +536,7 @@ pub mod js_bundler { config: this, did_set_target, }); - // Ownership of the plugin registry transfers to - // the continuation; disarm the error cleanup. + // Plugin registry ownership moves to the continuation; disarm cleanup. scopeguard::ScopeGuard::into_inner(plugins); return Ok(None); } @@ -576,14 +549,7 @@ pub mod js_bundler { Ok(Some(this)) } - /// Validate plugin `index` of the `plugins` array, lazily create the - /// shared plugin registry, and invoke the plugin's `setup()` through - /// the `runSetupFunction` builtin. - /// - /// `runSetupFunction` stores the `onStart()` promise array it is - /// given on the plugin registry before calling `setup()`, so the - /// chain has to advance one plugin at a time with the previous - /// plugin's settled result. + /// Run plugin `index`'s `setup()`; one at a time because the builtin stashes the prior step's array. #[allow(clippy::too_many_arguments)] fn run_one_plugin_setup( global_this: &JSGlobalObject, @@ -644,9 +610,6 @@ pub mod js_bundler { if let Some(promise) = plugin_result.as_any_promise() { promise.set_handled(global_this.vm()); if matches!(promise.status(), jsc::js_promise::Status::Pending) { - // Never block the event loop waiting for this promise - // (#33261): the caller attaches `.then` reactions and - // continues the chain from there. return Ok(SetupStep::Pending(plugin_result)); } match promise.unwrap(global_this.vm(), jsc::PromiseUnwrapMode::MarkHandled) { @@ -670,11 +633,7 @@ pub mod js_bundler { Ok(SetupStep::Settled(plugin_result)) } - /// Everything `from_js` parses after the plugin `setup()` chain has - /// settled. Split out so that, when a `setup()` suspends, the - /// remainder can run in its `.then` continuation: plugins are - /// allowed to mutate the config object before their promise settles, - /// so none of these fields may be read earlier. + /// The post-setup tail of `from_js`; plugins may mutate the config, so nothing here can be read earlier. fn finish_from_js( global_this: &JSGlobalObject, config: JSValue, @@ -1452,18 +1411,10 @@ pub mod js_bundler { let Some(config) = Config::from_js(global_this, arguments[0], &mut plugins, &mut suspended)? else { - // A plugin `setup()` (or the last plugin's `onStart()` promises) - // suspended on a promise that has not settled. `Bun.build` - // already returns a promise, so run the remaining plugins, finish - // the config parse, and schedule the bundle from that promise's - // `.then` continuation instead of re-entering the event loop - // (#33261). let suspended = suspended.expect("from_js returned None without a suspension"); let result = jsc::JSPromiseStrong::init(global_this); let return_value = result.value(); - // `suspended.pending` (the Strong that is GC-rooting the promise) - // drops at the end of this block, after `.then` has attached its - // reactions. + // `suspended.pending` stays rooted through this block and drops after `.then` attaches. let pending = suspended.pending.get(); let deferred = bun_core::heap::into_raw(Box::new(DeferredBuild { config_js: jsc::Strong::create(arguments[0], global_this), @@ -1516,48 +1467,32 @@ pub mod js_bundler { build(global_this, callframe.arguments()) } - /// A `Bun.build` whose plugin `setup()` chain suspended on a pending - /// promise. - /// - /// Leaked with `heap::into_raw` and handed to the promise's `.then` - /// reactions as their context pointer; exactly one of the two reactions - /// runs and reclaims it (or leaks it again if the chain suspends on a - /// later plugin). + /// Leaked via `heap::into_raw`; exactly one `.then` reaction reclaims it (or re-leaks on another suspension). struct DeferredBuild { - /// `Bun.build(config)`'s config argument; the rest of the parse reads - /// it after the chain settles. + /// `Bun.build`'s config argument; re-read by `finish_from_js` after the chain settles. config_js: jsc::Strong, /// See [`SuspendedPluginSetup::plugins_array`]. plugins_array: jsc::Strong, next_index: u32, length: u32, plugin_target: jsc::BunPluginTarget, - /// The plugin registry, owned here until it either transfers to the - /// completion task (success) or is destroyed (failure). + /// Owned here until moved to the completion task or destroyed on failure. plugin: Option<*mut Plugin>, - /// The partially-parsed config (pre-plugin `target` fields only); - /// `None` once it has moved into the completion task. + /// The pre-plugin config; `None` once it moves to the completion task. partial: Option, did_set_target: bool, - /// `Bun.build`'s result promise. Moved into the completion task on - /// success, rejected here on failure. + /// `Bun.build`'s result promise; moves to the completion task or is rejected here. result: jsc::JSPromiseStrong, } impl DeferredBuild { - /// Continue the plugin `setup()` chain from `next_index` with the - /// value the last pending step settled to, then, once every plugin - /// has settled, finish the config parse and schedule the bundle. - /// - /// Returns `Some(pending)` when the chain suspended again on a later - /// plugin's promise. + /// Continue from `next_index`; returns `Some(pending)` on another suspension. fn resume( &mut self, global_this: &JSGlobalObject, mut onstart_promise_array: JSValue, ) -> JsResult> { - // The synchronous chain rejects when a step settles to an Error; - // preserve that for a step that settled through `.then`. + // Preserve the sync chain's reject-on-Error for a `.then`-settled step. if let Some(err) = onstart_promise_array.to_error() { return Err(global_this.throw_value(err)); } @@ -1609,15 +1544,12 @@ pub mod js_bundler { Ok(None) } - /// Reject `Bun.build`'s result promise with the pending exception and - /// destroy the plugin registry. + /// Reject the result promise with the pending exception and free the plugin registry. fn fail(&mut self, global_this: &JSGlobalObject, err: JsError) { if let Some(plugin) = self.plugin.take() { Plugin::destroy(plugin); } if matches!(err, JsError::Terminated) { - // The VM is terminating; leave the termination exception in - // place and don't settle the promise. return; } let exception = global_this.take_exception(err); @@ -1625,9 +1557,7 @@ pub mod js_bundler { } } - /// `.then` resolve reaction for a suspended plugin `setup()` chain: the - /// step's settled value (the `onStart()` promise array) arrives as the - /// first argument. + /// `.then` resolve reaction; arg[0] is the settled `onStart()` promise array. #[bun_jsc::host_fn(export = "Bun__JSBundler__onResolvePluginSetup")] fn on_deferred_build_resolve( global_this: &JSGlobalObject, @@ -1641,8 +1571,6 @@ pub mod js_bundler { let mut this: Box = unsafe { bun_core::heap::take(deferred) }; match this.resume(global_this, onstart_promise_array) { Ok(Some(pending)) => { - // Another plugin suspended; hand the allocation to the next - // pair of reactions. let deferred = bun_core::heap::into_raw(this); pending.then( global_this, @@ -1657,9 +1585,7 @@ pub mod js_bundler { Ok(JSValue::UNDEFINED) } - /// `.then` reject reaction for a suspended plugin `setup()` chain: a - /// plugin's async `setup()` (or one of its `onStart()` callbacks) - /// rejected. + /// `.then` reject reaction for a suspended plugin `setup()` chain. #[bun_jsc::host_fn(export = "Bun__JSBundler__onRejectPluginSetup")] fn on_deferred_build_reject( global_this: &JSGlobalObject,