Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,15 +338,18 @@ English keeps the project history usable for every contributor and every downstr
There is still no `exports` field, so deep imports (`maxrects-packer/dist/...`) work; adding one
would seal off deep paths, which is breaking and therefore reserved for 3.0.0.
- Published content is decided by the `files` allowlist in `package.json`: `dist` + `src` +
`assets/{custom.css,custom.js,favicon.ico}` + `CHANGELOG.md` + `tsconfig*.json` + `typedoc.json`
(measured `npm pack`: ~62kB / 30 files — the byte count drifts slightly between builds, the file
count does not; the published 2.7.4 tarball was 67.0kB / 33 files). The differences from 2.7.4 are
`assets/favicon.ico` + `CHANGELOG.md` + `tsconfig*.json` + `typedoc.json`
(measured `npm pack`: ~66kB / 28 files — the byte count drifts slightly between builds, the file
count does not; the published 2.7.4 tarball was 67.0kB / 33 files). That file list is **pinned**:
`PUBLISHED_FILES` in `scripts/verify-package.mjs` is compared against `npm pack --dry-run --json`, so
an entry added to the allowlist (the site's `docs/` tree, `assets/*.png`) or dropped from it (`src/`,
a tsconfig) fails `npm run verify:package` by name. The differences from 2.7.4 are
all deliberate: `eslint.config.js`, `.eslintrc.json` and `.github/workflows/node.js.yml` are no
longer published, `dist/maxrects-packer.cjs` is new, and `UPGRADE_SUMMARY.md` was deleted as an
obsolete dependency-upgrade log. There is **no `.npmignore`**, so npm falls back to `.gitignore`
(which lists `dist`/`lib`), but `files` wins and `dist` still ships. Note that `assets/*.png` is not
in the allowlist (2.7.x did not ship it either), so the README images stay broken on the npm page;
adding the whole `assets` directory would fix that at the cost of growing the tarball from ~62kB to
adding the whole `assets` directory would fix that at the cost of growing the tarball from ~66kB to
~530kB (`assets/` is 468kB, mostly uncompressed PNG). `clean` removes both `dist` and the legacy
`lib`.
- The `resolved` fields in `package-lock.json` must point at `registry.npmjs.org`: this machine has a
Expand Down
21 changes: 0 additions & 21 deletions assets/custom.css

This file was deleted.

34 changes: 0 additions & 34 deletions assets/custom.js

This file was deleted.

4 changes: 2 additions & 2 deletions docs/contributor/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ deferred-work ledger holds it for a major release.

## What ships

The `files` allowlist decides: `dist`, `src`, `assets/{custom.css,custom.js,favicon.ico}`,
`CHANGELOG.md`, `tsconfig*.json` and `typedoc.json` — currently 30 files. `docs/`, the documentation
The `files` allowlist decides: `dist`, `src`, `assets/favicon.ico`, `CHANGELOG.md`, `tsconfig*.json`
and `typedoc.json` — currently 28 files. `docs/`, the documentation
site and internal plans are not in it and never ship.

## Node version
Expand Down
45 changes: 43 additions & 2 deletions docs/plans/deferred-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,46 @@ the old TypeDoc site published. Switching the repository's Pages source to **Git
maintainer step that has to happen before the first deployment; until then `doc:publish` still pushes to
`gh-pages`, and the two paths must not be used together.

Still open: retiring the old theme, `gh-pages` and the theme assets from `devDependencies` and the
`files` allowlist. The published tarball is unaffected: `docs/` is not in the `files` allowlist.
The old theme is retired: `typedoc-unhoax-theme`, `assets/custom.css` and `assets/custom.js` are gone
from `devDependencies`, the repository and the `files` allowlist, which takes the published tarball from
30 files to 28 (`assets/favicon.ico` stays).

Still open: dropping `gh-pages` (and the `doc:publish` alias that uses it) once the Pages source is
switched to GitHub Actions — until then it is the fallback deployment path. `cz-conventional-changelog`
is unrelated stale tooling (interactive commitizen commits only). The published tarball is otherwise
unaffected: `docs/` is not in the `files` allowlist.

## Workflow and packaging follow-ups

Noticed while auditing the stack before its first deployment. None of these is broken today — every run
on the three pull requests is green — so each waits for a commit of its own.

