npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@zvenigora/jse-eval

v1.10.0

Published

JavaScript expression parsing and evaluation.

Downloads

65

Readme

jse-eval

Latest NPM release License CI

Credits

Heavily based on jse-eval, expression-eval and jsep, with thanks to their awesome work.

Forked from jse-eval v1.5.1. Many thanks to @Shelly for the initial package

jse-eval was forked from expression-eval v5.0.0. Many thanks to @donmccurdy for the initial package

JavaScript expression parsing and evaluation.

IMPORTANT: As mentioned under Security below, this library does not attempt to provide a secure sandbox for evaluation. Evaluation involving user inputs (expressions or values) may lead to unsafe behavior. If your project requires a secure sandbox, consider alternatives such as vm2.

Usage

Evaluates an estree expression from jsep (as well as @babel/parser, esprima, acorn, or any other library that parses and returns a valid estree expression).

Install

Install:

npm install --save jse-eval

Import:

// ES6
import { parse, evaluate, compile, jsep } from 'jse-eval';

// CommonJS
const { parse, evaluate, compile, jsep } = require('jse-eval');

// UMD / standalone script
const { parse, evaluate, compile, jsep } = window.jseEval;

API

Parsing

import { parse } from 'jse-eval';
const ast = parse('1 + foo');

The result of the parse is an AST (abstract syntax tree), like:

{
  "type": "BinaryExpression",
  "operator": "+",
  "left": {
    "type": "Literal",
    "value": 1,
    "raw": "1"
  },
  "right": {
    "type": "Identifier",
    "name": "foo"
  }
}

Evaluation

