@zipbul/baker 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @zipbul/baker
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 8ea7162: Remove the global registration API in favor of the `Baker` class. `new Baker(config?)`
8
+ is now the only way to register and seal DTOs: use `@app.Recipe` to register a class and
9
+ `app.seal()` to seal it. The global `@Recipe`, `seal()`, `configure()`, and the `createBaker()`
10
+ factory have been removed — each `Baker` instance owns its own isolated registry and config, so
11
+ multiple apps in one process never mix. `@Field`, the rule/transformer factories, and
12
+ `deserialize`/`validate`/`serialize` are unchanged.
13
+
14
+ Migration: replace `configure(opts)` + global `@Recipe`/`seal()` with
15
+ `const app = new Baker(opts); @app.Recipe class Dto {}; app.seal();`.
16
+
3
17
  ## 4.0.0
4
18
 
5
19
  ### Major Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zipbul/baker
2
2
 
3
- The fastest decorator-based DTO validation library for TypeScript. baker generates optimized validation and serialization code once at `seal()` time, then reuses the sealed executors on every call.
3
+ The fastest decorator-based DTO validation library for TypeScript. baker generates optimized validation and serialization code once at seal time, then reuses the sealed executors on every call.
4
4
 
5
5
  ```bash
6
6
  bun add @zipbul/baker
@@ -27,18 +27,20 @@ Zero `reflect-metadata`. Sealed codegen.
27
27
  ## Quick Start
28
28
 
29
29
  ```typescript
30
- import { deserialize, isBakerIssueSet, Field, Recipe, seal } from '@zipbul/baker';
30
+ import { Baker, Field, deserialize, isBakerIssueSet } from '@zipbul/baker';
31
31
  import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
32
32
 
33
- @Recipe
33
+ const baker = new Baker();
34
+
35
+ @baker.Recipe
34
36
  class UserDto {
35
37
  @Field(isString, minLength(2)) name!: string;
36
38
  @Field(isNumber(), min(0)) age!: number;
37
39
  @Field(isString, isEmail()) email!: string;
38
40
  }
39
41
 
40
- // Call once at app startup, after every DTO has been imported.
41
- seal();
42
+ // Call once at startup, after this baker's DTOs are defined.
43
+ baker.seal();
42
44
 
43
45
  // All rules here are sync, so deserialize returns the value directly (no await).
