publint-all.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import { execFile } from 'node:child_process'
  2. import { existsSync, readdirSync } from 'node:fs'
  3. import { availableParallelism } from 'node:os'
  4. import { resolve } from 'node:path'
  5. import { promisify } from 'node:util'
  6. const execFileAsync = promisify(execFile)
  7. const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY'
  8. // Discover harness packages at packages/<group>/<pkg>; group containers,
  9. // examples, and private vendored sources are not package targets.
  10. const root = resolve(import.meta.dirname, '..')
  11. const packagesRoot = resolve(root, 'packages')
  12. type PublintResult =
  13. | { path: string; status: 'passed'; stdout: string; stderr: string }
  14. | { path: string; status: 'failed'; stdout: string; stderr: string; message: string }
  15. function workspacePackages(): string[] {
  16. return readdirSync(packagesRoot, { withFileTypes: true })
  17. .filter(group => group.isDirectory())
  18. .flatMap(group =>
  19. readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true })
  20. .filter(pkg => pkg.isDirectory())
  21. .filter(pkg => existsSync(resolve(packagesRoot, group.name, pkg.name, 'package.json')))
  22. .map(pkg => `packages/${group.name}/${pkg.name}`),
  23. )
  24. }
  25. function publintConcurrency(total: number): number {
  26. if (total === 0) return 0
  27. const raw = process.env[CONCURRENCY_ENV]
  28. if (raw !== undefined) {
  29. const parsed = Number.parseInt(raw, 10)
  30. if (!Number.isSafeInteger(parsed) || parsed < 1) {
  31. throw new Error(`publint-all: ${CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`)
  32. }
  33. return Math.min(total, parsed)
  34. }
  35. return Math.min(total, availableParallelism())
  36. }
  37. function outputText(value: unknown): string {
  38. if (typeof value === 'string') return value
  39. if (Buffer.isBuffer(value)) return value.toString()
  40. return ''
  41. }
  42. async function runPublint(path: string): Promise<PublintResult> {
  43. try {
  44. const { stdout, stderr } = await execFileAsync('node_modules/.bin/publint', [path], {
  45. cwd: root,
  46. encoding: 'utf8',
  47. maxBuffer: 10 * 1024 * 1024,
  48. })
  49. return { path, status: 'passed', stdout, stderr }
  50. } catch (error: unknown) {
  51. const failed = error as { stdout?: unknown; stderr?: unknown; message?: string }
  52. return {
  53. path,
  54. status: 'failed',
  55. stdout: outputText(failed.stdout),
  56. stderr: outputText(failed.stderr),
  57. message: failed.message ?? 'publint failed',
  58. }
  59. }
  60. }
  61. async function runAll(paths: string[], concurrency: number): Promise<PublintResult[]> {
  62. let next = 0
  63. const results: Array<PublintResult | undefined> = []
  64. await Promise.all(Array.from({ length: concurrency }, async () => {
  65. for (;;) {
  66. const index = next
  67. next += 1
  68. const path = paths[index]
  69. if (path === undefined) return
  70. results[index] = await runPublint(path)
  71. }
  72. }))
  73. return paths.map((path, index) => {
  74. const result = results[index]
  75. if (result === undefined) throw new Error(`publint-all: missing result for ${path}.`)
  76. return result
  77. })
  78. }
  79. function printResult(result: PublintResult): void {
  80. console.log(`Running publint for ${result.path}...`)
  81. process.stdout.write(result.stdout)
  82. process.stderr.write(result.stderr)
  83. if (result.status === 'failed') console.error(result.message)
  84. }
  85. const packages = workspacePackages()
  86. const concurrency = publintConcurrency(packages.length)
  87. console.log(`publint-all: linting ${packages.length} package(s) with ${concurrency} worker(s).`)
  88. const results = await runAll(packages, concurrency)
  89. for (const result of results) printResult(result)
  90. if (results.some(result => result.status === 'failed')) process.exit(1)