@wise/wds-codemods 0.0.1-experimental-4bee5c0 → 0.0.1-experimental-b974fa6

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 (51) hide show
  1. package/package.json +6 -1
  2. package/.changeset/better-impalas-drop.md +0 -5
  3. package/.changeset/config.json +0 -13
  4. package/.changeset/quick-mails-joke.md +0 -128
  5. package/.github/CODEOWNERS +0 -1
  6. package/.github/actions/bootstrap/action.yml +0 -49
  7. package/.github/actions/commitlint/action.yml +0 -27
  8. package/.github/actions/test/action.yml +0 -23
  9. package/.github/workflows/cd-cd.yml +0 -150
  10. package/.github/workflows/renovate.yml +0 -16
  11. package/.husky/commit-msg +0 -1
  12. package/.husky/pre-commit +0 -1
  13. package/.nvmrc +0 -1
  14. package/.prettierignore +0 -1
  15. package/.prettierrc.js +0 -5
  16. package/DEVELOPER.md +0 -783
  17. package/babel.config.js +0 -28
  18. package/commitlint.config.js +0 -3
  19. package/eslint.config.js +0 -15
  20. package/jest.config.js +0 -9
  21. package/mkdocs.yml +0 -4
  22. package/renovate.json +0 -9
  23. package/src/__tests__/runCodemod.test.ts +0 -96
  24. package/src/index.ts +0 -4
  25. package/src/runCodemod.ts +0 -88
  26. package/src/transforms/button/__tests__/button.test.tsx +0 -153
  27. package/src/transforms/button/transformer.ts +0 -418
  28. package/src/transforms/helpers/__tests__/createTestTransform.test.ts +0 -27
  29. package/src/transforms/helpers/__tests__/hasImport.test.ts +0 -52
  30. package/src/transforms/helpers/__tests__/iconUtils.test.ts +0 -207
  31. package/src/transforms/helpers/__tests__/jsxElementUtils.test.ts +0 -130
  32. package/src/transforms/helpers/__tests__/jsxReportingUtils.test.ts +0 -265
  33. package/src/transforms/helpers/createTestTransform.ts +0 -18
  34. package/src/transforms/helpers/hasImport.ts +0 -60
  35. package/src/transforms/helpers/iconUtils.ts +0 -87
  36. package/src/transforms/helpers/index.ts +0 -5
  37. package/src/transforms/helpers/jsxElementUtils.ts +0 -67
  38. package/src/transforms/helpers/jsxReportingUtils.ts +0 -224
  39. package/src/utils/__tests__/getOptions.test.ts +0 -170
  40. package/src/utils/__tests__/handleError.test.ts +0 -18
  41. package/src/utils/__tests__/loadTransformModules.test.ts +0 -51
  42. package/src/utils/__tests__/reportManualReview.test.ts +0 -42
  43. package/src/utils/getOptions.ts +0 -63
  44. package/src/utils/handleError.ts +0 -6
  45. package/src/utils/index.ts +0 -4
  46. package/src/utils/loadTransformModules.ts +0 -28
  47. package/src/utils/reportManualReview.ts +0 -17
  48. package/test-button.tsx +0 -230
  49. package/test-file.js +0 -2
  50. package/tsconfig.json +0 -14
  51. package/tsdown.config.js +0 -13
