Skip to content

Commit 8efd5fd

Browse files
committed
build: pin the published file set
What the tarball contains was decided by the `files` allowlist and never asserted: `npm pack` was measured once at 28 files with no `docs/` entry, and nothing failed if the site's source tree or the uncompressed PNGs joined the allowlist, or if `src/` and a tsconfig left it. `scripts/verify-package.mjs` now compares `npm pack --dry-run --json` against the intended list and names the difference, in both directions. Measured: with `docs` added to `files` it reports "179 files, not the 28 intended — not expected: docs/.vitepress/config.mts, … and 143 more"; with `src` removed, "21 files … missing: src/abstract-bin.ts, …"; on the intended allowlist it passes. AGENTS.md says the list is pinned and where.
1 parent d724d52 commit 8efd5fd

2 files changed

Lines changed: 65 additions & 6 deletions

File tree

‎AGENTS.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,10 @@ English keeps the project history usable for every contributor and every downstr
340340
- Published content is decided by the `files` allowlist in `package.json`: `dist` + `src` +
341341
`assets/favicon.ico` + `CHANGELOG.md` + `tsconfig*.json` + `typedoc.json`
342342
(measured `npm pack`: ~66kB / 28 files — the byte count drifts slightly between builds, the file
343-
count does not; the published 2.7.4 tarball was 67.0kB / 33 files). The differences from 2.7.4 are
343+
count does not; the published 2.7.4 tarball was 67.0kB / 33 files). That file list is **pinned**:
344+
`PUBLISHED_FILES` in `scripts/verify-package.mjs` is compared against `npm pack --dry-run --json`, so
345+
an entry added to the allowlist (the site's `docs/` tree, `assets/*.png`) or dropped from it (`src/`,
346+
a tsconfig) fails `npm run verify:package` by name. The differences from 2.7.4 are
344347
all deliberate: `eslint.config.js`, `.eslintrc.json` and `.github/workflows/node.js.yml` are no
345348
longer published, `dist/maxrects-packer.cjs` is new, and `UPGRADE_SUMMARY.md` was deleted as an
346349
obsolete dependency-upgrade log. There is **no `.npmignore`**, so npm falls back to `.gitignore`

