@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.
Files changed (62) hide show
  1. package/.changeset/better-impalas-drop.md +5 -0
  2. package/.changeset/config.json +13 -0
  3. package/.github/CODEOWNERS +1 -0
  4. package/.github/actions/bootstrap/action.yml +49 -0
  5. package/.github/actions/commitlint/action.yml +27 -0
  6. package/.github/actions/test/action.yml +23 -0
  7. package/.github/workflows/cd-cd.yml +127 -0
  8. package/.github/workflows/renovate.yml +16 -0
  9. package/.husky/commit-msg +1 -0
  10. package/.husky/pre-commit +1 -0
  11. package/.nvmrc +1 -0
  12. package/.prettierignore +1 -0
  13. package/.prettierrc.js +5 -0
  14. package/README.md +184 -0
  15. package/babel.config.js +28 -0
  16. package/codemod-report.md +81 -0
  17. package/commitlint.config.js +3 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +2448 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/transforms/button.d.ts +20 -0
  22. package/dist/transforms/button.js +640 -0
  23. package/dist/transforms/button.js.map +1 -0
  24. package/eslint.config.js +15 -0
  25. package/jest.config.js +9 -0
  26. package/mkdocs.yml +4 -0
  27. package/package.json +68 -0
  28. package/renovate.json +9 -0
  29. package/scripts/build.sh +10 -0
  30. package/src/__tests__/runCodemod.test.ts +109 -0
  31. package/src/index.ts +4 -0
  32. package/src/runCodemod.ts +149 -0
  33. package/src/transforms/button/__tests__/button.test.tsx +175 -0
  34. package/src/transforms/button/button.ts +453 -0
  35. package/src/transforms/helpers/__tests__/createTestTransform.test.ts +27 -0
  36. package/src/transforms/helpers/__tests__/hasImport.test.ts +52 -0
  37. package/src/transforms/helpers/__tests__/iconUtils.test.ts +207 -0
  38. package/src/transforms/helpers/__tests__/jsxElementUtils.test.ts +130 -0
  39. package/src/transforms/helpers/__tests__/jsxReportingUtils.test.ts +265 -0
  40. package/src/transforms/helpers/__tests__/packageValidation.test.ts +45 -0
  41. package/src/transforms/helpers/createTestTransform.ts +59 -0
  42. package/src/transforms/helpers/hasImport.ts +60 -0
  43. package/src/transforms/helpers/iconUtils.ts +87 -0
  44. package/src/transforms/helpers/index.ts +5 -0
  45. package/src/transforms/helpers/jsxElementUtils.ts +67 -0
  46. package/src/transforms/helpers/jsxReportingUtils.ts +224 -0
  47. package/src/transforms/helpers/packageValidation.ts +53 -0
  48. package/src/utils/__tests__/getOptions.test.ts +219 -0
  49. package/src/utils/__tests__/handleError.test.ts +18 -0
  50. package/src/utils/__tests__/hasPackageVersion.test.ts +191 -0
  51. package/src/utils/__tests__/loadTransformModules.test.ts +51 -0
  52. package/src/utils/__tests__/reportManualReview.test.ts +42 -0
  53. package/src/utils/getOptions.ts +78 -0
  54. package/src/utils/handleError.ts +6 -0
  55. package/src/utils/hasPackageVersion.ts +482 -0
  56. package/src/utils/index.ts +4 -0
  57. package/src/utils/loadTransformModules.ts +28 -0
  58. package/src/utils/reportManualReview.ts +17 -0
  59. package/test-button.tsx +230 -0
  60. package/test-file.js +2 -0
  61. package/tsconfig.json +14 -0
  62. package/tsup.config.js +13 -0
