@wise/wds-codemods 0.0.1-experimental-6c2101b

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 (62) hide show
  1. package/.changeset/better-impalas-drop.md +5 -0
  2. package/.changeset/config.json +13 -0
  3. package/.github/CODEOWNERS +1 -0
  4. package/.github/actions/bootstrap/action.yml +49 -0
  5. package/.github/actions/commitlint/action.yml +27 -0
  6. package/.github/actions/test/action.yml +23 -0
  7. package/.github/workflows/cd-cd.yml +127 -0
  8. package/.github/workflows/renovate.yml +16 -0
  9. package/.husky/commit-msg +1 -0
  10. package/.husky/pre-commit +1 -0
  11. package/.nvmrc +1 -0
  12. package/.prettierignore +1 -0
  13. package/.prettierrc.js +5 -0
  14. package/README.md +184 -0
  15. package/babel.config.js +28 -0
  16. package/codemod-report.md +81 -0
  17. package/commitlint.config.js +3 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +2448 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/transforms/button.d.ts +20 -0
  22. package/dist/transforms/button.js +640 -0
  23. package/dist/transforms/button.js.map +1 -0
  24. package/eslint.config.js +15 -0
  25. package/jest.config.js +9 -0
  26. package/mkdocs.yml +4 -0
  27. package/package.json +68 -0
  28. package/renovate.json +9 -0
  29. package/scripts/build.sh +10 -0
  30. package/src/__tests__/runCodemod.test.ts +109 -0
  31. package/src/index.ts +4 -0
  32. package/src/runCodemod.ts +149 -0
  33. package/src/transforms/button/__tests__/button.test.tsx +175 -0
  34. package/src/transforms/button/button.ts +453 -0
  35. package/src/transforms/helpers/__tests__/createTestTransform.test.ts +27 -0
  36. package/src/transforms/helpers/__tests__/hasImport.test.ts +52 -0
  37. package/src/transforms/helpers/__tests__/iconUtils.test.ts +207 -0
  38. package/src/transforms/helpers/__tests__/jsxElementUtils.test.ts +130 -0
  39. package/src/transforms/helpers/__tests__/jsxReportingUtils.test.ts +265 -0
  40. package/src/transforms/helpers/__tests__/packageValidation.test.ts +45 -0
  41. package/src/transforms/helpers/createTestTransform.ts +59 -0
  42. package/src/transforms/helpers/hasImport.ts +60 -0
  43. package/src/transforms/helpers/iconUtils.ts +87 -0
  44. package/src/transforms/helpers/index.ts +5 -0
  45. package/src/transforms/helpers/jsxElementUtils.ts +67 -0
  46. package/src/transforms/helpers/jsxReportingUtils.ts +224 -0
  47. package/src/transforms/helpers/packageValidation.ts +53 -0
  48. package/src/utils/__tests__/getOptions.test.ts +219 -0
  49. package/src/utils/__tests__/handleError.test.ts +18 -0
  50. package/src/utils/__tests__/hasPackageVersion.test.ts +191 -0
  51. package/src/utils/__tests__/loadTransformModules.test.ts +51 -0
  52. package/src/utils/__tests__/reportManualReview.test.ts +42 -0
  53. package/src/utils/getOptions.ts +78 -0
  54. package/src/utils/handleError.ts +6 -0
  55. package/src/utils/hasPackageVersion.ts +482 -0
  56. package/src/utils/index.ts +4 -0
  57. package/src/utils/loadTransformModules.ts +28 -0
  58. package/src/utils/reportManualReview.ts +17 -0
  59. package/test-button.tsx +230 -0
  60. package/test-file.js +2 -0
  61. package/tsconfig.json +14 -0
  62. package/tsup.config.js +13 -0