‎scripts/verify-package.mjs‎

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,40 @@ class SheetRect extends Rectangle {
4242
const copiedSheet: SheetRect = Rectangle.Clone(new SheetRect(4, 4));
4343
export const summary = [saved.length, bins.length, bin.width, oversized.width, oversizedWithoutData.width, rect.width, copied.width, copiedSheet.label];
4444
`;
45+
// What `npm pack` is meant to produce, sorted. Almost all of it comes from the `files` allowlist in
46+
// package.json; `package.json`, `README.md` and `LICENSE` are npm's own additions. Edit this list only
47+
// together with the allowlist — it is the record of what consumers download.
48+
const PUBLISHED_FILES = [
49+
"CHANGELOG.md",
50+
"LICENSE",
51+
"README.md",
52+
"assets/favicon.ico",
53+
"dist/abstract-bin.d.ts",
54+
"dist/geom/Rectangle.d.ts",
55+
"dist/index.d.ts",
56+
"dist/maxrects-bin.d.ts",
57+
"dist/maxrects-packer.cjs",
58+
"dist/maxrects-packer.d.ts",
59+
"dist/maxrects-packer.js",
60+
"dist/maxrects-packer.js.map",
61+
"dist/maxrects-packer.min.js",
62+
"dist/maxrects-packer.mjs",
63+
"dist/maxrects-packer.mjs.map",
64+
"dist/oversized-element-bin.d.ts",
65+
"dist/types.d.ts",
66+
"package.json",
67+
"src/abstract-bin.ts",
68+
"src/geom/Rectangle.ts",
69+
"src/index.ts",
70+
"src/maxrects-bin.ts",
71+
"src/maxrects-packer.ts",
72+
"src/oversized-element-bin.ts",
73+
"src/types.ts",
74+
"tsconfig.build.json",
75+
"tsconfig.json",
76+
"typedoc.json"
77+
].sort();
78+
4579
const root = fileURLToPath(new URL("..", import.meta.url));
4680
const workdir = mkdtempSync(join(tmpdir(), "maxrects-packer-verify-"));
4781

@@ -53,13 +87,35 @@ try {
5387
const tarball = join(workdir, tarballName);
5488
console.log(` ✓ npm pack -> ${tarballName}`);
5589

56-
// 2) Install into a brand-new consumer project (the temporary directory itself)
90+
// 2) The tarball carries exactly the files that are meant to ship. A `files` allowlist decides this,
91+
// so the two quiet mistakes are a path added to it (the documentation site's source tree, the
92+
// uncompressed PNGs) and an entry dropped from it (`src/`, a tsconfig) — neither of which any other
93+
// check would notice, since the entry points themselves would still resolve.
94+
const published = JSON.parse(run("npm", ["pack", "--dry-run", "--json"], { cwd: root }))[0]
95+
.files.map((file) => file.path)
96+
.sort();
97+
const added = published.filter((path) => !PUBLISHED_FILES.includes(path));
98+
const dropped = PUBLISHED_FILES.filter((path) => !published.includes(path));
99+
if (added.length > 0 || dropped.length > 0) {
100+
const list = (paths) =>
101+
paths.length > 8 ? `${paths.slice(0, 8).join(", ")} …and ${paths.length - 8} more` : paths.join(", ");
102+
const detail = [
103+
added.length > 0 ? `not expected: ${list(added)}` : null,
104+
dropped.length > 0 ? `missing: ${list(dropped)}` : null
105+
].filter(Boolean);
106+
throw new Error(
107+
`the tarball holds ${published.length} files, not the ${PUBLISHED_FILES.length} intended — ${detail.join("; ")}`
108+
);
109+
}
110+
console.log(` ✓ npm pack --dry-run lists the ${published.length} intended files`);
111+
112+
// 3) Install into a brand-new consumer project (the temporary directory itself)
57113
execFileSync("npm", ["init", "-y"], { cwd: workdir, stdio: "ignore" });
58114
writeFileSync(join(workdir, "package.json"), JSON.stringify({ name: "consumer", private: true }, null, 2));
59115
run("npm", ["install", "--no-save", "--no-package-lock", "--no-audit", "--no-fund", tarball], { cwd: workdir });
60116
console.log(" ✓ installed into the temporary consumer project");
61117

62-
// 3) Verify CJS by package name — this is the path that was broken historically
118+
// 4) Verify CJS by package name — this is the path that was broken historically
63119
const cjs = JSON.parse(
64120
run(
65121
"node",
@@ -80,7 +136,7 @@ try {
80136
throw new Error(`require("maxrects-packer") loaded but misbehaves: expected 1 bin, got ${cjs.bins}`);
81137
console.log(` ✓ require("maxrects-packer") -> ${cjs.keys.length} exports, real packing run OK`);
82138

83-
// 4) Verify ESM by package name (Node loads main through CJS interop; named exports come from cjs-module-lexer)
139+
// 5) Verify ESM by package name (Node loads main through CJS interop; named exports come from cjs-module-lexer)
84140
// Node 23+ also adds a synthetic "module.exports" key to the namespace of a CommonJS module, so the
85141
// raw key count is 6 on Node 20/22 and 7 on Node 24. Drop it: this gate is about which real exports
86142
// are reachable by name, and a count that changes with the Node version reads like a regression.
@@ -102,7 +158,7 @@ try {
102158
throw new Error(`import("maxrects-packer") is missing named exports: ${missingEsm.join(", ")}`);
103159
console.log(` ✓ import("maxrects-packer") -> ${esmKeys.length} named exports`);
104160

105-
// 5) Verify the published types by package name, in every resolution mode a consumer can use.
161+
// 6) Verify the published types by package name, in every resolution mode a consumer can use.
106162
// package.json "types" decides what a TypeScript consumer resolves, and it pointed at the
107163
// declaration of src/maxrects-packer.ts instead of the barrel's, so six of the nine documented
108164
// exports could not be imported while every runtime check above stayed green. Only compiling an
@@ -155,7 +211,7 @@ try {
155211
}
156212
console.log(` ✓ the published types accept the documented imports (${MODES.map((mode) => mode.name).join(", ")})`);
157213

158-
// 6) Run the documentation examples marked with `<!-- docs-example: name -->`, against the package
214+
// 7) Run the documentation examples marked with `<!-- docs-example: name -->`, against the package
159215
// installed by name. The guides are the first thing a user copies, and nothing else would notice an
160216
// example that stopped working: the test specs import `../src`, so a broken README or guide snippet
161217
// rots silently while every gate stays green. Every page is scanned rather than a hand-kept list of

0 commit comments

Comments
 (0)