@ape-egg/vibe 1.0.3 → 1.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/CHANGELOG.md +121 -0
- package/README.md +228 -23
- package/compiler/bin/vibe-compile.js +109 -0
- package/compiler/native/.gitkeep +0 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +1885 -0
- package/compiler/src/Cargo.toml +29 -0
- package/compiler/src/compiler/compile.rs +1209 -0
- package/compiler/src/compiler/mod.rs +5 -0
- package/compiler/src/config.rs +184 -0
- package/compiler/src/main.rs +284 -0
- package/compiler/src/parser/element.rs +96 -0
- package/compiler/src/parser/html.rs +339 -0
- package/compiler/src/parser/mod.rs +8 -0
- package/index.js +2 -233
- package/package.json +26 -3
- package/{affected.js → runtime/affected.js} +66 -14
- package/runtime/cleanup.js +59 -0
- package/runtime/component.js +116 -0
- package/{conditionals.js → runtime/conditionals.js} +27 -23
- package/{constants.js → runtime/constants.js} +23 -3
- package/runtime/debug.js +91 -0
- package/{hydrate.js → runtime/hydrate.js} +58 -20
- package/runtime/index.js +614 -0
- package/{iterate.js → runtime/iterate.js} +53 -45
- package/{iteration-utils.js → runtime/iteration-utils.js} +11 -1
- package/{parse.js → runtime/parse.js} +37 -7
- package/runtime/state.js +52 -0
- package/{utils.js → runtime/utils.js} +13 -0
- package/llms.txt +0 -279
- package/state.js +0 -26
- /package/{_vibe-compiled-iteration-batch.js → runtime/_vibe-compiled-iteration-batch.js} +0 -0
- /package/{link.js → runtime/manifest.js} +0 -0
- /package/{vibe.css → runtime/vibe.css} +0 -0
package/runtime/index.js
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import state from './state.js';
|
|
2
|
+
import parse from './parse.js';
|
|
3
|
+
import createManifest from './manifest.js';
|
|
4
|
+
import hydrate, { setPreviousState } from './hydrate.js';
|
|
5
|
+
import affected from './affected.js';
|
|
6
|
+
import { deepMerge, hash } from './utils.js';
|
|
7
|
+
import { renderAllIterations, setRenderAllConditionals } from './iterate.js';
|
|
8
|
+
import { renderAllConditionals } from './conditionals.js';
|
|
9
|
+
import {
|
|
10
|
+
NON_REACTIVE_ELEMENTS,
|
|
11
|
+
PHASE_ATTACH,
|
|
12
|
+
PHASE_PARSE,
|
|
13
|
+
PHASE_MANIFEST,
|
|
14
|
+
PHASE_HYDRATE,
|
|
15
|
+
PHASE_ITERATE,
|
|
16
|
+
PHASE_CONDITION,
|
|
17
|
+
PHASE_OBSERVE,
|
|
18
|
+
PHASE_UPDATE,
|
|
19
|
+
PHASE_MUTATE,
|
|
20
|
+
PHASE_COMPLETE,
|
|
21
|
+
} from './constants.js';
|
|
22
|
+
import { processComponent, abortComponentFetch } from './component.js';
|
|
23
|
+
import { debugLog } from './debug.js';
|
|
24
|
+
import { shouldCleanup, cleanup } from './cleanup.js';
|
|
25
|
+
|
|
26
|
+
// Wire up cross-module dependency after all modules are loaded
|
|
27
|
+
setRenderAllConditionals(renderAllConditionals);
|
|
28
|
+
|
|
29
|
+
// Check if node itself or any ancestor is non-reactive or dehydrated
|
|
30
|
+
const isNonReactiveOrInside = (node) => {
|
|
31
|
+
let current = node;
|
|
32
|
+
while (current && current !== document.body) {
|
|
33
|
+
if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (current.hasAttribute?.('dehydrate')) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
current = current.parentElement;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Navigate tree using dot notation (handles .children at each level)
|
|
45
|
+
const navigateTree = (tree, path) => {
|
|
46
|
+
if (!path) return tree;
|
|
47
|
+
return path.split('.').reduce((node, key) => node?.children?.[key], tree);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Get or create a node in the tree at the given path
|
|
51
|
+
const ensureNode = (tree, path) => {
|
|
52
|
+
const keys = path.split('.');
|
|
53
|
+
return keys.reduce((node, key) => {
|
|
54
|
+
if (!node.children[key]) {
|
|
55
|
+
node.children[key] = { children: {} };
|
|
56
|
+
}
|
|
57
|
+
return node.children[key];
|
|
58
|
+
}, tree);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Recursively add parsed tree nodes to manifest (like createManifest does)
|
|
62
|
+
const addToManifest = (tree, manifest, dotPath) => {
|
|
63
|
+
manifest[dotPath] = tree.element;
|
|
64
|
+
|
|
65
|
+
if (tree.children && Object.keys(tree.children).length > 0) {
|
|
66
|
+
Object.keys(tree.children).forEach((childKey) => {
|
|
67
|
+
addToManifest(tree.children[childKey], manifest, `${dotPath}.${childKey}`);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Core loop: processes a node through parse → hydrate → conditionals → iterate
|
|
74
|
+
* @param {Node} node - DOM node to process (unused in current implementation, parsedNode is primary)
|
|
75
|
+
* @param {Object} parsedNode - Parsed tree node
|
|
76
|
+
* @param {Object} state - Current global state
|
|
77
|
+
* @param {Object} manifest - DOM manifest
|
|
78
|
+
* @param {Boolean} isNewNode - Whether this is a newly added node (affects hydration)
|
|
79
|
+
* @param {Boolean} debug - Debug mode
|
|
80
|
+
* @returns {Object} - Counts of hydrated/iterated/evaluated elements
|
|
81
|
+
*/
|
|
82
|
+
const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) => {
|
|
83
|
+
let hydratedCount = 0;
|
|
84
|
+
let iteratedCount = 0;
|
|
85
|
+
let evaluatedCount = 0;
|
|
86
|
+
|
|
87
|
+
if (!parsedNode) return { hydratedCount, iteratedCount, evaluatedCount };
|
|
88
|
+
|
|
89
|
+
// 1. Parse - already done before calling core loop (in processMutations)
|
|
90
|
+
|
|
91
|
+
// 2. Hydrate (replace @[...] bindings)
|
|
92
|
+
// Use empty object as "previous state" for new nodes so all bindings are affected
|
|
93
|
+
const affectedElements = affected(parsedNode, isNewNode ? {} : previousState, state);
|
|
94
|
+
if (affectedElements.length > 0) {
|
|
95
|
+
hydratedCount = affectedElements.length;
|
|
96
|
+
hydrate(affectedElements, state, manifest);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. Conditionals (evaluate <!-- if -->)
|
|
100
|
+
const conditionalCount = renderAllConditionals(parsedNode, state, manifest);
|
|
101
|
+
if (conditionalCount > 0) {
|
|
102
|
+
evaluatedCount = conditionalCount;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. Iterate (render <!-- each -->)
|
|
106
|
+
const iterationCount = renderAllIterations(parsedNode, state, manifest);
|
|
107
|
+
if (iterationCount > 0) {
|
|
108
|
+
iteratedCount = iterationCount;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Note: Any DOM changes from steps 3-4 will trigger MutationObserver
|
|
112
|
+
// which will recursively call processMutations for nested content
|
|
113
|
+
|
|
114
|
+
return { hydratedCount, iteratedCount, evaluatedCount };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Store previous state for comparison (needs to be accessible by core loop)
|
|
118
|
+
let previousState = {};
|
|
119
|
+
|
|
120
|
+
const main = (s, attrName = 'vibe', config = {}) => {
|
|
121
|
+
const debug = !!config?.debug;
|
|
122
|
+
const verbose = !!config?.verbose;
|
|
123
|
+
|
|
124
|
+
// Reset previous state for each new instance
|
|
125
|
+
previousState = {};
|
|
126
|
+
|
|
127
|
+
// Find element(s) with the specified attribute
|
|
128
|
+
const elements = document.querySelectorAll(`[${attrName}]`);
|
|
129
|
+
|
|
130
|
+
if (elements.length === 0) {
|
|
131
|
+
console.info(`[vibe] No element found with attribute "${attrName}". Falling back to body.`);
|
|
132
|
+
} else if (elements.length > 1) {
|
|
133
|
+
console.info(
|
|
134
|
+
`[vibe] Multiple elements (${elements.length}) found with attribute "${attrName}". Hydrating the first one.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rootElement = elements[0] || document.body;
|
|
139
|
+
|
|
140
|
+
debugLog(PHASE_ATTACH, `Vibe attached to`, debug, 0, rootElement);
|
|
141
|
+
|
|
142
|
+
let parsedTree = parse(rootElement);
|
|
143
|
+
let manifest = createManifest(parsedTree);
|
|
144
|
+
|
|
145
|
+
const manifestEntries = Object.entries(manifest);
|
|
146
|
+
const totalCount = manifestEntries.length;
|
|
147
|
+
const elementsOnly = manifestEntries.filter(
|
|
148
|
+
([_, el]) => el && !el.nodeName.startsWith('#'),
|
|
149
|
+
).length;
|
|
150
|
+
const nodesOnly = totalCount - elementsOnly;
|
|
151
|
+
|
|
152
|
+
const segments = [
|
|
153
|
+
{ text: `${elementsOnly}`, colored: true },
|
|
154
|
+
{ text: ' elements (', colored: false },
|
|
155
|
+
{ text: `${totalCount}`, color: 'slate' },
|
|
156
|
+
{ text: ' total nodes)', colored: false },
|
|
157
|
+
];
|
|
158
|
+
if (parsedTree.stats?.skipped > 0) {
|
|
159
|
+
segments.push(
|
|
160
|
+
{ text: ' (', colored: false },
|
|
161
|
+
{ text: `${parsedTree.stats.skipped}`, color: 'yellow' },
|
|
162
|
+
{ text: ' skipped)', colored: false },
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
debugLog(PHASE_PARSE, segments, debug);
|
|
166
|
+
debugLog(PHASE_MANIFEST, `DOM manifest created`, debug);
|
|
167
|
+
|
|
168
|
+
// Lifecycle hooks that users can subscribe to
|
|
169
|
+
const hooks = {
|
|
170
|
+
afterUpdate: [],
|
|
171
|
+
afterDomMutation: [],
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Observer reference and callback - defined here so state handler can access processMutations
|
|
175
|
+
let observer = null;
|
|
176
|
+
let processMutations = null;
|
|
177
|
+
|
|
178
|
+
const $ = state(s, (newState) => {
|
|
179
|
+
const mergedState = deepMerge($, newState);
|
|
180
|
+
|
|
181
|
+
// Find what changed
|
|
182
|
+
const affectedElements = affected(parsedTree, previousState, mergedState);
|
|
183
|
+
|
|
184
|
+
if (affectedElements.length > 0) {
|
|
185
|
+
const changedKeys = Object.keys(newState).join(', ');
|
|
186
|
+
debugLog(PHASE_UPDATE, `state changed (${changedKeys})`, debug);
|
|
187
|
+
|
|
188
|
+
// Capture pending mutations before disconnecting (takeRecords clears the queue)
|
|
189
|
+
let pendingMutations = [];
|
|
190
|
+
if (observer) {
|
|
191
|
+
pendingMutations = observer.takeRecords();
|
|
192
|
+
observer.disconnect();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Run core loop on the full tree (hydrate will use affected list)
|
|
196
|
+
// isNewNode = false because this is a state update, not a DOM mutation
|
|
197
|
+
hydrate(affectedElements, mergedState, manifest);
|
|
198
|
+
|
|
199
|
+
// After hydrate, check for conditional/iteration changes
|
|
200
|
+
const conditionalCount = renderAllConditionals(parsedTree, mergedState, manifest);
|
|
201
|
+
const iterationCount = renderAllIterations(parsedTree, mergedState, manifest);
|
|
202
|
+
|
|
203
|
+
if (observer) {
|
|
204
|
+
observer.observe(rootElement, {
|
|
205
|
+
attributes: false,
|
|
206
|
+
characterData: false,
|
|
207
|
+
childList: true,
|
|
208
|
+
subtree: true,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Process mutations that were pending before we disconnected
|
|
212
|
+
if (pendingMutations.length > 0 && processMutations) {
|
|
213
|
+
processMutations(pendingMutations);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Deep clone that handles proxies (structuredClone fails on proxy objects)
|
|
219
|
+
const deepClone = (obj) => {
|
|
220
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
221
|
+
if (Array.isArray(obj)) return obj.map(deepClone);
|
|
222
|
+
const cloned = {};
|
|
223
|
+
for (const key in obj) {
|
|
224
|
+
if (obj.hasOwnProperty(key)) {
|
|
225
|
+
cloned[key] = deepClone(obj[key]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return cloned;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const prev = deepClone(previousState);
|
|
232
|
+
previousState = { ...$, ...newState };
|
|
233
|
+
hooks.afterUpdate.forEach((callback) => callback(deepClone({ ...$ }), prev));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
|
|
237
|
+
Object.defineProperty($, 'on', {
|
|
238
|
+
value: (event, callback) => {
|
|
239
|
+
if (hooks[event]) {
|
|
240
|
+
hooks[event].push(callback);
|
|
241
|
+
}
|
|
242
|
+
return () => (hooks[event] = hooks[event].filter((cb) => cb !== callback));
|
|
243
|
+
},
|
|
244
|
+
enumerable: false,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Initial hydration
|
|
248
|
+
const affectedElements = affected(parsedTree, $, $);
|
|
249
|
+
|
|
250
|
+
debugLog(
|
|
251
|
+
PHASE_HYDRATE,
|
|
252
|
+
[
|
|
253
|
+
{ text: `${affectedElements.length} bindings (`, colored: false },
|
|
254
|
+
{ text: '@[...]', color: 'pink' },
|
|
255
|
+
{ text: ')', colored: false },
|
|
256
|
+
],
|
|
257
|
+
debug,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
hydrate(affectedElements, $, manifest);
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
// Render all iterations and conditionals after initial hydration
|
|
264
|
+
setPreviousState($);
|
|
265
|
+
previousState = { ...$ };
|
|
266
|
+
|
|
267
|
+
const iterationCount = renderAllIterations(parsedTree, $, manifest);
|
|
268
|
+
if (iterationCount > 0)
|
|
269
|
+
debugLog(
|
|
270
|
+
PHASE_ITERATE,
|
|
271
|
+
[
|
|
272
|
+
{ text: `${iterationCount} iterations (`, colored: false },
|
|
273
|
+
{ text: '<!-- each -->', color: 'commentGreen' },
|
|
274
|
+
{ text: ')', colored: false },
|
|
275
|
+
],
|
|
276
|
+
debug,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const conditionalCount = renderAllConditionals(parsedTree, $, manifest);
|
|
280
|
+
if (conditionalCount > 0)
|
|
281
|
+
debugLog(
|
|
282
|
+
PHASE_CONDITION,
|
|
283
|
+
[
|
|
284
|
+
{ text: `${conditionalCount} conditionals (`, colored: false },
|
|
285
|
+
{ text: '<!-- if -->', color: 'commentGreen' },
|
|
286
|
+
{ text: ')', colored: false },
|
|
287
|
+
],
|
|
288
|
+
debug,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Define observer callback as named function so we can call it manually for pending mutations
|
|
292
|
+
processMutations = (mutations) => {
|
|
293
|
+
// Early exit if no mutations to process (common case)
|
|
294
|
+
if (mutations.length === 0) return;
|
|
295
|
+
|
|
296
|
+
const manifestSizeBefore = Object.keys(manifest).length;
|
|
297
|
+
let hadChanges = false;
|
|
298
|
+
let parsedParents = null; // Lazy init - only create Set when needed
|
|
299
|
+
let addedElements = 0;
|
|
300
|
+
let addedNodes = 0;
|
|
301
|
+
let removedElements = 0;
|
|
302
|
+
let removedNodes = 0;
|
|
303
|
+
let singleElement = null; // Track the single element when count is 1
|
|
304
|
+
let totalSkipped = 0; // Track skipped elements from parse
|
|
305
|
+
let hydratedCount = 0;
|
|
306
|
+
let iteratedCount = 0;
|
|
307
|
+
let evaluatedCount = 0;
|
|
308
|
+
let addedElementsList = []; // Track all added elements for verbose output
|
|
309
|
+
|
|
310
|
+
mutations.forEach(({ addedNodes: addedNodesList, removedNodes: removedNodesList, target }) => {
|
|
311
|
+
removedNodesList.forEach((node) => {
|
|
312
|
+
// If this is a component element with pending fetch, abort it
|
|
313
|
+
if (node.nodeName === 'COMPONENT') {
|
|
314
|
+
abortComponentFetch(node);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const entry = Object.entries(manifest).find(([_, element]) => element === node);
|
|
318
|
+
|
|
319
|
+
// Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
|
|
320
|
+
if (!entry) return;
|
|
321
|
+
|
|
322
|
+
const [dotAnnotation] = entry;
|
|
323
|
+
delete manifest[dotAnnotation];
|
|
324
|
+
|
|
325
|
+
const dotPath = dotAnnotation.split('.');
|
|
326
|
+
const name = dotPath.pop();
|
|
327
|
+
const parentDotAnnotation = dotPath.join('.');
|
|
328
|
+
|
|
329
|
+
const picked = navigateTree(parsedTree, parentDotAnnotation);
|
|
330
|
+
|
|
331
|
+
// If we can't navigate to the parent, skip
|
|
332
|
+
if (!picked || !picked.element) return;
|
|
333
|
+
|
|
334
|
+
// Update parent's parsed HTML (only once per parent)
|
|
335
|
+
if (!parsedParents) parsedParents = new Set();
|
|
336
|
+
if (!parsedParents.has(picked)) {
|
|
337
|
+
const { parsed } = parse(picked.element);
|
|
338
|
+
picked.parsed = parsed;
|
|
339
|
+
parsedParents.add(picked);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Remove the node from parent's children
|
|
343
|
+
delete picked.children[name];
|
|
344
|
+
|
|
345
|
+
// Count elements vs nodes separately
|
|
346
|
+
if (node.nodeName.startsWith('#')) {
|
|
347
|
+
removedNodes++;
|
|
348
|
+
} else {
|
|
349
|
+
removedElements++;
|
|
350
|
+
singleElement = node;
|
|
351
|
+
}
|
|
352
|
+
hadChanges = true;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
addedNodesList.forEach((node) => {
|
|
356
|
+
// Skip if node itself or any ancestor is non-reactive
|
|
357
|
+
if (isNonReactiveOrInside(node)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const entry = Object.entries(manifest).find(([_, element]) => element === target);
|
|
362
|
+
|
|
363
|
+
// If parent isn't tracked, this node is outside the reactive scope
|
|
364
|
+
if (!entry) return;
|
|
365
|
+
|
|
366
|
+
const [dotAnnotation] = entry;
|
|
367
|
+
const picked = navigateTree(parsedTree, dotAnnotation);
|
|
368
|
+
|
|
369
|
+
// If we can't navigate to the parent in the tree, skip
|
|
370
|
+
if (!picked) return;
|
|
371
|
+
|
|
372
|
+
// If parent has no element reference, re-parse from the actual DOM element
|
|
373
|
+
if (!picked.element) {
|
|
374
|
+
picked.element = target;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Parse the newly added node
|
|
378
|
+
const name = `${node.nodeName.toLowerCase()}_${hash()}`;
|
|
379
|
+
const parsedNode = parse(node);
|
|
380
|
+
|
|
381
|
+
// Accumulate skipped stats
|
|
382
|
+
if (parsedNode.stats?.skipped) {
|
|
383
|
+
totalSkipped += parsedNode.stats.skipped;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Update parent's parsed HTML (only once per parent)
|
|
387
|
+
if (!parsedParents) parsedParents = new Set();
|
|
388
|
+
if (!parsedParents.has(picked)) {
|
|
389
|
+
const { parsed } = parse(picked.element);
|
|
390
|
+
picked.parsed = parsed;
|
|
391
|
+
parsedParents.add(picked);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Add the parsed node to parent's children
|
|
395
|
+
picked.children[name] = parsedNode;
|
|
396
|
+
|
|
397
|
+
// Recursively add node and all descendants to manifest
|
|
398
|
+
addToManifest(parsedNode, manifest, `${dotAnnotation}.${name}`);
|
|
399
|
+
|
|
400
|
+
// Run core loop for the new node (parse already done, hydrate → conditionals → iterate)
|
|
401
|
+
const counts = processCoreLoop(node, parsedNode, $, manifest, true, debug);
|
|
402
|
+
hydratedCount += counts.hydratedCount;
|
|
403
|
+
iteratedCount += counts.iteratedCount;
|
|
404
|
+
evaluatedCount += counts.evaluatedCount;
|
|
405
|
+
|
|
406
|
+
// Count elements vs nodes separately
|
|
407
|
+
if (node.nodeName.startsWith('#')) {
|
|
408
|
+
addedNodes++;
|
|
409
|
+
} else {
|
|
410
|
+
addedElements++;
|
|
411
|
+
singleElement = node;
|
|
412
|
+
addedElementsList.push(node);
|
|
413
|
+
}
|
|
414
|
+
hadChanges = true;
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Fire hooks once after all mutations are processed (not per-node)
|
|
419
|
+
if (hadChanges) {
|
|
420
|
+
if (addedElements > 0 || addedNodes > 0 || removedElements > 0 || removedNodes > 0) {
|
|
421
|
+
const segments = [];
|
|
422
|
+
|
|
423
|
+
// Added
|
|
424
|
+
if (addedElements > 0) {
|
|
425
|
+
segments.push({ text: `+${addedElements}`, color: 'green' });
|
|
426
|
+
segments.push({ text: ' elements', colored: false });
|
|
427
|
+
}
|
|
428
|
+
if (addedNodes > 0) {
|
|
429
|
+
if (addedElements > 0) segments.push({ text: ', ', colored: false });
|
|
430
|
+
segments.push({ text: `+${addedNodes}`, color: 'slate' });
|
|
431
|
+
segments.push({ text: ' nodes', colored: false });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Removed
|
|
435
|
+
if (removedElements > 0) {
|
|
436
|
+
if (addedElements > 0 || addedNodes > 0) segments.push({ text: ', ', colored: false });
|
|
437
|
+
segments.push({ text: `-${removedElements}`, color: 'red' });
|
|
438
|
+
segments.push({ text: ' elements', colored: false });
|
|
439
|
+
}
|
|
440
|
+
if (removedNodes > 0) {
|
|
441
|
+
if (addedElements > 0 || addedNodes > 0 || removedElements > 0)
|
|
442
|
+
segments.push({ text: ', ', colored: false });
|
|
443
|
+
segments.push({ text: `-${removedNodes}`, color: 'slate' });
|
|
444
|
+
segments.push({ text: ' nodes', colored: false });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Pass element reference if exactly one element was mutated (not counting nodes)
|
|
448
|
+
// const totalElementCount = addedElements + removedElements;
|
|
449
|
+
// const elementToLog = totalElementCount === 1 ? singleElement : null;
|
|
450
|
+
// debugLog(PHASE_MUTATE, segments, debug, 0, elementToLog);
|
|
451
|
+
debugLog(PHASE_MUTATE, segments, debug);
|
|
452
|
+
|
|
453
|
+
// Verbose: log the added element (or topmost parent if multiple)
|
|
454
|
+
if (verbose && addedElementsList.length > 0) {
|
|
455
|
+
// Find the topmost parent among added elements
|
|
456
|
+
const topmostParent = addedElementsList.find((el) => {
|
|
457
|
+
// Check if this element is NOT a descendant of any other element in the list
|
|
458
|
+
return !addedElementsList.some((other) => other !== el && other.contains(el));
|
|
459
|
+
});
|
|
460
|
+
debugLog(PHASE_MUTATE, '', debug, 0, topmostParent || addedElementsList[0]);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Log parsed elements after mutation
|
|
464
|
+
const manifestSizeAfter = Object.keys(manifest).length;
|
|
465
|
+
const totalNodes = manifestSizeAfter - manifestSizeBefore;
|
|
466
|
+
|
|
467
|
+
if (totalNodes > 0) {
|
|
468
|
+
const newManifestEntries = Object.entries(manifest).slice(-totalNodes);
|
|
469
|
+
const elementsOnly = newManifestEntries.filter(
|
|
470
|
+
([_, el]) => el && !el.nodeName.startsWith('#'),
|
|
471
|
+
).length;
|
|
472
|
+
|
|
473
|
+
const parseSegments = [
|
|
474
|
+
{ text: `${elementsOnly}`, colored: true },
|
|
475
|
+
{ text: ' elements (', colored: false },
|
|
476
|
+
{ text: `${totalNodes}`, color: 'slate' },
|
|
477
|
+
{ text: ' total nodes)', colored: false },
|
|
478
|
+
];
|
|
479
|
+
|
|
480
|
+
if (totalSkipped > 0) {
|
|
481
|
+
parseSegments.push(
|
|
482
|
+
{ text: ' (', colored: false },
|
|
483
|
+
{ text: `${totalSkipped}`, color: 'yellow' },
|
|
484
|
+
{ text: ' skipped)', colored: false },
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
debugLog(PHASE_PARSE, parseSegments, debug);
|
|
489
|
+
|
|
490
|
+
// Log hydration after parse
|
|
491
|
+
if (hydratedCount > 0) {
|
|
492
|
+
debugLog(
|
|
493
|
+
PHASE_HYDRATE,
|
|
494
|
+
[
|
|
495
|
+
{ text: `${hydratedCount} bindings (`, colored: false },
|
|
496
|
+
{ text: '@[...]', color: 'pink' },
|
|
497
|
+
{ text: ')', colored: false },
|
|
498
|
+
],
|
|
499
|
+
debug,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Log iterations after hydration
|
|
504
|
+
if (iteratedCount > 0) {
|
|
505
|
+
debugLog(
|
|
506
|
+
PHASE_ITERATE,
|
|
507
|
+
[
|
|
508
|
+
{ text: `${iteratedCount} iterations (`, colored: false },
|
|
509
|
+
{ text: '<!-- each -->', color: 'commentGreen' },
|
|
510
|
+
{ text: ')', colored: false },
|
|
511
|
+
],
|
|
512
|
+
debug,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Log conditionals after iterations
|
|
517
|
+
if (evaluatedCount > 0) {
|
|
518
|
+
debugLog(
|
|
519
|
+
PHASE_CONDITION,
|
|
520
|
+
[
|
|
521
|
+
{ text: `${evaluatedCount} conditionals (`, colored: false },
|
|
522
|
+
{ text: '<!-- if -->', color: 'commentGreen' },
|
|
523
|
+
{ text: ')', colored: false },
|
|
524
|
+
],
|
|
525
|
+
debug,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Always call hooks, even if no changes detected (cleanup needs to check)
|
|
533
|
+
hooks.afterDomMutation.forEach((callback) => callback());
|
|
534
|
+
|
|
535
|
+
// Check for new <component> elements after DOM mutations
|
|
536
|
+
// Process component elements if we've started (initial call happened)
|
|
537
|
+
if (componentProcessingStarted && !cleanupExecuted) {
|
|
538
|
+
const componentConfig = {
|
|
539
|
+
...config,
|
|
540
|
+
_forceSync: true,
|
|
541
|
+
_observer: observer,
|
|
542
|
+
_processMutations: processMutations
|
|
543
|
+
};
|
|
544
|
+
processComponent(rootElement, () => {
|
|
545
|
+
// When all components are done, run cleanup check
|
|
546
|
+
checkCleanup();
|
|
547
|
+
}, componentConfig);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
observer = new MutationObserver(processMutations);
|
|
552
|
+
|
|
553
|
+
observer.observe(rootElement, {
|
|
554
|
+
attributes: false,
|
|
555
|
+
characterData: false,
|
|
556
|
+
childList: true,
|
|
557
|
+
subtree: true,
|
|
558
|
+
attributeOldValue: false,
|
|
559
|
+
characterDataOldValue: false,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
debugLog(PHASE_OBSERVE, `MutationObserver listening to DOM changes`, debug);
|
|
563
|
+
|
|
564
|
+
// Track cleanup state
|
|
565
|
+
let componentProcessingStarted = false;
|
|
566
|
+
let cleanupExecuted = false;
|
|
567
|
+
|
|
568
|
+
// Check if cleanup should run
|
|
569
|
+
const checkCleanup = () => {
|
|
570
|
+
if (cleanupExecuted) return;
|
|
571
|
+
|
|
572
|
+
// Check for pending mutations first
|
|
573
|
+
const pendingMutations = observer ? observer.takeRecords() : [];
|
|
574
|
+
|
|
575
|
+
if (pendingMutations.length > 0) {
|
|
576
|
+
// More mutations to process
|
|
577
|
+
processMutations(pendingMutations);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Check if all processing is complete
|
|
582
|
+
if (shouldCleanup(rootElement)) {
|
|
583
|
+
cleanup(rootElement, attrName, debug);
|
|
584
|
+
cleanupExecuted = true;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// Register hook to check for cleanup readiness after each mutation batch
|
|
589
|
+
hooks.afterDomMutation.push(checkCleanup);
|
|
590
|
+
|
|
591
|
+
// Process component elements after initialization - MutationObserver will handle hydration
|
|
592
|
+
componentProcessingStarted = true;
|
|
593
|
+
|
|
594
|
+
// Pass observer and processMutations to component for sync processing
|
|
595
|
+
const componentConfig = {
|
|
596
|
+
...config,
|
|
597
|
+
_forceSync: true,
|
|
598
|
+
_observer: observer,
|
|
599
|
+
_processMutations: processMutations
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
processComponent(
|
|
603
|
+
rootElement,
|
|
604
|
+
() => {
|
|
605
|
+
// When all components are done, run cleanup check
|
|
606
|
+
checkCleanup();
|
|
607
|
+
},
|
|
608
|
+
componentConfig,
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
return $;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
export default main;
|