Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add inverted prop codemod #368

Merged
merged 26 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
22989e3
chore: bump jscodeshift to 0.15.0
Aug 22, 2023
0ae4665
test: cover block property transform
Aug 22, 2023
6d8ba44
feat: replace colors in template string for css vars
Aug 25, 2023
34ddfc8
fix: adapt codemod to cover templates with an even number of quasis
Aug 25, 2023
99e23eb
chore: add single quote config option to block codemod
Aug 25, 2023
698dae9
build: include codemods in build
Aug 25, 2023
3ff89ea
docs: add new codemod info in migrating docs
Aug 25, 2023
a813a8a
feat: add codemod for deprecated icons replacement
Aug 28, 2023
c025d34
chore: remove deprecated icons folder
Aug 28, 2023
3af6bc6
refactor: simplify constants in deprecated icons codemod
Aug 28, 2023
f81d034
docs: add semantic tokens recommendation in migration docs
Aug 29, 2023
0c148ba
Merge branch 'colors-to-css-vars-codemod' into deprecated-icons-codemod
Aug 29, 2023
6b46750
docs: add description for deprecated icons codemod
Aug 29, 2023
e7f73db
feat: add inverted prop codemod
Aug 29, 2023
77bffc4
docs: add list of components supporting inverted prop
Aug 29, 2023
cfbd41c
feat: transform inverted prop in Text component
Aug 29, 2023
a316b3b
Merge branch 'next' into inverted-prop-codemod
Sep 14, 2023
ed3a292
chore: avoid prettier altering codemod fixtures
Sep 14, 2023
b5fcb34
Merge branch 'next' into inverted-prop-codemod
Oct 31, 2023
91335e5
feat: add Breadcrumbs component to inverted codemod
Oct 31, 2023
d1ecead
refactor: rename inverted codemod to be more generic
Oct 31, 2023
a6f440b
refactor: wrap with InvertedColorScheme in inverted codemod
Oct 31, 2023
755d9da
test: cover wrapping siblings individually in inverted codemod
Oct 31, 2023
9830629
feat: add Logo component to inverted codemod
Nov 2, 2023
e228898
docs: add Logo component to inverted-to-wrapper codemod docs
Nov 6, 2023
fd7d3b2
Merge branch 'next' into inverted-prop-codemod
Nov 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
package-lock.json
src/codemods/__testfixtures__/
src/codemods/__testfixtures__/
14 changes: 14 additions & 0 deletions docs/migrating.storybook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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,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 => (
<InvertedColorScheme>
(<Button size="small" onClick={onClick} disabled={disabled}>
{label}
</Button>)
</InvertedColorScheme>
);
Original file line number Diff line number Diff line change
@@ -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 => (
<Box>
<Button inverted size="small" onClick={onClick}>
{label}
</Button>
<Button inverted size="small" onClick={onClick}>
{label}
</Button>
</Box>
);
Original file line number Diff line number Diff line change
@@ -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 => (
<Box>
<InvertedColorScheme>
<Button size="small" onClick={onClick}>
{label}
</Button>
</InvertedColorScheme>
<InvertedColorScheme>
<Button size="small" onClick={onClick}>
{label}
</Button>
</InvertedColorScheme>
</Box>
);
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,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 => (
<InvertedColorScheme>
(<Button size="small" onClick={onClick} disabled={disabled}>
{label}
</Button>)
</InvertedColorScheme>
);
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,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 => (
<InvertedColorScheme>
(<Action size="small" onClick={onClick} disabled={disabled}>
{label}
</Action>)
</InvertedColorScheme>
);
18 changes: 18 additions & 0 deletions src/codemods/__tests__/inverted-to-wrapper-test.ts
Original file line number Diff line number Diff line change
@@ -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'
})
);
});
121 changes: 121 additions & 0 deletions src/codemods/inverted-to-wrapper.ts
Original file line number Diff line number Diff line change
@@ -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);
};