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.
- package/dist/Reactive.d.ts +3 -1
- package/dist/Static.d.ts +23 -0
- package/dist/extras.cjs +1 -1
- package/dist/extras.es.js +20 -15
- package/lib/Reactive.ts +3 -3
- package/lib/Static.ts +69 -12
- package/lib/__tests__/Static.vitest.spec.ts +286 -0
- package/package.json +2 -1
package/dist/Reactive.d.ts
CHANGED
|
@@ -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> =
|
|
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"})
|
|
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
|
-
|
|
2
|
-
|
|
1
|
+
const a = Object.hasOwn;
|
|
2
|
+
function u(s) {
|
|
3
|
+
const r = class extends s {
|
|
3
4
|
}, c = /* @__PURE__ */ new Set();
|
|
4
|
-
for (let
|
|
5
|
-
for (const
|
|
6
|
-
if (c.has(
|
|
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(
|
|
9
|
-
const
|
|
10
|
-
if (typeof
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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> =
|
|
438
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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",
|