ts-morph-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # ts-morph-react
2
+
3
+ [![npm version](https://img.shields.io/npm/v/ts-morph-react.svg)](https://www.npmjs.com/package/ts-morph-react)
4
+ [![npm downloads](https://img.shields.io/npm/dm/ts-morph-react.svg)](https://www.npmjs.com/package/ts-morph-react)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A powerful collection of **AST transformers for ts-morph** to automate React code refactoring. Enforce consistent code patterns, modernize your codebase, and enforce best practices with declarative, composable transformers.
8
+
9
+ ## Features
10
+
11
+ - 🔄 **Enforce Direct Exports** - Convert separate export statements to direct exports on function declarations
12
+ - 🎯 **Function Components** - Automatically convert function declarations to arrow function components with proper typing
13
+ - 📦 **Named Imports** - Transform default imports to named imports for consistency
14
+ - 🎨 **Code Formatting** - Format code and organize imports according to your style guide
15
+ - ⚡ **Composable** - Mix and match transformers to create your refactoring pipeline
16
+ - 🛡️ **Type-Safe** - Built with TypeScript for a fully typed experience
17
+ - 🎭 **AST-Powered** - Leverage ts-morph for precise, reliable code transformations
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install ts-morph-react
23
+ # or
24
+ pnpm add ts-morph-react
25
+ # or
26
+ yarn add ts-morph-react
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ### As a Library
32
+
33
+ ```typescript
34
+ import { Project } from 'ts-morph'
35
+ import { transform } from 'ts-morph-react'
36
+
37
+ const project = new Project()
38
+ const sourceFile = project.addSourceFileAtPath('src/Button.tsx')
39
+
40
+ // Run transformers with your configuration
41
+ await transform(sourceFile, {
42
+ enforceDirectExports: true,
43
+ enforceFunctionComponent: true,
44
+ enforceNamedImports: true,
45
+ enforceFormat: true
46
+ })
47
+
48
+ // Save changes
49
+ await sourceFile.save()
50
+ ```
51
+
52
+ ## Transformers
53
+
54
+ ### enforceDirectExports
55
+
56
+ Converts separate export statement to direct exports.
57
+
58
+ ❌ **Before:**
59
+ ```tsx
60
+ function Button<ButtonProps>({ label }) {
61
+ return <button>{label}</button>
62
+ }
63
+
64
+ export { Button }
65
+ ```
66
+
67
+ ✅ **After:**
68
+ ```tsx
69
+ export function Button(props) {
70
+ return <button>{props.label}</button>
71
+ }
72
+ ```
73
+
74
+ ### enforceFunctionComponent
75
+
76
+ Converts plain function components to properly typed `React.FunctionComponent` components, preserving prop types.
77
+
78
+ ❌ **Before:**
79
+ ```tsx
80
+ function Button(props: ButtonProps) {
81
+ return <button>{props.label}</button>
82
+ }
83
+ ```
84
+
85
+ ✅ **After:**
86
+ ```tsx
87
+ const Button: React.FunctionComponent<ButtonProps> = (props) => {
88
+ return <button>{props.label}</button>
89
+ }
90
+ ```
91
+
92
+ ### enforceNamedImports
93
+
94
+ Transforms default imports to named imports for better tree-shaking and consistency.
95
+
96
+ ❌ **Before:**
97
+ ```tsx
98
+ import * as React from 'react'
99
+
100
+ export const Button: React.FunctionComponent<ButtonProps> = ({ label }) => {
101
+ return <button>{label}</button>
102
+ }
103
+ ```
104
+
105
+ ✅ **After:**
106
+ ```tsx
107
+ import { FunctionComponent } from 'react'
108
+
109
+ export const Button: FunctionComponent<ButtonProps> = ({ label }) => {
110
+ return <button>{label}</button>
111
+ }
112
+ ```
113
+
114
+ ### enforceFormat
115
+
116
+ Formats code and organizes imports according to your style guide. Respects all standard TypeScript formatting options.
117
+
118
+ **Usage:**
119
+ ```tsx
120
+ import { transform } from 'ts-morph-react'
121
+
122
+ await transform(sourceFile, {
123
+ enforceFormat: true,
124
+ format: {
125
+ indentSize: 2,
126
+ convertTabsToSpaces: true,
127
+ semicolons: ts.SemicolonPreference.Remove
128
+ }
129
+ })
130
+ ```
131
+
132
+ ❌ **Before:**
133
+ ```tsx
134
+ import * as React from 'react';
135
+
136
+ import { Text } from '@/components/Text'
137
+
138
+ export const Button: React.FunctionComponent<ButtonProps> = ({ label }) => {
139
+ return <button><Text>{label}</Text></button>;
140
+ };
141
+ ```
142
+
143
+ ✅ **After:**
144
+ ```tsx
145
+ import * as React from 'react'
146
+ import { Text } from '@/components/Text'
147
+
148
+ export const Button: React.FunctionComponent<ButtonProps> = ({ label }) => {
149
+ return <button><Text>{label}</Text></button>
150
+ }
151
+ ```
152
+
153
+ ## API Reference
154
+
155
+ ### `transform(sourceFile: SourceFile, config?: TransformerConfig)`
156
+
157
+ Applies transformers to a source file.
158
+
159
+ ```typescript
160
+ interface TransformerConfig {
161
+ enforceDirectExports: boolean
162
+ enforceFunctionComponent: boolean
163
+ enforceNamedImports: boolean
164
+ enforceFormat: boolean
165
+ format: {
166
+ baseIndentSize: number
167
+ convertTabsToSpaces: boolean
168
+ ensureNewLineAtEndOfFile: boolean
169
+ indentMultiLineObjectLiteralBeginningOnBlankLine: boolean
170
+ indentSize: number
171
+ indentStyle: IndentStyle
172
+ indentSwitchCase: boolean
173
+ insertSpaceAfterCommaDelimiter: boolean
174
+ insertSpaceAfterConstructor: boolean
175
+ insertSpaceAfterFunctionKeywordForAnonymousFunctions: boolean
176
+ insertSpaceAfterKeywordsInControlFlowStatements: boolean
177
+ insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: boolean
178
+ insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: boolean
179
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: boolean
180
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: boolean
181
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: boolean
182
+ insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: boolean
183
+ insertSpaceAfterSemicolonInForStatements: boolean
184
+ insertSpaceAfterTypeAssertion: boolean
185
+ insertSpaceBeforeAndAfterBinaryOperators: boolean
186
+ insertSpaceBeforeFunctionParenthesis: boolean
187
+ insertSpaceBeforeTypeAnnotation: boolean
188
+ newLineCharacter: string
189
+ placeOpenBraceOnNewLineForControlBlocks: boolean
190
+ placeOpenBraceOnNewLineForFunctions: boolean
191
+ semicolons: SemicolonPreference
192
+ tabSize: number
193
+ trimTrailingWhitespace: boolean
194
+ }
195
+ }
196
+ ```
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ # Build the library
202
+ pnpm build
203
+
204
+ # Watch mode during development
205
+ pnpm watch
206
+
207
+ # Run tests
208
+ pnpm test
209
+
210
+ # Watch mode for tests
211
+ pnpm test:watch
212
+
213
+ # Run tests with UI
214
+ pnpm test:ui
215
+
216
+ # Lint and type-check
217
+ pnpm lint
218
+
219
+ # Clean build artifacts
220
+ pnpm clean
221
+ ```
222
+
223
+ ### Testing
224
+
225
+ The project uses **vitest** with snapshot testing to ensure transformer behavior is consistent and intentional:
226
+
227
+ ```bash
228
+ # Run tests once
229
+ pnpm test
230
+
231
+ # Run in watch mode
232
+ pnpm test:watch
233
+
234
+ # Update snapshots after intentional changes
235
+ pnpm test -- -u
236
+
237
+ # Run specific test file
238
+ pnpm test enforceFormat
239
+ ```
240
+
241
+ ## When to Use ts-morph-react
242
+
243
+ ✅ **Good for:**
244
+ - Enforcing code patterns across your codebase
245
+ - Large-scale refactoring of React components
246
+ - Automating style guide compliance
247
+ - One-time migrations (class → function components, etc.)
248
+ - Building custom code generators and linters
249
+
250
+ ❌ **Not ideal for:**
251
+ - Real-time code formatting (use Prettier for that)
252
+ - Rename refactoring with complex scope analysis (use your IDE)
253
+ - Performance-critical transformations of very large codebases
254
+
255
+ ## Under the Hood
256
+
257
+ ts-morph-react is built on top of [ts-morph](https://ts-morph.com/), a fantastic library that provides a fluent API for manipulating TypeScript ASTs. If you need lower-level control, you can access the ts-morph APIs directly.
258
+
259
+ ## Contributing
260
+
261
+ Contributions are welcome! Please feel free to submit a Pull Request.
262
+
263
+ ## License
264
+
265
+ MIT © [Tobias Strebitzer](mailto:tobias.strebitzer@atomic.bi)
266
+
267
+ ## See Also
268
+
269
+ - [ts-morph](https://ts-morph.com/) - The underlying AST manipulation library
270
+ - [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) - For deeper TypeScript understanding
@@ -0,0 +1,53 @@
1
+ import { FormatCodeSettings, SourceFile, VariableDeclaration, FunctionDeclaration, Node, ts } from 'ts-morph';
2
+ import { PartialDeep } from 'type-fest';
3
+
4
+ interface TransformerConfig {
5
+ enforceDirectExports: boolean;
6
+ enforceFunctionComponent: boolean;
7
+ enforceNamedImports: boolean;
8
+ enforceFormat: boolean;
9
+ format: FormatCodeSettings;
10
+ }
11
+ declare function mergeConfig({ format, ...config }?: PartialDeep<TransformerConfig>): TransformerConfig;
12
+
13
+ interface TransformerParams {
14
+ sourceFile: SourceFile;
15
+ config: TransformerConfig;
16
+ }
17
+ type MatchFn = (params: TransformerParams) => boolean;
18
+ type TransformFn = (params: TransformerParams) => Promise<void>;
19
+ interface Transformer {
20
+ match: MatchFn;
21
+ transform: TransformFn;
22
+ }
23
+
24
+ declare const enforceDirectExports: Transformer;
25
+
26
+ declare const enforceFunctionComponent: Transformer;
27
+
28
+ declare const enforceNamedImports: Transformer;
29
+
30
+ declare function createTempSourceFile(filename: string): Promise<string>;
31
+
32
+ declare const REACT_TYPES: string[];
33
+ declare const REACT_HOOKS: string[];
34
+ declare const REACT_APIS: string[];
35
+ declare function isReactComponent(varDecl: VariableDeclaration): boolean;
36
+ declare function isForwardRefComponent(varDecl: VariableDeclaration): boolean;
37
+ declare function isFunctionComponent(func: FunctionDeclaration): boolean;
38
+
39
+ declare const transformers: Transformer[];
40
+ declare function transform(sourceFile: SourceFile, config?: PartialDeep<TransformerConfig>): Promise<void>;
41
+
42
+ declare enum SeparationIntent {
43
+ IGNORE = 0,
44
+ COMBINE = 1,
45
+ SEPARATE = 2
46
+ }
47
+ interface SeparationEntry {
48
+ range: [number, number];
49
+ intent: SeparationIntent;
50
+ }
51
+ declare function enforeLineSeparation(sourceFile: SourceFile, predicate: (cur: Node<ts.Node>, prev: Node<ts.Node>, triviaWidth: number) => SeparationIntent): void;
52
+
53
+ export { type MatchFn, REACT_APIS, REACT_HOOKS, REACT_TYPES, type SeparationEntry, SeparationIntent, type TransformFn, type Transformer, type TransformerConfig, type TransformerParams, createTempSourceFile, enforceDirectExports, enforceFunctionComponent, enforceNamedImports, enforeLineSeparation, isForwardRefComponent, isFunctionComponent, isReactComponent, mergeConfig, transform, transformers };
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import {SyntaxKind,Node,StructureKind,VariableDeclarationKind,ts,ImportDeclaration}from'ts-morph';import {mkdtemp}from'fs/promises';import {tmpdir}from'os';import {join}from'path';var y=["ComponentProps","ComponentPropsWithRef","ComponentPropsWithoutRef","FC","FunctionComponent","ReactNode","ReactElement","JSX","RefObject","MutableRefObject","CSSProperties","HTMLAttributes","SVGAttributes","DOMAttributes","PropsWithChildren","PropsWithRef"],N=["useState","useEffect","useCallback","useMemo","useRef","useContext","useReducer","useImperativeHandle","useLayoutEffect","useDebugValue","useTransition","useDeferredValue","useId","useSyncExternalStore"],R=["forwardRef","memo","lazy","createContext","createElement","cloneElement","isValidElement","Children","Fragment","StrictMode","Suspense"];function b(e){try{let t=e.getInitializer?.();if(!t)return !1;let r=t.getDescendantsOfKind?.(SyntaxKind.JsxElement)||[],o=t.getDescendantsOfKind?.(SyntaxKind.JsxSelfClosingElement)||[];if(r.length>0||o.length>0)return !0;if(Node.isArrowFunction(t)){let n=t.getBody?.();if(n){let i=n.getDescendantsOfKind?.(SyntaxKind.JsxElement)||[],a=n.getDescendantsOfKind?.(SyntaxKind.JsxSelfClosingElement)||[];return i.length>0||a.length>0}}return !1}catch{return false}}function I(e){try{let t=e.getInitializer?.();if(!t)return !1;let r=t.getText?.();return !!(r?.includes("forwardRef(")||r?.includes("React.forwardRef("))}catch{return false}}function T(e){try{let t=e.getDescendantsOfKind?.(SyntaxKind.JsxElement)||[],r=e.getDescendantsOfKind?.(SyntaxKind.JsxSelfClosingElement)||[];return t.length>0||r.length>0}catch{return false}}var A={match:({config:e})=>e.enforceDirectExports,transform:async({sourceFile:e})=>{e.getFunctions().forEach(t=>{if(!T(t)||t.isExported()&&t.hasExportKeyword())return;let r=t.getName(),o=false;e.getExportDeclarations().forEach(n=>{n.getNamedExports().some(a=>a.getName()===r)&&(o=true);}),o&&(t.setIsExported(true),e.getExportDeclarations().forEach(n=>{let a=n.getNamedExports().filter(s=>s.getName()===r);a.length>0&&(a.forEach(s=>{s.remove();}),n.getNamedExports().length===0&&n.remove());}));}),e.getVariableDeclarations().forEach(t=>{if(!b(t)||I(t))return;let r=t.getInitializer();if(!r||!Node.isArrowFunction(r))return;let o=t.getName(),n=t.getVariableStatement();if(!n)return;let i=n.hasExportKeyword(),a=false;e.getExportDeclarations().forEach(f=>{f.getNamedExports().some(E=>E.getName()===o)&&(a=true);});let s=i||a,p=r.getParameters().map(f=>f.getText()).join(", "),g=r.getBody();if(!g)return;let x=g.getText(),d,c=x.trim();c.startsWith("(")&&c.endsWith(")")?d=`{
2
+ return ${c.slice(1,-1).trim()}
3
+ }`:c.startsWith("{")?d=x:d=`{
4
+ return ${x}
5
+ }`;let u=`${s?"export ":""}function ${o}(${p}) ${d}`;n.replaceWithText(u),a&&e.getExportDeclarations().forEach(f=>{let E=f.getNamedExports().filter(S=>S.getName()===o);E.length>0&&(E.forEach(S=>{S.remove();}),f.getNamedExports().length===0&&f.remove());});});}};var D={match:({config:e})=>e.enforceFunctionComponent,transform:async({sourceFile:e,config:{enforceDirectExports:t}})=>{e.getFunctions().forEach(r=>{if(!T(r))return;let o=r.getName();if(!o)return;let n=r.isExported(),i=false;e.getExportDeclarations().forEach(c=>{c.getNamedExports().some(f=>f.getName()===o)&&(i=true);});let a=t&&(n||i),m=r.getParameters()[0],g=(m.getTypeNode()??m.getType()).getText();m.removeType();let x=g==="any"?"React.FunctionComponent":`React.FunctionComponent<${g}>`,d=r.getChildIndex();e.insertVariableStatement(d,{declarations:[{name:o,type:x,initializer:c=>{c.write("(").write(r.getParameters().map(C=>C.getText()).join(", ")).write(")");let u=r.getReturnTypeNode()?.getText();u&&c.write(`: ${u}`),c.write(" => ");let f=r.getBody()?.getText()??"{}";c.block(()=>{c.write(f.replace(/^{|}$/g,"").trim());});}}],declarationKind:VariableDeclarationKind.Const,kind:StructureKind.VariableStatement,isExported:a}),r.remove();});}};var P={match:({config:e})=>e.enforceNamedImports,transform:async({sourceFile:e})=>{let t=e.getImportDeclarations().find(i=>i.getModuleSpecifier().getLiteralValue()==="react");if(!t||!t.getNamespaceImport())return;let o=new Set,n=new Set;if(e.forEachDescendant(i=>{if(i.getKind()===SyntaxKind.TypeReference){let a=i.asKind(SyntaxKind.TypeReference);if(!a)return;let s=a.getTypeName();if(Node.isQualifiedName(s)){let m=s.getLeft().getText(),p=s.getRight().getText();m==="React"&&y.includes(p)&&(s.replaceWithText(p),o.add(p));}}if(i.getKind()===SyntaxKind.PropertyAccessExpression){let a=i.asKind(SyntaxKind.PropertyAccessExpression);if(!a)return;let s=a.getExpression(),m=a.getName();s.getText()==="React"&&(N.includes(m)||R.includes(m)||y.includes(m))&&(a.replaceWithText(m),y.includes(m)?o.add(m):(N.includes(m)||R.includes(m))&&n.add(m));}}),o.size>0||n.size>0){t.removeNamespaceImport();let i=Array.from(o).sort();for(let s of i)t.addNamedImport({name:s,isTypeOnly:true});let a=Array.from(n).sort();for(let s of a)t.addNamedImport(s);}}};function O({format:e,...t}={}){return {enforceDirectExports:false,enforceFunctionComponent:false,enforceNamedImports:false,enforceFormat:false,format:{indentSize:2,convertTabsToSpaces:true,indentStyle:ts.IndentStyle.Smart,indentMultiLineObjectLiteralBeginningOnBlankLine:true,ensureNewLineAtEndOfFile:true,semicolons:ts.SemicolonPreference.Remove,trimTrailingWhitespace:true,...e},...t}}async function fe(e){let t=await mkdtemp(join(tmpdir(),"ts-morph-react-"));return join(t,e)}var w=(o=>(o[o.IGNORE=0]="IGNORE",o[o.COMBINE=1]="COMBINE",o[o.SEPARATE=2]="SEPARATE",o))(w||{});function V(e,t){let r=[],o=e.getChildSyntaxListOrThrow();for(let n=o.getChildCount()-1;n>0;n-=1){let i=o.getChildAtIndex(n-1),a=o.getChildAtIndex(n),s=t(a,i,a.getLeadingTriviaWidth());r.push({range:[i.getEnd(),a.getStart()],intent:s});}r.forEach(({range:n,intent:i})=>{i===1?e.replaceText(n,`
6
+ `):i===2&&e.replaceText(n,`
7
+
8
+ `);});}var L={match:({config:e})=>e.enforceFormat,transform:async({sourceFile:e,config:{format:t}})=>{e.formatText(t),V(e,(r,o)=>o instanceof ImportDeclaration&&r instanceof ImportDeclaration?1:2),e.organizeImports(t);}};var _=[A,D,P,L];async function Se(e,t={}){let r={config:O(t),sourceFile:e};for(let o of _)o.match(r)&&await o.transform(r);}export{R as REACT_APIS,N as REACT_HOOKS,y as REACT_TYPES,w as SeparationIntent,fe as createTempSourceFile,A as enforceDirectExports,D as enforceFunctionComponent,P as enforceNamedImports,V as enforeLineSeparation,I as isForwardRefComponent,T as isFunctionComponent,b as isReactComponent,O as mergeConfig,Se as transform,_ as transformers};//# sourceMappingURL=index.js.map
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/react.ts","../src/transformers/enforceDirectExports.ts","../src/transformers/enforceFunctionComponent.ts","../src/transformers/enforceNamedImports.ts","../src/utils/config.ts","../src/utils/file.ts","../src/utils/trivia.ts","../src/transformers/enforceFormat.ts","../src/utils/transform.ts"],"names":["REACT_TYPES","REACT_HOOKS","REACT_APIS","isReactComponent","varDecl","initializer","descendants","SyntaxKind","selfClosing","Node","body","bodyDescendants","bodySelfClosing","isForwardRefComponent","text","isFunctionComponent","func","enforceDirectExports","config","sourceFile","funcName","isExportedViaSeparateStatement","exportDecl","exp","matchingExports","namedExport","varName","varStatement","isExportedViaKeyword","isExported","paramsText","p","bodyText","functionBody","trimmedBody","funcText","enforceFunctionComponent","param","typeText","typeAnnotation","index","writer","returnType","VariableDeclarationKind","StructureKind","enforceNamedImports","reactImport","importDeclaration","typeImportsNeeded","valueImportsNeeded","node","typeRef","typeName","left","right","propAccess","expr","name","typeImportsArray","valueImportsArray","valueName","mergeConfig","format","ts","createTempSourceFile","filename","dir","mkdtemp","join","tmpdir","SeparationIntent","enforeLineSeparation","predicate","instructions","syntaxList","prev","cur","intent","range","enforceFormat","ImportDeclaration","transformers","transform","params","transformer"],"mappings":"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","file":"index.js","sourcesContent":["import { FunctionDeclaration, Node, SyntaxKind, VariableDeclaration } from 'ts-morph'\n\n// React types that should be imported as named imports\nexport const REACT_TYPES = [\n 'ComponentProps',\n 'ComponentPropsWithRef',\n 'ComponentPropsWithoutRef',\n 'FC',\n 'FunctionComponent',\n 'ReactNode',\n 'ReactElement',\n 'JSX',\n 'RefObject',\n 'MutableRefObject',\n 'CSSProperties',\n 'HTMLAttributes',\n 'SVGAttributes',\n 'DOMAttributes',\n 'PropsWithChildren',\n 'PropsWithRef'\n]\n\n// React hooks that should be imported as named imports\nexport const REACT_HOOKS = [\n 'useState',\n 'useEffect',\n 'useCallback',\n 'useMemo',\n 'useRef',\n 'useContext',\n 'useReducer',\n 'useImperativeHandle',\n 'useLayoutEffect',\n 'useDebugValue',\n 'useTransition',\n 'useDeferredValue',\n 'useId',\n 'useSyncExternalStore'\n]\n\n// React APIs that should be imported as named imports\nexport const REACT_APIS = [\n 'forwardRef',\n 'memo',\n 'lazy',\n 'createContext',\n 'createElement',\n 'cloneElement',\n 'isValidElement',\n 'Children',\n 'Fragment',\n 'StrictMode',\n 'Suspense'\n]\n\n// Helper to detect if a variable declaration is a React component\nexport function isReactComponent(varDecl: VariableDeclaration): boolean {\n try {\n const initializer = varDecl.getInitializer?.()\n if (!initializer) return false\n\n // Check if the initializer contains JSX\n const descendants = initializer.getDescendantsOfKind?.(SyntaxKind.JsxElement) || []\n const selfClosing = initializer.getDescendantsOfKind?.(SyntaxKind.JsxSelfClosingElement) || []\n\n if (descendants.length > 0 || selfClosing.length > 0) { return true }\n\n // Check if it's assigned to an arrow function that returns JSX\n if (Node.isArrowFunction(initializer)) {\n const body = initializer.getBody?.()\n if (body) {\n const bodyDescendants = body.getDescendantsOfKind?.(SyntaxKind.JsxElement) || []\n const bodySelfClosing = body.getDescendantsOfKind?.(SyntaxKind.JsxSelfClosingElement) || []\n return bodyDescendants.length > 0 || bodySelfClosing.length > 0\n }\n }\n\n return false\n } catch {\n return false\n }\n}\n\n// Helper to check if a variable is a forwardRef component\nexport function isForwardRefComponent(varDecl: VariableDeclaration): boolean {\n try {\n const initializer = varDecl.getInitializer?.()\n if (!initializer) return false\n\n const text = initializer.getText?.()\n return text?.includes('forwardRef(') || text?.includes('React.forwardRef(') ? true : false\n } catch {\n return false\n }\n}\n\n// Helper to check if a function is a React component (returns JSX)\nexport function isFunctionComponent(func: FunctionDeclaration): boolean {\n try {\n const descendants = func.getDescendantsOfKind?.(SyntaxKind.JsxElement) || []\n const selfClosing = func.getDescendantsOfKind?.(SyntaxKind.JsxSelfClosingElement) || []\n if (descendants.length > 0 || selfClosing.length > 0) { return true }\n return false\n } catch {\n return false\n }\n}\n","import { Node } from 'ts-morph'\nimport { isForwardRefComponent, isFunctionComponent, isReactComponent } from '../utils/react'\nimport { Transformer } from '../utils/transformer'\n\nexport const enforceDirectExports: Transformer = {\n match: ({ config }) => config.enforceDirectExports,\n transform: async ({ sourceFile }) => {\n // Transform function declarations: add direct export if it's a component\n sourceFile.getFunctions().forEach((func) => {\n // Skip if not a component\n if (!isFunctionComponent(func)) { return }\n\n // Skip if already exported directly\n if (func.isExported() && func.hasExportKeyword()) { return }\n\n const funcName = func.getName()\n\n // Check if it's exported via separate export statement\n let isExportedViaSeparateStatement = false\n sourceFile.getExportDeclarations().forEach((exportDecl) => {\n const namedExports = exportDecl.getNamedExports()\n if (namedExports.some((exp) => exp.getName() === funcName)) {\n isExportedViaSeparateStatement = true\n }\n })\n\n // Only convert if it's a component that should be exported\n if (isExportedViaSeparateStatement) {\n // Add direct export modifier\n func.setIsExported(true)\n\n // Remove from separate export statements\n sourceFile.getExportDeclarations().forEach((exportDecl) => {\n const namedExports = exportDecl.getNamedExports()\n const matchingExports = namedExports.filter((namedExport) => namedExport.getName() === funcName)\n if (matchingExports.length > 0) {\n matchingExports.forEach((namedExport) => { namedExport.remove() })\n if (exportDecl.getNamedExports().length === 0) { exportDecl.remove() }\n }\n })\n }\n })\n\n // Transform const arrow functions to function declarations (if they're components and not forwardRef)\n sourceFile.getVariableDeclarations().forEach((varDecl) => {\n // Skip if not a component\n if (!isReactComponent(varDecl)) return\n\n // Skip forwardRef and memo patterns\n if (isForwardRefComponent(varDecl)) return\n\n const initializer = varDecl.getInitializer()\n if (!initializer || !Node.isArrowFunction(initializer)) return\n\n const varName = varDecl.getName()\n const varStatement = varDecl.getVariableStatement()\n if (!varStatement) return\n\n const isExportedViaKeyword = varStatement.hasExportKeyword()\n\n // Check if it's exported via separate export statement\n let isExportedViaSeparateStatement = false\n sourceFile.getExportDeclarations().forEach((exportDecl) => {\n const namedExports = exportDecl.getNamedExports()\n if (namedExports.some((exp) => exp.getName() === varName)) {\n isExportedViaSeparateStatement = true\n }\n })\n\n const isExported = isExportedViaKeyword || isExportedViaSeparateStatement\n\n // Get the parameters\n const params = initializer.getParameters()\n const paramsText = params.map((p) => p.getText()).join(', ')\n\n // Get the body\n const body = initializer.getBody()\n if (!body) return\n\n const bodyText = body.getText()\n\n // Build the function body\n let functionBody: string\n const trimmedBody = bodyText.trim()\n\n if (trimmedBody.startsWith('(') && trimmedBody.endsWith(')')) {\n // Parenthesized expression like: (\n // <button />\n // )\n const innerBody = trimmedBody.slice(1, -1).trim()\n functionBody = `{\\n return ${innerBody}\\n }`\n } else if (trimmedBody.startsWith('{')) {\n // Already a block statement\n functionBody = bodyText\n } else {\n // Simple JSX expression like: <button />\n functionBody = `{\\n return ${bodyText}\\n }`\n }\n\n const funcText = `${isExported ? 'export ' : ''}function ${varName}(${paramsText}) ${functionBody}`\n\n // Replace the variable statement with function declaration\n varStatement.replaceWithText(funcText)\n\n // Remove from separate export statements if it was exported that way\n if (isExportedViaSeparateStatement) {\n sourceFile.getExportDeclarations().forEach((exportDecl) => {\n const namedExports = exportDecl.getNamedExports()\n const matchingExports = namedExports.filter((namedExport) => namedExport.getName() === varName)\n\n if (matchingExports.length > 0) {\n matchingExports.forEach((namedExport) => {\n namedExport.remove()\n })\n\n // Remove the entire export declaration if no exports remain\n if (exportDecl.getNamedExports().length === 0) {\n exportDecl.remove()\n }\n }\n })\n }\n })\n }\n}\n","import { StructureKind, VariableDeclarationKind } from 'ts-morph'\nimport { isFunctionComponent } from '../utils/react'\nimport { Transformer } from '../utils/transformer'\n\nexport const enforceFunctionComponent: Transformer = {\n match: ({ config }) => config.enforceFunctionComponent,\n transform: async ({ sourceFile, config: { enforceDirectExports } }) => {\n sourceFile.getFunctions().forEach((func) => {\n if (!isFunctionComponent(func)) { return }\n\n const funcName = func.getName()\n if (!funcName) { return }\n\n const isExportedViaKeyword = func.isExported()\n let isExportedViaSeparateStatement = false\n\n sourceFile.getExportDeclarations().forEach((exportDecl) => {\n const namedExports = exportDecl.getNamedExports()\n if (namedExports.some((exp) => exp.getName() === funcName)) {\n isExportedViaSeparateStatement = true\n }\n })\n\n const isExported = enforceDirectExports && (isExportedViaKeyword || isExportedViaSeparateStatement)\n\n // Get the parameters with their type annotations\n const params = func.getParameters()\n const param = params[0]\n const type = param.getTypeNode() ?? param.getType()\n const typeText = type.getText()\n param.removeType()\n const typeAnnotation = typeText === 'any' ? 'React.FunctionComponent' : `React.FunctionComponent<${typeText}>`\n\n const index = func.getChildIndex()\n sourceFile.insertVariableStatement(index, {\n declarations: [{\n name: funcName,\n type: typeAnnotation,\n initializer: (writer) => {\n writer.write('(').write(func.getParameters().map((p) => p.getText()).join(', ')).write(')')\n const returnType = func.getReturnTypeNode()?.getText()\n if (returnType) { writer.write(`: ${returnType}`) }\n writer.write(' => ')\n const bodyText = func.getBody()?.getText() ?? '{}'\n writer.block(() => { writer.write(bodyText.replace(/^{|}$/g, '').trim()) })\n }\n }],\n declarationKind: VariableDeclarationKind.Const,\n kind: StructureKind.VariableStatement,\n isExported\n })\n func.remove()\n })\n }\n}\n","import { Node, SyntaxKind } from 'ts-morph'\nimport { REACT_APIS, REACT_HOOKS, REACT_TYPES } from '../utils/react'\nimport { Transformer } from '../utils/transformer'\n\nexport const enforceNamedImports: Transformer = {\n match: ({ config }) => config.enforceNamedImports,\n transform: async ({ sourceFile }) => {\n // Find React import declaration\n const reactImport = sourceFile.getImportDeclarations().find((importDeclaration) => {\n return importDeclaration.getModuleSpecifier().getLiteralValue() === 'react'\n })\n\n // If no React import exists, no transformation needed\n if (!reactImport) { return }\n\n // Check if it's already using named imports only (early exit if enforceNamedImports is disabled)\n const namespaceImport = reactImport.getNamespaceImport()\n if (!namespaceImport) { return }\n\n // Initialize tracking sets\n const typeImportsNeeded = new Set<string>()\n const valueImportsNeeded = new Set<string>()\n\n // ============================================================\n // Phase 2 & 3: Transform Type References and Hook Calls\n // (Single-pass traversal for efficiency)\n // ============================================================\n\n sourceFile.forEachDescendant((node) => {\n // Handle TypeReference nodes (Phase 2)\n if (node.getKind() === SyntaxKind.TypeReference) {\n const typeRef = node.asKind(SyntaxKind.TypeReference)\n if (!typeRef) return\n\n const typeName = typeRef.getTypeName()\n\n // Check if it's a qualified name like React.ComponentProps or React.HTMLAttributes\n if (Node.isQualifiedName(typeName)) {\n const left = typeName.getLeft().getText()\n const right = typeName.getRight().getText()\n\n if (left === 'React') {\n // Check if it's a known React type\n if (REACT_TYPES.includes(right)) {\n typeName.replaceWithText(right)\n typeImportsNeeded.add(right)\n }\n }\n }\n }\n\n // Handle PropertyAccessExpression nodes (Phase 3)\n // This handles both React.Component and React.SomethingRef patterns\n if (node.getKind() === SyntaxKind.PropertyAccessExpression) {\n const propAccess = node.asKind(SyntaxKind.PropertyAccessExpression)\n if (!propAccess) return\n\n const expr = propAccess.getExpression()\n const name = propAccess.getName()\n\n if (expr.getText() === 'React') {\n // Check if it's a known hook or API\n if (REACT_HOOKS.includes(name) || REACT_APIS.includes(name) || REACT_TYPES.includes(name)) {\n propAccess.replaceWithText(name)\n\n // Determine if it's a type or value import\n if (REACT_TYPES.includes(name)) {\n typeImportsNeeded.add(name)\n } else if (REACT_HOOKS.includes(name) || REACT_APIS.includes(name)) {\n valueImportsNeeded.add(name)\n }\n }\n }\n }\n })\n\n // ============================================================\n // Phase 4: Import Management\n // ============================================================\n\n // Only add named imports and remove namespace if we have imports to add\n if (typeImportsNeeded.size > 0 || valueImportsNeeded.size > 0) {\n // Remove namespace import FIRST (before adding named imports)\n reactImport.removeNamespaceImport()\n\n // Add type imports (with 'type' keyword for type-only imports)\n const typeImportsArray = Array.from(typeImportsNeeded).sort()\n for (const typeName of typeImportsArray) {\n reactImport.addNamedImport({\n name: typeName,\n isTypeOnly: true\n })\n }\n\n // Add value imports\n const valueImportsArray = Array.from(valueImportsNeeded).sort()\n for (const valueName of valueImportsArray) {\n reactImport.addNamedImport(valueName)\n }\n }\n }\n}\n","import { FormatCodeSettings, ts } from 'ts-morph'\nimport { PartialDeep } from 'type-fest'\n\nexport interface TransformerConfig {\n enforceDirectExports: boolean\n enforceFunctionComponent: boolean\n enforceNamedImports: boolean\n enforceFormat: boolean\n format: FormatCodeSettings\n\n}\n\nexport function mergeConfig({ format, ...config }: PartialDeep<TransformerConfig> = {}): TransformerConfig {\n return {\n enforceDirectExports: false,\n enforceFunctionComponent: false,\n enforceNamedImports: false,\n enforceFormat: false,\n format: {\n indentSize: 2,\n convertTabsToSpaces: true,\n indentStyle: ts.IndentStyle.Smart,\n indentMultiLineObjectLiteralBeginningOnBlankLine: true,\n ensureNewLineAtEndOfFile: true,\n semicolons: ts.SemicolonPreference.Remove,\n trimTrailingWhitespace: true,\n ...format\n },\n ...config\n }\n}\n","import { mkdtemp } from 'fs/promises'\nimport { tmpdir } from 'os'\nimport { join } from 'path'\n\nexport async function createTempSourceFile(filename: string) {\n const dir = await mkdtemp(join(tmpdir(), 'ts-morph-react-'))\n return join(dir, filename)\n}\n","import { Node, SourceFile, ts } from 'ts-morph'\n\nexport enum SeparationIntent { IGNORE, COMBINE, SEPARATE }\n\nexport interface SeparationEntry {\n range: [number, number]\n intent: SeparationIntent\n}\n\nexport function enforeLineSeparation(sourceFile: SourceFile, predicate: (cur: Node<ts.Node>, prev: Node<ts.Node>, triviaWidth: number) => SeparationIntent) {\n const instructions: SeparationEntry[] = []\n const syntaxList = sourceFile.getChildSyntaxListOrThrow()\n for (let index = syntaxList.getChildCount() - 1; index > 0; index -= 1) {\n const prev = syntaxList.getChildAtIndex(index - 1)\n const cur = syntaxList.getChildAtIndex(index)\n const intent = predicate(cur, prev, cur.getLeadingTriviaWidth())\n instructions.push({ range: [prev.getEnd(), cur.getStart()], intent })\n }\n instructions.forEach(({ range, intent }) => {\n if (intent === SeparationIntent.COMBINE) {\n sourceFile.replaceText(range, '\\n')\n } else if (intent === SeparationIntent.SEPARATE) {\n sourceFile.replaceText(range, '\\n\\n')\n }\n })\n}\n","import { ImportDeclaration } from 'ts-morph'\nimport { Transformer } from '../utils/transformer'\nimport { enforeLineSeparation, SeparationIntent } from '../utils/trivia'\n\nexport const enforceFormat: Transformer = {\n match: ({ config }) => config.enforceFormat,\n transform: async ({ sourceFile, config: { format } }) => {\n sourceFile.formatText(format)\n enforeLineSeparation(sourceFile, (cur, prev) => (prev instanceof ImportDeclaration && cur instanceof ImportDeclaration)\n ? SeparationIntent.COMBINE\n : SeparationIntent.SEPARATE\n )\n sourceFile.organizeImports(format)\n }\n}\n","import { SourceFile } from 'ts-morph'\nimport { PartialDeep } from 'type-fest'\nimport { enforceDirectExports } from '../transformers/enforceDirectExports'\nimport { enforceFormat } from '../transformers/enforceFormat'\nimport { enforceFunctionComponent } from '../transformers/enforceFunctionComponent'\nimport { enforceNamedImports } from '../transformers/enforceNamedImports'\nimport { mergeConfig, TransformerConfig } from './config'\nimport { TransformerParams } from './transformer'\n\nexport const transformers = [enforceDirectExports, enforceFunctionComponent, enforceNamedImports, enforceFormat]\n\nexport async function transform(\n sourceFile: SourceFile,\n config: PartialDeep<TransformerConfig> = {}\n): Promise<void> {\n const params: TransformerParams = { config: mergeConfig(config), sourceFile }\n for (const transformer of transformers) {\n if (transformer.match(params)) {\n await transformer.transform(params)\n }\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "ts-morph-react",
3
+ "version": "1.0.0",
4
+ "description": "A collection of transformers for ts-morph to refactor react code",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Tobias Strebitzer",
8
+ "email": "tobias.strebitzer@atomic.bi"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+ssh://git@github.com/atomicbi/ts-morph-react.git"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "bin": "./dist/cli.js",
25
+ "dependencies": {
26
+ "ts-morph": "^27.0.2"
27
+ },
28
+ "devDependencies": {
29
+ "@base-ui/react": "^1.0.0",
30
+ "@eslint/js": "^9.39.2",
31
+ "@stylistic/eslint-plugin": "^5.6.1",
32
+ "@types/node": "^25.0.3",
33
+ "cli-highlight": "^2.1.11",
34
+ "eslint": "^9.39.2",
35
+ "eslint-plugin-jsx-a11y": "^6.10.2",
36
+ "eslint-plugin-react": "^7.37.5",
37
+ "eslint-plugin-react-hooks": "^7.0.1",
38
+ "react": "^19.2.3",
39
+ "rimraf": "^6.0.1",
40
+ "tsup": "^8.5.0",
41
+ "tsx": "^4.21.0",
42
+ "type-fest": "^4.41.0",
43
+ "typescript": "^5.9.2",
44
+ "typescript-eslint": "^8.50.1",
45
+ "vite-tsconfig-paths": "^6.0.3",
46
+ "vitest": "^4.0.16"
47
+ },
48
+ "scripts": {
49
+ "clean": "rimraf dist",
50
+ "build": "tsup",
51
+ "watch": "tsup --watch",
52
+ "lint": "eslint 'src/**/*.ts' && tsc --noEmit",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest --watch",
55
+ "test:ui": "vitest --ui"
56
+ }
57
+ }