@ape-egg/vibe 4.0.0 → 4.0.1

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 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`).
3
+ **Version 4.0.1** — 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": "4.0.0",
3
+ "version": "4.0.1",
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",
@@ -484,6 +484,16 @@ export const forceRemount = (el, debug = false) => {
484
484
  processSingle(el, debug);
485
485
  };
486
486
 
487
+ // A src still carrying an @[…] binding is not a URL — it's source material
488
+ // (an iteration or branch template the renderer hasn't consumed yet, or an
489
+ // outlet whose scope owner hasn't resolved it). Fetching it verbatim is never
490
+ // right: initializeBlock substitutes loop-scoped srcs, hydration remounts
491
+ // reactive outlets, and this pass only mounts real URLs.
492
+ const hasUnresolvedSrc = (el) => {
493
+ BINDING_REGEX.lastIndex = 0;
494
+ return BINDING_REGEX.test(el.getAttribute('src'));
495
+ };
496
+
487
497
  // Check if an element is nested inside another unprocessed component[src]
488
498
  const isNestedInUnprocessedComponent = (el, rootElement) => {
489
499
  let parent = el.parentElement;
@@ -1050,11 +1060,12 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
1050
1060
  return;
1051
1061
  }
1052
1062
 
1053
- // Only process top-level components skip those nested inside other
1063
+ // Only process top-level components with a fetchable src skip unresolved
1064
+ // binding srcs (never fetchable), and skip those nested inside other
1054
1065
  // unprocessed component[src] elements (they're slot content that will
1055
1066
  // be revealed when the parent component finalizes).
1056
1067
  const topLevel = Array.from(allComponents).filter(
1057
- (el) => !isNestedInUnprocessedComponent(el, rootElement)
1068
+ (el) => !hasUnresolvedSrc(el) && !isNestedInUnprocessedComponent(el, rootElement)
1058
1069
  );
1059
1070
 
1060
1071
  if (topLevel.length === 0) {
@@ -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 = '4.0.0';
3
+ export const VERSION = '4.0.1';
4
4
 
5
5
  // Debug logger name
6
6
  export const DEBUGGER_NAME = '[vibe-debug]:';
@@ -493,6 +493,24 @@ const resolveSlotContentBindings = (el, scopedState, aliases) => {
493
493
  }
494
494
  };
495
495
 
496
+ // Substitute the loop-scoped `@[expr]` parts of a component's src with their
497
+ // values from this block's scope. Only exprs whose dependencies include a loop
498
+ // alias are resolved — everything else keeps its binding form (see the caller
499
+ // in initializeBlock for why). An expr that evaluates to undefined stays raw
500
+ // too, so the unresolved-src invariant keeps the wrapper unfetchable instead
501
+ // of composing a garbage URL.
502
+ const resolveScopedSrc = (el, scopedState, scopeKeys) => {
503
+ const src = el.getAttribute('src');
504
+ BINDING_REGEX.lastIndex = 0;
505
+ if (!BINDING_REGEX.test(src)) return;
506
+ const resolved = src.replace(BINDING_REGEX, (match, expr) =>
507
+ extractDependencies(expr).some((dep) => scopeKeys.has(dep))
508
+ ? (evalInScope(expr, scopedState, el) ?? match)
509
+ : match,
510
+ );
511
+ if (resolved !== src) el.setAttribute('src', resolved);
512
+ };
513
+
496
514
  // For <component src> elements inside an iteration instance, evaluate any
497
515
  // `@[expr]` attribute bindings against the iteration's scoped state and route
498
516
  // every resolved value through the global iteration-prop registry. The prop
@@ -1016,10 +1034,22 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
1016
1034
  // el.innerHTML (next microtask, when MutationObserver fires) the inactive branch templates
1017
1035
  // would be gone. cloneNode(true) does not copy expando JS properties, so we must (re)capture
1018
1036
  // _vibeSlotContent on every clone.
1037
+ //
1038
+ // Also resolve loop-scoped src bindings here, BEFORE parse() reads the
1039
+ // attribute: the loop locals die with this render, so a src composed from
1040
+ // them (`src="/x/@[item.slug].html"`) must become a literal URL now. Left as
1041
+ // a binding it registers in this block's tree, and the remount transport
1042
+ // (_vibeSrcBinding → data-vibe-src on the finalized wrapper) re-evaluates it
1043
+ // against GLOBAL state on reparse — clobbering the mounted row with a fetch
1044
+ // of a garbage URL. Exprs that read no loop alias stay raw: a reactive
1045
+ // outlet (`src="@[page.src]"`) keeps its global binding, and an alias owned
1046
+ // by a deeper loop resolves when that loop clones its own instances.
1019
1047
  const components = parseContainer.querySelectorAll('component[src], div.component[src]');
1048
+ const scopeKeys = overlayKeysOf(scopedState);
1020
1049
  for (let i = 0; i < components.length; i++) {
1021
1050
  const el = components[i];
1022
1051
  if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
1052
+ if (scopeKeys) resolveScopedSrc(el, scopedState, scopeKeys);
1023
1053
  }
1024
1054
 
1025
1055
  // Give this row its own component ids (compiled mode) before parse() reads the