Skip to content

Commit

Permalink
feat: add inverted prop codemod
Browse files Browse the repository at this point in the history
  • Loading branch information
Martí Malek committed Aug 29, 2023
1 parent 6b46750 commit e7f73db
Show file tree
Hide file tree
Showing 11 changed files with 282 additions and 0 deletions.
12 changes: 12 additions & 0 deletions docs/migrating.storybook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,15 @@ This won't change how your icons look in any way since we already exported the d
```bash
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 `FlipTheme` component.

```bash
npx jscodeshift -t node_modules/@freenow/wave/lib/cjs/codemods/inverted-to-flip-theme.js path/to/src
```

Disclaimer: This codemod wraps every Wave component that is using the `inverted` property with `FlipTheme`, 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.
Original file line number Diff line number Diff line change
@@ -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 => (
<Button inverted={false} size="small" onClick={onClick} disabled={disabled}>
{label}
</Button>
);
Original file line number Diff line number Diff line change
@@ -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 => (
<Button size="small" onClick={onClick} disabled={disabled}>
{label}
</Button>
);
Original file line number Diff line number Diff line change
@@ -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 => (
<Button inverted={true} size="small" onClick={onClick} disabled={disabled}>
{label}
</Button>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Button, FlipTheme } from '@freenow/wave';

interface Props {
label: string;
disabled?: boolean;
onClick: () => void;
}

export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => (
<FlipTheme>
(
<Button size="small" onClick={onClick} disabled={disabled}>
{label}
</Button>
)
</FlipTheme>
);
Original file line number Diff line number Diff line change
@@ -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 => (
<Button inverted size="small" onClick={onClick} disabled={disabled}>
{label}
</Button>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Button, FlipTheme } from '@freenow/wave';

interface Props {
label: string;
disabled?: boolean;
onClick: () => void;
}

export const ActionItem = ({ label, onClick, disabled = false }: Props): JSX.Element => (
<FlipTheme>
(
<Button size="small" onClick={onClick} disabled={disabled}>
{label}
</Button>
)
</FlipTheme>
);
Original file line number Diff line number Diff line change
@@ -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 => (
<Action inverted size="small" onClick={onClick} disabled={disabled}>
{label}
</Action>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Button, Colors, Spaces, FlipTheme } 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 => (
<FlipTheme>
(
<Action size="small" onClick={onClick} disabled={disabled}>
{label}
</Action>
)
</FlipTheme>
);
12 changes: 12 additions & 0 deletions src/codemods/__tests__/inverted-to-flip-theme-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
jest.autoMockOff();
const { defineTest } = require('jscodeshift/dist/testUtils');

const tests = ['local-rename', 'explicit-boolean-false', 'explicit-boolean-true', 'styled-rename'];

describe('inverted-to-flip-theme', () => {
tests.forEach(test =>
defineTest(__dirname, 'inverted-to-flip-theme', { quote: 'single' }, `inverted-to-flip-theme/${test}`, {
parser: 'tsx'
})
);
});
118 changes: 118 additions & 0 deletions src/codemods/inverted-to-flip-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { API, FileInfo, ImportDeclaration, JSXAttribute, VariableDeclarator } from 'jscodeshift';
import { Options } from 'recast';

const ComponentNamesWithInvertedProp = [
'Input',
'Password',
'Textarea',
'Button',
'Select',
'SelectList',
'PhoneInput',
'Datepicker',
'Tooltip'
];

const WRAPPER_COMPONENT_NAME = 'FlipTheme';

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 components 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);
};

0 comments on commit e7f73db

Please sign in to comment.