From 6ec09ff0383f36d3bb311a0fa6c098f9b19863fc Mon Sep 17 00:00:00 2001 From: Shahmir Varqha Date: Mon, 8 Jun 2026 23:50:40 +0800 Subject: [PATCH] fix(ci): ship src/data/*.json in published tarball PR #128 split the release into separate build/publish jobs; the build job's upload-artifact step omitted src/, so src/data/*.json was absent on disk when the publish job ran pnpm publish. npm silently drops files entries that don't exist, leaving the ./data/* exports dead in 0.2.5+. Add src/data/ to the artifact path so the keyword JSON files reach the publish job. Add a vitest guard that runs npm pack and asserts every src/-based exports target is in the tarball, so this can't recur silently. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 1 + src/__tests__/package-exports.test.ts | 52 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/__tests__/package-exports.test.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ec0489..0e13b9b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,7 @@ jobs: name: package path: | dist/ + src/data/ package.json README.md LICENSE diff --git a/src/__tests__/package-exports.test.ts b/src/__tests__/package-exports.test.ts new file mode 100644 index 0000000..c33d7de --- /dev/null +++ b/src/__tests__/package-exports.test.ts @@ -0,0 +1,52 @@ +import { execSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const repoRoot = join(__dirname, "../.."); + +interface PackedFile { + path: string; +} + +/** + * Collect every string file path referenced anywhere in the package.json + * `exports` map (recursing through conditional-export objects like + * `{ import: { types, default } }`). + */ +function collectExportTargets(exportsField: unknown, out: string[] = []): string[] { + if (typeof exportsField === "string") { + out.push(exportsField); + } else if (exportsField && typeof exportsField === "object") { + for (const value of Object.values(exportsField as Record)) { + collectExportTargets(value, out); + } + } + return out; +} + +describe("published package", () => { + // Run the real `npm pack` so we assert against the actual tarball contents, + // not the source tree. Regression guard for the `./data/*` exports that + // shipped dead in 0.2.5–0.2.7 because `src/data/` was missing at publish time. + const packed: PackedFile[] = JSON.parse( + execSync("npm pack --dry-run --json", { cwd: repoRoot, encoding: "utf8" }), + )[0].files; + const packedPaths = new Set(packed.map((f) => f.path)); + + const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")); + + // Only assert source-committed targets (e.g. src/data/*.json). `dist/*` + // targets are intentionally skipped: in CI `pnpm test` runs before + // `pnpm build`, so dist does not exist yet when this test executes. + const sourceTargets = collectExportTargets(pkg.exports) + .map((p) => p.replace(/^\.\//, "")) + .filter((p) => p.startsWith("src/")); + + it("includes every src/ export target in the tarball", () => { + expect(sourceTargets.length).toBeGreaterThan(0); + for (const target of sourceTargets) { + expect(packedPaths, `${target} is referenced by exports but missing from the npm tarball`).toContain(target); + } + }); +});