@ape-egg/vibe 4.0.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/boot.js +10 -9
- package/component.js +0 -29
- package/hot-module-refresh.js +0 -0
- package/index.js +0 -28
- package/package.json +3 -2
- package/runtime/_vibe-compiled-iteration-batch.js +0 -56
- package/runtime/affected.js +6 -189
- package/runtime/cleanup.js +4 -35
- package/runtime/component-cache.js +0 -53
- package/runtime/component.js +7 -401
- package/runtime/conditionals.js +1 -143
- package/runtime/constants.js +19 -82
- package/runtime/debug.js +22 -47
- package/runtime/dispatch.js +0 -46
- package/runtime/hydrate.js +10 -129
- package/runtime/index.js +7 -365
- package/runtime/iterate.js +21 -579
- package/runtime/iteration-utils.js +18 -71
- package/runtime/loop-scope.js +0 -58
- package/runtime/manifest.js +0 -27
- package/runtime/parse.js +2 -116
- package/runtime/pre-compiled-iterations.js +3 -51
- package/runtime/pre-compiled-manifest.js +6 -169
- package/runtime/raw-html.js +0 -5
- package/runtime/reconcile.js +4 -159
- package/runtime/staging.js +0 -57
- package/runtime/state.js +0 -61
- package/runtime/this-scope.js +0 -17
- package/runtime/tracking.js +0 -65
- package/runtime/utils.js +1 -144
- package/runtime/vibe-css.js +54 -0
- package/spa.js +0 -76
- package/vibe.css +22 -44
package/runtime/index.js
CHANGED
|
@@ -38,22 +38,15 @@ import {
|
|
|
38
38
|
restoreMarkersFromManifest,
|
|
39
39
|
} from './pre-compiled-manifest.js';
|
|
40
40
|
|
|
41
|
-
// Wire up cross-module dependency after all modules are loaded
|
|
42
41
|
setRenderAllConditionals(renderAllConditionals);
|
|
43
42
|
|
|
44
|
-
// Stamped at module load, not boot — a console can read __vibe.version even
|
|
45
|
-
// on a page whose boot died, which is exactly when a bug report needs it.
|
|
46
43
|
((globalThis.__vibe ??= {}).version = VERSION);
|
|
47
44
|
|
|
48
|
-
// Check if node should be processed by Vibe
|
|
49
45
|
const shouldProcessNode = (node) => {
|
|
50
|
-
// Only process element nodes
|
|
51
46
|
if (node.nodeType !== 1) return false;
|
|
52
47
|
|
|
53
|
-
// Skip nodes already managed by mountBranch or renderIteration
|
|
54
48
|
if (managedNodes.has(node)) return false;
|
|
55
49
|
|
|
56
|
-
// Fast check first: skip nodes without Vibe syntax (cheapest check)
|
|
57
50
|
const html = node.outerHTML;
|
|
58
51
|
if (
|
|
59
52
|
!html.includes('@[') &&
|
|
@@ -64,7 +57,6 @@ const shouldProcessNode = (node) => {
|
|
|
64
57
|
return false;
|
|
65
58
|
}
|
|
66
59
|
|
|
67
|
-
// Node has Vibe syntax - now check if it's in a non-reactive context
|
|
68
60
|
let current = node;
|
|
69
61
|
while (current && current !== document.body) {
|
|
70
62
|
if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
|
|
@@ -82,22 +74,11 @@ const shouldProcessNode = (node) => {
|
|
|
82
74
|
return true;
|
|
83
75
|
};
|
|
84
76
|
|
|
85
|
-
// Navigate tree using dot notation (handles .children at each level)
|
|
86
77
|
const navigateTree = (tree, path) => {
|
|
87
78
|
if (!path) return tree;
|
|
88
79
|
return path.split('.').filter(k => k).reduce((node, key) => node?.children?.[key], tree);
|
|
89
80
|
};
|
|
90
81
|
|
|
91
|
-
// Locate the tree node whose `.element` is `target`, searching the full tree:
|
|
92
|
-
// plain `children`, plus conditional branch trees (`runtime.activeInstance`)
|
|
93
|
-
// and iteration instance trees (`runtime.instances`). navigateTree only walks
|
|
94
|
-
// `children`, so it can't reach content projected across a <slot> boundary — a
|
|
95
|
-
// conditional branch's slotted content is registered in the flat manifest by
|
|
96
|
-
// path, but that path isn't navigable through `children` (the slot node's
|
|
97
|
-
// children don't include the projected subtree). When a <component src> resolves
|
|
98
|
-
// inside such content, navigateTree returns null and the resolved subtree would
|
|
99
|
-
// be orphaned from the reactive tree. This identity search recovers the real
|
|
100
|
-
// parent node so the subtree links in regardless of slot/branch projection.
|
|
101
82
|
const findNodeByElement = (tree, target) => {
|
|
102
83
|
if (!target) return null;
|
|
103
84
|
const seen = new Set();
|
|
@@ -130,7 +111,6 @@ const findNodeByElement = (tree, target) => {
|
|
|
130
111
|
return walk(tree);
|
|
131
112
|
};
|
|
132
113
|
|
|
133
|
-
// Get or create a node in the tree at the given path
|
|
134
114
|
const ensureNode = (tree, path) => {
|
|
135
115
|
const keys = path.split('.').filter(k => k);
|
|
136
116
|
return keys.reduce((node, key) => {
|
|
@@ -141,7 +121,6 @@ const ensureNode = (tree, path) => {
|
|
|
141
121
|
}, tree);
|
|
142
122
|
};
|
|
143
123
|
|
|
144
|
-
// Recursively add parsed tree nodes to manifest (like createManifest does)
|
|
145
124
|
const addToManifest = (tree, manifest, dotPath) => {
|
|
146
125
|
setManifestEntry(manifest, dotPath, tree.element);
|
|
147
126
|
|
|
@@ -152,16 +131,6 @@ const addToManifest = (tree, manifest, dotPath) => {
|
|
|
152
131
|
}
|
|
153
132
|
};
|
|
154
133
|
|
|
155
|
-
/**
|
|
156
|
-
* Core loop: processes a node through parse → hydrate → conditionals → iterate
|
|
157
|
-
* @param {Node} node - DOM node to process (unused in current implementation, parsedNode is primary)
|
|
158
|
-
* @param {Object} parsedNode - Parsed tree node
|
|
159
|
-
* @param {Object} state - Current global state
|
|
160
|
-
* @param {Object} manifest - DOM manifest
|
|
161
|
-
* @param {Boolean} isNewNode - Whether this is a newly added node (affects hydration)
|
|
162
|
-
* @param {Boolean} debug - Debug mode
|
|
163
|
-
* @returns {Object} - Counts of hydrated/iterated/evaluated elements
|
|
164
|
-
*/
|
|
165
134
|
const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) => {
|
|
166
135
|
let hydratedCount = 0;
|
|
167
136
|
let iteratedCount = 0;
|
|
@@ -169,13 +138,6 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
|
|
|
169
138
|
|
|
170
139
|
if (!parsedNode) return { hydratedCount, iteratedCount, evaluatedCount };
|
|
171
140
|
|
|
172
|
-
// 1. Parse - already done before calling core loop (in processMutations)
|
|
173
|
-
|
|
174
|
-
// 2. Hydrate (replace @[...] bindings)
|
|
175
|
-
// For new nodes, use {} so all bindings are found. But filter out iterations
|
|
176
|
-
// and conditionals — those should only go through renderAllIterations/renderAllConditionals
|
|
177
|
-
// (initial render path), not updateIteration/updateConditional (which would diff against
|
|
178
|
-
// stale oldState and produce false adds/removes).
|
|
179
141
|
const oldStateForAffected = isNewNode ? {} : previousState;
|
|
180
142
|
let affectedElements = affected(parsedNode, oldStateForAffected, state);
|
|
181
143
|
if (isNewNode) {
|
|
@@ -186,33 +148,24 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
|
|
|
186
148
|
hydrate(affectedElements, state, manifest, oldStateForAffected);
|
|
187
149
|
}
|
|
188
150
|
|
|
189
|
-
// 3. Conditionals (evaluate <!-- if -->)
|
|
190
151
|
const conditionalCount = renderAllConditionals(parsedNode, state, manifest);
|
|
191
152
|
if (conditionalCount > 0) {
|
|
192
153
|
evaluatedCount = conditionalCount;
|
|
193
154
|
}
|
|
194
155
|
|
|
195
|
-
// 4. Iterate (render <!-- each -->)
|
|
196
156
|
const iterationCount = renderAllIterations(parsedNode, state, manifest);
|
|
197
157
|
if (iterationCount > 0) {
|
|
198
158
|
iteratedCount = iterationCount;
|
|
199
159
|
}
|
|
200
160
|
|
|
201
|
-
// Note: Any DOM changes from steps 3-4 will trigger MutationObserver
|
|
202
|
-
// which will recursively call processMutations for nested content
|
|
203
|
-
|
|
204
161
|
return { hydratedCount, iteratedCount, evaluatedCount };
|
|
205
162
|
};
|
|
206
163
|
|
|
207
|
-
/**
|
|
208
|
-
* Deep clone a tree node (to avoid mutating hyperspeed template)
|
|
209
|
-
*/
|
|
210
164
|
const deepCloneNode = (node) => {
|
|
211
165
|
if (!node || typeof node !== 'object') return node;
|
|
212
166
|
|
|
213
167
|
const cloned = { ...node };
|
|
214
168
|
|
|
215
|
-
// Clone children recursively
|
|
216
169
|
if (node.children && typeof node.children === 'object') {
|
|
217
170
|
cloned.children = {};
|
|
218
171
|
for (const key in node.children) {
|
|
@@ -220,7 +173,6 @@ const deepCloneNode = (node) => {
|
|
|
220
173
|
}
|
|
221
174
|
}
|
|
222
175
|
|
|
223
|
-
// Clone meta if it exists
|
|
224
176
|
if (node.meta && typeof node.meta === 'object') {
|
|
225
177
|
cloned.meta = { ...node.meta };
|
|
226
178
|
if (node.meta.template) {
|
|
@@ -231,7 +183,6 @@ const deepCloneNode = (node) => {
|
|
|
231
183
|
}
|
|
232
184
|
}
|
|
233
185
|
|
|
234
|
-
// Clone runtime if it exists
|
|
235
186
|
if (node.runtime && typeof node.runtime === 'object') {
|
|
236
187
|
cloned.runtime = { ...node.runtime };
|
|
237
188
|
if (Array.isArray(node.runtime.instances)) {
|
|
@@ -242,121 +193,87 @@ const deepCloneNode = (node) => {
|
|
|
242
193
|
return cloned;
|
|
243
194
|
};
|
|
244
195
|
|
|
245
|
-
/**
|
|
246
|
-
* Merge hyperspeed manifest with runtime manifest
|
|
247
|
-
* Hyperspeed provides structure/metadata, runtime populates DOM references
|
|
248
|
-
*/
|
|
249
196
|
const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
250
197
|
if (!hyperspeedTree) return runtimeTree;
|
|
251
198
|
if (!runtimeTree) return hyperspeedTree;
|
|
252
199
|
|
|
253
|
-
// hyperspeedTree is already a clone (done before restoration to avoid mutation)
|
|
254
200
|
const merged = hyperspeedTree;
|
|
255
201
|
|
|
256
|
-
// Helper to recursively augment hyperspeed with runtime data
|
|
257
202
|
const augmentWithRuntime = (mergedNode, runtimeNode) => {
|
|
258
203
|
if (!runtimeNode) return;
|
|
259
204
|
|
|
260
|
-
// Debug: log node types
|
|
261
|
-
// Populate DOM references from runtime
|
|
262
205
|
if (runtimeNode.element) {
|
|
263
206
|
mergedNode.element = runtimeNode.element;
|
|
264
207
|
}
|
|
265
208
|
|
|
266
|
-
// Populate parsed data from runtime
|
|
267
209
|
if (runtimeNode.parsed) {
|
|
268
210
|
mergedNode.parsed = runtimeNode.parsed;
|
|
269
211
|
}
|
|
270
212
|
|
|
271
|
-
// Populate name bindings from runtime (detected after restoration)
|
|
272
213
|
if (runtimeNode.nameBindings) {
|
|
273
214
|
mergedNode.nameBindings = runtimeNode.nameBindings;
|
|
274
215
|
}
|
|
275
216
|
|
|
276
|
-
// Populate attributes from runtime (detected after restoration)
|
|
277
217
|
if (runtimeNode.attributes) {
|
|
278
218
|
mergedNode.attributes = runtimeNode.attributes;
|
|
279
219
|
}
|
|
280
220
|
|
|
281
|
-
// Populate textNode reference from runtime (for text node children)
|
|
282
221
|
if (runtimeNode.textNode) {
|
|
283
222
|
mergedNode.textNode = runtimeNode.textNode;
|
|
284
223
|
}
|
|
285
224
|
|
|
286
|
-
// For iterations: populate all meta and runtime from runtime
|
|
287
225
|
if (mergedNode.type === 'iteration' && runtimeNode.type === 'iteration') {
|
|
288
|
-
// Preserve compiled data from manifest (has compiled batch function)
|
|
289
226
|
const compiledData = mergedNode.compiled;
|
|
290
227
|
|
|
291
|
-
// Copy entire meta object from runtime (all properties needed)
|
|
292
228
|
mergedNode.meta = runtimeNode.meta;
|
|
293
|
-
// Copy runtime object (instances, etc.)
|
|
294
229
|
mergedNode.runtime = runtimeNode.runtime;
|
|
295
230
|
|
|
296
|
-
// Restore compiled data if it was present
|
|
297
231
|
if (compiledData) {
|
|
298
232
|
mergedNode.compiled = compiledData;
|
|
299
233
|
}
|
|
300
234
|
}
|
|
301
235
|
|
|
302
|
-
// For conditionals: populate all meta and runtime from runtime
|
|
303
236
|
if (mergedNode.type === 'conditional' && runtimeNode.type === 'conditional') {
|
|
304
|
-
// Copy entire meta object from runtime (all properties needed)
|
|
305
237
|
mergedNode.meta = runtimeNode.meta;
|
|
306
|
-
// Copy runtime object (activeBranch, activeInstance, templateRemoved)
|
|
307
238
|
mergedNode.runtime = runtimeNode.runtime;
|
|
308
239
|
}
|
|
309
240
|
|
|
310
|
-
// The runtime parse of the live DOM is the ground truth for what exists.
|
|
311
|
-
// A manifest child with no runtime counterpart sits at an index the DOM
|
|
312
|
-
// no longer agrees with (a content script prepending into <body> shifts
|
|
313
|
-
// every sibling) — it can never bind an element, and hydrating its
|
|
314
|
-
// bindings would dereference null. Drop it; the runtime-discovered
|
|
315
|
-
// sibling added below carries the real element.
|
|
316
241
|
if (mergedNode.children) {
|
|
317
242
|
for (const key in mergedNode.children) {
|
|
318
243
|
if (!runtimeNode.children?.[key]) delete mergedNode.children[key];
|
|
319
244
|
}
|
|
320
245
|
}
|
|
321
246
|
|
|
322
|
-
// Augment children recursively
|
|
323
247
|
if (runtimeNode.children) {
|
|
324
248
|
if (!mergedNode.children) mergedNode.children = {};
|
|
325
249
|
|
|
326
250
|
for (const key in runtimeNode.children) {
|
|
327
251
|
const runtimeChild = runtimeNode.children[key];
|
|
328
252
|
|
|
329
|
-
// For iterations: augment existing node, don't replace
|
|
330
253
|
if (runtimeChild.type === 'iteration') {
|
|
331
254
|
const hyperspeedChild = mergedNode.children[key];
|
|
332
255
|
|
|
333
256
|
if (hyperspeedChild) {
|
|
334
|
-
// Preserve compiled data from hyperspeed
|
|
335
257
|
const compiledData = hyperspeedChild.compiled;
|
|
336
258
|
|
|
337
|
-
// Update meta and runtime from runtime node
|
|
338
259
|
mergedNode.children[key].meta = runtimeChild.meta;
|
|
339
260
|
mergedNode.children[key].runtime = runtimeChild.runtime;
|
|
340
261
|
|
|
341
|
-
// Keep compiled data from hyperspeed (it has batchFn)
|
|
342
262
|
if (compiledData) {
|
|
343
263
|
mergedNode.children[key].compiled = compiledData;
|
|
344
264
|
}
|
|
345
265
|
} else {
|
|
346
|
-
// No hyperspeed node, just use runtime
|
|
347
266
|
mergedNode.children[key] = runtimeChild;
|
|
348
267
|
}
|
|
349
268
|
continue;
|
|
350
269
|
}
|
|
351
270
|
|
|
352
|
-
// For conditionals: use runtime node but preserve compiled data
|
|
353
271
|
if (runtimeChild.type === 'conditional') {
|
|
354
272
|
const hyperspeedChild = mergedNode.children[key];
|
|
355
273
|
const compiledData = hyperspeedChild?.compiled;
|
|
356
274
|
|
|
357
275
|
mergedNode.children[key] = runtimeChild;
|
|
358
276
|
|
|
359
|
-
// Restore compiled data if it existed
|
|
360
277
|
if (compiledData) {
|
|
361
278
|
mergedNode.children[key].compiled = compiledData;
|
|
362
279
|
}
|
|
@@ -364,20 +281,16 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
|
364
281
|
}
|
|
365
282
|
|
|
366
283
|
if (mergedNode.children[key]) {
|
|
367
|
-
// Child exists in both - augment it
|
|
368
284
|
augmentWithRuntime(mergedNode.children[key], runtimeNode.children[key]);
|
|
369
285
|
} else {
|
|
370
|
-
// Child only in runtime - add it (dynamic element discovered at runtime)
|
|
371
286
|
mergedNode.children[key] = runtimeNode.children[key];
|
|
372
287
|
}
|
|
373
288
|
}
|
|
374
289
|
}
|
|
375
290
|
};
|
|
376
291
|
|
|
377
|
-
// Augment hyperspeed foundation with runtime data
|
|
378
292
|
augmentWithRuntime(merged, runtimeTree);
|
|
379
293
|
|
|
380
|
-
// Preserve stats from runtime (hyperspeed won't have stats)
|
|
381
294
|
if (runtimeTree.stats) {
|
|
382
295
|
merged.stats = runtimeTree.stats;
|
|
383
296
|
}
|
|
@@ -385,7 +298,6 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
|
385
298
|
return merged;
|
|
386
299
|
};
|
|
387
300
|
|
|
388
|
-
// Store previous state for comparison (needs to be accessible by core loop)
|
|
389
301
|
let previousState = {};
|
|
390
302
|
|
|
391
303
|
const main = (s, config = {}, stringSelector = '') => {
|
|
@@ -393,26 +305,19 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
393
305
|
const verbose = !!config?.verbose;
|
|
394
306
|
((globalThis.__vibe ??= {}).debug = debug);
|
|
395
307
|
|
|
396
|
-
// Enable/disable the component template cache from config (`{ noCache }`).
|
|
397
308
|
configureComponentCache(config);
|
|
398
309
|
|
|
399
|
-
// Expose the global `$scope` resolver used by loop-scoped `on*` handlers.
|
|
400
310
|
installScopeResolver();
|
|
401
311
|
|
|
402
|
-
// Detect if running in compiler's headless browser
|
|
403
|
-
// When true: skip cleanup to preserve [vibe] attribute in compiled HTML
|
|
404
312
|
const isCompiling = typeof window !== 'undefined' && window.__vibe?.compiling === true;
|
|
405
313
|
|
|
406
|
-
// Reset previous state for each new instance
|
|
407
314
|
previousState = {};
|
|
408
315
|
|
|
409
|
-
// Use page-specific hyperspeed manifest (detected at module load)
|
|
410
316
|
const hyperspeedTree = hyperspeedManifest;
|
|
411
317
|
|
|
412
318
|
let rootElement = document.body;
|
|
413
319
|
|
|
414
320
|
if (stringSelector) {
|
|
415
|
-
// Find element(s) with the specified attribute
|
|
416
321
|
const elements = document.querySelectorAll(`${stringSelector}`);
|
|
417
322
|
if (elements?.[0]) {
|
|
418
323
|
rootElement = elements[0];
|
|
@@ -421,16 +326,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
421
326
|
|
|
422
327
|
debugLog(PHASE_ATTACH, `Vibe attached to`, debug, 0, rootElement);
|
|
423
328
|
|
|
424
|
-
// Component tagging is now handled by component.js before boot
|
|
425
|
-
// Component state is merged into s by boot.js
|
|
426
|
-
|
|
427
|
-
// RESTORATION PHASE: If hyperspeed detected, restore @[...] markers in DOM
|
|
428
|
-
// This allows pre-rendered values to be visible (no FOUC) but makes DOM reactive
|
|
429
329
|
let hyperspeedSubtree = null;
|
|
430
330
|
if (hyperspeedTree) {
|
|
431
331
|
const manifestName = hyperspeedPath ? hyperspeedPath.split('/').pop() : 'manifest.js';
|
|
432
332
|
|
|
433
|
-
// Count compiled features
|
|
434
333
|
const countCompiledIterations = (tree) => {
|
|
435
334
|
let count = 0;
|
|
436
335
|
if (tree.type === 'iteration' && tree.compiled?.iterations?.batchFn) count++;
|
|
@@ -444,7 +343,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
444
343
|
|
|
445
344
|
const compiledIterationCount = countCompiledIterations(hyperspeedTree);
|
|
446
345
|
|
|
447
|
-
// Log features that are enabled
|
|
448
346
|
debugLog(PHASE_HYPERSPEED, `Loaded ${manifestName}, page is pre-compiled`, debug);
|
|
449
347
|
|
|
450
348
|
if (compiledIterationCount > 0) {
|
|
@@ -461,17 +359,13 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
461
359
|
);
|
|
462
360
|
}
|
|
463
361
|
|
|
464
|
-
// Find matching subtree by DOM path (not just tag name)
|
|
465
|
-
// Build path from document to rootElement
|
|
466
362
|
const buildDomPath = (element) => {
|
|
467
363
|
const path = [];
|
|
468
364
|
let current = element;
|
|
469
365
|
|
|
470
|
-
// Walk up to document itself (include html in the path)
|
|
471
366
|
while (current && current.parentNode && current !== document) {
|
|
472
367
|
const parent = current.parentNode;
|
|
473
368
|
|
|
474
|
-
// Skip document node itself
|
|
475
369
|
if (parent === document) {
|
|
476
370
|
const siblings = Array.from(document.childNodes);
|
|
477
371
|
const index = siblings.indexOf(current);
|
|
@@ -491,19 +385,16 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
491
385
|
return path;
|
|
492
386
|
};
|
|
493
387
|
|
|
494
|
-
// Walk manifest tree following DOM path
|
|
495
388
|
const findManifestNodeByPath = (manifest, path) => {
|
|
496
389
|
let current = manifest;
|
|
497
390
|
|
|
498
391
|
for (const { tag, index } of path) {
|
|
499
392
|
if (!current || !current.children) return null;
|
|
500
393
|
|
|
501
|
-
// Look for matching child by tag_index pattern
|
|
502
394
|
const key = `${tag}_${index}`;
|
|
503
395
|
if (current.children[key]) {
|
|
504
396
|
current = current.children[key];
|
|
505
397
|
} else {
|
|
506
|
-
// Fallback: search all children for matching tag at this level
|
|
507
398
|
let found = false;
|
|
508
399
|
for (const childKey in current.children) {
|
|
509
400
|
if (childKey.startsWith(tag + '_')) {
|
|
@@ -523,36 +414,27 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
523
414
|
let subtreeFromManifest = findManifestNodeByPath(hyperspeedTree, domPath);
|
|
524
415
|
|
|
525
416
|
if (!subtreeFromManifest) {
|
|
526
|
-
// Fallback to root if path matching fails
|
|
527
417
|
subtreeFromManifest = hyperspeedTree;
|
|
528
418
|
}
|
|
529
419
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
hyperspeedSubtree = deepCloneNode(subtreeFromManifest); // For merging
|
|
533
|
-
const cloneForRestoration = deepCloneNode(subtreeFromManifest); // For restoration
|
|
420
|
+
hyperspeedSubtree = deepCloneNode(subtreeFromManifest);
|
|
421
|
+
const cloneForRestoration = deepCloneNode(subtreeFromManifest);
|
|
534
422
|
|
|
535
423
|
restoreMarkersFromManifest(rootElement, cloneForRestoration, hyperspeedTree);
|
|
536
424
|
}
|
|
537
425
|
|
|
538
|
-
// Save raw slot content of fetched components before hydration replaces @[...] markers.
|
|
539
|
-
// component.js captures el.innerHTML when resolving — if hydration already ran, the
|
|
540
|
-
// binding syntax is gone and the resolved component won't be reactive.
|
|
541
426
|
rootElement.querySelectorAll('component[src], div.component[src]').forEach(el => {
|
|
542
427
|
el._vibeSlotContent = el.innerHTML;
|
|
543
428
|
});
|
|
544
429
|
|
|
545
|
-
// Runtime parses DOM (which now has restored markers if hyperspeed was used)
|
|
546
430
|
let parsedTree = parse(rootElement);
|
|
547
431
|
|
|
548
|
-
// Merge with hyperspeed if available (hyperspeed as foundation, runtime augments)
|
|
549
432
|
if (hyperspeedSubtree) {
|
|
550
433
|
parsedTree = mergeManifests(hyperspeedSubtree, parsedTree);
|
|
551
434
|
}
|
|
552
435
|
|
|
553
436
|
let manifest = createManifest(parsedTree);
|
|
554
437
|
|
|
555
|
-
// Build hyperspeed manifest (before hydration, extract markers from parsed strings)
|
|
556
438
|
const hyperspeedManifestData = buildHyperspeedManifest(parsedTree);
|
|
557
439
|
|
|
558
440
|
const manifestEntries = Object.entries(manifest);
|
|
@@ -582,7 +464,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
582
464
|
debug,
|
|
583
465
|
);
|
|
584
466
|
|
|
585
|
-
// Lifecycle hooks that users can subscribe to
|
|
586
467
|
const hooks = {
|
|
587
468
|
afterUpdate: [],
|
|
588
469
|
afterDomMutation: [],
|
|
@@ -590,25 +471,14 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
590
471
|
unmount: [],
|
|
591
472
|
};
|
|
592
473
|
|
|
593
|
-
// The ready phase happens once. A listener registered after it fires
|
|
594
|
-
// immediately (parity with the late-safe $.ready promise) — late
|
|
595
|
-
// registration is the SPA norm, where fragment scripts run on mount,
|
|
596
|
-
// long after the shell booted.
|
|
597
474
|
let readyFired = false;
|
|
598
475
|
|
|
599
|
-
// Page-scope 'unmount': the visitor actually leaving — pagehide (navigation
|
|
600
|
-
// away, tab close). Deliberately NOT visibilitychange: a tab switch is not
|
|
601
|
-
// an unmount, the visitor comes back. Inside a component script the same
|
|
602
|
-
// event name resolves to that component's unmount instead (the scoped `$`
|
|
603
|
-
// proxy in component.js intercepts it before it reaches this hook).
|
|
604
476
|
if (typeof window !== 'undefined') {
|
|
605
477
|
window.addEventListener('pagehide', () => {
|
|
606
478
|
hooks.unmount.forEach((callback) => callback());
|
|
607
479
|
});
|
|
608
480
|
}
|
|
609
481
|
|
|
610
|
-
// Extract plain values from proxy (removes proxy wrappers)
|
|
611
|
-
// Optimized: indexed loops, Object.keys (no prototype walk), inline primitive check
|
|
612
482
|
const extractPlainValue = (obj) => {
|
|
613
483
|
if (obj === null || typeof obj !== 'object') return obj;
|
|
614
484
|
if (Array.isArray(obj)) {
|
|
@@ -631,14 +501,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
631
501
|
return plain;
|
|
632
502
|
};
|
|
633
503
|
|
|
634
|
-
// Observer callback will be defined below (already declared above before processComponent)
|
|
635
|
-
|
|
636
|
-
// Pause the observer around an engine patch phase (its DOM writes are the
|
|
637
|
-
// engine's own), then replay anything that queued while paused and resolve
|
|
638
|
-
// fresh <component src> mounts. Shared by the walk flush and the
|
|
639
|
-
// subscription dispatch flush.
|
|
640
504
|
const patchWithObserverPaused = (patch) => {
|
|
641
|
-
// Capture pending mutations before disconnecting (takeRecords clears the queue)
|
|
642
505
|
let pendingMutations = [];
|
|
643
506
|
if (observer) {
|
|
644
507
|
pendingMutations = observer.takeRecords();
|
|
@@ -655,14 +518,11 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
655
518
|
subtree: true,
|
|
656
519
|
});
|
|
657
520
|
|
|
658
|
-
// Process mutations that were pending before we disconnected
|
|
659
521
|
if (pendingMutations.length > 0 && processMutations) {
|
|
660
522
|
processMutations(pendingMutations);
|
|
661
523
|
}
|
|
662
524
|
}
|
|
663
525
|
|
|
664
|
-
// The patch may have mounted new DOM while the observer was disconnected.
|
|
665
|
-
// Scan for unresolved <component src=""> elements that need fetching.
|
|
666
526
|
if (componentProcessingStarted) {
|
|
667
527
|
const componentConfig = {
|
|
668
528
|
...config,
|
|
@@ -675,17 +535,8 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
675
535
|
};
|
|
676
536
|
|
|
677
537
|
const $ = state(s, (changedProps) => {
|
|
678
|
-
// Selective extraction: only extract changed props, preserve references for unchanged.
|
|
679
|
-
// This ensures affected() correctly skips iterations whose arrays didn't change,
|
|
680
|
-
// while still detecting binding changes inside iteration instances.
|
|
681
538
|
const currentState = { ...previousState };
|
|
682
539
|
for (const prop of changedProps) {
|
|
683
|
-
// Distinguish "set to undefined" (key still present in $) from "deleted"
|
|
684
|
-
// (key absent from $). For deletions, removing from currentState matches
|
|
685
|
-
// the live proxy's shape — otherwise downstream consumers that pass
|
|
686
|
-
// currentState as `$` to evalInScope (e.g. iterations that re-render via
|
|
687
|
-
// bindings reading the root state) would see the deleted key as a phantom
|
|
688
|
-
// own-property with value `undefined`.
|
|
689
540
|
if (prop in $) {
|
|
690
541
|
currentState[prop] = extractPlainValue($[prop]);
|
|
691
542
|
} else {
|
|
@@ -693,11 +544,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
693
544
|
}
|
|
694
545
|
}
|
|
695
546
|
|
|
696
|
-
// THE update path: the flush is served from the subscription reverse
|
|
697
|
-
// index — a write notifies exactly its subscribers, O(change). The walk
|
|
698
|
-
// (`affected()`) exists only as the MOUNT path now: first hydration,
|
|
699
|
-
// processMutations, fresh branches and rows, where it registers new
|
|
700
|
-
// subscribers via their first evaluation.
|
|
701
547
|
const dirty = beginFlush(changedProps);
|
|
702
548
|
if (dirty.size > 0) {
|
|
703
549
|
debugLog(PHASE_UPDATE, 'state changed', debug);
|
|
@@ -706,30 +552,13 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
706
552
|
});
|
|
707
553
|
}
|
|
708
554
|
|
|
709
|
-
// Store previous state for hooks (currentState is already plain, no need to clone)
|
|
710
555
|
const prev = previousState;
|
|
711
556
|
previousState = currentState;
|
|
712
557
|
hooks.afterUpdate.forEach((callback) => callback(currentState, prev));
|
|
713
558
|
});
|
|
714
559
|
|
|
715
|
-
// Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
|
|
716
|
-
// `configurable: true` lets a component script's scoped `$` Proxy (built in
|
|
717
|
-
// component.js) legally return a wrapped `.on` that auto-registers cleanup
|
|
718
|
-
// — the Proxy invariant rejects overriding non-configurable + non-writable
|
|
719
|
-
// data properties. Enumerable stays false so `on` doesn't leak into state
|
|
720
|
-
// snapshots or Object.keys($).
|
|
721
|
-
// Late ready — a `$.on('ready')` AFTER boot (a fetched fragment's script,
|
|
722
|
-
// an SPA mount's app-boot module) defers until the registering mount
|
|
723
|
-
// SETTLES: content inserted, attributes hydrated, staging committed, fouc
|
|
724
|
-
// gates released. That makes `$.on('ready')` ≡ `await $.ready` genuinely
|
|
725
|
-
// true — DOM-touching ready work (querySelector, scroll-spy) sees the
|
|
726
|
-
// mounted subtree in both forms. With nothing in flight the microtask
|
|
727
|
-
// flush fires on the same tick `await $.ready` would resume on.
|
|
728
560
|
const lateReady = [];
|
|
729
561
|
let lateReadyWatch = null;
|
|
730
|
-
// Settlement is judged against the MANAGED root, same as boot ready —
|
|
731
|
-
// content outside a config.target root is never scanned, so its literal
|
|
732
|
-
// @[...] text must not starve the late-ready queue.
|
|
733
562
|
const mountsSettled = () =>
|
|
734
563
|
!document.querySelector('[vibe-staged], [vibe-fouc]') && shouldCleanup(rootElement);
|
|
735
564
|
const flushLateReady = () => {
|
|
@@ -770,46 +599,23 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
770
599
|
configurable: true,
|
|
771
600
|
});
|
|
772
601
|
|
|
773
|
-
// Promise that resolves after the ready hook fires and all ready callbacks
|
|
774
|
-
// have run. Lets late subscribers await readiness without missing the event:
|
|
775
|
-
// `await $.ready`. Non-enumerable so it won't leak into state snapshots.
|
|
776
602
|
let resolveReady;
|
|
777
603
|
Object.defineProperty($, 'ready', {
|
|
778
604
|
value: new Promise((resolve) => { resolveReady = resolve; }),
|
|
779
605
|
enumerable: false,
|
|
780
606
|
});
|
|
781
607
|
|
|
782
|
-
// Reconcile a managed subtree against new source HTML. Opt-in entry point;
|
|
783
|
-
// dormant unless called (so hot paths and benchmarks are unaffected).
|
|
784
608
|
Object.defineProperty($, 'reconcile', {
|
|
785
609
|
value: reconcile,
|
|
786
610
|
enumerable: false,
|
|
787
611
|
});
|
|
788
612
|
|
|
789
|
-
// Register a component's state bucket under its id (the fetched-mount path).
|
|
790
|
-
// A FRESH id is a key nothing in the live tree can bind yet — its subtree
|
|
791
|
-
// enters the reactive tree only after this write — so the write lands
|
|
792
|
-
// silently (raw target, no global flush) and previousState is deliberately
|
|
793
|
-
// NOT seeded: the mount's grouped notify must see undefined → state as a
|
|
794
|
-
// real diff (see the inline comment below — the load-bearing invariant).
|
|
795
|
-
// Re-registering an EXISTING id (an HMR re-run) is a real value change for
|
|
796
|
-
// live bindings and flushes normally.
|
|
797
613
|
Object.defineProperty($, '_register', {
|
|
798
614
|
value: (componentId, componentState) => {
|
|
799
615
|
if (componentId in $) {
|
|
800
616
|
$[componentId] = componentState;
|
|
801
617
|
return componentId;
|
|
802
618
|
}
|
|
803
|
-
// Raw write only — previousState is deliberately NOT seeded. The mount
|
|
804
|
-
// commits all its registrations in one notifyChanged batch when its
|
|
805
|
-
// scripts settle, and that flush must see each fresh id as a real
|
|
806
|
-
// change (undefined -> state): the subtree may have hydrated BEFORE the
|
|
807
|
-
// script ran (scripts execute in module order behind earlier imports),
|
|
808
|
-
// so this correction flush is what renders bindings, conditionals and
|
|
809
|
-
// iterations that read the component's state. A seeded previousState
|
|
810
|
-
// made the diff vacuous whenever the script filled its state object
|
|
811
|
-
// before calling component() — the game's fill-then-register pages
|
|
812
|
-
// stayed frozen at their pre-registration (empty) render.
|
|
813
619
|
silentSet($, componentId, componentState);
|
|
814
620
|
return componentId;
|
|
815
621
|
},
|
|
@@ -817,80 +623,35 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
817
623
|
configurable: true,
|
|
818
624
|
});
|
|
819
625
|
|
|
820
|
-
// Mark a trusted string as raw HTML. A binding that is the sole content of
|
|
821
|
-
// its element — `<p>@[$.unsafe(desc)]</p>` — sets innerHTML from the string
|
|
822
|
-
// instead of escaping it via textContent. Trusted input only (no sanitizing,
|
|
823
|
-
// like Svelte {@html}). Non-enumerable so it never leaks into state snapshots.
|
|
824
626
|
Object.defineProperty($, 'unsafe', {
|
|
825
627
|
value: unsafe,
|
|
826
628
|
enumerable: false,
|
|
827
629
|
configurable: true,
|
|
828
630
|
});
|
|
829
631
|
|
|
830
|
-
// Pure-render path for surgical component HMR. Given raw component template
|
|
831
|
-
// HTML, callsite props, slot HTML, and existing componentIds, returns the
|
|
832
|
-
// processed HTML string the plugin's HMR handler can hand to $.reconcile.
|
|
833
|
-
// Scripts are NOT executed — callers use this only when they've verified
|
|
834
|
-
// script contents haven't changed (so registered state is still valid).
|
|
835
632
|
Object.defineProperty($, '_renderComponent', {
|
|
836
633
|
value: renderComponentTemplate,
|
|
837
634
|
enumerable: false,
|
|
838
635
|
});
|
|
839
636
|
|
|
840
|
-
// Invalidate cached component templates. `$.clearComponentCache(path)` drops
|
|
841
|
-
// one entry, `$.clearComponentCache()` drops all. Templates are immutable in
|
|
842
|
-
// production (nothing to clear), so this exists for tooling that swaps a
|
|
843
|
-
// template under a live session — e.g. the dev server busts the changed file
|
|
844
|
-
// on hot update. Non-enumerable so it never leaks into state snapshots.
|
|
845
637
|
Object.defineProperty($, 'clearComponentCache', {
|
|
846
638
|
value: clearComponentCache,
|
|
847
639
|
enumerable: false,
|
|
848
640
|
configurable: true,
|
|
849
641
|
});
|
|
850
642
|
|
|
851
|
-
// Expose the live reactive proxy to the iteration stamper so loop-scoped
|
|
852
|
-
// `on*` handlers (`$scope`) resolve the SAME object identity the app sees via
|
|
853
|
-
// `$`, instead of the plain diff-snapshot clones iterations render against
|
|
854
|
-
// (see extractPlainValue below). Non-enumerable so it never shows up in the
|
|
855
|
-
// manifest's node-path entry iteration.
|
|
856
643
|
Object.defineProperty(manifest, '__live', { value: $, enumerable: false, configurable: true });
|
|
857
644
|
|
|
858
|
-
// Pair the flat manifest (dotPath -> element) with its parsed tree so removal
|
|
859
|
-
// paths can prune both views together. The page MutationObserver is
|
|
860
|
-
// disconnected while Vibe renders (iteration/conditional teardown removes DOM
|
|
861
|
-
// unobserved), so those paths must prune the manifest + tree themselves; this
|
|
862
|
-
// gives them the tree root without threading it through every call. Same
|
|
863
|
-
// non-enumerable contract as __live.
|
|
864
645
|
Object.defineProperty(manifest, '__tree', { value: parsedTree, enumerable: false, configurable: true });
|
|
865
646
|
|
|
866
|
-
// Bind `$` inside every expression to this live root proxy (see setRootState
|
|
867
|
-
// in utils.js). Done before the first hydration pass so `$.unsafe` and the
|
|
868
|
-
// other reserved methods are reachable from the initial render onward.
|
|
869
647
|
setRootState($);
|
|
870
648
|
|
|
871
|
-
// Publish the real proxy on `window.$` BEFORE the first hydration pass.
|
|
872
|
-
// boot.js sets `window.$ = main(...)`, but until main returns it's the
|
|
873
|
-
// pre-boot placeholder (vibeInstance with no state keys). User helpers
|
|
874
|
-
// defined on `window` that close over `$` — e.g. a global `brawlerActivity`
|
|
875
|
-
// function reading `$.combat?.duration` — would then read the placeholder
|
|
876
|
-
// during initial render and treat the whole world as empty. Assigning the
|
|
877
|
-
// live proxy here lets those closures see the right `$` from the very
|
|
878
|
-
// first conditional/binding eval.
|
|
879
649
|
if (typeof window !== 'undefined') window.$ = $;
|
|
880
650
|
|
|
881
|
-
// Compiled pages: execute build-inlined component scripts (neutered to
|
|
882
|
-
// type="vibe-module" by the compiler) through the runtime's component-script
|
|
883
|
-
// pipeline — same injected component(), same scoped `$`, same import
|
|
884
|
-
// rewriting as fetched scripts. Runs after `window.$` is live so
|
|
885
|
-
// `const id = component(state); $[id].x = ...` captures the reactive proxy,
|
|
886
|
-
// and before initial hydration so synchronous scripts' state is already
|
|
887
|
-
// registered when `this.` bindings first evaluate. Async scripts gate
|
|
888
|
-
// `ready` via compiledScriptsDone below.
|
|
889
651
|
let compiledScriptsDone = true;
|
|
890
652
|
const compiledScriptsPending = executeCompiledComponentScripts();
|
|
891
653
|
if (compiledScriptsPending) compiledScriptsDone = false;
|
|
892
654
|
|
|
893
|
-
// Initial hydration - pass plain values so iteration can do reference comparison
|
|
894
655
|
const initialState = extractPlainValue($);
|
|
895
656
|
const affectedElements = affected(parsedTree, initialState, initialState);
|
|
896
657
|
|
|
@@ -907,12 +668,8 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
907
668
|
debug,
|
|
908
669
|
);
|
|
909
670
|
|
|
910
|
-
// Hydrate with proxy $ so DOM bindings work
|
|
911
|
-
// Pass initialState as oldState for iterations (won't actually update, just initial render)
|
|
912
671
|
hydrate(affectedElements, $, manifest, initialState);
|
|
913
672
|
|
|
914
|
-
// Render all iterations and conditionals after initial hydration
|
|
915
|
-
// Use initialState (plain values) for iteration rendering so reference comparison works
|
|
916
673
|
const iterationCount = renderAllIterations(parsedTree, initialState, manifest);
|
|
917
674
|
if (iterationCount > 0)
|
|
918
675
|
debugLog(
|
|
@@ -943,35 +700,28 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
943
700
|
debug,
|
|
944
701
|
);
|
|
945
702
|
|
|
946
|
-
// After all initial rendering, capture a clean snapshot for comparison
|
|
947
703
|
previousState = extractPlainValue($);
|
|
948
704
|
|
|
949
|
-
// Observer reference and processMutations
|
|
950
705
|
let observer = null;
|
|
951
706
|
let processMutations = null;
|
|
952
707
|
|
|
953
|
-
// Define observer callback as named function so we can call it manually for pending mutations
|
|
954
708
|
processMutations = (mutations) => {
|
|
955
|
-
// Early exit if no mutations to process (common case)
|
|
956
709
|
if (mutations.length === 0) return;
|
|
957
710
|
|
|
958
711
|
const manifestSizeBefore = debug ? Object.keys(manifest).length : 0;
|
|
959
712
|
let hadChanges = false;
|
|
960
|
-
let parsedParents = null;
|
|
713
|
+
let parsedParents = null;
|
|
961
714
|
let addedElements = 0;
|
|
962
715
|
let addedNodes = 0;
|
|
963
716
|
let removedElements = 0;
|
|
964
717
|
let removedNodes = 0;
|
|
965
|
-
let singleElement = null;
|
|
966
|
-
let totalSkipped = 0;
|
|
718
|
+
let singleElement = null;
|
|
719
|
+
let totalSkipped = 0;
|
|
967
720
|
let hydratedCount = 0;
|
|
968
721
|
let iteratedCount = 0;
|
|
969
722
|
let evaluatedCount = 0;
|
|
970
|
-
let addedElementsList = [];
|
|
723
|
+
let addedElementsList = [];
|
|
971
724
|
|
|
972
|
-
// Collect data-vibe-component-id values across ALL removed subtrees in this
|
|
973
|
-
// batch before doing any per-node work, so we can evict their state after
|
|
974
|
-
// the DOM mutations have been applied.
|
|
975
725
|
const removedComponentIds = new Set();
|
|
976
726
|
mutations.forEach(({ removedNodes: removedNodesList }) => {
|
|
977
727
|
removedNodesList.forEach((node) => collectComponentIds(node, removedComponentIds));
|
|
@@ -979,21 +729,12 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
979
729
|
|
|
980
730
|
mutations.forEach(({ addedNodes: addedNodesList, removedNodes: removedNodesList, target }) => {
|
|
981
731
|
removedNodesList.forEach((node) => {
|
|
982
|
-
// If this is a component element with pending fetch, abort it
|
|
983
732
|
if (node.nodeName === 'COMPONENT') {
|
|
984
733
|
abortComponentFetch(node);
|
|
985
734
|
}
|
|
986
735
|
|
|
987
|
-
// If this node is tracked by a conditional branch (e.g. a <component src>
|
|
988
|
-
// that was replaced by processComponent via el.replaceWith), update the
|
|
989
|
-
// conditional's tracked reference to point to the replacement node.
|
|
990
736
|
const branchRef = branchNodeRegistry.get(node);
|
|
991
737
|
if (branchRef) {
|
|
992
|
-
// Find the replacement: an added node in the same mutation at the same
|
|
993
|
-
// parent. A STAGED remount splits add and remove into different
|
|
994
|
-
// batches (the incoming wrapper is inserted early, the outgoing one
|
|
995
|
-
// removed at commit) — the wrapper's replacement back-pointer covers
|
|
996
|
-
// that case.
|
|
997
738
|
const replacement =
|
|
998
739
|
Array.from(addedNodesList).find(n => n.parentNode === target) ||
|
|
999
740
|
(node._vibeReplacedBy?.isConnected ? node._vibeReplacedBy : null);
|
|
@@ -1006,30 +747,19 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1006
747
|
|
|
1007
748
|
const dotAnnotation = manifestPathOf(manifest, node);
|
|
1008
749
|
|
|
1009
|
-
// Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
|
|
1010
750
|
if (dotAnnotation === null) return;
|
|
1011
751
|
|
|
1012
|
-
// Prune the node's ENTIRE manifest scope — root entry plus every
|
|
1013
|
-
// descendant and branch-alias path under it. Deleting just the root
|
|
1014
|
-
// entry leaked the rest, pinning each swapped-out page's detached
|
|
1015
|
-
// DOM forever (the SPA navigation leak).
|
|
1016
752
|
removeManifestSubtree(manifest, dotAnnotation);
|
|
1017
753
|
|
|
1018
754
|
const dotPath = dotAnnotation.split('.');
|
|
1019
755
|
const name = dotPath.pop();
|
|
1020
756
|
const parentDotAnnotation = dotPath.join('.');
|
|
1021
757
|
|
|
1022
|
-
// Identity fallback mirrors the added-node path: a parent inside
|
|
1023
|
-
// slot-projected branch content isn't reachable via navigateTree's
|
|
1024
|
-
// `children` walk, so resolve it by element identity instead — otherwise
|
|
1025
|
-
// the removed node's stale tree entry lingers alongside its replacement.
|
|
1026
758
|
const picked =
|
|
1027
759
|
navigateTree(parsedTree, parentDotAnnotation) || findNodeByElement(parsedTree, target);
|
|
1028
760
|
|
|
1029
|
-
// If we can't navigate to the parent, skip
|
|
1030
761
|
if (!picked || !picked.element) return;
|
|
1031
762
|
|
|
1032
|
-
// Update parent's parsed HTML (only once per parent)
|
|
1033
763
|
if (!parsedParents) parsedParents = new Set();
|
|
1034
764
|
if (!parsedParents.has(picked)) {
|
|
1035
765
|
const { parsed } = parse(picked.element);
|
|
@@ -1037,10 +767,8 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1037
767
|
parsedParents.add(picked);
|
|
1038
768
|
}
|
|
1039
769
|
|
|
1040
|
-
// Remove the node from parent's children
|
|
1041
770
|
delete picked.children[name];
|
|
1042
771
|
|
|
1043
|
-
// Count elements vs nodes separately
|
|
1044
772
|
if (node.nodeName.startsWith('#')) {
|
|
1045
773
|
removedNodes++;
|
|
1046
774
|
} else {
|
|
@@ -1051,16 +779,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1051
779
|
});
|
|
1052
780
|
|
|
1053
781
|
addedNodesList.forEach((node) => {
|
|
1054
|
-
// Skip nodes that aren't element nodes, are non-reactive, or lack Vibe syntax
|
|
1055
782
|
if (!shouldProcessNode(node)) {
|
|
1056
783
|
return;
|
|
1057
784
|
}
|
|
1058
785
|
|
|
1059
|
-
// Capture raw slot content of nested <component src> elements BEFORE parse runs.
|
|
1060
|
-
// Parse creates conditional nodes from <!-- if --> comments, and renderConditional
|
|
1061
|
-
// later removes the template nodes between the comments. Without capturing slot
|
|
1062
|
-
// content first, conditionals inside a component's slot content lose their branch
|
|
1063
|
-
// templates, breaking reactive updates.
|
|
1064
786
|
if (node.nodeType === 1) {
|
|
1065
787
|
node.querySelectorAll('component[src], div.component[src]').forEach((el) => {
|
|
1066
788
|
if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
|
|
@@ -1069,23 +791,13 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1069
791
|
|
|
1070
792
|
const dotAnnotation = manifestPathOf(manifest, target);
|
|
1071
793
|
|
|
1072
|
-
// Parse the newly added node
|
|
1073
794
|
const parsedNode = parse(node);
|
|
1074
795
|
|
|
1075
|
-
// Accumulate skipped stats
|
|
1076
796
|
if (parsedNode.stats?.skipped) {
|
|
1077
797
|
totalSkipped += parsedNode.stats.skipped;
|
|
1078
798
|
}
|
|
1079
799
|
|
|
1080
|
-
// If parent is tracked in the manifest, register this new node in the parsed tree.
|
|
1081
|
-
// (If not — e.g. mutations inside an iteration instance whose rows aren't in the
|
|
1082
|
-
// global manifest — we still hydrate the node below; we just skip tree/manifest
|
|
1083
|
-
// registration since there's no tree branch to attach to.)
|
|
1084
800
|
if (dotAnnotation !== null) {
|
|
1085
|
-
// navigateTree walks `children` only; when the parent lives in a
|
|
1086
|
-
// conditional branch's slot-projected content its manifest path isn't
|
|
1087
|
-
// navigable that way (see findNodeByElement). Fall back to an identity
|
|
1088
|
-
// search so the resolved subtree still links into the reactive tree.
|
|
1089
801
|
const picked =
|
|
1090
802
|
navigateTree(parsedTree, dotAnnotation) || findNodeByElement(parsedTree, target);
|
|
1091
803
|
|
|
@@ -1094,15 +806,12 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1094
806
|
picked.element = target;
|
|
1095
807
|
}
|
|
1096
808
|
|
|
1097
|
-
// Parse the newly added node (use monotonically increasing counter for deterministic key)
|
|
1098
|
-
// Initialize counter if it doesn't exist
|
|
1099
809
|
if (!picked._nextChildIndex) {
|
|
1100
810
|
picked._nextChildIndex = Object.keys(picked.children).length;
|
|
1101
811
|
}
|
|
1102
812
|
const name = `${node.nodeName.toLowerCase()}_${picked._nextChildIndex}`;
|
|
1103
|
-
picked._nextChildIndex++;
|
|
813
|
+
picked._nextChildIndex++;
|
|
1104
814
|
|
|
1105
|
-
// Update parent's parsed HTML (only once per parent)
|
|
1106
815
|
if (!parsedParents) parsedParents = new Set();
|
|
1107
816
|
if (!parsedParents.has(picked)) {
|
|
1108
817
|
const { parsed } = parse(picked.element);
|
|
@@ -1110,34 +819,21 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1110
819
|
parsedParents.add(picked);
|
|
1111
820
|
}
|
|
1112
821
|
|
|
1113
|
-
// Add the parsed node to parent's children
|
|
1114
822
|
picked.children[name] = parsedNode;
|
|
1115
823
|
|
|
1116
|
-
// Recursively add node and all descendants to manifest
|
|
1117
824
|
addToManifest(parsedNode, manifest, `${dotAnnotation}.${name}`);
|
|
1118
825
|
}
|
|
1119
826
|
}
|
|
1120
827
|
|
|
1121
|
-
// Run core loop for the new node (parse already done, hydrate → conditionals → iterate)
|
|
1122
828
|
const counts = processCoreLoop(node, parsedNode, $, manifest, true, debug);
|
|
1123
829
|
hydratedCount += counts.hydratedCount;
|
|
1124
830
|
iteratedCount += counts.iteratedCount;
|
|
1125
831
|
evaluatedCount += counts.evaluatedCount;
|
|
1126
832
|
|
|
1127
|
-
// For inlined `<component>` wrappers belonging to an iteration row
|
|
1128
|
-
// (marked by component.js's resolveIterationComponentProps transfer),
|
|
1129
|
-
// stash the parsed tree on the wrapper. processCoreLoop has just run
|
|
1130
|
-
// hydrate + renderAllConditionals + renderAllIterations on it, so
|
|
1131
|
-
// `parsedNode` carries live `runtime.activeInstance` /
|
|
1132
|
-
// `runtime.instances` data — exactly what `affected.js` needs to walk
|
|
1133
|
-
// into branch / row content. Iterate.js's update path consumes this
|
|
1134
|
-
// tree to re-hydrate bindings inside the inlined component on each
|
|
1135
|
-
// row-scope change without rebuilding the wrapper's DOM.
|
|
1136
833
|
if (node.nodeType === 1 && node._vibeIterPropExprs) {
|
|
1137
834
|
node._vibeIterTree = parsedNode;
|
|
1138
835
|
}
|
|
1139
836
|
|
|
1140
|
-
// Count elements vs nodes separately
|
|
1141
837
|
if (node.nodeName.startsWith('#')) {
|
|
1142
838
|
addedNodes++;
|
|
1143
839
|
} else {
|
|
@@ -1149,23 +845,18 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1149
845
|
});
|
|
1150
846
|
});
|
|
1151
847
|
|
|
1152
|
-
// CLEANUP OF CURRENT STATE
|
|
1153
848
|
releaseOrphanedComponentState(removedComponentIds);
|
|
1154
849
|
let hadRemovals = false;
|
|
1155
850
|
mutations.forEach(({ removedNodes: removedNodesList }) => {
|
|
1156
851
|
if (removedNodesList.length > 0) hadRemovals = true;
|
|
1157
852
|
releaseOrphanedIterationProps(removedNodesList);
|
|
1158
853
|
});
|
|
1159
|
-
// Subscribers anchored in the removed subtrees are dead — drop them from
|
|
1160
|
-
// the reverse index (observed removals: page swaps, reconcile, app code).
|
|
1161
854
|
if (hadRemovals) pruneDisconnected();
|
|
1162
855
|
|
|
1163
|
-
// Fire hooks once after all mutations are processed (not per-node)
|
|
1164
856
|
if (hadChanges) {
|
|
1165
857
|
if (addedElements > 0 || addedNodes > 0 || removedElements > 0 || removedNodes > 0) {
|
|
1166
858
|
const segments = [];
|
|
1167
859
|
|
|
1168
|
-
// Added
|
|
1169
860
|
if (addedElements > 0) {
|
|
1170
861
|
segments.push({ text: `+${addedElements}`, color: 'green' });
|
|
1171
862
|
segments.push({
|
|
@@ -1179,7 +870,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1179
870
|
segments.push({ text: ` ${addedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
|
|
1180
871
|
}
|
|
1181
872
|
|
|
1182
|
-
// Removed
|
|
1183
873
|
if (removedElements > 0) {
|
|
1184
874
|
if (addedElements > 0 || addedNodes > 0) segments.push({ text: ', ', colored: false });
|
|
1185
875
|
segments.push({ text: `-${removedElements}`, color: 'red' });
|
|
@@ -1195,25 +885,15 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1195
885
|
segments.push({ text: ` ${removedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
|
|
1196
886
|
}
|
|
1197
887
|
|
|
1198
|
-
// Pass element reference if exactly one element was mutated (not counting nodes)
|
|
1199
|
-
// const totalElementCount = addedElements + removedElements;
|
|
1200
|
-
// const elementToLog = totalElementCount === 1 ? singleElement : null;
|
|
1201
|
-
// debugLog(PHASE_MUTATE, segments, debug, 0, elementToLog);
|
|
1202
888
|
debugLog(PHASE_MUTATE, segments, debug);
|
|
1203
889
|
|
|
1204
|
-
// Verbose: log the added element (or topmost parent if multiple)
|
|
1205
890
|
if (verbose && addedElementsList.length > 0) {
|
|
1206
|
-
// Find the topmost parent among added elements
|
|
1207
891
|
const topmostParent = addedElementsList.find((el) => {
|
|
1208
|
-
// Check if this element is NOT a descendant of any other element in the list
|
|
1209
892
|
return !addedElementsList.some((other) => other !== el && other.contains(el));
|
|
1210
893
|
});
|
|
1211
894
|
debugLog(PHASE_MUTATE, '', debug, 0, topmostParent || addedElementsList[0]);
|
|
1212
895
|
}
|
|
1213
896
|
|
|
1214
|
-
// Log parsed elements after mutation — debug-only: the entries
|
|
1215
|
-
// snapshot allocates the full manifest per batch, so skip it entirely
|
|
1216
|
-
// when nothing will be printed.
|
|
1217
897
|
const manifestSizeAfter = debug ? Object.keys(manifest).length : manifestSizeBefore;
|
|
1218
898
|
const totalNodes = manifestSizeAfter - manifestSizeBefore;
|
|
1219
899
|
|
|
@@ -1240,7 +920,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1240
920
|
|
|
1241
921
|
debugLog(PHASE_PARSE, parseSegments, debug);
|
|
1242
922
|
|
|
1243
|
-
// Log hydration after parse
|
|
1244
923
|
if (hydratedCount > 0) {
|
|
1245
924
|
debugLog(
|
|
1246
925
|
PHASE_HYDRATE,
|
|
@@ -1256,7 +935,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1256
935
|
);
|
|
1257
936
|
}
|
|
1258
937
|
|
|
1259
|
-
// Log iterations after hydration
|
|
1260
938
|
if (iteratedCount > 0) {
|
|
1261
939
|
debugLog(
|
|
1262
940
|
PHASE_ITERATE,
|
|
@@ -1272,7 +950,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1272
950
|
);
|
|
1273
951
|
}
|
|
1274
952
|
|
|
1275
|
-
// Log conditionals after iterations
|
|
1276
953
|
if (evaluatedCount > 0) {
|
|
1277
954
|
debugLog(
|
|
1278
955
|
PHASE_CONDITION,
|
|
@@ -1291,12 +968,8 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1291
968
|
}
|
|
1292
969
|
}
|
|
1293
970
|
|
|
1294
|
-
// Always call hooks, even if no changes detected (cleanup needs to check)
|
|
1295
971
|
hooks.afterDomMutation.forEach((callback) => callback());
|
|
1296
972
|
|
|
1297
|
-
// Check for new <component> elements after DOM mutations
|
|
1298
|
-
// Process component elements if we've started (initial call happened)
|
|
1299
|
-
// Note: Also process after cleanup — conditionals may reveal new components
|
|
1300
973
|
if (componentProcessingStarted) {
|
|
1301
974
|
const componentConfig = {
|
|
1302
975
|
...config,
|
|
@@ -1307,7 +980,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1307
980
|
processComponent(
|
|
1308
981
|
rootElement,
|
|
1309
982
|
() => {
|
|
1310
|
-
// When all components are done, run cleanup check
|
|
1311
983
|
checkCleanup();
|
|
1312
984
|
},
|
|
1313
985
|
componentConfig,
|
|
@@ -1328,34 +1000,21 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1328
1000
|
|
|
1329
1001
|
debugLog(PHASE_OBSERVE, `MutationObserver listening to DOM changes`, debug);
|
|
1330
1002
|
|
|
1331
|
-
// Track cleanup state
|
|
1332
1003
|
let componentProcessingStarted = false;
|
|
1333
1004
|
let cleanupExecuted = false;
|
|
1334
1005
|
let bootSettleDone = false;
|
|
1335
1006
|
|
|
1336
|
-
// Check if cleanup should run
|
|
1337
1007
|
const checkCleanup = () => {
|
|
1338
1008
|
if (cleanupExecuted || isCompiling || !compiledScriptsDone) return;
|
|
1339
1009
|
|
|
1340
|
-
// Check for pending mutations first
|
|
1341
1010
|
const pendingMutations = observer ? observer.takeRecords() : [];
|
|
1342
1011
|
|
|
1343
1012
|
if (pendingMutations.length > 0) {
|
|
1344
|
-
// More mutations to process
|
|
1345
1013
|
processMutations(pendingMutations);
|
|
1346
1014
|
return;
|
|
1347
1015
|
}
|
|
1348
1016
|
|
|
1349
|
-
// Check if all processing is complete
|
|
1350
1017
|
if (shouldCleanup(rootElement)) {
|
|
1351
|
-
// One-shot settle before ready: directives whose expressions read
|
|
1352
|
-
// globals provided by component scripts (window helpers) carry no
|
|
1353
|
-
// reactive dependency for those globals, so a directive that rendered
|
|
1354
|
-
// before the defining script settled — boot renders race async imports,
|
|
1355
|
-
// runtime fetch mounts land after initial hydration — would stay empty
|
|
1356
|
-
// forever. All scripts and mounts have settled here; re-render once
|
|
1357
|
-
// against the fully-scripted world. Idempotent: rendered iterations
|
|
1358
|
-
// early-return, value-unchanged conditionals are a no-op.
|
|
1359
1018
|
if (!bootSettleDone) {
|
|
1360
1019
|
bootSettleDone = true;
|
|
1361
1020
|
renderAllIterations(parsedTree, extractPlainValue($), manifest);
|
|
@@ -1364,7 +1023,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1364
1023
|
cleanup(rootElement, debug);
|
|
1365
1024
|
cleanupExecuted = true;
|
|
1366
1025
|
|
|
1367
|
-
// Fire ready hook after cleanup completes
|
|
1368
1026
|
readyFired = true;
|
|
1369
1027
|
hooks.ready.forEach((callback) => {
|
|
1370
1028
|
try {
|
|
@@ -1373,35 +1031,22 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1373
1031
|
console.error('[vibe] Error in ready hook:', error);
|
|
1374
1032
|
}
|
|
1375
1033
|
});
|
|
1376
|
-
// Resolve $.ready promise after all ready callbacks have run
|
|
1377
1034
|
resolveReady();
|
|
1378
1035
|
}
|
|
1379
1036
|
};
|
|
1380
1037
|
|
|
1381
|
-
// Register hook to check for cleanup readiness after each mutation batch
|
|
1382
1038
|
hooks.afterDomMutation.push(checkCleanup);
|
|
1383
1039
|
|
|
1384
|
-
// Async compiled component scripts (imports) finish after boot — unlock the
|
|
1385
|
-
// ready gate and re-check once their state has merged into `$`.
|
|
1386
1040
|
if (compiledScriptsPending) {
|
|
1387
1041
|
compiledScriptsPending.then(() => {
|
|
1388
1042
|
compiledScriptsDone = true;
|
|
1389
|
-
// The initial conditional pass ran before these module scripts settled
|
|
1390
|
-
// — native MPA ordering had page modules evaluate BEFORE vibe booted,
|
|
1391
|
-
// so a gate on a module-provided global (`<!-- if window.isDev -->`)
|
|
1392
|
-
// saw the booted world. A gate like that carries no reactive
|
|
1393
|
-
// dependency to re-check it later, so settle conditionals once against
|
|
1394
|
-
// the post-module world before ready fires. Value-unchanged branches
|
|
1395
|
-
// are a no-op.
|
|
1396
1043
|
settleConditionals(parsedTree, $, manifest);
|
|
1397
1044
|
checkCleanup();
|
|
1398
1045
|
});
|
|
1399
1046
|
}
|
|
1400
1047
|
|
|
1401
|
-
// Process component elements after initialization - MutationObserver will handle hydration
|
|
1402
1048
|
componentProcessingStarted = true;
|
|
1403
1049
|
|
|
1404
|
-
// Pass observer and processMutations to component for sync processing
|
|
1405
1050
|
const componentConfig = {
|
|
1406
1051
|
...config,
|
|
1407
1052
|
_forceSync: true,
|
|
@@ -1412,14 +1057,11 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1412
1057
|
processComponent(
|
|
1413
1058
|
rootElement,
|
|
1414
1059
|
() => {
|
|
1415
|
-
// When all components are done, run cleanup check
|
|
1416
1060
|
checkCleanup();
|
|
1417
1061
|
},
|
|
1418
1062
|
componentConfig,
|
|
1419
1063
|
);
|
|
1420
1064
|
|
|
1421
|
-
// Export hyperspeed manifest globally for compiler extraction and optimizations
|
|
1422
|
-
// Use pre-compiled manifest if available (has compiledBatchFn), otherwise runtime-generated
|
|
1423
1065
|
if (typeof window !== 'undefined') {
|
|
1424
1066
|
const manifestToExport = hyperspeedTree || hyperspeedManifestData;
|
|
1425
1067
|
(window.__vibe ??= {}).manifest = manifestToExport;
|