From 3da07e27508bc13abef03fe943cbbaed30f3ad63 Mon Sep 17 00:00:00 2001 From: Mike Olson Date: Thu, 30 Jul 2026 22:41:55 -0400 Subject: [PATCH] fix(oxlint-plugin): Resolve the oxlint bin without assuming a pnpm layout The rule-harness tests spawned node_modules/.pnpm/node_modules/.bin/oxlint, a pnpm virtual-store hoist path. oxlint is only a transitive dependency of vite-plus, so no package manager guarantees a stable .bin shim for it: under Bun-style hoisted installs that path does not exist, and the root node_modules/.bin/oxlint is vite-plus's LSP-only wrapper, so all 35 rule tests fail with spawn ENOENT. Resolve oxlint/package.json through vite-plus instead, at module scope so a broken install fails once with a clear resolution error, and run the package's own bin/oxlint through process.execPath so the spawn does not depend on shebang support or whichever node is first on PATH. --- oxlint-plugin-t3code/test/utils.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/oxlint-plugin-t3code/test/utils.ts b/oxlint-plugin-t3code/test/utils.ts index eb91d32d7d4..740da8a4f2b 100644 --- a/oxlint-plugin-t3code/test/utils.ts +++ b/oxlint-plugin-t3code/test/utils.ts @@ -9,6 +9,17 @@ import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; +import * as NodeModule from "node:module"; + +// oxlint is only a transitive dependency (via vite-plus), so its bin placement +// varies by package manager: pnpm hoists it into the virtual store, while +// other layouts expose vite-plus's LSP-only wrapper instead. Resolve the real +// package through vite-plus rather than hardcoding either layout, and do it at +// module scope so a broken install fails once with a resolution error instead +// of as an opaque defect in every test. +const oxlintPackageJsonPath = NodeModule.createRequire( + NodeModule.createRequire(import.meta.url).resolve("vite-plus/package.json"), +).resolve("oxlint/package.json"); class OxlintFixtureFailure extends Data.TaggedError("OxlintFixtureFailure")<{ readonly exitCode: number; @@ -93,14 +104,7 @@ export const createOxlintRuleHarness = ( const configPath = path.join(fixtureDir, ".oxlintrc.json"); const sourcePath = path.join(fixtureDir, options.filename ?? "fixture.ts"); const repoRoot = path.join(import.meta.dirname, "..", ".."); - const oxlintBin = path.join( - repoRoot, - "node_modules", - ".pnpm", - "node_modules", - ".bin", - "oxlint", - ); + const oxlintBin = path.join(path.dirname(oxlintPackageJsonPath), "bin", "oxlint"); const pluginPath = path.join(repoRoot, "oxlint-plugin-t3code", "index.ts"); yield* fs.writeFileString( @@ -112,8 +116,13 @@ export const createOxlintRuleHarness = ( ); yield* fs.writeFileString(sourcePath, source); + // Run through the current Node binary: oxlint's bin is an extensionless + // shebang script, which Windows cannot spawn directly and which would + // otherwise pick up whatever node is first on PATH. const output = yield* spawnAndCollectOutput( - ChildProcess.make(oxlintBin, ["--config", configPath, sourcePath], { cwd: repoRoot }), + ChildProcess.make(process.execPath, [oxlintBin, "--config", configPath, sourcePath], { + cwd: repoRoot, + }), ); if (output.exitCode !== 0) {