verify-runtime-closure.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. /**
  2. * Verify that the executable deploy manifest supplies every required workspace
  3. * peer in its dependency graph. With auto peer installation disabled, a missing
  4. * root peer can otherwise fail only when Cordis loads the packaged plugin.
  5. */
  6. import { readFile, readdir } from 'node:fs/promises'
  7. import { join, resolve } from 'node:path'
  8. import { parseArgs } from 'node:util'
  9. interface PackageManifest {
  10. name?: string
  11. dependencies?: Record<string, string>
  12. optionalDependencies?: Record<string, string>
  13. peerDependencies?: Record<string, string>
  14. peerDependenciesMeta?: Record<string, { optional?: boolean }>
  15. }
  16. interface WorkspacePackage {
  17. path: string
  18. manifest: PackageManifest
  19. }
  20. const root = resolve(import.meta.dirname, '..')
  21. const { values } = parseArgs({
  22. args: process.argv.slice(2),
  23. options: { manifest: { type: 'string' } },
  24. })
  25. const runtimeManifestPath = resolve(root, values.manifest ?? 'python/sdk-runtime/package.json')
  26. const runtimeManifest = await loadManifest(runtimeManifestPath)
  27. const runtimeName = runtimeManifest.name ?? 'python/sdk-runtime'
  28. const workspace = await loadWorkspacePackages()
  29. const runtimeDependencies = runtimeManifest.dependencies ?? {}
  30. const parents = new Map<string, string | undefined>()
  31. const queue: string[] = []
  32. for (const dependency of Object.keys(runtimeDependencies).sort()) {
  33. if (!workspace.has(dependency)) continue
  34. parents.set(dependency, undefined)
  35. queue.push(dependency)
  36. }
  37. const failures: string[] = []
  38. for (let index = 0; index < queue.length; index += 1) {
  39. const packageName = queue[index]
  40. if (packageName === undefined) continue
  41. const current = workspace.get(packageName)
  42. if (current === undefined) continue
  43. const peers = current.manifest.peerDependencies ?? {}
  44. const peerMeta = current.manifest.peerDependenciesMeta ?? {}
  45. for (const peer of Object.keys(peers).sort()) {
  46. if (!workspace.has(peer) || peerMeta[peer]?.optional === true) continue
  47. if (runtimeDependencies[peer]?.startsWith('workspace:') === true) continue
  48. failures.push(`${formatChain(runtimeName, packageName, parents)} -> ${peer}`)
  49. }
  50. const dependencies = {
  51. ...current.manifest.dependencies,
  52. ...current.manifest.optionalDependencies,
  53. }
  54. for (const dependency of Object.keys(dependencies).sort()) {
  55. if (!workspace.has(dependency) || parents.has(dependency)) continue
  56. parents.set(dependency, packageName)
  57. queue.push(dependency)
  58. }
  59. }
  60. if (failures.length > 0) {
  61. console.error('verify-runtime-closure: required workspace peers are missing from python/sdk-runtime dependencies:')
  62. for (const failure of failures) console.error(` ${failure}`)
  63. process.exit(1)
  64. }
  65. console.log(`verify-runtime-closure: ${queue.length} workspace packages form a closed runtime dependency graph.`)
  66. async function loadWorkspacePackages(): Promise<Map<string, WorkspacePackage>> {
  67. const paths: string[] = []
  68. for (const group of await childDirectories(join(root, 'packages'))) {
  69. for (const packageDir of await childDirectories(join(root, 'packages', group))) {
  70. paths.push(join(root, 'packages', group, packageDir, 'package.json'))
  71. }
  72. }
  73. for (const packageDir of await childDirectories(join(root, 'vendor'))) {
  74. paths.push(join(root, 'vendor', packageDir, 'package.json'))
  75. }
  76. const result = new Map<string, WorkspacePackage>()
  77. for (const path of paths) {
  78. const manifest = await loadManifest(path)
  79. if (manifest.name !== undefined) result.set(manifest.name, { path, manifest })
  80. }
  81. return result
  82. }
  83. async function childDirectories(path: string): Promise<string[]> {
  84. const entries = await readdir(path, { withFileTypes: true })
  85. return entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort()
  86. }
  87. async function loadManifest(path: string): Promise<PackageManifest> {
  88. return JSON.parse(await readFile(path, 'utf8')) as PackageManifest
  89. }
  90. function formatChain(
  91. runtimeName: string,
  92. packageName: string,
  93. parents: ReadonlyMap<string, string | undefined>,
  94. ): string {
  95. const chain = [packageName]
  96. let parent = parents.get(packageName)
  97. while (parent !== undefined) {
  98. chain.unshift(parent)
  99. parent = parents.get(parent)
  100. }
  101. return [runtimeName, ...chain].join(' -> ')
  102. }