A TypeScript binding for the Common Expression Language (CEL) using cel-rust. This project provides a Node.js native module that allows you to use CEL in your TypeScript/JavaScript projects.
Common Expression Language (CEL) is an expression language created by Google that implements common semantics for expression evaluation. It's a simple language for expressing boolean conditions, calculations, and variable substitutions. CEL is used in various Google products and open-source projects for policy enforcement, configuration validation, and business rule evaluation.
npm install @kevinmichaelchen/cel-typescript
Node.js 18 or later is required.
See the full language definition for a complete overview of CEL.
There are two ways to use CEL expressions in your code:
For simple use cases where you evaluate an expression once:
import { evaluate } from "@kevinmichaelchen/cel-typescript";
// Basic string and numeric operations
await evaluate(
// 1️⃣ Provide a CEL expression
"size(message) > 5",
// 2️⃣ Provide a context object
{ message: "Hello World" },
); // true
// Complex object traversal and comparison
await evaluate("user.age >= 18 && user.preferences.notifications", {
user: {
age: 25,
preferences: { notifications: true },
},
}); // true
For better performance when evaluating the same expression multiple times with different contexts:
import { CelProgram } from "@kevinmichaelchen/cel-typescript";
// Compile the expression once
const program = await CelProgram.compile(
"items.filter(i, i.price < max_price).size() > 0",
);
// Execute multiple times with different contexts
await program.execute({
items: [
{ name: "Book", price: 15 },
{ name: "Laptop", price: 1000 },
],
max_price: 100,
}); // true
await program.execute({
items: [
{ name: "Phone", price: 800 },
{ name: "Tablet", price: 400 },
],
max_price: 500,
}); // true
// Date/time operations using timestamp() macro
const timeProgram = await CelProgram.compile(
'timestamp(event_time) < timestamp("2025-01-01T00:00:00Z")',
);
await timeProgram.execute({
event_time: "2024-12-31T23:59:59Z",
}); // true
Note
Performance measurements on an Apple M3 Pro show that compiling a complex CEL
expression (with map/filter operations) takes about 1.4ms, while execution
takes about 0.7ms. The one-step evaluate()
function takes roughly 2ms as it
performs both steps.
Consider pre-compiling expressions when:
- You evaluate the same expression repeatedly with different data
- You're building a rules engine or validator that reuses expressions
- You want to amortize the compilation cost across multiple evaluations
- Performance is critical in your application
For one-off evaluations or when expressions change frequently, the convenience
of evaluate()
likely outweighs the performance benefit of pre-compilation.
This project consists of three main components:
-
cel-rust: The underlying Rust implementation of the CEL interpreter, created by clarkmcc. This provides the core CEL evaluation engine.
-
NAPI-RS Bindings: A thin Rust layer that bridges cel-rust with Node.js using NAPI-RS. NAPI-RS is a framework for building pre-compiled Node.js addons in Rust, providing:
- Type-safe bindings between Rust and Node.js
- Cross-platform compilation support
- Automatic TypeScript type definitions generation
-
TypeScript Wrapper: A TypeScript API that provides a clean interface to the native module, handling type conversions and providing a more idiomatic JavaScript experience.
Note
Only ESM is supported by this package.
The native module is built using NAPI-RS and provides cross-platform support:
- Platform-specific builds are named
cel-typescript.<platform>-<arch>.node
(e.g.,cel-typescript.darwin-arm64.node
for Apple Silicon Macs) - NAPI-RS generates a platform-agnostic loader (
index.js
) that automatically detects the current platform and loads the appropriate.node
file - The module interface is defined in
index.d.ts
which declares the types for the native module - At runtime, the TypeScript wrapper (
src/index.ts
) uses the NAPI-RS loader to dynamically load the correct native module - This structure allows for seamless cross-platform distribution while maintaining platform-specific optimizations
The npm package is relatively large (~37 MB unpacked) because it includes pre-compiled native binaries for all supported platforms:
- macOS (x64, arm64)
- Linux (x64, arm64)
- Windows (x64)
However, when you install this package, npm will only extract the .node
file
for your platform. For example:
- On Apple Silicon, only
cel-typescript.darwin-arm64.node
(~7.4 MB) is used - On Windows, only
cel-typescript.win32-x64.node
is used - On Linux, only
cel-typescript.linux-x64.node
orcel-typescript.linux-arm64.node
is used
This is sometimes a pattern for packages with native bindings. For comparison:
better-sqlite3
: 10.2 MB unpackedcanvas
: 408 kB unpackedsharp
: 522 kB unpacked
Tree-shaking (dead code elimination) primarily works with JavaScript modules and
doesn't affect native binaries. The .node
files are loaded dynamically at
runtime based on the platform, so tree-shaking can't eliminate unused platform
binaries during build time.
However, the JavaScript/TypeScript portion of this package is tree-shakeable.
For example, if you only use evaluate()
and not CelProgram
, a bundler like
webpack or Rollup can exclude the unused code.
When you build this project:
- The Rust code in
src/lib.rs
is compiled into a native Node.js addon (.node
file) using NAPI-RS - The TypeScript code in
src/index.ts
is compiled to JavaScript - The native module is loaded by Node.js when you import the package
The build process creates several important files:
.node
file: The compiled native module containing the Rust codeindex.js
: The compiled JavaScript wrapper around the native moduleindex.d.ts
: TypeScript type definitions generated from the Rust code
This project uses git submodules for its Rust dependencies. To get started:
# Clone with submodules
git clone --recurse-submodules https://github.com/clarkmcc/cel-typescript.git
# Or if you've already cloned the repository
git submodule update --init --recursive
After cloning:
npm install # Install dependencies
npm run build # Build the project
npm test # Run tests
[License information here]