@hugeicons/migrate-core 0.1.0

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 (38) hide show
  1. package/dist/constants.d.ts +40 -0
  2. package/dist/constants.d.ts.map +1 -0
  3. package/dist/constants.js +117 -0
  4. package/dist/constants.js.map +1 -0
  5. package/dist/data/hugeicons-map.json +41786 -0
  6. package/dist/data/mappings.d.ts +69 -0
  7. package/dist/data/mappings.d.ts.map +1 -0
  8. package/dist/data/mappings.js +402 -0
  9. package/dist/data/mappings.js.map +1 -0
  10. package/dist/engine/analyzer.d.ts +21 -0
  11. package/dist/engine/analyzer.d.ts.map +1 -0
  12. package/dist/engine/analyzer.js +374 -0
  13. package/dist/engine/analyzer.js.map +1 -0
  14. package/dist/engine/detector.d.ts +20 -0
  15. package/dist/engine/detector.d.ts.map +1 -0
  16. package/dist/engine/detector.js +124 -0
  17. package/dist/engine/detector.js.map +1 -0
  18. package/dist/engine/mapper.d.ts +31 -0
  19. package/dist/engine/mapper.d.ts.map +1 -0
  20. package/dist/engine/mapper.js +234 -0
  21. package/dist/engine/mapper.js.map +1 -0
  22. package/dist/engine/reporter.d.ts +11 -0
  23. package/dist/engine/reporter.d.ts.map +1 -0
  24. package/dist/engine/reporter.js +339 -0
  25. package/dist/engine/reporter.js.map +1 -0
  26. package/dist/engine/transformer.d.ts +26 -0
  27. package/dist/engine/transformer.d.ts.map +1 -0
  28. package/dist/engine/transformer.js +874 -0
  29. package/dist/engine/transformer.js.map +1 -0
  30. package/dist/index.d.ts +22 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +56 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/types/index.d.ts +292 -0
  35. package/dist/types/index.d.ts.map +1 -0
  36. package/dist/types/index.js +14 -0
  37. package/dist/types/index.js.map +1 -0
  38. package/package.json +63 -0
