@ape-egg/vibe 4.2.1 → 4.3.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vibe
2
2
 
3
- **Version 4.2.1** — Runtime-first reactivity for plain HTML. Drop a script tag into any page and get reactive bindings, control flow, and URL-loaded components — no build step required. Compile later if you want; the compiler is a separate, optional package (`@ape-egg/vibe-compiler`).
3
+ **Version 4.3.1** — Runtime-first reactivity for plain HTML. Drop a script tag into any page and get reactive bindings, control flow, and URL-loaded components — no build step required. Compile later if you want; the compiler is a separate, optional package (`@ape-egg/vibe-compiler`).
4
4
 
5
5
  ## Security model & CSP
6
6
 
@@ -291,7 +291,7 @@ A standalone client-side router built on one contract: `$.page = { path, route,
291
291
 
292
292
  **Keyed outlet**: `key` on a fetched `<component>` declares its identity — when the resolved key changes, the component remounts even if `src` is unchanged. With `key="@[page.path]"`, param→param navigation on the same route (`/brawlers/0 → /brawlers/1` — same fragment src) mounts fresh, exactly like an MPA reload on the new URL. `src` and `key` are the wrapper's own contract and are never passed to the component as props.
293
293
 
294
- **Link claiming**: one document-level click listener claims same-origin, unmodified, untargeted clicks whose pathname matches a **real** route — pushState + a fresh `$.page` assignment + `document.title` swap when the route carries one, scroll to top. Everything else navigates natively: other origins, modified clicks, `target`/`download` links, same-page hash anchors, and unrouted paths — which is what makes mixed MPA/SPA output work. `'*'` never claims a click; it only resolves deep-link entries and popstate. `popstate` re-resolves (including `'*'`) without scrolling — the browser restores position.
294
+ **Link claiming**: one document-level click listener claims same-origin, unmodified, untargeted clicks whose pathname matches a **real** route — pushState + a fresh `$.page` assignment + `document.title` swap when the route carries one, scroll to top. The entry route gets the same title swap when `setupSpa` runs, so a deep link never shows the shell's title. Everything else navigates natively: other origins, modified clicks, `target`/`download` links, same-page hash anchors, and unrouted paths — which is what makes mixed MPA/SPA output work. `'*'` never claims a click; it only resolves deep-link entries and popstate. `popstate` re-resolves (including `'*'`) without scrolling — the browser restores position.
295
295
 
296
296
  **No match, no `'*'`**: `$.page.src` stays unset and the outlet mounts nothing. There is no built-in 404.
297
297
 
@@ -312,7 +312,7 @@ Pre-boot, state accumulates and boot is queued as always — on a fresh document
312
312
 
313
313
  ### Subtree Reconciliation (advanced)
314
314
 
315
- `$.reconcile(el, html)` and `$.renderComponent(rawHtml, props, slot, opts)` are public-but-advanced APIs used by the vite plugin's HMR path. Their shape may evolve; treat them as plumbing rather than application code for now.
315
+ `$.reconcile(el, html)` is the template-aware subtree reconciliation the vite plugin's HMR path runs on: the fresh `html` is parsed by the same parser that built the page, and every paired node updates the runtime's knowledge before the DOM — a changed text or attribute template is re-hydrated in place, an iteration or conditional swaps its stored template so later renders follow it, structural inserts are hydrated under the right scope — while DOM identity, focus, selection and scroll position survive. The underscore-prefixed helpers beside it (`$._refreshComponent(el, rawHtml)`, `$._remountComponent(el)`, `$._renderComponent(...)`) are tooling plumbing, not application API; all of it may still evolve.
316
316
 
317
317
  ### Dehydrate
318
318
 
@@ -525,7 +525,7 @@ The pages tree becomes three things:
525
525
 
526
526
  1. **Page fragments** at `/components/vibe-spa/<pages-relative-path>` — each page's body content, with its head `<style>` tags prepended and its `<script type="module">` carried along byte-identical. No rewrites: `vibe()` itself applies defaults semantics once booted (see App-Lifetime State Defaults above), so page state gets app-lifetime behavior with the page's own import.
527
527
  2. **A generated route table** — `pages/brawlers/$index.html` → `{ route: '/brawlers/:index', src: '/components/vibe-spa/brawlers/$index.html', title: <harvested from the page's <title>> }`. `$param` → `:param`, terminal `$$name` → `:name*`, terminal `index` serves its directory path. Sorted most-specific-first.
528
- 3. **A composed shell** at the output root `/index.html` — plain runtime-Vibe code: the deduped union of every page's head resources (`<meta>` keeps only the set common to all pages; page-specific meta is dropped with a verbose note), the `/` route's title, the union of page body attributes minus `vibe-fouc`, one generated boot script (`resolve` seeds `$.page`, `setupSpa` wires navigation — imports reuse the pages' own import style), and the keyed route outlet: `<component src="@[page.src]" key="@[page.path]"></component>` (a path change remounts the fragment even when the route — and so the src — is unchanged: param→param navigation mounts fresh, like the MPA reload it replaces).
528
+ 3. **A composed shell** at the output root `/index.html` — plain runtime-Vibe code: the deduped union of every page's head resources (`<meta>` keeps only the set common to all pages; page-specific meta is dropped with a verbose note), the `/` route's title, the union of page body attributes minus `vibe-fouc`, a `window.__MANIFEST__` hint naming the shell's own hyperspeed manifest (the shell is the document at every route, so the runtime loads that one manifest directly instead of probing paths derived from the URL), one generated boot script (`resolve` seeds `$.page`, `setupSpa` wires navigation — imports reuse the pages' own import style), and the keyed route outlet: `<component src="@[page.src]" key="@[page.path]"></component>` (a path change remounts the fragment even when the route — and so the src — is unchanged: param→param navigation mounts fresh, like the MPA reload it replaces).
529
529
 
530
530
  **What SPA mode skips**: per-page stamped HTML, per-page manifests, and the `pages/` output directory. Fragments are runtime-parsed on mount (per-route lazy loading falls out of the reactive src for free); the shell itself gets a manifest but is deliberately left unstamped — it is served at every route path, so its first paint is location-dependent by nature. The shell ships **without** `vibe-fouc`: its absence is the compiled-mode marker (it gates hyperspeed manifest loading), and there is nothing to flash — the outlet is empty and fragments hydrate off-DOM before insertion.
531
531
 
@@ -539,7 +539,7 @@ The pages tree becomes three things:
539
539
  }
