prepare-package-for-publish.mjs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  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 } 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. // Walk up to workspace root (contains pnpm-workspace.yaml)
  15. function findWorkspaceRoot(startDir) {
  16. let dir = startDir;
  17. for (let i = 0; i < 10; i++) {
  18. try {
  19. // Sync check not available in ESM, so we just hardcode the relative path
  20. // since all packages are at packages/<name>/
  21. return resolve(dir, "..", "..");
  22. } catch {
  23. dir = resolve(dir, "..");
  24. }
  25. }
  26. throw new Error("Could not find workspace root");
  27. }
  28. function normalizeWorkspaceSpecifier(specifier, version) {
  29. const value = specifier.slice("workspace:".length);
  30. if (value === "*" || value === "") return version;
  31. if (value === "^") return `^${version}`;
  32. if (value === "~") return `~${version}`;
  33. return value;
  34. }
  35. async function loadWorkspaceVersions(workspaceRoot) {
  36. const packagesDir = join(workspaceRoot, "packages");
  37. const { readdir } = await import("node:fs/promises");
  38. const entries = await readdir(packagesDir);
  39. const versions = new Map();
  40. for (const entry of entries) {
  41. try {
  42. const raw = await readFile(join(packagesDir, entry, "package.json"), "utf-8");
  43. const pkg = JSON.parse(raw);
  44. versions.set(pkg.name, pkg.version);
  45. } catch {
  46. // not a package dir
  47. }
  48. }
  49. return versions;
  50. }
  51. async function main() {
  52. const raw = await readFile(packageJsonPath, "utf-8");
  53. const pkg = JSON.parse(raw);
  54. // Check if there are any workspace: specifiers at all
  55. let hasWorkspaceDeps = false;
  56. for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
  57. const deps = pkg[field];
  58. if (!deps) continue;
  59. for (const specifier of Object.values(deps)) {
  60. if (typeof specifier === "string" && specifier.startsWith("workspace:")) {
  61. hasWorkspaceDeps = true;
  62. break;
  63. }
  64. }
  65. if (hasWorkspaceDeps) break;
  66. }
  67. if (!hasWorkspaceDeps) {
  68. // Nothing to do — skip backup/rewrite to avoid unnecessary churn
  69. return;
  70. }
  71. const workspaceRoot = findWorkspaceRoot(packageDir);
  72. const versions = await loadWorkspaceVersions(workspaceRoot);
  73. // Backup original
  74. await copyFile(packageJsonPath, backupPath);
  75. for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
  76. const deps = pkg[field];
  77. if (!deps) continue;
  78. for (const [name, specifier] of Object.entries(deps)) {
  79. if (typeof specifier !== "string" || !specifier.startsWith("workspace:")) continue;
  80. const version = versions.get(name);
  81. if (!version) {
  82. throw new Error(`Unable to resolve workspace dependency version for "${name}"`);
  83. }
  84. deps[name] = normalizeWorkspaceSpecifier(specifier, version);
  85. }
  86. }
  87. await writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
  88. process.stderr.write(`[prepack] Replaced workspace:* deps in ${pkg.name}\n`);
  89. }
  90. await main();