| name | ast-code-manipulation |
| description | AST-based code reading, analysis, and mutation for AI agents. JavaScript AST parsing with espree/acorn, AST-to-code generation with escodegen, large-source string editing with magic-string and source maps, CSS AST walking with PostCSS. Never use regex for structural code transforms — use AST. Sources: eslint/espree, acornjs/acorn, estools/escodegen, rich-harris/magic-string, postcss/postcss. |
/ast-code-manipulation
When to Use
- Reading, analyzing, or modifying JavaScript/TypeScript structure (not text)
- "Find all function calls to X and rename them" — regex will break, AST won't
- Injecting code at a precise AST node without corrupting surrounding syntax
- Editing CSS at the property level while preserving formatting
- Generating a source map alongside a code transform
Do NOT use for
- Simple substring replace (no structural meaning — use string.replace())
- Extracting a comment from the top of a file (line regex is fine)
Decision: espree vs acorn
Need ESLint-compatible AST (includes range, loc, tokens by default)?
YES → espree (ESLint's own parser, adds scope analysis)
NO →
Need JSX / TypeScript support?
YES → @typescript-eslint/parser (built on espree)
Need absolute minimum size + speed?
YES → acorn (3KB, no extras, backbone of Webpack/Rollup/Vite)
Parse JavaScript → AST (espree)
import espree from 'espree'
const code = `
import { runAgent } from './agent'
async function main() {
const result = await runAgent({ task: 'summarize', depth: 3 })
return result
}
`
const ast = espree.parse(code, {
ecmaVersion: 2022,
sourceType: 'module',
range: true,
loc: true,
tokens: true,
comment: true,
})
function walk(node: any, visitor: (node: any) => void) {
if (!node || typeof node !== 'object') return
visitor(node)
for (const key of Object.keys(node)) {
if (key === 'parent') continue
const child = node[key]
if (Array.isArray(child)) child.forEach(c => walk(c, visitor))
else if (child?.type) walk(child, visitor)
}
}
const calls: string[] = []
walk(ast, (node) => {
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
calls.push(node.callee.name)
}
})
Fast Parse (acorn)
import * as acorn from 'acorn'
const ast = acorn.parse(code, {
ecmaVersion: 2022,
sourceType: 'module',
locations: true,
ranges: true,
})
import * as walk from 'acorn-walk'
walk.simple(ast, {
ImportDeclaration(node) {
console.log('import from:', (node as any).source.value)
},
FunctionDeclaration(node) {
console.log('function:', (node as any).id?.name)
},
})
AST → Code (escodegen)
import escodegen from 'escodegen'
import * as acorn from 'acorn'
const code = `function greet(name) { return 'Hello, ' + name }`
const ast = acorn.parse(code, { ecmaVersion: 2022 }) as any
const funcBody = ast.body[0].body.body
funcBody.unshift({
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' },
property: { type: 'Identifier', name: 'log' }, computed: false },
arguments: [{ type: 'Literal', value: '[greet called]', raw: "'[greet called]'" }],
},
})
const output = escodegen.generate(ast, {
: {
: { : },
: ,
},
: ,
: ,
})
Large-File String Editing (magic-string)
import MagicString from 'magic-string'
import espree from 'espree'
const source = `export const VERSION = "1.3.45"\nexport function run() {}`
const s = new MagicString(source)
const ast = espree.parse(source, { ecmaVersion: 2022, sourceType: 'module', range: true })
espree.parse(source, { ecmaVersion: 2022, sourceType: 'module', range: true, tokens: true })
let found = false
function walk(node: any) {
if (!node || typeof node !== 'object') return
if (node.type === 'Literal' && node.value === '1.3.45') {
s.overwrite(node.range![0], node.range![1], '"1.3.46"')
found =
}
( key .(node)) {
(.(node[key])) node[key].(walk)
(node[key]?.) (node[key])
}
}
(ast)
s.()
s.()
result = s.()
map = s.({ : , : , : })
CSS AST (PostCSS)
import postcss from 'postcss'
const css = `
.agent-output {
color: red;
font-size: 14px;
background: var(--color-bg);
}
`
const root = postcss.parse(css)
root.walkDecls((decl) => {
if (decl.prop === 'color') {
decl.prop = 'color-foreground'
}
if (decl.value.endsWith('px') && !decl.prop.startsWith('--')) {
const px = parseInt(decl.value)
decl.value = `var(--spacing-${px})`
}
})
root.walkRules((rule) => {
rule.append({ prop: 'box-sizing', value: 'border-box' })
})
root.walkDecls('background', (decl) => {
if (!decl.value.startsWith('var(')) decl.()
})
output = root.()
.(output.)
enforceTokens = postcss.(, {
{
root.(, {
(!decl..()) {
decl.(root.!, )
}
})
}
})
Anti-Fake-Pass Checklist
❌ Regex used for function rename (breaks on multi-line, template literals, comments)
❌ espree parsed without range: true then magic-string used (no byte offsets = wrong)
❌ AST node injected by string concatenation instead of node object (escodegen rejects)
❌ magic-string overwrite() on overlapping ranges (throws InvalidMapping error)
❌ escodegen without sourceMap option when transform needs debugging (no traceability)
❌ PostCSS root.toString() used instead of root.toResult().css (loses map data)
❌ acorn-walk.simple() used when parent chain is needed (use .ancestor() instead)
❌ Walk function missing 'parent' skip — espree backrefs cause infinite recursion