From e673524976bb2f5b0483be26ed8f686bcf69b38c Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:39:49 +0800 Subject: [PATCH] refactor: centralize Oxlint worker bounds --- lefthook.yml | 2 +- package.json | 4 ++-- scripts/run-gates.spec.ts | 13 +++-------- scripts/run-gates.ts | 20 +++-------------- scripts/run-oxlint.spec.ts | 28 +++++++++++++++++++++++ scripts/run-oxlint.ts | 46 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 83 insertions(+), 30 deletions(-) create mode 100644 scripts/run-oxlint.spec.ts create mode 100644 scripts/run-oxlint.ts diff --git a/lefthook.yml b/lefthook.yml index 3308b11afb..1a4e004842 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -15,7 +15,7 @@ pre-commit: glob: '*.{ts,tsx,mts,cts,mjs}' exclude: - 'vendor/*/src/**' - run: node_modules/.bin/oxlint --fix --no-error-on-unmatched-pattern {staged_files} + run: node_modules/.bin/tsx scripts/run-oxlint.ts --fix --no-error-on-unmatched-pattern {staged_files} stage_fixed: true - name: whitespace (staged) diff --git a/package.json b/package.json index a5d545bf7f..ecb7215e66 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "clean": "tsx scripts/clean.ts", "change-scope": "tsx scripts/change-scope.ts", "typecheck": "tsc -b", - "lint": "oxlint .", - "lint:fix": "eslint --config eslint.format.config.mjs --fix . && oxlint . --fix", + "lint": "tsx scripts/run-oxlint.ts .", + "lint:fix": "eslint --config eslint.format.config.mjs --fix . && tsx scripts/run-oxlint.ts . --fix", "duplication": "jscpd --config .jscpd.json packages scripts", "test": "vitest run", "test:coverage": "vitest run --coverage", diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index edd2b6f9d1..ab80a7329b 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -121,24 +121,17 @@ describe('Oxlint gate', () => { }) }) - it('passes the configured worker bound to both Oxlint backends', () => { + it('surfaces the configured worker bound on the shared package script', () => { const subject = withEnv('DSH_OXLINT_THREADS', '4', () => withPnpmEntrypoint(() => gatesForMode('ci-lint')[0])) expect(subject).toMatchObject({ id: 'lint', - displayCommand: 'pnpm exec oxlint . --threads=4', + displayCommand: 'DSH_OXLINT_THREADS=4 pnpm run lint', command: process.execPath, - args: ['/private/pnpm.cjs', 'exec', 'oxlint', '.', '--threads=4'], - env: { GOMAXPROCS: '4' }, + args: ['/private/pnpm.cjs', 'run', 'lint'], }) }) - - it('rejects a non-positive or non-integer worker bound', () => { - expect(() => withEnv('DSH_OXLINT_THREADS', 'auto', () => - withPnpmEntrypoint(() => gatesForMode('ci-lint')))) - .toThrow('DSH_OXLINT_THREADS must be a positive integer') - }) }) describe('Node 24 consumer graph', () => { diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 229c7a8b1f..e95d2b6e49 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -377,24 +377,10 @@ function ciWindowsObservationalGates(): Gate[] { } function lintGate(): Gate { - const threadBound = oxlintThreadBound() - if (threadBound !== undefined) { - return pnpmExec('lint', ['oxlint', '.', `--threads=${threadBound}`], { - label: 'lint', - env: { GOMAXPROCS: threadBound }, - }) - } - return pnpmScript('lint', 'lint') -} - -function oxlintThreadBound(): string | undefined { const raw = process.env.DSH_OXLINT_THREADS - if (raw === undefined || raw === '') return undefined - const parsed = Number.parseInt(raw, 10) - if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) { - throw new Error(`run-gates: DSH_OXLINT_THREADS must be a positive integer, got ${JSON.stringify(raw)}.`) - } - return raw + return pnpmScript('lint', 'lint', raw === undefined || raw === '' + ? {} + : { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` }) } function coverageGate(): Gate { diff --git a/scripts/run-oxlint.spec.ts b/scripts/run-oxlint.spec.ts new file mode 100644 index 0000000000..25245add32 --- /dev/null +++ b/scripts/run-oxlint.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest' +import { resolveOxlintInvocation } from './run-oxlint.ts' + +describe('Oxlint invocation', () => { + it('preserves the ordinary default invocation', () => { + expect(resolveOxlintInvocation(['.'], { PATH: '/bin' })).toEqual({ + args: ['.'], + env: { PATH: '/bin' }, + }) + }) + + it('bounds both worker pools from one setting', () => { + expect(resolveOxlintInvocation(['.', '--fix'], { DSH_OXLINT_THREADS: '4', GOMAXPROCS: '12' })).toEqual({ + args: ['.', '--fix', '--threads=4'], + env: { DSH_OXLINT_THREADS: '4', GOMAXPROCS: '4' }, + }) + }) + + it.each(['0', '-1', '1.5', 'auto'])('rejects invalid worker bound %s', (value) => { + expect(() => resolveOxlintInvocation(['.'], { DSH_OXLINT_THREADS: value })) + .toThrow('DSH_OXLINT_THREADS must be a positive integer') + }) + + it('rejects a competing direct worker bound', () => { + expect(() => resolveOxlintInvocation(['.', '--threads=2'], { DSH_OXLINT_THREADS: '4' })) + .toThrow('use DSH_OXLINT_THREADS instead') + }) +}) diff --git a/scripts/run-oxlint.ts b/scripts/run-oxlint.ts new file mode 100644 index 0000000000..e833f75c69 --- /dev/null +++ b/scripts/run-oxlint.ts @@ -0,0 +1,46 @@ +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url)) + +/** Complete Oxlint child-process arguments and environment. */ +export interface OxlintInvocation { + readonly args: readonly string[] + readonly env: NodeJS.ProcessEnv +} + +/** + * Apply the repository worker bound to both Oxlint backends. + * @param args - Oxlint CLI arguments requested by the caller. + * @param env - Environment inherited by the Oxlint process. + * @returns the complete CLI arguments and child environment. + */ +export function resolveOxlintInvocation(args: readonly string[], env: NodeJS.ProcessEnv): OxlintInvocation { + const raw = env.DSH_OXLINT_THREADS + if (raw === undefined || raw === '') return { args: [...args], env: { ...env } } + const parsed = Number.parseInt(raw, 10) + if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) { + throw new Error(`run-oxlint: DSH_OXLINT_THREADS must be a positive integer, got ${JSON.stringify(raw)}.`) + } + if (args.some(arg => arg === '--threads' || arg.startsWith('--threads='))) { + throw new Error('run-oxlint: use DSH_OXLINT_THREADS instead of passing --threads directly.') + } + return { + args: [...args, `--threads=${raw}`], + env: { ...env, GOMAXPROCS: raw }, + } +} + +function main(): void { + const invocation = resolveOxlintInvocation(process.argv.slice(2), process.env) + const result = spawnSync(process.execPath, [oxlintCli, ...invocation.args], { + env: invocation.env, + stdio: 'inherit', + }) + if (result.error !== undefined) throw result.error + process.exitCode = result.status ?? 1 +} + +const entrypoint = process.argv[1] +if (entrypoint !== undefined && resolve(entrypoint) === fileURLToPath(import.meta.url)) main()