├── .gitignore ├── README.md ├── bind-this-to-bind-expression.js ├── call-expression-bind-this-to-arrow-function-expression.js ├── function-expression-to-arrow-function-expression.js ├── props-to-destructuring.js └── pure-to-composite-component.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A collection of the js transforms using js-codeshift that I'm playing around with. -------------------------------------------------------------------------------- /bind-this-to-bind-expression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This transform converts stuff like: 3 | * 4 | * let x = this.foo.bind(this); 5 | * 6 | * to 7 | * 8 | * let x = ::this.foo; 9 | * 10 | */ 11 | 12 | module.exports = function (file, api) { 13 | const j = api.jscodeshift; 14 | return j(file.source) 15 | // Find stuff that looks like this.xyz.bind(this) 16 | .find(j.CallExpression, {callee: {object: {object: j.ThisExpression}, property: {name: 'bind'}}}) 17 | // Ensure that .bind() is being called with only one argument, and that argument is "this". 18 | .filter(p => p.value.arguments.length == 1 && p.value.arguments[0].type == "ThisExpression") 19 | // We can now replace it with ::this.xyz 20 | .replaceWith(p => j.bindExpression(null, p.value.callee.object)) 21 | .toSource(); 22 | }; -------------------------------------------------------------------------------- /call-expression-bind-this-to-arrow-function-expression.js: -------------------------------------------------------------------------------- 1 | /** Converts 2 | * onClick(function(a, b) { 3 | * return a + b; 4 | * }.bind(this), 5 | * function(b, c) { 6 | * return 1; 7 | * }.bind(this)); 8 | * 9 | * onClick(function(a) { 10 | * var a = 1; 11 | * return a; 12 | * }.bind(this)); 13 | * 14 | * var a = function(c) { return c; }.bind(this); 15 | * 16 | ** to 17 | * onClick((a, b) => a + b, 18 | * (b, c) => 1); 19 | * 20 | * onClick(a => { 21 | * var a = 1; 22 | * return a; 23 | * }); 24 | * 25 | * var a = c => c; 26 | * 27 | */ 28 | 29 | module.exports = function(file, api) { 30 | const j = api.jscodeshift; 31 | 32 | return j(file.source) 33 | // We're looking for a CallExpression that's calling .bind() onto a FunctionExpression. 34 | .find(j.CallExpression, {callee: {property: {name: 'bind'}, object: {type: 'FunctionExpression'}}}) 35 | // Verify that .bind() is only being called with `this` as it's sole arguments. 36 | .filter(p => p.value.arguments.length == 1 && p.value.arguments[0].type == "ThisExpression") 37 | .replaceWith(p => { 38 | // Grab the function body. Since we looked for the CallExpression originally, the "callee.object" would refer 39 | // to the FunctionExpression that's being called .bind(this) on. We need the body of that function 40 | // to transform into an ArrowFunctionExpression. 41 | var body = p.value.callee.object.body; 42 | // We can get a bit clever here. If we have a function that consists of a single return statement in it's body, 43 | // we can transform it to the more compact arrowFunctionExpression (a, b) => a + b, vs (a + b) => { return a + b } 44 | var useExpression = body.type == 'BlockStatement' && body.body.length == 1 && body.body[0].type == "ReturnStatement"; 45 | body = useExpression ? body.body[0].argument : body; 46 | 47 | return j.arrowFunctionExpression(p.value.callee.object.params, body, useExpression); 48 | }) 49 | .toSource(); 50 | }; -------------------------------------------------------------------------------- /function-expression-to-arrow-function-expression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a FunctionExpression to an ArrowFunctionExpression when safe to do so. 3 | * 4 | * var a = function(a, b) { 5 | * return a + b; 6 | * } 7 | * 8 | * var b = function(a, b) { 9 | * var c = 0; 10 | * return a + b + c; 11 | * } 12 | * 13 | * var a = function(a, b) { 14 | * return a + b + this.c; 15 | * } 16 | ** 17 | * var a = (a, b) => a + b 18 | * 19 | * var b = (a, b) => { 20 | * var c = 0; 21 | * return a + b + c; 22 | * } 23 | * 24 | * var a = function(a, b) { 25 | * return a + b + this.c; 26 | * } 27 | */ 28 | 29 | module.exports = function (file, api) { 30 | const j = api.jscodeshift; 31 | 32 | return j(file.source) 33 | .find(j.FunctionExpression) 34 | // We check for this expression, as if it's in a function expression, we don't want to re-bind "this" by 35 | // using the arrowFunctionExpression. As that could potentially have some unintended consequences. 36 | .filter(p => j(p).find(j.ThisExpression).size() == 0) 37 | .replaceWith(p => { 38 | var body = p.value.body; 39 | // We can get a bit clever here. If we have a function that consists of a single return statement in it's body, 40 | // we can transform it to the more compact arrowFunctionExpression (a, b) => a + b, vs (a + b) => { return a + b } 41 | var useExpression = body.type == 'BlockStatement' && body.body.length == 1 && body.body[0].type == "ReturnStatement"; 42 | body = useExpression ? body.body[0].argument : body; 43 | return j.arrowFunctionExpression(p.value.params, body, useExpression); 44 | }) 45 | .toSource(); 46 | }; 47 | -------------------------------------------------------------------------------- /props-to-destructuring.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms: 3 | * class C extends React.Component() { 4 | * render() { 5 | * return
6 | * } 7 | * } 8 | *** To: 9 | * 10 | * class C extends React.Component() { 11 | * render() { 12 | * const { 13 | * foo, 14 | * bar 15 | * } = this.props; 16 | * 17 | * return 18 | * } 19 | * } 20 | * 21 | */ 22 | 23 | let keywords = 'this function if return var else for new in typeof while case break try catch delete throw switch continue default instanceof do void finally with debugger implements interface package private protected public static class enum export extends import super true false null abstract boolean byte char const double final float goto int long native short synchronized throws transient volatile'; 24 | keywords = keywords.split(' ').reduce((f, k) => { 25 | f[k] = true; 26 | return f; 27 | }, {}); 28 | const isKeyword = k => keywords.hasOwnProperty(k); 29 | 30 | module.exports = function (file, api) { 31 | const j = api.jscodeshift; 32 | const {statement} = j.template; 33 | 34 | return j(file.source) 35 | .find(j.FunctionExpression) 36 | .replaceWith(p => { 37 | console.log(p); 38 | const root = j(p.value); 39 | const variablesToReplace = {}; 40 | 41 | // Figure out if the variable was defined from props, so that we can re-use that definition. 42 | const isFromProps = (name, resolvedScope) => { 43 | return resolvedScope.getBindings()[name].every( 44 | p => { 45 | const decl = j(p).closest(j.VariableDeclarator); 46 | // What happens when our VariableDeclarator is too high up the parent AST? 47 | 48 | if (!decl.size()) return false; 49 | const node = decl.nodes()[0]; 50 | 51 | if (!(node.init.type == 'MemberExpression' && 52 | node.init.object.type == 'ThisExpression' && 53 | node.init.property.name == 'props')) 54 | return false; 55 | 56 | // Check for the case where it could be aliased (i.e.) { baz: foo } = this.props; 57 | // In this case, we won't do a substitution. 58 | if (p.parentPath.value.type == 'Property' && p.parentPath.value.key.name !== name) 59 | return false; 60 | 61 | return true; 62 | } 63 | ); 64 | }; 65 | 66 | // Transform "this.props.xyz" to "xyz", and record what we've transformed. 67 | // Transform as long as we don't have "xyz" already defined in the scope. 68 | root 69 | .find(j.MemberExpression, { 70 | object: { 71 | type: 'MemberExpression', 72 | object: {type: 'ThisExpression'}, 73 | property: {name: 'props'} 74 | } 75 | }) 76 | .filter(e => { 77 | const resolvedScope = e.scope.lookup(e.value.property.name); 78 | // If the scope is null, that means that this property isn't defined in the scope yet, 79 | // and we can use it. Otherwise, if it is defined, we should see if it was defined from `this.props` 80 | // if none of these cases are true, we can't do substitution. 81 | return resolvedScope == null || isFromProps(e.value.property.name, resolvedScope); 82 | }) 83 | // Ensure that our substitution won't cause us to define a keyword, i.e. `this.props.while` won't 84 | // get converted into `while`. 85 | .filter(p => !isKeyword(p.value.property.name)) 86 | // Now, do the replacement, `this.props.xyz` => `xyz`. 87 | .replaceWith(p => p.value.property) 88 | // Finally, mark the variable as something we will need to define earlier in the function, 89 | // if it's not already defined. 90 | .forEach(p => { 91 | // Is this prop already defined somewhere else. 92 | if (!p.scope.lookup(p.value.name)) 93 | variablesToReplace[p.value.name] = true; 94 | }); 95 | 96 | 97 | // Create property definitions for variables that we've replaced. 98 | const properties = Object.keys(variablesToReplace) 99 | .sort() 100 | .map(k => { 101 | const prop = j.property('init', j.identifier(k), j.identifier(k)); 102 | prop.shorthand = true; 103 | return prop; 104 | }); 105 | 106 | // We have no properties to inject, so we can bail here. 107 | if (!properties.length) 108 | return p.value; 109 | 110 | // See if we already have a VariableDeclarator like { a, b, c } = this.props; 111 | const propDefinitions = root 112 | .find(j.VariableDeclarator, { 113 | id: {type: 'ObjectPattern'}, 114 | init: {type: 'MemberExpression', object: {type: 'ThisExpression'}, property: {name: 'props'}} 115 | }); 116 | 117 | if (propDefinitions.size()) { 118 | const nodePath = propDefinitions.paths()[0]; 119 | const node = nodePath.value; 120 | const newPattern = j.objectPattern(node.id.properties.concat(properties)); 121 | nodePath.replace(j.variableDeclarator(newPattern, node.init)); 122 | return p.value; 123 | } 124 | 125 | // Otherwise, we'll have to create our own, as none were suitable for use. 126 | // Create the variable definition `const { xyz } = this.props;` 127 | const decl = statement`const { ${properties} } = this.props;`; 128 | 129 | // Add the variable definition to the top of the function expression body. 130 | return j.functionExpression( 131 | p.value.id, 132 | p.value.params, 133 | j.blockStatement([decl].concat(p.value.body.body)) 134 | ); 135 | } 136 | ).toSource(); 137 | }; 138 | -------------------------------------------------------------------------------- /pure-to-composite-component.js: -------------------------------------------------------------------------------- 1 | /** For when you've gone too pure and want to go back. **/ 2 | /** Converts 3 | * let HistoryItem = (props) => { 4 | * const { 5 | * item 6 | * } = props; 7 | * return