verify-runtime-closure.ts 3.7 KB

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