Unist-utils
Strictly-typed AST utilities for unist/mdast/hast used by Boltdocs core, every official @bdocs/* plugin, and the Sätteri MDX processor. Single source of truth for visitor helpers, AST builders, h-properties helpers, class-list mutation, and meta string parsing.
@bdocs/unist-utils is a standalone npm package that exposes the
strictly-typed AST utilities shared by boltdocs core, every
official @bdocs/* plugin, and the Sätteri MDX processor. Before this
package existed, the same helpers lived in two places inside the
monorepo with subtly different typings; it is now the single source of
truth.
This page is the official reference. Use it whenever you need to walk or mutate the MDAST/HAST tree of an MDX file — inside a custom remark plugin, a build-time transformer, or a runtime component that wants statically verifiable shapes.
Every public export is fully typed. No any, no unknown at the boundaries —
Node, Parent, ElementNode, MdxJsxElement, etc. are all declared in
this package so plugin authors can build typed-only code with confidence.
Why it exists
Three reasons:
- Single source of truth. Before,
visitNodes,createMdxElement,setNodeProperty,parseMetaStringetc. were duplicated betweenpackages/core/src/node/plugins/plugin-utils.tsand the Sätteri plugin's adapters. Now they live in one place, with one signature. - Public surface for plugin authors. External plugins were forced
to either reach into
boltdocs's internal barrel or write their own AST helpers with looser types.@bdocs/unist-utilsgives them a typed surface and decouples them from boltdocs internals. - Sets the stage for richer plugin APIs. Subsequent boltdocs phases (better lifecycle, granular component injection, in-place middleware, MDX transformer API) all build on top of this package rather than on top of boltdocs itself.
Public surface
| Group | Exports |
|---|---|
| Node-type constants | MDX_NODES, MdxNodeType, re-exports SKIP, EXIT from unist-util-visit@5 |
| Generic unist types | Node, Parent |
| MDAST | MdxJsxAttribute, MdxJsxAttributeValueExpression, MdxJsxElement, MdxJsxChild, CodeNode, PlainTextNode |
| HAST | ElementNode, HastNode, HastChild |
| Helpers | NodeWithHProperties |
| Type guards | isMdxJsxElement, isMdxJsxTextElement, isMdxJsxLike, isElementNode, isTextNode |
| Visitors | visitNodes, visitRehypeElements, visitMdxElements, visitRemarkHeadings, visitRemarkLinks |
| Builders | createMdxAttribute, createMdxElement, createRehypeElement |
| Properties | setNodeProperty, getNodeProperty |
| Class list | addNodeClass, removeNodeClass, hasNodeClass |
| Meta parser | parseMetaString, ParsedMeta |
Every export is also re-exported by boltdocs core for back-compat so
existing plugin code keeps working without change.
Behavioural contract — SKIP / EXIT
unist-util-visit@5 exports SKIP and EXIT as the string
'skip' and the boolean false respectively — not as Symbols
as in earlier versions. The package pins this contract and exposes
tests so plugin authors can rely on it.
Examples
Walking MDAST code blocks (mermaid-style)
import {
MDX_NODES,
type CodeNode,
type Node,
type Parent,
} from "@bdocs/unist-utils";
export function remarkCodeFences() {
return (tree: Node) => {
const collected: Array<{ node: CodeNode; parent: Parent; index: number }> =
[];
visitNodes(tree, MDX_NODES.CODE, (node, index, parent) => {
if (node.lang === "mermaid") {
collected.push({ node, parent, index });
}
});
for (const { node, parent, index } of collected) {
parent.children[index] = {
type: "mdxJsxFlowElement",
name: "Mermaid",
} as Node;
}
};
}
Building MDX JSX (typed)
import {
createMdxElement,
createMdxAttribute,
type MdxJsxChild,
} from "@bdocs/unist-utils";
const children: MdxJsxChild[] = [];
const el = createMdxElement("MyChart", {
chart: "graph TD",
config: createMdxAttribute("config", { theme: "dark" }),
});
Manipulating class names
import {
addNodeClass,
removeNodeClass,
hasNodeClass,
} from "@bdocs/unist-utils";
addNodeClass(node, "shiki-fallback");
if (hasNodeClass(node, "shiki")) removeNodeClass(node, "shiki");
Parsing code-fence meta strings
import { parseMetaString } from "@bdocs/unist-utils";
const meta = parseMetaString('title="My Example" lineNumbers');
// → { title: 'My Example', lineNumbers: true }
parseMetaString populates title, lineNumbers, and wordWrap. The
__raw field on ParsedMeta is not set by parseMetaString —
callers attach it themselves before handing the meta to a downstream
consumer (e.g. shiki-adapter in Boltdocs core populates it before
passing meta to the highlighter so the original unparsed string is
recoverable).
Type guards as narrowing predicates
import { isMdxJsxElement, isElementNode } from "@bdocs/unist-utils";
function visit(tree: unknown) {
if (Array.isArray(tree)) {
tree.forEach((child) => {
if (isMdxJsxElement(child)) {
// child is narrowed to MdxJsxElement
} else if (isElementNode(child)) {
// child is narrowed to ElementNode
}
});
}
}
Migration from boltdocs
Old code (still works as a back-compat shim):
import { visitNodes, createMdxAttribute } from "boltdocs";
New code (preferred for new plugins):
import { visitNodes, createMdxAttribute } from "@bdocs/unist-utils";
Both paths are equal in functionality today. The old one forwards to
the new package via a thin shim. New code should import from
@bdocs/unist-utils directly so the dependency surfaces cleanly in
your bundle output and you avoid the back-compat shim's slight runtime
overhead.
Strictness
MdxJsxElement.children is typed as MdxJsxChild[] rather than any[],
and ElementNode.properties is typed as Record<string, unknown>
rather than Record<string, any>. Plugin authors who read these
fields will need to narrow with the provided isX guards or a custom
predicate.
Behavioural caveat — createMdxAttribute with objects
createMdxAttribute(name, objectValue) keeps the object verbatim
under value. For object values that you want to embed as a JS
expression in compiled MDX, stringify explicitly:
import { createMdxAttribute } from "@bdocs/unist-utils";
const attr = createMdxAttribute(
"config",
`{ theme: 'dark', animations: true }`,
);
A stricter builder that always wraps object values into a real
mdxJsxAttributeValueExpression will land in a future boltdocs
Phase. Until then, prefer the manual stringification above.
Edge cases & constraints
- Reserved prefix. If you call
createMdxAttribute(or any other helper) with a malicious-looking argument, the helpers do not sanitise HTML. They are designed to be run on already-trusted in-memory tree representations. Escape user-supplied content before it lands in the MDAST. - AST node validity. The visitors do not validate sibling/parent
invariants. If you mutate
parent.children[index], ensure you replace with a structurally valid node (use the builder helpers to guarantee this). - Tree boundary.
visit*does not walk across multiple trees. Each tree needs its ownvisitcall.
License
Released under the MIT License, matching the rest of the Boltdocs monorepo.