@cerios/cerios-builder
Advanced tools
+121
-4
@@ -110,2 +110,59 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| /** | ||
| * Structural equality over the same value shapes `deepClone` reproduces. | ||
| * | ||
| * Used by the class builder to tell a field initializer's untouched default apart from a | ||
| * value the target constructor derived from the incoming data - the two are distinguishable | ||
| * only by comparing against a no-data probe instance, and the probe's nested objects are | ||
| * never reference-equal to the built instance's. | ||
| * | ||
| * Prototypes must match, so a class instance never compares equal to a plain object with | ||
| * the same keys. `Object.is` handles `NaN` and `-0` so a `NaN` default is not mistaken for | ||
| * a constructor-derived value. The `seen` map pairs each visited left-hand object with its | ||
| * right-hand counterpart, which makes cyclic structures terminate. | ||
| * | ||
| * @param a - Left-hand value | ||
| * @param b - Right-hand value | ||
| * @param seen - Cycle-tracking map; callers do not pass this | ||
| * @internal | ||
| */ | ||
| function deepEquals(a, b, seen = /* @__PURE__ */ new WeakMap()) { | ||
| if (Object.is(a, b)) return true; | ||
| if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false; | ||
| if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false; | ||
| const pairedWith = seen.get(a); | ||
| if (pairedWith !== void 0) return pairedWith === b; | ||
| seen.set(a, b); | ||
| const builtIn = deepEqualsBuiltIn(a, b, seen); | ||
| if (builtIn !== null) return builtIn; | ||
| const keys = Object.keys(a); | ||
| if (keys.length !== Object.keys(b).length) return false; | ||
| return keys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && deepEquals(a[key], b[key], seen)); | ||
| } | ||
| /** | ||
| * The built-in half of `deepEquals`, split out to keep either function readable. | ||
| * | ||
| * Callers have already established that `a` and `b` are non-null objects sharing a | ||
| * prototype, so testing `a` alone identifies the shape of both. | ||
| * | ||
| * @returns The comparison result, or `null` if neither value is a recognised built-in | ||
| * @internal | ||
| */ | ||
| function deepEqualsBuiltIn(a, b, seen) { | ||
| if (a instanceof Date) return Object.is(a.getTime(), b.getTime()); | ||
| if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags; | ||
| if (Array.isArray(a)) { | ||
| const other = b; | ||
| return a.length === other.length && a.every((item, index) => deepEquals(item, other[index], seen)); | ||
| } | ||
| if (a instanceof Map) { | ||
| const other = b; | ||
| return a.size === other.size && [...a].every(([key, value]) => other.has(key) && deepEquals(value, other.get(key), seen)); | ||
| } | ||
| if (a instanceof Set) { | ||
| const other = b; | ||
| return a.size === other.size && [...a].every((value) => other.has(value)); | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Recursively freezes or seals a value and everything reachable from it. | ||
@@ -840,2 +897,63 @@ * | ||
| /** | ||
| * Field-initializer defaults per target class, computed once per class. | ||
| * | ||
| * `null` records a class whose constructor cannot be called without arguments. | ||
| * @internal | ||
| */ | ||
| const PROBE_DEFAULTS_CACHE = /* @__PURE__ */ new WeakMap(); | ||
| /** | ||
| * The property values a target class produces when constructed with no data at all. | ||
| * | ||
| * This is what separates a field initializer's untouched default from a value the | ||
| * constructor genuinely derived from the builder's data - both are non-`undefined` on the | ||
| * built instance, and nothing else distinguishes them. | ||
| * | ||
| * Constructed at most once per class and only on the ambiguous path, so classes that take | ||
| * no data parameter or assign straight through never pay for it. A constructor that demands | ||
| * arguments throws here; that is recorded as `null`, and the caller then falls back to | ||
| * assigning, which keeps the builder's "what you set is what you get" guarantee rather than | ||
| * silently dropping a value. | ||
| * | ||
| * @internal | ||
| */ | ||
| function probeDefaults(ctor) { | ||
| const cached = PROBE_DEFAULTS_CACHE.get(ctor); | ||
| if (cached !== void 0) return cached; | ||
| let defaults = null; | ||
| try { | ||
| const probe = new ctor(); | ||
| defaults = {}; | ||
| for (const key of Object.keys(probe)) defaults[key] = probe[key]; | ||
| } catch { | ||
| defaults = null; | ||
| } | ||
| PROBE_DEFAULTS_CACHE.set(ctor, defaults); | ||
| return defaults; | ||
| } | ||
| /** | ||
| * Decides whether a single built value still has to be assigned onto the instance. | ||
| * | ||
| * This used to be one all-or-nothing flag for the whole object: if any key came back | ||
| * `undefined` the constructor was assumed to have ignored the data and every key was | ||
| * overwritten, otherwise none was. A class with field initializers broke both halves of | ||
| * that - the initializer left nothing `undefined`, so a class that never read `data` at all | ||
| * (`class Envelope { resultSet = new ResultSet() }`) looked exactly like one that had | ||
| * consumed it, and the built value was silently dropped. | ||
| * | ||
| * The question is per key, and it is "did the constructor derive this value from the data?" | ||
| * | ||
| * A module-level function rather than a method: every runtime member of the builder has to | ||
| * be reserved, and reserving a name costs users a property they can no longer declare. | ||
| * | ||
| * @internal | ||
| */ | ||
| function shouldAssign(ctor, instance, data, key) { | ||
| if (instance[key] === data[key]) return false; | ||
| if (ctor.length === 0) return true; | ||
| const defaults = probeDefaults(ctor); | ||
| if (defaults === null) return true; | ||
| if (defaults[key] === void 0) return instance[key] === void 0; | ||
| return deepEquals(instance[key], defaults[key]); | ||
| } | ||
| /** | ||
| * Rejects writes to a getter-only accessor of the target class. | ||
@@ -913,5 +1031,4 @@ * | ||
| * 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. | ||
| * variants. Values the constructor derived from the data are left alone; the rest are | ||
| * assigned afterwards, per key - see `shouldAssign`. | ||
| */ | ||
@@ -924,3 +1041,3 @@ instantiate() { | ||
| const dataKeys = Object.keys(data).filter((key) => !getterOnly.has(key)); | ||
| if (dataKeys.some((key) => instance[key] === void 0 && data[key] !== void 0)) for (const key of dataKeys) instance[key] = data[key]; | ||
| for (const key of dataKeys) if (shouldAssign(ctor, instance, data, key)) instance[key] = data[key]; | ||
| return instance; | ||
@@ -927,0 +1044,0 @@ } |
+121
-4
@@ -109,2 +109,59 @@ const RESERVED_SET = /* @__PURE__ */ new Set([ | ||
| /** | ||
| * Structural equality over the same value shapes `deepClone` reproduces. | ||
| * | ||
| * Used by the class builder to tell a field initializer's untouched default apart from a | ||
| * value the target constructor derived from the incoming data - the two are distinguishable | ||
| * only by comparing against a no-data probe instance, and the probe's nested objects are | ||
| * never reference-equal to the built instance's. | ||
| * | ||
| * Prototypes must match, so a class instance never compares equal to a plain object with | ||
| * the same keys. `Object.is` handles `NaN` and `-0` so a `NaN` default is not mistaken for | ||
| * a constructor-derived value. The `seen` map pairs each visited left-hand object with its | ||
| * right-hand counterpart, which makes cyclic structures terminate. | ||
| * | ||
| * @param a - Left-hand value | ||
| * @param b - Right-hand value | ||
| * @param seen - Cycle-tracking map; callers do not pass this | ||
| * @internal | ||
| */ | ||
| function deepEquals(a, b, seen = /* @__PURE__ */ new WeakMap()) { | ||
| if (Object.is(a, b)) return true; | ||
| if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false; | ||
| if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false; | ||
| const pairedWith = seen.get(a); | ||
| if (pairedWith !== void 0) return pairedWith === b; | ||
| seen.set(a, b); | ||
| const builtIn = deepEqualsBuiltIn(a, b, seen); | ||
| if (builtIn !== null) return builtIn; | ||
| const keys = Object.keys(a); | ||
| if (keys.length !== Object.keys(b).length) return false; | ||
| return keys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && deepEquals(a[key], b[key], seen)); | ||
| } | ||
| /** | ||
| * The built-in half of `deepEquals`, split out to keep either function readable. | ||
| * | ||
| * Callers have already established that `a` and `b` are non-null objects sharing a | ||
| * prototype, so testing `a` alone identifies the shape of both. | ||
| * | ||
| * @returns The comparison result, or `null` if neither value is a recognised built-in | ||
| * @internal | ||
| */ | ||
| function deepEqualsBuiltIn(a, b, seen) { | ||
| if (a instanceof Date) return Object.is(a.getTime(), b.getTime()); | ||
| if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags; | ||
| if (Array.isArray(a)) { | ||
| const other = b; | ||
| return a.length === other.length && a.every((item, index) => deepEquals(item, other[index], seen)); | ||
| } | ||
| if (a instanceof Map) { | ||
| const other = b; | ||
| return a.size === other.size && [...a].every(([key, value]) => other.has(key) && deepEquals(value, other.get(key), seen)); | ||
| } | ||
| if (a instanceof Set) { | ||
| const other = b; | ||
| return a.size === other.size && [...a].every((value) => other.has(value)); | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Recursively freezes or seals a value and everything reachable from it. | ||
@@ -839,2 +896,63 @@ * | ||
| /** | ||
| * Field-initializer defaults per target class, computed once per class. | ||
| * | ||
| * `null` records a class whose constructor cannot be called without arguments. | ||
| * @internal | ||
| */ | ||
| const PROBE_DEFAULTS_CACHE = /* @__PURE__ */ new WeakMap(); | ||
| /** | ||
| * The property values a target class produces when constructed with no data at all. | ||
| * | ||
| * This is what separates a field initializer's untouched default from a value the | ||
| * constructor genuinely derived from the builder's data - both are non-`undefined` on the | ||
| * built instance, and nothing else distinguishes them. | ||
| * | ||
| * Constructed at most once per class and only on the ambiguous path, so classes that take | ||
| * no data parameter or assign straight through never pay for it. A constructor that demands | ||
| * arguments throws here; that is recorded as `null`, and the caller then falls back to | ||
| * assigning, which keeps the builder's "what you set is what you get" guarantee rather than | ||
| * silently dropping a value. | ||
| * | ||
| * @internal | ||
| */ | ||
| function probeDefaults(ctor) { | ||
| const cached = PROBE_DEFAULTS_CACHE.get(ctor); | ||
| if (cached !== void 0) return cached; | ||
| let defaults = null; | ||
| try { | ||
| const probe = new ctor(); | ||
| defaults = {}; | ||
| for (const key of Object.keys(probe)) defaults[key] = probe[key]; | ||
| } catch { | ||
| defaults = null; | ||
| } | ||
| PROBE_DEFAULTS_CACHE.set(ctor, defaults); | ||
| return defaults; | ||
| } | ||
| /** | ||
| * Decides whether a single built value still has to be assigned onto the instance. | ||
| * | ||
| * This used to be one all-or-nothing flag for the whole object: if any key came back | ||
| * `undefined` the constructor was assumed to have ignored the data and every key was | ||
| * overwritten, otherwise none was. A class with field initializers broke both halves of | ||
| * that - the initializer left nothing `undefined`, so a class that never read `data` at all | ||
| * (`class Envelope { resultSet = new ResultSet() }`) looked exactly like one that had | ||
| * consumed it, and the built value was silently dropped. | ||
| * | ||
| * The question is per key, and it is "did the constructor derive this value from the data?" | ||
| * | ||
| * A module-level function rather than a method: every runtime member of the builder has to | ||
| * be reserved, and reserving a name costs users a property they can no longer declare. | ||
| * | ||
| * @internal | ||
| */ | ||
| function shouldAssign(ctor, instance, data, key) { | ||
| if (instance[key] === data[key]) return false; | ||
| if (ctor.length === 0) return true; | ||
| const defaults = probeDefaults(ctor); | ||
| if (defaults === null) return true; | ||
| if (defaults[key] === void 0) return instance[key] === void 0; | ||
| return deepEquals(instance[key], defaults[key]); | ||
| } | ||
| /** | ||
| * Rejects writes to a getter-only accessor of the target class. | ||
@@ -912,5 +1030,4 @@ * | ||
| * 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. | ||
| * variants. Values the constructor derived from the data are left alone; the rest are | ||
| * assigned afterwards, per key - see `shouldAssign`. | ||
| */ | ||
@@ -923,3 +1040,3 @@ instantiate() { | ||
| const dataKeys = Object.keys(data).filter((key) => !getterOnly.has(key)); | ||
| if (dataKeys.some((key) => instance[key] === void 0 && data[key] !== void 0)) for (const key of dataKeys) instance[key] = data[key]; | ||
| for (const key of dataKeys) if (shouldAssign(ctor, instance, data, key)) instance[key] = data[key]; | ||
| return instance; | ||
@@ -926,0 +1043,0 @@ } |
+1
-1
| { | ||
| "name": "@cerios/cerios-builder", | ||
| "version": "1.7.0", | ||
| "version": "1.7.1", | ||
| "description": "A TypeScript builder pattern library providing compile-time type safety for object construction with method chaining and required field validation", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
580263
6.82%3017
8.41%0
-100%