prepare-package-for-publish.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. /**
  2. * prepack hook — replaces workspace:* with real version numbers in package.json.
  3. *
  4. * Shared by all publishable packages. Invoked as:
  5. * "prepack": "node ../../scripts/prepare-package-for-publish.mjs"
  6. *
  7. * Expects process.cwd() to be the package directory (npm/pnpm guarantee this).
  8. */
  9. import { readFile, writeFile, copyFile, rm, rename } from "node:fs/promises";
  10. import { join, resolve } from "node:path";
  11. const packageDir = process.cwd();
  12. const packageJsonPath = join(packageDir, "package.json");
  13. const backupPath = join(packageDir, ".package.json.publish-backup");
  14. async function writeAtomic(path, content) {
  15. const tempPath = `${path}.tmp-${process.pid}-${Date.now()}`;
  16. await writeFile(tempPath, content, "utf-8");
  17. await rename(tempPath, path);
  18. }
  19. // Walk up to workspace root (contains pnpm-workspace.yaml)
  20. function findWorkspaceRoot(startDir) {
  21. let dir = startDir;
  22. for (let i = 0; i < 10; i++) {
  23. try {
  24. // Sync check not available in ESM, so we just hardcode the relative path
  25. // since all packages are at packages/<name>/
  26. return resolve(dir, "..", "..");
  27. } catch {
  28. dir = resolve(dir, "..");
  29. }
  30. }
  31. throw new Error("Could not find workspace root");
  32. }
  33. function normalizeWorkspaceSpecifier(specifier, version) {
  34. const value = specifier.slice("workspace:".length);
  35. if (value === "*" || value === "") return version;
  36. if (value === "^") return `^${version}`;
  37. if (value === "~") return `~${version}`;
  38. return value;
  39. }
  40. async function loadWorkspaceVersions(workspaceRoot) {
  41. const packagesDir = join(workspaceRoot, "packages");
  42. const { readdir } = await import("node:fs/promises");
  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. // not a package dir
  52. }
  53. }
  54. return versions;
  55. }
  56. async function main() {
  57. const raw = await readFile(packageJsonPath, "utf-8");
  58. const pkg = JSON.parse(raw);
  59. // Check if there are any workspace: specifiers at all
  60. let hasWorkspaceDeps = false;
  61. for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
  62. const deps = pkg[field];
  63. if (!deps) continue;
  64. for (const specifier of Object.values(deps)) {
  65. if (typeof specifier === "string" && specifier.startsWith("workspace:")) {
  66. hasWorkspaceDeps = true;
  67. break;
  68. }
  69. }
  70. if (hasWorkspaceDeps) break;
  71. }
  72. if (!hasWorkspaceDeps) {
  73. // Nothing to do — skip backup/rewrite to avoid unnecessary churn
  74. return;
  75. }
  76. const workspaceRoot = findWorkspaceRoot(packageDir);
  77. const versions = await loadWorkspaceVersions(workspaceRoot);
  78. // Backup original
  79. await copyFile(packageJsonPath, backupPath);
  80. for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
  81. const deps = pkg[field];
  82. if (!deps) continue;
  83. for (const [name, specifier] of Object.entries(deps)) {
  84. if (typeof specifier !== "string" || !specifier.startsWith("workspace:")) continue;
  85. const version = versions.get(name);
  86. if (!version) {
  87. throw new Error(`Unable to resolve workspace dependency version for "${name}"`);
  88. }
  89. deps[name] = normalizeWorkspaceSpecifier(specifier, version);
  90. }
  91. }
  92. await writeAtomic(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
  93. process.stderr.write(`[prepack] Replaced workspace:* deps in ${pkg.name}\n`);
  94. // Verify: re-read and confirm no workspace: references remain
  95. const verifyRaw = await readFile(packageJsonPath, "utf-8");
  96. const verifyPkg = JSON.parse(verifyRaw);
  97. const violations = [];
  98. for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
  99. const deps = verifyPkg[field];
  100. if (!deps) continue;
  101. for (const [name, specifier] of Object.entries(deps)) {
  102. if (typeof specifier === "string" && specifier.startsWith("workspace:")) {
  103. violations.push(` ${field}.${name}: ${specifier}`);
  104. }
  105. }
  106. }
  107. if (violations.length > 0) {
  108. process.stderr.write(
  109. `[prepack] FATAL: workspace: references remain after replacement!\n` +
  110. `${violations.join("\n")}\n`,
  111. );
  112. // Restore backup before aborting
  113. const original = await readFile(backupPath, "utf-8");
  114. await writeAtomic(packageJsonPath, original);
  115. await rm(backupPath, { force: true });
  116. process.exit(1);
  117. }
  118. }
  119. await main();