
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@herb-tools/rewriter
Advanced tools
Rewriter system for transforming HTML+ERB AST nodes and formatted strings
Package: @herb-tools/rewriter
Rewriter system for transforming HTML+ERB AST nodes and formatted strings. Provides base classes and utilities for creating custom rewriters that can modify templates.
:::code-group
npm add @herb-tools/rewriter
pnpm add @herb-tools/rewriter
yarn add @herb-tools/rewriter
bun add @herb-tools/rewriter
:::
The rewriter package provides a plugin architecture for transforming HTML+ERB templates. Rewriters can be used to transform templates before formatting, implement linter autofixes, or perform any custom AST or string transformations.
ASTRewriter: Transform the parsed AST (e.g., sorting Tailwind classes, restructuring HTML)StringRewriter: Transform formatted strings (e.g., adding trailing newlines, normalizing whitespace)The rewriter package exposes two main functions for applying rewriters to templates:
rewrite() - Transform AST NodesUse rewrite() when you already have a parsed AST node:
import { Herb } from "@herb-tools/node-wasm"
import { rewrite } from "@herb-tools/rewriter"
import { tailwindClassSorter } from "@herb-tools/rewriter/loader"
await Herb.load()
const template = `<div class="text-red-500 p-4 mt-2"></div>`
const parseResult = Herb.parse(template, { track_whitespace: true })
const sorter = await tailwindClassSorter()
const { output, node } = rewrite(parseResult.value, [sorter])
// output: "<div class="mt-2 p-4 text-red-500"></div>"
// node: The rewritten AST node
rewriteString() - Transform Template StringsUse rewriteString() as a convenience wrapper when working with template strings:
import { Herb } from "@herb-tools/node-wasm"
import { rewriteString } from "@herb-tools/rewriter"
import { tailwindClassSorter } from "@herb-tools/rewriter/loader"
await Herb.load()
const template = `<div class="text-red-500 p-4 mt-2"></div>`
const sorter = await tailwindClassSorter()
const output = rewriteString(Herb, template, [sorter])
// output: "<div class="mt-2 p-4 text-red-500"></div>"
Note: rewrite() returns both the rewritten string (output) and the transformed AST (node), which allows for partial rewrites and further processing. rewriteString() is a convenience wrapper that returns just the string.
Automatically sorts Tailwind CSS classes in class attributes according to Tailwind's recommended order.
Usage:
import { Herb } from "@herb-tools/node-wasm"
import { rewriteString } from "@herb-tools/rewriter"
import { tailwindClassSorter } from "@herb-tools/rewriter/loader"
await Herb.load()
const template = `<div class="px-4 bg-blue-500 text-white rounded py-2"></div>`
const sorter = await tailwindClassSorter()
const output = rewriteString(Herb, template, [sorter])
// output: "<div class="rounded bg-blue-500 px-4 py-2 text-white"></div>"
Features:
class attributesExample transformation:
<!-- Before -->
<div class="px-4 bg-blue-500 text-white rounded py-2">
<span class="font-bold text-lg">Hello</span>
</div>
<!-- After -->
<div class="rounded bg-blue-500 px-4 py-2 text-white">
<span class="text-lg font-bold">Hello</span>
</div>
You can create custom rewriters to transform templates in any way you need.
ASTRewriters receive and modify AST nodes:
import { ASTRewriter } from "@herb-tools/rewriter"
import { Visitor } from "@herb-tools/core"
export default class MyASTRewriter extends ASTRewriter {
get name() {
return "my-ast-rewriter"
}
get description() {
return "Transforms the AST"
}
// Optional: Load configuration or setup
async initialize(context) {
// context.baseDir - project root directory
// context.filePath - current file being processed (optional)
}
// Transform the AST node
rewrite(node, context) {
// Use the Visitor pattern to traverse and modify the AST
const visitor = new MyVisitor()
visitor.visit(node)
// Return the modified node
return node
}
}
class MyVisitor extends Visitor {
visitHTMLElementNode(node) {
// Modify nodes as needed
// node.someProperty = "new value"
this.visitChildNodes(node)
}
}
StringRewriters receive and modify strings:
import { StringRewriter } from "@herb-tools/rewriter"
export default class AddTrailingNewline extends StringRewriter {
get name() {
return "add-trailing-newline"
}
get description() {
return "Ensures file ends with a newline"
}
async initialize(context) {
// Optional setup
}
rewrite(content, context) {
return content.endsWith("\n") ? content : content + "\n"
}
}
By default, rewriters are auto-discovered from: .herb/rewriters/**/*.mjs
::: info File Extension
Custom rewriters must use the .mjs extension to avoid Node.js module type warnings. The .mjs extension explicitly marks files as ES modules.
:::
.herb.ymlReference custom rewriters by their name in your configuration:
formatter:
enabled: true
rewriter:
pre:
- tailwind-class-sorter # Built-in rewriter
- my-ast-rewriter # Custom rewriter
post:
- add-trailing-newline # Custom rewriter
When custom rewriters are loaded, the formatter will display them:
Loaded 2 custom rewriters:
• my-ast-rewriter (.herb/rewriters/my-ast-rewriter.mjs)
• add-trailing-newline (.herb/rewriters/add-newline.mjs)
Using 2 pre-format rewriters:
• tailwind-class-sorter (built-in)
• my-ast-rewriter (.herb/rewriters/my-ast-rewriter.mjs)
Using 1 post-format rewriter:
• add-trailing-newline (.herb/rewriters/add-newline.mjs)
::: warning Rewriter Name Clashes If a custom rewriter has the same name as a built-in rewriter or another custom rewriter, you'll see a warning. The custom rewriter will override the built-in one. :::
::: tip Hot Reload Custom rewriters are automatically reloaded when changed in editors with the Herb Language Server. No need to restart your editor! :::
rewrite()Transform an AST node using the provided rewriters.
function rewrite<T extends Node>(
node: T,
rewriters: Rewriter[],
options?: RewriteOptions
): RewriteResult
Parameters:
node: The AST node to transformrewriters: Array of rewriter instances to apply (must be already initialized)options: Optional configuration
baseDir: Base directory for resolving config files (defaults to process.cwd())filePath: Optional file path for contextReturns: Object with:
output: The rewritten template stringnode: The transformed AST node (preserves input type)rewriteString()Convenience wrapper around rewrite() that parses the template string first and returns just the output string.
function rewriteString(
herb: HerbBackend,
template: string,
rewriters: Rewriter[],
options?: RewriteOptions
): string
Parameters:
herb: The Herb backend instance for parsingtemplate: The HTML+ERB template string to rewriterewriters: Array of rewriter instances to apply (must be already initialized)options: Optional configuration (same as rewrite())Returns: The rewritten template string
ASTRewriterBase class for rewriters that transform AST nodes:
import { ASTRewriter } from "@herb-tools/rewriter"
import type { Node, RewriteContext } from "@herb-tools/rewriter"
class MyRewriter extends ASTRewriter {
abstract get name(): string
abstract get description(): string
async initialize(context: RewriteContext): Promise<void> {
// Optional initialization
}
abstract rewrite<T extends Node>(node: T, context: RewriteContext): T
}
StringRewriterBase class for rewriters that transform strings:
import { StringRewriter } from "@herb-tools/rewriter"
import type { RewriteContext } from "@herb-tools/rewriter"
class MyRewriter extends StringRewriter {
abstract get name(): string
abstract get description(): string
async initialize(context: RewriteContext): Promise<void> {
// Optional initialization
}
abstract rewrite(content: string, context: RewriteContext): string
}
.herb.ymlFAQs
Rewriter system for transforming HTML+ERB AST nodes and formatted strings
The npm package @herb-tools/rewriter receives a total of 94,860 weekly downloads. As such, @herb-tools/rewriter popularity was classified as popular.
We found that @herb-tools/rewriter demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.