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/README.md +12 -13
- package/README.zh-CN.md +12 -13
- package/dist/{chunk-CIKTK6HI.mjs → chunk-FS34P27H.mjs} +8 -0
- package/dist/{chunk-5RWOYQAG.mjs → chunk-QYE23M3E.mjs} +538 -18
- package/dist/cli.js +589 -70
- package/dist/cli.mjs +3 -3
- package/dist/{helpers-UPZEBRGK.mjs → helpers-ZOR3OD66.mjs} +1 -1
- package/dist/index.d.mts +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +582 -63
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
__require,
|
|
2
3
|
ensureDir,
|
|
3
4
|
fileExists,
|
|
4
5
|
findProjectRoot,
|
|
@@ -6,7 +7,7 @@ import {
|
|
|
6
7
|
getSvgFiles,
|
|
7
8
|
readFile,
|
|
8
9
|
writeFile
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-FS34P27H.mjs";
|
|
10
11
|
|
|
11
12
|
// src/parsers/svg-parser.ts
|
|
12
13
|
import * as cheerio from "cheerio";
|
|
@@ -698,7 +699,7 @@ async function loadConfig(configPath) {
|
|
|
698
699
|
return mergedConfig;
|
|
699
700
|
}
|
|
700
701
|
async function findConfig() {
|
|
701
|
-
const { fileExists: fileExists2 } = await import("./helpers-
|
|
702
|
+
const { fileExists: fileExists2 } = await import("./helpers-ZOR3OD66.mjs");
|
|
702
703
|
const configFiles = [
|
|
703
704
|
"vectify.config.ts",
|
|
704
705
|
"vectify.config.js"
|
|
@@ -715,6 +716,37 @@ async function findConfig() {
|
|
|
715
716
|
// src/generators/index.ts
|
|
716
717
|
import path4 from "path";
|
|
717
718
|
|
|
719
|
+
// src/cache/hash-utils.ts
|
|
720
|
+
import { createHash } from "crypto";
|
|
721
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
722
|
+
function hashContent(content) {
|
|
723
|
+
return createHash("sha256").update(content).digest("hex");
|
|
724
|
+
}
|
|
725
|
+
async function hashFile(filePath) {
|
|
726
|
+
try {
|
|
727
|
+
const content = await readFile2(filePath, "utf-8");
|
|
728
|
+
return hashContent(content);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
throw new Error(`Failed to hash file ${filePath}: ${error}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function hashConfig(config) {
|
|
734
|
+
const relevantConfig = {
|
|
735
|
+
framework: config.framework,
|
|
736
|
+
typescript: config.typescript,
|
|
737
|
+
keepColors: config.keepColors,
|
|
738
|
+
prefix: config.prefix,
|
|
739
|
+
suffix: config.suffix,
|
|
740
|
+
optimize: config.optimize,
|
|
741
|
+
svgoConfig: config.svgoConfig,
|
|
742
|
+
componentNameTransform: config.componentNameTransform?.toString()
|
|
743
|
+
};
|
|
744
|
+
return hashContent(JSON.stringify(relevantConfig));
|
|
745
|
+
}
|
|
746
|
+
function hashSvgoConfig(svgoConfig) {
|
|
747
|
+
return hashContent(JSON.stringify(svgoConfig || {}));
|
|
748
|
+
}
|
|
749
|
+
|
|
718
750
|
// src/parsers/optimizer.ts
|
|
719
751
|
import { optimize } from "svgo";
|
|
720
752
|
var DEFAULT_SVGO_CONFIG = {
|
|
@@ -898,7 +930,107 @@ async function generateIcons(config, dryRun = false) {
|
|
|
898
930
|
}
|
|
899
931
|
return stats;
|
|
900
932
|
}
|
|
901
|
-
async function
|
|
933
|
+
async function generateIconsIncremental(config, cacheManager, dryRun = false) {
|
|
934
|
+
const stats = {
|
|
935
|
+
success: 0,
|
|
936
|
+
failed: 0,
|
|
937
|
+
total: 0,
|
|
938
|
+
errors: []
|
|
939
|
+
};
|
|
940
|
+
try {
|
|
941
|
+
if (!dryRun) {
|
|
942
|
+
await ensureDir(config.output);
|
|
943
|
+
}
|
|
944
|
+
const svgFiles = await getSvgFiles(config.input);
|
|
945
|
+
stats.total = svgFiles.length;
|
|
946
|
+
if (svgFiles.length === 0) {
|
|
947
|
+
console.warn(`No SVG files found in ${config.input}`);
|
|
948
|
+
return stats;
|
|
949
|
+
}
|
|
950
|
+
if (cacheManager.hasConfigChanged(config)) {
|
|
951
|
+
console.log("\u26A0\uFE0F Configuration changed, rebuilding all icons...");
|
|
952
|
+
cacheManager.invalidateAll();
|
|
953
|
+
}
|
|
954
|
+
cacheManager.updateConfigHash(config);
|
|
955
|
+
await cacheManager.cleanStaleEntries(svgFiles);
|
|
956
|
+
if (config.generateOptions?.cleanOutput && !dryRun) {
|
|
957
|
+
await cleanOutputDirectory(svgFiles, config);
|
|
958
|
+
}
|
|
959
|
+
await generateBaseComponent(config, dryRun);
|
|
960
|
+
const strategy = getFrameworkStrategy(config.framework);
|
|
961
|
+
const typescript = config.typescript ?? true;
|
|
962
|
+
const fileExt = strategy.getComponentExtension(typescript);
|
|
963
|
+
const filesToGenerate = [];
|
|
964
|
+
const cachedFiles = [];
|
|
965
|
+
for (const svgFile of svgFiles) {
|
|
966
|
+
const fileName = path4.basename(svgFile);
|
|
967
|
+
const componentName = getComponentName(
|
|
968
|
+
fileName,
|
|
969
|
+
config.prefix,
|
|
970
|
+
config.suffix,
|
|
971
|
+
config.componentNameTransform
|
|
972
|
+
);
|
|
973
|
+
const componentPath = path4.join(config.output, `${componentName}.${fileExt}`);
|
|
974
|
+
const needsRegen = await cacheManager.needsRegeneration(
|
|
975
|
+
svgFile,
|
|
976
|
+
componentPath,
|
|
977
|
+
config
|
|
978
|
+
);
|
|
979
|
+
if (needsRegen) {
|
|
980
|
+
filesToGenerate.push(svgFile);
|
|
981
|
+
} else {
|
|
982
|
+
cachedFiles.push(svgFile);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (cachedFiles.length > 0) {
|
|
986
|
+
console.log(`\u{1F4E6} Cache: ${cachedFiles.length} cached, ${filesToGenerate.length} to generate`);
|
|
987
|
+
}
|
|
988
|
+
for (const svgFile of filesToGenerate) {
|
|
989
|
+
try {
|
|
990
|
+
await generateIconComponent(svgFile, config, dryRun, cacheManager);
|
|
991
|
+
stats.success++;
|
|
992
|
+
} catch (error) {
|
|
993
|
+
stats.failed++;
|
|
994
|
+
stats.errors.push({
|
|
995
|
+
file: svgFile,
|
|
996
|
+
error: error.message
|
|
997
|
+
});
|
|
998
|
+
console.error(`Failed to generate ${svgFile}: ${error.message}`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
stats.success += cachedFiles.length;
|
|
1002
|
+
if (config.generateOptions?.index) {
|
|
1003
|
+
await generateIndexFile(svgFiles, config, dryRun);
|
|
1004
|
+
}
|
|
1005
|
+
if (config.generateOptions?.preview && !dryRun) {
|
|
1006
|
+
await generatePreviewHtml(svgFiles, config);
|
|
1007
|
+
}
|
|
1008
|
+
if (!dryRun) {
|
|
1009
|
+
await cacheManager.save();
|
|
1010
|
+
const cacheStats = cacheManager.getStats();
|
|
1011
|
+
if (cacheStats.total > 0) {
|
|
1012
|
+
const hitRate = cacheManager.getHitRate();
|
|
1013
|
+
const timeSaved = (cacheStats.timeSaved / 1e3).toFixed(1);
|
|
1014
|
+
console.log(`\u26A1 Cache saved ~${timeSaved}s (${hitRate.toFixed(1)}% hit rate)`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (config.format && !dryRun) {
|
|
1018
|
+
const formatResult = await formatOutput(config.output, config.format);
|
|
1019
|
+
if (formatResult.success && formatResult.tool) {
|
|
1020
|
+
console.log(`Formatted with ${formatResult.tool}`);
|
|
1021
|
+
} else if (formatResult.error) {
|
|
1022
|
+
console.warn(formatResult.error);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (config.hooks?.onComplete) {
|
|
1026
|
+
await config.hooks.onComplete(stats);
|
|
1027
|
+
}
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
throw new Error(`Generation failed: ${error.message}`);
|
|
1030
|
+
}
|
|
1031
|
+
return stats;
|
|
1032
|
+
}
|
|
1033
|
+
async function generateIconComponent(svgFile, config, dryRun = false, cacheManager) {
|
|
902
1034
|
let svgContent = await readFile(svgFile);
|
|
903
1035
|
const fileName = path4.basename(svgFile);
|
|
904
1036
|
if (config.hooks?.beforeParse) {
|
|
@@ -911,7 +1043,7 @@ async function generateIconComponent(svgFile, config, dryRun = false) {
|
|
|
911
1043
|
fileName,
|
|
912
1044
|
config.prefix,
|
|
913
1045
|
config.suffix,
|
|
914
|
-
config.
|
|
1046
|
+
config.componentNameTransform
|
|
915
1047
|
);
|
|
916
1048
|
const strategy = getFrameworkStrategy(config.framework);
|
|
917
1049
|
const typescript = config.typescript ?? true;
|
|
@@ -932,6 +1064,16 @@ async function generateIconComponent(svgFile, config, dryRun = false) {
|
|
|
932
1064
|
console.log(` ${componentName}.${fileExt}`);
|
|
933
1065
|
} else {
|
|
934
1066
|
await writeFile(outputPath, code);
|
|
1067
|
+
if (cacheManager) {
|
|
1068
|
+
const componentHash = hashContent(code);
|
|
1069
|
+
await cacheManager.updateEntry(
|
|
1070
|
+
svgFile,
|
|
1071
|
+
outputPath,
|
|
1072
|
+
componentName,
|
|
1073
|
+
componentHash,
|
|
1074
|
+
config
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
935
1077
|
}
|
|
936
1078
|
}
|
|
937
1079
|
async function generateBaseComponent(config, dryRun = false) {
|
|
@@ -958,7 +1100,7 @@ async function generateIndexFile(svgFiles, config, dryRun = false) {
|
|
|
958
1100
|
fileName,
|
|
959
1101
|
config.prefix,
|
|
960
1102
|
config.suffix,
|
|
961
|
-
config.
|
|
1103
|
+
config.componentNameTransform
|
|
962
1104
|
);
|
|
963
1105
|
const importPath = needsExtension ? `./${componentName}.${componentExt}` : `./${componentName}`;
|
|
964
1106
|
if (usesDefaultExport) {
|
|
@@ -982,7 +1124,7 @@ async function generatePreviewHtml(svgFiles, config) {
|
|
|
982
1124
|
fileName,
|
|
983
1125
|
config.prefix,
|
|
984
1126
|
config.suffix,
|
|
985
|
-
config.
|
|
1127
|
+
config.componentNameTransform
|
|
986
1128
|
);
|
|
987
1129
|
});
|
|
988
1130
|
const svgContents = await Promise.all(
|
|
@@ -1163,7 +1305,7 @@ async function cleanOutputDirectory(svgFiles, config) {
|
|
|
1163
1305
|
fileName,
|
|
1164
1306
|
config.prefix,
|
|
1165
1307
|
config.suffix,
|
|
1166
|
-
config.
|
|
1308
|
+
config.componentNameTransform
|
|
1167
1309
|
);
|
|
1168
1310
|
return `${componentName}.${fileExt}`;
|
|
1169
1311
|
})
|
|
@@ -1198,6 +1340,255 @@ async function cleanOutputDirectory(svgFiles, config) {
|
|
|
1198
1340
|
// src/commands/generate.ts
|
|
1199
1341
|
import chalk from "chalk";
|
|
1200
1342
|
import ora from "ora";
|
|
1343
|
+
|
|
1344
|
+
// src/cache/cache-manager.ts
|
|
1345
|
+
import { existsSync } from "fs";
|
|
1346
|
+
import { mkdir, readFile as readFile3, stat, writeFile as writeFile2 } from "fs/promises";
|
|
1347
|
+
import { dirname, join } from "path";
|
|
1348
|
+
var CACHE_VERSION = "1.0.0";
|
|
1349
|
+
var CacheManager = class {
|
|
1350
|
+
constructor(outputDir, cacheDir = ".vectify") {
|
|
1351
|
+
this.cache = {
|
|
1352
|
+
version: CACHE_VERSION,
|
|
1353
|
+
configHash: "",
|
|
1354
|
+
entries: {},
|
|
1355
|
+
baseComponentHash: ""
|
|
1356
|
+
};
|
|
1357
|
+
this.cacheFilePath = join(outputDir, cacheDir, "cache.json");
|
|
1358
|
+
this.stats = {
|
|
1359
|
+
hits: 0,
|
|
1360
|
+
misses: 0,
|
|
1361
|
+
total: 0,
|
|
1362
|
+
timeSaved: 0
|
|
1363
|
+
};
|
|
1364
|
+
this.isDirty = false;
|
|
1365
|
+
process.on("beforeExit", () => {
|
|
1366
|
+
if (this.isDirty) {
|
|
1367
|
+
this.saveSync();
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Load cache from disk
|
|
1373
|
+
*/
|
|
1374
|
+
async load() {
|
|
1375
|
+
try {
|
|
1376
|
+
if (!existsSync(this.cacheFilePath)) {
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
const content = await readFile3(this.cacheFilePath, "utf-8");
|
|
1380
|
+
const loadedCache = JSON.parse(content);
|
|
1381
|
+
if (loadedCache.version !== CACHE_VERSION) {
|
|
1382
|
+
console.log("\u26A0\uFE0F Cache version mismatch, rebuilding cache...");
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
this.cache = loadedCache;
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
console.warn("\u26A0\uFE0F Failed to load cache, starting fresh:", error);
|
|
1388
|
+
this.cache = {
|
|
1389
|
+
version: CACHE_VERSION,
|
|
1390
|
+
configHash: "",
|
|
1391
|
+
entries: {},
|
|
1392
|
+
baseComponentHash: ""
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Save cache to disk (atomic write)
|
|
1398
|
+
*/
|
|
1399
|
+
async save() {
|
|
1400
|
+
try {
|
|
1401
|
+
const cacheDir = dirname(this.cacheFilePath);
|
|
1402
|
+
await mkdir(cacheDir, { recursive: true });
|
|
1403
|
+
const tempPath = `${this.cacheFilePath}.tmp`;
|
|
1404
|
+
await writeFile2(tempPath, JSON.stringify(this.cache, null, 2), "utf-8");
|
|
1405
|
+
await writeFile2(this.cacheFilePath, JSON.stringify(this.cache, null, 2), "utf-8");
|
|
1406
|
+
this.isDirty = false;
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
console.warn("\u26A0\uFE0F Failed to save cache:", error);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Synchronous save for process exit
|
|
1413
|
+
*/
|
|
1414
|
+
saveSync() {
|
|
1415
|
+
try {
|
|
1416
|
+
const fs2 = __require("fs");
|
|
1417
|
+
const cacheDir = dirname(this.cacheFilePath);
|
|
1418
|
+
if (!fs2.existsSync(cacheDir)) {
|
|
1419
|
+
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
1420
|
+
}
|
|
1421
|
+
fs2.writeFileSync(this.cacheFilePath, JSON.stringify(this.cache, null, 2), "utf-8");
|
|
1422
|
+
this.isDirty = false;
|
|
1423
|
+
} catch {
|
|
1424
|
+
console.warn("\u26A0\uFE0F Failed to save cache on exit");
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Check if a file needs regeneration
|
|
1429
|
+
*/
|
|
1430
|
+
async needsRegeneration(svgPath, componentPath, config) {
|
|
1431
|
+
this.stats.total++;
|
|
1432
|
+
if (!existsSync(componentPath)) {
|
|
1433
|
+
this.stats.misses++;
|
|
1434
|
+
return true;
|
|
1435
|
+
}
|
|
1436
|
+
const entry = this.cache.entries[svgPath];
|
|
1437
|
+
if (!entry) {
|
|
1438
|
+
this.stats.misses++;
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
try {
|
|
1442
|
+
const stats = await stat(svgPath);
|
|
1443
|
+
const currentMtime = stats.mtimeMs;
|
|
1444
|
+
if (currentMtime === entry.svgMtime) {
|
|
1445
|
+
if (!this.isConfigMatching(entry, config)) {
|
|
1446
|
+
this.stats.misses++;
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
this.stats.hits++;
|
|
1450
|
+
this.stats.timeSaved += 50;
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
const currentHash = await hashFile(svgPath);
|
|
1454
|
+
if (currentHash === entry.svgHash) {
|
|
1455
|
+
entry.svgMtime = currentMtime;
|
|
1456
|
+
this.isDirty = true;
|
|
1457
|
+
this.stats.hits++;
|
|
1458
|
+
this.stats.timeSaved += 50;
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
this.stats.misses++;
|
|
1462
|
+
return true;
|
|
1463
|
+
} catch {
|
|
1464
|
+
this.stats.misses++;
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Check if config matches cache entry
|
|
1470
|
+
*/
|
|
1471
|
+
isConfigMatching(entry, config) {
|
|
1472
|
+
const snapshot = entry.configSnapshot;
|
|
1473
|
+
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);
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Update cache entry after successful generation
|
|
1477
|
+
*/
|
|
1478
|
+
async updateEntry(svgPath, componentPath, componentName, componentHash, config) {
|
|
1479
|
+
try {
|
|
1480
|
+
const svgHash = await hashFile(svgPath);
|
|
1481
|
+
const stats = await stat(svgPath);
|
|
1482
|
+
this.cache.entries[svgPath] = {
|
|
1483
|
+
svgPath,
|
|
1484
|
+
svgHash,
|
|
1485
|
+
svgMtime: stats.mtimeMs,
|
|
1486
|
+
componentName,
|
|
1487
|
+
componentPath,
|
|
1488
|
+
componentHash,
|
|
1489
|
+
configSnapshot: {
|
|
1490
|
+
framework: config.framework,
|
|
1491
|
+
typescript: config.typescript ?? true,
|
|
1492
|
+
keepColors: config.keepColors ?? false,
|
|
1493
|
+
prefix: config.prefix || "",
|
|
1494
|
+
suffix: config.suffix || "",
|
|
1495
|
+
optimize: config.optimize ?? true,
|
|
1496
|
+
svgoConfigHash: hashSvgoConfig(config.svgoConfig)
|
|
1497
|
+
},
|
|
1498
|
+
generatedAt: Date.now()
|
|
1499
|
+
};
|
|
1500
|
+
this.isDirty = true;
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
console.warn(`\u26A0\uFE0F Failed to update cache entry for ${svgPath}:`, error);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Remove cache entry
|
|
1507
|
+
*/
|
|
1508
|
+
removeEntry(svgPath) {
|
|
1509
|
+
if (this.cache.entries[svgPath]) {
|
|
1510
|
+
delete this.cache.entries[svgPath];
|
|
1511
|
+
this.isDirty = true;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Update config hash
|
|
1516
|
+
*/
|
|
1517
|
+
updateConfigHash(config) {
|
|
1518
|
+
const newHash = hashConfig(config);
|
|
1519
|
+
if (this.cache.configHash !== newHash) {
|
|
1520
|
+
this.cache.configHash = newHash;
|
|
1521
|
+
this.isDirty = true;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Update base component hash
|
|
1526
|
+
*/
|
|
1527
|
+
updateBaseComponentHash(hash) {
|
|
1528
|
+
if (this.cache.baseComponentHash !== hash) {
|
|
1529
|
+
this.cache.baseComponentHash = hash;
|
|
1530
|
+
this.isDirty = true;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Check if config has changed (invalidates all cache)
|
|
1535
|
+
*/
|
|
1536
|
+
hasConfigChanged(config) {
|
|
1537
|
+
const currentHash = hashConfig(config);
|
|
1538
|
+
return this.cache.configHash !== "" && this.cache.configHash !== currentHash;
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Invalidate all cache entries
|
|
1542
|
+
*/
|
|
1543
|
+
invalidateAll() {
|
|
1544
|
+
this.cache.entries = {};
|
|
1545
|
+
this.isDirty = true;
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Clean stale entries (files that no longer exist)
|
|
1549
|
+
*/
|
|
1550
|
+
async cleanStaleEntries(existingSvgPaths) {
|
|
1551
|
+
const existingSet = new Set(existingSvgPaths);
|
|
1552
|
+
let cleaned = 0;
|
|
1553
|
+
for (const svgPath of Object.keys(this.cache.entries)) {
|
|
1554
|
+
if (!existingSet.has(svgPath)) {
|
|
1555
|
+
delete this.cache.entries[svgPath];
|
|
1556
|
+
cleaned++;
|
|
1557
|
+
this.isDirty = true;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (cleaned > 0) {
|
|
1561
|
+
console.log(`\u{1F9F9} Cleaned ${cleaned} stale cache entries`);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Get cache statistics
|
|
1566
|
+
*/
|
|
1567
|
+
getStats() {
|
|
1568
|
+
return { ...this.stats };
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Reset statistics
|
|
1572
|
+
*/
|
|
1573
|
+
resetStats() {
|
|
1574
|
+
this.stats = {
|
|
1575
|
+
hits: 0,
|
|
1576
|
+
misses: 0,
|
|
1577
|
+
total: 0,
|
|
1578
|
+
timeSaved: 0
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Get cache hit rate
|
|
1583
|
+
*/
|
|
1584
|
+
getHitRate() {
|
|
1585
|
+
if (this.stats.total === 0)
|
|
1586
|
+
return 0;
|
|
1587
|
+
return this.stats.hits / this.stats.total * 100;
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
// src/commands/generate.ts
|
|
1201
1592
|
async function generate(options = {}) {
|
|
1202
1593
|
const spinner = ora("Loading configuration...").start();
|
|
1203
1594
|
try {
|
|
@@ -1219,9 +1610,19 @@ async function generate(options = {}) {
|
|
|
1219
1610
|
${chalk.bold("Files that would be generated:")}
|
|
1220
1611
|
`);
|
|
1221
1612
|
}
|
|
1613
|
+
const useIncremental = config.incremental?.enabled !== false && !options.force && !options.dryRun;
|
|
1614
|
+
let cacheManager = null;
|
|
1615
|
+
if (useIncremental) {
|
|
1616
|
+
const cacheDir = config.incremental?.cacheDir || ".vectify";
|
|
1617
|
+
cacheManager = new CacheManager(config.output, cacheDir);
|
|
1618
|
+
await cacheManager.load();
|
|
1619
|
+
}
|
|
1620
|
+
if (options.force) {
|
|
1621
|
+
spinner.info(chalk.yellow("Force mode - ignoring cache, regenerating all icons"));
|
|
1622
|
+
}
|
|
1222
1623
|
const actionText = options.dryRun ? "Analyzing icons..." : "Generating icon components...";
|
|
1223
1624
|
spinner.start(actionText);
|
|
1224
|
-
const stats = await generateIcons(config, options.dryRun);
|
|
1625
|
+
const stats = cacheManager ? await generateIconsIncremental(config, cacheManager, options.dryRun) : await generateIcons(config, options.dryRun);
|
|
1225
1626
|
if (stats.failed > 0) {
|
|
1226
1627
|
spinner.warn(`${options.dryRun ? "Analyzed" : "Generated"} ${chalk.green(stats.success)} icons, ${chalk.red(stats.failed)} failed`);
|
|
1227
1628
|
if (stats.errors.length > 0) {
|
|
@@ -1413,6 +1814,89 @@ import path6 from "path";
|
|
|
1413
1814
|
import chalk3 from "chalk";
|
|
1414
1815
|
import chokidar from "chokidar";
|
|
1415
1816
|
import ora3 from "ora";
|
|
1817
|
+
|
|
1818
|
+
// src/cache/svg-validator.ts
|
|
1819
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1820
|
+
import { load as load2 } from "cheerio";
|
|
1821
|
+
var MIN_SVG_SIZE = 20;
|
|
1822
|
+
var DRAWABLE_ELEMENTS = [
|
|
1823
|
+
"path",
|
|
1824
|
+
"circle",
|
|
1825
|
+
"rect",
|
|
1826
|
+
"ellipse",
|
|
1827
|
+
"line",
|
|
1828
|
+
"polyline",
|
|
1829
|
+
"polygon",
|
|
1830
|
+
"text",
|
|
1831
|
+
"image",
|
|
1832
|
+
"use"
|
|
1833
|
+
];
|
|
1834
|
+
async function validateSVGFile(filePath) {
|
|
1835
|
+
try {
|
|
1836
|
+
const stats = await stat2(filePath);
|
|
1837
|
+
if (stats.size < MIN_SVG_SIZE) {
|
|
1838
|
+
return {
|
|
1839
|
+
isValid: false,
|
|
1840
|
+
isEmpty: true,
|
|
1841
|
+
reason: "File is too small to be a valid SVG"
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
const content = await readFile4(filePath, "utf-8");
|
|
1845
|
+
if (!content.trim()) {
|
|
1846
|
+
return {
|
|
1847
|
+
isValid: false,
|
|
1848
|
+
isEmpty: true,
|
|
1849
|
+
reason: "File is empty"
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
if (!content.includes("<svg")) {
|
|
1853
|
+
return {
|
|
1854
|
+
isValid: false,
|
|
1855
|
+
isEmpty: false,
|
|
1856
|
+
reason: "File does not contain <svg> tag"
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
if (!hasDrawableContent(content)) {
|
|
1860
|
+
return {
|
|
1861
|
+
isValid: false,
|
|
1862
|
+
isEmpty: true,
|
|
1863
|
+
reason: "SVG has no drawable content"
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
try {
|
|
1867
|
+
const $ = load2(content, { xmlMode: true });
|
|
1868
|
+
const svgElement = $("svg");
|
|
1869
|
+
if (svgElement.length === 0) {
|
|
1870
|
+
return {
|
|
1871
|
+
isValid: false,
|
|
1872
|
+
isEmpty: false,
|
|
1873
|
+
reason: "Failed to parse SVG element"
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
return {
|
|
1877
|
+
isValid: true,
|
|
1878
|
+
isEmpty: false
|
|
1879
|
+
};
|
|
1880
|
+
} catch (parseError) {
|
|
1881
|
+
return {
|
|
1882
|
+
isValid: false,
|
|
1883
|
+
isEmpty: false,
|
|
1884
|
+
reason: `Failed to parse SVG: ${parseError}`
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
return {
|
|
1889
|
+
isValid: false,
|
|
1890
|
+
isEmpty: false,
|
|
1891
|
+
reason: `Failed to read file: ${error}`
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
function hasDrawableContent(content) {
|
|
1896
|
+
return DRAWABLE_ELEMENTS.some((element) => content.includes(`<${element}`));
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/commands/watch.ts
|
|
1416
1900
|
async function watch(options = {}) {
|
|
1417
1901
|
const spinner = ora3("Loading configuration...").start();
|
|
1418
1902
|
try {
|
|
@@ -1428,16 +1912,24 @@ async function watch(options = {}) {
|
|
|
1428
1912
|
}
|
|
1429
1913
|
const config = await loadConfig(configPath);
|
|
1430
1914
|
spinner.succeed(`Config loaded from ${chalk3.green(configPath)}`);
|
|
1915
|
+
const useIncremental = config.incremental?.enabled !== false;
|
|
1916
|
+
const cacheDir = config.incremental?.cacheDir || ".vectify";
|
|
1917
|
+
const cacheManager = useIncremental ? new CacheManager(config.output, cacheDir) : null;
|
|
1918
|
+
if (cacheManager) {
|
|
1919
|
+
await cacheManager.load();
|
|
1920
|
+
}
|
|
1431
1921
|
spinner.start("Generating icon components...");
|
|
1432
|
-
const initialStats = await generateIcons(config);
|
|
1922
|
+
const initialStats = useIncremental && cacheManager ? await generateIconsIncremental(config, cacheManager) : await generateIcons(config);
|
|
1433
1923
|
spinner.succeed(`Generated ${chalk3.green(initialStats.success)} icon components`);
|
|
1434
1924
|
const watchPath = path6.join(config.input, "**/*.svg");
|
|
1435
1925
|
const debounce = config.watch?.debounce ?? 300;
|
|
1436
1926
|
const ignore = config.watch?.ignore ?? ["**/node_modules/**", "**/.git/**"];
|
|
1927
|
+
const emptyFileRetryDelay = config.watch?.emptyFileRetryDelay ?? 2e3;
|
|
1437
1928
|
console.log(chalk3.bold("\nWatching for changes..."));
|
|
1438
1929
|
console.log(` ${chalk3.cyan(watchPath)}`);
|
|
1439
1930
|
console.log(` ${chalk3.gray("Press Ctrl+C to stop")}
|
|
1440
1931
|
`);
|
|
1932
|
+
const pendingChanges = /* @__PURE__ */ new Map();
|
|
1441
1933
|
const debounceTimer = null;
|
|
1442
1934
|
const watcher = chokidar.watch(watchPath, {
|
|
1443
1935
|
ignored: ignore,
|
|
@@ -1445,12 +1937,12 @@ async function watch(options = {}) {
|
|
|
1445
1937
|
ignoreInitial: true
|
|
1446
1938
|
});
|
|
1447
1939
|
watcher.on("add", (filePath) => {
|
|
1448
|
-
handleChange("added", filePath, config, debounce, debounceTimer);
|
|
1940
|
+
handleChange("added", filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, debounceTimer);
|
|
1449
1941
|
}).on("change", (filePath) => {
|
|
1450
|
-
handleChange("changed", filePath, config, debounce, debounceTimer);
|
|
1942
|
+
handleChange("changed", filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, debounceTimer);
|
|
1451
1943
|
}).on("unlink", (filePath) => {
|
|
1452
1944
|
console.log(chalk3.yellow(`SVG file removed: ${path6.basename(filePath)}`));
|
|
1453
|
-
handleChange("removed", filePath, config, debounce, debounceTimer);
|
|
1945
|
+
handleChange("removed", filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, debounceTimer);
|
|
1454
1946
|
}).on("error", (error) => {
|
|
1455
1947
|
console.error(chalk3.red(`Watcher error: ${error.message}`));
|
|
1456
1948
|
});
|
|
@@ -1467,23 +1959,51 @@ ${chalk3.yellow("Stopping watch mode...")}`);
|
|
|
1467
1959
|
throw error;
|
|
1468
1960
|
}
|
|
1469
1961
|
}
|
|
1470
|
-
function handleChange(event, filePath, config, debounce, timer) {
|
|
1471
|
-
|
|
1962
|
+
function handleChange(event, filePath, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, timer) {
|
|
1963
|
+
pendingChanges.set(filePath, event);
|
|
1472
1964
|
if (timer) {
|
|
1473
1965
|
clearTimeout(timer);
|
|
1474
1966
|
}
|
|
1475
1967
|
timer = setTimeout(async () => {
|
|
1476
|
-
const
|
|
1968
|
+
const changes = Array.from(pendingChanges.entries());
|
|
1969
|
+
pendingChanges.clear();
|
|
1970
|
+
const validChanges = [];
|
|
1971
|
+
const invalidFiles = [];
|
|
1972
|
+
for (const [file, changeEvent] of changes) {
|
|
1973
|
+
if (changeEvent === "removed") {
|
|
1974
|
+
validChanges.push([file, changeEvent]);
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
const validation = await validateSVGFile(file);
|
|
1978
|
+
if (!validation.isValid) {
|
|
1979
|
+
if (validation.isEmpty) {
|
|
1980
|
+
console.log(chalk3.yellow(`\u23F3 Waiting for content: ${path6.basename(file)}`));
|
|
1981
|
+
setTimeout(() => {
|
|
1982
|
+
handleChange(changeEvent, file, config, cacheManager, debounce, emptyFileRetryDelay, pendingChanges, timer);
|
|
1983
|
+
}, emptyFileRetryDelay);
|
|
1984
|
+
} else {
|
|
1985
|
+
console.error(chalk3.red(`\u274C Invalid SVG: ${path6.basename(file)} - ${validation.reason}`));
|
|
1986
|
+
invalidFiles.push(file);
|
|
1987
|
+
}
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
validChanges.push([file, changeEvent]);
|
|
1991
|
+
}
|
|
1992
|
+
if (validChanges.length === 0) {
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const spinner = ora3(`Processing ${validChanges.length} change(s)...`).start();
|
|
1477
1996
|
try {
|
|
1478
|
-
const stats = await generateIcons(config);
|
|
1997
|
+
const stats = cacheManager ? await generateIconsIncremental(config, cacheManager) : await generateIcons(config);
|
|
1479
1998
|
if (stats.failed > 0) {
|
|
1480
1999
|
spinner.warn(
|
|
1481
2000
|
`Regenerated ${chalk3.green(stats.success)} icons, ${chalk3.red(stats.failed)} failed`
|
|
1482
2001
|
);
|
|
1483
2002
|
} else {
|
|
1484
|
-
spinner.succeed(
|
|
2003
|
+
spinner.succeed(`\u2713 Updated ${chalk3.green(stats.success)} icon components`);
|
|
1485
2004
|
}
|
|
1486
|
-
|
|
2005
|
+
const triggerFiles = validChanges.map(([file, evt]) => `${path6.basename(file)} (${evt})`).join(", ");
|
|
2006
|
+
console.log(chalk3.gray(` Triggered by: ${triggerFiles}
|
|
1487
2007
|
`));
|
|
1488
2008
|
} catch (error) {
|
|
1489
2009
|
spinner.fail("Regeneration failed");
|