lucianofedericopereira
/
exo
Coordinate interactive sections of an Astro page through declarative HTML attributes—with zero component trees and no shared mutable state
EXO
Coordinate interactive sections of an Astro page through declarative HTML attributes—with zero component trees and no shared mutable state.
Exo is that event bus, engineered with a clean, declarative HTML flight deck on top.
It deploys at a razor-thin ~11 kB bundled—loaded exactly once, regardless of how many sectors utilize it.
The Mission Brief: Overcoming Orbital Decay
Astro launches content flawlessly. But the moment two isolated sectors need to coordinate—a product matrix and a payload summary, or a filter telemetry panel and a results grid—current architectural solutions compromise the integrity of the mission. You are forced to reach for heavy, multi-ton frameworks just to bridge a micro-gap.
-
The Alpine Drift: Alpine operates well for self-contained, isolated pods. But the moment those pods need to synchronize telemetry, you are forced to deploy $store—a chaotic, global, mutable void where any element can overwrite data at any time. It lacks named…
Modern Astro pages often contain multiple interactive sections that must react to each other: a product matrix updating a cart summary, a filter panel driving a results grid, or any pair of isolated DOM sectors that need to stay in sync.
Most solutions force you into heavy component runtimes or fragile global stores. Exo takes a different path: a declarative HTML directive layer on top of a real event bus, with no component tree, no shared mutable state, and no cross‑imports between sections.
Exo ships at ~11 kB bundled and loads exactly once, no matter how many sectors use it.
🚀 What Exo Is
Exo is a thin directive layer that coordinates independent DOM sectors through a central vapor‑chamber bus.
Each sector declares:
commands it emits (v-command)
state it reads (v-bind-text, v-show)
initial scope (v-scope)
There is no direct wiring between sectors. No component hierarchy. No global mutable object. All writes go through named commands; all reads come from reactive state.
🛰️ The Problem: Cross‑Sector Coordination Without Heavy Frameworks
Astro excels at static delivery and isolated islands. The trouble begins when two isolated sections must coordinate.
The Alpine Drift
Alpine is excellent for self‑contained widgets. But cross‑widget coordination collapses into $store: a global, mutable object with no named actions, no single home for side effects, and no deterministic write path.
The Island Isolation Trap
Vue islands are intentionally isolated. Sharing state requires either: wrapping both islands in a parent island (destroying the isolation boundary), or wiring custom DOM events manually (scattering coordination logic everywhere).
Both approaches reintroduce complexity that Astro was supposed to avoid.
When neither fits
Developers end up writing ad‑hoc event buses, singletons, or brittle pub/sub modules. Exo replaces that with a clean, declarative, HTML‑first coordination model.
🧩 What It Looks Like
Two sectors. Different DOM subtrees. Zero knowledge of each other.
<!-- Sector A: product list -->
<button
v-command="cartAdd"
v-target='{"id": 1, "name": "Coffee", "price": 4}'
>Add to cart</button>
<!-- Sector B: cart summary -->
<p v-show="empty">Your cart is empty.</p>
<span v-bind-text="count">0</span> item(s) —
$<span v-bind-text="total">0.00</span>
import { bus, busState } from './directives';
busState.count = 0;
busState.total = '0.00';
busState.empty = true;
let runningTotal = 0;
bus.register('cartAdd', (cmd) => {
busState.count += 1;
runningTotal += cmd.target.price;
busState.total = runningTotal.toFixed(2);
busState.empty = false;
});
Sector A emits a named command.
Sector B reacts to state.
Exo is the only bridge.
🧭 Why Exo Works: The CQRS Constraint
The core principle: read-only scopes and command-only writes.
v-scope seeds reactive state but cannot be mutated directly.
v-command is the only write path.
Every mutation is a named command handled in one explicit place.
Because Exo uses a real vapor‑chamber bus, you inherit its full middleware system:
logging
validation
throttling
undo callbacks
cross‑tab sync
WebSocket bridges
Exo stays small because it delegates complexity to the bus.
If you allow direct scope writes, the bus becomes decorative and the model collapses. CQRS is the mechanism that keeps the architecture coherent.
🧱 Why Not Alpine
Alpine is perfect when:
one widget owns its own state
no other widget needs to read or write it
The moment two sectors must coordinate, $store becomes a global mutable void. Exo keeps the ergonomics of HTML attributes but enforces a deterministic write path.
🏝️ Why Not Vue Islands
Vue islands shine when the UI itself is complex.
They break down when coordination is the complex part.
Sharing state between islands requires:
a parent island (rebuilding a component tree), or
custom DOM events (scattered coordination logic)
Exo keeps HTML as HTML and centralizes coordination in one place.
🧪 When Exo Stops Being the Right Tool
Exo is ideal for pages with:
a bounded set of named user actions
reactive sections listening to shared state
no need for complex component lifecycles
You’ve outgrown Exo when:
handlers start dispatching other commands
you need retry/circuit‑breaker middleware
a single action must await network calls
coordination logic becomes more complex than rendering
At that point, work directly with vapor‑chamber or use a full state library like Pinia, Zustand, or XState.
📦 v0.1 Specifications
Directives: v-scope, v-command, v-target / v-payload, v-bind-text, v-show
Dot‑paths only
No expression parser
No v-if, v-for, v-model
Click events only
~150 lines of runtime
~11 kB bundled including vapor‑chamber
The npm package ships once two or three real projects validate the API without changes.
🚀 Installation & Launch Parameters
v0.1 is a single-file drop‑in.
bash
npm install vapor-chamber
Copy src/directives/index.ts into your project.
In your Astro page:
ts
import { bus, busState } from '../directives/index.ts';
busState.count = 0;
busState.empty = true;
bus.register('cartAdd', (cmd) => {
busState.count += 1;
busState.empty = false;
});
The runtime binds all v-* attributes on DOMContentLoaded.
No configuration. No plugins.
🧑🚀 Author
Luciano Federico Pereira

Top comments (0)