# neotraverse Traverse and transform objects by visiting every node on a recursive walk. This is a fork and TypeScript rewrite of [traverse](https://github.com/ljharb/js-traverse) with 0 dependencies and major improvements: - 🤌 1.38KB min+brotli - 🚥 Zero dependencies - 🎹 TypeScript. Throw away the `@types/traverse` package - ❎ No polyfills - 🛸 ESM-first - 📜 Legacy mode supporting ES5 # Principles Rules this package aims to follow for an indefinite period of time: - No dependencies. - No polyfills. - ESM-first. - Pushing to be modern - Always provide a legacy mode - Always follow `traverse` API. There already are many packages that do this. `neotraverse` intends to be a drop-in replacement for `traverse` and provide the same API with 0 dependencies and enhanced Developer Experience. - All deviating changes happen in `neotraverse/modern` build. # Modern build `neotraverse/modern` provides a new class `new Traverse()`, and all methods and state is provided as first argument `ctx` (`this.update -> ctx.update`, `this.isLeaf -> ctx.isLeaf`, etc.) Before: ```js import traverse from 'neotraverse'; const obj = { a: 1, b: 2, c: [3, 4] }; traverse(obj).forEach(function (x) { if (x < 0) this.update(x + 128); }); ``` After: ```js import { Traverse } from 'neotraverse/modern'; const obj = { a: 1, b: 2, c: [3, 4] }; new Traverse(obj).forEach((ctx, x) => { if (x < 0) ctx.update(x + 128); }); ``` # Which build to use? `neotraverse` provides 3 builds: - default: Backwards compatible with `traverse` and provides the same API, but ESM only and compiled to ES2022 with Node 18+ - modern: Modern build with ESM only and compiled to ES2022 with Node 18+. Provides a new class `new Traverse()`, and all methods and state is provided as first argument `ctx` (`this.update -> ctx.update`, `this.isLeaf -> ctx.isLeaf`, etc.) - legacy: Legacy build with ES5 and CJS, compatible with `traverse` and provides the same API. Here's a matrix of the different builds: | Build | ESM | CJS | Browser | Node | Polyfills | Size | | ------- | --------- | --- | ------- | ---- | --------- | ----------------- | | default | ✅ ES2022 | | ✅ | ✅ | ❌ | 1.54KB min+brotli | | modern | ✅ ES2022 | | ✅ | ✅ | ❌ | 1.38KB min+brotli | | legacy | ✅ ES5 | ✅ | ✅ | ✅ | ❌ | 2.73KB min+brotli | If you are: ## starting from scratch ```ts import { Traverse } from 'neotraverse/modern'; const obj = { a: 1, b: 2, c: [3, 4] }; new Traverse(obj).forEach((ctx, x) => { if (x < 0) ctx.update(x + 128); // `this` is same as `ctx` when using regular function }); ``` ## migrating from `traverse` ### and you don't care about old browsers or Node versions: Use default build for no breaking changes, and a modern build for better developer experience. ```ts import traverse from 'neotraverse'; const obj = { a: 1, b: 2, c: [3, 4] }; traverse(obj).forEach(function (x) { if (x < 0) this.update(x + 128); }); ``` ### and you care about old browsers or Node versions: Use legacy build for compatibility with old browsers and Node versions. ```js const traverse = require('neotraverse/legacy'); ``` ESM: ```js import traverse from 'neotraverse/legacy'; ``` # examples ## transform negative numbers in-place negative.js ```js import { Traverse } from 'neotraverse/modern'; const obj = [5, 6, -3, [7, 8, -2, 1], { f: 10, g: -13 }]; new Traverse(obj).forEach(function (ctx, x) { if (x < 0) ctx.update(x + 128); }); console.dir(obj); ``` or in legacy mode: ```js import traverse from 'neotraverse'; // OR import traverse from 'neotraverse/legacy'; const obj = [5, 6, -3, [7, 8, -2, 1], { f: 10, g: -13 }]; traverse(obj).forEach(function (x) { if (x < 0) this.update(x + 128); }); // This is identical to the above traverse.forEach(obj, function (x) { if (x < 0) this.update(x + 128); }); console.dir(obj); ``` Output: [ 5, 6, 125, [ 7, 8, 126, 1 ], { f: 10, g: 115 } ] ## collect leaf nodes leaves.js ```js import { Traverse } from 'neotraverse/modern'; const obj = { a: [1, 2, 3], b: 4, c: [5, 6], d: { e: [7, 8], f: 9 } }; const leaves = new Traverse(obj).reduce((ctx, acc, x) => { if (ctx.isLeaf) acc.push(x); return acc; }, []); console.dir(leaves); ``` or in legacy mode: ```js import traverse from 'neotraverse'; // OR import traverse from 'neotraverse/legacy'; const obj = { a: [1, 2, 3], b: 4, c: [5, 6], d: { e: [7, 8], f: 9 } }; const leaves = traverse(obj).reduce(function (acc, x) { if (this.isLeaf) acc.push(x); return acc; }, []); // Equivalent to the above const leavesLegacy = traverse.reduce( obj, function (acc, x) { if (this.isLeaf) acc.push(x); return acc; }, [] ); console.dir(leaves); console.dir(leavesLegacy); ``` Output: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ] ## scrub circular references scrub.js: ```js import { Traverse } from 'neotraverse/modern'; const obj = { a: 1, b: 2, c: [3, 4] }; obj.c.push(obj); const scrubbed = new Traverse(obj).map(function (ctx, x) { if (ctx.circular) ctx.remove(); }); console.dir(scrubbed); ``` or in legacy mode: ```js import traverse from 'neotraverse'; // OR import traverse from 'neotraverse/legacy'; const obj = { a: 1, b: 2, c: [3, 4] }; obj.c.push(obj); const scrubbed = traverse(obj).map(function (x) { if (this.circular) this.remove(); }); // Equivalent to the above const scrubbedLegacy = traverse.map(obj, function (x) { if (this.circular) this.remove(); }); console.dir(scrubbed); console.dir(scrubbedLegacy); ``` output: { a: 1, b: 2, c: [ 3, 4 ] } ## commonjs neotraverse/legacy is compatible with commonjs and provides the same API as `traverse`, acting as a drop-in replacement: ```js const traverse = require('neotraverse/legacy'); ``` ## esm ```js import { Traverse } from 'neotraverse/modern'; ``` ```js import traverse from 'neotraverse'; ``` # Differences from `traverse` - ESM-first - ES2022, Node 18+ - Types included by default. No need to install `@types/traverse` - Works as-is in all major browsers and Deno - No polyfills - `new Traverse()` class instead of regular old `traverse()` - Legacy mode supporting `ES5` and `CJS` There is a legacy mode that provides the same API as `traverse`, acting as a drop-in replacement: ```js import traverse from 'neotraverse'; const obj = { a: 1, b: 2, c: [3, 4] }; traverse(obj).forEach(function (x) { if (x < 0) this.update(x + 128); }); ``` If you want to support really old browsers or NodeJS, supporting ES5, there's `neotraverse/legacy` which is compatible with ES5 and provides the same API as `traverse`, acting as a drop-in replacement for older browsers: ```js import traverse from 'neotraverse/legacy'; const obj = { a: 1, b: 2, c: [3, 4] }; traverse(obj).forEach(function (x) { if (x < 0) this.update(x + 128); }); ``` # Migrating from `traverse` ### Step 1: Install `neotraverse` ```sh npm install neotraverse npm uninstall traverse @types/traverse # Remove the old dependencies ``` ### Step 2: Replace `traverse` with `neotraverse` ```diff -import traverse from 'traverse'; +import traverse from 'neotraverse'; const obj = { a: 1, b: 2, c: [3, 4] }; -traverse(obj).forEach(function (x) { +traverse(obj).forEach(function (x) { if (x < 0) this.update(x + 128); }); ``` Optionally, there's also a legacy mode that provides the same API as `traverse`, acting as a drop-in replacement: ```js import traverse from 'neotraverse/legacy'; const obj = { a: 1, b: 2, c: [3, 4] }; traverse(obj).forEach(function (x) { if (x < 0) this.update(x + 128); }); ``` ### Step 3(Optional): Bundle time aliasing If you use Vite, you can aliss `traverse` to `neotravers/legacy` in your `vite.config.js`: ```js import { defineConfig } from 'vite'; export default defineConfig({ resolve: { alias: { traverse: 'neotraverse' // or 'neotraverse/legacy' } } }); ``` # methods Each method that takes an `fn` uses the context documented below in the context section. ## .map(fn) Execute `fn` for each node in the object and return a new object with the results of the walk. To update nodes in the result use `ctx.update(value)`(modern) or `this.update(value)`(legacy). ## .forEach(fn) Execute `fn` for each node in the object but unlike `.map()`, when `ctx.update()`(modern) or `this.update()`(legacy) is called it updates the object in-place. ## .reduce(fn, acc) For each node in the object, perform a [left-fold]() with the return value of `fn(acc, node)`. If `acc` isn't specified, `acc` is set to the root object for the first step and the root element is skipped. ## .paths() Return an `Array` of every possible non-cyclic path in the object. Paths are `Array`s of string keys. ## .nodes() Return an `Array` of every node in the object. ## .clone() Create a deep clone of the object. ## .get(path) Get the element at the array `path`. ## .set(path, value) Set the element at the array `path` to `value`. ## .has(path) Return whether the element at the array `path` exists. # context Each method that takes a callback has a context (its `ctx` object, or `this` object in legacy mode) with these attributes: ## this.node The present node on the recursive walk ## this.path An array of string keys from the root to the present node ## this.parent The context of the node's parent. This is `undefined` for the root node. ## this.key The name of the key of the present node in its parent. This is `undefined` for the root node. ## this.isRoot, this.notRoot Whether the present node is the root node ## this.isLeaf, this.notLeaf Whether or not the present node is a leaf node (has no children) ## this.level Depth of the node within the traversal ## this.circular If the node equals one of its parents, the `circular` attribute is set to the context of that parent and the traversal progresses no deeper. ## this.update(value, stopHere=false) Set a new value for the present node. All the elements in `value` will be recursively traversed unless `stopHere` is true. ## this.remove(stopHere=false) Remove the current element from the output. If the node is in an Array it will be spliced off. Otherwise it will be deleted from its parent. ## this.delete(stopHere=false) Delete the current element from its parent in the output. Calls `delete` even on Arrays. ## this.before(fn) Call this function before any of the children are traversed. You can assign into `ctx.keys`(modern) or `this.keys`(legacy) here to traverse in a custom order. ## this.after(fn) Call this function after any of the children are traversed. ## this.pre(fn) Call this function before each of the children are traversed. ## this.post(fn) Call this function after each of the children are traversed. # license MIT