ivue 2.1.0 → 2.2.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/dist/Static.d.ts CHANGED
@@ -6,6 +6,18 @@
6
6
  * retain as callbacks (routers, watchers, command handlers) while the
7
7
  * selected `Class` slot remains replaceable by a kernel/plugin.
8
8
  *
9
+ * Get-only static accessors whose name starts with `$` become
10
+ * compute-once-per-receiver caches: the getter body runs on first read
11
+ * through a given class, its (shallowly frozen) result is stored under a
12
+ * symbol OWN property of that receiver, and later reads return the stored
13
+ * value. The `Object.hasOwn` guard never walks the prototype chain, so a
14
+ * parent's cache can never shadow a subclass — each class in a hierarchy
15
+ * derives through its own overrides on its own first read, in ANY read
16
+ * order. The `$` prefix IS the API: a static getter that must stay live
17
+ * (a knob for subclasses to pinch, a fresh-per-read value) must not use
18
+ * it. Cached values are frozen shallowly — cache-and-freeze or
19
+ * return-fresh, never cache-mutable.
20
+ *
9
21
  * This is the namespace pattern's backend adapter: canonical namespace +
10
22
  * mutable `Class` slot + late reads, for STATELESS capability classes (a
11
23
  * function bag). Never wrap stateful/reactive instance classes with it — use
package/dist/extras.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),exports.Static=function(o){const r=class extends o{},c=new Set;for(let e=o;e!==Function.prototype;e=Object.getPrototypeOf(e))for(const t of Reflect.ownKeys(e)){if(c.has(t))continue;c.add(t);const n=Object.getOwnPropertyDescriptor(e,t);if(typeof n.value!="function")continue;const u=n.value;Object.defineProperty(r,t,{configurable:!0,enumerable:n.enumerable,get(){const i=u.bind(this);return Object.defineProperty(this,t,{...n,value:i}),i}})}return r};
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),exports.Static=function(i){const o=class extends i{},s=new Set;for(let n=i;n!==Function.prototype;n=Object.getPrototypeOf(n))for(const e of Reflect.ownKeys(n)){if(s.has(e))continue;s.add(e);const t=Object.getOwnPropertyDescriptor(n,e);if(typeof t.value=="function"){const c=t.value;Object.defineProperty(o,e,{configurable:!0,enumerable:t.enumerable,get(){const r=c.bind(this);return Object.defineProperty(this,e,{...t,value:r}),r}})}else if(t.get&&!t.set&&typeof e=="string"&&e.startsWith("$")){const c=t.get,r=Symbol.for(`ivue.staticCache.${e}`);Object.defineProperty(o,e,{configurable:!0,enumerable:t.enumerable,get(){return Object.hasOwn(this,r)||Object.defineProperty(this,r,{configurable:!0,value:Object.freeze(c.call(this))}),this[r]}})}}return o};
package/dist/extras.es.js CHANGED
@@ -1,22 +1,27 @@
1
- function f(o) {
2
- const r = class extends o {
3
- }, c = /* @__PURE__ */ new Set();
4
- for (let e = o; e !== Function.prototype; e = Object.getPrototypeOf(e))
5
- for (const t of Reflect.ownKeys(e)) {
6
- if (c.has(t))
1
+ function a(i) {
2
+ const o = class extends i {
3
+ }, s = /* @__PURE__ */ new Set();
4
+ for (let r = i; r !== Function.prototype; r = Object.getPrototypeOf(r))
5
+ for (const e of Reflect.ownKeys(r)) {
6
+ if (s.has(e))
7
7
  continue;
8
- c.add(t);
9
- const n = Object.getOwnPropertyDescriptor(e, t);
10
- if (typeof n.value != "function")
11
- continue;
12
- const s = n.value;
13
- Object.defineProperty(r, t, { configurable: !0, enumerable: n.enumerable, get() {
14
- const i = s.bind(this);
15
- return Object.defineProperty(this, t, { ...n, value: i }), i;
16
- } });
8
+ s.add(e);
9
+ const t = Object.getOwnPropertyDescriptor(r, e);
10
+ if (typeof t.value == "function") {
11
+ const c = t.value;
12
+ Object.defineProperty(o, e, { configurable: !0, enumerable: t.enumerable, get() {
13
+ const n = c.bind(this);
14
+ return Object.defineProperty(this, e, { ...t, value: n }), n;
15
+ } });
16
+ } else if (t.get && !t.set && typeof e == "string" && e.startsWith("$")) {
17
+ const c = t.get, n = Symbol.for(`ivue.staticCache.${e}`);
18
+ Object.defineProperty(o, e, { configurable: !0, enumerable: t.enumerable, get() {
19
+ return Object.hasOwn(this, n) || Object.defineProperty(this, n, { configurable: !0, value: Object.freeze(c.call(this)) }), this[n];
20
+ } });
21
+ }
17
22
  }
18
- return r;
23
+ return o;
19
24
  }
20
25
  export {
21
- f as Static
26
+ a as Static
22
27
  };
package/lib/Static.ts CHANGED
@@ -6,6 +6,18 @@
6
6
  * retain as callbacks (routers, watchers, command handlers) while the
7
7
  * selected `Class` slot remains replaceable by a kernel/plugin.
8
8
  *
9
+ * Get-only static accessors whose name starts with `$` become
10
+ * compute-once-per-receiver caches: the getter body runs on first read
11
+ * through a given class, its (shallowly frozen) result is stored under a
12
+ * symbol OWN property of that receiver, and later reads return the stored
13
+ * value. The `Object.hasOwn` guard never walks the prototype chain, so a
14
+ * parent's cache can never shadow a subclass — each class in a hierarchy
15
+ * derives through its own overrides on its own first read, in ANY read
16
+ * order. The `$` prefix IS the API: a static getter that must stay live
17
+ * (a knob for subclasses to pinch, a fresh-per-read value) must not use
18
+ * it. Cached values are frozen shallowly — cache-and-freeze or
19
+ * return-fresh, never cache-mutable.
20
+ *
9
21
  * This is the namespace pattern's backend adapter: canonical namespace +
10
22
  * mutable `Class` slot + late reads, for STATELESS capability classes (a
11
23
  * function bag). Never wrap stateful/reactive instance classes with it — use
@@ -31,18 +43,42 @@ export function Static<Class extends ClassConstructor>(targetClass: Class): Clas
31
43
  visitedKeys.add(key);
32
44
 
33
45
  const descriptor = Object.getOwnPropertyDescriptor(currentClass, key)!;
34
- if (typeof descriptor.value !== 'function') continue;
35
- const method = descriptor.value;
36
46
 
37
- Object.defineProperty(SelectedClass, key, {
38
- configurable: true,
39
- enumerable: descriptor.enumerable,
40
- get(this: ClassConstructor) {
41
- const boundMethod = method.bind(this);
42
- Object.defineProperty(this, key, { ...descriptor, value: boundMethod });
43
- return boundMethod;
44
- },
45
- });
47
+ if (typeof descriptor.value === 'function') {
48
+ const method = descriptor.value;
49
+
50
+ Object.defineProperty(SelectedClass, key, {
51
+ configurable: true,
52
+ enumerable: descriptor.enumerable,
53
+ get(this: ClassConstructor) {
54
+ const boundMethod = method.bind(this);
55
+ Object.defineProperty(this, key, { ...descriptor, value: boundMethod });
56
+ return boundMethod;
57
+ },
58
+ });
59
+ } else if (
60
+ descriptor.get &&
61
+ !descriptor.set &&
62
+ typeof key === 'string' &&
63
+ key.startsWith('$')
64
+ ) {
65
+ const getter = descriptor.get;
66
+ const cacheKey = Symbol.for(`ivue.staticCache.${key}`);
67
+
68
+ Object.defineProperty(SelectedClass, key, {
69
+ configurable: true,
70
+ enumerable: descriptor.enumerable,
71
+ get(this: any) {
72
+ if (!Object.hasOwn(this, cacheKey)) {
73
+ Object.defineProperty(this, cacheKey, {
74
+ configurable: true,
75
+ value: Object.freeze(getter.call(this)),
76
+ });
77
+ }
78
+ return this[cacheKey];
79
+ },
80
+ });
81
+ }
46
82
  }
47
83
  }
