publint-all.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  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. // Run publint's JS CLI through the current node, not the .bin shim: the
  13. // extensionless shim isn't spawnable on Windows (CVE-2024-27980) and the .cmd
  14. // variant needs shell:true, which space-joins args UNESCAPED (DEP0190) and
  15. // breaks when the repo path contains spaces. The JS entry is identical on every
  16. // platform (`bin` is `./src/cli.js` per publint's package.json).
  17. const publintCli = resolve(root, 'node_modules/publint/src/cli.js')
  18. type PublintResult =
  19. | { path: string; status: 'passed'; stdout: string; stderr: string }
  20. | { path: string; status: 'failed'; stdout: string; stderr: string; message: string }
  21. function workspacePackages(): string[] {
  22. return readdirSync(packagesRoot, { withFileTypes: true })
  23. .filter(group => group.isDirectory())
  24. .flatMap(group =>
  25. readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true })
  26. .filter(pkg => pkg.isDirectory())
  27. .filter(pkg => existsSync(resolve(packagesRoot, group.name, pkg.name, 'package.json')))
  28. .map(pkg => `packages/${group.name}/${pkg.name}`),
  29. )
  30. }
  31. function publintConcurrency(total: number): number {
  32. if (total === 0) return 0
  33. const raw = process.env[CONCURRENCY_ENV]
  34. if (raw !== undefined) {
  35. const parsed = Number.parseInt(raw, 10)
  36. if (!Number.isSafeInteger(parsed) || parsed < 1) {
  37. throw new Error(`publint-all: ${CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`)
  38. }
  39. return Math.min(total, parsed)
  40. }
  41. return Math.min(total, availableParallelism())
  42. }
  43. function outputText(value: unknown): string {
  44. if (typeof value === 'string') return value
  45. if (Buffer.isBuffer(value)) return value.toString()
  46. return ''
  47. }
  48. async function runPublint(path: string): Promise<PublintResult> {
  49. try {
  50. const { stdout, stderr } = await execFileAsync(process.execPath, [publintCli, path], {
  51. cwd: root,
  52. encoding: 'utf8',
  53. maxBuffer: 10 * 1024 * 1024,
  54. })
  55. return { path, status: 'passed', stdout, stderr }
  56. } catch (error: unknown) {
  57. const failed = error as { stdout?: unknown; stderr?: unknown; message?: string }
  58. return {
  59. path,
  60. status: 'failed',
  61. stdout: outputText(failed.stdout),
  62. stderr: outputText(failed.stderr),
  63. message: failed.message ?? 'publint failed',
  64. }
  65. }
  66. }
  67. async function runAll(paths: string[], concurrency: number): Promise<PublintResult[]> {
  68. let next = 0
  69. const results: Array<PublintResult | undefined> = []
  70. await Promise.all(Array.from({ length: concurrency }, async () => {
  71. for (;;) {
  72. const index = next
  73. next += 1
  74. const path = paths[index]
  75. if (path === undefined) return
  76. results[index] = await runPublint(path)
  77. }
  78. }))
  79. return paths.map((path, index) => {
  80. const result = results[index]
  81. if (result === undefined) throw new Error(`publint-all: missing result for ${path}.`)
  82. return result
  83. })
  84. }
  85. function printResult(result: PublintResult): void {
  86. console.log(`Running publint for ${result.path}...`)
  87. process.stdout.write(result.stdout)
  88. process.stderr.write(result.stderr)
  89. if (result.status === 'failed') console.error(result.message)
  90. }
  91. const packages = workspacePackages()
  92. const concurrency = publintConcurrency(packages.length)
  93. console.log(`publint-all: linting ${packages.length} package(s) with ${concurrency} worker(s).`)
  94. const results = await runAll(packages, concurrency)
  95. for (const result of results) printResult(result)
  96. if (results.some(result => result.status === 'failed')) process.exit(1)