verify-no-workspace-protocol.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. /**
  2. * Verify that source manifests are publishable once prepack normalization runs.
  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. * This script is safe to run on source manifests before prepack.
  9. *
  10. * Checks two invariants before publish:
  11. * 1. workspace:* / workspace:^ / workspace:~ references can be normalized to real versions
  12. * 2. non-workspace internal dependencies already point at the current workspace version
  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. function normalizeWorkspaceSpecifier(specifier, version) {
  57. const value = specifier.slice("workspace:".length);
  58. if (value === "*" || value === "") return version;
  59. if (value === "^") return `^${version}`;
  60. if (value === "~") return `~${version}`;
  61. return value;
  62. }
  63. let failed = false;
  64. const workspaceRoot = await findWorkspaceRoot(process.cwd());
  65. const workspaceVersions = await loadWorkspaceVersions(workspaceRoot);
  66. for (const dirArg of dirs) {
  67. const dir = resolve(process.cwd(), dirArg);
  68. const packageJsonPath = join(dir, "package.json");
  69. const raw = await readFile(packageJsonPath, "utf-8");
  70. const pkg = JSON.parse(raw);
  71. let dirFailed = false;
  72. for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
  73. const deps = pkg[field];
  74. if (!deps) continue;
  75. for (const [name, specifier] of Object.entries(deps)) {
  76. const workspaceVersion = workspaceVersions.get(name);
  77. if (typeof specifier !== "string") {
  78. continue;
  79. }
  80. if (specifier.startsWith("workspace:")) {
  81. if (!workspaceVersion) {
  82. process.stderr.write(`FAIL: ${dir} — ${field}.${name}: ${specifier} (workspace package not found)\n`);
  83. dirFailed = true;
  84. failed = true;
  85. continue;
  86. }
  87. const normalized = normalizeWorkspaceSpecifier(specifier, workspaceVersion);
  88. if (
  89. normalized !== workspaceVersion
  90. && normalized !== `^${workspaceVersion}`
  91. && normalized !== `~${workspaceVersion}`
  92. ) {
  93. process.stderr.write(
  94. `FAIL: ${dir} — ${field}.${name}: ${specifier} normalizes to ${normalized}, expected ${workspaceVersion}, ^${workspaceVersion}, or ~${workspaceVersion}\n`,
  95. );
  96. dirFailed = true;
  97. failed = true;
  98. }
  99. continue;
  100. }
  101. if (
  102. workspaceVersion
  103. && specifier !== workspaceVersion
  104. && specifier !== `^${workspaceVersion}`
  105. && specifier !== `~${workspaceVersion}`
  106. ) {
  107. process.stderr.write(
  108. `FAIL: ${dir} — ${field}.${name}: expected ${workspaceVersion}, ^${workspaceVersion}, or ~${workspaceVersion}, got ${specifier}\n`,
  109. );
  110. dirFailed = true;
  111. failed = true;
  112. }
  113. }
  114. }
  115. if (!dirFailed) {
  116. process.stderr.write(`OK: ${dir}\n`);
  117. }
  118. }
  119. if (failed) {
  120. process.exit(1);
  121. }