48
84
 
@@ -97,3 +97,184 @@ describe('Static', () => {
97
97
  expect(render()).toBe('HEADER|body');
98
98
  });
99
99
  });
100
+
101
+ describe('Static $-cached getters', () => {
102
+ it('computes a $-getter exactly once per receiver', () => {
103
+ let computeRuns = 0;
104
+
105
+ class $Palette {
106
+ static get $tokens() {
107
+ computeRuns++;
108
+ return { accent: 'cyan' };
109
+ }
110
+ }
111
+
112
+ const Palette = Static($Palette);
113
+ const firstRead = Palette.$tokens;
114
+
115
+ expect(Palette.$tokens).toBe(firstRead); // same object, cached
116
+ expect(Palette.$tokens.accent).toBe('cyan');
117
+ expect(computeRuns).toBe(1);
118
+ });
119
+
120
+ it('is order-correct: parent read first, child override still wins', () => {
121
+ class $Momentum {
122
+ static get friction() {
123
+ return 2; // a live knob — subclasses pinch it
124
+ }
125
+ static get $atRest() {
126
+ return { velocity: 0, threshold: this.friction * 10 };
127
+ }
128
+ }
129
+
130
+ const Momentum = Static($Momentum);
131
+ class TunedMomentum extends Momentum {
132
+ static override get friction() {
133
+ return 7;
134
+ }
135
+ }
136
+
137
+ // PARENT reads first — the hand-rolled self-replacement idiom
138
+ // caches on the parent here and silently shadows the child forever
139
+ expect(Momentum.$atRest.threshold).toBe(20);
140
+ expect(TunedMomentum.$atRest.threshold).toBe(70); // child derives itself
141
+ expect(TunedMomentum.$atRest).not.toBe(Momentum.$atRest);
142
+ });
143
+
144
+ it('is order-correct: child read first, parent unaffected', () => {
145
+ let computeRuns = 0;
146
+
147
+ class $Momentum {
148
+ static get friction() {
149
+ return 2;
150
+ }
151
+ static get $atRest() {
152
+ computeRuns++;
153
+ return { threshold: this.friction * 10 };
154
+ }
155
+ }
156
+
157
+ const Momentum = Static($Momentum);
158
+ class TunedMomentum extends Momentum {
159
+ static override get friction() {
160
+ return 7;
161
+ }
162
+ }
163
+
164
+ expect(TunedMomentum.$atRest.threshold).toBe(70); // CHILD reads first
165
+ expect(Momentum.$atRest.threshold).toBe(20);
166
+ expect(computeRuns).toBe(2); // once per receiver, never shared
167
+ });
168
+
169
+ it('freezes cached values — mutation throws instead of corrupting', () => {
170
+ class $Config {
171
+ static get $defaults() {
172
+ return { width: 80 };
173
+ }
174
+ }
175
+
176
+ const Config = Static($Config);
177
+ const defaults = Config.$defaults;
178
+
179
+ expect(Object.isFrozen(defaults)).toBe(true);
180
+ expect(() => {
181
+ (defaults as any).width = 120;
182
+ }).toThrow(TypeError);
183
+ expect(Config.$defaults.width).toBe(80);
184
+ });
185
+
186
+ it('caches primitive results too', () => {
187
+ let computeRuns = 0;
188
+
189
+ class $Layout {
190
+ static get $gutterWidth() {
191
+ computeRuns++;
192
+ return 4;
193
+ }
194
+ }
195
+
196
+ const Layout = Static($Layout);
197
+
198
+ expect(Layout.$gutterWidth).toBe(4);
199
+ expect(Layout.$gutterWidth).toBe(4);
200
+ expect(computeRuns).toBe(1);
201
+ });
202
+
203
+ it('leaves non-$ getters live — knobs re-read every time', () => {
204
+ let reads = 0;
205
+
206
+ class $Driver {
207
+ static get retainedLimit() {
208
+ reads++;
209
+ return 96;
210
+ }
211
+ }
212
+
213
+ const Driver = Static($Driver);
214
+ Driver.retainedLimit;
215
+ Driver.retainedLimit;
216
+
217
+ expect(reads).toBe(2); // no caching without the $ prefix
218
+ });
219
+
220
+ it('leaves accessor pairs with a setter untouched', () => {
221
+ class $Tunable {
222
+ static backing = 1;
223
+ static get $level() {
224
+ return this.backing;
225
+ }
226
+ static set $level(value: number) {
227
+ this.backing = value;
228
+ }
229
+ }
230
+
231
+ const Tunable = Static($Tunable);
232
+ Tunable.$level = 5;
233
+
234
+ expect(Tunable.$level).toBe(5); // still the live pair, not a cache
235
+ Tunable.$level = 9;
236
+ expect(Tunable.$level).toBe(9);
237
+ });
238
+
239
+ it('a subclass overriding the $-getter itself wins', () => {
240
+ class $Theme {
241
+ static get $accent() {
242
+ return 'blue';
243
+ }
244
+ }
245
+
246
+ const Theme = Static($Theme);
247
+ expect(Theme.$accent).toBe('blue');
248
+
249
+ class DarkTheme extends Theme {
250
+ static override get $accent() {
251
+ return 'violet';
252
+ }
253
+ }
254
+
255
+ // the subclass's own getter descriptor shadows the caching wrapper
256
+ expect(DarkTheme.$accent).toBe('violet');
257
+ expect(Theme.$accent).toBe('blue');
258
+ });
259
+
260
+ it('walks the raw inheritance chain — ancestor $-getters cache per receiver', () => {
261
+ class $Base {
262
+ static get scale() {
263
+ return 1;
264
+ }
265
+ static get $metrics() {
266
+ return { unit: this.scale * 8 };
267
+ }
268
+ }
269
+ class $Wide extends $Base {
270
+ static override get scale() {
271
+ return 3;
272
+ }
273
+ }
274
+
275
+ const Wide = Static($Wide);
276
+
277
+ expect(Wide.$metrics.unit).toBe(24); // derived through the override
278
+ expect(Wide.$metrics).toBe(Wide.$metrics); // and cached
279
+ });
280
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ivue",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Infinite Vue – Class Based Architecture for Vue 3",
5
5
  "type": "module",
6
6
  "exports": {
@@ -40,6 +40,7 @@
40
40
  "build:docs": "npm run sync:examples && npm --prefix docs_v2 run build",
41
41
  "render:og": "node docs_v2/scripts/brand-image-generator.mjs og",
42
42
  "render:form-header": "node docs_v2/scripts/brand-image-generator.mjs form-header",
43
+ "render:banner": "node docs_v2/scripts/brand-image-generator.mjs blog",
43
44
  "preview:demo": "npm run build:demo && vite preview demo --host",
44
45
  "preview:docs": "npm --prefix docs_v2 run preview",
45
46
  "release": "npm run build && npm publish",