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

jilt

v0.1.0

Published

JSON object filtering with a small expression language

Readme

jilt - JSON object filtering

Toolkit for turning human-readable/human-writable filter expressions into executable functions which check if a JSON object matches.

Vibe coded by codex using model gpt-5.2-codex.

Quickstart

import jilt from '@walkable/jilt'

const expr1 = jilt.parse('color = "red" and weight < 3.2 and size.x > 1')

console.log(expr1.tree())
// ['and', ['eq', ['field', 'color'], ['literal', 'red']], ...]

const func1 = expr1.compile()

console.log(func1({color: 'red', weight: 3, size: {x: 2, y: 2}}))
// => true

console.log(func1({color: 'red', weight: 3, size: {x: 1, y: 2}}))
// => false

CLI

# Test an expression against JSON input
echo '{"color": "red", "weight": 3}' | jilt 'color = "red" and weight < 5'
# => true

# Parse and display the AST
jilt --tree 'color = "red" and weight < 5'
# => ['and', ['eq', ['field', 'color'], ['literal', 'red']], ['lt', ['field', 'weight'], ['literal', 5]]]

# Interactive mode - enter JSON objects, see if they match
jilt -i 'price < 100 and inStock = true'
> {"price": 50, "inStock": true}
true
> {"price": 150, "inStock": true}
false

Expression Syntax

Literals

| Type | Examples | |------|----------| | String | "hello", 'hello' | | Number | 42, 3.14, -10, 2.5e10 | | Boolean | true, false | | Null | null |

Field Access

Access object properties using dot notation or bracket notation:

name                  # top-level field
user.name             # nested field
user.address.city     # deeply nested
items[0]              # array index
items[0].name         # array index then field
user["first name"]    # bracket notation for special characters

Comparison Operators

| Operator | Aliases | Description | |----------|---------|-------------| | = | ==, eq | Equal | | != | <>, ne | Not equal | | < | lt | Less than | | <= | le | Less than or equal | | > | gt | Greater than | | >= | ge | Greater than or equal |

Logical Operators

| Operator | Description | |----------|-------------| | and | Logical AND | | or | Logical OR | | not | Logical NOT |

Parentheses control precedence:

(color = "red" or color = "blue") and weight < 5
not (status = "deleted")

Arithmetic Operators

| Operator | Description | |----------|-------------| | + | Addition | | - | Subtraction | | * | Multiplication | | / | Division | | % | Modulo |

price * quantity > 100
(width + height) / 2 >= 10
count % 2 = 0

String Functions

| Function | Description | Example | |----------|-------------|---------| | length(s) | String length | length(name) > 5 | | upper(s) | Uppercase | upper(status) = "ACTIVE" | | lower(s) | Lowercase | lower(email) = "[email protected]" | | trim(s) | Remove leading/trailing whitespace | trim(input) != "" | | contains(s, sub) | Check if string contains substring | contains(title, "urgent") | | startsWith(s, prefix) | Check if string starts with prefix | startsWith(id, "usr_") | | endsWith(s, suffix) | Check if string ends with suffix | endsWith(filename, ".json") | | substr(s, start) | Substring from start index | substr(code, 0, 2) = "US" | | substr(s, start, len) | Substring with length | substr(code, 0, 2) = "US" | | concat(s1, s2, ...) | Concatenate strings | concat(first, " ", last) | | replace(s, old, new) | Replace first occurrence | replace(path, "/old/", "/new/") | | split(s, delim) | Split string into array | length(split(tags, ",")) > 2 |

Pattern Matching

| Function | Description | Example | |----------|-------------|---------| | regex(s, pattern) | Match against regular expression | regex(email, "^[a-z]+@[a-z]+\\.[a-z]+$") | | like(s, pattern) | SQL-style wildcards: % = any chars, _ = single char | like(email, "%@gmail.com") | | glob(s, pattern) | Shell-style wildcards: * = any chars, ? = single char | glob(filename, "*.json") |

Pattern matching examples:

# SQL-style LIKE
like(name, "John%")           # starts with "John"
like(code, "A_B")             # "A" + any single char + "B"
like(email, "%@%.com")        # contains "@" and ends with ".com"

# Shell-style glob
glob(file, "*.txt")           # ends with ".txt"
glob(path, "/home/*/docs")    # any user's docs folder
glob(name, "test?.js")        # test1.js, testA.js, etc.

# Regular expressions
regex(phone, "^\\d{3}-\\d{4}$")           # 123-4567
regex(id, "^[A-Z]{2}\\d{6}$")             # US123456
regex(version, "^v\\d+\\.\\d+\\.\\d+$")   # v1.2.3

Path Functions

| Function | Description | Example | |----------|-------------|---------| | basename(path) | Get filename from path | basename("/foo/bar.txt") = "bar.txt" | | dirname(path) | Get directory from path | dirname("/foo/bar.txt") = "/foo" | | ext(path) | Get file extension (without dot) | ext("/foo/bar.txt") = "txt" |

Path function examples:

ext(filename) = "json"
basename(path) = "config.yaml"
dirname(file) = "/etc/myapp"
startsWith(dirname(path), "/home/admin")

Array Functions

| Function | Description | Example | |----------|-------------|---------| | length(arr) | Array length | length(items) > 0 | | contains(arr, val) | Check if array contains value | contains(tags, "featured") | | first(arr) | First element | first(scores) > 90 | | last(arr) | Last element | last(events).type = "complete" | | at(arr, index) | Element at index | at(items, 2).price < 10 | | sum(arr) | Sum of numeric array | sum(prices) < 1000 | | avg(arr) | Average of numeric array | avg(scores) >= 70 | | min(arr) | Minimum value | min(bids) > 100 | | max(arr) | Maximum value | max(temperatures) < 30 | | join(arr, delim) | Join array into string | join(names, ", ") | | reverse(arr) | Reverse array | first(reverse(items)) | | sort(arr) | Sort array ascending | first(sort(scores)) | | unique(arr) | Remove duplicates | length(unique(tags)) = length(tags) |

