@ape-egg/vibe 4.1.4 → 4.2.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 +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 +45 -29
- 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.
|
|
3
|
+
**Version 4.2.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
|
|
|
@@ -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.
|
|
3
|
+
"version": "4.2.0",
|
|
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
|
+
export const VERSION = '4.2.0';
|
|
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,15 @@ 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,
|
|
16
26
|
} from './constants.js';
|
|
17
27
|
|
|
18
28
|
import * as compiled from './pre-compiled-iterations.js';
|
|
@@ -40,10 +50,10 @@ const hasNestedStructures = (tree) => {
|
|
|
40
50
|
};
|
|
41
51
|
|
|
42
52
|
const hasComponentSrc = (templateEl) =>
|
|
43
|
-
!!templateEl.querySelector?.(
|
|
53
|
+
!!templateEl.querySelector?.(COMPONENT_SRC_SELECTOR);
|
|
44
54
|
|
|
45
55
|
const hasInlinedComponentScript = (templateEl) =>
|
|
46
|
-
!!templateEl.querySelector?.(
|
|
56
|
+
!!templateEl.querySelector?.(MODULE_SCRIPT_SELECTOR);
|
|
47
57
|
|
|
48
58
|
const canUseBatchRender = (template) =>
|
|
49
59
|
!globalThis.__vibe?.forceClonePath &&
|
|
@@ -98,11 +108,13 @@ const isValueStyleAttr = (attrName) =>
|
|
|
98
108
|
attrName.startsWith('aria-') ||
|
|
99
109
|
attrName.startsWith('on');
|
|
100
110
|
|
|
101
|
-
const CID_ROOT_REGEX =
|
|
102
|
-
|
|
111
|
+
const CID_ROOT_REGEX = new RegExp(
|
|
112
|
+
String.raw`('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|(?<![\w$.])${COMPONENT_ID_PREFIX}(\d+)\b`,
|
|
113
|
+
'g',
|
|
114
|
+
);
|
|
103
115
|
|
|
104
116
|
export const canonicalizeComponentIds = (expr) =>
|
|
105
|
-
expr.replace(CID_ROOT_REGEX, (m, literal, id) => (literal !== undefined ? literal : `$['
|
|
117
|
+
expr.replace(CID_ROOT_REGEX, (m, literal, id) => (literal !== undefined ? literal : `$['${COMPONENT_ID_PREFIX}${id}']`));
|
|
106
118
|
|
|
107
119
|
const nameBindingEmit = (exprSrc) =>
|
|
108
120
|
'${(()=>{const _v=_e(()=>(' + exprSrc + '));return _v?\' \'+_v+\'=""\':\'\';})()}';
|
|
@@ -128,14 +140,14 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, ancho
|
|
|
128
140
|
domPropertyWrites.push({ prop: attr.name, expr });
|
|
129
141
|
}
|
|
130
142
|
if (indexes.length > 0) {
|
|
131
|
-
el.setAttribute(
|
|
143
|
+
el.setAttribute(BATCH_ATTR, indexes.join(','));
|
|
132
144
|
}
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
let templateHtml = tplClone.innerHTML.trim();
|
|
136
148
|
|
|
137
149
|
templateHtml = templateHtml.replace(
|
|
138
|
-
|
|
150
|
+
new RegExp(String.raw`\s${NAMEBIND_ATTR}="((?:@\[(?:[^[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+\])+)"`, 'g'),
|
|
139
151
|
(_, bindings) =>
|
|
140
152
|
bindings.replace(/@\[(?:[^[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+\]/g, (b) => ' ' + b + '=""'),
|
|
141
153
|
);
|
|
@@ -241,7 +253,7 @@ export const releaseOrphanedIterationProps = (nodes) => {
|
|
|
241
253
|
el._vibeIterPropIds = null;
|
|
242
254
|
};
|
|
243
255
|
free(node);
|
|
244
|
-
node
|
|
256
|
+
eachIterPropIn(node, free);
|
|
245
257
|
}
|
|
246
258
|
};
|
|
247
259
|
|
|
@@ -275,7 +287,7 @@ const resolveSlotContentBindings = (el, scopedState, aliases) => {
|
|
|
275
287
|
return undefined;
|
|
276
288
|
}
|
|
277
289
|
if (value === undefined) return undefined;
|
|
278
|
-
id =
|
|
290
|
+
id = `${ITER_PROP_PREFIX}${__vibeIterPropCounter++}`;
|
|
279
291
|
registry[id] = value;
|
|
280
292
|
idByExpr.set(expr, id);
|
|
281
293
|
(el._vibeIterPropExprs = el._vibeIterPropExprs || []).push({ id, attrName: null, expr });
|
|
@@ -309,7 +321,7 @@ const resolveSlotContentBindings = (el, scopedState, aliases) => {
|
|
|
309
321
|
}
|
|
310
322
|
if (idByExpr.size) {
|
|
311
323
|
el._vibeSlotContent = rewritten;
|
|
312
|
-
el
|
|
324
|
+
markIterProp(el);
|
|
313
325
|
bumpIterPropGeneration();
|
|
314
326
|
}
|
|
315
327
|
};
|
|
@@ -330,9 +342,9 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
330
342
|
for (let n = 0; n < nodes.length; n++) {
|
|
331
343
|
const node = nodes[n];
|
|
332
344
|
if (node.nodeType !== 1) continue;
|
|
333
|
-
const components = node.matches?.(
|
|
334
|
-
? [node, ...node.querySelectorAll(
|
|
335
|
-
: [...node.querySelectorAll(
|
|
345
|
+
const components = node.matches?.(COMPONENT_SRC_SELECTOR)
|
|
346
|
+
? [node, ...node.querySelectorAll(COMPONENT_SRC_SELECTOR)]
|
|
347
|
+
: [...node.querySelectorAll(COMPONENT_SRC_SELECTOR)];
|
|
336
348
|
for (let i = 0; i < components.length; i++) {
|
|
337
349
|
const el = components[i];
|
|
338
350
|
const attrs = el.attributes;
|
|
@@ -346,7 +358,7 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
346
358
|
/\bthis\b/.test(expr) ||
|
|
347
359
|
(aliases && extractDependencies(expr).some((d) => aliases.has(d)));
|
|
348
360
|
if (!usesLocalScope) {
|
|
349
|
-
el
|
|
361
|
+
markIterProp(el);
|
|
350
362
|
bumpIterPropGeneration();
|
|
351
363
|
el._vibeIterPropExprs = el._vibeIterPropExprs || [];
|
|
352
364
|
continue;
|
|
@@ -355,10 +367,10 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
355
367
|
const value = evalInScope(expr, scopedState, el);
|
|
356
368
|
if (value === undefined) continue;
|
|
357
369
|
const registry = ensureIterPropsRegistry();
|
|
358
|
-
const id =
|
|
370
|
+
const id = `${ITER_PROP_PREFIX}${__vibeIterPropCounter++}`;
|
|
359
371
|
registry[id] = value;
|
|
360
|
-
el.setAttribute(attr.name, `@[window
|
|
361
|
-
el
|
|
372
|
+
el.setAttribute(attr.name, `@[window.${ITER_PROP_PATH}.${id}]`);
|
|
373
|
+
markIterProp(el);
|
|
362
374
|
bumpIterPropGeneration();
|
|
363
375
|
(el._vibeIterPropIds = el._vibeIterPropIds || []).push(id);
|
|
364
376
|
(el._vibeIterPropExprs = el._vibeIterPropExprs || []).push({ id, attrName: attr.name, expr });
|
|
@@ -401,7 +413,7 @@ const refreshIterationComponentProps = (clonedNodes, scopedState) => {
|
|
|
401
413
|
return changed;
|
|
402
414
|
};
|
|
403
415
|
|
|
404
|
-
const REGISTRY_SLOT_REGEX =
|
|
416
|
+
const REGISTRY_SLOT_REGEX = new RegExp(`${ITER_PROP_PATH.replace('.', String.raw`\.`)}\\.(${ITER_PROP_PREFIX}\\d+)`);
|
|
405
417
|
const forceRegistryBackedIterationUpdates = (tree, state, manifest, parentScope, changedSlots) => {
|
|
406
418
|
if (!tree) return;
|
|
407
419
|
if (tree.type === 'iteration') {
|
|
@@ -435,12 +447,12 @@ const applyDomPropertyWrites = (instances, array, state, itemAlias, indexAlias,
|
|
|
435
447
|
if (!root || root.nodeType !== 1) continue;
|
|
436
448
|
const localState = { ...state, [itemAlias]: array[i], [indexAlias]: i };
|
|
437
449
|
const tagged = [];
|
|
438
|
-
if (root.matches?.(
|
|
439
|
-
const found = root.querySelectorAll?.(
|
|
450
|
+
if (root.matches?.(`[${BATCH_ATTR}]`)) tagged.push(root);
|
|
451
|
+
const found = root.querySelectorAll?.(`[${BATCH_ATTR}]`);
|
|
440
452
|
if (found) for (let f = 0; f < found.length; f++) tagged.push(found[f]);
|
|
441
453
|
for (let t = 0; t < tagged.length; t++) {
|
|
442
454
|
const el = tagged[t];
|
|
443
|
-
const indexes = el.getAttribute(
|
|
455
|
+
const indexes = el.getAttribute(BATCH_ATTR).split(',');
|
|
444
456
|
for (let k = 0; k < indexes.length; k++) {
|
|
445
457
|
const { prop, expr } = domPropertyWrites[indexes[k] | 0];
|
|
446
458
|
const value = evalInScope(expr, localState, el);
|
|
@@ -458,7 +470,7 @@ const applyDomPropertyWrites = (instances, array, state, itemAlias, indexAlias,
|
|
|
458
470
|
el.removeAttribute(prop);
|
|
459
471
|
}
|
|
460
472
|
}
|
|
461
|
-
el.removeAttribute(
|
|
473
|
+
el.removeAttribute(BATCH_ATTR);
|
|
462
474
|
}
|
|
463
475
|
}
|
|
464
476
|
};
|
|
@@ -520,6 +532,7 @@ const renderBatch = (iterationNode, array, state, parent, endComment, parentScop
|
|
|
520
532
|
instances[i] = { element: kids[i], item: array[i], index: i };
|
|
521
533
|
}
|
|
522
534
|
|
|
535
|
+
ingestComponentIds(frag);
|
|
523
536
|
parent.insertBefore(frag, endComment);
|
|
524
537
|
iterationNode.runtime.instances = instances;
|
|
525
538
|
|
|
@@ -628,15 +641,15 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
628
641
|
return cloned;
|
|
629
642
|
};
|
|
630
643
|
|
|
631
|
-
const COMPONENT_ID =
|
|
644
|
+
const COMPONENT_ID = COMPONENT_ID_REGEX;
|
|
632
645
|
|
|
633
646
|
const isolateInlinedComponentIds = (container) => {
|
|
634
647
|
const remap = new Map();
|
|
635
|
-
for (const el of container.querySelectorAll(
|
|
636
|
-
const oldId = el.getAttribute(
|
|
648
|
+
for (const el of container.querySelectorAll(`[${COMPONENT_ID_ATTR}]`)) {
|
|
649
|
+
const oldId = el.getAttribute(COMPONENT_ID_ATTR);
|
|
637
650
|
if (!COMPONENT_ID.test(oldId)) continue;
|
|
638
651
|
if (!remap.has(oldId)) remap.set(oldId, generateComponentId());
|
|
639
|
-
el.setAttribute(
|
|
652
|
+
el.setAttribute(COMPONENT_ID_ATTR, remap.get(oldId));
|
|
640
653
|
}
|
|
641
654
|
if (!remap.size) return false;
|
|
642
655
|
|
|
@@ -652,13 +665,13 @@ const isolateInlinedComponentIds = (container) => {
|
|
|
652
665
|
const walk = (node) => {
|
|
653
666
|
if (node.nodeType === 1) {
|
|
654
667
|
for (const attr of node.attributes) {
|
|
655
|
-
if (attr.value.includes(
|
|
668
|
+
if (attr.value.includes(COMPONENT_ID_PREFIX)) {
|
|
656
669
|
const next = rewrite(attr.value);
|
|
657
670
|
if (next !== attr.value) attr.value = next;
|
|
658
671
|
}
|
|
659
672
|
}
|
|
660
673
|
for (const child of node.childNodes) walk(child);
|
|
661
|
-
} else if (node.nodeType === 3 && node.textContent.includes(
|
|
674
|
+
} else if (node.nodeType === 3 && node.textContent.includes(COMPONENT_ID_PREFIX)) {
|
|
662
675
|
node.textContent = rewrite(node.textContent);
|
|
663
676
|
}
|
|
664
677
|
};
|
|
@@ -684,7 +697,7 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
|
|
|
684
697
|
}
|
|
685
698
|
}
|
|
686
699
|
|
|
687
|
-
const components = parseContainer.querySelectorAll(
|
|
700
|
+
const components = parseContainer.querySelectorAll(COMPONENT_SRC_SELECTOR);
|
|
688
701
|
const scopeKeys = overlayKeysOf(scopedState);
|
|
689
702
|
for (let i = 0; i < components.length; i++) {
|
|
690
703
|
const el = components[i];
|
|
@@ -717,6 +730,8 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
|
|
|
717
730
|
hydrate(affectedElements, scopedState);
|
|
718
731
|
}
|
|
719
732
|
|
|
733
|
+
ingestComponentIds(parseContainer);
|
|
734
|
+
|
|
720
735
|
return {
|
|
721
736
|
element: firstElement,
|
|
722
737
|
tree,
|
|
@@ -1179,6 +1194,7 @@ const renderInstances = (iterationNode, array, state, manifest, parentScope) =>
|
|
|
1179
1194
|
clonedNodes: built.clonedNodes, scopedState: built.scopedState,
|
|
1180
1195
|
});
|
|
1181
1196
|
}
|
|
1197
|
+
ingestComponentIds(frag);
|
|
1182
1198
|
parent.insertBefore(frag, endComment);
|
|
1183
1199
|
iterationNode.runtime.instances = instances;
|
|
1184
1200
|
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) => {
|