@@ -0,0 +1,219 @@
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
+ isMonorepo: false,
34
+ });
35
+ });
36
+
37
+ it('should parse --ignore-pattern argument from CLI', async () => {
38
+ process.argv = [
39
+ 'node',
40
+ 'script.js',
41
+ 'fileA.js',
42
+ './src',
43
+ '--ignore-pattern',
44
+ 'node_modules/**',
45
+ ];
46
+ const options = await getOptions(transformFiles);
47
+ expect(options.ignorePattern).toBe('node_modules/**');
48
+ expect(options.isMonorepo).toBe(false);
49
+ });
50
+
51
+ it('should parse --monorepo argument from CLI', async () => {
52
+ process.argv = ['node', 'script.js', 'fileA.js', './packages', '--monorepo'];
53
+ const options = await getOptions(transformFiles);
54
+ expect(options.isMonorepo).toBe(true);
55
+ });
56
+
57
+ it('should parse --gitignore and --no-gitignore arguments from CLI and prioritize --gitignore', async () => {
58
+ process.argv = ['node', 'script.js', 'fileA.js', './src', '--gitignore', '--no-gitignore'];
59
+ const options = await getOptions(transformFiles);
60
+ expect(options.gitignore).toBe(true);
61
+
62
+ process.argv = ['node', 'script.js', 'fileA.js', './src', '--no-gitignore'];
63
+ const optionsNo = await getOptions(transformFiles);
64
+ expect(optionsNo.gitignore).toBe(false);
65
+ });
66
+
67
+ it('should prompt for ignorePattern and gitignore when no CLI args', async () => {
68
+ (list as jest.Mock).mockResolvedValue('fileB.ts');
69
+ (input as jest.Mock).mockResolvedValueOnce('./output').mockResolvedValueOnce('node_modules/**');
70
+ (confirm as jest.Mock)
71
+ .mockResolvedValueOnce(true)
72
+ .mockResolvedValueOnce(true)
73
+ .mockResolvedValueOnce(false)
74
+ .mockResolvedValueOnce(true);
75
+
76
+ const options = await getOptions(transformFiles);
77
+
78
+ expect(input).toHaveBeenCalledWith({
79
+ message: 'Enter ignore pattern(s) (comma separated) or leave empty:',
80
+ validate: expect.any(Function),
81
+ });
82
+ expect(confirm).toHaveBeenCalledWith({
83
+ message: 'Respect .gitignore files?',
84
+ default: true,
85
+ });
86
+ expect(options.ignorePattern).toBe('node_modules/**');
87
+ expect(options.gitignore).toBe(true);
88
+ expect(options.isMonorepo).toBe(true);
89
+ });
90
+
91
+ it('should prompt for monorepo when target path looks like monorepo directory', async () => {
92
+ (list as jest.Mock).mockResolvedValue('fileB.ts');
93
+ (input as jest.Mock).mockResolvedValueOnce('./packages').mockResolvedValueOnce('');
94
+ (confirm as jest.Mock)
95
+ .mockResolvedValueOnce(true)
96
+ .mockResolvedValueOnce(true)
97
+ .mockResolvedValueOnce(false)
98
+ .mockResolvedValueOnce(true);
99
+
100
+ const options = await getOptions(transformFiles);
101
+
102
+ expect(confirm).toHaveBeenCalledWith({
103
+ message: 'Are you targeting a monorepo packages/apps directory?',
104
+ default: true,
105
+ });
106
+ expect(options.isMonorepo).toBe(true);
107
+ });
108
+
109
+ it('should prompt for transform file, target path, dry mode, and print when no arguments are provided', async () => {
110
+ (list as jest.Mock).mockResolvedValue('fileB.ts');
111
+ (input as jest.Mock).mockResolvedValueOnce('./output').mockResolvedValueOnce('');
112
+ (confirm as jest.Mock)
113
+ .mockResolvedValueOnce(false)
114
+ .mockResolvedValueOnce(true)
115
+ .mockResolvedValueOnce(false)
116
+ .mockResolvedValueOnce(true);
117
+
118
+ const options = await getOptions(transformFiles);
119
+
120
+ expect(list).toHaveBeenCalledWith({
121
+ message: 'Select a codemod transform to run:',
122
+ choices: transformFiles.map((file) => ({ name: file, value: file })),
123
+ });
124
+ expect(input).toHaveBeenCalledWith({
125
+ message: 'Enter the target directory or file path to run codemod on:',
126
+ validate: expect.any(Function),
127
+ });
128
+ expect(confirm).toHaveBeenCalledWith({
129
+ message: 'Run in dry mode (no changes written to files)?',
130
+ default: true,
131
+ });
132
+ expect(confirm).toHaveBeenCalledWith({
133
+ message: 'Print transformed source to console?',
134
+ default: false,
135
+ });
136
+ expect(options).toEqual({
137
+ transformFile: 'fileB.ts',
138
+ targetPath: './output',
139
+ dry: true,
140
+ print: false,
141
+ gitignore: true,
142
+ ignorePattern: '',
143
+ isMonorepo: false,
144
+ });
145
+ });
146
+
147
+ it('should handle target path validation error', async () => {
148
+ (list as jest.Mock).mockResolvedValue('fileA.js');
149
+ (input as jest.Mock).mockResolvedValueOnce('./valid-path').mockResolvedValueOnce('');
150
+ (confirm as jest.Mock)
151
+ .mockResolvedValueOnce(false)
152
+ .mockResolvedValueOnce(true)
153
+ .mockResolvedValueOnce(false)
154
+ .mockResolvedValueOnce(true);
155
+
156
+ await getOptions(transformFiles);
157
+
158
+ expect(input).toHaveBeenCalledTimes(2);
159
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
160
+ const validateFn = (input as jest.Mock).mock.calls[0][0].validate!;
161
+ expect(validateFn(' ')).toBe('Target path cannot be empty');
162
+ expect(validateFn('./valid-path')).toBe(true);
163
+ expect(input).toHaveBeenCalledWith({
164
+ message: 'Enter the target directory or file path to run codemod on:',
165
+ validate: expect.any(Function),
166
+ });
167
+ });
168
+
169
+ it('should allow changing the default dry mode', async () => {
170
+ (list as jest.Mock).mockResolvedValue('fileC.tsx');
171
+ (input as jest.Mock).mockResolvedValueOnce('./another-path').mockResolvedValueOnce('');
172
+ (confirm as jest.Mock)
173
+ .mockResolvedValueOnce(false)
174
+ .mockResolvedValueOnce(false)
175
+ .mockResolvedValueOnce(false)
176
+ .mockResolvedValueOnce(true);
177
+
178
+ const options = await getOptions(transformFiles);
179
+
180
+ expect(confirm).toHaveBeenCalledWith({
181
+ message: 'Run in dry mode (no changes written to files)?',
182
+ default: true,
183
+ });
184
+ expect(options.dry).toBe(false);
185
+ });
186
+
187
+ it('should allow changing the default print mode', async () => {
188
+ (list as jest.Mock).mockResolvedValue('fileA.js');
189
+ (input as jest.Mock).mockResolvedValueOnce('./yet-another-path').mockResolvedValueOnce('');
190
+ (confirm as jest.Mock)
191
+ .mockResolvedValueOnce(false)
192
+ .mockResolvedValueOnce(true)
193
+ .mockResolvedValueOnce(true)
194
+ .mockResolvedValueOnce(true);
195
+
196
+ const options = await getOptions(transformFiles);
197
+
198
+ expect(confirm).toHaveBeenCalledWith({
199
+ message: 'Print transformed source to console?',
200
+ default: false,
201
+ });
202
+ expect(options.print).toBe(true);
203
+ });
204
+
205
+ it('should not prompt for monorepo when target path does not look like monorepo directory', async () => {
206
+ (list as jest.Mock).mockResolvedValue('fileA.js');
207
+ (input as jest.Mock).mockResolvedValueOnce('./src').mockResolvedValueOnce('');
208
+ (confirm as jest.Mock)
209
+ .mockResolvedValueOnce(false)
210
+ .mockResolvedValueOnce(true)
211
+ .mockResolvedValueOnce(false)
212
+ .mockResolvedValueOnce(true);
213
+
214
+ const options = await getOptions(transformFiles);
215
+
216
+ expect(confirm).toHaveBeenCalledTimes(4);
217
+ expect(options.isMonorepo).toBe(false);
218
+ });
219
+ });
@@ -0,0 +1,18 @@
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
+ });
@@ -0,0 +1,191 @@
1
+ import { existsSync, readdirSync, readFileSync, type Stats, statSync } from 'node:fs';
2
+
3
+ import hasPackageVersion, { packageVersionCache } from '../hasPackageVersion';
4
+
5
+ jest.mock('node:fs');
6
+
7
+ const mockExistsSync = existsSync as jest.MockedFunction<typeof existsSync>;
8
+ const mockReadFileSync = readFileSync as jest.MockedFunction<typeof readFileSync>;
9
+ const mockReaddirSync = readdirSync as jest.MockedFunction<typeof readdirSync>;
10
+ const mockStatSync = statSync as jest.MockedFunction<typeof statSync>;
11
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
12
+
13
+ describe('hasPackageVersion', () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ mockExistsSync.mockReturnValue(false);
17
+
18
+ const mockStats: Partial<Stats> = {
19
+ isFile: jest.fn(() => false),
20
+ isDirectory: jest.fn(() => true),
21
+ isBlockDevice: jest.fn(() => false),
22
+ isCharacterDevice: jest.fn(() => false),
23
+ isFIFO: jest.fn(() => false),
24
+ isSocket: jest.fn(() => false),
25
+ isSymbolicLink: jest.fn(() => false),
26
+ };
27
+
28
+ mockStatSync.mockReturnValue(mockStats as Stats);
29
+ packageVersionCache.clear();
30
+ consoleSpy.mockClear();
31
+ });
32
+
33
+ afterAll(() => {
34
+ consoleSpy.mockRestore();
35
+ });
36
+
37
+ it('finds package in package.json dependencies', () => {
38
+ mockExistsSync.mockImplementation((path) => {
39
+ const pathStr = String(path);
40
+ return pathStr.endsWith('package.json') && !pathStr.includes('node_modules');
41
+ });
42
+ mockReadFileSync.mockReturnValue(
43
+ JSON.stringify({
44
+ dependencies: {
45
+ '@transferwise/components': '46.5.0',
46
+ },
47
+ }),
48
+ );
49
+
50
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(true);
51
+ });
52
+
53
+ it('finds package in standard node_modules directory', () => {
54
+ mockExistsSync.mockImplementation((path) => {
55
+ const pathStr = String(path);
56
+ return (
57
+ pathStr.includes('node_modules/@transferwise/components/package.json') &&
58
+ !pathStr.includes('.pnpm')
59
+ );
60
+ });
61
+ mockReadFileSync.mockReturnValue(
62
+ JSON.stringify({
63
+ version: '46.5.0',
64
+ }),
65
+ );
66
+
67
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(true);
68
+ });
69
+
70
+ it('finds package in pnpm node_modules structure', () => {
71
+ mockExistsSync.mockImplementation((path) => {
72
+ const pathStr = String(path);
73
+ return (
74
+ pathStr.includes('node_modules/.pnpm') ||
75
+ pathStr.includes(
76
+ '@transferwise+components@46.5.0/node_modules/@transferwise/components/package.json',
77
+ )
78
+ );
79
+ });
80
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
81
+ mockReaddirSync.mockReturnValue([
82
+ '@transferwise+components@46.5.0',
83
+ 'other-package@1.0.0',
84
+ ] as any);
85
+ mockReadFileSync.mockReturnValue(
86
+ JSON.stringify({
87
+ version: '46.5.0',
88
+ }),
89
+ );
90
+
91
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(true);
92
+ });
93
+
94
+ it('returns false when package is not found', () => {
95
+ mockExistsSync.mockReturnValue(false);
96
+
97
+ expect(hasPackageVersion('@nonexistent/package', '>=1.0.0')).toBe(false);
98
+ });
99
+
100
+ it('returns false when version does not satisfy requirement', () => {
101
+ mockExistsSync.mockImplementation((path) => {
102
+ const pathStr = String(path);
103
+ return pathStr.endsWith('package.json') && !pathStr.includes('node_modules');
104
+ });
105
+ mockReadFileSync.mockReturnValue(
106
+ JSON.stringify({
107
+ dependencies: {
108
+ '@transferwise/components': '40.0.0',
109
+ },
110
+ }),
111
+ );
112
+
113
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(false);
114
+ });
115
+
116
+ it('handles malformed JSON gracefully', () => {
117
+ mockExistsSync.mockImplementation((path) => {
118
+ const pathStr = String(path);
119
+ return pathStr.endsWith('package.json') && !pathStr.includes('node_modules');
120
+ });
121
+ mockReadFileSync.mockReturnValue('invalid json');
122
+
123
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(false);
124
+ });
125
+
126
+ it('finds package in devDependencies', () => {
127
+ mockExistsSync.mockImplementation((path) => {
128
+ const pathStr = String(path);
129
+ return pathStr.endsWith('package.json') && !pathStr.includes('node_modules');
130
+ });
131
+ mockReadFileSync.mockReturnValue(
132
+ JSON.stringify({
133
+ devDependencies: {
134
+ '@transferwise/components': '46.5.0',
135
+ },
136
+ }),
137
+ );
138
+
139
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(true);
140
+ });
141
+
142
+ it('finds package in peerDependencies', () => {
143
+ mockExistsSync.mockImplementation((path) => {
144
+ const pathStr = String(path);
145
+ return pathStr.endsWith('package.json') && !pathStr.includes('node_modules');
146
+ });
147
+ mockReadFileSync.mockReturnValue(
148
+ JSON.stringify({
149
+ peerDependencies: {
150
+ '@transferwise/components': '>=46.5.0',
151
+ },
152
+ }),
153
+ );
154
+
155
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(true);
156
+ });
157
+
158
+ it('handles complex version ranges', () => {
159
+ mockExistsSync.mockImplementation((path) => {
160
+ const pathStr = String(path);
161
+ return pathStr.endsWith('package.json') && !pathStr.includes('node_modules');
162
+ });
163
+ mockReadFileSync.mockReturnValue(
164
+ JSON.stringify({
165
+ dependencies: {
166
+ '@transferwise/components': '^46.5.0',
167
+ },
168
+ }),
169
+ );
170
+
171
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(true);
172
+ });
173
+
174
+ it('uses cache for repeated calls', () => {
175
+ mockExistsSync.mockImplementation((path) => {
176
+ const pathStr = String(path);
177
+ return pathStr.endsWith('package.json') && !pathStr.includes('node_modules');
178
+ });
179
+ mockReadFileSync.mockReturnValue(
180
+ JSON.stringify({
181
+ dependencies: {
182
+ '@transferwise/components': '46.5.0',
183
+ },
184
+ }),
185
+ );
186
+
187
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(true);
188
+ expect(hasPackageVersion('@transferwise/components', '>=46.5.0')).toBe(true);
189
+ expect(mockReadFileSync).toHaveBeenCalledTimes(2);
190
+ });
191
+ });
@@ -0,0 +1,51 @@
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
+ });
@@ -0,0 +1,42 @@
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
+ });
@@ -0,0 +1,78 @@
1
+ import { confirm, input, select as list } from '@inquirer/prompts';
2
+ import path from 'path';
3
+
4
+ async function getOptions(transformFiles: string[]) {
5
+ const args = process.argv.slice(2);
6
+ if (args.length > 0) {
7
+ const [transformFile, targetPath] = args;
8
+ const dry = args.includes('--dry') || args.includes('--dry-run');
9
+ const print = args.includes('--print');
10
+ const ignorePatternIndex = args.findIndex((arg) => arg === '--ignore-pattern');
11
+ let ignorePattern: string | undefined;
12
+ if (ignorePatternIndex !== -1 && args.length > ignorePatternIndex + 1) {
13
+ ignorePattern = args[ignorePatternIndex + 1];
14
+ }
15
+ const gitignore = args.includes('--gitignore');
16
+ const noGitignore = args.includes('--no-gitignore');
17
+ const isMonorepo = args.includes('--monorepo');
18
+
19
+ if (!transformFile || !transformFiles.includes(transformFile)) {
20
+ throw new Error('Invalid transform file specified.');
21
+ }
22
+ if (!targetPath) {
23
+ throw new Error('Target path cannot be empty.');
24
+ }
25
+
26
+ // If both --gitignore and --no-gitignore are specified, prioritize --gitignore
27
+ const useGitignore = !!(gitignore || (!gitignore && !noGitignore));
28
+
29
+ return {
30
+ transformFile,
31
+ targetPath,
32
+ dry,
33
+ print,
34
+ ignorePattern,
35
+ gitignore: useGitignore,
36
+ isMonorepo,
37
+ };
38
+ }
39
+
40
+ const transformFile = await list({
41
+ message: 'Select a codemod transform to run:',
42
+ choices: transformFiles.map((file) => ({ name: file, value: file })),
43
+ });
44
+
45
+ const targetPath = await input({
46
+ message: 'Enter the target directory or file path to run codemod on:',
47
+ validate: (value) => value.trim() !== '' || 'Target path cannot be empty',
48
+ });
49
+
50
+ const isMonorepo = await confirm({
51
+ message: 'Are you targeting a monorepo packages/apps directory?',
52
+ default: true,
53
+ });
54
+
55
+ const dry = await confirm({
56
+ message: 'Run in dry mode (no changes written to files)?',
57
+ default: true,
58
+ });
59
+
60
+ const print = await confirm({
61
+ message: 'Print transformed source to console?',
62
+ default: false,
63
+ });
64
+
65
+ const ignorePattern = await input({
66
+ message: 'Enter ignore pattern(s) (comma separated) or leave empty:',
67
+ validate: (value) => true,
68
+ });
69
+
70
+ const gitignore = await confirm({
71
+ message: 'Respect .gitignore files?',
72
+ default: true,
73
+ });
74
+
75
+ return { transformFile, targetPath, dry, print, ignorePattern, gitignore, isMonorepo };
76
+ }
77
+
78
+ export default getOptions;
@@ -0,0 +1,6 @@
1
+ function handleError(message: string): void {
2
+ console.error(message);
3
+ process.exit(1);
4
+ }
5
+
6
+ export default handleError;