MCPcopy Index your code
hub / github.com/danfry1/bonsai-js

github.com/danfry1/bonsai-js @v0.5.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.5.0 ↗ · + Follow
418 symbols 1,331 edges 90 files 29 documented · 7% updated 1d agov0.5.0 · 2026-06-02★ 172
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

bonsai-js

bonsai-js

npm version npm downloads CI CodeQL bundle size zero dependencies node TypeScript license OpenSSF Best Practices

A safe expression language for rules, filters, templates, and user-authored logic. Runs in any JavaScript runtime.

Bonsai gives you a constrained expression language with caching, typed errors, pluggable transforms/functions, and safety controls. It is designed for cases where eval() would be inappropriate: business rules, formula fields, admin-defined filters, template helpers, and product configuration.

Install

bun add bonsai-js
# or
npm install bonsai-js

npm · Playground · Docs

When to use it

  • Evaluate expressions from config, database records, or admin tools.
  • Let users define filters, conditions, or formatting rules without executing arbitrary JavaScript.
  • Build reusable compiled rules for hot paths.
  • Add a small expression language to a product without shipping a large runtime dependency tree.

Quick Start

import { bonsai } from 'bonsai-js'
import { arrays, math, strings } from 'bonsai-js/stdlib'

const expr = bonsai()
  .use(strings)
  .use(arrays)
  .use(math)

expr.evaluateSync('1 + 2 * 3') // 7

expr.evaluateSync('user.age >= 18', {
  user: { age: 25 },
}) // true

expr.evaluateSync('name |> trim |> upper', {
  name: '  dan  ',
}) // 'DAN'

expr.evaluateSync('users |> filter(.age >= 18) |> map(.name)', {
  users: [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 15 },
  ],
}) // ['Alice']

// JS-style method chaining works too
expr.evaluateSync('users.filter(.age >= 18).map(.name)', {
  users: [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 15 },
  ],
}) // ['Alice']

expr.evaluateSync('[1, 2, 3, 4].filter(. > 2)') // [3, 4]

expr.evaluateSync('user?.profile?.avatar ?? "default.png"', {
  user: null,
}) // 'default.png'

Choose the Right API

Need API
Repeated evaluations with caching, plugins, or safety options bonsai()
One-off evaluation with default behavior evaluateExpression()
Hot-path reuse of the same expression compile()
Syntax checks and reference extraction before execution validate()
Async transforms or async functions evaluate() / compiled .evaluate()
Sync-only execution evaluateSync() / compiled .evaluateSync()

Real-world Patterns

Rule engine

import { bonsai } from 'bonsai-js'

const expr = bonsai({
  timeout: 50,
  maxDepth: 50,
  allowedProperties: ['user', 'age', 'country', 'plan'],
})

const isEligible = expr.compile('user.age >= 18 && user.country == "GB" && user.plan == "pro"')

isEligible.evaluateSync({
  user: { age: 25, country: 'GB', plan: 'pro' },
}) // true

Async enrichment

import { bonsai } from 'bonsai-js'

const expr = bonsai()

expr.addFunction('lookupTier', async (userId) => {
  const row = await db.users.findById(String(userId))
  return row?.tier ?? 'free'
})

await expr.evaluate('lookupTier(userId) == "pro"', { userId: 'u_123' })

Context-aware functions

Register functions that read the evaluation context directly. The function receives the evaluation context as its first parameter (typed read-only), so you can keep expressions terse and let the function pull what it needs:

import { bonsai } from 'bonsai-js'

interface AppContext {
  currentUserId: string
  perms: readonly string[]
}

const app = bonsai<AppContext>()

app.addContextFunction('lookupCurrentUserTier', async (ctx) => {
  const row = await db.users.findById(ctx.currentUserId)
  return row?.tier ?? 'free'
})

app.addContextFunction('hasPermission', (ctx, action) =>
  ctx.perms.includes(String(action)))

await app.evaluate(
  'lookupCurrentUserTier() == "pro" && hasPermission("admin")',
  { currentUserId: 'u_123', perms: ['admin', 'write'] },
)

The instance is generic over the context type (bonsai<AppContext>()), giving you end-to-end type safety: ctx is typed inside the function, and the call site is type-checked against the same shape. If your context type has required fields, TypeScript also requires you to pass the context argument to evaluate, evaluateSync, and compiled-expression evaluation.

The context is passed to your function by reference, not copied or frozen. The Readonly<TCtx> parameter type signals that you should treat it as read-only: TypeScript flags reassigning its top-level fields. Bonsai does not deep-freeze it, so nested mutation and writes from untyped JavaScript reach the object you passed in. If you need isolation between evaluations, pass a fresh context object each time.

