@ape-egg/vibe 3.0.4 → 4.0.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 +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/runtime/component.js +9 -3
- package/runtime/constants.js +14 -39
- package/runtime/hydrate.js +2 -6
- package/runtime/index.js +17 -0
- package/runtime/iteration-utils.js +28 -1
- package/runtime/parse.js +45 -7
- package/runtime/this-scope.js +53 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vibe
|
|
2
2
|
|
|
3
|
-
**Version
|
|
3
|
+
**Version 4.0.0** — Runtime-first reactivity for plain HTML. Drop a script tag into any page and get reactive bindings, control flow, and URL-loaded components — no build step required. Compile later if you want; the compiler is a separate, optional package (`@ape-egg/vibe-compiler`).
|
|
4
4
|
|
|
5
5
|
## Security model & CSP
|
|
6
6
|
|
|
@@ -196,7 +196,7 @@ How it works:
|
|
|
196
196
|
|
|
197
197
|
1. `component({...})` generates a unique id (e.g. `_c0`, `_c1`) and registers the state at `$[id]`
|
|
198
198
|
2. The `<script>` and every following sibling is tagged with `data-vibe-component-id="<id>"`
|
|
199
|
-
3. Inside that subtree, `@[this.X.Y]` is rewritten to `@[_c0.X.Y]
|
|
199
|
+
3. Inside that subtree, `@[this.X.Y]` is rewritten to `@[_c0.X.Y]`, and `this.` in event handlers resolves at fire time by declared keys: names registered in `component({...})` address `$[id]` (`onclick="this.method()"`, `this.count++`), every other name reaches the native element (`this.value`, `this.closest(...)`, `this.focus()`). `$.this.X = ...` is always an explicit state write
|
|
200
200
|
4. When the component leaves the DOM, its state entry is freed automatically
|
|
201
201
|
|
|
202
202
|
Multi-segment paths (`@[this.user.profile.name]`), conditionals (`<!-- if this.editing -->`), and iterations (`<!-- each this.items as item -->`) all resolve against the component's bucket. Global `$` and component `this.X` coexist freely.
|
package/llms.txt
CHANGED
|
@@ -290,7 +290,7 @@ Use standard inline event handlers — they execute against `$` directly:
|
|
|
290
290
|
<form onsubmit="event.preventDefault(); handleSubmit()">
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
-
Inside a component, `this.X`
|
|
293
|
+
Inside a component, `this.X` resolves at fire time: keys declared in `component({...})` reference the component's bucket, every other name reaches the native DOM element (`this.value`, `this.dataset`, `this.nextElementSibling`, `this.closest(...)`, `this.focus()` — the whole DOM API). Declaring a key claims that name for state within the component's handlers; reach the shadowed element property through `event.currentTarget` if you need both.
|
|
294
294
|
|
|
295
295
|
## Dynamic Elements
|
|
296
296
|
|
package/package.json
CHANGED
package/runtime/component.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
ITERATION_START_REGEX,
|
|
12
12
|
} from './constants.js';
|
|
13
13
|
import { evalInScope } from './utils.js';
|
|
14
|
-
import { bumpIterPropGeneration } from './iteration-utils.js';
|
|
14
|
+
import { bumpIterPropGeneration, parkFetchableSrc } from './iteration-utils.js';
|
|
15
15
|
import { fetchComponentTemplate, isComponentCached } from './component-cache.js';
|
|
16
16
|
import { notifyChanged } from './state.js';
|
|
17
17
|
import {
|
|
@@ -506,8 +506,10 @@ const isNestedInUnprocessedComponent = (el, rootElement) => {
|
|
|
506
506
|
// any trailing `.Y.Z` chain), so `@[this.user.name]` and `@[this.x + 1]` both
|
|
507
507
|
// resolve correctly. Outside bindings — i.e. event handler attribute bodies
|
|
508
508
|
// like `onclick="$.this.mode = 'edit'"` — only `$.this.X` writes are
|
|
509
|
-
// rewritten; bare `this.X`
|
|
510
|
-
//
|
|
509
|
+
// rewritten; bare `this.X` in event handlers is handled later by parse.js,
|
|
510
|
+
// which lowers it to `$this(this).X` — resolved at fire time by
|
|
511
|
+
// this-scope.js (declared state keys hit the bucket, everything else stays
|
|
512
|
+
// the native element).
|
|
511
513
|
const rewriteBindingsInString = (str, componentId) =>
|
|
512
514
|
str.replace(BINDING_REGEX, (match, expr) => {
|
|
513
515
|
const rewritten = expr.replace(THIS_PROP_REGEX, `${componentId}.$1`);
|
|
@@ -857,6 +859,10 @@ const processSingle = (el, debug) => {
|
|
|
857
859
|
}
|
|
858
860
|
|
|
859
861
|
newWrapper.innerHTML = transformedHtml;
|
|
862
|
+
// Park binding-valued src while the wrapper still lives in the inert
|
|
863
|
+
// document — adoption into the live document is what starts image
|
|
864
|
+
// loads, so this is the last moment a raw `@[...]` src is harmless.
|
|
865
|
+
parkFetchableSrc(newWrapper);
|
|
860
866
|
if (firstComponentId !== null) {
|
|
861
867
|
newWrapper.setAttribute('data-vibe-component-id', firstComponentId);
|
|
862
868
|
}
|
package/runtime/constants.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// The released version — check-release.js holds this in lockstep with
|
|
2
|
+
// package.json, the READMEs and the CHANGELOG.
|
|
3
|
+
export const VERSION = '4.0.0';
|
|
4
|
+
|
|
1
5
|
// Debug logger name
|
|
2
6
|
export const DEBUGGER_NAME = '[vibe-debug]:';
|
|
3
7
|
export const FOUC_CLASS_OR_ATTR = 'vibe-fouc'; // Class or attribute used to prevent FOUC (default: [vibe])
|
|
@@ -26,6 +30,16 @@ export const PHASE_HYPERSPEED = 'Hyperspeed'; // Restores @[...] markers from pr
|
|
|
26
30
|
// Fetched components (<component src="">) are handled separately by processComponent()
|
|
27
31
|
export const NON_REACTIVE_ELEMENTS = ['SCRIPT', 'HEAD', 'PRE'];
|
|
28
32
|
|
|
33
|
+
// Elements whose src-family attributes the browser fetches the moment they are
|
|
34
|
+
// set — a raw `src="@[binding]"` reaching one of these fires a network request
|
|
35
|
+
// for the literal binding text before hydration can stamp the real value.
|
|
36
|
+
// Bindings on them are parked on data-vibe-<attr> (the established transport)
|
|
37
|
+
// until hydration writes the evaluated URL. <component src> is NOT here — its
|
|
38
|
+
// src is consumed by processComponent, never by the browser.
|
|
39
|
+
export const FETCH_SRC_ATTRS = ['src', 'srcset', 'poster'];
|
|
40
|
+
export const FETCH_SRC_SELECTOR = 'img, source, iframe, video, audio, embed, track';
|
|
41
|
+
export const FETCH_SRC_ELEMENTS = ['IMG', 'SOURCE', 'IFRAME', 'VIDEO', 'AUDIO', 'EMBED', 'TRACK'];
|
|
42
|
+
|
|
29
43
|
// Attributes where the string value is meaningful (should NOT be removed when falsy)
|
|
30
44
|
// All other attributes are treated as boolean-like (removed when falsy, present when truthy)
|
|
31
45
|
export const VALUE_ATTRS = [
|
|
@@ -162,45 +176,6 @@ export const VALUE_ATTRS = [
|
|
|
162
176
|
// Properties that should be set directly on the DOM element (not as attributes)
|
|
163
177
|
export const DOM_PROPERTIES = ['value', 'checked', 'selected'];
|
|
164
178
|
|
|
165
|
-
// DOM element properties that should NOT be rewritten in event handlers
|
|
166
|
-
// When parsing onclick="this.value = this.inputValue", preserve this.value (DOM) but rewrite this.inputValue (component state)
|
|
167
|
-
export const DOM_ELEMENT_PROPERTIES = new Set([
|
|
168
|
-
'value',
|
|
169
|
-
'checked',
|
|
170
|
-
'selected',
|
|
171
|
-
'disabled',
|
|
172
|
-
'readOnly',
|
|
173
|
-
'files',
|
|
174
|
-
'tagName',
|
|
175
|
-
'nodeName',
|
|
176
|
-
'nodeType',
|
|
177
|
-
'classList',
|
|
178
|
-
'className',
|
|
179
|
-
'id',
|
|
180
|
-
'innerHTML',
|
|
181
|
-
'outerHTML',
|
|
182
|
-
'textContent',
|
|
183
|
-
'innerText',
|
|
184
|
-
'parentElement',
|
|
185
|
-
'parentNode',
|
|
186
|
-
'nextSibling',
|
|
187
|
-
'previousSibling',
|
|
188
|
-
'firstChild',
|
|
189
|
-
'lastChild',
|
|
190
|
-
'children',
|
|
191
|
-
'childNodes',
|
|
192
|
-
'offsetWidth',
|
|
193
|
-
'offsetHeight',
|
|
194
|
-
'clientWidth',
|
|
195
|
-
'clientHeight',
|
|
196
|
-
'scrollTop',
|
|
197
|
-
'scrollLeft',
|
|
198
|
-
'scrollWidth',
|
|
199
|
-
'scrollHeight',
|
|
200
|
-
'style',
|
|
201
|
-
'dataset',
|
|
202
|
-
'attributes',
|
|
203
|
-
]);
|
|
204
179
|
|
|
205
180
|
// Regex for matching reactive bindings (@[expression])
|
|
206
181
|
// Supports nested brackets, single-quoted and double-quoted strings inside expressions:
|
package/runtime/hydrate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { updateIteration } from './iterate.js';
|
|
2
2
|
import { updateConditional, managedNodes } from './conditionals.js';
|
|
3
3
|
import { liveComponentWrapper, remountComponent, forceRemount } from './component.js';
|
|
4
|
-
import { isComponentWrapper, parkRootFor, parkBinding } from './staging.js';
|
|
4
|
+
import { isComponentWrapper, isRemountTrigger, parkRootFor, parkBinding } from './staging.js';
|
|
5
5
|
import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js';
|
|
6
6
|
import {
|
|
7
7
|
evalInScope,
|
|
@@ -177,11 +177,7 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
177
177
|
const remountTriggers = [];
|
|
178
178
|
const ordinary = [];
|
|
179
179
|
for (const aff of affected) {
|
|
180
|
-
|
|
181
|
-
aff.type === 'attribute' &&
|
|
182
|
-
(aff.attrName === 'src' || aff.attrName === 'key') &&
|
|
183
|
-
isComponentWrapper(aff.element);
|
|
184
|
-
(isRemountTrigger ? remountTriggers : ordinary).push(aff);
|
|
180
|
+
(isRemountTrigger(aff) ? remountTriggers : ordinary).push(aff);
|
|
185
181
|
}
|
|
186
182
|
|
|
187
183
|
const processEntry = (aff) => {
|
package/runtime/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { renderAllIterations, setRenderAllConditionals, releaseOrphanedIteration
|
|
|
9
9
|
import { renderAllConditionals, branchNodeRegistry, managedNodes , settleConditionals} from './conditionals.js';
|
|
10
10
|
import { installScopeResolver } from './loop-scope.js';
|
|
11
11
|
import {
|
|
12
|
+
VERSION,
|
|
12
13
|
NON_REACTIVE_ELEMENTS,
|
|
13
14
|
PHASE_ATTACH,
|
|
14
15
|
PHASE_PARSE,
|
|
@@ -40,6 +41,10 @@ import {
|
|
|
40
41
|
// Wire up cross-module dependency after all modules are loaded
|
|
41
42
|
setRenderAllConditionals(renderAllConditionals);
|
|
42
43
|
|
|
44
|
+
// Stamped at module load, not boot — a console can read __vibe.version even
|
|
45
|
+
// on a page whose boot died, which is exactly when a bug report needs it.
|
|
46
|
+
((globalThis.__vibe ??= {}).version = VERSION);
|
|
47
|
+
|
|
43
48
|
// Check if node should be processed by Vibe
|
|
44
49
|
const shouldProcessNode = (node) => {
|
|
45
50
|
// Only process element nodes
|
|
@@ -302,6 +307,18 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
|
302
307
|
mergedNode.runtime = runtimeNode.runtime;
|
|
303
308
|
}
|
|
304
309
|
|
|
310
|
+
// The runtime parse of the live DOM is the ground truth for what exists.
|
|
311
|
+
// A manifest child with no runtime counterpart sits at an index the DOM
|
|
312
|
+
// no longer agrees with (a content script prepending into <body> shifts
|
|
313
|
+
// every sibling) — it can never bind an element, and hydrating its
|
|
314
|
+
// bindings would dereference null. Drop it; the runtime-discovered
|
|
315
|
+
// sibling added below carries the real element.
|
|
316
|
+
if (mergedNode.children) {
|
|
317
|
+
for (const key in mergedNode.children) {
|
|
318
|
+
if (!runtimeNode.children?.[key]) delete mergedNode.children[key];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
305
322
|
// Augment children recursively
|
|
306
323
|
if (runtimeNode.children) {
|
|
307
324
|
if (!mergedNode.children) mergedNode.children = {};
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
// Utility functions for array iteration
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ITERATION_REGEX,
|
|
4
|
+
ITERATION_START_REGEX,
|
|
5
|
+
CONDITIONAL_START_REGEX,
|
|
6
|
+
BINDING_REGEX,
|
|
7
|
+
FETCH_SRC_ATTRS,
|
|
8
|
+
FETCH_SRC_SELECTOR,
|
|
9
|
+
} from './constants.js';
|
|
10
|
+
|
|
11
|
+
// Move binding-valued src/srcset/poster on browser-fetchable elements to
|
|
12
|
+
// data-vibe-<attr> so the literal `@[...]` text never becomes a fetchable URL.
|
|
13
|
+
// Called on subtrees that are still in the inert document (component finalize)
|
|
14
|
+
// — parse.js recaptures the parked binding under the real attribute name and
|
|
15
|
+
// hydration writes the evaluated URL, which is the first value the browser
|
|
16
|
+
// ever sees.
|
|
17
|
+
export const parkFetchableSrc = (root) => {
|
|
18
|
+
const elements = root.querySelectorAll(FETCH_SRC_SELECTOR);
|
|
19
|
+
for (let i = 0; i < elements.length; i++) {
|
|
20
|
+
for (const attr of FETCH_SRC_ATTRS) {
|
|
21
|
+
const value = elements[i].getAttribute(attr);
|
|
22
|
+
BINDING_REGEX.lastIndex = 0;
|
|
23
|
+
if (value && BINDING_REGEX.test(value)) {
|
|
24
|
+
elements[i].setAttribute(`data-vibe-${attr}`, value);
|
|
25
|
+
elements[i].removeAttribute(attr);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
3
30
|
|
|
4
31
|
// Parse an `each` directive body (the text inside `<!-- ... -->`, markers
|
|
5
32
|
// stripped) into its parts, or null when it isn't a valid each. The index alias
|
package/runtime/parse.js
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
BINDING_REGEX,
|
|
10
10
|
CONDITIONAL_REGEX,
|
|
11
11
|
ITERATION_START_REGEX,
|
|
12
|
-
DOM_ELEMENT_PROPERTIES,
|
|
13
12
|
DEHYDRATE_CLASS_OR_ATTR,
|
|
14
13
|
THIS_PROP_REGEX,
|
|
15
14
|
STATE_THIS_PROP_REGEX,
|
|
15
|
+
FETCH_SRC_ATTRS,
|
|
16
|
+
FETCH_SRC_ELEMENTS,
|
|
16
17
|
} from './constants.js';
|
|
17
18
|
import { rewriteHandlerAliases } from './loop-scope.js';
|
|
19
|
+
import './this-scope.js';
|
|
18
20
|
|
|
19
21
|
// Walks up the DOM for the nearest component wrapper tagged by component.js.
|
|
20
22
|
// Used to rewrite `this.property` in event handlers to the component's state path.
|
|
@@ -75,6 +77,8 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
75
77
|
|
|
76
78
|
const attributes = {};
|
|
77
79
|
const nameBindings = [];
|
|
80
|
+
const isFetchableElement = FETCH_SRC_ELEMENTS.includes(nodeName);
|
|
81
|
+
let srcToPark = null;
|
|
78
82
|
|
|
79
83
|
for (let j = 0; j < element.attributes.length; j++) {
|
|
80
84
|
const attr = element.attributes[j];
|
|
@@ -88,6 +92,31 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
88
92
|
continue;
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
// A src-family binding parked on data-vibe-<attr> (by an earlier parse of
|
|
96
|
+
// this template, or by component finalize neutralizing fetched HTML):
|
|
97
|
+
// bind it to the real attribute — hydration writes the evaluated URL there.
|
|
98
|
+
if (isFetchableElement && attr.name.startsWith('data-vibe-')) {
|
|
99
|
+
const realName = attr.name.slice(10);
|
|
100
|
+
if (FETCH_SRC_ATTRS.includes(realName)) {
|
|
101
|
+
attributes[realName] = attr.value;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A raw binding-valued src reaching a fetchable element fires a request
|
|
107
|
+
// for the literal `@[...]` text (innerHTML parse and cloneNode both
|
|
108
|
+
// trigger it). Park it on data-vibe-<attr> — after the loop, since
|
|
109
|
+
// removeAttribute would shift the live NamedNodeMap being iterated —
|
|
110
|
+
// so the template and every clone of it are inert until hydration.
|
|
111
|
+
if (isFetchableElement && FETCH_SRC_ATTRS.includes(attr.name)) {
|
|
112
|
+
BINDING_REGEX.lastIndex = 0;
|
|
113
|
+
if (BINDING_REGEX.test(attr.value)) {
|
|
114
|
+
attributes[attr.name] = attr.value;
|
|
115
|
+
(srcToPark ??= []).push([attr.name, attr.value]);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
91
120
|
// Attribute name itself contains a binding (e.g. <icon @[section.icon]>).
|
|
92
121
|
if (BINDING_REGEX.test(attr.name)) {
|
|
93
122
|
nameBindings.push(attr.name);
|
|
@@ -96,7 +125,11 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
96
125
|
|
|
97
126
|
// Event handlers get two compile-time rewrites, computed off the original
|
|
98
127
|
// value and written once:
|
|
99
|
-
// 1. `this
|
|
128
|
+
// 1. `this.` inside a component scope → `$this(this).` — resolved at
|
|
129
|
+
// FIRE time by this-scope.js: declared state keys hit the bucket,
|
|
130
|
+
// everything else stays the native element. Fire-time resolution is
|
|
131
|
+
// load-bearing: compiled pages register component state after the
|
|
132
|
+
// initial parse, so no key check is possible here.
|
|
100
133
|
// 2. bare loop-variable aliases → `$scope(this,'alias')` (loop-scoped
|
|
101
134
|
// handlers — only when an enclosing <!-- each --> alias is in scope).
|
|
102
135
|
if (attr.name.startsWith('on')) {
|
|
@@ -107,13 +140,11 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
107
140
|
// `$.this.X` (component-state write through the root) must be
|
|
108
141
|
// consumed as one reference BEFORE the bare `this.X` pass — that
|
|
109
142
|
// pass alone would leave the `$.` prefix behind and produce
|
|
110
|
-
// `$.$
|
|
111
|
-
// already rewritten by component.js; compiled pages inline the
|
|
143
|
+
// `$.$this(this).X`. Runtime-fetched components arrive with this
|
|
144
|
+
// form already rewritten by component.js; compiled pages inline the
|
|
112
145
|
// authored form, so parse meets it raw.
|
|
113
146
|
v = v.replace(STATE_THIS_PROP_REGEX, (_, prop) => `$['${componentId}'].${prop}`);
|
|
114
|
-
v = v.replace(THIS_PROP_REGEX, (
|
|
115
|
-
DOM_ELEMENT_PROPERTIES.has(prop) ? match : `$['${componentId}'].${prop}`,
|
|
116
|
-
);
|
|
147
|
+
v = v.replace(THIS_PROP_REGEX, (_, prop) => `$this(this).${prop}`);
|
|
117
148
|
}
|
|
118
149
|
}
|
|
119
150
|
if (aliasSet && aliasSet.size > 0) {
|
|
@@ -128,6 +159,13 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
128
159
|
}
|
|
129
160
|
}
|
|
130
161
|
|
|
162
|
+
if (srcToPark) {
|
|
163
|
+
for (const [name, value] of srcToPark) {
|
|
164
|
+
element.setAttribute(`data-vibe-${name}`, value);
|
|
165
|
+
element.removeAttribute(name);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
131
169
|
return {
|
|
132
170
|
attributes: Object.keys(attributes).length > 0 ? attributes : null,
|
|
133
171
|
nameBindings: nameBindings.length > 0 ? nameBindings : null,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Event-handler `this.` resolution.
|
|
2
|
+
//
|
|
3
|
+
// parse.js rewrites `this.` in on* handlers inside a component scope to
|
|
4
|
+
// `$this(this).` — this module is that `$this`. The returned proxy decides at
|
|
5
|
+
// FIRE time, per access: a key declared in the component's state routes to
|
|
6
|
+
// the state bucket; anything else routes to the native element (functions
|
|
7
|
+
// bound so DOM methods keep their receiver, state methods left raw so `this`
|
|
8
|
+
// inside them stays the proxy and nested writes stay reactive).
|
|
9
|
+
//
|
|
10
|
+
// The decision must happen at fire time, not parse time: compiled pages
|
|
11
|
+
// register component state AFTER the initial parse (boot runs
|
|
12
|
+
// executeCompiledComponentScripts post-parse, pre-hydrate), so a parse-time
|
|
13
|
+
// key check would see empty buckets and mis-route every handler.
|
|
14
|
+
//
|
|
15
|
+
// Same delivery pattern as loop-scope's `$scope`: a global resolver keeps the
|
|
16
|
+
// handler a readable native `on*` attribute.
|
|
17
|
+
|
|
18
|
+
import { findComponentIdForElement } from './utils.js';
|
|
19
|
+
|
|
20
|
+
const bucketFor = (element) => {
|
|
21
|
+
const componentId = findComponentIdForElement(element);
|
|
22
|
+
return componentId ? globalThis.$?.[componentId] : undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const resolveThisScope = (element) =>
|
|
26
|
+
new Proxy(element, {
|
|
27
|
+
get(el, prop) {
|
|
28
|
+
const bucket = bucketFor(el);
|
|
29
|
+
if (bucket && prop in bucket) return bucket[prop];
|
|
30
|
+
const value = el[prop];
|
|
31
|
+
return typeof value === 'function' ? value.bind(el) : value;
|
|
32
|
+
},
|
|
33
|
+
set(el, prop, value) {
|
|
34
|
+
const bucket = bucketFor(el);
|
|
35
|
+
if (bucket && prop in bucket) {
|
|
36
|
+
bucket[prop] = value;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (bucket && globalThis.__vibe?.debug && !(prop in el)) {
|
|
40
|
+
console.warn(
|
|
41
|
+
`[vibe] handler assigned undeclared "this.${prop}" — it lands on the element, not component state. Declare it in component({...}) to make it reactive.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
el[prop] = value;
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
has(el, prop) {
|
|
48
|
+
const bucket = bucketFor(el);
|
|
49
|
+
return (bucket && prop in bucket) || prop in el;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
globalThis.$this = resolveThisScope;
|