Menu

Guides

Compiler internals

You don't need this page to build a Kovo app — everywhere else in these docs, and in your codebase, you author components in TSX. Read it when you want to see what the compiler did with that TSX: reviewing emitted output, debugging a stamp, ejecting a component, or building tooling.

The pipeline#

You author in TSX: JSX renders, inline closures, one component per file. The compiler parses each .tsx module and emits a small set of plain files with a 1:1 mapping — x.tsx produces exactly x.server.js and x.client.js, plus shared generated registry types.

cart-badge.tsx ──► cart-badge.server.js   (render with derived stamps)
               ──► cart-badge.client.js   (named handler exports, update plans)
               ──► generated/registries.d.ts

Everything below is real compiler output, regenerated from the compiler on every build of this site, so it can't drift from what @kovojs/compiler actually emits.

What you write#

tsx
import { component } from '@kovojs/core';

export const CartBadge = component({
  queries: { cart: cartQuery },
  state: () => ({ count: 0 }),
  render: (props, state) => (
    <button class="badge" onClick={() => state.count += 1}>
      Cart (<span>{cart.count}</span>)
    </button>
  ),
});

Ordinary TSX: a query dependency, island state, an inline closure mutating that state, and a plain {cart.count} expression bound into the markup.

What the compiler emits#

The server module. Your render, with the wiring derived and stamped as attributes.

js
import { component } from '@kovojs/core';
import { componentLiveTargetRenderer, registerGeneratedLiveTargetRenderer } from '@kovojs/server/internal/wire';


export const CartBadge = component({
  queries: { cart: cartQuery },
  state: () => ({ count: 0 }),
  render: (props, state) => (
    <button class="badge" on:click="/c/__v/560fa203/src/cart-badge.client.js#CartBadge$button_click" kovo-c="cart-badge" kovo-deps="cart" kovo-fragment-target="cart-badge" kovo-live-component="cart-badge/cart-badge" kovo-state="{&quot;count&quot;:0}">
      Cart (<span data-bind="cart.count">{cart.count}</span>)
    </button>
  ),
});
CartBadge.name = "cart-badge/cart-badge";

export const CartBadge$liveTargetRenderer = registerGeneratedLiveTargetRenderer(componentLiveTargetRenderer({
  component: CartBadge,
  componentId: "cart-badge/cart-badge",
}));

Three derivations did the work:

  • The inline closure became a handler referenceon:click pointing at a named export in a versioned client module URL. The name is source-derived (CartBadge$button_click), appears in the HTML, and therefore can never be mangled. The compiler also emits a lint here — KV210 warning: Anonymous handler; name it for stable identity. — nudging you to name the handler yourself for a stable identity across refactors.
  • {cart.count} became data-bind="cart.count" — a typed path into the cart query's result shape, checked at compile time.
  • The query dependency became kovo-deps="cart" and the island state a serialized kovo-state stamp, which is how mutations later know this element wants fresh fragments.

You never write these stamps. Hand-writing one that duplicates the derivation, or drifts from it, is a compile error.

The client module. Named handler exports plus the compiled update plan for each query this component consumes:

js
// @kovojs-ir
import { applyCompiledQueryUpdatePlan, handler } from '@kovojs/runtime/generated';

export const CartBadge$button_click = handler((_event, ctx) => {
  return ctx.state.count += 1;
});

export const CartBadge$queryUpdatePlans = {
  "cart"(root, value) {
    return applyCompiledQueryUpdatePlan(root, "cart", value, { bindings: true, derives: [], stamps: [], templateStamps: [] });
  },
};

The closure's capture channels are checked here: a handler may reach component/query state via ctx, element params via data-p-*, and module scope. Anything else is a compile error whose message shows what the closure would have compiled to and how to fix it.

Emitted output is valid source#

The emitted form isn't a private artifact — it's authorable Kovo source. The marker comment // @kovojs-ir is informational, not load-bearing. Two consequences follow:

  1. The fixpoint. Compiling the compiler's own output is a no-op, byte-for-byte, and CI enforces it with assertFixpoint. If a compiler change ever makes lowering non-idempotent, the gate fails — there's no drift channel between what you wrote and what ships.
  2. Ejection. Any component can drop to its emitted form and keep working: check the emitted files in, delete the .tsx, and you own the output. Nothing else in the toolchain knows the difference. This is the escape hatch for something the TSX front-end can't express yet — though if you hit that, it's usually a compiler gap worth filing.

Render equivalence#

The compiler proves the lowered render and your authored render produce identical markup, with assertRenderEquivalence. The equivalence facts ride the same registry emit the rest of the toolchain consumes. Combined with the fixpoint, this is why fragments can be rendered by the same functions that render full pages without diverging from them.

Reading emitted output in practice#

  • vp build writes the compiled modules; in dev they're compiled on demand and served under the versioned /c/ namespace. Module URLs are immutable, and old versions stay published across deploys so long-lived documents never 404 on first interaction.
  • The generated registries.d.ts is where declare-once typing comes from: handler modules, fragment targets, query update plans, and route/mutation registries all land as interface augmentations, which is why renames propagate as type errors instead of stale strings.
  • kovo explain component <Name> graph.json shows the compiler's view of any component — queries consumed, fragments targeted, handlers exported — without reading the emitted files at all. See Reading kovo check & kovo explain.
Spec & diagnostics

The TSX-to-{server,client}.js pipeline and the 1:1 mapping: SPEC §5.1, §5.2. Derived stamps (kovo-deps, data-bind, kovo-state): SPEC §4.8; typed binding paths: SPEC §6.2; the mutation fragment contract behind kovo-state: SPEC §9.1. A hand-written stamp that drifts from the derivation is KV222; one that duplicates it is KV223 (SPEC §11.3). A handler reaching outside its allowed capture channels (ctx, data-p-*, module scope) is KV201 (SPEC §4.3). Source-derived, unmanglable handler names: Constitution #1. Emitted output as authorable source, the // @kovojs-ir marker, the fixpoint (assertFixpoint), and ejection: Constitution #3, SPEC §5.2. Render equivalence (assertRenderEquivalence): SPEC §5.2. Declare-once registry typing: SPEC §6.1. Immutable versioned module URLs retained across deploys: SPEC §6.6.