- **The workflows pin action majors that are several releases behind.** `.github/workflows/docs.yml` uses
`actions/checkout@v4`, `actions/setup-node@v4`, `actions/configure-pages@v5`,
`actions/upload-pages-artifact@v3` and `actions/deploy-pages@v4`; the current majors read through the
API on 2026-09-30 are v7, v7, v6, v5 and v5. `node.js.yml` and `release.yml` sit on
`checkout@v4`/`setup-node@v4` too, and the repository has no Dependabot or Renovate configuration, so
nothing raises these on its own — one `ci:` PR should move all three files together rather than leaving
the new workflow on majors the others do not use. Worth doing before the first deployment, because
`actions/deploy-pages` is the one step that has never run here (the job is gated on a push to `master`)
and its inputs have to be re-checked against the current `action.yml` — the versions in use were
verified against `upload-pages-artifact@v3` and `configure-pages@v5`.
- **The README's only local image is `assets/favicon32.png`, 2911 bytes, and it is not in the `files`
allowlist.** `AGENTS.md` frames the fix as adding the whole `assets` directory for ~468 kB, but
`assets/preview.png` alone is 444695 of those bytes, so a single allowlist entry would do it for
+2.9 kB — if the npm page resolves a relative image out of the tarball at all. That could not be
checked from here (npmjs.com answers 403 to a plain request) and npm's renderer may resolve against the
repository instead, in which case there is nothing to fix. The file is also served by neither site:
the old index only used it as an `<img>` in the README's heading, and that URL already 404s on the
published TypeDoc site.
- **A CommonJS TypeScript consumer cannot import the package under `node16`/`nodenext`.** Measured on the
published tarball, in a `.cts` file compiled with `module`/`moduleResolution` `node16`:
`import { MaxRectsPacker } from "maxrects-packer"` is TS1479 ("the referenced file is an ECMAScript
module and cannot be imported with `require`") and `import pkg = require("maxrects-packer")` is TS1471,
while `await import(...)`, `moduleResolution` `node10`/`bundler` and a JavaScript `require()` all work;
neither `skipLibCheck` nor the compiler version changes anything (TS 6 and TS 7 both). No gate can see
it: `verify:package` compiles its type fixture in an ESM consumer, which is exactly what makes
`node16`/`nodenext` read it as ESM, and the runtime `require()` path it does cover is fine. The fix is
an `exports` map with per-format conditions and declarations — which also seals off the deep imports
`maxrects-packer/dist/...` that the entry-points section above keeps working, so it belongs to 3.0.0.
Documented as a sharp edge in `docs/user/troubleshooting.md` meanwhile.
11 changes: 0 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
"files": [
"dist",
"src",
"assets/custom.css",
"assets/custom.js",
"assets/favicon.ico",
"CHANGELOG.md",
"tsconfig.json",
Expand Down Expand Up @@ -79,7 +77,6 @@
"tslib": "^2.8.1",
"typedoc": "^0.28.20",
"typedoc-plugin-markdown": "^4.13.1",
"typedoc-unhoax-theme": "^0.5.3",
"typedoc-vitepress-theme": "^1.1.4",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"vitepress": "^1.6.4",
Expand Down
66 changes: 61 additions & 5 deletions scripts/verify-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ class SheetRect extends Rectangle {
const copiedSheet: SheetRect = Rectangle.Clone(new SheetRect(4, 4));
export const summary = [saved.length, bins.length, bin.width, oversized.width, oversizedWithoutData.width, rect.width, copied.width, copiedSheet.label];
`;
// What `npm pack` is meant to produce, sorted. Almost all of it comes from the `files` allowlist in
// package.json; `package.json`, `README.md` and `LICENSE` are npm's own additions. Edit this list only
// together with the allowlist — it is the record of what consumers download.
const PUBLISHED_FILES = [
"CHANGELOG.md",
"LICENSE",
"README.md",
"assets/favicon.ico",
"dist/abstract-bin.d.ts",
"dist/geom/Rectangle.d.ts",
"dist/index.d.ts",
"dist/maxrects-bin.d.ts",
"dist/maxrects-packer.cjs",
"dist/maxrects-packer.d.ts",
"dist/maxrects-packer.js",
"dist/maxrects-packer.js.map",
"dist/maxrects-packer.min.js",
"dist/maxrects-packer.mjs",
"dist/maxrects-packer.mjs.map",
"dist/oversized-element-bin.d.ts",
"dist/types.d.ts",
"package.json",
"src/abstract-bin.ts",
"src/geom/Rectangle.ts",
"src/index.ts",
"src/maxrects-bin.ts",
"src/maxrects-packer.ts",
"src/oversized-element-bin.ts",
"src/types.ts",
"tsconfig.build.json",
"tsconfig.json",
"typedoc.json"
].sort();

const root = fileURLToPath(new URL("..", import.meta.url));
const workdir = mkdtempSync(join(tmpdir(), "maxrects-packer-verify-"));

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

// 2) Install into a brand-new consumer project (the temporary directory itself)
// 2) The tarball carries exactly the files that are meant to ship. A `files` allowlist decides this,
// so the two quiet mistakes are a path added to it (the documentation site's source tree, the
// uncompressed PNGs) and an entry dropped from it (`src/`, a tsconfig) — neither of which any other
// check would notice, since the entry points themselves would still resolve.
const published = JSON.parse(run("npm", ["pack", "--dry-run", "--json"], { cwd: root }))[0]
.files.map((file) => file.path)
.sort();
const added = published.filter((path) => !PUBLISHED_FILES.includes(path));
const dropped = PUBLISHED_FILES.filter((path) => !published.includes(path));
if (added.length > 0 || dropped.length > 0) {
const list = (paths) =>
paths.length > 8 ? `${paths.slice(0, 8).join(", ")} …and ${paths.length - 8} more` : paths.join(", ");
const detail = [
added.length > 0 ? `not expected: ${list(added)}` : null,
dropped.length > 0 ? `missing: ${list(dropped)}` : null
].filter(Boolean);
throw new Error(
`the tarball holds ${published.length} files, not the ${PUBLISHED_FILES.length} intended — ${detail.join("; ")}`
);
}
console.log(` ✓ npm pack --dry-run lists the ${published.length} intended files`);

// 3) Install into a brand-new consumer project (the temporary directory itself)
execFileSync("npm", ["init", "-y"], { cwd: workdir, stdio: "ignore" });
writeFileSync(join(workdir, "package.json"), JSON.stringify({ name: "consumer", private: true }, null, 2));
run("npm", ["install", "--no-save", "--no-package-lock", "--no-audit", "--no-fund", tarball], { cwd: workdir });
console.log(" ✓ installed into the temporary consumer project");

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

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

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

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