verify-no-workspace-protocol.mjs 2.9 KB

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