🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@marcbachmann/cel-js

Package Overview
Dependencies
Maintainers
1
Versions
87
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@marcbachmann/cel-js - npm Package Compare versions

Comparing version
7.5.2
to
7.5.3
+6
-3
lib/evaluator.js

@@ -110,5 +110,8 @@ import {createRegistry, RootContext} from './registry.js'

#evaluateAST(ast, ctx) {
ctx = new RootContext(this.#registry, ctx)
if (!ast.checkedType) this.#evalTypeChecker.check(ast, ctx)
return this.#evaluator.eval(ast, ctx)
if (ast.checkedType) {
return ast.evaluate(this.#evaluator, ast, new RootContext(this.#registry, ctx))
} else {
this.#evalTypeChecker.check(ast, (ctx = new RootContext(this.#registry, ctx)))
return ast.evaluate(this.#evaluator, ast, ctx)
}
}

@@ -115,0 +118,0 @@ }

import {celTypes} from './registry.js'
import {objKeys, isArray} from './globals.js'
import {objKeys, isArray, hasOwn} from './globals.js'
const dynType = celTypes.dyn

@@ -427,8 +427,16 @@

const empty = Object.create(null)
function fieldAccess(left, right, ast, ev) {
switch (left?.constructor) {
case undefined:
case Object:
case Map:
return ev.mapType.field(left, right, ast, ev)
case Object: {
const v = hasOwn(left || empty, right) ? left[right] : undefined
if (v !== undefined) return (ev.debugType(v), v)
break
}
case Map: {
const v = left.get(right)
if (v !== undefined) return (ev.debugType(v), v)
break
}
case Array:

@@ -440,5 +448,5 @@ case Set:

if (t) return t.type.field(left, right, ast, ev)
if (typeof left === 'object') unsupportedType(ev, left.constructor.name)
else if (typeof left === 'object') unsupportedType(ev, left.constructor.name)
}
return ev.nullType.field(left, right, ast, ev)
throw new ev.Error(`No such key: ${right}`, ast)
}

@@ -445,0 +453,0 @@

@@ -503,4 +503,5 @@ import {UnsignedInt} from './functions.js'

const globalLexer = new Lexer()
export class Parser {
lexer = null
lexer = globalLexer
input = null

@@ -516,3 +517,2 @@ maxDepthRemaining = null

this.registry = registry
this.lexer = new Lexer()
}

@@ -519,0 +519,0 @@

@@ -246,5 +246,2 @@ /**

/** Look up the declared type for a variable name. */
getType(name: string): TypeDeclaration | undefined
/** Look up the fallback value (built-ins) for a name. */

@@ -277,5 +274,2 @@ getValue(name: string): any

getValue(name: string): any
/** Resolve a declared type by name, respecting overlays. */
getType(name: string): TypeDeclaration | undefined
}

@@ -42,2 +42,43 @@ import {EvaluationError} from './errors.js'

