@ape-egg/vibe 1.0.3 → 1.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 +20 -0
- package/ROADMAP.md +289 -0
- package/affected.js +2 -10
- package/conditionals.js +2 -12
- package/hydrate.js +1 -13
- package/index.js +20 -5
- package/package.json +1 -1
- package/utils.js +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.5] - 2026-01-25
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Expression evaluation**: Normalize whitespace in `@[...]` expressions before evaluation, making bindings resilient to IDE auto-formatting that may break expressions across multiple lines
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **DRY refactor**: Consolidated expression evaluation into single `evalInScope()` function in utils.js, used by hydrate.js, conditionals.js, and affected.js
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## [1.0.4] - 2026-01-24
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **MutationObserver**: Use `takeRecords()` to preserve pending mutations before disconnecting during state changes, preventing queued DOM mutations (e.g., innerHTML replacements) from being lost when state updates occur simultaneously
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
3
23
|
## [1.0.3] - 2026-01-23
|
|
4
24
|
|
|
5
25
|
### Added
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Vibe Roadmap
|
|
2
|
+
|
|
3
|
+
Feature proposals and improvements for Vibe's runtime-first reactive framework.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Proposed: Manual Hydration API
|
|
8
|
+
|
|
9
|
+
**Status**: Proposal
|
|
10
|
+
**Priority**: Medium
|
|
11
|
+
**Category**: Core Runtime
|
|
12
|
+
|
|
13
|
+
### Problem
|
|
14
|
+
|
|
15
|
+
Vibe's MutationObserver automatically hydrates `@[bindings]` on initial page load and incremental DOM changes, but fails when developers perform wholesale DOM replacement via `innerHTML`:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// This doesn't trigger Vibe's hydration:
|
|
19
|
+
element.innerHTML = '<h1>Welcome, @[race]!</h1>';
|
|
20
|
+
// Result: Literal text "@[race]" instead of evaluated "human"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This happens because:
|
|
24
|
+
1. `innerHTML` replacement destroys all old DOM nodes and creates new ones
|
|
25
|
+
2. Vibe's MutationObserver is designed for incremental mutations, not complete replacement
|
|
26
|
+
3. No mechanism exists to manually trigger re-hydration
|
|
27
|
+
|
|
28
|
+
### Current Workaround
|
|
29
|
+
|
|
30
|
+
Users must manually evaluate bindings before setting innerHTML:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const evaluateBindings = (html) => {
|
|
34
|
+
return html.replace(/@\[([^\]]+)\]/g, (match, expression) => {
|
|
35
|
+
const keys = Object.keys(window.$);
|
|
36
|
+
const values = Object.values(window.$);
|
|
37
|
+
const result = new Function(...keys, `return ${expression}`)(...values);
|
|
38
|
+
return result ?? '';
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
element.innerHTML = evaluateBindings(html); // Manually evaluated
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Issues with this approach:**
|
|
46
|
+
- Not DRY - duplicates Vibe's internal evaluation logic
|
|
47
|
+
- Fragile - user's regex might not match Vibe's parser exactly
|
|
48
|
+
- Knowledge burden - users need to know when manual evaluation is needed
|
|
49
|
+
- Inconsistent - some bindings auto-hydrate, others need manual work
|
|
50
|
+
|
|
51
|
+
### Use Cases
|
|
52
|
+
|
|
53
|
+
This affects multiple real-world scenarios:
|
|
54
|
+
|
|
55
|
+
1. **Dynamic content replacement** (tutorials, articles, modals)
|
|
56
|
+
2. **Client-side routing** (replacing page sections with new HTML)
|
|
57
|
+
3. **Lazy-loaded sections** (loading HTML from server with bindings)
|
|
58
|
+
4. **Template cloning** (using `<template>` elements with `@[bindings]`)
|
|
59
|
+
5. **Server-sent HTML** (SSR-like patterns where server sends HTML with bindings)
|
|
60
|
+
|
|
61
|
+
### Proposed Solution
|
|
62
|
+
|
|
63
|
+
Add a manual hydration API that allows users to trigger Vibe's binding evaluation:
|
|
64
|
+
|
|
65
|
+
#### Option 1: Element Hydration
|
|
66
|
+
```js
|
|
67
|
+
vibe.hydrate(element);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Usage:**
|
|
71
|
+
```js
|
|
72
|
+
tutorial.innerHTML = '<h1>Welcome, @[race]!</h1>';
|
|
73
|
+
vibe.hydrate(tutorial); // Scan tutorial and children for @[bindings]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Pros:**
|
|
77
|
+
- Most flexible - works with any element
|
|
78
|
+
- Matches web component patterns (`connectedCallback()`)
|
|
79
|
+
- Clear intent - "scan this element"
|
|
80
|
+
|
|
81
|
+
**Cons:**
|
|
82
|
+
- Requires import/reference to vibe library
|
|
83
|
+
- Two-step process (set innerHTML, then hydrate)
|
|
84
|
+
|
|
85
|
+
#### Option 2: HTML String Evaluation
|
|
86
|
+
```js
|
|
87
|
+
const evaluated = vibe.evaluate(html, state);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Usage:**
|
|
91
|
+
```js
|
|
92
|
+
const html = '<h1>Welcome, @[race]!</h1>';
|
|
93
|
+
const evaluated = vibe.evaluate(html, window.$);
|
|
94
|
+
tutorial.innerHTML = evaluated;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Pros:**
|
|
98
|
+
- Pure function - easier to test
|
|
99
|
+
- Works without DOM access
|
|
100
|
+
- Can be used server-side or in workers
|
|
101
|
+
|
|
102
|
+
**Cons:**
|
|
103
|
+
- Users must manage state passing
|
|
104
|
+
- Doesn't handle nested/dynamic state updates
|
|
105
|
+
|
|
106
|
+
#### Option 3: Safe innerHTML Setter
|
|
107
|
+
```js
|
|
108
|
+
vibe.setHTML(element, html);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Usage:**
|
|
112
|
+
```js
|
|
113
|
+
vibe.setHTML(tutorial, '<h1>Welcome, @[race]!</h1>');
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Pros:**
|
|
117
|
+
- Single operation - set and hydrate in one call
|
|
118
|
+
- Matches platform APIs (`element.setHTML()`)
|
|
119
|
+
- Simplest API surface
|
|
120
|
+
|
|
121
|
+
**Cons:**
|
|
122
|
+
- Yet another setter abstraction
|
|
123
|
+
- Might conflict with future platform APIs
|
|
124
|
+
|
|
125
|
+
### Recommendation
|
|
126
|
+
|
|
127
|
+
**Implement Option 1** (`vibe.hydrate(element)`):
|
|
128
|
+
- Aligns with Vibe's runtime-first philosophy
|
|
129
|
+
- Gives users explicit control over hydration timing
|
|
130
|
+
- Most flexible for different scenarios
|
|
131
|
+
- Clear and predictable behavior
|
|
132
|
+
|
|
133
|
+
### Implementation Notes
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
// Expose on the state proxy:
|
|
137
|
+
window.$ = state({ race: 'human' });
|
|
138
|
+
window.$.vibe.hydrate(element); // Scan element for @[bindings]
|
|
139
|
+
|
|
140
|
+
// Or as a module export:
|
|
141
|
+
import state, { hydrate } from '@ape-egg/vibe';
|
|
142
|
+
hydrate(element);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Should support:
|
|
146
|
+
- Single element: `hydrate(tutorial)`
|
|
147
|
+
- Multiple elements: `hydrate([el1, el2])`
|
|
148
|
+
- Selector: `hydrate('tutorial')` (convenience)
|
|
149
|
+
|
|
150
|
+
### Related
|
|
151
|
+
|
|
152
|
+
Compare to other frameworks:
|
|
153
|
+
- **Alpine.js**: `Alpine.initTree(el)` - manual initialization
|
|
154
|
+
- **Vue**: `app.mount(el)` - mount to element
|
|
155
|
+
- **Svelte**: Compiler handles this at build time
|
|
156
|
+
- **HTMX**: `htmx.process(el)` - process element for attributes
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Known Issues
|
|
161
|
+
|
|
162
|
+
### innerHTML Replacement Corrupts Conditional Branches in Iterations
|
|
163
|
+
|
|
164
|
+
**Status**: Bug
|
|
165
|
+
**Priority**: Low (edge case)
|
|
166
|
+
**Category**: Core Runtime
|
|
167
|
+
**Discovered**: 2026-01-25
|
|
168
|
+
|
|
169
|
+
#### Problem
|
|
170
|
+
|
|
171
|
+
When a parent element containing iterations with nested conditionals has its `innerHTML` replaced multiple times with identical HTML, the conditional's `else` branches become `null` after 2-3 replacements. This causes conditionals to fail rendering.
|
|
172
|
+
|
|
173
|
+
**Root cause:**
|
|
174
|
+
1. Iteration template's `branches` object is shared across all iteration instances (iterate.js:65: `branches, // Branch templates are reused`)
|
|
175
|
+
2. When `innerHTML` replacement happens, something mutates the shared `branches.else` to `null`
|
|
176
|
+
3. All instances reference the same corrupted branches object
|
|
177
|
+
4. Subsequent renders have no `else` branch template to mount
|
|
178
|
+
|
|
179
|
+
#### Reproduction
|
|
180
|
+
|
|
181
|
+
Programmatic test case:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
// HTML structure
|
|
185
|
+
const html = `
|
|
186
|
+
<item-list>
|
|
187
|
+
<!-- each items as item, i -->
|
|
188
|
+
<item-card>
|
|
189
|
+
<span>@[item]</span>
|
|
190
|
+
<!-- if i % 2 === 0 -->
|
|
191
|
+
<badge>Even</badge>
|
|
192
|
+
<!-- else -->
|
|
193
|
+
<badge secondary>Odd</badge>
|
|
194
|
+
<!-- /if -->
|
|
195
|
+
</item-card>
|
|
196
|
+
<!-- /each -->
|
|
197
|
+
</item-list>
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
// State
|
|
201
|
+
window.$ = state({ items: ['Apple', 'Banana', 'Cherry'] });
|
|
202
|
+
|
|
203
|
+
// Trigger the bug
|
|
204
|
+
const container = document.querySelector('[vibe]');
|
|
205
|
+
|
|
206
|
+
// First replacement: works
|
|
207
|
+
container.innerHTML = html;
|
|
208
|
+
await new Promise(r => setTimeout(r, 100));
|
|
209
|
+
|
|
210
|
+
// Second replacement: works
|
|
211
|
+
container.innerHTML = html;
|
|
212
|
+
await new Promise(r => setTimeout(r, 100));
|
|
213
|
+
|
|
214
|
+
// Third replacement: branches.else becomes NULL
|
|
215
|
+
container.innerHTML = html;
|
|
216
|
+
await new Promise(r => setTimeout(r, 100));
|
|
217
|
+
|
|
218
|
+
// Result: Even/Odd badges fail to render in the third iteration
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Observations
|
|
222
|
+
|
|
223
|
+
1. Cloning `branches` object during iteration (shallow copy) doesn't prevent the bug
|
|
224
|
+
2. The mutation happens BEFORE cloning, meaning the original template is corrupted
|
|
225
|
+
3. Setting a property trap on `branches.else` doesn't catch the mutation (already null when accessed)
|
|
226
|
+
4. Only affects conditionals inside iterations - standalone conditionals work fine
|
|
227
|
+
5. Only triggers with multiple innerHTML replacements - single replacement works
|
|
228
|
+
|
|
229
|
+
#### Affected Patterns
|
|
230
|
+
|
|
231
|
+
This bug only affects:
|
|
232
|
+
- Replacing innerHTML multiple times with identical HTML containing iterations + conditionals
|
|
233
|
+
- Demo infrastructure like tutorial.js that re-renders on state changes
|
|
234
|
+
- Not representative of typical usage patterns
|
|
235
|
+
|
|
236
|
+
Does NOT affect:
|
|
237
|
+
- Normal reactivity (state changes)
|
|
238
|
+
- Single innerHTML replacement
|
|
239
|
+
- Iterations without conditionals
|
|
240
|
+
- Conditionals outside iterations
|
|
241
|
+
- Incremental DOM mutations (appendChild, insertBefore, etc.)
|
|
242
|
+
|
|
243
|
+
#### Workaround
|
|
244
|
+
|
|
245
|
+
Avoid multiple innerHTML replacements on parents containing iteration+conditional templates. Instead:
|
|
246
|
+
1. Use incremental DOM APIs (appendChild, createElement)
|
|
247
|
+
2. Replace innerHTML once at initialization only
|
|
248
|
+
3. Use Vibe's normal reactivity for updates
|
|
249
|
+
4. Don't wrap demo content in `<tutorial>` that re-renders via innerHTML
|
|
250
|
+
|
|
251
|
+
#### Investigation Log
|
|
252
|
+
|
|
253
|
+
Debugging attempts (2026-01-25):
|
|
254
|
+
- ✓ Confirmed `branches.else` becomes `null` after 3rd innerHTML replacement
|
|
255
|
+
- ✓ Added deep cloning of branches object - didn't help (already null before clone)
|
|
256
|
+
- ✓ Added Object.defineProperty trap - didn't fire (already null)
|
|
257
|
+
- ✓ Checked parsing logic - correctly finds else comments
|
|
258
|
+
- ✗ Unable to identify where mutation occurs
|
|
259
|
+
- ✗ Unable to reproduce with simpler test case (needs tutorial.js pattern)
|
|
260
|
+
|
|
261
|
+
Likely related to:
|
|
262
|
+
- MutationObserver's removedNodes callback cleaning up references
|
|
263
|
+
- Template caching/reuse strategy in iterate.js
|
|
264
|
+
- Interaction between parse → clone → hydrate → render cycle
|
|
265
|
+
|
|
266
|
+
#### Resolution Path
|
|
267
|
+
|
|
268
|
+
**Phase 1 (Current)**: Document and work around
|
|
269
|
+
- Remove `<tutorial>` wrapper from demos
|
|
270
|
+
- Add note in CLAUDE.md about limitation
|
|
271
|
+
- Tests validate core reactivity works correctly
|
|
272
|
+
|
|
273
|
+
**Phase 2+**: Consider fixing if real-world need emerges
|
|
274
|
+
- Deep investigation into branch reference lifecycle
|
|
275
|
+
- Possibly: deep clone branches instead of sharing reference
|
|
276
|
+
- Possibly: rebuild conditional metadata on each innerHTML replacement
|
|
277
|
+
- Possibly: manual hydration API (see "Manual Hydration API" proposal above)
|
|
278
|
+
|
|
279
|
+
This is acceptable technical debt since:
|
|
280
|
+
1. Edge case not representative of normal usage
|
|
281
|
+
2. Core reactivity (the 99% case) works correctly
|
|
282
|
+
3. Can be addressed when/if users report needing this pattern
|
|
283
|
+
4. Phase 1 goals (iteration + conditionals) are met
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Future Proposals
|
|
288
|
+
|
|
289
|
+
*This section reserved for additional feature proposals*
|
package/affected.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { resolvePath, deepEqual } from './iteration-utils.js';
|
|
2
2
|
import { extractDependencies } from './conditionals.js';
|
|
3
3
|
import { BINDING_REGEX } from './constants.js';
|
|
4
|
+
import { evalInScope } from './utils.js';
|
|
4
5
|
|
|
5
6
|
// Evaluate conditional expression
|
|
6
|
-
const evaluateCondition = (expression, state) =>
|
|
7
|
-
try {
|
|
8
|
-
const keys = Object.keys(state);
|
|
9
|
-
const values = Object.values(state);
|
|
10
|
-
const result = new Function(...keys, `'use strict'; return !!(${expression})`)(...values);
|
|
11
|
-
return !!result;
|
|
12
|
-
} catch (e) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
};
|
|
7
|
+
const evaluateCondition = (expression, state) => !!evalInScope(expression, state);
|
|
16
8
|
|
|
17
9
|
// Helper function to check if a match references a specific key
|
|
18
10
|
const matchesKey = (matchStr, key) => matchStr === key || matchStr.startsWith(key + '.');
|
package/conditionals.js
CHANGED
|
@@ -2,20 +2,10 @@ import parse from './parse.js';
|
|
|
2
2
|
import affected from './affected.js';
|
|
3
3
|
import hydrate from './hydrate.js';
|
|
4
4
|
import { createScopedState, renderAllIterations, initializeBlock } from './iterate.js';
|
|
5
|
+
import { evalInScope } from './utils.js';
|
|
5
6
|
|
|
6
7
|
// Evaluate conditional expression in state context
|
|
7
|
-
const evaluateCondition = (expression, state) =>
|
|
8
|
-
try {
|
|
9
|
-
const keys = Object.keys(state);
|
|
10
|
-
const values = Object.values(state);
|
|
11
|
-
// Create a function with state keys as parameters and evaluate the expression
|
|
12
|
-
const result = new Function(...keys, `'use strict'; return !!(${expression})`)(...values);
|
|
13
|
-
return !!result; // Coerce to boolean
|
|
14
|
-
} catch (e) {
|
|
15
|
-
console.warn(`Error evaluating condition "${expression}":`, e);
|
|
16
|
-
return false; // Default to false on error
|
|
17
|
-
}
|
|
18
|
-
};
|
|
8
|
+
const evaluateCondition = (expression, state) => !!evalInScope(expression, state);
|
|
19
9
|
|
|
20
10
|
// Extract state dependencies from an expression
|
|
21
11
|
// e.g., "count > 5" → ["count"]
|
package/hydrate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { updateIteration } from './iterate.js';
|
|
2
2
|
import { updateConditional } from './conditionals.js';
|
|
3
3
|
import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js';
|
|
4
|
+
import { evalInScope } from './utils.js';
|
|
4
5
|
|
|
5
6
|
// Keep track of old state for diffing
|
|
6
7
|
let previousState = {};
|
|
@@ -9,19 +10,6 @@ export const setPreviousState = (state) => {
|
|
|
9
10
|
previousState = { ...state };
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
// Evaluate expression in the context of state
|
|
13
|
-
const evalInScope = (expr, state) => {
|
|
14
|
-
try {
|
|
15
|
-
const keys = Object.keys(state);
|
|
16
|
-
const values = Object.values(state);
|
|
17
|
-
// Create a function with state keys as parameters and evaluate the expression
|
|
18
|
-
return new Function(...keys, `'use strict'; return (${expr})`)(...values);
|
|
19
|
-
} catch (e) {
|
|
20
|
-
// Fallback to undefined if evaluation fails
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
13
|
export default (affected, state, linkList = {}) => {
|
|
26
14
|
affected.forEach((aff) => {
|
|
27
15
|
// Handle iteration updates
|
package/index.js
CHANGED
|
@@ -68,15 +68,23 @@ const main = (s, attrName = 'vibe') => {
|
|
|
68
68
|
afterDomMutation: [],
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
// Observer reference -
|
|
71
|
+
// Observer reference and callback - defined here so state handler can access processMutations
|
|
72
72
|
let observer = null;
|
|
73
|
+
let processMutations = null;
|
|
73
74
|
|
|
74
75
|
const $ = state(s, (newState) => {
|
|
75
76
|
const mergedState = deepMerge($, newState);
|
|
76
77
|
const affectedElements = affected(parsedTree, previousState, mergedState);
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
// Capture pending mutations before disconnecting (takeRecords clears the queue)
|
|
80
|
+
let pendingMutations = [];
|
|
81
|
+
if (observer) {
|
|
82
|
+
pendingMutations = observer.takeRecords();
|
|
83
|
+
observer.disconnect();
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
hydrate(affectedElements, mergedState, linkList);
|
|
87
|
+
|
|
80
88
|
if (observer) {
|
|
81
89
|
observer.observe(rootElement, {
|
|
82
90
|
attributes: false,
|
|
@@ -84,6 +92,11 @@ const main = (s, attrName = 'vibe') => {
|
|
|
84
92
|
childList: true,
|
|
85
93
|
subtree: true,
|
|
86
94
|
});
|
|
95
|
+
|
|
96
|
+
// Process mutations that were pending before we disconnected
|
|
97
|
+
if (pendingMutations.length > 0 && processMutations) {
|
|
98
|
+
processMutations(pendingMutations);
|
|
99
|
+
}
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
const prev = structuredClone(previousState);
|
|
@@ -112,7 +125,8 @@ const main = (s, attrName = 'vibe') => {
|
|
|
112
125
|
renderAllIterations(parsedTree, $, linkList);
|
|
113
126
|
renderAllConditionals(parsedTree, $, linkList);
|
|
114
127
|
|
|
115
|
-
observer
|
|
128
|
+
// Define observer callback as named function so we can call it manually for pending mutations
|
|
129
|
+
processMutations = (mutations) => {
|
|
116
130
|
// Early exit if no mutations to process (common case)
|
|
117
131
|
if (mutations.length === 0) return;
|
|
118
132
|
|
|
@@ -120,7 +134,6 @@ const main = (s, attrName = 'vibe') => {
|
|
|
120
134
|
let parsedParents = null; // Lazy init - only create Set when needed
|
|
121
135
|
|
|
122
136
|
mutations.forEach(({ addedNodes, removedNodes, target }) => {
|
|
123
|
-
// Process removed nodes first (cleanup before additions)
|
|
124
137
|
removedNodes.forEach((node) => {
|
|
125
138
|
const entry = Object.entries(linkList).find(([_, element]) => element === node);
|
|
126
139
|
|
|
@@ -210,7 +223,9 @@ const main = (s, attrName = 'vibe') => {
|
|
|
210
223
|
if (hadChanges) {
|
|
211
224
|
hooks.afterDomMutation.forEach((callback) => callback());
|
|
212
225
|
}
|
|
213
|
-
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
observer = new MutationObserver(processMutations);
|
|
214
229
|
|
|
215
230
|
observer.observe(rootElement, {
|
|
216
231
|
attributes: false,
|
package/package.json
CHANGED
package/utils.js
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
let hashCounter = 0;
|
|
3
3
|
export const hash = () => `_${hashCounter++}`;
|
|
4
4
|
|
|
5
|
+
// Evaluate expression in the context of state
|
|
6
|
+
export const evalInScope = (expr, state) => {
|
|
7
|
+
try {
|
|
8
|
+
// Normalize whitespace - collapse newlines/spaces to single space (resilient to IDE formatting)
|
|
9
|
+
const normalized = expr.replace(/\s+/g, ' ').trim();
|
|
10
|
+
const keys = Object.keys(state);
|
|
11
|
+
const values = Object.values(state);
|
|
12
|
+
return new Function(...keys, `'use strict'; return (${normalized})`)(...values);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
5
18
|
// Instead of using lodash-es as a dependency, we run our own deepMerge (mergeWith in lodash)
|
|
6
19
|
export const deepMerge = (target, source) => {
|
|
7
20
|
// Handle null/undefined
|