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

tiny-rule.js

v0.0.3

Published

TypeScript rule engine with AST-based expressions, plugin system, and observability

Downloads

329

Readme

tiny-rule.js

TypeScript rule engine with AST-based expressions, a declarative plugin API, and built-in observability.

Tests TypeScript


Features

  • AST-based expressions — compose rules from typed literal, path, logical, and callable expression nodes
  • Plugin system — extend the engine with custom operators via PluginBuilder
  • Fluent rule builder — chainable RuleBuilder API for defining rules
  • Portability — import/export rule sets as JSON
  • Observability — emit lifecycle events (build:start, rule:end, rule:error, etc.)
  • Explanation — each compiled rule carries a human-readable explanation of its condition
  • Validation — built-in expression validator catches errors at build time
  • Fast — compiled closures avoid AST traversal at evaluation time
  • Optimizer — automatic constant folding and expression simplification during build

Installation

pnpm add tiny-rule.js
npm install tiny-rule.js
yarn add tiny-rule.js

Quick Start

import { RuleEngine } from "tiny-rule.js";
import { bool, and, num, path, str } from "tiny-rule.js/ast";
import { ComparisonPlugin, eq, gte, gt } from "tiny-rule.js/plugins/comparison";

const engine = new RuleEngine()
  .plugins(ComparisonPlugin)
  .rule((r) =>
    r
      .id("premium-user")
      .when(
        and(
          gte(path("age"), num(18)),
          eq(path("country"), str("PY")),
          gt(path("score"), num(700)),
          eq(path("active"), bool(true)),
        ),
      )
      .then("premium"),
  )
  .build();

const result = engine.evaluateOne("premium-user", {
  age: 21,
  country: "PY",
  score: 800,
  active: true,
});

console.log(result.matched); // true
console.log(result.explanation); // "age >= 18 && country == PY && score > 700 && active == true"

Guide

Expressions (AST)

tiny-rule.js uses a typed abstract syntax tree to represent conditions. Every expression node carries an __output type ("boolean", "number", "string", "date", or "unknown") enabling compile-time type checks.

Literal helpers — import from tiny-rule.js/ast:

| Helper | Output | Description | | --------- | ----------- | --------------- | | num(n) | "number" | Numeric literal | | str(s) | "string" | String literal | | bool(b) | "boolean" | Boolean literal | | date(d) | "date" | Date literal |

Logical helpers — import from tiny-rule.js/ast:

| Helper | Output | Description | | ----------------- | ----------- | ---------------------------- | | and(...exprs) | "boolean" | Logical AND | | or(...exprs) | "boolean" | Logical OR | | not(expr) | "boolean" | Logical NOT | | path(p) | unknown | Context path resolver | | regexp(exp, op) | "boolean" | RegExp test against a string |

Example:

import { and, or, not, path, num, str, bool, regexp } from "tiny-rule.js/ast";
import { gte, eq } from "tiny-rule.js/plugins/comparison";

const expr = and(
  gte(path("age"), num(18)),
  or(eq(path("role"), str("admin")), eq(path("role"), str("moderator"))),
  not(bool(false)),
);

Rules

Define rules with the fluent RuleBuilder API:

import { RuleBuilder } from "tiny-rule.js/rule";
import { path, num } from "tiny-rule.js/ast";
import { gte } from "tiny-rule.js/plugins/comparison";

const rule = new RuleBuilder()
  .id("is-adult")
  .name("Is Adult")
  .when(gte(path("user.age"), num(18)))
  .then("allow-access")
  .else("deny-access")
  .build();

Or inside the engine via the .rule() method:

engine.rule((r) =>
  r
    .id("is-adult")
    .when(gte(path("user.age"), num(18)))
    .then("allow-access"),
);

Rule interface:

| Field | Type | Description | | ----------- | --------------- | ------------------------------------------ | | id | string | Unique rule identifier | | name? | string | Human-readable name | | condition | AnyExpression | The condition expression (must be boolean) | | action | string[] | Action labels emitted when matched | | else | string[] | Action labels emitted when unmatched |

Rule Engine

The RuleEngine class orchestrates the full pipeline: validation, explanation, optimization, compilation, and evaluation.

import { RuleEngine } from "tiny-rule.js";

const engine = new RuleEngine()
  .plugins(ComparisonPlugin)
  .rule((r) =>
    r
      .id("r1")
      .when(gte(path("age"), num(18)))
      .then("adult"),
  )
  .rule((r) =>
    r
      .id("r2")
      .when(gte(path("score"), num(200)))
      .then("high-score"),
  )
  .build();

