Skip to Content
  • Website
Codsen
  • Home
  • Open Source
  • Articles
  • About

prevOpen Source→ast-monkeynext

ast-monkey10.0.0

Traverse and edit AST

Downloads per monthChangelogMIT Licenselibera manifesto
  • the top
  • Installation
  • Quick Take
  • Examples
  • TRAVERSAL CO…
  • THE CHALLENG…
  • IDEA
  • SUPPORTED TRE…
  • API — FIND()
  • API — GET()
  • API — SET()
  • API — DROP()
  • API — DEL()
  • API — ARRAYFIRSTONL…
  • API — TRAVERSE()
  • API — TRAVERSEWITH…
  • CHOOSE AN ENT…
  • MIGRATE TRAVE…
  • API — TYPES
  • Changelog

No 3rd party dependencies. All dependencies and devDependencies, checked recursively, are Codsen packages.

Permalink to InstallationInstallation

Permalink to Quick TakeQuick Take

Permalink to ExamplesExamples

  • Delete matches from arrays only
  • Delete every object key of a given name
  • Delete nodes by value
  • Drop a node by its traversal index
  • Find nodes matching both a key and a value
  • Find nodes by value alone
  • Find explicit undefined values
  • Search array elements only
  • Search object properties only
  • Get a node by its traversal index
  • Keep only the first element of every array
  • Quick Take
  • Look ahead by zero nodes by default
  • Inspect a node's metadata and its lookahead
  • Look ahead two nodes at a time
  • Signal a stop from the traversal callback
  • Set a node's value by its traversal index
  • Set a value to undefined
  • Set a node using key instead of val
  • Collect the path of every visited node
  • Quick Take
  • Compatible With object-path
  • Delete nodes with the collision-free token
  • Delete during traversal with the collision-free token
  • Traverse without mutating the input
  • Inspect each node's parent metadata
  • Traverse a tree using traverse
  • Stop
  • Transform values while traversing

Traversal consolidation

ast-monkey@10.0.0 includes both traversal APIs. Use ast-monkey/traverse for transformation and ast-monkey/lookahead for observation with lookahead. Both APIs are also exported from the package root.

Version 10 requires a migration when TypeScript code mixes canonical and standalone transformer declarations. Move traverse, DELETE, and callback types together when upgrading.

The standalone packages are retired. Their transformer and lookahead observer documentation, examples, changelogs, and published files remain available.

The challenge

Operations on AST’s — Abstract Syntax Trees — or anything deeply nested are difficult.

The main challenge is going “up the branch” — querying the parent and sibling nodes.

Second challenge, AST’s get VERY BIG very quickly. A single tag, <td>a</td>, 10 characters produced 398 characters of AST above. Enormous inputs are very hard to reason about, especially to troubleshoot, printed trees don’t fit into screen.

The “Going up” is often solved by putting circular references in the parsed tree, like "parent": "[Circular ~.0]",. The first drawback of using circular references is that it’s not standard JSON, you can’t even JSON.stringify (specialised stringification packagesopens in a new tab do exist) — everything from the algorithm up to the unit test runner are affected. The second drawback of circular references is that while they make it easier to query things, they also make it harder to amend things — you have to amend “circular extras” as well (or hope renderer will be OK, but that’s only for small operations).

This program doesn’t rely on circular references. It uses indexing of “breadcrumb” paths. For example, you traverse and find that node you want is index 58, whole path being [2, 14, 16, 58]. You save the path down. After the traversal is done, you fetch the monkey to delete the index 58. You can also use a for loop on breadcrumb index array, [2, 14, 16, 58] and fetch and check parent 16 and grandparent 14. Lots of possibilities. Function find() searches using key or value or both, and function get() searches using a known index. That’s the strategy.

Idea

Conceptually, we use two systems to mark paths in AST:

  1. Our unique, number-based indexing system — each encountered node is numbered, for example, 58 (along with “breadcrumb” path, an array of integers, for example, [2, 14, 16, 58]). If you know the number you can get monkey to fetch you the node at that number or make amends on it.
  2. object-path notation, as in foo.1.bar (instead of foo[1].bar). The dot marking system is also powerful, it is used in many of our programs, although it has some shortcomings (no dots in key namesopens in a new tab, for example).

