@kaizen/components 1.73.0 → 1.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ import ts from 'typescript'
2
+ import {
3
+ createJsxElementWithChildren,
4
+ getKaioTagName,
5
+ setImportToAdd,
6
+ setImportToRemove,
7
+ updateKaioImports,
8
+ type TagImportAttributesMap,
9
+ type UpdateKaioImportsArgs,
10
+ } from '../utils'
11
+ import { transformV1ButtonAttributes } from './transformV1ButtonAttributes'
12
+
13
+ const V1_BUTTONS = ['Button', 'IconButton']
14
+ const V1_BUTTONS_IMPORT_SOURCE = [
15
+ '@kaizen/components',
16
+ '@kaizen/components/v1/actions',
17
+ '@kaizen/components/v2/actions',
18
+ ]
19
+ const BUTTON_IMPORT_DESTINATION = '@kaizen/components/next'
20
+ const LINKBUTTON_IMPORT_DESTINATION = '@kaizen/components'
21
+
22
+ export const upgradeV1Buttons =
23
+ (tagsMap: TagImportAttributesMap): ts.TransformerFactory<ts.SourceFile> =>
24
+ (context) =>
25
+ (rootNode) => {
26
+ const importsToRemove: UpdateKaioImportsArgs['importsToRemove'] = new Map()
27
+ const importsToAdd: UpdateKaioImportsArgs['importsToAdd'] = new Map()
28
+
29
+ const importedTargetButtonTagName = getKaioTagName(
30
+ rootNode,
31
+ 'Button',
32
+ BUTTON_IMPORT_DESTINATION,
33
+ )
34
+
35
+ const importedTargetLinkButtonTagName = getKaioTagName(
36
+ rootNode,
37
+ 'LinkButton',
38
+ LINKBUTTON_IMPORT_DESTINATION,
39
+ )
40
+
41
+ const visit = (node: ts.Node): ts.Node => {
42
+ if (ts.isJsxSelfClosingElement(node)) {
43
+ const tagName = node.tagName.getText()
44
+ const tagImportAttributes = tagsMap.get(tagName)
45
+
46
+ if (!tagImportAttributes) return node
47
+
48
+ if (!V1_BUTTONS.includes(tagImportAttributes.originalName)) return node
49
+ if (!V1_BUTTONS_IMPORT_SOURCE.includes(tagImportAttributes.importModuleName)) return node
50
+
51
+ const { targetComponentName, newAttributes, childrenValue } = transformV1ButtonAttributes(
52
+ node,
53
+ tagImportAttributes.originalName,
54
+ )
55
+
56
+ const targetTagName =
57
+ targetComponentName === 'Button'
58
+ ? (importedTargetButtonTagName ?? 'Button')
59
+ : (importedTargetLinkButtonTagName ?? 'LinkButton')
60
+
61
+ setImportToRemove(
62
+ importsToRemove,
63
+ tagImportAttributes.importModuleName,
64
+ tagImportAttributes.originalName,
65
+ )
66
+
67
+ if (targetComponentName === 'Button' && !importedTargetButtonTagName) {
68
+ setImportToAdd(importsToAdd, BUTTON_IMPORT_DESTINATION, {
69
+ componentName: 'Button',
70
+ alias:
71
+ importedTargetButtonTagName !== 'Button' ? importedTargetButtonTagName : undefined,
72
+ })
73
+ }
74
+
75
+ if (targetComponentName === 'LinkButton' && !importedTargetLinkButtonTagName) {
76
+ setImportToAdd(importsToAdd, LINKBUTTON_IMPORT_DESTINATION, {
77
+ componentName: 'LinkButton',
78
+ alias:
79
+ importedTargetLinkButtonTagName !== 'LinkButton'
80
+ ? importedTargetLinkButtonTagName
81
+ : undefined,
82
+ })
83
+ }
84
+
85
+ return createJsxElementWithChildren(targetTagName, newAttributes, childrenValue)
86
+ }
87
+ return ts.visitEachChild(node, visit, context)
88
+ }
89
+
90
+ const node = ts.visitNode(rootNode, visit)
91
+
92
+ return updateKaioImports({ importsToRemove, importsToAdd })(context)(node as ts.SourceFile)
93
+ }
@@ -0,0 +1,119 @@
1
+ import ts from 'typescript'
2
+ import { parseJsx } from '../__tests__/utils'
3
+ import { createJsxElementWithChildren } from './createJsxElementWithChildren'
4
+ import { printAst } from './printAst'
5
+ import { transformSource, type TransformSourceArgs } from './transformSource'
6
+
7
+ export const mockedTransformer: ts.TransformerFactory<ts.SourceFile> = (context) => (rootNode) => {
8
+ const visit = (node: ts.Node): ts.Node => {
9
+ let childrenValue: ts.JsxAttributeValue | undefined
10
+
11
+ if (ts.isJsxSelfClosingElement(node)) {
12
+ const tagName = node.tagName.getText()
13
+ const attributes = node.attributes.properties.reduce<ts.JsxAttributeLike[]>((acc, attr) => {
14
+ if (ts.isJsxAttribute(attr) && attr.name.getText() === 'toChildren') {
15
+ childrenValue = attr.initializer
16
+ return acc
17
+ }
18
+
19
+ acc.push(attr)
20
+ return acc
21
+ }, [])
22
+
23
+ return createJsxElementWithChildren(tagName, attributes, childrenValue)
24
+ }
25
+ return ts.visitEachChild(node, visit, context)
26
+ }
27
+ return ts.visitNode(rootNode, visit) as ts.SourceFile
28
+ }
29
+
30
+ const testCreateJsxElementWithChildren = (sourceFile: TransformSourceArgs['sourceFile']): string =>
31
+ transformSource({ sourceFile, transformers: [mockedTransformer] })
32
+
33
+ describe('createJsxElementWithChildren()', () => {
34
+ it('transforms a string value', () => {
35
+ const inputAst = parseJsx('<Button toChildren="Hello" variant="primary" />')
36
+ const outputAst = parseJsx('<Button variant="primary">Hello</Button>')
37
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
38
+ })
39
+
40
+ it('transforms a string in brackets', () => {
41
+ const inputAst = parseJsx(`
42
+ <>
43
+ <Button toChildren={"Double quotes"} variant="primary" />
44
+ <Button toChildren={'Single quotes'} variant="primary" />
45
+ </>
46
+ `)
47
+ const outputAst = parseJsx(`
48
+ <>
49
+ <Button variant="primary">Double quotes</Button>
50
+ <Button variant="primary">Single quotes</Button>
51
+ </>
52
+ `)
53
+ // const outputAst = parseJsx('<Button variant="primary">Hello</Button>')
54
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
55
+ })
56
+
57
+ it('transforms a string with comments in brackets', () => {
58
+ const inputAst = parseJsx(`<Button
59
+ toChildren={
60
+ "Hello" // comment
61
+ }
62
+ variant="primary" />
63
+ `)
64
+ const outputAst = parseJsx(`<Button variant="primary">{
65
+ "Hello" // comment
66
+ }</Button>`)
67
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
68
+ })
69
+
70
+ it('transforms a template literal with variables', () => {
71
+ /* eslint-disable no-template-curly-in-string */
72
+ const inputAst = parseJsx('<Button toChildren={`Hello ${name}`} variant="primary" />')
73
+ const outputAst = parseJsx('<Button variant="primary">{`Hello ${name}`}</Button>')
74
+ /* eslint-enable no-template-curly-in-string */
75
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
76
+ })
77
+
78
+ it('transforms a variable', () => {
79
+ const inputAst = parseJsx('<Button toChildren={childrenVar} variant="primary" />')
80
+ const outputAst = parseJsx('<Button variant="primary">{childrenVar}</Button>')
81
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
82
+ })
83
+
84
+ it('transforms a ternary', () => {
85
+ const inputAst = parseJsx(
86
+ '<Button toChildren={isPancake ? "Pancake" : "Waffle"} variant="primary" />',
87
+ )
88
+ const outputAst = parseJsx(
89
+ '<Button variant="primary">{isPancake ? "Pancake" : "Waffle"}</Button>',
90
+ )
91
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
92
+ })
93
+
94
+ it('transforms a JSX element', () => {
95
+ const inputAst = parseJsx('<Button toChildren={<span>Hello</span>} variant="primary" />')
96
+ const outputAst = parseJsx('<Button variant="primary"><span>Hello</span></Button>')
97
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
98
+ })
99
+
100
+ it('transforms a JSX self-closing element', () => {
101
+ const inputAst = parseJsx('<Button toChildren={<Icon />} variant="primary" />')
102
+ const outputAst = parseJsx('<Button variant="primary"><Icon /></Button>')
103
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
104
+ })
105
+
106
+ it('transforms a JSX fragment', () => {
107
+ const inputAst = parseJsx('<Button toChildren={<>Hello</>} variant="primary" />')
108
+ const outputAst = parseJsx('<Button variant="primary"><>Hello</></Button>')
109
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
110
+ })
111
+
112
+ it('adds a comment if no value for children has been passed in', () => {
113
+ const inputAst = parseJsx('<Button variant="primary" />')
114
+ const outputAst = parseJsx(`<Button variant="primary">
115
+ /* @todo Children required but a value was not found during the codemod */
116
+ </Button>`)
117
+ expect(testCreateJsxElementWithChildren(inputAst)).toEqual(printAst(outputAst))
118
+ })
119
+ })
@@ -0,0 +1,55 @@
1
+ import ts from 'typescript'
2
+
3
+ const createJsxChildren = (childrenValue: ts.JsxAttributeValue): ts.JsxChild => {
4
+ if (ts.isStringLiteral(childrenValue)) {
5
+ return ts.factory.createJsxText(childrenValue.text)
6
+ }
7
+
8
+ if (ts.isJsxExpression(childrenValue)) {
9
+ const value = childrenValue.expression
10
+
11
+ if (value) {
12
+ if (ts.isStringLiteral(value)) {
13
+ // Tests for {"string"}, {'string'}
14
+ const regexExpContainsOnlyQuotedString = new RegExp(/^\{(["']).*(\1)\}$/g)
15
+
16
+ if (regexExpContainsOnlyQuotedString.test(childrenValue.getFullText())) {
17
+ return ts.factory.createJsxText(value.text)
18
+ }
19
+ }
20
+
21
+ if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) {
22
+ return value
23
+ }
24
+ }
25
+
26
+ return childrenValue
27
+ }
28
+
29
+ return childrenValue
30
+ }
31
+
32
+ /**
33
+ * Use this to replace a self-closing JSX element to a JSX element with children
34
+ */
35
+ export const createJsxElementWithChildren = (
36
+ tagName: string,
37
+ attributes: ts.JsxAttributeLike[],
38
+ childrenValue: ts.JsxAttributeValue | undefined,
39
+ ): ts.JsxElement => {
40
+ const tagNameId = ts.factory.createIdentifier(tagName)
41
+ const fallbackChildren = [
42
+ ts.factory.createJsxText('\n'),
43
+ ts.factory.createJsxText(
44
+ '/* @todo Children required but a value was not found during the codemod */',
45
+ ),
46
+ ts.factory.createJsxText('\n'),
47
+ ]
48
+ const children = childrenValue ? [createJsxChildren(childrenValue)] : fallbackChildren
49
+
50
+ return ts.factory.createJsxElement(
51
+ ts.factory.createJsxOpeningElement(tagNameId, [], ts.factory.createJsxAttributes(attributes)),
52
+ children,
53
+ ts.factory.createJsxClosingElement(tagNameId),
54
+ )
55
+ }
@@ -1,32 +1,88 @@
1
1
  import ts from 'typescript'
2
2
  import { parseJsx } from '../__tests__/utils/parseJsx'
3
- import { createStyleProp } from './createProp'
3
+ import { createProp, createStyleProp } from './createProp'
4
4
  import { printAst } from './printAst'
5
5
  import { transformSource, type TransformSourceArgs } from './transformSource'
6
6
  import { updateJsxElementWithNewProps } from './updateJsxElementWithNewProps'
7
7
 
8
- export const mockedTransformer: ts.TransformerFactory<ts.SourceFile> = (context) => (rootNode) => {
8
+ export const mockTransformer: ts.TransformerFactory<ts.SourceFile> = (context) => (rootNode) => {
9
9
  const visit = (node: ts.Node): ts.Node => {
10
10
  if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
11
- if (node.tagName.getText() === 'Pancakes') {
12
- const newAttributes = node.attributes.properties.map((attr) => {
13
- if (ts.isJsxAttribute(attr)) {
14
- if (attr.name.getText() === 'replaceWithExistingValue') {
15
- return createStyleProp({ width: attr.initializer! })
16
- }
11
+ const newAttributes = node.attributes.properties.map((attr) => {
12
+ if (ts.isJsxAttribute(attr)) {
13
+ return createProp(`${attr.name.getText()}New`, attr.initializer)
14
+ }
15
+ return attr
16
+ })
17
+ return updateJsxElementWithNewProps(node, newAttributes)
18
+ }
19
+ return ts.visitEachChild(node, visit, context)
20
+ }
21
+ return ts.visitNode(rootNode, visit) as ts.SourceFile
22
+ }
23
+
24
+ const testCreateProp = (sourceFile: TransformSourceArgs['sourceFile']): string =>
25
+ transformSource({
26
+ sourceFile,
27
+ transformers: [mockTransformer],
28
+ })
29
+
30
+ describe('createProp()', () => {
31
+ it('creates a prop with the pre-existing value', () => {
32
+ const inputAst = parseJsx('<Pancakes isBoolean={false} />')
33
+ const outputAst = parseJsx('<Pancakes isBooleanNew={false} />')
34
+ expect(testCreateProp(inputAst)).toEqual(printAst(outputAst))
35
+ })
17
36
 
18
- if (attr.name.getText() === 'replaceWithStringValue') {
19
- return createStyleProp({ width: '100px' })
20
- }
37
+ it('creates a prop and transforms true to undefined', () => {
38
+ const inputAst = parseJsx(`
39
+ export const TestComponent = () => (
40
+ <>
41
+ <Pancakes isBoolean />
42
+ <Pancakes isBoolean={false} />
43
+ <Pancakes stringProp="Hello" />
44
+ <Pancakes stringVarProp={stringVar} />
45
+ <Pancakes numberProp={3} />
46
+ <Pancakes objProp={{ key: 'value' }} />
47
+ </>
48
+ )
49
+ `)
50
+ const outputAst = parseJsx(`
51
+ export const TestComponent = () => (
52
+ <>
53
+ <Pancakes isBooleanNew />
54
+ <Pancakes isBooleanNew={false} />
55
+ <Pancakes stringPropNew="Hello" />
56
+ <Pancakes stringVarPropNew={stringVar} />
57
+ <Pancakes numberPropNew={3} />
58
+ <Pancakes objPropNew={{ key: 'value' }} />
59
+ </>
60
+ )
61
+ `)
62
+ expect(testCreateProp(inputAst)).toEqual(printAst(outputAst))
63
+ })
64
+ })
65
+
66
+ export const styleTransformer: ts.TransformerFactory<ts.SourceFile> = (context) => (rootNode) => {
67
+ const visit = (node: ts.Node): ts.Node => {
68
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
69
+ const newAttributes = node.attributes.properties.map((attr) => {
70
+ if (ts.isJsxAttribute(attr)) {
71
+ if (attr.name.getText() === 'replaceWithExistingValue') {
72
+ return createStyleProp({ width: attr.initializer! })
73
+ }
74
+
75
+ if (attr.name.getText() === 'replaceWithStringValue') {
76
+ return createStyleProp({ width: '100px' })
77
+ }
21
78
 
22
- if (attr.name.getText() === 'replaceWithNumberValue') {
23
- return createStyleProp({ width: 100 })
24
- }
79
+ if (attr.name.getText() === 'replaceWithNumberValue') {
80
+ return createStyleProp({ width: 100 })
25
81
  }
26
- return attr
27
- })
28
- return updateJsxElementWithNewProps(node, newAttributes)
29
- }
82
+ }
83
+ return attr
84
+ })
85
+ return updateJsxElementWithNewProps(node, newAttributes)
30
86
  }
31
87
  return ts.visitEachChild(node, visit, context)
32
88
  }
@@ -36,7 +92,7 @@ export const mockedTransformer: ts.TransformerFactory<ts.SourceFile> = (context)
36
92
  const testCreateStyleProp = (sourceFile: TransformSourceArgs['sourceFile']): string =>
37
93
  transformSource({
38
94
  sourceFile,
39
- transformers: [mockedTransformer],
95
+ transformers: [styleTransformer],
40
96
  })
41
97
 
42
98
  describe('createStyleProp()', () => {
@@ -3,7 +3,14 @@ import ts from 'typescript'
3
3
  export const createProp = (
4
4
  name: string,
5
5
  value?: ts.JsxAttributeValue | undefined,
6
- ): ts.JsxAttribute => ts.factory.createJsxAttribute(ts.factory.createIdentifier(name), value)
6
+ ): ts.JsxAttribute => {
7
+ // Transforms `propName={true}` to `propName`
8
+ if (value && ts.isJsxExpression(value) && value.expression?.kind === ts.SyntaxKind.TrueKeyword) {
9
+ return ts.factory.createJsxAttribute(ts.factory.createIdentifier(name), undefined)
10
+ }
11
+
12
+ return ts.factory.createJsxAttribute(ts.factory.createIdentifier(name), value)
13
+ }
7
14
 
8
15
  export const createStringProp = (name: string, value: string): ts.JsxAttribute =>
9
16
  createProp(name, ts.factory.createStringLiteral(value))
@@ -5,10 +5,17 @@ type ImportModuleNamedImports = {
5
5
  namedImports: ts.NodeArray<ts.ImportSpecifier>
6
6
  }
7
7
 
8
- const getKaioNamedImports = (visitedNode: ts.Node): ImportModuleNamedImports | undefined => {
8
+ const getKaioNamedImports = (
9
+ visitedNode: ts.Node,
10
+ importSource?: string,
11
+ ): ImportModuleNamedImports | undefined => {
9
12
  if (ts.isImportDeclaration(visitedNode)) {
10
13
  const moduleSpecifier = (visitedNode.moduleSpecifier as ts.StringLiteral).text
11
- if (moduleSpecifier.includes('@kaizen/components')) {
14
+ const hasMatch = importSource
15
+ ? moduleSpecifier === importSource
16
+ : moduleSpecifier.includes('@kaizen/components')
17
+
18
+ if (hasMatch) {
12
19
  const namedBindings = visitedNode.importClause?.namedBindings
13
20
  if (namedBindings && ts.isNamedImports(namedBindings)) {
14
21
  return {
@@ -42,12 +49,13 @@ const getNamesFromSpecifier = (importSpecifier: ts.ImportSpecifier): ImportSpeci
42
49
  */
43
50
  export const getKaioTagName = (
44
51
  node: ts.Node,
45
- importSpecifierTarget: string,
52
+ componentName: string,
53
+ importSource?: string,
46
54
  ): string | undefined => {
47
55
  let alias: string | undefined
48
56
 
49
57
  const visitNode = (visitedNode: ts.Node): string | undefined => {
50
- const kaioNamedImports = getKaioNamedImports(visitedNode)
58
+ const kaioNamedImports = getKaioNamedImports(visitedNode, importSource)
51
59
 
52
60
  if (!kaioNamedImports) {
53
61
  return ts.forEachChild(visitedNode, visitNode)
@@ -56,7 +64,7 @@ export const getKaioTagName = (
56
64
  kaioNamedImports.namedImports.find((importSpecifier) => {
57
65
  const { tagName, originalName } = getNamesFromSpecifier(importSpecifier)
58
66
 
59
- if (originalName === importSpecifierTarget) {
67
+ if (originalName === componentName) {
60
68
  alias = tagName
61
69
  return true
62
70
  }
@@ -1,3 +1,4 @@
1
+ export * from './createJsxElementWithChildren'
1
2
  export * from './createProp'
2
3
  export * from './getPropValueText'
3
4
  export * from './getKaioTagName'