Parcourir la source

refactor: centralize Oxlint worker bounds

Tianyi Cui il y a 1 mois
Parent
commit
e673524976
6 fichiers modifiés avec 83 ajouts et 30 suppressions
  1. 1 1
      lefthook.yml
  2. 2 2
      package.json
  3. 3 10
      scripts/run-gates.spec.ts
  4. 3 17
      scripts/run-gates.ts
  5. 28 0
      scripts/run-oxlint.spec.ts
  6. 46 0
      scripts/run-oxlint.ts

+ 1 - 1
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)

+ 2 - 2
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",

+ 3 - 10
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', () => {

+ 3 - 17
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 {

+ 28 - 0
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')
+  })
+})

+ 46 - 0
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()