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' }, + }, +});