verify-core-dependency-budget.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { readFileSync } from 'node:fs'
  2. import { resolve } from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. import {
  5. dependencyToRendererLine,
  6. dependencyToRendererLines,
  7. modularizedRendererLines
  8. } from './renderer-dependency-plan.mjs'
  9. const root = resolve(fileURLToPath(new URL('..', import.meta.url)))
  10. const readJson = path => JSON.parse(readFileSync(resolve(root, path), 'utf8'))
  11. const args = process.argv.slice(2)
  12. const strict = args.includes('--strict')
  13. const baseline = {
  14. maxDirectDependencies: 1,
  15. maxRendererDependencies: 0,
  16. maxPhaseDependencies: {
  17. 2: 0,
  18. 3: 0,
  19. 4: 0
  20. }
  21. }
  22. const numberArg = (name, fallback) => {
  23. const prefix = `${name}=`
  24. const value = args.find(arg => arg.startsWith(prefix))?.slice(prefix.length)
  25. if (value === undefined) {
  26. return fallback
  27. }
  28. const parsed = Number(value)
  29. if (!Number.isInteger(parsed) || parsed < 0) {
  30. throw new Error(`${name} must be a non-negative integer, got ${value}`)
  31. }
  32. return parsed
  33. }
  34. const corePackage = readJson('packages/core/package.json')
  35. const dependencies = Object.keys(corePackage.dependencies || {}).sort()
  36. const isRetainedDependency = name => dependencyToRendererLine.get(name)?.status === 'retained'
  37. const rendererDependencies = dependencies.filter(name => dependencyToRendererLine.has(name) && !isRetainedDependency(name))
  38. const unclassifiedDependencies = dependencies.filter(name => !dependencyToRendererLine.has(name))
  39. const phaseDependencySets = modularizedRendererLines.reduce((result, line) => {
  40. result[line.phase] ||= new Set()
  41. line.dependencies
  42. .filter(name => dependencies.includes(name))
  43. .forEach(name => result[line.phase].add(name))
  44. return result
  45. }, {})
  46. const phaseDependencyCount = Object.fromEntries(
  47. Object.entries(phaseDependencySets).map(([phase, names]) => [phase, names.size])
  48. )
  49. const retainedDependencyCount = dependencies.filter(isRetainedDependency).length
  50. const maxDirectDependencies = strict
  51. ? retainedDependencyCount
  52. : numberArg('--max-direct', baseline.maxDirectDependencies)
  53. const maxRendererDependencies = strict ? 0 : numberArg('--max-renderer', baseline.maxRendererDependencies)
  54. const maxPhaseDependencies = Object.fromEntries(
  55. Object.entries(baseline.maxPhaseDependencies).map(([phase, value]) => [
  56. phase,
  57. strict ? 0 : numberArg(`--max-phase-${phase}`, value)
  58. ])
  59. )
  60. const errors = []
  61. if (dependencies.length > maxDirectDependencies) {
  62. errors.push(
  63. `@file-viewer/core has ${dependencies.length} direct runtime dependencies, budget is ${maxDirectDependencies}`
  64. )
  65. }
  66. if (rendererDependencies.length > maxRendererDependencies) {
  67. errors.push(
  68. `@file-viewer/core has ${rendererDependencies.length} renderer runtime dependencies, budget is ${maxRendererDependencies}`
  69. )
  70. }
  71. if (unclassifiedDependencies.length) {
  72. errors.push(
  73. `@file-viewer/core has unclassified runtime dependencies: ${unclassifiedDependencies.join(', ')}`
  74. )
  75. }
  76. for (const [phase, max] of Object.entries(maxPhaseDependencies)) {
  77. const actual = phaseDependencyCount[phase] || 0
  78. if (actual > max) {
  79. errors.push(`phase ${phase} still has ${actual} core runtime dependencies, budget is ${max}`)
  80. }
  81. }
  82. if (errors.length) {
  83. console.error('[core-dependency-budget] Failed')
  84. errors.forEach(error => console.error(` - ${error}`))
  85. console.error('\nCurrent renderer dependency ownership:')
  86. rendererDependencies.forEach(name => {
  87. const lines = dependencyToRendererLines.get(name) || []
  88. console.error(` - ${name} -> ${lines.map(line => line.targetPackage).join(', ')}`)
  89. })
  90. process.exitCode = 1
  91. } else {
  92. console.log(
  93. `[core-dependency-budget] Passed: ${dependencies.length} direct dependencies, ${rendererDependencies.length} renderer dependencies, phase budgets ${Object.entries(maxPhaseDependencies).map(([phase, max]) => `P${phase}<=${
  94. max
  95. }`).join(', ')}${strict ? ' (strict)' : ''}.`
  96. )
  97. }