find() reports numeric breadcrumb indexes. traverse() reports the legacy dot path and exact pathSegments; it does not report numeric traversal indexes.

Supported tree values

The high-level helpers and traverse() accept ordinary arrays, ordinary object-literal objects, strings, numbers, Booleans, null, and undefined. An explicit null or undefined root is a value; omitting the input argument is an error.

Object properties must be own, enumerable, string-keyed data properties. Array holes are preserved but not visited. An existing undefined element is preserved and visited; the high-level helpers also assign it a numeric traversal index. Cycles, repeated object references, accessors, symbol or non-enumerable keys, extra array properties, custom prototypes, class instances, functions, bigint, and symbol values are rejected with a package-owned error. Accessor getters are not invoked during validation.

Every successful high-level helper and traverse() works on a clone. It does not mutate the input, including when get() or find() returns a nested object or array. The separate traverseWithLookahead() observer retains its legacy data contract; callback values can reference the original input.

Primitive, null, and undefined roots are accepted: find() returns [], get() returns null, and transform helpers return the cloned or identical scalar value. Omitting the input argument is an error. Invalid own options, selectors, indexes, trees, and replacements throw errors prefixed with the package name, function name, and a THROW_ID_XX; inherited option fields are ignored. drop() and del() splice selected array elements, while untouched sparse holes remain holes.

API — find()

find() searches supported arrays and objects by key, current value, or entry and returns a Finding for every match.

The function find() is imported like this:

It takes two input arguments:

Input argumentTypeObligatoryDescription
input
Type: JsonValue
Obligatory: yes
inputJsonValueyesSupported tree; explicit undefined is valid, but the argument must be present.
options
Type: FindOpts
Obligatory: yes
optionsFindOptsyesSelector and optional parent-container filter.

The options object has the following shape:

KeyTypeObligatoryDescription
criteria
Type: FindCriteria
Obligatory: alternative to key and val
criteriaFindCriteriaalternative to key and valAn explicit key, value, or key-and-value selector. It cannot be combined with the legacy selectors.
key
Type: String
Obligatory: alternative to criteria
keyStringalternative to criteriaLegacy object-key or array-element selector.
val
Type: Whatever, including undefined
Obligatory: alternative to criteria
valWhatever, including undefinedalternative to criteriaLegacy value selector; explicit undefined has compatibility behavior described below.
only
Type: Only
Obligatory: no (defaults to any)
onlyOnlyno (defaults to any)Restrict matches by their parent container: arrays, objects, or either.

criteria.kind: "key" compares an object key or array element; "value" compares an object value or array element; and "entry" compares (key, value), where value is undefined for arrays. Legacy { key } compares object keys and array elements. Legacy non-undefined { val } compares object values. An own { val: undefined } selects explicit undefined current values. For compatibility, { key, val: undefined } remains key-only. Do not combine criteria with key or val.

Prefer criteria when null or undefined is data because its intent is explicit:

find([undefined, null], {
  criteria: { kind: "value", value: undefined },
});

The typed only aliases are listed by the exported Only type. Runtime also accepts the legacy empty string as "any" and normalizes surrounding whitespace and letter case. Matching is based on the containing parent, not the matched value’s type, and filtering does not renumber the traversal.

Output

The output will be an array, comprising of zero or more plain objects in the following format:

A finding object’s keyTypeDescription
index
Type: Positive integer
indexPositive integerThe global pre-order index of the finding. Numbering starts at 1, skips array holes, and the index is also the last number in path.
key
Type: JsonValue
keyJsonValueAn object property’s string key, or the array element itself.
val
Type: JsonValue or undefined
valJsonValue or undefinedAn object property’s value. Array findings have an own val property whose value is undefined.
path
Type: Number array
pathNumber arrayThe traversal indexes of all ancestors followed by the finding’s own index.

