ivue 2.3.0 → 2.5.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.
@@ -17,6 +17,7 @@ import {
17
17
  propsWithDefaults,
18
18
  Reactive,
19
19
  type ReactiveInstance,
20
+ definePropTypes,
20
21
  } from '../Reactive';
21
22
 
22
23
  /**
@@ -418,18 +419,18 @@ describe('Reactive()', () => {
418
419
  }
419
420
  }
420
421
  class Mid extends Base {
421
- get summary() {
422
+ override get summary() {
422
423
  return computed(() => `(Mid>${super.summary.value})`);
423
424
  }
424
- get chain() {
425
+ override get chain() {
425
426
  return super.chain + '->Mid';
426
427
  }
427
428
  }
428
429
  class Leaf extends Mid {
429
- get summary() {
430
+ override get summary() {
430
431
  return computed(() => `{Leaf>${super.summary.value}}`);
431
432
  }
432
- get chain() {
433
+ override get chain() {
433
434
  return super.chain + '->Leaf';
434
435
  }
435
436
  }
@@ -446,7 +447,7 @@ describe('Reactive()', () => {
446
447
  }
447
448
  }
448
449
  class Child extends Base {
449
- get val() {
450
+ override get val() {
450
451
  return computed(() => 10 + super.val.value);
451
452
  }
452
453
  }
@@ -491,13 +492,13 @@ describe('Reactive()', () => {
491
492
  }
492
493
  }
493
494
  class L2 extends L1 {
494
- get tag() {
495
+ override get tag() {
495
496
  return computed(() => `L2(${super.tag.value})`);
496
497
  }
497
- get name() {
498
+ override get name() {
498
499
  return super.name + '>L2';
499
500
  }
500
- greet() {
501
+ override greet() {
501
502
  return super.greet() + '/L2';
502
503
  }
503
504
  }
@@ -505,18 +506,18 @@ describe('Reactive()', () => {
505
506
  get extra() {
506
507
  return ref(5);
507
508
  }
508
- get tag() {
509
+ override get tag() {
509
510
  return computed(() => `L3[${super.tag.value}]`);
510
511
  }
511
- get name() {
512
+ override get name() {
512
513
  return super.name + '>L3';
513
514
  }
514
515
  }
515
516
  class L4 extends L3 {
516
- get tag() {
517
+ override get tag() {
517
518
  return computed(() => `L4{${super.tag.value}}`);
518
519
  }
519
- get name() {
520
+ override get name() {
520
521
  return super.name + '>L4';
521
522
  }
522
523
  // computed in the child aggregating refs declared 3 and 1 levels up
@@ -525,7 +526,7 @@ describe('Reactive()', () => {
525
526
  () => (this as any).base.value + (this as any).extra.value,
526
527
  );
527
528
  }
528
- greet() {
529
+ override greet() {
529
530
  return super.greet() + '/L4';
530
531
  }
531
532
  }
@@ -735,6 +736,64 @@ describe('Reactive()', () => {
735
736
  expect(() => instance.$stopEffects()).not.toThrow();
736
737
  });
737
738
 
739
+ it('{ reset: false } stops watchers but every cached cell survives', async () => {
740
+ class Session {
741
+ get counter() {
742
+ return ref(0);
743
+ }
744
+ get doubled() {
745
+ return computed(() => (this as any).counter.value * 2);
746
+ }
747
+ }
748
+ const instance: any = new (Reactive(Session))();
749
+ let observed = 0;
750
+ instance.$watchEffect(() => {
751
+ observed = instance.doubled.value; // first-touches the computed IN the scope
752
+ });
753
+ const counterCell = instance.counter;
754
+ const doubledCell = instance.doubled;
755
+ instance.counter.value = 21;
756
+ await nextTick();
757
+ expect(observed).toBe(42);
758
+
759
+ instance.$stopEffects({ reset: false });
760
+
761
+ // watchers are dead…
762
+ instance.counter.value = 100;
763
+ await nextTick();
764
+ expect(observed).toBe(42);
765
+ // …but the cells survive with their CURRENT values (no re-init)
766
+ expect(instance.counter).toBe(counterCell);
767
+ expect(instance.doubled).toBe(doubledCell);
768
+ expect(instance.counter.value).toBe(100);
769
+ // the surviving computed still evaluates (pull-based past scope death)
770
+ expect(instance.doubled.value).toBe(200);
771
+
772
+ // and a SECOND life works: a fresh scope tracks the old cells
773
+ let observedAgain = 0;
774
+ instance.$watch(
775
+ () => instance.doubled.value,
776
+ (doubledValue: number) => {
777
+ observedAgain = doubledValue;
778
+ },
779
+ );
780
+ instance.counter.value = 7;
781
+ await nextTick();
782
+ expect(observedAgain).toBe(14);
783
+ });
784
+
785
+ it('default call still resets — cells re-initialize after teardown', () => {
786
+ class Session {
787
+ get counter() {
788
+ return ref(0);
789
+ }
790
+ }
791
+ const instance: any = new (Reactive(Session))();
792
+ instance.counter.value = 41;
793
+ instance.$stopEffects();
794
+ expect(instance.counter.value).toBe(0);
795
+ });
796
+
738
797
  it('$stopEffects is injected only once (idempotent re-Reactive)', () => {
739
798
  class Store {
740
799
  m() {
@@ -1054,3 +1113,14 @@ describe('$watchEffect (scoped watchEffect)', () => {
1054
1113
  expect(watchRuns).toBe(1); // sibling watcher in the same scope still fires
1055
1114
  });
1056
1115
  });
1116
+
1117
+ describe('definePropTypes()', () => {
1118
+ it('returns the same types map (identity, literal-preserving)', () => {
1119
+ const types = definePropTypes({
1120
+ title: { type: String, required: true },
1121
+ size: { type: Number },
1122
+ });
1123
+ expect(types.title.required).toBe(true);
1124
+ expect(types.size.type).toBe(Number);
1125
+ });
1126
+ });
@@ -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
+ });
package/lib/ivue.ts CHANGED
@@ -1,3 +1,20 @@
1
+ /**
2
+ * ============================================================
3
+ * DEPRECATED — ivue v1 (the reactive-proxy engine)
4
+ * ============================================================
5
+ *
6
+ * This file is the ORIGINAL v1 implementation: `ivue(Class, ...args)`
7
+ * wraps instances in `reactive()` and converts accessors to computeds.
8
+ * It is kept in-tree for reference and migration only.
9
+ *
10
+ * v2 is `Reactive()` in ./Reactive.ts (the package's main entry) — a
11
+ * one-time prototype transform: instances stay plain objects, plain
12
+ * getters cost zero bytes, creation measures 55-253x faster. New code
13
+ * must never import from this file.
14
+ *
15
+ * Migration guide: https://ivue.dev/guide/standard
16
+ */
17
+
1
18
  import type { ComputedRef, ExtractPropTypes, Ref, ToRef } from 'vue';
