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/cli.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
  }
@@ -133,8 +133,8 @@ var init_helpers = __esm({
133
133
  });
134
134
 
135
135
  // src/cli.ts
136
- var import_node_fs2 = require("fs");
137
- var import_node_path7 = require("path");
136
+ var import_node_fs3 = require("fs");
137
+ var import_node_path8 = require("path");
138
138
  var import_node_url2 = require("url");
139
139
  var import_chalk4 = __toESM(require("chalk"));
140
140
  var import_commander = require("commander");
@@ -143,8 +143,288 @@ var import_commander = require("commander");
143
143
  var import_chalk = __toESM(require("chalk"));
144
144
  var import_ora = __toESM(require("ora"));
145
145
 
146
+ // src/cache/cache-manager.ts
147
+ var import_node_fs = require("fs");
148
+ var import_promises2 = require("fs/promises");
149
+ var import_node_path = require("path");
150
+
151
+ // src/cache/hash-utils.ts
152
+ var import_node_crypto = require("crypto");
153
+ var import_promises = require("fs/promises");
154
+ function hashContent(content) {
155
+ return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
156
+ }
157
+ async function hashFile(filePath) {
158
+ try {
159
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
160
+ return hashContent(content);
161
+ } catch (error) {
162
+ throw new Error(`Failed to hash file ${filePath}: ${error}`);
163
+ }
164
+ }
165
+ function hashConfig(config) {
166
+ const relevantConfig = {
167
+ framework: config.framework,
168
+ typescript: config.typescript,
169
+ keepColors: config.keepColors,
170
+ prefix: config.prefix,
171
+ suffix: config.suffix,
172
+ optimize: config.optimize,
173
+ svgoConfig: config.svgoConfig,
174
+ componentNameTransform: config.componentNameTransform?.toString()
175
+ };
176
+ return hashContent(JSON.stringify(relevantConfig));
177
+ }
178
+ function hashSvgoConfig(svgoConfig) {
179
+ return hashContent(JSON.stringify(svgoConfig || {}));
180
+ }
181
+
182
+ // src/cache/cache-manager.ts
183
+ var CACHE_VERSION = "1.0.0";
184
+ var CacheManager = class {
185
+ constructor(outputDir, cacheDir = ".vectify") {
186
+ this.cache = {
187
+ version: CACHE_VERSION,
188
+ configHash: "",
189
+ entries: {},
190
+ baseComponentHash: ""
191
+ };
192
+ this.cacheFilePath = (0, import_node_path.join)(outputDir, cacheDir, "cache.json");
193
+ this.stats = {
194
+ hits: 0,
195
+ misses: 0,
196
+ total: 0,
197
+ timeSaved: 0
198
+ };
199
+ this.isDirty = false;
200
+ process.on("beforeExit", () => {
201
+ if (this.isDirty) {
202
+ this.saveSync();
203
+ }
204
+ });
205
+ }
206
+ /**
207
+ * Load cache from disk
208
+ */
209
+ async load() {
210
+ try {
211
+ if (!(0, import_node_fs.existsSync)(this.cacheFilePath)) {
212
+ return;
213
+ }
214
+ const content = await (0, import_promises2.readFile)(this.cacheFilePath, "utf-8");
215
+ const loadedCache = JSON.parse(content);
216
+ if (loadedCache.version !== CACHE_VERSION) {
217
+ console.log("\u26A0\uFE0F Cache version mismatch, rebuilding cache...");
218
+ return;
219
+ }
220
+ this.cache = loadedCache;
221
+ } catch (error) {
222
+ console.warn("\u26A0\uFE0F Failed to load cache, starting fresh:", error);
223
+ this.cache = {
224
+ version: CACHE_VERSION,
225
+ configHash: "",
226
+ entries: {},
227
+ baseComponentHash: ""
228
+ };
229
+ }
230
+ }
231
+ /**
232
+ * Save cache to disk (atomic write)
233
+ */
234
+ async save() {
235
+ try {
236
+ const cacheDir = (0, import_node_path.dirname)(this.cacheFilePath);
237
+ await (0, import_promises2.mkdir)(cacheDir, { recursive: true });
238
+ const tempPath = `${this.cacheFilePath}.tmp`;
239
+ await (0, import_promises2.writeFile)(tempPath, JSON.stringify(this.cache, null, 2), "utf-8");
240
+ await (0, import_promises2.writeFile)(this.cacheFilePath, JSON.stringify(this.cache, null, 2), "utf-8");
241
+ this.isDirty = false;
242
+ } catch (error) {
243
+ console.warn("\u26A0\uFE0F Failed to save cache:", error);
244
+ }
245
+ }
246
+ /**
247
+ * Synchronous save for process exit
248
+ */
249
+ saveSync() {
250
+ try {
251
+ const fs2 = require("fs");
252
+ const cacheDir = (0, import_node_path.dirname)(this.cacheFilePath);
253
+ if (!fs2.existsSync(cacheDir)) {
254
+ fs2.mkdirSync(cacheDir, { recursive: true });
255
+ }
256
+ fs2.writeFileSync(this.cacheFilePath, JSON.stringify(this.cache, null, 2), "utf-8");
257
+ this.isDirty = false;
258
+ } catch {
259
+ console.warn("\u26A0\uFE0F Failed to save cache on exit");
260
+ }
261
+ }
262
+ /**
263
+ * Check if a file needs regeneration
264
+ */
265
+ async needsRegeneration(svgPath, componentPath, config) {
266
+ this.stats.total++;
267
+ if (!(0, import_node_fs.existsSync)(componentPath)) {
268
+ this.stats.misses++;
269
+ return true;
270
+ }
271
+ const entry = this.cache.entries[svgPath];
272
+ if (!entry) {
273
+ this.stats.misses++;
274
+ return true;
275
+ }
276
+ try {
277
+ const stats = await (0, import_promises2.stat)(svgPath);
278
+ const currentMtime = stats.mtimeMs;
279
+ if (currentMtime === entry.svgMtime) {
280
+ if (!this.isConfigMatching(entry, config)) {
281
+ this.stats.misses++;
282
+ return true;
283
+ }
284
+ this.stats.hits++;
285
+ this.stats.timeSaved += 50;
286
+ return false;
287
+ }
288
+ const currentHash = await hashFile(svgPath);
289
+ if (currentHash === entry.svgHash) {
290
+ entry.svgMtime = currentMtime;
291
+ this.isDirty = true;
292
+ this.stats.hits++;
293
+ this.stats.timeSaved += 50;
294
+ return false;
295
+ }
296
+ this.stats.misses++;
297
+ return true;
298
+ } catch {
299
+ this.stats.misses++;
300
+ return true;
301
+ }
302
+ }
303
+ /**
304
+ * Check if config matches cache entry
305
+ */
306
+ isConfigMatching(entry, config) {
307
+ const snapshot = entry.configSnapshot;
308
+ 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);
309
+ }
310
+ /**
311
+ * Update cache entry after successful generation
312
+ */
313
+ async updateEntry(svgPath, componentPath, componentName, componentHash, config) {
314
+ try {
315
+ const svgHash = await hashFile(svgPath);
316
+ const stats = await (0, import_promises2.stat)(svgPath);
317
+ this.cache.entries[svgPath] = {
318
+ svgPath,
319
+ svgHash,
320
+ svgMtime: stats.mtimeMs,
321
+ componentName,
322
+ componentPath,
323
+ componentHash,
324
+ configSnapshot: {
325
+ framework: config.framework,
326
+ typescript: config.typescript ?? true,
327
+ keepColors: config.keepColors ?? false,
328
+ prefix: config.prefix || "",
329
+ suffix: config.suffix || "",
330
+ optimize: config.optimize ?? true,
331
+ svgoConfigHash: hashSvgoConfig(config.svgoConfig)
332
+ },
333
+ generatedAt: Date.now()
334
+ };
335
+ this.isDirty = true;
336
+ } catch (error) {
337
+ console.warn(`\u26A0\uFE0F Failed to update cache entry for ${svgPath}:`, error);
338
+ }
339
+ }
340
+ /**
341
+ * Remove cache entry
342
+ */
343
+ removeEntry(svgPath) {
344
+ if (this.cache.entries[svgPath]) {
345
+ delete this.cache.entries[svgPath];
346
+ this.isDirty = true;
347
+ }
348
+ }
349
+ /**
350
+ * Update config hash
351
+ */
352
+ updateConfigHash(config) {
353
+ const newHash = hashConfig(config);
354
+ if (this.cache.configHash !== newHash) {
355
+ this.cache.configHash = newHash;
356
+ this.isDirty = true;
357
+ }
358
+ }
359
+ /**
360
+ * Update base component hash
361
+ */
362
+ updateBaseComponentHash(hash) {
363
+ if (this.cache.baseComponentHash !== hash) {
364
+ this.cache.baseComponentHash = hash;
365
+ this.isDirty = true;
366
+ }
367
+ }
368
+ /**
369
+ * Check if config has changed (invalidates all cache)
370
+ */
371
+ hasConfigChanged(config) {
372
+ const currentHash = hashConfig(config);
373
+ return this.cache.configHash !== "" && this.cache.configHash !== currentHash;
374
+ }
375
+ /**
376
+ * Invalidate all cache entries
377
+ */
378
+ invalidateAll() {
379
+ this.cache.entries = {};
380
+ this.isDirty = true;
381
+ }
382
+ /**
383
+ * Clean stale entries (files that no longer exist)
384
+ */
385
+ async cleanStaleEntries(existingSvgPaths) {
386
+ const existingSet = new Set(existingSvgPaths);
387
+ let cleaned = 0;
388
+ for (const svgPath of Object.keys(this.cache.entries)) {
389
+ if (!existingSet.has(svgPath)) {
390
+ delete this.cache.entries[svgPath];
391
+ cleaned++;
392
+ this.isDirty = true;
393
+ }
394
+ }
395
+ if (cleaned > 0) {
396
+ console.log(`\u{1F9F9} Cleaned ${cleaned} stale cache entries`);
397
+ }
398
+ }
399
+ /**
400
+ * Get cache statistics
401
+ */
402
+ getStats() {
403
+ return { ...this.stats };
404
+ }
405
+ /**
406
+ * Reset statistics
407
+ */
408
+ resetStats() {
409
+ this.stats = {
410
+ hits: 0,
411
+ misses: 0,
412
+ total: 0,
413
+ timeSaved: 0
414
+ };
415
+ }
416
+ /**
417
+ * Get cache hit rate
418
+ */
419
+ getHitRate() {
420
+ if (this.stats.total === 0)
421
+ return 0;
422
+ return this.stats.hits / this.stats.total * 100;
423
+ }
424
+ };
425
+
146
426
  // src/config/loader.ts
