ivue 2.1.0 → 2.2.1

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.
@@ -87,7 +87,9 @@ export declare type ReactiveInstance<T> = T & WritableGetters<T> & {
87
87
  /** Stop the instance's effect scope and drop cached cells. */
88
88
  $stopEffects: () => void;
89
89
  };
90
- export declare type ReactiveClass<C extends new (...args: any) => any> = new (...args: ConstructorParameters<C>) => ReactiveInstance<InstanceType<C>>;
90
+ export declare type ReactiveClass<C extends new (...args: any) => any> = {
91
+ [Key in keyof C]: C[Key];
92
+ } & (new (...args: ConstructorParameters<C>) => ReactiveInstance<InstanceType<C>>);
91
93
  /**
92
94
  * Component-authoring type utilities (types only — erased at build time).
93
95
  * These complement `propsWithDefaults` for the params/defaults component
package/dist/Static.d.ts CHANGED
@@ -6,6 +6,29 @@
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 result is stored under a symbol OWN property
12
+ * of that receiver, and later reads return the stored value. The
13
+ * `Object.hasOwn` guard never walks the prototype chain, so a parent's
14
+ * cache can never shadow a subclass — each class in a hierarchy derives
15
+ * through its own overrides on its own first read, in ANY read order.
16
+ * The `$` prefix IS the API: it promises STABLE IDENTITY per receiver,
17
+ * nothing more — whether the cached value is then treated as immutable
18
+ * config or as a mutable memo table is the author's design. A static
19
+ * getter that must stay live (a knob for subclasses to pinch, a
20
+ * fresh-per-read value) must not use the prefix.
21
+ *
22
+ * Method binding uses the same per-receiver symbol discipline: the bound
23
+ * function is cached under a symbol own property, never under the method
24
+ * name — so a parent-first read can never install a parent-bound method
25
+ * where a subclass's chain lookup would find it.
26
+ *
27
+ * `$` semantics are GRANTED BY the transform: a raw class, a raw
28
+ * subclass, or a class only passed through `Reactive()` keeps native
29
+ * getter behavior. A class that needs instance reactivity AND static
30
+ * `$`-caches composes the transforms: `Static(Reactive($Class))`.
31
+ *
9
32
  * This is the namespace pattern's backend adapter: canonical namespace +
10
33
  * mutable `Class` slot + late reads, for STATELESS capability classes (a
11
34
  * 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"});const u=Object.hasOwn;exports.Static=function(c){const r=class extends c{},s=new Set;for(let o=c;o!==Function.prototype;o=Object.getPrototypeOf(o))for(const e of Reflect.ownKeys(o)){if(s.has(e))continue;s.add(e);const t=Object.getOwnPropertyDescriptor(o,e);if(typeof t.value=="function"){const i=t.value,n=typeof e=="string"?Symbol.for(`ivue.staticBound.${e}`):Symbol("ivue.staticBound");Object.defineProperty(r,e,{configurable:!0,enumerable:t.enumerable,get(){return u(this,n)||Object.defineProperty(this,n,{configurable:!0,value:i.bind(this)}),this[n]}})}else if(t.get&&!t.set&&typeof e=="string"&&e.startsWith("$")){const i=t.get,n=Symbol.for(`ivue.staticCache.${e}`);Object.defineProperty(r,e,{configurable:!0,enumerable:t.enumerable,get(){return u(this,n)||Object.defineProperty(this,n,{configurable:!0,value:i.call(this)}),this[n]}})}}return r};
package/dist/extras.es.js CHANGED
@@ -1,22 +1,27 @@
1
- function f(o) {
2
- const r = class extends o {
1
+ const a = Object.hasOwn;
2
+ function u(s) {
3
+ const r = class extends s {
3
4
  }, 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))
5
+ for (let n = s; n !== Function.prototype; n = Object.getPrototypeOf(n))
6
+ for (const e of Reflect.ownKeys(n)) {
7
+ if (c.has(e))
7
8
  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
- } });
9
+ c.add(e);
10
+ const t = Object.getOwnPropertyDescriptor(n, e);
11
+ if (typeof t.value == "function") {
12
+ const i = t.value, o = typeof e == "string" ? Symbol.for(`ivue.staticBound.${e}`) : Symbol("ivue.staticBound");
13
+ Object.defineProperty(r, e, { configurable: !0, enumerable: t.enumerable, get() {
14
+ return a(this, o) || Object.defineProperty(this, o, { configurable: !0, value: i.bind(this) }), this[o];
15
+ } });
16
+ } else if (t.get && !t.set && typeof e == "string" && e.startsWith("$")) {
17
+ const i = t.get, o = Symbol.for(`ivue.staticCache.${e}`);
18
+ Object.defineProperty(r, e, { configurable: !0, enumerable: t.enumerable, get() {
19
+ return a(this, o) || Object.defineProperty(this, o, { configurable: !0, value: i.call(this) }), this[o];
20
+ } });
21
+ }
17
22
  }
18
23
  return r;
19
24
  }
20
25
  export {
21
- f as Static
26
+ u as Static
22
27
  };
package/lib/Reactive.ts CHANGED
@@ -434,9 +434,9 @@ export type ReactiveInstance<T> = T &
434
434
  $stopEffects: () => void;
435
435
  };
436
436
 
437
- export type ReactiveClass<C extends new (...args: any) => any> = new (
438
- ...args: ConstructorParameters<C>
439
- ) => ReactiveInstance<InstanceType<C>>;
437
+ export type ReactiveClass<C extends new (...args: any) => any> = {
438
+ [Key in keyof C]: C[Key];
439
+ } & (new (...args: ConstructorParameters<C>) => ReactiveInstance<InstanceType<C>>);
440
440
 
441
441
  /**
442
442
  * Component-authoring type utilities (types only — erased at build time).
package/lib/Static.ts CHANGED
@@ -6,6 +6,29 @@
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 result is stored under a symbol OWN property
12
+ * of that receiver, and later reads return the stored value. The
13
+ * `Object.hasOwn` guard never walks the prototype chain, so a parent's
14
+ * cache can never shadow a subclass — each class in a hierarchy derives
15
+ * through its own overrides on its own first read, in ANY read order.
16
+ * The `$` prefix IS the API: it promises STABLE IDENTITY per receiver,
17
+ * nothing more — whether the cached value is then treated as immutable
18
+ * config or as a mutable memo table is the author's design. A static
19
+ * getter that must stay live (a knob for subclasses to pinch, a
20
+ * fresh-per-read value) must not use the prefix.
21
+ *
22
+ * Method binding uses the same per-receiver symbol discipline: the bound
23
+ * function is cached under a symbol own property, never under the method
24
+ * name — so a parent-first read can never install a parent-bound method
25
+ * where a subclass's chain lookup would find it.
26
+ *
27
+ * `$` semantics are GRANTED BY the transform: a raw class, a raw
28
+ * subclass, or a class only passed through `Reactive()` keeps native
29
+ * getter behavior. A class that needs instance reactivity AND static
30
+ * `$`-caches composes the transforms: `Static(Reactive($Class))`.
31
+ *
9
32
  * This is the namespace pattern's backend adapter: canonical namespace +
10
33
  * mutable `Class` slot + late reads, for STATELESS capability classes (a
11
34
  * function bag). Never wrap stateful/reactive instance classes with it — use
@@ -17,6 +40,8 @@
17
40
  */
