crapifyme
Version:
Ultra-fast developer productivity CLI tools - remove comments, logs, and more
527 lines • 21.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SvgProcessor = void 0;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const svgo_1 = require("svgo");
const fast_glob_1 = __importDefault(require("fast-glob"));
const chalk_1 = __importDefault(require("chalk"));
const cliProgress = __importStar(require("cli-progress"));
const ora_1 = __importDefault(require("ora"));
const is_svg_1 = __importDefault(require("is-svg"));
const types_1 = require("./types");
const shared_1 = require("../../shared");
class SvgProcessor {
constructor(logger) {
this.logger = logger;
}
async processSvgFiles(target, options) {
const startTime = Date.now();
const stats = {
filesProcessed: 0,
filesSkipped: 0,
bytesProcessed: 0,
bytesOriginal: 0,
bytesOptimized: 0,
bytesSaved: 0,
avgCompressionRatio: 0,
operationsCompleted: 0,
processingTime: 0,
errors: [],
warnings: []
};
const results = [];
const skippedFiles = [];
const failedFiles = [];
try {
await this.performSafetyChecks(options);
const files = await this.findSvgFiles(target, options);
if (files.length === 0) {
this.logger.warn('No SVG files found to process');
return { results, stats, skippedFiles, failedFiles };
}
const config = (0, types_1.createProcessingConfig)(options);
if (!options.quiet && files.length > 1) {
this.setupProgressTracking(files.length, options);
}
if (options.parallel && files.length > 1) {
await this.processFilesParallel(files, config, options, results, skippedFiles, failedFiles, stats);
}
else {
await this.processFilesSequential(files, config, options, results, skippedFiles, failedFiles, stats);
}
this.cleanupProgressTracking();
this.calculateFinalStats(stats, startTime);
if (options.report) {
await this.generateReport(results, stats, options);
}
return { results, stats, skippedFiles, failedFiles };
}
catch (error) {
this.cleanupProgressTracking();
throw error;
}
}
async processSvgCode(svgContent, options) {
const startTime = Date.now();
try {
const originalSize = Buffer.byteLength(svgContent, 'utf8');
const config = (0, types_1.createProcessingConfig)(options);
if (config.validateInput) {
const validation = this.validateSvg(svgContent, 'direct-input');
if (!validation.isValid) {
throw new Error(`Invalid SVG: ${validation.errors.join(', ')}`);
}
}
const svgoConfig = this.createSvgoConfig(config, options);
const result = (0, svgo_1.optimize)(svgContent, svgoConfig);
if ('error' in result) {
throw new Error(`SVGO optimization failed: ${result.error}`);
}
const optimizedContent = result.data;
const optimizedSize = Buffer.byteLength(optimizedContent, 'utf8');
if (config.validateOutput) {
const validation = this.validateSvg(optimizedContent, 'direct-output');
if (!validation.isValid) {
throw new Error(`Output validation failed: ${validation.errors.join(', ')}`);
}
}
const bytesSaved = originalSize - optimizedSize;
const compressionRatio = originalSize / optimizedSize;
const processingTime = Date.now() - startTime;
return {
inputPath: 'direct-input',
outputPath: 'stdout',
originalSize,
optimizedSize,
compressionRatio,
bytesSaved,
pluginsApplied: this.getAppliedPlugins(svgoConfig),
processingTime,
originalContent: svgContent,
optimizedContent
};
}
catch (error) {
throw new Error(`Failed to process SVG code: ${error instanceof Error ? error.message : String(error)}`);
}
}
async performSafetyChecks(options) {
if (!options.force && !options.dryRun) {
const vcs = (0, shared_1.detectVersionControl)();
if (!vcs.detected) {
throw new Error('No version control system detected. Use --force to proceed without VCS, or initialize git/svn/hg/bzr first.');
}
}
if (options.outputDir) {
const outputPath = (0, shared_1.resolvePath)(options.outputDir);
try {
await promises_1.default.access(outputPath);
}
catch {
if (!options.dryRun) {
await promises_1.default.mkdir(outputPath, { recursive: true });
}
}
}
if (options.config) {
const configPath = (0, shared_1.resolvePath)(options.config);
try {
await promises_1.default.access(configPath);
}
catch {
throw new Error(`Configuration file not found: ${configPath}`);
}
}
}
async findSvgFiles(target, options) {
const targetPath = (0, shared_1.resolvePath)(target);
let patterns = [];
try {
const stat = await promises_1.default.stat(targetPath);
if (stat.isFile()) {
if (!(0, types_1.isSupportedSvgExtension)(path_1.default.extname(targetPath).slice(1))) {
throw new Error(`Unsupported file type: ${path_1.default.extname(targetPath)}`);
}
return [targetPath];
}
else if (stat.isDirectory()) {
const extensions = options.extensions
? typeof options.extensions === 'string'
? options.extensions.split(',')
: options.extensions
: [...types_1.DEFAULT_SVG_EXTENSIONS];
if (options.glob) {
patterns.push(path_1.default.join(targetPath, options.glob));
}
else {
if (extensions.length === 1) {
patterns.push(path_1.default.join(targetPath, `**/*.${extensions[0]}`));
}
else {
patterns.push(path_1.default.join(targetPath, `**/*.{${extensions.join(',')}}`));
}
}
}
}
catch {
patterns.push(targetPath);
}
const files = await (0, fast_glob_1.default)(patterns, {
ignore: [
'**/node_modules/**',
'**/.git/**',
'**/*.min.svg',
'**/*.optimized.svg',
'**/*.original.svg',
...(options.exclude || [])
],
absolute: true,
onlyFiles: true
});
return files.filter((file) => (0, types_1.isSupportedSvgExtension)(path_1.default.extname(file).slice(1)));
}
setupProgressTracking(totalFiles, options) {
if (options.verbose) {
this.progressBar = new cliProgress.SingleBar({
format: chalk_1.default.cyan('Optimizing SVGs') +
' [{bar}] {percentage}% | {value}/{total} files | ETA: {eta}s',
barCompleteChar: '█',
barIncompleteChar: '░',
hideCursor: true
}, cliProgress.Presets.shades_classic);
this.progressBar.start(totalFiles, 0);
}
else {
this.spinner = (0, ora_1.default)({
text: `Optimizing ${totalFiles} SVG files...`,
color: 'cyan'
}).start();
}
}
updateProgress(completed, total) {
if (this.progressBar) {
this.progressBar.update(completed);
}
else if (this.spinner) {
this.spinner.text = `Optimizing SVG files... ${completed}/${total}`;
}
}
cleanupProgressTracking() {
if (this.progressBar) {
this.progressBar.stop();
this.progressBar = undefined;
}
if (this.spinner) {
this.spinner.stop();
this.spinner = undefined;
}
}
async processFilesSequential(files, config, options, results, skippedFiles, failedFiles, stats) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
try {
const result = await this.processSingleFile(file, config, options);
if (result) {
results.push(result);
this.updateStats(stats, result);
}
else {
skippedFiles.push(file);
stats.filesSkipped++;
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
failedFiles.push({ file, error: errorMessage });
stats.errors.push({ file, error: errorMessage });
}
this.updateProgress(i + 1, files.length);
}
}
async processFilesParallel(files, config, options, results, skippedFiles, failedFiles, stats) {
const concurrency = Math.min(options.maxConcurrency || 4, files.length);
const chunks = this.chunkArray(files, Math.ceil(files.length / concurrency));
let completed = 0;
const processChunk = async (chunk) => {
for (const file of chunk) {
try {
const result = await this.processSingleFile(file, config, options);
if (result) {
results.push(result);
this.updateStats(stats, result);
}
else {
skippedFiles.push(file);
stats.filesSkipped++;
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
failedFiles.push({ file, error: errorMessage });
stats.errors.push({ file, error: errorMessage });
}
completed++;
this.updateProgress(completed, files.length);
}
};
await Promise.all(chunks.map(processChunk));
}
async processSingleFile(filePath, config, options) {
const startTime = Date.now();
try {
const originalContent = await promises_1.default.readFile(filePath, 'utf8');
const originalSize = Buffer.byteLength(originalContent, 'utf8');
if (config.validateInput) {
const validation = this.validateSvg(originalContent, filePath);
if (!validation.isValid) {
throw new Error(`Invalid SVG: ${validation.errors.join(', ')}`);
}
}
if (this.isAlreadyOptimized(originalContent, options)) {
return null;
}
const svgoConfig = this.createSvgoConfig(config, options);
const result = (0, svgo_1.optimize)(originalContent, svgoConfig);
if ('error' in result) {
throw new Error(`SVGO optimization failed: ${result.error}`);
}
const optimizedContent = result.data;
const optimizedSize = Buffer.byteLength(optimizedContent, 'utf8');
if (config.validateOutput) {
const validation = this.validateSvg(optimizedContent, filePath);
if (!validation.isValid) {
throw new Error(`Output validation failed: ${validation.errors.join(', ')}`);
}
}
const bytesSaved = originalSize - optimizedSize;
const compressionRatio = originalSize / optimizedSize;
const outputPath = this.getOutputPath(filePath, options);
if (!options.dryRun) {
await this.handleOutput(filePath, outputPath, originalContent, optimizedContent, options);
}
const processingTime = Date.now() - startTime;
return {
inputPath: filePath,
outputPath,
originalSize,
optimizedSize,
compressionRatio,
bytesSaved,
pluginsApplied: this.getAppliedPlugins(svgoConfig),
processingTime,
originalContent,
optimizedContent
};
}
catch (error) {
throw new Error(`Failed to process ${path_1.default.basename(filePath)}: ${error instanceof Error ? error.message : String(error)}`);
}
}
validateSvg(content, filePath) {
const errors = [];
const warnings = [];
if (!(0, is_svg_1.default)(content)) {
errors.push('Not a valid SVG file');
}
if (!content.includes('<svg')) {
errors.push('Missing SVG root element');
}
const fileSize = Buffer.byteLength(content, 'utf8');
if (fileSize === 0) {
errors.push('Empty file');
}
else if (fileSize > 10 * 1024 * 1024) {
warnings.push('Large file size (>10MB)');
}
const hasViewBox = content.includes('viewBox');
if (!hasViewBox) {
warnings.push('Missing viewBox attribute');
}
const hasTitle = content.includes('<title');
const hasDesc = content.includes('<desc');
const elementCount = (content.match(/<[^\/][^>]*>/g) || []).length;
return {
isValid: errors.length === 0,
errors,
warnings,
fileSize,
hasViewBox,
hasTitle,
hasDesc,
elementCount
};
}
isAlreadyOptimized(content, options) {
if (options.force) {
return false;
}
const indicators = [
'<!-- Generated by SVGO -->',
'<!-- Optimized by SVGO -->',
'data-svgo',
'.min.svg',
'.optimized.svg'
];
return indicators.some(indicator => content.includes(indicator));
}
createSvgoConfig(config, options) {
const plugins = [];
const presetDefaultConfig = {
name: 'preset-default',
params: {
overrides: {}
}
};
if (config.keepIds) {
presetDefaultConfig.params.overrides.cleanupIds = false;
}
if (config.keepTitles) {
presetDefaultConfig.params.overrides.removeTitle = false;
presetDefaultConfig.params.overrides.removeDesc = false;
}
if (options.removeViewbox) {
presetDefaultConfig.params.overrides.removeViewBox = true;
}
if (options.sortAttrs) {
presetDefaultConfig.params.overrides.sortAttrs = true;
}
if (options.removeXmlns) {
presetDefaultConfig.params.overrides.removeXMLNS = true;
}
if (options.convertColors) {
presetDefaultConfig.params.overrides.convertColors = true;
}
plugins.push(presetDefaultConfig);
if (config.customPlugins.length > 0) {
plugins.push(...config.customPlugins);
}
const svgoConfig = {
plugins,
multipass: config.multipass,
floatPrecision: config.precision
};
return svgoConfig;
}
getOutputPath(inputPath, options) {
const dir = path_1.default.dirname(inputPath);
const name = path_1.default.basename(inputPath, path_1.default.extname(inputPath));
const ext = path_1.default.extname(inputPath);
if (options.outputDir) {
return path_1.default.join((0, shared_1.resolvePath)(options.outputDir), path_1.default.basename(inputPath));
}
if (options.copy) {
return path_1.default.join(dir, `${name}.optimized${ext}`);
}
return inputPath;
}
async handleOutput(inputPath, outputPath, originalContent, optimizedContent, options) {
if (options.backup && outputPath === inputPath) {
const backupPath = inputPath.replace(/\.svg$/, '.original.svg');
await promises_1.default.writeFile(backupPath, originalContent);
}
if (options.stdout && !options.outputDir) {
console.log(optimizedContent);
return;
}
await promises_1.default.writeFile(outputPath, optimizedContent);
}
getAppliedPlugins(config) {
if (!config.plugins)
return [];
return config.plugins
.map(plugin => (typeof plugin === 'string' ? plugin : plugin.name))
.filter(Boolean);
}
updateStats(stats, result) {
stats.filesProcessed++;
stats.bytesProcessed += result.originalSize;
stats.bytesOriginal += result.originalSize;
stats.bytesOptimized += result.optimizedSize;
stats.bytesSaved += result.bytesSaved;
stats.operationsCompleted++;
}
calculateFinalStats(stats, startTime) {
stats.processingTime = Date.now() - startTime;
stats.avgCompressionRatio =
stats.filesProcessed > 0 ? stats.bytesOriginal / stats.bytesOptimized : 0;
}
chunkArray(array, chunkSize) {
const chunks = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
async generateReport(results, stats, options) {
if (!options.report)
return;
const reportData = {
summary: stats,
files: results.map(result => ({
file: path_1.default.basename(result.inputPath),
originalSize: result.originalSize,
optimizedSize: result.optimizedSize,
bytesSaved: result.bytesSaved,
compressionRatio: result.compressionRatio,
processingTime: result.processingTime,
plugins: result.pluginsApplied
}))
};
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const reportPath = `svg-optimization-report-${timestamp}.${options.report}`;
if (options.report === 'json') {
await promises_1.default.writeFile(reportPath, JSON.stringify(reportData, null, 2));
}
else if (options.report === 'csv') {
const csv = this.convertToCsv(reportData.files);
await promises_1.default.writeFile(reportPath, csv);
}
this.logger.info(`Report saved to: ${reportPath}`);
}
convertToCsv(data) {
if (data.length === 0)
return '';
const headers = Object.keys(data[0]);
const rows = data.map(row => headers.map(header => row[header]).join(','));
return [headers.join(','), ...rows].join('\n');
}
}
exports.SvgProcessor = SvgProcessor;
//# sourceMappingURL=logic.js.map