🌳 fix: make cereale/min tree-shake too, and correct what I overclaimed
An audit of the last commit found two real problems and several claims of mine that went further than the evidence. **cereale/min was not tree-shakable.** scripts/build-bundle.mjs used esbuild's `minify: true`, whose minifyWhitespace pass strips comments — /*#__PURE__*/ annotations included. The published entry point therefore reproduced exactly the bug the previous commit fixed: one decorator came out at 5,066 bytes with all 26 unrelated rule messages, against 1,837 from the per-module entry. Every source-level check stayed green, because they all bundled src/ and the annotations are stripped on the way into dist/. It is now minified for syntax and identifiers but not whitespace: 33.9 KB raw and 9.6 KB gzipped against 26.0/8.7, so about a kilobyte over the wire for a file that behaves correctly however it is used. One decorator via cereale/min is now 1,996 bytes. The build asserts the annotation count survives, and treeshake.test.ts now bundles the published artifact as well as the source — the gap that let this through. **sideEffects was partly inert.** `./dist/cjs/metadata.js` could never match: the build writes dist/cjs/package.json, which becomes the nearest descriptor for everything beneath it, so bundlers read sideEffects from there. That file now carries its own declaration. `./src/metadata.ts` was missing while src/ is published, which declared the Symbol.metadata install droppable in the source tree. Five module-scope caches in utils.ts are annotated for the same reason as the rules. I checked the audit's third blocker — that the Symbol.metadata install is dropped by bundlers — and it is not. It survives every case where it is load-bearing (a decorator import, toPlainSync, modelOf, and a decorated model bundled with an app). It is dropped only when importing nothing but flattenErrors, which needs no metadata, so that is correct. Corrections to my own wording: - "all three bundlers land within 20 bytes" held only for the one-decorator row; larger imports differ by up to a few hundred bytes - "measured through three bundlers, and pinned by a test" read as though the test covered all three; it covers esbuild, on source and on dist - "every rule is a top-level call" — 30 of the 68 are - the docs page "loads nothing from the network" — it fetches its own vendored compiler, same-origin, on first Run. It has no third-party dependencies, which is the claim I should have made - cereale/min's size, everywhere it appears Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
@@ -27,8 +27,9 @@ user.greet(); // your methods are still there
|
||||
|
||||
**[avalon-vanguard.github.io/cereale](https://avalon-vanguard.github.io/cereale/)** — an
|
||||
interactive playground that runs this library in your browser, the full decorator reference,
|
||||
and the toolchain matrix. The page is self-contained and loads nothing from the network; it is
|
||||
served from `docs/` on `main`, and `npm run build:docs` rebuilds its assets to open locally.
|
||||
and the toolchain matrix. The page has no third-party dependencies — no CDN, no analytics, no webfonts; the only thing it
|
||||
fetches is its own vendored compiler, and only when you first press Run. It is served from
|
||||
`docs/` on `main`, and `npm run build:docs` rebuilds its assets to open locally.
|
||||
|
||||
## Where it fits
|
||||
|
||||
@@ -498,29 +499,38 @@ it is one `Symbol.toStringTag` read per object.
|
||||
|
||||
Cereale tree-shakes. Every rule is declared so that a bundler can drop the ones you did not
|
||||
import, which matters for a library with 68 decorators — you pay for what you name and nothing
|
||||
else. Minified bytes, measured through three bundlers, and
|
||||
[pinned by a test](src/treeshake.test.ts) so it cannot quietly regress:
|
||||
else. Minified bytes, measured through esbuild, rollup and webpack. The table was measured by hand;
|
||||
what is [pinned by a test](src/treeshake.test.ts) is the property behind it — that a given
|
||||
import drops the parts of the library it does not reach — checked through esbuild on both the
|
||||
source and the published bundle:
|
||||
|
||||
| What you import | esbuild | rollup | webpack |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `flattenErrors` | 287 | 292 | 291 |
|
||||
| one decorator | 1,837 | 1,823 | 1,818 |
|
||||
| `validateSync` | 3,722 | 3,554 | 3,823 |
|
||||
| `toPlainSync` | 7,744 | 7,769 | 7,832 |
|
||||
| `toInstanceSync` | 7,900 | 7,942 | 7,956 |
|
||||
| a typical DTO — 5 decorators, map, validate, format errors | 10,395 | 10,402 | 10,372 |
|
||||
| `validateSync` | 3,722 | 3,554 | 3,738 |
|
||||
| `toPlainSync` | 7,744 | 7,769 | 7,771 |
|
||||
| `toInstanceSync` | 7,900 | 7,942 | 7,944 |
|
||||
| a typical DTO — 5 decorators, map, validate, format errors | 10,395 | 10,402 | 10,360 |
|
||||
| the whole library | 26,266 | 25,671 | 26,879 |
|
||||
|
||||
The serializer and the deserializer drop independently: read JSON and you do not pay for
|
||||
writing it. The validator is kept by both, because `validate` defaults to `true` and the entry
|
||||
points reference it whatever a given call site passes.
|
||||
|
||||
This did not come for free. Every rule is a top-level call — `export const IsString = rule(…)` —
|
||||
and rollup can prove such a call side-effect-free by reading the factory, but esbuild and
|
||||
webpack will not. Without a `/*#__PURE__*/` annotation on each of them, importing one decorator
|
||||
pulled in the message and validator of all 68: **4,909 bytes instead of 1,837**. Nothing failed;
|
||||
the library was simply three times heavier in every consumer's bundle, and the only way to find
|
||||
out was to measure.
|
||||
This did not come for free. Thirty of the rules are declared as top-level calls —
|
||||
`export const IsString = rule(…)` — and rollup can prove such a call side-effect-free by reading
|
||||
the factory, but esbuild and webpack will not. Without a `/*#__PURE__*/` annotation on each of
|
||||
them, importing one decorator pulled in the message and validator of all 68: **4,909 bytes
|
||||
instead of 1,837**. Nothing failed; the library was simply three times heavier in every
|
||||
consumer's bundle, and the only way to find out was to measure. Note what that means for
|
||||
measuring: rollup alone would have shown nothing wrong.
|
||||
|
||||
`cereale/min` tree-shakes too, which took a second fix — esbuild's `minify` strips comments,
|
||||
annotations included, so the flat bundle was silently reproducing the same bug (5,066 bytes for
|
||||
one decorator). It is now minified for syntax and identifiers but not whitespace: 33.9 KB raw,
|
||||
9.6 KB gzipped, about a kilobyte over the wire more than full minification would give. Still,
|
||||
if you are using a bundler, import from `cereale` rather than `cereale/min`.
|
||||
|
||||
## Notes and Limitations
|
||||
|
||||
|
||||
Reference in New Issue
Block a user