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

cruzo

v0.9.801

Published

Zero-dependency reactive framework with an expression VM

Downloads

449

Readme

C R U Z O

zero-dependency reactive framework + expression VM
no vdom. no magic build step. just html + rx + bytecode.

npm version License: MIT


// What Is This

cruzo is a tiny UI framework with:

  • reactive primitives (newRx, newRxFunc) inside components
  • template engine with {{ }} expressions compiled to bytecode VM
  • shared data bus via RxBucket
  • built-in router (RouteUrlBucket, routerService)
  • built-in HTTP client (HttpClient) with interceptors, cache, abort
  • optional UI components as separate entrypoints

If you want full control over DOM and a small runtime footprint, this is your lane.


// Install

npm i cruzo
  • Official site and examples: (https://cruzo.org)
  • VSCode extension (syntax): cruzo-syntax
  • Cruzo starter (Vite) https://github.com/MaratBektemirov/cruzo-starter

// Fast Start

import { AbstractComponent, componentsRegistryService } from "cruzo";

class CounterComponent extends AbstractComponent {
  static selector = "counter-component";

  count$ = this.newRx(0);

  getHTML() {
    return `
      <button onclick="{{root.count$.update(root.count$::rx + 1)}}">
        ping: {{root.count$::rx}}
      </button>
    `;
  }
}

componentsRegistryService.define(CounterComponent);
componentsRegistryService.initApp();

Place <counter-component></counter-component> in HTML and it works.


// RxBucket: shared config/state/value/events

import { AbstractComponent, RxBucket } from "cruzo";

class SearchPanelComponent extends AbstractComponent {
  static selector = "search-panel-component";

  innerBucket = new RxBucket({
    searchInput: { config: { placeholder: "find by title..." } },
    sortSelect: {
      config: {
        options: [
          { id: "new", value: "Newest" },
          { id: "old", value: "Oldest" },
        ],
      },
    },
  });

  query$ = this.newRxValueFromBucket(this.innerBucket, "searchInput");
  sort$ = this.newRxValueFromBucket(this.innerBucket, "sortSelect");

  getHTML() {
    return `
      <section>
        <!-- `toolbar-layout` is just layout wrapper: no props relay needed -->
        <toolbar-layout>
          <input-component component-id="searchInput" bucket-id="${this.innerBucket.id}"></input-component>
          <select-component component-id="sortSelect" bucket-id="${this.innerBucket.id}"></select-component>
        </toolbar-layout>

        <pre>query: {{root.query$::rx}}</pre>
        <pre>sort: {{root.sort$::rx}}</pre>
      </section>
    `;
  }
}

Use bucket-id + component-id to route descriptor/config/value into components. Even if UI is nested through layout wrappers, components share state via bucket directly (no prop drilling through every level).

Why RxBucket

  • avoids prop drilling by passing context through bucket-id/component-id instead of multi-level props relay
  • keeps state local to feature boundaries without forcing a single global store
  • works with existing newRx/newRxFunc primitives, so no extra architecture layer is required
  • lower boilerplate than redux/flux/ngrx (no action constants, reducers, effects setup for simple shared state)
  • predictable reactive updates without VDOM diffing and without store ceremony for component-level flows
  • easy incremental adoption: use RxBucket only where cross-component state/config sharing is needed

// Template Syntax Cheatsheet

Supported in templates:

  • text interpolation: {{ expr }}
  • events: onclick="{{ root.doStuff() }}"
  • reactive read: rxValue$::rx
  • one-time evaluation: once::expr
  • loop: repeat="{{ root.list$::rx }}"
  • conditional mount: attached="{{ root.flag$::rx }}"
  • lexical vars: let-item="{{ this::rx }}"
  • raw html: inner-html="{{ root.html$::rx }}"

Example:

<div repeat="{{root.items$::rx}}" let-name="{{this::rx.name}}">
  <button onclick="{{root.select(this::rx.id)}}">{{name}}</button>
</div>

<section attached="{{root.open$::rx}}">
  selected: {{root.selected$::rx ?? "none"}}
</section>

// Router

import { RouteUrlBucket } from "cruzo";

const routes = new RouteUrlBucket({
  home: {
    url: "/",
    componentSelectorUnbox: () => "home-page",
    routeSelectorUnbox: () => "#app",
  },
  docs: {
    url: "/docs/:slug",
    componentSelectorUnbox: () => "docs-page",
    routeSelectorUnbox: () => "#app",
  },
  oldDocs: {
    url: "/guide/*rest",
    redirectTo: "/docs/intro",
  },
});

routes.buildUrl("docs", { slug: "template-vm" }); // /docs/template-vm (history mode)
// With routerService.setHashMode(true): "#/docs/template-vm"
// Optional: buildUrl(key, params, query?)

routerService also provides:

  • pushHistory(href)
  • pushHistoryLink(event, href)
  • hrefIsActive(href, { startsWith, ignoreSearch })
  • reactive URL streams: pathname$ and search$ (both follow the routed path and query — see hash mode below)

Hash mode (#/path?query)

For static hosting without server-side fallback to index.html, enable hash routing so the real page path stays fixed (for example / or /app.html) while the SPA path lives in the fragment:

import { routerService } from "cruzo";

routerService.setHashMode(true);
routerService.update();

Behavior when hash mode is on:

  • Matching — route patterns such as /docs/:slug are matched against the path parsed from location.hash, not location.pathname. The expected shape is #/path/to/page with an optional query inside the hash: #/docs/intro?tab=api.
  • pathname$ / search$ — reflect that virtual path and query string (the part after #), not the browser pathname/search.
  • pushHistory — you can pass either a normal path (/docs/intro?tab=api) or a hash URL (#/docs/intro?tab=api); both are normalized to the same location.hash.
  • routes.buildUrl — returns #/path?query in the same shape as pushHistory expects (no leading document pathname). Optional URLSearchParams builds the query string; append a fragment yourself if you need #section in history mode.
  • redirectTo — still written as a path (e.g. /docs/intro); it is applied as the corresponding #/… entry on the current document URL.

// HTTP

import { HttpClient } from "cruzo";

const api = new HttpClient("https://api.example.com", {
  params: async (_method, _url, options) => {
    options.headers ??= {};
    options.headers.Authorization = "Bearer " + token();
  },
  error: async (_method, _url, _options, status) => {
    if (status === 401) logout();
  },
}, false, 30_000);

const me = await api.get("/me", { useCache: true });
await api.clearCache("GET", "/me");

Features:

  • auto content-type inference
  • JSON/text/form-urlencoded body normalization
  • AbortSignal support (factory(signal))
  • in-memory request cache (cacheTime + useCache)

// UI Components (separate imports)

cruzo now exposes UI components via dedicated subpaths:

import { InputComponent, InputConfig } from "cruzo/ui-components/input";
import { ButtonGroupComponent, ButtonGroupConfig } from "cruzo/ui-components/button-group";
import { SelectComponent, SelectConfig } from "cruzo/ui-components/select";
import { RouterLinkComponent, RouterLinkConfig } from "cruzo/ui-components/router-link";
import { SpinnerComponent, SpinnerConfig, SpinnerValue } from "cruzo/ui-components/spinner";
import { UploadComponent, UploadConfig } from "cruzo/ui-components/upload";
import { ModalComponent, ModalConfig } from "cruzo/ui-components/modal";

CSS is per-component. Shared tokens live in vars.css (:root: typography, spacing, surfaces, accents, radii, …). Import vars.css first, then only the stylesheets you need. Optional margin.css adds spacing utilities (.mt_*, .mb_*, …). Override tokens on :root or a wrapper after vars.css to theme.

UI_KIT — string prefix for all UI class names (same value as in the CSS files: cruzo-ui-component). It is defined in ui-components/const.ts and re-exported from the root package so your markup can stay aligned with the stylesheets without hardcoding the prefix:

import { UI_KIT } from "cruzo/ui-components/const";

const cls = `${UI_KIT}_button ${UI_KIT}_button-s ${UI_KIT}_button-primary`;
// → "cruzo-ui-component_button cruzo-ui-component_button-s cruzo-ui-component_button-primary"

Use ${UI_KIT}_… for element classes and ${UI_KIT}--… for modifiers. The value must match the prefix used in the bundled .css files (see ui-components/const.ts).

Stylesheet index (pick what you use): checkbox.css (multi-select), margin.css, button.css, button-group.css, input.css, select.css, spinner.css, modal.css, upload.css.

InputComponent uses base class cruzo-ui-component_input, plus optional classes from bucket state.cls.

Standalone button (button.css) — there is no <button> component; apply classes to a normal <button type="button">. Combine one size modifier with one variant (or neither for default look).

| Modifier | Class | | --- | --- | | (default) | .cruzo-ui-component_button | | Size | _xxs, _xs, _s, _m, _l, _xl, _xxl → e.g. .cruzo-ui-component_button-s | | Variant | .cruzo-ui-component_button-primary, .cruzo-ui-component_button-secondary |

<button type="button" class="cruzo-ui-component_button cruzo-ui-component_button-s cruzo-ui-component_button-primary">
  Save
</button>
import "cruzo/ui-components/vars.css";
import "cruzo/ui-components/input.css";
import "cruzo/ui-components/button.css";
import "cruzo/ui-components/checkbox.css";
import "cruzo/ui-components/margin.css";
import "cruzo/ui-components/button-group.css";
import "cruzo/ui-components/select.css";
import "cruzo/ui-components/spinner.css";
import "cruzo/ui-components/modal.css";
import "cruzo/ui-components/upload.css";

// Public API (current)

Root import:

import {
  Template,
  AbstractComponent,
  AbstractService,
  RxBucket,
  componentsRegistryService,
  routerService,
  RouteUrlBucket,
  HttpClient,
  HttpError,
  delay,
  debounce,
  arrayToHash,
} from "cruzo";

Also available via dedicated subpath:

import { delay, debounce, arrayToHash } from "cruzo/utils";

Also exported types:

import type {
  HttpRequestOptions,
  Interceptors,
  AbstractComponentConstructor,
  ComponentDescriptor,
  ComponentConnectedParams,
  BucketEvent,
  Rx,
  RxFunc,
} from "cruzo";

// Notes For Night Shift

  • no default export
  • no runtime deps
  • no global css reset bundled
  • UI components are opt-in by import path
  • template expressions run in Cruzo VM (not eval)

License

MIT