@kaizen/components 1.80.2 → 1.80.4

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.
Files changed (74) hide show
  1. package/codemods/README.md +24 -0
  2. package/codemods/migrateV2NextToCurrent/index.ts +40 -0
  3. package/codemods/migrateV2NextToCurrent/migrateV2NextToCurrent.spec.ts +555 -0
  4. package/codemods/migrateV2NextToCurrent/migrateV2NextToCurrent.ts +104 -0
  5. package/codemods/renameV2ComponentImportsAndUsages/index.ts +30 -0
  6. package/codemods/renameV2ComponentImportsAndUsages/renameV2ComponentImportsAndUsages.spec.ts +390 -0
  7. package/codemods/renameV2ComponentImportsAndUsages/renameV2ComponentImportsAndUsages.ts +151 -0
  8. package/codemods/utils/createModulePathTransformer.spec.ts +209 -0
  9. package/codemods/utils/createModulePathTransformer.ts +59 -0
  10. package/codemods/utils/createRenameMapFromGroups.ts +31 -0
  11. package/codemods/utils/index.ts +3 -0
  12. package/codemods/utils/updateJsxElementTagName.spec.ts +129 -0
  13. package/codemods/utils/updateJsxElementTagName.ts +56 -0
  14. package/codemods/utils/updateKaioImports.spec.ts +82 -0
  15. package/codemods/utils/updateKaioImports.ts +16 -7
  16. package/dist/cjs/src/__alpha__/SingleSelect/SingleSelect.cjs +69 -16
  17. package/dist/cjs/src/__alpha__/SingleSelect/context/SingleSelectContext.cjs +13 -0
  18. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.cjs +54 -0
  19. package/dist/cjs/src/__alpha__/SingleSelect/{SingleSelect.module.css.cjs → subcomponents/Popover/Popover.module.css.cjs} +1 -1
  20. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.cjs +94 -0
  21. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.cjs +69 -0
  22. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.cjs +12 -0
  23. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.cjs +41 -5
  24. package/dist/esm/src/__alpha__/SingleSelect/SingleSelect.mjs +60 -10
  25. package/dist/esm/src/__alpha__/SingleSelect/context/SingleSelectContext.mjs +10 -0
  26. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.mjs +49 -0
  27. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css.mjs +4 -0
  28. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.mjs +92 -0
  29. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.mjs +67 -0
  30. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.mjs +10 -0
  31. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.mjs +43 -7
  32. package/dist/styles.css +43 -21
  33. package/dist/types/__alpha__/SingleSelect/SingleSelect.d.ts +7 -9
  34. package/dist/types/__alpha__/SingleSelect/context/SingleSelectContext.d.ts +12 -0
  35. package/dist/types/__alpha__/SingleSelect/context/index.d.ts +1 -0
  36. package/dist/types/__alpha__/SingleSelect/subcomponents/List/List.d.ts +2 -1
  37. package/dist/types/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.d.ts +2 -1
  38. package/dist/types/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.d.ts +2 -1
  39. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/Popover.d.ts +6 -0
  40. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/index.d.ts +1 -0
  41. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/index.d.ts +2 -0
  42. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.d.ts +4 -0
  43. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.d.ts +4 -0
  44. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.d.ts +1 -0
  45. package/dist/types/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.d.ts +2 -1
  46. package/dist/types/__alpha__/SingleSelect/subcomponents/index.d.ts +1 -0
  47. package/dist/types/__alpha__/SingleSelect/types.d.ts +45 -0
  48. package/package.json +4 -4
  49. package/src/__alpha__/SingleSelect/SingleSelect.tsx +79 -14
  50. package/src/__alpha__/SingleSelect/_docs/SingleSelect.mdx +5 -2
  51. package/src/__alpha__/SingleSelect/_docs/SingleSelect.spec.stories.tsx +100 -0
  52. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stickersheet.stories.tsx +4 -4
  53. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stories.tsx +21 -2
  54. package/src/__alpha__/SingleSelect/context/SingleSelectContext.tsx +21 -0
  55. package/src/__alpha__/SingleSelect/context/index.ts +1 -0
  56. package/src/__alpha__/SingleSelect/subcomponents/List/List.module.css +0 -1
  57. package/src/__alpha__/SingleSelect/subcomponents/List/List.tsx +2 -1
  58. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css +7 -0
  59. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.tsx +2 -1
  60. package/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.tsx +3 -1
  61. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css +24 -0
  62. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.tsx +54 -0
  63. package/src/__alpha__/SingleSelect/subcomponents/Popover/index.ts +1 -0
  64. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/index.ts +2 -0
  65. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.ts +108 -0
  66. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.ts +75 -0
  67. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.ts +13 -0
  68. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css +1 -0
  69. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.tsx +29 -7
  70. package/src/__alpha__/SingleSelect/subcomponents/index.ts +1 -0
  71. package/src/__alpha__/SingleSelect/types.ts +58 -0
  72. package/dist/esm/src/__alpha__/SingleSelect/SingleSelect.module.css.mjs +0 -4
  73. package/src/__alpha__/SingleSelect/SingleSelect.module.css +0 -9
  74. package/src/__alpha__/SingleSelect/SingleSelect.spec.tsx +0 -26