Array Predicates

Test conditions across array elements using @ to reference the current element:

| Function | Description | Example | |----------|-------------|---------| | any(arr, pred) | True if any element matches | any(items, @.price > 100) | | all(arr, pred) | True if all elements match | all(scores, @ >= 60) | | none(arr, pred) | True if no elements match | none(items, @.status = "error") | | count(arr, pred) | Count matching elements | count(items, @.inStock) > 5 | | filter(arr, pred) | Filter to matching elements | length(filter(users, @.active)) > 0 | | map(arr, expr) | Transform elements | sum(map(items, @.price * @.qty)) |

Type Checking

| Function | Description | Example | |----------|-------------|---------| | type(val) | Get type as string | type(data) = "array" | | isString(val) | Check if string | isString(name) | | isNumber(val) | Check if number | isNumber(count) | | isBool(val) | Check if boolean | isBool(enabled) | | isNull(val) | Check if null | isNull(deletedAt) | | isArray(val) | Check if array | isArray(tags) | | isObject(val) | Check if object | isObject(metadata) |

Conditional

if(condition, thenValue, elseValue)

Example:

if(premium, price * 0.9, price) < 50

Null Handling

| Operator | Description | Example | |----------|-------------|---------| | ?? | Null coalescing | (nickname ?? name) = "Admin" | | ?. | Optional chaining | user?.profile?.avatar != null |

Operator Precedence

From highest to lowest:

  1. Parentheses ()
  2. Function calls, field access ., [], ?.
  3. Unary not, -
  4. Multiplicative *, /, %
  5. Additive +, -
  6. Comparison =, !=, <, <=, >, >=
  7. Logical and
  8. Logical or
  9. Null coalescing ??

Complex Examples

Computed string comparison

Check if concatenated name fields match the full name:

concat(firstName, " ", lastName) = fullName

With case-insensitive comparison:

lower(concat(firstName, " ", lastName)) = lower(fullName)

Validating data consistency

Check that line items sum to the total:

sum(map(items, @.price * @.quantity)) = orderTotal

Verify no duplicate IDs in an array:

length(unique(map(items, @.id))) = length(items)

Complex filtering conditions

Find orders with at least one high-value item that's in stock:

any(items, @.price > 100 and @.inStock = true) and status != "cancelled"

Filter files by extension and location:

ext(path) = "log" and startsWith(dirname(path), "/var/log")

Match versioned config files:

glob(filename, "config-v*.json") and dirname(path) = "/etc/myapp"

Conditional pricing logic

Apply tiered discount based on quantity:

price * quantity * if(quantity > 100, 0.8, if(quantity > 50, 0.9, 1.0)) < budget

Working with nested data

Check that all addresses in a user's profile have a valid zip code:

all(user.addresses, regex(@.zipCode, "^\\d{5}(-\\d{4})?$"))

Find users who have made a purchase in a specific category:

any(purchases, @.category = "electronics" and @.amount > 500)

Null-safe operations

Handle potentially missing nested fields:

(user?.profile?.email ?? user?.email ?? "unknown") != "unknown"

Check optional array:

length(tags ?? []) > 0

AST Format

The parsed expression is represented as a JSON-compatible tree:

// Input: 'color = "red" and price < 100'
// AST:
['and',
  ['eq', ['field', 'color'], ['literal', 'red']],
  ['lt', ['field', 'price'], ['literal', 100]]
]

Node Types

| Node | Format | |------|--------| | Literal | ['literal', value] | | Field access | ['field', 'name'] or ['field', 'parent', 'child'] | | Comparison | ['eq' \| 'ne' \| 'lt' \| 'le' \| 'gt' \| 'ge', left, right] | | Logical | ['and' \| 'or', ...children] or ['not', child] | | Arithmetic | ['add' \| 'sub' \| 'mul' \| 'div' \| 'mod', left, right] | | Function call | ['call', 'funcName', ...args] | | Array index | ['index', array, indexExpr] | | Conditional | ['if', condition, thenExpr, elseExpr] | | Null coalesce | ['coalesce', left, right] | | Optional chain | ['optfield', base, 'fieldName'] |

JavaScript API

jilt.parse(expression: string): Expression

Parse an expression string into an Expression object.

const expr = jilt.parse('status = "active"')

Expression.tree(): Array

Get the AST representation.

expr.tree()
// => ['eq', ['field', 'status'], ['literal', 'active']]

Expression.compile(): (obj: any) => boolean

Compile to an executable function.

const match = expr.compile()
match({status: 'active'})  // => true
match({status: 'deleted'}) // => false

Expression.toString(): string

Convert back to expression string (normalized form).

jilt.parse('x=1 AND y = 2').toString()
// => 'x = 1 and y = 2'

jilt.evaluate(expression: string, obj: any): boolean

Parse, compile, and evaluate in one step.

jilt.evaluate('price < 100', {price: 50})
// => true

jilt.evaluateConstant(node: ExprNode, options?): unknown

Evaluate an AST node as a constant expression (no input object needed).

const expr = jilt.parse('2 + 3 * 4')
jilt.evaluateConstant(expr.tree())
// => 14

Error Handling

Parse errors include position information:

try {
  jilt.parse('name = ')
} catch (e) {
  console.log(e.message)
  // => "Unexpected end of expression at position 7"
  console.log(e.position)
  // => 7
}