@openrewrite/recipes-react 0.2.9

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.
Files changed (251) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +130 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/migration/change-component-prop-value.d.ts +20 -0
  6. package/dist/migration/change-component-prop-value.d.ts.map +1 -0
  7. package/dist/migration/change-component-prop-value.js +217 -0
  8. package/dist/migration/change-component-prop-value.js.map +1 -0
  9. package/dist/react-native/view-prop-types.d.ts +8 -0
  10. package/dist/react-native/view-prop-types.d.ts.map +1 -0
  11. package/dist/react-native/view-prop-types.js +65 -0
  12. package/dist/react-native/view-prop-types.js.map +1 -0
  13. package/dist/react16/error-boundaries.d.ts +8 -0
  14. package/dist/react16/error-boundaries.d.ts.map +1 -0
  15. package/dist/react16/error-boundaries.js +42 -0
  16. package/dist/react16/error-boundaries.js.map +1 -0
  17. package/dist/react16/find-dom-node.d.ts +8 -0
  18. package/dist/react16/find-dom-node.d.ts.map +1 -0
  19. package/dist/react16/find-dom-node.js +48 -0
  20. package/dist/react16/find-dom-node.js.map +1 -0
  21. package/dist/react16/react-dom-factories.d.ts +8 -0
  22. package/dist/react16/react-dom-factories.d.ts.map +1 -0
  23. package/dist/react16/react-dom-factories.js +72 -0
  24. package/dist/react16/react-dom-factories.js.map +1 -0
  25. package/dist/react16/react-prop-types.d.ts +8 -0
  26. package/dist/react16/react-prop-types.d.ts.map +1 -0
  27. package/dist/react16/react-prop-types.js +69 -0
  28. package/dist/react16/react-prop-types.js.map +1 -0
  29. package/dist/react16/react-to-react-dom.d.ts +8 -0
  30. package/dist/react16/react-to-react-dom.d.ts.map +1 -0
  31. package/dist/react16/react-to-react-dom.js +97 -0
  32. package/dist/react16/react-to-react-dom.js.map +1 -0
  33. package/dist/react16/replace-create-factory.d.ts +8 -0
  34. package/dist/react16/replace-create-factory.d.ts.map +1 -0
  35. package/dist/react16/replace-create-factory.js +69 -0
  36. package/dist/react16/replace-create-factory.js.map +1 -0
  37. package/dist/react16/upgrade-to-react-16.d.ts +8 -0
  38. package/dist/react16/upgrade-to-react-16.d.ts.map +1 -0
  39. package/dist/react16/upgrade-to-react-16.js +41 -0
  40. package/dist/react16/upgrade-to-react-16.js.map +1 -0
  41. package/dist/react17/remove-event-persist.d.ts +8 -0
  42. package/dist/react17/remove-event-persist.d.ts.map +1 -0
  43. package/dist/react17/remove-event-persist.js +114 -0
  44. package/dist/react17/remove-event-persist.js.map +1 -0
  45. package/dist/react17/rename-unsafe-lifecycles.d.ts +8 -0
  46. package/dist/react17/rename-unsafe-lifecycles.d.ts.map +1 -0
  47. package/dist/react17/rename-unsafe-lifecycles.js +48 -0
  48. package/dist/react17/rename-unsafe-lifecycles.js.map +1 -0
  49. package/dist/react17/update-react-imports.d.ts +8 -0
  50. package/dist/react17/update-react-imports.d.ts.map +1 -0
  51. package/dist/react17/update-react-imports.js +40 -0
  52. package/dist/react17/update-react-imports.js.map +1 -0
  53. package/dist/react17/upgrade-to-react-17.d.ts +8 -0
  54. package/dist/react17/upgrade-to-react-17.d.ts.map +1 -0
  55. package/dist/react17/upgrade-to-react-17.js +37 -0
  56. package/dist/react17/upgrade-to-react-17.js.map +1 -0
  57. package/dist/react18/remove-unstable-batched-updates.d.ts +8 -0
  58. package/dist/react18/remove-unstable-batched-updates.d.ts.map +1 -0
  59. package/dist/react18/remove-unstable-batched-updates.js +170 -0
  60. package/dist/react18/remove-unstable-batched-updates.js.map +1 -0
  61. package/dist/react18/replace-reactdom-render.d.ts +8 -0
  62. package/dist/react18/replace-reactdom-render.d.ts.map +1 -0
  63. package/dist/react18/replace-reactdom-render.js +55 -0
  64. package/dist/react18/replace-reactdom-render.js.map +1 -0
  65. package/dist/react18/replace-render-callback.d.ts +8 -0
  66. package/dist/react18/replace-render-callback.d.ts.map +1 -0
  67. package/dist/react18/replace-render-callback.js +60 -0
  68. package/dist/react18/replace-render-callback.js.map +1 -0
  69. package/dist/react18/replace-unmount-component-at-node.d.ts +8 -0
  70. package/dist/react18/replace-unmount-component-at-node.d.ts.map +1 -0
  71. package/dist/react18/replace-unmount-component-at-node.js +54 -0
  72. package/dist/react18/replace-unmount-component-at-node.js.map +1 -0
  73. package/dist/react18/upgrade-to-react-18.d.ts +8 -0
  74. package/dist/react18/upgrade-to-react-18.d.ts.map +1 -0
  75. package/dist/react18/upgrade-to-react-18.js +39 -0
  76. package/dist/react18/upgrade-to-react-18.js.map +1 -0
  77. package/dist/react19/deprecated-react-types.d.ts +8 -0
  78. package/dist/react19/deprecated-react-types.d.ts.map +1 -0
  79. package/dist/react19/deprecated-react-types.js +135 -0
  80. package/dist/react19/deprecated-react-types.js.map +1 -0
  81. package/dist/react19/find-context-consumer.d.ts +9 -0
  82. package/dist/react19/find-context-consumer.d.ts.map +1 -0
  83. package/dist/react19/find-context-consumer.js +128 -0
  84. package/dist/react19/find-context-consumer.js.map +1 -0
  85. package/dist/react19/find-deprecated-reactdom-apis.d.ts +9 -0
  86. package/dist/react19/find-deprecated-reactdom-apis.d.ts.map +1 -0
  87. package/dist/react19/find-deprecated-reactdom-apis.js +132 -0
  88. package/dist/react19/find-deprecated-reactdom-apis.js.map +1 -0
  89. package/dist/react19/find-element-ref.d.ts +9 -0
  90. package/dist/react19/find-element-ref.d.ts.map +1 -0
  91. package/dist/react19/find-element-ref.js +88 -0
  92. package/dist/react19/find-element-ref.js.map +1 -0
  93. package/dist/react19/find-legacy-context-api.d.ts +9 -0
  94. package/dist/react19/find-legacy-context-api.d.ts.map +1 -0
  95. package/dist/react19/find-legacy-context-api.js +163 -0
  96. package/dist/react19/find-legacy-context-api.js.map +1 -0
  97. package/dist/react19/no-implicit-ref-callback-return.d.ts +8 -0
  98. package/dist/react19/no-implicit-ref-callback-return.d.ts.map +1 -0
  99. package/dist/react19/no-implicit-ref-callback-return.js +107 -0
  100. package/dist/react19/no-implicit-ref-callback-return.js.map +1 -0
  101. package/dist/react19/remove-context-provider.d.ts +8 -0
  102. package/dist/react19/remove-context-provider.d.ts.map +1 -0
  103. package/dist/react19/remove-context-provider.js +59 -0
  104. package/dist/react19/remove-context-provider.js.map +1 -0
  105. package/dist/react19/remove-forward-ref.d.ts +8 -0
  106. package/dist/react19/remove-forward-ref.d.ts.map +1 -0
  107. package/dist/react19/remove-forward-ref.js +73 -0
  108. package/dist/react19/remove-forward-ref.js.map +1 -0
  109. package/dist/react19/remove-prop-types.d.ts +8 -0
  110. package/dist/react19/remove-prop-types.d.ts.map +1 -0
  111. package/dist/react19/remove-prop-types.js +76 -0
  112. package/dist/react19/remove-prop-types.js.map +1 -0
  113. package/dist/react19/remove-react-fc.d.ts +8 -0
  114. package/dist/react19/remove-react-fc.d.ts.map +1 -0
  115. package/dist/react19/remove-react-fc.js +149 -0
  116. package/dist/react19/remove-react-fc.js.map +1 -0
  117. package/dist/react19/replace-act-import.d.ts +9 -0
  118. package/dist/react19/replace-act-import.d.ts.map +1 -0
  119. package/dist/react19/replace-act-import.js +34 -0
  120. package/dist/react19/replace-act-import.js.map +1 -0
  121. package/dist/react19/replace-default-props.d.ts +8 -0
  122. package/dist/react19/replace-default-props.d.ts.map +1 -0
  123. package/dist/react19/replace-default-props.js +195 -0
  124. package/dist/react19/replace-default-props.js.map +1 -0
  125. package/dist/react19/replace-react-shallow-renderer.d.ts +8 -0
  126. package/dist/react19/replace-react-shallow-renderer.d.ts.map +1 -0
  127. package/dist/react19/replace-react-shallow-renderer.js +69 -0
  128. package/dist/react19/replace-react-shallow-renderer.js.map +1 -0
  129. package/dist/react19/replace-reactdom-hydrate.d.ts +8 -0
  130. package/dist/react19/replace-reactdom-hydrate.d.ts.map +1 -0
  131. package/dist/react19/replace-reactdom-hydrate.js +55 -0
  132. package/dist/react19/replace-reactdom-hydrate.js.map +1 -0
  133. package/dist/react19/replace-string-ref.d.ts +8 -0
  134. package/dist/react19/replace-string-ref.d.ts.map +1 -0
  135. package/dist/react19/replace-string-ref.js +75 -0
  136. package/dist/react19/replace-string-ref.js.map +1 -0
  137. package/dist/react19/replace-use-form-state.d.ts +8 -0
  138. package/dist/react19/replace-use-form-state.d.ts.map +1 -0
  139. package/dist/react19/replace-use-form-state.js +54 -0
  140. package/dist/react19/replace-use-form-state.js.map +1 -0
  141. package/dist/react19/upgrade-to-react-19.d.ts +8 -0
  142. package/dist/react19/upgrade-to-react-19.d.ts.map +1 -0
  143. package/dist/react19/upgrade-to-react-19.js +59 -0
  144. package/dist/react19/upgrade-to-react-19.js.map +1 -0
  145. package/dist/react19/use-context-hook.d.ts +8 -0
  146. package/dist/react19/use-context-hook.d.ts.map +1 -0
  147. package/dist/react19/use-context-hook.js +54 -0
  148. package/dist/react19/use-context-hook.js.map +1 -0
  149. package/dist/react19/use-ref-required-initial.d.ts +8 -0
  150. package/dist/react19/use-ref-required-initial.d.ts.map +1 -0
  151. package/dist/react19/use-ref-required-initial.js +74 -0
  152. package/dist/react19/use-ref-required-initial.js.map +1 -0
  153. package/dist/refactoring/class-to-functional.d.ts +8 -0
  154. package/dist/refactoring/class-to-functional.d.ts.map +1 -0
  155. package/dist/refactoring/class-to-functional.js +205 -0
  156. package/dist/refactoring/class-to-functional.js.map +1 -0
  157. package/dist/refactoring/create-class-to-es6.d.ts +8 -0
  158. package/dist/refactoring/create-class-to-es6.d.ts.map +1 -0
  159. package/dist/refactoring/create-class-to-es6.js +289 -0
  160. package/dist/refactoring/create-class-to-es6.js.map +1 -0
  161. package/dist/refactoring/create-element-to-jsx.d.ts +8 -0
  162. package/dist/refactoring/create-element-to-jsx.d.ts.map +1 -0
  163. package/dist/refactoring/create-element-to-jsx.js +167 -0
  164. package/dist/refactoring/create-element-to-jsx.js.map +1 -0
  165. package/dist/refactoring/manual-bind-to-arrow.d.ts +8 -0
  166. package/dist/refactoring/manual-bind-to-arrow.d.ts.map +1 -0
  167. package/dist/refactoring/manual-bind-to-arrow.js +134 -0
  168. package/dist/refactoring/manual-bind-to-arrow.js.map +1 -0
  169. package/dist/refactoring/pure-render-mixin.d.ts +8 -0
  170. package/dist/refactoring/pure-render-mixin.d.ts.map +1 -0
  171. package/dist/refactoring/pure-render-mixin.js +253 -0
  172. package/dist/refactoring/pure-render-mixin.js.map +1 -0
  173. package/dist/refactoring/sort-comp.d.ts +8 -0
  174. package/dist/refactoring/sort-comp.d.ts.map +1 -0
  175. package/dist/refactoring/sort-comp.js +128 -0
  176. package/dist/refactoring/sort-comp.js.map +1 -0
  177. package/dist/search/find-hook-usage.d.ts +9 -0
  178. package/dist/search/find-hook-usage.d.ts.map +1 -0
  179. package/dist/search/find-hook-usage.js +262 -0
  180. package/dist/search/find-hook-usage.js.map +1 -0
  181. package/dist/search/find-prop-usage.d.ts +15 -0
  182. package/dist/search/find-prop-usage.d.ts.map +1 -0
  183. package/dist/search/find-prop-usage.js +177 -0
  184. package/dist/search/find-prop-usage.js.map +1 -0
  185. package/dist/search/find-react-component.d.ts +15 -0
  186. package/dist/search/find-react-component.d.ts.map +1 -0
  187. package/dist/search/find-react-component.js +260 -0
  188. package/dist/search/find-react-component.js.map +1 -0
  189. package/dist/search/find-server-rendering-usage.d.ts +9 -0
  190. package/dist/search/find-server-rendering-usage.d.ts.map +1 -0
  191. package/dist/search/find-server-rendering-usage.js +131 -0
  192. package/dist/search/find-server-rendering-usage.js.map +1 -0
  193. package/dist/simplify-object-pattern-property.d.ts +8 -0
  194. package/dist/simplify-object-pattern-property.d.ts.map +1 -0
  195. package/dist/simplify-object-pattern-property.js +59 -0
  196. package/dist/simplify-object-pattern-property.js.map +1 -0
  197. package/dist/simplify-react-imports.d.ts +8 -0
  198. package/dist/simplify-react-imports.d.ts.map +1 -0
  199. package/dist/simplify-react-imports.js +199 -0
  200. package/dist/simplify-react-imports.js.map +1 -0
  201. package/package.json +39 -0
  202. package/src/index.ts +149 -0
  203. package/src/migration/change-component-prop-value.ts +268 -0
  204. package/src/react-native/view-prop-types.ts +63 -0
  205. package/src/react16/error-boundaries.ts +46 -0
  206. package/src/react16/find-dom-node.ts +55 -0
  207. package/src/react16/react-dom-factories.ts +99 -0
  208. package/src/react16/react-prop-types.ts +71 -0
  209. package/src/react16/react-to-react-dom.ts +104 -0
  210. package/src/react16/replace-create-factory.ts +96 -0
  211. package/src/react16/upgrade-to-react-16.ts +37 -0
  212. package/src/react17/remove-event-persist.ts +121 -0
  213. package/src/react17/rename-unsafe-lifecycles.ts +57 -0
  214. package/src/react17/update-react-imports.ts +50 -0
  215. package/src/react17/upgrade-to-react-17.ts +30 -0
  216. package/src/react18/remove-unstable-batched-updates.ts +192 -0
  217. package/src/react18/replace-reactdom-render.ts +68 -0
  218. package/src/react18/replace-render-callback.ts +66 -0
  219. package/src/react18/replace-unmount-component-at-node.ts +66 -0
  220. package/src/react18/upgrade-to-react-18.ts +33 -0
  221. package/src/react19/deprecated-react-types.ts +120 -0
  222. package/src/react19/find-context-consumer.ts +127 -0
  223. package/src/react19/find-deprecated-reactdom-apis.ts +125 -0
  224. package/src/react19/find-element-ref.ts +86 -0
  225. package/src/react19/find-legacy-context-api.ts +157 -0
  226. package/src/react19/no-implicit-ref-callback-return.ts +123 -0
  227. package/src/react19/remove-context-provider.ts +87 -0
  228. package/src/react19/remove-forward-ref.ts +69 -0
  229. package/src/react19/remove-prop-types.ts +86 -0
  230. package/src/react19/remove-react-fc.ts +247 -0
  231. package/src/react19/replace-act-import.ts +36 -0
  232. package/src/react19/replace-default-props.ts +220 -0
  233. package/src/react19/replace-react-shallow-renderer.ts +75 -0
  234. package/src/react19/replace-reactdom-hydrate.ts +67 -0
  235. package/src/react19/replace-string-ref.ts +89 -0
  236. package/src/react19/replace-use-form-state.ts +66 -0
  237. package/src/react19/upgrade-to-react-19.ts +66 -0
  238. package/src/react19/use-context-hook.ts +67 -0
  239. package/src/react19/use-ref-required-initial.ts +75 -0
  240. package/src/refactoring/class-to-functional.ts +229 -0
  241. package/src/refactoring/create-class-to-es6.ts +309 -0
  242. package/src/refactoring/create-element-to-jsx.ts +200 -0
  243. package/src/refactoring/manual-bind-to-arrow.ts +139 -0
  244. package/src/refactoring/pure-render-mixin.ts +346 -0
  245. package/src/refactoring/sort-comp.ts +135 -0
  246. package/src/search/find-hook-usage.ts +226 -0
  247. package/src/search/find-prop-usage.ts +176 -0
  248. package/src/search/find-react-component.ts +254 -0
  249. package/src/search/find-server-rendering-usage.ts +120 -0
  250. package/src/simplify-object-pattern-property.ts +71 -0
  251. package/src/simplify-react-imports.ts +241 -0
