ivue 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/Static.ts CHANGED
@@ -42,6 +42,11 @@ export type ClassConstructor = new (...arguments_: any[]) => any;
42
42
 
43
43
  const hasOwn = Object.hasOwn;
44
44
 
45
+ // Every bind/cache symbol this module ever issues — so a second wrap can
46
+ // recognize an ancestor's runtime residue even for the unregistered
47
+ // symbols that back symbol-keyed methods.
48
+ const issuedCacheKeys = new Set<PropertyKey>();
49
+
45
50
  export function Static<Class extends ClassConstructor>(targetClass: Class): Class {
46
51
  const SelectedClass = class extends targetClass {};
47
52
  const visitedKeys = new Set<PropertyKey>();
@@ -55,14 +60,38 @@ export function Static<Class extends ClassConstructor>(targetClass: Class): Clas
55
60
  if (visitedKeys.has(key)) continue;
56
61
  visitedKeys.add(key);
57
62
 
63
+ // An already-wrapped ancestor that has been READ owns its bind/cache
64
+ // symbol properties. They are runtime residue, not API — re-wrapping
65
+ // them would install an ancestor-bound function where the child's
66
+ // own chain lookup expects to bind for itself.
67
+ if (typeof key === 'symbol' && Symbol.keyFor(key)?.startsWith('ivue.static')) continue;
68
+ if (issuedCacheKeys.has(key)) continue; // the unregistered symbols backing symbol-keyed methods
69
+
58
70
  const descriptor = Object.getOwnPropertyDescriptor(currentClass, key)!;
59
71
 
60
72
  if (typeof descriptor.value === 'function') {
73
+ // HOT-LOOP READY — measured, twice, after two wrong theories. The
74
+ // bind happens ONCE: the first read defines an own bind-key
75
+ // property holding the bound function; every later read returns
76
+ // it. A HOISTED bound method is exactly plain-function speed
77
+ // (in-browser, 9M calls, fresh-page medians: module fn 31.7ms,
78
+ // hoisted bound method 30.0ms). The ONLY per-call cost is reading
79
+ // the method THROUGH the accessor inside the loop (84.6ms same
80
+ // loop) — so in a million-call loop, destructure the methods once
81
+ // (`const { method } = X.Class` — a late read of the mutable slot,
82
+ // so a subclass swap is still honored) and pay the accessor once.
83
+ // Ordinary call counts never notice any of this.
84
+ //
85
+ // Benchmark honestly: a shared bench(fn) harness makes the call
86
+ // site megamorphic and slows every variant measured after the
87
+ // first — that artifact once misread bound calls as "48% slower."
88
+ // Fresh page per variant, dedicated loops.
61
89
  const method = descriptor.value;
62
90
  const bindKey =
63
91
  typeof key === 'string'
64
92
  ? Symbol.for(`ivue.staticBound.${key}`)
65
93
  : Symbol('ivue.staticBound');
94
+ issuedCacheKeys.add(bindKey);
66
95
 
67
96
  Object.defineProperty(SelectedClass, key, {
68
97
  configurable: true,
@@ -85,6 +114,7 @@ export function Static<Class extends ClassConstructor>(targetClass: Class): Clas
85
114
  ) {
86
115
  const getter = descriptor.get;
87
116
  const cacheKey = Symbol.for(`ivue.staticCache.${key}`);
117
+ issuedCacheKeys.add(cacheKey);
88
118
 
89
119
  Object.defineProperty(SelectedClass, key, {
90
120
  configurable: true,
@@ -13,10 +13,12 @@ import {
13
13
  } from 'vue';
14
14
 
15
15
  import {
16
+ clone,
16
17
  isClass,
17
18
  propsWithDefaults,
18
19
  Reactive,
19
20
  type ReactiveInstance,
21
+ definePropTypes,
20
22
  } from '../Reactive';
21
23
 
22
24
  /**
@@ -418,18 +420,18 @@ describe('Reactive()', () => {
418
420
  }
419
421
  }
420
422
  class Mid extends Base {
421
- get summary() {
423
+ override get summary() {
422
424
  return computed(() => `(Mid>${super.summary.value})`);
423
425
  }
424
- get chain() {
426
+ override get chain() {
425
427
  return super.chain + '->Mid';
426
428
  }
427
429
  }
428
430
  class Leaf extends Mid {
429
- get summary() {
431
+ override get summary() {
430
432
  return computed(() => `{Leaf>${super.summary.value}}`);
431
433
  }
432
- get chain() {
434
+ override get chain() {
433
435
  return super.chain + '->Leaf';
434
436
  }
435
437
  }
@@ -446,7 +448,7 @@ describe('Reactive()', () => {
446
448
  }
447
449
  }
448
450
  class Child extends Base {
449
- get val() {
451
+ override get val() {
450
452
  return computed(() => 10 + super.val.value);
451
453
  }
452
454
  }
@@ -491,13 +493,13 @@ describe('Reactive()', () => {
491
493
  }
492
494
  }
493
495
  class L2 extends L1 {
494
- get tag() {
496
+ override get tag() {
495
497
  return computed(() => `L2(${super.tag.value})`);
496
498
  }
497
- get name() {
499
+ override get name() {
498
500
  return super.name + '>L2';
499
501
  }
500
- greet() {
502
+ override greet() {
501
503
  return super.greet() + '/L2';
502
504
  }
503
505
  }
@@ -505,18 +507,18 @@ describe('Reactive()', () => {
505
507
  get extra() {
506
508
  return ref(5);
507
509
  }
508
- get tag() {
510
+ override get tag() {
509
511
  return computed(() => `L3[${super.tag.value}]`);
510
512
  }
511
- get name() {
513
+ override get name() {
512
514
  return super.name + '>L3';
513
515
  }
514
516
  }
515
517
  class L4 extends L3 {
516
- get tag() {
518
+ override get tag() {
517
519
  return computed(() => `L4{${super.tag.value}}`);
518
520
  }
519
- get name() {
521
+ override get name() {
520
522
  return super.name + '>L4';
521
523
  }
522
524
  // computed in the child aggregating refs declared 3 and 1 levels up
@@ -525,7 +527,7 @@ describe('Reactive()', () => {
525
527
  () => (this as any).base.value + (this as any).extra.value,
526
528
  );
527
529
  }
528
- greet() {
530
+ override greet() {
529
531
  return super.greet() + '/L4';
530
532
  }
531
533
  }
@@ -985,7 +987,7 @@ describe('propsWithDefaults()', () => {
985
987
  expect(out.nul.default).toBe(null); // null → else branch, assigned directly
986
988
  });
987
989
 
988
- it('wraps object/array defaults in a factory that structuredClones', () => {
990
+ it('wraps object/array defaults in a factory that copies their containers', () => {
989
991
  const typed = { o: { type: Object }, a: { type: Array } };
990
992
  const defaults = { o: { nested: { k: 1 } }, a: [1, 2, 3] };
991
993
  const out = propsWithDefaults(defaults, { ...typed }) as Record<
@@ -1021,6 +1023,49 @@ describe('propsWithDefaults()', () => {
1021
1023
  expect(v).toEqual({ k: 1, cloned: true });
1022
1024
  });
1023
1025
 
1026
+ it('isolates default containers while preserving nested constructors, callbacks and opaque objects', () => {
1027
+ class Runner {}
1028
+ class Tuning { gain = 1; }
1029
+ const onChange = () => 1;
1030
+ const tuning = new Tuning();
1031
+ const date = new Date(0);
1032
+ const cache = new Map([['gain', 1]]);
1033
+ const defaults = { options: { limits: { count: 5 }, runner: Runner, onChange, tuning, date, cache } };
1034
+ const props = propsWithDefaults(defaults, { options: { type: Object } }) as Record<string, any>;
1035
+ const first = props.options.default();
1036
+ const second = props.options.default();
1037
+
1038
+ first.limits.count = 2;
1039
+ expect(second.limits.count).toBe(5);
1040
+ expect(defaults.options.limits.count).toBe(5);
1041
+ expect(first.runner).toBe(Runner);
1042
+ expect(first.onChange).toBe(onChange);
1043
+ expect(first.tuning).toBe(tuning);
1044
+ expect(first.date).toBe(date);
1045
+ expect(first.cache).toBe(cache);
1046
+ // The public copier uses the same ownership policy as the props factory.
1047
+ const copied = clone(defaults.options);
1048
+ expect(copied.limits).not.toBe(defaults.options.limits);
1049
+ expect(copied.runner).toBe(Runner);
1050
+ });
1051
+
1052
+ it('accepts structuredClone explicitly for cyclic data and independent built-in objects', () => {
1053
+ const options: { date: Date; cache: Map<string, number>; self?: unknown } = {
1054
+ date: new Date(0), cache: new Map([['gain', 1]]),
1055
+ };
1056
+ options.self = options;
1057
+ const props = propsWithDefaults({ options }, { options: { type: Object } }, structuredClone) as Record<string, any>;
1058
+ const first = props.options.default();
1059
+ const second = props.options.default();
1060
+
1061
+ expect(first.self).toBe(first);
1062
+ expect(first.date).not.toBe(second.date);
1063
+ expect(first.date.getTime()).toBe(0);
1064
+ first.cache.set('gain', 9);
1065
+ expect(second.cache.get('gain')).toBe(1);
1066
+ expect(options.cache.get('gain')).toBe(1);
1067
+ });
1068
+
1024
1069
  it('wraps a class default in a factory that returns the class itself', () => {
1025
1070
  class Cool {
1026
1071
  x = 1;
@@ -1112,3 +1157,14 @@ describe('$watchEffect (scoped watchEffect)', () => {
1112
1157
  expect(watchRuns).toBe(1); // sibling watcher in the same scope still fires
1113
1158
  });
1114
1159
  });
1160
+
1161
+ describe('definePropTypes()', () => {
1162
+ it('returns the same types map (identity, literal-preserving)', () => {
1163
+ const types = definePropTypes({
1164
+ title: { type: String, required: true },
1165
+ size: { type: Number },
1166
+ });
1167
+ expect(types.title.required).toBe(true);
1168
+ expect(types.size.type).toBe(Number);
1169
+ });
1170
+ });
@@ -49,7 +49,7 @@ describe('Static', () => {
49
49
  }
50
50
  }
51
51
  class $Child extends $Base {
52
- static describe() {
52
+ static override describe() {
53
53
  return 'child';
54
54
  }
55
55
  }
@@ -426,6 +426,65 @@ describe('Static $-cached getters', () => {
426
426
  expect(settle()).toBe(70); // inherited method binds to the child, detached
427
427
  });
428
428
 
429
+ // invariant: A prototype level is transformed at most once (ivue.invariants.md)
430
+ it('re-wrapping a subclass never resurrects a parent-bound method through the bind cache', () => {
431
+ // The double-wrap pattern the manual prescribes: a Static parent, a raw
432
+ // subclass extending it, and Static() applied again to the subclass.
433
+ // The parent is READ FIRST, so its bind cache (an own symbol property)
434
+ // exists when the second wrap walks the chain — that cache must be
435
+ // invisible to the walk, or the child's methods run with the parent
436
+ // as receiver.
437
+ class $Fleet {
438
+ static get vessels(): string[] {
439
+ return ['tug'];
440
+ }
441
+
442
+ static roster() {
443
+ return this.vessels.join(',');
444
+ }
445
+ }
446
+
447
+ const Fleet = Static($Fleet);
448
+ expect(Fleet.roster()).toBe('tug'); // parent reads (and caches its bound method) first
449
+
450
+ class $HarborFleet extends Fleet {
451
+ static override get vessels(): string[] {
452
+ return [...super.vessels, 'ferry'];
453
+ }
454
+ }
455
+ const HarborFleet = Static($HarborFleet);
456
+
457
+ expect(HarborFleet.roster()).toBe('tug,ferry'); // the child's override, not the parent's snapshot
458
+ const roster = HarborFleet.roster;
459
+ expect(roster()).toBe('tug,ferry'); // detached, still child-bound
460
+ });
461
+
462
+ // invariant: A prototype level is transformed at most once (ivue.invariants.md)
463
+ it('re-wrapping skips the unregistered bind caches of symbol-keyed methods too', () => {
464
+ const describeKind = Symbol('describeKind');
465
+ class $Signal {
466
+ static get kind(): string {
467
+ return 'base';
468
+ }
469
+
470
+ static [describeKind]() {
471
+ return `kind:${this.kind}`;
472
+ }
473
+ }
474
+
475
+ const Signal = Static($Signal);
476
+ expect((Signal as any)[describeKind]()).toBe('kind:base'); // parent reads first — unregistered bind cache lands
477
+
478
+ class $AlertSignal extends Signal {
479
+ static override get kind(): string {
480
+ return 'alert';
481
+ }
482
+ }
483
+ const AlertSignal = Static($AlertSignal);
484
+
485
+ expect((AlertSignal as any)[describeKind]()).toBe('kind:alert'); // child receiver, not the parent snapshot
486
+ });
487
+
429
488
  it('walks the raw inheritance chain — ancestor $-getters cache per receiver', () => {
430
489
  class $Base {
431
490
  static get scale() {
@@ -13,6 +13,7 @@ import {
13
13
  ivue,
14
14
  propertiesAccessorsMaps,
15
15
  propsWithDefaults,
16
+ definePropTypes,
16
17
  } from '../ivue';
17
18
  const {
18
19
  copyOwnProps,
@@ -76,21 +77,21 @@ class ProductItem extends Item {
76
77
  }
77
78
 
78
79
  class StoreItem extends ProductItem {
79
- _productType = iref('store');
80
- get productType() {
80
+ override _productType = iref('store');
81
+ override get productType() {
81
82
  const prefix = super.productFeel ?? '';
82
83
  return (prefix ? prefix + ':' : '') + this._productType;
83
84
  }
84
- set productFeel(value: string) {
85
+ override set productFeel(value: string) {
85
86
  this._productFeel = value;
86
87
  }
87
88
  }
88
89
 
89
90
  class RetailStoreItem extends StoreItem {
90
- _productType = iref('retail');
91
+ override _productType = iref('retail');
91
92
  /** Do not overwrite productType getter here, on purpose. */
92
93
  productHistory = iref([]);
93
- get testProperty() {
94
+ override get testProperty() {
94
95
  return this._testProperty;
95
96
  }
96
97
  calculateSize() {
@@ -1885,3 +1886,14 @@ describe('ivue coverage edge cases', () => {
1885
1886
  expect(cloned.value).toBe(7);
1886
1887
  });
1887
1888
  });
1889
+
1890
+ describe('definePropTypes()', () => {
1891
+ it('returns the same types map (identity, literal-preserving)', () => {
1892
+ const types = definePropTypes({
1893
+ title: { type: String, required: true },
1894
+ size: { type: Number },
1895
+ });
1896
+ expect(types.title.required).toBe(true);
1897
+ expect(types.size.type).toBe(Number);
1898
+ });
1899
+ });
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { shallowReactive, shallowReadonly } from 'vue';
3
+ import { nestedProps, type NestedPartial } from '../extras';
4
+
5
+ type Knobs = {
6
+ wheel: { gain: number; follow: number };
7
+ touch: { gain: number; inertia: number; list: number[] };
8
+ label: string;
9
+ };
10
+
11
+ const defaults = (): Knobs => ({
12
+ wheel: { gain: 1, follow: 0.1 },
13
+ touch: { gain: 1.3, inertia: 30, list: [1, 2, 3] },
14
+ label: 'default'
15
+ });
16
+
17
+ describe('nestedProps', () => {
18
+ it('preserves null-prototype defaults, literal __proto__ keys and sparse arrays', () => {
19
+ const sparse = new Array<{ offset: number }>(2);
20
+ sparse[1] = { offset: 2 };
21
+ const bare = Object.assign(Object.create(null), { gain: 1 });
22
+ const literal = { ['__proto__']: { gain: 2 } };
23
+ const sharedDefaults = { scroll: { sparse, bare, literal } };
24
+ const filled = nestedProps({ scroll: {} }, sharedDefaults) as typeof sharedDefaults;
25
+
26
+ expect(Object.getPrototypeOf(filled.scroll.bare)).toBeNull();
27
+ expect(filled.scroll.bare).not.toBe(bare);
28
+ expect(Object.getPrototypeOf(filled.scroll.literal)).toBe(Object.prototype);
29
+ expect(Object.hasOwn(filled.scroll.literal, '__proto__')).toBe(true);
30
+ expect(filled.scroll.literal.__proto__).not.toBe(literal.__proto__);
31
+ expect(filled.scroll.sparse.length).toBe(2);
32
+ expect(0 in filled.scroll.sparse).toBe(false);
33
+ filled.scroll.sparse[1].offset = 9;
34
+ expect(sparse[1].offset).toBe(2);
35
+ });
36
+
37
+ it('copies missing default objects and arrays independently for each instance', () => {
38
+ const sharedDefaults = { scroll: { touch: { gain: 1, points: [{ offset: 2 }] }, steps: [1, 2] } };
39
+ const first = nestedProps({ scroll: {} }, sharedDefaults) as typeof sharedDefaults;
40
+ const second = nestedProps({ scroll: {} }, sharedDefaults) as typeof sharedDefaults;
41
+
42
+ first.scroll.touch.gain = 9;
43
+ first.scroll.touch.points[0].offset = 8;
44
+ first.scroll.steps.push(3);
45
+
46
+ expect(second).toEqual({ scroll: { touch: { gain: 1, points: [{ offset: 2 }] }, steps: [1, 2] } });
47
+ expect(sharedDefaults).toEqual(second);
48
+ expect(second.scroll.touch).not.toBe(sharedDefaults.scroll.touch);
49
+ expect(second.scroll.steps).not.toBe(sharedDefaults.scroll.steps);
50
+ });
51
+
52
+ it('preserves supplied containers and opaque default values when copying missing branches', () => {
53
+ class Tuning { gain = 9; }
54
+ const tuning = new Tuning();
55
+ const callback = () => 1;
56
+ const suppliedList = [7];
57
+ const supplied = { scroll: { list: suppliedList } };
58
+ const sharedDefaults = { scroll: { list: [1], touch: { tuning, callback, label: null } } };
59
+ const filled = nestedProps(supplied, sharedDefaults) as typeof sharedDefaults;
60
+
61
+ expect(filled).toBe(supplied);
62
+ expect(filled.scroll.list).toBe(suppliedList);
63
+ expect(filled.scroll.touch).not.toBe(sharedDefaults.scroll.touch);
64
+ expect(filled.scroll.touch.tuning).toBe(tuning);
65
+ expect(filled.scroll.touch.callback).toBe(callback);
66
+ expect(filled.scroll.touch.label).toBeNull();
67
+ });
68
+
69
+ it('fills a partial object from the defaults at every depth, in place, keeping every supplied leaf', () => {
70
+ const supplied: NestedPartial<Knobs> = { wheel: { gain: 2 }, label: 'mine' };
71
+ const props = nestedProps(supplied, defaults());
72
+ expect(props).toBe(supplied);
73
+ expect(props.wheel).toEqual({ gain: 2, follow: 0.1 });
74
+ expect(props.label).toBe('mine');
75
+ // A top-level prop is Vue's to default: never written here.
76
+ expect('touch' in props).toBe(false);
77
+ });
78
+
79
+ it('recurses through every plain-object level, filling only what is missing', () => {
80
+ const tree = { a: { b: { c: 9 }, e: 5 } };
81
+ const filled = nestedProps({ tree }, { tree: { a: { b: { c: 1, d: 2 }, e: 3, f: 4 } } });
82
+ expect(filled.tree).toBe(tree);
83
+ expect(filled.tree).toEqual({ a: { b: { c: 9, d: 2 }, e: 5, f: 4 } });
84
+ });
85
+
86
+ it('skips a top-level prop that is not a plain object on both sides, and fills a prototype-less default', () => {
87
+ const bareDefault = Object.create(null) as { z: number; y: number };
88
+ bareDefault.z = 1;
89
+ bareDefault.y = 2;
90
+ const props = { list: null as unknown as number[], label: 'mine', bare: { z: 5 }, fn: () => 1 };
91
+ const filled = nestedProps(props, { list: [1, 2], label: 'default', bare: bareDefault, fn: { a: 1 } });
92
+ expect(filled.list).toBeNull();
93
+ expect(filled.label).toBe('mine');
94
+ expect(filled.bare).toEqual({ z: 5, y: 2 });
95
+ expect(typeof filled.fn).toBe('function');
96
+ });
97
+
98
+ it('treats a nested undefined as absent and a nested null as a value', () => {
99
+ const props = nestedProps(
100
+ { wheel: { gain: undefined, follow: null } } as unknown as NestedPartial<Knobs>,
101
+ defaults()
102
+ );
103
+ expect(props.wheel.gain).toBe(1);
104
+ expect(props.wheel.follow).toBeNull();
105
+ });
106
+
107
+ it('takes arrays, class instances and functions whole, never merged', () => {
108
+ class Tuning {
109
+ gain = 9;
110
+ }
111
+ const tuning = new Tuning();
112
+ const props = nestedProps(
113
+ { touch: { list: [7] }, wheel: tuning } as unknown as NestedPartial<Knobs>,
114
+ defaults()
115
+ );
116
+ expect(props.touch.list).toEqual([7]);
117
+ expect(props.touch.inertia).toBe(30);
118
+ expect(props.wheel).toBe(tuning);
119
+ expect((props.wheel as unknown as Tuning).gain).toBe(9);
120
+ // An object supplied where the default is an array is kept as is.
121
+ const odd = nestedProps({ touch: { list: { 0: 7 } } } as unknown as NestedPartial<Knobs>, defaults());
122
+ expect(odd.touch.list).toEqual({ 0: 7 });
123
+ // A prototype-less object is plain too.
124
+ const bare = Object.create(null) as { gain: number };
125
+ bare.gain = 4;
126
+ const filled = nestedProps({ wheel: bare } as unknown as NestedPartial<Knobs>, defaults());
127
+ expect(filled.wheel).toBe(bare);
128
+ expect(filled.wheel.follow).toBe(0.1);
129
+ });
130
+
131
+ it('works through Vue’s shallow props proxy: the nested object is the parent’s and is filled directly', () => {
132
+ const wheel = { gain: 2 };
133
+ const props = shallowReadonly(shallowReactive({ wheel } as NestedPartial<Knobs>));
134
+ const filled = nestedProps(props, defaults());
135
+ expect(filled.wheel.follow).toBe(0.1);
136
+ expect(wheel).toEqual({ gain: 2, follow: 0.1 });
137
+ });
138
+
139
+ it('customCloner is the copy policy for default branches: identity shares them, structuredClone copies built-ins', () => {
140
+ const wheelDefault = { gain: 1, follow: 0.1 };
141
+ const first = nestedProps({} as NestedPartial<Knobs>, { wheel: wheelDefault } as Knobs);
142
+ expect(first.wheel).not.toBe(wheelDefault);
143
+ // The caller owns a fresh tree per instance and says so: no copy.
144
+ const owned = { list: [7] };
145
+ const identity = nestedProps({ touch: {} } as NestedPartial<Knobs>, { touch: owned } as unknown as Knobs, (value) => value);
146
+ expect(identity.touch.list).toBe(owned.list);
147
+ // structuredClone gives Date defaults their own copies.
148
+ const stamp = new Date(0);
149
+ const copied = nestedProps({ meta: {} } as { meta: { stamp?: Date } }, { meta: { stamp } }, structuredClone);
150
+ expect(copied.meta.stamp).not.toBe(stamp);
151
+ expect(copied.meta.stamp?.getTime()).toBe(0);
152
+ });
153
+ });
package/lib/clone.ts ADDED
@@ -0,0 +1,21 @@
1
+ /** Copy plain configuration containers; retain callbacks, constructors and
2
+ * opaque objects by reference. Container trees must be acyclic. */
3
+ export function clone<T>(value: T): T {
4
+ if (Array.isArray(value)) {
5
+ const copy = new Array(value.length);
6
+ for (let index = 0; index < value.length; index++) {
7
+ if (index in value) copy[index] = clone(value[index]);
8
+ }
9
+ return copy as T;
10
+ }
11
+ if (
12
+ value === null ||
13
+ typeof value !== 'object' ||
14
+ (value.constructor !== Object && value.constructor !== undefined)
15
+ ) return value;
16
+
17
+ // Preserve null prototypes and literal __proto__ keys without entry arrays.
18
+ const copy: Record<string, unknown> = { __proto__: Object.getPrototypeOf(value), ...value };
19
+ for (const key in copy) copy[key] = clone(copy[key]);
20
+ return copy as T;
21
+ }
package/lib/extras.ts CHANGED
@@ -8,3 +8,4 @@
8
8
  */
9
9
  export { Static, type ClassConstructor } from './Static';
10
10
  export { LazyShared } from './LazyShared';
11
+ export { nestedProps, type NestedPartial, type NestedProps } from './nestedProps';
package/lib/ivue.ts CHANGED
@@ -130,9 +130,15 @@ export type ExtractEmitTypes<T extends Record<string, any>> =
130
130
 
131
131
  /**
132
132
  * Extract properties as all assigned properties because they have defaults.
133
+ * Props declared `required: true` are filtered out (they can never carry a
134
+ * default); declare the types map with a literal-preserving generic call so
135
+ * the `required: true` literal survives `typeof`.
133
136
  */
134
137
  export type ExtractPropDefaultTypes<O> = {
135
- [K in keyof O]: ValueOf<ExtractPropTypes<O>, K>;
138
+ [K in keyof O as O[K] extends { required: true } ? never : K]: ValueOf<
139
+ ExtractPropTypes<O>,
140
+ K
141
+ >;
136
142
  };
137
143
 
138
144
  /**
@@ -779,6 +785,14 @@ export const isClass = (val: any): boolean => {
779
785
  * @param typedProps Props declared in defineComponent() style with type and possibly required declared, but without default
780
786
  * @returns Props declared in defineComponent() style with all properties having default property declared.
781
787
  */
788
+ /**
789
+ * Identity helper for a prop-TYPES map: generic inference preserves the
790
+ * `required: true` literal that a bare object const widens to `boolean`,
791
+ * which ExtractPropDefaultTypes' required-key filter depends on.
792
+ */
793
+ export const definePropTypes = <T extends VuePropsObject>(types: T): T =>
794
+ types;
795
+
782
796
  export const propsWithDefaults = <T extends VuePropsObject>(
783
797
  defaults: Record<string, any>,
784
798
  typedProps: T,