DEV Community

Cover image for Exo: Declarative Cross‑Section Coordination for Astro Without Components or Global Mutable State
Luciano Federico Pereira
Luciano Federico Pereira

Posted on

Exo: Declarative Cross‑Section Coordination for Astro Without Components or Global Mutable State

GitHub logo lucianofedericopereira / exo

Coordinate interactive sections of an Astro page through declarative HTML attributes—with zero component trees and no shared mutable state

EXO

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>
Enter fullscreen mode Exit fullscreen mode
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;
});
Enter fullscreen mode Exit fullscreen mode

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)