@ape-egg/vibe 1.2.0 → 1.3.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/CHANGELOG.md +62 -0
- package/README.md +1 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/compiler/compile.rs +204 -51
- package/compiler/src/compiler/manifest_builder.rs +432 -0
- package/compiler/src/compiler/mod.rs +3 -0
- package/compiler/src/compiler/state_extractor.rs +148 -0
- package/compiler/src/compiler/value_stamper.rs +222 -0
- package/compiler/src/config.rs +0 -5
- package/compiler/src/main.rs +45 -20
- package/compiler/src/parser/html.rs +22 -8
- package/index.js +30 -86
- package/package.json +1 -1
- package/runtime/affected.js +12 -6
- package/runtime/cleanup.js +45 -24
- package/runtime/component-state.js +1 -1
- package/runtime/component.js +20 -13
- package/runtime/constants.js +41 -10
- package/runtime/debug.js +1 -0
- package/runtime/hydrate.js +6 -1
- package/runtime/hyperspeed.js +425 -0
- package/runtime/index.js +236 -49
- package/runtime/iterate.js +3 -0
- package/runtime/parse.js +21 -29
- package/runtime/scope.js +5 -25
- package/runtime/utils.js +4 -13
- package/vibe.css +4 -2
package/runtime/index.js
CHANGED
|
@@ -17,12 +17,17 @@ import {
|
|
|
17
17
|
PHASE_OBSERVE,
|
|
18
18
|
PHASE_UPDATE,
|
|
19
19
|
PHASE_MUTATE,
|
|
20
|
-
|
|
20
|
+
PHASE_HYPERSPEED,
|
|
21
21
|
} from './constants.js';
|
|
22
22
|
import { processComponent, abortComponentFetch } from './component.js';
|
|
23
23
|
import { debugLog } from './debug.js';
|
|
24
24
|
import { shouldCleanup, cleanup } from './cleanup.js';
|
|
25
25
|
import { generateComponentId, executeComponentScript } from './component-state.js';
|
|
26
|
+
import {
|
|
27
|
+
buildHyperspeedManifest,
|
|
28
|
+
hyperspeedManifest,
|
|
29
|
+
restoreMarkersFromManifest,
|
|
30
|
+
} from './hyperspeed.js';
|
|
26
31
|
|
|
27
32
|
// Wire up cross-module dependency after all modules are loaded
|
|
28
33
|
setRenderAllConditionals(renderAllConditionals);
|
|
@@ -132,73 +137,239 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
|
|
|
132
137
|
return { hydratedCount, iteratedCount, evaluatedCount };
|
|
133
138
|
};
|
|
134
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Deep clone a tree node (to avoid mutating hyperspeed template)
|
|
142
|
+
*/
|
|
143
|
+
const deepCloneNode = (node) => {
|
|
144
|
+
if (!node || typeof node !== 'object') return node;
|
|
145
|
+
|
|
146
|
+
const cloned = { ...node };
|
|
147
|
+
|
|
148
|
+
// Clone children recursively
|
|
149
|
+
if (node.children && typeof node.children === 'object') {
|
|
150
|
+
cloned.children = {};
|
|
151
|
+
for (const key in node.children) {
|
|
152
|
+
cloned.children[key] = deepCloneNode(node.children[key]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clone meta if it exists
|
|
157
|
+
if (node.meta && typeof node.meta === 'object') {
|
|
158
|
+
cloned.meta = { ...node.meta };
|
|
159
|
+
if (node.meta.template) {
|
|
160
|
+
cloned.meta.template = { ...node.meta.template };
|
|
161
|
+
}
|
|
162
|
+
if (node.meta.branches) {
|
|
163
|
+
cloned.meta.branches = { ...node.meta.branches };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clone runtime if it exists
|
|
168
|
+
if (node.runtime && typeof node.runtime === 'object') {
|
|
169
|
+
cloned.runtime = { ...node.runtime };
|
|
170
|
+
if (Array.isArray(node.runtime.instances)) {
|
|
171
|
+
cloned.runtime.instances = [...node.runtime.instances];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return cloned;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Merge hyperspeed manifest with runtime manifest
|
|
180
|
+
* Hyperspeed provides structure/metadata, runtime populates DOM references
|
|
181
|
+
*/
|
|
182
|
+
const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
183
|
+
if (!hyperspeedTree) return runtimeTree;
|
|
184
|
+
if (!runtimeTree) return hyperspeedTree;
|
|
185
|
+
|
|
186
|
+
// Start with deep clone of hyperspeed (foundation)
|
|
187
|
+
const merged = deepCloneNode(hyperspeedTree);
|
|
188
|
+
|
|
189
|
+
// Helper to recursively augment hyperspeed with runtime data
|
|
190
|
+
const augmentWithRuntime = (mergedNode, runtimeNode) => {
|
|
191
|
+
if (!runtimeNode) return;
|
|
192
|
+
|
|
193
|
+
// Populate DOM references from runtime
|
|
194
|
+
if (runtimeNode.element) {
|
|
195
|
+
mergedNode.element = runtimeNode.element;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Populate parsed data from runtime
|
|
199
|
+
if (runtimeNode.parsed) {
|
|
200
|
+
mergedNode.parsed = runtimeNode.parsed;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// For iterations: populate all meta and runtime from runtime
|
|
204
|
+
if (mergedNode.type === 'iteration' && runtimeNode.type === 'iteration') {
|
|
205
|
+
// Copy entire meta object from runtime (all properties needed)
|
|
206
|
+
mergedNode.meta = runtimeNode.meta;
|
|
207
|
+
// Copy runtime object (instances, etc.)
|
|
208
|
+
mergedNode.runtime = runtimeNode.runtime;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// For conditionals: populate all meta from runtime
|
|
212
|
+
if (mergedNode.type === 'conditional' && runtimeNode.type === 'conditional') {
|
|
213
|
+
// Copy entire meta object from runtime (all properties needed)
|
|
214
|
+
mergedNode.meta = runtimeNode.meta;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Augment children recursively
|
|
218
|
+
if (runtimeNode.children) {
|
|
219
|
+
if (!mergedNode.children) mergedNode.children = {};
|
|
220
|
+
|
|
221
|
+
for (const key in runtimeNode.children) {
|
|
222
|
+
const runtimeChild = runtimeNode.children[key];
|
|
223
|
+
|
|
224
|
+
// Skip iterations - let runtime control them entirely
|
|
225
|
+
// Iterations are dynamic and restoration changes DOM structure
|
|
226
|
+
if (runtimeChild.type === 'iteration') {
|
|
227
|
+
mergedNode.children[key] = runtimeChild;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (mergedNode.children[key]) {
|
|
232
|
+
// Child exists in both - augment it
|
|
233
|
+
augmentWithRuntime(mergedNode.children[key], runtimeNode.children[key]);
|
|
234
|
+
} else {
|
|
235
|
+
// Child only in runtime - add it (dynamic element discovered at runtime)
|
|
236
|
+
mergedNode.children[key] = runtimeNode.children[key];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Augment hyperspeed foundation with runtime data
|
|
243
|
+
augmentWithRuntime(merged, runtimeTree);
|
|
244
|
+
|
|
245
|
+
// Preserve stats from runtime (hyperspeed won't have stats)
|
|
246
|
+
if (runtimeTree.stats) {
|
|
247
|
+
merged.stats = runtimeTree.stats;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return merged;
|
|
251
|
+
};
|
|
252
|
+
|
|
135
253
|
// Store previous state for comparison (needs to be accessible by core loop)
|
|
136
254
|
let previousState = {};
|
|
137
255
|
|
|
138
|
-
const main = (s,
|
|
256
|
+
const main = (s, config = {}, stringSelector = '') => {
|
|
139
257
|
const debug = !!config?.debug;
|
|
140
258
|
const verbose = !!config?.verbose;
|
|
141
259
|
|
|
260
|
+
// Detect if running in compiler's headless browser
|
|
261
|
+
// When true: skip cleanup to preserve [vibe] attribute in compiled HTML
|
|
262
|
+
const isCompiling = typeof window !== 'undefined' && window.__vibeCompiling === true;
|
|
263
|
+
|
|
142
264
|
// Reset previous state for each new instance
|
|
143
265
|
previousState = {};
|
|
144
266
|
|
|
145
|
-
//
|
|
146
|
-
const
|
|
267
|
+
// Use page-specific hyperspeed manifest (detected at module load)
|
|
268
|
+
const hyperspeedTree = hyperspeedManifest;
|
|
147
269
|
|
|
148
|
-
|
|
149
|
-
console.info(`[vibe] No element found with attribute "${attrName}". Falling back to body.`);
|
|
150
|
-
} else if (elements.length > 1) {
|
|
151
|
-
console.info(
|
|
152
|
-
`[vibe] Multiple elements (${elements.length}) found with attribute "${attrName}". Hydrating the first one.`,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
270
|
+
let rootElement = document.body;
|
|
155
271
|
|
|
156
|
-
|
|
272
|
+
if (stringSelector) {
|
|
273
|
+
// Find element(s) with the specified attribute
|
|
274
|
+
const elements = document.querySelectorAll(`${stringSelector}`);
|
|
275
|
+
if (elements?.[0]) {
|
|
276
|
+
rootElement = elements[0];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
157
279
|
|
|
158
|
-
debugLog(PHASE_ATTACH, `Vibe attached to`,
|
|
280
|
+
debugLog(PHASE_ATTACH, `Vibe attached to`, true, 0, rootElement);
|
|
281
|
+
|
|
282
|
+
// Component tagging is now handled by component.js before boot
|
|
283
|
+
// Component state is merged into s by boot.js
|
|
284
|
+
|
|
285
|
+
// RESTORATION PHASE: If hyperspeed detected, restore @[...] markers in DOM
|
|
286
|
+
// This allows pre-rendered values to be visible (no FOUC) but makes DOM reactive
|
|
287
|
+
let hyperspeedSubtree = null;
|
|
288
|
+
if (hyperspeedTree) {
|
|
289
|
+
debugLog(PHASE_HYPERSPEED, 'Applied pre-compiled vibe-hyperspeed/**/*.manifest.js', debug);
|
|
290
|
+
|
|
291
|
+
// Find matching subtree by DOM path (not just tag name)
|
|
292
|
+
// Build path from document to rootElement
|
|
293
|
+
const buildDomPath = (element) => {
|
|
294
|
+
const path = [];
|
|
295
|
+
let current = element;
|
|
296
|
+
|
|
297
|
+
// Walk up to document itself (include html in the path)
|
|
298
|
+
while (current && current.parentNode && current !== document) {
|
|
299
|
+
const parent = current.parentNode;
|
|
300
|
+
|
|
301
|
+
// Skip document node itself
|
|
302
|
+
if (parent === document) {
|
|
303
|
+
const siblings = Array.from(document.childNodes);
|
|
304
|
+
const index = siblings.indexOf(current);
|
|
305
|
+
const tag = current.nodeName.toLowerCase();
|
|
306
|
+
path.unshift({ tag, index });
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
159
309
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
310
|
+
const siblings = Array.from(parent.childNodes);
|
|
311
|
+
const index = siblings.indexOf(current);
|
|
312
|
+
const tag = current.nodeName.toLowerCase();
|
|
163
313
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (script.hasAttribute('data-vibe-component-id')) return;
|
|
314
|
+
path.unshift({ tag, index });
|
|
315
|
+
current = parent;
|
|
316
|
+
}
|
|
168
317
|
|
|
169
|
-
|
|
170
|
-
|
|
318
|
+
return path;
|
|
319
|
+
};
|
|
171
320
|
|
|
172
|
-
|
|
321
|
+
// Walk manifest tree following DOM path
|
|
322
|
+
const findManifestNodeByPath = (manifest, path) => {
|
|
323
|
+
let current = manifest;
|
|
173
324
|
|
|
174
|
-
|
|
175
|
-
|
|
325
|
+
for (const { tag, index } of path) {
|
|
326
|
+
if (!current || !current.children) return null;
|
|
176
327
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
328
|
+
// Look for matching child by tag_index pattern
|
|
329
|
+
const key = `${tag}_${index}`;
|
|
330
|
+
if (current.children[key]) {
|
|
331
|
+
current = current.children[key];
|
|
332
|
+
} else {
|
|
333
|
+
// Fallback: search all children for matching tag at this level
|
|
334
|
+
let found = false;
|
|
335
|
+
for (const childKey in current.children) {
|
|
336
|
+
if (childKey.startsWith(tag + '_')) {
|
|
337
|
+
current = current.children[childKey];
|
|
338
|
+
found = true;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!found) return null;
|
|
184
343
|
}
|
|
185
|
-
// Tag only the sibling (descendants will use closest() to find it)
|
|
186
|
-
sibling.setAttribute('data-vibe-component-id', componentId);
|
|
187
|
-
sibling = sibling.nextElementSibling;
|
|
188
344
|
}
|
|
189
345
|
|
|
190
|
-
|
|
191
|
-
|
|
346
|
+
return current;
|
|
347
|
+
};
|
|
192
348
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
349
|
+
const domPath = buildDomPath(rootElement);
|
|
350
|
+
hyperspeedSubtree = findManifestNodeByPath(hyperspeedTree, domPath);
|
|
351
|
+
|
|
352
|
+
if (!hyperspeedSubtree) {
|
|
353
|
+
// Fallback to root if path matching fails
|
|
354
|
+
hyperspeedSubtree = hyperspeedTree;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
restoreMarkersFromManifest(rootElement, hyperspeedSubtree, hyperspeedTree);
|
|
196
358
|
}
|
|
197
359
|
|
|
198
|
-
//
|
|
360
|
+
// Runtime parses DOM (which now has restored markers if hyperspeed was used)
|
|
199
361
|
let parsedTree = parse(rootElement);
|
|
362
|
+
|
|
363
|
+
// Merge with hyperspeed if available (hyperspeed as foundation, runtime augments)
|
|
364
|
+
if (hyperspeedSubtree) {
|
|
365
|
+
parsedTree = mergeManifests(hyperspeedSubtree, parsedTree);
|
|
366
|
+
}
|
|
367
|
+
|
|
200
368
|
let manifest = createManifest(parsedTree);
|
|
201
369
|
|
|
370
|
+
// Build hyperspeed manifest (before hydration, extract markers from parsed strings)
|
|
371
|
+
const hyperspeedManifestData = buildHyperspeedManifest(parsedTree);
|
|
372
|
+
|
|
202
373
|
const manifestEntries = Object.entries(manifest);
|
|
203
374
|
const totalCount = manifestEntries.length;
|
|
204
375
|
const elementsOnly = manifestEntries.filter(
|
|
@@ -220,7 +391,11 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
220
391
|
);
|
|
221
392
|
}
|
|
222
393
|
debugLog(PHASE_PARSE, segments, debug);
|
|
223
|
-
debugLog(
|
|
394
|
+
debugLog(
|
|
395
|
+
PHASE_MANIFEST,
|
|
396
|
+
hyperspeedTree ? 'DOM manifest merged with hyperspeed' : 'DOM manifest created',
|
|
397
|
+
debug,
|
|
398
|
+
);
|
|
224
399
|
|
|
225
400
|
// Lifecycle hooks that users can subscribe to
|
|
226
401
|
const hooks = {
|
|
@@ -241,9 +416,7 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
241
416
|
return plain;
|
|
242
417
|
};
|
|
243
418
|
|
|
244
|
-
// Observer
|
|
245
|
-
let observer = null;
|
|
246
|
-
let processMutations = null;
|
|
419
|
+
// Observer callback will be defined below (already declared above before processComponent)
|
|
247
420
|
|
|
248
421
|
const $ = state(s, (newState, oldState) => {
|
|
249
422
|
// Extract current state (after mutation)
|
|
@@ -350,6 +523,10 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
350
523
|
// After all initial rendering, capture a clean snapshot for comparison
|
|
351
524
|
previousState = extractPlainValue($);
|
|
352
525
|
|
|
526
|
+
// Observer reference and processMutations
|
|
527
|
+
let observer = null;
|
|
528
|
+
let processMutations = null;
|
|
529
|
+
|
|
353
530
|
// Define observer callback as named function so we can call it manually for pending mutations
|
|
354
531
|
processMutations = (mutations) => {
|
|
355
532
|
// Early exit if no mutations to process (common case)
|
|
@@ -436,8 +613,13 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
436
613
|
picked.element = target;
|
|
437
614
|
}
|
|
438
615
|
|
|
439
|
-
// Parse the newly added node
|
|
440
|
-
|
|
616
|
+
// Parse the newly added node (use monotonically increasing counter for deterministic key)
|
|
617
|
+
// Initialize counter if it doesn't exist
|
|
618
|
+
if (!picked._nextChildIndex) {
|
|
619
|
+
picked._nextChildIndex = Object.keys(picked.children).length;
|
|
620
|
+
}
|
|
621
|
+
const name = `${node.nodeName.toLowerCase()}_${picked._nextChildIndex}`;
|
|
622
|
+
picked._nextChildIndex++; // Always increment, never decrement
|
|
441
623
|
const parsedNode = parse(node);
|
|
442
624
|
|
|
443
625
|
// Accumulate skipped stats
|
|
@@ -633,7 +815,7 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
633
815
|
|
|
634
816
|
// Check if cleanup should run
|
|
635
817
|
const checkCleanup = () => {
|
|
636
|
-
if (cleanupExecuted) return;
|
|
818
|
+
if (cleanupExecuted || isCompiling) return;
|
|
637
819
|
|
|
638
820
|
// Check for pending mutations first
|
|
639
821
|
const pendingMutations = observer ? observer.takeRecords() : [];
|
|
@@ -646,7 +828,7 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
646
828
|
|
|
647
829
|
// Check if all processing is complete
|
|
648
830
|
if (shouldCleanup(rootElement)) {
|
|
649
|
-
cleanup(rootElement,
|
|
831
|
+
cleanup(rootElement, debug);
|
|
650
832
|
cleanupExecuted = true;
|
|
651
833
|
}
|
|
652
834
|
};
|
|
@@ -674,6 +856,11 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
674
856
|
componentConfig,
|
|
675
857
|
);
|
|
676
858
|
|
|
859
|
+
// Export hyperspeed manifest globally for compiler extraction
|
|
860
|
+
if (typeof window !== 'undefined') {
|
|
861
|
+
window.__vibeManifest = hyperspeedManifestData;
|
|
862
|
+
}
|
|
863
|
+
|
|
677
864
|
return $;
|
|
678
865
|
};
|
|
679
866
|
|
package/runtime/iterate.js
CHANGED
|
@@ -322,8 +322,11 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
322
322
|
iterationNode.runtime.instances = instances;
|
|
323
323
|
|
|
324
324
|
// Mark comment as rendered (survives re-parsing since comment nodes persist in DOM)
|
|
325
|
+
// Also store runtime data on the DOM node so it persists across re-parses
|
|
325
326
|
// @ts-ignore - adding custom property to comment node
|
|
326
327
|
startComment.__vibeRendered = true;
|
|
328
|
+
// @ts-ignore - adding custom property to comment node
|
|
329
|
+
startComment.__vibeIterationRuntime = iterationNode.runtime;
|
|
327
330
|
};
|
|
328
331
|
|
|
329
332
|
// Update an iteration block when array changes
|
package/runtime/parse.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { hash } from './utils.js';
|
|
2
1
|
import { findEndComment, findConditionalEnd } from './iteration-utils.js';
|
|
3
2
|
import {
|
|
4
3
|
NON_REACTIVE_ELEMENTS,
|
|
@@ -60,9 +59,9 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
60
59
|
// Parse the template recursively
|
|
61
60
|
const templateParsed = recursive([...templateContainer.childNodes], undefined, new Set(), stats);
|
|
62
61
|
|
|
63
|
-
// Store iteration metadata
|
|
64
|
-
const iterationKey = `iteration_${
|
|
65
|
-
|
|
62
|
+
// Store iteration metadata (use index for deterministic keys)
|
|
63
|
+
const iterationKey = `iteration_${i}`;
|
|
64
|
+
const iterationNode = {
|
|
66
65
|
type: 'iteration',
|
|
67
66
|
meta: {
|
|
68
67
|
arrayPath,
|
|
@@ -76,12 +75,15 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
76
75
|
children: templateParsed,
|
|
77
76
|
},
|
|
78
77
|
},
|
|
79
|
-
runtime
|
|
78
|
+
// Preserve runtime data from previous parse if it exists (stored on startComment by iterate.js)
|
|
79
|
+
// @ts-ignore - custom property added by iterate.js
|
|
80
|
+
runtime: element.__vibeIterationRuntime || {
|
|
80
81
|
instances: [],
|
|
81
82
|
templateRemoved: false,
|
|
82
83
|
},
|
|
83
84
|
children: {},
|
|
84
85
|
};
|
|
86
|
+
result[iterationKey] = iterationNode;
|
|
85
87
|
|
|
86
88
|
// Mark template indices as processed
|
|
87
89
|
for (let j = i + 1; j < endIndex; j++) {
|
|
@@ -133,8 +135,8 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
133
135
|
falseBranchParsed = recursive([...falseBranchContainer.childNodes], undefined, new Set(), stats);
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
// Store conditional metadata
|
|
137
|
-
const conditionalKey = `conditional_${
|
|
138
|
+
// Store conditional metadata (use index for deterministic keys)
|
|
139
|
+
const conditionalKey = `conditional_${i}`;
|
|
138
140
|
result[conditionalKey] = {
|
|
139
141
|
type: 'conditional',
|
|
140
142
|
meta: {
|
|
@@ -194,7 +196,13 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
194
196
|
// Check for attribute bindings
|
|
195
197
|
const attributes = {};
|
|
196
198
|
const nameBindings = [];
|
|
197
|
-
|
|
199
|
+
|
|
200
|
+
// Skip hydrating attributes on fetched components - they need to be passed raw
|
|
201
|
+
const isFetchedComponent =
|
|
202
|
+
(nodeName === 'COMPONENT' || (nodeName === 'DIV' && element.classList?.contains('component'))) &&
|
|
203
|
+
element.hasAttribute('src');
|
|
204
|
+
|
|
205
|
+
if (element.attributes && !isFetchedComponent) {
|
|
198
206
|
for (let j = 0; j < element.attributes.length; j++) {
|
|
199
207
|
const attr = element.attributes[j];
|
|
200
208
|
// Reset lastIndex before test - BINDING_REGEX has 'g' flag which persists state
|
|
@@ -227,29 +235,13 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
// Helper to find component ID for an element
|
|
238
|
+
// Looks for nearest ancestor with data-vibe-component-id
|
|
230
239
|
function findComponentIdForElement(element) {
|
|
231
|
-
if (!element
|
|
232
|
-
|
|
233
|
-
let current = element;
|
|
234
|
-
|
|
235
|
-
while (current && current !== document.body) {
|
|
236
|
-
// Check previous siblings for script[data-vibe-component-id]
|
|
237
|
-
let sibling = current.previousElementSibling;
|
|
238
|
-
while (sibling) {
|
|
239
|
-
if (
|
|
240
|
-
sibling.tagName === 'SCRIPT' &&
|
|
241
|
-
sibling.getAttribute('type') === 'component' &&
|
|
242
|
-
sibling.hasAttribute('data-vibe-component-id')
|
|
243
|
-
) {
|
|
244
|
-
return sibling.getAttribute('data-vibe-component-id');
|
|
245
|
-
}
|
|
246
|
-
sibling = sibling.previousElementSibling;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
current = current.parentElement;
|
|
250
|
-
}
|
|
240
|
+
if (!element) return null;
|
|
251
241
|
|
|
252
|
-
|
|
242
|
+
// Find nearest component wrapper (tagged by component.js)
|
|
243
|
+
const wrapper = element.closest('[data-vibe-component-id]');
|
|
244
|
+
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
253
245
|
}
|
|
254
246
|
const hasAttributeBindings = Object.keys(attributes).length > 0;
|
|
255
247
|
const hasNameBindings = nameBindings.length > 0;
|
package/runtime/scope.js
CHANGED
|
@@ -2,36 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Find the component ID that owns this element
|
|
5
|
-
*
|
|
5
|
+
* Finds nearest ancestor with data-vibe-component-id (set by component.js)
|
|
6
6
|
* @param {Element} element - DOM element to find component for
|
|
7
7
|
* @returns {string|null} - Component ID or null if not in component scope
|
|
8
8
|
*/
|
|
9
9
|
export const findComponentId = (element) => {
|
|
10
|
-
|
|
10
|
+
if (!element || !element.closest) return null;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return current.getAttribute('data-vibe-component-id');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Check previous siblings for script[data-vibe-component-id]
|
|
19
|
-
let sibling = current.previousElementSibling;
|
|
20
|
-
while (sibling) {
|
|
21
|
-
if (
|
|
22
|
-
sibling.tagName === 'SCRIPT' &&
|
|
23
|
-
sibling.getAttribute('type') === 'component' &&
|
|
24
|
-
sibling.hasAttribute('data-vibe-component-id')
|
|
25
|
-
) {
|
|
26
|
-
return sibling.getAttribute('data-vibe-component-id');
|
|
27
|
-
}
|
|
28
|
-
sibling = sibling.previousElementSibling;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
current = current.parentElement;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return null;
|
|
12
|
+
// Find nearest component wrapper (tagged by component.js)
|
|
13
|
+
const wrapper = element.closest('[data-vibe-component-id]');
|
|
14
|
+
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
35
15
|
};
|
|
36
16
|
|
|
37
17
|
/**
|
package/runtime/utils.js
CHANGED
|
@@ -50,21 +50,12 @@ export const evalInScope = (expr, state, element = null) => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
// Helper to find component ID for an element
|
|
53
|
+
// Walks up DOM tree to find nearest component wrapper
|
|
53
54
|
export const findComponentIdForElement = (element) => {
|
|
54
|
-
if (!element) return null;
|
|
55
|
+
if (!element || !element.closest) return null;
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return element.getAttribute('data-vibe-component-id');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Check if any ancestor has the ID
|
|
62
|
-
const ancestor = element.closest('[data-vibe-component-id]');
|
|
63
|
-
if (ancestor) {
|
|
64
|
-
return ancestor.getAttribute('data-vibe-component-id');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return null;
|
|
57
|
+
const wrapper = element.closest('[data-vibe-component-id]');
|
|
58
|
+
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
68
59
|
};
|
|
69
60
|
|
|
70
61
|
// Helper to resolve this.property paths to componentId.property
|
package/vibe.css
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Elements with [vibe] attribute are hidden until framework removes it after hydration.
|
|
3
3
|
* This prevents flash of unprocessed content and disables transitions during init.
|
|
4
4
|
*/
|
|
5
|
-
[vibe]
|
|
5
|
+
[vibe-fouc],
|
|
6
|
+
.vibe-fouc {
|
|
6
7
|
visibility: hidden;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
[vibe]
|
|
10
|
+
[vibe-fouc] *,
|
|
11
|
+
.vibe-fouc * {
|
|
10
12
|
transition: none !important;
|
|
11
13
|
}
|