run-oxlint.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import { spawnSync } from 'node:child_process'
  2. import { resolve } from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
  5. const MAX_CAPTURED_OUTPUT_BYTES = 64 * 1024 * 1024
  6. const FIX_FLAGS = new Set(['--fix', '--fix-dangerously', '--fix-suggestions'])
  7. function isFixInvocation(args: readonly string[]): boolean {
  8. return args.some(arg => FIX_FLAGS.has(arg))
  9. }
  10. /** Complete Oxlint child-process arguments and environment. */
  11. export interface OxlintInvocation {
  12. readonly args: readonly string[]
  13. readonly env: NodeJS.ProcessEnv
  14. }
  15. /**
  16. * Apply the repository worker bound to both Oxlint backends.
  17. * @param args - Oxlint CLI arguments requested by the caller.
  18. * @param env - Environment inherited by the Oxlint process.
  19. * @returns the complete CLI arguments and child environment.
  20. */
  21. export function resolveOxlintInvocation(args: readonly string[], env: NodeJS.ProcessEnv): OxlintInvocation {
  22. const raw = env.DSH_OXLINT_THREADS
  23. if (raw === undefined || raw === '') return { args: [...args], env: { ...env } }
  24. const parsed = Number.parseInt(raw, 10)
  25. if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
  26. throw new Error(`run-oxlint: DSH_OXLINT_THREADS must be a positive integer, got ${JSON.stringify(raw)}.`)
  27. }
  28. if (args.some(arg => arg === '--threads' || arg.startsWith('--threads='))) {
  29. throw new Error('run-oxlint: use DSH_OXLINT_THREADS instead of passing --threads directly.')
  30. }
  31. return {
  32. args: [...args, `--threads=${raw}`],
  33. env: { ...env, GOMAXPROCS: raw },
  34. }
  35. }
  36. function completeFrom(result: { readonly signal: NodeJS.Signals | null; readonly status: number | null }): void {
  37. if (result.signal !== null) {
  38. process.kill(process.pid, result.signal)
  39. return
  40. }
  41. process.exitCode = result.status ?? 1
  42. }
  43. function main(): void {
  44. const invocation = resolveOxlintInvocation(process.argv.slice(2), process.env)
  45. if (!isFixInvocation(invocation.args)) {
  46. const result = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
  47. env: invocation.env,
  48. stdio: 'inherit',
  49. })
  50. if (result.error !== undefined) throw result.error
  51. completeFrom(result)
  52. return
  53. }
  54. const first = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
  55. encoding: 'utf8',
  56. env: invocation.env,
  57. maxBuffer: MAX_CAPTURED_OUTPUT_BYTES,
  58. })
  59. if (first.error !== undefined) throw first.error
  60. if (first.signal !== null) {
  61. completeFrom(first)
  62. return
  63. }
  64. if (first.status === 0) {
  65. process.stdout.write(first.stdout)
  66. process.stderr.write(first.stderr)
  67. process.exitCode = 0
  68. return
  69. }
  70. // Overlapping JS-plugin fixes can expose one more fixable diagnostic after the first pass.
  71. const second = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
  72. env: invocation.env,
  73. stdio: 'inherit',
  74. })
  75. if (second.error !== undefined) throw second.error
  76. completeFrom(second)
  77. }
  78. const entrypoint = process.argv[1]
  79. if (entrypoint !== undefined && resolve(entrypoint) === fileURLToPath(import.meta.url)) main()