@ape-egg/vibe 2.1.22 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -5
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +26 -17
- package/llms.txt +36 -5
- package/package.json +20 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +360 -98
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +277 -110
- package/runtime/index.js +189 -65
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +81 -11
- package/runtime/pre-compiled-iterations.js +19 -6
- package/runtime/pre-compiled-manifest.js +13 -4
- package/runtime/staging.js +153 -0
- package/runtime/state.js +31 -0
- package/runtime/tracking.js +173 -0
- package/runtime/utils.js +155 -78
- package/spa.js +206 -0
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1159
- package/ROADMAP.md +0 -397
- package/compiler/bin/vibe-compile.js +0 -121
- package/compiler/native/.gitkeep +0 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +0 -2023
- package/compiler/src/Cargo.toml +0 -38
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
- package/compiler/src/compiler/binding_case.rs +0 -88
- package/compiler/src/compiler/compile.rs +0 -2522
- package/compiler/src/compiler/component_tagger.rs +0 -469
- package/compiler/src/compiler/iteration_optimizer.rs +0 -455
- package/compiler/src/compiler/js_analyzer.rs +0 -715
- package/compiler/src/compiler/manifest_builder.rs +0 -693
- package/compiler/src/compiler/mod.rs +0 -15
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- package/compiler/src/compiler/state_extractor.rs +0 -263
- package/compiler/src/compiler/value_stamper.rs +0 -921
- package/compiler/src/compiler/watcher.rs +0 -1147
- package/compiler/src/config.rs +0 -239
- package/compiler/src/main.rs +0 -347
- package/compiler/src/parser/element.rs +0 -96
- package/compiler/src/parser/html.rs +0 -1004
- package/compiler/src/parser/mod.rs +0 -8
- package/runtime/pre-compiled-manifest.test.mjs +0 -58
- package/runtime/scope.js +0 -50
- package/test-results/.last-run.json +0 -4
package/runtime/affected.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import { resolvePath, deepEqual } from "./iteration-utils.js";
|
|
2
|
-
import { extractDependencies } from "./conditionals.js";
|
|
1
|
+
import { resolvePath, deepEqual, iterPropWrappersOf } from "./iteration-utils.js";
|
|
3
2
|
import { BINDING_REGEX } from "./constants.js";
|
|
4
|
-
import {
|
|
3
|
+
import { liveNode, isOutgoing } from "./staging.js";
|
|
4
|
+
import { beginSpeculative, endSpeculative } from "./debug.js";
|
|
5
|
+
import {
|
|
6
|
+
evalInScope,
|
|
7
|
+
resolveThisPath,
|
|
8
|
+
ownKeysOf,
|
|
9
|
+
scopedOverlayOf,
|
|
10
|
+
rememberOverlay,
|
|
11
|
+
overlayKeysOf,
|
|
12
|
+
} from "./utils.js";
|
|
13
|
+
import { beginTracking, endTracking, unsubscribe } from "./tracking.js";
|
|
14
|
+
|
|
15
|
+
// Drop the subscribers of a stale binding-cache list. A cache rebuild means
|
|
16
|
+
// the parsed source or element changed — the old binding objects are dead,
|
|
17
|
+
// but their anchors may still be connected (a reparse of a live element), so
|
|
18
|
+
// disconnect-pruning alone would leave them serving stale patches.
|
|
19
|
+
const unsubscribeBindings = (list) => {
|
|
20
|
+
if (!list) return;
|
|
21
|
+
for (let i = 0; i < list.length; i++) {
|
|
22
|
+
if (list[i]._sub) unsubscribe(list[i]._sub);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
5
25
|
|
|
6
26
|
// The @[...] bindings in a tree node are static: the parsed template string and
|
|
7
27
|
// the element's component scope don't change between renders. Re-running the
|
|
@@ -11,6 +31,7 @@ import { evalInScope, resolveThisPath, ownKeysOf, scopedOverlayOf } from "./util
|
|
|
11
31
|
// and element so a re-parse or element swap recomputes.
|
|
12
32
|
const textBindingsOf = (tree) => {
|
|
13
33
|
if (tree._tbSrc === tree.parsed && tree._tbEl === tree.element) return tree._tb;
|
|
34
|
+
unsubscribeBindings(tree._tb);
|
|
14
35
|
const out = [];
|
|
15
36
|
BINDING_REGEX.lastIndex = 0;
|
|
16
37
|
let m;
|
|
@@ -24,6 +45,9 @@ const textBindingsOf = (tree) => {
|
|
|
24
45
|
tokens: tokensOf(resolvedInner),
|
|
25
46
|
});
|
|
26
47
|
}
|
|
48
|
+
// Back-ref so dispatch can expand one dirty binding to its whole text
|
|
49
|
+
// span (a text node hydrates all-or-nothing).
|
|
50
|
+
for (let i = 0; i < out.length; i++) out[i]._group = out;
|
|
27
51
|
tree._tb = out;
|
|
28
52
|
tree._tbSrc = tree.parsed;
|
|
29
53
|
tree._tbEl = tree.element;
|
|
@@ -32,6 +56,7 @@ const textBindingsOf = (tree) => {
|
|
|
32
56
|
|
|
33
57
|
const attrBindingsOf = (tree) => {
|
|
34
58
|
if (tree._abSrc === tree.attributes && tree._abEl === tree.element) return tree._ab;
|
|
59
|
+
if (tree._ab) for (const entry of tree._ab) unsubscribeBindings(entry.matches);
|
|
35
60
|
const out = [];
|
|
36
61
|
for (const attrName in tree.attributes) {
|
|
37
62
|
const attrValue = tree.attributes[attrName];
|
|
@@ -55,6 +80,7 @@ const attrBindingsOf = (tree) => {
|
|
|
55
80
|
// text/attr binding caches above.
|
|
56
81
|
const nameBindingsOf = (tree) => {
|
|
57
82
|
if (tree._nbSrc === tree.nameBindings && tree._nbEl === tree.element) return tree._nb;
|
|
83
|
+
unsubscribeBindings(tree._nb);
|
|
58
84
|
const out = [];
|
|
59
85
|
for (const nameBinding of tree.nameBindings) {
|
|
60
86
|
BINDING_REGEX.lastIndex = 0;
|
|
@@ -74,6 +100,12 @@ const nameBindingsOf = (tree) => {
|
|
|
74
100
|
const evaluateCondition = (expression, state, element = null) =>
|
|
75
101
|
!!evalInScope(expression, state, element);
|
|
76
102
|
|
|
103
|
+
// One subscriber per conditional/iteration tree node, hosted on the node —
|
|
104
|
+
// stable across flushes, dead with the node. The start comment anchors
|
|
105
|
+
// liveness (disconnect-pruning).
|
|
106
|
+
export const nodeSubscriberOf = (tree, kind) =>
|
|
107
|
+
(tree._sub ??= { kind, node: tree, anchor: tree.meta.startComment });
|
|
108
|
+
|
|
77
109
|
// Helper function to check if a match references a specific key.
|
|
78
110
|
// Fast path: exact match or property access (`user.name` matches `user`).
|
|
79
111
|
// Slow path: word-boundary search for complex expressions like `Math.floor(coins / 100)`
|
|
@@ -124,14 +156,17 @@ const keySetOf = (snapshot) => {
|
|
|
124
156
|
// A binding is affected on update when it reads a state key whose value changed,
|
|
125
157
|
// OR when it reads no known state key at all (can't prove it's unaffected, so
|
|
126
158
|
// re-evaluate — hydrate self-guards the DOM write). False can only be returned
|
|
127
|
-
// when every key the binding reads is present AND unchanged.
|
|
159
|
+
// when every key the binding reads is present AND unchanged. A key absent from
|
|
160
|
+
// the OLD snapshot is a change even when its new value is undefined — mount
|
|
161
|
+
// walks diff against {}, and skipping there means the binding never reaches
|
|
162
|
+
// tracked() and never registers a subscriber.
|
|
128
163
|
const bindingAffected = (tokens, keySet, state, newState) => {
|
|
129
164
|
let matched = false;
|
|
130
165
|
for (let i = 0; i < tokens.length; i++) {
|
|
131
166
|
const t = tokens[i];
|
|
132
167
|
if (keySet.has(t)) {
|
|
133
168
|
matched = true;
|
|
134
|
-
if (state[t] !== newState[t]) return true;
|
|
169
|
+
if (!(t in state) || state[t] !== newState[t]) return true;
|
|
135
170
|
}
|
|
136
171
|
}
|
|
137
172
|
return !matched;
|
|
@@ -162,7 +197,7 @@ const nameBindingAffected = (tokens, keyMap, state, newState) => {
|
|
|
162
197
|
const realKey = keyMap.get(tokens[i]);
|
|
163
198
|
if (realKey !== undefined) {
|
|
164
199
|
matched = true;
|
|
165
|
-
if (state[realKey] !== newState[realKey]) return true;
|
|
200
|
+
if (!(realKey in state) || state[realKey] !== newState[realKey]) return true;
|
|
166
201
|
}
|
|
167
202
|
}
|
|
168
203
|
return !matched;
|
|
@@ -178,16 +213,6 @@ const nameBindingAffected = (tokens, keyMap, state, newState) => {
|
|
|
178
213
|
// churn when the value didn't actually flip.
|
|
179
214
|
const CALL_EXPR_REGEX = /\b[A-Za-z_$][\w$]*\s*\(/;
|
|
180
215
|
|
|
181
|
-
// Resolve a clone-list entry to its live counterpart. processComponent swaps
|
|
182
|
-
// the original `<component src>` for a post-process `<component>` wrapper and
|
|
183
|
-
// records the new node on the original via `_vibeReplacedBy`. Mirrors
|
|
184
|
-
// `liveCloneNode` in iterate.js — kept inline to avoid a circular import.
|
|
185
|
-
const liveCloneNode = (node) => {
|
|
186
|
-
let cur = node;
|
|
187
|
-
while (cur && cur._vibeReplacedBy) cur = cur._vibeReplacedBy;
|
|
188
|
-
return cur;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
216
|
// Descend into any inlined `<component>` parsed trees attached to an
|
|
192
217
|
// iteration row. `_vibeIterTree` is stamped on the post-process wrapper by
|
|
193
218
|
// index.js (mutation-observer path) and on nested inlined components reached
|
|
@@ -202,15 +227,13 @@ const walkInlinedComponentTrees = (
|
|
|
202
227
|
depth,
|
|
203
228
|
) => {
|
|
204
229
|
for (let n = 0; n < clonedNodes.length; n++) {
|
|
205
|
-
const node =
|
|
230
|
+
const node = liveNode(clonedNodes[n]);
|
|
206
231
|
if (!node || node.nodeType !== 1) continue;
|
|
207
232
|
const wrappers = [];
|
|
208
233
|
if (node._vibeIterTree) wrappers.push(node);
|
|
209
|
-
const found = node
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (found[i]._vibeIterTree) wrappers.push(found[i]);
|
|
213
|
-
}
|
|
234
|
+
const found = iterPropWrappersOf(node);
|
|
235
|
+
for (let i = 0; i < found.length; i++) {
|
|
236
|
+
if (found[i]._vibeIterTree) wrappers.push(found[i]);
|
|
214
237
|
}
|
|
215
238
|
for (let w = 0; w < wrappers.length; w++) {
|
|
216
239
|
recursive(
|
|
@@ -225,6 +248,18 @@ const walkInlinedComponentTrees = (
|
|
|
225
248
|
}
|
|
226
249
|
};
|
|
227
250
|
|
|
251
|
+
// One affected() pass can reach the same parsed tree through several linkage
|
|
252
|
+
// channels — a conditional walks both its runtime-mounted branch and its
|
|
253
|
+
// processMutations-linked children, an iteration row's inlined component tree
|
|
254
|
+
// hangs off the wrapper's `_vibeIterTree` AND off an ancestor's children. Each
|
|
255
|
+
// duplicate walk multiplies at every nesting level (measured ~1000 visits per
|
|
256
|
+
// node per flush on the game's armory page). A tree object is one DOM position
|
|
257
|
+
// — the first visit already collected its entries, so the rest are skipped.
|
|
258
|
+
// Scoped states stay correct: a row-scoped tree is reached through its owning
|
|
259
|
+
// iteration first (document order), and inlined component trees carry
|
|
260
|
+
// registry-rewritten expressions that evaluate identically from any channel.
|
|
261
|
+
let walkVisited = null;
|
|
262
|
+
|
|
228
263
|
const recursive = (
|
|
229
264
|
tree,
|
|
230
265
|
state,
|
|
@@ -233,6 +268,10 @@ const recursive = (
|
|
|
233
268
|
scopedStateForHydration = null,
|
|
234
269
|
depth = 0,
|
|
235
270
|
) => {
|
|
271
|
+
if (walkVisited) {
|
|
272
|
+
if (walkVisited.has(tree)) return affected;
|
|
273
|
+
walkVisited.add(tree);
|
|
274
|
+
}
|
|
236
275
|
// Handle iteration nodes specially
|
|
237
276
|
if (tree.type === "iteration") {
|
|
238
277
|
const resolvedExpr = resolveThisPath(
|
|
@@ -243,22 +282,48 @@ const recursive = (
|
|
|
243
282
|
const oldArray =
|
|
244
283
|
evalInScope(resolvedExpr, state, tree.meta.startComment?.parentElement) ??
|
|
245
284
|
resolvePath(state, resolvedExpr);
|
|
285
|
+
// The new-side eval is the iteration's subscription: whatever the
|
|
286
|
+
// arrayPath expression reads NOW is what should notify it. Old-side
|
|
287
|
+
// evals never track — they read the previous snapshot.
|
|
288
|
+
const iterSub = nodeSubscriberOf(tree, "iteration");
|
|
289
|
+
iterSub.lastScope = newState;
|
|
290
|
+
beginTracking(iterSub, overlayKeysOf(newState));
|
|
246
291
|
const newArray =
|
|
247
292
|
evalInScope(
|
|
248
293
|
resolvedExpr,
|
|
249
294
|
newState,
|
|
250
295
|
tree.meta.startComment?.parentElement,
|
|
251
296
|
) ?? resolvePath(newState, resolvedExpr);
|
|
297
|
+
endTracking();
|
|
252
298
|
|
|
253
299
|
// Fast path: reference comparison (arrays are typically replaced, not mutated)
|
|
254
|
-
// This avoids expensive O(n) deepEqual for large arrays
|
|
300
|
+
// This avoids expensive O(n) deepEqual for large arrays.
|
|
301
|
+
//
|
|
302
|
+
// Call-expression array paths (`<!-- each filterItems(...) -->`, `.filter()`
|
|
303
|
+
// chains) build a FRESH array on every eval, so the two evals above never
|
|
304
|
+
// share a reference even when nothing relevant changed — the ref compare
|
|
305
|
+
// alone would flag every such iteration as a full array change on every
|
|
306
|
+
// flush and rewalk all its rows (the armory 900ms-per-tick hang). For call
|
|
307
|
+
// expressions only, same length + same item refs ⇒ the rendered rows are
|
|
308
|
+
// already right: fall through to the per-instance walk below, which still
|
|
309
|
+
// catches outer-state bindings inside rows. Plain paths keep pure ref
|
|
310
|
+
// semantics — `$.items = [...$.items]` after an in-place item mutation must
|
|
311
|
+
// still refresh every row (the documented reassign-to-rerender pattern).
|
|
255
312
|
if (oldArray !== newArray) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
313
|
+
const freshRefArtifact =
|
|
314
|
+
CALL_EXPR_REGEX.test(resolvedExpr) &&
|
|
315
|
+
Array.isArray(oldArray) &&
|
|
316
|
+
Array.isArray(newArray) &&
|
|
317
|
+
oldArray.length === newArray.length &&
|
|
318
|
+
oldArray.every((item, i) => item === newArray[i]);
|
|
319
|
+
if (!freshRefArtifact) {
|
|
320
|
+
affected.push({
|
|
321
|
+
type: "iteration",
|
|
322
|
+
node: tree,
|
|
323
|
+
changeType: "array",
|
|
324
|
+
});
|
|
325
|
+
return affected;
|
|
326
|
+
}
|
|
262
327
|
}
|
|
263
328
|
|
|
264
329
|
// Array didn't change, but check for affected elements inside iteration instances
|
|
@@ -312,6 +377,15 @@ const recursive = (
|
|
|
312
377
|
const mergedNewState = overlay
|
|
313
378
|
? { ...newState, ...overlay }
|
|
314
379
|
: { ...instance.scopedState, ...newState };
|
|
380
|
+
// Carry the alias set onto the merged plain snapshots so tracking
|
|
381
|
+
// windows opened against them can tell alias reads from global
|
|
382
|
+
// reads (skipKeys). Without the recorded overlay (legacy scoped
|
|
383
|
+
// states) alias names subscribe as phantom keys — harmless, they
|
|
384
|
+
// never appear in changedProps.
|
|
385
|
+
if (overlay) {
|
|
386
|
+
rememberOverlay(mergedOldState, overlay);
|
|
387
|
+
rememberOverlay(mergedNewState, overlay);
|
|
388
|
+
}
|
|
315
389
|
// scopedStateForHydration must reflect the new state so hydrate's
|
|
316
390
|
// bindings inside the row see post-update values. instance.scopedState
|
|
317
391
|
// is frozen against whatever target renderIteration was called with
|
|
@@ -360,11 +434,18 @@ const recursive = (
|
|
|
360
434
|
state,
|
|
361
435
|
tree.meta.startComment?.parentElement,
|
|
362
436
|
);
|
|
437
|
+
// New-side eval doubles as the conditional's subscription (see the
|
|
438
|
+
// iteration note above). Helper bodies reading live `$` record through
|
|
439
|
+
// the proxy trap even though the direct reads here hit the snapshot.
|
|
440
|
+
const condSub = nodeSubscriberOf(tree, "conditional");
|
|
441
|
+
condSub.lastScope = newState;
|
|
442
|
+
beginTracking(condSub, overlayKeysOf(newState));
|
|
363
443
|
const newValue = evaluateCondition(
|
|
364
444
|
tree.meta.expression,
|
|
365
445
|
newState,
|
|
366
446
|
tree.meta.startComment?.parentElement,
|
|
367
447
|
);
|
|
448
|
+
endTracking();
|
|
368
449
|
|
|
369
450
|
// Check if condition result changed
|
|
370
451
|
if (oldValue !== newValue) {
|
|
@@ -381,7 +462,10 @@ const recursive = (
|
|
|
381
462
|
changeType: "expression",
|
|
382
463
|
scopedState: newState,
|
|
383
464
|
oldScopedState: state,
|
|
465
|
+
wasConnected: !!tree.meta.startComment?.isConnected,
|
|
384
466
|
});
|
|
467
|
+
// The branch swap re-renders everything beneath — no point collecting
|
|
468
|
+
// entries for content that's about to be replaced.
|
|
385
469
|
return affected;
|
|
386
470
|
}
|
|
387
471
|
|
|
@@ -393,6 +477,8 @@ const recursive = (
|
|
|
393
477
|
// this cycle so updateConditional re-runs against live state; if the
|
|
394
478
|
// result hasn't really flipped, mountBranch's branchChanged guard absorbs
|
|
395
479
|
// the call cheaply. Skip during initial hydration (state === newState).
|
|
480
|
+
// Does NOT stop the walk: the branch is (almost always) unchanged, so the
|
|
481
|
+
// content beneath still needs its own entries collected below.
|
|
396
482
|
if (state !== newState && CALL_EXPR_REGEX.test(tree.meta.expression)) {
|
|
397
483
|
const stateKeys = Object.keys(state);
|
|
398
484
|
const anyChanged = stateKeys.some((k) => state[k] !== newState[k]);
|
|
@@ -403,14 +489,24 @@ const recursive = (
|
|
|
403
489
|
changeType: "expression-fn-call",
|
|
404
490
|
scopedState: newState,
|
|
405
491
|
oldScopedState: state,
|
|
492
|
+
wasConnected: !!tree.meta.startComment?.isConnected,
|
|
406
493
|
});
|
|
407
|
-
return affected;
|
|
408
494
|
}
|
|
409
495
|
}
|
|
410
496
|
|
|
411
|
-
// Condition didn't change
|
|
497
|
+
// Condition didn't change: walk the live content. Runtime-mounted
|
|
498
|
+
// branches live in runtime.activeInstance.parsedTree. COMPILED
|
|
499
|
+
// pre-rendered conditionals never went through mountBranch — their live
|
|
500
|
+
// DOM is what parse put in `children` — and processMutations links
|
|
501
|
+
// later-mounted subtrees (SPA fragments, slot-projected content) into
|
|
502
|
+
// `children` too. Both channels must be walked or content under a
|
|
503
|
+
// compiled conditional is frozen at its mount-time render (the game's
|
|
504
|
+
// blank fragment pages). Children whose root isn't connected are the
|
|
505
|
+
// detached both-branch template nodes of a runtime conditional — skip
|
|
506
|
+
// those; their restoration templates are strings on meta.branches, and
|
|
507
|
+
// hydrating detached template DOM is pure waste.
|
|
412
508
|
if (tree.runtime.activeInstance && tree.runtime.activeInstance.parsedTree) {
|
|
413
|
-
|
|
509
|
+
recursive(
|
|
414
510
|
tree.runtime.activeInstance.parsedTree,
|
|
415
511
|
state,
|
|
416
512
|
newState,
|
|
@@ -419,6 +515,15 @@ const recursive = (
|
|
|
419
515
|
depth + 1,
|
|
420
516
|
);
|
|
421
517
|
}
|
|
518
|
+
if (tree.children) {
|
|
519
|
+
for (const key in tree.children) {
|
|
520
|
+
const child = tree.children[key];
|
|
521
|
+
if (!child || typeof child !== "object") continue;
|
|
522
|
+
const root = child.element ?? child.textNode;
|
|
523
|
+
if (!root || !root.isConnected) continue;
|
|
524
|
+
recursive(child, state, newState, affected, scopedStateForHydration, depth + 1);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
422
527
|
|
|
423
528
|
return affected;
|
|
424
529
|
}
|
|
@@ -450,6 +555,8 @@ const recursive = (
|
|
|
450
555
|
element: tree.element,
|
|
451
556
|
textNode: tree.textNode, // Reference to specific text node (prevents wiping children)
|
|
452
557
|
scopedState: scopedStateForHydration, // Pass scoped state from iteration context
|
|
558
|
+
binding: m, // Stable per-binding host — hydrate registers its subscriber here
|
|
559
|
+
wasConnected: !!(tree.textNode ?? tree.element)?.isConnected,
|
|
453
560
|
});
|
|
454
561
|
}
|
|
455
562
|
}
|
|
@@ -475,6 +582,8 @@ const recursive = (
|
|
|
475
582
|
matchInner: m.inner, // Keep original, evalInScope will resolve this.
|
|
476
583
|
element: tree.element,
|
|
477
584
|
scopedState: scopedStateForHydration, // Pass scoped state from iteration context
|
|
585
|
+
binding: m,
|
|
586
|
+
wasConnected: !!tree.element?.isConnected,
|
|
478
587
|
});
|
|
479
588
|
}
|
|
480
589
|
}
|
|
@@ -498,14 +607,18 @@ const recursive = (
|
|
|
498
607
|
matchInner: m.inner, // Keep original, evalInScope will resolve this.
|
|
499
608
|
element: tree.element,
|
|
500
609
|
scopedState: scopedStateForHydration,
|
|
610
|
+
binding: m,
|
|
611
|
+
wasConnected: !!tree.element?.isConnected,
|
|
501
612
|
});
|
|
502
613
|
}
|
|
503
614
|
}
|
|
504
615
|
}
|
|
505
616
|
|
|
506
|
-
// Process children
|
|
617
|
+
// Process children — except under an outgoing wrapper, whose subtree is
|
|
618
|
+
// frozen until the swap (the node's own bindings above stay live: the next
|
|
619
|
+
// navigation must still re-trigger the remount through them).
|
|
507
620
|
const children = tree.children;
|
|
508
|
-
if (children) {
|
|
621
|
+
if (children && !isOutgoing(tree.element)) {
|
|
509
622
|
for (const key in children) {
|
|
510
623
|
const child = children[key];
|
|
511
624
|
if (child && typeof child === "object") {
|
|
@@ -524,7 +637,17 @@ const recursive = (
|
|
|
524
637
|
return affected;
|
|
525
638
|
};
|
|
526
639
|
|
|
640
|
+
// The whole collect is a speculative context: evaluations here probe "did
|
|
641
|
+
// this change?" against snapshots where scoped aliases may legitimately be
|
|
642
|
+
// absent — expected throws, not author errors (debug.js reportEvalError
|
|
643
|
+
// stays quiet inside).
|
|
527
644
|
export default (tree, state, newState) => {
|
|
528
|
-
|
|
529
|
-
|
|
645
|
+
walkVisited = new WeakSet();
|
|
646
|
+
beginSpeculative();
|
|
647
|
+
try {
|
|
648
|
+
return recursive(tree, state, newState, [], null, 0);
|
|
649
|
+
} finally {
|
|
650
|
+
endSpeculative();
|
|
651
|
+
walkVisited = null;
|
|
652
|
+
}
|
|
530
653
|
};
|
package/runtime/cleanup.js
CHANGED
|
@@ -45,7 +45,51 @@ export const shouldCleanup = (rootElement) => {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// 3.
|
|
48
|
+
// 3. Check for unhydrated ATTRIBUTE bindings — style="width: @[…]%" is
|
|
49
|
+
// invalid CSS until hydrated (width computes as auto → a full-width bar),
|
|
50
|
+
// so revealing on text-settled alone paints attribute-only content wrong
|
|
51
|
+
// (the game's "Making potion" loader flashed full on every navigation).
|
|
52
|
+
// data-vibe-* attributes are runtime transport that legitimately carries
|
|
53
|
+
// raw bindings across remounts, and src/key on a component wrapper are the
|
|
54
|
+
// wrapper's own remount contract — both excluded.
|
|
55
|
+
const elementWalker = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT, {
|
|
56
|
+
acceptNode(el) {
|
|
57
|
+
let parent = el;
|
|
58
|
+
while (parent && parent !== rootElement) {
|
|
59
|
+
if (NON_REACTIVE_ELEMENTS.includes(parent.nodeName)) {
|
|
60
|
+
return NodeFilter.FILTER_REJECT;
|
|
61
|
+
}
|
|
62
|
+
if (parent.hasAttribute(DEHYDRATE_CLASS_OR_ATTR) || parent.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)) {
|
|
63
|
+
return NodeFilter.FILTER_REJECT;
|
|
64
|
+
}
|
|
65
|
+
parent = parent.parentElement;
|
|
66
|
+
}
|
|
67
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let el;
|
|
72
|
+
while ((el = elementWalker.nextNode())) {
|
|
73
|
+
// A component wrapper's attributes are all its own machinery: src/key
|
|
74
|
+
// are the remount contract, data-vibe-* is transport, and every other
|
|
75
|
+
// attribute is a PROP — owned and consumed by processComponent at mount,
|
|
76
|
+
// parked raw by design on a declaration-form outlet whose src resolved
|
|
77
|
+
// to nothing. None of them are unhydrated page bindings.
|
|
78
|
+
if (
|
|
79
|
+
el.nodeName === 'COMPONENT' ||
|
|
80
|
+
(el.nodeName === 'DIV' && el.classList?.contains('component'))
|
|
81
|
+
) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
for (const attr of el.attributes) {
|
|
85
|
+
if (attr.name.startsWith('data-vibe-')) continue;
|
|
86
|
+
if (/@\[.+?\]/.test(attr.value)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 4. All processing appears complete
|
|
49
93
|
return true;
|
|
50
94
|
};
|
|
51
95
|
|