@wise/wds-codemods 0.0.1-experimental-731cdc7 → 0.0.1-experimental-cbae00f
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.
- package/.changeset/better-impalas-drop.md +5 -0
- package/.changeset/config.json +13 -0
- package/.changeset/quick-mails-joke.md +128 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/actions/bootstrap/action.yml +49 -0
- package/.github/actions/commitlint/action.yml +27 -0
- package/.github/actions/test/action.yml +23 -0
- package/.github/workflows/cd-cd.yml +127 -0
- package/.github/workflows/renovate.yml +16 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierignore +1 -0
- package/.prettierrc.js +5 -0
- package/DEVELOPER.md +783 -0
- package/babel.config.js +28 -0
- package/commitlint.config.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +135 -133
- package/dist/index.js.map +1 -1
- package/dist/transforms/button.d.ts +16 -0
- package/dist/transforms/button.js +566 -493
- package/dist/transforms/button.js.map +1 -1
- package/eslint.config.js +15 -0
- package/jest.config.js +9 -0
- package/mkdocs.yml +4 -0
- package/package.json +14 -19
- package/renovate.json +9 -0
- package/scripts/build.sh +10 -0
- package/src/__tests__/runCodemod.test.ts +96 -0
- package/src/index.ts +4 -0
- package/src/runCodemod.ts +88 -0
- package/src/transforms/button/__tests__/button.test.tsx +153 -0
- package/src/transforms/button/button.ts +418 -0
- package/src/transforms/helpers/__tests__/createTestTransform.test.ts +27 -0
- package/src/transforms/helpers/__tests__/hasImport.test.ts +52 -0
- package/src/transforms/helpers/__tests__/iconUtils.test.ts +207 -0
- package/src/transforms/helpers/__tests__/jsxElementUtils.test.ts +130 -0
- package/src/transforms/helpers/__tests__/jsxReportingUtils.test.ts +265 -0
- package/src/transforms/helpers/createTestTransform.ts +18 -0
- package/src/transforms/helpers/hasImport.ts +60 -0
- package/src/transforms/helpers/iconUtils.ts +87 -0
- package/src/transforms/helpers/index.ts +5 -0
- package/src/transforms/helpers/jsxElementUtils.ts +67 -0
- package/src/transforms/helpers/jsxReportingUtils.ts +224 -0
- package/src/utils/__tests__/getOptions.test.ts +170 -0
- package/src/utils/__tests__/handleError.test.ts +18 -0
- package/src/utils/__tests__/loadTransformModules.test.ts +51 -0
- package/src/utils/__tests__/reportManualReview.test.ts +42 -0
- package/src/utils/getOptions.ts +63 -0
- package/src/utils/handleError.ts +6 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/loadTransformModules.ts +28 -0
- package/src/utils/reportManualReview.ts +17 -0
- package/test-button.tsx +230 -0
- package/test-file.js +2 -0
- package/tsconfig.json +14 -0
- package/tsup.config.js +13 -0
- package/dist/reportManualReview-DQ00-OKx.js +0 -50
- package/dist/reportManualReview-DQ00-OKx.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('<Button v2 type="button">Click</Button>');
|
|
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,418 @@
|
|
|
1
|
+
import type { API, FileInfo, JSCodeshift, JSXIdentifier, Options } from 'jscodeshift';
|
|
2
|
+
|
|
3
|
+
import reportManualReview from '../../utils/reportManualReview';
|
|
4
|
+
import hasImport from '../helpers/hasImport';
|
|
5
|
+
import processIconChildren from '../helpers/iconUtils';
|
|
6
|
+
import {
|
|
7
|
+
addAttributesIfMissing,
|
|
8
|
+
hasAttributeOnElement,
|
|
9
|
+
setNameIfJSXIdentifier,
|
|
10
|
+
} from '../helpers/jsxElementUtils';
|
|
11
|
+
import { createReporter } from '../helpers/jsxReportingUtils';
|
|
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
|
+
];
|
|
86
|
+
return type && legacyButtonTypes.includes(type) ? type : null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const convertEnumValue = (value?: string): string | undefined => {
|
|
90
|
+
if (!value) return value;
|
|
91
|
+
const strippedValue = value.replace(/^['"]|['"]$/gu, '');
|
|
92
|
+
const enumMapping: Record<string, string> = {
|
|
93
|
+
'Priority.SECONDARY': 'secondary',
|
|
94
|
+
'Priority.PRIMARY': 'primary',
|
|
95
|
+
'Priority.TERTIARY': 'tertiary',
|
|
96
|
+
'ControlType.NEGATIVE': 'negative',
|
|
97
|
+
'ControlType.POSITIVE': 'positive',
|
|
98
|
+
'ControlType.ACCENT': 'accent',
|
|
99
|
+
};
|
|
100
|
+
return enumMapping[strippedValue] || strippedValue;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* This transform function modifies the Button and ActionButton components from the @transferwise/components library.
|
|
105
|
+
* It updates the ActionButton component to use the Button component with specific attributes and mappings.
|
|
106
|
+
* It also processes icon children and removes legacy props.
|
|
107
|
+
*
|
|
108
|
+
* @param {FileInfo} file - The file information object.
|
|
109
|
+
* @param {API} api - The API object for jscodeshift.
|
|
110
|
+
* @param {Options} options - The options object for jscodeshift.
|
|
111
|
+
* @returns {string} - The transformed source code.
|
|
112
|
+
*/
|
|
113
|
+
const transformer = (file: FileInfo, api: API, options: Options) => {
|
|
114
|
+
const j: JSCodeshift = api.jscodeshift;
|
|
115
|
+
const root = j(file.source);
|
|
116
|
+
const manualReviewIssues: string[] = [];
|
|
117
|
+
|
|
118
|
+
// Create reporter instance
|
|
119
|
+
const reporter = createReporter(j, manualReviewIssues);
|
|
120
|
+
|
|
121
|
+
const { exists: hasButtonImport } = hasImport(root, '@transferwise/components', 'Button', j);
|
|
122
|
+
const { exists: hasActionButtonImport, remove: removeActionButtonImport } = hasImport(
|
|
123
|
+
root,
|
|
124
|
+
'@transferwise/components',
|
|
125
|
+
'ActionButton',
|
|
126
|
+
j,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const iconImports = new Set<string>();
|
|
130
|
+
root.find(j.ImportDeclaration, { source: { value: '@transferwise/icons' } }).forEach((path) => {
|
|
131
|
+
path.node.specifiers?.forEach((specifier) => {
|
|
132
|
+
if (
|
|
133
|
+
(specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportSpecifier') &&
|
|
134
|
+
specifier.local
|
|
135
|
+
) {
|
|
136
|
+
const localName = (specifier.local as { name: string }).name;
|
|
137
|
+
iconImports.add(localName);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (hasActionButtonImport) {
|
|
143
|
+
root.findJSXElements('ActionButton').forEach((path) => {
|
|
144
|
+
const { openingElement, closingElement } = path.node;
|
|
145
|
+
|
|
146
|
+
openingElement.name = setNameIfJSXIdentifier(openingElement.name, 'Button')!;
|
|
147
|
+
if (closingElement) {
|
|
148
|
+
closingElement.name = setNameIfJSXIdentifier(closingElement.name, 'Button')!;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
addAttributesIfMissing(j, openingElement, [
|
|
152
|
+
{ attribute: j.jsxAttribute(j.jsxIdentifier('v2')), name: 'v2' },
|
|
153
|
+
{ attribute: j.jsxAttribute(j.jsxIdentifier('size'), j.literal('sm')), name: 'size' },
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
processIconChildren(j, path.node.children, iconImports, openingElement);
|
|
157
|
+
|
|
158
|
+
if ((openingElement.attributes ?? []).some((attr) => attr.type === 'JSXSpreadAttribute')) {
|
|
159
|
+
reporter.reportSpreadProps(path);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const legacyPropNames = ['priority', 'text'];
|
|
163
|
+
const legacyProps: LegacyProps = {};
|
|
164
|
+
|
|
165
|
+
openingElement.attributes?.forEach((attr) => {
|
|
166
|
+
if (attr.type === 'JSXAttribute' && attr.name && attr.name.type === 'JSXIdentifier') {
|
|
167
|
+
const { name } = attr.name;
|
|
168
|
+
if (legacyPropNames.includes(name)) {
|
|
169
|
+
if (attr.value) {
|
|
170
|
+
if (attr.value.type === 'StringLiteral') {
|
|
171
|
+
legacyProps[name] = attr.value.value;
|
|
172
|
+
} else if (attr.value.type === 'JSXExpressionContainer') {
|
|
173
|
+
reporter.reportAttribute(attr, path);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const hasTextProp = openingElement.attributes?.some(
|
|
181
|
+
(attr) =>
|
|
182
|
+
attr.type === 'JSXAttribute' &&
|
|
183
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
184
|
+
attr.name.name === 'text',
|
|
185
|
+
);
|
|
186
|
+
const hasChildren = path.node.children?.some(
|
|
187
|
+
(child) =>
|
|
188
|
+
(child.type === 'JSXText' && child.value.trim() !== '') ||
|
|
189
|
+
child.type === 'JSXElement' ||
|
|
190
|
+
child.type === 'JSXExpressionContainer',
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (hasTextProp && hasChildren) {
|
|
194
|
+
reporter.reportPropWithChildren(path, 'text');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
(path.node.children || []).forEach((child) => {
|
|
198
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
199
|
+
const expr = child.expression;
|
|
200
|
+
if (
|
|
201
|
+
expr.type === 'ConditionalExpression' ||
|
|
202
|
+
expr.type === 'CallExpression' ||
|
|
203
|
+
expr.type === 'Identifier' ||
|
|
204
|
+
expr.type === 'MemberExpression'
|
|
205
|
+
) {
|
|
206
|
+
reporter.reportAmbiguousChildren(path, 'icon');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
removeActionButtonImport();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (hasButtonImport) {
|
|
216
|
+
root.findJSXElements('Button').forEach((path) => {
|
|
217
|
+
const { openingElement } = path.node;
|
|
218
|
+
|
|
219
|
+
if (hasAttributeOnElement(openingElement, 'v2')) return;
|
|
220
|
+
|
|
221
|
+
addAttributesIfMissing(j, openingElement, [
|
|
222
|
+
{ attribute: j.jsxAttribute(j.jsxIdentifier('v2')), name: 'v2' },
|
|
223
|
+
]);
|
|
224
|
+
processIconChildren(j, path.node.children, iconImports, openingElement);
|
|
225
|
+
|
|
226
|
+
const legacyProps: LegacyProps = {};
|
|
227
|
+
const legacyPropNames = ['priority', 'size', 'type', 'htmlType', 'sentiment'];
|
|
228
|
+
|
|
229
|
+
openingElement.attributes?.forEach((attr) => {
|
|
230
|
+
if (attr.type === 'JSXAttribute' && attr.name && attr.name.type === 'JSXIdentifier') {
|
|
231
|
+
const { name } = attr.name;
|
|
232
|
+
if (legacyPropNames.includes(name)) {
|
|
233
|
+
if (attr.value) {
|
|
234
|
+
if (attr.value.type === 'StringLiteral') {
|
|
235
|
+
legacyProps[name] = attr.value.value;
|
|
236
|
+
} else if (attr.value.type === 'JSXExpressionContainer') {
|
|
237
|
+
legacyProps[name] = convertEnumValue(String(j(attr.value.expression).toSource()));
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
legacyProps[name] = undefined;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (openingElement.attributes) {
|
|
247
|
+
openingElement.attributes = openingElement.attributes.filter(
|
|
248
|
+
(attr) =>
|
|
249
|
+
!(
|
|
250
|
+
attr.type === 'JSXAttribute' &&
|
|
251
|
+
attr.name &&
|
|
252
|
+
legacyPropNames.includes((attr.name as JSXIdentifier).name)
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if ('size' in legacyProps) {
|
|
258
|
+
const rawValue = legacyProps.size;
|
|
259
|
+
const resolved = resolveSize(rawValue);
|
|
260
|
+
const supportedSizes = ['xs', 'sm', 'md', 'lg', 'xl'];
|
|
261
|
+
|
|
262
|
+
if (
|
|
263
|
+
typeof rawValue === 'string' &&
|
|
264
|
+
typeof resolved === 'string' &&
|
|
265
|
+
supportedSizes.includes(resolved)
|
|
266
|
+
) {
|
|
267
|
+
openingElement.attributes?.push(
|
|
268
|
+
j.jsxAttribute(j.jsxIdentifier('size'), j.literal(resolved)),
|
|
269
|
+
);
|
|
270
|
+
} else if (typeof rawValue === 'string') {
|
|
271
|
+
reporter.reportUnsupportedValue(path, 'size', rawValue);
|
|
272
|
+
} else if (rawValue !== undefined) {
|
|
273
|
+
reporter.reportAmbiguousExpression(path, 'size');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if ('priority' in legacyProps) {
|
|
278
|
+
const rawValue = legacyProps.priority;
|
|
279
|
+
const converted = convertEnumValue(rawValue);
|
|
280
|
+
const mapped = resolvePriority(legacyProps.type, converted);
|
|
281
|
+
const supportedPriorities = ['primary', 'secondary', 'tertiary', 'secondary-neutral'];
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
typeof rawValue === 'string' &&
|
|
285
|
+
typeof mapped === 'string' &&
|
|
286
|
+
supportedPriorities.includes(mapped)
|
|
287
|
+
) {
|
|
288
|
+
openingElement.attributes?.push(
|
|
289
|
+
j.jsxAttribute(j.jsxIdentifier('priority'), j.literal(mapped)),
|
|
290
|
+
);
|
|
291
|
+
} else if (typeof rawValue === 'string') {
|
|
292
|
+
reporter.reportUnsupportedValue(path, 'priority', rawValue);
|
|
293
|
+
} else if (rawValue !== undefined) {
|
|
294
|
+
reporter.reportAmbiguousExpression(path, 'priority');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if ('type' in legacyProps || 'htmlType' in legacyProps) {
|
|
299
|
+
const rawType = legacyProps.type;
|
|
300
|
+
const rawHtmlType = legacyProps.htmlType;
|
|
301
|
+
|
|
302
|
+
const resolvedType =
|
|
303
|
+
typeof rawType === 'string'
|
|
304
|
+
? rawType
|
|
305
|
+
: rawType && typeof rawType === 'object'
|
|
306
|
+
? convertEnumValue(j(rawType).toSource())
|
|
307
|
+
: undefined;
|
|
308
|
+
|
|
309
|
+
const resolved = resolveType(resolvedType, rawHtmlType);
|
|
310
|
+
|
|
311
|
+
const supportedTypes = [
|
|
312
|
+
'accent',
|
|
313
|
+
'negative',
|
|
314
|
+
'positive',
|
|
315
|
+
'primary',
|
|
316
|
+
'pay',
|
|
317
|
+
'secondary',
|
|
318
|
+
'danger',
|
|
319
|
+
'link',
|
|
320
|
+
'submit',
|
|
321
|
+
'button',
|
|
322
|
+
'reset',
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
if (typeof resolved === 'string' && supportedTypes.includes(resolved)) {
|
|
326
|
+
openingElement.attributes?.push(
|
|
327
|
+
j.jsxAttribute(j.jsxIdentifier('type'), j.literal(resolved)),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (resolved === 'negative') {
|
|
331
|
+
openingElement.attributes?.push(
|
|
332
|
+
j.jsxAttribute(j.jsxIdentifier('sentiment'), j.literal('negative')),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
} else if (typeof rawType === 'string' || typeof rawHtmlType === 'string') {
|
|
336
|
+
reporter.reportUnsupportedValue(path, 'type', rawType ?? rawHtmlType ?? '');
|
|
337
|
+
} else if (rawType !== undefined || rawHtmlType !== undefined) {
|
|
338
|
+
reporter.reportAmbiguousExpression(path, 'type');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if ('sentiment' in legacyProps) {
|
|
343
|
+
const rawValue = legacyProps.sentiment;
|
|
344
|
+
if (rawValue === 'negative') {
|
|
345
|
+
openingElement.attributes?.push(
|
|
346
|
+
j.jsxAttribute(j.jsxIdentifier('sentiment'), j.literal('negative')),
|
|
347
|
+
);
|
|
348
|
+
} else if (typeof rawValue === 'string') {
|
|
349
|
+
reporter.reportUnsupportedValue(path, 'sentiment', rawValue);
|
|
350
|
+
} else if (rawValue !== undefined) {
|
|
351
|
+
reporter.reportAmbiguousExpression(path, 'sentiment');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let asIndex = -1;
|
|
356
|
+
let asValue: string | null = null;
|
|
357
|
+
let hrefExists = false;
|
|
358
|
+
let asAmbiguous = false;
|
|
359
|
+
let hrefAmbiguous = false;
|
|
360
|
+
|
|
361
|
+
openingElement.attributes?.forEach((attr, index) => {
|
|
362
|
+
if (attr.type === 'JSXAttribute' && attr.name) {
|
|
363
|
+
if (attr.name.name === 'as') {
|
|
364
|
+
if (attr.value) {
|
|
365
|
+
if (attr.value.type === 'StringLiteral') {
|
|
366
|
+
asValue = attr.value.value;
|
|
367
|
+
} else if (attr.value.type === 'JSXExpressionContainer') {
|
|
368
|
+
asAmbiguous = true;
|
|
369
|
+
reporter.reportAttribute(attr, path);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
asIndex = index;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (attr.name.name === 'href') {
|
|
376
|
+
hrefExists = true;
|
|
377
|
+
if (attr.value && attr.value.type !== 'StringLiteral') {
|
|
378
|
+
hrefAmbiguous = true;
|
|
379
|
+
reporter.reportAttribute(attr, path);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
if (asValue && asValue !== 'a') {
|
|
386
|
+
reporter.reportUnsupportedValue(path, 'as', asValue);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (asValue === 'a') {
|
|
390
|
+
if (asIndex !== -1) {
|
|
391
|
+
openingElement.attributes = openingElement.attributes?.filter(
|
|
392
|
+
(_, idx) => idx !== asIndex,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (!hrefExists) {
|
|
396
|
+
openingElement.attributes = [
|
|
397
|
+
...(openingElement.attributes ?? []),
|
|
398
|
+
j.jsxAttribute(j.jsxIdentifier('href'), j.literal('#')),
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if ((openingElement.attributes ?? []).some((attr) => attr.type === 'JSXSpreadAttribute')) {
|
|
404
|
+
reporter.reportSpreadProps(path);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (manualReviewIssues.length > 0) {
|
|
410
|
+
manualReviewIssues.forEach(async (issue) => {
|
|
411
|
+
await reportManualReview(file.path, issue);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return root.toSource();
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
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
|
+
});
|