WebAssembly module for evaluating CEL (Common Expression Language) expressions in Node.js and browsers
npm install wasm-celWebAssembly module for evaluating CEL (Common Expression Language) expressions
in Node.js and browsers.
``bash`
npm install wasm-celor
pnpm add wasm-celor
yarn add wasm-cel
In Node.js, the library automatically loads the WASM module. Just import and
use:
`typescript
import { Env } from "wasm-cel";
// Create an environment with variable declarations
const env = await Env.new({
variables: [
{ name: "x", type: "double" },
{ name: "y", type: "double" },
{ name: "name", type: "string" },
{ name: "age", type: "double" },
],
});
// Compile an expression
const program = await env.compile("x + y");
// Evaluate with variables
const result = await program.eval({ x: 10, y: 20 });
console.log(result); // 30
// You can reuse the same program with different variables
const result2 = await program.eval({ x: 5, y: 15 });
console.log(result2); // 20
// Compile and evaluate multiple expressions with the same environment
const program2 = await env.compile(
'name + " is " + string(age) + " years old"',
);
const result3 = await program2.eval({ name: "Alice", age: 30 });
console.log(result3); // "Alice is 30 years old"
`
In browsers, you need to initialize the WASM module first by providing the WASM
bytes or URL.
#### With Vite (Recommended)
Vite can process and optimize the WASM file automatically:
`typescript
import { init, Env } from "wasm-cel";
import wasmUrl from "wasm-cel/main.wasm?url";
// Initialize with Vite-processed WASM URL
await init(wasmUrl);
// Now use the library normally
const env = await Env.new({
variables: [{ name: "x", type: "int" }],
});
const program = await env.compile("x + 10");
const result = await program.eval({ x: 5 });
console.log(result); // 15
`
Loading wasm_exec.js with Vite:
If you need to load wasm_exec.js dynamically (it's usually loaded via script
tag):
`typescript
import { init, Env } from "wasm-cel";
import wasmUrl from "wasm-cel/main.wasm?url";
import wasmExecUrl from "wasm-cel/wasm_exec.js?url";
// Initialize with both WASM and wasm_exec URLs
await init(wasmUrl, wasmExecUrl);
// Use the library
const env = await Env.new({ variables: [{ name: "x", type: "int" }] });
`
#### Without a Bundler (Native ES Modules)
For native ES modules without a bundler, you can import directly:
`html`
Loading wasm_exec.js:
You can either:
1. Load it via script tag before initializing:
`html`
2. Or pass the URL to init():`
typescript`
await init("/path/to/main.wasm", "/path/to/wasm_exec.js");
#### Other Browser Patterns
`typescript
import { init, Env } from "wasm-cel";
// Using a URL string
await init("/path/to/main.wasm");
// Using a URL object
await init(new URL("/main.wasm", window.location.origin));
// Using direct bytes (Uint8Array)
const response = await fetch("/main.wasm");
const bytes = new Uint8Array(await response.arrayBuffer());
await init(bytes);
// Using Response object
const response = await fetch("/main.wasm");
await init(response);
`
The package exports the following for direct imports:
- wasm-cel - Main entry point (auto-selects Node.js or browser)wasm-cel/browser
- - Browser-specific entry pointwasm-cel/main.wasm
- - WASM module filewasm-cel/wasm_exec.js
- - Go WASM runtime (for browsers)wasm-cel/wasm_exec.cjs
- - Go WASM runtime (CommonJS, for Node.js)
The library supports configurable CEL environment options to enable additional
CEL features. Options can be provided during environment creation or added later
using the extend() method.
#### OptionalTypes
Enables support for optional syntax and types in CEL, including optional field
access (obj.?field), optional indexing (list[?0]), and optional valueoptional.of(value)
creation ().
`typescript
import { Env, Options } from "wasm-cel";
const env = await Env.new({
variables: [
{
name: "data",
type: { kind: "map", keyType: "string", valueType: "string" },
},
],
options: [Options.optionalTypes()],
});
const program = await env.compile('data.?name.orValue("Anonymous")');
const result = await program.eval({ data: {} });
console.log(result); // "Anonymous"
`
#### ASTValidators
Enables custom validation rules during CEL expression compilation. Validators
can report errors, warnings, or info messages that are collected during
compilation and can prevent compilation or provide detailed feedback.
Location Information: Each AST node provides accurate location information
through nodeData.location with line and column properties, enabling
precise error reporting.
`typescript
import { Env, Options } from "wasm-cel";
const env = await Env.new({
variables: [
{
name: "user",
type: { kind: "map", keyType: "string", valueType: "string" },
},
],
options: [
Options.astValidators({
validators: [
// Validator that warns about accessing potentially unsafe fields
(nodeType, nodeData, context) => {
if (nodeType === "select" && nodeData.field === "password") {
return {
issues: [
{
severity: "warning",
message: "Accessing password field may not be secure",
location: nodeData.location, // Precise location from AST
},
],
};
}
},
// Validator that prevents certain function calls
(nodeType, nodeData, context) => {
if (
nodeType === "call" &&
nodeData.function === "dangerousFunction"
) {
return {
issues: [
{
severity: "error",
message: "Use of dangerousFunction is not allowed",
location: nodeData.location, // Precise location from AST
},
],
};
}
},
],
options: {
failOnWarning: false, // Don't fail compilation on warnings
includeWarnings: true, // Include warnings in results
},
}),
],
});
// Use compileDetailed() to see validation issues
const result = await env.compileDetailed("user.password");
if (result.success) {
console.log("Compiled with issues:", result.issues);
// Example issue: { severity: "warning", message: "...", location: { line: 1, column: 5 } }
const evalResult = await result.program.eval({
user: { password: "secret" },
});
} else {
console.log("Compilation failed:", result.error);
}
`
Available Node Types and Data:
- select: Field access (obj.field)nodeData.field
- : Field namenodeData.testOnly
- : Whether it's a test-only accessnodeData.location
- : Position in sourcecall
- : Function calls (func(args))nodeData.function
- : Function namenodeData.argCount
- : Number of argumentsnodeData.hasTarget
- : Whether it's a method callnodeData.location
- : Position in sourceliteral
- : Literal values ("string", 42, true)nodeData.value
- : The literal valuenodeData.type
- : Type namenodeData.location
- : Position in sourceident
- : Variable references (varName)nodeData.name
- : Variable namenodeData.location
- : Position in sourcelist
- : List literals ([1, 2, 3])nodeData.elementCount
- : Number of elementsnodeData.location
- : Position in sourcemap
- : Map literals ({"key": "value"})nodeData.entryCount
- : Number of entriesnodeData.location
- : Position in source
#### CrossTypeNumericComparisons
Enables cross-type numeric comparisons for ordering operators (<, <=, >,>=). This allows comparing values of different numeric types likedouble > int or int <= double. Note that this only affects ordering==
operators, not equality operators (, !=).
`typescript
import { Env, Options } from "wasm-cel";
const env = await Env.new({
variables: [
{ name: "doubleValue", type: "double" },
{ name: "intValue", type: "int" },
],
options: [Options.crossTypeNumericComparisons()],
});
// Now you can use cross-type ordering comparisons:
const program = await env.compile("doubleValue > intValue");
const result = await program.eval({ doubleValue: 3.14, intValue: 3 });
console.log(result); // true
// Works with all ordering operators
const program2 = await env.compile("intValue <= doubleValue + 1.0");
const result2 = await program2.eval({ doubleValue: 2.5, intValue: 3 });
console.log(result2); // true
`
You can also extend an environment with options after it's created:
`typescript
const env = await Env.new({
variables: [
{
name: "data",
type: { kind: "map", keyType: "string", valueType: "string" },
},
],
});
// Add options later
await env.extend([Options.optionalTypes()]);
const program = await env.compile('data.?greeting.orValue("Hello")');
const result = await program.eval({ data: {} });
console.log(result); // "Hello"
`
The library supports an inverted architecture where complex options can handle
their own JavaScript-side setup operations. This enables options that need to
register custom functions or perform other setup tasks.
Architecture Benefits:
- Options handle their own complexity and setup operations
- Environment class stays simple and focused
- Easy to add new complex options without modifying core code
- Clean separation of concerns
Complex options implement the OptionWithSetup interface and can perform setup
operations before being applied to the environment.
Creates a new CEL environment with variable declarations, optional function
definitions, and CEL environment options.
Parameters:
- options (EnvOptions, optional): Options including:variables
- (VariableDeclaration[], optional): Array of variablefunctions
declarations with name and type
- (CELFunctionDefinition[], optional): Array of custom functionoptions
definitions
- (EnvOptionInput[], optional): Array of CEL environment options
(like OptionalTypes)
Returns:
- Promise: A promise that resolves to a new Env instance
Example:
`typescript
import { Env, Options } from "wasm-cel";
const env = await Env.new({
variables: [
{ name: "x", type: "int" },
{
name: "data",
type: { kind: "map", keyType: "string", valueType: "string" },
},
],
options: [
Options.optionalTypes(), // Enable optional syntax like data.?field
],
});
`
Compiles a CEL expression in the environment.
Parameters:
- expr (string): The CEL expression to compile
Returns:
- Promise: A promise that resolves to a compiled Program
Example:
`typescript`
const program = await env.compile("x + 10");
Compiles a CEL expression with detailed results including warnings and
validation issues. This method is particularly useful when using ASTValidators
or when you need comprehensive feedback about the compilation process.
Parameters:
- expr (string): The CEL expression to compile
Returns:
- Promise: A promise that resolves to detailed compilationsuccess
results with:
- (boolean): Whether compilation succeedederror
- (string, optional): Error message if compilation failed completelyissues
- (CompilationIssue[]): All issues found during compilation (errors,program
warnings, info)
- (Program, optional): The compiled program if compilation succeeded
Example:
`typescript`
const result = await env.compileDetailed("user.password");
if (result.success) {
console.log("Compiled successfully");
if (result.issues.length > 0) {
console.log("Validation issues:", result.issues);
// Example issue: { severity: "warning", message: "Accessing password field may not be secure" }
}
const evalResult = await result.program.eval({
user: { password: "secret" },
});
} else {
console.log("Compilation failed:", result.error);
console.log("All issues:", result.issues);
}
Extends the environment with additional CEL environment options after creation.
Parameters:
- options (EnvOptionInput[]): Array of CEL environment option configurations
or complex options with setup
Returns:
- Promise: A promise that resolves when the environment has been
extended
Example:
`typescript
const env = await Env.new({
variables: [{ name: "x", type: "int" }],
});
// Add options after creation
await env.extend([Options.optionalTypes()]);
`
Typechecks a CEL expression in the environment without compiling it. This is
useful for validating expressions and getting type information before
compilation.
Parameters:
- expr (string): The CEL expression to typecheck
Returns:
- Promise: A promise that resolves to type information with atype
property containing the inferred type
Example:
`typescript
const env = await Env.new({
variables: [
{ name: "x", type: "int" },
{ name: "y", type: "int" },
],
});
// Typecheck a simple expression
const typeInfo = await env.typecheck("x + y");
console.log(typeInfo.type); // "int"
// Typecheck a list expression
const listType = await env.typecheck("[1, 2, 3]");
console.log(listType.type); // { kind: "list", elementType: "int" }
// Typecheck a map expression
const mapType = await env.typecheck('{"key": "value"}');
console.log(mapType.type); // { kind: "map", keyType: "string", valueType: "string" }
// Typechecking will throw an error for invalid expressions
try {
await env.typecheck('x + "invalid"'); // Type mismatch
} catch (error) {
console.error(error.message); // Typecheck error message
}
`
Evaluates the compiled program with the given variables.
Parameters:
- vars (Recordnull
evaluation. Defaults to .
Returns:
- Promise: A promise that resolves to the evaluation result
Example:
`typescript`
const result = await program.eval({ x: 5 });
Destroys the environment and marks it as destroyed. After calling destroy(),
you cannot create new programs or typecheck expressions with this environment.
However, programs that were already created from this environment will continue
to work until they are destroyed themselves.
Note: This method is idempotent - calling it multiple times is safe and has
no effect after the first call.
Example:
`typescript
const env = await Env.new();
const program = await env.compile("10 + 20");
// Destroy the environment
env.destroy();
// This will throw an error
await expect(env.compile("5 + 5")).rejects.toThrow();
// But existing programs still work
const result = await program.eval();
console.log(result); // 30
`
Destroys the compiled program and frees associated WASM resources. After calling
destroy(), you cannot evaluate the program anymore.
Note: This method is idempotent - calling it multiple times is safe and has
no effect after the first call.
Example:
`typescript
const program = await env.compile("10 + 20");
program.destroy();
// This will throw an error
await expect(program.eval()).rejects.toThrow();
`
Initializes the WASM module.
Node.js:
- No parameters needed - automatically loads from file system
- Called automatically by API functions, but can be called manually to
pre-initialize
Browser:
- Required: wasmBytes - The WASM module. Can be:Uint8Array
- - Direct bytesstring
- - URL to fetch the WASM file from (supports Vite-processed URLs)URL
- - URL object pointing to the WASM fileResponse
- - Fetch Response object containing WASM bytesPromise
- - Async import of WASM byteswasmExecUrl
- Optional: - URL to wasm_exec.js if it needs to be loaded
dynamically
- Must be called before using the library
Examples:
`typescript
// Node.js - no parameters
await init();
// Browser - with Vite
import wasmUrl from "wasm-cel/main.wasm?url";
await init(wasmUrl);
// Browser - with URL
await init("/path/to/main.wasm");
// Browser - with wasm_exec.js URL
await init("/path/to/main.wasm", "/path/to/wasm_exec.js");
// Browser - with direct bytes
const bytes = new Uint8Array(
await fetch("/main.wasm").then((r) => r.arrayBuffer()),
);
await init(bytes);
`
This library implements comprehensive memory leak prevention mechanisms to
ensure WASM resources are properly cleaned up.
Both Env and Program instances provide a destroy() method for explicit
cleanup:
`typescript
const env = await Env.new();
const program = await env.compile("x + y");
// When done, explicitly destroy resources
program.destroy();
env.destroy();
`
The library uses JavaScript's FinalizationRegistry (available in Node.js 14+)destroy()
to automatically clean up resources when objects are garbage collected. This
provides a best-effort safety net in case you forget to call .
Important limitations:
- FinalizationRegistry callbacks are not guaranteed to run immediately or at all
- They may run long after an object is garbage collected, or not at all in some
cases
- The timing is non-deterministic and depends on the JavaScript engine's garbage
collector
Best practice: Always explicitly call destroy() when you're done with an
environment or program. Don't rely solely on automatic cleanup.
The library uses reference counting to manage custom JavaScript functions
registered with environments:
1. When a program is created from an environment, reference counts are
incremented for all custom functions in that environment
2. When a program is destroyed, reference counts are decremented
3. Functions are only unregistered when their reference count reaches zero
This means:
- Programs continue to work even after their parent environment is destroyed
- Functions remain available as long as any program that might use them
still exists
- Functions are automatically cleaned up when all programs using them are
destroyed
Example:
`typescript
const add = CELFunction.new("add")
.param("a", "int")
.param("b", "int")
.returns("int")
.implement((a, b) => a + b);
const env = await Env.new({ functions: [add] });
const program = await env.compile("add(10, 20)");
// Destroy the environment - functions are still available
env.destroy();
// Program still works because functions are reference counted
const result = await program.eval();
console.log(result); // 30
// When program is destroyed, functions are cleaned up
program.destroy();
`
- Destroyed environments cannot create new programs or typecheck expressions
- Existing programs from a destroyed environment continue to work
- The environment entry is cleaned up when all programs using it are
destroyed
1. Always call destroy() when you're done with environments and programs
2. Destroy programs before environments if you want to ensure functions are
cleaned up immediately
3. Don't rely on automatic cleanup - it's a safety net, not a guarantee
4. In long-running applications, explicitly manage the lifecycle of
resources to prevent memory leaks
This package includes TypeScript type definitions. Import types as needed:
`typescript`
import {
Env,
Program,
Options,
EnvOptions,
VariableDeclaration,
TypeCheckResult,
CompilationResult,
CompilationIssue,
ValidationIssue,
ValidationContext,
ValidatorResult,
ASTValidatorFunction,
ASTValidatorsConfig,
CrossTypeNumericComparisonsConfig,
OptionalTypesConfig,
EnvOptionConfig,
EnvOptionInput,
} from "wasm-cel";
To build the package from source, you'll need:
- Go 1.21 or later
- Node.js 18 or later
- pnpm (or npm/yarn)
`bashInstall dependencies
pnpm install
Requirements
- Node.js: >= 18.0.0
- Browsers: Modern browsers with WebAssembly support (all current browsers)
Package Type
This is an ESM-only package. It uses modern ES modules and NodeNext module
resolution. If you're using TypeScript, make sure your
tsconfig.json has
"module": "NodeNext" or "moduleResolution": "NodeNext" for proper type
resolution.Environment Detection
The library automatically detects the environment:
- With bundlers (Vite, Webpack, Rollup, etc.): Uses
package.json`You don't need to change your import statements - the library handles the
environment detection automatically.
MIT