This skill should be used when authoring OpenRewrite recipes in TypeScript for automated code transformations. Covers recipe structure, visitor patterns, pattern matching, templates, testing strategies, and troubleshooting.
/plugin marketplace add openrewrite/rewrite-docs/plugin install openrewrite-openrewrite-recipe-openrewrite-recipe-writer-2@openrewrite/rewrite-docsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/template-basic-recipe.tsassets/template-pattern-rewrite.tsassets/template-recipe-test.tsassets/template-recipe-with-options.tsexamples/manual-bind-to-arrow-simple.tsexamples/manual-bind-to-arrow.tsexamples/react-bind-to-arrow-working.tsexamples/react-manual-bind-to-arrow.tsreferences/checklist-recipe-development.mdreferences/common-patterns.mdreferences/examples.mdreferences/lst-concepts.mdreferences/patterns-and-templates.mdreferences/testing-recipes.mdreferences/type-attribution-guide.mdDo NOT use this skill for:
writing-openrewrite-recipes skill insteadThis skill includes supporting files organized by purpose:
assets/)Starting points for recipe development:
Load when: Creating a new recipe or needing a template to start from
references/)Detailed reference documentation:
Load when: Deep dive into specific concepts or troubleshooting
references/)Ready-to-use code:
Load when: Needing to see a complete example or looking for a specific pattern
references/)Verification guide:
Load when: Reviewing a recipe for completeness or ensuring best practices
Important: The OpenRewrite JavaScript/TypeScript API is designed specifically for TypeScript. While it can transform JavaScript code, recipe authoring should be done in TypeScript to leverage:
npm install @openrewrite/rewrite@next # Latest features
npm install --save-dev typescript @types/node immer @jest/globals jest
{
"compilerOptions": {
"target": "es2016",
"module": "Node16", // Required for ESM
"moduleResolution": "node16",
"strict": true,
"experimentalDecorators": true // Required for @Option decorator
}
}
Follow this checklist when creating recipes:
Recipename, displayName, description properties@Option fields if configuration needededitor() method returning a visitorJavaScriptVisitorrewrite() helper with tryOn() methodproduce() from immer for immutable updatesproduceAsync() from @openrewrite/rewriteRecipeSpec and rewriteRun()activate() function (see Recipe Registration)import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
import {JavaScriptVisitor} from "@openrewrite/rewrite/javascript";
import {J} from "@openrewrite/rewrite/java";
export class MyRecipe extends Recipe {
name = "org.openrewrite.javascript.MyRecipe";
displayName = "My Recipe";
description = "What this recipe does.";
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
return new class extends JavaScriptVisitor<ExecutionContext> {
protected async visitMethodInvocation(
method: J.MethodInvocation,
ctx: ExecutionContext
): Promise<J | undefined> {
// Transform or return unchanged
return method;
}
}
}
}
import {Option} from "@openrewrite/rewrite";
export class ConfigurableRecipe extends Recipe {
@Option({
displayName: "Method name",
description: "The method to rename",
example: "oldMethod"
})
methodName!: string;
constructor(options?: { methodName?: string }) {
super(options);
this.methodName ??= 'defaultMethod';
}
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
const methodName = this.methodName; // Capture for closure
return new class extends JavaScriptVisitor<ExecutionContext> {
// Use captured methodName
}
}
}
LST preserves everything about source code. Key wrapper types:
J.RightPadded<T> - Element with trailing space/commentsJ.LeftPadded<T> - Element with leading space/commentsJ.Container<T> - Delimited lists// Always unwrap elements
const selectExpr = method.select.element; // Unwrap RightPadded
const firstArg = method.arguments.elements[0].element; // Unwrap Container
š See references/lst-concepts.md for comprehensive details.
Use patterns for declarative transformations:
import {capture, pattern, template} from "@openrewrite/rewrite/javascript";
const args = capture({ variadic: true });
const pat = pattern`oldApi.method(${args})`; // Lenient type checking by default
const match = await pat.match(node, this.cursor);
if (match) {
return await template`newApi.methodAsync(${args})`
.apply(node, this.cursor, { values: match });
}
ā ļø Template Construction Rule: Templates must produce syntactically valid JavaScript/TypeScript code. Template parameters become placeholders, so surrounding syntax must be complete. For example, template\function f() { ${method.body!.statements} }`works because braces are included, buttemplate`function f() ${method.body}`` fails because it would generate invalid code.
š See references/patterns-and-templates.md (section "How Template Construction Works") for complete details on the two-phase template construction process.
Configure patterns for strict type checking, type attribution, or debugging:
const tmpl = template`isDate(${capture('value')})`
.configure({
lenientTypeMatching: false, // Override default lenient type matching
context: ['import { isDate } from "date-utils"'],
dependencies: {'date-utils': '^2.0.0'},
debug: true // Enable debug logging globally, or pass { debug: true } to individual match() calls
});
šÆ Semantic Matching: When patterns are configured with context and dependencies, they use type-based semantic matching instead of syntax-only matching. This means a single pattern like pattern\repl.REPLServer()`can automatically matchrepl.REPLServer(), REPLServer(), and new REPLServer()` - regardless of import style - because they all resolve to the same type.
š See references/patterns-and-templates.md for complete guide including semantic matching examples.
rewrite() Helper (Simple Pattern-to-Template Transformations)ā RECOMMENDED for simple substitutions: When you need to replace one subtree with another, use rewrite() + tryOn() - this is the cleanest and most declarative approach:
import {rewrite, capture, pattern, template} from "@openrewrite/rewrite/javascript";
// Define transformation rule
const rule = rewrite(() => {
const args = capture({ variadic: true });
return {
before: pattern`oldApi.method(${args})`,
after: template`newApi.methodAsync(${args})`
};
});
// In visitor method
protected async visitMethodInvocation(
method: J.MethodInvocation,
ctx: ExecutionContext
): Promise<J | undefined> {
// Try to apply the rule - returns transformed node or undefined
return await rule.tryOn(this.cursor, method) || method;
}
When rewrite() works well:
undefined if no match, making fallback easyorElse() and andThen()When to use pattern/template directly instead:
Example - Complex logic requiring direct pattern/template use:
protected async visitMethodInvocation(
method: J.MethodInvocation,
ctx: ExecutionContext
): Promise<J | undefined> {
const methodName = capture<J.Identifier>('method');
const args = capture({ variadic: true });
const pat = pattern`api.${methodName}(${args})`;
const match = await pat.match(method, this.cursor);
if (!match) return method;
const nameNode = match.get(methodName);
if (!isIdentifier(nameNode)) return method;
// Complex conditional logic based on captured values
let tmpl;
if (nameNode.simpleName.startsWith('get')) {
tmpl = template`newApi.${methodName}Sync(${args})`;
} else if (nameNode.simpleName.startsWith('set')) {
tmpl = template`newApi.${methodName}Async(${args}, callback)`;
} else {
// Don't transform this case
return method;
}
return await tmpl.apply(method, this.cursor, { values: match });
}
Trade-off: rewrite() is more declarative but less flexible. For complex transformations, the procedural approach with direct pattern/template usage offers full control.
Important: template (and by extension rewrite()) automatically formats the generated code according to OpenRewrite's formatting rules. This means:
Return value semantics:
tryOn() returns the transformed node if pattern matchestryOn() returns undefined if pattern doesn't match|| node to fall back to original when no matchComposing rules:
// Try multiple transformations
const combined = rule1.orElse(rule2).orElse(rule3);
return await combined.tryOn(this.cursor, method) || method;
// Sequential transformations
const pipeline = rule1.andThen(rule2);
return await pipeline.tryOn(this.cursor, method) || method;
Override specific methods in JavaScriptVisitor:
class MyVisitor extends JavaScriptVisitor<ExecutionContext> {
// Common visitor methods:
visitJsCompilationUnit() // Root file
visitMethodInvocation() // Method calls
visitMethodDeclaration() // Function declarations
visitIdentifier() // Identifiers
visitLiteral() // Literals
visitBinary() // Binary operations
visitVariableDeclarations() // Variable declarations
visitArrowFunction() // Arrow functions
visitClassDeclaration() // Classes
// JSX/TSX visitor methods:
visitJsxTag() // JSX elements: <Component>...</Component>
visitJsxAttribute() // JSX attributes: key="value"
visitJsxSpreadAttribute() // JSX spread: {...props}
visitJsxEmbeddedExpression() // JSX expressions: {value}
}
protected async visitMethodInvocation(
method: J.MethodInvocation,
ctx: ExecutionContext
): Promise<J | undefined> {
// ā
DEFAULT: Visit children first by calling super
method = await super.visitMethodInvocation(method, ctx) as J.MethodInvocation;
// Then apply transformations
if (shouldTransform(method)) {
return transform(method);
}
return method;
}
Why call super first: Most recipes need bottom-up transformation - children are visited before parents. This is the safest default pattern.
When to skip super:
// Example: Skip super when replacing entire node
protected async visitMethodInvocation(
method: J.MethodInvocation,
ctx: ExecutionContext
): Promise<J | undefined> {
if (shouldCompletelyReplace(method)) {
// Don't call super - we're replacing the whole thing
return await template`newExpression()`.apply(method, this.cursor);
}
// For other cases, visit children first
return await super.visitMethodInvocation(method, ctx);
}
// ā ļø CRITICAL: Wrapper types need unwrapping!
const selectExpr = method.select.element; // ā
Use .element to unwrap RightPadded
const firstArg = method.arguments.elements[0].element; // ā
Unwrap from Container
// ā WRONG - Accessing wrapper directly causes type errors
const selectExpr = method.select; // This is J.RightPadded<Expression>, not Expression!
Troubleshooting: If you see "Property X does not exist on type RightPadded<Y>", you forgot to unwrap with .element.
import {isMethodInvocation} from "@openrewrite/rewrite/java";
if (!isMethodInvocation(node)) {
return node; // Return unchanged if wrong type
}
// Now TypeScript knows node is J.MethodInvocation
return produce(node, draft => {
draft.name = newName;
});
if (shouldDelete) {
return undefined;
}
const cursor = this.cursor;
const parent = cursor.parent?.value; // Direct parent
const parentTree = cursor.parentTree()?.value; // Parent skipping wrappers
const enclosing = cursor.firstEnclosing(isMethodDeclaration);
autoFormat(node, ctx, cursor) - Format entire filemaybeAutoFormat(before, after, ctx, cursor) - Format if changedUtility functions for managing imports:
maybeAddImport(visitor, options) - Add import if missingmaybeRemoveImport(visitor, module, member?) - Remove unused importprotected async visitJsCompilationUnit(
cu: JS.CompilationUnit,
ctx: ExecutionContext
): Promise<J | undefined> {
// Add import: import { debounce } from "lodash"
maybeAddImport(this, { module: "lodash", member: "debounce" });
// Add default import: import React from "react"
maybeAddImport(this, { module: "react", member: "default", alias: "React" });
// Add namespace import: import * as fs from "fs"
maybeAddImport(this, { module: "fs", member: "*", alias: "fs" });
// Add side-effect import: import "polyfills"
maybeAddImport(this, { module: "polyfills", sideEffectOnly: true });
// Remove import
maybeRemoveImport(this, "old-lib", "oldFn");
return cu;
}
ā ļø Known Limitation: Direct ES6 import statement transformations can be challenging due to complex AST structure. Prefer using maybeAddImport/maybeRemoveImport or transforming import usage instead of the import statement itself.
š See references/common-patterns.md (Pattern 7) for CommonJS require() transformations and ES6 import workarounds.
import {RecipeSpec} from "@openrewrite/rewrite/test";
import {javascript, typescript, jsx, tsx} from "@openrewrite/rewrite/javascript";
test("transforms code", () => {
const spec = new RecipeSpec();
spec.recipe = new MyRecipe();
return spec.rewriteRun(
javascript(
`const x = oldPattern();`, // before
`const x = newPattern();` // after
)
);
});
ā ļø Important: rewriteRun() checks the output for exact formatting match, including all whitespace, indentation, and newlines. Tests will fail if the transformation produces semantically correct but differently formatted code.
Common test failures:
Tip: If tests fail due to formatting, check:
template (which auto-formats) or manual AST constructionmaybeAutoFormat() should be applied after transformationImportant: Always test cases where your recipe should NOT make changes. This ensures your recipe doesn't transform unrelated code.
test("does not transform unrelated code", () => {
const spec = new RecipeSpec();
spec.recipe = new MyRecipe();
return spec.rewriteRun(
// Single argument = no change expected
javascript(`const x = unrelatedPattern();`)
);
});
Pattern:
javascript(before, after) - Expects transformationjavascript(code) - Expects NO change (code stays the same)Example - Testing both positive and negative cases:
test("transforms only target pattern", () => {
const spec = new RecipeSpec();
spec.recipe = new RenameMethodRecipe({ oldName: "oldMethod", newName: "newMethod" });
return spec.rewriteRun(
// Should transform
javascript(
`obj.oldMethod();`,
`obj.newMethod();`
),
// Should NOT transform - different method name
javascript(`obj.differentMethod();`),
// Should NOT transform - different context
javascript(`const oldMethod = 'string';`)
);
});
Best practice: Include multiple no-change test cases to verify your recipe's specificity and avoid false positives.
Use npm with packageJson for type attribution:
import {npm, packageJson, typescript} from "@openrewrite/rewrite/javascript";
import {withDir} from 'tmp-promise';
test("with dependencies", async () => {
await withDir(async (tmpDir) => {
const sources = npm(
tmpDir.path, // Temp directory for clean tests
packageJson(JSON.stringify({
dependencies: {
"lodash": "^4.17.21"
},
devDependencies: {
"@types/lodash": "^4.14.195"
}
})),
typescript(
`import _ from "lodash";
_.debounce(fn, 100);`,
`import { debounce } from "lodash";
debounce(fn, 100);`
)
);
// Convert async generator
const sourcesArray = [];
for await (const source of sources) {
sourcesArray.push(source);
}
return spec.rewriteRun(...sourcesArray);
}, {unsafeCleanup: true});
});
š See references/testing-recipes.md for advanced testing.
editor() returns a visitorQuick debugging steps:
any() for parts to ignoreDebug Logging (Recommended):
When pattern matches fail unexpectedly, enable debug logging to see exactly why:
const args = capture({ variadic: true });
const pat = pattern`oldApi.method(${args})`;
// Option 1: Enable debug globally for all matches
const patWithDebug = pat.configure({ debug: true });
const match = await patWithDebug.match(node, cursor);
// Option 2: Enable debug for a single match() call
const match2 = await pat.match(node, cursor, { debug: true });
// If match fails, debug logs show:
// - Which AST node caused the mismatch
// - The exact path through the AST where it failed
// - Expected vs actual values at the failure point
// - Backtracking attempts for variadic captures
Debug output example:
[Pattern #1] foo(${args}, 999)
[Pattern #1] ā FAILED matching against J$MethodInvocation:
[Pattern #1] foo(1, 2, 3, 42)
[Pattern #1] At path: [J$MethodInvocation#arguments ā 3]
[Pattern #1] Reason: structural-mismatch
[Pattern #1] Expected: 999
[Pattern #1] Actual: 42
What debug logs reveal:
[J$MethodInvocation#arguments ā 3] means the 4th argument)Common mismatch reasons:
structural-mismatch - Values differ (e.g., different method names, different literal values)kind-mismatch - AST node types don't match (e.g., expecting Identifier but got Literal)value-mismatch - Property values don't matchconstraint-failed - Capture constraint returned falsearray-length-mismatch - Container lengths differ (when no variadic captures present)Tip: Debug logs are especially useful for:
// ā Wrong - no runtime validation
const x = capture<J.Literal>();
// ā
Correct - with constraint
const x = capture<J.Literal>({
constraint: (n) => isLiteral(n)
});
// ā Wrong - reassigning draft
return produce(node, draft => {
draft = someOtherNode; // Won't work
});
// ā
Correct - modify properties
return produce(node, draft => {
draft.name = newName;
});
Error: Property 'simpleName' does not exist on type 'RightPadded<Expression>'
Cause: You forgot to unwrap the wrapper type with .element
// ā Wrong - accessing wrapper directly
const name = method.select.simpleName; // method.select is RightPadded<Expression>!
// ā
Correct - unwrap first
const selectExpr = method.select.element; // Now it's Expression
if (isIdentifier(selectExpr)) {
const name = selectExpr.simpleName; // ā
Works
}
Common unwrapping patterns:
method.select.element - Unwrap RightPaddedmethod.arguments.elements[0].element - Unwrap from Containerbinary.operator.element - Unwrap LeftPaddedIssue: Tests fail even though transformation is semantically correct
Cause: rewriteRun() checks for exact formatting match, including whitespace
// ā Test fails - formatting mismatch
javascript(
`const x=1;`, // before (no spaces)
`const x = 1;` // after (spaces added)
)
// If transformation preserves original formatting, test will fail
// ā
Test passes - expected output matches actual formatting
javascript(
`const x=1;`, // before
`const x=1;` // after (preserves original spacing)
)
Solutions:
template which auto-formats consistentlymaybeAutoFormat() to normalize formatting after transformationš See references/common-patterns.md for 18 ready-to-use patterns including:
š See references/examples.md for 9 complete recipes including:
// Core
import {Recipe, TreeVisitor, ExecutionContext} from "@openrewrite/rewrite";
// Java AST and type guards
import {J, isIdentifier, isLiteral} from "@openrewrite/rewrite/java";
// JavaScript/TypeScript
import {JavaScriptVisitor, capture, pattern, template} from "@openrewrite/rewrite/javascript";
import {JSX} from "@openrewrite/rewrite/javascript"; // For JSX/TSX transformations
import {maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
// Testing
import {RecipeSpec} from "@openrewrite/rewrite/test";
import {javascript, typescript, jsx, tsx, npm, packageJson} from "@openrewrite/rewrite/javascript";
// Recipe Registration
import {RecipeRegistry} from "@openrewrite/rewrite";
To make recipes discoverable by OpenRewrite, export an activate() function from the package entry point (typically index.ts). This function receives a RecipeRegistry and registers recipe classes with it.
import { RecipeRegistry } from '@openrewrite/rewrite';
import { MyRecipe } from './my-recipe';
import { AnotherRecipe } from './another-recipe';
export async function activate(registry: RecipeRegistry): Promise<void> {
registry.register(MyRecipe);
registry.register(AnotherRecipe);
}
// Also export recipe classes for direct use
export { MyRecipe } from './my-recipe';
export { AnotherRecipe } from './another-recipe';
Pass the class, not an instance: registry.register(MyRecipe) not registry.register(new MyRecipe())
Recipes must be instantiable without arguments: The registry creates a temporary instance to read the recipe's name property. Recipes with required options (no defaults) cannot be registered this way.
// ā
Can be registered - no required options
export class MyRecipe extends Recipe {
name = "org.example.MyRecipe";
// ...
}
// ā
Can be registered - has default value
export class ConfigurableRecipe extends Recipe {
@Option({ displayName: "Target", description: "..." })
target!: string;
constructor(options?: { target?: string }) {
super(options);
this.target ??= 'default'; // Default allows no-arg construction
}
}
// ā Cannot be registered - required option with no default
export class RequiredOptionRecipe extends Recipe {
@Option({ displayName: "Required", description: "..." })
required!: string;
constructor(options: { required: string }) { // No default!
super(options);
}
}
activate() function should be async and return Promise<void>.// src/index.ts
import { RecipeRegistry } from '@openrewrite/rewrite';
// Re-export all recipes for direct import
export { MigrateApiCalls } from './migrate-api-calls';
export { UpdateImports } from './update-imports';
export { FindDeprecatedUsage } from './find-deprecated-usage';
// Import for registration
import { MigrateApiCalls } from './migrate-api-calls';
import { UpdateImports } from './update-imports';
// FindDeprecatedUsage not imported - has required options
/**
* Register all recipes that can be instantiated without arguments.
*/
export async function activate(registry: RecipeRegistry): Promise<void> {
registry.register(MigrateApiCalls);
registry.register(UpdateImports);
// FindDeprecatedUsage omitted - requires options
}
rewrite() for simple pattern-to-template substitutions (most declarative)pattern/template directly for complex conditional logic or procedural transformationssuper.visitX() first (default) - Ensures children are visited before parent transformations; skip only when you have a specific reason.element to access actual nodes from RightPadded/Container/LeftPaddedconstraint for runtimeCreating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.