// prettier-ignore
const valueTypeMatchers = {
dyn (v, ev) {
// prettier-ignore
switch (typeof v) {
case 'string': case 'bigint': case 'number': case 'boolean': return true
case 'object':
switch (v ? v.constructor : v) {
case null: case undefined: case Object: case Map: case Array: case Set: return true
default:
if (ev.objectTypesByConstructor.get(v.constructor)) return true
}
}
return !!ev.debugType(v)
},
string (v) { return typeof v === 'string' },
int (v) { return typeof v === 'bigint' },
double (v) { return typeof v === 'number' },
bool (v) { return typeof v === 'boolean' },
null (v) { return v === null },
bytes (v) { return v instanceof Uint8Array },
uint (v) { return v instanceof UnsignedInt },
type (v) { return v instanceof Type },
list (v) {
switch (v?.constructor) {
case Array: case Set: return true
default: return false
}
},
map (v) {
switch (typeof v === 'object' && v ? v.constructor : null) {
case undefined: case Object: case Map: return true
default: return false
}
},
optional (v) { return v instanceof Optional },
message (v, ev) { return this === ev.debugType(v) }
}
valueTypeMatchers.param = valueTypeMatchers.dyn
export class TypeDeclaration {

@@ -69,15 +110,6 @@ #matchesCache = new WeakMap()

this.matchesValueType = valueTypeMatchers[name] || valueTypeMatchers[kind]
objFreeze(this)
}
/** @deprecated Please use .hasDynType */
hasDyn() {
return this.hasDynType
}
/** @deprecated Please use .hasDynType === false */
hasNoDynTypes() {
return this.hasDynType === false
}
isDynOrBool() {

@@ -91,7 +123,2 @@ return this.type === 'bool' || this.kind === 'dyn'

/** @deprecated Please use .hasPlaceholderType */
hasPlaceholder() {
return this.hasPlaceholderType
}
unify(r, t2) {

@@ -163,6 +190,7 @@ const t1 = this

if (value === undefined) return
const valueType = ev.debugType(value)
if (type.matchesDebugType(valueType)) return value
throw new EvaluationError(`Field '${key}' is not of type '${type}', got '${valueType}'`, ast)
if (type.matchesValueType(value, ev)) return value
throw new EvaluationError(
`Field '${key}' is not of type '${type}', got '${ev.debugType(value)}'`,
ast
)
}

@@ -174,8 +202,6 @@

if (value === undefined) return
if (this.valueType.matchesValueType(value, ev)) return value
const type = ev.debugType(value)
if (this.valueType.matchesDebugType(type)) return value
throw new EvaluationError(
`Field '${key}' is not of type '${this.valueType}', got '${type}'`,
`Field '${key}' is not of type '${this.valueType}', got '${ev.debugType(value)}'`,
ast

@@ -214,7 +240,6 @@ )

const type = ev.debugType(value)
if (this.valueType.matchesDebugType(type)) return value
if (this.valueType.matchesValueType(value, ev)) return value
throw new EvaluationError(
`List item with index '${key}' is not of type '${this.valueType}', got '${type}'`,
`List item with index '${key}' is not of type '${this.valueType}', got '${ev.debugType(value)}'`,
ast

@@ -236,6 +261,2 @@ )

matchesDebugType(o) {
return this === o || this === dynType || !!this.valueType
}
matches(o) {

@@ -318,6 +339,2 @@ const s = this.unwrappedType

hasPlaceholder() {
return this.hasPlaceholderType
}
matchesArgs(argTypes) {

@@ -349,6 +366,2 @@ return argTypes.length === this.argTypes.length &&

hasPlaceholder() {
return this.hasPlaceholderType
}
equals(other) {

@@ -501,5 +514,4 @@ return (

#cacheBinary(cache, left, right, result) {
cache = cache.get(left) || cache.set(left, new Map()).get(left)
return (cache.set(right, result), result)
#cacheBinary(c, l, r, v) {
return ((c.get(l) || c.set(l, new Map()).get(l)).set(r, v), v)
}

@@ -542,5 +554,5 @@

const bindings = decl.hasPlaceholderType ? new Map() : null
const leftType = this.registry.matchTypeWithPlaceholders(decl.leftType, actualLeft, bindings)
const leftType = this.#matchTypeWithPlaceholders(decl.leftType, actualLeft, bindings)
if (!leftType) return
const rightType = this.registry.matchTypeWithPlaceholders(decl.rightType, actualRight, bindings)
const rightType = this.#matchTypeWithPlaceholders(decl.rightType, actualRight, bindings)
if (!rightType) return

@@ -578,3 +590,3 @@

if (receiverType && fn.receiverType) {
if (!this.registry.matchTypeWithPlaceholders(fn.receiverType, receiverType, bindings)) {
if (!this.#matchTypeWithPlaceholders(fn.receiverType, receiverType, bindings)) {
return null

@@ -585,3 +597,3 @@ }

for (let i = 0; i < argTypes.length; i++) {
if (!this.registry.matchTypeWithPlaceholders(fn.argTypes[i], argTypes[i], bindings)) {
if (!this.#matchTypeWithPlaceholders(fn.argTypes[i], argTypes[i], bindings)) {
return null

@@ -598,2 +610,47 @@ }

}
#matchTypeWithPlaceholders(declared, actual, bindings) {
if (!declared.hasPlaceholderType) return actual.matches(declared) ? actual : null
const treatAsDyn = actual.kind === 'dyn'
if (!this.#collectPlaceholderBindings(declared, actual, bindings, treatAsDyn)) return null
if (treatAsDyn) return actual
return actual.matches(declared.templated(this.registry, bindings)) ? actual : null
}
#collectPlaceholderBindings(dec, act, bind, fromDyn = false) {
if (!dec.hasPlaceholderType) return true
if (!act) return false
const asDyn = fromDyn || act.kind === 'dyn'
act = act.unwrappedType
switch (dec.kind) {
case 'param': {
const type = asDyn ? dynType : act
const existing = bind.get(dec.name)
if (!existing) return bind.set(dec.name, type) && true
return existing.kind === 'dyn' || type.kind === 'dyn' ? true : existing.matchesBoth(type)
}
case 'list': {
if (act.name === 'dyn') act = dec
if (act.kind !== 'list') return false
return this.#collectPlaceholderBindings(dec.valueType, act.valueType, bind, asDyn)
}
case 'map': {
if (act.name === 'dyn') act = dec
if (act.kind !== 'map') return false
return (
this.#collectPlaceholderBindings(dec.keyType, act.keyType, bind, asDyn) &&
this.#collectPlaceholderBindings(dec.valueType, act.valueType, bind, asDyn)
)
}
case 'optional': {
if (act.name === 'dyn') act = dec
if (act.kind !== 'optional') return false
return this.#collectPlaceholderBindings(dec.valueType, act.valueType, bind, asDyn)
}
}
return true
}
}

@@ -961,73 +1018,2 @@

matchTypeWithPlaceholders(declared, actual, bindings) {
if (!declared.hasPlaceholderType) return actual.matches(declared) ? actual : null
const treatAsDyn = actual.kind === 'dyn'
if (!this.#collectPlaceholderBindings(declared, actual, bindings, treatAsDyn)) return null
if (treatAsDyn) return actual
return actual.matches(declared.templated(this, bindings)) ? actual : null
}
#bindPlaceholder(name, candidateType, bindings) {
const existing = bindings.get(name)
if (!existing) return bindings.set(name, candidateType) && true
return existing.kind === 'dyn' || candidateType.kind === 'dyn'
? true
: existing.matchesBoth(candidateType)
}
#collectPlaceholderBindings(declared, actual, bindings, fromDyn = false) {
if (!declared.hasPlaceholderType) return true
if (!actual) return false
const treatAsDyn = fromDyn || actual.kind === 'dyn'
actual = actual.unwrappedType
switch (declared.kind) {
case 'param': {
const candidateType = treatAsDyn ? dynType : actual
return this.#bindPlaceholder(declared.name, candidateType, bindings)
}
case 'list': {
if (actual.name === 'dyn') actual = declared
if (actual.kind !== 'list') return false
return this.#collectPlaceholderBindings(
declared.valueType,
actual.valueType,
bindings,
treatAsDyn
)
}
case 'map': {
if (actual.name === 'dyn') actual = declared
if (actual.kind !== 'map') return false
return (
this.#collectPlaceholderBindings(
declared.keyType,
actual.keyType,
bindings,
treatAsDyn
) &&
this.#collectPlaceholderBindings(
declared.valueType,
actual.valueType,
bindings,
treatAsDyn
)
)
}
case 'optional': {
if (actual.name === 'dyn') actual = declared
if (actual.kind !== 'optional') return false
return this.#collectPlaceholderBindings(
declared.valueType,
actual.valueType,
bindings,
treatAsDyn
)
}
}
return true
}
#toCelFieldType(field) {

