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 +12 -0
- package/dist/extras.cjs +1 -1
- package/dist/extras.es.js +22 -17
- package/lib/Static.ts +47 -11
- package/lib/__tests__/Static.vitest.spec.ts +181 -0
- package/package.json +2 -1
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(
|
|
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
|
|
2
|
-
const
|
|
3
|
-
},
|
|
4
|
-
for (let
|
|
5
|
-
for (const
|
|
6
|
-
if (
|
|
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
|
-
|
|
9
|
-
const
|
|
10
|
-
if (typeof
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
23
|
+
return o;
|
|
19
24
|
}
|
|
20
25
|
export {
|
|
21
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
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",
|