verify-ecosystem-versions.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import { dirname, resolve } from 'node:path'
  2. import { fileURLToPath } from 'node:url'
  3. import { loadEcosystemReleaseContext } from './lib/ecosystem-packages.mjs'
  4. const scriptDir = dirname(fileURLToPath(import.meta.url))
  5. const sourceRoot = resolve(scriptDir, '..')
  6. const { rootPackage, wrapperManifest, entries } = await loadEcosystemReleaseContext(sourceRoot)
  7. const rootVersion = rootPackage.version
  8. const expectedWorkspaceRange = `workspace:^${rootVersion}`
  9. const corePackageName = wrapperManifest.corePackage.packageName
  10. const entryByName = new Map()
  11. const entryById = new Map()
  12. const entryByDir = new Map()
  13. const historicalPackageNames = new Set()
  14. const standardPackageByHistoricalName = new Map()
  15. const compatibilityPackageByName = new Map()
  16. const standardWrapperPackageNames = new Set(wrapperManifest.wrappers.map(wrapper => wrapper.packageName))
  17. const wrapperByPackageName = new Map(wrapperManifest.wrappers.map(wrapper => [wrapper.packageName, wrapper]))
  18. function assert(condition, message) {
  19. if (!condition) {
  20. throw new Error(message)
  21. }
  22. }
  23. function installDependencies(packageJson) {
  24. return {
  25. ...packageJson.dependencies,
  26. ...packageJson.optionalDependencies
  27. }
  28. }
  29. function allDependencyFields(packageJson) {
  30. return {
  31. dependencies: packageJson.dependencies || {},
  32. optionalDependencies: packageJson.optionalDependencies || {},
  33. peerDependencies: packageJson.peerDependencies || {},
  34. devDependencies: packageJson.devDependencies || {}
  35. }
  36. }
  37. function assertUnique(map, key, label, packageName) {
  38. assert(!map.has(key), `Duplicate ${label} ${key} in ecosystem release packages`)
  39. map.set(key, packageName)
  40. }
  41. function normalizeRepositoryUrl(value) {
  42. return String(value || '')
  43. .replace(/^git\+/, '')
  44. .replace(/\.git$/, '')
  45. }
  46. function assertPackageFiles(entry, requiredFiles) {
  47. const files = new Set(entry.packageJson.files || [])
  48. for (const requiredFile of requiredFiles) {
  49. assert(
  50. files.has(requiredFile),
  51. `${entry.packageName} package.json files must include ${requiredFile}`
  52. )
  53. }
  54. }
  55. function assertRepositoryMetadata(entry, expectedUrl, expectedDirectory) {
  56. assert(entry.packageJson.repository?.type === 'git', `${entry.packageName} repository.type must be git`)
  57. assert(
  58. normalizeRepositoryUrl(entry.packageJson.repository?.url) === normalizeRepositoryUrl(expectedUrl),
  59. `${entry.packageName} repository.url must point to ${expectedUrl}`
  60. )
  61. assert(
  62. entry.packageJson.repository?.directory === expectedDirectory,
  63. `${entry.packageName} repository.directory must be ${expectedDirectory}`
  64. )
  65. }
  66. for (const wrapper of wrapperManifest.wrappers) {
  67. for (const packageName of wrapper.historicalPackages) {
  68. assert(!historicalPackageNames.has(packageName), `Duplicate historical package ${packageName}`)
  69. historicalPackageNames.add(packageName)
  70. standardPackageByHistoricalName.set(packageName, wrapper.packageName)
  71. }
  72. }
  73. for (const renderer of wrapperManifest.renderers || []) {
  74. const entry = entries.find(candidate => candidate.id === `renderer-${renderer.id}`)
  75. assert(entry, `Missing renderer release entry for ${renderer.id}`)
  76. assert(entry.kind === 'renderer', `${renderer.packageName} must be a renderer release entry`)
  77. assert(entry.packageName === renderer.packageName, `${renderer.id} package name drifted from wrappers.json`)
  78. assert(entry.packageDir === renderer.packageDir, `${renderer.id} packageDir drifted from wrappers.json`)
  79. assertRepositoryMetadata(entry, renderer.github, renderer.packageDir)
  80. assert(
  81. entry.packageJson.bugs?.url === `${renderer.github}/issues`,
  82. `${entry.packageName} bugs.url must be ${renderer.github}/issues`
  83. )
  84. }
  85. for (const preset of wrapperManifest.presets || []) {
  86. const entry = entries.find(candidate => candidate.id === `preset-${preset.id}`)
  87. assert(entry, `Missing preset release entry for ${preset.id}`)
  88. assert(entry.kind === 'preset', `${preset.packageName} must be a preset release entry`)
  89. assert(entry.packageName === preset.packageName, `${preset.id} package name drifted from wrappers.json`)
  90. assert(entry.packageDir === preset.packageDir, `${preset.id} packageDir drifted from wrappers.json`)
  91. assertRepositoryMetadata(entry, preset.github, preset.packageDir)
  92. assert(
  93. entry.packageJson.bugs?.url === `${preset.github}/issues`,
  94. `${entry.packageName} bugs.url must be ${preset.github}/issues`
  95. )
  96. }
  97. for (const compatibilityPackage of wrapperManifest.compatibilityPackages || []) {
  98. assert(
  99. compatibilityPackage.id,
  100. `Compatibility package ${compatibilityPackage.packageName || '(unnamed)'} must declare id`
  101. )
  102. assert(
  103. compatibilityPackage.packageName,
  104. `Compatibility package ${compatibilityPackage.id} must declare packageName`
  105. )
  106. assert(
  107. compatibilityPackage.targetPackage,
  108. `Compatibility package ${compatibilityPackage.packageName} must declare targetPackage`
  109. )
  110. assert(
  111. compatibilityPackage.packageDir,
  112. `Compatibility package ${compatibilityPackage.packageName} must declare packageDir`
  113. )
  114. assert(
  115. historicalPackageNames.has(compatibilityPackage.packageName),
  116. `${compatibilityPackage.packageName} compatibility package is not declared in wrappers.json historicalPackages`
  117. )
  118. assert(
  119. standardPackageByHistoricalName.get(compatibilityPackage.packageName) === compatibilityPackage.targetPackage,
  120. `${compatibilityPackage.packageName} targetPackage must match its wrapper historical package owner`
  121. )
  122. assert(
  123. !compatibilityPackageByName.has(compatibilityPackage.packageName),
  124. `Duplicate compatibility package manifest entry ${compatibilityPackage.packageName}`
  125. )
  126. compatibilityPackageByName.set(compatibilityPackage.packageName, compatibilityPackage)
  127. }
  128. for (const historicalPackageName of historicalPackageNames) {
  129. assert(
  130. compatibilityPackageByName.has(historicalPackageName),
  131. `${historicalPackageName} is declared as historical package but has no compatibilityPackages entry`
  132. )
  133. }
  134. for (const entry of entries) {
  135. assertUnique(entryByName, entry.packageName, 'package name', entry.packageName)
  136. assertUnique(entryById, entry.id, 'package id', entry.packageName)
  137. assertUnique(entryByDir, entry.packageDir, 'package directory', entry.packageName)
  138. assert(
  139. entry.version === rootVersion,
  140. `${entry.packageName} version ${entry.version} must match root version ${rootVersion}`
  141. )
  142. assert(entry.packageJson.private !== true, `${entry.packageName} must not be private`)
  143. assert(
  144. entry.packageJson.publishConfig?.access === 'public',
  145. `${entry.packageName} publishConfig.access must be public`
  146. )
  147. assertPackageFiles(entry, ['README.md', 'README.en.md'])
  148. }
  149. const releasePackageNames = new Set(entryByName.keys())
  150. const coreEntry = entries.find(entry => entry.packageName === corePackageName)
  151. assert(coreEntry, `Missing core package ${corePackageName} in ecosystem release list`)
  152. assert(coreEntry.kind === 'core', `${corePackageName} must be marked as the core release package`)
  153. assert(coreEntry.version === rootVersion, `${corePackageName} version must match ${rootVersion}`)
  154. assertRepositoryMetadata(coreEntry, wrapperManifest.corePackage.sourceRepository, coreEntry.packageDir)
  155. for (const wrapper of wrapperManifest.wrappers) {
  156. const entry = entries.find(candidate => candidate.id === wrapper.id)
  157. assert(entry, `Missing standard wrapper release entry for ${wrapper.id}`)
  158. assert(entry.kind === 'standard-wrapper', `${wrapper.packageName} must be a standard-wrapper release entry`)
  159. assert(entry.packageName === wrapper.packageName, `${wrapper.id} package name drifted from wrappers.json`)
  160. assert(entry.packageDir === wrapper.packageDir, `${wrapper.id} packageDir drifted from wrappers.json`)
  161. assertRepositoryMetadata(entry, wrapper.github, wrapper.packageDir)
  162. assert(
  163. entry.packageJson.bugs?.url === `${wrapper.github}/issues`,
  164. `${entry.packageName} bugs.url must be ${wrapper.github}/issues`
  165. )
  166. }
  167. for (const entry of entries) {
  168. for (const [field, dependencies] of Object.entries(allDependencyFields(entry.packageJson))) {
  169. for (const [packageName, range] of Object.entries(dependencies)) {
  170. if (releasePackageNames.has(packageName)) {
  171. assert(
  172. range === expectedWorkspaceRange,
  173. `${entry.packageName} ${field}.${packageName} must be ${expectedWorkspaceRange}, got ${range}`
  174. )
  175. }
  176. }
  177. }
  178. if (entry.kind === 'standard-wrapper') {
  179. const wrapper = wrapperByPackageName.get(entry.packageName)
  180. const allowedWrapperDependencies = new Set(
  181. wrapper?.flavor === 'full' && wrapper.basePackage ? [wrapper.basePackage] : []
  182. )
  183. const dependencies = installDependencies(entry.packageJson)
  184. for (const packageName of Object.keys(dependencies)) {
  185. assert(
  186. !historicalPackageNames.has(packageName),
  187. `${entry.packageName} must not depend on historical compatibility package ${packageName}`
  188. )
  189. assert(
  190. !standardWrapperPackageNames.has(packageName)
  191. || packageName === entry.packageName
  192. || allowedWrapperDependencies.has(packageName),
  193. `${entry.packageName} must not depend on another standard wrapper package ${packageName}`
  194. )
  195. }
  196. if (wrapper?.flavor === 'full') {
  197. assert(
  198. dependencies['@file-viewer/preset-all'] === expectedWorkspaceRange,
  199. `${entry.packageName} full package must depend on @file-viewer/preset-all@${expectedWorkspaceRange}`
  200. )
  201. assert(
  202. wrapper.basePackage && dependencies[wrapper.basePackage] === expectedWorkspaceRange,
  203. `${entry.packageName} full package must depend on ${wrapper.basePackage}@${expectedWorkspaceRange}`
  204. )
  205. } else {
  206. assert(
  207. dependencies[corePackageName] === expectedWorkspaceRange,
  208. `${entry.packageName} must depend on ${corePackageName}@${expectedWorkspaceRange}`
  209. )
  210. }
  211. }
  212. if (entry.kind === 'compatibility') {
  213. const compatibilityPackage = compatibilityPackageByName.get(entry.packageName)
  214. assert(
  215. compatibilityPackage,
  216. `${entry.packageName} compatibility package is not declared in ecosystem/wrappers.json compatibilityPackages`
  217. )
  218. assert(
  219. entry.id === compatibilityPackage.id,
  220. `${entry.packageName} release entry id ${entry.id} must match compatibilityPackages id ${compatibilityPackage.id}`
  221. )
  222. assert(
  223. entry.packageDir === compatibilityPackage.packageDir,
  224. `${entry.packageName} packageDir must match compatibilityPackages.packageDir`
  225. )
  226. const standardPackageName = standardPackageByHistoricalName.get(entry.packageName)
  227. assert(
  228. standardPackageName === compatibilityPackage.targetPackage,
  229. `${entry.packageName} target package must be ${standardPackageName}`
  230. )
  231. const dependencies = installDependencies(entry.packageJson)
  232. if (standardPackageName === '@file-viewer/react') {
  233. assert(
  234. !dependencies[corePackageName] && !dependencies['@file-viewer/web'],
  235. `${entry.packageName} must remain a thin React alias instead of depending on core/web directly`
  236. )
  237. assert(
  238. dependencies['@file-viewer/react'] === expectedWorkspaceRange,
  239. `${entry.packageName} must depend on @file-viewer/react@${expectedWorkspaceRange}`
  240. )
  241. }
  242. if (standardPackageName === '@file-viewer/web') {
  243. assert(
  244. dependencies['@file-viewer/web'] === expectedWorkspaceRange,
  245. `${entry.packageName} must depend on @file-viewer/web@${expectedWorkspaceRange}`
  246. )
  247. }
  248. if (standardPackageName === '@file-viewer/vue2.7') {
  249. assert(
  250. dependencies['@file-viewer/vue2.7'] === expectedWorkspaceRange,
  251. `${entry.packageName} must depend on @file-viewer/vue2.7@${expectedWorkspaceRange}`
  252. )
  253. }
  254. if (standardPackageName === '@file-viewer/vue3') {
  255. assert(
  256. dependencies['@file-viewer/vue3'] === expectedWorkspaceRange,
  257. `${entry.packageName} must depend on @file-viewer/vue3@${expectedWorkspaceRange}`
  258. )
  259. }
  260. if (entry.packageName === 'file-viewer3') {
  261. assert(
  262. !dependencies['@flyfish-group/file-viewer3'],
  263. `${entry.packageName} must alias @file-viewer/vue3 directly instead of chaining through @flyfish-group/file-viewer3`
  264. )
  265. }
  266. }
  267. }
  268. console.log(
  269. `Verified ${entries.length} ecosystem package versions and workspace dependency ranges at ${rootVersion}.`
  270. )