From 803776658313bc2b825c6a4a2c2c48dc94d68f7c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 14 Nov 2019 20:12:20 +0100 Subject: [PATCH] Calypso Codemods: Pull in to monorepo (#36645) * first commit * copy files from wp-calypso * Run script + setup test harness * Make index.js a binary * add travis config and tweak README * add section to readme explaining how to add new tests * prettify + remove all references to config * add tests: merge-lodash-imports * tests: add modular-lodash-no-more * tests: add combine-reducer-with-persistence * tests: add combine-state-utils-imports * tests: add i18n-mixin-to-localize * tests: add modular-lodash-requires-no-more * i18n-mixin-to-localize: must use dbl quotes * bump package for release * Fix build for npm * should be able to run from anywhere * fix broken deps so tests can pass. do not use yarn * no yarn * add single tree rendering * Update dependencies to latests versions (#4) * Update dependencies to latests versions Updated deps, hopefully to get rid of GitHub's security warnings about `hoek`. The update also updated `package.json` to NPM 6 format. * Tell Travis CI to use Node 10 * Fix codemod name in documentation (#5) * Merge pull request #8 from Automattic/add/remove-create-reducer Add codemod to remove create-reducer * Prep for calypso * npm install --- .eslintignore | 1 + package-lock.json | 680 ++++++++++++++++++ package.json | 1 + packages/calypso-codemods/.eslintrc.js | 6 + packages/calypso-codemods/README.md | 113 +++ packages/calypso-codemods/api.js | 84 +++ packages/calypso-codemods/config.js | 72 ++ packages/calypso-codemods/index.js | 31 + packages/calypso-codemods/jest.config.js | 6 + packages/calypso-codemods/package.json | 29 + packages/calypso-codemods/setup-tests.js | 20 + .../__snapshots__/codemod.spec.js.snap | 12 + .../combine-reducer-with-persistence/basic.js | 6 + .../codemod.spec.js | 1 + .../__snapshots__/codemod.spec.js.snap | 7 + .../combine-state-utils-imports/basic.js | 2 + .../codemod.spec.js | 1 + .../__snapshots__/codemod.spec.js.snap | 17 + .../tests/i18n-mixin-to-localize/basic.js | 9 + .../i18n-mixin-to-localize/codemod.spec.js | 1 + .../__snapshots__/codemod.spec.js.snap | 15 + .../tests/merge-lodash-imports/basic.js | 4 + .../merge-lodash-imports/codemod.spec.js | 1 + .../comments-in-between.js | 9 + .../__snapshots__/codemod.spec.js.snap | 8 + .../tests/modular-lodash-no-more/basic.js | 2 + .../modular-lodash-no-more/codemod.spec.js | 1 + .../__snapshots__/codemod.spec.js.snap | 15 + .../modular-lodash-requires-no-more/basic.js | 3 + .../codemod.spec.js | 1 + .../comments-in-between.js | 9 + .../__snapshots__/codemod.spec.js.snap | 105 +++ .../remove-create-reducer/codemod.spec.js | 1 + .../remove-create-reducer/create-reducer.js | 52 ++ .../__snapshots__/codemod.spec.js.snap | 12 + .../tests/rename-combine-reducers/basic.js | 6 + .../rename-combine-reducers/codemod.spec.js | 1 + .../__snapshots__/codemod.spec.js.snap | 50 ++ .../tests/sort-imports/codemod.spec.js | 1 + .../tests/sort-imports/incorrect-docblocks.js | 12 + .../tests/sort-imports/only-imports.js | 3 + .../tests/sort-imports/with-body.js | 5 + .../combine-reducer-with-persistence.js | 54 ++ .../transforms/combine-state-utils-imports.js | 75 ++ .../transforms/i18n-mixin-to-localize.js | 145 ++++ .../transforms/merge-lodash-imports.js | 95 +++ .../transforms/modular-lodash-no-more.js | 45 ++ .../modular-lodash-requires-no-more.js | 141 ++++ .../transforms/remove-create-reducer.js | 262 +++++++ .../transforms/rename-combine-reducers.js | 101 +++ .../transforms/single-tree-rendering.js | 452 ++++++++++++ .../transforms/sort-imports.js | 153 ++++ 52 files changed, 2938 insertions(+) create mode 100644 packages/calypso-codemods/.eslintrc.js create mode 100644 packages/calypso-codemods/README.md create mode 100644 packages/calypso-codemods/api.js create mode 100644 packages/calypso-codemods/config.js create mode 100755 packages/calypso-codemods/index.js create mode 100644 packages/calypso-codemods/jest.config.js create mode 100644 packages/calypso-codemods/package.json create mode 100644 packages/calypso-codemods/setup-tests.js create mode 100644 packages/calypso-codemods/tests/combine-reducer-with-persistence/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/combine-reducer-with-persistence/basic.js create mode 100644 packages/calypso-codemods/tests/combine-reducer-with-persistence/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/combine-state-utils-imports/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/combine-state-utils-imports/basic.js create mode 100644 packages/calypso-codemods/tests/combine-state-utils-imports/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/i18n-mixin-to-localize/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/i18n-mixin-to-localize/basic.js create mode 100644 packages/calypso-codemods/tests/i18n-mixin-to-localize/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/merge-lodash-imports/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/merge-lodash-imports/basic.js create mode 100644 packages/calypso-codemods/tests/merge-lodash-imports/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/merge-lodash-imports/comments-in-between.js create mode 100644 packages/calypso-codemods/tests/modular-lodash-no-more/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/modular-lodash-no-more/basic.js create mode 100644 packages/calypso-codemods/tests/modular-lodash-no-more/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/modular-lodash-requires-no-more/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/modular-lodash-requires-no-more/basic.js create mode 100644 packages/calypso-codemods/tests/modular-lodash-requires-no-more/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/modular-lodash-requires-no-more/comments-in-between.js create mode 100644 packages/calypso-codemods/tests/remove-create-reducer/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/remove-create-reducer/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/remove-create-reducer/create-reducer.js create mode 100644 packages/calypso-codemods/tests/rename-combine-reducers/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/rename-combine-reducers/basic.js create mode 100644 packages/calypso-codemods/tests/rename-combine-reducers/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/sort-imports/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/sort-imports/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/sort-imports/incorrect-docblocks.js create mode 100644 packages/calypso-codemods/tests/sort-imports/only-imports.js create mode 100644 packages/calypso-codemods/tests/sort-imports/with-body.js create mode 100644 packages/calypso-codemods/transforms/combine-reducer-with-persistence.js create mode 100644 packages/calypso-codemods/transforms/combine-state-utils-imports.js create mode 100644 packages/calypso-codemods/transforms/i18n-mixin-to-localize.js create mode 100644 packages/calypso-codemods/transforms/merge-lodash-imports.js create mode 100644 packages/calypso-codemods/transforms/modular-lodash-no-more.js create mode 100644 packages/calypso-codemods/transforms/modular-lodash-requires-no-more.js create mode 100644 packages/calypso-codemods/transforms/remove-create-reducer.js create mode 100644 packages/calypso-codemods/transforms/rename-combine-reducers.js create mode 100644 packages/calypso-codemods/transforms/single-tree-rendering.js create mode 100644 packages/calypso-codemods/transforms/sort-imports.js 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() ) ); +};