Files
oxc-project__oxc/apps/oxfmt/src-js/cli/worker-proxy.ts
leaysgur a26f856402 refactor(oxfmt): rename external_formatter to external_services (#25429)
`ExternalFormatter` is merely a transport that calls the JS side via napi; it is not referred to as a formatter.
2026-08-09 15:07:40 +00:00

106 lines
3.3 KiB
TypeScript

import Tinypool from "tinypool";
import { toFormatFileResult, toNullable } from "../libs/napi-callbacks";
import type { FormatFileResult } from "../libs/napi-callbacks";
import type {
FormatFileParam,
FormatEmbeddedCodeParam,
FormatEmbeddedDocParam,
SortTailwindClassesArgs,
} from "../libs/apis";
// Worker pool for parallel Prettier formatting
let pool: Tinypool | null = null;
let poolSize: number | null = null;
export async function initExternalServices(numThreads: number): Promise<void> {
// In LSP mode, this can be called repeatedly for the lifetime of the process.
// e.g. on every workspace folder build, config-triggered rebuild, etc
// The process-wide pool must never be recreated or destroyed on re-init:
// that leaks the previous `child_process` workers, and other workspace folders may have formats in-flight.
// (https://github.com/oxc-project/oxc/issues/24147)
// NOTE: `numThreads` never changes within a single session, so the first value wins.
poolSize ??= numThreads;
}
// Create the pool lazily on first use,
// so runs that never delegate to Prettier spawn no `child_process` workers at all.
// (e.g. Rust-tier files only)
async function getPool(): Promise<Tinypool> {
// Rust always calls `initExternalServices` before formatting, so this is defensive.
if (poolSize === null) throw new Error("External services are not initialized");
pool ??= new Tinypool({
filename: new URL("./cli-worker.js", import.meta.url).href,
minThreads: poolSize,
maxThreads: poolSize,
// XXX: Use `child_process` instead of `worker_threads`.
// Not sure why, but when using `worker_threads`,
// calls from NAPI (CLI) -> worker threads -> NAPI (prettier-plugin-oxfmt) causes a hang...
runtime: "child_process",
// When setting the `runtime: child_process`,
// `process.env` is not inherited (likely a bug), so it needs to be explicitly specified.
env: process.env as Record<string, string>,
});
return pool;
}
export async function disposeExternalServices(): Promise<void> {
await pool?.destroy();
pool = null;
poolSize = null;
}
// ---
export function formatFile(
options: FormatFileParam["options"],
code: string,
): Promise<FormatFileResult> {
return toFormatFileResult(
getPool().then((pool) =>
pool.run({ options, code } satisfies FormatFileParam, { name: "formatFile" }),
),
);
}
// ---
export function formatEmbeddedCode(
options: FormatEmbeddedCodeParam["options"],
code: string,
): Promise<string | null> {
return toNullable(
getPool().then((pool) =>
pool.run({ options, code } satisfies FormatEmbeddedCodeParam, {
name: "formatEmbeddedCode",
}),
),
);
}
export function formatEmbeddedDoc(
options: FormatEmbeddedDocParam["options"],
code: string,
): Promise<string | null> {
return toNullable(
getPool().then((pool) =>
pool.run({ options, code } satisfies FormatEmbeddedDocParam, {
name: "formatEmbeddedDoc",
}),
),
);
}
export function sortTailwindClasses(
options: SortTailwindClassesArgs["options"],
classes: string[],
): Promise<string[] | null> {
return toNullable(
getPool().then((pool) =>
pool.run({ classes, options } satisfies SortTailwindClassesArgs, {
name: "sortTailwindClasses",
}),
),
);
}