Skip to content

Commit

Permalink
feat: add convertSimpleClassComponentsToFunctions transform
Browse files Browse the repository at this point in the history
  • Loading branch information
jedwards1211 committed Apr 22, 2020
1 parent 3e341bd commit 640282d
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,7 @@ const Foo = () => (
</div>
)
```

# `convertSimpleClassComponentsToFunctions`

Converts `React.Component` subclasses with only a `render` method (no lifecycle methods, constructors, or class properties other than `propTypes`, `contextTypes`, `defaultProps`, and no `state` type parameter) into functional components.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
},
"dependencies": {
"@babel/runtime": "^7.1.5",
"jscodeshift-find-imports": "^2.0.0",
"jscodeshift-paths-in-range": "^1.0.0"
},
"renovate": {
Expand Down
128 changes: 128 additions & 0 deletions src/convertSimpleClassComponentsToFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
ASTPath,
FileInfo,
API,
ClassDeclaration,
ClassMethod,
ClassProperty,
} from 'jscodeshift'
import findImports from 'jscodeshift-find-imports'

module.exports = function convertSimpleClassComponentsToFunctions(
fileInfo: FileInfo,
api: API
): string | null | undefined | void {
const j = api.jscodeshift

const root = j(fileInfo.source)
const { Component } = findImports(
root,
j.template.statement`import { Component } from 'react'`
)
root
.find(j.ClassDeclaration, {
superClass:
Component.type === 'Identifier'
? { type: 'Identifier', name: Component.name }
: Component.type === 'MemberExpression'
? {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: (Component.object as any).name,
},
property: {
type: 'Identifier',
name: (Component.property as any).name,
},
}
: null,
})
.forEach((path: ASTPath<ClassDeclaration>) => {
const { id, superTypeParameters, body } = path.node
const superTypeParams = superTypeParameters?.params || []
const renderMethodColl = j([path]).find(j.ClassMethod, {
key: { type: 'Identifier', name: 'render' },
})
const renderMethod: ClassMethod | void = renderMethodColl.nodes()[0]
const contextTypesColl = j([path]).find(j.ClassProperty, {
static: true,
key: { type: 'Identifier', name: 'contextTypes' },
})
const contextTypes: ClassProperty | void = contextTypesColl.nodes()[0]
const propTypesColl = j([path]).find(j.ClassProperty, {
static: true,
key: { type: 'Identifier', name: 'propTypes' },
})
const propTypes: ClassProperty | void = propTypesColl.nodes()[0]
const defaultPropsColl = j([path]).find(j.ClassProperty, {
static: true,
key: { type: 'Identifier', name: 'defaultProps' },
})
const defaultProps: ClassProperty | void = defaultPropsColl.nodes()[0]

if (
!id ||
!renderMethod ||
superTypeParams.length > 1 ||
body.body.find(
node =>
node !== renderMethod &&
node !== propTypes &&
node !== contextTypes &&
node !== defaultProps
)
)
return

j([path])
.find(j.MemberExpression, {
object: { type: 'ThisExpression' },
property: { type: 'Identifier', name: 'props' },
})
.replaceWith(() => j.identifier('props'))
j([path])
.find(j.MemberExpression, {
object: { type: 'ThisExpression' },
property: { type: 'Identifier', name: 'context' },
})
.replaceWith(() => j.identifier('context'))
const propsParam = j.identifier('props')
if (superTypeParams[0]) {
propsParam.typeAnnotation =
(superTypeParameters as any).type === 'TSTypeParameterInstantiation'
? j.tsTypeAnnotation(superTypeParams[0] as any)
: j.typeAnnotation(superTypeParams[0] as any)
}
const params = [propsParam]
if (contextTypes) {
params.push(j.identifier('context'))
}
const func = j.functionDeclaration(id, params, renderMethod.body)
func.returnType = renderMethod.returnType
const [replaced] = path.replace(func)
if (contextTypes) {
j([replaced])
.closest(j.Statement)
.insertAfter(
j.template.statement`${id}.contextTypes = ${contextTypes.value}\n`
)
}
if (defaultProps) {
j([replaced])
.closest(j.Statement)
.insertAfter(
j.template.statement`${id}.defaultProps = ${defaultProps.value}\n`
)
}
if (propTypes) {
j([replaced])
.closest(j.Statement)
.insertAfter(
j.template.statement`${id}.propTypes = ${propTypes.value}\n`
)
}
})

return root.toSource()
}
38 changes: 38 additions & 0 deletions test/convertSimpleClassComponentsToFunctions/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export const file = 'test.js'
export const parser = 'babylon'

export const options = {}

export const input = `
import * as React from 'react'
export default class Foo extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
}
static contextTypes = {
temp: PropTypes.string,
}
static defaultProps = {
title: 'Title',
}
render() {
return <div>{this.props.title} {this.context.temp}</div>
}
}
`

export const expected = `
import * as React from 'react'
export default function Foo(props, context) {
return <div>{props.title} {context.temp}</div>
}
Foo.propTypes = {
title: PropTypes.string.isRequired,
}
Foo.defaultProps = {
title: 'Title',
}
Foo.contextTypes = {
temp: PropTypes.string,
}
`
16 changes: 16 additions & 0 deletions test/convertSimpleClassComponentsToFunctions/cantConvertBasic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const file = 'test.js'
export const parser = 'babylon'

export const options = {}

export const input = `
import * as React from 'react'
class Bar extends React.Component {
componentDidUpdate(prevProps) {}
render() {
return <div>{this.props.title}</div>
}
}
`

export const expected = input
21 changes: 21 additions & 0 deletions test/convertSimpleClassComponentsToFunctions/cantConvertFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const file = 'test.js'
export const parser = 'babylon'

export const options = {}

export const input = `
import * as React from 'react'
class Foo extends React.Component<Props, State> {
render(): React.Node | null {
return <div>{this.props.title}</div>
}
}
class Bar extends React.Component<Props> {
componentDidUpdate(prevProps: Props) {}
render(): React.Node | null {
return <div>{this.props.title}</div>
}
}
`

export const expected = input
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const file = 'test.js'
export const parser = 'babylon'

export const options = {}

export const input = `
import * as React from 'react'
class Foo extends React.Component<Props, State> {
render(): React.ReactNode | null {
return <div>{this.props.title}</div>
}
}
class Bar extends React.Component<Props> {
componentDidUpdate(prevProps: Props) {}
render(): React.ReactNode | null {
return <div>{this.props.title}</div>
}
}
`

export const expected = input
26 changes: 26 additions & 0 deletions test/convertSimpleClassComponentsToFunctions/flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const file = 'test.js'
export const parser = 'babylon'

export const options = {}

export const input = `
import * as React from 'react'
export default class Foo extends React.Component<Props> {
static propTypes = {
title: PropTypes.string.isRequired,
}
render(): React.Node | null {
return <div>{this.props.title}</div>
}
}
`

export const expected = `
import * as React from 'react'
export default function Foo(props: Props): React.Node | null {
return <div>{props.title}</div>
}
Foo.propTypes = {
title: PropTypes.string.isRequired,
}
`
20 changes: 20 additions & 0 deletions test/convertSimpleClassComponentsToFunctions/typescript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const file = 'test.tsx'
export const parser = 'tsx'

export const options = {}

export const input = `
import * as React from 'react'
export default class Foo extends React.Component<Props> {
render(): React.ReactNode | null {
return <div>{this.props.title}</div>
}
}
`

export const expected = `
import * as React from 'react'
export default function Foo(props: Props): React.ReactNode | null {
return <div>{props.title}</div>
}
`
46 changes: 46 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,11 @@ assign-symbols@^1.0.0:
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=

[email protected]:
version "0.11.7"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.7.tgz#f318bf44e339db6a320be0009ded64ec1471f46c"
integrity sha512-2mP3TwtkY/aTv5X3ZsMpNAbOnyoC/aMJwJSoaELPkHId0nSQgFcnU4dRW3isxiz7+zBexk0ym3WNVjMiQBnJSw==

[email protected]:
version "0.12.1"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.12.1.tgz#55d3737a8a68e1ccde131067005ce7ee3dd42b99"
Expand Down Expand Up @@ -4873,13 +4878,44 @@ jsbn@~0.1.0:
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=

jscodeshift-find-imports@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/jscodeshift-find-imports/-/jscodeshift-find-imports-2.0.0.tgz#d9bddc3eddbc927017004664fefd5bcf49e432de"
integrity sha512-ZYmZJ17pU/CGDaB6PWo4j7NVazVuwBDBXUrGnF57YcGvSA1toTToE/YitdBbCwariM4ky+gPWbM383rAmlbJQw==
dependencies:
jscodeshift "^0.6.4"

jscodeshift-paths-in-range@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/jscodeshift-paths-in-range/-/jscodeshift-paths-in-range-1.1.0.tgz#accc511afd807094f274a497f136b6d338efda8d"
integrity sha512-Hy/wwoLxXxLQXnvo2wBnqMXgvQODaU5EdjJjQp/5sPhbBujSJa+8K+jV75uJC+TnPeOM7ofdvQu72P6l/idn9g==
dependencies:
"@babel/runtime" "^7.1.5"

jscodeshift@^0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.6.4.tgz#e19ab86214edac86a75c4557fc88b3937d558a8e"
integrity sha512-+NF/tlNbc2WEhXUuc4WEJLsJumF84tnaMUZW2hyJw3jThKKRvsPX4sPJVgO1lPE28z0gNL+gwniLG9d8mYvQCQ==
dependencies:
"@babel/core" "^7.1.6"
"@babel/parser" "^7.1.6"
"@babel/plugin-proposal-class-properties" "^7.1.0"
"@babel/plugin-proposal-object-rest-spread" "^7.0.0"
"@babel/preset-env" "^7.1.6"
"@babel/preset-flow" "^7.0.0"
"@babel/preset-typescript" "^7.1.0"
"@babel/register" "^7.0.0"
babel-core "^7.0.0-bridge.0"
colors "^1.1.2"
flow-parser "0.*"
graceful-fs "^4.1.11"
micromatch "^3.1.10"
neo-async "^2.5.0"
node-dir "^0.1.17"
recast "^0.16.1"
temp "^0.8.1"
write-file-atomic "^2.3.0"

jscodeshift@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.7.0.tgz#4eee7506fd4fdacbd80340287d61575af991fdab"
Expand Down Expand Up @@ -7255,6 +7291,16 @@ [email protected]:
private "~0.1.5"
source-map "~0.6.1"

recast@^0.16.1:
version "0.16.2"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.16.2.tgz#3796ebad5fe49ed85473b479cd6df554ad725dc2"
integrity sha512-O/7qXi51DPjRVdbrpNzoBQH5dnAPQNbfoOFyRiUwreTMJfIHYOEBzwuH+c0+/BTSJ3CQyKs6ILSWXhESH6Op3A==
dependencies:
ast-types "0.11.7"
esprima "~4.0.0"
private "~0.1.5"
source-map "~0.6.1"

recast@^0.18.1:
version "0.18.5"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.18.5.tgz#9d5adbc07983a3c8145f3034812374a493e0fe4d"
Expand Down

0 comments on commit 640282d

Please sign in to comment.