ecma-evaluator 1.0.0 → 2.0.1

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/README.md CHANGED
@@ -5,31 +5,505 @@
5
5
  ![Node](https://img.shields.io/badge/node-%3E=14-blue.svg?style=flat-square)
6
6
  [![npm version](https://badge.fury.io/js/ecma-evaluator.svg)](https://badge.fury.io/js/ecma-evaluator)
7
7
 
8
- A tiny and fast JavaScript expression evaluator.
8
+ A tiny, fast, and **secure** JavaScript expression evaluator for safely evaluating expressions and template strings in a sandboxed environment.
9
+
10
+ ## Features
11
+
12
+ - ✨ **Secure by design** - Sandboxed execution environment that blocks mutable operations and prevents side effects
13
+ - 🚀 **Fast & lightweight** - Minimal dependencies, uses the efficient `acorn` parser
14
+ - 📦 **Zero configuration** - Works out of the box with sensible defaults
15
+ - 🎯 **Rich feature set** - Supports most JavaScript expressions including arithmetic, logical operations, functions, and more
16
+ - 🔒 **No eval()** - Does not use `eval()` or `Function()` constructor
17
+ - 💪 **TypeScript support** - Includes TypeScript type definitions
18
+ - 📝 **Template strings** - Evaluate expressions within template strings using `{{ }}` syntax
9
19
 
10
20
  ## Installation
11
21
 
12
22
  ```bash
13
- npm install ecma-evaluator --save
23
+ npm install ecma-evaluator
14
24
  ```
15
25
 
16
- ## Usage
26
+ ## Quick Start
17
27
 
18
28
  ```js
19
- import { evaluatorExpression, evaluatorTemplate } from "ecma-evaluator";
29
+ import { evalExpression, evalTemplate } from "ecma-evaluator";
20
30
 
21
31
  // Evaluate expression
22
- const expr = "a + b * c";
23
- const context = { a: 1, b: 2, c: 3 };
24
- const result = evaluatorExpression(expr, context);
32
+ const result = evalExpression("a + b * c", { a: 1, b: 2, c: 3 });
25
33
  console.log(result); // Output: 7
26
34
 
27
35
  // Evaluate template
28
- const template = "The result of {{ a }} + {{ b }} * {{ c }} is {{ a + b * c }}.";
29
- const templateResult = evaluatorTemplate(template, context);
30
- console.log(templateResult); // Output: "The result of 1 + 2 * 3 is 7."
36
+ const text = evalTemplate("Hello {{ name }}!", { name: "World" });
37
+ console.log(text); // Output: "Hello World!"
38
+ ```
39
+
40
+ ## API Reference
41
+
42
+ ### `evalExpression(expression, context?)`
43
+
44
+ Evaluates a JavaScript expression with an optional context.
45
+
46
+ **Parameters:**
47
+
48
+ - `expression` (string): The JavaScript expression to evaluate
49
+ - `context` (object, optional): An object containing variables to use in the expression
50
+
51
+ **Returns:** The result of evaluating the expression
52
+
53
+ **Example:**
54
+
55
+ ```js
56
+ import { evalExpression } from "ecma-evaluator";
57
+
58
+ // Basic arithmetic
59
+ evalExpression("2 + 3 * 4"); // 14
60
+
61
+ // With variables
62
+ evalExpression("x + y", { x: 10, y: 20 }); // 30
63
+
64
+ // Using built-in functions
65
+ evalExpression("Math.max(a, b, c)", { a: 5, b: 15, c: 10 }); // 15
66
+
67
+ // String operations
68
+ evalExpression("greeting + ', ' + name", {
69
+ greeting: "Hello",
70
+ name: "Alice",
71
+ }); // "Hello, Alice"
72
+
73
+ // Array methods
74
+ evalExpression("[1, 2, 3].map(x => x * 2)"); // [2, 4, 6]
75
+
76
+ // Conditional expressions
77
+ evalExpression("score >= 60 ? 'Pass' : 'Fail'", { score: 75 }); // "Pass"
78
+ ```
79
+
80
+ ### `evalTemplate(template, context?, templateParserOptions?)`
81
+
82
+ Evaluates a template string by replacing `{{ expression }}` patterns with their evaluated values.
83
+
84
+ **Parameters:**
85
+
86
+ - `template` (string): The template string to evaluate
87
+ - `context` (object, optional): An object containing variables to use in the template
88
+ - `templateParserOptions` (object, optional): Options for the template parser
89
+
90
+ **Returns:** The evaluated template string
91
+
92
+ **Example:**
93
+
94
+ ```js
95
+ import { evalTemplate } from "ecma-evaluator";
96
+
97
+ // Basic variable replacement
98
+ evalTemplate("Hello, {{ name }}!", { name: "World" });
99
+ // Output: "Hello, World!"
100
+
101
+ // Multiple expressions
102
+ evalTemplate("{{ a }} + {{ b }} = {{ a + b }}", { a: 10, b: 20 });
103
+ // Output: "10 + 20 = 30"
104
+
105
+ // Complex expressions
106
+ evalTemplate("The sum is {{ [1, 2, 3].reduce((a, b) => a + b, 0) }}");
107
+ // Output: "The sum is 6"
108
+
109
+ // Template literals within expressions
110
+ evalTemplate("{{ `Hello ${name}, welcome!` }}", { name: "Alice" });
111
+ // Output: "Hello Alice, welcome!"
112
+
113
+ // Date formatting
114
+ evalTemplate("Today is {{ new Date().toLocaleDateString() }}");
115
+ // Output: "Today is 11/18/2025" (varies by locale)
116
+
117
+ // Conditional rendering
118
+ evalTemplate("Status: {{ isActive ? 'Active' : 'Inactive' }}", { isActive: true });
119
+ // Output: "Status: Active"
120
+
121
+ // Optional chaining
122
+ evalTemplate("Value: {{ obj?.prop?.value ?? 'N/A' }}", { obj: null });
123
+ // Output: "Value: N/A"
124
+ ```
125
+
126
+ ### Error Handling
127
+
128
+ When an undefined variable is referenced in a template, it's replaced with `"undefined"` instead of throwing an error:
129
+
130
+ ```js
131
+ evalTemplate("Hello {{ name }}!", {}); // "Hello undefined!"
132
+ ```
133
+
134
+ For other errors (syntax errors, type errors, etc.), an exception will be thrown:
135
+
136
+ ```js
137
+ evalTemplate("{{ 1 + }}", {}); // Throws SyntaxError
138
+ evalTemplate("{{ obj.prop }}", { obj: null }); // Throws TypeError
139
+ ```
140
+
141
+ ## Supported JavaScript Features
142
+
143
+ ### Operators
144
+
145
+ #### Arithmetic Operators
146
+
147
+ ```js
148
+ evalExpression("10 + 5"); // 15 (addition)
149
+ evalExpression("10 - 5"); // 5 (subtraction)
150
+ evalExpression("10 * 5"); // 50 (multiplication)
151
+ evalExpression("10 / 5"); // 2 (division)
152
+ evalExpression("10 % 3"); // 1 (modulo)
153
+ evalExpression("2 ** 3"); // 8 (exponentiation)
154
+ ```
155
+
156
+ #### Comparison Operators
157
+
158
+ ```js
159
+ evalExpression("5 > 3"); // true
160
+ evalExpression("5 >= 5"); // true
161
+ evalExpression("5 < 3"); // false
162
+ evalExpression("5 <= 5"); // true
163
+ evalExpression("5 == '5'"); // true (loose equality)
164
+ evalExpression("5 === '5'"); // false (strict equality)
165
+ evalExpression("5 != '5'"); // false (loose inequality)
166
+ evalExpression("5 !== '5'"); // true (strict inequality)
167
+ ```
168
+
169
+ #### Logical Operators
170
+
171
+ ```js
172
+ evalExpression("true && false"); // false (AND)
173
+ evalExpression("true || false"); // true (OR)
174
+ evalExpression("null ?? 'default'"); // "default" (nullish coalescing)
175
+ evalExpression("!true"); // false (NOT)
176
+ ```
177
+
178
+ #### Bitwise Operators
179
+
180
+ ```js
181
+ evalExpression("5 & 3"); // 1 (AND)
182
+ evalExpression("5 | 3"); // 7 (OR)
183
+ evalExpression("5 ^ 3"); // 6 (XOR)
184
+ evalExpression("~5"); // -6 (NOT)
185
+ evalExpression("5 << 1"); // 10 (left shift)
186
+ evalExpression("5 >> 1"); // 2 (right shift)
187
+ evalExpression("5 >>> 1"); // 2 (unsigned right shift)
188
+ ```
189
+
190
+ #### Unary Operators
191
+
192
+ ```js
193
+ evalExpression("-5"); // -5 (negation)
194
+ evalExpression("+5"); // 5 (unary plus)
195
+ evalExpression("typeof 5"); // "number"
196
+ evalExpression("void 0"); // undefined
197
+ ```
198
+
199
+ ### Data Types
200
+
201
+ #### Literals
202
+
203
+ ```js
204
+ evalExpression("42"); // Number
205
+ evalExpression("'hello'"); // String
206
+ evalExpression("true"); // Boolean
207
+ evalExpression("null"); // null
208
+ evalExpression("undefined"); // undefined
209
+ ```
210
+
211
+ #### Arrays
212
+
213
+ ```js
214
+ evalExpression("[1, 2, 3]"); // [1, 2, 3]
215
+ evalExpression("[1, 2, 3][1]"); // 2
216
+ evalExpression("[1, 2, 3].length"); // 3
217
+ evalExpression("[1, 2, 3].map(x => x * 2)"); // [2, 4, 6]
218
+ evalExpression("[1, 2, 3].filter(x => x > 1)"); // [2, 3]
219
+ evalExpression("[1, 2, 3].reduce((a, b) => a + b, 0)"); // 6
220
+ ```
221
+
222
+ #### Objects
223
+
224
+ ```js
225
+ evalExpression("{ a: 1, b: 2 }"); // { a: 1, b: 2 }
226
+ evalExpression("{ a: 1, b: 2 }.a"); // 1
227
+ evalExpression("{ a: 1, b: 2 }['b']"); // 2
228
+ ```
229
+
230
+ #### Template Literals
231
+
232
+ ```js
233
+ evalExpression("`Hello ${'World'}`"); // "Hello World"
234
+ evalExpression("`2 + 2 = ${2 + 2}`", {}); // "2 + 2 = 4"
235
+ evalExpression("`Hello ${name}`", { name: "Bob" }); // "Hello Bob"
236
+ ```
237
+
238
+ ### Functions
239
+
240
+ #### Arrow Functions
241
+
242
+ ```js
243
+ evalExpression("((x) => x * 2)(5)"); // 10
244
+ evalExpression("[1, 2, 3].map(x => x * 2)"); // [2, 4, 6]
245
+ evalExpression("((a, b) => a + b)(3, 4)"); // 7
246
+ ```
247
+
248
+ #### Built-in Objects and Functions
249
+
250
+ ```js
251
+ // Math
252
+ evalExpression("Math.max(1, 2, 3)"); // 3
253
+ evalExpression("Math.min(1, 2, 3)"); // 1
254
+ evalExpression("Math.round(4.7)"); // 5
255
+ evalExpression("Math.floor(4.7)"); // 4
256
+ evalExpression("Math.ceil(4.3)"); // 5
257
+ evalExpression("Math.abs(-5)"); // 5
258
+ evalExpression("Math.sqrt(16)"); // 4
259
+
260
+ // String methods
261
+ evalExpression("'hello'.toUpperCase()"); // "HELLO"
262
+ evalExpression("'HELLO'.toLowerCase()"); // "hello"
263
+ evalExpression("'hello world'.split(' ')"); // ["hello", "world"]
264
+
265
+ // Array methods (non-mutating only)
266
+ evalExpression("[1,2,3].join(', ')"); // "1, 2, 3"
267
+ evalExpression("[1,2,3].slice(1)"); // [2, 3]
268
+ evalExpression("[1,2,3].concat([4,5])"); // [1, 2, 3, 4, 5]
269
+
270
+ // JSON
271
+ evalExpression("JSON.stringify({ a: 1 })"); // '{"a":1}'
272
+ evalExpression("JSON.parse('{\"a\":1}')"); // { a: 1 }
273
+
274
+ // Date
275
+ evalExpression("new Date(0).getTime()"); // 0
276
+ evalExpression("new Date().getFullYear()"); // current year
277
+
278
+ // Object methods
279
+ evalExpression("Object.keys({ a: 1, b: 2 })"); // ["a", "b"]
280
+ evalExpression("Object.values({ a: 1, b: 2 })"); // [1, 2]
281
+
282
+ // Number methods
283
+ evalExpression("Number.parseInt('42')"); // 42
284
+ evalExpression("Number.parseFloat('3.14')"); // 3.14
285
+ evalExpression("Number.isNaN(NaN)"); // true
286
+ evalExpression("Number.isFinite(42)"); // true
287
+
288
+ // Global functions
289
+ evalExpression("isNaN(NaN)"); // true
290
+ evalExpression("isFinite(Infinity)"); // false
291
+ evalExpression("parseInt('42')"); // 42
292
+ evalExpression("parseFloat('3.14')"); // 3.14
293
+ evalExpression("encodeURIComponent('hello world')"); // "hello%20world"
294
+ evalExpression("decodeURIComponent('hello%20world')"); // "hello world"
295
+ ```
296
+
297
+ ### Advanced Features
298
+
299
+ #### Conditional (Ternary) Operator
300
+
301
+ ```js
302
+ evalExpression("5 > 3 ? 'yes' : 'no'"); // "yes"
303
+ evalExpression("age >= 18 ? 'adult' : 'minor'", { age: 20 }); // "adult"
304
+ ```
305
+
306
+ #### Optional Chaining
307
+
308
+ ```js
309
+ evalExpression("obj?.prop", { obj: null }); // undefined
310
+ evalExpression("obj?.prop?.value", { obj: {} }); // undefined
311
+ evalExpression("arr?.[0]", { arr: null }); // undefined
312
+ evalExpression("func?.()", { func: null }); // undefined
313
+ ```
314
+
315
+ #### Member Access
316
+
317
+ ```js
318
+ evalExpression("obj.prop", { obj: { prop: 42 } }); // 42
319
+ evalExpression("obj['prop']", { obj: { prop: 42 } }); // 42
320
+ evalExpression("arr[0]", { arr: [1, 2, 3] }); // 1
321
+ ```
322
+
323
+ #### Constructor Expressions
324
+
325
+ ```js
326
+ evalExpression("new Date(2024, 0, 1)"); // Date object
327
+ evalExpression("new Array(1, 2, 3)"); // [1, 2, 3]
328
+ evalExpression("new Set([1, 2, 2, 3])"); // Set {1, 2, 3}
329
+ evalExpression("new Map([['a', 1], ['b', 2]])"); // Map {"a" => 1, "b" => 2}
330
+ ```
331
+
332
+ ## Security Features
333
+
334
+ ### Sandboxed Environment
335
+
336
+ `ecma-evaluator` runs expressions in a sandboxed environment with several security features:
337
+
338
+ 1. **No access to `eval()` or `Function()` constructor** - Prevents dynamic code execution
339
+ 2. **Blocked mutable methods** - Methods that mutate objects are blocked to prevent side effects:
340
+
341
+ - Array: `push`, `pop`, `shift`, `unshift`, `splice`, `reverse`, `sort`, `fill`, `copyWithin`
342
+ - Object: `freeze`, `defineProperty`, `defineProperties`, `preventExtensions`, `setPrototypeOf`, `assign`
343
+ - Set/Map: `add`, `set`, `delete`, `clear`
344
+ - Date: All setter methods (`setDate`, `setFullYear`, etc.)
345
+ - TypedArray: `set`, `fill`, `copyWithin`, `reverse`, `sort`
346
+
347
+ 3. **No `delete` operator** - The `delete` operator is blocked as it's a mutating operation
348
+ 4. **Limited global scope** - Only safe built-in objects are available (Math, JSON, Array, Object, etc.)
349
+ 5. **No file system or network access** - Cannot access Node.js APIs or perform I/O operations
350
+ 6. **No access to process or global variables** - Cannot access `process`, `global`, `require`, etc.
351
+
352
+ ### Safe Built-in Objects
353
+
354
+ The following built-in objects are available in the sandboxed environment:
355
+
356
+ - **Numbers & Math**: `Number`, `Math`, `Infinity`, `NaN`, `isNaN`, `isFinite`, `parseInt`, `parseFloat`
357
+ - **Strings**: `String`, `encodeURI`, `encodeURIComponent`, `decodeURI`, `decodeURIComponent`
358
+ - **Data Structures**: `Array`, `Object`, `Set`, `WeakSet`, `Map`, `WeakMap`
359
+ - **Date & Time**: `Date`
360
+ - **JSON**: `JSON`
361
+ - **Types**: `Boolean`, `Symbol`, `BigInt`, `RegExp`
362
+ - **TypedArrays**: `Int8Array`, `Uint8Array`, `Int16Array`, `Uint16Array`, `Int32Array`, `Uint32Array`, `Float32Array`, `Float64Array`, `BigInt64Array`, `BigUint64Array`
363
+ - **Errors**: `Error`, `EvalError`, `RangeError`, `ReferenceError`, `SyntaxError`, `TypeError`, `URIError`
364
+ - **Promises**: `Promise`
365
+
366
+ ### Error Prevention
367
+
368
+ ```js
369
+ // ❌ These will throw errors:
370
+ evalExpression("arr.push(1)", { arr: [1, 2, 3] });
371
+ // Error: Cannot call mutable prototype method: push
372
+
373
+ evalExpression("new Function('return 1')");
374
+ // Error: Cannot use new with Function constructor
375
+
376
+ evalExpression("delete obj.prop", { obj: { prop: 1 } });
377
+ // Error: Delete operator is not allow
378
+ ```
379
+
380
+ ## Use Cases
381
+
382
+ ### Configuration & Rules Engine
383
+
384
+ ```js
385
+ // Evaluate business rules
386
+ const rule = "age >= 18 && country === 'US'";
387
+ const isEligible = evalExpression(rule, { age: 25, country: "US" }); // true
388
+
389
+ // Dynamic pricing
390
+ const priceFormula = "basePrice * (1 - discount / 100)";
391
+ const finalPrice = evalExpression(priceFormula, {
392
+ basePrice: 100,
393
+ discount: 20,
394
+ }); // 80
395
+ ```
396
+
397
+ ### Template Rendering
398
+
399
+ ```js
400
+ // Email templates
401
+ const emailTemplate = `
402
+ Hello {{ user.name }},
403
+
404
+ Your order #{{ order.id }} has been {{ order.status }}.
405
+ Total: ${{ order.total.toFixed(2) }}
406
+
407
+ Thank you for shopping with us!
408
+ `;
409
+
410
+ const email = evalTemplate(emailTemplate, {
411
+ user: { name: "John Doe" },
412
+ order: { id: 12345, status: "shipped", total: 99.99 }
413
+ });
414
+
415
+ // Dynamic content
416
+ const greeting = evalTemplate(
417
+ "Good {{ hour < 12 ? 'morning' : hour < 18 ? 'afternoon' : 'evening' }}, {{ name }}!",
418
+ { hour: new Date().getHours(), name: "Alice" }
419
+ );
31
420
  ```
32
421
 
422
+ ### Data Transformation
423
+
424
+ ```js
425
+ // Transform API responses
426
+ const transform = "data.items.filter(x => x.active).map(x => x.name)";
427
+ const result = evalExpression(transform, {
428
+ data: {
429
+ items: [
430
+ { name: "Item 1", active: true },
431
+ { name: "Item 2", active: false },
432
+ { name: "Item 3", active: true },
433
+ ],
434
+ },
435
+ }); // ["Item 1", "Item 3"]
436
+ ```
437
+
438
+ ### Form Validation
439
+
440
+ ```js
441
+ // Conditional validation
442
+ const validationRule = "email.includes('@') && password.length >= 8";
443
+ const isValid = evalExpression(validationRule, {
444
+ email: "user@example.com",
445
+ password: "secretpassword",
446
+ }); // true
447
+ ```
448
+
449
+ ## Limitations
450
+
451
+ 1. **No statements** - Only expressions are supported, not statements (no `if`, `for`, `while`, etc.)
452
+ 2. **No variable assignment** - Cannot use assignment operators (`=`, `+=`, etc.)
453
+ 3. **No mutable operations** - Mutable array/object methods are blocked
454
+ 4. **No async operations** - Promises work but cannot use `await`
455
+ 5. **No function declarations** - Only arrow functions in expressions are supported
456
+ 6. **Limited error recovery** - Syntax errors will throw immediately
457
+ 7. **No imports/requires** - Cannot import external modules
458
+
459
+ ## Advanced Usage
460
+
461
+ ### Custom Evaluator Instance
462
+
463
+ ```js
464
+ import { Evaluator } from "ecma-evaluator";
465
+
466
+ // Create a reusable evaluator with fixed context
467
+ const evaluator = new Evaluator({ x: 10, y: 20 });
468
+
469
+ // Evaluate multiple expressions with the same context
470
+ console.log(evaluator.evaluate("x + y")); // 30
471
+ console.log(evaluator.evaluate("x * y")); // 200
472
+ console.log(evaluator.evaluate("Math.max(x, y)")); // 20
473
+ ```
474
+
475
+ ### Custom Template Parser
476
+
477
+ ```js
478
+ import { evalTemplate } from "ecma-evaluator";
479
+
480
+ evalTemplate(
481
+ "Hello ${ name }!",
482
+ { name: "World" },
483
+ {
484
+ expressionStart: "${",
485
+ expressionEnd: "}",
486
+ preserveWhitespace: false,
487
+ }
488
+ );
489
+ // Output: "Hello World!"
490
+ ```
491
+
492
+ ## Performance Tips
493
+
494
+ 1. **Reuse evaluator instances** when evaluating multiple expressions with the same context
495
+ 2. **Avoid complex nested expressions** - Break them into smaller parts if possible
496
+ 3. **Cache parsed templates** if you're rendering the same template multiple times
497
+ 4. **Use simple variable access** instead of complex property chains when possible
498
+
499
+ ## TypeScript Support
500
+
501
+ The package includes TypeScript type definitions:
502
+
503
+ ## Contributing
504
+
505
+ Contributions are welcome! Please feel free to submit a Pull Request.
506
+
33
507
  ## License
34
508
 
35
509
  The [Anti 996 License](LICENSE)
@@ -0,0 +1,11 @@
1
+ import { Node } from "acorn";
2
+
3
+ export declare class Evaluator {
4
+ constructor(variables: unknown);
5
+
6
+ static evaluate<T = unknown>(expression: string, variables: unknown): T;
7
+
8
+ evaluate<T = unknown>(expression: string): T;
9
+ }
10
+
11
+ export declare function getNodeString(node: Node): string;
@@ -0,0 +1,29 @@
1
+ export type TemplateToken = {
2
+ type: "text" | "expression";
3
+ value: string;
4
+ start: number;
5
+ end: number;
6
+ // Optional: the position of the content inside expression tokens
7
+ contentStart?: number;
8
+ contentEnd?: number;
9
+ };
10
+
11
+ export interface TemplateParserOptions {
12
+ preserveWhitespace?: boolean;
13
+ expressionStart?: string;
14
+ expressionEnd?: string;
15
+ }
16
+
17
+ export class TemplateParser {
18
+ constructor(options?: TemplateParserOptions);
19
+
20
+ /**
21
+ * 解析模板字符串为 token 数组
22
+ */
23
+ parse(template: string): TemplateToken[];
24
+
25
+ /**
26
+ * 静态方法:快速解析模板
27
+ */
28
+ static parse(template: string, options?: TemplateParserOptions): TemplateToken[];
29
+ }