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/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watches a source folder for changes to SVG files and automatically
|
|
3
|
+
* rebuilds React components when SVGs are added, modified, or deleted.
|
|
4
|
+
*
|
|
5
|
+
* @param {Object} config - Watch configuration object.
|
|
6
|
+
* @param {string} config.src - Source folder containing SVG files to watch.
|
|
7
|
+
* @param {string} config.out - Output folder where React components are generated.
|
|
8
|
+
* @returns {import("chokidar").FSWatcher} A chokidar file watcher instance.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* watchSVGs({ src: "./src/assets/svg", out: "./src/components/icons" });
|
|
12
|
+
*
|
|
13
|
+
* // Watches the SVG folder and:
|
|
14
|
+
* // - Generates new components when files are added.
|
|
15
|
+
* // - Updates components when files change.
|
|
16
|
+
* // - Removes components when SVGs are deleted.
|
|
17
|
+
*/
|
|
18
|
+
export declare function watchSVGs(config: {
|
|
19
|
+
src: string;
|
|
20
|
+
out: string;
|
|
21
|
+
}): import("chokidar").FSWatcher;
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
* Watches a source folder for changes to SVG files and automatically
|
|
9
|
+
* rebuilds React components when SVGs are added, modified, or deleted.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} config - Watch configuration object.
|
|
12
|
+
* @param {string} config.src - Source folder containing SVG files to watch.
|
|
13
|
+
* @param {string} config.out - Output folder where React components are generated.
|
|
14
|
+
* @returns {import("chokidar").FSWatcher} A chokidar file watcher instance.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* watchSVGs({ src: "./src/assets/svg", out: "./src/components/icons" });
|
|
18
|
+
*
|
|
19
|
+
* // Watches the SVG folder and:
|
|
20
|
+
* // - Generates new components when files are added.
|
|
21
|
+
* // - Updates components when files change.
|
|
22
|
+
* // - Removes components when SVGs are deleted.
|
|
23
|
+
*/
|
|
24
|
+
export function watchSVGs(config) {
|
|
25
|
+
const srcDir = path.resolve(config.src);
|
|
26
|
+
const outDir = path.resolve(config.out);
|
|
27
|
+
const svgConfig = readConfig();
|
|
28
|
+
if (!fs.existsSync(srcDir)) {
|
|
29
|
+
console.error("โ Source folder not found:", srcDir);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
console.log(`๐ Watching for SVG changes in: ${srcDir}`);
|
|
33
|
+
console.log("๐ Watch mode active โ waiting for file changes...");
|
|
34
|
+
const watcher = chokidar.watch(srcDir, {
|
|
35
|
+
persistent: true,
|
|
36
|
+
ignoreInitial: false,
|
|
37
|
+
depth: 0,
|
|
38
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
|
|
39
|
+
ignored: /(^|[\/\\])\../, // Ignore hidden files
|
|
40
|
+
});
|
|
41
|
+
// ---- Handle new SVG files ----
|
|
42
|
+
watcher.on("add", async (filePath) => {
|
|
43
|
+
if (path.extname(filePath) !== ".svg")
|
|
44
|
+
return;
|
|
45
|
+
console.log("Detected new file:", filePath);
|
|
46
|
+
if (isLocked(filePath)) {
|
|
47
|
+
console.log(`โ ๏ธ Skipped locked file: ${path.basename(filePath)}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(`โ New SVG detected: ${path.basename(filePath)}`);
|
|
51
|
+
await generateSVG({ svgFile: filePath, outDir });
|
|
52
|
+
});
|
|
53
|
+
// ---- Handle modified SVG files ----
|
|
54
|
+
watcher.on("change", async (filePath) => {
|
|
55
|
+
if (path.extname(filePath) !== ".svg")
|
|
56
|
+
return;
|
|
57
|
+
console.log("Detected change in file:", filePath);
|
|
58
|
+
if (isLocked(filePath)) {
|
|
59
|
+
console.log(`โ ๏ธ Skipped locked file: ${path.basename(filePath)}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log(`โ๏ธ SVG updated: ${path.basename(filePath)}`);
|
|
63
|
+
await generateSVG({ svgFile: filePath, outDir });
|
|
64
|
+
});
|
|
65
|
+
// ---- Handle deleted SVG files ----
|
|
66
|
+
watcher.on("unlink", async (filePath) => {
|
|
67
|
+
if (path.extname(filePath) !== ".svg")
|
|
68
|
+
return;
|
|
69
|
+
const componentName = path.basename(filePath, ".svg");
|
|
70
|
+
const outFile = path.join(outDir, `${componentName}.tsx`);
|
|
71
|
+
if (fs.existsSync(outFile)) {
|
|
72
|
+
await fs.remove(outFile);
|
|
73
|
+
console.log(`๐๏ธ Removed component: ${componentName}.tsx`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return watcher;
|
|
77
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
**ADR** How to implement correctly SVG Integration Methods in React
|
|
2
|
+
|
|
3
|
+
**Version: 1.0.0**
|
|
4
|
+
**Date: 10/14/2025**
|
|
5
|
+
**Author: Engineer Navid Rezadoost**
|
|
6
|
+
**TDR Document ID: [TDR-SVG-INTRGRATION-METHODS-001](https://docs.google.com/document/d/1b04_V01xOvLiSMzuPdaRynANlnt2wYdJ_vjs9MAqtn4/edit?tab=t.0)**
|
|
7
|
+
**Status**: Proposed
|
|
8
|
+
|
|
9
|
+
**Context**
|
|
10
|
+
This service is responsible for managing svg files. Given the documentation provided in the TDR for implementing Method 3, which covers the weaknesses of svgr, it was decided to release this package as an open source service on npm.
|
|
11
|
+
|
|
12
|
+
**Similar Weaknesses That We Are Looking To Solve In This Process**
|
|
13
|
+
|
|
14
|
+
* Add more commands for more control and mastery over svg file management
|
|
15
|
+
* Add different commands for rendering and rebuilding svg components with more control over files that we do not want to be rebuilt
|
|
16
|
+
* Add a command to create a component for new files that are added
|
|
17
|
+
* A command to componentize a specific svg
|
|
18
|
+
* Add settings for files that should remain unchanged due to specific styling
|
|
19
|
+
* Introduce the folder and run the build as \--watch as soon as a new svg file is added to the specified folder, the svg will be built automatically
|
|
20
|
+
* Create a config file or command on the command line to specify the source file to monitor or process and specify the destination directory
|
|
21
|
+
* Specify rules for building components in width and height dimensions and even styles that should be set as default
|
|
22
|
+
|
|
23
|
+
#### **Decision**
|
|
24
|
+
|
|
25
|
+
We will develop a custom SVG integration CLI and runtime service for React that:
|
|
26
|
+
|
|
27
|
+
* Watches directories for new SVG files.
|
|
28
|
+
* Generates React components automatically with flexible configuration.
|
|
29
|
+
* Allows selective rebuilds and fine-grained control over output styles and props.
|
|
30
|
+
|
|
31
|
+
#### **Rationale**
|
|
32
|
+
|
|
33
|
+
SVGR, while popular, has the following shortcomings:
|
|
34
|
+
|
|
35
|
+
* Lacks a flexible CLI API for selective component generation.
|
|
36
|
+
* No built-in `--watch` mode for automatic builds on new file additions.
|
|
37
|
+
* Limited support for default dimension and styling rules.
|
|
38
|
+
* Difficult to exclude or lock specific SVGs from rebuilds.
|
|
39
|
+
|
|
40
|
+
#### **Consequences**
|
|
41
|
+
|
|
42
|
+
* More control and automation for developers.
|
|
43
|
+
* Slightly increased setup complexity.
|
|
44
|
+
* Responsibility to maintain a custom build tool.
|
|
45
|
+
|
|
46
|
+
## **Example Configuration and Commands**
|
|
47
|
+
|
|
48
|
+
**Include examples so developers can easily visualize the usage.**
|
|
49
|
+
|
|
50
|
+
### Example `.svgconfig.json`
|
|
51
|
+
|
|
52
|
+
| { "source": "./src/assets/svg", "output": "./src/components/icons", "watch": true, "defaultWidth": "24", "defaultHeight": "24", "defaultFill": "currentColor", "exclude": \["logo.svg", "brand-icon.svg"\], "styleRules": { "fill": "inherit", "stroke": "none" }} |
|
|
53
|
+
| :---- |
|
|
54
|
+
|
|
55
|
+
**Commands**
|
|
56
|
+
|
|
57
|
+
**Build Command**
|
|
58
|
+
Builds React components from all SVG files in the specified source directory.
|
|
59
|
+
|
|
60
|
+
| svg-tool build \[options\] |
|
|
61
|
+
| :---- |
|
|
62
|
+
|
|
63
|
+
| svg-tool build \--src ./src/assets/svg \--out ./src/components/icons |
|
|
64
|
+
| :---- |
|
|
65
|
+
|
|
66
|
+
### **What It Does**
|
|
67
|
+
|
|
68
|
+
* Converts every `.svg` in the source folder to a React component.
|
|
69
|
+
* Applies rules from `.svgconfig.json` if present.
|
|
70
|
+
* Generates file names based on kebab-case or PascalCase (configurable).
|
|
71
|
+
|
|
72
|
+
**Watch Command**
|
|
73
|
+
Continuously watches the source directory for new or updated SVG files, and automatically builds them.
|
|
74
|
+
|
|
75
|
+
| svg-tool watch \[options\] |
|
|
76
|
+
| :---- |
|
|
77
|
+
|
|
78
|
+
| svg-tool watch \--src ./src/assets/svg \--out ./src/components/icons |
|
|
79
|
+
| :---- |
|
|
80
|
+
|
|
81
|
+
### **What It Does**
|
|
82
|
+
|
|
83
|
+
* Automatically rebuilds when an SVG file is added, updated, or removed.
|
|
84
|
+
* Skips locked SVG files.
|
|
85
|
+
* Ideal for active development environments.
|
|
86
|
+
|
|
87
|
+
## **Generate Command**
|
|
88
|
+
|
|
89
|
+
Converts a specific SVG file into a React component on demand.
|
|
90
|
+
|
|
91
|
+
| svg-tool generate \<svgFile\> \[options\] |
|
|
92
|
+
| :---- |
|
|
93
|
+
|
|
94
|
+
| svg-tool generate ./src/assets/svg/heart.svg \--out ./src/components/icons |
|
|
95
|
+
| :---- |
|
|
96
|
+
|
|
97
|
+
### **What It Does**
|
|
98
|
+
|
|
99
|
+
* Converts only the specified file.
|
|
100
|
+
* Ideal for adding individual icons to an existing collection.
|
|
101
|
+
|
|
102
|
+
## **Lock Command**
|
|
103
|
+
|
|
104
|
+
Locks one or more SVG files to prevent them from being rebuilt or overwritten during batch operations.
|
|
105
|
+
|
|
106
|
+
| svg-tool lock \<svgFile\> \[options\] |
|
|
107
|
+
| :---- |
|
|
108
|
+
|
|
109
|
+
| svg-tool lock ./src/assets/svg/logo.svg |
|
|
110
|
+
| :---- |
|
|
111
|
+
|
|
112
|
+
### **What It Does**
|
|
113
|
+
|
|
114
|
+
* Adds the specified file(s) to a `.svg-lock` or `.svgconfig.json` list.
|
|
115
|
+
* Protects specific SVGs with custom branding or styling.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## **Unlock Command**
|
|
119
|
+
|
|
120
|
+
Unlocks one or more SVG files previously marked as locked, allowing them to be rebuilt.
|
|
121
|
+
|
|
122
|
+
| svg-tool unlock \<svgFile\> \[options\] |
|
|
123
|
+
| :---- |
|
|
124
|
+
|
|
125
|
+
| svg-tool unlock ./src/assets/svg/logo.svg |
|
|
126
|
+
| :---- |
|
|
127
|
+
|
|
128
|
+
## **Config Command**
|
|
129
|
+
|
|
130
|
+
Creates or modifies a `.svgconfig.json` file with default settings for builds and watches.
|
|
131
|
+
|
|
132
|
+
| svg-tool config \[options\] |
|
|
133
|
+
| :---- |
|
|
134
|
+
|
|
135
|
+
| svg-tool config \--init |
|
|
136
|
+
| :---- |
|
|
137
|
+
|
|
138
|
+
**\--init**
|
|
139
|
+
|
|
140
|
+
| svg-tool config \--init |
|
|
141
|
+
| :---- |
|
|
142
|
+
|
|
143
|
+
**\--set \<key=value\>**
|
|
144
|
+
|
|
145
|
+
| svg-tool config \--set defaultWidth=32 |
|
|
146
|
+
| :---- |
|
|
147
|
+
|
|
148
|
+
**`--show`**
|
|
149
|
+
|
|
150
|
+
| svg-tool config \--show |
|
|
151
|
+
| :---- |
|
|
152
|
+
|
|
153
|
+
**`Full One-Line Command Closet (for Bash / Mac / Linux / PowerShell)`**
|
|
154
|
+
|
|
155
|
+
| svg-tool config \--init && svg-tool config \--set source=./src/assets/svg && svg-tool config \--set output=./src/components/icons && svg-tool config \--set watch=true && svg-tool config \--set defaultWidth=24 && svg-tool config \--set defaultHeight=24 && svg-tool config \--set defaultFill=currentColor && svg-tool lock ./src/assets/svg/logo.svg && svg-tool build \--src ./src/assets/svg \--out ./src/components/icons \--verbose && svg-tool generate ./src/assets/svg/new-icon.svg \--out ./src/components/icons && svg-tool unlock ./src/assets/svg/logo.svg && svg-tool watch \--src ./src/assets/svg \--out ./src/components/icons && svg-tool clean \--out ./src/components/icons |
|
|
156
|
+
| :---- |
|
|
157
|
+
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Technical Design Report: SVG Integration Methods in React (TDR)
|
|
2
|
+
|
|
3
|
+
# Version: 0.0.1
|
|
4
|
+
|
|
5
|
+
# Date: 10/13/25
|
|
6
|
+
|
|
7
|
+
# Owner: engineer Ehsan jafari
|
|
8
|
+
|
|
9
|
+
# Attendees: Navid Rezadoost , Faeze Mohadespor , Amir Bazgir , Ehsan Jafari
|
|
10
|
+
|
|
11
|
+
# Link : https://docs.google.com/document/d/1b04_V01xOvLiSMzuPdaRynANlnt2wYdJ_vjs9MAqtn4/edit?tab=t.0
|
|
12
|
+
|
|
13
|
+
## 1\. Introduction
|
|
14
|
+
|
|
15
|
+
This document compares five common methods for integrating and managing SVG icons in React projects. The goal is to provide a detailed guide for choosing the appropriate method considering **performance, maintainability, flexibility, and bundle size**.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2\. Methods
|
|
20
|
+
|
|
21
|
+
### Method 1: Loading SVGs from the Public Folder (Runtime Fetch)
|
|
22
|
+
|
|
23
|
+
SVG files are stored in public/assets/icons/svg/ and fetched at runtime.
|
|
24
|
+
|
|
25
|
+
| function SvgIcon({ name, ...props }) { const \[svg, setSvg\] \= React.useState(null); React.useEffect(() \=\> { fetch(\`/assets/icons/svg/${name}.svg\`) .then(res \=\> res.text()) .then(setSvg); }, \[name\]); return svg ? ( \<span {...props} dangerouslySetInnerHTML={{ \_\_html: svg }} /\> ) : null;} |
|
|
26
|
+
| :---- |
|
|
27
|
+
|
|
28
|
+
**Pros:** \- โ
Small bundle size \- โ
Easy icon replacement without rebuild \- โ
Suitable for dynamic icon sets
|
|
29
|
+
|
|
30
|
+
**Cons:** \- โ ๏ธ Network request at runtime (slower) \- โ ๏ธ Cannot directly apply props to SVG \- โ ๏ธ Requires async logic (useEffect \+ useState)
|
|
31
|
+
|
|
32
|
+
**Performance:** CPU low | RAM low | Build Time fast
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### Method 2: Using SVGR
|
|
37
|
+
|
|
38
|
+
SVG files are converted to React components with props.
|
|
39
|
+
|
|
40
|
+
| import { ReactComponent as StarIcon } from './icons/star.svg';export const Example \= () \=\> \<StarIcon width={24} height={24} fill="gold" /\>; |
|
|
41
|
+
| :---- |
|
|
42
|
+
|
|
43
|
+
**Pros:** \- โ
Direct prop manipulation \- โ
Easy bundler integration \- โ
Automatic SVG optimization
|
|
44
|
+
|
|
45
|
+
**Cons:** \- โ ๏ธ All SVGs included in bundle โ increases size \- โ ๏ธ Rebuild can overwrite manual changes \- โ ๏ธ Slightly longer build time
|
|
46
|
+
|
|
47
|
+
**Performance:** CPU moderate | RAM higher during build | Build Time slightly slower
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### Method 3: Custom svg-to-react Script
|
|
52
|
+
|
|
53
|
+
Automatically converts all SVG files to React components with new and all modes.
|
|
54
|
+
|
|
55
|
+
| yarn svg-to-react:all\# oryarn svg-to-react:new |
|
|
56
|
+
| :---- |
|
|
57
|
+
|
|
58
|
+
Script:
|
|
59
|
+
|
|
60
|
+
| import fs from "fs";import path from "path";import { fileURLToPath } from "url";const \_\_filename \= fileURLToPath(import.meta.url);const \_\_dirname \= path.dirname(\_\_filename);const inputDir \= path.join(\_\_dirname, "../public/assets/icons/svg");const outputDir \= path.join(\_\_dirname, "../public/assets/icons/svgComponent");if (\!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });// Get argument from command line// 'new' \=\> only new files// 'all' \=\> rebuild all files (except MANUAL\_EDIT)const mode \= process.argv\[2\] || "all";const files \= fs.readdirSync(inputDir).filter((f) \=\> f.endsWith(".svg"));for (const file of files) { const componentName \= path.basename(file, ".svg").replace(/\[^a-zA-Z0-9\]/g, "\_") \+ "Icon"; const outputPath \= path.join(outputDir, \`${componentName}.jsx\`); // Check if file exists and MANUAL\_EDIT comment if (fs.existsSync(outputPath)) { const existingContent \= fs.readFileSync(outputPath, "utf8"); if (existingContent.includes("// MANUAL\_EDIT")) { console.log(\`โ Skipped manual icon: ${componentName}\`); continue; } if (mode \=== "new") { console.log(\`โญ Skipped existing icon (new mode): ${componentName}\`); continue; } } let svgContent \= fs.readFileSync(path.join(inputDir, file), "utf8"); // Clean up SVG for JSX svgContent \= svgContent .replace(/\<\\?xml.\*?\\?\>/g, "") .replace(/\<\!DOCTYPE.\*?\>/g, "") .replace(/\\s+xmlns(:xlink)?="\[^"\]\*"/g, "") .replace(/\\s+xlink:\[^=\]+="\[^"\]\*"/g, "") .replace(/\<(path|rect|circle|ellipse|line|polyline|polygon)(\[^\>\]\*?)(?\<\!\\/)\>/g, "\<$1$2 /\>") .replace(/\\s+style="\[^"\]\*"/g, "") .replace(/\\s+fill="\[^"\]\*"/g, "") .replace(/\<path(\[^\>\]\*)\\/\>/g, \`\<path$1 fill="currentColor" /\>\`); svgContent \= svgContent.replace( /\<svg(\[^\>\]\*)\>/, \`\<svg$1 {...props} xmlns="http://www.w3.org/2000/svg"\>\` ); const component \= \`import React from "react";// MANUAL\_EDIT: remove this comment if you manually edit this iconexport const ${componentName} \= (props) \=\> ( ${svgContent.trim()});\`; fs.writeFileSync(outputPath, component, "utf8"); console.log(\`โ
Created/Updated: ${componentName}\`);}console.log(\`๐ SVG processing done\! Mode: ${mode}\`); |
|
|
61
|
+
| :---- |
|
|
62
|
+
|
|
63
|
+
**Pros:** \- โ
Full control over generation \- โ
MANUAL\_EDIT protects custom edits \- โ
Consistent components with unified props \- โ
No runtime fetching
|
|
64
|
+
|
|
65
|
+
**Cons:** \- โ ๏ธ Requires script maintenance \- โ ๏ธ Initial generation for large sets can take time \- โ ๏ธ Must rerun when new SVGs are added
|
|
66
|
+
|
|
67
|
+
**Performance:** CPU low at runtime | RAM minimal | Build Time fast
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### Method 4: Synchronous XMLHttpRequest Inline Loader
|
|
72
|
+
|
|
73
|
+
Loads SVGs from the public folder synchronously and allows inline manipulation.
|
|
74
|
+
|
|
75
|
+
| import React from "react";export const SvgIcon \= ({ src, width, height, fill, keepOriginalColor \= false, className }) \=\> { let svgContent \= ""; try { const xhr \= new XMLHttpRequest(); xhr.open("GET", src, false); xhr.send(null); if (xhr.status \=== 200) { svgContent \= xhr.responseText; if (width) svgContent \= svgContent.replace(/width="\[^"\]\*"/, \`width="${width}"\`); if (height) svgContent \= svgContent.replace(/height="\[^"\]\*"/, \`height="${height}"\`); if (fill && \!keepOriginalColor) { svgContent \= svgContent .replace(/fill="\[^"\]\*"/g, \`fill="${fill}"\`) .replace(/\<path(?\!\[^\>\]\*fill)/g, \`\<path fill="${fill}"\`); } } } catch (e) { console.error("SvgIcon load error:", e); } if (\!svgContent) return null; return \<span className={className} dangerouslySetInnerHTML={{ \_\_html: svgContent }} /\>;}; |
|
|
76
|
+
| :---- |
|
|
77
|
+
|
|
78
|
+
**Pros:** โ๏ธ Simple | โ๏ธ Inline control | โ๏ธ Suitable for low-frequency rendering **Cons:** โ ๏ธ Blocks main thread | โ ๏ธ Not for large-scale | โ ๏ธ SSR issues
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### Method 5: Using react-svg Library
|
|
83
|
+
|
|
84
|
+
Dynamically loads and injects SVGs into the DOM with full control.
|
|
85
|
+
|
|
86
|
+
| import { ReactSVG } from 'react-svg';export const ExampleIcon \= () \=\> ( \<ReactSVG src="/assets/icons/truck.svg" beforeInjection={(svg) \=\> { svg.classList.add('svg-class-name'); svg.setAttribute('style', 'width: 200px'); }} afterInjection={(svg) \=\> { console.log(svg); }} className="wrapper-class-name" loading={() \=\> \<span\>Loading...\</span\>} fallback={() \=\> \<span\>Error\!\</span\>} wrapper="span" evalScripts="always" /\>); |
|
|
87
|
+
| :---- |
|
|
88
|
+
|
|
89
|
+
**Performance & Features:** \- Network: fetch per icon (cached if enabled) \- Bundle Size: smaller than SVGR \- CPU: light but includes DOM parsing \- RAM: minimal
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 3\. Final Comparison Table
|
|
94
|
+
|
|
95
|
+
| Feature | Public Fetch | SVGR | Custom Script | XHR Sync | react-svg |
|
|
96
|
+
| :---- | :---- | :---- | :---- | :---- | :---- |
|
|
97
|
+
| | | | | | |
|
|
98
|
+
| Bundle Size | โ
Small | โ ๏ธ Medium | โ
Small | โ
Small | โ
Small |
|
|
99
|
+
| | | | | | |
|
|
100
|
+
| Runtime Performance | โ ๏ธ Slower | โ
Fast | โ
Fast | โ ๏ธ Moderate | โ
Fast |
|
|
101
|
+
| Build Time | โ
Fast | โ ๏ธ Moderate | โ
Fast | โ
Fast | โ
Fast |
|
|
102
|
+
| Customization | โ Limited | โ
Full | โ
Full | โ
Full | โ
Full |
|
|
103
|
+
| Manual Control | โ No | โ ๏ธ Partial | โ
Full | โ
Full | โ
Full |
|
|
104
|
+
| Dynamic Updates | โ
Easy | โ Needs rebuild | โ ๏ธ Needs rerun | โ Static | โ
Dynamic |
|
|
105
|
+
| Setup Complexity | โ
Simple | โ
Simple | โ ๏ธ Requires Script | โ
Simple | โ
Simple |
|
|
106
|
+
|
|
107
|
+
## ---
|
|
108
|
+
|
|
109
|
+
## 4\. Conclusion
|
|
110
|
+
|
|
111
|
+
* For **high-performance and fully controllable projects** โ **Custom svg-to-react script**
|
|
112
|
+
|
|
113
|
+
* For **fast, dynamic loading** โ **react-svg** or **Public Fetch**
|
|
114
|
+
|
|
115
|
+
* For **small, simple projects** โ **XHR Sync** or **Public Fetch**
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "svger-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI and runtime for converting SVGs to React components with watch support",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"svger-cli": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "ts-node src/cli.ts"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"svg",
|
|
16
|
+
"react",
|
|
17
|
+
"cli",
|
|
18
|
+
"components"
|
|
19
|
+
],
|
|
20
|
+
"author": "faeze mohadespoor",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"change-case": "^5.4.4",
|
|
24
|
+
"chokidar": "^4.0.3",
|
|
25
|
+
"commander": "^14.0.2",
|
|
26
|
+
"fs-extra": "^11.3.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/fs-extra": "^11.0.4",
|
|
30
|
+
"@types/node": "^24.9.2",
|
|
31
|
+
"ts-node": "^10.9.2",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/builder.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pascalCase } from "change-case";
|
|
4
|
+
import { reactTemplate } from "./templates/ComponentTemplate.js";
|
|
5
|
+
import { isLocked } from "./lock.js";
|
|
6
|
+
import { readConfig } from "./config.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts all SVG files from a source directory into React components and writes them to an output directory.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} config - Configuration object.
|
|
12
|
+
* @param {string} config.src - Path to the source folder containing SVG files.
|
|
13
|
+
* @param {string} config.out - Path to the output folder where React components will be generated.
|
|
14
|
+
* @returns {Promise<void>} Resolves when all SVGs have been processed.
|
|
15
|
+
*/
|
|
16
|
+
export async function buildAll(config: { src: string; out: string }) {
|
|
17
|
+
const svgConfig = readConfig();
|
|
18
|
+
const srcDir = path.resolve(config.src);
|
|
19
|
+
const outDir = path.resolve(config.out);
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(srcDir)) {
|
|
22
|
+
console.error("โ Source folder not found:", srcDir);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await fs.ensureDir(outDir);
|
|
27
|
+
const files = (await fs.readdir(srcDir)).filter(f => f.endsWith(".svg"));
|
|
28
|
+
|
|
29
|
+
if (!files.length) {
|
|
30
|
+
console.log("โ ๏ธ No SVG files found in", srcDir);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
const svgPath = path.join(srcDir, file);
|
|
36
|
+
|
|
37
|
+
if (isLocked(svgPath)) {
|
|
38
|
+
console.log(`โ ๏ธ Skipped locked file: ${file}`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const svgContent = await fs.readFile(svgPath, "utf-8");
|
|
43
|
+
const componentName = pascalCase(file.replace(".svg", ""));
|
|
44
|
+
const componentCode = reactTemplate({
|
|
45
|
+
componentName,
|
|
46
|
+
svgContent,
|
|
47
|
+
defaultWidth: svgConfig.defaultWidth,
|
|
48
|
+
defaultHeight: svgConfig.defaultHeight,
|
|
49
|
+
defaultFill: svgConfig.defaultFill,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const outFile = path.join(outDir, `${componentName}.tsx`);
|
|
53
|
+
await fs.writeFile(outFile, componentCode, "utf-8");
|
|
54
|
+
console.log(`โ
Generated: ${componentName}.tsx`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log("๐ All SVGs have been converted successfully!");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generates a single React component from an SVG file.
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} params - Parameters object.
|
|
64
|
+
* @param {string} params.svgFile - Path to the SVG file to be converted.
|
|
65
|
+
* @param {string} params.outDir - Path to the output folder for the generated component.
|
|
66
|
+
* @returns {Promise<void>} Resolves when the SVG has been converted.
|
|
67
|
+
*/
|
|
68
|
+
export async function generateSVG({
|
|
69
|
+
svgFile,
|
|
70
|
+
outDir,
|
|
71
|
+
}: {
|
|
72
|
+
svgFile: string;
|
|
73
|
+
outDir: string;
|
|
74
|
+
}) {
|
|
75
|
+
const svgConfig = readConfig();
|
|
76
|
+
const filePath = path.resolve(svgFile);
|
|
77
|
+
|
|
78
|
+
if (isLocked(filePath)) {
|
|
79
|
+
console.log(`โ ๏ธ Skipped locked file: ${path.basename(svgFile)}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(filePath)) {
|
|
84
|
+
console.error("โ SVG file not found:", filePath);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const svgContent = await fs.readFile(filePath, "utf-8");
|
|
89
|
+
const componentName = pascalCase(path.basename(svgFile, ".svg"));
|
|
90
|
+
const componentCode = reactTemplate({
|
|
91
|
+
componentName,
|
|
92
|
+
svgContent,
|
|
93
|
+
defaultWidth: svgConfig.defaultWidth,
|
|
94
|
+
defaultHeight: svgConfig.defaultHeight,
|
|
95
|
+
defaultFill: svgConfig.defaultFill,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const outputFolder = path.resolve(outDir);
|
|
99
|
+
await fs.ensureDir(outputFolder);
|
|
100
|
+
|
|
101
|
+
const outFile = path.join(outputFolder, `${componentName}.tsx`);
|
|
102
|
+
await fs.writeFile(outFile, componentCode, "utf-8");
|
|
103
|
+
|
|
104
|
+
console.log(`โ
Generated: ${componentName}.tsx`);
|
|
105
|
+
}
|
package/src/clean.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cleans the specified output directory by removing all files and folders inside it.
|
|
6
|
+
* Typically used to clear previously generated SVG React components before a new build.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} outDir - Path to the output directory to be cleaned.
|
|
9
|
+
* @returns {Promise<void>} Resolves when the directory has been emptied.
|
|
10
|
+
*/
|
|
11
|
+
export async function clean(outDir: string) {
|
|
12
|
+
const targetDir = path.resolve(outDir);
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(targetDir)) {
|
|
15
|
+
console.log(`โ ๏ธ Directory not found: ${targetDir}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await fs.emptyDir(targetDir);
|
|
20
|
+
console.log(`๐งน Cleaned all generated SVG components in: ${targetDir}`);
|
|
21
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { buildAll, generateSVG } from "./builder.js";
|
|
4
|
+
import { lockFiles, unlockFiles } from "./lock.js";
|
|
5
|
+
import { initConfig, setConfig, showConfig } from "./config.js";
|
|
6
|
+
import { watchSVGs } from "./watch.js";
|
|
7
|
+
import { clean } from "./clean.js";
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* svger-cli CLI
|
|
13
|
+
* Custom SVG to React component converter.
|
|
14
|
+
*/
|
|
15
|
+
program
|
|
16
|
+
.name("svger-cli")
|
|
17
|
+
.description("Custom SVG to React component converter")
|
|
18
|
+
.version("1.0.0");
|
|
19
|
+
|
|
20
|
+
// -------- Build Command --------
|
|
21
|
+
/**
|
|
22
|
+
* Build all SVGs from a source folder to an output folder.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} src - Source folder containing SVG files.
|
|
25
|
+
* @param {string} out - Output folder for generated React components.
|
|
26
|
+
*/
|
|
27
|
+
program
|
|
28
|
+
.command("build <src> <out>")
|
|
29
|
+
.description("Build all SVGs from source to output")
|
|
30
|
+
.action(async (src: string, out: string) => {
|
|
31
|
+
console.log("๐ ๏ธ Building SVGs...");
|
|
32
|
+
console.log("Source:", src);
|
|
33
|
+
console.log("Output:", out);
|
|
34
|
+
await buildAll({ src, out });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// -------- Watch Command --------
|
|
38
|
+
/**
|
|
39
|
+
* Watch a source folder and rebuild SVGs automatically on changes.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} src - Source folder to watch.
|
|
42
|
+
* @param {string} out - Output folder for generated components.
|
|
43
|
+
*/
|
|
44
|
+
program
|
|
45
|
+
.command("watch <src> <out>")
|
|
46
|
+
.description("Watch source folder and rebuild SVGs automatically")
|
|
47
|
+
.action((src: string, out: string) => {
|
|
48
|
+
console.log("๐ Starting watch mode...");
|
|
49
|
+
watchSVGs({ src, out });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// -------- Generate Single SVG --------
|
|
53
|
+
/**
|
|
54
|
+
* Generate a React component from a single SVG file.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} svgFile - Path to the SVG file.
|
|
57
|
+
* @param {string} out - Output folder for the generated component.
|
|
58
|
+
*/
|
|
59
|
+
program
|
|
60
|
+
.command("generate <svgFile> <out>")
|
|
61
|
+
.description("Convert a single SVG file into a React component")
|
|
62
|
+
.action(async (svgFile: string, out: string) => {
|
|
63
|
+
await generateSVG({ svgFile, outDir: out });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// -------- Lock / Unlock --------
|
|
67
|
+
/**
|
|
68
|
+
* Lock one or more SVG files to prevent accidental overwrites.
|
|
69
|
+
*
|
|
70
|
+
* @param {string[]} files - Paths to SVG files to lock.
|
|
71
|
+
*/
|
|
72
|
+
program
|
|
73
|
+
.command("lock <files...>")
|
|
74
|
+
.description("Lock one or more SVG files")
|
|
75
|
+
.action((files: string[]) => lockFiles(files));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Unlock one or more SVG files to allow modifications.
|
|
79
|
+
*
|
|
80
|
+
* @param {string[]} files - Paths to SVG files to unlock.
|
|
81
|
+
*/
|
|
82
|
+
program
|
|
83
|
+
.command("unlock <files...>")
|
|
84
|
+
.description("Unlock one or more SVG files")
|
|
85
|
+
.action((files: string[]) => unlockFiles(files));
|
|
86
|
+
|
|
87
|
+
// -------- Config --------
|
|
88
|
+
/**
|
|
89
|
+
* Manage svger-cli configuration.
|
|
90
|
+
*
|
|
91
|
+
* Options:
|
|
92
|
+
* --init: Create default .svgconfig.json
|
|
93
|
+
* --set key=value: Set a configuration value
|
|
94
|
+
* --show: Show current configuration
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} opts - CLI options
|
|
97
|
+
*/
|
|
98
|
+
program
|
|
99
|
+
.command("config")
|
|
100
|
+
.description("Manage svger-cli configuration")
|
|
101
|
+
.option("--init", "Create default .svgconfig.json")
|
|
102
|
+
.option("--set <keyValue>", "Set config key=value")
|
|
103
|
+
.option("--show", "Show current config")
|
|
104
|
+
.action((opts) => {
|
|
105
|
+
if (opts.init) return initConfig();
|
|
106
|
+
if (opts.set) {
|
|
107
|
+
const [key, value] = opts.set.split("=");
|
|
108
|
+
if (!key || value === undefined) {
|
|
109
|
+
console.error("โ Invalid format. Use key=value");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const parsedValue = !isNaN(Number(value)) ? Number(value) : value;
|
|
113
|
+
return setConfig(key, parsedValue);
|
|
114
|
+
}
|
|
115
|
+
if (opts.show) return showConfig();
|
|
116
|
+
console.log("โ No option provided. Use --init, --set, or --show");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// -------- Clean Command --------
|
|
120
|
+
/**
|
|
121
|
+
* Remove all generated SVG React components from an output folder.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} out - Output folder to clean.
|
|
124
|
+
*/
|
|
125
|
+
program
|
|
126
|
+
.command("clean <out>")
|
|
127
|
+
.description("Remove all generated SVG React components from output folder")
|
|
128
|
+
.action(async (out: string) => {
|
|
129
|
+
await clean(out);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
program.parse();
|