JSON.stringify() omits an undefined val, even though the finding still has that own property.

A use example

Find out, what is the path to the key that equals ‘b’.

import {
  find,
  get,
  set,
  drop,
  del,
  arrayFirstOnly,
  traverse,
} from "ast-monkey";
const input = ["a", [["b"], "c"]];
const key = "b";
const result = find(input, { key: key });
console.log(result);
// => [
//      {
//        index: 4,
//        key: 'b',
//        val: undefined,
//        path: [2, 3, 4]
//      }
//    ]

Once you know that the path is [2, 3, 4], call get() with 3 or 2 to inspect the finding’s parent or grandparent. The last number in each finding’s path is that finding’s own index.

This makes find() versatile: walk the numbers in a finding’s path backwards and pass each one to get() to inspect its ancestors.

API — get()

Use method get() to query AST trees by branch’s index (a numeric id). You would get that index from a previously performed find() or you can pick a number manually.

Call get() with a finding’s index or with an earlier number in its path. Depending on your needs, pass that index to set() or drop() afterward.

The function get() is imported like this:

It’s a function which takes two input arguments:

Input argumentTypeObligatoryDescription
input
Type: JsonValue
Obligatory: yes
inputJsonValueyesSupported tree; explicit undefined is valid, but the argument must be present.
opts
Type: GetOpts
Obligatory: yes
optsGetOptsyesTraversal index and optional parent-container filter.

The Obligatory Options Object has the following shape:

KeyTypeObligatoryDescription
index
Type: TraversalIndex
Obligatory: yes
indexTraversalIndexyesA non-negative safe integer or its unsigned decimal string spelling.
only
Type: Only
Obligatory: no (defaults to any)
onlyOnlyno (defaults to any)Require the indexed node’s parent to be an array, an object, or either.

Index strings must contain decimal digits only and convert to a non-negative safe integer. Index 0 is valid but never identifies a visited node: get() returns null, while set() and drop() return a cloned no-op result.

Output

For an object property, get() returns a one-property object. For an array element, it returns the element itself. That result can therefore be any JsonValue, including explicit undefined. It returns null when no index passes the optional parent filter. An own __proto__ property is returned as ordinary data without changing the result’s prototype.

A use example

If you know that you want an index number two, you can query it using get():

import {
  find,
  get,
  set,
  drop,
  del,
  arrayFirstOnly,
  traverse,
} from "ast-monkey";
const input = {
  a: {
    b: "c",
  },
};
const index = 2;
const result = get(input, { index: index });
console.log("result = " + JSON.stringify(result, null, 4));
// => {
//      b: 'c'
//    }

In practice, you would query a list of indexes programmatically using a for loop.

API — set()

Use set() to overwrite a piece of an AST when you know its index.

The function set() is imported like this:

It’s a function which takes two input arguments:

Input argumentTypeObligatoryDescription
input
Type: JsonValue
Obligatory: yes
inputJsonValueyesSupported tree; explicit undefined is valid, but the argument must be present.
options
Type: SetOpts
Obligatory: yes
optionsSetOptsyesTraversal index and replacement.

The Obligatory Options Object has the following shape:

KeyTypeObligatoryDescription
index
Type: TraversalIndex
Obligatory: yes
indexTraversalIndexyesThe non-negative safe traversal index to replace.
val
Type: JsonValue
Obligatory: yes unless key is present
valJsonValueyes unless key is presentReplacement value. To write explicit undefined, provide own val: undefined and omit key.
key
Type: String
Obligatory: yes unless val is present
keyStringyes unless val is presentLegacy string replacement, used when val is absent or undefined.

Output

Function returns an amended cloned input.

A use example

Let’s say you identified the index of a piece of AST you want to write over:

import {
  find,
  get,
  set,
  drop,
  del,
  arrayFirstOnly,
  traverse,
} from "ast-monkey";
const input = {
  a: { b: [{ c: { d: "e" } }] },
  f: { g: ["h"] },
};
const index = "7";
const val = "zzz";
const result = set(input, { index: index, val: val });
console.log("result = " + JSON.stringify(result, null, 4));
// => {
//      a: {b: [{c: {d: 'e'}}]},
//      f: {g: 'zzz'}
//    }

