@wise/wds-codemods 0.0.1-experimental-2eb5228 → 0.0.1-experimental-0d8d466

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.
@@ -1,18 +1,17 @@
1
- import fs from 'node:fs/promises';
2
-
3
1
  import { execSync } from 'child_process';
4
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
5
4
 
6
5
  import { runCodemod } from '../runCodemod';
7
6
  import * as utils from '../utils';
8
- import { findClosestGitignore, hasPackageVersionFromPath } from '../utils/hasPackageVersion';
9
7
 
10
- // Mock dynamic import - only define once
11
- const mockImport = jest.fn();
12
8
  Object.defineProperty(globalThis, 'import', {
13
- value: mockImport,
9
+ value: {
10
+ meta: {
11
+ url: 'file:///current/file/path.js',
12
+ },
13
+ },
14
14
  writable: true,
15
- configurable: true,
16
15
  });
17
16
 
18
17
  jest.mock('node:fs/promises');
@@ -20,14 +19,10 @@ jest.mock('child_process', () => ({
20
19
  execSync: jest.fn(),
21
20
  }));
22
21
 
23
- jest.mock('../utils/hasPackageVersion', () => ({
24
- hasPackageVersionFromPath: jest.fn(),
25
- findClosestGitignore: jest.fn(() => null), // Return null to simulate no gitignore found
26
- }));
27
-
28
22
  jest.mock('path', () => ({
29
23
  resolve: jest.fn((...args: string[]) => args.join('/')),
30
24
  dirname: jest.fn((filePath: string) => {
25
+ // Simple implementation that mimics path.dirname
31
26
  const lastSlashIndex = filePath.lastIndexOf('/');
32
27
  return lastSlashIndex !== -1 ? filePath.substring(0, lastSlashIndex) : '.';
33
28
  }),
@@ -44,14 +39,6 @@ describe('runCodemod', () => {
44
39
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
45
40
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
46
41
 
47
- beforeEach(() => {
48
- jest.clearAllMocks();
49
- (fs.access as jest.Mock).mockRejectedValue(new Error('File not found'));
50
- (fs.readFile as jest.Mock).mockRejectedValue(new Error('File not found'));
51
- mockImport.mockResolvedValue({});
52
- (hasPackageVersionFromPath as jest.Mock).mockReturnValue(true);
53
- });
54
-
55
42
  afterEach(() => {
56
43
  jest.clearAllMocks();
57
44
  });
@@ -68,9 +55,6 @@ describe('runCodemod', () => {
68
55
  targetPath: 'src/',
69
56
  dry: true,
70
57
  print: false,
71
- gitignore: true,
72
- ignorePattern: undefined,
73
- isMonorepo: false,
74
58
  });
75
59
 
76
60
  await runCodemod(mockTransformsDir);
@@ -78,12 +62,12 @@ describe('runCodemod', () => {
78
62
  expect(utils.loadTransformModules).toHaveBeenCalledWith(mockTransformsDir);
79
63
  expect(utils.getOptions).toHaveBeenCalledWith(['transformA', 'transformB']);
80
64
  expect(execSync).toHaveBeenCalledWith(
81
- 'npx jscodeshift -t /mock/dist/transforms/transformA.js src/ --extensions=tsx,ts,jsx,js --ignore-pattern=**/node_modules/** --ignore-pattern=**/dist/** --ignore-pattern=**/*.d.ts --dry',
65
+ 'npx jscodeshift -t /mock/dist/transforms/transformA.js src/ --dry',
82
66
  { stdio: 'inherit' },
83
67
  );
84
68
  });
85
69
 
86
- it('should log error if no transform scripts found', async () => {
70
+ it('should throw error if no transform scripts found', async () => {
87
71
  (path.resolve as jest.Mock).mockReturnValue('/mock/dist/transforms');
88
72
  (utils.loadTransformModules as jest.Mock).mockResolvedValue({
89
73
  transformFiles: [],
@@ -91,10 +75,6 @@ describe('runCodemod', () => {
91
75
 
92
76
  await runCodemod('/mock/dist/transforms');
93
77
 
94
- expect(consoleErrorSpy).toHaveBeenCalledWith(
95
- 'Error running codemod:',
96
- 'No transform scripts found in directory: /mock/dist/transforms',
97
- );
98
78
  expect(execSync).not.toHaveBeenCalled();
99
79
  });
100
80
 
@@ -103,36 +83,14 @@ describe('runCodemod', () => {
103
83
 
104
84
  (utils.loadTransformModules as jest.Mock).mockRejectedValue(testError);
105
85
 
106
- await runCodemod('/mock/dist/transforms');
107
-
108
- expect(consoleErrorSpy).toHaveBeenCalledWith('Error running codemod:', testError.message);
109
- });
86
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
87
+ throw new Error(`Process exited with code ${code}`);
88
+ }) as never);
110
89
 
111
- // Add a test for when gitignore is found
112
- it('should include gitignore flag when gitignore file is found', async () => {
113
- const mockTransformsDir = '/mock/dist/transforms';
114
- const mockGitignorePath = '/path/to/.gitignore';
115
-
116
- (findClosestGitignore as jest.Mock).mockReturnValueOnce(mockGitignorePath);
117
- (path.resolve as jest.Mock).mockImplementation((...args: string[]) => args.join('/'));
118
- (utils.loadTransformModules as jest.Mock).mockResolvedValue({
119
- transformFiles: [Promise.resolve('transformA'), Promise.resolve('transformB')],
120
- });
121
- (utils.getOptions as jest.Mock).mockResolvedValue({
122
- transformFile: 'transformA',
123
- targetPath: 'src/',
124
- dry: true,
125
- print: false,
126
- gitignore: true,
127
- ignorePattern: undefined,
128
- isMonorepo: false,
129
- });
90
+ await runCodemod('/mock/dist/transforms').catch(() => {});
130
91
 
131
- await runCodemod(mockTransformsDir);
92
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error running codemod:', testError.message);
132
93
 
133
- expect(execSync).toHaveBeenCalledWith(
134
- `npx jscodeshift -t /mock/dist/transforms/transformA.js src/ --extensions=tsx,ts,jsx,js --ignore-pattern=**/node_modules/** --ignore-pattern=**/dist/** --ignore-pattern=**/*.d.ts --dry --ignore-config=${mockGitignorePath}`,
135
- { stdio: 'inherit' },
136
- );
94
+ mockExit.mockRestore();
137
95
  });
138
96
  });
package/src/runCodemod.ts CHANGED
@@ -7,26 +7,10 @@ import path from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
 
9
9
  import { getOptions, handleError, loadTransformModules } from './utils';
10
- import { findClosestGitignore, hasPackageVersionFromPath } from './utils/hasPackageVersion';
11
10
 
12
11
  const currentFilePath = fileURLToPath(import.meta.url);
13
12
  const currentDirPath = path.dirname(currentFilePath);
14
13
 
15
- // Function to get package requirements from a transform
16
- async function getPackageRequirements(
17
- transformPath: string,
18
- ): Promise<{ name: string; version: string }[]> {
19
- try {
20
- const transformModule = (await import(transformPath)) as {
21
- packageRequirements?: { name: string; version: string }[];
22
- };
23
- return transformModule.packageRequirements ?? [];
24
- } catch (error) {
25
- console.debug('Could not read package requirements from transform:', error);
26
- return [];
27
- }
28
- }
29
-
30
14
  async function runCodemod(transformsDir?: string) {
31
15
  try {
32
16
  const resolvedTransformsDir =
@@ -45,55 +29,10 @@ async function runCodemod(transformsDir?: string) {
45
29
  const codemodPath = path.resolve(resolvedTransformsDir, `${options.transformFile}.js`);
46
30
  console.debug(`Resolved codemod path: ${codemodPath}`);
47
31
 
48
- // Check package requirements from the transform
49
- const packageRequirements = await getPackageRequirements(codemodPath);
50
- const packageResults: Record<string, boolean> = {};
51
-
52
- if (packageRequirements.length > 0) {
53
- console.log('📦 Checking package requirements...');
54
-
55
- for (const pkg of packageRequirements) {
56
- console.log(`🔍 Checking for ${pkg.name}@${pkg.version} in ${options.targetPath}...`);
57
-
58
- // Pass the monorepo flag to the package checker
59
- const found = hasPackageVersionFromPath(
60
- pkg.name,
61
- pkg.version,
62
- options.targetPath,
63
- options.isMonorepo,
64
- );
65
- packageResults[`${pkg.name}@${pkg.version}`] = found;
66
-
67
- if (found) {
68
- console.log(`✅ Found ${pkg.name}@${pkg.version}`);
69
- } else {
70
- console.warn(
71
- `❌ Package ${pkg.name}@${pkg.version} not found in target. Transform may be skipped.`,
72
- );
73
- }
74
- }
75
- }
76
-
77
- let gitignoreArg = '';
78
- if (options.gitignore) {
79
- const closestGitignore = findClosestGitignore(options.targetPath);
80
- if (closestGitignore) {
81
- console.debug(`Found .gitignore at: ${closestGitignore}`);
82
- gitignoreArg = `--ignore-config=${closestGitignore}`;
83
- } else {
84
- console.debug('No .gitignore found in directory tree, skipping gitignore option');
85
- gitignoreArg = '';
86
- }
87
- }
88
-
89
32
  const args = [
90
33
  '-t',
91
34
  codemodPath,
92
35
  options.targetPath,
93
- '--extensions=tsx,ts,jsx,js',
94
- '--ignore-pattern=**/node_modules/**',
95
- '--ignore-pattern=**/dist/**',
96
- '--ignore-pattern=**/*.d.ts',
97
36
  options.dry ? '--dry' : '',
98
37
  options.print ? '--print' : '',
99
38
  options.ignorePattern
@@ -102,10 +41,7 @@ async function runCodemod(transformsDir?: string) {
102
41
  .map((pattern) => `--ignore-pattern=${pattern.trim()}`)
103
42
  .join(' ')
104
43
  : '',
105
- gitignoreArg,
106
- Object.keys(packageResults).length > 0
107
- ? `--packageResults='${JSON.stringify(packageResults)}'`
108
- : '',
44
+ options.gitignore ? '--gitignore' : '',
109
45
  ].filter(Boolean);
110
46
 
111
47
  const command = `npx jscodeshift ${args.join(' ')}`;
@@ -1,8 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
 
3
- import createTestTransform, {
4
- createTestTransformWithoutPackage,
5
- } from '../../helpers/createTestTransform';
3
+ import createTestTransform from '../../helpers/createTestTransform';
6
4
  import transformer from '../button';
7
5
 
8
6
  jest.mock(
@@ -14,10 +12,7 @@ jest.mock(
14
12
  );
15
13
 
16
14
  describe('button codemod', () => {
17
- const transform = createTestTransform(transformer, [
18
- { name: '@transferwise/components', version: '>=46.5.0' },
19
- ]);
20
-
15
+ const transform = createTestTransform(transformer);
21
16
  it('renames ActionButton to Button and adds v2 and size', () => {
22
17
  const input = `
23
18
  import { ActionButton } from '@transferwise/components';
@@ -38,7 +33,7 @@ describe('button codemod', () => {
38
33
  }
39
34
  `;
40
35
  const output = transform({ source: input });
41
- expect(output).toContain('v2');
36
+ expect(output).toContain('<Button v2 type="button">Click</Button>');
42
37
  });
43
38
 
44
39
  it('removes legacy props and adds new ones', () => {
@@ -155,21 +150,4 @@ describe('button codemod', () => {
155
150
  'Manual review required: prop "size" on <Button> has unsupported value "dynamicSize".',
156
151
  );
157
152
  });
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
153
  });
@@ -1,18 +1,16 @@
1
1
  import type { API, FileInfo, JSCodeshift, JSXIdentifier, Options } from 'jscodeshift';
2
2
 
3
3
  import reportManualReview from '../../utils/reportManualReview';
4
+ import hasImport from '../helpers/hasImport';
5
+ import processIconChildren from '../helpers/iconUtils';
4
6
  import {
5
7
  addAttributesIfMissing,
6
- createReporter,
7
8
  hasAttributeOnElement,
8
- hasImport,
9
- processIconChildren,
10
9
  setNameIfJSXIdentifier,
11
- validatePackageRequirements,
12
- } from '../helpers';
10
+ } from '../helpers/jsxElementUtils';
11
+ import { createReporter } from '../helpers/jsxReportingUtils';
13
12
 
14
13
  export const parser = 'tsx';
15
- export const packageRequirements = [{ name: '@transferwise/components', version: '>=46.5.0' }];
16
14
 
17
15
  interface LegacyProps {
18
16
  priority?: string;
@@ -84,16 +82,12 @@ const resolveType = (type?: string, htmlType?: string): string | null => {
84
82
  'secondary',
85
83
  'danger',
86
84
  'link',
87
- 'button',
88
- 'reset',
89
- 'submit',
90
85
  ];
91
86
  return type && legacyButtonTypes.includes(type) ? type : null;
92
87
  };
93
88
 
94
- const convertEnumValue = (value?: string): { converted: string | undefined; wasEnum: boolean } => {
95
- if (!value) return { converted: value, wasEnum: false };
96
-
89
+ const convertEnumValue = (value?: string): string | undefined => {
90
+ if (!value) return value;
97
91
  const strippedValue = value.replace(/^['"]|['"]$/gu, '');
98
92
  const enumMapping: Record<string, string> = {
99
93
  'Priority.SECONDARY': 'secondary',
@@ -102,18 +96,8 @@ const convertEnumValue = (value?: string): { converted: string | undefined; wasE
102
96
  'ControlType.NEGATIVE': 'negative',
103
97
  'ControlType.POSITIVE': 'positive',
104
98
  '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
99
  };
112
-
113
- const wasEnum = strippedValue in enumMapping;
114
- const converted = enumMapping[strippedValue] || strippedValue;
115
-
116
- return { converted, wasEnum };
100
+ return enumMapping[strippedValue] || strippedValue;
117
101
  };
118
102
 
119
103
  /**
@@ -127,10 +111,6 @@ const convertEnumValue = (value?: string): { converted: string | undefined; wasE
127
111
  * @returns {string} - The transformed source code.
128
112
  */
129
113
  const transformer = (file: FileInfo, api: API, options: Options) => {
130
- if (!validatePackageRequirements(options, packageRequirements)) {
131
- return file.source;
132
- }
133
-
134
114
  const j: JSCodeshift = api.jscodeshift;
135
115
  const root = j(file.source);
136
116
  const manualReviewIssues: string[] = [];
@@ -241,163 +221,144 @@ const transformer = (file: FileInfo, api: API, options: Options) => {
241
221
  addAttributesIfMissing(j, openingElement, [
242
222
  { attribute: j.jsxAttribute(j.jsxIdentifier('v2')), name: 'v2' },
243
223
  ]);
244
-
245
224
  processIconChildren(j, path.node.children, iconImports, openingElement);
246
225
 
247
226
  const legacyProps: LegacyProps = {};
248
227
  const legacyPropNames = ['priority', 'size', 'type', 'htmlType', 'sentiment'];
249
- const keepAttributes: typeof openingElement.attributes = [];
250
228
 
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
- ) {
229
+ openingElement.attributes?.forEach((attr) => {
230
+ if (attr.type === 'JSXAttribute' && attr.name && attr.name.type === 'JSXIdentifier') {
258
231
  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');
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;
336
241
  }
337
242
  }
338
-
339
- if (!migrated) keepAttributes.push(attr);
340
- } else {
341
- keepAttributes.push(attr);
342
243
  }
343
244
  });
344
245
 
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)));
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
+ );
354
255
  }
355
256
 
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
- }
257
+ if ('size' in legacyProps) {
258
+ const rawValue = legacyProps.size;
259
+ const resolved = resolveSize(rawValue);
260
+ const supportedSizes = ['xs', 'sm', 'md', 'lg', 'xl'];
366
261
 
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)));
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');
380
274
  }
381
- if (resolved === 'negative') {
382
- newAttributes.push(j.jsxAttribute(j.jsxIdentifier('sentiment'), j.literal('negative')));
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');
383
295
  }
384
296
  }
385
297
 
386
- const rawSentiment = legacyProps.sentiment;
387
- if (rawSentiment === 'negative') {
388
- newAttributes.push(j.jsxAttribute(j.jsxIdentifier('sentiment'), j.literal('negative')));
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
+ }
389
340
  }
390
341
 
391
- Array.prototype.push.apply(newAttributes, keepAttributes);
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
+ }
392
354
 
393
- // as/href logic as before
394
355
  let asIndex = -1;
395
356
  let asValue: string | null = null;
396
357
  let hrefExists = false;
397
358
  let asAmbiguous = false;
398
359
  let hrefAmbiguous = false;
399
360
 
400
- newAttributes.forEach((attr, index) => {
361
+ openingElement.attributes?.forEach((attr, index) => {
401
362
  if (attr.type === 'JSXAttribute' && attr.name) {
402
363
  if (attr.name.name === 'as') {
403
364
  if (attr.value) {
@@ -410,6 +371,7 @@ const transformer = (file: FileInfo, api: API, options: Options) => {
410
371
  }
411
372
  asIndex = index;
412
373
  }
374
+
413
375
  if (attr.name.name === 'href') {
414
376
  hrefExists = true;
415
377
  if (attr.value && attr.value.type !== 'StringLiteral') {
@@ -426,15 +388,18 @@ const transformer = (file: FileInfo, api: API, options: Options) => {
426
388
 
427
389
  if (asValue === 'a') {
428
390
  if (asIndex !== -1) {
429
- newAttributes.splice(asIndex, 1);
391
+ openingElement.attributes = openingElement.attributes?.filter(
392
+ (_, idx) => idx !== asIndex,
393
+ );
430
394
  }
431
395
  if (!hrefExists) {
432
- newAttributes.push(j.jsxAttribute(j.jsxIdentifier('href'), j.literal('#')));
396
+ openingElement.attributes = [
397
+ ...(openingElement.attributes ?? []),
398
+ j.jsxAttribute(j.jsxIdentifier('href'), j.literal('#')),
399
+ ];
433
400
  }
434
401
  }
435
402
 
436
- openingElement.attributes = newAttributes;
437
-
438
403
  if ((openingElement.attributes ?? []).some((attr) => attr.type === 'JSXSpreadAttribute')) {
439
404
  reporter.reportSpreadProps(path);
440
405
  }