svger-cli 2.0.1 → 2.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.
- package/.svgerconfig.example.json +38 -0
- package/CHANGELOG.md +64 -0
- package/DEVELOPMENT.md +353 -0
- package/README.md +24 -5
- package/SECURITY.md +69 -0
- package/dist/builder.js +16 -16
- package/dist/clean.js +2 -2
- package/dist/cli.js +38 -38
- package/dist/config.js +11 -11
- package/dist/core/error-handler.d.ts +63 -0
- package/dist/core/error-handler.js +227 -0
- package/dist/core/framework-templates.d.ts +17 -0
- package/{src/core/framework-templates.ts → dist/core/framework-templates.js} +104 -139
- package/dist/core/logger.d.ts +22 -0
- package/dist/core/logger.js +85 -0
- package/dist/core/performance-engine.d.ts +67 -0
- package/dist/core/performance-engine.js +252 -0
- package/dist/core/plugin-manager.d.ts +56 -0
- package/dist/core/plugin-manager.js +191 -0
- package/dist/core/style-compiler.d.ts +88 -0
- package/dist/core/style-compiler.js +468 -0
- package/dist/core/template-manager.d.ts +64 -0
- package/{src/core/template-manager.ts → dist/core/template-manager.js} +172 -255
- package/dist/index.d.ts +153 -0
- package/{src/index.ts → dist/index.js} +32 -110
- package/dist/lock.js +7 -7
- package/dist/processors/svg-processor.d.ts +73 -0
- package/dist/processors/svg-processor.js +261 -0
- package/dist/services/config.d.ts +55 -0
- package/dist/services/config.js +211 -0
- package/dist/services/file-watcher.d.ts +54 -0
- package/dist/services/file-watcher.js +180 -0
- package/dist/services/svg-service.d.ts +81 -0
- package/dist/services/svg-service.js +395 -0
- package/dist/templates/ComponentTemplate.js +25 -25
- package/dist/types/index.d.ts +146 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/native.d.ts +104 -0
- package/dist/utils/native.js +340 -0
- package/dist/watch.d.ts +1 -1
- package/dist/watch.js +14 -14
- package/package.json +154 -14
- package/.svgconfig.json +0 -3
- package/CODE_OF_CONDUCT.md +0 -79
- package/CONTRIBUTING.md +0 -146
- package/TESTING.md +0 -143
- package/cli-framework.test.js +0 -16
- package/cli-test-angular/Arrowbenddownleft.component.ts +0 -27
- package/cli-test-angular/Vite.component.ts +0 -27
- package/cli-test-angular/index.ts +0 -25
- package/cli-test-output/Arrowbenddownleft.vue +0 -33
- package/cli-test-output/Vite.vue +0 -33
- package/cli-test-output/index.ts +0 -25
- package/cli-test-react/Arrowbenddownleft.tsx +0 -39
- package/cli-test-react/Vite.tsx +0 -39
- package/cli-test-react/index.ts +0 -25
- package/cli-test-svelte/Arrowbenddownleft.svelte +0 -22
- package/cli-test-svelte/Vite.svelte +0 -22
- package/cli-test-svelte/index.ts +0 -25
- package/frameworks.test.js +0 -170
- package/my-svgs/ArrowBendDownLeft.svg +0 -6
- package/my-svgs/vite.svg +0 -1
- package/src/builder.ts +0 -104
- package/src/clean.ts +0 -21
- package/src/cli.ts +0 -221
- package/src/config.ts +0 -81
- package/src/core/error-handler.ts +0 -303
- package/src/core/logger.ts +0 -104
- package/src/core/performance-engine.ts +0 -327
- package/src/core/plugin-manager.ts +0 -228
- package/src/core/style-compiler.ts +0 -605
- package/src/lock.ts +0 -74
- package/src/processors/svg-processor.ts +0 -288
- package/src/services/config.ts +0 -241
- package/src/services/file-watcher.ts +0 -218
- package/src/services/svg-service.ts +0 -468
- package/src/templates/ComponentTemplate.ts +0 -57
- package/src/types/index.ts +0 -169
- package/src/utils/native.ts +0 -352
- package/src/watch.ts +0 -88
- package/test-output-mulit/TestIcon-angular-module.component.ts +0 -26
- package/test-output-mulit/TestIcon-angular-standalone.component.ts +0 -27
- package/test-output-mulit/TestIcon-lit.ts +0 -35
- package/test-output-mulit/TestIcon-preact.tsx +0 -38
- package/test-output-mulit/TestIcon-react.tsx +0 -35
- package/test-output-mulit/TestIcon-solid.tsx +0 -27
- package/test-output-mulit/TestIcon-svelte.svelte +0 -22
- package/test-output-mulit/TestIcon-vanilla.ts +0 -37
- package/test-output-mulit/TestIcon-vue-composition.vue +0 -33
- package/test-output-mulit/TestIcon-vue-options.vue +0 -31
- package/tsconfig.json +0 -18
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { FileWatchEvent, WatchOptions } from '../types/index.js';
|
|
4
|
-
import { FileSystem } from '../utils/native.js';
|
|
5
|
-
import { logger } from '../core/logger.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Professional file watching service with debouncing and event filtering
|
|
9
|
-
*/
|
|
10
|
-
export class FileWatcherService {
|
|
11
|
-
private static instance: FileWatcherService;
|
|
12
|
-
private watchers: Map<string, fs.FSWatcher> = new Map();
|
|
13
|
-
private eventHandlers: Map<string, Function[]> = new Map();
|
|
14
|
-
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
15
|
-
private readonly debounceDelay = 300; // milliseconds
|
|
16
|
-
|
|
17
|
-
private constructor() {}
|
|
18
|
-
|
|
19
|
-
public static getInstance(): FileWatcherService {
|
|
20
|
-
if (!FileWatcherService.instance) {
|
|
21
|
-
FileWatcherService.instance = new FileWatcherService();
|
|
22
|
-
}
|
|
23
|
-
return FileWatcherService.instance;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Start watching a directory for SVG file changes
|
|
28
|
-
*/
|
|
29
|
-
public async watchDirectory(
|
|
30
|
-
watchPath: string,
|
|
31
|
-
options: Partial<WatchOptions> = {}
|
|
32
|
-
): Promise<string> {
|
|
33
|
-
const resolvedPath = path.resolve(watchPath);
|
|
34
|
-
|
|
35
|
-
if (!(await FileSystem.exists(resolvedPath))) {
|
|
36
|
-
throw new Error(`Watch path does not exist: ${resolvedPath}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const watchId = `watch-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const watcher = fs.watch(resolvedPath, {
|
|
43
|
-
persistent: true,
|
|
44
|
-
recursive: false
|
|
45
|
-
}, (eventType, filename) => {
|
|
46
|
-
if (filename) {
|
|
47
|
-
this.handleFileEvent(watchId, eventType, path.join(resolvedPath, filename));
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
this.watchers.set(watchId, watcher);
|
|
52
|
-
this.eventHandlers.set(watchId, []);
|
|
53
|
-
|
|
54
|
-
logger.info(`Started watching directory: ${resolvedPath}`);
|
|
55
|
-
return watchId;
|
|
56
|
-
|
|
57
|
-
} catch (error) {
|
|
58
|
-
logger.error(`Failed to start watching ${resolvedPath}:`, error);
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Handle file system events with debouncing
|
|
65
|
-
*/
|
|
66
|
-
private handleFileEvent(watchId: string, eventType: string, filePath: string): void {
|
|
67
|
-
// Only process SVG files
|
|
68
|
-
if (!filePath.endsWith('.svg')) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const debounceKey = `${watchId}-${filePath}`;
|
|
73
|
-
|
|
74
|
-
// Clear existing timer
|
|
75
|
-
if (this.debounceTimers.has(debounceKey)) {
|
|
76
|
-
clearTimeout(this.debounceTimers.get(debounceKey)!);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Set new timer
|
|
80
|
-
const timer = setTimeout(async () => {
|
|
81
|
-
this.debounceTimers.delete(debounceKey);
|
|
82
|
-
await this.processFileEvent(watchId, eventType, filePath);
|
|
83
|
-
}, this.debounceDelay);
|
|
84
|
-
|
|
85
|
-
this.debounceTimers.set(debounceKey, timer);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Process debounced file events
|
|
90
|
-
*/
|
|
91
|
-
private async processFileEvent(watchId: string, eventType: string, filePath: string): Promise<void> {
|
|
92
|
-
try {
|
|
93
|
-
const exists = await FileSystem.exists(filePath);
|
|
94
|
-
let actualEventType: FileWatchEvent['type'];
|
|
95
|
-
|
|
96
|
-
// Determine actual event type
|
|
97
|
-
if (eventType === 'rename') {
|
|
98
|
-
actualEventType = exists ? 'add' : 'unlink';
|
|
99
|
-
} else {
|
|
100
|
-
actualEventType = 'change';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const event: FileWatchEvent = {
|
|
104
|
-
type: actualEventType,
|
|
105
|
-
filePath,
|
|
106
|
-
timestamp: Date.now()
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
logger.debug(`File event: ${actualEventType} - ${path.basename(filePath)}`);
|
|
110
|
-
|
|
111
|
-
// Emit event to registered handlers
|
|
112
|
-
this.emitEvent(watchId, event);
|
|
113
|
-
|
|
114
|
-
} catch (error) {
|
|
115
|
-
logger.error(`Error processing file event for ${filePath}:`, error);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Register an event handler for a specific watcher
|
|
121
|
-
*/
|
|
122
|
-
public onFileEvent(watchId: string, handler: (event: FileWatchEvent) => void | Promise<void>): void {
|
|
123
|
-
const handlers = this.eventHandlers.get(watchId) || [];
|
|
124
|
-
handlers.push(handler);
|
|
125
|
-
this.eventHandlers.set(watchId, handlers);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Emit event to all registered handlers
|
|
130
|
-
*/
|
|
131
|
-
private emitEvent(watchId: string, event: FileWatchEvent): void {
|
|
132
|
-
const handlers = this.eventHandlers.get(watchId) || [];
|
|
133
|
-
|
|
134
|
-
for (const handler of handlers) {
|
|
135
|
-
try {
|
|
136
|
-
const result = handler(event);
|
|
137
|
-
// Handle async handlers
|
|
138
|
-
if (result && typeof result.then === 'function') {
|
|
139
|
-
result.catch((error: any) => {
|
|
140
|
-
logger.error(`Error in file event handler:`, error);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
} catch (error) {
|
|
144
|
-
logger.error(`Error in file event handler:`, error);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Stop watching a specific directory
|
|
151
|
-
*/
|
|
152
|
-
public stopWatching(watchId: string): void {
|
|
153
|
-
const watcher = this.watchers.get(watchId);
|
|
154
|
-
if (watcher) {
|
|
155
|
-
watcher.close();
|
|
156
|
-
this.watchers.delete(watchId);
|
|
157
|
-
this.eventHandlers.delete(watchId);
|
|
158
|
-
|
|
159
|
-
// Clear any pending debounce timers for this watcher
|
|
160
|
-
for (const [key, timer] of this.debounceTimers.entries()) {
|
|
161
|
-
if (key.startsWith(`${watchId}-`)) {
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
this.debounceTimers.delete(key);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
logger.info(`Stopped watching: ${watchId}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Stop all watchers
|
|
173
|
-
*/
|
|
174
|
-
public stopAllWatchers(): void {
|
|
175
|
-
for (const watchId of this.watchers.keys()) {
|
|
176
|
-
this.stopWatching(watchId);
|
|
177
|
-
}
|
|
178
|
-
logger.info('Stopped all file watchers');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Get active watch statistics
|
|
183
|
-
*/
|
|
184
|
-
public getWatchStats(): {
|
|
185
|
-
activeWatchers: number;
|
|
186
|
-
pendingEvents: number;
|
|
187
|
-
totalHandlers: number;
|
|
188
|
-
} {
|
|
189
|
-
let totalHandlers = 0;
|
|
190
|
-
for (const handlers of this.eventHandlers.values()) {
|
|
191
|
-
totalHandlers += handlers.length;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
activeWatchers: this.watchers.size,
|
|
196
|
-
pendingEvents: this.debounceTimers.size,
|
|
197
|
-
totalHandlers
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Cleanup on shutdown
|
|
203
|
-
*/
|
|
204
|
-
public shutdown(): void {
|
|
205
|
-
this.stopAllWatchers();
|
|
206
|
-
|
|
207
|
-
// Clear all debounce timers
|
|
208
|
-
for (const timer of this.debounceTimers.values()) {
|
|
209
|
-
clearTimeout(timer);
|
|
210
|
-
}
|
|
211
|
-
this.debounceTimers.clear();
|
|
212
|
-
|
|
213
|
-
logger.info('File watcher service shutdown complete');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Export singleton instance
|
|
218
|
-
export const fileWatcher = FileWatcherService.getInstance();
|
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { FileSystem } from '../utils/native.js';
|
|
3
|
-
import { BuildOptions, GenerateOptions, WatchOptions, FileWatchEvent } from '../types/index.js';
|
|
4
|
-
import { logger } from '../core/logger.js';
|
|
5
|
-
import { configService } from './config.js';
|
|
6
|
-
import { svgProcessor } from '../processors/svg-processor.js';
|
|
7
|
-
import { fileWatcher } from './file-watcher.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Main SVG service that orchestrates all SVG processing operations
|
|
11
|
-
*/
|
|
12
|
-
export class SVGService {
|
|
13
|
-
private static instance: SVGService;
|
|
14
|
-
private activeWatchers: Set<string> = new Set();
|
|
15
|
-
public lockService: LockService;
|
|
16
|
-
|
|
17
|
-
private constructor() {
|
|
18
|
-
this.lockService = LockService.getInstance();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
public static getInstance(): SVGService {
|
|
22
|
-
if (!SVGService.instance) {
|
|
23
|
-
SVGService.instance = new SVGService();
|
|
24
|
-
}
|
|
25
|
-
return SVGService.instance;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Build all SVG files from source to output directory
|
|
30
|
-
*/
|
|
31
|
-
public async buildAll(options: BuildOptions): Promise<void> {
|
|
32
|
-
logger.info('Starting SVG build process');
|
|
33
|
-
logger.info(`Source: ${options.src}`);
|
|
34
|
-
logger.info(`Output: ${options.out}`);
|
|
35
|
-
|
|
36
|
-
const srcDir = path.resolve(options.src);
|
|
37
|
-
const outDir = path.resolve(options.out);
|
|
38
|
-
|
|
39
|
-
// Validate source directory
|
|
40
|
-
if (!(await FileSystem.exists(srcDir))) {
|
|
41
|
-
throw new Error(`Source folder not found: ${srcDir}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Ensure output directory exists
|
|
45
|
-
await FileSystem.ensureDir(outDir);
|
|
46
|
-
|
|
47
|
-
// Get configuration - merge config file with options
|
|
48
|
-
const config = configService.readConfig();
|
|
49
|
-
const mergedConfig = {
|
|
50
|
-
...config,
|
|
51
|
-
...(options.config || {}),
|
|
52
|
-
// Support direct properties on options for CLI convenience
|
|
53
|
-
...(options as any).framework && { framework: (options as any).framework },
|
|
54
|
-
...(options as any).typescript !== undefined && { typescript: (options as any).typescript },
|
|
55
|
-
...(options as any).frameworkOptions && { frameworkOptions: (options as any).frameworkOptions }
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Read all SVG files
|
|
59
|
-
const files = await FileSystem.readDir(srcDir);
|
|
60
|
-
const svgFiles = files.filter((file: string) => file.endsWith('.svg'));
|
|
61
|
-
|
|
62
|
-
if (svgFiles.length === 0) {
|
|
63
|
-
logger.warn('No SVG files found in source directory');
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
logger.info(`Found ${svgFiles.length} SVG files to process`);
|
|
68
|
-
|
|
69
|
-
const results: Array<{ success: boolean; file: string; error?: Error }> = [];
|
|
70
|
-
|
|
71
|
-
// Process each SVG file
|
|
72
|
-
for (const file of svgFiles) {
|
|
73
|
-
const svgPath = path.join(srcDir, file);
|
|
74
|
-
|
|
75
|
-
// Check if file is locked
|
|
76
|
-
if (this.lockService.isLocked(svgPath)) {
|
|
77
|
-
logger.warn(`Skipped locked file: ${file}`);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const processingResult = await svgProcessor.processSVGFile(svgPath, outDir, {
|
|
83
|
-
framework: mergedConfig.framework,
|
|
84
|
-
typescript: mergedConfig.typescript,
|
|
85
|
-
frameworkOptions: mergedConfig.frameworkOptions,
|
|
86
|
-
defaultWidth: mergedConfig.defaultWidth,
|
|
87
|
-
defaultHeight: mergedConfig.defaultHeight,
|
|
88
|
-
defaultFill: mergedConfig.defaultFill,
|
|
89
|
-
styleRules: Object.fromEntries(
|
|
90
|
-
Object.entries(mergedConfig.styleRules || {}).filter(([_, v]) => v !== undefined)
|
|
91
|
-
) as Record<string, string>
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
results.push({
|
|
95
|
-
success: processingResult.success,
|
|
96
|
-
file,
|
|
97
|
-
error: processingResult.error
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
} catch (error) {
|
|
101
|
-
logger.error(`Failed to process ${file}:`, error);
|
|
102
|
-
results.push({
|
|
103
|
-
success: false,
|
|
104
|
-
file,
|
|
105
|
-
error: error as Error
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Log summary
|
|
111
|
-
const successful = results.filter(r => r.success).length;
|
|
112
|
-
const failed = results.filter(r => !r.success).length;
|
|
113
|
-
|
|
114
|
-
logger.info(`Build complete: ${successful} successful, ${failed} failed`);
|
|
115
|
-
|
|
116
|
-
if (failed > 0) {
|
|
117
|
-
logger.warn('Some files failed to process:');
|
|
118
|
-
results.filter(r => !r.success).forEach(r => {
|
|
119
|
-
logger.warn(` - ${r.file}: ${r.error?.message}`);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Generate index.ts file with all component exports
|
|
124
|
-
if (successful > 0) {
|
|
125
|
-
await this.generateIndexFile(outDir, results.filter(r => r.success).map(r => r.file));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Generate a React component from a single SVG file
|
|
131
|
-
*/
|
|
132
|
-
public async generateSingle(options: GenerateOptions): Promise<void> {
|
|
133
|
-
logger.info(`Generating component from: ${options.svgFile}`);
|
|
134
|
-
|
|
135
|
-
const filePath = path.resolve(options.svgFile);
|
|
136
|
-
const outDir = path.resolve(options.outDir);
|
|
137
|
-
|
|
138
|
-
// Validate SVG file
|
|
139
|
-
if (!(await FileSystem.exists(filePath))) {
|
|
140
|
-
throw new Error(`SVG file not found: ${filePath}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Check if file is locked
|
|
144
|
-
if (this.lockService.isLocked(filePath)) {
|
|
145
|
-
logger.warn(`File is locked: ${path.basename(options.svgFile)}`);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Get configuration
|
|
150
|
-
const config = configService.readConfig();
|
|
151
|
-
const mergedConfig = { ...config, ...options.config };
|
|
152
|
-
|
|
153
|
-
// Process the file
|
|
154
|
-
const result = await svgProcessor.processSVGFile(filePath, outDir, {
|
|
155
|
-
defaultWidth: mergedConfig.defaultWidth,
|
|
156
|
-
defaultHeight: mergedConfig.defaultHeight,
|
|
157
|
-
defaultFill: mergedConfig.defaultFill,
|
|
158
|
-
styleRules: Object.fromEntries(
|
|
159
|
-
Object.entries(mergedConfig.styleRules || {}).filter(([_, v]) => v !== undefined)
|
|
160
|
-
) as Record<string, string>
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
if (!result.success) {
|
|
164
|
-
throw result.error || new Error('Failed to generate component');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
logger.success(`Component generated: ${result.componentName}`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Start watching SVG files for changes
|
|
172
|
-
*/
|
|
173
|
-
public async startWatching(options: WatchOptions): Promise<string> {
|
|
174
|
-
logger.info('Starting watch mode');
|
|
175
|
-
logger.info(`Watching: ${options.src}`);
|
|
176
|
-
logger.info(`Output: ${options.out}`);
|
|
177
|
-
|
|
178
|
-
const srcDir = path.resolve(options.src);
|
|
179
|
-
const outDir = path.resolve(options.out);
|
|
180
|
-
|
|
181
|
-
// Validate source directory
|
|
182
|
-
if (!(await FileSystem.exists(srcDir))) {
|
|
183
|
-
throw new Error(`Source folder not found: ${srcDir}`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Start watching
|
|
187
|
-
const watchId = await fileWatcher.watchDirectory(srcDir, options);
|
|
188
|
-
this.activeWatchers.add(watchId);
|
|
189
|
-
|
|
190
|
-
// Register event handler
|
|
191
|
-
fileWatcher.onFileEvent(watchId, async (event: FileWatchEvent) => {
|
|
192
|
-
await this.handleWatchEvent(event, outDir, options.config);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
logger.success(`Watch mode active - waiting for file changes...`);
|
|
196
|
-
return watchId;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Handle file watch events
|
|
201
|
-
*/
|
|
202
|
-
private async handleWatchEvent(
|
|
203
|
-
event: FileWatchEvent,
|
|
204
|
-
outDir: string,
|
|
205
|
-
config?: Partial<any>
|
|
206
|
-
): Promise<void> {
|
|
207
|
-
const fileName = path.basename(event.filePath);
|
|
208
|
-
|
|
209
|
-
switch (event.type) {
|
|
210
|
-
case 'add':
|
|
211
|
-
logger.info(`New SVG detected: ${fileName}`);
|
|
212
|
-
await this.processWatchedFile(event.filePath, outDir, config);
|
|
213
|
-
break;
|
|
214
|
-
|
|
215
|
-
case 'change':
|
|
216
|
-
logger.info(`SVG updated: ${fileName}`);
|
|
217
|
-
await this.processWatchedFile(event.filePath, outDir, config);
|
|
218
|
-
break;
|
|
219
|
-
|
|
220
|
-
case 'unlink':
|
|
221
|
-
logger.info(`SVG removed: ${fileName}`);
|
|
222
|
-
await this.handleFileRemoval(event.filePath, outDir);
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Process a watched file
|
|
229
|
-
*/
|
|
230
|
-
private async processWatchedFile(
|
|
231
|
-
filePath: string,
|
|
232
|
-
outDir: string,
|
|
233
|
-
config?: Partial<any>
|
|
234
|
-
): Promise<void> {
|
|
235
|
-
try {
|
|
236
|
-
// Check if file is locked
|
|
237
|
-
if (this.lockService.isLocked(filePath)) {
|
|
238
|
-
logger.warn(`Skipped locked file: ${path.basename(filePath)}`);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Get configuration
|
|
243
|
-
const fullConfig = configService.readConfig();
|
|
244
|
-
const mergedConfig = { ...fullConfig, ...config };
|
|
245
|
-
|
|
246
|
-
// Process the file
|
|
247
|
-
await svgProcessor.processSVGFile(filePath, outDir, {
|
|
248
|
-
defaultWidth: mergedConfig.defaultWidth,
|
|
249
|
-
defaultHeight: mergedConfig.defaultHeight,
|
|
250
|
-
defaultFill: mergedConfig.defaultFill,
|
|
251
|
-
styleRules: Object.fromEntries(
|
|
252
|
-
Object.entries(mergedConfig.styleRules || {}).filter(([_, v]) => v !== undefined)
|
|
253
|
-
) as Record<string, string>
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
} catch (error) {
|
|
257
|
-
logger.error(`Failed to process watched file ${path.basename(filePath)}:`, error);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Handle file removal in watch mode
|
|
263
|
-
*/
|
|
264
|
-
private async handleFileRemoval(filePath: string, outDir: string): Promise<void> {
|
|
265
|
-
try {
|
|
266
|
-
const componentName = svgProcessor.generateComponentName(path.basename(filePath));
|
|
267
|
-
const componentPath = path.join(outDir, `${componentName}.tsx`);
|
|
268
|
-
|
|
269
|
-
if (await FileSystem.exists(componentPath)) {
|
|
270
|
-
await FileSystem.unlink(componentPath);
|
|
271
|
-
logger.success(`Removed component: ${componentName}.tsx`);
|
|
272
|
-
}
|
|
273
|
-
} catch (error) {
|
|
274
|
-
logger.error(`Failed to remove component for ${path.basename(filePath)}:`, error);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Stop watching files
|
|
280
|
-
*/
|
|
281
|
-
public stopWatching(watchId?: string): void {
|
|
282
|
-
if (watchId) {
|
|
283
|
-
fileWatcher.stopWatching(watchId);
|
|
284
|
-
this.activeWatchers.delete(watchId);
|
|
285
|
-
} else {
|
|
286
|
-
// Stop all watchers
|
|
287
|
-
for (const id of this.activeWatchers) {
|
|
288
|
-
fileWatcher.stopWatching(id);
|
|
289
|
-
}
|
|
290
|
-
this.activeWatchers.clear();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Clean output directory
|
|
296
|
-
*/
|
|
297
|
-
public async clean(outDir: string): Promise<void> {
|
|
298
|
-
const targetDir = path.resolve(outDir);
|
|
299
|
-
|
|
300
|
-
if (!(await FileSystem.exists(targetDir))) {
|
|
301
|
-
logger.warn(`Directory not found: ${targetDir}`);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
await FileSystem.emptyDir(targetDir);
|
|
306
|
-
logger.success(`Cleaned all generated SVG components in: ${targetDir}`);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Generate index.ts file with all component exports
|
|
311
|
-
*/
|
|
312
|
-
private async generateIndexFile(outDir: string, svgFiles: string[]): Promise<void> {
|
|
313
|
-
try {
|
|
314
|
-
const componentNames = svgFiles.map(file => {
|
|
315
|
-
const baseName = path.basename(file, '.svg');
|
|
316
|
-
return svgProcessor.generateComponentName(baseName);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
const indexContent = this.generateIndexContent(componentNames);
|
|
320
|
-
const indexPath = path.join(outDir, 'index.ts');
|
|
321
|
-
|
|
322
|
-
await FileSystem.writeFile(indexPath, indexContent, 'utf-8');
|
|
323
|
-
logger.success(`Generated index.ts with ${componentNames.length} component exports`);
|
|
324
|
-
} catch (error) {
|
|
325
|
-
logger.error('Failed to generate index.ts:', error);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Generate the content for index.ts file
|
|
331
|
-
*/
|
|
332
|
-
private generateIndexContent(componentNames: string[]): string {
|
|
333
|
-
const imports = componentNames.map(name => `export { default as ${name} } from './${name}';`).join('\n');
|
|
334
|
-
|
|
335
|
-
const exportAll = `
|
|
336
|
-
// Export all components
|
|
337
|
-
export {
|
|
338
|
-
${componentNames.map(name => ` ${name},`).join('\n')}
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
// Re-export for convenience
|
|
342
|
-
export default {
|
|
343
|
-
${componentNames.map(name => ` ${name},`).join('\n')}
|
|
344
|
-
};
|
|
345
|
-
`;
|
|
346
|
-
|
|
347
|
-
return `/**
|
|
348
|
-
* SVG Components Index
|
|
349
|
-
* Generated by svger-cli
|
|
350
|
-
*
|
|
351
|
-
* Import individual components:
|
|
352
|
-
* import { ${componentNames[0] || 'ComponentName'} } from './components';
|
|
353
|
-
*
|
|
354
|
-
* Import all components:
|
|
355
|
-
* import * as Icons from './components';
|
|
356
|
-
* import Icons from './components'; // default export
|
|
357
|
-
*/
|
|
358
|
-
|
|
359
|
-
${imports}${exportAll}`;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Get service statistics
|
|
364
|
-
*/
|
|
365
|
-
public getStats(): {
|
|
366
|
-
activeWatchers: number;
|
|
367
|
-
processingQueue: any;
|
|
368
|
-
watcherStats: any;
|
|
369
|
-
} {
|
|
370
|
-
return {
|
|
371
|
-
activeWatchers: this.activeWatchers.size,
|
|
372
|
-
processingQueue: svgProcessor.getProcessingStats(),
|
|
373
|
-
watcherStats: fileWatcher.getWatchStats()
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Shutdown service
|
|
379
|
-
*/
|
|
380
|
-
public shutdown(): void {
|
|
381
|
-
this.stopWatching();
|
|
382
|
-
fileWatcher.shutdown();
|
|
383
|
-
svgProcessor.clearQueue();
|
|
384
|
-
logger.info('SVG service shutdown complete');
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Simple file locking service
|
|
390
|
-
*/
|
|
391
|
-
export class LockService {
|
|
392
|
-
private static instance: LockService;
|
|
393
|
-
private static readonly LOCK_FILE = '.svg-lock';
|
|
394
|
-
private cachedLocks: Set<string> | null = null;
|
|
395
|
-
|
|
396
|
-
private constructor() {}
|
|
397
|
-
|
|
398
|
-
public static getInstance(): LockService {
|
|
399
|
-
if (!LockService.instance) {
|
|
400
|
-
LockService.instance = new LockService();
|
|
401
|
-
}
|
|
402
|
-
return LockService.instance;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
private getLockFilePath(): string {
|
|
406
|
-
return path.resolve(LockService.LOCK_FILE);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
private readLockFile(): Set<string> {
|
|
410
|
-
if (this.cachedLocks) {
|
|
411
|
-
return this.cachedLocks;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
try {
|
|
415
|
-
const data = FileSystem.readJSONSync(this.getLockFilePath());
|
|
416
|
-
this.cachedLocks = new Set(Array.isArray(data) ? data : []);
|
|
417
|
-
return this.cachedLocks;
|
|
418
|
-
} catch {
|
|
419
|
-
this.cachedLocks = new Set();
|
|
420
|
-
return this.cachedLocks;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
private writeLockFile(locks: Set<string>): void {
|
|
425
|
-
try {
|
|
426
|
-
FileSystem.writeJSONSync(this.getLockFilePath(), Array.from(locks), { spaces: 2 });
|
|
427
|
-
this.cachedLocks = locks;
|
|
428
|
-
} catch (error) {
|
|
429
|
-
logger.error('Failed to write lock file:', error);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
public lockFiles(files: string[]): void {
|
|
434
|
-
const fileNames = files.map(f => path.basename(f));
|
|
435
|
-
const current = this.readLockFile();
|
|
436
|
-
|
|
437
|
-
for (const fileName of fileNames) {
|
|
438
|
-
current.add(fileName);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
this.writeLockFile(current);
|
|
442
|
-
logger.success(`Locked files: ${fileNames.join(', ')}`);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
public unlockFiles(files: string[]): void {
|
|
446
|
-
const fileNames = files.map(f => path.basename(f));
|
|
447
|
-
const current = this.readLockFile();
|
|
448
|
-
|
|
449
|
-
for (const fileName of fileNames) {
|
|
450
|
-
current.delete(fileName);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
this.writeLockFile(current);
|
|
454
|
-
logger.success(`Unlocked files: ${fileNames.join(', ')}`);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
public isLocked(file: string): boolean {
|
|
458
|
-
const locks = this.readLockFile();
|
|
459
|
-
return locks.has(path.basename(file));
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
public clearCache(): void {
|
|
463
|
-
this.cachedLocks = null;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Export singleton instance
|
|
468
|
-
export const svgService = SVGService.getInstance();
|