diff --git a/packages/components/codemods/README.md b/packages/components/codemods/README.md
index 200bb3296a5..d640b35ef9c 100644
--- a/packages/components/codemods/README.md
+++ b/packages/components/codemods/README.md
@@ -103,6 +103,41 @@ 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`
+- (TO DO) Variants
+- (TO DO) Sizes
+- `reversed` becomes `isReversed`
+- `classNameOverride` becomes `className`
+- `data-automation-id` becomes `data-testid`
+- `disabled` becomes `isDisabled`
+- (TO DO) Only when `href` exists:
+ - `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
+ - (TO DO) A `@todo` comment will be prepended to the prop
+- 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 `@kaizen/components/v3/actions`.
+
### `upgradeIconV1`
Released in `1.67.0`; last updated in `1.68.1`
diff --git a/packages/components/codemods/upgradeIconV1/index.ts b/packages/components/codemods/upgradeIconV1/index.ts
index 24ac5ba9089..dea5c3a35de 100644
--- a/packages/components/codemods/upgradeIconV1/index.ts
+++ b/packages/components/codemods/upgradeIconV1/index.ts
@@ -1,6 +1,4 @@
-import fs from 'fs'
-import { getKaioTagNamesByRegex, transformSource, traverseDir } from '../utils'
-import { createEncodedSourceFile } from '../utils/createEncodedSourceFile'
+import { transformComponentsAndImportsInDirByPattern } from '../utils'
import { upgradeIconV1 } from './upgradeIconV1'
const run = (): void => {
@@ -11,20 +9,9 @@ const run = (): void => {
process.exit(1)
}
- const transformFile = (componentFilePath: string, sourceCode: string): void => {
- const sourceFile = createEncodedSourceFile(componentFilePath, sourceCode)
- const tagNames = getKaioTagNamesByRegex(sourceFile, 'Icon$')
- if (tagNames) {
- const updatedSourceFile = transformSource({
- sourceFile,
- transformers: [upgradeIconV1(tagNames)],
- })
-
- fs.writeFileSync(componentFilePath, updatedSourceFile, 'utf8')
- }
- }
-
- traverseDir(targetDir, transformFile)
+ transformComponentsAndImportsInDirByPattern(targetDir, 'Icon$', (tagNames) => [
+ upgradeIconV1(tagNames),
+ ])
}
run()
diff --git a/packages/components/codemods/upgradeIconV1/upgradeIconV1.spec.ts b/packages/components/codemods/upgradeIconV1/upgradeIconV1.spec.ts
index b23543f4e30..7277df50acc 100644
--- a/packages/components/codemods/upgradeIconV1/upgradeIconV1.spec.ts
+++ b/packages/components/codemods/upgradeIconV1/upgradeIconV1.spec.ts
@@ -1,20 +1,19 @@
import { parseJsx } from '../__tests__/utils'
import {
+ getKaioTagNamesMapByPattern,
printAst,
transformSource,
- type ImportModuleNameTagsMap,
type TransformSourceArgs,
} from '../utils'
import { upgradeIconV1 } from './upgradeIconV1'
-const transformIcons = (
- sourceFile: TransformSourceArgs['sourceFile'],
- tagNames: ImportModuleNameTagsMap,
-): string =>
- transformSource({
+const transformIcons = (sourceFile: TransformSourceArgs['sourceFile']): string => {
+ const kaioTagNamesMap = getKaioTagNamesMapByPattern(sourceFile, 'Icon$')
+ return transformSource({
sourceFile,
- transformers: [upgradeIconV1(tagNames)],
+ transformers: [upgradeIconV1(kaioTagNamesMap!)],
})
+}
describe('upgradeIconV1()', () => {
describe('CaMonogramIcon to Brand', () => {
@@ -29,12 +28,7 @@ describe('upgradeIconV1()', () => {
import { Brand } from "@kaizen/components"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([['@kaizen/components', new Map([['CaMonogramIcon', 'CaMonogramIcon']])]]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
it('updates import from CaMonogramIcon using alias to Brand', () => {
@@ -46,12 +40,7 @@ describe('upgradeIconV1()', () => {
import { Brand } from "@kaizen/components"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([['@kaizen/components', new Map([['LogoAlias', 'CaMonogramIcon']])]]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
it('does not add another Brand import if it is already imported', () => {
@@ -63,20 +52,7 @@ describe('upgradeIconV1()', () => {
import { Brand } from "@kaizen/components"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([
- [
- '@kaizen/components',
- new Map([
- ['Brand', 'Brand'],
- ['CaMonogramIcon', 'CaMonogramIcon'],
- ]),
- ],
- ]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
it('uses Brand alias for an existing import', () => {
@@ -88,20 +64,7 @@ describe('upgradeIconV1()', () => {
import { Brand as KzBrand } from "@kaizen/components"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([
- [
- '@kaizen/components',
- new Map([
- ['KzBrand', 'Brand'],
- ['CaMonogramIcon', 'CaMonogramIcon'],
- ]),
- ],
- ]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
})
@@ -117,12 +80,7 @@ describe('upgradeIconV1()', () => {
import { LoadingSpinner } from "@kaizen/components"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([['@kaizen/components', new Map([['SpinnerIcon', 'SpinnerIcon']])]]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
it('updates import from SpinnerIcon using alias to LoadingSpinner', () => {
@@ -134,12 +92,7 @@ describe('upgradeIconV1()', () => {
import { LoadingSpinner } from "@kaizen/components"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([['@kaizen/components', new Map([['LogoAlias', 'SpinnerIcon']])]]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
it('does not add another LoadingSpinner import if it is already imported', () => {
@@ -151,20 +104,7 @@ describe('upgradeIconV1()', () => {
import { LoadingSpinner } from "@kaizen/components"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([
- [
- '@kaizen/components',
- new Map([
- ['LoadingSpinner', 'LoadingSpinner'],
- ['SpinnerIcon', 'SpinnerIcon'],
- ]),
- ],
- ]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
it('uses LoadingSpinner alias for an existing import', () => {
@@ -176,20 +116,7 @@ describe('upgradeIconV1()', () => {
import { LoadingSpinner as KzLoadingSpinner } from "@kaizen/components"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([
- [
- '@kaizen/components',
- new Map([
- ['KzLoadingSpinner', 'LoadingSpinner'],
- ['SpinnerIcon', 'SpinnerIcon'],
- ]),
- ],
- ]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
})
@@ -202,12 +129,7 @@ describe('upgradeIconV1()', () => {
import { Icon } from "@kaizen/components/future"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([['@kaizen/components', new Map([['FlagOnIcon', 'FlagOnIcon']])]]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
it('transforms aliased old Icon', () => {
@@ -219,12 +141,7 @@ describe('upgradeIconV1()', () => {
import { Icon } from "@kaizen/components/future"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([['@kaizen/components', new Map([['IconAlias', 'HamburgerIcon']])]]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
describe('import statements', () => {
@@ -238,12 +155,7 @@ describe('upgradeIconV1()', () => {
import { Icon } from "@kaizen/components/future"
export const TestComponent = () =>
`)
- expect(
- transformIcons(
- inputAst,
- new Map([['@kaizen/components', new Map([['AddIcon', 'AddIcon']])]]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
it('does not update import of components which are not from KAIO', () => {
@@ -269,21 +181,7 @@ describe('upgradeIconV1()', () => {
>
)
`)
- expect(
- transformIcons(
- inputAst,
- new Map([
- [
- '@kaizen/components',
- new Map([
- ['AddIcon', 'AddIcon'],
- ['IconAlias', 'HamburgerIcon'],
- ]),
- ],
- ['somewhere-else', new Map([['HamHam', 'HamburgerIcon']])],
- ]),
- ),
- ).toEqual(printAst(outputAst))
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
})
})
})
diff --git a/packages/components/codemods/upgradeIconV1/upgradeIconV1.ts b/packages/components/codemods/upgradeIconV1/upgradeIconV1.ts
index c75ac521cc4..09730d21327 100644
--- a/packages/components/codemods/upgradeIconV1/upgradeIconV1.ts
+++ b/packages/components/codemods/upgradeIconV1/upgradeIconV1.ts
@@ -1,9 +1,10 @@
import ts from 'typescript'
import {
+ getKaioTagName,
setImportToAdd,
setImportToRemove,
updateKaioImports,
- type ImportModuleNameTagsMap,
+ type TagImportAttributesMap,
type UpdateKaioImportsArgs,
} from '../utils'
import { getNewIconPropsFromOldIconName } from './getNewIconPropsFromOldIconName'
@@ -11,56 +12,51 @@ import { transformCaMonogramIconToBrand } from './transformCaMonogramIconToBrand
import { transformIcon } from './transformIcon'
import { transformSpinnerIconToLoadingSpinner } from './transformSpinnerIconToLoadingSpinner'
-const reverseStringMap = (
- map: Map,
-): Map => {
- const reverseMap = new Map()
- map.forEach((value, key) => reverseMap.set(value, key))
- return reverseMap
-}
-
export const upgradeIconV1 =
- (tagNames: ImportModuleNameTagsMap): ts.TransformerFactory =>
+ (tagsMap: TagImportAttributesMap): ts.TransformerFactory =>
(context) =>
(rootNode) => {
- const oldImportSource = '@kaizen/components'
-
- const kaioTagNames = tagNames.get(oldImportSource)
- if (!kaioTagNames) return rootNode
+ const importedBrandTagName = getKaioTagName(rootNode, 'Brand')
+ const importedLoadingSpinnerTagName = getKaioTagName(rootNode, 'LoadingSpinner')
- const componentToAliasMap = reverseStringMap(kaioTagNames)
const importsToRemove = new Map() satisfies UpdateKaioImportsArgs['importsToRemove']
const importsToAdd = new Map() satisfies UpdateKaioImportsArgs['importsToAdd']
const visit = (node: ts.Node): ts.Node => {
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
const tagName = node.tagName.getText()
- const kaioComponentName = kaioTagNames.get(tagName)
+ const tagImportAttributes = tagsMap.get(tagName)
+
+ if (!tagImportAttributes) return node
+
+ const kaioComponentName = tagImportAttributes.originalName
+ const oldImportSource = tagImportAttributes.importModuleName
if (kaioComponentName === 'CaMonogramIcon') {
setImportToRemove(importsToRemove, oldImportSource, kaioComponentName)
- const alias = componentToAliasMap.get('Brand')!
- if (!kaioTagNames.has(alias)) {
+ if (!importedBrandTagName) {
setImportToAdd(importsToAdd, '@kaizen/components', {
componentName: 'Brand',
- alias: alias !== 'Brand' ? alias : undefined,
+ alias: importedBrandTagName !== 'Brand' ? importedBrandTagName : undefined,
})
}
- return transformCaMonogramIconToBrand(node, alias)
+ return transformCaMonogramIconToBrand(node, importedBrandTagName)
}
if (kaioComponentName === 'SpinnerIcon') {
setImportToRemove(importsToRemove, oldImportSource, kaioComponentName)
- const alias = componentToAliasMap.get('LoadingSpinner')!
- if (!kaioTagNames.has(alias)) {
+ if (!importedLoadingSpinnerTagName) {
setImportToAdd(importsToAdd, '@kaizen/components', {
componentName: 'LoadingSpinner',
- alias: alias !== 'LoadingSpinner' ? alias : undefined,
+ alias:
+ importedLoadingSpinnerTagName !== 'LoadingSpinner'
+ ? importedLoadingSpinnerTagName
+ : undefined,
})
}
- return transformSpinnerIconToLoadingSpinner(node, alias)
+ return transformSpinnerIconToLoadingSpinner(node, importedLoadingSpinnerTagName)
}
if (kaioComponentName) {
@@ -83,8 +79,5 @@ export const upgradeIconV1 =
const node = ts.visitNode(rootNode, visit)
- return updateKaioImports({
- importsToRemove: importsToRemove.size > 0 ? importsToRemove : undefined,
- importsToAdd: importsToAdd.size > 0 ? importsToAdd : undefined,
- })(context)(node as ts.SourceFile)
+ return updateKaioImports({ importsToRemove, importsToAdd })(context)(node as ts.SourceFile)
}
diff --git a/packages/components/codemods/upgradeV1Buttons/index.ts b/packages/components/codemods/upgradeV1Buttons/index.ts
new file mode 100644
index 00000000000..2e80265e769
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/index.ts
@@ -0,0 +1,17 @@
+import { transformComponentsAndImportsInDir } from '../utils'
+import { upgradeV1Buttons } from './upgradeV1Buttons'
+
+const run = (): void => {
+ console.log('~(-_- ~) Running IconButton to Button upgrade (~ -_-)~')
+
+ const targetDir = process.argv[2]
+ if (!targetDir) {
+ process.exit(1)
+ }
+
+ transformComponentsAndImportsInDir(targetDir, ['IconButton', 'Button'], (tagNames) => [
+ upgradeV1Buttons(tagNames),
+ ])
+}
+
+run()
diff --git a/packages/components/codemods/upgradeV1Buttons/transformV1Buttons.spec.ts b/packages/components/codemods/upgradeV1Buttons/transformV1Buttons.spec.ts
new file mode 100644
index 00000000000..90a623eda94
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/transformV1Buttons.spec.ts
@@ -0,0 +1,163 @@
+import ts from 'typescript'
+import { parseJsx } from '../__tests__/utils'
+import { printAst } from '../utils'
+import { transformV1Buttons } from './transformV1Buttons'
+
+export const mockedTransformer =
+ (kaioComponentName: string, alias?: string) =>
+ (context: ts.TransformationContext) =>
+ (rootNode: ts.Node): ts.Node => {
+ const visit = (node: ts.Node): ts.Node => {
+ if (ts.isJsxSelfClosingElement(node)) {
+ return transformV1Buttons(node, kaioComponentName, alias)
+ }
+ return ts.visitEachChild(node, visit, context)
+ }
+ return ts.visitNode(rootNode, visit)
+ }
+
+const transformInput = (
+ sourceFile: ts.SourceFile,
+ kaioComponentName: string = 'Button',
+ alias?: string,
+): string => {
+ const result = ts.transform(sourceFile, [mockedTransformer(kaioComponentName, alias)])
+ const transformedSource = result.transformed[0] as ts.SourceFile
+ return printAst(transformedSource)
+}
+
+describe('transformV1Buttons()', () => {
+ 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('uses alias if it is defined', () => {
+ const inputAst = parseJsx('')
+ const outputAst = parseJsx('Pancakes')
+ expect(transformInput(inputAst, 'Button', 'ButtonAlias')).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))
+ })
+
+ // @todo: Update when we know what to change variants to
+ // describe('transform variant', () => {
+ // it('changes default (undefined) to TBC', () => {
+ // const inputAst = parseJsx('')
+ // const outputAst = parseJsx('')
+ // expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ // })
+
+ // it('changes primary to TBC', () => {
+ // const inputAst = parseJsx('')
+ // const outputAst = parseJsx('')
+ // expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ // })
+
+ // it('changes secondary to TBC', () => {
+ // const inputAst = parseJsx('')
+ // const outputAst = parseJsx('')
+ // expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ // })
+
+ // it('changes destructive to TBC', () => {
+ // const inputAst = parseJsx('')
+ // const outputAst = parseJsx('')
+ // expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ // })
+
+ // it('changes secondary destructive to TBC', () => {
+ // const inputAst = parseJsx('')
+ // const outputAst = parseJsx('')
+ // expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ // })
+ // })
+
+ // @todo: Update when we know what to change sizes to
+ // describe('transform size', () => {
+ // it('changes default (undefined) to TBC', () => {
+ // const inputAst = parseJsx('')
+ // const outputAst = parseJsx('')
+ // expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ // })
+
+ // it('changes small to TBC', () => {
+ // const inputAst = parseJsx('')
+ // const outputAst = parseJsx('')
+ // expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ // })
+
+ // it('changes regular to TBC', () => {
+ // const inputAst = parseJsx('')
+ // const outputAst = parseJsx('')
+ // expect(transformInput(inputAst)).toEqual(printAst(outputAst))
+ // })
+ // })
+ })
+})
diff --git a/packages/components/codemods/upgradeV1Buttons/transformV1Buttons.ts b/packages/components/codemods/upgradeV1Buttons/transformV1Buttons.ts
new file mode 100644
index 00000000000..2bdbd7a606b
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/transformV1Buttons.ts
@@ -0,0 +1,71 @@
+import ts from 'typescript'
+import { createJsxElementWithChildren, createProp, createStringProp } from '../utils'
+
+/**
+ * @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 'disabled':
+ return createProp('isDisabled', propValue)
+ default:
+ return undefined
+ }
+}
+
+export const transformV1Buttons = (
+ node: ts.JsxSelfClosingElement,
+ kaioComponentName: string,
+ tagName: string = 'Button',
+): ts.Node => {
+ let childrenValue: ts.JsxAttributeValue | undefined
+
+ 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
+ }
+
+ const newProp = transformProp(propName, attr.initializer)
+
+ if (newProp === null) return acc
+
+ if (newProp) {
+ acc.push(newProp)
+ return acc
+ }
+ }
+
+ acc.push(attr)
+ return acc
+ }, [])
+
+ if (kaioComponentName === 'IconButton') {
+ newAttributes.push(createProp('hasHiddenLabel'))
+ }
+
+ return createJsxElementWithChildren(tagName, 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..90d619da777
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/upgradeV1Buttons.spec.ts
@@ -0,0 +1,292 @@
+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()', () => {
+ 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/v3/actions"
+ export const TestComponent = () => (
+ <>
+
+ } hasHiddenLabel>More pls
+ >
+ )
+ `)
+
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ 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/v3/actions"
+ 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/v3/actions"
+ export const TestComponent = () => } onPress={handleClick} 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/v3/actions"
+ 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/v3/actions"
+ export const TestComponent = () => (
+ <>>
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button as ButtonAlias } from "@kaizen/components/v3/actions"
+ export const TestComponent = () => (
+ <>WafflePancake>
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ // @todo
+ // it('does not transform IconButton when href prop is set', () => {
+ // const inputAst = parseJsx(`
+ // import { IconButton } from "@kaizen/components"
+ // export const TestComponent = () =>
+ // `)
+ // const outputAst = parseJsx(`
+ // import { IconButton } from "@kaizen/components"
+ // export const TestComponent = () =>
+ // `)
+ // expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ // })
+
+ // @todo
+ // it('does not transform IconButton when component prop is set', () => {
+ // const inputAst = parseJsx(`
+ // import { IconButton } from "@kaizen/components"
+ // export const TestComponent = () =>
+ // `)
+ // const outputAst = parseJsx(`
+ // import { IconButton } from "@kaizen/components"
+ // export const TestComponent = () =>
+ // `)
+ // 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/v3/actions"
+ 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/v3/actions"
+ export const TestComponent = () => (
+ <>>
+ )
+ `)
+ expect(transformIcons(inputAst)).toEqual(printAst(outputAst))
+ })
+
+ it('updates IconButton 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/v3/actions"
+ 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/v3/actions"
+ 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/v3/actions"
+ 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/v3/actions"
+ export const TestComponent = () => (
+ <>
+
+
+
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button } from "@kaizen/components/v3/actions"
+ 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/v3/actions"
+ export const TestComponent = () => (
+ <>
+
+
+ Waffles
+ >
+ )
+ `)
+ const outputAst = parseJsx(`
+ import { Button as ButtonAlias } from "@kaizen/components/v3/actions"
+ 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/v3/actions"
+ export const TestComponent = () => (
+ <>
+
+
+ >
+ )
+ `)
+ 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..ccb98460b74
--- /dev/null
+++ b/packages/components/codemods/upgradeV1Buttons/upgradeV1Buttons.ts
@@ -0,0 +1,51 @@
+import ts from 'typescript'
+import {
+ setImportToAdd,
+ setImportToRemove,
+ updateKaioImports,
+ type TagImportAttributesMap,
+ type UpdateKaioImportsArgs,
+} from '../utils'
+import { transformV1Buttons } from './transformV1Buttons'
+
+export const upgradeV1Buttons =
+ (tagsMap: TagImportAttributesMap): ts.TransformerFactory =>
+ (context) =>
+ (rootNode) => {
+ const IMPORT_DESTINATION = '@kaizen/components/v3/actions'
+ const importsToRemove: UpdateKaioImportsArgs['importsToRemove'] = new Map()
+ const importsToAdd: UpdateKaioImportsArgs['importsToAdd'] = new Map()
+
+ const importedButtonTagName = Array.from(tagsMap.values()).find(
+ ({ originalName, importModuleName }) =>
+ originalName === 'Button' && importModuleName === IMPORT_DESTINATION,
+ )?.tagName
+
+ const visit = (node: ts.Node): ts.Node => {
+ if (ts.isJsxSelfClosingElement(node)) {
+ const tagName = node.tagName.getText()
+ const tagImportAttributes = tagsMap.get(tagName)
+
+ if (tagImportAttributes) {
+ setImportToRemove(
+ importsToRemove,
+ tagImportAttributes.importModuleName,
+ tagImportAttributes.originalName,
+ )
+
+ if (!importedButtonTagName) {
+ setImportToAdd(importsToAdd, IMPORT_DESTINATION, {
+ componentName: importedButtonTagName ?? 'Button',
+ })
+ }
+
+ return transformV1Buttons(node, tagImportAttributes.originalName, importedButtonTagName)
+ }
+ }
+ 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('