Library / SDK
EricSmekens/jsep avatar
EricSmekens/jsep

jsep is a tiny expression parser you can rewrite from the inside

JavaScript Expression Parser

969 stars145 forksJavaScriptMIT

At a glance

What is it?
jsep parses JavaScript expressions and returns a tree that is almost identical to esprima's, which in turn follows SpiderMonkey's parser API. The grammar is extensible at three levels, with custom operators, identifiers and literals, and the whole parser can be modified at runtime through plugins and hooks, which is what makes it more than a small convenience library.
Who is it for?
jsep fits a project that needs to evaluate a user supplied expression, in a spreadsheet like tool, a rule engine or a form builder, and wants a parser small enough to inline in a bundle with an AST that matches what other JavaScript tooling produces.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Expressions, and the difference from operations

jsep is a simple expression parser written in JavaScript, and the README is precise about the scope. It can parse JavaScript expressions but not operations, and the difference between expressions and operations is likened to the difference between a cell in an Excel spreadsheet and a proper JavaScript program. That is a useful analogy, because a spreadsheet formula is a single value producing expression while a program is a sequence of statements with control flow. The reason jsep exists is size. The author wanted a lightweight, tiny parser to include in one of his other libraries, and while esprima and other parsers are great, they had more power than was needed and were way too large to be included in a library he wanted to keep relatively small. The result is a parser that handles the arithmetic and logic of an expression and stops there, which is a constraint that turns out to be a feature for a bundler conscious library.

An AST you can hand to other tools

jsep's output is almost identical to esprima's, which is in turn based on SpiderMonkey's parser API. That lineage is the reason the tree is worth having, because a consumer of the parse tree can often reuse code written against esprima or against the Mozilla parser conventions. The tree is a plain object structure with node types such as literal, identifier, binary and unary expressions, and because the shape is familiar, a caller can walk it, evaluate it or translate it without a translation layer. The README also mentions that jsep is a plugin host, with a plugin system and hooks, which is a second reason the library is not just a parser but a small parsing framework that happens to fit in a small number of kilobytes.

Running it in Node and in a page

In Node the package is installed with npm install jsep and then imported. There are four documented ways to get at the parser, and they exist because the package ships both ES module and CommonJS builds:

javascript
// ESM:
import jsep from 'jsep';
const parse_tree = jsep('1 + 1');

The named export form gives you a static parse method, so Jsep.parse('1 + 1') does the same thing without an instance, and the CommonJS form needs the default property because the package is type module:

javascript
// CJS:
const jsep = require('jsep').default;
const parsed = jsep('1 + 1');

In the browser there are two more shapes, an ES module script that imports the minified build from a path, and a plain script tag for the IIFE bundle that exposes a global. The package file confirms the layout, with module pointing at dist/jsep.js, main at dist/cjs/jsep.cjs.js and typings at typings/tsd.d.ts, and the type module flag set. The engines field requires Node 20.16.0 or newer.

Custom operators, identifiers and literals

The grammar is extensible at three levels and the API is symmetric, with an add and a remove for each. Operators come first:

javascript
// Add a custom ^ binary operator with precedence 10
// (Note that higher number = higher precedence)
jsep.addBinaryOp("^", 10);

// Add exponentiation operator (right-to-left)
jsep.addBinaryOp('**', 11, true); // now included by default

// Add a custom @ unary operator
jsep.addUnaryOp('@');

The third argument marks an operator as right-to-left, which is what exponentiation needs, and the precedence number follows the usual convention that a higher number binds tighter. Identifiers are extended separately, since the set of characters allowed in a name is a property of the language rather than of the grammar, and the underscore and the dollar sign are already treated that way by default. Literals work the same way, with addLiteral taking a name and a value, and by default only true, false and null are defined, so undefined, Infinity and NaN are things you add yourself.

Plugins, and the one that is built in

The plugin system is where the interesting extension points are, and the plugin list shows how much of JavaScript you can bolt on. The ternary plugin is built in by default and adds support for a ? b : c expressions. The rest are opt-in packages: arrow for arrow functions, assignment for a = 2 and a++, comment for ignoring comments, new for the new keyword, numbers for hex, octal and binary literals, object for object literals, regex for regular expression literals, spread for the spread operator, and template for template literals. Plugins are registered by calling jsep.plugins.register() with the plugin or plugins as the argument:

javascript
import jsep from 'jsep';
import ternary from '@jsep-plugin/ternary';
import object from '@jsep-plugin/object';
jsep.plugins.register(object);
jsep.plugins.register(ternary, object);