Pure functions (addFunction) and context-aware functions (addContextFunction) share a single namespace. Registering the same name with either method overwrites the prior registration, so re-registering a context-aware name with addFunction turns it back into a pure function (check isContextFunction(name) if the kind matters). Functions registered with addFunction never receive the context; reach for addContextFunction when a function needs it. Plugins typed against a minimal context requirement can still be applied to instances with a wider context shape.

Editor validation

const result = expr.validate('user.name |> upper')

if (result.valid) {
  result.references.identifiers // ['user']
  result.references.transforms  // ['upper']
} else {
  console.error(result.errors[0]?.formatted)
}

Array Methods and Lambdas

Bonsai supports two styles for working with arrays: pipe transforms and JS-style method chaining. Both use the same lambda shorthand.

Lambda shorthand

Inside array methods, . refers to the current item:

  • .property — access a property on each item (e.g., .age, .name)
  • . > value — compare each item directly (e.g., . > 2, . == "x")

Compound predicates work too: .age >= 18 && .active

A lambda is built from the accessor plus operators, member access, and methods. The shorthand is itself a function value, so it cannot be passed into another function call: map(myFn(.x)) does not mean map(item => myFn(item.x)) and will throw a BonsaiTypeError. To transform each item through a function, chain instead: items |> map(.x) |> map(myFn) (or items.map(.x).map(myFn)).

Pipe transforms (via stdlib)

import { arrays } from 'bonsai-js/stdlib'

const expr = bonsai().use(arrays)

expr.evaluateSync('users |> filter(.age >= 18) |> map(.name)', {
  users: [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 15 }],
}) // ['Alice']

expr.evaluateSync('[1, 2, 3, 4] |> filter(. > 2)') // [3, 4]

JS-style method chaining

No stdlib import required — filter, map, find, some, and every work as native array methods:

const expr = bonsai()

expr.evaluateSync('users.filter(.age >= 18).map(.name)', {
  users: [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 15 }],
}) // ['Alice']

expr.evaluateSync('[1, 2, 3, 4].filter(. > 2)') // [3, 4]
expr.evaluateSync('[1, 2, 3].map(. * 10)') // [10, 20, 30]
expr.evaluateSync('[1, 2, 3].find(. > 1)') // 2
expr.evaluateSync('[1, 2, 3].some(. > 2)') // true
expr.evaluateSync('[1, 2, 3].every(. > 0)') // true

Both styles support async evaluation via evaluate().

Built-in safe methods

These methods work via native .method() syntax without any imports. Mutating methods (reverse, sort, push, pop, splice, etc.) are blocked to prevent context mutation.

String methods:

Method Example
trim, trimStart, trimEnd " hi ".trim()"hi"
toLowerCase, toUpperCase "Hello".toLowerCase()"hello"
startsWith, endsWith "hello".startsWith("hel")true
includes, indexOf, lastIndexOf "hello".includes("ell")true
slice, substring, at "hello".slice(1, 3)"el"
replace, replaceAll "abc".replace("a", "x")"xbc"
split "a,b,c".split(",")["a", "b", "c"]
padStart, padEnd "5".padStart(3, "0")"005"
charAt, charCodeAt, repeat, concat "ab".repeat(2)"abab"

Array methods (with lambda support):

Method Example
filter [1,2,3].filter(. > 1)[2, 3]
map [1,2,3].map(. * 10)[10, 20, 30]
find, findIndex [1,2,3].find(. > 1)2
some, every [1,2,3].some(. > 2)true
flatMap [[1],[2,3]].flatMap(.)

Array methods (non-callback):

Method Example
join [1,2,3].join(", ")"1, 2, 3"
includes, indexOf, lastIndexOf [1,2,3].includes(2)true
slice, at, concat, flat [1,2,3].concat([4])[1, 2, 3, 4]
toReversed, toSorted, toSpliced, with [3,1,2].toSorted()[1, 2, 3]

Number methods: toFixed, toString

Pipe-only transforms (require stdlib import): count, first, last, reverse, flatten, unique, sort, upper, lower, trim, sum, avg, clamp, and more. See Standard Library for the full list.

API Reference

bonsai(options?)

Creates a reusable evaluator instance with its own extension registry and caches.

import { bonsai } from 'bonsai-js'

const expr = bonsai(options?: BonsaiOptions)

BonsaiOptions:

