@herb-tools/rewriter 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ import type { Node } from "@herb-tools/core";
2
+ import type { RewriteContext } from "./context.js";
3
+ /**
4
+ * Base class for AST rewriters that transform AST nodes before formatting
5
+ *
6
+ * AST rewriters receive a Node and can mutate it in place or return a modified Node.
7
+ * They run before the formatting step.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { ASTRewriter, asMutable } from "@herb-tools/rewriter"
12
+ * import { Visitor } from "@herb-tools/core"
13
+ *
14
+ * class MyRewriter extends ASTRewriter {
15
+ * get name() { return "my-rewriter" }
16
+ * get description() { return "My custom AST rewriter" }
17
+ *
18
+ * async initialize(context) {
19
+ * // Load config, initialize dependencies, etc.
20
+ * }
21
+ *
22
+ * rewrite(node, context) {
23
+ * // Use visitor pattern to traverse and modify AST
24
+ * const visitor = new MyVisitor()
25
+ * visitor.visit(node)
26
+ *
27
+ * return node
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ export declare abstract class ASTRewriter {
33
+ /**
34
+ * Unique identifier for this rewriter
35
+ * Used in configuration and error messages
36
+ */
37
+ abstract get name(): string;
38
+ /**
39
+ * Human-readable description of what this rewriter does
40
+ */
41
+ abstract get description(): string;
42
+ /**
43
+ * Optional async initialization hook
44
+ *
45
+ * Called once before the first rewrite operation. Use this to:
46
+ * - Load configuration files
47
+ * - Initialize dependencies
48
+ * - Perform expensive setup operations
49
+ *
50
+ * @param context - Context with baseDir and optional filePath
51
+ */
52
+ initialize(_context: RewriteContext): Promise<void>;
53
+ /**
54
+ * Transform the AST node
55
+ *
56
+ * This method is called synchronously for each file being formatted.
57
+ * Modify the AST in place or return a new Node.
58
+ *
59
+ * @param node - The AST node from @herb-tools/core
60
+ * @param context - Context with filePath and baseDir
61
+ * @returns The modified Node (can be the same object mutated in place)
62
+ */
63
+ abstract rewrite<T extends Node>(node: T, context: RewriteContext): T;
64
+ }
@@ -0,0 +1,14 @@
1
+ import type { RewriterClass } from "../type-guards.js";
2
+ export { TailwindClassSorterRewriter } from "./tailwind-class-sorter.js";
3
+ /**
4
+ * All built-in rewriters available in the package
5
+ */
6
+ export declare const builtinRewriters: RewriterClass[];
7
+ /**
8
+ * Get a built-in rewriter by name
9
+ */
10
+ export declare function getBuiltinRewriter(name: string): RewriterClass | undefined;
11
+ /**
12
+ * Get all built-in rewriter names
13
+ */
14
+ export declare function getBuiltinRewriterNames(): string[];
@@ -0,0 +1,13 @@
1
+ import { ASTRewriter } from "../ast-rewriter.js";
2
+ import type { RewriteContext } from "../context.js";
3
+ import type { Node } from "@herb-tools/core";
4
+ /**
5
+ * Built-in rewriter that sorts Tailwind CSS classes in class and className attributes
6
+ */
7
+ export declare class TailwindClassSorterRewriter extends ASTRewriter {
8
+ private sorter?;
9
+ get name(): string;
10
+ get description(): string;
11
+ initialize(context: RewriteContext): Promise<void>;
12
+ rewrite<T extends Node>(node: T, _context: RewriteContext): T;
13
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Context passed to rewriters during initialization and rewriting
3
+ *
4
+ * Provides information about the current file and project being processed
5
+ */
6
+ export interface RewriteContext {
7
+ /**
8
+ * Path to the file being rewritten (if available)
9
+ */
10
+ filePath?: string;
11
+ /**
12
+ * Base directory of the project
13
+ */
14
+ baseDir: string;
15
+ /**
16
+ * Additional context data that can be added by the framework
17
+ */
18
+ [key: string]: any;
19
+ }
@@ -0,0 +1,67 @@
1
+ import type { RewriterClass } from "./type-guards.js";
2
+ export interface CustomRewriterLoaderOptions {
3
+ /**
4
+ * Base directory to search for custom rewriters
5
+ * Defaults to current working directory
6
+ */
7
+ baseDir?: string;
8
+ /**
9
+ * Glob patterns to search for custom rewriter files
10
+ * Defaults to looking in .herb/rewriters/
11
+ */
12
+ patterns?: string[];
13
+ /**
14
+ * Whether to suppress errors when loading custom rewriters
15
+ * Defaults to false
16
+ */
17
+ silent?: boolean;
18
+ }
19
+ /**
20
+ * Loads custom rewriters from the user's project
21
+ *
22
+ * Auto-discovers rewriter files in `.herb/rewriters/` by default
23
+ * and dynamically imports them for use in the formatter.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const loader = new CustomRewriterLoader({ baseDir: process.cwd() })
28
+ * const customRewriters = await loader.loadRewriters()
29
+ * ```
30
+ */
31
+ export declare class CustomRewriterLoader {
32
+ private baseDir;
33
+ private patterns;
34
+ private silent;
35
+ constructor(options?: CustomRewriterLoaderOptions);
36
+ /**
37
+ * Discovers custom rewriter files in the project
38
+ */
39
+ discoverRewriterFiles(): Promise<string[]>;
40
+ /**
41
+ * Loads a single rewriter file
42
+ */
43
+ loadRewriterFile(filePath: string): Promise<RewriterClass[]>;
44
+ /**
45
+ * Loads all custom rewriters from the project
46
+ */
47
+ loadRewriters(): Promise<RewriterClass[]>;
48
+ /**
49
+ * Loads all custom rewriters and returns detailed information about each rewriter
50
+ */
51
+ loadRewritersWithInfo(): Promise<{
52
+ rewriters: RewriterClass[];
53
+ rewriterInfo: Array<{
54
+ name: string;
55
+ path: string;
56
+ }>;
57
+ duplicateWarnings: string[];
58
+ }>;
59
+ /**
60
+ * Static helper to check if custom rewriters exist in a project
61
+ */
62
+ static hasCustomRewriters(baseDir?: string): Promise<boolean>;
63
+ /**
64
+ * Static helper to load custom rewriters and merge with built-in rewriters
65
+ */
66
+ static loadAndMergeRewriters(builtinRewriters: RewriterClass[], options?: CustomRewriterLoaderOptions): Promise<RewriterClass[]>;
67
+ }
@@ -0,0 +1,10 @@
1
+ export { ASTRewriter } from "./ast-rewriter.js";
2
+ export { StringRewriter } from "./string-rewriter.js";
3
+ export { asMutable } from "./mutable.js";
4
+ export { isASTRewriterClass, isStringRewriterClass, isRewriterClass } from "./type-guards.js";
5
+ export { rewrite, rewriteString } from "./rewrite.js";
6
+ export type { RewriteContext } from "./context.js";
7
+ export type { Mutable } from "./mutable.js";
8
+ export type { RewriterClass } from "./type-guards.js";
9
+ export type { Rewriter, RewriteOptions, RewriteResult } from "./rewrite.js";
10
+ export type { TailwindClassSorterOptions } from "./rewriter-factories.js";
@@ -0,0 +1,6 @@
1
+ export * from "./index.js";
2
+ export { CustomRewriterLoader } from "./custom-rewriter-loader.js";
3
+ export type { CustomRewriterLoaderOptions } from "./custom-rewriter-loader.js";
4
+ export { TailwindClassSorterRewriter } from "./built-ins/index.js";
5
+ export { tailwindClassSorter } from "./rewriter-factories.js";
6
+ export { builtinRewriters, getBuiltinRewriter, getBuiltinRewriterNames } from "./built-ins/index.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Utility type for making readonly properties mutable
3
+ *
4
+ * This is useful when you need to modify AST nodes which typically have
5
+ * readonly properties. Use sparingly and only in rewriter contexts.
6
+ */
7
+ export type Mutable<T> = T extends ReadonlyArray<infer U> ? Array<Mutable<U>> : T extends object ? {
8
+ -readonly [K in keyof T]: Mutable<T[K]>;
9
+ } : T;
10
+ /**
11
+ * Cast a readonly value to a mutable version
12
+ *
13
+ * @example
14
+ * const literalNode = asMutable(node)
15
+ * literalNode.content = "new value"
16
+ */
17
+ export declare function asMutable<T>(node: T): Mutable<T>;
@@ -0,0 +1,77 @@
1
+ import { ASTRewriter } from "./ast-rewriter.js";
2
+ import { StringRewriter } from "./string-rewriter.js";
3
+ import type { HerbBackend, Node } from "@herb-tools/core";
4
+ export type Rewriter = ASTRewriter | StringRewriter;
5
+ export interface RewriteOptions {
6
+ /**
7
+ * Base directory for resolving configuration files
8
+ * Defaults to process.cwd()
9
+ */
10
+ baseDir?: string;
11
+ /**
12
+ * Optional file path for context
13
+ */
14
+ filePath?: string;
15
+ }
16
+ export interface RewriteResult {
17
+ /**
18
+ * The rewritten template string
19
+ */
20
+ output: string;
21
+ /**
22
+ * The rewritten AST node
23
+ */
24
+ node: Node;
25
+ }
26
+ /**
27
+ * Rewrite an AST Node using the provided rewriters
28
+ *
29
+ * This is the main rewrite function that operates on AST nodes.
30
+ * For string input, use `rewriteString()` instead.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { Herb } from '@herb-tools/node-wasm'
35
+ * import { rewrite } from '@herb-tools/rewriter'
36
+ * import { tailwindClassSorter } from '@herb-tools/rewriter/loader'
37
+ *
38
+ * await Herb.load()
39
+ *
40
+ * const template = '<div class="text-red-500 p-4 mt-2"></div>'
41
+ * const parseResult = Herb.parse(template)
42
+ * const { output, node } = rewrite(parseResult.value, [tailwindClassSorter()])
43
+ * ```
44
+ *
45
+ * @param node - The AST Node to rewrite
46
+ * @param rewriters - Array of rewriter instances to apply
47
+ * @param options - Optional configuration for the rewrite operation
48
+ * @returns Object containing the rewritten string and Node
49
+ */
50
+ export declare function rewrite<T extends Node>(node: T, rewriters: Rewriter[], options?: RewriteOptions): RewriteResult & {
51
+ node: T;
52
+ };
53
+ /**
54
+ * Rewrite an HTML+ERB template string
55
+ *
56
+ * Convenience wrapper around `rewrite()` that parses the string first.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * import { Herb } from '@herb-tools/node-wasm'
61
+ * import { rewriteString } from '@herb-tools/rewriter'
62
+ * import { tailwindClassSorter } from '@herb-tools/rewriter/loader'
63
+ *
64
+ * await Herb.load()
65
+ *
66
+ * const template = '<div class="text-red-500 p-4 mt-2"></div>'
67
+ * const output = rewriteString(Herb, template, [tailwindClassSorter()])
68
+ * // output: '<div class="mt-2 p-4 text-red-500"></div>'
69
+ * ```
70
+ *
71
+ * @param herb - The Herb backend instance for parsing
72
+ * @param template - The HTML+ERB template string to rewrite
73
+ * @param rewriters - Array of rewriter instances to apply
74
+ * @param options - Optional configuration for the rewrite operation
75
+ * @returns The rewritten template string
76
+ */
77
+ export declare function rewriteString(herb: HerbBackend, template: string, rewriters: Rewriter[], options?: RewriteOptions): string;
@@ -0,0 +1,28 @@
1
+ import { TailwindClassSorterRewriter } from "./built-ins/tailwind-class-sorter.js";
2
+ export interface TailwindClassSorterOptions {
3
+ /**
4
+ * Base directory for resolving Tailwind configuration
5
+ * Defaults to process.cwd()
6
+ */
7
+ baseDir?: string;
8
+ }
9
+ /**
10
+ * Factory function for creating a Tailwind class sorter rewriter
11
+ *
12
+ * Automatically initializes the rewriter before returning it.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { rewrite } from '@herb-tools/rewriter'
17
+ * import { tailwindClassSorter } from '@herb-tools/rewriter/loader'
18
+ *
19
+ * const template = '<div class="text-red-500 p-4 mt-2"></div>'
20
+ * const sorter = await tailwindClassSorter()
21
+ * const result = rewrite(template, [sorter])
22
+ * // Result: '<div class="mt-2 p-4 text-red-500"></div>'
23
+ * ```
24
+ *
25
+ * @param options - Optional configuration for the Tailwind class sorter
26
+ * @returns A configured and initialized TailwindClassSorterRewriter instance
27
+ */
28
+ export declare function tailwindClassSorter(options?: TailwindClassSorterOptions): Promise<TailwindClassSorterRewriter>;
@@ -0,0 +1,54 @@
1
+ import type { RewriteContext } from "./context.js";
2
+ /**
3
+ * Base class for string rewriters that transform the formatted output
4
+ *
5
+ * String rewriters receive the formatted string and can modify it before
6
+ * returning the final output. They run after the formatting step.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { StringRewriter } from "@herb-tools/rewriter"
11
+ *
12
+ * class AddTrailingNewline extends StringRewriter {
13
+ * get name() { return "add-trailing-newline" }
14
+ * get description() { return "Ensures file ends with a newline" }
15
+ *
16
+ * rewrite(formatted, context) {
17
+ * return formatted.endsWith("\n") ? formatted : formatted + "\n"
18
+ * }
19
+ * }
20
+ * ```
21
+ */
22
+ export declare abstract class StringRewriter {
23
+ /**
24
+ * Unique identifier for this rewriter
25
+ * Used in configuration and error messages
26
+ */
27
+ abstract get name(): string;
28
+ /**
29
+ * Human-readable description of what this rewriter does
30
+ */
31
+ abstract get description(): string;
32
+ /**
33
+ * Optional async initialization hook
34
+ *
35
+ * Called once before the first rewrite operation. Use this to:
36
+ * - Load configuration files
37
+ * - Initialize dependencies
38
+ * - Perform expensive setup operations
39
+ *
40
+ * @param context - Context with baseDir and optional filePath
41
+ */
42
+ initialize(_context: RewriteContext): Promise<void>;
43
+ /**
44
+ * Transform the formatted string
45
+ *
46
+ * This method is called synchronously for each file being formatted.
47
+ * Return the modified string.
48
+ *
49
+ * @param formatted - The formatted string output from the formatter
50
+ * @param context - Context with filePath and baseDir
51
+ * @returns The modified string
52
+ */
53
+ abstract rewrite(formatted: string, context: RewriteContext): string;
54
+ }
@@ -0,0 +1,20 @@
1
+ import { ASTRewriter } from "./ast-rewriter.js";
2
+ import { StringRewriter } from "./string-rewriter.js";
3
+ /**
4
+ * Type guard to check if a class is an ASTRewriter
5
+ * Uses duck typing to work across module boundaries
6
+ */
7
+ export declare function isASTRewriterClass(obj: any): obj is new () => ASTRewriter;
8
+ /**
9
+ * Type guard to check if a class is a StringRewriter
10
+ * Uses duck typing to work across module boundaries
11
+ */
12
+ export declare function isStringRewriterClass(obj: any): obj is new () => StringRewriter;
13
+ /**
14
+ * Union type for all rewriter classes
15
+ */
16
+ export type RewriterClass = (new () => ASTRewriter) | (new () => StringRewriter);
17
+ /**
18
+ * Type guard to check if a class is any kind of rewriter
19
+ */
20
+ export declare function isRewriterClass(obj: any): obj is RewriterClass;
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@herb-tools/rewriter",
3
+ "version": "0.8.0",
4
+ "description": "Rewriter system for transforming HTML+ERB AST nodes and formatted strings",
5
+ "license": "MIT",
6
+ "homepage": "https://herb-tools.dev",
7
+ "bugs": "https://github.com/marcoroth/herb/issues/new?title=Package%20%60@herb-tools/rewriter%60:%20",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/marcoroth/herb.git",
11
+ "directory": "javascript/packages/rewriter"
12
+ },
13
+ "main": "./dist/index.cjs",
14
+ "module": "./dist/index.esm.js",
15
+ "require": "./dist/index.cjs",
16
+ "types": "./dist/types/index.d.ts",
17
+ "scripts": {
18
+ "build": "yarn clean && rollup -c rollup.config.mjs",
19
+ "dev": "rollup -c rollup.config.mjs -w",
20
+ "clean": "rimraf dist",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest --watch",
23
+ "prepublishOnly": "yarn clean && yarn build && yarn test"
24
+ },
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ ".": {
28
+ "types": "./dist/types/index.d.ts",
29
+ "import": "./dist/index.esm.js",
30
+ "require": "./dist/index.cjs",
31
+ "default": "./dist/index.esm.js"
32
+ },
33
+ "./loader": {
34
+ "types": "./dist/types/loader.d.ts",
35
+ "import": "./dist/loader.esm.js",
36
+ "require": "./dist/loader.cjs",
37
+ "default": "./dist/loader.esm.js"
38
+ }
39
+ },
40
+ "dependencies": {
41
+ "@herb-tools/core": "0.8.0",
42
+ "@herb-tools/tailwind-class-sorter": "0.8.0",
43
+ "glob": "^11.0.3"
44
+ },
45
+ "devDependencies": {
46
+ "@herb-tools/printer": "0.8.0"
47
+ },
48
+ "files": [
49
+ "package.json",
50
+ "README.md",
51
+ "dist/",
52
+ "src/"
53
+ ]
54
+ }
@@ -0,0 +1,70 @@
1
+ import type { Node } from "@herb-tools/core"
2
+ import type { RewriteContext } from "./context.js"
3
+
4
+ /**
5
+ * Base class for AST rewriters that transform AST nodes before formatting
6
+ *
7
+ * AST rewriters receive a Node and can mutate it in place or return a modified Node.
8
+ * They run before the formatting step.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { ASTRewriter, asMutable } from "@herb-tools/rewriter"
13
+ * import { Visitor } from "@herb-tools/core"
14
+ *
15
+ * class MyRewriter extends ASTRewriter {
16
+ * get name() { return "my-rewriter" }
17
+ * get description() { return "My custom AST rewriter" }
18
+ *
19
+ * async initialize(context) {
20
+ * // Load config, initialize dependencies, etc.
21
+ * }
22
+ *
23
+ * rewrite(node, context) {
24
+ * // Use visitor pattern to traverse and modify AST
25
+ * const visitor = new MyVisitor()
26
+ * visitor.visit(node)
27
+ *
28
+ * return node
29
+ * }
30
+ * }
31
+ * ```
32
+ */
33
+ export abstract class ASTRewriter {
34
+ /**
35
+ * Unique identifier for this rewriter
36
+ * Used in configuration and error messages
37
+ */
38
+ abstract get name(): string
39
+
40
+ /**
41
+ * Human-readable description of what this rewriter does
42
+ */
43
+ abstract get description(): string
44
+
45
+ /**
46
+ * Optional async initialization hook
47
+ *
48
+ * Called once before the first rewrite operation. Use this to:
49
+ * - Load configuration files
50
+ * - Initialize dependencies
51
+ * - Perform expensive setup operations
52
+ *
53
+ * @param context - Context with baseDir and optional filePath
54
+ */
55
+ async initialize(_context: RewriteContext): Promise<void> {
56
+ // Override in subclass if needed
57
+ }
58
+
59
+ /**
60
+ * Transform the AST node
61
+ *
62
+ * This method is called synchronously for each file being formatted.
63
+ * Modify the AST in place or return a new Node.
64
+ *
65
+ * @param node - The AST node from @herb-tools/core
66
+ * @param context - Context with filePath and baseDir
67
+ * @returns The modified Node (can be the same object mutated in place)
68
+ */
69
+ abstract rewrite<T extends Node>(node: T, context: RewriteContext): T
70
+ }
@@ -0,0 +1,33 @@
1
+ import type { RewriterClass } from "../type-guards.js"
2
+
3
+ export { TailwindClassSorterRewriter } from "./tailwind-class-sorter.js"
4
+ import { TailwindClassSorterRewriter } from "./tailwind-class-sorter.js"
5
+
6
+ /**
7
+ * All built-in rewriters available in the package
8
+ */
9
+ export const builtinRewriters: RewriterClass[] = [
10
+ TailwindClassSorterRewriter
11
+ ]
12
+
13
+ /**
14
+ * Get a built-in rewriter by name
15
+ */
16
+ export function getBuiltinRewriter(name: string): RewriterClass | undefined {
17
+ return builtinRewriters.find(RewriterClass => {
18
+ const instance = new RewriterClass()
19
+
20
+ return instance.name === name
21
+ })
22
+ }
23
+
24
+ /**
25
+ * Get all built-in rewriter names
26
+ */
27
+ export function getBuiltinRewriterNames(): string[] {
28
+ return builtinRewriters.map(RewriterClass => {
29
+ const instance = new RewriterClass()
30
+
31
+ return instance.name
32
+ })
33
+ }