🌳 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:
+5
-5
@@ -42,7 +42,7 @@ export class JsonMappingError extends Error {
|
||||
* next steps in a pollution chain. This library exists to parse request bodies, so the
|
||||
* transform layer drops them rather than trusting callers to sanitise first.
|
||||
*/
|
||||
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
||||
const FORBIDDEN_KEYS = /*#__PURE__*/ new Set(['__proto__', 'constructor', 'prototype']);
|
||||
|
||||
/**
|
||||
* Stands in for the value of a property that is never serialized, so that a failing password
|
||||
@@ -96,7 +96,7 @@ interface OutboundProperty {
|
||||
serializer?: any;
|
||||
}
|
||||
|
||||
const outboundCache = new WeakMap<ClassModel, { version: number; byStrategy: Map<unknown, Map<string, OutboundProperty>> }>();
|
||||
const outboundCache = /*#__PURE__*/ new WeakMap<ClassModel, { version: number; byStrategy: Map<unknown, Map<string, OutboundProperty>> }>();
|
||||
|
||||
/**
|
||||
* Resolves how one property is written out, memoized per (prototype, naming strategy).
|
||||
@@ -167,7 +167,7 @@ interface InboundNames {
|
||||
|
||||
// Name maps are derived purely from decorator metadata, which is fixed once a class is
|
||||
// declared, so they are cached per (prototype, naming strategy).
|
||||
const inboundCache = new WeakMap<ClassModel, { version: number; byStrategy: Map<unknown, InboundNames> }>();
|
||||
const inboundCache = /*#__PURE__*/ new WeakMap<ClassModel, { version: number; byStrategy: Map<unknown, InboundNames> }>();
|
||||
|
||||
/**
|
||||
* Builds the JSON-name -> property-key lookup used when reading a payload.
|
||||
@@ -614,7 +614,7 @@ interface CachedPlan {
|
||||
// Turning a class model into a per-property plan is cheap, but doing it on every call was
|
||||
// measurably not: profiling showed roughly half of all validation time re-deriving answers
|
||||
// that cannot change. Plans are memoized per model and invalidated by the model version.
|
||||
const planCache = new WeakMap<ClassModel, CachedPlan>();
|
||||
const planCache = /*#__PURE__*/ new WeakMap<ClassModel, CachedPlan>();
|
||||
|
||||
/**
|
||||
* Collapses rules that are genuinely identical.
|
||||
@@ -663,7 +663,7 @@ function validationPlan(model: ClassModel): PropertyPlan[] {
|
||||
|
||||
// Serializers and deserializers are stateless by contract, so one instance per class is
|
||||
// enough. Constructing a fresh one for every property of every object was pure waste.
|
||||
const converterCache = new WeakMap<object, any>();
|
||||
const converterCache = /*#__PURE__*/ new WeakMap<object, any>();
|
||||
|
||||
function converterFor(clazz: any): any {
|
||||
let instance = converterCache.get(clazz);
|
||||
|
||||
Reference in New Issue
Block a user