ivue 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/LazyShared.d.ts +4 -4
- package/dist/Reactive.d.ts +18 -8
- package/dist/clone.d.ts +3 -0
- package/dist/extras.cjs +1 -1
- package/dist/extras.d.ts +1 -0
- package/dist/extras.es.js +1 -49
- package/dist/index.cjs +1 -1
- package/dist/index.es.js +1 -103
- package/dist/nestedProps.d.ts +67 -0
- package/lib/LazyShared.ts +4 -4
- package/lib/Reactive.ts +32 -19
- package/lib/Static.ts +16 -0
- package/lib/__tests__/Reactive.vitest.spec.ts +45 -1
- package/lib/__tests__/nestedProps.vitest.spec.ts +153 -0
- package/lib/clone.ts +21 -0
- package/lib/extras.ts +1 -0
- package/lib/nestedProps.ts +126 -0
- package/package.json +8 -4
- package/skills/ivue/SKILL.md +236 -102
- package/skills/ivue/constitution.test.ts +2 -2
- package/skills/ivue/ivue-docs-skip.json +37 -0
- package/skills/ivue/ivue-generator-standard.ts +24 -24
- package/skills/ivue/ivue-house-gate.ts +5 -4
- package/skills/ivue/ivue-standards-check.ts +485 -300
package/skills/ivue/SKILL.md
CHANGED
|
@@ -270,31 +270,50 @@ defineExpose(box as Box.Instance);
|
|
|
270
270
|
</template>
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
-
## The
|
|
274
|
-
|
|
275
|
-
A class FILE
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
-
|
|
293
|
-
|
|
273
|
+
## The class carries the WHOLE contract; the namespace is identity and types
|
|
274
|
+
|
|
275
|
+
A class FILE is a SINGLE-FILE MODEL — the model-side twin of the
|
|
276
|
+
single-file component. It has exactly three residents: imports, the
|
|
277
|
+
class, the namespace. The component contract — prop types, prop defaults, their
|
|
278
|
+
fusion, emits, and every tuning constant — lives ON THE CLASS as static
|
|
279
|
+
getters, beside the state and behavior it governs. The namespace holds
|
|
280
|
+
identity and TYPES only, every type DERIVED from `$Class`. Two worlds
|
|
281
|
+
would make a class half extensible: a `const` in a namespace cannot be
|
|
282
|
+
overridden by a subclass, is not inherited, and does not swap with
|
|
283
|
+
`Class` under a global override — so a runtime declaration never lives
|
|
284
|
+
there.
|
|
285
|
+
|
|
286
|
+
- **Contract (on the class, static)** — `static get propsTypes()`
|
|
287
|
+
(defineComponent-style, no defaults, returned through
|
|
288
|
+
`definePropTypes({...})` so the `required: true` literal survives
|
|
289
|
+
`typeof`); `static get propsDefaults()` (plain values, annotated
|
|
290
|
+
`ExtractPropDefaultTypes<typeof $X.propsTypes>` — required props are
|
|
291
|
+
filtered out of the check automatically, and a deliberately
|
|
292
|
+
default-free optional prop is declared `key: undefined`, stating the
|
|
293
|
+
ruling in data); `static get props()` — the ONE fusion line,
|
|
294
|
+
`propsWithDefaults(this.propsDefaults, this.propsTypes)`, reading
|
|
295
|
+
through the receiver so a subclass's `props` fuses ITS types and
|
|
296
|
+
defaults; `static get emits()` (object-declared validators). Tuning
|
|
297
|
+
constants are plain static getters too — live knobs a subclass or test
|
|
298
|
+
double overrides; the `$` prefix stays reserved for compute-once caches.
|
|
299
|
+
Types and defaults stay two members ON PURPOSE: a variant re-tunes
|
|
300
|
+
defaults without re-typing. A nested object prop (a knobs tree) is
|
|
301
|
+
filled from the defaults at every depth with `nestedProps(props,
|
|
302
|
+
this.self.propsDefaults)` from `ivue/extras`, once, in the
|
|
303
|
+
constructor (in place — lodash's `defaultsDeep` with arrays taken
|
|
304
|
+
whole); the class reads complete props and never merges in a getter.
|
|
305
|
+
Vue itself never merges a supplied object with its default.
|
|
306
|
+
- **Identity (namespace)** — `$Class` (raw, for children to extend),
|
|
307
|
+
`Class` (`Reactive()`, for you to `new`), `Instance` (and `Model` when
|
|
308
|
+
used).
|
|
309
|
+
- **Types (namespace)** — DERIVED from the class, never hand-duplicated:
|
|
310
|
+
`Props` is `ExtractPropTypes<typeof $Class.props>` (a generic component
|
|
294
311
|
grafts its parameter back over the one prop a runtime map cannot
|
|
295
|
-
carry: `Omit<ExtractPropTypes<typeof props>, 'modelValue'> &
|
|
296
|
-
{ modelValue: T[] }`); `Emits` is `ExtractEmitTypes<typeof
|
|
297
|
-
`Slots`; `Exposed` is `ShallowUnwrapRef<Instance>`.
|
|
312
|
+
carry: `Omit<ExtractPropTypes<typeof $Class.props>, 'modelValue'> &
|
|
313
|
+
{ modelValue: T[] }`); `Emits` is `ExtractEmitTypes<typeof
|
|
314
|
+
$Class.emits>`; `Slots`; `Exposed` is `ShallowUnwrapRef<Instance>`.
|
|
315
|
+
Domain types the contract refers to (an item shape, a variant preset)
|
|
316
|
+
live here as namespace types; the class reads them as `X.Item`.
|
|
298
317
|
|
|
299
318
|
Combined — the canonical file, everything above in one shape:
|
|
300
319
|
|
|
@@ -308,10 +327,63 @@ import {
|
|
|
308
327
|
type ExtractEmitTypes,
|
|
309
328
|
type ExtractPropDefaultTypes,
|
|
310
329
|
} from 'ivue';
|
|
330
|
+
import { Static } from 'ivue/extras';
|
|
311
331
|
|
|
312
332
|
class $Box {
|
|
313
|
-
|
|
314
|
-
|
|
333
|
+
/* Contract — STATIC: owned by the class, extended with `super` */
|
|
334
|
+
|
|
335
|
+
/** 1 — the TYPES: a defineComponent-style object, no defaults inside.
|
|
336
|
+
* definePropTypes is an identity call that keeps `required: true` a
|
|
337
|
+
* LITERAL — a bare object widens it to boolean, which would blind the
|
|
338
|
+
* defaults check below. */
|
|
339
|
+
static get propsTypes() {
|
|
340
|
+
return definePropTypes({
|
|
341
|
+
title: { type: String as PropType<string>, required: true },
|
|
342
|
+
size: { type: Number as PropType<number> },
|
|
343
|
+
maxHeight: { type: Number as PropType<number> },
|
|
344
|
+
disabled: { type: Boolean as PropType<boolean> },
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 2 — the DEFAULTS: plain values, typed against the types object.
|
|
349
|
+
* Required props (`title`) are filtered out of the check
|
|
350
|
+
* automatically; every OPTIONAL prop must appear — `undefined` is the
|
|
351
|
+
* explicit "no default ON PURPOSE" ruling, stated in data. */
|
|
352
|
+
static get propsDefaults(): ExtractPropDefaultTypes<typeof $Box.propsTypes> {
|
|
353
|
+
return {
|
|
354
|
+
size: this.defaultSize,
|
|
355
|
+
maxHeight: undefined, // unset = unbounded — deliberately default-free
|
|
356
|
+
disabled: false,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 3 — the FUSION: a standard Vue props object, ready for defineProps.
|
|
361
|
+
* Reads through the receiver — a subclass's `props` fuses ITS own
|
|
362
|
+
* types and defaults. Written once per hierarchy; a subclass that ADDS
|
|
363
|
+
* props re-declares this one line so its derived `Props` widens (a
|
|
364
|
+
* static's return type is not polymorphic). */
|
|
365
|
+
static get props() {
|
|
366
|
+
return propsWithDefaults(this.propsDefaults, this.propsTypes);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
static get emits() {
|
|
370
|
+
return {
|
|
371
|
+
close: (title: string) => true,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** A tuning constant: a LIVE static knob (no `$`), overridable. */
|
|
376
|
+
static get defaultSize() {
|
|
377
|
+
return 400;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** The one cast per class: instance code reads its own statics here. */
|
|
381
|
+
protected get self() {
|
|
382
|
+
return this.constructor as typeof $Box;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Type positions resolve non-positionally — the class names its own
|
|
386
|
+
// namespace's derived types freely.
|
|
315
387
|
constructor(
|
|
316
388
|
public props: Box.Props,
|
|
317
389
|
public emit: Box.Emits,
|
|
@@ -333,48 +405,14 @@ class $Box {
|
|
|
333
405
|
export namespace Box {
|
|
334
406
|
/* Identity */
|
|
335
407
|
|
|
336
|
-
export const $Class = $Box; //
|
|
408
|
+
export const $Class = Static($Box); // anchor — it declares statics; children `extends` this
|
|
337
409
|
export let Class = Reactive($Class); // reactive — you `new` this
|
|
338
410
|
export type Instance = typeof Class.Instance; // defineExpose type & reactive() interop
|
|
339
411
|
|
|
340
|
-
/*
|
|
341
|
-
|
|
342
|
-
// A value the module keeps to itself: NON-EXPORTED — private to the
|
|
343
|
-
// file, invisible to importers. Never a module-level const.
|
|
344
|
-
const DEFAULT_SIZE = 400;
|
|
345
|
-
|
|
346
|
-
/** 1 — the TYPES: a defineComponent-style object, no defaults inside.
|
|
347
|
-
* definePropTypes is an identity call that keeps `required: true` a
|
|
348
|
-
* LITERAL — a bare const widens it to boolean, which would blind the
|
|
349
|
-
* defaults check below. */
|
|
350
|
-
export const propsTypes = definePropTypes({
|
|
351
|
-
title: { type: String as PropType<string>, required: true },
|
|
352
|
-
size: { type: Number as PropType<number> },
|
|
353
|
-
maxHeight: { type: Number as PropType<number> },
|
|
354
|
-
disabled: { type: Boolean as PropType<boolean> },
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
/** 2 — the DEFAULTS: plain values, typed against the types object.
|
|
358
|
-
* Required props (`title`) are filtered out of the check
|
|
359
|
-
* automatically; every OPTIONAL prop must appear — `undefined` is the
|
|
360
|
-
* explicit "no default ON PURPOSE" ruling, stated in data. */
|
|
361
|
-
export const propsDefaults: ExtractPropDefaultTypes<typeof propsTypes> = {
|
|
362
|
-
size: DEFAULT_SIZE,
|
|
363
|
-
maxHeight: undefined, // unset = unbounded — deliberately default-free
|
|
364
|
-
disabled: false,
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
/** 3 — the MERGE: a standard Vue props object, ready for defineProps. */
|
|
368
|
-
export const props = propsWithDefaults(propsDefaults, propsTypes);
|
|
412
|
+
/* Types — DERIVED from the class's statics, never hand-duplicated */
|
|
369
413
|
|
|
370
|
-
export
|
|
371
|
-
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
/* Types — DERIVED from the values, never hand-duplicated */
|
|
375
|
-
|
|
376
|
-
export type Props = ExtractPropTypes<typeof props>;
|
|
377
|
-
export type Emits = ExtractEmitTypes<typeof emits>;
|
|
414
|
+
export type Props = ExtractPropTypes<typeof $Class.props>;
|
|
415
|
+
export type Emits = ExtractEmitTypes<typeof $Class.emits>;
|
|
378
416
|
|
|
379
417
|
export interface Slots {
|
|
380
418
|
default: (scope: { title: string }) => any;
|
|
@@ -385,37 +423,63 @@ export namespace Box {
|
|
|
385
423
|
}
|
|
386
424
|
```
|
|
387
425
|
|
|
388
|
-
The SFC is pure wiring against the seam
|
|
389
|
-
|
|
426
|
+
The SFC is pure wiring against the seam, and it reads the contract
|
|
427
|
+
through `Class` — the mutable slot — so a global override swaps the
|
|
428
|
+
contract together with the runner. The macros receive RUNTIME objects,
|
|
429
|
+
so no compiler macro ever resolves a cross-file type:
|
|
390
430
|
|
|
391
431
|
```ts
|
|
392
|
-
const props = defineProps(Box.props); // non-generic: the type is inferred
|
|
393
|
-
const emit = defineEmits(Box.emits) as Box.Emits;
|
|
432
|
+
const props = defineProps(Box.Class.props); // non-generic: the type is inferred
|
|
433
|
+
const emit = defineEmits(Box.Class.emits) as Box.Emits;
|
|
394
434
|
defineSlots<Box.Slots>();
|
|
395
435
|
// generic components cast the one graft:
|
|
396
|
-
// defineProps(X.props) as unknown as X.Props<T>
|
|
436
|
+
// defineProps(X.Class.props) as unknown as X.Props<T>
|
|
397
437
|
```
|
|
398
438
|
|
|
399
|
-
A subclass
|
|
400
|
-
|
|
401
|
-
|
|
439
|
+
A subclass extends its contract the way it extends behavior — with
|
|
440
|
+
`super`, overriding only what defines the specialization, with the
|
|
441
|
+
reason on the line. Re-tuning a default needs ONE override; adding a
|
|
442
|
+
prop needs the types override plus the one-line `props` re-declaration:
|
|
402
443
|
|
|
403
444
|
```ts
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
445
|
+
class $CardBox extends Box.$Class {
|
|
446
|
+
static override get propsDefaults(): typeof Box.$Class.propsDefaults {
|
|
447
|
+
return {
|
|
448
|
+
...super.propsDefaults,
|
|
449
|
+
size: 300, // cards are hundreds of px wide; rows were tens tall
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
class $TaggedBox extends Box.$Class {
|
|
455
|
+
static override get propsTypes() {
|
|
456
|
+
return definePropTypes({
|
|
457
|
+
...super.propsTypes,
|
|
458
|
+
tag: { type: String as PropType<string> },
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
static override get propsDefaults(): ExtractPropDefaultTypes<typeof $TaggedBox.propsTypes> {
|
|
462
|
+
return { ...super.propsDefaults, tag: '' };
|
|
463
|
+
}
|
|
464
|
+
static override get props() {
|
|
465
|
+
return propsWithDefaults(this.propsDefaults, this.propsTypes); // widens TaggedBox.Props
|
|
466
|
+
}
|
|
467
|
+
}
|
|
410
468
|
```
|
|
411
469
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
`
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
470
|
+
The anchor rule is unchanged and now reaches every component class: a
|
|
471
|
+
class that DECLARES statics — and the contract is statics — anchors at
|
|
472
|
+
`$Class` with `Static()` (`export const $Class = Static($Box)`), and so
|
|
473
|
+
does a subclass that overrides one. A subclass that only inherits stays
|
|
474
|
+
raw. The anchor costs nothing on getters (native reads) and is what
|
|
475
|
+
gives a `$`-cached static its compute-once semantics.
|
|
476
|
+
|
|
477
|
+
**One seam, any size.** A contract of forty documented props is still
|
|
478
|
+
authored on its class — a static getter scrolls like any other member,
|
|
479
|
+
and a sibling `XProps.ts` would be the parallel world again (a second
|
|
480
|
+
runtime owner the class mechanics cannot reach). Shared base surfaces
|
|
481
|
+
are a base CLASS (`class $ChooseField extends Field.$Class`), never a
|
|
482
|
+
spread-in const: inheritance is the only composition the contract uses.
|
|
419
483
|
|
|
420
484
|
**Overrides say so out loud.** `noImplicitOverride` is on: every member
|
|
421
485
|
that overrides a base member carries the `override` keyword
|
|
@@ -460,6 +524,17 @@ belong in plain getters, setup work belongs in the constructor, and event
|
|
|
460
524
|
handlers belong in methods — even when the handler only normalizes a DOM event
|
|
461
525
|
before delegating to a domain model.
|
|
462
526
|
|
|
527
|
+
**One DOM event, one handler, named for the event.** A template never binds
|
|
528
|
+
two events to the same method (`@pointerup="x.onUp" @pointercancel="x.onUp"`),
|
|
529
|
+
and a class never registers one method for two event types. A cancel gets
|
|
530
|
+
`onPointerCancel`, whose body may be one line delegating to `onPointerUp`;
|
|
531
|
+
a track's `touchstart` and `touchmove` get `onTrackTouchStart` and
|
|
532
|
+
`onTrackTouchMove` even when both only claim the touch. The reason is the
|
|
533
|
+
override seam: a subclass that must treat a cancel differently can override
|
|
534
|
+
`onPointerCancel` alone, where a shared handler would make it re-derive
|
|
535
|
+
which event it is handling from the event object — and the standard's
|
|
536
|
+
whole point is that behavior extends by name.
|
|
537
|
+
|
|
463
538
|
When building on a class-backed component, **extend its class, not its
|
|
464
539
|
`<script setup>`**. Add behavior to the existing class when it belongs to the
|
|
465
540
|
same component contract. When it is a real specialization, subclass the raw
|
|
@@ -527,7 +602,10 @@ call site. Rules that keep it clean:
|
|
|
527
602
|
In ordinary Vue this discipline costs a `computed()` per condition, so
|
|
528
603
|
nobody keeps it; here a named plain getter costs zero bytes, so there is
|
|
529
604
|
no excuse. Templates read as prose: bindings, names, and events — never
|
|
530
|
-
expressions.
|
|
605
|
+
expressions. The split is stage directions and script: the template says
|
|
606
|
+
who is on stage and what happens when someone acts; the class says what
|
|
607
|
+
everything MEANS. Structure stays in the template, meaning moves to the
|
|
608
|
+
class.
|
|
531
609
|
- **The rule covers EVERY binding kind, not just `v-if`** — the common
|
|
532
610
|
leaks are display strings, disabled states, and class objects:
|
|
533
611
|
|
|
@@ -553,6 +631,8 @@ created in a callback — watchers go in the instance's OWN scope, and the
|
|
|
553
631
|
owner of its lifetime disposes it:
|
|
554
632
|
|
|
555
633
|
```ts
|
|
634
|
+
import { Reactive, type ReactiveHelpers } from 'ivue';
|
|
635
|
+
|
|
556
636
|
class $Session {
|
|
557
637
|
get user() {
|
|
558
638
|
return ref<User | null>(null);
|
|
@@ -615,6 +695,12 @@ export namespace Session {
|
|
|
615
695
|
export type Instance = typeof Class.Instance;
|
|
616
696
|
}
|
|
617
697
|
|
|
698
|
+
// The engine installs $watch / $watchEffect / $stopEffects at Reactive(),
|
|
699
|
+
// AFTER the class body was typed — so a class that calls them merges the
|
|
700
|
+
// helpers into its own instance type. One line, zero runtime; the gate
|
|
701
|
+
// reads it as the class's second half, never as a stray type.
|
|
702
|
+
interface $Session extends ReactiveHelpers {}
|
|
703
|
+
|
|
618
704
|
// The owner disposes — the class's own method, like any other:
|
|
619
705
|
session.dispose();
|
|
620
706
|
```
|
|
@@ -636,6 +722,7 @@ session.dispose();
|
|
|
636
722
|
| ✅ `defineExpose(box as X.Instance)` | ❌ `defineExpose(box)` raw — readonly-accessor writes will type-error for consumers |
|
|
637
723
|
| ✅ constructor runs init; register hooks/watchers there | ❌ add an `init()` method expecting auto-call — ivue never calls it |
|
|
638
724
|
| ✅ 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 |
|
|
725
|
+
| ✅ a class that calls `this.$watch` / `$watchEffect` / `$stopEffects` merges the engine's helpers beside itself: `interface $X extends ReactiveHelpers {}` (one line, zero runtime) | ❌ `(this as any).$watch(...)` or per-member `declare $watch: …` lines — the body should typecheck without a cast |
|
|
639
726
|
| ✅ compose cleanup as an ordinary method — `dispose() { /* non-Vue cleanup */ this.$stopEffects(); }` | ❌ expect a teardown hook — ivue auto-calls NOTHING (no `init()`, no `stopEffects()`) |
|
|
640
727
|
| ✅ 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` |
|
|
641
728
|
| ✅ `protected` for every internal member — subclasses reach every seam | ❌ `private` anywhere in an ivue class — it forbids only the legitimate extender |
|
|
@@ -755,8 +842,21 @@ threads one object through every component and constructor signature it
|
|
|
755
842
|
crosses; the store pattern deletes the thread.
|
|
756
843
|
|
|
757
844
|
```ts
|
|
758
|
-
// app/AppStore.ts — the store IS an ivue class;
|
|
845
|
+
// app/AppStore.ts — the store IS an ivue class; a static owns the singleton
|
|
846
|
+
// (imports: Reactive from 'ivue'; Static from 'ivue/extras')
|
|
759
847
|
class $AppStore {
|
|
848
|
+
// The ONE instance, as a `$`-static: constructed on first read, after
|
|
849
|
+
// the app exists, and cached on the receiver. It constructs through the
|
|
850
|
+
// namespace slot, so a test double swapped into `Class` is what gets
|
|
851
|
+
// built — the store has one receiver, the slot, so nothing forks.
|
|
852
|
+
protected static get $shared(): AppStore.Instance {
|
|
853
|
+
return new AppStore.Class();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
static use(): AppStore.Instance {
|
|
857
|
+
return this.$shared;
|
|
858
|
+
}
|
|
859
|
+
|
|
760
860
|
get authenticated() {
|
|
761
861
|
return ref(false);
|
|
762
862
|
}
|
|
@@ -767,14 +867,9 @@ class $AppStore {
|
|
|
767
867
|
}
|
|
768
868
|
|
|
769
869
|
export namespace AppStore {
|
|
770
|
-
export const $Class = $AppStore;
|
|
771
|
-
export let Class = Reactive($Class);
|
|
870
|
+
export const $Class = Static($AppStore); // anchor — it declares statics
|
|
871
|
+
export let Class = Reactive($Class); // reactive — use() does the one `new`
|
|
772
872
|
export type Instance = typeof Class.Instance;
|
|
773
|
-
|
|
774
|
-
let singleton: Instance | null = null;
|
|
775
|
-
export function use(): Instance {
|
|
776
|
-
return (singleton ??= new Class());
|
|
777
|
-
}
|
|
778
873
|
}
|
|
779
874
|
```
|
|
780
875
|
|
|
@@ -784,7 +879,7 @@ Consumers never receive it — they REACH for it:
|
|
|
784
879
|
// any model — the `$`-getter caches the store per instance, forever
|
|
785
880
|
class $SubscribersModel {
|
|
786
881
|
protected get $app() {
|
|
787
|
-
return AppStore.use();
|
|
882
|
+
return AppStore.Class.use();
|
|
788
883
|
}
|
|
789
884
|
|
|
790
885
|
async refresh() {
|
|
@@ -802,7 +897,7 @@ class $SubscribersModel {
|
|
|
802
897
|
// any component — call use() directly; no prop, no provide/inject
|
|
803
898
|
import { AppStore } from '../app/AppStore';
|
|
804
899
|
|
|
805
|
-
const app = AppStore.use();
|
|
900
|
+
const app = AppStore.Class.use();
|
|
806
901
|
const { authenticated } = app;
|
|
807
902
|
</script>
|
|
808
903
|
|
|
@@ -815,13 +910,25 @@ Why this shape and not alternatives:
|
|
|
815
910
|
|
|
816
911
|
- **`use()` is lazy** — the singleton constructs on first touch, after the
|
|
817
912
|
app exists, so module-load order and circular imports stay non-events
|
|
818
|
-
(the same late-read property as every cross-module reference).
|
|
913
|
+
(the same late-read property as every cross-module reference). It lives
|
|
914
|
+
in a `$`-static on the class — never a namespace `let`, which is a
|
|
915
|
+
parallel world no subclass can reach (the gate's
|
|
916
|
+
`the_namespace_holds_identity_and_types_only` check refuses it). A
|
|
917
|
+
`$`-static caches per receiver, and a store reached only through
|
|
918
|
+
`X.Class.use()` has one receiver, so nothing forks; `LazyShared` is for
|
|
919
|
+
a REGISTRY that several receivers (subclasses) must share.
|
|
819
920
|
- **The `$`-getter is the injection point** — cached whole, per instance,
|
|
820
921
|
on first read. A model names its dependency once; every method reads
|
|
821
922
|
`this.$app` with zero lookup cost and zero constructor plumbing.
|
|
923
|
+
- **A store may publish itself as a `reactive()` view** — `use()` stays
|
|
924
|
+
the one door; the `$`-static behind it returns
|
|
925
|
+
`reactive(new AppStore.Class() as AppStore.Instance)` and consumers read
|
|
926
|
+
and write with no `.value`. The `as Instance` cast is the interop form
|
|
927
|
+
the gate sanctions (it is what makes the unwrapped writes typecheck);
|
|
928
|
+
bare `reactive(new …)` is refused.
|
|
822
929
|
- **Tests swap the slot, not the callers** — `AppStore.Class = $TestStore`
|
|
823
|
-
before the first `use()`
|
|
824
|
-
gets the double through the same seam.
|
|
930
|
+
before the first `use()` and every consumer, calling
|
|
931
|
+
`AppStore.Class.use()`, gets the double through the same seam.
|
|
825
932
|
- A store is component-OUTLIVING by definition: watchers inside it use
|
|
826
933
|
`this.$watch`/`$watchEffect`, never plain `watch`, and lifecycle hooks
|
|
827
934
|
never belong in it.
|
|
@@ -1029,6 +1136,26 @@ export namespace Settings {
|
|
|
1029
1136
|
|
|
1030
1137
|
No static members → no wrapper: `$Class = $X`, the standard form unchanged.
|
|
1031
1138
|
|
|
1139
|
+
**Hot loops read the method through the accessor — hoist it, not the
|
|
1140
|
+
class.** The bound method itself is plain-function speed (measured,
|
|
1141
|
+
Chromium, 9M calls, fresh page per variant: module function 31.7 ms,
|
|
1142
|
+
hoisted bound method 30.0 ms); the ONLY per-call cost is re-reading it
|
|
1143
|
+
through the accessor inside the loop (84.6 ms same loop — the
|
|
1144
|
+
own-property guard that buys per-receiver binding). Ordinary call
|
|
1145
|
+
frequency never notices. In a million-call loop, destructure once,
|
|
1146
|
+
INSIDE the function:
|
|
1147
|
+
|
|
1148
|
+
```ts
|
|
1149
|
+
// one accessor read per method — a late read of the mutable slot,
|
|
1150
|
+
// so a swapped-in subclass is still honored
|
|
1151
|
+
const { isDataCol, numDataValue } = FlyweightLogic.Class;
|
|
1152
|
+
for (let row = 0; row < ROWS_1M; row++) sum += numDataValue(row, col) ?? 0;
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
Never hoist at module scope (captures today's `Class` forever, blind to
|
|
1156
|
+
swaps) and never reach for `$Class` as a "fast path" — the raw class
|
|
1157
|
+
skips per-receiver binding, which is the capability seam itself.
|
|
1158
|
+
|
|
1032
1159
|
## Reading your own statics — the ladder
|
|
1033
1160
|
|
|
1034
1161
|
`Reactive(X) === X`, so a namespace's `Class` slot IS the base class. A getter
|
|
@@ -1126,6 +1253,11 @@ like prose — don't ruin it with letter soup:
|
|
|
1126
1253
|
- Abbreviate only when the abbreviation IS the domain term (`px`, `id`,
|
|
1127
1254
|
`fx`, A1-notation like `startRow`/`endCol`).
|
|
1128
1255
|
- Tests are code — the same rules apply to specs.
|
|
1256
|
+
- **A `v-for` alias is a declaration the template makes** — the same
|
|
1257
|
+
rule: `v-for="(cell, columnIndex) in sheet.grid[row]"`, never
|
|
1258
|
+
`(cell, ci) in sheet.grid[r]`. The template is read by the same
|
|
1259
|
+
people as the class; `r`, `c`, `ci` cost them the same re-derivation
|
|
1260
|
+
there.
|
|
1129
1261
|
|
|
1130
1262
|
```ts
|
|
1131
1263
|
// ❌ const v = this.cellVersions.get(k);
|
|
@@ -1153,7 +1285,7 @@ Constants use one form per role:
|
|
|
1153
1285
|
|
|
1154
1286
|
| Role | Form |
|
|
1155
1287
|
| --- | --- |
|
|
1156
|
-
| Tunable or overridable class constant | `static get SCREAMING_SNAKE_CASE()` |
|
|
1288
|
+
| Tunable or overridable class constant — a literal, or a tree composed of other SCREAMING constants (`{ mouse: Selection.Class.AUTOSCROLL_MOUSE }`) | `static get SCREAMING_SNAKE_CASE()` |
|
|
1157
1289
|
| Protocol or byte constant on a hot path, never overridden | `static readonly SCREAMING_SNAKE_CASE` with a one-line hot-path comment |
|
|
1158
1290
|
| Contributor or pane identity data | Instance `readonly lowerCamelCase` field |
|
|
1159
1291
|
| Extensible constructed dependency | Field assigned from a prototype `createX()` factory method |
|
|
@@ -1222,12 +1354,14 @@ convention and check it in review.
|
|
|
1222
1354
|
- [ ] Watch sources are the FUNCTION form; component-scoped constructors use plain `watch`/`watchEffect`; `this.$watch`/`this.$watchEffect` only for component-outliving instances — each with a dispose path (`$stopEffects()` owner or `onScopeDispose` auto-wire).
|
|
1223
1355
|
- [ ] Lifecycle hooks / init logic live in the constructor (no `init()` expecting auto-call); template refs guarded with `?.` where read pre-mount.
|
|
1224
1356
|
- [ ] 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)`.
|
|
1225
|
-
- [ ] 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.
|
|
1357
|
+
- [ ] Identifiers are unfolded to domain words (`row`/`col`/`cell`/`cellValue`/`versionRef`…), loop indices, `v-for` aliases and specs included — no single-letter names, no name meaning different things in different methods.
|
|
1226
1358
|
- [ ] 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.
|
|
1227
1359
|
- [ ] 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`.
|
|
1360
|
+
- [ ] Million-call loops over a `Static()` class destructure the bound methods once inside the function (never module-scope, never `$Class`); `Class.method()` stays the form everywhere else.
|
|
1228
1361
|
- [ ] Instance reads of own statics go through `this.self` (declared once per class needing it, cast to `typeof $X`, plain getter never `$self`); 2+ reads or loops hoist `const self = this.self`; no per-site `this.constructor` casts; `Namespace.Class` reads stay reserved for late-bound capability dispatch.
|
|
1229
1362
|
- [ ] Static members precede the constructor; the constructor precedes state, prop, and derived getters; methods come last.
|
|
1230
1363
|
- [ ] 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.
|
|
1231
|
-
- [ ]
|
|
1364
|
+
- [ ] A class that calls `this.$watch` / `$watchEffect` / `$stopEffects` merges the engine's helpers beside itself — `interface $X extends ReactiveHelpers {}` — so the body typechecks (never `(this as any)`, never per-member `declare` lines).
|
|
1365
|
+
- [ ] The class carries the WHOLE contract as static getters (`propsTypes`, `propsDefaults`, the one-line `props` fusion, `emits`, tuning knobs) and the namespace holds identity and types ONLY, every type derived from `$Class`; no module-level consts or TYPE declarations beside imports/class/namespace (every type a class file declares is a namespace member, read as `X.Name`), no `const`, `let`, or `function` of any kind in the namespace — contract data, tuning knobs, seed data, singletons (`use()`), helpers all live on the class as statics (the gate's `the_namespace_holds_identity_and_types_only` check enforces it), no sibling `XProps.ts`; the SFC reads `X.Class.props` / `X.Class.emits`; a subclass extends the contract with `super` and re-declares the fusion line only when it ADDS props.
|
|
1232
1366
|
- [ ] Every member that overrides a base member carries `override` (with `noImplicitOverride` enabled).
|
|
1233
1367
|
- [ ] No `private` members — internal members are `protected` (three-tier visibility: public = consumer surface, protected = hierarchy seam, private = banned).
|
|
@@ -26,7 +26,7 @@ import { GeneratorStandard } from './ivue-generator-standard';
|
|
|
26
26
|
// domain-invariant: $CheckStandard — If a check is in the manifest, then its proofs entry carries the claim, the impossibility, and at least one red and one green arm
|
|
27
27
|
test('the shipped constitution is complete for every manifest check', () => {
|
|
28
28
|
const GateClass = Gate.CheckStandard.Class;
|
|
29
|
-
expect(GateClass.checks.length).toBe(
|
|
29
|
+
expect(GateClass.checks.length).toBe(32);
|
|
30
30
|
const report = GateClass.prove({ completenessOnly: true });
|
|
31
31
|
expect(report.problems).toEqual([]);
|
|
32
32
|
for (const check of GateClass.checks) {
|
|
@@ -55,7 +55,7 @@ test('the generator standard is the extension mechanism eating its own cooking',
|
|
|
55
55
|
// ten methodology checks arrive the same way a house check does:
|
|
56
56
|
// getters + checks + proofs on a subclass — fully proven, opt-in
|
|
57
57
|
const GeneratorClass = GeneratorStandard.Class;
|
|
58
|
-
expect(GeneratorClass.checks.length).toBe(
|
|
58
|
+
expect(GeneratorClass.checks.length).toBe(42);
|
|
59
59
|
const report = GeneratorClass.prove({ completenessOnly: true });
|
|
60
60
|
expect(report.problems).toEqual([]);
|
|
61
61
|
// the base stays ivue-only: no header check leaks upward
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"path": "examples/playground/src/examples/benchmarks/creationBench.ts",
|
|
4
|
+
"check": "a_public_class_publishes_its_namespace_manifest",
|
|
5
|
+
"reason": "benchmark arms: PlainBox and reactive(new PlainBox()) ARE the measured competitors; changing their shape changes the numbers the docs quote"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"path": "examples/playground/src/examples/benchmarks/creationBench.ts",
|
|
9
|
+
"check": "construction_goes_through_the_namespace_class_slot",
|
|
10
|
+
"reason": "benchmark arms: PlainBox and reactive(new PlainBox()) ARE the measured competitors; changing their shape changes the numbers the docs quote"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "examples/playground/src/ivue.ts",
|
|
14
|
+
"check": "declarations_use_full_descriptive_names",
|
|
15
|
+
"reason": "vendored copy of the engine (lib/Reactive.ts, synced by sync:examples) \u2014 the engine keeps its own conventions"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "examples/playground/src/lenis/Emitter.ts",
|
|
19
|
+
"check": "declarations_use_full_descriptive_names",
|
|
20
|
+
"reason": "vendored third-party (the Lenis smooth-scroll library, ported as-is) \u2014 not teaching code"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "examples/playground/src/lenis/Lenis.ts",
|
|
24
|
+
"check": "declarations_use_full_descriptive_names",
|
|
25
|
+
"reason": "vendored third-party (the Lenis smooth-scroll library, ported as-is) \u2014 not teaching code"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "examples/playground/src/lenis/LenisUtils.ts",
|
|
29
|
+
"check": "declarations_use_full_descriptive_names",
|
|
30
|
+
"reason": "vendored third-party (the Lenis smooth-scroll library, ported as-is) \u2014 not teaching code"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "docs_v2/.vitepress/theme/components/grid/GridBenchmark.vue",
|
|
34
|
+
"check": "one_handler_per_event",
|
|
35
|
+
"reason": "benchmark arms: the composable and the ivue grid ARE the measured competitors, bound identically on purpose; giving either its own per-event methods changes the shape the numbers compare"
|
|
36
|
+
}
|
|
37
|
+
]
|