🔊 feat!: make the three silent failures loud
Every change here answers one question: where does cereale currently fail
without saying so?
**Vite 8 / Vitest 4 drop decorators silently.** Both transform with oxc,
which does not implement the standard decorator transform and does not
report that. `vitest` prints "0 test" beside a bare SyntaxError, and
`vite build` reports success while emitting a bundle that throws on first
import. Ship the plugin that fixes it as `cereale/vite`, transforming with
esbuild and falling back to tsc — cereale depends on neither. The library's
own suite now runs through it, so it is exercised by every test.
**Legacy decorators died opaquely.** With `experimentalDecorators: true`,
still the default in most existing TypeScript projects, decorators are
invoked as (prototype, "name") and cereale raised "TypeError: Cannot convert
undefined or null to object". All decorators now resolve metadata through
one checkpoint that names the tsconfig setting instead, and reject
application to a method, getter or accessor field.
**Values JSON cannot carry were emptied.** A populated Map serialized to
{}, a Uint8Array to index-keyed noise, a bigint straight through so the
caller's own JSON.stringify threw somewhere unrelated. All now raise
JsonMappingError naming the property path and both ways out. Covers what a
@JsonSerialize serializer returns, sync or async. Circular-reference and
depth errors name the path too.
Also fixed: defineRule on a subclass with no decorators of its own wrote
the rule into its base class, because the base's metadata object is
inherited through the static prototype chain and `??=` found it non-nullish.
The README's toolchain table (tsc, esbuild, swc ✅, oxc ❌) is now executed
by a test rather than asserted, and the positioning leads with
class-validator + class-transformer, the stack cereale actually replaces,
rather than Zod, which it deliberately is not.
193 -> 249 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
@@ -219,3 +219,44 @@ describe('validate() accepts options', () => {
|
||||
expect(await validate(order)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decorator context guards', () => {
|
||||
// Every decorator resolves its metadata through one checkpoint, so one representative
|
||||
// decorator per shape is enough to cover the rule.
|
||||
const shapes: [string, unknown][] = [
|
||||
['a method', { kind: 'method', name: 'run', metadata: {} }],
|
||||
['a getter', { kind: 'getter', name: 'total', metadata: {} }],
|
||||
['an accessor', { kind: 'accessor', name: 'value', metadata: {} }],
|
||||
['a class', { kind: 'class', name: 'Thing', metadata: {} }],
|
||||
];
|
||||
|
||||
for (const [label, context] of shapes) {
|
||||
it(`refuses being applied to ${label}`, () => {
|
||||
expect(() => (IsString() as any)(undefined, context)).toThrow(/apply to fields/);
|
||||
});
|
||||
}
|
||||
|
||||
it('explains why `accessor` in particular cannot work', () => {
|
||||
const context = { kind: 'accessor', name: 'value', metadata: {} };
|
||||
expect(() => (IsString() as any)(undefined, context)).toThrow(/private slot/);
|
||||
});
|
||||
|
||||
it('refuses a legacy decorator call shape', () => {
|
||||
// What `experimentalDecorators: true` emits: (prototype, propertyKey).
|
||||
expect(() => (IsString() as any)({}, 'name')).toThrow(/experimentalDecorators/);
|
||||
});
|
||||
|
||||
it('refuses a standard context that carries no metadata', () => {
|
||||
const context = { kind: 'field', name: 'value', metadata: undefined };
|
||||
expect(() => (IsString() as any)(undefined, context)).toThrow(/no metadata object/);
|
||||
});
|
||||
|
||||
it('names the field it could not record', () => {
|
||||
const context = { kind: 'field', name: 'nickname', metadata: null };
|
||||
expect(() => (IsString() as any)(undefined, context)).toThrow(/"nickname"/);
|
||||
});
|
||||
|
||||
it('guards the mapping decorators too, not just the rules', () => {
|
||||
expect(() => (JsonSerialize(class {} as any) as any)({}, 'name')).toThrow(/experimentalDecorators/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user