@@ -0,0 +1,209 @@
1
+ import type ts from 'typescript'
2
+ import { parseJsx } from '../__tests__/utils'
3
+ import { processImportDeclaration, type ProcessImportOptions } from './createModulePathTransformer'
4
+
5
+ const createTestOptions = (): ProcessImportOptions => ({
6
+ importsToRemove: new Map(),
7
+ importsToAdd: new Map(),
8
+ renameMap: new Map([
9
+ [
10
+ 'OldComponent',
11
+ {
12
+ newName: 'NewComponent',
13
+ fromModules: ['@kaizen/components/v3/actions'],
14
+ toModule: '@kaizen/components',
15
+ },
16
+ ],
17
+ [
18
+ 'Select',
19
+ {
20
+ newName: 'SingleSelect',
21
+ fromModules: ['@kaizen/components/next', '@kaizen/components/future'],
22
+ toModule: '@kaizen/components',
23
+ },
24
+ ],
25
+ ]),
26
+ validRenames: new Set(),
27
+ })
28
+
29
+ const getImportDeclaration = (code: string): ts.ImportDeclaration => {
30
+ const sourceFile = parseJsx(code)
31
+ return sourceFile.statements[0] as ts.ImportDeclaration
32
+ }
33
+
34
+ describe('processImportDeclaration', () => {
35
+ it('should transform basic import', () => {
36
+ const options = createTestOptions()
37
+ const importDeclaration = getImportDeclaration(
38
+ `import { OldComponent } from "@kaizen/components/v3/actions"`,
39
+ )
40
+
41
+ processImportDeclaration(importDeclaration, options)
42
+
43
+ expect(options.importsToRemove.get('@kaizen/components/v3/actions')).toContain('OldComponent')
44
+ expect(options.importsToAdd.get('@kaizen/components')?.get('NewComponent')).toEqual({
45
+ componentName: 'NewComponent',
46
+ alias: undefined,
47
+ isTypeOnly: false,
48
+ })
49
+ expect(options.validRenames?.has('OldComponent')).toBe(true)
50
+ })
51
+
52
+ it('should transform aliased import', () => {
53
+ const options = createTestOptions()
54
+ const importDeclaration = getImportDeclaration(
55
+ `import { OldComponent as MyComponent } from "@kaizen/components/v3/actions"`,
56
+ )
57
+
58
+ processImportDeclaration(importDeclaration, options)
59
+
60
+ expect(options.importsToRemove.get('@kaizen/components/v3/actions')).toContain('OldComponent')
61
+ expect(options.importsToAdd.get('@kaizen/components')?.get('NewComponent')).toEqual({
62
+ componentName: 'NewComponent',
63
+ alias: 'MyComponent',
64
+ isTypeOnly: false,
65
+ })
66
+ expect(options.validRenames?.has('OldComponent')).toBe(true)
67
+ })
68
+
69
+ it('should transform type-only import at import clause level', () => {
70
+ const options = createTestOptions()
71
+ const importDeclaration = getImportDeclaration(
72
+ `import type { OldComponent } from "@kaizen/components/v3/actions"`,
73
+ )
74
+
75
+ processImportDeclaration(importDeclaration, options)
76
+
77
+ expect(options.importsToRemove.get('@kaizen/components/v3/actions')).toContain('OldComponent')
78
+ expect(options.importsToAdd.get('@kaizen/components')?.get('NewComponent')).toEqual({
79
+ componentName: 'NewComponent',
80
+ alias: undefined,
81
+ isTypeOnly: true,
82
+ })
83
+ expect(options.validRenames?.has('OldComponent')).toBe(true)
84
+ })
85
+
86
+ it('should transform inline type-only import', () => {
87
+ const options = createTestOptions()
88
+ const importDeclaration = getImportDeclaration(
89
+ `import { type OldComponent } from "@kaizen/components/v3/actions"`,
90
+ )
91
+
92
+ processImportDeclaration(importDeclaration, options)
93
+
94
+ expect(options.importsToRemove.get('@kaizen/components/v3/actions')).toContain('OldComponent')
95
+ expect(options.importsToAdd.get('@kaizen/components')?.get('NewComponent')).toEqual({
96
+ componentName: 'NewComponent',
97
+ alias: undefined,
98
+ isTypeOnly: true,
99
+ })
100
+ expect(options.validRenames?.has('OldComponent')).toBe(true)
101
+ })
102
+
103
+ it('should handle mixed imports in single declaration', () => {
104
+ const options = createTestOptions()
105
+ const importDeclaration = getImportDeclaration(
106
+ `import { OldComponent, AnotherComponent } from "@kaizen/components/v3/actions"`,
107
+ )
108
+
109
+ processImportDeclaration(importDeclaration, options)
110
+
111
+ expect(options.importsToRemove.get('@kaizen/components/v3/actions')).toContain('OldComponent')
112
+ expect(options.importsToRemove.get('@kaizen/components/v3/actions')).not.toContain(
113
+ 'AnotherComponent',
114
+ )
115
+ expect(options.importsToAdd.get('@kaizen/components')?.get('NewComponent')).toEqual({
116
+ componentName: 'NewComponent',
117
+ alias: undefined,
118
+ isTypeOnly: false,
119
+ })
120
+ expect(options.validRenames?.has('OldComponent')).toBe(true)
121
+ })
122
+
123
+ it('should ignore imports from non-targeted modules', () => {
124
+ const options = createTestOptions()
125
+ const importDeclaration = getImportDeclaration(
126
+ `import { OldComponent } from "some-other-library"`,
127
+ )
128
+
129
+ processImportDeclaration(importDeclaration, options)
130
+
131
+ expect(options.importsToRemove.size).toBe(0)
132
+ expect(options.importsToAdd.size).toBe(0)
133
+ expect(options.validRenames?.size).toBe(0)
134
+ })
135
+
136
+ it('should ignore imports not in rename map', () => {
137
+ const options = createTestOptions()
138
+ const importDeclaration = getImportDeclaration(
139
+ `import { SomeOtherComponent } from "@kaizen/components/v3/actions"`,
140
+ )
141
+
142
+ processImportDeclaration(importDeclaration, options)
143
+
144
+ expect(options.importsToRemove.size).toBe(0)
145
+ expect(options.importsToAdd.size).toBe(0)
146
+ expect(options.validRenames?.size).toBe(0)
147
+ })
148
+
149
+ it('should handle multiple fromModules for same component', () => {
150
+ const options = createTestOptions()
151
+ const importDeclaration1 = getImportDeclaration(
152
+ `import { Select } from "@kaizen/components/next"`,
153
+ )
154
+ const importDeclaration2 = getImportDeclaration(
155
+ `import { Select } from "@kaizen/components/future"`,
156
+ )
157
+
158
+ processImportDeclaration(importDeclaration1, options)
159
+
160
+ expect(options.importsToRemove.get('@kaizen/components/next')).toContain('Select')
161
+ expect(options.importsToAdd.get('@kaizen/components')?.get('SingleSelect')).toEqual({
162
+ componentName: 'SingleSelect',
163
+ alias: undefined,
164
+ isTypeOnly: false,
165
+ })
166
+ expect(options.validRenames?.has('Select')).toBe(true)
167
+
168
+ // Reset for second test
169
+ options.importsToRemove.clear()
170
+ options.importsToAdd.clear()
171
+ options.validRenames?.clear()
172
+
173
+ processImportDeclaration(importDeclaration2, options)
174
+
175
+ expect(options.importsToRemove.get('@kaizen/components/future')).toContain('Select')
176
+ expect(options.importsToAdd.get('@kaizen/components')?.get('SingleSelect')).toEqual({
177
+ componentName: 'SingleSelect',
178
+ alias: undefined,
179
+ isTypeOnly: false,
180
+ })
181
+ expect(options.validRenames?.has('Select')).toBe(true)
182
+ })
183
+
184
+ it('should handle default imports gracefully', () => {
185
+ const options = createTestOptions()
186
+ const importDeclaration = getImportDeclaration(
187
+ `import OldComponent from "@kaizen/components/v3/actions"`,
188
+ )
189
+
190
+ processImportDeclaration(importDeclaration, options)
191
+
192
+ expect(options.importsToRemove.size).toBe(0)
193
+ expect(options.importsToAdd.size).toBe(0)
194
+ expect(options.validRenames?.size).toBe(0)
195
+ })
196
+
197
+ it('should handle namespace imports gracefully', () => {
198
+ const options = createTestOptions()
199
+ const importDeclaration = getImportDeclaration(
200
+ `import * as Components from "@kaizen/components/v3/actions"`,
201
+ )
202
+
203
+ processImportDeclaration(importDeclaration, options)
204
+
205
+ expect(options.importsToRemove.size).toBe(0)
206
+ expect(options.importsToAdd.size).toBe(0)
207
+ expect(options.validRenames?.size).toBe(0)
208
+ })
209
+ })
@@ -0,0 +1,59 @@
1
+ import ts from 'typescript'
2
+ import { setImportToAdd, setImportToRemove, type UpdateKaioImportsArgs } from './updateKaioImports'
3
+
4
+ export type ModuleRenameConfig = {
5
+ newName: string
6
+ fromModules: string[]
7
+ toModule: string
8
+ }
9
+
10
+ export type ProcessImportOptions = {
11
+ importsToRemove: NonNullable<UpdateKaioImportsArgs['importsToRemove']>
12
+ importsToAdd: NonNullable<UpdateKaioImportsArgs['importsToAdd']>
13
+ renameMap: Map<string, ModuleRenameConfig>
14
+ validRenames?: Set<string>
15
+ }
16
+
17
+ /**
18
+ * Takes an import declaration and transforms it as specified in the options.renameMap
19
+ *
20
+ * @param node - The import declaration to process
21
+ * @param options - Configuration for import transformations
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // For an import like: import { OldButton } from '@kaizen/legacy'
26
+ * // With renameMap: { 'OldButton': { newName: 'Button', fromModules: ['@kaizen/legacy'], toModule: '@kaizen/button' } }
27
+ * // Results in: import { Button } from '@kaizen/button'
28
+ * ```
29
+ */
30
+ export const processImportDeclaration = (
31
+ node: ts.ImportDeclaration,
32
+ options: ProcessImportOptions,
33
+ ): void => {
34
+ const { importsToRemove, importsToAdd, renameMap, validRenames } = options
35
+ const moduleSpecifier = node.moduleSpecifier.getText().slice(1, -1)
36
+
37
+ const importClause = node.importClause
38
+ if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
39
+ importClause.namedBindings.elements.forEach((importSpecifier) => {
40
+ const importName = importSpecifier.propertyName?.getText() ?? importSpecifier.name?.getText()
41
+
42
+ const renameConfig = renameMap.get(importName)
43
+ if (renameConfig?.fromModules.includes(moduleSpecifier)) {
44
+ validRenames?.add(importName)
45
+ setImportToRemove(importsToRemove, moduleSpecifier, importName)
46
+
47
+ const alias = importSpecifier.propertyName?.getText()
48
+ ? importSpecifier.name?.getText()
49
+ : undefined
50
+
51
+ setImportToAdd(importsToAdd, renameConfig.toModule, {
52
+ componentName: renameConfig.newName,
53
+ alias,
54
+ isTypeOnly: importSpecifier.isTypeOnly || importClause.isTypeOnly,
55
+ })
56
+ }
57
+ })
58
+ }
59
+ }
@@ -0,0 +1,31 @@
1
+ import type { ModuleRenameConfig } from './createModulePathTransformer'
2
+
3
+ export type ComponentEntry = string | [string, string] // [oldName, newName]
4
+
5
+ export type ComponentGroup = {
6
+ components: ComponentEntry[]
7
+ fromModules: string[]
8
+ toModule: string
9
+ }
10
+
11
+ export const createRenameMapFromGroups = (
12
+ groups: ComponentGroup[],
13
+ ): Map<string, ModuleRenameConfig> => {
14
+ const renameMap = new Map<string, ModuleRenameConfig>()
15
+
16
+ groups.forEach(({ components, fromModules, toModule }) => {
17
+ components.forEach((componentEntry) => {
18
+ const [oldName, newName] = Array.isArray(componentEntry)
19
+ ? componentEntry
20
+ : [componentEntry, componentEntry]
21
+
22
+ renameMap.set(oldName, {
23
+ newName,
24
+ fromModules,
25
+ toModule,
26
+ })
27
+ })
28
+ })
29
+
30
+ return renameMap
31
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './createJsxElementWithChildren'
2
2
  export * from './createProp'
3
+ export * from './createRenameMapFromGroups'
3
4
  export * from './getPropValueText'
4
5
  export * from './getKaioTagName'
5
6
  export * from './migrateStringProp'
@@ -9,3 +10,5 @@ export * from './transformSource'
9
10
  export * from './updateKaioImports'
10
11
  export * from './updateJsxElementWithNewProps'
11
12
  export * from './transformV1ButtonPropsToButtonOrLinkButton'
13
+ export * from './updateJsxElementTagName'
14
+ export * from './createModulePathTransformer'
@@ -0,0 +1,129 @@
1
+ import ts from 'typescript'
2
+ import { parseJsx } from '../__tests__/utils'
3
+ import { updateJsxElementTagName, type ComponentRenameConfig } from './updateJsxElementTagName'
4
+
5
+ const componentRenameMap = new Map<string, ComponentRenameConfig>([
6
+ [
7
+ 'Pancakes',
8
+ {
9
+ newName: 'Beer',
10
+ fromModule: '@kaizen/components/next',
11
+ toModule: '@kaizen/components',
12
+ },
13
+ ],
14
+ ['Bart', { newName: 'Bender', fromModule: '@kaizen/components', toModule: '@kaizen/components' }],
15
+ [
16
+ 'EatMyShorts',
17
+ { newName: 'MeatBags', fromModule: '@kaizen/components', toModule: '@kaizen/components' },
18
+ ],
19
+ ])
20
+
21
+ describe('updateJsxElementTagName()', () => {
22
+ const factory = ts.factory
23
+
24
+ it('should ignore nodes not in componentMap', () => {
25
+ const elem = factory.createJsxSelfClosingElement(
26
+ factory.createIdentifier('Cowabunga'),
27
+ undefined,
28
+ factory.createJsxAttributes([]),
29
+ )
30
+
31
+ const result = updateJsxElementTagName(factory, elem, 'Cowabunga', componentRenameMap)
32
+
33
+ expect(ts.isJsxSelfClosingElement(result)).toBe(true)
34
+ expect((result.tagName as ts.Identifier).text).toBe('Cowabunga')
35
+ })
36
+
37
+ it('should update self closing tag', () => {
38
+ const elem = factory.createJsxSelfClosingElement(
39
+ factory.createIdentifier('Pancakes'),
40
+ undefined,
41
+ factory.createJsxAttributes([]),
42
+ )
43
+
44
+ const result = updateJsxElementTagName(factory, elem, 'Beer', componentRenameMap)
45
+
46
+ expect(ts.isJsxSelfClosingElement(result)).toBe(true)
47
+ expect((result.tagName as ts.Identifier).text).toBe('Beer')
48
+ })
49
+
50
+ it('should handle Component.SubComponent when in Component is found in componentMap', () => {
51
+ const source = `<Bart.Shorts />`
52
+ const sourceFile = parseJsx(source)
53
+ const statement = sourceFile.statements[0] as ts.ExpressionStatement
54
+ const elem = statement.expression as ts.JsxSelfClosingElement
55
+
56
+ const result = updateJsxElementTagName(factory, elem, 'ignored', componentRenameMap)
57
+
58
+ expect(ts.isJsxSelfClosingElement(result)).toBe(true)
59
+ expect(ts.isPropertyAccessExpression(result.tagName)).toBe(true)
60
+
61
+ const tagName = result.tagName as ts.PropertyAccessExpression
62
+ expect((tagName.expression as ts.Identifier).text).toBe('Bender')
63
+ expect(tagName.name.text).toBe('Shorts')
64
+ })
65
+
66
+ it('should ignore Component.SubComponent when in Component is not in componentMap', () => {
67
+ const source = `<Not.Here />`
68
+ const sourceFile = parseJsx(source)
69
+ const statement = sourceFile.statements[0] as ts.ExpressionStatement
70
+ const elem = statement.expression as ts.JsxSelfClosingElement
71
+
72
+ const result = updateJsxElementTagName(factory, elem, 'ignored', componentRenameMap)
73
+
74
+ expect(ts.isJsxSelfClosingElement(result)).toBe(true)
75
+ expect(ts.isPropertyAccessExpression(result.tagName)).toBe(true)
76
+
77
+ const tagName = result.tagName as ts.PropertyAccessExpression
78
+ expect((tagName.expression as ts.Identifier).text).toBe('Not')
79
+ expect(tagName.name.text).toBe('Here')
80
+ })
81
+
82
+ it('should update JSX opening element tag name', () => {
83
+ const openingElement = factory.createJsxOpeningElement(
84
+ factory.createIdentifier('Pancakes'),
85
+ undefined,
86
+ factory.createJsxAttributes([]),
87
+ )
88
+
89
+ const result = updateJsxElementTagName(factory, openingElement, 'Pizza', componentRenameMap)
90
+
91
+ expect(ts.isJsxOpeningElement(result)).toBe(true)
92
+ expect((result.tagName as ts.Identifier).text).toBe('Pizza')
93
+ })
94
+
95
+ it('should update JSX closing element tag name', () => {
96
+ const closingElement = factory.createJsxClosingElement(factory.createIdentifier('EatMyShorts'))
97
+
98
+ const result = updateJsxElementTagName(factory, closingElement, 'MeatBags', componentRenameMap)
99
+
100
+ expect(ts.isJsxClosingElement(result)).toBe(true)
101
+ expect((result.tagName as ts.Identifier).text).toBe('MeatBags')
102
+ })
103
+
104
+ it('should preserve attributes when updating JSX elements', () => {
105
+ const attributes = factory.createJsxAttributes([
106
+ factory.createJsxAttribute(
107
+ factory.createIdentifier('className'),
108
+ factory.createStringLiteral('test-class'),
109
+ ),
110
+ ])
111
+
112
+ const element = factory.createJsxSelfClosingElement(
113
+ factory.createIdentifier('Pancakes'),
114
+ undefined,
115
+ attributes,
116
+ )
117
+
118
+ const result = updateJsxElementTagName(factory, element, 'Pizza', componentRenameMap)
119
+
120
+ expect(ts.isJsxSelfClosingElement(result)).toBe(true)
121
+ expect((result.tagName as ts.Identifier).text).toBe('Pizza')
122
+ expect((result as ts.JsxSelfClosingElement).attributes.properties.length).toBe(1)
123
+
124
+ const attribute = (result as ts.JsxSelfClosingElement).attributes
125
+ .properties[0] as ts.JsxAttribute
126
+ expect((attribute.name as ts.Identifier).text).toBe('className')
127
+ expect((attribute.initializer as ts.StringLiteral).text).toBe('test-class')
128
+ })
129
+ })
@@ -0,0 +1,56 @@
1
+ import ts from 'typescript'
2
+
3
+ export type ComponentRenameConfig = {
4
+ newName: string
5
+ fromModule: string
6
+ toModule: string
7
+ }
8
+
9
+ export const updateJsxElementTagName = (
10
+ factory: ts.NodeFactory,
11
+ node: ts.JsxOpeningElement | ts.JsxClosingElement | ts.JsxSelfClosingElement,
12
+ newTagName: string,
13
+ componentRenameMap: Map<string, ComponentRenameConfig>,
14
+ ): ts.JsxOpeningElement | ts.JsxClosingElement | ts.JsxSelfClosingElement => {
15
+ let newTagNameExpr: ts.JsxTagNameExpression
16
+
17
+ if (ts.isPropertyAccessExpression(node.tagName)) {
18
+ const baseComponentName = node.tagName.expression.getText()
19
+ const rename = componentRenameMap.get(baseComponentName)
20
+
21
+ if (rename) {
22
+ newTagNameExpr = factory.createPropertyAccessExpression(
23
+ factory.createIdentifier(rename.newName),
24
+ node.tagName.name,
25
+ ) as ts.JsxTagNameExpression
26
+ } else {
27
+ newTagNameExpr = node.tagName
28
+ }
29
+ } else {
30
+ newTagNameExpr = factory.createIdentifier(newTagName)
31
+ }
32
+
33
+ if (ts.isJsxSelfClosingElement(node)) {
34
+ return factory.updateJsxSelfClosingElement(
35
+ node,
36
+ newTagNameExpr,
37
+ node.typeArguments,
38
+ node.attributes,
39
+ )
40
+ }
41
+
42
+ if (ts.isJsxOpeningElement(node)) {
43
+ return factory.updateJsxOpeningElement(
44
+ node,
45
+ newTagNameExpr,
46
+ node.typeArguments,
47
+ node.attributes,
48
+ )
49
+ }
50
+
51
+ if (ts.isJsxClosingElement(node)) {
52
+ return factory.updateJsxClosingElement(node, newTagNameExpr)
53
+ }
54
+
55
+ return node
56
+ }
@@ -200,6 +200,88 @@ describe('updateKaioImports()', () => {
200
200
  }),
201
201
  ).toEqual(printAst(outputAst))