The one rule is that a plugin has a name property so that it can only be registered once, and any subsequent registration of the same plugin has no effect. Registered plugins are stored in an object keyed by their name and can be retrieved through jsep.plugins.registered.

Hooks rewrite the parser at seven points

Most plugins are built from hooks, and the hook list is the real API. There are seven types. before-all is called just before starting all expression parsing, and after-all just after parsing all. The expression level has gobble-expression, called just before attempting to parse an expression, and after-expression, just after. The token level has gobble-token and after-token. And gobble-spaces is called when whitespace is gobbled. All hooks are bound to the jsep instance, are called with a single argument and return void. The this context gives access to the internal parsing methods so a hook can reuse them, and the hook scope is typed, with an index, the expression, the current character and its code, a gobbleSpaces method and more. A minimal plugin looks like this:

javascript
const plugin = {
  name: 'the plugin',
  init(jsep) {
    jsep.addIdentifierChar('@');
    jsep.hooks.add('gobble-expression', function myPlugin(env) {
      if (this.char === '@') {
        this.index += 1;
        env.node = {
          type: 'MyCustom@Detector',
        };
      }
    });
  },
};

The init function receives the jsep instance, and the hook reads the current character from the scope, advances the index and sets the node, which in this example turns the @ character into a custom expression type.

A monorepo with automated releases

The repository shows a project that has been modernised around a plugin ecosystem. It is a pnpm workspace with a packages/ directory holding the plugins, and separate rollup configurations, with rollup.config.js for the core and plugin.rollup.config.js for the plugins, and a package-cjs.json for the CommonJS build. The tooling is extensive for a library this small, with eslint, husky, commitlint on the angular preset, semantic-release with the monorepo plugin, a benchmark package, docco for documentation, an http-server for the docs site, and puppeteer driven tests through node-qunit-puppeteer, which is how a parser gets tested in a real browser. The release configuration in the package file is worth noting, because it lists the commit paths that trigger a release, and those paths include the core source, the ternary plugin source, the types and typings, and the package and rollup files. The release branches are master plus alpha and beta prerelease branches, and the project shipped v1.6.0 on 2026-10-11 along with @jsep-plugin/ternary-v1.2.0 and @jsep-plugin/template-v1.1.0 the same day, with the last push on 2026-10-11. The licence is MIT, and the maintainers listed are Eric Smekens and Lea Verou.

Editorial conclusion

jsep fits a project that needs to evaluate a user supplied expression, in a spreadsheet like tool, a rule engine or a form builder, and wants a parser small enough to inline in a bundle with an AST that matches what other JavaScript tooling produces. It does not fit a project that needs to parse statements, because it parses expressions and not operations, and it does not fit a team that wants a maintained full JavaScript grammar, since most of the language is opt-in through plugins. Before you adopt it, decide which plugins you need and register them explicitly, because only the ternary plugin is built in by default, and note the release state, where v1.6.0 and two plugin packages were all published on 2026-10-11 and the last push was the same day.

Frequently asked questions

What is jsep?

It is a small JavaScript expression parser, roughly a cell in a spreadsheet rather than a full program, written to be tiny enough to embed in another library, and it produces a tree that is almost identical to esprima's, which is itself based on the SpiderMonkey parser API.

How do I use jsep in Node?

Install it with npm install jsep, then import it as an ES module with import jsep from 'jsep' and call jsep('1 + 1'), or use the named Jsep export and its static parse method. In CommonJS you need the default property because the package is type module.

Which JavaScript features does jsep support out of the box?

Only the ternary operator is built in. Everything else comes from a plugin, with separate packages for arrow functions, assignment and update expressions, comments, new, hex and binary numbers, object literals, regular expressions, spread and template literals.

How do I extend the jsep grammar?

With addBinaryOp and addUnaryOp and their remove counterparts for operators, addIdentifierChar and removeIdentifierChar for names, and addLiteral and removeLiteral for literals, and with plugins registered through jsep.plugins.register when you need to change parsing behaviour itself.

Is jsep still updated?

Yes. The repository is not archived, the last push was on 2026-10-11, and v1.6.0 was published the same day along with @jsep-plugin/ternary-v1.2.0 and @jsep-plugin/template-v1.1.0. The package is MIT licensed and requires Node 20.16.0 or newer.

Official sources

  1. EricSmekens/jsep on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ericsmekens-jsep.svg)](https://hysenlabs.com/projects/ericsmekens-jsep)

Was this useful?