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(codemods): add codemod to upgrade V1 Buttons #5408

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/young-bags-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kaizen/components': patch
---

Add codemod to upgrade V1 `Button` and `IconButton`.
45 changes: 45 additions & 0 deletions packages/components/codemods/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,51 @@ Released in `1.60.0`

Removes `Popover` component props `variant` and `customIcon`.

### `upgradeV1Buttons`

Released in `TBC`

Migrates `Button` and `IconButton` component to `Button` V3 or `LinkButton`.

#### Props

- `label` becomes `children`
- eg. `<IconButton label="Hello" />` becomes `<Button>Hello</Button>`
- `onClick` becomes `onPress`
- Variants:
- Default (undefined):
- For `Button` becomes `variant="secondary"`
- For `IconButton` becomes `variant="tertiary"`
- `primary` becomes `variant="primary"`
- `secondary` becomes `variant="tertiary"`
- `destructive` will be removed (no longer available as a variant)
- Sizes:
- Default (undefined) becomes `large`
- `small` becomes `medium`
- `regular` becomes `large`
- `reversed` becomes `isReversed`
- `classNameOverride` becomes `className`
- `data-automation-id` becomes `data-testid`
- `disabled` becomes `isDisabled`
- `newTabAndIUnderstandTheAccessibilityImplications` becomes `target="_blank"`
- `rel="noopener noreferrer"` is also added
- `component` will not be removed by the codemod, but will throw a TypeScript error as the prop itself no longer exists
- For `IconButton` only:
- `hasHiddenLabel` will be added

#### Component transformation

- `Button`/`IconButton` without the `href` or `component` prop will become `Button` V3
- `Button`/`IconButton` with the `href` prop will become `LinkButton`
- `Button`/`IconButton` with the `component` prop will become `LinkButton`

#### Imports

All imports of V1 Buttons will now point to either:

- `@kaizen/components/v3/actions` for `Button`
- `@kaizen/components` for `LinkButton`

### `upgradeIconV1`

Released in `1.67.0`; last updated in `1.68.1`
Expand Down
17 changes: 17 additions & 0 deletions packages/components/codemods/upgradeV1Buttons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { transformComponentsInDir } from '../utils'
import { upgradeV1Buttons } from './upgradeV1Buttons'

const run = (): void => {
console.log('~(-_- ~) Running V1 Buttons upgrade (~ -_-)~')

const targetDir = process.argv[2]
if (!targetDir) {
process.exit(1)
}

transformComponentsInDir(targetDir, ['IconButton', 'Button'], (tagNames) => [
upgradeV1Buttons(tagNames),
])
}

run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import ts from 'typescript'
import { parseJsx } from '../__tests__/utils'
import { createJsxElementWithChildren, printAst } from '../utils'
import { transformV1ButtonAttributes } from './transformV1ButtonAttributes'

export const mockedTransformer =
(kaioComponentName: string) =>
(context: ts.TransformationContext) =>
(rootNode: ts.Node): ts.Node => {
const visit = (node: ts.Node): ts.Node => {
if (ts.isJsxSelfClosingElement(node)) {
const { targetComponentName, newAttributes, childrenValue } = transformV1ButtonAttributes(
node,
kaioComponentName,
)
return createJsxElementWithChildren(targetComponentName, newAttributes, childrenValue)
}
return ts.visitEachChild(node, visit, context)
}
return ts.visitNode(rootNode, visit)
}

const transformInput = (
sourceFile: ts.SourceFile,
kaioComponentName: string = 'Button',
): string => {
const result = ts.transform(sourceFile, [mockedTransformer(kaioComponentName)])
const transformedSource = result.transformed[0] as ts.SourceFile
return printAst(transformedSource)
}

