@ape-egg/vibe 3.0.4 → 3.0.5

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.4** — 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 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`).
4
4
 
5
5
  ## Security model & CSP
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
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",
@@ -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 {
@@ -857,6 +857,10 @@ const processSingle = (el, debug) => {
857
857
  }
858
858
 
859
859
  newWrapper.innerHTML = transformedHtml;
860
+ // Park binding-valued src while the wrapper still lives in the inert
861
+ // document — adoption into the live document is what starts image
862
+ // loads, so this is the last moment a raw `@[...]` src is harmless.
863
+ parkFetchableSrc(newWrapper);
860
864
  if (firstComponentId !== null) {
861
865
  newWrapper.setAttribute('data-vibe-component-id', firstComponentId);
862
866
  }
@@ -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 = '3.0.5';
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 = [
@@ -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
- const isRemountTrigger =
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 { ITERATION_REGEX, ITERATION_START_REGEX, CONDITIONAL_START_REGEX } from './constants.js';
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
@@ -13,6 +13,8 @@ import {
13
13
  DEHYDRATE_CLASS_OR_ATTR,
14
14
  THIS_PROP_REGEX,
15
15
  STATE_THIS_PROP_REGEX,
16
+ FETCH_SRC_ATTRS,
17
+ FETCH_SRC_ELEMENTS,
16
18
  } from './constants.js';
17
19
  import { rewriteHandlerAliases } from './loop-scope.js';
18
20
 
@@ -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);
@@ -128,6 +157,13 @@ const captureAttributeBindings = (element, aliasSet) => {
128
157
  }
129
158
  }
130
159
 
160
+ if (srcToPark) {
161
+ for (const [name, value] of srcToPark) {
162
+ element.setAttribute(`data-vibe-${name}`, value);
163
+ element.removeAttribute(name);
164
+ }
165
+ }
166
+
131
167
  return {
132
168
  attributes: Object.keys(attributes).length > 0 ? attributes : null,
133
169
  nameBindings: nameBindings.length > 0 ? nameBindings : null,