verify-on-demand-boundaries.mjs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import { dirname, resolve } from 'node:path'
  2. import { fileURLToPath } from 'node:url'
  3. import { loadEcosystemReleaseContext } from './lib/ecosystem-packages.mjs'
  4. import { rendererModularizationLines } from './renderer-dependency-plan.mjs'
  5. const scriptDir = dirname(fileURLToPath(import.meta.url))
  6. const sourceRoot = resolve(scriptDir, '..')
  7. const { entries } = await loadEcosystemReleaseContext(sourceRoot)
  8. const entriesByPackageName = new Map(entries.map(entry => [entry.packageName, entry]))
  9. const rendererEntries = entries.filter(entry => entry.kind === 'renderer')
  10. const wrapperEntries = entries.filter(entry => entry.kind === 'standard-wrapper')
  11. const compatibilityEntries = entries.filter(entry => entry.kind === 'compatibility')
  12. const presetEntries = entries.filter(entry => entry.kind === 'preset')
  13. const coreEntry = entries.find(entry => entry.kind === 'core')
  14. const rendererPackageNames = new Set(rendererEntries.map(entry => entry.packageName))
  15. const wrapperPackageNames = new Set(wrapperEntries.map(entry => entry.packageName))
  16. const compatibilityPackageNames = new Set(compatibilityEntries.map(entry => entry.packageName))
  17. const presetPackageNames = new Set(presetEntries.map(entry => entry.packageName))
  18. const highLevelPackageNames = new Set([
  19. ...rendererPackageNames,
  20. ...wrapperPackageNames,
  21. ...compatibilityPackageNames,
  22. ...presetPackageNames
  23. ])
  24. const pluginEnginePackageNames = new Set([
  25. '@file-viewer/pptx',
  26. '@file-viewer/eda-layout',
  27. '@file-viewer/eda-orcad',
  28. '@file-viewer/geometry-engine'
  29. ])
  30. const pluginRendererPackageNames = new Set(
  31. rendererEntries
  32. .map(entry => entry.packageName)
  33. .filter(packageName => !pluginEnginePackageNames.has(packageName))
  34. )
  35. const rendererPackageNamesByRendererId = new Map(
  36. rendererEntries.map(entry => [entry.renderer?.id, entry.packageName])
  37. )
  38. const presetRendererAllowList = {
  39. lite: ['text', 'image', 'media'],
  40. office: ['pdf', 'word', 'spreadsheet', 'presentation', 'ofd'],
  41. engineering: ['cad', 'model', 'drawing', 'mindmap', 'geo', 'typst', 'archive', 'data', 'eda']
  42. }
  43. function assert(condition, message) {
  44. if (!condition) {
  45. throw new Error(message)
  46. }
  47. }
  48. function dependencyNames(packageJson) {
  49. return {
  50. dependencies: Object.keys(packageJson.dependencies || {}),
  51. optionalDependencies: Object.keys(packageJson.optionalDependencies || {}),
  52. peerDependencies: Object.keys(packageJson.peerDependencies || {})
  53. }
  54. }
  55. function assertNoPackages(entry, packageNames, message) {
  56. for (const [field, names] of Object.entries(dependencyNames(entry.packageJson))) {
  57. const blocked = names.filter(name => packageNames.has(name))
  58. assert(
  59. blocked.length === 0,
  60. `${entry.packageName} ${field} must not include ${message}: ${blocked.join(', ')}`
  61. )
  62. }
  63. }
  64. function assertOnlyAllowedWorkspaceDeps(entry, allowedPackageNames, reason) {
  65. for (const [field, names] of Object.entries(dependencyNames(entry.packageJson))) {
  66. const blocked = names.filter(name => highLevelPackageNames.has(name) && !allowedPackageNames.has(name))
  67. assert(
  68. blocked.length === 0,
  69. `${entry.packageName} ${field} has invalid File Viewer dependency for ${reason}: ${blocked.join(', ')}`
  70. )
  71. }
  72. }
  73. assert(coreEntry, 'Missing @file-viewer/core ecosystem entry')
  74. assertNoPackages(
  75. coreEntry,
  76. highLevelPackageNames,
  77. 'renderer, preset, wrapper, or compatibility packages'
  78. )
  79. for (const wrapperEntry of wrapperEntries) {
  80. assertOnlyAllowedWorkspaceDeps(
  81. wrapperEntry,
  82. new Set(['@file-viewer/core']),
  83. 'standard component packages'
  84. )
  85. }
  86. for (const compatibilityEntry of compatibilityEntries) {
  87. const targetPackage = compatibilityEntry.compatibilityPackage?.targetPackage
  88. assert(targetPackage, `${compatibilityEntry.packageName} must declare a compatibility targetPackage`)
  89. assert(
  90. entriesByPackageName.has(targetPackage),
  91. `${compatibilityEntry.packageName} target package ${targetPackage} is not part of the ecosystem manifest`
  92. )
  93. assertOnlyAllowedWorkspaceDeps(
  94. compatibilityEntry,
  95. new Set([targetPackage]),
  96. 'compatibility alias packages'
  97. )
  98. assert(
  99. compatibilityEntry.packageJson.dependencies?.[targetPackage],
  100. `${compatibilityEntry.packageName} must depend on its target package ${targetPackage}`
  101. )
  102. }
  103. for (const presetEntry of presetEntries) {
  104. const dependencies = new Set(Object.keys(presetEntry.packageJson.dependencies || {}))
  105. if (presetEntry.packageName === '@file-viewer/preset-all') {
  106. assert(
  107. dependencies.has('@file-viewer/core'),
  108. '@file-viewer/preset-all must depend on @file-viewer/core'
  109. )
  110. for (const rendererPackageName of pluginRendererPackageNames) {
  111. assert(
  112. dependencies.has(rendererPackageName),
  113. `@file-viewer/preset-all must aggregate ${rendererPackageName}`
  114. )
  115. }
  116. assertNoPackages(
  117. presetEntry,
  118. new Set([...wrapperPackageNames, ...compatibilityPackageNames]),
  119. 'wrapper or compatibility packages'
  120. )
  121. } else if (presetRendererAllowList[presetEntry.preset.id]) {
  122. const allowedRendererPackages = new Set(
  123. presetRendererAllowList[presetEntry.preset.id].map(rendererId => {
  124. const packageName = rendererPackageNamesByRendererId.get(rendererId)
  125. assert(
  126. packageName,
  127. `${presetEntry.packageName} allow-list renderer ${rendererId} is not in ecosystem/wrappers.json`
  128. )
  129. return packageName
  130. })
  131. )
  132. assert(
  133. dependencies.has('@file-viewer/core'),
  134. `${presetEntry.packageName} must depend on @file-viewer/core`
  135. )
  136. for (const rendererPackageName of allowedRendererPackages) {
  137. assert(
  138. dependencies.has(rendererPackageName),
  139. `${presetEntry.packageName} must aggregate ${rendererPackageName}`
  140. )
  141. }
  142. assertOnlyAllowedWorkspaceDeps(
  143. presetEntry,
  144. new Set(['@file-viewer/core', ...allowedRendererPackages]),
  145. `${presetEntry.preset.id} preset packages`
  146. )
  147. assertNoPackages(
  148. presetEntry,
  149. new Set([...wrapperPackageNames, ...compatibilityPackageNames]),
  150. 'wrapper or compatibility packages'
  151. )
  152. } else {
  153. assertOnlyAllowedWorkspaceDeps(
  154. presetEntry,
  155. new Set(['@file-viewer/core']),
  156. 'automation/plugin packages'
  157. )
  158. }
  159. }
  160. for (const rendererEntry of rendererEntries) {
  161. const allowedPackages = new Set(['@file-viewer/core'])
  162. for (const line of rendererModularizationLines) {
  163. if (line.targetPackage !== rendererEntry.packageName) {
  164. continue
  165. }
  166. for (const dependency of line.dependencies) {
  167. if (pluginEnginePackageNames.has(dependency)) {
  168. allowedPackages.add(dependency)
  169. }
  170. }
  171. }
  172. assertOnlyAllowedWorkspaceDeps(rendererEntry, allowedPackages, 'renderer packages')
  173. }
  174. const coreDependencies = new Set(Object.keys(coreEntry.packageJson.dependencies || {}))
  175. const linesByDependency = new Map()
  176. for (const line of rendererModularizationLines) {
  177. for (const dependency of line.dependencies) {
  178. const lines = linesByDependency.get(dependency) || []
  179. lines.push(line)
  180. linesByDependency.set(dependency, lines)
  181. }
  182. }
  183. for (const line of rendererModularizationLines) {
  184. if (line.status === 'retained') {
  185. for (const dependency of line.dependencies) {
  186. assert(
  187. coreDependencies.has(dependency),
  188. `${dependency} is marked retained in core but is not declared by @file-viewer/core`
  189. )
  190. }
  191. continue
  192. }
  193. if (line.status !== 'planned') {
  194. assert(
  195. entriesByPackageName.has(line.targetPackage),
  196. `${line.id} target package ${line.targetPackage} is missing from ecosystem/wrappers.json`
  197. )
  198. }
  199. if (line.status === 'extracted') {
  200. const leakedDependencies = line.dependencies.filter(dependency => {
  201. if (!coreDependencies.has(dependency)) {
  202. return false
  203. }
  204. const owners = linesByDependency.get(dependency) || []
  205. return owners.every(owner => owner.status === 'extracted')
  206. })
  207. assert(
  208. leakedDependencies.length === 0,
  209. `${line.id} is extracted but @file-viewer/core still depends on ${leakedDependencies.join(', ')}`
  210. )
  211. }
  212. }
  213. const linesByTargetPackage = new Map()
  214. for (const line of rendererModularizationLines) {
  215. if (line.targetPackage === '@file-viewer/core') {
  216. continue
  217. }
  218. const lines = linesByTargetPackage.get(line.targetPackage) || []
  219. lines.push(line)
  220. linesByTargetPackage.set(line.targetPackage, lines)
  221. }
  222. for (const rendererEntry of rendererEntries) {
  223. if (pluginEnginePackageNames.has(rendererEntry.packageName)) {
  224. continue
  225. }
  226. assert(
  227. linesByTargetPackage.has(rendererEntry.packageName),
  228. `${rendererEntry.packageName} is a renderer package but has no renderer-dependency-plan entry`
  229. )
  230. }
  231. console.log(
  232. `[on-demand-boundaries] Verified ${wrapperEntries.length} wrappers, ${compatibilityEntries.length} compatibility aliases, ${presetEntries.length} presets, ${rendererEntries.length} renderer packages, and ${rendererModularizationLines.length} renderer dependency lines.`
  233. )