diff --git a/.changeset/young-bags-deliver.md b/.changeset/young-bags-deliver.md
new file mode 100644
index 00000000000..6c99c2afb67
--- /dev/null
+++ b/.changeset/young-bags-deliver.md
@@ -0,0 +1,5 @@
+---
+'@kaizen/components': patch
+---
+
+Add codemod to upgrade V1 `Button` and `IconButton`.
diff --git a/packages/components/bin/codemod.sh b/packages/components/bin/codemod.sh
index 07c43c4bbac..070a86bf0aa 100755
--- a/packages/components/bin/codemod.sh
+++ b/packages/components/bin/codemod.sh
@@ -31,6 +31,8 @@ echo ""
if npx tsx@latest $CODEMOD_PATH $TARGET_DIR; then
echo "Codemod '$codemodFileName' completed successfully in directory '$transformDir'"
+ echo "---"
+ echo "Run linting and prettier to correct issues with re-writes"
else
echo "Codemod '$codemodFileName' could not be run in '$TARGET_DIR'"
exit 1
diff --git a/packages/components/codemods/README.md b/packages/components/codemods/README.md
index 200bb3296a5..7dc3dd98742 100644
--- a/packages/components/codemods/README.md
+++ b/packages/components/codemods/README.md
@@ -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. `` becomes ``
+- `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`
diff --git a/packages/components/codemods/upgradeV1Buttons/index.ts b/packages/components/codemods/upgradeV1Buttons/index.ts
new file mode 100644
index 00000000000..ef1fe9b7e40
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/index.ts
@@ -0,0 +1,19 @@
+import { transformComponentsInDir } from '../utils'
+import { upgradeV1Buttons } from './upgradeV1Buttons'
+
+const run = (): void => {
+ console.log('It is recommended that the `upgradeIconV1` codemod be run prior to this')
+ console.log('---')
+ console.log('~(-_- ~) Running V1 Buttons upgrade (~ -_-)~')
+
+ const targetDir = process.argv[2]
+ if (!targetDir) {
+ process.exit(1)
+ }
+
+ transformComponentsInDir(targetDir, ['IconButton', 'Button'], (tagNames) => [
+ upgradeV1Buttons(tagNames),
+ ])
+}
+
+run()
diff --git a/packages/components/codemods/upgradeV1Buttons/transformV1ButtonAttributes.spec.ts b/packages/components/codemods/upgradeV1Buttons/transformV1ButtonAttributes.spec.ts
new file mode 100644
index 00000000000..56f331ae843
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/transformV1ButtonAttributes.spec.ts
@@ -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('')
+ const outputAst = parseJsx('')
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('replaces IconButton with Button and changes label to children and adds hasHiddenLabel', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx(
+ '',
+ )
+ expect(transformInput(inputAst, 'IconButton')).toEqual(printAst(outputAst))
+ })
+
+ it('replaces V1 Buttons with LinkButton if href exists', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx(
+ 'Pancakes',
+ )
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('replaces V1 Buttons with LinkButton if component prop exists', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx(
+ 'Pancakes',
+ )
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ describe('transform existing props', () => {
+ it('changes onClick to onPress', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx(
+ '',
+ )
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('changes reversed to isReversed', () => {
+ const inputAst = parseJsx(`
+ <>
+
+
+ >
+ `)
+ const outputAst = parseJsx(`
+ <>
+
+
+ >
+ `)
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('changes classNameOverride to className', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx(
+ '',
+ )
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('changes data-automation-id to data-testid', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx(
+ '',
+ )
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('changes disabled to isDisabled', () => {
+ const inputAst = parseJsx(`
+ <>
+
+
+ >
+ `)
+ const outputAst = parseJsx(`
+ <>
+
+
+ >
+ `)
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('changes newTabAndIUnderstandTheAccessibilityImplications to target="_blank" and rel="noopener noreferrer"', () => {
+ const inputAst = parseJsx(
+ '',
+ )
+ const outputAst = parseJsx(
+ '',
+ )
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ describe('transform variant', () => {
+ it('changes default (undefined) for Button to variant secondary', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('changes default (undefined) for IconButton to variant tertiary', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx(
+ '',
+ )
+ expect(transformInput(inputAst, 'IconButton')).toEqual(printAst(outputAst))
+ })
+
+ it('changes primary to variant primary', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('changes secondary to variant tertiary', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('removes destructive', () => {
+ const inputAst = parseJsx(`
+ <>
+
+
+
+ >
+ `)
+ const outputAst = parseJsx(`
+ <>
+
+
+
+ >
+ `)
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+ })
+
+ describe('transform size', () => {
+ it('changes default (undefined) to large', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('changes small to medium', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('changes regular to large', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('does not change a non-string value', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ })
+ })
+ })
+})
diff --git a/packages/components/codemods/upgradeV1Buttons/transformV1ButtonAttributes.ts b/packages/components/codemods/upgradeV1Buttons/transformV1ButtonAttributes.ts
new file mode 100644
index 00000000000..fc0eb6ce0b9
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/transformV1ButtonAttributes.ts
@@ -0,0 +1,146 @@
+import ts from 'typescript'
+import { type ButtonProps as V1ButtonProps } from '~components/Button'
+import { type ButtonProps as RCButtonProps } from '~components/__rc__/Button'
+import { createProp, createStringProp, getPropValueText } from '../utils'
+
+const getNewSizeValue = (
+ oldValue: Exclude,
+): Exclude => {
+ switch (oldValue) {
+ case 'small':
+ return 'medium'
+ case 'regular':
+ return 'large'
+ }
+}
+
+/**
+ * @returns
+ * - `ts.JsxAttribute` if the prop should be transformed
+ * - `null` if the prop should be removed
+ * - `undefined` if the prop should be kept as is
+ */
+const transformProp = (
+ propName: string,
+ propValue: ts.JsxAttributeValue | undefined,
+): ts.JsxAttribute | null | undefined => {
+ switch (propName) {
+ case 'onClick':
+ return createProp('onPress', propValue)
+ case 'reversed':
+ return createProp('isReversed', propValue)
+ case 'classNameOverride':
+ return createProp('className', propValue)
+ case 'data-automation-id':
+ return createProp('data-testid', propValue)
+ case 'fullWidth':
+ return createProp('isFullWidth', propValue)
+ case 'working':
+ return createProp('isPending', propValue)
+ case 'workingLabel':
+ return createProp('pendingLabel', propValue)
+ case 'workingLabelHidden':
+ return createProp('hasHiddenPendingLabel', propValue)
+ case 'onMouseDown':
+ return createProp('onPressStart', propValue)
+ case 'disableTabFocusAndIUnderstandTheAccessibilityImplications':
+ return createProp('onPressEnd', propValue)
+ case 'newTabAndIUnderstandTheAccessibilityImplications':
+ return null
+ case 'disabled':
+ return createProp('isDisabled', propValue)
+ case 'size': {
+ if (!propValue) return createStringProp('size', 'large')
+
+ const sizeValue = getPropValueText(propValue) as Exclude
+ return sizeValue
+ ? createStringProp('size', getNewSizeValue(sizeValue))
+ : createProp('size', propValue)
+ }
+ case 'primary':
+ return createStringProp('variant', 'primary')
+ case 'secondary':
+ return createStringProp('variant', 'tertiary')
+ case 'destructive':
+ return null
+ default:
+ return undefined
+ }
+}
+
+type TransformedButtonAttributes = {
+ targetComponentName: string
+ newAttributes: ts.JsxAttributeLike[]
+ childrenValue: ts.JsxAttributeValue | undefined
+}
+
+export const transformV1ButtonAttributes = (
+ node: ts.JsxSelfClosingElement,
+ kaioComponentName: string,
+): TransformedButtonAttributes => {
+ let childrenValue: ts.JsxAttributeValue | undefined
+ let hasSizeProp = false
+ let hasVariant = false
+ let hasLinkAttr = false
+
+ const newAttributes = node.attributes.properties.reduce((acc, attr) => {
+ if (ts.isJsxAttribute(attr)) {
+ const propName = attr.name.getText()
+
+ if (propName === 'label') {
+ childrenValue = attr.initializer
+ return acc
+ }
+
+ if (propName === 'newTabAndIUnderstandTheAccessibilityImplications') {
+ acc.push(createStringProp('target', '_blank'))
+ acc.push(createStringProp('rel', 'noopener noreferrer'))
+ return acc
+ }
+
+ if (propName === 'primary' || propName === 'secondary') {
+ hasVariant = true
+ }
+
+ if (propName === 'size') {
+ hasSizeProp = true
+ }
+
+ if (propName === 'href' || propName === 'component') {
+ hasLinkAttr = true
+ }
+
+ const newProp = transformProp(propName, attr.initializer)
+
+ if (newProp === null) return acc
+
+ if (newProp) {
+ acc.push(newProp)
+ return acc
+ }
+ }
+
+ acc.push(attr)
+ return acc
+ }, [])
+
+ if (!hasVariant) {
+ newAttributes.push(
+ createStringProp('variant', kaioComponentName === 'IconButton' ? 'tertiary' : 'secondary'),
+ )
+ }
+
+ if (!hasSizeProp) {
+ newAttributes.push(createStringProp('size', 'large'))
+ }
+
+ if (kaioComponentName === 'IconButton') {
+ newAttributes.push(createProp('hasHiddenLabel'))
+ }
+
+ return {
+ targetComponentName: hasLinkAttr ? 'LinkButton' : 'Button',
+ newAttributes,
+ childrenValue,
+ }
+}
diff --git a/packages/components/codemods/upgradeV1Buttons/upgradeV1Buttons.spec.ts b/packages/components/codemods/upgradeV1Buttons/upgradeV1Buttons.spec.ts
new file mode 100644
index 00000000000..42aefa13e3f
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/upgradeV1Buttons.spec.ts
@@ -0,0 +1,658 @@
+import { parseJsx } from '../__tests__/utils'
+import {
+ getKaioTagNamesMapByComponentName,
+ printAst,
+ transformSource,
+ type TransformSourceArgs,
+} from '../utils'
+import { upgradeV1Buttons } from './upgradeV1Buttons'
+
+const transformIcons = (sourceFile: TransformSourceArgs['sourceFile']): string => {
+ const kaioTagNamesMap = getKaioTagNamesMapByComponentName(sourceFile, ['IconButton', 'Button'])
+ return transformSource({
+ sourceFile,
+ transformers: [upgradeV1Buttons(kaioTagNamesMap!)],
+ })
+}
+
+describe('upgradeV1Buttons()', () => {
+ describe('to Button', () => {
+ it('transforms Button v1 to Button v3 when href and component prop are not set', () => {
+ const inputAst = parseJsx(`
+ import { Button } from "@kaizen/components"
+ export const TestComponent = () =>
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () =>
+ `)
+
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms IconButton to Button v3 when href and component prop are not set', () => {
+ const inputAst = parseJsx(`
+ import { IconButton } from "@kaizen/components"
+ export const TestComponent = () => } label="More pls" onClick={handleClick} />
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => } onPress={handleClick} variant="tertiary" size="large" hasHiddenLabel>More pls
+ `)
+
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms both IconButton and Button v1 to Button v3 in the same iteration', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+ } label="More pls" />
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+ } variant="tertiary" size="large" hasHiddenLabel>More pls
+ >
+ )
+ `)
+
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms aliased V1 Buttons to Button when href and component prop are not set', () => {
+ const inputAst = parseJsx(`
+ import { IconButton as KzIconButton, Button as KzButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms V1 Buttons to aliased Button', () => {
+ const inputAst = parseJsx(`
+ import { IconButton, Button } from "@kaizen/components"
+ import { Button as ButtonAlias } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+ Toast
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button as ButtonAlias } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+ Waffle
+ Pancake
+ Toast
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ describe('import statements', () => {
+ it('updates V1 Buttons from @kaizen/components', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('updates V1 Buttons from @kaizen/components/v1/actions', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components/v1/actions"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('updates V1 Buttons from @kaizen/components/v2/actions', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components/v2/actions"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('updates aliased V1 Buttons to Button', () => {
+ const inputAst = parseJsx(`
+ import { Button as KzButton, IconButton as KzIconButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('updates imports of multiple V1 Buttons from different KAIO imports', () => {
+ const inputAst = parseJsx(`
+ import { Button as KzButton, IconButton as KzIconButton } from "@kaizen/components"
+ import { Button as ButtonV1, IconButton as IconButtonV1 } from "@kaizen/components/v1/actions"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('does not duplicate Button import if it already exists', () => {
+ const inputAst = parseJsx(`
+ import { IconButton, Button as KzButton } from "@kaizen/components"
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('does not add Button if aliased Button exists', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components"
+ import { Button as ButtonAlias } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+ Waffles
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button as ButtonAlias } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+ Pancakes
+ Scones
+ Waffles
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('does not update import of irrelevant KAIO components', () => {
+ const inputAst = parseJsx(`
+ import { IconButton, FilterButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { FilterButton } from "@kaizen/components"
+ import { Button } from "@kaizen/components/next"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+ })
+ })
+
+ describe('to LinkButton', () => {
+ it('transforms V1 Buttons to LinkButton when href prop is set', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Pancakes
+ Waffles
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms V1 Buttons to LinkButton when component prop is set', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Pancakes
+ Waffles
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms both IconButton and Button to LinkButton in the same iteration', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms aliased V1 Buttons to LinkButton when href or component prop are set', () => {
+ const inputAst = parseJsx(`
+ import { IconButton as KzIconButton, Button as KzButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms V1 Buttons to aliased LinkButton', () => {
+ const inputAst = parseJsx(`
+ import { IconButton, Button } from "@kaizen/components"
+ import { LinkButton as LinkButtonAlias } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton as LinkButtonAlias } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ describe('import statements', () => {
+ it('updates V1 Buttons from @kaizen/components', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('updates V1 Buttons from @kaizen/components/v1/actions', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components/v1/actions"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('updates V1 Buttons from @kaizen/components/v2/actions', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton } from "@kaizen/components/v2/actions"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('updates aliased V1 Buttons to LinkButton', () => {
+ const inputAst = parseJsx(`
+ import { Button as KzButton, IconButton as KzIconButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('updates imports of multiple V1 Buttons from different KAIO imports', () => {
+ const inputAst = parseJsx(`
+ import { Button as KzButton, IconButton as KzIconButton } from "@kaizen/components"
+ import { Button as ButtonV1, IconButton as IconButtonV1 } from "@kaizen/components/v1/actions"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+
+ Summer
+ Autumn
+ Winter
+ Spring
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('does not duplicate LinkButton import if it already exists', () => {
+ const inputAst = parseJsx(`
+ import { IconButton, Button, LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ Waffles
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+ Waffles
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('does not add LinkButton if aliased LinkButton exists', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton, LinkButton as LinkButtonAlias } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+ Waffles
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { LinkButton as LinkButtonAlias } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+ Waffles
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('does not update import of irrelevant KAIO components', () => {
+ const inputAst = parseJsx(`
+ import { Button, IconButton, FilterButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+
+
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { FilterButton, LinkButton } from "@kaizen/components"
+ export const TestComponent = () => (
+ <>
+ Summer
+ Autumn
+ Winter
+ Spring
+
+ >
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+ })
+ })
+})
diff --git a/packages/components/codemods/upgradeV1Buttons/upgradeV1Buttons.ts b/packages/components/codemods/upgradeV1Buttons/upgradeV1Buttons.ts
new file mode 100644
index 00000000000..d1d8ed20df0
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/upgradeV1Buttons.ts
@@ -0,0 +1,93 @@
+import ts from 'typescript'
+import {
+ createJsxElementWithChildren,
+ getKaioTagName,
+ setImportToAdd,
+ setImportToRemove,
+ updateKaioImports,
+ type TagImportAttributesMap,
+ type UpdateKaioImportsArgs,
+} from '../utils'
+import { transformV1ButtonAttributes } from './transformV1ButtonAttributes'
+
+const V1_BUTTONS = ['Button', 'IconButton']
+const V1_BUTTONS_IMPORT_SOURCE = [
+ '@kaizen/components',
+ '@kaizen/components/v1/actions',
+ '@kaizen/components/v2/actions',
+]
+const BUTTON_IMPORT_DESTINATION = '@kaizen/components/next'
+const LINKBUTTON_IMPORT_DESTINATION = '@kaizen/components'
+
+export const upgradeV1Buttons =
+ (tagsMap: TagImportAttributesMap): ts.TransformerFactory =>
+ (context) =>
+ (rootNode) => {
+ const importsToRemove: UpdateKaioImportsArgs['importsToRemove'] = new Map()
+ const importsToAdd: UpdateKaioImportsArgs['importsToAdd'] = new Map()
+
+ const importedTargetButtonTagName = getKaioTagName(
+ rootNode,
+ 'Button',
+ BUTTON_IMPORT_DESTINATION,
+ )
+
+ const importedTargetLinkButtonTagName = getKaioTagName(
+ rootNode,
+ 'LinkButton',
+ LINKBUTTON_IMPORT_DESTINATION,
+ )
+
+ const visit = (node: ts.Node): ts.Node => {
+ if (ts.isJsxSelfClosingElement(node)) {
+ const tagName = node.tagName.getText()
+ const tagImportAttributes = tagsMap.get(tagName)
+
+ if (!tagImportAttributes) return node
+
+ if (!V1_BUTTONS.includes(tagImportAttributes.originalName)) return node
+ if (!V1_BUTTONS_IMPORT_SOURCE.includes(tagImportAttributes.importModuleName)) return node
+
+ const { targetComponentName, newAttributes, childrenValue } = transformV1ButtonAttributes(
+ node,
+ tagImportAttributes.originalName,
+ )
+
+ const targetTagName =
+ targetComponentName === 'Button'
+ ? (importedTargetButtonTagName ?? 'Button')
+ : (importedTargetLinkButtonTagName ?? 'LinkButton')
+
+ setImportToRemove(
+ importsToRemove,
+ tagImportAttributes.importModuleName,
+ tagImportAttributes.originalName,
+ )
+
+ if (targetComponentName === 'Button' && !importedTargetButtonTagName) {
+ setImportToAdd(importsToAdd, BUTTON_IMPORT_DESTINATION, {
+ componentName: 'Button',
+ alias:
+ importedTargetButtonTagName !== 'Button' ? importedTargetButtonTagName : undefined,
+ })
+ }
+
+ if (targetComponentName === 'LinkButton' && !importedTargetLinkButtonTagName) {
+ setImportToAdd(importsToAdd, LINKBUTTON_IMPORT_DESTINATION, {
+ componentName: 'LinkButton',
+ alias:
+ importedTargetLinkButtonTagName !== 'LinkButton'
+ ? importedTargetLinkButtonTagName
+ : undefined,
+ })
+ }
+
+ return createJsxElementWithChildren(targetTagName, newAttributes, childrenValue)
+ }
+ return ts.visitEachChild(node, visit, context)
+ }
+
+ const node = ts.visitNode(rootNode, visit)
+
+ return updateKaioImports({ importsToRemove, importsToAdd })(context)(node as ts.SourceFile)
+ }
diff --git a/packages/components/codemods/utils/createJsxElementWithChildren.spec.ts b/packages/components/codemods/utils/createJsxElementWithChildren.spec.ts
new file mode 100644
index 00000000000..af7b91e6951
--- /dev/null
+++ b/packages/components/codemods/utils/createJsxElementWithChildren.spec.ts
@@ -0,0 +1,119 @@
+import ts from 'typescript'
+import { parseJsx } from '../__tests__/utils'
+import { createJsxElementWithChildren } from './createJsxElementWithChildren'
+import { printAst } from './printAst'
+import { transformSource, type TransformSourceArgs } from './transformSource'
+
+export const mockedTransformer: ts.TransformerFactory = (context) => (rootNode) => {
+ const visit = (node: ts.Node): ts.Node => {
+ let childrenValue: ts.JsxAttributeValue | undefined
+
+ if (ts.isJsxSelfClosingElement(node)) {
+ const tagName = node.tagName.getText()
+ const attributes = node.attributes.properties.reduce((acc, attr) => {
+ if (ts.isJsxAttribute(attr) && attr.name.getText() === 'toChildren') {
+ childrenValue = attr.initializer
+ return acc
+ }
+
+ acc.push(attr)
+ return acc
+ }, [])
+
+ return createJsxElementWithChildren(tagName, attributes, childrenValue)
+ }
+ return ts.visitEachChild(node, visit, context)
+ }
+ return ts.visitNode(rootNode, visit) as ts.SourceFile
+}
+
+const testCreateJsxElementWithChildren = (sourceFile: TransformSourceArgs['sourceFile']): string =>
+ transformSource({ sourceFile, transformers: [mockedTransformer] })
+
+describe('createJsxElementWithChildren()', () => {
+ it('transforms a string value', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms a string in brackets', () => {
+ const inputAst = parseJsx(`
+ <>
+
+
+ >
+ `)
+ const outputAst = parseJsx(`
+ <>
+
+
+ >
+ `)
+ // const outputAst = parseJsx('')
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms a string with comments in brackets', () => {
+ const inputAst = parseJsx(`
+ `)
+ const outputAst = parseJsx(``)
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms a template literal with variables', () => {
+ /* eslint-disable no-template-curly-in-string */
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ /* eslint-enable no-template-curly-in-string */
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms a variable', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('')
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms a ternary', () => {
+ const inputAst = parseJsx(
+ '',
+ )
+ const outputAst = parseJsx(
+ '',
+ )
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('transforms a JSX element', () => {
+ const inputAst = parseJsx('