npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ape-egg/vibe

v2.0.0

Published

Runtime-first reactivity with optional compiler

Readme

Vibe

Version 2.0.0 (Beta) — 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/vibe

Status: 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';

      window.$ = vibe({ name: 'World', count: 0 });
    </script>
  </head>
  <body vibe-fouc>
    <h1>Hello, @[name]!</h1>
    <button onclick="$.count++">Clicked @[count] times</button>
  </body>
</html>

The vibe() Signature

window.$ = vibe(state, config?, targetSelector?);
  • state (object, required) — initial reactive state. Becomes window.$. Mutate freely ($.count++, $.user.name = 'Alice'); deep mutations trigger updates automatically. Methods on the object are preserved.

  • config (object, optional) — runtime configuration. Currently supported keys:

    • debug (boolean, default false) — colored console logs for every lifecycle phase (parse, hydrate, iterate, mutate, …). Useful for debugging reactivity issues.
  • targetSelector (string, optional) — CSS selector for the root element vibe attaches to. Defaults to document.body. Vibe parses, hydrates, and observes mutations only inside this root — anything outside (e.g. <head>, sibling <aside> elements) is ignored. If the selector matches nothing, vibe silently falls back to document.body. Pass 'html' to include <head> (e.g. for binding <title>@[pageTitle]</title>).

window.$ = vibe({ count: 0 }, { debug: true }, '#app');

Prevent FOUC

To prevent a flash of unstyled content while Vibe hydrates:

  1. Include vibe.css in your HTML
  2. Add the vibe-fouc attribute (or class="vibe-fouc") to an element — typically <body>

vibe.css hides [vibe-fouc] and .vibe-fouc until hydration completes; Vibe removes the attribute/class once it's done.

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 -->

The "array" position accepts any JS expression evaluated in scope, not just a state path:

<!-- each items.filter(i => i.active) as item -->...<!-- /each -->
<!-- each Array.from({ length: count }) as n, index -->...<!-- /each -->

Keyed iteration — give each row a stable identity with (keyExpr) so survivors keep their DOM (and listeners / animation state) when the list reorders or items are removed. The key comes before the optional index. Without a key, Vibe falls back to an index-coupled hash and bulk-re-renders the tail on reorder.

<!-- each rows as row (row.id) -->
<li>@[row.label]</li>
<!-- /each -->

<!-- key + index -->
<!-- each rows as row (row.id), index -->
<li>@[index]: @[row.label]</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. Slot content (between the tags) replaces <slot></slot> inside the component template:

<!-- /components/card.html -->
<div class="card">
  <h3>@[title]</h3>
  <slot></slot>
</div>

<!-- usage -->
<component src="/components/card.html" title="@[pageTitle]" theme="dark">
  <p>Content passed as slot</p>
</component>

<div class="component" src="..."> is an equivalent alternative to <component src="..."> for cases where standard HTML elements are required (validation, accessibility tooling).

Props can be reactive bindings (title="@[pageTitle]"), static literals (theme="dark"), or live objects/arrays passed through iteration scope (<component src="/card.html" card="@[card]"> inside <!-- each cards as card -->). Non-primitive props are stashed in an internal registry so the child template can dot/iterate into them (@[card.name], <!-- each card.abilities as a -->).

Component-Local State

For state scoped to a single component, import component from @ape-egg/vibe/component and call it from a <script type="module"> inside the component file:

<!-- /components/Counter.html -->
<script type="module">
  import component from '@ape-egg/vibe/component';
  component({
    count: 0,
    increment() { this.count++; }
  });
</script>

<button onclick="this.increment()">Clicked @[this.count] times</button>

How it works:

  1. component({...}) generates a unique id (e.g. _c0, _c1) and registers the state at $[id]
  2. The <script> and every following sibling is tagged with data-vibe-component-id="<id>"
  3. Inside that subtree, @[this.X.Y] is rewritten to @[_c0.X.Y] and event handlers like onclick="this.method()" or oninput="$.this.value = ..." are rewritten to address $[id]
  4. When the component leaves the DOM, its state entry is freed automatically

Multi-segment paths (@[this.user.profile.name]), conditionals (<!-- if this.editing -->), and iterations (<!-- each this.items as item -->) all resolve against the component's bucket. Global $ and component this.X coexist freely.

Drop-In Components (no vibe() needed)

component() auto-boots the runtime. You can drop a self-contained reactive block into any HTML page — no top-level vibe(...) call, no global state setup, no build step. Import directly from a CDN and the runtime wires itself up:

<component>
  <script type="module">
    import component from 'https://esm.sh/@ape-egg/vibe/component.js';
    component({ count: 0 });
  </script>
  <button onclick="this.count--">-</button>
  <span>Count: <strong>@[this.count]</strong></span>
  <button onclick="this.count++">+</button>
</component>

How it works:

  1. component({...}) claims the nearest unprocessed <component> (or <div class="component">) wrapper, registers state at $[id], and tags the wrapper with data-vibe-component-id
  2. Internally it calls ensureBoot() (from @ape-egg/vibe/boot), which initializes window.$, parses the DOM, hydrates bindings, and starts the MutationObserver — exactly once, even if multiple <component> blocks call component()
  3. From there, @[this.X], onclick="this.fn()", and <!-- if this.X --> work as documented

