@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,75 @@
1
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor} from "@openrewrite/rewrite/javascript";
3
+ import {isIdentifier, J} from "@openrewrite/rewrite/java";
4
+ import {create} from "mutative";
5
+
6
+ /**
7
+ * Adds `undefined` argument to `useRef()` calls with no arguments.
8
+ *
9
+ * In `@types/react` 19, `useRef()` with no arguments is no longer valid.
10
+ * You must pass an explicit initial value, typically `undefined` or `null`.
11
+ *
12
+ * Before:
13
+ * ```tsx
14
+ * const ref = useRef();
15
+ * ```
16
+ *
17
+ * After:
18
+ * ```tsx
19
+ * const ref = useRef(undefined);
20
+ * ```
21
+ *
22
+ * @see https://react.dev/blog/2024/04/25/react-19-upgrade-guide#different-types
23
+ */
24
+ export class UseRefRequiredInitial extends Recipe {
25
+ readonly name = "org.openrewrite.react.19.use-ref-required-initial";
26
+ readonly displayName: string = "Add initial value to `useRef()` calls";
27
+ readonly description: string = "Adds `undefined` as initial argument to `useRef()` calls with no arguments. Required by `@types/react` 19.";
28
+
29
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
30
+ return new class extends JavaScriptVisitor<ExecutionContext> {
31
+ override async visitMethodInvocation(method: J.MethodInvocation, ctx: ExecutionContext): Promise<J | undefined> {
32
+ let m = await super.visitMethodInvocation(method, ctx) as J.MethodInvocation;
33
+
34
+ // Match useRef() or React.useRef() with no arguments
35
+ if (m.name.simpleName !== 'useRef') return m;
36
+
37
+ // Check it has no arguments (empty args list)
38
+ const args = m.arguments;
39
+ if (!args) return m;
40
+
41
+ const argElements = args.elements;
42
+ if (!argElements) return m;
43
+
44
+ // Empty args means a single J.Empty element
45
+ if (argElements.length === 1) {
46
+ const firstArg = argElements[0];
47
+ const argNode = (firstArg as any).element ?? firstArg;
48
+ if (argNode.kind === J.Kind.Empty) {
49
+ // Replace the empty arg with `undefined`
50
+ const undefinedIdent: J.Identifier = {
51
+ kind: J.Kind.Identifier,
52
+ id: argNode.id,
53
+ prefix: argNode.prefix,
54
+ markers: argNode.markers,
55
+ annotations: [],
56
+ simpleName: 'undefined'
57
+ };
58
+
59
+ return create(m, draft => {
60
+ const draftArg = draft.arguments!.elements[0];
61
+ const draftArgPadded = draftArg as any;
62
+ if (draftArgPadded.element) {
63
+ draftArgPadded.element = undefinedIdent;
64
+ } else {
65
+ draft.arguments!.elements[0] = undefinedIdent as any;
66
+ }
67
+ });
68
+ }
69
+ }
70
+
71
+ return m;
72
+ }
73
+ }();
74
+ }
75
+ }
@@ -0,0 +1,229 @@
1
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {capture, JavaScriptVisitor, pattern, raw, rewrite, template} from "@openrewrite/rewrite/javascript";
3
+ import {J} from "@openrewrite/rewrite/java";
4
+
5
+ const LIFECYCLE_METHODS = new Set([
6
+ 'componentWillMount', 'UNSAFE_componentWillMount',
7
+ 'componentDidMount', 'componentWillReceiveProps',
8
+ 'UNSAFE_componentWillReceiveProps', 'shouldComponentUpdate',
9
+ 'componentWillUpdate', 'UNSAFE_componentWillUpdate',
10
+ 'getSnapshotBeforeUpdate', 'componentDidUpdate',
11
+ 'componentDidCatch', 'componentWillUnmount',
12
+ 'getDerivedStateFromProps', 'getDerivedStateFromError',
13
+ 'getDefaultProps', 'getInitialState',
14
+ ]);
15
+
16
+ /**
17
+ * Converts simple class components (render-only) to functional components.
18
+ *
19
+ * Only transforms class components that:
20
+ * - Have only a `render` method (no other instance methods)
21
+ * - Have no state
22
+ * - Have no lifecycle methods
23
+ * - Have no refs
24
+ *
25
+ * Before:
26
+ * ```tsx
27
+ * class MyComponent extends React.Component {
28
+ * render() {
29
+ * return <div>{this.props.name}</div>;
30
+ * }
31
+ * }
32
+ * ```
33
+ *
34
+ * After:
35
+ * ```tsx
36
+ * function MyComponent(props) {
37
+ * return <div>{props.name}</div>;
38
+ * }
39
+ * ```
40
+ *
41
+ * @see https://github.com/reactjs/react-codemod#pure-component
42
+ */
43
+ export class ClassToFunctional extends Recipe {
44
+ readonly name = "org.openrewrite.react.refactoring.class-to-functional";
45
+ readonly displayName: string = "Convert class components to functional components";
46
+ readonly description: string = "Converts simple render-only class components to functional components.";
47
+
48
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
49
+ // Rewrite rules for replacing this.props references
50
+ const field = capture();
51
+ const thisPropsFieldRule = rewrite(() => ({
52
+ before: pattern`this.props.${field}`,
53
+ after: template`props.${field}`
54
+ }));
55
+ const thisPropsRule = rewrite(() => ({
56
+ before: pattern`this.props`,
57
+ after: template`props`
58
+ }));
59
+
60
+ return new class extends JavaScriptVisitor<ExecutionContext> {
61
+ override async visitClassDeclaration(classDecl: J.ClassDeclaration, ctx: ExecutionContext): Promise<J | undefined> {
62
+ let cd = await super.visitClassDeclaration(classDecl, ctx) as J.ClassDeclaration;
63
+
64
+ if (!this.extendsReactComponent(cd)) {
65
+ return cd;
66
+ }
67
+
68
+ const analysis = this.analyzeClassBody(cd);
69
+ if (!analysis.canConvert) {
70
+ return cd;
71
+ }
72
+
73
+ const renderMethod = analysis.renderMethod!;
74
+ if (!renderMethod.body) {
75
+ return cd;
76
+ }
77
+
78
+ // Replace this.props references with props in the render body
79
+ const transformedBody = await this.replaceThisProps(renderMethod.body, ctx);
80
+
81
+ // Use template to construct the function declaration
82
+ const className = cd.name.simpleName;
83
+ const funcDecl = await template`function ${raw(className)}(props) {}`.apply(cd, this.cursor);
84
+ if (!funcDecl) return cd;
85
+
86
+ // Adjust render body indentation to match function (not class) nesting
87
+ const classBodyIndent = this.getIndent(renderMethod.prefix);
88
+ const adjustedBody = this.adjustIndentation(transformedBody as J.Block, classBodyIndent);
89
+
90
+ // Splice the transformed render body into the template-constructed function
91
+ const func = funcDecl as J.MethodDeclaration;
92
+ return {...func, body: adjustedBody} as any as J.MethodDeclaration;
93
+ }
94
+
95
+ private extendsReactComponent(cd: J.ClassDeclaration): boolean {
96
+ if (!cd.extends) return false;
97
+ const extendsType = cd.extends.element;
98
+
99
+ if (extendsType.kind === J.Kind.FieldAccess) {
100
+ const fa = extendsType as J.FieldAccess;
101
+ const name = fa.name.element.simpleName;
102
+ if (name !== 'Component' && name !== 'PureComponent') return false;
103
+ if (fa.target.kind !== J.Kind.Identifier) return false;
104
+ return (fa.target as J.Identifier).simpleName === 'React';
105
+ }
106
+
107
+ if (extendsType.kind === J.Kind.Identifier) {
108
+ const name = (extendsType as J.Identifier).simpleName;
109
+ return name === 'Component' || name === 'PureComponent';
110
+ }
111
+
112
+ return false;
113
+ }
114
+
115
+ private analyzeClassBody(cd: J.ClassDeclaration): {
116
+ canConvert: boolean;
117
+ renderMethod?: J.MethodDeclaration;
118
+ } {
119
+ let renderMethod: J.MethodDeclaration | undefined;
120
+ let hasDisqualifyingMember = false;
121
+
122
+ for (const stmt of cd.body.statements) {
123
+ const s = stmt.element;
124
+ if (!s) continue;
125
+
126
+ if (s.kind === J.Kind.MethodDeclaration) {
127
+ const method = s as J.MethodDeclaration;
128
+ const name = method.name.simpleName;
129
+
130
+ if (name === 'render') {
131
+ renderMethod = method;
132
+ } else if (name === 'constructor') {
133
+ if (!this.isSimpleConstructor(method)) {
134
+ hasDisqualifyingMember = true;
135
+ }
136
+ } else if (LIFECYCLE_METHODS.has(name)) {
137
+ hasDisqualifyingMember = true;
138
+ } else {
139
+ const isStatic = method.modifiers?.some(m => m.keyword === 'static');
140
+ if (!isStatic) {
141
+ hasDisqualifyingMember = true;
142
+ }
143
+ }
144
+ } else if (s.kind === J.Kind.VariableDeclarations) {
145
+ const vars = s as J.VariableDeclarations;
146
+ const isStatic = vars.modifiers?.some(m => m.keyword === 'static');
147
+ if (!isStatic) {
148
+ hasDisqualifyingMember = true;
149
+ }
150
+ }
151
+ }
152
+
153
+ return {
154
+ canConvert: !!renderMethod && !hasDisqualifyingMember,
155
+ renderMethod
156
+ };
157
+ }
158
+
159
+ private isSimpleConstructor(method: J.MethodDeclaration): boolean {
160
+ if (!method.body) return true;
161
+ const stmts = method.body.statements;
162
+ if (stmts.length === 0) return true;
163
+ if (stmts.length > 1) return false;
164
+
165
+ const firstStmt = stmts[0].element;
166
+ if (!firstStmt) return true;
167
+
168
+ if (firstStmt.kind === J.Kind.MethodInvocation) {
169
+ const call = firstStmt as J.MethodInvocation;
170
+ return call.name.simpleName === 'super';
171
+ }
172
+
173
+ return false;
174
+ }
175
+
176
+ private async replaceThisProps(body: J.Block, ctx: ExecutionContext): Promise<J.Block> {
177
+ const replacer = new class extends JavaScriptVisitor<ExecutionContext> {
178
+ override async visitFieldAccess(fieldAccess: J.FieldAccess, ctx: ExecutionContext): Promise<J | undefined> {
179
+ let fa = await super.visitFieldAccess(fieldAccess, ctx) as J.FieldAccess;
180
+
181
+ let result = await thisPropsFieldRule.tryOn(this.cursor, fa);
182
+ if (result) return result;
183
+
184
+ result = await thisPropsRule.tryOn(this.cursor, fa);
185
+ if (result) return result;
186
+
187
+ return fa;
188
+ }
189
+ }();
190
+
191
+ return await replacer.visit(body, ctx) as J.Block;
192
+ }
193
+
194
+ private getIndent(space: any): string {
195
+ if (!space || typeof space.whitespace !== 'string') return '';
196
+ const ws = space.whitespace as string;
197
+ const lastNewline = ws.lastIndexOf('\n');
198
+ if (lastNewline === -1) return '';
199
+ return ws.substring(lastNewline + 1);
200
+ }
201
+
202
+ private adjustIndentation(body: J.Block, indentToRemove: string): J.Block {
203
+ if (!indentToRemove) return body;
204
+ return {
205
+ ...body,
206
+ statements: body.statements.map(stmt => ({
207
+ ...stmt,
208
+ element: stmt.element ? {
209
+ ...stmt.element,
210
+ prefix: this.removeIndent(stmt.element.prefix, indentToRemove)
211
+ } : stmt.element
212
+ })) as any,
213
+ end: this.removeIndent(body.end, indentToRemove)
214
+ };
215
+ }
216
+
217
+ private removeIndent(space: any, indent: string): any {
218
+ if (!space || typeof space.whitespace !== 'string') return space;
219
+ const ws = space.whitespace as string;
220
+ const adjusted = ws.split('\n').map((line: string, i: number) => {
221
+ if (i === 0) return line;
222
+ if (line.startsWith(indent)) return line.substring(indent.length);
223
+ return line;
224
+ }).join('\n');
225
+ return adjusted !== ws ? {...space, whitespace: adjusted} : space;
226
+ }
227
+ }();
228
+ }
229
+ }
@@ -0,0 +1,309 @@
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
+ const LIFECYCLE_METHODS = new Set([
6
+ 'render',
7
+ 'componentWillMount', 'UNSAFE_componentWillMount',
8
+ 'componentDidMount', 'componentWillReceiveProps',
9
+ 'UNSAFE_componentWillReceiveProps', 'shouldComponentUpdate',
10
+ 'componentWillUpdate', 'UNSAFE_componentWillUpdate',
11
+ 'getSnapshotBeforeUpdate', 'componentDidUpdate',
12
+ 'componentDidCatch', 'componentWillUnmount',
13
+ 'getDerivedStateFromProps', 'getDerivedStateFromError',
14
+ ]);
15
+
16
+ const SEMICOLON_KIND = "org.openrewrite.java.marker.Semicolon";
17
+
18
+ /**
19
+ * Converts `React.createClass()` to ES6 class syntax.
20
+ *
21
+ * Handles:
22
+ * - `getInitialState()` → class property `state = {...}`
23
+ * - `getDefaultProps()` → `static defaultProps = {...}`
24
+ * - Static properties (propTypes, contextTypes) → class statics
25
+ * - Auto-binding of methods → arrow function class fields
26
+ * - Lifecycle methods and render → regular class methods
27
+ * - Skips conversion if mixins are present
28
+ *
29
+ * @see https://github.com/reactjs/react-codemod#class
30
+ */
31
+ export class CreateClassToES6 extends Recipe {
32
+ readonly name = "org.openrewrite.react.refactoring.create-class-to-es6";
33
+ readonly displayName: string = "Convert `createClass` to ES6 class";
34
+ readonly description: string = "Converts `React.createClass()` and `createReactClass()` calls to ES6 class syntax.";
35
+
36
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
37
+ return new class extends JavaScriptVisitor<ExecutionContext> {
38
+ private convertedClassIds = new Set<string>();
39
+
40
+ private stripSemicolonFromConverted(statements: any[]): { modified: boolean, statements: any[] } {
41
+ let modified = false;
42
+ const newStatements = statements.map((stmt: any) => {
43
+ if (stmt.element && this.convertedClassIds.has(stmt.element.id)) {
44
+ const hasSemicolon = stmt.markers?.markers?.some(
45
+ (m: any) => m.kind === SEMICOLON_KIND
46
+ );
47
+ if (hasSemicolon) {
48
+ modified = true;
49
+ return {
50
+ ...stmt,
51
+ markers: {
52
+ ...stmt.markers,
53
+ markers: stmt.markers.markers.filter(
54
+ (m: any) => m.kind !== SEMICOLON_KIND
55
+ )
56
+ }
57
+ };
58
+ }
59
+ }
60
+ return stmt;
61
+ });
62
+ return { modified, statements: newStatements };
63
+ }
64
+
65
+ override async visitJsCompilationUnit(cu: JS.CompilationUnit, ctx: ExecutionContext): Promise<J | undefined> {
66
+ let c = await super.visitJsCompilationUnit(cu, ctx) as JS.CompilationUnit;
67
+ if (this.convertedClassIds.size === 0) return c;
68
+
69
+ const result = this.stripSemicolonFromConverted(c.statements as any);
70
+ return result.modified ? {...c, statements: result.statements} as any : c;
71
+ }
72
+
73
+ override async visitBlock(block: J.Block, ctx: ExecutionContext): Promise<J | undefined> {
74
+ let b = await super.visitBlock(block, ctx) as J.Block;
75
+ if (this.convertedClassIds.size === 0) return b;
76
+
77
+ const result = this.stripSemicolonFromConverted(b.statements as any);
78
+ return result.modified ? {...b, statements: result.statements} as any : b;
79
+ }
80
+
81
+ override async visitVariableDeclarations(varDecl: J.VariableDeclarations, ctx: ExecutionContext): Promise<J | undefined> {
82
+ let vd = await super.visitVariableDeclarations(varDecl, ctx) as J.VariableDeclarations;
83
+
84
+ if (!vd.variables || vd.variables.length !== 1) return vd;
85
+
86
+ const namedVar = vd.variables[0].element as J.VariableDeclarations.NamedVariable;
87
+ if (!namedVar.initializer) return vd;
88
+
89
+ const init = namedVar.initializer.element;
90
+ if (init.kind !== J.Kind.MethodInvocation) return vd;
91
+
92
+ const methodInv = init as J.MethodInvocation;
93
+ if (!this.isCreateClassCall(methodInv)) return vd;
94
+
95
+ const componentName = (namedVar.name as J.Identifier).simpleName;
96
+
97
+ // Get spec object (first argument)
98
+ const args = methodInv.arguments.elements;
99
+ if (args.length === 0) return vd;
100
+
101
+ const specArg = args[0].element;
102
+ if (specArg.kind !== J.Kind.NewClass) return vd;
103
+
104
+ const specObj = specArg as J.NewClass;
105
+ if (!specObj.body) return vd;
106
+
107
+ // Check for mixins — skip conversion if present
108
+ if (this.hasMixins(specObj.body)) return vd;
109
+
110
+ // Build transformed class members
111
+ const memberIndent = this.getMemberIndent(specObj.body);
112
+ const members = await this.transformMembers(specObj.body, memberIndent, ctx);
113
+
114
+ // Construct class declaration using template
115
+ const classDecl = await template`class ${raw(componentName)} extends React.Component {}`.apply(vd, this.cursor);
116
+ if (!classDecl) return vd;
117
+
118
+ // Splice transformed members into class body
119
+ const cd = classDecl as J.ClassDeclaration;
120
+ const result = {
121
+ ...cd,
122
+ body: {
123
+ ...cd.body,
124
+ statements: members
125
+ }
126
+ } as any as J.ClassDeclaration;
127
+
128
+ // Track converted node ID so visitBlock can strip the trailing semicolon
129
+ this.convertedClassIds.add(result.id);
130
+
131
+ return result;
132
+ }
133
+
134
+ private isCreateClassCall(method: J.MethodInvocation): boolean {
135
+ if (method.name.simpleName === 'createClass' && method.select) {
136
+ if (method.select.element.kind === J.Kind.Identifier) {
137
+ return (method.select.element as J.Identifier).simpleName === 'React';
138
+ }
139
+ }
140
+ if (method.name.simpleName === 'createReactClass' && !method.select) {
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ private hasMixins(body: J.Block): boolean {
147
+ for (const stmt of body.statements) {
148
+ const s = stmt.element;
149
+ if (s.kind === JS.Kind.PropertyAssignment) {
150
+ const prop = s as JS.PropertyAssignment;
151
+ if (this.getPropertyKeyName(prop) === 'mixins') return true;
152
+ }
153
+ }
154
+ return false;
155
+ }
156
+
157
+ private getPropertyKeyName(prop: JS.PropertyAssignment): string | undefined {
158
+ const keyExpr = prop.name.element;
159
+ if (keyExpr.kind === J.Kind.Identifier) {
160
+ return (keyExpr as J.Identifier).simpleName;
161
+ }
162
+ return undefined;
163
+ }
164
+
165
+ private getMemberIndent(body: J.Block): any {
166
+ // Get the prefix of the first member to use as indentation reference
167
+ for (const stmt of body.statements) {
168
+ if (stmt.element?.prefix) return stmt.element.prefix;
169
+ }
170
+ return undefined;
171
+ }
172
+
173
+ private async transformMembers(body: J.Block, memberIndent: any, ctx: ExecutionContext): Promise<any[]> {
174
+ const members: any[] = [];
175
+
176
+ for (const stmt of body.statements) {
177
+ const s = stmt.element;
178
+
179
+ if (s.kind === J.Kind.MethodDeclaration) {
180
+ const method = s as J.MethodDeclaration;
181
+ const name = method.name.simpleName;
182
+
183
+ if (name === 'getInitialState') {
184
+ const stateInit = await this.convertGetInitialState(method, memberIndent, ctx);
185
+ if (stateInit) members.push({...stmt, element: stateInit});
186
+ } else if (name === 'getDefaultProps') {
187
+ const defaultProps = await this.convertGetDefaultProps(method, memberIndent, ctx);
188
+ if (defaultProps) members.push({...stmt, element: defaultProps});
189
+ } else if (LIFECYCLE_METHODS.has(name)) {
190
+ members.push(stmt);
191
+ } else {
192
+ const arrowProp = await this.convertToArrowProperty(method, memberIndent, ctx);
193
+ if (arrowProp) {
194
+ members.push({...stmt, element: arrowProp});
195
+ } else {
196
+ members.push(stmt);
197
+ }
198
+ }
199
+ } else if (s.kind === JS.Kind.PropertyAssignment) {
200
+ const prop = s as JS.PropertyAssignment;
201
+ const keyName = this.getPropertyKeyName(prop);
202
+
203
+ if (keyName === 'propTypes' || keyName === 'contextTypes' || keyName === 'childContextTypes') {
204
+ const staticProp = await this.convertToStaticProperty(prop, keyName, memberIndent, ctx);
205
+ if (staticProp) {
206
+ members.push({...stmt, element: staticProp});
207
+ } else {
208
+ members.push(stmt);
209
+ }
210
+ } else if (keyName === 'displayName' || keyName === 'statics' || keyName === 'mixins') {
211
+ // Skip displayName (inferred from class name), statics, mixins
212
+ } else {
213
+ members.push(stmt);
214
+ }
215
+ }
216
+ }
217
+
218
+ return members;
219
+ }
220
+
221
+ private async convertGetInitialState(method: J.MethodDeclaration, indent: any, ctx: ExecutionContext): Promise<J | undefined> {
222
+ if (!method.body) return undefined;
223
+ const stmts = method.body.statements;
224
+ if (stmts.length !== 1) return undefined;
225
+
226
+ const firstStmt = stmts[0].element;
227
+ if (firstStmt.kind !== J.Kind.Return) return undefined;
228
+
229
+ const returnStmt = firstStmt as J.Return;
230
+ if (!returnStmt.expression) return undefined;
231
+
232
+ const result = await template`state = ${returnStmt.expression}`.apply(method, this.cursor);
233
+ if (result && indent) {
234
+ return {...(result as any), prefix: indent};
235
+ }
236
+ return result;
237
+ }
238
+
239
+ private async convertGetDefaultProps(method: J.MethodDeclaration, indent: any, ctx: ExecutionContext): Promise<J | undefined> {
240
+ if (!method.body) return undefined;
241
+ const stmts = method.body.statements;
242
+ if (stmts.length !== 1) return undefined;
243
+
244
+ const firstStmt = stmts[0].element;
245
+ if (firstStmt.kind !== J.Kind.Return) return undefined;
246
+
247
+ const returnStmt = firstStmt as J.Return;
248
+ if (!returnStmt.expression) return undefined;
249
+
250
+ const result = await template`static defaultProps = ${returnStmt.expression}`.apply(method, this.cursor);
251
+ if (result && indent) {
252
+ return {...(result as any), prefix: indent};
253
+ }
254
+ return result;
255
+ }
256
+
257
+ private async convertToArrowProperty(method: J.MethodDeclaration, indent: any, ctx: ExecutionContext): Promise<J | undefined> {
258
+ if (!method.body) return undefined;
259
+
260
+ const name = method.name.simpleName;
261
+
262
+ const params = method.parameters?.elements ?? [];
263
+ let paramsSource = '';
264
+ if (params.length > 0) {
265
+ const paramStrs = await Promise.all(params.map(async (p: any) =>
266
+ (await printer(this.cursor).print(p.element)).trim()
267
+ ));
268
+ paramsSource = paramStrs.join(', ');
269
+ }
270
+
271
+ // Create stub with empty body, then splice in original body to preserve whitespace
272
+ const stub = await template`${raw(name)} = (${raw(paramsSource)}) => {}`.apply(method, this.cursor);
273
+ if (!stub) return undefined;
274
+
275
+ // Navigate into the J.Assignment -> JS.ArrowFunction -> J.Lambda to replace the empty body
276
+ const s = stub as any;
277
+ const result = {
278
+ ...s,
279
+ assignment: {
280
+ ...s.assignment,
281
+ element: {
282
+ ...s.assignment.element,
283
+ lambda: {
284
+ ...s.assignment.element.lambda,
285
+ body: method.body
286
+ }
287
+ }
288
+ }
289
+ };
290
+ if (result && indent) {
291
+ return {...(result as any), prefix: indent};
292
+ }
293
+ return result;
294
+ }
295
+
296
+
297
+
298
+ private async convertToStaticProperty(prop: JS.PropertyAssignment, name: string, indent: any, ctx: ExecutionContext): Promise<J | undefined> {
299
+ if (!prop.initializer) return undefined;
300
+
301
+ const result = await template`static ${raw(name)} = ${prop.initializer}`.apply(prop, this.cursor);
302
+ if (result && indent) {
303
+ return {...(result as any), prefix: indent};
304
+ }
305
+ return result;
306
+ }
307
+ }();
308
+ }
309
+ }