🌳 fix: keep the Symbol.metadata install through tree-shaking
`sideEffects` named metadata.js, the module holding the global install, but not the
barrel that re-exports it. A side-effect-free barrel is droppable whole, so every
bundler pruned the `export * from './metadata.js'` edge before metadata.js's own
marking was ever consulted, and the install vanished.
Measured on `import { configure } from 'cereale'`: 145 bytes through esbuild, 143
through webpack, 144 through rollup, with `Symbol.metadata` absent from all three.
It matters because tsc's decorator emit reads the well-known symbol directly —
const _metadata = typeof Symbol === "function" && Symbol.metadata
? Object.create(null) : void 0;
so without it a decorated class is handed `metadata: undefined` and ends up with no
rules at all. Two places in the source promised this survived bundling; it did not.
index.js and index.ts are listed now. The cost is ~100 bytes and lands only on
imports that reach nothing else: the single-decorator, validateSync, toPlainSync,
toInstanceSync and whole-library cases are byte-identical before and after, across
all three bundlers. dist/cjs needs nothing — tsc emits `__exportStar(require(...))`,
an unconditional statement no bundler can drop.
Two new cases in treeshake.test.ts pin it, one on src and one on dist/esm, since
those are separate manifest paths and a typo in either is invisible from the other.
Both were checked by removing the entry and watching them fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
@@ -506,7 +506,7 @@ source and the published bundle:
|
||||
|
||||
| What you import | esbuild | rollup | webpack |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `flattenErrors` | 287 | 292 | 291 |
|
||||
| `flattenErrors` | 394 | 367 | 394 |
|
||||
| one decorator | 1,837 | 1,823 | 1,818 |
|
||||
| `validateSync` | 3,722 | 3,554 | 3,738 |
|
||||
| `toPlainSync` | 7,744 | 7,769 | 7,771 |
|
||||
@@ -518,6 +518,13 @@ The serializer and the deserializer drop independently: read JSON and you do not
|
||||
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.
|
||||
|
||||
The floor is about a hundred bytes: cereale installs `Symbol.metadata` if the runtime lacks it,
|
||||
and that install has to survive tree-shaking or a `tsc`-compiled consumer decorates its classes
|
||||
with no metadata at all. It is why `sideEffects` names `index.js` as well as `metadata.js` —
|
||||
marking only the latter leaves the barrel itself droppable, so the edge to it is pruned before
|
||||
its own marking is ever read. That cost lands only on the first row; every import that touches a
|
||||
model was already carrying it.
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user