@@ -42,6 +42,40 @@ class SheetRect extends Rectangle {
4242const copiedSheet: SheetRect = Rectangle.Clone(new SheetRect(4, 4));
4343export 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+
4579const root = fileURLToPath ( new URL ( ".." , import . meta. url ) ) ;
4680const 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" ,
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