@ape-egg/vibe
v1.7.0
Published
Runtime-first reactivity with optional compiler
Downloads
1,184
Maintainers
Readme
Vibe
Version 1.6.0 (Alpha) — A runtime-first reactive framework with optional compilation.
No virtual DOM. No build step required. Just modern JavaScript. When you need production optimizations, add the optional Rust-based compiler.
npm install @ape-egg/vibeStatus: Functional and ready to use, but expect bugs and breaking changes daily until stable. Use in production at your own risk.
Vibe Runtime
The core reactive runtime. Works directly in the browser without any build tools.
Quick Start
<html>
<head>
<link rel="stylesheet" href="./node_modules/@ape-egg/vibe/vibe.css" />
<script type="module">
import vibe from './node_modules/@ape-egg/vibe/index.js';
vibe({ name: 'World', count: 0 });
</script>
</head>
<body vibe-fouc>
<h1>Hello, @[name]!</h1>
<button onclick="$.count++">Clicked @[count] times</button>
</body>
</html>Prevent FOUC
To prevent a flash of unstyled content while Vibe hydrates:
- Include
vibe.cssin your HTML - Add the
vibeattribute to an element (typically<body>) to mark the hydration boundary
The CSS targets [vibe] and hides it until hydration completes. Once Vibe finishes processing, it removes the attribute, revealing your content.
Reactive Bindings
Vibe supports bindings in three positions:
Text content — Inside element tags:
<div>@[firstName] @[lastName]</div>
<h1>Hello, @[name]!</h1>Attribute values — In attribute value position:
<input value="@[username]" />
<div class="@[theme]" style="color: @[color]"></div>
<a href="@[url]">Link</a>Attribute names — In attribute name position (useful for dynamic attributes):
<icon @[iconName]></icon>
<button @[state]>Click me</button>CSS — Bindings also work in style tags:
<style>
.box {
background: @[themeColor];
}
</style>Iteration
<!-- each items as item, index -->
<li>@[index]: @[item]</li>
<!-- /each -->Nested iteration with dot paths:
<!-- each categories as category -->
<!-- each category.items as item -->
<span>@[item.name]</span>
<!-- /each -->
<!-- /each -->Conditionals
<!-- if user.isAdmin -->
<admin-badge>Admin</admin-badge>
<!-- else -->
<span>User</span>
<!-- /if -->Components
Runtime component loading with props and slots:
<component src="/components/card.html" title="@[pageTitle]" theme="dark">
<p>Content passed as slot</p>
</component>Dehydrate
Skip reactive processing for an element:
<code vibe-dehydrate>@[this] displays literally</code>Reserved Words & Gotchas
Vibe uses specific patterns and keywords that have special meaning. Avoid using these for other purposes to prevent unexpected behavior:
Classes & Attributes
vibe-fouc— Class or attribute for FOUC (Flash of Unstyled Content) prevention. Automatically removed after hydration completes.<body vibe-fouc> <!-- or class="vibe-fouc" --> </body>vibe-dehydrate— Class or attribute to skip reactive processing. Useful for displaying literal@[...]syntax in documentation.<code vibe-dehydrate>@[variable]</code> <!-- or class="vibe-dehydrate" -->
Element Names & Classes
<component>— Element name for component system. Used withsrcattribute for runtime component loading, or as a wrapper for inlined components.<component src="/path/to/component.html"></component>class="component"— Alternative syntax for components using standard HTML elements. Useful for HTML validation or accessibility.<div class="component" src="/path/to/component.html"></div><slot>— Element name for component content injection. Gets replaced with content passed between component tags.<!-- In component file --> <slot></slot> <!-- Usage --> <component src="..."> <p>This replaces the slot</p> </component>
Comment Syntax
<!-- each -->/<!-- /each -->— Iteration block markers.<!-- each items as item --> <!-- /each --><!-- if -->/<!-- else -->/<!-- /if -->— Conditional block markers.<!-- if condition --> <!-- else --> <!-- /if -->
Binding Syntax
@[...]— Reactive binding syntax. Reserved for state references.<div>@[variable]</div>
Global Properties
window.$— Global reactive state object. All reactive data should be accessed through this.window.$ = state({ count: 0 }); $.count++; // Triggers reactive updateswindow.__vibeManifest— Internal manifest data. Used by the compiler for optimization. Don't modify.window.__vibeCompiling— Internal flag. Set totruewhen running in compiler context.
Data Attributes
data-vibe-component-id— Internal attribute for component scoping. Automatically added to component elements. Don't use manually.
Event Names
vibe:ready— Custom event fired when Vibe completes initial hydration.document.addEventListener('vibe:ready', () => { console.info('Vibe is ready'); });
Special Attribute Meanings
srcon<component>or<div class="component">— Triggers runtime component fetching. Components withoutsrcare treated as inline wrappers.<!-- Both work the same way --> <component src="/components/card.html"></component> <div class="component" src="/components/card.html"></div>dehydrate— Attribute or class name to skip reactive processing (alias forvibe-dehydrate).
Deep Reactivity
Vibe uses recursive proxies to detect changes at any nesting level:
// All of these trigger reactive updates:
$.user.name = 'Alice';
$.todos[2].completed = true;
$.config.theme.colors.primary = '#007bff';No need for immutable update patterns or spread operators. Just mutate and Vibe handles the rest.
How It Works
- Proxy-based state —
window.$intercepts property changes - Deep reactivity — Nested mutations trigger updates automatically (
$.obj.nested.prop = x) - DOM parsing — Finds all
@[...]bindings on load - Surgical updates — Only affected elements re-render
- MutationObserver — Tracks dynamically added elements
Vibe Compiler
Optional build step for production optimization. The compiler provides component inlining, iteration optimization, watch mode, and hydration manifests while preserving directory structure.
Installation
The compiler requires Rust for non-macOS ARM64 platforms:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shNote: A prebuilt binary for macOS ARM64 is included. Other platforms will compile from source automatically.
Usage
# Initialize config in package.json
bunx vibe compile --init
# Basic compilation
bunx vibe compile
# or shorthand
bunx vibe c
# Watch mode (incremental compilation)
bunx vibe compile --watch
# Production build
bunx vibe compile --minify --source-maps
# With options
bunx vibe compile --verbose # Step-by-step logging
bunx vibe compile --minify # Minify output
bunx vibe compile --elements-as-is # Keep custom elements as-is
bunx vibe compile --validate # Validate HTML syntax
bunx vibe compile --source-maps # Generate source maps
bunx vibe compile --node-modules-as-is # Copy node_modules as-is
bunx vibe compile --components-as-is # Skip component inlining
bunx vibe compile --runtime-as-is # Skip manifest generation
bunx vibe compile --iterations-as-is # Skip iteration optimizationOr via npm scripts:
{
"scripts": {
"compile": "vibe compile",
"compile:watch": "vibe compile --watch",
"compile:prod": "vibe compile --minify --source-maps"
}
}Then run with npm run compile or bun compile.
Configuration
Add to your package.json:
{
"vibe-compiler": {
"source": "./",
"output": "./compiled",
"components": "components",
"pages": "pages",
"assets": "assets",
"minify": false,
"elementsAsIs": false,
"reservedElements": [],
"sourceMaps": false,
"validate": false,
"nodeModulesAsIs": false,
"componentsAsIs": false,
"runtimeAsIs": false,
"iterationsAsIs": false
}
}Key Options:
elementsAsIs: false— Transform custom elements to divs (default)reservedElements: []— Additional element names to reserve (appends to built-in HTML5 elements + "component")componentsAsIs: false— Inline components (default) or keep separate for runtimeiterationsAsIs: false— Optimize iterations (default) or use runtime renderingruntimeAsIs: false— Generate manifest (default) or skip for runtime-only
Defaults (when no config):
source:./output:./compiledcomponents:<source>/componentspages:<source>/pagesassets:<source>/assets
Asset Handling
The compiler uses a whitelist approach. These file types are automatically copied:
Fonts: .ttf, .otf, .woff, .woff2, .eot
Images: .png, .jpg, .jpeg, .gif, .svg, .webp, .avif, .ico
Media: .mp4, .webm, .ogg, .mp3, .wav, .flac, .aac
Documents: .pdf
Data: .json, .xml, .csv
HTML, CSS, and JavaScript files are processed by the compiler.
Node Modules (Self-Contained Output)
By default, the compiler creates deployable output by installing production dependencies directly into the output directory:
- Copies
package.jsonand lockfile to output - Runs
<package-manager> install --productionin output directory - Removes
package.jsonand lockfile from output (cleanup)
This ensures:
- Compiled output only includes runtime dependencies (from
dependencies, notdevDependencies) - Local
node_modulesis never modified - Faster than copying (no intermediate copy step)
Package Manager Detection:
Auto-detects based on lockfiles: bun.lockb, pnpm-lock.yaml, yarn.lock, package-lock.json
Opt-out:
Set nodeModulesAsIs: true in config or use --node-modules-as-is flag to copy node_modules as-is.
Watch Mode
Watch mode enables incremental compilation with intelligent dependency tracking:
bunx vibe compile --watchFeatures:
- Incremental builds — Only recompiles changed files (~100ms)
- Dependency tracking — Changes to components trigger recompilation of pages using them
- Debounced — 300ms debounce prevents excessive compilation during rapid changes
- Delta output — First compile shows full output, subsequent compiles show only changes
Component System
Components are automatically inlined during compilation with full support for props and slots:
<!-- Source: components/card.html -->
<div class="card">
<h2>@[title]</h2>
<p>@[description]</p>
<slot></slot>
</div>
<!-- Usage in page -->
<component src="/components/card.html" title="My Card" description="Card description">
<p>Slot content</p>
</component>
<!-- Compiled output -->
<component>
<div class="card">
<h2>My Card</h2>
<p>Card description</p>
<p>Slot content</p>
</div>
</component>Custom element syntax: Components can also use custom element syntax (e.g., <card> → auto-converts to <component src="/components/card.html">).
Opt-out: Set componentsAsIs: true to keep components as separate files for runtime loading.
Iteration Optimization
The compiler generates optimized batch functions for <!-- each --> loops, providing 2-3x faster rendering:
<!-- Source -->
<!-- each items as item, index -->
<li>@[index]: @[item]</li>
<!-- /each -->
<!-- Compiled to optimized batch function -->Opt-out: Set iterationsAsIs: true to use runtime rendering for all iterations.
Output Structure
Mirrors source structure:
src/ compiled/
├── pages/ ├── pages/
│ └── index.html │ └── index.html
├── components/ │ (components inlined)
│ └── counter.html
├── assets/ ├── assets/
│ └── image.png │ └── image.png
└── node_modules/ └── node_modules/
└── @ape-egg/ └── @ape-egg/
└── vibe/ └── vibe/Element Transformation
By default (elementsAsIs: false), custom HTML elements are transformed to divs with classes for better HTML validity:
<!-- input -->
<counter-header>Count</counter-header>
<!-- output -->
<div class="counter-header">Count</div>Set elementsAsIs: true or use --elements-as-is flag to keep custom elements unchanged.
Note: The <component> element is a framework element and is never transformed.
Reserved Element Names
The compiler validates component filenames against reserved element names (all HTML5 elements + "component" + your custom list). This prevents confusing bugs where components share names with HTML elements.
{
"vibe-compiler": {
"reservedElements": ["my-custom-element", "another-reserved-name"]
}
}Case-sensitive validation: nav.html conflicts with <nav> and will error, but Nav.html is allowed.
Building Native Binaries
For distribution without Rust:
cd node_modules/@ape-egg/vibe/compiler/src
# Build for current platform
cargo build --release
# Copy binary to native directory
cp target/release/vibe-compiler ../native/vibe-compiler-darwin-arm64Supported platforms:
vibe-compiler-darwin-arm64(macOS Apple Silicon) ✅ Includedvibe-compiler-darwin-x64(macOS Intel)vibe-compiler-linux-x64vibe-compiler-linux-arm64vibe-compiler-win32-x64.exe
The CLI wrapper automatically falls back to cargo run when native binaries aren't present.
Browser Support
Modern browsers with Proxy and MutationObserver support.
License
ISC
