diff --git a/nx.json b/nx.json
index 305eff2..98820b5 100644
--- a/nx.json
+++ b/nx.json
@@ -56,6 +56,15 @@
"devTargetName": "dev",
"serveStaticTargetName": "serve-static"
}
+ },
+ {
+ "plugin": "@nx/react/plugin",
+ "options": {
+ "startTargetName": "start",
+ "buildTargetName": "build",
+ "devTargetName": "dev",
+ "serveStaticTargetName": "serve-static"
+ }
}
],
"generators": {
diff --git a/package.json b/package.json
index 8db1aad..aad89a2 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"@nx/next": "19.3.0",
"@nx/vite": "19.3.0",
"@nx/web": "19.3.0",
+ "@nx/react": "19.3.0",
"@swc-node/register": "~1.9.1",
"@swc/core": "~1.5.7",
"@swc/helpers": "~0.5.11",
diff --git a/packages/react/.eslintrc.json b/packages/react/.eslintrc.json
new file mode 100644
index 0000000..0dc93dd
--- /dev/null
+++ b/packages/react/.eslintrc.json
@@ -0,0 +1,30 @@
+{
+ "extends": ["../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.json"],
+ "parser": "jsonc-eslint-parser",
+ "rules": {
+ "@nx/dependency-checks": [
+ "error",
+ {
+ "ignoredFiles": ["{projectRoot}/vite.config.{js,ts,mjs,mts}"]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/react/README.md b/packages/react/README.md
new file mode 100644
index 0000000..64c64a4
--- /dev/null
+++ b/packages/react/README.md
@@ -0,0 +1,69 @@
+# @mutates/react
+
+🌟 **@mutates/react** is a specialized package within the Mutates toolset, offering powerful tools to mutate the Abstract Syntax Tree (AST) of React projects. Built on top of `@mutates/core`, this package provides React-specific transformations, making it easier to work with React components, hooks, and more.
+
+[![](https://raw.githubusercontent.com/IKatsuba/mutates/main/docs/src/app/opengraph-image.png)](https://mutates.katsuba.dev)
+
+## Features
+
+- **React-Specific Transformations:** Modify the AST of React components, hooks, and other files.
+- **Seamless Integration:** Works in conjunction with `@mutates/core` for a smooth development experience.
+- **Efficient:** Designed to handle the unique structure and requirements of React projects.
+
+## Installation
+
+To install the React package, use the following command:
+
+```sh
+npm install @mutates/react @mutates/core
+```
+
+## Usage
+
+### Basic Example
+
+Here is a simple example demonstrating how to use `@mutates/react` to modify a React component:
+
+```typescript
+import { addHooks, getComponents } from '@mutates/react';
+import { createProject, createSourceFile, saveProject } from '@mutates/core';
+
+// Initialize a new React project
+createProject();
+
+// Add a React component file to the project
+createSourceFile(
+ 'App.tsx',
+ `
+ import React from 'react';
+
+ const App: React.FC = () => {
+ return
Hello, World!
;
+ };
+
+ export default App;
+`,
+);
+
+// Perform some React-specific transformations
+addHooks(getComponents('App.tsx').at(0)!, ['useEffect']);
+
+// Save the modified file
+saveProject();
+```
+
+## API Reference
+
+For a comprehensive guide on the available APIs and their usage, please refer to the [official documentation](https://mutates.katsuba.dev/packages/react)
+
+## Contributing
+
+🤝 Contributions are welcome! If you have any improvements or suggestions, feel free to open an issue or submit a pull request.
+
+## License
+
+📄 @mutates/react is licensed under the Apache-2.0 License. See the [LICENSE](https://github.com/ikatsuba/mutates/blob/main/LICENSE) file for more information.
+
+---
+
+For further assistance or to report issues, please visit our [GitHub repository](https://github.com/ikatsuba/mutates).
diff --git a/packages/react/package.json b/packages/react/package.json
new file mode 100644
index 0000000..0d2fee6
--- /dev/null
+++ b/packages/react/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@mutates/react",
+ "version": "0.0.0-development",
+ "keywords": [
+ "typescript",
+ "ast",
+ "mutations",
+ "react"
+ ],
+ "homepage": "https://github.com/IKatsuba/mutates",
+ "repository": "https://github.com/IKatsuba/mutates",
+ "license": "Apache-2.0",
+ "contributors": [
+ "Igor Katsuba "
+ ],
+ "type": "commonjs",
+ "main": "./src/index.js",
+ "typings": "./src/index.d.ts",
+ "dependencies": {
+ "@mutates/core": "0.0.0-development",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0",
+ "ts-morph": ">=22.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "authors": [
+ "Igor Katsuba "
+ ]
+}
diff --git a/packages/react/project.json b/packages/react/project.json
new file mode 100644
index 0000000..ef01ccb
--- /dev/null
+++ b/packages/react/project.json
@@ -0,0 +1,36 @@
+{
+ "name": "react",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "packages/react/src",
+ "projectType": "library",
+ "release": {
+ "version": {
+ "generatorOptions": {
+ "packageRoot": "dist/{projectRoot}",
+ "currentVersionResolver": "git-tag"
+ }
+ }
+ },
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@nx/js:tsc",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/packages/react",
+ "main": "packages/react/src/index.ts",
+ "tsConfig": "packages/react/tsconfig.lib.json",
+ "assets": ["packages/react/*.md"],
+ "additionalEntryPoints": ["packages/react/src/testing.ts"],
+ "generateExportsField": true
+ }
+ },
+ "test": {
+ "executor": "@nx/vite:test",
+ "outputs": ["{options.reportsDirectory}"],
+ "options": {
+ "reportsDirectory": "../../coverage/packages/react"
+ }
+ }
+ }
+}
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
new file mode 100644
index 0000000..f41a696
--- /dev/null
+++ b/packages/react/src/index.ts
@@ -0,0 +1 @@
+export * from './lib';
diff --git a/packages/react/src/lib/component/get-components.ts b/packages/react/src/lib/component/get-components.ts
new file mode 100644
index 0000000..abe84bc
--- /dev/null
+++ b/packages/react/src/lib/component/get-components.ts
@@ -0,0 +1,12 @@
+import { getClasses, Pattern, Query, StructureType } from '@mutates/core';
+
+export function getComponents(
+ pattern: Pattern,
+ query?: Query, 'kind'>>,
+): ClassDeclaration[] {
+ return getClasses(pattern, query).filter(isComponent);
+}
+
+export function isComponent(declaration: ClassDeclaration): boolean {
+ return !!declaration.getDecorator('Component');
+}
diff --git a/packages/react/src/lib/component/index.ts b/packages/react/src/lib/component/index.ts
new file mode 100644
index 0000000..641c2e7
--- /dev/null
+++ b/packages/react/src/lib/component/index.ts
@@ -0,0 +1 @@
+export * from './get-components';
diff --git a/packages/react/src/lib/create-react-project.ts b/packages/react/src/lib/create-react-project.ts
new file mode 100644
index 0000000..2a0b91a
--- /dev/null
+++ b/packages/react/src/lib/create-react-project.ts
@@ -0,0 +1,5 @@
+import { createProject } from '@mutates/core';
+
+export function createReactProject() {
+ return createProject();
+}
diff --git a/packages/react/src/lib/hooks/get-hooks.ts b/packages/react/src/lib/hooks/get-hooks.ts
new file mode 100644
index 0000000..d9e83bc
--- /dev/null
+++ b/packages/react/src/lib/hooks/get-hooks.ts
@@ -0,0 +1,12 @@
+import { getFunctions, Pattern, Query, StructureType } from '@mutates/core';
+
+export function getHooks(
+ pattern: Pattern,
+ query?: Query, 'kind'>>,
+): FunctionDeclaration[] {
+ return getFunctions(pattern, query).filter(isHook);
+}
+
+export function isHook(declaration: FunctionDeclaration): boolean {
+ return declaration.getName()?.startsWith('use') ?? false;
+}
diff --git a/packages/react/src/lib/hooks/index.ts b/packages/react/src/lib/hooks/index.ts
new file mode 100644
index 0000000..1cd681c
--- /dev/null
+++ b/packages/react/src/lib/hooks/index.ts
@@ -0,0 +1 @@
+export * from './get-hooks';
diff --git a/packages/react/src/lib/react-tree-file-system.ts b/packages/react/src/lib/react-tree-file-system.ts
new file mode 100644
index 0000000..02caf43
--- /dev/null
+++ b/packages/react/src/lib/react-tree-file-system.ts
@@ -0,0 +1,103 @@
+import { FileSystemHost, RuntimeDirEntry } from 'ts-morph';
+
+export class ReactTreeFileSystem implements FileSystemHost {
+ isCaseSensitive(): boolean {
+ return true;
+ }
+
+ async delete(path: string): Promise {
+ // Implement delete logic
+ }
+
+ deleteSync(path: string): void {
+ // Implement deleteSync logic
+ }
+
+ readDirSync(dirPath: string): RuntimeDirEntry[] {
+ // Implement readDirSync logic
+ return [];
+ }
+
+ async readFile(filePath: string, encoding?: string): Promise {
+ // Implement readFile logic
+ return '';
+ }
+
+ readFileSync(filePath: string, encoding?: string): string {
+ // Implement readFileSync logic
+ return '';
+ }
+
+ async writeFile(filePath: string, fileText: string): Promise {
+ // Implement writeFile logic
+ }
+
+ writeFileSync(filePath: string, fileText: string): void {
+ // Implement writeFileSync logic
+ }
+
+ mkdir(dirPath: string): Promise {
+ // Implement mkdir logic
+ return Promise.resolve();
+ }
+
+ mkdirSync(dirPath: string): void {
+ // Implement mkdirSync logic
+ }
+
+ async move(srcPath: string, destPath: string): Promise {
+ // Implement move logic
+ }
+
+ moveSync(srcPath: string, destPath: string): void {
+ // Implement moveSync logic
+ }
+
+ async copy(srcPath: string, destPath: string): Promise {
+ // Implement copy logic
+ }
+
+ copySync(srcPath: string, destPath: string): void {
+ // Implement copySync logic
+ }
+
+ async fileExists(filePath: string): Promise {
+ // Implement fileExists logic
+ return false;
+ }
+
+ fileExistsSync(filePath: string): boolean {
+ // Implement fileExistsSync logic
+ return false;
+ }
+
+ async directoryExists(dirPath: string): Promise {
+ // Implement directoryExists logic
+ return false;
+ }
+
+ directoryExistsSync(dirPath: string): boolean {
+ // Implement directoryExistsSync logic
+ return false;
+ }
+
+ realpathSync(path: string): string {
+ // Implement realpathSync logic
+ return path;
+ }
+
+ getCurrentDirectory(): string {
+ // Implement getCurrentDirectory logic
+ return '/';
+ }
+
+ async glob(patterns: readonly string[]): Promise {
+ // Implement glob logic
+ return [];
+ }
+
+ globSync(patterns: readonly string[]): string[] {
+ // Implement globSync logic
+ return [];
+ }
+}
diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json
new file mode 100644
index 0000000..ef01859
--- /dev/null
+++ b/packages/react/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "composite": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "target": "es2015",
+ "lib": ["es2020", "dom"],
+ "jsx": "react"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/react/tsconfig.lib.json b/packages/react/tsconfig.lib.json
new file mode 100644
index 0000000..ce3b531
--- /dev/null
+++ b/packages/react/tsconfig.lib.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "declaration": true,
+ "types": ["node"]
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["vite.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
+}
diff --git a/packages/react/tsconfig.spec.json b/packages/react/tsconfig.spec.json
new file mode 100644
index 0000000..0edf3f2
--- /dev/null
+++ b/packages/react/tsconfig.spec.json
@@ -0,0 +1,20 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"]
+ },
+ "include": [
+ "vite.config.ts",
+ "vitest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.test.tsx",
+ "src/**/*.spec.tsx",
+ "src/**/*.test.js",
+ "src/**/*.spec.js",
+ "src/**/*.test.jsx",
+ "src/**/*.spec.jsx",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts
new file mode 100644
index 0000000..39bf4e5
--- /dev/null
+++ b/packages/react/vite.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ root: __dirname,
+ cacheDir: '../../node_modules/.vite/packages/react',
+
+ plugins: [],
+
+ test: {
+ watch: false,
+ globals: true,
+ cache: { dir: '../../node_modules/.vitest/packages/react' },
+ environment: 'node',
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ reporters: ['default'],
+ coverage: { reportsDirectory: '../../coverage/packages/react', provider: 'v8' },
+ },
+});