540
540
  ```
541
541
 
542
- Dev-server equivalents: any "history API fallback" option (`http-server` can't rewrite; `vite preview`, `serve -s`, and Caddy `try_files` all can).
542
+ Dev-server equivalents: any "history API fallback" option (`vite preview`, `serve -s`, Caddy `try_files`, or `http-server -P http://localhost:8080?` — the trailing `?` proxies unresolved paths to the root).
543
543
 
544
544
  **Watch mode**: `--spa --watch` re-runs the SPA pass on any pages-tree change — edited pages re-transform, added/removed pages resync the route table and prune orphan fragments, title/head edits recompose the shell, and the shell's manifest refreshes.
545
545
 
Binary file
package/llms.txt CHANGED
@@ -338,7 +338,7 @@ A Promise that resolves once vibe has finished initial parse + hydrate + compone
338
338
 
339
339
  ### `$.reconcile(el, html)` (advanced)
340
340
 
341
- Subtree reconciliation used by `@ape-egg/vite-plugin-vibe` for surgical HMR: diffs `el`'s children against fresh `html` while preserving DOM identity, focus, and selection; vibe-managed regions (iterations, conditionals, components, slot pairs) are treated as opaque. Treat as plumbing — shape may evolve. (Internal renders/registration live on underscore-prefixed `$` members and are not API.)
341
+ Template-aware subtree reconciliation used by `@ape-egg/vite-plugin-vibe` for surgical HMR: the fresh `html` is parsed by the same parser that built the page and diffed against `el`'s children together with the runtime's own tree, so a changed text or attribute template is re-hydrated in place, an iteration or conditional swaps its stored template (later renders follow it), and DOM identity, focus, selection and scroll position survive; component wrappers re-mount only when their src or props changed. Treat as plumbing — shape may evolve. (`$._refreshComponent`, `$._remountComponent` and other underscore-prefixed `$` members are tooling internals, not API.)
342
342
 
