@marcbachmann/cel-js
Advanced tools
+6
-3
@@ -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 @@ } |
+14
-6
| 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 @@ |
+2
-2
@@ -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 | ||
| } |
+111
-179
@@ -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 @@ } |
+6
-6
| { | ||
| "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 [](https://www.npmjs.com/package/@marcbachmann/cel-js) [](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 @@ |
209034
1.32%827
14.07%33
-2.94%5048
-0.96%