@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,482 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import semver from 'semver';
5
+
6
+ interface PackageJson {
7
+ name?: string;
8
+ version?: string;
9
+ dependencies?: Record<string, string>;
10
+ devDependencies?: Record<string, string>;
11
+ peerDependencies?: Record<string, string>;
12
+ optionalDependencies?: Record<string, string>;
13
+ workspaces?: string[] | { packages: string[] };
14
+ }
15
+
16
+ interface CheckOptions {
17
+ silent: boolean;
18
+ limitedScope: boolean;
19
+ checkType?: 'dependencies' | 'nodeModules';
20
+ }
21
+
22
+ export const packageVersionCache = new Map<string, boolean>();
23
+
24
+ export default function hasPackageVersion(
25
+ packageName: string,
26
+ versionRequirement: string,
27
+ ): boolean {
28
+ return hasPackageVersionFromPath(packageName, versionRequirement, process.cwd(), false);
29
+ }
30
+
31
+ export function hasPackageVersionFromPath(
32
+ packageName: string,
33
+ versionRequirement: string,
34
+ startPath: string,
35
+ isMonorepo = false,
36
+ ): boolean {
37
+ const cacheKey = `${packageName}@${versionRequirement}@${startPath}@${isMonorepo}`;
38
+
39
+ if (packageVersionCache.has(cacheKey)) {
40
+ return packageVersionCache.get(cacheKey)!;
41
+ }
42
+
43
+ // Find project root and check for workspace packages
44
+ const projectRoot = findProjectRoot(startPath);
45
+ const workspacePackages = projectRoot ? getWorkspacePackages(projectRoot) : [];
46
+
47
+ const result =
48
+ workspacePackages.length > 0
49
+ ? checkWorkspacePackages(projectRoot!, packageName, versionRequirement, workspacePackages)
50
+ : isMonorepo
51
+ ? checkMonorepoPackages(startPath, packageName, versionRequirement)
52
+ : checkSinglePackage(startPath, packageName, versionRequirement);
53
+
54
+ packageVersionCache.set(cacheKey, result);
55
+ return result;
56
+ }
57
+
58
+ function getWorkspacePackages(projectRoot: string): string[] {
59
+ const packages: string[] = [];
60
+
61
+ // Check package.json workspaces
62
+ const packageJsonPath = path.join(projectRoot, 'package.json');
63
+ if (existsSync(packageJsonPath)) {
64
+ try {
65
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as PackageJson;
66
+ if (packageJson.workspaces) {
67
+ const workspaces = Array.isArray(packageJson.workspaces)
68
+ ? packageJson.workspaces
69
+ : packageJson.workspaces.packages;
70
+
71
+ for (const pattern of workspaces) {
72
+ const searchPath = path.join(projectRoot, pattern.replace('/*', ''));
73
+
74
+ if (existsSync(searchPath) && statSync(searchPath).isDirectory()) {
75
+ if (pattern.endsWith('/*')) {
76
+ const entries = readdirSync(searchPath);
77
+ const matchedPackages = entries
78
+ .map((entry) => path.join(searchPath, entry))
79
+ .filter((p) => {
80
+ try {
81
+ return statSync(p).isDirectory() && existsSync(path.join(p, 'package.json'));
82
+ } catch {
83
+ return false;
84
+ }
85
+ });
86
+ packages.push(...matchedPackages);
87
+ } else if (existsSync(path.join(searchPath, 'package.json'))) {
88
+ packages.push(searchPath);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ } catch {
94
+ // Silent fallback
95
+ }
96
+ }
97
+
98
+ // Check pnpm-workspace.yaml if no packages found yet
99
+ if (packages.length === 0) {
100
+ const pnpmWorkspacePath = path.join(projectRoot, 'pnpm-workspace.yaml');
101
+ if (existsSync(pnpmWorkspacePath)) {
102
+ try {
103
+ const content = readFileSync(pnpmWorkspacePath, 'utf8');
104
+ const lines = content.split('\n');
105
+ let inPackagesSection = false;
106
+ const patterns: string[] = [];
107
+
108
+ for (const line of lines) {
109
+ const trimmedLine = line.trim();
110
+
111
+ if (trimmedLine === 'packages:') {
112
+ inPackagesSection = true;
113
+ } else if (inPackagesSection) {
114
+ if (trimmedLine && !line.startsWith(' ') && !line.startsWith('\t')) {
115
+ break;
116
+ }
117
+
118
+ if (trimmedLine.startsWith('-')) {
119
+ const pattern = trimmedLine
120
+ .substring(1)
121
+ .trim()
122
+ .replace(/^['"]|['"]$/u, '');
123
+ if (pattern) {
124
+ patterns.push(pattern);
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ for (const pattern of patterns) {
131
+ const searchPath = path.join(projectRoot, pattern.replace('/*', ''));
132
+
133
+ if (existsSync(searchPath) && statSync(searchPath).isDirectory()) {
134
+ if (pattern.endsWith('/*')) {
135
+ const entries = readdirSync(searchPath);
136
+ const matchedPackages = entries
137
+ .map((entry) => path.join(searchPath, entry))
138
+ .filter((p) => {
139
+ try {
140
+ return statSync(p).isDirectory() && existsSync(path.join(p, 'package.json'));
141
+ } catch {
142
+ return false;
143
+ }
144
+ });
145
+ packages.push(...matchedPackages);
146
+ } else if (existsSync(path.join(searchPath, 'package.json'))) {
147
+ packages.push(searchPath);
148
+ }
149
+ }
150
+ }
151
+ } catch {
152
+ // Silent fallback
153
+ }
154
+ }
155
+ }
156
+
157
+ return packages;
158
+ }
159
+
160
+ function checkWorkspacePackages(
161
+ projectRoot: string,
162
+ packageName: string,
163
+ versionRequirement: string,
164
+ workspacePaths: string[],
165
+ ): boolean {
166
+ const foundPackages: string[] = [];
167
+ const notFoundPackages: string[] = [];
168
+
169
+ for (const workspacePath of workspacePaths) {
170
+ const packageDir = path.basename(workspacePath);
171
+ const found = checkPackageInDirectory(workspacePath, packageName, versionRequirement, {
172
+ silent: true,
173
+ limitedScope: true,
174
+ });
175
+
176
+ (found ? foundPackages : notFoundPackages).push(packageDir);
177
+ }
178
+
179
+ console.log(`📁 Found ${workspacePaths.length} packages to check`);
180
+
181
+ if (foundPackages.length > 0) {
182
+ console.log(
183
+ `✅ Found ${packageName} in ${foundPackages.length}/${workspacePaths.length} packages:`,
184
+ );
185
+ console.log(` 📦 ${foundPackages.join(', ')}`);
186
+
187
+ if (notFoundPackages.length > 0) {
188
+ console.log(`❌ Not found in ${notFoundPackages.length} packages:`);
189
+ console.log(` 📦 ${notFoundPackages.join(', ')}`);
190
+ }
191
+ } else {
192
+ console.log(`❌ Package ${packageName} not found in any of ${workspacePaths.length} packages`);
193
+ if (notFoundPackages.length > 0) {
194
+ console.log(` 📦 Checked: ${notFoundPackages.join(', ')}`);
195
+ }
196
+ }
197
+
198
+ return foundPackages.length > 0;
199
+ }
200
+
201
+ function checkMonorepoPackages(
202
+ packagesPath: string,
203
+ packageName: string,
204
+ versionRequirement: string,
205
+ ): boolean {
206
+ console.log(`📦 Checking monorepo packages in: ${packagesPath}`);
207
+ try {
208
+ if (!existsSync(packagesPath)) {
209
+ return false;
210
+ }
211
+
212
+ const packageDirs = readdirSync(packagesPath).filter((entry) =>
213
+ statSync(path.join(packagesPath, entry)).isDirectory(),
214
+ );
215
+
216
+ console.log(`📁 Found ${packageDirs.length} packages to check`);
217
+
218
+ const foundPackages: string[] = [];
219
+ const notFoundPackages: string[] = [];
220
+
221
+ for (const entry of packageDirs) {
222
+ const packageDir = path.join(packagesPath, entry);
223
+ const found = checkPackageInDirectory(packageDir, packageName, versionRequirement, {
224
+ silent: true,
225
+ limitedScope: true,
226
+ });
227
+
228
+ (found ? foundPackages : notFoundPackages).push(entry);
229
+ }
230
+
231
+ if (foundPackages.length > 0) {
232
+ console.log(
233
+ `✅ Found ${packageName} in ${foundPackages.length}/${packageDirs.length} packages:`,
234
+ );
235
+ console.log(` 📦 ${foundPackages.join(', ')}`);
236
+
237
+ if (notFoundPackages.length > 0) {
238
+ console.log(`❌ Not found in ${notFoundPackages.length} packages:`);
239
+ console.log(` 📦 ${notFoundPackages.join(', ')}`);
240
+ }
241
+ } else {
242
+ console.log(`❌ Package ${packageName} not found in any of ${packageDirs.length} packages`);
243
+ console.log(` 📦 Checked: ${notFoundPackages.join(', ')}`);
244
+ }
245
+
246
+ return foundPackages.length > 0;
247
+ } catch (error) {
248
+ console.log(`⚠️ Error checking monorepo packages:`, error);
249
+ return false;
250
+ }
251
+ }
252
+
253
+ function checkSinglePackage(
254
+ packagePath: string,
255
+ packageName: string,
256
+ versionRequirement: string,
257
+ ): boolean {
258
+ const checks = [
259
+ { type: 'dependencies' as const, msg: 'package.json dependencies' },
260
+ { type: 'nodeModules' as const, msg: 'node_modules' },
261
+ ];
262
+
263
+ for (const { type } of checks) {
264
+ if (
265
+ checkPackageInDirectory(packagePath, packageName, versionRequirement, {
266
+ silent: true,
267
+ limitedScope: false,
268
+ checkType: type,
269
+ })
270
+ ) {
271
+ return true;
272
+ }
273
+ }
274
+
275
+ return false;
276
+ }
277
+
278
+ function checkPackageInDirectory(
279
+ startPath: string,
280
+ packageName: string,
281
+ versionRequirement: string,
282
+ options: CheckOptions,
283
+ ): boolean {
284
+ const { checkType } = options;
285
+ const packageJsonResult = checkDirectDependencies(
286
+ startPath,
287
+ packageName,
288
+ versionRequirement,
289
+ options,
290
+ );
291
+
292
+ if (checkType === 'dependencies') return packageJsonResult;
293
+ if (packageJsonResult) return true;
294
+ if (checkType === 'nodeModules')
295
+ return checkNodeModules(startPath, packageName, versionRequirement, options);
296
+
297
+ return checkNodeModules(startPath, packageName, versionRequirement, options);
298
+ }
299
+
300
+ function checkDirectDependencies(
301
+ startPath: string,
302
+ packageName: string,
303
+ versionRequirement: string,
304
+ { limitedScope }: CheckOptions,
305
+ ): boolean {
306
+ const checker = (dir: string) =>
307
+ checkPackageJsonInDirectory(dir, packageName, versionRequirement);
308
+ return limitedScope ? checker(startPath) : walkDirectoryTree(startPath, checker);
309
+ }
310
+
311
+ function checkNodeModules(
312
+ startPath: string,
313
+ packageName: string,
314
+ versionRequirement: string,
315
+ { limitedScope }: CheckOptions,
316
+ ): boolean {
317
+ const checker = (dir: string) =>
318
+ checkStandardNodeModules(dir, packageName, versionRequirement) ||
319
+ checkPnpmNodeModules(dir, packageName, versionRequirement);
320
+
321
+ return limitedScope ? checker(startPath) : walkDirectoryTree(startPath, checker);
322
+ }
323
+
324
+ function walkDirectoryTree(startPath: string, checkFunction: (dir: string) => boolean): boolean {
325
+ let currentDir =
326
+ existsSync(startPath) && statSync(startPath).isFile() ? path.dirname(startPath) : startPath;
327
+
328
+ currentDir = path.resolve(currentDir);
329
+
330
+ const { root } = path.parse(currentDir);
331
+ let dirCount = 0;
332
+ let previousDir = '';
333
+
334
+ while (currentDir !== root && currentDir !== previousDir && dirCount < 10) {
335
+ dirCount += 1;
336
+
337
+ if (checkFunction(currentDir)) {
338
+ return true;
339
+ }
340
+
341
+ previousDir = currentDir;
342
+ currentDir = path.dirname(currentDir);
343
+ }
344
+
345
+ return false;
346
+ }
347
+
348
+ function checkPackageJsonInDirectory(
349
+ dir: string,
350
+ packageName: string,
351
+ versionRequirement: string,
352
+ ): boolean {
353
+ const packageJsonPath = path.join(dir, 'package.json');
354
+
355
+ if (!existsSync(packageJsonPath)) return false;
356
+
357
+ try {
358
+ const packageJson: PackageJson = JSON.parse(
359
+ readFileSync(packageJsonPath, 'utf8'),
360
+ ) as PackageJson;
361
+ const allDeps = {
362
+ ...(packageJson.dependencies ?? {}),
363
+ ...(packageJson.devDependencies ?? {}),
364
+ ...(packageJson.peerDependencies ?? {}),
365
+ ...(packageJson.optionalDependencies ?? {}),
366
+ };
367
+
368
+ const installedVersion = allDeps[packageName];
369
+ return installedVersion ? isVersionSatisfied(installedVersion, versionRequirement) : false;
370
+ } catch {
371
+ return false;
372
+ }
373
+ }
374
+
375
+ function isVersionSatisfied(installedVersion: string, versionRequirement: string): boolean {
376
+ if (semver.valid(installedVersion) && semver.satisfies(installedVersion, versionRequirement)) {
377
+ return true;
378
+ }
379
+
380
+ const cleanVersion = semver.coerce(installedVersion);
381
+ return cleanVersion ? semver.satisfies(cleanVersion.version, versionRequirement) : false;
382
+ }
383
+
384
+ function checkStandardNodeModules(
385
+ baseDir: string,
386
+ packageName: string,
387
+ versionRequirement: string,
388
+ ): boolean {
389
+ const packagePath = path.join(baseDir, 'node_modules', packageName, 'package.json');
390
+ if (!existsSync(packagePath)) return false;
391
+
392
+ try {
393
+ const packageJson: PackageJson = JSON.parse(readFileSync(packagePath, 'utf8')) as PackageJson;
394
+ return packageJson.version ? semver.satisfies(packageJson.version, versionRequirement) : false;
395
+ } catch {
396
+ return false;
397
+ }
398
+ }
399
+
400
+ function checkPnpmNodeModules(
401
+ baseDir: string,
402
+ packageName: string,
403
+ versionRequirement: string,
404
+ ): boolean {
405
+ const pnpmDir = path.join(baseDir, 'node_modules', '.pnpm');
406
+ if (!existsSync(pnpmDir)) return false;
407
+
408
+ try {
409
+ const entries = readdirSync(pnpmDir);
410
+
411
+ for (const entry of entries) {
412
+ if (entry.startsWith(packageName.replace('/', '+')) || entry.includes(`${packageName}@`)) {
413
+ const packagePath = path.join(pnpmDir, entry, 'node_modules', packageName, 'package.json');
414
+
415
+ if (existsSync(packagePath)) {
416
+ const packageJson: PackageJson = JSON.parse(
417
+ readFileSync(packagePath, 'utf8'),
418
+ ) as PackageJson;
419
+ if (packageJson.version && semver.satisfies(packageJson.version, versionRequirement)) {
420
+ return true;
421
+ }
422
+ }
423
+ }
424
+ }
425
+ } catch {
426
+ // Silent
427
+ }
428
+
429
+ return false;
430
+ }
431
+
432
+ export function findClosestGitignore(startPath: string): string | null {
433
+ let currentDir =
434
+ existsSync(startPath) && statSync(startPath).isFile() ? path.dirname(startPath) : startPath;
435
+
436
+ currentDir = path.resolve(currentDir);
437
+
438
+ const { root } = path.parse(currentDir);
439
+ let dirCount = 0;
440
+ let previousDir = '';
441
+
442
+ while (currentDir !== root && currentDir !== previousDir && dirCount < 10) {
443
+ dirCount += 1;
444
+ const gitignorePath = path.join(currentDir, '.gitignore');
445
+
446
+ if (existsSync(gitignorePath)) {
447
+ return gitignorePath;
448
+ }
449
+
450
+ previousDir = currentDir;
451
+ currentDir = path.dirname(currentDir);
452
+ }
453
+
454
+ return null;
455
+ }
456
+
457
+ export function findProjectRoot(startPath: string): string | null {
458
+ let currentDir =
459
+ existsSync(startPath) && statSync(startPath).isFile() ? path.dirname(startPath) : startPath;
460
+
461
+ currentDir = path.resolve(currentDir);
462
+
463
+ const { root } = path.parse(currentDir);
464
+ let dirCount = 0;
465
+ let previousDir = '';
466
+
467
+ while (currentDir !== root && currentDir !== previousDir && dirCount < 10) {
468
+ dirCount += 1;
469
+ const packageJsonPath = path.join(currentDir, 'package.json');
470
+ const gitPath = path.join(currentDir, '.git');
471
+ const pnpmWorkspacePath = path.join(currentDir, 'pnpm-workspace.yaml');
472
+
473
+ if (existsSync(packageJsonPath) || existsSync(gitPath) || existsSync(pnpmWorkspacePath)) {
474
+ return currentDir;
475
+ }
476
+
477
+ previousDir = currentDir;
478
+ currentDir = path.dirname(currentDir);
479
+ }
480
+
481
+ return null;
482
+ }
@@ -0,0 +1,4 @@
1
+ export { default as getOptions } from './getOptions';
2
+ export { default as handleError } from './handleError';
3
+ export { default as loadTransformModules } from './loadTransformModules';
4
+ export { default as reportManualReview } from './reportManualReview';
@@ -0,0 +1,28 @@
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;
@@ -0,0 +1,17 @@
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;