@ape-egg/vibe 1.8.0 → 1.9.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/CHANGELOG.md +107 -0
- package/README.md +72 -2
- package/boot.js +5 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/parser/html.rs +15 -15
- package/index.js +15 -0
- package/llms.txt +151 -84
- package/package.json +3 -2
- package/runtime/_vibe-compiled-iteration-batch.js +18 -9
- package/runtime/affected.js +36 -16
- package/runtime/component.js +237 -86
- package/runtime/conditionals.js +12 -1
- package/runtime/constants.js +17 -3
- package/runtime/hydrate.js +27 -10
- package/runtime/index.js +91 -34
- package/runtime/iterate.js +323 -108
- package/runtime/manifest.js +2 -1
- package/runtime/parse.js +67 -80
- package/runtime/pre-compiled-manifest.js +38 -13
- package/runtime/reconcile.js +621 -0
- package/runtime/state.js +5 -2
- package/runtime/utils.js +77 -2
- package/vibe.css +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,112 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.9.1] - 2026-04-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Non-primitive iteration props** (`runtime/iterate.js`) — `<component src>` props inside `<!-- each -->` can now receive objects and arrays. Primitives stringify as before; non-primitives snapshot into `window.__vibeIterProps` and the attribute becomes `@[window.__vibeIterProps._pN]`, so child templates can dot/iterate into the value (`@[card.name]`, `<!-- each card.abilities as a -->`). Slots are released on wrapper detach via `releaseOrphanedIterationProps`.
|
|
8
|
+
- Test: `tests/e2e/component-in-iteration-rich-props.spec.js`
|
|
9
|
+
- **Idempotent hydrate writes** (`runtime/hydrate.js`) — attribute, boolean-attribute, and text writes now compare current vs. new value and skip the write when unchanged. Eliminates needless paints and CSS-transition jitter on every re-hydrate (e.g. 4 Hz client-clock tick re-running unrelated bindings).
|
|
10
|
+
- Test: `tests/e2e/idempotent-hydrate.spec.js`
|
|
11
|
+
- **Multi-segment `this.X.Y` rewriting** (`runtime/component.js`, `runtime/constants.js`) — `@[this.user.name]`, `@[this.x + 1]`, etc. inside a component now rewrite the whole `this.X` head consistently. Centralized via `THIS_PROP_REGEX` / `STATE_THIS_PROP_REGEX` so `parse.js`, `utils.js`, and `component.js` share one definition.
|
|
12
|
+
- **`$.renderComponent` — pure-render path for surgical HMR** (`runtime/component.js`, `runtime/index.js`) — extracted prop substitution + slot inlining into a non-executing helper. The vite plugin uses this with `$.reconcile` to update components in place when only the template changes; scripts are not re-run, so registered component state survives.
|
|
13
|
+
- **`./boot` package export** (`package.json`) — `import { boot } from '@ape-egg/vibe/boot'` is now usable from consumers.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Iteration-prop registry slots freed prematurely on wrapper swap** (`runtime/component.js`, `runtime/iterate.js`) — `processSingle.finalize` and the plugin's `remount` now transfer `_vibeIterPropIds` from the soon-to-be-detached element to its replacement. Without the transfer, `releaseOrphanedIterationProps` would clear registry entries that the new wrapper's bindings still reference, rendering them as `undefined`.
|
|
18
|
+
- **Batch-render path stringified `<component src>` props** (`runtime/iterate.js`) — `canUseBatchRender` now skips templates containing `<component src>` so iteration items with rich props go through the clone+hydrate path that resolves them via the registry instead of through template-literal interpolation that coerces objects to `[object Object]`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [1.9.0] - 2026-04-18
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`$.reconcile()` — subtree reconciliation API** (`runtime/reconcile.js`, new 621-line module) — reconcile a Vibe-managed subtree against new source HTML with a tag-aligned, two-pointer walker. Vibe-owned regions (iterations, conditionals, components, `<slot>` pairs) are treated as opaque; their internals stay state-driven. DOM identity, focus, and selection survive wherever a match is found.
|
|
27
|
+
- Exposed as `$.reconcile` (non-enumerable so it stays out of state snapshots)
|
|
28
|
+
- Test: `tests/e2e/reconcile.spec.js`
|
|
29
|
+
|
|
30
|
+
- **Component state cleanup on unmount** (`runtime/component.js`, `runtime/conditionals.js`, `runtime/index.js`, `runtime/manifest.js`) — when a `<component>` leaves the DOM (conditional branch swap, iteration key removal, etc.), its entry in `window.__vibeComponents[id]` and `window.$[id]` is evicted. `collectComponentIds` walks removed subtrees; `releaseOrphanedComponentState` verifies the DOM is fully gone before deleting state. Prevents state leaks from components that mount and unmount repeatedly.
|
|
31
|
+
- Test: `tests/e2e/component-state-cleanup.spec.js`
|
|
32
|
+
|
|
33
|
+
- **Arbitrary JS expressions in `<!-- each -->`** (`runtime/constants.js`) — `ITERATION_REGEX` relaxed from `[\w.\[\]]+` to `.+`. The array expression is now evaluated via `evalInScope`, so you can iterate over method calls, window globals, inline array literals, filters, and `Array.from(...)`:
|
|
34
|
+
```html
|
|
35
|
+
<!-- each items.filter(x => x.active) as item -->
|
|
36
|
+
<!-- each Array.from({length: 10}, (_, i) => i) as n -->
|
|
37
|
+
<!-- each window.fights as fight, i -->
|
|
38
|
+
```
|
|
39
|
+
- Test: `tests/e2e/each-expression.spec.js`
|
|
40
|
+
|
|
41
|
+
- **Missing-identifier safety in expressions** (`runtime/utils.js`) — `evalInScope` now wraps free identifiers that aren't state keys, reserved words, or known globals in `typeof` guards so they resolve to `undefined` instead of throwing `ReferenceError`. `@[!missingProp]` → `true`, `@[optional?.x]` → `undefined`. Uses `typeof` rather than a `var` hoist so real globals (app functions on `window` like `getLevelByExperience`) stay reachable.
|
|
42
|
+
|
|
43
|
+
- **Arrow function parameters and object-literal keys recognized** (`runtime/utils.js`) — identifier rewriting now skips `(a, b) =>` / `x =>` parameter lists and `{ key: value }` property names. Expressions like `items.map(x => x.name)` and `@[{ a: 1 }[k]]` no longer get turned into invalid JS by the missing-identifier pass.
|
|
44
|
+
|
|
45
|
+
- **Prop substitution covers directive comments and event handlers** (`runtime/component.js`) — prop identifiers now resolve inside `<!-- if propName -->`, `<!-- else if propName -->`, `<!-- each propName as item -->` and `onclick="$.propName = x"` in addition to `@[...]`. Writing `$.value = x` inside an event handler on a child component whose parent passed `value="@[email]"` updates `$.email` on the parent — natural two-way binding with no extra API.
|
|
46
|
+
- Tests: `tests/e2e/prop-in-directive.spec.js`, `tests/e2e/prop-in-expression.spec.js`, `tests/e2e/two-way-binding.spec.js`
|
|
47
|
+
|
|
48
|
+
- **Pre-boot `$.ready` promise** (`index.js`, `boot.js`) — the placeholder returned by `vibe()` before boot now exposes a `.ready` promise that chains to the real post-boot `$.ready`. Consumers that captured `window.$` early (tests, auto-initializers) can `await $.ready` without polling. New `chainInstanceReady` export links the two.
|
|
49
|
+
|
|
50
|
+
- **Async component scripts** (`runtime/component.js`) — `<script type="module">` blocks in components with `import` statements are rewritten to `await import()` and executed via `AsyncFunction`. Sync execution preserved when there are no imports (keeps boot timing unchanged). Component finalization waits for all async scripts before inlining.
|
|
51
|
+
- Tests: `tests/e2e/async-component-state.spec.js`, `tests/e2e/parallel-components.spec.js`
|
|
52
|
+
|
|
53
|
+
- **Promises never proxied as reactive state** (`runtime/state.js`) — proxy `get` trap now bails on `value instanceof Promise`. Wrapping a Promise violates the Proxy invariant when exposed as a non-writable property (e.g. `$.ready`) and makes no sense as reactive state.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **HTML-entity decoding in compiled iteration templates** (`runtime/_vibe-compiled-iteration-batch.js`) — when a template's attributes get serialized via `innerHTML`, the browser encodes `<`, `>`, `"`, `&`, `'`. The batch-function compiler was wrapping those encoded expressions directly in a template literal, producing invalid JS for bindings like `@[x > 0 ? 'a' : 'b']` when used inside an attribute. Entities are now decoded before interpolation.
|
|
58
|
+
|
|
59
|
+
- **`<!-- each -->` over `Array.from(...)` and other inline expressions** — previously failed silently because `ITERATION_REGEX` only accepted simple paths. See `ITERATION_REGEX` change above.
|
|
60
|
+
|
|
61
|
+
- **Attribute bindings on fetched `<component src>` wrappers were coerced to strings** (`runtime/parse.js`) — `captureAttributeBindings` now returns nulls for elements that are still `<component src>` / `<div class="component" src>` (pre-fetch), so raw prop values survive untouched for `processComponent` to substitute. Post-fetch wrappers (no `src`) capture attributes normally.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- **Iteration engine rework** (`runtime/iterate.js`, +378 lines over 1.8.1) — unified update path around tagged instance keys, bulk-replacement fast path when old/new arrays share no common keys (uses `Range.deleteContents` + single `innerHTML`), and DocumentFragment batching inside `renderIteration` so N inserts become one.
|
|
66
|
+
|
|
67
|
+
- **Debug scaffolding removed** (`runtime/index.js`) — an ad-hoc `[vibe-debug] rerender` `console.info` and the associated `window.__lastHits` / `window.__parsedTree` probes (leftover from an investigation) have been deleted.
|
|
68
|
+
|
|
69
|
+
### Documentation
|
|
70
|
+
|
|
71
|
+
- **README — "Future improvements to refactor"** — documents the string-based dependency-tracking approach used by `affected()` and its always-affected fallback semantics, and the HTML-lowercased attribute-name handling.
|
|
72
|
+
|
|
73
|
+
### Packaging
|
|
74
|
+
|
|
75
|
+
- **`@ape-egg/vite-plugin-vibe` published** — new sibling package providing a Vite plugin with HMR for Vibe pages and components. See its own `README.md` and `CHANGELOG.md`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## [1.8.1] - 2026-04-12
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **`<slot>` boundary preserved in processed DOM** — component slot content is now wrapped in a `<slot>` element instead of being spliced in directly. Dev tools, HMR, and anything that needs to locate slot boundaries can query for `<slot>` instead of tracking comment pairs.
|
|
84
|
+
- `runtime/component.js` — `<slot></slot>` replaced with `<slot>${children}</slot>`
|
|
85
|
+
- `compiler/src/parser/html.rs` — compiler emits the same wrapping
|
|
86
|
+
- `vibe.css` — `slot, div.slot { display: contents }` so the wrapper is layout-transparent
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
|
|
90
|
+
- **Conditionals inside doubly-nested component slots lost their templates** — when a `<component src>` appeared inside another component's slot content, Vibe's parser ran `renderConditional` (which strips template nodes) before `processComponent` captured the slot content, so branch templates were gone by the time the nested component tried to use them.
|
|
91
|
+
- `runtime/index.js` — `processMutations` now captures `_vibeSlotContent` for all nested `<component src>` elements before parse/hydrate runs
|
|
92
|
+
- Test: `tests/e2e/conditional-in-nested-slot.spec.js`
|
|
93
|
+
|
|
94
|
+
- **Bindings with complex expressions re-hydrated on every state change** — `matchesKey` did prefix-only matching, so expressions like `@[Math.floor(coins / 100)]` couldn't be matched to any state key and hit the "always affected" fallback. A 250ms client clock would re-hydrate every such binding 4× per second.
|
|
95
|
+
- `runtime/affected.js` — `matchesKey` now also does word-boundary search so it finds `coins` as an identifier inside the expression
|
|
96
|
+
- Test: `tests/e2e/expression-dependency.spec.js`
|
|
97
|
+
|
|
98
|
+
- **Iteration instance comparisons used live Proxy as "old state"** — `scopedState` is a Proxy that reflects current global state, so comparing `state[k]` vs `newState[k]` showed both as the new value. Global state changes inside iterations weren't detected as changes.
|
|
99
|
+
- `runtime/affected.js` — iteration recursion now builds plain-object snapshots from previousState/currentState merged with scopedState's local vars
|
|
100
|
+
|
|
101
|
+
- **Name bindings re-hydrated on every state change** — HTML lowercases attribute names, so `<page @[pageName]>` becomes `@[pagename]` in the DOM. `matchesKey('pagename', 'pageName')` failed (case-sensitive), triggering the always-affected fallback and causing constant attribute flashing.
|
|
102
|
+
- `runtime/affected.js` — name binding dependency check now matches state keys case-insensitively, mirroring `hydrate.js`'s existing case-insensitive evaluation fallback
|
|
103
|
+
|
|
104
|
+
### Documentation
|
|
105
|
+
|
|
106
|
+
- `README.md` — "Future improvements to refactor" section documenting string-based dependency tracking limitations and HTML lowercase attribute handling
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
3
110
|
## [1.8.0] - 2026-04-10
|
|
4
111
|
|
|
5
112
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vibe
|
|
2
2
|
|
|
3
|
-
**Version 1.
|
|
3
|
+
**Version 1.9.1 (Alpha)** — A runtime-first reactive framework with optional compilation.
|
|
4
4
|
|
|
5
5
|
No virtual DOM. No build step required. Just modern JavaScript. When you need production optimizations, add the optional Rust-based compiler.
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ The core reactive runtime. Works directly in the browser without any build tools
|
|
|
25
25
|
<script type="module">
|
|
26
26
|
import vibe from './node_modules/@ape-egg/vibe/index.js';
|
|
27
27
|
|
|
28
|
-
vibe({ name: 'World', count: 0 });
|
|
28
|
+
window.$ = vibe({ name: 'World', count: 0 });
|
|
29
29
|
</script>
|
|
30
30
|
</head>
|
|
31
31
|
<body vibe-fouc>
|
|
@@ -118,6 +118,48 @@ Runtime component loading with props and slots:
|
|
|
118
118
|
</component>
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
Props can be reactive bindings (`title="@[pageTitle]"`), static literals (`theme="dark"`), or live objects/arrays passed through iteration scope (`<component src="/card.html" card="@[card]">` inside `<!-- each cards as card -->`). Non-primitive props are stashed in an internal registry so the child template can dot/iterate into them (`@[card.name]`, `<!-- each card.abilities as a -->`).
|
|
122
|
+
|
|
123
|
+
### Component-Local State
|
|
124
|
+
|
|
125
|
+
For state scoped to a single component, import `component` from `@ape-egg/vibe/component` and call it from a `<script type="module">` inside the component file:
|
|
126
|
+
|
|
127
|
+
```html
|
|
128
|
+
<!-- /components/Counter.html -->
|
|
129
|
+
<script type="module">
|
|
130
|
+
import component from '@ape-egg/vibe/component';
|
|
131
|
+
component({
|
|
132
|
+
count: 0,
|
|
133
|
+
increment() { this.count++; }
|
|
134
|
+
});
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<button onclick="this.increment()">Clicked @[this.count] times</button>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
How it works:
|
|
141
|
+
|
|
142
|
+
1. `component({...})` generates a unique id (e.g. `_c0`, `_c1`) and registers the state at `$[id]`
|
|
143
|
+
2. The `<script>` and every following sibling is tagged with `data-vibe-component-id="<id>"`
|
|
144
|
+
3. Inside that subtree, `@[this.X.Y]` is rewritten to `@[_c0.X.Y]` and event handlers like `onclick="this.method()"` or `oninput="$.this.value = ..."` are rewritten to address `$[id]`
|
|
145
|
+
4. When the component leaves the DOM, its state entry is freed automatically
|
|
146
|
+
|
|
147
|
+
Multi-segment paths (`@[this.user.profile.name]`), conditionals (`<!-- if this.editing -->`), and iterations (`<!-- each this.items as item -->`) all resolve against the component's bucket. Global `$` and component `this.X` coexist freely.
|
|
148
|
+
|
|
149
|
+
### Lifecycle Hooks
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
$.on('ready', () => {}); // once, after initial parse + first hydrate + all components mounted
|
|
153
|
+
$.on('afterUpdate', (cur, prev) => {}); // every state change (batched per microtask)
|
|
154
|
+
$.on('afterDomMutation', () => {}); // after every MutationObserver batch
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`$.ready` is also exposed as a Promise (`await $.ready`), useful for code that captured `window.$` before boot.
|
|
158
|
+
|
|
159
|
+
### Subtree Reconciliation (advanced)
|
|
160
|
+
|
|
161
|
+
`$.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.
|
|
162
|
+
|
|
121
163
|
### Dehydrate
|
|
122
164
|
|
|
123
165
|
Skip reactive processing for an element:
|
|
@@ -525,3 +567,31 @@ Modern browsers with Proxy and MutationObserver support.
|
|
|
525
567
|
## License
|
|
526
568
|
|
|
527
569
|
ISC
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Future improvements to refactor
|
|
574
|
+
|
|
575
|
+
### Dependency tracking uses string matching, not AST
|
|
576
|
+
|
|
577
|
+
`affected()` determines which bindings to re-hydrate by string-matching expression text against state key names. When matching fails, a safety fallback marks the binding as affected on every state change.
|
|
578
|
+
|
|
579
|
+
- `runtime/affected.js:15` — `matchesKey` does exact/prefix match, then word-boundary search
|
|
580
|
+
- `runtime/affected.js:~122` — `shouldAffect = noMatch || ...` fallback for unmatched expressions
|
|
581
|
+
- `runtime/affected.js:~248` — same fallback for name bindings
|
|
582
|
+
|
|
583
|
+
Every time `matchesKey` can't prove an expression is unrelated to a state change, that binding re-hydrates. A proper AST-based dependency extraction at parse time would eliminate the fallback entirely: each binding stores its exact dependency set, and runtime just checks if any dep key changed.
|
|
584
|
+
|
|
585
|
+
### HTML lowercases attribute names — breaks name binding matching
|
|
586
|
+
|
|
587
|
+
The browser lowercases all attribute names during HTML parsing. `<page @[pageName]>` becomes `@[pagename]` in the DOM. Every value hydrated into an attribute NAME position loses its case. `@[myCoolPage]` → DOM stores `@[mycoolpage]`, causing comparison mismatches against camelCase state keys.
|
|
588
|
+
|
|
589
|
+
Handled today by case-insensitive fallbacks in two places:
|
|
590
|
+
- `runtime/hydrate.js:~32` — case-insensitive state key lookup when evaluating name bindings
|
|
591
|
+
- `runtime/affected.js:~235` — case-insensitive `matchesKey` wrapper for name bindings
|
|
592
|
+
|
|
593
|
+
Both are workarounds for HTML's behavior. A cleaner architecture would centralize the case-insensitive state key resolution into one helper, or normalize the name binding expression to canonical state-key case at first evaluation and cache it on the tree node.
|
|
594
|
+
|
|
595
|
+
### Related: value hydration into attribute values is case-preserving
|
|
596
|
+
|
|
597
|
+
Only attribute NAMES are lowercased by HTML, not values. So `title="@[pageName]"` and `@[pageName]` in text content preserve case and work without fallbacks — the issue is specific to name bindings.
|
package/boot.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Used by both index.js (global state) and component.js (component state)
|
|
3
3
|
|
|
4
4
|
import main from './runtime/index.js';
|
|
5
|
-
import { getPendingListeners } from './index.js';
|
|
5
|
+
import { getPendingListeners, chainInstanceReady } from './index.js';
|
|
6
6
|
|
|
7
7
|
let bootQueued = false;
|
|
8
8
|
let booted = false;
|
|
@@ -40,6 +40,10 @@ export const boot = () => {
|
|
|
40
40
|
// Boot with merged state
|
|
41
41
|
window.$ = main(mergedState, config, targetSelector);
|
|
42
42
|
|
|
43
|
+
// Forward real $.ready to the pre-boot placeholder's ready promise so
|
|
44
|
+
// any consumer that captured the placeholder can still await readiness.
|
|
45
|
+
chainInstanceReady(window.$.ready);
|
|
46
|
+
|
|
43
47
|
// Apply pending listeners from vibe instance
|
|
44
48
|
const pendingListeners = getPendingListeners();
|
|
45
49
|
if (pendingListeners) {
|
|
Binary file
|
|
Binary file
|
|
@@ -231,15 +231,15 @@ impl HtmlParser {
|
|
|
231
231
|
replacement = replacement.replace(&prop_binding, &prop_value);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// Replace <slot> tags
|
|
235
|
-
let
|
|
236
|
-
|
|
234
|
+
// Replace <slot> tags — wrap children in <slot> boundary, or remove if empty
|
|
235
|
+
let slot_wrapped = if slot_content.trim().is_empty() {
|
|
236
|
+
String::new()
|
|
237
237
|
} else {
|
|
238
|
-
|
|
238
|
+
format!("<slot>{}</slot>", slot_content)
|
|
239
239
|
};
|
|
240
|
-
replacement = replacement.replace("<slot></slot>",
|
|
241
|
-
replacement = replacement.replace("<slot/>",
|
|
242
|
-
replacement = replacement.replace("<slot />",
|
|
240
|
+
replacement = replacement.replace("<slot></slot>", &slot_wrapped);
|
|
241
|
+
replacement = replacement.replace("<slot/>", &slot_wrapped);
|
|
242
|
+
replacement = replacement.replace("<slot />", &slot_wrapped);
|
|
243
243
|
|
|
244
244
|
// Keep the wrapper for consistency with runtime (using generic <component> wrapper)
|
|
245
245
|
// No src attribute = wrapper won't be re-processed
|
|
@@ -371,10 +371,10 @@ impl HtmlParser {
|
|
|
371
371
|
replacement = replacement.replace(&prop_binding, prop_value);
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
let
|
|
375
|
-
replacement = replacement.replace("<slot></slot>",
|
|
376
|
-
replacement = replacement.replace("<slot/>",
|
|
377
|
-
replacement = replacement.replace("<slot />",
|
|
374
|
+
let slot_wrapped = if slot_content.trim().is_empty() { String::new() } else { format!("<slot>{}</slot>", slot_content) };
|
|
375
|
+
replacement = replacement.replace("<slot></slot>", &slot_wrapped);
|
|
376
|
+
replacement = replacement.replace("<slot/>", &slot_wrapped);
|
|
377
|
+
replacement = replacement.replace("<slot />", &slot_wrapped);
|
|
378
378
|
|
|
379
379
|
let wrapper = format!("<component>{}</component>", replacement);
|
|
380
380
|
result.replace_range(*start..*end, &wrapper);
|
|
@@ -447,10 +447,10 @@ impl HtmlParser {
|
|
|
447
447
|
replacement = replacement.replace(&prop_binding, prop_value);
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
let
|
|
451
|
-
replacement = replacement.replace("<slot></slot>",
|
|
452
|
-
replacement = replacement.replace("<slot/>",
|
|
453
|
-
replacement = replacement.replace("<slot />",
|
|
450
|
+
let slot_wrapped = if slot_content.trim().is_empty() { String::new() } else { format!("<slot>{}</slot>", slot_content) };
|
|
451
|
+
replacement = replacement.replace("<slot></slot>", &slot_wrapped);
|
|
452
|
+
replacement = replacement.replace("<slot/>", &slot_wrapped);
|
|
453
|
+
replacement = replacement.replace("<slot />", &slot_wrapped);
|
|
454
454
|
|
|
455
455
|
let wrapper = format!("<component>{}</component>", replacement);
|
|
456
456
|
result.replace_range(start..end, &wrapper);
|
package/index.js
CHANGED
|
@@ -5,9 +5,16 @@ import { boot, isBooted, ensureBoot } from './boot.js';
|
|
|
5
5
|
|
|
6
6
|
// Shared instance for queueing listeners before boot
|
|
7
7
|
let vibeInstance = null;
|
|
8
|
+
let resolveInstanceReady = null;
|
|
8
9
|
|
|
9
10
|
const createVibeInstance = () => ({
|
|
10
11
|
_pendingListeners: { afterUpdate: [], afterDomMutation: [], ready: [] },
|
|
12
|
+
// Promise that resolves when the real $.ready resolves post-boot. Lets
|
|
13
|
+
// consumers holding the pre-boot placeholder (e.g. tests awaiting
|
|
14
|
+
// `window.$.ready` before boot has replaced $ with the reactive proxy)
|
|
15
|
+
// wait for readiness without polling. Non-enumerable so it stays out of
|
|
16
|
+
// state snapshots.
|
|
17
|
+
ready: new Promise((resolve) => { resolveInstanceReady = resolve; }),
|
|
11
18
|
on(event, callback) {
|
|
12
19
|
// If booted, delegate to window.$
|
|
13
20
|
if (isBooted() && window.$) {
|
|
@@ -66,4 +73,12 @@ const vibe = (state = {}, config, targetSelector) => {
|
|
|
66
73
|
// Export function to get pending listeners (used by boot.js)
|
|
67
74
|
export const getPendingListeners = () => vibeInstance?._pendingListeners || null;
|
|
68
75
|
|
|
76
|
+
// Forward post-boot $.ready to the pre-boot placeholder's ready promise.
|
|
77
|
+
// Called by boot.js after main() creates the real reactive proxy.
|
|
78
|
+
export const chainInstanceReady = (realReadyPromise) => {
|
|
79
|
+
if (resolveInstanceReady && realReadyPromise) {
|
|
80
|
+
realReadyPromise.then(() => resolveInstanceReady());
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
69
84
|
export default vibe;
|