147
- var import_node_path2 = __toESM(require("path"));
427
+ var import_node_path3 = __toESM(require("path"));
148
428
  var import_node_process = __toESM(require("process"));
149
429
  var import_jiti = require("jiti");
150
430
 
@@ -269,22 +549,22 @@ function formatAttrs(attrs) {
269
549
  }
270
550
 
271
551
  // src/generators/templates/template-engine.ts
272
- var import_node_fs = __toESM(require("fs"));
273
- var import_node_path = __toESM(require("path"));
552
+ var import_node_fs2 = __toESM(require("fs"));
553
+ var import_node_path2 = __toESM(require("path"));
274
554
  var import_node_url = require("url");
275
555
  var import_handlebars = __toESM(require("handlebars"));
276
556
  var import_meta = {};
277
557
  function getTemplatesDir() {
278
558
  if (typeof __dirname !== "undefined") {
279
- return import_node_path.default.join(__dirname, "templates");
559
+ return import_node_path2.default.join(__dirname, "templates");
280
560
  }
281
561
  const currentFile = (0, import_node_url.fileURLToPath)(import_meta.url);
282
- return import_node_path.default.join(import_node_path.default.dirname(currentFile), "templates");
562
+ return import_node_path2.default.join(import_node_path2.default.dirname(currentFile), "templates");
283
563
  }
