svger-cli 1.0.0
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/.svg-lock +1 -0
- package/README.md +121 -0
- package/bin/svg-tool.js +2 -0
- package/dist/builder.d.ts +24 -0
- package/dist/builder.js +83 -0
- package/dist/clean.d.ts +8 -0
- package/dist/clean.js +18 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +124 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +74 -0
- package/dist/lock.d.ts +19 -0
- package/dist/lock.js +69 -0
- package/dist/templates/ComponentTemplate.d.ts +35 -0
- package/dist/templates/ComponentTemplate.js +48 -0
- package/dist/watch.d.ts +21 -0
- package/dist/watch.js +77 -0
- package/docs/ADR-SVG-INTRGRATION-METHODS-001.adr.md +157 -0
- package/docs/TDR-SVG-INTRGRATION-METHODS-001.tdr.md +115 -0
- package/package.json +34 -0
- package/src/builder.ts +105 -0
- package/src/clean.ts +21 -0
- package/src/cli.ts +132 -0
- package/src/config.ts +82 -0
- package/src/lock.ts +74 -0
- package/src/templates/ComponentTemplate.ts +64 -0
- package/src/watch.ts +88 -0
- package/tsconfig.json +16 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const CONFIG_FILE = ".svgconfig.json";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the absolute path to the configuration file.
|
|
8
|
+
*
|
|
9
|
+
* @returns {string} Absolute path to .svgconfig.json
|
|
10
|
+
*/
|
|
11
|
+
function getConfigPath(): string {
|
|
12
|
+
return path.resolve(CONFIG_FILE);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read the current svger-cli configuration.
|
|
17
|
+
*
|
|
18
|
+
* @returns {Record<string, any>} Configuration object. Returns an empty object if no config file exists.
|
|
19
|
+
*/
|
|
20
|
+
export function readConfig(): Record<string, any> {
|
|
21
|
+
if (!fs.existsSync(getConfigPath())) return {};
|
|
22
|
+
return fs.readJSONSync(getConfigPath());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Write a configuration object to the config file.
|
|
27
|
+
*
|
|
28
|
+
* @param {Record<string, any>} config - Configuration object to write.
|
|
29
|
+
*/
|
|
30
|
+
export function writeConfig(config: Record<string, any>) {
|
|
31
|
+
fs.writeJSONSync(getConfigPath(), config, { spaces: 2 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize the svger-cli configuration with default values.
|
|
36
|
+
* If a config file already exists, this function will not overwrite it.
|
|
37
|
+
*/
|
|
38
|
+
export function initConfig() {
|
|
39
|
+
if (fs.existsSync(getConfigPath())) {
|
|
40
|
+
console.log("⚠️ Config file already exists:", getConfigPath());
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const defaultConfig = {
|
|
45
|
+
source: "./src/assets/svg",
|
|
46
|
+
output: "./src/components/icons",
|
|
47
|
+
watch: false,
|
|
48
|
+
defaultWidth: 24,
|
|
49
|
+
defaultHeight: 24,
|
|
50
|
+
defaultFill: "currentColor",
|
|
51
|
+
exclude: [] as string[],
|
|
52
|
+
styleRules: {
|
|
53
|
+
fill: "inherit",
|
|
54
|
+
stroke: "none",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
writeConfig(defaultConfig);
|
|
59
|
+
console.log("✅ Config file created:", getConfigPath());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Set a specific configuration key to a new value.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} key - The config key to set.
|
|
66
|
+
* @param {any} value - The value to assign to the key.
|
|
67
|
+
*/
|
|
68
|
+
export function setConfig(key: string, value: any) {
|
|
69
|
+
const config = readConfig();
|
|
70
|
+
config[key] = value;
|
|
71
|
+
writeConfig(config);
|
|
72
|
+
console.log(`✅ Set config ${key}=${value}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Display the current configuration in the console.
|
|
77
|
+
*/
|
|
78
|
+
export function showConfig() {
|
|
79
|
+
const config = readConfig();
|
|
80
|
+
console.log("📄 Current Config:");
|
|
81
|
+
console.log(JSON.stringify(config, null, 2));
|
|
82
|
+
}
|
package/src/lock.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const LOCK_FILE = ".svg-lock";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the absolute path to the lock file.
|
|
8
|
+
*
|
|
9
|
+
* @returns {string} Absolute path to .svg-lock
|
|
10
|
+
*/
|
|
11
|
+
function getLockFilePath(): string {
|
|
12
|
+
return path.resolve(LOCK_FILE);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read the current locked SVG files from the lock file.
|
|
17
|
+
*
|
|
18
|
+
* @returns {string[]} Array of locked SVG file names.
|
|
19
|
+
*/
|
|
20
|
+
function readLockFile(): string[] {
|
|
21
|
+
if (!fs.existsSync(getLockFilePath())) return [];
|
|
22
|
+
try {
|
|
23
|
+
const data = fs.readFileSync(getLockFilePath(), "utf-8");
|
|
24
|
+
return JSON.parse(data);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Write the list of locked SVG files to the lock file.
|
|
32
|
+
*
|
|
33
|
+
* @param {string[]} files - Array of SVG file names to lock.
|
|
34
|
+
*/
|
|
35
|
+
function writeLockFile(files: string[]) {
|
|
36
|
+
fs.writeFileSync(getLockFilePath(), JSON.stringify(files, null, 2), "utf-8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Lock one or more SVG files to prevent them from being processed.
|
|
41
|
+
*
|
|
42
|
+
* @param {string[]} files - Paths to SVG files to lock.
|
|
43
|
+
*/
|
|
44
|
+
export function lockFiles(files: string[]) {
|
|
45
|
+
const fileNames = files.map(f => path.basename(f));
|
|
46
|
+
const current = readLockFile();
|
|
47
|
+
const newFiles = Array.from(new Set([...current, ...fileNames]));
|
|
48
|
+
writeLockFile(newFiles);
|
|
49
|
+
console.log(`🔒 Locked files: ${newFiles.join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Unlock one or more SVG files, allowing them to be processed again.
|
|
54
|
+
*
|
|
55
|
+
* @param {string[]} files - Paths to SVG files to unlock.
|
|
56
|
+
*/
|
|
57
|
+
export function unlockFiles(files: string[]) {
|
|
58
|
+
const fileNames = files.map(f => path.basename(f));
|
|
59
|
+
const current = readLockFile();
|
|
60
|
+
const remaining = current.filter(f => !fileNames.includes(f));
|
|
61
|
+
writeLockFile(remaining);
|
|
62
|
+
console.log(`🔓 Unlocked files: ${fileNames.join(", ")}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a specific SVG file is locked.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} file - Path to the SVG file to check.
|
|
69
|
+
* @returns {boolean} True if the file is locked, false otherwise.
|
|
70
|
+
*/
|
|
71
|
+
export function isLocked(file: string): boolean {
|
|
72
|
+
const current = readLockFile();
|
|
73
|
+
return current.includes(path.basename(file));
|
|
74
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a React functional component string from an SVG file's content.
|
|
3
|
+
*
|
|
4
|
+
* This template replaces XML/DOCTYPE declarations, cleans up formatting,
|
|
5
|
+
* and injects React props (`width`, `height`, `fill`, and any others via `...props`)
|
|
6
|
+
* directly into the root `<svg>` tag.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} params - Template generation parameters.
|
|
9
|
+
* @param {string} params.componentName - The name of the generated React component.
|
|
10
|
+
* @param {string} params.svgContent - The raw SVG markup to transform into a React component.
|
|
11
|
+
* @param {number} [params.defaultWidth=24] - Default width of the SVG (used if none is provided via props).
|
|
12
|
+
* @param {number} [params.defaultHeight=24] - Default height of the SVG (used if none is provided via props).
|
|
13
|
+
* @param {string} [params.defaultFill="currentColor"] - Default fill color of the SVG.
|
|
14
|
+
*
|
|
15
|
+
* @returns {string} The complete TypeScript React component code as a string.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const svg = '<svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z"/></svg>';
|
|
19
|
+
* const componentCode = reactTemplate({
|
|
20
|
+
* componentName: "MyIcon",
|
|
21
|
+
* svgContent: svg,
|
|
22
|
+
* defaultWidth: 32,
|
|
23
|
+
* defaultHeight: 32,
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Result: a ready-to-write .tsx file containing a typed React component
|
|
27
|
+
* console.log(componentCode);
|
|
28
|
+
*/
|
|
29
|
+
export function reactTemplate({
|
|
30
|
+
componentName,
|
|
31
|
+
svgContent,
|
|
32
|
+
defaultWidth = 24,
|
|
33
|
+
defaultHeight = 24,
|
|
34
|
+
defaultFill = "currentColor",
|
|
35
|
+
}: {
|
|
36
|
+
componentName: string;
|
|
37
|
+
svgContent: string;
|
|
38
|
+
defaultWidth?: number;
|
|
39
|
+
defaultHeight?: number;
|
|
40
|
+
defaultFill?: string;
|
|
41
|
+
}) {
|
|
42
|
+
const cleaned = svgContent
|
|
43
|
+
.replace(/<\?xml.*?\?>/g, "") // Remove XML declarations
|
|
44
|
+
.replace(/<!DOCTYPE.*?>/g, "") // Remove DOCTYPE lines
|
|
45
|
+
.replace(/\r?\n|\r/g, "") // Remove newlines
|
|
46
|
+
.trim();
|
|
47
|
+
|
|
48
|
+
return `import * as React from "react";
|
|
49
|
+
|
|
50
|
+
export const ${componentName}: React.FC<React.SVGProps<SVGSVGElement>> = ({
|
|
51
|
+
width = ${defaultWidth},
|
|
52
|
+
height = ${defaultHeight},
|
|
53
|
+
fill = "${defaultFill}",
|
|
54
|
+
...props
|
|
55
|
+
}) => (
|
|
56
|
+
${cleaned.replace(
|
|
57
|
+
/<svg/,
|
|
58
|
+
`<svg width={width} height={height} fill={fill} {...props}`
|
|
59
|
+
)}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export default ${componentName};
|
|
63
|
+
`;
|
|
64
|
+
}
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chokidar from "chokidar";
|
|
4
|
+
import { generateSVG } from "./builder.js";
|
|
5
|
+
import { isLocked } from "./lock.js";
|
|
6
|
+
import { readConfig } from "./config.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Watches a source folder for changes to SVG files and automatically
|
|
10
|
+
* rebuilds React components when SVGs are added, modified, or deleted.
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} config - Watch configuration object.
|
|
13
|
+
* @param {string} config.src - Source folder containing SVG files to watch.
|
|
14
|
+
* @param {string} config.out - Output folder where React components are generated.
|
|
15
|
+
* @returns {import("chokidar").FSWatcher} A chokidar file watcher instance.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* watchSVGs({ src: "./src/assets/svg", out: "./src/components/icons" });
|
|
19
|
+
*
|
|
20
|
+
* // Watches the SVG folder and:
|
|
21
|
+
* // - Generates new components when files are added.
|
|
22
|
+
* // - Updates components when files change.
|
|
23
|
+
* // - Removes components when SVGs are deleted.
|
|
24
|
+
*/
|
|
25
|
+
export function watchSVGs(config: { src: string; out: string }) {
|
|
26
|
+
const srcDir = path.resolve(config.src);
|
|
27
|
+
const outDir = path.resolve(config.out);
|
|
28
|
+
const svgConfig = readConfig();
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(srcDir)) {
|
|
31
|
+
console.error("❌ Source folder not found:", srcDir);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`👀 Watching for SVG changes in: ${srcDir}`);
|
|
36
|
+
console.log("🚀 Watch mode active — waiting for file changes...");
|
|
37
|
+
|
|
38
|
+
const watcher = chokidar.watch(srcDir, {
|
|
39
|
+
persistent: true,
|
|
40
|
+
ignoreInitial: false,
|
|
41
|
+
depth: 0,
|
|
42
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
|
|
43
|
+
ignored: /(^|[\/\\])\../, // Ignore hidden files
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ---- Handle new SVG files ----
|
|
47
|
+
watcher.on("add", async (filePath) => {
|
|
48
|
+
if (path.extname(filePath) !== ".svg") return;
|
|
49
|
+
console.log("Detected new file:", filePath);
|
|
50
|
+
|
|
51
|
+
if (isLocked(filePath)) {
|
|
52
|
+
console.log(`⚠️ Skipped locked file: ${path.basename(filePath)}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`➕ New SVG detected: ${path.basename(filePath)}`);
|
|
57
|
+
await generateSVG({ svgFile: filePath, outDir });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ---- Handle modified SVG files ----
|
|
61
|
+
watcher.on("change", async (filePath) => {
|
|
62
|
+
if (path.extname(filePath) !== ".svg") return;
|
|
63
|
+
console.log("Detected change in file:", filePath);
|
|
64
|
+
|
|
65
|
+
if (isLocked(filePath)) {
|
|
66
|
+
console.log(`⚠️ Skipped locked file: ${path.basename(filePath)}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`✏️ SVG updated: ${path.basename(filePath)}`);
|
|
71
|
+
await generateSVG({ svgFile: filePath, outDir });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ---- Handle deleted SVG files ----
|
|
75
|
+
watcher.on("unlink", async (filePath) => {
|
|
76
|
+
if (path.extname(filePath) !== ".svg") return;
|
|
77
|
+
|
|
78
|
+
const componentName = path.basename(filePath, ".svg");
|
|
79
|
+
const outFile = path.join(outDir, `${componentName}.tsx`);
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(outFile)) {
|
|
82
|
+
await fs.remove(outFile);
|
|
83
|
+
console.log(`🗑️ Removed component: ${componentName}.tsx`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return watcher;
|
|
88
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|
|
16
|
+
|