npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@herb-tools/rewriter

v0.8.4

Published

Rewriter system for transforming HTML+ERB AST nodes and formatted strings

Readme

Herb Rewriter

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.

Installation

:::code-group

npm add @herb-tools/rewriter
pnpm add @herb-tools/rewriter
yarn add @herb-tools/rewriter
bun add @herb-tools/rewriter

:::

Overview

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.

Rewriter Types

  • ASTRewriter: Transform the parsed AST (e.g., sorting Tailwind classes, restructuring HTML)
  • StringRewriter: Transform formatted strings (e.g., adding trailing newlines, normalizing whitespace)

Usage

Quick Start

The rewriter package exposes two main functions for applying rewriters to templates:

rewrite() - Transform AST Nodes

Use 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 Strings

Use 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.

Built-in Rewriters

Tailwind Class Sorter

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:

  • Sorts classes in class attributes
  • Auto-discovers Tailwind configuration from your project
  • Supports both Tailwind v3 and v4
  • Works with ERB expressions inside class attributes

Example 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>

Custom Rewriters

You can create custom rewriters to transform templates in any way you need.

Creating an ASTRewriter

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)
  }
}

Creating a StringRewriter

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"
  }
}

Using Custom Rewriters

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. :::

Configuring in .herb.yml

Reference 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! :::

API Reference

Functions

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 transform
  • rewriters: 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 context

Returns: Object with:

  • output: The rewritten template string
  • node: 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 parsing
  • template: The HTML+ERB template string to rewrite
  • rewriters: Array of rewriter instances to apply (must be already initialized)
  • options: Optional configuration (same as rewrite())

Returns: The rewritten template string

Base Classes

ASTRewriter

Base 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
}

StringRewriter

Base 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
}

See Also