diff --git a/.gitignore b/.gitignore index 7a4000d195..bd213e9caf 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ junit.xml /src/icons/filled/material /src/icons/outlined/material cypress/config/a11y-components.json +lib/codemod/.eslintcache diff --git a/lib/codemod/README.md b/lib/codemod/README.md index fd55c241d1..2472eb3d49 100644 --- a/lib/codemod/README.md +++ b/lib/codemod/README.md @@ -26,6 +26,7 @@ The list includes these transformers - [`emotion-icons`](#emotion-icons) - [`remove-redundant-marker-ul`](#remove-redundant-marker-ul) - [`update-list-item-marker-ul-value`](#update-list-item-marker-ul-value) +- [`enum-to-union`](#enum-to-union) #### `emotion-icons` @@ -61,6 +62,14 @@ Unordered List has now a default marker, the script passes `listItemMarker` with - + ``` +#### `enum-to-union` + +Some of NewsKit components support enum as the prop type, the script remove the imports of enum and replace enum type with union type. + +```diff +- ++ +``` diff --git a/lib/codemod/src/cli.js b/lib/codemod/src/cli.js index f1960cb746..7cf78a3d8f 100755 --- a/lib/codemod/src/cli.js +++ b/lib/codemod/src/cli.js @@ -18,6 +18,8 @@ const TRANSFORMS = { 'Unordered List has now a default marker, removes the prop passing the same icon now set as default.', 'update-list-item-marker-ul-value': 'Unordered List has now a default marker, the script passes "listItemMarker" with a value of "null" to UnorderedList components originally not passing any marker.', + 'enum-to-union': + 'Replace enum type with union type and remove the imports of enums', }; function expandFilePathsIfNeeded(filesBeforeExpansion) { diff --git a/lib/codemod/src/transforms/__tests__/enums-to-union/actual.js b/lib/codemod/src/transforms/__tests__/enums-to-union/actual.js new file mode 100644 index 0000000000..64ecc593db --- /dev/null +++ b/lib/codemod/src/transforms/__tests__/enums-to-union/actual.js @@ -0,0 +1,88 @@ +import { + Button, + IconButton, + ButtonSize, + MenuItemAlign, + MenuItemSize, + Menu, + MenuItem, + Slider, + LabelPosition, + Stack, + Flow, + StackDistribution, + StackChild, + AlignSelfValues, + TagSize, + Tabs, + Tab, + TabAlign, + TabsIndicatorPosition, + TabSize, + TabsDistribution, + FormInput, + FormInputTextField, + TextFieldSize, + TextInputSize, +} from 'newskit'; + +const Component = () => ( + <> + + + + Menu item knickknackatory 1 + Menu item knickknackatory 2 + Menu item knickknackatory 3 + + + + child 1 + child 2 + + + + + Tag + + + Tab + + + +
+ + E-mail + + + + + New image + Header + +); diff --git a/lib/codemod/src/transforms/__tests__/enums-to-union/enum-to-union.test.js b/lib/codemod/src/transforms/__tests__/enums-to-union/enum-to-union.test.js new file mode 100644 index 0000000000..cac2277b56 --- /dev/null +++ b/lib/codemod/src/transforms/__tests__/enums-to-union/enum-to-union.test.js @@ -0,0 +1,23 @@ +const path = require('path'); +const jscodeshift = require('jscodeshift'); +const transform = require('../../enum-to-union'); +const {readFile} = require('../utils'); + +function read(fileName) { + return readFile(path.join(__dirname, fileName)); +} + +describe('@newskit/codemod:', () => { + test('enum-to-union', () => { + const actual = transform( + { + source: read('./actual.js'), + }, + {jscodeshift}, + {}, + ); + + const expected = read('./expected.js'); + expect(actual).toBe(expected); + }); +}); diff --git a/lib/codemod/src/transforms/__tests__/enums-to-union/expected.js b/lib/codemod/src/transforms/__tests__/enums-to-union/expected.js new file mode 100644 index 0000000000..05660afe91 --- /dev/null +++ b/lib/codemod/src/transforms/__tests__/enums-to-union/expected.js @@ -0,0 +1,74 @@ +import { + Button, + IconButton, + Menu, + MenuItem, + Slider, + Stack, + StackChild, + Tabs, + Tab, + FormInput, + FormInputTextField, +} from 'newskit'; + +const Component = () => ( + <> + + + + Menu item knickknackatory 1 + Menu item knickknackatory 2 + Menu item knickknackatory 3 + + + + child 1 + child 2 + + + + + Tag + + + Tab + + + +
+ + E-mail + + + + + New image + Header + +); diff --git a/lib/codemod/src/transforms/enum-to-union.js b/lib/codemod/src/transforms/enum-to-union.js new file mode 100644 index 0000000000..3bd94308c3 --- /dev/null +++ b/lib/codemod/src/transforms/enum-to-union.js @@ -0,0 +1,89 @@ +const {toKebabCase} = require('../utils/to-kebab-case'); + +const enumNames = [ + 'ButtonSize', + 'FlagSize', + 'MenuItemAlign', + 'MenuItemSize', + 'LabelPosition', + 'Flow', + 'StackDistribution', + 'AlignSelfValues', + 'TabAlign', + 'TabSize', + 'TabsDistribution', + 'TabsIndicatorPosition', + 'TagSize', + 'TextFieldSize', + 'TextInputSize', +]; + +const enumPropList = [ + 'size', + 'flow', + 'stackDistribution', + 'align', + 'alignSelf', + 'labelPosition', + 'indicatorPosition', + 'distribution', +]; + +const getNewskitImports = (j, root) => + root.find(j.ImportDeclaration, {source: {value: 'newskit'}}).nodes(); + +const removeEnumImports = (j, root) => { + let newskitImports = getNewskitImports(j, root); + + j(newskitImports) + .find(j.ImportSpecifier) + .filter(s => { + const name = s.value.imported.name; + if (enumNames.includes(name)) { + j(s).remove(); + } + }); +}; + +const transformEnums = (j, root) => { + enumPropList.forEach(propName => { + root + .find(j.JSXAttribute, { + name: { + type: 'JSXIdentifier', + name: propName, + }, + value: { + type: 'JSXExpressionContainer', + }, + }) + .find(j.JSXExpressionContainer) + .filter(path => enumNames.includes(path.value.expression.object.name)) + .replaceWith(path => { + const propValue = path.value.expression.property.name; + const newPropValue = `${toKebabCase(propValue)}`; + const newPath = + propName === 'stackDistribution' && + ['start', 'end'].includes(newPropValue) + ? j.literal(`flex-${toKebabCase(propValue)}`) + : j.literal(`${toKebabCase(propValue)}`); + return newPath; + }); + }); +}; + +module.exports = function transformer(file, api, options) { + const j = api.jscodeshift; + const root = j(file.source); + + const printOptions = options.printOptions || { + quote: 'double', + objectCurlySpacing: false, + }; + + removeEnumImports(j, root); + + transformEnums(j, root); + + return root.toSource(printOptions); +}; diff --git a/lib/codemod/src/utils/__tests__/to-kebab-case.test.js b/lib/codemod/src/utils/__tests__/to-kebab-case.test.js new file mode 100644 index 0000000000..bfe1474141 --- /dev/null +++ b/lib/codemod/src/utils/__tests__/to-kebab-case.test.js @@ -0,0 +1,8 @@ +const {toKebabCase} = require('../to-kebab-case'); + +describe('to-kebab-case', () => { + test('toKebabCase', () => { + expect(toKebabCase('HelloWorld')).toBe('hello-world'); + expect(toKebabCase('Hello World')).toBe('hello-world'); + }); +}); diff --git a/lib/codemod/src/utils/to-kebab-case.js b/lib/codemod/src/utils/to-kebab-case.js new file mode 100644 index 0000000000..2c99fc3e5b --- /dev/null +++ b/lib/codemod/src/utils/to-kebab-case.js @@ -0,0 +1,15 @@ +function toKebabCase(str) { + if (str) { + return str + .match(/[A-Z]{2,}(?=[A-Z][a-z0-9]*|\b)|[A-Z]?[a-z0-9]*|[A-Z]|[0-9]+/g) + .filter(Boolean) + .map(x => x.toLowerCase()) + .join('-'); + } + /* istanbul ignore next */ + return ''; +} + +module.exports = { + toKebabCase, +};