From df4ea8e1e42734f71656e5b4c0dcf60b8e00a86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Thu, 11 Jun 2026 10:52:53 +0900 Subject: [PATCH 1/3] fix(cli): expose tsgolint binary --- packages/cli/bin/tsgolint | 14 ++++++++++++++ packages/cli/package.json | 1 + .../bin-tsgolint-wrapper/package.json | 5 +++++ .../snap-tests/bin-tsgolint-wrapper/snap.txt | 19 +++++++++++++++++++ .../bin-tsgolint-wrapper/src/index.ts | 1 + .../bin-tsgolint-wrapper/steps.json | 6 ++++++ .../bin-tsgolint-wrapper/tsconfig.json | 4 ++++ 7 files changed, 50 insertions(+) create mode 100755 packages/cli/bin/tsgolint create mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/package.json create mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/snap.txt create mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/src/index.ts create mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/steps.json create mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/tsconfig.json diff --git a/packages/cli/bin/tsgolint b/packages/cli/bin/tsgolint new file mode 100755 index 0000000000..81f4d63f6a --- /dev/null +++ b/packages/cli/bin/tsgolint @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +// Wrapper for oxlint-tsgolint. +// This exposes Vite+'s pinned tsgolint binary to tools that discover it from node_modules/.bin. + +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const require = createRequire(import.meta.url); +const tsgolintPackageJsonPath = require.resolve('oxlint-tsgolint/package.json'); +const tsgolintBin = join(dirname(tsgolintPackageJsonPath), 'bin', 'tsgolint.js'); + +await import(pathToFileURL(tsgolintBin).href); diff --git a/packages/cli/package.json b/packages/cli/package.json index ef6bd95d1a..377fece420 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -16,6 +16,7 @@ "bin": { "oxfmt": "./bin/oxfmt", "oxlint": "./bin/oxlint", + "tsgolint": "./bin/tsgolint", "vp": "./bin/vp" }, "files": [ diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/package.json b/packages/cli/snap-tests/bin-tsgolint-wrapper/package.json new file mode 100644 index 0000000000..f7c159b51c --- /dev/null +++ b/packages/cli/snap-tests/bin-tsgolint-wrapper/package.json @@ -0,0 +1,5 @@ +{ + "name": "bin-tsgolint-wrapper", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/snap.txt b/packages/cli/snap-tests/bin-tsgolint-wrapper/snap.txt new file mode 100644 index 0000000000..38b7a75739 --- /dev/null +++ b/packages/cli/snap-tests/bin-tsgolint-wrapper/snap.txt @@ -0,0 +1,19 @@ +> node ../node_modules/vite-plus/bin/tsgolint --help # should exercise import path +Warning: the `tsgolint` CLI entrypoint is unsupported! +Use Oxlint type-aware linting instead: https://oxc.rs/docs/guide/usage/linter/type-aware + +✨ tsgolint - speedy TypeScript linter + +Usage: + tsgolint [OPTIONS] + +Options: + --tsconfig PATH Which tsconfig to use. Defaults to tsconfig.json. + --list-files List matched files + -h, --help Show help + +> node ../node_modules/vite-plus/bin/tsgolint --tsconfig tsconfig.json --list-files | grep "Found file" # should allow --list-files mode +Warning: the `tsgolint` CLI entrypoint is unsupported! +Use Oxlint type-aware linting instead: https://oxc.rs/docs/guide/usage/linter/type-aware + +Found file: src/index.ts diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/src/index.ts b/packages/cli/snap-tests/bin-tsgolint-wrapper/src/index.ts new file mode 100644 index 0000000000..16e4ba982e --- /dev/null +++ b/packages/cli/snap-tests/bin-tsgolint-wrapper/src/index.ts @@ -0,0 +1 @@ +const value: number = 1; diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/steps.json b/packages/cli/snap-tests/bin-tsgolint-wrapper/steps.json new file mode 100644 index 0000000000..53804ca91f --- /dev/null +++ b/packages/cli/snap-tests/bin-tsgolint-wrapper/steps.json @@ -0,0 +1,6 @@ +{ + "commands": [ + "node ../node_modules/vite-plus/bin/tsgolint --help # should exercise import path", + "node ../node_modules/vite-plus/bin/tsgolint --tsconfig tsconfig.json --list-files | grep \"Found file\" # should allow --list-files mode" + ] +} diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/tsconfig.json b/packages/cli/snap-tests/bin-tsgolint-wrapper/tsconfig.json new file mode 100644 index 0000000000..f29a8c8751 --- /dev/null +++ b/packages/cli/snap-tests/bin-tsgolint-wrapper/tsconfig.json @@ -0,0 +1,4 @@ +{ + "compilerOptions": {}, + "include": ["src/**/*.ts"] +} From e122c6c15ea532d8ad82954c849f0f1a5c526683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Thu, 11 Jun 2026 12:22:13 +0900 Subject: [PATCH 2/3] fix(cli): set tsgolint path in oxlint wrapper --- packages/cli/bin/oxlint | 20 ++++++++++++++++++- packages/cli/bin/tsgolint | 14 ------------- packages/cli/package.json | 1 - .../bin-tsgolint-wrapper/package.json | 5 ----- .../snap-tests/bin-tsgolint-wrapper/snap.txt | 19 ------------------ .../bin-tsgolint-wrapper/src/index.ts | 1 - .../bin-tsgolint-wrapper/steps.json | 6 ------ .../bin-tsgolint-wrapper/tsconfig.json | 4 ---- 8 files changed, 19 insertions(+), 51 deletions(-) delete mode 100755 packages/cli/bin/tsgolint delete mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/package.json delete mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/snap.txt delete mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/src/index.ts delete mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/steps.json delete mode 100644 packages/cli/snap-tests/bin-tsgolint-wrapper/tsconfig.json diff --git a/packages/cli/bin/oxlint b/packages/cli/bin/oxlint index e1a0316937..b406d0cbb9 100755 --- a/packages/cli/bin/oxlint +++ b/packages/cli/bin/oxlint @@ -10,14 +10,31 @@ if (!process.argv.includes('--lsp')) { process.exit(1); } +import { existsSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import pkg from '../package.json' with { type: 'json' }; const require = createRequire(import.meta.url); const oxlintMainPath = require.resolve('oxlint'); const oxlintBin = join(dirname(dirname(oxlintMainPath)), 'bin', 'oxlint'); +const tsgolintPackageJsonPath = require.resolve('oxlint-tsgolint/package.json'); +let tsgolintBin = join(dirname(tsgolintPackageJsonPath), 'bin', 'tsgolint.js'); + +if (process.platform === 'win32') { + const scriptDir = dirname(fileURLToPath(import.meta.url)); + const localBinDir = join(scriptDir, '..', 'node_modules', '.bin'); + const oxlintTsgolintPackagePath = dirname(tsgolintPackageJsonPath); + const projectBinDir = join(oxlintTsgolintPackagePath, '..', '.bin'); + const candidates = [ + join(localBinDir, 'tsgolint.exe'), + join(localBinDir, 'tsgolint.cmd'), + join(projectBinDir, 'tsgolint.exe'), + join(projectBinDir, 'tsgolint.cmd'), + ]; + tsgolintBin = candidates.find((candidate) => existsSync(candidate)) ?? tsgolintBin; +} // This allows oxlint to load vite.config.ts. // For `vp check` and `vp lint`, VP_VERSION is injected by @@ -25,4 +42,5 @@ const oxlintBin = join(dirname(dirname(oxlintMainPath)), 'bin', 'oxlint'); // by `SubcommandResolver::resolve()`. process.env.VP_VERSION = pkg.version; process.env.VP_COMMAND ??= 'lint'; +process.env.OXLINT_TSGOLINT_PATH ??= tsgolintBin; await import(pathToFileURL(oxlintBin).href); diff --git a/packages/cli/bin/tsgolint b/packages/cli/bin/tsgolint deleted file mode 100755 index 81f4d63f6a..0000000000 --- a/packages/cli/bin/tsgolint +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node - -// Wrapper for oxlint-tsgolint. -// This exposes Vite+'s pinned tsgolint binary to tools that discover it from node_modules/.bin. - -import { createRequire } from 'node:module'; -import { dirname, join } from 'node:path'; -import { pathToFileURL } from 'node:url'; - -const require = createRequire(import.meta.url); -const tsgolintPackageJsonPath = require.resolve('oxlint-tsgolint/package.json'); -const tsgolintBin = join(dirname(tsgolintPackageJsonPath), 'bin', 'tsgolint.js'); - -await import(pathToFileURL(tsgolintBin).href); diff --git a/packages/cli/package.json b/packages/cli/package.json index 377fece420..ef6bd95d1a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -16,7 +16,6 @@ "bin": { "oxfmt": "./bin/oxfmt", "oxlint": "./bin/oxlint", - "tsgolint": "./bin/tsgolint", "vp": "./bin/vp" }, "files": [ diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/package.json b/packages/cli/snap-tests/bin-tsgolint-wrapper/package.json deleted file mode 100644 index f7c159b51c..0000000000 --- a/packages/cli/snap-tests/bin-tsgolint-wrapper/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "bin-tsgolint-wrapper", - "version": "0.0.0", - "private": true -} diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/snap.txt b/packages/cli/snap-tests/bin-tsgolint-wrapper/snap.txt deleted file mode 100644 index 38b7a75739..0000000000 --- a/packages/cli/snap-tests/bin-tsgolint-wrapper/snap.txt +++ /dev/null @@ -1,19 +0,0 @@ -> node ../node_modules/vite-plus/bin/tsgolint --help # should exercise import path -Warning: the `tsgolint` CLI entrypoint is unsupported! -Use Oxlint type-aware linting instead: https://oxc.rs/docs/guide/usage/linter/type-aware - -✨ tsgolint - speedy TypeScript linter - -Usage: - tsgolint [OPTIONS] - -Options: - --tsconfig PATH Which tsconfig to use. Defaults to tsconfig.json. - --list-files List matched files - -h, --help Show help - -> node ../node_modules/vite-plus/bin/tsgolint --tsconfig tsconfig.json --list-files | grep "Found file" # should allow --list-files mode -Warning: the `tsgolint` CLI entrypoint is unsupported! -Use Oxlint type-aware linting instead: https://oxc.rs/docs/guide/usage/linter/type-aware - -Found file: src/index.ts diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/src/index.ts b/packages/cli/snap-tests/bin-tsgolint-wrapper/src/index.ts deleted file mode 100644 index 16e4ba982e..0000000000 --- a/packages/cli/snap-tests/bin-tsgolint-wrapper/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -const value: number = 1; diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/steps.json b/packages/cli/snap-tests/bin-tsgolint-wrapper/steps.json deleted file mode 100644 index 53804ca91f..0000000000 --- a/packages/cli/snap-tests/bin-tsgolint-wrapper/steps.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "commands": [ - "node ../node_modules/vite-plus/bin/tsgolint --help # should exercise import path", - "node ../node_modules/vite-plus/bin/tsgolint --tsconfig tsconfig.json --list-files | grep \"Found file\" # should allow --list-files mode" - ] -} diff --git a/packages/cli/snap-tests/bin-tsgolint-wrapper/tsconfig.json b/packages/cli/snap-tests/bin-tsgolint-wrapper/tsconfig.json deleted file mode 100644 index f29a8c8751..0000000000 --- a/packages/cli/snap-tests/bin-tsgolint-wrapper/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "compilerOptions": {}, - "include": ["src/**/*.ts"] -} From cadb0127aa2b104770ca343df8f3846283ff6250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Thu, 11 Jun 2026 15:01:43 +0900 Subject: [PATCH 3/3] refactor(cli): share tsgolint path resolution --- packages/cli/bin/oxlint | 23 +++------- packages/cli/src/resolve-lint.ts | 58 +++---------------------- packages/cli/src/utils/tsgolint-path.ts | 55 +++++++++++++++++++++++ packages/cli/tsdown.config.ts | 1 + 4 files changed, 68 insertions(+), 69 deletions(-) create mode 100644 packages/cli/src/utils/tsgolint-path.ts diff --git a/packages/cli/bin/oxlint b/packages/cli/bin/oxlint index b406d0cbb9..e135a09cac 100755 --- a/packages/cli/bin/oxlint +++ b/packages/cli/bin/oxlint @@ -10,31 +10,20 @@ if (!process.argv.includes('--lsp')) { process.exit(1); } -import { existsSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { pathToFileURL } from 'node:url'; +import { resolveTsgolintExecutable } from '../dist/tsgolint-path.js'; import pkg from '../package.json' with { type: 'json' }; const require = createRequire(import.meta.url); const oxlintMainPath = require.resolve('oxlint'); const oxlintBin = join(dirname(dirname(oxlintMainPath)), 'bin', 'oxlint'); const tsgolintPackageJsonPath = require.resolve('oxlint-tsgolint/package.json'); -let tsgolintBin = join(dirname(tsgolintPackageJsonPath), 'bin', 'tsgolint.js'); - -if (process.platform === 'win32') { - const scriptDir = dirname(fileURLToPath(import.meta.url)); - const localBinDir = join(scriptDir, '..', 'node_modules', '.bin'); - const oxlintTsgolintPackagePath = dirname(tsgolintPackageJsonPath); - const projectBinDir = join(oxlintTsgolintPackagePath, '..', '.bin'); - const candidates = [ - join(localBinDir, 'tsgolint.exe'), - join(localBinDir, 'tsgolint.cmd'), - join(projectBinDir, 'tsgolint.exe'), - join(projectBinDir, 'tsgolint.cmd'), - ]; - tsgolintBin = candidates.find((candidate) => existsSync(candidate)) ?? tsgolintBin; -} +const tsgolintBin = resolveTsgolintExecutable( + join(dirname(tsgolintPackageJsonPath), 'bin', 'tsgolint.js'), + import.meta.url, +); // This allows oxlint to load vite.config.ts. // For `vp check` and `vp lint`, VP_VERSION is injected by diff --git a/packages/cli/src/resolve-lint.ts b/packages/cli/src/resolve-lint.ts index e1b0fd598d..faa13fc77a 100644 --- a/packages/cli/src/resolve-lint.ts +++ b/packages/cli/src/resolve-lint.ts @@ -11,35 +11,12 @@ * provides ESLint-compatible linting with significantly better performance. */ -import { existsSync, realpathSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; import { DEFAULT_ENVS, resolve } from './utils/constants.ts'; +import { resolveTsgolintExecutable } from './utils/tsgolint-path.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; -} +export { resolveWindowsTsgolintExecutable } from './utils/tsgolint-path.ts'; /** * Resolves the oxlint binary path and environment variables. @@ -63,33 +40,10 @@ export async function lint(): Promise<{ const oxlintMainPath = resolve('oxlint'); const oxlintPackageRoot = dirname(dirname(oxlintMainPath)); const binPath = join(oxlintPackageRoot, 'bin', 'oxlint'); - let oxlintTsgolintPath = resolve('oxlint-tsgolint/bin/tsgolint'); - if (process.platform === 'win32') { - // On Windows, try .exe first (bun creates .exe), then .cmd (npm/pnpm/yarn create .cmd) - const scriptDir = dirname(fileURLToPath(import.meta.url)); - const localBinDir = join(scriptDir, '..', 'node_modules', '.bin'); - const oxlintTsgolintPackagePath = dirname(dirname(oxlintTsgolintPath)); - const projectBinDir = join(oxlintTsgolintPackagePath, '..', '.bin'); - const pathCandidates = [ - join(localBinDir, 'tsgolint.exe'), - join(localBinDir, 'tsgolint.cmd'), - join(projectBinDir, 'tsgolint.exe'), - join(projectBinDir, 'tsgolint.cmd'), - ]; - 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'); - 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 oxlintTsgolintPath = resolveTsgolintExecutable( + resolve('oxlint-tsgolint/bin/tsgolint'), + import.meta.url, + ); const result = { binPath, // TODO: provide envs inference API diff --git a/packages/cli/src/utils/tsgolint-path.ts b/packages/cli/src/utils/tsgolint-path.ts new file mode 100644 index 0000000000..e98f1d41ca --- /dev/null +++ b/packages/cli/src/utils/tsgolint-path.ts @@ -0,0 +1,55 @@ +import { existsSync, realpathSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +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; +} + +export function resolveTsgolintExecutable(tsgolintBinPath: string, scriptUrl: string): string { + if (process.platform !== 'win32') { + return tsgolintBinPath; + } + + // On Windows, try .exe first (bun creates .exe), then .cmd (npm/pnpm/yarn create .cmd) + const scriptDir = dirname(fileURLToPath(scriptUrl)); + const localBinDir = join(scriptDir, '..', 'node_modules', '.bin'); + const oxlintTsgolintPackagePath = dirname(dirname(tsgolintBinPath)); + const projectBinDir = join(oxlintTsgolintPackagePath, '..', '.bin'); + const pathCandidates = [ + join(localBinDir, 'tsgolint.exe'), + join(localBinDir, 'tsgolint.cmd'), + join(projectBinDir, 'tsgolint.exe'), + join(projectBinDir, 'tsgolint.cmd'), + ]; + + return 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'); + return [join(realBinDir, 'tsgolint.exe'), join(realBinDir, 'tsgolint.cmd')]; + }, + }); +} diff --git a/packages/cli/tsdown.config.ts b/packages/cli/tsdown.config.ts index f9a1f360bf..9b1f2e8bff 100644 --- a/packages/cli/tsdown.config.ts +++ b/packages/cli/tsdown.config.ts @@ -29,6 +29,7 @@ export default defineConfig([ fmt: './src/fmt.ts', lint: './src/lint.ts', 'oxlint-plugin': './src/oxlint-plugin.ts', + 'tsgolint-path': './src/utils/tsgolint-path.ts', pack: './src/pack.ts', 'pack-bin': './src/pack-bin.ts', // Global commands — explicit entries ensure lazy loading via dynamic import in bin.ts.