svger-cli 4.0.1 → 4.0.3

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/README.md +27 -27
  3. package/dist/builder.d.ts +6 -3
  4. package/dist/builder.js +34 -24
  5. package/dist/cli.js +122 -10
  6. package/dist/config.d.ts +8 -2
  7. package/dist/config.js +17 -124
  8. package/dist/core/enhanced-plugin-manager.d.ts +1 -0
  9. package/dist/core/enhanced-plugin-manager.js +37 -11
  10. package/dist/core/framework-templates.js +4 -0
  11. package/dist/core/logger.js +8 -4
  12. package/dist/core/performance-engine.js +16 -3
  13. package/dist/core/style-compiler.js +6 -7
  14. package/dist/core/template-manager.js +18 -14
  15. package/dist/index.d.ts +1 -2
  16. package/dist/index.js +8 -2
  17. package/dist/integrations/jest-preset.js +30 -2
  18. package/dist/lock.js +1 -1
  19. package/dist/optimizers/basic-cleaner.js +4 -0
  20. package/dist/optimizers/path-parser.js +199 -115
  21. package/dist/optimizers/path-simplifier.js +27 -24
  22. package/dist/optimizers/remove-unused-defs.js +16 -0
  23. package/dist/optimizers/shape-conversion.js +22 -27
  24. package/dist/optimizers/style-optimizer.js +5 -0
  25. package/dist/optimizers/svg-tree-parser.js +4 -0
  26. package/dist/optimizers/transform-collapsing.js +11 -15
  27. package/dist/optimizers/transform-optimizer.js +20 -21
  28. package/dist/optimizers/types.js +64 -74
  29. package/dist/plugins/gradient-optimizer.js +4 -0
  30. package/dist/processors/svg-processor.js +28 -10
  31. package/dist/services/config.js +28 -11
  32. package/dist/services/file-watcher.js +8 -3
  33. package/dist/services/svg-service.d.ts +1 -1
  34. package/dist/services/svg-service.js +24 -11
  35. package/dist/utils/native.d.ts +0 -1
  36. package/dist/utils/native.js +6 -14
  37. package/dist/utils/visual-diff.js +7 -2
  38. package/dist/watch.js +4 -3
  39. package/docs/ERROR-HANDLING-STANDARD.md +111 -0
  40. package/docs/OPTIONAL-DEPENDENCIES.md +1 -1
  41. package/package.json +1 -1
