@ape-egg/vibe 1.9.9 → 2.0.5
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 +84 -0
- package/README.md +1 -1
- package/ROADMAP.md +45 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +1 -1
- package/compiler/src/Cargo.toml +1 -1
- package/compiler/src/compiler/compile.rs +100 -13
- package/compiler/src/compiler/iteration_optimizer.rs +188 -112
- package/compiler/src/compiler/manifest_builder.rs +5 -9
- package/compiler/src/parser/html.rs +194 -31
- package/package.json +1 -1
- package/runtime/cleanup.js +12 -3
- package/runtime/component.js +130 -43
- package/runtime/conditionals.js +16 -9
- package/runtime/hydrate.js +12 -3
- package/runtime/index.js +23 -2
- package/runtime/iterate.js +112 -22
- package/runtime/loop-scope.js +49 -3
- package/runtime/parse.js +8 -0
- package/runtime/pre-compiled-manifest.js +56 -155
- package/runtime/utils.js +12 -2
package/runtime/loop-scope.js
CHANGED
|
@@ -51,8 +51,11 @@ const copyString = (value, i, push) => {
|
|
|
51
51
|
// Rewrite standalone references to loop-variable aliases inside an event-handler
|
|
52
52
|
// expression into `$scope(this,'alias')` calls. Skips `@[...]` binding spans
|
|
53
53
|
// (they keep their existing hydrate-time stringifying behavior), string literals,
|
|
54
|
-
//
|
|
55
|
-
//
|
|
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.
|
|
56
59
|
export const rewriteHandlerAliases = (value, aliasSet) => {
|
|
57
60
|
if (!aliasSet || aliasSet.size === 0 || typeof value !== 'string') return value;
|
|
58
61
|
|
|
@@ -62,6 +65,8 @@ export const rewriteHandlerAliases = (value, aliasSet) => {
|
|
|
62
65
|
};
|
|
63
66
|
let i = 0;
|
|
64
67
|
const n = value.length;
|
|
68
|
+
const frames = [{ bracket: '', ternaries: 0 }];
|
|
69
|
+
const frame = () => frames[frames.length - 1];
|
|
65
70
|
|
|
66
71
|
while (i < n) {
|
|
67
72
|
const ch = value[i];
|
|
@@ -92,6 +97,37 @@ export const rewriteHandlerAliases = (value, aliasSet) => {
|
|
|
92
97
|
continue;
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
101
|
+
frames.push({ bracket: ch, ternaries: 0 });
|
|
102
|
+
push(ch);
|
|
103
|
+
i++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
107
|
+
if (frames.length > 1) frames.pop();
|
|
108
|
+
push(ch);
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ch === '?') {
|
|
113
|
+
const next = value[i + 1];
|
|
114
|
+
if (next === '.' || next === '?') {
|
|
115
|
+
push(ch + next);
|
|
116
|
+
i += 2;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
frame().ternaries++;
|
|
120
|
+
push(ch);
|
|
121
|
+
i++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (ch === ':') {
|
|
125
|
+
if (frame().ternaries > 0) frame().ternaries--;
|
|
126
|
+
push(ch);
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
95
131
|
// Identifier — rewrite when it's a standalone alias reference.
|
|
96
132
|
if (IDENT_START.test(ch)) {
|
|
97
133
|
let j = i + 1;
|
|
@@ -99,7 +135,17 @@ export const rewriteHandlerAliases = (value, aliasSet) => {
|
|
|
99
135
|
const ident = value.slice(i, j);
|
|
100
136
|
const isMember = lastNonSpace(out) === '.';
|
|
101
137
|
if (!isMember && aliasSet.has(ident)) {
|
|
102
|
-
|
|
138
|
+
let k = j;
|
|
139
|
+
while (k < n && /\s/.test(value[k])) k++;
|
|
140
|
+
const next = value[k] || '';
|
|
141
|
+
const prev = lastNonSpace(out);
|
|
142
|
+
const inObject = frame().bracket === '{';
|
|
143
|
+
const isKey = inObject && next === ':' && frame().ternaries === 0;
|
|
144
|
+
const isShorthand =
|
|
145
|
+
inObject && (prev === '{' || prev === ',') && (next === ',' || next === '}');
|
|
146
|
+
if (isKey) push(ident);
|
|
147
|
+
else if (isShorthand) push(`${ident}: $scope(this,'${ident}')`);
|
|
148
|
+
else push(`$scope(this,'${ident}')`);
|
|
103
149
|
} else {
|
|
104
150
|
push(ident);
|
|
105
151
|
}
|
package/runtime/parse.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
DOM_ELEMENT_PROPERTIES,
|
|
8
8
|
DEHYDRATE_CLASS_OR_ATTR,
|
|
9
9
|
THIS_PROP_REGEX,
|
|
10
|
+
STATE_THIS_PROP_REGEX,
|
|
10
11
|
} from './constants.js';
|
|
11
12
|
import { rewriteHandlerAliases } from './loop-scope.js';
|
|
12
13
|
|
|
@@ -57,6 +58,13 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
57
58
|
if (v.includes('this.')) {
|
|
58
59
|
const componentId = findComponentIdForElement(element);
|
|
59
60
|
if (componentId) {
|
|
61
|
+
// `$.this.X` (component-state write through the root) must be
|
|
62
|
+
// consumed as one reference BEFORE the bare `this.X` pass — that
|
|
63
|
+
// pass alone would leave the `$.` prefix behind and produce
|
|
64
|
+
// `$.$['id'].X`. Runtime-fetched components arrive with this form
|
|
65
|
+
// already rewritten by component.js; compiled pages inline the
|
|
66
|
+
// authored form, so parse meets it raw.
|
|
67
|
+
v = v.replace(STATE_THIS_PROP_REGEX, (_, prop) => `$['${componentId}'].${prop}`);
|
|
60
68
|
v = v.replace(THIS_PROP_REGEX, (match, prop) =>
|
|
61
69
|
DOM_ELEMENT_PROPERTIES.has(prop) ? match : `$['${componentId}'].${prop}`,
|
|
62
70
|
);
|
|
@@ -374,172 +374,73 @@ export const restoreMarkersFromManifest = (
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
//
|
|
378
|
-
//
|
|
377
|
+
// Restore iterations/conditionals at THIS level by their childNodes index.
|
|
378
|
+
// The manifest key encodes the start comment's pre-stamp position
|
|
379
|
+
// (`conditional_13` = element.childNodes[13]); processing in ascending
|
|
380
|
+
// order keeps every index valid, because each restoration returns its
|
|
381
|
+
// region to the exact pre-stamp node count before the next index is
|
|
382
|
+
// consulted. Locating by index — not by expression text — is what
|
|
383
|
+
// disambiguates sibling directives that share the same expression.
|
|
379
384
|
if (tree.children) {
|
|
380
|
-
|
|
385
|
+
const directives = Object.keys(tree.children)
|
|
386
|
+
.filter((key) => {
|
|
387
|
+
const type = tree.children[key].type;
|
|
388
|
+
return type === "iteration" || type === "conditional";
|
|
389
|
+
})
|
|
390
|
+
.map((key) => ({ key, index: Number(key.match(/_(\d+)$/)?.[1]) }))
|
|
391
|
+
.filter(({ index }) => Number.isInteger(index))
|
|
392
|
+
.sort((a, b) => a.index - b.index);
|
|
393
|
+
|
|
394
|
+
for (const { key, index } of directives) {
|
|
381
395
|
const childTree = tree.children[key];
|
|
396
|
+
const restoration = childTree.compiled?.restoration;
|
|
382
397
|
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
// Find iteration comment by matching the expression
|
|
388
|
-
let startComment = null;
|
|
389
|
-
let endComment = null;
|
|
390
|
-
let depth = 0;
|
|
391
|
-
|
|
392
|
-
const walker = document.createTreeWalker(
|
|
393
|
-
element,
|
|
394
|
-
NodeFilter.SHOW_COMMENT,
|
|
395
|
-
);
|
|
396
|
-
while (walker.nextNode()) {
|
|
397
|
-
const comment = walker.currentNode;
|
|
398
|
-
const trimmed = comment.textContent.trim();
|
|
399
|
-
|
|
400
|
-
// Skip already processed iterations
|
|
401
|
-
if (comment._vibeProcessed) continue;
|
|
402
|
-
|
|
403
|
-
// Match by expression: "each items as item, i"
|
|
404
|
-
const expectedComment = `each ${restoration.expression}`;
|
|
405
|
-
if (trimmed === expectedComment && !startComment) {
|
|
406
|
-
startComment = comment;
|
|
407
|
-
depth = 1;
|
|
408
|
-
} else if (startComment) {
|
|
409
|
-
if (trimmed.startsWith("each ")) {
|
|
410
|
-
depth++;
|
|
411
|
-
} else if (trimmed === "/each") {
|
|
412
|
-
depth--;
|
|
413
|
-
if (depth === 0) {
|
|
414
|
-
endComment = comment;
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Always delete the iteration node from tree - runtime will re-create it
|
|
422
|
-
// Do this even if comments weren't found (they might have been removed by parent restoration)
|
|
423
|
-
delete tree.children[key];
|
|
424
|
-
|
|
425
|
-
if (startComment && endComment) {
|
|
426
|
-
// Mark as processed
|
|
427
|
-
startComment._vibeProcessed = true;
|
|
428
|
-
endComment._vibeProcessed = true;
|
|
429
|
-
|
|
430
|
-
const parent = startComment.parentNode;
|
|
431
|
-
const insertionPoint = endComment.nextSibling;
|
|
398
|
+
// Always delete the directive node from the tree — the runtime
|
|
399
|
+
// re-creates it when it parses the restored DOM.
|
|
400
|
+
delete tree.children[key];
|
|
432
401
|
|
|
433
|
-
|
|
434
|
-
let current = startComment.nextSibling;
|
|
435
|
-
while (current && current !== endComment) {
|
|
436
|
-
const next = current.nextSibling;
|
|
437
|
-
if (current.nodeType !== Node.COMMENT_NODE) {
|
|
438
|
-
current.remove();
|
|
439
|
-
}
|
|
440
|
-
current = next;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Insert the template (single item)
|
|
444
|
-
const tempContainer = document.createElement("div");
|
|
445
|
-
tempContainer.innerHTML = restoration.template;
|
|
446
|
-
|
|
447
|
-
const fragment = document.createDocumentFragment();
|
|
448
|
-
while (tempContainer.firstChild) {
|
|
449
|
-
fragment.appendChild(tempContainer.firstChild);
|
|
450
|
-
}
|
|
451
|
-
parent.insertBefore(fragment, endComment);
|
|
452
|
-
}
|
|
402
|
+
if (!restoration?.template) continue;
|
|
453
403
|
|
|
404
|
+
const startComment = element.childNodes[index];
|
|
405
|
+
if (!startComment || startComment.nodeType !== Node.COMMENT_NODE) {
|
|
454
406
|
continue;
|
|
455
407
|
}
|
|
456
408
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
while (walker.nextNode()) {
|
|
475
|
-
const comment = walker.currentNode;
|
|
476
|
-
const text = comment.textContent.trim();
|
|
477
|
-
|
|
478
|
-
// Track if we're inside an iteration block
|
|
479
|
-
if (text.startsWith("each ")) {
|
|
480
|
-
insideIteration = true;
|
|
481
|
-
continue;
|
|
482
|
-
} else if (text === "/each") {
|
|
483
|
-
insideIteration = false;
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Skip conditionals inside iterations - runtime will handle them
|
|
488
|
-
if (insideIteration) continue;
|
|
489
|
-
|
|
490
|
-
// Skip already processed conditionals
|
|
491
|
-
if (comment._vibeProcessed) continue;
|
|
492
|
-
|
|
493
|
-
if (text.startsWith("if")) {
|
|
494
|
-
if (!startComment) {
|
|
495
|
-
const expression = childTree.meta?.expression;
|
|
496
|
-
const expectedText = expression ? "if " + expression : null;
|
|
497
|
-
if (expectedText && text === expectedText) {
|
|
498
|
-
startComment = comment;
|
|
499
|
-
conditionalDepth = 1;
|
|
500
|
-
}
|
|
501
|
-
} else {
|
|
502
|
-
// Track nested conditionals
|
|
503
|
-
conditionalDepth++;
|
|
504
|
-
}
|
|
505
|
-
} else if (text === "/if" && startComment) {
|
|
506
|
-
conditionalDepth--;
|
|
507
|
-
if (conditionalDepth === 0) {
|
|
508
|
-
endComment = comment;
|
|
509
|
-
break;
|
|
510
|
-
}
|
|
409
|
+
// Find the matching end marker at this sibling level, depth-counted
|
|
410
|
+
// so nested same-kind directives inside the region don't end it early.
|
|
411
|
+
const isIteration = childTree.type === "iteration";
|
|
412
|
+
const openPrefix = isIteration ? "each " : "if ";
|
|
413
|
+
const closeMarker = isIteration ? "/each" : "/if";
|
|
414
|
+
let depth = 0;
|
|
415
|
+
let endComment = null;
|
|
416
|
+
for (let cur = startComment.nextSibling; cur; cur = cur.nextSibling) {
|
|
417
|
+
if (cur.nodeType !== Node.COMMENT_NODE) continue;
|
|
418
|
+
const text = cur.textContent.trim();
|
|
419
|
+
if (text.startsWith(openPrefix)) {
|
|
420
|
+
depth++;
|
|
421
|
+
} else if (text === closeMarker) {
|
|
422
|
+
if (depth === 0) {
|
|
423
|
+
endComment = cur;
|
|
424
|
+
break;
|
|
511
425
|
}
|
|
426
|
+
depth--;
|
|
512
427
|
}
|
|
513
|
-
|
|
514
|
-
// Always delete the conditional node from tree - runtime will re-create it
|
|
515
|
-
// Do this even if comments weren't found (they might have been removed by parent restoration)
|
|
516
|
-
delete tree.children[key];
|
|
517
|
-
|
|
518
|
-
if (startComment && endComment) {
|
|
519
|
-
// Mark as processed
|
|
520
|
-
startComment._vibeProcessed = true;
|
|
521
|
-
endComment._vibeProcessed = true;
|
|
522
|
-
|
|
523
|
-
// Remove ALL pre-rendered content between start and end comments
|
|
524
|
-
// This includes the <!-- else --> marker from compiled HTML
|
|
525
|
-
let current = startComment.nextSibling;
|
|
526
|
-
while (current && current !== endComment) {
|
|
527
|
-
const next = current.nextSibling;
|
|
528
|
-
current.remove(); // Remove ALL nodes, including comment nodes
|
|
529
|
-
current = next;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Insert the template content BEFORE endComment
|
|
533
|
-
const tempContainer = document.createElement("div");
|
|
534
|
-
tempContainer.innerHTML = restoration.template;
|
|
535
|
-
|
|
536
|
-
const fragment = document.createDocumentFragment();
|
|
537
|
-
while (tempContainer.firstChild) {
|
|
538
|
-
fragment.appendChild(tempContainer.firstChild);
|
|
539
|
-
}
|
|
540
|
-
endComment.parentNode.insertBefore(fragment, endComment);
|
|
541
|
-
}
|
|
542
428
|
}
|
|
429
|
+
if (!endComment) continue;
|
|
430
|
+
|
|
431
|
+
// Drop the stamped content (including the <!-- else --> marker and
|
|
432
|
+
// any stamped-row comments) and re-insert the pre-stamp template —
|
|
433
|
+
// node-for-node identical to what the manifest was built against.
|
|
434
|
+
let current = startComment.nextSibling;
|
|
435
|
+
while (current && current !== endComment) {
|
|
436
|
+
const next = current.nextSibling;
|
|
437
|
+
current.remove();
|
|
438
|
+
current = next;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const tpl = document.createElement("template");
|
|
442
|
+
tpl.innerHTML = restoration.template;
|
|
443
|
+
endComment.parentNode.insertBefore(tpl.content, endComment);
|
|
543
444
|
}
|
|
544
445
|
}
|
|
545
446
|
|
package/runtime/utils.js
CHANGED
|
@@ -46,13 +46,23 @@ export const setRootState = (proxy) => { rootProxy = proxy; };
|
|
|
46
46
|
// delegate to the live root (their target is `$`), so they need no wrapper.
|
|
47
47
|
const dollarCache = new WeakMap();
|
|
48
48
|
const RESERVED_PROBE = 'unsafe'; // reserved method present on root-backed states, absent on plain snapshots
|
|
49
|
+
// The fallback serves ONLY the root's non-enumerable helper methods. A state
|
|
50
|
+
// key that is merely missing from this snapshot must read as undefined — the
|
|
51
|
+
// old/new snapshot diff depends on it. Letting it leak through to the live
|
|
52
|
+
// root would make both sides of the diff read the same current value (e.g. a
|
|
53
|
+
// component state bucket registered mid-cycle), silently defeating change
|
|
54
|
+
// detection.
|
|
55
|
+
const rootHelper = (k) => {
|
|
56
|
+
const desc = Object.getOwnPropertyDescriptor(rootProxy, k);
|
|
57
|
+
return desc && !desc.enumerable;
|
|
58
|
+
};
|
|
49
59
|
const dollarFor = (state) => {
|
|
50
60
|
if (!rootProxy || state === rootProxy || RESERVED_PROBE in state) return state;
|
|
51
61
|
let wrapped = dollarCache.get(state);
|
|
52
62
|
if (!wrapped) {
|
|
53
63
|
wrapped = new Proxy(state, {
|
|
54
|
-
get: (t, k) => (k in t ? t[k] : rootProxy[k]),
|
|
55
|
-
has: (t, k) => k in t || k
|
|
64
|
+
get: (t, k) => (k in t ? t[k] : rootHelper(k) ? rootProxy[k] : undefined),
|
|
65
|
+
has: (t, k) => k in t || rootHelper(k),
|
|
56
66
|
});
|
|
57
67
|
dollarCache.set(state, wrapped);
|
|
58
68
|
}
|