@wise/wds-codemods 0.0.1-experimental-6c2101b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/better-impalas-drop.md +5 -0
- package/.changeset/config.json +13 -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/README.md +184 -0
- package/babel.config.js +28 -0
- package/codemod-report.md +81 -0
- package/commitlint.config.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2448 -0
- package/dist/index.js.map +1 -0
- package/dist/transforms/button.d.ts +20 -0
- package/dist/transforms/button.js +640 -0
- package/dist/transforms/button.js.map +1 -0
- package/eslint.config.js +15 -0
- package/jest.config.js +9 -0
- package/mkdocs.yml +4 -0
- package/package.json +68 -0
- package/renovate.json +9 -0
- package/scripts/build.sh +10 -0
- package/src/__tests__/runCodemod.test.ts +109 -0
- package/src/index.ts +4 -0
- package/src/runCodemod.ts +149 -0
- package/src/transforms/button/__tests__/button.test.tsx +175 -0
- package/src/transforms/button/button.ts +453 -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/__tests__/packageValidation.test.ts +45 -0
- package/src/transforms/helpers/createTestTransform.ts +59 -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/transforms/helpers/packageValidation.ts +53 -0
- package/src/utils/__tests__/getOptions.test.ts +219 -0
- package/src/utils/__tests__/handleError.test.ts +18 -0
- package/src/utils/__tests__/hasPackageVersion.test.ts +191 -0
- package/src/utils/__tests__/loadTransformModules.test.ts +51 -0
- package/src/utils/__tests__/reportManualReview.test.ts +42 -0
- package/src/utils/getOptions.ts +78 -0
- package/src/utils/handleError.ts +6 -0
- package/src/utils/hasPackageVersion.ts +482 -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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Options } from 'jscodeshift';
|
|
2
|
+
|
|
3
|
+
import { validatePackageRequirements } from '../packageValidation';
|
|
4
|
+
|
|
5
|
+
describe('validatePackageRequirements', () => {
|
|
6
|
+
const requirements = [{ name: '@transferwise/components', version: '>=46.5.0' }];
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.spyOn(console, 'debug').mockImplementation(() => {});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
jest.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns true when package is found (object format)', () => {
|
|
17
|
+
const options: Options = {
|
|
18
|
+
packageResults: { '@transferwise/components@>=46.5.0': true },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
expect(validatePackageRequirements(options, requirements)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns true when package is found (JSON string format)', () => {
|
|
25
|
+
const options: Options = {
|
|
26
|
+
packageResults: '{"@transferwise/components@>=46.5.0":true}',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
expect(validatePackageRequirements(options, requirements)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns false when package is missing', () => {
|
|
33
|
+
const options: Options = {
|
|
34
|
+
packageResults: { '@transferwise/components@>=46.5.0': false },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
expect(validatePackageRequirements(options, requirements)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns false when no packageResults provided', () => {
|
|
41
|
+
const options: Options = {};
|
|
42
|
+
|
|
43
|
+
expect(validatePackageRequirements(options, requirements)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Options, Transform } from 'jscodeshift';
|
|
2
|
+
import { applyTransform, type TestOptions } from 'jscodeshift/src/testUtils';
|
|
3
|
+
|
|
4
|
+
interface PackageRequirement {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
found?: boolean; // Optional - defaults to true
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This function creates a test transform function that applies a given transformer to the input code.
|
|
12
|
+
* It uses the 'tsx' parser to support both TypeScript and JSX syntax.
|
|
13
|
+
*
|
|
14
|
+
* The transform function is used to modify the input code based on the provided transformer.
|
|
15
|
+
*
|
|
16
|
+
* @param transformer - The jscodeshift transformer function
|
|
17
|
+
* @param packageRequirements - Array of package requirements to mock (defaults to empty array)
|
|
18
|
+
*/
|
|
19
|
+
function createTestTransform(
|
|
20
|
+
transformer: Transform,
|
|
21
|
+
packageRequirements: PackageRequirement[] = [],
|
|
22
|
+
) {
|
|
23
|
+
// Create mock package results from requirements
|
|
24
|
+
const packageResults: Record<string, boolean> = {};
|
|
25
|
+
packageRequirements.forEach((pkg) => {
|
|
26
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
27
|
+
packageResults[key] = pkg.found ?? true; // Default to found unless explicitly set to false
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const options: Options = {
|
|
31
|
+
// Only add packageResults if we have package requirements
|
|
32
|
+
...(packageRequirements.length > 0 ? { packageResults: JSON.stringify(packageResults) } : {}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Use 'tsx' parser to support both TypeScript and JSX syntax
|
|
36
|
+
const testOptions: TestOptions = { parser: 'tsx' };
|
|
37
|
+
|
|
38
|
+
return (input: { path?: string; source: string }) =>
|
|
39
|
+
applyTransform(transformer, options, input, testOptions);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Convenience functions for common use cases
|
|
43
|
+
export const createTestTransformWithPackage = (
|
|
44
|
+
transformer: Transform,
|
|
45
|
+
packageName: string,
|
|
46
|
+
version: string,
|
|
47
|
+
) => {
|
|
48
|
+
return createTestTransform(transformer, [{ name: packageName, version }]);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const createTestTransformWithoutPackage = (
|
|
52
|
+
transformer: Transform,
|
|
53
|
+
packageName: string,
|
|
54
|
+
version: string,
|
|
55
|
+
) => {
|
|
56
|
+
return createTestTransform(transformer, [{ name: packageName, version, found: false }]);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default createTestTransform;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Collection, JSCodeshift } from 'jscodeshift';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a specific import exists in the given root collection and provides
|
|
5
|
+
* a method to remove it if found.
|
|
6
|
+
*/
|
|
7
|
+
function hasImport(
|
|
8
|
+
root: Collection,
|
|
9
|
+
sourceValue: string,
|
|
10
|
+
importName: string,
|
|
11
|
+
j: JSCodeshift,
|
|
12
|
+
): { exists: boolean; remove: () => void } {
|
|
13
|
+
const importDeclarations = root.find(j.ImportDeclaration, {
|
|
14
|
+
source: { value: sourceValue },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (importDeclarations.size() === 0) {
|
|
18
|
+
return {
|
|
19
|
+
exists: false,
|
|
20
|
+
remove: () => {},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const namedImport = importDeclarations.find(j.ImportSpecifier, {
|
|
25
|
+
imported: { name: importName },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const defaultImport = importDeclarations.find(j.ImportDefaultSpecifier, {
|
|
29
|
+
local: { name: importName },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const exists = namedImport.size() > 0 || defaultImport.size() > 0;
|
|
33
|
+
|
|
34
|
+
const remove = () => {
|
|
35
|
+
importDeclarations.forEach((path) => {
|
|
36
|
+
const filteredSpecifiers =
|
|
37
|
+
path.node.specifiers?.filter((specifier) => {
|
|
38
|
+
if (specifier.type === 'ImportSpecifier' && specifier.imported.name === importName) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (specifier.type === 'ImportDefaultSpecifier' && specifier.local?.name === importName) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}) ?? [];
|
|
46
|
+
|
|
47
|
+
if (filteredSpecifiers.length === 0) {
|
|
48
|
+
path.prune();
|
|
49
|
+
} else {
|
|
50
|
+
j(path).replaceWith(
|
|
51
|
+
j.importDeclaration(filteredSpecifiers, path.node.source, path.node.importKind),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return { exists, remove };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default hasImport;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { JSCodeshift, JSXElement, JSXExpressionContainer } from 'jscodeshift';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Process children of a JSX element to detect icon components and add iconStart or iconEnd attributes accordingly.
|
|
5
|
+
* This is specific to icon handling but can be reused in codemods dealing with icon children.
|
|
6
|
+
*/
|
|
7
|
+
const processIconChildren = (
|
|
8
|
+
j: JSCodeshift,
|
|
9
|
+
children: (JSXElement | JSXExpressionContainer | unknown)[] | undefined,
|
|
10
|
+
iconImports: Set<string>,
|
|
11
|
+
openingElement: JSXElement['openingElement'],
|
|
12
|
+
) => {
|
|
13
|
+
if (!children || !openingElement.attributes) return;
|
|
14
|
+
|
|
15
|
+
const unwrapJsxElement = (node: unknown): JSXElement | unknown => {
|
|
16
|
+
if (
|
|
17
|
+
typeof node === 'object' &&
|
|
18
|
+
node !== null &&
|
|
19
|
+
'type' in node &&
|
|
20
|
+
node.type === 'JSXExpressionContainer' &&
|
|
21
|
+
j.JSXElement.check((node as JSXExpressionContainer).expression)
|
|
22
|
+
) {
|
|
23
|
+
return (node as JSXExpressionContainer).expression;
|
|
24
|
+
}
|
|
25
|
+
return node;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const totalChildren = children.length;
|
|
29
|
+
|
|
30
|
+
// Find index of icon child
|
|
31
|
+
const iconChildIndex = children.findIndex((child) => {
|
|
32
|
+
const unwrapped = unwrapJsxElement(child);
|
|
33
|
+
return (
|
|
34
|
+
j.JSXElement.check(unwrapped) &&
|
|
35
|
+
unwrapped.openingElement.name.type === 'JSXIdentifier' &&
|
|
36
|
+
iconImports.has(unwrapped.openingElement.name.name)
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (iconChildIndex === -1) return;
|
|
41
|
+
|
|
42
|
+
const iconChild = unwrapJsxElement(children[iconChildIndex]) as JSXElement;
|
|
43
|
+
|
|
44
|
+
if (!iconChild || iconChild.openingElement.name.type !== 'JSXIdentifier') return;
|
|
45
|
+
|
|
46
|
+
const iconName = iconChild.openingElement.name.name;
|
|
47
|
+
|
|
48
|
+
// Determine if icon is closer to start or end
|
|
49
|
+
const distanceToStart = iconChildIndex;
|
|
50
|
+
const distanceToEnd = totalChildren - 1 - iconChildIndex;
|
|
51
|
+
const iconPropName = distanceToStart <= distanceToEnd ? 'addonStart' : 'addonEnd';
|
|
52
|
+
|
|
53
|
+
// Build: { type: 'icon', value: <IconName /> }
|
|
54
|
+
const iconObject = j.objectExpression([
|
|
55
|
+
j.property('init', j.identifier('type'), j.literal('icon')),
|
|
56
|
+
j.property('init', j.identifier('value'), iconChild),
|
|
57
|
+
]);
|
|
58
|
+
const iconProp = j.jsxAttribute(
|
|
59
|
+
j.jsxIdentifier(iconPropName),
|
|
60
|
+
j.jsxExpressionContainer(iconObject),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
openingElement.attributes.push(iconProp);
|
|
64
|
+
|
|
65
|
+
// Remove the icon child
|
|
66
|
+
children.splice(iconChildIndex, 1);
|
|
67
|
+
|
|
68
|
+
// Helper to check if a child is whitespace-only JSXText
|
|
69
|
+
const isWhitespaceJsxText = (node: unknown): boolean => {
|
|
70
|
+
return (
|
|
71
|
+
typeof node === 'object' &&
|
|
72
|
+
node !== null &&
|
|
73
|
+
(node as { type?: unknown }).type === 'JSXText' &&
|
|
74
|
+
typeof (node as { value?: string }).value === 'string' &&
|
|
75
|
+
(node as { value?: string }).value!.trim() === ''
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Remove adjacent whitespace-only JSXText node if any
|
|
80
|
+
if (iconChildIndex - 1 >= 0 && isWhitespaceJsxText(children[iconChildIndex - 1])) {
|
|
81
|
+
children.splice(iconChildIndex - 1, 1);
|
|
82
|
+
} else if (isWhitespaceJsxText(children[iconChildIndex])) {
|
|
83
|
+
children.splice(iconChildIndex, 1);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default processIconChildren;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
JSCodeshift,
|
|
3
|
+
JSXAttribute,
|
|
4
|
+
JSXElement,
|
|
5
|
+
JSXIdentifier,
|
|
6
|
+
JSXMemberExpression,
|
|
7
|
+
JSXNamespacedName,
|
|
8
|
+
JSXSpreadAttribute,
|
|
9
|
+
} from 'jscodeshift';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Rename a JSX element name if it is a JSXIdentifier.
|
|
13
|
+
*/
|
|
14
|
+
export const setNameIfJSXIdentifier = (
|
|
15
|
+
elementName: JSXIdentifier | JSXNamespacedName | JSXMemberExpression | undefined,
|
|
16
|
+
newName: string,
|
|
17
|
+
): JSXIdentifier | JSXNamespacedName | JSXMemberExpression | undefined => {
|
|
18
|
+
if (elementName && elementName.type === 'JSXIdentifier') {
|
|
19
|
+
return { ...elementName, name: newName };
|
|
20
|
+
}
|
|
21
|
+
return elementName;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if a list of attributes contains a specific attribute by name.
|
|
26
|
+
*/
|
|
27
|
+
export const hasAttribute = (
|
|
28
|
+
attributes: (JSXAttribute | JSXSpreadAttribute)[] | undefined,
|
|
29
|
+
attributeName: string,
|
|
30
|
+
): boolean => {
|
|
31
|
+
return (
|
|
32
|
+
Array.isArray(attributes) &&
|
|
33
|
+
attributes.some(
|
|
34
|
+
(attr): attr is JSXAttribute =>
|
|
35
|
+
attr.type === 'JSXAttribute' &&
|
|
36
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
37
|
+
attr.name.name === attributeName,
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a JSX element's openingElement has a specific attribute.
|
|
44
|
+
*/
|
|
45
|
+
export const hasAttributeOnElement = (
|
|
46
|
+
element: JSXElement['openingElement'],
|
|
47
|
+
attributeName: string,
|
|
48
|
+
): boolean => {
|
|
49
|
+
return hasAttribute(element.attributes, attributeName);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add specified attributes to a JSX element's openingElement if they are not already present.
|
|
54
|
+
*/
|
|
55
|
+
export const addAttributesIfMissing = (
|
|
56
|
+
j: JSCodeshift,
|
|
57
|
+
openingElement: JSXElement['openingElement'],
|
|
58
|
+
attributesToAdd: { attribute: JSXAttribute; name: string }[],
|
|
59
|
+
) => {
|
|
60
|
+
if (!Array.isArray(openingElement.attributes)) return;
|
|
61
|
+
const attrs = openingElement.attributes;
|
|
62
|
+
attributesToAdd.forEach(({ attribute, name }) => {
|
|
63
|
+
if (!hasAttributeOnElement(openingElement, name)) {
|
|
64
|
+
attrs.push(attribute);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { ASTPath, JSCodeshift, JSXAttribute, JSXElement, Node } from 'jscodeshift';
|
|
2
|
+
|
|
3
|
+
export interface ReporterOptions {
|
|
4
|
+
jscodeshift: JSCodeshift;
|
|
5
|
+
issues: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CodemodReporter is a utility class for reporting issues found during codemod transformations.
|
|
10
|
+
* It provides methods to report issues related to JSX elements, props, and attributes.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const issues: string[] = [];
|
|
15
|
+
* const reporter = createReporter(j, issues);
|
|
16
|
+
*
|
|
17
|
+
* // Report a deprecated prop
|
|
18
|
+
* reporter.reportDeprecatedProp(buttonElement, 'flat', 'variant="text"');
|
|
19
|
+
*
|
|
20
|
+
* // Report complex expression that needs review
|
|
21
|
+
* reporter.reportAmbiguousExpression(element, 'size');
|
|
22
|
+
*
|
|
23
|
+
* // Auto-detect common issues
|
|
24
|
+
* reporter.reportAttributeIssues(element);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class CodemodReporter {
|
|
28
|
+
private readonly j: JSCodeshift;
|
|
29
|
+
private readonly issues: string[];
|
|
30
|
+
|
|
31
|
+
constructor(options: ReporterOptions) {
|
|
32
|
+
this.j = options.jscodeshift;
|
|
33
|
+
this.issues = options.issues;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reports an issue with a JSX element
|
|
38
|
+
*/
|
|
39
|
+
reportElement(element: JSXElement | ASTPath<JSXElement>, reason: string): void {
|
|
40
|
+
const node = this.getNode(element);
|
|
41
|
+
const componentName = this.getComponentName(node);
|
|
42
|
+
const line = this.getLineNumber(node);
|
|
43
|
+
|
|
44
|
+
this.addIssue(`Manual review required: <${componentName}> at line ${line} ${reason}.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reports an issue with a specific prop
|
|
49
|
+
*/
|
|
50
|
+
reportProp(element: JSXElement | ASTPath<JSXElement>, propName: string, reason: string): void {
|
|
51
|
+
const node = this.getNode(element);
|
|
52
|
+
const componentName = this.getComponentName(node);
|
|
53
|
+
const line = this.getLineNumber(node);
|
|
54
|
+
|
|
55
|
+
this.addIssue(
|
|
56
|
+
`Manual review required: prop "${propName}" on <${componentName}> at line ${line} ${reason}.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reports an issue with a JSX attribute directly
|
|
62
|
+
*/
|
|
63
|
+
reportAttribute(
|
|
64
|
+
attr: JSXAttribute,
|
|
65
|
+
element: JSXElement | ASTPath<JSXElement>,
|
|
66
|
+
reason?: string,
|
|
67
|
+
): void {
|
|
68
|
+
const node = this.getNode(element);
|
|
69
|
+
const componentName = this.getComponentName(node);
|
|
70
|
+
const propName = this.getAttributeName(attr);
|
|
71
|
+
const line = this.getLineNumber(attr) || this.getLineNumber(node);
|
|
72
|
+
|
|
73
|
+
const defaultReason = this.getAttributeReason(attr);
|
|
74
|
+
const finalReason = reason || defaultReason;
|
|
75
|
+
|
|
76
|
+
this.addIssue(
|
|
77
|
+
`Manual review required: prop "${propName}" on <${componentName}> at line ${line} ${finalReason}.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reports spread props on an element
|
|
83
|
+
*/
|
|
84
|
+
reportSpreadProps(element: JSXElement | ASTPath<JSXElement>): void {
|
|
85
|
+
this.reportElement(element, 'contains spread props that need manual review');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reports conflicting prop and children
|
|
90
|
+
*/
|
|
91
|
+
reportPropWithChildren(element: JSXElement | ASTPath<JSXElement>, propName: string): void {
|
|
92
|
+
this.reportProp(
|
|
93
|
+
element,
|
|
94
|
+
propName,
|
|
95
|
+
`conflicts with children - both "${propName}" prop and children are present`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reports unsupported prop value
|
|
101
|
+
*/
|
|
102
|
+
reportUnsupportedValue(
|
|
103
|
+
element: JSXElement | ASTPath<JSXElement>,
|
|
104
|
+
propName: string,
|
|
105
|
+
value: string,
|
|
106
|
+
): void {
|
|
107
|
+
this.reportProp(element, propName, `has unsupported value "${value}"`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reports ambiguous expression in prop
|
|
112
|
+
*/
|
|
113
|
+
reportAmbiguousExpression(element: JSXElement | ASTPath<JSXElement>, propName: string): void {
|
|
114
|
+
this.reportProp(element, propName, 'contains a complex expression that needs manual review');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Reports ambiguous children (like dynamic icons)
|
|
119
|
+
*/
|
|
120
|
+
reportAmbiguousChildren(element: JSXElement | ASTPath<JSXElement>, childType = 'content'): void {
|
|
121
|
+
this.reportElement(element, `contains ambiguous ${childType} that needs manual review`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Reports deprecated prop usage
|
|
126
|
+
*/
|
|
127
|
+
reportDeprecatedProp(
|
|
128
|
+
element: JSXElement | ASTPath<JSXElement>,
|
|
129
|
+
propName: string,
|
|
130
|
+
alternative?: string,
|
|
131
|
+
): void {
|
|
132
|
+
const suggestion = alternative ? ` Use ${alternative} instead` : '';
|
|
133
|
+
this.reportProp(element, propName, `is deprecated${suggestion}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reports missing required prop
|
|
138
|
+
*/
|
|
139
|
+
reportMissingRequiredProp(element: JSXElement | ASTPath<JSXElement>, propName: string): void {
|
|
140
|
+
this.reportProp(element, propName, 'is required but missing');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Reports conflicting props
|
|
145
|
+
*/
|
|
146
|
+
reportConflictingProps(element: JSXElement | ASTPath<JSXElement>, propNames: string[]): void {
|
|
147
|
+
const propList = propNames.map((name) => `"${name}"`).join(', ');
|
|
148
|
+
this.reportElement(element, `has conflicting props: ${propList} cannot be used together`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Auto-detects and reports common attribute issues
|
|
153
|
+
*/
|
|
154
|
+
reportAttributeIssues(element: JSXElement | ASTPath<JSXElement>): void {
|
|
155
|
+
const node = this.getNode(element);
|
|
156
|
+
const { attributes } = node.openingElement;
|
|
157
|
+
|
|
158
|
+
if (!attributes) return;
|
|
159
|
+
|
|
160
|
+
// Check for spread props
|
|
161
|
+
if (attributes.some((attr) => attr.type === 'JSXSpreadAttribute')) {
|
|
162
|
+
this.reportSpreadProps(element);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check for complex expressions in attributes
|
|
166
|
+
attributes.forEach((attr) => {
|
|
167
|
+
if (attr.type === 'JSXAttribute' && attr.value?.type === 'JSXExpressionContainer') {
|
|
168
|
+
this.reportAttribute(attr, element);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Private helper methods
|
|
174
|
+
private getNode(element: JSXElement | ASTPath<JSXElement>): JSXElement {
|
|
175
|
+
return 'node' in element ? element.node : element;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private getComponentName(node: JSXElement): string {
|
|
179
|
+
const { name } = node.openingElement;
|
|
180
|
+
if (name.type === 'JSXIdentifier') {
|
|
181
|
+
return name.name;
|
|
182
|
+
}
|
|
183
|
+
// Handle JSXMemberExpression, JSXNamespacedName, etc.
|
|
184
|
+
return this.j(name).toSource();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private getLineNumber(node: JSXElement | JSXAttribute | Node): string {
|
|
188
|
+
return node.loc?.start.line?.toString() || 'unknown';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private getAttributeName(attr: JSXAttribute): string {
|
|
192
|
+
if (attr.name.type === 'JSXIdentifier') {
|
|
193
|
+
return attr.name.name;
|
|
194
|
+
}
|
|
195
|
+
return this.j(attr.name).toSource();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private getAttributeReason(attr: JSXAttribute): string {
|
|
199
|
+
if (!attr.value) return 'has no value';
|
|
200
|
+
|
|
201
|
+
if (attr.value.type === 'JSXExpressionContainer') {
|
|
202
|
+
const expr = attr.value.expression;
|
|
203
|
+
const expressionType = expr.type.replace('Expression', '').toLowerCase();
|
|
204
|
+
|
|
205
|
+
// Show actual value for simple cases
|
|
206
|
+
if (expr.type === 'Identifier' || expr.type === 'MemberExpression') {
|
|
207
|
+
const valueText = this.j(expr).toSource();
|
|
208
|
+
return `contains a ${expressionType} (${valueText})`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return `contains a complex ${expressionType} expression`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return 'needs manual review';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private addIssue(message: string): void {
|
|
218
|
+
this.issues.push(message);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const createReporter = (j: JSCodeshift, issues: string[]): CodemodReporter => {
|
|
223
|
+
return new CodemodReporter({ jscodeshift: j, issues });
|
|
224
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Options } from 'jscodeshift';
|
|
2
|
+
|
|
3
|
+
interface PackageRequirement {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if package requirements are met
|
|
10
|
+
* Returns true if all packages are found, false otherwise
|
|
11
|
+
*/
|
|
12
|
+
export function validatePackageRequirements(
|
|
13
|
+
options: Options,
|
|
14
|
+
requirements: PackageRequirement[],
|
|
15
|
+
): boolean {
|
|
16
|
+
let packageResults: Record<string, boolean> = {};
|
|
17
|
+
|
|
18
|
+
// Handle both string and object cases
|
|
19
|
+
if (options.packageResults) {
|
|
20
|
+
if (typeof options.packageResults === 'string') {
|
|
21
|
+
// Parse JSON string
|
|
22
|
+
try {
|
|
23
|
+
if (options.packageResults.trim() && options.packageResults.trim().startsWith('{')) {
|
|
24
|
+
packageResults = JSON.parse(options.packageResults) as Record<string, boolean>;
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.debug('Failed to parse packageResults:', error);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
} else if (typeof options.packageResults === 'object') {
|
|
31
|
+
// Already an object, use directly
|
|
32
|
+
packageResults = options.packageResults as Record<string, boolean>;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if all requirements are met
|
|
37
|
+
const allRequirementsMet = requirements.every((req) => {
|
|
38
|
+
const key = `${req.name}@${req.version}`;
|
|
39
|
+
return packageResults[key];
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!allRequirementsMet) {
|
|
43
|
+
const missing = requirements.filter((req) => {
|
|
44
|
+
const key = `${req.name}@${req.version}`;
|
|
45
|
+
return !packageResults[key];
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const missingPackages = missing.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ');
|
|
49
|
+
console.debug(`Skipping transform - missing required packages: ${missingPackages}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return allRequirementsMet;
|
|
53
|
+
}
|