18
41
  export type ClassConstructor = new (...arguments_: any[]) => any;
19
42
 
43
+ const hasOwn = Object.hasOwn;
44
+
20
45
  export function Static<Class extends ClassConstructor>(targetClass: Class): Class {
21
46
  const SelectedClass = class extends targetClass {};
22
47
  const visitedKeys = new Set<PropertyKey>();
@@ -31,18 +56,50 @@ export function Static<Class extends ClassConstructor>(targetClass: Class): Clas
31
56
  visitedKeys.add(key);
32
57
 
33
58
  const descriptor = Object.getOwnPropertyDescriptor(currentClass, key)!;
34
- if (typeof descriptor.value !== 'function') continue;
35
- const method = descriptor.value;
36
-
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
- });
59
+
60
+ if (typeof descriptor.value === 'function') {
61
+ const method = descriptor.value;
62
+ const bindKey =
63
+ typeof key === 'string'
64
+ ? Symbol.for(`ivue.staticBound.${key}`)
65
+ : Symbol('ivue.staticBound');
66
+
67
+ Object.defineProperty(SelectedClass, key, {
68
+ configurable: true,
69
+ enumerable: descriptor.enumerable,
70
+ get(this: any) {
71
+ if (!hasOwn(this, bindKey)) {
72
+ Object.defineProperty(this, bindKey, {
73
+ configurable: true,
74
+ value: method.bind(this),
75
+ });
76
+ }
77
+ return this[bindKey];
78
+ },
79
+ });
80
+ } else if (
81
+ descriptor.get &&
82
+ !descriptor.set &&
83
+ typeof key === 'string' &&
84
+ key.startsWith('$')
85
+ ) {
86
+ const getter = descriptor.get;
87
+ const cacheKey = Symbol.for(`ivue.staticCache.${key}`);
88
+
89
+ Object.defineProperty(SelectedClass, key, {
90
+ configurable: true,
91
+ enumerable: descriptor.enumerable,
92
+ get(this: any) {
93
+ if (!hasOwn(this, cacheKey)) {
94
+ Object.defineProperty(this, cacheKey, {
95
+ configurable: true,
96
+ value: getter.call(this),
97
+ });
98
+ }
99
+ return this[cacheKey];
100
+ },
101
+ });
102
+ }
46
103
  }
