publish-package.test.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
  2. import { tmpdir } from "node:os";
  3. import { dirname, join, resolve } from "node:path";
  4. import { fileURLToPath } from "node:url";
  5. import { execFileSync } from "node:child_process";
  6. import { describe, expect, it } from "vitest";
  7. const testDir = dirname(fileURLToPath(import.meta.url));
  8. const cliDir = resolve(testDir, "..", "..");
  9. const workspaceRoot = resolve(cliDir, "..", "..");
  10. const studioDir = resolve(workspaceRoot, "packages", "studio");
  11. const sourceCliPackageJsonPromise = readFile(resolve(cliDir, "package.json"), "utf-8").then((raw) =>
  12. JSON.parse(raw),
  13. );
  14. const sourceStudioPackageJsonPromise = readFile(resolve(studioDir, "package.json"), "utf-8").then((raw) =>
  15. JSON.parse(raw),
  16. );
  17. async function packPackage(packageDir: string, packDir: string) {
  18. const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
  19. execFileSync(npmCmd, ["pack", "--pack-destination", packDir], {
  20. cwd: packageDir,
  21. env: process.env,
  22. encoding: "utf-8",
  23. shell: process.platform === "win32",
  24. });
  25. const tgzFiles = (await readdir(packDir)).filter((name) => name.endsWith(".tgz"));
  26. if (tgzFiles.length !== 1) {
  27. throw new Error(`Expected exactly one tarball in ${packDir}, found ${tgzFiles.length}`);
  28. }
  29. return join(packDir, tgzFiles[0]);
  30. }
  31. async function extractPackedPackageJson(packageDir: string, packDir: string) {
  32. const tarballPath = await packPackage(packageDir, packDir);
  33. const tarArgs = process.platform === "win32" ? ["--force-local", "-xOf"] : ["-xOf"];
  34. return execFileSync("tar", [...tarArgs, tarballPath, "package/package.json"], {
  35. cwd: workspaceRoot,
  36. encoding: "utf-8",
  37. });
  38. }
  39. describe.sequential("publish packaging", () => {
  40. it("rewrites workspace package versions for canary publishing", async () => {
  41. const tempRoot = await mkdtemp(join(tmpdir(), "inkos-version-script-"));
  42. const tempPackagesDir = join(tempRoot, "packages");
  43. const tempCoreDir = join(tempPackagesDir, "core");
  44. const tempCliDir = join(tempPackagesDir, "cli");
  45. try {
  46. await mkdir(tempCoreDir, { recursive: true });
  47. await mkdir(tempCliDir, { recursive: true });
  48. await writeFile(
  49. join(tempRoot, "package.json"),
  50. `${JSON.stringify({ name: "inkos", version: "0.4.6" }, null, 2)}\n`,
  51. );
  52. await writeFile(
  53. join(tempCoreDir, "package.json"),
  54. `${JSON.stringify({ name: "@actalk/inkos-core", version: "0.4.6" }, null, 2)}\n`,
  55. );
  56. await writeFile(
  57. join(tempCliDir, "package.json"),
  58. `${JSON.stringify(
  59. {
  60. name: "@actalk/inkos",
  61. version: "0.4.6",
  62. dependencies: {
  63. "@actalk/inkos-core": "workspace:*",
  64. commander: "^13.0.0",
  65. },
  66. },
  67. null,
  68. 2,
  69. )}\n`,
  70. );
  71. execFileSync(
  72. "node",
  73. [resolve(workspaceRoot, "scripts/set-package-versions.mjs"), "0.4.8-canary.7", "--root", tempRoot],
  74. {
  75. cwd: workspaceRoot,
  76. env: process.env,
  77. encoding: "utf-8",
  78. },
  79. );
  80. const rootPackageJson = JSON.parse(await readFile(join(tempRoot, "package.json"), "utf-8"));
  81. const corePackageJson = JSON.parse(await readFile(join(tempCoreDir, "package.json"), "utf-8"));
  82. const cliPackageJson = JSON.parse(await readFile(join(tempCliDir, "package.json"), "utf-8"));
  83. expect(rootPackageJson.version).toBe("0.4.8-canary.7");
  84. expect(corePackageJson.version).toBe("0.4.8-canary.7");
  85. expect(cliPackageJson.version).toBe("0.4.8-canary.7");
  86. expect(cliPackageJson.dependencies["@actalk/inkos-core"]).toBe("0.4.8-canary.7");
  87. } finally {
  88. await rm(tempRoot, { recursive: true, force: true });
  89. }
  90. });
  91. it("keeps source CLI dependencies linked through the workspace protocol", async () => {
  92. const cliPackageJson = await sourceCliPackageJsonPromise;
  93. expect(cliPackageJson.dependencies["@actalk/inkos-core"]).toBe("workspace:*");
  94. expect(cliPackageJson.dependencies["@actalk/inkos-studio"]).toBe("workspace:*");
  95. });
  96. it("verifies publishable manifests before npm publish runs", async () => {
  97. const cliPackageJson = await sourceCliPackageJsonPromise;
  98. const corePackageJson = JSON.parse(
  99. await readFile(resolve(workspaceRoot, "packages/core/package.json"), "utf-8"),
  100. );
  101. expect(cliPackageJson.scripts.prepublishOnly).toBe(
  102. "node ../../scripts/verify-no-workspace-protocol.mjs .",
  103. );
  104. expect(corePackageJson.scripts.prepublishOnly).toBe(
  105. "node ../../scripts/verify-no-workspace-protocol.mjs .",
  106. );
  107. });
  108. it("allows source workspace protocol manifests when they normalize cleanly for publish", async () => {
  109. const tempRoot = await mkdtemp(join(tmpdir(), "inkos-publish-verify-pass-"));
  110. const tempPackagesDir = join(tempRoot, "packages");
  111. const tempCoreDir = join(tempPackagesDir, "core");
  112. const tempCliDir = join(tempPackagesDir, "cli");
  113. try {
  114. await mkdir(tempCoreDir, { recursive: true });
  115. await mkdir(tempCliDir, { recursive: true });
  116. await writeFile(
  117. join(tempRoot, "package.json"),
  118. `${JSON.stringify({ name: "inkos", version: "0.5.1" }, null, 2)}\n`,
  119. );
  120. await writeFile(
  121. join(tempCoreDir, "package.json"),
  122. `${JSON.stringify({ name: "@actalk/inkos-core", version: "0.5.1" }, null, 2)}\n`,
  123. );
  124. await writeFile(
  125. join(tempCliDir, "package.json"),
  126. `${JSON.stringify(
  127. {
  128. name: "@actalk/inkos",
  129. version: "0.5.1",
  130. dependencies: {
  131. "@actalk/inkos-core": "workspace:*",
  132. commander: "^13.0.0",
  133. },
  134. },
  135. null,
  136. 2,
  137. )}\n`,
  138. );
  139. expect(() =>
  140. execFileSync(
  141. "node",
  142. [resolve(workspaceRoot, "scripts/verify-no-workspace-protocol.mjs"), "packages/core", "packages/cli"],
  143. {
  144. cwd: tempRoot,
  145. env: process.env,
  146. encoding: "utf-8",
  147. stdio: "pipe",
  148. },
  149. )).not.toThrow();
  150. } finally {
  151. await rm(tempRoot, { recursive: true, force: true });
  152. }
  153. });
  154. it("rejects workspace protocol manifests that normalize to the wrong internal version", async () => {
  155. const tempRoot = await mkdtemp(join(tmpdir(), "inkos-publish-verify-fail-"));
  156. const tempPackagesDir = join(tempRoot, "packages");
  157. const tempCoreDir = join(tempPackagesDir, "core");
  158. const tempCliDir = join(tempPackagesDir, "cli");
  159. try {
  160. await mkdir(tempCoreDir, { recursive: true });
  161. await mkdir(tempCliDir, { recursive: true });
  162. await writeFile(
  163. join(tempRoot, "package.json"),
  164. `${JSON.stringify({ name: "inkos", version: "0.5.1" }, null, 2)}\n`,
  165. );
  166. await writeFile(
  167. join(tempCoreDir, "package.json"),
  168. `${JSON.stringify({ name: "@actalk/inkos-core", version: "0.5.1" }, null, 2)}\n`,
  169. );
  170. await writeFile(
  171. join(tempCliDir, "package.json"),
  172. `${JSON.stringify(
  173. {
  174. name: "@actalk/inkos",
  175. version: "0.5.1",
  176. dependencies: {
  177. "@actalk/inkos-core": "workspace:0.5.0",
  178. },
  179. },
  180. null,
  181. 2,
  182. )}\n`,
  183. );
  184. expect(() =>
  185. execFileSync(
  186. "node",
  187. [resolve(workspaceRoot, "scripts/verify-no-workspace-protocol.mjs"), "packages/cli"],
  188. {
  189. cwd: tempRoot,
  190. env: process.env,
  191. encoding: "utf-8",
  192. stdio: "pipe",
  193. },
  194. )).toThrow(/normalizes to 0\.5\.0, expected 0\.5\.1/);
  195. } finally {
  196. await rm(tempRoot, { recursive: true, force: true });
  197. }
  198. });
  199. it("replaces workspace dependencies before npm pack", { timeout: 30_000 }, async () => {
  200. const packDir = await mkdtemp(join(tmpdir(), "inkos-cli-pack-"));
  201. try {
  202. const packedPackageJson = JSON.parse(await extractPackedPackageJson(cliDir, packDir));
  203. const corePackageJson = JSON.parse(
  204. await readFile(resolve(workspaceRoot, "packages/core/package.json"), "utf-8"),
  205. );
  206. const studioPackageJson = await sourceStudioPackageJsonPromise;
  207. expect(packedPackageJson.dependencies["@actalk/inkos-core"]).toBe(corePackageJson.version);
  208. expect(packedPackageJson.dependencies["@actalk/inkos-studio"]).toBe(studioPackageJson.version);
  209. } finally {
  210. await rm(packDir, { recursive: true, force: true });
  211. }
  212. });
  213. it("packs the studio runtime entry alongside the built frontend", { timeout: 60_000 }, async () => {
  214. const packDir = await mkdtemp(join(tmpdir(), "inkos-studio-pack-"));
  215. try {
  216. const tarballPath = await packPackage(studioDir, packDir);
  217. const tarArgs = process.platform === "win32" ? ["--force-local", "-tf"] : ["-tf"];
  218. const archiveListing = execFileSync("tar", [...tarArgs, tarballPath], {
  219. cwd: workspaceRoot,
  220. encoding: "utf-8",
  221. });
  222. expect(archiveListing).toContain("package/dist/index.html");
  223. expect(archiveListing).toContain("package/dist/api/index.js");
  224. } finally {
  225. await rm(packDir, { recursive: true, force: true });
  226. }
  227. });
  228. });