run-oxlint.ts 3.4 KB

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