284
564
  function loadTemplate(templatePath) {
285
565
  const templatesDir = getTemplatesDir();
286
- const fullPath = import_node_path.default.join(templatesDir, templatePath);
287
- const templateContent = import_node_fs.default.readFileSync(fullPath, "utf-8");
566
+ const fullPath = import_node_path2.default.join(templatesDir, templatePath);
567
+ const templateContent = import_node_fs2.default.readFileSync(fullPath, "utf-8");
288
568
  return import_handlebars.default.compile(templateContent, { noEscape: true });
289
569
  }
290
570
  function renderTemplate(templatePath, data) {
@@ -790,8 +1070,8 @@ var DEFAULT_CONFIG = {
790
1070
  }
791
1071
  };
792
1072
  async function loadConfig(configPath) {
793
- const absolutePath = import_node_path2.default.resolve(import_node_process.default.cwd(), configPath);
794
- const configDir = import_node_path2.default.dirname(absolutePath);
1073
+ const absolutePath = import_node_path3.default.resolve(import_node_process.default.cwd(), configPath);
1074
+ const configDir = import_node_path3.default.dirname(absolutePath);
795
1075
  const jiti = (0, import_jiti.createJiti)(configDir, {
796
1076
  interopDefault: true
797
1077
  });
@@ -818,12 +1098,12 @@ async function loadConfig(configPath) {
818
1098
  }
819
1099
  };
820
1100
  if (mergedConfig.configDir && mergedConfig.configDir !== ".") {
821
- const projectRoot = import_node_path2.default.resolve(configDir, mergedConfig.configDir);
822
- mergedConfig.input = import_node_path2.default.resolve(projectRoot, mergedConfig.input);
823
- mergedConfig.output = import_node_path2.default.resolve(projectRoot, mergedConfig.output);
1101
+ const projectRoot = import_node_path3.default.resolve(configDir, mergedConfig.configDir);
1102
+ mergedConfig.input = import_node_path3.default.resolve(projectRoot, mergedConfig.input);
1103
+ mergedConfig.output = import_node_path3.default.resolve(projectRoot, mergedConfig.output);
824
1104
  } else {
825
- mergedConfig.input = import_node_path2.default.resolve(configDir, mergedConfig.input);
826
- mergedConfig.output = import_node_path2.default.resolve(configDir, mergedConfig.output);
1105
+ mergedConfig.input = import_node_path3.default.resolve(configDir, mergedConfig.input);
1106
+ mergedConfig.output = import_node_path3.default.resolve(configDir, mergedConfig.output);
827
1107
  }
