Skip to main content

acorn vs @babel/parser vs espree: JavaScript AST Parsers (2026)

·PkgPulse Team

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/types helpers)
  • For TypeScript: @babel/parser (no type checking) or typescript compiler 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

Featureacorn@babel/parserespree
AST specESTreeBabel (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.

Compare developer tooling and parser packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.