a2deaffe0b849c27ca04f2a3d48fbbc2cdea9c60
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b4f0657d09 |
📦 feat: add cereale/min, and a verified framework guide
**cereale/min** — the library flattened into one minified ES module, 25.5 KB / 8.6 KB gzipped, for import maps, <script type="module">, Deno and Workers. Built from dist/esm/index.js, so the decorator lowering and the ES2025 target are tsc's; esbuild only flattens and minifies. It is an addition rather than a replacement, and the measurement is the reason. Bundled through esbuild the flat and per-module builds land within 2 bytes of each other; through rollup + terser the flat one is 165 bytes smaller; unused decorators tree-shake out of both. With the size argument a wash, per-module stays the default import for the one thing it does better — readable stack traces without source maps. (My first pass at that measurement reported "shaken" for every symbol because both rollup builds had failed and grep was reading missing files as absence. The check now asserts the bundle is non-empty and that a *used* symbol is present, so it can tell a real result from a broken harness.) **FRAMEWORKS.md** — a recipe per framework, each one run before it was written, with the versions and date verified against. Angular works, which was not obvious: the CLI scaffolds experimentalDecorators: true, but ngtsc erases @Component and @Injectable into static properties rather than leaning on TypeScript's decorator emit. Flip the flag and both systems coexist. Verified with ngc on Angular 21.2 with strictTemplates — templates still type-check and a wrong cereale rule is still TS1240 inside the Angular build. Next.js cannot work inline, structurally: it derives both the SWC parser's decorator support and the transform mode from the one flag, so on gives legacy emit and off makes @ a syntax error. NestJS cannot either — its DI needs design:type from emitDecoratorMetadata. Both have the same answer: keep the cereale classes in a package compiled by tsc and import the built output. Verified inside a program with BOTH legacy flags on, alongside @Injectable() — mapping and validation work, and the compile-time guarantee still holds where the rules are written. Also verified: Bun 1.3 needs no configuration, and a real Vite 8 build with the plugin works where the same build without it silently leaves decorator syntax in the bundle. Version 0.4.0: cereale/min is a new public entry point, and cutting a minor keeps the existing v0.3.0 tag meaningful instead of force-moving it onto a commit it was never cut from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK |
||
|
|
fd42675d3d |
🔗 docs: link the live site, now that Pages is confirmed
Pages serves docs/ from main, so the page rebuilt in #6 is live at avalon-vanguard.github.io/cereale. The README pointed at the local file because the URL could not be verified from here; it now links the site and keeps the local instructions as the fallback. package.json homepage moves there too — npm renders it as the package's headline link, and a live playground is a better landing spot than an anchor inside the README. Adds canonical and Open Graph tags. No og:image: a preview card with a broken image is worse than one without, and there is no artwork yet. check-docs.mjs flagged the canonical link as a remote subresource, which it is not — the browser never fetches it. Rather than exempt the URL, the check now looks at rel and only flags the relations that actually fetch or connect. Verified it still catches a CDN stylesheet, a preconnect and a script src; a check that cannot tell a declaration from a request is one that gets switched off the first time it is wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK |
||
|
|
478f852b42 |
🔒 fix!: refuse names that no longer reach their property
A rename did not actually take effect. @JsonProperty stopped the old name
from being *mapped*, but not from being *accepted*: unlike @JsonReadOnly,
whose JSON name goes into the blocked set, the old key fell through to the
unknown-key policy, and the default `allow` copied it onto the instance
untouched.
The value therefore landed on a declared property having skipped everything
declared for it:
@JsonProperty('home_address') @JsonType(() => Addr) @ValidateNested()
address!: Addr;
toInstanceSync(Order, { address: { city: 'Paris' } })
-> address is a plain object, instanceof Addr === false
-> validateSync() returns [] <- nothing complains
A payload aimed at the previous version of a class was accepted in part, in
silence. Three routes led to the same hole, and all three are now closed:
- the property key of a field renamed with @JsonProperty
- the raw key of a field a naming strategy renders differently
(`firstName` under snake_case)
- the property key of a field that is both renamed and @JsonReadOnly, which
was still settable under its own key
Refused, not swallowed. A stale name is a mismatch with whatever produced
the payload, not a deliberate refusal like @JsonReadOnly, so it is kept in
its own map rather than lumped into `blocked`: under unknownKeys: 'error'
it is still reported, and the report now names the property it was reaching
for and what that property is called now.
"ref" is not a JSON name for Order: property "ref" is mapped to
"order_ref". Send that name, or add @JsonAlias("ref") to keep
accepting this one.
@JsonAlias still keeps an old name working, and a key that some *other*
property legitimately answers to is still mapped to that property — both
asserted. The resolution happens once when the name map is built, which is
memoized per class and naming strategy, so deserialization is unchanged at
~45µs for 50 nested orders.
The behaviour this replaces was pinned by tests two commits ago, pending
this decision; those tests now assert the fix, and six more cover the
naming-strategy, read-only, alias and key-collision cases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
|
||
|
|
057f008ac0 |
🔍 fix: correct the last three findings from the page review
Of 26 findings raised across five auditors, 23 were refuted on a second
pass. These three survived.
**A renamed property's old key is still writable.** The README, the landing
page and two doc comments all said that once a property carries
@JsonProperty its original name "is no longer accepted on input". It is no
longer *mapped* — but it is not rejected either. Unlike @JsonReadOnly,
whose JSON name goes into the blocked set, the old key falls through to the
unknown-key policy, and the default `allow` copies it onto the instance
untouched. Reproduced against dist:
@JsonProperty('home_address') @JsonType(() => Addr) @ValidateNested()
address!: Addr;
toInstanceSync(Order, { address: { city: 'Paris' } })
-> address is a plain object, instanceof Addr === false
-> validateSync() returns [] <- nothing complains
-> round-trips out as home_address <- silently accepted
Blocking the old key would fix it, but would also swallow the
`unknownKeys: 'error'` report a strict caller gets today, which is arguably
the more useful signal. That is a judgement call the library has not made,
so this commit states the behaviour accurately everywhere it was stated
wrongly and pins it with five tests covering the default, `strip`, `error`
and the @JsonAlias fix — so it cannot drift either way while the question
is open.
**@IsNotEmpty and @IsEmpty are not complements.** `[]` and `{}` pass BOTH:
isNotEmpty checks only null/undefined/'' while isEmpty also treats empty
arrays and objects as empty. Listed one line apart as "must not be empty" /
"must be empty", they invited exactly the wrong inference.
**unknownKeys is deserialization-only**, in a group whose blurb says these
apply per call or via configure().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
|
||
|
|
8c9aea3062 |
📐 fix: correct what the adversarial review of the page found
Five auditors read the finished page against the source; a second pass tried to refute each finding. What survived: **The esbuild row was wrong, and dangerously so.** It said esbuild takes "the same settings via tsconfigRaw" as tsc. It does not: esbuild lowers standard decorators only when its own *top-level* `target` is below `esnext`. A `target` inside `tsconfigRaw` sets the `useDefineForClassFields` default and nothing else. I ran it — the decorator survives verbatim and the module throws SyntaxError on import, which is the exact silent passthrough the section blames on oxc. The repo's own vite plugin and toolchain test always passed `target` top-level, so the executed matrix never backed the advice the docs gave. Both halves are now asserted in src/toolchain.test.ts. **"This table is executed by a test" did not cover the oxc row** — the only ✗, and the row the whole section is built around. It cannot be: oxc ships as a native binary with no standalone transform API, which the test file already said in a comment. Fixed on the page and in the README. Reference corrections, each verified against the source: - fromRequest has no …Sync twin; the group blurb claimed every entry did - @IsNotIn does not narrow its field, unlike its five neighbours - @MinDate/@MaxDate take a Date as well as a thunk - @Validate has three parameters, not two; defineRule has four - getConfig() and resetConfig() were missing from a group rendered under the heading "Everything cereale exports" And on the page itself: the vite.config.ts snippet never imported defineConfig, so pasting it failed; and the plugin note omitted that .tsx is excluded by default, which would drop a reader straight back into the 0-test hole the section exists to describe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK |
||
|
|
c828f5cfe9 |
🌾 feat: rebuild the landing page, and stop it from rotting again
The old page had been quietly broken for some time. It loaded
@babel/standalone from an **unpinned** CDN URL, which rolled over to Babel
8 and dropped the `proposal-class-properties` plugin the page asked for, so
Babel.transform threw before it ever reached the decorators — and the
decorator config it passed was `{ legacy: true }`, which 0.2.0 had already
made wrong. Nothing on the page said so. The copy was still selling the
0.1.0 pitch ("Spring-like"), listed about half the decorators, showed
`npm install cereale` for a package the registry returns 404 for, and
claimed "Zero overhead" against a README that publishes the real
microsecond costs.
The rebuild is one self-contained page: hand-written CSS, no Tailwind CDN,
no CodeMirror, and a vendored compiler pinned by package.json. It loads
nothing from the network. The playground runs the real bundled library
across six examples, all verified in a headless browser. The reference
covers all 68 decorators and the full API, counted from the bundle at
runtime so it cannot drift.
The hero's compiler error is not typed into the HTML. scripts/build-docs.mjs
compiles the snippets with the real tsc and writes the verbatim diagnostics
into docs/diagnostics.js, failing the build if a snippet the page calls a
compile error ever compiles — and two snippets that must compile guard
against the harness passing vacuously.
Three guards keep it honest, all wired into CI:
- check:docs fails on any remote subresource
- build:docs + git diff fails if docs/ is stale against src/
- check:types compiles a consumer against dist/ with no DOM lib, no
@types/node and no skipLibCheck
That last one found a real packaging defect: `fromRequest` was declared as
taking the global `Request`, so cereale's own published .d.ts raised
"Cannot find name 'Request'" in any project whose lib and types did not
happen to supply it — inside a dependency, in code they may never call, and
unfixable from the outside. It now takes a structural JsonBody, which a
Request still satisfies. The library's own type tests had been hiding it by
enabling both DOM and skipLibCheck.
An adversarial review of the finished page caught four more: the lede
claimed *every* rule is type-checked (@IsDefined and @IsNotIn deliberately
are not), the guarantee section was wrong about the mechanism (a legacy
decorator does get design:type under emitDecoratorMetadata — the real claim
is about its type signature), one sample called a Movie method on a Media[]
and did not compile, and "nested objects come back as real classes" omitted
that you have to declare them. WCAG contrast was measured rather than
eyeballed: seven real failures fixed in the two themes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
|
||
|
|
83ad2a289d |
📝 docs: bring README, demo and playground in line with the code; release 0.1.0
The README described a library that did not exist in places. It showed
@ValidateNested({ each: true }), which did not compile; told users to enable
emitDecoratorMetadata, which the library never reads; and documented none of the
mapping API. Its Quick Start now runs verbatim — verified by compiling and
executing it against the local source.
- README: document field-name mapping, access control, options, error helpers
and the 30 new validators; drop the emitDecoratorMetadata instruction; add a
Notes and Limitations section covering circular references, validate() on
plain objects, and the fact that @JsonProperty stops the original name from
being accepted unless you add @JsonAlias
- CHANGELOG.md: new, covering 0.1.0
- example.ts: rewritten as a tour of the current API — read-only ids, write-only
secrets, renamed fields, conditional validation, flattened errors, and a
base-class rule reaching a subclass
- docs: the playground hand-listed its symbol table in three parallel places and
exposed IsEmail, which is not an export. It now derives scope from the bundle,
so new decorators work there as soon as they ship. Bundle regenerated
- version 0.1.0
136 tests, 97% statement and 100% function coverage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
|
||
|
|
fa75147e4e |
✨ feat: implement core Cereale library for JSON mapping and validation
- 🎨 add Spring-like decorators (@JsonSerialize, @JsonDeserialize, etc.) - ⚙️ implement JsonMapper and metadata storage for transformations - 🧪 add comprehensive test suite using Vitest - 📝 add README, CONTRIBUTING, and API documentation - 👷 setup GitHub Actions CI workflow - 🔧 configure TypeScript and project settings |
||
|
|
7fe7e8c7c3 |
✨ feat: implement core Optimus library for JSON mapping and validation
- 🎨 add Spring-like decorators (@JsonSerialize, @JsonDeserialize, etc.) - ⚙️ implement JsonMapper and metadata storage for transformations - 🧪 add comprehensive test suite using Vitest - 📝 add README, CONTRIBUTING, and API documentation - 👷 setup GitHub Actions CI workflow - 🔧 configure TypeScript and project settings |