From 2536e84dca724fac95f72912821bc4e587f88976 Mon Sep 17 00:00:00 2001 From: Ronald Veth Date: Sun, 19 Jul 2026 21:50:47 +0200 Subject: [PATCH 1/2] Add new auto builders --- .changeset/auto-builder-factories.md | 47 + .changeset/base-auto-builder-support.md | 11 + .changeset/deprecate-standard-builders.md | 7 + .changeset/packaging-dual-esm-cjs.md | 5 + .gitattributes | 1 + .github/workflows/ci.yml | 55 +- .github/workflows/release.yml | 16 +- .gitignore | 3 +- .oxlintrc.json | 10 +- MIGRATION.md | 264 + README.md | 2301 ++------ package-lock.json | 5112 +++++++++-------- package.json | 53 +- src/auto-builder-core.ts | 534 ++ src/builder-error.ts | 91 + src/cerios-auto-builder.ts | 210 + src/cerios-builder.ts | 509 +- src/cerios-class-auto-builder.ts | 244 + src/cerios-class-builder.ts | 495 +- src/index.ts | 30 +- src/types.ts | 62 + .../auto-builder-core-build-isolation.test.ts | 152 + .../auto-builder-core-deep-clone.test.ts | 121 + .../auto-builder-core-errors.test.ts | 150 + .../auto-builder-core-reserved-names.test.ts | 114 + .../auto-builder-core-safety.test.ts | 154 + .../auto-builder-core-state-fidelity.test.ts | 162 + ...s-auto-builder-base-type-extension.test.ts | 184 + .../cerios-auto-builder-build-methods.test.ts | 78 + .../cerios-auto-builder-cloning.test.ts | 92 + .../cerios-auto-builder-composition.test.ts | 183 + .../cerios-auto-builder-constructor.test.ts | 78 + .../cerios-auto-builder-director.test.ts | 137 + ...ios-auto-builder-nested-properties.test.ts | 82 + ...rios-auto-builder-property-removal.test.ts | 107 + ...rios-auto-builder-proxy-edge-cases.test.ts | 113 + ...cerios-auto-builder-subclass-state.test.ts | 82 + .../cerios-auto-builder-type-safety.test.ts | 126 + .../cerios-auto-builder.test.ts | 109 + .../cerios-builder-build-gate.test.ts | 88 + .../cerios-builder-from-branded.test.ts | 56 + ...erios-builder-post-patch-extension.test.ts | 4 +- .../cerios-builder-property-removal.test.ts | 2 +- .../cerios-builder-validators.test.ts | 2 +- ...-auto-builder-base-class-extension.test.ts | 180 + ...s-class-auto-builder-build-methods.test.ts | 118 + .../cerios-class-auto-builder-cloning.test.ts | 101 + ...ios-class-auto-builder-composition.test.ts | 201 + ...ios-class-auto-builder-constructor.test.ts | 102 + ...ass-auto-builder-nested-properties.test.ts | 95 + ...lass-auto-builder-property-removal.test.ts | 115 + ...lass-auto-builder-proxy-edge-cases.test.ts | 169 + ...s-class-auto-builder-runtime-cache.test.ts | 95 + ...-class-auto-builder-subclass-state.test.ts | 62 + ...ios-class-auto-builder-type-safety.test.ts | 166 + .../cerios-class-auto-builder.test.ts | 160 + .../cerios-class-builder-build-gate.test.ts | 116 + ...ss-builder-data-properties-methods.test.ts | 54 + tests/index/index-exports.test.ts | 96 + tsup.config.ts => tsdown.config.mts | 3 +- 60 files changed, 9301 insertions(+), 4968 deletions(-) create mode 100644 .changeset/auto-builder-factories.md create mode 100644 .changeset/base-auto-builder-support.md create mode 100644 .changeset/deprecate-standard-builders.md create mode 100644 .changeset/packaging-dual-esm-cjs.md create mode 100644 .gitattributes create mode 100644 MIGRATION.md create mode 100644 src/auto-builder-core.ts create mode 100644 src/builder-error.ts create mode 100644 src/cerios-auto-builder.ts create mode 100644 src/cerios-class-auto-builder.ts create mode 100644 tests/auto-builder-core/auto-builder-core-build-isolation.test.ts create mode 100644 tests/auto-builder-core/auto-builder-core-deep-clone.test.ts create mode 100644 tests/auto-builder-core/auto-builder-core-errors.test.ts create mode 100644 tests/auto-builder-core/auto-builder-core-reserved-names.test.ts create mode 100644 tests/auto-builder-core/auto-builder-core-safety.test.ts create mode 100644 tests/auto-builder-core/auto-builder-core-state-fidelity.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-base-type-extension.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-build-methods.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-cloning.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-composition.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-constructor.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-director.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-nested-properties.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-property-removal.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-proxy-edge-cases.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-subclass-state.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder-type-safety.test.ts create mode 100644 tests/cerios-auto-builder/cerios-auto-builder.test.ts create mode 100644 tests/cerios-builder/cerios-builder-build-gate.test.ts create mode 100644 tests/cerios-builder/cerios-builder-from-branded.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-base-class-extension.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-build-methods.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-cloning.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-composition.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-constructor.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-nested-properties.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-property-removal.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-proxy-edge-cases.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-runtime-cache.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-subclass-state.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-type-safety.test.ts create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder.test.ts create mode 100644 tests/cerios-class-builder/cerios-class-builder-build-gate.test.ts create mode 100644 tests/cerios-class-builder/cerios-class-builder-data-properties-methods.test.ts create mode 100644 tests/index/index-exports.test.ts rename tsup.config.ts => tsdown.config.mts (72%) diff --git a/.changeset/auto-builder-factories.md b/.changeset/auto-builder-factories.md new file mode 100644 index 0000000..c388709 --- /dev/null +++ b/.changeset/auto-builder-factories.md @@ -0,0 +1,47 @@ +--- +"@cerios/cerios-builder": minor +--- + +Add `CeriosAutoBuilder()` and `CeriosClassAutoBuilder(Class)` base-class factories that give a subclass an automatic bare-name setter for every property — no more hand-written `setProperty` wrappers. Custom logic stays as ordinary class methods (their return type can be inferred), and mixes freely with the auto setters while keeping the existing compile-time required-property tracking: calling `build()` before all required properties are set is a compiler error. Reserved names (e.g. a `build` property) get a `Prop`-suffixed setter, non-identifier keys are set via bracket access, and the full build-variant/validation/`clone`/`from` API is inherited unchanged. + +Adds nested-builder composition to both auto builders. `BuilderComposerFromFactory` and `ClassBuilderComposerFromFactory` now accept factories from `CeriosAutoBuilder` / `CeriosClassAutoBuilder` subclasses, not just hand-written ones, so a custom method can take a callback, hand it a preconfigured child builder, and store the result — with the callback refusing to compile unless it returns a fully-set child builder of the right type. Previously these types resolved to `never` for auto builders, because an auto builder's declared instance type is structural and is not assignable to the nominal `CeriosBuilder`/`CeriosClassBuilder`. + +Adds `BuilderWith` and `ClassBuilderWith` for writing explicit return types, which matters if your lint rules require them. The built type is derived from the builder, so these take two arguments instead of the three needed by `BuilderStep`/`BuilderPreset`: `static createWithDefaults(): BuilderWith` and `inRotterdam(): BuilderWith`. They work for hand-written builders too. Use `this` (not the class name) in instance methods so previously set keys are preserved, and name root keys only — setting a nested value brands its root property. + +Both factories now expose one identical constructor shape, `new Builder(data, { requiredFields, validators })`. Naming the options removes a trap: the two base builders take those two arguments in opposite positional order, and the class auto builder previously had no supported way to pass either through `super()` at all. `requiredFields` additionally accepts an exhaustive record (`{ id: true, name: true }`) that the compiler forces you to keep complete — the array form is checked for validity but not completeness, so adding a required property to your type would otherwise leave runtime validation silently behind. + +The auto-builder API deliberately exposes **only** the generated root setters: `setProperty`, `setProperties`, `setNestedProperty`, and `addToArrayProperty` are not part of it. Finer-grained updates (nested values, appending to arrays) belong in a distinctly-named custom method that delegates to a root setter, or in a director that composes multiple builders — see the Director pattern section in the README. + +Fixes a class of silent state corruption in both auto builders. The proxy treated any name reachable on the prototype chain as a real member, but only 22 names were declared reserved, so a modelled property named `setProperty`, `createBuilder`, `hasOwnProperty` (and about a dozen others) type-checked as a bare setter and then invoked the real method instead — `builder.createBuilder("x")` replaced the entire builder state with the string `"x"`. All such names now route through the `*Prop` suffix like any other reserved name, and a test walks both prototype chains to fail the build if a member is ever added without being reserved. + +Copy-on-write no longer runs the subclass constructor. Every setter re-creates the builder, which previously meant calling `new this.constructor(...)` with an internal argument shape that a user-written `constructor(data?)` cannot receive — silently dropping validators and required fields on the class side, and reverting `setRequiredFields()` on the object side. + +Also fixes `DataPropertiesOnly` in the class builders so methods **with parameters** are correctly excluded from the compile-time required-property check (previously only zero-argument methods were stripped under `strictFunctionTypes`, which could make a class with a parameterized method impossible to `build()`). + +Copy-on-write carries a subclass's own instance fields across. Because it bypasses the subclass constructor, a field like `private cache = new Map()` was dropped on the first setter call — and since the name was then absent from the proxy target, the proxy handed back an auto-setter _function_ for it, so `this.cache.set(...)` failed with a `TypeError` naming `.set`. Truthiness checks silently took the wrong branch. + +Passing a bare required-fields record positionally — `new B({}, { name: true })` instead of `new B({}, { requiredFields: { name: true } })` — now throws. It structurally matches the options object, so it was previously accepted and silently produced a builder with **zero** required fields, meaning runtime validation passed for every incomplete object. + +`__proto__` is no longer a reserved builder name, so no `__proto__Prop` setter is generated. That setter mapped straight back to the real `__proto__` key; on the class builder the resulting assignment re-parented the built instance, which silently stopped being an instance of its own class and lost every method. + +`CeriosClassAutoBuilder(Person)` returns one memoised runtime class per target class — it previously minted a new class on every call — so `instanceof` holds across separate factory calls. The deprecated `requiredDataProperties` static is no longer part of the returned constructor type — with the runtime class now shared, writing it there would have been visible to every other consumer of that class. Declaring the static on your own subclass is unaffected. + +**Changes affecting existing (non-auto) builders:** + +- `buildFrozen()` and `buildSealed()` no longer freeze and return the builder's own internal state. They returned `this._actual` directly, so two calls yielded the same object, mutating a nested field of the result mutated the builder, and a `buildSealed()` after a `buildFrozen()` came back frozen rather than sealed. The deep variants were already correct. +- `CeriosClassBuilder` no longer aliases its internal state into the instance it builds. Mutating a built instance's nested object changed the builder, and `buildDeepFrozen()` froze the builder's own state permanently, so every later build from it silently dropped writes. +- Deep clone preserves prototypes, so a nested class instance stays an instance of its class instead of being flattened to a plain object and losing its methods. +- `setProperty`, `setProperties`, and `addToArrayProperty` reject `__proto__` as a key. `setNestedProperty` already rejected it inside a path; a plain key was unguarded. `constructor` and `prototype` remain settable as ordinary keys — only path traversal through them is blocked. +- `CeriosBuilder.from()` returns a branded, immediately buildable builder, matching the auto builders. Seeding from a complete object no longer forces a `buildUnsafe()` fallback. Note it requires the subclass to declare a public constructor. +- `setNestedProperty` no longer re-clones each intermediate level of the path, which made its cost O(depth x size) on state that was already freshly cloned. + +- Deep clone now preserves `Date`, `Map`, `Set`, and `RegExp` (each previously cloned to `{}`, so a `Date` in seed data was silently destroyed) and handles circular references (which previously threw `RangeError: Maximum call stack size exceeded` from `from()`, `clone()`, and `buildDeepFrozen()`). One implementation replaces five near-identical private copies; `deepFreeze`/`deepSeal` likewise collapse from four copies to one, and are cycle-safe. +- Build variants no longer hand out the builder's live internal state. `build()` and `buildUnsafe()` returned `this._actual` by reference, so mutating the result mutated the builder and everything built from it afterwards; `buildFrozen()` called `Object.freeze` on the builder's own state. `buildPartial()` is now a deep copy, which is what its documentation already claimed. +- `setNestedProperty()` rejects paths containing `__proto__`, `constructor`, or `prototype` instead of writing through a prototype and silently swallowing the value. +- Validation errors are now `CeriosBuilderError`, an `Error` subclass carrying `missingFields` and `validationErrors` arrays so callers can stop string-matching. Existing message text is unchanged apart from the two items below. +- A validator returning `false` is now identified by index and function name. Three failing validators previously produced `"Validation failed: Validation failed; Validation failed; Validation failed"`, with no way to tell which failed. A validator returning `undefined` — easy to write by forgetting a `return` — was treated as a **pass** and is now an error, as is an empty-string return. +- The "Please set these fields before calling build." advice now names the variant actually called, so `buildDeepSealed()` no longer points at `build()`. + +- `CeriosClassBuilder.clearOptionalProperties()` now preserves the root object of a nested required path. A required path like `"address.city"` was matched as a single literal key, found nothing, and discarded `address` entirely — destroying the data the call was meant to keep. `CeriosBuilder` already handled this correctly; both now share one implementation. +- `CeriosBuilder.buildPartial()` returns a copy instead of the live internal state, so callers can no longer mutate a builder through it. This matches `CeriosClassBuilder.buildPartial()`. +- `CeriosBuilder.setRequiredFields()` is now copy-on-write and returns a new builder rather than mutating and returning `this`. This matches `CeriosClassBuilder.setRequiredFields()` and every other method on both classes. Fluent chains are unaffected; only code holding the pre-call reference and expecting it to have changed will see a difference. diff --git a/.changeset/base-auto-builder-support.md b/.changeset/base-auto-builder-support.md new file mode 100644 index 0000000..be1f4bd --- /dev/null +++ b/.changeset/base-auto-builder-support.md @@ -0,0 +1,11 @@ +--- +"@cerios/cerios-builder": minor +--- + +Add first-class support for base types and base classes to both auto builders. + +Building a derived target already worked — setters are generated from `keyof T`, which includes inherited members, and `CeriosClassAutoBuilder` runs the real constructor chain — and is now covered by tests for inherited data properties, `instanceof` both classes, inherited methods, required fields on inherited properties, `from()`, and compile-time gating. + +What was missing is the deprecated builders' shared base builder (`abstract class BaseXBuilder extends CeriosBuilder`), whose literal port is illegal TypeScript for a factory-based auto builder. Its replacement is the **base-builder function**: shared custom methods are written once in a function constrained on the new `AutoBuilderBase` (`ClassAutoBuilderBase` for classes, where TBase may be abstract) and applied on top of each concrete auto builder, returned as the new `BuilderExtension` so the derived builder keeps its full setter set, statics, and per-builder required fields. Setter calls inside shared methods brand the derived builder's compile-time build gate like any direct setter call. + +See the README section "Base Types and Shared Builder Logic" and MIGRATION.md section 7 ("Generic base builder classes → base-builder functions"). diff --git a/.changeset/deprecate-standard-builders.md b/.changeset/deprecate-standard-builders.md new file mode 100644 index 0000000..ada620c --- /dev/null +++ b/.changeset/deprecate-standard-builders.md @@ -0,0 +1,7 @@ +--- +"@cerios/cerios-builder": minor +--- + +**Deprecate `CeriosBuilder` and `CeriosClassBuilder`.** Both hand-written builder base classes are deprecated and will be **removed in the next major version**. Migrate to `CeriosAutoBuilder()` / `CeriosClassAutoBuilder(Class)`, which generate every setter automatically with the same compile-time required-property tracking — see MIGRATION.md for a per-feature before/after guide. The `setNestedProperty` dot-path feature is intentionally not carried over: compose nested objects with a director that coordinates multiple builders (or with nested-builder callbacks via `BuilderComposerFromFactory`) instead. + +**An all-optional type can now `build()` immediately.** The compile-time gate on `build()` and the other validated build variants dissolves when the target type has no required properties, so a builder for an all-optional type no longer needs a throwaway setter call (or a `buildUnsafe()` fallback) before building. This applies to all four builder base classes and is exposed as the `BuildGate` / `ClassBuildGate` types. diff --git a/.changeset/packaging-dual-esm-cjs.md b/.changeset/packaging-dual-esm-cjs.md new file mode 100644 index 0000000..a71faa2 --- /dev/null +++ b/.changeset/packaging-dual-esm-cjs.md @@ -0,0 +1,5 @@ +--- +"@cerios/cerios-builder": patch +--- + +The published package now ships explicit dual CJS/ESM artifacts with per-condition type declarations. The build tool moved from tsup to tsdown, and the dist filenames changed: `dist/index.js` → `dist/index.cjs`, `dist/index.d.ts` → `dist/index.d.cts`, with `dist/index.d.mts` added alongside `dist/index.mjs`. The `exports` map now declares a `types` condition per branch (`.d.mts` for `import`, `.d.cts` for `require`/`default`), so TypeScript resolves the matching declaration format under every module resolution mode; the layout is validated with `@arethetypeswrong/cli` in CI. Importing the package entry point (`import`/`require` of `@cerios/cerios-builder`) is unaffected — only code deep-importing dist filenames directly would notice. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..94f480d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 653f271..706ab3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,65 +11,34 @@ concurrency: cancel-in-progress: true jobs: - lint: - name: Lint, Type & Format Check + build-test: + name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: "22" + node-version: 24 cache: "npm" - name: Install dependencies run: npm ci - - name: Run linter and type check + - name: Linting & Type-aware lint run: npm run lint - - name: Check formatting - run: npm run format:check - - build: - name: Build Packages - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v6 - with: - node-version: "22" - cache: "npm" - - - name: Install dependencies - run: npm ci - - name: Type check run: npm run compile - - name: Build packages - run: npm run build - - test: - name: Test (Node ${{ matrix.node-version }}) - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [20, 22] - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - cache: "npm" + - name: Check formatting + run: npm run format:check - - name: Install dependencies - run: npm ci + - name: Run tests + run: npm run test - name: Build packages run: npm run build - - name: Run tests - run: npm run test + - name: Validate package exports + run: npm run check-exports diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b9b8f6..c1559c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,23 +11,25 @@ jobs: runs-on: ubuntu-latest permissions: contents: write - id-token: write pull-requests: write + id-token: write steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "22" + node-version: 24 cache: "npm" registry-url: "https://registry.npmjs.org" - - name: Upgrade npm - run: npm install -g npm@latest + - name: Show Node and npm versions + run: | + node -v + npm -v - name: Install dependencies run: npm ci @@ -42,8 +44,8 @@ jobs: id: changesets uses: changesets/action@v1 with: - version: npm run changeset:prepare-release - publish: npm run changeset:publish + version: npm run version-packages:prepare-release + publish: npm run release commit: "chore: version packages" title: "chore: version packages" env: diff --git a/.gitignore b/.gitignore index 04c01ba..94934f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -dist/ \ No newline at end of file +dist/ +coverage/ \ No newline at end of file diff --git a/.oxlintrc.json b/.oxlintrc.json index 949f668..c740eea 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -18,5 +18,13 @@ "typescript/no-unnecessary-type-assertion": "error", "typescript/prefer-nullish-coalescing": "error", "typescript/require-await": "error" - } + }, + "overrides": [ + { + "files": ["tests/**"], + "rules": { + "typescript/no-deprecated": "off" + } + } + ] } diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..b041fbb --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,264 @@ +# Migrating to the Auto Builders + +`CeriosBuilder` and `CeriosClassBuilder` are **deprecated** and will be **removed in the next major version**. Their replacements are the auto builder factories: + +| Deprecated | Replacement | +| --------------------------------------- | ------------------------------------------- | +| `class B extends CeriosBuilder` | `class B extends CeriosAutoBuilder()` | +| `class B extends CeriosClassBuilder` | `class B extends CeriosClassAutoBuilder(T)` | + +The auto builders keep the complete compile-time guarantee — `build()` does not compile until every required property is set — but generate every setter for you. This guide shows the before/after for each feature. + +## 1. Basic builder: drop the setter wrappers + +**Before** — every property needs a hand-written `setProperty` wrapper: + +```typescript +class UserBuilder extends CeriosBuilder { + static create() { + return new UserBuilder({}); + } + + id(value: string) { + return this.setProperty("id", value); + } + + name(value: string) { + return this.setProperty("name", value); + } + + email(value: string) { + return this.setProperty("email", value); + } +} +``` + +**After** — the setters are generated; only real logic remains: + +```typescript +class UserBuilder extends CeriosAutoBuilder() { + static create(): UserBuilder { + return new UserBuilder({}); + } +} + +const user = UserBuilder.create().id("1").name("Alice").email("a@b.io").build(); +``` + +Note the `()` — `CeriosAutoBuilder()` is a factory call that returns the base class. + +## 2. Class builder: drop the constructor plumbing + +**Before** — the subclass constructor forwards the class constructor (and, if it wants `setRequiredFields`/`addValidator` to survive copy-on-write, the whole internal 4-argument shape): + +```typescript +class PersonBuilder extends CeriosClassBuilder { + constructor(classConstructor: ClassConstructor = Person, data: Partial = {}) { + super(classConstructor, data); + } + + static create() { + return new PersonBuilder(Person); + } + + name(value: string) { + return this.setProperty("name", value); + } +} +``` + +**After** — the class is passed once, to the factory; the constructor is just `(data?, init?)`: + +```typescript +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); + } +} + +const person = PersonBuilder.create().name("Alice").age(30).build(); +person.greet(); // real instance - methods, getters, and decorators preserved +``` + +Methods are excluded from the generated setters and from the required-property check automatically. + +## 3. Custom fluent methods + +Custom methods carry over unchanged — they are ordinary class methods that delegate to the generated setters. Leave the return type to inference, or use `BuilderWith` (`ClassBuilderWith` for classes) if your lint requires explicit return types: + +```typescript +class UserBuilder extends CeriosAutoBuilder() { + static create(): UserBuilder { + return new UserBuilder({}); + } + + asAdmin(): BuilderWith { + return this.role("admin"); + } +} +``` + +`BuilderWith` replaces the longer `BuilderStep` — the built type is derived from the builder, so you never repeat it. Always use `this`, not the class name, so previously set keys are preserved. + +**A custom method cannot reuse a property's exact name** — the method would shadow the generated setter it needs to call. Give it a distinct name: + +```typescript +// Before (hand-written): name(value) { return this.setProperty("name", value.trim()); } +// After: +trimmedName(value: string): BuilderWith { + return this.name(value.trim()); +} +``` + +## 4. `static requiredTemplate` / `requiredDataProperties` → constructor options + +Runtime validation of required fields moves from the deprecated statics to the constructor options (or `setRequiredFields()`), identical on both auto builders: + +```typescript +class UserBuilder extends CeriosAutoBuilder() { + constructor(data?: Partial) { + super(data, { + requiredFields: { id: true, name: true, email: true }, + validators: [(u): boolean | string => (u.email?.includes("@") ? true : "Invalid email")], + }); + } + + static create(): UserBuilder { + return new UserBuilder(); + } +} +``` + +Prefer the exhaustive **record form** (`{ id: true, ... }`): adding a required property to `User` then fails to compile here, so runtime validation cannot silently fall behind. The **array form** (`["id", "name", "email"]`) is still available and is the one to use for nested dot-notation paths (`"order.details.customerId"`). + +The options object is the exported `BuilderInit` type, should you need to name it — for example to build the `init` argument up front before passing it to `super()`. + +## 5. `setNestedProperty` → a director composing multiple builders + +The dot-path helper is deliberately **not** part of the auto builders. Instead of one builder writing deep paths into one big object, give each level its own small builder and put the assembly recipe in a **director**: + +```typescript +class AddressBuilder extends CeriosAutoBuilder
() { + static create(): AddressBuilder { + return new AddressBuilder({}); + } + + static createDomestic(): BuilderWith { + return AddressBuilder.create().country("USA"); + } +} + +class OrderDetailsBuilder extends CeriosAutoBuilder() { + static create(): OrderDetailsBuilder { + return new OrderDetailsBuilder({}); + } + + static createPending(): BuilderWith { + return OrderDetailsBuilder.create().status("pending"); + } +} + +class OrderRequestBuilder extends CeriosAutoBuilder() { + static create(): OrderRequestBuilder { + return new OrderRequestBuilder({}); + } +} + +/** One place that knows how a complete OrderRequest is put together. */ +class OrderRequestDirector { + static standardOrder(input: { customerId: string; totalAmount: number; street: string; city: string }): OrderRequest { + const shippingAddress = AddressBuilder.createDomestic().street(input.street).city(input.city).build(); + + const details = OrderDetailsBuilder.createPending() + .customerId(input.customerId) + .totalAmount(input.totalAmount) + .shippingAddress(shippingAddress) + .build(); + + return OrderRequestBuilder.create().order({ details }).build(); + } +} +``` + +Compared to `setNestedProperty("Order.Details.ShippingAddress.Street", ...)`: + +- every level is compile-time checked by its own builder — a missing `city` is a compile error, not a runtime surprise; +- each child builder is reusable on its own and in other directors; +- callers can customize a level through a callback (`BuilderComposerFromFactory`) without the director losing control of the recipe. + +The runnable version of this example lives in [tests/cerios-auto-builder/cerios-auto-builder-director.test.ts](tests/cerios-auto-builder/cerios-auto-builder-director.test.ts). + +For a one-off small update, a custom method replacing the whole root property also works: + +```typescript +inCity(city: string): BuilderWith { + const address = this.buildPartial().address ?? { street: "", city: "" }; + return this.address({ ...address, city }); +} +``` + +## 6. `addToArrayProperty` → a custom method on the root setter + +```typescript +// Before: addTag(tag: string) { return this.addToArrayProperty("tags", tag); } +// After: +addTag(tag: string): BuilderWith { + return this.tags([...(this.buildPartial().tags ?? []), tag]); +} +``` + +## 7. Generic base builder classes → base-builder functions + +A hand-written base builder generic over a base type cannot be ported literally: `class B extends CeriosAutoBuilder()` is illegal, because a base-class expression cannot reference the class's own type parameter. It also isn't needed for the setters — auto builders generate setters for inherited properties automatically, so only the **shared custom methods** still have to move. They go into a **base-builder function**, constrained on `AutoBuilderBase` (`ClassAutoBuilderBase` for classes) and returned as `BuilderExtension`: + +```typescript +// Before: shared setters and helpers in an abstract generic base class +abstract class BasePostRequestBuilder extends CeriosBuilder { + postId(value: T["postId"]): BuilderStep { + return this.setProperty("postId", value); + } + addTag(value: string): BuilderStep { + return this.setProperty("tags", [...(this.buildPartial().tags ?? []), value] as T["tags"]); + } +} +class CreatePostRequestBuilder extends BasePostRequestBuilder { + authorId(value: string): BuilderStep { + return this.setProperty("authorId", value); + } +} + +// After: setters exist automatically; only shared helpers need a home +function BasePostRequestBuilder>(Builder: TBuilder) { + abstract class PostRequestBuilder extends Builder { + addTag(tag: string) { + return this.tags([...(this.buildPartial().tags ?? []), tag]); + } + } + return PostRequestBuilder as BuilderExtension; +} +class CreatePostRequestBuilder extends BasePostRequestBuilder(CeriosAutoBuilder()) { + static create(): CreatePostRequestBuilder { + return new CreatePostRequestBuilder({}, { requiredFields: ["postId", "title", "content", "authorId"] }); + } +} +``` + +Keep the function's name identical to the abstract class it replaces and the extends clause stays recognizable. Each derived builder keeps its full API — all setters, its own required fields, `clone()`, static `from()` — and shared setter calls count toward the derived compile-time gate. One nuance: a shared method setting a property the derived type strengthens from optional to required brands only the base flavor; set such properties through the derived builder's own setter. See the README section [Base Types and Shared Builder Logic](README.md#-base-types-and-shared-builder-logic). + +## 8. Everything that carries over unchanged + +The full builder API is inherited by the auto builders — no changes needed for: + +- all build variants: `build`, `buildWithoutRuntimeValidation`, `buildWithoutCompileTimeValidation`, `buildUnsafe`, `buildPartial`, `buildFrozen`, `buildDeepFrozen`, `buildSealed`, `buildDeepSealed` +- `addValidator()`, `setRequiredFields()`, `removeOptionalProperty()`, `clearOptionalProperties()`, `clone()` +- `CeriosBuilderError` with `missingFields` / `validationErrors` +- nested-builder composition with `BuilderComposerFromFactory` / `ClassBuilderComposerFromFactory` + +`static from()` gets **better**: `CeriosClassAutoBuilder(...).from(instance)` takes just the instance (the deprecated `CeriosClassBuilder.from` needed the class constructor as a first argument), and the deprecated `CeriosBuilder.from()` requirement of a _public_ subclass constructor disappears. + +## 9. Behavioral notes on the auto builders + +- **Reserved names**: a property named like a builder method (`build`, `clone`, `then`, ...) gets a `Prop`-suffixed setter (`buildProp`); the real method keeps its name. +- **Non-identifier keys** (`"content-type"`) are set with bracket access: `builder["content-type"]("application/json")`. +- **All-optional types build immediately**: `build()` is available on a fresh builder when the type has no required properties (this also applies to the deprecated builders as of this release). +- Setters are generated by a `Proxy`; `await builder` and `JSON.stringify(builder)` behave normally. diff --git a/README.md b/README.md index a0465c5..f2a60ab 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,17 @@ # @cerios/cerios-builder -A powerful TypeScript builder pattern library that provides **compile-time type safety** for object construction. Build complex objects with method chaining while ensuring all required properties are set at compile time. +A TypeScript builder pattern library with **compile-time type safety** and **zero boilerplate**. Extend an auto builder and you get an automatic, fully typed setter for every property — and `build()` is a compile error until every required property has been set. ## 🚀 Features -- **Type-Safe Building**: Compile-time validation ensures all required properties are set -- **Nested Properties Support**: Build deeply nested objects with dot-notation paths -- **Flexible Runtime Validation**: Choose between compile-time, runtime, or both validations -- **Immutability Options**: Create frozen or sealed objects with shallow or deep variants -- **Dynamic Required Fields**: Add required fields at runtime based on your business logic -- **Method Chaining**: Fluent API for readable object construction -- **Partial Building**: Build incomplete objects when needed -- **Zero Runtime Overhead**: All type checking happens at compile time (unless using runtime validation) -- **Extensible**: Easy to create custom builder methods +- **Zero Boilerplate**: `CeriosAutoBuilder()` / `CeriosClassAutoBuilder(Class)` generate every setter — no hand-written wrappers +- **Type-Safe Building**: Compile-time validation ensures all required properties are set before `build()` compiles +- **Class Support**: Build real class instances — methods, getters, decorators, and prototype chains preserved +- **Flexible Runtime Validation**: Required-field checks and custom validators, independent of the compile-time tracking +- **Immutability Options**: Frozen or sealed results, shallow or deep +- **Composable**: Nested-builder callbacks and the director pattern for deep structures +- **Immutable Builders**: Every setter is copy-on-write — forked builders never affect each other - **TypeScript First**: Built with TypeScript, for TypeScript -- **Array Property Helpers**: Easily add values to array properties with type safety ## 📦 Installation @@ -31,2148 +28,539 @@ npm install --save-dev @cerios/cerios-builder ## 🎯 Quick Start ```typescript -import { CeriosBuilder, RequiredFieldsTemplate } from "@cerios/cerios-builder"; +import { CeriosAutoBuilder } from "@cerios/cerios-builder"; -// 1. Define your types +// 1. Define your type type User = { id: string; name: string; email: string; + role: string; age?: number; }; -// 2. Create your builder -class UserBuilder extends CeriosBuilder { - static create() { +// 2. Extend the auto builder - every property gets a setter automatically +class UserBuilder extends CeriosAutoBuilder() { + static create(): UserBuilder { return new UserBuilder({}); } - id(value: string) { - return this.setProperty("id", value); - } - - name(value: string) { - return this.setProperty("name", value); - } - - email(value: string) { - return this.setProperty("email", value); - } - - age(value: number) { - return this.setProperty("age", value); + // Custom logic is an ordinary method delegating to a generated setter + asAdmin() { + return this.role("admin"); } } -// 3. Build your object -const user = UserBuilder.create().id("123").name("John Doe").email("john@example.com").age(30).build(); -``` - -## 📖 Feature Overview - -| Feature | Method | Use Case | -| ------------------------ | ------------------------------------- | --------------------------------------------------- | -| **Simple Properties** | `setProperty()` | Set flat object properties | -| **Nested Properties** | `setNestedProperty()` | Set deeply nested properties with dot notation | -| **Array Properties** | `addToArrayProperty()` | Add values to array properties | -| **Multiple Properties** | `setProperties()` | Set multiple properties at once | -| **Required Template** | `static requiredTemplate` | Define required fields for runtime validation | -| **Dynamic Requirements** | `setRequiredFields()` | Add required fields at runtime | -| **Custom Validators** | `addValidator()` | Add custom validation logic with error messages | -| **Property Removal** | `removeOptionalProperty()` | Remove optional properties from builder | -| **Clear Optional Props** | `clearOptionalProperties()` | Clear all optional properties, keep required | -| **Full Validation** | `build()` | Build with both compile-time and runtime validation | -| **Compile-Time Only** | `buildWithoutRuntimeValidation()` | Build with TypeScript checking only | -| **Runtime Only** | `buildWithoutCompileTimeValidation()` | Build with runtime validation only | -| **No Validation** | `buildUnsafe()` | Build without any validation | -| **Partial Build** | `buildPartial()` | Build incomplete objects | -| **Shallow Freeze** | `buildFrozen()` | Create immutable object (top-level only) | -| **Deep Freeze** | `buildDeepFrozen()` | Create fully immutable object tree | -| **Shallow Seal** | `buildSealed()` | Lock structure, allow modifications | -| **Deep Seal** | `buildDeepSealed()` | Lock all structures, allow modifications | - -## 🔧 Basic Usage - -### 1. Define Your Type and Builder +// 3. Build - all required properties enforced at compile time +const user = UserBuilder.create().id("1").name("Alice").email("a@b.io").asAdmin().build(); -```typescript -import { CeriosBuilder } from "@cerios/cerios-builder"; +// ❌ Compile error - email and role are not set: +// UserBuilder.create().id("1").name("Alice").build(); -// Define your data type -type User = { - id: string; - name: string; - email: string; - age?: number; // Optional property - roles?: string[]; // Optional array property -}; +// ✅ Optional properties can be omitted (age is optional) +``` -// Create your builder class -class UserBuilder extends CeriosBuilder { - static create() { - return new UserBuilder({}); - } +Every property becomes a bare setter (`id`, `name`, …). Custom methods use those setters on `this` and mix freely into the chain, each contributing to the compile-time required-field tracking. If the type has **no** required properties at all, `build()` is available immediately. - id(value: string) { - return this.setProperty("id", value); - } +## 🧩 Building Classes: `CeriosClassAutoBuilder` - name(value: string) { - return this.setProperty("name", value); - } +Building instantiates the real class, so methods, getters, and decorators are preserved. Class methods are automatically excluded from the setters and from the required-property check: - email(value: string) { - return this.setProperty("email", value); - } +```typescript +import { CeriosClassAutoBuilder } from "@cerios/cerios-builder"; - age(value: number) { - return this.setProperty("age", value); - } +class Person { + name!: string; + age!: number; + email?: string; - addRole(role: string) { - return this.addToArrayProperty("roles", role); + constructor(data?: Partial) { + if (data) Object.assign(this, data); } - // Custom methods for common patterns - withRandomId() { - return this.setProperty("id", crypto.randomUUID()); + greet(): string { + return `Hi, ${this.name}`; } +} - withAdminEmail(username: string) { - return this.setProperty("email", `${username}@admin.company.com`); +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); } } -``` - -### 2. Build Objects Safely - -```typescript -// ✅ This compiles - all required fields are set -const user = UserBuilder.create() - .id("123") - .name("John Doe") - .email("john@example.com") - .age(30) - .addRole("admin") - .addRole("editor") - .build(); -// ✅ Optional fields and arrays can be omitted -const basicUser = UserBuilder.create().id("456").name("Jane Doe").email("jane@example.com").build(); // age and roles are optional - -// ❌ This won't compile - missing required 'email' field -const incompleteUser = UserBuilder.create().id("789").name("Bob Smith").build(); // 💥 TypeScript error! +const person = PersonBuilder.create().name("Alice").age(30).build(); +person.greet(); // "Hi, Alice" +person instanceof Person; // true ``` -### 3. Keep Custom Method Return Types Short +## ✍️ Custom Methods -If you want explicit return types on custom fluent methods, use `BuilderStep` and `ClassBuilderStep`. -These helpers keep method signatures readable while preserving compile-time required-field tracking. +### Return types -```typescript -import { BuilderStep, CeriosBuilder } from "@cerios/cerios-builder"; +Leave the return type off and TypeScript infers it, so the brand keeps flowing and `build()` stays fully checked. If your lint requires explicit return types, use `BuilderWith<>` (`ClassBuilderWith<>` for class builders). It takes the **builder** and the **keys the method sets** — the built type is derived from the builder, so you never repeat it: -type User = { - name: string; - age: number; - address: { - city: string; - }; -}; +```typescript +import { BuilderWith, CeriosAutoBuilder } from "@cerios/cerios-builder"; -class UserBuilder extends CeriosBuilder { - static create() { - return new UserBuilder({}); +class AddressBuilder extends CeriosAutoBuilder
() { + static create(): AddressBuilder { + return new AddressBuilder({}); } - name(value: string): BuilderStep { - return this.setProperty("name", value); + static createWithDefaults(): BuilderWith { + return AddressBuilder.create().country("NL"); } - age(value: number): BuilderStep { - return this.setProperty("age", value); + static createComplete(): BuilderWith { + return AddressBuilder.create().country("NL").street("Main St").city("Rotterdam"); } - city(value: string): BuilderStep { - return this.setNestedProperty("address.city", value); + inRotterdam(): BuilderWith { + return this.city("Rotterdam"); } } ``` -```typescript -import { CeriosClassBuilder, ClassBuilderStep, ClassConstructor } from "@cerios/cerios-builder"; +| What the method does | Return type | +| ------------------------------- | ---------------------------------------- | +| Factory, nothing preset | `AddressBuilder` | +| Factory presetting some fields | `BuilderWith` | +| Factory presetting everything | `BuilderWith` | +| Custom method setting one field | `BuilderWith` | +| Custom method setting several | `BuilderWith` | +| Class builders | the same, with `ClassBuilderWith<>` | -class Person { - name!: string; - address!: { city: string }; -} +**Instance methods must use `this`, not the class name.** `BuilderWith` keeps everything set earlier in the chain; naming the concrete class would discard it and `build()` would stop compiling. Returning a plain `AddressBuilder` has the same problem — always return the setter result. -class PersonBuilder extends CeriosClassBuilder { - constructor(classConstructor: ClassConstructor = Person, data: Partial = {}) { - super(classConstructor, data); - } +### Method names - static create() { - return new PersonBuilder(Person); - } +A custom method **cannot reuse a property's exact name** — the method would shadow the generated setter it needs to call. Give it a distinct name and delegate: - name(value: string): ClassBuilderStep { - return this.setProperty("name", value); - } +```typescript +trimmedName(value: string): BuilderWith { + return this.name(value.trim()); +} - city(value: string): ClassBuilderStep { - return this.setNestedProperty("address.city", value); - } +addTag(tag: string): BuilderWith { + return this.tags([...(this.buildPartial().tags ?? []), tag]); } ``` -### 4. Use Custom Methods +### Reserved Names and Non-Identifier Keys -```typescript -const adminUser = UserBuilder.create() - .withRandomId() - .name("Admin User") - .withAdminEmail("admin") - .addRole("admin") - .age(25) - .build(); +- A property whose name collides with a builder method — `build`, `clone`, `then`, `toJSON`, `setRequiredFields`, `addValidator`, and the other builder members — gets a **`Prop`-suffixed** setter instead, and the real method keeps its name: -console.log(adminUser); -// Output: { id: "550e8400-...", name: "Admin User", email: "admin@admin.company.com", roles: ["admin"], age: 25 } -``` + ```typescript + type Config = { url: string; build: number }; + class ConfigBuilder extends CeriosAutoBuilder() { + /* ... */ + } + ConfigBuilder.create().url("/x").buildProp(3).build(); // buildProp sets `build`; build() still builds + ``` -## 🎯 Advanced Examples +- A property whose name isn't a valid identifier (for example `"content-type"`) keeps its exact name and is set with **bracket access** — fully type-safe: -### Deeply Nested Properties + ```typescript + builder["content-type"]("application/json"); + ``` -Build complex nested structures with type-safe dot-notation paths: +## 🧬 Base Types and Shared Builder Logic -```typescript -import { CeriosBuilder, RequiredFieldsTemplate } from "@cerios/cerios-builder"; +### Derived types just work -type Address = { - Street: string; - City: string; - PostalCode?: string; - Country: string; -}; +Setters are generated from `keyof T`, which includes everything inherited — so a builder for a derived type automatically has setters for the base properties too. No base builder is needed just to share setters: -type OrderDetails = { - OrderNumber?: string; - CustomerId: string; - TotalAmount: number; - Status: string; - ShippingAddress: Address; -}; +```typescript +type BasePostRequest = { postId: string; title?: string; tags?: string[] }; +type CreatePostRequest = BasePostRequest & { title: string; authorId: string }; -type Order = { - Details?: OrderDetails; -}; +class CreatePostRequestBuilder extends CeriosAutoBuilder() {} +// .postId(), .title(), .tags() and .authorId() all exist, fully typed +``` -type OrderRequest = { - Order: Order; -}; +The same holds for class hierarchies: `CeriosClassAutoBuilder(Order)` where `class Order extends BaseEntity` generates setters for the inherited data properties, and building runs the real constructor chain — the result is `instanceof` both classes with all inherited methods intact. -class OrderRequestBuilder extends CeriosBuilder { - static requiredTemplate: RequiredFieldsTemplate = [ - "Order.Details.CustomerId", - "Order.Details.TotalAmount", - "Order.Details.Status", - "Order.Details.ShippingAddress.Street", - "Order.Details.ShippingAddress.City", - "Order.Details.ShippingAddress.Country", - ]; - - static create() { - return new OrderRequestBuilder({}); - } +### Sharing custom methods: the base-builder function - static createWithDefaults() { - return this.create().status("pending"); - } +Custom methods you want to reuse across several derived types go in a **base-builder function**: a function constrained on `AutoBuilderBase` (or `ClassAutoBuilderBase` for classes) that layers the shared methods on top of each concrete auto builder. Name it after the abstract base builder class you would have written by hand — the extends clause then reads the same: - orderNumber(value: string) { - return this.setNestedProperty("Order.Details.OrderNumber", value); - } +```typescript +import { AutoBuilderBase, BuilderExtension, CeriosAutoBuilder } from "@cerios/cerios-builder"; - customerId(value: string) { - return this.setNestedProperty("Order.Details.CustomerId", value); +function BasePostRequestBuilder>(Builder: TBuilder) { + abstract class PostRequestBuilder extends Builder { + addTag(tag: string) { + return this.tags([...(this.buildPartial().tags ?? []), tag]); + } } + return PostRequestBuilder as BuilderExtension; +} - totalAmount(value: number) { - return this.setNestedProperty("Order.Details.TotalAmount", value); +class CreatePostRequestBuilder extends BasePostRequestBuilder(CeriosAutoBuilder()) { + static create(): CreatePostRequestBuilder { + return new CreatePostRequestBuilder({}, { requiredFields: ["postId", "title", "authorId"] }); } +} +``` - status(value: string) { - return this.setNestedProperty("Order.Details.Status", value); - } +Inside the function, `this` exposes a setter for every property of the base type plus `buildPartial()`, and each derived builder keeps its own full API: all setters, per-builder required fields, `clone()`, and the static `from()`. Setter calls in shared methods brand the build gate like any other setter, so `create().addTag("x")` counts toward compile-time validation — and if your lint requires explicit return types, `BuilderWith` (or `ClassBuilderWith<>`) works on shared methods exactly as in [Custom Methods](#-custom-methods). The `BuilderExtension` return type is what keeps the derived setters and statics visible on the result — don't leave it off. - shippingStreet(value: string) { - return this.setNestedProperty("Order.Details.ShippingAddress.Street", value); - } +For class hierarchies the shape is identical; the base class may be abstract, because it is only used as a type: - shippingCity(value: string) { - return this.setNestedProperty("Order.Details.ShippingAddress.City", value); - } +```typescript +import { BuilderExtension, CeriosClassAutoBuilder, ClassAutoBuilderBase } from "@cerios/cerios-builder"; - shippingPostalCode(value: string) { - return this.setNestedProperty("Order.Details.ShippingAddress.PostalCode", value); - } +abstract class BaseEntity { + id!: string; + createdAt?: Date; +} - shippingCountry(value: string) { - return this.setNestedProperty("Order.Details.ShippingAddress.Country", value); +function BaseEntityBuilder>(Builder: TBuilder) { + abstract class EntityBuilder extends Builder { + createdNow() { + return this.createdAt(new Date()); + } } + return EntityBuilder as BuilderExtension; } -// Usage with full validation (both compile-time and runtime) -const order = OrderRequestBuilder.createWithDefaults() - .customerId("CUST-001") - .totalAmount(299.99) - .orderNumber("ORD-12345") - .shippingStreet("123 Main St") - .shippingCity("New York") - .shippingCountry("USA") - .build(); // ✅ TypeScript + runtime validation - -// ❌ This will throw an error at runtime -try { - const invalidOrder = OrderRequestBuilder.createWithDefaults() - .customerId("CUST-002") - .buildWithoutCompileTimeValidation(); // Missing TotalAmount and shipping address -} catch (error) { - console.error(error.message); - // "Missing required fields: Order.Details.TotalAmount, Order.Details.ShippingAddress.Street, ..." -} +class OrderBuilder extends BaseEntityBuilder(CeriosClassAutoBuilder(Order)) {} +class CustomerBuilder extends BaseEntityBuilder(CeriosClassAutoBuilder(Customer)) {} ``` -### Runtime Validation with Required Templates +One nuance: when a derived type **strengthens** an optional base property to required (like `title` above), a shared method that sets it brands the _base_ type's optional flavor, which doesn't count toward the derived build gate. Set strengthened properties through the derived builder's own setter; shared methods are at their best on properties whose optionality doesn't change. -Define which fields are required and validate them at runtime: +## ✅ Required Fields and Validators -```typescript -type Product = { - id: string; - name: string; - price: number; - description?: string; -}; - -class ProductBuilder extends CeriosBuilder { - static requiredTemplate: RequiredFieldsTemplate = ["id", "name", "price"]; - - static create() { - return new ProductBuilder({}); - } +Compile-time tracking works on its own. On top of it, both auto builders accept the same constructor options for **runtime** validation, so a subclass declares everything once: - id(value: string) { - return this.setProperty("id", value); - } - - name(value: string) { - return this.setProperty("name", value); - } - - price(value: number) { - return this.setProperty("price", value); +```typescript +class UserBuilder extends CeriosAutoBuilder() { + constructor(data?: Partial) { + super(data, { + requiredFields: { id: true, name: true, email: true, role: true }, + validators: [(u): boolean | string => (u.email?.includes("@") ? true : "Invalid email")], + }); } - description(value: string) { - return this.setProperty("description", value); + static create(): UserBuilder { + return new UserBuilder(); } } +``` -// Full validation (compile-time + runtime) -const product = ProductBuilder.create().id("PROD-001").name("Laptop").price(999.99).build(); // ✅ All required fields are set +### Record form vs array form -// Runtime-only validation (useful for dynamic data) -const dynamicProduct = ProductBuilder.create() - .id("PROD-002") - .name("Mouse") - .price(29.99) - .buildWithoutCompileTimeValidation(); // ✅ Runtime check passes +The **record form** (`{ id: true, ... }`) is exhaustive: add a required property to `User` and this line fails to compile, so runtime validation cannot silently fall behind. The **array form** is validated against your type but not checked for completeness — use it when you need nested dot-notation paths: -// ❌ This will throw an error at runtime -try { - const invalidProduct = ProductBuilder.create().id("PROD-003").name("Keyboard").buildWithoutCompileTimeValidation(); // Missing price -} catch (error) { - console.error(error.message); - // "Missing required fields: price" -} +```typescript +super(data, { + requiredFields: ["order.details.customerId", "order.details.shippingAddress.city"], +}); ``` -### Dynamic Required Fields +> Deriving the list from the type alone is not possible — types are erased before runtime, so the keys must exist as a value somewhere. -Add required fields at runtime based on business logic: +### Dynamic requirements: `setRequiredFields()` + +When the list depends on data you only have at runtime: ```typescript -type Employee = { - firstName: string; - lastName: string; - email: string; - employeeId?: string; - phone?: string; -}; +const builder = employee.isContractor + ? base.setRequiredFields(["id", "name", "contractEndDate"]) + : base.setRequiredFields(["id", "name", "employeeId"]); +``` -class EmployeeBuilder extends CeriosBuilder { - static requiredTemplate: RequiredFieldsTemplate = ["firstName", "lastName", "email"]; +`setRequiredFields()` **replaces** the instance list — the last call wins, so pass one complete list per call. It is copy-on-write like every other builder method. - static create() { - return new EmployeeBuilder({}); - } +### Custom validators - firstName(value: string) { - return this.setProperty("firstName", value); - } +Validators receive the partial object and return `true` (pass), `false` (generic failure), or a `string` (failure with message): - lastName(value: string) { - return this.setProperty("lastName", value); - } +```typescript +const user = UserBuilder.create() + .id("1") + .name("Bob") + .email("bob@example.com") + .asAdmin() + .addValidator((obj) => (obj.age ?? 18) >= 18 || "User must be at least 18 years old") + .addValidator((obj) => obj.email?.includes("@") || "Invalid email format") + .build(); +``` - email(value: string) { - return this.setProperty("email", value); - } +- Validators run on `build()`, `buildWithoutCompileTimeValidation()`, and the frozen/sealed variants; `buildUnsafe()` and `buildPartial()` skip them. +- Every validator runs; all failure messages are joined into a single error. +- A validator returning `false` is identified by index and function name; returning `undefined` (a forgotten `return`) or an empty string is treated as a failure, not a pass. - employeeId(value: string) { - return this.setProperty("employeeId", value); - } +### Handling errors: `CeriosBuilderError` - phone(value: string) { - return this.setProperty("phone", value); - } -} +Every validating build variant throws a `CeriosBuilderError`. It extends `Error`, and carries the failure as data: + +```typescript +import { CeriosBuilderError } from "@cerios/cerios-builder"; -// Scenario 1: New employee (only basic fields required) -const newEmployee = EmployeeBuilder.create().firstName("John").lastName("Doe").email("john.doe@company.com").build(); // ✅ Only validates firstName, lastName, email - -// Scenario 2: Existing employee (ID required) -const existingEmployee = EmployeeBuilder.create() - .setRequiredFields(["employeeId"]) // Add employeeId as required dynamically - .firstName("Jane") - .lastName("Smith") - .email("jane.smith@company.com") - .employeeId("EMP-12345") - .buildWithoutCompileTimeValidation(); // ✅ Runtime validates all fields including employeeId - -// Scenario 3: Employee with contact requirement -const contactEmployee = EmployeeBuilder.create() - .setRequiredFields(["phone", "employeeId"]) // Add multiple dynamic fields - .firstName("Bob") - .lastName("Johnson") - .email("bob.johnson@company.com") - .phone("+1-555-0123") - .employeeId("EMP-67890") - .buildWithoutCompileTimeValidation(); // ✅ Validates all including phone and employeeId - -// ❌ This will fail - missing dynamically required field try { - const invalidEmployee = EmployeeBuilder.create() - .setRequiredFields(["employeeId"]) - .firstName("Alice") - .lastName("Brown") - .email("alice.brown@company.com") - .buildWithoutCompileTimeValidation(); // Missing employeeId + builder.build(); } catch (error) { - console.error(error.message); - // "Missing required fields: employeeId" + if (error instanceof CeriosBuilderError) { + error.missingFields; // e.g. ["email", "role"] - empty if a validator failed instead + error.validationErrors; // e.g. ["Age must be 18 or older"] - empty if fields were missing + } } ``` -### Sharing Methods Across Related Request Builders (POST + PATCH) +The `instanceof` check is also what makes this compile under `useUnknownInCatchVariables`. -When you have related request types, share fluent methods in a generic base builder and keep concrete builders per request type. -This preserves type-safe chaining and allows each request type to keep its own required-field template. +## 🛠️ Build Methods + +| Method | Compile-Time Check | Runtime Check | Use Case | +| ------------------------------------- | ------------------ | ------------- | ---------------------- | +| `build()` | ✅ | ✅ | Production code | +| `buildWithoutRuntimeValidation()` | ✅ | ❌ | Performance-critical | +| `buildWithoutCompileTimeValidation()` | ❌ | ✅ | External/dynamic data | +| `buildUnsafe()` | ❌ | ❌ | Trusted scenarios only | +| `buildPartial()` | ❌ | ❌ | Incomplete objects | ```typescript -import { BuilderStep, CeriosBuilder, RequiredFieldsTemplate } from "@cerios/cerios-builder"; +// Runtime-only validation is useful when building from external data: +function buildFromApi(data: Record): User { + let builder = UserBuilder.create(); + if (typeof data.id === "string") builder = builder.id(data.id); + // ... + return builder.buildWithoutCompileTimeValidation(); // throws CeriosBuilderError if incomplete +} +``` -type BasePostRequest = { - postId: string; - title?: string; - content?: string; - tags?: string[]; -}; +### Immutable results -type CreatePostRequest = BasePostRequest & { - title: string; - content: string; - authorId: string; -}; +Each validating build variant also has frozen and sealed versions: -type PatchPostRequest = BasePostRequest & { - patchReason: string; - notifySubscribers?: boolean; -}; +| Method | Modify Top-Level | Modify Nested | Add/Delete Properties | Returns | +| ------------------- | ---------------- | ------------- | --------------------- | ----------------- | +| `buildFrozen()` | ❌ | ✅ | ❌ (top level) | `Readonly` | +| `buildDeepFrozen()` | ❌ | ❌ | ❌ (all levels) | `DeepReadonly` | +| `buildSealed()` | ✅ | ✅ | ❌ (top level) | `T` | +| `buildDeepSealed()` | ✅ | ✅ | ❌ (all levels) | `T` | -abstract class BasePostRequestBuilder extends CeriosBuilder { - postId(value: string): BuilderStep { - return this.setProperty("postId", value as T["postId"]); - } +Use **deep frozen** for configuration objects and snapshots, **deep sealed** for fixed schemas with mutable values, and the shallow variants when nested objects should stay flexible. All of them validate first and return a fresh copy — the builder itself is never frozen. - title(value: string): BuilderStep { - return this.setProperty("title", value as T["title"]); - } +## 🏗️ Composing Nested Objects - content(value: string): BuilderStep { - return this.setProperty("content", value as T["content"]); - } +An auto setter always replaces a **whole root property**. Deep structures are composed from small builders, in two complementary ways. - addTag(value: string): BuilderStep { - return this.addToArrayProperty("tags", value); - } -} +### Nested builders: callback composition -class CreatePostRequestBuilder extends BasePostRequestBuilder { - static create() { - return new CreatePostRequestBuilder({}); - } +To let callers build a nested object inline, add a method taking a callback typed with `BuilderComposerFromFactory<>` (`ClassBuilderComposerFromFactory<>` for classes). It infers the callback's input **and** its required output from the child factory: the callback gets a builder with the factory's defaults already counted as set, and it will not compile unless it returns a fully-set child builder. - authorId(value: string): BuilderStep { - return this.setProperty("authorId", value); - } -} +```typescript +import { BuilderComposerFromFactory, BuilderWith, CeriosAutoBuilder } from "@cerios/cerios-builder"; -class PatchPostRequestBuilder extends BasePostRequestBuilder { - static create() { - return new PatchPostRequestBuilder({}); +class CustomerBuilder extends CeriosAutoBuilder() { + static create(): CustomerBuilder { + return new CustomerBuilder({}); } - patchReason(value: string): BuilderStep { - return this.setProperty("patchReason", value); + withAddress(fn: BuilderComposerFromFactory): BuilderWith { + return this.address(fn(AddressBuilder.createWithDefaults()).build()); } - notifySubscribers(value: boolean): BuilderStep { - return this.setProperty("notifySubscribers", value); + // Factory presets everything -> the callback can be optional + withCompleteAddress( + fn?: BuilderComposerFromFactory, + ): BuilderWith { + const builder = AddressBuilder.createComplete(); + return this.address(fn ? fn(builder).build() : builder.build()); } } -const createRequest = CreatePostRequestBuilder.create() - .postId("post-100") - .title("Builder patterns") - .content("Full create payload") - .authorId("author-1") - .addTag("typescript") +const customer = CustomerBuilder.create() + .id("1") + .name("Alice") + .withAddress((address) => address.street("Main St").city("Rotterdam")) // `country` already defaulted .build(); -const patchRequest = PatchPostRequestBuilder.create() - .postId("post-100") - .patchReason("Fix title") - .title("Updated title") - .notifySubscribers(true) - .build(); +// Compile errors you get for free: +CustomerBuilder.create().withAddress((address) => address.street("Main St")); // city not set +CustomerBuilder.create().withAddress(() => ContactBuilder.create().email("a@b.c")); // wrong target type ``` -The same pattern works with `CeriosClassBuilder`. -See the class-based test example in `tests/cerios-class-builder/cerios-class-builder-post-patch-extension.test.ts`. - -### Four Ways to Set Up Required Fields for Deeply Nested Properties +Give the composer method a distinct name (`withAddress`) and let it delegate to the auto setter (`this.address(...)`) — that keeps the generated setter available to do the actual work. -When working with deeply nested structures, you have **multiple approaches** to configure required field validation: +### The Director Pattern -#### 1. Static Template at Class Level (Recommended for Fixed Requirements) +For deep structures — where the deprecated builders used `setNestedProperty` dot-paths — give each level its own builder and put the assembly recipe in a **director**: ```typescript -type Address = { - Street: string; - City: string; - Country: string; -}; - -type OrderDetails = { - CustomerId: string; - TotalAmount: number; - ShippingAddress: Address; -}; - -type Order = { - Details: OrderDetails; -}; - -class OrderBuilder extends CeriosBuilder { - // Method 1: Static template defined at class level - static requiredTemplate: RequiredFieldsTemplate = [ - "Details.CustomerId", - "Details.TotalAmount", - "Details.ShippingAddress.Street", - "Details.ShippingAddress.City", - "Details.ShippingAddress.Country", - ]; - - static create() { - return new OrderBuilder({}); - } - - customerId(value: string) { - return this.setNestedProperty("Details.CustomerId", value); - } - - totalAmount(value: number) { - return this.setNestedProperty("Details.TotalAmount", value); - } - - shippingStreet(value: string) { - return this.setNestedProperty("Details.ShippingAddress.Street", value); - } +type OrderRequest = { order: { details?: OrderDetails } }; - shippingCity(value: string) { - return this.setNestedProperty("Details.ShippingAddress.City", value); +class AddressBuilder extends CeriosAutoBuilder
() { + static create(): AddressBuilder { + return new AddressBuilder({}); } - shippingCountry(value: string) { - return this.setNestedProperty("Details.ShippingAddress.Country", value); + static createDomestic(): BuilderWith { + return AddressBuilder.create().country("USA"); } } -// Usage: Runtime validation checks all required fields -const order = OrderBuilder.create() - .customerId("CUST-001") - .totalAmount(299.99) - .shippingStreet("123 Main St") - .shippingCity("New York") - .shippingCountry("USA") - .buildWithoutCompileTimeValidation(); // ✅ Validates all 5 required nested fields -``` - -#### 2. Dynamic Required Fields via `setRequiredFields()` (Best for Conditional Logic) - -```typescript -class OrderBuilder extends CeriosBuilder { - static requiredTemplate: RequiredFieldsTemplate = ["Details.CustomerId", "Details.TotalAmount"]; - - static create() { - return new OrderBuilder({}); - } - - static createInternational() { - return this.create().setRequiredFields([ - "Details.ShippingAddress.Street", - "Details.ShippingAddress.City", - "Details.ShippingAddress.Country", - "Details.ShippingAddress.PostalCode", // Extra requirement for international - ]); - } - - static createDomestic() { - return this.create().setRequiredFields([ - "Details.ShippingAddress.Street", - "Details.ShippingAddress.City", - "Details.ShippingAddress.Country", - ]); // No postal code required - } - - customerId(value: string) { - return this.setNestedProperty("Details.CustomerId", value); - } - - totalAmount(value: number) { - return this.setNestedProperty("Details.TotalAmount", value); - } - - shippingStreet(value: string) { - return this.setNestedProperty("Details.ShippingAddress.Street", value); - } - - shippingCity(value: string) { - return this.setNestedProperty("Details.ShippingAddress.City", value); - } - - shippingCountry(value: string) { - return this.setNestedProperty("Details.ShippingAddress.Country", value); +class OrderDetailsBuilder extends CeriosAutoBuilder() { + static create(): OrderDetailsBuilder { + return new OrderDetailsBuilder({}); } - shippingPostalCode(value: string) { - return this.setNestedProperty("Details.ShippingAddress.PostalCode", value); + static createPending(): BuilderWith { + return OrderDetailsBuilder.create().status("pending"); } } -// Domestic order: postal code is optional -const domesticOrder = OrderBuilder.createDomestic() - .customerId("CUST-001") - .totalAmount(99.99) - .shippingStreet("123 Main St") - .shippingCity("New York") - .shippingCountry("USA") - .buildWithoutCompileTimeValidation(); // ✅ Valid without postal code - -// International order: postal code is required -const internationalOrder = OrderBuilder.createInternational() - .customerId("CUST-002") - .totalAmount(299.99) - .shippingStreet("10 Downing Street") - .shippingCity("London") - .shippingCountry("UK") - .shippingPostalCode("SW1A 2AA") - .buildWithoutCompileTimeValidation(); // ✅ All fields including postal code validated - -// ❌ This will fail - missing postal code for international -try { - const invalidOrder = OrderBuilder.createInternational() - .customerId("CUST-003") - .totalAmount(150.0) - .shippingStreet("Rue de Rivoli") - .shippingCity("Paris") - .shippingCountry("France") - .buildWithoutCompileTimeValidation(); // Missing PostalCode -} catch (error) { - console.error(error.message); - // "Missing required fields: Details.ShippingAddress.PostalCode" -} -``` - -#### 3. Constructor Parameter (For Instance-Specific Requirements) - -```typescript -class OrderBuilder extends CeriosBuilder { - // Base template with common required fields - static requiredTemplate: RequiredFieldsTemplate = ["Details.CustomerId", "Details.TotalAmount"]; - - // Constructor accepts additional required fields - constructor(initial: Partial, additionalRequiredFields?: RequiredFieldsTemplate) { - super(initial, additionalRequiredFields); - } - - static create() { - return new OrderBuilder({}); - } - - // Factory method for orders with shipping requirements - static createWithShipping() { - return new OrderBuilder({}, [ - "Details.ShippingAddress.Street", - "Details.ShippingAddress.City", - "Details.ShippingAddress.Country", - ]); - } - - customerId(value: string) { - return this.setNestedProperty("Details.CustomerId", value); +class OrderRequestBuilder extends CeriosAutoBuilder() { + static create(): OrderRequestBuilder { + return new OrderRequestBuilder({}); } +} - totalAmount(value: number) { - return this.setNestedProperty("Details.TotalAmount", value); - } +/** One place that knows how a complete OrderRequest is put together. */ +class OrderRequestDirector { + static standardOrder(input: { customerId: string; totalAmount: number; street: string; city: string }): OrderRequest { + const shippingAddress = AddressBuilder.createDomestic().street(input.street).city(input.city).build(); - shippingStreet(value: string) { - return this.setNestedProperty("Details.ShippingAddress.Street", value); - } + const details = OrderDetailsBuilder.createPending() + .customerId(input.customerId) + .totalAmount(input.totalAmount) + .shippingAddress(shippingAddress) + .build(); - shippingCity(value: string) { - return this.setNestedProperty("Details.ShippingAddress.City", value); + return OrderRequestBuilder.create().order({ details }).build(); } - shippingCountry(value: string) { - return this.setNestedProperty("Details.ShippingAddress.Country", value); + // Callers can customise one level without the director losing control of the recipe: + static orderShippedTo( + input: { customerId: string; totalAmount: number }, + composeAddress: BuilderComposerFromFactory, + ): OrderRequest { + const shippingAddress = composeAddress(AddressBuilder.createDomestic()).build(); + // ... assemble as above } } -// Order without shipping (only validates base required fields) -const digitalOrder = OrderBuilder.create() - .customerId("CUST-001") - .totalAmount(49.99) - .buildWithoutCompileTimeValidation(); // ✅ Valid - shipping not required - -// Order with shipping (validates base + shipping fields) -const physicalOrder = OrderBuilder.createWithShipping() - .customerId("CUST-002") - .totalAmount(99.99) - .shippingStreet("123 Main St") - .shippingCity("New York") - .shippingCountry("USA") - .buildWithoutCompileTimeValidation(); // ✅ All required fields validated - -// ❌ This will fail - missing shipping address -try { - const invalidOrder = OrderBuilder.createWithShipping() - .customerId("CUST-003") - .totalAmount(75.0) - .buildWithoutCompileTimeValidation(); // Missing shipping fields -} catch (error) { - console.error(error.message); - // "Missing required fields: Details.ShippingAddress.Street, ..." -} +const order = OrderRequestDirector.standardOrder({ + customerId: "CUST-001", + totalAmount: 299.99, + street: "123 Main St", + city: "New York", +}); ``` -#### 4. Hybrid Approach (Static + Dynamic Combined) +Why this beats dot-path writing into one big object: -```typescript -class OrderBuilder extends CeriosBuilder { - // Static template for always-required fields - static requiredTemplate: RequiredFieldsTemplate = ["Details.CustomerId", "Details.TotalAmount"]; - - static create() { - return new OrderBuilder({}); - } +- every level is compile-time checked by its own builder — a missing `city` is a compile error, not a runtime surprise; +- each child builder is reusable on its own and in other directors; +- the director gives the recipe a name (`standardOrder`) instead of scattering path strings. - // Factory with static template + additional dynamic fields - static createPriorityOrder() { - return this.create().setRequiredFields([ - "Details.ShippingAddress.Street", - "Details.ShippingAddress.City", - "Details.ShippingAddress.Country", - "Details.ShippingAddress.PostalCode", - "Details.Priority", // Extra field for priority orders - ]); - } +The runnable version lives in [tests/cerios-auto-builder/cerios-auto-builder-director.test.ts](tests/cerios-auto-builder/cerios-auto-builder-director.test.ts). - // Another factory combining different requirements - static createGiftOrder() { - return this.create().setRequiredFields([ - "Details.ShippingAddress.Street", - "Details.ShippingAddress.City", - "Details.ShippingAddress.Country", - "Details.GiftMessage", // Required for gift orders - "Details.RecipientName", // Required for gift orders - ]); - } +## 🔧 Seeding, Cloning, and Removing - customerId(value: string) { - return this.setNestedProperty("Details.CustomerId", value); - } +```typescript +// Seed from an existing object or instance (deep-cloned; immediately buildable): +const updated = UserBuilder.from(existingUser).name("Renamed").build(); - totalAmount(value: number) { - return this.setNestedProperty("Details.TotalAmount", value); - } +// Independent copy of a builder in progress: +const fork = builder.clone(); - priority(value: string) { - return this.setNestedProperty("Details.Priority", value); - } +// Remove optional properties: +builder.removeOptionalProperty("age"); // one optional key +builder.clearOptionalProperties(); // all optional keys, required ones survive +``` - giftMessage(value: string) { - return this.setNestedProperty("Details.GiftMessage", value); - } +Everything is copy-on-write: each call returns a new builder, and builders never share mutable state with each other or with the objects they build. - recipientName(value: string) { - return this.setNestedProperty("Details.RecipientName", value); - } +## 🧪 Testing Integration - shippingStreet(value: string) { - return this.setNestedProperty("Details.ShippingAddress.Street", value); - } +Auto builders shine for test fixtures — defaults in factories, variation in the test: - shippingCity(value: string) { - return this.setNestedProperty("Details.ShippingAddress.City", value); +```typescript +class ProductBuilder extends CeriosAutoBuilder() { + static create(): ProductBuilder { + return new ProductBuilder({}); } - shippingCountry(value: string) { - return this.setNestedProperty("Details.ShippingAddress.Country", value); + static createValid(): BuilderWith { + return ProductBuilder.create().name("Widget").price(9.99).category("Tools"); } - shippingPostalCode(value: string) { - return this.setNestedProperty("Details.ShippingAddress.PostalCode", value); + asFree(): BuilderWith { + return this.price(0); } } -// Priority order: requires shipping + priority + base fields -const priorityOrder = OrderBuilder.createPriorityOrder() - .customerId("CUST-001") - .totalAmount(299.99) - .priority("express") - .shippingStreet("123 Main St") - .shippingCity("New York") - .shippingCountry("USA") - .shippingPostalCode("10001") - .buildWithoutCompileTimeValidation(); // ✅ All priority order fields validated - -// Gift order: requires shipping + gift fields + base fields -const giftOrder = OrderBuilder.createGiftOrder() - .customerId("CUST-002") - .totalAmount(149.99) - .giftMessage("Happy Birthday!") - .recipientName("John Doe") - .shippingStreet("456 Oak Ave") - .shippingCity("Boston") - .shippingCountry("USA") - .buildWithoutCompileTimeValidation(); // ✅ All gift order fields validated - -// ❌ This will fail - missing gift-specific fields -try { - const invalidGiftOrder = OrderBuilder.createGiftOrder() - .customerId("CUST-003") - .totalAmount(99.99) - .shippingStreet("789 Elm St") - .shippingCity("Chicago") - .shippingCountry("USA") - .buildWithoutCompileTimeValidation(); // Missing GiftMessage and RecipientName -} catch (error) { - console.error(error.message); - // "Missing required fields: Details.GiftMessage, Details.RecipientName" -} +test("free products skip payment", () => { + const product = ProductBuilder.createValid().asFree().build(); + expect(paymentRequired(product)).toBe(false); +}); ``` -**Summary of the Four Approaches:** - -| Approach | When to Use | Flexibility | Complexity | -| ---------------------------------------- | ----------------------------------------------------- | ----------- | ---------- | -| **1. Static Template** | Fixed requirements that never change | Low | Low | -| **2. Dynamic via `setRequiredFields()`** | Requirements change based on runtime conditions | High | Medium | -| **3. Constructor Parameter** | Requirements known at builder creation time | Medium | Medium | -| **4. Hybrid (Static + Dynamic)** | Common base requirements + context-specific additions | High | Higher | - -### Building Test Data - -Perfect for creating test fixtures with sensible defaults: - -```typescript -type Product = { - name: string; - price: number; - category: string; - tags?: string[]; -}; +## ⚠️ Limitations -class ProductBuilder extends CeriosBuilder { - static create() { - return new ProductBuilder({}); - } +- Class data properties that are function-typed (e.g. `onChange: (x: number) => void`) are treated like methods and get no setter — there's no reliable way to distinguish them from methods at the type level. +- A custom method cannot have the same name as a property; use a distinct name that delegates to the generated setter (see [Method names](#method-names)). +- Inside a **generic** base builder (e.g. `class Base extends ...`), the compile-time gate cannot resolve until `T` is concrete — the same rule as for hand-written builders. To share logic across derived types, use a [base-builder function](#sharing-custom-methods-the-base-builder-function) instead. - name(value: string) { - return this.setProperty("name", value); - } +## 🗑️ Deprecated: `CeriosBuilder` and `CeriosClassBuilder` - price(value: number) { - return this.setProperty("price", value); - } +The hand-written builder base classes — where you write a `setProperty` wrapper per property — are **deprecated and will be removed in the next major version**. They still work in this release, unchanged, including `setNestedProperty` and `addToArrayProperty`. - category(value: string) { - return this.setProperty("category", value); - } +**Migrate to the auto builders**: see **[MIGRATION.md](MIGRATION.md)** for a per-feature before/after guide, including: - addTag(tag: string) { - return this.addToArrayProperty("tags", tag); - } +- dropping setter wrappers and constructor plumbing, +- moving `static requiredTemplate` / `requiredDataProperties` to constructor options, +- replacing `setNestedProperty` with the [director pattern](#the-director-pattern), +- replacing `addToArrayProperty` with a custom method on the root setter. - // Custom methods for testing - asElectronics() { - return this.category("Electronics").price(299.99); - } +## 📚 API Reference - asFreeProduct() { - return this.price(0); - } +### Factories - withRandomName() { - const names = ["Widget", "Gadget", "Tool", "Device"]; - return this.name(names[Math.floor(Math.random() * names.length)]); - } -} +- `CeriosAutoBuilder()` — returns an abstract base class with an auto setter per property of `T`. +- `CeriosClassAutoBuilder(Class)` — the same for a class; building instantiates `Class`. Returns the same (memoised) base class for the same `Class`, so `instanceof` works across calls. -// In your tests -const testProduct = ProductBuilder.create().withRandomName().asElectronics().addTag("featured").addTag("sale").build(); -``` +### Constructor -### Building Complex Nested Objects +Both auto builders share one constructor shape, with the options named by the exported `BuilderInit` type: ```typescript -type Address = { - street: string; - city: string; - country: string; - zipCode?: string; -}; +new Builder(data?: Partial, init?: BuilderInit) -type Customer = { - id: string; - name: string; - address: Address; - phoneNumber?: string; - notes?: string[]; +type BuilderInit = { + requiredFields?: ReadonlyArray> | RequiredFieldsRecord; + validators?: Array<(obj: Partial) => boolean | string>; }; +``` -class AddressBuilder extends CeriosBuilder
{ - static create() { - return new AddressBuilder({}); - } - - static createWithDefaults() { - return this.create().setProperties({ - city: "Othertown", - country: "United States", - }); - } - - // NEW: Create with ALL required fields set - static createComplete() { - return this.create().street("123 Default Street").city("Othertown").country("United States"); - } - - street(value: string) { - return this.setProperty("street", value); - } - - city(value: string) { - return this.setProperty("city", value); - } +### Instance methods - country(value: string) { - return this.setProperty("country", value); - } +`build()`, `buildWithoutRuntimeValidation()`, `buildWithoutCompileTimeValidation()`, `buildUnsafe()`, `buildPartial()`, `buildFrozen()`, `buildDeepFrozen()`, `buildSealed()`, `buildDeepSealed()`, `addValidator()`, `setRequiredFields()`, `removeOptionalProperty()`, `clearOptionalProperties()`, `clone()` — see the sections above. - zipCode(value: string) { - return this.setProperty("zipCode", value); - } +### Statics - asUSAddress() { - return this.country("United States"); - } -} - -class CustomerBuilder extends CeriosBuilder { - static create() { - return new CustomerBuilder({}); - } - - id(value: string) { - return this.setProperty("id", value); - } - - name(value: string) { - return this.setProperty("name", value); - } - - address(value: Address) { - return this.setProperty("address", value); - } - - phoneNumber(value: string) { - return this.setProperty("phoneNumber", value); - } - - addNote(note: string) { - return this.addToArrayProperty("notes", note); - } - - // Pattern 1: No defaults - must set all required fields - withAddress(builderFn: BuilderComposerFromFactory) { - const address = builderFn(AddressBuilder.create()).build(); - return this.setProperty("address", address); - } - - // Pattern 2: Partial defaults - must set remaining required fields (street) - withAddressDefaults(builderFn: BuilderComposerFromFactory) { - const address = builderFn(AddressBuilder.createWithDefaults()).build(); - return this.setProperty("address", address); - } - - // Pattern 3: Complete defaults - callback is OPTIONAL - withCompleteAddress(builderFn?: BuilderComposerFromFactory) { - const builder = AddressBuilder.createComplete(); - const address = builderFn ? builderFn(builder).build() : builder.build(); - return this.setProperty("address", address); - } -} -``` - -#### Using `BuilderComposerFromFactory<>` for Simpler Type Signatures - -When accepting builders with defaults already set, use `BuilderComposerFromFactory<>` to infer callback input/output types from the factory automatically. - -```typescript -import { BuilderComposerFromFactory, CeriosBuilder } from "@cerios/cerios-builder"; - -class CustomerBuilder extends CeriosBuilder { - // No manual Pick<> or ReturnType<> needed: - withAddressDefaults(builderFn: BuilderComposerFromFactory) { - const address = builderFn(AddressBuilder.createWithDefaults()).build(); - return this.setProperty("address", address); - } -} -``` - -**Benefits:** - -- ✅ No need to manually track which fields are set with `Pick<>` -- ✅ Automatically infers callback input/output from your factory method -- ✅ Changes to `createWithDefaults()` automatically update the type -- ✅ Much cleaner and more maintainable - -**Usage examples:** - -```typescript -// Pattern 1: No defaults - must set all required fields -const customer1 = CustomerBuilder.create() - .id("CUST-001") - .name("Alice Johnson") - .withAddress((addr) => addr.street("123 Main St").city("New York").asUSAddress().zipCode("10001")) - .addNote("VIP customer") - .phoneNumber("+1-555-0123") - .build(); - -// Pattern 2: Partial defaults - city and country already set, must provide street -const customer2 = CustomerBuilder.create() - .id("CUST-002") - .name("Bob Smith") - .withAddressDefaults((addr) => addr.street("456 Elm St")) - .addNote("New customer") - .build(); - -console.log(customer2); -// Output: { id: 'CUST-002', name: 'Bob Smith', address: { street: '456 Elm St', city: 'Othertown', country: 'United States' }, notes: ['New customer'] } - -// Pattern 3a: Complete defaults - no callback needed, use all defaults -const customer3 = CustomerBuilder.create() - .id("CUST-003") - .name("Charlie Davis") - .withCompleteAddress() // ✅ No callback needed - all required fields already set! - .build(); - -console.log(customer3); -// Output: { id: 'CUST-003', name: 'Charlie Davis', address: { street: '123 Default Street', city: 'Othertown', country: 'United States' } } - -// Pattern 3b: Complete defaults - optional callback to modify/add optional fields -const customer4 = CustomerBuilder.create() - .id("CUST-004") - .name("Diana Evans") - .withCompleteAddress( - (addr) => addr.zipCode("90210"), // Only modify/add optional fields if needed - ) - .build(); - -console.log(customer4); -// Output: { id: 'CUST-004', name: 'Diana Evans', address: { street: '123 Default Street', city: 'Othertown', country: 'United States', zipCode: '90210' } } - -// Pattern 3c: Complete defaults - optional callback to override any field -const customer5 = CustomerBuilder.create() - .id("CUST-005") - .name("Eve Foster") - .withCompleteAddress((addr) => - addr - .street("789 Custom Ave") // Override the default street - .zipCode("10001"), - ) - .addNote("Premium customer") - .build(); - -console.log(customer5); -// Output: { id: 'CUST-005', name: 'Eve Foster', address: { street: '789 Custom Ave', city: 'Othertown', country: 'United States', zipCode: '10001' }, notes: ['Premium customer'] } -``` - -#### Using `createWithDefaults()` with Nested Builders - -The key pattern is: - -1. **`createComplete()`**: Returns a builder with **all required fields already set** -2. **Optional callback parameter**: The callback parameter is typed as `optional` (`?`), so you don't have to provide it -3. **Already buildable**: Since all required fields are set, you can call `build()` immediately - -This pattern is perfect when: - -- You have sensible defaults for all required fields -- Most users will use the defaults -- Some users may want to customize optional fields or override defaults -- You want a clean API without forcing users to write empty callbacks - -**Comparison of the three patterns:** - -```typescript -// Pattern 1: Must set ALL required fields -.withAddress(addr => addr.street('...').city('...').country('...')) - -// Pattern 2: Must set REMAINING required fields (street) -.withAddressDefaults(addr => addr.street('...')) - -// Pattern 3a: NO callback needed - all defaults -.withCompleteAddress() - -// Pattern 3b: OPTIONAL callback - only if you want to customize -.withCompleteAddress(addr => addr.zipCode('...')) - -// Pattern 3c: OPTIONAL callback - can override any field -.withCompleteAddress(addr => addr.street('...').zipCode('...')) -``` - -## 🔍 Custom Validators - -Cerios Builder supports custom validation logic that runs when you build an object. Validators give you fine-grained control over business rules and constraints beyond simple "field required" checks. - -### Adding Validators - -Use the `addValidator()` method to add custom validation logic. Validators receive the partial object being built and can: - -- Return `true` if validation passes -- Return `false` if validation fails (generic error) -- Return a `string` error message for detailed feedback - -```typescript -type User = { - name: string; - email: string; - age?: number; - password?: string; - confirmPassword?: string; -}; - -class UserBuilder extends CeriosBuilder { - static requiredTemplate: RequiredFieldsTemplate = ["name", "email"]; - - static create() { - return new UserBuilder({}); - } - - name(value: string) { - return this.setProperty("name", value); - } - - email(value: string) { - return this.setProperty("email", value); - } - - age(value: number) { - return this.setProperty("age", value); - } - - password(value: string) { - return this.setProperty("password", value); - } - - confirmPassword(value: string) { - return this.setProperty("confirmPassword", value); - } -} - -// Example 1: Simple boolean validation -const user1 = UserBuilder.create() - .name("John Doe") - .email("john@example.com") - .age(20) - .addValidator((obj) => (obj.age ?? 0) >= 18) // Returns true/false - .build(); // ✅ Passes - age is >= 18 - -// Example 2: Validation with custom error messages -try { - const user2 = UserBuilder.create() - .name("Jane Smith") - .email("invalid-email") - .addValidator((obj) => (obj.email?.includes("@") ? true : "Email must contain @")) - .build(); -} catch (error) { - console.error(error.message); - // Output: "Validation failed: Email must contain @" -} - -// Example 3: Multiple validators -const user3 = UserBuilder.create() - .name("Bob Johnson") - .email("bob@example.com") - .age(25) - .password("secret123") - .confirmPassword("secret123") - .addValidator((obj) => (obj.age ?? 0) >= 18 || "User must be at least 18 years old") - .addValidator((obj) => obj.email?.includes("@") || "Invalid email format") - .addValidator((obj) => { - if (obj.password && obj.confirmPassword) { - return obj.password === obj.confirmPassword || "Passwords do not match"; - } - return true; // Skip validation if passwords not set - }) - .build(); // ✅ All validators pass -``` - -### Validator Return Types - -| Return Type | Meaning | Example | -| ----------- | ------------------------------ | ----------------------------------------- | -| `true` | Validation passed | `obj => obj.age >= 18` | -| `false` | Validation failed (generic) | `obj => obj.price > 0` | -| `string` | Validation failed with message | `obj => obj.age >= 18 \|\| 'Must be 18+'` | - -### Common Validation Patterns - -#### 1. Age Validation - -```typescript -const user = UserBuilder.create() - .name("Alice") - .email("alice@example.com") - .age(16) - .addValidator((obj) => { - if (obj.age === undefined) return true; // Optional field - return obj.age >= 18 || "User must be at least 18 years old"; - }) - .build(); -``` - -#### 2. Email Format Validation - -```typescript -const user = UserBuilder.create() - .name("Charlie") - .email("charlie@example.com") - .addValidator((obj) => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(obj.email || "") || "Invalid email format"; - }) - .build(); -``` - -#### 3. Password Strength Validation - -```typescript -const user = UserBuilder.create() - .name("Diana") - .email("diana@example.com") - .password("SecurePass123!") - .addValidator((obj) => { - if (!obj.password) return true; // Skip if not set - - const hasUpperCase = /[A-Z]/.test(obj.password); - const hasLowerCase = /[a-z]/.test(obj.password); - const hasNumber = /[0-9]/.test(obj.password); - const hasSpecialChar = /[!@#$%^&*]/.test(obj.password); - const isLongEnough = obj.password.length >= 8; - - if (!isLongEnough) return "Password must be at least 8 characters"; - if (!hasUpperCase) return "Password must contain an uppercase letter"; - if (!hasLowerCase) return "Password must contain a lowercase letter"; - if (!hasNumber) return "Password must contain a number"; - if (!hasSpecialChar) return "Password must contain a special character"; - - return true; - }) - .build(); -``` - -#### 4. Cross-Field Validation - -```typescript -type Order = { - totalAmount: number; - discountAmount?: number; - finalAmount: number; -}; - -class OrderBuilder extends CeriosBuilder { - // ... methods -} - -const order = OrderBuilder.create() - .totalAmount(100) - .discountAmount(20) - .finalAmount(80) - .addValidator((obj) => { - const expected = (obj.totalAmount || 0) - (obj.discountAmount || 0); - return obj.finalAmount === expected || `Final amount must be ${expected} (total - discount)`; - }) - .build(); -``` - -#### 5. Conditional Required Fields - -```typescript -type Employee = { - firstName: string; - lastName: string; - employeeType: "full-time" | "contractor"; - employeeId?: string; - contractorId?: string; -}; - -class EmployeeBuilder extends CeriosBuilder { - static requiredTemplate: RequiredFieldsTemplate = ["firstName", "lastName", "employeeType"]; - - // ... methods -} - -const employee = EmployeeBuilder.create() - .firstName("John") - .lastName("Doe") - .employeeType("full-time") - .employeeId("EMP-12345") - .addValidator((obj) => { - if (obj.employeeType === "full-time") { - return obj.employeeId !== undefined || "Full-time employees must have an employeeId"; - } - if (obj.employeeType === "contractor") { - return obj.contractorId !== undefined || "Contractors must have a contractorId"; - } - return true; - }) - .build(); -``` - -#### 6. Range Validation - -```typescript -type Product = { - name: string; - price: number; - quantity?: number; -}; - -class ProductBuilder extends CeriosBuilder { - // ... methods -} - -const product = ProductBuilder.create() - .name("Widget") - .price(29.99) - .quantity(5) - .addValidator((obj) => obj.price > 0 || "Price must be positive") - .addValidator((obj) => obj.price < 10000 || "Price cannot exceed $10,000") - .addValidator((obj) => { - if (obj.quantity === undefined) return true; - return obj.quantity >= 0 || "Quantity cannot be negative"; - }) - .build(); -``` - -### Validator Execution - -- **Validators run during build**: All validators are executed when you call `build()` or `buildWithoutCompileTimeValidation()` -- **Order matters**: Validators run in the order they were added -- **First failure stops**: If a validator fails, an error is thrown immediately with that validator's message -- **No validation for unsafe builds**: `buildUnsafe()` and `buildPartial()` skip all validators - -```typescript -// Validators run for these build methods: -builder.build(); // ✅ Runs validators -builder.buildWithoutCompileTimeValidation(); // ✅ Runs validators - -// Validators are skipped for these: -builder.buildWithoutRuntimeValidation(); // ❌ Skips validators -builder.buildUnsafe(); // ❌ Skips validators -builder.buildPartial(); // ❌ Skips validators -``` - -### Combining Validators with Required Fields - -Validators work alongside the `requiredTemplate` for comprehensive validation: - -```typescript -type Registration = { - username: string; - email: string; - age?: number; - acceptedTerms?: boolean; -}; - -class RegistrationBuilder extends CeriosBuilder { - static requiredTemplate: RequiredFieldsTemplate = ["username", "email"]; - - // ... methods -} - -const registration = RegistrationBuilder.create() - .username("john_doe") - .email("john@example.com") - .age(25) - .acceptedTerms(true) - // Required template validates username and email are present - // Custom validators check business rules - .addValidator((obj) => (obj.username && obj.username.length >= 3) || "Username must be at least 3 characters") - .addValidator((obj) => obj.email?.includes("@") || "Invalid email format") - .addValidator((obj) => !obj.age || obj.age >= 13 || "Must be at least 13 years old") - .addValidator((obj) => obj.acceptedTerms === true || "Must accept terms and conditions") - .build(); -// First checks: username and email are set (requiredTemplate) -// Then runs: all custom validators in order -``` - -### Reusable Validators - -Create reusable validator functions for common patterns: - -```typescript -// Reusable validator functions -const validators = { - email: (obj: Partial) => obj.email?.includes("@") || "Invalid email format", - - minAge: (minAge: number) => (obj: Partial) => - !obj.age || obj.age >= minAge || `Must be at least ${minAge} years old`, - - passwordMatch: (obj: Partial) => { - if (!obj.password || !obj.confirmPassword) return true; - return obj.password === obj.confirmPassword || "Passwords do not match"; - }, - - required: - (field: K, message?: string) => - (obj: Partial) => - obj[field] !== undefined || message || `${String(field)} is required`, -}; - -// Use them in your builders -const user = UserBuilder.create() - .name("John Doe") - .email("john@example.com") - .age(20) - .password("secret123") - .confirmPassword("secret123") - .addValidator(validators.email) - .addValidator(validators.minAge(18)) - .addValidator(validators.passwordMatch) - .build(); -``` - -### Testing with Validators - -Validators are perfect for test data creation with realistic constraints: - -```typescript -describe("User Registration", () => { - const createValidUser = () => - UserBuilder.create() - .name("Test User") - .email("test@example.com") - .age(25) - .addValidator((obj) => (obj.age ?? 0) >= 18 || "Must be 18+") - .addValidator((obj) => obj.email?.includes("@") || "Invalid email"); - - test("should accept valid user", () => { - const user = createValidUser().build(); - expect(user.age).toBeGreaterThanOrEqual(18); - }); - - test("should reject underage user", () => { - expect(() => { - createValidUser().age(16).build(); - }).toThrow("Must be 18+"); - }); - - test("should reject invalid email", () => { - expect(() => { - createValidUser().email("invalid-email").build(); - }).toThrow("Invalid email"); - }); -}); -``` - -## 🧪 Testing Integration - -Cerios Builder is perfect for test data creation: - -```typescript -describe("User Service", () => { - test("should create user with valid data", () => { - const userData = UserBuilder.create() - .withRandomId() - .name("Test User") - .email("test@example.com") - .addRole("tester") - .age(25) - .build(); - - const result = userService.createUser(userData); - - expect(result.success).toBe(true); - expect(result.user.name).toBe("Test User"); - expect(result.user.roles).toContain("tester"); - }); - - test("should handle users without age", () => { - const userData = UserBuilder.create() - .withRandomId() - .name("Ageless User") - .email("ageless@example.com") - .addRole("guest") - .build(); - - const result = userService.createUser(userData); - - expect(result.success).toBe(true); - expect(result.user.age).toBeUndefined(); - expect(result.user.roles).toContain("guest"); - }); -}); -``` - -## 🆚 Why Cerios Builder? - -### Traditional Object Creation - -```typescript -// ❌ No compile-time safety -const order = { - Order: { - Details: { - CustomerId: "CUST-001", - TotalAmount: 299.99, - // Oops! Forgot required Status field - ShippingAddress: { - Street: "123 Main St", - // Oops! Forgot required City and Country - }, - }, - }, -}; - -// ❌ Runtime error waiting to happen -orderService.createOrder(order); -``` - -### With Cerios Builder - -```typescript -// ✅ Type-safe nested property building -const order = OrderRequestBuilder.createWithDefaults() - .customerId("CUST-001") - .totalAmount(299.99) - .shippingStreet("123 Main St") - .shippingCity("New York") - .shippingCountry("USA") - .build(); // ✅ Both compile-time and runtime validation - -// ✅ Clear error messages at runtime -try { - const invalid = OrderRequestBuilder.createWithDefaults().customerId("CUST-002").buildWithoutCompileTimeValidation(); -} catch (error) { - console.error(error.message); - // "Missing required fields: Order.Details.TotalAmount, - // Order.Details.ShippingAddress.Street, ..." -} -``` - -### Benefits - -- **Type-Safe Paths**: Dot notation with full IntelliSense support -- **Flexible Validation**: Choose compile-time, runtime, both, or neither -- **Dynamic Requirements**: Static templates + dynamic required fields -- **Deep Nesting**: Handle complex nested structures easily -- **Developer Experience**: Better autocomplete and error messages - -## 📚 API Reference - -### CeriosBuilder - -Base class for all builders. - -#### Static Properties - -- `requiredTemplate?: RequiredFieldsTemplate` - Optional array of required field paths for runtime validation - -#### Instance Methods - -- `setProperty(key: K, value: T[K])` - Sets a property and returns a new builder instance -- `setProperties(props: Pick)` - Sets multiple properties at once and returns a new builder instance -- `setNestedProperty

(path: P, value: PathValue)` - Sets a deeply nested property using dot notation -- `addToArrayProperty(key: K, value: V)` - Adds a value to an array property and returns a new builder instance -- `setRequiredFields(fields: ReadonlyArray>)` - Sets required fields dynamically for this instance -- `addValidator(validator: (obj: Partial) => boolean | string)` - Adds a custom validator that runs during build -- `removeOptionalProperty(key: K)` - Removes an optional property from the builder -- `clearOptionalProperties()` - Clears all optional properties, keeping only required ones -- `clone()` - Creates a clone of the current builder instance -- `static from(instance: T)` - Creates a new builder from an existing object - -#### Build Methods - -- **`build()`** - Builds with **both compile-time and runtime validation** (recommended) - - Compile-time: TypeScript enforces all required properties are set - - Runtime: Validates all fields in the `requiredTemplate` - - Throws error if any required field is missing - -- **`buildWithoutRuntimeValidation()`** - Builds with **compile-time validation only** - - Compile-time: TypeScript enforces all required properties are set - - Runtime: No validation (better performance) - - Use when you trust the type system and need speed - -- **`buildWithoutCompileTimeValidation()`** - Builds with **runtime validation only** - - Compile-time: No TypeScript enforcement - - Runtime: Validates all fields in the `requiredTemplate` - - Use when building from external data where compile-time checks aren't possible - - Throws error if any required field is missing - -- **`buildUnsafe()`** - Builds **without any validation** - - Compile-time: No TypeScript enforcement - - Runtime: No validation - - Use only when you're certain the object is valid and need maximum performance - - Returns potentially incomplete object - -- **`buildPartial()`** - Builds a **partial object** - - Returns `Partial` with currently set properties - - Useful for progressive form filling or template objects - -### RequiredFieldsTemplate - -Type-safe array of paths for defining required fields. - -```typescript -type RequiredFieldsTemplate = ReadonlyArray>; -``` - -Example: - -```typescript -static requiredTemplate: RequiredFieldsTemplate = [ - 'Order.Details.CustomerId', - 'Order.Details.TotalAmount', - 'Order.Details.ShippingAddress.Street', -]; -``` - -### Path - -Type utility that generates all valid dot-notation paths for a type. - -```typescript -// For a type like: -type User = { - profile: { - name: string; - email: string; - }; -}; - -// Path includes: -// 'profile' | 'profile.name' | 'profile.email' -``` - -### CeriosBrand - -Deprecated for direct consumer usage. Prefer `BuilderStep`, `BuilderPreset`, `BuilderComposer`, and `BuilderComposerFromFactory`. - -`CeriosBrand` remains exported for backward compatibility. - -## 🛠️ Build Methods Explained - -### `build()` - Full Validation (Recommended) - -Use `build()` when you want **both compile-time and runtime validation**. This is the safest option. - -```typescript -class UserBuilder extends CeriosBuilder { - static requiredTemplate: RequiredFieldsTemplate = ["id", "name", "email"]; - // ... methods -} - -const user = UserBuilder.create().id("123").name("John").email("john@example.com").build(); // ✅ TypeScript + runtime validation - -// ❌ TypeScript error - missing email -const invalid1 = UserBuilder.create().id("123").name("John").build(); - -// ❌ Runtime error - missing email -try { - const invalid2 = UserBuilder.create().id("123").name("John").build(); -} catch (error) { - console.error(error.message); // "Missing required fields: email" -} -``` - -**Use when:** - -- You want maximum safety -- Building critical business objects -- You have a required template defined - -### `buildWithoutRuntimeValidation()` - Compile-Time Only - -Use when you want **TypeScript safety** but need to skip runtime validation for performance. - -```typescript -const user = UserBuilder.create().id("123").name("John").email("john@example.com").buildWithoutRuntimeValidation(); // ✅ Fast build, no runtime check - -// ❌ Still won't compile - TypeScript enforces types -const invalid = UserBuilder.create().id("123").name("John").buildWithoutRuntimeValidation(); -``` - -**Use when:** - -- Performance is critical -- You trust the type system -- Building many objects in loops - -### `buildWithoutCompileTimeValidation()` - Runtime Only - -Use when building from **external data** where compile-time checks aren't possible. - -```typescript -function buildFromAPI(data: any) { - const builder = UserBuilder.create(); - - if (data.id) builder = builder.id(data.id); - if (data.name) builder = builder.name(data.name); - if (data.email) builder = builder.email(data.email); - - return builder.buildWithoutCompileTimeValidation(); // Runtime validation -} - -// ✅ Valid data passes -const user = buildFromAPI({ id: "123", name: "John", email: "john@example.com" }); - -// ❌ Invalid data throws -try { - const invalid = buildFromAPI({ id: "123", name: "John" }); // Missing email -} catch (error) { - console.error(error.message); // "Missing required fields: email" -} -``` - -**Use when:** - -- Building from API responses -- Processing user input -- Working with dynamic data -- Need runtime validation without TypeScript constraints - -### `buildUnsafe()` - No Validation - -Use **only when you're certain** the object is valid and need maximum performance. - -```typescript -const user = UserBuilder.create().id("123").buildUnsafe(); // ⚠️ No checks at all - -console.log(user); // { id: '123' } - potentially incomplete! -``` - -**Use when:** - -- You're absolutely certain the data is valid -- Maximum performance is required -- Building throwaway test objects - -### `buildPartial()` - Partial Objects - -Use when you need **incomplete objects**. - -```typescript -const partial = UserBuilder.create().name("Incomplete User").buildPartial(); // Returns Partial - -console.log(partial); // { name: 'Incomplete User' } -``` - -**Use when:** - -- Progressive form filling -- Template objects -- Partial updates -- Inspecting builder state - -### Comparison Table - -| Method | Compile-Time Check | Runtime Check | Performance | Safety | Use Case | -| ------------------------------------- | ------------------ | ------------- | ----------- | ------- | ---------------------- | -| `build()` | ✅ | ✅ | Medium | Highest | Production code | -| `buildWithoutRuntimeValidation()` | ✅ | ❌ | Fast | High | Performance-critical | -| `buildWithoutCompileTimeValidation()` | ❌ | ✅ | Medium | Medium | External data | -| `buildUnsafe()` | ❌ | ❌ | Fastest | Lowest | Trusted scenarios only | -| `buildPartial()` | ❌ | ❌ | Fast | N/A | Incomplete objects | - -## 🧊 Immutable Build Methods - -In addition to the standard build methods, `@cerios/cerios-builder` provides **frozen** and **sealed** variants that create immutable objects. These methods help prevent accidental mutations and enforce data integrity. - -### Understanding Freeze vs Seal - -Before diving into the methods, it's important to understand the difference between **frozen** and **sealed** objects: - -| Feature | `Object.freeze()` | `Object.seal()` | -| --------------------- | --------------------- | -------------------------- | -| **Add properties** | ❌ Prevented | ❌ Prevented | -| **Delete properties** | ❌ Prevented | ❌ Prevented | -| **Modify properties** | ❌ Prevented | ✅ Allowed | -| **Use case** | Complete immutability | Prevent structural changes | - -**Shallow vs Deep:** - -- **Shallow**: Only applies to the top-level object -- **Deep**: Recursively applies to all nested objects and arrays - -### `buildFrozen()` - Shallow Freeze - -Creates a **shallowly frozen** object where top-level properties cannot be modified, but nested objects remain mutable. - -```typescript -const user = UserBuilder.create().id("123").name("John").email("john@example.com").buildFrozen(); // Returns Readonly - -// ❌ Error: Cannot modify top-level properties -user.name = "Jane"; // TypeError in strict mode - -// ⚠️ Nested objects can still be modified (shallow freeze) -user.address.city = "New York"; // Works if address is an object -``` - -**Use when:** - -- You want to prevent accidental top-level modifications -- Nested objects are intentionally mutable -- Performance is a concern (shallow operations are faster) - -### `buildDeepFrozen()` - Deep Freeze - -Creates a **deeply frozen** object where all properties at all levels are immutable. - -```typescript -const user = UserBuilder.create() - .id("123") - .name("John") - .email("john@example.com") - .setNestedProperty("address.city", "Boston") - .buildDeepFrozen(); // Returns DeepReadonly - -// ❌ Error: Cannot modify top-level properties -user.name = "Jane"; // TypeError - -// ❌ Error: Cannot modify nested properties -user.address.city = "New York"; // TypeError - -// ❌ Error: Cannot modify arrays -user.tags.push("new tag"); // TypeError -``` - -**Use when:** - -- You need complete immutability -- Building configuration objects -- Creating frozen state snapshots -- Working with Redux/immutable state patterns -- Passing objects to untrusted code - -### `buildSealed()` - Shallow Seal - -Creates a **shallowly sealed** object where properties cannot be added or removed, but existing properties can be modified. - -```typescript -const user = UserBuilder.create().id("123").name("John").email("john@example.com").buildSealed(); // Returns T - -// ✅ Can modify existing properties -user.name = "Jane"; // Works! - -// ❌ Cannot add new properties -user.newProp = "value"; // TypeError in strict mode - -// ❌ Cannot delete properties -delete user.email; // TypeError in strict mode - -// ⚠️ Can modify nested objects (shallow seal) -user.address.city = "New York"; // Works if address is an object -``` - -**Use when:** - -- You want to lock the object structure -- Properties should be modifiable -- Preventing accidental property additions -- Building objects with fixed schemas - -### `buildDeepSealed()` - Deep Seal - -Creates a **deeply sealed** object where no properties can be added or removed at any level, but all properties can still be modified. - -```typescript -const user = UserBuilder.create() - .id("123") - .name("John") - .email("john@example.com") - .setNestedProperty("address.city", "Boston") - .buildDeepSealed(); // Returns T - -// ✅ Can modify properties at all levels -user.name = "Jane"; // Works -user.address.city = "New York"; // Works - -// ❌ Cannot add properties at any level -user.newProp = "value"; // TypeError -user.address.newProp = "value"; // TypeError - -// ❌ Cannot add array elements -user.tags.push("new tag"); // TypeError - -// ✅ Can modify array elements -user.tags[0] = "updated tag"; // Works -``` - -**Use when:** - -- You want to lock all object structures -- Values should be modifiable -- Preventing structural changes throughout the tree -- Building objects with deeply fixed schemas - -### Immutability Comparison - -| Method | Modify Top-Level | Modify Nested | Add Properties | Delete Properties | Performance | -| ------------------- | ---------------- | ------------- | -------------- | ----------------- | ----------- | -| `buildFrozen()` | ❌ | ✅ | ❌ | ❌ | Fast | -| `buildDeepFrozen()` | ❌ | ❌ | ❌ | ❌ | Slower | -| `buildSealed()` | ✅ | ✅ | ❌ | ❌ | Fast | -| `buildDeepSealed()` | ✅ | ✅ | ❌ | ❌ | Slower | - -### Immutability Examples - -#### Example 1: Configuration Object - -```typescript -class ConfigBuilder extends CeriosBuilder { - static requiredTemplate: RequiredFieldsTemplate = ["apiUrl", "timeout", "features.authentication"]; - - static create() { - return new ConfigBuilder({}); - } - - // ... builder methods -} - -const config = ConfigBuilder.create() - .setApiUrl("https://api.example.com") - .setTimeout(5000) - .setNestedProperty("features.authentication", true) - .buildDeepFrozen(); // Configuration should never change - -// ❌ Configuration is completely frozen -config.timeout = 10000; // TypeError -config.features.authentication = false; // TypeError -``` - -#### Example 2: Mutable Data with Fixed Structure - -```typescript -class UserStateBuilder extends CeriosBuilder { - static create() { - return new UserStateBuilder({}); - } - - // ... builder methods -} - -const userState = UserStateBuilder.create() - .setUserId("123") - .setStatus("active") - .setLastSeen(new Date()) - .buildDeepSealed(); // Structure is fixed, but values can change - -// ✅ Can update state values -userState.status = "inactive"; -userState.lastSeen = new Date(); - -// ❌ Cannot add new properties -userState.newField = "value"; // TypeError -``` - -#### Example 3: Snapshot for Time Travel Debugging - -```typescript -class AppStateBuilder extends CeriosBuilder { - // ... methods -} - -const snapshots: DeepReadonly[] = []; - -// Capture immutable snapshots -snapshots.push(AppStateBuilder.create().setCurrentUser(user).setOpenDocuments(documents).buildDeepFrozen()); - -// Snapshots cannot be modified -snapshots[0].currentUser.name = "Changed"; // TypeError -``` - -### When to Use Immutability - -**Use `buildDeepFrozen()`:** - -- ✅ Configuration objects -- ✅ State snapshots -- ✅ Event data -- ✅ Cached results -- ✅ Objects passed to untrusted code - -**Use `buildDeepSealed()`:** - -- ✅ State objects with fixed schemas -- ✅ Form data models -- ✅ Database records with fixed columns -- ✅ API response models - -**Use shallow variants (`buildFrozen()`, `buildSealed()`):** - -- ✅ When nested objects need flexibility -- ✅ Performance-critical scenarios -- ✅ Intentional selective mutability - -**Avoid immutable builds when:** - -- ❌ Building frequently updated objects -- ❌ Working with large data structures (performance cost) -- ❌ Objects that need frequent transformations - -## 💡 Best Practices - -1. **Use `setNestedProperty()` for deep structures**: Instead of building nested objects separately, use dot notation for better type safety and cleaner code. - -2. **Define `requiredTemplate` for runtime validation**: When working with external data or complex validation rules, define a required template. - -3. **Choose the right build method**: - - Default to `build()` for safety - - Use `buildWithoutRuntimeValidation()` for performance - - Use `buildWithoutCompileTimeValidation()` for external data - - Use `buildUnsafe()` only when absolutely necessary - - Use `buildDeepFrozen()` for configuration and immutable data - - Use `buildDeepSealed()` when you need fixed schemas with mutable values - -4. **Use `setRequiredFields()` for conditional requirements**: When requirements change based on context (e.g., new vs existing records), use dynamic required fields. - -5. **Create factory methods**: Use static `create()` and `createWithDefaults()` methods for better ergonomics: - - ```typescript - static createWithDefaults() { - return this.create() - .status('pending') - .createdAt(new Date()); - } - ``` - -6. **Combine with validation libraries**: Use runtime validation with libraries like Zod for comprehensive validation: - - ```typescript - const data = builder.buildWithoutCompileTimeValidation(); - const validated = orderSchema.parse(data); // Zod validation - ``` - -7. **Keep builders focused**: One builder per entity type - don't try to handle multiple unrelated types in one builder. - -8. **Use type-safe paths**: The `RequiredFieldsTemplate` type ensures you can only specify valid paths: - ```typescript - static requiredTemplate: RequiredFieldsTemplate = [ - 'Details.CustomerId', // ✅ Valid path - 'Details.InvalidField', // ❌ TypeScript error - ]; - ``` - -## 🧩 CeriosClassBuilder (Work in Progress) - -`CeriosClassBuilder` is an experimental builder base class for **TypeScript classes** (not just types/interfaces). It provides the same fluent, type-safe builder API as `CeriosBuilder`, but returns actual class instances—preserving methods, decorators, and prototype chains. - -### Key Features - -- **Class Instance Output**: Returns real class instances, not plain objects. -- **Decorator Support**: Works with classes using decorators (e.g., for serialization). -- **Type-Safe Chaining**: Same compile-time safety and method chaining as `CeriosBuilder`. -- **Nested Property Support**: Supports `setNestedProperty` for deep class graphs. - -### Example - -```typescript -import { CeriosClassBuilder } from "@cerios/cerios-builder"; - -class Person { - name!: string; - age!: number; - greet() { - return `Hello, I'm ${this.name}`; - } -} - -class PersonBuilder extends CeriosClassBuilder { - constructor(classConstructor: ClassConstructor = Person, data: Partial = {}) { - super(classConstructor, data); - } - - static create() { - return new PersonBuilder(Person); - } - name(value: string) { - return this.setProperty("name", value); - } - age(value: number) { - return this.setProperty("age", value); - } -} - -const person = PersonBuilder.create().name("Alice").age(30).build(); -console.log(person.greet()); // "Hello, I'm Alice" -console.log(person instanceof Person); // true -``` +- `Builder.from(instance)` — seeds a new builder from an existing object/instance (deep-cloned), immediately buildable. -### Status +### Helper types -- ⚠️ **Work in progress:** API and features may change. -- Feature parity with `CeriosBuilder` is the goal. -- Please report issues or suggestions! +- `BuilderWith` / `ClassBuilderWith` — explicit return types for factories and custom methods; `S` is the union of keys set, omit for "fully buildable". +- `AutoBuilderBase` / `ClassAutoBuilderBase` — constraint for [base-builder functions](#sharing-custom-methods-the-base-builder-function) sharing logic across derived types. +- `BuilderExtension` — return type of a base-builder function; keeps the derived builder's setters and statics visible. +- `BuilderComposerFromFactory` / `ClassBuilderComposerFromFactory` — callback types for nested-builder composition, inferred from a factory. +- `BuildGate` / `ClassBuildGate` — the `this` constraint on the validated build variants; dissolves for all-optional types. +- `BuilderInit` — the named `{ requiredFields?, validators? }` constructor-options type both auto builders accept. +- `AutoBuilderConstructor` / `ClassAutoBuilderConstructor` — the abstract constructor type a factory returns; name it when storing or passing a factory result. +- `AutoBuilderApi` / `ClassAutoBuilderApi` — the build/validate/clone instance API every auto builder exposes, without the generated setters. +- `AutoSetters` / `ClassAutoSetters` — the generated bare-name setters (one per property; the class variant covers data properties only). +- `DataPropertiesOnly` — a class type with its methods stripped; the shape the class builders track and build from. +- `ClassConstructor` — the constructor signature `CeriosClassBuilder` requires of a target class. +- `RequiredFieldsRecord`, `RequiredFieldsTemplate`, `Path`, `ClassPath`, `DeepReadonly`, `OptionalKeys`, `RequiredKeys`. +- `BuilderStep` / `ClassBuilderStep`, `BuilderPreset` / `ClassBuilderPreset`, `BuilderComposer` / `ClassBuilderComposer` — the longer three-argument forms `BuilderWith` supersedes; still exported and supported. ## 🤝 Contributing @@ -2187,3 +575,4 @@ MIT © Ronald Veth - Cerios - [GitHub Repository](https://github.com/CeriosTesting/cerios-builder) - [Issue Tracker](https://github.com/CeriosTesting/cerios-builder/issues) - [NPM Package](https://www.npmjs.com/package/@cerios/cerios-builder) +- [Migration Guide](MIGRATION.md) diff --git a/package-lock.json b/package-lock.json index f2a2dd0..3ce3a85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,19 @@ "version": "1.6.0", "license": "MIT", "devDependencies": { - "@arethetypeswrong/cli": "^0.18.2", - "@changesets/cli": "^2.30.0", - "@types/node": "^25.4.0", + "@arethetypeswrong/cli": "^0.18.5", + "@changesets/cli": "^2.31.1", + "@types/node": "^26.1.1", + "@vitest/coverage-v8": "^4.1.10", "husky": "^9.1.7", - "lint-staged": "^16.3.3", - "npm-check-updates": "^19.6.3", - "oxfmt": "^0.38.0", - "oxlint": "^1.53.0", - "oxlint-tsgolint": "^0.16.0", - "tsup": "^8.5.1", - "typescript": "^5.9.3", - "vitest": "^4.0.18" + "lint-staged": "^17.1.0", + "npm-check-updates": "^22.2.9", + "oxfmt": "^0.59.0", + "oxlint": "^1.74.0", + "oxlint-tsgolint": "^0.25.0", + "tsdown": "^0.22.12", + "typescript": "^7.0.2", + "vitest": "^4.1.10" } }, "node_modules/@andrewbranch/untar.js": { @@ -30,13 +31,13 @@ "dev": true }, "node_modules/@arethetypeswrong/cli": { - "version": "0.18.2", - "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.18.2.tgz", - "integrity": "sha512-PcFM20JNlevEDKBg4Re29Rtv2xvjvQZzg7ENnrWFSS0PHgdP2njibVFw+dRUhNkPgNfac9iUqO0ohAXqQL4hbw==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.18.5.tgz", + "integrity": "sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==", "dev": true, "license": "MIT", "dependencies": { - "@arethetypeswrong/core": "0.18.2", + "@arethetypeswrong/core": "0.18.5", "chalk": "^4.1.2", "cli-table3": "^0.6.3", "commander": "^10.0.1", @@ -62,16 +63,16 @@ } }, "node_modules/@arethetypeswrong/core": { - "version": "0.18.2", - "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.18.2.tgz", - "integrity": "sha512-GiwTmBFOU1/+UVNqqCGzFJYfBXEytUkiI+iRZ6Qx7KmUVtLm00sYySkfe203C9QtPG11yOz1ZaMek8dT/xnlgg==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.18.5.tgz", + "integrity": "sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==", "dev": true, "license": "MIT", "dependencies": { "@andrewbranch/untar.js": "^1.0.3", "@loaderkit/resolve": "^1.0.2", "cjs-module-lexer": "^1.2.3", - "fflate": "^0.8.2", + "fflate": "^0.8.3", "lru-cache": "^11.0.1", "semver": "^7.5.4", "typescript": "5.6.1-rc", @@ -105,16 +106,76 @@ "node": ">=14.17" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@braidai/lang": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", @@ -123,13 +184,13 @@ "license": "ISC" }, "node_modules/@changesets/apply-release-plan": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.0.tgz", - "integrity": "sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.1.tgz", + "integrity": "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/config": "^3.1.3", + "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", @@ -145,14 +206,14 @@ } }, "node_modules/@changesets/assemble-release-plan": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz", - "integrity": "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.10.tgz", + "integrity": "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==", "dev": true, "license": "MIT", "dependencies": { "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", @@ -170,19 +231,19 @@ } }, "node_modules/@changesets/cli": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.30.0.tgz", - "integrity": "sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==", + "version": "2.31.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.31.1.tgz", + "integrity": "sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/apply-release-plan": "^7.1.0", - "@changesets/assemble-release-plan": "^6.0.9", + "@changesets/apply-release-plan": "^7.1.1", + "@changesets/assemble-release-plan": "^6.0.10", "@changesets/changelog-git": "^0.2.1", - "@changesets/config": "^3.1.3", + "@changesets/config": "^3.1.4", "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.3", - "@changesets/get-release-plan": "^4.0.15", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/get-release-plan": "^4.0.16", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", @@ -208,14 +269,14 @@ } }, "node_modules/@changesets/config": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.3.tgz", - "integrity": "sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.4.tgz", + "integrity": "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==", "dev": true, "license": "MIT", "dependencies": { "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/get-dependents-graph": "^2.1.4", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", @@ -235,9 +296,9 @@ } }, "node_modules/@changesets/get-dependents-graph": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz", - "integrity": "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.4.tgz", + "integrity": "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==", "dev": true, "license": "MIT", "dependencies": { @@ -248,14 +309,14 @@ } }, "node_modules/@changesets/get-release-plan": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.15.tgz", - "integrity": "sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.16.tgz", + "integrity": "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/assemble-release-plan": "^6.0.9", - "@changesets/config": "^3.1.3", + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/config": "^3.1.4", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", @@ -375,316 +436,311 @@ "node": ">=0.1.90" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], + "node_modules/@inquirer/external-editor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", + "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.7.0" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], + "node_modules/@loaderkit/resolve": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@loaderkit/resolve/-/resolve-1.0.4.tgz", + "integrity": "sha512-rJzYKVcV4dxJv+vW6jlvagF8zvGxHJ2+HTr1e2qOejfmGhAApgJHl8Aog4mMszxceTRiKTTbnpgmTO1bEZHV/A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "license": "ISC", + "dependencies": { + "@braidai/lang": "^1.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, "engines": { - "node": ">=18" + "node": ">=6 <7 || >=8" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", + "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", + "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/get-packages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, "engines": { - "node": ">=18" + "node": ">=6 <7 || >=8" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, "engines": { - "node": ">=18" + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.59.0.tgz", + "integrity": "sha512-bNTnfbuG7sAwb2PakMNaDukx5kXeW9duXOBeWtTOiLz3fXz3q2DlWguufPZ+c2IHEVrRXHD+M4aUgEWm841LDA==", "cpu": [ - "riscv64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.59.0.tgz", + "integrity": "sha512-R/Sn7z52QtdAKNqQLLY0EK7hVMjXiz3XUlvoCFCm/60jgIzAnQtiqLKBCFaBkimCQL5rs2ezPMcicpjCsrl54Q==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.59.0.tgz", + "integrity": "sha512-vm/ynUqE4HjC0ZIEjmXv1UJu1/GngccQ+T+TJudTMxUxm6r+GQTg1TO3E5jJfI71pBaXxSzs1+vWHIwuilGHhw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.59.0.tgz", + "integrity": "sha512-uTtYDpLN/obfKVWGpgEc8BqYlLZBQTPz2uYEvLRy3HPZxjZ34wiFzukUBU2bf64JuCYZI//GTV1EOMmWlPjf/w==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.59.0.tgz", + "integrity": "sha512-e2UnxL/ifStSPy8ffBCDbdy595SYsGy+U1pur4G65TuMmWxAMBzYGG7atZo/3mp515p8rZdsflxVD/E1FAdPLQ==", "cpu": [ "x64" ], @@ -692,419 +748,362 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "freebsd" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.59.0.tgz", + "integrity": "sha512-LtdeZ1l0urxte3VNi3g8cocZwv1xGM1NKHSgF/fJEEVhyQmlgGh7WFWKFd/pNuO7djfvPNtNO1+MS+FEWkgVSA==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.59.0.tgz", + "integrity": "sha512-dBTciSsj9GTMl7p+h2gMSI0hoPn2ijfc/dUsbnWsP0RbwgPl2r0C/5zkMb3Pb+gGj17LH7f1o4qLo9aes/pAvA==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.59.0.tgz", + "integrity": "sha512-tXVdJ/JINsNWdponPHN0OuKHtC+HdpyoS9sd6IDPNiiEYsRki8b7tefRZ1iMnRkdbyT4SEbguWsr6o+5awvbPQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.59.0.tgz", + "integrity": "sha512-RRTq38i2zT5fnw6XGHjvT6w2mh6x/G3m6AZcAZ56OTDTT/lsOeYnG3SVjwmH40z5kPqF+lf+o35e6m6PpKy9Dw==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "sunos" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.59.0.tgz", + "integrity": "sha512-lD3k7glAJSaXW0D6xzu8VOZbYbosvy+0ktOVkfLEoQF5HJlMSxTQ2KNW0JO+08ccP/1ElOKktVEMI0fqRbVB4w==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.59.0.tgz", + "integrity": "sha512-WH5ZP1RbuHKBO/yfPRQKpNO/ijHcEDNbnmC4VPf/Bcd3+mbMAZpRiJWRa1PL5bREdIZZHo343mk3sqlc9x7Usw==", "cpu": [ - "ia32" + "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.59.0.tgz", + "integrity": "sha512-743wOiaI9RZY4QVGkWkfGRavD5ZJUJ6gscFjVrVu1dP8AZh9jM+a6v3NhlR+OIzHdS6DhLM96w+gcVskskz7rw==", "cpu": [ - "x64" + "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", - "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.59.0.tgz", + "integrity": "sha512-xjRXQsRnrRZCcCkIEnbd2lmsQNobtwwkJxdy2bWXhZ1lIN0ouZwsBXRsoovW3yATuziAYwr9HMiQuR/Cc75NIw==", + "cpu": [ + "s390x" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.7.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.59.0.tgz", + "integrity": "sha512-4hNjqq/Rbr9B+StY9zMMAfm72+mtM4v80xYL5Qkb59Qd72g2vJMI0iFlPj3kf6miMsie/yJ7rt4urJT292HBgA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.59.0.tgz", + "integrity": "sha512-NH579iN8EVQYsWowUB8B5vFchcylJtwPVJ7NmUAqEQHNLfhPbDT3K56KrECNAkUN4QpF4qiMgN2vsfZwVvjm7g==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.59.0.tgz", + "integrity": "sha512-mzZy3Z5Aj1D75Aq9FVlmoRQH5ei8Ga4o/NZmlXkKyeZ5EmPrUXRR7c6BMBteV1ZuZ/356UYDuLRLjAMxTDTiBA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.59.0.tgz", + "integrity": "sha512-0CpDJ1gE3jN1Gk6xms1Ie6LPfPcOtY4FAtoOmVLHQoAf8DvO2wd0DW2dIX2f7YTp5dxrr0ND8JeUEjm3DP3k5g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.59.0.tgz", + "integrity": "sha512-zwdKBu3pt87uW0bRcywZb0oGMS7C6n87qogwRYFUgmk44T90ZzYlPjtlFYXs/DnBFrgNCvlHwCuWKfVWLeE7kw==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.59.0.tgz", + "integrity": "sha512-dUUbZkKgWrmAeI/puzv4bxN8lzcYaFnQVwFTFtwO2Gp8M7lZGSE2qJjC58g518+1bltJ8mizjYwD0BGHym0l/w==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@loaderkit/resolve": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@loaderkit/resolve/-/resolve-1.0.4.tgz", - "integrity": "sha512-rJzYKVcV4dxJv+vW6jlvagF8zvGxHJ2+HTr1e2qOejfmGhAApgJHl8Aog4mMszxceTRiKTTbnpgmTO1bEZHV/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "@braidai/lang": "^1.0.0" - } - }, - "node_modules/@manypkg/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.5.5", - "@types/node": "^12.7.1", - "find-up": "^4.1.0", - "fs-extra": "^8.1.0" - } - }, - "node_modules/@manypkg/find-root/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@manypkg/find-root/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-87opKlwFP8qS9WHAeETV+kA0fC9Oyj4sg7OxWdI4xQY0WC7zlN6BgG66uE5mvtN5mahkt/gL0i/AVEnX6POq2Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@manypkg/get-packages": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", - "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-HJmuZexsrhqp4WmETn+Soq7Ogt5F0jirv+cYRSniIPe+d/x5beQzLX69xOLhQRE+8FLGETe7FahWMVP8x0dW4g==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.5.5", - "@changesets/types": "^4.0.1", - "@manypkg/find-root": "^1.1.0", - "fs-extra": "^8.1.0", - "globby": "^11.0.0", - "read-yaml-file": "^1.1.0" - } - }, - "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", - "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", - "dev": true, - "license": "MIT" + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@manypkg/get-packages/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-aNyYsPREvCJi3qjfBA0sQB7DhT3y/W5Ac2JI2D8IJynoTOAhVZj401Si6901oDajlBWyqJqqojudn0VgHB6+7A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-+60+VjK9Mch3uA5WlTdNHuAm5+WA7wPPjuWdPWlU0F6JJpYpGZXUpO1RPKuFEWsBpNbLcLeJ0LbCJ1doWu58NA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-r53TO+eHp/t53nnUkQJfrYYXODPAxmtf3RUFQG5XsE2hD21IunliOaAdZXP2UwzCx+r/fbNaEelqTaAHcDr57w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">= 8" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-vqe66B+gL9HarhyHemdlfC2VWT7eoA+o/ufZ7zT6AGHv64boyDZIJS3U+rpZo+ey4O7wZtiS/vYR2fWqDoFeBw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.38.0.tgz", - "integrity": "sha512-lTN4//sgYywK8ulQo7a/EZVzOTGomGQv2IG/7tMYdqTV3xN3QTqWpXcZBGUzaicC4B882N+5zJLYZ37IWfUMcg==", + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.74.0.tgz", + "integrity": "sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw==", "cpu": [ "arm" ], @@ -1118,10 +1117,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.38.0.tgz", - "integrity": "sha512-XbVgqR1WsIcCkfxwh2tdg3M1MWgR23YOboW2nbB8ab0gInNNLGy7cIAdr78XaoG/bGdaF4488XRhuGWq67xrzA==", + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.74.0.tgz", + "integrity": "sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw==", "cpu": [ "arm64" ], @@ -1135,10 +1134,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.38.0.tgz", - "integrity": "sha512-AHb6zUzWaSJra7lnPkI+Sqwu33bVWVTwCozcw9QTX8vwHaI1+5d5STqBcsJf63eSuRVRlflwMS4erlAPh3fXZw==", + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.74.0.tgz", + "integrity": "sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ==", "cpu": [ "arm64" ], @@ -1152,10 +1151,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.38.0.tgz", - "integrity": "sha512-VmlmTyn7LL7Xi5htjosxGpJJHf3Drx5mgXxKE8+NT10uBXTaG3FHpRYhW3Zg5Qp7omH92Lj1+IHYqQG/HZpLnw==", + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.74.0.tgz", + "integrity": "sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q==", "cpu": [ "x64" ], @@ -1169,10 +1168,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.38.0.tgz", - "integrity": "sha512-LynMLRqaUEAV6n4svTFanFOAnJ9D6aCCfymJ2yhMSh5fYFgCCO4q5LzPV2nATKKoyPocSErFSmYREsOFbkIlCg==", + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.74.0.tgz", + "integrity": "sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g==", "cpu": [ "x64" ], @@ -1186,10 +1185,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.38.0.tgz", - "integrity": "sha512-HRRZtOXcss5+bGqQcYahILgt14+Iu/Olf6fnoKq5ctOzU21PGHVB+zuocgt+/+ixoMLV1Drvok3ns7QwnLwNTA==", + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.74.0.tgz", + "integrity": "sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g==", "cpu": [ "arm" ], @@ -1203,10 +1202,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.38.0.tgz", - "integrity": "sha512-kScH8XnH7TRUckMOSZ5115Vvr2CQq+iPsuXPEzwUXSxh+gDLzt+GsXuvCsaPxp1KP+dQj88VrIjeQ4V0f9NRKw==", + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.74.0.tgz", + "integrity": "sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ==", "cpu": [ "arm" ], @@ -1220,14 +1219,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.38.0.tgz", - "integrity": "sha512-PUVn/vGsMs83eLhNXLNjR+Qw/EPiNxU9Tx+p+aZBK0RT9/k6RNgh/O4F1TxS4tdISmf3SSgjdnMOVW3ZfQZ2mA==", + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.74.0.tgz", + "integrity": "sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1237,14 +1239,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.38.0.tgz", - "integrity": "sha512-LhtmaLCMGtAIEtaTBAoKLF3QVt+IDKIjdEZvsf0msLeTUFKxyoTNScYBXbkmvqGrm37vV0JjTPvm+OaSh3np5A==", + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.74.0.tgz", + "integrity": "sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1254,14 +1259,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.38.0.tgz", - "integrity": "sha512-tO6tPaS21o0MaRqmOi9e3sDotlW4c+1gCx4SwdrfDXm3Y1vmIZWh0qB6t/Xh77bIGVr/4fC95eKOhKLPGwdL+Q==", + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.74.0.tgz", + "integrity": "sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1271,14 +1279,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.38.0.tgz", - "integrity": "sha512-djEqwFUHczstFKp5aT43TuRWxyKZSkIZUfGXIEKa0srmIAt1CXQO5O8xLgNG4SGkXTRB1domFfCE68t9SkSmfA==", + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.74.0.tgz", + "integrity": "sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1288,14 +1299,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.38.0.tgz", - "integrity": "sha512-76EgMMtS6sIE+9Pl9q2GZgZpbZSzqtjQhUUIWl0RVNfHg66tstdJMhY2LXESjDYhc5vFYt9qdQNM0w0zg3onPw==", + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.74.0.tgz", + "integrity": "sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1305,14 +1319,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.38.0.tgz", - "integrity": "sha512-JYNr3i9z/YguZg088kopjvz49hDxTEL193mYL2/02uq/6BLlQRMaKrePEITTHm/vUu4ZquAKgu4mDib6pGWdyg==", + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.74.0.tgz", + "integrity": "sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1322,14 +1339,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.38.0.tgz", - "integrity": "sha512-Lf+/Keaw1kBKx0U3HT5PsA7/3VO4ZOmaqo4sWaeAJ6tYeX8h/2IZcEONhjry6T4BETza78z6xI3Qx+18QZix6A==", + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.74.0.tgz", + "integrity": "sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1339,14 +1359,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.38.0.tgz", - "integrity": "sha512-4O6sf6OQuz1flk0TDrrtmXOVO3letA7fYe2IEAiJOQvKhJcMU08NiIVODQjMGZ6IQh1q91B+TlliDfbsYalw8A==", + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.74.0.tgz", + "integrity": "sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1356,10 +1379,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.38.0.tgz", - "integrity": "sha512-GNocbjYnielmKVBk+r/2Vc4E3oTsAO4+5gRuroUVx86Jv+mpD+hyFkf260/by0YtpF1ipqyxR8chOSgRQvD2zQ==", + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.74.0.tgz", + "integrity": "sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg==", "cpu": [ "arm64" ], @@ -1373,10 +1396,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.38.0.tgz", - "integrity": "sha512-AwgjBHRxPckbazLpECuPOSzYlppYR1CBeUSuzZuClsmTnlZA9O1MexCEP9CROe03Yo1xBGvYtiCjwKZMBChGkg==", + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.74.0.tgz", + "integrity": "sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA==", "cpu": [ "arm64" ], @@ -1390,10 +1413,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.38.0.tgz", - "integrity": "sha512-c3u+ak6Zrh1g6pM2TgNVvOgkm7q1XaIX+5Mgxvu38ozJ5OfM8c7HZk3glMdBzlTD2uK0sSfgBq1kuXwCe1NOGg==", + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.74.0.tgz", + "integrity": "sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ==", "cpu": [ "ia32" ], @@ -1407,10 +1430,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.38.0.tgz", - "integrity": "sha512-wud1Hz0D2hYrhk6exxQQndn1htcA28wAcFb1vtP3ZXSzPFtMvc7ag/VNPv6nz6mDzM8X660jUwGEac99QcrVsA==", + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.74.0.tgz", + "integrity": "sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA==", "cpu": [ "x64" ], @@ -1424,111 +1447,40 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint-tsgolint/darwin-arm64": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.16.0.tgz", - "integrity": "sha512-WQt5lGwRPJBw7q2KNR0mSPDAaMmZmVvDlEEti96xLO7ONhyomQc6fBZxxwZ4qTFedjJnrHX94sFelZ4OKzS7UQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxlint-tsgolint/darwin-x64": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.16.0.tgz", - "integrity": "sha512-VJo29XOzdkalvCTiE2v6FU3qZlgHaM8x8hUEVJGPU2i5W+FlocPpmn00+Ld2n7Q0pqIjyD5EyvZ5UmoIEJMfqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxlint-tsgolint/linux-arm64": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.16.0.tgz", - "integrity": "sha512-MPfqRt1+XRHv9oHomcBMQ3KpTE+CSkZz14wUxDQoqTNdUlV0HWdzwIE9q65I3D9YyxEnqpM7j4qtDQ3apqVvbQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint-tsgolint/linux-x64": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-0.16.0.tgz", - "integrity": "sha512-XQSwVUsnwLokMhe1TD6IjgvW5WMTPzOGGkdFDtXWQmlN2YeTw94s/NN0KgDrn2agM1WIgAenEkvnm0u7NgwEyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint-tsgolint/win32-arm64": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.16.0.tgz", - "integrity": "sha512-EWdlspQiiFGsP2AiCYdhg5dTYyAlj6y1nRyNI2dQWq4Q/LITFHiSRVPe+7m7K7lcsZCEz2icN/bCeSkZaORqIg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxlint-tsgolint/win32-x64": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-0.16.0.tgz", - "integrity": "sha512-1ufk8cgktXJuJZHKF63zCHAkaLMwZrEXnZ89H2y6NO85PtOXqu4zbdNl0VBpPP3fCUuUBu9RvNqMFiv0VsbXWA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.53.0.tgz", - "integrity": "sha512-JC89/jAx4d2zhDIbK8MC4L659FN1WiMXMBkNg7b33KXSkYpUgcbf+0nz7+EPRg+VwWiZVfaoFkNHJ7RXYb5Neg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-android-arm64": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.53.0.tgz", - "integrity": "sha512-CY+pZfi+uyeU7AwFrEnjsNT+VfxYmKLMuk7bVxArd8f+09hQbJb8f7C7EpvTfNqrCK1J8zZlaYI4LltmEctgbQ==", + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@quansync/fs/node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -1542,10 +1494,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.53.0.tgz", - "integrity": "sha512-0aqsC4HDQ94oI6kMz64iaOJ1f3bCVArxvaHJGOScBvFz6CcQedXi5b70Xg09CYjKNaHA56dW0QJfoZ/111kz1A==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -1559,10 +1511,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.53.0.tgz", - "integrity": "sha512-e+KvuaWtnisyWojO/t5qKDbp2dvVpg+1dl4MGnTb21QpY4+4+9Y1XmZPaztcA2XNvy4BIaXFW+9JH9tMpSBqUg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -1576,10 +1528,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.53.0.tgz", - "integrity": "sha512-hpU0ZHVeblFjmZDfgi9BxhhCpURh0KjoFy5V+Tvp9sg/fRcnMUEfaJrgz+jQfOX4jctlVWrAs1ANs91+5iV+zA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -1593,27 +1545,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.53.0.tgz", - "integrity": "sha512-ccKxOpw+X4xa2pO+qbTOpxQ2x1+Ag3ViRQMnWt3gHp1LcpNgS1xd6GYc3OvehmHtrXqEV3YGczZ0I1qpBB4/2A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.53.0.tgz", - "integrity": "sha512-UBkBvmzSmlyH2ZObQMDKW/TuyTmUtP/XClPUyU2YLwj0qLopZTZxnDz4VG5d3wz1HQuZXO0o1QqsnQUW1v4a6Q==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -1627,14 +1562,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.53.0.tgz", - "integrity": "sha512-PQJJ1izoH9p61las6rZ0BWOznAhTDMmdUPL2IEBLuXFwhy2mSloYHvRkk39PSYJ1DyG+trqU5Z9ZbtHSGH6plg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1644,14 +1582,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.53.0.tgz", - "integrity": "sha512-GXI1o4Thn/rtnRIL38BwrDMwVcUbIHKCsOixIWf/CkU3fCG3MXFzFTtDMt+34ik0Qk452d8kcpksL0w/hUkMZA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1661,14 +1602,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.53.0.tgz", - "integrity": "sha512-Uahk7IVs2yBamCgeJ3XKpKT9Vh+de0pDKISFKnjEcI3c/w2CFHk1+W6Q6G3KI56HGwE9PWCp6ayhA9whXWkNIQ==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1678,14 +1622,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.53.0.tgz", - "integrity": "sha512-sWtcU9UkrKMWsGKdFy8R6jkm9Q0VVG1VCpxVuh0HzRQQi3ENI1Nh5CkpsdfUs2MKRcOoHKbXqTscunuXjhxoxQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ - "riscv64" + "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1695,14 +1642,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.53.0.tgz", - "integrity": "sha512-aXew1+HDvCdExijX/8NBVC854zJwxhKP3l9AHFSHQNo4EanlHtzDMIlIvP3raUkL0vXtFCkTFYezzU5HjstB8A==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ - "riscv64" + "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1712,14 +1662,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.53.0.tgz", - "integrity": "sha512-rVpyBSqPGou9sITcsoXqUoGBUH74bxYLYOAGUqN599Zu6BQBlBU9hh3bJQ/20D1xrhhrsbiCpVPvXpLPM5nL1w==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ - "s390x" + "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1729,44 +1682,46 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.53.0.tgz", - "integrity": "sha512-eOyeQ8qFQ2geXmlWJuXAOaek0hFhbMLlYsU457NMLKDRoC43Xf+eDPZ9Yk0n9jDaGJ5zBl/3Dy8wo41cnIXuLA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.53.0.tgz", - "integrity": "sha512-S6rBArW/zD1tob8M9PwKYrRmz+j1ss1+wjbRAJCWKd7TC3JB6noDiA95pIj9zOZVVp04MIzy5qymnYusrEyXzg==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ - "x64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.53.0.tgz", - "integrity": "sha512-sd/A0Ny5sN0D/MJtlk7w2jGY4bJQou7gToa9WZF7Sj6HTyVzvlzKJWiOHfr4SulVk4ndiFQ8rKmF9rXP0EcF3A==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], @@ -1774,18 +1729,18 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" + "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.53.0.tgz", - "integrity": "sha512-QC3q7b51Er/ZurEFcFzc7RpQ/YEoEBLJuCp3WoOzhSHHH/nkUKFy+igOxlj1z3LayhEZPDQQ7sXvv2PM2cdG3Q==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", @@ -1797,482 +1752,476 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.53.0.tgz", - "integrity": "sha512-3OvLgOqwd705hWHV2i8ni80pilvg6BUgpC2+xtVu++e/q28LKVohGh5J5QYJOrRMfWmxK0M/AUu43vUw62LAKQ==", - "cpu": [ - "ia32" - ], + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.53.0.tgz", - "integrity": "sha512-xTiOkntexCdJytZ7ArIIgl3vGW5ujMM3sJNM7/+iqGAVJagCqjFFWn68HRWRLeyT66c95uR+CeFmQFI6mLQqDw==", - "cpu": [ - "x64" - ], + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "undici-types": "~8.3.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "android" - ] + "aix" + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", "cpu": [ - "arm" + "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", "cpu": [ - "arm64" + "loong64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", "cpu": [ - "loong64" + "mips64el" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "linux" - ] + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "openbsd" - ] + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "openharmony" - ] + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" - ] + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", "cpu": [ - "ia32" + "x64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" - ] + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "node": ">=16.20.0" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", - "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2281,7 +2230,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -2293,26 +2242,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.10", "pathe": "^2.0.3" }, "funding": { @@ -2320,13 +2269,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2335,9 +2285,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -2345,76 +2295,403 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/@yuku-codegen/binding-darwin-arm64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-darwin-arm64/-/binding-darwin-arm64-0.7.0.tgz", + "integrity": "sha512-RLfjSuJUolQJ04zYq3kM2vSUk9BkFFSOhmOWARb7Pc7Gnf0vMLfj/fepnn9IdsyE2FsJjoCJPKM5bBze4ld5pQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "node_modules/@yuku-codegen/binding-darwin-x64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-darwin-x64/-/binding-darwin-x64-0.7.0.tgz", + "integrity": "sha512-OI9z6U69j2TvaWgDzheUNlMPSrlNQs8PD3isfyuGxQVbO8Dqi3F7PRT1JnG7pkId+IKvmEu+40sjXWDHRbtlMA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@yuku-codegen/binding-freebsd-x64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-freebsd-x64/-/binding-freebsd-x64-0.7.0.tgz", + "integrity": "sha512-nhoH3yXu2e6itB1Hbpj1+kZJ6yTJtsXBN5dWYm20TKvs1Iq79di6hdCKBlHE12RHFW39aQKcnn+vlFF1J1RGsw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@yuku-codegen/binding-linux-arm-gnu": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-arm-gnu/-/binding-linux-arm-gnu-0.7.0.tgz", + "integrity": "sha512-bgmapvrk/f/1bOSl+XA9KTLKb7vmxuXhqgCchlhTvgNOWnHB4rPLfzu0TkQU5CjVFzZQmHoEmhObf9DKUzcDMA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "node_modules/@yuku-codegen/binding-linux-arm-musl": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-arm-musl/-/binding-linux-arm-musl-0.7.0.tgz", + "integrity": "sha512-HpVSId+bxtqSkIsbBUP78J0j4ZUnCvqj14WGmAjKDP25oaQ0SZuFtVIZJmTisuKVSM4AIx1CVBYlJ2CCIm/vsQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-linux-arm64-gnu": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.7.0.tgz", + "integrity": "sha512-oL8OGWo99U0arObIl5UKov3ZvsoNGWUMWRJrlXP99P4LpVz+gZC9KwfYCzxlhGnNk9rpp/fcTUUHYHbTskuZwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-linux-arm64-musl": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.7.0.tgz", + "integrity": "sha512-ULsFt9PnI5vBMCG5pGir1YQtJ03o0Sb5SsRTDYRSeKaVaxdog+kA9Guwnk8q8OMPFsI3s40Fhu/+45cQXHSZ8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-linux-x64-gnu": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.7.0.tgz", + "integrity": "sha512-l1TP6G39gnF4eiHEApjViyA12n+YHT18A0y3FDUy1jF5hkZt2RWlBNrU4HPhzTd6kQlozJEWyjhVNja2J3/eBw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-linux-x64-musl": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-x64-musl/-/binding-linux-x64-musl-0.7.0.tgz", + "integrity": "sha512-h8GZdSNSaBWySA4p8eYFWkH8OWdqA4peOAFeWs+DltJo8r9ZXSvVB0XBypMxkLwA0qLDqwtWoVssg2LK1zJKdw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-win32-arm64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-win32-arm64/-/binding-win32-arm64-0.7.0.tgz", + "integrity": "sha512-hVkxv4UTPXex7q5ldvqelSMvvYc6bCAYlDSMMg3wmttHZF9yQzPb4yYw69eWvR4coEMFaUv63ADJNjiGjNxkYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@yuku-codegen/binding-win32-x64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-win32-x64/-/binding-win32-x64-0.7.0.tgz", + "integrity": "sha512-g3YQpqvsTbDHjjBDnNGhaO4ejN+m2GpsWc50dcGPR/RUx+yt++uDEXKSEyiAaCpEM2p5PuDIW/YEjXx0oVdsoQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@yuku-parser/binding-darwin-arm64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.7.0.tgz", + "integrity": "sha512-LoZ945MxFzc6+ltMenaFtXhJ4rdj11j1IwkRWLR7WPim2jdXUURTfWhAm7VzQDtSCHtnDz8PAH55eIHmNilTlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@yuku-parser/binding-darwin-x64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-darwin-x64/-/binding-darwin-x64-0.7.0.tgz", + "integrity": "sha512-HVS2bgNGqCl5oU5wP16tZUMmXAO1avgxp/uzDOJcNqA7WOlV1PqTWD3P/1GXaMAijIZNu3VLSXkU1dovwwPpVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@yuku-parser/binding-freebsd-x64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.7.0.tgz", + "integrity": "sha512-nVZgLmiuEdkRb8oJsXBoQphfZwsMTcatSEB5t1QL9aH92/hx2TxAGayZy/g326l5rGVMmieCHc9FYxu/MnVFHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@yuku-parser/binding-linux-arm-gnu": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-arm-gnu/-/binding-linux-arm-gnu-0.7.0.tgz", + "integrity": "sha512-rIq85RCqwMQFtQSBIvUbmEAkNw0pu89PsrikSt+uVMCxjN1ZjbekTWw4hi0NVax8kAb5t2Xgs6kQrFoapyQ3Xg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-arm-musl": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-arm-musl/-/binding-linux-arm-musl-0.7.0.tgz", + "integrity": "sha512-BNoI0mP8o3iV6dJEgfJq1U6cAMn7iSYe3gSYkN05DL1FyOU6ni2NwcDScsV6RBBQHj4V1s5123F85JLPhhXfqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-arm64-gnu": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.7.0.tgz", + "integrity": "sha512-YrFNrrc0bq5B+cV2EuxDU4VPhuH0RHkV0M1rne8UTxPbqjPmarWxLcl1JEeyaXcL0856kwnaV728GMeB1o/Gdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-arm64-musl": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.7.0.tgz", + "integrity": "sha512-WaDi6BsjZ/vrO7EgX36C4sLN9fUPZd/vM0Nh+82uOjygCxgFtiRf5NsFxdaLSzqMi+sbGsg+hWM/De4vkana3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-x64-gnu": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.7.0.tgz", + "integrity": "sha512-gxlbaqglGr7ir9UZLF0945JKTImI9MFDZiny57mRiqktCTDVQPnb5Lmj0okKJ6w0nBX1NzwviMX6v4i+rwLSWw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-x64-musl": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.7.0.tgz", + "integrity": "sha512-n7pCgW5iNoaKEpt8K3UTkg7ACX87MueWkJQD7cQSOgxyu/Qx0BCdwi0iOs60Gjj88wyKwqkYIBPMN3wKE5P7Yw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-win32-arm64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-win32-arm64/-/binding-win32-arm64-0.7.0.tgz", + "integrity": "sha512-kyuC7W1mGIMbJp6t86hIhDjnptFxZiTaOrbHhYkCpfsXPF6pszWNK03DUMc//Udnw3Af3d5w93CRJlRNpCb5eg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@yuku-parser/binding-win32-x64": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-win32-x64/-/binding-win32-x64-0.7.0.tgz", + "integrity": "sha512-xEm5rwtETud7iNbxSocACgXzPrv2x4vkk339BtAqZAKoCS+edaO767EKAGcUO3xo8STBgCud1cAA5oIyzN8APA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@yuku-toolchain/types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yuku-toolchain/types/-/types-0.7.0.tgz", + "integrity": "sha512-kKleXJmXcZnJ5LUDTeYvK350SB+BXdpoHUwT78GGyOXOhCZ0tWf+seDPAvVnCrjoJhf+FhqtR5HRUfWW+yILQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", + "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", @@ -2441,12 +2718,17 @@ "node": ">=12" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } }, "node_modules/better-path-resolve": { "version": "1.0.0", @@ -2474,32 +2756,6 @@ "node": ">=8" } }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2534,32 +2790,16 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=10" - } - }, - "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=10" } }, + "node_modules/chardet": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "dev": true, + "license": "MIT" + }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -2567,22 +2807,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-highlight": { "version": "2.1.11", "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", @@ -2662,69 +2886,6 @@ "@colors/colors": "1.5.0" } }, - "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2745,40 +2906,13 @@ "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2794,23 +2928,12 @@ "node": ">= 8" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } + "license": "MIT" }, "node_modules/detect-indent": { "version": "6.1.0", @@ -2822,6 +2945,16 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2835,12 +2968,26 @@ "node": ">=8" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "node_modules/dts-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-3.0.0.tgz", + "integrity": "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "oxc-resolver": ">=11.0.0" + }, + "peerDependenciesMeta": { + "oxc-resolver": { + "optional": true + } + } }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -2856,6 +3003,16 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -2884,54 +3041,12 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2966,13 +3081,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3018,9 +3126,9 @@ } }, "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "dev": true, "license": "MIT" }, @@ -3051,35 +3159,6 @@ "node": ">=8" } }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -3120,17 +3199,20 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.5.tgz", + "integrity": "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==", "dev": true, "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, "engines": { - "node": ">=18" + "node": ">=20.20.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, "node_modules/glob-parent": { @@ -3194,6 +3276,20 @@ "node": "*" } }, + "node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/human-id": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.2.tgz", @@ -3247,6 +3343,19 @@ "node": ">= 4" } }, + "node_modules/import-without-cache": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.4.0.tgz", + "integrity": "sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3320,37 +3429,67 @@ "dev": true, "license": "ISC" }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "BSD-3-Clause", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": ">=10" } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, "engines": { - "node": ">=10" + "node": ">=8" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3359,181 +3498,321 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lint-staged": { - "version": "16.3.3", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.3.3.tgz", - "integrity": "sha512-RLq2koZ5fGWrx7tcqx2tSTMQj4lRkfNJaebO/li/uunhCJbtZqwTuwPHpgIimAHHi/2nZIiGrkCHDCOeR1onxA==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^14.0.3", - "listr2": "^9.0.5", - "micromatch": "^4.0.8", - "string-argv": "^0.3.2", - "tinyexec": "^1.0.2", - "yaml": "^2.8.2" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=20.17" + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/lint-staged" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lint-staged/node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=20.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "node_modules/lint-staged": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.1.0.tgz", + "integrity": "sha512-d7UQRu/9ZPgfu4+hu/k0wny5GEaIxo+2jb2LJqQDkE7cHRTm1HGqNUDq5UOwsGPpjpaNAFmgAsYo3TR+i9cSJw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "picomatch": "^4.0.5", + "string-argv": "^0.3.2", + "tinyexec": "^1.2.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=18" + "node": ">=22.22.1" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.0" } }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "node_modules/lint-staged/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" } }, "node_modules/locate-path": { @@ -3556,137 +3835,44 @@ "dev": true, "license": "MIT" }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", - "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/marked": { "version": "9.1.6", "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", @@ -3779,49 +3965,26 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8.6" } }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/mri": { @@ -3834,13 +3997,6 @@ "node": ">=4" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -3854,9 +4010,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -3889,9 +4045,9 @@ } }, "node_modules/npm-check-updates": { - "version": "19.6.3", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-19.6.3.tgz", - "integrity": "sha512-VAt9Bp26eLaymZ0nZyh5n/by+YZIuegXlvWR0yv1zBqd984f8VnEnBbn+1lS3nN5LyEjn62BJ+yYgzNSpb6Gzg==", + "version": "22.2.9", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-22.2.9.tgz", + "integrity": "sha512-DVeZ0KirHfliSsHuR2o7cHE+tW439sVHfJjF6cGWeDiY0Wyl3BI/jS4zV0eixtcMOquFbcF1Su/FsxOvk5MoYA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3899,8 +4055,8 @@ "npm-check-updates": "build/cli.js" }, "engines": { - "node": ">=20.0.0", - "npm": ">=8.12.1" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10.0.0" } }, "node_modules/object-assign": { @@ -3914,15 +4070,18 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/outdent": { "version": "0.5.0", @@ -3932,9 +4091,9 @@ "license": "MIT" }, "node_modules/oxfmt": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.38.0.tgz", - "integrity": "sha512-RGYfnnxmCz8dMQ1Oo5KrYkNRc9cne2WL2vfE+datWNkgiSAkfUsqpGLR7rnkN6cQFgQkHDZH400eXN6izJ8Lww==", + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.59.0.tgz", + "integrity": "sha512-Xqk6cPZS1yMvVa7OAuenaDZUsgMDutvvbZ9/L5gSvAfW64+WN4HVhgipLj5rVERbYQt8fLs9TopyZ1rU1XEG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3950,31 +4109,43 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.38.0", - "@oxfmt/binding-android-arm64": "0.38.0", - "@oxfmt/binding-darwin-arm64": "0.38.0", - "@oxfmt/binding-darwin-x64": "0.38.0", - "@oxfmt/binding-freebsd-x64": "0.38.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.38.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.38.0", - "@oxfmt/binding-linux-arm64-gnu": "0.38.0", - "@oxfmt/binding-linux-arm64-musl": "0.38.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.38.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.38.0", - "@oxfmt/binding-linux-riscv64-musl": "0.38.0", - "@oxfmt/binding-linux-s390x-gnu": "0.38.0", - "@oxfmt/binding-linux-x64-gnu": "0.38.0", - "@oxfmt/binding-linux-x64-musl": "0.38.0", - "@oxfmt/binding-openharmony-arm64": "0.38.0", - "@oxfmt/binding-win32-arm64-msvc": "0.38.0", - "@oxfmt/binding-win32-ia32-msvc": "0.38.0", - "@oxfmt/binding-win32-x64-msvc": "0.38.0" + "@oxfmt/binding-android-arm-eabi": "0.59.0", + "@oxfmt/binding-android-arm64": "0.59.0", + "@oxfmt/binding-darwin-arm64": "0.59.0", + "@oxfmt/binding-darwin-x64": "0.59.0", + "@oxfmt/binding-freebsd-x64": "0.59.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.59.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.59.0", + "@oxfmt/binding-linux-arm64-gnu": "0.59.0", + "@oxfmt/binding-linux-arm64-musl": "0.59.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.59.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.59.0", + "@oxfmt/binding-linux-riscv64-musl": "0.59.0", + "@oxfmt/binding-linux-s390x-gnu": "0.59.0", + "@oxfmt/binding-linux-x64-gnu": "0.59.0", + "@oxfmt/binding-linux-x64-musl": "0.59.0", + "@oxfmt/binding-openharmony-arm64": "0.59.0", + "@oxfmt/binding-win32-arm64-msvc": "0.59.0", + "@oxfmt/binding-win32-ia32-msvc": "0.59.0", + "@oxfmt/binding-win32-x64-msvc": "0.59.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } } }, "node_modules/oxlint": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.53.0.tgz", - "integrity": "sha512-TLW0PzGbpO1JxUnuy1pIqVPjQUGh4fNfxu5XJbdFIRFVaJ0UFzTjjk/hSFTMRxN6lZub53xL/IwJNEkrh7VtDg==", + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.74.0.tgz", + "integrity": "sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA==", "dev": true, "license": "MIT", "bin": { @@ -3987,51 +4158,55 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.53.0", - "@oxlint/binding-android-arm64": "1.53.0", - "@oxlint/binding-darwin-arm64": "1.53.0", - "@oxlint/binding-darwin-x64": "1.53.0", - "@oxlint/binding-freebsd-x64": "1.53.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.53.0", - "@oxlint/binding-linux-arm-musleabihf": "1.53.0", - "@oxlint/binding-linux-arm64-gnu": "1.53.0", - "@oxlint/binding-linux-arm64-musl": "1.53.0", - "@oxlint/binding-linux-ppc64-gnu": "1.53.0", - "@oxlint/binding-linux-riscv64-gnu": "1.53.0", - "@oxlint/binding-linux-riscv64-musl": "1.53.0", - "@oxlint/binding-linux-s390x-gnu": "1.53.0", - "@oxlint/binding-linux-x64-gnu": "1.53.0", - "@oxlint/binding-linux-x64-musl": "1.53.0", - "@oxlint/binding-openharmony-arm64": "1.53.0", - "@oxlint/binding-win32-arm64-msvc": "1.53.0", - "@oxlint/binding-win32-ia32-msvc": "1.53.0", - "@oxlint/binding-win32-x64-msvc": "1.53.0" + "@oxlint/binding-android-arm-eabi": "1.74.0", + "@oxlint/binding-android-arm64": "1.74.0", + "@oxlint/binding-darwin-arm64": "1.74.0", + "@oxlint/binding-darwin-x64": "1.74.0", + "@oxlint/binding-freebsd-x64": "1.74.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.74.0", + "@oxlint/binding-linux-arm-musleabihf": "1.74.0", + "@oxlint/binding-linux-arm64-gnu": "1.74.0", + "@oxlint/binding-linux-arm64-musl": "1.74.0", + "@oxlint/binding-linux-ppc64-gnu": "1.74.0", + "@oxlint/binding-linux-riscv64-gnu": "1.74.0", + "@oxlint/binding-linux-riscv64-musl": "1.74.0", + "@oxlint/binding-linux-s390x-gnu": "1.74.0", + "@oxlint/binding-linux-x64-gnu": "1.74.0", + "@oxlint/binding-linux-x64-musl": "1.74.0", + "@oxlint/binding-openharmony-arm64": "1.74.0", + "@oxlint/binding-win32-arm64-msvc": "1.74.0", + "@oxlint/binding-win32-ia32-msvc": "1.74.0", + "@oxlint/binding-win32-x64-msvc": "1.74.0" }, "peerDependencies": { - "oxlint-tsgolint": ">=0.15.0" + "oxlint-tsgolint": ">=0.24.0", + "vite-plus": "*" }, "peerDependenciesMeta": { "oxlint-tsgolint": { "optional": true + }, + "vite-plus": { + "optional": true } } }, "node_modules/oxlint-tsgolint": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-0.16.0.tgz", - "integrity": "sha512-4RuJK2jP08XwqtUu+5yhCbxEauCm6tv2MFHKEMsjbosK2+vy5us82oI3VLuHwbNyZG7ekZA26U2LLHnGR4frIA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-0.25.0.tgz", + "integrity": "sha512-7DBpqyLZCfyoXiivyfzt9Xmju/K1RcN+Y1W7buEwrgRCWWF11v9alypPqWGZBmh2erDkKL/kVyhKUH2Px+t13A==", "dev": true, "license": "MIT", "bin": { "tsgolint": "bin/tsgolint.js" }, "optionalDependencies": { - "@oxlint-tsgolint/darwin-arm64": "0.16.0", - "@oxlint-tsgolint/darwin-x64": "0.16.0", - "@oxlint-tsgolint/linux-arm64": "0.16.0", - "@oxlint-tsgolint/linux-x64": "0.16.0", - "@oxlint-tsgolint/win32-arm64": "0.16.0", - "@oxlint-tsgolint/win32-x64": "0.16.0" + "@oxlint-tsgolint/darwin-arm64": "0.25.0", + "@oxlint-tsgolint/darwin-x64": "0.25.0", + "@oxlint-tsgolint/linux-arm64": "0.25.0", + "@oxlint-tsgolint/linux-x64": "0.25.0", + "@oxlint-tsgolint/win32-arm64": "0.25.0", + "@oxlint-tsgolint/win32-x64": "0.25.0" } }, "node_modules/p-filter": { @@ -4096,13 +4271,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/package-manager-detector": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", @@ -4157,30 +4325,6 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4206,13 +4350,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -4228,32 +4372,10 @@ "node": ">=6" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", "dev": true, "funding": [ { @@ -4271,7 +4393,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4279,49 +4401,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -4403,9 +4482,9 @@ } }, "node_modules/read-yaml-file/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "dev": true, "license": "MIT", "dependencies": { @@ -4416,20 +4495,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4450,37 +4515,14 @@ "node": ">=8" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/reusify": { @@ -4494,56 +4536,80 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/rolldown-plugin-dts": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.27.12.tgz", + "integrity": "sha512-DJg5ELVEdLAVhirvtak7GS4nbNvBzLNAtYL1M8hLghDURBFKU2MwEH6ncHibYs6khq1NaRQ97iFMiHvzw+WoSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dts-resolver": "^3.0.0", + "get-tsconfig": "5.0.0-beta.5", + "obug": "^2.1.4", + "yuku-ast": "^0.7.0", + "yuku-codegen": "^0.7.0", + "yuku-parser": "^0.7.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@typescript/native-preview": "*", + "@volar/typescript": "~2.4.0", + "rolldown": "^1.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ~7.0.0", + "vue-tsc": "~3.2.0 || ~3.3.0" + }, + "peerDependenciesMeta": { + "@typescript/native-preview": { + "optional": true + }, + "@volar/typescript": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } } }, "node_modules/run-parallel": { @@ -4578,9 +4644,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -4656,62 +4722,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4748,9 +4758,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -4779,138 +4789,27 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "ansi-regex": "^5.0.1" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=8" } }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=4" } }, "node_modules/supports-color": { @@ -4986,189 +4885,597 @@ "dev": true, "license": "MIT" }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tsdown": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.22.12.tgz", + "integrity": "sha512-IzGRfGwSsufXJWOcQ0tmECKMG/dbxhUwDOySTS7JE1AM8pkB9+bpcTPs8bTxxSNrSvGocSczrBlOh97tZObq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.3.1", + "cac": "^7.0.0", + "defu": "^6.1.7", + "empathic": "^2.0.1", + "hookable": "^6.1.1", + "import-without-cache": "^0.4.0", + "obug": "^2.1.4", + "picomatch": "^4.0.5", + "rolldown": "~1.2.0", + "rolldown-plugin-dts": "^0.27.9", + "tinyexec": "^1.2.4", + "tinyglobby": "^0.2.17", + "tree-kill": "^1.2.2", + "unconfig-core": "^7.5.0", + "verkit": "^0.1.0" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@tsdown/css": "0.22.12", + "@tsdown/exe": "0.22.12", + "@vitejs/devtools": "*", + "publint": "^0.3.8", + "tsx": "*", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", + "unplugin-unused": "^0.5.0", + "unrun": "*" }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@tsdown/css": { + "optional": true + }, + "@tsdown/exe": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "publint": { + "optional": true + }, + "tsx": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-unused": { + "optional": true + }, + "unrun": { + "optional": true + } + } + }, + "node_modules/tsdown/node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/tsdown/node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/tsdown/node_modules/@oxc-project/types": { + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.140.0.tgz", + "integrity": "sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/tsdown/node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.0.tgz", + "integrity": "sha512-9yB1l95IrJuNGDFdOYe79vdApdz6WWBCObE+rQ2LUliYUlcyFwSYIb2xb5/Ifw7dAtMy2ZqNyd8QTSOc7duAKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/tsdown/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-pexNaW9ACLUOaBITOpU6qVu4VrsOFIjTv6bzgu0YUATo4eUJx0V605PxwZfndpPOn0ilqGqvGQ0M8UW0IE24jg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/tsdown/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.0.tgz", + "integrity": "sha512-NqKYaq0355ZmNMG4QGpxtEDxsc7tGDhjhCm4PpE0cwnBW+5Il95LJyq414niEiaKLVjnVHBEjSo1wngKxJNiFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/tsdown/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.0.tgz", + "integrity": "sha512-3vPoHzh6eBTz9IbB0/qZdSr0Qeks2echn+I4cHu2joV74VriPDdldswksEDzrl1mBB+oPRi+67+3Ib59paxIPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/tsdown/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.0.tgz", + "integrity": "sha512-E6NNefZ1bUVmKJq2tJkf45J4Zyczj7qm9rUT7NY+Xo2474Y13qWAwc2tvBt0BAVbmtXR1llkxXg0Ou1jbDf2SQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/tsdown/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.0.tgz", + "integrity": "sha512-D+TgkdgM1vu+7/Fpf8+v0ARW+RXEP9Ccazgm8zQ4JFFd9Q7SrYQ2TakU5S5ihazQDgpKyAgZDOcIFsvoHmTZ8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/tsdown/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.0.tgz", + "integrity": "sha512-wUqdwJBbAv0APN87GecstdMUtLjjNTs0hBALpxETD73mccFxdmt/XeizXDtN5RAlBwNKmI+Tg+blect2G+8IeQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/tsdown/node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.0.tgz", + "integrity": "sha512-9DtF35qR9/NrfhM4oxLplCzVVjE+KKm8Pjemi0i/sdhAWkUasjmSo8WTTubNJClhSHCfyk2yeyoXDQEDPtDAAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/tsdown/node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.0.tgz", + "integrity": "sha512-RzuHrBh8X8Hntd2N4VR02QGEciq/9JhcZoTpR/Cee6otRrlILGCf3cg2ygHuih+ZebUnWmMrDX6ITI85btO6rQ==", + "cpu": [ + "s390x" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/tsdown/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.0.tgz", + "integrity": "sha512-MK7L0018jjh1jR3mh21G2j1zAVcpscJBlPo2z19pRjv2XOYGRhaV4LyiD8HO6nCDdZln9IFgCMIV5yt4E3klGQ==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/tinypool": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", - "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "node_modules/tsdown/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.0.tgz", + "integrity": "sha512-gyrxLQ9NfGb/9LoVnC4kb9miUghw1mghnkfYvNHSnVIXriabnfgGPUP4RLcJm87q3KgYz4FYUG8IDiWUT+CpSw==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "node_modules/tsdown/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.0.tgz", + "integrity": "sha512-/6VFMQGRmrhP77KXDC+StIxGzcNp5JOIyYtw0CQ8gPlzhpiIRucYfoM5FaFamHd5BJYIdH86yfP46l1p3WdrFA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tsdown/node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.0.tgz", + "integrity": "sha512-rwdbUL465kisF24WEJLvP3JrEG6E5GRuIHt5wpMwHGERtHe4Wm2CIvtf5gTBgr2tGOHKh5NdKEAFS2VkOPE91g==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "is-number": "^7.0.0" + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { - "node": ">=8.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "node_modules/tsdown/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.0.tgz", + "integrity": "sha512-+5suHwRiKGmhwyUaNT8a5QbrBvLFh2DbO910TEmGRH1aSxwrCezodvGQnulv4uiWEIv1Kq4ypRsJ5+O+ry1DiA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "bin": { - "tree-kill": "cli.js" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/tsdown/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.0.tgz", + "integrity": "sha512-WfFv6/qGufotqBSBzBYwgpCkJBk8Nj7697LL9vTz/XWc67e0r3oewu8iMRwQj3AUL45GVD7wVsPjCsAAtW66Wg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "node_modules/tsdown/node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", "dev": true, - "license": "Apache-2.0" + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "node_modules/tsdown/node_modules/rolldown": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.0.tgz", + "integrity": "sha512-u7tgm5l4Yw1iTqUL4EcYOAt7fFvCgQMLeidrnD4GALlC6aOznCjezYajgxeyKw27u0Q5N7fwgCzjVyPIWzwuBA==", "dev": true, "license": "MIT", "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" + "@oxc-project/types": "=0.140.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" + "node": "^20.19.0 || >=22.12.0" }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.0", + "@rolldown/binding-darwin-arm64": "1.2.0", + "@rolldown/binding-darwin-x64": "1.2.0", + "@rolldown/binding-freebsd-x64": "1.2.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.0", + "@rolldown/binding-linux-arm64-gnu": "1.2.0", + "@rolldown/binding-linux-arm64-musl": "1.2.0", + "@rolldown/binding-linux-ppc64-gnu": "1.2.0", + "@rolldown/binding-linux-s390x-gnu": "1.2.0", + "@rolldown/binding-linux-x64-gnu": "1.2.0", + "@rolldown/binding-linux-x64-musl": "1.2.0", + "@rolldown/binding-openharmony-arm64": "1.2.0", + "@rolldown/binding-wasm32-wasi": "1.2.0", + "@rolldown/binding-win32-arm64-msvc": "1.2.0", + "@rolldown/binding-win32-x64-msvc": "1.2.0" + } + }, + "node_modules/tsdown/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "tsc": "bin/tsc" }, "engines": { - "node": ">=14.17" + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/unconfig-core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", + "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "node_modules/unconfig-core/node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], "license": "MIT" }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, @@ -5202,19 +5509,31 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/verkit": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.1.0.tgz", + "integrity": "sha512-hot+EjuRuW/QsivBRTVKlL5FUVPIGpnjJv0fzuyYyD9Bqaj3Kx2KoSw1WVIoCTOhFkmNJZ0fhl66JdsXjWXztw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || ^24.11.0 || >=26.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -5230,9 +5549,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -5245,13 +5565,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -5277,63 +5600,32 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -5349,12 +5641,15 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -5375,6 +5670,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -5383,22 +5684,12 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vitest/node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -5460,25 +5751,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5490,11 +5762,12 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", + "optional": true, "bin": { "yaml": "bin.mjs" }, @@ -5504,6 +5777,63 @@ "funding": { "url": "https://github.com/sponsors/eemeli" } + }, + "node_modules/yuku-ast": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/yuku-ast/-/yuku-ast-0.7.0.tgz", + "integrity": "sha512-W5UzqYg/Xuyz41cAyxwo/TIPpDX221OuC9U5f44+NulDAHDwtzvGE3M3Xz3mdcLEutWOmTc/WP/y7NO6ANA2gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yuku-toolchain/types": "0.7.0" + } + }, + "node_modules/yuku-codegen": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/yuku-codegen/-/yuku-codegen-0.7.0.tgz", + "integrity": "sha512-RJgoLaIU2AGKRkUHqMtw6gQhYm+NXZm/AFnfn+5YAHgRfApIc/PNJABJHihHoxWltayjbwEOCa68zqxdJafgfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yuku-toolchain/types": "0.7.0" + }, + "optionalDependencies": { + "@yuku-codegen/binding-darwin-arm64": "0.7.0", + "@yuku-codegen/binding-darwin-x64": "0.7.0", + "@yuku-codegen/binding-freebsd-x64": "0.7.0", + "@yuku-codegen/binding-linux-arm-gnu": "0.7.0", + "@yuku-codegen/binding-linux-arm-musl": "0.7.0", + "@yuku-codegen/binding-linux-arm64-gnu": "0.7.0", + "@yuku-codegen/binding-linux-arm64-musl": "0.7.0", + "@yuku-codegen/binding-linux-x64-gnu": "0.7.0", + "@yuku-codegen/binding-linux-x64-musl": "0.7.0", + "@yuku-codegen/binding-win32-arm64": "0.7.0", + "@yuku-codegen/binding-win32-x64": "0.7.0" + } + }, + "node_modules/yuku-parser": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/yuku-parser/-/yuku-parser-0.7.0.tgz", + "integrity": "sha512-72HXJhlPOolJN4ER0xZy1wtAeWZEG4uXfZnzEm2uD7yAWt32VE/52FwIfVGi2Hs2P0JRZK2+sPwULQMTuEzYtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yuku-toolchain/types": "0.7.0", + "yuku-ast": "0.7.0" + }, + "optionalDependencies": { + "@yuku-parser/binding-darwin-arm64": "0.7.0", + "@yuku-parser/binding-darwin-x64": "0.7.0", + "@yuku-parser/binding-freebsd-x64": "0.7.0", + "@yuku-parser/binding-linux-arm-gnu": "0.7.0", + "@yuku-parser/binding-linux-arm-musl": "0.7.0", + "@yuku-parser/binding-linux-arm64-gnu": "0.7.0", + "@yuku-parser/binding-linux-arm64-musl": "0.7.0", + "@yuku-parser/binding-linux-x64-gnu": "0.7.0", + "@yuku-parser/binding-linux-x64-musl": "0.7.0", + "@yuku-parser/binding-win32-arm64": "0.7.0", + "@yuku-parser/binding-win32-x64": "0.7.0" + } } } } diff --git a/package.json b/package.json index 3187e65..5fd6d42 100644 --- a/package.json +++ b/package.json @@ -22,46 +22,53 @@ "dist" ], "type": "commonjs", - "main": "./dist/index.js", + "main": "./dist/index.cjs", "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", + "types": "./dist/index.d.cts", "exports": { "./package.json": "./package.json", ".": { - "import": "./dist/index.mjs", - "default": "./dist/index.js" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "default": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, "scripts": { - "build": "tsup", + "build": "tsdown", "changeset": "changeset", - "changeset:publish": "changeset publish --provenance", - "changeset:version": "changeset version && npm i", - "changeset:prepare-release": "npm run changeset:version && npm run format", + "check-exports": "attw --pack .", + "compile": "tsc --noEmit", + "dev": "tsc --watch", "format": "oxfmt", "format:check": "oxfmt --check", "lint": "oxlint --type-aware", "lint:fix": "oxlint --type-aware --fix", - "check-exports": "attw --pack .", - "compile": "tsc --noEmit", - "dev": "tsc --watch", + "release": "changeset publish --provenance", "test": "vitest --run", "test:coverage": "vitest --coverage", "test:watch": "vitest --watch", - "update-all-packages": "npm-check-updates -u && npm i" + "update-all-packages": "npm-check-updates -u && npm i", + "version-packages": "changeset version && npm i", + "version-packages:prepare-release": "npm run version-packages && npm run format" }, "devDependencies": { - "@arethetypeswrong/cli": "^0.18.2", - "@changesets/cli": "^2.30.0", - "@types/node": "^25.4.0", + "@arethetypeswrong/cli": "^0.18.5", + "@changesets/cli": "^2.31.1", + "@types/node": "^26.1.1", + "@vitest/coverage-v8": "^4.1.10", "husky": "^9.1.7", - "lint-staged": "^16.3.3", - "npm-check-updates": "^19.6.3", - "oxfmt": "^0.38.0", - "oxlint": "^1.53.0", - "oxlint-tsgolint": "^0.16.0", - "tsup": "^8.5.1", - "typescript": "^5.9.3", - "vitest": "^4.0.18" + "lint-staged": "^17.1.0", + "npm-check-updates": "^22.2.9", + "oxfmt": "^0.59.0", + "oxlint": "^1.74.0", + "oxlint-tsgolint": "^0.25.0", + "tsdown": "^0.22.12", + "typescript": "^7.0.2", + "vitest": "^4.1.10" } } diff --git a/src/auto-builder-core.ts b/src/auto-builder-core.ts new file mode 100644 index 0000000..ddcc43d --- /dev/null +++ b/src/auto-builder-core.ts @@ -0,0 +1,534 @@ +/** + * Shared runtime and type primitives for the auto-setter builder factories + * (`CeriosAutoBuilder` and `CeriosClassAutoBuilder`). + * + * @internal + */ + +import type { DeepReadonly, OptionalKeys, RequiredFieldsRecord } from "./types"; + +/** + * Names that are real methods/fields on the builder and therefore cannot be used + * as bare property setters. A property whose name collides with one of these gets + * a `${name}Prop` setter instead (e.g. a property named `build` -> `buildProp`), + * while the real method keeps its name. + * + * @internal + */ +export const RESERVED_BUILDER_NAMES = [ + // Public build variants. + "build", + "buildWithoutRuntimeValidation", + "buildWithoutCompileTimeValidation", + "buildUnsafe", + "buildPartial", + "buildFrozen", + "buildDeepFrozen", + "buildSealed", + "buildDeepSealed", + // Public configuration and state helpers. + "clone", + "addValidator", + "setRequiredFields", + "removeOptionalProperty", + "clearOptionalProperties", + // Property mutators. Protected on `CeriosBuilder`, but still present on the + // prototype chain, so the proxy would return the real method instead of a setter. + "setProperty", + "setProperties", + "setNestedProperty", + "addToArrayProperty", + // Internal helpers. Private in TypeScript, ordinary members at runtime. + // `createBuilder`, `getClassConstructor` and `instantiate` exist only on + // `CeriosClassBuilder`, `snapshot` only on `CeriosBuilder`, but both builders share this + // list so the two stay symmetric. Deep clone/freeze/seal are module functions, not + // members, so they are deliberately absent. + "createBuilder", + "instantiateBuilder", + "getClassConstructor", + "getRequiredTemplate", + "validateRequiredFields", + "runValidators", + "assertValid", + "instantiate", + "snapshot", + // Backing fields. + "_actual", + "_requiredFields", + "_validators", + "_classConstructor", + // `Object.prototype`, reachable through the prototype chain on every instance. + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", + // `__proto__` is deliberately NOT reserved. Reserving it generated a `__proto__Prop` + // setter that mapped straight back to the `__proto__` key, which re-parented the built + // object. It is rejected outright by `assertSafeKey` instead, and the proxy's + // `prop in target` fallback still keeps the real accessor reachable. + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + // Not real members: intercepted by the proxy so `await builder` resolves with the + // builder and `JSON.stringify(builder)` does not pick up a setter function. + "then", + "toJSON", +] as const; + +/** + * Union of the reserved builder member names. + * @internal + */ +export type ReservedBuilderName = (typeof RESERVED_BUILDER_NAMES)[number]; + +/** + * Maps a property key to its auto-setter method name. + * Reserved names get a `Prop` suffix; a literal `${Reserved}Prop` key is excluded + * (no setter) so the suffix scheme stays unambiguous. + * + * @internal + */ +export type SetterName = K extends ReservedBuilderName + ? `${K}Prop` + : K extends `${ReservedBuilderName}Prop` + ? never + : K; + +const RESERVED_SET: ReadonlySet = new Set(RESERVED_BUILDER_NAMES); + +/** + * Resolves an auto-setter method name back to the property key it sets. + * `buildProp` -> `build` (reserved); everything else is identity. + * @internal + */ +function setterNameToKey(name: string): string { + if (name.endsWith("Prop")) { + const base = name.slice(0, -4); + if (RESERVED_SET.has(base)) { + return base; + } + } + return name; +} + +/** + * Deep clone helper for seeding builders from existing objects/instances, and for `clone()`. + * + * The single implementation for both base builders and both auto builders. It is + * cycle-safe, and preserves the built-in types a plain key-walk destroys: `new Date(0)` + * used to clone to `{}`, and self-referencing data threw a `RangeError`. + * + * The prototype is preserved, so a nested class instance stays an instance of its class. + * Flattening it to a plain object loses every method on it, which matters as soon as a + * nested builder contributes a real instance to the parent's state. + * + * @param obj - The value to clone + * @param seen - Cycle-tracking map; callers do not pass this + * @internal + */ +export function deepClone(obj: V, seen: WeakMap = new WeakMap()): V { + if (obj === null || typeof obj !== "object") { + return obj; + } + + const existing = seen.get(obj); + if (existing !== undefined) { + return existing as V; + } + + if (obj instanceof Date) { + return new Date(obj.getTime()) as unknown as V; + } + if (obj instanceof RegExp) { + return new RegExp(obj.source, obj.flags) as unknown as V; + } + + if (Array.isArray(obj)) { + const cloned: unknown[] = []; + seen.set(obj, cloned); + for (const item of obj) { + cloned.push(deepClone(item, seen)); + } + return cloned as unknown as V; + } + if (obj instanceof Map) { + const cloned = new Map(); + seen.set(obj, cloned); + for (const [key, value] of obj) { + cloned.set(deepClone(key, seen), deepClone(value, seen)); + } + return cloned as unknown as V; + } + if (obj instanceof Set) { + const cloned = new Set(); + seen.set(obj, cloned); + for (const value of obj) { + cloned.add(deepClone(value, seen)); + } + return cloned as unknown as V; + } + + const cloned = Object.create(Object.getPrototypeOf(obj) as object | null) as Record; + seen.set(obj, cloned); + for (const key of Object.keys(obj)) { + // defineProperty, not assignment: an own key literally named `__proto__` (which + // JSON.parse produces) would otherwise hit Object.prototype's setter and vanish. + Object.defineProperty(cloned, key, { + value: deepClone((obj as Record)[key], seen), + writable: true, + enumerable: true, + configurable: true, + }); + } + return cloned as V; +} + +/** + * Recursively freezes or seals a value and everything reachable from it. + * + * One implementation for both operations and both builders. The `seen` set makes it + * cycle-safe - `buildDeepFrozen()` on self-referencing data previously threw a + * `RangeError`. Functions are not traversed: walking into a function would freeze its + * `prototype`, which may be an object the caller does not own. + * + * @internal + */ +export function deepHarden(obj: V, mode: "freeze" | "seal", seen: WeakSet = new WeakSet()): V { + if (obj === null || typeof obj !== "object" || seen.has(obj)) { + return obj; + } + seen.add(obj); + + for (const key of Object.getOwnPropertyNames(obj)) { + const descriptor = Object.getOwnPropertyDescriptor(obj, key); + // Getters are left alone; reading one here could run arbitrary user code. + if (descriptor && "value" in descriptor) { + deepHarden(descriptor.value, mode, seen); + } + } + + return mode === "freeze" ? Object.freeze(obj) : Object.seal(obj); +} + +/** + * Builds the copy-on-write successor of an auto builder without invoking any constructor. + * + * Copy-on-write cannot call the subclass constructor: users write `constructor(data?)`, + * which has no way to receive the base class's internal argument shape. But creating a bare + * object off the prototype and assigning only the internal fields silently discards every + * other field the subclass declared - and because the discarded name is then absent from + * the target, the proxy hands back an auto-setter function for it, so `this.cache` reads as + * a function rather than `undefined`. Carrying the source's own properties over first fixes + * both halves of that. + * + * Subclass fields are carried **shallowly**: forks share the same field references, so a + * `Map` held in a field is shared between every builder forked from it. That matches how + * copy-on-write already treats nested data. + * + * @param source - The builder being forked. This is the Proxy; `Reflect.*` forwards to the + * target, and copying descriptors preserves getters and non-enumerable fields. + * @param internals - The internal fields to overwrite after the carry-over + * @internal + */ +export function createBuilderCopy(source: S, internals: Record): S { + const copy = Object.create(Object.getPrototypeOf(source) as object) as Record; + for (const key of Reflect.ownKeys(source)) { + const descriptor = Reflect.getOwnPropertyDescriptor(source, key); + if (descriptor) { + Object.defineProperty(copy, key, descriptor); + } + } + Object.assign(copy, internals); + return copy as S; +} + +/** + * Proxy handler shared by both auto builders. Real members always win; any other + * accessed name becomes a bare property setter that delegates to the inherited + * (hidden) `setProperty`, so copy-on-write re-proxies through `this.constructor`. + * @internal + */ +export const autoSetterHandler: ProxyHandler = { + get(target, prop, receiver) { + // Symbols (Symbol.toPrimitive, Symbol.iterator, inspection hooks, ...) forward to the target. + if (typeof prop === "symbol") { + return Reflect.get(target, prop, receiver); + } + // Reserved names are checked before `prop in target` so this branch stays the single + // source of truth: a name is reserved because the list says so, not because it happens + // to resolve on the prototype chain today. `SetterName` is derived from the same list, + // so the generated types and this runtime can never disagree. `then`/`toJSON` are the + // two entries with no backing member; `Reflect.get` returns undefined for them, which + // is exactly what makes `await builder` resolve and `JSON.stringify` skip them. + // `auto-builder-core-reserved-names.test.ts` fails if a member escapes the list. + if (RESERVED_SET.has(prop)) { + return Reflect.get(target, prop, receiver); + } + // Subclass custom methods and own fields are not reserved but must still win. + if (prop in target) { + return Reflect.get(target, prop, receiver); + } + const key = setterNameToKey(prop); + return (value: unknown) => + (receiver as { setProperty(k: PropertyKey, v: unknown): unknown }).setProperty(key, value); + }, +}; + +/** + * Options accepted by an auto builder's constructor. + * + * The two base builders take `requiredFields` and `validators` in opposite positional + * order (`CeriosBuilder` is `(data, requiredFields, validators)`, `CeriosClassBuilder` is + * `(classConstructor, data, validators, requiredFields)`). Naming them removes that trap + * and lets both auto builders expose one identical constructor shape. + * + * @template T - The type being built + * @template P - The valid required-field path type for this builder + */ +export type BuilderInit = { + requiredFields?: ReadonlyArray

| RequiredFieldsRecord; + validators?: Array<(obj: Partial) => boolean | string>; +}; + +/** + * The return type of a base-builder function: the concrete builder TBuilder extended with + * the shared-logic class instance TShared. + * + * A class declared inside a generic function keeps its base type at the *constraint* of + * the type parameter - the concrete builder's own setters and statics would vanish from + * the returned type without this annotation. The instance intersection puts + * `InstanceType` first so that where a property setter exists on both sides + * (base view and derived builder), calls resolve to the derived flavor and brand the + * derived type's build gate. + * + * @example + * ```typescript + * function BasePostRequestBuilder>(Builder: TBuilder) { + * abstract class PostRequestBuilder extends Builder { + * addTag(tag: string) { + * return this.tags([...(this.buildPartial().tags ?? []), tag]); + * } + * } + * return PostRequestBuilder as BuilderExtension; + * } + * ``` + * + * @template TBuilder - The concrete auto-builder constructor being extended + * @template TShared - The instance type of the shared-logic class + */ +export type BuilderExtension< + // oxlint-disable-next-line typescript/no-explicit-any -- the canonical mixin constraint; `any[]` keeps every builder constructor assignable + TBuilder extends abstract new (...args: any[]) => object, + TShared extends object, +> = (abstract new (...args: ConstructorParameters) => InstanceType & TShared) & { + [K in keyof TBuilder]: TBuilder[K]; +}; + +/** + * Path segments that must never be written through, because assigning to them mutates a + * prototype rather than the object itself. + * @internal + */ +const UNSAFE_PATH_SEGMENTS: ReadonlySet = new Set(["__proto__", "constructor", "prototype"]); + +/** + * Rejects dot-notation paths that would write through a prototype. + * + * `setNestedProperty("__proto__.x", v)` used to walk onto the clone's prototype and assign + * there, so the write silently vanished from the built object. It is harmless today only + * because the clone is always a fresh literal; refusing the path keeps it that way. + * + * @throws {Error} If any segment of the path is `__proto__`, `constructor`, or `prototype` + * @internal + */ +export function assertSafePath(path: string): void { + for (const segment of path.split(".")) { + if (UNSAFE_PATH_SEGMENTS.has(segment)) { + throw new Error( + `Unsafe property path "${path}": the segment "${segment}" would write through a prototype rather than the object.`, + ); + } + } +} + +/** + * Rejects a single property key that would write through a prototype. + * + * Guarding only dot-notation paths was not enough. `__proto__` was reserved, so + * `SetterName` generated a `__proto__Prop` setter, and `setterNameToKey` mapped it straight + * back to `__proto__`. On the class builder the resulting `Object.assign` then re-parented + * the built instance, so it silently stopped being an instance of its own class and lost + * every method. + * + * @throws {Error} If the key is `__proto__`, `constructor`, or `prototype` + * @internal + */ +export function assertSafeKey(key: PropertyKey): void { + // Only `__proto__` is rejected here, not the whole `UNSAFE_PATH_SEGMENTS` set. + // `constructor` and `prototype` are ordinary writable data properties when used as a + // single key - it is only *walking through* them that reaches a prototype, which is + // what `assertSafePath` guards. `__proto__` is different: it has an inherited setter, + // so assigning it re-parents the object. + if (key === "__proto__") { + throw new Error(`Unsafe property key "__proto__": writing it would re-parent the object.`); + } +} + +/** + * Guards the auto-builder constructors' `init` argument. + * + * `setRequiredFields` accepts a bare {@link RequiredFieldsRecord}, so passing that same + * record straight to the constructor - `new B({}, { name: true })` - is an easy mistake. + * It structurally matches `BuilderInit`, so it used to be accepted silently and produce a + * builder with *zero* required fields: validation then passed for every incomplete object. + * Failing loudly is the only safe response. + * + * @throws {Error} If the object has keys but none of them are `BuilderInit` keys + * @internal + */ +export function assertBuilderInit(init: object): void { + const keys = Object.keys(init); + if (keys.length === 0 || keys.some((key) => key === "requiredFields" || key === "validators")) { + return; + } + throw new Error( + `Invalid builder options: expected { requiredFields?, validators? } but received keys ${keys + .map((key) => `"${key}"`) + .join(", ")}. ` + "If you meant to declare required fields, wrap them: { requiredFields: { ... } }.", + ); +} + +/** + * Normalises the two accepted required-field shapes - an array of dot-notation paths, or + * an exhaustive {@link RequiredFieldsRecord} - to the array of paths used internally. + * + * @internal + */ +export function toRequiredPaths(fields: ReadonlyArray | object): ReadonlyArray { + return (Array.isArray(fields) ? fields : Object.keys(fields)) as ReadonlyArray; +} + +/** + * The members both auto builders declare identically once their two axes of variation are + * named: the data view (`T` itself, or `DataPropertiesOnly` for classes) and the brand + * used to gate the validated build variants. + * + * `setProperty`, `setNestedProperty`, and `addToArrayProperty` are deliberately **not** + * here or anywhere on the auto-builder API: an auto builder sets a whole root property + * through its generated setter, and anything finer-grained belongs in a custom method or + * in a director composing multiple builders. + * + * @template T - The type being built + * @template Data - The settable view of T (`T`, or `DataPropertiesOnly` for classes) + * @template Brand - The phantom brand that gates the validated build variants + * @template PathType - The valid required-field path type for this builder + */ +export interface CommonAutoBuilderApi { + /** + * Builds with compile-time and runtime validation. + * Only callable once every required property has been set. + * @throws {Error} If a required field is missing or a validator fails + */ + build(this: this & Brand): T; + + /** + * Builds with compile-time validation only, skipping runtime checks. + */ + buildWithoutRuntimeValidation(this: this & Brand): T; + + /** + * Builds with runtime validation only, skipping compile-time checks. + * @throws {Error} If a required field is missing or a validator fails + */ + buildWithoutCompileTimeValidation(): T; + + /** + * Builds without any validation. + */ + buildUnsafe(): T; + + /** + * Returns the current (possibly incomplete) state. No validation. + */ + buildPartial(): Partial; + + /** + * Builds, validates, and shallowly freezes the result. + * @throws {Error} If a required field is missing or a validator fails + */ + buildFrozen(this: this & Brand): Readonly; + + /** + * Builds, validates, and recursively freezes the result. + * @throws {Error} If a required field is missing or a validator fails + */ + buildDeepFrozen(this: this & Brand): DeepReadonly; + + /** + * Builds, validates, and shallowly seals the result. + * @throws {Error} If a required field is missing or a validator fails + */ + buildSealed(this: this & Brand): T; + + /** + * Builds, validates, and recursively seals the result. + * @throws {Error} If a required field is missing or a validator fails + */ + buildDeepSealed(this: this & Brand): T; + + /** + * Creates an independent copy of the builder with deep-cloned state. + */ + clone(): this; + + /** + * Adds a custom validator that runs during build. + */ + addValidator(validator: (obj: Partial) => boolean | string): this; + + /** + * Sets the runtime-validated required field paths. + * + * Accepts either an array of dot-notation paths, or an exhaustive + * {@link RequiredFieldsRecord} that the compiler forces you to keep complete. + */ + setRequiredFields(fields: ReadonlyArray | RequiredFieldsRecord): this; + + /** + * Removes a previously set optional property. + */ + removeOptionalProperty>(key: K): this; + + /** + * Clears all optional properties, keeping the required ones. + */ + clearOptionalProperties(): this; +} + +/** + * Picks the properties to keep when clearing optional properties. + * + * A required path may be nested (`"address.city"`), in which case the whole root property + * (`address`) is preserved - the required leaf cannot survive without it. Shared by both + * base builders so the two cannot drift; `CeriosClassBuilder` previously treated + * `"address.city"` as a single literal key, found no match, and dropped `address` entirely. + * + * @internal + */ +export function pickRequiredRoots( + actual: Partial, + requiredPaths: ReadonlyArray, +): Partial { + const kept: Partial = {}; + for (const path of requiredPaths) { + const rootKey = path.split(".")[0] as keyof T; + if (rootKey in actual && !(rootKey in kept)) { + kept[rootKey] = actual[rootKey]; + } + } + return kept; +} diff --git a/src/builder-error.ts b/src/builder-error.ts new file mode 100644 index 0000000..17f6aa4 --- /dev/null +++ b/src/builder-error.ts @@ -0,0 +1,91 @@ +/** + * The error thrown by every validating build variant. + * + * The message text is unchanged from when these were plain `Error`s, so existing + * string-matching callers keep working. The structured fields are the reason this type + * exists: they let callers branch on *what* failed without parsing the message. + * + * @example + * ```typescript + * try { + * builder.build(); + * } catch (error) { + * if (error instanceof CeriosBuilderError && error.missingFields.length > 0) { + * promptFor(error.missingFields); + * } + * } + * ``` + */ +export class CeriosBuilderError extends Error { + /** Required field paths that were not set. Empty when a validator failed instead. */ + readonly missingFields: ReadonlyArray; + + /** Messages from failed validators. Empty when required fields were missing instead. */ + readonly validationErrors: ReadonlyArray; + + constructor(message: string, missingFields: ReadonlyArray, validationErrors: ReadonlyArray) { + super(message); + this.name = "CeriosBuilderError"; + this.missingFields = missingFields; + this.validationErrors = validationErrors; + // Required for `instanceof` to work when the package is compiled down to ES5. + Object.setPrototypeOf(this, CeriosBuilderError.prototype); + } +} + +/** + * Describes a validator for error messages: its index, plus its name when it has one. + * Arrow functions assigned to a `const` are named; truly anonymous ones are not. + */ +function describeValidator(validator: unknown, index: number): string { + const name = (validator as { name?: string }).name; + return name ? `Validator #${index} (${name})` : `Validator #${index}`; +} + +/** + * Runs every validator and collects the failures. + * + * Shared by both base builders, which previously held byte-identical private copies. + * + * Two behaviours differ from those copies, both of which were silent failures: + * - A validator returning `false` used to push the bare string `"Validation failed"`, so + * three failing validators produced `"Validation failed: Validation failed; Validation + * failed; Validation failed"` and you could not tell which one failed. It now names the + * validator. + * - A validator returning anything other than `true`, `false`, or a non-empty string - most + * easily `undefined`, from a function that forgot to return - used to count as a *pass*. + * It is now an error, because silently accepting invalid data is the worse outcome. + * + * @param validators - The validators to run + * @param actual - The current builder state to validate + * @returns One message per failure, in validator order + * @internal + */ +export function runValidatorsAgainst( + validators: ReadonlyArray<(obj: Partial) => boolean | string>, + actual: Partial, +): string[] { + const errors: string[] = []; + + for (const [index, validator] of validators.entries()) { + const result = validator(actual); + + if (result === true) { + continue; + } + if (result === false) { + errors.push(`${describeValidator(validator, index)} returned false`); + continue; + } + if (typeof result === "string" && result.length > 0) { + errors.push(result); + continue; + } + errors.push( + `${describeValidator(validator, index)} returned ${result === "" ? "an empty string" : String(result)}; ` + + "expected true, false, or a non-empty error message", + ); + } + + return errors; +} diff --git a/src/cerios-auto-builder.ts b/src/cerios-auto-builder.ts new file mode 100644 index 0000000..af17696 --- /dev/null +++ b/src/cerios-auto-builder.ts @@ -0,0 +1,210 @@ +// oxlint-disable typescript/no-deprecated -- the runtime class extends the deprecated CeriosBuilder until its removal +import { + BuilderInit, + CommonAutoBuilderApi, + SetterName, + assertBuilderInit, + autoSetterHandler, + createBuilderCopy, + toRequiredPaths, +} from "./auto-builder-core"; +import { + BuildGate, + BuilderStep, + CeriosBuilder, + InternalBuilderBrand, + Path, + RequiredFieldsTemplate, +} from "./cerios-builder"; +import { BuilderTargetMarker } from "./types"; + +/** + * Automatic setter methods: one bare `` method per property of T. + * + * Property names that collide with a builder method (e.g. `build`) get a `Prop` + * suffix (`buildProp`); the real method keeps its name. Keys that aren't valid + * identifiers (e.g. `"content-type"`) are set with bracket access + * (`builder["content-type"](value)`). + * + * Each setter returns a {@link BuilderStep} so the phantom-brand compile-time + * tracking accumulates exactly like a hand-written setter. + * + * @template T - The type being built + */ +export type AutoSetters = { + [K in keyof T & string as SetterName]: (this: Self, value: T[K]) => BuilderStep; +}; + +/** + * The public API shared by every auto builder instance. + * + * Deliberately omits the protected `setProperty`/`setProperties`/`setNestedProperty`/ + * `addToArrayProperty` helpers: an auto builder sets a whole root property through its + * generated setter, and anything finer-grained (nested values, appending to arrays) + * belongs in a custom method or a director that composes multiple builders. + * + * @template T - The type being built + */ +export interface AutoBuilderApi + extends BuilderTargetMarker, CommonAutoBuilderApi, Path> {} + +/** + * The typed abstract constructor returned by {@link CeriosAutoBuilder}. Instances + * expose {@link AutoBuilderApi} plus {@link AutoSetters}; the static `from` and + * `requiredTemplate` are inherited from {@link CeriosBuilder}. + * + * @template T - The type being built + */ +export type AutoBuilderConstructor = (abstract new ( + data?: Partial, + init?: BuilderInit>, +) => AutoBuilderApi & AutoSetters) & { + /** + * Creates a builder seeded from an existing object (deep-cloned). + * All properties count as set, so the builder is immediately buildable. + */ + from(this: new (data?: Partial) => R, instance: T): R & InternalBuilderBrand; + + /** + * Optional static list of required field paths validated at runtime. + * @deprecated Prefer passing required fields via the constructor or `setRequiredFields()`. + */ + requiredTemplate?: ReadonlyArray; +}; + +/** + * Constraint for base-builder functions: any auto builder whose target extends TBase. + * + * A base-class expression cannot reference the class's own type parameter, so the + * standard-builder pattern `class B extends CeriosAutoBuilder()` is + * illegal. Instead, write shared logic once in a function constrained on this type and + * apply it on top of each concrete auto builder. Inside the function, `this` carries + * concrete setters for every base property, so shared methods can call them; the derived + * builder still gets its full setter set (base and derived properties alike) from its own + * `CeriosAutoBuilder()` call. + * + * The view is deliberately narrow, for assignability from every derived builder: + * - Setter values are `NonNullable`, so a derived type may strengthen an optional base + * property to required (the same convention a hand-written base builder uses). A shared + * method that sets such a strengthened property brands it as the *base* type's optional + * flavor, which does not count toward the derived build gate - set strengthened + * properties through the derived builder's own setter instead. + * - Of the build/state API only `buildPartial` is visible; the other members either gate + * on a brand or return `this`, both of which make the full {@link AutoBuilderApi} + * contravariant and would reject every derived builder. + * + * @example + * ```typescript + * type BasePostRequest = { postId: string; tags?: string[] }; + * type CreatePostRequest = BasePostRequest & { authorId: string }; + * + * function BasePostRequestBuilder>(Builder: TBuilder) { + * abstract class PostRequestBuilder extends Builder { + * addTag(tag: string) { + * return this.tags([...(this.buildPartial().tags ?? []), tag]); + * } + * } + * return PostRequestBuilder as BuilderExtension; + * } + * + * class CreatePostRequestBuilder extends BasePostRequestBuilder(CeriosAutoBuilder()) { + * static create() { return new CreatePostRequestBuilder(); } + * } + * ``` + * + * @template TBase - The base type the shared logic is written against + */ +export type AutoBuilderBase = abstract new ( + // oxlint-disable-next-line typescript/no-explicit-any -- the canonical mixin constraint; `any[]` keeps every concrete auto-builder constructor assignable + ...args: any[] +) => { + [K in keyof TBase & string as SetterName]: ( + this: Self, + value: NonNullable, + ) => BuilderStep; +} & BuilderTargetMarker & + Pick, "buildPartial">; + +/** + * Concrete runtime backing class for {@link CeriosAutoBuilder}. Extends the real + * builder so it inherits all build/validation/clone/from machinery, and returns a + * Proxy from its constructor so bare-name property setters work. + * @internal + */ +class AutoBuilderRuntime extends CeriosBuilder { + public constructor( + data: Partial = {}, + initOrRequiredFields?: BuilderInit> | RequiredFieldsTemplate, + validators?: Array<(obj: Partial) => boolean | string>, + ) { + // CeriosBuilder's copy-on-write re-creates instances as + // `new this.constructor(data, requiredFields, validators)`, so an array in the second + // position is that internal path; anything else is the user-facing `init` object. + if (initOrRequiredFields !== undefined && !Array.isArray(initOrRequiredFields)) { + const init = initOrRequiredFields as BuilderInit>; + assertBuilderInit(init); + const required = + init.requiredFields === undefined + ? undefined + : (toRequiredPaths(init.requiredFields) as RequiredFieldsTemplate); + super(data, required, init.validators); + } else { + super(data, initOrRequiredFields as RequiredFieldsTemplate | undefined, validators); + } + // The assertion drives Proxy's generic inference to `this`; without it tsc infers + // `object` from the handler and rejects the constructor return type (TS2409). + // oxlint-disable-next-line typescript/no-unnecessary-type-assertion + return new Proxy(this, autoSetterHandler as ProxyHandler); + } + + /** + * Copy-on-write must not go through the subclass constructor. Users write + * `constructor(data?) { super(data, { requiredFields: [...] }) }`, which discards the + * arguments the base class passes - so a later `setRequiredFields()` would be silently + * reverted to whatever the constructor hardcodes. {@link createBuilderCopy} sidesteps + * user constructors while still carrying the subclass's own fields across. + */ + protected override instantiateBuilder( + data: Partial, + requiredFields: RequiredFieldsTemplate | ReadonlySet, + validators: Array<(obj: Partial) => boolean | string>, + ): this { + return new Proxy( + createBuilderCopy(this, { + _actual: data, + _requiredFields: new Set(requiredFields as Iterable), + _validators: [...validators], + }), + autoSetterHandler, + ) as this; + } +} + +/** + * Creates a base class with automatic bare-name setters for every property of T. + * Extend the returned class; add ordinary methods for any custom logic (they use + * the auto setters on `this`, and their return types can be left to inference). + * + * @example + * ```typescript + * interface User { id: string; name: string; role: string; age?: number; } + * + * class UserBuilder extends CeriosAutoBuilder() { + * static create(): UserBuilder { return new UserBuilder({}); } + * asAdmin() { return this.role("admin"); } // inferred BuilderStep + * } + * + * const user = UserBuilder.create().id("1").name("Alice").asAdmin().build(); + * // UserBuilder.create().id("1").build(); // compile error: name and role not set + * ``` + * + * Custom methods use a name of their own (`asAdmin`, `trimmedName`) and delegate to the + * generated setter — a custom method cannot reuse a property's exact name, because the + * method would shadow the setter it needs to call. + * + * @template T - The type to build + * @returns An abstract base class to extend + */ +export function CeriosAutoBuilder(): AutoBuilderConstructor { + return AutoBuilderRuntime as unknown as AutoBuilderConstructor; +} diff --git a/src/cerios-builder.ts b/src/cerios-builder.ts index 44e88ff..8bdac8b 100644 --- a/src/cerios-builder.ts +++ b/src/cerios-builder.ts @@ -1,5 +1,14 @@ // oxlint-disable typescript/no-deprecated -import { DeepReadonly } from "./types"; +import { + assertSafeKey, + assertSafePath, + deepClone, + deepHarden, + pickRequiredRoots, + toRequiredPaths, +} from "./auto-builder-core"; +import { CeriosBuilderError, runValidatorsAgainst } from "./builder-error"; +import { BuilderTargetMarker, DeepReadonly, RequiredFieldsRecord, RequiredKeys, TargetOfMarker } from "./types"; /** * Unique symbol used internally to brand types and track which properties have been set in the builder's type. @@ -11,8 +20,9 @@ declare const __brand: unique symbol; /** * Internal brand for builder type-state tracking. * Prefer helper aliases like `BuilderStep` in public APIs. + * @internal */ -type InternalBuilderBrand = { [__brand]: T }; +export type InternalBuilderBrand = { [__brand]: T }; /** * Type utility for branding builder types with information about which properties have been set. @@ -26,6 +36,18 @@ type InternalBuilderBrand = { [__brand]: T }; */ export type CeriosBrand = InternalBuilderBrand; +/** + * The `this` constraint gating the compile-time-validated build variants. + * + * Normally the accumulated {@link InternalBuilderBrand} must cover every required key of T. + * When T has no required keys there is nothing to track, so the gate dissolves to `unknown` + * and `build()` is callable on a fresh builder — an all-optional type no longer needs a + * throwaway setter call (or a `buildUnsafe()` fallback) before it can build. + * + * @template T - The type being built + */ +export type BuildGate = [RequiredKeys] extends [never] ? unknown : InternalBuilderBrand; + type RootFromPath

= P extends `${infer K}.${string}` ? K : P; type StepKey> = S extends keyof T @@ -73,15 +95,61 @@ export type BuilderComposer, ) => BuilderPreset; +/** + * Extracts the target type built by any builder type. + * + * Hand-written builders are matched nominally against `CeriosBuilder`. Auto builders + * (`CeriosAutoBuilder()`) declare their instance type as `AutoBuilderApi & AutoSetters`, + * which is not assignable to `CeriosBuilder` (its `protected _actual` only matches when it + * originates from the same declaration), so they are matched structurally on `buildUnsafe`. + * + * @internal + */ +type BuilderTargetOf = + B extends CeriosBuilder ? T : B extends { buildUnsafe(): infer T } ? (T extends object ? T : never) : never; + +/** + * The return type of a builder factory or custom builder method, expressed in terms of + * the builder itself. Prefer this over `BuilderStep`/`BuilderPreset` in your own code: + * the target type is derived from the builder, so you only name the builder and the + * keys the method sets. + * + * Works for both hand-written `CeriosBuilder` subclasses and `CeriosAutoBuilder` subclasses. + * + * Use root keys only. A method that sets a nested path brands the *root* key, so a + * method calling `setNestedProperty("meta.note", ...)` returns `BuilderWith`. + * + * @template B - The builder type (a concrete builder class, or `this` inside a method) + * @template S - The keys this method sets. Omit to mean "every key" (fully buildable). + * + * @example + * ```typescript + * class AddressBuilder extends CeriosAutoBuilder

() { + * static create(): AddressBuilder { return new AddressBuilder({}); } + * static createWithDefaults(): BuilderWith { + * return AddressBuilder.create().country("NL"); + * } + * static createComplete(): BuilderWith { ... } + * + * inRotterdam(): BuilderWith { return this.city("Rotterdam"); } + * } + * ``` + */ +export type BuilderWith< + B extends BuilderTargetMarker, + S extends keyof TargetOfMarker = keyof TargetOfMarker, +> = B & InternalBuilderBrand, S>>; + type BuilderBaseFromFactoryReturn = R extends (infer B) & InternalBuilderBrand ? B : R; -type BuilderTargetFromFactoryReturn = BuilderBaseFromFactoryReturn extends CeriosBuilder ? T : never; +type BuilderTargetFromFactoryReturn = BuilderTargetOf>; /** * Helper type for composition callbacks based on a builder factory method. * * This infers both the callback input type (including presets/defaults) and the - * fully-buildable output type directly from the factory return type. + * fully-buildable output type directly from the factory return type. The factory may + * belong to a hand-written `CeriosBuilder` subclass or a `CeriosAutoBuilder` subclass. * * @template F - A builder factory function type (for example: `typeof MyBuilder.createWithDefaults`) */ @@ -110,7 +178,7 @@ export type Path = PathImpl; /** * Helper type to get the value at a specific path, handling optional properties */ -type PathValue = P extends keyof T +export type PathValue = P extends keyof T ? T[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof T @@ -124,46 +192,6 @@ type PathValue = P extends keyof T */ export type RequiredFieldsTemplate = ReadonlyArray>; -/** - * Recursively freezes an object and all its nested properties. - * @param obj - The object to freeze - * @returns The frozen object - * @internal - */ -function deepFreeze(obj: T): T { - // Retrieve the property names defined on obj - Object.getOwnPropertyNames(obj).forEach((prop) => { - const value = (obj as Record)[prop]; - - // Freeze properties before freezing self - if (value !== null && (typeof value === "object" || typeof value === "function")) { - deepFreeze(value); - } - }); - - return Object.freeze(obj); -} - -/** - * Recursively seals an object and all its nested properties. - * @param obj - The object to seal - * @returns The sealed object - * @internal - */ -function deepSeal(obj: T): T { - // Retrieve the property names defined on obj - Object.getOwnPropertyNames(obj).forEach((prop) => { - const value = (obj as Record)[prop]; - - // Seal properties before sealing self - if (value !== null && (typeof value === "object" || typeof value === "function")) { - deepSeal(value); - } - }); - - return Object.seal(obj); -} - /** * Abstract base class for creating type-safe builders with automatic property setters and compile-time validation of required fields. * @@ -187,6 +215,18 @@ function deepSeal(obj: T): T { * * @template T - The complete type being built */ +/** + * Merged declaration giving every `CeriosBuilder` the phantom target marker, so + * `BuilderWith` can resolve what the builder builds. Adds no runtime members. + */ +// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- phantom type-only marker, no runtime members +export interface CeriosBuilder extends BuilderTargetMarker {} + +/** + * @deprecated Will be removed in the next major version. Migrate to `CeriosAutoBuilder()`, + * which generates every setter automatically with the same compile-time safety — see + * MIGRATION.md for a per-feature guide (including how a director replaces `setNestedProperty`). + */ export abstract class CeriosBuilder { /** * Template defining which fields are required for this builder. @@ -227,9 +267,15 @@ export abstract class CeriosBuilder { * .buildSafe(); * ``` */ - setRequiredFields(fields: ReadonlyArray>): this { - this._requiredFields = new Set([...fields] as string[]); - return this; + setRequiredFields(fields: ReadonlyArray> | RequiredFieldsRecord): this { + // Copy-on-write like every other method here (and like + // CeriosClassBuilder.setRequiredFields), so a forked builder cannot retroactively + // change the required fields of the builder it was forked from. + return this.instantiateBuilder( + this._actual, + toRequiredPaths(fields) as RequiredFieldsTemplate, + this._validators, + ); } /** @@ -251,15 +297,7 @@ export abstract class CeriosBuilder { * ``` */ addValidator(validator: (obj: Partial) => boolean | string): this { - const BuilderClass = this.constructor as new ( - data: Partial, - requiredFields?: RequiredFieldsTemplate, - validators?: Array<(obj: Partial) => boolean | string>, - ) => this; - return new BuilderClass(this._actual, Array.from(this._requiredFields) as unknown as RequiredFieldsTemplate, [ - ...this._validators, - validator, - ]); + return this.instantiateBuilder(this._actual, this._requiredFields, [...this._validators, validator]); } /** @@ -269,10 +307,16 @@ export abstract class CeriosBuilder { private getRequiredTemplate(): ReadonlyArray { const ctor = this.constructor as typeof CeriosBuilder; const staticFields = ctor.requiredTemplate ?? []; - const instanceFields = Array.from(this._requiredFields); - // Combine and deduplicate - return [...new Set([...staticFields, ...instanceFields])]; + // Short-circuit the common cases: merging allocates four objects, and one side is + // almost always empty. Matches CeriosClassBuilder.getRequiredTemplate. + if (this._requiredFields.size === 0) { + return staticFields; + } + if (staticFields.length === 0) { + return [...this._requiredFields]; + } + return [...new Set([...staticFields, ...this._requiredFields])]; } /** @@ -282,6 +326,8 @@ export abstract class CeriosBuilder { private validateRequiredFields(): string[] { const requiredPaths = this.getRequiredTemplate(); const missing: string[] = []; + // Mirrors `missing` so the duplicate check below is O(1) instead of a linear scan. + const seen = new Set(); for (const path of requiredPaths) { const keys = path.split("."); @@ -291,6 +337,7 @@ export abstract class CeriosBuilder { const key = keys[i]; if (current === null || current === undefined || typeof current !== "object" || !(key in current)) { missing.push(path); + seen.add(path); break; } current = (current as Record)[key]; @@ -298,8 +345,9 @@ export abstract class CeriosBuilder { // Check if the final value is null or undefined if (current === null || current === undefined) { - if (!missing.includes(path)) { + if (!seen.has(path)) { missing.push(path); + seen.add(path); } } } @@ -312,19 +360,7 @@ export abstract class CeriosBuilder { * @private */ private runValidators(): string[] { - const errors: string[] = []; - - for (const validator of this._validators) { - const result = validator(this._actual); - if (result === false) { - errors.push("Validation failed"); - } else if (typeof result === "string") { - errors.push(result); - } - // If result is true, validation passed - } - - return errors; + return runValidatorsAgainst(this._validators, this._actual); } /** @@ -345,18 +381,9 @@ export abstract class CeriosBuilder { * ``` */ removeOptionalProperty>(key: K): this { - const BuilderClass = this.constructor as new ( - data: Partial, - requiredFields?: RequiredFieldsTemplate, - validators?: Array<(obj: Partial) => boolean | string>, - ) => this; const newData = { ...this._actual }; delete newData[key]; - return new BuilderClass( - newData, - Array.from(this._requiredFields) as unknown as RequiredFieldsTemplate, - this._validators, - ); + return this.instantiateBuilder(newData, this._requiredFields, this._validators); } /** @@ -377,36 +404,10 @@ export abstract class CeriosBuilder { * ``` */ clearOptionalProperties(): this { - const BuilderClass = this.constructor as new ( - data: Partial, - requiredFields?: RequiredFieldsTemplate, - validators?: Array<(obj: Partial) => boolean | string>, - ) => this; - const requiredPaths = this.getRequiredTemplate(); - const newData: Partial = {}; - - // Keep only properties that are in the required template - for (const path of requiredPaths) { - const keys = path.split("."); - if (keys.length === 1) { - const key = keys[0] as keyof T; - if (key in this._actual) { - newData[key] = this._actual[key]; - } - } else { - // For nested paths, preserve the root object if it exists - const rootKey = keys[0] as keyof T; - if (rootKey in this._actual && !(rootKey in newData)) { - newData[rootKey] = this._actual[rootKey]; - } - } - } + // Keep only properties in the required template; a nested path keeps its root object. + const newData = pickRequiredRoots(this._actual, this.getRequiredTemplate()); - return new BuilderClass( - newData, - Array.from(this._requiredFields) as unknown as RequiredFieldsTemplate, - this._validators, - ); + return this.instantiateBuilder(newData, this._requiredFields, this._validators); } /** @@ -441,17 +442,13 @@ export abstract class CeriosBuilder { * @protected */ protected setProperty(key: K, value: T[K]): BuilderStep { - const BuilderClass = this.constructor as new ( - data: Partial, - requiredFields?: RequiredFieldsTemplate, - validators?: Array<(obj: Partial) => boolean | string>, - ) => this; - return new BuilderClass( + assertSafeKey(key); + return this.instantiateBuilder( { ...this._actual, [key]: value, }, - Array.from(this._requiredFields) as unknown as RequiredFieldsTemplate, + this._requiredFields, this._validators, ) as BuilderStep; } @@ -463,17 +460,13 @@ export abstract class CeriosBuilder { * @protected */ protected setProperties(props: Pick): BuilderStep { - const BuilderClass = this.constructor as new ( - data: Partial, - requiredFields?: RequiredFieldsTemplate, - validators?: Array<(obj: Partial) => boolean | string>, - ) => this; - return new BuilderClass( + Object.keys(props).forEach(assertSafeKey); + return this.instantiateBuilder( { ...this._actual, ...props, }, - Array.from(this._requiredFields) as unknown as RequiredFieldsTemplate, + this._requiredFields, this._validators, ) as BuilderStep; } @@ -494,13 +487,9 @@ export abstract class CeriosBuilder { * ``` */ protected setNestedProperty

>(path: P, value: PathValue): BuilderStep { - const BuilderClass = this.constructor as new ( - data: Partial, - requiredFields?: RequiredFieldsTemplate, - validators?: Array<(obj: Partial) => boolean | string>, - ) => this; + assertSafePath(path as string); const keys = (path as string).split("."); - const newActual = this.deepClone(this._actual); + const newActual = deepClone(this._actual); let current = newActual as Record; for (let i = 0; i < keys.length - 1; i++) { @@ -508,39 +497,15 @@ export abstract class CeriosBuilder { const existing = current[key]; if (!(key in current) || typeof existing !== "object" || existing === null) { current[key] = {}; - } else { - current[key] = this.deepClone(existing); } + // No clone here: `newActual` is already a fresh deep clone, so `existing` is + // private to this builder. Re-cloning made the cost O(depth x size). current = current[key] as Record; } - current[keys[keys.length - 1]] = value as unknown; - - return new BuilderClass( - newActual, - Array.from(this._requiredFields) as unknown as RequiredFieldsTemplate, - this._validators, - ) as BuilderStep; - } + current[keys[keys.length - 1]] = value; - /** - * Deep clone helper for nested objects - * @private - */ - private deepClone(obj: V): V { - if (obj === null || typeof obj !== "object") { - return obj; - } - if (Array.isArray(obj)) { - return obj.map((item) => this.deepClone(item)) as unknown as V; - } - const cloned: Record = {}; - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - cloned[key] = this.deepClone((obj as Record)[key]); - } - } - return cloned as V; + return this.instantiateBuilder(newActual, this._requiredFields, this._validators) as BuilderStep; } /** @@ -556,20 +521,16 @@ export abstract class CeriosBuilder { */ protected addToArrayProperty< K extends { [P in keyof T]: NonNullable extends Array ? P : never }[keyof T], - V extends T[K] extends Array ? U : T[K] extends Array | undefined ? U : never, + V extends (T[K] extends Array ? U : T[K] extends Array | undefined ? U : never), >(key: K, value: V): BuilderStep { - const BuilderClass = this.constructor as new ( - data: Partial, - requiredFields?: RequiredFieldsTemplate, - validators?: Array<(obj: Partial) => boolean | string>, - ) => this; + assertSafeKey(key); const currentArray = (this._actual[key] as Array | undefined) ?? []; - return new BuilderClass( + return this.instantiateBuilder( { ...this._actual, [key]: [...currentArray, value], }, - Array.from(this._requiredFields) as unknown as RequiredFieldsTemplate, + this._requiredFields, this._validators, ) as BuilderStep; } @@ -584,18 +545,10 @@ export abstract class CeriosBuilder { * @returns The fully built object of type T * @throws {Error} If any required field is missing at runtime */ - build(this: this & InternalBuilderBrand): T { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } - - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } + build(this: this & BuildGate): T { + this.assertValid("build"); - return this._actual as T; + return this.snapshot(); } /** @@ -607,8 +560,8 @@ export abstract class CeriosBuilder { * * @returns The fully built object of type T */ - buildWithoutRuntimeValidation(this: this & InternalBuilderBrand): T { - return this._actual as T; + buildWithoutRuntimeValidation(this: this & BuildGate): T { + return this.snapshot(); } /** @@ -622,18 +575,9 @@ export abstract class CeriosBuilder { * @throws {Error} If any required field is missing at runtime */ buildWithoutCompileTimeValidation(): T { - const missing = this.validateRequiredFields(); - - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } - - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } + this.assertValid("buildWithoutCompileTimeValidation"); - return this._actual as T; + return this.snapshot(); } /** @@ -646,7 +590,7 @@ export abstract class CeriosBuilder { * @returns The object of type T (may be incomplete) */ buildUnsafe(): T { - return this._actual as T; + return this.snapshot(); } /** @@ -655,8 +599,80 @@ export abstract class CeriosBuilder { * * @returns The partially built object */ + /** + * Runs both runtime checks, throwing on the first failure. + * + * The single copy of what used to be an identical nine-line block in all six validating + * build variants. Centralising it is what lets the thrown error name the method the + * caller actually invoked instead of always saying "build". + * + * @param methodName - The build variant being run, used in the error message + * @throws {CeriosBuilderError} If a required field is missing or a validator fails + */ + private assertValid(methodName: string): void { + const missing = this.validateRequiredFields(); + if (missing.length > 0) { + throw new CeriosBuilderError( + `Missing required fields: ${missing.join(", ")}. Please set these fields before calling ${methodName}.`, + missing, + [], + ); + } + + const validationErrors = this.runValidators(); + if (validationErrors.length > 0) { + throw new CeriosBuilderError(`Validation failed: ${validationErrors.join("; ")}`, [], validationErrors); + } + } + + /** + * Returns the built value as a deep copy. + * + * Every build variant goes through this. Returning `this._actual` directly - as the + * build methods used to - handed the caller the builder's live internal state, so + * mutating the result mutated the builder and every object built from it afterwards. + * `buildFrozen()` was worse: it froze the builder's own state in place. + */ + private snapshot(): T { + return deepClone(this._actual) as T; + } + + /** + * The single construction seam used by every copy-on-write method. + * + * By default this calls the subclass constructor with the internal 3-argument shape, + * which requires the subclass to accept and forward it. Subclasses whose constructor + * has a different signature - such as the auto builders, where users write + * `constructor(data?)` - override this to build the copy without invoking a + * constructor at all, so the required fields and validators cannot be lost. + * + * @param data - The state for the new builder + * @param requiredFields - Required field paths to carry over + * @param validators - Validators to carry over + * @returns A new builder instance of the same concrete type + */ + protected instantiateBuilder( + data: Partial, + requiredFields: RequiredFieldsTemplate | ReadonlySet, + validators: Array<(obj: Partial) => boolean | string>, + ): this { + const BuilderClass = this.constructor as new ( + data: Partial, + requiredFields?: RequiredFieldsTemplate, + validators?: Array<(obj: Partial) => boolean | string>, + ) => this; + // Callers pass the internal Set directly to avoid a Set -> Array -> Set round trip on + // every setter; the constructor only needs something iterable. + const asTemplate = ( + requiredFields instanceof Set ? [...requiredFields] : requiredFields + ) as RequiredFieldsTemplate; + return new BuilderClass(data, asTemplate, validators); + } + buildPartial(): Partial { - return this._actual; + // Deep-copied, not returned live: a shallow spread still let a caller reach into a + // nested object and mutate the builder through it. + return deepClone(this._actual); } /** @@ -673,8 +689,20 @@ export abstract class CeriosBuilder { * const updated = builder.setAge(31).build(); * ``` */ + // The brand marks every property as set, matching `AutoBuilderConstructor.from`: seeding + // from a complete object yields a buildable builder, so callers no longer have to fall + // back to `buildUnsafe()`. + // + // The subclass must declare a public constructor. TypeScript has no way to write "a + // constructor that may be protected" in a `this` constraint, so widening to + // `abstract new` does not help - the auto builders avoid this only because their `from` + // is a declared type rather than a real class static. + static from) => unknown>( + this: B, + instance: T, + ): InstanceType & InternalBuilderBrand; static from) => unknown>(this: B, instance: T): InstanceType { - const clonedData = CeriosBuilder.deepCloneStatic(instance); + const clonedData = deepClone(instance); return new this(clonedData) as InstanceType; } @@ -692,37 +720,8 @@ export abstract class CeriosBuilder { * ``` */ clone(): this { - const BuilderClass = this.constructor as new ( - data: Partial, - requiredFields?: RequiredFieldsTemplate, - validators?: Array<(obj: Partial) => boolean | string>, - ) => this; - const clonedData = this.deepClone(this._actual); - return new BuilderClass( - clonedData, - Array.from(this._requiredFields) as unknown as RequiredFieldsTemplate, - this._validators, - ); - } - - /** - * Static deep clone helper for the from() method. - * @private - */ - private static deepCloneStatic(obj: V): V { - if (obj === null || typeof obj !== "object") { - return obj; - } - if (Array.isArray(obj)) { - return obj.map((item) => this.deepCloneStatic(item)) as unknown as V; - } - const cloned: Record = {}; - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - cloned[key] = this.deepCloneStatic((obj as Record)[key]); - } - } - return cloned as V; + const clonedData = deepClone(this._actual); + return this.instantiateBuilder(clonedData, this._requiredFields, this._validators); } /** @@ -736,18 +735,10 @@ export abstract class CeriosBuilder { * @returns The frozen object of type Readonly * @throws {Error} If any required field is missing at runtime */ - buildFrozen(this: this & InternalBuilderBrand): Readonly { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } - - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } + buildFrozen(this: this & BuildGate): Readonly { + this.assertValid("buildFrozen"); - return Object.freeze(this._actual as T); + return Object.freeze(this.snapshot()); } /** @@ -760,18 +751,10 @@ export abstract class CeriosBuilder { * @returns The deeply frozen object of type DeepReadonly * @throws {Error} If any required field is missing at runtime */ - buildDeepFrozen(this: this & InternalBuilderBrand): DeepReadonly { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } + buildDeepFrozen(this: this & BuildGate): DeepReadonly { + this.assertValid("buildDeepFrozen"); - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } - - return deepFreeze(this._actual as T) as DeepReadonly; + return deepHarden(this.snapshot(), "freeze") as DeepReadonly; } /** @@ -785,18 +768,10 @@ export abstract class CeriosBuilder { * @returns The sealed object of type T * @throws {Error} If any required field is missing at runtime */ - buildSealed(this: this & InternalBuilderBrand): T { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } - - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } + buildSealed(this: this & BuildGate): T { + this.assertValid("buildSealed"); - return Object.seal(this._actual as T); + return Object.seal(this.snapshot()); } /** @@ -810,17 +785,9 @@ export abstract class CeriosBuilder { * @returns The deeply sealed object of type T * @throws {Error} If any required field is missing at runtime */ - buildDeepSealed(this: this & InternalBuilderBrand): T { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } - - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } + buildDeepSealed(this: this & BuildGate): T { + this.assertValid("buildDeepSealed"); - return deepSeal(this._actual as T); + return deepHarden(this.snapshot(), "seal"); } } diff --git a/src/cerios-class-auto-builder.ts b/src/cerios-class-auto-builder.ts new file mode 100644 index 0000000..1c0add0 --- /dev/null +++ b/src/cerios-class-auto-builder.ts @@ -0,0 +1,244 @@ +// oxlint-disable typescript/no-deprecated -- the runtime class extends the deprecated CeriosClassBuilder until its removal +import { + BuilderInit, + CommonAutoBuilderApi, + SetterName, + assertBuilderInit, + autoSetterHandler, + createBuilderCopy, + deepClone, + toRequiredPaths, +} from "./auto-builder-core"; +import { + CeriosClassBuilder, + ClassBuildGate, + ClassBuilderStep, + ClassConstructor, + ClassPath, + DataPropertiesOnly, + InternalClassBrand, +} from "./cerios-class-builder"; +import { BuilderTargetMarker } from "./types"; + +/** + * Automatic setter methods for class data properties: one bare `` + * method per data property of T (methods are excluded automatically). + * + * Property names that collide with a builder method (e.g. `build`) get a `Prop` + * suffix; keys that aren't valid identifiers are set with bracket access. + * Each setter returns a {@link ClassBuilderStep} so brand tracking accumulates. + * + * @template T - The class type being built + */ +export type ClassAutoSetters = { + [K in keyof DataPropertiesOnly & string as SetterName]: ( + this: Self, + value: DataPropertiesOnly[K], + ) => ClassBuilderStep; +}; + +/** + * The public API shared by every class auto builder instance. + * + * Deliberately omits the protected `setProperty`/`setProperties`/`setNestedProperty`/ + * `addToArrayProperty` helpers: an auto builder sets a whole root data property through + * its generated setter, and anything finer-grained (nested values, appending to arrays) + * belongs in a custom method or a director that composes multiple builders. + * + * @template T - The class type being built + */ +export interface ClassAutoBuilderApi + extends + BuilderTargetMarker, + CommonAutoBuilderApi & object, ClassBuildGate, ClassPath> {} + +/** + * The typed abstract constructor returned by {@link CeriosClassAutoBuilder}. + * + * @template T - The class type being built + */ +export type ClassAutoBuilderConstructor = (abstract new ( + data?: Partial>, + init?: BuilderInit, DataPropertiesOnly>, +) => ClassAutoBuilderApi & ClassAutoSetters) & { + /** + * Creates a builder seeded from an existing instance (deep-cloned). + * All data properties count as set, so the builder is immediately buildable. + */ + from( + this: abstract new (data?: Partial>) => R, + instance: T, + ): R & InternalClassBrand>; +}; + +/** + * Constraint for base-builder functions: any class auto builder whose target class + * extends TBase. + * + * A base-class expression cannot reference the class's own type parameter, so a builder + * generic over every subclass of TBase cannot be written directly. Instead, write shared + * logic once in a function constrained on this type and apply it on top of each concrete + * class auto builder. Inside the function, `this` carries concrete setters for every + * inherited data property, so shared methods can call them; the derived builder still + * gets its full setter set from its own `CeriosClassAutoBuilder(Derived)` call. + * + * TBase is used purely as a type and may be an abstract class - only the concrete derived + * classes are ever passed to {@link CeriosClassAutoBuilder}. + * + * The view is deliberately narrow, for assignability from every derived builder: + * - Setter values are `NonNullable`, so a derived class may strengthen an optional base + * property to required. A shared method that sets such a strengthened property brands + * it as the *base* class's optional flavor, which does not count toward the derived + * build gate - set strengthened properties through the derived builder's own setter. + * - Of the build/state API only `buildPartial` is visible; the other members either gate + * on a brand or return `this`, both of which make the full {@link ClassAutoBuilderApi} + * contravariant and would reject every derived builder. + * + * @example + * ```typescript + * abstract class BaseEntity { id!: string; } + * class Order extends BaseEntity { total!: number; } + * + * function BaseEntityBuilder>(Builder: TBuilder) { + * abstract class EntityBuilder extends Builder { + * newId() { return this.id(crypto.randomUUID()); } + * } + * return EntityBuilder as BuilderExtension; + * } + * + * class OrderBuilder extends BaseEntityBuilder(CeriosClassAutoBuilder(Order)) { + * static create() { return new OrderBuilder(); } + * } + * ``` + * + * @template TBase - The base class the shared logic is written against + */ +export type ClassAutoBuilderBase = abstract new ( + // oxlint-disable-next-line typescript/no-explicit-any -- the canonical mixin constraint; `any[]` keeps every concrete auto-builder constructor assignable + ...args: any[] +) => { + [K in keyof DataPropertiesOnly & string as SetterName]: ( + this: Self, + value: NonNullable[K]>, + ) => ClassBuilderStep; +} & BuilderTargetMarker & + Pick, "buildPartial">; + +/** + * One runtime class per target class, rather than per factory call. + * + * Without this, `CeriosClassAutoBuilder(Person) === CeriosClassAutoBuilder(Person)` is + * false, so `instanceof` silently fails whenever the factory is called twice for the same + * class, and every call adds a distinct object shape for the engine to track. Caching is + * only sound because the runtime class captures nothing per call beyond the cache key: + * `instantiateBuilder` reads `this.getClassConstructor()` rather than the closure. + * + * @internal + */ +const RUNTIME_CACHE = new WeakMap>(); + +/** + * Creates a base class with automatic bare-name setters for every data property + * of the class T. Building instantiates the real class, so methods, getters, and + * decorators are preserved. Extend the returned class and add ordinary methods + * for any custom logic. + * + * @example + * ```typescript + * class Person { + * name!: string; + * age!: number; + * constructor(data?: Partial) { if (data) Object.assign(this, data); } + * greet(): string { return `Hi, ${this.name}`; } + * } + * + * class PersonBuilder extends CeriosClassAutoBuilder(Person) { + * static create(): PersonBuilder { return new PersonBuilder(); } + * } + * + * const person = PersonBuilder.create().name("Alice").age(30).build(); + * person.greet(); // "Hi, Alice" + * ``` + * + * Custom methods use a name of their own (`asAdult`, `trimmedName`) and delegate to the + * generated setter — a custom method cannot reuse a data property's exact name, because + * the method would shadow the setter it needs to call. + * + * @param classConstructor - The class constructor to instantiate on build + * @template T - The class type to build + * @returns An abstract base class to extend + */ +export function CeriosClassAutoBuilder( + classConstructor: ClassConstructor, +): ClassAutoBuilderConstructor { + const cached = RUNTIME_CACHE.get(classConstructor); + if (cached) { + return cached as ClassAutoBuilderConstructor; + } + + class ClassAutoBuilderRuntime extends CeriosClassBuilder { + public constructor( + dataOrConstructor?: Partial | ClassConstructor, + initOrData?: BuilderInit, DataPropertiesOnly> | Partial, + validators?: Array<(obj: Partial) => boolean | string>, + requiredFields?: ReadonlyArray> | Set, + ) { + // The base builder's copy-on-write re-creates instances as + // `new this.constructor(classConstructor, data, validators, requiredFields)`, + // so a function first argument is that internal path; anything else is user data. + if (typeof dataOrConstructor === "function") { + super(dataOrConstructor, initOrData as Partial, validators, requiredFields); + } else { + // User-facing shape is (data, init). Required fields and validators are read + // by name, so the opposite positional orders of the two base constructors + // stay an implementation detail rather than a trap. + const init = (initOrData ?? {}) as BuilderInit, DataPropertiesOnly>; + assertBuilderInit(init); + const required = init.requiredFields === undefined ? undefined : new Set(toRequiredPaths(init.requiredFields)); + super(classConstructor, dataOrConstructor ?? {}, init.validators, required); + } + // The assertion drives Proxy's generic inference to `this`; without it tsc infers + // `object` from the handler and rejects the constructor return type (TS2409). + // oxlint-disable-next-line typescript/no-unnecessary-type-assertion + return new Proxy(this, autoSetterHandler as ProxyHandler); + } + + /** + * Copy-on-write must not go through the subclass constructor. Users write + * `constructor(data?) { super(data, { requiredFields: [...] }) }`, which cannot + * receive the base class's internal 4-argument shape - forwarding it would land the + * class constructor in the data slot and drop the validators and required fields. + * {@link createBuilderCopy} sidesteps user constructors while still carrying the + * subclass's own fields across. + */ + protected override instantiateBuilder( + data: Partial, + validators: Array<(obj: Partial) => boolean | string>, + requiredFields: ReadonlyArray> | Set, + ): this { + return new Proxy( + createBuilderCopy(this, { + // Read from the instance, not the captured `classConstructor`, so the + // runtime class holds no per-factory-call state and can be memoised. + _classConstructor: this.getClassConstructor(), + _actual: data, + _validators: [...validators], + _requiredFields: new Set(requiredFields as Iterable), + }), + autoSetterHandler, + ) as this; + } + } + + const Runtime = ClassAutoBuilderRuntime as unknown as { + from: (instance: T) => R; + } & (new (data?: Partial) => unknown); + + Runtime.from = function (this: new (data?: Partial) => R, instance: T): R { + return new this(deepClone(instance)); + }; + + RUNTIME_CACHE.set(classConstructor, ClassAutoBuilderRuntime as unknown as ClassAutoBuilderConstructor); + + return ClassAutoBuilderRuntime as unknown as ClassAutoBuilderConstructor; +} diff --git a/src/cerios-class-builder.ts b/src/cerios-class-builder.ts index 992724c..c0dbbc6 100644 --- a/src/cerios-class-builder.ts +++ b/src/cerios-class-builder.ts @@ -1,5 +1,14 @@ // oxlint-disable typescript/no-deprecated -import type { DeepReadonly } from "./types"; +import { + assertSafeKey, + assertSafePath, + deepClone, + deepHarden, + pickRequiredRoots, + toRequiredPaths, +} from "./auto-builder-core"; +import { CeriosBuilderError, runValidatorsAgainst } from "./builder-error"; +import type { BuilderTargetMarker, DeepReadonly, RequiredFieldsRecord, RequiredKeys, TargetOfMarker } from "./types"; /** * Type representing a class constructor that can be instantiated with optional partial data. @@ -10,16 +19,18 @@ export type ClassConstructor = new (data?: Partial) => T; /** * Helper to extract only data properties (exclude methods) from a class type. * This allows compile-time validation to work properly with classes. + * @internal */ -type DataPropertiesOnly = { - [K in keyof T as T[K] extends (...args: unknown[]) => unknown ? never : K]: T[K]; +export type DataPropertiesOnly = { + [K in keyof T as T[K] extends (...args: never[]) => unknown ? never : K]: T[K]; }; /** * Internal brand for class-builder type-state tracking. * Prefer helper aliases like `ClassBuilderStep` in public APIs. + * @internal */ -type InternalClassBrand = { +export type InternalClassBrand = { readonly __classBuilderBrand: T; }; @@ -32,6 +43,20 @@ type InternalClassBrand = { */ export type CeriosClassBrand = InternalClassBrand; +/** + * The `this` constraint gating the compile-time-validated build variants. + * + * Normally the accumulated {@link InternalClassBrand} must cover every required data + * property of T. When T has no required data properties there is nothing to track, so the + * gate dissolves to `unknown` and `build()` is callable on a fresh builder — an all-optional + * class no longer needs a throwaway setter call before it can build. + * + * @template T - The class type being built + */ +export type ClassBuildGate = [RequiredKeys>] extends [never] + ? unknown + : InternalClassBrand>; + type RootFromPath

= P extends `${infer K}.${string}` ? K : P; type ClassStepKey> = S extends keyof T @@ -77,16 +102,71 @@ export type ClassBuilderComposer< builder: [Preset] extends [never] ? B : ClassBuilderPreset, ) => ClassBuilderPreset>; +/** + * Extracts the class type built by any class-builder type. + * + * Hand-written builders are matched nominally against `CeriosClassBuilder`. Class auto + * builders (`CeriosClassAutoBuilder(Class)`) declare their instance type as + * `ClassAutoBuilderApi & ClassAutoSetters`, which is not assignable to + * `CeriosClassBuilder` (its `protected _actual` only matches when it originates from the + * same declaration), so they are matched structurally on `buildUnsafe`. + * + * @internal + */ +type ClassBuilderTargetOf = + B extends CeriosClassBuilder + ? T + : B extends { buildUnsafe(): infer T } + ? T extends object + ? T + : never + : never; + +/** + * The return type of a class-builder factory or custom method, expressed in terms of the + * builder itself. Prefer this over `ClassBuilderStep`/`ClassBuilderPreset` in your own code: + * the class type is derived from the builder, so you only name the builder and the data + * properties the method sets. Class methods are never counted as properties to set. + * + * Works for both hand-written `CeriosClassBuilder` subclasses and `CeriosClassAutoBuilder` + * subclasses. + * + * Use root data-property names only. A method that sets a nested path brands the *root* + * property, so a method calling `setNestedProperty("address.city", ...)` returns + * `ClassBuilderWith`. + * + * @template B - The class-builder type (a concrete builder class, or `this` inside a method) + * @template S - The data properties this method sets. Omit to mean "every data property". + * + * @example + * ```typescript + * class PersonBuilder extends CeriosClassAutoBuilder(Person) { + * static create(): PersonBuilder { return new PersonBuilder(); } + * static createWithDefaults(): ClassBuilderWith { + * return PersonBuilder.create().age(30); + * } + * } + * ``` + */ +export type ClassBuilderWith< + B extends BuilderTargetMarker, + // Constrained on `keyof TargetOfMarker` rather than `keyof DataPropertiesOnly<...>`: + // the latter is a key-remapping mapped type, which stays deferred against the + // polymorphic `this` type and would reject valid keys in `ClassBuilderWith`. + // `ClassStepKey` below still narrows the brand payload to data properties only. + S extends keyof TargetOfMarker = keyof DataPropertiesOnly>, +> = B & InternalClassBrand>, ClassStepKey, S>>>; + type ClassBuilderBaseFromFactoryReturn = R extends (infer B) & InternalClassBrand ? B : R; -type ClassBuilderTargetFromFactoryReturn = - ClassBuilderBaseFromFactoryReturn extends CeriosClassBuilder ? T : never; +type ClassBuilderTargetFromFactoryReturn = ClassBuilderTargetOf>; /** * Helper type for composition callbacks based on a class-builder factory method. * * This infers both the callback input type (including presets/defaults) and the - * fully-buildable output type directly from the factory return type. + * fully-buildable output type directly from the factory return type. The factory may belong + * to a hand-written `CeriosClassBuilder` subclass or a `CeriosClassAutoBuilder` subclass. * * @template F - A class-builder factory function type (for example: `typeof MyBuilder.createWithDefaults`) */ @@ -125,7 +205,7 @@ export type ClassPath = PathImpl; * @template P - The path string * @internal */ -type ClassPathValue = P extends keyof DataPropertiesOnly +export type ClassPathValue = P extends keyof DataPropertiesOnly ? DataPropertiesOnly[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof DataPropertiesOnly @@ -163,6 +243,18 @@ type ClassPathValue = P extends keyof DataPropertiesOnly * * @template T - The class type being built */ +/** + * Merged declaration giving every `CeriosClassBuilder` the phantom target marker, so + * `ClassBuilderWith` can resolve what the builder builds. Adds no runtime members. + */ +// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- phantom type-only marker, no runtime members +export interface CeriosClassBuilder extends BuilderTargetMarker {} + +/** + * @deprecated Will be removed in the next major version. Migrate to `CeriosClassAutoBuilder(Class)`, + * which generates every setter automatically with the same compile-time safety — see + * MIGRATION.md for a per-feature guide (including how a director replaces `setNestedProperty`). + */ export class CeriosClassBuilder { /** * Optional static template defining which data properties are required. @@ -232,9 +324,12 @@ export class CeriosClassBuilder { /** * Gets the class constructor for this builder. - * @private + * + * Protected rather than private so `instantiateBuilder` overrides can read it from the + * instance instead of capturing it, which is what lets the class auto builder's runtime + * class hold no per-factory-call state. */ - private getClassConstructor(): ClassConstructor { + protected getClassConstructor(): ClassConstructor { return this._classConstructor; } @@ -244,13 +339,87 @@ export class CeriosClassBuilder { * @private */ private createBuilder(data: Partial): this { + return this.instantiateBuilder(data, this._validators, this._requiredFields); + } + + /** + * Runs both runtime checks, throwing on the first failure. + * + * The single copy of what used to be an identical nine-line block in all six validating + * build variants. Centralising it is what lets the thrown error name the method the + * caller actually invoked instead of always saying "build". + * + * @param methodName - The build variant being run, used in the error message + * @throws {CeriosBuilderError} If a required field is missing or a validator fails + */ + private assertValid(methodName: string): void { + const missing = this.validateRequiredFields(); + if (missing.length > 0) { + throw new CeriosBuilderError( + `Missing required fields: ${missing.join(", ")}. Please set these fields before calling ${methodName}.`, + missing, + [], + ); + } + + const validationErrors = this.runValidators(); + if (validationErrors.length > 0) { + throw new CeriosBuilderError(`Validation failed: ${validationErrors.join("; ")}`, [], validationErrors); + } + } + + /** + * Instantiates the target class from the current state. + * + * The single copy of what used to be an identical eight-line block in all eight build + * variants. Classes that assign in their constructor need nothing further; those that + * do not get the data assigned afterwards, which is what the `needsAssign` check + * detects. + */ + private instantiate(): T { + const ctor = this.getClassConstructor(); + // Cloned first: handing `this._actual` to the constructor made the built instance + // alias the builder's own nested state, so mutating the result mutated the builder - + // and `buildDeepFrozen()` froze the builder in place, silently dropping every later + // write. The object builder does the same through `snapshot()`. + const data = deepClone(this._actual); + const instance: T = new ctor(data); + + const dataKeys = Object.keys(data) as (keyof T)[]; + const needsAssign = dataKeys.some((key) => instance[key] === undefined && data[key] !== undefined); + if (needsAssign) { + Object.assign(instance, data); + } + + return instance; + } + + /** + * The single construction seam used by every copy-on-write method. + * + * By default this calls the subclass constructor with the internal 4-argument shape, + * which requires the subclass to accept it. Subclasses whose constructor has a + * different signature - such as the class auto builders, where users write + * `constructor(data?)` - override this to build the copy without invoking a + * constructor at all. + * + * @param data - The state for the new builder + * @param validators - Validators to carry over + * @param requiredFields - Required fields to carry over + * @returns A new builder instance of the same concrete type + */ + protected instantiateBuilder( + data: Partial, + validators: Array<(obj: Partial) => boolean | string>, + requiredFields: ReadonlyArray> | Set, + ): this { const BuilderClass = this.constructor as new ( classConstructor: ClassConstructor, data: Partial, validators?: Array<(obj: Partial) => boolean | string>, requiredFields?: ReadonlyArray> | Set, ) => this; - return new BuilderClass(this._classConstructor, data, this._validators, this._requiredFields); + return new BuilderClass(this._classConstructor, data, validators, requiredFields); } /** @@ -277,10 +446,11 @@ export class CeriosClassBuilder { value: DataPropertiesOnly[K], ): ClassBuilderStep; protected setProperty(key: K, value: T[K]): ClassBuilderStep { + assertSafeKey(key); const newBuilder = this.createBuilder({ ...this._actual, [key]: value, - } as Partial); + }); return newBuilder as ClassBuilderStep; } @@ -294,10 +464,11 @@ export class CeriosClassBuilder { protected setProperties>( props: Pick, K>, ): ClassBuilderStep { + Object.keys(props).forEach(assertSafeKey); const newBuilder = this.createBuilder({ ...this._actual, ...props, - } as Partial); + }); return newBuilder as ClassBuilderStep; } @@ -332,8 +503,9 @@ export class CeriosClassBuilder { path: P, value: ClassPathValue, ): ClassBuilderStep { + assertSafePath(path as string); const keys = (path as string).split("."); - const newActual = this.deepClone(this._actual); + const newActual = deepClone(this._actual); let current = newActual as Record; for (let i = 0; i < keys.length - 1; i++) { @@ -341,13 +513,13 @@ export class CeriosClassBuilder { const existing = current[key]; if (!(key in current) || typeof existing !== "object" || existing === null) { current[key] = {}; - } else { - current[key] = this.deepClone(existing); } + // No clone here: `newActual` is already a fresh deep clone, so `existing` is + // private to this builder. Re-cloning made the cost O(depth x size). current = current[key] as Record; } - current[keys[keys.length - 1]] = value as unknown; + current[keys[keys.length - 1]] = value; const newBuilder = this.createBuilder(newActual); return newBuilder as ClassBuilderStep; @@ -369,14 +541,8 @@ export class CeriosClassBuilder { * .buildWithoutCompileTimeValidation(); * ``` */ - setRequiredFields(fields: ReadonlyArray>): this { - const BuilderClass = this.constructor as new ( - classConstructor: ClassConstructor, - data: Partial, - validators?: Array<(obj: Partial) => boolean | string>, - requiredFields?: ReadonlyArray> | Set, - ) => this; - return new BuilderClass(this._classConstructor, this._actual, this._validators, new Set([...fields] as string[])); + setRequiredFields(fields: ReadonlyArray> | RequiredFieldsRecord>): this { + return this.instantiateBuilder(this._actual, this._validators, new Set(toRequiredPaths(fields))); } /** @@ -404,6 +570,8 @@ export class CeriosClassBuilder { private validateRequiredFields(): string[] { const requiredPaths = this.getRequiredTemplate(); const missing: string[] = []; + // Mirrors `missing` so the duplicate check below is O(1) instead of a linear scan. + const seen = new Set(); for (const path of requiredPaths) { const keys = path.split("."); @@ -413,6 +581,7 @@ export class CeriosClassBuilder { const key = keys[i]; if (current === null || current === undefined || typeof current !== "object" || !(key in current)) { missing.push(path); + seen.add(path); break; } current = (current as Record)[key]; @@ -420,8 +589,9 @@ export class CeriosClassBuilder { // Check if the final value is null or undefined if (current === null || current === undefined) { - if (!missing.includes(path)) { + if (!seen.has(path)) { missing.push(path); + seen.add(path); } } } @@ -448,18 +618,7 @@ export class CeriosClassBuilder { * ``` */ addValidator(validator: (obj: Partial) => boolean | string): this { - const BuilderClass = this.constructor as new ( - classConstructor: ClassConstructor, - data: Partial, - validators?: Array<(obj: Partial) => boolean | string>, - requiredFields?: ReadonlyArray> | Set, - ) => this; - return new BuilderClass( - this._classConstructor, - this._actual, - [...this._validators, validator], - this._requiredFields, - ); + return this.instantiateBuilder(this._actual, [...this._validators, validator], this._requiredFields); } /** @@ -467,19 +626,7 @@ export class CeriosClassBuilder { * @private */ private runValidators(): string[] { - const errors: string[] = []; - - for (const validator of this._validators) { - const result = validator(this._actual); - if (result === false) { - errors.push("Validation failed"); - } else if (typeof result === "string") { - errors.push(result); - } - // If result is true, validation passed - } - - return errors; + return runValidatorsAgainst(this._validators, this._actual); } /** @@ -538,18 +685,8 @@ export class CeriosClassBuilder { * ``` */ clearOptionalProperties(): this { - const requiredProps = this.getRequiredTemplate(); - const newData: Partial = {}; - - // Preserve only properties that are in the required list - for (const prop of requiredProps) { - const key = prop as keyof T; - if (key in this._actual) { - newData[key] = this._actual[key]; - } - } - - return this.createBuilder(newData); + // Keep only properties in the required list; a nested path keeps its root object. + return this.createBuilder(pickRequiredRoots(this._actual, this.getRequiredTemplate())); } /** @@ -564,17 +701,18 @@ export class CeriosClassBuilder { K extends { [P in keyof DataPropertiesOnly]: NonNullable[P]> extends Array ? P : never; }[keyof DataPropertiesOnly], - V extends DataPropertiesOnly[K] extends Array + V extends (DataPropertiesOnly[K] extends Array ? U : DataPropertiesOnly[K] extends Array | undefined ? U - : never, + : never), >(key: K, value: V): ClassBuilderStep { + assertSafeKey(key); const currentArray = (this._actual[key as keyof T] as Array | undefined) ?? []; const newBuilder = this.createBuilder({ ...this._actual, [key]: [...currentArray, value], - } as Partial); + }); return newBuilder as ClassBuilderStep; } @@ -586,25 +724,10 @@ export class CeriosClassBuilder { * @returns The fully built and validated class instance * @throws {Error} If required fields are missing or validation fails */ - build(this: this & InternalClassBrand>): T { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } + build(this: this & ClassBuildGate): T { + this.assertValid("build"); - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } - - const ctor = this.getClassConstructor(); - const instance: T = new ctor(this._actual as T); - // If the class does not assign properties in the constructor, assign them manually - const dataKeys = Object.keys(this._actual) as (keyof T)[]; - const needsAssign = dataKeys.some((key) => instance[key] === undefined && this._actual[key] !== undefined); - if (needsAssign) { - Object.assign(instance, this._actual); - } + const instance = this.instantiate(); return instance; } @@ -617,15 +740,8 @@ export class CeriosClassBuilder { * * @returns The fully built class instance */ - buildWithoutRuntimeValidation(this: this & InternalClassBrand>): T { - const ctor = this.getClassConstructor(); - const instance: T = new ctor(this._actual as T); - // If the class does not assign properties in the constructor, assign them manually - const dataKeys = Object.keys(this._actual) as (keyof T)[]; - const needsAssign = dataKeys.some((key) => instance[key] === undefined && this._actual[key] !== undefined); - if (needsAssign) { - Object.assign(instance, this._actual); - } + buildWithoutRuntimeValidation(this: this & ClassBuildGate): T { + const instance = this.instantiate(); return instance; } @@ -640,24 +756,9 @@ export class CeriosClassBuilder { * @throws {Error} If required fields are missing or validation fails */ buildWithoutCompileTimeValidation(): T { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } - - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } + this.assertValid("buildWithoutCompileTimeValidation"); - const ctor = this.getClassConstructor(); - const instance: T = new ctor(this._actual as T); - // If the class does not assign properties in the constructor, assign them manually - const dataKeys = Object.keys(this._actual) as (keyof T)[]; - const needsAssign = dataKeys.some((key) => instance[key] === undefined && this._actual[key] !== undefined); - if (needsAssign) { - Object.assign(instance, this._actual); - } + const instance = this.instantiate(); return instance; } @@ -668,13 +769,7 @@ export class CeriosClassBuilder { * @returns The built class instance (may be incomplete) */ buildUnsafe(): T { - const ctor = this.getClassConstructor(); - const instance: T = new ctor(this._actual as T); - const dataKeys = Object.keys(this._actual) as (keyof T)[]; - const needsAssign = dataKeys.some((key) => instance[key] === undefined && this._actual[key] !== undefined); - if (needsAssign) { - Object.assign(instance, this._actual); - } + const instance = this.instantiate(); return instance; } @@ -684,7 +779,9 @@ export class CeriosClassBuilder { * @returns The partially built object */ buildPartial(): Partial { - return { ...this._actual }; + // Deep-copied, not returned live: a shallow spread still let a caller reach into a + // nested object and mutate the builder through it. + return deepClone(this._actual); } /** @@ -693,25 +790,10 @@ export class CeriosClassBuilder { * @returns The frozen class instance * @throws {Error} If required fields are missing or validation fails */ - buildFrozen(this: this & InternalClassBrand>): Readonly { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } + buildFrozen(this: this & ClassBuildGate): Readonly { + this.assertValid("buildFrozen"); - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } - - const ctor = this.getClassConstructor(); - const instance: T = new ctor(this._actual as T); - // If the class does not assign properties in the constructor, assign them manually - const dataKeys = Object.keys(this._actual) as (keyof T)[]; - const needsAssign = dataKeys.some((key) => instance[key] === undefined && this._actual[key] !== undefined); - if (needsAssign) { - Object.assign(instance, this._actual); - } + const instance = this.instantiate(); return Object.freeze(instance); } @@ -721,26 +803,11 @@ export class CeriosClassBuilder { * @returns The deeply frozen class instance * @throws {Error} If required fields are missing or validation fails */ - buildDeepFrozen(this: this & InternalClassBrand>): DeepReadonly { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } - - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } + buildDeepFrozen(this: this & ClassBuildGate): DeepReadonly { + this.assertValid("buildDeepFrozen"); - const ctor = this.getClassConstructor(); - const instance: T = new ctor(this._actual as T); - // If the class does not assign properties in the constructor, assign them manually - const dataKeys = Object.keys(this._actual) as (keyof T)[]; - const needsAssign = dataKeys.some((key) => instance[key] === undefined && this._actual[key] !== undefined); - if (needsAssign) { - Object.assign(instance, this._actual); - } - return this.deepFreeze(instance) as DeepReadonly; + const instance = this.instantiate(); + return deepHarden(instance, "freeze") as DeepReadonly; } /** @@ -749,25 +816,10 @@ export class CeriosClassBuilder { * @returns The sealed class instance * @throws {Error} If required fields are missing or validation fails */ - buildSealed(this: this & InternalClassBrand>): T { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } - - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } + buildSealed(this: this & ClassBuildGate): T { + this.assertValid("buildSealed"); - const ctor = this.getClassConstructor(); - const instance: T = new ctor(this._actual as T); - // If the class does not assign properties in the constructor, assign them manually - const dataKeys = Object.keys(this._actual) as (keyof T)[]; - const needsAssign = dataKeys.some((key) => instance[key] === undefined && this._actual[key] !== undefined); - if (needsAssign) { - Object.assign(instance, this._actual); - } + const instance = this.instantiate(); return Object.seal(instance); } @@ -777,78 +829,11 @@ export class CeriosClassBuilder { * @returns The deeply sealed class instance * @throws {Error} If required fields are missing or validation fails */ - buildDeepSealed(this: this & InternalClassBrand>): T { - const missing = this.validateRequiredFields(); - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(", ")}. Please set these fields before calling build.`); - } + buildDeepSealed(this: this & ClassBuildGate): T { + this.assertValid("buildDeepSealed"); - const validationErrors = this.runValidators(); - if (validationErrors.length > 0) { - throw new Error(`Validation failed: ${validationErrors.join("; ")}`); - } - - const ctor = this.getClassConstructor(); - const instance: T = new ctor(this._actual as T); - // If the class does not assign properties in the constructor, assign them manually - const dataKeys = Object.keys(this._actual) as (keyof T)[]; - const needsAssign = dataKeys.some((key) => instance[key] === undefined && this._actual[key] !== undefined); - if (needsAssign) { - Object.assign(instance, this._actual); - } - return this.deepSeal(instance); - } - - /** - * Deep freeze helper. - * @private - */ - private deepFreeze(obj: V): V { - if (obj === null || typeof obj !== "object") { - return obj; - } - Object.getOwnPropertyNames(obj).forEach((prop) => { - const value = (obj as Record)[prop]; - if (value !== null && (typeof value === "object" || typeof value === "function")) { - this.deepFreeze(value); - } - }); - return Object.freeze(obj); - } - - /** - * Deep seal helper. - * @private - */ - private deepSeal(obj: V): V { - if (obj === null || typeof obj !== "object") { - return obj; - } - Object.getOwnPropertyNames(obj).forEach((prop) => { - const value = (obj as Record)[prop]; - if (value !== null && (typeof value === "object" || typeof value === "function")) { - this.deepSeal(value); - } - }); - return Object.seal(obj); - } - - /** - * Deep clone helper for nested objects. - * @private - */ - private deepClone(obj: V): V { - if (obj === null || typeof obj !== "object") { - return obj; - } - if (Array.isArray(obj)) { - return obj.map((item) => this.deepClone(item)) as unknown as V; - } - const cloned: Record = {}; - for (const key of Object.keys(obj)) { - cloned[key] = this.deepClone((obj as Record)[key]); - } - return cloned as V; + const instance = this.instantiate(); + return deepHarden(instance, "seal"); } /** @@ -871,7 +856,7 @@ export class CeriosClassBuilder { classConstructor: ClassConstructor, instance: T, ): InstanceType { - const clonedData = CeriosClassBuilder.deepCloneStatic(instance); + const clonedData = deepClone(instance); return new this(classConstructor, clonedData) as InstanceType; } @@ -889,25 +874,7 @@ export class CeriosClassBuilder { * ``` */ clone(): this { - const clonedData = this.deepClone(this._actual); + const clonedData = deepClone(this._actual); return this.createBuilder(clonedData); } - - /** - * Static deep clone helper for the from() method. - * @private - */ - private static deepCloneStatic(obj: V): V { - if (obj === null || typeof obj !== "object") { - return obj; - } - if (Array.isArray(obj)) { - return obj.map((item) => this.deepCloneStatic(item)) as unknown as V; - } - const cloned: Record = {}; - for (const key of Object.keys(obj)) { - cloned[key] = this.deepCloneStatic((obj as Record)[key]); - } - return cloned as V; - } } diff --git a/src/index.ts b/src/index.ts index 9a553e7..79c8873 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,40 @@ // oxlint-disable typescript/no-deprecated -export { +export { CeriosBuilder } from "./cerios-builder.js"; +export type { + BuildGate, BuilderComposer, BuilderComposerFromFactory, BuilderPreset, BuilderStep, + BuilderWith, CeriosBrand, - CeriosBuilder, + InternalBuilderBrand, + Path, RequiredFieldsTemplate, } from "./cerios-builder.js"; -export { - CeriosClassBuilder, +export type { BuilderExtension, BuilderInit } from "./auto-builder-core.js"; +export { CeriosBuilderError } from "./builder-error.js"; +export { CeriosAutoBuilder } from "./cerios-auto-builder.js"; +export type { AutoBuilderApi, AutoBuilderBase, AutoBuilderConstructor, AutoSetters } from "./cerios-auto-builder.js"; +export { CeriosClassAutoBuilder } from "./cerios-class-auto-builder.js"; +export type { + ClassAutoBuilderApi, + ClassAutoBuilderBase, + ClassAutoBuilderConstructor, + ClassAutoSetters, +} from "./cerios-class-auto-builder.js"; +export { CeriosClassBuilder } from "./cerios-class-builder.js"; +export type { + CeriosClassBrand, + ClassBuildGate, ClassBuilderComposer, ClassBuilderComposerFromFactory, ClassBuilderPreset, ClassBuilderStep, + ClassBuilderWith, ClassConstructor, ClassPath, + DataPropertiesOnly, + InternalClassBrand, } from "./cerios-class-builder.js"; -export { BuilderType, DeepReadonly, OptionalKeys } from "./types.js"; +export type { BuilderType, DeepReadonly, OptionalKeys, RequiredFieldsRecord, RequiredKeys } from "./types.js"; diff --git a/src/types.ts b/src/types.ts index ab882f6..6eb6840 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,6 +20,35 @@ */ export type BuilderType = B; +/** + * Unique symbol keying the phantom "what does this builder build" marker. + * Never present at runtime - the property is declared optional and never assigned. + * @internal + */ +declare const __target: unique symbol; + +/** + * Phantom marker carried by every builder, recording the type it builds. + * + * This exists so `BuilderWith`/`ClassBuilderWith` can read the target type through an + * *indexed access* (`B[typeof __target]`) rather than a conditional type. Conditional + * types stay deferred when applied to the polymorphic `this` type, which would make + * `BuilderWith` unusable inside a builder method; an indexed access + * resolves against `this`'s constraint and works. + * + * @internal + */ +export interface BuilderTargetMarker { + readonly [__target]?: T; +} + +/** + * Reads the target type out of a builder type by indexed access. Uses `& {}` (an + * intersection, which resolves on `this`) rather than `NonNullable`'s conditional form. + * @internal + */ +export type TargetOfMarker> = B[typeof __target] & {}; + /** * Helper type to extract optional keys from a type. * Returns keys where the property can be undefined. @@ -30,6 +59,39 @@ export type OptionalKeys = { [K in keyof T]-?: undefined extends T[K] ? K : never; }[keyof T]; +/** + * Helper type to extract required keys from a type. + * The complement of {@link OptionalKeys}. + * + * @template T - The type to extract required keys from + */ +export type RequiredKeys = { + [K in keyof T]-?: undefined extends T[K] ? never : K; +}[keyof T]; + +/** + * An exhaustive map of the required keys of T, used as an alternative to a hand-written + * array of required-field paths. + * + * The array form (`["id", "name"]`) is checked for validity but not for completeness: + * `build()` compile-gates on every required key of T, while runtime validation only checks + * the paths you listed, so adding a required property to T silently leaves runtime + * validation behind. This record form must name every required key, so the same change + * becomes a compile error at the builder. + * + * Full derivation from T alone is impossible - types are erased before runtime, so the + * key list has to exist as a value somewhere. This is the closest the compiler can enforce. + * + * @template T - The type being built + * + * @example + * ```typescript + * // Adding `email: string` to User makes this line fail to compile. + * builder.setRequiredFields({ id: true, name: true }); + * ``` + */ +export type RequiredFieldsRecord = { [K in RequiredKeys]: true }; + /** * Recursively makes all properties readonly for deep immutability. * Handles arrays, objects, and primitive types. diff --git a/tests/auto-builder-core/auto-builder-core-build-isolation.test.ts b/tests/auto-builder-core/auto-builder-core-build-isolation.test.ts new file mode 100644 index 0000000..187add1 --- /dev/null +++ b/tests/auto-builder-core/auto-builder-core-build-isolation.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +type Config = { + name: string; + profile?: { tags: string[] }; +}; + +class Person { + name!: string; + address?: { city: string }; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + greet(): string { + return `hi ${this.name}`; + } +} + +class ConfigBuilder extends CeriosAutoBuilder() { + static create(): ConfigBuilder { + return new ConfigBuilder({}); + } +} + +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); + } +} + +// A build result must be the caller's to keep. Earlier suites only asserted that a build +// result was frozen/sealed and that the builder stayed *usable* - which it does, because +// setters spread into a new object. Neither checked that the result was a copy, so two +// separate aliasing bugs survived. Every variant is covered here, on both builders. +// Both builders are exercised through one shape. The factories are erased to a common +// structural type because the two brand chains are unrelated and would not unify. +type AnyBuilder = { + build(): object; + buildUnsafe(): object; + buildFrozen(): object; + buildSealed(): object; + buildDeepFrozen(): object; + buildDeepSealed(): object; + buildPartial(): object; +}; + +describe.each([ + [ + "object", + (): AnyBuilder => + ConfigBuilder.create() + .name("n") + .profile({ tags: ["a"] }) as unknown as AnyBuilder, + ], + ["class", (): AnyBuilder => PersonBuilder.create().name("n").address({ city: "R" }) as unknown as AnyBuilder], +] as const)("%s builder - build results are owned by the caller", (_kind, make) => { + it("returns a distinct object from build()", () => { + const builder = make(); + + expect(builder.build()).not.toBe(builder.build()); + }); + + it("returns a distinct object from buildUnsafe()", () => { + const builder = make(); + + expect(builder.buildUnsafe()).not.toBe(builder.buildUnsafe()); + }); + + it("returns a distinct object from buildFrozen()", () => { + const builder = make(); + + expect(builder.buildFrozen()).not.toBe(builder.buildFrozen()); + }); + + it("returns a distinct object from buildSealed()", () => { + const builder = make(); + + expect(builder.buildSealed()).not.toBe(builder.buildSealed()); + }); + + it("does not let a mutated build result reach the builder", () => { + const builder = make(); + const built = builder.build() as Record; + + if (built.profile?.tags) { + built.profile.tags.push("MUTATED"); + } + if (built.address) { + built.address.city = "MUTATED"; + } + + const rebuilt = builder.build() as Record; + expect(rebuilt.profile?.tags ?? ["a"]).toEqual(["a"]); + expect(rebuilt.address?.city ?? "R").toBe("R"); + }); + + it("leaves the builder unfrozen after buildFrozen()", () => { + const builder = make(); + builder.buildFrozen(); + + // The builder's own state must not be frozen in place, or later builds silently + // drop writes. + expect(Object.isFrozen(builder.buildPartial())).toBe(false); + expect(builder.buildUnsafe()).toBeDefined(); + }); + + it("still produces a mutable result after buildDeepFrozen()", () => { + const builder = make(); + builder.buildDeepFrozen(); + + const after = builder.buildUnsafe() as Record; + expect(Object.isFrozen(after)).toBe(false); + const nested = (after.profile ?? after.address) as object | undefined; + expect(nested).toBeDefined(); + expect(Object.isFrozen(nested)).toBe(false); + }); + + it("still produces an unsealed result after buildDeepSealed()", () => { + const builder = make(); + builder.buildDeepSealed(); + + expect(Object.isSealed(builder.buildUnsafe())).toBe(false); + }); + + it("does not let buildFrozen() make a later buildSealed() frozen", () => { + const builder = make(); + builder.buildFrozen(); + + // buildSealed promises the result's existing properties stay writable. + expect(Object.isFrozen(builder.buildSealed())).toBe(false); + }); +}); + +describe("class builder keeps its prototype and identity", () => { + it("returns a real instance that does not alias builder state", () => { + const builder = PersonBuilder.create().name("n").address({ city: "R" }); + const first = builder.build(); + first.address!.city = "MUTATED"; + + const second = builder.build(); + expect(second).toBeInstanceOf(Person); + expect(second.greet()).toBe("hi n"); + expect(second.address?.city).toBe("R"); + }); +}); diff --git a/tests/auto-builder-core/auto-builder-core-deep-clone.test.ts b/tests/auto-builder-core/auto-builder-core-deep-clone.test.ts new file mode 100644 index 0000000..b3f0128 --- /dev/null +++ b/tests/auto-builder-core/auto-builder-core-deep-clone.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it } from "vitest"; + +import { deepClone, deepHarden } from "../../src/auto-builder-core"; +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; + +type Bag = { + when?: Date; + pattern?: RegExp; + lookup?: Map; + tags?: Set; + nested?: { deep: { value: number } }; + list?: number[]; + // `unknown`, not `any`: a cyclic fixture typed as `any` makes the recursive Path + // helper expand without bound. + cyclic?: unknown; +}; + +class BagBuilder extends CeriosAutoBuilder() { + static create(): BagBuilder { + return new BagBuilder({}); + } +} + +// A plain key-walk turns every one of these into `{}` and overflows the stack on a cycle. +// Both base builders and both auto builders share this one implementation. +describe("deepClone - built-in types", () => { + it("round-trips a Date as a Date, not an empty object", () => { + const source = new Date(0); + const cloned = deepClone({ when: source }).when; + + expect(cloned).toBeInstanceOf(Date); + expect(cloned.getTime()).toBe(0); + expect(cloned).not.toBe(source); + }); + + it("round-trips a RegExp with its flags", () => { + const cloned = deepClone({ pattern: /abc/gi }).pattern; + + expect(cloned).toBeInstanceOf(RegExp); + expect(cloned.source).toBe("abc"); + expect(cloned.flags).toBe("gi"); + }); + + it("round-trips a Map and a Set, deep-cloning their contents", () => { + const source = { lookup: new Map([["a", 1]]), tags: new Set(["x"]) }; + const cloned = deepClone(source); + + expect(cloned.lookup).toBeInstanceOf(Map); + expect(cloned.lookup.get("a")).toBe(1); + expect(cloned.tags).toBeInstanceOf(Set); + expect(cloned.tags.has("x")).toBe(true); + expect(cloned.lookup).not.toBe(source.lookup); + }); + + it("preserves an own key literally named __proto__", () => { + // JSON.parse produces exactly this. Plain assignment hits Object.prototype's setter + // and drops the key. + const parsed = JSON.parse('{"__proto__":{"x":1},"a":2}') as Record; + const cloned = deepClone({ data: parsed }).data; + + expect(Object.keys(cloned).sort()).toEqual(["__proto__", "a"]); + expect(({} as Record).x).toBeUndefined(); + }); + + it("terminates on a self-referencing object and preserves the cycle", () => { + const cyclic: Record = { n: 1 }; + cyclic.self = cyclic; + + const cloned = deepClone(cyclic); + + expect(cloned.n).toBe(1); + expect(cloned.self).toBe(cloned); + expect(cloned).not.toBe(cyclic); + }); + + it("preserves shared references rather than duplicating them", () => { + const shared = { id: 1 }; + const cloned = deepClone({ a: shared, b: shared }); + + expect(cloned.a).toBe(cloned.b); + }); +}); + +describe("deepHarden - cycles", () => { + it("freezes a self-referencing object without overflowing the stack", () => { + const cyclic: Record = { n: 1 }; + cyclic.self = cyclic; + + expect(() => deepHarden(cyclic, "freeze")).not.toThrow(); + expect(Object.isFrozen(cyclic)).toBe(true); + }); + + it("seals a self-referencing object without overflowing the stack", () => { + const cyclic: Record = { n: 1 }; + cyclic.self = cyclic; + + expect(() => deepHarden(cyclic, "seal")).not.toThrow(); + expect(Object.isSealed(cyclic)).toBe(true); + }); +}); + +describe("builders with awkward data", () => { + it("seeds from an object holding a Date and keeps it usable", () => { + const built = BagBuilder.from({ when: new Date(0) }).build(); + + expect(built.when).toBeInstanceOf(Date); + }); + + it("clones a builder holding a Date without flattening it", () => { + const cloned = BagBuilder.create().when(new Date(0)).clone(); + + expect(cloned.buildPartial().when).toBeInstanceOf(Date); + }); + + it("deep-freezes cyclic data instead of overflowing the stack", () => { + const cyclic: Record = { n: 1 }; + cyclic.self = cyclic; + + expect(() => BagBuilder.create().cyclic(cyclic).buildDeepFrozen()).not.toThrow(); + }); +}); diff --git a/tests/auto-builder-core/auto-builder-core-errors.test.ts b/tests/auto-builder-core/auto-builder-core-errors.test.ts new file mode 100644 index 0000000..d38133d --- /dev/null +++ b/tests/auto-builder-core/auto-builder-core-errors.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosBuilderError } from "../../src/builder-error"; +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +type User = { + id: string; + name: string; + age?: number; +}; + +class Person { + id!: string; + name!: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +class UserBuilder extends CeriosAutoBuilder() { + static create(): UserBuilder { + return new UserBuilder({}, { requiredFields: ["id", "name"] }); + } +} + +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(undefined, { requiredFields: ["id", "name"] }); + } +} + +function captureError(fn: () => unknown): unknown { + try { + fn(); + } catch (error) { + return error; + } + return expect.unreachable("should have thrown"); +} + +describe("CeriosBuilderError", () => { + it("carries the missing fields as data, not just in the message", () => { + const error = captureError(() => UserBuilder.create().id("1").buildWithoutCompileTimeValidation()); + + expect(error).toBeInstanceOf(CeriosBuilderError); + expect((error as CeriosBuilderError).missingFields).toEqual(["name"]); + expect((error as CeriosBuilderError).validationErrors).toEqual([]); + }); + + it("carries validator failures as data", () => { + const error = captureError(() => + UserBuilder.create() + .id("1") + .name("n") + .addValidator(() => "too short") + .buildWithoutCompileTimeValidation(), + ); + + expect((error as CeriosBuilderError).validationErrors).toEqual(["too short"]); + expect((error as CeriosBuilderError).missingFields).toEqual([]); + }); + + it("is still an Error, so existing catch blocks keep working", () => { + expect(() => UserBuilder.create().buildWithoutCompileTimeValidation()).toThrow(Error); + }); + + it("names the build variant that was actually called", () => { + const incomplete = UserBuilder.create().id("1"); + const unsafe = incomplete as unknown as { buildDeepSealed: () => User }; + + // This used to advise "before calling build." no matter which variant threw. + expect(() => unsafe.buildDeepSealed()).toThrow("before calling buildDeepSealed."); + }); + + it("reports the same way from the class auto builder", () => { + const error = captureError(() => PersonBuilder.create().id("1").buildWithoutCompileTimeValidation()); + + expect(error).toBeInstanceOf(CeriosBuilderError); + expect((error as CeriosBuilderError).missingFields).toEqual(["name"]); + }); +}); + +describe("validator result handling", () => { + it("identifies which validator returned false", () => { + const builder = UserBuilder.create() + .id("1") + .name("n") + .addValidator(function tooYoung() { + return false; + }) + .addValidator(function tooOld() { + return false; + }); + + // Previously this was "Validation failed: Validation failed; Validation failed" - + // three failing validators were indistinguishable. + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("tooYoung"); + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("tooOld"); + }); + + it("falls back to the index for an anonymous validator", () => { + const builder = UserBuilder.create() + .id("1") + .name("n") + .addValidator(() => false); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Validator #0"); + }); + + it("treats a validator returning undefined as a failure, not a pass", () => { + // Forgetting to return is easy; silently accepting invalid data is the worse outcome. + const builder = UserBuilder.create() + .id("1") + .name("n") + .addValidator(() => undefined as unknown as boolean); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("expected true, false, or a non-empty"); + }); + + it("treats an empty-string return as a failure rather than an empty message", () => { + const builder = UserBuilder.create() + .id("1") + .name("n") + .addValidator(() => ""); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("an empty string"); + }); + + it("still passes on true and still reports a string message verbatim", () => { + expect(() => + UserBuilder.create() + .id("1") + .name("n") + .addValidator(() => true) + .buildWithoutCompileTimeValidation(), + ).not.toThrow(); + + expect(() => + UserBuilder.create() + .id("1") + .name("n") + .addValidator(() => "custom message") + .buildWithoutCompileTimeValidation(), + ).toThrow("Validation failed: custom message"); + }); +}); diff --git a/tests/auto-builder-core/auto-builder-core-reserved-names.test.ts b/tests/auto-builder-core/auto-builder-core-reserved-names.test.ts new file mode 100644 index 0000000..c12cbcc --- /dev/null +++ b/tests/auto-builder-core/auto-builder-core-reserved-names.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from "vitest"; + +import { RESERVED_BUILDER_NAMES } from "../../src/auto-builder-core"; +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +type Plain = { name: string }; + +class Instance { + name!: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +class PlainBuilder extends CeriosAutoBuilder() { + static create(): PlainBuilder { + return new PlainBuilder({}); + } +} + +class InstanceBuilder extends CeriosClassAutoBuilder(Instance) { + static create(): InstanceBuilder { + return new InstanceBuilder(); + } +} + +const RESERVED = new Set(RESERVED_BUILDER_NAMES); + +// `then` and `toJSON` are reserved without being real members: the proxy intercepts them so +// `await builder` resolves and `JSON.stringify(builder)` does not serialise a setter. +const RESERVED_WITHOUT_MEMBER = new Set(["then", "toJSON"]); + +// `__proto__` is the reverse: a real member that must NOT be reserved. Reserving it +// generated a `__proto__Prop` setter which mapped back to the `__proto__` key and +// re-parented the built object - on the class builder that silently stripped the instance +// of its prototype and every method. It is rejected by `assertSafeKey` instead, and the +// proxy's `prop in target` fallback still returns the real accessor. +const DELIBERATELY_UNRESERVED = new Set(["__proto__"]); + +/** + * Every name reachable on the instance, walking the prototype chain through Object.prototype. + * This is exactly the set the proxy's `prop in target` check would match. + */ +function reachableNames(instance: object): string[] { + const found = new Set(); + let current: object | null = instance; + while (current) { + for (const name of Object.getOwnPropertyNames(current)) { + found.add(name); + } + current = Object.getPrototypeOf(current) as object | null; + } + return [...found]; +} + +// The auto setters are generated from RESERVED_BUILDER_NAMES, while the proxy falls back to +// `prop in target`. If a real member is missing from the list, the generated type says +// "bare setter" and the runtime hands back the real method instead - a silent mismatch that +// corrupts state rather than raising an error. These tests keep the two in lockstep. +describe("auto builder reserved names", () => { + it("reserves every runtime member of the object auto builder", () => { + const missing = reachableNames(PlainBuilder.create() as object).filter( + (name) => !RESERVED.has(name) && !DELIBERATELY_UNRESERVED.has(name), + ); + + expect(missing).toEqual([]); + }); + + it("reserves every runtime member of the class auto builder", () => { + const missing = reachableNames(InstanceBuilder.create() as object).filter( + (name) => !RESERVED.has(name) && !DELIBERATELY_UNRESERVED.has(name), + ); + + expect(missing).toEqual([]); + }); + + it("has no stale entries - every reserved name is a real member of one of the builders", () => { + const real = new Set([ + ...reachableNames(PlainBuilder.create() as object), + ...reachableNames(InstanceBuilder.create() as object), + ]); + const stale = [...RESERVED].filter((name) => !real.has(name) && !RESERVED_WITHOUT_MEMBER.has(name)); + + expect(stale).toEqual([]); + }); + + it("routes every reserved name to a *Prop setter that sets the underlying property", () => { + for (const name of RESERVED_BUILDER_NAMES) { + const builder = PlainBuilder.create() as unknown as Record unknown>; + const stepped = builder[`${name}Prop`]("x") as { buildPartial(): Record }; + + expect(stepped.buildPartial()[name]).toBe("x"); + } + }); + + it("returns the real member, never a setter, for a bare reserved name", () => { + const builder = PlainBuilder.create() as unknown as Record; + const memberless = [...RESERVED_BUILDER_NAMES].filter((name) => RESERVED_WITHOUT_MEMBER.has(name)); + const withMember = [...RESERVED_BUILDER_NAMES].filter((name) => !RESERVED_WITHOUT_MEMBER.has(name)); + + for (const name of memberless) { + expect(builder[name]).toBeUndefined(); + } + for (const name of withMember) { + // A setter would be a fresh 1-arg arrow function on every access; a real member + // is stable across accesses. + expect(builder[name]).toBe(builder[name]); + } + }); +}); diff --git a/tests/auto-builder-core/auto-builder-core-safety.test.ts b/tests/auto-builder-core/auto-builder-core-safety.test.ts new file mode 100644 index 0000000..5c0eee7 --- /dev/null +++ b/tests/auto-builder-core/auto-builder-core-safety.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +type Config = { + name: string; + nested?: { inner: { count: number } }; +}; + +class Person { + name!: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +class ConfigBuilder extends CeriosAutoBuilder() { + static create(): ConfigBuilder { + return new ConfigBuilder({}); + } +} + +// Handing back `this._actual` let a caller mutate the builder through the built object, +// and buildFrozen() froze the builder's own state in place. +describe("build variants return owned state", () => { + it("build() does not alias the builder's internal state", () => { + const builder = ConfigBuilder.create() + .name("n") + .nested({ inner: { count: 1 } }); + + const first = builder.build(); + first.nested!.inner.count = 99; + + expect(builder.build().nested?.inner.count).toBe(1); + }); + + it("returns a distinct object on each build", () => { + const builder = ConfigBuilder.create().name("n"); + + expect(builder.build()).not.toBe(builder.build()); + }); + + it("buildUnsafe() does not alias the builder's internal state", () => { + const builder = ConfigBuilder.create().name("n"); + builder.buildUnsafe().name = "mutated"; + + expect(builder.buildUnsafe().name).toBe("n"); + }); + + it("buildPartial() does not leak nested state", () => { + const builder = ConfigBuilder.create() + .name("n") + .nested({ inner: { count: 1 } }); + + const partial = builder.buildPartial(); + partial.nested!.inner.count = 99; + + expect(builder.buildPartial().nested?.inner.count).toBe(1); + }); + + it("buildFrozen() freezes the result without freezing the builder", () => { + const builder = ConfigBuilder.create().name("n"); + const frozen = builder.buildFrozen(); + + expect(Object.isFrozen(frozen)).toBe(true); + // The builder must remain usable; it used to have its own state frozen underneath it. + expect(builder.name("other").build().name).toBe("other"); + }); +}); + +describe("prototype-unsafe paths are rejected", () => { + it("refuses __proto__ in a nested path instead of silently swallowing the write", () => { + const builder = ConfigBuilder.create() as unknown as { + setNestedProperty: (path: string, value: unknown) => unknown; + }; + + expect(() => builder.setNestedProperty("__proto__.polluted", "yes")).toThrow("Unsafe property path"); + expect(({} as Record).polluted).toBeUndefined(); + }); + + it("refuses constructor and prototype segments", () => { + const builder = ConfigBuilder.create() as unknown as { + setNestedProperty: (path: string, value: unknown) => unknown; + }; + + expect(() => builder.setNestedProperty("constructor.x", "y")).toThrow("Unsafe property path"); + expect(() => builder.setNestedProperty("a.prototype.x", "y")).toThrow("Unsafe property path"); + }); +}); + +describe("ambiguous constructor options are rejected", () => { + it("refuses a bare required-fields record passed positionally", () => { + // This structurally matches BuilderInit, so it used to be accepted and silently + // produce a builder with zero required fields. + expect(() => new (ConfigBuilder as never as new (d: object, i: object) => unknown)({}, { name: true })).toThrow( + "Invalid builder options", + ); + }); + + it("refuses it on the class auto builder too", () => { + class PersonBuilder extends CeriosClassAutoBuilder(Person) {} + + expect(() => new (PersonBuilder as never as new (d: object, i: object) => unknown)({}, { name: true })).toThrow( + "Invalid builder options", + ); + }); + + it("still accepts an empty options object and the correct wrapped form", () => { + expect(() => new (ConfigBuilder as never as new (d: object, i: object) => unknown)({}, {})).not.toThrow(); + expect( + () => + new (ConfigBuilder as never as new (d: object, i: object) => unknown)({}, { requiredFields: { name: true } }), + ).not.toThrow(); + }); +}); + +describe("class auto builder runtime identity", () => { + it("returns the same runtime class for the same target class", () => { + expect(CeriosClassAutoBuilder(Person)).toBe(CeriosClassAutoBuilder(Person)); + }); + + it("keeps instanceof working across separate factory calls", () => { + class BuilderA extends CeriosClassAutoBuilder(Person) {} + const Base = CeriosClassAutoBuilder(Person); + + expect(new BuilderA() instanceof Base).toBe(true); + }); + + it("keeps separate target classes on separate runtime classes", () => { + class Other { + label!: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + } + + expect(CeriosClassAutoBuilder(Person)).not.toBe(CeriosClassAutoBuilder(Other)); + }); + + it("builds the right class from two builders sharing one runtime class", () => { + class First extends CeriosClassAutoBuilder(Person) {} + class Second extends CeriosClassAutoBuilder(Person) {} + + expect(new First().name("a").build()).toBeInstanceOf(Person); + expect(new Second().name("b").build()).toBeInstanceOf(Person); + }); +}); diff --git a/tests/auto-builder-core/auto-builder-core-state-fidelity.test.ts b/tests/auto-builder-core/auto-builder-core-state-fidelity.test.ts new file mode 100644 index 0000000..bcc7053 --- /dev/null +++ b/tests/auto-builder-core/auto-builder-core-state-fidelity.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +type Payload = { + when?: Date; + pattern?: RegExp; + lookup?: Map; + tags?: Set; + name?: string; +}; + +class Address { + city!: string; + + constructor(data?: Partial
) { + if (data) { + Object.assign(this, data); + } + } + + label(): string { + return `city=${this.city}`; + } +} + +class Person { + name!: string; + address?: Address; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +class PayloadBuilder extends CeriosAutoBuilder() { + static create(): PayloadBuilder { + return new PayloadBuilder({}); + } +} + +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); + } +} + +// The deep-clone unit tests cover the function directly. These cover the same values +// carried as builder *state* through a real build, which is the path users hit. +describe("built results preserve built-in types", () => { + it("keeps a Date through build()", () => { + const built = PayloadBuilder.create().when(new Date(0)).build(); + + expect(built.when).toBeInstanceOf(Date); + expect(built.when?.getTime()).toBe(0); + }); + + it("keeps a Map and a Set through build()", () => { + const built = PayloadBuilder.create() + .lookup(new Map([["a", 1]])) + .tags(new Set(["x"])) + .build(); + + expect(built.lookup).toBeInstanceOf(Map); + expect(built.lookup?.get("a")).toBe(1); + expect(built.tags).toBeInstanceOf(Set); + expect(built.tags?.has("x")).toBe(true); + }); + + it("keeps a RegExp with its flags through build()", () => { + const built = PayloadBuilder.create().pattern(/abc/gi).build(); + + expect(built.pattern).toBeInstanceOf(RegExp); + expect(built.pattern?.flags).toBe("gi"); + }); + + it("does not alias a Map between the builder and the result", () => { + const builder = PayloadBuilder.create().lookup(new Map([["a", 1]])); + builder.build().lookup?.set("b", 2); + + expect(builder.build().lookup?.has("b")).toBe(false); + }); +}); + +// Deep-cloning builder state must not flatten a nested class instance to a plain object - +// that would silently strip its methods. +describe("built results preserve nested class instances", () => { + it("keeps a nested class instance and its methods", () => { + const built = PersonBuilder.create() + .name("a") + .address(new Address({ city: "Rotterdam" })) + .build(); + + expect(built.address).toBeInstanceOf(Address); + expect(built.address?.label()).toBe("city=Rotterdam"); + }); + + it("keeps nested class instances through clone() and buildFrozen()", () => { + const builder = PersonBuilder.create() + .name("a") + .address(new Address({ city: "R" })); + + expect(builder.clone().build().address).toBeInstanceOf(Address); + expect(builder.buildFrozen().address).toBeInstanceOf(Address); + }); +}); + +describe("prototype-unsafe keys are rejected everywhere, not just in paths", () => { + it("no longer maps a *Prop setter back to the __proto__ key", () => { + // `__proto__Prop` still resolves to a setter - but now it sets a property literally + // named "__proto__Prop", which is harmless. Previously `__proto__` was reserved, so + // this name mapped *back* to the real `__proto__` key and re-parented the result. + const objectBuilder = PayloadBuilder.create() as unknown as Record unknown>; + const stepped = objectBuilder.__proto__Prop({ polluted: true }) as { + buildPartial(): Record; + }; + + const partial = stepped.buildPartial(); + expect(Object.keys(partial)).toEqual(["__proto__Prop"]); + expect(Object.getPrototypeOf(partial)).toBe(Object.prototype); + }); + + it("keeps a class instance on its prototype even when __proto__Prop is used", () => { + const classBuilder = PersonBuilder.create().name("a") as unknown as Record< + string, + (v: unknown) => { buildUnsafe(): Person } + >; + const built = classBuilder.__proto__Prop({ polluted: true }).buildUnsafe(); + + // This used to strip the instance of its prototype, so `greet()` vanished. + expect(built).toBeInstanceOf(Person); + expect(Object.getPrototypeOf(built)).toBe(Person.prototype); + }); + + it("keeps a built class instance on its own prototype", () => { + const built = PersonBuilder.create().name("a").build(); + + expect(built).toBeInstanceOf(Person); + expect(Object.getPrototypeOf(built)).toBe(Person.prototype); + }); + + it("rejects __proto__ passed as a plain key", () => { + const builder = PayloadBuilder.create() as unknown as { + setProperty: (key: string, value: unknown) => unknown; + }; + + expect(() => builder.setProperty("__proto__", { polluted: true })).toThrow("Unsafe property key"); + expect(({} as Record).polluted).toBeUndefined(); + }); + + it("still allows constructor as an ordinary data key", () => { + // Only `__proto__` has an inherited setter; `constructor` is a plain writable + // property, so setting it must keep working. + const builder = PayloadBuilder.create() as unknown as Record unknown>; + const stepped = builder.constructorProp("x") as { buildPartial(): Record }; + + expect(stepped.buildPartial().constructor).toBe("x"); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-base-type-extension.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-base-type-extension.test.ts new file mode 100644 index 0000000..10b52d8 --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-base-type-extension.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { BuilderExtension } from "../../src/auto-builder-core"; +import { AutoBuilderBase, CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { BuilderWith } from "../../src/cerios-builder"; + +type BasePostRequest = { + postId: string; + title?: string; + content?: string; + tags?: string[]; +}; + +// Derived via intersection ... +type CreatePostRequest = BasePostRequest & { + title: string; + content: string; + authorId: string; +}; + +// ... and derived via interface extends; both shapes must flow through keyof identically. +interface PatchPostRequest extends BasePostRequest { + patchReason: string; + notifySubscribers?: boolean; +} + +/** + * Shared builder logic for every post-request builder, written once against the base + * type. The standard-builder equivalent is an abstract class generic over + * `T extends BasePostRequest`; a base-class expression cannot reference the class's own + * type parameter, so for auto builders the shared logic lives in a base-builder function + * applied on top of each concrete auto builder. + */ +// oxlint-disable-next-line typescript/explicit-function-return-type -- the return type names the class declared inside; BuilderExtension is applied in the return statement +function BasePostRequestBuilder>(Builder: TBuilder) { + abstract class PostRequestBuilder extends Builder { + addTag(tag: string): BuilderWith { + return this.tags([...(this.buildPartial().tags ?? []), tag]); + } + + generatedPostId(): BuilderWith { + // postId is required on the base type, so this brand counts toward every + // derived builder's build gate. + return this.postId(`post-${this.buildPartial().title?.length ?? 0}`); + } + } + return PostRequestBuilder as BuilderExtension; +} + +class CreatePostRequestBuilder extends BasePostRequestBuilder(CeriosAutoBuilder()) { + static create(): CreatePostRequestBuilder { + return new CreatePostRequestBuilder({}, { requiredFields: ["postId", "title", "content", "authorId"] }); + } +} + +class PatchPostRequestBuilder extends BasePostRequestBuilder(CeriosAutoBuilder()) { + static create(): PatchPostRequestBuilder { + return new PatchPostRequestBuilder({}, { requiredFields: ["postId", "patchReason"] }); + } +} + +describe("CeriosAutoBuilder - base type extension", () => { + it("generates setters for inherited properties on a derived intersection type", () => { + const request = CreatePostRequestBuilder.create() + .postId("post-100") + .title("Builder Patterns") + .content("Compile-time safety is great.") + .authorId("author-1") + .addTag("typescript") + .addTag("builders") + .build(); + + expect(request).toEqual({ + postId: "post-100", + title: "Builder Patterns", + content: "Compile-time safety is great.", + authorId: "author-1", + tags: ["typescript", "builders"], + }); + }); + + it("generates setters for inherited properties on a derived interface", () => { + const request = PatchPostRequestBuilder.create() + .postId("post-200") + .patchReason("Fix typo") + .title("Fixed title") + .notifySubscribers(true) + .build(); + + expect(request).toEqual({ + postId: "post-200", + patchReason: "Fix typo", + title: "Fixed title", + notifySubscribers: true, + }); + }); + + it("shares custom methods across derived builders and preserves the fluent API", () => { + const request = PatchPostRequestBuilder.create() + .title("Patched") + .generatedPostId() + .patchReason("Patch") + .addTag("shared") + .build(); + + expect(request.postId).toBe("post-7"); + expect(request.patchReason).toBe("Patch"); + expect(request.tags).toEqual(["shared"]); + }); + + it("counts a shared method's setter brand toward the derived build gate", () => { + // generatedPostId() brands postId via the base type; the gate for the derived + // type must accept it just like a direct .postId() call. + const request = CreatePostRequestBuilder.create().title("t").content("c").authorId("a").generatedPostId().build(); + + expectTypeOf(request).toEqualTypeOf(); + expect(request.postId).toBe("post-1"); + }); + + it("gates build() per derived type at compile time", () => { + expect(() => { + // @ts-expect-error - authorId (derived-only, required) not set + return CreatePostRequestBuilder.create().postId("p").title("t").content("c").build(); + }).toThrow("Missing required fields: authorId"); + expect(() => { + // @ts-expect-error - title (inherited, strengthened to required) not set + return CreatePostRequestBuilder.create().postId("p").content("c").authorId("a").build(); + }).toThrow("Missing required fields: title"); + expect(() => { + // @ts-expect-error - postId (inherited, required) not set + return PatchPostRequestBuilder.create().patchReason("r").build(); + }).toThrow("Missing required fields: postId"); + + const complete = PatchPostRequestBuilder.create().postId("p").patchReason("r"); + expectTypeOf(complete.build()).toEqualTypeOf(); + expect(complete.build()).toEqual({ postId: "p", patchReason: "r" }); + }); + + it("validates required fields per derived builder independently at runtime", () => { + expect(() => + CreatePostRequestBuilder.create() + .postId("post-500") + .title("Missing author") + .content("No author yet") + .buildWithoutCompileTimeValidation(), + ).toThrow("Missing required fields: authorId"); + + expect(() => PatchPostRequestBuilder.create().postId("post-501").buildWithoutCompileTimeValidation()).toThrow( + "Missing required fields: patchReason", + ); + }); + + it("clone preserves the concrete builder including shared methods", () => { + const cloned = PatchPostRequestBuilder.create() + .postId("post-400") + .patchReason("Initial reason") + .clone() + .notifySubscribers(true) + .patchReason("Updated reason") + .addTag("cloned"); + + expect(cloned.build()).toEqual({ + postId: "post-400", + patchReason: "Updated reason", + notifySubscribers: true, + tags: ["cloned"], + }); + }); + + it("keeps the static from() available through the base-builder function", () => { + const existing: CreatePostRequest = { + postId: "post-600", + title: "Seeded", + content: "From an existing request", + authorId: "author-2", + }; + + const request = CreatePostRequestBuilder.from(existing).addTag("seeded").build(); + + expect(request).toEqual({ ...existing, tags: ["seeded"] }); + // The seed was deep-cloned, not aliased. + expect(existing.tags).toBeUndefined(); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-build-methods.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-build-methods.test.ts new file mode 100644 index 0000000..d1c16c3 --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-build-methods.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { InternalBuilderBrand, RequiredFieldsTemplate } from "../../src/cerios-builder"; + +type User = { + id: string; + name: string; + email: string; + age?: number; + address?: { street: string; city: string }; +}; + +class UserBuilder extends CeriosAutoBuilder() { + static requiredTemplate: RequiredFieldsTemplate = ["id", "name", "email"]; + + static create(): UserBuilder { + return new UserBuilder({}, { requiredFields: this.requiredTemplate }); + } +} + +// The intersection with the brand marks all required properties as set, +// so the returned builder is buildable (id/name/email are the required ones). +function complete(): UserBuilder & InternalBuilderBrand { + return UserBuilder.create().id("1").name("John").email("john@example.com"); +} + +describe("CeriosAutoBuilder build methods", () => { + it("build() returns the object when required fields are set", () => { + expect(complete().build()).toEqual({ id: "1", name: "John", email: "john@example.com" }); + }); + + it("buildWithoutRuntimeValidation() skips the runtime check", () => { + const builder = UserBuilder.create().id("1"); + const unsafe = builder as unknown as { buildWithoutRuntimeValidation: () => User }; + expect(unsafe.buildWithoutRuntimeValidation()).toEqual({ id: "1" }); + }); + + it("buildWithoutCompileTimeValidation() runs the runtime check", () => { + expect(complete().buildWithoutCompileTimeValidation().email).toBe("john@example.com"); + + const builder = UserBuilder.create().id("1"); + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow( + "Missing required fields: name, email. Please set these fields before calling buildWithoutCompileTimeValidation.", + ); + }); + + it("buildUnsafe() and buildPartial() skip validation", () => { + expect(UserBuilder.create().id("1").buildUnsafe()).toEqual({ id: "1" }); + expect(UserBuilder.create().id("1").age(30).buildPartial()).toEqual({ id: "1", age: 30 }); + }); + + it("buildFrozen() and buildDeepFrozen() freeze the result", () => { + const shallow = complete().address({ street: "Main St", city: "Springfield" }).buildFrozen(); + expect(Object.isFrozen(shallow)).toBe(true); + expect(Object.isFrozen(shallow.address)).toBe(false); + + const deep = complete().address({ street: "Main St", city: "Springfield" }).buildDeepFrozen(); + expect(Object.isFrozen(deep)).toBe(true); + expect(Object.isFrozen(deep.address)).toBe(true); + }); + + it("buildSealed() and buildDeepSealed() seal the result", () => { + const shallow = complete().address({ street: "Main St", city: "Springfield" }).buildSealed(); + expect(Object.isSealed(shallow)).toBe(true); + expect(Object.isSealed(shallow.address)).toBe(false); + + const deep = complete().address({ street: "Main St", city: "Springfield" }).buildDeepSealed(); + expect(Object.isSealed(deep)).toBe(true); + expect(Object.isSealed(deep.address)).toBe(true); + }); + + it("frozen build still runs runtime validation", () => { + const builder = UserBuilder.create().id("1"); + const unsafe = builder as unknown as { buildFrozen: () => User }; + expect(() => unsafe.buildFrozen()).toThrow("Missing required fields: name, email"); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-cloning.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-cloning.test.ts new file mode 100644 index 0000000..41ff59e --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-cloning.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; + +type Person = { + name: string; + age: number; + hobbies?: string[]; + address?: { street: string; city: string }; +}; + +class PersonBuilder extends CeriosAutoBuilder() { + static create(): PersonBuilder { + return new PersonBuilder({}); + } +} + +describe("CeriosAutoBuilder - clone", () => { + it("returns an instance of the concrete subclass", () => { + expect(PersonBuilder.create().name("John").clone()).toBeInstanceOf(PersonBuilder); + }); + + it("deep-clones nested objects, so mutating the clone does not affect the original", () => { + const original = PersonBuilder.create().name("John").address({ street: "Main St", city: "Springfield" }); + const cloned = original.clone(); + + const clonedAddress = cloned.buildPartial().address; + if (clonedAddress) { + clonedAddress.city = "Shelbyville"; + } + + expect(original.buildPartial().address?.city).toBe("Springfield"); + }); + + it("deep-clones arrays", () => { + const original = PersonBuilder.create().name("John").hobbies(["chess"]); + const cloned = original.clone(); + + cloned.buildPartial().hobbies?.push("running"); + + expect(original.buildPartial().hobbies).toEqual(["chess"]); + }); + + it("preserves validators", () => { + const cloned = PersonBuilder.create() + .addValidator((p) => (p.age !== undefined && p.age >= 18 ? true : "Age must be 18 or older")) + .name("John") + .age(16) + .clone(); + + expect(() => cloned.build()).toThrow("Validation failed: Age must be 18 or older"); + }); + + it("preserves instance-level required fields", () => { + const cloned = PersonBuilder.create().setRequiredFields(["name", "age"]).name("John").clone(); + + expect(() => cloned.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); +}); + +describe("CeriosAutoBuilder - from", () => { + it("returns the concrete subclass, not the base runtime", () => { + expect(PersonBuilder.from({ name: "John", age: 30 })).toBeInstanceOf(PersonBuilder); + }); + + it("deep-clones the source, so building cannot reach back into it", () => { + const source: Person = { name: "John", age: 30, address: { street: "Main St", city: "Springfield" } }; + const built = PersonBuilder.from(source).build(); + + if (built.address) { + built.address.city = "Shelbyville"; + } + + expect(source.address?.city).toBe("Springfield"); + }); + + it("produces a builder that is immediately buildable", () => { + expect(PersonBuilder.from({ name: "John", age: 30 }).build()).toEqual({ name: "John", age: 30 }); + }); +}); + +describe("CeriosAutoBuilder - buildPartial isolation", () => { + // buildPartial() returns a copy; returning the live internal object would let callers + // mutate a builder that is supposed to be immutable. + it("returns a copy, so mutating it cannot reach the builder", () => { + const builder = PersonBuilder.create().name("John"); + const partial = builder.buildPartial(); + partial.name = "mutated"; + + expect(builder.buildPartial().name).toBe("John"); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-composition.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-composition.test.ts new file mode 100644 index 0000000..f8ad5f6 --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-composition.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { BuilderComposerFromFactory, BuilderWith, CeriosBuilder } from "../../src/cerios-builder"; + +type Address = { street: string; city: string; country: string }; +type Contact = { email: string }; +type Customer = { + id: string; + tier: string; + address: Address; + meta: { note: string; source: string }; +}; + +// Every builder below carries explicit return types on purpose: writing them by hand +// with a single two-argument helper is part of what these tests cover. +class AddressBuilder extends CeriosAutoBuilder
() { + static create(): AddressBuilder { + return new AddressBuilder({}); + } + + static createWithDefaults(): BuilderWith { + return AddressBuilder.create().country("NL"); + } + + static createComplete(): BuilderWith { + return AddressBuilder.create().country("NL").street("Main St").city("Rotterdam"); + } +} + +class ContactBuilder extends CeriosAutoBuilder() { + static create(): ContactBuilder { + return new ContactBuilder({}); + } +} + +class CustomerBuilder extends CeriosAutoBuilder() { + static create(): CustomerBuilder { + return new CustomerBuilder({}); + } + + asVip(): BuilderWith { + return this.tier("vip"); + } + + withNote(note: string): BuilderWith { + return this.meta({ note, source: "manual" }); + } + + // Composer methods take a distinct name and delegate to the auto setter. + withAddress(fn: BuilderComposerFromFactory): BuilderWith { + return this.address(fn(AddressBuilder.create()).build()); + } + + withAddressDefaults( + fn: BuilderComposerFromFactory, + ): BuilderWith { + return this.address(fn(AddressBuilder.createWithDefaults()).build()); + } + + withCompleteAddress( + fn?: BuilderComposerFromFactory, + ): BuilderWith { + const builder = AddressBuilder.createComplete(); + return this.address(fn ? fn(builder).build() : builder.build()); + } +} + +describe("CeriosAutoBuilder - nested builder composition", () => { + it("composes a nested builder that has no defaults", () => { + const customer = CustomerBuilder.create() + .id("1") + .asVip() + .withNote("hello") + .withAddress((b) => b.street("Main St").city("Rotterdam").country("NL")) + .build(); + + expectTypeOf(customer).toEqualTypeOf(); + expect(customer.address).toEqual({ street: "Main St", city: "Rotterdam", country: "NL" }); + expect(customer.tier).toBe("vip"); + expect(customer.meta.note).toBe("hello"); + }); + + it("treats keys preset by the factory as already set", () => { + const customer = CustomerBuilder.create() + .id("1") + .asVip() + .withNote("n") + // `country` is preset by createWithDefaults and must not be required here + .withAddressDefaults((b) => b.street("Main St").city("Rotterdam")) + .build(); + + expect(customer.address.country).toBe("NL"); + }); + + it("makes the callback optional when the factory presets everything", () => { + const customer = CustomerBuilder.create().id("1").asVip().withNote("n").withCompleteAddress().build(); + expect(customer.address).toEqual({ street: "Main St", city: "Rotterdam", country: "NL" }); + + const overridden = CustomerBuilder.create() + .id("1") + .asVip() + .withNote("n") + .withCompleteAddress((b) => b.city("Amsterdam")) + .build(); + expect(overridden.address.city).toBe("Amsterdam"); + }); + + it("rejects a callback that returns an incompletely-set child builder", () => { + // @ts-expect-error - city and country are never set on the nested address builder + CustomerBuilder.create().withAddress((b) => b.street("Main St")); + + // @ts-expect-error - street and city still missing even with the country default + CustomerBuilder.create().withAddressDefaults((b) => b); + + // the compile-time gate above matches the actual runtime state + expect(AddressBuilder.create().street("Main St").buildPartial()).toEqual({ street: "Main St" }); + expect(AddressBuilder.createWithDefaults().buildPartial()).toEqual({ country: "NL" }); + }); + + it("rejects a callback that returns a builder for a different target type", () => { + // @ts-expect-error - ContactBuilder does not build an Address + CustomerBuilder.create().withAddress(() => ContactBuilder.create().email("a@b.c")); + + expect(ContactBuilder.create().email("a@b.c").buildPartial()).toEqual({ email: "a@b.c" }); + }); + + it("still gates the parent builder until every property is set", () => { + // @ts-expect-error - tier, address and meta are not set + CustomerBuilder.create().id("1").build(); + + // @ts-expect-error - meta is not set + CustomerBuilder.create().id("1").asVip().withCompleteAddress().build(); + + expect(Object.keys(CustomerBuilder.create().id("1").asVip().withCompleteAddress().buildPartial()).sort()).toEqual([ + "address", + "id", + "tier", + ]); + }); +}); + +describe("BuilderWith", () => { + it("accumulates across custom methods rather than replacing prior state", () => { + const partial = CustomerBuilder.create().id("1").asVip(); + + // @ts-expect-error - address and meta still missing + partial.build(); + + expect(partial.withNote("n").withCompleteAddress().build().id).toBe("1"); + }); + + it("brands the root key when a method sets a nested value", () => { + const customer = CustomerBuilder.create().id("1").asVip().withCompleteAddress().withNote("deep").build(); + expect(customer.meta).toEqual({ note: "deep", source: "manual" }); + }); + + it("still resolves for hand-written CeriosBuilder subclasses", () => { + class PlainAddressBuilder extends CeriosBuilder
{ + static create(): PlainAddressBuilder { + return new PlainAddressBuilder({}); + } + static createWithDefaults(): BuilderWith { + return PlainAddressBuilder.create().country("NL"); + } + country(value: string): BuilderWith { + return this.setProperty("country", value); + } + street(value: string): BuilderWith { + return this.setProperty("street", value); + } + city(value: string): BuilderWith { + return this.setProperty("city", value); + } + } + + const address = PlainAddressBuilder.createWithDefaults().street("Main St").city("Rotterdam").build(); + expect(address).toEqual({ country: "NL", street: "Main St", city: "Rotterdam" }); + + // @ts-expect-error - city missing + PlainAddressBuilder.createWithDefaults().street("Main St").build(); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-constructor.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-constructor.test.ts new file mode 100644 index 0000000..0975bfa --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-constructor.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; + +type Person = { + name: string; + age: number; + email?: string; +}; + +// Required fields and validators are declared once, in the subclass constructor. +class PersonBuilder extends CeriosAutoBuilder() { + constructor(data?: Partial) { + super(data, { + requiredFields: ["name", "age"], + validators: [(p): boolean | string => (p.age !== undefined && p.age >= 18 ? true : "Age must be 18 or older")], + }); + } + + static create(): PersonBuilder { + return new PersonBuilder(); + } +} + +describe("CeriosAutoBuilder - constructor options", () => { + it("applies required fields passed through super()", () => { + const builder = PersonBuilder.create().name("John"); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); + + it("applies validators passed through super()", () => { + const builder = PersonBuilder.create().name("John").age(16); + + expect(() => builder.build()).toThrow("Validation failed: Age must be 18 or older"); + }); + + it("applies seed data passed through super()", () => { + expect(new PersonBuilder({ name: "Seeded" }).buildPartial().name).toBe("Seeded"); + }); + + it("carries required fields and validators through copy-on-write setters", () => { + const builder = PersonBuilder.create().name("John").email("j@x.io").age(16); + + expect(() => builder.build()).toThrow("Validation failed: Age must be 18 or older"); + }); + + it("carries required fields and validators through clone()", () => { + const builder = PersonBuilder.create().name("John").clone(); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); + + // Copy-on-write bypasses the subclass constructor, so a later setRequiredFields() is not + // reverted to the constructor's hardcoded list by the next setter call. + it("does not let the subclass constructor override a later setRequiredFields()", () => { + const builder = PersonBuilder.create().setRequiredFields(["name"]).age(30).name("John"); + + expect(() => builder.buildWithoutCompileTimeValidation()).not.toThrow(); + }); + + it("accepts an exhaustive required-fields record instead of a path array", () => { + class RecordBuilder extends CeriosAutoBuilder() { + constructor(data?: Partial) { + // The record must name every required property of Person; adding a new + // required property makes this line fail to compile. + super(data, { requiredFields: { name: true, age: true } }); + } + + static create(): RecordBuilder { + return new RecordBuilder(); + } + } + + const builder = RecordBuilder.create().name("John"); + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-director.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-director.test.ts new file mode 100644 index 0000000..1a0d213 --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-director.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { BuilderComposerFromFactory, BuilderWith } from "../../src/cerios-builder"; + +// The deep structure the deprecated builders addressed with setNestedProperty dot-paths. +// With auto builders, each level gets its own small builder and a director owns the recipe +// for assembling them - same end result, but every level is compile-time checked and +// reusable on its own. This file is the runnable version of the director example in +// README.md / MIGRATION.md. + +type Address = { + street: string; + city: string; + country: string; + postalCode?: string; +}; + +type OrderDetails = { + customerId: string; + totalAmount: number; + status: string; + shippingAddress: Address; + orderNumber?: string; +}; + +type OrderRequest = { + order: { details?: OrderDetails }; +}; + +class AddressBuilder extends CeriosAutoBuilder
() { + static create(): AddressBuilder { + return new AddressBuilder({}); + } + + static createDomestic(): BuilderWith { + return AddressBuilder.create().country("USA"); + } +} + +class OrderDetailsBuilder extends CeriosAutoBuilder() { + static create(): OrderDetailsBuilder { + return new OrderDetailsBuilder({}); + } + + static createPending(): BuilderWith { + return OrderDetailsBuilder.create().status("pending"); + } +} + +class OrderRequestBuilder extends CeriosAutoBuilder() { + static create(): OrderRequestBuilder { + return new OrderRequestBuilder({}); + } +} + +/** + * The director: one place that knows how a complete OrderRequest is put together. + * Each level is built by its own builder, so the compiler still proves every required + * field of every level, and the child builders stay independently reusable. + */ +class OrderRequestDirector { + static standardOrder(input: { customerId: string; totalAmount: number; street: string; city: string }): OrderRequest { + const shippingAddress = AddressBuilder.createDomestic().street(input.street).city(input.city).build(); + + const details = OrderDetailsBuilder.createPending() + .customerId(input.customerId) + .totalAmount(input.totalAmount) + .shippingAddress(shippingAddress) + .build(); + + return OrderRequestBuilder.create().order({ details }).build(); + } + + // Callback variant: the caller customises the address without the director losing + // control of the recipe. The composer type forces the callback to return a + // fully-set address builder. + static orderShippedTo( + input: { customerId: string; totalAmount: number }, + composeAddress: BuilderComposerFromFactory, + ): OrderRequest { + const shippingAddress = composeAddress(AddressBuilder.createDomestic()).build(); + + const details = OrderDetailsBuilder.createPending() + .customerId(input.customerId) + .totalAmount(input.totalAmount) + .shippingAddress(shippingAddress) + .build(); + + return OrderRequestBuilder.create().order({ details }).build(); + } +} + +describe("Director pattern - composing nested objects from multiple auto builders", () => { + it("assembles a deep structure without setNestedProperty", () => { + const order = OrderRequestDirector.standardOrder({ + customerId: "CUST-001", + totalAmount: 299.99, + street: "123 Main St", + city: "New York", + }); + + expect(order).toEqual({ + order: { + details: { + customerId: "CUST-001", + totalAmount: 299.99, + status: "pending", + shippingAddress: { street: "123 Main St", city: "New York", country: "USA" }, + }, + }, + }); + }); + + it("lets callers customise one level through a composer callback", () => { + const order = OrderRequestDirector.orderShippedTo({ customerId: "CUST-002", totalAmount: 50 }, (address) => + address.street("456 Oak Ave").city("Boston").postalCode("02101"), + ); + + expect(order.order.details?.shippingAddress).toEqual({ + street: "456 Oak Ave", + city: "Boston", + country: "USA", + postalCode: "02101", + }); + }); + + it("keeps compile-time checking at every level", () => { + // @ts-expect-error - the callback must return a fully-set address builder (city missing) + OrderRequestDirector.orderShippedTo({ customerId: "C", totalAmount: 1 }, (address) => address.street("x")); + + // @ts-expect-error - the child builder alone still gates build() until complete + AddressBuilder.createDomestic().street("only street").build(); + + expect(true).toBe(true); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-nested-properties.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-nested-properties.test.ts new file mode 100644 index 0000000..e152ca2 --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-nested-properties.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { BuilderStep } from "../../src/cerios-builder"; + +type Person = { + name: string; + hobbies?: string[]; + address?: { street: string; city: string }; +}; + +class PersonBuilder extends CeriosAutoBuilder() { + static create(): PersonBuilder { + return new PersonBuilder({}); + } + + // The supported replacement for the removed addToArrayProperty helper: a custom + // method that rebuilds the array through the root setter. + addHobby(hobby: string): BuilderStep { + return this.hobbies([...(this.buildPartial().hobbies ?? []), hobby]); + } + + // The supported replacement for the removed setNestedProperty helper: a custom + // method that replaces the root property with an updated whole value. + inCity(city: string): BuilderStep { + const address = this.buildPartial().address ?? { street: "", city: "" }; + return this.address({ ...address, city }); + } +} + +// An auto builder exposes only whole-root-property setters. The dot-path and array-append +// helpers of the deprecated base builders are intentionally not part of its API: nested +// structures are composed with custom methods or a director coordinating multiple builders. +describe("CeriosAutoBuilder - no fine-grained mutation helpers", () => { + it("hides every fine-grained mutation helper at the type level", () => { + const builder = PersonBuilder.create().name("John"); + + // @ts-expect-error - setNestedProperty is not part of the auto-builder API + builder.setNestedProperty("address.city", "Springfield"); + // @ts-expect-error - addToArrayProperty is not part of the auto-builder API + builder.addToArrayProperty("hobbies", "chess"); + // @ts-expect-error - setProperty is not part of the auto-builder API + builder.setProperty("name", "Jane"); + // @ts-expect-error - setProperties is not part of the auto-builder API + builder.setProperties({ name: "Jane" }); + + // Copy-on-write: even the (runtime-reachable) hidden members left the builder alone. + expect(builder.buildPartial()).toEqual({ name: "John" }); + }); +}); + +describe("CeriosAutoBuilder - custom-method replacements", () => { + it("appends to an array via a custom method delegating to the root setter", () => { + const builder = PersonBuilder.create().name("John").addHobby("chess").addHobby("running"); + + expect(builder.buildPartial().hobbies).toEqual(["chess", "running"]); + }); + + it("updates a nested value via a custom method replacing the whole root property", () => { + const builder = PersonBuilder.create() + .name("John") + .address({ street: "Main St", city: "Springfield" }) + .inCity("Shelbyville"); + + expect(builder.buildPartial().address).toEqual({ street: "Main St", city: "Shelbyville" }); + }); + + it("stays copy-on-write: the original builder is left intact", () => { + const original = PersonBuilder.create().name("John").hobbies(["chess"]); + original.addHobby("running"); + original.inCity("Shelbyville"); + + expect(original.buildPartial().hobbies).toEqual(["chess"]); + expect(original.buildPartial().address).toBeUndefined(); + }); + + it("counts the custom method toward the compile-time tracking", () => { + const person = PersonBuilder.create().name("John").addHobby("chess").build(); + + expect(person).toEqual({ name: "John", hobbies: ["chess"] }); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-property-removal.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-property-removal.test.ts new file mode 100644 index 0000000..77c5338 --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-property-removal.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; + +type Person = { + name: string; + age: number; + email?: string; + phone?: string; + address?: { street: string; city: string }; +}; + +class PersonBuilder extends CeriosAutoBuilder() { + static create(): PersonBuilder { + return new PersonBuilder({}); + } +} + +class StaticRequiredBuilder extends CeriosAutoBuilder() { + static requiredTemplate: ReadonlyArray = ["name", "address.city"]; + + static create(): StaticRequiredBuilder { + return new StaticRequiredBuilder({}); + } +} + +describe("CeriosAutoBuilder - removeOptionalProperty", () => { + it("drops a previously set optional property", () => { + const builder = PersonBuilder.create().name("John").age(30).email("j@x.io").removeOptionalProperty("email"); + + expect(builder.buildPartial()).toEqual({ name: "John", age: 30 }); + }); + + it("returns a new builder, leaving the original intact", () => { + const withEmail = PersonBuilder.create().name("John").email("j@x.io"); + const without = withEmail.removeOptionalProperty("email"); + + expect(withEmail.buildPartial().email).toBe("j@x.io"); + expect(without.buildPartial().email).toBeUndefined(); + }); + + it("preserves validators and required fields across the removal", () => { + const builder = PersonBuilder.create() + .setRequiredFields(["name", "age"]) + .addValidator((p) => (p.name === "" ? "Name must not be empty" : true)) + .name("John") + .email("j@x.io") + .removeOptionalProperty("email"); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); +}); + +describe("CeriosAutoBuilder - clearOptionalProperties", () => { + it("keeps root required properties and clears the rest", () => { + const builder = PersonBuilder.create() + .setRequiredFields(["name", "age"]) + .name("John") + .age(30) + .email("j@x.io") + .phone("555") + .clearOptionalProperties(); + + expect(builder.buildPartial()).toEqual({ name: "John", age: 30 }); + }); + + it("clears everything when no required fields are configured", () => { + const builder = PersonBuilder.create().name("John").email("j@x.io").clearOptionalProperties(); + + expect(builder.buildPartial()).toEqual({}); + }); + + it("preserves the root object of a nested required path", () => { + const builder = PersonBuilder.create() + .setRequiredFields(["name", "address.city"]) + .name("John") + .address({ street: "Main St", city: "Springfield" }) + .email("j@x.io") + .clearOptionalProperties(); + + expect(builder.buildPartial()).toEqual({ + name: "John", + address: { street: "Main St", city: "Springfield" }, + }); + }); + + it("preserves a nested required root supplied via static requiredTemplate", () => { + const builder = StaticRequiredBuilder.create() + .name("John") + .address({ street: "Main St", city: "Springfield" }) + .email("j@x.io") + .clearOptionalProperties(); + + expect(builder.buildPartial().address?.city).toBe("Springfield"); + }); + + it("still satisfies runtime validation of a nested required path afterwards", () => { + const builder = PersonBuilder.create() + .setRequiredFields(["name", "address.city"]) + .name("John") + .address({ street: "Main St", city: "Springfield" }) + .email("j@x.io") + .clearOptionalProperties(); + + expect(() => builder.buildWithoutCompileTimeValidation()).not.toThrow(); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-proxy-edge-cases.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-proxy-edge-cases.test.ts new file mode 100644 index 0000000..6200622 --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-proxy-edge-cases.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; + +type Weird = { + id: string; + build: number; // collides with build() -> setter is buildProp + clone: string; // collides with clone() -> setter is cloneProp + then: string; // collides with then handling -> setter is thenProp + "content-type": string; // not a valid identifier -> bracket access +}; + +class WeirdBuilder extends CeriosAutoBuilder() { + static create(): WeirdBuilder { + return new WeirdBuilder({}); + } +} + +class SimpleBuilder extends CeriosAutoBuilder<{ id: string; name: string }>() { + static create(): SimpleBuilder { + return new SimpleBuilder({}); + } +} + +describe("CeriosAutoBuilder - proxy edge cases", () => { + it("should resolve immediately when awaited (not a thenable)", async () => { + const builder = SimpleBuilder.create().id("1"); + // oxlint-disable-next-line await-thenable -- intentionally awaiting a non-thenable + const resolved = await builder; + expect(resolved.buildPartial()).toEqual({ id: "1" }); + }); + + it("should not throw on JSON.stringify", () => { + const builder = SimpleBuilder.create().id("1"); + expect(() => JSON.stringify(builder)).not.toThrow(); + }); + + it("should not throw on console.log / inspection", () => { + const builder = SimpleBuilder.create().id("1"); + expect(() => console.log(builder)).not.toThrow(); + }); + + it("should forward symbol properties to the target instead of minting setters", () => { + const builder = SimpleBuilder.create().id("1"); + + // Symbols can never be property setters; they resolve like on a plain object. + expect(Reflect.get(builder, Symbol.toStringTag)).toBeUndefined(); + expect(Reflect.get(builder, Symbol.asyncIterator)).toBeUndefined(); + }); + + it("should expose real methods to the in operator", () => { + const builder = SimpleBuilder.create(); + expect("build" in builder).toBe(true); + expect("clone" in builder).toBe(true); + }); + + it("should route reserved-name properties through *Prop setters while keeping the real methods", () => { + const builder = WeirdBuilder.create() + .id("1") + .buildProp(42) + .cloneProp("c") + .thenProp("t") + .buildWithoutCompileTimeValidation(); + + expect(builder).toEqual({ id: "1", build: 42, clone: "c", then: "t" }); + }); + + it("should keep build() and clone() working even when those property names are set", () => { + const builder = WeirdBuilder.create().id("1").buildProp(42); + + expect(typeof builder.build).toBe("function"); + expect(builder.clone()).toBeInstanceOf(WeirdBuilder); + }); + + it("should set non-identifier keys via bracket access", () => { + const builder = WeirdBuilder.create().id("1")["content-type"]("application/json"); + + expect(builder.buildPartial()["content-type"]).toBe("application/json"); + }); +}); + +// Names that are real members of the builder but were historically absent from +// RESERVED_BUILDER_NAMES. The proxy's `prop in target` fallback returned the real method +// while the generated type still advertised a bare setter, so calling one silently ran the +// wrong method instead of setting the property. They now route through `*Prop`. +describe("CeriosAutoBuilder - unreserved member-name collisions", () => { + const collisions = [ + "setProperty", + "setProperties", + "setNestedProperty", + "addToArrayProperty", + "instantiateBuilder", + "getRequiredTemplate", + "validateRequiredFields", + "runValidators", + "hasOwnProperty", + ] as const; + + for (const name of collisions) { + it(`sets a property named ${name} through ${name}Prop`, () => { + const builder = WeirdBuilder.create() as unknown as Record unknown>; + const stepped = builder[`${name}Prop`]("x") as { buildPartial(): Record }; + + expect(stepped.buildPartial()).toEqual({ [name]: "x" }); + }); + + it(`keeps the real ${name} member reachable under its bare name`, () => { + const builder = WeirdBuilder.create() as unknown as Record; + + expect(builder[name]).toBeTypeOf("function"); + }); + } +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-subclass-state.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-subclass-state.test.ts new file mode 100644 index 0000000..f84e012 --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-subclass-state.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; + +type Doc = { + title: string; + body?: string; +}; + +// Copy-on-write bypasses the subclass constructor, so it has to carry the subclass's own +// fields across itself. Getting this wrong is not merely lossy: a missing field is absent +// from the proxy target, so the proxy hands back an auto-setter *function* for it, and +// `this.cache.set(...)` fails with "is not a function" far from the real cause. +class TrackingBuilder extends CeriosAutoBuilder() { + cache = new Map(); + counter = 0; + + constructor(data?: Partial) { + super(data); + } + + static create(): TrackingBuilder { + return new TrackingBuilder(); + } + + get upper(): string { + return (this.buildPartial().title ?? "").toUpperCase(); + } + + remember(key: string, value: string): this { + this.cache.set(key, value); + this.counter += 1; + return this; + } +} + +describe("CeriosAutoBuilder - subclass instance state across copy-on-write", () => { + it("keeps a subclass field after a setter call", () => { + const forked = TrackingBuilder.create().title("t"); + + expect(forked.cache).toBeInstanceOf(Map); + expect(forked.counter).toBe(0); + }); + + it("does not shadow a subclass field with an auto setter", () => { + const forked = TrackingBuilder.create().title("t"); + + // The regression made this a function, so `typeof` is the assertion that matters. + expect(typeof forked.cache).not.toBe("function"); + }); + + it("leaves subclass methods usable after a setter call", () => { + const forked = TrackingBuilder.create().title("t").remember("a", "1"); + + expect(forked.cache.get("a")).toBe("1"); + expect(forked.counter).toBe(1); + }); + + it("keeps subclass fields through clone()", () => { + const cloned = TrackingBuilder.create().title("t").clone(); + + expect(cloned.cache).toBeInstanceOf(Map); + }); + + it("keeps subclass fields through setRequiredFields() and removeOptionalProperty()", () => { + const configured = TrackingBuilder.create().setRequiredFields(["title"]).title("t").body("b"); + + expect(configured.cache).toBeInstanceOf(Map); + expect(configured.removeOptionalProperty("body").cache).toBeInstanceOf(Map); + }); + + it("keeps subclass getters working after a setter call", () => { + expect(TrackingBuilder.create().title("hello").upper).toBe("HELLO"); + }); + + it("does not let a subclass field mask the builder's own members", () => { + const forked = TrackingBuilder.create().title("t"); + + expect(forked.build()).toEqual({ title: "t" }); + expect(forked.clone()).toBeInstanceOf(TrackingBuilder); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-type-safety.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-type-safety.test.ts new file mode 100644 index 0000000..f48a8ab --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder-type-safety.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { BuilderStep } from "../../src/cerios-builder"; + +type User = { + id: string; + name: string; + role: string; + age?: number; + build?: number; // reserved-name property -> setter buildProp + "content-type"?: string; // non-identifier -> bracket access +}; + +class UserBuilder extends CeriosAutoBuilder() { + static create(): UserBuilder { + return new UserBuilder({}); + } +} + +// A builder with custom logic: distinctly-named methods delegating to auto setters. +class AdminUserBuilder extends CeriosAutoBuilder() { + static create(): AdminUserBuilder { + return new AdminUserBuilder({}); + } + asAdmin(): BuilderStep { + return this.role("admin"); + } +} + +type OptionalOnly = { a?: string; b?: number }; +class OptionalOnlyBuilder extends CeriosAutoBuilder() { + static create(): OptionalOnlyBuilder { + return new OptionalOnlyBuilder({}); + } +} + +describe("CeriosAutoBuilder - compile-time safety", () => { + it("allows build() only once every required property is set", () => { + const complete = UserBuilder.create().id("1").name("n").role("r"); + expectTypeOf(complete.build()).toEqualTypeOf(); + + // @ts-expect-error - name and role not set + UserBuilder.create().id("1").build(); + // @ts-expect-error - nothing set + UserBuilder.create().build(); + + expect(complete.build()).toEqual({ id: "1", name: "n", role: "r" }); + }); + + it("gates all compile-time-validated build variants", () => { + const incomplete = UserBuilder.create().id("1"); + + // @ts-expect-error - required properties missing + incomplete.buildWithoutRuntimeValidation(); + // @ts-expect-error - required properties missing + incomplete.buildFrozen(); + // @ts-expect-error - required properties missing + incomplete.buildDeepFrozen(); + // @ts-expect-error - required properties missing + incomplete.buildSealed(); + // @ts-expect-error - required properties missing + incomplete.buildDeepSealed(); + + expect(incomplete.buildPartial()).toEqual({ id: "1" }); + expect(incomplete.buildUnsafe()).toEqual({ id: "1" }); + }); + + it("enforces setter value types", () => { + // @ts-expect-error - id must be a string + const wrong = UserBuilder.create().id(123); + // @ts-expect-error - unknown property has no setter + UserBuilder.create().unknownProp("x"); + + expect(wrong.buildPartial()).toEqual({ id: 123 }); + }); + + it("hides the protected setProperty helper", () => { + // @ts-expect-error - setProperty is not part of the auto-builder API + UserBuilder.create().setProperty("id", "1"); + + expect(UserBuilder.create().id("1").buildPartial()).toEqual({ id: "1" }); + }); + + it("routes reserved-name properties to *Prop and tracks the brand", () => { + // The `build` property is set via buildProp; build is optional so it isn't required. + const built = UserBuilder.create().id("1").name("n").role("r").buildProp(5).build(); + expect(built.build).toBe(5); + + // @ts-expect-error - `build` is the method, not a property setter + UserBuilder.create().build(5); + }); + + it("sets non-identifier keys via bracket access with tracking", () => { + const built = UserBuilder.create().id("1").name("n").role("r")["content-type"]("application/json").build(); + expect(built["content-type"]).toBe("application/json"); + }); + + it("builds an all-optional type without any setter call", () => { + // With no required keys there is nothing to track, so the build gate dissolves + // and a fresh builder can build() immediately. + expectTypeOf(OptionalOnlyBuilder.create().build()).toEqualTypeOf(); + expect(OptionalOnlyBuilder.create().build()).toEqual({}); + expect(OptionalOnlyBuilder.create().buildFrozen()).toEqual({}); + + // Setting optional keys keeps working, before and after the gate. + expectTypeOf(OptionalOnlyBuilder.create().a("x").build()).toEqualTypeOf(); + expect(OptionalOnlyBuilder.create().a("x").build()).toEqual({ a: "x" }); + }); + + it("supports custom logic through distinctly-named methods delegating to auto setters", () => { + const built = AdminUserBuilder.create().id("1").name("n").asAdmin().build(); + expect(built.role).toBe("admin"); + }); + + it("takes exactly one type argument - the excluded-keys parameter is gone", () => { + // @ts-expect-error - the factory has no second (excluded keys) type parameter + const base = CeriosAutoBuilder(); + expect(base).toBeDefined(); + }); + + it("keeps a fully-set builder assignable to the plain subclass type", () => { + const plain: UserBuilder = UserBuilder.create().id("1").name("n").role("r"); + expect(plain.buildPartial().id).toBe("1"); + }); +}); diff --git a/tests/cerios-auto-builder/cerios-auto-builder.test.ts b/tests/cerios-auto-builder/cerios-auto-builder.test.ts new file mode 100644 index 0000000..d38a685 --- /dev/null +++ b/tests/cerios-auto-builder/cerios-auto-builder.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosAutoBuilder } from "../../src/cerios-auto-builder"; +import { BuilderStep, RequiredFieldsTemplate } from "../../src/cerios-builder"; + +type User = { + id: string; + name: string; + email: string; + role: string; + age?: number; + tags?: string[]; + address?: { street: string; city: string }; +}; + +class UserBuilder extends CeriosAutoBuilder() { + static requiredTemplate: RequiredFieldsTemplate = ["id", "name", "email", "role"]; + + static create(): UserBuilder { + return new UserBuilder({}, { requiredFields: this.requiredTemplate }); + } + + // Custom method mixing with auto setters; return type inferred (no annotation needed). + // oxlint-disable-next-line explicit-function-return-type + asAdmin() { + return this.role("admin"); + } + + // Explicit BuilderStep return type also works. + asGuest(): BuilderStep { + return this.role("guest"); + } +} + +describe("CeriosAutoBuilder - automatic setters", () => { + it("should provide a bare setter for every property", () => { + const user = UserBuilder.create().id("1").name("John").email("john@example.com").role("user").age(30).build(); + + expect(user).toEqual({ id: "1", name: "John", email: "john@example.com", role: "user", age: 30 }); + }); + + it("should overwrite a previously set property", () => { + const user = UserBuilder.create().id("1").name("John").name("Jane").email("e@x.io").role("user").build(); + + expect(user.name).toBe("Jane"); + }); + + it("should mix custom methods with auto setters", () => { + const user = UserBuilder.create().id("1").name("John").email("e@x.io").asAdmin().build(); + + expect(user.role).toBe("admin"); + }); + + it("should support custom methods with an explicit BuilderStep return type", () => { + const user = UserBuilder.create().id("1").name("John").email("e@x.io").asGuest().build(); + + expect(user.role).toBe("guest"); + }); + + it("should be immutable - setters fork independent builders", () => { + const base = UserBuilder.create().id("1").name("John").email("e@x.io"); + + const admin = base.role("admin").build(); + const guest = base.role("guest").build(); + + expect(admin.role).toBe("admin"); + expect(guest.role).toBe("guest"); + }); + + it("should preserve the concrete subclass type through the chain", () => { + const builder = UserBuilder.create().id("1"); + + expect(builder).toBeInstanceOf(UserBuilder); + expect(builder.clone()).toBeInstanceOf(UserBuilder); + }); +}); + +describe("CeriosAutoBuilder - runtime validation", () => { + it("should throw when a required field is missing at runtime", () => { + const builder = UserBuilder.create().id("1").name("John"); + + // TypeScript prevents build() here; force it to exercise runtime validation. + const unsafe = builder as unknown as { build: () => User }; + expect(() => unsafe.build()).toThrow("Missing required fields: email, role"); + }); + + it("should run validators added fluently", () => { + const builder = UserBuilder.create() + .id("1") + .name("John") + .email("e@x.io") + .role("user") + .addValidator((u) => (u.age === undefined || u.age >= 18 ? true : "Age must be 18 or older")) + .age(16); + + expect(() => builder.build()).toThrow("Validation failed: Age must be 18 or older"); + }); +}); + +describe("CeriosAutoBuilder - static from", () => { + it("should seed a builder from an existing object and rebuild", () => { + const existing: User = { id: "1", name: "John", email: "john@example.com", role: "user" }; + + const updated = UserBuilder.from(existing).name("Jane").build(); + + expect(updated).toEqual({ id: "1", name: "Jane", email: "john@example.com", role: "user" }); + expect(existing.name).toBe("John"); + }); +}); diff --git a/tests/cerios-builder/cerios-builder-build-gate.test.ts b/tests/cerios-builder/cerios-builder-build-gate.test.ts new file mode 100644 index 0000000..e0823af --- /dev/null +++ b/tests/cerios-builder/cerios-builder-build-gate.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { BuilderStep, CeriosBuilder } from "../../src/cerios-builder"; + +type Prefs = { + theme?: string; + locale?: string; +}; + +class PrefsBuilder extends CeriosBuilder { + static create(): PrefsBuilder { + return new PrefsBuilder({}); + } + + theme(value: string): BuilderStep { + return this.setProperty("theme", value); + } + + locale(value: string): BuilderStep { + return this.setProperty("locale", value); + } +} + +type Account = { + id: string; + nickname?: string; +}; + +class AccountBuilder extends CeriosBuilder { + static create(): AccountBuilder { + return new AccountBuilder({}); + } + + id(value: string): BuilderStep { + return this.setProperty("id", value); + } + + nickname(value: string): BuilderStep { + return this.setProperty("nickname", value); + } +} + +// The build gate dissolves when the target type has no required keys: an all-optional type +// builds without a single setter call. A type with any required key keeps the strict gate. +describe("CeriosBuilder - build gate on all-optional types", () => { + it("builds an all-optional type without any setter call", () => { + expectTypeOf(PrefsBuilder.create().build()).toEqualTypeOf(); + expect(PrefsBuilder.create().build()).toEqual({}); + }); + + it("keeps every compile-time-validated variant available on a fresh builder", () => { + expect(PrefsBuilder.create().buildWithoutRuntimeValidation()).toEqual({}); + expect(PrefsBuilder.create().buildFrozen()).toEqual({}); + expect(Object.isFrozen(PrefsBuilder.create().buildFrozen())).toBe(true); + expect(PrefsBuilder.create().buildDeepFrozen()).toEqual({}); + expect(PrefsBuilder.create().buildSealed()).toEqual({}); + expect(PrefsBuilder.create().buildDeepSealed()).toEqual({}); + }); + + it("still builds after setting and removing optional properties", () => { + expect(PrefsBuilder.create().theme("dark").build()).toEqual({ theme: "dark" }); + expect(PrefsBuilder.create().theme("dark").removeOptionalProperty("theme").build()).toEqual({}); + expect(PrefsBuilder.create().theme("dark").locale("nl").clearOptionalProperties().build()).toEqual({}); + }); + + it("keeps the strict gate for a type with required keys", () => { + // @ts-expect-error - id is required and not set + AccountBuilder.create().build(); + // @ts-expect-error - setting only an optional key does not satisfy the gate + AccountBuilder.create().nickname("x").build(); + + expect(AccountBuilder.create().id("1").build()).toEqual({ id: "1" }); + }); + + it("still runs runtime validation when configured", () => { + const builder = PrefsBuilder.create().setRequiredFields(["theme"]); + + expect(() => builder.build()).toThrow("Missing required fields: theme"); + expect(builder.theme("dark").build()).toEqual({ theme: "dark" }); + }); + + it("reports a required field whose key exists but whose value is null or undefined", () => { + const builder = PrefsBuilder.create().setRequiredFields(["theme"]); + + expect(() => builder.theme(null as unknown as string).build()).toThrow("Missing required fields: theme"); + expect(() => builder.theme(undefined as unknown as string).build()).toThrow("Missing required fields: theme"); + }); +}); diff --git a/tests/cerios-builder/cerios-builder-from-branded.test.ts b/tests/cerios-builder/cerios-builder-from-branded.test.ts new file mode 100644 index 0000000..3a0d403 --- /dev/null +++ b/tests/cerios-builder/cerios-builder-from-branded.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; + +import { BuilderStep, CeriosBuilder } from "../../src/cerios-builder"; + +type User = { + id: string; + name: string; + age?: number; +}; + +// `from()` requires a public constructor. TypeScript cannot express "constructor may be +// protected" in a `this` constraint, so a subclass that wants `from()` has to widen it. +class UserBuilder extends CeriosBuilder { + public constructor(data: Partial = {}) { + super(data); + } + + static create(): UserBuilder { + return new UserBuilder({}); + } + + id(value: string): BuilderStep { + return this.setProperty("id", value); + } + + name(value: string): BuilderStep { + return this.setProperty("name", value); + } +} + +describe("CeriosBuilder.from - branded result", () => { + it("returns a builder that is immediately buildable", () => { + // Seeding from a complete object means every property is set, so build() must be + // callable. Before, from() returned an unbranded builder and every call site had to + // fall back to buildUnsafe() - including the example in from()'s own JSDoc. + const built = UserBuilder.from({ id: "1", name: "John" }).build(); + + expect(built).toEqual({ id: "1", name: "John" }); + }); + + it("stays buildable after further setters", () => { + expect(UserBuilder.from({ id: "1", name: "John" }).name("Renamed").build().name).toBe("Renamed"); + }); + + it("returns the concrete subclass", () => { + expect(UserBuilder.from({ id: "1", name: "John" })).toBeInstanceOf(UserBuilder); + }); + + it("deep-clones the source", () => { + const source: User = { id: "1", name: "John" }; + const built = UserBuilder.from(source).build(); + built.name = "Mutated"; + + expect(source.name).toBe("John"); + }); +}); diff --git a/tests/cerios-builder/cerios-builder-post-patch-extension.test.ts b/tests/cerios-builder/cerios-builder-post-patch-extension.test.ts index 1751ab2..b64209f 100644 --- a/tests/cerios-builder/cerios-builder-post-patch-extension.test.ts +++ b/tests/cerios-builder/cerios-builder-post-patch-extension.test.ts @@ -34,11 +34,11 @@ abstract class BasePostRequestBuilder extends CeriosB } title(value: NonNullable): BuilderStep { - return this.setProperty("title", value as T["title"]); + return this.setProperty("title", value); } content(value: NonNullable): BuilderStep { - return this.setProperty("content", value as T["content"]); + return this.setProperty("content", value); } addTag(value: string): BuilderStep { diff --git a/tests/cerios-builder/cerios-builder-property-removal.test.ts b/tests/cerios-builder/cerios-builder-property-removal.test.ts index a997761..02236fe 100644 --- a/tests/cerios-builder/cerios-builder-property-removal.test.ts +++ b/tests/cerios-builder/cerios-builder-property-removal.test.ts @@ -16,7 +16,7 @@ class PersonBuilder extends CeriosBuilder { requiredFields?: ReadonlyArray, validators?: Array<(obj: Partial) => boolean | string>, ) { - super(data, requiredFields as ReadonlyArray, validators); + super(data, requiredFields, validators); } static requiredTemplate = ["name", "age"] as const; diff --git a/tests/cerios-builder/cerios-builder-validators.test.ts b/tests/cerios-builder/cerios-builder-validators.test.ts index 68f73df..988f750 100644 --- a/tests/cerios-builder/cerios-builder-validators.test.ts +++ b/tests/cerios-builder/cerios-builder-validators.test.ts @@ -15,7 +15,7 @@ class PersonBuilder extends CeriosBuilder { requiredFields?: ReadonlyArray, validators?: Array<(obj: Partial) => boolean | string>, ) { - super(data, requiredFields as ReadonlyArray, validators); + super(data, requiredFields, validators); } setName(value: string): BuilderStep { diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-base-class-extension.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-base-class-extension.test.ts new file mode 100644 index 0000000..0d32972 --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-base-class-extension.test.ts @@ -0,0 +1,180 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { BuilderExtension } from "../../src/auto-builder-core"; +import { CeriosClassAutoBuilder, ClassAutoBuilderBase } from "../../src/cerios-class-auto-builder"; +import { ClassBuilderWith } from "../../src/cerios-class-builder"; + +abstract class BaseEntity { + id!: string; + createdAt?: Date; + + describe(): string { + return `${this.constructor.name}#${this.id}`; + } +} + +// Derived class whose own constructor assigns the data it receives. +class Order extends BaseEntity { + total!: number; + reference?: string; + + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + summaryText(): string { + return `${this.describe()}: ${this.total}`; + } +} + +// Derived class without any constructor: building must fall back to assigning the data +// after instantiation. +class Customer extends BaseEntity { + name!: string; +} + +/** + * Shared builder logic for every entity builder, written once against the abstract base + * class. BaseEntity is used purely as a type here; only the concrete derived classes are + * ever passed to CeriosClassAutoBuilder. + */ +// oxlint-disable-next-line typescript/explicit-function-return-type -- the return type names the class declared inside; BuilderExtension is applied in the return statement +function BaseEntityBuilder>(Builder: TBuilder) { + abstract class EntityBuilder extends Builder { + sequentialId(sequence: number): ClassBuilderWith { + return this.id(`entity-${sequence}`); + } + + createdOn(year: number): ClassBuilderWith { + return this.createdAt(new Date(Date.UTC(year, 0, 1))); + } + } + return EntityBuilder as BuilderExtension; +} + +class OrderBuilder extends BaseEntityBuilder(CeriosClassAutoBuilder(Order)) { + static create(): OrderBuilder { + return new OrderBuilder({}, { requiredFields: ["id", "total"] }); + } +} + +class CustomerBuilder extends BaseEntityBuilder(CeriosClassAutoBuilder(Customer)) { + static create(): CustomerBuilder { + return new CustomerBuilder({}, { requiredFields: ["id", "name"] }); + } +} + +describe("CeriosClassAutoBuilder - base class extension", () => { + it("generates setters for inherited data properties", () => { + const order = OrderBuilder.create() + .id("order-1") + .createdAt(new Date(Date.UTC(2026, 5, 1))) + .total(99) + .build(); + + expect(order.id).toBe("order-1"); + expect(order.createdAt).toEqual(new Date(Date.UTC(2026, 5, 1))); + expect(order.total).toBe(99); + }); + + it("builds a real instance of the derived class with the base prototype chain intact", () => { + const order = OrderBuilder.create().id("order-2").total(150).reference("ref-1").build(); + + expect(order).toBeInstanceOf(Order); + expect(order).toBeInstanceOf(BaseEntity); + expect(order.describe()).toBe("Order#order-2"); + expect(order.summaryText()).toBe("Order#order-2: 150"); + }); + + it("assigns inherited properties even when no constructor accepts the data", () => { + // Customer declares no constructor, so the builder's post-construction assignment + // must carry both the inherited and the own data properties onto the instance. + const customer = CustomerBuilder.create().id("customer-1").name("Ada").build(); + + expect(customer).toBeInstanceOf(Customer); + expect(customer).toBeInstanceOf(BaseEntity); + expect(customer.id).toBe("customer-1"); + expect(customer.name).toBe("Ada"); + expect(customer.describe()).toBe("Customer#customer-1"); + }); + + it("shares custom methods across sibling builders of the same base class", () => { + const order = OrderBuilder.create().sequentialId(1).createdOn(2026).total(10).build(); + const customer = CustomerBuilder.create().sequentialId(2).name("Grace").build(); + + expect(order.id).toBe("entity-1"); + expect(order.createdAt).toEqual(new Date(Date.UTC(2026, 0, 1))); + expect(customer.id).toBe("entity-2"); + }); + + it("counts a shared method's setter brand toward the derived build gate", () => { + // sequentialId() brands the inherited required id via the base class; the derived + // gate must accept it just like a direct .id() call. + const order = OrderBuilder.create().sequentialId(3).total(25).build(); + + expectTypeOf(order).toEqualTypeOf(); + expect(order.id).toBe("entity-3"); + }); + + it("gates build() per derived class at compile time", () => { + expect(() => { + // @ts-expect-error - total (derived-only, required) not set + return OrderBuilder.create().id("order-3").build(); + }).toThrow("Missing required fields: total"); + expect(() => { + // @ts-expect-error - id (inherited, required) not set + return OrderBuilder.create().total(10).build(); + }).toThrow("Missing required fields: id"); + expect(() => { + // @ts-expect-error - name (derived-only, required) not set + return CustomerBuilder.create().id("customer-2").build(); + }).toThrow("Missing required fields: name"); + }); + + it("validates required fields per derived builder independently at runtime", () => { + expect(() => OrderBuilder.create().id("order-4").buildWithoutCompileTimeValidation()).toThrow( + "Missing required fields: total", + ); + expect(() => CustomerBuilder.create().name("Edsger").buildWithoutCompileTimeValidation()).toThrow( + "Missing required fields: id", + ); + }); + + it("clone preserves the concrete builder including shared methods", () => { + const cloned = OrderBuilder.create().sequentialId(4).total(40).clone().createdOn(2025).total(45); + + const order = cloned.build(); + expect(order).toBeInstanceOf(Order); + expect(order.id).toBe("entity-4"); + expect(order.total).toBe(45); + expect(order.createdAt).toEqual(new Date(Date.UTC(2025, 0, 1))); + }); + + it("keeps the static from() available through the base-builder function", () => { + const existing = new Order({ id: "order-5", total: 75, reference: "ref-2" }); + + const order = OrderBuilder.from(existing).reference("ref-3").build(); + + expect(order).toBeInstanceOf(Order); + expect(order.reference).toBe("ref-3"); + // The seed was deep-cloned, not aliased. + expect(existing.reference).toBe("ref-2"); + }); + + it("keeps instanceof stable when another builder hierarchy targets the same class", () => { + class DirectOrderBuilder extends CeriosClassAutoBuilder(Order) { + static create(): DirectOrderBuilder { + return new DirectOrderBuilder(); + } + } + + const viaBase = OrderBuilder.create().sequentialId(5).total(5).build(); + const direct = DirectOrderBuilder.create().id("order-6").total(6).buildUnsafe(); + + expect(viaBase).toBeInstanceOf(Order); + expect(direct).toBeInstanceOf(Order); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-build-methods.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-build-methods.test.ts new file mode 100644 index 0000000..f8a3d7a --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-build-methods.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; +import { DataPropertiesOnly, InternalClassBrand } from "../../src/cerios-class-builder"; + +class User { + id!: string; + name!: string; + email!: string; + age?: number; + address?: { street: string; city: string }; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + greet(): string { + return `Hello, ${this.name}`; + } +} + +class UserBuilder extends CeriosClassAutoBuilder(User) { + static requiredDataProperties: ReadonlyArray = ["id", "name", "email"]; + + static create(): UserBuilder { + return new UserBuilder(); + } +} + +// The intersection with the brand marks all required data properties as set, +// so the returned builder is buildable (id/name/email are the required ones). +function complete(): UserBuilder & InternalClassBrand> { + return UserBuilder.create().id("1").name("John").email("john@example.com"); +} + +describe("CeriosClassAutoBuilder build methods", () => { + it("build() returns a class instance when required fields are set", () => { + const user = complete().build(); + + expect(user).toBeInstanceOf(User); + expect(user.greet()).toBe("Hello, John"); + }); + + it("buildWithoutRuntimeValidation() skips the runtime check", () => { + const builder = UserBuilder.create().id("1"); + const unsafe = builder as unknown as { buildWithoutRuntimeValidation: () => User }; + + expect(unsafe.buildWithoutRuntimeValidation().id).toBe("1"); + }); + + it("buildWithoutCompileTimeValidation() runs the runtime check", () => { + expect(complete().buildWithoutCompileTimeValidation().email).toBe("john@example.com"); + + const builder = UserBuilder.create().id("1"); + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow( + "Missing required fields: name, email. Please set these fields before calling buildWithoutCompileTimeValidation.", + ); + }); + + it("buildUnsafe() skips validation but still returns a real class instance", () => { + const user = UserBuilder.create().id("1").buildUnsafe(); + + expect(user).toBeInstanceOf(User); + expect(user.id).toBe("1"); + }); + + it("buildPartial() returns a plain object, not a class instance", () => { + const partial = UserBuilder.create().id("1").age(30).buildPartial(); + + expect(partial).toEqual({ id: "1", age: 30 }); + expect(partial).not.toBeInstanceOf(User); + }); + + it("buildPartial() returns a copy, so mutating it cannot reach the builder", () => { + const builder = UserBuilder.create().id("1"); + const partial = builder.buildPartial(); + partial.id = "mutated"; + + expect(builder.buildPartial().id).toBe("1"); + }); + + it("buildFrozen() and buildDeepFrozen() freeze the result", () => { + const shallow = complete().address({ street: "Main St", city: "Springfield" }).buildFrozen(); + expect(Object.isFrozen(shallow)).toBe(true); + expect(Object.isFrozen(shallow.address)).toBe(false); + + const deep = complete().address({ street: "Main St", city: "Springfield" }).buildDeepFrozen(); + expect(Object.isFrozen(deep)).toBe(true); + expect(Object.isFrozen(deep.address)).toBe(true); + }); + + it("buildSealed() and buildDeepSealed() seal the result", () => { + const shallow = complete().address({ street: "Main St", city: "Springfield" }).buildSealed(); + expect(Object.isSealed(shallow)).toBe(true); + expect(Object.isSealed(shallow.address)).toBe(false); + + const deep = complete().address({ street: "Main St", city: "Springfield" }).buildDeepSealed(); + expect(Object.isSealed(deep)).toBe(true); + expect(Object.isSealed(deep.address)).toBe(true); + }); + + it("frozen build still runs runtime validation", () => { + const builder = UserBuilder.create().id("1"); + const unsafe = builder as unknown as { buildFrozen: () => User }; + + expect(() => unsafe.buildFrozen()).toThrow("Missing required fields: name, email"); + }); + + it("honours static requiredDataProperties without calling setRequiredFields", () => { + // The static list is the only source of required fields here - the builder never + // calls setRequiredFields(). + const builder = UserBuilder.create().id("1").name("John"); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: email"); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-cloning.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-cloning.test.ts new file mode 100644 index 0000000..f83a12a --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-cloning.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +class Person { + name!: string; + age!: number; + hobbies?: string[]; + address?: { street: string; city: string }; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + greet(): string { + return `Hello, ${this.name}`; + } +} + +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); + } +} + +describe("CeriosClassAutoBuilder - clone", () => { + it("returns an instance of the concrete subclass", () => { + expect(PersonBuilder.create().name("John").clone()).toBeInstanceOf(PersonBuilder); + }); + + it("deep-clones nested objects, so mutating the clone does not affect the original", () => { + const original = PersonBuilder.create().name("John").address({ street: "Main St", city: "Springfield" }); + const cloned = original.clone(); + + const clonedAddress = cloned.buildPartial().address; + if (clonedAddress) { + clonedAddress.city = "Shelbyville"; + } + + expect(original.buildPartial().address?.city).toBe("Springfield"); + }); + + it("deep-clones arrays", () => { + const original = PersonBuilder.create().name("John").hobbies(["chess"]); + const cloned = original.clone(); + + cloned.buildPartial().hobbies?.push("running"); + + expect(original.buildPartial().hobbies).toEqual(["chess"]); + }); + + it("preserves validators", () => { + const cloned = PersonBuilder.create() + .addValidator((p) => (p.age !== undefined && p.age >= 18 ? true : "Age must be 18 or older")) + .name("John") + .age(16) + .clone(); + + expect(() => cloned.build()).toThrow("Validation failed: Age must be 18 or older"); + }); + + it("preserves instance-level required fields", () => { + const cloned = PersonBuilder.create().setRequiredFields(["name", "age"]).name("John").clone(); + + expect(() => cloned.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); +}); + +describe("CeriosClassAutoBuilder - from", () => { + it("returns the concrete subclass, not the base runtime", () => { + expect(PersonBuilder.from(new Person({ name: "John", age: 30 }))).toBeInstanceOf(PersonBuilder); + }); + + it("deep-clones the source, so building cannot reach back into it", () => { + const source = new Person({ name: "John", age: 30, address: { street: "Main St", city: "Springfield" } }); + const built = PersonBuilder.from(source).build(); + + if (built.address) { + built.address.city = "Shelbyville"; + } + + expect(source.address?.city).toBe("Springfield"); + }); + + it("produces a builder that is immediately buildable and keeps class methods", () => { + const built = PersonBuilder.from(new Person({ name: "John", age: 30 })).build(); + + expect(built).toBeInstanceOf(Person); + expect(built.greet()).toBe("Hello, John"); + }); + + it("seeds data only - the source instance's identity is not reused", () => { + const source = new Person({ name: "John", age: 30 }); + const built = PersonBuilder.from(source).build(); + + expect(built).not.toBe(source); + expect(built.name).toBe("John"); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-composition.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-composition.test.ts new file mode 100644 index 0000000..0836c70 --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-composition.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; +import { CeriosClassBuilder, ClassBuilderComposerFromFactory, ClassBuilderWith } from "../../src/cerios-class-builder"; + +class Person { + name!: string; + age!: number; + city!: string; + + constructor(data?: Partial) { + if (data) Object.assign(this, data); + } + + greet(): string { + return `Hi, ${this.name}`; + } + + // A method with parameters - must never count as a property to set. + describe(prefix: string): string { + return `${prefix} ${this.name} (${this.age})`; + } +} + +class Company { + title!: string; + constructor(data?: Partial) { + if (data) Object.assign(this, data); + } +} + +class Team { + title!: string; + lead!: Person; + + constructor(data?: Partial) { + if (data) Object.assign(this, data); + } + + summary(): string { + return `${this.title} led by ${this.lead.name}`; + } +} + +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); + } + + static createWithDefaults(): ClassBuilderWith { + return PersonBuilder.create().city("Rotterdam"); + } + + static createComplete(): ClassBuilderWith { + return PersonBuilder.create().name("Alice").age(30).city("Rotterdam"); + } +} + +class CompanyBuilder extends CeriosClassAutoBuilder(Company) { + static create(): CompanyBuilder { + return new CompanyBuilder(); + } +} + +class TeamBuilder extends CeriosClassAutoBuilder(Team) { + static create(): TeamBuilder { + return new TeamBuilder(); + } + + asSquad(): ClassBuilderWith { + return this.title("squad"); + } + + withLead(fn: ClassBuilderComposerFromFactory): ClassBuilderWith { + return this.lead(fn(PersonBuilder.create()).build()); + } + + withLeadDefaults( + fn: ClassBuilderComposerFromFactory, + ): ClassBuilderWith { + return this.lead(fn(PersonBuilder.createWithDefaults()).build()); + } + + withDefaultLead( + fn?: ClassBuilderComposerFromFactory, + ): ClassBuilderWith { + const builder = PersonBuilder.createComplete(); + return this.lead(fn ? fn(builder).build() : builder.build()); + } +} + +describe("CeriosClassAutoBuilder - nested builder composition", () => { + it("composes a nested class builder and preserves class methods", () => { + const team = TeamBuilder.create() + .asSquad() + .withLead((b) => b.name("Alice").age(30).city("Rotterdam")) + .build(); + + expectTypeOf(team).toEqualTypeOf(); + expect(team).toBeInstanceOf(Team); + expect(team.lead).toBeInstanceOf(Person); + expect(team.lead.greet()).toBe("Hi, Alice"); + expect(team.summary()).toBe("squad led by Alice"); + expect(team.lead.describe("Lead:")).toBe("Lead: Alice (30)"); + }); + + it("treats data properties preset by the factory as already set", () => { + const team = TeamBuilder.create() + .title("t") + // `city` is preset by createWithDefaults and must not be required here + .withLeadDefaults((b) => b.name("Bob").age(41)) + .build(); + + expect(team.lead.city).toBe("Rotterdam"); + expect(team.lead).toBeInstanceOf(Person); + }); + + it("makes the callback optional when the factory presets every data property", () => { + const team = TeamBuilder.create().title("t").withDefaultLead().build(); + expect(team.lead.greet()).toBe("Hi, Alice"); + + const overridden = TeamBuilder.create() + .title("t") + .withDefaultLead((b) => b.name("Carol")) + .build(); + expect(overridden.lead.name).toBe("Carol"); + }); + + it("rejects a callback that returns an incompletely-set child builder", () => { + // @ts-expect-error - age and city are never set on the nested person builder + TeamBuilder.create().withLead((b) => b.name("Alice")); + + // @ts-expect-error - name and age still missing even with the city default + TeamBuilder.create().withLeadDefaults((b) => b); + + // the compile-time gate above matches the actual runtime state + expect(PersonBuilder.create().name("Alice").buildPartial()).toEqual({ name: "Alice" }); + expect(PersonBuilder.createWithDefaults().buildPartial()).toEqual({ city: "Rotterdam" }); + }); + + it("rejects a callback that returns a builder for a different class", () => { + // @ts-expect-error - CompanyBuilder does not build a Person + TeamBuilder.create().withLead(() => CompanyBuilder.create().title("acme")); + + expect(CompanyBuilder.create().title("acme").build()).toBeInstanceOf(Company); + }); + + it("still gates the parent builder until every data property is set", () => { + // @ts-expect-error - lead is not set + TeamBuilder.create().asSquad().build(); + + // @ts-expect-error - title is not set + TeamBuilder.create().withDefaultLead().build(); + + expect(Object.keys(TeamBuilder.create().asSquad().buildPartial())).toEqual(["title"]); + }); +}); + +describe("ClassBuilderWith", () => { + it("accumulates across custom methods rather than replacing prior state", () => { + const partial = TeamBuilder.create().asSquad(); + + // @ts-expect-error - lead still missing + partial.build(); + + expect(partial.withDefaultLead().build().title).toBe("squad"); + }); + + it("never requires class methods to be set", () => { + // Person has greet() and describe(prefix) - build() is reachable with data only. + const person = PersonBuilder.create().name("Dave").age(20).city("Utrecht").build(); + expect(person.greet()).toBe("Hi, Dave"); + }); + + it("still resolves for hand-written CeriosClassBuilder subclasses", () => { + class PlainPersonBuilder extends CeriosClassBuilder { + static create(): PlainPersonBuilder { + return new PlainPersonBuilder(Person); + } + static createWithDefaults(): ClassBuilderWith { + return PlainPersonBuilder.create().city("Rotterdam"); + } + name(value: string): ClassBuilderWith { + return this.setProperty("name", value); + } + age(value: number): ClassBuilderWith { + return this.setProperty("age", value); + } + city(value: string): ClassBuilderWith { + return this.setProperty("city", value); + } + } + + const person = PlainPersonBuilder.createWithDefaults().name("Eve").age(33).build(); + expect(person).toBeInstanceOf(Person); + expect(person.greet()).toBe("Hi, Eve"); + + // @ts-expect-error - age missing + PlainPersonBuilder.createWithDefaults().name("Eve").build(); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-constructor.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-constructor.test.ts new file mode 100644 index 0000000..fa659dc --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-constructor.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +class Person { + name!: string; + age!: number; + email?: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +// Required fields and validators are declared once, in the subclass constructor. +// Before the options object existed there was no supported way to do this: the declared +// constructor accepted only `data`, and the second positional argument was ignored. +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + constructor(data?: Partial) { + super(data, { + requiredFields: ["name", "age"], + validators: [(p): boolean | string => (p.age !== undefined && p.age >= 18 ? true : "Age must be 18 or older")], + }); + } + + static create(): PersonBuilder { + return new PersonBuilder(); + } +} + +describe("CeriosClassAutoBuilder - constructor options", () => { + it("applies required fields passed through super()", () => { + const builder = PersonBuilder.create().name("John"); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); + + it("applies validators passed through super()", () => { + const builder = PersonBuilder.create().name("John").age(16); + + expect(() => builder.build()).toThrow("Validation failed: Age must be 18 or older"); + }); + + it("applies seed data passed through super()", () => { + const builder = new PersonBuilder({ name: "Seeded" }); + + expect(builder.buildPartial().name).toBe("Seeded"); + }); + + it("carries required fields and validators through copy-on-write setters", () => { + // Every setter re-creates the builder via the base class's internal constructor + // path, which must not lose the options supplied by the subclass. + const builder = PersonBuilder.create().name("John").email("j@x.io").age(16); + + expect(() => builder.build()).toThrow("Validation failed: Age must be 18 or older"); + }); + + it("carries required fields and validators through clone()", () => { + const builder = PersonBuilder.create().name("John").clone(); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); + + it("accepts the internal (classConstructor, data, ...) shape for base-class compatibility", () => { + // The base class's copy-on-write contract is `new this.constructor(classConstructor, + // data, validators, requiredFields)`. The auto builder overrides instantiateBuilder, + // but the runtime class still honours that shape so code constructing through the + // base contract cannot land the class constructor in the data slot. + type MinimalApi = { + buildPartial(): Partial; + age(value: number): MinimalApi; + buildWithoutCompileTimeValidation(): Person; + }; + const InternalShape = CeriosClassAutoBuilder(Person) as unknown as new ( + classConstructor: typeof Person, + data: Partial, + ) => MinimalApi; + const builder = new InternalShape(Person, { name: "Internal" }); + + expect(builder.buildPartial()).toEqual({ name: "Internal" }); + expect(builder.age(30).buildWithoutCompileTimeValidation()).toBeInstanceOf(Person); + }); + + it("accepts an exhaustive required-fields record instead of a path array", () => { + class RecordBuilder extends CeriosClassAutoBuilder(Person) { + constructor(data?: Partial) { + // The record must name every required data property of Person; adding a new + // required property to the class makes this line fail to compile. + super(data, { requiredFields: { name: true, age: true } }); + } + + static create(): RecordBuilder { + return new RecordBuilder(); + } + } + + const builder = RecordBuilder.create().name("John"); + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-nested-properties.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-nested-properties.test.ts new file mode 100644 index 0000000..32566e5 --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-nested-properties.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; +import { ClassBuilderStep } from "../../src/cerios-class-builder"; + +class Person { + name!: string; + hobbies?: string[]; + address?: { street: string; city: string }; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + greet(): string { + return `Hello, ${this.name}`; + } +} + +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); + } + + // The supported replacement for the removed addToArrayProperty helper: a custom + // method that rebuilds the array through the root setter. + addHobby(hobby: string): ClassBuilderStep { + return this.hobbies([...(this.buildPartial().hobbies ?? []), hobby]); + } + + // The supported replacement for the removed setNestedProperty helper: a custom + // method that replaces the root data property with an updated whole value. + inCity(city: string): ClassBuilderStep { + const address = this.buildPartial().address ?? { street: "", city: "" }; + return this.address({ ...address, city }); + } +} + +// A class auto builder exposes only whole-root-property setters. The dot-path and +// array-append helpers of the deprecated base builders are intentionally not part of its +// API: nested structures are composed with custom methods or a director coordinating +// multiple builders. +describe("CeriosClassAutoBuilder - no fine-grained mutation helpers", () => { + it("hides every fine-grained mutation helper at the type level", () => { + const builder = PersonBuilder.create().name("John"); + + // @ts-expect-error - setNestedProperty is not part of the auto-builder API + builder.setNestedProperty("address.city", "Springfield"); + // @ts-expect-error - addToArrayProperty is not part of the auto-builder API + builder.addToArrayProperty("hobbies", "chess"); + // @ts-expect-error - setProperty is not part of the auto-builder API + builder.setProperty("name", "Jane"); + // @ts-expect-error - setProperties is not part of the auto-builder API + builder.setProperties({ name: "Jane" }); + + // Copy-on-write: even the (runtime-reachable) hidden members left the builder alone. + expect(builder.buildPartial()).toEqual({ name: "John" }); + }); +}); + +describe("CeriosClassAutoBuilder - custom-method replacements", () => { + it("appends to an array via a custom method delegating to the root setter", () => { + const builder = PersonBuilder.create().name("John").addHobby("chess").addHobby("running"); + + expect(builder.buildPartial().hobbies).toEqual(["chess", "running"]); + }); + + it("updates a nested value via a custom method replacing the whole root property", () => { + const builder = PersonBuilder.create() + .name("John") + .address({ street: "Main St", city: "Springfield" }) + .inCity("Shelbyville"); + + expect(builder.buildPartial().address).toEqual({ street: "Main St", city: "Shelbyville" }); + }); + + it("stays copy-on-write: the original builder is left intact", () => { + const original = PersonBuilder.create().name("John").hobbies(["chess"]); + original.addHobby("running"); + original.inCity("Shelbyville"); + + expect(original.buildPartial().hobbies).toEqual(["chess"]); + expect(original.buildPartial().address).toBeUndefined(); + }); + + it("builds a real instance with methods intact", () => { + const person = PersonBuilder.create().name("John").addHobby("chess").build(); + + expect(person).toBeInstanceOf(Person); + expect(person.greet()).toBe("Hello, John"); + expect(person.hobbies).toEqual(["chess"]); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-property-removal.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-property-removal.test.ts new file mode 100644 index 0000000..984c179 --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-property-removal.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +class Person { + name!: string; + age!: number; + email?: string; + phone?: string; + address?: { street: string; city: string }; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); + } +} + +class StaticRequiredBuilder extends CeriosClassAutoBuilder(Person) { + static requiredDataProperties: ReadonlyArray = ["name", "address.city"]; + + static create(): StaticRequiredBuilder { + return new StaticRequiredBuilder(); + } +} + +describe("CeriosClassAutoBuilder - removeOptionalProperty", () => { + it("drops a previously set optional data property", () => { + const builder = PersonBuilder.create().name("John").age(30).email("j@x.io").removeOptionalProperty("email"); + + expect(builder.buildPartial()).toEqual({ name: "John", age: 30 }); + }); + + it("returns a new builder, leaving the original intact", () => { + const withEmail = PersonBuilder.create().name("John").email("j@x.io"); + const without = withEmail.removeOptionalProperty("email"); + + expect(withEmail.buildPartial().email).toBe("j@x.io"); + expect(without.buildPartial().email).toBeUndefined(); + }); + + it("preserves validators and required fields across the removal", () => { + const builder = PersonBuilder.create() + .setRequiredFields(["name", "age"]) + .addValidator((p) => (p.name === "" ? "Name must not be empty" : true)) + .name("John") + .email("j@x.io") + .removeOptionalProperty("email"); + + expect(() => builder.buildWithoutCompileTimeValidation()).toThrow("Missing required fields: age"); + }); +}); + +describe("CeriosClassAutoBuilder - clearOptionalProperties", () => { + it("keeps root required properties and clears the rest", () => { + const builder = PersonBuilder.create() + .setRequiredFields(["name", "age"]) + .name("John") + .age(30) + .email("j@x.io") + .phone("555") + .clearOptionalProperties(); + + expect(builder.buildPartial()).toEqual({ name: "John", age: 30 }); + }); + + it("clears everything when no required fields are configured", () => { + const builder = PersonBuilder.create().name("John").email("j@x.io").clearOptionalProperties(); + + expect(builder.buildPartial()).toEqual({}); + }); + + // A required path may point inside a nested object. The root has to survive, otherwise + // the required leaf is destroyed by the very call that is meant to preserve it. + it("preserves the root object of a nested required path", () => { + const builder = PersonBuilder.create() + .setRequiredFields(["name", "address.city"]) + .name("John") + .address({ street: "Main St", city: "Springfield" }) + .email("j@x.io") + .clearOptionalProperties(); + + expect(builder.buildPartial()).toEqual({ + name: "John", + address: { street: "Main St", city: "Springfield" }, + }); + }); + + it("preserves a nested required root supplied via static requiredDataProperties", () => { + const builder = StaticRequiredBuilder.create() + .name("John") + .address({ street: "Main St", city: "Springfield" }) + .email("j@x.io") + .clearOptionalProperties(); + + expect(builder.buildPartial().address?.city).toBe("Springfield"); + }); + + it("still satisfies runtime validation of a nested required path afterwards", () => { + const builder = PersonBuilder.create() + .setRequiredFields(["name", "address.city"]) + .name("John") + .address({ street: "Main St", city: "Springfield" }) + .email("j@x.io") + .clearOptionalProperties(); + + expect(() => builder.buildWithoutCompileTimeValidation()).not.toThrow(); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-proxy-edge-cases.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-proxy-edge-cases.test.ts new file mode 100644 index 0000000..e3e40f1 --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-proxy-edge-cases.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +class Weird { + id!: string; + build!: number; // collides with build() -> setter is buildProp + clone!: string; // collides with clone() -> setter is cloneProp + then!: string; // collides with then handling -> setter is thenProp + "content-type"!: string; // not a valid identifier -> bracket access + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +class Simple { + id!: string; + name!: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + describe(prefix: string): string { + return `${prefix}: ${this.name}`; + } +} + +class Base { + inherited(): string { + return "base"; + } +} + +class Derived extends Base { + id!: string; + + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } +} + +class WeirdBuilder extends CeriosClassAutoBuilder(Weird) { + static create(): WeirdBuilder { + return new WeirdBuilder(); + } +} + +class SimpleBuilder extends CeriosClassAutoBuilder(Simple) { + static create(): SimpleBuilder { + return new SimpleBuilder(); + } +} + +class DerivedBuilder extends CeriosClassAutoBuilder(Derived) { + static create(): DerivedBuilder { + return new DerivedBuilder(); + } +} + +describe("CeriosClassAutoBuilder - proxy edge cases", () => { + it("should resolve immediately when awaited (not a thenable)", async () => { + const builder = SimpleBuilder.create().id("1"); + // oxlint-disable-next-line await-thenable -- intentionally awaiting a non-thenable + const resolved = await builder; + expect(resolved.buildPartial()).toEqual({ id: "1" }); + }); + + it("should not throw on JSON.stringify", () => { + const builder = SimpleBuilder.create().id("1"); + expect(() => JSON.stringify(builder)).not.toThrow(); + }); + + it("should not throw on console.log / inspection", () => { + const builder = SimpleBuilder.create().id("1"); + expect(() => console.log(builder)).not.toThrow(); + }); + + it("should expose real methods to the in operator", () => { + const builder = SimpleBuilder.create(); + expect("build" in builder).toBe(true); + expect("clone" in builder).toBe(true); + }); + + it("should route reserved-name properties through *Prop setters while keeping the real methods", () => { + const weird = WeirdBuilder.create() + .id("1") + .buildProp(42) + .cloneProp("c") + .thenProp("t") + .buildWithoutCompileTimeValidation(); + + expect(weird).toBeInstanceOf(Weird); + expect(weird.build).toBe(42); + expect(weird.clone).toBe("c"); + expect(weird.then).toBe("t"); + }); + + it("should keep build() and clone() working even when those property names are set", () => { + const builder = WeirdBuilder.create().id("1").buildProp(42); + + expect(typeof builder.build).toBe("function"); + expect(builder.clone()).toBeInstanceOf(WeirdBuilder); + }); + + it("should set non-identifier keys via bracket access", () => { + const builder = WeirdBuilder.create().id("1")["content-type"]("application/json"); + + expect(builder.buildPartial()["content-type"]).toBe("application/json"); + }); + + it("should not generate setters for the class's own methods", () => { + const builder = SimpleBuilder.create() as unknown as Record; + + // `describe` is a real method on Simple, not a data property, so it must not become + // a setter - and the built instance must still carry it. + expect(SimpleBuilder.create().id("1").name("John").build().describe("Name")).toBe("Name: John"); + expect(builder.describe).toBeTypeOf("function"); + }); + + it("should not generate setters for inherited class methods", () => { + const instance = DerivedBuilder.create().id("1").build(); + + expect(instance.inherited()).toBe("base"); + }); +}); + +// Names that are real members of the builder but were historically absent from +// RESERVED_BUILDER_NAMES. The proxy's `prop in target` fallback returned the real method +// while the generated type still advertised a bare setter, so calling one silently ran the +// wrong method - `createBuilder("x")` replaced the whole state with the string "x". +// They now route through `*Prop` like any other reserved name. +describe("CeriosClassAutoBuilder - unreserved member-name collisions", () => { + const collisions = [ + "setProperty", + "setProperties", + "setNestedProperty", + "addToArrayProperty", + "createBuilder", + "getClassConstructor", + "getRequiredTemplate", + "validateRequiredFields", + "runValidators", + "hasOwnProperty", + ] as const; + + for (const name of collisions) { + it(`sets a data property named ${name} through ${name}Prop`, () => { + const builder = WeirdBuilder.create() as unknown as Record unknown>; + const stepped = builder[`${name}Prop`]("x") as { buildPartial(): Record }; + + expect(stepped.buildPartial()).toEqual({ [name]: "x" }); + }); + + it(`keeps the real ${name} member reachable under its bare name`, () => { + const builder = WeirdBuilder.create() as unknown as Record; + + expect(builder[name]).toBeTypeOf("function"); + }); + } +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-runtime-cache.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-runtime-cache.test.ts new file mode 100644 index 0000000..b7bbc0f --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-runtime-cache.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +class Person { + name!: string; + age?: number; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + greet(): string { + return `hi ${this.name}`; + } +} + +class Other { + label!: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +// The factory memoises one runtime class per target class. Before that, every call minted a +// new class, so `instanceof` silently failed across two calls and every call added a +// distinct object shape. +describe("CeriosClassAutoBuilder - runtime class identity", () => { + it("returns the same runtime class for the same target class", () => { + expect(CeriosClassAutoBuilder(Person)).toBe(CeriosClassAutoBuilder(Person)); + }); + + it("returns different runtime classes for different target classes", () => { + expect(CeriosClassAutoBuilder(Person)).not.toBe(CeriosClassAutoBuilder(Other)); + }); + + it("keeps instanceof working across separate factory calls", () => { + class Sub extends CeriosClassAutoBuilder(Person) {} + + expect(new Sub() instanceof CeriosClassAutoBuilder(Person)).toBe(true); + }); + + it("builds the correct class from two subclasses sharing a runtime class", () => { + class First extends CeriosClassAutoBuilder(Person) {} + class Second extends CeriosClassAutoBuilder(Person) {} + + expect(new First().name("a").build()).toBeInstanceOf(Person); + expect(new Second().name("b").build().greet()).toBe("hi b"); + }); +}); + +// Sharing one runtime class means anything written to it is shared. Per-subclass statics +// stay isolated because the required-field lookup reads `this.constructor`. +describe("CeriosClassAutoBuilder - static isolation between consumers", () => { + it("keeps a subclass required-field list off its siblings", () => { + class Strict extends CeriosClassAutoBuilder(Person) { + static requiredDataProperties: ReadonlyArray = ["name"]; + } + class Loose extends CeriosClassAutoBuilder(Person) {} + + expect(() => new Strict().age(1).buildWithoutCompileTimeValidation()).toThrow("Missing required fields: name"); + expect(() => new Loose().age(1).buildWithoutCompileTimeValidation()).not.toThrow(); + }); + + it("does not expose a settable required-fields static on the shared base", () => { + const Base = CeriosClassAutoBuilder(Person); + + // Memoisation means this object is shared by every consumer of `Person`, so writing + // a static onto it would be visible to all of them. The deprecated static is no + // longer part of the returned constructor type, so the supported path is blocked at + // compile time. It is deliberately not locked at runtime: `target: es2019` gives + // class static fields `[[Set]]` semantics, and a non-writable base property would + // then break a subclass declaring its own `static requiredDataProperties`. + // This assertion is compile-time only - no runtime write, which would leak into + // every later test in this file. + expectTypeOf(Base).not.toHaveProperty("requiredDataProperties"); + }); + + it("keeps each subclass's own validators and required fields separate", () => { + class WithValidator extends CeriosClassAutoBuilder(Person) { + constructor(data?: Partial) { + super(data, { validators: [(p): boolean | string => (p.name === "bad" ? "nope" : true)] }); + } + } + class Plain extends CeriosClassAutoBuilder(Person) {} + + expect(() => new WithValidator().name("bad").build()).toThrow("nope"); + expect(() => new Plain().name("bad").build()).not.toThrow(); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-subclass-state.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-subclass-state.test.ts new file mode 100644 index 0000000..887d471 --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-subclass-state.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +class Doc { + title!: string; + body?: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +// Same contract as the object auto builder: copy-on-write bypasses the subclass +// constructor, so it must carry the subclass's own fields across itself. +class TrackingBuilder extends CeriosClassAutoBuilder(Doc) { + log: string[] = []; + + static create(): TrackingBuilder { + return new TrackingBuilder(); + } + + track(message: string): this { + this.log.push(message); + return this; + } +} + +describe("CeriosClassAutoBuilder - subclass instance state across copy-on-write", () => { + it("keeps a subclass field after a setter call", () => { + const forked = TrackingBuilder.create().title("t"); + + expect(Array.isArray(forked.log)).toBe(true); + }); + + it("does not shadow a subclass field with an auto setter", () => { + expect(typeof TrackingBuilder.create().title("t").log).not.toBe("function"); + }); + + it("leaves subclass methods usable after a setter call", () => { + const forked = TrackingBuilder.create().title("t").track("set title"); + + expect(forked.log).toEqual(["set title"]); + }); + + it("keeps subclass fields through clone()", () => { + expect(Array.isArray(TrackingBuilder.create().title("t").clone().log)).toBe(true); + }); + + it("keeps subclass fields through setRequiredFields() and removeOptionalProperty()", () => { + const configured = TrackingBuilder.create().setRequiredFields(["title"]).title("t").body("b"); + + expect(Array.isArray(configured.log)).toBe(true); + expect(Array.isArray(configured.removeOptionalProperty("body").log)).toBe(true); + }); + + it("still builds a real class instance", () => { + expect(TrackingBuilder.create().title("t").build()).toBeInstanceOf(Doc); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-type-safety.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-type-safety.test.ts new file mode 100644 index 0000000..ffdd8c9 --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-type-safety.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; +import { ClassBuilderStep } from "../../src/cerios-class-builder"; + +class Person { + name!: string; + age!: number; + email?: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + greet(): string { + return `Hi, ${this.name}`; + } + + // Method WITH parameters - the DataPropertiesOnly fix must strip it. + rename(next: string): void { + this.name = next; + } +} + +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); + } +} + +describe("CeriosClassAutoBuilder - compile-time safety", () => { + it("allows build() only once all required data properties are set", () => { + const complete = PersonBuilder.create().name("John").age(30); + expectTypeOf(complete.build()).toEqualTypeOf(); + + // @ts-expect-error - age not set + PersonBuilder.create().name("John").build(); + // @ts-expect-error - nothing set + PersonBuilder.create().build(); + + expect(complete.build().greet()).toBe("Hi, John"); + }); + + it("does not generate setters for methods (with or without parameters)", () => { + // @ts-expect-error - greet is a method, not a data property + PersonBuilder.create().greet(() => "x"); + // @ts-expect-error - rename is a parameterized method, not a data property + PersonBuilder.create().rename("x"); + + expect(PersonBuilder.create().name("n").age(1).build()).toBeInstanceOf(Person); + }); + + it("does not require methods or optional properties to build", () => { + const person = PersonBuilder.create().name("John").age(30).build(); + expect(person.email).toBeUndefined(); + }); + + it("enforces setter value types", () => { + // @ts-expect-error - age must be a number + const wrong = PersonBuilder.create().age("thirty"); + // @ts-expect-error - unknown property + PersonBuilder.create().unknownProp("x"); + + expect(wrong.buildPartial()).toEqual({ age: "thirty" }); + }); + + it("gates compile-time-validated build variants until complete", () => { + const incomplete = PersonBuilder.create().name("John"); + + // @ts-expect-error - age missing + incomplete.buildWithoutRuntimeValidation(); + // @ts-expect-error - age missing + incomplete.buildFrozen(); + + expect(incomplete.buildPartial()).toEqual({ name: "John" }); + }); + + it("makes builders seeded with from immediately buildable", () => { + const existing = new Person({ name: "John", age: 30 }); + const person = PersonBuilder.from(existing).build(); + expect(person).toBeInstanceOf(Person); + }); + + it("gates every compile-time-validated build variant", () => { + const incomplete = PersonBuilder.create().name("John"); + + // @ts-expect-error - age missing + incomplete.build(); + // @ts-expect-error - age missing + incomplete.buildWithoutRuntimeValidation(); + // @ts-expect-error - age missing + incomplete.buildFrozen(); + // @ts-expect-error - age missing + incomplete.buildDeepFrozen(); + // @ts-expect-error - age missing + incomplete.buildSealed(); + // @ts-expect-error - age missing + incomplete.buildDeepSealed(); + + // The unvalidated variants stay callable at any point. + expect(incomplete.buildUnsafe()).toBeInstanceOf(Person); + expect(incomplete.buildPartial()).toEqual({ name: "John" }); + }); + + it("hides the protected setProperty and setProperties helpers", () => { + // @ts-expect-error - setProperty is not part of the class auto-builder API + const viaSetProperty = PersonBuilder.create().setProperty("name", "John"); + // @ts-expect-error - setProperties is not part of the class auto-builder API + const viaSetProperties = PersonBuilder.create().setProperties({ name: "John" }); + + // Hidden at compile time only - the protected members still exist at runtime. + expect(viaSetProperty).toBeDefined(); + expect(viaSetProperties).toBeDefined(); + }); + + it("keeps a fully-set builder assignable to the plain subclass type", () => { + const complete: PersonBuilder = PersonBuilder.create().name("John").age(30); + + expect(complete.buildPartial()).toEqual({ name: "John", age: 30 }); + }); + + it("supports custom logic through distinctly-named methods delegating to auto setters", () => { + class TrimmedBuilder extends CeriosClassAutoBuilder(Person) { + static create(): TrimmedBuilder { + return new TrimmedBuilder(); + } + + trimmedName(value: string): ClassBuilderStep { + return this.name(value.trim()); + } + } + + expect(TrimmedBuilder.create().trimmedName(" John ").age(30).build().name).toBe("John"); + }); + + it("takes exactly one argument - the excluded-keys parameter is gone", () => { + // @ts-expect-error - the factory no longer takes an excluded-keys argument + const base = CeriosClassAutoBuilder(Person, ["name"]); + expect(base).toBeDefined(); + }); + + it("builds an all-optional class without any setter call", () => { + class Settings { + theme?: string; + locale?: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + } + + class SettingsBuilder extends CeriosClassAutoBuilder(Settings) { + static create(): SettingsBuilder { + return new SettingsBuilder(); + } + } + + // No required data properties: the build gate dissolves and a fresh builder builds. + expect(SettingsBuilder.create().build()).toBeInstanceOf(Settings); + expect(SettingsBuilder.create().theme("dark").build().theme).toBe("dark"); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder.test.ts new file mode 100644 index 0000000..a1566be --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder.test.ts @@ -0,0 +1,160 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; +import { ClassBuilderStep } from "../../src/cerios-class-builder"; + +class Person { + name!: string; + age!: number; + email?: string; + hobbies?: string[]; + address?: { street: string; city: string }; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + greet(): string { + return `Hello, my name is ${this.name}`; + } + + // Method with parameters - must not become a setter and must not block build(). + describe(prefix: string): string { + return `${prefix}: ${this.name}`; + } +} + +class NoConstructorAssignment { + label!: string; + count!: number; +} + +class PersonBuilder extends CeriosClassAutoBuilder(Person) { + static create(): PersonBuilder { + return new PersonBuilder(); + } + + // Custom method mixing with auto setters; explicit return type. + adult(): ClassBuilderStep { + return this.age(18); + } +} + +describe("CeriosClassAutoBuilder - automatic setters", () => { + it("builds a real class instance with methods preserved", () => { + const person = PersonBuilder.create().name("John").age(30).build(); + + expect(person).toBeInstanceOf(Person); + expect(person.greet()).toBe("Hello, my name is John"); + expect(person.describe("Name")).toBe("Name: John"); + expect(person.name).toBe("John"); + }); + + it("assigns properties manually when the constructor does not", () => { + class LabelBuilder extends CeriosClassAutoBuilder(NoConstructorAssignment) { + static create(): LabelBuilder { + return new LabelBuilder(); + } + } + + const instance = LabelBuilder.create().label("a").count(2).build(); + expect(instance).toBeInstanceOf(NoConstructorAssignment); + expect(instance.label).toBe("a"); + expect(instance.count).toBe(2); + }); + + it("sets optional and nested properties", () => { + const person = PersonBuilder.create() + .name("John") + .age(30) + .email("j@x.io") + .address({ + street: "Main St", + city: "Springfield", + }) + .build(); + + expect(person.email).toBe("j@x.io"); + expect(person.address?.city).toBe("Springfield"); + }); + + it("mixes custom methods with auto setters", () => { + const person = PersonBuilder.create().name("John").adult().build(); + expect(person.age).toBe(18); + }); + + it("is immutable - setters fork independent builders", () => { + const base = PersonBuilder.create().name("John").age(30); + const a = base.email("a@x.io").build(); + const b = base.email("b@x.io").build(); + + expect(a.email).toBe("a@x.io"); + expect(b.email).toBe("b@x.io"); + expect(base.buildPartial()).toEqual({ name: "John", age: 30 }); + }); + + it("preserves the concrete subclass through the chain and clone", () => { + const builder = PersonBuilder.create().name("John"); + expect(builder).toBeInstanceOf(PersonBuilder); + expect(builder.clone()).toBeInstanceOf(PersonBuilder); + }); +}); + +describe("CeriosClassAutoBuilder - runtime validation and options", () => { + it("validates required data properties at runtime via setRequiredFields", () => { + const builder = PersonBuilder.create().setRequiredFields(["name", "age"]).name("John"); + const unsafe = builder as unknown as { buildWithoutCompileTimeValidation: () => Person }; + + expect(() => unsafe.buildWithoutCompileTimeValidation()).toThrow( + "Missing required fields: age. Please set these fields before calling buildWithoutCompileTimeValidation.", + ); + }); + + it("runs validators added fluently", () => { + const builder = PersonBuilder.create() + .name("John") + .age(16) + .addValidator((p) => (p.age !== undefined && p.age >= 18 ? true : "Age must be 18 or older")); + + expect(() => builder.build()).toThrow("Validation failed: Age must be 18 or older"); + }); + + it("seeds from an existing instance and is immediately buildable", () => { + const existing = new Person({ name: "John", age: 30, email: "j@x.io" }); + const person = PersonBuilder.from(existing).age(31).build(); + + expect(person).toBeInstanceOf(Person); + expect(person.greet()).toBe("Hello, my name is John"); + expect(person.age).toBe(31); + expect(existing.age).toBe(30); + }); +}); + +describe("CeriosClassAutoBuilder - build variants", () => { + it("freezes and seals built instances", () => { + const frozen = PersonBuilder.create().name("John").age(30).buildFrozen(); + expect(Object.isFrozen(frozen)).toBe(true); + + const sealed = PersonBuilder.create().name("John").age(30).buildSealed(); + expect(Object.isSealed(sealed)).toBe(true); + }); + + it("deep-freezes nested objects", () => { + const person = PersonBuilder.create() + .name("John") + .age(30) + .address({ street: "Main St", city: "Springfield" }) + .buildDeepFrozen(); + + expect(Object.isFrozen(person)).toBe(true); + expect(Object.isFrozen(person.address)).toBe(true); + }); + + it("appends to array properties with the array setter", () => { + // hobbies is an array property; the auto setter replaces it (arrays set wholesale). + const person = PersonBuilder.create().name("John").age(30).hobbies(["chess", "running"]).build(); + expect(person.hobbies).toEqual(["chess", "running"]); + }); +}); diff --git a/tests/cerios-class-builder/cerios-class-builder-build-gate.test.ts b/tests/cerios-class-builder/cerios-class-builder-build-gate.test.ts new file mode 100644 index 0000000..8e26e2c --- /dev/null +++ b/tests/cerios-class-builder/cerios-class-builder-build-gate.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { CeriosClassBuilder, ClassBuilderStep, ClassConstructor, ClassPath } from "../../src/cerios-class-builder"; + +class Settings { + theme?: string; + locale?: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + describe(): string { + return `${this.theme ?? "default"}/${this.locale ?? "default"}`; + } +} + +class SettingsBuilder extends CeriosClassBuilder { + // The internal 4-argument shape must be forwarded, or copy-on-write methods like + // setRequiredFields() silently lose their state - the exact trap the auto builders fixed. + constructor( + classConstructor: ClassConstructor = Settings, + data: Partial = {}, + validators?: Array<(obj: Partial) => boolean | string>, + requiredFields?: ReadonlyArray> | Set, + ) { + super(classConstructor, data, validators, requiredFields); + } + + static create(): SettingsBuilder { + return new SettingsBuilder(); + } + + theme(value: string): ClassBuilderStep { + return this.setProperty("theme", value); + } +} + +class Profile { + name!: string; + bio?: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } +} + +class ProfileBuilder extends CeriosClassBuilder { + constructor(classConstructor: ClassConstructor = Profile, data: Partial = {}) { + super(classConstructor, data); + } + + static create(): ProfileBuilder { + return new ProfileBuilder(); + } + + name(value: string): ClassBuilderStep { + return this.setProperty("name", value); + } + + bio(value: string): ClassBuilderStep { + return this.setProperty("bio", value); + } +} + +// The build gate dissolves when the class has no required data properties: an all-optional +// class builds without a single setter call. Methods never count as required. +describe("CeriosClassBuilder - build gate on all-optional classes", () => { + it("builds an all-optional class without any setter call", () => { + const settings = SettingsBuilder.create().build(); + + expectTypeOf(settings).toEqualTypeOf(); + expect(settings).toBeInstanceOf(Settings); + expect(settings.describe()).toBe("default/default"); + }); + + it("keeps every compile-time-validated variant available on a fresh builder", () => { + expect(SettingsBuilder.create().buildWithoutRuntimeValidation()).toBeInstanceOf(Settings); + expect(Object.isFrozen(SettingsBuilder.create().buildFrozen())).toBe(true); + expect(SettingsBuilder.create().buildDeepFrozen()).toBeInstanceOf(Settings); + expect(SettingsBuilder.create().buildSealed()).toBeInstanceOf(Settings); + expect(SettingsBuilder.create().buildDeepSealed()).toBeInstanceOf(Settings); + }); + + it("still builds after setting and removing optional properties", () => { + expect(SettingsBuilder.create().theme("dark").build().theme).toBe("dark"); + expect(SettingsBuilder.create().theme("dark").removeOptionalProperty("theme").build().theme).toBeUndefined(); + }); + + it("keeps the strict gate for a class with required data properties", () => { + // @ts-expect-error - name is required and not set + ProfileBuilder.create().build(); + // @ts-expect-error - setting only an optional property does not satisfy the gate + ProfileBuilder.create().bio("hi").build(); + + expect(ProfileBuilder.create().name("n").build()).toBeInstanceOf(Profile); + }); + + it("still runs runtime validation when configured", () => { + const builder = SettingsBuilder.create().setRequiredFields(["theme"]); + + expect(() => builder.build()).toThrow("Missing required fields: theme"); + expect(builder.theme("dark").build().theme).toBe("dark"); + }); + + it("reports a required field whose key exists but whose value is null or undefined", () => { + const builder = SettingsBuilder.create().setRequiredFields(["theme"]); + + expect(() => builder.theme(null as unknown as string).build()).toThrow("Missing required fields: theme"); + expect(() => builder.theme(undefined as unknown as string).build()).toThrow("Missing required fields: theme"); + }); +}); diff --git a/tests/cerios-class-builder/cerios-class-builder-data-properties-methods.test.ts b/tests/cerios-class-builder/cerios-class-builder-data-properties-methods.test.ts new file mode 100644 index 0000000..cd126fc --- /dev/null +++ b/tests/cerios-class-builder/cerios-class-builder-data-properties-methods.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { CeriosClassBuilder, ClassBuilderStep } from "../../src/cerios-class-builder"; + +// Regression: DataPropertiesOnly must strip methods that take parameters, not only +// zero-arg methods. Before the `(...args: never[]) => unknown` fix, a parameterized +// method survived the filter and made build() impossible to satisfy. +class Account { + id!: string; + balance!: number; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + deposit(amount: number): void { + this.balance += amount; + } + + describe(prefix: string, suffix: string): string { + return `${prefix}${this.id}${suffix}`; + } +} + +class AccountBuilder extends CeriosClassBuilder { + static create(): AccountBuilder { + return new AccountBuilder(Account); + } + + id(value: string): ClassBuilderStep { + return this.setProperty("id", value); + } + + balance(value: number): ClassBuilderStep { + return this.setProperty("balance", value); + } +} + +describe("CeriosClassBuilder - parameterized methods are excluded from data properties", () => { + it("allows build() with only the data properties set", () => { + const account = AccountBuilder.create().id("a-1").balance(100).build(); + + expect(account).toBeInstanceOf(Account); + expect(account.id).toBe("a-1"); + expect(account.balance).toBe(100); + expectTypeOf(account).toEqualTypeOf(); + + account.deposit(50); + expect(account.balance).toBe(150); + expect(account.describe("[", "]")).toBe("[a-1]"); + }); +}); diff --git a/tests/index/index-exports.test.ts b/tests/index/index-exports.test.ts new file mode 100644 index 0000000..f83744d --- /dev/null +++ b/tests/index/index-exports.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import * as api from "../../src/index"; +import type { + AutoBuilderApi, + BuilderInit, + CeriosClassBrand, + ClassAutoBuilderApi, + DataPropertiesOnly, + InternalBuilderBrand, + InternalClassBrand, + Path, + RequiredFieldsRecord, + RequiredKeys, +} from "../../src/index"; + +type User = { id: string; name: string; age?: number }; + +class Person { + name!: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + greet(): string { + return `Hi, ${this.name}`; + } +} + +// The barrel is the package's only entry point. A type used in a public signature but +// missing here cannot be named by consumers, even though it appears in their editor. +describe("public export surface", () => { + it("exports both builder factories and both auto-builder factories at runtime", () => { + // Only these four are values; everything else in the barrel is a type. (Under + // transpile-only test runs the type names still appear as undefined keys, so this + // checks the bindings rather than the key list.) + expect(api.CeriosBuilder).toBeTypeOf("function"); + expect(api.CeriosClassBuilder).toBeTypeOf("function"); + expect(api.CeriosAutoBuilder).toBeTypeOf("function"); + expect(api.CeriosClassAutoBuilder).toBeTypeOf("function"); + }); + + it("exports the brands used in every gated build signature", () => { + // These appear in `from`'s return type and in each `this:` parameter, so consumers + // need them to write helper functions that return an already-complete builder. + expectTypeOf>().toEqualTypeOf>(); + expectTypeOf>>().toEqualTypeOf< + CeriosClassBrand> + >(); + }); + + it("exports the path types used by setRequiredFields and setNestedProperty", () => { + expectTypeOf>().toEqualTypeOf<"id" | "name" | "age">(); + expectTypeOf>().toEqualTypeOf<"name">(); + }); + + it("exports the required-fields helper types", () => { + expectTypeOf>().toEqualTypeOf<"id" | "name">(); + expectTypeOf>().toEqualTypeOf<{ id: true; name: true }>(); + }); + + it("exports the constructor options type both auto builders accept", () => { + expectTypeOf>["requiredFields"]>().toEqualTypeOf< + ReadonlyArray> | RequiredFieldsRecord | undefined + >(); + }); + + it("exports both auto-builder API interfaces", () => { + expectTypeOf["buildUnsafe"]>().toEqualTypeOf<() => User>(); + expectTypeOf["buildUnsafe"]>().toEqualTypeOf<() => Person>(); + }); + + it("exports the base-builder constraint and extension helper types", () => { + // The base view exposes NonNullable-valued setters plus buildPartial, so shared + // logic can be written once against a base type. + type BaseView = InstanceType>; + expectTypeOf[0]>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf>(); + + type ClassBaseView = InstanceType>; + expectTypeOf[0]>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf>(); + + // BuilderExtension merges the shared instance in and keeps the builder's statics. + type Extended = api.BuilderExtension, { helper(): void }>; + expectTypeOf["helper"]>().toEqualTypeOf<() => void>(); + expectTypeOf().toHaveProperty("from"); + }); + + it("exports DataPropertiesOnly, which every class-builder signature is expressed in", () => { + expectTypeOf>().toEqualTypeOf<"name">(); + }); +}); diff --git a/tsup.config.ts b/tsdown.config.mts similarity index 72% rename from tsup.config.ts rename to tsdown.config.mts index 82c43d6..0206eca 100644 --- a/tsup.config.ts +++ b/tsdown.config.mts @@ -1,10 +1,9 @@ -import { defineConfig } from "tsup"; +import { defineConfig } from "tsdown"; export default defineConfig({ entry: ["src/index.ts"], format: ["cjs", "esm"], dts: true, - splitting: false, sourcemap: true, clean: true, outDir: "dist", From 446bf56f18e280b30e5c2671c7596b9386cf19a2 Mon Sep 17 00:00:00 2001 From: Ronald Veth Date: Mon, 20 Jul 2026 09:27:21 +0200 Subject: [PATCH 2/2] The class auto builder generates setters for writable public data properties only --- .changeset/getter-only-accessor-guard.md | 19 +++ .changeset/richer-base-builder-view.md | 11 ++ MIGRATION.md | 1 + README.md | 18 ++- src/auto-builder-core.ts | 11 +- src/cerios-auto-builder.ts | 16 +- src/cerios-class-auto-builder.ts | 45 ++++-- src/cerios-class-builder.ts | 117 ++++++++++++-- src/index.ts | 9 +- src/types.ts | 20 +++ ...s-auto-builder-base-type-extension.test.ts | 84 ++++++++++ ...rios-auto-builder-proxy-edge-cases.test.ts | 23 +++ ...cerios-auto-builder-subclass-state.test.ts | 15 ++ .../cerios-auto-builder-type-safety.test.ts | 21 +++ ...erios-class-auto-builder-accessors.test.ts | 153 ++++++++++++++++++ ...-auto-builder-base-class-extension.test.ts | 54 +++++++ ...-class-auto-builder-subclass-state.test.ts | 15 ++ ...ios-class-auto-builder-type-safety.test.ts | 69 ++++++++ .../cerios-class-builder-accessors.test.ts | 67 ++++++++ 19 files changed, 732 insertions(+), 36 deletions(-) create mode 100644 .changeset/getter-only-accessor-guard.md create mode 100644 .changeset/richer-base-builder-view.md create mode 100644 tests/cerios-class-auto-builder/cerios-class-auto-builder-accessors.test.ts create mode 100644 tests/cerios-class-builder/cerios-class-builder-accessors.test.ts diff --git a/.changeset/getter-only-accessor-guard.md b/.changeset/getter-only-accessor-guard.md new file mode 100644 index 0000000..985de5d --- /dev/null +++ b/.changeset/getter-only-accessor-guard.md @@ -0,0 +1,19 @@ +--- +"@cerios/cerios-builder": patch +--- + +Fix getter-only class accessors breaking the class builders. + +A getter-only accessor (`get displayName() { ... }`) is structurally indistinguishable from a data property, so `CeriosClassAutoBuilder` generated a setter for it and the build step later ran into `TypeError: Cannot set property displayName ... which has only a getter` — a confusing failure far from the misuse. Worse, the compile-time build gate _demanded_ the accessor be set, making such classes unbuildable through `build()`. + +Now: + +- `CeriosClassAutoBuilder` generates setters for **writable public data properties only**. Methods, getter-only accessors, and `readonly` fields get no setter and don't count toward the build gate — a class owns its `readonly` fields through its own constructor, so seed them with `new Builder({ id: "1" })` or `Builder.from(instance)`. This makes setting a getter-only accessor a **compile error**, on top of the runtime guard below. Accessors with both a getter and a setter are writable and get a setter as normal; a derived class that redeclares an inherited getter-only accessor as a getter/setter pair is writable again. +- A runtime guard throws a `CeriosBuilderError` naming the accessor and the class when a getter-only accessor is written through any path that erases the `readonly` type — untyped/`any` access, `setProperty`/`setProperties`, a `setNestedProperty` root, `addToArrayProperty`, or constructor seed data — replacing the former `TypeError: Cannot set property ... which has only a getter` at build time. This guard also covers the deprecated `CeriosClassBuilder`. +- `ClassBuildGate` only demands **writable** required data properties, so classes with getter-only accessors or `readonly` fields are buildable through `build()`; enforce a genuinely required `readonly` field with runtime `requiredFields`. +- The post-construction assignment inside build skips non-writable keys instead of tripping over them. +- New `WritableKeys` helper type exported. + +Note: `CeriosAutoBuilder` (plain object types) deliberately keeps setters for `readonly` properties — an interface has no constructor, so the builder is the only construction path, and the built object stays `readonly`-typed. + +Also documents and locks with tests the previously unspecified edge cases: symbol-keyed properties are outside the builder's model (no setter, dropped by state snapshots), private/protected class members get no setters, and builder subclass fields are carried by reference across copy-on-write forks. diff --git a/.changeset/richer-base-builder-view.md b/.changeset/richer-base-builder-view.md new file mode 100644 index 0000000..21c2d22 --- /dev/null +++ b/.changeset/richer-base-builder-view.md @@ -0,0 +1,11 @@ +--- +"@cerios/cerios-builder": minor +--- + +Expose more of the build/state API to shared methods in base-builder functions. + +The `AutoBuilderBase` / `ClassAutoBuilderBase` views previously exposed only the generated setters and `buildPartial()`, so shared methods could not fork, validate, or build on `this`. The views now also expose every brand-free member whose type is covariant in the base type: `buildUnsafe()`, `buildWithoutCompileTimeValidation()`, `clone()`, and `addValidator()`. The compile-gated build variants still require the derived builder's brand and stay derived-only. + +To make this sound, `clone` and `addValidator` on the public `CommonAutoBuilderApi` are now declared in the same generic-`Self` shape the generated setters use (`clone(this: Self): Self` instead of `clone(): this`). Call sites behave identically — `Self` infers to the receiver. + +Also documented and locked with tests: a shared method that sets a base-optional property a derived type strengthens to required counts toward the derived build gate **when annotated** with `BuilderWith` / `ClassBuilderWith` — the annotation re-resolves the brand against the concrete builder's target at the call site. With an inferred return type the brand stays the base-optional flavor and does not satisfy the strengthened gate. diff --git a/MIGRATION.md b/MIGRATION.md index b041fbb..fe80663 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -262,3 +262,4 @@ The full builder API is inherited by the auto builders — no changes needed for - **Non-identifier keys** (`"content-type"`) are set with bracket access: `builder["content-type"]("application/json")`. - **All-optional types build immediately**: `build()` is available on a fresh builder when the type has no required properties (this also applies to the deprecated builders as of this release). - Setters are generated by a `Proxy`; `await builder` and `JSON.stringify(builder)` behave normally. +- **The class auto builder generates setters for writable public data properties only.** Methods, getter-only accessors, and `readonly` fields get no setter — a class owns its `readonly` fields through its own constructor, so seed them with `new Builder({ id: "1" })` or `Builder.from(instance)` rather than a setter. Setting a getter-only accessor is now a compile error; a runtime `CeriosBuilderError` naming the accessor also guards untyped access and seed data (replacing the former `TypeError: Cannot set property ... which has only a getter` at build time). This runtime guard also applies to the deprecated `CeriosClassBuilder.setProperty`. Enforce a genuinely required `readonly` field with runtime `requiredFields`. diff --git a/README.md b/README.md index f2a60ab..09b5d60 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Every property becomes a bare setter (`id`, `name`, …). Custom methods use tho ## 🧩 Building Classes: `CeriosClassAutoBuilder` -Building instantiates the real class, so methods, getters, and decorators are preserved. Class methods are automatically excluded from the setters and from the required-property check: +Building instantiates the real class, so methods, getters, and decorators are preserved. Setters are generated for **writable** data properties only: methods, getter-only accessors, and `readonly` fields get no setter and don't count toward the required-property check — a class's `readonly` fields are seeded through its own constructor (see [Limitations](#️-limitations)): ```typescript import { CeriosClassAutoBuilder } from "@cerios/cerios-builder"; @@ -133,6 +133,10 @@ class AddressBuilder extends CeriosAutoBuilder
() { **Instance methods must use `this`, not the class name.** `BuilderWith` keeps everything set earlier in the chain; naming the concrete class would discard it and `build()` would stop compiling. Returning a plain `AddressBuilder` has the same problem — always return the setter result. +### Builder fields + +Fields you add to a builder subclass (a `Map` cache, a counter) survive every setter call — but they are carried **by reference**: each setter returns a copy-on-write fork, and all forks and clones share the same field instances. Only the target data is deep-cloned. Keep anything fork-specific in the target data itself; builder fields are for genuinely shared state such as memoized lookups. + ### Method names A custom method **cannot reuse a property's exact name** — the method would shadow the generated setter it needs to call. Give it a distinct name and delegate: @@ -204,7 +208,7 @@ class CreatePostRequestBuilder extends BasePostRequestBuilder(CeriosAutoBuilder< } ``` -Inside the function, `this` exposes a setter for every property of the base type plus `buildPartial()`, and each derived builder keeps its own full API: all setters, per-builder required fields, `clone()`, and the static `from()`. Setter calls in shared methods brand the build gate like any other setter, so `create().addTag("x")` counts toward compile-time validation — and if your lint requires explicit return types, `BuilderWith` (or `ClassBuilderWith<>`) works on shared methods exactly as in [Custom Methods](#-custom-methods). The `BuilderExtension` return type is what keeps the derived setters and statics visible on the result — don't leave it off. +Inside the function, `this` exposes a setter for every property of the base type plus the brand-free part of the build/state API: `buildPartial()`, `buildUnsafe()`, `buildWithoutCompileTimeValidation()`, `clone()`, and `addValidator()` — so shared methods can fork, validate, and build. (The compile-gated variants like `build()` need the derived builder's brand and stay derived-only.) Each derived builder keeps its own full API: all setters, per-builder required fields, and the static `from()`. Setter calls in shared methods brand the build gate like any other setter, so `create().addTag("x")` counts toward compile-time validation — and if your lint requires explicit return types, `BuilderWith` (or `ClassBuilderWith<>`) works on shared methods exactly as in [Custom Methods](#-custom-methods). The `BuilderExtension` return type is what keeps the derived setters and statics visible on the result — don't leave it off. For class hierarchies the shape is identical; the base class may be abstract, because it is only used as a type: @@ -229,7 +233,7 @@ class OrderBuilder extends BaseEntityBuilder(CeriosClassAutoBuilder(Order)) {} class CustomerBuilder extends BaseEntityBuilder(CeriosClassAutoBuilder(Customer)) {} ``` -One nuance: when a derived type **strengthens** an optional base property to required (like `title` above), a shared method that sets it brands the _base_ type's optional flavor, which doesn't count toward the derived build gate. Set strengthened properties through the derived builder's own setter; shared methods are at their best on properties whose optionality doesn't change. +One nuance: when a derived type **strengthens** an optional base property to required (like `title` above), a shared method whose return type is _inferred_ brands the _base_ type's optional flavor, which doesn't count toward the derived build gate. Annotating the shared method with `BuilderWith` (or `ClassBuilderWith<>`) fixes this: `BuilderWith` re-resolves the brand against the concrete builder's own target at the call site, so the same call satisfies the strengthened gate. Prefer the annotation on any shared method that sets a property a derived type might strengthen. ## ✅ Required Fields and Validators @@ -506,6 +510,10 @@ test("free products skip payment", () => { - Class data properties that are function-typed (e.g. `onChange: (x: number) => void`) are treated like methods and get no setter — there's no reliable way to distinguish them from methods at the type level. - A custom method cannot have the same name as a property; use a distinct name that delegates to the generated setter (see [Method names](#method-names)). - Inside a **generic** base builder (e.g. `class Base extends ...`), the compile-time gate cannot resolve until `T` is concrete — the same rule as for hand-written builders. To share logic across derived types, use a [base-builder function](#sharing-custom-methods-the-base-builder-function) instead. +- **`readonly` object properties keep their setters, `readonly` class fields do not.** For a plain object type there is no constructor — the builder _is_ the construction (the same reason an object literal may initialize a `readonly id`), so `CeriosAutoBuilder` generates a working setter and the built object stays `readonly`-typed. A **class** owns its `readonly` fields through its own constructor, so `CeriosClassAutoBuilder` generates no setter for them and the build gate doesn't demand them — seed them with `new Builder({ id: "1" })` or `Builder.from(instance)` instead, or enforce a genuinely required one with runtime `requiredFields`. +- **Getter-only class accessors** (`get displayName() { ... }`) are computed by the class. Because a getter without a setter is `readonly` at the type level, no setter is generated — the misuse is a **compile error**. A runtime guard also throws a clear `CeriosBuilderError` naming the accessor, for untyped access (types are erased at runtime, so the proxy cannot see `readonly`) and for seed data. Accessors with both a getter and a setter are writable and get a setter as normal. +- **Symbol-keyed properties** are outside the builder's model: setters are generated for string keys only, and state snapshots deep-clone via string keys, so a symbol-keyed value doesn't survive a build. If a required property is symbol-keyed, the compile gate cannot be satisfied — seed via `from()` or use `buildUnsafe()`. +- **Builder subclass fields are carried by reference** across the copy-on-write forks every setter creates: a `Map` or array field on your builder class is shared between all forks and clones. Only the target data is deep-cloned — keep per-fork state in the target data, not in builder fields. ## 🗑️ Deprecated: `CeriosBuilder` and `CeriosClassBuilder` @@ -556,10 +564,10 @@ type BuilderInit = { - `BuilderInit` — the named `{ requiredFields?, validators? }` constructor-options type both auto builders accept. - `AutoBuilderConstructor` / `ClassAutoBuilderConstructor` — the abstract constructor type a factory returns; name it when storing or passing a factory result. - `AutoBuilderApi` / `ClassAutoBuilderApi` — the build/validate/clone instance API every auto builder exposes, without the generated setters. -- `AutoSetters` / `ClassAutoSetters` — the generated bare-name setters (one per property; the class variant covers data properties only). +- `AutoSetters` / `ClassAutoSetters` — the generated bare-name setters (one per property; the class variant covers **writable** data properties only — methods, getter-only accessors, and `readonly` fields are excluded). - `DataPropertiesOnly` — a class type with its methods stripped; the shape the class builders track and build from. - `ClassConstructor` — the constructor signature `CeriosClassBuilder` requires of a target class. -- `RequiredFieldsRecord`, `RequiredFieldsTemplate`, `Path`, `ClassPath`, `DeepReadonly`, `OptionalKeys`, `RequiredKeys`. +- `RequiredFieldsRecord`, `RequiredFieldsTemplate`, `Path`, `ClassPath`, `DeepReadonly`, `OptionalKeys`, `RequiredKeys`, `WritableKeys`. - `BuilderStep` / `ClassBuilderStep`, `BuilderPreset` / `ClassBuilderPreset`, `BuilderComposer` / `ClassBuilderComposer` — the longer three-argument forms `BuilderWith` supersedes; still exported and supported. ## 🤝 Contributing diff --git a/src/auto-builder-core.ts b/src/auto-builder-core.ts index ddcc43d..5ae03e2 100644 --- a/src/auto-builder-core.ts +++ b/src/auto-builder-core.ts @@ -482,13 +482,20 @@ export interface CommonAutoBuilderApi(this: Self): Self; /** * Adds a custom validator that runs during build. + * + * Generic-`Self` shaped for the same reason as {@link clone}. */ - addValidator(validator: (obj: Partial) => boolean | string): this; + addValidator(this: Self, validator: (obj: Partial) => boolean | string): Self; /** * Sets the runtime-validated required field paths. diff --git a/src/cerios-auto-builder.ts b/src/cerios-auto-builder.ts index af17696..16639de 100644 --- a/src/cerios-auto-builder.ts +++ b/src/cerios-auto-builder.ts @@ -89,9 +89,12 @@ export type AutoBuilderConstructor = (abstract new ( * method that sets such a strengthened property brands it as the *base* type's optional * flavor, which does not count toward the derived build gate - set strengthened * properties through the derived builder's own setter instead. - * - Of the build/state API only `buildPartial` is visible; the other members either gate - * on a brand or return `this`, both of which make the full {@link AutoBuilderApi} - * contravariant and would reject every derived builder. + * - Of the build/state API only the brand-free members whose types are covariant in TBase + * are visible: `buildPartial`, `buildUnsafe`, `buildWithoutCompileTimeValidation`, and + * `clone`/`addValidator` (in the same generic-`Self` shape the setters use, so they keep + * returning the concrete builder type). The validated build variants gate on a brand the + * base view cannot know, and members like `setRequiredFields` take types contravariant in + * TBase; either would reject every derived builder. * * @example * ```typescript @@ -123,7 +126,12 @@ export type AutoBuilderBase = abstract new ( value: NonNullable, ) => BuilderStep; } & BuilderTargetMarker & - Pick, "buildPartial">; + Pick, "buildPartial" | "buildUnsafe" | "buildWithoutCompileTimeValidation"> & { + /** Creates an independent copy of the builder with deep-cloned state. */ + clone(this: Self): Self; + /** Adds a custom validator that runs during build. */ + addValidator(this: Self, validator: (obj: Partial) => boolean | string): Self; + }; /** * Concrete runtime backing class for {@link CeriosAutoBuilder}. Extends the real diff --git a/src/cerios-class-auto-builder.ts b/src/cerios-class-auto-builder.ts index 1c0add0..6de6ec9 100644 --- a/src/cerios-class-auto-builder.ts +++ b/src/cerios-class-auto-builder.ts @@ -18,11 +18,18 @@ import { DataPropertiesOnly, InternalClassBrand, } from "./cerios-class-builder"; -import { BuilderTargetMarker } from "./types"; +import { BuilderTargetMarker, WritableKeys } from "./types"; /** * Automatic setter methods for class data properties: one bare `` - * method per data property of T (methods are excluded automatically). + * method per **writable** data property of T (methods are excluded automatically, and so + * are `readonly` properties - which is also what excludes getter-only accessors, since a + * getter without a setter is indistinguishable from a `readonly` field at the type level). + * + * A class's `readonly` fields are its own to establish, through its constructor - seed the + * builder with `new Builder({ id: "1" })` or `Builder.from(instance)` rather than a setter. + * External code should only ever write through a public setter (a writable field, or a + * getter/setter accessor pair). * * Property names that collide with a builder method (e.g. `build`) get a `Prop` * suffix; keys that aren't valid identifiers are set with bracket access. @@ -31,7 +38,7 @@ import { BuilderTargetMarker } from "./types"; * @template T - The class type being built */ export type ClassAutoSetters = { - [K in keyof DataPropertiesOnly & string as SetterName]: ( + [K in WritableKeys> & string as SetterName]: ( this: Self, value: DataPropertiesOnly[K], ) => ClassBuilderStep; @@ -79,8 +86,8 @@ export type ClassAutoBuilderConstructor = (abstract new ( * generic over every subclass of TBase cannot be written directly. Instead, write shared * logic once in a function constrained on this type and apply it on top of each concrete * class auto builder. Inside the function, `this` carries concrete setters for every - * inherited data property, so shared methods can call them; the derived builder still - * gets its full setter set from its own `CeriosClassAutoBuilder(Derived)` call. + * inherited *writable* data property, so shared methods can call them; the derived builder + * still gets its full setter set from its own `CeriosClassAutoBuilder(Derived)` call. * * TBase is used purely as a type and may be an abstract class - only the concrete derived * classes are ever passed to {@link CeriosClassAutoBuilder}. @@ -90,9 +97,12 @@ export type ClassAutoBuilderConstructor = (abstract new ( * property to required. A shared method that sets such a strengthened property brands * it as the *base* class's optional flavor, which does not count toward the derived * build gate - set strengthened properties through the derived builder's own setter. - * - Of the build/state API only `buildPartial` is visible; the other members either gate - * on a brand or return `this`, both of which make the full {@link ClassAutoBuilderApi} - * contravariant and would reject every derived builder. + * - Of the build/state API only the brand-free members whose types are covariant in TBase + * are visible: `buildPartial`, `buildUnsafe`, `buildWithoutCompileTimeValidation`, and + * `clone`/`addValidator` (in the same generic-`Self` shape the setters use, so they keep + * returning the concrete builder type). The validated build variants gate on a brand the + * base view cannot know, and members like `setRequiredFields` take types contravariant in + * TBase; either would reject every derived builder. * * @example * ```typescript @@ -117,12 +127,17 @@ export type ClassAutoBuilderBase = abstract new ( // oxlint-disable-next-line typescript/no-explicit-any -- the canonical mixin constraint; `any[]` keeps every concrete auto-builder constructor assignable ...args: any[] ) => { - [K in keyof DataPropertiesOnly & string as SetterName]: ( + [K in WritableKeys> & string as SetterName]: ( this: Self, value: NonNullable[K]>, ) => ClassBuilderStep; } & BuilderTargetMarker & - Pick, "buildPartial">; + Pick, "buildPartial" | "buildUnsafe" | "buildWithoutCompileTimeValidation"> & { + /** Creates an independent copy of the builder with deep-cloned state. */ + clone(this: Self): Self; + /** Adds a custom validator that runs during build. */ + addValidator(this: Self, validator: (obj: Partial) => boolean | string): Self; + }; /** * One runtime class per target class, rather than per factory call. @@ -138,10 +153,12 @@ export type ClassAutoBuilderBase = abstract new ( const RUNTIME_CACHE = new WeakMap>(); /** - * Creates a base class with automatic bare-name setters for every data property - * of the class T. Building instantiates the real class, so methods, getters, and - * decorators are preserved. Extend the returned class and add ordinary methods - * for any custom logic. + * Creates a base class with automatic bare-name setters for every **writable** data + * property of the class T. Building instantiates the real class, so methods, getters, and + * decorators are preserved. Methods, getter-only accessors, and `readonly` fields get no + * setter - a class's `readonly` fields are established through its constructor, so seed them + * with `new Builder({ ... })` or `Builder.from(instance)`. Extend the returned class and add + * ordinary methods for any custom logic. * * @example * ```typescript diff --git a/src/cerios-class-builder.ts b/src/cerios-class-builder.ts index c0dbbc6..d6ebc99 100644 --- a/src/cerios-class-builder.ts +++ b/src/cerios-class-builder.ts @@ -8,7 +8,14 @@ import { toRequiredPaths, } from "./auto-builder-core"; import { CeriosBuilderError, runValidatorsAgainst } from "./builder-error"; -import type { BuilderTargetMarker, DeepReadonly, RequiredFieldsRecord, RequiredKeys, TargetOfMarker } from "./types"; +import type { + BuilderTargetMarker, + DeepReadonly, + RequiredFieldsRecord, + RequiredKeys, + TargetOfMarker, + WritableKeys, +} from "./types"; /** * Type representing a class constructor that can be instantiated with optional partial data. @@ -16,6 +23,67 @@ import type { BuilderTargetMarker, DeepReadonly, RequiredFieldsRecord, RequiredK */ export type ClassConstructor = new (data?: Partial) => T; +/** + * Getter-only accessor names per target class, computed once per class. + * @internal + */ +const GETTER_ONLY_KEYS_CACHE = new WeakMap>(); + +/** + * Collects the names on the class's prototype chain that resolve to an accessor with a + * getter but no setter. The nearest descriptor wins, so a derived class that redeclares an + * inherited getter-only accessor as a getter/setter pair makes the name writable again. + * + * `DataPropertiesOnly` cannot exclude these at the type level - a getter is structurally + * indistinguishable from a data property - so they must be caught at runtime instead. + * + * @internal + */ +function getterOnlyKeys(ctor: ClassConstructor): ReadonlySet { + const cached = GETTER_ONLY_KEYS_CACHE.get(ctor); + if (cached) { + return cached; + } + const keys = new Set(); + const seen = new Set(); + let proto: object | null = ctor.prototype as object | null; + while (proto && proto !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(proto)) { + if (seen.has(name)) { + continue; + } + seen.add(name); + const descriptor = Object.getOwnPropertyDescriptor(proto, name); + if (descriptor?.get && !descriptor.set) { + keys.add(name); + } + } + proto = Object.getPrototypeOf(proto) as object | null; + } + GETTER_ONLY_KEYS_CACHE.set(ctor, keys); + return keys; +} + +/** + * Rejects writes to a getter-only accessor of the target class. + * + * Without this, the value sat in the builder state until build, where assigning it threw + * `TypeError: Cannot set property ... which has only a getter` - far from the misuse and + * without naming the builder as the culprit. + * + * @throws {CeriosBuilderError} If the key is a getter-only accessor on the target class + * @internal + */ +function assertNotGetterOnly(ctor: ClassConstructor, key: PropertyKey): void { + if (typeof key === "string" && getterOnlyKeys(ctor).has(key)) { + throw new CeriosBuilderError( + `"${key}" is a getter-only accessor on ${ctor.name}; its value is computed by the class and cannot be set through the builder.`, + [], + [], + ); + } +} + /** * Helper to extract only data properties (exclude methods) from a class type. * This allows compile-time validation to work properly with classes. @@ -43,19 +111,31 @@ export type InternalClassBrand = { */ export type CeriosClassBrand = InternalClassBrand; +/** + * The writable data properties of T: what the gate may demand and what a builder can + * actually assign. Getter-only accessors surface as `readonly` properties, so this view is + * what keeps them out of the build gate — a gate demanding a computed property would make + * the class unbuildable through the validated variants. + * @internal + */ +type WritableDataProperties = Pick, WritableKeys>>; + /** * The `this` constraint gating the compile-time-validated build variants. * - * Normally the accumulated {@link InternalClassBrand} must cover every required data - * property of T. When T has no required data properties there is nothing to track, so the - * gate dissolves to `unknown` and `build()` is callable on a fresh builder — an all-optional - * class no longer needs a throwaway setter call before it can build. + * Normally the accumulated {@link InternalClassBrand} must cover every required *writable* + * data property of T. Readonly-typed properties are excluded: a getter-only accessor is + * indistinguishable from a `readonly` field at the type level, and demanding a computed + * property would make the class unbuildable (setting it throws at runtime). When T has no + * required writable data properties there is nothing to track, so the gate dissolves to + * `unknown` and `build()` is callable on a fresh builder — an all-optional class no longer + * needs a throwaway setter call before it can build. * * @template T - The class type being built */ -export type ClassBuildGate = [RequiredKeys>] extends [never] +export type ClassBuildGate = [RequiredKeys>] extends [never] ? unknown - : InternalClassBrand>; + : InternalClassBrand>; type RootFromPath

= P extends `${infer K}.${string}` ? K : P; @@ -311,6 +391,10 @@ export class CeriosClassBuilder { _validators?: Array<(obj: Partial) => boolean | string>, _requiredFields?: ReadonlyArray> | Set, ) { + // Seed data bypasses `setProperty`, so it needs the same getter-only guard here. + for (const key of Object.keys(data)) { + assertNotGetterOnly(classConstructor, key); + } this._classConstructor = classConstructor; this._actual = data; if (_validators) { @@ -385,10 +469,16 @@ export class CeriosClassBuilder { const data = deepClone(this._actual); const instance: T = new ctor(data); - const dataKeys = Object.keys(data) as (keyof T)[]; + // The write-path guards keep getter-only accessor keys out of the state; filtering + // here as well keeps [[Set]] from ever reaching a get-only accessor (which throws a + // bare TypeError) should a future write path miss the guard. + const getterOnly = getterOnlyKeys(ctor); + const dataKeys = Object.keys(data).filter((key) => !getterOnly.has(key)) as (keyof T)[]; const needsAssign = dataKeys.some((key) => instance[key] === undefined && data[key] !== undefined); if (needsAssign) { - Object.assign(instance, data); + for (const key of dataKeys) { + (instance as Partial)[key] = data[key]; + } } return instance; @@ -447,6 +537,7 @@ export class CeriosClassBuilder { ): ClassBuilderStep; protected setProperty(key: K, value: T[K]): ClassBuilderStep { assertSafeKey(key); + assertNotGetterOnly(this.getClassConstructor(), key); const newBuilder = this.createBuilder({ ...this._actual, [key]: value, @@ -464,7 +555,10 @@ export class CeriosClassBuilder { protected setProperties>( props: Pick, K>, ): ClassBuilderStep { - Object.keys(props).forEach(assertSafeKey); + for (const key of Object.keys(props)) { + assertSafeKey(key); + assertNotGetterOnly(this.getClassConstructor(), key); + } const newBuilder = this.createBuilder({ ...this._actual, ...props, @@ -505,6 +599,8 @@ export class CeriosClassBuilder { ): ClassBuilderStep { assertSafePath(path as string); const keys = (path as string).split("."); + // Only the root key lands on the target class; deeper segments live on nested values. + assertNotGetterOnly(this.getClassConstructor(), keys[0]); const newActual = deepClone(this._actual); let current = newActual as Record; @@ -708,6 +804,7 @@ export class CeriosClassBuilder { : never), >(key: K, value: V): ClassBuilderStep { assertSafeKey(key); + assertNotGetterOnly(this.getClassConstructor(), key); const currentArray = (this._actual[key as keyof T] as Array | undefined) ?? []; const newBuilder = this.createBuilder({ ...this._actual, diff --git a/src/index.ts b/src/index.ts index 79c8873..6141af6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,4 +37,11 @@ export type { DataPropertiesOnly, InternalClassBrand, } from "./cerios-class-builder.js"; -export type { BuilderType, DeepReadonly, OptionalKeys, RequiredFieldsRecord, RequiredKeys } from "./types.js"; +export type { + BuilderType, + DeepReadonly, + OptionalKeys, + RequiredFieldsRecord, + RequiredKeys, + WritableKeys, +} from "./types.js"; diff --git a/src/types.ts b/src/types.ts index 6eb6840..f953b4e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,6 +69,26 @@ export type RequiredKeys = { [K in keyof T]-?: undefined extends T[K] ? never : K; }[keyof T]; +/** + * Resolves to A when X and Y are identical types (including modifiers), otherwise B. + * The function-signature comparison is the only relation TypeScript checks invariantly, + * which is what makes it able to see the `readonly` modifier that assignability ignores. + * @internal + */ +type IfEquals = (() => V extends X ? 1 : 2) extends () => V extends Y ? 1 : 2 ? A : B; + +/** + * Helper type to extract the writable (non-`readonly`) keys of a type. + * + * A getter-only class accessor surfaces as a `readonly` property, so this is also the only + * compile-time signal that a class member cannot be assigned at runtime. + * + * @template T - The type to extract writable keys from + */ +export type WritableKeys = { + [K in keyof T]-?: IfEquals<{ [Q in K]: T[K] }, { -readonly [Q in K]: T[K] }, K, never>; +}[keyof T]; + /** * An exhaustive map of the required keys of T, used as an alternative to a hand-written * array of required-field paths. diff --git a/tests/cerios-auto-builder/cerios-auto-builder-base-type-extension.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-base-type-extension.test.ts index 10b52d8..ce67ece 100644 --- a/tests/cerios-auto-builder/cerios-auto-builder-base-type-extension.test.ts +++ b/tests/cerios-auto-builder/cerios-auto-builder-base-type-extension.test.ts @@ -43,6 +43,39 @@ function BasePostRequestBuilder { + // Annotated with BuilderWith: the brand re-resolves against the + // *derived* target at the call site, so it does count toward a strengthened gate. + return this.title("Untitled"); + } + + // The base view exposes clone(), addValidator(), buildUnsafe(), and + // buildWithoutCompileTimeValidation() alongside buildPartial(), so shared methods + // can fork, validate, and build without the derived builder's brand. + draftCopy(): this { + return this.clone(); + } + + requireTags(): this { + return this.addValidator((post) => ((post.tags?.length ?? 0) > 0 ? true : "post must have at least one tag")); + } + + preview(): BasePostRequest { + return this.buildUnsafe(); + } + + submit(): BasePostRequest { + return this.buildWithoutCompileTimeValidation(); + } } return PostRequestBuilder as BuilderExtension; } @@ -167,6 +200,57 @@ describe("CeriosAutoBuilder - base type extension", () => { }); }); + it("brands strengthened properties per the shared method's return annotation", () => { + // CreatePostRequest strengthens the base-optional title to required. A shared method + // with an *inferred* return type brands the base optional flavor, which the derived + // gate must reject - the value itself IS set, so only the compile gate objects. + const inferred = CreatePostRequestBuilder.create().postId("p").content("c").authorId("a").untitled(); + // @ts-expect-error - title's base-optional brand does not satisfy the derived gate + const request = inferred.build(); + expect(request.title).toBe("Untitled"); + + // Annotating the shared method with BuilderWith re-resolves the brand + // against the derived target at the call site, so the same call satisfies the gate. + const tracked = CreatePostRequestBuilder.create().postId("p").content("c").authorId("a").untitledTracked(); + expectTypeOf(tracked.build()).toEqualTypeOf(); + expect(tracked.build().title).toBe("Untitled"); + }); + + it("clone() through a shared method keeps the concrete builder type", () => { + // draftCopy() calls this.clone() through the base view; the result must still have + // derived-only setters and shared methods, and must be an independent fork. + const original = CreatePostRequestBuilder.create().postId("p").title("t").content("c"); + const draft = original.draftCopy().authorId("author-9").addTag("draft"); + + expect(draft.build()).toEqual({ + postId: "p", + title: "t", + content: "c", + authorId: "author-9", + tags: ["draft"], + }); + expect(original.buildPartial().tags).toBeUndefined(); + }); + + it("addValidator() through a shared method runs during build", () => { + const builder = CreatePostRequestBuilder.create().postId("p").title("t").content("c").authorId("a").requireTags(); + + expect(() => builder.build()).toThrow("post must have at least one tag"); + expect(builder.addTag("ok").build().tags).toEqual(["ok"]); + }); + + it("buildUnsafe() and buildWithoutCompileTimeValidation() are callable from shared methods", () => { + const incomplete = CreatePostRequestBuilder.create().title("t"); + + // preview() skips all validation and returns the current state. + expect(incomplete.preview()).toEqual({ title: "t" }); + // submit() skips the compile gate but still validates at runtime. + expect(() => incomplete.submit()).toThrow("Missing required fields"); + + const complete = CreatePostRequestBuilder.create().postId("p").title("t").content("c").authorId("a"); + expect(complete.submit()).toEqual({ postId: "p", title: "t", content: "c", authorId: "a" }); + }); + it("keeps the static from() available through the base-builder function", () => { const existing: CreatePostRequest = { postId: "post-600", diff --git a/tests/cerios-auto-builder/cerios-auto-builder-proxy-edge-cases.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-proxy-edge-cases.test.ts index 6200622..f46afb3 100644 --- a/tests/cerios-auto-builder/cerios-auto-builder-proxy-edge-cases.test.ts +++ b/tests/cerios-auto-builder/cerios-auto-builder-proxy-edge-cases.test.ts @@ -48,6 +48,29 @@ describe("CeriosAutoBuilder - proxy edge cases", () => { expect(Reflect.get(builder, Symbol.asyncIterator)).toBeUndefined(); }); + it("should leave symbol-keyed target properties entirely outside the builder's model", () => { + const kind = Symbol("kind"); + type Tagged = { id: string; [kind]?: string }; + + class TaggedBuilder extends CeriosAutoBuilder() { + static create(): TaggedBuilder { + return new TaggedBuilder({}); + } + } + + // AutoSetters keys on `keyof T & string`, so no setter exists for the symbol key - + // at the type level or at runtime (symbol access forwards to the builder itself). + const builder = TaggedBuilder.create().id("1"); + expect(Reflect.get(builder, kind)).toBeUndefined(); + expect(builder.build()[kind]).toBeUndefined(); + + // A symbol-keyed value in the seed data does not survive either: state snapshots + // deep-clone via string keys. Symbol-keyed properties cannot be built. + const seeded = new TaggedBuilder({ id: "2", [kind]: "x" }).buildUnsafe(); + expect(seeded.id).toBe("2"); + expect(seeded[kind]).toBeUndefined(); + }); + it("should expose real methods to the in operator", () => { const builder = SimpleBuilder.create(); expect("build" in builder).toBe(true); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-subclass-state.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-subclass-state.test.ts index f84e012..daa23a1 100644 --- a/tests/cerios-auto-builder/cerios-auto-builder-subclass-state.test.ts +++ b/tests/cerios-auto-builder/cerios-auto-builder-subclass-state.test.ts @@ -73,6 +73,21 @@ describe("CeriosAutoBuilder - subclass instance state across copy-on-write", () expect(TrackingBuilder.create().title("hello").upper).toBe("HELLO"); }); + it("carries subclass reference fields shallowly - every fork shares one instance", () => { + // Copy-on-write carries subclass fields by reference, deliberately: only the target + // data is deep-cloned. A mutable field like a Map is therefore aliased across forks; + // per-fork state belongs in the target data, not in builder fields. + const original = TrackingBuilder.create().remember("a", "1"); + const forked = original.title("t"); + + expect(forked.cache).toBe(original.cache); + forked.cache.set("b", "2"); + expect(original.cache.get("b")).toBe("2"); + + // clone() deep-clones only the target data; subclass fields stay shared too. + expect(original.clone().cache).toBe(original.cache); + }); + it("does not let a subclass field mask the builder's own members", () => { const forked = TrackingBuilder.create().title("t"); diff --git a/tests/cerios-auto-builder/cerios-auto-builder-type-safety.test.ts b/tests/cerios-auto-builder/cerios-auto-builder-type-safety.test.ts index f48a8ab..7dafd2c 100644 --- a/tests/cerios-auto-builder/cerios-auto-builder-type-safety.test.ts +++ b/tests/cerios-auto-builder/cerios-auto-builder-type-safety.test.ts @@ -48,6 +48,27 @@ describe("CeriosAutoBuilder - compile-time safety", () => { expect(complete.build()).toEqual({ id: "1", name: "n", role: "r" }); }); + it("generates working setters for readonly properties and keeps the built object readonly", () => { + // `readonly` forbids reassignment *after* construction, and the builder is the + // construction - the same reason a literal may initialize a readonly property. + // Excluding readonly properties would make immutable types unbuildable. + type ImmutableUser = { readonly id: string; name: string }; + + class ImmutableUserBuilder extends CeriosAutoBuilder() { + static create(): ImmutableUserBuilder { + return new ImmutableUserBuilder({}); + } + } + + const built = ImmutableUserBuilder.create().id("1").name("n").build(); + expect(built).toEqual({ id: "1", name: "n" }); + expectTypeOf(built).toEqualTypeOf(); + + // The built object keeps its readonly typing. + // @ts-expect-error - id is readonly once built + built.id = "2"; + }); + it("gates all compile-time-validated build variants", () => { const incomplete = UserBuilder.create().id("1"); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-accessors.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-accessors.test.ts new file mode 100644 index 0000000..3fbf489 --- /dev/null +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-accessors.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, it } from "vitest"; + +import { CeriosBuilderError } from "../../src/builder-error"; +import { CeriosClassAutoBuilder } from "../../src/cerios-class-auto-builder"; + +// Getter-only accessor: computed from data properties, no backing storage. +class User { + firstName!: string; + lastName!: string; + + get displayName(): string { + return `${this.firstName} ${this.lastName}`; + } +} + +class UserBuilder extends CeriosClassAutoBuilder(User) { + static create(): UserBuilder { + return new UserBuilder(); + } +} + +// No constructor assignment, so build() takes the post-construction assignment path - +// the path where a getter-only key in the state used to throw a bare TypeError. +class Report { + title!: string; + + get heading(): string { + return `Report: ${this.title}`; + } +} + +// Getter/setter pair: writable through the accessor, so the builder may set it. +class Temperature { + celsius!: number; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + get fahrenheit(): number { + return this.celsius * 1.8 + 32; + } + + set fahrenheit(value: number) { + this.celsius = (value - 32) / 1.8; + } +} + +describe("CeriosClassAutoBuilder - getter-only accessors", () => { + it("generates no setter for a getter-only accessor and guards untyped access at runtime", () => { + // A getter-only accessor is `readonly` at the type level, so no setter is generated - + // the compiler catches the misuse first. The runtime guard remains for untyped access + // (types are erased at runtime, so the proxy cannot see `readonly`). + expect(() => + // @ts-expect-error - displayName is a getter-only accessor, no setter exists + UserBuilder.create().displayName("Jane Doe"), + ).toThrow(CeriosBuilderError); + expect(() => + // @ts-expect-error - displayName is a getter-only accessor, no setter exists + UserBuilder.create().displayName("Jane Doe"), + ).toThrow(/"displayName" is a getter-only accessor on User/); + }); + + it("throws the same error when seed data contains a getter-only key", () => { + expect(() => new UserBuilder({ displayName: "Jane Doe" })).toThrow(CeriosBuilderError); + expect(() => new UserBuilder({ displayName: "Jane Doe" })).toThrow(/getter-only accessor/); + }); + + it("builds normally when the getter-only accessor is never set", () => { + const user = UserBuilder.create().firstName("Jane").lastName("Doe").build(); + + expect(user).toBeInstanceOf(User); + expect(user.displayName).toBe("Jane Doe"); + }); + + it("builds through the post-construction assignment path without touching the accessor", () => { + class ReportBuilder extends CeriosClassAutoBuilder(Report) { + static create(): ReportBuilder { + return new ReportBuilder(); + } + } + + const report = ReportBuilder.create().title("Q3").build(); + + expect(report).toBeInstanceOf(Report); + expect(report.title).toBe("Q3"); + expect(report.heading).toBe("Report: Q3"); + }); + + it("allows setting a getter/setter accessor pair and routes the value through the setter", () => { + class TemperatureBuilder extends CeriosClassAutoBuilder(Temperature) { + static create(): TemperatureBuilder { + return new TemperatureBuilder(); + } + } + + const temperature = TemperatureBuilder.create().celsius(0).fahrenheit(212).build(); + + expect(temperature.celsius).toBe(100); + expect(temperature.fahrenheit).toBe(212); + }); + + it("rejects a getter-only accessor inherited from a base class", () => { + class Admin extends User { + level!: number; + } + + class AdminBuilder extends CeriosClassAutoBuilder(Admin) { + static create(): AdminBuilder { + return new AdminBuilder(); + } + } + + expect(() => + // @ts-expect-error - displayName is an inherited getter-only accessor, no setter exists + AdminBuilder.create().displayName("Root"), + ).toThrow(/getter-only accessor on Admin/); + }); + + it("allows a derived class that redeclares an inherited getter-only accessor with a setter", () => { + class NamedUser extends User { + private _override?: string; + + override get displayName(): string { + return this._override ?? `${this.firstName} ${this.lastName}`; + } + + override set displayName(value: string) { + this._override = value; + } + } + + class NamedUserBuilder extends CeriosClassAutoBuilder(NamedUser) { + static create(): NamedUserBuilder { + return new NamedUserBuilder(); + } + } + + const user = NamedUserBuilder.create().firstName("Jane").lastName("Doe").displayName("JD").build(); + + expect(user.displayName).toBe("JD"); + }); + + it("keeps rejecting getter-only accessors on copy-on-write forks", () => { + const forked = UserBuilder.create().firstName("Jane").clone().lastName("Doe"); + + // @ts-expect-error - displayName is a getter-only accessor, no setter exists + expect(() => forked.displayName("Jane Doe")).toThrow(/getter-only accessor/); + expect(forked.build().displayName).toBe("Jane Doe"); + }); +}); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-base-class-extension.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-base-class-extension.test.ts index 0d32972..bb136dc 100644 --- a/tests/cerios-class-auto-builder/cerios-class-auto-builder-base-class-extension.test.ts +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-base-class-extension.test.ts @@ -51,6 +51,25 @@ function BaseEntityBuilder>(Bu createdOn(year: number): ClassBuilderWith { return this.createdAt(new Date(Date.UTC(year, 0, 1))); } + + // The base view exposes clone(), addValidator(), buildUnsafe(), and + // buildWithoutCompileTimeValidation() alongside buildPartial(), so shared methods + // can fork, validate, and build without the derived builder's brand. + draftCopy(): this { + return this.clone(); + } + + requireCreatedAt(): this { + return this.addValidator((entity) => (entity.createdAt ? true : "entity must have a creation date")); + } + + preview(): BaseEntity { + return this.buildUnsafe(); + } + + submit(): BaseEntity { + return this.buildWithoutCompileTimeValidation(); + } } return EntityBuilder as BuilderExtension; } @@ -153,6 +172,41 @@ describe("CeriosClassAutoBuilder - base class extension", () => { expect(order.createdAt).toEqual(new Date(Date.UTC(2025, 0, 1))); }); + it("clone() through a shared method keeps the concrete builder type", () => { + // draftCopy() calls this.clone() through the base view; the result must still have + // derived-only setters and shared methods, and must be an independent fork. + const original = OrderBuilder.create().sequentialId(7).total(70); + const draft = original.draftCopy().reference("draft").total(77); + + const order = draft.build(); + expect(order).toBeInstanceOf(Order); + expect(order.total).toBe(77); + expect(order.reference).toBe("draft"); + expect(original.buildPartial().total).toBe(70); + expect(original.buildPartial().reference).toBeUndefined(); + }); + + it("addValidator() through a shared method runs during build", () => { + const builder = OrderBuilder.create().sequentialId(8).total(80).requireCreatedAt(); + + expect(() => builder.build()).toThrow("entity must have a creation date"); + expect(builder.createdOn(2026).build().createdAt).toEqual(new Date(Date.UTC(2026, 0, 1))); + }); + + it("buildUnsafe() and buildWithoutCompileTimeValidation() are callable from shared methods", () => { + const incomplete = OrderBuilder.create().total(9); + + // preview() skips all validation and returns a real (partial) instance. + const previewed = incomplete.preview(); + expect(previewed).toBeInstanceOf(Order); + expect((previewed as Order).total).toBe(9); + // submit() skips the compile gate but still validates at runtime. + expect(() => incomplete.submit()).toThrow("Missing required fields"); + + const submitted = OrderBuilder.create().sequentialId(9).total(90).submit(); + expect(submitted).toBeInstanceOf(Order); + }); + it("keeps the static from() available through the base-builder function", () => { const existing = new Order({ id: "order-5", total: 75, reference: "ref-2" }); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-subclass-state.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-subclass-state.test.ts index 887d471..e477987 100644 --- a/tests/cerios-class-auto-builder/cerios-class-auto-builder-subclass-state.test.ts +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-subclass-state.test.ts @@ -56,6 +56,21 @@ describe("CeriosClassAutoBuilder - subclass instance state across copy-on-write" expect(Array.isArray(configured.removeOptionalProperty("body").log)).toBe(true); }); + it("carries subclass reference fields shallowly - every fork shares one instance", () => { + // Copy-on-write carries subclass fields by reference, deliberately: only the target + // data is deep-cloned. A mutable field like an array is therefore aliased across + // forks; per-fork state belongs in the target data, not in builder fields. + const original = TrackingBuilder.create().track("created"); + const forked = original.title("t"); + + expect(forked.log).toBe(original.log); + forked.track("forked"); + expect(original.log).toEqual(["created", "forked"]); + + // clone() deep-clones only the target data; subclass fields stay shared too. + expect(original.clone().log).toBe(original.log); + }); + it("still builds a real class instance", () => { expect(TrackingBuilder.create().title("t").build()).toBeInstanceOf(Doc); }); diff --git a/tests/cerios-class-auto-builder/cerios-class-auto-builder-type-safety.test.ts b/tests/cerios-class-auto-builder/cerios-class-auto-builder-type-safety.test.ts index ffdd8c9..4073931 100644 --- a/tests/cerios-class-auto-builder/cerios-class-auto-builder-type-safety.test.ts +++ b/tests/cerios-class-auto-builder/cerios-class-auto-builder-type-safety.test.ts @@ -43,6 +43,75 @@ describe("CeriosClassAutoBuilder - compile-time safety", () => { expect(complete.build().greet()).toBe("Hi, John"); }); + it("does not generate setters for private or protected members", () => { + class Secretive { + name!: string; + private secret = "s3cret"; + protected internalCode = 7; + + revealSecret(): string { + return this.secret; + } + + revealCode(): number { + return this.internalCode; + } + } + + class SecretiveBuilder extends CeriosClassAutoBuilder(Secretive) { + static create(): SecretiveBuilder { + return new SecretiveBuilder(); + } + } + + // `keyof` only surfaces public members, so non-public properties get no setter and + // do not count toward the build gate; the class's own field initializers still run. + const builder = SecretiveBuilder.create().name("n"); + // @ts-expect-error - secret is private, no setter exists + void builder.secret; + // @ts-expect-error - internalCode is protected, no setter exists + void builder.internalCode; + + const built = builder.build(); + expect(built.revealSecret()).toBe("s3cret"); + expect(built.revealCode()).toBe(7); + }); + + it("does not generate a setter for readonly fields; they are seeded through the constructor", () => { + class Entity { + readonly id!: string; + name!: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + } + + class EntityBuilder extends CeriosClassAutoBuilder(Entity) { + static create(data?: Partial): EntityBuilder { + return new EntityBuilder(data); + } + } + + // A readonly field belongs to the class - external code must not write it, so no + // setter is generated and the gate does not demand it. It is established through the + // class's own constructor via seed data or from(). + // @ts-expect-error - id is readonly, no setter is generated + EntityBuilder.create().id("1"); + + const built = EntityBuilder.create().name("n").build(); + expect(built.name).toBe("n"); + + // Seeding the readonly field through the constructor carries it into the instance. + const seeded = EntityBuilder.create({ id: "1" }).name("n").build(); + expect(seeded.id).toBe("1"); + + const fromInstance = EntityBuilder.from(new Entity({ id: "2", name: "n" })).build(); + expect(fromInstance.id).toBe("2"); + }); + it("does not generate setters for methods (with or without parameters)", () => { // @ts-expect-error - greet is a method, not a data property PersonBuilder.create().greet(() => "x"); diff --git a/tests/cerios-class-builder/cerios-class-builder-accessors.test.ts b/tests/cerios-class-builder/cerios-class-builder-accessors.test.ts new file mode 100644 index 0000000..aebd1c4 --- /dev/null +++ b/tests/cerios-class-builder/cerios-class-builder-accessors.test.ts @@ -0,0 +1,67 @@ +// oxlint-disable typescript/no-deprecated -- exercises the deprecated manual builder until its removal +import { describe, expect, it } from "vitest"; + +import { CeriosBuilderError } from "../../src/builder-error"; +import { CeriosClassBuilder, ClassBuilderStep, ClassConstructor } from "../../src/cerios-class-builder"; + +class User { + firstName!: string; + lastName!: string; + + constructor(data?: Partial) { + if (data) { + Object.assign(this, data); + } + } + + get displayName(): string { + return `${this.firstName} ${this.lastName}`; + } +} + +class UserBuilder extends CeriosClassBuilder { + constructor( + classConstructor: ClassConstructor = User, + data: Partial = {}, + validators?: Array<(obj: Partial) => boolean | string>, + requiredFields?: Set, + ) { + super(classConstructor, data, validators, requiredFields); + } + + firstName(value: string): ClassBuilderStep { + return this.setProperty("firstName", value); + } + + lastName(value: string): ClassBuilderStep { + return this.setProperty("lastName", value); + } + + displayName(value: string): ClassBuilderStep { + return this.setProperty("displayName", value); + } +} + +describe("CeriosClassBuilder - getter-only accessors", () => { + it("throws a CeriosBuilderError when setProperty targets a getter-only accessor", () => { + expect(() => new UserBuilder().displayName("Jane Doe")).toThrow(CeriosBuilderError); + expect(() => new UserBuilder().displayName("Jane Doe")).toThrow(/"displayName" is a getter-only accessor on User/); + }); + + it("throws the same error when seed data contains a getter-only key", () => { + class SeededBuilder extends CeriosClassBuilder { + constructor(data?: Partial) { + super(User, data); + } + } + + expect(() => new SeededBuilder({ displayName: "Jane Doe" })).toThrow(/getter-only accessor/); + }); + + it("builds normally when the getter-only accessor is never set", () => { + const user = new UserBuilder().firstName("Jane").lastName("Doe").build(); + + expect(user).toBeInstanceOf(User); + expect(user.displayName).toBe("Jane Doe"); + }); +});