├── .gitattributes ├── .eslintignore ├── packages ├── react-pure-to-class │ ├── .npmignore │ ├── __testfixtures__ │ │ ├── typescript.input.tsx │ │ ├── arrow-functions.input.js │ │ ├── regular-functions.input.js │ │ ├── typescript.output.tsx │ │ ├── arrow-functions.output.js │ │ └── regular-functions.output.js │ ├── package.json │ ├── cli.js │ ├── __tests__ │ │ └── pure-to-class-test.js │ ├── README.md │ └── pure-to-class.js └── react-pure-to-class-vscode │ ├── icon.png │ ├── example.gif │ ├── extension.js │ ├── webpack.config.js │ ├── .vscode │ └── launch.json │ ├── README.md │ ├── package.json │ └── init.js ├── .gitignore ├── README.md ├── .editorconfig ├── .eslintrc └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | __testfixtures__ 2 | node_modules 3 | __bundle.js 4 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/.npmignore: -------------------------------------------------------------------------------- 1 | __testfixtures__ 2 | __tests__ 3 | -------------------------------------------------------------------------------- /packages/react-pure-to-class-vscode/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angryobject/react-pure-to-class/HEAD/packages/react-pure-to-class-vscode/icon.png -------------------------------------------------------------------------------- /packages/react-pure-to-class-vscode/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angryobject/react-pure-to-class/HEAD/packages/react-pure-to-class-vscode/example.gif -------------------------------------------------------------------------------- /packages/react-pure-to-class-vscode/extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const vscode = require('vscode'); 4 | const { init } = require('./__bundle'); 5 | 6 | module.exports = init(vscode); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | *.swp 5 | .idea 6 | ./.vscode 7 | 8 | npm-debug.log 9 | lerna-debug.log 10 | yarn-error.log 11 | 12 | .babel_cache 13 | node_modules 14 | build 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A [jscodeshift](https://github.com/facebook/jscodeshift) transformer to create react class component from pure functional component. 2 | 3 | This repo uses [yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/) feature, it contains the transformer itself and a vscode extension. 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:react/recommended" 6 | ], 7 | "plugins": [ 8 | "react", 9 | "prettier" 10 | ], 11 | "env": { 12 | "node": true, 13 | "es6": true, 14 | "jest": true 15 | }, 16 | "settings": { 17 | "react": { 18 | "version": "16.0" 19 | } 20 | }, 21 | "rules": { 22 | "prettier/prettier": [ 23 | "error", 24 | { 25 | "trailingComma": "es5", 26 | "singleQuote": true 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/react-pure-to-class-vscode/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | 6 | context: __dirname, 7 | 8 | target: 'node', 9 | 10 | entry: { 11 | index: './init.js', 12 | }, 13 | 14 | output: { 15 | path: __dirname, 16 | filename: '__bundle.js', 17 | library: 'init', 18 | libraryTarget: 'commonjs', 19 | }, 20 | 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': { 24 | NODE_ENV: JSON.stringify('production'), 25 | }, 26 | }), 27 | ], 28 | 29 | stats: 'none', 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "scripts": { 7 | "lint": "eslint packages", 8 | "test": "jest", 9 | "precommit": "npm test && npm run lint", 10 | "publish-lib": "cd packages/react-pure-to-class && npm publish", 11 | "publish-ext": "cd packages/react-pure-to-class-vscode && vsce publish" 12 | }, 13 | "devDependencies": { 14 | "babel-eslint": "^10.0.1", 15 | "eslint": "^5.14.1", 16 | "eslint-plugin-prettier": "^3.0.1", 17 | "eslint-plugin-react": "^7.12.4", 18 | "husky": "^1.3.1", 19 | "jest": "^24.1.0", 20 | "prettier": "^1.16.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/__testfixtures__/typescript.input.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as React from 'react'; 3 | 4 | interface IProps { 5 | message: string; 6 | children: React.Element; 7 | } 8 | 9 | type OtherProps = {}; 10 | 11 | const MyComponent1 = (props: IProps) => { 12 | return
{props.message}
; 13 | }; 14 | 15 | function MyComponent2({ 16 | message = 'foobar', 17 | }: IProps & OtherProps) { 18 | return
{message}
; 19 | } 20 | 21 | const MyComponent3 = (p: IProps & OtherProps) => 22 | p.children ?
{p.children}
: null; 23 | 24 | function MyComponent4(p: IProps) { 25 | return p.children ?
{p.children}
: null; 26 | } 27 | 28 | const NonReact = v => v.x + v.y; 29 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pure-to-class", 3 | "description": "A transformer to convert pure component to class component", 4 | "version": "1.1.6", 5 | "main": "pure-to-class.js", 6 | "license": "MIT", 7 | "bin": "./cli.js", 8 | "peerDependencies": { 9 | "jscodeshift": "^0.4.0" 10 | }, 11 | "devDependencies": { 12 | "jscodeshift": "^0.6.3" 13 | }, 14 | "author": { 15 | "name": "Max Shishkin" 16 | }, 17 | "homepage": "https://github.com/angryobject/react-pure-to-class", 18 | "keywords": [ 19 | "codemod", 20 | "jscodeshift", 21 | "react" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/angryobject/react-pure-to-class" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const stdin = process.openStdin(); 4 | const jscodeshift = require('jscodeshift'); 5 | const getParser = require('jscodeshift/src/getParser'); 6 | const pureToClass = require('./pure-to-class'); 7 | const parser = getParser(process.argv[2] || 'babel'); 8 | 9 | let input = ''; 10 | 11 | stdin.on('data', function(chunk) { 12 | input += chunk; 13 | }); 14 | 15 | stdin.on('end', function() { 16 | let output; 17 | 18 | try { 19 | output = pureToClass( 20 | { source: input }, 21 | { 22 | jscodeshift, 23 | stats: () => {}, 24 | }, 25 | { 26 | parser, 27 | } 28 | ); 29 | } finally { 30 | console.log(output || input); // eslint-disable-line no-console 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/__testfixtures__/arrow-functions.input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MyComponent = props => { 4 | return
{props.message}
; 5 | }; 6 | 7 | const MyComponent = (props) =>
{props.message}
; 8 | 9 | const MyComponent = p => p.children ?
{p.children}
: null; 10 | 11 | export const MyComponent = p => p.children ?
{p.children}
: null; 12 | 13 | module.exports = (props) => { 14 | return
{props.message}
; 15 | }; 16 | 17 | export default props => { 18 | return
{props.message}
; 19 | }; 20 | 21 | const NonReact = v => v.x + v.y; 22 | 23 | const StillReact = () => { 24 | const a =
bla-bla
; 25 | return 2 + 2; 26 | }; 27 | 28 | export const AnotherMyComponent = ({ items }) => () 31 | -------------------------------------------------------------------------------- /packages/react-pure-to-class-vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false 12 | }, 13 | { 14 | "name": "Launch Tests", 15 | "type": "extensionHost", 16 | "request": "launch", 17 | "runtimeExecutable": "${execPath}", 18 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ], 19 | "stopOnEntry": false 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/__testfixtures__/regular-functions.input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function MyComponent(props) { 4 | return
{props.message}
; 5 | }; 6 | 7 | module.exports = function MyComponent(props) { 8 | return
{props.message}
; 9 | }; 10 | 11 | export default function MyComponent(props) { 12 | return
{props.message}
; 13 | }; 14 | 15 | function MyComponent() { 16 | return
bla-bla
; 17 | } 18 | 19 | function MyComponent(p) { 20 | return p.children ?
{children}
: null; 21 | } 22 | 23 | const MyComponent = function() { 24 | return
bla-bla
; 25 | } 26 | 27 | const MyComponent = function MyFunction() { 28 | return
bla-bla
; 29 | } 30 | 31 | function NonReact(v) { 32 | return v.x + v.y; 33 | } 34 | 35 | function StillReact() { 36 | const a =
bla-bla
; 37 | return 2 + 2; 38 | } 39 | 40 | function MyComponent({ items }) { 41 | return (); 44 | } 45 | 46 | function MyComponent({ items }) { 47 | const lis = items.map(item =>
  • {item}
  • ); 48 | 49 | return (); 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/__testfixtures__/typescript.output.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as React from 'react'; 3 | 4 | interface IProps { 5 | message: string; 6 | children: React.Element; 7 | } 8 | 9 | type OtherProps = {}; 10 | 11 | class MyComponent1 extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | } 15 | 16 | render() { 17 | const { 18 | props, 19 | } = this; 20 | 21 | return
    {props.message}
    ; 22 | } 23 | } 24 | 25 | class MyComponent2 extends React.Component { 26 | constructor(props) { 27 | super(props); 28 | } 29 | 30 | render() { 31 | const { 32 | message = 'foobar', 33 | } = this.props; 34 | 35 | return
    {message}
    ; 36 | } 37 | } 38 | 39 | class MyComponent3 extends React.Component { 40 | constructor(props) { 41 | super(props); 42 | } 43 | 44 | render() { 45 | const p = this.props; 46 | return p.children ?
    {p.children}
    : null; 47 | } 48 | } 49 | 50 | class MyComponent4 extends React.Component { 51 | constructor(props) { 52 | super(props); 53 | } 54 | 55 | render() { 56 | const p = this.props; 57 | return p.children ?
    {p.children}
    : null; 58 | } 59 | } 60 | 61 | class NonReact extends React.Component { 62 | constructor(props) { 63 | super(props); 64 | } 65 | 66 | render() { 67 | const v = this.props; 68 | return v.x + v.y; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/react-pure-to-class-vscode/README.md: -------------------------------------------------------------------------------- 1 | Replaces pure functional react components with class components. Works both for JavaScript and TypeScript. 2 | 3 | Select a block of code, choose `React Pure To Class` from the Command Palette. 4 | 5 | ![Demo](https://raw.githubusercontent.com/angryobject/react-pure-to-class/master/packages/react-pure-to-class-vscode/example.gif) 6 | 7 | Turns this: 8 | 9 | ```javascript 10 | function MyComponent(props) { 11 | return ( 12 |
    13 | {props.children} 14 |
    15 | ); 16 | } 17 | ``` 18 | 19 | into this: 20 | 21 | ```javascript 22 | class MyComponent extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | } 26 | 27 | render() { 28 | const { 29 | props, 30 | } = this; 31 | 32 | return ( 33 |
    34 | {props.children} 35 |
    36 | ); 37 | } 38 | } 39 | ``` 40 | 41 | It makes some assumptions about functions that can be transformed: 42 | 43 | * function should has zero or one argument (i.e. `props`, though the name me be different) 44 | * the argument, if present, should be an identifier (`foo => {..}`) or object pattern(`({ foo }) => {...}`). This means array patterns (`([foo]) => {...}`) and default function parameters (`(foo = defaultFoo) => {...}`) don't work. `props` is always an object and default props are handled differently in React 45 | * the functions should not appear inside other functions, be property of an objects or method of a class 46 | 47 | Extension options: 48 | 49 | `reactPureToClass.reactComponent` - string, where to find base react component, defaults to `React.Component`. 50 | -------------------------------------------------------------------------------- /packages/react-pure-to-class-vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "react-pure-to-class-vscode", 4 | "displayName": "React Pure To Class", 5 | "description": "Convert pure react components to class components", 6 | "version": "1.1.8", 7 | "publisher": "angryobject", 8 | "license": "MIT", 9 | "author": { 10 | "name": "Max Shishkin" 11 | }, 12 | "homepage": "https://github.com/angryobject/react-pure-to-class", 13 | "keywords": [ 14 | "codemod", 15 | "jscodeshift", 16 | "react", 17 | "vscode" 18 | ], 19 | "main": "./extension.js", 20 | "engines": { 21 | "vscode": "^1.12.0" 22 | }, 23 | "categories": [ 24 | "Formatters", 25 | "Other" 26 | ], 27 | "activationEvents": [ 28 | "onCommand:extension.reactPureToClass" 29 | ], 30 | "contributes": { 31 | "commands": [ 32 | { 33 | "command": "extension.reactPureToClass", 34 | "title": "React Pure To Class" 35 | } 36 | ], 37 | "configuration": { 38 | "type": "object", 39 | "title": "React Pure To Class", 40 | "properties": { 41 | "reactPureToClass.reactComponent": { 42 | "type": "string", 43 | "default": "React.Component", 44 | "description": "Where to find base React component" 45 | } 46 | } 47 | } 48 | }, 49 | "scripts": { 50 | "watch": "webpack --watch", 51 | "build": "webpack" 52 | }, 53 | "devDependencies": { 54 | "jscodeshift": "^0.6.3", 55 | "react-pure-to-class": "^1.1.5", 56 | "vscode": "^1.1.29", 57 | "webpack": "^4.29.5", 58 | "webpack-cli": "^3.2.3" 59 | }, 60 | "repository": { 61 | "type": "git", 62 | "url": "https://github.com/angryobject/react-pure-to-class" 63 | }, 64 | "icon": "icon.png" 65 | } 66 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/__tests__/pure-to-class-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 4 | 5 | defineTest(__dirname, 'pure-to-class', null, 'regular-functions'); 6 | defineTest(__dirname, 'pure-to-class', null, 'arrow-functions'); 7 | 8 | const fs = require('fs'); 9 | const jscodeshift = require('jscodeshift'); 10 | const getParser = require('jscodeshift/src/getParser'); 11 | const pureToClass = require('../pure-to-class'); 12 | 13 | const api = { 14 | jscodeshift, 15 | stats: () => {}, 16 | }; 17 | 18 | const getFixture = name => 19 | fs.readFileSync(__dirname + `/../__testfixtures__/${name}`, 'utf-8'); 20 | 21 | const transform = (source, options = {}) => 22 | pureToClass({ source }, api, options); 23 | 24 | test('invalid transforms', () => { 25 | expect(transform('function C(arg1, arg2) {return }')).toBe( 26 | 'function C(arg1, arg2) {return }' 27 | ); 28 | 29 | expect(transform('function C(arg1 = defaultArg) {return }')).toBe( 30 | 'function C(arg1 = defaultArg) {return }' 31 | ); 32 | 33 | expect( 34 | transform('const obj = {c(arg1 = defaultArg) {return }}') 35 | ).toBe('const obj = {c(arg1 = defaultArg) {return }}'); 36 | 37 | expect(transform('class C { someFn(props) {return }}')).toBe( 38 | 'class C { someFn(props) {return }}' 39 | ); 40 | 41 | expect(transform('function someFn(a, b) {return (props) => }')).toBe( 42 | 'function someFn(a, b) {return (props) => }' 43 | ); 44 | }); 45 | 46 | test('typescript transforms', () => { 47 | const inputTS = getFixture('typescript.input.tsx'); 48 | const outputTS = getFixture('typescript.output.tsx'); 49 | 50 | expect(transform(inputTS, { parser: getParser('tsx') })).toBe(outputTS); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/README.md: -------------------------------------------------------------------------------- 1 | A `jscodeshift` transformer to create react class component from pure functional component. Works both for JavaScript and TypeScript. 2 | 3 | Turns this: 4 | 5 | ```javascript 6 | function MyComponent(props) { 7 | return
    {props.message}
    ; 8 | }; 9 | ``` 10 | 11 | into this: 12 | 13 | ```javascript 14 | class MyComponent extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | } 18 | 19 | render() { 20 | const { 21 | props, 22 | } = this; 23 | 24 | return
    {props.message}
    ; 25 | } 26 | } 27 | ``` 28 | 29 | It makes some assumptions about functions that can be transformed: 30 | 31 | * function should has zero or one argument (i.e. `props`, though the name me be different) 32 | * the argument, if present, should be an identifier (`foo => {..}`) or object pattern(`({ foo }) => {...}`). This means array patterns (`([foo]) => {...}`) and default function parameters (`(foo = defaultFoo) => {...}`) don't work. `props` is always an object and default props are handled differently in React 33 | * the functions should not appear inside other functions, be property of an objects or method of a class 34 | 35 | See [jscodeshift](https://github.com/facebook/jscodeshift) for more info on transformations. 36 | 37 | Basic manual usage in node (you probably don't need it): 38 | 39 | ```javascript 40 | const jscodeshift = require('jscodeshift'); 41 | const pureToClass = require('react-pure-to-class'); 42 | 43 | const options = { 44 | reactComponent: 'React.Component', 45 | printOptions: { 46 | quote: 'single', 47 | trailingComma: true, 48 | }, 49 | }; 50 | 51 | const source = ''; // your source code here; 52 | 53 | const transformedSource = pureToClass( 54 | { source }, 55 | { jscodeshift }, 56 | options // or empty object for defaults 57 | ); 58 | ``` 59 | 60 | It also works as a cli, which may be useful in vim to transform selected code, like so: 61 | 62 | ``` 63 | :'<,'>!react-pure-to-class 64 | ``` 65 | 66 | -------------------------------------------------------------------------------- /packages/react-pure-to-class-vscode/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jscodeshift = require('jscodeshift'); 4 | const getParser = require('jscodeshift/src/getParser'); 5 | const pureToClass = require('react-pure-to-class'); 6 | 7 | const tsFiles = ['typescript', 'typescriptreact']; 8 | const supportedFiles = ['javascript', 'javascriptreact', ...tsFiles]; 9 | 10 | module.exports = vscode => ({ 11 | activate(context) { 12 | const disposable = vscode.commands.registerCommand( 13 | 'extension.reactPureToClass', 14 | () => { 15 | const editor = vscode.window.activeTextEditor; 16 | const config = vscode.workspace.getConfiguration('reactPureToClass'); 17 | 18 | if (!editor) { 19 | return; 20 | } 21 | 22 | const doc = editor.document; 23 | 24 | if (!supportedFiles.includes(doc.languageId)) { 25 | vscode.window.showInformationMessage( 26 | 'Only available for javascript/typescript/react file types' 27 | ); 28 | return; 29 | } 30 | 31 | const parser = tsFiles.includes(doc.languageId) ? 'tsx' : 'babel'; 32 | 33 | const selection = editor.selection; 34 | const text = doc.getText(selection); 35 | 36 | let output; 37 | 38 | try { 39 | output = pureToClass( 40 | { source: text }, 41 | { 42 | jscodeshift, 43 | stats: () => {}, 44 | }, 45 | { 46 | parser: getParser(parser), 47 | reactComponent: config.reactComponent, 48 | } 49 | ); 50 | } catch (e) { 51 | vscode.window.showInformationMessage( 52 | 'Something went wrong (probably bad selection)' 53 | ); 54 | return; 55 | } 56 | 57 | if (output === text) { 58 | vscode.window.showInformationMessage('Nothing to transform'); 59 | return; 60 | } 61 | 62 | editor.edit(function(builder) { 63 | builder.replace(selection, output); 64 | }); 65 | } 66 | ); 67 | 68 | context.subscriptions.push(disposable); 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/__testfixtures__/arrow-functions.output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class MyComponent extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | const { 10 | props, 11 | } = this; 12 | 13 | return
    {props.message}
    ; 14 | } 15 | } 16 | 17 | class MyComponent extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | } 21 | 22 | render() { 23 | const { 24 | props, 25 | } = this; 26 | 27 | return
    {props.message}
    ; 28 | } 29 | } 30 | 31 | class MyComponent extends React.Component { 32 | constructor(props) { 33 | super(props); 34 | } 35 | 36 | render() { 37 | const p = this.props; 38 | return p.children ?
    {p.children}
    : null; 39 | } 40 | } 41 | 42 | export class MyComponent extends React.Component { 43 | constructor(props) { 44 | super(props); 45 | } 46 | 47 | render() { 48 | const p = this.props; 49 | return p.children ?
    {p.children}
    : null; 50 | } 51 | } 52 | 53 | module.exports = class extends React.Component { 54 | constructor(props) { 55 | super(props); 56 | } 57 | 58 | render() { 59 | const { 60 | props, 61 | } = this; 62 | 63 | return
    {props.message}
    ; 64 | } 65 | }; 66 | 67 | export default class extends React.Component { 68 | constructor(props) { 69 | super(props); 70 | } 71 | 72 | render() { 73 | const { 74 | props, 75 | } = this; 76 | 77 | return
    {props.message}
    ; 78 | } 79 | } 80 | 81 | class NonReact extends React.Component { 82 | constructor(props) { 83 | super(props); 84 | } 85 | 86 | render() { 87 | const v = this.props; 88 | return v.x + v.y; 89 | } 90 | } 91 | 92 | class StillReact extends React.Component { 93 | constructor(props) { 94 | super(props); 95 | } 96 | 97 | render() { 98 | const a =
    bla-bla
    ; 99 | return 2 + 2; 100 | } 101 | } 102 | 103 | export class AnotherMyComponent extends React.Component { 104 | constructor(props) { 105 | super(props); 106 | } 107 | 108 | render() { 109 | const { items } = this.props; 110 | 111 | return ( 112 |
      113 | {items.map(item =>
    • {item}
    • )} 114 |
    115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/__testfixtures__/regular-functions.output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class MyComponent extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | const { 10 | props, 11 | } = this; 12 | 13 | return
    {props.message}
    ; 14 | } 15 | } 16 | 17 | module.exports = class MyComponent extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | } 21 | 22 | render() { 23 | const { 24 | props, 25 | } = this; 26 | 27 | return
    {props.message}
    ; 28 | } 29 | }; 30 | 31 | export default class MyComponent extends React.Component { 32 | constructor(props) { 33 | super(props); 34 | } 35 | 36 | render() { 37 | const { 38 | props, 39 | } = this; 40 | 41 | return
    {props.message}
    ; 42 | } 43 | } 44 | 45 | class MyComponent extends React.Component { 46 | constructor(props) { 47 | super(props); 48 | } 49 | 50 | render() { 51 | return
    bla-bla
    ; 52 | } 53 | } 54 | 55 | class MyComponent extends React.Component { 56 | constructor(props) { 57 | super(props); 58 | } 59 | 60 | render() { 61 | const p = this.props; 62 | return p.children ?
    {children}
    : null; 63 | } 64 | } 65 | 66 | class MyComponent extends React.Component { 67 | constructor(props) { 68 | super(props); 69 | } 70 | 71 | render() { 72 | return
    bla-bla
    ; 73 | } 74 | } 75 | 76 | const MyComponent = class MyFunction extends React.Component { 77 | constructor(props) { 78 | super(props); 79 | } 80 | 81 | render() { 82 | return
    bla-bla
    ; 83 | } 84 | } 85 | 86 | class NonReact extends React.Component { 87 | constructor(props) { 88 | super(props); 89 | } 90 | 91 | render() { 92 | const v = this.props; 93 | return v.x + v.y; 94 | } 95 | } 96 | 97 | class StillReact extends React.Component { 98 | constructor(props) { 99 | super(props); 100 | } 101 | 102 | render() { 103 | const a =
    bla-bla
    ; 104 | return 2 + 2; 105 | } 106 | } 107 | 108 | class MyComponent extends React.Component { 109 | constructor(props) { 110 | super(props); 111 | } 112 | 113 | render() { 114 | const { items } = this.props; 115 | return (
      116 | {items.map(item =>
    • {item}
    • )} 117 |
    ); 118 | } 119 | } 120 | 121 | class MyComponent extends React.Component { 122 | constructor(props) { 123 | super(props); 124 | } 125 | 126 | render() { 127 | const { items } = this.props; 128 | const lis = items.map(item =>
  • {item}
  • ); 129 | 130 | return (
      {lis}
    ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/react-pure-to-class/pure-to-class.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(file, api, options) { 4 | const j = api.jscodeshift; 5 | 6 | const reactComponent = options.reactComponent || 'React.Component'; 7 | 8 | const printOptions = options.printOptions || { 9 | quote: 'single', 10 | trailingComma: true, 11 | }; 12 | 13 | const areValidArguments = args => { 14 | const hasOneArgumentMax = args.length <= 1; 15 | const argumentIsIdentifierOrObjectPattern = 16 | !args[0] || j.Identifier.check(args[0]) || j.ObjectPattern.check(args[0]); 17 | 18 | return hasOneArgumentMax && argumentIsIdentifierOrObjectPattern; 19 | }; 20 | 21 | const isInsideJSXOrFunctionOrObjectOrClass = path => { 22 | const jp = j(path); 23 | 24 | return ( 25 | jp.closest(j.JSXElement).size() || 26 | jp.closest(j.FunctionDeclaration).size() || 27 | jp.closest(j.FunctionExpression).size() || 28 | jp.closest(j.ArrowFunctionExpression).size() || 29 | jp.closest(j.ObjectExpression).size() || 30 | jp.closest(j.ClassBody).size() 31 | ); 32 | }; 33 | 34 | const canBeReplaced = path => { 35 | const hasValidArguments = areValidArguments(path.value.params); 36 | return hasValidArguments && !isInsideJSXOrFunctionOrObjectOrClass(path); 37 | }; 38 | 39 | const createBodyWithReturn = body => 40 | j.BlockStatement.check(body) 41 | ? body 42 | : j.blockStatement([j.returnStatement(body)]); 43 | 44 | const createPropsDecl = param => { 45 | if (j.ObjectPattern.check(param) || param.name !== 'props') { 46 | return j.variableDeclaration('const', [ 47 | j.variableDeclarator( 48 | param, 49 | j.memberExpression(j.thisExpression(), j.identifier('props')) 50 | ), 51 | ]); 52 | } 53 | 54 | const props = j.property('init', j.identifier('props'), param); 55 | props.shorthand = true; 56 | 57 | return j.variableDeclaration('const', [ 58 | j.variableDeclarator(j.objectPattern([props]), j.thisExpression()), 59 | ]); 60 | }; 61 | 62 | const createConstructor = () => 63 | j.methodDefinition( 64 | 'constructor', 65 | j.identifier('constructor'), 66 | j.functionExpression( 67 | null, 68 | [j.identifier('props')], 69 | j.blockStatement([ 70 | j.expressionStatement( 71 | j.callExpression(j.super(), [j.identifier('props')]) 72 | ), 73 | ]) 74 | ) 75 | ); 76 | 77 | const createRenderMethod = body => 78 | j.methodDefinition( 79 | 'method', 80 | j.identifier('render'), 81 | j.functionExpression(null, [], body) 82 | ); 83 | 84 | const createClassComponent = (name, renderBody, propsType) => { 85 | const cls = j.classDeclaration( 86 | name ? j.identifier(name) : null, 87 | j.classBody([createConstructor(), createRenderMethod(renderBody)]) 88 | ); 89 | 90 | cls.superClass = j.template.expression([reactComponent]); 91 | 92 | if (propsType) { 93 | cls.superTypeParameters = j.tsTypeParameterInstantiation([ 94 | propsType.typeAnnotation, 95 | ]); 96 | } 97 | 98 | return cls; 99 | }; 100 | 101 | const replaceWithClass = collection => 102 | collection 103 | .map(path => { 104 | const grandParent = path.parent.parent; 105 | const hasOwnName = path.value.id && path.value.id.name; 106 | 107 | if ( 108 | !hasOwnName && 109 | j.VariableDeclaration.check(grandParent.value) && 110 | grandParent.value.declarations.length === 1 111 | ) { 112 | return grandParent; 113 | } 114 | 115 | return path; 116 | }) 117 | .replaceWith(path => { 118 | const isVarDecl = j.VariableDeclaration.check(path.value); 119 | const fn = isVarDecl ? path.value.declarations[0].init : path.value; 120 | 121 | const name = isVarDecl 122 | ? path.value.declarations[0].id.name 123 | : fn.id && fn.id.name; 124 | 125 | const props = fn.params[0]; 126 | const propsType = props && fn.params[0].typeAnnotation; 127 | const body = createBodyWithReturn(fn.body); 128 | 129 | if (props) { 130 | delete props.typeAnnotation; 131 | body.body.unshift(createPropsDecl(props)); 132 | } 133 | 134 | return createClassComponent(name, body, propsType); 135 | }); 136 | 137 | const root = j(file.source, { 138 | parser: options.parser, 139 | }); 140 | 141 | [ 142 | root.find(j.FunctionDeclaration).filter(canBeReplaced), 143 | root.find(j.FunctionExpression).filter(canBeReplaced), 144 | root.find(j.ArrowFunctionExpression).filter(canBeReplaced), 145 | ].forEach(replaceWithClass); 146 | 147 | return root.toSource(printOptions); 148 | }; 149 | --------------------------------------------------------------------------------