fix(oxfmt): avoid embedded TSFN crash by returning errors as data#19742
fix(oxfmt): avoid embedded TSFN crash by returning errors as data#19742
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes an intermittent Node/V8 crash in oxfmt’s NAPI-based CLI when embedded formatting is enabled under high concurrency by preventing embedded-formatter failures from surfacing as rejected promises in TSFN await paths.
Changes:
- Updates the embedded formatter callback contract to return a resolved “result object” (
{ ok, code?/error? }) instead of rejecting. - Adds a JS
formatEmbeddedCodeSafe()wrapper and wires both JS API and CLI worker-proxy to use the safe embedded callback path. - Updates Rust to accept
Promise<Value>for embedded formatting and parses the wrapped result, and removes the CLI exit delay workaround.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/oxfmt/src/main_napi.rs | Updates NAPI TS argument types for embedded formatter callback to the new wrapped-result contract. |
| apps/oxfmt/src/core/external_formatter.rs | Changes embedded TSFN return type to Promise<Value> and adds parsing for wrapped embedded results (with legacy string fallback). |
| apps/oxfmt/src-js/libs/apis.ts | Introduces FormatEmbeddedCodeResult + formatEmbeddedCodeSafe() that never rejects and encodes errors as data. |
| apps/oxfmt/src-js/index.ts | Switches the public JS API to pass the safe embedded formatter callback to NAPI. |
| apps/oxfmt/src-js/cli/worker-proxy.ts | Wraps embedded worker formatting so it never rejects; improves error-object reconstruction handling. |
| apps/oxfmt/src-js/cli.ts | Removes the Node-version-based exit delay workaround. |
| apps/oxfmt/src-js/bindings.d.ts | Updates generated TS types for the embedded formatter callback signature. |
| #[napi( | ||
| ts_arg_type = "(options: Record<string, any>, code: string) => Promise<{ ok: boolean; code?: string; error?: string }>" | ||
| )] |
There was a problem hiding this comment.
Same as above: the Rust parser accepts legacy string results, but the generated TS arg type only permits the wrapped object. Consider widening this callback type to Promise<string | { ok: boolean; code?: string; error?: string }> (or remove the legacy parsing path if not supported).
| init_external_formatter_cb: JsInitExternalFormatterCb, | ||
| #[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string>")] | ||
| #[napi( | ||
| ts_arg_type = "(options: Record<string, any>, code: string) => Promise<{ ok: boolean; code?: string; error?: string }>" |
There was a problem hiding this comment.
Same as above: this callback ts_arg_type doesn’t reflect that Rust still accepts a legacy string response. Either widen the TS type to include string or drop the legacy support so type-level and runtime contracts match.
| ts_arg_type = "(options: Record<string, any>, code: string) => Promise<{ ok: boolean; code?: string; error?: string }>" | |
| ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | { ok: boolean; code?: string; error?: string }>" |
| init_external_formatter_cb: JsInitExternalFormatterCb, | ||
| #[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string>")] | ||
| #[napi( | ||
| ts_arg_type = "(options: Record<string, any>, code: string) => Promise<{ ok: boolean; code?: string; error?: string }>" |
There was a problem hiding this comment.
The Rust side now accepts a legacy plain string result for backward compatibility (parse_embedded_callback_result), but this ts_arg_type only allows the wrapped { ok, code?, error? } object. If backward compatibility is intended, update the TypeScript callback type to include Promise<string | { ok: boolean; code?: string; error?: string }>; otherwise consider removing the legacy parsing path to keep runtime and types aligned.
| ts_arg_type = "(options: Record<string, any>, code: string) => Promise<{ ok: boolean; code?: string; error?: string }>" | |
| ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | { ok: boolean; code?: string; error?: string }>" |
6234a4d to
df05046
Compare
Summary
This fixes a crash in
oxfmtNAPI CLI when formatting large repos with embedded formatting enabled.Root cause: embedded formatter errors were propagated as rejected JS promises, which become
napi::Errorvalues in Rust TSFN await paths. In heavily concurrent runs, dropping those error values could reachnapi_reference_unrefduring teardown and trigger V8 fatal checks.Fix:
Promise<string>toPromise<{ ok: boolean; code?: string; error?: string }>.formatEmbeddedCodeSafe()in JS that never rejects and encodes errors as data.parse_embedded_callback_result) and keep existing fallback behavior.waitForImmediateworkaround in CLI exit path.Details
JsFormatEmbeddedCbnow returnsPromise<Value>.{ ok: true, code: string }{ ok: false, error: string }string(backward-compatible fallback)bindings.d.tsupdated accordingly.Validation
Executed locally on Node
v24.12.0:pnpm -C apps/oxfmt run build-dev/Users/boshen/github/linear-app:node /Users/boshen/oxc/oxc4/apps/oxfmt/dist/cli.js --check=>PASS 80/80PASS 200/200node /Users/boshen/oxc/oxc4/apps/oxfmt/dist/cli.js --check --ignore-path /tmp/oxfmt-js-only.ignore=>PASS 40/40AI Usage Disclosure
This PR was prepared with AI assistance (Codex) for investigation, implementation, and stress-test scripting. Changes were reviewed and validated in local runs before submission.
Fixes #19713