47
104
  }
48
105
 
@@ -1,4 +1,6 @@
1
+ import { ref } from 'vue';
1
2
  import { describe, expect, it } from 'vitest';
3
+ import { Reactive } from '../Reactive';
2
4
  import { Static } from '../Static';
3
5
 
4
6
  describe('Static', () => {
@@ -81,6 +83,23 @@ describe('Static', () => {
81
83
  expect(Config.resolve('/path')).toBe('https://example.test/path');
82
84
  });
83
85
 
86
+ it('binds symbol-keyed static methods with the same discipline', () => {
87
+ const describeKind = Symbol('describeKind');
88
+
89
+ class $Shape {
90
+ static kind = 'circle';
91
+ static [describeKind]() {
92
+ return `kind:${this.kind}`;
93
+ }
94
+ }
95
+
96
+ const Shape = Static($Shape);
97
+ const described = (Shape as any)[describeKind];
98
+
99
+ expect((Shape as any)[describeKind]).toBe(described); // identity-stable
100
+ expect(described()).toBe('kind:circle'); // bound through detachment
101
+ });
102
+
84
103
  it('binds `this` to the wrapped class, so statics compose', () => {
85
104
  class $Report {
86
105
  static header() {
@@ -97,3 +116,270 @@ describe('Static', () => {
97
116
  expect(render()).toBe('HEADER|body');
98
117
  });
99
118
  });
119
+
120
+ describe('Static $-cached getters', () => {
121
+ it('computes a $-getter exactly once per receiver', () => {
122
+ let computeRuns = 0;
123
+
124
+ class $Palette {
125
+ static get $tokens() {
126
+ computeRuns++;
127
+ return { accent: 'cyan' };
128
+ }
129
+ }
130
+
131
+ const Palette = Static($Palette);
132
+ const firstRead = Palette.$tokens;
133
+
134
+ expect(Palette.$tokens).toBe(firstRead); // same object, cached
135
+ expect(Palette.$tokens.accent).toBe('cyan');
136
+ expect(computeRuns).toBe(1);
137
+ });
138
+
139
+ it('is order-correct: parent read first, child override still wins', () => {
140
+ class $Momentum {
141
+ static get friction() {
142
+ return 2; // a live knob — subclasses pinch it
143
+ }
144
+ static get $atRest() {
145
+ return { velocity: 0, threshold: this.friction * 10 };
146
+ }
147
+ }
148
+
149
+ const Momentum = Static($Momentum);
150
+ class TunedMomentum extends Momentum {
151
+ static override get friction() {
152
+ return 7;
153
+ }
154
+ }
155
+
156
+ // PARENT reads first — the hand-rolled self-replacement idiom
157
+ // caches on the parent here and silently shadows the child forever
158
+ expect(Momentum.$atRest.threshold).toBe(20);
159
+ expect(TunedMomentum.$atRest.threshold).toBe(70); // child derives itself
160
+ expect(TunedMomentum.$atRest).not.toBe(Momentum.$atRest);
161
+ });
162
+
163
+ it('is order-correct: child read first, parent unaffected', () => {
164
+ let computeRuns = 0;
165
+
166
+ class $Momentum {
167
+ static get friction() {
168
+ return 2;
169
+ }
170
+ static get $atRest() {
171
+ computeRuns++;
172
+ return { threshold: this.friction * 10 };
173
+ }
174
+ }
175
+
176
+ const Momentum = Static($Momentum);
177
+ class TunedMomentum extends Momentum {
178
+ static override get friction() {
179
+ return 7;
180
+ }
181
+ }
182
+
183
+ expect(TunedMomentum.$atRest.threshold).toBe(70); // CHILD reads first
184
+ expect(Momentum.$atRest.threshold).toBe(20);
185
+ expect(computeRuns).toBe(2); // once per receiver, never shared
186
+ });
187
+
188
+ it('promises stable identity, not immutability — memo tables mutate freely', () => {
189
+ class $Wrap {
190
+ static get $memo() {
191
+ return new Map<string, number>();
192
+ }
193
+ static get $state() {
194
+ return { frame: 0, quiescent: false };
195
+ }
196
+ }
197
+
198
+ const Wrap = Static($Wrap);
199
+
200
+ // a cached Map used as a per-class memo table — the dominant real pattern
201
+ Wrap.$memo.set('row', 42);
202
+ expect(Wrap.$memo.get('row')).toBe(42); // same table, mutation retained
203
+
204
+ // a cached plain object used as deliberate mutable state
205
+ Object.assign(Wrap.$state, { frame: 7, quiescent: true });
206
+ expect(Wrap.$state.frame).toBe(7);
207
+ expect(Wrap.$state).toBe(Wrap.$state); // identity stable throughout
208
+ });
209
+
210
+ it('caches primitive results too', () => {
211
+ let computeRuns = 0;
212
+
213
+ class $Layout {
214
+ static get $gutterWidth() {
215
+ computeRuns++;
216
+ return 4;
217
+ }
218
+ }
219
+
220
+ const Layout = Static($Layout);
221
+
222
+ expect(Layout.$gutterWidth).toBe(4);
223
+ expect(Layout.$gutterWidth).toBe(4);
224
+ expect(computeRuns).toBe(1);
225
+ });
226
+
227
+ it('leaves non-$ getters live — knobs re-read every time', () => {
228
+ let reads = 0;
229
+
230
+ class $Driver {
231
+ static get retainedLimit() {
232
+ reads++;
233
+ return 96;
234
+ }
235
+ }
236
+
237
+ const Driver = Static($Driver);
238
+ Driver.retainedLimit;
239
+ Driver.retainedLimit;
240
+
241
+ expect(reads).toBe(2); // no caching without the $ prefix
242
+ });
243
+
244
+ it('leaves accessor pairs with a setter untouched', () => {
245
+ class $Tunable {
246
+ static backing = 1;
247
+ static get $level() {
248
+ return this.backing;
249
+ }
250
+ static set $level(value: number) {
251
+ this.backing = value;
252
+ }
253
+ }
254
+
255
+ const Tunable = Static($Tunable);
256
+ Tunable.$level = 5;
257
+
258
+ expect(Tunable.$level).toBe(5); // still the live pair, not a cache
259
+ Tunable.$level = 9;
260
+ expect(Tunable.$level).toBe(9);
261
+ });
262
+
263
+ it('a subclass overriding the $-getter itself wins', () => {
264
+ class $Theme {
265
+ static get $accent() {
266
+ return 'blue';
267
+ }
268
+ }
269
+
270
+ const Theme = Static($Theme);
271
+ expect(Theme.$accent).toBe('blue');
272
+
273
+ class DarkTheme extends Theme {
274
+ static override get $accent() {
275
+ return 'violet';
276
+ }
277
+ }
278
+
279
+ // the subclass's own getter descriptor shadows the caching wrapper
280
+ expect(DarkTheme.$accent).toBe('violet');
281
+ expect(Theme.$accent).toBe('blue');
282
+ });
283
+
284
+ it('method binding is order-correct: parent read first, child dispatch intact', () => {
285
+ class $Render {
286
+ static get glyph() {
287
+ return '-'; // a knob subclasses pinch
288
+ }
289
+ static paint() {
290
+ return this.glyph.repeat(3);
291
+ }
292
+ }
293
+
294
+ const Render = Static($Render);
295
+ class DottedRender extends Render {
296
+ static override get glyph() {
297
+ return '.';
298
+ }
299
+ }
300
+
301
+ // PARENT reads first — the named-own-property shape poisoned this order
302
+ const parentPaint = Render.paint;
303
+ expect(parentPaint()).toBe('---');
304
+ expect(DottedRender.paint()).toBe('...'); // child binds itself, chain not shadowed
305
+ expect(DottedRender.paint).toBe(DottedRender.paint); // still identity-stable
306
+ expect(Render.paint).toBe(parentPaint);
307
+ });
308
+
309
+ it('method binding is order-correct: child read first, parent unaffected', () => {
310
+ class $Render {
311
+ static get glyph() {
312
+ return '-';
313
+ }
314
+ static paint() {
315
+ return this.glyph.repeat(2);
316
+ }
317
+ }
318
+
319
+ const Render = Static($Render);
320
+ class DottedRender extends Render {
321
+ static override get glyph() {
322
+ return '.';
323
+ }
324
+ }
325
+
326
+ expect(DottedRender.paint()).toBe('..');
327
+ expect(Render.paint()).toBe('--');
328
+ });
329
+
330
+ it('composes with Reactive(): Static(Reactive($Class)) grants both contracts', () => {
331
+ let derivations = 0;
332
+
333
+ class $Channel {
334
+ get status() {
335
+ return ref('idle');
336
+ }
337
+ announce() {
338
+ return `channel:${this.status.value}`;
339
+ }
340
+ static get $defaults() {
341
+ derivations++;
342
+ return { retries: 3 };
343
+ }
344
+ static open() {
345
+ return this.$defaults.retries;
346
+ }
347
+ }
348
+
349
+ const Channel = Static(Reactive($Channel));
350
+
351
+ // instance side: Reactive semantics intact through the Static subclass
352
+ const channel: any = new Channel();
353
+ const statusCell = channel.status;
354
+ expect(statusCell.value).toBe('idle');
355
+ expect(channel.status).toBe(statusCell); // cached ref cell
356
+ expect(channel.announce()).toBe('channel:idle');
357
+
358
+ // static side: $-cache + bound methods from Static
359
+ expect(Channel.$defaults).toBe(Channel.$defaults);
360
+ expect(derivations).toBe(1);
361
+ const open = Channel.open;
362
+ expect(open()).toBe(3); // detached, still bound
363
+ });
364
+
365
+ it('walks the raw inheritance chain — ancestor $-getters cache per receiver', () => {
366
+ class $Base {
367
+ static get scale() {
368
+ return 1;
369
+ }
370
+ static get $metrics() {
371
+ return { unit: this.scale * 8 };
372
+ }
373
+ }
374
+ class $Wide extends $Base {
375
+ static override get scale() {
376
+ return 3;
377
+ }
378
+ }
379
+
380
+ const Wide = Static($Wide);
381
+
382
+ expect(Wide.$metrics.unit).toBe(24); // derived through the override
383
+ expect(Wide.$metrics).toBe(Wide.$metrics); // and cached
384
+ });
385
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ivue",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
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",