API — drop()

Use drop() to delete a piece of an AST with a known index.

The function drop() is imported like this:

It takes two input arguments:

Input argumentTypeObligatoryDescription
input
Type: JsonValue
Obligatory: yes
inputJsonValueyesSupported tree; explicit undefined is valid, but the argument must be present.
options
Type: DropOpts
Obligatory: yes
optionsDropOptsyesTraversal index to delete.

The Obligatory Options Object has the following shape:

KeyTypeObligatoryDescription
index
Type: TraversalIndex
Obligatory: yes
indexTraversalIndexyesThe non-negative safe traversal index to delete.

Output

Function returns an amended cloned input.

A use example

Let’s say you want to delete the piece of AST with an index number 8. That’s 'h':

import {
  find,
  get,
  set,
  drop,
  del,
  arrayFirstOnly,
  traverse,
} from "ast-monkey";
const input = {
  a: { b: [{ c: { d: "e" } }] },
  f: { g: ["h"] },
};
const index = "8"; // can be integer as well
const result = drop(input, { index: index });
console.log("result = " + JSON.stringify(result, null, 4));
// => {
//      a: {b: [{c: {d: 'e'}}]},
//      f: {g: []}
//    }

API — del()

Use del() to delete all chosen key/value pairs from all objects found within an AST, or all chosen elements from all arrays.

The function del() is imported like this:

It takes two input arguments:

Input argumentTypeObligatoryDescription
input
Type: JsonValue
Obligatory: yes
inputJsonValueyesSupported tree; explicit undefined is valid, but the argument must be present.
opts
Type: DelOpts
Obligatory: yes
optsDelOptsyesSelector and optional parent-container filter.

The Obligatory Options Object has the following shape:

KeyTypeObligatoryDescription
criteria
Type: FindCriteria
Obligatory: alternative to key and val
criteriaFindCriteriaalternative to key and valAn explicit key, value, or key-and-value selector.
key
Type: String
Obligatory: alternative to criteria
keyStringalternative to criteriaLegacy key selector.
val
Type: Whatever, including undefined
Obligatory: alternative to criteria
valWhatever, including undefinedalternative to criteriaLegacy value selector; explicit undefined has compatibility behavior described below.
only
Type: Only
Obligatory: no (defaults to any)
onlyOnlyno (defaults to any)Restrict deletion by the matched node’s parent container.

Explicit criteria uses the same key, current-value, and entry semantics as find(). Legacy { key } compares object keys and array elements, while a non-undefined { val } compares object values. An own { val: undefined } selects explicit undefined current values. For compatibility, { key, val: undefined } remains key-only. Use criteria: { kind: "value", value } to select the current value unambiguously across objects and arrays, including undefined and NaN.

Output

Function returns an amended cloned input.

A use example

Let’s say you want to delete all key/value pairs from objects that have a key equal to ‘c’. Value does not matter.

import {
  find,
  get,
  set,
  drop,
  del,
  arrayFirstOnly,
  traverse,
} from "ast-monkey";
const input = {
  a: { b: [{ c: { d: "e" } }] },
  c: { d: ["h"] },
};
const key = "c";
const result = del(input, { key: key });
console.log("result = " + JSON.stringify(result, null, 4));
// => {
//      a: {b: [{}]}
//    }

API — arrayFirstOnly()

arrayFirstOnly() will take an input (whatever), if it’s traversable, it will traverse it, leaving only the first element within each array it encounters.

The function arrayFirstOnly() is imported like this:

It takes one input argument:

Input argumentTypeObligatoryDescription
input
Type: JsonValue
Obligatory: yes
inputJsonValueyesSupported tree; explicit undefined is valid, but the argument must be present.

Output

Function returns an amended cloned input.

A use example