202
202
  })
203
+
204
+ describe('type-only imports', () => {
205
+ it('creates a new type-only import declaration', () => {
206
+ const inputAst = parseJsx(`
207
+ import { Well } from "@kaizen/components"
208
+ `)
209
+ const outputAst = parseJsx(`
210
+ import { Well } from "@kaizen/components"
211
+ import type { Card } from "@kaizen/components/next"
212
+ `)
213
+ expect(
214
+ transformInput(inputAst)({
215
+ importsToAdd: new Map([
216
+ [
217
+ '@kaizen/components/next',
218
+ new Map([['Card', { componentName: 'Card', isTypeOnly: true }]]),
219
+ ],
220
+ ]),
221
+ }),
222
+ ).toEqual(printAst(outputAst))
223
+ })
224
+
225
+ it('adds type-only import to existing regular imports', () => {
226
+ const inputAst = parseJsx(`
227
+ import { Select } from "@kaizen/components/next"
228
+ `)
229
+ const outputAst = parseJsx(`
230
+ import { Select, type Card } from "@kaizen/components/next"
231
+ `)
232
+ expect(
233
+ transformInput(inputAst)({
234
+ importsToAdd: new Map([
235
+ [
236
+ '@kaizen/components/next',
237
+ new Map([['Card', { componentName: 'Card', isTypeOnly: true }]]),
238
+ ],
239
+ ]),
240
+ }),
241
+ ).toEqual(printAst(outputAst))
242
+ })
243
+
244
+ it('adds type-only import to existing type-only imports', () => {
245
+ const inputAst = parseJsx(`
246
+ import type { CardProps } from "@kaizen/components/next"
247
+ `)
248
+ const outputAst = parseJsx(`
249
+ import type { CardProps, type ButtonProps } from "@kaizen/components/next"
250
+ `)
251
+ expect(
252
+ transformInput(inputAst)({
253
+ importsToAdd: new Map([
254
+ [
255
+ '@kaizen/components/next',
256
+ new Map([['ButtonProps', { componentName: 'ButtonProps', isTypeOnly: true }]]),
257
+ ],
258
+ ]),
259
+ }),
260
+ ).toEqual(printAst(outputAst))
261
+ })
262
+
263
+ it('adds mix of type-only and regular imports', () => {
264
+ const inputAst = parseJsx(`
265
+ import { Select } from "@kaizen/components/next"
266
+ `)
267
+ const outputAst = parseJsx(`
268
+ import { Select, type CardProps, Button } from "@kaizen/components/next"
269
+ `)
270
+ expect(
271
+ transformInput(inputAst)({
272
+ importsToAdd: new Map([
273
+ [
274
+ '@kaizen/components/next',
275
+ new Map([
276
+ ['CardProps', { componentName: 'CardProps', isTypeOnly: true }],
277
+ ['Button', { componentName: 'Button', isTypeOnly: false }],
278
+ ]),
279
+ ],
280
+ ]),
281
+ }),
282
+ ).toEqual(printAst(outputAst))
283
+ })
284
+ })
203
285
  })
