🔊 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:
+15
-14
@@ -2,7 +2,7 @@ import type {
|
||||
ClassConstructor, FieldDecorator, JsonDeserializer, JsonSerializer,
|
||||
} from './interfaces.js';
|
||||
import {
|
||||
addConstraint, propertyModel,
|
||||
addConstraint, fieldMetadata, propertyModel,
|
||||
type EachValidationOptions, type PolymorphicInfo, type ValidationArguments,
|
||||
type ValidationConstraint, type ValidationOptions, type ValidatorConstraintInterface,
|
||||
} from './metadata.js';
|
||||
@@ -35,7 +35,7 @@ function decorate(
|
||||
): FieldDecorator<unknown> {
|
||||
return ((_target: undefined, context: ClassFieldDecoratorContext) => {
|
||||
const property = String(context.name);
|
||||
addConstraint(context.metadata, property, build(property), options);
|
||||
addConstraint(fieldMetadata(context), property, build(property), options);
|
||||
}) as FieldDecorator<unknown>;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ function pattern(name: string, regex: RegExp, message: (property: string) => str
|
||||
*/
|
||||
export function JsonProperty(name: string): FieldDecorator<unknown> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
propertyModel(context.metadata, String(context.name)).name = name;
|
||||
propertyModel(fieldMetadata(context), String(context.name)).name = name;
|
||||
}) as FieldDecorator<unknown>;
|
||||
}
|
||||
|
||||
@@ -84,14 +84,14 @@ export function JsonProperty(name: string): FieldDecorator<unknown> {
|
||||
*/
|
||||
export function JsonAlias(...names: string[]): FieldDecorator<unknown> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
const model = propertyModel(context.metadata, String(context.name));
|
||||
const model = propertyModel(fieldMetadata(context), String(context.name));
|
||||
model.aliases = [...(model.aliases ?? []), ...names];
|
||||
}) as FieldDecorator<unknown>;
|
||||
}
|
||||
|
||||
function access(value: 'none' | 'readonly' | 'writeonly'): FieldDecorator<unknown> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
propertyModel(context.metadata, String(context.name)).access = value;
|
||||
propertyModel(fieldMetadata(context), String(context.name)).access = value;
|
||||
}) as FieldDecorator<unknown>;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export function JsonSerialize<T>(
|
||||
serializer: ClassConstructor<JsonSerializer<T, any>>
|
||||
): FieldDecorator<T | null | undefined> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
propertyModel(context.metadata, String(context.name)).serializer = serializer;
|
||||
propertyModel(fieldMetadata(context), String(context.name)).serializer = serializer;
|
||||
}) as FieldDecorator<T | null | undefined>;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export function JsonDeserialize<R>(
|
||||
deserializer: ClassConstructor<JsonDeserializer<any, R>>
|
||||
): FieldDecorator<R | null | undefined> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
propertyModel(context.metadata, String(context.name)).deserializer = deserializer;
|
||||
propertyModel(fieldMetadata(context), String(context.name)).deserializer = deserializer;
|
||||
}) as FieldDecorator<R | null | undefined>;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ export function JsonType<T extends object>(
|
||||
typeFunction: () => ClassConstructor<T>
|
||||
): FieldDecorator<T | readonly T[] | null | undefined> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
propertyModel(context.metadata, String(context.name)).type = typeFunction;
|
||||
propertyModel(fieldMetadata(context), String(context.name)).type = typeFunction;
|
||||
}) as FieldDecorator<T | readonly T[] | null | undefined>;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ export function JsonPolymorphic<Base extends object = object>(
|
||||
onUnknown: options?.onUnknown ?? 'keep',
|
||||
...(options?.fallback ? { fallback: options.fallback as ClassConstructor<any> } : {}),
|
||||
};
|
||||
propertyModel(context.metadata, String(context.name)).polymorphic = info;
|
||||
propertyModel(fieldMetadata(context), String(context.name)).polymorphic = info;
|
||||
}) as FieldDecorator<Base | readonly Base[] | null | undefined>;
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export function JsonPolymorphic<Base extends object = object>(
|
||||
/** Skips every other rule on this field when the value is `null` or `undefined`. */
|
||||
export function IsOptional(): FieldDecorator<unknown> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
propertyModel(context.metadata, String(context.name)).optional = true;
|
||||
propertyModel(fieldMetadata(context), String(context.name)).optional = true;
|
||||
}) as FieldDecorator<unknown>;
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ export function IsOptional(): FieldDecorator<unknown> {
|
||||
*/
|
||||
export function ValidateIf<This>(condition: (object: This) => boolean): FieldDecorator<unknown> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
propertyModel(context.metadata, String(context.name)).condition = condition as (o: any) => boolean;
|
||||
propertyModel(fieldMetadata(context), String(context.name)).condition = condition as (o: any) => boolean;
|
||||
}) as FieldDecorator<unknown>;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ export function ValidateIf<This>(condition: (object: This) => boolean): FieldDec
|
||||
*/
|
||||
export function Allow(): FieldDecorator<unknown> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
propertyModel(context.metadata, String(context.name));
|
||||
propertyModel(fieldMetadata(context), String(context.name));
|
||||
}) as FieldDecorator<unknown>;
|
||||
}
|
||||
|
||||
@@ -244,10 +244,11 @@ export function Allow(): FieldDecorator<unknown> {
|
||||
*/
|
||||
export function ValidateNested(options?: ValidationOptions): FieldDecorator<object | readonly unknown[] | null | undefined> {
|
||||
return ((_t: undefined, context: ClassFieldDecoratorContext) => {
|
||||
const metadata = fieldMetadata(context);
|
||||
const property = String(context.name);
|
||||
propertyModel(context.metadata, property).nested = true;
|
||||
propertyModel(metadata, property).nested = true;
|
||||
if (options?.each) {
|
||||
addConstraint(context.metadata, property, {
|
||||
addConstraint(metadata, property, {
|
||||
name: 'nestedEach',
|
||||
validate: v => Array.isArray(v),
|
||||
message: `${property} must be an array`,
|
||||
|
||||
Reference in New Issue
Block a user