import {
  find,
  get,
  set,
  drop,
  del,
  arrayFirstOnly,
  traverse,
} from "ast-monkey";
const input = [
  {
    a: "a",
  },
  {
    b: "b",
  },
];
const result = arrayFirstOnly(input);
console.log("result = " + JSON.stringify(result, null, 4));
// => [
//      {
//        a: 'a'
//      }
//    ]

The complete input is validated before trimming. Every array retains at most its first slot. If that slot is a sparse hole, the hole remains sparse; if it explicitly contains undefined, the property remains present. The implementation scales linearly with the size and nesting depth of the tree.

In practice, it’s handy when you want to simplify the data objects. For example, all our email templates have content separated from the template layout. Content sits in index.json file. For dev purposes, we want to show, let’s say two products in the shopping basket listing. However, in a production build, we want to have only one item, but have it sprinkled with back-end code (loop logic and so on). This means, we have to take data object meant for a dev build, and flatten all arrays in the data, so they contain only the first element. ast-monkey comes to help.

API — traverse()

The function traverse() is imported like this:

traverse(tree, callback) transforms a cloned tree. Return the current value to keep it, another supported value to replace it, or DELETE to remove it. A callback without an explicit return replaces the current value with undefined. NaN is ordinary numeric data.

import { DELETE, traverse } from "ast-monkey/traverse";

const result = traverse({ keep: undefined, remove: "obsolete" },
  (key, value, innerObj) => {
    const current = innerObj.parentType === "array" ? key : value;
    return current === "obsolete" ? DELETE : current;
  },
);
// result: { keep: undefined }

Object entries receive (key, value); array entries receive (value, undefined). Use innerObj.parentType to distinguish them because an object’s value can also be undefined. The root itself receives no callback.

Traversal skips sparse array holes, descends into callback replacements, and stops further callbacks when you set stop.now = true. Metadata includes the dotted path, exact pathSegments, parentKey, depth, topmostKey, and a lazy detached read-only parent snapshot. Read parent during its callback to capture that callback’s parent state. The iterative implementation supports deep trees without depending on the JavaScript call stack.

The complete transformer contract remains applicable. The consolidated implementation keeps the shared deletion token Symbol.for("ast-monkey-traverse.delete").

API — traverseWithLookahead()

traverseWithLookahead(tree, callback, count = 0) observes depth-first visits and returns undefined. Callback return values are ignored. Use count to request upcoming visits in innerObj.next:

import { traverseWithLookahead } from "ast-monkey/lookahead";

const visits = [];
traverseWithLookahead({ a: { b: 1 }, c: 2 }, (key, value, innerObj) => {
  visits.push({
    path: innerObj.path,
    nextPaths: innerObj.next.map(([, , next]) => next.path),
  });
}, 2);
// visits:
// [
//   { path: "a", nextPaths: ["a.b", "c"] },
//   { path: "a.b", nextPaths: ["c"] },
//   { path: "c", nextPaths: [] },
// ]

Each next entry is a [key, value, innerObj] tuple. These are upcoming traversal visits, which can include descendants and need not be siblings. The callback receives the same object-entry and array-entry argument shapes as traverse(), and neither API reports the root itself.

The observer retains the standalone package’s behavior:

  • It visits sparse array holes and explicit undefined entries.
  • Callback values can reference original subtrees. Mutating those values can change the input; ignored return values do not provide mutation isolation.
  • parent and future metadata use eager detached mutable clones. Paths use dotted path strings; there is no pathSegments or parentKey field.
  • Setting stop.now = true stops collecting visits, but already buffered callbacks still run. With a lookahead of 2, up to two callbacks can remain.
  • Traversal is recursive, so sufficiently deep trees can exhaust the call stack. The API validates the callback, but does not apply the transformer’s supported-tree validation or finite-integer validation to count.

For more examples, see the lookahead observer contract.

Choose an entry point

// Helpers and both traversal APIs:
import { find, traverse, DELETE, traverseWithLookahead } from "ast-monkey";
// Transformation only:
import { traverse, DELETE } from "ast-monkey/traverse";
// Observation only:
import { traverseWithLookahead } from "ast-monkey/lookahead";