Multiple drop-in blocks on the same page each get their own state bucket. They can read each other's state via global $['_c0'].count if they need to coordinate, but in most drop-in cases they're independent.

Lifecycle Hooks

$.on('ready', () => {});            // once, after initial parse + first hydrate + all components mounted
$.on('afterUpdate', (cur, prev) => {}); // every state change (batched per microtask)
$.on('afterDomMutation', () => {});  // after every MutationObserver batch

$.ready is also exposed as a Promise (await $.ready), useful for code that captured window.$ before boot.

Subtree Reconciliation (advanced)

$.reconcile(el, html) and $.renderComponent(rawHtml, props, slot, opts) are public-but-advanced APIs used by the vite plugin's HMR path. Their shape may evolve; treat them as plumbing rather than application code for now.

Dehydrate

Skip reactive processing for an element:

<code vibe-dehydrate>@[this] displays literally</code>

Raw HTML ($.unsafe)

@[expr] escapes its output (textContent) — safe by default. To render trusted markup instead, wrap the value in $.unsafe(...); the binding sets innerHTML. This is Vibe's equivalent of Svelte {@html} / Vue v-html / React dangerouslySetInnerHTML.

<p>@[$.unsafe(description)]</p>
  • The binding must be the sole content of its element (innerHTML semantics). Mixed into surrounding text, it falls back to escaped literal text.
  • Injected markup is inert@[...] / <!-- if --> / <!-- each --> inside it are not processed (matches Svelte {@html}); the subtree is opaque to the MutationObserver.
  • Fully reactive — re-renders on value change; works in iterations, conditionals, and components. Compiled mode paints the markup at stamp time and re-hydrates at runtime.
  • Trusted input only — no sanitizing. Don't pass user-supplied strings.

Internal Names (don't collide)

These are used by the runtime — don't repurpose them in your code:

  • window.__vibeManifest, window.__vibeCompiling, window.__vibeComponents, window.__vibeIterProps — internal registries
  • data-vibe-component-id, data-vibe-iter-prop — element-level bookkeeping attributes (set automatically)

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

  1. Proxy-based statewindow.$ intercepts property changes
  2. Deep reactivity — Nested mutations trigger updates automatically ($.obj.nested.prop = x)
  3. DOM parsing — Finds all @[...] bindings on load
  4. Surgical updates — Only affected elements re-render
  5. 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 | sh

Note: 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 --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 optimization

Or 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,
    "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 runtime
  • iterationsAsIs: false — Optimize iterations (default) or use runtime rendering
  • runtimeAsIs: false — Generate manifest (default) or skip for runtime-only

Defaults (when no config):

  • source: ./
  • output: ./compiled
  • components: <source>/components
  • pages: <source>/pages
  • assets: <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:

  1. Copies package.json and lockfile to output
  2. Runs <package-manager> install --production in output directory
  3. Removes package.json and lockfile from output (cleanup)

This ensures:

  • Compiled output only includes runtime dependencies (from dependencies, not devDependencies)
  • Local node_modules is 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 --watch

Features:

  • 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-arm64

Supported platforms:

  • vibe-compiler-darwin-arm64 (macOS Apple Silicon) ✅ Included
  • vibe-compiler-darwin-x64 (macOS Intel)
  • vibe-compiler-linux-x64
  • vibe-compiler-linux-arm64
  • vibe-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


Future improvements to refactor

Dependency tracking uses string matching, not AST

affected() determines which bindings to re-hydrate by string-matching expression text against state key names. When matching fails, a safety fallback marks the binding as affected on every state change.

  • runtime/affected.js:15matchesKey does exact/prefix match, then word-boundary search
  • runtime/affected.js:~122shouldAffect = noMatch || ... fallback for unmatched expressions
  • runtime/affected.js:~248 — same fallback for name bindings

Every time matchesKey can't prove an expression is unrelated to a state change, that binding re-hydrates. A proper AST-based dependency extraction at parse time would eliminate the fallback entirely: each binding stores its exact dependency set, and runtime just checks if any dep key changed.

HTML lowercases attribute names — breaks name binding matching

The browser lowercases all attribute names during HTML parsing. <page @[pageName]> becomes @[pagename] in the DOM. Every value hydrated into an attribute NAME position loses its case. @[myCoolPage] → DOM stores @[mycoolpage], causing comparison mismatches against camelCase state keys.

Handled today by case-insensitive fallbacks in three places:

  • runtime/hydrate.js:~32 — case-insensitive state key lookup when evaluating name bindings (clone path)
  • runtime/affected.js:~235 — case-insensitive matchesKey wrapper for name bindings
  • runtime/iterate.js — case-insensitive lookup against stateKeys inside compileBatchFn's name-binding emission (batch path)

All three are workarounds for HTML's behavior. A cleaner architecture would centralize the case-insensitive state key resolution into one helper, or normalize the name binding expression to canonical state-key case at first evaluation and cache it on the tree node.

Related: value hydration into attribute values is case-preserving

Only attribute NAMES are lowercased by HTML, not values. So title="@[pageName]" and @[pageName] in text content preserve case and work without fallbacks — the issue is specific to name bindings.