2
19
  import { computed, markRaw, reactive, ref, shallowRef, toRef } from 'vue';
3
20
  import type { Ref as DemiRef } from 'vue-demi';
@@ -113,9 +130,15 @@ export type ExtractEmitTypes<T extends Record<string, any>> =
113
130
 
114
131
  /**
115
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`.
116
136
  */
117
137
  export type ExtractPropDefaultTypes<O> = {
118
- [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
+ >;
119
142
  };
120
143
 
121
144
  /**
@@ -399,6 +422,8 @@ const getClassPropertiesAccessorsMap = (obj: object): PropsMapValue => {
399
422
  * get getter () { return 'hello world'; }
400
423
  * }
401
424
  *
425
+ * @deprecated v1 engine — use `Reactive()` from the main `ivue` entry
426
+ * instead; see https://ivue.dev/guide/standard for the migration.
402
427
  * @param className Any Class
403
428
  * @param args Class constructor arguments that you would pass to a `new AnyClass(args...)`
404
429
  * @returns {IVue<T>}
@@ -760,6 +785,14 @@ export const isClass = (val: any): boolean => {
760
785
  * @param typedProps Props declared in defineComponent() style with type and possibly required declared, but without default
761
786
  * @returns Props declared in defineComponent() style with all properties having default property declared.
762
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
+
763
796
  export const propsWithDefaults = <T extends VuePropsObject>(
764
797
  defaults: Record<string, any>,
765
798
  typedProps: T,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ivue",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Infinite Vue – Class Based Architecture for Vue 3",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,12 +14,16 @@
14
14
  "import": "./dist/extras.es.js",
15
15
  "require": "./dist/extras.cjs"
16
16
  },
17
- "./package.json": "./package.json"
17
+ "./package.json": "./package.json",
18
+ "./skills/*": "./skills/*"
18
19
  },
19
20
  "main": "./dist/index.cjs",
20
21
  "module": "./dist/index.es.js",
21
22
  "types": "./dist/index.d.ts",
22
23
  "scripts": {
24
+ "gate": "vite-node skills/ivue/ivue-standards-check.ts --",
25
+ "gate:house": "vite-node skills/ivue/ivue-house-gate.ts --",
26
+ "gate:newsletter": "vite-node skills/ivue/ivue-generator-standard.ts -- --source-root newsletter --skip-list skills/ivue/ivue-standards-skip.json --test-glob 'newsletter/src/**/*.test.ts'",
23
27
  "dev": "vite demo --host",
24
28
  "dev:demo": "vite demo --host",
25
29
  "dev:flyweight": "vite examples/playground --port 5181 --host",
@@ -39,7 +43,7 @@
39
43
  "cypress:headed": "cypress run --component --browser chrome --headed --no-exit",
40
44
  "build": "tsc --version;tsc --p ./tsconfig.json && vite build",
41
45
  "build:demo": "tsc --p ./tsconfig.json && vite build demo",
42
- "build:docs": "npm run sync:examples && npm run sync:releases && npm run sync:blog-index && npm --prefix docs_v2 run build && npm run check:links && node docs_v2/scripts/check-related-posts.mjs",
46
+ "build:docs": "npm run sync:examples && npm run sync:releases && npm run sync:blog-index && node docs_v2/scripts/rss-generator.mjs && npm --prefix docs_v2 run build && npm run check:links && node docs_v2/scripts/check-related-posts.mjs",
43
47
  "sync:releases": "node docs_v2/scripts/releases-page-generator.mjs",
44
48
  "sync:blog-index": "node docs_v2/scripts/blog-index-generator.mjs",
45
49
  "check:links": "node docs_v2/scripts/check-links.mjs",
@@ -47,6 +51,7 @@
47
51
  "render:form-header": "node docs_v2/scripts/brand-image-generator.mjs form-header",
48
52
  "render:banner": "node docs_v2/scripts/brand-image-generator.mjs blog",
49
53
  "render:diagram": "node docs_v2/scripts/brand-image-generator.mjs diagram",
54
+ "render:page-og": "node docs_v2/scripts/page-og-generator.mjs",
50
55
  "render:embeds": "node docs_v2/scripts/blog-embed-shots.mjs",
51
56
  "render:code-shots": "node docs_v2/scripts/blog-code-shots.mjs",
52
57
  "sync:blog-dates": "node docs_v2/scripts/blog-dates-generator.mjs",