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

prevOpen Source→ast-monkeynext

ast-monkey9.2.5

Traverse and edit AST

Downloads per monthChangelogMIT Licenselibera manifesto
  • the top
  • Installation
  • Quick Take
  • Examples
  • THE CHALLENG…
  • IDEA
  • SUPPORTED TRE…
  • API — FIND()
  • API — GET()
  • API — SET()
  • API — DROP()
  • API — DEL()
  • API — ARRAYFIRSTONL…
  • API — TRAVERSE()
  • 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
  • Set a node's value by its traversal index
  • Set a value to undefined
  • Set a node using key instead of val
  • Delete during traversal with the collision-free token
  • Traverse a tree using the re-exported traverse

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 helpers 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, while an existing undefined element is preserved and receives its own 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 helper works on a clone. It does not mutate the input, including when get() or find() returns a nested object or array.

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() and its DELETE token are re-exported from ast-monkey-traverse. Return DELETE from a traversal callback to remove the current node. NaN is ordinary data and is never a deletion command.

For the callback and metadata contract, see the traversal documentation.

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