@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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Utility functions for array iteration
|
|
2
1
|
import {
|
|
3
2
|
ITERATION_REGEX,
|
|
4
3
|
ITERATION_START_REGEX,
|
|
@@ -8,12 +7,6 @@ import {
|
|
|
8
7
|
FETCH_SRC_SELECTOR,
|
|
9
8
|
} from './constants.js';
|
|
10
9
|
|
|
11
|
-
// Move binding-valued src/srcset/poster on browser-fetchable elements to
|
|
12
|
-
// data-vibe-<attr> so the literal `@[...]` text never becomes a fetchable URL.
|
|
13
|
-
// Called on subtrees that are still in the inert document (component finalize)
|
|
14
|
-
// — parse.js recaptures the parked binding under the real attribute name and
|
|
15
|
-
// hydration writes the evaluated URL, which is the first value the browser
|
|
16
|
-
// ever sees.
|
|
17
10
|
export const parkFetchableSrc = (root) => {
|
|
18
11
|
const elements = root.querySelectorAll(FETCH_SRC_SELECTOR);
|
|
19
12
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -28,12 +21,6 @@ export const parkFetchableSrc = (root) => {
|
|
|
28
21
|
}
|
|
29
22
|
};
|
|
30
23
|
|
|
31
|
-
// Parse an `each` directive body (the text inside `<!-- ... -->`, markers
|
|
32
|
-
// stripped) into its parts, or null when it isn't a valid each. The index alias
|
|
33
|
-
// and the (key) expression are both optional and may be written in either order
|
|
34
|
-
// — `as item, i (item.id)` and `as item (item.id), i` are equivalent. Single
|
|
35
|
-
// source of truth for the grammar, used by the runtime parser and (via DOM
|
|
36
|
-
// re-parse of the restored markers) by compiled pages.
|
|
37
24
|
export const parseIterationHeader = (text) => {
|
|
38
25
|
const match = text.match(ITERATION_REGEX);
|
|
39
26
|
if (!match) return null;
|
|
@@ -47,25 +34,34 @@ export const parseIterationHeader = (text) => {
|
|
|
47
34
|
};
|
|
48
35
|
};
|
|
49
36
|
|
|
50
|
-
// Resolve nested paths in state (e.g., "user.items" -> state.user.items)
|
|
51
|
-
// Supports bracket notation: "teams[0].combatants" -> state.teams[0].combatants
|
|
52
37
|
export const resolvePath = (obj, path) => {
|
|
53
38
|
if (!path || !obj) return undefined;
|
|
54
|
-
// Split on dots and brackets: "a[0].b[1].c" → ["a", "0", "b", "1", "c"]
|
|
55
39
|
const parts = path.match(/[^.\[\]]+/g);
|
|
56
40
|
if (!parts) return undefined;
|
|
57
41
|
return parts.reduce((acc, part) => acc?.[part], obj);
|
|
58
42
|
};
|
|
59
43
|
|
|
60
|
-
// Clone template element preserving structure
|
|
61
44
|
export const cloneTemplate = (templateElement) => {
|
|
62
45
|
return templateElement.cloneNode(true);
|
|
63
46
|
};
|
|
64
47
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
export const markBoundValues = (root, html) => {
|
|
49
|
+
if (!html.includes('@[')) return;
|
|
50
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
|
51
|
+
let node;
|
|
52
|
+
while ((node = walker.nextNode())) {
|
|
53
|
+
if (node.nodeType === 3) {
|
|
54
|
+
if (node.textContent.includes('@[')) node._vibeBoundValue = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
for (const attr of node.attributes) {
|
|
58
|
+
if (!attr.name.startsWith('data-vibe-') && attr.value.includes('@[')) {
|
|
59
|
+
(node._vibeBoundAttrs ??= new Set()).add(attr.name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
69
65
|
export const findEndCommentIndex = (nodes, startIndex) => {
|
|
70
66
|
let depth = 1;
|
|
71
67
|
for (let i = startIndex; i < nodes.length; i++) {
|
|
@@ -90,12 +86,6 @@ export const findEndComment = (nodes, startIndex) => {
|
|
|
90
86
|
return i;
|
|
91
87
|
};
|
|
92
88
|
|
|
93
|
-
// Find matching <!-- /if --> and optional <!-- else --> / <!-- else if -->
|
|
94
|
-
// with depth tracking. Returns { elseIndex, endIndex, elseText } — elseText
|
|
95
|
-
// is the boundary comment's trimmed text ('else' or 'else if <expr>'), so
|
|
96
|
-
// parse.js can desugar a chain: only the FIRST boundary at depth 1 is
|
|
97
|
-
// captured; the rest of the chain lives inside the false-branch span and
|
|
98
|
-
// desugars recursively into nested conditionals.
|
|
99
89
|
export const findConditionalEnd = (nodes, startIndex) => {
|
|
100
90
|
let depth = 1;
|
|
101
91
|
let elseIndex = null;
|
|
@@ -105,11 +95,9 @@ export const findConditionalEnd = (nodes, startIndex) => {
|
|
|
105
95
|
if (nodes[i].nodeName === '#comment') {
|
|
106
96
|
const text = nodes[i].textContent.trim();
|
|
107
97
|
|
|
108
|
-
// Check for nested if
|
|
109
98
|
if (CONDITIONAL_START_REGEX.test(text)) {
|
|
110
99
|
depth++;
|
|
111
100
|
}
|
|
112
|
-
// Check for else / else-if at current depth
|
|
113
101
|
else if (
|
|
114
102
|
depth === 1 &&
|
|
115
103
|
elseIndex === null &&
|
|
@@ -118,7 +106,6 @@ export const findConditionalEnd = (nodes, startIndex) => {
|
|
|
118
106
|
elseIndex = i;
|
|
119
107
|
elseText = text;
|
|
120
108
|
}
|
|
121
|
-
// Check for /if
|
|
122
109
|
else if (text === '/if') {
|
|
123
110
|
depth--;
|
|
124
111
|
if (depth === 0) {
|
|
@@ -128,7 +115,6 @@ export const findConditionalEnd = (nodes, startIndex) => {
|
|
|
128
115
|
}
|
|
129
116
|
}
|
|
130
117
|
|
|
131
|
-
// Build error message with context
|
|
132
118
|
const nodeTypes = nodes.slice(startIndex - 1, Math.min(startIndex + 10, nodes.length)).map((n, idx) => {
|
|
133
119
|
const actualIdx = startIndex - 1 + idx;
|
|
134
120
|
const prefix = actualIdx === startIndex - 1 ? '→ ' : ' ';
|
|
@@ -141,33 +127,28 @@ export const findConditionalEnd = (nodes, startIndex) => {
|
|
|
141
127
|
throw new Error(`Unmatched <!-- if --> comment: missing <!-- /if -->\nSearching from index ${startIndex} in ${nodes.length} nodes\nContext:\n${nodeTypes.join('\n')}`);
|
|
142
128
|
};
|
|
143
129
|
|
|
144
|
-
// Generate stable hash for objects
|
|
145
130
|
export const stableHash = (obj) => {
|
|
146
131
|
if (obj === null || obj === undefined) return 'null';
|
|
147
132
|
if (typeof obj !== 'object') return String(obj);
|
|
148
133
|
|
|
149
134
|
try {
|
|
150
|
-
// Sort keys for stable hashing
|
|
151
135
|
const str = JSON.stringify(obj, Object.keys(obj).sort());
|
|
152
136
|
return simpleHash(str);
|
|
153
137
|
} catch (e) {
|
|
154
|
-
// Fallback for circular references
|
|
155
138
|
return simpleHash(String(obj));
|
|
156
139
|
}
|
|
157
140
|
};
|
|
158
141
|
|
|
159
|
-
// Simple hash function
|
|
160
142
|
const simpleHash = (str) => {
|
|
161
143
|
let hash = 0;
|
|
162
144
|
for (let i = 0; i < str.length; i++) {
|
|
163
145
|
const char = str.charCodeAt(i);
|
|
164
146
|
hash = (hash << 5) - hash + char;
|
|
165
|
-
hash = hash & hash;
|
|
147
|
+
hash = hash & hash;
|
|
166
148
|
}
|
|
167
149
|
return Math.abs(hash).toString(36);
|
|
168
150
|
};
|
|
169
151
|
|
|
170
|
-
// Deep equality check
|
|
171
152
|
export const deepEqual = (a, b) => {
|
|
172
153
|
if (a === b) return true;
|
|
173
154
|
|
|
@@ -204,7 +185,6 @@ export const deepEqual = (a, b) => {
|
|
|
204
185
|
return true;
|
|
205
186
|
};
|
|
206
187
|
|
|
207
|
-
// Longest Common Subsequence algorithm
|
|
208
188
|
export const longestCommonSubsequence = (arr1, arr2) => {
|
|
209
189
|
const m = arr1.length;
|
|
210
190
|
const n = arr2.length;
|
|
@@ -212,7 +192,6 @@ export const longestCommonSubsequence = (arr1, arr2) => {
|
|
|
212
192
|
.fill(null)
|
|
213
193
|
.map(() => Array(n + 1).fill(0));
|
|
214
194
|
|
|
215
|
-
// Build LCS table
|
|
216
195
|
for (let i = 1; i <= m; i++) {
|
|
217
196
|
for (let j = 1; j <= n; j++) {
|
|
218
197
|
if (arr1[i - 1] === arr2[j - 1]) {
|
|
@@ -223,7 +202,6 @@ export const longestCommonSubsequence = (arr1, arr2) => {
|
|
|
223
202
|
}
|
|
224
203
|
}
|
|
225
204
|
|
|
226
|
-
// Backtrack to find LCS
|
|
227
205
|
const lcs = [];
|
|
228
206
|
let i = m,
|
|
229
207
|
j = n;
|
|
@@ -242,16 +220,6 @@ export const longestCommonSubsequence = (arr1, arr2) => {
|
|
|
242
220
|
return lcs;
|
|
243
221
|
};
|
|
244
222
|
|
|
245
|
-
// The affected walk and the iteration update path both ask "which
|
|
246
|
-
// [data-vibe-iter-prop] wrappers live under this row node?" for every row on
|
|
247
|
-
// every flush — re-querying each time dominated flush cost on iteration-heavy
|
|
248
|
-
// pages (~24k querySelectorAll calls per flush on the game's armory, nearly all
|
|
249
|
-
// returning nothing). The wrapper set only changes when a mount path stamps the
|
|
250
|
-
// attribute, so each stamp site bumps a global generation and the query result
|
|
251
|
-
// is cached per node against it. A cache hit still prunes wrappers whose
|
|
252
|
-
// subtree was torn down since (nested each/if removals keep the row node but
|
|
253
|
-
// drop descendants) — additions always arrive through a stamp ⇒ a bump ⇒ a
|
|
254
|
-
// fresh query.
|
|
255
223
|
let iterPropGeneration = 0;
|
|
256
224
|
|
|
257
225
|
export const bumpIterPropGeneration = () => {
|
|
@@ -274,52 +242,39 @@ export const iterPropWrappersOf = (node) => {
|
|
|
274
242
|
return list;
|
|
275
243
|
};
|
|
276
244
|
|
|
277
|
-
// Generate unique key for array items.
|
|
278
|
-
// `customKey` (when defined and not null) wins over every default heuristic —
|
|
279
|
-
// the developer has declared identity explicitly via `<!-- each xs as x (expr) -->`.
|
|
280
|
-
// The default heuristic only runs when no custom key was provided.
|
|
281
245
|
export const getItemKey = (item, index, customKey) => {
|
|
282
246
|
if (customKey !== undefined && customKey !== null) {
|
|
283
247
|
return `key_${customKey}`;
|
|
284
248
|
}
|
|
285
249
|
|
|
286
|
-
// 1. If item has 'id' property, use it
|
|
287
250
|
if (item && typeof item === 'object' && 'id' in item) {
|
|
288
251
|
return `id_${item.id}`;
|
|
289
252
|
}
|
|
290
253
|
|
|
291
|
-
// 2. If item is primitive, use value + index
|
|
292
254
|
if (typeof item !== 'object' || item === null) {
|
|
293
255
|
return `val_${item}_${index}`;
|
|
294
256
|
}
|
|
295
257
|
|
|
296
|
-
// 3. For objects without id, use stable hash
|
|
297
258
|
return `hash_${stableHash(item)}_${index}`;
|
|
298
259
|
};
|
|
299
260
|
|
|
300
|
-
// Compute diff operations between old and new arrays
|
|
301
261
|
export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
|
|
302
262
|
const operations = [];
|
|
303
263
|
|
|
304
|
-
// Use LCS to find common elements
|
|
305
264
|
const common = longestCommonSubsequence(oldKeys, newKeys);
|
|
306
265
|
|
|
307
|
-
// Build index maps for quick lookup
|
|
308
266
|
const newKeyMap = new Map(newKeys.map((key, idx) => [key, idx]));
|
|
309
267
|
const oldKeyMap = new Map(oldKeys.map((key, idx) => [key, idx]));
|
|
310
268
|
|
|
311
|
-
// Track which keys we've processed
|
|
312
269
|
const processedOld = new Set();
|
|
313
270
|
const processedNew = new Set();
|
|
314
271
|
|
|
315
|
-
// First pass: identify items to keep and check for updates
|
|
316
272
|
common.forEach((key) => {
|
|
317
273
|
const oldIdx = oldKeyMap.get(key);
|
|
318
274
|
const newIdx = newKeyMap.get(key);
|
|
319
275
|
processedOld.add(oldIdx);
|
|
320
276
|
processedNew.add(newIdx);
|
|
321
277
|
|
|
322
|
-
// Check if item content changed
|
|
323
278
|
if (!deepEqual(oldArray[oldIdx], newArray[newIdx])) {
|
|
324
279
|
operations.push({
|
|
325
280
|
type: 'UPDATE',
|
|
@@ -329,12 +284,8 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
|
|
|
329
284
|
});
|
|
330
285
|
}
|
|
331
286
|
|
|
332
|
-
// Note: We don't generate MOVE for index changes here.
|
|
333
|
-
// Items in LCS maintain relative order - after REMOVEs and ADDs,
|
|
334
|
-
// they'll naturally be in correct positions.
|
|
335
287
|
});
|
|
336
288
|
|
|
337
|
-
// Second pass: identify removals
|
|
338
289
|
oldKeys.forEach((key, idx) => {
|
|
339
290
|
if (!processedOld.has(idx)) {
|
|
340
291
|
operations.push({
|
|
@@ -345,7 +296,6 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
|
|
|
345
296
|
}
|
|
346
297
|
});
|
|
347
298
|
|
|
348
|
-
// Third pass: identify additions
|
|
349
299
|
newKeys.forEach((key, idx) => {
|
|
350
300
|
if (!processedNew.has(idx)) {
|
|
351
301
|
operations.push({
|
|
@@ -357,17 +307,14 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
|
|
|
357
307
|
}
|
|
358
308
|
});
|
|
359
309
|
|
|
360
|
-
// Sort operations: REMOVE first (from end), then MOVE, then ADD, then UPDATE
|
|
361
310
|
return operations.sort((a, b) => {
|
|
362
311
|
const priority = { REMOVE: 0, MOVE: 1, ADD: 2, UPDATE: 3 };
|
|
363
312
|
if (priority[a.type] !== priority[b.type]) {
|
|
364
313
|
return priority[a.type] - priority[b.type];
|
|
365
314
|
}
|
|
366
|
-
// For REMOVE, process from end to beginning
|
|
367
315
|
if (a.type === 'REMOVE') {
|
|
368
316
|
return b.index - a.index;
|
|
369
317
|
}
|
|
370
|
-
// For others, process in order
|
|
371
318
|
return a.index - b.index;
|
|
372
319
|
});
|
|
373
320
|
};
|
package/runtime/loop-scope.js
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
// Loop-scoped event handlers.
|
|
2
|
-
//
|
|
3
|
-
// An `on*` handler written inside a `<!-- each X as alias -->` loop can reference
|
|
4
|
-
// the bare loop variable directly, e.g. `onclick="pick(ability)"`. At parse time
|
|
5
|
-
// the alias token is rewritten to `$scope(this,'alias')`; at fire time that
|
|
6
|
-
// global resolver walks up the DOM to the nearest instance root stamped with the
|
|
7
|
-
// live item/index and returns it. This passes the *live object* (identity, not a
|
|
8
|
-
// stringified copy), works for derived-source loops, and survives reorders — all
|
|
9
|
-
// while keeping the handler a visible native `on*` attribute.
|
|
10
|
-
//
|
|
11
|
-
// See implement-loop-scoped-event-handlers.md for the full rationale.
|
|
12
|
-
|
|
13
1
|
const IDENT_START = /[A-Za-z_$]/;
|
|
14
2
|
const IDENT_PART = /[A-Za-z0-9_$]/;
|
|
15
3
|
|
|
16
|
-
// Last non-whitespace character already emitted — lets us tell a standalone
|
|
17
|
-
// identifier (rewrite) from a member access like `foo.alias` (leave alone).
|
|
18
4
|
const lastNonSpace = (s) => {
|
|
19
5
|
for (let i = s.length - 1; i >= 0; i--) {
|
|
20
6
|
const c = s[i];
|
|
@@ -23,8 +9,6 @@ const lastNonSpace = (s) => {
|
|
|
23
9
|
return '';
|
|
24
10
|
};
|
|
25
11
|
|
|
26
|
-
// Copy a quoted string literal beginning at `i` (value[i] is the opening quote)
|
|
27
|
-
// verbatim, honoring backslash escapes. Returns the index just past the closer.
|
|
28
12
|
const copyString = (value, i, push) => {
|
|
29
13
|
const quote = value[i];
|
|
30
14
|
push(quote);
|
|
@@ -48,14 +32,6 @@ const copyString = (value, i, push) => {
|
|
|
48
32
|
return i;
|
|
49
33
|
};
|
|
50
34
|
|
|
51
|
-
// Rewrite standalone references to loop-variable aliases inside an event-handler
|
|
52
|
-
// expression into `$scope(this,'alias')` calls. Skips `@[...]` binding spans
|
|
53
|
-
// (they keep their existing hydrate-time stringifying behavior), string literals,
|
|
54
|
-
// member accesses, and object-literal keys (`{ alias: x }` keeps its key; the
|
|
55
|
-
// shorthand `{ alias }` expands to `{ alias: $scope(this,'alias') }`), so only
|
|
56
|
-
// identifiers that genuinely name a loop alias are touched. Bracket frames carry
|
|
57
|
-
// a pending-ternary count per nesting level, which is what tells an object key's
|
|
58
|
-
// `:` apart from a ternary's.
|
|
59
35
|
export const rewriteHandlerAliases = (value, aliasSet) => {
|
|
60
36
|
if (!aliasSet || aliasSet.size === 0 || typeof value !== 'string') return value;
|
|
61
37
|
|
|
@@ -71,8 +47,6 @@ export const rewriteHandlerAliases = (value, aliasSet) => {
|
|
|
71
47
|
while (i < n) {
|
|
72
48
|
const ch = value[i];
|
|
73
49
|
|
|
74
|
-
// @[...] binding span — copy verbatim. Track bracket depth and skip inner
|
|
75
|
-
// strings so a `]` inside a quoted expression doesn't close the span early.
|
|
76
50
|
if (ch === '@' && value[i + 1] === '[') {
|
|
77
51
|
push('@[');
|
|
78
52
|
i += 2;
|
|
@@ -91,7 +65,6 @@ export const rewriteHandlerAliases = (value, aliasSet) => {
|
|
|
91
65
|
continue;
|
|
92
66
|
}
|
|
93
67
|
|
|
94
|
-
// String literal — copy verbatim.
|
|
95
68
|
if (ch === "'" || ch === '"' || ch === '`') {
|
|
96
69
|
i = copyString(value, i, push);
|
|
97
70
|
continue;
|
|
@@ -128,7 +101,6 @@ export const rewriteHandlerAliases = (value, aliasSet) => {
|
|
|
128
101
|
continue;
|
|
129
102
|
}
|
|
130
103
|
|
|
131
|
-
// Identifier — rewrite when it's a standalone alias reference.
|
|
132
104
|
if (IDENT_START.test(ch)) {
|
|
133
105
|
let j = i + 1;
|
|
134
106
|
while (j < n && IDENT_PART.test(value[j])) j++;
|
|
@@ -160,10 +132,6 @@ export const rewriteHandlerAliases = (value, aliasSet) => {
|
|
|
160
132
|
return out;
|
|
161
133
|
};
|
|
162
134
|
|
|
163
|
-
// Walk up from `el` to the nearest ancestor stamped with a scope that defines
|
|
164
|
-
// `name`, returning the live value. Returns undefined if no enclosing loop
|
|
165
|
-
// defines the alias. Nested loops resolve naturally: the innermost stamp is hit
|
|
166
|
-
// first; an outer alias is found by continuing up past inner stamps.
|
|
167
135
|
export const resolveScope = (el, name) => {
|
|
168
136
|
let node = el;
|
|
169
137
|
while (node) {
|
|
@@ -174,16 +142,6 @@ export const resolveScope = (el, name) => {
|
|
|
174
142
|
return undefined;
|
|
175
143
|
};
|
|
176
144
|
|
|
177
|
-
// Re-stamp the instance scope onto conditional-branch roots mounted inside a
|
|
178
|
-
// row. A `<!-- if -->` within a loop mounts its branch content separately from
|
|
179
|
-
// the iteration's own clonedNodes and stamps it once, at mount time
|
|
180
|
-
// (conditionals.js). When the row later updates in place — its DOM node reused
|
|
181
|
-
// for a new item — the iteration refreshes its own root stamp, but the branch
|
|
182
|
-
// root keeps the stale mount-time stamp, and `resolveScope` hits that first on
|
|
183
|
-
// the walk up (so a handler inside the conditional resolves the previous item).
|
|
184
|
-
// Walking the instance's parsed tree and re-stamping every active conditional
|
|
185
|
-
// branch root with the same fresh `scope` object closes that gap. Nested loops
|
|
186
|
-
// are skipped: each iteration node manages its own instances' scopes.
|
|
187
145
|
const restampConditionalBranches = (tree, scope) => {
|
|
188
146
|
if (!tree || typeof tree !== 'object') return;
|
|
189
147
|
if (tree.type === 'iteration') return;
|
|
@@ -203,22 +161,11 @@ const restampConditionalBranches = (tree, scope) => {
|
|
|
203
161
|
}
|
|
204
162
|
};
|
|
205
163
|
|
|
206
|
-
// Stamp the in-scope loop vars onto every iteration instance's root element
|
|
207
|
-
// node(s). The stamp accumulates the enclosing loop vars (`parentScope`) plus
|
|
208
|
-
// this loop's item/index, so a single innermost stamp resolves every alias in
|
|
209
|
-
// scope — which is what makes arbitrarily nested `each`/`if` combinations work
|
|
210
|
-
// even when a loop's template is purely another loop (no wrapper element to walk
|
|
211
|
-
// up to). Re-applied after each render and update so a stamp always reflects the
|
|
212
|
-
// current item/index — including after keyed reorders, where instance objects
|
|
213
|
-
// keep current `.item`/`.index`.
|
|
214
164
|
export const stampInstanceScopes = (iterationNode, parentScope = {}) => {
|
|
215
165
|
const { itemAlias, indexAlias } = iterationNode.meta;
|
|
216
166
|
const instances = iterationNode.runtime.instances;
|
|
217
167
|
for (let k = 0; k < instances.length; k++) {
|
|
218
168
|
const inst = instances[k];
|
|
219
|
-
// `liveItem` (set by stampScopes in iterate.js) is the live `$`-proxy
|
|
220
|
-
// element so handlers get the identity the app sees; `item` is the plain
|
|
221
|
-
// diff-snapshot clone used for rendering. Prefer live when available.
|
|
222
169
|
const item = inst.liveItem !== undefined ? inst.liveItem : inst.item;
|
|
223
170
|
const scope = { ...parentScope, [itemAlias]: item, [indexAlias]: inst.index };
|
|
224
171
|
const roots = inst.clonedNodes || (inst.element ? [inst.element] : []);
|
|
@@ -226,15 +173,10 @@ export const stampInstanceScopes = (iterationNode, parentScope = {}) => {
|
|
|
226
173
|
const node = roots[r];
|
|
227
174
|
if (node && node.nodeType === 1) node.__vibeScope = scope;
|
|
228
175
|
}
|
|
229
|
-
// Conditional branches inside the row carry their own stamp from mount time;
|
|
230
|
-
// refresh them with the same fresh scope so in-place row updates don't leave
|
|
231
|
-
// a handler inside an `<!-- if -->` resolving the previous item.
|
|
232
176
|
if (inst.tree) restampConditionalBranches(inst.tree, scope);
|
|
233
177
|
}
|
|
234
178
|
};
|
|
235
179
|
|
|
236
|
-
// Install the global `$scope` resolver so native inline handlers (which run in
|
|
237
|
-
// global scope at fire time) can call it. Idempotent across boots.
|
|
238
180
|
export const installScopeResolver = () => {
|
|
239
181
|
globalThis.$scope = resolveScope;
|
|
240
182
|
};
|
package/runtime/manifest.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
// The manifest is the one-way dotPath→element map the runtime hangs DOM
|
|
2
|
-
// bookkeeping on. Three hot paths need the REVERSE direction (element→path):
|
|
3
|
-
// conditional branch mounts and the mutation observer's add/remove tracking.
|
|
4
|
-
// Scanning Object.entries per lookup is O(manifest) per node — quadratic as
|
|
5
|
-
// pages grow, and the dominant cost of mounting conditional-heavy pages. A
|
|
6
|
-
// WeakMap rides on the manifest object (non-enumerable, like __live/__tree)
|
|
7
|
-
// and is maintained by every write; lookups validate against the forward map
|
|
8
|
-
// so a deleted entry can never resolve stale.
|
|
9
1
|
const indexOf = (manifest) => {
|
|
10
2
|
if (!manifest.__paths) {
|
|
11
3
|
Object.defineProperty(manifest, '__paths', {
|
|
@@ -17,13 +9,6 @@ const indexOf = (manifest) => {
|
|
|
17
9
|
return manifest.__paths;
|
|
18
10
|
};
|
|
19
11
|
|
|
20
|
-
// Single write point: forward map + reverse index together. Non-object
|
|
21
|
-
// "elements" (text nodes stored as strings, null placeholders) can't be
|
|
22
|
-
// WeakMap keys and are never reverse-looked-up — forward map only. An
|
|
23
|
-
// element can legitimately hold MORE than one path (its own node plus an
|
|
24
|
-
// inlined-slot alias, branch-graft re-registrations); the index keeps every
|
|
25
|
-
// registered path shortest-first, so lookups answer with the subtree ROOT —
|
|
26
|
-
// removal-time pruning must sweep the whole region, not a deeper alias.
|
|
27
12
|
export const setManifestEntry = (manifest, path, element) => {
|
|
28
13
|
manifest[path] = element;
|
|
29
14
|
if (element !== null && typeof element === 'object') {
|
|
@@ -39,10 +24,6 @@ export const setManifestEntry = (manifest, path, element) => {
|
|
|
39
24
|
}
|
|
40
25
|
};
|
|
41
26
|
|
|
42
|
-
// O(paths-per-element) reverse lookup, validated: entries whose forward
|
|
43
|
-
// mapping was deleted (branch unmounts, subtree pruning) compact away; the
|
|
44
|
-
// first path the forward map still agrees with — the shortest live one —
|
|
45
|
-
// wins. Null when none survive.
|
|
46
27
|
export const manifestPathOf = (manifest, element) => {
|
|
47
28
|
if (element === null || typeof element !== 'object') return null;
|
|
48
29
|
const paths = manifest.__paths?.get(element);
|
|
@@ -57,14 +38,6 @@ export const manifestPathOf = (manifest, element) => {
|
|
|
57
38
|
return null;
|
|
58
39
|
};
|
|
59
40
|
|
|
60
|
-
// Removal counterpart of addToManifest (index.js): drop the root entry and
|
|
61
|
-
// every entry under its dot-scope. A PREFIX SWEEP over the manifest keys, not
|
|
62
|
-
// a tree walk — branch mounts register alias paths a tree walk can't reach,
|
|
63
|
-
// and the sweep stays correct through any tree/manifest drift. Deleting only
|
|
64
|
-
// the root entry leaked the rest: an SPA outlet swap left ~4,400
|
|
65
|
-
// detached-element entries behind per navigation, pinning the DOM of every
|
|
66
|
-
// page ever visited. The manifest stays bounded, so O(manifest) per removal
|
|
67
|
-
// batch is cheap. An empty root path is never a legitimate removal target.
|
|
68
41
|
export const removeManifestSubtree = (manifest, dotPath) => {
|
|
69
42
|
if (!dotPath) return;
|
|
70
43
|
delete manifest[dotPath];
|