@ape-egg/vibe 4.0.1 → 4.1.3
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 +22 -71
- package/runtime/component-cache.js +0 -53
- package/runtime/component.js +6 -407
- 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 +11 -130
- package/runtime/index.js +9 -382
- package/runtime/inert.js +18 -0
- package/runtime/iterate.js +7 -595
- 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 +37 -0
- package/spa.js +0 -76
- package/vibe.css +5 -44
package/runtime/index.js
CHANGED
|
@@ -8,9 +8,9 @@ import { unsafe } from './raw-html.js';
|
|
|
8
8
|
import { renderAllIterations, setRenderAllConditionals, releaseOrphanedIterationProps } from './iterate.js';
|
|
9
9
|
import { renderAllConditionals, branchNodeRegistry, managedNodes , settleConditionals} from './conditionals.js';
|
|
10
10
|
import { installScopeResolver } from './loop-scope.js';
|
|
11
|
+
import { isInert } from './inert.js';
|
|
11
12
|
import {
|
|
12
13
|
VERSION,
|
|
13
|
-
NON_REACTIVE_ELEMENTS,
|
|
14
14
|
PHASE_ATTACH,
|
|
15
15
|
PHASE_PARSE,
|
|
16
16
|
PHASE_MANIFEST,
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
PHASE_MUTATE,
|
|
23
23
|
PHASE_HYPERSPEED,
|
|
24
24
|
PHASE_READY,
|
|
25
|
-
DEHYDRATE_CLASS_OR_ATTR,
|
|
26
25
|
} from './constants.js';
|
|
27
26
|
import { processComponent, abortComponentFetch, collectComponentIds, releaseOrphanedComponentState, renderComponentTemplate, executeCompiledComponentScripts } from './component.js';
|
|
28
27
|
import { configureComponentCache, clearComponentCache } from './component-cache.js';
|
|
@@ -38,22 +37,15 @@ import {
|
|
|
38
37
|
restoreMarkersFromManifest,
|
|
39
38
|
} from './pre-compiled-manifest.js';
|
|
40
39
|
|
|
41
|
-
// Wire up cross-module dependency after all modules are loaded
|
|
42
40
|
setRenderAllConditionals(renderAllConditionals);
|
|
43
41
|
|
|
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
42
|
((globalThis.__vibe ??= {}).version = VERSION);
|
|
47
43
|
|
|
48
|
-
// Check if node should be processed by Vibe
|
|
49
44
|
const shouldProcessNode = (node) => {
|
|
50
|
-
// Only process element nodes
|
|
51
45
|
if (node.nodeType !== 1) return false;
|
|
52
46
|
|
|
53
|
-
// Skip nodes already managed by mountBranch or renderIteration
|
|
54
47
|
if (managedNodes.has(node)) return false;
|
|
55
48
|
|
|
56
|
-
// Fast check first: skip nodes without Vibe syntax (cheapest check)
|
|
57
49
|
const html = node.outerHTML;
|
|
58
50
|
if (
|
|
59
51
|
!html.includes('@[') &&
|
|
@@ -64,40 +56,14 @@ const shouldProcessNode = (node) => {
|
|
|
64
56
|
return false;
|
|
65
57
|
}
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
let current = node;
|
|
69
|
-
while (current && current !== document.body) {
|
|
70
|
-
if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
if (
|
|
74
|
-
current.hasAttribute?.(DEHYDRATE_CLASS_OR_ATTR) ||
|
|
75
|
-
current.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)
|
|
76
|
-
) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
current = current.parentElement;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return true;
|
|
59
|
+
return !isInert(node);
|
|
83
60
|
};
|
|
84
61
|
|
|
85
|
-
// Navigate tree using dot notation (handles .children at each level)
|
|
86
62
|
const navigateTree = (tree, path) => {
|
|
87
63
|
if (!path) return tree;
|
|
88
64
|
return path.split('.').filter(k => k).reduce((node, key) => node?.children?.[key], tree);
|
|
89
65
|
};
|
|
90
66
|
|
|
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
67
|
const findNodeByElement = (tree, target) => {
|
|
102
68
|
if (!target) return null;
|
|
103
69
|
const seen = new Set();
|
|
@@ -130,7 +96,6 @@ const findNodeByElement = (tree, target) => {
|
|
|
130
96
|
return walk(tree);
|
|
131
97
|
};
|
|
132
98
|
|
|
133
|
-
// Get or create a node in the tree at the given path
|
|
134
99
|
const ensureNode = (tree, path) => {
|
|
135
100
|
const keys = path.split('.').filter(k => k);
|
|
136
101
|
return keys.reduce((node, key) => {
|
|
@@ -141,7 +106,6 @@ const ensureNode = (tree, path) => {
|
|
|
141
106
|
}, tree);
|
|
142
107
|
};
|
|
143
108
|
|
|
144
|
-
// Recursively add parsed tree nodes to manifest (like createManifest does)
|
|
145
109
|
const addToManifest = (tree, manifest, dotPath) => {
|
|
146
110
|
setManifestEntry(manifest, dotPath, tree.element);
|
|
147
111
|
|
|
@@ -152,16 +116,6 @@ const addToManifest = (tree, manifest, dotPath) => {
|
|
|
152
116
|
}
|
|
153
117
|
};
|
|
154
118
|
|
|
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
119
|
const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) => {
|
|
166
120
|
let hydratedCount = 0;
|
|
167
121
|
let iteratedCount = 0;
|
|
@@ -169,13 +123,6 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
|
|
|
169
123
|
|
|
170
124
|
if (!parsedNode) return { hydratedCount, iteratedCount, evaluatedCount };
|
|
171
125
|
|
|
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
126
|
const oldStateForAffected = isNewNode ? {} : previousState;
|
|
180
127
|
let affectedElements = affected(parsedNode, oldStateForAffected, state);
|
|
181
128
|
if (isNewNode) {
|
|
@@ -186,33 +133,24 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
|
|
|
186
133
|
hydrate(affectedElements, state, manifest, oldStateForAffected);
|
|
187
134
|
}
|
|
188
135
|
|
|
189
|
-
// 3. Conditionals (evaluate <!-- if -->)
|
|
190
136
|
const conditionalCount = renderAllConditionals(parsedNode, state, manifest);
|
|
191
137
|
if (conditionalCount > 0) {
|
|
192
138
|
evaluatedCount = conditionalCount;
|
|
193
139
|
}
|
|
194
140
|
|
|
195
|
-
// 4. Iterate (render <!-- each -->)
|
|
196
141
|
const iterationCount = renderAllIterations(parsedNode, state, manifest);
|
|
197
142
|
if (iterationCount > 0) {
|
|
198
143
|
iteratedCount = iterationCount;
|
|
199
144
|
}
|
|
200
145
|
|
|
201
|
-
// Note: Any DOM changes from steps 3-4 will trigger MutationObserver
|
|
202
|
-
// which will recursively call processMutations for nested content
|
|
203
|
-
|
|
204
146
|
return { hydratedCount, iteratedCount, evaluatedCount };
|
|
205
147
|
};
|
|
206
148
|
|
|
207
|
-
/**
|
|
208
|
-
* Deep clone a tree node (to avoid mutating hyperspeed template)
|
|
209
|
-
*/
|
|
210
149
|
const deepCloneNode = (node) => {
|
|
211
150
|
if (!node || typeof node !== 'object') return node;
|
|
212
151
|
|
|
213
152
|
const cloned = { ...node };
|
|
214
153
|
|
|
215
|
-
// Clone children recursively
|
|
216
154
|
if (node.children && typeof node.children === 'object') {
|
|
217
155
|
cloned.children = {};
|
|
218
156
|
for (const key in node.children) {
|
|
@@ -220,7 +158,6 @@ const deepCloneNode = (node) => {
|
|
|
220
158
|
}
|
|
221
159
|
}
|
|
222
160
|
|
|
223
|
-
// Clone meta if it exists
|
|
224
161
|
if (node.meta && typeof node.meta === 'object') {
|
|
225
162
|
cloned.meta = { ...node.meta };
|
|
226
163
|
if (node.meta.template) {
|
|
@@ -231,7 +168,6 @@ const deepCloneNode = (node) => {
|
|
|
231
168
|
}
|
|
232
169
|
}
|
|
233
170
|
|
|
234
|
-
// Clone runtime if it exists
|
|
235
171
|
if (node.runtime && typeof node.runtime === 'object') {
|
|
236
172
|
cloned.runtime = { ...node.runtime };
|
|
237
173
|
if (Array.isArray(node.runtime.instances)) {
|
|
@@ -242,121 +178,87 @@ const deepCloneNode = (node) => {
|
|
|
242
178
|
return cloned;
|
|
243
179
|
};
|
|
244
180
|
|
|
245
|
-
/**
|
|
246
|
-
* Merge hyperspeed manifest with runtime manifest
|
|
247
|
-
* Hyperspeed provides structure/metadata, runtime populates DOM references
|
|
248
|
-
*/
|
|
249
181
|
const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
250
182
|
if (!hyperspeedTree) return runtimeTree;
|
|
251
183
|
if (!runtimeTree) return hyperspeedTree;
|
|
252
184
|
|
|
253
|
-
// hyperspeedTree is already a clone (done before restoration to avoid mutation)
|
|
254
185
|
const merged = hyperspeedTree;
|
|
255
186
|
|
|
256
|
-
// Helper to recursively augment hyperspeed with runtime data
|
|
257
187
|
const augmentWithRuntime = (mergedNode, runtimeNode) => {
|
|
258
188
|
if (!runtimeNode) return;
|
|
259
189
|
|
|
260
|
-
// Debug: log node types
|
|
261
|
-
// Populate DOM references from runtime
|
|
262
190
|
if (runtimeNode.element) {
|
|
263
191
|
mergedNode.element = runtimeNode.element;
|
|
264
192
|
}
|
|
265
193
|
|
|
266
|
-
// Populate parsed data from runtime
|
|
267
194
|
if (runtimeNode.parsed) {
|
|
268
195
|
mergedNode.parsed = runtimeNode.parsed;
|
|
269
196
|
}
|
|
270
197
|
|
|
271
|
-
// Populate name bindings from runtime (detected after restoration)
|
|
272
198
|
if (runtimeNode.nameBindings) {
|
|
273
199
|
mergedNode.nameBindings = runtimeNode.nameBindings;
|
|
274
200
|
}
|
|
275
201
|
|
|
276
|
-
// Populate attributes from runtime (detected after restoration)
|
|
277
202
|
if (runtimeNode.attributes) {
|
|
278
203
|
mergedNode.attributes = runtimeNode.attributes;
|
|
279
204
|
}
|
|
280
205
|
|
|
281
|
-
// Populate textNode reference from runtime (for text node children)
|
|
282
206
|
if (runtimeNode.textNode) {
|
|
283
207
|
mergedNode.textNode = runtimeNode.textNode;
|
|
284
208
|
}
|
|
285
209
|
|
|
286
|
-
// For iterations: populate all meta and runtime from runtime
|
|
287
210
|
if (mergedNode.type === 'iteration' && runtimeNode.type === 'iteration') {
|
|
288
|
-
// Preserve compiled data from manifest (has compiled batch function)
|
|
289
211
|
const compiledData = mergedNode.compiled;
|
|
290
212
|
|
|
291
|
-
// Copy entire meta object from runtime (all properties needed)
|
|
292
213
|
mergedNode.meta = runtimeNode.meta;
|
|
293
|
-
// Copy runtime object (instances, etc.)
|
|
294
214
|
mergedNode.runtime = runtimeNode.runtime;
|
|
295
215
|
|
|
296
|
-
// Restore compiled data if it was present
|
|
297
216
|
if (compiledData) {
|
|
298
217
|
mergedNode.compiled = compiledData;
|
|
299
218
|
}
|
|
300
219
|
}
|
|
301
220
|
|
|
302
|
-
// For conditionals: populate all meta and runtime from runtime
|
|
303
221
|
if (mergedNode.type === 'conditional' && runtimeNode.type === 'conditional') {
|
|
304
|
-
// Copy entire meta object from runtime (all properties needed)
|
|
305
222
|
mergedNode.meta = runtimeNode.meta;
|
|
306
|
-
// Copy runtime object (activeBranch, activeInstance, templateRemoved)
|
|
307
223
|
mergedNode.runtime = runtimeNode.runtime;
|
|
308
224
|
}
|
|
309
225
|
|
|
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
226
|
if (mergedNode.children) {
|
|
317
227
|
for (const key in mergedNode.children) {
|
|
318
228
|
if (!runtimeNode.children?.[key]) delete mergedNode.children[key];
|
|
319
229
|
}
|
|
320
230
|
}
|
|
321
231
|
|
|
322
|
-
// Augment children recursively
|
|
323
232
|
if (runtimeNode.children) {
|
|
324
233
|
if (!mergedNode.children) mergedNode.children = {};
|
|
325
234
|
|
|
326
235
|
for (const key in runtimeNode.children) {
|
|
327
236
|
const runtimeChild = runtimeNode.children[key];
|
|
328
237
|
|
|
329
|
-
// For iterations: augment existing node, don't replace
|
|
330
238
|
if (runtimeChild.type === 'iteration') {
|
|
331
239
|
const hyperspeedChild = mergedNode.children[key];
|
|
332
240
|
|
|
333
241
|
if (hyperspeedChild) {
|
|
334
|
-
// Preserve compiled data from hyperspeed
|
|
335
242
|
const compiledData = hyperspeedChild.compiled;
|
|
336
243
|
|
|
337
|
-
// Update meta and runtime from runtime node
|
|
338
244
|
mergedNode.children[key].meta = runtimeChild.meta;
|
|
339
245
|
mergedNode.children[key].runtime = runtimeChild.runtime;
|
|
340
246
|
|
|
341
|
-
// Keep compiled data from hyperspeed (it has batchFn)
|
|
342
247
|
if (compiledData) {
|
|
343
248
|
mergedNode.children[key].compiled = compiledData;
|
|
344
249
|
}
|
|
345
250
|
} else {
|
|
346
|
-
// No hyperspeed node, just use runtime
|
|
347
251
|
mergedNode.children[key] = runtimeChild;
|
|
348
252
|
}
|
|
349
253
|
continue;
|
|
350
254
|
}
|
|
351
255
|
|
|
352
|
-
// For conditionals: use runtime node but preserve compiled data
|
|
353
256
|
if (runtimeChild.type === 'conditional') {
|
|
354
257
|
const hyperspeedChild = mergedNode.children[key];
|
|
355
258
|
const compiledData = hyperspeedChild?.compiled;
|
|
356
259
|
|
|
357
260
|
mergedNode.children[key] = runtimeChild;
|
|
358
261
|
|
|
359
|
-
// Restore compiled data if it existed
|
|
360
262
|
if (compiledData) {
|
|
361
263
|
mergedNode.children[key].compiled = compiledData;
|
|
362
264
|
}
|
|
@@ -364,20 +266,16 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
|
364
266
|
}
|
|
365
267
|
|
|
366
268
|
if (mergedNode.children[key]) {
|
|
367
|
-
// Child exists in both - augment it
|
|
368
269
|
augmentWithRuntime(mergedNode.children[key], runtimeNode.children[key]);
|
|
369
270
|
} else {
|
|
370
|
-
// Child only in runtime - add it (dynamic element discovered at runtime)
|
|
371
271
|
mergedNode.children[key] = runtimeNode.children[key];
|
|
372
272
|
}
|
|
373
273
|
}
|
|
374
274
|
}
|
|
375
275
|
};
|
|
376
276
|
|
|
377
|
-
// Augment hyperspeed foundation with runtime data
|
|
378
277
|
augmentWithRuntime(merged, runtimeTree);
|
|
379
278
|
|
|
380
|
-
// Preserve stats from runtime (hyperspeed won't have stats)
|
|
381
279
|
if (runtimeTree.stats) {
|
|
382
280
|
merged.stats = runtimeTree.stats;
|
|
383
281
|
}
|
|
@@ -385,7 +283,6 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
|
385
283
|
return merged;
|
|
386
284
|
};
|
|
387
285
|
|
|
388
|
-
// Store previous state for comparison (needs to be accessible by core loop)
|
|
389
286
|
let previousState = {};
|
|
390
287
|
|
|
391
288
|
const main = (s, config = {}, stringSelector = '') => {
|
|
@@ -393,26 +290,19 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
393
290
|
const verbose = !!config?.verbose;
|
|
394
291
|
((globalThis.__vibe ??= {}).debug = debug);
|
|
395
292
|
|
|
396
|
-
// Enable/disable the component template cache from config (`{ noCache }`).
|
|
397
293
|
configureComponentCache(config);
|
|
398
294
|
|
|
399
|
-
// Expose the global `$scope` resolver used by loop-scoped `on*` handlers.
|
|
400
295
|
installScopeResolver();
|
|
401
296
|
|
|
402
|
-
// Detect if running in compiler's headless browser
|
|
403
|
-
// When true: skip cleanup to preserve [vibe] attribute in compiled HTML
|
|
404
297
|
const isCompiling = typeof window !== 'undefined' && window.__vibe?.compiling === true;
|
|
405
298
|
|
|
406
|
-
// Reset previous state for each new instance
|
|
407
299
|
previousState = {};
|
|
408
300
|
|
|
409
|
-
// Use page-specific hyperspeed manifest (detected at module load)
|
|
410
301
|
const hyperspeedTree = hyperspeedManifest;
|
|
411
302
|
|
|
412
303
|
let rootElement = document.body;
|
|
413
304
|
|
|
414
305
|
if (stringSelector) {
|
|
415
|
-
// Find element(s) with the specified attribute
|
|
416
306
|
const elements = document.querySelectorAll(`${stringSelector}`);
|
|
417
307
|
if (elements?.[0]) {
|
|
418
308
|
rootElement = elements[0];
|
|
@@ -421,16 +311,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
421
311
|
|
|
422
312
|
debugLog(PHASE_ATTACH, `Vibe attached to`, debug, 0, rootElement);
|
|
423
313
|
|
|
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
314
|
let hyperspeedSubtree = null;
|
|
430
315
|
if (hyperspeedTree) {
|
|
431
316
|
const manifestName = hyperspeedPath ? hyperspeedPath.split('/').pop() : 'manifest.js';
|
|
432
317
|
|
|
433
|
-
// Count compiled features
|
|
434
318
|
const countCompiledIterations = (tree) => {
|
|
435
319
|
let count = 0;
|
|
436
320
|
if (tree.type === 'iteration' && tree.compiled?.iterations?.batchFn) count++;
|
|
@@ -444,7 +328,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
444
328
|
|
|
445
329
|
const compiledIterationCount = countCompiledIterations(hyperspeedTree);
|
|
446
330
|
|
|
447
|
-
// Log features that are enabled
|
|
448
331
|
debugLog(PHASE_HYPERSPEED, `Loaded ${manifestName}, page is pre-compiled`, debug);
|
|
449
332
|
|
|
450
333
|
if (compiledIterationCount > 0) {
|
|
@@ -461,17 +344,13 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
461
344
|
);
|
|
462
345
|
}
|
|
463
346
|
|
|
464
|
-
// Find matching subtree by DOM path (not just tag name)
|
|
465
|
-
// Build path from document to rootElement
|
|
466
347
|
const buildDomPath = (element) => {
|
|
467
348
|
const path = [];
|
|
468
349
|
let current = element;
|
|
469
350
|
|
|
470
|
-
// Walk up to document itself (include html in the path)
|
|
471
351
|
while (current && current.parentNode && current !== document) {
|
|
472
352
|
const parent = current.parentNode;
|
|
473
353
|
|
|
474
|
-
// Skip document node itself
|
|
475
354
|
if (parent === document) {
|
|
476
355
|
const siblings = Array.from(document.childNodes);
|
|
477
356
|
const index = siblings.indexOf(current);
|
|
@@ -491,19 +370,16 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
491
370
|
return path;
|
|
492
371
|
};
|
|
493
372
|
|
|
494
|
-
// Walk manifest tree following DOM path
|
|
495
373
|
const findManifestNodeByPath = (manifest, path) => {
|
|
496
374
|
let current = manifest;
|
|
497
375
|
|
|
498
376
|
for (const { tag, index } of path) {
|
|
499
377
|
if (!current || !current.children) return null;
|
|
500
378
|
|
|
501
|
-
// Look for matching child by tag_index pattern
|
|
502
379
|
const key = `${tag}_${index}`;
|
|
503
380
|
if (current.children[key]) {
|
|
504
381
|
current = current.children[key];
|
|
505
382
|
} else {
|
|
506
|
-
// Fallback: search all children for matching tag at this level
|
|
507
383
|
let found = false;
|
|
508
384
|
for (const childKey in current.children) {
|
|
509
385
|
if (childKey.startsWith(tag + '_')) {
|
|
@@ -523,36 +399,27 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
523
399
|
let subtreeFromManifest = findManifestNodeByPath(hyperspeedTree, domPath);
|
|
524
400
|
|
|
525
401
|
if (!subtreeFromManifest) {
|
|
526
|
-
// Fallback to root if path matching fails
|
|
527
402
|
subtreeFromManifest = hyperspeedTree;
|
|
528
403
|
}
|
|
529
404
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
hyperspeedSubtree = deepCloneNode(subtreeFromManifest); // For merging
|
|
533
|
-
const cloneForRestoration = deepCloneNode(subtreeFromManifest); // For restoration
|
|
405
|
+
hyperspeedSubtree = deepCloneNode(subtreeFromManifest);
|
|
406
|
+
const cloneForRestoration = deepCloneNode(subtreeFromManifest);
|
|
534
407
|
|
|
535
408
|
restoreMarkersFromManifest(rootElement, cloneForRestoration, hyperspeedTree);
|
|
536
409
|
}
|
|
537
410
|
|
|
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
411
|
rootElement.querySelectorAll('component[src], div.component[src]').forEach(el => {
|
|
542
412
|
el._vibeSlotContent = el.innerHTML;
|
|
543
413
|
});
|
|
544
414
|
|
|
545
|
-
// Runtime parses DOM (which now has restored markers if hyperspeed was used)
|
|
546
415
|
let parsedTree = parse(rootElement);
|
|
547
416
|
|
|
548
|
-
// Merge with hyperspeed if available (hyperspeed as foundation, runtime augments)
|
|
549
417
|
if (hyperspeedSubtree) {
|
|
550
418
|
parsedTree = mergeManifests(hyperspeedSubtree, parsedTree);
|
|
551
419
|
}
|
|
552
420
|
|
|
553
421
|
let manifest = createManifest(parsedTree);
|
|
554
422
|
|
|
555
|
-
// Build hyperspeed manifest (before hydration, extract markers from parsed strings)
|
|
556
423
|
const hyperspeedManifestData = buildHyperspeedManifest(parsedTree);
|
|
557
424
|
|
|
558
425
|
const manifestEntries = Object.entries(manifest);
|
|
@@ -582,7 +449,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
582
449
|
debug,
|
|
583
450
|
);
|
|
584
451
|
|
|
585
|
-
// Lifecycle hooks that users can subscribe to
|
|
586
452
|
const hooks = {
|
|
587
453
|
afterUpdate: [],
|
|
588
454
|
afterDomMutation: [],
|
|
@@ -590,25 +456,14 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
590
456
|
unmount: [],
|
|
591
457
|
};
|
|
592
458
|
|
|
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
459
|
let readyFired = false;
|
|
598
460
|
|
|
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
461
|
if (typeof window !== 'undefined') {
|
|
605
462
|
window.addEventListener('pagehide', () => {
|
|
606
463
|
hooks.unmount.forEach((callback) => callback());
|
|
607
464
|
});
|
|
608
465
|
}
|
|
609
466
|
|
|
610
|
-
// Extract plain values from proxy (removes proxy wrappers)
|
|
611
|
-
// Optimized: indexed loops, Object.keys (no prototype walk), inline primitive check
|
|
612
467
|
const extractPlainValue = (obj) => {
|
|
613
468
|
if (obj === null || typeof obj !== 'object') return obj;
|
|
614
469
|
if (Array.isArray(obj)) {
|
|
@@ -631,14 +486,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
631
486
|
return plain;
|
|
632
487
|
};
|
|
633
488
|
|
|
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
489
|
const patchWithObserverPaused = (patch) => {
|
|
641
|
-
// Capture pending mutations before disconnecting (takeRecords clears the queue)
|
|
642
490
|
let pendingMutations = [];
|
|
643
491
|
if (observer) {
|
|
644
492
|
pendingMutations = observer.takeRecords();
|
|
@@ -655,14 +503,11 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
655
503
|
subtree: true,
|
|
656
504
|
});
|
|
657
505
|
|
|
658
|
-
// Process mutations that were pending before we disconnected
|
|
659
506
|
if (pendingMutations.length > 0 && processMutations) {
|
|
660
507
|
processMutations(pendingMutations);
|
|
661
508
|
}
|
|
662
509
|
}
|
|
663
510
|
|
|
664
|
-
// The patch may have mounted new DOM while the observer was disconnected.
|
|
665
|
-
// Scan for unresolved <component src=""> elements that need fetching.
|
|
666
511
|
if (componentProcessingStarted) {
|
|
667
512
|
const componentConfig = {
|
|
668
513
|
...config,
|
|
@@ -675,17 +520,8 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
675
520
|
};
|
|
676
521
|
|
|
677
522
|
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
523
|
const currentState = { ...previousState };
|
|
682
524
|
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
525
|
if (prop in $) {
|
|
690
526
|
currentState[prop] = extractPlainValue($[prop]);
|
|
691
527
|
} else {
|
|
@@ -693,11 +529,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
693
529
|
}
|
|
694
530
|
}
|
|
695
531
|
|
|
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
532
|
const dirty = beginFlush(changedProps);
|
|
702
533
|
if (dirty.size > 0) {
|
|
703
534
|
debugLog(PHASE_UPDATE, 'state changed', debug);
|
|
@@ -706,30 +537,13 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
706
537
|
});
|
|
707
538
|
}
|
|
708
539
|
|
|
709
|
-
// Store previous state for hooks (currentState is already plain, no need to clone)
|
|
710
540
|
const prev = previousState;
|
|
711
541
|
previousState = currentState;
|
|
712
542
|
hooks.afterUpdate.forEach((callback) => callback(currentState, prev));
|
|
713
543
|
});
|
|
714
544
|
|
|
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
545
|
const lateReady = [];
|
|
729
546
|
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
547
|
const mountsSettled = () =>
|
|
734
548
|
!document.querySelector('[vibe-staged], [vibe-fouc]') && shouldCleanup(rootElement);
|
|
735
549
|
const flushLateReady = () => {
|
|
@@ -770,46 +584,23 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
770
584
|
configurable: true,
|
|
771
585
|
});
|
|
772
586
|
|
|
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
587
|
let resolveReady;
|
|
777
588
|
Object.defineProperty($, 'ready', {
|
|
778
589
|
value: new Promise((resolve) => { resolveReady = resolve; }),
|
|
779
590
|
enumerable: false,
|
|
780
591
|
});
|
|
781
592
|
|
|
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
593
|
Object.defineProperty($, 'reconcile', {
|
|
785
594
|
value: reconcile,
|
|
786
595
|
enumerable: false,
|
|
787
596
|
});
|
|
788
597
|
|
|
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
598
|
Object.defineProperty($, '_register', {
|
|
798
599
|
value: (componentId, componentState) => {
|
|
799
600
|
if (componentId in $) {
|
|
800
601
|
$[componentId] = componentState;
|
|
801
602
|
return componentId;
|
|
802
603
|
}
|
|
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
604
|
silentSet($, componentId, componentState);
|
|
814
605
|
return componentId;
|
|
815
606
|
},
|
|
@@ -817,80 +608,35 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
817
608
|
configurable: true,
|
|
818
609
|
});
|
|
819
610
|
|
|
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
611
|
Object.defineProperty($, 'unsafe', {
|
|
825
612
|
value: unsafe,
|
|
826
613
|
enumerable: false,
|
|
827
614
|
configurable: true,
|
|
828
615
|
});
|
|
829
616
|
|
|
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
617
|
Object.defineProperty($, '_renderComponent', {
|
|
836
618
|
value: renderComponentTemplate,
|
|
837
619
|
enumerable: false,
|
|
838
620
|
});
|
|
839
621
|
|
|
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
622
|
Object.defineProperty($, 'clearComponentCache', {
|
|
846
623
|
value: clearComponentCache,
|
|
847
624
|
enumerable: false,
|
|
848
625
|
configurable: true,
|
|
849
626
|
});
|
|
850
627
|
|
|
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
628
|
Object.defineProperty(manifest, '__live', { value: $, enumerable: false, configurable: true });
|
|
857
629
|
|
|
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
630
|
Object.defineProperty(manifest, '__tree', { value: parsedTree, enumerable: false, configurable: true });
|
|
865
631
|
|
|
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
632
|
setRootState($);
|
|
870
633
|
|
|
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
634
|
if (typeof window !== 'undefined') window.$ = $;
|
|
880
635
|
|
|
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
636
|
let compiledScriptsDone = true;
|
|
890
637
|
const compiledScriptsPending = executeCompiledComponentScripts();
|
|
891
638
|
if (compiledScriptsPending) compiledScriptsDone = false;
|
|
892
639
|
|
|
893
|
-
// Initial hydration - pass plain values so iteration can do reference comparison
|
|
894
640
|
const initialState = extractPlainValue($);
|
|
895
641
|
const affectedElements = affected(parsedTree, initialState, initialState);
|
|
896
642
|
|
|
@@ -907,12 +653,8 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
907
653
|
debug,
|
|
908
654
|
);
|
|
909
655
|
|
|
910
|
-
// Hydrate with proxy $ so DOM bindings work
|
|
911
|
-
// Pass initialState as oldState for iterations (won't actually update, just initial render)
|
|
912
656
|
hydrate(affectedElements, $, manifest, initialState);
|
|
913
657
|
|
|
914
|
-
// Render all iterations and conditionals after initial hydration
|
|
915
|
-
// Use initialState (plain values) for iteration rendering so reference comparison works
|
|
916
658
|
const iterationCount = renderAllIterations(parsedTree, initialState, manifest);
|
|
917
659
|
if (iterationCount > 0)
|
|
918
660
|
debugLog(
|
|
@@ -943,35 +685,28 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
943
685
|
debug,
|
|
944
686
|
);
|
|
945
687
|
|
|
946
|
-
// After all initial rendering, capture a clean snapshot for comparison
|
|
947
688
|
previousState = extractPlainValue($);
|
|
948
689
|
|
|
949
|
-
// Observer reference and processMutations
|
|
950
690
|
let observer = null;
|
|
951
691
|
let processMutations = null;
|
|
952
692
|
|
|
953
|
-
// Define observer callback as named function so we can call it manually for pending mutations
|
|
954
693
|
processMutations = (mutations) => {
|
|
955
|
-
// Early exit if no mutations to process (common case)
|
|
956
694
|
if (mutations.length === 0) return;
|
|
957
695
|
|
|
958
696
|
const manifestSizeBefore = debug ? Object.keys(manifest).length : 0;
|
|
959
697
|
let hadChanges = false;
|
|
960
|
-
let parsedParents = null;
|
|
698
|
+
let parsedParents = null;
|
|
961
699
|
let addedElements = 0;
|
|
962
700
|
let addedNodes = 0;
|
|
963
701
|
let removedElements = 0;
|
|
964
702
|
let removedNodes = 0;
|
|
965
|
-
let singleElement = null;
|
|
966
|
-
let totalSkipped = 0;
|
|
703
|
+
let singleElement = null;
|
|
704
|
+
let totalSkipped = 0;
|
|
967
705
|
let hydratedCount = 0;
|
|
968
706
|
let iteratedCount = 0;
|
|
969
707
|
let evaluatedCount = 0;
|
|
970
|
-
let addedElementsList = [];
|
|
708
|
+
let addedElementsList = [];
|
|
971
709
|
|
|
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
710
|
const removedComponentIds = new Set();
|
|
976
711
|
mutations.forEach(({ removedNodes: removedNodesList }) => {
|
|
977
712
|
removedNodesList.forEach((node) => collectComponentIds(node, removedComponentIds));
|
|
@@ -979,21 +714,12 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
979
714
|
|
|
980
715
|
mutations.forEach(({ addedNodes: addedNodesList, removedNodes: removedNodesList, target }) => {
|
|
981
716
|
removedNodesList.forEach((node) => {
|
|
982
|
-
// If this is a component element with pending fetch, abort it
|
|
983
717
|
if (node.nodeName === 'COMPONENT') {
|
|
984
718
|
abortComponentFetch(node);
|
|
985
719
|
}
|
|
986
720
|
|
|
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
721
|
const branchRef = branchNodeRegistry.get(node);
|
|
991
722
|
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
723
|
const replacement =
|
|
998
724
|
Array.from(addedNodesList).find(n => n.parentNode === target) ||
|
|
999
725
|
(node._vibeReplacedBy?.isConnected ? node._vibeReplacedBy : null);
|
|
@@ -1006,30 +732,19 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1006
732
|
|
|
1007
733
|
const dotAnnotation = manifestPathOf(manifest, node);
|
|
1008
734
|
|
|
1009
|
-
// Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
|
|
1010
735
|
if (dotAnnotation === null) return;
|
|
1011
736
|
|
|
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
737
|
removeManifestSubtree(manifest, dotAnnotation);
|
|
1017
738
|
|
|
1018
739
|
const dotPath = dotAnnotation.split('.');
|
|
1019
740
|
const name = dotPath.pop();
|
|
1020
741
|
const parentDotAnnotation = dotPath.join('.');
|
|
1021
742
|
|
|
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
743
|
const picked =
|
|
1027
744
|
navigateTree(parsedTree, parentDotAnnotation) || findNodeByElement(parsedTree, target);
|
|
1028
745
|
|
|
1029
|
-
// If we can't navigate to the parent, skip
|
|
1030
746
|
if (!picked || !picked.element) return;
|
|
1031
747
|
|
|
1032
|
-
// Update parent's parsed HTML (only once per parent)
|
|
1033
748
|
if (!parsedParents) parsedParents = new Set();
|
|
1034
749
|
if (!parsedParents.has(picked)) {
|
|
1035
750
|
const { parsed } = parse(picked.element);
|
|
@@ -1037,10 +752,8 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1037
752
|
parsedParents.add(picked);
|
|
1038
753
|
}
|
|
1039
754
|
|
|
1040
|
-
// Remove the node from parent's children
|
|
1041
755
|
delete picked.children[name];
|
|
1042
756
|
|
|
1043
|
-
// Count elements vs nodes separately
|
|
1044
757
|
if (node.nodeName.startsWith('#')) {
|
|
1045
758
|
removedNodes++;
|
|
1046
759
|
} else {
|
|
@@ -1051,16 +764,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1051
764
|
});
|
|
1052
765
|
|
|
1053
766
|
addedNodesList.forEach((node) => {
|
|
1054
|
-
// Skip nodes that aren't element nodes, are non-reactive, or lack Vibe syntax
|
|
1055
767
|
if (!shouldProcessNode(node)) {
|
|
1056
768
|
return;
|
|
1057
769
|
}
|
|
1058
770
|
|
|
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
771
|
if (node.nodeType === 1) {
|
|
1065
772
|
node.querySelectorAll('component[src], div.component[src]').forEach((el) => {
|
|
1066
773
|
if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
|
|
@@ -1069,23 +776,13 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1069
776
|
|
|
1070
777
|
const dotAnnotation = manifestPathOf(manifest, target);
|
|
1071
778
|
|
|
1072
|
-
// Parse the newly added node
|
|
1073
779
|
const parsedNode = parse(node);
|
|
1074
780
|
|
|
1075
|
-
// Accumulate skipped stats
|
|
1076
781
|
if (parsedNode.stats?.skipped) {
|
|
1077
782
|
totalSkipped += parsedNode.stats.skipped;
|
|
1078
783
|
}
|
|
1079
784
|
|
|
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
785
|
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
786
|
const picked =
|
|
1090
787
|
navigateTree(parsedTree, dotAnnotation) || findNodeByElement(parsedTree, target);
|
|
1091
788
|
|
|
@@ -1094,15 +791,12 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1094
791
|
picked.element = target;
|
|
1095
792
|
}
|
|
1096
793
|
|
|
1097
|
-
// Parse the newly added node (use monotonically increasing counter for deterministic key)
|
|
1098
|
-
// Initialize counter if it doesn't exist
|
|
1099
794
|
if (!picked._nextChildIndex) {
|
|
1100
795
|
picked._nextChildIndex = Object.keys(picked.children).length;
|
|
1101
796
|
}
|
|
1102
797
|
const name = `${node.nodeName.toLowerCase()}_${picked._nextChildIndex}`;
|
|
1103
|
-
picked._nextChildIndex++;
|
|
798
|
+
picked._nextChildIndex++;
|
|
1104
799
|
|
|
1105
|
-
// Update parent's parsed HTML (only once per parent)
|
|
1106
800
|
if (!parsedParents) parsedParents = new Set();
|
|
1107
801
|
if (!parsedParents.has(picked)) {
|
|
1108
802
|
const { parsed } = parse(picked.element);
|
|
@@ -1110,34 +804,21 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1110
804
|
parsedParents.add(picked);
|
|
1111
805
|
}
|
|
1112
806
|
|
|
1113
|
-
// Add the parsed node to parent's children
|
|
1114
807
|
picked.children[name] = parsedNode;
|
|
1115
808
|
|
|
1116
|
-
// Recursively add node and all descendants to manifest
|
|
1117
809
|
addToManifest(parsedNode, manifest, `${dotAnnotation}.${name}`);
|
|
1118
810
|
}
|
|
1119
811
|
}
|
|
1120
812
|
|
|
1121
|
-
// Run core loop for the new node (parse already done, hydrate → conditionals → iterate)
|
|
1122
813
|
const counts = processCoreLoop(node, parsedNode, $, manifest, true, debug);
|
|
1123
814
|
hydratedCount += counts.hydratedCount;
|
|
1124
815
|
iteratedCount += counts.iteratedCount;
|
|
1125
816
|
evaluatedCount += counts.evaluatedCount;
|
|
1126
817
|
|
|
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
818
|
if (node.nodeType === 1 && node._vibeIterPropExprs) {
|
|
1137
819
|
node._vibeIterTree = parsedNode;
|
|
1138
820
|
}
|
|
1139
821
|
|
|
1140
|
-
// Count elements vs nodes separately
|
|
1141
822
|
if (node.nodeName.startsWith('#')) {
|
|
1142
823
|
addedNodes++;
|
|
1143
824
|
} else {
|
|
@@ -1149,23 +830,18 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1149
830
|
});
|
|
1150
831
|
});
|
|
1151
832
|
|
|
1152
|
-
// CLEANUP OF CURRENT STATE
|
|
1153
833
|
releaseOrphanedComponentState(removedComponentIds);
|
|
1154
834
|
let hadRemovals = false;
|
|
1155
835
|
mutations.forEach(({ removedNodes: removedNodesList }) => {
|
|
1156
836
|
if (removedNodesList.length > 0) hadRemovals = true;
|
|
1157
837
|
releaseOrphanedIterationProps(removedNodesList);
|
|
1158
838
|
});
|
|
1159
|
-
// Subscribers anchored in the removed subtrees are dead — drop them from
|
|
1160
|
-
// the reverse index (observed removals: page swaps, reconcile, app code).
|
|
1161
839
|
if (hadRemovals) pruneDisconnected();
|
|
1162
840
|
|
|
1163
|
-
// Fire hooks once after all mutations are processed (not per-node)
|
|
1164
841
|
if (hadChanges) {
|
|
1165
842
|
if (addedElements > 0 || addedNodes > 0 || removedElements > 0 || removedNodes > 0) {
|
|
1166
843
|
const segments = [];
|
|
1167
844
|
|
|
1168
|
-
// Added
|
|
1169
845
|
if (addedElements > 0) {
|
|
1170
846
|
segments.push({ text: `+${addedElements}`, color: 'green' });
|
|
1171
847
|
segments.push({
|
|
@@ -1179,7 +855,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1179
855
|
segments.push({ text: ` ${addedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
|
|
1180
856
|
}
|
|
1181
857
|
|
|
1182
|
-
// Removed
|
|
1183
858
|
if (removedElements > 0) {
|
|
1184
859
|
if (addedElements > 0 || addedNodes > 0) segments.push({ text: ', ', colored: false });
|
|
1185
860
|
segments.push({ text: `-${removedElements}`, color: 'red' });
|
|
@@ -1195,25 +870,15 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1195
870
|
segments.push({ text: ` ${removedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
|
|
1196
871
|
}
|
|
1197
872
|
|
|
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
873
|
debugLog(PHASE_MUTATE, segments, debug);
|
|
1203
874
|
|
|
1204
|
-
// Verbose: log the added element (or topmost parent if multiple)
|
|
1205
875
|
if (verbose && addedElementsList.length > 0) {
|
|
1206
|
-
// Find the topmost parent among added elements
|
|
1207
876
|
const topmostParent = addedElementsList.find((el) => {
|
|
1208
|
-
// Check if this element is NOT a descendant of any other element in the list
|
|
1209
877
|
return !addedElementsList.some((other) => other !== el && other.contains(el));
|
|
1210
878
|
});
|
|
1211
879
|
debugLog(PHASE_MUTATE, '', debug, 0, topmostParent || addedElementsList[0]);
|
|
1212
880
|
}
|
|
1213
881
|
|
|
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
882
|
const manifestSizeAfter = debug ? Object.keys(manifest).length : manifestSizeBefore;
|
|
1218
883
|
const totalNodes = manifestSizeAfter - manifestSizeBefore;
|
|
1219
884
|
|
|
@@ -1240,7 +905,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1240
905
|
|
|
1241
906
|
debugLog(PHASE_PARSE, parseSegments, debug);
|
|
1242
907
|
|
|
1243
|
-
// Log hydration after parse
|
|
1244
908
|
if (hydratedCount > 0) {
|
|
1245
909
|
debugLog(
|
|
1246
910
|
PHASE_HYDRATE,
|
|
@@ -1256,7 +920,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1256
920
|
);
|
|
1257
921
|
}
|
|
1258
922
|
|
|
1259
|
-
// Log iterations after hydration
|
|
1260
923
|
if (iteratedCount > 0) {
|
|
1261
924
|
debugLog(
|
|
1262
925
|
PHASE_ITERATE,
|
|
@@ -1272,7 +935,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1272
935
|
);
|
|
1273
936
|
}
|
|
1274
937
|
|
|
1275
|
-
// Log conditionals after iterations
|
|
1276
938
|
if (evaluatedCount > 0) {
|
|
1277
939
|
debugLog(
|
|
1278
940
|
PHASE_CONDITION,
|
|
@@ -1291,12 +953,8 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1291
953
|
}
|
|
1292
954
|
}
|
|
1293
955
|
|
|
1294
|
-
// Always call hooks, even if no changes detected (cleanup needs to check)
|
|
1295
956
|
hooks.afterDomMutation.forEach((callback) => callback());
|
|
1296
957
|
|
|
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
958
|
if (componentProcessingStarted) {
|
|
1301
959
|
const componentConfig = {
|
|
1302
960
|
...config,
|
|
@@ -1307,7 +965,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1307
965
|
processComponent(
|
|
1308
966
|
rootElement,
|
|
1309
967
|
() => {
|
|
1310
|
-
// When all components are done, run cleanup check
|
|
1311
968
|
checkCleanup();
|
|
1312
969
|
},
|
|
1313
970
|
componentConfig,
|
|
@@ -1328,34 +985,21 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1328
985
|
|
|
1329
986
|
debugLog(PHASE_OBSERVE, `MutationObserver listening to DOM changes`, debug);
|
|
1330
987
|
|
|
1331
|
-
// Track cleanup state
|
|
1332
988
|
let componentProcessingStarted = false;
|
|
1333
989
|
let cleanupExecuted = false;
|
|
1334
990
|
let bootSettleDone = false;
|
|
1335
991
|
|
|
1336
|
-
// Check if cleanup should run
|
|
1337
992
|
const checkCleanup = () => {
|
|
1338
993
|
if (cleanupExecuted || isCompiling || !compiledScriptsDone) return;
|
|
1339
994
|
|
|
1340
|
-
// Check for pending mutations first
|
|
1341
995
|
const pendingMutations = observer ? observer.takeRecords() : [];
|
|
1342
996
|
|
|
1343
997
|
if (pendingMutations.length > 0) {
|
|
1344
|
-
// More mutations to process
|
|
1345
998
|
processMutations(pendingMutations);
|
|
1346
999
|
return;
|
|
1347
1000
|
}
|
|
1348
1001
|
|
|
1349
|
-
// Check if all processing is complete
|
|
1350
1002
|
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
1003
|
if (!bootSettleDone) {
|
|
1360
1004
|
bootSettleDone = true;
|
|
1361
1005
|
renderAllIterations(parsedTree, extractPlainValue($), manifest);
|
|
@@ -1364,7 +1008,6 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1364
1008
|
cleanup(rootElement, debug);
|
|
1365
1009
|
cleanupExecuted = true;
|
|
1366
1010
|
|
|
1367
|
-
// Fire ready hook after cleanup completes
|
|
1368
1011
|
readyFired = true;
|
|
1369
1012
|
hooks.ready.forEach((callback) => {
|
|
1370
1013
|
try {
|
|
@@ -1373,35 +1016,22 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1373
1016
|
console.error('[vibe] Error in ready hook:', error);
|
|
1374
1017
|
}
|
|
1375
1018
|
});
|
|
1376
|
-
// Resolve $.ready promise after all ready callbacks have run
|
|
1377
1019
|
resolveReady();
|
|
1378
1020
|
}
|
|
1379
1021
|
};
|
|
1380
1022
|
|
|
1381
|
-
// Register hook to check for cleanup readiness after each mutation batch
|
|
1382
1023
|
hooks.afterDomMutation.push(checkCleanup);
|
|
1383
1024
|
|
|
1384
|
-
// Async compiled component scripts (imports) finish after boot — unlock the
|
|
1385
|
-
// ready gate and re-check once their state has merged into `$`.
|
|
1386
1025
|
if (compiledScriptsPending) {
|
|
1387
1026
|
compiledScriptsPending.then(() => {
|
|
1388
1027
|
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
1028
|
settleConditionals(parsedTree, $, manifest);
|
|
1397
1029
|
checkCleanup();
|
|
1398
1030
|
});
|
|
1399
1031
|
}
|
|
1400
1032
|
|
|
1401
|
-
// Process component elements after initialization - MutationObserver will handle hydration
|
|
1402
1033
|
componentProcessingStarted = true;
|
|
1403
1034
|
|
|
1404
|
-
// Pass observer and processMutations to component for sync processing
|
|
1405
1035
|
const componentConfig = {
|
|
1406
1036
|
...config,
|
|
1407
1037
|
_forceSync: true,
|
|
@@ -1412,14 +1042,11 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1412
1042
|
processComponent(
|
|
1413
1043
|
rootElement,
|
|
1414
1044
|
() => {
|
|
1415
|
-
// When all components are done, run cleanup check
|
|
1416
1045
|
checkCleanup();
|
|
1417
1046
|
},
|
|
1418
1047
|
componentConfig,
|
|
1419
1048
|
);
|
|
1420
1049
|
|
|
1421
|
-
// Export hyperspeed manifest globally for compiler extraction and optimizations
|
|
1422
|
-
// Use pre-compiled manifest if available (has compiledBatchFn), otherwise runtime-generated
|
|
1423
1050
|
if (typeof window !== 'undefined') {
|
|
1424
1051
|
const manifestToExport = hyperspeedTree || hyperspeedManifestData;
|
|
1425
1052
|
(window.__vibe ??= {}).manifest = manifestToExport;
|