44
46
  const result = deserialize(UserDto, {
@@ -61,10 +63,13 @@ if (isBakerIssueSet(result)) {
61
63
 
62
64
  | Concept | What it does |
63
65
  | ------------------- | ------------------------------------------------------------------------------ |
64
- | `@Recipe` | Marks a class as a DTO and registers it with baker. |
65
- | `@Field(...rules)` | Declares a validated field. Only `@Field` properties are part of the contract. |
66
- | `seal()` | Compiles every registered DTO into executor functions. Call once, at startup. |
67
- | `deserialize` / `validate` / `serialize` | Run the sealed executors: parse+validate, validate-only, or emit a plain object. |
66
+ | `new Baker(config?)` | An isolated registration + seal scope. Multiple bakers never mix. Use `@app.Recipe` and `app.seal()`. |
67
+ | `@app.Recipe` | Marks a class as a DTO of that baker. Only `@Field` properties are part of the contract. |
68
+ | `@Field(...rules)` | Declares a validated field. Global works with any baker. |
69
+ | `app.seal()` | Compiles that baker's DTOs into executor functions. Call once, at startup. |
70
+ | `deserialize` / `validate` / `serialize` | Global — run the sealed executors stored on the class: parse+validate, validate-only, or emit a plain object. |
71
+
72
+ > Examples below assume a `const baker = new Baker()` in scope and a single `baker.seal()` after the DTOs are defined.
68
73
 
69
74
  ## Why baker?
70
75
 
@@ -122,7 +127,7 @@ Most fields need only rules. The options below cover nested, conditional, collec
122
127
  ### Conditional fields & custom messages
123
128
 
124
129
  ```typescript
125
- @Recipe
130
+ @baker.Recipe
126
131
  class UserDto {
127
132
  @Field(isString) name!: string;
128
133
 
@@ -250,7 +255,7 @@ email!: string;
250
255
  import { luxonTransformer } from '@zipbul/baker/transformers';
251
256
  const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });
252
257
 
253
- @Recipe
258
+ @baker.Recipe
254
259
  class EventDto {
255
260
  @Field({ transform: luxon }) startAt!: DateTime;
256
261
  }
@@ -269,12 +274,12 @@ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
269
274
  ### Nested DTOs
270
275
 
271
276
  ```typescript
272
- @Recipe
277
+ @baker.Recipe
273
278
  class AddressDto {
274
279
  @Field(isString) city!: string;
275
280
  }
276
281
 
277
- @Recipe
282
+ @baker.Recipe
278
283
  class UserDto {
279
284
  @Field({ type: () => AddressDto }) address!: AddressDto;
280
285
  @Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
@@ -284,7 +289,7 @@ class UserDto {
284
289
  ### Collections
285
290
 
286
291
  ```typescript
287
- @Recipe
292
+ @baker.Recipe
288
293
  class UserDto {
289
294
  @Field({ type: () => Set, setValue: () => TagDto }) tags!: Set<TagDto>;
290
295
  @Field({ type: () => Map, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
@@ -296,7 +301,7 @@ class UserDto {
296
301
  ### Discriminator
297
302
 
298
303
  ```typescript
299
- @Recipe
304
+ @baker.Recipe
300
305
  class PetOwner {
301
306
  @Field({
302
307
  type: () => CatDto,
@@ -315,12 +320,12 @@ class PetOwner {
315
320
  ### Inheritance
316
321
 
317
322
  ```typescript
318
- @Recipe
323
+ @baker.Recipe
319
324
  class BaseDto {
320
325
  @Field(isString) id!: string;
321
326
  }
322
327
 
323
- @Recipe
328
+ @baker.Recipe
324
329
  class UserDto extends BaseDto {
325
330
  @Field(isString) name!: string;
326
331
  // inherits 'id' field with isString rule
@@ -329,11 +334,26 @@ class UserDto extends BaseDto {
329
334
 
330
335
  ## Runtime API
331
336
 
332
- ### `seal()`
337
+ ### `new Baker(config?)`
338
+
339
+ A `Baker` is an isolated registration + seal scope. Construct one per app/library; multiple bakers in one process never mix.
340
+
341
+ - `@app.Recipe` — class decorator; registers the class as one of this baker's DTOs.
342
+ - `app.seal()` — **required.** Compiles the baker's DTOs (and any nested DTOs they reach) into executor functions. Call once at startup, after the baker's DTOs are defined. Idempotent.
343
+ - Config is passed to the constructor:
344
+
345
+ ```typescript
346
+ const app = new Baker({
347
+ autoConvert: true, // coerce "123" → 123
348
+ allowClassDefaults: true, // use class field initializers for missing keys
349
+ stopAtFirstError: true, // return on first validation failure
350
+ forbidUnknown: true, // reject undeclared fields
351
+ });
352
+ ```
333
353
 
334
- **Required.** Call once at app startup, after every DTO module has been imported. Takes no argumentsseals every class registered via `@Recipe` so far, plus any nested DTOs they reach. Idempotent: a second call is a no-op.
354
+ `deserialize` / `serialize` / `validate` are global they read the sealed executor off the classand throw `BakerError` if the class is not sealed.
335
355
 
336
- `deserialize` / `serialize` / `validate` throw `BakerError` if the DTO is not sealed.
356
+ **Isolation:** distinct classes are fully isolated (each sealed with its baker's config). A class shared across bakers is sealed once (first seal wins) — use separate classes if you need different config.
337
357
 
338
358
  ### `deserialize` / `serialize` / `validate`
339
359
 
@@ -368,19 +388,6 @@ interface RuntimeOptions {
368
388
 
369
389
  Groups are passed at call time (not on `@Field`) because the active set typically varies per request.
370
390
 
371
- ### `configure(config)`
372
-
373
- Global configuration. Must be called **before** `seal()`. After seal, `configure(...)` throws `BakerError`.
374
-
375
- ```typescript
376
- configure({
377
- autoConvert: true, // coerce "123" → 123
378
- allowClassDefaults: true, // use class field initializers for missing keys
379
- stopAtFirstError: true, // return on first validation failure
380
- forbidUnknown: true, // reject undeclared fields
381
- });
382
- ```
383
-
384
391
  ### `createRule(name, validate)` / `createRule(options)`
385
392
 
386
393
  Custom validation rule. Two forms — a `(name, validate)` shorthand or an options object:
@@ -390,10 +397,12 @@ const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as strin
390
397
  ```
391
398
 
392
399
  ```typescript
400
+ import { RequiredType } from '@zipbul/baker';
401
+
393
402
  const isEven = createRule({
394
403
  name: 'isEven',
395
404
  validate: v => typeof v === 'number' && v % 2 === 0,
396
- requiresType: 'number',
405
+ requiresType: RequiredType.Number,
397
406
  });
398
407
  ```
399
408
 
@@ -405,7 +414,7 @@ Type guard. Narrows a result to `BakerIssueSet`, whose `errors` array holds `{ p
405
414
 
406
415
  baker separates two failure modes:
407
416
 
408
- - **`BakerError` (thrown)** — a programming mistake: using a DTO before `seal()`, passing a raw rule function, calling `configure()` after seal, or calling a strict `*Sync` variant on an async DTO. Fix the code; don't catch it in request handlers.
417
+ - **`BakerError` (thrown)** — a programming mistake: using a DTO before `app.seal()`, passing a raw rule function, an unknown config key, or calling a strict `*Sync` variant on an async DTO. Fix the code; don't catch it in request handlers.
409
418
  - **`BakerIssueSet` (returned)** — a validation failure. `deserialize` and `validate` return it instead of throwing. Guard with `isBakerIssueSet` and read `.errors`.
410
419
 
411
420
  ```typescript
@@ -440,7 +449,7 @@ Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `deserialize()
440
449
 
441
450
  ### How does the AOT code generation work?
442
451
 
443
- Calling `seal()` once at app startup walks every registered DTO, analyzes field metadata, generates optimized JavaScript executor functions, and caches them. Subsequent `deserialize` / `serialize` / `validate` calls run the pre-compiled functions directly. There is no auto-seal — forgetting to call `seal()` raises `BakerError` on first use.
452
+ Calling `app.seal()` once at startup walks the baker's DTOs (and their nested DTOs), analyzes field metadata, generates optimized JavaScript executor functions, and stores them on each class. Subsequent `deserialize` / `serialize` / `validate` calls run the pre-compiled functions directly. There is no auto-seal — using a DTO before `app.seal()` raises `BakerError`.
444
453
 
445
454
  > baker builds its executors with `new Function()`. Under a strict Content-Security-Policy this requires `'unsafe-eval'`; baker will not run in environments that forbid runtime code generation.
446
455
 
@@ -448,13 +457,13 @@ Calling `seal()` once at app startup walks every registered DTO, analyzes field
448
457
 
449
458
  ```typescript
450
459
  import {
451
- seal,
460
+ Baker,
452
461
  deserialize, deserializeSync, deserializeAsync,
453
462
  validate, validateSync, validateAsync,
454
463
  serialize, serializeSync, serializeAsync,
455
- configure, createRule, Field, Recipe, arrayOf, isBakerIssueSet, BakerError,
464
+ createRule, Field, arrayOf, isBakerIssueSet, BakerError, RequiredType, ExcludeMode,
456
465
  } from '@zipbul/baker';
457
- import type { Transformer, TransformParams, BakerIssue, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
466
+ import type { Transformer, TransformParams, BakerIssue, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions, BakerConfig } from '@zipbul/baker';
458
467
  import { isString, isEmail, isULID, isCUID2 /* …114 rules */ } from '@zipbul/baker/rules';
459
468
  import { trimTransformer, jsonTransformer /* …and more */ } from '@zipbul/baker/transformers';
460
469
  ```
package/dist/index.d.ts CHANGED
@@ -1,13 +1,10 @@
1
1
  export { deserialize, deserializeSync, deserializeAsync } from './src/functions/deserialize';
2
2
  export { validate, validateSync, validateAsync } from './src/functions/validate';
3
3
  export { serialize, serializeSync, serializeAsync } from './src/functions/serialize';
4
- export { configure } from './src/configure';
5
4
  export { createRule } from './src/create-rule';
6
- export { seal } from './src/seal/seal';
7
- export { Field, arrayOf, Recipe } from './src/decorators/index';
5
+ export { Field, arrayOf } from './src/decorators/index';
8
6
  export type { FieldOptions, ArrayOfMarker } from './src/decorators/index';
9
- export { createBaker } from './src/baker';
10
- export type { Baker } from './src/baker';
7
+ export { Baker } from './src/baker';
11
8
  export { ExcludeMode, RequiredType } from './src/enums';
12
9
  export type { BakerIssue, BakerIssueSet } from './src/errors';
13
10
  export { isBakerIssueSet, BakerError } from './src/errors';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export{deserialize,deserializeSync,deserializeAsync}from"./src/functions/deserialize.js";export{validate,validateSync,validateAsync}from"./src/functions/validate.js";export{serialize,serializeSync,serializeAsync}from"./src/functions/serialize.js";export{configure}from"./src/configure.js";export{createRule}from"./src/create-rule.js";export{seal}from"./src/seal/seal.js";export{Field,arrayOf,Recipe}from"./src/decorators/index.js";export{createBaker}from"./src/baker.js";export{ExcludeMode,RequiredType}from"./src/enums.js";export{isBakerIssueSet,BakerError}from"./src/errors.js";
1
+ export{deserialize,deserializeSync,deserializeAsync}from"./src/functions/deserialize.js";export{validate,validateSync,validateAsync}from"./src/functions/validate.js";export{serialize,serializeSync,serializeAsync}from"./src/functions/serialize.js";export{createRule}from"./src/create-rule.js";export{Field,arrayOf}from"./src/decorators/index.js";export{Baker}from"./src/baker.js";export{ExcludeMode,RequiredType}from"./src/enums.js";export{isBakerIssueSet,BakerError}from"./src/errors.js";
@@ -1,26 +1,31 @@
1
1
  import type { BakerConfig } from './configure';
2
2
  /**
3
- * A baker scope — an isolated registration + seal boundary. Each `createBaker()` owns its own
4
- * registry and config; classes sealed through it are attributed to it, so separate scopes never
5
- * mix. `@Field`, rules, transformers, and `deserialize/serialize/validate` stay global (they read
6
- * the metadata/executor stored on the class), so only the class-collecting `Recipe` and `seal` are
7
- * scoped.
8
- */
9
- export interface Baker {
10
- /** Class decorator — registers the class as a root of THIS scope. Use as `@app.Recipe`. */
11
- readonly Recipe: (value: Function, context: ClassDecoratorContext) => void;
12
- /** Seal every root registered to this scope (and its nested DTOs) with this scope's config. */
13
- readonly seal: () => void;
14
- }
15
- /**
16
- * Create an isolated baker scope. Use for libraries and multi-app processes where each app must
17
- * not mix with another. Single-app code can keep using the global `@Recipe` / `seal()` / `configure()`.
3
+ * A baker — an isolated registration + seal boundary. Each `new Baker()` owns its own registry and
4
+ * config, so multiple bakers in one process (or a bundler-duplicated copy of the library) never
5
+ * fragment each other. `@Field`, the rule factories, and `deserialize`/`serialize`/`validate` stay
6
+ * global — they read the metadata/executor stored on the class itself — so only the class-collecting
7
+ * `Recipe` and `seal` belong to the instance.
18
8
  *
19
9
  * ```ts
20
- * const app = createBaker({ autoConvert: true });
10
+ * const app = new Baker({ autoConvert: true });
21
11
  * @app.Recipe class UserDto { @Field(isString) name!: string }
22
12
  * app.seal();
23
- * deserialize(UserDto, input); // global — reads UserDto's sealed executor
13
+ * deserialize(UserDto, input);
24
14
  * ```
15
+ *
16
+ * Isolation boundary is class identity: distinct classes are fully isolated (each sealed with its
17
+ * baker's config); a class shared across bakers is reused as one sealed form.
18
+ *
19
+ * `Recipe` and `seal` are arrow-field properties, not prototype methods, by design: `@app.Recipe`
20
+ * is applied as a detached value (the runtime calls the decorator with no `this` receiver), so it
21
+ * must be bound to the instance; arrow fields also keep `const { Recipe, seal } = new Baker()`
22
+ * working.
25
23
  */
26
- export declare function createBaker(config?: BakerConfig): Baker;
24
+ export declare class Baker {
25
+ #private;
26
+ constructor(config?: BakerConfig);
27
+ /** Class decorator — registers the class as a root of this baker. Use as `@app.Recipe`. */
28
+ readonly Recipe: (value: Function, _context: ClassDecoratorContext) => void;
29
+ /** Seal every root registered to this baker (and its nested DTOs) with this baker's config. */
30
+ readonly seal: () => void;
31
+ }
package/dist/src/baker.js CHANGED
@@ -1 +1 @@
1
- import{normalizeConfig as H}from"./configure.js";import{sealRegistry as I}from"./seal/seal.js";export function createBaker(j){const q=new Set,E=j===void 0?Object.freeze({}):H(j);let A=!1;return{Recipe(G){q.add(G)},seal(){if(A)return;I(q,E);A=!0}}}
1
+ export class Baker{#j=new Set;#q;#x=!1;constructor(j){this.#q=j===void 0?Object.freeze({}):q(j)}Recipe=(j,A)=>{this.#j.add(j)};seal=()=>{if(this.#x)return;x(this.#j,this.#q);this.#x=!0}}import{normalizeConfig as q}from"./configure.js";import{sealRegistry as x}from"./seal/seal.js";
@@ -12,19 +12,8 @@ interface BakerConfig {
12
12
  debug?: boolean;
13
13
  }
14
14
  /**
15
- * Baker global configuration. Call before `seal()`.
16
- * If not called, defaults are applied.
17
- */
18
- declare function configure(config: BakerConfig): void;
19
- /**
20
- * Validate a BakerConfig and map it to the internal SealOptions. Shared by `configure()`
21
- * (default instance) and `createBaker()` (per-scope instances). Does NOT check seal state —
22
- * that gate is specific to the global `configure()`.
15
+ * Validate a BakerConfig and map it to the internal SealOptions. Used by `new Baker(config)`.
23
16
  */
24
17
  declare function normalizeConfig(config: BakerConfig): SealOptions;
25
- /** @internal used by seal. Returns the frozen global options; the only way to change them is configure(). */
26
- declare function getGlobalOptions(): SealOptions;
27
- /** @internal — reset to defaults on unseal */
28
- declare function resetConfigForTesting(): void;
29
- export { configure, getGlobalOptions, resetConfigForTesting, normalizeConfig };
18
+ export { normalizeConfig };
30
19
  export type { BakerConfig };
@@ -1 +1 @@
1
- import{BakerError as j}from"./errors.js";import{isSealed as x}from"./seal/seal-state.js";const w=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug"]);let q=Object.freeze({});function configure(h){if(x())throw new j("[baker] configure() called after seal(). Already-sealed classes are not affected. Call configure() before seal().");q=normalizeConfig(h)}function normalizeConfig(h){if(h===null||typeof h!=="object"||Array.isArray(h))throw new j(`[baker] config requires a plain object. Received: ${h===null?"null":Array.isArray(h)?"array":typeof h}.`);for(const v of Object.keys(h))if(!w.has(v))throw new j(`[baker] unknown key '${v}'. Valid keys: ${[...w].join(", ")}.`);return Object.freeze({enableImplicitConversion:h.autoConvert??!1,exposeDefaultValues:h.allowClassDefaults??!1,stopAtFirstError:h.stopAtFirstError??!1,whitelist:h.forbidUnknown??!1,debug:h.debug??!1})}function getGlobalOptions(){return q}function resetConfigForTesting(){q=Object.freeze({})}export{configure,getGlobalOptions,resetConfigForTesting,normalizeConfig};
1
+ import{BakerError as u}from"./errors.js";const v=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug"]);function normalizeConfig(j){if(j===null||typeof j!=="object"||Array.isArray(j))throw new u(`[baker] config requires a plain object. Received: ${j===null?"null":Array.isArray(j)?"array":typeof j}.`);for(const q of Object.keys(j))if(!v.has(q))throw new u(`[baker] unknown key '${q}'. Valid keys: ${[...v].join(", ")}.`);return Object.freeze({enableImplicitConversion:j.autoConvert??!1,exposeDefaultValues:j.allowClassDefaults??!1,stopAtFirstError:j.stopAtFirstError??!1,whitelist:j.forbidUnknown??!1,debug:j.debug??!1})}export{normalizeConfig};
@@ -1,3 +1,2 @@
1
1
  export { Field, arrayOf } from './field';
2
2
  export type { FieldOptions, ArrayOfMarker } from './field';
3
- export { Recipe } from './recipe';
@@ -1 +1 @@
1
- export{Field,arrayOf}from"./field.js";export{Recipe}from"./recipe.js";
1
+ export{Field,arrayOf}from"./field.js";
@@ -47,7 +47,7 @@ export declare function toBakerIssueSet(errors: BakerIssue[]): BakerIssueSet;
47
47
  *
48
48
  * Thrown when, e.g.:
49
49
  * - deserialize()/serialize()/validate() is called on an unsealed class
50
- * - configure() is called after seal(), or with an unknown key
50
+ * - new Baker() receives a config object with an unknown key or a non-plain-object
51
51
  * - seal-time metadata invariants fail (discriminator, Map keys, banned names, …)
52
52
  * - per-call options contain unsupported keys
53
53
  * - @Field receives a non-rule value, or a rule/transformer factory is misused
@@ -1 +1 @@
1
- export const BAKER_ERROR=Symbol.for("baker:error");export function isBakerIssueSet(j){return j!=null&&typeof j==="object"&&!Array.isArray(j)&&j[BAKER_ERROR]===!0}export function toBakerIssueSet(j){return{[BAKER_ERROR]:!0,errors:j}}export class BakerError extends Error{constructor(j,q){super(j,q);this.name="BakerError"}}
1
+ export const BAKER_ERROR=Symbol.for("baker:error");export function isBakerIssueSet(q){return q!=null&&typeof q==="object"&&!Array.isArray(q)&&q[BAKER_ERROR]===!0}export function toBakerIssueSet(q){return{[BAKER_ERROR]:!0,errors:q}}export class BakerError extends Error{constructor(q,C){super(q,C);this.name="BakerError"}}
@@ -2,7 +2,7 @@ import type { RuntimeOptions } from '../interfaces';
2
2
  /**
3
3
  * @internal — validate per-call options object at public-API entry.
4
4
  * `groups` is the only valid per-call key; everything else is rejected:
5
- * - seal-time keys (BakerConfig / SealOptions) → "move to configure({...})"
5
+ * - seal-time keys (BakerConfig / SealOptions) → "move to new Baker({...})"
6
6
  * - any other key → "unknown call option"
7
7
  */
8
8
  export declare function checkCallOptions(opts: unknown): RuntimeOptions | undefined;
@@ -1 +1 @@
1
- import{BakerError as v}from"../errors.js";const x=new Set(["groups"]);const F=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug","enableImplicitConversion","exposeDefaultValues","whitelist"]);export function checkCallOptions(d){if(d===void 0||d===null)return;if(typeof d!=="object"||Array.isArray(d))throw new v(`Call options must be a plain object. Received: ${Array.isArray(d)?"array":typeof d}.`);const D=Object.getPrototypeOf(d);if(D!==null&&D!==Object.prototype){const q=d.constructor?.name??"unknown";throw new v(`Call options must be a plain object literal. Received instance of ${q}.`)}for(const q of Object.keys(d)){if(x.has(q))continue;if(F.has(q))throw new v(`Option '${q}' is a seal-time setting and cannot be passed per-call. Move it to configure({ ${q}: ... }) at app startup. Per-call options: ${[...x].join(", ")}.`);throw new v(`Unknown per-call option '${q}'. Valid per-call options: ${[...x].join(", ")}. Seal-time options go to configure({...}).`)}return d}
1
+ import{BakerError as v}from"../errors.js";const x=new Set(["groups"]);const F=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug","enableImplicitConversion","exposeDefaultValues","whitelist"]);export function checkCallOptions(d){if(d===void 0||d===null)return;if(typeof d!=="object"||Array.isArray(d))throw new v(`Call options must be a plain object. Received: ${Array.isArray(d)?"array":typeof d}.`);const D=Object.getPrototypeOf(d);if(D!==null&&D!==Object.prototype){const q=d.constructor?.name??"unknown";throw new v(`Call options must be a plain object literal. Received instance of ${q}.`)}for(const q of Object.keys(d)){if(x.has(q))continue;if(F.has(q))throw new v(`Option '${q}' is a seal-time setting and cannot be passed per-call. Move it to new Baker({ ${q}: ... }) at app startup. Per-call options: ${[...x].join(", ")}.`);throw new v(`Unknown per-call option '${q}'. Valid per-call options: ${[...x].join(", ")}. Seal-time options go to new Baker({...}).`)}return d}
@@ -2,7 +2,7 @@ import type { RuntimeOptions } from '../interfaces';
2
2
  import { type BakerIssueSet } from '../errors';
3
3
  /**
4
4
  * Converts input to a Class instance + validates.
5
- * - Requires `seal()` to be called beforehand; throws `BakerError` if not sealed
5
+ * - Requires the class's baker to be sealed (`new Baker().seal()`) beforehand; throws `BakerError` if not sealed
6
6
  * - Sync DTOs return directly; async DTOs return Promise
7
7
  * - Success: T
8
8
  * - Validation failure: BakerIssueSet (use isBakerIssueSet() to narrow)
@@ -1,7 +1,7 @@
1
1
  import type { RuntimeOptions } from '../interfaces';
2
2
  /**
3
3
  * Converts a Class instance to a plain object.
4
- * - Requires `seal()` to be called beforehand; throws `BakerError` if not sealed
4
+ * - Requires the class's baker to be sealed (`new Baker().seal()`) beforehand; throws `BakerError` if not sealed
5
5
  * - Sync DTOs return directly; async DTOs return Promise
6
6
  * - No validation — always returns Record<string, unknown>
7
7
  */
@@ -4,38 +4,19 @@ import type { RawClassMeta, SealedExecutors } from '../types';
4
4
  declare function circularPlaceholder(className: string): SealedExecutors<unknown>;
5
5
  /**
6
6
  * @internal — used by serialize/deserialize. Returns the sealed executor.
7
- * Throws if the class was never sealed. Users must call `seal()` at app startup.
7
+ * Throws if the class was never sealed. Seal a class via `new Baker().seal()` first.
8
8
  */
9
9
  declare function ensureSealed(Class: Function): SealedExecutors<unknown>;
10
10
  /**
11
- * Seal every class in `registry` with `options`. Shared core of both the global default `seal()`
12
- * and per-scope `createBaker().seal()`. Transactional: on any failure every class sealed by this
13
- * call is rolled back. Clears `registry` on success.
11
+ * Seal every class in `registry` with `options`. The core used by `new Baker().seal()`.
12
+ * Transactional: on any failure every class sealed by this call is rolled back. Clears `registry`
13
+ * on success.
14
14
  *
15
- * A class already sealed (e.g. a shared value-type DTO reached from another scope's roots) is
15
+ * A class already sealed (e.g. a shared value-type DTO reached from another Baker's roots) is
16
16
  * reused as-is — class identity is the isolation boundary, so a shared class carries one sealed
17
- * behaviour. Distinct classes stay fully isolated because each is sealed with its scope's options.
18
- *
19
- * @param track Optional set recording successfully-sealed classes. The default seal passes the
20
- * global `sealedClasses` so `unseal()` can restore them; instances pass nothing.
21
- */
22
- declare function sealRegistry(registry: Set<Function>, options: SealOptions, track?: Set<Function>): void;
23
- /**
24
- * Seal a single class (and its nested DTOs). Not part of the public API — `seal()` (argless)
25
- * is the only public entry. Exposed via `__testing__.sealClass` so tests can seal one class in
26
- * isolation. Class[Symbol.metadata][RAW] must exist; Class[SEALED] must not.
27
- * Transactional: on failure, every placeholder installed by this call (the class and any
28
- * nested DTO reached by recursion) is removed so a future seal attempt can re-run cleanly.
29
- */
30
- declare function sealOneClass(Class: Function): void;
31
- /**
32
- * Public — call once at app startup. Seals every @Recipe-decorated class (and its nested DTOs)
33
- * and clears the registry. Idempotent: a second call is a no-op.
34
- *
35
- * Baker requires this call before any deserialize/serialize/validate. There is no implicit seal.
36
- * All DTOs must be imported before this call — baker has no lazy/on-demand sealing.
17
+ * behaviour. Distinct classes stay fully isolated because each is sealed with its Baker's options.
37
18
  */
38
- declare function seal(): void;
19
+ declare function sealRegistry(registry: Set<Function>, options: SealOptions): void;
39
20
  /**
40
21
  * Merges RAW metadata child-first along the prototype chain of Class.
41
22
  *
@@ -48,10 +29,4 @@ declare function seal(): void;
48
29
  * - flags: child takes priority, only missing flags are supplemented from parent
49
30
  */
50
31
  declare function mergeInheritance(Class: Function): RawClassMeta;
51
- declare const __testing__: {
52
- mergeInheritance: typeof mergeInheritance;
53
- circularPlaceholder: typeof circularPlaceholder;
54
- sealClass: typeof sealOneClass;
55
- };
56
- export { ensureSealed, seal, sealRegistry, mergeInheritance, __testing__ };
57
- export { sealedClasses, resetForTesting } from './seal-state';
32
+ export { ensureSealed, sealRegistry, mergeInheritance, circularPlaceholder };
@@ -1 +1 @@
1
- import{getGlobalOptions as h}from"../configure.js";import{CollectionType as O,Direction as M}from"../enums.js";import{BakerError as G}from"../errors.js";import{deleteSealed as T,freezeRaw as I,getRaw as R,getSealed as E,hasRawOwn as f,hasSealedOwn as S,setSealed as g}from"../meta-access.js";import{globalRegistry as k}from"../registry.js";import{isAsyncFunction as u}from"../utils.js";import{analyzeCircular as y}from"./circular-analyzer.js";import{buildDeserializeCode as p,buildValidateCode as C}from"./deserialize-builder.js";import{validateExposeStacks as m}from"./expose-validator.js";import{sealedClasses as v,isSealed as n,markSealed as c}from"./seal-state.js";import{buildSerializeCode as d}from"./serialize-builder.js";import{validateMeta as o}from"./validate-meta.js";const i=new Set(["__proto__","constructor","prototype"]);const D=new Set([Number,String,Boolean,Date]);function A(H){const q=`Circular dependency during seal: ${H} is still being sealed`;return{deserialize(){throw new G(q)},serialize(){throw new G(q)},validate(){throw new G(q)},isAsync:!1,isSerializeAsync:!1}}function B(H,q,J){const Q=q===M.Deserialize?"isAsync":"isSerializeAsync",j=J??new Set,$=(X)=>{if(j.has(X))return!1;j.add(X);const W=E(X);if(W?.merged)return W[Q]===!0;return B(mergeInheritance(X),q,j)};for(const X of Object.values(H)){if(q===M.Deserialize&&X.validation.some((W)=>W.rule.isAsync))return!0;for(const W of X.transform){if(q===M.Deserialize?W.options?.serializeOnly:W.options?.deserializeOnly)continue;if(W.isAsync??u(W.fn))return!0}if(r(X).some($))return!0}return!1}function r(H){const q=H.type;if(!q)return[];const J=[];if(q.resolvedClass)J.push(q.resolvedClass);if(q.resolvedCollectionValue)J.push(q.resolvedCollectionValue);if(q.discriminator)for(const Q of q.discriminator.subTypes)J.push(Q.value);if(J.length===0&&q.fn){const Q=q.fn();if(Q===Map||Q===Set){const j=q.collectionValue?.();if(typeof j==="function"&&!D.has(j))J.push(j)}else{const j=Array.isArray(Q)?Q[0]:Q;if(typeof j==="function"&&!D.has(j))J.push(j)}}return J}function ensureSealed(H){const q=E(H);if(!q){const J=H.name||"<anonymous class>";throw new G(`${J} is not sealed. Call seal() at app startup before deserialize/validate/serialize. (If ${J} has no @Field decorators, decorate at least one property.)`)}return q}function t(){if(n())return;sealRegistry(k,h(),v);c()}function sealRegistry(H,q,J){const Q=new Set;try{for(const j of H)P(j,q,Q)}catch(j){for(const $ of Q)T($);throw j}for(const j of Q){J?.add(j);I(j)}H.clear()}function l(H){if(S(H))return;const q=h(),J=new Set;try{P(H,q,J)}catch(Q){for(const j of J)T(j);throw Q}for(const Q of J){v.add(Q);I(Q);k.delete(Q)}}function seal(){t()}function P(H,q,J){if(S(H))return;const Q=A(H.name);g(H,Q);try{const j=mergeInheritance(H);for(const U of Object.keys(j))if(i.has(U))throw new G(`${H.name}: field name '${U}' is not allowed (reserved property name)`);for(const[U,K]of Object.entries(j)){if(!K.type?.fn)continue;let F;try{F=K.type.fn()}catch(w){throw new G(`${H.name}.${U}: type function threw: ${w.message}`,{cause:w})}if(F===Map||F===Set){const w=F===Map?O.Map:O.Set,b={...K.type,collection:w,isArray:!1};if(K.type.collectionValue){let x;try{x=K.type.collectionValue()}catch(z){throw new G(`${H.name}.${U}: collectionValue function threw: ${z.message}`,{cause:z})}if(x!=null&&typeof x==="function"&&!D.has(x))b.resolvedCollectionValue=x}j[U]={...K,type:b};continue}const N=Array.isArray(F),V=N?F[0]:F;if(V==null||typeof V!=="function")throw new G(`${H.name}: @Type/@Field type must return a constructor or [constructor], got ${String(V)}`);const _={...K.type,isArray:N};if(!D.has(V)){_.resolvedClass=V;if(!K.flags.validateNested||!K.flags.validateNestedEach){K.flags={...K.flags};if(!K.flags.validateNested)K.flags.validateNested=!0;if(N&&!K.flags.validateNestedEach)K.flags.validateNestedEach=!0}}j[U]={...K,type:_}}m(j,H.name);o(H,j);const $=y(H);for(const U of Object.values(j)){if(U.type?.resolvedClass)P(U.type.resolvedClass,q,J);if(U.type?.resolvedCollectionValue)P(U.type.resolvedCollectionValue,q,J);if(U.type?.discriminator)for(const K of U.type.discriminator.subTypes)P(K.value,q,J)}const X=B(j,M.Deserialize),W=B(j,M.Serialize),L=p(H,j,q,$,X),Y=C(H,j,q,$,X),Z=d(H,j,q,W);Object.assign(Q,{deserialize:L,serialize:Z,validate:Y,isAsync:X,isSerializeAsync:W,merged:j})}catch(j){T(H);throw j}J?.add(H)}function mergeInheritance(H){const q=[];let J=H;while(J&&J!==Object){if(f(J))q.push(J);const $=Object.getPrototypeOf(J);J=$===J?null:$}const Q=Object.create(null),j=q.length>1;for(const $ of q){const X=R($);for(const[W,L]of Object.entries(X))if(!Q[W])Q[W]=j?{validation:[...L.validation],transform:[...L.transform],expose:[...L.expose],exclude:L.exclude,type:L.type,flags:{...L.flags}}:L;else{const Y=Q[W],Z=L;for(const F of Z.validation)if(!Y.validation.some((N)=>N.rule.ruleName===F.rule.ruleName))Y.validation.push(F);if(Y.transform.length===0&&Z.transform.length>0)Y.transform=[...Z.transform];if(Y.expose.length===0&&Z.expose.length>0)Y.expose=[...Z.expose];if(Y.exclude===null&&Z.exclude!==null)Y.exclude=Z.exclude;if(Y.type===null&&Z.type!==null)Y.type=Z.type;const U=Y.flags,K=Z.flags;if(K.isOptional!==void 0&&U.isOptional===void 0)U.isOptional=K.isOptional;if(K.isDefined!==void 0&&U.isDefined===void 0)U.isDefined=K.isDefined;if(K.validateIf!==void 0&&U.validateIf===void 0)U.validateIf=K.validateIf;if(K.isNullable!==void 0&&U.isNullable===void 0)U.isNullable=K.isNullable;if(K.validateNested!==void 0&&U.validateNested===void 0)U.validateNested=K.validateNested;if(K.validateNestedEach!==void 0&&U.validateNestedEach===void 0)U.validateNestedEach=K.validateNestedEach}}return Q}const __testing__={mergeInheritance,circularPlaceholder:A,sealClass:l};export{ensureSealed,seal,sealRegistry,mergeInheritance,__testing__};export{sealedClasses,resetForTesting}from"./seal-state.js";
1
+ import{CollectionType as O,Direction as w}from"../enums.js";import{BakerError as F}from"../errors.js";import{deleteSealed as z,freezeRaw as S,getRaw as I,getSealed as h,hasRawOwn as E,hasSealedOwn as R,setSealed as k}from"../meta-access.js";import{isAsyncFunction as v}from"../utils.js";import{analyzeCircular as A}from"./circular-analyzer.js";import{buildDeserializeCode as f,buildValidateCode as g}from"./deserialize-builder.js";import{validateExposeStacks as u}from"./expose-validator.js";import{buildSerializeCode as y}from"./serialize-builder.js";import{validateMeta as p}from"./validate-meta.js";const C=new Set(["__proto__","constructor","prototype"]);const _=new Set([Number,String,Boolean,Date]);function circularPlaceholder(H){const j=`Circular dependency during seal: ${H} is still being sealed`;return{deserialize(){throw new F(j)},serialize(){throw new F(j)},validate(){throw new F(j)},isAsync:!1,isSerializeAsync:!1}}function b(H,j,J){const Q=j===w.Deserialize?"isAsync":"isSerializeAsync",q=J??new Set,L=(W)=>{if(q.has(W))return!1;q.add(W);const U=h(W);if(U?.merged)return U[Q]===!0;return b(mergeInheritance(W),j,q)};for(const W of Object.values(H)){if(j===w.Deserialize&&W.validation.some((U)=>U.rule.isAsync))return!0;for(const U of W.transform){if(j===w.Deserialize?U.options?.serializeOnly:U.options?.deserializeOnly)continue;if(U.isAsync??v(U.fn))return!0}if(n(W).some(L))return!0}return!1}function n(H){const j=H.type;if(!j)return[];const J=[];if(j.resolvedClass)J.push(j.resolvedClass);if(j.resolvedCollectionValue)J.push(j.resolvedCollectionValue);if(j.discriminator)for(const Q of j.discriminator.subTypes)J.push(Q.value);if(J.length===0&&j.fn){const Q=j.fn();if(Q===Map||Q===Set){const q=j.collectionValue?.();if(typeof q==="function"&&!_.has(q))J.push(q)}else{const q=Array.isArray(Q)?Q[0]:Q;if(typeof q==="function"&&!_.has(q))J.push(q)}}return J}function ensureSealed(H){const j=h(H);if(!j){const J=H.name||"<anonymous class>";throw new F(`${J} is not sealed. Call your baker's seal() (new Baker().seal()) at app startup before deserialize/validate/serialize. (If ${J} has no @Field decorators, decorate at least one property.)`)}return j}function sealRegistry(H,j){const J=new Set;try{for(const Q of H)P(Q,j,J)}catch(Q){for(const q of J)z(q);throw Q}for(const Q of J)S(Q);H.clear()}function P(H,j,J){if(R(H))return;const Q=circularPlaceholder(H.name);k(H,Q);try{const q=mergeInheritance(H);for(const K of Object.keys(q))if(C.has(K))throw new F(`${H.name}: field name '${K}' is not allowed (reserved property name)`);for(const[K,G]of Object.entries(q)){if(!G.type?.fn)continue;let $;try{$=G.type.fn()}catch(M){throw new F(`${H.name}.${K}: type function threw: ${M.message}`,{cause:M})}if($===Map||$===Set){const M=$===Map?O.Map:O.Set,B={...G.type,collection:M,isArray:!1};if(G.type.collectionValue){let V;try{V=G.type.collectionValue()}catch(T){throw new F(`${H.name}.${K}: collectionValue function threw: ${T.message}`,{cause:T})}if(V!=null&&typeof V==="function"&&!_.has(V))B.resolvedCollectionValue=V}q[K]={...G,type:B};continue}const x=Array.isArray($),N=x?$[0]:$;if(N==null||typeof N!=="function")throw new F(`${H.name}: @Type/@Field type must return a constructor or [constructor], got ${String(N)}`);const D={...G.type,isArray:x};if(!_.has(N)){D.resolvedClass=N;if(!G.flags.validateNested||!G.flags.validateNestedEach){G.flags={...G.flags};if(!G.flags.validateNested)G.flags.validateNested=!0;if(x&&!G.flags.validateNestedEach)G.flags.validateNestedEach=!0}}q[K]={...G,type:D}}u(q,H.name);p(H,q);const L=A(H);for(const K of Object.values(q)){if(K.type?.resolvedClass)P(K.type.resolvedClass,j,J);if(K.type?.resolvedCollectionValue)P(K.type.resolvedCollectionValue,j,J);if(K.type?.discriminator)for(const G of K.type.discriminator.subTypes)P(G.value,j,J)}const W=b(q,w.Deserialize),U=b(q,w.Serialize),Z=f(H,q,j,L,W),X=g(H,q,j,L,W),Y=y(H,q,j,U);Object.assign(Q,{deserialize:Z,serialize:Y,validate:X,isAsync:W,isSerializeAsync:U,merged:q})}catch(q){z(H);throw q}J?.add(H)}function mergeInheritance(H){const j=[];let J=H;while(J&&J!==Object){if(E(J))j.push(J);const L=Object.getPrototypeOf(J);J=L===J?null:L}const Q=Object.create(null),q=j.length>1;for(const L of j){const W=I(L);for(const[U,Z]of Object.entries(W))if(!Q[U])Q[U]=q?{validation:[...Z.validation],transform:[...Z.transform],expose:[...Z.expose],exclude:Z.exclude,type:Z.type,flags:{...Z.flags}}:Z;else{const X=Q[U],Y=Z;for(const $ of Y.validation)if(!X.validation.some((x)=>x.rule.ruleName===$.rule.ruleName))X.validation.push($);if(X.transform.length===0&&Y.transform.length>0)X.transform=[...Y.transform];if(X.expose.length===0&&Y.expose.length>0)X.expose=[...Y.expose];if(X.exclude===null&&Y.exclude!==null)X.exclude=Y.exclude;if(X.type===null&&Y.type!==null)X.type=Y.type;const K=X.flags,G=Y.flags;if(G.isOptional!==void 0&&K.isOptional===void 0)K.isOptional=G.isOptional;if(G.isDefined!==void 0&&K.isDefined===void 0)K.isDefined=G.isDefined;if(G.validateIf!==void 0&&K.validateIf===void 0)K.validateIf=G.validateIf;if(G.isNullable!==void 0&&K.isNullable===void 0)K.isNullable=G.isNullable;if(G.validateNested!==void 0&&K.validateNested===void 0)K.validateNested=G.validateNested;if(G.validateNestedEach!==void 0&&K.validateNestedEach===void 0)K.validateNestedEach=G.validateNestedEach}}return Q}export{ensureSealed,sealRegistry,mergeInheritance,circularPlaceholder};
@@ -4,5 +4,5 @@
4
4
  */
5
5
  /** Tier 1 collection metadata (stored on Class[Symbol.metadata] by decorators) */
6
6
  export declare const RAW: unique symbol;
7
- /** Tier 2 seal result (dual executor stored on Class by seal()) */
7
+ /** Tier 2 seal result (dual executor stored on Class at seal time) */
8
8
  export declare const SEALED: unique symbol;
@@ -121,15 +121,15 @@ export interface TypeDef {
121
121
  }[];
122
122
  };
123
123
  keepDiscriminatorProperty?: boolean;
124
- /** seal() normalization result — true if fn() returns an array */
124
+ /** seal-time normalization result — true if fn() returns an array */
125
125
  isArray?: boolean;
126
- /** seal() normalization result — cached class after resolving fn() (DTOs only, excluding primitives) */
126
+ /** seal-time normalization result — cached class after resolving fn() (DTOs only, excluding primitives) */
127
127
  resolvedClass?: ClassCtor;
128
- /** seal() normalization result — Map or Set collection type */
128
+ /** seal-time normalization result — Map or Set collection type */
129
129
  collection?: CollectionType;
130
130
  /** Nested DTO class thunk for Map value / Set element */
131
131
  collectionValue?: () => ClassCtor;
132
- /** seal() normalization result — cached class after resolving collectionValue */
132
+ /** seal-time normalization result — cached class after resolving collectionValue */
133
133
  resolvedCollectionValue?: ClassCtor;
134
134
  }
135
135
  export interface PropertyFlags {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zipbul/baker",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Bun-only AOT decorator-based DTO validation & serialization. class-validator DX, sealed code generation, zero reflect-metadata.",
5
5
  "keywords": [
6
6
  "aot",
@@ -1,17 +0,0 @@
1
- /**
2
- * Marks a class as a baker DTO so `seal()` (called with no arguments) discovers and seals it.
3
- *
4
- * Modern (TC39) field decorators receive no class reference, so `@Field` alone cannot register
5
- * the owning class. `@Recipe` runs after the field decorators and registers the class itself.
6
- *
7
- * @example
8
- * ```ts
9
- * \@Recipe
10
- * class UserDto {
11
- * \@Field(isString()) name!: string;
12
- * }
13
- * seal();
14
- * ```
15
- */
16
- declare function Recipe<T extends Function>(value: T, _context: ClassDecoratorContext): void;
17
- export { Recipe };
@@ -1 +0,0 @@
1
- import{globalRegistry as q}from"../registry.js";function Recipe(j,z){q.add(j)}export{Recipe};
@@ -1,8 +0,0 @@
1
- /**
2
- * Global registry — automatically registers classes with at least one decorator attached
3
- *
4
- * - Automatically called from ensureMeta()
5
- * - seal() iterates this Set to seal all DTOs
6
- * - Metadata is not stored here — used only as an index (which classes are registered)
7
- */
8
- export declare const globalRegistry: Set<Function>;
@@ -1 +0,0 @@
1
- export const globalRegistry=new Set;
@@ -1,10 +0,0 @@
1
- /**
2
- * @internal — shared seal state, extracted so `configure.ts` can read `isSealed()`
3
- * without importing `seal.ts` (which would create a cycle: seal → configure → seal).
4
- */
5
- /** List of sealed classes — used by unseal to remove SEALED */
6
- export declare const sealedClasses: Set<Function>;
7
- export declare function isSealed(): boolean;
8
- export declare function markSealed(): void;
9
- /** @internal — used by unseal() in test helpers */
10
- export declare function resetForTesting(): void;
@@ -1 +0,0 @@
1
- let c=!1;export const sealedClasses=new Set;export function isSealed(){return c}export function markSealed(){c=!0}export function resetForTesting(){c=!1;sealedClasses.clear()}