343
343
  ## Scoped Variables in Iterations
344
344
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "4.2.1",
3
+ "version": "4.3.1",
4
4
  "type": "module",
5
5
  "description": "Runtime-first reactivity for plain HTML \u2014 no build step, no virtual DOM, no new syntax to learn",
6
6
  "main": "index.js",
@@ -67,7 +67,7 @@ const nameBindingsOf = (tree) => {
67
67
  if (tree._nbSrc === tree.nameBindings && tree._nbEl === tree.element) return tree._nb;
68
68
  unsubscribeBindings(tree._nb);
69
69
  const out = [];
70
- for (const nameBinding of tree.nameBindings) {
70
+ for (const nameBinding of tree.nameBindings ?? []) {
71
71
  BINDING_REGEX.lastIndex = 0;
72
72
  let m;
73
73
  while ((m = BINDING_REGEX.exec(nameBinding))) {
@@ -87,6 +87,61 @@ const evaluateCondition = (expression, state, element = null) =>
87
87
  export const nodeSubscriberOf = (tree, kind) =>
88
88
  (tree._sub ??= { kind, node: tree, anchor: tree.meta.startComment });
89
89
 
90
+ export const ownBindingsOf = (tree, scope) => {
91
+ const out = [];
92
+ for (const m of textBindingsOf(tree)) {
93
+ out.push({
94
+ matchOuter: m.outer,
95
+ matchInner: m.inner,
96
+ input: m.input,
97
+ element: tree.element,
98
+ textNode: tree.textNode,
99
+ scopedState: scope,
100
+ binding: m,
101
+ });
102
+ }
103
+ for (const { attrName, matches } of attrBindingsOf(tree)) {
104
+ const attrValue = tree.attributes[attrName];
105
+ if (!matches.length) {
106
+ out.push({
107
+ type: "attribute",
108
+ attrName,
109
+ attrValue,
110
+ matchOuter: null,
111
+ matchInner: null,
112
+ element: tree.element,
113
+ scopedState: scope,
114
+ binding: null,
115
+ });
116
+ continue;
117
+ }
118
+ for (const m of matches) {
119
+ out.push({
120
+ type: "attribute",
121
+ attrName,
122
+ attrValue,
123
+ matchOuter: m.outer,
124
+ matchInner: m.inner,
125
+ element: tree.element,
126
+ scopedState: scope,
127
+ binding: m,
128
+ });
129
+ }
130
+ }
131
+ for (const m of nameBindingsOf(tree)) {
132
+ out.push({
133
+ type: "nameBinding",
134
+ nameBinding: m.nameBinding,
135
+ matchOuter: m.outer,
136
+ matchInner: m.inner,
137
+ element: tree.element,
138
+ scopedState: scope,
139
+ binding: m,
140
+ });
141
+ }
142
+ return out;
143
+ };
144
+
90
145
  const isIdentChar = (c) =>
91
146
  (c >= "a" && c <= "z") ||
92
147
  (c >= "A" && c <= "Z") ||
@@ -262,6 +262,18 @@ export const dispatchConditional = (node, newState, manifest, parentScope = {})
262
262
  }
263
263
  };
264
264
 
265
+ export const remountBranch = (node, state, manifest, parentScope = {}) => {
266
+ if (!node.runtime.templateRemoved) return;
267
+ const sub = nodeSubscriberOf(node, 'conditional');
268
+ sub.lastScope = state;
269
+ beginTracking(sub, overlayKeysOf(state));
270
+ const result = evaluateCondition(node.meta.expression, state, node.meta.startComment?.parentElement);
271
+ endTracking();
272
+ const branch = result ? node.meta.branches.if : node.meta.branches.else;
273
+ mountBranch(node, branch, state, manifest, parentScope);
274
+ node.runtime.activeBranch = branch;
275
+ };
276
+
265
277
  export const updateConditional = (node, newState, oldState, manifest, parentScope = {}) => {
266
278
  const { expression, branches, startComment } = node.meta;
267
279
 
@@ -1,4 +1,4 @@
1
- export const VERSION = '4.2.1';
1
+ export const VERSION = '4.3.1';
2
2
 
3
3
  export const DEBUGGER_NAME = '[vibe-debug]:';
4
4
  export const FOUC_CLASS_OR_ATTR = 'vibe-fouc';
package/runtime/index.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import state, { silentSet } from './state.js';
2
2
  import parse from './parse.js';
3
- import createManifest, { setManifestEntry, manifestPathOf, removeManifestSubtree } from './manifest.js';
3
+ import createManifest, {
4
+ setManifestEntry,
5
+ manifestPathOf,
6
+ removeManifestSubtree,
7
+ findNodeByElement,
8
+ addToManifest,
9
+ } from './manifest.js';
4
10
  import hydrate from './hydrate.js';
5
11
  import affected from './affected.js';
6
12
  import { deepMerge, hash, setRootState } from './utils.js';
@@ -25,13 +31,13 @@ import {
25
31
  STAGED_ATTR,
26
32
  FOUC_CLASS_OR_ATTR,
27
33
  } from './constants.js';
28
- import { processComponent, abortComponentFetch, collectComponentIds, releaseOrphanedComponentState, renderComponentTemplate, executeCompiledComponentScripts } from './component.js';
34
+ import { processComponent, abortComponentFetch, collectComponentIds, releaseOrphanedComponentState, renderComponentTemplate, executeCompiledComponentScripts, forceRemount } from './component.js';
29
35
  import { configureComponentCache, clearComponentCache } from './component-cache.js';
30
36
  import { debugLog } from './debug.js';
31
37
  import { pruneDisconnected, beginFlush } from './tracking.js';
32
38
  import dispatchFlush from './dispatch.js';
33
39
  import { shouldCleanup, cleanup } from './cleanup.js';
34
- import { reconcile } from './reconcile.js';
40
+ import { reconcile, refreshComponent } from './reconcile.js';
35
41
  import {
36
42
  buildHyperspeedManifest,
37
43
  hyperspeedManifest,
@@ -66,38 +72,6 @@ const navigateTree = (tree, path) => {
66
72
  return path.split('.').filter(k => k).reduce((node, key) => node?.children?.[key], tree);
67
73
  };
68
74
 
69
- const findNodeByElement = (tree, target) => {
70
- if (!target) return null;
71
- const seen = new Set();
72
- const walk = (node) => {
73
- if (!node || typeof node !== 'object' || seen.has(node)) return null;
74
- seen.add(node);
75
- if (node.element === target) return node;
76
- if (node.children) {
77
- for (const key in node.children) {
78
- const hit = walk(node.children[key]);
79
- if (hit) return hit;
80
- }
81
- }
82
- const branchTree = node.runtime?.activeInstance?.parsedTree;
83
- if (branchTree) {
84
- const hit = walk(branchTree);
85
- if (hit) return hit;
86
- }
87
- const instances = node.runtime?.instances;
88
- if (instances) {
89
- for (let i = 0; i < instances.length; i++) {
90
- if (instances[i]?.tree) {
91
- const hit = walk(instances[i].tree);
92
- if (hit) return hit;
93
- }
94
- }
95
- }
96
- return null;
97
- };
98
- return walk(tree);
99
- };
100
-
101
75
  const ensureNode = (tree, path) => {
102
76
  const keys = path.split('.').filter(k => k);
103
77
  return keys.reduce((node, key) => {
@@ -108,16 +82,6 @@ const ensureNode = (tree, path) => {
108
82
  }, tree);
109
83
  };
110
84
 
111
- const addToManifest = (tree, manifest, dotPath) => {
112
- setManifestEntry(manifest, dotPath, tree.element);
113
-
114
- if (tree.children && Object.keys(tree.children).length > 0) {
115
- Object.keys(tree.children).forEach((childKey) => {
116
- addToManifest(tree.children[childKey], manifest, `${dotPath}.${childKey}`);
117
- });
118
- }
119
- };
120
-
121
85
  const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) => {
122
86
  let hydratedCount = 0;
123
87
  let iteratedCount = 0;
@@ -593,7 +557,17 @@ const main = (s, config = {}, stringSelector = '') => {
593
557
  });
594
558
 
595
559
  Object.defineProperty($, 'reconcile', {
596
- value: reconcile,
560
+ value: (target, source) => reconcile(target, source, manifest),
561
+ enumerable: false,
562
+ });
563
+
564
+ Object.defineProperty($, '_refreshComponent', {
565
+ value: (el, rawHtml) => refreshComponent(el, rawHtml, manifest),
566
+ enumerable: false,
567
+ });
568
+
569
+ Object.defineProperty($, '_remountComponent', {
570
+ value: (el) => forceRemount(el, debug),
597
571
  enumerable: false,
598
572
  });
599
573
 
@@ -779,6 +753,7 @@ const main = (s, config = {}, stringSelector = '') => {
779
753
  const dotAnnotation = manifestPathOf(manifest, target);
780
754
 
781
755
  const parsedNode = parse(node);
756
+ node._vibeTree = parsedNode;
782
757
 
783
758
  if (parsedNode.stats?.skipped) {
784
759
  totalSkipped += parsedNode.stats.skipped;
@@ -690,6 +690,16 @@ const isolateInlinedComponentIds = (container) => {
690
690
  return true;
691
691
  };
692
692
 
693
+ export const prepareTemplateComponents = (container, scopedState) => {
694
+ const components = container.querySelectorAll(COMPONENT_SRC_SELECTOR);
695
+ const scopeKeys = overlayKeysOf(scopedState);
696
+ for (let i = 0; i < components.length; i++) {
697
+ const el = components[i];
698
+ if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
699
+ if (scopeKeys) resolveScopedSrc(el, scopedState, scopeKeys);
700
+ }
701
+ };
702
+
693
703
  export const initializeBlock = (templateNodes, scopedState, cachedTree = null, componentId = null, aliasSet = undefined, isolateComponents = false) => {
694
704
  let tree;
695
705
  let clonedNodes = [];
@@ -708,13 +718,7 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
708
718
  }
709
719
  }
710
720
 
711
- const components = parseContainer.querySelectorAll(COMPONENT_SRC_SELECTOR);
712
- const scopeKeys = overlayKeysOf(scopedState);
713
- for (let i = 0; i < components.length; i++) {
714
- const el = components[i];
715
- if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
716
- if (scopeKeys) resolveScopedSrc(el, scopedState, scopeKeys);
717
- }
721
+ prepareTemplateComponents(parseContainer, scopedState);
718
722
 
719
723
  if (isolateComponents && isolateInlinedComponentIds(parseContainer)) {
720
724
  executeCompiledComponentScriptsIn([...parseContainer.childNodes]);
@@ -1027,6 +1031,71 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
1027
1031
  stampScopes(iterationNode, manifest, parentScope);
1028
1032
  };
1029
1033
 
1034
+ export const swapIterationTemplate = (iterationNode, meta) => {
1035
+ const { arrayPath, itemAlias, indexAlias, keyExpr, hasScopedHandlers, scopeAliases, template } = meta;
1036
+ Object.assign(iterationNode.meta, { arrayPath, itemAlias, indexAlias, keyExpr, hasScopedHandlers, scopeAliases, template });
1037
+ delete iterationNode.compiled;
1038
+ const rt = iterationNode.runtime;
1039
+ rt.batchFn = null;
1040
+ rt.lastBatchHtml = undefined;
1041
+ rt.domPropertyWrites = null;
1042
+ rt.stateKeys = null;
1043
+ };
1044
+
1045
+ export const rerenderIteration = (iterationNode, state, manifest, parentScope = {}) => {
1046
+ if (!iterationNode.runtime.templateRemoved) return;
1047
+ const { arrayPath, startComment } = iterationNode.meta;
1048
+ const resolvedExpr = resolveThisPath(arrayPath, startComment.parentElement);
1049
+ const trackSub = nodeSubscriberOf(iterationNode, 'iteration');
1050
+ trackSub.lastScope = state;
1051
+ beginTracking(trackSub, overlayKeysOf(state));
1052
+ const array = evalInScope(resolvedExpr, state, startComment.parentElement) ?? resolvePath(state, resolvedExpr);
1053
+ endTracking();
1054
+ bulkReplace(iterationNode, Array.isArray(array) ? array : [], state, manifest, parentScope);
1055
+ };
1056
+
1057
+ export const instanceRange = (iterationNode, index) => {
1058
+ const parent = iterationNode.meta.startComment.parentNode;
1059
+ const start = findInstanceAnchor(iterationNode.runtime.instances[index], parent);
1060
+ const anchor = resolveInsertBefore(iterationNode, index + 1, parent);
1061
+ const nodes = [];
1062
+ for (let cur = start; cur && cur !== anchor; cur = cur.nextSibling) nodes.push(cur);
1063
+ return { nodes, anchor };
1064
+ };
1065
+
1066
+ export const patchTreelessInstances = (iterationNode, state, manifest, parentScope, patchRow) => {
1067
+ const { itemAlias, indexAlias, template, startComment } = iterationNode.meta;
1068
+ const rt = iterationNode.runtime;
1069
+ const array = rt.instances.map((inst) => inst.item);
1070
+
1071
+ if (!canUseBatchRender(template)) {
1072
+ bulkReplace(iterationNode, array, state, manifest, parentScope);
1073
+ return;
1074
+ }
1075
+
1076
+ const stateKeys = Object.keys(state);
1077
+ const compiledBatch = compileBatchFn(template, itemAlias, indexAlias, stateKeys, startComment.parentNode);
1078
+ rt.batchFn = compiledBatch.fn;
1079
+ rt.domPropertyWrites = compiledBatch.domPropertyWrites;
1080
+ rt.stateKeys = stateKeys;
1081
+ const html = rt.batchFn(array, ...stateKeys.map((k) => state[k]), state);
1082
+ rt.lastBatchHtml = html;
1083
+
1084
+ batchParseTemplate.innerHTML = html;
1085
+ const frag = batchParseTemplate.content;
1086
+ markBoundValues(frag, html);
1087
+ const fresh = [...frag.children];
1088
+ if (fresh.length !== rt.instances.length) {
1089
+ bulkReplace(iterationNode, array, state, manifest, parentScope);
1090
+ return;
1091
+ }
1092
+
1093
+ const freshInstances = fresh.map((element, i) => ({ element, item: array[i], index: i }));
1094
+ applyDomPropertyWrites(freshInstances, array, state, itemAlias, indexAlias, rt.domPropertyWrites);
1095
+ for (let i = 0; i < fresh.length; i++) patchRow(rt.instances[i].element, fresh[i]);
1096
+ stampScopes(iterationNode, manifest, parentScope);
1097
+ };
1098
+
1030
1099
  const releaseRemovedSubtrees = (removedRoots, manifest) => {
1031
1100
  if (!manifest || removedRoots.length === 0) return;
1032
1101
  const tree = manifest.__tree;
@@ -47,6 +47,48 @@ export const removeManifestSubtree = (manifest, dotPath) => {
47
47
  }
48
48
  };
49
49
 
50
+ export const findNodeByElement = (tree, target) => {
51
+ if (!target) return null;
52
+ const seen = new Set();
53
+ const walk = (node) => {
54
+ if (!node || typeof node !== 'object' || seen.has(node)) return null;
55
+ seen.add(node);
56
+ if (node.element === target) return node;
57
+ if (node.children) {
58
+ for (const key in node.children) {
59
+ const hit = walk(node.children[key]);
60
+ if (hit) return hit;
61
+ }
62
+ }
63
+ const branchTree = node.runtime?.activeInstance?.parsedTree;
64
+ if (branchTree) {
65
+ const hit = walk(branchTree);
66
+ if (hit) return hit;
67
+ }
68
+ const instances = node.runtime?.instances;
69
+ if (instances) {
70
+ for (let i = 0; i < instances.length; i++) {
71
+ if (instances[i]?.tree) {
72
+ const hit = walk(instances[i].tree);
73
+ if (hit) return hit;
74
+ }
75
+ }
76
+ }
77
+ return null;
78
+ };
79
+ return walk(tree);
80
+ };
81
+
82
+ export const addToManifest = (tree, manifest, dotPath) => {
83
+ setManifestEntry(manifest, dotPath, tree.element);
84
+
85
+ if (tree.children && Object.keys(tree.children).length > 0) {
86
+ Object.keys(tree.children).forEach((childKey) => {
87
+ addToManifest(tree.children[childKey], manifest, `${dotPath}.${childKey}`);
88
+ });
89
+ }
90
+ };
91
+
50
92
  const recursive = (tree, results, tagChain) => {
51
93
  setManifestEntry(results, tagChain.join('.'), tree.element);
52
94
 
package/runtime/parse.js CHANGED
@@ -26,6 +26,19 @@ import './this-scope.js';
26
26
 
27
27
  const findComponentIdForElement = (element) => closestComponentId(element);
28
28
 
29
+ export const rewriteHandler = (value, element, aliasSet) => {
30
+ let v = value;
31
+ if (v.includes('this.')) {
32
+ const componentId = findComponentIdForElement(element);
33
+ if (componentId) {
34
+ v = v.replace(STATE_THIS_PROP_REGEX, (_, prop) => `$['${componentId}'].${prop}`);
35
+ v = v.replace(THIS_PROP_REGEX, (_, prop) => `$this(this).${prop}`);
36
+ }
37
+ }
38
+ if (aliasSet && aliasSet.size > 0) v = rewriteHandlerAliases(v, aliasSet);
39
+ return v;
40
+ };
41
+
29
42
  const warnedStaticKeys = new Set();
30
43
  const warnStaticKey = (key) => {
31
44
  if (warnedStaticKeys.has(key)) return;
@@ -97,17 +110,7 @@ const captureAttributeBindings = (element, aliasSet) => {
97
110
  }
98
111
 
99
112
  if (attr.name.startsWith('on')) {
100
- let v = attr.value;
101
- if (v.includes('this.')) {
102
- const componentId = findComponentIdForElement(element);
103
- if (componentId) {
104
- v = v.replace(STATE_THIS_PROP_REGEX, (_, prop) => `$['${componentId}'].${prop}`);
105
- v = v.replace(THIS_PROP_REGEX, (_, prop) => `$this(this).${prop}`);
106
- }
107
- }
108
- if (aliasSet && aliasSet.size > 0) {
109
- v = rewriteHandlerAliases(v, aliasSet);
110
- }
113
+ const v = rewriteHandler(attr.value, element, aliasSet);
111
114
  if (v !== attr.value) element.setAttribute(attr.name, v);
112
115
  }
113
116