From 40e9b735fbbf8e0a88e1982f212d020aebee1e41 Mon Sep 17 00:00:00 2001 From: semimikoh Date: Thu, 4 Jun 2026 13:49:01 +0900 Subject: [PATCH 1/2] fix(cli): keep absolute tsgolint path for workspace lint runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows-only `relative(process.cwd(), ...)` conversion downgraded an already-resolved absolute path. When oxlint is spawned with a different cwd than the launcher (the workspace package dir under `vp run -r`), the root-relative path resolved against the wrong base and failed — pnpm's `.pnpm` only exists at the monorepo root. Extract `resolveWindowsTsgolintExecutable` and add a regression test that locks the resolved path as absolute (never relativized). Closes #1747 --- .../cli/src/__tests__/resolve-lint.spec.ts | 19 +++++- packages/cli/src/resolve-lint.ts | 58 ++++++++++++------- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/__tests__/resolve-lint.spec.ts b/packages/cli/src/__tests__/resolve-lint.spec.ts index de1c7755f9..ff7be75548 100644 --- a/packages/cli/src/__tests__/resolve-lint.spec.ts +++ b/packages/cli/src/__tests__/resolve-lint.spec.ts @@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'; import { describe, expect, it } from '@voidzero-dev/vite-plus-test'; -import { lint } from '../resolve-lint.js'; +import { lint, resolveWindowsTsgolintExecutable } from '../resolve-lint.js'; describe('resolve-lint', () => { it('should resolve binPath and OXLINT_TSGOLINT_PATH to existing files', async () => { @@ -21,4 +21,21 @@ describe('resolve-lint', () => { `OXLINT_TSGOLINT_PATH should point to an existing file, got: ${tsgolintPath}`, ).toBe(true); }); + + it('should keep the Windows tsgolint executable path absolute', () => { + const tsgolintPath = + 'C:\\repo\\node_modules\\.pnpm\\vite-plus@0.1.24\\node_modules\\vite-plus\\node_modules\\.bin\\tsgolint.cmd'; + const result = resolveWindowsTsgolintExecutable( + [ + 'C:\\repo\\node_modules\\.pnpm\\vite-plus@0.1.24\\node_modules\\vite-plus\\node_modules\\.bin\\tsgolint.exe', + tsgolintPath, + ], + { + exists: (path) => path === tsgolintPath, + }, + ); + + expect(result).toBe(tsgolintPath); + expect(result).not.toMatch(/^\.\\/); + }); }); diff --git a/packages/cli/src/resolve-lint.ts b/packages/cli/src/resolve-lint.ts index 671b88c650..818fcfc90d 100644 --- a/packages/cli/src/resolve-lint.ts +++ b/packages/cli/src/resolve-lint.ts @@ -13,11 +13,35 @@ import { existsSync, realpathSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import { relative } from 'node:path/win32'; import { fileURLToPath } from 'node:url'; import { DEFAULT_ENVS, resolve } from './utils/constants.ts'; +export function resolveWindowsTsgolintExecutable( + pathCandidates: string[], + options: { + exists: (path: string) => boolean; + getRealpathCandidates?: () => string[]; + }, +): string { + let oxlintTsgolintPath = pathCandidates.find((p) => options.exists(p)) ?? ''; + if (!oxlintTsgolintPath && options.getRealpathCandidates) { + try { + oxlintTsgolintPath = + options.getRealpathCandidates().find((p) => options.exists(p)) ?? ''; + } catch { + // realpath failed, fall through to default + } + } + if (!oxlintTsgolintPath) { + throw new Error( + 'Unable to resolve oxlint-tsgolint executable, tried:\n' + + pathCandidates.map((path) => `- ${path}`).join('\n'), + ); + } + return oxlintTsgolintPath; +} + /** * Resolves the oxlint binary path and environment variables. * @@ -53,29 +77,19 @@ export async function lint(): Promise<{ join(projectBinDir, 'tsgolint.exe'), join(projectBinDir, 'tsgolint.cmd'), ]; - oxlintTsgolintPath = pathCandidates.find((p) => existsSync(p)) ?? ''; - // Bun stores packages in .bun/ cache dirs where the symlinked paths above won't match. - if (!oxlintTsgolintPath) { - try { + oxlintTsgolintPath = resolveWindowsTsgolintExecutable(pathCandidates, { + exists: existsSync, + // Bun stores packages in .bun/ cache dirs where the symlinked paths above won't match. + getRealpathCandidates: () => { const realPkgDir = realpathSync(join(scriptDir, '..')); const realBinDir = join(dirname(realPkgDir), '.bin'); - oxlintTsgolintPath = - [join(realBinDir, 'tsgolint.exe'), join(realBinDir, 'tsgolint.cmd')].find((p) => - existsSync(p), - ) ?? ''; - } catch { - // realpath failed, fall through to default - } - } - if (!oxlintTsgolintPath) { - throw new Error( - 'Unable to resolve oxlint-tsgolint executable, tried:\n' + - pathCandidates.map((path) => `- ${path}`).join('\n'), - ); - } - const relativePath = relative(process.cwd(), oxlintTsgolintPath); - // Only prepend .\ if it's actually a relative path (not an absolute path returned by relative()) - oxlintTsgolintPath = /^[a-zA-Z]:/.test(relativePath) ? relativePath : `.\\${relativePath}`; + return [join(realBinDir, 'tsgolint.exe'), join(realBinDir, 'tsgolint.cmd')]; + }, + }); + // Keep the resolved absolute path. oxlint may be spawned with a different cwd than + // this launcher (e.g. the workspace package dir under `vp run -r`), where a path made + // relative to the launcher's process.cwd() would resolve against the wrong base + // directory and fail (e.g. pnpm's `.pnpm` only exists at the monorepo root). } const result = { binPath, From e00d119c6b883b71c6be183883775eea2ef01b7e Mon Sep 17 00:00:00 2001 From: semimikoh Date: Thu, 4 Jun 2026 14:49:42 +0900 Subject: [PATCH 2/2] style(cli): format resolve-lint --- packages/cli/src/resolve-lint.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/src/resolve-lint.ts b/packages/cli/src/resolve-lint.ts index 818fcfc90d..e1b0fd598d 100644 --- a/packages/cli/src/resolve-lint.ts +++ b/packages/cli/src/resolve-lint.ts @@ -27,8 +27,7 @@ export function resolveWindowsTsgolintExecutable( let oxlintTsgolintPath = pathCandidates.find((p) => options.exists(p)) ?? ''; if (!oxlintTsgolintPath && options.getRealpathCandidates) { try { - oxlintTsgolintPath = - options.getRealpathCandidates().find((p) => options.exists(p)) ?? ''; + oxlintTsgolintPath = options.getRealpathCandidates().find((p) => options.exists(p)) ?? ''; } catch { // realpath failed, fall through to default }