// Evaluate all rules
const allResults = engine.evaluate({ age: 25, score: 500 });

// Evaluate a single rule by ID
const singleResult = engine.evaluateOne("r1", { age: 25, score: 500 });

Returned IRuleExecution:

| Field | Type | Description | | ------------- | ---------- | ------------------------------------ | | ruleId | string | Matched rule ID | | matched | boolean | Whether the condition was satisfied | | explanation | string | Human-readable condition explanation | | action | string[] | Action labels from .then() | | else | string[] | Action labels from .else() |

Portability (Import / Export)

Export rules to JSON for storage or transport:

import { JsonRuleSerializer } from "tiny-rule.js/serializer";

const json = engine.export(new JsonRuleSerializer(), { pretty: true });

Import rules from JSON:

import { JsonRuleSerializer } from "tiny-rule.js/serializer";

const engine = new RuleEngine()
  .plugins(ComparisonPlugin)
  .import(new JsonRuleSerializer(), json)
  .build();

The serializer system is generic — any IRuleSerializer<T, O> implementation can be used:

interface IRuleSerializer<T, O = unknown> {
  serialize(payload: RuleSet, options?: O): T;
  deserialize(payload: T): RuleSet;
}

Observability (Events)

The engine emits lifecycle events. Subscribe via .on() and get an unsubscribe function:

const unsubscribe = engine.on("build:start", ({ rule }) => {
  console.log(`Building rule: ${rule.id}`);
});

engine.on("rule:end", ({ rule, result }) => {
  console.log(`${rule.id} matched: ${result.matched}`);
});

engine.on("build:error", ({ err }) => {
  console.error("Build failed:", err);
});

Available events:

| Event | Payload | | ------------- | ------------------------------------------------------------- | | build:start | { rule: IRule } | | build:end | { rule: ICompiledRule } | | build:error | { err: unknown } | | rule:start | { rule: ICompiledRule; ctx?: TCtx } | | rule:end | { rule: ICompiledRule; result: IRuleExecution; ctx?: TCtx } | | rule:error | { rule: ICompiledRule; err: unknown; ctx?: TCtx } |

Optimizer

The engine automatically optimizes expressions during the build phase using constant folding and simplification:

  • Boolean foldingand(true, expr)expr, and(false, expr)false, or(true, expr)true, or(false, expr)expr
  • Unary simplificationand(expr)expr (single operand unwrapped)
  • Empty reductionand()true, or()false
  • Plugin-level folding — plugins can fold constant sub-expressions (e.g., plus(num(10), num(20))num(30))

Optimization runs automatically on build() — no manual invocation needed.


Plugins

Plugins extend the engine with custom callable expression operators. The system has two layers:

  1. IPlugin — a bundle that registers one or more expression plugins into the ExpressionRegistry
  2. IExprPlugin — a single operator (e.g. >=, len, startsWith) with compile, explain, and optional verify functions

Creating a Plugin

Use PluginBuilder to define a custom operator:

import { PluginBuilder, ExpressionRegistry } from "tiny-rule.js/registry";
import { IPlugin } from "tiny-rule.js/plugins";
import { str, num } from "tiny-rule.js/ast";
import { asString, asNumber } from "tiny-rule.js/pipeline";

// 1. Build the expression plugin
const { plugin: lenP, helper: len } = new PluginBuilder<number>()
  .operator("len") // operator name used in expressions
  .output("number") // output type
  .explain((value) => `${value}.length`) // human-readable explanation
  .compile((value) => (ctx) => asString(value(ctx)).length)
  .build();

// 2. Create the plugin bundle
const StringPlugin: IPlugin = {
  load(registry: ExpressionRegistry) {
    registry.add(lenP);
  },
};

Using the Plugin

import { RuleEngine } from "tiny-rule.js";
import { str } from "tiny-rule.js/ast";
import { eq } from "tiny-rule.js/plugins/comparison";

const engine = new RuleEngine()
  .plugins(StringPlugin)
  .rule((r) =>
    r
      .id("short-name")
      .when(eq(len(str("alice")), num(5)))
      .then("valid"),
  )
  .build();

PluginBuilder API

