diff --git a/javascript/packages/formatter/src/formatter.ts b/javascript/packages/formatter/src/formatter.ts index 68062cd65..06b5ce7b9 100644 --- a/javascript/packages/formatter/src/formatter.ts +++ b/javascript/packages/formatter/src/formatter.ts @@ -64,29 +64,27 @@ export class Formatter { const resolvedOptions = resolveFormatOptions({ ...this.options, ...options }) + const context: RewriteContext = { + filePath, + baseDir: process.cwd() + } + + let node = result.value + if (resolvedOptions.preRewriters.length > 0) { - const context: RewriteContext = { - filePath, - baseDir: process.cwd() - } for (const rewriter of resolvedOptions.preRewriters) { try { - result = rewriter.rewrite(result, context) + node = rewriter.rewrite(node, context) } catch (error) { console.error(`Pre-format rewriter "${rewriter.name}" failed:`, error) } } } - let formatted = new FormatPrinter(source, resolvedOptions).print(result.value) + let formatted = new FormatPrinter(source, resolvedOptions).print(node) if (resolvedOptions.postRewriters.length > 0) { - const context: RewriteContext = { - filePath, - baseDir: process.cwd() - } - for (const rewriter of resolvedOptions.postRewriters) { try { formatted = rewriter.rewrite(formatted, context) diff --git a/javascript/packages/formatter/test/rewriters/fixtures/uppercase-tags.js b/javascript/packages/formatter/test/rewriters/fixtures/uppercase-tags.js index 8918029b1..357c8557c 100644 --- a/javascript/packages/formatter/test/rewriters/fixtures/uppercase-tags.js +++ b/javascript/packages/formatter/test/rewriters/fixtures/uppercase-tags.js @@ -43,9 +43,9 @@ export class UppercaseTagsRewriter extends ASTRewriter { // No initialization needed } - rewrite(parseResult, context) { + rewrite(node, context) { const visitor = new UppercaseTagsVisitor() - visitor.visit(parseResult.value) - return parseResult + visitor.visit(node) + return node } } diff --git a/javascript/packages/rewriter/README.md b/javascript/packages/rewriter/README.md index ae03e9683..587fdd0fe 100644 --- a/javascript/packages/rewriter/README.md +++ b/javascript/packages/rewriter/README.md @@ -6,15 +6,6 @@ Rewriter system for transforming HTML+ERB AST nodes and formatted strings. Provides base classes and utilities for creating custom rewriters that can modify templates. -## 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) - ## Installation :::code-group @@ -37,6 +28,61 @@ 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: + +```typescript +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 = `
` +const parseResult = Herb.parse(template, { track_whitespace: true }) + +const { output, node } = await rewrite(parseResult.value, [tailwindClassSorter()]) +// output: "" +// node: The rewritten AST node +``` + +#### `rewriteString()` - Transform Template Strings + +Use `rewriteString()` as a convenience wrapper when working with template strings: + +```typescript +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 = `` + +const output = await rewriteString(Herb, template, [tailwindClassSorter()]) +// output: "" +``` + +**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 @@ -45,18 +91,22 @@ Automatically sorts Tailwind CSS classes in `class` attributes according to Tail **Usage:** ```typescript -import { TailwindClassSorterRewriter } from "@herb-tools/rewriter" +import { Herb } from "@herb-tools/node-wasm" +import { rewriteString } from "@herb-tools/rewriter" +import { tailwindClassSorter } from "@herb-tools/rewriter/loader" -const rewriter = new TailwindClassSorterRewriter() -await rewriter.initialize({ baseDir: process.cwd() }) +await Herb.load() -const result = rewriter.rewrite(parseResult, { baseDir: process.cwd() }) +const template = `` +const output = await rewriteString(Herb, template, [tailwindClassSorter()]) +// output: "" ``` **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:** @@ -78,10 +128,11 @@ You can create custom rewriters to transform templates in any way you need. ### Creating an ASTRewriter -ASTRewriters receive and modify the parsed AST: +ASTRewriters receive and modify AST nodes: ```javascript [.herb/rewriters/my-rewriter.mjs] -import { ASTRewriter, asMutable } from "@herb-tools/rewriter" +import { ASTRewriter } from "@herb-tools/rewriter" +import { Visitor } from "@herb-tools/core" export default class MyASTRewriter extends ASTRewriter { get name() { @@ -98,38 +149,23 @@ export default class MyASTRewriter extends ASTRewriter { // context.filePath - current file being processed (optional) } - // Transform the parsed AST - rewrite(parseResult, context) { - if (parseResult.failed) return parseResult - - // Access and modify the AST - // parseResult.value contains the root AST node + // Transform the AST node + rewrite(node, context) { + // Use the Visitor pattern to traverse and modify the AST + const visitor = new MyVisitor() + visitor.visit(node) - // To mutate readonly properties, use asMutable(): - // const node = asMutable(someNode) - // node.content = "new value" - - return parseResult + // Return the modified node + return node } } -``` - -**Mutating AST Nodes:** - -AST nodes have readonly properties. To modify them, use the `asMutable()` helper: - -```javascript -import { asMutable } from "@herb-tools/rewriter" -import { Visitor } from "@herb-tools/core" class MyVisitor extends Visitor { - visitHTMLAttributeNode(node) { - if (node.value?.children?.[0]?.type === "AST_LITERAL_NODE") { - const literalNode = asMutable(node.value.children[0]) - literalNode.content = "modified" - } + visitHTMLElementNode(node) { + // Modify nodes as needed + // node.someProperty = "new value" - super.visitHTMLAttributeNode(node) + this.visitChildNodes(node) } } ``` @@ -168,13 +204,61 @@ Which means you can just reference and configure them in `.herb.yml` using their ## API Reference -### `ASTRewriter` +### Functions + +#### `rewrite()` + +Transform an AST node using the provided rewriters. + +```typescript +async function rewrite