bonsai-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/LICENSE +21 -0
- package/README.md +478 -0
- package/dist/errors-t5i9AEaf.mjs +165 -0
- package/dist/errors-t5i9AEaf.mjs.map +1 -0
- package/dist/index.d.mts +77 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2226 -0
- package/dist/index.mjs.map +1 -0
- package/dist/stdlib/index.d.mts +23 -0
- package/dist/stdlib/index.d.mts.map +1 -0
- package/dist/stdlib/index.mjs +178 -0
- package/dist/stdlib/index.mjs.map +1 -0
- package/dist/types-Cbouo-QC.d.mts +244 -0
- package/dist/types-Cbouo-QC.d.mts.map +1 -0
- package/package.json +58 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Deterministic property-based parser and evaluator invariant tests
|
|
13
|
+
- Random fuzz coverage for malformed parser input
|
|
14
|
+
- Adversarial regression tests for deep nesting, oversized arrays, nested blocked keys, and spread misuse
|
|
15
|
+
- Public runtime export stability tests for the root package and stdlib subpath
|
|
16
|
+
- CI performance gate and packed npm artifact smoke test
|
|
17
|
+
- Explicit stability policy documenting semver scope and compatibility guarantees
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Release and CI verification now run the Vitest suite via `bun run test`
|
|
22
|
+
- Type checking now covers `src`, `tests`, `scripts`, `benchmarks`, and tool configs
|
|
23
|
+
- Publish validation now checks packed artifact contents and Node import resolution before release
|
|
24
|
+
|
|
25
|
+
## [0.1.0] - 2026-03-07
|
|
26
|
+
|
|
27
|
+
First public release.
|
|
28
|
+
|
|
29
|
+
### Core
|
|
30
|
+
|
|
31
|
+
- Hand-written lexer and recursive descent parser
|
|
32
|
+
- Compiler with constant folding and dead branch elimination
|
|
33
|
+
- LRU-cached compile-once, evaluate-many architecture
|
|
34
|
+
- Synchronous and true async evaluation (awaits async transforms/functions)
|
|
35
|
+
- Safety sandbox: blocks `__proto__`/`constructor`/`prototype`, enforces depth/timeout/array limits
|
|
36
|
+
- Plugin system for custom transforms and functions
|
|
37
|
+
|
|
38
|
+
### Syntax
|
|
39
|
+
|
|
40
|
+
- Arithmetic: `+`, `-`, `*`, `/`, `%`, `**`, unary `+`/`-`
|
|
41
|
+
- Comparison: `==`, `!=`, `<`, `>`, `<=`, `>=` (strict equality)
|
|
42
|
+
- Logical: `&&`, `||`, `!` (short-circuit)
|
|
43
|
+
- Ternary: `a ? b : c`
|
|
44
|
+
- Nullish coalescing: `a ?? b`
|
|
45
|
+
- Membership: `x in arr`, `x not in arr`
|
|
46
|
+
- Pipe operator: `x |> transform`
|
|
47
|
+
- Optional chaining: `a?.b`, `a?.[i]`
|
|
48
|
+
- Property access: dot and bracket notation
|
|
49
|
+
- Safe method calls: `.includes()`, `.slice()`, `.startsWith()`, etc.
|
|
50
|
+
- Array/object literals with spread and trailing commas
|
|
51
|
+
- Object shorthand properties: `{ name }`
|
|
52
|
+
- Template literals: `` `Hello ${name}` ``
|
|
53
|
+
- Lambda predicates: `.active`, `.age >= 18`
|
|
54
|
+
- Number formats: hex (`0xFF`), binary (`0b101`), octal (`0o77`), scientific (`1e5`), separators (`1_000`)
|
|
55
|
+
- String escapes: unicode (`\u{1F600}`), hex (`\x41`), null (`\0`)
|
|
56
|
+
|
|
57
|
+
### Standard Library
|
|
58
|
+
|
|
59
|
+
- **strings:** `upper`, `lower`, `trim`, `split`, `replace`, `replaceAll`, `startsWith`, `endsWith`, `includes`, `padStart`, `padEnd`
|
|
60
|
+
- **arrays:** `count`, `first`, `last`, `reverse`, `flatten`, `unique`, `join`, `sort`, `filter`, `map`, `find`, `some`, `every`
|
|
61
|
+
- **math:** `round`, `floor`, `ceil`, `abs`, `sum`, `avg`, `clamp`, `min()`, `max()`
|
|
62
|
+
- **types:** `isString`, `isNumber`, `isArray`, `isNull`, `toBool`, `toNumber`, `toString`
|
|
63
|
+
- **dates:** `now()`, `formatDate`, `diffDays`
|
|
64
|
+
- **all:** convenience plugin that loads the entire stdlib
|
|
65
|
+
|
|
66
|
+
### API
|
|
67
|
+
|
|
68
|
+
- `bonsai(options?)` factory with `timeout`, `maxDepth`, `maxArrayLength`, `cacheSize`, `allowedProperties`, `deniedProperties`
|
|
69
|
+
- `evaluateSync<T>(expr, context?)` and `evaluate<T>(expr, context?)` with typed generics
|
|
70
|
+
- `compile(expr)` for pre-compiled repeated evaluation
|
|
71
|
+
- `validate(expr)` with AST, expression references (`identifiers`, `transforms`, `functions`), and formatted error strings
|
|
72
|
+
- `evaluateExpression<T>(expr, context?)` standalone shorthand
|
|
73
|
+
- Method chaining: `use()`, `addTransform()`, and `addFunction()` return `this`
|
|
74
|
+
- Registry introspection: `hasTransform()`, `hasFunction()`, `listTransforms()`, `listFunctions()`
|
|
75
|
+
- `removeTransform()`, `removeFunction()` for dynamic plugin management
|
|
76
|
+
- `clearCache()` to flush compiled expression caches
|
|
77
|
+
|
|
78
|
+
### Error Handling
|
|
79
|
+
|
|
80
|
+
- `ExpressionError` — parse errors with source position and caret highlighting
|
|
81
|
+
- `BonsaiTypeError` — runtime type mismatches with `transform`, `expected`, `received`
|
|
82
|
+
- `BonsaiReferenceError` — unknown transform/function with typo suggestions
|
|
83
|
+
- `BonsaiSecurityError` — security violations with error codes (`TIMEOUT`, `BLOCKED_PROPERTY`, `PROPERTY_NOT_ALLOWED`, `PROPERTY_DENIED`, `MAX_DEPTH`, `MAX_ARRAY_LENGTH`)
|
|
84
|
+
- All evaluation errors include `location` pointing to the exact source position
|
|
85
|
+
- `formatError()` utility exported for custom error formatting
|
|
86
|
+
|
|
87
|
+
### Performance
|
|
88
|
+
|
|
89
|
+
- 11–32M ops/sec on Apple Silicon with full stdlib loaded
|
|
90
|
+
- 88x faster than Jexl in default usage, 3–6x faster pre-compiled
|
|
91
|
+
- `sideEffects: false` for proper tree-shaking
|
|
92
|
+
- Zero runtime dependencies
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Fry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# bonsai-js
|
|
2
|
+
|
|
3
|
+
Safe expression evaluation for rules, filters, templates, and user-authored logic in JavaScript and TypeScript.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add bonsai-js
|
|
11
|
+
# or
|
|
12
|
+
npm install bonsai-js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## When to use it
|
|
16
|
+
|
|
17
|
+
- Evaluate expressions from config, database records, or admin tools.
|
|
18
|
+
- Let users define filters, conditions, or formatting rules without executing arbitrary JavaScript.
|
|
19
|
+
- Build reusable compiled rules for hot paths.
|
|
20
|
+
- Add a small expression language to a product without shipping a large runtime dependency tree.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { bonsai } from 'bonsai-js'
|
|
26
|
+
import { arrays, math, strings } from 'bonsai-js/stdlib'
|
|
27
|
+
|
|
28
|
+
const expr = bonsai()
|
|
29
|
+
.use(strings)
|
|
30
|
+
.use(arrays)
|
|
31
|
+
.use(math)
|
|
32
|
+
|
|
33
|
+
expr.evaluateSync('1 + 2 * 3') // 7
|
|
34
|
+
|
|
35
|
+
expr.evaluateSync('user.age >= 18', {
|
|
36
|
+
user: { age: 25 },
|
|
37
|
+
}) // true
|
|
38
|
+
|
|
39
|
+
expr.evaluateSync('name |> trim |> upper', {
|
|
40
|
+
name: ' dan ',
|
|
41
|
+
}) // 'DAN'
|
|
42
|
+
|
|
43
|
+
expr.evaluateSync('users |> filter(.age >= 18) |> map(.name)', {
|
|
44
|
+
users: [
|
|
45
|
+
{ name: 'Alice', age: 25 },
|
|
46
|
+
{ name: 'Bob', age: 15 },
|
|
47
|
+
],
|
|
48
|
+
}) // ['Alice']
|
|
49
|
+
|
|
50
|
+
expr.evaluateSync('user?.profile?.avatar ?? "default.png"', {
|
|
51
|
+
user: null,
|
|
52
|
+
}) // 'default.png'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Choose the Right API
|
|
56
|
+
|
|
57
|
+
| Need | API |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| Repeated evaluations with caching, plugins, or safety options | `bonsai()` |
|
|
60
|
+
| One-off evaluation with default behavior | `evaluateExpression()` |
|
|
61
|
+
| Hot-path reuse of the same expression | `compile()` |
|
|
62
|
+
| Syntax checks and reference extraction before execution | `validate()` |
|
|
63
|
+
| Async transforms or async functions | `evaluate()` / compiled `.evaluate()` |
|
|
64
|
+
| Sync-only execution | `evaluateSync()` / compiled `.evaluateSync()` |
|
|
65
|
+
|
|
66
|
+
## Real-world Patterns
|
|
67
|
+
|
|
68
|
+
### Rule engine
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { bonsai } from 'bonsai-js'
|
|
72
|
+
|
|
73
|
+
const expr = bonsai({
|
|
74
|
+
timeout: 50,
|
|
75
|
+
maxDepth: 50,
|
|
76
|
+
allowedProperties: ['user', 'age', 'country', 'plan'],
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const isEligible = expr.compile('user.age >= 18 && user.country == "GB" && user.plan == "pro"')
|
|
80
|
+
|
|
81
|
+
isEligible.evaluateSync({
|
|
82
|
+
user: { age: 25, country: 'GB', plan: 'pro' },
|
|
83
|
+
}) // true
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Async enrichment
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { bonsai } from 'bonsai-js'
|
|
90
|
+
|
|
91
|
+
const expr = bonsai()
|
|
92
|
+
|
|
93
|
+
expr.addFunction('lookupTier', async (userId) => {
|
|
94
|
+
const row = await db.users.findById(String(userId))
|
|
95
|
+
return row?.tier ?? 'free'
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await expr.evaluate('lookupTier(userId) == "pro"', { userId: 'u_123' })
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Editor validation
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const result = expr.validate('user.name |> upper')
|
|
105
|
+
|
|
106
|
+
if (result.valid) {
|
|
107
|
+
result.references.identifiers // ['user']
|
|
108
|
+
result.references.transforms // ['upper']
|
|
109
|
+
} else {
|
|
110
|
+
console.error(result.errors[0]?.formatted)
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API Reference
|
|
115
|
+
|
|
116
|
+
### `bonsai(options?)`
|
|
117
|
+
|
|
118
|
+
Creates a reusable evaluator instance with its own extension registry and caches.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { bonsai } from 'bonsai-js'
|
|
122
|
+
|
|
123
|
+
const expr = bonsai(options?: BonsaiOptions)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`BonsaiOptions`:
|
|
127
|
+
|
|
128
|
+
| Option | Type | Default | Notes |
|
|
129
|
+
| --- | --- | --- | --- |
|
|
130
|
+
| `timeout` | `number` | `0` | Evaluation timeout in milliseconds. `0` disables the timeout check. |
|
|
131
|
+
| `maxDepth` | `number` | `100` | Maximum evaluation depth before throwing `BonsaiSecurityError('MAX_DEPTH', ...)`. |
|
|
132
|
+
| `maxArrayLength` | `number` | `100000` | Maximum array literal or expanded spread size. |
|
|
133
|
+
| `cacheSize` | `number` | `256` | Per-instance cache size for compiled expressions and parsed AST reuse. |
|
|
134
|
+
| `allowedProperties` | `string[]` | `undefined` | Whitelist of allowed member/method names. Does not apply to root identifiers or object-literal keys. |
|
|
135
|
+
| `deniedProperties` | `string[]` | `undefined` | Denylist of blocked member/method names. Does not apply to root identifiers or object-literal keys. |
|
|
136
|
+
|
|
137
|
+
Important notes:
|
|
138
|
+
|
|
139
|
+
- `allowedProperties` and `deniedProperties` apply to **member access** (`obj.name`) and **method calls** (`str.slice()`), not root identifiers (`name`) or object-literal keys (`{ name: value }`).
|
|
140
|
+
- If you whitelist `user.name`, you must allow both `user` and `name` as member names.
|
|
141
|
+
- Numeric array indices (e.g., `items[0]`) bypass allow/deny lists automatically.
|
|
142
|
+
- `__proto__`, `constructor`, and `prototype` are always blocked at every access level, even if you include them in an allowlist.
|
|
143
|
+
|
|
144
|
+
### `evaluateSync<T>(expression, context?)`
|
|
145
|
+
|
|
146
|
+
Runs an expression synchronously and returns its result immediately.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const result = expr.evaluateSync<number>('price * quantity', {
|
|
150
|
+
price: 9.99,
|
|
151
|
+
quantity: 3,
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use this when:
|
|
156
|
+
|
|
157
|
+
- your transforms and functions are synchronous
|
|
158
|
+
- you want the lowest overhead path
|
|
159
|
+
- the caller is already synchronous
|
|
160
|
+
|
|
161
|
+
If any registered transform, function, or method returns a `Promise`, `evaluateSync()` will throw an `BonsaiTypeError` identifying the offending call and suggesting `evaluate()` instead.
|
|
162
|
+
|
|
163
|
+
### `evaluate<T>(expression, context?)`
|
|
164
|
+
|
|
165
|
+
Runs an expression asynchronously and returns a `Promise<T>`.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const tier = await expr.evaluate<string>('userId |> fetchTier', {
|
|
169
|
+
userId: 'u_123',
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use this when:
|
|
174
|
+
|
|
175
|
+
- any transform or function is async
|
|
176
|
+
- you need to await host I/O during evaluation
|
|
177
|
+
|
|
178
|
+
### `compile(expression)`
|
|
179
|
+
|
|
180
|
+
Compiles an expression once and returns a reusable `CompiledExpression`.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const compiled = expr.compile('user.age >= minAge')
|
|
184
|
+
|
|
185
|
+
compiled.evaluateSync({ user: { age: 25 }, minAge: 18 }) // true
|
|
186
|
+
compiled.evaluateSync({ user: { age: 15 }, minAge: 18 }) // false
|
|
187
|
+
await compiled.evaluate({ user: { age: 21 }, minAge: 21 }) // true
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use `compile()` when the same expression will run many times with different contexts. This avoids repeated parse/compile work and gives you an explicit object to keep in memory.
|
|
191
|
+
|
|
192
|
+
Notes:
|
|
193
|
+
|
|
194
|
+
- compiled expressions stay tied to the instance that created them
|
|
195
|
+
- compiled evaluation uses that instance's current transforms/functions and safety options
|
|
196
|
+
- `compiled.ast` exposes the optimized AST for advanced tooling/debugging
|
|
197
|
+
|
|
198
|
+
### `validate(expression)`
|
|
199
|
+
|
|
200
|
+
Parses an expression without evaluating it.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const result = expr.validate('user.name |> upper')
|
|
204
|
+
|
|
205
|
+
if (result.valid) {
|
|
206
|
+
result.ast
|
|
207
|
+
result.references.identifiers // ['user']
|
|
208
|
+
result.references.transforms // ['upper']
|
|
209
|
+
result.references.functions // []
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
When invalid, `validate()` returns formatted errors:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const invalid = expr.validate('1 + * 2')
|
|
217
|
+
|
|
218
|
+
if (!invalid.valid) {
|
|
219
|
+
invalid.errors[0]?.message
|
|
220
|
+
invalid.errors[0]?.formatted
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`validate()` is useful for:
|
|
225
|
+
|
|
226
|
+
- form validation
|
|
227
|
+
- editor integrations
|
|
228
|
+
- autocomplete/reference extraction
|
|
229
|
+
- preflight checks before storing expressions
|
|
230
|
+
|
|
231
|
+
Important note: `validate()` checks syntax and extracts references. It does not execute the expression and it does not verify that referenced transforms/functions are currently registered.
|
|
232
|
+
|
|
233
|
+
### `evaluateExpression<T>(expression, context?)`
|
|
234
|
+
|
|
235
|
+
Convenience helper for one-off evaluation without manually creating an instance.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import { evaluateExpression } from 'bonsai-js'
|
|
239
|
+
|
|
240
|
+
evaluateExpression('1 + 2') // 3
|
|
241
|
+
evaluateExpression<number>('x * 2', { x: 21 }) // 42
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`evaluateExpression()` uses a lazily created shared default instance. It is useful for quick scripts, tests, and simple one-off calls, but it does not let you configure safety options or register custom transforms/functions.
|
|
245
|
+
|
|
246
|
+
## Instance Methods
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
interface BonsaiInstance {
|
|
250
|
+
use(plugin: BonsaiPlugin): this
|
|
251
|
+
addTransform(name: string, fn: TransformFn): this
|
|
252
|
+
addFunction(name: string, fn: FunctionFn): this
|
|
253
|
+
removeTransform(name: string): boolean
|
|
254
|
+
removeFunction(name: string): boolean
|
|
255
|
+
hasTransform(name: string): boolean
|
|
256
|
+
hasFunction(name: string): boolean
|
|
257
|
+
listTransforms(): string[]
|
|
258
|
+
listFunctions(): string[]
|
|
259
|
+
clearCache(): void
|
|
260
|
+
compile(expression: string): CompiledExpression
|
|
261
|
+
evaluate<T = unknown>(expression: string, context?: Record<string, unknown>): Promise<T>
|
|
262
|
+
evaluateSync<T = unknown>(expression: string, context?: Record<string, unknown>): T
|
|
263
|
+
validate(expression: string): ValidationResult
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Method notes:
|
|
268
|
+
|
|
269
|
+
- `use()` runs a plugin immediately and returns the same instance.
|
|
270
|
+
- `addTransform()` and `addFunction()` overwrite any existing registration with the same name.
|
|
271
|
+
- `listTransforms()` and `listFunctions()` return the currently registered names.
|
|
272
|
+
- `clearCache()` clears the internal AST cache and compiled-expression cache. It does not remove registered transforms/functions.
|
|
273
|
+
|
|
274
|
+
## Extending the Runtime
|
|
275
|
+
|
|
276
|
+
### Transforms
|
|
277
|
+
|
|
278
|
+
Transforms receive the piped value as their first argument.
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
expr.addTransform('repeat', (value, times) =>
|
|
282
|
+
String(value).repeat(Number(times)),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
expr.evaluateSync('"ha" |> repeat(3)') // 'hahaha'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`TransformFn`:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
type TransformFn = (value: unknown, ...args: unknown[]) => unknown | Promise<unknown>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Functions
|
|
295
|
+
|
|
296
|
+
Functions are called directly by name inside expressions.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
expr.addFunction('clamp', (value, min, max) =>
|
|
300
|
+
Math.min(Math.max(Number(value), Number(min)), Number(max)),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
expr.evaluateSync('clamp(score, 0, 100)', { score: 150 }) // 100
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`FunctionFn`:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
type FunctionFn = (...args: unknown[]) => unknown | Promise<unknown>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Plugins
|
|
313
|
+
|
|
314
|
+
Plugins are just functions that receive an `BonsaiInstance`.
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import type { BonsaiPlugin } from 'bonsai-js'
|
|
318
|
+
|
|
319
|
+
const currency: BonsaiPlugin = (expr) => {
|
|
320
|
+
expr.addTransform('usd', (value) => `$${Number(value).toFixed(2)}`)
|
|
321
|
+
expr.addFunction('discount', (price, pct) => Number(price) * (1 - Number(pct) / 100))
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const expr = bonsai().use(currency)
|
|
325
|
+
|
|
326
|
+
expr.evaluateSync('discount(price, 20) |> usd', { price: 100 }) // '$80.00'
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Important note: custom transforms, functions, and plugins run as normal host JavaScript. Bonsai constrains the expression language, not the code you register into it.
|
|
330
|
+
|
|
331
|
+
## Standard Library
|
|
332
|
+
|
|
333
|
+
Import only what you need:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { arrays, dates, math, strings, types } from 'bonsai-js/stdlib'
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Or load everything:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import { all } from 'bonsai-js/stdlib'
|
|
343
|
+
|
|
344
|
+
const expr = bonsai().use(all)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Modules:
|
|
348
|
+
|
|
349
|
+
| Module | Includes |
|
|
350
|
+
| --- | --- |
|
|
351
|
+
| `strings` | `upper`, `lower`, `trim`, `split`, `replace`, `replaceAll`, `startsWith`, `endsWith`, `includes`, `padStart`, `padEnd` |
|
|
352
|
+
| `arrays` | `count`, `first`, `last`, `reverse`, `flatten`, `unique`, `join`, `sort`, `filter`, `map`, `find`, `some`, `every` |
|
|
353
|
+
| `math` | transforms `round`, `floor`, `ceil`, `abs`, `sum`, `avg`, `clamp`; functions `min`, `max` |
|
|
354
|
+
| `types` | `isString`, `isNumber`, `isArray`, `isNull`, `toBool`, `toNumber`, `toString` |
|
|
355
|
+
| `dates` | function `now`; transforms `formatDate`, `diffDays` |
|
|
356
|
+
| `all` | registers every stdlib module above |
|
|
357
|
+
|
|
358
|
+
## Error Handling
|
|
359
|
+
|
|
360
|
+
Runtime exports:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import {
|
|
364
|
+
ExpressionError,
|
|
365
|
+
BonsaiReferenceError,
|
|
366
|
+
BonsaiSecurityError,
|
|
367
|
+
BonsaiTypeError,
|
|
368
|
+
formatError,
|
|
369
|
+
formatBonsaiError,
|
|
370
|
+
} from 'bonsai-js'
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Error classes:
|
|
374
|
+
|
|
375
|
+
| Error | When | Useful fields |
|
|
376
|
+
| --- | --- | --- |
|
|
377
|
+
| `ExpressionError` | parse/syntax errors | `source`, `start`, `end`, `suggestion?` |
|
|
378
|
+
| `BonsaiTypeError` | wrong runtime value type or sync/async mismatch | `transform`, `expected`, `received`, `location?`, `formatted?` |
|
|
379
|
+
| `BonsaiReferenceError` | unknown transform/function/method | `kind`, `identifier`, `suggestion?`, `location?`, `formatted?` |
|
|
380
|
+
| `BonsaiSecurityError` | blocked access or resource limit violation | `code`, `location?`, `formatted?` |
|
|
381
|
+
|
|
382
|
+
Example:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
try {
|
|
386
|
+
expr.evaluateSync('name |> unknownTransform', { name: 'Alice' })
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (error instanceof BonsaiReferenceError) {
|
|
389
|
+
console.error(error.identifier)
|
|
390
|
+
console.error(error.suggestion)
|
|
391
|
+
console.error(error.location)
|
|
392
|
+
console.error(error.formatted ?? formatBonsaiError(error))
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`formatError()` formats a source span directly, and `formatBonsaiError()` formats a caught Bonsai runtime error using its attached location:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
const parseMessage = formatError('Unexpected token "*"', {
|
|
401
|
+
source: '1 + * 2',
|
|
402
|
+
start: 4,
|
|
403
|
+
end: 5,
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
expr.evaluateSync('count |> upper', { count: 42 })
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error(formatBonsaiError(error))
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Safety Model
|
|
414
|
+
|
|
415
|
+
Bonsai is designed to safely evaluate expressions, but it is not a process sandbox.
|
|
416
|
+
|
|
417
|
+
What Bonsai does:
|
|
418
|
+
|
|
419
|
+
- blocks access to `__proto__`, `constructor`, and `prototype` at every access level, even if explicitly allowed
|
|
420
|
+
- enforces `maxDepth`, `maxArrayLength`, and optional `timeout`
|
|
421
|
+
- lets you allowlist or denylist member/method names via `allowedProperties`/`deniedProperties`
|
|
422
|
+
- prevents expressions from reaching globals or importing modules
|
|
423
|
+
- looks up root identifiers via own-property checks only (`Object.hasOwn`), so context prototype chains cannot leak
|
|
424
|
+
- creates object literals with `null` prototypes, preventing prototype pollution through expression-constructed objects
|
|
425
|
+
- validates method call receivers against a safe allowlist of types (string, number, array, plain object)
|
|
426
|
+
- automatically bypasses allow/deny lists for canonical numeric array indices (e.g., `items[0]`)
|
|
427
|
+
- rejects `Promise` values in `evaluateSync()` with actionable errors that name the offending function/transform/method and suggest using `evaluate()` instead
|
|
428
|
+
|
|
429
|
+
Important operational caveats:
|
|
430
|
+
|
|
431
|
+
- `allowedProperties` and `deniedProperties` apply to member access (`obj.name`) and method calls (`str.slice()`), not root identifiers (`name`) or object-literal keys (`{ name: value }`)
|
|
432
|
+
- `timeout` is cooperative and checked during evaluator traversal; it cannot forcibly interrupt arbitrary synchronous code inside your own custom transforms/functions
|
|
433
|
+
- async transforms/functions are bounded only at awaited boundaries
|
|
434
|
+
- custom transforms/functions/plugins are trusted host code
|
|
435
|
+
|
|
436
|
+
Recommended configuration for untrusted expressions:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
const expr = bonsai({
|
|
440
|
+
timeout: 50,
|
|
441
|
+
maxDepth: 50,
|
|
442
|
+
maxArrayLength: 10000,
|
|
443
|
+
allowedProperties: ['user', 'age', 'country', 'plan'],
|
|
444
|
+
})
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Practical guidance:
|
|
448
|
+
|
|
449
|
+
- pass the smallest context object you can
|
|
450
|
+
- prefer `allowedProperties` over `deniedProperties` for user-authored expressions
|
|
451
|
+
- keep custom extensions small and deterministic
|
|
452
|
+
- if you need hard isolation from untrusted host code, run evaluation in a worker/process boundary
|
|
453
|
+
|
|
454
|
+
## Performance Guidance
|
|
455
|
+
|
|
456
|
+
Bonsai is optimized for repeated evaluation.
|
|
457
|
+
|
|
458
|
+
- Reuse an instance instead of recreating one per request.
|
|
459
|
+
- Use `compile()` when the same expression runs many times.
|
|
460
|
+
- Use `evaluateSync()` for sync-only runtimes.
|
|
461
|
+
- Import only the stdlib modules you need.
|
|
462
|
+
- Avoid calling `clearCache()` unless you truly need to drop cached expressions.
|
|
463
|
+
|
|
464
|
+
Benchmark guidance and current numbers live in the website docs and benchmark suite. Treat raw benchmark numbers as directional, not part of the API contract.
|
|
465
|
+
|
|
466
|
+
## Stability
|
|
467
|
+
|
|
468
|
+
Bonsai follows SemVer for the documented package entrypoints `bonsai-js` and `bonsai-js/stdlib`.
|
|
469
|
+
|
|
470
|
+
- Supported runtimes are Node 18+ and current Bun releases.
|
|
471
|
+
- The packed npm artifact is smoke-tested on Node 18, 20, and 22.
|
|
472
|
+
- Internal modules under `src/*` are not public API.
|
|
473
|
+
|
|
474
|
+
See [stability policy](./docs/stability-policy.md) for the compatibility boundary and release rules.
|
|
475
|
+
|
|
476
|
+
## License
|
|
477
|
+
|
|
478
|
+
MIT
|