verify-no-workspace-protocol.mjs 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. /**
  2. * Standalone verification for publish-time package manifests.
  3. *
  4. * Usage:
  5. * node scripts/verify-no-workspace-protocol.mjs packages/cli packages/core
  6. * node ../../scripts/verify-no-workspace-protocol.mjs .
  7. *
  8. * Run this only after publish versions have been rewritten.
  9. *
  10. * Checks two invariants before publish:
  11. * 1. publishable dependency fields must not contain workspace: specifiers
  12. * 2. internal workspace dependencies must match the current workspace package version exactly
  13. */
  14. import { access, readdir, readFile } from "node:fs/promises";
  15. import { join, resolve } from "node:path";
  16. const dirs = process.argv.slice(2);
  17. if (dirs.length === 0) {
  18. process.stderr.write("Usage: node verify-no-workspace-protocol.mjs <pkg-dir> [<pkg-dir>...]\n");
  19. process.exit(1);
  20. }
  21. async function exists(path) {
  22. try {
  23. await access(path);
  24. return true;
  25. } catch {
  26. return false;
  27. }
  28. }
  29. async function findWorkspaceRoot(startDir) {
  30. let dir = resolve(startDir);
  31. for (let i = 0; i < 10; i++) {
  32. if (await exists(join(dir, "packages"))) {
  33. return dir;
  34. }
  35. const parent = resolve(dir, "..");
  36. if (parent === dir) break;
  37. dir = parent;
  38. }
  39. throw new Error(`Could not find workspace root from ${startDir}`);
  40. }
  41. async function loadWorkspaceVersions(workspaceRoot) {
  42. const packagesDir = join(workspaceRoot, "packages");
  43. const entries = await readdir(packagesDir);
  44. const versions = new Map();
  45. for (const entry of entries) {
  46. try {
  47. const raw = await readFile(join(packagesDir, entry, "package.json"), "utf-8");
  48. const pkg = JSON.parse(raw);
  49. versions.set(pkg.name, pkg.version);
  50. } catch {
  51. // ignore directories that are not publishable packages
  52. }
  53. }
  54. return versions;
  55. }
  56. let failed = false;
  57. const workspaceRoot = await findWorkspaceRoot(process.cwd());
  58. const workspaceVersions = await loadWorkspaceVersions(workspaceRoot);
  59. for (const dirArg of dirs) {
  60. const dir = resolve(process.cwd(), dirArg);
  61. const packageJsonPath = join(dir, "package.json");
  62. const raw = await readFile(packageJsonPath, "utf-8");
  63. const pkg = JSON.parse(raw);
  64. let dirFailed = false;
  65. for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
  66. const deps = pkg[field];
  67. if (!deps) continue;
  68. for (const [name, specifier] of Object.entries(deps)) {
  69. if (typeof specifier === "string" && specifier.startsWith("workspace:")) {
  70. process.stderr.write(`FAIL: ${dir} — ${field}.${name}: ${specifier}\n`);
  71. dirFailed = true;
  72. failed = true;
  73. continue;
  74. }
  75. const workspaceVersion = workspaceVersions.get(name);
  76. if (workspaceVersion && specifier !== workspaceVersion) {
  77. process.stderr.write(
  78. `FAIL: ${dir} — ${field}.${name}: expected ${workspaceVersion}, got ${specifier}\n`,
  79. );
  80. dirFailed = true;
  81. failed = true;
  82. }
  83. }
  84. }
  85. if (!dirFailed) {
  86. process.stderr.write(`OK: ${dir}\n`);
  87. }
  88. }
  89. if (failed) {
  90. process.exit(1);
  91. }