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 🙏

© 2026 – Pkg Stats / Ryan Hefner

solve-engine

v2.16.0

Published

An embeddable expression engine for natural-language calculations: units, currencies, percentages, dates and matrices, with the parsing and evaluation plumbing already built.

Readme

A calculator that reads like a sentence.

Type what you mean. Units, currencies, percentages, dates, matrices and plain-English phrasing all work in the same expression, and the answer appears as you type.

npm CI Node License

Documentation  •  Playground  •  Syntax reference

That's what a user sees. This package is the engine behind it: a lexer, a Pratt parser, a bytecode VM, and an extensible package system, with units, currencies, percentages, dates and matrices already built in. There's no expression parser to write and no plumbing to wire up. It evaluates natural-language-flavoured expressions out of the box: 2 + 2 * 10, 50% of 200, 3 days + 4 hours, 10 USD to GBP, 100 cm + 2 m.

Originally the engine inside Solve for Obsidian, extracted so it can be embedded in any host: an editor plugin, a CLI, a desktop app, a server. No dependency on a UI framework, a DOM, or an editor.

See ARCHITECTURE.md for how the pipeline, package system, async evaluation model, and caching layers fit together, plus a candid list of known architectural debt.

Installation

npm install solve-engine

Quick start

import { createEngine } from "solve-engine";

const engine = createEngine();
const value = engine.evaluateExpression("2 + 2 * 10");

console.log(value.toNumber()); // 22

evaluateExpression throws an EngineError (see solve-engine/errors) on a parse or evaluation failure, wrap calls with untrusted input in a try/catch.

For line-oriented input (e.g. a document made of multiple expressions, some referencing variables defined on earlier lines), use evaluateLine/parseDocument instead, see the engine subpath below.

Engine lifecycle

Call clear() when you are finished with an engine that has parsed a document. Dropping your last reference is not enough on its own: the async batcher is reachable from the module-level data query service, so a parsed engine stays retained until clear() releases it.

const engine = createEngine();
engine.parseDocument(text);
// ... read results ...
engine.clear();

Measured per engine after a forced collection:

| Lifecycle | Retained | | --- | --- | | constructed, never parsed | 8.2KB | | constructed and parsed | 128KB | | constructed, parsed, cleared | 10KB |

This matters most for hosts that create one engine per document or per tab. Over 10,000 create-and-drop cycles the uncleared path reaches roughly 1.2GB.

Reusing one engine across documents is also fine. clear() resets an engine for the next document rather than consuming it, so there is no separate teardown call to remember.

Formatting a result for display

import { formatValue } from "solve-engine/format";

const value = engine.evaluateExpression("10 USD to GBP");
console.log(formatValue(value)); // uses DEFAULT_FORMATTING_SETTINGS if no settings passed

Package structure

solve-engine exposes its API as a set of subpath exports, grouped by how stable/low-level they are:

| Subpath | Purpose | |---|---| | solve-engine | Start here, ExpressionEngine, createEngine, IEnginePackage. | | solve-engine/engine | ExpressionEngine and its supporting types (Explanation, EngineSnapshot, etc.) directly, without the package-registration wrapper. | | solve-engine/vm | The bytecode VM: Value/ValueType, opcode dispatch, allocatePluginFunctionIndex. | | solve-engine/format | Turning a Value into a display string (numbers, dates, units, vectors, ...). | | solve-engine/language | Editor-agnostic language service: token categories, completions, highlighting. | | solve-engine/packages | The built-in packages (arithmetic, datetime, time, dice, uom, currency, vector, conditionals, converters, mathphrases, ...). | | solve-engine/constants | Engine configuration types and defaults (EngineConfig, VMConfig, ...). |

The following subpaths are advanced-public, everything a third-party package author needs to extend the engine, but with a looser stability contract than the tier above (these are the pieces the built-in packages and the OSRS example themselves depend on):

| Subpath | Purpose | |---|---| | solve-engine/lexer | Tokenizer, LexerVocabulary for registering custom keywords/operators/units. | | solve-engine/parser | Pratt parser, BytecodeBuilder, OpCode. | | solve-engine/normalizer | Post-lexer token transforms (phrase fusion, implicit multiply). | | solve-engine/resolvers | Async resolvers (IAsyncResolver) for data that loads asynchronously. | | solve-engine/errors | EngineError and the error factory. | | solve-engine/utilities | Small stateless helpers (e.g. stripQuotes). | | solve-engine/uom | Units-of-measurement conversion tables and currency exchange. | | solve-engine/services | Supporting services (query client construction, etc). |

