From 0c6c9abe7a6f09c211d371d0fafb9f5f9ff2737e Mon Sep 17 00:00:00 2001 From: martimalek <46452321+martimalek@users.noreply.github.com> Date: Tue, 29 Aug 2023 17:44:51 +0200 Subject: [PATCH] Create codemod to migrate colors to css variables (#363) * chore: bump jscodeshift to 0.15.0 * test: cover block property transform * feat: replace colors in template string for css vars * fix: adapt codemod to cover templates with an even number of quasis * chore: add single quote config option to block codemod * build: include codemods in build * docs: add new codemod info in migrating docs * docs: add semantic tokens recommendation in migration docs * feat: pass print options to block codemod --- .eslintignore | 2 + codemods/v2.ts | 68 ---- docs/migrating.storybook.mdx | 41 ++- package-lock.json | 334 +----------------- package.json | 5 +- .../block-to-width-100.input.tsx | 8 + .../block-to-width-100.output.tsx | 8 + .../color-in-JSX-multi-import.input.tsx | 5 + .../color-in-JSX-multi-import.output.tsx | 5 + .../color-in-JSX-single-import.input.tsx | 9 + .../color-in-JSX-single-import.output.tsx | 7 + .../template-multi-quasis-even.input.tsx | 25 ++ .../template-multi-quasis-even.output.tsx | 25 ++ .../template-multi-quasis-odd.input.tsx | 28 ++ .../template-multi-quasis-odd.output.tsx | 28 ++ .../template-single-quasis.input.tsx | 6 + .../template-single-quasis.output.tsx | 5 + .../__tests__/block-to-width-100-test.ts | 6 + .../__tests__/colors-to-css-vars-test.ts | 18 + src/codemods/block-to-width-100.ts | 59 ++++ src/codemods/colors-to-css-vars.ts | 159 +++++++++ tsconfig.json | 2 +- 22 files changed, 451 insertions(+), 402 deletions(-) delete mode 100644 codemods/v2.ts create mode 100644 src/codemods/__testfixtures__/block-to-width-100.input.tsx create mode 100644 src/codemods/__testfixtures__/block-to-width-100.output.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-multi-import.input.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-multi-import.output.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-single-import.input.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-single-import.output.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-even.input.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-even.output.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-odd.input.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-odd.output.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/template-single-quasis.input.tsx create mode 100644 src/codemods/__testfixtures__/colors-to-css-vars/template-single-quasis.output.tsx create mode 100644 src/codemods/__tests__/block-to-width-100-test.ts create mode 100644 src/codemods/__tests__/colors-to-css-vars-test.ts create mode 100644 src/codemods/block-to-width-100.ts create mode 100644 src/codemods/colors-to-css-vars.ts diff --git a/.eslintignore b/.eslintignore index 1cd74997d..dfccc9844 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,3 +12,5 @@ assets/ fixtures/ src/icons/ + +src/codemods/ \ No newline at end of file diff --git a/codemods/v2.ts b/codemods/v2.ts deleted file mode 100644 index 145fb5193..000000000 --- a/codemods/v2.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { API, Collection, FileInfo, JSCodeshift } from 'jscodeshift'; - -function transformBlockProperty(j: JSCodeshift, ast: Collection) { - const localButtonNames = []; - - const fullWidthProp = { - type: 'JSXAttribute', - name: 'width', - value: { - type: 'StringLiteral', - value: '100%' - } - }; - - ast - .find(j.ImportDeclaration, decl => decl.source.value === '@freenow/wave') - .forEach(decl => { - j(decl) - .find(j.ImportSpecifier) - .forEach(spec => { - if (spec.node.imported.name === 'Button' || spec.node.imported.name === 'TextButton') { - localButtonNames.push(spec.node.local.name); - } - }); - }); - - ast - .find(j.JSXElement, { - openingElement: { - name: { - name: name => { - return localButtonNames.includes(name); - } - } - } - }) - .forEach(el => { - j(el) - .find(j.JSXAttribute, { - name: name => { - return name.name === 'block'; - } - }) - .forEach(attr => { - if (attr.value.value) { - j(attr).find(j.Literal).forEach((literal) => { - if (literal.value.value === false) { - j(attr).remove(); - } else { - j(attr).replaceWith(fullWidthProp); - } - }); - } else { - j(attr).replaceWith(fullWidthProp); - } - }); - }); -} - - -module.exports = (file: FileInfo, api: API) => { - const j = api.jscodeshift; - const ast = j(file.source); - - transformBlockProperty(j, ast) - - return ast.toSource() -}; diff --git a/docs/migrating.storybook.mdx b/docs/migrating.storybook.mdx index 3c267c3d2..ced28dba1 100644 --- a/docs/migrating.storybook.mdx +++ b/docs/migrating.storybook.mdx @@ -4,20 +4,7 @@ import { Meta } from '@storybook/blocks'; # Migrating -Migrating from one major version to the next with ease. - -## From v1 to v2 - -A codemod exists to upgrade your components for this version. - -```bash -npx jscodeshift -t node_modules/@freenow/wave/codemods/v2.ts path/to/src -``` - -### Button and TextButton - -The `block` property used to act as a shortcut to give the button components a width of 100%. In the future, use the `width` property -directly. It uses the styled-system variable, which is a lot more flexible than just the boolean flag. +Migrate to the next Wave major version with ease. ## Codemods @@ -28,3 +15,29 @@ previous major versions first. Beware that codemods are not 100% fool-proof and If you're working in a typescript environment, you need to add `--parser=tsx` to the command in each codemod command to allow jscodeshift to parse your typescript/jsx files. + +## From v1 to v2 + +Two codemods exist to upgrade your components for version 2. In order to apply the transformations in your project you simply need to run the +command for your desired codemod pointing to your project's source path. + +### Button and TextButton block property + +The `block` property used to act as a shortcut to give the button components a width of 100%. In the future, use the `width` property +directly. It uses the styled-system variable, which is a lot more flexible than just the boolean flag. + +```bash +npx jscodeshift -t node_modules/@freenow/wave/lib/cjs/codemods/block-to-width-100.js path/to/src +``` + +### Colors to CSS variables + +Our theme colors structure has changed significantly in this major. In order to use Wave colors in your project you should now use the CSS variables +that our theme brings. + +```bash +npx jscodeshift -t node_modules/@freenow/wave/lib/cjs/codemods/colors-to-css-vars.js path/to/src +``` + +Disclaimer: This codemod transforms usages of `Colors` to our bare colors CSS variables to ensure we don't introduce breaking changes, that being said, +we recommend using semantic tokens instead as a best practice and offer a `getSemanticValue` API for just that. diff --git a/package-lock.json b/package-lock.json index ce4be7140..586c44f0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "@types/babel__traverse": "7.17.1", "@types/jest": "^25.1.4", "@types/jest-axe": "^3.2.1", - "@types/jscodeshift": "^0.11.1", + "@types/jscodeshift": "^0.11.6", "@types/node": "^17.0.41", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -76,7 +76,7 @@ "jest-axe": "^3.4.0", "jest-date-mock": "^1.0.8", "jest-styled-components": "^7.0.2", - "jscodeshift": "^0.13.0", + "jscodeshift": "^0.15.0", "minimatch": "^3.0.4", "prettier": "^2.0.2", "pretty-quick": "^2.0.1", @@ -22451,9 +22451,9 @@ } }, "node_modules/jscodeshift": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.13.1.tgz", - "integrity": "sha512-lGyiEbGOvmMRKgWk4vf+lUrCWO/8YR8sUR3FKF1Cq5fovjZDlIcw3Hu5ppLHAnEXshVffvaM0eyuY/AbOeYpnQ==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.0.tgz", + "integrity": "sha512-t337Wx7Vy1ffhas7E1KZUHaR9YPdeCfxPvxz9k6DKwYW88pcs1piR1eR9d+7GQZGSQIZd6a+cfIM3XpMe9rFKQ==", "dev": true, "dependencies": { "@babel/core": "^7.13.16", @@ -22469,10 +22469,10 @@ "chalk": "^4.1.2", "flow-parser": "0.*", "graceful-fs": "^4.2.4", - "micromatch": "^3.1.10", + "micromatch": "^4.0.4", "neo-async": "^2.5.0", "node-dir": "^0.1.17", - "recast": "^0.20.4", + "recast": "^0.23.1", "temp": "^0.8.4", "write-file-atomic": "^2.3.0" }, @@ -22481,6 +22481,11 @@ }, "peerDependencies": { "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } } }, "node_modules/jscodeshift/node_modules/ansi-styles": { @@ -22498,27 +22503,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jscodeshift/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jscodeshift/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -22553,21 +22537,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/jscodeshift/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jscodeshift/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -22577,118 +22546,6 @@ "node": ">=8" } }, - "node_modules/jscodeshift/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/jscodeshift/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/recast": { - "version": "0.20.5", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.20.5.tgz", - "integrity": "sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==", - "dev": true, - "dependencies": { - "ast-types": "0.14.2", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/jscodeshift/node_modules/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, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jscodeshift/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -22701,25 +22558,6 @@ "node": ">=8" } }, - "node_modules/jscodeshift/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, "node_modules/jscodeshift/node_modules/write-file-atomic": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", @@ -50929,9 +50767,9 @@ } }, "jscodeshift": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.13.1.tgz", - "integrity": "sha512-lGyiEbGOvmMRKgWk4vf+lUrCWO/8YR8sUR3FKF1Cq5fovjZDlIcw3Hu5ppLHAnEXshVffvaM0eyuY/AbOeYpnQ==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.0.tgz", + "integrity": "sha512-t337Wx7Vy1ffhas7E1KZUHaR9YPdeCfxPvxz9k6DKwYW88pcs1piR1eR9d+7GQZGSQIZd6a+cfIM3XpMe9rFKQ==", "dev": true, "requires": { "@babel/core": "^7.13.16", @@ -50947,10 +50785,10 @@ "chalk": "^4.1.2", "flow-parser": "0.*", "graceful-fs": "^4.2.4", - "micromatch": "^3.1.10", + "micromatch": "^4.0.4", "neo-async": "^2.5.0", "node-dir": "^0.1.17", - "recast": "^0.20.4", + "recast": "^0.23.1", "temp": "^0.8.4", "write-file-atomic": "^2.3.0" }, @@ -50964,24 +50802,6 @@ "color-convert": "^2.0.1" } }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -51007,116 +50827,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - } - } - }, - "recast": { - "version": "0.20.5", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.20.5.tgz", - "integrity": "sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==", - "dev": true, - "requires": { - "ast-types": "0.14.2", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tslib": "^2.0.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 - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -51126,22 +50842,6 @@ "has-flag": "^4.0.0" } }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, "write-file-atomic": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", diff --git a/package.json b/package.json index 053b09f27..964234075 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test": "jest", "test:fixture": "./scripts/run_fixtures.sh", "test:watch": "jest --watch", + "test:codemods": "jest codemods", "lint": "npx concurrently \"npm run lint:eslint\" \"npm run lint:stylelint\"", "lint:stylelint": "stylelint --config .stylelintrc 'src/**/*.ts?(x)'", "lint:eslint": "eslint --format pretty --ext .js,.jsx,.ts,.tsx src/", @@ -79,7 +80,7 @@ "@types/babel__traverse": "7.17.1", "@types/jest": "^25.1.4", "@types/jest-axe": "^3.2.1", - "@types/jscodeshift": "^0.11.1", + "@types/jscodeshift": "^0.11.6", "@types/node": "^17.0.41", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -105,7 +106,7 @@ "jest-axe": "^3.4.0", "jest-date-mock": "^1.0.8", "jest-styled-components": "^7.0.2", - "jscodeshift": "^0.13.0", + "jscodeshift": "^0.15.0", "minimatch": "^3.0.4", "prettier": "^2.0.2", "pretty-quick": "^2.0.1", diff --git a/src/codemods/__testfixtures__/block-to-width-100.input.tsx b/src/codemods/__testfixtures__/block-to-width-100.input.tsx new file mode 100644 index 000000000..88d862eb7 --- /dev/null +++ b/src/codemods/__testfixtures__/block-to-width-100.input.tsx @@ -0,0 +1,8 @@ +import { Button } from '@freenow/wave'; +import React from 'react'; + +export const ButtonTest = (): JSX.Element => ( + +); diff --git a/src/codemods/__testfixtures__/block-to-width-100.output.tsx b/src/codemods/__testfixtures__/block-to-width-100.output.tsx new file mode 100644 index 000000000..c1833e9b6 --- /dev/null +++ b/src/codemods/__testfixtures__/block-to-width-100.output.tsx @@ -0,0 +1,8 @@ +import { Button } from '@freenow/wave'; +import React from 'react'; + +export const ButtonTest = (): JSX.Element => ( + +); diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-multi-import.input.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-multi-import.input.tsx new file mode 100644 index 000000000..6cf25eab2 --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-multi-import.input.tsx @@ -0,0 +1,5 @@ +import { CloseIcon, Colors } from '@freenow/wave'; + +export const CloseIconWrapper = () => ( + +); diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-multi-import.output.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-multi-import.output.tsx new file mode 100644 index 000000000..147508193 --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-multi-import.output.tsx @@ -0,0 +1,5 @@ +import { CloseIcon } from '@freenow/wave'; + +export const CloseIconWrapper = () => ( + +); diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-single-import.input.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-single-import.input.tsx new file mode 100644 index 000000000..aafd4aa12 --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-single-import.input.tsx @@ -0,0 +1,9 @@ +import { Colors } from '@freenow/wave'; + +export const GhostIcon = () => ( + + + + + +); diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-single-import.output.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-single-import.output.tsx new file mode 100644 index 000000000..b66b7cc48 --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/color-in-JSX-single-import.output.tsx @@ -0,0 +1,7 @@ +export const GhostIcon = () => ( + + + + + +); diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-even.input.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-even.input.tsx new file mode 100644 index 000000000..019f79ab1 --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-even.input.tsx @@ -0,0 +1,25 @@ +import { Button, Colors, Spaces } from '@freenow/wave'; +import styled from 'styled-components'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +const Action = styled(Button)` + justify-content: flex-start; + + color: ${Colors.AUTHENTIC_BLUE_900}; + font-weight: normal; + line-height: 1.43; + + border-radius: 0; + padding: ${Spaces[2]}; +`; + +export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => ( + + {label} + +); diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-even.output.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-even.output.tsx new file mode 100644 index 000000000..b79c5221c --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-even.output.tsx @@ -0,0 +1,25 @@ +import { Button, Spaces } from '@freenow/wave'; +import styled from 'styled-components'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +const Action = styled(Button)` + justify-content: flex-start; + + color: var(--wave-b-color-blue-primary-900); + font-weight: normal; + line-height: 1.43; + + border-radius: 0; + padding: ${Spaces[2]}; +`; + +export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => ( + + {label} + +); diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-odd.input.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-odd.input.tsx new file mode 100644 index 000000000..db9579054 --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-odd.input.tsx @@ -0,0 +1,28 @@ +import { Colors, Spaces, TextButton, TextButtonProps } from '@freenow/wave'; +import styled from 'styled-components'; + +export const NavigationLink: React.FC = styled(TextButton)<{ active: boolean }>` + display: flex; + align-items: center; + justify-content: flex-start; + + width: 100%; + + padding: ${Spaces[2]}; + padding-left: ${({ active }) => (active ? '20px' : Spaces[3])}; + + font-size: 14px; + font-weight: normal; + text-decoration: none; + + ${props => + props.active && + ` + border-left: 4px solid ${Colors.ACTION_BLUE_900}; + `} + + :hover :first-of-type { + text-decoration: none; + color: ${Colors.ACTION_BLUE_900}; + } +`; diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-odd.output.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-odd.output.tsx new file mode 100644 index 000000000..10f95e96a --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/template-multi-quasis-odd.output.tsx @@ -0,0 +1,28 @@ +import { Spaces, TextButton, TextButtonProps } from '@freenow/wave'; +import styled from 'styled-components'; + +export const NavigationLink: React.FC = styled(TextButton)<{ active: boolean }>` + display: flex; + align-items: center; + justify-content: flex-start; + + width: 100%; + + padding: ${Spaces[2]}; + padding-left: ${({ active }) => (active ? '20px' : Spaces[3])}; + + font-size: 14px; + font-weight: normal; + text-decoration: none; + + ${props => + props.active && + ` + border-left: 4px solid var(--wave-b-color-blue-secondary-900); + `} + + :hover :first-of-type { + text-decoration: none; + color: var(--wave-b-color-blue-secondary-900); + } +`; diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/template-single-quasis.input.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/template-single-quasis.input.tsx new file mode 100644 index 000000000..73ba30fb5 --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/template-single-quasis.input.tsx @@ -0,0 +1,6 @@ +import { Colors } from '@freenow/wave'; +import styled from 'styled-components'; + +export const GreyList = styled.ul` + color: ${Colors.AUTHENTIC_BLUE_550}; +`; diff --git a/src/codemods/__testfixtures__/colors-to-css-vars/template-single-quasis.output.tsx b/src/codemods/__testfixtures__/colors-to-css-vars/template-single-quasis.output.tsx new file mode 100644 index 000000000..3d6f0afc8 --- /dev/null +++ b/src/codemods/__testfixtures__/colors-to-css-vars/template-single-quasis.output.tsx @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const GreyList = styled.ul` + color: var(--wave-b-color-blue-primary-550); +`; diff --git a/src/codemods/__tests__/block-to-width-100-test.ts b/src/codemods/__tests__/block-to-width-100-test.ts new file mode 100644 index 000000000..e4aefa652 --- /dev/null +++ b/src/codemods/__tests__/block-to-width-100-test.ts @@ -0,0 +1,6 @@ +// import { defineTest } from 'jscodeshift/dist/testUtils' + +jest.autoMockOff(); +const { defineTest } = require('jscodeshift/dist/testUtils'); + +defineTest(__dirname, 'block-to-width-100', { quote: 'single' }, 'block-to-width-100', { parser: 'tsx' }); diff --git a/src/codemods/__tests__/colors-to-css-vars-test.ts b/src/codemods/__tests__/colors-to-css-vars-test.ts new file mode 100644 index 000000000..43b702203 --- /dev/null +++ b/src/codemods/__tests__/colors-to-css-vars-test.ts @@ -0,0 +1,18 @@ +jest.autoMockOff(); +const { defineTest } = require('jscodeshift/dist/testUtils'); + +const tests = [ + 'color-in-JSX-multi-import', + 'color-in-JSX-single-import', + 'template-multi-quasis-even', + 'template-multi-quasis-odd', + 'template-single-quasis' +]; + +describe('colors-to-css-vars', () => { + tests.forEach(test => + defineTest(__dirname, 'colors-to-css-vars', { quote: 'single' }, `colors-to-css-vars/${test}`, { + parser: 'tsx' + }) + ); +}); diff --git a/src/codemods/block-to-width-100.ts b/src/codemods/block-to-width-100.ts new file mode 100644 index 000000000..68d921cda --- /dev/null +++ b/src/codemods/block-to-width-100.ts @@ -0,0 +1,59 @@ +import { API, FileInfo } from 'jscodeshift'; +import { Options } from 'recast'; + +module.exports = (file: FileInfo, api: API, options: Options) => { + const j = api.jscodeshift; + const ast = j(file.source); + const printOptions = options ?? { quote: 'single' }; + + const localButtonNames = []; + + const fullWidthProp = { + type: 'JSXAttribute', + name: 'width', + value: { + type: 'StringLiteral', + value: '100%' + } + }; + + ast.find(j.ImportDeclaration, decl => decl.source.value === '@freenow/wave').forEach(decl => { + j(decl) + .find(j.ImportSpecifier) + .forEach(spec => { + if (spec.node.imported.name === 'Button' || spec.node.imported.name === 'TextButton') { + localButtonNames.push(spec.node.local.name); + } + }); + }); + + ast.find(j.JSXElement, { + openingElement: { + name: { + name: name => localButtonNames.includes(name) + } + } + }).forEach(el => { + j(el) + .find(j.JSXAttribute, { + name: name => name.name === 'block' + }) + .forEach(attr => { + if (attr.value.value) { + j(attr) + .find(j.Literal) + .forEach(literal => { + if (literal.value.value === false) { + j(attr).remove(); + } else { + j(attr).replaceWith(fullWidthProp); + } + }); + } else { + j(attr).replaceWith(fullWidthProp); + } + }); + }); + + return ast.toSource(printOptions); +}; diff --git a/src/codemods/colors-to-css-vars.ts b/src/codemods/colors-to-css-vars.ts new file mode 100644 index 000000000..a82c1ce2b --- /dev/null +++ b/src/codemods/colors-to-css-vars.ts @@ -0,0 +1,159 @@ +import { API, FileInfo, Identifier, JSCodeshift, TemplateLiteral } from 'jscodeshift'; +import { Options } from 'recast'; + +const ColorsToCssVariablesMap = { + WHITE: 'var(--wave-b-color-white)', + BLACK: 'var(--wave-b-color-black)', + AUTHENTIC_BLUE_1100: 'var(--wave-b-color-blue-primary-1100)', + AUTHENTIC_BLUE_900: 'var(--wave-b-color-blue-primary-900)', + AUTHENTIC_BLUE_550: 'var(--wave-b-color-blue-primary-550)', + AUTHENTIC_BLUE_350: 'var(--wave-b-color-blue-primary-350)', + AUTHENTIC_BLUE_200: 'var(--wave-b-color-blue-primary-200)', + AUTHENTIC_BLUE_50: 'var(--wave-b-color-blue-primary-50)', + FREEDOM_RED_1000: 'var(--wave-b-color-red-1000)', + FREEDOM_RED_900: 'var(--wave-b-color-red-900)', + ACTION_BLUE_1000: 'var(--wave-b-color-blue-secondary-1000)', + ACTION_BLUE_900: 'var(--wave-b-color-blue-secondary-900)', + ACTION_BLUE_350: 'var(--wave-b-color-blue-secondary-350)', + ACTION_BLUE_150: 'var(--wave-b-color-blue-secondary-150)', + ACTION_BLUE_100: 'var(--wave-b-color-blue-secondary-100)', + ACTION_BLUE_50: 'var(--wave-b-color-blue-secondary-50)', + BUMPY_MAGENTA_1000: 'var(--wave-b-color-magenta-1000)', + BUMPY_MAGENTA_900: 'var(--wave-b-color-magenta-900)', + BUMPY_MAGENTA_350: 'var(--wave-b-color-magenta-350)', + BUMPY_MAGENTA_50: 'var(--wave-b-color-magenta-50)', + POSITIVE_GREEN_1000: 'var(--wave-b-color-green-1000)', + POSITIVE_GREEN_900: 'var(--wave-b-color-green-900)', + POSITIVE_GREEN_350: 'var(--wave-b-color-green-350)', + POSITIVE_GREEN_50: 'var(--wave-b-color-green-50)', + ATTENTION_YELLOW_900: 'var(--wave-b-color-yellow-900)', + ATTENTION_YELLOW_350: 'var(--wave-b-color-yellow-350)', + ATTENTION_YELLOW_50: 'var(--wave-b-color-yellow-50)', + NEGATIVE_ORANGE_1000: 'var(--wave-b-color-orange-1000)', + NEGATIVE_ORANGE_900: 'var(--wave-b-color-orange-900)', + NEGATIVE_ORANGE_350: 'var(--wave-b-color-orange-350)', + NEGATIVE_ORANGE_50: 'var(--wave-b-color-orange-50)' +}; + +const replaceColorsForCssVarsInTemplateLiterals = ( + j: JSCodeshift, + localColorNames: string[], + templateLiteral: TemplateLiteral +) => { + const { quasis } = templateLiteral; + const { expressions } = templateLiteral; + + const expressionsToRemoveIndexes: number[] = []; + const quasisToRemoveIndexes: number[] = []; + + // Iterate over the quasis of the template string (the parts before the `${` and after the `}`) + // e.g. in the template string `color: ${Colors.x};` there are 2 quasis, `color: ` and `;`, the `Colors.x` is an expression + quasis.forEach((el, index) => { + const expressionAfterQuasis = expressions[index]; + + // Check if there are arrow functions inside the template string, since they can also have nested template string + if (expressionAfterQuasis && expressionAfterQuasis.type === 'ArrowFunctionExpression') { + const templateExpressions = j(expressionAfterQuasis).find(j.TemplateLiteral); + + // For every template string in the arrow function recursively replace colors for css vars + templateExpressions.forEach(ex => replaceColorsForCssVarsInTemplateLiterals(j, localColorNames, ex.node)); + } + + // Check if the expression is a MemberExpression (regular object property access) + if (expressionAfterQuasis && expressionAfterQuasis.type === 'MemberExpression') { + const expressionObject = expressionAfterQuasis.object as Identifier; + + // Identify if it's a usage of Colors + const isColorsExpression = localColorNames.includes(expressionObject.name); + + if (isColorsExpression) { + // Find the color being used + const color = (expressionAfterQuasis.property as Identifier).name; + const cssVar: string = ColorsToCssVariablesMap[color]; + + if (!cssVar) return; + + const nextQuasisValue = quasis[index + 1].value.raw; + // Append the mapped css var and the value of the next quasis to the end of the current quasis (where the color expression is) + el.value.raw = el.value.raw + cssVar + nextQuasisValue; + + // Since the color is mapped to the css var we don't need the expression anymore, so we flag it for removal later + expressionsToRemoveIndexes.push(index); + + // The number of quasis always has to match the number of expressions + 1, since we've flagged the expression for removal we need to + // flag the next quasis for removal as well, we've already appended it's value to the current one so we don't lose information + quasisToRemoveIndexes.push(index + 1); + } + } + }); + + // Check if there are any expression that have to be removed and remove them + expressionsToRemoveIndexes.forEach(indexToRemove => { + expressions.splice(indexToRemove, 1); + }); + + // Check if there are any quasis that have to be removed and remove them + quasisToRemoveIndexes.forEach(indexToRemove => { + quasis.splice(indexToRemove, 1); + }); +}; + +export default (file: FileInfo, api: API, options: Options) => { + const j = api.jscodeshift; + const ast = j(file.source); + const printOptions = options ?? { quote: 'single' }; + + const localColorNames: string[] = []; + + // Find @freenow/wave imports + const waveImports = ast.find(j.ImportDeclaration, { + source: { + value: '@freenow/wave' + } + }); + + const waveNamedImports = waveImports.find(j.ImportSpecifier); + + // Find Colors named imports in @freenow/wave imports + const colorsImports = waveNamedImports.filter(path => path.node.imported.name === 'Colors'); + + // Get the local Colors import names + colorsImports.forEach(spec => { + if (spec.node.local?.name) localColorNames.push(spec.node.local.name); + }); + + // Iterate over template strings + ast.find(j.TaggedTemplateExpression).forEach(el => { + // Get template literals in template expression + const templateLiteral = el.node.quasi; + replaceColorsForCssVarsInTemplateLiterals(j, localColorNames, templateLiteral); + }); + + // Find all remaining Colors usage + ast.find(j.MemberExpression, { + object: { + name: (colorName: string) => localColorNames.includes(colorName) + } + }).forEach(ex => { + // Map the Color to a css var + const color = (ex.node.property as Identifier).name; + const cssVar = ColorsToCssVariablesMap[color]; + + if (!cssVar) return; + + // Replace the Colors usage for a string literal (e.g. 'var(--wave-b-color-y)') + const cssVarStringNode = j.stringLiteral(ColorsToCssVariablesMap[color]); + ex.replace(cssVarStringNode); + }); + + // If it is the only named import from wave, remove the whole Wave import + if (waveImports.size() === 1 && waveNamedImports.size() === 1 && colorsImports.size() === 1) { + waveImports.remove(); + + // If there are other named imports from wave, remove only the Colors named import + } else if (waveNamedImports.size() > 1) { + colorsImports.remove(); + } + + return ast.toSource(printOptions); +}; diff --git a/tsconfig.json b/tsconfig.json index 1b12576c9..ddec86e8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,5 +20,5 @@ ] }, "include": ["src/**/*"], - "exclude": ["src/**/docs", "**/node_modules"] + "exclude": ["src/**/docs", "**/node_modules", "src/codemods/__*"] }