@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 +1 -1
- package/package.json +1 -1
- package/runtime/component.js +13 -2
- package/runtime/constants.js +1 -1
- package/runtime/iterate.js +30 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vibe
|
|
2
2
|
|
|
3
|
-
**Version 4.0.
|
|
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
package/runtime/component.js
CHANGED
|
@@ -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
|
|
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) {
|
package/runtime/constants.js
CHANGED
package/runtime/iterate.js
CHANGED
|
@@ -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
|