publint-all.ts 5.4 KB

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