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

@boltmcp/jq

v0.1.11

Published

jq filter engine for TypeScript, powered by jaq and NAPI-RS

Readme

@boltmcp/jq

Native jq filter engine for TypeScript, powered by jaq and NAPI-RS.

Compile and run jq filters on JSON values at native speed, directly from TypeScript. No child processes, no WASM overhead.

Installation

npm install @boltmcp/jq

npm automatically installs the correct native binary for your platform.

Supported Platforms

| Platform | Architecture | libc | | -------- | --------------------- | ---- | | Linux | x64 | musl | | Linux | arm64 | musl | | macOS | arm64 (Apple Silicon) | - |

Usage

import { compile } from "@boltmcp/jq"

// Compile a jq filter (reusable across many inputs)
const filter = compile(".items | map(select(.active)) | length")

// Run against a JSON value
const result = filter.run({
  items: [
    { name: "a", active: true },
    { name: "b", active: false },
    { name: "c", active: true },
  ],
})

console.log(result.outputs) // [2]
console.log(result.errors) // []

Handling multiple outputs

jq filters can produce zero, one, or many outputs per input:

const explode = compile(".[]")
explode.run([10, 20, 30])
// { outputs: [10, 20, 30], errors: [] }

const nothing = compile("empty")
nothing.run(null)
// { outputs: [], errors: [] }

Error handling

Compilation errors throw immediately:

try {
  compile("invalid syntax [[[")
} catch (e) {
  // Error: parse/compile error details
}

Runtime errors are collected in the errors array (the filter does not throw):

const filter = compile(".foo")
filter.run(42)
// { outputs: [], errors: ['cannot index 42 with "foo"'] }

Filter reuse

The compile() step parses and compiles the jq filter. Reuse the CompiledFilter object to amortize this cost across many inputs:

const filter = compile(".timestamp | fromdateiso8601")
for (const event of events) {
  const { outputs } = filter.run(event)
  // ...
}

API Reference

compile(filter: string): CompiledFilter

Compiles a jq filter string. Throws on syntax or compilation errors.

CompiledFilter

.filter: string (getter)

Returns the original filter string.

.run(input: unknown): RunResult

Runs the compiled filter against a JSON-compatible value. Never throws. Returns a RunResult.

RunResult

interface RunResult {
  outputs: unknown[] // Values produced by the filter
  errors: string[] // Runtime error messages (empty on success)
}

Design Decisions

Why jaq, not jq

jaq is a Rust reimplementation of jq that provides:

  • Embeddable library (jaq-core): jq does not expose a stable C library API suitable for embedding. jaq-core is designed for library use and is memory-safe.
  • Performance: jaq is fastest on 20 out of 30 benchmarks vs jq and gojq (see jaq README).
  • Thread safety: jaq-core can be used in multi-threaded environments. jq's C implementation has global state.

Why NAPI-RS, not WASM

NAPI-RS compiles Rust to a native Node.js addon (.node file). Alternatives considered:

  • WASM (e.g., jq-wasm): Portable but slower. WASM has overhead for JS/WASM boundary crossing and limited access to native instructions. NAPI-RS produces native machine code with zero interop overhead.
  • Child process (e.g., node-jq): Spawns jq as a subprocess per invocation. High per-call overhead from process creation, JSON serialization to/from stdin/stdout, and requires jq to be installed on the system.
  • FFI/C binding: Would require wrapping jq's C library, which has no stable embedding API and is not memory-safe.

Why separate compile() + run(), not a single function

The two-step API amortizes compilation cost. In hot paths the filter is compiled once and reused. A convenience run(filter, input) function was considered but omitted to keep the API surface minimal and encourage correct usage. Wrapping the two-step API in a one-shot helper is trivial:

function run(filter: string, input: unknown) {
  return compile(filter).run(input)
}

Why JS values, not JSON strings

Input and output are JS values (unknown), not JSON strings. Alternatives considered:

  • JSON string I/O: Would require the caller to JSON.stringify input and JSON.parse output, adding boilerplate and serialization overhead. NAPI-RS efficiently bridges JS values to Rust's serde_json::Value without an intermediate string representation.
  • Both: Offering both run(input: unknown) and runRaw(json: string) was considered but deferred. The JS value API covers all use cases, and a raw string API can be added later if profiling shows benefit for large payloads.

Why runtime errors are collected, not thrown

Compilation errors throw (they indicate a broken filter). Runtime errors are collected in RunResult.errors because:

  • jq filters can produce a mix of values and errors in a single run (e.g., .[] | .foo on [{"foo":1}, 42] produces one value and one error).
  • Throwing on the first error would discard partial results.
  • The caller can check result.errors.length and decide how to handle errors based on their use case.

Why always return an array

RunResult.outputs is always an array, even for filters that produce exactly one output. Alternative considered:

  • Return single value when exactly one output: Would require the caller to check Array.isArray() or handle two return shapes. Always returning an array is consistent and predictable. The caller can do result.outputs[0] when they know the filter produces a single value.

Why musl + darwin only

The target platforms match the deployment environment:

  • Linux musl (x64 + arm64): For node:24-alpine Docker containers. Alpine uses musl libc, not glibc.
  • macOS (arm64 + x64): For local development on macOS.
  • Linux glibc: Not included because the consumer runs on Alpine. Adding glibc targets is straightforward if needed (add targets to napi config in package.json).
  • Windows: Not included because the consumer runs in Linux containers. Can be added if needed.

Why jaq-core/std/json, not jaq-all

The package depends on jaq-core, jaq-std, and jaq-json individually, not on the jaq-all convenience crate. jaq-all bundles jaq-fmts which adds YAML, TOML, CBOR, and XML format support that we don't need. Using the lower-level crates produces a smaller binary and fewer dependencies.

Development

Prerequisites

Building locally

npm install
npm run build        # release build
npm run build:debug  # debug build (faster compilation)

This produces:

  • index.js — ESM loader that selects the correct native binary
  • index.d.ts — TypeScript type declarations
  • jaq-ts.<platform>.node — native binary for your platform

Testing locally

node --input-type=module -e '
import { compile } from "./index.js"
const f = compile(".foo | map(. + 1)")
console.log(f.run({ foo: [1, 2, 3] }))
'

Publishing

Publishing is automated via GitHub Actions. Push a semver tag to trigger a release:

git tag v0.1.0
git push origin v0.1.0

CI builds native binaries for all 3 platforms, then publishes to npm using trusted publishing (OIDC — no npm token needed). Provenance is disabled because the source repository is private.

npm packages published

| Package | Contents | | ---------------------------------- | ------------------------------ | | @boltmcp/jq | JS loader + TypeScript types | | @boltmcp/jaq-ts-linux-x64-musl | Linux x64 musl native binary | | @boltmcp/jaq-ts-linux-arm64-musl | Linux arm64 musl native binary | | @boltmcp/jaq-ts-darwin-arm64 | macOS arm64 native binary |

The "packageName": "@boltmcp/jaq-ts" in the napi section of package.json controls the naming of these platform-specific packages in the generated index.js. It is independent of the parent package name (@boltmcp/jq) — without it, napi build would derive platform package names from the parent, producing incorrect names like @boltmcp/jq-darwin-arm64.

License

MIT