@@ -0,0 +1,175 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ import createTestTransform, {
4
+ createTestTransformWithoutPackage,
5
+ } from '../../helpers/createTestTransform';
6
+ import transformer from '../button';
7
+
8
+ jest.mock(
9
+ '../../../utils',
10
+ (): Record<string, unknown> => ({
11
+ ...jest.requireActual('../../../utils'),
12
+ reportManualReview: jest.fn(),
13
+ }),
14
+ );
15
+
16
+ describe('button codemod', () => {
17
+ const transform = createTestTransform(transformer, [
18
+ { name: '@transferwise/components', version: '>=46.5.0' },
19
+ ]);
20
+
21
+ it('renames ActionButton to Button and adds v2 and size', () => {
22
+ const input = `
23
+ import { ActionButton } from '@transferwise/components';
24
+ export default function Test() {
25
+ return <ActionButton>Click</ActionButton>;
26
+ }
27
+ `;
28
+ const output = transform({ source: input });
29
+ expect(output).toContain('<Button v2 size="sm">Click</Button>');
30
+ expect(output).not.toContain('ActionButton');
31
+ });
32
+
33
+ it('adds v2 to Button if missing', () => {
34
+ const input = `
35
+ import { Button } from '@transferwise/components';
36
+ export default function Test() {
37
+ return <Button htmlType="button">Click</Button>;
38
+ }
39
+ `;
40
+ const output = transform({ source: input });
41
+ expect(output).toContain('v2');
42
+ });
43
+
44
+ it('removes legacy props and adds new ones', () => {
45
+ const input = `
46
+ import { Button } from '@transferwise/components';
47
+ export default function Test() {
48
+ return (
49
+ <Button priority="primary" size="xs" type="accent" htmlType="submit" sentiment="positive">Test</Button>
50
+ );
51
+ }
52
+ `;
53
+ const output = transform({ source: input });
54
+ expect(output).toContain('size="sm"');
55
+ expect(output).toContain('priority="primary"');
56
+ expect(output).toContain('type="submit"');
57
+ expect(output).not.toContain(
58
+ 'priority="primary" size="xs" type="accent" htmlType="submit" sentiment="positive"',
59
+ );
60
+ });
61
+
62
+ it('converts enum props to string values', () => {
63
+ const input = `
64
+ import { Button } from '@transferwise/components';
65
+ export default function Test() {
66
+ return (
67
+ <Button priority={Priority.SECONDARY} type={ControlType.NEGATIVE}>Secondary Negative</Button>
68
+ );
69
+ }
70
+ `;
71
+ const output = transform({ source: input });
72
+ expect(output).toContain('priority="secondary"');
73
+ expect(output).toContain('sentiment="negative"');
74
+ });
75
+
76
+ it('handles htmlType="submit" and legacy type/priority enums', () => {
77
+ const input = `
78
+ import { Button } from '@transferwise/components';
79
+ export default function Test() {
80
+ return (
81
+ <Button htmlType="submit" type={ControlType.ACCENT} priority={Priority.PRIMARY} disabled>
82
+ Button Disabled
83
+ </Button>
84
+ );
85
+ }
86
+ `;
87
+ const output = transform({ source: input });
88
+ expect(output).toContain('type="submit"');
89
+ expect(output).toContain('priority="primary"');
90
+ expect(output).toContain('disabled');
91
+ expect(output).not.toContain('htmlType');
92
+ });
93
+
94
+ it('removes legacy props and keeps style and other non-legacy props', () => {
95
+ const input = `
96
+ import { Button } from '@transferwise/components';
97
+ export default function Test() {
98
+ return (
99
+ <Button type={ControlType.NEGATIVE} style={{ color: 'red' }} priority={Priority.PRIMARY}>
100
+ Primary Negative
101
+ </Button>
102
+ );
103
+ }
104
+ `;
105
+ const output = transform({ source: input });
106
+ expect(output).toContain('sentiment="negative"');
107
+ expect(output).toContain('priority="primary"');
108
+ expect(output).toContain("style={{ color: 'red' }}");
109
+ expect(output).not.toContain('ControlType');
110
+ expect(output).not.toContain('Priority');
111
+ });
112
+
113
+ it('adds v2 to Button if missing and keeps block/as/href', () => {
114
+ const input = `
115
+ import { Button } from '@transferwise/components';
116
+ export default function Test() {
117
+ return (
118
+ <Button as="a" href="/account" block>
119
+ Go to account
120
+ </Button>
121
+ );
122
+ }
123
+ `;
124
+ const output = transform({ source: input });
125
+ expect(output).toContain('v2');
126
+ expect(output).not.toContain('as="a"');
127
+ expect(output).toContain('href="/account"');
128
+ expect(output).toContain('block');
129
+ });
130
+
131
+ it('removes as="a" and adds href="#" if missing', () => {
132
+ const input = `
133
+ import { Button } from '@transferwise/components';
134
+ export default function Test() {
135
+ return (
136
+ <Button as="a">Link</Button>
137
+ );
138
+ }
139
+ `;
140
+ const output = transform({ source: input });
141
+ expect(output).not.toContain('as="a"');
142
+ expect(output).toContain('href="#"');
143
+ });
144
+
145
+ it('reports ambiguous size prop (identifier)', async () => {
146
+ const input = `
147
+ import { Button } from '@transferwise/components';
148
+ export default function Test() {
149
+ return <Button size={dynamicSize}>Test</Button>;
150
+ }
151
+ `;
152
+ transform({ source: input });
153
+ const report = await fs.readFile('codemod-report.txt', 'utf8');
154
+ expect(report).toContain(
155
+ 'Manual review required: prop "size" on <Button> has unsupported value "dynamicSize".',
156
+ );
157
+ });
158
+
159
+ it('skips transformation when package is not found', () => {
160
+ const noPackageTransform = createTestTransformWithoutPackage(
161
+ transformer,
162
+ '@transferwise/components',
163
+ '>=46.5.0',
164
+ );
165
+
166
+ const input = `
167
+ import { Button } from '@transferwise/components';
168
+ export default function Test() {
169
+ return <Button htmlType="button">Click</Button>;
170
+ }
171
+ `;
172
+ const output = noPackageTransform({ source: input });
173
+ expect(output.trim()).toBe(input.trim());
174
+ });
175
+ });
@@ -0,0 +1,453 @@
1
+ import type { API, FileInfo, JSCodeshift, JSXIdentifier, Options } from 'jscodeshift';
2
+
3
+ import reportManualReview from '../../utils/reportManualReview';
4
+ import {
5
+ addAttributesIfMissing,
6
+ createReporter,
7
+ hasAttributeOnElement,
8
+ hasImport,
9
+ processIconChildren,
10
+ setNameIfJSXIdentifier,
11
+ validatePackageRequirements,
12
+ } from '../helpers';
13
+
14
+ export const parser = 'tsx';
15
+ export const packageRequirements = [{ name: '@transferwise/components', version: '>=46.5.0' }];
16
+
17
+ interface LegacyProps {
18
+ priority?: string;
19
+ size?: string;
20
+ type?: string;
21
+ htmlType?: string;
22
+ sentiment?: string;
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ const priorityMapping: Record<string, Record<string, string>> = {
27
+ accent: {
28
+ primary: 'primary',
29
+ secondary: 'secondary-neutral',
30
+ tertiary: 'tertiary',
31
+ },
32
+ positive: {
33
+ primary: 'primary',
34
+ secondary: 'secondary-neutral',
35
+ tertiary: 'secondary-neutral',
36
+ },
37
+ negative: {
38
+ primary: 'primary',
39
+ secondary: 'secondary',
40
+ tertiary: 'secondary',
41
+ },
42
+ };
43
+
44
+ const sizeMap: Record<string, string> = {
45
+ EXTRA_SMALL: 'xs',
46
+ SMALL: 'sm',
47
+ MEDIUM: 'md',
48
+ LARGE: 'lg',
49
+ EXTRA_LARGE: 'xl',
50
+ xs: 'sm',
51
+ sm: 'sm',
52
+ md: 'md',
53
+ lg: 'lg',
54
+ xl: 'xl',
55
+ };
56
+
57
+ const resolveSize = (size?: string): string | undefined => {
58
+ if (!size) return size;
59
+ const match = /^Size\.(EXTRA_SMALL|SMALL|MEDIUM|LARGE|EXTRA_LARGE)$/u.exec(size);
60
+ if (match) {
61
+ return sizeMap[match[1]];
62
+ }
63
+ return sizeMap[size] || size;
64
+ };
65
+
66
+ const resolvePriority = (type?: string, priority?: string): string | undefined => {
67
+ if (type && priority) {
68
+ return priorityMapping[type]?.[priority] || priority;
69
+ }
70
+ return priority;
71
+ };
72
+
73
+ const resolveType = (type?: string, htmlType?: string): string | null => {
74
+ if (htmlType) {
75
+ return htmlType;
76
+ }
77
+
78
+ const legacyButtonTypes = [
79
+ 'accent',
80
+ 'negative',
81
+ 'positive',
82
+ 'primary',
83
+ 'pay',
84
+ 'secondary',
85
+ 'danger',
86
+ 'link',
87
+ 'button',
88
+ 'reset',
89
+ 'submit',
90
+ ];
91
+ return type && legacyButtonTypes.includes(type) ? type : null;
92
+ };
93
+
94
+ const convertEnumValue = (value?: string): { converted: string | undefined; wasEnum: boolean } => {
95
+ if (!value) return { converted: value, wasEnum: false };
96
+
97
+ const strippedValue = value.replace(/^['"]|['"]$/gu, '');
98
+ const enumMapping: Record<string, string> = {
99
+ 'Priority.SECONDARY': 'secondary',
100
+ 'Priority.PRIMARY': 'primary',
101
+ 'Priority.TERTIARY': 'tertiary',
102
+ 'ControlType.NEGATIVE': 'negative',
103
+ 'ControlType.POSITIVE': 'positive',
104
+ 'ControlType.ACCENT': 'accent',
105
+ 'ControlType.BUTTON': 'button',
106
+ 'ControlType.RESET': 'reset',
107
+ 'HtmlType.SUBMIT': 'submit',
108
+ 'HtmlType.BUTTON': 'button',
109
+ 'HtmlType.RESET': 'reset',
110
+ 'Sentiment.NEGATIVE': 'negative',
111
+ };
112
+
113
+ const wasEnum = strippedValue in enumMapping;
114
+ const converted = enumMapping[strippedValue] || strippedValue;
115
+
116
+ return { converted, wasEnum };
117
+ };
118
+
119
+ /**
120
+ * This transform function modifies the Button and ActionButton components from the @transferwise/components library.
121
+ * It updates the ActionButton component to use the Button component with specific attributes and mappings.
122
+ * It also processes icon children and removes legacy props.
123
+ *
124
+ * @param {FileInfo} file - The file information object.
125
+ * @param {API} api - The API object for jscodeshift.
126
+ * @param {Options} options - The options object for jscodeshift.
127
+ * @returns {string} - The transformed source code.
128
+ */
129
+ const transformer = (file: FileInfo, api: API, options: Options) => {
130
+ if (!validatePackageRequirements(options, packageRequirements)) {
131
+ return file.source;
132
+ }
133
+
134
+ const j: JSCodeshift = api.jscodeshift;
135
+ const root = j(file.source);
136
+ const manualReviewIssues: string[] = [];
137
+
138
+ // Create reporter instance
139
+ const reporter = createReporter(j, manualReviewIssues);
140
+
141
+ const { exists: hasButtonImport } = hasImport(root, '@transferwise/components', 'Button', j);
142
+ const { exists: hasActionButtonImport, remove: removeActionButtonImport } = hasImport(
143
+ root,
144
+ '@transferwise/components',
145
+ 'ActionButton',
146
+ j,
147
+ );
148
+
149
+ const iconImports = new Set<string>();
150
+ root.find(j.ImportDeclaration, { source: { value: '@transferwise/icons' } }).forEach((path) => {
151
+ path.node.specifiers?.forEach((specifier) => {
152
+ if (
153
+ (specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportSpecifier') &&
154
+ specifier.local
155
+ ) {
156
+ const localName = (specifier.local as { name: string }).name;
157
+ iconImports.add(localName);
158
+ }
159
+ });
160
+ });
161
+
162
+ if (hasActionButtonImport) {
163
+ root.findJSXElements('ActionButton').forEach((path) => {
164
+ const { openingElement, closingElement } = path.node;
165
+
166
+ openingElement.name = setNameIfJSXIdentifier(openingElement.name, 'Button')!;
167
+ if (closingElement) {
168
+ closingElement.name = setNameIfJSXIdentifier(closingElement.name, 'Button')!;
169
+ }
170
+
171
+ addAttributesIfMissing(j, openingElement, [
172
+ { attribute: j.jsxAttribute(j.jsxIdentifier('v2')), name: 'v2' },
173
+ { attribute: j.jsxAttribute(j.jsxIdentifier('size'), j.literal('sm')), name: 'size' },
174
+ ]);
175
+
176
+ processIconChildren(j, path.node.children, iconImports, openingElement);
177
+
178
+ if ((openingElement.attributes ?? []).some((attr) => attr.type === 'JSXSpreadAttribute')) {
179
+ reporter.reportSpreadProps(path);
180
+ }
181
+
182
+ const legacyPropNames = ['priority', 'text'];
183
+ const legacyProps: LegacyProps = {};
184
+
185
+ openingElement.attributes?.forEach((attr) => {
186
+ if (attr.type === 'JSXAttribute' && attr.name && attr.name.type === 'JSXIdentifier') {
187
+ const { name } = attr.name;
188
+ if (legacyPropNames.includes(name)) {
189
+ if (attr.value) {
190
+ if (attr.value.type === 'StringLiteral') {
191
+ legacyProps[name] = attr.value.value;
192
+ } else if (attr.value.type === 'JSXExpressionContainer') {
193
+ reporter.reportAttribute(attr, path);
194
+ }
195
+ }
196
+ }
197
+ }
198
+ });
199
+
200
+ const hasTextProp = openingElement.attributes?.some(
201
+ (attr) =>
202
+ attr.type === 'JSXAttribute' &&
203
+ attr.name.type === 'JSXIdentifier' &&
204
+ attr.name.name === 'text',
205
+ );
206
+ const hasChildren = path.node.children?.some(
207
+ (child) =>
208
+ (child.type === 'JSXText' && child.value.trim() !== '') ||
209
+ child.type === 'JSXElement' ||
210
+ child.type === 'JSXExpressionContainer',
211
+ );
212
+
213
+ if (hasTextProp && hasChildren) {
214
+ reporter.reportPropWithChildren(path, 'text');
215
+ }
216
+
217
+ (path.node.children || []).forEach((child) => {
218
+ if (child.type === 'JSXExpressionContainer') {
219
+ const expr = child.expression;
220
+ if (
221
+ expr.type === 'ConditionalExpression' ||
222
+ expr.type === 'CallExpression' ||
223
+ expr.type === 'Identifier' ||
224
+ expr.type === 'MemberExpression'
225
+ ) {
226
+ reporter.reportAmbiguousChildren(path, 'icon');
227
+ }
228
+ }
229
+ });
230
+ });
231
+
232
+ removeActionButtonImport();
233
+ }
234
+
235
+ if (hasButtonImport) {
236
+ root.findJSXElements('Button').forEach((path) => {
237
+ const { openingElement } = path.node;
238
+
239
+ if (hasAttributeOnElement(openingElement, 'v2')) return;
240
+
241
+ addAttributesIfMissing(j, openingElement, [
242
+ { attribute: j.jsxAttribute(j.jsxIdentifier('v2')), name: 'v2' },
243
+ ]);
244
+
245
+ processIconChildren(j, path.node.children, iconImports, openingElement);
246
+
247
+ const legacyProps: LegacyProps = {};
248
+ const legacyPropNames = ['priority', 'size', 'type', 'htmlType', 'sentiment'];
249
+ const keepAttributes: typeof openingElement.attributes = [];
250
+
251
+ (openingElement.attributes ?? []).forEach((attr) => {
252
+ if (
253
+ attr.type === 'JSXAttribute' &&
254
+ attr.name &&
255
+ attr.name.type === 'JSXIdentifier' &&
256
+ legacyPropNames.includes(attr.name.name)
257
+ ) {
258
+ const { name } = attr.name;
259
+ let migrated = false;
260
+ let rawStringValue: string | undefined;
261
+
262
+ if (attr.value) {
263
+ if (attr.value.type === 'StringLiteral') {
264
+ rawStringValue = attr.value.value;
265
+ legacyProps[name] = attr.value.value;
266
+ } else if (attr.value.type === 'JSXExpressionContainer') {
267
+ rawStringValue = String(j(attr.value.expression).toSource());
268
+ const { converted } = convertEnumValue(rawStringValue);
269
+ legacyProps[name] = converted;
270
+ }
271
+ } else {
272
+ legacyProps[name] = undefined;
273
+ }
274
+
275
+ // Check if this is an enum value that should be removed
276
+ const { wasEnum } = convertEnumValue(rawStringValue);
277
+ if (rawStringValue && wasEnum) {
278
+ migrated = true; // Remove enum values
279
+ } else if (name === 'size') {
280
+ const rawValue = legacyProps.size;
281
+ const resolved = resolveSize(rawValue);
282
+ const supportedSizes = ['xs', 'sm', 'md', 'lg', 'xl'];
283
+ if (
284
+ typeof rawValue === 'string' &&
285
+ typeof resolved === 'string' &&
286
+ supportedSizes.includes(resolved)
287
+ ) {
288
+ migrated = true;
289
+ } else if (typeof rawValue === 'string') {
290
+ reporter.reportUnsupportedValue(path, 'size', rawValue);
291
+ } else if (rawValue !== undefined) {
292
+ reporter.reportAmbiguousExpression(path, 'size');
293
+ }
294
+ } else if (name === 'priority') {
295
+ const rawValue = legacyProps.priority;
296
+ const { converted } = convertEnumValue(rawValue);
297
+ const mapped = resolvePriority(legacyProps.type, converted);
298
+ const supportedPriorities = ['primary', 'secondary', 'tertiary', 'secondary-neutral'];
299
+ if (
300
+ typeof rawValue === 'string' &&
301
+ typeof mapped === 'string' &&
302
+ supportedPriorities.includes(mapped)
303
+ ) {
304
+ migrated = true;
305
+ } else if (typeof rawValue === 'string') {
306
+ reporter.reportUnsupportedValue(path, 'priority', rawValue);
307
+ } else if (rawValue !== undefined) {
308
+ reporter.reportAmbiguousExpression(path, 'priority');
309
+ }
310
+ } else if (name === 'type' || name === 'htmlType') {
311
+ const rawType = legacyProps.type;
312
+ const rawHtmlType = legacyProps.htmlType;
313
+ const resolvedType =
314
+ typeof rawType === 'string'
315
+ ? rawType
316
+ : rawType && typeof rawType === 'object'
317
+ ? convertEnumValue(j(rawType).toSource()).converted
318
+ : undefined;
319
+ const resolved = resolveType(resolvedType, rawHtmlType);
320
+ const supportedTypes = ['button', 'reset', 'submit'];
321
+ if (typeof resolved === 'string' && supportedTypes.includes(resolved)) {
322
+ migrated = true;
323
+ } else if (typeof rawType === 'string' || typeof rawHtmlType === 'string') {
324
+ reporter.reportUnsupportedValue(path, 'type', rawType ?? rawHtmlType ?? '');
325
+ } else if (rawType !== undefined || rawHtmlType !== undefined) {
326
+ reporter.reportAmbiguousExpression(path, 'type');
327
+ }
328
+ } else if (name === 'sentiment') {
329
+ const rawValue = legacyProps.sentiment;
330
+ if (rawValue === 'negative') {
331
+ migrated = true;
332
+ } else if (typeof rawValue === 'string') {
333
+ reporter.reportUnsupportedValue(path, 'sentiment', rawValue);
334
+ } else if (rawValue !== undefined) {
335
+ reporter.reportAmbiguousExpression(path, 'sentiment');
336
+ }
337
+ }
338
+
339
+ if (!migrated) keepAttributes.push(attr);
340
+ } else {
341
+ keepAttributes.push(attr);
342
+ }
343
+ });
344
+
345
+ const newAttributes = [];
346
+ const rawSize = legacyProps.size;
347
+ const resolvedSize = resolveSize(rawSize);
348
+ if (
349
+ typeof rawSize === 'string' &&
350
+ typeof resolvedSize === 'string' &&
351
+ ['xs', 'sm', 'md', 'lg', 'xl'].includes(resolvedSize)
352
+ ) {
353
+ newAttributes.push(j.jsxAttribute(j.jsxIdentifier('size'), j.literal(resolvedSize)));
354
+ }
355
+
356
+ const rawPriority = legacyProps.priority;
357
+ const { converted: convertedPriority } = convertEnumValue(rawPriority);
358
+ const mappedPriority = resolvePriority(legacyProps.type, convertedPriority);
359
+ if (
360
+ typeof rawPriority === 'string' &&
361
+ typeof mappedPriority === 'string' &&
362
+ ['primary', 'secondary', 'tertiary', 'secondary-neutral'].includes(mappedPriority)
363
+ ) {
364
+ newAttributes.push(j.jsxAttribute(j.jsxIdentifier('priority'), j.literal(mappedPriority)));
365
+ }
366
+
367
+ const rawType = legacyProps.type;
368
+ const rawHtmlType = legacyProps.htmlType;
369
+ const resolvedType =
370
+ typeof rawType === 'string'
371
+ ? rawType
372
+ : rawType && typeof rawType === 'object'
373
+ ? convertEnumValue(j(rawType).toSource()).converted
374
+ : undefined;
375
+ const resolved = resolveType(resolvedType, rawHtmlType);
376
+ const supportedTypes = ['button', 'reset', 'submit', 'negative'];
377
+ if (typeof resolved === 'string' && supportedTypes.includes(resolved)) {
378
+ if (resolved !== 'negative') {
379
+ newAttributes.push(j.jsxAttribute(j.jsxIdentifier('type'), j.literal(resolved)));
380
+ }
381
+ if (resolved === 'negative') {
382
+ newAttributes.push(j.jsxAttribute(j.jsxIdentifier('sentiment'), j.literal('negative')));
383
+ }
384
+ }
385
+
386
+ const rawSentiment = legacyProps.sentiment;
387
+ if (rawSentiment === 'negative') {
388
+ newAttributes.push(j.jsxAttribute(j.jsxIdentifier('sentiment'), j.literal('negative')));
389
+ }
390
+
391
+ Array.prototype.push.apply(newAttributes, keepAttributes);
392
+
393
+ // as/href logic as before
394
+ let asIndex = -1;
395
+ let asValue: string | null = null;
396
+ let hrefExists = false;
397
+ let asAmbiguous = false;
398
+ let hrefAmbiguous = false;
399
+
400
+ newAttributes.forEach((attr, index) => {
401
+ if (attr.type === 'JSXAttribute' && attr.name) {
402
+ if (attr.name.name === 'as') {
403
+ if (attr.value) {
404
+ if (attr.value.type === 'StringLiteral') {
405
+ asValue = attr.value.value;
406
+ } else if (attr.value.type === 'JSXExpressionContainer') {
407
+ asAmbiguous = true;
408
+ reporter.reportAttribute(attr, path);
409
+ }
410
+ }
411
+ asIndex = index;
412
+ }
413
+ if (attr.name.name === 'href') {
414
+ hrefExists = true;
415
+ if (attr.value && attr.value.type !== 'StringLiteral') {
416
+ hrefAmbiguous = true;
417
+ reporter.reportAttribute(attr, path);
418
+ }
419
+ }
420
+ }
421
+ });
422
+
423
+ if (asValue && asValue !== 'a') {
424
+ reporter.reportUnsupportedValue(path, 'as', asValue);
425
+ }
426
+
427
+ if (asValue === 'a') {
428
+ if (asIndex !== -1) {
429
+ newAttributes.splice(asIndex, 1);
430
+ }
431
+ if (!hrefExists) {
432
+ newAttributes.push(j.jsxAttribute(j.jsxIdentifier('href'), j.literal('#')));
433
+ }
434
+ }
435
+
436
+ openingElement.attributes = newAttributes;
437
+
438
+ if ((openingElement.attributes ?? []).some((attr) => attr.type === 'JSXSpreadAttribute')) {
439
+ reporter.reportSpreadProps(path);
440
+ }
441
+ });
442
+ }
443
+
444
+ if (manualReviewIssues.length > 0) {
445
+ manualReviewIssues.forEach(async (issue) => {
446
+ await reportManualReview(file.path, issue);
447
+ });
448
+ }
449
+
450
+ return root.toSource();
451
+ };
452
+
453
+ export default transformer;
@@ -0,0 +1,27 @@
1
+ import { applyTransform } from 'jscodeshift/src/testUtils';
2
+
3
+ import createTestTransform from '../createTestTransform';
4
+
5
+ jest.mock('jscodeshift/src/testUtils', () => ({
6
+ applyTransform: jest.fn(),
7
+ }));
8
+
9
+ describe('createTestTransform', () => {
10
+ it('should call applyTransform with the correct arguments', () => {
11
+ const mockTransformer = jest.fn();
12
+ const mockInput = { path: 'test-file.ts', source: 'const a = 1;' };
13
+ const mockOptions = {};
14
+ const mockTestOptions = { parser: 'tsx' };
15
+
16
+ const transform = createTestTransform(mockTransformer);
17
+
18
+ transform(mockInput);
19
+
20
+ expect(applyTransform).toHaveBeenCalledWith(
21
+ mockTransformer,
22
+ mockOptions,
23
+ mockInput,
24
+ mockTestOptions,
25
+ );
26
+ });
27
+ });