ivue 2.2.0 → 2.2.2

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/README.md CHANGED
@@ -10,52 +10,35 @@
10
10
  <h3 align="center">Plain classes. Full reactivity.<br>Infinite scalability. One kilobyte.</h3>
11
11
 
12
12
  <p align="center">
13
- <a href="https://www.npmjs.com/package/ivue"><img src="https://img.shields.io/npm/v/ivue.svg" alt="npm"></a>
13
+ <a href="https://www.npmjs.com/package/ivue"><img src="https://img.shields.io/npm/v/ivue.svg" alt="npm version"></a>
14
+ <a href="https://www.npmjs.com/package/ivue"><img src="https://img.shields.io/npm/dm/ivue.svg" alt="npm downloads"></a>
14
15
  <a href="https://github.com/infinite-system/ivue/actions/workflows/ci.yml"><img src="https://github.com/infinite-system/ivue/actions/workflows/ci.yml/badge.svg?branch=main" alt="build status"></a>
16
+ <img src="https://img.shields.io/badge/coverage-100%25-34d399" alt="100% test coverage">
17
+ <img src="https://img.shields.io/badge/gzipped-1.1%20kB-818cf8" alt="1.1 kB gzipped">
18
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license"></a>
15
19
  </p>
16
20
 
17
21
  <p align="center">
18
- <strong>Docs: <a href="https://ivue.dev/">ivue.dev</a></strong>
22
+ <a href="https://ivue.dev/">Docs</a> ·
23
+ <a href="https://ivue.dev/guide/getting-started">Getting Started</a> ·
24
+ <a href="https://ivue.dev/guide/standard">The Standard</a> ·
25
+ <a href="https://ivue.dev/examples/">Examples</a> ·
26
+ <a href="https://ivue.dev/guide/benchmarks">Benchmarks</a> ·
27
+ <a href="https://ivue.dev/blog/">Blog</a> ·
28
+ <a href="https://ivue.dev/community">Community</a>
19
29
  </p>
20
30
 
31
+ ---
32
+
21
33
  ivue builds Vue 3 reactivity out of plain TypeScript classes. Real
22
34
  inheritance, real encapsulation, real polymorphism — on ordinary objects,
23
- with nothing paid until first access. The whole engine is **1.1kb gzipped**
35
+ with nothing paid until first access. The whole engine is **1.1 kB gzipped**
24
36
  with zero dependencies.
25
37
 
26
- - **Native class API** — `extends`, `super`, getters, setters, private
27
- fields. Real inheritance, encapsulation and polymorphism, all reactive.
28
- - **Zero-cost creation** — instances are plain objects. A million of them
29
- take 22 ms — 6 to 132× faster than the alternatives.
30
- - **One kilobyte** — 1.1kb gzipped, zero dependencies, 100% test coverage.
31
- Stripped to the load-bearing core — an API you can hold in your head.
32
- - **Store or ViewModel** — the same class serves as a global store, a
33
- component ViewModel, or a domain model. One mental model everywhere.
34
- - **Composition API, fully compatible** — composables plug in through
35
- `$`-getters. The entire Vue ecosystem works inside your classes.
36
- - **TypeScript first** — writable ref-getters, fully typed instances,
37
- precise inference. The type system shaped the engine's design.
38
-
39
- ## Getting started
40
-
41
38
  ```sh
42
39
  npm i ivue vue
43
40
  ```
44
41
 
