🌳 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:
+16
-2
@@ -15,8 +15,22 @@ import type { ClassConstructor } from './interfaces.js';
|
||||
*/
|
||||
const METADATA_KEY: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for('Symbol.metadata');
|
||||
|
||||
// Also installed globally, because a consumer's own compiler emit may read `Symbol.metadata`
|
||||
// directly. package.json marks this module as having side effects so it survives bundling.
|
||||
// Also installed globally, because a consumer's own compiler emit reads `Symbol.metadata`
|
||||
// directly and does not share our fallback. tsc emits
|
||||
//
|
||||
// const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
||||
//
|
||||
// so on a runtime without the well-known symbol the class is decorated with `metadata:
|
||||
// undefined` and ends up with no metadata at all. (esbuild's `__knownSymbol` has the same
|
||||
// `Symbol.for` fallback we do and needs nothing from us; tsc does.)
|
||||
//
|
||||
// `sideEffects` in package.json is what keeps this statement through bundling — and it has to
|
||||
// name `index.ts`/`index.js` as well as this module. Marking only this one is not enough: the
|
||||
// barrel is then itself side-effect-free, so a bundler drops the `export * from './metadata.js'`
|
||||
// edge before this module's own marking is ever consulted, and the install silently vanishes.
|
||||
// Measured on `import { configure } from 'cereale'`: absent from all three of esbuild, webpack
|
||||
// and rollup until the barrel was listed too. It costs ~100 bytes, and only for imports that
|
||||
// pull in nothing else — every entry point that touches a model was already byte-identical.
|
||||
((Symbol as { metadata?: symbol }).metadata as symbol | undefined) ??= METADATA_KEY;
|
||||
|
||||
export interface ValidationArguments {
|
||||
|
||||
Reference in New Issue
Block a user