diff --git a/.changeset/biome-native-binary.md b/.changeset/biome-native-binary.md new file mode 100644 index 0000000..72abc19 --- /dev/null +++ b/.changeset/biome-native-binary.md @@ -0,0 +1,5 @@ +--- +"diagnostics-webpack-plugin": patch +--- + +Run Biome's binary directly rather than through its Node launcher, which started a Node process of its own on every run only to start the binary. diff --git a/README.md b/README.md index a59d455..8357b3a 100644 --- a/README.md +++ b/README.md @@ -772,6 +772,10 @@ new DiagnosticsPlugin({ checks: ["biome"] }); Like the oxlint check this runs a binary and reads the JSON it answers with, which Biome calls an experimental reporter — a release of its own may move the shape, and it is the only reporter carrying the severities this check needs. +The binary is the one published for the platform beside `@biomejs/biome`, or +what `BIOME_BINARY` names, as Biome's own launcher finds it — run directly +rather than through that launcher, which is a Node process of its own on every +run for nothing but to start it. Biome's project and types rules — `noImportCycles`, `noFloatingPromises` — answer for a file from the files it imports, and which rules those are is not diff --git a/src/checks/biome.js b/src/checks/biome.js index 2d49f57..c15523f 100644 --- a/src/checks/biome.js +++ b/src/checks/biome.js @@ -67,6 +67,75 @@ function formatDiagnostic(filename, diagnostic) { return `${filename}${at} ${diagnostic.severity} ${diagnostic.message}${rule}`; } +/** @type {Map} */ +const natives = new Map(); + +// Where each platform's build of Biome is published, as Biome's own launcher +// names them. +/** @type {Record>} */ +const PLATFORMS = { + win32: { + x64: "@biomejs/cli-win32-x64/biome.exe", + arm64: "@biomejs/cli-win32-arm64/biome.exe", + }, + darwin: { + x64: "@biomejs/cli-darwin-x64/biome", + arm64: "@biomejs/cli-darwin-arm64/biome", + }, + linux: { + x64: "@biomejs/cli-linux-x64/biome", + arm64: "@biomejs/cli-linux-arm64/biome", + }, + "linux-musl": { + x64: "@biomejs/cli-linux-x64-musl/biome", + arm64: "@biomejs/cli-linux-arm64-musl/biome", + }, +}; + +/** + * @param {string} script the launcher + * @returns {string | undefined} the binary published for this platform beside it + */ +function lookUpNative(script) { + // Node built against glibc says which one; on musl it says nothing. + const { header } = /** @type {EXPECTED_ANY} */ (process.report.getReport()); + const platform = + process.platform === "linux" && !header.glibcVersionRuntime + ? "linux-musl" + : process.platform; + const published = PLATFORMS[platform] && PLATFORMS[platform][process.arch]; + + if (!published) return undefined; + + try { + // Resolved from the launcher, whose optional dependency the build is. + return createRequire(script).resolve(published); + } catch { + return undefined; + } +} + +/** + * The binary Biome's launcher would run, found as the launcher finds it: what + * `BIOME_BINARY` names, or the build published for this platform beside the + * package. The launcher is a Node process of its own on every run, for nothing + * but to start this one. + * @param {string} script the launcher + * @returns {string | undefined} the binary, or nothing where none is found + */ +function findNative(script) { + if (process.env.BIOME_BINARY) return process.env.BIOME_BINARY; + + // Asked by every compilation, and the answer is the machine's. + if (natives.has(script)) return natives.get(script); + + const found = lookUpNative(script); + + natives.set(script, found); + + return found; +} + /** * @param {CheckContext} context check context * @returns {Promise} biome check @@ -76,6 +145,7 @@ async function create({ options }) { String(options.biomePath || "@biomejs/biome"), "biome", ); + const native = findNative(script); const cwd = String(options.context); const flags = [ String(options.command || "lint"), @@ -96,9 +166,10 @@ async function create({ options }) { for (let i = 0; i < files.length; i += FILES_PER_RUN) { const answer = await runJson( - script, + native || script, [...flags, ...files.slice(i, i + FILES_PER_RUN)], cwd, + Boolean(native), ); found.push(...(answer.diagnostics || [])); @@ -169,3 +240,5 @@ export default { // of those rules can write `readsAcrossFiles: false`. readsAcrossFiles: () => true, }; + +export { findNative }; diff --git a/src/cli.js b/src/cli.js index 4e8cbf6..7da961e 100644 --- a/src/cli.js +++ b/src/cli.js @@ -35,16 +35,17 @@ function findBinary(specifier, binary) { * Runs a tool and reads the JSON it answers with on stdout. A tool that found * something answers non-zero, which is not a failure to run; an empty answer * with one is. - * @param {string} script the entry to run + * @param {string} script the entry to run, a script Node runs or a binary * @param {string[]} args what to run it with * @param {string} cwd where to run it + * @param {boolean=} native whether the entry is a binary run on its own * @returns {Promise} what it answered */ -function runJson(script, args, cwd) { +function runJson(script, args, cwd, native = false) { return new Promise((promiseResolve, reject) => { execFile( - process.execPath, - [script, ...args], + native ? script : process.execPath, + native ? args : [script, ...args], { cwd, maxBuffer: OUTPUT_LIMIT }, (error, stdout) => { if (!stdout && error) { diff --git a/test/biome/native.test.js b/test/biome/native.test.js new file mode 100644 index 0000000..6f45030 --- /dev/null +++ b/test/biome/native.test.js @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { afterEach, describe, it } from "node:test"; + +import { findNative } from "../../src/checks/biome.js"; + +const require = createRequire(import.meta.url); +const launcher = join( + require.resolve("@biomejs/biome/package.json"), + "..", + "bin", + "biome", +); + +describe("biome binary", () => { + afterEach(() => { + delete process.env.BIOME_BINARY; + }); + + it("should run the build published for this platform rather than its launcher", () => { + const found = /** @type {string} */ (findNative(launcher)); + + assert.ok(existsSync(found), found); + assert.match(basename(found), /^biome(?:\.exe)?$/u); + assert.match(found, /@biomejs[\\/]cli-/u); + }); + + it("should run what BIOME_BINARY names, as the launcher does", () => { + process.env.BIOME_BINARY = "/opt/biome"; + + assert.strictEqual(findNative(launcher), "/opt/biome"); + }); + + it("should leave a launcher with no build beside it to run itself", () => { + const alone = mkdtempSync(join(tmpdir(), "biome-alone-")); + + try { + const script = join(alone, "biome"); + + writeFileSync(script, ""); + + assert.strictEqual(findNative(script), undefined); + } finally { + rmSync(alone, { recursive: true, force: true }); + } + }); +}); diff --git a/types/checks/biome.d.ts b/types/checks/biome.d.ts index 64dc9c3..e69931f 100644 --- a/types/checks/biome.d.ts +++ b/types/checks/biome.d.ts @@ -61,3 +61,12 @@ export type Diagnostic = { * @returns {Promise} biome check */ declare function create({ options }: CheckContext): Promise; +/** + * The binary Biome's launcher would run, found as the launcher finds it: what + * `BIOME_BINARY` names, or the build published for this platform beside the + * package. The launcher is a Node process of its own on every run, for nothing + * but to start this one. + * @param {string} script the launcher + * @returns {string | undefined} the binary, or nothing where none is found + */ +export function findNative(script: string): string | undefined; diff --git a/types/cli.d.ts b/types/cli.d.ts index 6e456bc..b9d19a2 100644 --- a/types/cli.d.ts +++ b/types/cli.d.ts @@ -53,15 +53,17 @@ export function groupByFile( * Runs a tool and reads the JSON it answers with on stdout. A tool that found * something answers non-zero, which is not a failure to run; an empty answer * with one is. - * @param {string} script the entry to run + * @param {string} script the entry to run, a script Node runs or a binary * @param {string[]} args what to run it with * @param {string} cwd where to run it + * @param {boolean=} native whether the entry is a binary run on its own * @returns {Promise} what it answered */ export function runJson( script: string, args: string[], cwd: string, + native?: boolean | undefined, ): Promise; /** * Splits a file's diagnostics by the severity the tool gave them, so that