@ape-egg/vibe 1.1.2 → 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 +140 -0
- package/README.md +30 -23
- 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 +40 -2
- package/package.json +2 -1
- package/runtime/affected.js +44 -24
- package/runtime/cleanup.js +45 -24
- package/runtime/component-state.js +63 -0
- package/runtime/component.js +59 -15
- package/runtime/conditionals.js +4 -4
- package/runtime/constants.js +46 -2
- package/runtime/debug.js +1 -0
- package/runtime/hydrate.js +23 -21
- package/runtime/hyperspeed.js +425 -0
- package/runtime/index.js +322 -69
- package/runtime/iterate.js +15 -4
- package/runtime/parse.js +48 -10
- package/runtime/scope.js +50 -0
- package/runtime/state.js +10 -5
- package/runtime/utils.js +63 -5
- package/{runtime/vibe.css → vibe.css} +4 -2
package/runtime/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import state from './state.js';
|
|
2
2
|
import parse from './parse.js';
|
|
3
3
|
import createManifest from './manifest.js';
|
|
4
|
-
import hydrate
|
|
4
|
+
import hydrate from './hydrate.js';
|
|
5
5
|
import affected from './affected.js';
|
|
6
6
|
import { deepMerge, hash } from './utils.js';
|
|
7
7
|
import { renderAllIterations, setRenderAllConditionals } from './iterate.js';
|
|
@@ -17,28 +17,50 @@ 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
|
+
import { generateComponentId, executeComponentScript } from './component-state.js';
|
|
26
|
+
import {
|
|
27
|
+
buildHyperspeedManifest,
|
|
28
|
+
hyperspeedManifest,
|
|
29
|
+
restoreMarkersFromManifest,
|
|
30
|
+
} from './hyperspeed.js';
|
|
25
31
|
|
|
26
32
|
// Wire up cross-module dependency after all modules are loaded
|
|
27
33
|
setRenderAllConditionals(renderAllConditionals);
|
|
28
34
|
|
|
29
|
-
// Check if node
|
|
30
|
-
const
|
|
35
|
+
// Check if node should be processed by Vibe
|
|
36
|
+
const shouldProcessNode = (node) => {
|
|
37
|
+
// Only process element nodes
|
|
38
|
+
if (node.nodeType !== 1) return false;
|
|
39
|
+
|
|
40
|
+
// Fast check first: skip nodes without Vibe syntax (cheapest check)
|
|
41
|
+
const html = node.outerHTML;
|
|
42
|
+
if (
|
|
43
|
+
!html.includes('@[') &&
|
|
44
|
+
!html.includes('<!-- each') &&
|
|
45
|
+
!html.includes('<!-- if') &&
|
|
46
|
+
!html.includes('<component')
|
|
47
|
+
) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Node has Vibe syntax - now check if it's in a non-reactive context
|
|
31
52
|
let current = node;
|
|
32
53
|
while (current && current !== document.body) {
|
|
33
54
|
if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
|
|
34
|
-
return
|
|
55
|
+
return false;
|
|
35
56
|
}
|
|
36
57
|
if (current.hasAttribute?.('dehydrate')) {
|
|
37
|
-
return
|
|
58
|
+
return false;
|
|
38
59
|
}
|
|
39
60
|
current = current.parentElement;
|
|
40
61
|
}
|
|
41
|
-
|
|
62
|
+
|
|
63
|
+
return true;
|
|
42
64
|
};
|
|
43
65
|
|
|
44
66
|
// Navigate tree using dot notation (handles .children at each level)
|
|
@@ -90,10 +112,11 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
|
|
|
90
112
|
|
|
91
113
|
// 2. Hydrate (replace @[...] bindings)
|
|
92
114
|
// Use empty object as "previous state" for new nodes so all bindings are affected
|
|
93
|
-
const
|
|
115
|
+
const oldStateForAffected = isNewNode ? {} : previousState;
|
|
116
|
+
const affectedElements = affected(parsedNode, oldStateForAffected, state);
|
|
94
117
|
if (affectedElements.length > 0) {
|
|
95
118
|
hydratedCount = affectedElements.length;
|
|
96
|
-
hydrate(affectedElements, state, manifest);
|
|
119
|
+
hydrate(affectedElements, state, manifest, oldStateForAffected);
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
// 3. Conditionals (evaluate <!-- if -->)
|
|
@@ -114,34 +137,239 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
|
|
|
114
137
|
return { hydratedCount, iteratedCount, evaluatedCount };
|
|
115
138
|
};
|
|
116
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
|
+
|
|
117
253
|
// Store previous state for comparison (needs to be accessible by core loop)
|
|
118
254
|
let previousState = {};
|
|
119
255
|
|
|
120
|
-
const main = (s,
|
|
256
|
+
const main = (s, config = {}, stringSelector = '') => {
|
|
121
257
|
const debug = !!config?.debug;
|
|
122
258
|
const verbose = !!config?.verbose;
|
|
123
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
|
+
|
|
124
264
|
// Reset previous state for each new instance
|
|
125
265
|
previousState = {};
|
|
126
266
|
|
|
127
|
-
//
|
|
128
|
-
const
|
|
267
|
+
// Use page-specific hyperspeed manifest (detected at module load)
|
|
268
|
+
const hyperspeedTree = hyperspeedManifest;
|
|
129
269
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
270
|
+
let rootElement = document.body;
|
|
271
|
+
|
|
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
|
+
}
|
|
136
278
|
}
|
|
137
279
|
|
|
138
|
-
|
|
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
|
+
}
|
|
309
|
+
|
|
310
|
+
const siblings = Array.from(parent.childNodes);
|
|
311
|
+
const index = siblings.indexOf(current);
|
|
312
|
+
const tag = current.nodeName.toLowerCase();
|
|
313
|
+
|
|
314
|
+
path.unshift({ tag, index });
|
|
315
|
+
current = parent;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return path;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Walk manifest tree following DOM path
|
|
322
|
+
const findManifestNodeByPath = (manifest, path) => {
|
|
323
|
+
let current = manifest;
|
|
324
|
+
|
|
325
|
+
for (const { tag, index } of path) {
|
|
326
|
+
if (!current || !current.children) return null;
|
|
327
|
+
|
|
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;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return current;
|
|
347
|
+
};
|
|
139
348
|
|
|
140
|
-
|
|
349
|
+
const domPath = buildDomPath(rootElement);
|
|
350
|
+
hyperspeedSubtree = findManifestNodeByPath(hyperspeedTree, domPath);
|
|
141
351
|
|
|
352
|
+
if (!hyperspeedSubtree) {
|
|
353
|
+
// Fallback to root if path matching fails
|
|
354
|
+
hyperspeedSubtree = hyperspeedTree;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
restoreMarkersFromManifest(rootElement, hyperspeedSubtree, hyperspeedTree);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Runtime parses DOM (which now has restored markers if hyperspeed was used)
|
|
142
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
|
+
|
|
143
368
|
let manifest = createManifest(parsedTree);
|
|
144
369
|
|
|
370
|
+
// Build hyperspeed manifest (before hydration, extract markers from parsed strings)
|
|
371
|
+
const hyperspeedManifestData = buildHyperspeedManifest(parsedTree);
|
|
372
|
+
|
|
145
373
|
const manifestEntries = Object.entries(manifest);
|
|
146
374
|
const totalCount = manifestEntries.length;
|
|
147
375
|
const elementsOnly = manifestEntries.filter(
|
|
@@ -163,7 +391,11 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
163
391
|
);
|
|
164
392
|
}
|
|
165
393
|
debugLog(PHASE_PARSE, segments, debug);
|
|
166
|
-
debugLog(
|
|
394
|
+
debugLog(
|
|
395
|
+
PHASE_MANIFEST,
|
|
396
|
+
hyperspeedTree ? 'DOM manifest merged with hyperspeed' : 'DOM manifest created',
|
|
397
|
+
debug,
|
|
398
|
+
);
|
|
167
399
|
|
|
168
400
|
// Lifecycle hooks that users can subscribe to
|
|
169
401
|
const hooks = {
|
|
@@ -171,19 +403,31 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
171
403
|
afterDomMutation: [],
|
|
172
404
|
};
|
|
173
405
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
406
|
+
// Extract plain values from proxy (removes proxy wrappers)
|
|
407
|
+
const extractPlainValue = (obj) => {
|
|
408
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
409
|
+
if (Array.isArray(obj)) return obj.map(extractPlainValue);
|
|
410
|
+
const plain = {};
|
|
411
|
+
for (const key in obj) {
|
|
412
|
+
if (obj.hasOwnProperty(key)) {
|
|
413
|
+
plain[key] = extractPlainValue(obj[key]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return plain;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Observer callback will be defined below (already declared above before processComponent)
|
|
177
420
|
|
|
178
|
-
const $ = state(s, (newState) => {
|
|
179
|
-
|
|
421
|
+
const $ = state(s, (newState, oldState) => {
|
|
422
|
+
// Extract current state (after mutation)
|
|
423
|
+
const currentState = extractPlainValue($);
|
|
424
|
+
const changedProp = Object.keys(newState)[0];
|
|
180
425
|
|
|
181
|
-
// Find what changed
|
|
182
|
-
const affectedElements = affected(parsedTree, previousState,
|
|
426
|
+
// Find what changed (compare previousState vs currentState)
|
|
427
|
+
const affectedElements = affected(parsedTree, previousState, currentState);
|
|
183
428
|
|
|
184
429
|
if (affectedElements.length > 0) {
|
|
185
|
-
|
|
186
|
-
debugLog(PHASE_UPDATE, `state changed (${changedKeys})`, debug);
|
|
430
|
+
debugLog(PHASE_UPDATE, `state changed (${changedProp})`, debug);
|
|
187
431
|
|
|
188
432
|
// Capture pending mutations before disconnecting (takeRecords clears the queue)
|
|
189
433
|
let pendingMutations = [];
|
|
@@ -194,11 +438,11 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
194
438
|
|
|
195
439
|
// Run core loop on the full tree (hydrate will use affected list)
|
|
196
440
|
// isNewNode = false because this is a state update, not a DOM mutation
|
|
197
|
-
hydrate(affectedElements,
|
|
441
|
+
hydrate(affectedElements, currentState, manifest, previousState);
|
|
198
442
|
|
|
199
443
|
// After hydrate, check for conditional/iteration changes
|
|
200
|
-
const conditionalCount = renderAllConditionals(parsedTree,
|
|
201
|
-
const iterationCount = renderAllIterations(parsedTree,
|
|
444
|
+
const conditionalCount = renderAllConditionals(parsedTree, currentState, manifest);
|
|
445
|
+
const iterationCount = renderAllIterations(parsedTree, currentState, manifest);
|
|
202
446
|
|
|
203
447
|
if (observer) {
|
|
204
448
|
observer.observe(rootElement, {
|
|
@@ -215,22 +459,10 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
215
459
|
}
|
|
216
460
|
}
|
|
217
461
|
|
|
218
|
-
//
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const cloned = {};
|
|
223
|
-
for (const key in obj) {
|
|
224
|
-
if (obj.hasOwnProperty(key)) {
|
|
225
|
-
cloned[key] = deepClone(obj[key]);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return cloned;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const prev = deepClone(previousState);
|
|
232
|
-
previousState = { ...$, ...newState };
|
|
233
|
-
hooks.afterUpdate.forEach((callback) => callback(deepClone({ ...$ }), prev));
|
|
462
|
+
// Store previous state for hooks (currentState is already plain, no need to clone)
|
|
463
|
+
const prev = previousState;
|
|
464
|
+
previousState = currentState;
|
|
465
|
+
hooks.afterUpdate.forEach((callback) => callback(currentState, prev));
|
|
234
466
|
});
|
|
235
467
|
|
|
236
468
|
// Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
|
|
@@ -244,8 +476,9 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
244
476
|
enumerable: false,
|
|
245
477
|
});
|
|
246
478
|
|
|
247
|
-
// Initial hydration
|
|
248
|
-
const
|
|
479
|
+
// Initial hydration - pass plain values so iteration can do reference comparison
|
|
480
|
+
const initialState = extractPlainValue($);
|
|
481
|
+
const affectedElements = affected(parsedTree, initialState, initialState);
|
|
249
482
|
|
|
250
483
|
debugLog(
|
|
251
484
|
PHASE_HYDRATE,
|
|
@@ -257,14 +490,13 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
257
490
|
debug,
|
|
258
491
|
);
|
|
259
492
|
|
|
260
|
-
|
|
261
|
-
|
|
493
|
+
// Hydrate with proxy $ so DOM bindings work
|
|
494
|
+
// Pass initialState as oldState for iterations (won't actually update, just initial render)
|
|
495
|
+
hydrate(affectedElements, $, manifest, initialState);
|
|
262
496
|
|
|
263
497
|
// Render all iterations and conditionals after initial hydration
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const iterationCount = renderAllIterations(parsedTree, $, manifest);
|
|
498
|
+
// Use initialState (plain values) for iteration rendering so reference comparison works
|
|
499
|
+
const iterationCount = renderAllIterations(parsedTree, initialState, manifest);
|
|
268
500
|
if (iterationCount > 0)
|
|
269
501
|
debugLog(
|
|
270
502
|
PHASE_ITERATE,
|
|
@@ -288,6 +520,13 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
288
520
|
debug,
|
|
289
521
|
);
|
|
290
522
|
|
|
523
|
+
// After all initial rendering, capture a clean snapshot for comparison
|
|
524
|
+
previousState = extractPlainValue($);
|
|
525
|
+
|
|
526
|
+
// Observer reference and processMutations
|
|
527
|
+
let observer = null;
|
|
528
|
+
let processMutations = null;
|
|
529
|
+
|
|
291
530
|
// Define observer callback as named function so we can call it manually for pending mutations
|
|
292
531
|
processMutations = (mutations) => {
|
|
293
532
|
// Early exit if no mutations to process (common case)
|
|
@@ -353,8 +592,8 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
353
592
|
});
|
|
354
593
|
|
|
355
594
|
addedNodesList.forEach((node) => {
|
|
356
|
-
// Skip
|
|
357
|
-
if (
|
|
595
|
+
// Skip nodes that aren't element nodes, are non-reactive, or lack Vibe syntax
|
|
596
|
+
if (!shouldProcessNode(node)) {
|
|
358
597
|
return;
|
|
359
598
|
}
|
|
360
599
|
|
|
@@ -374,8 +613,13 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
374
613
|
picked.element = target;
|
|
375
614
|
}
|
|
376
615
|
|
|
377
|
-
// Parse the newly added node
|
|
378
|
-
|
|
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
|
|
379
623
|
const parsedNode = parse(node);
|
|
380
624
|
|
|
381
625
|
// Accumulate skipped stats
|
|
@@ -539,12 +783,16 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
539
783
|
...config,
|
|
540
784
|
_forceSync: true,
|
|
541
785
|
_observer: observer,
|
|
542
|
-
_processMutations: processMutations
|
|
786
|
+
_processMutations: processMutations,
|
|
543
787
|
};
|
|
544
|
-
processComponent(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
788
|
+
processComponent(
|
|
789
|
+
rootElement,
|
|
790
|
+
() => {
|
|
791
|
+
// When all components are done, run cleanup check
|
|
792
|
+
checkCleanup();
|
|
793
|
+
},
|
|
794
|
+
componentConfig,
|
|
795
|
+
);
|
|
548
796
|
}
|
|
549
797
|
};
|
|
550
798
|
|
|
@@ -567,7 +815,7 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
567
815
|
|
|
568
816
|
// Check if cleanup should run
|
|
569
817
|
const checkCleanup = () => {
|
|
570
|
-
if (cleanupExecuted) return;
|
|
818
|
+
if (cleanupExecuted || isCompiling) return;
|
|
571
819
|
|
|
572
820
|
// Check for pending mutations first
|
|
573
821
|
const pendingMutations = observer ? observer.takeRecords() : [];
|
|
@@ -580,7 +828,7 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
580
828
|
|
|
581
829
|
// Check if all processing is complete
|
|
582
830
|
if (shouldCleanup(rootElement)) {
|
|
583
|
-
cleanup(rootElement,
|
|
831
|
+
cleanup(rootElement, debug);
|
|
584
832
|
cleanupExecuted = true;
|
|
585
833
|
}
|
|
586
834
|
};
|
|
@@ -596,7 +844,7 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
596
844
|
...config,
|
|
597
845
|
_forceSync: true,
|
|
598
846
|
_observer: observer,
|
|
599
|
-
_processMutations: processMutations
|
|
847
|
+
_processMutations: processMutations,
|
|
600
848
|
};
|
|
601
849
|
|
|
602
850
|
processComponent(
|
|
@@ -608,6 +856,11 @@ const main = (s, attrName = 'vibe', config = {}) => {
|
|
|
608
856
|
componentConfig,
|
|
609
857
|
);
|
|
610
858
|
|
|
859
|
+
// Export hyperspeed manifest globally for compiler extraction
|
|
860
|
+
if (typeof window !== 'undefined') {
|
|
861
|
+
window.__vibeManifest = hyperspeedManifestData;
|
|
862
|
+
}
|
|
863
|
+
|
|
611
864
|
return $;
|
|
612
865
|
};
|
|
613
866
|
|
package/runtime/iterate.js
CHANGED
|
@@ -2,6 +2,7 @@ import parse from './parse.js';
|
|
|
2
2
|
import affected from './affected.js';
|
|
3
3
|
import hydrate from './hydrate.js';
|
|
4
4
|
import { resolvePath, getItemKey, computeDiff } from './iteration-utils.js';
|
|
5
|
+
import { resolveThisPath } from './utils.js';
|
|
5
6
|
|
|
6
7
|
// Fast path for iteration rendering (opt-in via window.__VIBE_FAST_ITERATION__)
|
|
7
8
|
// See: _vibe-compiled-iteration-batch.js for implementation details
|
|
@@ -153,7 +154,7 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
|
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
if (tree) {
|
|
156
|
-
const affectedElements = affected(tree, {}, scopedState);
|
|
157
|
+
const affectedElements = affected(tree, {}, scopedState, [], scopedState);
|
|
157
158
|
hydrate(affectedElements, scopedState);
|
|
158
159
|
}
|
|
159
160
|
|
|
@@ -278,7 +279,10 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
278
279
|
|
|
279
280
|
const parent = startComment.parentNode;
|
|
280
281
|
|
|
281
|
-
|
|
282
|
+
// Handle this.property for component-scoped arrays
|
|
283
|
+
const resolvedArrayPath = resolveThisPath(arrayPath, startComment.parentElement);
|
|
284
|
+
|
|
285
|
+
const array = resolvePath(state, resolvedArrayPath);
|
|
282
286
|
if (!Array.isArray(array) || array.length === 0) {
|
|
283
287
|
iterationNode.runtime.instances = [];
|
|
284
288
|
return;
|
|
@@ -318,8 +322,11 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
318
322
|
iterationNode.runtime.instances = instances;
|
|
319
323
|
|
|
320
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
|
|
321
326
|
// @ts-ignore - adding custom property to comment node
|
|
322
327
|
startComment.__vibeRendered = true;
|
|
328
|
+
// @ts-ignore - adding custom property to comment node
|
|
329
|
+
startComment.__vibeIterationRuntime = iterationNode.runtime;
|
|
323
330
|
};
|
|
324
331
|
|
|
325
332
|
// Update an iteration block when array changes
|
|
@@ -327,8 +334,12 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
327
334
|
if (!iterationNode.runtime.instances || !iterationNode.runtime.templateRemoved) return;
|
|
328
335
|
|
|
329
336
|
const { arrayPath, template, startComment, endComment } = iterationNode.meta;
|
|
330
|
-
|
|
331
|
-
|
|
337
|
+
|
|
338
|
+
// Handle this.property for component-scoped arrays
|
|
339
|
+
const resolvedArrayPath = resolveThisPath(arrayPath, startComment.parentElement);
|
|
340
|
+
|
|
341
|
+
const oldArray = resolvePath(oldState, resolvedArrayPath) || [];
|
|
342
|
+
const newArray = resolvePath(newState, resolvedArrayPath) || [];
|
|
332
343
|
|
|
333
344
|
// Fast path: opt-in via window.__VIBE_FAST_ITERATION__ (preview of Vibe Compiled)
|
|
334
345
|
// Use for bulk operations (large arrays or empty→full transitions)
|