acorn vs @babel/parser vs espree: JavaScript AST Parsers (2026)
TL;DR
acorn is the foundational ECMAScript parser — small, fast, spec-compliant ESTree output, and the parser that espree and many other tools build on. @babel/parser is Babel's parser — supports TypeScript, JSX, Flow, and experimental proposals through plugins, and produces Babel-specific AST (a superset of ESTree). espree is ESLint's parser — wraps acorn, ensures compatibility with ESLint's AST expectations. In 2026: use acorn for plain JavaScript parsing with minimal overhead, @babel/parser when you need TypeScript or JSX support, and espree when you're building ESLint rules or plugins.
Key Takeaways
- acorn: ~20M weekly downloads — tiny (100KB), ESTree-compliant, powers espree and rollup
- @babel/parser: ~15M weekly downloads — TypeScript + JSX + experimental proposals, Babel AST
- espree: ~20M weekly downloads — ESLint's parser, wraps acorn, ESTree-compliant
- ESTree is the standard AST spec — most tools expect ESTree-compatible output
- @babel/parser AST differs from ESTree in some nodes (use
@babel/typeshelpers) - For TypeScript:
@babel/parser(no type checking) ortypescriptcompiler API (type-aware)
What is an AST?
// Source code: "const x = 1 + 2"
// AST (Abstract Syntax Tree):
{
"type": "Program",
"body": [{
"type": "VariableDeclaration",
"kind": "const",
"declarations": [{
"type": "VariableDeclarator",
"id": { "type": "Identifier", "name": "x" },
"init": {
"type": "BinaryExpression",
"operator": "+",
"left": { "type": "Literal", "value": 1 },
"right": { "type": "Literal", "value": 2 }
}
}]
}]
}
// Uses: linters, formatters, bundlers, codemods, transpilers,
// tree-shaking, dead code elimination, code generation
acorn
acorn — the tiny, fast JavaScript parser:
Basic parsing
import * as acorn from "acorn"
// Parse JavaScript:
const ast = acorn.parse(`const x = 1 + 2`, {
ecmaVersion: 2022, // Supported ECMAScript version
sourceType: "module", // "module" or "script"
locations: true, // Include line/column info in nodes
ranges: true, // Include start/end character positions
})
console.log(ast.type) // "Program"
console.log(ast.body[0].type) // "VariableDeclaration"
// Walk the AST:
import { simple as simpleWalk } from "acorn-walk"
simpleWalk(ast, {
Identifier(node) {
console.log("Found identifier:", node.name)
},
Literal(node) {
console.log("Found literal:", node.value)
},
})
Plugins (acorn-jsx, import assertions)
import * as acorn from "acorn"
import jsx from "acorn-jsx"
// Add JSX support via plugin:
const jsxParser = acorn.Parser.extend(jsx())
const jsxAst = jsxParser.parse(`const el = <div className="app">Hello</div>`, {
ecmaVersion: 2022,
sourceType: "module",
})
// acorn-import-attributes for import assertions:
import importAttributes from "acorn-import-attributes"
const extendedParser = acorn.Parser.extend(importAttributes)
const withImport = extendedParser.parse(
`import data from "./data.json" with { type: "json" }`,
{ ecmaVersion: 2022, sourceType: "module" }
)
Used by: rollup, webpack, vite (under the hood)
acorn is the parser for:
- Rollup (bundles using acorn-parsed ASTs)
- webpack (via terser for minification, which uses acorn)
- ESLint (via espree, which wraps acorn)
- Prettier (via @babel/parser, but acorn for some cases)
- Vite (uses magic-string with acorn for HMR transforms)
Characteristics:
- ESTree-compliant (all tools that read ESTree work with acorn output)
- No TypeScript (use @babel/parser or typescript compiler)
- No JSX by default (use acorn-jsx plugin)
- Very fast and small (core is ~80KB)
@babel/parser
@babel/parser — Babel's extensible parser:
Basic parsing with TypeScript
import { parse } from "@babel/parser"
import type { File } from "@babel/types"
// Parse JavaScript:
const jsAst = parse(`const x = 1 + 2`, {
sourceType: "module",
})
// Parse TypeScript:
const tsAst = parse(
`
interface Package {
name: string
version: string
score: number
}
const getPackage = async (name: string): Promise<Package> => {
return fetch(\`/api/\${name}\`).then(r => r.json())
}
`,
{
sourceType: "module",
plugins: ["typescript"],
}
)
console.log(tsAst.program.body[0].type) // "TSInterfaceDeclaration"
Plugin combinations
import { parse } from "@babel/parser"
// TypeScript + JSX (for .tsx files):
const tsxAst = parse(
`
interface Props { name: string }
const Greeting: React.FC<Props> = ({ name }) => (
<div className="greeting">Hello, {name}!</div>
)
`,
{
sourceType: "module",
plugins: [
"typescript",
"jsx",
],
}
)
// Experimental proposals:
const decoratorsAst = parse(
`
@Injectable()
class UserService {
@Inject(DB)
private db: Database
}
`,
{
sourceType: "module",
plugins: [
"typescript",
["decorators", { decoratorsBeforeExport: true }],
"classProperties",
],
}
)
AST traversal with @babel/traverse
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
import * as t from "@babel/types"
const code = `
const greet = (name) => {
console.log("Hello, " + name)
}
`
const ast = parse(code, { sourceType: "module" })
// Traverse and transform the AST:
traverse(ast, {
// Called for every arrow function:
ArrowFunctionExpression(path) {
// Convert arrow functions to regular functions:
path.replaceWith(
t.functionExpression(
null,
path.node.params,
path.node.body as t.BlockStatement
)
)
},
// Called for every string concatenation:
BinaryExpression(path) {
if (path.node.operator === "+" &&
t.isStringLiteral(path.node.left)) {
// Convert: "Hello, " + name → `Hello, ${name}`
path.replaceWith(
t.templateLiteral(
[t.templateElement({ raw: "Hello, " }), t.templateElement({ raw: "" })],
[path.node.right]
)
)
}
},
})
// Generate code back from modified AST:
const { code: transformed } = generate(ast)
console.log(transformed)
// const greet = function(name) { console.log(`Hello, ${name}`) }
Scope analysis
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
const code = `
import { useState } from "react"
const [count, setCount] = useState(0)
const doubled = count * 2
`
const ast = parse(code, {
sourceType: "module",
plugins: ["typescript"],
})
traverse(ast, {
ImportDeclaration(path) {
console.log(`Import from: ${path.node.source.value}`)
// "react"
for (const spec of path.node.specifiers) {
console.log(` → ${spec.local.name}`)
}
},
// Scope tracking:
Identifier(path) {
if (path.scope.hasBinding(path.node.name)) {
const binding = path.scope.getBinding(path.node.name)
console.log(`${path.node.name} is defined at line ${binding?.path.node.loc?.start.line}`)
}
},
})
espree
espree — ESLint's JavaScript parser:
Used for ESLint rules
import espree from "espree"
// Parse JavaScript (ESTree-compliant output):
const ast = espree.parse(`const x = 1 + 2`, {
ecmaVersion: 2022,
sourceType: "module",
loc: true, // Location info
range: true, // Range info
tokens: true, // Include token array (ESLint uses these)
comment: true, // Include comments
})
// espree AST matches ESTree exactly — compatible with:
// - esquery (CSS-like AST selector engine used by ESLint)
// - esrecurse (recursive AST visitor)
// - ESLint's rule API
Writing an ESLint rule (uses espree under the hood)
// packages/eslint-plugin-pkgpulse/src/rules/no-lodash-clonedeep.ts
import type { Rule } from "eslint"
const rule: Rule.RuleModule = {
meta: {
type: "suggestion",
docs: {
description: "Prefer structuredClone over lodash.cloneDeep",
recommended: true,
},
fixable: "code",
},
create(context) {
return {
// ESLint calls this for every CallExpression node:
CallExpression(node) {
if (
node.callee.type === "MemberExpression" &&
node.callee.object.type === "Identifier" &&
node.callee.object.name === "_" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "cloneDeep"
) {
context.report({
node,
message: "Use structuredClone() instead of _.cloneDeep()",
fix(fixer) {
const arg = node.arguments[0]
const argText = context.getSourceCode().getText(arg)
return fixer.replaceText(node, `structuredClone(${argText})`)
},
})
}
},
}
},
}
export default rule
Feature Comparison
| Feature | acorn | @babel/parser | espree |
|---|---|---|---|
| AST spec | ESTree | Babel (superset) | ESTree |
| TypeScript | ❌ (plugin) | ✅ | ❌ |
| JSX | ✅ (plugin) | ✅ | ✅ (plugin) |
| Decorators | ❌ | ✅ | ❌ |
| Bundle size | ~80KB | ~1MB | ~100KB |
| Token output | ✅ | ✅ | ✅ |
| ESLint compatible | ⚠️ (via espree) | ⚠️ (via @babel/eslint-parser) | ✅ |
| Scope analysis | ❌ (needs eslint-scope) | ✅ @babel/traverse | ✅ ESLint API |
| Weekly downloads | ~20M | ~15M | ~20M |
When to Use Each
Choose acorn if:
- Parsing plain JavaScript (no TypeScript, no JSX)
- Building a bundler, minifier, or low-level tool
- Need the smallest footprint with ESTree-compliant output
- Already using rollup/webpack ecosystem tools
Choose @babel/parser if:
- Need TypeScript, JSX, or Flow parsing
- Writing Babel plugins or transforms
- Building a codemod tool that transforms TypeScript/JSX
- Need scope analysis via
@babel/traverse
Choose espree if:
- Writing ESLint rules or plugins
- Building tools that need to be compatible with ESLint's AST
- Working in the ESLint ecosystem (eslint-plugin-, eslint-config-)
For TypeScript with full type information:
// None of these parsers provide TypeScript type checking
// For type-aware analysis, use the TypeScript compiler API:
import ts from "typescript"
const program = ts.createProgram(["src/index.ts"], {
strict: true,
target: ts.ScriptTarget.ES2022,
})
const typeChecker = program.getTypeChecker()
// Access types, symbols, declarations — full semantic analysis
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on acorn v8.x, @babel/parser v7.x, and espree v9.x.