Anything not listed above (telemetry, cache, diagnostics, types, workers) is internal and not part of the package's public contract, it may change or disappear between minor versions without notice.

Authoring a package

A package (IEnginePackage) is a plain data descriptor bundling everything needed to extend the engine with a new domain: custom tokens, parselets, VM opcode handlers, and optional async resolvers. See solve-engine/api's IEnginePackage for the full field list with inline documentation and examples for each field.

Minimal shape:

import { allocatePluginFunctionIndex } from "solve-engine/vm";
import type { IEnginePackage } from "solve-engine";

const MY_FN_IDX = allocatePluginFunctionIndex();

export const MY_PACKAGE: IEnginePackage = {
  name: "MyPackage",
  // engineVersion: "^0.1.0", // optional, see below
  prefixParselets: [{ tokenType: "MY_FUNC", parselet: new MyParselet() }],
  pluginFunctions: [{ index: MY_FN_IDX, handler: (args) => /* ... */ }],
};

Register it either as one of the packages passed to the ExpressionEngine constructor, or at runtime via ExpressionEngine.registerPackage() / unregisterPackage().

Declaring engine-version compatibility

IEnginePackage.engineVersion is an optional semver range (e.g. "^0.1.0") declaring which solve-engine versions your package is built against. It's checked against the real, running engine version at registration time. Omit it and your package always registers, exactly as before this field existed, this is the default for every package that predates it. Declare it once you want protection against the reverse case: your package being loaded into a much newer (or much older) engine whose IEnginePackage contract has since changed shape.

Unlike every other compatibility signal in this codebase (see ARCHITECTURE.md §5.2's sibling-package collision warnings, which always log and proceed), a declared range the running engine does not satisfy causes registerPackage() to throw, not warn, see ARCHITECTURE.md §5.3 for the full reasoning.

Three more extension points, beyond pluginFunctions

  • solve-engine/parser's definePhrasePattern(), build a phrase-grammar parselet (roll between X and Y, average of X, Y, Z) from a declarative list of { slots, emit } alternatives instead of hand-writing parser.consume()/ parseExpression() calls. See packages/mathphrases/ for several real examples, and its own JSDoc for the one hard constraint (every alternative must start with a keyword slot) and when a hand-written parselet is the right call instead.
  • solve-engine/resolvers's createQueryResolver(), a factory for the common "one cached async fetch → one Value" shape (weather, stock prices, a game-item price API, see examples/osrs), generalizing the caching/staleness plumbing so a package only needs to write the fetch call and the response mapping.
  • IEnginePackage.asConverters, contribute a custom as <name> conversion (e.g. 50% as decimal) to the built-in converters package's grammar: { myUnit: (value) => /* ... */ }. No lexer keyword registration needed, any bare word after "as" that isn't one of the built-in names resolves against this registry at runtime.

See ARCHITECTURE.md's §5.1 for the full reasoning behind each, including a real regression (and its fix pattern) worth reading before picking a keyword for your own package: a colon-prefixed variable name (:name = expr) can never be a keyword-shaped word in this engine, so a common-noun trigger word (like "total") should be phrase-fused with its qualifying keyword rather than claimed bare.

Two runnable examples, both under examples/ (example code, not part of the published package, see files in package.json, only dist/ ships):

  • examples/basic, the smallest complete package: one custom keyword (reverse("text")) dispatched through a plugin function, nothing else. Start here. Its test, __tests__/examples/basic/BasicPackage.spec.ts, shows the full register-and-evaluate loop end to end.
  • examples/osrs, a fuller example covering everything basic leaves out: a phrase-fused multi-word item name, an async resolver backed by a real HTTP API, a custom highlight category, and completion items. Prices Old School RuneScape Grand Exchange items (e.g. ge("Abyssal whip")).

Known limitations

Cross-instance isolation is partial. Plugin functions and the opcode registry are now owned per ExpressionEngine, so two engines with different package sets no longer interfere across those. The lexer and the currency exchange rates are still module-level singletons, so full isolation between two engines in one process cannot yet be assumed. Tracked as "L1, EngineContext"; three of its five migrations have landed and the remaining two are a prerequisite for 1.0.0 proper.

Async results need a host hook. AsyncResolutionBatcher.onLineResult is the only mechanism that patches a resolved async value back into the document model, and it is not wired inside the package. A host that does not supply it gets async values that never resolve, with no error to explain why.

Development

Developed in the solve-engine repository as an npm workspace (packages/engine).

npm run build   # tsup, emits ESM + CJS + .d.ts to dist/
npm run dev     # tsup --watch
npm test        # standalone jest run, scoped to this package

License

MIT, see LICENSE.