check-workspace-constraints.ts 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. /**
  2. * Workspace package invariant checks for package-manager-independent quality
  3. * gates.
  4. *
  5. * Run: `tsx scripts/check-workspace-constraints.ts`.
  6. */
  7. import { readdirSync, readFileSync } from 'node:fs'
  8. import { join, relative, resolve } from 'node:path'
  9. const root = resolve(import.meta.dirname, '..')
  10. const workspaceGlobs = ['vendor', 'packages'] as const
  11. const vendoredPackages = new Set([
  12. 'cordis',
  13. 'cosmokit',
  14. 'schemastery',
  15. '@cordisjs/plugin-loader',
  16. '@cordisjs/plugin-include',
  17. '@cordisjs/plugin-group',
  18. '@cordisjs/plugin-timer',
  19. '@cordisjs/plugin-hmr',
  20. '@cordisjs/plugin-logger-console',
  21. ])
  22. /** The subset of package.json fields this constraint check cares about. */
  23. interface PackageManifest {
  24. name?: string
  25. version?: string
  26. private?: boolean
  27. type?: string
  28. peerDependencies?: Record<string, string>
  29. devDependencies?: Record<string, string>
  30. }
  31. /** One workspace manifest and its repo-relative path. */
  32. interface WorkspaceManifest {
  33. dir: string
  34. manifest: PackageManifest
  35. }
  36. function readJson(path: string): PackageManifest {
  37. return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
  38. }
  39. function workspaceManifests(): WorkspaceManifest[] {
  40. const manifests: WorkspaceManifest[] = [
  41. { dir: '.', manifest: readJson(join(root, 'package.json')) },
  42. ]
  43. for (const workspaceDir of workspaceGlobs) {
  44. for (const entry of readdirSync(join(root, workspaceDir), { withFileTypes: true })) {
  45. if (!entry.isDirectory()) continue
  46. const dir = join(workspaceDir, entry.name)
  47. manifests.push({ dir, manifest: readJson(join(root, dir, 'package.json')) })
  48. }
  49. }
  50. return manifests
  51. }
  52. function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
  53. const errors: string[] = []
  54. const label = manifest.name ?? dir
  55. if (manifest.private !== true) {
  56. errors.push(`${label}: package.json must set "private": true`)
  57. }
  58. if (manifest.name && vendoredPackages.has(manifest.name)) {
  59. return errors
  60. }
  61. if (manifest.name?.startsWith('@deepseek-ai/dsh-') && manifest.name !== '@deepseek-ai/dsh-root') {
  62. const peer = manifest.peerDependencies?.cordis
  63. const dev = manifest.devDependencies?.cordis
  64. if (!peer) errors.push(`${label}: cordis must be a peerDependency`)
  65. if (!dev) errors.push(`${label}: cordis must also be a devDependency`)
  66. if (peer && dev && peer !== dev) {
  67. errors.push(`${label}: cordis peer (${peer}) and dev (${dev}) ranges must match`)
  68. }
  69. if (manifest.version !== '0.0.1') {
  70. errors.push(`${label}: package.json must set "version": "0.0.1"`)
  71. }
  72. if (manifest.type !== 'module') {
  73. errors.push(`${label}: package.json must set "type": "module"`)
  74. }
  75. }
  76. return errors.map(error => `${relative(root, join(root, dir, 'package.json'))}: ${error}`)
  77. }
  78. const errors = workspaceManifests().flatMap(checkWorkspace)
  79. if (errors.length > 0) {
  80. console.error(errors.join('\n'))
  81. process.exitCode = 1
  82. }