@ape-egg/vibe 3.0.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vibe
2
2
 
3
- **Version 3.0.5** — 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`).
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]` and event handlers like `onclick="this.method()"` or `oninput="$.this.value = ..."` are rewritten to address `$[id]`
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` references the component's bucket (rewritten at parse time); `this.value`, `this.checked`, `this.dataset` etc. still reach the DOM element since DOM properties are preserved.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "3.0.5",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "description": "Runtime-first reactivity for plain HTML — no build step, no virtual DOM, no new syntax to learn",
6
6
  "main": "index.js",
@@ -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` reads in event handlers are handled later by
510
- // parse.js (which preserves DOM properties like `this.value`).
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`);
@@ -1,6 +1,6 @@
1
1
  // The released version — check-release.js holds this in lockstep with
2
2
  // package.json, the READMEs and the CHANGELOG.
3
- export const VERSION = '3.0.5';
3
+ export const VERSION = '4.0.0';
4
4
 
5
5
  // Debug logger name
6
6
  export const DEBUGGER_NAME = '[vibe-debug]:';
@@ -176,45 +176,6 @@ export const VALUE_ATTRS = [
176
176
  // Properties that should be set directly on the DOM element (not as attributes)
177
177
  export const DOM_PROPERTIES = ['value', 'checked', 'selected'];
178
178
 
179
- // DOM element properties that should NOT be rewritten in event handlers
180
- // When parsing onclick="this.value = this.inputValue", preserve this.value (DOM) but rewrite this.inputValue (component state)
181
- export const DOM_ELEMENT_PROPERTIES = new Set([
182
- 'value',
183
- 'checked',
184
- 'selected',
185
- 'disabled',
186
- 'readOnly',
187
- 'files',
188
- 'tagName',
189
- 'nodeName',
190
- 'nodeType',
191
- 'classList',
192
- 'className',
193
- 'id',
194
- 'innerHTML',
195
- 'outerHTML',
196
- 'textContent',
197
- 'innerText',
198
- 'parentElement',
199
- 'parentNode',
200
- 'nextSibling',
201
- 'previousSibling',
202
- 'firstChild',
203
- 'lastChild',
204
- 'children',
205
- 'childNodes',
206
- 'offsetWidth',
207
- 'offsetHeight',
208
- 'clientWidth',
209
- 'clientHeight',
210
- 'scrollTop',
211
- 'scrollLeft',
212
- 'scrollWidth',
213
- 'scrollHeight',
214
- 'style',
215
- 'dataset',
216
- 'attributes',
217
- ]);
218
179
 
219
180
  // Regex for matching reactive bindings (@[expression])
220
181
  // Supports nested brackets, single-quoted and double-quoted strings inside expressions:
package/runtime/parse.js CHANGED
@@ -9,7 +9,6 @@ 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,
@@ -17,6 +16,7 @@ import {
17
16
  FETCH_SRC_ELEMENTS,
18
17
  } from './constants.js';
19
18
  import { rewriteHandlerAliases } from './loop-scope.js';
19
+ import './this-scope.js';
20
20
 
21
21
  // Walks up the DOM for the nearest component wrapper tagged by component.js.
22
22
  // Used to rewrite `this.property` in event handlers to the component's state path.
@@ -125,7 +125,11 @@ const captureAttributeBindings = (element, aliasSet) => {
125
125
 
126
126
  // Event handlers get two compile-time rewrites, computed off the original
127
127
  // value and written once:
128
- // 1. `this.property` the component's state path (component-local state).
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.
129
133
  // 2. bare loop-variable aliases → `$scope(this,'alias')` (loop-scoped
130
134
  // handlers — only when an enclosing <!-- each --> alias is in scope).
131
135
  if (attr.name.startsWith('on')) {
@@ -136,13 +140,11 @@ const captureAttributeBindings = (element, aliasSet) => {
136
140
  // `$.this.X` (component-state write through the root) must be
137
141
  // consumed as one reference BEFORE the bare `this.X` pass — that
138
142
  // pass alone would leave the `$.` prefix behind and produce
139
- // `$.$['id'].X`. Runtime-fetched components arrive with this form
140
- // 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
141
145
  // authored form, so parse meets it raw.
142
146
  v = v.replace(STATE_THIS_PROP_REGEX, (_, prop) => `$['${componentId}'].${prop}`);
143
- v = v.replace(THIS_PROP_REGEX, (match, prop) =>
144
- DOM_ELEMENT_PROPERTIES.has(prop) ? match : `$['${componentId}'].${prop}`,
145
- );
147
+ v = v.replace(THIS_PROP_REGEX, (_, prop) => `$this(this).${prop}`);
146
148
  }
147
149
  }
148
150
  if (aliasSet && aliasSet.size > 0) {
@@ -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;