From f90ccb7501b7d79119779440f13cf1c27cb1acde Mon Sep 17 00:00:00 2001 From: James Kyle Date: Tue, 20 Oct 2015 16:16:49 -0700 Subject: [PATCH] Add pure component transform --- test/__tests__/transform-tests.js | 7 ++ test/pure-component-test.js | 24 +++++++ test/pure-component-test.output.js | 22 ++++++ test/pure-component-test2.js | 24 +++++++ test/pure-component-test2.output.js | 22 ++++++ transforms/pure-component.js | 100 ++++++++++++++++++++++++++++ 6 files changed, 199 insertions(+) create mode 100644 test/pure-component-test.js create mode 100644 test/pure-component-test.output.js create mode 100644 test/pure-component-test2.js create mode 100644 test/pure-component-test2.output.js create mode 100644 transforms/pure-component.js diff --git a/test/__tests__/transform-tests.js b/test/__tests__/transform-tests.js index 4ae1f434..9a37baea 100644 --- a/test/__tests__/transform-tests.js +++ b/test/__tests__/transform-tests.js @@ -60,4 +60,11 @@ describe('Transform Tests', () => { test('class', 'export-default-class-test'); }); + it('transforms the "pure-component" tests correctly', () => { + test('pure-component', 'pure-component-test'); + test('pure-component', 'pure-component-test2', { + useArrows: true + }); + }); + }); diff --git a/test/pure-component-test.js b/test/pure-component-test.js new file mode 100644 index 00000000..90c69a43 --- /dev/null +++ b/test/pure-component-test.js @@ -0,0 +1,24 @@ +'use strict'; + +var React = require('React'); + +function render() { + return
; +} + +class Pure extends React.Component { + render() { + return
; + } +} + +class Impure extends React.Component { + componentWillMount() { + // such impure + } + render() { + return
; + } +} + +var A = props =>
; diff --git a/test/pure-component-test.output.js b/test/pure-component-test.output.js new file mode 100644 index 00000000..3403e0eb --- /dev/null +++ b/test/pure-component-test.output.js @@ -0,0 +1,22 @@ +'use strict'; + +var React = require('React'); + +function render() { + return
; +} + +function Pure(props) { + return
; +} + +class Impure extends React.Component { + componentWillMount() { + // such impure + } + render() { + return
; + } +} + +var A = props =>
; diff --git a/test/pure-component-test2.js b/test/pure-component-test2.js new file mode 100644 index 00000000..90c69a43 --- /dev/null +++ b/test/pure-component-test2.js @@ -0,0 +1,24 @@ +'use strict'; + +var React = require('React'); + +function render() { + return
; +} + +class Pure extends React.Component { + render() { + return
; + } +} + +class Impure extends React.Component { + componentWillMount() { + // such impure + } + render() { + return
; + } +} + +var A = props =>
; diff --git a/test/pure-component-test2.output.js b/test/pure-component-test2.output.js new file mode 100644 index 00000000..31adacea --- /dev/null +++ b/test/pure-component-test2.output.js @@ -0,0 +1,22 @@ +'use strict'; + +var React = require('React'); + +function render() { + return
; +} + +const Pure = props => { + return
; +}; + +class Impure extends React.Component { + componentWillMount() { + // such impure + } + render() { + return
; + } +} + +var A = props =>
; diff --git a/transforms/pure-component.js b/transforms/pure-component.js new file mode 100644 index 00000000..dc30df0b --- /dev/null +++ b/transforms/pure-component.js @@ -0,0 +1,100 @@ +module.exports = function(file, api, options) { + const j = api.jscodeshift; + const ReactUtils = require('./utils/ReactUtils')(j); + + const useArrows = options.useArrows || false; + const silenceWarnings = options.silenceWarnings || false; + const printOptions = options.printOptions || { + quote: 'single', + trailingComma: true + }; + + const getClassName = path => + path.node.id.name; + + const isRenderMethod = node => ( + node.type == 'MethodDefinition' && + node.key.type == 'Identifier' && + node.key.name == 'render' + ); + + const onlyHasRenderMethod = path => + j(path) + .find(j.MethodDefinition) + .filter(p => !isRenderMethod(p.value)) + .size() === 0; + + const THIS_PROPS = { + object: { + type: 'ThisExpression' + }, + property: { + name: 'props' + } + }; + + const replaceThisProps = path => + j(path) + .find(j.MemberExpression, THIS_PROPS) + .replaceWith(j.identifier('props')); + + const buildPureComponentFunction = (name, body) => + j.functionDeclaration( + j.identifier(name), + [j.identifier('props')], + body + ); + + const buildPureComponentArrowFunction = (name, body) => + j.variableDeclaration( + 'const', [ + j.variableDeclarator( + j.identifier(name), + j.arrowFunctionExpression( + [j.identifier('props')], + body + ) + ) + ] + ); + + const reportSkipped = path => { + const name = getClassName(path); + const fileName = file.path; + const {line, column} = path.value.loc.start; + + console.warn(`Class "${name}" skipped in ${fileName} on ${line}:${column}`); + }; + + const f = j(file.source); + + const pureClasses = ReactUtils.findReactES6ClassDeclaration(f) + .filter(path => { + const isPure = onlyHasRenderMethod(path); + if (!isPure && !silenceWarnings) { + reportSkipped(path); + } + return isPure; + }); + + if (pureClasses.size() === 0) { + return null; + } + + pureClasses.replaceWith(p => { + const name = p.node.id.name; + const renderMethod = p.value.body.body.filter(isRenderMethod)[0]; + const renderBody = renderMethod.value.body; + + replaceThisProps(renderBody); + + if (useArrows) { + return buildPureComponentArrowFunction(name, renderBody); + } else { + return buildPureComponentFunction(name, renderBody); + } + }) + + return f.toSource(printOptions); +}; +