@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.
- package/dist/constants.d.ts +40 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +117 -0
- package/dist/constants.js.map +1 -0
- package/dist/data/hugeicons-map.json +41786 -0
- package/dist/data/mappings.d.ts +69 -0
- package/dist/data/mappings.d.ts.map +1 -0
- package/dist/data/mappings.js +402 -0
- package/dist/data/mappings.js.map +1 -0
- package/dist/engine/analyzer.d.ts +21 -0
- package/dist/engine/analyzer.d.ts.map +1 -0
- package/dist/engine/analyzer.js +374 -0
- package/dist/engine/analyzer.js.map +1 -0
- package/dist/engine/detector.d.ts +20 -0
- package/dist/engine/detector.d.ts.map +1 -0
- package/dist/engine/detector.js +124 -0
- package/dist/engine/detector.js.map +1 -0
- package/dist/engine/mapper.d.ts +31 -0
- package/dist/engine/mapper.d.ts.map +1 -0
- package/dist/engine/mapper.js +234 -0
- package/dist/engine/mapper.js.map +1 -0
- package/dist/engine/reporter.d.ts +11 -0
- package/dist/engine/reporter.d.ts.map +1 -0
- package/dist/engine/reporter.js +339 -0
- package/dist/engine/reporter.js.map +1 -0
- package/dist/engine/transformer.d.ts +26 -0
- package/dist/engine/transformer.d.ts.map +1 -0
- package/dist/engine/transformer.js +874 -0
- package/dist/engine/transformer.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +292 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +14 -0
- package/dist/types/index.js.map +1 -0
- 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
|