Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions javascript/packages/rewriter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ await Herb.load()
const template = `<div class="text-red-500 p-4 mt-2"></div>`
const parseResult = Herb.parse(template, { track_whitespace: true })

const { output, node } = await rewrite(parseResult.value, [tailwindClassSorter()])
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
```
Expand All @@ -77,7 +78,8 @@ await Herb.load()

const template = `<div class="text-red-500 p-4 mt-2"></div>`

const output = await rewriteString(Herb, template, [tailwindClassSorter()])
const sorter = await tailwindClassSorter()
const output = rewriteString(Herb, template, [sorter])
// output: "<div class="mt-2 p-4 text-red-500"></div>"
```

Expand All @@ -98,7 +100,8 @@ 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 output = await rewriteString(Herb, template, [tailwindClassSorter()])
const sorter = await tailwindClassSorter()
const output = rewriteString(Herb, template, [sorter])
// output: "<div class="rounded bg-blue-500 px-4 py-2 text-white"></div>"
```

Expand Down Expand Up @@ -211,16 +214,16 @@ Which means you can just reference and configure them in `.herb.yml` using their
Transform an AST node using the provided rewriters.

```typescript
async function rewrite<T extends Node>(
function rewrite<T extends Node>(
node: T,
rewriters: Rewriter[],
options?: RewriteOptions
): Promise<RewriteResult>
): RewriteResult
```

**Parameters:**
- `node`: The AST node to transform
- `rewriters`: Array of rewriter instances to apply
- `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
Expand All @@ -234,18 +237,18 @@ async function rewrite<T extends Node>(
Convenience wrapper around `rewrite()` that parses the template string first and returns just the output string.

```typescript
async function rewriteString(
function rewriteString(
herb: HerbBackend,
template: string,
rewriters: Rewriter[],
options?: RewriteOptions
): Promise<string>
): string
```

**Parameters:**
- `herb`: The Herb backend instance for parsing
- `template`: The HTML+ERB template string to rewrite
- `rewriters`: Array of rewriter instances to apply
- `rewriters`: Array of rewriter instances to apply (must be already initialized)
- `options`: Optional configuration (same as `rewrite()`)

**Returns:** The rewritten template string
Expand Down
14 changes: 5 additions & 9 deletions javascript/packages/rewriter/src/rewrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,19 @@ export interface RewriteResult {
*
* const template = '<div class="text-red-500 p-4 mt-2"></div>'
* const parseResult = Herb.parse(template)
* const { output, document } = await rewrite(parseResult.value, [tailwindClassSorter()])
* const { output, node } = rewrite(parseResult.value, [tailwindClassSorter()])
* ```
*
* @param node - The AST Node to rewrite
* @param rewriters - Array of rewriter instances to apply
* @param options - Optional configuration for the rewrite operation
* @returns Object containing the rewritten string and Node
*/
export async function rewrite<T extends Node>(node: T, rewriters: Rewriter[], options: RewriteOptions = {}): Promise<RewriteResult & { node: T }> {
export function rewrite<T extends Node>(node: T, rewriters: Rewriter[], options: RewriteOptions = {}): RewriteResult & { node: T } {
const { baseDir = process.cwd(), filePath } = options

const context: RewriteContext = { baseDir, filePath }

for (const rewriter of rewriters) {
await rewriter.initialize(context)
}

let currentNode = node

const astRewriters = rewriters.filter(rewriter => rewriter instanceof ASTRewriter)
Expand Down Expand Up @@ -109,7 +105,7 @@ export async function rewrite<T extends Node>(node: T, rewriters: Rewriter[], op
* await Herb.load()
*
* const template = '<div class="text-red-500 p-4 mt-2"></div>'
* const output = await rewriteString(Herb, template, [tailwindClassSorter()])
* const output = rewriteString(Herb, template, [tailwindClassSorter()])
* // output: '<div class="mt-2 p-4 text-red-500"></div>'
* ```
*
Expand All @@ -119,14 +115,14 @@ export async function rewrite<T extends Node>(node: T, rewriters: Rewriter[], op
* @param options - Optional configuration for the rewrite operation
* @returns The rewritten template string
*/
export async function rewriteString(herb: HerbBackend, template: string, rewriters: Rewriter[], options: RewriteOptions = {}): Promise<string> {
export function rewriteString(herb: HerbBackend, template: string, rewriters: Rewriter[], options: RewriteOptions = {}): string {
const parseResult = herb.parse(template, { track_whitespace: true })

if (parseResult.failed) {
return template
}

const { output } = await rewrite(
const { output } = rewrite(
parseResult.value,
rewriters,
options
Expand Down
18 changes: 8 additions & 10 deletions javascript/packages/rewriter/src/rewriter-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,27 @@ export interface TailwindClassSorterOptions {
/**
* Factory function for creating a Tailwind class sorter rewriter
*
* Automatically initializes the rewriter before returning it.
*
* @example
* ```typescript
* import { rewrite } from '@herb-tools/rewriter'
* import { tailwindClassSorter } from '@herb-tools/rewriter/loader'
*
* const template = '<div class="text-red-500 p-4 mt-2"></div>'
* const result = await rewrite(template, [tailwindClassSorter()])
* const sorter = await tailwindClassSorter()
* const result = rewrite(template, [sorter])
* // Result: '<div class="mt-2 p-4 text-red-500"></div>'
* ```
*
* @param options - Optional configuration for the Tailwind class sorter
* @returns A configured TailwindClassSorterRewriter instance
* @returns A configured and initialized TailwindClassSorterRewriter instance
*/
export function tailwindClassSorter(options: TailwindClassSorterOptions = {}): TailwindClassSorterRewriter {
export async function tailwindClassSorter(options: TailwindClassSorterOptions = {}): Promise<TailwindClassSorterRewriter> {
const rewriter = new TailwindClassSorterRewriter()
const baseDir = options.baseDir || process.cwd()

if (options.baseDir) {
const originalInitialize = rewriter.initialize.bind(rewriter)

rewriter.initialize = async (context) => {
return originalInitialize({ ...context, baseDir: options.baseDir || context.baseDir })
}
}
await rewriter.initialize({ baseDir })

return rewriter
}
65 changes: 34 additions & 31 deletions javascript/packages/rewriter/test/rewrite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,32 @@ import { Herb } from "@herb-tools/node-wasm"
import { rewrite, rewriteString, tailwindClassSorter } from "../src/loader.js"

describe("rewrite", () => {
let sorter: Awaited<ReturnType<typeof tailwindClassSorter>>

beforeAll(async () => {
await Herb.load()
sorter = await tailwindClassSorter()
})

describe("basic usage", () => {
test("rewrites Node with tailwindClassSorter", async () => {
test("rewrites Node with tailwindClassSorter", () => {
const template = '<div class="text-red-500 p-4 mt-2"></div>'
const parseResult = Herb.parse(template, { track_whitespace: true })
const { output, node } = await rewrite(parseResult.value, [tailwindClassSorter()])
const { output, node } = rewrite(parseResult.value, [sorter])

expect(output).toBe('<div class="mt-2 p-4 text-red-500"></div>')
expect(node.type).toBe("AST_DOCUMENT_NODE")
})

test("handles empty rewriters array", async () => {
test("handles empty rewriters array", () => {
const template = '<div class="text-red-500 p-4 mt-2"></div>'
const parseResult = Herb.parse(template, { track_whitespace: true })
const { output } = await rewrite(parseResult.value, [])
const { output } = rewrite(parseResult.value, [])

expect(output).toBe(template)
})

test("handles multiple elements", async () => {
test("handles multiple elements", () => {
const template = dedent`
<div class="px-4 bg-blue-500">
<span class="text-white font-bold"></span>
Expand All @@ -40,81 +43,81 @@ describe("rewrite", () => {
`

const parseResult = Herb.parse(template, { track_whitespace: true })
const { output } = await rewrite(parseResult.value, [tailwindClassSorter()])
const { output } = rewrite(parseResult.value, [sorter])

expect(output).toBe(expected)
})
})

describe("rewriteString", () => {
test("rewrites string with tailwindClassSorter", async () => {
test("rewrites string with tailwindClassSorter", () => {
const template = '<div class="text-red-500 p-4 mt-2"></div>'
const output = await rewriteString(Herb, template, [tailwindClassSorter()])
const output = rewriteString(Herb, template, [sorter])

expect(output).toBe('<div class="mt-2 p-4 text-red-500"></div>')
})

test("handles empty rewriters array", async () => {
test("handles empty rewriters array", () => {
const template = '<div class="text-red-500 p-4 mt-2"></div>'
const output = await rewriteString(Herb, template, [])
const output = rewriteString(Herb, template, [])

expect(output).toBe(template)
})

test("returns original template on parse failure", async () => {
test("returns original template on parse failure", () => {
const template = '<div class="unclosed'
const output = await rewriteString(Herb, template, [tailwindClassSorter()])
const output = rewriteString(Herb, template, [sorter])

expect(output).toBe(template)
})
})

describe("options", () => {
test("accepts baseDir option", async () => {
test("accepts baseDir option", () => {
const template = '<div class="text-red-500 p-4 mt-2"></div>'
const parseResult = Herb.parse(template, { track_whitespace: true })
const { output } = await rewrite(parseResult.value, [tailwindClassSorter()], {
const { output } = rewrite(parseResult.value, [sorter], {
baseDir: "/custom/path"
})

expect(output).toBe('<div class="mt-2 p-4 text-red-500"></div>')
})

test("accepts filePath option", async () => {
test("accepts filePath option", () => {
const template = '<div class="text-red-500 p-4 mt-2"></div>'
const parseResult = Herb.parse(template, { track_whitespace: true })
const { output } = await rewrite(parseResult.value, [tailwindClassSorter()], {
const { output } = rewrite(parseResult.value, [sorter], {
filePath: "/custom/path/file.html.erb"
})

expect(output).toBe('<div class="mt-2 p-4 text-red-500"></div>')
})

test("uses process.cwd() as default baseDir", async () => {
test("uses process.cwd() as default baseDir", () => {
const template = '<div class="text-red-500 p-4 mt-2"></div>'
const parseResult = Herb.parse(template, { track_whitespace: true })
const { output } = await rewrite(parseResult.value, [tailwindClassSorter()])
const { output } = rewrite(parseResult.value, [sorter])

expect(output).toBe('<div class="mt-2 p-4 text-red-500"></div>')
})
})

describe("complex templates", () => {
test("handles ERB expressions", async () => {
test("handles ERB expressions", () => {
const template = '<div class="px-4 <%= extra_classes %> bg-blue-500"></div>'
const output = await rewriteString(Herb, template, [tailwindClassSorter()])
const output = rewriteString(Herb, template, [sorter])

expect(output).toBe('<div class="bg-blue-500 px-4 <%= extra_classes %>"></div>')
})

test("handles ERB conditionals", async () => {
test("handles ERB conditionals", () => {
const template = '<div class="px-4 <% if admin? %> text-red-500 font-bold <% end %> bg-blue-500"></div>'
const output = await rewriteString(Herb, template, [tailwindClassSorter()])
const output = rewriteString(Herb, template, [sorter])

expect(output).toBe('<div class="bg-blue-500 px-4 <% if admin? %> font-bold text-red-500 <% end %>"></div>')
})

test("handles multiline templates", async () => {
test("handles multiline templates", () => {
const template = dedent`
<div class="px-4
<% if valid? %>
Expand All @@ -134,45 +137,45 @@ describe("rewrite", () => {
</div>
`

const output = await rewriteString(Herb, template, [tailwindClassSorter()])
const output = rewriteString(Herb, template, [sorter])

expect(output).toBe(expected)
})
})

describe("error handling", () => {
test("continues processing on rewriter error", async () => {
test("continues processing on rewriter error", () => {
const template = '<div class="text-red-500 p-4"></div>'

const errorRewriter = tailwindClassSorter()
const errorRewriter = sorter
errorRewriter.rewrite.bind(errorRewriter)

errorRewriter.rewrite = () => {
throw new Error("Test error")
}

const parseResult = Herb.parse(template, { track_whitespace: true })
const { output } = await rewrite(parseResult.value, [errorRewriter])
const { output } = rewrite(parseResult.value, [errorRewriter])

expect(output).toBe(template)
})
})

describe("tailwindClassSorter factory", () => {
test("creates rewriter with default options", () => {
const rewriter = tailwindClassSorter()
const rewriter = sorter

expect(rewriter.name).toBe("tailwind-class-sorter")
})

test("creates rewriter with custom baseDir", () => {
const rewriter = tailwindClassSorter({ baseDir: "/custom/path" })
test("creates rewriter with custom baseDir", async () => {
const rewriter = await tailwindClassSorter({ baseDir: "/custom/path" })

expect(rewriter.name).toBe("tailwind-class-sorter")
})

test("rewriter has initialize method", async () => {
const rewriter = tailwindClassSorter()
const rewriter = sorter

await expect(
rewriter.initialize({ baseDir: process.cwd() })
Expand Down
Loading