diff --git a/javascript/packages/formatter/rollup.config.mjs b/javascript/packages/formatter/rollup.config.mjs index b22d7bcba..40d9c1751 100644 --- a/javascript/packages/formatter/rollup.config.mjs +++ b/javascript/packages/formatter/rollup.config.mjs @@ -8,6 +8,8 @@ const external = [ "url", "fs", "module", + "@herb-tools/node-wasm", + "@herb-tools/printer", "@herb-tools/rewriter" ] diff --git a/javascript/packages/formatter/test/cli/__snapshots__/rewriters.test.ts.snap b/javascript/packages/formatter/test/cli/__snapshots__/rewriters.test.ts.snap index ccf53a111..5880bd205 100644 --- a/javascript/packages/formatter/test/cli/__snapshots__/rewriters.test.ts.snap +++ b/javascript/packages/formatter/test/cli/__snapshots__/rewriters.test.ts.snap @@ -5,7 +5,7 @@ exports[`CLI > Rewriters > should actually apply Tailwind class sorting 1`] = ` ⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md  -Using 1 pre-format rewriter: +Using 1 post-format rewriter:  • tailwind-class-sorter (built-in) " @@ -16,7 +16,7 @@ exports[`CLI > Rewriters > should format from stdin with rewriters configured 1` ⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md  -Using 1 pre-format rewriter: +Using 1 post-format rewriter:  • tailwind-class-sorter (built-in) " @@ -32,7 +32,7 @@ exports[`CLI > Rewriters > should handle both pre and post rewriters 1`] = ` ⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md  -Using 1 pre-format rewriter: +Using 1 post-format rewriter:  • tailwind-class-sorter (built-in) " @@ -50,18 +50,18 @@ exports[`CLI > Rewriters > should show multiple rewriters on stderr 1`] = ` ⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md  -Using 1 pre-format rewriter: +Using 1 post-format rewriter:  • tailwind-class-sorter (built-in) " `; -exports[`CLI > Rewriters > should show rewriter info on stderr when pre-format rewriters are configured 1`] = ` +exports[`CLI > Rewriters > should show rewriter info on stderr when post-format rewriters are configured 1`] = ` "✓ Using Herb config file at test-rewriters/.herb.yml ⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md  -Using 1 pre-format rewriter: +Using 1 post-format rewriter:  • tailwind-class-sorter (built-in) " diff --git a/javascript/packages/formatter/test/cli/rewriters.test.ts b/javascript/packages/formatter/test/cli/rewriters.test.ts index 1ec0c9d97..c63f3178e 100644 --- a/javascript/packages/formatter/test/cli/rewriters.test.ts +++ b/javascript/packages/formatter/test/cli/rewriters.test.ts @@ -27,12 +27,12 @@ describe("CLI", () => { await rm(testDir, { recursive: true }).catch(() => {}) }) - it("should show rewriter info on stderr when pre-format rewriters are configured", async () => { + it("should show rewriter info on stderr when post-format rewriters are configured", async () => { const config = dedent` formatter: enabled: true rewriter: - pre: + post: - tailwind-class-sorter ` @@ -52,7 +52,7 @@ describe("CLI", () => { formatter: enabled: true rewriter: - pre: + post: - tailwind-class-sorter ` @@ -72,7 +72,7 @@ describe("CLI", () => { formatter: enabled: true rewriter: - pre: + post: - tailwind-class-sorter ` @@ -97,7 +97,7 @@ describe("CLI", () => { formatter: enabled: true rewriter: - pre: + post: - tailwind-class-sorter ` @@ -153,7 +153,7 @@ describe("CLI", () => { formatter: enabled: true rewriter: - pre: + post: - tailwind-class-sorter ` diff --git a/javascript/packages/formatter/test/rewriters/custom-rewriters.test.ts b/javascript/packages/formatter/test/rewriters/custom-rewriters.test.ts index e81ba5382..caa8357cc 100644 --- a/javascript/packages/formatter/test/rewriters/custom-rewriters.test.ts +++ b/javascript/packages/formatter/test/rewriters/custom-rewriters.test.ts @@ -39,13 +39,15 @@ describe("Formatter with Rewriters Integration", () => { }) test("combines custom rewriter with built-in Tailwind sorter", async () => { - const { preRewriters, postRewriters, preCount, warnings } = await loadRewritersHelper({ + const { preRewriters, postRewriters, preCount, postCount, warnings } = await loadRewritersHelper({ baseDir: process.cwd(), patterns: ["test/rewriters/fixtures/**/*.js"], - pre: ["tailwind-class-sorter", "uppercase-tags"] + pre: ["uppercase-tags"], + post: ["tailwind-class-sorter"] }) - expect(preCount).toBe(2) + expect(preCount).toBe(1) + expect(postCount).toBe(1) expect(warnings).toEqual([]) const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters }) diff --git a/javascript/packages/formatter/test/rewriters/formatter-integration.test.ts b/javascript/packages/formatter/test/rewriters/formatter-integration.test.ts index ebaa6faed..ea33b4b0b 100644 --- a/javascript/packages/formatter/test/rewriters/formatter-integration.test.ts +++ b/javascript/packages/formatter/test/rewriters/formatter-integration.test.ts @@ -36,22 +36,22 @@ describe("Formatter with Rewriters Integration", () => { expect(info.warnings).toEqual([]) }) - test("loadRewriters with Tailwind class sorter", async () => { + test("loadRewriters with Tailwind class sorter as post-rewriter", async () => { const info = await loadRewritersHelper({ baseDir: process.cwd(), - pre: ["tailwind-class-sorter"], - post: [], + pre: [], + post: ["tailwind-class-sorter"], loadCustomRewriters: false }) - expect(info.preCount).toBe(1) - expect(info.postCount).toBe(0) + expect(info.preCount).toBe(0) + expect(info.postCount).toBe(1) }) test("formats with Tailwind class sorter enabled", async () => { const { preRewriters, postRewriters } = await loadRewritersHelper({ baseDir: process.cwd(), - pre: ["tailwind-class-sorter"], + post: ["tailwind-class-sorter"], loadCustomRewriters: false }) @@ -82,32 +82,28 @@ describe("Formatter with Rewriters Integration", () => { test("loadRewriters is idempotent", async () => { const info1 = await loadRewritersHelper({ baseDir: process.cwd(), - pre: ["tailwind-class-sorter"], + post: ["tailwind-class-sorter"], loadCustomRewriters: false }) const info2 = await loadRewritersHelper({ baseDir: process.cwd(), - pre: ["tailwind-class-sorter"], + post: ["tailwind-class-sorter"], loadCustomRewriters: false }) - expect(info1.preCount).toBe(info2.preCount) + expect(info1.postCount).toBe(info2.postCount) }) test("format works with file path parameter", async () => { const { preRewriters, postRewriters } = await loadRewritersHelper({ baseDir: process.cwd(), - pre: ["tailwind-class-sorter"], + post: ["tailwind-class-sorter"], loadCustomRewriters: false }) const formatter = new Formatter(Herb, { indentWidth: 2, preRewriters, postRewriters }) - - const source = dedent` -
- ` - + const source = `
` const result = formatter.format(source, {}, "path/to/file.html.erb") expect(result).toBeDefined() @@ -116,7 +112,7 @@ describe("Formatter with Rewriters Integration", () => { test("continues formatting even if rewriter fails", async () => { const { preRewriters, postRewriters } = await loadRewritersHelper({ baseDir: process.cwd(), - pre: ["tailwind-class-sorter"], + post: ["tailwind-class-sorter"], loadCustomRewriters: false }) @@ -145,7 +141,7 @@ describe("Formatter with Rewriters Integration", () => { test("formats complex ERB with rewriters", async () => { const { preRewriters, postRewriters } = await loadRewritersHelper({ baseDir: process.cwd(), - pre: ["tailwind-class-sorter"], + post: ["tailwind-class-sorter"], loadCustomRewriters: false }) @@ -173,4 +169,93 @@ describe("Formatter with Rewriters Integration", () => { `) }) + + describe("Action View Tag Helper class sorting", () => { + test("sorts classes in tag.div with block", async () => { + const { preRewriters, postRewriters } = await loadRewritersHelper({ + baseDir: process.cwd(), + post: ["tailwind-class-sorter"], + loadCustomRewriters: false + }) + + const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters }) + const source = `<%= tag.div class: "px-4 bg-blue-500 text-white" do %><% end %>` + const result = formatter.format(source) + + expect(result).toBe(`<%= tag.div class: "bg-blue-500 px-4 text-white" do %>\n<% end %>`) + + }) + + test("sorts classes in tag.div with single quotes", async () => { + const { preRewriters, postRewriters } = await loadRewritersHelper({ + baseDir: process.cwd(), + post: ["tailwind-class-sorter"], + loadCustomRewriters: false + }) + + const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters }) + const source = `<%= tag.div class: 'px-4 bg-blue-500 text-white' do %><% end %>` + const result = formatter.format(source) + + expect(result).toBe(`<%= tag.div class: 'bg-blue-500 px-4 text-white' do %>\n<% end %>`) + + }) + + test("sorts classes in content_tag with block", async () => { + const { preRewriters, postRewriters } = await loadRewritersHelper({ + baseDir: process.cwd(), + post: ["tailwind-class-sorter"], + loadCustomRewriters: false + }) + + const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters }) + const source = `<%= content_tag :div, class: "px-4 bg-blue-500 text-white" do %><% end %>` + const result = formatter.format(source) + + expect(result).toBe(`<%= content_tag :div, class: "bg-blue-500 px-4 text-white" do %>\n<% end %>`) + + }) + + test("sorts both HTML and Action View Tag Helper classes together", async () => { + const { preRewriters, postRewriters } = await loadRewritersHelper({ + baseDir: process.cwd(), + post: ["tailwind-class-sorter"], + loadCustomRewriters: false + }) + + const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters }) + + const source = dedent` +
+ <%= tag.div class: "text-white rounded px-2" do %> + Content + <% end %> +
+ ` + + const result = formatter.format(source) + + expect(result).toBe(dedent` +
+ <%= tag.div class: "rounded px-2 text-white" do %> + Content + <% end %> +
+ `) + }) + + test("does not sort dynamic Action View Tag Helper class values", async () => { + const { preRewriters, postRewriters } = await loadRewritersHelper({ + baseDir: process.cwd(), + post: ["tailwind-class-sorter"], + loadCustomRewriters: false + }) + + const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters }) + const source = `<%= tag.div class: dynamic_classes do %><% end %>` + const result = formatter.format(source) + + expect(result).toBe(`<%= tag.div class: dynamic_classes do %>\n<% end %>`) + }) + }) }) diff --git a/javascript/packages/rewriter/rollup.config.mjs b/javascript/packages/rewriter/rollup.config.mjs index cf1aea451..8ac5908c9 100644 --- a/javascript/packages/rewriter/rollup.config.mjs +++ b/javascript/packages/rewriter/rollup.config.mjs @@ -8,6 +8,8 @@ const external = [ "url", "fs", "module", + "@herb-tools/node-wasm", + "@herb-tools/printer", "@herb-tools/tailwind-class-sorter", "tinyglobby" ] diff --git a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts index 1f2946917..dd57a1da3 100644 --- a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts +++ b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts @@ -1,29 +1,37 @@ -import { getStaticAttributeName, isLiteralNode, isPureWhitespaceNode, splitLiteralsAtWhitespace, groupNodesByClass } from "@herb-tools/core" +import { getStaticAttributeName, isLiteralNode, isPureWhitespaceNode, splitLiteralsAtWhitespace, groupNodesByClass, isERBOpenTagNode, isHTMLAttributeNode } from "@herb-tools/core" import { LiteralNode, Location, Visitor } from "@herb-tools/core" +import { Herb } from "@herb-tools/node-wasm" +import { IdentityPrinter } from "@herb-tools/printer" + import { TailwindClassSorter } from "@herb-tools/tailwind-class-sorter" -import { ASTRewriter } from "../ast-rewriter.js" +import { StringRewriter } from "../string-rewriter.js" import { asMutable } from "../mutable.js" import type { RewriteContext } from "../context.js" + +type ClassSplice = { from: string, to: string } + import type { - HTMLAttributeNode, - HTMLAttributeValueNode, - Node, - ERBIfNode, - ERBUnlessNode, - ERBElseNode, + ERBBeginNode, ERBBlockNode, - ERBForNode, - ERBCaseNode, - ERBWhenNode, ERBCaseMatchNode, + ERBCaseNode, + ERBElseNode, + ERBEnsureNode, + ERBForNode, + ERBIfNode, ERBInNode, - ERBWhileNode, - ERBUntilNode, - ERBBeginNode, + ERBOpenTagNode, ERBRescueNode, - ERBEnsureNode + ERBUnlessNode, + ERBUntilNode, + ERBWhenNode, + ERBWhileNode, + HTMLAttributeNode, + HTMLAttributeValueNode, + HTMLElementNode, + Node, } from "@herb-tools/core" /** @@ -322,9 +330,92 @@ class ClassAttributeSorter extends Visitor { } /** - * Built-in rewriter that sorts Tailwind CSS classes in class and className attributes + * Visitor that extracts class attribute splice operations from Action View Tag Helper elements. + * Finds static class attributes and records old/new ERB tag text for string-level replacement. + */ +class ActionViewClassSorterVisitor extends Visitor { + private sorter: TailwindClassSorter + private splices: ClassSplice[] + + constructor(sorter: TailwindClassSorter, splices: ClassSplice[]) { + super() + + this.sorter = sorter + this.splices = splices + } + + visitHTMLElementNode(node: HTMLElementNode): void { + if (node.element_source && isERBOpenTagNode(node.open_tag)) { + this.extractSplice(node) + } + + this.visitChildNodes(node) + } + + private extractSplice(node: HTMLElementNode): void { + const openTag = node.open_tag as ERBOpenTagNode + + if (!openTag.content) return + if (!openTag.tag_opening) return + if (!openTag.tag_closing) return + if (!openTag.children) return + + for (const child of openTag.children) { + if (!isHTMLAttributeNode(child)) continue + if (!child.name || !child.value) continue + + const attributeName = getStaticAttributeName(child.name) + if (attributeName !== "class") continue + + const valueChildren = child.value.children + if (!valueChildren || valueChildren.length === 0) continue + + if (!valueChildren.every(isLiteralNode)) continue + + const classValue = (valueChildren as LiteralNode[]).map(n => n.content).join("") + if (!classValue.trim()) continue + + let sortedValue: string + + try { + sortedValue = this.sorter.sortClasses(classValue) + } catch { + continue + } + + if (sortedValue === classValue) continue + + const oldContent = openTag.content.value + const doubleQuoted = `"${classValue}"` + const singleQuoted = `'${classValue}'` + + let newContent: string + + if (oldContent.includes(doubleQuoted)) { + newContent = oldContent.replace(doubleQuoted, `"${sortedValue}"`) + } else if (oldContent.includes(singleQuoted)) { + newContent = oldContent.replace(singleQuoted, `'${sortedValue}'`) + } else { + continue + } + + const oldTag = openTag.tag_opening.value + oldContent + openTag.tag_closing.value + const newTag = openTag.tag_opening.value + newContent + openTag.tag_closing.value + + this.splices.push({ from: oldTag, to: newTag }) + } + } +} + +/** + * Built-in rewriter that sorts Tailwind CSS classes in class and className attributes. + * + * Operates as a string rewriter with two phases: + * 1. Parse the template normally and sort HTML element class attributes via AST manipulation. + * 2. Parse again with action_view_helpers enabled to locate and sort class attributes in + * Action View Tag Helper expressions (tag.div, content_tag, etc.) via string splicing. */ -export class TailwindClassSorterRewriter extends ASTRewriter { +export class TailwindClassSorterRewriter extends StringRewriter { private sorter?: TailwindClassSorter get name(): string { @@ -357,15 +448,54 @@ export class TailwindClassSorterRewriter extends ASTRewriter { } } - rewrite(node: T, _context: RewriteContext): T { - if (!this.sorter) { - return node + rewrite(formatted: string, _context: RewriteContext): string { + if (!this.sorter) return formatted + + let htmlSorted: string + + try { + const parseResult = Herb.parse(formatted, { track_whitespace: true }) + + if (parseResult.failed) return formatted + + const visitor = new TailwindClassSorterVisitor(this.sorter) + visitor.visit(parseResult.value) + + htmlSorted = IdentityPrinter.print(parseResult.value) + } catch { + return formatted } - const visitor = new TailwindClassSorterVisitor(this.sorter) + return this.sortActionViewHelperClasses(htmlSorted) + } - visitor.visit(node) + private sortActionViewHelperClasses(source: string): string { + let parseResult - return node + try { + parseResult = Herb.parse(source, { + track_whitespace: true, + action_view_helpers: true + }) + } catch { + return source + } + + if (parseResult.failed) return source + + const splices: ClassSplice[] = [] + const visitor = new ActionViewClassSorterVisitor(this.sorter!, splices) + + visitor.visit(parseResult.value) + + if (splices.length === 0) return source + + let result = source + + for (const { from, to } of splices) { + result = result.replaceAll(from, to) + } + + return result } } diff --git a/javascript/packages/rewriter/test/built-ins/tailwind-class-sorter.test.ts b/javascript/packages/rewriter/test/built-ins/tailwind-class-sorter.test.ts index 6368d2990..534606ee0 100644 --- a/javascript/packages/rewriter/test/built-ins/tailwind-class-sorter.test.ts +++ b/javascript/packages/rewriter/test/built-ins/tailwind-class-sorter.test.ts @@ -2,7 +2,6 @@ import dedent from "dedent" import { describe, test, expect, beforeAll } from "vitest" import { Herb } from "@herb-tools/node-wasm" -import { IdentityPrinter } from "@herb-tools/printer" import { TailwindClassSorterRewriter } from "../../src/built-ins/tailwind-class-sorter.js" import { createRewriterTest } from "../helpers/rewriter-test-helper.js" @@ -29,29 +28,24 @@ describe("tailwind-class-sorter", () => { await expect(rewriter.initialize({ baseDir: process.cwd() })).resolves.not.toThrow() }) - test("returns original AST when sorter not initialized", async () => { + test("returns original string when sorter not initialized", async () => { const input = dedent`
` const rewriter = new TailwindClassSorterRewriter() + const output = rewriter.rewrite(input, { baseDir: process.cwd() }) - const parseResult = Herb.parse(input, { track_whitespace: true }) - const document = rewriter.rewrite(parseResult.value, { baseDir: process.cwd() }) - - expect(document).toBe(parseResult.value) - - const output = IdentityPrinter.print(document) expect(output).toBe(input) }) - test("returns rewritten AST for inspection", async () => { - const document = await expectTransform( + test("returns rewritten string", async () => { + const result = await expectTransform( `
`, `
` ) - expect(document.type).toBe("AST_DOCUMENT_NODE") + expect(result).toBeDefined() }) }) @@ -589,4 +583,76 @@ describe("tailwind-class-sorter", () => { await expectNoTransform(`
`) }) }) + + describe("Action View Tag Helper class attributes", () => { + describe("tag.div", () => { + test("sorts classes in tag.div with block (double quotes)", async () => { + await expectTransform( + `<%= tag.div class: "px-4 bg-blue-500 text-white" do %><% end %>`, + `<%= tag.div class: "bg-blue-500 px-4 text-white" do %><% end %>` + ) + }) + + test("sorts classes in tag.div with block (single quotes)", async () => { + await expectTransform( + `<%= tag.div class: 'px-4 bg-blue-500 text-white' do %><% end %>`, + `<%= tag.div class: 'bg-blue-500 px-4 text-white' do %><% end %>` + ) + }) + + test("sorts classes in inline tag.div (no block)", async () => { + await expectTransform( + `<%= tag.div class: "px-4 bg-blue-500 text-white" %>`, + `<%= tag.div class: "bg-blue-500 px-4 text-white" %>` + ) + }) + + test("does not change already sorted classes", async () => { + await expectNoTransform( + `<%= tag.div class: "bg-blue-500 px-4 text-white" do %><% end %>` + ) + }) + + test("does not sort dynamic class values (RubyLiteralNode)", async () => { + await expectNoTransform( + `<%= tag.div class: dynamic_classes do %><% end %>` + ) + }) + + test("does not sort class_names() calls", async () => { + await expectNoTransform( + `<%= tag.div class: class_names("px-4 bg-blue-500") do %><% end %>` + ) + }) + + test("sorts multiple attributes, only touches class", async () => { + await expectTransform( + `<%= tag.div id: "main", class: "px-4 bg-blue-500" do %><% end %>`, + `<%= tag.div id: "main", class: "bg-blue-500 px-4" do %><% end %>` + ) + }) + }) + + describe("content_tag", () => { + test("sorts classes in content_tag with block", async () => { + await expectTransform( + `<%= content_tag :div, class: "px-4 bg-blue-500 text-white" do %><% end %>`, + `<%= content_tag :div, class: "bg-blue-500 px-4 text-white" do %><% end %>` + ) + }) + + test("sorts classes in inline content_tag (no content argument)", async () => { + await expectTransform( + `<%= content_tag :div, class: "px-4 bg-blue-500" %>`, + `<%= content_tag :div, class: "bg-blue-500 px-4" %>` + ) + }) + + test("does not sort dynamic class values", async () => { + await expectNoTransform( + `<%= content_tag :div, class: some_classes do %><% end %>` + ) + }) + }) + }) }) diff --git a/javascript/packages/rewriter/test/helpers/rewriter-test-helper.ts b/javascript/packages/rewriter/test/helpers/rewriter-test-helper.ts index d03dce7a7..f58c60a3a 100644 --- a/javascript/packages/rewriter/test/helpers/rewriter-test-helper.ts +++ b/javascript/packages/rewriter/test/helpers/rewriter-test-helper.ts @@ -3,28 +3,36 @@ import { beforeAll, expect } from "vitest" import { Herb } from "@herb-tools/node-wasm" import { IdentityPrinter } from "@herb-tools/printer" -import type { ASTRewriter, RewriteContext } from "../../src/index.js" +import { ASTRewriter } from "../../src/ast-rewriter.js" +import { StringRewriter } from "../../src/string-rewriter.js" + +import type { RewriteContext } from "../../src/index.js" import type { Node } from "@herb-tools/core" +import type { ParseOptions } from "@herb-tools/core" interface RewriterTestOptions { context?: RewriteContext + parseOptions?: Partial } interface RewriterTestHelpers { - expectTransform: (input: string, expected: string, options?: RewriterTestOptions) => Promise - expectNoTransform: (input: string, options?: RewriterTestOptions) => Promise + expectTransform: (input: string, expected: string, options?: RewriterTestOptions) => Promise + expectNoTransform: (input: string, options?: RewriterTestOptions) => Promise } /** * Creates a test helper for rewriters that reduces boilerplate in tests. * + * Supports both ASTRewriter (parses input and rewrites the AST) and + * StringRewriter (passes the input string directly to the rewriter). + * * @param RewriterClass - The rewriter class to test * @returns Object with helper functions for testing */ export function createRewriterTest( - RewriterClass: new () => ASTRewriter + RewriterClass: new () => ASTRewriter | StringRewriter ): RewriterTestHelpers { - let rewriter: ASTRewriter + let rewriter: ASTRewriter | StringRewriter beforeAll(async () => { await Herb.load() @@ -36,9 +44,18 @@ export function createRewriterTest( input: string, expected: string, options?: RewriterTestOptions - ): Promise => { + ): Promise => { const context = options?.context ?? { baseDir: process.cwd() } - const parseResult = Herb.parse(input, { track_whitespace: true }) + + if (rewriter instanceof StringRewriter) { + const output = rewriter.rewrite(input, context) + + expect(output).toBe(expected) + + return output + } + + const parseResult = Herb.parse(input, { track_whitespace: true, ...options?.parseOptions }) if (parseResult.failed) { throw new Error( @@ -48,7 +65,7 @@ export function createRewriterTest( ) } - const node = rewriter.rewrite(parseResult.value, context) + const node = (rewriter as ASTRewriter).rewrite(parseResult.value, context) const output = IdentityPrinter.print(node) expect(output).toBe(expected) @@ -59,7 +76,7 @@ export function createRewriterTest( const expectNoTransform = async ( input: string, options?: RewriterTestOptions - ): Promise => { + ): Promise => { return await expectTransform(input, input, options) }