diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a1cfb4..18a1e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 -that must compile guards against the harness passing vacuously. +failing the build if a snippet the page calls a compile error ever compiles. Two more snippets +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 diff --git a/README.md b/README.md index 6717f65..855f05b 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,15 @@ than a `TypeError` from somewhere inside the engine. ### Toolchain support -Whether Cereale works at all depends on your compiler emitting standard decorators, so this -table is [checked by a test](src/toolchain.test.ts) rather than asserted here: +Whether Cereale works at all depends on your compiler emitting standard decorators, so the three +✅ 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 | | --- | --- | --- | | `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"` | | **oxc** | ❌ | Used by **Vite 8** and **Vitest 4** — see below | diff --git a/docs/index.html b/docs/index.html index 1808ef9..e09561e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -650,8 +650,11 @@ Break the cycle with @JsonIgnore() on the back-reference, or supply a

The toolchain cost, stated plainly

cereale reads the metadata that only a standard decorator transform emits, so - which compiler you use decides whether it works at all. This table is executed by a test on - every CI run, not asserted here. + which compiler you use decides whether it works at all. The three ✓ rows are executed by a + 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.

@@ -663,7 +666,7 @@ Break the cycle with @JsonIgnore() on the back-reference, or supply a tsc✓experimentalDecorators: false, target: ES2022+ - esbuild✓the same settings via tsconfigRaw + esbuild✓experimentalDecorators: false via tsconfigRaw, plus esbuild's own top-level target: es2022 — its default esnext leaves the decorators in place swc✓jsc.transform.decoratorVersion: "2022-03" oxc✗used by Vite 8 and Vitest 4 — see below @@ -680,16 +683,21 @@ Break the cycle with @JsonIgnore() on the back-reference, or supply a
vite.config.ts
-
import { standardDecorators } from 'cereale/vite';
+      
import { defineConfig } from 'vite';
+import { standardDecorators } from 'cereale/vite';
 
 export default defineConfig({
   plugins: [standardDecorators()],
 });

- It transforms with esbuild, falling back to the TypeScript compiler — cereale depends on - neither. Nothing in it is specific to cereale; it can be deleted once oxc implements the - transform. + It transforms .ts, .mts and + .cts outside node_modules with + esbuild, falling back to the TypeScript compiler — cereale depends on neither. Decorated + classes in .tsx need an include + 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.

diff --git a/docs/page.js b/docs/page.js index fad7bf8..af08448 100644 --- a/docs/page.js +++ b/docs/page.js @@ -168,7 +168,7 @@ ["@StartsWith(text)", 'must start with the prefix'], ["@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'], ["@NotEquals(value)", 'must not equal the value'], ["@IsIn(values)", 'must be one of the listed values'], @@ -185,15 +185,15 @@ ["@ArrayContains(values)", 'must contain all 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.', [ - ["@MinDate(() => date)", 'must not be earlier than the date'], - ["@MaxDate(() => date)", 'must not be later than the date'] + ['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 | (() => date))", 'must not be earlier than the date'], + ["@MaxDate(date | (() => date))", 'must not be later than the date'] ]], ['Custom rules', 'When the built-ins run out.', [ - ["@Validate(constraint, options?)", 'applies a custom validator class or function'], - ["defineRule(Class, field, rule)", 'registers a rule from outside a decorator'] + ["@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, 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'], ["fromJson(Class, json, options?)", 'JSON string → validated instance'], ["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'], ["unknownKeys: policy", 'allow (default), strip, or error'], ["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.', [ ["standardDecorators(options?)", "the Vite and Vitest plugin, from 'cereale/vite'"] diff --git a/src/toolchain.test.ts b/src/toolchain.test.ts index 9e44a0a..2ad55cb 100644 --- a/src/toolchain.test.ts +++ b/src/toolchain.test.ts @@ -105,6 +105,26 @@ describe('compilers that emit standard decorators', () => { const { Probe } = await load(await emit.swc(PROBE, STANDARD)); 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', () => {