package/DEVELOPER.md DELETED
@@ -1,783 +0,0 @@
1
- # Developer Documentation
2
-
3
- A comprehensive guide for developing codemods for the Wise Design System.
4
-
5
- ## Table of Contents
6
-
7
- - [Getting Started](#getting-started)
8
- - [Transform Structure](#transform-structure)
9
- - [Helper Functions](#helper-functions)
10
- - [Testing](#testing)
11
- - [Package Requirements](#package-requirements)
12
- - [Manual Review System](#manual-review-system)
13
- - [Best Practices](#best-practices)
14
- - [Examples](#examples)
15
-
16
- ## Getting Started
17
-
18
- ### Prerequisites
19
-
20
- - Node.js (version specified in `.nvmrc`)
21
- - pnpm 9.15.4
22
- - Understanding of AST (Abstract Syntax Trees)
23
- - Basic knowledge of jscodeshift
24
-
25
- ### Development Setup
26
-
27
- ```bash
28
- # Install dependencies
29
- pnpm install
30
-
31
- # Build the project
32
- pnpm run build
33
-
34
- # Run tests
35
- pnpm test
36
-
37
- # Run tests in watch mode
38
- pnpm test:watch
39
-
40
- # Run with coverage
41
- pnpm test:coverage
42
-
43
- # Lint code
44
- pnpm run lint
45
-
46
- # Auto-fix linting issues
47
- pnpm run lint:fix
48
-
49
- # Type checking
50
- pnpm run lint:types
51
-
52
- # Create a changeset for releases
53
- pnpm run changeset
54
-
55
- # Publish releases
56
- pnpm run release
57
- ```
58
-
59
- ## Transform Structure
60
-
61
- Every transform follows this basic structure:
62
-
63
- ```typescript
64
- // src/transforms/my-transform/my-transform.ts
65
- import type { API, FileInfo, JSCodeshift, Options } from 'jscodeshift';
66
- import { validatePackageRequirements, createReporter, hasImport } from '../helpers';
67
- import reportManualReview from '../../utils/reportManualReview';
68
-
69
- export const parser = 'tsx'; // Always use tsx for TypeScript + JSX support
70
- export const packageRequirements = [{ name: '@transferwise/components', version: '>=46.5.0' }];
71
-
72
- const transformer = (file: FileInfo, api: API, options: Options) => {
73
- // 1. Validate package requirements
74
- if (!validatePackageRequirements(options, packageRequirements)) {
75
- return file.source;
76
- }
77
-
78
- // 2. Set up jscodeshift and reporter
79
- const j: JSCodeshift = api.jscodeshift;
80
- const root = j(file.source);
81
- const manualReviewIssues: string[] = [];
82
- const reporter = createReporter(j, manualReviewIssues);
83
-
84
- // 3. Check for relevant imports
85
- const { exists: hasComponentImport } = hasImport(
86
- root,
87
- '@transferwise/components',
88
- 'MyComponent',
89
- j,
90
- );
91
-
92
- if (!hasComponentImport) {
93
- return file.source; // No relevant imports, skip file
94
- }
95
-
96
- // 4. Perform transformations
97
- root.findJSXElements('MyComponent').forEach((path) => {
98
- // Transform logic here
99
- });
100
-
101
- // 5. Report manual review issues
102
- if (manualReviewIssues.length > 0) {
103
- manualReviewIssues.forEach(async (issue) => {
104
- await reportManualReview(file.path, issue);
105
- });
106
- }
107
-
108
- return root.toSource();
109
- };
110
-
111
- export default transformer;
112
- ```
113
-
114
- ## Helper Functions
115
-
116
- ### Import Management
117
-
118
- #### `hasImport(root, moduleName, importName, j)`
119
-
120
- Checks if a specific import exists and provides utilities to remove it.
121
-
122
- ```typescript
123
- import { hasImport } from '../helpers';
124
-
125
- // Check for named import
126
- const { exists: hasComponentImport, remove: removeComponentImport } = hasImport(
127
- root,
128
- '@transferwise/components',
129
- 'MyComponent',
130
- j,
131
- );
132
-
133
- // Check for legacy component import with removal capability
134
- const { exists: hasLegacyImport, remove: removeLegacyImport } = hasImport(
135
- root,
136
- '@transferwise/components',
137
- 'LegacyComponent',
138
- j,
139
- );
140
-
141
- if (hasComponentImport) {
142
- // Transform MyComponent elements
143
- root.findJSXElements('MyComponent').forEach(transformComponent);
144
- }
145
-
146
- if (hasLegacyImport) {
147
- // Transform LegacyComponent to MyComponent
148
- root.findJSXElements('LegacyComponent').forEach(transformLegacyComponent);
149
-
150
- // Remove the LegacyComponent import after transformation
151
- removeLegacyImport();
152
- }
153
- ```
154
-
155
- **Returns:**
156
-
157
- - `exists: boolean` - Whether the import exists
158
- - `remove: () => void` - Function to remove the import
159
-
160
- ### JSX Element Utilities
161
-
162
- #### `setNameIfJSXIdentifier(elementName, newName)`
163
-
164
- Safely renames JSX elements, only if they are simple identifiers (not member expressions).
165
-
166
- ```typescript
167
- import { setNameIfJSXIdentifier } from '../helpers';
168
-
169
- root.findJSXElements('LegacyComponent').forEach((path) => {
170
- const { openingElement, closingElement } = path.node;
171
-
172
- // Rename opening tag from LegacyComponent to NewComponent
173
- openingElement.name = setNameIfJSXIdentifier(openingElement.name, 'NewComponent')!;
174
-
175
- // Rename closing tag if it exists
176
- if (closingElement) {
177
- closingElement.name = setNameIfJSXIdentifier(closingElement.name, 'NewComponent')!;
178
- }
179
- });
180
- ```
181
-
182
- #### `hasAttributeOnElement(openingElement, attributeName)`
183
-
184
- Checks if a JSX element has a specific attribute.
185
-
186
- ```typescript
187
- import { hasAttributeOnElement } from '../helpers';
188
-
189
- root.findJSXElements('MyComponent').forEach((path) => {
190
- const { openingElement } = path.node;
191
-
192
- // Skip if already has v2 prop
193
- if (hasAttributeOnElement(openingElement, 'v2')) {
194
- return;
195
- }
196
-
197
- // Add v2 prop
198
- addAttributesIfMissing(j, openingElement, [
199
- { attribute: j.jsxAttribute(j.jsxIdentifier('v2')), name: 'v2' },
200
- ]);
201
- });
202
- ```
203
-
204
- #### `addAttributesIfMissing(j, openingElement, attributes)`
205
-
206
- Adds attributes to a JSX element only if they don't already exist.
207
-
208
- ```typescript
209
- import { addAttributesIfMissing } from '../helpers';
210
-
211
- // Add multiple attributes if missing
212
- addAttributesIfMissing(j, openingElement, [
213
- {
214
- attribute: j.jsxAttribute(j.jsxIdentifier('v2')),
215
- name: 'v2',
216
- },
217
- {
218
- attribute: j.jsxAttribute(j.jsxIdentifier('variant'), j.literal('primary')),
219
- name: 'variant',
220
- },
221
- ]);
222
- ```
223
-
224
- **Parameters:**
225
-
226
- - `j` - jscodeshift instance
227
- - `openingElement` - JSX opening element
228
- - `attributes` - Array of `{ attribute: JSXAttribute, name: string }`
229
-
230
- ### Icon Processing
231
-
232
- #### `processIconChildren(j, children, iconImports, openingElement)`
233
-
234
- Automatically processes icon children and converts them to appropriate addon props.
235
-
236
- ```typescript
237
- import { processIconChildren } from '../helpers';
238
-
239
- // First, collect icon imports
240
- const iconImports = new Set<string>();
241
- root.find(j.ImportDeclaration, { source: { value: '@transferwise/icons' } }).forEach((path) => {
242
- path.node.specifiers?.forEach((specifier) => {
243
- if (specifier.local) {
244
- const localName = (specifier.local as { name: string }).name;
245
- iconImports.add(localName);
246
- }
247
- });
248
- });
249
-
250
- // Then process icons in components
251
- root.findJSXElements('MyComponent').forEach((path) => {
252
- processIconChildren(j, path.node.children, iconImports, path.node.openingElement);
253
- });
254
- ```
255
-
256
- This function:
257
-
258
- - Detects icon components in children
259
- - Converts them to addon object props: `addonStart={{ type: 'icon', value: <IconComponent /> }}`
260
- - Removes the icon children from the component
261
- - Handles complex icon expressions that need manual review
262
-
263
- **Example transformation:**
264
-
265
- ```typescript
266
- // Before
267
- <MyComponent>
268
- <CheckIcon />
269
- Content here
270
- </MyComponent>
271
-
272
- // After
273
- <MyComponent addonStart={{ type: 'icon', value: <CheckIcon /> }}>
274
- Content here
275
- </MyComponent>
276
- ```
277
-
278
- ### Reporting System
279
-
280
- #### `createReporter(j, issues)`
281
-
282
- Creates a reporter instance for collecting manual review issues.
283
-
284
- ```typescript
285
- import { createReporter } from '../helpers';
286
-
287
- const manualReviewIssues: string[] = [];
288
- const reporter = createReporter(j, manualReviewIssues);
289
-
290
- // The reporter provides many methods for different scenarios
291
- ```
292
-
293
- #### Reporter Methods
294
-
295
- **Element-level reporting:**
296
-
297
- ```typescript
298
- // Report general element issues
299
- reporter.reportElement(componentElement, 'has complex configuration that needs review');
300
-
301
- // Report spread props
302
- reporter.reportSpreadProps(componentElement);
303
-
304
- // Report attribute issues automatically
305
- reporter.reportAttributeIssues(componentElement);
306
- ```
307
-
308
- **Prop-specific reporting:**
309
-
310
- ```typescript
311
- // Report specific prop issues
312
- reporter.reportProp(element, 'variant', 'has unsupported value');
313
-
314
- // Report attribute with custom reason
315
- reporter.reportAttribute(attr, element, 'contains dynamic expression');
316
-
317
- // Report unsupported values
318
- reporter.reportUnsupportedValue(element, 'theme', 'custom-theme');
319
-
320
- // Report ambiguous expressions
321
- reporter.reportAmbiguousExpression(element, 'onClick');
322
-
323
- // Report conflicting props and children
324
- reporter.reportPropWithChildren(element, 'label');
325
-
326
- // Report deprecated props
327
- reporter.reportDeprecatedProp(element, 'legacy', 'use "variant" instead');
328
-
329
- // Report missing required props
330
- reporter.reportMissingRequiredProp(element, 'children');
331
-
332
- // Report conflicting props
333
- reporter.reportConflictingProps(element, ['variant', 'theme']);
334
- ```
335
-
336
- **Children-specific reporting:**
337
-
338
- ```typescript
339
- // Report ambiguous children (icons, conditional rendering, etc.)
340
- reporter.reportAmbiguousChildren(element, 'icon');
341
- ```
342
-
343
- ### Package Validation
344
-
345
- #### `validatePackageRequirements(options, packageRequirements)`
346
-
347
- Validates that required packages are available before running the transform.
348
-
349
- ```typescript
350
- import { validatePackageRequirements } from '../helpers';
351
-
352
- export const packageRequirements = [
353
- { name: '@transferwise/components', version: '>=46.5.0' },
354
- { name: '@transferwise/icons', version: '>=1.0.0' },
355
- ];
356
-
357
- const transformer = (file: FileInfo, api: API, options: Options) => {
358
- // This will return false if packages aren't found/compatible
359
- if (!validatePackageRequirements(options, packageRequirements)) {
360
- return file.source; // Skip transformation
361
- }
362
-
363
- // Continue with transformation...
364
- };
365
- ```
366
-
367
- ## Testing
368
-
369
- ### Basic Test Setup
370
-
371
- ```typescript
372
- // src/transforms/my-transform/__tests__/my-transform.test.ts
373
- import { createTestTransform } from '../../helpers/createTestTransform';
374
- import transform from '../my-transform';
375
-
376
- // Create test transform with package requirements
377
- const testTransform = createTestTransform(transform, [
378
- { name: '@transferwise/components', version: '46.5.0' },
379
- ]);
380
-
381
- describe('my-transform', () => {
382
- it('should transform basic component', () => {
383
- const input = `
384
- import { OldComponent } from '@transferwise/components';
385
-
386
- <OldComponent prop="value" />
387
- `;
388
-
389
- const expected = `
390
- import { NewComponent } from '@transferwise/components';
391
-
392
- <NewComponent newProp="value" />
393
- `;
394
-
395
- expect(testTransform({ source: input }).trim()).toBe(expected.trim());
396
- });
397
- });
398
- ```
399
-
400
- ### Testing with Different Package Scenarios
401
-
402
- ```typescript
403
- import {
404
- createTestTransform,
405
- createTestTransformWithPackage,
406
- createTestTransformWithoutPackage,
407
- } from '../../helpers/createTestTransform';
408
-
409
- describe('package requirement scenarios', () => {
410
- it('should transform when package is available', () => {
411
- const testTransform = createTestTransformWithPackage(
412
- transform,
413
- '@transferwise/components',
414
- '46.5.0',
415
- );
416
-
417
- const result = testTransform({ source: input });
418
- expect(result).toBe(expectedOutput);
419
- });
420
-
421
- it('should skip transformation when package is missing', () => {
422
- const testTransform = createTestTransformWithoutPackage(
423
- transform,
424
- '@transferwise/components',
425
- '46.5.0',
426
- );
427
-
428
- const result = testTransform({ source: input });
429
- expect(result).toBe(input); // Should remain unchanged
430
- });
431
- });
432
- ```
433
-
434
- ### Testing Manual Review Cases
435
-
436
- ```typescript
437
- describe('manual review cases', () => {
438
- it('should handle spread props', () => {
439
- const input = `<MyComponent {...props} />`;
440
- const result = testTransform({ source: input });
441
-
442
- // The transform should still run but flag for manual review
443
- expect(result).toContain('v2');
444
- // Manual review issues would be captured in a real scenario
445
- });
446
-
447
- it('should handle complex expressions', () => {
448
- const input = `<MyComponent variant={computeVariant()} />`;
449
- const result = testTransform({ source: input });
450
-
451
- // Should preserve the expression but flag for review
452
- expect(result).toContain('computeVariant()');
453
- });
454
- });
455
- ```
456
-
457
- ### Testing Icon Processing
458
-
459
- ```typescript
460
- describe('icon processing', () => {
461
- it('should convert leading icons to addon props', () => {
462
- const input = `
463
- import { MyComponent } from '@transferwise/components';
464
- import { CheckIcon } from '@transferwise/icons';
465
-
466
- <MyComponent>
467
- <CheckIcon />
468
- Content text
469
- </MyComponent>
470
- `;
471
-
472
- const expected = `
473
- import { MyComponent } from '@transferwise/components';
474
- import { CheckIcon } from '@transferwise/icons';
475
-
476
- <MyComponent v2 addonStart={{ type: 'icon', value: <CheckIcon /> }}>
477
- Content text
478
- </MyComponent>
479
- `;
480
-
481
- expect(testTransform({ source: input })).toBe(expected);
482
- });
483
- });
484
- ```
485
-
486
- ## Package Requirements
487
-
488
- ### Defining Requirements
489
-
490
- ```typescript
491
- export const packageRequirements = [
492
- {
493
- name: '@transferwise/components',
494
- version: '>=46.5.0', // Semantic version range
495
- },
496
- {
497
- name: '@transferwise/icons',
498
- version: '^1.0.0', // Caret range
499
- },
500
- {
501
- name: '@transferwise/tokens',
502
- version: '~1.5.0', // Tilde range
503
- },
504
- ];
505
- ```
506
-
507
- ### How Package Validation Works
508
-
509
- The system checks for packages in this order:
510
-
511
- 1. **Direct dependencies** - `package.json` files
512
- 2. **Lockfiles** - `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`
513
- 3. **Node modules** - Installed packages in `node_modules/`
514
-
515
- For monorepos, it checks across all workspace packages and provides a summary.
516
-
517
- ## Manual Review System
518
-
519
- ### When to Use Manual Review
520
-
521
- Use manual review reporting when:
522
-
523
- - **Spread props** are present: `<MyComponent {...props} />`
524
- - **Dynamic expressions** can't be safely transformed: `<MyComponent variant={calculateVariant()} />`
525
- - **Complex children** need human review: `<MyComponent>{condition ? <Icon1 /> : <Icon2 />}</MyComponent>`
526
- - **Unsupported prop values** are found: `<MyComponent theme="custom" />`
527
- - **Conflicting props** are present: `<MyComponent label="Click" children="Also click" />`
528
-
529
- ### Best Practices for Reporting
530
-
531
- ```typescript
532
- // ✅ Good - Specific and actionable
533
- reporter.reportUnsupportedValue(element, 'variant', 'custom');
534
- // "prop "variant" on <MyComponent> at line 42 has unsupported value "custom""
535
-
536
- // ✅ Good - Explains the conflict
537
- reporter.reportPropWithChildren(element, 'label');
538
- // "prop "label" on <MyComponent> at line 42 conflicts with children"
539
-
540
- // ❌ Avoid - Too vague
541
- reporter.reportElement(element, 'needs review');
542
- ```
543
-
544
- ## Best Practices
545
-
546
- ### Transform Design
547
-
548
- 1. **Start small** - Begin with simple transformations and gradually add complexity
549
- 2. **Fail safely** - When in doubt, preserve existing code and report for manual review
550
- 3. **Be specific** - Provide clear, actionable manual review messages
551
- 4. **Test edge cases** - Include tests for complex scenarios and error conditions
552
-
553
- ### Code Organisation
554
-
555
- ```typescript
556
- // Group related transformations
557
- const transformComponent = (path: ASTPath<JSXElement>) => {
558
- // Component-specific logic
559
- };
560
-
561
- const transformLegacyComponent = (path: ASTPath<JSXElement>) => {
562
- // Legacy component-specific logic
563
- };
564
-
565
- // Main transformer delegates to specific functions
566
- const transformer = (file: FileInfo, api: API, options: Options) => {
567
- // Setup...
568
-
569
- if (hasComponentImport) {
570
- root.findJSXElements('MyComponent').forEach(transformComponent);
571
- }
572
-
573
- if (hasLegacyImport) {
574
- root.findJSXElements('LegacyComponent').forEach(transformLegacyComponent);
575
- removeLegacyImport();
576
- }
577
-
578
- // Cleanup...
579
- };
580
- ```
581
-
582
- ### Error Handling
583
-
584
- ```typescript
585
- // Wrap risky operations in try-catch
586
- openingElement.attributes?.forEach((attr) => {
587
- try {
588
- if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier') {
589
- // Safe to access attr.name.name
590
- const propName = attr.name.name;
591
- // Process attribute...
592
- }
593
- } catch (error) {
594
- reporter.reportAttribute(attr, path, 'could not be processed automatically');
595
- }
596
- });
597
- ```
598
-
599
- ### Performance Considerations
600
-
601
- 1. **Early returns** - Skip files without relevant imports
602
- 2. **Efficient queries** - Use specific selectors instead of broad searches
603
- 3. **Minimise DOM traversal** - Cache frequently accessed nodes
604
-
605
- ```typescript
606
- // ✅ Good - Early return if no relevant imports
607
- if (!hasComponentImport && !hasLegacyImport) {
608
- return file.source;
609
- }
610
-
611
- // ✅ Good - Specific query
612
- root.findJSXElements('MyComponent').forEach(transformComponent);
613
-
614
- // ❌ Avoid - Broad search
615
- root.find(j.JSXElement).forEach((path) => {
616
- if (
617
- path.node.openingElement.name.type === 'JSXIdentifier' &&
618
- path.node.openingElement.name.name === 'MyComponent'
619
- ) {
620
- transformComponent(path);
621
- }
622
- });
623
- ```
624
-
625
- ## Examples
626
-
627
- ### Complete Transform Example (Simplified)
628
-
629
- ```typescript
630
- // src/transforms/my-component-v2/my-component-v2.ts
631
- import type { API, FileInfo, JSCodeshift, Options } from 'jscodeshift';
632
- import reportManualReview from '../../utils/reportManualReview';
633
- import {
634
- addAttributesIfMissing,
635
- createReporter,
636
- hasAttributeOnElement,
637
- hasImport,
638
- validatePackageRequirements,
639
- } from '../helpers';
640
-
641
- export const parser = 'tsx';
642
- export const packageRequirements = [{ name: '@transferwise/components', version: '>=46.5.0' }];
643
-
644
- const transformer = (file: FileInfo, api: API, options: Options) => {
645
- // 1. Validate package requirements
646
- if (!validatePackageRequirements(options, packageRequirements)) {
647
- return file.source;
648
- }
649
-
650
- // 2. Setup
651
- const j: JSCodeshift = api.jscodeshift;
652
- const root = j(file.source);
653
- const manualReviewIssues: string[] = [];
654
- const reporter = createReporter(j, manualReviewIssues);
655
-
656
- // 3. Check for component import
657
- const { exists: hasComponentImport } = hasImport(
658
- root,
659
- '@transferwise/components',
660
- 'MyComponent',
661
- j,
662
- );
663
-
664
- if (!hasComponentImport) {
665
- return file.source;
666
- }
667
-
668
- // 4. Transform components
669
- root.findJSXElements('MyComponent').forEach((path) => {
670
- const { openingElement } = path.node;
671
-
672
- // Skip if already has v2 prop
673
- if (hasAttributeOnElement(openingElement, 'v2')) {
674
- return;
675
- }
676
-
677
- // Add v2 prop
678
- addAttributesIfMissing(j, openingElement, [
679
- { attribute: j.jsxAttribute(j.jsxIdentifier('v2')), name: 'v2' },
680
- ]);
681
-
682
- // Handle variant prop transformation
683
- const variantAttr = openingElement.attributes?.find(
684
- (attr) =>
685
- attr.type === 'JSXAttribute' &&
686
- attr.name.type === 'JSXIdentifier' &&
687
- attr.name.name === 'variant',
688
- );
689
-
690
- if (
691
- variantAttr &&
692
- variantAttr.type === 'JSXAttribute' &&
693
- variantAttr.value?.type === 'StringLiteral'
694
- ) {
695
- const oldVariant = variantAttr.value.value;
696
- const newVariant =
697
- oldVariant === 'accent' ? 'primary' : oldVariant === 'subtle' ? 'secondary' : oldVariant;
698
- variantAttr.value.value = newVariant;
699
- }
700
-
701
- // Report spread props for manual review
702
- if (openingElement.attributes?.some((attr) => attr.type === 'JSXSpreadAttribute')) {
703
- reporter.reportSpreadProps(path);
704
- }
705
- });
706
-
707
- // 5. Report manual review issues
708
- if (manualReviewIssues.length > 0) {
709
- manualReviewIssues.forEach(async (issue) => {
710
- await reportManualReview(file.path, issue);
711
- });
712
- }
713
-
714
- return root.toSource();
715
- };
716
-
717
- export default transformer;
718
- ```
719
-
720
- ### Complete Test Example
721
-
722
- ```typescript
723
- // src/transforms/my-component-v2/__tests__/my-component-v2.test.ts
724
- import { createTestTransform } from '../../helpers/createTestTransform';
725
- import transform from '../my-component-v2';
726
-
727
- const testTransform = createTestTransform(transform, [
728
- { name: '@transferwise/components', version: '46.5.0' },
729
- ]);
730
-
731
- describe('my-component-v2 transform', () => {
732
- it('should add v2 prop to MyComponent instances', () => {
733
- const input = `
734
- import { MyComponent } from '@transferwise/components';
735
-
736
- <MyComponent variant="accent">Content</MyComponent>
737
- `;
738
-
739
- const expected = `
740
- import { MyComponent } from '@transferwise/components';
741
-
742
- <MyComponent v2 variant="primary">Content</MyComponent>
743
- `;
744
-
745
- expect(testTransform({ source: input })).toBe(expected);
746
- });
747
-
748
- it('should skip components that already have v2 prop', () => {
749
- const input = `
750
- import { MyComponent } from '@transferwise/components';
751
-
752
- <MyComponent v2 variant="primary">Already upgraded</MyComponent>
753
- `;
754
-
755
- expect(testTransform({ source: input })).toBe(input);
756
- });
757
-
758
- it('should skip files without component imports', () => {
759
- const input = `
760
- import { OtherComponent } from '@transferwise/components';
761
-
762
- <OtherComponent>Content</OtherComponent>
763
- `;
764
-
765
- expect(testTransform({ source: input })).toBe(input);
766
- });
767
-
768
- it('should handle spread props', () => {
769
- const input = `
770
- import { MyComponent } from '@transferwise/components';
771
-
772
- <MyComponent {...props}>Content</MyComponent>
773
- `;
774
-
775
- const result = testTransform({ source: input });
776
-
777
- // Should still add v2 prop but flag for manual review
778
- expect(result).toContain('v2');
779
- });
780
- });
781
- ```
782
-
783
- This comprehensive guide should help developers understand how to create effective, robust, and well-tested codemods for the Wise Design System.