diff --git a/.eslintignore b/.eslintignore
index bd43ff781782c..ecae1c09c9d60 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -6,6 +6,7 @@
node_modules/
!.eslintrc.js
/server/devdocs/search-index.js
+/packages/calypso-codemods/tests
# Built packages
/packages/*/dist/
diff --git a/package-lock.json b/package-lock.json
index 4f3a44f0fec6d..03ab99af5852c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,50 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+ "5to6-codemod": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/5to6-codemod/-/5to6-codemod-1.8.0.tgz",
+ "integrity": "sha512-RUHjjwl9+p1d46USvmoKsmMaHODFUAESE1de/q0qQM+hwzgk/HssTwb1Nc5dbUpKEkJ7duLg6ggMIwScd+TRig==",
+ "dev": true,
+ "requires": {
+ "jscodeshift": "^0.6.3",
+ "lodash": "^4.17.4",
+ "recast": "^0.12.1"
+ },
+ "dependencies": {
+ "ast-types": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.10.1.tgz",
+ "integrity": "sha512-UY7+9DPzlJ9VM8eY0b2TUZcZvF+1pO0hzMtAyjBYKhOmnvRlqYNYnWdtsMj0V16CGaMlpL0G1jnLbLo4AyotuQ==",
+ "dev": true
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "recast": {
+ "version": "0.12.9",
+ "resolved": "https://registry.npmjs.org/recast/-/recast-0.12.9.tgz",
+ "integrity": "sha512-y7ANxCWmMW8xLOaiopiRDlyjQ9ajKRENBH+2wjntIbk3A6ZR1+BLQttkmSHMY7Arl+AAZFwJ10grg2T6f1WI8A==",
+ "dev": true,
+ "requires": {
+ "ast-types": "0.10.1",
+ "core-js": "^2.4.1",
+ "esprima": "~4.0.0",
+ "private": "~0.1.5",
+ "source-map": "~0.6.1"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
"@automattic/babel-plugin-i18n-calypso": {
"version": "file:packages/babel-plugin-i18n-calypso",
"dev": true,
@@ -847,6 +891,15 @@
"@babel/helper-plugin-utils": "^7.0.0"
}
},
+ "@babel/plugin-syntax-flow": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.7.0.tgz",
+ "integrity": "sha512-vQMV07p+L+jZeUnvX3pEJ9EiXGCjB5CTTvsirFD9rpEuATnoAvLBLoYbw1v5tyn3d2XxSuvEKi8cV3KqYUa0vQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.0.0"
+ }
+ },
"@babel/plugin-syntax-json-strings": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz",
@@ -1021,6 +1074,16 @@
"@babel/helper-plugin-utils": "^7.0.0"
}
},
+ "@babel/plugin-transform-flow-strip-types": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.6.3.tgz",
+ "integrity": "sha512-l0ETkyEofkqFJ9LS6HChNIKtVJw2ylKbhYMlJ5C6df+ldxxaLIyXY4yOdDQQspfFpV8/vDiaWoJlvflstlYNxg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/plugin-syntax-flow": "^7.2.0"
+ }
+ },
"@babel/plugin-transform-for-of": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz",
@@ -1356,6 +1419,16 @@
"semver": "^5.5.0"
}
},
+ "@babel/preset-flow": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.0.0.tgz",
+ "integrity": "sha512-bJOHrYOPqJZCkPVbG1Lot2r5OSsB+iUOaxiHdlOeB1yPWS6evswVHwvkDLZ54WTaTRIk89ds0iHmGZSnxlPejQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/plugin-transform-flow-strip-types": "^7.0.0"
+ }
+ },
"@babel/preset-react": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.7.0.tgz",
@@ -1379,6 +1452,37 @@
"@babel/plugin-transform-typescript": "^7.7.2"
}
},
+ "@babel/register": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.7.0.tgz",
+ "integrity": "sha512-HV3GJzTvSoyOMWGYn2TAh6uL6g+gqKTgEZ99Q3+X9UURT1VPT/WcU46R61XftIc5rXytcOHZ4Z0doDlsjPomIg==",
+ "dev": true,
+ "requires": {
+ "find-cache-dir": "^2.0.0",
+ "lodash": "^4.17.13",
+ "make-dir": "^2.1.0",
+ "pirates": "^4.0.0",
+ "source-map-support": "^0.5.16"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
+ "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ }
+ }
+ },
"@babel/runtime": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz",
@@ -5414,6 +5518,12 @@
"ast-types-flow": "0.0.7"
}
},
+ "babel-core": {
+ "version": "7.0.0-bridge.0",
+ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz",
+ "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==",
+ "dev": true
+ },
"babel-eslint": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.3.tgz",
@@ -6225,6 +6335,16 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
"integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA="
},
+ "calypso-codemods": {
+ "version": "file:packages/calypso-codemods",
+ "dev": true,
+ "requires": {
+ "5to6-codemod": "^1.8.0",
+ "jscodeshift": "^0.6.4",
+ "lodash": "^4.17.10",
+ "react-codemod": "^5.0.5"
+ }
+ },
"camel-case": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
@@ -8168,6 +8288,27 @@
"which": "^2.0.1"
}
},
+ "cross-spawn-async": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz",
+ "integrity": "sha1-hF/wwINKPe2dFg2sptOQkGuyiMw=",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^4.0.0",
+ "which": "^1.2.8"
+ },
+ "dependencies": {
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ }
+ }
+ },
"crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@@ -11355,6 +11496,12 @@
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
"dev": true
},
+ "flow-parser": {
+ "version": "0.112.0",
+ "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.112.0.tgz",
+ "integrity": "sha512-sxjnwhR76B/fUN6n/XerYzn8R1HvtVo3SM8Il3WiZ4nkAlb2BBzKe1TSVKGSyZgD6FW9Bsxom/57ktkqrqmXGA==",
+ "dev": true
+ },
"flush-write-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
@@ -12710,6 +12857,12 @@
"debug": "^3.1.0"
}
},
+ "human-signals": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
+ "dev": true
+ },
"humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
@@ -13443,6 +13596,66 @@
"integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
"dev": true
},
+ "is-git-clean": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-git-clean/-/is-git-clean-1.1.0.tgz",
+ "integrity": "sha1-E6vW3acRuwiq/UJgTaSHhF3c+I0=",
+ "dev": true,
+ "requires": {
+ "execa": "^0.4.0",
+ "is-obj": "^1.0.1",
+ "multimatch": "^2.1.0"
+ },
+ "dependencies": {
+ "array-differ": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz",
+ "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=",
+ "dev": true
+ },
+ "execa": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-0.4.0.tgz",
+ "integrity": "sha1-TrZGejaglfq7KXD/nV4/t7zm68M=",
+ "dev": true,
+ "requires": {
+ "cross-spawn-async": "^2.1.1",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "path-key": "^1.0.0",
+ "strip-eof": "^1.0.0"
+ }
+ },
+ "multimatch": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz",
+ "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=",
+ "dev": true,
+ "requires": {
+ "array-differ": "^1.0.0",
+ "array-union": "^1.0.1",
+ "arrify": "^1.0.0",
+ "minimatch": "^3.0.0"
+ }
+ },
+ "npm-run-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-1.0.0.tgz",
+ "integrity": "sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8=",
+ "dev": true,
+ "requires": {
+ "path-key": "^1.0.0"
+ }
+ },
+ "path-key": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-1.0.0.tgz",
+ "integrity": "sha1-XVPVeAGWRsDWiADbThRua9wqx68=",
+ "dev": true
+ }
+ }
+ },
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
@@ -15437,6 +15650,70 @@
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
+ "jscodeshift": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.6.4.tgz",
+ "integrity": "sha512-+NF/tlNbc2WEhXUuc4WEJLsJumF84tnaMUZW2hyJw3jThKKRvsPX4sPJVgO1lPE28z0gNL+gwniLG9d8mYvQCQ==",
+ "dev": true,
+ "requires": {
+ "@babel/core": "^7.1.6",
+ "@babel/parser": "^7.1.6",
+ "@babel/plugin-proposal-class-properties": "^7.1.0",
+ "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
+ "@babel/preset-env": "^7.1.6",
+ "@babel/preset-flow": "^7.0.0",
+ "@babel/preset-typescript": "^7.1.0",
+ "@babel/register": "^7.0.0",
+ "babel-core": "^7.0.0-bridge.0",
+ "colors": "^1.1.2",
+ "flow-parser": "0.*",
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "neo-async": "^2.5.0",
+ "node-dir": "^0.1.17",
+ "recast": "^0.16.1",
+ "temp": "^0.8.1",
+ "write-file-atomic": "^2.3.0"
+ },
+ "dependencies": {
+ "ast-types": {
+ "version": "0.11.7",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.11.7.tgz",
+ "integrity": "sha512-2mP3TwtkY/aTv5X3ZsMpNAbOnyoC/aMJwJSoaELPkHId0nSQgFcnU4dRW3isxiz7+zBexk0ym3WNVjMiQBnJSw==",
+ "dev": true
+ },
+ "colors": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
+ "dev": true
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "recast": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/recast/-/recast-0.16.2.tgz",
+ "integrity": "sha512-O/7qXi51DPjRVdbrpNzoBQH5dnAPQNbfoOFyRiUwreTMJfIHYOEBzwuH+c0+/BTSJ3CQyKs6ILSWXhESH6Op3A==",
+ "dev": true,
+ "requires": {
+ "ast-types": "0.11.7",
+ "esprima": "~4.0.0",
+ "private": "~0.1.5",
+ "source-map": "~0.6.1"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
"jsdom": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
@@ -16999,6 +17276,15 @@
"resolved": "https://registry.npmjs.org/node-contains/-/node-contains-1.0.0.tgz",
"integrity": "sha1-0sJzUkU22jtWGvBTnjM0T3yQLLg="
},
+ "node-dir": {
+ "version": "0.1.17",
+ "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz",
+ "integrity": "sha1-X1Zl2TNRM1yqvvjxxVRRbPXx5OU=",
+ "dev": true,
+ "requires": {
+ "minimatch": "^3.0.2"
+ }
+ },
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
@@ -22384,6 +22670,374 @@
}
}
},
+ "react-codemod": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/react-codemod/-/react-codemod-5.1.1.tgz",
+ "integrity": "sha512-UvN3X8cki4qFzgnEmxJHXHKq3GBxxE6wwTbjNuQyC7LWkxyaDovoHBuxGGpL41UlD7iz92fPQusJCF2BI55jCw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.2",
+ "execa": "^3.2.0",
+ "globby": "^10.0.1",
+ "inquirer": "^7.0.0",
+ "is-git-clean": "^1.1.0",
+ "jscodeshift": "^0.6.4",
+ "meow": "^5.0.0"
+ },
+ "dependencies": {
+ "@nodelib/fs.stat": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
+ "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
+ "dev": true
+ },
+ "ansi-escapes": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz",
+ "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.5.2"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "camelcase": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "requires": {
+ "path-type": "^4.0.0"
+ }
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "execa": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-3.3.0.tgz",
+ "integrity": "sha512-j5Vit5WZR/cbHlqU97+qcnw9WHRCIL4V1SVe75VcHcD1JRBdt8fv0zw89b7CQHQdUHTt2VjuhcF5ibAgVOxqpg==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.0",
+ "get-stream": "^5.0.0",
+ "human-signals": "^1.1.1",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.0",
+ "onetime": "^5.1.0",
+ "p-finally": "^2.0.0",
+ "signal-exit": "^3.0.2",
+ "strip-final-newline": "^2.0.0"
+ }
+ },
+ "fast-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.0.tgz",
+ "integrity": "sha512-TrUz3THiq2Vy3bjfQUB2wNyPdGBeGmdjbzzBLhfHN4YFurYptCKwGq/TfiRavbGywFRzY6U2CdmQ1zmsY5yYaw==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.0",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.2"
+ }
+ },
+ "figures": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz",
+ "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "get-stream": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
+ "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
+ "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "globby": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz",
+ "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==",
+ "dev": true,
+ "requires": {
+ "@types/glob": "^7.1.1",
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.0.3",
+ "glob": "^7.1.3",
+ "ignore": "^5.1.1",
+ "merge2": "^1.2.3",
+ "slash": "^3.0.0"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "ignore": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
+ "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
+ "dev": true
+ },
+ "inquirer": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz",
+ "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^2.4.2",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.15",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.2.0",
+ "rxjs": "^6.4.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^5.1.0",
+ "through": "^2.3.6"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+ "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
+ "dev": true
+ },
+ "meow": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz",
+ "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==",
+ "dev": true,
+ "requires": {
+ "camelcase-keys": "^4.0.0",
+ "decamelize-keys": "^1.0.0",
+ "loud-rejection": "^1.0.0",
+ "minimist-options": "^3.0.1",
+ "normalize-package-data": "^2.3.4",
+ "read-pkg-up": "^3.0.0",
+ "redent": "^2.0.0",
+ "trim-newlines": "^2.0.0",
+ "yargs-parser": "^10.0.0"
+ }
+ },
+ "micromatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+ "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.0.5"
+ }
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
+ "npm-run-path": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.0.tgz",
+ "integrity": "sha512-8eyAOAH+bYXFPSnNnKr3J+yoybe8O87Is5rtAQ8qRczJz1ajcsjg8l2oZqP+Ppx15Ii3S1vUTjQN2h4YO2tWWQ==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.0.0"
+ }
+ },
+ "onetime": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
+ "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "p-finally": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
+ "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==",
+ "dev": true
+ },
+ "path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true
+ },
+ "restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "type-fest": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz",
+ "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==",
+ "dev": true
+ },
+ "yargs-parser": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
+ "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^4.1.0"
+ }
+ }
+ }
+ },
"react-dates": {
"version": "17.2.0",
"resolved": "https://registry.npmjs.org/react-dates/-/react-dates-17.2.0.tgz",
@@ -25156,6 +25810,12 @@
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
+ "strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true
+ },
"strip-indent": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
@@ -25831,6 +26491,26 @@
}
}
},
+ "temp": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz",
+ "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==",
+ "dev": true,
+ "requires": {
+ "rimraf": "~2.6.2"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
"temp-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
diff --git a/package.json b/package.json
index 6f07e7b2b3f7f..da6b36b0f9cb3 100644
--- a/package.json
+++ b/package.json
@@ -332,6 +332,7 @@
"babel-jest": "24.9.0",
"babel-plugin-dynamic-import-node": "2.3.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
+ "calypso-codemods": "file:./packages/calypso-codemods",
"chai": "4.2.0",
"chai-enzyme": "1.0.0-beta.1",
"check-node-version": "3.3.0",
diff --git a/packages/calypso-codemods/.eslintrc.js b/packages/calypso-codemods/.eslintrc.js
new file mode 100644
index 0000000000000..f8196083004b6
--- /dev/null
+++ b/packages/calypso-codemods/.eslintrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ rules: {
+ 'import/no-nodejs-modules': 'off',
+ 'import/no-extraneous-dependencies': [ 'error', { packageDir: __dirname } ],
+ },
+};
diff --git a/packages/calypso-codemods/README.md b/packages/calypso-codemods/README.md
new file mode 100644
index 0000000000000..cc72e1458fbf7
--- /dev/null
+++ b/packages/calypso-codemods/README.md
@@ -0,0 +1,113 @@
+# Calypso Codemods
+
+## What are codemods?
+
+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).
+
+## Getting started
+
+Install calypso-codemods using `npm` or `yarn`:
+```
+npm install -g calypso-codemods
+```
+
+Now you can run codemods using the following cli:
+```bash
+calypso-codemods transformation-name[,second-name,third-name] target [additional targets]
+```
+
+For example, if I wanted to run the `commonjs-exports` transformation on `client/devdocs/`, I can do the following:
+
+```bash
+calypso-codemods commonjs-exports client/devdocs/
+```
+
+Do you want to target files individually? We can do that, too!
+
+```bash
+calypso-codemods commonjs-exports client/devdocs/a.js client/devdocs/b.js client/devdocs/c.js
+```
+
+How about chaining codemods on multiple directories?
+
+```bash
+calypso-codemods commonjs-imports,commonjs-exports,named-exports-from-default client/blocks/ client/components/
+```
+
+## List of available transformations
+
+### 5to6-codemod scripts ([docs](https://github.com/5to6/5to6-codemod#transforms))
+
+- commonjs-exports
+ - This codemod converts `module.exports` to `export` and `export default`.
+
+- commonjs-imports
+ - 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.
+
+- commonjs-imports-hoist
+ - This transformation hoists all occurrences of `require( '...' )` inside if, loop, and function blocks. This can cause breakage! Use with caution.
+
+- named-exports-from-default
+ - 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'`).
+
+### React scripts ([docs](https://github.com/reactjs/react-codemod))
+
+- react-create-class
+ - This transformation converts instances of React.createClass to use React.Component instead.
+
+- react-proptypes
+ - This transformation converts instances of React.PropTypes to use prop-types instead.
+
+### Local scripts
+- combine-reducer-with-persistence
+ - This transformation converts combineReducers imports to use combineReducersWithPersistence.
+
+- combine-state-utils-imports
+ - This transformation combines state/utils imports.
+
+- i18n-mixin-to-localize
+ - This transformation converts the following:
+ - `this.translate` to `this.props.translate`
+ - `this.moment` to `this.props.moment`
+ - `this.numberFormat` to `this.props.numberFormat`
+ - If any of the above conversions is performed, this transformation will wrap the React.createClass instance with a `localize()` higher-order component.
+
+- merge-lodash-imports
+ - This transformation merges multiple named lodash imports into one
+
+- modular-lodash-no-more
+ - This transformation converts modular lodash imports to ES2015-style imports
+
+- modular-lodash-requires-no-more
+ - This transformation converts modular lodash requires to ES2015-style imports
+
+- rename-combine-reducers
+ - This transformation converts combineReducersWithPersistence imports to use combineReducers from 'state/utils'
+
+- single-tree-rendering
+ - Instead of rendering two distinct React element trees to the `#primary` and `#secondary`
s,
+ use a single `Layout` component tree that includes both, and render it to `#layout`.
+
+- sort-imports
+ - This transformation adds import comment blocks and sorts them as necessary.
+ - Note: It only needs to be run twice because of a bug where in certain cases an extra newline is added
+ on the first run. The second run removes the extra newline.
+
+## Contributing codemods
+### Write the transform
+Write your transform using the standard jscodeshift api and place it in the transforms directory.
+You can look at the current directory for inspiration.
+
+### Add some tests!
+calypso-codemods uses jest snapshots to maintain its tests.
+in order to easily add tests for a new transform, follow these steps:
+
+1. add a directory to `tests` with the exact same name as the added transform.
+2. add a file named `codemod.spec.js` with this as its contents contents:
+```javascript
+test_folder(__dirname);
+```
+3. add any input files to the folder that you wish to be tested
+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`.
+
+5. make sure to commit any modified snapshots and include it in your pull request
diff --git a/packages/calypso-codemods/api.js b/packages/calypso-codemods/api.js
new file mode 100644
index 0000000000000..45900f52f7ae1
--- /dev/null
+++ b/packages/calypso-codemods/api.js
@@ -0,0 +1,84 @@
+#!/usr/bin/env node
+
+/**
+ * External dependencies
+ */
+const fs = require( 'fs' );
+const path = require( 'path' );
+const child_process = require( 'child_process' );
+
+/**
+ * Internal dependencies
+ */
+const config = require( path.join( __dirname, 'config' ) );
+const transformsDir = path.join( __dirname, './transforms' );
+
+const jscodeshiftBin = require( 'module' )
+ .createRequireFromPath( require.resolve( 'jscodeshift' ) )
+ .resolve( require( 'jscodeshift/package.json' ).bin.jscodeshift );
+
+function getLocalCodemodFileNames() {
+ const jsFiles = fs
+ .readdirSync( transformsDir )
+ .filter( filename => filename.endsWith( '.js' ) )
+ .map( name => path.basename( name, '.js' ) ); // strip path and extension from filename
+
+ return jsFiles;
+}
+
+function getValidCodemodNames() {
+ return [ ...getLocalCodemodFileNames(), ...Object.getOwnPropertyNames( config.codemodArgs ) ]
+ .map( name => '- ' + name )
+ .sort();
+}
+
+function generateBinArgs( name ) {
+ if ( Object.prototype.hasOwnProperty.call( config.codemodArgs, name ) ) {
+ // Is the codemod defined in the codemodArgs object?
+ return config.codemodArgs[ name ];
+ }
+
+ if ( getLocalCodemodFileNames().includes( name ) ) {
+ return [ `--transform=${ transformsDir }/${ name }.js` ];
+ }
+
+ throw new Error( `"${ name }" is an unrecognized codemod.` );
+}
+
+function runCodemod( codemodName, transformTargets ) {
+ const binArgs = [
+ ...config.jscodeshiftArgs,
+ ...generateBinArgs( codemodName ),
+ ...transformTargets,
+ ];
+
+ process.stdout.write( `\nRunning ${ codemodName } on ${ transformTargets.join( ' ' ) }\n` );
+
+ child_process.spawnSync( jscodeshiftBin, binArgs, {
+ stdio: [ 'ignore', process.stdout, process.stderr ],
+ } );
+}
+
+function runCodemodDry( codemodName, filepath ) {
+ const binArgs = [
+ ...config.jscodeshiftArgs,
+ ...generateBinArgs( codemodName ),
+ '--dry',
+ '--print',
+ '--silent',
+ filepath,
+ ];
+ const result = child_process.spawnSync( jscodeshiftBin, binArgs, {
+ stdio: 'pipe',
+ } );
+
+ return result.stdout.toString();
+}
+
+module.exports = {
+ runCodemod,
+ runCodemodDry,
+ generateBinArgs,
+ getValidCodemodNames,
+ getLocalCodemodFileNames,
+};
diff --git a/packages/calypso-codemods/config.js b/packages/calypso-codemods/config.js
new file mode 100644
index 0000000000000..6c757adea5449
--- /dev/null
+++ b/packages/calypso-codemods/config.js
@@ -0,0 +1,72 @@
+const jscodeshiftArgs = [ '--extensions=js,jsx' ];
+
+// Used primarily by 5to6-codemod transformations
+const recastArgs = [ '--useTabs=true', '--arrayBracketSpacing=true' ];
+
+const recastOptions = {
+ arrayBracketSpacing: true,
+ objectCurlySpacing: true,
+ quote: 'single',
+ useTabs: true,
+ trailingComma: {
+ objects: true,
+ arrays: true,
+ parameters: false,
+ },
+};
+
+const commonArgs = {
+ '5to6': [
+ // Recast options via 5to6
+ ...recastArgs,
+ ],
+ react: [
+ // Recast options via react-codemod
+ `--printOptions=${ JSON.stringify( recastOptions ) }`,
+ ],
+};
+
+const codemodArgs = {
+ 'commonjs-exports': [
+ ...commonArgs[ '5to6' ],
+ `--transform=${ require.resolve( '5to6-codemod/transforms/exports.js' ) }`,
+ ],
+
+ 'commonjs-imports': [
+ ...commonArgs[ '5to6' ],
+ `--transform=${ require.resolve( '5to6-codemod/transforms/cjs.js' ) }`,
+ ],
+
+ 'commonjs-imports-hoist': [
+ ...commonArgs[ '5to6' ],
+ `--transform=${ require.resolve( '5to6-codemod/transforms/cjs.js' ) }`,
+ '--hoist=true',
+ ],
+
+ 'named-exports-from-default': [
+ ...commonArgs[ '5to6' ],
+ `--transform=${ require.resolve( '5to6-codemod/transforms/named-export-generation.js' ) }`,
+ ],
+
+ 'react-create-class': [
+ ...commonArgs.react,
+ `--transform=${ require.resolve( 'react-codemod/transforms/class.js' ) }`,
+
+ // react-codemod options
+ '--pure-component=true',
+ '--mixin-module-name="react-pure-render/mixin"', // Your days are numbered, pure-render-mixin!
+ ],
+
+ 'react-proptypes': [
+ ...commonArgs.react,
+ `--transform=${ require.resolve(
+ 'react-codemod/transforms/React-PropTypes-to-prop-types.js'
+ ) }`,
+ ],
+};
+
+module.exports = {
+ codemodArgs,
+ jscodeshiftArgs,
+ recastOptions,
+};
diff --git a/packages/calypso-codemods/index.js b/packages/calypso-codemods/index.js
new file mode 100755
index 0000000000000..06828eb88f508
--- /dev/null
+++ b/packages/calypso-codemods/index.js
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+
+/**
+ * Internal dependencies
+ */
+const api = require( './api' );
+
+function main() {
+ const args = process.argv.slice( 2 );
+ if ( args.length === 0 || args.length === 1 ) {
+ process.stdout.write(
+ [
+ '',
+ 'calypso-codemods codemodName[,additionalCodemods…] target1 [additionalTargets…]',
+ '',
+ 'Valid transformation names:',
+ api.getValidCodemodNames().join( '\n' ),
+ '',
+ 'Example: "calypso-codemods commonjs-imports client/blocks client/devdocs"',
+ '',
+ ].join( '\n' )
+ );
+
+ process.exit( 0 ); // eslint-disable-line no-process-exit
+ }
+
+ const [ names, ...targets ] = args;
+ names.split( ',' ).forEach( codemodName => api.runCodemod( codemodName, targets ) );
+}
+
+main();
diff --git a/packages/calypso-codemods/jest.config.js b/packages/calypso-codemods/jest.config.js
new file mode 100644
index 0000000000000..268917f1ebce5
--- /dev/null
+++ b/packages/calypso-codemods/jest.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ preset: '@automattic/calypso-build',
+ rootDir: __dirname,
+ testMatch: [ '/tests/*/codemod.spec.js' ],
+ setupFiles: [ '/setup-tests.js' ],
+};
diff --git a/packages/calypso-codemods/package.json b/packages/calypso-codemods/package.json
new file mode 100644
index 0000000000000..b9ee92f4ef0a1
--- /dev/null
+++ b/packages/calypso-codemods/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "calypso-codemods",
+ "version": "0.1.6",
+ "description": "jscodeshift transforms used to upgrade calypso code",
+ "main": "./api.js",
+ "bin": {
+ "calypso-codemods": "./index.js"
+ },
+ "scripts": {
+ "test": "jest"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Automattic/wp-calypso.git",
+ "directory": "packages/calypso-codemods"
+ },
+ "author": "Automattic Inc.",
+ "license": "GPL-2.0-or-later",
+ "bugs": {
+ "url": "https://github.com/Automattic/wp-calypso/issues"
+ },
+ "homepage": "https://github.com/Automattic/wp-calypso/tree/master/packages/calypso-codemods#readme",
+ "dependencies": {
+ "5to6-codemod": "^1.8.0",
+ "jscodeshift": "^0.6.4",
+ "lodash": "^4.17.10",
+ "react-codemod": "^5.0.5"
+ }
+}
diff --git a/packages/calypso-codemods/setup-tests.js b/packages/calypso-codemods/setup-tests.js
new file mode 100644
index 0000000000000..595b3db11d470
--- /dev/null
+++ b/packages/calypso-codemods/setup-tests.js
@@ -0,0 +1,20 @@
+const fs = require( 'fs' );
+const path = require( 'path' );
+const api = require.requireActual( './api' );
+
+function test_folder( dir ) {
+ const testFiles = fs
+ .readdirSync( dir )
+ .filter( f => ! f.endsWith( '.spec.js' ) && f.endsWith( '.js' ) );
+
+ testFiles.forEach( filename => {
+ const filepath = path.join( dir, filename );
+
+ test( filename, () => {
+ const result = api.runCodemodDry( path.basename( dir ), filepath );
+ expect( result ).toMatchSnapshot();
+ } );
+ } );
+}
+
+global.test_folder = test_folder;
diff --git a/packages/calypso-codemods/tests/combine-reducer-with-persistence/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/combine-reducer-with-persistence/__snapshots__/codemod.spec.js.snap
new file mode 100644
index 0000000000000..80f9eb5eadc97
--- /dev/null
+++ b/packages/calypso-codemods/tests/combine-reducer-with-persistence/__snapshots__/codemod.spec.js.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`basic.js 1`] = `
+"/**
+ * Internal dependencies
+ */
+import hello from 'internal/lib';
+
+import { combineReducers } from \\"state/utils\\";
+
+"
+`;
diff --git a/packages/calypso-codemods/tests/combine-reducer-with-persistence/basic.js b/packages/calypso-codemods/tests/combine-reducer-with-persistence/basic.js
new file mode 100644
index 0000000000000..56e4fdeec85a7
--- /dev/null
+++ b/packages/calypso-codemods/tests/combine-reducer-with-persistence/basic.js
@@ -0,0 +1,6 @@
+import { combineReducers } from 'redux';
+
+/**
+ * Internal dependencies
+ */
+import hello from 'internal/lib';
diff --git a/packages/calypso-codemods/tests/combine-reducer-with-persistence/codemod.spec.js b/packages/calypso-codemods/tests/combine-reducer-with-persistence/codemod.spec.js
new file mode 100644
index 0000000000000..b8437733fac80
--- /dev/null
+++ b/packages/calypso-codemods/tests/combine-reducer-with-persistence/codemod.spec.js
@@ -0,0 +1 @@
+test_folder( __dirname );
diff --git a/packages/calypso-codemods/tests/combine-state-utils-imports/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/combine-state-utils-imports/__snapshots__/codemod.spec.js.snap
new file mode 100644
index 0000000000000..3ebb32692ea39
--- /dev/null
+++ b/packages/calypso-codemods/tests/combine-state-utils-imports/__snapshots__/codemod.spec.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`basic.js 1`] = `
+"import { baz, combineReducersWithPersistence as bar, createReducer } from \\"state/utils\\";
+
+"
+`;
diff --git a/packages/calypso-codemods/tests/combine-state-utils-imports/basic.js b/packages/calypso-codemods/tests/combine-state-utils-imports/basic.js
new file mode 100644
index 0000000000000..9c684e4a22e89
--- /dev/null
+++ b/packages/calypso-codemods/tests/combine-state-utils-imports/basic.js
@@ -0,0 +1,2 @@
+import { createReducer } from 'state/utils';
+import { combineReducersWithPersistence as bar, baz } from 'state/utils';
diff --git a/packages/calypso-codemods/tests/combine-state-utils-imports/codemod.spec.js b/packages/calypso-codemods/tests/combine-state-utils-imports/codemod.spec.js
new file mode 100644
index 0000000000000..b8437733fac80
--- /dev/null
+++ b/packages/calypso-codemods/tests/combine-state-utils-imports/codemod.spec.js
@@ -0,0 +1 @@
+test_folder( __dirname );
diff --git a/packages/calypso-codemods/tests/i18n-mixin-to-localize/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/i18n-mixin-to-localize/__snapshots__/codemod.spec.js.snap
new file mode 100644
index 0000000000000..b77f1429a5db4
--- /dev/null
+++ b/packages/calypso-codemods/tests/i18n-mixin-to-localize/__snapshots__/codemod.spec.js.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`basic.js 1`] = `
+"import React from 'react';
+
+import { localize } from \\"i18n-calypso\\";
+
+const PrettyComponent = React.createClass( {
+ render() {
+ this.props.translate( 'codemods are fun' );
+ },
+} );
+
+export default localize(PrettyComponent);
+
+"
+`;
diff --git a/packages/calypso-codemods/tests/i18n-mixin-to-localize/basic.js b/packages/calypso-codemods/tests/i18n-mixin-to-localize/basic.js
new file mode 100644
index 0000000000000..27cae8a118568
--- /dev/null
+++ b/packages/calypso-codemods/tests/i18n-mixin-to-localize/basic.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const PrettyComponent = React.createClass( {
+ render() {
+ this.translate( 'codemods are fun' );
+ },
+} );
+
+export default PrettyComponent;
diff --git a/packages/calypso-codemods/tests/i18n-mixin-to-localize/codemod.spec.js b/packages/calypso-codemods/tests/i18n-mixin-to-localize/codemod.spec.js
new file mode 100644
index 0000000000000..b8437733fac80
--- /dev/null
+++ b/packages/calypso-codemods/tests/i18n-mixin-to-localize/codemod.spec.js
@@ -0,0 +1 @@
+test_folder( __dirname );
diff --git a/packages/calypso-codemods/tests/merge-lodash-imports/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/merge-lodash-imports/__snapshots__/codemod.spec.js.snap
new file mode 100644
index 0000000000000..ae94507e25f01
--- /dev/null
+++ b/packages/calypso-codemods/tests/merge-lodash-imports/__snapshots__/codemod.spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`basic.js 1`] = `
+"/* @format */
+import { map, pick, zip } from \\"lodash\\";
+
+"
+`;
+
+exports[`comments-in-between.js 1`] = `
+"// 1 - zippy
+import { map, pick, zip } from \\"lodash\\";
+
+"
+`;
diff --git a/packages/calypso-codemods/tests/merge-lodash-imports/basic.js b/packages/calypso-codemods/tests/merge-lodash-imports/basic.js
new file mode 100644
index 0000000000000..c55d9215d80cd
--- /dev/null
+++ b/packages/calypso-codemods/tests/merge-lodash-imports/basic.js
@@ -0,0 +1,4 @@
+/* @format */
+import { zip } from 'lodash';
+import { map } from 'lodash';
+import { pick } from 'lodash';
diff --git a/packages/calypso-codemods/tests/merge-lodash-imports/codemod.spec.js b/packages/calypso-codemods/tests/merge-lodash-imports/codemod.spec.js
new file mode 100644
index 0000000000000..b8437733fac80
--- /dev/null
+++ b/packages/calypso-codemods/tests/merge-lodash-imports/codemod.spec.js
@@ -0,0 +1 @@
+test_folder( __dirname );
diff --git a/packages/calypso-codemods/tests/merge-lodash-imports/comments-in-between.js b/packages/calypso-codemods/tests/merge-lodash-imports/comments-in-between.js
new file mode 100644
index 0000000000000..63cc8ab5e4b35
--- /dev/null
+++ b/packages/calypso-codemods/tests/merge-lodash-imports/comments-in-between.js
@@ -0,0 +1,9 @@
+// 1 - zippy
+import { zip } from 'lodash';
+// 2 - mappy
+import { map } from 'lodash';
+// 3 - picky
+import { pick } from 'lodash';
+/*
+ * comment block. Note how this is currently broken and gets garbled in the snapshot
+ */
diff --git a/packages/calypso-codemods/tests/modular-lodash-no-more/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/modular-lodash-no-more/__snapshots__/codemod.spec.js.snap
new file mode 100644
index 0000000000000..1422f97d91c97
--- /dev/null
+++ b/packages/calypso-codemods/tests/modular-lodash-no-more/__snapshots__/codemod.spec.js.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`basic.js 1`] = `
+"import { map } from \\"lodash\\";
+import { zip } from \\"lodash\\";
+
+"
+`;
diff --git a/packages/calypso-codemods/tests/modular-lodash-no-more/basic.js b/packages/calypso-codemods/tests/modular-lodash-no-more/basic.js
new file mode 100644
index 0000000000000..dc7fe4f721715
--- /dev/null
+++ b/packages/calypso-codemods/tests/modular-lodash-no-more/basic.js
@@ -0,0 +1,2 @@
+import map from 'lodash/map';
+import zip from 'lodash/zip';
diff --git a/packages/calypso-codemods/tests/modular-lodash-no-more/codemod.spec.js b/packages/calypso-codemods/tests/modular-lodash-no-more/codemod.spec.js
new file mode 100644
index 0000000000000..b8437733fac80
--- /dev/null
+++ b/packages/calypso-codemods/tests/modular-lodash-no-more/codemod.spec.js
@@ -0,0 +1 @@
+test_folder( __dirname );
diff --git a/packages/calypso-codemods/tests/modular-lodash-requires-no-more/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/modular-lodash-requires-no-more/__snapshots__/codemod.spec.js.snap
new file mode 100644
index 0000000000000..70c28b8142e42
--- /dev/null
+++ b/packages/calypso-codemods/tests/modular-lodash-requires-no-more/__snapshots__/codemod.spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`basic.js 1`] = `
+"/**
+ * External dependencies
+ */
+import { zip as zippy } from \\"lodash\\";
+
+import { map as mappy } from \\"lodash\\";
+import { find } from \\"lodash\\";
+
+"
+`;
+
+exports[`comments-in-between.js 1`] = `""`;
diff --git a/packages/calypso-codemods/tests/modular-lodash-requires-no-more/basic.js b/packages/calypso-codemods/tests/modular-lodash-requires-no-more/basic.js
new file mode 100644
index 0000000000000..18e7c51e5ccf8
--- /dev/null
+++ b/packages/calypso-codemods/tests/modular-lodash-requires-no-more/basic.js
@@ -0,0 +1,3 @@
+const zippy = require( 'lodash/zip' );
+const mappy = require( 'lodash/map' );
+const find = require( 'lodash/find' );
diff --git a/packages/calypso-codemods/tests/modular-lodash-requires-no-more/codemod.spec.js b/packages/calypso-codemods/tests/modular-lodash-requires-no-more/codemod.spec.js
new file mode 100644
index 0000000000000..b8437733fac80
--- /dev/null
+++ b/packages/calypso-codemods/tests/modular-lodash-requires-no-more/codemod.spec.js
@@ -0,0 +1 @@
+test_folder( __dirname );
diff --git a/packages/calypso-codemods/tests/modular-lodash-requires-no-more/comments-in-between.js b/packages/calypso-codemods/tests/modular-lodash-requires-no-more/comments-in-between.js
new file mode 100644
index 0000000000000..63cc8ab5e4b35
--- /dev/null
+++ b/packages/calypso-codemods/tests/modular-lodash-requires-no-more/comments-in-between.js
@@ -0,0 +1,9 @@
+// 1 - zippy
+import { zip } from 'lodash';
+// 2 - mappy
+import { map } from 'lodash';
+// 3 - picky
+import { pick } from 'lodash';
+/*
+ * comment block. Note how this is currently broken and gets garbled in the snapshot
+ */
diff --git a/packages/calypso-codemods/tests/remove-create-reducer/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/remove-create-reducer/__snapshots__/codemod.spec.js.snap
new file mode 100644
index 0000000000000..53442646bc760
--- /dev/null
+++ b/packages/calypso-codemods/tests/remove-create-reducer/__snapshots__/codemod.spec.js.snap
@@ -0,0 +1,105 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`create-reducer.js 1`] = `
+"// This comment should be preserved even if the line below is removed.
+import { withSchemaValidation, withoutPersistence } from 'state/utils';
+
+const COMPUTED_IDENTIFIER = 'COMPUTED_IDENTIFIER';
+
+const isFetchingSettings = withoutPersistence((state = false, action) => {
+ switch (action.type) {
+ case COMPUTED_IDENTIFIER:
+ return 'computed_id';
+ case 'COMPUTED_STRING':
+ return state;
+ case \\"NON_COMPUTED_STRING\\":
+ return action.thing;
+ case \\"2\\":
+ return 2;
+ case \\"FUNCTION_HANDLER\\":
+ return function( s, a ) {
+ return s;
+ }(state, action);
+ case \\"ARROW_FUNCTION_HANDLER\\":
+ return state;
+ case \\"ARROW_FUNCTION_WITH_DESTRUCT\\":
+ {
+ const { thing } = action;
+ return thing;
+ }
+ case \\"VARIABLE_HANDLER\\":
+ return f(state, action);
+ }
+
+ return state;
+});
+
+function f() {
+ return 'a function reducer';
+}
+
+const persistentReducer = (state = false, action) => {
+ switch (action.type) {
+ case COMPUTED_IDENTIFIER:
+ return 'computed_id';
+ case 'SERIALIZE':
+ return state;
+ }
+
+ return state;
+};
+
+persistentReducer.hasCustomPersistence = true;
+
+export const exportedPersistentReducer = (state = false, action) => {
+ switch (action.type) {
+ case COMPUTED_IDENTIFIER:
+ return 'computed_id';
+ case 'SERIALIZE':
+ return state;
+ }
+
+ return state;
+};
+
+exportedPersistentReducer.hasCustomPersistence = true;
+
+const persistentReducerArray = [];
+reducerArray[ 0 ] = (state = false, action) => {
+ switch (action.type) {
+ case COMPUTED_IDENTIFIER:
+ return 'computed_id';
+ case 'DESERIALIZE':
+ return state;
+ }
+
+ return state;
+};
+
+reducerArray[ 0 ].hasCustomPersistence = true;
+
+const persistentReducerObj = {
+ key: // TODO: HANDLE PERSISTENCE
+ (state = false, action) => {
+ switch (action.type) {
+ case COMPUTED_IDENTIFIER:
+ return 'computed_id';
+ case 'DESERIALIZE':
+ return state;
+ }
+
+ return state;
+ },
+};
+
+const validatedReducer = withSchemaValidation(schema, (state = false, action) => {
+ switch (action.type) {
+ case COMPUTED_IDENTIFIER:
+ return 'computed_id';
+ }
+
+ return state;
+});
+
+"
+`;
diff --git a/packages/calypso-codemods/tests/remove-create-reducer/codemod.spec.js b/packages/calypso-codemods/tests/remove-create-reducer/codemod.spec.js
new file mode 100644
index 0000000000000..b8437733fac80
--- /dev/null
+++ b/packages/calypso-codemods/tests/remove-create-reducer/codemod.spec.js
@@ -0,0 +1 @@
+test_folder( __dirname );
diff --git a/packages/calypso-codemods/tests/remove-create-reducer/create-reducer.js b/packages/calypso-codemods/tests/remove-create-reducer/create-reducer.js
new file mode 100644
index 0000000000000..90b79f17cf779
--- /dev/null
+++ b/packages/calypso-codemods/tests/remove-create-reducer/create-reducer.js
@@ -0,0 +1,52 @@
+// This comment should be preserved even if the line below is removed.
+import { createReducer, createReducerWithValidation } from 'state/utils';
+
+const COMPUTED_IDENTIFIER = 'COMPUTED_IDENTIFIER';
+
+const isFetchingSettings = createReducer( false, {
+ [ COMPUTED_IDENTIFIER ]: () => 'computed_id',
+ [ 'COMPUTED_STRING' ]: state => state,
+ NON_COMPUTED_STRING: ( state, action ) => action.thing,
+ 2: () => 2,
+ FUNCTION_HANDLER: function( s, a ) {
+ return s;
+ },
+ ARROW_FUNCTION_HANDLER: ( state, action ) => state,
+ ARROW_FUNCTION_WITH_DESTRUCT: ( state, { thing } ) => thing,
+ VARIABLE_HANDLER: f,
+} );
+
+function f() {
+ return 'a function reducer';
+}
+
+const persistentReducer = createReducer( false, {
+ [ COMPUTED_IDENTIFIER ]: () => 'computed_id',
+ [ 'SERIALIZE' ]: state => state,
+} );
+
+export const exportedPersistentReducer = createReducer( false, {
+ [ COMPUTED_IDENTIFIER ]: () => 'computed_id',
+ [ 'SERIALIZE' ]: state => state,
+} );
+
+const persistentReducerArray = [];
+reducerArray[ 0 ] = createReducer( false, {
+ [ COMPUTED_IDENTIFIER ]: () => 'computed_id',
+ [ 'DESERIALIZE' ]: state => state,
+} );
+
+const persistentReducerObj = {
+ key: createReducer( false, {
+ [ COMPUTED_IDENTIFIER ]: () => 'computed_id',
+ [ 'DESERIALIZE' ]: state => state,
+ } ),
+};
+
+const validatedReducer = createReducerWithValidation(
+ false,
+ {
+ [ COMPUTED_IDENTIFIER ]: () => 'computed_id',
+ },
+ schema
+);
diff --git a/packages/calypso-codemods/tests/rename-combine-reducers/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/rename-combine-reducers/__snapshots__/codemod.spec.js.snap
new file mode 100644
index 0000000000000..3416ca739b28b
--- /dev/null
+++ b/packages/calypso-codemods/tests/rename-combine-reducers/__snapshots__/codemod.spec.js.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`basic.js 1`] = `
+"import { combineReducers } from \\"state/utils\\";
+
+combineReducers( {
+ foo,
+ bar,
+} );
+
+"
+`;
diff --git a/packages/calypso-codemods/tests/rename-combine-reducers/basic.js b/packages/calypso-codemods/tests/rename-combine-reducers/basic.js
new file mode 100644
index 0000000000000..42c892effb553
--- /dev/null
+++ b/packages/calypso-codemods/tests/rename-combine-reducers/basic.js
@@ -0,0 +1,6 @@
+import { combineReducersWithPersistence } from 'state/utils';
+
+combineReducersWithPersistence( {
+ foo,
+ bar,
+} );
diff --git a/packages/calypso-codemods/tests/rename-combine-reducers/codemod.spec.js b/packages/calypso-codemods/tests/rename-combine-reducers/codemod.spec.js
new file mode 100644
index 0000000000000..b8437733fac80
--- /dev/null
+++ b/packages/calypso-codemods/tests/rename-combine-reducers/codemod.spec.js
@@ -0,0 +1 @@
+test_folder( __dirname );
diff --git a/packages/calypso-codemods/tests/sort-imports/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/sort-imports/__snapshots__/codemod.spec.js.snap
new file mode 100644
index 0000000000000..fc79de859df79
--- /dev/null
+++ b/packages/calypso-codemods/tests/sort-imports/__snapshots__/codemod.spec.js.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`incorrect-docblocks.js 1`] = `
+"/**
+ * External dependencies
+ */
+import fs from 'fs';
+
+/**
+ * Internal dependencies
+ */
+import chickenLibrary from 'chicken';
+import okapiMe from 'okapi-me';
+
+function hello() {}
+
+"
+`;
+
+exports[`only-imports.js 1`] = `
+"/**
+ * External dependencies
+ */
+import fs from 'fs';
+
+/**
+ * Internal dependencies
+ */
+import chickenLibrary from 'chicken';
+import okapiMe from 'okapi-me';
+
+"
+`;
+
+exports[`with-body.js 1`] = `
+"/**
+ * External dependencies
+ */
+import fs from 'fs';
+
+/**
+ * Internal dependencies
+ */
+import chickenLibrary from 'chicken';
+import okapiMe from 'okapi-me';
+
+const x = 5;
+
+"
+`;
diff --git a/packages/calypso-codemods/tests/sort-imports/codemod.spec.js b/packages/calypso-codemods/tests/sort-imports/codemod.spec.js
new file mode 100644
index 0000000000000..b8437733fac80
--- /dev/null
+++ b/packages/calypso-codemods/tests/sort-imports/codemod.spec.js
@@ -0,0 +1 @@
+test_folder( __dirname );
diff --git a/packages/calypso-codemods/tests/sort-imports/incorrect-docblocks.js b/packages/calypso-codemods/tests/sort-imports/incorrect-docblocks.js
new file mode 100644
index 0000000000000..c3e3c0cc62f75
--- /dev/null
+++ b/packages/calypso-codemods/tests/sort-imports/incorrect-docblocks.js
@@ -0,0 +1,12 @@
+/**
+ * External dependencies
+ */
+import chickenLibrary from 'chicken';
+import okapiMe from 'okapi-me';
+
+/**
+ * Magic dependencies
+ */
+import fs from 'fs';
+
+function hello() {}
diff --git a/packages/calypso-codemods/tests/sort-imports/only-imports.js b/packages/calypso-codemods/tests/sort-imports/only-imports.js
new file mode 100644
index 0000000000000..3b8017eea6391
--- /dev/null
+++ b/packages/calypso-codemods/tests/sort-imports/only-imports.js
@@ -0,0 +1,3 @@
+import fs from 'fs';
+import chickenLibrary from 'chicken';
+import okapiMe from 'okapi-me';
diff --git a/packages/calypso-codemods/tests/sort-imports/with-body.js b/packages/calypso-codemods/tests/sort-imports/with-body.js
new file mode 100644
index 0000000000000..e67b849795fbd
--- /dev/null
+++ b/packages/calypso-codemods/tests/sort-imports/with-body.js
@@ -0,0 +1,5 @@
+import fs from 'fs';
+import chickenLibrary from 'chicken';
+import okapiMe from 'okapi-me';
+
+const x = 5;
diff --git a/packages/calypso-codemods/transforms/combine-reducer-with-persistence.js b/packages/calypso-codemods/transforms/combine-reducer-with-persistence.js
new file mode 100644
index 0000000000000..99a560b35944b
--- /dev/null
+++ b/packages/calypso-codemods/transforms/combine-reducer-with-persistence.js
@@ -0,0 +1,54 @@
+/*
+ This codemod updates
+
+ import { combineReducers } from 'redux'; to
+ import { combineReducers } from 'state/utils';
+ */
+
+module.exports = function( file, api ) {
+ // alias the jscodeshift API
+ const j = api.jscodeshift;
+ // parse JS code into an AST
+ const root = j( file.source );
+
+ //remove combineReducer import
+ const combineReducerImport = root
+ .find( j.ImportDeclaration, {
+ source: {
+ type: 'Literal',
+ value: 'redux',
+ },
+ } )
+ .filter( importDeclaration => {
+ if ( importDeclaration.value.specifiers.length === 1 ) {
+ return importDeclaration.value.specifiers[ 0 ].imported.name === 'combineReducers';
+ }
+ return false;
+ } );
+
+ if ( ! combineReducerImport.length ) {
+ return;
+ }
+
+ combineReducerImport.remove();
+
+ // find the first external import
+ const firstInternalImport = root.find( j.ImportDeclaration ).filter( item => {
+ if ( item.node.comments && item.node.comments.length > 0 ) {
+ return item.node.comments[ 0 ].value.match( /Internal dependencies/ );
+ }
+ return false;
+ } );
+
+ const combineReducersImport = () => {
+ return j.importDeclaration(
+ [ j.importSpecifier( j.identifier( 'combineReducers' ) ) ],
+ j.literal( 'state/utils' )
+ );
+ };
+ //note the extra whitespace coming from https://github.com/benjamn/recast/issues/371
+ firstInternalImport.insertAfter( combineReducersImport );
+
+ // print
+ return root.toSource();
+};
diff --git a/packages/calypso-codemods/transforms/combine-state-utils-imports.js b/packages/calypso-codemods/transforms/combine-state-utils-imports.js
new file mode 100644
index 0000000000000..b71888c5e5f98
--- /dev/null
+++ b/packages/calypso-codemods/transforms/combine-state-utils-imports.js
@@ -0,0 +1,75 @@
+/*
+ This codemod updates
+
+ import { createReducer } from 'state/utils';
+ import { combineReducersWithPersistence as bar, baz } from 'state/utils'
+
+ to
+
+ import { baz, combineReducersWithPersistence as bar, createReducer } from 'state/utils';
+ */
+
+module.exports = function( file, api ) {
+ // alias the jscodeshift API
+ const j = api.jscodeshift;
+ // parse JS code into an AST
+ const root = j( file.source );
+
+ const stateUtilsImports = root.find( j.ImportDeclaration, {
+ source: {
+ type: 'Literal',
+ value: 'state/utils',
+ },
+ } );
+
+ if ( stateUtilsImports.length < 2 ) {
+ return;
+ }
+
+ //grab each identifier
+ const importNames = [];
+ stateUtilsImports.find( j.ImportSpecifier ).forEach( item => {
+ importNames.push( {
+ local: item.value.local.name,
+ imported: item.value.imported.name,
+ } );
+ } );
+
+ //sort by imported name
+ importNames.sort( ( a, b ) => {
+ if ( a.imported < b.imported ) {
+ return -1;
+ }
+ if ( a.imported > b.imported ) {
+ return 1;
+ }
+ return 0;
+ } );
+
+ //Save Comment if possible
+ const comments = stateUtilsImports.at( 0 ).get().node.comments;
+
+ const addImport = imports => {
+ const names = imports.map( name => {
+ if ( name.local === name.imported ) {
+ return j.importSpecifier( j.identifier( name.local ) );
+ }
+ if ( name.local !== name.imported ) {
+ return j.importSpecifier( j.identifier( name.imported ), j.identifier( name.local ) );
+ }
+ } );
+ const combinedImport = j.importDeclaration( names, j.literal( 'state/utils' ) );
+ combinedImport.comments = comments;
+ return combinedImport;
+ };
+
+ //replace the first one with the combined import
+ stateUtilsImports.at( 0 ).replaceWith( addImport( importNames ) );
+ //remove the rest
+ for ( let i = 1; i < stateUtilsImports.length; i++ ) {
+ stateUtilsImports.at( i ).remove();
+ }
+
+ // print
+ return root.toSource();
+};
diff --git a/packages/calypso-codemods/transforms/i18n-mixin-to-localize.js b/packages/calypso-codemods/transforms/i18n-mixin-to-localize.js
new file mode 100644
index 0000000000000..d0c356fc01e7f
--- /dev/null
+++ b/packages/calypso-codemods/transforms/i18n-mixin-to-localize.js
@@ -0,0 +1,145 @@
+export default function transformer( file, api ) {
+ const j = api.jscodeshift;
+ const ReactUtils = require( 'react-codemod/transforms/utils/ReactUtils' )( j );
+ const root = j( file.source );
+ let foundMixinUsage = false;
+
+ const createClassesInstances = ReactUtils.findAllReactCreateClassCalls( root );
+
+ // Find the declaration to wrap with the localize HOC. It can be the React.createClass
+ // itself, or an 'export default' or 'module.exports =' declaration, if present.
+ function findDeclarationsToWrap( createClassInstance ) {
+ // Is the created class being assigned to a variable?
+ const parentNode = createClassInstance.parentPath.value;
+ if ( parentNode.type !== 'VariableDeclarator' || parentNode.id.type !== 'Identifier' ) {
+ return j( createClassInstance );
+ }
+
+ // AST matcher for the class identifier
+ const classIdentifier = {
+ type: 'Identifier',
+ name: parentNode.id.name,
+ };
+
+ // AST matcher for the connected class identifier
+ const connectedClassIdentifier = {
+ type: 'CallExpression',
+ callee: {
+ type: 'CallExpression',
+ callee: {
+ type: 'Identifier',
+ name: 'connect',
+ },
+ },
+ arguments: [ classIdentifier ],
+ };
+
+ // AST matcher for the module.exports expression
+ const moduleExportsExpression = {
+ type: 'MemberExpression',
+ object: {
+ type: 'Identifier',
+ name: 'module',
+ },
+ property: {
+ type: 'Identifier',
+ name: 'exports',
+ },
+ };
+
+ // Is the variable later exported with 'export default'?
+ const exportDefaultDeclarations = root.find( j.ExportDefaultDeclaration, {
+ declaration: classIdentifier,
+ } );
+ if ( exportDefaultDeclarations.size() ) {
+ return exportDefaultDeclarations.map( d => d.get( 'declaration' ) );
+ }
+
+ // Is the variable later exported with 'export default connect()'?
+ const exportDefaultConnectDeclarations = root.find( j.ExportDefaultDeclaration, {
+ declaration: connectedClassIdentifier,
+ } );
+ if ( exportDefaultConnectDeclarations.size() ) {
+ return exportDefaultConnectDeclarations.map( d =>
+ d.get( 'declaration' ).get( 'arguments', 0 )
+ );
+ }
+
+ // Is the variable later exported with 'module.exports ='?
+ const moduleExportsDeclarations = root.find( j.AssignmentExpression, {
+ left: moduleExportsExpression,
+ right: classIdentifier,
+ } );
+ if ( moduleExportsDeclarations.size() ) {
+ return moduleExportsDeclarations.map( d => d.get( 'right' ) );
+ }
+
+ // Is the variable later exported with 'module.exports = connect()'?
+ const moduleExportsConnectDeclarations = root.find( j.AssignmentExpression, {
+ left: moduleExportsExpression,
+ right: connectedClassIdentifier,
+ } );
+ if ( moduleExportsConnectDeclarations.size() ) {
+ return moduleExportsConnectDeclarations.map( d => d.get( 'right' ).get( 'arguments', 0 ) );
+ }
+
+ return j( createClassInstance );
+ }
+
+ createClassesInstances.forEach( createClassInstance => {
+ const propertiesToModify = [ 'translate', 'moment', 'numberFormat' ];
+
+ propertiesToModify.forEach( property => {
+ const propertyInstances = j( createClassInstance ).find( j.MemberExpression, {
+ object: { type: 'ThisExpression' },
+ property: {
+ type: 'Identifier',
+ name: property,
+ },
+ } );
+
+ propertyInstances.replaceWith( () =>
+ j.memberExpression(
+ j.memberExpression( j.thisExpression(), j.identifier( 'props' ) ),
+ j.identifier( property )
+ )
+ );
+
+ if ( propertyInstances.size() ) {
+ foundMixinUsage = true;
+ }
+ } );
+
+ if ( foundMixinUsage ) {
+ const declarationsToWrap = findDeclarationsToWrap( createClassInstance );
+ declarationsToWrap.replaceWith( decl => {
+ return j.callExpression( j.identifier( 'localize' ), [ decl.value ] );
+ } );
+ }
+ } );
+
+ if ( foundMixinUsage ) {
+ const i18nCalypsoImports = root.find( j.ImportDeclaration, {
+ source: { value: 'i18n-calypso' },
+ } );
+ if ( i18nCalypsoImports.size() ) {
+ const i18nCalypsoImport = i18nCalypsoImports.get();
+ const localizeImport = j( i18nCalypsoImport ).find( j.ImportSpecifier, {
+ local: {
+ type: 'Identifier',
+ name: 'localize',
+ },
+ } );
+ if ( ! localizeImport.size() ) {
+ i18nCalypsoImport.value.specifiers.push( j.importSpecifier( j.identifier( 'localize' ) ) );
+ }
+ } else {
+ root
+ .find( j.ImportDeclaration )
+ .at( 0 )
+ .insertAfter( 'import { localize } from "i18n-calypso";' );
+ }
+ }
+
+ return root.toSource();
+}
diff --git a/packages/calypso-codemods/transforms/merge-lodash-imports.js b/packages/calypso-codemods/transforms/merge-lodash-imports.js
new file mode 100644
index 0000000000000..a440085fc6718
--- /dev/null
+++ b/packages/calypso-codemods/transforms/merge-lodash-imports.js
@@ -0,0 +1,95 @@
+/**
+ * Merges multiple lodash imports into a single statement
+ *
+ * @example
+ * // input
+ * import { zip } from 'lodash';
+ * import { map } from 'lodash';
+ * import { pick } from 'lodash';
+ *
+ * // output
+ * import { map, pick, zip } from 'lodash'
+ *
+ * @format
+ *
+ * @param file
+ * @param api
+ * @returns {string}
+ */
+
+export default function transformer( file, api ) {
+ const specSorter = ( a, b ) => a.imported.name.localeCompare( b.imported.name );
+
+ const j = api.jscodeshift;
+
+ const source = j( file.source );
+ const lodash = new Set();
+ const decs = [];
+ const maps = new Map();
+
+ const sourceDecs = source.find( j.ImportDeclaration, {
+ source: { value: 'lodash' },
+ } );
+
+ // bail if we only have a single declaration
+ if ( sourceDecs.nodes().length === 1 ) {
+ return file.source;
+ }
+
+ sourceDecs.forEach( dec => {
+ decs.push( dec );
+ j( dec )
+ .find( j.ImportSpecifier )
+ .forEach( spec => {
+ const local = spec.value.local.name;
+ const name = spec.value.imported.name;
+
+ if ( local === name ) {
+ lodash.add( name );
+ } else {
+ maps.set( name, ( maps.get( name ) || new Set() ).add( local ) );
+ }
+ } );
+ } );
+
+ // Insert new statement above first existing lodash import
+ if ( decs.length ) {
+ const newSpecs = Array.from( lodash ).map( name =>
+ j.importSpecifier( j.identifier( name ), j.identifier( name ) )
+ );
+
+ // start adding renamed imports
+ const renames = [];
+ maps.forEach( ( localSet, name ) => {
+ const locals = Array.from( localSet );
+ const hasDefault = lodash.has( name );
+ const topName = hasDefault ? name : locals[ 0 ];
+
+ // add first renamed import if no default
+ // already exists in import statement
+ if ( ! hasDefault ) {
+ locals.shift();
+ newSpecs.push( j.importSpecifier( j.identifier( name ), j.identifier( topName ) ) );
+ }
+
+ // add remaining renames underneath
+ locals.forEach( local => {
+ const rename = j.variableDeclaration( 'const', [
+ j.variableDeclarator( j.identifier( local ), j.identifier( topName ) ),
+ ] );
+
+ renames.push( rename );
+ } );
+ } );
+
+ // sort and insert…
+ const newImport = j.importDeclaration( newSpecs.sort( specSorter ), j.literal( 'lodash' ) );
+ newImport.comments = decs[ 0 ].value.comments;
+ j( decs[ 0 ] ).insertBefore( [ newImport, ...renames ] );
+ }
+
+ // remove old declarations
+ decs.forEach( dec => j( dec ).remove() );
+
+ return source.toSource();
+}
diff --git a/packages/calypso-codemods/transforms/modular-lodash-no-more.js b/packages/calypso-codemods/transforms/modular-lodash-no-more.js
new file mode 100644
index 0000000000000..2e8de9251d208
--- /dev/null
+++ b/packages/calypso-codemods/transforms/modular-lodash-no-more.js
@@ -0,0 +1,45 @@
+/**
+ * Replaces lodash modular imports with ES2015-style imports
+ *
+ * Note: this does not attempt to merge imports into existing
+ * 'lodash' imports in the same module. This process
+ * should be handled in a separate codemod
+ *
+ * @example
+ * // input
+ * import _map from 'lodash/map';
+ *
+ * // output
+ * import { map as _map } from 'lodash'
+ *
+ * @format
+ *
+ * @param file
+ * @param api
+ * @returns {string}
+ */
+
+export default function transformer( file, api ) {
+ const j = api.jscodeshift;
+
+ return j( file.source )
+ .find( j.ImportDeclaration )
+ .filter( dec => dec.value.source.value.startsWith( 'lodash/' ) )
+ .replaceWith( node => {
+ return Object.assign(
+ j.importDeclaration(
+ [
+ j.importSpecifier(
+ j.identifier( node.value.source.value.replace( 'lodash/', '' ) ),
+ j.identifier( node.value.specifiers[ 0 ].local.name )
+ ),
+ ],
+ j.literal( 'lodash' )
+ ),
+ {
+ comments: node.value.comments,
+ }
+ );
+ } )
+ .toSource();
+}
diff --git a/packages/calypso-codemods/transforms/modular-lodash-requires-no-more.js b/packages/calypso-codemods/transforms/modular-lodash-requires-no-more.js
new file mode 100644
index 0000000000000..eeb3b21aaf336
--- /dev/null
+++ b/packages/calypso-codemods/transforms/modular-lodash-requires-no-more.js
@@ -0,0 +1,141 @@
+/**
+ * @format
+ */
+
+let j;
+
+const getImports = source => {
+ const imports = [];
+
+ source.find( j.ImportDeclaration ).forEach( dec => imports.push( dec ) );
+
+ return imports;
+};
+
+const getRequires = source => {
+ const requires = [];
+
+ source
+ .find( j.VariableDeclarator, {
+ init: {
+ type: 'CallExpression',
+ callee: {
+ type: 'Identifier',
+ name: 'require',
+ },
+ },
+ } )
+ .forEach( req => requires.push( req ) );
+
+ return requires;
+};
+
+const getModularLodashDecs = requires =>
+ requires.filter(
+ ( { value: { init } } ) =>
+ init.type === 'CallExpression' &&
+ init.callee.name === 'require' &&
+ init.arguments.length &&
+ init.arguments[ 0 ].value &&
+ init.arguments[ 0 ].value.startsWith( 'lodash/' )
+ );
+
+const makeNewImports = decs => {
+ const imports = [];
+
+ decs.forEach( dec => {
+ const local = dec.value.init.arguments[ 0 ].value.replace( 'lodash/', '' );
+ const name = dec.value.id.name;
+
+ const newImport = j.importDeclaration(
+ [ j.importSpecifier( j.identifier( local ), j.identifier( name ) ) ],
+ j.literal( 'lodash' )
+ );
+
+ imports.push( newImport );
+ } );
+
+ return imports;
+};
+
+const findInsertionPoint = ( imports, requires ) => {
+ if ( ! imports.length ) {
+ const declaration = requires[ 0 ].parentPath.parentPath;
+ const isAnnotated =
+ declaration.value.comments &&
+ declaration.value.comments.length &&
+ declaration.value.comments[ 0 ].value === '*\n * External dependencies\n ';
+
+ return [ declaration, isAnnotated ? 'annotated-requires' : 'no-imports' ];
+ }
+
+ // see if we have an external import
+ const externalAt = imports.findIndex(
+ dec => dec.value.comments && dec.value.comments[ 0 ].value === '*\n * External dependencies\n '
+ );
+
+ if ( externalAt >= 0 ) {
+ const internalAt = imports.findIndex(
+ dec =>
+ dec.value.comments && dec.value.comments[ 0 ].value === '*\n * Internal dependencies\n '
+ );
+
+ // insertion point is at last external import
+ // or just before first internal import
+ return internalAt >= 0
+ ? [ imports[ internalAt ], 'before-internals' ]
+ : [ imports[ imports.length - 1 ], 'no-internals' ];
+ }
+
+ // no externals, so put this at the top
+ return [ imports[ 0 ], 'no-externals' ];
+};
+
+export default function transformer( file, api ) {
+ j = api.jscodeshift;
+ const source = j( file.source );
+
+ const requires = getRequires( source );
+
+ // if we don't have any requires
+ // then don't do anything
+ if ( ! requires.length ) {
+ return file.source;
+ }
+
+ const decs = getModularLodashDecs( requires );
+
+ // if we don't have any modular lodash requires
+ // then don't do anything
+ if ( ! decs.length ) {
+ return file.source;
+ }
+
+ const imports = getImports( source );
+
+ const newImports = makeNewImports( decs );
+
+ const [ insertionPoint, status ] = findInsertionPoint( imports, requires );
+
+ switch ( status ) {
+ case 'no-imports':
+ case 'no-externals':
+ newImports[ 0 ].comments = [ j.commentBlock( '*\n * External dependencies\n ' ) ];
+ j( insertionPoint ).insertBefore( newImports );
+ break;
+ case 'annotated-requires':
+ newImports[ 0 ].comments = insertionPoint.value.comments;
+ insertionPoint.value.comments = [];
+ case 'before-internals':
+ j( insertionPoint ).insertBefore( newImports );
+ break;
+ case 'no-internals':
+ j( insertionPoint ).insertAfter( newImports );
+ break;
+ }
+
+ // remove the old ones
+ j( decs ).remove();
+
+ return source.toSource();
+}
diff --git a/packages/calypso-codemods/transforms/remove-create-reducer.js b/packages/calypso-codemods/transforms/remove-create-reducer.js
new file mode 100644
index 0000000000000..d5850944f6a39
--- /dev/null
+++ b/packages/calypso-codemods/transforms/remove-create-reducer.js
@@ -0,0 +1,262 @@
+function arrowFunctionBodyToCase( j, test, body ) {
+ if ( body.type === 'BlockStatement' ) {
+ return j.switchCase( test, [ body ] );
+ }
+ return j.switchCase( test, [ j.returnStatement( body ) ] );
+}
+
+function getCases( j, handlerMap ) {
+ let hasPersistence = false;
+
+ const cases = handlerMap.properties.map( actionNode => {
+ const test = actionNode.computed
+ ? actionNode.key
+ : j.literal( actionNode.key.name || String( actionNode.key.value ) );
+ const fn = actionNode.value;
+
+ if (
+ test.type === 'Identifier' &&
+ ( test.name === 'SERIALIZE' || test.name === 'DESERIALIZE' )
+ ) {
+ hasPersistence = true;
+ }
+
+ if (
+ test.type === 'Literal' &&
+ ( test.value === 'SERIALIZE' || test.value === 'DESERIALIZE' )
+ ) {
+ hasPersistence = true;
+ }
+
+ // If it's an arrow function without parameters, just return the body.
+ if ( fn.type === 'ArrowFunctionExpression' && fn.params.length === 0 ) {
+ return arrowFunctionBodyToCase( j, test, fn.body );
+ }
+
+ // If it's an arrow function with the right parameter names, just return the body.
+ if (
+ fn.type === 'ArrowFunctionExpression' &&
+ fn.params[ 0 ].name === 'state' &&
+ ( fn.params.length === 1 || ( fn.params.length === 2 && fn.params[ 1 ].name === 'action' ) )
+ ) {
+ return arrowFunctionBodyToCase( j, test, fn.body );
+ }
+
+ // If it's an arrow function with a deconstructed action, do magic.
+ if (
+ fn.type === 'ArrowFunctionExpression' &&
+ fn.params[ 0 ].name === 'state' &&
+ ( fn.params.length === 2 && fn.params[ 1 ].type === 'ObjectPattern' )
+ ) {
+ const declaration = j.variableDeclaration( 'const', [
+ j.variableDeclarator( fn.params[ 1 ], j.identifier( 'action' ) ),
+ ] );
+ const prevBody =
+ fn.body.type === 'BlockStatement' ? fn.body.body : [ j.returnStatement( fn.body ) ];
+ const body = j.blockStatement( [ declaration, ...prevBody ] );
+ return arrowFunctionBodyToCase( j, test, body );
+ }
+
+ return j.switchCase( test, [
+ j.returnStatement(
+ j.callExpression( actionNode.value, [ j.identifier( 'state' ), j.identifier( 'action' ) ] )
+ ),
+ ] );
+ } );
+
+ return { cases, hasPersistence };
+}
+
+function handlePersistence( j, createReducerPath, newNode ) {
+ const parent = createReducerPath.parentPath;
+ const grandParentValue =
+ parent &&
+ parent.parentPath.value &&
+ parent.parentPath.value.length === 1 &&
+ parent.parentPath.value[ 0 ];
+ const greatGrandParent =
+ grandParentValue && parent && parent.parentPath && parent.parentPath.parentPath;
+
+ if (
+ parent &&
+ grandParentValue &&
+ greatGrandParent &&
+ parent.value.type === 'VariableDeclarator' &&
+ grandParentValue.type === 'VariableDeclarator' &&
+ greatGrandParent.value.type === 'VariableDeclaration'
+ ) {
+ const varName = parent.value.id.name;
+ const persistenceNode = j.expressionStatement(
+ j.assignmentExpression(
+ '=',
+ j.memberExpression(
+ j.identifier( varName ),
+ j.identifier( 'hasCustomPersistence' ),
+ false
+ ),
+ j.literal( true )
+ )
+ );
+
+ if ( greatGrandParent.parentPath.value.type === 'ExportNamedDeclaration' ) {
+ // Handle `export const reducer = ...` case.
+ greatGrandParent.parentPath.insertAfter( persistenceNode );
+ } else {
+ // Handle `const reducer = ...` case.
+ greatGrandParent.insertAfter( persistenceNode );
+ }
+ } else if ( parent && parent.value.type === 'AssignmentExpression' ) {
+ const persistenceNode = j.expressionStatement(
+ j.assignmentExpression(
+ '=',
+ j.memberExpression( parent.value.left, j.identifier( 'hasCustomPersistence' ), false ),
+ j.literal( true )
+ )
+ );
+ parent.parentPath.insertAfter( persistenceNode );
+ } else {
+ newNode.comments = newNode.comments || [];
+ newNode.comments.push( j.commentLine( ' TODO: HANDLE PERSISTENCE', true, false ) );
+ }
+
+ return newNode;
+}
+
+export default function transformer( file, api ) {
+ const j = api.jscodeshift;
+
+ const root = j( file.source );
+
+ let usedWithoutPersistence = false;
+
+ // Handle createReducer
+ root
+ .find(
+ j.CallExpression,
+ node => node.callee.type === 'Identifier' && node.callee.name === 'createReducer'
+ )
+ .forEach( createReducerPath => {
+ if ( createReducerPath.value.arguments.length !== 2 ) {
+ throw new Error( 'Unable to translate createReducer' );
+ }
+
+ const [ defaultState, handlerMap ] = createReducerPath.value.arguments;
+
+ const { cases, hasPersistence } = getCases( j, handlerMap );
+
+ let newNode = j.arrowFunctionExpression(
+ [ j.assignmentPattern( j.identifier( 'state' ), defaultState ), j.identifier( 'action' ) ],
+
+ j.blockStatement( [
+ j.switchStatement(
+ j.memberExpression( j.identifier( 'action' ), j.identifier( 'type' ) ),
+ cases
+ ),
+ j.returnStatement( j.identifier( 'state' ) ),
+ ] )
+ );
+
+ if ( hasPersistence ) {
+ newNode = handlePersistence( j, createReducerPath, newNode );
+ } else {
+ usedWithoutPersistence = true;
+ newNode = j.callExpression( j.identifier( 'withoutPersistence' ), [ newNode ] );
+ }
+
+ createReducerPath.replace( newNode );
+ } );
+
+ // Handle createReducerWithValidation
+ root
+ .find(
+ j.CallExpression,
+ node =>
+ node.callee.type === 'Identifier' && node.callee.name === 'createReducerWithValidation'
+ )
+ .forEach( createReducerPath => {
+ if ( createReducerPath.value.arguments.length !== 3 ) {
+ throw new Error( 'Unable to translate createReducerWithValidation' );
+ }
+
+ const [ defaultState, handlerMap, schema ] = createReducerPath.value.arguments;
+
+ const { cases } = getCases( j, handlerMap );
+
+ const newNode = j.callExpression( j.identifier( 'withSchemaValidation' ), [
+ schema,
+ j.arrowFunctionExpression(
+ [
+ j.assignmentPattern( j.identifier( 'state' ), defaultState ),
+ j.identifier( 'action' ),
+ ],
+
+ j.blockStatement( [
+ j.switchStatement(
+ j.memberExpression( j.identifier( 'action' ), j.identifier( 'type' ) ),
+ cases
+ ),
+ j.returnStatement( j.identifier( 'state' ) ),
+ ] )
+ ),
+ ] );
+
+ createReducerPath.replace( newNode );
+ } );
+
+ // Handle imports.
+ root
+ .find(
+ j.ImportDeclaration,
+ node =>
+ node.specifiers &&
+ node.specifiers.some(
+ s =>
+ s &&
+ s.imported &&
+ ( s.imported.name === 'createReducer' ||
+ s.imported.name === 'createReducerWithValidation' )
+ )
+ )
+ .forEach( nodePath => {
+ const filtered = nodePath.value.specifiers.filter(
+ s =>
+ s.imported.name !== 'createReducer' && s.imported.name !== 'createReducerWithValidation'
+ );
+
+ if (
+ nodePath.value.specifiers.find( s => s.imported.name === 'createReducerWithValidation' )
+ ) {
+ if ( ! filtered.find( s => s.imported.name === 'withSchemaValidation' ) ) {
+ filtered.push(
+ j.importSpecifier(
+ j.identifier( 'withSchemaValidation' ),
+ j.identifier( 'withSchemaValidation' )
+ )
+ );
+ }
+ }
+
+ if ( usedWithoutPersistence ) {
+ if ( ! filtered.find( s => s.imported.name === 'withoutPersistence' ) ) {
+ filtered.push(
+ j.importSpecifier(
+ j.identifier( 'withoutPersistence' ),
+ j.identifier( 'withoutPersistence' )
+ )
+ );
+ }
+ }
+
+ if ( filtered.length === 0 ) {
+ const { comments } = nodePath.node;
+ const { parentPath } = nodePath;
+ const nextNode = parentPath.value[ nodePath.name + 1 ];
+ j( nodePath ).remove();
+ nextNode.comments = comments;
+ } else {
+ nodePath.value.specifiers = filtered;
+ }
+ } );
+
+ return root.toSource();
+}
diff --git a/packages/calypso-codemods/transforms/rename-combine-reducers.js b/packages/calypso-codemods/transforms/rename-combine-reducers.js
new file mode 100644
index 0000000000000..f1896b3aecbd0
--- /dev/null
+++ b/packages/calypso-codemods/transforms/rename-combine-reducers.js
@@ -0,0 +1,101 @@
+/**
+ * This codemod updates
+ *
+ * import { combineReducersWithPersistence } from 'state/utils'; to
+ * import { combineReducers } from 'state/utils';
+ *
+ * and updates
+ *
+ * combineReducersWithPersistence( {
+ * foo,
+ * bar
+ * } );
+ *
+ * to
+ *
+ * combineReducers( {
+ * foo,
+ * bar
+ * } );
+ */
+
+module.exports = function( file, api ) {
+ // alias the jscodeshift API
+ const j = api.jscodeshift;
+ // parse JS code into an AST
+ const root = j( file.source );
+
+ const importNames = [];
+
+ const combineReducerImport = root
+ .find( j.ImportDeclaration, {
+ source: {
+ type: 'Literal',
+ value: 'state/utils',
+ },
+ } )
+ .filter( importDeclaration => {
+ if ( importDeclaration.value.specifiers.length > 0 ) {
+ return (
+ importDeclaration.value.specifiers.filter( specifier => {
+ const importedName = specifier.imported.name;
+ const localName = specifier.local.name;
+ const shouldRename = importedName === 'combineReducersWithPersistence';
+ importNames.push( {
+ local: localName === 'combineReducersWithPersistence' ? 'combineReducers' : localName,
+ imported: shouldRename ? 'combineReducers' : importedName,
+ } );
+ return shouldRename;
+ } ).length > 0
+ );
+ }
+ return false;
+ } );
+
+ if ( ! combineReducerImport.length ) {
+ return;
+ }
+
+ //sort by imported name
+ importNames.sort( ( a, b ) => {
+ if ( a.imported < b.imported ) {
+ return -1;
+ }
+ if ( a.imported > b.imported ) {
+ return 1;
+ }
+ return 0;
+ } );
+
+ //save the comment if possible
+ const comments = combineReducerImport.at( 0 ).get().node.comments;
+ const addImport = imports => {
+ const names = imports.map( name => {
+ if ( name.local === name.imported ) {
+ return j.importSpecifier( j.identifier( name.local ) );
+ }
+ if ( name.local !== name.imported ) {
+ return j.importSpecifier( j.identifier( name.imported ), j.identifier( name.local ) );
+ }
+ } );
+ const combinedImport = j.importDeclaration( names, j.literal( 'state/utils' ) );
+ combinedImport.comments = comments;
+ return combinedImport;
+ };
+
+ combineReducerImport.replaceWith( addImport( importNames ) );
+
+ //update combineReducers call
+ const renameIdentifier = newName => imported => {
+ j( imported ).replaceWith( () => j.identifier( newName ) );
+ };
+ const combineReducerIdentifier = root
+ .find( j.CallExpression )
+ .find( j.Identifier )
+ .filter( identifier => identifier.value.name === 'combineReducersWithPersistence' );
+
+ combineReducerIdentifier.forEach( renameIdentifier( 'combineReducers' ) );
+
+ // print
+ return root.toSource();
+};
diff --git a/packages/calypso-codemods/transforms/single-tree-rendering.js b/packages/calypso-codemods/transforms/single-tree-rendering.js
new file mode 100644
index 0000000000000..593519f8a3d72
--- /dev/null
+++ b/packages/calypso-codemods/transforms/single-tree-rendering.js
@@ -0,0 +1,452 @@
+/**
+ * Single Tree Rendering Codemod
+ *
+ * Transforms `ReactDom.render()` to `context.primary/secondary`.
+ *
+ * Transforms `renderWithReduxStore()` to `context.primary/secondary`.
+ *
+ * Adds `context` to params in middlewares when needed
+ *
+ * Adds `next` to params and `next()` to body in middlewares when using
+ * `ReactDom.render()` or `renderWithReduxStore()`.
+ *
+ * Adds `makeLayout` and `clientRender` to `page()` route definitions and
+ * accompanying import statement.
+ *
+ * Removes:
+ * `ReactDom.unmountComponentAtNode( document.getElementById( 'secondary' ) );`
+ *
+ * Removes:
+ * Un-used ReactDom imports.
+ *
+ * Replaces `navigation` middleware with `makeNavigation` when imported
+ * from `my-sites/controller`.
+ */
+
+/**
+ * External dependencies
+ */
+const _ = require( 'lodash' );
+const fs = require( 'fs' );
+const repl = require( 'repl' );
+
+/**
+ * Internal dependencies
+ */
+const config = require( './config' );
+
+export default function transformer( file, api ) {
+ const j = api.jscodeshift;
+ const root = j( file.source );
+
+ /**
+ * Gather all of the external deps and throw them in a set
+ */
+ const nodeJsDeps = repl._builtinLibs;
+ const packageJson = JSON.parse( fs.readFileSync( './package.json', 'utf8' ) );
+ const packageJsonDeps = []
+ .concat( nodeJsDeps )
+ .concat( Object.keys( packageJson.dependencies ) )
+ .concat( Object.keys( packageJson.devDependencies ) );
+
+ const externalDependenciesSet = new Set( packageJsonDeps );
+
+ /**
+ * Is an import external
+ *
+ * @param {object} importNode Node object
+ * @return {boolean} True if import is external
+ */
+ const isExternal = importNode =>
+ externalDependenciesSet.has( importNode.source.value.split( '/' )[ 0 ] );
+
+ /**
+ * Removes the extra newlines between two import statements
+ * caused by `insertAfter()`:
+ * @link https://github.com/benjamn/recast/issues/371
+ *
+ * @param {string} str String
+ * @return {string} Cleaned string
+ */
+ function removeExtraNewlines( str ) {
+ return str.replace( /(import.*\n)\n+(import)/g, '$1$2' );
+ }
+
+ /**
+ * Check if `parameters` has `param` either as a string or as a name of
+ * an object, which could be e.g. an `Identifier`.
+ *
+ * @param {array} params Parameters to look from. Could be an array of strings or Identifier objects.
+ * @param {string} paramValue Parameter value
+ * @return {boolean} True if parameter is present
+ */
+ function hasParam( params = [], paramValue ) {
+ return _.some( params, param => {
+ return ( param.name || param ) === paramValue;
+ } );
+ }
+
+ /**
+ * Removes imports maintaining any comments above them
+ *
+ * @param {object} collection Collection containing at least one node. Comments are preserved only from first node.
+ */
+ function removeImport( collection ) {
+ const node = collection.nodes()[ 0 ];
+
+ // Find out if import had comments above it
+ const comments = _.get( node, 'comments', [] );
+
+ // Remove import (and any comments with it)
+ collection.remove();
+
+ // Put back that removed comment (if any)
+ if ( comments.length ) {
+ const isRemovedExternal = isExternal( node );
+
+ // Find remaining external or internal dependencies and place comments above first one
+ root
+ .find( j.ImportDeclaration )
+ .filter( p => {
+ // Look for only imports that are same type as the removed import was
+ return isExternal( p.value ) === isRemovedExternal;
+ } )
+ .at( 0 )
+ .replaceWith( p => {
+ p.value.comments = p.value.comments ? p.value.comments.concat( comments ) : comments;
+ return p.value;
+ } );
+ }
+ }
+
+ /**
+ * Catch simple redirect middlewares by looking for `page.redirect()`
+ *
+ * @example
+ * // Middleware could look like this:
+ * () => page.redirect('/foo')
+ *
+ * // ...or this:
+ * context => { page.redirect(`/foo/${context.bar}`) }
+ *
+ * // ...or even:
+ * () => {
+ * if (true) {
+ * page.redirect('/foo');
+ * } else {
+ * page.redirect('/bar');
+ * }
+ * }
+ *
+ * @param {object} node AST Node
+ * @return {boolean} True if any `page.redirect()` exist inside the function node, otherwise False
+ */
+ function isRedirectMiddleware( node ) {
+ return (
+ j( node )
+ .find( j.MemberExpression, {
+ object: {
+ type: 'Identifier',
+ name: 'page',
+ },
+ property: {
+ type: 'Identifier',
+ name: 'redirect',
+ },
+ } )
+ .size() > 0
+ );
+ }
+
+ /**
+ * Ensure `context` is among params
+ *
+ * @param {object} path Path object that wraps a single node
+ * @returns {object} Single node object
+ */
+ function ensureContextMiddleware( path ) {
+ // `context` param is already in
+ if ( hasParam( path.value.params, 'context' ) ) {
+ return path.value;
+ }
+ const ret = path.value;
+ ret.params = [ j.identifier( 'context' ), ...ret.params ];
+
+ return ret;
+ }
+
+ /**
+ * Ensure `next` is among params and `next()` is in the block's body
+ *
+ * @param {object} path Path object that wraps a single node
+ * @returns {object} Single node object
+ */
+ function ensureNextMiddleware( path ) {
+ // `next` param is already in
+ if ( hasParam( path.value.params, 'next' ) ) {
+ return path.value;
+ }
+ if ( path.value.params.length > 1 ) {
+ // More than just a context arg, possibly not a middleware
+ return path.value;
+ }
+ const ret = path.value;
+ ret.params = [ ...ret.params, j.identifier( 'next' ) ];
+ ret.body = j.blockStatement( [
+ ...path.value.body.body,
+ j.expressionStatement( j.callExpression( j.identifier( 'next' ), [] ) ),
+ ] );
+
+ return ret;
+ }
+
+ function getTarget( arg ) {
+ if ( arg.type === 'Literal' ) {
+ return arg.value;
+ }
+ if ( arg.type === 'CallExpression' ) {
+ // More checks?
+ return arg.arguments[ 0 ].value;
+ }
+ }
+
+ /**
+ * Transform `renderWithReduxStore()` CallExpressions.
+ *
+ * @example
+ * Input
+ * ```
+ * renderWithReduxStore(
+ * ,
+ * 'primary',
+ * context.store
+ * );
+ * ```
+ *
+ * Output:
+ * ```
+ * context.primary = ;
+ * ```
+ *
+ * @param {object} path Path object that wraps a single node
+ * @returns {object} Single node object
+ */
+ function transformRenderWithReduxStore( path ) {
+ const expressionCallee = {
+ name: 'renderWithReduxStore',
+ };
+
+ return transformToContextLayout( path, expressionCallee );
+ }
+
+ /**
+ * Transform `ReactDom.render()` CallExpressions.
+ *
+ * @example
+ * Input
+ * ```
+ * ReactDom.render(
+ * ,
+ * document.getElementById( 'primary' )
+ * );
+ * ```
+ *
+ * Output:
+ * ```
+ * context.primary = ;
+ * ```
+ *
+ * @param {object} path Path object that wraps a single node
+ * @returns {object} Single node object
+ */
+ function transformReactDomRender( path ) {
+ const expressionCallee = {
+ type: 'MemberExpression',
+ object: {
+ name: 'ReactDom',
+ },
+ property: {
+ name: 'render',
+ },
+ };
+
+ return transformToContextLayout( path, expressionCallee );
+ }
+
+ /**
+ * Transform CallExpressions.
+ * What kind of CallExpressions this replaces depends on `expressionCallee`
+ * parameter.
+ *
+ * @example
+ * Input
+ * ```
+ * DefinedByExpressionCallee(
+ * ,
+ * document.getElementById( 'primary' )
+ * );
+ * ```
+ *
+ * Output:
+ * ```
+ * context.primary = ;
+ * ```
+ *
+ * @param {object} path Path object that wraps a single node
+ * @param {object} expressionCallee `callee` parameter for finding `CallExpression` nodes.
+ * @returns {object} Single node object
+ */
+ function transformToContextLayout( path, expressionCallee ) {
+ if ( path.value.params.length !== 2 ) {
+ // More than just context and next args, possibly not a middleware
+ return path.value;
+ }
+ return j( path )
+ .find( j.CallExpression, {
+ callee: expressionCallee,
+ } )
+ .replaceWith( p => {
+ const contextArg = path.value.params[ 0 ];
+ const target = getTarget( p.value.arguments[ 1 ] );
+ return j.assignmentExpression(
+ '=',
+ j.memberExpression( contextArg, j.identifier( target ) ),
+ p.value.arguments[ 0 ]
+ );
+ } );
+ }
+
+ // Transform `ReactDom.render()` to `context.primary/secondary`
+ root
+ .find( j.CallExpression, {
+ callee: {
+ type: 'MemberExpression',
+ object: {
+ name: 'ReactDom',
+ },
+ property: {
+ name: 'render',
+ },
+ },
+ } )
+ .closest( j.Function )
+ .replaceWith( ensureContextMiddleware )
+ // Receives already transformed object from `replaceWith()` above
+ .replaceWith( ensureNextMiddleware )
+ .forEach( transformReactDomRender );
+
+ // Transform `renderWithReduxStore()` to `context.primary/secondary`
+ root
+ .find( j.CallExpression, {
+ callee: {
+ name: 'renderWithReduxStore',
+ },
+ } )
+ .closestScope()
+ .replaceWith( ensureNextMiddleware )
+ .forEach( transformRenderWithReduxStore );
+
+ // Remove `renderWithReduxStore` from `import { a, renderWithReduxStore, b } from 'lib/react-helpers'`
+ root
+ .find( j.ImportSpecifier, {
+ local: {
+ name: 'renderWithReduxStore',
+ },
+ } )
+ .remove();
+
+ // Find empty `import 'lib/react-helpers'`
+ const orphanImportHelpers = root
+ .find( j.ImportDeclaration, {
+ source: {
+ value: 'lib/react-helpers',
+ },
+ } )
+ .filter( p => ! p.value.specifiers.length );
+
+ if ( orphanImportHelpers.size() ) {
+ removeImport( orphanImportHelpers );
+ }
+
+ /**
+ * Removes:
+ * ```
+ * ReactDom.unmountComponentAtNode( document.getElementById( 'secondary' ) );
+ * ```
+ */
+ root
+ .find( j.CallExpression, {
+ callee: {
+ type: 'MemberExpression',
+ object: {
+ name: 'ReactDom',
+ },
+ property: {
+ name: 'unmountComponentAtNode',
+ },
+ },
+ } )
+ // Ensures we remove only nodes containing `document.getElementById( 'secondary' )`
+ .filter( p => _.get( p, 'value.arguments[0].arguments[0].value' ) === 'secondary' )
+ .remove();
+
+ // Find if `ReactDom` is used
+ const reactDomDefs = root.find( j.MemberExpression, {
+ object: {
+ name: 'ReactDom',
+ },
+ } );
+
+ // Remove stranded `react-dom` imports
+ if ( ! reactDomDefs.size() ) {
+ const importReactDom = root.find( j.ImportDeclaration, {
+ specifiers: [
+ {
+ local: {
+ name: 'ReactDom',
+ },
+ },
+ ],
+ source: {
+ value: 'react-dom',
+ },
+ } );
+
+ if ( importReactDom.size() ) {
+ removeImport( importReactDom );
+ }
+ }
+
+ // Add makeLayout and clientRender middlewares to route definitions
+ const routeDefs = root
+ .find( j.CallExpression, {
+ callee: {
+ name: 'page',
+ },
+ } )
+ .filter( p => {
+ const lastArgument = _.last( p.value.arguments );
+
+ return (
+ p.value.arguments.length > 1 &&
+ p.value.arguments[ 0 ].value !== '*' &&
+ [ 'Identifier', 'MemberExpression', 'CallExpression' ].indexOf( lastArgument.type ) > -1 &&
+ ! isRedirectMiddleware( lastArgument )
+ );
+ } )
+ .forEach( p => {
+ p.value.arguments.push( j.identifier( 'makeLayout' ) );
+ p.value.arguments.push( j.identifier( 'clientRender' ) );
+ } );
+
+ if ( routeDefs.size() ) {
+ root
+ .find( j.ImportDeclaration )
+ .at( -1 )
+ .insertAfter( "import { makeLayout, render as clientRender } from 'controller';" );
+ }
+
+ const source = root.toSource( config.recastOptions );
+
+ return routeDefs.size() ? removeExtraNewlines( source ) : source;
+}
diff --git a/packages/calypso-codemods/transforms/sort-imports.js b/packages/calypso-codemods/transforms/sort-imports.js
new file mode 100644
index 0000000000000..bd1344dfb11a0
--- /dev/null
+++ b/packages/calypso-codemods/transforms/sort-imports.js
@@ -0,0 +1,153 @@
+/**
+ * This codeshift takes all of the imports for a file, and organizes them into two sections:
+ * External dependencies and Internal Dependencies.
+ *
+ * It is smart enough to retain whether or not a docblock should keep a prettier/formatter pragma
+ */
+
+/**
+ * External dependencies
+ */
+const fs = require( 'fs' );
+const path = require( 'path' );
+const _ = require( 'lodash' );
+const nodeJsDeps = require( 'repl' )._builtinLibs;
+
+function findPkgJson( target ) {
+ let root = path.dirname( target );
+ while ( root !== '/' ) {
+ const filepath = path.join( root, 'package.json' );
+ if ( fs.existsSync( filepath ) ) {
+ return JSON.parse( fs.readFileSync( filepath, 'utf8' ) );
+ }
+ root = path.join( root, '../' );
+ }
+ throw new Error( 'could not find a pkg json' );
+}
+
+/**
+ * Gather all of the external deps and throw them in a set
+ */
+const getPackageJsonDeps = ( function() {
+ let packageJsonDeps;
+
+ return root => {
+ if ( packageJsonDeps ) {
+ return packageJsonDeps;
+ }
+
+ const json = findPkgJson( root );
+ packageJsonDeps = []
+ .concat( nodeJsDeps )
+ .concat( json.dependencies ? Object.keys( json.dependencies ) : [] )
+ .concat( json.devDependencies ? Object.keys( json.devDependencies ) : [] );
+
+ return new Set( packageJsonDeps );
+ };
+} )();
+
+const externalBlock = {
+ type: 'Block',
+ value: '*\n * External dependencies\n ',
+};
+const internalBlock = {
+ type: 'Block',
+ value: '*\n * Internal dependencies\n ',
+};
+
+/**
+ * Returns true if the given text contains @format.
+ * within its first docblock. False otherwise.
+ *
+ * @param {string} text text to scan for the format keyword within the first docblock
+ * @return {boolean} True if @format is found, otherwise false
+ */
+const shouldFormat = text => {
+ const firstDocBlockStartIndex = text.indexOf( '/**' );
+
+ if ( -1 === firstDocBlockStartIndex ) {
+ return false;
+ }
+
+ const firstDocBlockEndIndex = text.indexOf( '*/', firstDocBlockStartIndex + 1 );
+
+ if ( -1 === firstDocBlockEndIndex ) {
+ return false;
+ }
+
+ const firstDocBlockText = text.substring( firstDocBlockStartIndex, firstDocBlockEndIndex + 1 );
+ return firstDocBlockText.indexOf( '@format' ) >= 0;
+};
+
+/**
+ * Removes the extra newlines between two import statements
+ *
+ * @param {string} str Input string
+ * @return {string} Transformed string
+ */
+const removeExtraNewlines = str => str.replace( /(import.*\n)\n+(import)/g, '$1$2' );
+
+/**
+ * Adds a newline in between the last import of external deps + the internal deps docblock
+ *
+ * @param {string} str Input string
+ * @return {string} Transformed string
+ */
+const addNewlineBeforeDocBlock = str => str.replace( /(import.*\n)(\/\*\*)/, '$1\n$2' );
+
+/**
+ *
+ * @param {Array} importNodes the import nodes to sort
+ * @returns {Array} the sorted set of import nodes
+ */
+const sortImports = importNodes => _.sortBy( importNodes, node => node.source.value );
+
+module.exports = function( file, api ) {
+ const j = api.jscodeshift;
+ const src = j( file.source );
+ const includeFormatBlock = shouldFormat( src.toSource().toString() );
+ const declarations = src.find( j.ImportDeclaration );
+
+ // this is dependent on the projects package.json file which is why its initialized so late
+ // we recursively search up from the file.path to figure out the location of the package.json file
+ const externalDependenciesSet = getPackageJsonDeps( file.path );
+ const isExternal = importNode =>
+ externalDependenciesSet.has( importNode.source.value.split( '/' )[ 0 ] );
+
+ // if there are no deps at all, then return early.
+ if ( _.isEmpty( declarations.nodes() ) ) {
+ return file.source;
+ }
+
+ const withoutComments = declarations.nodes().map( node => {
+ node.comments = '';
+ return node;
+ } );
+
+ const externalDeps = sortImports( withoutComments.filter( node => isExternal( node ) ) );
+ const internalDeps = sortImports( withoutComments.filter( node => ! isExternal( node ) ) );
+
+ if ( externalDeps[ 0 ] ) {
+ externalDeps[ 0 ].comments = [ externalBlock ];
+ }
+ if ( internalDeps[ 0 ] ) {
+ internalDeps[ 0 ].comments = [ internalBlock ];
+ }
+
+ const newDeclarations = []
+ .concat( includeFormatBlock && '/** @format */' )
+ .concat( externalDeps )
+ .concat( internalDeps );
+
+ let isFirst = true;
+ /* remove all imports and insert the new ones in the first imports place */
+ declarations.replaceWith( () => {
+ if ( isFirst ) {
+ isFirst = false;
+ return newDeclarations;
+ }
+ return;
+ } );
+
+ return addNewlineBeforeDocBlock( removeExtraNewlines( src.toSource() ) );
+};