Files
oxc-project__oxc/apps/oxfmt/src-js/cli.ts
Pablo García d35d89f78c fix(oxfmt): skip shutdown delay on fixed Node 24 releases (#26518)
Oxfmt still schedules its 50 ms forced-exit workaround on every Node 24
release, although Node 24.13.1 backported the ThreadsafeFunction fix.
Exempt fixed Node 24 releases while retaining the existing workaround
for older Node 20/22/24 versions and Node 25 before 25.4.0.

The backport is documented in [Node 24.13.1's release
notes](https://nodejs.org/en/blog/release/v24.13.1) and
[nodejs/node@1e13e84f16](https://github.com/nodejs/node/commit/1e13e84f162397f1adbe58f7a6f62974cfd0fa95).
This narrows the workaround introduced in #18980; it preserves worker
disposal and `process.exitCode` handling.

Earlier downstream validation of this same condition on Linux/Node 24.19
included ten successful checks of 12,032 synthetic files with complete
output. In eleven paired downstream Vite+ check rounds, removing the
timer saved a median of about 42 ms for full checks and 44 ms for
three-file checks. These are project-specific workflow timings, not a
universal 50 ms guarantee.

Validation on `805650e8f60e25abb8ae1038580ab799e7f8dcd9`, using the
repository's frozen pnpm 12.3.2 dependency installation and Rust 1.98.1:

- `pnpm --dir apps/oxfmt build-dev`: native and JavaScript build passed.
- `env -u NO_COLOR -u FORCE_COLOR pnpm --dir apps/oxfmt test
--maxWorkers=4`: **480 passed, 10 skipped**, using the pinned Vitest
5.0.0. This covers the complete Oxfmt JavaScript suite, including CLI,
API, LSP and the ten new regression cases. The first run had two color
snapshot mismatches caused by inherited `NO_COLOR`; clearing the ambient
color variables resolved them without changing tests or snapshots.
- `cargo test -p oxfmt --locked`: **84 passed**.
- `cargo clippy -p oxfmt --all-targets --locked -- -D warnings`: passed.
- Changed-file formatting and repository-configured type-aware
Oxlint/type checks: passed with the pinned tools.

Additional checks exercised the freshly built native CLI on **real Node
24.13.1 and 24.19.0**: five successful runs per runtime over 12,032
synthetic JS/Markdown files, including worker callbacks; no 50 ms timers
were registered. Both runtimes preserved all 497,780 bytes of a large
stdin-formatting result and the expected parse-error exit code 2. These
are correctness/stress checks of the dev build, not performance
benchmarks.

The entire Oxc workspace's other packages and the cross-platform CI
matrix were not run locally.

The follow-up commit `1c98d210501e276c2d6e3009562b2ebe41217ab3` removes
the ten mocked shutdown regression cases at the maintainer's request.
Its only change is deleting that test file; the production CLI is
byte-for-byte unchanged from the fully validated revision above. The
final PR changes only `apps/oxfmt/src-js/cli.ts`. The suite counts above
describe the earlier run, including the subsequently removed tests; no
new full-suite run is claimed for the deletion-only follow-up.

AI assistance: implementation and self-review performed with OpenAI
Codex using GPT-6 Astra (`gpt-6-astra`), with reasoning effort set to
`high`.
2026-09-10 16:45:24 +09:00

86 lines
3.6 KiB
TypeScript

import { runCli } from "./bindings";
import {
initExternalServices,
disposeExternalServices,
formatFile,
formatEmbeddedCode,
formatEmbeddedDoc,
sortTailwindClasses,
} from "./cli/worker-proxy";
import { loadJsConfig, loadVitePlusConfig } from "./cli/js_config";
// napi-JS `oxfmt` CLI entry point
// See also `run_cli()` function in `./src/main_napi.rs`
void (async () => {
const args = process.argv.slice(2);
// Node.js sets non-TTY `stdio` to non-blocking mode,
// which causes `WouldBlock` errors in Rust.
// As a workaround, if used with pipe, set blocking mode before calling NAPI bindings.
// See: https://github.com/napi-rs/napi-rs/issues/1630
//
// stdout: Writing large formatted output via `--stdin-filepath` can overflow the pipe buffer.
// https://github.com/oxc-project/oxc/issues/17939 (observed on macOS)
// @ts-expect-error: `_handle` is an internal API
if (!process.stdout.isTTY) process.stdout._handle?.setBlocking?.(true);
// stdin: In LSP mode (`--lsp`), VSCode communicates via stdin/stdout pipes.
// Rust reads stdin expecting blocking I/O, but non-blocking mode returns `EAGAIN` (os error 11).
// https://github.com/oxc-project/oxc/issues/20285
// @ts-expect-error: `_handle` is an internal API
if (!process.stdin.isTTY) process.stdin._handle?.setBlocking?.(true);
// LSP uses stdout for communication, so write logs to stderr to avoid breaking the protocol.
// Since LSP is handled on the Rust side, we have to check the flag here. (`runCli()` starts the server and waits)
// Also, for Oxfmt, this only actually affects loading JS/TS config files.
// The call to Prettier is currently done via `child_process`, so it won't break LSP.
if (args.includes("--lsp")) process.stdout.write = process.stderr.write.bind(process.stderr);
// Call the Rust CLI first, to parse args and determine mode
// NOTE: If the mode is formatter CLI, it will also perform formatting and return an exit code
const [mode, exitCode] = await runCli(
args,
process.env.VP_VERSION ? loadVitePlusConfig : loadJsConfig,
initExternalServices,
formatFile,
formatEmbeddedCode,
formatEmbeddedDoc,
sortTailwindClasses,
);
// Migration modes are handled by JS
if (mode === "init") {
await import("./cli/migration/init").then((m) => m.runInit());
return;
}
if (mode === "migrate:prettier") {
await import("./cli/migration/migrate-prettier").then((m) => m.runMigratePrettier());
return;
}
if (mode === "migrate:biome") {
await import("./cli/migration/migrate-biome").then((m) => m.runMigrateBiome());
return;
}
// Other modes are handled by Rust, just need to set `exitCode`
// Clean up worker pool to not V8 crashes on process exit
await disposeExternalServices();
// 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.
// https://nodejs.org/api/process.html#processexitcode
process.exitCode = exitCode!;
// Older Node.js versions have a race condition with ThreadsafeFunction cleanup that causes
// crashes on large codebases. Add a small delay to allow pending NAPI operations
// to complete before exit. Fixed in Node.js 25.4.0+ and backported to 24.13.1.
// See: https://github.com/nodejs/node/issues/55706
// Backport: https://nodejs.org/en/blog/release/v24.13.1
const [major, minor, patch] = process.versions.node.split(".").map(Number);
const hasNode24Fix = major === 24 && (minor > 13 || (minor === 13 && patch >= 1));
if (!hasNode24Fix && (major < 25 || (major === 25 && minor < 4))) {
setTimeout(() => process.exit(), 50);
}
})();