diff --git a/.prettierignore b/.prettierignore index 6ab7f3a36..f6be79b9f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,2 @@ package-lock.json -src/codemods/__testfixtures__/ \ No newline at end of file +src/codemods/__testfixtures__/ diff --git a/docs/migrating.storybook.mdx b/docs/migrating.storybook.mdx index c386d4ff1..7a499b3c4 100644 --- a/docs/migrating.storybook.mdx +++ b/docs/migrating.storybook.mdx @@ -53,6 +53,20 @@ This won't change how your icons look in any way since we already exported the d npx jscodeshift -t node_modules/@freenow/wave/lib/cjs/codemods/deprecated-icons.js path/to/src ``` +### Inverted property deprecation + +With the introduction of our new dark theme in Wave we've removed the `inverted` property that several components had. In case you want a specific component to have +the styling of the opposite theme you need to wrap it with the `InvertedColorScheme` component. + +The components that supported the `inverted` prop are: `Input`, `Password`, `Textarea`, `Button`, `Select`, `SelectList`, `PhoneInput`, `Datepicker`, `Tooltip`, `Text`, `Logo` and `Breadcrumbs`. + +```bash +npx jscodeshift -t node_modules/@freenow/wave/lib/cjs/codemods/inverted-to-wrapper.js path/to/src +``` + +Disclaimer: This codemod wraps every Wave component that is using the `inverted` property with `InvertedColorScheme`, this could lead to a decline in performance in case the +DOM gets too deep. Ideally when you have Wave components using the `inverted` prop with a common parent you wrap the parent instead of each individual component. + ### Text weak property The `weak` property was the initial way to indicate secondary information in a `Text` component, it has been deprecated for a while in favour of diff --git a/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-false.input.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-false.input.tsx new file mode 100644 index 000000000..1fa088856 --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-false.input.tsx @@ -0,0 +1,13 @@ +import { Button } from '@freenow/wave'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => ( + +); diff --git a/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-false.output.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-false.output.tsx new file mode 100644 index 000000000..bb65d147a --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-false.output.tsx @@ -0,0 +1,13 @@ +import { Button } from '@freenow/wave'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => ( + +); diff --git a/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-true.input.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-true.input.tsx new file mode 100644 index 000000000..2a4efe95a --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-true.input.tsx @@ -0,0 +1,13 @@ +import { Button } from '@freenow/wave'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => ( + +); diff --git a/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-true.output.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-true.output.tsx new file mode 100644 index 000000000..ba0e6cfdb --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/explicit-boolean-true.output.tsx @@ -0,0 +1,15 @@ +import { Button, InvertedColorScheme } from '@freenow/wave'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => ( + + () + +); diff --git a/src/codemods/__testfixtures__/inverted-to-wrapper/individual-wrap-siblings.input.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/individual-wrap-siblings.input.tsx new file mode 100644 index 000000000..964ea0988 --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/individual-wrap-siblings.input.tsx @@ -0,0 +1,18 @@ +import { Button, Box } from '@freenow/wave'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +export const ActionItem = ({ label, onClick }: Props): JSX.Element => ( + + + + +); diff --git a/src/codemods/__testfixtures__/inverted-to-wrapper/individual-wrap-siblings.output.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/individual-wrap-siblings.output.tsx new file mode 100644 index 000000000..c1d8a5cb8 --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/individual-wrap-siblings.output.tsx @@ -0,0 +1,22 @@ +import { Button, Box, InvertedColorScheme } from '@freenow/wave'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +export const ActionItem = ({ label, onClick }: Props): JSX.Element => ( + + + + + + + + +); diff --git a/src/codemods/__testfixtures__/inverted-to-wrapper/local-rename.input.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/local-rename.input.tsx new file mode 100644 index 000000000..d21af799f --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/local-rename.input.tsx @@ -0,0 +1,13 @@ +import { Button } from '@freenow/wave'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => ( + +); diff --git a/src/codemods/__testfixtures__/inverted-to-wrapper/local-rename.output.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/local-rename.output.tsx new file mode 100644 index 000000000..ba0e6cfdb --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/local-rename.output.tsx @@ -0,0 +1,15 @@ +import { Button, InvertedColorScheme } from '@freenow/wave'; + +interface Props { + label: string; + disabled?: boolean; + onClick: () => void; +} + +export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => ( + + () + +); diff --git a/src/codemods/__testfixtures__/inverted-to-wrapper/styled-rename.input.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/styled-rename.input.tsx new file mode 100644 index 000000000..019f79ab1 --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/styled-rename.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__/inverted-to-wrapper/styled-rename.output.tsx b/src/codemods/__testfixtures__/inverted-to-wrapper/styled-rename.output.tsx new file mode 100644 index 000000000..1067ad773 --- /dev/null +++ b/src/codemods/__testfixtures__/inverted-to-wrapper/styled-rename.output.tsx @@ -0,0 +1,27 @@ +import { Button, Colors, Spaces, InvertedColorScheme } 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/__tests__/inverted-to-wrapper-test.ts b/src/codemods/__tests__/inverted-to-wrapper-test.ts new file mode 100644 index 000000000..5a898d59d --- /dev/null +++ b/src/codemods/__tests__/inverted-to-wrapper-test.ts @@ -0,0 +1,18 @@ +jest.autoMockOff(); +const { defineTest } = require('jscodeshift/dist/testUtils'); + +const tests = [ + 'local-rename', + 'explicit-boolean-false', + 'explicit-boolean-true', + 'styled-rename', + 'individual-wrap-siblings' +]; + +describe('inverted-to-wrapper', () => { + tests.forEach(test => + defineTest(__dirname, 'inverted-to-wrapper', { quote: 'single' }, `inverted-to-wrapper/${test}`, { + parser: 'tsx' + }) + ); +}); diff --git a/src/codemods/inverted-to-wrapper.ts b/src/codemods/inverted-to-wrapper.ts new file mode 100644 index 000000000..0ca2bd206 --- /dev/null +++ b/src/codemods/inverted-to-wrapper.ts @@ -0,0 +1,121 @@ +import { API, FileInfo, ImportDeclaration, JSXAttribute, VariableDeclarator } from 'jscodeshift'; +import { Options } from 'recast'; + +const ComponentNamesWithInvertedProp = [ + 'Input', + 'Password', + 'Textarea', + 'Button', + 'Select', + 'SelectList', + 'PhoneInput', + 'Datepicker', + 'Tooltip', + 'Text', + 'Breadcrumbs', + 'Logo' +]; + +const WRAPPER_COMPONENT_NAME = 'InvertedColorScheme'; + +export default (file: FileInfo, api: API, options: Options) => { + const j = api.jscodeshift; + const ast = j(file.source); + const printOptions = options ?? { quote: 'single' }; + + const localComponentNames: string[] = []; + + // Find @freenow/wave imports + const waveImports = ast.find(j.ImportDeclaration, { + source: { + value: '@freenow/wave' + } + }); + + // Find component named imports in @freenow/wave imports that potentially have an inverted prop + const componentImports = waveImports + .find(j.ImportSpecifier) + .filter(path => ComponentNamesWithInvertedProp.includes(path.node.imported.name)); + + // Get the local icons import names + componentImports.forEach(spec => { + if (spec.node.local?.name) localComponentNames.push(spec.node.local.name); + }); + + // Find declarations of styled components that use a component which has the inverted prop + const styledExpressions = ast.find(j.TaggedTemplateExpression, { + tag: { + arguments: ([argument]) => + argument?.type === 'Identifier' && ComponentNamesWithInvertedProp.includes(argument.name) + } + }); + + styledExpressions.forEach(ex => { + if (ex.parent?.node && ex.parent.node.type === 'VariableDeclarator') { + const styledDeclaration: VariableDeclarator = ex.parent.node; + // Mark the name of the declared styled component as a local component which can have the inverted prop + if (styledDeclaration.id.type === 'Identifier') localComponentNames.push(styledDeclaration.id.name); + } + }); + + // Find usages of the components + const jsxComponents = ast.find(j.JSXElement, { + openingElement: { + name: { + name: name => localComponentNames.includes(name) + } + } + }); + + let shouldAddWrapperImport = false; + + // Iterate over jsx components + jsxComponents.forEach(el => { + // Find inverted props + const invertedProps = j(el).find(j.JSXAttribute, { name: name => name.name === 'inverted' }); + + if (invertedProps.size() !== 1) return; + + let shouldWrap = false; + const invertedProp: JSXAttribute = invertedProps.get(0).node; + + // console.log(invertedProp) + // In case the prop has a value (`inverted={true}` or `inverted={false}`) set shouldWrap based on the value + if ( + invertedProp.value && + invertedProp.value.type === 'JSXExpressionContainer' && + invertedProp.value.expression.type === 'BooleanLiteral' + ) { + shouldWrap = invertedProp.value.expression.value; + } else { + // In case the prop has an implicit `true` value set shouldWrap to `true` + shouldWrap = true; + } + + // Remove the inverted prop + invertedProps.at(0).remove(); + + // In case `inverted` was `true` wrap the element with the wrapper component + if (shouldWrap) { + shouldAddWrapperImport = true; + + // Build wrapper component with the current element as it's children + const WrapperComponent = j.jsxElement( + j.jsxOpeningElement(j.jsxIdentifier(WRAPPER_COMPONENT_NAME)), + j.jsxClosingElement(j.jsxIdentifier(WRAPPER_COMPONENT_NAME)), + [j.jsxText('\n ', '\n '), el.node, j.jsxText('\n ', '\n ')] + ); + + // Replace element with wrapper + el.replace(WrapperComponent); + } + }); + + // Add wrapper import + if (shouldAddWrapperImport && waveImports.size() > 0) { + const importDeclaration: ImportDeclaration = waveImports.get(0).node; + importDeclaration.specifiers.push(j.importSpecifier(j.identifier(WRAPPER_COMPONENT_NAME))); + } + + return ast.toSource(printOptions); +};