45
- ## Built for humans and AI
46
-
47
- ivue ships with an [Operating Manual](https://ivue.dev/guide/standard) —
48
- the complete authoring standard as annotated templates, rules, and a review
49
- checklist. It reads as documentation and works as a drop-in skill for AI
50
- coding agents, so generated code follows the same standard your team writes:
51
-
52
- ```sh
53
- npx ivue skill # installs .claude/skills/ivue/SKILL.md, version-locked
54
- npx ivue skill --all # + Codex/Cursor/Copilot where already in use
55
- ```
56
-
57
- ### Usage
58
-
59
42
  ```ts
60
43
  import { Reactive } from 'ivue';
61
44
  import { ref } from 'vue';
@@ -79,8 +62,6 @@ export namespace Counter {
79
62
  }
80
63
  ```
81
64
 
82
- In a component:
83
-
84
65
  ```vue
85
66
  <script setup lang="ts">
86
67
  import { Counter } from './Counter';
@@ -98,19 +79,20 @@ const { count } = counter;
98
79
 
99
80
  Full walkthrough: [Getting Started](https://ivue.dev/guide/getting-started).
100
81
 
101
- ## Hard problems, solved together
102
-
103
- Each of these sank earlier class-reactivity attempts. ivue ships all of them
104
- as one coherent design:
82
+ ## Why classes, why now
105
83
 
106
- - **Bound methods** — `this.method` is always correct, always the same reference.
107
- - **Reactive inheritance** deep `super.x.value` chains resolve level-safe.
108
- - **Development parity** — the same class identity, direct binding, and engine branches in development and production.
109
- - **Circular import immunity** the namespace pattern resolves mutual references in any load order.
110
- - **Writable getter types** — ref-returning getters type as writable; instances fully inferred.
111
- - **Deterministic teardown** `$watch` scopes per instance, `$stopEffects()` cleans up.
112
- - **Minimal memory footprint** — derivations are shared prototype getters, not per-instance allocations.
113
- - **Hot paths** reads hoist to native ref speed with one line where it matters.
84
+ - **Native class API** — `extends`, `super`, getters, setters, private
85
+ fields. Real inheritance, encapsulation and polymorphism, all reactive.
86
+ - **Zero-cost creation** — instances are plain objects. A million of them
87
+ take 22 ms6 to 132× faster than the alternatives.
88
+ - **One kilobyte** — 1.1 kB gzipped, zero dependencies, 100% test coverage.
89
+ Stripped to the load-bearing corean API you can hold in your head.
90
+ - **Store or ViewModel** — the same class serves as a global store, a
91
+ component ViewModel, or a domain model. One mental model everywhere.
92
+ - **Composition API, fully compatible** — composables plug in through
93
+ `$`-getters. The entire Vue ecosystem works inside your classes.
94
+ - **TypeScript first** — writable ref-getters, fully typed instances,
95
+ precise inference. The type system shaped the engine's design.
114
96
 
115
97
  ## One idea, carried through
116
98
 
@@ -120,10 +102,8 @@ plain and re-derives on every read — reactive with zero allocation. Methods
120
102
  bind themselves once, to the right `this`. Instances stay ordinary objects:
121
103
  no proxy wraps them, no work happens at construction.
122
104
 
123
- Inheritance, teardown, development parity, speed — consequences of that one move.
124
-
125
- Composables plug straight in — the entire Vue ecosystem works inside your
126
- classes:
105
+ Inheritance, teardown, development parity, speed — consequences of that one
106
+ move. And composables plug straight in:
127
107
 
128
108
  ```ts
129
109
  import { useMouse } from '@vueuse/core';
@@ -141,10 +121,73 @@ class $Pointer {
141
121
  }
142
122
  ```
143
123
 
124
+ ## Hard problems, solved together
125
+
126
+ Each of these sank earlier class-reactivity attempts. ivue ships all of them
127
+ as one coherent design:
128
+
129
+ - **Bound methods** — `this.method` is always correct, always the same reference.
130
+ - **Reactive inheritance** — deep `super.x.value` chains resolve level-safe.
131
+ - **Development parity** — the same class identity, direct binding, and engine branches in development and production.
132
+ - **[Circular import immunity](https://ivue.dev/blog/circular-imports-dissolved)** — the namespace pattern resolves mutual references in any load order.
133
+ - **Writable getter types** — ref-returning getters type as writable; instances fully inferred.
134
+ - **Deterministic teardown** — `$watch` scopes per instance, `$stopEffects()` cleans up.
135
+ - **Minimal memory footprint** — derivations are shared prototype getters, not per-instance allocations.
136
+ - **Hot paths** — reads hoist to native ref speed with one line where it matters.
137
+
138
+ ## `Static()` — capability classes <sub>(`ivue/extras`, +0.5 kB)</sub>
139
+
140
+ The same discipline for the class-level surface:
141
+ [`Static()`](https://ivue.dev/guide/static) makes static methods lazy-bound
142
+ and `$`-prefixed static getters cached **per receiver** — a lazy singleton,
143
+ an inheritance-aware store, an override seam, and a test boundary in one
144
+ declaration. It retires
145
+ [module-level state](https://ivue.dev/blog/module-level-state) outright, and
146
+ it has no Vue dependency — the identical idiom runs under Node and Bun:
147
+
148
+ ```ts
149
+ import { Static } from 'ivue/extras';
150
+
151
+ class $TextSegmentation {
152
+ protected static get $segmenter() {
153
+ return new Intl.Segmenter(undefined, { granularity: 'grapheme' });
154
+ }
155
+ }
156
+
157
+ export namespace TextSegmentation {
158
+ export const $Class = Static($TextSegmentation); // anchor — children extend this
159
+ export let Class = $Class;
160
+ }
161
+ ```
162
+
163
+ ## Proven at scale
164
+
165
+ <p align="center">
166
+ <a href="https://ivue.dev/examples/invar">
167
+ <img src="docs_v2/public/invar-editor.svg" alt="Invar — a terminal IDE built on ivue" width="720">
168
+ </a>
169
+ </p>
170
+
171
+ [**Invar**](https://ivue.dev/examples/invar) is ivue at full scale: a
172
+ complete terminal IDE — editor, workspace search, git, terminals, LSP,
173
+ agents — running on ivue classes under Bun, with no DOM and no Vue
174
+ components. **94,000 source lines, 345 classes, 35 invariant contracts,
175
+ zero import cycles**, built almost entirely by AI agents holding the
176
+ [Standard](https://ivue.dev/guide/standard) as their base discipline.
177
+
178
+ And at the other end of scale on the web: a
179
+ [1,000,000-row virtual scroller](https://ivue.dev/examples/virtual-scroller),
180
+ a [20,000,000-cell flyweight grid](https://ivue.dev/examples/flyweight-grid)
181
+ at 4.7 bytes per live cell, and
182
+ [production-grade Quasar field components](https://ivue.dev/examples/choose-field) —
183
+ all with full source on the page.
184
+
144
185
  ## The numbers
145
186
 
146
- Measured, not promised — method and live in-browser benchmarks in
147
- [the docs](https://ivue.dev/guide/benchmarks).
187
+ Measured, not promised — every number carries its method, and the
188
+ load-bearing benchmarks
189
+ [run live in your browser](https://ivue.dev/guide/benchmarks) on the
190
+ shipped engine:
148
191
 
149
192
  | creating 1,000,000 instances | time | ivue is |
150
193
  | --- | --- | --- |
@@ -164,22 +207,49 @@ Taken all the way down: a fully reactive spreadsheet model holding
164
207
  floor — because in ivue, everything costs proportional to what's *observed*,
165
208
  nothing costs proportional to what *exists*.
166
209
 
210
+ ## Built for humans and AI
211
+
212
+ ivue ships with a
213
+ [Standard Operating Manual](https://ivue.dev/guide/standard) — the complete
214
+ authoring standard as annotated templates, rules, and a review checklist. It
215
+ reads as documentation and works as a drop-in skill for AI coding agents, so
216
+ generated code follows the same standard your team writes:
217
+
218
+ ```sh
219
+ npx ivue skill # installs .claude/skills/ivue/SKILL.md, version-locked
220
+ npx ivue skill --all # + Codex/Cursor/Copilot where already in use
221
+ ```
222
+
223
+ Agents holding the Standard have
224
+ [derived correct patterns its own author never wrote](https://ivue.dev/blog/patterns-the-author-never-wrote) —
225
+ the manual is a generator, not a catalog. The wider argument:
226
+ [Reactive framework for the AI era](https://ivue.dev/blog/reactive-framework-for-the-ai-era).
227
+
228
+ ## Go deeper
229
+
230
+ - [Fundamental Principles](https://ivue.dev/guide/principles) — the design, from first ideas
231
+ - [The Engine](https://ivue.dev/engine) — how the transform works, internals annotated
232
+ - [Advanced Patterns](https://ivue.dev/guide/namespace-pattern) — Namespace, Computed Seed, Keyed Version Signals, Flyweight, Static, Backend ivue
233
+ - [The Blog](https://ivue.dev/blog/) — 30 posts of measured argument, from [circular imports](https://ivue.dev/blog/circular-imports-dissolved) to [what JavaScript becomes](https://ivue.dev/blog/what-javascript-becomes)
234
+
235
+ ## Community
236
+
237
+ Questions, ideas, bug reports — there's no ticket queue, just the people who
238
+ write the code: [Discussions](https://github.com/infinite-system/ivue/discussions) ·
239
+ [Issues](https://github.com/infinite-system/ivue/issues) ·
240
+ [Discord](https://discord.gg/8MgZNsrfv) ·
241
+ [X @evgenykalash](https://x.com/evgenykalash) ·
242
+ [Community page](https://ivue.dev/community)
243
+
167
244
  ## A note on the size
168
245
 
169
- The complete engine is **1.1kb gzipped**: lazy prototype transformation,
246
+ The complete engine is **1.1 kB gzipped**: lazy prototype transformation,
170
247
  bound methods, inheritance, lifecycle ownership, and the public utilities.
171
248
  Development uses that same engine without a second hot-update execution path.
172
249
 
173
250
  > *Perfection is achieved, not when there is nothing more to add, but when
174
251
  > there is nothing left to take away.* — Antoine de Saint-Exupéry
175
252
 
176
- ## Documentation
177
-
178
- Full guide, principles, live benchmarks, advanced examples (a 1,000,000-row
179
- virtual scroller, a 20,000,000-cell flyweight grid, production-grade Quasar
180
- field components), and the API reference:
181
- **https://ivue.dev/**
182
-
183
253
  ## License
184
254
 
185
255
  [MIT](./LICENSE)
@@ -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
@@ -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 (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.
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"}),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};
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
- 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))
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
- s.add(e);
9
- const t = Object.getOwnPropertyDescriptor(r, e);
9
+ c.add(e);
10
+ const t = Object.getOwnPropertyDescriptor(n, e);
10
11
  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;
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 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];
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 o;
23
+ return r;
24
24
  }
25
25
  export {
26
- a as Static
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> = 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
@@ -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 (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.
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: ClassConstructor) {
54
- const boundMethod = method.bind(this);
55
- Object.defineProperty(this, key, { ...descriptor, value: boundMethod });
56
- return boundMethod;
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 (!Object.hasOwn(this, cacheKey)) {
93
+ if (!hasOwn(this, cacheKey)) {
73
94
  Object.defineProperty(this, cacheKey, {
74
95
  configurable: true,
75
- value: Object.freeze(getter.call(this)),
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('freezes cached valuesmutation throws instead of corrupting', () => {
170
- class $Config {
171
- static get $defaults() {
172
- return { width: 80 };
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 Config = Static($Config);
177
- const defaults = Config.$defaults;
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
178
203
 
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);
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,151 @@ 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
+
365
+ it('the anchor shape: $Class = Static($X), Class = Reactive($Class)', () => {
366
+ class $Panel {
367
+ constructor(public seed: number) {}
368
+ get width() {
369
+ return ref(this.seed);
370
+ }
371
+ static get glyph() {
372
+ return '#'; // live knob
373
+ }
374
+ static get $legend() {
375
+ return { marker: this.glyph.repeat(2) };
376
+ }
377
+ static describe() {
378
+ return this.$legend.marker;
379
+ }
380
+ }
381
+
382
+ const Anchor = Static($Panel);
383
+ const Panel = Reactive(Anchor);
384
+
385
+ // instance side: full Reactive semantics through the wrapper
386
+ const panel: any = new Panel(7);
387
+ const widthCell = panel.width;
388
+ expect(widthCell.value).toBe(7);
389
+ expect(panel.width).toBe(widthCell); // cached cell
390
+ panel.$stopEffects();
391
+ expect(panel.width).not.toBe(widthCell); // teardown reset works
392
+
393
+ // static side: caches + binding live on the same published class
394
+ expect(Panel.$legend).toBe(Panel.$legend);
395
+ const describe = Panel.describe;
396
+ expect(describe()).toBe('##');
397
+
398
+ // Reactive() is identity, so anchor and published class are one object
399
+ expect(Panel).toBe(Anchor);
400
+ });
401
+
402
+ it('a bare double extending the anchor inherits working semantics', () => {
403
+ class $Momentum {
404
+ static get friction() {
405
+ return 2; // knob
406
+ }
407
+ static get $atRest() {
408
+ return { threshold: this.friction * 10 };
409
+ }
410
+ static settle() {
411
+ return this.$atRest.threshold;
412
+ }
413
+ }
414
+
415
+ const Anchor = Static($Momentum);
416
+ // the downstream case: a pinch-knob double with NO wrap of its own
417
+ class TunedMomentum extends Anchor {
418
+ static override get friction() {
419
+ return 7;
420
+ }
421
+ }
422
+
423
+ expect(Anchor.$atRest.threshold).toBe(20); // parent reads FIRST
424
+ expect(TunedMomentum.$atRest.threshold).toBe(70); // child still derives itself
425
+ const settle = TunedMomentum.settle;
426
+ expect(settle()).toBe(70); // inherited method binds to the child, detached
427
+ });
428
+
260
429
  it('walks the raw inheritance chain — ancestor $-getters cache per receiver', () => {
261
430
  class $Base {
262
431
  static get scale() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ivue",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Infinite Vue – Class Based Architecture for Vue 3",
5
5
  "type": "module",
6
6
  "exports": {
@@ -37,10 +37,12 @@
37
37
  "cypress:headed": "cypress run --component --browser chrome --headed --no-exit",
38
38
  "build": "tsc --version;tsc --p ./tsconfig.json && vite build",
39
39
  "build:demo": "tsc --p ./tsconfig.json && vite build demo",
40
- "build:docs": "npm run sync:examples && npm --prefix docs_v2 run build",
40
+ "build:docs": "npm run sync:examples && npm --prefix docs_v2 run build && npm run check:links",
41
+ "check:links": "node docs_v2/scripts/check-links.mjs",
41
42
  "render:og": "node docs_v2/scripts/brand-image-generator.mjs og",
42
43
  "render:form-header": "node docs_v2/scripts/brand-image-generator.mjs form-header",
43
44
  "render:banner": "node docs_v2/scripts/brand-image-generator.mjs blog",
45
+ "sync:blog-dates": "node docs_v2/scripts/blog-dates-generator.mjs",
44
46
  "preview:demo": "npm run build:demo && vite preview demo --host",
45
47
  "preview:docs": "npm --prefix docs_v2 run preview",
46
48
  "release": "npm run build && npm publish",
@@ -134,5 +136,6 @@
134
136
  "license": "MIT",
135
137
  "bin": {
136
138
  "ivue": "bin/ivue.mjs"
137
- }
139
+ },
140
+ "packageManager": "npm@11.6.2"
138
141
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ivue
3
- description: Use when writing or editing ivue `Reactive()` classes, converting a Vue component or composable to ivue, or resolving any `.value`-in-template, `defineExpose`/`reactive()` instance-typing, `ReactiveInstance`/`Instance`, `$watch`/`$watchEffect`, or namespace-export question — the operating manual for Vue 3 class-based reactivity where state is ref-getters, derived values are plain getters, and Refs/Computeds are `.value` everywhere.
3
+ description: Use when writing or editing ivue `Reactive()` classes, converting a Vue component or composable to ivue, or resolving any `.value`-in-template, `defineExpose`/`reactive()` instance-typing, `ReactiveInstance`/`Instance`, `$watch`/`$watchEffect`, or namespace-export question, or any `Static()` capability-class / `$`-cached-static / anchor question — the operating manual for Vue 3 class-based reactivity where state is ref-getters, derived values are plain getters, and Refs/Computeds are `.value` everywhere.
4
4
  ---
5
5
 
6
6
  # ivue `Reactive`
@@ -30,6 +30,7 @@ path and skip the install; never add the dependency alongside a vendored copy.
30
30
 
31
31
  ```ts
32
32
  import { Reactive } from 'ivue'; // in this app: 'src/utils/ivue'
33
+ import { Static } from 'ivue/extras';
33
34
  import {
34
35
  ref,
35
36
  shallowRef,
@@ -42,6 +43,10 @@ import {
42
43
  import { useProjectStore } from 'src/stores/project.store';
43
44
 
44
45
  class $Box {
46
+ static get DEFAULT_HEIGHT() {
47
+ return 4;
48
+ }
49
+
45
50
  // Constructor runs SYNCHRONOUSLY where you `new` — in setup() that
46
51
  // means the constructor body IS setup code, and the whole toolbox
47
52
  // works here:
@@ -68,7 +73,7 @@ class $Box {
68
73
  // RAW: read AND write via .value. shallowRef for big structures you
69
74
  // REPLACE wholesale.
70
75
  get height() {
71
- return ref(4);
76
+ return ref((this.constructor as typeof $Box).DEFAULT_HEIGHT);
72
77
  }
73
78
  get rows() {
74
79
  return shallowRef<Row[]>([]);
@@ -146,11 +151,6 @@ class $Box {
146
151
  return this.$project.projectId;
147
152
  }
148
153
 
149
- // CONSTANTS / CONFIG — plain fields ONLY. A plain field written
150
- // from a method triggers NOTHING (no Ref/Computed, no dependency
151
- // edge). Never store mutable state here.
152
- baseWidth = 400;
153
-
154
154
  // METHODS — plain; engine-binds to raw (stable identity, safe as
155
155
  // handlers). Reactive-closure bodies above delegate HERE (the
156
156
  // thin-closure rule).
@@ -179,7 +179,7 @@ class $Box {
179
179
  }
180
180
 
181
181
  export namespace Box {
182
- export const $Class = $Box; // raw — children `extends` this
182
+ export const $Class = Static($Box); // static anchor — children `extends` this
183
183
  export let Class = Reactive($Class); // reactive — you `new` this
184
184
  // the type of every unwrapping surface (defineExpose, reactive())
185
185
  export type Instance = typeof Class.Instance;
@@ -421,6 +421,7 @@ session.dispose();
421
421
  | ✅ constructor runs init; register hooks/watchers there | ❌ add an `init()` method expecting auto-call — ivue never calls it |
422
422
  | ✅ plain `watch` in component-scoped constructors; `$watch` + a `$stopEffects` dispose path for outliving instances | ❌ default to `this.$watch` in a component-scoped class — its scope silently outlives unmount |
423
423
  | ✅ compose cleanup as an ordinary method — `dispose() { /* non-Vue cleanup */ this.$stopEffects(); }` | ❌ expect a teardown hook — ivue auto-calls NOTHING (no `init()`, no `stopEffects()`) |
424
+ | ✅ a class with static members anchors them: `const $Class = Static($X)` (`ivue/extras`) | ❌ `extends X.Class` — the mutable slot is an eager snapshot of one generation; always extend `$Class` |
424
425
 
425
426
  ## The unwrapping-surface typing invariant
426
427
 
@@ -498,6 +499,101 @@ safe without ordering discipline or `forwardRef`-style workarounds:
498
499
  stays impossible because it evaluates at load time and both parents cannot
499
500
  exist first.
500
501
 
502
+ ## `Static()` — the static-side sibling (from `ivue/extras`)
503
+
504
+ `Reactive()` owns instances. Stateless CAPABILITY classes — function bags for
505
+ files, git, parsers, clocks: never constructed, only called and swapped — use
506
+ `Static()` from the `ivue/extras` entry (separate, so core stays the engine):
507
+
508
+ - **Static methods bind lazily with stable identity** — detachable, safe as a
509
+ router/queue/listener callback, bound to the RECEIVING class.
510
+ - **Get-only statics named `$…` compute once PER RECEIVER.** The `$` prefix
511
+ promises stable identity, NOT immutability — a mutable memo table is a
512
+ legitimate `$`-cache. Non-`$` static getters stay LIVE: those are the knobs
513
+ test subclasses pinch.
514
+
515
+ THE ANCHOR RULE — a class that declares static members wraps them ONCE, at
516
+ `$Class`, so subclasses and test doubles inherit working semantics by
517
+ extending `$Class` bare:
518
+
519
+ ```ts
520
+ import { Static } from 'ivue/extras';
521
+
522
+ class $GitCommands {
523
+ static get binary() {
524
+ return 'git'; // LIVE knob — no $ prefix
525
+ }
526
+ static get $environment() {
527
+ return { LC_ALL: 'C' }; // computed once per receiver
528
+ }
529
+ static stage(path: string) {
530
+ return this.run(['add', '--', path]); // `this` = receiving class
531
+ }
532
+ }
533
+
534
+ export namespace GitCommands {
535
+ export const $Class = Static($GitCommands); // anchor — wrap HERE
536
+ export let Class = $Class; // selection — kernels/tests swap this
537
+ }
538
+ ```
539
+
540
+ Statics AND reactive instances on one class — anchor the statics, then
541
+ `Reactive()`:
542
+
543
+ ```ts
544
+ export namespace Settings {
545
+ export const $Class = Static($Settings);
546
+ export let Class = Reactive($Class); // in-place: Class === $Class
547
+ export type Instance = typeof Class.Instance;
548
+ }
549
+ ```
550
+
551
+ No static members → no wrapper: `$Class = $X`, the standard form unchanged.
552
+
553
+ ## Reading your own statics — the ladder
554
+
555
+ `Reactive(X) === X`, so a namespace's `Class` slot IS the base class. A getter
556
+ that reads statics through it therefore hard-binds to the base and silently
557
+ IGNORES a subclass override — the exact opposite of what a live (non-`$`)
558
+ static getter is for:
559
+
560
+ ```ts
561
+ // ❌ three members, a double cast, and the override never applies
562
+ protected get Tooltip() {
563
+ return Tooltip.Class as unknown as typeof $Tooltip;
564
+ }
565
+ public static get TOOLTIP_DWELL_SECONDS() { return 0.4; }
566
+ protected get tooltipDwellSeconds() {
567
+ return this.Tooltip.TOOLTIP_DWELL_SECONDS; // base value forever
568
+ }
569
+ ```
570
+
571
+ Measured: a subclass setting `0.1` still reads `0.4` through this shape.
572
+
573
+ Take the first rung that applies:
574
+
575
+ 1. **Nothing outside the instance reads it** → delete the static. A plain
576
+ instance getter is zero bytes per instance and natively overridable:
577
+ ```ts
578
+ protected get tooltipDwellSeconds() { return 0.4; }
579
+ ```
580
+ 2. **Something outside reads it** (tests pinching the knob, another class)
581
+ → keep the static and read it live off the receiver:
582
+ ```ts
583
+ protected get tooltipDwellSeconds() {
584
+ return (this.constructor as typeof $Tooltip).TOOLTIP_DWELL_SECONDS;
585
+ }
586
+ ```
587
+ `this.constructor` is the actual class: the subclass when subclassed, and
588
+ an engine class that INHERITS `$Class` for a plain reactive instance, so
589
+ statics resolve in both cases. TypeScript types `constructor` as
590
+ `Function`, so the one cast is required and is the honest cost.
591
+ 3. **Overriding must NOT happen** → name the class directly,
592
+ `$Tooltip.TOOLTIP_DWELL_SECONDS`, and let the code say so.
593
+
594
+ Never introduce a `protected get <ClassName>()` self-reference getter. It is a
595
+ cast wearing a getter costume: it looks live and is not.
596
+
501
597
  ## Generic classes (brief)
502
598
 
503
599
  `ReactiveClass<C>` cannot carry `<T>` through (no higher-kinded types), but
@@ -664,6 +760,27 @@ Contiguity says "same kind of thing"; a blank line says "the kind changes,
664
760
  or complexity rises." Spend the signal deliberately — a blanket
665
761
  newline-between-everything rule makes air mean nothing.
666
762
 
763
+ Class members use one order: static members → constructor → state getters →
764
+ prop getters → derived getters → methods. The constructor is the first
765
+ instance member. Comments and invariant annotations can precede the member
766
+ they describe.
767
+
768
+ Constants use one form per role:
769
+
770
+ | Role | Form |
771
+ | --- | --- |
772
+ | Tunable or overridable class constant | `static get SCREAMING_SNAKE_CASE()` |
773
+ | Protocol or byte constant on a hot path, never overridden | `static readonly SCREAMING_SNAKE_CASE` with a one-line hot-path comment |
774
+ | Contributor or pane identity data | Instance `readonly lowerCamelCase` field |
775
+ | Extensible constructed dependency | Field assigned from a prototype `createX()` factory method |
776
+ | Any other supposed constant | Defect: choose the real role or remove it |
777
+
778
+ Read live statics through the receiving class. JavaScript dispatches getter
779
+ and prototype method overrides while a parent constructor runs. A subclass
780
+ field initializer runs only after `super()` returns. It cannot change parent
781
+ construction. This mechanism makes getters safe tunables and `createX()`
782
+ methods safe construction seams.
783
+
667
784
  ```ts
668
785
  // state block — CONTIGUOUS: reads as the instance's STATE TABLE
669
786
  get sheet() {
@@ -708,7 +825,7 @@ convention and check it in review.
708
825
  ## Self-review checklist (run over your ivue diff)
709
826
 
710
827
  - [ ] Every mutable state member is `get x() { return ref(...) }` — no mutable plain fields.
711
- - [ ] Inside the class, every Ref/Computed read/write uses `.value`; plain fields are constants/config only.
828
+ - [ ] Inside the class, every Ref/Computed read/write uses `.value`; every plain field matches one role in the constants table.
712
829
  - [ ] Derived values are PLAIN getters; `computed()` appears only for expensive / render-suppressing / stable-handle cases.
713
830
  - [ ] Stores/composables are injected via `private get $store() { return useStore() }`, not field initializers.
714
831
  - [ ] The class is exported through the namespace (`$Class` / `Class = Reactive($Class)` / `Instance`); generics cast `Class` and hand-apply `ReactiveInstance` to `Instance<T>`.
@@ -723,4 +840,6 @@ convention and check it in review.
723
840
  - [ ] Every `computed()`/constructor-watch CALLBACK delegates to a method (`computed(() => this.recalculate())`) — no logic inlined in reactive closures; the arrow form, never `computed(this.method)`.
724
841
  - [ ] Identifiers are unfolded to domain words (`row`/`col`/`cell`/`cellValue`/`versionRef`…), loop indices and specs included — no single-letter names, no name meaning different things in different methods.
725
842
  - [ ] Keyed/sparse state uses the Map-of-refs shape (get-or-create on read, peek-only bump on write, explicit release path) — never one getter per key, never a deep `reactive()` collection.
843
+ - [ ] Static members are anchored (`const $Class = Static($X)`); `$`-prefixed static getters are compute-once-per-receiver caches, non-`$` statics stay live knobs, and inheritance extends `$Class` — never the mutable `Class`.
844
+ - [ ] Static members precede the constructor; the constructor precedes state, prop, and derived getters; methods come last.
726
845
  - [ ] Spacing carries meaning: declaration-like getters contiguous within their group; blank lines only where a doc comment / multi-line body / category boundary begins; methods always separated.