├── .gitignore ├── tests ├── sort-imports │ ├── codemod.spec.js │ ├── only-imports.js │ ├── with-body.js │ ├── incorrect-docblocks.js │ └── __snapshots__ │ │ └── codemod.spec.js.snap ├── i18n-mixin-to-localize │ ├── codemod.spec.js │ ├── basic.js │ └── __snapshots__ │ │ └── codemod.spec.js.snap ├── merge-lodash-imports │ ├── codemod.spec.js │ ├── basic.js │ ├── comments-in-between.js │ └── __snapshots__ │ │ └── codemod.spec.js.snap ├── modular-lodash-no-more │ ├── codemod.spec.js │ ├── basic.js │ └── __snapshots__ │ │ └── codemod.spec.js.snap ├── rename-combine-reducers │ ├── codemod.spec.js │ ├── basic.js │ └── __snapshots__ │ │ └── codemod.spec.js.snap ├── combine-state-utils-imports │ ├── codemod.spec.js │ ├── basic.js │ └── __snapshots__ │ │ └── codemod.spec.js.snap ├── remove-create-reducer │ ├── codemod.spec.js │ ├── create-reducer.js │ └── __snapshots__ │ │ └── codemod.spec.js.snap ├── combine-reducer-with-persistence │ ├── codemod.spec.js │ ├── basic.js │ └── __snapshots__ │ │ └── codemod.spec.js.snap └── modular-lodash-requires-no-more │ ├── codemod.spec.js │ ├── basic.js │ ├── comments-in-between.js │ └── __snapshots__ │ └── codemod.spec.js.snap ├── .vscode └── settings.json ├── .travis.yml ├── .prettierc ├── setup-tests.js ├── package.json ├── index.js ├── transforms ├── modular-lodash-no-more.js ├── combine-reducer-with-persistence.js ├── combine-state-utils-imports.js ├── merge-lodash-imports.js ├── rename-combine-reducers.js ├── modular-lodash-requires-no-more.js ├── sort-imports.js ├── i18n-mixin-to-localize.js ├── remove-create-reducer.js └── single-tree-rendering.js ├── config.js ├── api.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /tests/sort-imports/codemod.spec.js: -------------------------------------------------------------------------------- 1 | test_folder(__dirname); 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.singleQuote": false 3 | } -------------------------------------------------------------------------------- /tests/i18n-mixin-to-localize/codemod.spec.js: -------------------------------------------------------------------------------- 1 | test_folder(__dirname); -------------------------------------------------------------------------------- /tests/merge-lodash-imports/codemod.spec.js: -------------------------------------------------------------------------------- 1 | test_folder(__dirname); -------------------------------------------------------------------------------- /tests/modular-lodash-no-more/codemod.spec.js: -------------------------------------------------------------------------------- 1 | test_folder(__dirname); -------------------------------------------------------------------------------- /tests/rename-combine-reducers/codemod.spec.js: -------------------------------------------------------------------------------- 1 | test_folder(__dirname); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | cache: yarn 5 | -------------------------------------------------------------------------------- /tests/combine-state-utils-imports/codemod.spec.js: -------------------------------------------------------------------------------- 1 | test_folder(__dirname); -------------------------------------------------------------------------------- /tests/remove-create-reducer/codemod.spec.js: -------------------------------------------------------------------------------- 1 | test_folder(__dirname); 2 | -------------------------------------------------------------------------------- /tests/combine-reducer-with-persistence/codemod.spec.js: -------------------------------------------------------------------------------- 1 | test_folder(__dirname); -------------------------------------------------------------------------------- /tests/modular-lodash-requires-no-more/codemod.spec.js: -------------------------------------------------------------------------------- 1 | test_folder(__dirname); -------------------------------------------------------------------------------- /tests/modular-lodash-no-more/basic.js: -------------------------------------------------------------------------------- 1 | import map from "lodash/map"; 2 | import zip from "lodash/zip"; 3 | -------------------------------------------------------------------------------- /.prettierc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 2, 4 | "printWidth": 80, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /tests/sort-imports/only-imports.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import chickenLibrary from "chicken"; 3 | import okapiMe from "okapi-me"; -------------------------------------------------------------------------------- /tests/sort-imports/with-body.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import chickenLibrary from "chicken"; 3 | import okapiMe from "okapi-me"; 4 | 5 | const x = 5; -------------------------------------------------------------------------------- /tests/merge-lodash-imports/basic.js: -------------------------------------------------------------------------------- 1 | /* @format */ 2 | import { zip } from "lodash"; 3 | import { map } from "lodash"; 4 | import { pick } from "lodash"; 5 | -------------------------------------------------------------------------------- /tests/combine-state-utils-imports/basic.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from "state/utils"; 2 | import { combineReducersWithPersistence as bar, baz } from "state/utils"; 3 | -------------------------------------------------------------------------------- /tests/modular-lodash-requires-no-more/basic.js: -------------------------------------------------------------------------------- 1 | const zippy = require("lodash/zip"); 2 | const mappy = require("lodash/map"); 3 | const find = require("lodash/find"); 4 | -------------------------------------------------------------------------------- /tests/rename-combine-reducers/basic.js: -------------------------------------------------------------------------------- 1 | import { combineReducersWithPersistence } from "state/utils"; 2 | 3 | combineReducersWithPersistence({ 4 | foo, 5 | bar, 6 | }); 7 | -------------------------------------------------------------------------------- /tests/combine-reducer-with-persistence/basic.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | /** 4 | * Internal dependencies 5 | */ 6 | import hello from "internal/lib"; 7 | -------------------------------------------------------------------------------- /tests/modular-lodash-no-more/__snapshots__/codemod.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic.js 1`] = ` 4 | "import { map } from \\"lodash\\"; 5 | import { zip } from \\"lodash\\"; 6 | 7 | " 8 | `; 9 | -------------------------------------------------------------------------------- /tests/i18n-mixin-to-localize/basic.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PrettyComponent = React.createClass({ 4 | render() { 5 | this.translate("codemods are fun"); 6 | }, 7 | }); 8 | 9 | export default PrettyComponent; 10 | -------------------------------------------------------------------------------- /tests/combine-state-utils-imports/__snapshots__/codemod.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic.js 1`] = ` 4 | "import { baz, combineReducersWithPersistence as bar, createReducer } from \\"state/utils\\"; 5 | 6 | " 7 | `; 8 | -------------------------------------------------------------------------------- /tests/sort-imports/incorrect-docblocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import chickenLibrary from "chicken"; 5 | import okapiMe from "okapi-me"; 6 | 7 | /** 8 | * Magic dependencies 9 | */ 10 | import fs from "fs"; 11 | 12 | function hello() {} 13 | -------------------------------------------------------------------------------- /tests/rename-combine-reducers/__snapshots__/codemod.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic.js 1`] = ` 4 | "import { combineReducers } from \\"state/utils\\"; 5 | 6 | combineReducers({ 7 | foo, 8 | bar, 9 | }); 10 | 11 | " 12 | `; 13 | -------------------------------------------------------------------------------- /tests/merge-lodash-imports/comments-in-between.js: -------------------------------------------------------------------------------- 1 | // 1 - zippy 2 | import { zip } from "lodash"; 3 | // 2 - mappy 4 | import { map } from "lodash"; 5 | // 3 - picky 6 | import { pick } from "lodash"; 7 | /* 8 | * comment block. Note how this is currently broken and gets garbled in the snapshot 9 | */ 10 | -------------------------------------------------------------------------------- /tests/modular-lodash-requires-no-more/comments-in-between.js: -------------------------------------------------------------------------------- 1 | // 1 - zippy 2 | import { zip } from "lodash"; 3 | // 2 - mappy 4 | import { map } from "lodash"; 5 | // 3 - picky 6 | import { pick } from "lodash"; 7 | /* 8 | * comment block. Note how this is currently broken and gets garbled in the snapshot 9 | */ 10 | -------------------------------------------------------------------------------- /tests/combine-reducer-with-persistence/__snapshots__/codemod.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic.js 1`] = ` 4 | "/** 5 | * Internal dependencies 6 | */ 7 | import hello from \\"internal/lib\\"; 8 | 9 | import { combineReducers } from \\"state/utils\\"; 10 | 11 | " 12 | `; 13 | -------------------------------------------------------------------------------- /tests/merge-lodash-imports/__snapshots__/codemod.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic.js 1`] = ` 4 | "/* @format */ 5 | import { map, pick, zip } from \\"lodash\\"; 6 | 7 | " 8 | `; 9 | 10 | exports[`comments-in-between.js 1`] = ` 11 | "// 1 - zippy 12 | import { map, pick, zip } from \\"lodash\\"; 13 | 14 | " 15 | `; 16 | -------------------------------------------------------------------------------- /tests/modular-lodash-requires-no-more/__snapshots__/codemod.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic.js 1`] = ` 4 | "/** 5 | * External dependencies 6 | */ 7 | import { zip as zippy } from \\"lodash\\"; 8 | 9 | import { map as mappy } from \\"lodash\\"; 10 | import { find } from \\"lodash\\"; 11 | 12 | " 13 | `; 14 | 15 | exports[`comments-in-between.js 1`] = `""`; 16 | -------------------------------------------------------------------------------- /tests/i18n-mixin-to-localize/__snapshots__/codemod.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic.js 1`] = ` 4 | "import React from \\"react\\"; 5 | 6 | import { localize } from \\"i18n-calypso\\"; 7 | 8 | const PrettyComponent = React.createClass({ 9 | render() { 10 | this.props.translate(\\"codemods are fun\\"); 11 | }, 12 | }); 13 | 14 | export default localize(PrettyComponent); 15 | 16 | " 17 | `; 18 | -------------------------------------------------------------------------------- /setup-tests.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const api = require.requireActual("./api"); 4 | 5 | function test_folder(dir) { 6 | const testFiles = fs.readdirSync(dir).filter(f => !f.endsWith(".spec.js") && f.endsWith(".js")); 7 | 8 | testFiles.forEach(filename => { 9 | const filepath = path.join(dir, filename); 10 | 11 | test(filename, () => { 12 | const result = api.runCodemodDry(path.basename(dir), filepath); 13 | expect(result).toMatchSnapshot(); 14 | }); 15 | }); 16 | } 17 | 18 | global.test_folder = test_folder; 19 | -------------------------------------------------------------------------------- /tests/sort-imports/__snapshots__/codemod.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`incorrect-docblocks.js 1`] = ` 4 | "/** 5 | * External dependencies 6 | */ 7 | import fs from \\"fs\\"; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import chickenLibrary from \\"chicken\\"; 13 | import okapiMe from \\"okapi-me\\"; 14 | 15 | function hello() {} 16 | 17 | " 18 | `; 19 | 20 | exports[`only-imports.js 1`] = ` 21 | "/** 22 | * External dependencies 23 | */ 24 | import fs from \\"fs\\"; 25 | 26 | /** 27 | * Internal dependencies 28 | */ 29 | import chickenLibrary from \\"chicken\\"; 30 | import okapiMe from \\"okapi-me\\"; 31 | " 32 | `; 33 | 34 | exports[`with-body.js 1`] = ` 35 | "/** 36 | * External dependencies 37 | */ 38 | import fs from \\"fs\\"; 39 | 40 | /** 41 | * Internal dependencies 42 | */ 43 | import chickenLibrary from \\"chicken\\"; 44 | import okapiMe from \\"okapi-me\\"; 45 | 46 | const x = 5; 47 | " 48 | `; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calypso-codemods", 3 | "version": "0.1.5", 4 | "description": "jscodeshift transforms used to upgrade calypso code", 5 | "main": "./api.js", 6 | "bin": { 7 | "calypso-codemods": "./index.js" 8 | }, 9 | "scripts": { 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Automattic/calypso-codemods.git" 15 | }, 16 | "author": "samouri", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/Automattic/calypso-codemods/issues" 20 | }, 21 | "homepage": "https://github.com/Automattic/calypso-codemods#readme", 22 | "devDependencies": { 23 | "jest": "^23.2.0", 24 | "prettier": "^1.13.6" 25 | }, 26 | "dependencies": { 27 | "5to6-codemod": "^1.7.1", 28 | "jscodeshift": "^0.5.1", 29 | "lodash": "^4.17.10", 30 | "react-codemod": "github:reactjs/react-codemod" 31 | }, 32 | "jest": { 33 | "setupFiles": [ 34 | "/setup-tests.js" 35 | ], 36 | "testRegex": "codemod\\.spec\\.js$" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const child_process = require('child_process'); 9 | const glob = require('glob'); 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | const config = require('./config'); 15 | const api = require('./api'); 16 | 17 | function main() { 18 | const args = process.argv.slice(2); 19 | if (args.length === 0 || args.length === 1) { 20 | process.stdout.write( 21 | [ 22 | '', 23 | 'calypso-codemods codemodName[,additionalCodemods…] target1 [additionalTargets…]', 24 | '', 25 | 'Valid transformation names:', 26 | api.getValidCodemodNames().join('\n'), 27 | '', 28 | 'Example: "calypso-codemods commonjs-imports client/blocks client/devdocs"', 29 | '', 30 | ].join('\n') 31 | ); 32 | 33 | process.exit(0); 34 | } 35 | 36 | const [names, ...targets] = args; 37 | names.split(',').forEach(codemodName => api.runCodemod(codemodName, targets)); 38 | } 39 | 40 | main(); 41 | -------------------------------------------------------------------------------- /transforms/modular-lodash-no-more.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Replaces lodash modular imports with ES2015-style imports 3 | * 4 | * Note: this does not attempt to merge imports into existing 5 | * 'lodash' imports in the same module. This process 6 | * should be handled in a separate codemod 7 | * 8 | * @example 9 | * // input 10 | * import _map from 'lodash/map'; 11 | * 12 | * // output 13 | * import { map as _map } from 'lodash' 14 | * 15 | * @format 16 | * 17 | * @param file 18 | * @param api 19 | * @returns {string} 20 | */ 21 | 22 | export default function transformer(file, api) { 23 | const j = api.jscodeshift; 24 | 25 | return j(file.source) 26 | .find(j.ImportDeclaration) 27 | .filter(dec => dec.value.source.value.startsWith("lodash/")) 28 | .replaceWith(node => { 29 | return Object.assign( 30 | j.importDeclaration( 31 | [ 32 | j.importSpecifier( 33 | j.identifier(node.value.source.value.replace("lodash/", "")), 34 | j.identifier(node.value.specifiers[0].local.name) 35 | ), 36 | ], 37 | j.literal("lodash") 38 | ), 39 | { 40 | comments: node.value.comments, 41 | } 42 | ); 43 | }) 44 | .toSource(); 45 | } 46 | -------------------------------------------------------------------------------- /tests/remove-create-reducer/create-reducer.js: -------------------------------------------------------------------------------- 1 | // This comment should be preserved even if the line below is removed. 2 | import { createReducer, createReducerWithValidation } from 'state/utils'; 3 | 4 | const COMPUTED_IDENTIFIER = 'COMPUTED_IDENTIFIER'; 5 | 6 | const isFetchingSettings = createReducer( false, { 7 | [ COMPUTED_IDENTIFIER ]: () => 'computed_id', 8 | [ 'COMPUTED_STRING' ]: state => state, 9 | NON_COMPUTED_STRING: ( state, action ) => action.thing, 10 | 2: () => 2, 11 | FUNCTION_HANDLER: function( s, a ) { 12 | return s; 13 | }, 14 | ARROW_FUNCTION_HANDLER: ( state, action ) => state, 15 | ARROW_FUNCTION_WITH_DESTRUCT: ( state, { thing } ) => thing, 16 | VARIABLE_HANDLER: f, 17 | } ); 18 | 19 | function f() { 20 | return 'a function reducer'; 21 | } 22 | 23 | const persistentReducer = createReducer(false, { 24 | [COMPUTED_IDENTIFIER]: () => "computed_id", 25 | ["SERIALIZE"]: state => state, 26 | }); 27 | 28 | export const exportedPersistentReducer = createReducer(false, { 29 | [COMPUTED_IDENTIFIER]: () => "computed_id", 30 | ["SERIALIZE"]: state => state, 31 | }); 32 | 33 | const persistentReducerArray = []; 34 | reducerArray[0] = createReducer(false, { 35 | [COMPUTED_IDENTIFIER]: () => "computed_id", 36 | ["DESERIALIZE"]: state => state, 37 | }); 38 | 39 | const persistentReducerObj = { 40 | key: createReducer(false, { 41 | [COMPUTED_IDENTIFIER]: () => "computed_id", 42 | ["DESERIALIZE"]: state => state, 43 | }) 44 | }; 45 | 46 | const validatedReducer = createReducerWithValidation(false, { 47 | [COMPUTED_IDENTIFIER]: () => "computed_id", 48 | }, schema); 49 | -------------------------------------------------------------------------------- /transforms/combine-reducer-with-persistence.js: -------------------------------------------------------------------------------- 1 | /* 2 | This codemod updates 3 | 4 | import { combineReducers } from 'redux'; to 5 | import { combineReducers } from 'state/utils'; 6 | */ 7 | 8 | module.exports = function(file, api) { 9 | // alias the jscodeshift API 10 | const j = api.jscodeshift; 11 | // parse JS code into an AST 12 | const root = j(file.source); 13 | 14 | //remove combineReducer import 15 | const combineReducerImport = root 16 | .find(j.ImportDeclaration, { 17 | source: { 18 | type: "Literal", 19 | value: "redux", 20 | }, 21 | }) 22 | .filter(importDeclaration => { 23 | if (importDeclaration.value.specifiers.length === 1) { 24 | return importDeclaration.value.specifiers[0].imported.name === "combineReducers"; 25 | } 26 | return false; 27 | }); 28 | 29 | if (!combineReducerImport.length) { 30 | return; 31 | } 32 | 33 | combineReducerImport.remove(); 34 | 35 | // find the first external import 36 | const firstInternalImport = root.find(j.ImportDeclaration).filter(item => { 37 | if (item.node.comments && item.node.comments.length > 0) { 38 | return item.node.comments[0].value.match(/Internal dependencies/); 39 | } 40 | return false; 41 | }); 42 | 43 | const combineReducersImport = () => { 44 | return j.importDeclaration( 45 | [j.importSpecifier(j.identifier("combineReducers"))], 46 | j.literal("state/utils") 47 | ); 48 | }; 49 | //note the extra whitespace coming from https://github.com/benjamn/recast/issues/371 50 | firstInternalImport.insertAfter(combineReducersImport); 51 | 52 | // print 53 | return root.toSource(); 54 | }; 55 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const jscodeshiftArgs = [ '--extensions=js,jsx' ]; 2 | 3 | // Used primarily by 5to6-codemod transformations 4 | const recastArgs = [ '--useTabs=true', '--arrayBracketSpacing=true' ]; 5 | 6 | const recastOptions = { 7 | arrayBracketSpacing: true, 8 | objectCurlySpacing: true, 9 | quote: 'single', 10 | useTabs: true, 11 | trailingComma: { 12 | objects: true, 13 | arrays: true, 14 | parameters: false, 15 | }, 16 | }; 17 | 18 | const commonArgs = { 19 | '5to6': [ 20 | // Recast options via 5to6 21 | ...recastArgs, 22 | ], 23 | react: [ 24 | // Recast options via react-codemod 25 | `--printOptions=${ JSON.stringify( recastOptions ) }`, 26 | ], 27 | }; 28 | 29 | const codemodArgs = { 30 | 'commonjs-exports': [ 31 | ...commonArgs[ '5to6' ], 32 | '--transform=node_modules/5to6-codemod/transforms/exports.js', 33 | ], 34 | 35 | 'commonjs-imports': [ 36 | ...commonArgs[ '5to6' ], 37 | '--transform=node_modules/5to6-codemod/transforms/cjs.js', 38 | ], 39 | 40 | 'commonjs-imports-hoist': [ 41 | ...commonArgs[ '5to6' ], 42 | '--transform=node_modules/5to6-codemod/transforms/cjs.js', 43 | '--hoist=true', 44 | ], 45 | 46 | 'named-exports-from-default': [ 47 | ...commonArgs[ '5to6' ], 48 | '--transform=node_modules/5to6-codemod/transforms/named-export-generation.js', 49 | ], 50 | 51 | 'react-create-class': [ 52 | ...commonArgs[ 'react' ], 53 | '--transform=node_modules/react-codemod/transforms/class.js', 54 | 55 | // react-codemod options 56 | '--pure-component=true', 57 | '--mixin-module-name="react-pure-render/mixin"', // Your days are numbered, pure-render-mixin! 58 | ], 59 | 60 | 'react-proptypes': [ 61 | ...commonArgs[ 'react' ], 62 | '--transform=node_modules/react-codemod/transforms/React-PropTypes-to-prop-types.js', 63 | ], 64 | }; 65 | 66 | module.exports = { 67 | codemodArgs, 68 | jscodeshiftArgs, 69 | recastOptions, 70 | }; 71 | -------------------------------------------------------------------------------- /transforms/combine-state-utils-imports.js: -------------------------------------------------------------------------------- 1 | /* 2 | This codemod updates 3 | 4 | import { createReducer } from 'state/utils'; 5 | import { combineReducersWithPersistence as bar, baz } from 'state/utils' 6 | 7 | to 8 | 9 | import { baz, combineReducersWithPersistence as bar, createReducer } from 'state/utils'; 10 | */ 11 | 12 | module.exports = function(file, api) { 13 | // alias the jscodeshift API 14 | const j = api.jscodeshift; 15 | // parse JS code into an AST 16 | const root = j(file.source); 17 | 18 | const stateUtilsImports = root.find(j.ImportDeclaration, { 19 | source: { 20 | type: "Literal", 21 | value: "state/utils", 22 | }, 23 | }); 24 | 25 | if (stateUtilsImports.length < 2) { 26 | return; 27 | } 28 | 29 | //grab each identifier 30 | const importNames = []; 31 | stateUtilsImports.find(j.ImportSpecifier).forEach(item => { 32 | importNames.push({ 33 | local: item.value.local.name, 34 | imported: item.value.imported.name, 35 | }); 36 | }); 37 | 38 | //sort by imported name 39 | importNames.sort((a, b) => { 40 | if (a.imported < b.imported) { 41 | return -1; 42 | } 43 | if (a.imported > b.imported) { 44 | return 1; 45 | } 46 | return 0; 47 | }); 48 | 49 | //Save Comment if possible 50 | const comments = stateUtilsImports.at(0).get().node.comments; 51 | 52 | const addImport = importNames => { 53 | const names = importNames.map(name => { 54 | if (name.local === name.imported) { 55 | return j.importSpecifier(j.identifier(name.local)); 56 | } 57 | if (name.local !== name.imported) { 58 | return j.importSpecifier(j.identifier(name.imported), j.identifier(name.local)); 59 | } 60 | }); 61 | const combinedImport = j.importDeclaration(names, j.literal("state/utils")); 62 | combinedImport.comments = comments; 63 | return combinedImport; 64 | }; 65 | 66 | //replace the first one with the combined import 67 | stateUtilsImports.at(0).replaceWith(addImport(importNames)); 68 | //remove the rest 69 | for (let i = 1; i < stateUtilsImports.length; i++) { 70 | stateUtilsImports.at(i).remove(); 71 | } 72 | 73 | // print 74 | return root.toSource(); 75 | }; 76 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | const fs = require("fs"); 7 | const path = require("path"); 8 | const child_process = require("child_process"); 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | const config = require(path.join(__dirname, "config")); 14 | const transformsDir = path.join(__dirname, "./transforms"); 15 | 16 | function getLocalCodemodFileNames() { 17 | const jsFiles = fs 18 | .readdirSync(transformsDir) 19 | .filter(filename => filename.endsWith(".js")) 20 | .map(name => path.basename(name, ".js")); // strip path and extension from filename 21 | 22 | return jsFiles; 23 | } 24 | 25 | function getValidCodemodNames() { 26 | return [...getLocalCodemodFileNames(), ...Object.getOwnPropertyNames(config.codemodArgs)] 27 | .map(name => "- " + name) 28 | .sort(); 29 | } 30 | 31 | function generateBinArgs(name) { 32 | if (config.codemodArgs.hasOwnProperty(name)) { 33 | // Is the codemod defined in the codemodArgs object? 34 | return config.codemodArgs[name]; 35 | } 36 | 37 | if (getLocalCodemodFileNames().includes(name)) { 38 | return [`--transform=${transformsDir}/${name}.js`]; 39 | } 40 | 41 | throw new Error(`"${name}" is an unrecognized codemod.`); 42 | } 43 | 44 | function runCodemod(codemodName, transformTargets) { 45 | const binArgs = [...config.jscodeshiftArgs, ...generateBinArgs(codemodName), ...transformTargets]; 46 | 47 | process.stdout.write(`\nRunning ${codemodName} on ${transformTargets.join(" ")}\n`); 48 | 49 | const binPath = path.join(".", "node_modules", ".bin", "jscodeshift"); 50 | const jscodeshift = child_process.spawnSync(binPath, binArgs, { 51 | stdio: ["ignore", process.stdout, process.stderr], 52 | }); 53 | } 54 | 55 | function runCodemodDry(codemodName, filepath) { 56 | const binArgs = [ 57 | ...config.jscodeshiftArgs, 58 | ...generateBinArgs(codemodName), 59 | "--dry", 60 | "--print", 61 | "--silent", 62 | filepath, 63 | ]; 64 | const binPath = path.join(".", "node_modules", ".bin", "jscodeshift"); 65 | 66 | const result = child_process.spawnSync(binPath, binArgs, { 67 | stdio: "pipe", 68 | }); 69 | 70 | return result.stdout.toString(); 71 | } 72 | 73 | module.exports = { 74 | runCodemod, 75 | runCodemodDry, 76 | generateBinArgs, 77 | getValidCodemodNames, 78 | getLocalCodemodFileNames, 79 | }; 80 | -------------------------------------------------------------------------------- /transforms/merge-lodash-imports.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Merges multiple lodash imports into a single statement 3 | * 4 | * @example 5 | * // input 6 | * import { zip } from 'lodash'; 7 | * import { map } from 'lodash'; 8 | * import { pick } from 'lodash'; 9 | * 10 | * // output 11 | * import { map, pick, zip } from 'lodash' 12 | * 13 | * @format 14 | * 15 | * @param file 16 | * @param api 17 | * @returns {string} 18 | */ 19 | 20 | export default function transformer(file, api) { 21 | const specSorter = (a, b) => a.imported.name.localeCompare(b.imported.name); 22 | 23 | const j = api.jscodeshift; 24 | 25 | const source = j(file.source); 26 | const lodash = new Set(); 27 | const decs = []; 28 | const maps = new Map(); 29 | 30 | const sourceDecs = source.find(j.ImportDeclaration, { 31 | source: { value: "lodash" }, 32 | }); 33 | 34 | // bail if we only have a single declaration 35 | if (sourceDecs.nodes().length === 1) { 36 | return file.source; 37 | } 38 | 39 | sourceDecs.forEach(dec => { 40 | decs.push(dec); 41 | j(dec) 42 | .find(j.ImportSpecifier) 43 | .forEach(spec => { 44 | const local = spec.value.local.name; 45 | const name = spec.value.imported.name; 46 | 47 | if (local === name) { 48 | lodash.add(name); 49 | } else { 50 | maps.set(name, (maps.get(name) || new Set()).add(local)); 51 | } 52 | }); 53 | }); 54 | 55 | // Insert new statement above first existing lodash import 56 | if (decs.length) { 57 | const newSpecs = Array.from(lodash).map(name => 58 | j.importSpecifier(j.identifier(name), j.identifier(name)) 59 | ); 60 | 61 | // start adding renamed imports 62 | const renames = []; 63 | maps.forEach((localSet, name) => { 64 | const locals = Array.from(localSet); 65 | const hasDefault = lodash.has(name); 66 | const topName = hasDefault ? name : locals[0]; 67 | 68 | // add first renamed import if no default 69 | // already exists in import statement 70 | if (!hasDefault) { 71 | locals.shift(); 72 | newSpecs.push(j.importSpecifier(j.identifier(name), j.identifier(topName))); 73 | } 74 | 75 | // add remaining renames underneath 76 | locals.forEach(local => { 77 | const rename = j.variableDeclaration("const", [ 78 | j.variableDeclarator(j.identifier(local), j.identifier(topName)), 79 | ]); 80 | 81 | renames.push(rename); 82 | }); 83 | }); 84 | 85 | // sort and insert… 86 | const newImport = j.importDeclaration(newSpecs.sort(specSorter), j.literal("lodash")); 87 | newImport.comments = decs[0].value.comments; 88 | j(decs[0]).insertBefore([newImport, ...renames]); 89 | } 90 | 91 | // remove old declarations 92 | decs.forEach(dec => j(dec).remove()); 93 | 94 | return source.toSource(); 95 | } 96 | -------------------------------------------------------------------------------- /tests/remove-create-reducer/__snapshots__/codemod.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`create-reducer.js 1`] = ` 4 | "// This comment should be preserved even if the line below is removed. 5 | import { withSchemaValidation, withoutPersistence } from 'state/utils'; 6 | 7 | const COMPUTED_IDENTIFIER = 'COMPUTED_IDENTIFIER'; 8 | 9 | const isFetchingSettings = withoutPersistence((state = false, action) => { 10 | switch (action.type) { 11 | case COMPUTED_IDENTIFIER: 12 | return 'computed_id'; 13 | case 'COMPUTED_STRING': 14 | return state; 15 | case \\"NON_COMPUTED_STRING\\": 16 | return action.thing; 17 | case \\"2\\": 18 | return 2; 19 | case \\"FUNCTION_HANDLER\\": 20 | return function( s, a ) { 21 | return s; 22 | }(state, action); 23 | case \\"ARROW_FUNCTION_HANDLER\\": 24 | return state; 25 | case \\"ARROW_FUNCTION_WITH_DESTRUCT\\": 26 | { 27 | const { thing } = action; 28 | return thing; 29 | } 30 | case \\"VARIABLE_HANDLER\\": 31 | return f(state, action); 32 | } 33 | 34 | return state; 35 | }); 36 | 37 | function f() { 38 | return 'a function reducer'; 39 | } 40 | 41 | const persistentReducer = (state = false, action) => { 42 | switch (action.type) { 43 | case COMPUTED_IDENTIFIER: 44 | return \\"computed_id\\"; 45 | case \\"SERIALIZE\\": 46 | return state; 47 | } 48 | 49 | return state; 50 | }; 51 | 52 | persistentReducer.hasCustomPersistence = true; 53 | 54 | export const exportedPersistentReducer = (state = false, action) => { 55 | switch (action.type) { 56 | case COMPUTED_IDENTIFIER: 57 | return \\"computed_id\\"; 58 | case \\"SERIALIZE\\": 59 | return state; 60 | } 61 | 62 | return state; 63 | }; 64 | 65 | exportedPersistentReducer.hasCustomPersistence = true; 66 | 67 | const persistentReducerArray = []; 68 | reducerArray[0] = (state = false, action) => { 69 | switch (action.type) { 70 | case COMPUTED_IDENTIFIER: 71 | return \\"computed_id\\"; 72 | case \\"DESERIALIZE\\": 73 | return state; 74 | } 75 | 76 | return state; 77 | }; 78 | 79 | reducerArray[0].hasCustomPersistence = true; 80 | 81 | const persistentReducerObj = { 82 | key: // TODO: HANDLE PERSISTENCE 83 | (state = false, action) => { 84 | switch (action.type) { 85 | case COMPUTED_IDENTIFIER: 86 | return \\"computed_id\\"; 87 | case \\"DESERIALIZE\\": 88 | return state; 89 | } 90 | 91 | return state; 92 | } 93 | }; 94 | 95 | const validatedReducer = withSchemaValidation(schema, (state = false, action) => { 96 | switch (action.type) { 97 | case COMPUTED_IDENTIFIER: 98 | return \\"computed_id\\"; 99 | } 100 | 101 | return state; 102 | }); 103 | 104 | " 105 | `; 106 | -------------------------------------------------------------------------------- /transforms/rename-combine-reducers.js: -------------------------------------------------------------------------------- 1 | /* 2 | This codemod updates 3 | 4 | import { combineReducersWithPersistence } from 'state/utils'; to 5 | import { combineReducers } from 'state/utils'; 6 | 7 | and updates 8 | 9 | combineReducersWithPersistence( { 10 | foo, 11 | bar 12 | } ); 13 | 14 | to 15 | 16 | combineReducers( { 17 | foo, 18 | bar 19 | } ); 20 | */ 21 | 22 | module.exports = function(file, api) { 23 | // alias the jscodeshift API 24 | const j = api.jscodeshift; 25 | // parse JS code into an AST 26 | const root = j(file.source); 27 | 28 | const importNames = []; 29 | 30 | const combineReducerImport = root 31 | .find(j.ImportDeclaration, { 32 | source: { 33 | type: "Literal", 34 | value: "state/utils", 35 | }, 36 | }) 37 | .filter(importDeclaration => { 38 | if (importDeclaration.value.specifiers.length > 0) { 39 | return ( 40 | importDeclaration.value.specifiers.filter(specifier => { 41 | const importedName = specifier.imported.name; 42 | const localName = specifier.local.name; 43 | const shouldRename = importedName === "combineReducersWithPersistence"; 44 | importNames.push({ 45 | local: localName === "combineReducersWithPersistence" ? "combineReducers" : localName, 46 | imported: shouldRename ? "combineReducers" : importedName, 47 | }); 48 | return shouldRename; 49 | }).length > 0 50 | ); 51 | } 52 | return false; 53 | }); 54 | 55 | if (!combineReducerImport.length) { 56 | return; 57 | } 58 | 59 | //sort by imported name 60 | importNames.sort((a, b) => { 61 | if (a.imported < b.imported) { 62 | return -1; 63 | } 64 | if (a.imported > b.imported) { 65 | return 1; 66 | } 67 | return 0; 68 | }); 69 | 70 | //save the comment if possible 71 | const comments = combineReducerImport.at(0).get().node.comments; 72 | const addImport = importNames => { 73 | const names = importNames.map(name => { 74 | if (name.local === name.imported) { 75 | return j.importSpecifier(j.identifier(name.local)); 76 | } 77 | if (name.local !== name.imported) { 78 | return j.importSpecifier(j.identifier(name.imported), j.identifier(name.local)); 79 | } 80 | }); 81 | const combinedImport = j.importDeclaration(names, j.literal("state/utils")); 82 | combinedImport.comments = comments; 83 | return combinedImport; 84 | }; 85 | 86 | combineReducerImport.replaceWith(addImport(importNames)); 87 | 88 | //update combineReducers call 89 | const renameIdentifier = newName => imported => { 90 | j(imported).replaceWith(() => j.identifier(newName)); 91 | }; 92 | const combineReducerIdentifier = root 93 | .find(j.CallExpression) 94 | .find(j.Identifier) 95 | .filter(identifier => identifier.value.name === "combineReducersWithPersistence"); 96 | 97 | combineReducerIdentifier.forEach(renameIdentifier("combineReducers")); 98 | 99 | // print 100 | return root.toSource(); 101 | }; 102 | -------------------------------------------------------------------------------- /transforms/modular-lodash-requires-no-more.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | let j; 6 | 7 | const getImports = source => { 8 | const imports = []; 9 | 10 | source.find(j.ImportDeclaration).forEach(dec => imports.push(dec)); 11 | 12 | return imports; 13 | }; 14 | 15 | const getRequires = source => { 16 | const requires = []; 17 | 18 | source 19 | .find(j.VariableDeclarator, { 20 | init: { 21 | type: "CallExpression", 22 | callee: { 23 | type: "Identifier", 24 | name: "require", 25 | }, 26 | }, 27 | }) 28 | .forEach(req => requires.push(req)); 29 | 30 | return requires; 31 | }; 32 | 33 | const getModularLodashDecs = requires => 34 | requires.filter( 35 | ({ value: { init } }) => 36 | init.type === "CallExpression" && 37 | init.callee.name === "require" && 38 | init.arguments.length && 39 | init.arguments[0].value && 40 | init.arguments[0].value.startsWith("lodash/") 41 | ); 42 | 43 | const makeNewImports = decs => { 44 | const imports = []; 45 | 46 | decs.forEach(dec => { 47 | const local = dec.value.init.arguments[0].value.replace("lodash/", ""); 48 | const name = dec.value.id.name; 49 | 50 | const newImport = j.importDeclaration( 51 | [j.importSpecifier(j.identifier(local), j.identifier(name))], 52 | j.literal("lodash") 53 | ); 54 | 55 | imports.push(newImport); 56 | }); 57 | 58 | return imports; 59 | }; 60 | 61 | const findInsertionPoint = (imports, requires) => { 62 | if (!imports.length) { 63 | const declaration = requires[0].parentPath.parentPath; 64 | const isAnnotated = 65 | declaration.value.comments && 66 | declaration.value.comments.length && 67 | declaration.value.comments[0].value === "*\n * External dependencies\n "; 68 | 69 | return [declaration, isAnnotated ? "annotated-requires" : "no-imports"]; 70 | } 71 | 72 | // see if we have an external import 73 | const externalAt = imports.findIndex( 74 | dec => dec.value.comments && dec.value.comments[0].value === "*\n * External dependencies\n " 75 | ); 76 | 77 | if (externalAt >= 0) { 78 | const internalAt = imports.findIndex( 79 | dec => dec.value.comments && dec.value.comments[0].value === "*\n * Internal dependencies\n " 80 | ); 81 | 82 | // insertion point is at last external import 83 | // or just before first internal import 84 | return internalAt >= 0 85 | ? [imports[internalAt], "before-internals"] 86 | : [imports[imports.length - 1], "no-internals"]; 87 | } 88 | 89 | // no externals, so put this at the top 90 | return [imports[0], "no-externals"]; 91 | }; 92 | 93 | export default function transformer(file, api) { 94 | j = api.jscodeshift; 95 | const source = j(file.source); 96 | 97 | const requires = getRequires(source); 98 | 99 | // if we don't have any requires 100 | // then don't do anything 101 | if (!requires.length) { 102 | return file.source; 103 | } 104 | 105 | const decs = getModularLodashDecs(requires); 106 | 107 | // if we don't have any modular lodash requires 108 | // then don't do anything 109 | if (!decs.length) { 110 | return file.source; 111 | } 112 | 113 | const imports = getImports(source); 114 | 115 | const newImports = makeNewImports(decs); 116 | 117 | const [insertionPoint, status] = findInsertionPoint(imports, requires); 118 | 119 | switch (status) { 120 | case "no-imports": 121 | case "no-externals": 122 | newImports[0].comments = [j.commentBlock("*\n * External dependencies\n ")]; 123 | j(insertionPoint).insertBefore(newImports); 124 | break; 125 | case "annotated-requires": 126 | newImports[0].comments = insertionPoint.value.comments; 127 | insertionPoint.value.comments = []; 128 | case "before-internals": 129 | j(insertionPoint).insertBefore(newImports); 130 | break; 131 | case "no-internals": 132 | j(insertionPoint).insertAfter(newImports); 133 | break; 134 | } 135 | 136 | // remove the old ones 137 | j(decs).remove(); 138 | 139 | return source.toSource(); 140 | } 141 | -------------------------------------------------------------------------------- /transforms/sort-imports.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This codeshift takes all of the imports for a file, and organizes them into two sections: 3 | * External dependencies and Internal Dependencies. 4 | * 5 | * It is smart enough to retain whether or not a docblock should keep a prettier/formatter pragma 6 | */ 7 | const fs = require("fs"); 8 | const path = require("path"); 9 | const _ = require("lodash"); 10 | const config = require(path.join(__dirname, "../config")); 11 | 12 | function findPkgJson(target) { 13 | let root = path.dirname(target); 14 | while (root !== "/") { 15 | const filepath = path.join(root, "package.json"); 16 | if (fs.existsSync(filepath)) { 17 | return JSON.parse(fs.readFileSync(filepath, "utf8")); 18 | } 19 | root = path.join(root, "../"); 20 | } 21 | throw new Error("could not find a pkg json"); 22 | } 23 | 24 | /** 25 | * Gather all of the external deps and throw them in a set 26 | */ 27 | const getPackageJsonDeps = (function() { 28 | let packageJsonDeps; 29 | 30 | return root => { 31 | if (packageJsonDeps) { 32 | return packageJsonDeps; 33 | } 34 | 35 | const json = findPkgJson(root); 36 | packageJsonDeps = [] 37 | .concat(nodeJsDeps) 38 | .concat(Object.keys(json.dependencies)) 39 | .concat(Object.keys(json.devDependencies)); 40 | 41 | return new Set(packageJsonDeps); 42 | }; 43 | })(); 44 | 45 | const nodeJsDeps = require("repl")._builtinLibs; 46 | const externalBlock = { 47 | type: "Block", 48 | value: "*\n * External dependencies\n ", 49 | }; 50 | const internalBlock = { 51 | type: "Block", 52 | value: "*\n * Internal dependencies\n ", 53 | }; 54 | 55 | /** 56 | * Returns true if the given text contains @format. 57 | * within its first docblock. False otherwise. 58 | * 59 | * @param {String} text text to scan for the format keyword within the first docblock 60 | */ 61 | const shouldFormat = text => { 62 | const firstDocBlockStartIndex = text.indexOf("/**"); 63 | 64 | if (-1 === firstDocBlockStartIndex) { 65 | return false; 66 | } 67 | 68 | const firstDocBlockEndIndex = text.indexOf("*/", firstDocBlockStartIndex + 1); 69 | 70 | if (-1 === firstDocBlockEndIndex) { 71 | return false; 72 | } 73 | 74 | const firstDocBlockText = text.substring(firstDocBlockStartIndex, firstDocBlockEndIndex + 1); 75 | return firstDocBlockText.indexOf("@format") >= 0; 76 | }; 77 | 78 | /** 79 | * Removes the extra newlines between two import statements 80 | */ 81 | const removeExtraNewlines = str => str.replace(/(import.*\n)\n+(import)/g, "$1$2"); 82 | 83 | /** 84 | * Adds a newline in between the last import of external deps + the internal deps docblock 85 | */ 86 | const addNewlineBeforeDocBlock = str => str.replace(/(import.*\n)(\/\*\*)/, "$1\n$2"); 87 | 88 | /** 89 | * 90 | * @param {Array} importNodes the import nodes to sort 91 | * @returns {Array} the sorted set of import nodes 92 | */ 93 | const sortImports = importNodes => _.sortBy(importNodes, node => node.source.value); 94 | 95 | module.exports = function(file, api) { 96 | const j = api.jscodeshift; 97 | const src = j(file.source); 98 | const includeFormatBlock = shouldFormat(src.toSource().toString()); 99 | const declarations = src.find(j.ImportDeclaration); 100 | 101 | // this is dependent on the projects package.json file which is why its initialized so late 102 | // we recursively search up from the file.path to figure out the location of the package.json file 103 | const externalDependenciesSet = getPackageJsonDeps(file.path); 104 | const isExternal = importNode => 105 | externalDependenciesSet.has(importNode.source.value.split("/")[0]); 106 | 107 | // if there are no deps at all, then return early. 108 | if (_.isEmpty(declarations.nodes())) { 109 | return file.source; 110 | } 111 | 112 | const withoutComments = declarations.nodes().map(node => { 113 | node.comments = ""; 114 | return node; 115 | }); 116 | 117 | const externalDeps = sortImports(withoutComments.filter(node => isExternal(node))); 118 | const internalDeps = sortImports(withoutComments.filter(node => !isExternal(node))); 119 | 120 | if (externalDeps[0]) { 121 | externalDeps[0].comments = [externalBlock]; 122 | } 123 | if (internalDeps[0]) { 124 | internalDeps[0].comments = [internalBlock]; 125 | } 126 | 127 | const newDeclarations = [] 128 | .concat(includeFormatBlock && "/** @format */") 129 | .concat(externalDeps) 130 | .concat(internalDeps); 131 | 132 | let isFirst = true; 133 | /* remove all imports and insert the new ones in the first imports place */ 134 | declarations.replaceWith(() => { 135 | if (isFirst) { 136 | isFirst = false; 137 | return newDeclarations; 138 | } 139 | return; 140 | }); 141 | 142 | return addNewlineBeforeDocBlock(removeExtraNewlines(src.toSource())); 143 | }; 144 | -------------------------------------------------------------------------------- /transforms/i18n-mixin-to-localize.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | export default function transformer(file, api) { 3 | const j = api.jscodeshift; 4 | const ReactUtils = require("react-codemod/transforms/utils/ReactUtils")(j); 5 | const root = j(file.source); 6 | let foundMixinUsage = false; 7 | 8 | const createClassesInstances = ReactUtils.findAllReactCreateClassCalls(root); 9 | 10 | // Find the declaration to wrap with the localize HOC. It can be the React.createClass 11 | // itself, or an 'export default' or 'module.exports =' declaration, if present. 12 | function findDeclarationsToWrap(createClassInstance) { 13 | // Is the created class being assigned to a variable? 14 | const parentNode = createClassInstance.parentPath.value; 15 | if (parentNode.type !== "VariableDeclarator" || parentNode.id.type !== "Identifier") { 16 | return j(createClassInstance); 17 | } 18 | 19 | // AST matcher for the class identifier 20 | const classIdentifier = { 21 | type: "Identifier", 22 | name: parentNode.id.name, 23 | }; 24 | 25 | // AST matcher for the connected class identifier 26 | const connectedClassIdentifier = { 27 | type: "CallExpression", 28 | callee: { 29 | type: "CallExpression", 30 | callee: { 31 | type: "Identifier", 32 | name: "connect", 33 | }, 34 | }, 35 | arguments: [classIdentifier], 36 | }; 37 | 38 | // AST matcher for the module.exports expression 39 | const moduleExportsExpression = { 40 | type: "MemberExpression", 41 | object: { 42 | type: "Identifier", 43 | name: "module", 44 | }, 45 | property: { 46 | type: "Identifier", 47 | name: "exports", 48 | }, 49 | }; 50 | 51 | // Is the variable later exported with 'export default'? 52 | const exportDefaultDeclarations = root.find(j.ExportDefaultDeclaration, { 53 | declaration: classIdentifier, 54 | }); 55 | if (exportDefaultDeclarations.size()) { 56 | return exportDefaultDeclarations.map(d => d.get("declaration")); 57 | } 58 | 59 | // Is the variable later exported with 'export default connect()'? 60 | const exportDefaultConnectDeclarations = root.find(j.ExportDefaultDeclaration, { 61 | declaration: connectedClassIdentifier, 62 | }); 63 | if (exportDefaultConnectDeclarations.size()) { 64 | return exportDefaultConnectDeclarations.map(d => d.get("declaration").get("arguments", 0)); 65 | } 66 | 67 | // Is the variable later exported with 'module.exports ='? 68 | const moduleExportsDeclarations = root.find(j.AssignmentExpression, { 69 | left: moduleExportsExpression, 70 | right: classIdentifier, 71 | }); 72 | if (moduleExportsDeclarations.size()) { 73 | return moduleExportsDeclarations.map(d => d.get("right")); 74 | } 75 | 76 | // Is the variable later exported with 'module.exports = connect()'? 77 | const moduleExportsConnectDeclarations = root.find(j.AssignmentExpression, { 78 | left: moduleExportsExpression, 79 | right: connectedClassIdentifier, 80 | }); 81 | if (moduleExportsConnectDeclarations.size()) { 82 | return moduleExportsConnectDeclarations.map(d => d.get("right").get("arguments", 0)); 83 | } 84 | 85 | return j(createClassInstance); 86 | } 87 | 88 | createClassesInstances.forEach(createClassInstance => { 89 | const propertiesToModify = ["translate", "moment", "numberFormat"]; 90 | 91 | propertiesToModify.forEach(property => { 92 | const propertyInstances = j(createClassInstance).find(j.MemberExpression, { 93 | object: { type: "ThisExpression" }, 94 | property: { 95 | type: "Identifier", 96 | name: property, 97 | }, 98 | }); 99 | 100 | propertyInstances.replaceWith(() => 101 | j.memberExpression( 102 | j.memberExpression(j.thisExpression(), j.identifier("props")), 103 | j.identifier(property) 104 | ) 105 | ); 106 | 107 | if (propertyInstances.size()) { 108 | foundMixinUsage = true; 109 | } 110 | }); 111 | 112 | if (foundMixinUsage) { 113 | const declarationsToWrap = findDeclarationsToWrap(createClassInstance); 114 | declarationsToWrap.replaceWith(decl => { 115 | return j.callExpression(j.identifier("localize"), [decl.value]); 116 | }); 117 | } 118 | }); 119 | 120 | if (foundMixinUsage) { 121 | const i18nCalypsoImports = root.find(j.ImportDeclaration, { 122 | source: { value: "i18n-calypso" }, 123 | }); 124 | if (i18nCalypsoImports.size()) { 125 | const i18nCalypsoImport = i18nCalypsoImports.get(); 126 | const localizeImport = j(i18nCalypsoImport).find(j.ImportSpecifier, { 127 | local: { 128 | type: "Identifier", 129 | name: "localize", 130 | }, 131 | }); 132 | if (!localizeImport.size()) { 133 | i18nCalypsoImport.value.specifiers.push(j.importSpecifier(j.identifier("localize"))); 134 | } 135 | } else { 136 | root 137 | .find(j.ImportDeclaration) 138 | .at(0) 139 | .insertAfter('import { localize } from "i18n-calypso";'); 140 | } 141 | } 142 | 143 | return root.toSource(); 144 | } 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository has moved! 2 | 3 | It is now part of the [Calypso](https://github.com/Automattic/wp-calypso/tree/master/packages/calypso-codemods) repository. 4 | 5 | The published npm package will continue to be available as before, no changes necessary! 6 | 7 | --- 8 | 9 | # Calypso Codemods 10 | 11 | [![Build Status](https://travis-ci.org/Automattic/calypso-codemods.svg?branch=master)](https://travis-ci.org/Automattic/calypso-codemods) 12 | 13 | 14 | ## What are codemods? 15 | 16 | Code modification scripts, also known as codemods, are transformation scripts that can simultaneously modify multiple files with precision and reliability. Codemods were popularized by [Facebook's engineering team](https://medium.com/@cpojer/effective-javascript-codemods-5a6686bb46fb) and depends greatly on Facebook's [jscodeshift](https://github.com/facebook/jscodeshift) library, which wraps over a library named [recast](https://github.com/benjamn/recast) (author of which is associated with the [Meteor](https://www.meteor.com/) project). 17 | 18 | ## Getting started 19 | 20 | Install calypso-codemods using `npm` or `yarn`: 21 | ``` 22 | npm install -g calypso-codemods 23 | ``` 24 | 25 | Now you can run codemods using the following cli: 26 | ```bash 27 | calypso-codemods transformation-name[,second-name,third-name] target [additional targets] 28 | ``` 29 | 30 | For example, if I wanted to run the `commonjs-exports` transformation on `client/devdocs/`, I can do the following: 31 | 32 | ```bash 33 | calypso-codemods commonjs-exports client/devdocs/ 34 | ``` 35 | 36 | Do you want to target files individually? We can do that, too! 37 | 38 | ```bash 39 | calypso-codemods commonjs-exports client/devdocs/a.js client/devdocs/b.js client/devdocs/c.js 40 | ``` 41 | 42 | How about chaining codemods on multiple directories? 43 | 44 | ```bash 45 | calypso-codemods commonjs-imports,commonjs-exports,named-exports-from-default client/blocks/ client/components/ 46 | ``` 47 | 48 | ## List of available transformations 49 | 50 | ### 5to6-codemod scripts ([docs](https://github.com/5to6/5to6-codemod#transforms)) 51 | 52 | - commonjs-exports 53 | - This codemod converts `module.exports` to `export` and `export default`. 54 | 55 | - commonjs-imports 56 | - This transformation converts occurrences of `require( '...' )` to `import ... from '...'` occurring at the top level scope. It will ignore CommonJS imports inside block statements, like conditionals or function definitions. 57 | 58 | - commonjs-imports-hoist 59 | - This transformation hoists all occurrences of `require( '...' )` inside if, loop, and function blocks. This can cause breakage! Use with caution. 60 | 61 | - named-exports-from-default 62 | - This transformation generates named exports given a `default export { ... }`. This can be useful in transitioning away from namespace imports (`import * as blah from 'blah'`) to named imports (`import named from 'blah'`). 63 | 64 | ### React scripts ([docs](https://github.com/reactjs/react-codemod)) 65 | 66 | - react-create-class 67 | - This transformation converts instances of React.createClass to use React.Component instead. 68 | 69 | - react-proptypes 70 | - This transformation converts instances of React.PropTypes to use prop-types instead. 71 | 72 | ### Local scripts 73 | - combine-reducer-with-persistence 74 | - This transformation converts combineReducers imports to use combineReducersWithPersistence. 75 | 76 | - combine-state-utils-imports 77 | - This transformation combines state/utils imports. 78 | 79 | - i18n-mixin-to-localize 80 | - This transformation converts the following: 81 | - `this.translate` to `this.props.translate` 82 | - `this.moment` to `this.props.moment` 83 | - `this.numberFormat` to `this.props.numberFormat` 84 | - If any of the above conversions is performed, this transformation will wrap the React.createClass instance with a `localize()` higher-order component. 85 | 86 | - merge-lodash-imports 87 | - This transformation merges multiple named lodash imports into one 88 | 89 | - modular-lodash-no-more 90 | - This transformation converts modular lodash imports to ES2015-style imports 91 | 92 | - modular-lodash-requires-no-more 93 | - This transformation converts modular lodash requires to ES2015-style imports 94 | 95 | - rename-combine-reducers 96 | - This transformation converts combineReducersWithPersistence imports to use combineReducers from 'state/utils' 97 | 98 | - single-tree-rendering 99 | - Instead of rendering two distinct React element trees to the `#primary` and `#secondary`
s, 100 | use a single `Layout` component tree that includes both, and render it to `#layout`. 101 | 102 | - sort-imports 103 | - This transformation adds import comment blocks and sorts them as necessary. 104 | - Note: It only needs to be run twice because of a bug where in certain cases an extra newline is added 105 | on the first run. The second run removes the extra newline. 106 | 107 | ## Contributing codemods 108 | ### Write the transform 109 | Write your transform using the standard jscodeshift api and place it in the transforms directory. 110 | You can look at the current directory for inspiration. 111 | 112 | ### Add some tests! 113 | calypso-codemods uses jest snapshots to maintain its tests. 114 | in order to easily add tests for a new transform, follow these steps: 115 | 116 | 1. add a directory to `tests` with the exact same name as the added transform. 117 | 2. add a file named `codemod.spec.js` with this as its contents contents: 118 | ```javascript 119 | test_folder(__dirname); 120 | ``` 121 | 3. add any input files to the folder that you wish to be tested 122 | 4. run `npm test` or `yarn test`. if the tests fail, its usually because a snapshot would be modified and behavior has changed. If you've verified that the updated snapshots look correct, then you can update the snapshots with: `yarn test -- -u`. 123 | 124 | 5. make sure to commit any modified snapshots and include it in your pull request 125 | -------------------------------------------------------------------------------- /transforms/remove-create-reducer.js: -------------------------------------------------------------------------------- 1 | function arrowFunctionBodyToCase(j, test, body) { 2 | if (body.type === "BlockStatement") { 3 | return j.switchCase(test, [body]); 4 | } 5 | return j.switchCase(test, [j.returnStatement(body)]); 6 | } 7 | 8 | function getCases(j, handlerMap) { 9 | let hasPersistence = false; 10 | 11 | const cases = handlerMap.properties.map(actionNode => { 12 | const test = actionNode.computed 13 | ? actionNode.key 14 | : j.literal(actionNode.key.name || String(actionNode.key.value)); 15 | const fn = actionNode.value; 16 | 17 | if ( 18 | test.type === "Identifier" && 19 | (test.name === "SERIALIZE" || test.name === "DESERIALIZE") 20 | ) { 21 | hasPersistence = true; 22 | } 23 | 24 | if ( 25 | test.type === "Literal" && 26 | (test.value === "SERIALIZE" || test.value === "DESERIALIZE") 27 | ) { 28 | hasPersistence = true; 29 | } 30 | 31 | // If it's an arrow function without parameters, just return the body. 32 | if (fn.type === "ArrowFunctionExpression" && fn.params.length === 0) { 33 | return arrowFunctionBodyToCase(j, test, fn.body); 34 | } 35 | 36 | // If it's an arrow function with the right parameter names, just return the body. 37 | if ( 38 | fn.type === "ArrowFunctionExpression" && 39 | fn.params[0].name === "state" && 40 | (fn.params.length === 1 || 41 | (fn.params.length === 2 && fn.params[1].name === "action")) 42 | ) { 43 | return arrowFunctionBodyToCase(j, test, fn.body); 44 | } 45 | 46 | // If it's an arrow function with a deconstructed action, do magic. 47 | if ( 48 | fn.type === "ArrowFunctionExpression" && 49 | fn.params[0].name === "state" && 50 | (fn.params.length === 2 && fn.params[1].type === "ObjectPattern") 51 | ) { 52 | const declaration = j.variableDeclaration("const", [ 53 | j.variableDeclarator(fn.params[1], j.identifier("action")) 54 | ]); 55 | const prevBody = 56 | fn.body.type === "BlockStatement" 57 | ? fn.body.body 58 | : [j.returnStatement(fn.body)]; 59 | const body = j.blockStatement([declaration, ...prevBody]); 60 | return arrowFunctionBodyToCase(j, test, body); 61 | } 62 | 63 | return j.switchCase(test, [ 64 | j.returnStatement( 65 | j.callExpression(actionNode.value, [ 66 | j.identifier("state"), 67 | j.identifier("action") 68 | ]) 69 | ) 70 | ]); 71 | }); 72 | 73 | return { cases, hasPersistence }; 74 | } 75 | 76 | function handlePersistence(j, createReducerPath, newNode) { 77 | const parent = createReducerPath.parentPath; 78 | const grandParentValue = 79 | parent && 80 | parent.parentPath.value && 81 | parent.parentPath.value.length === 1 && 82 | parent.parentPath.value[0]; 83 | const greatGrandParent = 84 | grandParentValue && 85 | parent && 86 | parent.parentPath && 87 | parent.parentPath.parentPath; 88 | 89 | if ( 90 | parent && 91 | grandParentValue && 92 | greatGrandParent && 93 | parent.value.type === "VariableDeclarator" && 94 | grandParentValue.type === "VariableDeclarator" && 95 | greatGrandParent.value.type === "VariableDeclaration" 96 | ) { 97 | const varName = parent.value.id.name; 98 | const persistenceNode = j.expressionStatement( 99 | j.assignmentExpression( 100 | "=", 101 | j.memberExpression( 102 | j.identifier(varName), 103 | j.identifier("hasCustomPersistence"), 104 | false 105 | ), 106 | j.literal(true) 107 | ) 108 | ); 109 | 110 | if (greatGrandParent.parentPath.value.type === "ExportNamedDeclaration") { 111 | // Handle `export const reducer = ...` case. 112 | greatGrandParent.parentPath.insertAfter(persistenceNode); 113 | } else { 114 | // Handle `const reducer = ...` case. 115 | greatGrandParent.insertAfter(persistenceNode); 116 | } 117 | } else if (parent && parent.value.type === "AssignmentExpression") { 118 | const persistenceNode = j.expressionStatement( 119 | j.assignmentExpression( 120 | "=", 121 | j.memberExpression( 122 | parent.value.left, 123 | j.identifier("hasCustomPersistence"), 124 | false 125 | ), 126 | j.literal(true) 127 | ) 128 | ); 129 | parent.parentPath.insertAfter(persistenceNode); 130 | } else { 131 | newNode.comments = newNode.comments || []; 132 | newNode.comments.push( 133 | j.commentLine(" TODO: HANDLE PERSISTENCE", true, false) 134 | ); 135 | } 136 | 137 | return newNode; 138 | } 139 | 140 | export default function transformer(file, api) { 141 | const j = api.jscodeshift; 142 | 143 | const root = j(file.source); 144 | 145 | let usedWithoutPersistence = false; 146 | 147 | // Handle createReducer 148 | root 149 | .find( 150 | j.CallExpression, 151 | node => 152 | node.callee.type === "Identifier" && 153 | node.callee.name === "createReducer" 154 | ) 155 | .forEach(createReducerPath => { 156 | if (createReducerPath.value.arguments.length !== 2) { 157 | throw new Error("Unable to translate createReducer"); 158 | } 159 | 160 | const [defaultState, handlerMap] = createReducerPath.value.arguments; 161 | 162 | const { cases, hasPersistence } = getCases(j, handlerMap); 163 | 164 | let newNode = j.arrowFunctionExpression( 165 | [ 166 | j.assignmentPattern(j.identifier("state"), defaultState), 167 | j.identifier("action") 168 | ], 169 | 170 | j.blockStatement([ 171 | j.switchStatement( 172 | j.memberExpression(j.identifier("action"), j.identifier("type")), 173 | cases 174 | ), 175 | j.returnStatement(j.identifier("state")) 176 | ]) 177 | ); 178 | 179 | if (hasPersistence) { 180 | newNode = handlePersistence(j, createReducerPath, newNode); 181 | } else { 182 | usedWithoutPersistence = true; 183 | newNode = j.callExpression(j.identifier("withoutPersistence"), [ 184 | newNode 185 | ]); 186 | } 187 | 188 | createReducerPath.replace(newNode); 189 | }); 190 | 191 | // Handle createReducerWithValidation 192 | root 193 | .find( 194 | j.CallExpression, 195 | node => 196 | node.callee.type === "Identifier" && 197 | node.callee.name === "createReducerWithValidation" 198 | ) 199 | .forEach(createReducerPath => { 200 | if (createReducerPath.value.arguments.length !== 3) { 201 | throw new Error("Unable to translate createReducerWithValidation"); 202 | } 203 | 204 | const [ 205 | defaultState, 206 | handlerMap, 207 | schema 208 | ] = createReducerPath.value.arguments; 209 | 210 | const { cases } = getCases(j, handlerMap); 211 | 212 | const newNode = j.callExpression(j.identifier("withSchemaValidation"), [ 213 | schema, 214 | j.arrowFunctionExpression( 215 | [ 216 | j.assignmentPattern(j.identifier("state"), defaultState), 217 | j.identifier("action") 218 | ], 219 | 220 | j.blockStatement([ 221 | j.switchStatement( 222 | j.memberExpression(j.identifier("action"), j.identifier("type")), 223 | cases 224 | ), 225 | j.returnStatement(j.identifier("state")) 226 | ]) 227 | ) 228 | ]); 229 | 230 | createReducerPath.replace(newNode); 231 | }); 232 | 233 | // Handle imports. 234 | root 235 | .find( 236 | j.ImportDeclaration, 237 | node => 238 | node.specifiers && 239 | node.specifiers.some( 240 | s => 241 | s && 242 | s.imported && 243 | (s.imported.name === "createReducer" || 244 | s.imported.name === "createReducerWithValidation") 245 | ) 246 | ) 247 | .forEach(nodePath => { 248 | const filtered = nodePath.value.specifiers.filter( 249 | s => 250 | s.imported.name !== "createReducer" && 251 | s.imported.name !== "createReducerWithValidation" 252 | ); 253 | 254 | if ( 255 | nodePath.value.specifiers.find( 256 | s => s.imported.name === "createReducerWithValidation" 257 | ) 258 | ) { 259 | if (!filtered.find(s => s.imported.name === "withSchemaValidation")) { 260 | filtered.push( 261 | j.importSpecifier( 262 | j.identifier("withSchemaValidation"), 263 | j.identifier("withSchemaValidation") 264 | ) 265 | ); 266 | } 267 | } 268 | 269 | if (usedWithoutPersistence) { 270 | if (!filtered.find(s => s.imported.name === "withoutPersistence")) { 271 | filtered.push( 272 | j.importSpecifier( 273 | j.identifier("withoutPersistence"), 274 | j.identifier("withoutPersistence") 275 | ) 276 | ); 277 | } 278 | } 279 | 280 | if (filtered.length === 0) { 281 | const { comments } = nodePath.node; 282 | const { parentPath } = nodePath; 283 | const nextNode = parentPath.value[nodePath.name + 1]; 284 | j(nodePath).remove(); 285 | nextNode.comments = comments; 286 | } else { 287 | nodePath.value.specifiers = filtered; 288 | } 289 | }); 290 | 291 | return root.toSource(); 292 | } 293 | -------------------------------------------------------------------------------- /transforms/single-tree-rendering.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | /** 4 | * Single Tree Rendering Codemod 5 | * 6 | * Transforms `ReactDom.render()` to `context.primary/secondary`. 7 | * 8 | * Transforms `renderWithReduxStore()` to `context.primary/secondary`. 9 | * 10 | * Adds `context` to params in middlewares when needed 11 | * 12 | * Adds `next` to params and `next()` to body in middlewares when using 13 | * `ReactDom.render()` or `renderWithReduxStore()`. 14 | * 15 | * Adds `makeLayout` and `clientRender` to `page()` route definitions and 16 | * accompanying import statement. 17 | * 18 | * Removes: 19 | * `ReactDom.unmountComponentAtNode( document.getElementById( 'secondary' ) );` 20 | * 21 | * Removes: 22 | * Un-used ReactDom imports. 23 | * 24 | * Replaces `navigation` middleware with `makeNavigation` when imported 25 | * from `my-sites/controller`. 26 | */ 27 | 28 | /** 29 | * External dependencies 30 | */ 31 | const _ = require( 'lodash' ); 32 | const fs = require( 'fs' ); 33 | const repl = require( 'repl' ); 34 | 35 | /** 36 | * Internal dependencies 37 | */ 38 | const config = require( './config' ); 39 | 40 | export default function transformer( file, api ) { 41 | const j = api.jscodeshift; 42 | const root = j( file.source ); 43 | 44 | /** 45 | * Gather all of the external deps and throw them in a set 46 | */ 47 | const nodeJsDeps = repl._builtinLibs; 48 | const packageJson = JSON.parse( fs.readFileSync( './package.json', 'utf8' ) ); 49 | const packageJsonDeps = [] 50 | .concat( nodeJsDeps ) 51 | .concat( Object.keys( packageJson.dependencies ) ) 52 | .concat( Object.keys( packageJson.devDependencies ) ); 53 | 54 | const externalDependenciesSet = new Set( packageJsonDeps ); 55 | 56 | /** 57 | * @param {object} importNode Node object 58 | * @returns {boolean} 59 | */ 60 | const isExternal = importNode => 61 | externalDependenciesSet.has( importNode.source.value.split( '/' )[ 0 ] ); 62 | 63 | /** 64 | * Removes the extra newlines between two import statements 65 | * caused by `insertAfter()`: 66 | * @link https://github.com/benjamn/recast/issues/371 67 | * 68 | * @param {string} str 69 | * @returns {string} 70 | */ 71 | function removeExtraNewlines( str ) { 72 | return str.replace( /(import.*\n)\n+(import)/g, '$1$2' ); 73 | } 74 | 75 | /** 76 | * Check if `parameters` has `param` either as a string or as a name of 77 | * an object, which could be e.g. an `Identifier`. 78 | * 79 | * @param {array} parameters Parameters to look from. Could be an array of strings or Identifier objects. 80 | * @param {string} parameter Parameter name 81 | * @returns {boolean} 82 | */ 83 | function hasParam( params = [], paramValue ) { 84 | return _.some( params, param => { 85 | return ( param.name || param ) === paramValue; 86 | } ); 87 | } 88 | 89 | /** 90 | * Removes imports maintaining any comments above them 91 | * 92 | * @param {object} collection Collection containing at least one node. Comments are preserved only from first node. 93 | */ 94 | function removeImport( collection ) { 95 | const node = collection.nodes()[ 0 ]; 96 | 97 | // Find out if import had comments above it 98 | const comments = _.get( node, 'comments', [] ); 99 | 100 | // Remove import (and any comments with it) 101 | collection.remove(); 102 | 103 | // Put back that removed comment (if any) 104 | if ( comments.length ) { 105 | const isRemovedExternal = isExternal( node ); 106 | 107 | // Find remaining external or internal dependencies and place comments above first one 108 | root 109 | .find( j.ImportDeclaration ) 110 | .filter( p => { 111 | // Look for only imports that are same type as the removed import was 112 | return isExternal( p.value ) === isRemovedExternal; 113 | } ) 114 | .at( 0 ) 115 | .replaceWith( p => { 116 | p.value.comments = p.value.comments ? p.value.comments.concat( comments ) : comments; 117 | return p.value; 118 | } ); 119 | } 120 | } 121 | 122 | /** 123 | * Catch simple redirect middlewares by looking for `page.redirect()` 124 | * 125 | * @example 126 | * // Middleware could look like this: 127 | * () => page.redirect('/foo') 128 | * 129 | * // ...or this: 130 | * context => { page.redirect(`/foo/${context.bar}`) } 131 | * 132 | * // ...or even: 133 | * () => { 134 | * if (true) { 135 | * page.redirect('/foo'); 136 | * } else { 137 | * page.redirect('/bar'); 138 | * } 139 | * } 140 | * 141 | * @param {object} node 142 | * @returns {boolean} True if any `page.redirect()` exist inside the function node, otherwise False 143 | */ 144 | function isRedirectMiddleware( node ) { 145 | return ( 146 | j( node ) 147 | .find( j.MemberExpression, { 148 | object: { 149 | type: 'Identifier', 150 | name: 'page', 151 | }, 152 | property: { 153 | type: 'Identifier', 154 | name: 'redirect', 155 | }, 156 | } ) 157 | .size() > 0 158 | ); 159 | } 160 | 161 | /** 162 | * Ensure `context` is among params 163 | * 164 | * @param {object} path Path object that wraps a single node 165 | * @returns {object} Single node object 166 | */ 167 | function ensureContextMiddleware( path ) { 168 | // `context` param is already in 169 | if ( hasParam( path.value.params, 'context' ) ) { 170 | return path.value; 171 | } 172 | let ret = path.value; 173 | ret.params = [ j.identifier( 'context' ), ...ret.params ]; 174 | 175 | return ret; 176 | } 177 | 178 | /** 179 | * Ensure `next` is among params and `next()` is in the block's body 180 | * 181 | * @param {object} path Path object that wraps a single node 182 | * @returns {object} Single node object 183 | */ 184 | function ensureNextMiddleware( path ) { 185 | // `next` param is already in 186 | if ( hasParam( path.value.params, 'next' ) ) { 187 | return path.value; 188 | } 189 | if ( path.value.params.length > 1 ) { 190 | // More than just a context arg, possibly not a middleware 191 | return path.value; 192 | } 193 | let ret = path.value; 194 | ret.params = [ ...ret.params, j.identifier( 'next' ) ]; 195 | ret.body = j.blockStatement( [ 196 | ...path.value.body.body, 197 | j.expressionStatement( j.callExpression( j.identifier( 'next' ), [] ) ), 198 | ] ); 199 | 200 | return ret; 201 | } 202 | 203 | function getTarget( arg ) { 204 | if ( arg.type === 'Literal' ) { 205 | return arg.value; 206 | } 207 | if ( arg.type === 'CallExpression' ) { 208 | // More checks? 209 | return arg.arguments[ 0 ].value; 210 | } 211 | } 212 | 213 | /** 214 | * Transform `renderWithReduxStore()` CallExpressions. 215 | * 216 | * @example 217 | * Input 218 | * ``` 219 | * renderWithReduxStore( 220 | * , 221 | * 'primary', 222 | * context.store 223 | * ); 224 | * ``` 225 | * 226 | * Output: 227 | * ``` 228 | * context.primary = ; 229 | * ``` 230 | * 231 | * @param {object} path Path object that wraps a single node 232 | * @returns {object} Single node object 233 | */ 234 | function transformRenderWithReduxStore( path ) { 235 | const expressionCallee = { 236 | name: 'renderWithReduxStore', 237 | }; 238 | 239 | return transformToContextLayout( path, expressionCallee ); 240 | } 241 | 242 | /** 243 | * Transform `ReactDom.render()` CallExpressions. 244 | * 245 | * @example 246 | * Input 247 | * ``` 248 | * ReactDom.render( 249 | * , 250 | * document.getElementById( 'primary' ) 251 | * ); 252 | * ``` 253 | * 254 | * Output: 255 | * ``` 256 | * context.primary = ; 257 | * ``` 258 | * 259 | * @param {object} path Path object that wraps a single node 260 | * @returns {object} Single node object 261 | */ 262 | function transformReactDomRender( path ) { 263 | const expressionCallee = { 264 | type: 'MemberExpression', 265 | object: { 266 | name: 'ReactDom', 267 | }, 268 | property: { 269 | name: 'render', 270 | }, 271 | }; 272 | 273 | return transformToContextLayout( path, expressionCallee ); 274 | } 275 | 276 | /** 277 | * Transform CallExpressions. 278 | * What kind of CallExpressions this replaces depends on `expressionCallee` 279 | * parameter. 280 | * 281 | * @example 282 | * Input 283 | * ``` 284 | * DefinedByExpressionCallee( 285 | * , 286 | * document.getElementById( 'primary' ) 287 | * ); 288 | * ``` 289 | * 290 | * Output: 291 | * ``` 292 | * context.primary = ; 293 | * ``` 294 | * 295 | * @param {object} path Path object that wraps a single node 296 | * @param {object} expressionCallee `callee` parameter for finding `CallExpression` nodes. 297 | * @returns {object} Single node object 298 | */ 299 | function transformToContextLayout( path, expressionCallee ) { 300 | if ( path.value.params.length !== 2 ) { 301 | // More than just context and next args, possibly not a middleware 302 | return path.value; 303 | } 304 | return j( path ) 305 | .find( j.CallExpression, { 306 | callee: expressionCallee, 307 | } ) 308 | .replaceWith( p => { 309 | const contextArg = path.value.params[ 0 ]; 310 | const target = getTarget( p.value.arguments[ 1 ] ); 311 | return j.assignmentExpression( 312 | '=', 313 | j.memberExpression( contextArg, j.identifier( target ) ), 314 | p.value.arguments[ 0 ] 315 | ); 316 | } ); 317 | 318 | return j( node ); 319 | } 320 | 321 | // Transform `ReactDom.render()` to `context.primary/secondary` 322 | root 323 | .find( j.CallExpression, { 324 | callee: { 325 | type: 'MemberExpression', 326 | object: { 327 | name: 'ReactDom', 328 | }, 329 | property: { 330 | name: 'render', 331 | }, 332 | }, 333 | } ) 334 | .closest( j.Function ) 335 | .replaceWith( ensureContextMiddleware ) 336 | // Receives already transformed object from `replaceWith()` above 337 | .replaceWith( ensureNextMiddleware ) 338 | .forEach( transformReactDomRender ); 339 | 340 | // Transform `renderWithReduxStore()` to `context.primary/secondary` 341 | root 342 | .find( j.CallExpression, { 343 | callee: { 344 | name: 'renderWithReduxStore', 345 | }, 346 | } ) 347 | .closestScope() 348 | .replaceWith( ensureNextMiddleware ) 349 | .forEach( transformRenderWithReduxStore ); 350 | 351 | // Remove `renderWithReduxStore` from `import { a, renderWithReduxStore, b } from 'lib/react-helpers'` 352 | root 353 | .find( j.ImportSpecifier, { 354 | local: { 355 | name: 'renderWithReduxStore', 356 | }, 357 | } ) 358 | .remove(); 359 | 360 | // Find empty `import 'lib/react-helpers'` 361 | const orphanImportHelpers = root 362 | .find( j.ImportDeclaration, { 363 | source: { 364 | value: 'lib/react-helpers', 365 | }, 366 | } ) 367 | .filter( p => ! p.value.specifiers.length ); 368 | 369 | if ( orphanImportHelpers.size() ) { 370 | removeImport( orphanImportHelpers ); 371 | } 372 | 373 | /** 374 | * Removes: 375 | * ``` 376 | * ReactDom.unmountComponentAtNode( document.getElementById( 'secondary' ) ); 377 | * ``` 378 | */ 379 | root 380 | .find( j.CallExpression, { 381 | callee: { 382 | type: 'MemberExpression', 383 | object: { 384 | name: 'ReactDom', 385 | }, 386 | property: { 387 | name: 'unmountComponentAtNode', 388 | }, 389 | }, 390 | } ) 391 | // Ensures we remove only nodes containing `document.getElementById( 'secondary' )` 392 | .filter( p => _.get( p, 'value.arguments[0].arguments[0].value' ) === 'secondary' ) 393 | .remove(); 394 | 395 | // Find if `ReactDom` is used 396 | const reactDomDefs = root.find( j.MemberExpression, { 397 | object: { 398 | name: 'ReactDom', 399 | }, 400 | } ); 401 | 402 | // Remove stranded `react-dom` imports 403 | if ( ! reactDomDefs.size() ) { 404 | const importReactDom = root.find( j.ImportDeclaration, { 405 | specifiers: [ 406 | { 407 | local: { 408 | name: 'ReactDom', 409 | }, 410 | }, 411 | ], 412 | source: { 413 | value: 'react-dom', 414 | }, 415 | } ); 416 | 417 | if ( importReactDom.size() ) { 418 | removeImport( importReactDom ); 419 | } 420 | } 421 | 422 | // Add makeLayout and clientRender middlewares to route definitions 423 | const routeDefs = root 424 | .find( j.CallExpression, { 425 | callee: { 426 | name: 'page', 427 | }, 428 | } ) 429 | .filter( p => { 430 | const lastArgument = _.last( p.value.arguments ); 431 | 432 | return ( 433 | p.value.arguments.length > 1 && 434 | p.value.arguments[ 0 ].value !== '*' && 435 | [ 'Identifier', 'MemberExpression', 'CallExpression' ].indexOf( lastArgument.type ) > -1 && 436 | ! isRedirectMiddleware( lastArgument ) 437 | ); 438 | } ) 439 | .forEach( p => { 440 | p.value.arguments.push( j.identifier( 'makeLayout' ) ); 441 | p.value.arguments.push( j.identifier( 'clientRender' ) ); 442 | } ); 443 | 444 | if ( routeDefs.size() ) { 445 | root 446 | .find( j.ImportDeclaration ) 447 | .at( -1 ) 448 | .insertAfter( "import { makeLayout, render as clientRender } from 'controller';" ); 449 | } 450 | 451 | const source = root.toSource( config.recastOptions ); 452 | 453 | return routeDefs.size() ? removeExtraNewlines( source ) : source; 454 | } 455 | --------------------------------------------------------------------------------