@hyperfixi/reactivity 2.4.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/LICENSE +20 -0
- package/README.md +137 -0
- package/dist/index.cjs +772 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +744 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/bind.ts +355 -0
- package/src/caret-var.test.ts +137 -0
- package/src/caret-var.ts +125 -0
- package/src/index.ts +132 -0
- package/src/integration.test.ts +585 -0
- package/src/live.ts +68 -0
- package/src/signals.test.ts +369 -0
- package/src/signals.ts +444 -0
- package/src/types.ts +46 -0
- package/src/when.ts +72 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* caret-var unit tests — `^name` read/write and inherited scope walking.
|
|
3
|
+
* The parse layer is exercised via the integration test; here we test the
|
|
4
|
+
* storage semantics in isolation via the Reactive API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
8
|
+
import { Reactive } from './signals';
|
|
9
|
+
|
|
10
|
+
describe('caret-var storage', () => {
|
|
11
|
+
let r: Reactive;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
r = new Reactive();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('read returns undefined for an untouched name', () => {
|
|
18
|
+
const el = document.createElement('div');
|
|
19
|
+
expect(r.readCaret(el, 'any')).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('write then read on the same element', () => {
|
|
23
|
+
const el = document.createElement('div');
|
|
24
|
+
r.writeCaret(el, 'counter', 1);
|
|
25
|
+
expect(r.readCaret(el, 'counter')).toBe(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('inherited scope: child reads parent var', () => {
|
|
29
|
+
const grand = document.createElement('section');
|
|
30
|
+
const parent = document.createElement('article');
|
|
31
|
+
const child = document.createElement('p');
|
|
32
|
+
grand.appendChild(parent);
|
|
33
|
+
parent.appendChild(child);
|
|
34
|
+
r.writeCaret(grand, 'theme', 'dark');
|
|
35
|
+
expect(r.readCaret(child, 'theme')).toBe('dark');
|
|
36
|
+
expect(r.readCaret(parent, 'theme')).toBe('dark');
|
|
37
|
+
expect(r.readCaret(grand, 'theme')).toBe('dark');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('write without explicit target updates the inherited owner', () => {
|
|
41
|
+
const outer = document.createElement('div');
|
|
42
|
+
const inner = document.createElement('span');
|
|
43
|
+
outer.appendChild(inner);
|
|
44
|
+
r.writeCaret(outer, 'x', 'a');
|
|
45
|
+
// From inner, no explicit target → walks up, finds outer as owner, updates it.
|
|
46
|
+
r.writeCaret(inner, 'x', 'b');
|
|
47
|
+
expect(r.readCaret(outer, 'x')).toBe('b');
|
|
48
|
+
expect(r.readCaret(inner, 'x')).toBe('b'); // inherited
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('shadowing requires explicit target to create a local owner', () => {
|
|
52
|
+
const outer = document.createElement('div');
|
|
53
|
+
const inner = document.createElement('span');
|
|
54
|
+
outer.appendChild(inner);
|
|
55
|
+
r.writeCaret(outer, 'x', 'outer-val');
|
|
56
|
+
// Explicit target = inner → creates a new owner on inner, shadowing outer.
|
|
57
|
+
r.writeCaret(inner, 'x', 'inner-val', inner);
|
|
58
|
+
expect(r.readCaret(inner, 'x')).toBe('inner-val');
|
|
59
|
+
expect(r.readCaret(outer, 'x')).toBe('outer-val');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('write with explicit target bypasses inheritance walk', () => {
|
|
63
|
+
const parent = document.createElement('div');
|
|
64
|
+
const child = document.createElement('span');
|
|
65
|
+
parent.appendChild(child);
|
|
66
|
+
// Write from child but target parent explicitly.
|
|
67
|
+
r.writeCaret(child, 'pinned', 'yes', parent);
|
|
68
|
+
expect(r.readCaret(parent, 'pinned')).toBe('yes');
|
|
69
|
+
expect(r.readCaret(child, 'pinned')).toBe('yes'); // inherits
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('dom-scope="isolated" boundary', () => {
|
|
73
|
+
it('read stops at an isolation boundary that does not define the var', () => {
|
|
74
|
+
const outer = document.createElement('div');
|
|
75
|
+
const boundary = document.createElement('article');
|
|
76
|
+
boundary.setAttribute('dom-scope', 'isolated');
|
|
77
|
+
const inner = document.createElement('span');
|
|
78
|
+
outer.appendChild(boundary);
|
|
79
|
+
boundary.appendChild(inner);
|
|
80
|
+
|
|
81
|
+
r.writeCaret(outer, 'theme', 'dark');
|
|
82
|
+
// Inner can't see outer's theme — boundary blocks the walk.
|
|
83
|
+
expect(r.readCaret(inner, 'theme')).toBeUndefined();
|
|
84
|
+
expect(r.readCaret(boundary, 'theme')).toBeUndefined();
|
|
85
|
+
// Outer itself is unaffected.
|
|
86
|
+
expect(r.readCaret(outer, 'theme')).toBe('dark');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('boundary itself can define the var (read returns boundary value)', () => {
|
|
90
|
+
const outer = document.createElement('div');
|
|
91
|
+
const boundary = document.createElement('article');
|
|
92
|
+
boundary.setAttribute('dom-scope', 'isolated');
|
|
93
|
+
const inner = document.createElement('span');
|
|
94
|
+
outer.appendChild(boundary);
|
|
95
|
+
boundary.appendChild(inner);
|
|
96
|
+
|
|
97
|
+
r.writeCaret(outer, 'theme', 'dark');
|
|
98
|
+
r.writeCaret(boundary, 'theme', 'light', boundary);
|
|
99
|
+
// Inner sees boundary's local theme; outer's is shadowed.
|
|
100
|
+
expect(r.readCaret(inner, 'theme')).toBe('light');
|
|
101
|
+
expect(r.readCaret(boundary, 'theme')).toBe('light');
|
|
102
|
+
expect(r.readCaret(outer, 'theme')).toBe('dark');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('write from inside boundary falls back to lookupRoot when no owner is reachable', () => {
|
|
106
|
+
const outer = document.createElement('div');
|
|
107
|
+
const boundary = document.createElement('article');
|
|
108
|
+
boundary.setAttribute('dom-scope', 'isolated');
|
|
109
|
+
const inner = document.createElement('span');
|
|
110
|
+
outer.appendChild(boundary);
|
|
111
|
+
boundary.appendChild(inner);
|
|
112
|
+
|
|
113
|
+
// Outer has count, but inner is inside the boundary — write from inner
|
|
114
|
+
// should NOT reach outer; it should land on inner (lookupRoot fallback).
|
|
115
|
+
r.writeCaret(outer, 'count', 99);
|
|
116
|
+
r.writeCaret(inner, 'count', 1);
|
|
117
|
+
expect(r.readCaret(inner, 'count')).toBe(1);
|
|
118
|
+
// Inner's value is on inner itself; outer's stays.
|
|
119
|
+
expect(r.readCaret(outer, 'count')).toBe(99);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('two sibling boundaries hold independent ^var state', () => {
|
|
123
|
+
const root = document.createElement('div');
|
|
124
|
+
const a = document.createElement('article');
|
|
125
|
+
a.setAttribute('dom-scope', 'isolated');
|
|
126
|
+
const b = document.createElement('article');
|
|
127
|
+
b.setAttribute('dom-scope', 'isolated');
|
|
128
|
+
root.appendChild(a);
|
|
129
|
+
root.appendChild(b);
|
|
130
|
+
|
|
131
|
+
r.writeCaret(a, 'count', 1);
|
|
132
|
+
r.writeCaret(b, 'count', 2);
|
|
133
|
+
expect(r.readCaret(a, 'count')).toBe(1);
|
|
134
|
+
expect(r.readCaret(b, 'count')).toBe(2);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
package/src/caret-var.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `^name` — DOM-scoped inherited variable.
|
|
3
|
+
*
|
|
4
|
+
* Upstream syntax:
|
|
5
|
+
* ^counter → read from nearest ancestor that has `counter` set
|
|
6
|
+
* ^counter on #target → read from (or near) #target
|
|
7
|
+
* set ^counter to 42 → write to owner (or lookupRoot if not yet defined)
|
|
8
|
+
*
|
|
9
|
+
* Parse side: register `^` as a Pratt prefix operator. The handler consumes
|
|
10
|
+
* the identifier token and an optional `on <target>` clause, emitting a
|
|
11
|
+
* `caretVar` AST node.
|
|
12
|
+
*
|
|
13
|
+
* Eval side: `caretVar` node evaluator calls `reactive.readCaret(anchor,
|
|
14
|
+
* name)` which walks the DOM tree for the owner and records deps as it goes.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ASTNode, ExecutionContext } from './types';
|
|
18
|
+
import { reactive } from './signals';
|
|
19
|
+
|
|
20
|
+
export interface CaretVarNode extends ASTNode {
|
|
21
|
+
type: 'caretVar';
|
|
22
|
+
name: string;
|
|
23
|
+
onTarget: ASTNode | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CaretVarRuntime {
|
|
27
|
+
execute(node: ASTNode, ctx: ExecutionContext): Promise<unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Coerce a runtime-evaluated `on <target>` result to a single Element.
|
|
32
|
+
* Selector expressions resolve to an array of matched elements; bare
|
|
33
|
+
* `me`/`it`/`you`/identifier references resolve to single Elements. Take the
|
|
34
|
+
* first element of an array, or the value itself if it's already an Element.
|
|
35
|
+
*/
|
|
36
|
+
function coerceToElement(value: unknown): Element | null {
|
|
37
|
+
if (value instanceof Element) return value;
|
|
38
|
+
if (Array.isArray(value) && value[0] instanceof Element) return value[0];
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pratt prefix handler for `^`. Consumes the following identifier token and
|
|
44
|
+
* an optional `on <expr>` clause.
|
|
45
|
+
*/
|
|
46
|
+
export function parseCaretPrefix(token: unknown, ctx: unknown): ASTNode {
|
|
47
|
+
// `ctx` is hyperfixi's PrattContext: { peek, advance, parseExpr, isStopToken, atEnd }.
|
|
48
|
+
const pctx = ctx as {
|
|
49
|
+
peek(): { value?: string; kind?: string } | undefined;
|
|
50
|
+
advance(): { value: string; kind?: string };
|
|
51
|
+
parseExpr(minBp: number): ASTNode;
|
|
52
|
+
};
|
|
53
|
+
const ident = pctx.advance();
|
|
54
|
+
if (!ident || !ident.value) {
|
|
55
|
+
throw new Error("Expected identifier after '^'");
|
|
56
|
+
}
|
|
57
|
+
let onTarget: ASTNode | null = null;
|
|
58
|
+
const next = pctx.peek();
|
|
59
|
+
if (next && next.value === 'on') {
|
|
60
|
+
pctx.advance(); // consume 'on'
|
|
61
|
+
// Binding power 86 — higher than standard comparisons so `^x on me` doesn't
|
|
62
|
+
// over-consume into surrounding expressions.
|
|
63
|
+
onTarget = pctx.parseExpr(86);
|
|
64
|
+
}
|
|
65
|
+
const startTok = token as { start?: number; end?: number; line?: number; column?: number };
|
|
66
|
+
return {
|
|
67
|
+
type: 'caretVar',
|
|
68
|
+
name: ident.value,
|
|
69
|
+
onTarget,
|
|
70
|
+
start: startTok?.start ?? 0,
|
|
71
|
+
end: startTok?.end ?? 0,
|
|
72
|
+
line: startTok?.line,
|
|
73
|
+
column: startTok?.column,
|
|
74
|
+
} as CaretVarNode;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a node evaluator for `caretVar` bound to a specific runtime. The
|
|
79
|
+
* evaluator walks up from the resolved anchor element, tracking every element
|
|
80
|
+
* visited so writes at any ancestor notify dependent effects.
|
|
81
|
+
*
|
|
82
|
+
* Capturing `runtime` via a factory closure (matching live/when/bind) keeps
|
|
83
|
+
* the evaluator independent of any module-scope state.
|
|
84
|
+
*/
|
|
85
|
+
export function makeEvaluateCaretVar(
|
|
86
|
+
runtime: CaretVarRuntime
|
|
87
|
+
): (node: ASTNode, ctx: unknown) => Promise<unknown> {
|
|
88
|
+
return async function evaluateCaretVar(node, ctx) {
|
|
89
|
+
const n = node as CaretVarNode;
|
|
90
|
+
const context = ctx as ExecutionContext;
|
|
91
|
+
let anchor: Element | null = (context.me as Element | null) ?? null;
|
|
92
|
+
|
|
93
|
+
if (n.onTarget) {
|
|
94
|
+
const resolved = await runtime.execute(n.onTarget, context);
|
|
95
|
+
const el = coerceToElement(resolved);
|
|
96
|
+
if (el) anchor = el;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!anchor) return undefined;
|
|
100
|
+
return reactive.readCaret(anchor, n.name);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build a node writer for `caretVar` bound to a specific runtime. Used by the
|
|
106
|
+
* core `set` command via `parserExtensions.registerNodeWriter`.
|
|
107
|
+
*/
|
|
108
|
+
export function makeWriteCaretVar(
|
|
109
|
+
runtime: CaretVarRuntime
|
|
110
|
+
): (node: ASTNode, value: unknown, ctx: unknown) => Promise<void> {
|
|
111
|
+
return async function writeCaretVar(node, value, ctx) {
|
|
112
|
+
const n = node as CaretVarNode;
|
|
113
|
+
const context = ctx as ExecutionContext;
|
|
114
|
+
const anchor: Element | null = (context.me as Element | null) ?? null;
|
|
115
|
+
if (!anchor) return;
|
|
116
|
+
|
|
117
|
+
let target: Element | undefined;
|
|
118
|
+
if (n.onTarget) {
|
|
119
|
+
const resolved = await runtime.execute(n.onTarget, context);
|
|
120
|
+
const el = coerceToElement(resolved);
|
|
121
|
+
if (el) target = el;
|
|
122
|
+
}
|
|
123
|
+
reactive.writeCaret(anchor, n.name, value, target);
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hyperfixi/reactivity — signal-based reactive features for hyperfixi.
|
|
3
|
+
*
|
|
4
|
+
* Adds four constructs from upstream _hyperscript 0.9.90:
|
|
5
|
+
*
|
|
6
|
+
* live [commandList] end reactive block — body re-runs
|
|
7
|
+
* when tracked reads change.
|
|
8
|
+
*
|
|
9
|
+
* when <expr> [or <expr>]* changes observer — body runs when any
|
|
10
|
+
* [commandList] end watched expression changes.
|
|
11
|
+
*
|
|
12
|
+
* bind <var> to <element> two-way DOM ⇄ var binding.
|
|
13
|
+
*
|
|
14
|
+
* ^name [on <target>] DOM-scoped inherited variable
|
|
15
|
+
* (read + write with `^`).
|
|
16
|
+
*
|
|
17
|
+
* Install:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createRuntime, installPlugin } from '@hyperfixi/core';
|
|
21
|
+
* import { reactivityPlugin } from '@hyperfixi/reactivity';
|
|
22
|
+
*
|
|
23
|
+
* const runtime = createRuntime();
|
|
24
|
+
* installPlugin(runtime, reactivityPlugin);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { HyperfixiPlugin, HyperfixiPluginContext } from '@hyperfixi/core';
|
|
29
|
+
import { reactive } from './signals';
|
|
30
|
+
import { parseCaretPrefix, makeEvaluateCaretVar, makeWriteCaretVar } from './caret-var';
|
|
31
|
+
import { parseLiveFeature, makeEvaluateLiveFeature } from './live';
|
|
32
|
+
import { parseWhenFeature, makeEvaluateWhenFeature } from './when';
|
|
33
|
+
import { parseBindFeature, makeEvaluateBindFeature } from './bind';
|
|
34
|
+
|
|
35
|
+
export { reactive } from './signals';
|
|
36
|
+
export type { CaretVarNode } from './caret-var';
|
|
37
|
+
export type { LiveFeatureNode } from './live';
|
|
38
|
+
export type { WhenFeatureNode } from './when';
|
|
39
|
+
export type { BindFeatureNode } from './bind';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The plugin object. Install once at app startup; re-installing is idempotent
|
|
43
|
+
* (guarded via a `parserExtensions.hasFeature('live')` check).
|
|
44
|
+
*
|
|
45
|
+
* Registers:
|
|
46
|
+
* - Features: `live`, `when`, `bind` (top-level features with `end` bodies)
|
|
47
|
+
* - Prefix op: `^` (primary expression for DOM-scoped vars)
|
|
48
|
+
* - Node evaluators: `liveFeature`, `whenFeature`, `bindFeature`, `caretVar`
|
|
49
|
+
* - A node writer for `caretVar` so `set ^X to Y` flows through `reactive.writeCaret`
|
|
50
|
+
* - Global read/write hooks so `$name` reads track and writes notify
|
|
51
|
+
*
|
|
52
|
+
* Effect cleanup: each effect-creating evaluator calls
|
|
53
|
+
* `context.registerCleanup(owner, stop, ...)` so the core runtime tears effects
|
|
54
|
+
* down when their owning element is cleaned up. There is no separate plugin-level
|
|
55
|
+
* cleanup hook; `reactive.stopElementEffects(el)` is exposed for explicit teardown
|
|
56
|
+
* by tests and consumers that manage element lifecycle outside the runtime.
|
|
57
|
+
*/
|
|
58
|
+
export const reactivityPlugin: HyperfixiPlugin & { version: string } = {
|
|
59
|
+
name: '@hyperfixi/reactivity',
|
|
60
|
+
version: '2.3.1',
|
|
61
|
+
install(ctx: HyperfixiPluginContext) {
|
|
62
|
+
const { parserExtensions, runtime } = ctx;
|
|
63
|
+
|
|
64
|
+
// Idempotency: the parser-extension registry is process-singleton, so
|
|
65
|
+
// re-installing into a fresh runtime would otherwise stack additional
|
|
66
|
+
// global read/write hooks on every call. `snapshot()`/`restore()` clears
|
|
67
|
+
// the feature registry — when tests roll back the registry, this guard
|
|
68
|
+
// re-enables a fresh install.
|
|
69
|
+
if (parserExtensions.hasFeature('live')) return;
|
|
70
|
+
|
|
71
|
+
// Parser hooks — three block features plus a primary-expression caret.
|
|
72
|
+
parserExtensions.registerFeature('live', parseLiveFeature as never);
|
|
73
|
+
parserExtensions.registerFeature('when', parseWhenFeature as never);
|
|
74
|
+
parserExtensions.registerFeature('bind', parseBindFeature as never);
|
|
75
|
+
parserExtensions.registerPrefixOperator('^', 85, parseCaretPrefix as never);
|
|
76
|
+
|
|
77
|
+
// Runtime evaluators. Features capture `runtime` so effect re-runs can
|
|
78
|
+
// dispatch body commands without going through module-scope state.
|
|
79
|
+
parserExtensions.registerNodeEvaluator(
|
|
80
|
+
'liveFeature',
|
|
81
|
+
makeEvaluateLiveFeature(runtime as never) as never
|
|
82
|
+
);
|
|
83
|
+
parserExtensions.registerNodeEvaluator(
|
|
84
|
+
'whenFeature',
|
|
85
|
+
makeEvaluateWhenFeature(runtime as never) as never
|
|
86
|
+
);
|
|
87
|
+
parserExtensions.registerNodeEvaluator(
|
|
88
|
+
'bindFeature',
|
|
89
|
+
makeEvaluateBindFeature(runtime as never) as never
|
|
90
|
+
);
|
|
91
|
+
parserExtensions.registerNodeEvaluator(
|
|
92
|
+
'caretVar',
|
|
93
|
+
makeEvaluateCaretVar(runtime as never) as never
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Caret-var write: lets the core `set` command dispatch `set ^X to Y`
|
|
97
|
+
// through `reactive.writeCaret`. Resolves `on <target>` via the captured
|
|
98
|
+
// runtime — no global-scope indirection.
|
|
99
|
+
parserExtensions.registerNodeWriter('caretVar', makeWriteCaretVar(runtime as never) as never);
|
|
100
|
+
|
|
101
|
+
// Global-write hook: notify the reactive graph whenever `$name` is set.
|
|
102
|
+
// The returned disposer is intentionally discarded — the install-time
|
|
103
|
+
// idempotency guard above is the sole gate against double-registration.
|
|
104
|
+
parserExtensions.registerGlobalWriteHook((name: string, _value: unknown, _context: unknown) => {
|
|
105
|
+
reactive.notifyGlobal(name);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Global-read hook: track the read against the current effect (if any)
|
|
109
|
+
// so effects re-run when the global changes.
|
|
110
|
+
parserExtensions.registerGlobalReadHook((name: string, _context: unknown) => {
|
|
111
|
+
reactive.trackGlobal(name);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Local hooks — analogous to globals, but keyed by `context.me` so each
|
|
115
|
+
// element holds independent state. A `set :foo to ...` running in a
|
|
116
|
+
// handler with `me=button1` notifies effects subscribed to (button1, 'foo');
|
|
117
|
+
// a `set :foo` in a different `me` won't reach them. This matches how
|
|
118
|
+
// locals already work — they're never cross-context — and lets two
|
|
119
|
+
// components hold independent `:foo` state without interference.
|
|
120
|
+
parserExtensions.registerLocalWriteHook((name: string, _value: unknown, context) => {
|
|
121
|
+
const owner = (context as { me?: Element | null }).me ?? null;
|
|
122
|
+
if (owner) reactive.notifyElement(owner, name);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
parserExtensions.registerLocalReadHook((name: string, context) => {
|
|
126
|
+
const owner = (context as { me?: Element | null }).me ?? null;
|
|
127
|
+
if (owner) reactive.trackElement(owner, name);
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export default reactivityPlugin;
|