diff --git a/apps/oxfmt/src/cli/format.rs b/apps/oxfmt/src/cli/format.rs index b3cfe891d690c..1b38ba8183e64 100644 --- a/apps/oxfmt/src/cli/format.rs +++ b/apps/oxfmt/src/cli/format.rs @@ -87,13 +87,14 @@ impl FormatRunner { // - Parse returned `languages` // - Allow its `extensions` and `filenames` in `walk.rs` // - Pass `parser` to `SourceFormatter` + // Use `block_in_place()` to avoid nested async runtime access #[cfg(feature = "napi")] - if let Err(err) = self - .external_formatter - .as_ref() - .expect("External formatter must be set when `napi` feature is enabled") - .setup_config(&external_config.to_string(), num_of_threads) - { + if let Err(err) = tokio::task::block_in_place(|| { + self.external_formatter + .as_ref() + .expect("External formatter must be set when `napi` feature is enabled") + .setup_config(&external_config.to_string(), num_of_threads) + }) { utils::print_and_flush( stderr, &format!("Failed to setup external formatter config.\n{err}\n"), diff --git a/apps/oxfmt/src/core/external_formatter.rs b/apps/oxfmt/src/core/external_formatter.rs index bb655ac60fe91..59813ae626430 100644 --- a/apps/oxfmt/src/core/external_formatter.rs +++ b/apps/oxfmt/src/core/external_formatter.rs @@ -5,7 +5,6 @@ use napi::{ bindgen_prelude::{FnArgs, Promise, block_on}, threadsafe_function::ThreadsafeFunction, }; -use tokio::task::block_in_place; use oxc_formatter::EmbeddedFormatterCallback; @@ -126,24 +125,32 @@ impl ExternalFormatter { // --- +// NOTE: These methods are all wrapped by `block_on` to run the async JS calls in a blocking manner. +// +// When called from `rayon` worker threads (Mode::Cli), this works fine. +// Because `rayon` threads are separate from the `tokio` runtime. +// +// However, in cases like `--stdin-filepath` or Node.js API calls, +// where already inside an async context (the `napi`'s `async` function), +// calling `block_on` directly would cause issues with nested async runtime access. +// +// Therefore, `block_in_place()` is used at the call site +// to temporarily convert the current async task into a blocking context. + /// Wrap JS `setupConfig` callback as a normal Rust function. -// NOTE: Use `block_in_place()` because this is called from a sync context, unlike the others fn wrap_setup_config(cb: JsSetupConfigCb) -> SetupConfigCallback { Arc::new(move |config_json: &str, num_threads: usize| { - block_in_place(|| { - tokio::runtime::Handle::current().block_on(async { - #[expect(clippy::cast_possible_truncation)] - let status = cb - .call_async(FnArgs::from((config_json.to_string(), num_threads as u32))) - .await; - match status { - Ok(promise) => match promise.await { - Ok(languages) => Ok(languages), - Err(err) => Err(format!("JS setupConfig promise rejected: {err}")), - }, - Err(err) => Err(format!("Failed to call JS setupConfig callback: {err}")), - } - }) + block_on(async { + #[expect(clippy::cast_possible_truncation)] + let status = + cb.call_async(FnArgs::from((config_json.to_string(), num_threads as u32))).await; + match status { + Ok(promise) => match promise.await { + Ok(languages) => Ok(languages), + Err(err) => Err(format!("JS setupConfig promise rejected: {err}")), + }, + Err(err) => Err(format!("Failed to call JS setupConfig callback: {err}")), + } }) }) } diff --git a/apps/oxfmt/src/stdin/mod.rs b/apps/oxfmt/src/stdin/mod.rs index 2b6f81a5cd042..c0500e3194780 100644 --- a/apps/oxfmt/src/stdin/mod.rs +++ b/apps/oxfmt/src/stdin/mod.rs @@ -77,9 +77,10 @@ impl StdinRunner { }; // TODO: Plugins support - if let Err(err) = + // Use `block_in_place()` to avoid nested async runtime access + if let Err(err) = tokio::task::block_in_place(|| { external_formatter.setup_config(&external_config.to_string(), num_of_threads) - { + }) { utils::print_and_flush( stderr, &format!("Failed to setup external formatter config.\n{err}\n"), @@ -97,8 +98,7 @@ impl StdinRunner { let source_formatter = SourceFormatter::new(num_of_threads, format_options) .with_external_formatter(Some(external_formatter), oxfmt_options.sort_package_json); - // Run formatting in a blocking task within tokio runtime - // This is needed because external formatter uses `tokio::runtime::Handle::current()` + // Use `block_in_place()` to avoid nested async runtime access match tokio::task::block_in_place(|| source_formatter.format(&strategy, &source_text)) { FormatResult::Success { code, .. } => { utils::print_and_flush(stdout, &code);