@ape-egg/vibe 4.1.4 → 4.2.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 +4 -4
- package/component.js +4 -3
- package/llms.txt +1 -1
- package/package.json +4 -3
- package/runtime/affected.js +17 -1
- package/runtime/cleanup.js +12 -2
- package/runtime/component-registry.js +113 -0
- package/runtime/component.js +42 -31
- package/runtime/constants.js +25 -1
- package/runtime/hydrate.js +14 -4
- package/runtime/index.js +3 -1
- package/runtime/iterate.js +59 -32
- package/runtime/iteration-utils.js +5 -4
- package/runtime/parse.js +24 -22
- package/runtime/pre-compiled-manifest.js +14 -9
- package/runtime/reconcile.js +14 -7
- package/runtime/staging.js +5 -4
- package/runtime/utils.js +10 -6
- package/runtime/vibe-css.js +33 -9
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vibe
|
|
2
2
|
|
|
3
|
-
**Version 4.1
|
|
3
|
+
**Version 4.2.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
|
|
|
@@ -197,7 +197,7 @@ For state scoped to a single component, import `component` from `@ape-egg/vibe/c
|
|
|
197
197
|
How it works:
|
|
198
198
|
|
|
199
199
|
1. `component({...})` generates a unique id (e.g. `_c0`, `_c1`) and registers the state at `$[id]`
|
|
200
|
-
2. The `<script>` and every following sibling is tagged with `data-vibe-component-id="<id>"`
|
|
200
|
+
2. The `<script>` and every following sibling is tagged with `data-vibe-internal-component-id="<id>"`
|
|
201
201
|
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
|
|
202
202
|
4. When the component leaves the DOM, its state entry is freed automatically
|
|
203
203
|
|
|
@@ -221,7 +221,7 @@ Multi-segment paths (`@[this.user.profile.name]`), conditionals (`<!-- if this.e
|
|
|
221
221
|
|
|
222
222
|
How it works:
|
|
223
223
|
|
|
224
|
-
1. `component({...})` claims the nearest unprocessed `<component>` (or `<div class="component">`) wrapper, registers state at `$[id]`, and tags the wrapper with `data-vibe-component-id`
|
|
224
|
+
1. `component({...})` claims the nearest unprocessed `<component>` (or `<div class="component">`) wrapper, registers state at `$[id]`, and tags the wrapper with `data-vibe-internal-component-id`
|
|
225
225
|
2. Internally it triggers the boot pipeline, which initializes `window.$`, parses the DOM, hydrates bindings, and starts the MutationObserver — exactly once, even if multiple `<component>` blocks call `component()`
|
|
226
226
|
3. From there, `@[this.X]`, `onclick="this.fn()"`, and `<!-- if this.X -->` work as documented
|
|
227
227
|
|
|
@@ -340,7 +340,7 @@ Skip reactive processing for an element:
|
|
|
340
340
|
These are used by the runtime — don't repurpose them in your code:
|
|
341
341
|
|
|
342
342
|
- `window.__vibeManifest`, `window.__vibeCompiling`, `window.__vibeComponents`, `window.__vibeIterProps` — internal registries
|
|
343
|
-
- `data-vibe-component-id`, `data-vibe-iter-prop` — element-level bookkeeping attributes (set automatically)
|
|
343
|
+
- `data-vibe-internal-component-id`, `data-vibe-internal-iter-prop` — element-level bookkeeping attributes (set automatically)
|
|
344
344
|
|
|
345
345
|
### Deep Reactivity
|
|
346
346
|
|
package/component.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generateComponentId } from './runtime/component.js';
|
|
2
|
+
import { componentIdOf, assignComponentId } from './runtime/component-registry.js';
|
|
2
3
|
import { ensureBoot } from './boot.js';
|
|
3
4
|
|
|
4
5
|
const component = (state = {}, config) => {
|
|
@@ -11,7 +12,7 @@ const component = (state = {}, config) => {
|
|
|
11
12
|
(child) => child.matches?.('script[type="module"]') && /component\s*\(/.test(child.textContent),
|
|
12
13
|
);
|
|
13
14
|
if (!ownsCall) return false;
|
|
14
|
-
const existingId = el
|
|
15
|
+
const existingId = componentIdOf(el);
|
|
15
16
|
return !existingId || !ns.components[existingId];
|
|
16
17
|
});
|
|
17
18
|
|
|
@@ -20,10 +21,10 @@ const component = (state = {}, config) => {
|
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
let componentId = wrapper
|
|
24
|
+
let componentId = componentIdOf(wrapper);
|
|
24
25
|
if (!componentId) {
|
|
25
26
|
componentId = generateComponentId();
|
|
26
|
-
wrapper
|
|
27
|
+
assignComponentId(wrapper, componentId);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
ns.components[componentId] = state;
|
package/llms.txt
CHANGED
|
@@ -179,7 +179,7 @@ For state scoped to a single component (form drafts, toggles, derived view-state
|
|
|
179
179
|
How it works:
|
|
180
180
|
|
|
181
181
|
1. `component({...})` generates a unique id (e.g. `_c0`) and stores state at `$[id]`
|
|
182
|
-
2. The `<script>` and every following sibling get `data-vibe-component-id="<id>"`
|
|
182
|
+
2. The `<script>` and every following sibling get `data-vibe-internal-component-id="<id>"`
|
|
183
183
|
3. Inside that subtree, `@[this.X.Y]` is rewritten to `@[_c0.X.Y]`; `onclick="this.fn()"` becomes `onclick="$['_c0'].fn()"`; `$.this.x = v` (in event handlers) becomes `$['_c0'].x = v`
|
|
184
184
|
4. When the component leaves the DOM, its bucket is freed automatically
|
|
185
185
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ape-egg/vibe",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Runtime-first reactivity for plain HTML
|
|
5
|
+
"description": "Runtime-first reactivity for plain HTML \u2014 no build step, no virtual DOM, no new syntax to learn",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"homepage": "https://vibe.korte.kim",
|
|
8
8
|
"repository": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"./component": "./component.js",
|
|
15
15
|
"./spa": "./spa.js",
|
|
16
16
|
"./hot-module-refresh": "./hot-module-refresh.js",
|
|
17
|
-
"./vibe.css": "./vibe.css"
|
|
17
|
+
"./vibe.css": "./vibe.css",
|
|
18
|
+
"./constants": "./runtime/constants.js"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"index.js",
|
package/runtime/affected.js
CHANGED
|
@@ -55,7 +55,7 @@ const attrBindingsOf = (tree) => {
|
|
|
55
55
|
const resolvedInner = resolveThisPath(m[1], tree.element);
|
|
56
56
|
ms.push({ outer: m[0], inner: m[1], resolvedInner, tokens: tokensOf(resolvedInner) });
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
out.push({ attrName, matches: ms });
|
|
59
59
|
}
|
|
60
60
|
tree._ab = out;
|
|
61
61
|
tree._abSrc = tree.attributes;
|
|
@@ -407,6 +407,22 @@ const recursive = (
|
|
|
407
407
|
|
|
408
408
|
for (const { attrName, matches: attrMatches } of attrBindingsOf(tree)) {
|
|
409
409
|
const attrValue = tree.attributes[attrName];
|
|
410
|
+
if (!attrMatches.length) {
|
|
411
|
+
if (isInitialHydration) {
|
|
412
|
+
affected.push({
|
|
413
|
+
type: "attribute",
|
|
414
|
+
attrName,
|
|
415
|
+
attrValue,
|
|
416
|
+
matchOuter: null,
|
|
417
|
+
matchInner: null,
|
|
418
|
+
element: tree.element,
|
|
419
|
+
scopedState: scopedStateForHydration,
|
|
420
|
+
binding: null,
|
|
421
|
+
wasConnected: !!tree.element?.isConnected,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
410
426
|
for (const m of attrMatches) {
|
|
411
427
|
const shouldAffect =
|
|
412
428
|
isInitialHydration || bindingAffected(m.tokens, keySet, state, newState);
|
package/runtime/cleanup.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { componentIdOf } from './component-registry.js';
|
|
1
2
|
import { debugLog } from './debug.js';
|
|
2
3
|
import { isInert } from './inert.js';
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
PHASE_READY,
|
|
6
|
+
FOUC_CLASS_OR_ATTR,
|
|
7
|
+
COMPONENT_ID_ATTR,
|
|
8
|
+
DEFER_ATTR_PREFIX,
|
|
9
|
+
} from './constants.js';
|
|
4
10
|
|
|
5
11
|
const skipInert = (root) => ({
|
|
6
12
|
acceptNode: (node) => (isInert(node, root) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT),
|
|
@@ -42,7 +48,7 @@ export const shouldCleanup = (rootElement) => {
|
|
|
42
48
|
continue;
|
|
43
49
|
}
|
|
44
50
|
for (const attr of el.attributes) {
|
|
45
|
-
if (attr.name.startsWith(
|
|
51
|
+
if (attr.name.startsWith(DEFER_ATTR_PREFIX)) continue;
|
|
46
52
|
if (el._vibeBoundAttrs?.has(attr.name)) continue;
|
|
47
53
|
if (/@\[.+?\]/.test(attr.value)) {
|
|
48
54
|
return false;
|
|
@@ -56,6 +62,10 @@ export const shouldCleanup = (rootElement) => {
|
|
|
56
62
|
export const cleanup = (rootElement, debug = false) => {
|
|
57
63
|
rootElement.offsetHeight;
|
|
58
64
|
|
|
65
|
+
document.querySelectorAll(`[${COMPONENT_ID_ATTR}]`).forEach((el) => {
|
|
66
|
+
if (!isInert(el, document.body)) componentIdOf(el);
|
|
67
|
+
});
|
|
68
|
+
|
|
59
69
|
const isClass = FOUC_CLASS_OR_ATTR.startsWith('.');
|
|
60
70
|
const cleanName = FOUC_CLASS_OR_ATTR.replace(/^\./, '').replace(/^\[/, '').replace(/\]$/, '');
|
|
61
71
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMPONENT_ID_ATTR,
|
|
3
|
+
CONDITIONAL_START_REGEX,
|
|
4
|
+
ITERATION_START_REGEX,
|
|
5
|
+
} from './constants.js';
|
|
6
|
+
const ids = new WeakMap();
|
|
7
|
+
const refs = new Map();
|
|
8
|
+
|
|
9
|
+
const ATTR = COMPONENT_ID_ATTR;
|
|
10
|
+
|
|
11
|
+
const remember = (element, id) => {
|
|
12
|
+
ids.set(element, id);
|
|
13
|
+
let bucket = refs.get(id);
|
|
14
|
+
if (!bucket) refs.set(id, (bucket = new Set()));
|
|
15
|
+
bucket.add(new WeakRef(element));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const componentIdOf = (element) => {
|
|
19
|
+
const known = ids.get(element);
|
|
20
|
+
if (known) return known;
|
|
21
|
+
const carried = element.getAttribute?.(ATTR);
|
|
22
|
+
if (!carried) return null;
|
|
23
|
+
remember(element, carried);
|
|
24
|
+
if (element.isConnected) element.removeAttribute(ATTR);
|
|
25
|
+
return carried;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const assignComponentId = (element, id) => remember(element, id);
|
|
29
|
+
|
|
30
|
+
export const componentIdTaken = (id) => {
|
|
31
|
+
const bucket = refs.get(id);
|
|
32
|
+
if (!bucket) return false;
|
|
33
|
+
for (const ref of bucket) {
|
|
34
|
+
const element = ref.deref();
|
|
35
|
+
if (!element) {
|
|
36
|
+
bucket.delete(ref);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (element.isConnected) return true;
|
|
40
|
+
}
|
|
41
|
+
if (!bucket.size) refs.delete(id);
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const closestComponentWrapper = (element) => {
|
|
46
|
+
for (let current = element; current; current = current.parentElement) {
|
|
47
|
+
if (componentIdOf(current)) return current;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const closestComponentId = (element) => {
|
|
53
|
+
const wrapper = element ? closestComponentWrapper(element) : null;
|
|
54
|
+
return wrapper ? componentIdOf(wrapper) : null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const eachComponentIn = (root, visit) => {
|
|
58
|
+
if (!root || (root.nodeType !== 1 && root.nodeType !== 11)) return;
|
|
59
|
+
if (root.nodeType === 1) {
|
|
60
|
+
const id = componentIdOf(root);
|
|
61
|
+
if (id) visit(root, id);
|
|
62
|
+
}
|
|
63
|
+
const walker = (root.ownerDocument ?? document).createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
64
|
+
let element;
|
|
65
|
+
while ((element = walker.nextNode())) {
|
|
66
|
+
const id = componentIdOf(element);
|
|
67
|
+
if (id) visit(element, id);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const insideDirectiveBlock = (element, root) => {
|
|
72
|
+
for (let node = element; node && node !== root; node = node.parentNode) {
|
|
73
|
+
let depth = 0;
|
|
74
|
+
for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) {
|
|
75
|
+
if (sibling.nodeType !== 8) continue;
|
|
76
|
+
const text = sibling.textContent.trim();
|
|
77
|
+
if (text === '/if' || text === '/each') depth++;
|
|
78
|
+
else if (CONDITIONAL_START_REGEX.test(text) || ITERATION_START_REGEX.test(text)) {
|
|
79
|
+
if (!depth) return true;
|
|
80
|
+
depth--;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const ingestComponentIds = (root) => {
|
|
88
|
+
if (!root || (root.nodeType !== 1 && root.nodeType !== 11)) return;
|
|
89
|
+
const claim = (element) => {
|
|
90
|
+
const carried = element.getAttribute(ATTR);
|
|
91
|
+
if (!carried) return;
|
|
92
|
+
if (insideDirectiveBlock(element, root)) return;
|
|
93
|
+
remember(element, carried);
|
|
94
|
+
element.removeAttribute(ATTR);
|
|
95
|
+
};
|
|
96
|
+
if (root.nodeType === 1) claim(root);
|
|
97
|
+
root.querySelectorAll(`[${ATTR}]`).forEach(claim);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const iterProps = new WeakSet();
|
|
101
|
+
|
|
102
|
+
export const markIterProp = (element) => iterProps.add(element);
|
|
103
|
+
|
|
104
|
+
export const hasIterProp = (element) => iterProps.has(element);
|
|
105
|
+
|
|
106
|
+
export const eachIterPropIn = (root, visit) => {
|
|
107
|
+
if (!root || (root.nodeType !== 1 && root.nodeType !== 11)) return;
|
|
108
|
+
const walker = (root.ownerDocument ?? document).createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
109
|
+
let element;
|
|
110
|
+
while ((element = walker.nextNode())) {
|
|
111
|
+
if (iterProps.has(element)) visit(element);
|
|
112
|
+
}
|
|
113
|
+
};
|
package/runtime/component.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
componentIdOf,
|
|
3
|
+
componentIdTaken,
|
|
4
|
+
closestComponentWrapper,
|
|
5
|
+
eachComponentIn,
|
|
6
|
+
ingestComponentIds,
|
|
7
|
+
markIterProp,
|
|
8
|
+
hasIterProp,
|
|
9
|
+
} from './component-registry.js';
|
|
1
10
|
import { debugLog } from './debug.js';
|
|
2
11
|
import { shouldCleanup } from './cleanup.js';
|
|
3
12
|
import { isInert } from './inert.js';
|
|
@@ -10,6 +19,14 @@ import {
|
|
|
10
19
|
STATE_THIS_PROP_REGEX,
|
|
11
20
|
CONDITIONAL_START_REGEX,
|
|
12
21
|
ITERATION_START_REGEX,
|
|
22
|
+
COMPONENT_ID_PREFIX,
|
|
23
|
+
MODULE_SCRIPT_SELECTOR,
|
|
24
|
+
COMPONENT_ID_ATTR,
|
|
25
|
+
DEFER_ATTR_PREFIX,
|
|
26
|
+
DEFERRED_SRC_ATTR,
|
|
27
|
+
KEY_ATTR,
|
|
28
|
+
COMPONENT_SRC_SELECTOR,
|
|
29
|
+
FOUC_CLASS_OR_ATTR,
|
|
13
30
|
} from './constants.js';
|
|
14
31
|
import { evalInScope } from './utils.js';
|
|
15
32
|
import { bumpIterPropGeneration, parkFetchableSrc } from './iteration-utils.js';
|
|
@@ -31,7 +48,7 @@ export { activeOutgoingRoots, isComponentWrapper };
|
|
|
31
48
|
let componentCounter = 1000000;
|
|
32
49
|
|
|
33
50
|
export const generateComponentId = () => {
|
|
34
|
-
return
|
|
51
|
+
return `${COMPONENT_ID_PREFIX}${componentCounter++}`;
|
|
35
52
|
};
|
|
36
53
|
|
|
37
54
|
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -41,7 +58,7 @@ export const trackComponentOwnership = () => {};
|
|
|
41
58
|
export const releaseOrphanedComponentState = (collectedIds) => {
|
|
42
59
|
if (!collectedIds || collectedIds.size === 0) return;
|
|
43
60
|
for (const id of collectedIds) {
|
|
44
|
-
if (
|
|
61
|
+
if (componentIdTaken(id)) continue;
|
|
45
62
|
delete window.__vibe?.components?.[id];
|
|
46
63
|
delete window.$[id];
|
|
47
64
|
runComponentCleanups(id);
|
|
@@ -94,13 +111,7 @@ const createScopedDollar = (componentId) => {
|
|
|
94
111
|
|
|
95
112
|
export const collectComponentIds = (node, into = new Set()) => {
|
|
96
113
|
if (!node) return into;
|
|
97
|
-
|
|
98
|
-
const id = node.getAttribute('data-vibe-component-id');
|
|
99
|
-
if (id) into.add(id);
|
|
100
|
-
node.querySelectorAll?.('[data-vibe-component-id]').forEach((el) => {
|
|
101
|
-
into.add(el.getAttribute('data-vibe-component-id'));
|
|
102
|
-
});
|
|
103
|
-
}
|
|
114
|
+
eachComponentIn(node, (el, id) => into.add(id));
|
|
104
115
|
return into;
|
|
105
116
|
};
|
|
106
117
|
|
|
@@ -164,14 +175,14 @@ const collectMountedModuleScripts = (nodes, out) => {
|
|
|
164
175
|
continue;
|
|
165
176
|
}
|
|
166
177
|
if (depth > 0 || node.nodeType !== 1) continue;
|
|
167
|
-
if (node.matches?.(
|
|
178
|
+
if (node.matches?.(MODULE_SCRIPT_SELECTOR)) out.push(node);
|
|
168
179
|
else collectMountedModuleScripts(node.childNodes, out);
|
|
169
180
|
}
|
|
170
181
|
};
|
|
171
182
|
|
|
172
183
|
const advanceComponentCounterPastIds = (root) => {
|
|
173
|
-
root
|
|
174
|
-
const m =
|
|
184
|
+
eachComponentIn(root, (el, id) => {
|
|
185
|
+
const m = id.match(new RegExp(`^${COMPONENT_ID_PREFIX}(\\d+)$`));
|
|
175
186
|
if (m) componentCounter = Math.max(componentCounter, Number(m[1]) + 1);
|
|
176
187
|
});
|
|
177
188
|
};
|
|
@@ -220,8 +231,8 @@ const executeScriptUnit = (componentId, content, hasImports, scopedDollar, compo
|
|
|
220
231
|
};
|
|
221
232
|
|
|
222
233
|
const wrapperSetupScript = (wrapper) => {
|
|
223
|
-
for (const s of wrapper.querySelectorAll(
|
|
224
|
-
if (s
|
|
234
|
+
for (const s of wrapper.querySelectorAll(MODULE_SCRIPT_SELECTOR)) {
|
|
235
|
+
if (closestComponentWrapper(s) === wrapper) return s;
|
|
225
236
|
}
|
|
226
237
|
return null;
|
|
227
238
|
};
|
|
@@ -239,10 +250,10 @@ const runVibeModuleScripts = (scripts, silent = false) => {
|
|
|
239
250
|
if (!rawContent) continue;
|
|
240
251
|
script.__vibeExecuted = true;
|
|
241
252
|
|
|
242
|
-
const wrapper = script
|
|
253
|
+
const wrapper = closestComponentWrapper(script);
|
|
243
254
|
const componentId =
|
|
244
255
|
wrapper && wrapperSetupScript(wrapper) === script
|
|
245
|
-
? wrapper
|
|
256
|
+
? componentIdOf(wrapper)
|
|
246
257
|
: generateComponentId();
|
|
247
258
|
|
|
248
259
|
const { content, hasImports } = transformScriptContent(rawContent);
|
|
@@ -359,8 +370,8 @@ const tagScriptSiblings = (script, componentId) => {
|
|
|
359
370
|
if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'module') {
|
|
360
371
|
break;
|
|
361
372
|
}
|
|
362
|
-
if (!sibling.hasAttribute(
|
|
363
|
-
sibling.setAttribute(
|
|
373
|
+
if (!sibling.hasAttribute(COMPONENT_ID_ATTR)) {
|
|
374
|
+
sibling.setAttribute(COMPONENT_ID_ATTR, componentId);
|
|
364
375
|
rewriteThisBindings(sibling, componentId);
|
|
365
376
|
}
|
|
366
377
|
sibling = sibling.nextElementSibling;
|
|
@@ -488,7 +499,7 @@ const processSingle = (el, debug) => {
|
|
|
488
499
|
if (!props) {
|
|
489
500
|
props = {};
|
|
490
501
|
Array.from(el.attributes).forEach((attr) => {
|
|
491
|
-
if (attr.name !== 'src' && attr.name !== 'key' && !attr.name.startsWith(
|
|
502
|
+
if (attr.name !== 'src' && attr.name !== 'key' && !attr.name.startsWith(DEFER_ATTR_PREFIX)) {
|
|
492
503
|
props[attr.name] = attr.value;
|
|
493
504
|
}
|
|
494
505
|
});
|
|
@@ -572,37 +583,37 @@ const processSingle = (el, debug) => {
|
|
|
572
583
|
newWrapper.innerHTML = transformedHtml;
|
|
573
584
|
parkFetchableSrc(newWrapper);
|
|
574
585
|
if (firstComponentId !== null) {
|
|
575
|
-
newWrapper.setAttribute(
|
|
586
|
+
newWrapper.setAttribute(COMPONENT_ID_ATTR, firstComponentId);
|
|
576
587
|
}
|
|
577
588
|
newWrapper._vibeRawSource = html;
|
|
578
589
|
newWrapper._vibeMountedSrc = src;
|
|
579
590
|
newWrapper._vibeRemountProps = props;
|
|
580
591
|
newWrapper._vibeSlotContent = children;
|
|
581
|
-
const srcBinding = el._vibeSrcBinding ?? el.getAttribute(
|
|
582
|
-
if (srcBinding) newWrapper.setAttribute(
|
|
583
|
-
const keyBinding = el._vibeKeyBinding ?? el.getAttribute(
|
|
584
|
-
if (keyBinding) newWrapper.setAttribute(
|
|
592
|
+
const srcBinding = el._vibeSrcBinding ?? el.getAttribute(DEFERRED_SRC_ATTR);
|
|
593
|
+
if (srcBinding) newWrapper.setAttribute(DEFERRED_SRC_ATTR, srcBinding);
|
|
594
|
+
const keyBinding = el._vibeKeyBinding ?? el.getAttribute(KEY_ATTR);
|
|
595
|
+
if (keyBinding) newWrapper.setAttribute(KEY_ATTR, keyBinding);
|
|
585
596
|
if (el._vibeMountedKey !== undefined) newWrapper._vibeMountedKey = el._vibeMountedKey;
|
|
586
597
|
if (el._vibeIterPropIds) {
|
|
587
598
|
newWrapper._vibeIterPropIds = el._vibeIterPropIds;
|
|
588
599
|
el._vibeIterPropIds = null;
|
|
589
600
|
}
|
|
590
|
-
if (el
|
|
591
|
-
newWrapper
|
|
592
|
-
el.removeAttribute('data-vibe-iter-prop');
|
|
601
|
+
if (hasIterProp(el)) {
|
|
602
|
+
markIterProp(newWrapper);
|
|
593
603
|
bumpIterPropGeneration();
|
|
594
604
|
}
|
|
595
605
|
if (el._vibeIterPropExprs) {
|
|
596
606
|
newWrapper._vibeIterPropExprs = el._vibeIterPropExprs;
|
|
597
607
|
el._vibeIterPropExprs = null;
|
|
598
608
|
}
|
|
599
|
-
newWrapper.setAttribute(
|
|
609
|
+
newWrapper.setAttribute(FOUC_CLASS_OR_ATTR, '');
|
|
600
610
|
el._vibeReplacedBy = newWrapper;
|
|
601
611
|
if (el._vibeOutgoing) {
|
|
602
612
|
stageIncoming(el, newWrapper);
|
|
603
613
|
} else {
|
|
604
614
|
el.replaceWith(newWrapper);
|
|
605
615
|
}
|
|
616
|
+
ingestComponentIds(newWrapper);
|
|
606
617
|
debugLog(fromCache ? PHASE_FETCH_CACHED : PHASE_FETCH, src, debug);
|
|
607
618
|
|
|
608
619
|
notifyChanged(registeredComponentIds);
|
|
@@ -615,13 +626,13 @@ const processSingle = (el, debug) => {
|
|
|
615
626
|
const tryReveal = () => {
|
|
616
627
|
if (!newWrapper.isConnected) {
|
|
617
628
|
abandonStaged(newWrapper);
|
|
618
|
-
newWrapper.removeAttribute(
|
|
629
|
+
newWrapper.removeAttribute(FOUC_CLASS_OR_ATTR);
|
|
619
630
|
unfouc();
|
|
620
631
|
return;
|
|
621
632
|
}
|
|
622
633
|
if (!scriptsDone || !shouldCleanup(newWrapper)) return;
|
|
623
634
|
commitStaged(newWrapper);
|
|
624
|
-
newWrapper.removeAttribute(
|
|
635
|
+
newWrapper.removeAttribute(FOUC_CLASS_OR_ATTR);
|
|
625
636
|
unfouc();
|
|
626
637
|
};
|
|
627
638
|
const unfouc = window.$.on('afterDomMutation', tryReveal);
|
|
@@ -658,7 +669,7 @@ const processSingle = (el, debug) => {
|
|
|
658
669
|
|
|
659
670
|
export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
660
671
|
const debug = !!config?.debug;
|
|
661
|
-
const allComponents = rootElement.querySelectorAll(
|
|
672
|
+
const allComponents = rootElement.querySelectorAll(COMPONENT_SRC_SELECTOR);
|
|
662
673
|
|
|
663
674
|
if (allComponents.length === 0) {
|
|
664
675
|
if (onComplete) queueMicrotask(() => onComplete());
|
package/runtime/constants.js
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
|
-
export const VERSION = '4.1
|
|
1
|
+
export const VERSION = '4.2.1';
|
|
2
2
|
|
|
3
3
|
export const DEBUGGER_NAME = '[vibe-debug]:';
|
|
4
4
|
export const FOUC_CLASS_OR_ATTR = 'vibe-fouc';
|
|
5
5
|
export const DEHYDRATE_CLASS_OR_ATTR = 'vibe-dehydrate';
|
|
6
|
+
export const STAGED_ATTR = 'vibe-staged';
|
|
7
|
+
|
|
8
|
+
export const DEFER_ATTR_PREFIX = 'data-vibe-';
|
|
9
|
+
export const INTERNAL_ATTR_PREFIX = `${DEFER_ATTR_PREFIX}internal-`;
|
|
10
|
+
export const COMPONENT_ID_ATTR = `${INTERNAL_ATTR_PREFIX}component-id`;
|
|
11
|
+
export const KEY_ATTR = `${INTERNAL_ATTR_PREFIX}key`;
|
|
12
|
+
export const BATCH_ATTR = `${INTERNAL_ATTR_PREFIX}batch`;
|
|
13
|
+
export const NAMEBIND_ATTR = `${INTERNAL_ATTR_PREFIX}namebind`;
|
|
14
|
+
export const CSS_COVER_ATTR = `${INTERNAL_ATTR_PREFIX}cover`;
|
|
15
|
+
export const CSS_LAYOUT_ATTR = `${INTERNAL_ATTR_PREFIX}css`;
|
|
16
|
+
export const DEFERRED_SRC_ATTR = `${DEFER_ATTR_PREFIX}src`;
|
|
17
|
+
|
|
18
|
+
export const MODULE_SCRIPT_TYPE = 'vibe-module';
|
|
19
|
+
export const MODULE_SCRIPT_SELECTOR = `script[type="${MODULE_SCRIPT_TYPE}"]`;
|
|
20
|
+
|
|
21
|
+
export const HYPERSPEED_DIR = 'vibe-hyperspeed';
|
|
22
|
+
export const MANIFEST_SUFFIX = '.manifest.js';
|
|
23
|
+
|
|
24
|
+
export const COMPONENT_ID_PREFIX = '_c';
|
|
25
|
+
export const COMPONENT_ID_REGEX = new RegExp(`^${COMPONENT_ID_PREFIX}\\d+$`);
|
|
26
|
+
export const ITER_PROP_PREFIX = '_p';
|
|
27
|
+
export const ITER_PROP_PATH = '__vibe.iterProps';
|
|
28
|
+
|
|
29
|
+
export const COMPONENT_SRC_SELECTOR = 'component[src], div.component[src]';
|
|
6
30
|
|
|
7
31
|
export const PHASE_ATTACH = 'Attached';
|
|
8
32
|
export const PHASE_MANIFEST = 'Manifested';
|
package/runtime/hydrate.js
CHANGED
|
@@ -2,7 +2,15 @@ import { updateIteration } from './iterate.js';
|
|
|
2
2
|
import { updateConditional, managedNodes } from './conditionals.js';
|
|
3
3
|
import { liveComponentWrapper, remountComponent, forceRemount } from './component.js';
|
|
4
4
|
import { isComponentWrapper, isRemountTrigger, parkRootFor, parkBinding } from './staging.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
VALUE_ATTRS,
|
|
7
|
+
DOM_PROPERTIES,
|
|
8
|
+
BINDING_REGEX,
|
|
9
|
+
PURE_BINDING_REGEX,
|
|
10
|
+
NAMEBIND_ATTR,
|
|
11
|
+
DEFER_ATTR_PREFIX,
|
|
12
|
+
DEFERRED_SRC_ATTR,
|
|
13
|
+
} from './constants.js';
|
|
6
14
|
import {
|
|
7
15
|
evalInScope,
|
|
8
16
|
resolveCaseInsensitivePath,
|
|
@@ -60,8 +68,8 @@ const applyNameBinding = (aff, effectiveState) => {
|
|
|
60
68
|
element._vibeNameBindings.delete(nameBinding);
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
if (element.hasAttribute(
|
|
64
|
-
element.removeAttribute(
|
|
71
|
+
if (element.hasAttribute(NAMEBIND_ATTR)) {
|
|
72
|
+
element.removeAttribute(NAMEBIND_ATTR);
|
|
65
73
|
}
|
|
66
74
|
} catch (e) {
|
|
67
75
|
console.error('Error hydrating name binding:', e);
|
|
@@ -71,6 +79,8 @@ const applyNameBinding = (aff, effectiveState) => {
|
|
|
71
79
|
const applyAttributeBinding = (aff, effectiveState) => {
|
|
72
80
|
const { attrName, attrValue, element } = aff;
|
|
73
81
|
try {
|
|
82
|
+
const deferredName = `${DEFER_ATTR_PREFIX}${attrName}`;
|
|
83
|
+
if (element.hasAttribute(deferredName)) element.removeAttribute(deferredName);
|
|
74
84
|
const isPureBinding = attrValue.match(PURE_BINDING_REGEX);
|
|
75
85
|
const isDomProperty = DOM_PROPERTIES.includes(attrName);
|
|
76
86
|
const isValueAttr =
|
|
@@ -209,7 +219,7 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
209
219
|
remountComponent(live, newSrc);
|
|
210
220
|
if (live._vibeOutgoing) (outgoingRoots ??= []).push(live);
|
|
211
221
|
} else if (live.hasAttribute('src')) {
|
|
212
|
-
live.setAttribute(
|
|
222
|
+
live.setAttribute(DEFERRED_SRC_ATTR, attrValue);
|
|
213
223
|
live.removeAttribute('src');
|
|
214
224
|
}
|
|
215
225
|
return;
|
package/runtime/index.js
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
PHASE_MUTATE,
|
|
23
23
|
PHASE_HYPERSPEED,
|
|
24
24
|
PHASE_READY,
|
|
25
|
+
STAGED_ATTR,
|
|
26
|
+
FOUC_CLASS_OR_ATTR,
|
|
25
27
|
} from './constants.js';
|
|
26
28
|
import { processComponent, abortComponentFetch, collectComponentIds, releaseOrphanedComponentState, renderComponentTemplate, executeCompiledComponentScripts } from './component.js';
|
|
27
29
|
import { configureComponentCache, clearComponentCache } from './component-cache.js';
|
|
@@ -545,7 +547,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
545
547
|
const lateReady = [];
|
|
546
548
|
let lateReadyWatch = null;
|
|
547
549
|
const mountsSettled = () =>
|
|
548
|
-
!document.querySelector(
|
|
550
|
+
!document.querySelector(`[${STAGED_ATTR}], [${FOUC_CLASS_OR_ATTR}]`) && shouldCleanup(rootElement);
|
|
549
551
|
const flushLateReady = () => {
|
|
550
552
|
if (lateReady.length === 0 || !mountsSettled()) return;
|
|
551
553
|
if (lateReadyWatch) {
|
package/runtime/iterate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ingestComponentIds, markIterProp, eachIterPropIn } from './component-registry.js';
|
|
1
2
|
import parse from './parse.js';
|
|
2
3
|
import affected, { nodeSubscriberOf } from './affected.js';
|
|
3
4
|
import hydrate from './hydrate.js';
|
|
@@ -13,6 +14,17 @@ import {
|
|
|
13
14
|
VALUE_ATTRS,
|
|
14
15
|
DOM_PROPERTIES,
|
|
15
16
|
THIS_PROP_REGEX,
|
|
17
|
+
MODULE_SCRIPT_SELECTOR,
|
|
18
|
+
COMPONENT_ID_PREFIX,
|
|
19
|
+
COMPONENT_ID_REGEX,
|
|
20
|
+
BATCH_ATTR,
|
|
21
|
+
NAMEBIND_ATTR,
|
|
22
|
+
ITER_PROP_PREFIX,
|
|
23
|
+
ITER_PROP_PATH,
|
|
24
|
+
COMPONENT_ID_ATTR,
|
|
25
|
+
COMPONENT_SRC_SELECTOR,
|
|
26
|
+
DEFER_ATTR_PREFIX,
|
|
27
|
+
INTERNAL_ATTR_PREFIX,
|
|
16
28
|
} from './constants.js';
|
|
17
29
|
|
|
18
30
|
import * as compiled from './pre-compiled-iterations.js';
|
|
@@ -40,10 +52,10 @@ const hasNestedStructures = (tree) => {
|
|
|
40
52
|
};
|
|
41
53
|
|
|
42
54
|
const hasComponentSrc = (templateEl) =>
|
|
43
|
-
!!templateEl.querySelector?.(
|
|
55
|
+
!!templateEl.querySelector?.(COMPONENT_SRC_SELECTOR);
|
|
44
56
|
|
|
45
57
|
const hasInlinedComponentScript = (templateEl) =>
|
|
46
|
-
!!templateEl.querySelector?.(
|
|
58
|
+
!!templateEl.querySelector?.(MODULE_SCRIPT_SELECTOR);
|
|
47
59
|
|
|
48
60
|
const canUseBatchRender = (template) =>
|
|
49
61
|
!globalThis.__vibe?.forceClonePath &&
|
|
@@ -61,6 +73,14 @@ const BATCH_ATTR_BINDING_REGEX = new RegExp(
|
|
|
61
73
|
String.raw`(\s)([\w-]+)="@\[(${BATCH_BINDING_INNER})\]"`,
|
|
62
74
|
'g',
|
|
63
75
|
);
|
|
76
|
+
const DEFERRED_ATTR_REGEX = new RegExp(
|
|
77
|
+
String.raw`(\s)${DEFER_ATTR_PREFIX}(?!${INTERNAL_ATTR_PREFIX.slice(DEFER_ATTR_PREFIX.length)})(?=[\w-])`,
|
|
78
|
+
'g',
|
|
79
|
+
);
|
|
80
|
+
const realAttrName = (name) =>
|
|
81
|
+
name.startsWith(DEFER_ATTR_PREFIX) && !name.startsWith(INTERNAL_ATTR_PREFIX)
|
|
82
|
+
? name.slice(DEFER_ATTR_PREFIX.length)
|
|
83
|
+
: name;
|
|
64
84
|
|
|
65
85
|
const mapTagSpans = (html, fn) => {
|
|
66
86
|
let out = '';
|
|
@@ -98,11 +118,13 @@ const isValueStyleAttr = (attrName) =>
|
|
|
98
118
|
attrName.startsWith('aria-') ||
|
|
99
119
|
attrName.startsWith('on');
|
|
100
120
|
|
|
101
|
-
const CID_ROOT_REGEX =
|
|
102
|
-
|
|
121
|
+
const CID_ROOT_REGEX = new RegExp(
|
|
122
|
+
String.raw`('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|(?<![\w$.])${COMPONENT_ID_PREFIX}(\d+)\b`,
|
|
123
|
+
'g',
|
|
124
|
+
);
|
|
103
125
|
|
|
104
126
|
export const canonicalizeComponentIds = (expr) =>
|
|
105
|
-
expr.replace(CID_ROOT_REGEX, (m, literal, id) => (literal !== undefined ? literal : `$['
|
|
127
|
+
expr.replace(CID_ROOT_REGEX, (m, literal, id) => (literal !== undefined ? literal : `$['${COMPONENT_ID_PREFIX}${id}']`));
|
|
106
128
|
|
|
107
129
|
const nameBindingEmit = (exprSrc) =>
|
|
108
130
|
'${(()=>{const _v=_e(()=>(' + exprSrc + '));return _v?\' \'+_v+\'=""\':\'\';})()}';
|
|
@@ -119,23 +141,24 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, ancho
|
|
|
119
141
|
const attrs = [...el.attributes];
|
|
120
142
|
for (let a = 0; a < attrs.length; a++) {
|
|
121
143
|
const attr = attrs[a];
|
|
122
|
-
|
|
144
|
+
const prop = realAttrName(attr.name);
|
|
145
|
+
if (!DOM_PROPERTIES.includes(prop)) continue;
|
|
123
146
|
const m = attr.value.match(PURE_BINDING_REGEX);
|
|
124
147
|
if (!m) continue;
|
|
125
148
|
let expr = m[1];
|
|
126
149
|
if (componentId) expr = expr.replace(THIS_PROP_REGEX, `$['${componentId}'].$1`);
|
|
127
150
|
indexes.push(domPropertyWrites.length);
|
|
128
|
-
domPropertyWrites.push({ prop
|
|
151
|
+
domPropertyWrites.push({ prop, expr });
|
|
129
152
|
}
|
|
130
153
|
if (indexes.length > 0) {
|
|
131
|
-
el.setAttribute(
|
|
154
|
+
el.setAttribute(BATCH_ATTR, indexes.join(','));
|
|
132
155
|
}
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
let templateHtml = tplClone.innerHTML.trim();
|
|
136
159
|
|
|
137
160
|
templateHtml = templateHtml.replace(
|
|
138
|
-
|
|
161
|
+
new RegExp(String.raw`\s${NAMEBIND_ATTR}="((?:@\[(?:[^[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+\])+)"`, 'g'),
|
|
139
162
|
(_, bindings) =>
|
|
140
163
|
bindings.replace(/@\[(?:[^[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+\]/g, (b) => ' ' + b + '=""'),
|
|
141
164
|
);
|
|
@@ -160,7 +183,7 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, ancho
|
|
|
160
183
|
let code = escaped;
|
|
161
184
|
|
|
162
185
|
let needsCiWalker = false;
|
|
163
|
-
code = mapTagSpans(code, (tag) => tag.replace(BATCH_NAME_BINDING_REGEX, (_, _ws, expr) => {
|
|
186
|
+
code = mapTagSpans(code, (tag) => tag.replace(DEFERRED_ATTR_REGEX, '$1').replace(BATCH_NAME_BINDING_REGEX, (_, _ws, expr) => {
|
|
164
187
|
let decExpr = decodeEntities(expr);
|
|
165
188
|
if (decExpr.includes('[') || decExpr.includes('(')) {
|
|
166
189
|
return nameBindingEmit(decExpr);
|
|
@@ -241,7 +264,7 @@ export const releaseOrphanedIterationProps = (nodes) => {
|
|
|
241
264
|
el._vibeIterPropIds = null;
|
|
242
265
|
};
|
|
243
266
|
free(node);
|
|
244
|
-
node
|
|
267
|
+
eachIterPropIn(node, free);
|
|
245
268
|
}
|
|
246
269
|
};
|
|
247
270
|
|
|
@@ -275,7 +298,7 @@ const resolveSlotContentBindings = (el, scopedState, aliases) => {
|
|
|
275
298
|
return undefined;
|
|
276
299
|
}
|
|
277
300
|
if (value === undefined) return undefined;
|
|
278
|
-
id =
|
|
301
|
+
id = `${ITER_PROP_PREFIX}${__vibeIterPropCounter++}`;
|
|
279
302
|
registry[id] = value;
|
|
280
303
|
idByExpr.set(expr, id);
|
|
281
304
|
(el._vibeIterPropExprs = el._vibeIterPropExprs || []).push({ id, attrName: null, expr });
|
|
@@ -309,7 +332,7 @@ const resolveSlotContentBindings = (el, scopedState, aliases) => {
|
|
|
309
332
|
}
|
|
310
333
|
if (idByExpr.size) {
|
|
311
334
|
el._vibeSlotContent = rewritten;
|
|
312
|
-
el
|
|
335
|
+
markIterProp(el);
|
|
313
336
|
bumpIterPropGeneration();
|
|
314
337
|
}
|
|
315
338
|
};
|
|
@@ -330,9 +353,9 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
330
353
|
for (let n = 0; n < nodes.length; n++) {
|
|
331
354
|
const node = nodes[n];
|
|
332
355
|
if (node.nodeType !== 1) continue;
|
|
333
|
-
const components = node.matches?.(
|
|
334
|
-
? [node, ...node.querySelectorAll(
|
|
335
|
-
: [...node.querySelectorAll(
|
|
356
|
+
const components = node.matches?.(COMPONENT_SRC_SELECTOR)
|
|
357
|
+
? [node, ...node.querySelectorAll(COMPONENT_SRC_SELECTOR)]
|
|
358
|
+
: [...node.querySelectorAll(COMPONENT_SRC_SELECTOR)];
|
|
336
359
|
for (let i = 0; i < components.length; i++) {
|
|
337
360
|
const el = components[i];
|
|
338
361
|
const attrs = el.attributes;
|
|
@@ -346,7 +369,7 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
346
369
|
/\bthis\b/.test(expr) ||
|
|
347
370
|
(aliases && extractDependencies(expr).some((d) => aliases.has(d)));
|
|
348
371
|
if (!usesLocalScope) {
|
|
349
|
-
el
|
|
372
|
+
markIterProp(el);
|
|
350
373
|
bumpIterPropGeneration();
|
|
351
374
|
el._vibeIterPropExprs = el._vibeIterPropExprs || [];
|
|
352
375
|
continue;
|
|
@@ -355,10 +378,10 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
355
378
|
const value = evalInScope(expr, scopedState, el);
|
|
356
379
|
if (value === undefined) continue;
|
|
357
380
|
const registry = ensureIterPropsRegistry();
|
|
358
|
-
const id =
|
|
381
|
+
const id = `${ITER_PROP_PREFIX}${__vibeIterPropCounter++}`;
|
|
359
382
|
registry[id] = value;
|
|
360
|
-
el.setAttribute(attr.name, `@[window
|
|
361
|
-
el
|
|
383
|
+
el.setAttribute(attr.name, `@[window.${ITER_PROP_PATH}.${id}]`);
|
|
384
|
+
markIterProp(el);
|
|
362
385
|
bumpIterPropGeneration();
|
|
363
386
|
(el._vibeIterPropIds = el._vibeIterPropIds || []).push(id);
|
|
364
387
|
(el._vibeIterPropExprs = el._vibeIterPropExprs || []).push({ id, attrName: attr.name, expr });
|
|
@@ -401,7 +424,7 @@ const refreshIterationComponentProps = (clonedNodes, scopedState) => {
|
|
|
401
424
|
return changed;
|
|
402
425
|
};
|
|
403
426
|
|
|
404
|
-
const REGISTRY_SLOT_REGEX =
|
|
427
|
+
const REGISTRY_SLOT_REGEX = new RegExp(`${ITER_PROP_PATH.replace('.', String.raw`\.`)}\\.(${ITER_PROP_PREFIX}\\d+)`);
|
|
405
428
|
const forceRegistryBackedIterationUpdates = (tree, state, manifest, parentScope, changedSlots) => {
|
|
406
429
|
if (!tree) return;
|
|
407
430
|
if (tree.type === 'iteration') {
|
|
@@ -435,12 +458,12 @@ const applyDomPropertyWrites = (instances, array, state, itemAlias, indexAlias,
|
|
|
435
458
|
if (!root || root.nodeType !== 1) continue;
|
|
436
459
|
const localState = { ...state, [itemAlias]: array[i], [indexAlias]: i };
|
|
437
460
|
const tagged = [];
|
|
438
|
-
if (root.matches?.(
|
|
439
|
-
const found = root.querySelectorAll?.(
|
|
461
|
+
if (root.matches?.(`[${BATCH_ATTR}]`)) tagged.push(root);
|
|
462
|
+
const found = root.querySelectorAll?.(`[${BATCH_ATTR}]`);
|
|
440
463
|
if (found) for (let f = 0; f < found.length; f++) tagged.push(found[f]);
|
|
441
464
|
for (let t = 0; t < tagged.length; t++) {
|
|
442
465
|
const el = tagged[t];
|
|
443
|
-
const indexes = el.getAttribute(
|
|
466
|
+
const indexes = el.getAttribute(BATCH_ATTR).split(',');
|
|
444
467
|
for (let k = 0; k < indexes.length; k++) {
|
|
445
468
|
const { prop, expr } = domPropertyWrites[indexes[k] | 0];
|
|
446
469
|
const value = evalInScope(expr, localState, el);
|
|
@@ -458,7 +481,7 @@ const applyDomPropertyWrites = (instances, array, state, itemAlias, indexAlias,
|
|
|
458
481
|
el.removeAttribute(prop);
|
|
459
482
|
}
|
|
460
483
|
}
|
|
461
|
-
el.removeAttribute(
|
|
484
|
+
el.removeAttribute(BATCH_ATTR);
|
|
462
485
|
}
|
|
463
486
|
}
|
|
464
487
|
};
|
|
@@ -520,6 +543,7 @@ const renderBatch = (iterationNode, array, state, parent, endComment, parentScop
|
|
|
520
543
|
instances[i] = { element: kids[i], item: array[i], index: i };
|
|
521
544
|
}
|
|
522
545
|
|
|
546
|
+
ingestComponentIds(frag);
|
|
523
547
|
parent.insertBefore(frag, endComment);
|
|
524
548
|
iterationNode.runtime.instances = instances;
|
|
525
549
|
|
|
@@ -628,15 +652,15 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
628
652
|
return cloned;
|
|
629
653
|
};
|
|
630
654
|
|
|
631
|
-
const COMPONENT_ID =
|
|
655
|
+
const COMPONENT_ID = COMPONENT_ID_REGEX;
|
|
632
656
|
|
|
633
657
|
const isolateInlinedComponentIds = (container) => {
|
|
634
658
|
const remap = new Map();
|
|
635
|
-
for (const el of container.querySelectorAll(
|
|
636
|
-
const oldId = el.getAttribute(
|
|
659
|
+
for (const el of container.querySelectorAll(`[${COMPONENT_ID_ATTR}]`)) {
|
|
660
|
+
const oldId = el.getAttribute(COMPONENT_ID_ATTR);
|
|
637
661
|
if (!COMPONENT_ID.test(oldId)) continue;
|
|
638
662
|
if (!remap.has(oldId)) remap.set(oldId, generateComponentId());
|
|
639
|
-
el.setAttribute(
|
|
663
|
+
el.setAttribute(COMPONENT_ID_ATTR, remap.get(oldId));
|
|
640
664
|
}
|
|
641
665
|
if (!remap.size) return false;
|
|
642
666
|
|
|
@@ -652,13 +676,13 @@ const isolateInlinedComponentIds = (container) => {
|
|
|
652
676
|
const walk = (node) => {
|
|
653
677
|
if (node.nodeType === 1) {
|
|
654
678
|
for (const attr of node.attributes) {
|
|
655
|
-
if (attr.value.includes(
|
|
679
|
+
if (attr.value.includes(COMPONENT_ID_PREFIX)) {
|
|
656
680
|
const next = rewrite(attr.value);
|
|
657
681
|
if (next !== attr.value) attr.value = next;
|
|
658
682
|
}
|
|
659
683
|
}
|
|
660
684
|
for (const child of node.childNodes) walk(child);
|
|
661
|
-
} else if (node.nodeType === 3 && node.textContent.includes(
|
|
685
|
+
} else if (node.nodeType === 3 && node.textContent.includes(COMPONENT_ID_PREFIX)) {
|
|
662
686
|
node.textContent = rewrite(node.textContent);
|
|
663
687
|
}
|
|
664
688
|
};
|
|
@@ -684,7 +708,7 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
|
|
|
684
708
|
}
|
|
685
709
|
}
|
|
686
710
|
|
|
687
|
-
const components = parseContainer.querySelectorAll(
|
|
711
|
+
const components = parseContainer.querySelectorAll(COMPONENT_SRC_SELECTOR);
|
|
688
712
|
const scopeKeys = overlayKeysOf(scopedState);
|
|
689
713
|
for (let i = 0; i < components.length; i++) {
|
|
690
714
|
const el = components[i];
|
|
@@ -717,6 +741,8 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
|
|
|
717
741
|
hydrate(affectedElements, scopedState);
|
|
718
742
|
}
|
|
719
743
|
|
|
744
|
+
ingestComponentIds(parseContainer);
|
|
745
|
+
|
|
720
746
|
return {
|
|
721
747
|
element: firstElement,
|
|
722
748
|
tree,
|
|
@@ -1179,6 +1205,7 @@ const renderInstances = (iterationNode, array, state, manifest, parentScope) =>
|
|
|
1179
1205
|
clonedNodes: built.clonedNodes, scopedState: built.scopedState,
|
|
1180
1206
|
});
|
|
1181
1207
|
}
|
|
1208
|
+
ingestComponentIds(frag);
|
|
1182
1209
|
parent.insertBefore(frag, endComment);
|
|
1183
1210
|
iterationNode.runtime.instances = instances;
|
|
1184
1211
|
stampScopes(iterationNode, manifest, parentScope);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { eachIterPropIn } from './component-registry.js';
|
|
1
2
|
import {
|
|
2
3
|
ITERATION_REGEX,
|
|
3
4
|
ITERATION_START_REGEX,
|
|
@@ -5,6 +6,7 @@ import {
|
|
|
5
6
|
BINDING_REGEX,
|
|
6
7
|
FETCH_SRC_ATTRS,
|
|
7
8
|
FETCH_SRC_SELECTOR,
|
|
9
|
+
DEFER_ATTR_PREFIX,
|
|
8
10
|
} from './constants.js';
|
|
9
11
|
|
|
10
12
|
export const parkFetchableSrc = (root) => {
|
|
@@ -14,7 +16,7 @@ export const parkFetchableSrc = (root) => {
|
|
|
14
16
|
const value = elements[i].getAttribute(attr);
|
|
15
17
|
BINDING_REGEX.lastIndex = 0;
|
|
16
18
|
if (value && BINDING_REGEX.test(value)) {
|
|
17
|
-
elements[i].setAttribute(
|
|
19
|
+
elements[i].setAttribute(`${DEFER_ATTR_PREFIX}${attr}`, value);
|
|
18
20
|
elements[i].removeAttribute(attr);
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -55,7 +57,7 @@ export const markBoundValues = (root, html) => {
|
|
|
55
57
|
continue;
|
|
56
58
|
}
|
|
57
59
|
for (const attr of node.attributes) {
|
|
58
|
-
if (!attr.name.startsWith(
|
|
60
|
+
if (!attr.name.startsWith(DEFER_ATTR_PREFIX) && attr.value.includes('@[')) {
|
|
59
61
|
(node._vibeBoundAttrs ??= new Set()).add(attr.name);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -236,8 +238,7 @@ export const iterPropWrappersOf = (node) => {
|
|
|
236
238
|
return list;
|
|
237
239
|
}
|
|
238
240
|
const list = [];
|
|
239
|
-
|
|
240
|
-
if (found) for (let i = 0; i < found.length; i++) list.push(found[i]);
|
|
241
|
+
eachIterPropIn(node, (el) => list.push(el));
|
|
241
242
|
node._vibeIterPropWrappers = { gen: iterPropGeneration, list };
|
|
242
243
|
return list;
|
|
243
244
|
};
|
package/runtime/parse.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closestComponentId } from './component-registry.js';
|
|
1
2
|
import {
|
|
2
3
|
findEndComment,
|
|
3
4
|
findEndCommentIndex,
|
|
@@ -14,15 +15,16 @@ import {
|
|
|
14
15
|
STATE_THIS_PROP_REGEX,
|
|
15
16
|
FETCH_SRC_ATTRS,
|
|
16
17
|
FETCH_SRC_ELEMENTS,
|
|
18
|
+
DEFERRED_SRC_ATTR,
|
|
19
|
+
KEY_ATTR,
|
|
20
|
+
NAMEBIND_ATTR,
|
|
21
|
+
DEFER_ATTR_PREFIX,
|
|
22
|
+
INTERNAL_ATTR_PREFIX,
|
|
17
23
|
} from './constants.js';
|
|
18
24
|
import { rewriteHandlerAliases } from './loop-scope.js';
|
|
19
25
|
import './this-scope.js';
|
|
20
26
|
|
|
21
|
-
const findComponentIdForElement = (element) =>
|
|
22
|
-
if (!element?.closest) return null;
|
|
23
|
-
const wrapper = element.closest('[data-vibe-component-id]');
|
|
24
|
-
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
25
|
-
};
|
|
27
|
+
const findComponentIdForElement = (element) => closestComponentId(element);
|
|
26
28
|
|
|
27
29
|
const warnedStaticKeys = new Set();
|
|
28
30
|
const warnStaticKey = (key) => {
|
|
@@ -37,11 +39,11 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
37
39
|
const nodeName = element.nodeName;
|
|
38
40
|
const isFetchedComponent =
|
|
39
41
|
(nodeName === 'COMPONENT' || (nodeName === 'DIV' && element.classList?.contains('component'))) &&
|
|
40
|
-
(element.hasAttribute?.('src') || element.hasAttribute?.(
|
|
42
|
+
(element.hasAttribute?.('src') || element.hasAttribute?.(DEFERRED_SRC_ATTR));
|
|
41
43
|
|
|
42
44
|
if (isFetchedComponent) {
|
|
43
|
-
const src = element.getAttribute?.(
|
|
44
|
-
const key = element.getAttribute?.(
|
|
45
|
+
const src = element.getAttribute?.(DEFERRED_SRC_ATTR) ?? element.getAttribute?.('src');
|
|
46
|
+
const key = element.getAttribute?.(KEY_ATTR) ?? element.getAttribute?.('key');
|
|
45
47
|
const attributes = {};
|
|
46
48
|
BINDING_REGEX.lastIndex = 0;
|
|
47
49
|
if (BINDING_REGEX.test(src)) attributes.src = src;
|
|
@@ -61,30 +63,30 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
61
63
|
const attributes = {};
|
|
62
64
|
const nameBindings = [];
|
|
63
65
|
const isFetchableElement = FETCH_SRC_ELEMENTS.includes(nodeName);
|
|
64
|
-
let
|
|
66
|
+
let toPark = null;
|
|
67
|
+
let deferredNames = null;
|
|
65
68
|
|
|
66
69
|
for (let j = 0; j < element.attributes.length; j++) {
|
|
67
70
|
const attr = element.attributes[j];
|
|
68
71
|
BINDING_REGEX.lastIndex = 0;
|
|
69
72
|
|
|
70
|
-
if (attr.name ===
|
|
73
|
+
if (attr.name === NAMEBIND_ATTR) {
|
|
71
74
|
nameBindings.push(attr.value);
|
|
72
75
|
continue;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
if (
|
|
76
|
-
const realName = attr.name.slice(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
78
|
+
if (attr.name.startsWith(DEFER_ATTR_PREFIX) && !attr.name.startsWith(INTERNAL_ATTR_PREFIX)) {
|
|
79
|
+
const realName = attr.name.slice(DEFER_ATTR_PREFIX.length);
|
|
80
|
+
attributes[realName] = attr.value;
|
|
81
|
+
(deferredNames ??= new Set()).add(realName);
|
|
82
|
+
continue;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
if (isFetchableElement && FETCH_SRC_ATTRS.includes(attr.name)) {
|
|
84
86
|
BINDING_REGEX.lastIndex = 0;
|
|
85
87
|
if (BINDING_REGEX.test(attr.value)) {
|
|
86
|
-
attributes[attr.name] = attr.value;
|
|
87
|
-
(
|
|
88
|
+
if (!deferredNames?.has(attr.name)) attributes[attr.name] = attr.value;
|
|
89
|
+
(toPark ??= []).push([attr.name, attr.value]);
|
|
88
90
|
continue;
|
|
89
91
|
}
|
|
90
92
|
}
|
|
@@ -110,14 +112,14 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
BINDING_REGEX.lastIndex = 0;
|
|
113
|
-
if (BINDING_REGEX.test(attr.value)) {
|
|
115
|
+
if (BINDING_REGEX.test(attr.value) && !deferredNames?.has(attr.name)) {
|
|
114
116
|
attributes[attr.name] = attr.value;
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
if (
|
|
119
|
-
for (const [name, value] of
|
|
120
|
-
element.setAttribute(
|
|
120
|
+
if (toPark) {
|
|
121
|
+
for (const [name, value] of toPark) {
|
|
122
|
+
element.setAttribute(`${DEFER_ATTR_PREFIX}${name}`, value);
|
|
121
123
|
element.removeAttribute(name);
|
|
122
124
|
}
|
|
123
125
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { parkFetchableSrc } from './iteration-utils.js';
|
|
2
|
+
import { HYPERSPEED_DIR, MANIFEST_SUFFIX, COMPONENT_ID_PREFIX } from './constants.js';
|
|
3
|
+
|
|
4
|
+
const CID_SCOPE_BINDING_REGEX = new RegExp(String.raw`@\[${COMPONENT_ID_PREFIX}\d+\.`, 'g');
|
|
1
5
|
export const buildHyperspeedManifest = (parsedTree) => {
|
|
2
6
|
const splitByMarkers = (text) => {
|
|
3
7
|
const regex = /@\[[^\]]+\]/g;
|
|
@@ -137,30 +141,30 @@ export const buildManifestCandidatePaths = (pathname, route) => {
|
|
|
137
141
|
const prefix = rawSegments
|
|
138
142
|
.slice(0, routeSegments.length - 1)
|
|
139
143
|
.map((seg, i) => (routeSegments[i].startsWith(":") ? "$" : seg));
|
|
140
|
-
possiblePaths.push(
|
|
144
|
+
possiblePaths.push(`/${HYPERSPEED_DIR}/${[...prefix, "$"].join("/")}.html${MANIFEST_SUFFIX}`);
|
|
141
145
|
} else if (routeSegments && routeSegments.some((s) => s.startsWith(":"))) {
|
|
142
146
|
const tokenized = pathSegments.map((seg, i) => {
|
|
143
147
|
if (!routeSegments[i]?.startsWith(":")) return seg;
|
|
144
148
|
const dot = seg.indexOf(".");
|
|
145
149
|
return dot >= 0 ? "$" + seg.slice(dot) : "$";
|
|
146
150
|
});
|
|
147
|
-
possiblePaths.push(
|
|
151
|
+
possiblePaths.push(`/${HYPERSPEED_DIR}/${tokenized.join("/")}${MANIFEST_SUFFIX}`);
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
if (dirSegments.length >= 1) {
|
|
151
155
|
const subPath = dirSegments.slice(1).join("/");
|
|
152
156
|
const baseDir = "/" + dirSegments[0];
|
|
153
157
|
possiblePaths.push(
|
|
154
|
-
`${baseDir}
|
|
158
|
+
`${baseDir}/${HYPERSPEED_DIR}/${subPath ? subPath + "/" : ""}${fileName}${MANIFEST_SUFFIX}`,
|
|
155
159
|
);
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
possiblePaths.push(
|
|
162
|
+
possiblePaths.push(`/${HYPERSPEED_DIR}${pagePath}${MANIFEST_SUFFIX}`);
|
|
159
163
|
|
|
160
164
|
if (dirSegments.length > 0) {
|
|
161
165
|
const relativePath = dirSegments.join("/");
|
|
162
166
|
possiblePaths.push(
|
|
163
|
-
|
|
167
|
+
`/${HYPERSPEED_DIR}/${relativePath}/${fileName}${MANIFEST_SUFFIX}`,
|
|
164
168
|
);
|
|
165
169
|
}
|
|
166
170
|
|
|
@@ -168,7 +172,7 @@ export const buildManifestCandidatePaths = (pathname, route) => {
|
|
|
168
172
|
const tokenized = dot >= 0 ? "$" + fileName.slice(dot) : "$";
|
|
169
173
|
if (tokenized !== fileName) {
|
|
170
174
|
const dirPrefix = dirSegments.length ? `/${dirSegments.join("/")}` : "";
|
|
171
|
-
possiblePaths.push(
|
|
175
|
+
possiblePaths.push(`/${HYPERSPEED_DIR}${dirPrefix}/${tokenized}${MANIFEST_SUFFIX}`);
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
return [...new Set(possiblePaths)];
|
|
@@ -236,7 +240,7 @@ export const restoreMarkersFromManifest = (
|
|
|
236
240
|
if (hasBindings) {
|
|
237
241
|
let originalContent = restoration.parsed.join("");
|
|
238
242
|
|
|
239
|
-
originalContent = originalContent.replace(
|
|
243
|
+
originalContent = originalContent.replace(CID_SCOPE_BINDING_REGEX, "@[this.");
|
|
240
244
|
|
|
241
245
|
if (
|
|
242
246
|
element.childNodes.length === 1 &&
|
|
@@ -253,7 +257,7 @@ export const restoreMarkersFromManifest = (
|
|
|
253
257
|
for (let [attrName, attrValue] of Object.entries(
|
|
254
258
|
restoration.attributes,
|
|
255
259
|
)) {
|
|
256
|
-
attrValue = attrValue.replace(
|
|
260
|
+
attrValue = attrValue.replace(CID_SCOPE_BINDING_REGEX, "@[this.");
|
|
257
261
|
|
|
258
262
|
element.setAttribute(attrName, attrValue);
|
|
259
263
|
}
|
|
@@ -298,7 +302,7 @@ export const restoreMarkersFromManifest = (
|
|
|
298
302
|
if (hasBindings) {
|
|
299
303
|
const originalContent = restoration.parsed
|
|
300
304
|
.join("")
|
|
301
|
-
.replace(
|
|
305
|
+
.replace(CID_SCOPE_BINDING_REGEX, "@[this.");
|
|
302
306
|
|
|
303
307
|
const node = element.childNodes[index];
|
|
304
308
|
if (index === 0 && Object.keys(tree.children).length === 1) {
|
|
@@ -372,6 +376,7 @@ export const restoreMarkersFromManifest = (
|
|
|
372
376
|
|
|
373
377
|
const tpl = document.createElement("template");
|
|
374
378
|
tpl.innerHTML = restoration.template;
|
|
379
|
+
parkFetchableSrc(tpl.content);
|
|
375
380
|
endComment.parentNode.insertBefore(tpl.content, endComment);
|
|
376
381
|
}
|
|
377
382
|
}
|
package/runtime/reconcile.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parkFetchableSrc } from './iteration-utils.js';
|
|
2
|
+
import {
|
|
3
|
+
ITERATION_START_REGEX,
|
|
4
|
+
CONDITIONAL_START_REGEX,
|
|
5
|
+
DEFER_ATTR_PREFIX,
|
|
6
|
+
FOUC_CLASS_OR_ATTR,
|
|
7
|
+
} from './constants.js';
|
|
2
8
|
|
|
3
9
|
const COMMENT = 8;
|
|
4
10
|
const ELEMENT = 1;
|
|
@@ -146,9 +152,9 @@ const findElseMarker = (nodes, startIdx, endIdx) => {
|
|
|
146
152
|
return -1;
|
|
147
153
|
};
|
|
148
154
|
|
|
149
|
-
const PRESERVED_ATTRS = new Set([
|
|
150
|
-
const PRESERVED_CLASSES = new Set([
|
|
151
|
-
const isPreservedAttr = (name) => name.startsWith(
|
|
155
|
+
const PRESERVED_ATTRS = new Set([FOUC_CLASS_OR_ATTR]);
|
|
156
|
+
const PRESERVED_CLASSES = new Set([FOUC_CLASS_OR_ATTR]);
|
|
157
|
+
const isPreservedAttr = (name) => name.startsWith(DEFER_ATTR_PREFIX) || PRESERVED_ATTRS.has(name);
|
|
152
158
|
|
|
153
159
|
const mergeClass = (srcValue, live) => {
|
|
154
160
|
const tokens = new Set(srcValue.split(/\s+/).filter(Boolean));
|
|
@@ -269,9 +275,9 @@ const reconcileRange = (liveParent, liveNodes, liStart, liEnd, srcNodes, siStart
|
|
|
269
275
|
if (live._vibeProps && src.hasAttribute('src')) {
|
|
270
276
|
const srcProps = {};
|
|
271
277
|
for (const a of src.attributes) {
|
|
272
|
-
if (a.name ===
|
|
278
|
+
if (a.name === FOUC_CLASS_OR_ATTR) continue;
|
|
273
279
|
if (a.name === 'class') {
|
|
274
|
-
const kept = a.value.split(/\s+/).filter((t) => t && t !==
|
|
280
|
+
const kept = a.value.split(/\s+/).filter((t) => t && t !== FOUC_CLASS_OR_ATTR);
|
|
275
281
|
if (kept.length) srcProps.class = kept.join(' ');
|
|
276
282
|
continue;
|
|
277
283
|
}
|
|
@@ -291,7 +297,7 @@ const reconcileRange = (liveParent, liveNodes, liStart, liEnd, srcNodes, siStart
|
|
|
291
297
|
for (const [name, value] of Object.entries(srcProps)) {
|
|
292
298
|
fresh.setAttribute(name, value);
|
|
293
299
|
}
|
|
294
|
-
fresh.setAttribute(
|
|
300
|
+
fresh.setAttribute(FOUC_CLASS_OR_ATTR, '');
|
|
295
301
|
const slotHtml = src.innerHTML.trim();
|
|
296
302
|
fresh._vibeSlotContent = slotHtml;
|
|
297
303
|
fresh._vibePluginSlot = slotHtml;
|
|
@@ -458,6 +464,7 @@ export const reconcile = (target, source) => {
|
|
|
458
464
|
const log = { text: 0, attr: 0, insert: 0, remove: 0, replace: 0, changes: [] };
|
|
459
465
|
const tpl = document.createElement('template');
|
|
460
466
|
tpl.innerHTML = typeof source === 'string' ? source : source.outerHTML;
|
|
467
|
+
parkFetchableSrc(tpl.content);
|
|
461
468
|
reconcileChildren(live, [...tpl.content.childNodes], log);
|
|
462
469
|
|
|
463
470
|
return new Promise((resolve) => {
|
package/runtime/staging.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STAGED_ATTR } from './constants.js';
|
|
1
2
|
export const liveNode = (node) => {
|
|
2
3
|
let live = node;
|
|
3
4
|
while (live && live._vibeReplacedBy) live = live._vibeReplacedBy;
|
|
@@ -62,14 +63,14 @@ export const isRemountTrigger = (entry) =>
|
|
|
62
63
|
isComponentWrapper(entry.element);
|
|
63
64
|
|
|
64
65
|
export const stageIncoming = (el, newWrapper) => {
|
|
65
|
-
if (el.hasAttribute(
|
|
66
|
+
if (el.hasAttribute(STAGED_ATTR)) {
|
|
66
67
|
newWrapper._vibeCommitOld = el._vibeCommitOld;
|
|
67
68
|
activeOutgoingRoots.delete(el);
|
|
68
|
-
newWrapper.setAttribute(
|
|
69
|
+
newWrapper.setAttribute(STAGED_ATTR, '');
|
|
69
70
|
el.replaceWith(newWrapper);
|
|
70
71
|
} else {
|
|
71
72
|
newWrapper._vibeCommitOld = el;
|
|
72
|
-
newWrapper.setAttribute(
|
|
73
|
+
newWrapper.setAttribute(STAGED_ATTR, '');
|
|
73
74
|
el.removeAttribute('src');
|
|
74
75
|
el.after(newWrapper);
|
|
75
76
|
}
|
|
@@ -83,7 +84,7 @@ export const commitStaged = (newWrapper) => {
|
|
|
83
84
|
replayParked(old);
|
|
84
85
|
newWrapper._vibeCommitOld = null;
|
|
85
86
|
}
|
|
86
|
-
newWrapper.removeAttribute(
|
|
87
|
+
newWrapper.removeAttribute(STAGED_ATTR);
|
|
87
88
|
};
|
|
88
89
|
|
|
89
90
|
export const abandonStaged = (newWrapper) => {
|
package/runtime/utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closestComponentId } from './component-registry.js';
|
|
2
|
+
import { THIS_PROP_REGEX, COMPONENT_ID_PREFIX, COMPONENT_ID_REGEX } from './constants.js';
|
|
2
3
|
import { recordRead, recordAbsentRead } from './tracking.js';
|
|
3
4
|
import { reportEvalError } from './debug.js';
|
|
4
5
|
|
|
@@ -89,11 +90,14 @@ const compileExpression = (normalized) => {
|
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
const canonical = normalized.replace(
|
|
92
|
-
|
|
93
|
+
new RegExp(
|
|
94
|
+
String.raw`('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|\$\['(${COMPONENT_ID_PREFIX}\d+)'\]|((?<=[{,]\s*)[a-zA-Z_$][\w$]*(?=\s*:))|(?<![.\w$'"])[a-zA-Z_$][\w$]*`,
|
|
95
|
+
'g',
|
|
96
|
+
),
|
|
93
97
|
(match, strLit, bracketCid, objKey) => {
|
|
94
98
|
if (strLit !== undefined || objKey !== undefined) return match;
|
|
95
99
|
if (bracketCid !== undefined) return `$[${cidParamFor(bracketCid)}]`;
|
|
96
|
-
if (
|
|
100
|
+
if (COMPONENT_ID_REGEX.test(match)) return `$[${cidParamFor(match)}]`;
|
|
97
101
|
if (match === '$' || locals.has(match) || EVAL_IDENT_EXCLUDE.has(match)) return match;
|
|
98
102
|
if (!paramSet.has(match)) {
|
|
99
103
|
paramSet.add(match);
|
|
@@ -226,10 +230,10 @@ export const resolveCaseInsensitivePath = (state, path) => {
|
|
|
226
230
|
};
|
|
227
231
|
|
|
228
232
|
export const findComponentIdForElement = (element) => {
|
|
229
|
-
if (!element
|
|
233
|
+
if (!element) return null;
|
|
230
234
|
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
235
|
+
const id = closestComponentId(element);
|
|
236
|
+
if (id) return id;
|
|
233
237
|
let root = element;
|
|
234
238
|
while (root.parentNode) root = root.parentNode;
|
|
235
239
|
if (root._vibeComponentId) return root._vibeComponentId;
|
package/runtime/vibe-css.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { FOUC_CLASS_OR_ATTR, STAGED_ATTR, CSS_COVER_ATTR, CSS_LAYOUT_ATTR } from './constants.js';
|
|
2
|
+
|
|
3
|
+
export const COVER_CSS = `[${FOUC_CLASS_OR_ATTR}], .${FOUC_CLASS_OR_ATTR} { visibility: hidden }
|
|
4
|
+
[${STAGED_ATTR}] { display: none }
|
|
5
|
+
[${FOUC_CLASS_OR_ATTR}], .${FOUC_CLASS_OR_ATTR}, [${FOUC_CLASS_OR_ATTR}] *, .${FOUC_CLASS_OR_ATTR} * { transition: none !important; animation: none !important }`;
|
|
4
6
|
|
|
5
7
|
export const OPTIONAL_MARKER = '/* vibe:optional — everything below is skipped by disableVibeCss */';
|
|
6
8
|
|
|
@@ -8,21 +10,43 @@ export const LAYOUT_CSS = `${OPTIONAL_MARKER}
|
|
|
8
10
|
|
|
9
11
|
component, div.component, slot, div.slot { display: contents }`;
|
|
10
12
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
+
const sheets = new WeakMap();
|
|
14
|
+
|
|
15
|
+
const sheetRefs = (doc) => {
|
|
16
|
+
let refs = sheets.get(doc);
|
|
17
|
+
if (!refs) {
|
|
18
|
+
refs = {};
|
|
19
|
+
sheets.set(doc, refs);
|
|
20
|
+
}
|
|
21
|
+
return refs;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const claim = (doc, key, marker) => {
|
|
25
|
+
const refs = sheetRefs(doc);
|
|
26
|
+
if (refs[key]) return refs[key];
|
|
27
|
+
const served = doc.querySelector(`style[${marker}]`);
|
|
28
|
+
if (served) {
|
|
29
|
+
served.removeAttribute(marker);
|
|
30
|
+
refs[key] = served;
|
|
31
|
+
}
|
|
32
|
+
return refs[key];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const inject = (doc, key, marker, css) => {
|
|
36
|
+
if (claim(doc, key, marker)) return;
|
|
13
37
|
const style = doc.createElement('style');
|
|
14
|
-
style.setAttribute(marker, '');
|
|
15
38
|
style.textContent = css;
|
|
16
39
|
doc.head.appendChild(style);
|
|
40
|
+
sheetRefs(doc)[key] = style;
|
|
17
41
|
};
|
|
18
42
|
|
|
19
43
|
export const injectVibeCss = (doc) => {
|
|
20
|
-
inject(doc, '
|
|
21
|
-
inject(doc, '
|
|
44
|
+
inject(doc, 'cover', CSS_COVER_ATTR, COVER_CSS);
|
|
45
|
+
inject(doc, 'layout', CSS_LAYOUT_ATTR, LAYOUT_CSS);
|
|
22
46
|
};
|
|
23
47
|
|
|
24
48
|
export const removeLayoutCss = (doc) => {
|
|
25
|
-
doc
|
|
49
|
+
claim(doc, 'layout', CSS_LAYOUT_ATTR)?.remove();
|
|
26
50
|
};
|
|
27
51
|
|
|
28
52
|
export const warnIfCoverDefeated = (doc) => {
|