diff --git a/apps/oxfmt/src-js/bindings.d.ts b/apps/oxfmt/src-js/bindings.d.ts index 3994a3a271066..5929b83a558db 100644 --- a/apps/oxfmt/src-js/bindings.d.ts +++ b/apps/oxfmt/src-js/bindings.d.ts @@ -5,9 +5,10 @@ * * JS side passes in: * 1. `args`: Command line arguments (process.argv.slice(2)) - * 2. `format_embedded_cb`: Callback to format embedded code in templates - * 3. `format_file_cb`: Callback to format files + * 2. `setup_config_cb`: Callback to setup Prettier config + * 3. `format_embedded_cb`: Callback to format embedded code in templates + * 4. `format_file_cb`: Callback to format files * * Returns `true` if formatting succeeded without errors, `false` otherwise. */ -export declare function format(args: Array, formatEmbeddedCb: (tagName: string, code: string) => Promise, formatFileCb: (parserName: string, code: string) => Promise): Promise +export declare function format(args: Array, setupConfigCb: (configJSON: string) => Promise, formatEmbeddedCb: (tagName: string, code: string) => Promise, formatFileCb: (parserName: string, code: string) => Promise): Promise diff --git a/apps/oxfmt/src-js/cli.ts b/apps/oxfmt/src-js/cli.ts index 1585a6ccdfe51..61a806c0d3890 100644 --- a/apps/oxfmt/src-js/cli.ts +++ b/apps/oxfmt/src-js/cli.ts @@ -1,10 +1,10 @@ import { format } from "./bindings.js"; -import { formatEmbeddedCode, formatFile } from "./prettier-proxy.js"; +import { setupConfig, formatEmbeddedCode, formatFile } from "./prettier-proxy.js"; const args = process.argv.slice(2); // Call the Rust formatter with our JS callback -const success = await format(args, formatEmbeddedCode, formatFile); +const success = await format(args, setupConfig, formatEmbeddedCode, formatFile); // NOTE: It's recommended to set `process.exitCode` instead of calling `process.exit()`. // `process.exit()` kills the process immediately and `stdout` may not be flushed before process dies. diff --git a/apps/oxfmt/src-js/index.ts b/apps/oxfmt/src-js/index.ts index 8d1081d099923..a8ab01e7da50f 100644 --- a/apps/oxfmt/src-js/index.ts +++ b/apps/oxfmt/src-js/index.ts @@ -1,2 +1,2 @@ export * from "./bindings.js"; -export { formatEmbeddedCode, formatFile } from "./prettier-proxy.js"; +export { setupConfig, formatEmbeddedCode, formatFile } from "./prettier-proxy.js"; diff --git a/apps/oxfmt/src-js/prettier-proxy.ts b/apps/oxfmt/src-js/prettier-proxy.ts index e062ac9e7e460..c978e51f8d3a3 100644 --- a/apps/oxfmt/src-js/prettier-proxy.ts +++ b/apps/oxfmt/src-js/prettier-proxy.ts @@ -1,3 +1,5 @@ +import type { Options } from "prettier"; + // Import Prettier lazily. // This helps to reduce initial load time if not needed. // @@ -11,6 +13,37 @@ // But actually, this makes `oxfmt --lsp` immediately stop with `Parse error` JSON-RPC error let prettierCache: typeof import("prettier"); +// Cache for Prettier options. +// Set by `setupConfig` function once. +// +// Read `.oxfmtrc.json(c)` directly does not work, +// because our brand new defaults are not compatible with Prettier's defaults. +// So we need to pass the config from Rust side after merging with our defaults. +let configCache: Options = {}; + +// --- + +/** + * Setup Prettier configuration. + * NOTE: Called from Rust via NAPI ThreadsafeFunction with FnArgs + * @param configJSON - Prettier configuration as JSON string + * @returns Array of loaded plugin's `languages` info + * */ +export async function setupConfig(configJSON: string): Promise { + // NOTE: `napi-rs` has ability to pass `Object` directly. + // But since we don't know what options various plugins may specify, + // we have to receive it as a JSON string and parse it. + // + // SAFETY: This is valid JSON string generated in Rust side + configCache = JSON.parse(configJSON) as Options; + + // TODO: Plugins support + // - Read `plugins` field + // - Load plugins dynamically and parse `languages` field + // - Map file extensions and filenames to Prettier parsers + return []; +} + // --- // Map template tag names to Prettier parsers @@ -52,17 +85,15 @@ export async function formatEmbeddedCode(tagName: string, code: string): Promise return prettierCache .format(code, { + ...configCache, parser, - // TODO: Read config - printWidth: 80, - tabWidth: 2, - semi: true, - singleQuote: false, }) .then((formatted) => formatted.trimEnd()) .catch(() => code); } +// --- + /** * Format whole file content using Prettier. * NOTE: Called from Rust via NAPI ThreadsafeFunction with FnArgs @@ -76,7 +107,7 @@ export async function formatFile(parserName: string, code: string): Promise CliRunResult { let start_time = Instant::now(); @@ -78,6 +80,25 @@ impl FormatRunner { } }; + // TODO: Plugins support + // - Parse returned `languages` + // - Allow its `extensions` and `filenames` in `walk.rs` + // - Pass `parser` to `SourceFormatter` + #[cfg(feature = "napi")] + if let Err(err) = self + .external_formatter + .as_ref() + .expect("External formatter must be set when `napi` feature is enabled") + // TODO: Construct actual config + .setup_config("{}") + { + print_and_flush( + stderr, + &format!("Failed to setup external formatter config.\n{err}\n"), + ); + return CliRunResult::InvalidOptionConfig; + } + let walker = match Walk::build( &cwd, &paths, diff --git a/apps/oxfmt/src/core/external_formatter.rs b/apps/oxfmt/src/core/external_formatter.rs index 0a72b5da51dcd..a801074729b7c 100644 --- a/apps/oxfmt/src/core/external_formatter.rs +++ b/apps/oxfmt/src/core/external_formatter.rs @@ -6,6 +6,22 @@ use napi::{ threadsafe_function::ThreadsafeFunction, }; use oxc_formatter::EmbeddedFormatterCallback; +use tokio::task::block_in_place; + +/// Type alias for the setup config callback function signature. +/// Takes config_json as argument and returns plugin languages. +pub type JsSetupConfigCb = ThreadsafeFunction< + // Input arguments + FnArgs<(String,)>, // (config_json,) + // Return type (what JS function returns) + Promise>, + // Arguments (repeated) + FnArgs<(String,)>, + // Error status + Status, + // CalleeHandled + false, +>; /// Type alias for the callback function signature. /// Takes (tag_name, code) as separate arguments and returns formatted code. @@ -41,9 +57,14 @@ pub type JsFormatFileCb = ThreadsafeFunction< /// Takes (parser_name, code) and returns formatted code or an error. type FileFormatterCallback = Arc Result + Send + Sync>; +/// Callback function type for setup config. +/// Takes config_json and returns plugin languages. +type SetupConfigCallback = Arc Result, String> + Send + Sync>; + /// External formatter that wraps a JS callback. #[derive(Clone)] pub struct ExternalFormatter { + pub setup_config: SetupConfigCallback, pub format_embedded: EmbeddedFormatterCallback, pub format_file: FileFormatterCallback, } @@ -51,6 +72,7 @@ pub struct ExternalFormatter { impl std::fmt::Debug for ExternalFormatter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ExternalFormatter") + .field("setup_config", &"") .field("format_embedded", &"") .field("format_file", &"") .finish() @@ -59,10 +81,24 @@ impl std::fmt::Debug for ExternalFormatter { impl ExternalFormatter { /// Create an [`ExternalFormatter`] from JS callbacks. - pub fn new(format_embedded_cb: JsFormatEmbeddedCb, format_file_cb: JsFormatFileCb) -> Self { + pub fn new( + setup_config_cb: JsSetupConfigCb, + format_embedded_cb: JsFormatEmbeddedCb, + format_file_cb: JsFormatFileCb, + ) -> Self { + let rust_setup_config = wrap_setup_config(setup_config_cb); let rust_format_embedded = wrap_format_embedded(format_embedded_cb); let rust_format_file = wrap_format_file(format_file_cb); - Self { format_embedded: rust_format_embedded, format_file: rust_format_file } + Self { + setup_config: rust_setup_config, + format_embedded: rust_format_embedded, + format_file: rust_format_file, + } + } + + /// Setup Prettier config using the JS callback. + pub fn setup_config(&self, config_json: &str) -> Result, String> { + (self.setup_config)(config_json) } /// Convert this external formatter to the oxc_formatter::EmbeddedFormatter type @@ -78,6 +114,27 @@ impl ExternalFormatter { } } +// --- + +/// 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| { + block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let status = cb.call_async(FnArgs::from((config_json.to_string(),))).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}")), + } + }) + }) + }) +} + /// Wrap JS `formatEmbeddedCode` callback as a normal Rust function. fn wrap_format_embedded(cb: JsFormatEmbeddedCb) -> EmbeddedFormatterCallback { Arc::new(move |tag_name: &str, code: &str| { diff --git a/apps/oxfmt/src/core/mod.rs b/apps/oxfmt/src/core/mod.rs index 7f8116360e0de..684f60dea0abf 100644 --- a/apps/oxfmt/src/core/mod.rs +++ b/apps/oxfmt/src/core/mod.rs @@ -8,4 +8,6 @@ pub use format::{FormatResult, SourceFormatter}; pub use support::FormatFileSource; #[cfg(feature = "napi")] -pub use external_formatter::{ExternalFormatter, JsFormatEmbeddedCb, JsFormatFileCb}; +pub use external_formatter::{ + ExternalFormatter, JsFormatEmbeddedCb, JsFormatFileCb, JsSetupConfigCb, +}; diff --git a/apps/oxfmt/src/main_napi.rs b/apps/oxfmt/src/main_napi.rs index 0e70de39ae998..a2e65cc774fe8 100644 --- a/apps/oxfmt/src/main_napi.rs +++ b/apps/oxfmt/src/main_napi.rs @@ -8,7 +8,7 @@ use napi_derive::napi; use crate::{ cli::{CliRunResult, FormatRunner, format_command, init_miette, init_tracing}, - core::{ExternalFormatter, JsFormatEmbeddedCb, JsFormatFileCb}, + core::{ExternalFormatter, JsFormatEmbeddedCb, JsFormatFileCb, JsSetupConfigCb}, lsp::run_lsp, }; @@ -19,8 +19,9 @@ use crate::{ /// /// JS side passes in: /// 1. `args`: Command line arguments (process.argv.slice(2)) -/// 2. `format_embedded_cb`: Callback to format embedded code in templates -/// 3. `format_file_cb`: Callback to format files +/// 2. `setup_config_cb`: Callback to setup Prettier config +/// 3. `format_embedded_cb`: Callback to format embedded code in templates +/// 4. `format_file_cb`: Callback to format files /// /// Returns `true` if formatting succeeded without errors, `false` otherwise. #[expect(clippy::allow_attributes)] @@ -28,17 +29,21 @@ use crate::{ #[napi] pub async fn format( args: Vec, + #[napi(ts_arg_type = "(configJSON: string) => Promise")] + setup_config_cb: JsSetupConfigCb, #[napi(ts_arg_type = "(tagName: string, code: string) => Promise")] format_embedded_cb: JsFormatEmbeddedCb, #[napi(ts_arg_type = "(parserName: string, code: string) => Promise")] format_file_cb: JsFormatFileCb, ) -> bool { - format_impl(args, format_embedded_cb, format_file_cb).await.report() == ExitCode::SUCCESS + format_impl(args, setup_config_cb, format_embedded_cb, format_file_cb).await.report() + == ExitCode::SUCCESS } /// Run the formatter. async fn format_impl( args: Vec, + setup_config_cb: JsSetupConfigCb, format_embedded_cb: JsFormatEmbeddedCb, format_file_cb: JsFormatFileCb, ) -> CliRunResult { @@ -71,7 +76,8 @@ async fn format_impl( command.handle_threads(); // Create external formatter from JS callback - let external_formatter = ExternalFormatter::new(format_embedded_cb, format_file_cb); + let external_formatter = + ExternalFormatter::new(setup_config_cb, format_embedded_cb, format_file_cb); // stdio is blocked by LineWriter, use a BufWriter to reduce syscalls. // See `https://github.com/rust-lang/rust/issues/60673`.