estree-util-build-jsx
Converts JSX embedded in an ESTree syntax tree into React.createElement or automatic jsx-runtime calls.
Repository Health
Technical Analysis
estree-util-build-jsx is a small, focused unified/syntax-tree ecosystem utility that walks an ESTree-compliant JavaScript syntax tree and rewrites any embedded JSX elements and fragments into plain function calls. Rather than parsing source text the way Babel or SWC do, it operates purely on an already-parsed AST, so it can slot into any toolchain that already produces an estree — Acorn with acorn-jsx, Espree, or any other ESTree-compatible parser — without imposing its own parsing step.
It supports both JSX transform runtimes: the classic React.createElement/React.Fragment pragma style (configurable via pragma/pragmaFrag options or @jsx-style comments) and the modern automatic runtime that imports jsx, jsxs, and Fragment from a configurable importSource (with an optional development mode that emits jsxDEV calls carrying source location metadata). Because it works purely on estrees, it underpins JSX support in unified-ecosystem tooling such as MDX, letting Markdown-embedded JSX compile down to real function calls without depending on Babel.
What You Get
- A single
buildJsx(tree, options)function that mutates an estree in place, replacing JSX nodes with call expressions - Support for both the classic (
React.createElement) and automatic (jsx/jsxs/jsxDEV) JSX runtimes - In-file configuration via
@jsx,@jsxFrag,@jsxImportSource, and@jsxRuntimecomment pragmas, in addition to programmatic options - Fully typed TypeScript definitions with 100% type coverage and a documented
Options/RuntimeAPI - Automatic namespace handling (
<a:b>becomesh('a:b', ...)) and prop-order-preserving handling of spread attributes
Common Use Cases
- Compiling JSX embedded in Markdown/MDX documents down to function calls as part of a unified/remark/mdast processing pipeline
- Building a custom JSX compiler on top of Acorn (with acorn-jsx) or Espree without adopting Babel’s parser or AST shape
- Targeting the modern automatic JSX runtime (React 17+, Preact, or other hyperscript-compatible libraries) from tools that already produce estrees
- Adding development-mode JSX transforms that emit source file/line/column metadata for debugging tools
Under The Hood
Architecture
The package is a single module (lib/index.js) exporting one function, buildJsx, built on estree-walker’s walk() traversal. The enter visitor only scans Program-level comments for @jsx-style pragmas; all real work happens in leave, so post-order traversal guarantees a JSX element’s children have already been replaced with CallExpression nodes (via this.replace(call)) before the parent element itself is processed. Helper functions — toIdentifier, toProperty, and toMemberExpression — build the pieces of each call (callee name, props object, children arguments), and a small create() helper copies start/end/loc/range/comments from the original JSX node onto its replacement so source maps stay accurate. Import bookkeeping is done through an imports object closed over the whole traversal, consulted at the end of Program to decide whether an ImportDeclaration for jsx/jsxs/jsxDEV/Fragment needs to be spliced in after any leading directive prologue. It’s a single deterministic, in-place tree-rewrite pass with no plugin system or intermediate representation.
Tech Stack
Runtime dependencies are minimal and purpose-built: @types/estree-jsx (types only), devlop (a dev-mode assert helper stripped from production builds via conditional exports), estree-util-is-identifier-name (validates generated identifier names), and estree-walker@3 (the traversal engine) — no Babel, no bundler, no framework. The real tooling shows up in devDependencies: TypeScript 5 compiles the JSDoc-typed .js source into hand-checked .d.ts declarations via tsc --build, with type-coverage enforcing 100% strict type coverage; acorn, acorn-jsx, and astring are dev-only, used in tests to parse real JSX source and regenerate JavaScript from the transformed tree; c8 enforces 100% test coverage; xo (a strict ESLint preset) plus prettier handle linting and formatting; remark-cli with remark-preset-wooorm lints the Markdown docs themselves. The package is ESM-only (type: module, single exports: './index.js' entry) and targets Node.js 16+.
Code Quality
A single 47KB test.js file uses Node’s built-in node:test runner with dozens of nested t.test() subtests and around 69 assertions, covering both runtimes, comment-pragma overrides, namespace names, spread-attribute ordering, development-mode source metadata, and edge cases like empty expression children. Tests parse real JSX source with acorn/acorn-jsx and regenerate code via astring to assert exact string output rather than only structural snapshots, and c8 --100 is wired into the test script so any untested branch fails CI. Error handling is explicit: buildJsx throws descriptive Errors for invalid pragma combinations (e.g. @jsxRuntime automatic combined with @jsx) and uses devlop’s ok() assertions for invariants that should never be false. The whole codebase is authored as typed JSDoc with checkJs: true and exactOptionalPropertyTypes: true in tsconfig.json, giving full static type checking without a separate .ts source tree. CI (.github/workflows/main.yml) runs the full build+format+coverage suite across two Node versions with Codecov upload.
API Design
The public API is deliberately narrow: one function, buildJsx(tree, options), that mutates the passed tree in place and returns nothing — minimal ceremony to call, though it does mean callers must already hold a mutable reference to the tree and there’s no pure/functional variant that returns a new tree. Configuration is dual-sourced: an options object, or in-file @jsx/@jsxFrag/@jsxImportSource/@jsxRuntime comment pragmas that mirror Babel’s classic convention, which lowers the porting cost for anyone migrating a Babel-based JSX pipeline. Exported Options and Runtime TypeScript types are documented per-field in JSDoc, so editor tooling surfaces the docs without an external documentation site. Its differentiation from alternatives (chiefly Babel’s own JSX plugin) is narrow but real: it’s the practical choice for any purely Acorn/Espree-based toolchain, which is exactly why unified-ecosystem tools like MDX depend on it rather than pulling in Babel.