ivue 2.2.1 → 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 +130 -60
- package/lib/__tests__/Static.vitest.spec.ts +64 -0
- package/package.json +6 -3
- package/skills/ivue/SKILL.md +128 -9
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
|
-
<
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
- **
|
|
107
|
-
|
|
108
|
-
- **
|
|
109
|
-
|
|
110
|
-
- **
|
|
111
|
-
-
|
|
112
|
-
- **
|
|
113
|
-
|
|
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 ms — 6 to 132× faster than the alternatives.
|
|
88
|
+
- **One kilobyte** — 1.1 kB gzipped, zero dependencies, 100% test coverage.
|
|
89
|
+
Stripped to the load-bearing core — an 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
|
|
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 —
|
|
147
|
-
|
|
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.
|
|
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)
|
|
@@ -362,6 +362,70 @@ describe('Static $-cached getters', () => {
|
|
|
362
362
|
expect(open()).toBe(3); // detached, still bound
|
|
363
363
|
});
|
|
364
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
|
+
|
|
365
429
|
it('walks the raw inheritance chain — ancestor $-getters cache per receiver', () => {
|
|
366
430
|
class $Base {
|
|
367
431
|
static get scale() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ivue",
|
|
3
|
-
"version": "2.2.
|
|
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
|
}
|
package/skills/ivue/SKILL.md
CHANGED
|
@@ -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(
|
|
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; //
|
|
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
|
|
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.
|