publint-all.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /** Run publint over the exact manifest-declared publication view of every package. */
  2. import {
  3. globSync,
  4. readFileSync,
  5. readdirSync,
  6. statSync,
  7. } from 'node:fs'
  8. import { availableParallelism } from 'node:os'
  9. import { dirname, relative, resolve, sep } from 'node:path'
  10. import { publint, type Message, type PackFile } from 'publint'
  11. import { formatMessage } from 'publint/utils'
  12. const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY'
  13. const repositoryRoot = resolve(import.meta.dirname, '..')
  14. const options = parseOptions(process.argv.slice(2))
  15. const packagesRoot = resolve(options.get('--packages-root') ?? repositoryRoot)
  16. interface PackageTarget {
  17. path: string
  18. directory: string
  19. manifest: PackageManifest
  20. }
  21. interface PackageManifest {
  22. name?: string
  23. files?: unknown
  24. }
  25. type PublintResult =
  26. | { path: string; status: 'passed'; messages: Message[]; manifest: Record<string, unknown> }
  27. | { path: string; status: 'failed'; messages: Message[]; manifest: Record<string, unknown>; failure?: string }
  28. function workspacePackages(): PackageTarget[] {
  29. return globSync('packages/*/*/package.json', { cwd: packagesRoot })
  30. .sort()
  31. .map((manifestPath) => {
  32. const absoluteManifestPath = resolve(packagesRoot, manifestPath)
  33. const manifest = JSON.parse(readFileSync(absoluteManifestPath, 'utf8')) as PackageManifest
  34. return { path: dirname(manifestPath), directory: dirname(absoluteManifestPath), manifest }
  35. })
  36. }
  37. function publintConcurrency(total: number): number {
  38. if (total === 0) return 0
  39. const raw = process.env[CONCURRENCY_ENV]
  40. if (raw !== undefined && raw !== '') {
  41. const parsed = Number.parseInt(raw, 10)
  42. if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
  43. throw new Error(`publint-all: ${CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`)
  44. }
  45. return Math.min(total, parsed)
  46. }
  47. return Math.min(total, availableParallelism())
  48. }
  49. function publicationFiles(target: PackageTarget): PackFile[] {
  50. const paths = new Set<string>()
  51. addPath(resolve(target.directory, 'package.json'), paths)
  52. const declared = Array.isArray(target.manifest.files)
  53. ? target.manifest.files.filter((value): value is string => typeof value === 'string')
  54. : []
  55. for (const pattern of [
  56. ...declared,
  57. 'README*',
  58. 'LICENSE*',
  59. 'LICENCE*',
  60. 'CHANGELOG*',
  61. 'CHANGES*',
  62. 'HISTORY*',
  63. 'NOTICE*',
  64. ]) {
  65. for (const match of globSync(pattern, { cwd: target.directory })) {
  66. addPath(resolve(target.directory, match), paths)
  67. }
  68. }
  69. return [...paths]
  70. .sort()
  71. .map(path => ({
  72. name: `package/${relative(target.directory, path).split(sep).join('/')}`,
  73. data: readFileSync(path),
  74. }))
  75. }
  76. function addPath(path: string, paths: Set<string>): void {
  77. const stat = statSync(path)
  78. if (stat.isDirectory()) {
  79. for (const entry of readdirSync(path)) addPath(resolve(path, entry), paths)
  80. } else if (stat.isFile()) {
  81. paths.add(path)
  82. }
  83. }
  84. async function runPublint(target: PackageTarget): Promise<PublintResult> {
  85. try {
  86. const result = await publint({
  87. pkgDir: 'package',
  88. pack: { files: publicationFiles(target) },
  89. })
  90. const manifest = result.pkg as Record<string, unknown>
  91. return result.messages.some(message => message.type === 'error')
  92. ? { path: target.path, status: 'failed', messages: result.messages, manifest }
  93. : { path: target.path, status: 'passed', messages: result.messages, manifest }
  94. } catch (error: unknown) {
  95. return {
  96. path: target.path,
  97. status: 'failed',
  98. messages: [],
  99. manifest: target.manifest as Record<string, unknown>,
  100. failure: error instanceof Error ? error.message : String(error),
  101. }
  102. }
  103. }
  104. async function runAll(targets: PackageTarget[], concurrency: number): Promise<PublintResult[]> {
  105. let next = 0
  106. const results: Array<PublintResult | undefined> = []
  107. await Promise.all(Array.from({ length: concurrency }, async () => {
  108. for (;;) {
  109. const index = next
  110. next += 1
  111. const target = targets[index]
  112. if (target === undefined) return
  113. results[index] = await runPublint(target)
  114. }
  115. }))
  116. return targets.map((target, index) => {
  117. const result = results[index]
  118. if (result === undefined) throw new Error(`publint-all: missing result for ${target.path}.`)
  119. return result
  120. })
  121. }
  122. function printResult(result: PublintResult): void {
  123. console.log(`Running publint for ${result.path}...`)
  124. if ('failure' in result) console.error(result.failure)
  125. for (const message of result.messages) {
  126. console.log(formatMessage(message, result.manifest, { color: false }) ?? message.code)
  127. }
  128. if (result.status === 'passed' && result.messages.length === 0) console.log('All good!')
  129. }
  130. function parseOptions(args: string[]): Map<string, string> {
  131. const parsed = new Map<string, string>()
  132. for (let index = 0; index < args.length; index += 2) {
  133. const name = args[index]
  134. const value = args[index + 1]
  135. if (name !== '--packages-root' || value === undefined || value.startsWith('--')) {
  136. throw new Error(`publint-all: expected [--packages-root PATH], got ${JSON.stringify(args)}.`)
  137. }
  138. if (parsed.has(name)) throw new Error(`publint-all: duplicate option ${name}.`)
  139. parsed.set(name, value)
  140. }
  141. return parsed
  142. }
  143. const packages = workspacePackages()
  144. const concurrency = publintConcurrency(packages.length)
  145. console.log(`publint-all: linting ${packages.length} package(s) with ${concurrency} worker(s).`)
  146. const results = await runAll(packages, concurrency)
  147. for (const result of results) printResult(result)
  148. if (results.some(result => result.status === 'failed')) process.exit(1)