| Method | Description | | ------------------------------------- | ---------------------------------------------- | | .operator(name) | Sets the operator string (e.g. "startsWith") | | .output(type) | Sets the output ExpressionType | | .explain((...args) => string) | Generates human-readable explanation | | .compile((...closures) => Closure) | Produces a compiled closure from sub-closures | | .optimize((expr) => { expression }) | Optional constant folding / simplification | | .verify((...exprs) => { errors }) | Optional validation logic | | .build() | Returns { plugin, helper } |

The helper function creates an AST ICallExpr node that references the operator:

const expr = len(str("hello"));
// => { kind: "call", operator: "len", operands: [...], __output: "number" }

Built-in ComparisonPlugin

The ComparisonPlugin registers six comparison operators:

| Helper | Operator | Description | | ------ | -------- | ---------------- | | gte | >= | Greater or equal | | gt | > | Greater than | | lte | <= | Less or equal | | lt | < | Less than | | eq | == | Equal | | neq | != | Not equal |

Built-in MathPlugin

The MathPlugin registers six arithmetic operators:

| Helper | Operator | Description | | ------ | -------- | -------------- | | plus | + | Addition | | sub | - | Subtraction | | mul | * | Multiplication | | div | / | Division | | mod | % | Modulus | | pow | ^ | Exponentiation |


API Reference

tiny-rule.js

export * from "./ast";
export * from "./core";
export * from "./plugins";
export * from "./plugins/comparison";
export * from "./registry";
export * from "./pipeline";
export * from "./monitor";

tiny-rule.js/ast

  • Types: ExpressionType, IExpression<T>, INumLiteralExpr, IStrLiteralExpr, IBoolLiteralExpr, IDateLiteralExpr, IPathExpr<T>, IRegExpExpr, IAndExpr, IOrExpr, INotExpr, ICallExpr<T>, BooleanExpression, AnyExpression<T>
  • Helpers: num(), str(), bool(), date(), and(), or(), not(), path(), regexp()

tiny-rule.js/core

  • RuleEngine<TCtx> — main engine class with .plugins(), .rule(), .build(), .evaluate(), .evaluateOne(), .import(), .export(), .on()
  • IRuleExecution — evaluation result
  • RuleEngineEventsMap<TCtx> — event type map

tiny-rule.js/pipeline

  • Compiler — compiles AnyExpression tree into executable Closure
  • Explanator — generates human-readable explanation strings
  • Validator — validates expression trees at build time
  • Optimizer — constant folding and expression simplification
  • Closure(ctx?: unknown) => TResult
  • asString(), asNumber(), asBoolean(), asDate(), asComparable()

tiny-rule.js/rule

  • RuleBuilder — fluent rule builder: .id(), .name(), .when(), .then(), .else(), .build()
  • IRule — rule descriptor
  • ICompiledRule — compiled rule with Closure condition

tiny-rule.js/registry

  • ExpressionRegistry — registry of callable expression plugins
  • PluginBuilder<T> — builder for creating expression plugins and their helper functions

tiny-rule.js/plugins

  • IPlugin{ load(registry: ExpressionRegistry): void }

tiny-rule.js/plugins/comparison

  • ComparisonPlugin — registers gte, gt, lte, lt, eq, neq
  • gte, gt, lte, lt, eq, neq — helper functions for expression construction

tiny-rule.js/plugins/math

  • MathPlugin — registers plus, sub, mul, div, mod, pow
  • plus, sub, mul, div, mod, pow — helper functions for expression construction

tiny-rule.js/serializer

  • JsonRuleSerializer — built-in JSON serializer with optional pretty-printing
  • IRuleSerializer<T, O> — generic serializer interface
  • RuleSet{ rules: IRule[] } serializable rule collection type

tiny-rule.js/monitor

  • TBus<T> — typed event bus with .on() and .emit()

Benchmarks

Benchmarks compare tiny-rule.js against json-rules-engine (v7) using identical rule logic on randomized contexts. They cover single-rule evaluation, multi-rule evaluation (10 to 10,000 rules), and build/compilation time.

Running Benchmarks

pnpm bench

This executes all benchmark suites via vitest bench:

| Suite | File | What it measures | | --------------------- | ----------------------------- | ------------------------------------- | | Single-rule execution | benchmark/exec.bench.ts | evaluateOne() vs jsonEngine.run() | | Multi-rule execution | benchmark/multiple.bench.ts | evaluate() with 10–10,000 rules | | Build time | benchmark/build.bench.ts | Compilation of 1–1,000 rules |


License

MIT