@@ -0,0 +1,200 @@
1
+ import {ExecutionContext, printer, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor, JS, raw, template} from "@openrewrite/rewrite/javascript";
3
+ import {J} from "@openrewrite/rewrite/java";
4
+
5
+ /**
6
+ * Converts `React.createElement()` calls to JSX syntax.
7
+ *
8
+ * Handles:
9
+ * - String tags: `createElement('div', ...)` → `<div ... />`
10
+ * - Component tags: `createElement(MyComponent, ...)` → `<MyComponent ... />`
11
+ * - Props objects: `{ className: 'foo' }` → `className="foo"`
12
+ * - String children: `'Hello'` → text node
13
+ * - Expression children: `{expr}` → `{expr}`
14
+ * - Nested createElement calls are handled recursively
15
+ *
16
+ * Before:
17
+ * ```tsx
18
+ * React.createElement('div', { className: 'foo' }, 'Hello')
19
+ * React.createElement(MyComponent, { name: 'World' })
20
+ * ```
21
+ *
22
+ * After:
23
+ * ```tsx
24
+ * <div className="foo">Hello</div>
25
+ * <MyComponent name="World" />
26
+ * ```
27
+ *
28
+ * @see https://github.com/reactjs/react-codemod#create-element-to-jsx
29
+ */
30
+ export class CreateElementToJsx extends Recipe {
31
+ readonly name = "org.openrewrite.react.refactoring.create-element-to-jsx";
32
+ readonly displayName: string = "Convert `createElement` to JSX";
33
+ readonly description: string = "Converts `React.createElement()` calls to JSX syntax for improved readability.";
34
+
35
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
36
+ return new class extends JavaScriptVisitor<ExecutionContext> {
37
+ override async visitMethodInvocation(method: J.MethodInvocation, ctx: ExecutionContext): Promise<J | undefined> {
38
+ let m = await super.visitMethodInvocation(method, ctx) as J.MethodInvocation;
39
+
40
+ if (!this.isCreateElementCall(m)) return m;
41
+
42
+ const args = m.arguments.elements;
43
+ if (args.length === 0) return m;
44
+
45
+ // Extract tag name
46
+ const tagName = this.getTagName(args[0].element);
47
+ if (!tagName) return m;
48
+
49
+ // Extract props
50
+ const propsStr = args.length > 1
51
+ ? await this.serializeProps(args[1].element)
52
+ : '';
53
+
54
+ // Extract children
55
+ const childArgs = args.length > 2
56
+ ? args.slice(2).map(a => a.element)
57
+ : [];
58
+ const childrenStr = await this.serializeChildren(childArgs);
59
+
60
+ // Build JSX
61
+ let jsxSource: string;
62
+ if (childrenStr) {
63
+ jsxSource = `<${tagName}${propsStr}>${childrenStr}</${tagName}>`;
64
+ } else {
65
+ jsxSource = `<${tagName}${propsStr} />`;
66
+ }
67
+
68
+ return await template`${raw(jsxSource)}`.apply(m, this.cursor);
69
+ }
70
+
71
+ private isCreateElementCall(method: J.MethodInvocation): boolean {
72
+ if (method.name.simpleName !== 'createElement') return false;
73
+
74
+ // React.createElement(...)
75
+ if (method.select?.element?.kind === J.Kind.Identifier) {
76
+ return (method.select.element as J.Identifier).simpleName === 'React';
77
+ }
78
+
79
+ return false;
80
+ }
81
+
82
+ private getTagName(expr: any): string | undefined {
83
+ // String literal: 'div', 'span', etc.
84
+ if (expr.kind === J.Kind.Literal) {
85
+ const lit = expr as J.Literal;
86
+ if (typeof lit.value === 'string') {
87
+ return lit.value;
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ // Identifier: MyComponent
93
+ if (expr.kind === J.Kind.Identifier) {
94
+ return (expr as J.Identifier).simpleName;
95
+ }
96
+
97
+ // FieldAccess: Foo.Bar
98
+ if (expr.kind === J.Kind.FieldAccess) {
99
+ const fa = expr as J.FieldAccess;
100
+ if (fa.target.kind === J.Kind.Identifier) {
101
+ return `${(fa.target as J.Identifier).simpleName}.${fa.name.element.simpleName}`;
102
+ }
103
+ return undefined;
104
+ }
105
+
106
+ return undefined;
107
+ }
108
+
109
+ private async serializeProps(propsExpr: any): Promise<string> {
110
+ // null or undefined → no props
111
+ if (propsExpr.kind === J.Kind.Literal) {
112
+ const lit = propsExpr as J.Literal;
113
+ if (lit.value === null) return '';
114
+ }
115
+ if (propsExpr.kind === J.Kind.Identifier) {
116
+ if ((propsExpr as J.Identifier).simpleName === 'undefined') return '';
117
+ }
118
+
119
+ // Object literal (represented as J.NewClass with body)
120
+ if (propsExpr.kind === J.Kind.NewClass) {
121
+ const obj = propsExpr as J.NewClass;
122
+ if (!obj.body) return '';
123
+
124
+ const attrs: string[] = [];
125
+ for (const stmt of obj.body.statements) {
126
+ const s = stmt.element;
127
+
128
+ if (s.kind === JS.Kind.PropertyAssignment) {
129
+ const prop = s as JS.PropertyAssignment;
130
+ const keyExpr = prop.name.element;
131
+ let key: string | undefined;
132
+
133
+ if (keyExpr.kind === J.Kind.Identifier) {
134
+ key = (keyExpr as J.Identifier).simpleName;
135
+ }
136
+
137
+ if (key && prop.initializer) {
138
+ const value = prop.initializer;
139
+
140
+ // String literal values → key="value"
141
+ if (value.kind === J.Kind.Literal) {
142
+ const lit = value as J.Literal;
143
+ if (typeof lit.value === 'string') {
144
+ attrs.push(`${key}="${lit.value}"`);
145
+ continue;
146
+ }
147
+ }
148
+
149
+ // Other values → key={value}
150
+ const valueSource = await printer(this.cursor).print(value);
151
+ attrs.push(`${key}={${valueSource.trim()}}`);
152
+ } else if (key && !prop.initializer) {
153
+ // Shorthand: { disabled } → disabled
154
+ attrs.push(key);
155
+ }
156
+ } else if (s.kind === JS.Kind.Spread) {
157
+ // Spread: {...rest} → {...rest}
158
+ const spread = s as JS.Spread;
159
+ const spreadSource = await printer(this.cursor).print(spread.expression);
160
+ attrs.push(`{...${spreadSource.trim()}}`);
161
+ }
162
+ }
163
+
164
+ return attrs.length > 0 ? ' ' + attrs.join(' ') : '';
165
+ }
166
+
167
+ // Non-object props (variable reference) → {...props}
168
+ const source = await printer(this.cursor).print(propsExpr);
169
+ return ` {...${source.trim()}}`;
170
+ }
171
+
172
+ private async serializeChildren(children: any[]): Promise<string> {
173
+ if (children.length === 0) return '';
174
+
175
+ const parts: string[] = [];
176
+ for (const child of children) {
177
+ // String literal children → text node
178
+ if (child.kind === J.Kind.Literal) {
179
+ const lit = child as J.Literal;
180
+ if (typeof lit.value === 'string') {
181
+ // Escape HTML special characters
182
+ const escaped = (lit.value as string)
183
+ .replace(/&/g, '&amp;')
184
+ .replace(/</g, '&lt;')
185
+ .replace(/>/g, '&gt;');
186
+ parts.push(escaped);
187
+ continue;
188
+ }
189
+ }
190
+
191
+ // Other expressions → {expression}
192
+ const source = await printer(this.cursor).print(child);
193
+ parts.push(`{${source.trim()}}`);
194
+ }
195
+
196
+ return parts.join('');
197
+ }
198
+ }();
199
+ }
200
+ }
@@ -0,0 +1,139 @@
1
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor} from "@openrewrite/rewrite/javascript";
3
+ import {J} from "@openrewrite/rewrite/java";
4
+
5
+ /**
6
+ * Converts manual `.bind(this)` in constructors to class field arrow functions.
7
+ *
8
+ * Before:
9
+ * ```tsx
10
+ * class MyComponent extends React.Component {
11
+ * constructor(props) {
12
+ * super(props);
13
+ * this.handleClick = this.handleClick.bind(this);
14
+ * }
15
+ * handleClick() { ... }
16
+ * }
17
+ * ```
18
+ *
19
+ * After:
20
+ * ```tsx
21
+ * class MyComponent extends React.Component {
22
+ * constructor(props) {
23
+ * super(props);
24
+ * }
25
+ * handleClick = () => { ... };
26
+ * }
27
+ * ```
28
+ *
29
+ * @see https://github.com/reactjs/react-codemod#manual-bind-to-arrow-transform
30
+ */
31
+ export class ManualBindToArrow extends Recipe {
32
+ readonly name = "org.openrewrite.react.refactoring.manual-bind-to-arrow";
33
+ readonly displayName: string = "Convert manual `.bind(this)` to arrow functions";
34
+ readonly description: string = "Converts `this.method = this.method.bind(this)` in constructors to class field arrow function syntax.";
35
+
36
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
37
+ return new class extends JavaScriptVisitor<ExecutionContext> {
38
+ private boundMethods = new Set<string>();
39
+
40
+ override async visitClassDeclaration(classDecl: J.ClassDeclaration, ctx: ExecutionContext): Promise<J | undefined> {
41
+ this.boundMethods.clear();
42
+
43
+ // First pass: find bind statements in constructor
44
+ await this.findBindStatements(classDecl);
45
+
46
+ if (this.boundMethods.size === 0) {
47
+ return classDecl;
48
+ }
49
+
50
+ // Second pass: remove bind statements and convert methods to arrow functions
51
+ return await super.visitClassDeclaration(classDecl, ctx);
52
+ }
53
+
54
+ private async findBindStatements(classDecl: J.ClassDeclaration): Promise<void> {
55
+ const body = classDecl.body;
56
+ if (!body) return;
57
+
58
+ for (const stmt of body.statements) {
59
+ const s = stmt.element;
60
+ if (!s) continue;
61
+
62
+ if (s.kind === J.Kind.MethodDeclaration) {
63
+ const method = s as J.MethodDeclaration;
64
+ if (method.name.simpleName === 'constructor') {
65
+ // Look for this.x = this.x.bind(this) in constructor body
66
+ if (method.body) {
67
+ for (const bodyStmt of method.body.statements) {
68
+ const bs = bodyStmt.element;
69
+ if (!bs) continue;
70
+ this.checkForBindStatement(bs);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ private checkForBindStatement(stmt: J): void {
79
+ // Match: this.x = this.x.bind(this)
80
+ if (stmt.kind !== J.Kind.Assignment) return;
81
+ const assignment = stmt as J.Assignment;
82
+
83
+ // LHS: this.x
84
+ if (assignment.variable.kind !== J.Kind.FieldAccess) return;
85
+ const lhs = assignment.variable as J.FieldAccess;
86
+ if (lhs.target.kind !== J.Kind.Identifier) return;
87
+ if ((lhs.target as J.Identifier).simpleName !== 'this') return;
88
+ const methodName = lhs.name.element.simpleName;
89
+
90
+ // RHS: this.x.bind(this)
91
+ const rhs = assignment.assignment.element;
92
+ if (!rhs || rhs.kind !== J.Kind.MethodInvocation) return;
93
+ const call = rhs as J.MethodInvocation;
94
+ if (call.name.simpleName !== 'bind') return;
95
+
96
+ // Check select is this.x
97
+ if (!call.select) return;
98
+ const select = (call.select as any).element ?? call.select;
99
+ if (select.kind !== J.Kind.FieldAccess) return;
100
+ const selectFa = select as J.FieldAccess;
101
+ if (selectFa.target.kind !== J.Kind.Identifier) return;
102
+ if ((selectFa.target as J.Identifier).simpleName !== 'this') return;
103
+ if (selectFa.name.element.simpleName !== methodName) return;
104
+
105
+ this.boundMethods.add(methodName);
106
+ }
107
+
108
+ override async visitMethodDeclaration(method: J.MethodDeclaration, ctx: ExecutionContext): Promise<J | undefined> {
109
+ let m = await super.visitMethodDeclaration(method, ctx) as J.MethodDeclaration;
110
+
111
+ // Remove bind statements from constructor
112
+ if (m.name.simpleName === 'constructor' && m.body && this.boundMethods.size > 0) {
113
+ const filteredStatements = m.body.statements.filter(stmt => {
114
+ const s = stmt.element;
115
+ if (!s || s.kind !== J.Kind.Assignment) return true;
116
+ const assignment = s as J.Assignment;
117
+ if (assignment.variable.kind !== J.Kind.FieldAccess) return true;
118
+ const fa = assignment.variable as J.FieldAccess;
119
+ if (fa.target.kind !== J.Kind.Identifier) return true;
120
+ if ((fa.target as J.Identifier).simpleName !== 'this') return true;
121
+ return !this.boundMethods.has(fa.name.element.simpleName);
122
+ });
123
+
124
+ if (filteredStatements.length !== m.body.statements.length) {
125
+ m = {
126
+ ...m,
127
+ body: {
128
+ ...m.body,
129
+ statements: filteredStatements
130
+ }
131
+ } as any as J.MethodDeclaration;
132
+ }
133
+ }
134
+
135
+ return m;
136
+ }
137
+ }();
138
+ }
139
+ }
@@ -0,0 +1,346 @@
1
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor, JS, maybeRemoveImport, raw, template} from "@openrewrite/rewrite/javascript";
3
+ import {J} from "@openrewrite/rewrite/java";
4
+
5
+ /**
6
+ * Removes `PureRenderMixin` from `React.createClass` components and replaces it
7
+ * with an explicit `shouldComponentUpdate` method using `shallowCompare`.
8
+ *
9
+ * Before:
10
+ * ```tsx
11
+ * var PureRenderMixin = require('react-addons-pure-render-mixin');
12
+ * var MyComponent = React.createClass({
13
+ * mixins: [PureRenderMixin],
14
+ * render() { return <div />; }
15
+ * });
16
+ * ```
17
+ *
18
+ * After:
19
+ * ```tsx
20
+ * var MyComponent = React.createClass({
21
+ * shouldComponentUpdate(nextProps, nextState) {
22
+ * return React.addons.shallowCompare(this, nextProps, nextState);
23
+ * },
24
+ * render() { return <div />; }
25
+ * });
26
+ * ```
27
+ *
28
+ * @see https://github.com/reactjs/react-codemod#pure-render-mixin
29
+ */
30
+ export class PureRenderMixin extends Recipe {
31
+ readonly name = "org.openrewrite.react.refactoring.pure-render-mixin";
32
+ readonly displayName: string = "Remove `PureRenderMixin`";
33
+ readonly description: string = "Removes `PureRenderMixin` from `React.createClass` mixins and adds an explicit `shouldComponentUpdate` method.";
34
+
35
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
36
+ return new class extends JavaScriptVisitor<ExecutionContext> {
37
+ private transformed = false;
38
+
39
+ override async visitJsCompilationUnit(cu: JS.CompilationUnit, ctx: ExecutionContext): Promise<J | undefined> {
40
+ this.transformed = false;
41
+ const result = await super.visitJsCompilationUnit(cu, ctx);
42
+ if (this.transformed) {
43
+ maybeRemoveImport(this, "react-addons-pure-render-mixin", "default");
44
+ }
45
+ return result;
46
+ }
47
+
48
+ override async visitMethodInvocation(method: J.MethodInvocation, ctx: ExecutionContext): Promise<J | undefined> {
49
+ let m = await super.visitMethodInvocation(method, ctx) as J.MethodInvocation;
50
+
51
+ if (!this.isCreateClassCall(m)) return m;
52
+
53
+ const args = m.arguments.elements;
54
+ if (args.length === 0) return m;
55
+
56
+ const specArg = args[0].element;
57
+ if (specArg.kind !== J.Kind.NewClass) return m;
58
+
59
+ const specObj = specArg as J.NewClass;
60
+ if (!specObj.body) return m;
61
+
62
+ // Find the mixins property and check for PureRenderMixin
63
+ const mixinsInfo = this.findPureRenderMixin(specObj.body);
64
+ if (!mixinsInfo) return m;
65
+
66
+ // Check that shouldComponentUpdate doesn't already exist
67
+ if (this.hasShouldComponentUpdate(specObj.body)) return m;
68
+
69
+ // Get member indent from existing members
70
+ const memberIndent = this.getMemberIndent(specObj.body);
71
+
72
+ // Build shouldComponentUpdate by cloning an existing method's structure
73
+ // and replacing its internals with the desired content
74
+ const existingMethod = this.findAnyMethod(specObj.body);
75
+ if (!existingMethod) return m;
76
+
77
+ // Create the return expression via template
78
+ const returnExpr = await template`React.addons.shallowCompare(this, nextProps, nextState)`.apply(specObj, this.cursor);
79
+ if (!returnExpr) return m;
80
+
81
+ // Create parameter identifiers via template
82
+ const nextPropsId = await template`nextProps`.apply(specObj, this.cursor);
83
+ const nextStateId = await template`nextState`.apply(specObj, this.cursor);
84
+ if (!nextPropsId || !nextStateId) return m;
85
+
86
+ // Build the shouldComponentUpdate method by cloning structure from existing method
87
+ const scuMethod = this.buildMethodFromTemplate(
88
+ existingMethod,
89
+ 'shouldComponentUpdate',
90
+ [nextPropsId, nextStateId],
91
+ returnExpr
92
+ );
93
+
94
+ // Build new statements: remove mixins (or PureRenderMixin from it), add shouldComponentUpdate
95
+ const newStatements = this.buildNewStatements(
96
+ specObj.body.statements,
97
+ mixinsInfo,
98
+ scuMethod,
99
+ memberIndent
100
+ );
101
+
102
+ this.transformed = true;
103
+
104
+ // Return updated method invocation with modified spec object
105
+ return {
106
+ ...m,
107
+ arguments: {
108
+ ...m.arguments,
109
+ elements: [
110
+ {
111
+ ...args[0],
112
+ element: {
113
+ ...specObj,
114
+ body: {
115
+ ...specObj.body,
116
+ statements: newStatements
117
+ }
118
+ }
119
+ },
120
+ ...args.slice(1)
121
+ ]
122
+ }
123
+ } as any as J.MethodInvocation;
124
+ }
125
+
126
+ private isCreateClassCall(method: J.MethodInvocation): boolean {
127
+ if (method.name.simpleName === 'createClass' && method.select) {
128
+ const select = (method.select as any).element ?? method.select;
129
+ if (select.kind === J.Kind.Identifier) {
130
+ return (select as J.Identifier).simpleName === 'React';
131
+ }
132
+ }
133
+ if (method.name.simpleName === 'createReactClass' && !method.select) {
134
+ return true;
135
+ }
136
+ return false;
137
+ }
138
+
139
+ private findPureRenderMixin(body: J.Block): { stmtIndex: number; isOnlyMixin: boolean } | undefined {
140
+ for (let i = 0; i < body.statements.length; i++) {
141
+ const s = body.statements[i].element;
142
+ if (s.kind !== JS.Kind.PropertyAssignment) continue;
143
+
144
+ const prop = s as JS.PropertyAssignment;
145
+ const keyExpr = prop.name.element;
146
+ if (keyExpr.kind !== J.Kind.Identifier) continue;
147
+ if ((keyExpr as J.Identifier).simpleName !== 'mixins') continue;
148
+
149
+ // Check if the initializer is an array containing PureRenderMixin
150
+ if (!prop.initializer) continue;
151
+ const init = prop.initializer;
152
+ if (init.kind !== J.Kind.NewArray) continue;
153
+
154
+ const arr = init as J.NewArray;
155
+ if (!arr.initializer) continue;
156
+
157
+ const elements = arr.initializer.elements ?? [];
158
+ const hasPRM = elements.some((el: any) => {
159
+ const elem = el.element ?? el;
160
+ return elem.kind === J.Kind.Identifier &&
161
+ (elem as J.Identifier).simpleName === 'PureRenderMixin';
162
+ });
163
+
164
+ if (hasPRM) {
165
+ return {
166
+ stmtIndex: i,
167
+ isOnlyMixin: elements.length === 1
168
+ };
169
+ }
170
+ }
171
+ return undefined;
172
+ }
173
+
174
+ private hasShouldComponentUpdate(body: J.Block): boolean {
175
+ return body.statements.some(stmt => {
176
+ const s = stmt.element;
177
+ if (s.kind === J.Kind.MethodDeclaration) {
178
+ return (s as J.MethodDeclaration).name.simpleName === 'shouldComponentUpdate';
179
+ }
180
+ return false;
181
+ });
182
+ }
183
+
184
+ private findAnyMethod(body: J.Block): J.MethodDeclaration | undefined {
185
+ for (const stmt of body.statements) {
186
+ if (stmt.element?.kind === J.Kind.MethodDeclaration) {
187
+ return stmt.element as J.MethodDeclaration;
188
+ }
189
+ }
190
+ return undefined;
191
+ }
192
+
193
+ private buildMethodFromTemplate(
194
+ existingMethod: J.MethodDeclaration,
195
+ name: string,
196
+ params: J[],
197
+ returnExpr: J
198
+ ): J.MethodDeclaration {
199
+ // Clone the existing method structure and replace name, params, and body
200
+ const nameId = {
201
+ ...existingMethod.name,
202
+ simpleName: name,
203
+ } as J.Identifier;
204
+
205
+ const emptySpace = {kind: 'org.openrewrite.java.tree.Space' as const, whitespace: '', comments: []};
206
+ const singleSpace = {kind: 'org.openrewrite.java.tree.Space' as const, whitespace: ' ', comments: []};
207
+ const emptyMarkers = existingMethod.markers;
208
+
209
+ // Build parameter list with proper spacing between params
210
+ const paramElements = params.map((param, i) => ({
211
+ kind: 'org.openrewrite.java.tree.JRightPadded' as const,
212
+ element: i > 0 ? {...(param as any), prefix: singleSpace} : param,
213
+ after: emptySpace,
214
+ markers: emptyMarkers,
215
+ }));
216
+
217
+ const bodyPrefix = existingMethod.body?.prefix ?? existingMethod.prefix;
218
+
219
+ // Build return statement by wrapping the expression
220
+ // Add a space prefix to the expression so it renders as "return expr" not "returnexpr"
221
+ const returnExprWithSpace = {...(returnExpr as any), prefix: singleSpace};
222
+ const returnStmt: any = {
223
+ kind: J.Kind.Return,
224
+ id: (returnExpr as any).id,
225
+ prefix: {
226
+ kind: 'org.openrewrite.java.tree.Space' as const,
227
+ whitespace: '\n ',
228
+ comments: []
229
+ },
230
+ markers: emptyMarkers,
231
+ expression: returnExprWithSpace,
232
+ };
233
+
234
+ const body: J.Block = {
235
+ kind: J.Kind.Block,
236
+ id: existingMethod.body?.id ?? (existingMethod as any).id,
237
+ prefix: bodyPrefix,
238
+ markers: existingMethod.markers,
239
+ static: existingMethod.body?.static ?? {
240
+ kind: 'org.openrewrite.java.tree.JRightPadded' as const,
241
+ element: false,
242
+ after: {kind: 'org.openrewrite.java.tree.Space' as const, whitespace: '', comments: []},
243
+ markers: existingMethod.markers,
244
+ },
245
+ statements: [{
246
+ kind: 'org.openrewrite.java.tree.JRightPadded' as const,
247
+ element: returnStmt,
248
+ after: {kind: 'org.openrewrite.java.tree.Space' as const, whitespace: '', comments: []},
249
+ markers: existingMethod.markers,
250
+ }],
251
+ end: {
252
+ kind: 'org.openrewrite.java.tree.Space' as const,
253
+ whitespace: '\n ',
254
+ comments: []
255
+ },
256
+ } as any as J.Block;
257
+
258
+ const parameters = {
259
+ ...existingMethod.parameters,
260
+ elements: paramElements,
261
+ } as any;
262
+
263
+ return {
264
+ ...existingMethod,
265
+ name: nameId,
266
+ parameters: parameters,
267
+ body: body,
268
+ } as any as J.MethodDeclaration;
269
+ }
270
+
271
+ private getMemberIndent(body: J.Block): any {
272
+ for (const stmt of body.statements) {
273
+ if (stmt.element?.prefix) return stmt.element.prefix;
274
+ }
275
+ return undefined;
276
+ }
277
+
278
+ private buildNewStatements(
279
+ statements: any[],
280
+ mixinsInfo: { stmtIndex: number; isOnlyMixin: boolean },
281
+ scuMethod: J,
282
+ indent: any
283
+ ): any[] {
284
+ const result: any[] = [];
285
+ let scuInserted = false;
286
+
287
+ for (let i = 0; i < statements.length; i++) {
288
+ if (i === mixinsInfo.stmtIndex) {
289
+ if (mixinsInfo.isOnlyMixin) {
290
+ // Remove the entire mixins property
291
+ continue;
292
+ } else {
293
+ // Remove PureRenderMixin from the array but keep other mixins
294
+ const stmt = statements[i];
295
+ const prop = stmt.element as JS.PropertyAssignment;
296
+ const arr = prop.initializer as J.NewArray;
297
+ const filteredElements = (arr.initializer!.elements as any[]).filter((el: any) => {
298
+ const elem = el.element ?? el;
299
+ return !(elem.kind === J.Kind.Identifier &&
300
+ (elem as J.Identifier).simpleName === 'PureRenderMixin');
301
+ });
302
+ result.push({
303
+ ...stmt,
304
+ element: {
305
+ ...prop,
306
+ initializer: {
307
+ ...arr,
308
+ initializer: {
309
+ ...arr.initializer,
310
+ elements: filteredElements
311
+ }
312
+ }
313
+ }
314
+ });
315
+ continue;
316
+ }
317
+ }
318
+
319
+ const s = statements[i].element;
320
+ // Insert shouldComponentUpdate before render
321
+ if (!scuInserted && s.kind === J.Kind.MethodDeclaration &&
322
+ (s as J.MethodDeclaration).name.simpleName === 'render') {
323
+ const scuStmt = {
324
+ ...statements[i],
325
+ element: indent ? {...(scuMethod as any), prefix: indent} : scuMethod
326
+ };
327
+ result.push(scuStmt);
328
+ scuInserted = true;
329
+ }
330
+ result.push(statements[i]);
331
+ }
332
+
333
+ // If render wasn't found, append at the end
334
+ if (!scuInserted) {
335
+ const lastStmt = statements[statements.length - 1];
336
+ result.push({
337
+ ...lastStmt,
338
+ element: indent ? {...(scuMethod as any), prefix: indent} : scuMethod
339
+ });
340
+ }
341
+
342
+ return result;
343
+ }
344
+ }();
345
+ }
346
+ }