publish-package.test.ts 9.3 KB

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