@@ -0,0 +1,874 @@
1
+ /**
2
+ * Code Transformer
3
+ * AST-based code transformations for migrating icons
4
+ */
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import { Project, Node, } from 'ts-morph';
8
+ import { HUGEICONS_REACT_PACKAGE, getIconPackage, FA_PROP_CONVERSIONS, SUPPORTED_LIBRARIES } from '../constants.js';
9
+ import { hugeIconExists } from '../data/mappings.js';
10
+ /**
11
+ * Apply migration plan to source files
12
+ */
13
+ export async function applyPlan(scanResult, plan, options) {
14
+ const { root, dryRun, backup, patch } = options;
15
+ const transformations = [];
16
+ let backupPath;
17
+ // Create backup if requested and not dry run
18
+ if (backup && !dryRun) {
19
+ backupPath = await createBackup(root, plan.filesToModify);
20
+ }
21
+ // Create ts-morph project
22
+ const project = new Project({
23
+ skipAddingFilesFromTsConfig: true,
24
+ skipFileDependencyResolution: true,
25
+ });
26
+ // Process each file
27
+ for (const filePath of plan.filesToModify) {
28
+ const absolutePath = path.join(root, filePath);
29
+ const fileAnalysis = scanResult.files.find((f) => f.filePath === filePath);
30
+ if (!fileAnalysis)
31
+ continue;
32
+ try {
33
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
34
+ const original = sourceFile.getFullText();
35
+ const result = transformFile(sourceFile, fileAnalysis, plan, options.style || 'stroke');
36
+ if (!dryRun && result.success) {
37
+ sourceFile.saveSync();
38
+ }
39
+ transformations.push({
40
+ ...result,
41
+ original,
42
+ });
43
+ }
44
+ catch (error) {
45
+ transformations.push({
46
+ filePath,
47
+ original: '',
48
+ transformed: '',
49
+ changes: [],
50
+ success: false,
51
+ error: error instanceof Error ? error.message : String(error),
52
+ });
53
+ }
54
+ }
55
+ // Calculate summary
56
+ const summary = {
57
+ filesModified: transformations.filter((t) => t.success && t.changes.length > 0).length,
58
+ totalReplacements: transformations.reduce((sum, t) => sum + t.changes.filter((c) => c.type === 'jsx-replace').length, 0),
59
+ totalImportEdits: transformations.reduce((sum, t) => sum +
60
+ t.changes.filter((c) => ['import-add', 'import-remove', 'import-modify'].includes(c.type)).length, 0),
61
+ errors: transformations.filter((t) => !t.success).length,
62
+ };
63
+ // Generate patch if requested
64
+ let patchContent;
65
+ if (patch) {
66
+ patchContent = generatePatch(transformations);
67
+ }
68
+ return {
69
+ transformations,
70
+ summary,
71
+ dryRun,
72
+ patch: patchContent,
73
+ backupPath,
74
+ };
75
+ }
76
+ /**
77
+ * Collect all identifiers used in the file that are NOT from icon libraries
78
+ * This helps detect naming conflicts when adding icon imports
79
+ */
80
+ function collectExistingIdentifiers(sourceFile, iconLibraries) {
81
+ const identifiers = new Set();
82
+ // Collect from non-icon imports
83
+ for (const importDecl of sourceFile.getImportDeclarations()) {
84
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
85
+ // Skip icon library imports
86
+ if (iconLibraries.has(moduleSpecifier) ||
87
+ Array.from(iconLibraries).some(lib => moduleSpecifier.startsWith(lib))) {
88
+ continue;
89
+ }
90
+ // Get all named imports
91
+ const namedImports = importDecl.getNamedImports();
92
+ for (const named of namedImports) {
93
+ const alias = named.getAliasNode();
94
+ identifiers.add(alias ? alias.getText() : named.getName());
95
+ }
96
+ // Get default import
97
+ const defaultImport = importDecl.getDefaultImport();
98
+ if (defaultImport) {
99
+ identifiers.add(defaultImport.getText());
100
+ }
101
+ // Get namespace import
102
+ const namespaceImport = importDecl.getNamespaceImport();
103
+ if (namespaceImport) {
104
+ identifiers.add(namespaceImport.getText());
105
+ }
106
+ }
107
+ // Collect function declarations
108
+ for (const func of sourceFile.getFunctions()) {
109
+ const name = func.getName();
110
+ if (name)
111
+ identifiers.add(name);
112
+ }
113
+ // Collect class declarations
114
+ for (const cls of sourceFile.getClasses()) {
115
+ const name = cls.getName();
116
+ if (name)
117
+ identifiers.add(name);
118
+ }
119
+ // Collect variable declarations
120
+ for (const varDecl of sourceFile.getVariableDeclarations()) {
121
+ identifiers.add(varDecl.getName());
122
+ }
123
+ // Collect type aliases
124
+ for (const typeAlias of sourceFile.getTypeAliases()) {
125
+ identifiers.add(typeAlias.getName());
126
+ }
127
+ // Collect interfaces
128
+ for (const iface of sourceFile.getInterfaces()) {
129
+ identifiers.add(iface.getName());
130
+ }
131
+ // Collect exported declarations from export statements
132
+ for (const exportDecl of sourceFile.getExportDeclarations()) {
133
+ for (const named of exportDecl.getNamedExports()) {
134
+ identifiers.add(named.getName());
135
+ }
136
+ }
137
+ return identifiers;
138
+ }
139
+ /**
140
+ * Transform a single source file with comprehensive validation
141
+ */
142
+ function transformFile(sourceFile, fileAnalysis, plan, style) {
143
+ const changes = [];
144
+ const filePath = fileAnalysis.filePath;
145
+ // ============================================================================
146
+ // STEP 1: Capture ALL old icon imports BEFORE any changes
147
+ // ============================================================================
148
+ const oldIconImports = [];
149
+ // Build set of icon library module specifiers
150
+ const iconLibraryModules = new Set();
151
+ for (const lib of SUPPORTED_LIBRARIES) {
152
+ iconLibraryModules.add(lib.name);
153
+ for (const alias of lib.aliases) {
154
+ iconLibraryModules.add(alias);
155
+ }
156
+ }
157
+ // Scan ALL import declarations to find icon imports
158
+ for (const importDecl of sourceFile.getImportDeclarations()) {
159
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
160
+ // Check if this is from a supported icon library
161
+ const matchedLibrary = SUPPORTED_LIBRARIES.find((lib) => moduleSpecifier === lib.name ||
162
+ lib.aliases.includes(moduleSpecifier) ||
163
+ lib.aliases.some((alias) => moduleSpecifier.startsWith(alias)));
164
+ if (matchedLibrary) {
165
+ // Get all named imports
166
+ const namedImports = importDecl.getNamedImports();
167
+ for (const namedImport of namedImports) {
168
+ const importedName = namedImport.getName();
169
+ const aliasNode = namedImport.getAliasNode();
170
+ const localName = aliasNode ? aliasNode.getText() : importedName;
171
+ oldIconImports.push({
172
+ localName,
173
+ importedName,
174
+ library: matchedLibrary.name,
175
+ moduleSpecifier,
176
+ });
177
+ }
178
+ // Also check for default imports (rare but possible)
179
+ const defaultImport = importDecl.getDefaultImport();
180
+ if (defaultImport) {
181
+ oldIconImports.push({
182
+ localName: defaultImport.getText(),
183
+ importedName: defaultImport.getText(),
184
+ library: matchedLibrary.name,
185
+ moduleSpecifier,
186
+ });
187
+ }
188
+ }
189
+ }
190
+ const originalIconCount = oldIconImports.length;
191
+ // ============================================================================
192
+ // STEP 2: Build mappings for ALL old icons
193
+ // ============================================================================
194
+ // Build a map of icons to their mappings from the plan
195
+ const mappingMap = new Map();
196
+ for (const mapping of plan.mappings) {
197
+ const key = `${mapping.source.library}:${mapping.source.icon}`;
198
+ mappingMap.set(key, mapping);
199
+ }
200
+ // Collect existing identifiers to detect conflicts
201
+ const existingIdentifiers = collectExistingIdentifiers(sourceFile, iconLibraryModules);
202
+ // Build import tracking - maps hugeicon component name to the alias we'll use
203
+ const hugeIconsToImport = new Map(); // hugeIconName -> aliasOrOriginal
204
+ // Track which local names map to which hugeicons names
205
+ const iconReplacements = new Map(); // oldLocalName -> newHugeiconName
206
+ // Track unmapped icons
207
+ const unmappedIcons = [];
208
+ /**
209
+ * Get a non-conflicting name for an icon
210
+ */
211
+ function getIconImportName(iconComponent) {
212
+ if (hugeIconsToImport.has(iconComponent)) {
213
+ return hugeIconsToImport.get(iconComponent);
214
+ }
215
+ let importName = iconComponent;
216
+ if (existingIdentifiers.has(iconComponent)) {
217
+ const baseName = iconComponent.replace(/Icon$/, '');
218
+ importName = `Huge${baseName}Icon`;
219
+ let counter = 2;
220
+ while (existingIdentifiers.has(importName)) {
221
+ importName = `Huge${baseName}${counter}Icon`;
222
+ counter++;
223
+ }
224
+ }
225
+ hugeIconsToImport.set(iconComponent, importName);
226
+ existingIdentifiers.add(importName);
227
+ return importName;
228
+ }
229
+ // Track icons that should be KEPT as original imports (no valid Hugeicons replacement)
230
+ const iconsToKeepAsOriginal = new Set(); // localName -> keep original
231
+ // Process EVERY old icon import and create a mapping
232
+ for (const oldImport of oldIconImports) {
233
+ const key = `${oldImport.library}:${oldImport.importedName}`;
234
+ const mapping = mappingMap.get(key);
235
+ if (mapping?.target) {
236
+ const iconComponent = mapping.target.component;
237
+ // VALIDATE: Make sure this icon actually exists (check both primary names AND aliases)
238
+ if (hugeIconExists(iconComponent)) {
239
+ const iconImportName = getIconImportName(iconComponent);
240
+ iconReplacements.set(oldImport.localName, iconImportName);
241
+ }
242
+ else {
243
+ // The mapped icon doesn't exist! This is a bug in our mappings
244
+ console.warn(`[KEEP ORIGINAL] ${oldImport.importedName} -> mapped to ${iconComponent} but it doesn't exist in Hugeicons. Keeping original.`);
245
+ unmappedIcons.push(`${oldImport.importedName} (mapped to non-existent ${iconComponent})`);
246
+ iconsToKeepAsOriginal.add(oldImport.localName);
247
+ }
248
+ }
249
+ else {
250
+ // No mapping found - try to find if the icon exists with same name + "Icon" suffix
251
+ const fallbackName = oldImport.importedName.endsWith('Icon')
252
+ ? oldImport.importedName
253
+ : `${oldImport.importedName}Icon`;
254
+ if (hugeIconExists(fallbackName)) {
255
+ const iconImportName = getIconImportName(fallbackName);
256
+ iconReplacements.set(oldImport.localName, iconImportName);
257
+ }
258
+ else if (hugeIconExists(oldImport.importedName)) {
259
+ // The exact name exists
260
+ const iconImportName = getIconImportName(oldImport.importedName);
261
+ iconReplacements.set(oldImport.localName, iconImportName);
262
+ }
263
+ else {
264
+ // No valid Hugeicons replacement found - KEEP THE ORIGINAL LUCIDE ICON
265
+ console.warn(`[KEEP ORIGINAL] No Hugeicons equivalent for: ${oldImport.importedName}. Keeping original Lucide import.`);
266
+ unmappedIcons.push(oldImport.importedName);
267
+ iconsToKeepAsOriginal.add(oldImport.localName);
268
+ }
269
+ }
270
+ }
271
+ // ============================================================================
272
+ // STEP 3: Replace ALL identifier references in the code
273
+ // ============================================================================
274
+ let needsHugeiconsIcon = false;
275
+ let needsIconSvgElementType = false;
276
+ const processedIdentifiers = new Set();
277
+ // Multiple passes to ensure we catch everything
278
+ for (let pass = 0; pass < 3; pass++) {
279
+ sourceFile.forEachDescendant((node) => {
280
+ // Skip if already processed
281
+ if (processedIdentifiers.has(node))
282
+ return;
283
+ // Handle JSX elements
284
+ if (Node.isJsxOpeningElement(node) || Node.isJsxSelfClosingElement(node)) {
285
+ const tagName = node.getTagNameNode().getText();
286
+ // Handle FontAwesomeIcon specially
287
+ if (tagName === 'FontAwesomeIcon') {
288
+ const localToImported = new Map();
289
+ for (const imp of oldIconImports) {
290
+ localToImported.set(imp.localName, { library: imp.library, importedName: imp.importedName });
291
+ }
292
+ const transformed = transformFontAwesomeElement(node, mappingMap, localToImported, hugeIconsToImport, existingIdentifiers, changes, filePath);
293
+ if (transformed)
294
+ needsHugeiconsIcon = true;
295
+ processedIdentifiers.add(node);
296
+ return;
297
+ }
298
+ // Handle direct icon components
299
+ const replacement = iconReplacements.get(tagName);
300
+ if (replacement) {
301
+ const original = node.getText();
302
+ needsHugeiconsIcon = true;
303
+ const propsToKeep = [];
304
+ for (const attr of node.getAttributes()) {
305
+ if (Node.isJsxAttribute(attr)) {
306
+ propsToKeep.push(attr.getText());
307
+ }
308
+ else if (Node.isJsxSpreadAttribute(attr)) {
309
+ propsToKeep.push(attr.getText());
310
+ }
311
+ }
312
+ const propsString = propsToKeep.length > 0 ? ' ' + propsToKeep.join(' ') : '';
313
+ let replacementText;
314
+ if (Node.isJsxSelfClosingElement(node)) {
315
+ replacementText = `<HugeiconsIcon icon={${replacement}}${propsString} />`;
316
+ }
317
+ else {
318
+ replacementText = `<HugeiconsIcon icon={${replacement}}${propsString}>`;
319
+ }
320
+ node.replaceWithText(replacementText);
321
+ processedIdentifiers.add(node);
322
+ changes.push({
323
+ type: 'jsx-replace',
324
+ filePath,
325
+ line: node.getStartLineNumber(),
326
+ original,
327
+ replacement: replacementText,
328
+ });
329
+ return;
330
+ }
331
+ // Handle dynamic icon patterns like <item.icon /> or <props.icon />
332
+ // These need to be transformed to <HugeiconsIcon icon={item.icon} />
333
+ // because Hugeicons are data objects, not React components
334
+ const tagNameNode = node.getTagNameNode();
335
+ if (Node.isPropertyAccessExpression(tagNameNode)) {
336
+ // This is a member expression like item.icon or props.icon
337
+ const propertyName = tagNameNode.getName();
338
+ const objectExpr = tagNameNode.getExpression();
339
+ const objectName = objectExpr.getText();
340
+ // SKIP if the object is a PascalCase namespace (UI library component)
341
+ // e.g., SelectPrimitive.Icon, DialogPrimitive.Close, etc.
342
+ // These are React components, NOT icon data objects
343
+ const isPascalCase = /^[A-Z]/.test(objectName);
344
+ const isUiLibraryPattern = objectName.includes('Primitive') ||
345
+ objectName.includes('Radix') ||
346
+ objectName.includes('Component');
347
+ if (isPascalCase || isUiLibraryPattern) {
348
+ // This is a UI library component like SelectPrimitive.Icon
349
+ // Don't transform it
350
+ return;
351
+ }
352
+ // Check if this looks like an icon property (common patterns)
353
+ // Only transform lowercase object properties like item.icon, props.icon
354
+ if (propertyName === 'icon' || propertyName === 'Icon' ||
355
+ propertyName.endsWith('Icon') || propertyName.endsWith('icon')) {
356
+ const original = node.getText();
357
+ const dynamicIconExpr = tagNameNode.getText(); // e.g., "item.icon"
358
+ needsHugeiconsIcon = true;
359
+ const propsToKeep = [];
360
+ for (const attr of node.getAttributes()) {
361
+ if (Node.isJsxAttribute(attr)) {
362
+ propsToKeep.push(attr.getText());
363
+ }
364
+ else if (Node.isJsxSpreadAttribute(attr)) {
365
+ propsToKeep.push(attr.getText());
366
+ }
367
+ }
368
+ const propsString = propsToKeep.length > 0 ? ' ' + propsToKeep.join(' ') : '';
369
+ let replacementText;
370
+ if (Node.isJsxSelfClosingElement(node)) {
371
+ replacementText = `<HugeiconsIcon icon={${dynamicIconExpr}}${propsString} />`;
372
+ }
373
+ else {
374
+ replacementText = `<HugeiconsIcon icon={${dynamicIconExpr}}${propsString}>`;
375
+ }
376
+ node.replaceWithText(replacementText);
377
+ processedIdentifiers.add(node);
378
+ changes.push({
379
+ type: 'jsx-replace',
380
+ filePath,
381
+ line: node.getStartLineNumber(),
382
+ original,
383
+ replacement: replacementText,
384
+ });
385
+ }
386
+ }
387
+ return;
388
+ }
389
+ // Handle TypeScript type references like "LucideIcon" -> "IconSvgElement"
390
+ if (Node.isTypeReference(node)) {
391
+ const fullTypeText = node.getText();
392
+ const typeName = node.getTypeName().getText();
393
+ // Handle shadcn-style SVG component props: React.ComponentProps<"svg"> or ComponentProps<"svg">
394
+ // Transform to: Omit<React.ComponentProps<typeof HugeiconsIcon>, "icon">
395
+ if ((typeName === 'React.ComponentProps' || typeName === 'ComponentProps') &&
396
+ (fullTypeText.includes('<"svg">') || fullTypeText.includes("<'svg'>"))) {
397
+ try {
398
+ const original = node.getText();
399
+ const prefix = typeName.startsWith('React.') ? 'React.' : '';
400
+ const replacement = `Omit<${prefix}ComponentProps<typeof HugeiconsIcon>, "icon">`;
401
+ node.replaceWithText(replacement);
402
+ needsHugeiconsIcon = true;
403
+ changes.push({
404
+ type: 'identifier-replace',
405
+ filePath,
406
+ line: node.getStartLineNumber(),
407
+ original,
408
+ replacement,
409
+ });
410
+ }
411
+ catch {
412
+ // Node might have been modified
413
+ }
414
+ return;
415
+ }
416
+ // Handle SVGProps<SVGSVGElement> -> Omit<React.ComponentProps<typeof HugeiconsIcon>, "icon">
417
+ if ((typeName === 'SVGProps' || typeName === 'React.SVGProps') &&
418
+ fullTypeText.includes('SVGSVGElement')) {
419
+ try {
420
+ const original = node.getText();
421
+ const replacement = `Omit<React.ComponentProps<typeof HugeiconsIcon>, "icon">`;
422
+ node.replaceWithText(replacement);
423
+ needsHugeiconsIcon = true;
424
+ changes.push({
425
+ type: 'identifier-replace',
426
+ filePath,
427
+ line: node.getStartLineNumber(),
428
+ original,
429
+ replacement,
430
+ });
431
+ }
432
+ catch {
433
+ // Node might have been modified
434
+ }
435
+ return;
436
+ }
437
+ // Replace Lucide type references with Hugeicons type
438
+ if (typeName === 'LucideIcon' || typeName === 'Icon') {
439
+ try {
440
+ const original = node.getText();
441
+ node.replaceWithText('IconSvgElement');
442
+ needsIconSvgElementType = true;
443
+ changes.push({
444
+ type: 'identifier-replace',
445
+ filePath,
446
+ line: node.getStartLineNumber(),
447
+ original,
448
+ replacement: 'IconSvgElement',
449
+ });
450
+ }
451
+ catch {
452
+ // Node might have been modified
453
+ }
454
+ }
455
+ // Replace LucideProps with HugeiconsIcon props type
456
+ if (typeName === 'LucideProps') {
457
+ try {
458
+ const original = node.getText();
459
+ const replacement = `Omit<React.ComponentProps<typeof HugeiconsIcon>, "icon">`;
460
+ node.replaceWithText(replacement);
461
+ needsHugeiconsIcon = true;
462
+ changes.push({
463
+ type: 'identifier-replace',
464
+ filePath,
465
+ line: node.getStartLineNumber(),
466
+ original,
467
+ replacement,
468
+ });
469
+ }
470
+ catch {
471
+ // Node might have been modified
472
+ }
473
+ }
474
+ return;
475
+ }
476
+ // Handle identifier references (for props, object values, etc.)
477
+ if (Node.isIdentifier(node)) {
478
+ const identifierName = node.getText();
479
+ const replacement = iconReplacements.get(identifierName);
480
+ if (replacement) {
481
+ const parent = node.getParent();
482
+ // Skip import declarations
483
+ if (parent && (Node.isImportSpecifier(parent) || Node.isImportClause(parent))) {
484
+ return;
485
+ }
486
+ // Skip JSX tag names (handled above)
487
+ if (parent && (Node.isJsxOpeningElement(parent) || Node.isJsxSelfClosingElement(parent))) {
488
+ try {
489
+ const tagNameNode = parent.getTagNameNode();
490
+ if (tagNameNode === node || tagNameNode.getText() === identifierName) {
491
+ return;
492
+ }
493
+ }
494
+ catch {
495
+ // Node might have been modified
496
+ return;
497
+ }
498
+ }
499
+ // Replace the identifier
500
+ try {
501
+ const original = node.getText();
502
+ node.replaceWithText(replacement);
503
+ processedIdentifiers.add(node);
504
+ changes.push({
505
+ type: 'identifier-replace',
506
+ filePath,
507
+ line: node.getStartLineNumber(),
508
+ original,
509
+ replacement,
510
+ });
511
+ }
512
+ catch {
513
+ // Node might have been modified in a previous pass
514
+ }
515
+ }
516
+ }
517
+ });
518
+ }
519
+ // ============================================================================
520
+ // STEP 4: Handle old icon library imports
521
+ // - Remove imports that have been migrated
522
+ // - KEEP imports that don't have valid Hugeicons replacements
523
+ // ============================================================================
524
+ for (const importDecl of sourceFile.getImportDeclarations()) {
525
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
526
+ const isIconLibrary = SUPPORTED_LIBRARIES.some((lib) => moduleSpecifier === lib.name ||
527
+ lib.aliases.includes(moduleSpecifier) ||
528
+ lib.aliases.some((alias) => moduleSpecifier.startsWith(alias)));
529
+ if (isIconLibrary) {
530
+ const namedImports = importDecl.getNamedImports();
531
+ // Separate imports: ones to keep (unmapped) vs ones to remove (migrated)
532
+ const importsToKeep = [];
533
+ const importsToRemoveFromDecl = [];
534
+ for (const namedImport of namedImports) {
535
+ const importedName = namedImport.getName();
536
+ const aliasNode = namedImport.getAliasNode();
537
+ const localName = aliasNode ? aliasNode.getText() : importedName;
538
+ if (iconsToKeepAsOriginal.has(localName)) {
539
+ // This icon has no valid Hugeicons replacement - KEEP IT
540
+ importsToKeep.push(namedImport.getText());
541
+ }
542
+ else {
543
+ // This icon was migrated - remove it
544
+ importsToRemoveFromDecl.push(localName);
545
+ }
546
+ }
547
+ if (importsToKeep.length === 0) {
548
+ // All imports from this declaration are migrated - remove the whole statement
549
+ changes.push({
550
+ type: 'import-remove',
551
+ filePath,
552
+ line: importDecl.getStartLineNumber(),
553
+ original: importDecl.getText(),
554
+ replacement: '',
555
+ });
556
+ try {
557
+ importDecl.remove();
558
+ }
559
+ catch {
560
+ // Already removed
561
+ }
562
+ }
563
+ else if (importsToRemoveFromDecl.length > 0) {
564
+ // Some imports migrated, some kept - update the import statement
565
+ const originalText = importDecl.getText();
566
+ // Remove the migrated imports, keep the unmapped ones
567
+ for (const namedImport of namedImports) {
568
+ const aliasNode = namedImport.getAliasNode();
569
+ const localName = aliasNode ? aliasNode.getText() : namedImport.getName();
570
+ if (!iconsToKeepAsOriginal.has(localName)) {
571
+ try {
572
+ namedImport.remove();
573
+ }
574
+ catch {
575
+ // Already removed
576
+ }
577
+ }
578
+ }
579
+ changes.push({
580
+ type: 'import-modify',
581
+ filePath,
582
+ line: importDecl.getStartLineNumber(),
583
+ original: originalText,
584
+ replacement: importDecl.getText(),
585
+ });
586
+ }
587
+ // else: all imports are unmapped - keep the whole statement as is
588
+ }
589
+ }
590
+ // ============================================================================
591
+ // STEP 5: Add Hugeicons imports
592
+ // ============================================================================
593
+ if (hugeIconsToImport.size > 0 || needsHugeiconsIcon || needsIconSvgElementType) {
594
+ const iconPackage = getIconPackage(style);
595
+ // Build the new imports
596
+ const imports = [];
597
+ // Build react package imports (HugeiconsIcon and/or IconSvgElement type)
598
+ const reactPackageImports = [];
599
+ if (needsHugeiconsIcon) {
600
+ reactPackageImports.push('HugeiconsIcon');
601
+ }
602
+ if (needsIconSvgElementType) {
603
+ reactPackageImports.push('type IconSvgElement');
604
+ }
605
+ if (reactPackageImports.length > 0) {
606
+ imports.push(`import { ${reactPackageImports.join(', ')} } from "${HUGEICONS_REACT_PACKAGE}";`);
607
+ }
608
+ // Import icons from the style-specific package (if any)
609
+ if (hugeIconsToImport.size > 0) {
610
+ const iconSpecifiers = Array.from(hugeIconsToImport.entries())
611
+ .sort((a, b) => a[0].localeCompare(b[0]))
612
+ .map(([original, alias]) => {
613
+ if (original === alias) {
614
+ return original;
615
+ }
616
+ return `${original} as ${alias}`;
617
+ })
618
+ .join(', ');
619
+ imports.push(`import { ${iconSpecifiers} } from "${iconPackage}";`);
620
+ }
621
+ const newImport = imports.join('\n');
622
+ // Find the best position for the new import (after removing old ones)
623
+ const remainingImports = sourceFile.getImportDeclarations();
624
+ if (remainingImports.length > 0) {
625
+ // Insert before the first remaining import
626
+ remainingImports[0].replaceWithText(newImport + '\n' + remainingImports[0].getText());
627
+ }
628
+ else {
629
+ // No imports left, add at the beginning
630
+ sourceFile.insertText(0, newImport + '\n\n');
631
+ }
632
+ changes.push({
633
+ type: 'import-add',
634
+ filePath,
635
+ line: 1,
636
+ original: '',
637
+ replacement: newImport,
638
+ });
639
+ }
640
+ // ============================================================================
641
+ // STEP 6: Validation - Check for any leftover old library references
642
+ // ============================================================================
643
+ const finalIconCount = hugeIconsToImport.size;
644
+ const fileContent = sourceFile.getFullText();
645
+ // Check for any remaining old library imports
646
+ const remainingOldImports = [];
647
+ for (const lib of SUPPORTED_LIBRARIES) {
648
+ // Check for import statements
649
+ if (fileContent.includes(`from "${lib.name}"`) || fileContent.includes(`from '${lib.name}'`)) {
650
+ remainingOldImports.push(lib.name);
651
+ }
652
+ for (const alias of lib.aliases) {
653
+ if (fileContent.includes(`from "${alias}"`) || fileContent.includes(`from '${alias}'`)) {
654
+ remainingOldImports.push(alias);
655
+ }
656
+ }
657
+ }
658
+ // Check for any remaining identifier references to old icons
659
+ const remainingOldReferences = [];
660
+ for (const oldImport of oldIconImports) {
661
+ // Use regex to find standalone identifier references (not part of new imports)
662
+ const identifierRegex = new RegExp(`\\b${oldImport.localName}\\b`, 'g');
663
+ const matches = fileContent.match(identifierRegex);
664
+ if (matches && matches.length > 0) {
665
+ // Check if this is not in the new hugeicons import
666
+ const newImportName = iconReplacements.get(oldImport.localName);
667
+ if (newImportName && !fileContent.includes(newImportName)) {
668
+ remainingOldReferences.push(oldImport.localName);
669
+ }
670
+ }
671
+ }
672
+ // Log validation results
673
+ if (remainingOldImports.length > 0 || remainingOldReferences.length > 0 || unmappedIcons.length > 0) {
674
+ console.warn(`[${filePath}] Migration validation warnings:`);
675
+ if (originalIconCount !== finalIconCount) {
676
+ console.warn(` - Icon count mismatch: ${originalIconCount} original -> ${finalIconCount} new`);
677
+ }
678
+ if (unmappedIcons.length > 0) {
679
+ console.warn(` - Unmapped icons (using fallback): ${unmappedIcons.join(', ')}`);
680
+ }
681
+ if (remainingOldImports.length > 0) {
682
+ console.warn(` - Remaining old imports: ${remainingOldImports.join(', ')}`);
683
+ }
684
+ if (remainingOldReferences.length > 0) {
685
+ console.warn(` - Possible remaining old references: ${remainingOldReferences.join(', ')}`);
686
+ }
687
+ }
688
+ return {
689
+ filePath,
690
+ original: '', // Will be set by caller
691
+ transformed: sourceFile.getFullText(),
692
+ changes,
693
+ success: true,
694
+ };
695
+ }
696
+ /**
697
+ * Transform FontAwesomeIcon element
698
+ * Returns true if transformation was applied
699
+ */
700
+ function transformFontAwesomeElement(element, mappingMap, localToImported, hugeIconsToImport, existingIdentifiers, changes, filePath) {
701
+ // Find the icon prop
702
+ const iconAttr = element.getAttribute('icon');
703
+ if (!iconAttr || !Node.isJsxAttribute(iconAttr))
704
+ return false;
705
+ const initializer = iconAttr.getInitializer();
706
+ if (!initializer || !Node.isJsxExpression(initializer))
707
+ return false;
708
+ const expression = initializer.getExpression();
709
+ if (!expression || !Node.isIdentifier(expression))
710
+ return false;
711
+ const iconName = expression.getText();
712
+ const importInfo = localToImported.get(iconName);
713
+ if (!importInfo)
714
+ return false;
715
+ const key = `${importInfo.library}:${importInfo.importedName}`;
716
+ const mapping = mappingMap.get(key);
717
+ if (!mapping?.target)
718
+ return false;
719
+ const targetComponent = mapping.target.component;
720
+ // Get non-conflicting import name
721
+ let iconImportName = targetComponent;
722
+ if (hugeIconsToImport.has(targetComponent)) {
723
+ iconImportName = hugeIconsToImport.get(targetComponent);
724
+ }
725
+ else {
726
+ if (existingIdentifiers.has(targetComponent)) {
727
+ const baseName = targetComponent.replace(/Icon$/, '');
728
+ iconImportName = `Huge${baseName}Icon`;
729
+ let counter = 2;
730
+ while (existingIdentifiers.has(iconImportName)) {
731
+ iconImportName = `Huge${baseName}${counter}Icon`;
732
+ counter++;
733
+ }
734
+ }
735
+ hugeIconsToImport.set(targetComponent, iconImportName);
736
+ existingIdentifiers.add(iconImportName);
737
+ }
738
+ // Collect props to keep and convert
739
+ const propsToKeep = [];
740
+ const classNames = [];
741
+ for (const attr of element.getAttributes()) {
742
+ if (!Node.isJsxAttribute(attr))
743
+ continue;
744
+ const propName = attr.getNameNode().getText();
745
+ // Skip the icon prop
746
+ if (propName === 'icon')
747
+ continue;
748
+ // Check if it's a FA-specific prop that needs conversion
749
+ const conversion = FA_PROP_CONVERSIONS[propName];
750
+ if (conversion) {
751
+ if (conversion.type === 'class' && conversion.value) {
752
+ classNames.push(conversion.value);
753
+ }
754
+ // Remove or warn for other types
755
+ continue;
756
+ }
757
+ // Keep the prop as-is
758
+ propsToKeep.push(attr.getText());
759
+ }
760
+ // Add converted classes to className if we have any
761
+ if (classNames.length > 0) {
762
+ const existingClassName = element.getAttribute('className');
763
+ if (existingClassName && Node.isJsxAttribute(existingClassName)) {
764
+ const init = existingClassName.getInitializer();
765
+ if (init && Node.isStringLiteral(init)) {
766
+ const existing = init.getLiteralValue();
767
+ propsToKeep.push(`className="${existing} ${classNames.join(' ')}"`);
768
+ }
769
+ else {
770
+ propsToKeep.push(`className="${classNames.join(' ')}"`);
771
+ }
772
+ // Remove the original className from propsToKeep
773
+ const idx = propsToKeep.findIndex((p) => p.startsWith('className'));
774
+ if (idx > 0)
775
+ propsToKeep.splice(idx - 1, 1);
776
+ }
777
+ else {
778
+ propsToKeep.push(`className="${classNames.join(' ')}"`);
779
+ }
780
+ }
781
+ // Build the replacement JSX with HugeiconsIcon wrapper
782
+ const propsString = propsToKeep.length > 0 ? ' ' + propsToKeep.join(' ') : '';
783
+ const original = element.getText();
784
+ let replacement;
785
+ if (Node.isJsxSelfClosingElement(element)) {
786
+ replacement = `<HugeiconsIcon icon={${iconImportName}}${propsString} />`;
787
+ }
788
+ else {
789
+ replacement = `<HugeiconsIcon icon={${iconImportName}}${propsString}>`;
790
+ }
791
+ // Replace the element
792
+ element.replaceWithText(replacement);
793
+ // If this was an opening element, we need to handle the closing tag
794
+ if (Node.isJsxOpeningElement(element)) {
795
+ const parent = element.getParent();
796
+ if (parent && Node.isJsxElement(parent)) {
797
+ const closingElement = parent.getClosingElement();
798
+ if (closingElement) {
799
+ closingElement.replaceWithText(`</HugeiconsIcon>`);
800
+ }
801
+ }
802
+ }
803
+ changes.push({
804
+ type: 'jsx-replace',
805
+ filePath,
806
+ line: element.getStartLineNumber(),
807
+ original,
808
+ replacement,
809
+ });
810
+ return true;
811
+ }
812
+ /**
813
+ * Create backup of files to be modified
814
+ */
815
+ async function createBackup(root, filesToModify) {
816
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
817
+ const backupDir = path.join(root, `hugeicons-migrate-backup-${timestamp}`);
818
+ fs.mkdirSync(backupDir, { recursive: true });
819
+ for (const filePath of filesToModify) {
820
+ const sourcePath = path.join(root, filePath);
821
+ const targetPath = path.join(backupDir, filePath);
822
+ // Create directory structure
823
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
824
+ // Copy file
825
+ fs.copyFileSync(sourcePath, targetPath);
826
+ }
827
+ return backupDir;
828
+ }
829
+ /**
830
+ * Generate unified diff patch
831
+ */
832
+ function generatePatch(transformations) {
833
+ const lines = [];
834
+ for (const transform of transformations) {
835
+ if (!transform.success || transform.changes.length === 0)
836
+ continue;
837
+ lines.push(`--- a/${transform.filePath}`);
838
+ lines.push(`+++ b/${transform.filePath}`);
839
+ // Generate simple diff representation
840
+ for (const change of transform.changes) {
841
+ if (change.original) {
842
+ lines.push(`@@ -${change.line} @@`);
843
+ lines.push(`-${change.original}`);
844
+ }
845
+ if (change.replacement) {
846
+ if (!change.original) {
847
+ lines.push(`@@ +${change.line} @@`);
848
+ }
849
+ lines.push(`+${change.replacement}`);
850
+ }
851
+ }
852
+ lines.push('');
853
+ }
854
+ return lines.join('\n');
855
+ }
856
+ /**
857
+ * Check git status
858
+ */
859
+ export function checkGitStatus(root) {
860
+ try {
861
+ const gitDir = path.join(root, '.git');
862
+ const isRepo = fs.existsSync(gitDir);
863
+ if (!isRepo) {
864
+ return { isRepo: false, isDirty: false, modifiedFiles: [] };
865
+ }
866
+ // Try to check status via simple file check
867
+ // In a real implementation, we'd use git commands
868
+ return { isRepo: true, isDirty: false, modifiedFiles: [] };
869
+ }
870
+ catch {
871
+ return { isRepo: false, isDirty: false, modifiedFiles: [] };
872
+ }
873
+ }
874
+ //# sourceMappingURL=transformer.js.map