vectify 2.1.0 → 2.1.1

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/index.js CHANGED
@@ -40,10 +40,10 @@ __export(helpers_exports, {
40
40
  getComponentName: () => getComponentName,
41
41
  getSvgFiles: () => getSvgFiles,
42
42
  mergeClasses: () => mergeClasses,
43
- readFile: () => readFile,
43
+ readFile: () => readFile3,
44
44
  toKebabCase: () => toKebabCase,
45
45
  toPascalCase: () => toPascalCase,
46
- writeFile: () => writeFile
46
+ writeFile: () => writeFile2
47
47
  });
48
48
  function toPascalCase(str) {
49
49
  return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase()).replace(/^(.)/, (char) => char.toUpperCase()).replace(/[^a-z0-9]/gi, "");
@@ -93,11 +93,11 @@ async function fileExists(filePath) {
93
93
  return false;
94
94
  }
95
95
  }
96
- async function readFile(filePath) {
96
+ async function readFile3(filePath) {
97
97
  const fs2 = await import("fs/promises");
98
98
  return await fs2.readFile(filePath, "utf-8");
99
99
  }
100
- async function writeFile(filePath, content) {
100
+ async function writeFile2(filePath, content) {
101
101
  const fs2 = await import("fs/promises");
102
102
  await fs2.writeFile(filePath, content, "utf-8");
103
103
  }
@@ -151,8 +151,288 @@ module.exports = __toCommonJS(src_exports);
151
151
  var import_chalk = __toESM(require("chalk"));
152
152
  var import_ora = __toESM(require("ora"));
153
153
 
154
+ // src/cache/cache-manager.ts
155
+ var import_node_fs = require("fs");
156
+ var import_promises2 = require("fs/promises");
157
+ var import_node_path = require("path");
158
+
159
+ // src/cache/hash-utils.ts
160
+ var import_node_crypto = require("crypto");
161
+ var import_promises = require("fs/promises");
162
+ function hashContent(content) {
163
+ return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
164
+ }
165
+ async function hashFile(filePath) {
166
+ try {
167
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
168
+ return hashContent(content);
169
+ } catch (error) {
170
+ throw new Error(`Failed to hash file ${filePath}: ${error}`);
171
+ }
172
+ }
173
+ function hashConfig(config) {
174
+ const relevantConfig = {
175
+ framework: config.framework,
176
+ typescript: config.typescript,
177
+ keepColors: config.keepColors,
178
+ prefix: config.prefix,
179
+ suffix: config.suffix,
180
+ optimize: config.optimize,
181
+ svgoConfig: config.svgoConfig,
182
+ componentNameTransform: config.componentNameTransform?.toString()
183
+ };
184
+ return hashContent(JSON.stringify(relevantConfig));
185
+ }
186
+ function hashSvgoConfig(svgoConfig) {
187
+ return hashContent(JSON.stringify(svgoConfig || {}));
188
+ }
189
+
190
+ // src/cache/cache-manager.ts
191
+ var CACHE_VERSION = "1.0.0";
192
+ var CacheManager = class {
193
+ constructor(outputDir, cacheDir = ".vectify") {
194
+ this.cache = {
195
+ version: CACHE_VERSION,
196
+ configHash: "",
197
+ entries: {},
198
+ baseComponentHash: ""
199
+ };
200
+ this.cacheFilePath = (0, import_node_path.join)(outputDir, cacheDir, "cache.json");
201
+ this.stats = {
202
+ hits: 0,
203
+ misses: 0,
204
+ total: 0,
205
+ timeSaved: 0
206
+ };
207
+ this.isDirty = false;
208
+ process.on("beforeExit", () => {
209
+ if (this.isDirty) {
210
+ this.saveSync();
211
+ }
212
+ });
213
+ }
214
+ /**
215
+ * Load cache from disk
216
+ */
217
+ async load() {
218
+ try {
219
+ if (!(0, import_node_fs.existsSync)(this.cacheFilePath)) {
220
+ return;
221
+ }
222
+ const content = await (0, import_promises2.readFile)(this.cacheFilePath, "utf-8");
223
+ const loadedCache = JSON.parse(content);
224
+ if (loadedCache.version !== CACHE_VERSION) {
225
+ console.log("\u26A0\uFE0F Cache version mismatch, rebuilding cache...");
226
+ return;
227
+ }
228
+ this.cache = loadedCache;
229
+ } catch (error) {
230
+ console.warn("\u26A0\uFE0F Failed to load cache, starting fresh:", error);
231
+ this.cache = {
232
+ version: CACHE_VERSION,
233
+ configHash: "",
234
+ entries: {},
235
+ baseComponentHash: ""
236
+ };
237
+ }
238
+ }
239
+ /**
240
+ * Save cache to disk (atomic write)
241
+ */
242
+ async save() {
243
+ try {
244
+ const cacheDir = (0, import_node_path.dirname)(this.cacheFilePath);
245
+ await (0, import_promises2.mkdir)(cacheDir, { recursive: true });
246
+ const tempPath = `${this.cacheFilePath}.tmp`;
247
+ await (0, import_promises2.writeFile)(tempPath, JSON.stringify(this.cache, null, 2), "utf-8");
248
+ await (0, import_promises2.writeFile)(this.cacheFilePath, JSON.stringify(this.cache, null, 2), "utf-8");
249
+ this.isDirty = false;
250
+ } catch (error) {
251
+ console.warn("\u26A0\uFE0F Failed to save cache:", error);
252
+ }
253
+ }
254
+ /**
255
+ * Synchronous save for process exit
256
+ */
257
+ saveSync() {
258
+ try {
259
+ const fs2 = require("fs");
260
+ const cacheDir = (0, import_node_path.dirname)(this.cacheFilePath);
261
+ if (!fs2.existsSync(cacheDir)) {
262
+ fs2.mkdirSync(cacheDir, { recursive: true });
263
+ }
264
+ fs2.writeFileSync(this.cacheFilePath, JSON.stringify(this.cache, null, 2), "utf-8");
265
+ this.isDirty = false;
266
+ } catch {
267
+ console.warn("\u26A0\uFE0F Failed to save cache on exit");
268
+ }
269
+ }
270
+ /**
271
+ * Check if a file needs regeneration
272
+ */
273
+ async needsRegeneration(svgPath, componentPath, config) {
274
+ this.stats.total++;
275
+ if (!(0, import_node_fs.existsSync)(componentPath)) {
276
+ this.stats.misses++;
277
+ return true;
278
+ }
279
+ const entry = this.cache.entries[svgPath];
280
+ if (!entry) {
281
+ this.stats.misses++;
282
+ return true;
283
+ }
284
+ try {
285
+ const stats = await (0, import_promises2.stat)(svgPath);
286
+ const currentMtime = stats.mtimeMs;
287
+ if (currentMtime === entry.svgMtime) {
288
+ if (!this.isConfigMatching(entry, config)) {
289
+ this.stats.misses++;
290
+ return true;
291
+ }
292
+ this.stats.hits++;
293
+ this.stats.timeSaved += 50;
294
+ return false;
295
+ }
296
+ const currentHash = await hashFile(svgPath);
297
+ if (currentHash === entry.svgHash) {
298
+ entry.svgMtime = currentMtime;
299
+ this.isDirty = true;
300
+ this.stats.hits++;
301
+ this.stats.timeSaved += 50;
302
+ return false;
303
+ }
304
+ this.stats.misses++;
305
+ return true;
306
+ } catch {
307
+ this.stats.misses++;
308
+ return true;
309
+ }
310
+ }
311
+ /**
312
+ * Check if config matches cache entry
313
+ */
314
+ isConfigMatching(entry, config) {
315
+ const snapshot = entry.configSnapshot;
316
+ return snapshot.framework === config.framework && snapshot.typescript === config.typescript && snapshot.keepColors === config.keepColors && snapshot.prefix === (config.prefix || "") && snapshot.suffix === (config.suffix || "") && snapshot.optimize === config.optimize && snapshot.svgoConfigHash === hashSvgoConfig(config.svgoConfig);
317
+ }
318
+ /**
319
+ * Update cache entry after successful generation
320
+ */
321
+ async updateEntry(svgPath, componentPath, componentName, componentHash, config) {
322
+ try {
323
+ const svgHash = await hashFile(svgPath);
324
+ const stats = await (0, import_promises2.stat)(svgPath);
325
+ this.cache.entries[svgPath] = {
326
+ svgPath,
327
+ svgHash,
328
+ svgMtime: stats.mtimeMs,
329
+ componentName,
330
+ componentPath,
331
+ componentHash,
332
+ configSnapshot: {
333
+ framework: config.framework,
334
+ typescript: config.typescript ?? true,
335
+ keepColors: config.keepColors ?? false,
336
+ prefix: config.prefix || "",
337
+ suffix: config.suffix || "",
338
+ optimize: config.optimize ?? true,
339
+ svgoConfigHash: hashSvgoConfig(config.svgoConfig)
340
+ },
341
+ generatedAt: Date.now()
342
+ };
343
+ this.isDirty = true;
344
+ } catch (error) {
345
+ console.warn(`\u26A0\uFE0F Failed to update cache entry for ${svgPath}:`, error);
346
+ }
347
+ }
348
+ /**
349
+ * Remove cache entry
350
+ */
351
+ removeEntry(svgPath) {
352
+ if (this.cache.entries[svgPath]) {
353
+ delete this.cache.entries[svgPath];
354
+ this.isDirty = true;
355
+ }
356
+ }
357
+ /**
358
+ * Update config hash
359
+ */
360
+ updateConfigHash(config) {
361
+ const newHash = hashConfig(config);
362
+ if (this.cache.configHash !== newHash) {
363
+ this.cache.configHash = newHash;
364
+ this.isDirty = true;
365
+ }
366
+ }
367
+ /**
368
+ * Update base component hash
369
+ */
370
+ updateBaseComponentHash(hash) {
371
+ if (this.cache.baseComponentHash !== hash) {
372
+ this.cache.baseComponentHash = hash;
373
+ this.isDirty = true;
374
+ }
375
+ }
376
+ /**
377
+ * Check if config has changed (invalidates all cache)
378
+ */
379
+ hasConfigChanged(config) {
380
+ const currentHash = hashConfig(config);
381
+ return this.cache.configHash !== "" && this.cache.configHash !== currentHash;
382
+ }
383
+ /**
384
+ * Invalidate all cache entries
385
+ */
386
+ invalidateAll() {
387
+ this.cache.entries = {};
388
+ this.isDirty = true;
389
+ }
390
+ /**
391
+ * Clean stale entries (files that no longer exist)
392
+ */
393
+ async cleanStaleEntries(existingSvgPaths) {
394
+ const existingSet = new Set(existingSvgPaths);
395
+ let cleaned = 0;
396
+ for (const svgPath of Object.keys(this.cache.entries)) {
397
+ if (!existingSet.has(svgPath)) {
398
+ delete this.cache.entries[svgPath];
399
+ cleaned++;
400
+ this.isDirty = true;
401
+ }
402
+ }
403
+ if (cleaned > 0) {
404
+ console.log(`\u{1F9F9} Cleaned ${cleaned} stale cache entries`);
405
+ }
406
+ }
407
+ /**
408
+ * Get cache statistics
409
+ */
410
+ getStats() {
411
+ return { ...this.stats };
412
+ }
413
+ /**
414
+ * Reset statistics
415
+ */
416
+ resetStats() {
417
+ this.stats = {
418
+ hits: 0,
419
+ misses: 0,
420
+ total: 0,
421
+ timeSaved: 0
422
+ };
423
+ }
424
+ /**
425
+ * Get cache hit rate
426
+ */
427
+ getHitRate() {
428
+ if (this.stats.total === 0)
429
+ return 0;
430
+ return this.stats.hits / this.stats.total * 100;
431
+ }
432
+ };
433
+
154
434
  // src/config/loader.ts
155
- var import_node_path2 = __toESM(require("path"));
435
+ var import_node_path3 = __toESM(require("path"));
156
436
  var import_node_process = __toESM(require("process"));
157
437
  var import_jiti = require("jiti");
158
438
 
@@ -277,22 +557,22 @@ function formatAttrs(attrs) {
277
557
  }
278
558
 
279
559
  // src/generators/templates/template-engine.ts
280
- var import_node_fs = __toESM(require("fs"));
281
- var import_node_path = __toESM(require("path"));
560
+ var import_node_fs2 = __toESM(require("fs"));
561
+ var import_node_path2 = __toESM(require("path"));
282
562
  var import_node_url = require("url");
283
563
  var import_handlebars = __toESM(require("handlebars"));
284
564
  var import_meta = {};
285
565
  function getTemplatesDir() {
286
566
  if (typeof __dirname !== "undefined") {
287
- return import_node_path.default.join(__dirname, "templates");
567
+ return import_node_path2.default.join(__dirname, "templates");
288
568
  }
289
569
  const currentFile = (0, import_node_url.fileURLToPath)(import_meta.url);
290
- return import_node_path.default.join(import_node_path.default.dirname(currentFile), "templates");
570
+ return import_node_path2.default.join(import_node_path2.default.dirname(currentFile), "templates");
291
571
  }
292
572
  function loadTemplate(templatePath) {
293
573
  const templatesDir = getTemplatesDir();
294
- const fullPath = import_node_path.default.join(templatesDir, templatePath);
295
- const templateContent = import_node_fs.default.readFileSync(fullPath, "utf-8");
574
+ const fullPath = import_node_path2.default.join(templatesDir, templatePath);
575
+ const templateContent = import_node_fs2.default.readFileSync(fullPath, "utf-8");
296
576
  return import_handlebars.default.compile(templateContent, { noEscape: true });
297
577
  }
298
578
  function renderTemplate(templatePath, data) {
@@ -798,8 +1078,8 @@ var DEFAULT_CONFIG = {
798
1078
  }
799
1079
  };
800
1080
  async function loadConfig(configPath) {
801
- const absolutePath = import_node_path2.default.resolve(import_node_process.default.cwd(), configPath);
802
- const configDir = import_node_path2.default.dirname(absolutePath);
1081
+ const absolutePath = import_node_path3.default.resolve(import_node_process.default.cwd(), configPath);
1082
+ const configDir = import_node_path3.default.dirname(absolutePath);
803
1083
  const jiti = (0, import_jiti.createJiti)(configDir, {
804
1084
  interopDefault: true
805
1085
  });
@@ -826,12 +1106,12 @@ async function loadConfig(configPath) {
826
1106
  }
827
1107
  };
828
1108
  if (mergedConfig.configDir && mergedConfig.configDir !== ".") {
829
- const projectRoot = import_node_path2.default.resolve(configDir, mergedConfig.configDir);
830
- mergedConfig.input = import_node_path2.default.resolve(projectRoot, mergedConfig.input);
831
- mergedConfig.output = import_node_path2.default.resolve(projectRoot, mergedConfig.output);
1109
+ const projectRoot = import_node_path3.default.resolve(configDir, mergedConfig.configDir);
1110
+ mergedConfig.input = import_node_path3.default.resolve(projectRoot, mergedConfig.input);
1111
+ mergedConfig.output = import_node_path3.default.resolve(projectRoot, mergedConfig.output);
832
1112
  } else {
833
- mergedConfig.input = import_node_path2.default.resolve(configDir, mergedConfig.input);
834
- mergedConfig.output = import_node_path2.default.resolve(configDir, mergedConfig.output);
1113
+ mergedConfig.input = import_node_path3.default.resolve(configDir, mergedConfig.input);
1114
+ mergedConfig.output = import_node_path3.default.resolve(configDir, mergedConfig.output);
835
1115
  }
836
1116
  if (!mergedConfig.framework) {
837
1117
  const supported = frameworkRegistry.getSupportedFrameworks().join(", ");
@@ -850,7 +1130,7 @@ async function findConfig() {
850
1130
  "vectify.config.js"
851
1131
  ];
852
1132
  for (const file of configFiles) {
853
- const configPath = import_node_path2.default.resolve(import_node_process.default.cwd(), file);
1133
+ const configPath = import_node_path3.default.resolve(import_node_process.default.cwd(), file);
854
1134
  if (await fileExists2(configPath)) {
855
1135
  return configPath;
856
1136
  }
@@ -859,7 +1139,7 @@ async function findConfig() {
859
1139
  }
860
1140
 
861
1141
  // src/generators/index.ts
862
- var import_node_path4 = __toESM(require("path"));
1142
+ var import_node_path5 = __toESM(require("path"));
863
1143
 
864
1144
  // src/parsers/optimizer.ts
865
1145
  var import_svgo = require("svgo");
@@ -895,7 +1175,7 @@ async function optimizeSvg(svgContent, config) {
895
1175
 
896
1176
  // src/utils/formatter.ts
897
1177
  var import_node_child_process = require("child_process");
898
- var import_node_path3 = __toESM(require("path"));
1178
+ var import_node_path4 = __toESM(require("path"));
899
1179
  var import_node_process2 = __toESM(require("process"));
900
1180
  var import_node_util = require("util");
901
1181
  init_helpers();
@@ -950,7 +1230,7 @@ async function detectFormatter() {
950
1230
  for (const tool of priority) {
951
1231
  const patterns = FORMATTER_PATTERNS[tool];
952
1232
  for (const pattern of patterns) {
953
- const configPath = import_node_path3.default.join(cwd, pattern);
1233
+ const configPath = import_node_path4.default.join(cwd, pattern);
954
1234
  if (await fileExists(configPath)) {
955
1235
  return tool;
956
1236
  }
@@ -1046,9 +1326,109 @@ async function generateIcons(config, dryRun = false) {
1046
1326
  }
1047
1327
  return stats;
1048
1328
  }
1049
- async function generateIconComponent(svgFile, config, dryRun = false) {
1050
- let svgContent = await readFile(svgFile);
1051
- const fileName = import_node_path4.default.basename(svgFile);
1329
+ async function generateIconsIncremental(config, cacheManager, dryRun = false) {
1330
+ const stats = {
1331
+ success: 0,
1332
+ failed: 0,
1333
+ total: 0,
1334
+ errors: []
1335
+ };
1336
+ try {
1337
+ if (!dryRun) {
1338
+ await ensureDir(config.output);
1339
+ }
1340
+ const svgFiles = await getSvgFiles(config.input);
1341
+ stats.total = svgFiles.length;
1342
+ if (svgFiles.length === 0) {
1343
+ console.warn(`No SVG files found in ${config.input}`);
1344
+ return stats;
1345
+ }
1346
+ if (cacheManager.hasConfigChanged(config)) {
1347
+ console.log("\u26A0\uFE0F Configuration changed, rebuilding all icons...");
1348
+ cacheManager.invalidateAll();
1349
+ }
1350
+ cacheManager.updateConfigHash(config);
1351
+ await cacheManager.cleanStaleEntries(svgFiles);
1352
+ if (config.generateOptions?.cleanOutput && !dryRun) {
1353
+ await cleanOutputDirectory(svgFiles, config);
1354
+ }
1355
+ await generateBaseComponent(config, dryRun);
1356
+ const strategy = getFrameworkStrategy(config.framework);
1357
+ const typescript = config.typescript ?? true;
1358
+ const fileExt = strategy.getComponentExtension(typescript);
1359
+ const filesToGenerate = [];
1360
+ const cachedFiles = [];
1361
+ for (const svgFile of svgFiles) {
1362
+ const fileName = import_node_path5.default.basename(svgFile);
1363
+ const componentName = getComponentName(
1364
+ fileName,
1365
+ config.prefix,
1366
+ config.suffix,
1367
+ config.componentNameTransform
1368
+ );
1369
+ const componentPath = import_node_path5.default.join(config.output, `${componentName}.${fileExt}`);
1370
+ const needsRegen = await cacheManager.needsRegeneration(
1371
+ svgFile,
1372
+ componentPath,
1373
+ config
1374
+ );
1375
+ if (needsRegen) {
1376
+ filesToGenerate.push(svgFile);
1377
+ } else {
1378
+ cachedFiles.push(svgFile);
1379
+ }
1380
+ }
1381
+ if (cachedFiles.length > 0) {
1382
+ console.log(`\u{1F4E6} Cache: ${cachedFiles.length} cached, ${filesToGenerate.length} to generate`);
1383
+ }
1384
+ for (const svgFile of filesToGenerate) {
1385
+ try {
1386
+ await generateIconComponent(svgFile, config, dryRun, cacheManager);
1387
+ stats.success++;
1388
+ } catch (error) {
1389
+ stats.failed++;
1390
+ stats.errors.push({
1391
+ file: svgFile,
1392
+ error: error.message
1393
+ });
1394
+ console.error(`Failed to generate ${svgFile}: ${error.message}`);
1395
+ }
1396
+ }
1397
+ stats.success += cachedFiles.length;
1398
+ if (config.generateOptions?.index) {
1399
+ await generateIndexFile(svgFiles, config, dryRun);
1400
+ }
1401
+ if (config.generateOptions?.preview && !dryRun) {
1402
+ await generatePreviewHtml(svgFiles, config);
1403
+ }
1404
+ if (!dryRun) {
1405
+ await cacheManager.save();
1406
+ const cacheStats = cacheManager.getStats();
1407
+ if (cacheStats.total > 0) {
1408
+ const hitRate = cacheManager.getHitRate();
1409
+ const timeSaved = (cacheStats.timeSaved / 1e3).toFixed(1);
1410
+ console.log(`\u26A1 Cache saved ~${timeSaved}s (${hitRate.toFixed(1)}% hit rate)`);
1411
+ }
1412
+ }
1413
+ if (config.format && !dryRun) {
1414
+ const formatResult = await formatOutput(config.output, config.format);
1415
+ if (formatResult.success && formatResult.tool) {
1416
+ console.log(`Formatted with ${formatResult.tool}`);
1417
+ } else if (formatResult.error) {
1418
+ console.warn(formatResult.error);
1419
+ }
1420
+ }
1421
+ if (config.hooks?.onComplete) {
1422
+ await config.hooks.onComplete(stats);
1423
+ }
1424
+ } catch (error) {
1425
+ throw new Error(`Generation failed: ${error.message}`);
1426
+ }
1427
+ return stats;
1428
+ }
1429
+ async function generateIconComponent(svgFile, config, dryRun = false, cacheManager) {
1430
+ let svgContent = await readFile3(svgFile);
1431
+ const fileName = import_node_path5.default.basename(svgFile);
1052
1432
  if (config.hooks?.beforeParse) {
1053
1433
  svgContent = await config.hooks.beforeParse(svgContent, fileName);
1054
1434
  }
@@ -1059,7 +1439,7 @@ async function generateIconComponent(svgFile, config, dryRun = false) {
1059
1439
  fileName,
1060
1440
  config.prefix,
1061
1441
  config.suffix,
1062
- config.transform
1442
+ config.componentNameTransform
1063
1443
  );
1064
1444
  const strategy = getFrameworkStrategy(config.framework);
1065
1445
  const typescript = config.typescript ?? true;
@@ -1075,22 +1455,32 @@ async function generateIconComponent(svgFile, config, dryRun = false) {
1075
1455
  code = await config.hooks.afterGenerate(code, componentName);
1076
1456
  }
1077
1457
  const fileExt = strategy.getComponentExtension(typescript);
1078
- const outputPath = import_node_path4.default.join(config.output, `${componentName}.${fileExt}`);
1458
+ const outputPath = import_node_path5.default.join(config.output, `${componentName}.${fileExt}`);
1079
1459
  if (dryRun) {
1080
1460
  console.log(` ${componentName}.${fileExt}`);
1081
1461
  } else {
1082
- await writeFile(outputPath, code);
1462
+ await writeFile2(outputPath, code);
1463
+ if (cacheManager) {
1464
+ const componentHash = hashContent(code);
1465
+ await cacheManager.updateEntry(
1466
+ svgFile,
1467
+ outputPath,
1468
+ componentName,
1469
+ componentHash,
1470
+ config
1471
+ );
1472
+ }
1083
1473
  }
1084
1474
  }
1085
1475
  async function generateBaseComponent(config, dryRun = false) {
1086
1476
  const typescript = config.typescript ?? true;
1087
1477
  const strategy = getFrameworkStrategy(config.framework);
1088
1478
  const { code, fileName } = strategy.generateBaseComponent(typescript);
1089
- const outputPath = import_node_path4.default.join(config.output, fileName);
1479
+ const outputPath = import_node_path5.default.join(config.output, fileName);
1090
1480
  if (dryRun) {
1091
1481
  console.log(` ${fileName}`);
1092
1482
  } else {
1093
- await writeFile(outputPath, code);
1483
+ await writeFile2(outputPath, code);
1094
1484
  }
1095
1485
  }
1096
1486
  async function generateIndexFile(svgFiles, config, dryRun = false) {
@@ -1101,12 +1491,12 @@ async function generateIndexFile(svgFiles, config, dryRun = false) {
1101
1491
  const usesDefaultExport = ["vue", "vue2", "svelte", "react", "preact"].includes(config.framework);
1102
1492
  const needsExtension = ["vue", "vue2", "svelte"].includes(config.framework);
1103
1493
  const exports2 = svgFiles.map((svgFile) => {
1104
- const fileName = import_node_path4.default.basename(svgFile);
1494
+ const fileName = import_node_path5.default.basename(svgFile);
1105
1495
  const componentName = getComponentName(
1106
1496
  fileName,
1107
1497
  config.prefix,
1108
1498
  config.suffix,
1109
- config.transform
1499
+ config.componentNameTransform
1110
1500
  );
1111
1501
  const importPath = needsExtension ? `./${componentName}.${componentExt}` : `./${componentName}`;
1112
1502
  if (usesDefaultExport) {
@@ -1115,27 +1505,27 @@ async function generateIndexFile(svgFiles, config, dryRun = false) {
1115
1505
  return `export { ${componentName} } from '${importPath}'`;
1116
1506
  }
1117
1507
  }).join("\n");
1118
- const indexPath = import_node_path4.default.join(config.output, `index.${ext}`);
1508
+ const indexPath = import_node_path5.default.join(config.output, `index.${ext}`);
1119
1509
  if (dryRun) {
1120
1510
  console.log(` index.${ext}`);
1121
1511
  } else {
1122
- await writeFile(indexPath, `${exports2}
1512
+ await writeFile2(indexPath, `${exports2}
1123
1513
  `);
1124
1514
  }
1125
1515
  }
1126
1516
  async function generatePreviewHtml(svgFiles, config) {
1127
1517
  const componentNames = svgFiles.map((svgFile) => {
1128
- const fileName = import_node_path4.default.basename(svgFile);
1518
+ const fileName = import_node_path5.default.basename(svgFile);
1129
1519
  return getComponentName(
1130
1520
  fileName,
1131
1521
  config.prefix,
1132
1522
  config.suffix,
1133
- config.transform
1523
+ config.componentNameTransform
1134
1524
  );
1135
1525
  });
1136
1526
  const svgContents = await Promise.all(
1137
1527
  svgFiles.map(async (svgFile) => {
1138
- return await readFile(svgFile);
1528
+ return await readFile3(svgFile);
1139
1529
  })
1140
1530
  );
1141
1531
  const html = `<!DOCTYPE html>
@@ -1297,8 +1687,8 @@ async function generatePreviewHtml(svgFiles, config) {
1297
1687
  </script>
1298
1688
  </body>
1299
1689
  </html>`;
1300
- const previewPath = import_node_path4.default.join(config.output, "preview.html");
1301
- await writeFile(previewPath, html);
1690
+ const previewPath = import_node_path5.default.join(config.output, "preview.html");
1691
+ await writeFile2(previewPath, html);
1302
1692
  }
1303
1693
  async function cleanOutputDirectory(svgFiles, config) {
1304
1694
  const { readdir, unlink } = await import("fs/promises");
@@ -1306,12 +1696,12 @@ async function cleanOutputDirectory(svgFiles, config) {
1306
1696
  const fileExt = strategy.getComponentExtension(config.typescript ?? true);
1307
1697
  const expectedComponents = new Set(
1308
1698
  svgFiles.map((svgFile) => {
1309
- const fileName = import_node_path4.default.basename(svgFile, ".svg");
1699
+ const fileName = import_node_path5.default.basename(svgFile, ".svg");
1310
1700
  const componentName = getComponentName(
1311
1701
  fileName,
1312
1702
  config.prefix,
1313
1703
  config.suffix,
1314
- config.transform
1704
+ config.componentNameTransform
1315
1705
  );
1316
1706
  return `${componentName}.${fileExt}`;
1317
1707
  })
@@ -1333,7 +1723,7 @@ async function cleanOutputDirectory(svgFiles, config) {
1333
1723
  continue;
1334
1724
  }
1335
1725
  if (!expectedComponents.has(file)) {
1336
- const filePath = import_node_path4.default.join(config.output, file);
1726
+ const filePath = import_node_path5.default.join(config.output, file);
1337
1727
  await unlink(filePath);
1338
1728
  console.log(`Deleted orphaned component: ${file}`);
1339
1729
  }
@@ -1365,9 +1755,19 @@ async function generate(options = {}) {
1365
1755
  ${import_chalk.default.bold("Files that would be generated:")}
1366
1756
  `);
1367
1757
  }
1758
+ const useIncremental = config.incremental?.enabled !== false && !options.force && !options.dryRun;
1759
+ let cacheManager = null;
1760
+ if (useIncremental) {
1761
+ const cacheDir = config.incremental?.cacheDir || ".vectify";
1762
+ cacheManager = new CacheManager(config.output, cacheDir);
1763
+ await cacheManager.load();
1764
+ }
1765
+ if (options.force) {
1766
+ spinner.info(import_chalk.default.yellow("Force mode - ignoring cache, regenerating all icons"));
1767
+ }
1368
1768
  const actionText = options.dryRun ? "Analyzing icons..." : "Generating icon components...";
1369
1769
  spinner.start(actionText);
1370
- const stats = await generateIcons(config, options.dryRun);
1770
+ const stats = cacheManager ? await generateIconsIncremental(config, cacheManager, options.dryRun) : await generateIcons(config, options.dryRun);
1371
1771
  if (stats.failed > 0) {
1372
1772
  spinner.warn(`${options.dryRun ? "Analyzed" : "Generated"} ${import_chalk.default.green(stats.success)} icons, ${import_chalk.default.red(stats.failed)} failed`);
1373
1773
  if (stats.errors.length > 0) {
@@ -1400,7 +1800,7 @@ ${import_chalk.default.bold("Output:")} ${import_chalk.default.cyan(config.outpu
1400
1800
  }
1401
1801
 
1402
1802
  // src/commands/init.ts
1403
- var import_node_path5 = __toESM(require("path"));
1803
+ var import_node_path6 = __toESM(require("path"));
1404
1804
  var import_node_process3 = __toESM(require("process"));
1405
1805
  var import_chalk2 = __toESM(require("chalk"));
1406
1806
  var import_inquirer = __toESM(require("inquirer"));
@@ -1430,8 +1830,8 @@ Note: Project root detected at ${import_chalk2.default.cyan(projectRoot)}`));
1430
1830
  }
1431
1831
  }
1432
1832
  ]);
1433
- const configPath = import_node_path5.default.resolve(projectRoot, pathAnswers.configPath);
1434
- const configDir = import_node_path5.default.dirname(configPath);
1833
+ const configPath = import_node_path6.default.resolve(projectRoot, pathAnswers.configPath);
1834
+ const configDir = import_node_path6.default.dirname(configPath);
1435
1835
  if (!options.force && await fileExists(configPath)) {
1436
1836
  const { overwrite } = await import_inquirer.default.prompt([
1437
1837
  {
@@ -1505,18 +1905,18 @@ Note: Project root detected at ${import_chalk2.default.cyan(projectRoot)}`));
1505
1905
  default: ""
1506
1906
  }
1507
1907
  ]);
1508
- const inputPath = import_node_path5.default.resolve(projectRoot, answers.input);
1509
- const outputPath = import_node_path5.default.resolve(projectRoot, answers.output);
1908
+ const inputPath = import_node_path6.default.resolve(projectRoot, answers.input);
1909
+ const outputPath = import_node_path6.default.resolve(projectRoot, answers.output);
1510
1910
  const spinner = (0, import_ora2.default)("Setting up directories...").start();
1511
1911
  await ensureDir(inputPath);
1512
1912
  spinner.text = `Created input directory: ${import_chalk2.default.cyan(answers.input)}`;
1513
1913
  await ensureDir(outputPath);
1514
1914
  spinner.succeed(`Created output directory: ${import_chalk2.default.cyan(answers.output)}`);
1515
- const relativeConfigDir = import_node_path5.default.relative(configDir, projectRoot) || ".";
1915
+ const relativeConfigDir = import_node_path6.default.relative(configDir, projectRoot) || ".";
1516
1916
  const finalFramework = answers.vueVersion || answers.framework;
1517
1917
  const configContent = generateConfigContent({ ...answers, framework: finalFramework }, relativeConfigDir);
1518
1918
  spinner.start("Creating config file...");
1519
- await writeFile(configPath, configContent);
1919
+ await writeFile2(configPath, configContent);
1520
1920
  spinner.succeed(`Config file created at ${import_chalk2.default.green(configPath)}`);
1521
1921
  console.log(`
1522
1922
  ${import_chalk2.default.bold("Next steps:")}`);
@@ -1556,10 +1956,93 @@ export default defineConfig({
1556
1956
  }
1557
1957
 
1558
1958
  // src/commands/watch.ts
1559
- var import_node_path6 = __toESM(require("path"));
1959
+ var import_node_path7 = __toESM(require("path"));
1560
1960
  var import_chalk3 = __toESM(require("chalk"));
1561
1961
  var import_chokidar = __toESM(require("chokidar"));
1562
1962
  var import_ora3 = __toESM(require("ora"));
1963
+
1964
+ // src/cache/svg-validator.ts
1965
+ var import_promises3 = require("fs/promises");
1966
+ var import_cheerio = require("cheerio");
1967
+ var MIN_SVG_SIZE = 20;
1968
+ var DRAWABLE_ELEMENTS = [
1969
+ "path",
1970
+ "circle",
1971
+ "rect",
1972
+ "ellipse",
1973
+ "line",
1974
+ "polyline",
1975
+ "polygon",
1976
+ "text",
1977
+ "image",
1978
+ "use"
1979
+ ];
1980
+ async function validateSVGFile(filePath) {
1981
+ try {
1982
+ const stats = await (0, import_promises3.stat)(filePath);
1983
+ if (stats.size < MIN_SVG_SIZE) {
1984
+ return {
1985
+ isValid: false,
1986
+ isEmpty: true,
1987
+ reason: "File is too small to be a valid SVG"
1988
+ };
1989
+ }
1990
+ const content = await (0, import_promises3.readFile)(filePath, "utf-8");
1991
+ if (!content.trim()) {
1992
+ return {
1993
+ isValid: false,
1994
+ isEmpty: true,
1995
+ reason: "File is empty"
1996
+ };
1997
+ }
1998
+ if (!content.includes("<svg")) {
1999
+ return {
2000
+ isValid: false,
2001
+ isEmpty: false,
2002
+ reason: "File does not contain <svg> tag"
2003
+ };
2004
+ }
2005
+ if (!hasDrawableContent(content)) {
2006
+ return {
2007
+ isValid: false,
2008
+ isEmpty: true,
2009
+ reason: "SVG has no drawable content"
2010
+ };
2011
+ }
2012
+ try {
2013
+ const $ = (0, import_cheerio.load)(content, { xmlMode: true });
2014
+ const svgElement = $("svg");
2015
+ if (svgElement.length === 0) {
2016
+ return {
2017
+ isValid: false,
2018
+ isEmpty: false,
2019
+ reason: "Failed to parse SVG element"
2020
+ };
2021
+ }
2022
+ return {
2023
+ isValid: true,
2024
+ isEmpty: false
2025
+ };
2026
+ } catch (parseError) {
2027
+ return {
2028
+ isValid: false,
2029
+ isEmpty: false,
2030
+ reason: `Failed to parse SVG: ${parseError}`
2031
+ };
2032
+ }
2033
+ } catch (error) {
2034
+ return {
2035
+ isValid: false,
2036
+ isEmpty: false,
2037
+ reason: `Failed to read file: ${error}`
2038
+ };
2039
+ }
2040
+ }
2041
+ function hasDrawableContent(content) {
2042
+ return DRAWABLE_ELEMENTS.some((element) => content.includes(`<${element}`));
2043
+ }
2044
+
2045
+ // src/commands/watch.ts
1563
2046
  async function watch(options = {}) {
1564
2047
  const spinner = (0, import_ora3.default)("Loading configuration...").start();
1565
2048
  try {
@@ -1575,16 +2058,24 @@ async function watch(options = {}) {
1575
2058
  }
1576
2059
  const config = await loadConfig(configPath);
1577
2060
  spinner.succeed(`Config loaded from ${import_chalk3.default.green(configPath)}`);
2061
+ const useIncremental = config.incremental?.enabled !== false;
2062
+ const cacheDir = config.incremental?.cacheDir || ".vectify";
2063
+ const cacheManager = useIncremental ? new CacheManager(config.output, cacheDir) : null;
2064
+ if (cacheManager) {
2065
+ await cacheManager.load();
2066
+ }
1578
2067
  spinner.start("Generating icon components...");
1579
- const initialStats = await generateIcons(config);
2068
+ const initialStats = useIncremental && cacheManager ? await generateIconsIncremental(config, cacheManager) : await generateIcons(config);
1580
2069
  spinner.succeed(`Generated ${import_chalk3.default.green(initialStats.success)} icon components`);
1581
- const watchPath = import_node_path6.default.join(config.input, "**/*.svg");
2070
+ const watchPath = import_node_path7.default.join(config.input, "**/*.svg");
1582
2071
  const debounce = config.watch?.debounce ?? 300;
1583
2072
  const ignore = config.watch?.ignore ?? ["**/node_modules/**", "**/.git/**"];
2073
+ const emptyFileRetryDelay = config.watch?.emptyFileRetryDelay ?? 2e3;
1584
2074
  console.log(import_chalk3.default.bold("\nWatching for changes..."));
1585
2075
  console.log(` ${import_chalk3.default.cyan(watchPath)}`);
1586
2076
  console.log(` ${import_chalk3.default.gray("Press Ctrl+C to stop")}
1587
2077
  `);
2078
+ const pendingChanges = /* @__PURE__ */ new Map();
1588
2079
  const debounceTimer = null;
1589
2080
  const watcher = import_chokidar.default.watch(watchPath, {
1590
2081
  ignored: ignore,
@@ -1592,12 +2083,12 @@ async function watch(options = {}) {
1592
2083
  ignoreInitial: true
1593
2084
  });
1594
2085
  watcher.on("add", (filePath) => {
1595
- handleChange("added", filePath, config, debounce, debounceTimer);
2086
+ handleChange("added", filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, debounceTimer);
1596
2087
  }).on("change", (filePath) => {
1597
- handleChange("changed", filePath, config, debounce, debounceTimer);
2088
+ handleChange("changed", filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, debounceTimer);
1598
2089
  }).on("unlink", (filePath) => {
1599
- console.log(import_chalk3.default.yellow(`SVG file removed: ${import_node_path6.default.basename(filePath)}`));
1600
- handleChange("removed", filePath, config, debounce, debounceTimer);
2090
+ console.log(import_chalk3.default.yellow(`SVG file removed: ${import_node_path7.default.basename(filePath)}`));
2091
+ handleChange("removed", filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, debounceTimer);
1601
2092
  }).on("error", (error) => {
1602
2093
  console.error(import_chalk3.default.red(`Watcher error: ${error.message}`));
1603
2094
  });
@@ -1614,23 +2105,51 @@ ${import_chalk3.default.yellow("Stopping watch mode...")}`);
1614
2105
  throw error;
1615
2106
  }
1616
2107
  }
1617
- function handleChange(event, filePath, config, debounce, timer) {
1618
- const fileName = import_node_path6.default.basename(filePath);
2108
+ function handleChange(event, filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, timer) {
2109
+ pendingChanges.set(filePath, event);
1619
2110
  if (timer) {
1620
2111
  clearTimeout(timer);
1621
2112
  }
1622
2113
  timer = setTimeout(async () => {
1623
- const spinner = (0, import_ora3.default)(`Regenerating icons...`).start();
2114
+ const changes = Array.from(pendingChanges.entries());
2115
+ pendingChanges.clear();
2116
+ const validChanges = [];
2117
+ const invalidFiles = [];
2118
+ for (const [file, changeEvent] of changes) {
2119
+ if (changeEvent === "removed") {
2120
+ validChanges.push([file, changeEvent]);
2121
+ continue;
2122
+ }
2123
+ const validation = await validateSVGFile(file);
2124
+ if (!validation.isValid) {
2125
+ if (validation.isEmpty) {
2126
+ console.log(import_chalk3.default.yellow(`\u23F3 Waiting for content: ${import_node_path7.default.basename(file)}`));
2127
+ setTimeout(() => {
2128
+ handleChange(changeEvent, file, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, timer);
2129
+ }, emptyFileRetryDelay);
2130
+ } else {
2131
+ console.error(import_chalk3.default.red(`\u274C Invalid SVG: ${import_node_path7.default.basename(file)} - ${validation.reason}`));
2132
+ invalidFiles.push(file);
2133
+ }
2134
+ continue;
2135
+ }
2136
+ validChanges.push([file, changeEvent]);
2137
+ }
2138
+ if (validChanges.length === 0) {
2139
+ return;
2140
+ }
2141
+ const spinner = (0, import_ora3.default)(`Processing ${validChanges.length} change(s)...`).start();
1624
2142
  try {
1625
- const stats = await generateIcons(config);
2143
+ const stats = cacheManager ? await generateIconsIncremental(config, cacheManager) : await generateIcons(config);
1626
2144
  if (stats.failed > 0) {
1627
2145
  spinner.warn(
1628
2146
  `Regenerated ${import_chalk3.default.green(stats.success)} icons, ${import_chalk3.default.red(stats.failed)} failed`
1629
2147
  );
1630
2148
  } else {
1631
- spinner.succeed(`Regenerated ${import_chalk3.default.green(stats.success)} icon components`);
2149
+ spinner.succeed(`\u2713 Updated ${import_chalk3.default.green(stats.success)} icon components`);
1632
2150
  }
1633
- console.log(import_chalk3.default.gray(` Triggered by: ${fileName} (${event})
2151
+ const triggerFiles = validChanges.map(([file, evt]) => `${import_node_path7.default.basename(file)} (${evt})`).join(", ");
2152
+ console.log(import_chalk3.default.gray(` Triggered by: ${triggerFiles}
1634
2153
  `));
1635
2154
  } catch (error) {
1636
2155
  spinner.fail("Regeneration failed");