CEL (Common Expression Language) to JavaScript transpiler. Compiles CEL expressions into JavaScript functions.
import{compile}from"cel2js";const{ evaluate }=compile('name.startsWith("J") && age > 21');evaluate({name: "Jane",age: 30n});// trueevaluate({name: "Bob",age: 25n});// falsenpm install cel2js
cel2js is a transpiler, not an interpreter. The pipeline:
CEL source --> Peggy parser --> CelExpr AST --> ESTree transformer --> astring --> JS source --> new Function()
Once compiled, evaluate() is a direct function call with no AST walking, no interpretation loop, no overhead beyond the generated code itself. The generated code calls runtime helpers for type-safe operations (overflow checks, cross-type comparison, error handling).
Hot path (pre-compiled, evaluate only):
| Expression | ops/sec | % of native |
|---|---|---|
true | 15.5M | 97% |
x + y * 2 | 7.6M | 49% |
name.startsWith("J") && name.size() > 3 | 8.8M | 57% |
x > 0 ? x * 2 : -x | 9.4M | 60% |
[1,2,3,4,5].filter(x, x > 2) | 8.0M | 68% |
request.auth.claims.email.endsWith("@example.com") && request.method == "GET" | 4.8M | 41% |
"Native" baseline is a hand-written new Function() doing the equivalent work.
Cold path (compile + evaluate): 22K-94K ops/sec depending on expression complexity.
vs other CEL implementations (hot path):
| cel2js | @marcbachmann/cel-js | @bufbuild/cel | cel-js | |
|---|---|---|---|---|
x + y * 2 | 7.6M | 4.3M | 2.6M | 600K |
[1,2,3,4,5].filter(x, x > 2) | 8.0M | 2.3M | 238K | -- |
| real-world field access | 4.8M | 2.2M | 376K | -- |
3,119 tests passing. Zero failures.
- 2,344 tests from the cel-spec conformance suite (28 suites)
- 664 supplementary tests harvested from existing JS implementations
- 43 parser unit tests
- 19 individual conformance tests skipped -- all require proto binary deserialization or schema-level type info not available at JS runtime (e.g.,
google.protobuf.Anyunpacking, strong enumtype())
functioncompile(cel: string,options?: CompileOptions): CompileResult;interfaceCompileOptions{container?: string;// CEL container (namespace) for identifier resolution}interfaceCompileResult{evaluate: (bindings?: Record<string,unknown>)=>unknown;source: string;// The generated JavaScript source}evaluate() throws CelError (extends Error, name === "CelError") on runtime errors: division by zero, type mismatch, overflow, missing field.
import{CelUint,celUint,isCelUint,CelType,isCelType}from"cel2js";importtype{CelValue,CompileOptions,CompileResult}from"cel2js";CEL int64 maps to JavaScript BigInt. Pass integers as BigInt literals:
evaluate({age: 30n});// correctevaluate({age: 30});// wrong -- 30 is a double in CEL, not an integerFor unsigned integers, wrap with CelUint:
import{celUint}from"cel2js";evaluate({port: celUint(8080n)});Overflow is checked: int64 results outside [-2^63, 2^63-1] and uint64 results outside [0, 2^64-1] produce errors.
compile() returns the generated JavaScript source in source. For x + y * 2:
((_rt,_qb,x,y)=>{return_rt.add(x,_rt.mul(y,2n));})For name.startsWith("J") && age > 21:
((_rt,_qb,name,age)=>{let_a=undefined;let_b=undefined;return(_a=_rt.startsWith(name,"J"),_b=_rt.gt(age,21n),_a===false ? false : _b===false ? false :
_a===true&&_b===true ? true : undefined);})_rt is the runtime helpers object. _qb is reserved for qualified bindings. All CEL operations route through runtime helpers for type safety and overflow checking. Logical AND/OR evaluate both sides and use commutative error absorption (false wins over error for AND, true wins over error for OR) -- no try/catch anywhere in generated code.
Pure ESM, ES2022 target. No Node-specific APIs -- works in any environment that supports ES2022 and new Function() (will not work in CSP-restricted environments that block dynamic code generation).
One runtime dependency: astring (ESTree-to-JS code generator, zero transitive dependencies).
MIT