@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.
- package/package.json +6 -1
- package/.changeset/better-impalas-drop.md +0 -5
- package/.changeset/config.json +0 -13
- package/.changeset/quick-mails-joke.md +0 -128
- package/.github/CODEOWNERS +0 -1
- package/.github/actions/bootstrap/action.yml +0 -49
- package/.github/actions/commitlint/action.yml +0 -27
- package/.github/actions/test/action.yml +0 -23
- package/.github/workflows/cd-cd.yml +0 -150
- package/.github/workflows/renovate.yml +0 -16
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -1
- package/.nvmrc +0 -1
- package/.prettierignore +0 -1
- package/.prettierrc.js +0 -5
- package/DEVELOPER.md +0 -783
- package/babel.config.js +0 -28
- package/commitlint.config.js +0 -3
- package/eslint.config.js +0 -15
- package/jest.config.js +0 -9
- package/mkdocs.yml +0 -4
- package/renovate.json +0 -9
- package/src/__tests__/runCodemod.test.ts +0 -96
- package/src/index.ts +0 -4
- package/src/runCodemod.ts +0 -88
- package/src/transforms/button/__tests__/button.test.tsx +0 -153
- package/src/transforms/button/transformer.ts +0 -418
- package/src/transforms/helpers/__tests__/createTestTransform.test.ts +0 -27
- package/src/transforms/helpers/__tests__/hasImport.test.ts +0 -52
- package/src/transforms/helpers/__tests__/iconUtils.test.ts +0 -207
- package/src/transforms/helpers/__tests__/jsxElementUtils.test.ts +0 -130
- package/src/transforms/helpers/__tests__/jsxReportingUtils.test.ts +0 -265
- package/src/transforms/helpers/createTestTransform.ts +0 -18
- package/src/transforms/helpers/hasImport.ts +0 -60
- package/src/transforms/helpers/iconUtils.ts +0 -87
- package/src/transforms/helpers/index.ts +0 -5
- package/src/transforms/helpers/jsxElementUtils.ts +0 -67
- package/src/transforms/helpers/jsxReportingUtils.ts +0 -224
- package/src/utils/__tests__/getOptions.test.ts +0 -170
- package/src/utils/__tests__/handleError.test.ts +0 -18
- package/src/utils/__tests__/loadTransformModules.test.ts +0 -51
- package/src/utils/__tests__/reportManualReview.test.ts +0 -42
- package/src/utils/getOptions.ts +0 -63
- package/src/utils/handleError.ts +0 -6
- package/src/utils/index.ts +0 -4
- package/src/utils/loadTransformModules.ts +0 -28
- package/src/utils/reportManualReview.ts +0 -17
- package/test-button.tsx +0 -230
- package/test-file.js +0 -2
- package/tsconfig.json +0 -14
- package/tsdown.config.js +0 -13
|
@@ -1,224 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
3
|
-
import { confirm, input, select as list } from '@inquirer/prompts';
|
|
4
|
-
|
|
5
|
-
import getOptions from '../getOptions';
|
|
6
|
-
|
|
7
|
-
jest.mock('@inquirer/prompts', () => ({
|
|
8
|
-
select: jest.fn(),
|
|
9
|
-
input: jest.fn(),
|
|
10
|
-
confirm: jest.fn(),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
describe('getOptions', () => {
|
|
14
|
-
const transformFiles = ['fileA.js', 'fileB.ts', 'fileC.tsx'];
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
process.argv = ['node', 'script.js'];
|
|
18
|
-
(list as jest.Mock).mockClear();
|
|
19
|
-
(input as jest.Mock).mockClear();
|
|
20
|
-
(confirm as jest.Mock).mockClear();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should return options from command line arguments when provided', async () => {
|
|
24
|
-
process.argv = ['node', 'script.js', 'fileB.ts', './src', '--dry', '--print'];
|
|
25
|
-
const options = await getOptions(transformFiles);
|
|
26
|
-
expect(options).toEqual({
|
|
27
|
-
transformFile: 'fileB.ts',
|
|
28
|
-
targetPath: './src',
|
|
29
|
-
dry: true,
|
|
30
|
-
print: true,
|
|
31
|
-
gitignore: true,
|
|
32
|
-
ignorePattern: undefined,
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should parse --ignore-pattern argument from CLI', async () => {
|
|
37
|
-
process.argv = [
|
|
38
|
-
'node',
|
|
39
|
-
'script.js',
|
|
40
|
-
'fileA.js',
|
|
41
|
-
'./src',
|
|
42
|
-
'--ignore-pattern',
|
|
43
|
-
'node_modules/**',
|
|
44
|
-
];
|
|
45
|
-
const options = await getOptions(transformFiles);
|
|
46
|
-
expect(options.ignorePattern).toBe('node_modules/**');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should parse --gitignore and --no-gitignore arguments from CLI and prioritize --gitignore', async () => {
|
|
50
|
-
process.argv = ['node', 'script.js', 'fileA.js', './src', '--gitignore', '--no-gitignore'];
|
|
51
|
-
const options = await getOptions(transformFiles);
|
|
52
|
-
expect(options.gitignore).toBe(true);
|
|
53
|
-
|
|
54
|
-
process.argv = ['node', 'script.js', 'fileA.js', './src', '--no-gitignore'];
|
|
55
|
-
const optionsNo = await getOptions(transformFiles);
|
|
56
|
-
expect(optionsNo.gitignore).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should prompt for ignorePattern and gitignore when no CLI args', async () => {
|
|
60
|
-
(list as jest.Mock).mockResolvedValue('fileB.ts');
|
|
61
|
-
(input as jest.Mock).mockResolvedValueOnce('./output').mockResolvedValueOnce('node_modules/**');
|
|
62
|
-
(confirm as jest.Mock)
|
|
63
|
-
.mockResolvedValueOnce(true)
|
|
64
|
-
.mockResolvedValueOnce(false)
|
|
65
|
-
.mockResolvedValueOnce(true);
|
|
66
|
-
|
|
67
|
-
const optionsPromise = getOptions(transformFiles);
|
|
68
|
-
const options = await optionsPromise;
|
|
69
|
-
|
|
70
|
-
expect(input).toHaveBeenCalledWith({
|
|
71
|
-
message: 'Enter ignore pattern(s) (comma separated) or leave empty:',
|
|
72
|
-
validate: expect.any(Function),
|
|
73
|
-
});
|
|
74
|
-
expect(confirm).toHaveBeenCalledWith({
|
|
75
|
-
message: 'Respect .gitignore files?',
|
|
76
|
-
default: true,
|
|
77
|
-
});
|
|
78
|
-
expect(options.ignorePattern).toBe('node_modules/**');
|
|
79
|
-
expect(options.gitignore).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should throw an error if transform file is invalid via command line', async () => {
|
|
83
|
-
process.argv = ['node', 'script.js', 'invalid.js', './dist'];
|
|
84
|
-
await expect(getOptions(transformFiles)).rejects.toThrow('Invalid transform file specified.');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should throw an error if target path is empty via command line', async () => {
|
|
88
|
-
process.argv = ['node', 'script.js', 'fileA.js', ''];
|
|
89
|
-
await expect(getOptions(transformFiles)).rejects.toThrow('Target path cannot be empty.');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should prompt for transform file, target path, dry mode, and print when no arguments are provided', async () => {
|
|
93
|
-
(list as jest.Mock).mockResolvedValue('fileB.ts');
|
|
94
|
-
(input as jest.Mock).mockResolvedValue('./output');
|
|
95
|
-
(confirm as jest.Mock).mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
|
96
|
-
|
|
97
|
-
const options = await getOptions(transformFiles);
|
|
98
|
-
|
|
99
|
-
expect(list).toHaveBeenCalledWith({
|
|
100
|
-
message: 'Select a codemod transform to run:',
|
|
101
|
-
choices: transformFiles.map((file) => ({ name: file, value: file })),
|
|
102
|
-
});
|
|
103
|
-
expect(input).toHaveBeenCalledWith({
|
|
104
|
-
message: 'Enter the target directory or file path to run codemod on:',
|
|
105
|
-
validate: expect.any(Function),
|
|
106
|
-
});
|
|
107
|
-
expect(confirm).toHaveBeenCalledWith({
|
|
108
|
-
message: 'Run in dry mode (no changes written to files)?',
|
|
109
|
-
default: true,
|
|
110
|
-
});
|
|
111
|
-
expect(confirm).toHaveBeenCalledWith({
|
|
112
|
-
message: 'Print transformed source to console?',
|
|
113
|
-
default: false,
|
|
114
|
-
});
|
|
115
|
-
expect(options).toEqual({
|
|
116
|
-
transformFile: 'fileB.ts',
|
|
117
|
-
targetPath: './output',
|
|
118
|
-
dry: true,
|
|
119
|
-
print: false,
|
|
120
|
-
gitignore: undefined,
|
|
121
|
-
ignorePattern: './output',
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should handle target path validation error', async () => {
|
|
126
|
-
(list as jest.Mock).mockResolvedValue('fileA.js');
|
|
127
|
-
(input as jest.Mock).mockResolvedValueOnce(' ');
|
|
128
|
-
(confirm as jest.Mock).mockResolvedValue(true);
|
|
129
|
-
|
|
130
|
-
await getOptions(transformFiles);
|
|
131
|
-
|
|
132
|
-
expect(input).toHaveBeenCalledTimes(2);
|
|
133
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
134
|
-
const validateFn = (input as jest.Mock).mock.calls[0][0].validate!;
|
|
135
|
-
expect(validateFn(' ')).toBe('Target path cannot be empty');
|
|
136
|
-
expect(validateFn('./valid-path')).toBe(true); // Ensure valid input passes validation
|
|
137
|
-
expect(input).toHaveBeenCalledWith({
|
|
138
|
-
message: 'Enter the target directory or file path to run codemod on:',
|
|
139
|
-
validate: expect.any(Function),
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should allow changing the default dry mode', async () => {
|
|
144
|
-
(list as jest.Mock).mockResolvedValue('fileC.tsx');
|
|
145
|
-
(input as jest.Mock).mockResolvedValue('./another-path');
|
|
146
|
-
(confirm as jest.Mock).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
147
|
-
|
|
148
|
-
const options = await getOptions(transformFiles);
|
|
149
|
-
|
|
150
|
-
expect(confirm).toHaveBeenCalledWith({
|
|
151
|
-
message: 'Run in dry mode (no changes written to files)?',
|
|
152
|
-
default: true,
|
|
153
|
-
});
|
|
154
|
-
expect(options.dry).toBe(false);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should allow changing the default print mode', async () => {
|
|
158
|
-
(list as jest.Mock).mockResolvedValue('fileA.js');
|
|
159
|
-
(input as jest.Mock).mockResolvedValue('./yet-another-path');
|
|
160
|
-
(confirm as jest.Mock).mockResolvedValueOnce(true).mockResolvedValueOnce(true);
|
|
161
|
-
|
|
162
|
-
const options = await getOptions(transformFiles);
|
|
163
|
-
|
|
164
|
-
expect(confirm).toHaveBeenCalledWith({
|
|
165
|
-
message: 'Print transformed source to console?',
|
|
166
|
-
default: false,
|
|
167
|
-
});
|
|
168
|
-
expect(options.print).toBe(true);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import handleError from '../handleError';
|
|
2
|
-
|
|
3
|
-
describe('handleError', () => {
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
6
|
-
throw new Error('process.exit called');
|
|
7
|
-
});
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
jest.restoreAllMocks();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('should throw an error with the given message', () => {
|
|
15
|
-
const errorMessage = 'Test error message';
|
|
16
|
-
expect(() => handleError(errorMessage)).toThrow(new Error('process.exit called'));
|
|
17
|
-
});
|
|
18
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
|
|
3
|
-
import loadTransformModules from '../loadTransformModules';
|
|
4
|
-
|
|
5
|
-
jest.mock('fs', () => ({
|
|
6
|
-
promises: {
|
|
7
|
-
readdir: jest.fn(),
|
|
8
|
-
},
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
jest.mock('path', () => ({
|
|
12
|
-
join: jest.fn((...args) => args.join('/')),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
jest.mock(
|
|
16
|
-
'/mock/transforms/transformA.js',
|
|
17
|
-
() => ({
|
|
18
|
-
default: { default: 'transformA' },
|
|
19
|
-
}),
|
|
20
|
-
{ virtual: true },
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
jest.mock(
|
|
24
|
-
'/mock/transforms/transformB.js',
|
|
25
|
-
() => ({
|
|
26
|
-
default: { default: 'transformB' },
|
|
27
|
-
}),
|
|
28
|
-
{ virtual: true },
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
describe('loadTransformModules - simplified test', () => {
|
|
32
|
-
it('should return transformModules and transformFiles correctly', async () => {
|
|
33
|
-
const mockTransformsDir = '/mock/transforms';
|
|
34
|
-
|
|
35
|
-
const mockFiles = ['transformA.js', 'transformB.js'];
|
|
36
|
-
(fs.readdir as jest.Mock).mockResolvedValue(mockFiles);
|
|
37
|
-
|
|
38
|
-
const { transformModules, transformFiles } = await loadTransformModules(mockTransformsDir);
|
|
39
|
-
|
|
40
|
-
expect(transformModules).toEqual({
|
|
41
|
-
'transformA.js': {
|
|
42
|
-
default: 'transformA',
|
|
43
|
-
},
|
|
44
|
-
'transformB.js': {
|
|
45
|
-
default: 'transformB',
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
expect(transformFiles).toEqual(['transformA', 'transformB']);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
import reportManualReview from '../reportManualReview';
|
|
6
|
-
|
|
7
|
-
const REPORT_PATH = path.resolve(process.cwd(), 'codemod-report.txt');
|
|
8
|
-
|
|
9
|
-
describe('reportManualReview', () => {
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
try {
|
|
12
|
-
await fs.access(REPORT_PATH);
|
|
13
|
-
await fs.unlink(REPORT_PATH);
|
|
14
|
-
} catch {
|
|
15
|
-
// File doesn't exist, that's fine
|
|
16
|
-
}
|
|
17
|
-
await fs.writeFile(REPORT_PATH, '', 'utf8');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
try {
|
|
22
|
-
await fs.access(REPORT_PATH);
|
|
23
|
-
await fs.unlink(REPORT_PATH);
|
|
24
|
-
} catch {
|
|
25
|
-
// File doesn't exist, that's fine
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('writes a manual review entry to the report file', async () => {
|
|
30
|
-
await reportManualReview('src/file.tsx', 'Manual review required: something at line 10');
|
|
31
|
-
const content = await fs.readFile(REPORT_PATH, 'utf8');
|
|
32
|
-
expect(content).toContain('[src/file.tsx:10] Manual review required: something');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('appends multiple manual review entries', async () => {
|
|
36
|
-
await reportManualReview('src/file1.tsx', 'Manual review required: issue 1');
|
|
37
|
-
await reportManualReview('src/file2.tsx', 'Manual review required: issue 2');
|
|
38
|
-
const content = await fs.readFile(REPORT_PATH, 'utf8');
|
|
39
|
-
expect(content).toContain('[src/file1.tsx] Manual review required: issue 1');
|
|
40
|
-
expect(content).toContain('[src/file2.tsx] Manual review required: issue 2');
|
|
41
|
-
});
|
|
42
|
-
});
|
package/src/utils/getOptions.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { confirm, input, select as list } from '@inquirer/prompts';
|
|
2
|
-
|
|
3
|
-
async function getOptions(transformFiles: string[]) {
|
|
4
|
-
const args = process.argv.slice(2);
|
|
5
|
-
if (args.length > 0) {
|
|
6
|
-
const [transformFile, targetPath] = args;
|
|
7
|
-
const dry = args.includes('--dry') || args.includes('--dry-run');
|
|
8
|
-
const print = args.includes('--print');
|
|
9
|
-
const ignorePatternIndex = args.findIndex((arg) => arg === '--ignore-pattern');
|
|
10
|
-
let ignorePattern: string | undefined;
|
|
11
|
-
if (ignorePatternIndex !== -1 && args.length > ignorePatternIndex + 1) {
|
|
12
|
-
ignorePattern = args[ignorePatternIndex + 1];
|
|
13
|
-
}
|
|
14
|
-
const gitignore = args.includes('--gitignore');
|
|
15
|
-
const noGitignore = args.includes('--no-gitignore');
|
|
16
|
-
|
|
17
|
-
if (!transformFile || !transformFiles.includes(transformFile)) {
|
|
18
|
-
throw new Error('Invalid transform file specified.');
|
|
19
|
-
}
|
|
20
|
-
if (!targetPath) {
|
|
21
|
-
throw new Error('Target path cannot be empty.');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// If both --gitignore and --no-gitignore are specified, prioritize --gitignore
|
|
25
|
-
const useGitignore = !!(gitignore || (!gitignore && !noGitignore));
|
|
26
|
-
|
|
27
|
-
return { transformFile, targetPath, dry, print, ignorePattern, gitignore: useGitignore };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const transformFile = await list({
|
|
31
|
-
message: 'Select a codemod transform to run:',
|
|
32
|
-
choices: transformFiles.map((file) => ({ name: file, value: file })),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const targetPath = await input({
|
|
36
|
-
message: 'Enter the target directory or file path to run codemod on:',
|
|
37
|
-
validate: (value) => value.trim() !== '' || 'Target path cannot be empty',
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const dry = await confirm({
|
|
41
|
-
message: 'Run in dry mode (no changes written to files)?',
|
|
42
|
-
default: true,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const print = await confirm({
|
|
46
|
-
message: 'Print transformed source to console?',
|
|
47
|
-
default: false,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const ignorePattern = await input({
|
|
51
|
-
message: 'Enter ignore pattern(s) (comma separated) or leave empty:',
|
|
52
|
-
validate: (value) => true,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const gitignore = await confirm({
|
|
56
|
-
message: 'Respect .gitignore files?',
|
|
57
|
-
default: true,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return { transformFile, targetPath, dry, print, ignorePattern, gitignore };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export default getOptions;
|
package/src/utils/handleError.ts
DELETED
package/src/utils/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
interface TransformModule {
|
|
5
|
-
default: {
|
|
6
|
-
default: unknown;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async function loadTransformModules(transformsDir: string) {
|
|
11
|
-
let transformModules: Record<string, unknown> = {};
|
|
12
|
-
|
|
13
|
-
const files = await fs.readdir(transformsDir);
|
|
14
|
-
const transformFiles = await Promise.all(
|
|
15
|
-
files
|
|
16
|
-
.filter((file) => file.endsWith('.js'))
|
|
17
|
-
.map(async (file) => {
|
|
18
|
-
const transformPath = path.join(transformsDir, file);
|
|
19
|
-
const transformModule = (await import(transformPath)) as TransformModule;
|
|
20
|
-
transformModules = { ...transformModules, [file]: transformModule.default.default };
|
|
21
|
-
return file.replace('.js', '');
|
|
22
|
-
}),
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
return { transformModules, transformFiles };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export default loadTransformModules;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
const REPORT_PATH = path.resolve(process.cwd(), 'codemod-report.txt');
|
|
6
|
-
|
|
7
|
-
const reportManualReview = async (filePath: string, message: string): Promise<void> => {
|
|
8
|
-
const lineMatch = /at line (\d+)/u.exec(message);
|
|
9
|
-
const lineNumber = lineMatch?.[1];
|
|
10
|
-
|
|
11
|
-
const cleanMessage = message.replace(/ at line \d+/u, '');
|
|
12
|
-
const lineInfo = lineNumber ? `:${lineNumber}` : '';
|
|
13
|
-
|
|
14
|
-
await fs.appendFile(REPORT_PATH, `[${filePath}${lineInfo}] ${cleanMessage}\n`, 'utf8');
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export default reportManualReview;
|