📐 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
This commit is contained in:
+16
-2
@@ -97,8 +97,22 @@ at runtime so it cannot drift.
|
|||||||
|
|
||||||
The hero's compiler error is not typed into the HTML — `scripts/build-docs.mjs` compiles the
|
The hero's compiler error is not typed into the HTML — `scripts/build-docs.mjs` compiles the
|
||||||
snippet with the real `tsc` and writes the verbatim diagnostic into `docs/diagnostics.js`,
|
snippet with the real `tsc` and writes the verbatim diagnostic into `docs/diagnostics.js`,
|
||||||
failing the build if a snippet the page calls a compile error ever compiles. A third snippet
|
failing the build if a snippet the page calls a compile error ever compiles. Two more snippets
|
||||||
that must compile guards against the harness passing vacuously.
|
that must compile guard against the harness passing vacuously.
|
||||||
|
|
||||||
|
### Corrected
|
||||||
|
|
||||||
|
The README's toolchain table said esbuild takes "the same settings via `tsconfigRaw`". 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, so following that advice leaves decorator syntax in the output — the same silent
|
||||||
|
passthrough the section blames on oxc. Both the table and the landing page now say so, and
|
||||||
|
`src/toolchain.test.ts` asserts both halves, so the trap is documented by a test rather than by
|
||||||
|
a sentence.
|
||||||
|
|
||||||
|
Also corrected in the same pass: the toolchain table is described as executed by a test, but
|
||||||
|
the oxc row — the only ✗ — cannot be, because oxc ships inside a native binary with no
|
||||||
|
standalone transform API. The claim now covers the three rows it actually covers.
|
||||||
|
|
||||||
### Positioning
|
### Positioning
|
||||||
|
|
||||||
|
|||||||
@@ -95,13 +95,15 @@ than a `TypeError` from somewhere inside the engine.
|
|||||||
|
|
||||||
### Toolchain support
|
### Toolchain support
|
||||||
|
|
||||||
Whether Cereale works at all depends on your compiler emitting standard decorators, so this
|
Whether Cereale works at all depends on your compiler emitting standard decorators, so the three
|
||||||
table is [checked by a test](src/toolchain.test.ts) rather than asserted here:
|
✅ rows are [checked by a test](src/toolchain.test.ts) rather than asserted here — each compiles a
|
||||||
|
decorated class with that tool and asserts the metadata arrived. The ❌ row cannot be: oxc ships
|
||||||
|
inside a native binary with no standalone transform API.
|
||||||
|
|
||||||
| Transformer | Status | Notes |
|
| Transformer | Status | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `tsc` | ✅ | With `experimentalDecorators: false` and `target: ES2022`+ |
|
| `tsc` | ✅ | With `experimentalDecorators: false` and `target: ES2022`+ |
|
||||||
| esbuild | ✅ | Same settings via `tsconfigRaw` |
|
| esbuild | ✅ | `experimentalDecorators: false` via `tsconfigRaw`, **plus** esbuild's own top-level `target: es2022`. Its default `esnext` target leaves decorator syntax in the output |
|
||||||
| swc | ✅ | `jsc.transform.decoratorVersion: "2022-03"` |
|
| swc | ✅ | `jsc.transform.decoratorVersion: "2022-03"` |
|
||||||
| **oxc** | ❌ | Used by **Vite 8** and **Vitest 4** — see below |
|
| **oxc** | ❌ | Used by **Vite 8** and **Vitest 4** — see below |
|
||||||
|
|
||||||
|
|||||||
+15
-7
@@ -650,8 +650,11 @@ Break the cycle with @JsonIgnore() on the back-reference, or supply a
|
|||||||
<h2>The toolchain cost, stated plainly</h2>
|
<h2>The toolchain cost, stated plainly</h2>
|
||||||
<p class="lede">
|
<p class="lede">
|
||||||
cereale reads the metadata that only a <strong>standard</strong> decorator transform emits, so
|
cereale reads the metadata that only a <strong>standard</strong> decorator transform emits, so
|
||||||
which compiler you use decides whether it works at all. This table is executed by a test on
|
which compiler you use decides whether it works at all. The three ✓ rows are executed by a
|
||||||
every CI run, not asserted here.
|
test on every CI run rather than asserted here — each one compiles a decorated class with
|
||||||
|
that tool and checks the metadata arrived. The ✗ row cannot be: oxc ships inside a native
|
||||||
|
binary with no standalone transform API, so it was established by hand, and the plugin below
|
||||||
|
is what came of it.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -663,7 +666,7 @@ Break the cycle with @JsonIgnore() on the back-reference, or supply a
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td><code>tsc</code></td><td><span class="tick">✓</span></td><td class="note"><code>experimentalDecorators: false</code>, <code>target: ES2022</code>+</td></tr>
|
<tr><td><code>tsc</code></td><td><span class="tick">✓</span></td><td class="note"><code>experimentalDecorators: false</code>, <code>target: ES2022</code>+</td></tr>
|
||||||
<tr><td>esbuild</td><td><span class="tick">✓</span></td><td class="note">the same settings via <code>tsconfigRaw</code></td></tr>
|
<tr><td>esbuild</td><td><span class="tick">✓</span></td><td class="note"><code>experimentalDecorators: false</code> via <code>tsconfigRaw</code>, <em>plus</em> esbuild's own top-level <code>target: es2022</code> — its default <code>esnext</code> leaves the decorators in place</td></tr>
|
||||||
<tr><td>swc</td><td><span class="tick">✓</span></td><td class="note"><code>jsc.transform.decoratorVersion: "2022-03"</code></td></tr>
|
<tr><td>swc</td><td><span class="tick">✓</span></td><td class="note"><code>jsc.transform.decoratorVersion: "2022-03"</code></td></tr>
|
||||||
<tr><td>oxc</td><td><span class="cross">✗</span></td><td class="note">used by <strong>Vite 8</strong> and <strong>Vitest 4</strong> — see below</td></tr>
|
<tr><td>oxc</td><td><span class="cross">✗</span></td><td class="note">used by <strong>Vite 8</strong> and <strong>Vitest 4</strong> — see below</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -680,16 +683,21 @@ Break the cycle with @JsonIgnore() on the back-reference, or supply a
|
|||||||
|
|
||||||
<div class="code" style="margin-top:1.25rem;max-width:640px">
|
<div class="code" style="margin-top:1.25rem;max-width:640px">
|
||||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">vite.config.ts</span></div>
|
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">vite.config.ts</span></div>
|
||||||
<pre><code data-lang="ts">import { standardDecorators } from 'cereale/vite';
|
<pre><code data-lang="ts">import { defineConfig } from 'vite';
|
||||||
|
import { standardDecorators } from 'cereale/vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [standardDecorators()],
|
plugins: [standardDecorators()],
|
||||||
});</code></pre>
|
});</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p class="pg-note" style="max-width:var(--measure)">
|
<p class="pg-note" style="max-width:var(--measure)">
|
||||||
It transforms with esbuild, falling back to the TypeScript compiler — cereale depends on
|
It transforms <code class="inline-code">.ts</code>, <code class="inline-code">.mts</code> and
|
||||||
neither. Nothing in it is specific to cereale; it can be deleted once oxc implements the
|
<code class="inline-code">.cts</code> outside <code class="inline-code">node_modules</code> with
|
||||||
transform.
|
esbuild, falling back to the TypeScript compiler — cereale depends on neither. Decorated
|
||||||
|
classes in <code class="inline-code">.tsx</code> need an <code class="inline-code">include</code>
|
||||||
|
of your own; they are excluded by default because lowering them means also deciding what
|
||||||
|
happens to the JSX. Nothing in the plugin is specific to cereale; it can be deleted once oxc
|
||||||
|
implements the transform.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+10
-8
@@ -168,7 +168,7 @@
|
|||||||
["@StartsWith(text)", 'must start with the prefix'],
|
["@StartsWith(text)", 'must start with the prefix'],
|
||||||
["@EndsWith(text)", 'must end with the suffix']
|
["@EndsWith(text)", 'must end with the suffix']
|
||||||
]],
|
]],
|
||||||
['Equality and membership', 'Pinning a field to specific values. These narrow the field’s type too.', [
|
['Equality and membership', 'Pinning a field to specific values. These narrow the field’s type too — except @IsNotIn, which accepts any field, because narrowing a deny-list would be backwards.', [
|
||||||
["@Equals(value)", 'must equal the value'],
|
["@Equals(value)", 'must equal the value'],
|
||||||
["@NotEquals(value)", 'must not equal the value'],
|
["@NotEquals(value)", 'must not equal the value'],
|
||||||
["@IsIn(values)", 'must be one of the listed values'],
|
["@IsIn(values)", 'must be one of the listed values'],
|
||||||
@@ -185,15 +185,15 @@
|
|||||||
["@ArrayContains(values)", 'must contain all the listed values'],
|
["@ArrayContains(values)", 'must contain all the listed values'],
|
||||||
["@ArrayNotContains(values)", 'must not contain any of the listed values']
|
["@ArrayNotContains(values)", 'must not contain any of the listed values']
|
||||||
]],
|
]],
|
||||||
['Dates', 'Both take a thunk, so a moving boundary is evaluated per validation.', [
|
['Dates', 'Both take a Date, or a thunk so a moving boundary is evaluated per validation rather than frozen when the class was declared.', [
|
||||||
["@MinDate(() => date)", 'must not be earlier than the date'],
|
["@MinDate(date | (() => date))", 'must not be earlier than the date'],
|
||||||
["@MaxDate(() => date)", 'must not be later than the date']
|
["@MaxDate(date | (() => date))", 'must not be later than the date']
|
||||||
]],
|
]],
|
||||||
['Custom rules', 'When the built-ins run out.', [
|
['Custom rules', 'When the built-ins run out.', [
|
||||||
["@Validate(constraint, options?)", 'applies a custom validator class or function'],
|
["@Validate(validator, constraints?, options?)", 'applies a custom validator class or predicate; annotate the predicate\'s parameter to constrain the field type'],
|
||||||
["defineRule(Class, field, rule)", 'registers a rule from outside a decorator']
|
["defineRule(Class, field, rule, options?)", 'registers a rule from outside a decorator']
|
||||||
]],
|
]],
|
||||||
['Reading JSON', 'Every one of these has a …Sync twin that needs no await.', [
|
['Reading JSON', 'Each of these has a …Sync twin that needs no await, except fromRequest.', [
|
||||||
["toInstance(Class, plain, options?)", 'plain object → validated instance'],
|
["toInstance(Class, plain, options?)", 'plain object → validated instance'],
|
||||||
["fromJson(Class, json, options?)", 'JSON string → validated instance'],
|
["fromJson(Class, json, options?)", 'JSON string → validated instance'],
|
||||||
["toInstanceArray(Class, plain, options?)", 'array of plain objects → instances'],
|
["toInstanceArray(Class, plain, options?)", 'array of plain objects → instances'],
|
||||||
@@ -220,7 +220,9 @@
|
|||||||
["namingStrategy: strategy", 'identity (default), camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, or your own function'],
|
["namingStrategy: strategy", 'identity (default), camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, or your own function'],
|
||||||
["unknownKeys: policy", 'allow (default), strip, or error'],
|
["unknownKeys: policy", 'allow (default), strip, or error'],
|
||||||
["maxDepth: number", 'nesting limit before a JsonMappingError — default 64'],
|
["maxDepth: number", 'nesting limit before a JsonMappingError — default 64'],
|
||||||
["configure(options)", 'sets the library-wide defaults']
|
["configure(options)", 'sets the library-wide defaults'],
|
||||||
|
["getConfig()", 'reads the defaults currently in force'],
|
||||||
|
["resetConfig()", 'restores the built-in defaults — the one a test suite needs']
|
||||||
]],
|
]],
|
||||||
['Build', 'Only needed on toolchains that transform with oxc.', [
|
['Build', 'Only needed on toolchains that transform with oxc.', [
|
||||||
["standardDecorators(options?)", "the Vite and Vitest plugin, from 'cereale/vite'"]
|
["standardDecorators(options?)", "the Vite and Vitest plugin, from 'cereale/vite'"]
|
||||||
|
|||||||
@@ -105,6 +105,26 @@ describe('compilers that emit standard decorators', () => {
|
|||||||
const { Probe } = await load(await emit.swc(PROBE, STANDARD));
|
const { Probe } = await load(await emit.swc(PROBE, STANDARD));
|
||||||
expect(Object.keys(modelOf(Probe))).toEqual(['name']);
|
expect(Object.keys(modelOf(Probe))).toEqual(['name']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// so the natural-looking "put the tsconfig settings in tsconfigRaw" configuration leaves the
|
||||||
|
// decorator syntax in the output — the same silent passthrough oxc produces. Documented here
|
||||||
|
// because the README and the landing page both tell people how to configure esbuild.
|
||||||
|
it('needs esbuild’s own target, not one inside tsconfigRaw', async () => {
|
||||||
|
const withoutTarget = await transform(PROBE, {
|
||||||
|
loader: 'ts',
|
||||||
|
tsconfigRaw: { compilerOptions: { experimentalDecorators: false, target: 'es2022' } },
|
||||||
|
});
|
||||||
|
expect(withoutTarget.code, 'expected the decorator to survive untransformed').toMatch(/@Rule\(\)/);
|
||||||
|
|
||||||
|
const withTarget = await transform(PROBE, {
|
||||||
|
loader: 'ts',
|
||||||
|
target: 'es2022',
|
||||||
|
tsconfigRaw: { compilerOptions: { experimentalDecorators: false, useDefineForClassFields: true } },
|
||||||
|
});
|
||||||
|
expect(withTarget.code).not.toMatch(/@Rule\(\)/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('compilers configured for legacy decorators', () => {
|
describe('compilers configured for legacy decorators', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user