204
286
  })
205
287
  })
@@ -34,7 +34,7 @@ const removeNamedImports = (
34
34
  return node
35
35
  }
36
36
 
37
- type NewImportAttributes = { componentName: string; alias?: string }
37
+ type NewImportAttributes = { componentName: string; alias?: string; isTypeOnly?: boolean }
38
38
  type ImportsToAdd = Map<string, NewImportAttributes>
39
39
 
40
40
  const createImportDeclaration = (
@@ -42,9 +42,12 @@ const createImportDeclaration = (
42
42
  importsToAdd: ImportsToAdd,
43
43
  moduleSpecifier: string,
44
44
  ): ts.ImportDeclaration => {
45
- const namedImports = Array.from(importsToAdd.values()).map(({ componentName, alias }) =>
45
+ const imports = Array.from(importsToAdd.values())
46
+ const allTypeOnly = imports.every(({ isTypeOnly }) => isTypeOnly)
47
+
48
+ const namedImports = imports.map(({ componentName, alias, isTypeOnly }) =>
46
49
  factory.createImportSpecifier(
47
- false,
50
+ allTypeOnly ? false : (isTypeOnly ?? false),
48
51
  alias ? factory.createIdentifier(componentName) : undefined,
49
52
  factory.createIdentifier(alias ?? componentName),
50
53
  ),
@@ -52,7 +55,7 @@ const createImportDeclaration = (
52
55
 
53
56
  return factory.createImportDeclaration(
54
57
  undefined,
55
- factory.createImportClause(false, undefined, factory.createNamedImports(namedImports)),
58
+ factory.createImportClause(allTypeOnly, undefined, factory.createNamedImports(namedImports)),
56
59
  factory.createStringLiteral(moduleSpecifier),
57
60
  )
58
61
  }
@@ -75,9 +78,13 @@ const updateNamedImports = (
75
78
  })
76
79
  }
77
80
 
78
- Array.from(importsToAdd.values()).forEach(({ alias, componentName }) => {
81
+ const newImports = Array.from(importsToAdd.values())
82
+ const allNewTypeOnly = newImports.every(({ isTypeOnly }) => isTypeOnly)
83
+ const hasExistingImports = importSpecifiers.length > 0
84
+
85
+ newImports.forEach(({ alias, componentName, isTypeOnly }) => {
79
86
  const newImport = factory.createImportSpecifier(
80
- false,
87
+ !allNewTypeOnly || hasExistingImports ? (isTypeOnly ?? false) : false,
81
88
  alias ? factory.createIdentifier(componentName) : undefined,
82
89
  factory.createIdentifier(alias ?? componentName),
83
90
  )
@@ -87,12 +94,14 @@ const updateNamedImports = (
87
94
  }
88
95
  })
89
96
 
97
+ const isModuleLevelTypeOnly = allNewTypeOnly && !hasExistingImports
98
+
90
99
  return factory.updateImportDeclaration(
91
100
  node,
92
101
  node.modifiers,
93
102
  factory.updateImportClause(
94
103
  node.importClause,
95
- node.importClause.isTypeOnly,
104
+ isModuleLevelTypeOnly || node.importClause.isTypeOnly,
96
105
  node.importClause.name,
97
106
  factory.createNamedImports(importSpecifiers),
98
107
  ),