describe('transformV1ButtonAttributes()', () => {
it('changes label to children', () => {
const inputAst = parseJsx('<Button label="Pancakes" />')
const outputAst = parseJsx('<Button variant="secondary" size="large">Pancakes</Button>')
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('replaces IconButton with Button and changes label to children and adds hasHiddenLabel', () => {
const inputAst = parseJsx('<IconButton icon={icon} label="Pancakes" />')
const outputAst = parseJsx(
'<Button icon={icon} variant="tertiary" size="large" hasHiddenLabel>Pancakes</Button>',
)
expect(transformInput(inputAst, 'IconButton')).toEqual(printAst(outputAst))
})

it('replaces V1 Buttons with LinkButton if href exists', () => {
const inputAst = parseJsx('<Button label="Pancakes" href="#" />')
const outputAst = parseJsx(
'<LinkButton href="#" variant="secondary" size="large">Pancakes</LinkButton>',
)
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('replaces V1 Buttons with LinkButton if component prop exists', () => {
const inputAst = parseJsx('<Button label="Pancakes" component={Component} />')
const outputAst = parseJsx(
'<LinkButton component={Component} variant="secondary" size="large">Pancakes</LinkButton>',
)
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

describe('transform existing props', () => {
it('changes onClick to onPress', () => {
const inputAst = parseJsx('<Button label="Pancakes" onClick={handleClick} />')
const outputAst = parseJsx(
'<Button onPress={handleClick} variant="secondary" size="large">Pancakes</Button>',
)
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('changes reversed to isReversed', () => {
const inputAst = parseJsx(`
<>
<Button label="Pancakes" reversed />
<Button label="Pancakes" reversed={false} />
</>
`)
const outputAst = parseJsx(`
<>
<Button isReversed variant="secondary" size="large">Pancakes</Button>
<Button isReversed={false} variant="secondary" size="large">Pancakes</Button>
</>
`)
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('changes classNameOverride to className', () => {
const inputAst = parseJsx('<Button label="Pancakes" classNameOverride="hello" />')
const outputAst = parseJsx(
'<Button className="hello" variant="secondary" size="large">Pancakes</Button>',
)
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('changes data-automation-id to data-testid', () => {
const inputAst = parseJsx('<Button label="Pancakes" data-automation-id="pancakes" />')
const outputAst = parseJsx(
'<Button data-testid="pancakes" variant="secondary" size="large">Pancakes</Button>',
)
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('changes disabled to isDisabled', () => {
const inputAst = parseJsx(`
<>
<Button label="Pancakes" disabled />
<Button label="Pancakes" disabled={false} />
</>
`)
const outputAst = parseJsx(`
<>
<Button isDisabled variant="secondary" size="large">Pancakes</Button>
<Button isDisabled={false} variant="secondary" size="large">Pancakes</Button>
</>
`)
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('changes newTabAndIUnderstandTheAccessibilityImplications to target="_blank" and rel="noopener noreferrer"', () => {
const inputAst = parseJsx(
'<Button label="Pancakes" newTabAndIUnderstandTheAccessibilityImplications />',
)
const outputAst = parseJsx(
'<Button target="_blank" rel="noopener noreferrer" variant="secondary" size="large">Pancakes</Button>',
)
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

describe('transform variant', () => {
it('changes default (undefined) for Button to variant secondary', () => {
const inputAst = parseJsx('<Button label="Pancakes" />')
const outputAst = parseJsx('<Button variant="secondary" size="large">Pancakes</Button>')
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('changes default (undefined) for IconButton to variant tertiary', () => {
const inputAst = parseJsx('<IconButton label="Pancakes" />')
const outputAst = parseJsx(
'<Button variant="tertiary" size="large" hasHiddenLabel>Pancakes</Button>',
)
expect(transformInput(inputAst, 'IconButton')).toEqual(printAst(outputAst))
})

it('changes primary to variant primary', () => {
const inputAst = parseJsx('<Button label="Pancakes" primary />')
const outputAst = parseJsx('<Button variant="primary" size="large">Pancakes</Button>')
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('changes secondary to variant tertiary', () => {
const inputAst = parseJsx('<Button label="Pancakes" secondary />')
const outputAst = parseJsx('<Button variant="tertiary" size="large">Pancakes</Button>')
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('removes destructive', () => {
const inputAst = parseJsx(`
<>
<Button label="Pancakes" destructive />
<Button label="Pancakes" primary destructive />
<Button label="Pancakes" secondary destructive />
</>
`)
const outputAst = parseJsx(`
<>
<Button variant="secondary" size="large">Pancakes</Button>
<Button variant="primary" size="large">Pancakes</Button>
<Button variant="tertiary" size="large">Pancakes</Button>
</>
`)
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})
})

describe('transform size', () => {
it('changes default (undefined) to large', () => {
const inputAst = parseJsx('<Button label="Pancakes" />')
const outputAst = parseJsx('<Button variant="secondary" size="large">Pancakes</Button>')
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('changes small to medium', () => {
const inputAst = parseJsx('<Button label="Pancakes" size="small" />')
const outputAst = parseJsx('<Button size="medium" variant="secondary">Pancakes</Button>')
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('changes regular to large', () => {
const inputAst = parseJsx('<Button label="Pancakes" size="regular" />')
const outputAst = parseJsx('<Button size="large" variant="secondary">Pancakes</Button>')
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})

it('does not change a non-string value', () => {
const inputAst = parseJsx('<Button label="Pancakes" size={size} />')
const outputAst = parseJsx('<Button size={size} variant="secondary">Pancakes</Button>')
expect(transformInput(inputAst)).toEqual(printAst(outputAst))
})
})
})
})
Loading
Loading