Option Type Default Notes
timeout number 0 Evaluation timeout in milliseconds. 0 disables the timeout check.
maxDepth number 100 Maximum evaluation depth before throwing BonsaiSecurityError('MAX_DEPTH', ...).
maxArrayLength number 100000 Maximum array size produced during evaluation, including array literals, expanded spread, and array-returning methods (split, map, flat, concat, ...). Exceeding it throws BonsaiSecurityError('MAX_ARRAY_LENGTH', ...).
maxStringLength number 100000 Maximum string size produced by a string-returning method (padStart, padEnd, repeat, join, concat, slice, ...). Applies to the produced length (e.g. arr.join(sep) is bounded by its full output, not just the inputs). Exceeding it throws BonsaiSecurityError('MAX_STRING_LENGTH', ...).
cacheSize number 256 Per-instance cache size for compiled expressions and parsed AST reuse. 0 disables caching.
allowedProperties string[] undefined Whitelist of allowed member/method names. Does not apply to root identifiers or object-literal keys.
deniedProperties string[] undefined Denylist of blocked member/method names. Does not apply to root identifiers or object-literal keys.

Important notes:

  • Options are validated at construction: out-of-range values (a negative cacheSize, a non-positive maxDepth, a negative size limit, a negative/non-finite timeout) throw a RangeError/TypeError immediately rather than failing silently later.
  • allowedProperties and deniedProperties apply to member access (obj.name) and method calls (str.slice()), not root identifiers (name) or object-literal keys ({ name: value }).
  • If you whitelist user.name, you must allow both user and name as member names.
  • Numeric array indices (e.g., items[0]) bypass allow/deny lists automatically.
  • __proto__, constructor, and prototype are always blocked at every access level, even if you include them in an allowlist.

evaluateSync<T>(expression, context?)

Runs an expression synchronously and returns its result immediately.

const result = expr.evaluateSync<number>('price * quantity', {
  price: 9.99,
  quantity: 3,
})

Use this when:

  • your transforms and functions are synchronous
  • you want the lowest overhead path
  • the caller is already synchronous

If any registered transform, function, or method returns a Promise, evaluateSync() will throw an BonsaiTypeError identifying the offending call and suggesting evaluate() instead.

evaluate<T>(expression, context?)

Runs an expression asynchronously and returns a Promise<T>.

const tier = await expr.evaluate<string>('userId |> fetchTier', {
  userId: 'u_123',
})

Use this when:

  • any transform or function is async
  • you need to await host I/O during evaluation

compile(expression)

Compiles an expression once and returns a reusable CompiledExpression.

const compiled = expr.compile('user.age >= minAge')

compiled.evaluateSync({ user: { age: 25 }, minAge: 18 }) // true
compiled.evaluateSync({ user: { age: 15 }, minAge: 18 }) // false
await compiled.evaluate({ user: { age: 21 }, minAge: 21 }) // true

Use compile() when the same expression

Extension points exported contracts — how you extend this code

ErrorLocation (Interface)
(no doc)
src/errors.ts
EvalEnv (Interface)
(no doc)
src/evaluator.ts
PolicySnapshot (Interface)
(no doc)
src/types.ts
Bindings (Interface)
(no doc)
src/plugins.ts
AutocompleteOptions (Interface)
(no doc)
src/autocomplete/index.ts
PerfCase (Interface)
(no doc)
scripts/perf-gate.ts
Ctx (Interface)
(no doc)
tests/context-functions-types.test.ts
Ctx (Interface)
(no doc)
tests/context-functions.test.ts

Core symbols most depended-on inside this repo

expect
called by 1330
src/parser.ts
evaluateSync
called by 508
src/types.ts
bonsai
called by 358
src/index.ts
use
called by 142
src/types.ts
parse
called by 91
src/parser.ts
complete
called by 89
src/autocomplete/index.ts
evaluate
called by 87
src/types.ts
addTransform
called by 80
src/types.ts

Shape

Function 296
Interface 55
Method 53
Class 14

Languages

TypeScript100%

Modules by API surface

src/types.ts49 symbols
src/plugins.ts30 symbols
website/playground-full.js29 symbols
website/how-it-works.js29 symbols
src/index.ts27 symbols
src/errors.ts23 symbols
src/autocomplete/index.ts17 symbols
src/parser.ts16 symbols
src/execution-context.ts16 symbols
src/autocomplete/completions.ts15 symbols
website/playground.js14 symbols
src/eval-ops.ts13 symbols

For agents

$ claude mcp add bonsai-js \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact