@wise/wds-codemods 0.0.1-experimental-ccf69a3 → 0.0.1-experimental-cc95209

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