828
1108
  if (!mergedConfig.framework) {
829
1109
  const supported = frameworkRegistry.getSupportedFrameworks().join(", ");
@@ -842,7 +1122,7 @@ async function findConfig() {
842
1122
  "vectify.config.js"
843
1123
  ];
844
1124
  for (const file of configFiles) {
845
- const configPath = import_node_path2.default.resolve(import_node_process.default.cwd(), file);
1125
+ const configPath = import_node_path3.default.resolve(import_node_process.default.cwd(), file);
846
1126
  if (await fileExists2(configPath)) {
847
1127
  return configPath;
848
1128
  }
@@ -851,7 +1131,7 @@ async function findConfig() {
851
1131
  }
852
1132
 
853
1133
  // src/generators/index.ts
854
- var import_node_path4 = __toESM(require("path"));
1134
+ var import_node_path5 = __toESM(require("path"));
855
1135
 
856
1136
  // src/parsers/optimizer.ts
857
1137
  var import_svgo = require("svgo");
@@ -887,7 +1167,7 @@ async function optimizeSvg(svgContent, config) {
887
1167
 
888
1168
  // src/utils/formatter.ts
889
1169
  var import_node_child_process = require("child_process");
890
- var import_node_path3 = __toESM(require("path"));
1170
+ var import_node_path4 = __toESM(require("path"));
891
1171
  var import_node_process2 = __toESM(require("process"));
892
1172
  var import_node_util = require("util");
893
1173
  init_helpers();
@@ -942,7 +1222,7 @@ async function detectFormatter() {
942
1222
  for (const tool of priority) {
943
1223
  const patterns = FORMATTER_PATTERNS[tool];
944
1224
  for (const pattern of patterns) {
945
- const configPath = import_node_path3.default.join(cwd, pattern);
1225
+ const configPath = import_node_path4.default.join(cwd, pattern);
946
1226
  if (await fileExists(configPath)) {
947
1227
  return tool;
948
1228
  }
@@ -1038,9 +1318,109 @@ async function generateIcons(config, dryRun = false) {
1038
1318
  }
1039
1319
  return stats;
1040
1320
  }
1041
- async function generateIconComponent(svgFile, config, dryRun = false) {
1042
- let svgContent = await readFile(svgFile);
1043
- const fileName = import_node_path4.default.basename(svgFile);
1321
+ async function generateIconsIncremental(config, cacheManager, dryRun = false) {
1322
+ const stats = {
1323
+ success: 0,
1324
+ failed: 0,
1325
+ total: 0,
1326
+ errors: []
1327
+ };
1328
+ try {
1329
+ if (!dryRun) {
1330
+ await ensureDir(config.output);
1331
+ }
1332
+ const svgFiles = await getSvgFiles(config.input);
1333
+ stats.total = svgFiles.length;
1334
+ if (svgFiles.length === 0) {
1335
+ console.warn(`No SVG files found in ${config.input}`);
1336
+ return stats;
1337
+ }
1338
+ if (cacheManager.hasConfigChanged(config)) {
1339
+ console.log("\u26A0\uFE0F Configuration changed, rebuilding all icons...");
1340
+ cacheManager.invalidateAll();
1341
+ }
1342
+ cacheManager.updateConfigHash(config);
1343
+ await cacheManager.cleanStaleEntries(svgFiles);
1344
+ if (config.generateOptions?.cleanOutput && !dryRun) {
1345
+ await cleanOutputDirectory(svgFiles, config);
1346
+ }
1347
+ await generateBaseComponent(config, dryRun);
1348
+ const strategy = getFrameworkStrategy(config.framework);
1349
+ const typescript = config.typescript ?? true;
1350
+ const fileExt = strategy.getComponentExtension(typescript);
1351
+ const filesToGenerate = [];
1352
+ const cachedFiles = [];
1353
+ for (const svgFile of svgFiles) {
1354
+ const fileName = import_node_path5.default.basename(svgFile);
1355
+ const componentName = getComponentName(
1356
+ fileName,
1357
+ config.prefix,
1358
+ config.suffix,
1359
+ config.componentNameTransform
1360
+ );
1361
+ const componentPath = import_node_path5.default.join(config.output, `${componentName}.${fileExt}`);
1362
+ const needsRegen = await cacheManager.needsRegeneration(
1363
+ svgFile,
1364
+ componentPath,
1365
+ config
1366
+ );
1367
+ if (needsRegen) {
1368
+ filesToGenerate.push(svgFile);
1369
+ } else {
1370
+ cachedFiles.push(svgFile);
1371
+ }
1372
+ }
1373
+ if (cachedFiles.length > 0) {
1374
+ console.log(`\u{1F4E6} Cache: ${cachedFiles.length} cached, ${filesToGenerate.length} to generate`);
1375
+ }
1376
+ for (const svgFile of filesToGenerate) {
1377
+ try {
1378
+ await generateIconComponent(svgFile, config, dryRun, cacheManager);
1379
+ stats.success++;
1380
+ } catch (error) {
1381
+ stats.failed++;
1382
+ stats.errors.push({
1383
+ file: svgFile,
1384
+ error: error.message
1385
+ });
1386
+ console.error(`Failed to generate ${svgFile}: ${error.message}`);
1387
+ }
1388
+ }
1389
+ stats.success += cachedFiles.length;
1390
+ if (config.generateOptions?.index) {
1391
+ await generateIndexFile(svgFiles, config, dryRun);
1392
+ }
1393
+ if (config.generateOptions?.preview && !dryRun) {
1394
+ await generatePreviewHtml(svgFiles, config);
1395
+ }
1396
+ if (!dryRun) {
1397
+ await cacheManager.save();
1398
+ const cacheStats = cacheManager.getStats();
1399
+ if (cacheStats.total > 0) {
1400
+ const hitRate = cacheManager.getHitRate();
1401
+ const timeSaved = (cacheStats.timeSaved / 1e3).toFixed(1);
1402
+ console.log(`\u26A1 Cache saved ~${timeSaved}s (${hitRate.toFixed(1)}% hit rate)`);
1403
+ }
1404
+ }
1405
+ if (config.format && !dryRun) {
1406
+ const formatResult = await formatOutput(config.output, config.format);
1407
+ if (formatResult.success && formatResult.tool) {
1408
+ console.log(`Formatted with ${formatResult.tool}`);
1409
+ } else if (formatResult.error) {
1410
+ console.warn(formatResult.error);
1411
+ }
1412
+ }
1413
+ if (config.hooks?.onComplete) {
1414
+ await config.hooks.onComplete(stats);
1415
+ }
1416
+ } catch (error) {
1417
+ throw new Error(`Generation failed: ${error.message}`);
1418
+ }
1419
+ return stats;
1420
+ }
1421
+ async function generateIconComponent(svgFile, config, dryRun = false, cacheManager) {
1422
+ let svgContent = await readFile3(svgFile);
1423
+ const fileName = import_node_path5.default.basename(svgFile);
1044
1424
  if (config.hooks?.beforeParse) {
1045
1425
  svgContent = await config.hooks.beforeParse(svgContent, fileName);
1046
1426
  }
@@ -1051,7 +1431,7 @@ async function generateIconComponent(svgFile, config, dryRun = false) {
1051
1431
  fileName,
1052
1432
  config.prefix,
1053
1433
  config.suffix,
1054
- config.transform
1434
+ config.componentNameTransform
1055
1435
  );
1056
1436
  const strategy = getFrameworkStrategy(config.framework);
1057
1437
  const typescript = config.typescript ?? true;
@@ -1067,22 +1447,32 @@ async function generateIconComponent(svgFile, config, dryRun = false) {
1067
1447
  code = await config.hooks.afterGenerate(code, componentName);
1068
1448
  }
1069
1449
  const fileExt = strategy.getComponentExtension(typescript);
1070
- const outputPath = import_node_path4.default.join(config.output, `${componentName}.${fileExt}`);
1450
+ const outputPath = import_node_path5.default.join(config.output, `${componentName}.${fileExt}`);
1071
1451
  if (dryRun) {
1072
1452
  console.log(` ${componentName}.${fileExt}`);
1073
1453
  } else {
1074
- await writeFile(outputPath, code);
1454
+ await writeFile2(outputPath, code);
1455
+ if (cacheManager) {
1456
+ const componentHash = hashContent(code);
1457
+ await cacheManager.updateEntry(
1458
+ svgFile,
1459
+ outputPath,
1460
+ componentName,
1461
+ componentHash,
1462
+ config
1463
+ );
1464
+ }
1075
1465
  }
1076
1466
  }
1077
1467
  async function generateBaseComponent(config, dryRun = false) {
1078
1468
  const typescript = config.typescript ?? true;
1079
1469
  const strategy = getFrameworkStrategy(config.framework);
1080
1470
  const { code, fileName } = strategy.generateBaseComponent(typescript);
1081
- const outputPath = import_node_path4.default.join(config.output, fileName);
1471
+ const outputPath = import_node_path5.default.join(config.output, fileName);
1082
1472
  if (dryRun) {
1083
1473
  console.log(` ${fileName}`);
1084
1474
  } else {
1085
- await writeFile(outputPath, code);
1475
+ await writeFile2(outputPath, code);
1086
1476
  }
1087
1477
  }
1088
1478
  async function generateIndexFile(svgFiles, config, dryRun = false) {
@@ -1093,12 +1483,12 @@ async function generateIndexFile(svgFiles, config, dryRun = false) {
1093
1483
  const usesDefaultExport = ["vue", "vue2", "svelte", "react", "preact"].includes(config.framework);
1094
1484
  const needsExtension = ["vue", "vue2", "svelte"].includes(config.framework);
1095
1485
  const exports2 = svgFiles.map((svgFile) => {
1096
- const fileName = import_node_path4.default.basename(svgFile);
1486
+ const fileName = import_node_path5.default.basename(svgFile);
1097
1487
  const componentName = getComponentName(
1098
1488
  fileName,
1099
1489
  config.prefix,
1100
1490
  config.suffix,
1101
- config.transform
1491
+ config.componentNameTransform
1102
1492
  );
1103
1493
  const importPath = needsExtension ? `./${componentName}.${componentExt}` : `./${componentName}`;
1104
1494
  if (usesDefaultExport) {
@@ -1107,27 +1497,27 @@ async function generateIndexFile(svgFiles, config, dryRun = false) {
1107
1497
  return `export { ${componentName} } from '${importPath}'`;
1108
1498
  }
1109
1499
  }).join("\n");
1110
- const indexPath = import_node_path4.default.join(config.output, `index.${ext}`);
1500
+ const indexPath = import_node_path5.default.join(config.output, `index.${ext}`);
1111
1501
  if (dryRun) {
1112
1502
  console.log(` index.${ext}`);
1113
1503
  } else {
1114
- await writeFile(indexPath, `${exports2}
1504
+ await writeFile2(indexPath, `${exports2}
1115
1505
  `);
1116
1506
  }
1117
1507
  }
1118
1508
  async function generatePreviewHtml(svgFiles, config) {
1119
1509
  const componentNames = svgFiles.map((svgFile) => {
1120
- const fileName = import_node_path4.default.basename(svgFile);
1510
+ const fileName = import_node_path5.default.basename(svgFile);
1121
1511
  return getComponentName(
1122
1512
  fileName,
1123
1513
  config.prefix,
1124
1514
  config.suffix,
1125
- config.transform
1515
+ config.componentNameTransform
1126
1516
  );
1127
1517
  });
1128
1518
  const svgContents = await Promise.all(
1129
1519
  svgFiles.map(async (svgFile) => {
1130
- return await readFile(svgFile);
1520
+ return await readFile3(svgFile);
1131
1521
  })
1132
1522
  );
1133
1523
  const html = `<!DOCTYPE html>
@@ -1289,8 +1679,8 @@ async function generatePreviewHtml(svgFiles, config) {
1289
1679
  </script>
1290
1680
  </body>
1291
1681
  </html>`;
1292
- const previewPath = import_node_path4.default.join(config.output, "preview.html");
1293
- await writeFile(previewPath, html);
1682
+ const previewPath = import_node_path5.default.join(config.output, "preview.html");
1683
+ await writeFile2(previewPath, html);
1294
1684
  }
1295
1685
  async function cleanOutputDirectory(svgFiles, config) {
1296
1686
  const { readdir, unlink } = await import("fs/promises");
@@ -1298,12 +1688,12 @@ async function cleanOutputDirectory(svgFiles, config) {
1298
1688
  const fileExt = strategy.getComponentExtension(config.typescript ?? true);
1299
1689
  const expectedComponents = new Set(
1300
1690
  svgFiles.map((svgFile) => {
1301
- const fileName = import_node_path4.default.basename(svgFile, ".svg");
1691
+ const fileName = import_node_path5.default.basename(svgFile, ".svg");
1302
1692
  const componentName = getComponentName(
1303
1693
  fileName,
1304
1694
  config.prefix,
1305
1695
  config.suffix,
1306
- config.transform
1696
+ config.componentNameTransform
1307
1697
  );
1308
1698
  return `${componentName}.${fileExt}`;
1309
1699
  })
@@ -1325,7 +1715,7 @@ async function cleanOutputDirectory(svgFiles, config) {
1325
1715
  continue;
1326
1716
  }
1327
1717
  if (!expectedComponents.has(file)) {
1328
- const filePath = import_node_path4.default.join(config.output, file);
1718
+ const filePath = import_node_path5.default.join(config.output, file);
1329
1719
  await unlink(filePath);
1330
1720
  console.log(`Deleted orphaned component: ${file}`);
1331
1721
  }
@@ -1357,9 +1747,19 @@ async function generate(options = {}) {
1357
1747
  ${import_chalk.default.bold("Files that would be generated:")}
1358
1748
  `);
1359
1749
  }
1750
+ const useIncremental = config.incremental?.enabled !== false && !options.force && !options.dryRun;
1751
+ let cacheManager = null;
1752
+ if (useIncremental) {
1753
+ const cacheDir = config.incremental?.cacheDir || ".vectify";
1754
+ cacheManager = new CacheManager(config.output, cacheDir);
1755
+ await cacheManager.load();
1756
+ }
1757
+ if (options.force) {
1758
+ spinner.info(import_chalk.default.yellow("Force mode - ignoring cache, regenerating all icons"));
1759
+ }
1360
1760
  const actionText = options.dryRun ? "Analyzing icons..." : "Generating icon components...";
1361
1761
  spinner.start(actionText);
1362
- const stats = await generateIcons(config, options.dryRun);
1762
+ const stats = cacheManager ? await generateIconsIncremental(config, cacheManager, options.dryRun) : await generateIcons(config, options.dryRun);
1363
1763
  if (stats.failed > 0) {
1364
1764
  spinner.warn(`${options.dryRun ? "Analyzed" : "Generated"} ${import_chalk.default.green(stats.success)} icons, ${import_chalk.default.red(stats.failed)} failed`);
1365
1765
  if (stats.errors.length > 0) {
@@ -1392,7 +1792,7 @@ ${import_chalk.default.bold("Output:")} ${import_chalk.default.cyan(config.outpu
1392
1792
  }
1393
1793
 
1394
1794
  // src/commands/init.ts
1395
- var import_node_path5 = __toESM(require("path"));
1795
+ var import_node_path6 = __toESM(require("path"));
1396
1796
  var import_node_process3 = __toESM(require("process"));
1397
1797
  var import_chalk2 = __toESM(require("chalk"));
1398
1798
  var import_inquirer = __toESM(require("inquirer"));
@@ -1422,8 +1822,8 @@ Note: Project root detected at ${import_chalk2.default.cyan(projectRoot)}`));
1422
1822
  }
1423
1823
  }
1424
1824
  ]);
1425
- const configPath = import_node_path5.default.resolve(projectRoot, pathAnswers.configPath);
1426
- const configDir = import_node_path5.default.dirname(configPath);
1825
+ const configPath = import_node_path6.default.resolve(projectRoot, pathAnswers.configPath);
1826
+ const configDir = import_node_path6.default.dirname(configPath);
1427
1827
  if (!options.force && await fileExists(configPath)) {
1428
1828
  const { overwrite } = await import_inquirer.default.prompt([
1429
1829
  {
@@ -1497,18 +1897,18 @@ Note: Project root detected at ${import_chalk2.default.cyan(projectRoot)}`));
1497
1897
  default: ""
1498
1898
  }
1499
1899
  ]);
1500
- const inputPath = import_node_path5.default.resolve(projectRoot, answers.input);
1501
- const outputPath = import_node_path5.default.resolve(projectRoot, answers.output);
1900
+ const inputPath = import_node_path6.default.resolve(projectRoot, answers.input);
1901
+ const outputPath = import_node_path6.default.resolve(projectRoot, answers.output);
1502
1902
  const spinner = (0, import_ora2.default)("Setting up directories...").start();
1503
1903
  await ensureDir(inputPath);
1504
1904
  spinner.text = `Created input directory: ${import_chalk2.default.cyan(answers.input)}`;
1505
1905
  await ensureDir(outputPath);
1506
1906
  spinner.succeed(`Created output directory: ${import_chalk2.default.cyan(answers.output)}`);
1507
- const relativeConfigDir = import_node_path5.default.relative(configDir, projectRoot) || ".";
1907
+ const relativeConfigDir = import_node_path6.default.relative(configDir, projectRoot) || ".";
1508
1908
  const finalFramework = answers.vueVersion || answers.framework;
1509
1909
  const configContent = generateConfigContent({ ...answers, framework: finalFramework }, relativeConfigDir);
1510
1910
  spinner.start("Creating config file...");
1511
- await writeFile(configPath, configContent);
1911
+ await writeFile2(configPath, configContent);
1512
1912
  spinner.succeed(`Config file created at ${import_chalk2.default.green(configPath)}`);
1513
1913
  console.log(`
1514
1914
  ${import_chalk2.default.bold("Next steps:")}`);
@@ -1548,10 +1948,93 @@ export default defineConfig({
1548
1948
  }
1549
1949
 
1550
1950
  // src/commands/watch.ts
1551
- var import_node_path6 = __toESM(require("path"));
1951
+ var import_node_path7 = __toESM(require("path"));
1552
1952
  var import_chalk3 = __toESM(require("chalk"));
1553
1953
  var import_chokidar = __toESM(require("chokidar"));
1554
1954
  var import_ora3 = __toESM(require("ora"));
1955
+
1956
+ // src/cache/svg-validator.ts
1957
+ var import_promises3 = require("fs/promises");
1958
+ var import_cheerio = require("cheerio");
1959
+ var MIN_SVG_SIZE = 20;
1960
+ var DRAWABLE_ELEMENTS = [
1961
+ "path",
1962
+ "circle",
1963
+ "rect",
1964
+ "ellipse",
1965
+ "line",
1966
+ "polyline",
1967
+ "polygon",
1968
+ "text",
1969
+ "image",
1970
+ "use"
1971
+ ];
1972
+ async function validateSVGFile(filePath) {
1973
+ try {
1974
+ const stats = await (0, import_promises3.stat)(filePath);
1975
+ if (stats.size < MIN_SVG_SIZE) {
1976
+ return {
1977
+ isValid: false,
1978
+ isEmpty: true,
1979
+ reason: "File is too small to be a valid SVG"
1980
+ };
1981
+ }
1982
+ const content = await (0, import_promises3.readFile)(filePath, "utf-8");
1983
+ if (!content.trim()) {
1984
+ return {
1985
+ isValid: false,
1986
+ isEmpty: true,
1987
+ reason: "File is empty"
1988
+ };
1989
+ }
1990
+ if (!content.includes("<svg")) {
1991
+ return {
1992
+ isValid: false,
1993
+ isEmpty: false,
1994
+ reason: "File does not contain <svg> tag"
1995
+ };
1996
+ }
1997
+ if (!hasDrawableContent(content)) {
1998
+ return {
1999
+ isValid: false,
2000
+ isEmpty: true,
2001
+ reason: "SVG has no drawable content"
2002
+ };
2003
+ }
2004
+ try {
2005
+ const $ = (0, import_cheerio.load)(content, { xmlMode: true });
2006
+ const svgElement = $("svg");
2007
+ if (svgElement.length === 0) {
2008
+ return {
2009
+ isValid: false,
2010
+ isEmpty: false,
2011
+ reason: "Failed to parse SVG element"
2012
+ };
2013
+ }
2014
+ return {
2015
+ isValid: true,
2016
+ isEmpty: false
2017
+ };
2018
+ } catch (parseError) {
2019
+ return {
2020
+ isValid: false,
2021
+ isEmpty: false,
2022
+ reason: `Failed to parse SVG: ${parseError}`
2023
+ };
2024
+ }
2025
+ } catch (error) {
2026
+ return {
2027
+ isValid: false,
2028
+ isEmpty: false,
2029
+ reason: `Failed to read file: ${error}`
2030
+ };
2031
+ }
2032
+ }
2033
+ function hasDrawableContent(content) {
2034
+ return DRAWABLE_ELEMENTS.some((element) => content.includes(`<${element}`));
2035
+ }
2036
+
2037
+ // src/commands/watch.ts
1555
2038
  async function watch(options = {}) {
1556
2039
  const spinner = (0, import_ora3.default)("Loading configuration...").start();
1557
2040
  try {
@@ -1567,16 +2050,24 @@ async function watch(options = {}) {
1567
2050
  }
1568
2051
  const config = await loadConfig(configPath);
1569
2052
  spinner.succeed(`Config loaded from ${import_chalk3.default.green(configPath)}`);
2053
+ const useIncremental = config.incremental?.enabled !== false;
2054
+ const cacheDir = config.incremental?.cacheDir || ".vectify";
2055
+ const cacheManager = useIncremental ? new CacheManager(config.output, cacheDir) : null;
2056
+ if (cacheManager) {
2057
+ await cacheManager.load();
2058
+ }
1570
2059
  spinner.start("Generating icon components...");
1571
- const initialStats = await generateIcons(config);
2060
+ const initialStats = useIncremental && cacheManager ? await generateIconsIncremental(config, cacheManager) : await generateIcons(config);
1572
2061
  spinner.succeed(`Generated ${import_chalk3.default.green(initialStats.success)} icon components`);
1573
- const watchPath = import_node_path6.default.join(config.input, "**/*.svg");
2062
+ const watchPath = import_node_path7.default.join(config.input, "**/*.svg");
1574
2063
  const debounce = config.watch?.debounce ?? 300;
1575
2064
  const ignore = config.watch?.ignore ?? ["**/node_modules/**", "**/.git/**"];
2065
+ const emptyFileRetryDelay = config.watch?.emptyFileRetryDelay ?? 2e3;
1576
2066
  console.log(import_chalk3.default.bold("\nWatching for changes..."));
1577
2067
  console.log(` ${import_chalk3.default.cyan(watchPath)}`);
1578
2068
  console.log(` ${import_chalk3.default.gray("Press Ctrl+C to stop")}
1579
2069
  `);
2070
+ const pendingChanges = /* @__PURE__ */ new Map();
1580
2071
  const debounceTimer = null;
1581
2072
  const watcher = import_chokidar.default.watch(watchPath, {
1582
2073
  ignored: ignore,
@@ -1584,12 +2075,12 @@ async function watch(options = {}) {
1584
2075
  ignoreInitial: true
1585
2076
  });
1586
2077
  watcher.on("add", (filePath) => {
1587
- handleChange("added", filePath, config, debounce, debounceTimer);
2078
+ handleChange("added", filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, debounceTimer);
1588
2079
  }).on("change", (filePath) => {
1589
- handleChange("changed", filePath, config, debounce, debounceTimer);
2080
+ handleChange("changed", filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, debounceTimer);
1590
2081
  }).on("unlink", (filePath) => {
1591
- console.log(import_chalk3.default.yellow(`SVG file removed: ${import_node_path6.default.basename(filePath)}`));
1592
- handleChange("removed", filePath, config, debounce, debounceTimer);
2082
+ console.log(import_chalk3.default.yellow(`SVG file removed: ${import_node_path7.default.basename(filePath)}`));
2083
+ handleChange("removed", filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, debounceTimer);
1593
2084
  }).on("error", (error) => {
1594
2085
  console.error(import_chalk3.default.red(`Watcher error: ${error.message}`));
1595
2086
  });
@@ -1606,23 +2097,51 @@ ${import_chalk3.default.yellow("Stopping watch mode...")}`);
1606
2097
  throw error;
1607
2098
  }
1608
2099
  }
1609
- function handleChange(event, filePath, config, debounce, timer) {
1610
- const fileName = import_node_path6.default.basename(filePath);
2100
+ function handleChange(event, filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, timer) {
2101
+ pendingChanges.set(filePath, event);
1611
2102
  if (timer) {
1612
2103
  clearTimeout(timer);
1613
2104
  }
1614
2105
  timer = setTimeout(async () => {
1615
- const spinner = (0, import_ora3.default)(`Regenerating icons...`).start();
2106
+ const changes = Array.from(pendingChanges.entries());
2107
+ pendingChanges.clear();
2108
+ const validChanges = [];
2109
+ const invalidFiles = [];
2110
+ for (const [file, changeEvent] of changes) {
2111
+ if (changeEvent === "removed") {
2112
+ validChanges.push([file, changeEvent]);
2113
+ continue;
2114
+ }
2115
+ const validation = await validateSVGFile(file);
2116
+ if (!validation.isValid) {
2117
+ if (validation.isEmpty) {
2118
+ console.log(import_chalk3.default.yellow(`\u23F3 Waiting for content: ${import_node_path7.default.basename(file)}`));
2119
+ setTimeout(() => {
2120
+ handleChange(changeEvent, file, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, timer);
2121
+ }, emptyFileRetryDelay);
2122
+ } else {
2123
+ console.error(import_chalk3.default.red(`\u274C Invalid SVG: ${import_node_path7.default.basename(file)} - ${validation.reason}`));
2124
+ invalidFiles.push(file);
2125
+ }
2126
+ continue;
2127
+ }
2128
+ validChanges.push([file, changeEvent]);
2129
+ }
2130
+ if (validChanges.length === 0) {
2131
+ return;
2132
+ }
2133
+ const spinner = (0, import_ora3.default)(`Processing ${validChanges.length} change(s)...`).start();
1616
2134
  try {
1617
- const stats = await generateIcons(config);
2135
+ const stats = cacheManager ? await generateIconsIncremental(config, cacheManager) : await generateIcons(config);
1618
2136
  if (stats.failed > 0) {
1619
2137
  spinner.warn(
1620
2138
  `Regenerated ${import_chalk3.default.green(stats.success)} icons, ${import_chalk3.default.red(stats.failed)} failed`
1621
2139
  );
1622
2140
  } else {
1623
- spinner.succeed(`Regenerated ${import_chalk3.default.green(stats.success)} icon components`);
2141
+ spinner.succeed(`\u2713 Updated ${import_chalk3.default.green(stats.success)} icon components`);
1624
2142
  }
1625
- console.log(import_chalk3.default.gray(` Triggered by: ${fileName} (${event})
2143
+ const triggerFiles = validChanges.map(([file, evt]) => `${import_node_path7.default.basename(file)} (${evt})`).join(", ");
2144
+ console.log(import_chalk3.default.gray(` Triggered by: ${triggerFiles}
1626
2145
  `));
1627
2146
  } catch (error) {
1628
2147
  spinner.fail("Regeneration failed");
@@ -1636,13 +2155,13 @@ var import_meta2 = {};
1636
2155
  function getPackageJson() {
1637
2156
  let pkgPath;
1638
2157
  if (typeof __dirname !== "undefined") {
1639
- pkgPath = (0, import_node_path7.join)(__dirname, "../package.json");
2158
+ pkgPath = (0, import_node_path8.join)(__dirname, "../package.json");
1640
2159
  } else {
1641
2160
  const __filename = (0, import_node_url2.fileURLToPath)(import_meta2.url);
1642
- const __dirname2 = (0, import_node_path7.dirname)(__filename);
1643
- pkgPath = (0, import_node_path7.join)(__dirname2, "../package.json");
2161
+ const __dirname2 = (0, import_node_path8.dirname)(__filename);
2162
+ pkgPath = (0, import_node_path8.join)(__dirname2, "../package.json");
1644
2163
  }
1645
- return JSON.parse((0, import_node_fs2.readFileSync)(pkgPath, "utf-8"));
2164
+ return JSON.parse((0, import_node_fs3.readFileSync)(pkgPath, "utf-8"));
1646
2165
  }
1647
2166
  var packageJson = getPackageJson();
1648
2167
  var program = new import_commander.Command();
@@ -1655,7 +2174,7 @@ program.command("init").description("Initialize a new configuration file").optio
1655
2174
  process.exit(1);
1656
2175
  }
1657
2176
  });
1658
- program.command("generate").description("Generate icon components from SVG files").option("-c, --config <path>", "Path to config file").option("--dry-run", "Preview what will be generated without writing files").action(async (options) => {
2177
+ program.command("generate").description("Generate icon components from SVG files").option("-c, --config <path>", "Path to config file").option("--dry-run", "Preview what will be generated without writing files").option("--force", "Force full regeneration, ignoring cache").action(async (options) => {
1659
2178
  try {
1660
2179
  await generate(options);
1661
2180
  } catch (error) {