ivue 2.2.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 +20 -9
- package/dist/extras.cjs +1 -1
- package/dist/extras.es.js +17 -17
- package/lib/Reactive.ts +3 -3
- package/lib/Static.ts +36 -15
- package/lib/__tests__/Static.vitest.spec.ts +116 -11
- package/package.json +1 -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
|
@@ -8,15 +8,26 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Get-only static accessors whose name starts with `$` become
|
|
10
10
|
* compute-once-per-receiver caches: the getter body runs on first read
|
|
11
|
-
* through a given class, its
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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))`.
|
|
20
31
|
*
|
|
21
32
|
* This is the namespace pattern's backend adapter: canonical namespace +
|
|
22
33
|
* mutable `Class` slot + late reads, for STATELESS capability classes (a
|
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,27 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
const a = Object.hasOwn;
|
|
2
|
+
function u(s) {
|
|
3
|
+
const r = class extends s {
|
|
4
|
+
}, c = /* @__PURE__ */ new Set();
|
|
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
|
-
|
|
9
|
-
const t = Object.getOwnPropertyDescriptor(
|
|
9
|
+
c.add(e);
|
|
10
|
+
const t = Object.getOwnPropertyDescriptor(n, e);
|
|
10
11
|
if (typeof t.value == "function") {
|
|
11
|
-
const
|
|
12
|
-
Object.defineProperty(
|
|
13
|
-
|
|
14
|
-
return Object.defineProperty(this, e, { ...t, value: n }), n;
|
|
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
15
|
} });
|
|
16
16
|
} else if (t.get && !t.set && typeof e == "string" && e.startsWith("$")) {
|
|
17
|
-
const
|
|
18
|
-
Object.defineProperty(
|
|
19
|
-
return
|
|
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
20
|
} });
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
return
|
|
23
|
+
return r;
|
|
24
24
|
}
|
|
25
25
|
export {
|
|
26
|
-
|
|
26
|
+
u as Static
|
|
27
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
|
@@ -8,15 +8,26 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Get-only static accessors whose name starts with `$` become
|
|
10
10
|
* compute-once-per-receiver caches: the getter body runs on first read
|
|
11
|
-
* through a given class, its
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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))`.
|
|
20
31
|
*
|
|
21
32
|
* This is the namespace pattern's backend adapter: canonical namespace +
|
|
22
33
|
* mutable `Class` slot + late reads, for STATELESS capability classes (a
|
|
@@ -29,6 +40,8 @@
|
|
|
29
40
|
*/
|
|
30
41
|
export type ClassConstructor = new (...arguments_: any[]) => any;
|
|
31
42
|
|
|
43
|
+
const hasOwn = Object.hasOwn;
|
|
44
|
+
|
|
32
45
|
export function Static<Class extends ClassConstructor>(targetClass: Class): Class {
|
|
33
46
|
const SelectedClass = class extends targetClass {};
|
|
34
47
|
const visitedKeys = new Set<PropertyKey>();
|
|
@@ -46,14 +59,22 @@ export function Static<Class extends ClassConstructor>(targetClass: Class): Clas
|
|
|
46
59
|
|
|
47
60
|
if (typeof descriptor.value === 'function') {
|
|
48
61
|
const method = descriptor.value;
|
|
62
|
+
const bindKey =
|
|
63
|
+
typeof key === 'string'
|
|
64
|
+
? Symbol.for(`ivue.staticBound.${key}`)
|
|
65
|
+
: Symbol('ivue.staticBound');
|
|
49
66
|
|
|
50
67
|
Object.defineProperty(SelectedClass, key, {
|
|
51
68
|
configurable: true,
|
|
52
69
|
enumerable: descriptor.enumerable,
|
|
53
|
-
get(this:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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];
|
|
57
78
|
},
|
|
58
79
|
});
|
|
59
80
|
} else if (
|
|
@@ -69,10 +90,10 @@ export function Static<Class extends ClassConstructor>(targetClass: Class): Clas
|
|
|
69
90
|
configurable: true,
|
|
70
91
|
enumerable: descriptor.enumerable,
|
|
71
92
|
get(this: any) {
|
|
72
|
-
if (!
|
|
93
|
+
if (!hasOwn(this, cacheKey)) {
|
|
73
94
|
Object.defineProperty(this, cacheKey, {
|
|
74
95
|
configurable: true,
|
|
75
|
-
value:
|
|
96
|
+
value: getter.call(this),
|
|
76
97
|
});
|
|
77
98
|
}
|
|
78
99
|
return this[cacheKey];
|
|
@@ -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() {
|
|
@@ -166,21 +185,26 @@ describe('Static $-cached getters', () => {
|
|
|
166
185
|
expect(computeRuns).toBe(2); // once per receiver, never shared
|
|
167
186
|
});
|
|
168
187
|
|
|
169
|
-
it('
|
|
170
|
-
class $
|
|
171
|
-
static get $
|
|
172
|
-
return
|
|
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 };
|
|
173
195
|
}
|
|
174
196
|
}
|
|
175
197
|
|
|
176
|
-
const
|
|
177
|
-
const defaults = Config.$defaults;
|
|
198
|
+
const Wrap = Static($Wrap);
|
|
178
199
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
184
208
|
});
|
|
185
209
|
|
|
186
210
|
it('caches primitive results too', () => {
|
|
@@ -257,6 +281,87 @@ describe('Static $-cached getters', () => {
|
|
|
257
281
|
expect(Theme.$accent).toBe('blue');
|
|
258
282
|
});
|
|
259
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
|
+
|
|
260
365
|
it('walks the raw inheritance chain — ancestor $-getters cache per receiver', () => {
|
|
261
366
|
class $Base {
|
|
262
367
|
static get scale() {
|