belzium
v0.2.0-beta.0
Published
Framework frontend minimalista en TypeScript con compiler propio (.bel), reactividad, componentes, DI/IoC y pipelines de datos — cero dependencias de runtime.
Maintainers
Readme
Belzium (Beta)
Minimalist TypeScript frontend framework with its own compiler (.bel), reactivity, components, DI/IoC, data pipelines and a VSCode extension — zero runtime dependencies.
Install:
npm install belzium— zero runtime dependencies.
Live demo: run
npm run demoand openexamples/demo.htmlin your browser.
English
Why Belzium?
Most apps are built around a fragmentation problem: data lives in a CSV, an API, a store or an event — and every screen maps, filters and aggregates it by hand, with ad-hoc glue. That glue is where bugs breed and where every change hurts. Belzium treats that as the problem to solve: one reactive model that flows from your data pipelines to your components, instead of three disconnected paradigms.
- One mental model —
refs,computedand effects are the same primitives in the UI and in the data plane. - Three pillars — reactive UI + components, DI/IoC, and engine-agnostic pipelines (batch or live, same definition).
- Zero runtime dependencies — no framework tax for building a "simple" app.
Features
- Reactivity (Vue-style):
reactive(): reactive proxies over objects and collections (Map,Set, arrays).ref(): single reactive values with automatic unwrapping.computed(): derived values with caching and lazy recalculation.effect(),watch(),watchEffect(): effects that react to changes.EffectScope/effectScope(): group and dispose effects together.queueJob()/unqueueJob(): job queue with deduplication and microtask flush.toRaw(),toReactive(): conversion between raw and reactive objects.- Scheduler with job queue and deduplication (
flush: "sync" | "pre").
- Components:
@Component(),@UI(): decorators for declaring components.setup(props, { emit }): configuration function per component.- Public proxy with
refunwrapping. provide()/inject(): values shared between ancestors and descendants.useSlots(): slot content access.input()/output(): reactive props and event emission to parent.- Lifecycle:
onMounted(),onUnmounted(),onUpdated().
- DI / IoC:
createApp(App).mount(...): runtime bootstrap with component mounting.@Configuration()/@Bean()/@UI(): Spring-style declarative configuration (public barrel).@Service(): marks a class as a managed component.createApplication({ providers }): DI-only bootstrap.ApplicationContext:register,registerProvider,registerComponent,createScope,resolve,has.- Scopes:
SINGLETON(default),TRANSIENT,SCOPED.- Circular dependency detection and scope validation.
- Stores (global reactive state):
@Store(): marks a class as a global non-IoC store.useStore(StoreClass): returns a reactive singleton instance.resetStores(): clears all live instances (useful in tests).
- Hooks (reusable lifecycle logic):
@Hook(): marks a class as a component hook.useHook(HookClass): creates a new instance per consuming component.- Its
onMounted/onUnmountedmethods bind to the consumer's lifecycle.
- Custom Directives:
@Directive(): marks a class as a reusable template directive.- Usage in
.bel:<Clickable enabled={...}>{...}</Clickable>compiles toh(Clickable, { enabled: ... }, [...]).
.belCompiler → TypeScript:- JSX to
h()/text()calls. - XML directives:
<if>/<else>/<else-if>to ternary expressions. <for each={item of items} key={key}>to.map()with keyed VNodes.<switch value>/<case test>/<default>to IIFE withswitch.- PascalCase custom components to components.
- Auto-injection of runtime imports.
- Typed expression layer with semantic roles (
text,attrValue,eventHandler,spread,condition,iterable,key,discriminant,caseTest). - Structured errors (
CompileError) with line:column and snippet. - Dashed attributes (
data-*,aria-*, SVG) emitted correctly. - String literals allowed in
<switch value>/<case test>. - Note: the
.belcompiler is tooling (VSCode extension / build); it is not included in the npm runtime package.
- JSX to
- VSCode Extension:
- Syntax highlighting for
.bel(decorators, directives, keywords). - IntelliSense: completions, hover, go-to-definition.
- Diagnostics (syntactic + semantic).
- Semantic tokens and folding ranges.
- Syntax highlighting for
- VNodes:
h(),text(),createTextVNode().- Virtual tree diffing and patching.
isSameVNode()for type + key comparison.
Requirements
- Node.js 22+.
- TypeScript with standard decorators (TC39).
Getting started
npm install
npm test # runs the suite (537 tests / 50 files)
npm run demo # builds the demo page (open examples/demo.html)Examples
.bel component
import { ref } from "belzium";
@Component()
class Counter {
count = ref(0);
items = [1, 2, 3];
render() {
return (
<div>
<h1>Count: {this.count.value}</h1>
<if condition={this.count.value >= 3}>
<p>Big</p>
</if>
<else>
<p>Small</p>
</else>
<ul>
<for each={n of this.items} key={n}>
<li>Item {n}</li>
</for>
</ul>
<button onClick={() => this.count.value++}>+1</button>
</div>
);
}
}TypeScript equivalent
import { Component, ref, h, text, createApp } from "belzium";
@Component()
class Counter {
count = ref(0);
items = [1, 2, 3];
render() {
return h("div", null, [
h("h1", null, [text(`Count: ${this.count.value}`)]),
...(this.count.value >= 3
? [h("p", null, [text("Big")])]
: [h("p", null, [text("Small")])]),
h("ul", null, [
...this.items.map((n) =>
h("li", { key: n }, [text(`Item ${n}`)])
),
]),
h("button", { onClick: () => this.count.value++ }, [text("+1")]),
]);
}
}
createApp(Counter).mount(document.body);Fluent ETL pipeline (CSV → aggregation)
import {
Pipeline, source, runPipeline, reactiveEngine,
getPipelineMetadata,
} from "belzium";
@Pipeline()
class SalesETL {
raw = source.csv(`
dept,amount
ventas,10
ventas,20
it,5
`);
typed = this.raw.transform((row) => ({
dept: row.dept as string,
amount: Number(row.amount),
}));
byDept = this.typed.groupBy((row) => row.dept);
totals = this.byDept.aggregate(
(acc, row) => acc + row.amount,
0,
);
report = this.totals.sink((row) => console.log(row));
}
// Batch: runs once and gives per-node partial outputs.
const result = runPipeline(SalesETL);
// Reactive (Pulses): the same definition runs live with the reactive engine.
const live = runPipeline(SalesETL, reactiveEngine);
// Inject new rows: invalidates only the affected branch and batches sinks.
const rawId = getPipelineMetadata(SalesETL)!.nodes.find((n) => n.type === "source")!.id;
live.ref(rawId).value = [
{ dept: "ventas", amount: 30 },
];API
Reactivity
import { reactive, ref, computed, effect, watch, watchEffect, toRaw, isRef } from "belzium";
const state = reactive({ count: 0 });
const num = ref(0);
const doubled = computed(() => num.value * 2);
effect(() => console.log(state.count));
watch(() => state.count, (next, prev) => console.log(next, prev));
watchEffect(() => console.log(num.value));Components
import {
Component, createComponentInstance, setupComponent,
getCurrentInstance, provide, inject, useSlots,
onMounted, onUnmounted, onUpdated,
} from "belzium";
@Component()
class MyComponent {
setup() {
const theme = ref("dark");
return { theme };
}
render() {
return h("div", null, [text(this.theme.value)]);
}
}Pipelines
Typed node factories and both executors:
import {
PipelineGraph, createSourceNode, createTransformNode, createSinkNode,
LocalExecutor, ReactiveExecutor,
} from "belzium";
const graph = new PipelineGraph<number>();
const source = createSourceNode({ produce: () => [1, 2, 3] });
const doubled = createTransformNode({ map: (v) => v * 2 });
const sink = createSinkNode({ consume: (v) => console.log(v) });
graph.link(source, doubled);
graph.link(doubled, sink);
new LocalExecutor().execute(graph);
const live = new ReactiveExecutor().create(graph);
live.ref(source.id).value = [10, 20]; // re-runs the affected branchSchema + sources layer:
import { createSchema, field, validateRows, parseCSV, fetchCSV, silver, gold } from "belzium";
const schema = createSchema([field("id", "number"), field("name", "string")]);
const rows = parseCSV("id,name\n1,ana\n2,leo");
validateRows(rows, schema); // { valid, errors }
const { clean, invalid } = silver(rows, schema); // splits invalid rows
gold(clean, { by: (r) => r.id, reduce: (acc) => acc + 1, seed: 0 }); // {key,value}[]
await fetchCSV("https://api.example.com/data.csv"); // rows ready to inject into a pulseDI / IoC
import { Service, createApplication, ApplicationContext, createApp } from "belzium";
@Service()
class Logger {
log(msg: string) { console.log(`[APP] ${msg}`); }
}
@Service({ dependencies: [Logger] })
class UserService {
constructor(private logger: Logger) {}
hello() { this.logger.log("Hello from Belzium"); }
}
// With createApplication (DI only):
const app = createApplication({ providers: [Logger, UserService] });
app.resolve(UserService).hello();
// With ApplicationContext:
const ctx = new ApplicationContext();
ctx.registerProvider({ useClass: UserService, dependencies: [Logger] });
ctx.resolve(UserService).hello();
// With createApp (runtime):
const runtime = createApp(MyRootComponent);
runtime.mount(document.body);Stores
import { Store, useStore, resetStores, ref } from "belzium";
@Store()
class CounterStore {
count = ref(0);
}
// In any component:
const store = useStore(CounterStore);
store.count.value++; // reactive, re-renders consuming components
// In tests:
resetStores();Hooks
import { Hook, useHook, onMounted, ref } from "belzium";
@Hook()
class Timer {
elapsed = ref(0);
onMounted() { /* runs when the consuming component mounts */ }
onUnmounted() { /* runs when it unmounts */ }
}
// Inside a component:
const timer = useHook(Timer);
console.log(timer.elapsed.value);Directives
import { Directive, h, text } from "belzium";
@Directive()
class Clickable {
props!: Readonly<{ enabled?: boolean }>;
render() {
return h("button", null, [text(String(this.props.enabled))]);
}
}Usage in .bel template: <Clickable enabled={this.isEnabled}><span>Click</span></Clickable>
VNodes
import { h, text, createTextVNode, isSameVNode } from "belzium";
const vnode = h("div", { class: "card" }, [
text("Hello"),
h("span", null, [text("World")]),
]);
isSameVNode(vnode, h("div", null, [])); // true (same type, no key).bel Compiler
The compiler transforms .bel files (TypeScript + JSX + Belzium directives) into valid TypeScript:
| .bel Syntax | Output |
|---------------|--------|
| <div> | h("div", null, [...]) |
| <UserCard /> | h(UserCard, null, [...]) |
| {expr} | text(String(expr)) |
| <if condition={c}>...</if> <else>...</else> | ...((c) ? [...] : [...]) |
| <for each={n of items} key={k}>...</for> | ...items.map((n) => h(..., { key: k }, [...])) |
| <switch value={e}><case test={"v"}>...</case></switch> | IIFE with switch |
| <Clickable enabled={p}>...</Clickable> | h(Clickable, { enabled: p }, [...]) |
The compiler models each {} JS snippet with a typed semantic role
(text, attrValue, eventHandler, spread, condition, iterable, key,
discriminant, caseTest), enabling correct emission and tooling.
It accepts string literals in <switch value="a"> / <case test="b"> and emits
dashed attributes (data-*, aria-*, SVG) as valid keys. Compilation errors are
CompileError with line:column and snippet.
Usage within this repository (the compiler is tooling, not published to npm):
import { compile } from "./src/compiler";
const ts = compile(belSource, { importPath: "belzium" });VSCode Extension
The .bel extension is located in tools/belzium-language/ (id bel).
Installation (recommended, from the repo root):
npm install # install monorepo deps (esbuild, typescript, ...)
npm run package:language # build + package tools/belzium-language/belzium-language-0.1.1.vsix
code --install-extension tools/belzium-language/belzium-language-0.1.1.vsix --forceOr in one step: npm run install:language.
Note: after installing (or rebuilding) reload the VSCode window (
Ctrl+Shift+P→Developer: Reload Window) so the extension activates.dist/andtypes/are build artifacts (gitignored): a fresh clone must runnpm run package:languagebefore the extension works.
Features:
- TextMate syntax highlighting, including XML directives
(
<if>,<for>,<switch>,<case>,<default>,<else-if>,<else>) and decorators (@Component,@Store, ...) with dedicated colors. - Directive completions (
<if>,<for>,<switch>, ...) and full IntelliSense. - Hover with inferred types.
- Go-to-definition across
.belfiles. - Diagnostics (syntactic + semantic) with debounce.
- Semantic tokens and folding ranges.
- Debug: press
F5with the built-in "Run Belzium Extension" launch config (uses an Extension Development Host).
Vite Plugin
Dev-time compilation of .bel files (no JIT in the browser): the compiler runs
inside the Vite dev server / build and emits plain h()/text() code, lowering
standard decorators to es2020 and chaining source maps. Lives in
tools/vite-plugin-belzium/.
// vite.config.ts (dev, while the plugin is private in this repo)
import { defineConfig } from "vite";
import belzium from "./tools/vite-plugin-belzium/dist/index.js";
export default defineConfig({
plugins: [belzium()],
});Then import { Hello } from "./hello.bel" just works. The package has zero
runtime dependencies (dependencies: {}; peers: vite, esbuild,
typescript, already present in any Vite/TS project). HMR v1 reloads the page
on .bel edits.
Dev from this repo: npm run build:plugin, npm run test:plugin,
npm run package:plugin (tarball dry-run).
Pipelines & Data Engineering
Pipelines are the second pillar of Belzium: an engine-agnostic data layer that reuses the same reactive core. The same definition runs as a batch job or live, by switching the engine.
- Definition via
@Pipeline()— discovers the nodes chained as class fields (source.csv(...).transform(...).groupBy(...).aggregate(...).sink(...)) and stores the graph in metadata.getPipelineMetadata(Clazz).graphruns with anyPipelineEngineunchanged. - Swappable engines —
runPipeline(Clazz)(batch, defaultlocalEngine) orrunPipeline(Clazz, reactiveEngine)(live Pulses). - Nodes — typed factories
createSourceNode…createSinkNode(source, transform, filter, join, group, aggregate, sink) plus a fluent API on every node (transform,filter,groupBy,aggregate,join,sink). LocalExecutor— synchronous batch execution with topological order, cycle detection and per-node partial outputs.ReactiveExecutor— live execution on top of Pulses: sources→writableref, derived→computed, sinks→effects with batching; selective invalidation anddispose().- ETL / data engineering (
src/etl, depends on the pipeline core — never the other way around):- Schema —
createSchema()/field()+validateRows()/assertValidRows()withstring | number | boolean | date | anytypes. - Sources —
source.csv()/source.json()/source.from()(fluent builder),parseCSV()/parseJSON()(sync) andreadCSVFile()/fetchCSV()/fetchJSON()(async), ready to inject rows into pulses. - Medallion —
bronze()(raw ingest) →silver()(clean + validate →{ clean, invalid }) →gold()(keyed aggregations).
- Schema —
Project structure
src/
reactive/ Reactivity (proxies, refs, effects, scheduler, effect scopes)
component/ Components (decorators, lifecycle, slots, I/O, hooks, directives)
di/ DI/IoC (ApplicationContext, scopes, tokens, decorators)
runtime/ Runtime (createApp, VNodes, renderers/patching)
core/ Application (createApplication, bootstrap)
pipeline/ Pipeline definition (nodes + graph), engine-agnostic
decorator.ts @Pipeline (discovers fluent nodes via class fields) + metadata
graph.ts PipelineGraph (node registration and linking)
node.ts createNode with fluent API / link / isPipelineNode
run.ts runPipeline (runs a @Pipeline with the engine you choose)
nodes/ per-operation factories (source, transform, filter, join, group, aggregate, sink)
execution/ shared operators + LocalExecutor (batch)
+ ReactiveExecutor (Pulses) + engine.ts (local/reactive engines)
etl/ Data engineering: depends on the pipeline core (not vice versa)
schema/ createSchema / field + validateRows / assertValidRows
sources/ csv, json (sync) · file, http (async) · source (fluent builder)
medallion/ bronze → silver → gold
compiler.ts `.bel` → TypeScript compiler (tooling for the VSCode extension; not shipped in the npm package)
compiler/ astBuilder, codegen, templateLowering, expressionValidator, sourceMap, errors, nodes
tsxTransform.ts `.bel` → TSX transform for IDE support
store.ts @Store (global reactive state)
application.ts Public Application (runtime)
applicationBootstrap.ts
index.ts Public API
tools/
build-demo.mjs Builds the demo page (compiles examples/.bel → examples/demo.bundle.js)
belzium-language/ VSCode extension for `.bel` (id `bel`)
vite-plugin-belzium/ Dev-time Vite plugin for `.bel` (compiles .bel in dev/build; zero runtime deps)
examples/ Usage examples (e.g. `basic-di.ts`) + demo.html (live demo page)
img/ Logo and brand assets
tests/ 537 tests (50 files)
docs/ Language spec + compiler architectureScripts
npm test # run tests
npm run typecheck # verify types
npm run build # build dist/ (ESM bundle + .d.ts types)
npm run demo # build demo page: examples/demo.bundle.js (open examples/demo.html)
npm pack --dry-run # inspect tarball contents
npm run build:types # generate .d.ts files (VSCode extension)
npm run build:language # build VSCode extension
npm run typecheck:language # verify extension types
npm run package:language # build and package tools/belzium-language/*.vsix
npm run install:language # package and install the extension in VSCode
npm run build:plugin # build tools/vite-plugin-belzium/dist/index.js
npm run test:plugin # unit + Vite-build integration tests for the plugin
npm run package:plugin # tarball dry-run for the plugin
npm run test:e2e # real-browser E2E (Playwright/Chromium) of the Vite demo
npm run e2e:install # download the Playwright Chromium browser (first run)Publishing:
npm login
npm publish --tag beta
git tag v0.1.0-beta.0 && git push --tagsTests
npx vitest run537 tests covering: reactivity, components, DI/IoC, stores, hooks, directives, pipelines (engines, fluent, schema, sources, medallion), compiler, vite plugin, language service.
The unit/integration suite runs on jsdom. On top of it, npm run test:e2e
opens a real Chromium (Playwright) against the Vite demo and asserts the
component renders and reacts to events in the DOM.
Roadmap & status
- Current:
0.2.0-beta.0— 537 tests passing. The public API is not yet frozen: until 1.0 you should expect breaking changes between releases. - Blocking 1.0:
- Freeze the public API surface for reactivity, components and DI/IoC.
- Stabilize the
.belcompiler output across every supported template directive. - Set the stability contract for pipelines/ETL (schema + engines).
- Formal release of the VSCode extension and the docs site.
- Not on the radar: SSR, server components.
Contributing
Issues and PRs are welcome. See CONTRIBUTING.md for how to
run the test suite, typecheck and build. MIT licensed — see LICENSE.
Español
¿Por qué Belzium?
La mayoría de apps nacen de un problema de fragmentación: los datos viven en un CSV, una API, un store o un evento — y cada pantalla los mapea, filtra y agrega a mano, con pegamento ad-hoc. Ese pegamento es donde se crían los bugs y donde cada cambio duele. Belzium encara eso como el problema a resolver: un único modelo reactivo que fluye de tus pipelines de datos a tus componentes, en vez de tres paradigmas desconectados.
- Un solo modelo mental — los
ref,computedy efectos son los mismos primitivos en la UI y en la capa de datos. - Tres pilares — UI reactiva + componentes, DI/IoC y pipelines agnósticos al motor (batch o en vivo, misma definición).
- Cero dependencias de runtime — sin impuesto de framework para una app "simple".
Características
- Reactividad (estilo Vue):
reactive(): proxies reactivos sobre objetos y colecciones (Map,Set, arrays).ref(): variables reactivas individuales con desempaquetado automático.computed(): valores derivados con caché y recálculo perezoso.effect(),watch(),watchEffect(): efectos que reaccionan a los cambios.EffectScope/effectScope(): agrupa y detiene efectos de forma conjunta.queueJob()/unqueueJob(): cola de jobs con deduplicación y flush por microtask.toRaw(),toReactive(): conversión entre objetos crudos y reactivos.- Scheduler con cola de jobs y deduplicación (
flush: "sync" | "pre").
- Componentes:
@Component(),@UI(): decoradores para declarar componentes.setup(props, { emit }): función de configuración de cada componente.- Proxy público con desempaquetado de
ref. provide()/inject(): valores compartidos entre ancestros y descendientes.useSlots(): acceso al contenido de slots.input()/output(): props reactivas y emisión de eventos al padre.- Lifecycle:
onMounted(),onUnmounted(),onUpdated().
- DI / IoC:
createApp(App).mount(...): arranque del runtime con montaje de componentes.@Configuration()/@Bean()/@UI(): configuración declarativa estilo Spring (barrel público).@Service(): marca una clase como componente gestionado.createApplication({ providers }): arranque solo-DI.ApplicationContext:register,registerProvider,registerComponent,createScope,resolve,has.- Scopes:
SINGLETON(por defecto),TRANSIENT,SCOPED.- Detección de dependencias circulares y validación de scope.
- Stores (estado global reactivo):
@Store(): marca una clase como store global sin IoC.useStore(StoreClass): retorna una instancia singleton reactiva.resetStores(): limpia todas las instancias (útil en tests).
- Hooks (lógica reutilizable):
@Hook(): marca una clase como hook de componentes.useHook(HookClass): crea una instancia nueva por componente consumidor.- Sus métodos
onMounted/onUnmountedse enlazan al ciclo de vida del componente.
- Directivas personalizadas:
@Directive(): marca una clase como directiva reutilizable en templates.- Uso en
.bel:<Clickable enabled={...}>{...}</Clickable>compila ah(Clickable, { enabled: ... }, [...]).
- Compiler
.bel→ TypeScript:- JSX a llamadas
h()/text(). - Directivas XML:
<if>/<else>/<else-if>a expresiones ternarias. <for each={item of items} key={key}>a.map()con VNodes keyados.<switch value>/<case test>/<default>a IIFE conswitch.- Componentes custom PascalCase a componentes.
- Auto-inyección de imports del runtime.
- Capa de expresiones tipadas con roles semánticos (
text,attrValue,eventHandler,spread,condition,iterable,key,discriminant,caseTest). - Errores estructurados (
CompileError) con línea:columna y snippet. - Atributos con guion (
data-*,aria-*, SVG) emitidos correctamente. - Literales string admitidos en
<switch value>/<case test>. - Nota: el compiler
.beles tooling (extensión VSCode / build); no está incluido en el paquete npm (runtime).
- JSX a llamadas
- Extensión VSCode:
- Syntax highlighting para
.bel(decoradores, directivas, keywords). - IntelliSense: completions, hover, go-to-definition.
- Diagnostics (syntactic + semantic).
- Semantic tokens y folding ranges.
- Syntax highlighting para
- VNodes:
h(),text(),createTextVNode().- Diffing y patching de árbol virtual.
isSameVNode()para comparación por tipo + key.
Requisitos
- Node.js 22+.
- TypeScript con decoradores estándar (TC39).
Empezar rápido
npm install
npm test # ejecuta la suite (537 tests / 50 archivos)
npm run demo # genera la página demo (abrir examples/demo.html)Ejemplos
Componente .bel
import { ref } from "belzium";
@Component()
class Counter {
count = ref(0);
items = [1, 2, 3];
render() {
return (
<div>
<h1>Conteo: {this.count.value}</h1>
<if condition={this.count.value >= 3}>
<p>Grande</p>
</if>
<else>
<p>Pequeño</p>
</else>
<ul>
<for each={n of this.items} key={n}>
<li>Item {n}</li>
</for>
</ul>
<button onClick={() => this.count.value++}>+1</button>
</div>
);
}
}Equivalente en TypeScript
import { Component, ref, h, text, createApp } from "belzium";
@Component()
class Counter {
count = ref(0);
items = [1, 2, 3];
render() {
return h("div", null, [
h("h1", null, [text(`Conteo: ${this.count.value}`)]),
...(this.count.value >= 3
? [h("p", null, [text("Grande")])]
: [h("p", null, [text("Pequeño")])]),
h("ul", null, [
...this.items.map((n) =>
h("li", { key: n }, [text(`Item ${n}`)])
),
]),
h("button", { onClick: () => this.count.value++ }, [text("+1")]),
]);
}
}
createApp(Counter).mount(document.body);Pipeline ETL fluido (CSV → agregación)
import {
Pipeline, source, runPipeline, reactiveEngine,
getPipelineMetadata,
} from "belzium";
@Pipeline()
class SalesETL {
raw = source.csv(`
dept,amount
ventas,10
ventas,20
it,5
`);
typed = this.raw.transform((row) => ({
dept: row.dept as string,
amount: Number(row.amount),
}));
byDept = this.typed.groupBy((row) => row.dept);
totals = this.byDept.aggregate(
(acc, row) => acc + row.amount,
0,
);
report = this.totals.sink((row) => console.log(row));
}
// Batch: ejecuta una vez y entrega outputs parciales por nodo.
const result = runPipeline(SalesETL);
// Reactivo (Pulses): la misma definición corre en vivo con el motor reactivo.
const live = runPipeline(SalesETL, reactiveEngine);
// Inyectar nuevas filas: invalida solo la rama afectada y consolida los sinks.
const rawId = getPipelineMetadata(SalesETL)!.nodes.find((n) => n.type === "source")!.id;
live.ref(rawId).value = [
{ dept: "ventas", amount: 30 },
];API
Reactividad
import { reactive, ref, computed, effect, watch, watchEffect, toRaw, isRef } from "belzium";
const state = reactive({ count: 0 });
const num = ref(0);
const doubled = computed(() => num.value * 2);
effect(() => console.log(state.count));
watch(() => state.count, (next, prev) => console.log(next, prev));
watchEffect(() => console.log(num.value));Componentes
import {
Component, createComponentInstance, setupComponent,
getCurrentInstance, provide, inject, useSlots,
onMounted, onUnmounted, onUpdated,
} from "belzium";
@Component()
class MyComponent {
setup() {
const theme = ref("dark");
return { theme };
}
render() {
return h("div", null, [text(this.theme.value)]);
}
}Pipelines
Nodos con factories tipadas y ejecución con los dos motores:
import {
PipelineGraph, createSourceNode, createTransformNode, createSinkNode,
LocalExecutor, ReactiveExecutor,
} from "belzium";
const graph = new PipelineGraph<number>();
const source = createSourceNode({ produce: () => [1, 2, 3] });
const doubled = createTransformNode({ map: (v) => v * 2 });
const sink = createSinkNode({ consume: (v) => console.log(v) });
graph.link(source, doubled);
graph.link(doubled, sink);
new LocalExecutor().execute(graph);
const live = new ReactiveExecutor().create(graph);
live.ref(source.id).value = [10, 20]; // re-procesa la rama afectadaLa capa Schema + fuentes:
import { createSchema, field, validateRows, parseCSV, fetchCSV, silver, gold } from "belzium";
const schema = createSchema([field("id", "number"), field("name", "string")]);
const rows = parseCSV("id,name\n1,ana\n2,leo");
validateRows(rows, schema); // { valid, errors }
const { clean, invalid } = silver(rows, schema); // separa inválidas
gold(clean, { by: (r) => r.id, reduce: (acc) => acc + 1, seed: 0 }); // {key,value}[]
await fetchCSV("https://api.example.com/data.csv"); // filas listas para inyectar a un pulseDI / IoC
import { Service, createApplication, ApplicationContext, createApp } from "belzium";
@Service()
class Logger {
log(msg: string) { console.log(`[APP] ${msg}`); }
}
@Service({ dependencies: [Logger] })
class UserService {
constructor(private logger: Logger) {}
hello() { this.logger.log("Hola desde Belzium"); }
}
// Con createApplication (solo DI):
const app = createApplication({ providers: [Logger, UserService] });
app.resolve(UserService).hello();
// Con ApplicationContext:
const ctx = new ApplicationContext();
ctx.registerProvider({ useClass: UserService, dependencies: [Logger] });
ctx.resolve(UserService).hello();
// Con createApp (runtime):
const runtime = createApp(MyRootComponent);
runtime.mount(document.body);Stores
import { Store, useStore, resetStores, ref } from "belzium";
@Store()
class CounterStore {
count = ref(0);
}
// En cualquier componente:
const store = useStore(CounterStore);
store.count.value++; // reactiva, re-renderiza componentes que la lean
// En tests:
resetStores();Hooks
import { Hook, useHook, onMounted, ref } from "belzium";
@Hook()
class Timer {
elapsed = ref(0);
onMounted() { /* se ejecuta al montar el componente consumidor */ }
onUnmounted() { /* se ejecuta al desmontar */ }
}
// Dentro de un componente:
const timer = useHook(Timer);
console.log(timer.elapsed.value);Directivas
import { Directive, h, text } from "belzium";
@Directive()
class Clickable {
props!: Readonly<{ enabled?: boolean }>;
render() {
return h("button", null, [text(String(this.props.enabled))]);
}
}Uso en template .bel: <Clickable enabled={this.isEnabled}><span>Click</span></Clickable>
VNodes
import { h, text, createTextVNode, isSameVNode } from "belzium";
const vnode = h("div", { class: "card" }, [
text("Hello"),
h("span", null, [text("World")]),
]);
isSameVNode(vnode, h("div", null, [])); // true (mismo tipo, sin key)Compiler .bel
El compiler transforma archivos .bel (TypeScript + JSX + directivas Belzium) en TypeScript válido:
| Sintaxis .bel | Salida |
|-----------------|--------|
| <div> | h("div", null, [...]) |
| <UserCard /> | h(UserCard, null, [...]) |
| {expr} | text(String(expr)) |
| <if condition={c}>...</if> <else>...</else> | ...((c) ? [...] : [...]) |
| <for each={n of items} key={k}>...</for> | ...items.map((n) => h(..., { key: k }, [...])) |
| <switch value={e}><case test={"v"}>...</case></switch> | IIFE con switch |
| <Clickable enabled={p}>...</Clickable> | h(Clickable, { enabled: p }, [...]) |
El compiler modela cada fragmento JS de {} con un rol semántico tipado
(text, attrValue, eventHandler, spread, condition, iterable, key,
discriminant, caseTest), lo que permite emisión correcta y tooling.
Acepta literales string en <switch value="a"> / <case test="b"> y emite
atributos con guion (data-*, aria-*, SVG) como claves válidas. Los errores
de compilación son CompileError con línea:columna y snippet.
Uso dentro de este repositorio (el compiler es tooling, no se publica en npm):
import { compile } from "./src/compiler";
const ts = compile(belSource, { importPath: "belzium" });Extensión VSCode
La extensión para .bel se encuentra en tools/belzium-language/ (id bel).
Instalación (recomendada, desde la raíz del repo):
npm install # instalar deps del monorepo (esbuild, typescript, ...)
npm run package:language # compila y genera tools/belzium-language/belzium-language-0.1.1.vsix
code --install-extension tools/belzium-language/belzium-language-0.1.1.vsix --forceO en un solo paso: npm run install:language.
Nota: tras instalar (o recompilar), recarga la ventana de VSCode (
Ctrl+Shift+P→Developer: Reload Window) para que la extensión se active.dist/ytypes/son artefactos de build (gitignored): un clon fresco necesita ejecutarnpm run package:languageantes de usar la extensión.
Features:
- Syntax highlighting con TextMate grammar, incluidas las directivas XML
(
<if>,<for>,<switch>,<case>,<default>,<else-if>,<else>) y los decoradores (@Component,@Store, ...) con colores propios. - Completions de directivas (
<if>,<for>,<switch>, ...) e IntelliSense completo. - Hover con tipo inferido.
- Go-to-definition entre archivos
.bel. - Diagnostics (syntactic + semantic) con debounce.
- Semantic tokens y folding ranges.
- Debug:
F5con la configuración "Run Belzium Extension" ya incluida (usa Extension Development Host).
Plugin Vite
Compilación dev-time de archivos .bel (sin JIT en el navegador): el compiler
corre dentro del dev server / build de Vite y emite código h()/text() plano,
bajando los decoradores estándar a es2020 y encadenando source maps. Vive en
tools/vite-plugin-belzium/.
// vite.config.ts (dev, mientras el plugin es privado en este repo)
import { defineConfig } from "vite";
import belzium from "./tools/vite-plugin-belzium/dist/index.js";
export default defineConfig({
plugins: [belzium()],
});Luego import { Hello } from "./hello.bel" funciona directo. El paquete tiene
cero dependencias de runtime (dependencies: {}; peers: vite, esbuild,
typescript, ya presentes en cualquier proyecto Vite/TS). HMR v1 recarga la
página al editar un .bel.
Desde el repo: npm run build:plugin, npm run test:plugin,
npm run package:plugin (tarball dry-run).
Pipelines e ingeniería de datos
Los pipelines son el segundo pilar de Belzium: una capa de datos agnóstica al motor que reutiliza el mismo core reactivo. La misma definición corre como job batch o en vivo, solo cambiando el motor.
- Definición con
@Pipeline()— descubre los nodos encadenados como campos de la clase (source.csv(...).transform(...).groupBy(...).aggregate(...).sink(...)) y guarda el grafo en metadata.getPipelineMetadata(Clase).graphse ejecuta con cualquierPipelineEnginesin cambios. - Motores intercambiables —
runPipeline(Clase)(batch, por defectolocalEngine) orunPipeline(Clase, reactiveEngine)(Pulses en vivo). - Nodos — factories tipadas
createSourceNode…createSinkNode(source, transform, filter, join, group, aggregate, sink) y API fluido sobre cada nodo (transform,filter,groupBy,aggregate,join,sink). LocalExecutor— ejecución batch, síncrona, con orden topológico, detección de ciclos y outputs parciales por nodo.ReactiveExecutor— ejecución viva sobre Pulses: sources→refescribible, derivados→computed, sinks→efectos con batching; invalidación selectiva ydispose().- ETL / ingeniería de datos (
src/etl, depende del core del pipeline — nunca al revés):- Schema —
createSchema()/field()+validateRows()/assertValidRows()con tiposstring | number | boolean | date | any. - Fuentes —
source.csv()/source.json()/source.from()(builder fluido),parseCSV()/parseJSON()(sync) yreadCSVFile()/fetchCSV()/fetchJSON()(async), listas para inyectar filas a los pulses. - Medallion —
bronze()(ingesta cruda) →silver()(limpieza + validación →{ clean, invalid }) →gold()(agregaciones por clave).
- Schema —
Estructura del proyecto
src/
reactive/ Reactividad (proxies, refs, efectos, scheduler, effect scopes)
component/ Componentes (decoradores, lifecycle, slots, I/O, hooks, directives)
di/ DI/IoC (ApplicationContext, scopes, tokens, decorators)
runtime/ Runtime (createApp, VNodes, renderers/patching)
core/ Application (createApplication, bootstrap)
pipeline/ Definición del pipeline (nodos + grafo), agnóstica al motor
decorator.ts @Pipeline (descubre los nodos fluyentes por campos) + metadata
graph.ts PipelineGraph (registro y conexión de nodos)
node.ts createNode con API fluido / link / isPipelineNode
run.ts runPipeline (ejecuta una @Pipeline con el motor que elijas)
nodes/ factories por operación (source, transform, filter, join, group, aggregate, sink)
execution/ operadores compartidos + LocalExecutor (batch)
+ ReactiveExecutor (Pulses) + engine.ts (motores: local/reactive)
etl/ Ingeniería de datos: depende del core del pipeline (no al revés)
schema/ createSchema / field + validateRows / assertValidRows
sources/ csv, json (sync) · file, http (async) · source (builder fluido)
medallion/ bronze → silver → gold
compiler.ts Compiler .bel → TypeScript (tooling de la extensión VSCode; no va al paquete npm)
compiler/ astBuilder, codegen, templateLowering, expressionValidator, sourceMap, errors, nodes
tsxTransform.ts Transform .bel → TSX para soporte IDE
store.ts @Store (estado global reactivo)
application.ts Application pública (runtime)
applicationBootstrap.ts
index.ts API pública
tools/
build-demo.mjs Genera la página demo (compila examples/.bel → examples/demo.bundle.js)
belzium-language/ Extensión VSCode para `.bel` (id `bel`)
vite-plugin-belzium/ Plugin Vite dev-time para `.bel` (compila .bel en dev/build; cero deps de runtime)
examples/ Ejemplos de uso (p. ej. `basic-di.ts`) + demo.html (página demo en vivo)
img/ Logo y recursos de marca
tests/ 537 tests (50 archivos)
docs/ Language spec + compiler architectureScripts
npm test # ejecutar tests
npm run typecheck # verificar tipos
npm run build # generar dist/ (JS bundle + tipos .d.ts)
npm run demo # generar la página demo: examples/demo.bundle.js (abrir examples/demo.html)
npm pack --dry-run # inspeccionar el contenido del tarball
npm run build:types # generar archivos .d.ts (extensión VSCode)
npm run build:language # compilar extensión VSCode
npm run typecheck:language # verificar tipos de la extensión
npm run package:language # compilar y empaquetar tools/belzium-language/*.vsix
npm run install:language # empaquetar e instalar la extensión en VSCode
npm run build:plugin # compilar tools/vite-plugin-belzium/dist/index.js
npm run test:plugin # tests unit + integración (vite build) del plugin
npm run package:plugin # tarball dry-run del plugin
npm run test:e2e # E2E en navegador real (Playwright/Chromium) de la demo Vite
npm run e2e:install # descargar el navegador Chromium de Playwright (primera vez)Publicación:
npm login
npm publish --tag beta
git tag v0.1.0-beta.0 && git push --tagsTests
npx vitest run537 tests cubriendo: reactividad, componentes, DI/IoC, stores, hooks, directivas, pipelines (motores, fluido, schema, fuentes, medallion), compiler, plugin vite, language service.
La suite unit/integración corre sobre jsdom. Encima, npm run test:e2e
abre un Chromium real (Playwright) contra la demo de Vite y verifica que el
componente se renderiza y reacciona a eventos en el DOM.
Roadmap y estado
- Actual:
0.2.0-beta.0— 537 tests en verde. La API pública aún no está congelada: hasta 1.0 espera cambios que rompen compatibilidad entre releases. - Bloqueantes para 1.0:
- Congelar la superficie de API pública de reactividad, componentes y DI/IoC.
- Estabilizar la salida del compiler
.belen todas las directivas soportadas. - Fijar el contrato de estabilidad de pipelines/ETL (schema + motores).
- Release formal de la extensión VSCode y el sitio de docs.
- Fuera del roadmap: SSR, server components.
Contribución
Issues y PRs bienvenidos. Mira CONTRIBUTING.md para correr
tests, typecheck y build. Licencia MIT — ver LICENSE.
