diff --git a/apps/oxfmt/src-js/bindings.d.ts b/apps/oxfmt/src-js/bindings.d.ts index b1d213313a8f9..9d71d41897dd1 100644 --- a/apps/oxfmt/src-js/bindings.d.ts +++ b/apps/oxfmt/src-js/bindings.d.ts @@ -34,7 +34,7 @@ export declare const enum Severity { * # Panics * Panics if the current working directory cannot be determined. */ -export declare function format(filename: string, sourceText: string, options: any | undefined | null, initExternalFormatterCb: (numThreads: number) => Promise, formatEmbeddedCb: (options: Record, code: string) => Promise, formatEmbeddedDocCb: (options: Record, texts: string[]) => Promise, formatFileCb: (options: Record, code: string) => Promise, sortTailwindClassesCb: (options: Record, classes: string[]) => Promise): Promise +export declare function format(filename: string, sourceText: string, options: any | undefined | null, initExternalFormatterCb: (numThreads: number) => Promise, formatEmbeddedCb: (options: Record, code: string) => Promise, formatEmbeddedDocCb: (options: Record, texts: string[]) => Promise, formatFileCb: (options: Record, code: string) => Promise, sortTailwindClassesCb: (options: Record, classes: string[]) => Promise): Promise export interface FormatResult { /** The formatted code. */ @@ -49,7 +49,7 @@ export interface FormatResult { * This API is specialized for JS/TS snippets embedded in non-JS files. * Unlike `format()`, it is called only for js-in-xxx `textToDoc()` flow. */ -export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmtPluginOptionsJson: string, parentContext: string, initExternalFormatterCb: (numThreads: number) => Promise, formatEmbeddedCb: (options: Record, code: string) => Promise, formatEmbeddedDocCb: (options: Record, texts: string[]) => Promise, formatFileCb: (options: Record, code: string) => Promise, sortTailwindClassesCb: (options: Record, classes: string[]) => Promise): Promise +export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmtPluginOptionsJson: string, parentContext: string, initExternalFormatterCb: (numThreads: number) => Promise, formatEmbeddedCb: (options: Record, code: string) => Promise, formatEmbeddedDocCb: (options: Record, texts: string[]) => Promise, formatFileCb: (options: Record, code: string) => Promise, sortTailwindClassesCb: (options: Record, classes: string[]) => Promise): Promise /** * NAPI based JS CLI entry point. @@ -66,4 +66,4 @@ export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmt * - `mode`: If main logic will run in JS side, use this to indicate which mode * - `exitCode`: If main logic already ran in Rust side, return the exit code */ -export declare function runCli(args: Array, initExternalFormatterCb: (numThreads: number) => Promise, formatEmbeddedCb: (options: Record, code: string) => Promise, formatEmbeddedDocCb: (options: Record, texts: string[]) => Promise, formatFileCb: (options: Record, code: string) => Promise, sortTailwindcssClassesCb: (options: Record, classes: string[]) => Promise): Promise<[string, number | undefined | null]> +export declare function runCli(args: Array, initExternalFormatterCb: (numThreads: number) => Promise, formatEmbeddedCb: (options: Record, code: string) => Promise, formatEmbeddedDocCb: (options: Record, texts: string[]) => Promise, formatFileCb: (options: Record, code: string) => Promise, sortTailwindcssClassesCb: (options: Record, classes: string[]) => Promise): Promise<[string, number | undefined | null]> diff --git a/apps/oxfmt/src-js/cli/worker-proxy.ts b/apps/oxfmt/src-js/cli/worker-proxy.ts index 5c7ac947b4126..b84f21082c6e5 100644 --- a/apps/oxfmt/src-js/cli/worker-proxy.ts +++ b/apps/oxfmt/src-js/cli/worker-proxy.ts @@ -32,58 +32,67 @@ export async function disposeExternalFormatter(): Promise { pool = null; } +// --- + +// Used for non-JS files formatting +export async function formatFile( + options: FormatFileParam["options"], + code: string, +): Promise { + return ( + pool! + .run({ options, code } satisfies FormatFileParam, { name: "formatFile" }) + // `tinypool` with `runtime: "child_process"` serializes Error as plain objects via IPC. + // (e.g. `{ name, message, stack, ... }`) + // And napi-rs converts unknown JS values to Rust Error by calling `String()` on them, + // which yields `"[object Object]"` for plain objects... + // So, this function reconstructs a proper `Error` instance so napi-rs can extract the message. + .catch((err) => { + if (err instanceof Error) throw err; + if (err !== null && typeof err === "object") { + const obj = err as { name: string; message: string }; + const newErr = new Error(obj.message); + newErr.name = obj.name; + throw newErr; + } + throw new Error(String(err)); + }) + ); +} + +// --- + +// All functions below are used for JS files with embedded code +// +// NOTE: These functions return `null` on error instead of throwing. +// When errors were propagated as rejected JS promises, which become `napi::Error` values in Rust TSFN await paths. +// In heavily concurrent runs, dropping those error values could reach `napi_reference_unref` during teardown and trigger V8 fatal checks. + export async function formatEmbeddedCode( options: FormatEmbeddedCodeParam["options"], code: string, -): Promise { +): Promise { return pool! .run({ options, code } satisfies FormatEmbeddedCodeParam, { name: "formatEmbeddedCode" }) - .catch(rethrowAsError); + .catch(() => null); } export async function formatEmbeddedDoc( options: FormatEmbeddedDocParam["options"], texts: string[], -): Promise { +): Promise { return pool! .run({ options, texts } satisfies FormatEmbeddedDocParam, { name: "formatEmbeddedDoc", }) - .catch(rethrowAsError); -} - -export async function formatFile( - options: FormatFileParam["options"], - code: string, -): Promise { - return pool! - .run({ options, code } satisfies FormatFileParam, { name: "formatFile" }) - .catch(rethrowAsError); + .catch(() => null); } export async function sortTailwindClasses( options: SortTailwindClassesArgs["options"], classes: string[], -): Promise { +): Promise { return pool! .run({ classes, options } satisfies SortTailwindClassesArgs, { name: "sortTailwindClasses" }) - .catch(rethrowAsError); -} - -// --- - -// `tinypool` with `runtime: "child_process"` serializes Error as plain objects via IPC. -// (e.g. `{ name, message, stack, ... }`) -// And napi-rs converts unknown JS values to Rust Error by calling `String()` on them, -// which yields `"[object Object]"` for plain objects... -// So, this function reconstructs a proper `Error` instance so napi-rs can extract the message. -function rethrowAsError(err: unknown): never { - if (err instanceof Error) throw err; - if (err !== null && typeof err === "object") { - const obj = err as { name: string; message: string }; - const newErr = new Error(obj.message); - newErr.name = obj.name; - throw newErr; - } - throw new Error(String(err)); + .catch(() => null); } diff --git a/apps/oxfmt/src/core/external_formatter.rs b/apps/oxfmt/src/core/external_formatter.rs index 18dff8919725a..12881e42c701d 100644 --- a/apps/oxfmt/src/core/external_formatter.rs +++ b/apps/oxfmt/src/core/external_formatter.rs @@ -31,13 +31,13 @@ pub type JsInitExternalFormatterCb = ThreadsafeFunction< >; /// Type alias for the callback function signature. -/// Takes (options, code) as arguments and returns formatted code. +/// Takes (options, code) as arguments and returns formatted code or null on error. /// The `options` object includes `parser` field set by Rust side. pub type JsFormatEmbeddedCb = ThreadsafeFunction< // Input arguments FnArgs<(Value, String)>, // (options, code) // Return type (what JS function returns) - Promise, + Promise>, // Arguments (repeated) FnArgs<(Value, String)>, // Error status @@ -47,13 +47,13 @@ pub type JsFormatEmbeddedCb = ThreadsafeFunction< >; /// Type alias for the Doc-path callback function signature (batch). -/// Takes (options, texts[]) as arguments and returns Doc JSON string[] (one per text). +/// Takes (options, texts[]) as arguments and returns Doc JSON string[] (one per text) or null on error. /// The `options` object includes `parser` field set by Rust side. pub type JsFormatEmbeddedDocCb = ThreadsafeFunction< // Input arguments FnArgs<(Value, Vec)>, // (options, texts) // Return type (what JS function returns) - Promise>, + Promise>>, // Arguments (repeated) FnArgs<(Value, Vec)>, // Error status @@ -79,11 +79,11 @@ pub type JsFormatFileCb = ThreadsafeFunction< >; /// Type alias for Tailwind class processing callback. -/// Takes (options, classes) and returns sorted array. +/// Takes (options, classes) and returns sorted array or null on error. /// The `filepath` is included in `options`. pub type JsSortTailwindClassesCb = ThreadsafeFunction< FnArgs<(Value, Vec)>, // Input: (options, classes) - Promise>, // Return: promise of sorted array + Promise>>, // Return: promise of sorted array or null FnArgs<(Value, Vec)>, Status, false, @@ -427,7 +427,10 @@ fn wrap_format_embedded( let status = cb.call_async(FnArgs::from((options, code.to_string()))).await; match status { Ok(promise) => match promise.await { - Ok(formatted_code) => Ok(formatted_code), + Ok(Some(formatted_code)) => Ok(formatted_code), + Ok(None) => Err("Embedded formatting failed".to_string()), + // JS side never rejects; it returns `null` on error instead. + // `Err` here would only come from a napi-rs internal failure. Err(err) => Err(err.reason.clone()), }, Err(err) => Err(err.reason.clone()), @@ -453,7 +456,10 @@ fn wrap_format_embedded_doc( let status = cb.call_async(FnArgs::from((options, texts_owned))).await; match status { Ok(promise) => match promise.await { - Ok(doc_jsons) => Ok(doc_jsons), + Ok(Some(doc_jsons)) => Ok(doc_jsons), + Ok(None) => Err("Embedded doc formatting failed".to_string()), + // JS side never rejects; it returns `null` on error instead. + // `Err` here would only come from a napi-rs internal failure. Err(err) => Err(err.reason.clone()), }, Err(err) => Err(err.reason.clone()), @@ -503,11 +509,11 @@ fn wrap_sort_tailwind_classes( let args = FnArgs::from((options.clone(), classes.clone())); match cb.call_async(args).await { Ok(promise) => match promise.await { - Ok(sorted) => sorted, - // Return original classes on error - Err(_) => classes, + Ok(Some(sorted)) => sorted, + // JS side never rejects; it returns `null` on error instead. + // `Err` here would only come from a napi-rs internal failure. + Ok(None) | Err(_) => classes, }, - // Return original classes on error Err(_) => classes, } }); diff --git a/apps/oxfmt/src/main_napi.rs b/apps/oxfmt/src/main_napi.rs index 5c09d807ee085..b47bdc203b956 100644 --- a/apps/oxfmt/src/main_napi.rs +++ b/apps/oxfmt/src/main_napi.rs @@ -36,13 +36,17 @@ pub async fn run_cli( args: Vec, #[napi(ts_arg_type = "(numThreads: number) => Promise")] init_external_formatter_cb: JsInitExternalFormatterCb, - #[napi(ts_arg_type = "(options: Record, code: string) => Promise")] + #[napi(ts_arg_type = "(options: Record, code: string) => Promise")] format_embedded_cb: JsFormatEmbeddedCb, - #[napi(ts_arg_type = "(options: Record, texts: string[]) => Promise")] + #[napi( + ts_arg_type = "(options: Record, texts: string[]) => Promise" + )] format_embedded_doc_cb: JsFormatEmbeddedDocCb, #[napi(ts_arg_type = "(options: Record, code: string) => Promise")] format_file_cb: JsFormatFileCb, - #[napi(ts_arg_type = "(options: Record, classes: string[]) => Promise")] + #[napi( + ts_arg_type = "(options: Record, classes: string[]) => Promise" + )] sort_tailwindcss_classes_cb: JsSortTailwindClassesCb, ) -> (String, Option) { // Convert `String` args to `OsString` for compatibility with `bpaf` @@ -143,13 +147,17 @@ pub async fn format( options: Option, #[napi(ts_arg_type = "(numThreads: number) => Promise")] init_external_formatter_cb: JsInitExternalFormatterCb, - #[napi(ts_arg_type = "(options: Record, code: string) => Promise")] + #[napi(ts_arg_type = "(options: Record, code: string) => Promise")] format_embedded_cb: JsFormatEmbeddedCb, - #[napi(ts_arg_type = "(options: Record, texts: string[]) => Promise")] + #[napi( + ts_arg_type = "(options: Record, texts: string[]) => Promise" + )] format_embedded_doc_cb: JsFormatEmbeddedDocCb, #[napi(ts_arg_type = "(options: Record, code: string) => Promise")] format_file_cb: JsFormatFileCb, - #[napi(ts_arg_type = "(options: Record, classes: string[]) => Promise")] + #[napi( + ts_arg_type = "(options: Record, classes: string[]) => Promise" + )] sort_tailwind_classes_cb: JsSortTailwindClassesCb, ) -> FormatResult { let format_api::ApiFormatResult { code, errors } = format_api::run( @@ -182,13 +190,17 @@ pub async fn js_text_to_doc( parent_context: String, #[napi(ts_arg_type = "(numThreads: number) => Promise")] init_external_formatter_cb: JsInitExternalFormatterCb, - #[napi(ts_arg_type = "(options: Record, code: string) => Promise")] + #[napi(ts_arg_type = "(options: Record, code: string) => Promise")] format_embedded_cb: JsFormatEmbeddedCb, - #[napi(ts_arg_type = "(options: Record, texts: string[]) => Promise")] + #[napi( + ts_arg_type = "(options: Record, texts: string[]) => Promise" + )] format_embedded_doc_cb: JsFormatEmbeddedDocCb, #[napi(ts_arg_type = "(options: Record, code: string) => Promise")] format_file_cb: JsFormatFileCb, - #[napi(ts_arg_type = "(options: Record, classes: string[]) => Promise")] + #[napi( + ts_arg_type = "(options: Record, classes: string[]) => Promise" + )] sort_tailwind_classes_cb: JsSortTailwindClassesCb, ) -> Option { utils::init_tracing();