@@ -144,8 +144,9 @@ function shouldConvert(element, pathData, threshold) {
144
144
  function convertShapeToPath(node, threshold) {
145
145
  const tag = node.tag;
146
146
  let pathData = null;
147
- switch (tag) {
148
- case 'rect': {
147
+ // O(1) object lookup for shape tag → path converter
148
+ const shapeConverters = {
149
+ rect: () => {
149
150
  const x = parseFloat(node.attrs.get('x') || '0');
150
151
  const y = parseFloat(node.attrs.get('y') || '0');
151
152
  const width = parseFloat(node.attrs.get('width') || '0');
@@ -156,41 +157,35 @@ function convertShapeToPath(node, threshold) {
156
157
  const ry = node.attrs.has('ry')
157
158
  ? parseFloat(node.attrs.get('ry'))
158
159
  : undefined;
159
- pathData = rectToPath(x, y, width, height, rx, ry);
160
- break;
161
- }
162
- case 'circle': {
160
+ return rectToPath(x, y, width, height, rx, ry);
161
+ },
162
+ circle: () => {
163
163
  const cx = parseFloat(node.attrs.get('cx') || '0');
164
164
  const cy = parseFloat(node.attrs.get('cy') || '0');
165
165
  const r = parseFloat(node.attrs.get('r') || '0');
166
- pathData = circleToPath(cx, cy, r);
167
- break;
168
- }
169
- case 'ellipse': {
166
+ return circleToPath(cx, cy, r);
167
+ },
168
+ ellipse: () => {
170
169
  const cx = parseFloat(node.attrs.get('cx') || '0');
171
170
  const cy = parseFloat(node.attrs.get('cy') || '0');
172
171
  const rx = parseFloat(node.attrs.get('rx') || '0');
173
172
  const ry = parseFloat(node.attrs.get('ry') || '0');
174
- pathData = ellipseToPath(cx, cy, rx, ry);
175
- break;
176
- }
177
- case 'polygon': {
173
+ return ellipseToPath(cx, cy, rx, ry);
174
+ },
175
+ polygon: () => {
178
176
  const points = node.attrs.get('points');
179
- if (points) {
180
- pathData = polygonToPath(points);
181
- }
182
- break;
183
- }
184
- case 'polyline': {
177
+ return points ? polygonToPath(points) : null;
178
+ },
179
+ polyline: () => {
185
180
  const points = node.attrs.get('points');
186
- if (points) {
187
- pathData = polylineToPath(points);
188
- }
189
- break;
190
- }
191
- default:
192
- return { converted: false, savings: 0 };
181
+ return points ? polylineToPath(points) : null;
182
+ },
183
+ };
184
+ const converter = shapeConverters[tag];
185
+ if (!converter) {
186
+ return { converted: false, savings: 0 };
193
187
  }
188
+ pathData = converter();
194
189
  // Check if conversion was possible
195
190
  if (!pathData) {
196
191
  return { converted: false, savings: 0 };
@@ -45,6 +45,11 @@ function parseCSSRules(cssText: string): Map<string, Map<string, string>> {
45
45
  let match;
46
46
 
47
47
  while ((match = ruleRegex.exec(cleaned)) !== null) {
48
+ // Prevent infinite loop if regex doesn't advance
49
+ if (match.index === ruleRegex.lastIndex) {
50
+ ruleRegex.lastIndex++;
51
+ }
52
+
48
53
  const selector = match[1].trim();
49
54
  const declarationsText = match[2].trim();
50
55
 
@@ -60,6 +60,10 @@ function parseAttributes(attrString) {
60
60
  const attrRegex = /([a-zA-Z][a-zA-Z0-9-:]*)=["']([^"']*)["']/g;
61
61
  let match;
62
62
  while ((match = attrRegex.exec(attrString)) !== null) {
63
+ // Prevent infinite loop if regex doesn't advance
64
+ if (match.index === attrRegex.lastIndex) {
65
+ attrRegex.lastIndex++;
66
+ }
63
67
  const [, name, value] = match;
64
68
  attrs.set(name, value);
65
69
  }
@@ -220,21 +220,17 @@ function applyTransformToPath(node, matrix) {
220
220
  * Apply transform to shape coordinates
221
221
  */
222
222
  function applyTransformToShape(node, matrix) {
223
- switch (node.tag) {
224
- case 'rect':
225
- return applyTransformToRect(node, matrix);
226
- case 'circle':
227
- return applyTransformToCircle(node, matrix);
228
- case 'line':
229
- return applyTransformToLine(node, matrix);
230
- case 'polygon':
231
- case 'polyline':
232
- return applyTransformToPoints(node, matrix);
233
- case 'path':
234
- return applyTransformToPath(node, matrix);
235
- default:
236
- return false;
237
- }
223
+ // O(1) object lookup for shape tag → transform handler
224
+ const shapeHandlers = {
225
+ rect: applyTransformToRect,
226
+ circle: applyTransformToCircle,
227
+ line: applyTransformToLine,
228
+ polygon: applyTransformToPoints,
229
+ polyline: applyTransformToPoints,
230
+ path: applyTransformToPath,
231
+ };
232
+ const handler = shapeHandlers[node.tag];
233
+ return handler ? handler(node, matrix) : false;
238
234
  }
239
235
  /**
240
236
  * Collapse transforms by propagating down the tree
@@ -36,6 +36,10 @@ export function parseTransformList(transformStr) {
36
36
  const regex = /(\w+)\s*\([^)]+\)/g;
37
37
  let match;
38
38
  while ((match = regex.exec(transformStr)) !== null) {
39
+ // Prevent infinite loop if regex doesn't advance
40
+ if (match.index === regex.lastIndex) {
41
+ regex.lastIndex++;
42
+ }
39
43
  const cmd = parseTransformCommand(match[0]);
40
44
  if (cmd) {
41
45
  transforms.push(cmd);
@@ -63,32 +67,27 @@ function multiplyMatrices(m1, m2) {
63
67
  */
64
68
  function transformToMatrix(transform) {
65
69
  const { type, values } = transform;
66
- switch (type) {
67
- case 'matrix':
68
- if (values.length === 6) {
69
- return values;
70
- }
71
- return IDENTITY_MATRIX;
72
- case 'translate': {
70
+ // O(1) object lookup for transform type → matrix factory
71
+ const matrixFactories = {
72
+ matrix: () => (values.length === 6 ? values : IDENTITY_MATRIX),
73
+ translate: () => {
73
74
  const tx = values[0] || 0;
74
75
  const ty = values[1] || 0;
75
76
  return [1, 0, 0, 1, tx, ty];
76
- }
77
- case 'scale': {
77
+ },
78
+ scale: () => {
78
79
  const sx = values[0] || 1;
79
80
  const sy = values[1] !== undefined ? values[1] : sx;
80
81
  return [sx, 0, 0, sy, 0, 0];
81
- }
82
- case 'rotate': {
82
+ },
83
+ rotate: () => {
83
84
  const angle = values[0] || 0;
84
85
  const rad = (angle * Math.PI) / 180;
85
86
  const cos = Math.cos(rad);
86
87
  const sin = Math.sin(rad);
87
- // Rotate around point (cx, cy) if provided
88
88
  if (values.length === 3) {
89
89
  const cx = values[1];
90
90
  const cy = values[2];
91
- // translate(-cx, -cy) × rotate(angle) × translate(cx, cy)
92
91
  return [
93
92
  cos,
94
93
  sin,
@@ -99,22 +98,22 @@ function transformToMatrix(transform) {
99
98
  ];
100
99
  }
101
100
  return [cos, sin, -sin, cos, 0, 0];
102
- }
103
- case 'skewX': {
101
+ },
102
+ skewX: () => {
104
103
  const angle = values[0] || 0;
105
104
  const rad = (angle * Math.PI) / 180;
106
105
  const tan = Math.tan(rad);
107
106
  return [1, 0, tan, 1, 0, 0];
108
- }
109
- case 'skewY': {
107
+ },
108
+ skewY: () => {
110
109
  const angle = values[0] || 0;
111
110
  const rad = (angle * Math.PI) / 180;
112
111
  const tan = Math.tan(rad);
113
112
  return [1, tan, 0, 1, 0, 0];
114
- }
115
- default:
116
- return IDENTITY_MATRIX;
117
- }
113
+ },
114
+ };
115
+ const factory = matrixFactories[type];
116
+ return factory ? factory() : IDENTITY_MATRIX;
118
117
  }
119
118
  /**
120
119
  * Consolidate transform list into single matrix
@@ -45,78 +45,68 @@ export function getDefaultOptConfig(level) {
45
45
  shapeConversionThreshold: 5,
46
46
  optimizationLevel: level,
47
47
  };
48
- switch (level) {
49
- case OptLevel.NONE:
50
- return {
51
- ...baseConfig,
52
- removeMetadata: false,
53
- removeComments: false,
54
- normalizeWhitespace: false,
55
- removeUnnecessaryAttrs: false,
56
- shortenColors: false,
57
- removeEmptyContainers: false,
58
- removeDoctype: false,
59
- removeXMLProcInst: false,
60
- enableNumericOptimization: false,
61
- enableStyleOptimization: false,
62
- enableTransformOptimization: false,
63
- enableTransformCollapsing: false,
64
- enablePathOptimization: false,
65
- enablePathSimplification: false,
66
- };
67
- case OptLevel.BASIC:
68
- return baseConfig;
69
- case OptLevel.BALANCED:
70
- return {
71
- ...baseConfig,
72
- removeHiddenElements: true,
73
- floatPrecision: 3,
74
- enableNumericOptimization: true,
75
- enableStyleOptimization: true,
76
- enableTransformOptimization: false,
77
- enableTransformCollapsing: false,
78
- enablePathOptimization: false,
79
- };
80
- case OptLevel.AGGRESSIVE:
81
- return {
82
- ...baseConfig,
83
- removeHiddenElements: true,
84
- mergePaths: true,
85
- collapseGroups: true,
86
- floatPrecision: 2,
87
- pathTolerance: 0.7,
88
- sortAttrs: true,
89
- enableNumericOptimization: true,
90
- enableStyleOptimization: true,
91
- enableTransformOptimization: true,
92
- enableTransformCollapsing: true,
93
- enablePathOptimization: true,
94
- // DISABLED: Shape conversion conflicts with floatPrecision=2
95
- // Causes visual regressions (coordinates get rounded before conversion)
96
- shapeConversion: false,
97
- shapeConversionThreshold: 5,
98
- };
99
- case OptLevel.MAXIMUM:
100
- return {
101
- ...baseConfig,
102
- removeHiddenElements: true,
103
- mergePaths: true,
104
- collapseGroups: true,
105
- inlineStyles: true,
106
- floatPrecision: 1,
107
- pathTolerance: 0.9,
108
- sortAttrs: true,
109
- removeViewBox: false, // Keep viewBox even at maximum for compatibility
110
- enableNumericOptimization: true,
111
- enableStyleOptimization: true,
112
- enableTransformOptimization: true,
113
- enableTransformCollapsing: true,
114
- enablePathOptimization: true,
115
- enablePathSimplification: true,
116
- shapeConversion: true,
117
- shapeConversionThreshold: 0, // Convert even 1-byte savings
118
- };
119
- default:
120
- return baseConfig;
121
- }
48
+ // O(1) object lookup for optimization level → config overrides
49
+ const levelOverrides = {
50
+ [OptLevel.NONE]: {
51
+ removeMetadata: false,
52
+ removeComments: false,
53
+ normalizeWhitespace: false,
54
+ removeUnnecessaryAttrs: false,
55
+ shortenColors: false,
56
+ removeEmptyContainers: false,
57
+ removeDoctype: false,
58
+ removeXMLProcInst: false,
59
+ enableNumericOptimization: false,
60
+ enableStyleOptimization: false,
61
+ enableTransformOptimization: false,
62
+ enableTransformCollapsing: false,
63
+ enablePathOptimization: false,
64
+ enablePathSimplification: false,
65
+ },
66
+ [OptLevel.BASIC]: {},
67
+ [OptLevel.BALANCED]: {
68
+ removeHiddenElements: true,
69
+ floatPrecision: 3,
70
+ enableNumericOptimization: true,
71
+ enableStyleOptimization: true,
72
+ enableTransformOptimization: false,
73
+ enableTransformCollapsing: false,
74
+ enablePathOptimization: false,
75
+ },
76
+ [OptLevel.AGGRESSIVE]: {
77
+ removeHiddenElements: true,
78
+ mergePaths: true,
79
+ collapseGroups: true,
80
+ floatPrecision: 2,
81
+ pathTolerance: 0.7,
82
+ sortAttrs: true,
83
+ enableNumericOptimization: true,
84
+ enableStyleOptimization: true,
85
+ enableTransformOptimization: true,
86
+ enableTransformCollapsing: true,
87
+ enablePathOptimization: true,
88
+ shapeConversion: false,
89
+ shapeConversionThreshold: 5,
90
+ },
91
+ [OptLevel.MAXIMUM]: {
92
+ removeHiddenElements: true,
93
+ mergePaths: true,
94
+ collapseGroups: true,
95
+ inlineStyles: true,
96
+ floatPrecision: 1,
97
+ pathTolerance: 0.9,
98
+ sortAttrs: true,
99
+ removeViewBox: false,
100
+ enableNumericOptimization: true,
101
+ enableStyleOptimization: true,
102
+ enableTransformOptimization: true,
103
+ enableTransformCollapsing: true,
104
+ enablePathOptimization: true,
105
+ enablePathSimplification: true,
106
+ shapeConversion: true,
107
+ shapeConversionThreshold: 0,
108
+ },
109
+ };
110
+ const overrides = levelOverrides[level] ?? {};
111
+ return { ...baseConfig, ...overrides };
122
112
  }
@@ -44,6 +44,10 @@ function removeDuplicateStops(gradientContent) {
44
44
  const seen = new Set();
45
45
  let match;
46
46
  while ((match = stopRegex.exec(gradientContent)) !== null) {
47
+ // Prevent infinite loop if regex doesn't advance
48
+ if (match.index === stopRegex.lastIndex) {
49
+ stopRegex.lastIndex++;
50
+ }
47
51
  const stopTag = match[0];
48
52
  const normalized = stopTag.replace(/\s+/g, ' ').trim();
49
53
  if (!seen.has(normalized)) {
@@ -187,7 +187,7 @@ export class SVGProcessor {
187
187
  const baseName = path.basename(fileName, '.svg');
188
188
  // Object lookup map for naming conventions - O(1) performance
189
189
  const namingHandlers = {
190
- kebab: () => toPascalCase(baseName),
190
+ kebab: () => toKebabCase(baseName),
191
191
  camel: () => {
192
192
  const pascalName = toPascalCase(baseName);
193
193
  return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
@@ -340,13 +340,26 @@ export class SVGProcessor {
340
340
  error: error,
341
341
  };
342
342
  logger.error(`Failed to process ${svgFilePath}:`, error);
343
+ // Immediately remove failed jobs to prevent memory leaks
344
+ this.processingQueue.delete(jobId);
343
345
  return result;
344
346
  }
345
347
  finally {
346
- // Clean up completed jobs after some time
347
- setTimeout(() => {
348
- this.processingQueue.delete(jobId);
349
- }, 30000); // 30 seconds
348
+ // Clean up completed jobs after a short delay (allows stats queries)
349
+ if (job.status === 'completed') {
350
+ setTimeout(() => {
351
+ this.processingQueue.delete(jobId);
352
+ }, 30000); // 30 seconds
353
+ }
354
+ // Cap queue size as a safety net to prevent unbounded growth
355
+ if (this.processingQueue.size > 10000) {
356
+ const oldestEntries = Array.from(this.processingQueue.entries())
357
+ .filter(([, j]) => j.status === 'completed' || j.status === 'failed')
358
+ .slice(0, this.processingQueue.size - 5000);
359
+ for (const [key] of oldestEntries) {
360
+ this.processingQueue.delete(key);
361
+ }
362
+ }
350
363
  }
351
364
  }
352
365
  /**
@@ -354,13 +367,18 @@ export class SVGProcessor {
354
367
  */
355
368
  getProcessingStats() {
356
369
  const jobs = Array.from(this.processingQueue.values());
357
- return {
370
+ // Direct property increment via object key — O(1) per job, avoids switch branching
371
+ const stats = {
358
372
  total: jobs.length,
359
- pending: jobs.filter(j => j.status === 'pending').length,
360
- processing: jobs.filter(j => j.status === 'processing').length,
361
- completed: jobs.filter(j => j.status === 'completed').length,
362
- failed: jobs.filter(j => j.status === 'failed').length,
373
+ pending: 0,
374
+ processing: 0,
375
+ completed: 0,
376
+ failed: 0,
363
377
  };
378
+ for (const job of jobs) {
379
+ stats[job.status]++;
380
+ }
381
+ return stats;
364
382
  }
365
383
  /**
366
384
  * Clear processing queue
@@ -1,6 +1,14 @@
1
1
  import path from 'path';
2
2
  import { FileSystem } from '../utils/native.js';
3
3
  import { logger } from '../core/logger.js';
4
+ import { readFileSync } from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from 'path';
7
+ // Get package version dynamically
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
11
+ const CURRENT_VERSION = packageJson.version;
4
12
  /**
5
13
  * Professional configuration management service
6
14
  */
@@ -21,7 +29,7 @@ export class ConfigService {
21
29
  getDefaultConfig() {
22
30
  return {
23
31
  // Configuration Version (for migration compatibility)
24
- version: '4.0.0',
32
+ version: CURRENT_VERSION,
25
33
  // Source & Output
26
34
  source: './src/assets/svg',
27
35
  output: './src/components/icons',
@@ -130,7 +138,7 @@ export class ConfigService {
130
138
  return this.cachedConfig;
131
139
  }
132
140
  // Check if migration is needed
133
- if (!configData.version || configData.version !== '4.0.0') {
141
+ if (!configData.version || configData.version !== CURRENT_VERSION) {
134
142
  logger.info('Detected older configuration version, migrating...');
135
143
  const migratedConfig = this.migrateConfig(configData);
136
144
  this.cachedConfig = migratedConfig;
@@ -220,16 +228,21 @@ export class ConfigService {
220
228
  * Migrate configuration from older versions to v4.0.0
221
229
  */
222
230
  migrateConfig(config) {
231
+ // Validate config is not null/undefined
232
+ if (!config || typeof config !== 'object') {
233
+ logger.warn('Invalid config provided for migration, using defaults');
234
+ return this.getDefaultConfig();
235
+ }
223
236
  const currentVersion = config.version || '3.0.0';
224
- // No migration needed if already v4.0.0
225
- if (currentVersion === '4.0.0') {
226
- return config;
237
+ // No migration needed if already current version
238
+ if (currentVersion === CURRENT_VERSION) {
239
+ return { ...this.getDefaultConfig(), ...config };
227
240
  }
228
- logger.info(`Migrating configuration from ${currentVersion} to 4.0.0...`);
241
+ logger.info(`Migrating configuration from ${currentVersion} to ${CURRENT_VERSION}...`);
229
242
  // Migration from v3.x to v4.0.0
230
243
  const migratedConfig = { ...config };
231
244
  // Add version field
232
- migratedConfig.version = '4.0.0';
245
+ migratedConfig.version = CURRENT_VERSION;
233
246
  // Ensure plugins array exists (new in v4.0.0)
234
247
  if (!migratedConfig.plugins) {
235
248
  migratedConfig.plugins = [];
@@ -262,10 +275,14 @@ export class ConfigService {
262
275
  logger.info(`Migrated optimization level: ${oldOptimization} → ${migratedConfig.performance.optimization}`);
263
276
  }
264
277
  }
265
- // Save migrated config
266
- this.writeConfig(migratedConfig);
267
- logger.success('Configuration migrated to v4.0.0');
268
- return migratedConfig;
278
+ // Save migrated config merged with defaults to ensure all new keys exist
279
+ const mergedMigratedConfig = {
280
+ ...this.getDefaultConfig(),
281
+ ...migratedConfig,
282
+ };
283
+ this.writeConfig(mergedMigratedConfig);
284
+ logger.success(`Configuration migrated from ${currentVersion} to ${CURRENT_VERSION}`);
285
+ return mergedMigratedConfig;
269
286
  }
270
287
  /**
271
288
  * Validate configuration
@@ -59,10 +59,15 @@ export class FileWatcherService {
59
59
  if (this.debounceTimers.has(debounceKey)) {
60
60
  clearTimeout(this.debounceTimers.get(debounceKey));
61
61
  }
62
- // Set new timer
62
+ // Set new timer with error handling
63
63
  const timer = setTimeout(async () => {
64
64
  this.debounceTimers.delete(debounceKey);
65
- await this.processFileEvent(watchId, eventType, filePath);
65
+ try {
66
+ await this.processFileEvent(watchId, eventType, filePath);
67
+ }
68
+ catch (error) {
69
+ logger.error(`Failed to process file event for ${filePath}:`, error);
70
+ }
66
71
  }, this.debounceDelay);
67
72
  this.debounceTimers.set(debounceKey, timer);
68
73
  }
@@ -130,7 +135,7 @@ export class FileWatcherService {
130
135
  watcher.close();
131
136
  this.watchers.delete(watchId);
132
137
  this.eventHandlers.delete(watchId);
133
- // Clear any pending debounce timers for this watcher
138
+ // Clear all pending debounce timers for this watcher to prevent orphaned timers
134
139
  for (const [key, timer] of this.debounceTimers.entries()) {
135
140
  if (key.startsWith(`${watchId}-`)) {
136
141
  clearTimeout(timer);
@@ -11,7 +11,7 @@ export declare class SVGService {
11
11
  /**
12
12
  * Set optimizer level for SVG processing
13
13
  */
14
- private setOptimizerLevel;
14
+ setOptimizerLevel(level: string): void;
15
15
  /**
16
16
  * Build all SVG files from source to output directory
17
17
  */
@@ -25,8 +25,15 @@ export class SVGService {
25
25
  * Set optimizer level for SVG processing
26
26
  */
27
27
  setOptimizerLevel(level) {
28
- const validLevels = ['none', 'basic', 'balanced', 'aggressive', 'maximum'];
29
- if (!validLevels.includes(level.toLowerCase())) {
28
+ // O(1) Set lookup instead of O(n) Array.includes()
29
+ const validLevels = new Set([
30
+ 'none',
31
+ 'basic',
32
+ 'balanced',
33
+ 'aggressive',
34
+ 'maximum',
35
+ ]);
36
+ if (!validLevels.has(level.toLowerCase())) {
30
37
  logger.warn(`Invalid optimization level "${level}". Using "basic" instead.`);
31
38
  svgProcessor.setOptimizationLevel(OptLevel.BASIC);
32
39
  return;
@@ -117,15 +124,21 @@ export class SVGService {
117
124
  });
118
125
  }
119
126
  }
120
- // Log summary
121
- const successful = results.filter(r => r.success).length;
122
- const failed = results.filter(r => !r.success).length;
123
- logger.info(`Build complete: ${successful} successful, ${failed} failed`);
124
- if (failed > 0) {
127
+ // Log summary - single pass through results
128
+ const successfulResults = [];
129
+ const failedResults = [];
130
+ for (const result of results) {
131
+ if (result.success) {
132
+ successfulResults.push(result);
133
+ }
134
+ else {
135
+ failedResults.push(result);
136
+ }
137
+ }
138
+ logger.info(`Build complete: ${successfulResults.length} successful, ${failedResults.length} failed`);
139
+ if (failedResults.length > 0) {
125
140
  logger.warn('Some files failed to process:');
126
- results
127
- .filter(r => !r.success)
128
- .forEach(r => {
141
+ failedResults.forEach(r => {
129
142
  logger.warn(` - ${r.file}: ${r.error?.message}`);
130
143
  });
131
144
  }
@@ -210,7 +223,7 @@ export class SVGService {
210
223
  },
211
224
  unlink: async () => {
212
225
  logger.info(`SVG removed: ${fileName}`);
213
- await this.handleFileRemoval(event.filePath, outDir);
226
+ await this.handleFileRemoval(event.filePath, outDir, config);
214
227
  },
215
228
  };
216
229
  const handler = eventHandlers[event.type];
@@ -45,7 +45,6 @@ export declare class FileSystem {
45
45
  private static _readdir;
46
46
  private static _stat;
47
47
  private static _mkdir;
48
- private static _rmdir;
49
48
  private static _unlink;
50
49
  static exists(path: string): Promise<boolean>;
51
50
  static readFile(path: string, encoding?: BufferEncoding): Promise<string>;