Use a subpath when you only need one traversal API. It avoids loading the high-level helper implementation and lets bundlers include less JavaScript. All entry points belong to the same npm package, so subpaths do not reduce the dependencies installed by npm.

The existing direct-browser file remains ast-monkey.umd.js, with the global astMonkey. The former standalone files and globals remain associated with their published packages: ast-monkey-traverse.umd.js / astMonkeyTraverse and ast-monkey-traverse-with-lookahead.umd.js / astMonkeyTraverseWithLookahead.

Migrate traversal imports

Install version 10 or later:

npm install ast-monkey@^10.0.0

Update imports as follows:

Previous importConsolidated import
import { traverse, DELETE } from "ast-monkey-traverse"
import { traverse, DELETE } from "ast-monkey-traverse"import { traverse, DELETE } from "ast-monkey/traverse"
import { traverse } from "ast-monkey-traverse-with-lookahead"
import { traverse } from "ast-monkey-traverse-with-lookahead"import { traverseWithLookahead as traverse } from "ast-monkey/lookahead"

Keep observer callbacks on traverseWithLookahead(): their ignored returns, lookahead tuples, and buffered stopping differ from transformation. Run your application’s traversal tests before removing its old dependency.

Migrate traverse, DELETE, and callback types together to ast-monkey/traverse. The canonical root and subpaths share one DELETE type, but the standalone package’s declaration has a separate TypeScript unique symbol identity. Mixing a legacy traversal or callback type with the canonical token (or the reverse) fails TypeScript assignment even though the runtime token still uses the same global symbol registry key.

Canonical validation errors use ast-monkey/traverse(): [THROW_ID_XX] and ast-monkey/traverseWithLookahead(): [THROW_ID_XX]. Update assertions that match the previous package prefixes. Transformer validation identifiers follow source order: invalid trees and replacements use THROW_ID_01, and invalid callbacks use THROW_ID_02 (the standalone transformer uses the reverse numbering). The observer retains THROW_ID_01 for an invalid callback. Exceptions thrown by your own callback propagate unchanged.

Traversal types are exported from the root and their corresponding subpaths. The transformer retains names such as Callback, InnerObj, Stop, and TreeValue; observer types use LookaheadCallback, LookaheadInnerObj, LookaheadNextToken, and LookaheadObj so the two contracts remain distinct.

API — types

This package is written in TypeScript and exports the following types:

TypeDescription
Finding
Type: Finding
FindingOne find() result: its index, key, val, and breadcrumb path.
FindCriteria
Type: FindCriteria
FindCriteriaThe explicit key, value, or entry selector union.
FindOpts
Type: FindOpts
FindOptsThe options accepted by find().
GetOpts
Type: GetOpts
GetOptsThe options accepted by get().
SetOpts
Type: SetOpts
SetOptsThe options accepted by set().
DropOpts
Type: DropOpts
DropOptsThe options accepted by drop().
DelOpts
Type: DelOpts
DelOptsThe options accepted by del().
Only
Type: Only
OnlyAll accepted parent-container filter aliases.
TraversalIndex
Type: TraversalIndex
TraversalIndexA numeric traversal index or its decimal string spelling.
JsonValue
Type: JsonValue
JsonValueA supported recursive value, including undefined and NaN.
JsonObject
Type: JsonObject
JsonObjectAn ordinary string-keyed object whose values are JsonValues.
JsonArray
Type: JsonArray
JsonArrayAn array of JsonValues.
import type {
  DelOpts,
  DropOpts,
  FindCriteria,
  Finding,
  FindOpts,
  GetOpts,
  JsonArray,
  JsonObject,
  JsonValue,
  Only,
  SetOpts,
  TraversalIndex,
} from "ast-monkey";

Permalink to changelogChangelog

Open Changelog
↑ back to top
prev next

Copyright

All rights reserved © Roy Revelt 2026
All our open source packages are under MIT licenceopens in a new tab

Activities

🐛 See a bug? Raise an issueopens in a new tab
💘 Check out the Indiewebopens in a new tab and Libera manifestoopens in a new tab