From 42acf5e38cab70e5b4b1555dc545b7b5ce078d99 Mon Sep 17 00:00:00 2001 From: Shen Yiming Date: Wed, 30 Sep 2026 14:56:46 +0800 Subject: [PATCH 1/4] build: retire the old TypeDoc theme and its assets Phase E of #81. `typedoc-unhoax-theme` has been unreferenced since the API reference moved to markdown, and the two theme assets it loaded are dead weight in the published package. - `typedoc-unhoax-theme` is out of `devDependencies`; `npm ci` still installs the committed lockfile, and the package is gone from `node_modules`. - `assets/custom.css` and `assets/custom.js` are deleted, and out of the `files` allowlist with them: measured `npm pack` goes from 30 files / 67kB to **28 files / 66.6kB**, with `assets/favicon.ico` as the only asset left. - `AGENTS.md` and `docs/contributor/compatibility.md` carry the new allowlist and the measured counts. - `gh-pages` stays for now: `doc:publish` is the fallback deployment path until the repository's Pages source is switched to GitHub Actions, and the ledger records that as the one remaining step. Verified after the change: `npm ci --include=dev` against the committed lockfile, lint 0/0, format:check, typecheck (TS 7), verify:docs, docs:build (11 legacy redirects written), 126 tests passed / 2 skipped, coverage 100%, verify:package green (bundler, node16, nodenext, and the three documentation examples). --- AGENTS.md | 6 +++--- assets/custom.css | 21 ------------------- assets/custom.js | 34 ------------------------------- docs/contributor/compatibility.md | 4 ++-- docs/plans/deferred-work.md | 10 +++++++-- package-lock.json | 11 ---------- package.json | 3 --- 7 files changed, 13 insertions(+), 76 deletions(-) delete mode 100644 assets/custom.css delete mode 100644 assets/custom.js diff --git a/AGENTS.md b/AGENTS.md index f77a387..31c617c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -338,15 +338,15 @@ 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 + `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). 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 diff --git a/assets/custom.css b/assets/custom.css deleted file mode 100644 index 0d5c189..0000000 --- a/assets/custom.css +++ /dev/null @@ -1,21 +0,0 @@ -/* Custom CSS for TypeDoc with Favicon */ - -/* Add favicon to the site title in header */ -.header-logo::before { - content: ""; - display: inline-block; - width: 24px; - height: 24px; - background-image: url('./favicon.png'); - background-size: contain; - background-repeat: no-repeat; - margin-right: 8px; - vertical-align: middle; -} - -/* Ensure the title container can accommodate the favicon */ -.header-logo { - display: flex; - align-items: center; -} - diff --git a/assets/custom.js b/assets/custom.js deleted file mode 100644 index 6515c2a..0000000 --- a/assets/custom.js +++ /dev/null @@ -1,34 +0,0 @@ -// Custom JavaScript for TypeDoc to add favicon -(function () { - "use strict"; - - // Add favicon to document head - function addFavicon() { - // Add ICO favicon - var icoFavicon = document.createElement("link"); - icoFavicon.rel = "icon"; - icoFavicon.type = "image/x-icon"; - icoFavicon.href = "./favicon.ico"; - document.head.appendChild(icoFavicon); - - // Add PNG favicon - var pngFavicon = document.createElement("link"); - pngFavicon.rel = "icon"; - pngFavicon.type = "image/png"; - pngFavicon.href = "./favicon.png"; - document.head.appendChild(pngFavicon); - - // Add Apple touch icon - var appleTouchIcon = document.createElement("link"); - appleTouchIcon.rel = "apple-touch-icon"; - appleTouchIcon.href = "./favicon.png"; - document.head.appendChild(appleTouchIcon); - } - - // Run when DOM is loaded - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", addFavicon); - } else { - addFavicon(); - } -})(); diff --git a/docs/contributor/compatibility.md b/docs/contributor/compatibility.md index b1bc1a3..5896f6c 100644 --- a/docs/contributor/compatibility.md +++ b/docs/contributor/compatibility.md @@ -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 diff --git a/docs/plans/deferred-work.md b/docs/plans/deferred-work.md index 6dca8fb..903bca8 100644 --- a/docs/plans/deferred-work.md +++ b/docs/plans/deferred-work.md @@ -147,5 +147,11 @@ 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. diff --git a/package-lock.json b/package-lock.json index 075d1b6..90af541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,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", @@ -8183,16 +8182,6 @@ "typedoc": "0.28.x" } }, - "node_modules/typedoc-unhoax-theme": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/typedoc-unhoax-theme/-/typedoc-unhoax-theme-0.5.3.tgz", - "integrity": "sha512-7J8cp1/OQCdYz1c9k6tTWg1u5qwChKF/dR011xa/Cz5sQldEdYHaxSZ9ci4E8s6/DKcxiMoQpcRReLix/3WgwQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typedoc": "^0.28.0" - } - }, "node_modules/typedoc-vitepress-theme": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/typedoc-vitepress-theme/-/typedoc-vitepress-theme-1.1.4.tgz", diff --git a/package.json b/package.json index e6fd60f..93a22ba 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,6 @@ "files": [ "dist", "src", - "assets/custom.css", - "assets/custom.js", "assets/favicon.ico", "CHANGELOG.md", "tsconfig.json", @@ -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", From 0374ef466797e689707bd5c6a3d7f3c1b7b1872f Mon Sep 17 00:00:00 2001 From: Shen Yiming Date: Wed, 30 Sep 2026 19:19:38 +0800 Subject: [PATCH 2/4] docs: note the housekeeping the pre-deploy audit turned up Two findings that are not broken today and belong in their own commits: the workflows pin action majors several releases behind (docs.yml four of them, and nothing in the repository raises those automatically), and the README's only local image is a 2.9 kB file the `files` allowlist omits while `AGENTS.md` frames the fix as costing 468 kB. --- docs/plans/deferred-work.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/plans/deferred-work.md b/docs/plans/deferred-work.md index 903bca8..9248998 100644 --- a/docs/plans/deferred-work.md +++ b/docs/plans/deferred-work.md @@ -155,3 +155,27 @@ Still open: dropping `gh-pages` (and the `doc:publish` alias that uses it) once 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. Neither is broken today — every run on the +three pull requests is green — so both wait for a commit of their 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 `` in the README's heading, and that URL already 404s on the + published TypeDoc site. From 906290ec144904a594edb4e8cca2d71e5abdf247 Mon Sep 17 00:00:00 2001 From: Shen Yiming Date: Wed, 30 Sep 2026 20:17:51 +0800 Subject: [PATCH 3/4] build: pin the published file set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- AGENTS.md | 5 ++- scripts/verify-package.mjs | 66 +++++++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 31c617c..f0f1605 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -340,7 +340,10 @@ English keeps the project history usable for every contributor and every downstr - Published content is decided by the `files` allowlist in `package.json`: `dist` + `src` + `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). The differences from 2.7.4 are + 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` diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index ce8f4e7..6f0c8cf 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -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-")); @@ -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", @@ -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. @@ -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 @@ -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 ``, against the package + // 7) Run the documentation examples marked with ``, 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 From 91f973a050fd44a1b51d585539f960acb97bde65 Mon Sep 17 00:00:00 2001 From: Shen Yiming Date: Wed, 30 Sep 2026 21:12:56 +0800 Subject: [PATCH 4/4] docs: record the CommonJS TypeScript import gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix for it is an `exports` map, and that also seals the deep imports this project keeps working, so it belongs to 3.0.0 rather than to the documentation stack — which is exactly what the deferred-work ledger is for. Measured on the published tarball: TS1479 for a static import from a `.cts` file under node16/nodenext and TS1471 for `import ... = require(...)`, while dynamic `import()`, node10/bundler resolution and JavaScript `require()` all compile. --- docs/plans/deferred-work.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/plans/deferred-work.md b/docs/plans/deferred-work.md index 9248998..7e879b7 100644 --- a/docs/plans/deferred-work.md +++ b/docs/plans/deferred-work.md @@ -158,8 +158,8 @@ unaffected: `docs/` is not in the `files` allowlist. ## Workflow and packaging follow-ups -Noticed while auditing the stack before its first deployment. Neither is broken today — every run on the -three pull requests is green — so both wait for a commit of their own. +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`, @@ -179,3 +179,14 @@ three pull requests is green — so both wait for a commit of their own. 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 `` 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.