Evaluation executes the AST using the given context (eval(ast, context). By default, the context is empty.

import { parse, evaluate } from 'jse-eval';
const ast = parse('a + b / c'); // abstract syntax tree (AST)
const value = eval(ast, {a: 2, b: 2, c: 5}); // 2.4

// alternatively:
const value = await evalAsync(ast, {a: 2, b: 2, c: 5}); // 2.4

Since the default context is empty, it prevents using built-in JS functions. To allow those functions, they can be added to the context argument passed into the eval method:

const context = {
  Date,
  Array,
  Object,
  encodeURI,
  decodeURI,
  isFinite,
  isNaN,
  JSON,
  Math,
  parseFloat,
  parseInt,
  RegExp,
  // ...myCustomPropertiesAndFunctions,
};

Compilation

import { compile } from 'jse-eval';
const fn = compile('foo.bar + 10');
fn({foo: {bar: 'baz'}}); // 'baz10'

// alternatively:
import { compileAsync } from 'jse-eval';
const fn = compileAsync('foo.bar + 10');
fn({foo: {bar: 'baz'}}); // 'baz10'

One-Line Parse + Evaluation

import { evalExpr } from 'jse-eval';
evalExpr('foo.bar + 10', {foo: {bar: 'baz'}}); // baz10

// alternatively:
import { evalExprAsync } from 'jse-eval';
evalExprAsync('foo.bar + 10', {foo: {bar: 'baz'}}); // baz10

JSEP Plugins

import { registerPlugin } from 'jse-eval';
registerPlugin(
  require('@jsep-plugin/arrow'),
  require('@jsep-plugin/assignment'),
  require('@jsep-plugin/async-await'),
  require('@jsep-plugin/new'),
  require('@jsep-plugin/object'),
  require('@jsep-plugin/regex'),
  require('@jsep-plugin/spread'),
  require('@jsep-plugin/template'),
  require('@jsep-plugin/ternary')
);

// or alternatively:
const { jsep } = require('jse-eval');
jsep.plugins.register(
  require('@jsep-plugin/arrow'),
  require('@jsep-plugin/assignment'),
  // ...
);

Extending evaluation

To modify the evaluation, use any of the modification methods:

  • addUnaryOp(operator, evaluator). Will add the operator to jsep, and the function to evaluate the operator
  • addBinaryOp(operator, precedence | evaluator, evaluator). Will add the operator to jsep at the given precedence (if provided), and the function to evaluate the operator
  • addEvaluator(nodeType, evaluator). Will add the evaluator function to the map of functions for each node type. This evaluator will be called with the ExpressionEval instance bound to it. The evaluator is responsible for handling both sync and async, as needed, but can use the this.isAsync or this.evalSyncAsync() to help.
    • to prevent unsafe code execution, redefine CallExpression and ArrowFunctionExpression to throw an error
    • If the node type is unknown, jse-eval will check for a default node type handler before throwing an error for an unknown node type. If any other behavior is desired, this can be overridden by providing a new default evaluator.
  • addConditionalEvaluator(nodeType, predicate, evaluator). Will evaluate predicate function and add the evaluator function to the map of functions for each node type.

Extensions may also be added as plugins using the registerPlugin(myPlugin1, myPlugin2...) method. The plugins are extensions of the JSEP format. If the init method is defined in the plugin, then the plugin will be added to JSEP, and/or if the initEval method is defined in the plugin, then the initEval method will be called with the JseEval class as both this and as an argument so the plugin code may extend as necessary.

Example Extensions:

import * as expr from 'jse-eval';

expr.addBinaryOp('**', 11, true, (a, b) => a ** b);
console.log(expr.evalExpr('2 ** 3 ** 2')); // 512

expr.addBinaryOp('^', 11, (a, b) => Math.pow(a, b)); // Replace XOR with Exponent
console.log(expr.evalExpr('3^2')); // 9

expr.addEvaluator('TestNodeType', function(node, context) {
  return node.test + this.context.string
});
console.log(expr.eval({ type: 'TestNodeType', test: 'testing ' }, { string: 'jse-eval' })); // 'testing jse-eval'

// override default implementation:
expr.addEvaluator('Identifier', function myIdentifier(node: Identifier, context: Context) {
  return context?.[node.name];
});
console.log(expr.eval({ type: 'Identifier', name: 'x' }, { x: 'jse-eval' })); // 'jse-eval'


const myPlugin = {
  name: 'Exponentiation',
  init(jsep) {
    // if only adding to jsep. Otherwise it's redundant with initEval
    jsep.addBinaryOp('**', 11, true);
  },
  initEval(JseEval) {
    JseEval.addBinaryOp('**', (a, b) => a ** b);
  },
};
expr.registerPlugin(myPlugin);
console.log(expr.evalExpr('2 ** 3 ** 2')); // 512

Node Types Supported:

This project will try to stay current with all JSEP's node types::

  • ArrayExpression
  • LogicalExpression/BinaryExpression
  • CallExpression potentially unsafe
  • ConditionalExpression
  • Compound Compound support will evaluate each expression and return the result of the final one
  • Identifier
  • Literal
  • MemberExpression
  • ThisExpression
  • UnaryExpression

As well as the optional plugin node types:

  • ArrowFunctionExpression potentially unsafe
  • AssignmentExpression/UpdateExpression
  • AwaitExpression
  • NewExpression
  • ObjectExpression
  • SpreadElement
  • TaggedTemplateExpression/TemplateLiteral

Options

To change the default behavior of the evaluator, use options. Options may be provided as an argument to the function call of eval or options may be added as default to JseEval.

Case-insensitive evaluation

While JavaScript is a case-sensitive language, some may find it hard to use. To provide case-insensitive evaluation, set caseSensitive to false.

import { parse, evaluate } from 'jse-eval';

const options = {caseSensitive: false};
const ast = parse('A + B / C');

// Pass options as argument
const value = eval(ast, {a: 2, b: 2, c: 5}, options); // 2.4
import { compile } from 'jse-eval';

// Add options to evaluator
const options = {caseSensitive: false};
JseEval.addOptions(options);
const fn = JseEval.compile('Foo.BAR + 10', options);
const value = fn({foo: {bar: 'baz'}}); // 'baz10'

BlockList

blockList prevents the execution of functions or the evaluation of variables, except those explictly specified. For example, blocklist may restrict the calling of the non-secure JavaScript eval function.

import { parse, evaluate } from 'jse-eval';

const options = {blockList: ['badName', 'eval']};
const ast = parse('eval("1+2")');

const value = eval(ast, {}, options); // error: Access to member "eval" from blockList disallowed 

AllowList

allowList explictly permits the execution of functions or the evaluation of variables. For example, allowlist may restrict the calling of the non-secure JavaScript eval function.

import { parse, evaluate } from 'jse-eval';

const options = {allowList: ['goodName', 'func']};
const ast = parse('eval("1+2")');

const value = eval(ast, {}, options); // error: Access to member "eval" not in allowList disallowed 

Function Bindings

To give the reference to this of the context or / and provide additional arguments, use functionBindings. The feature utilises the JavaScript Function.prototype.bind() method.

NOTE: Add "allowSyntheticDefaultImports": true to compilerOptions of tsconfig.json

import { parse, evaluate } from 'jse-eval';

const context = {
  num: 3,
  name: 'Miss Kitty',
  action: function(args, n, t) {return this.name + ' ' + args.join(' ') + ' ' + n + ' ' + t;},
  says: function() {return this.name + ' says meow';},
}

const bindings = {
  action: { thisRef: context, arguments: [['says', 'meow']] },
  says: { thisRef: context },
}

const options = { functionBindings: {...bindings} };

const ast = parse('says()');
const value = eval(ast, context, options); // Miss Kitty says meow

const ast2 = parse('action(num, "times")');
const value2 = eval(ast2, context, options); // Miss Kitty says meow 3 times

Function Bindings with Scopes

Function Bindings may be extended with scopes. Scopes faintly resemble namespaces and they allow to use the functions with the same name. CurrentScopeName and GlobalScopeName allow to remove reference to object instance.

import { parse, evaluate } from 'jse-eval';

const catObject = {
  type: 'Cat',
  name: 'Miss Kitty',
  num: 3,
  says: function() {return this.type + ' ' + this.name + ' says meow';},
  action: function(args, n, t) {return this.name + ' ' + args.join(' ') + ' ' + n + ' ' + t;},
}

const catFunctionBindings = {
  says: { thisRef: catObject },
  action: { thisRef: catObject, arguments: [['says', 'meow']]
  },
}

const dogObject = {
  type: 'Dog',
  name: 'Ralph',
  num: 5,
  says: function() {return this.type + ' ' + this.name + ' says woof';},
  action: function(args, n, t) {return this.name + ' ' + args.join(' ') + ' ' + n + ' ' + t;},
}

const dogFunctionBindings = {
  says: { thisRef: dogObject },
  action: { thisRef: dogObject, arguments: [['says', 'woof']] },
}

const context = {
  cat: catObject,
  dog: dogObject
}

const options = {
  scopes: {
    cat: { options: {functionBindings: {...catFunctionBindings}} },
    dog: { options: {functionBindings: {...dogFunctionBindings}} }
  },
  currentScopeName: 'cat'
}

const ast = parse('cat.says()');
const value = eval(ast, context, options); // Cat Miss Kitty says meow

const ast2 = parse('dog.says()');
const value2 = eval(ast2, context, options); // Dog Ralph says woof

const ast3 = parse('cat.action(cat.num, "times")');
const value3 = eval(ast3, context, options); // Miss Kitty says meow 3 times

const ast4 = parse('dog.action(dog.num, "times")');
const value4 = eval(ast4, context, options); // Ralph says woof 5 times

const ast5 = parse('says()');               // reference to 'cat' is omitted because of currentScopeName
const value5 = eval(ast, context, options); // Cat Miss Kitty says meow

Related Packages

Depending on your specific use-case, there are other related packages available, including:

Security

Although this package does avoid the use of eval(), it cannot guarantee that user-provided expressions, or user-provided inputs to evaluation, will not modify the state or behavior of your application. This library does not attempt to provide a secure sandbox for evaluation. Evaluation of arbitrary user inputs (expressions or values) may lead to unsafe behavior. If your project requires a secure sandbox, consider alternatives such as vm2.

Contributing

Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on the guidelines for contributing and then feel free to submit a PR with your contribution.

Code of Conduct

Help us keep this project open and inclusive. Please read and follow the Code of Conduct.

License

MIT License.