@@ -1300,3 +1286,3 @@ if (typeof field === 'string') return {type: field}

if (this.#locked) throw new Error('Cannot modify frozen registry')
returnTypeStr ??= isRelational(op) ? 'bool' : leftTypeStr
returnTypeStr ??= isRelational.has(op) ? 'bool' : leftTypeStr

@@ -1312,3 +1298,3 @@ const sig = `${leftTypeStr} ${op} ${rightTypeStr}: ${returnTypeStr}`

if (isRelational(op) && returnType.type !== 'bool') {
if (isRelational.has(op) && returnType.type !== 'bool') {
throw new Error(`Comparison operator '${op}' must return 'bool', got '${returnType.type}'`)

@@ -1372,13 +1358,3 @@ }

function isRelational(op) {
return (
op === '<' ||
op === '<=' ||
op === '>' ||
op === '>=' ||
op === '==' ||
op === '!=' ||
op === 'in'
)
}
const isRelational = new Set(['<', '<=', '>', '>=', '==', '!=', 'in'])

@@ -1402,6 +1378,2 @@ export function createRegistry(opts) {

getType(name) {
return this.getVariable(name)?.type
}
getValue(key) {

@@ -1424,20 +1396,6 @@ return (

if (v === undefined) throw new ev.Error(`Unknown variable: ${ast.args}`, ast)
if (ast.checkedType.matchesValueType(v, ev)) return v
if (ast.checkedType === dynType) {
// prettier-ignore
switch (typeof v) {
case 'string': case 'bigint': case 'number': case 'boolean': return v
case 'object':
switch (v ? v.constructor : v) {
case null: return v
case undefined: case Object: case Map: case Array: case Set: return v
default: if (ev.objectTypesByConstructor.get(v.constructor)) return v
}
}
}
const type = ast.checkedType
const valueType = ev.debugType(v)
if (type.matchesDebugType(valueType)) return v
// Convert plain objects to typed instances when a convert function is registered

@@ -1480,30 +1438,13 @@ if (type.kind === 'message' && valueType.kind === 'map') {

setIterValue(v, ev) {
if (this.iterType === dynType) {
// prettier-ignore
switch (typeof v) {
case 'string': case 'bigint': case 'number': case 'boolean':
return ((this.iterValue = v), this)
case 'object':
switch (v ? v.constructor : v) {
case null: case undefined: case Object: case Map: case Array: case Set:
return ((this.iterValue = v), this)
default:
if (ev.objectTypesByConstructor.get(v.constructor))
return ((this.iterValue = v), this)
}
}
}
if (this.iterType.matchesValueType(v, ev)) return ((this.iterValue = v), this)
const type = this.iterType
const valueType = ev.debugType(v)
if (this.iterType.matchesDebugType(valueType)) return ((this.iterValue = v), this)
// Convert plain objects to typed instances when a convert function is registered
if (this.iterType.kind === 'message' && valueType.kind === 'map') {
const c = ev.objectTypes.get(this.iterType.name)?.convert?.(v)
if (type.kind === 'message' && valueType.kind === 'map') {
const c = ev.objectTypes.get(type.name)?.convert?.(v)
if (c) return ((this.iterValue = c), this)
}
throw new ev.Error(
`Variable '${this.iterVar}' is not of type '${this.iterType}', got '${valueType}'`
)
throw new ev.Error(`Variable '${this.iterVar}' is not of type '${type}', got '${valueType}'`)
}

@@ -1532,11 +1473,2 @@

}
setConverted(name, value) {
return this.iterVar === name ? (this.iterValue = value) : this.#parent.setConverted(name, value)
}
// getVariable makes this obsolete
getType(key) {
return this.iterVar === key ? this.iterType : this.#parent.getType(key)
}
}

@@ -1543,0 +1475,0 @@

@@ -16,3 +16,2 @@ import {TypeError, EvaluationError} from './errors.js'

super(opts)
this.isEvaluating = isEvaluating
this.Error = isEvaluating ? EvaluationError : TypeError

@@ -19,0 +18,0 @@ }

{
"name": "@marcbachmann/cel-js",
"version": "7.5.2",
"version": "7.5.3",
"description": "A lightweight Common Expression Language (CEL) implementation in JavaScript with zero dependencies",

@@ -72,12 +72,12 @@ "keywords": [

"service": "drone",
"commit": "4dd97919826967c709e9b8522663ab07bc51c960",
"build": "255",
"buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/255",
"commit": "7497170f8567f37a50d9428243fcc7140c02c2a2",
"build": "256",
"buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/256",
"branch": "main",
"jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/255",
"jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/256",
"isPr": false,
"slug": "marcbachmann/cel-js",
"root": "/drone/src",
"date": "2026-03-04T01:56:33.076Z"
"date": "2026-03-10T23:23:14.229Z"
}
}
+103
-1

@@ -11,2 +11,4 @@ # @marcbachmann/cel-js [![npm version](https://img.shields.io/npm/v/@marcbachmann/cel-js.svg)](https://www.npmjs.com/package/@marcbachmann/cel-js) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

To migrate from [cel-js](https://www.npmjs.com/package/cel-js) to `@marcbachmann/cel-js`, please see [Migrating from cel-js](#migrating-from-cel-js)
## Features

@@ -693,3 +695,3 @@

```typescript
import {Environment, evaluate, ParseError} from '@marcbachmann/cel-js'
import {Environment, ParseError} from '@marcbachmann/cel-js'

@@ -704,2 +706,102 @@ // Instantiating an environment is expensive, please do that outside hot code paths

## Migrating from `cel-js`
This library is a drop-in-spirit replacement for the [`cel-js`](https://www.npmjs.com/package/cel-js) package (by ChromeGG) with full CEL spec coverage and ~10x better performance.
### Key differences
| | `cel-js` | `@marcbachmann/cel-js` |
|---|---|---|
| **Package** | `cel-js` | `@marcbachmann/cel-js` |
| **`evaluate()` args** | `(expr, vars, functions)` | `(expr, vars)` — 2 args only |
| **`parse()` result** | `{isSuccess, errors, cst}` | throws `ParseError` on failure, returns a callable |
| **Reusing parsed expr** | `evaluate(result.cst, vars)` | `compiled(vars)` |
| **Custom functions** | 3rd arg to `evaluate()` | `env.registerFunction(signature, handler)` |
| **Integer values** | plain `number` | `BigInt` (e.g. `42n`) |
| **Floating-point values** | plain `number` | plain `number` (unchanged) |
| **Undeclared variables** | always allowed | requires `unlistedVariablesAreDyn: true` |
| **Mixed-type list/map literals** | always allowed | requires `homogeneousAggregateLiterals: false` |
### `evaluate()` — no change for simple use
```js
// Before
import { evaluate } from 'cel-js'
evaluate('user.role == "admin"', { user: { role: 'admin' } })
// After
import { evaluate } from '@marcbachmann/cel-js'
evaluate('user.role == "admin"', { user: { role: 'admin' } })
```
### `parse()` — result is now a callable
`cel-js` returned `{isSuccess, errors, cst}` and required passing `cst` back to `evaluate()`. Now `parse()` throws a `ParseError` on invalid syntax and returns a compiled, directly callable function.
```js
// Before
import { evaluate, parse } from 'cel-js'
const result = parse('2 + a')
if (!result.isSuccess) throw new Error('Expression failed to parse')
const value = evaluate(result.cst, { a: 2 })
// After
import { parse, ParseError } from '@marcbachmann/cel-js'
// Throws ParseError if the expression is not valid
const compiled = parse('2 + a')
// Throws EvaluationError if the evaluation fails
const value = compiled({ a: 2n })
```
### Custom functions — use `Environment`
`cel-js` accepted a `functions` object as the third argument to `evaluate()`. This library uses an `Environment` instead, which also unlocks type safety, reuse, and better performance.
```js
// Before
import { evaluate } from 'cel-js'
evaluate('greet(name)', { name: 'Alice' }, {
greet: (name) => `Hello, ${name}!`
})
// After
import { Environment } from '@marcbachmann/cel-js'
const env = new Environment({ unlistedVariablesAreDyn: true })
.registerFunction('greet(string): string', (name) => `Hello, ${name}!`)
env.evaluate('greet(name)', { name: 'Alice' })
```
Create the `Environment` once outside of hot code paths and reuse it — parsing and environment setup are the expensive parts.
### `unlistedVariablesAreDyn` and `homogeneousAggregateLiterals`
`cel-js` treats all variables as dynamic and allows mixed-type lists and maps by default. To replicate that behavior:
```js
const env = new Environment({
unlistedVariablesAreDyn: true, // allow undeclared variables
homogeneousAggregateLiterals: false // allow mixed-type list/map literals
})
```
Without `unlistedVariablesAreDyn: true`, all variables must be declared via `env.registerVariable()` before use. The global `evaluate()` and `parse()` functions always behave as if `unlistedVariablesAreDyn: true`.
### Integer values are `BigInt`
CEL integers are returned as `BigInt` (`42n`) instead of plain JS numbers. Pass integer context values as `BigInt` too, or use `unlistedVariablesAreDyn: true` to accept plain numbers via coercion.
```js
// Before
evaluate('count + 1', { count: 5 }) // => 6
// After
evaluate('count + 1', { count: 5n }) // => 6n
```
Floating-point (`double`) values remain plain JS `number` in both libraries.
## Contributing

@@ -706,0 +808,0 @@