vite-plugin-svg-var 0.1.1
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/README.md +229 -0
- package/lib.js +147 -0
- package/package.json +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# vite-plugin-svg-var
|
|
2
|
+
|
|
3
|
+
[English](#en) | [中文](#zh)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<a id="en"></a>
|
|
8
|
+
# vite-plugin-svg-var
|
|
9
|
+
|
|
10
|
+
A Vite plugin to automatically convert SVG files into CSS variables.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
1. **Auto Scanning**: Recursively scans all `.svg` files within the `public/svg` directory under the project root.
|
|
15
|
+
2. **Smart Deduplication**: Computes MD5 hashes of SVGs. Identical SVGs mapped from different paths will share the same CSS variable, minimizing the final bundle size.
|
|
16
|
+
3. **Optimized UTF-8 Encoding**: Encodes SVGs into CSS `data:image/svg+xml` URLs using UTF-8 encoding (which is smaller and cleaner than Base64).
|
|
17
|
+
4. **Collision Resolution**: Employs a camelCase variable naming strategy. If name collisions occur (e.g. from different subdirectories), parent directory names are prefixed or counter suffixes are appended.
|
|
18
|
+
5. **Auto Injection & Replacement**:
|
|
19
|
+
- Automatically prepends `import "virtual:svgVar.css";` to entry files matching `/page/entry/**/*.js`.
|
|
20
|
+
- Replaces references like `url("/svg/xxx.svg")` in CSS, Stylus, Svelte, or JS files with `var(--xxxSvg)`.
|
|
21
|
+
6. **HMR Support**: Watches the `public/svg` directory, automatically rescanning SVGs and invalidating modules to trigger a full page reload upon addition, update, or deletion.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Install the package as a development dependency:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun i -D vite-plugin-svg-var
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### 1. Register the Vite Plugin
|
|
36
|
+
|
|
37
|
+
Import and register the plugin in your `vite.config.js`:
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
import svgVar from "vite-plugin-svg-var";
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
plugins: [svgVar()],
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Add SVG Files
|
|
48
|
+
|
|
49
|
+
Place your SVG files into `public/svg`:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
public/
|
|
53
|
+
└── svg/
|
|
54
|
+
├── close.svg
|
|
55
|
+
└── user/
|
|
56
|
+
└── avatar.svg
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The plugin dynamically maps them to CSS variables:
|
|
60
|
+
|
|
61
|
+
- `public/svg/close.svg` -> `--closeSvg`
|
|
62
|
+
- `public/svg/user/avatar.svg` -> `--userAvatarSvg`
|
|
63
|
+
|
|
64
|
+
### 3. Reference in Stylesheets
|
|
65
|
+
|
|
66
|
+
Reference your SVG path normally using standard URLs in `.styl`, `.css`, or Svelte `<style>` blocks:
|
|
67
|
+
|
|
68
|
+
```stylus
|
|
69
|
+
.btn-close
|
|
70
|
+
background-image: url("/svg/close.svg")
|
|
71
|
+
background-size: contain
|
|
72
|
+
width: 16px
|
|
73
|
+
height: 16px
|
|
74
|
+
|
|
75
|
+
.avatar
|
|
76
|
+
background: url("/svg/user/avatar.svg") no-repeat center
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
During build or dev mode, the plugin automatically replaces the url references with CSS variables:
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
.btn-close {
|
|
83
|
+
background-image: var(--closeSvg);
|
|
84
|
+
background-size: contain;
|
|
85
|
+
width: 16px;
|
|
86
|
+
height: 16px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.avatar {
|
|
90
|
+
background: var(--userAvatarSvg) no-repeat center;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The corresponding CSS variables are defined in the global `:root`:
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
:root {
|
|
98
|
+
--closeSvg: url("data:image/svg+xml,...");
|
|
99
|
+
--userAvatarSvg: url("data:image/svg+xml,...");
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
<a id="zh"></a>
|
|
106
|
+
# vite-plugin-svg-var
|
|
107
|
+
|
|
108
|
+
一个用于将 SVG 文件自动转换为 CSS 变量的 Vite 插件。
|
|
109
|
+
|
|
110
|
+
## 功能特性
|
|
111
|
+
|
|
112
|
+
1. **自动扫描**:自动递归扫描项目根目录下的 `public/svg` 文件夹中的所有 `.svg` 文件。
|
|
113
|
+
2. **智能去重**:通过对 SVG 内容进行 MD5 哈希计算,对于内容相同的 SVG,只会生成一个 CSS 变量,多个引用路径会自动映射到同一个变量名,从而减少打包体积。
|
|
114
|
+
3. **安全编码**:使用更高效、体积更小的 UTF-8 编码将 SVG 转换为 CSS `data:image/svg+xml` URL,而不是使用 Base64。
|
|
115
|
+
4. **命名冲突解决**:采用智能驼峰命名算法。如果存在同名 SVG(例如位于不同子目录下),插件会自动前缀子目录名(如项目名)或追加数字后缀。
|
|
116
|
+
5. **自动注入与替换**:
|
|
117
|
+
- 在入口 JS 文件(匹配 `/page/entry/**/*.js`)中自动注入 `import "virtual:svgVar.css";`。
|
|
118
|
+
- 在 CSS/Stylus/Svelte/JS 文件中,将 CSS 中的 `url("/svg/xxx.svg")` 形式的引用自动替换为 `var(--xxxSvg)`。
|
|
119
|
+
6. **开发阶段热更新 (HMR)**:监听 `public/svg` 目录,在 SVG 增删改时自动重新扫描并触发页面刷新。
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 安装说明
|
|
124
|
+
|
|
125
|
+
在你的包目录下使用以下命令安装:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
bun i -D vite-plugin-svg-var
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 使用方案
|
|
132
|
+
|
|
133
|
+
### 1. 配置 Vite 插件
|
|
134
|
+
|
|
135
|
+
在你的 `vite.config.js` 中引入并配置该插件:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
import svgVar from "vite-plugin-svg-var";
|
|
139
|
+
|
|
140
|
+
export default {
|
|
141
|
+
plugins: [svgVar()],
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 2. 添加 SVG 文件
|
|
146
|
+
|
|
147
|
+
将你的 SVG 文件放入 `public/svg` 目录中:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
public/
|
|
151
|
+
└── svg/
|
|
152
|
+
├── close.svg
|
|
153
|
+
└── user/
|
|
154
|
+
└── avatar.svg
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
插件会自动为它们分配变量名:
|
|
158
|
+
|
|
159
|
+
- `public/svg/close.svg` -> `--closeSvg`
|
|
160
|
+
- `public/svg/user/avatar.svg` -> `--userAvatarSvg`
|
|
161
|
+
|
|
162
|
+
### 3. 在样式中直接引用
|
|
163
|
+
|
|
164
|
+
在你的样式文件(如 `.styl`、`.css` 或 Svelte `<style>` 标签)中,可以像平常一样直接引用 SVG 的 URL 路径:
|
|
165
|
+
|
|
166
|
+
```stylus
|
|
167
|
+
.btn-close
|
|
168
|
+
background-image: url("/svg/close.svg")
|
|
169
|
+
background-size: contain
|
|
170
|
+
width: 16px
|
|
171
|
+
height: 16px
|
|
172
|
+
|
|
173
|
+
.avatar
|
|
174
|
+
background: url("/svg/user/avatar.svg") no-repeat center
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
在构建或开发运行时,插件会自动将其转换为:
|
|
178
|
+
|
|
179
|
+
```css
|
|
180
|
+
.btn-close {
|
|
181
|
+
background-image: var(--closeSvg);
|
|
182
|
+
background-size: contain;
|
|
183
|
+
width: 16px;
|
|
184
|
+
height: 16px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.avatar {
|
|
188
|
+
background: var(--userAvatarSvg) no-repeat center;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
同时,对应的 CSS 变量已在 `:root` 中定义:
|
|
193
|
+
|
|
194
|
+
```css
|
|
195
|
+
:root {
|
|
196
|
+
--closeSvg: url("data:image/svg+xml,...");
|
|
197
|
+
--userAvatarSvg: url("data:image/svg+xml,...");
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## About
|
|
204
|
+
|
|
205
|
+
This project is an open-source component of [i18n.site ⋅ Internationalization Solution](https://i18n.site).
|
|
206
|
+
|
|
207
|
+
* [i18 : MarkDown Command Line Translation Tool](https://i18n.site/i18)
|
|
208
|
+
|
|
209
|
+
The translation perfectly maintains the Markdown format.
|
|
210
|
+
|
|
211
|
+
It recognizes file changes and only translates the modified files.
|
|
212
|
+
|
|
213
|
+
The translated Markdown content is editable; if you modify the original text and translate it again, manually edited translations will not be overwritten (as long as the original text has not been changed).
|
|
214
|
+
|
|
215
|
+
* [i18n.site : MarkDown Multi-language Static Site Generator](https://i18n.site/i18n.site)
|
|
216
|
+
|
|
217
|
+
Optimized for a better reading experience
|
|
218
|
+
|
|
219
|
+
## 关于
|
|
220
|
+
|
|
221
|
+
本项目为 [i18n.site ⋅ 国际化解决方案](https://i18n.site) 的开源组件。
|
|
222
|
+
|
|
223
|
+
* [i18 : MarkDown命令行翻译工具](https://i18n.site/i18)
|
|
224
|
+
|
|
225
|
+
翻译能够完美保持 Markdown 的格式。能识别文件的修改,仅翻译有变动的文件。
|
|
226
|
+
|
|
227
|
+
Markdown 翻译内容可编辑;如果你修改原文并再次机器翻译,手动修改过的翻译不会被覆盖(如果这段原文没有被修改)。
|
|
228
|
+
|
|
229
|
+
* [i18n.site : MarkDown多语言静态站点生成器](https://i18n.site/i18n.site) 为阅读体验而优化。
|
package/lib.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { BinSet } from "@3-/binset/_.js";
|
|
5
|
+
const ROOT = process.cwd(),
|
|
6
|
+
SVG_DIR = join(ROOT, "public/svg"),
|
|
7
|
+
path_to_var_name = new Map(),
|
|
8
|
+
md5_to_var_name = new Map(),
|
|
9
|
+
assigned_vars = new Set(),
|
|
10
|
+
svg_content_map = new Map(),
|
|
11
|
+
camel = (str) => str.replace(/[-_]([a-z])/g, (g) => g[1].toUpperCase()),
|
|
12
|
+
capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1),
|
|
13
|
+
md5 = (content) => createHash("md5").update(content).digest("hex"),
|
|
14
|
+
encode = (svg) =>
|
|
15
|
+
[..."%#<>,"].reduce((r, c) => r.replaceAll(c, encodeURIComponent(c)), svg.replaceAll('"', "'"));
|
|
16
|
+
|
|
17
|
+
const scan = () => {
|
|
18
|
+
if (!existsSync(SVG_DIR)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const files = [],
|
|
23
|
+
walk = (dir, prefix = "") => {
|
|
24
|
+
for (const item of readdirSync(dir, { withFileTypes: true })) {
|
|
25
|
+
if (item.isDirectory()) {
|
|
26
|
+
walk(join(dir, item.name), prefix + item.name + "/");
|
|
27
|
+
} else if (item.name.endsWith(".svg")) {
|
|
28
|
+
files.push(prefix + item.name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
walk(SVG_DIR);
|
|
33
|
+
files.sort();
|
|
34
|
+
|
|
35
|
+
path_to_var_name.clear();
|
|
36
|
+
md5_to_var_name.clear();
|
|
37
|
+
assigned_vars.clear();
|
|
38
|
+
svg_content_map.clear();
|
|
39
|
+
|
|
40
|
+
const bin_set = new BinSet(),
|
|
41
|
+
encoder = new TextEncoder();
|
|
42
|
+
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const rel_path = file.slice(0, -4),
|
|
45
|
+
file_path = join(SVG_DIR, file),
|
|
46
|
+
raw = readFileSync(file_path, "utf8");
|
|
47
|
+
|
|
48
|
+
const cleaned = raw.trim(),
|
|
49
|
+
svg_bin = encoder.encode(cleaned);
|
|
50
|
+
|
|
51
|
+
if (bin_set.has(svg_bin)) {
|
|
52
|
+
const var_name = md5_to_var_name.get(md5(cleaned));
|
|
53
|
+
path_to_var_name.set(rel_path, var_name);
|
|
54
|
+
} else {
|
|
55
|
+
bin_set.add(svg_bin);
|
|
56
|
+
const hash = md5(cleaned),
|
|
57
|
+
parts = rel_path.split("/"),
|
|
58
|
+
filename = parts.at(-1),
|
|
59
|
+
default_var = camel(filename) + "Svg";
|
|
60
|
+
|
|
61
|
+
let var_name = default_var;
|
|
62
|
+
if (assigned_vars.has(default_var)) {
|
|
63
|
+
const project_name = parts.length > 1 ? camel(parts[0]) : "",
|
|
64
|
+
fallback_var = project_name + capitalize(camel(filename)) + "Svg";
|
|
65
|
+
var_name = fallback_var;
|
|
66
|
+
|
|
67
|
+
let counter = 1;
|
|
68
|
+
while (assigned_vars.has(var_name)) {
|
|
69
|
+
var_name = fallback_var + counter;
|
|
70
|
+
++counter;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
assigned_vars.add(var_name);
|
|
75
|
+
md5_to_var_name.set(hash, var_name);
|
|
76
|
+
path_to_var_name.set(rel_path, var_name);
|
|
77
|
+
svg_content_map.set(var_name, cleaned);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
bin_set.free();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
scan();
|
|
84
|
+
|
|
85
|
+
export const replace = (code) =>
|
|
86
|
+
code.replace(
|
|
87
|
+
/url\(['"]?(?:[^'"()]*?)\/svg\/([^'")(#?\s]+)\.svg(?:#[^'")\s]*)?['"]?\)/g,
|
|
88
|
+
(match, rel_path) => {
|
|
89
|
+
const var_name = path_to_var_name.get(rel_path);
|
|
90
|
+
return var_name ? "var(--" + var_name + ")" : match;
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const render = () =>
|
|
95
|
+
[
|
|
96
|
+
":root {",
|
|
97
|
+
...[...svg_content_map].map(
|
|
98
|
+
([var_name, content]) =>
|
|
99
|
+
" --" + var_name + ': url("data:image/svg+xml,' + encode(content) + '");',
|
|
100
|
+
),
|
|
101
|
+
"}",
|
|
102
|
+
].join("\n");
|
|
103
|
+
|
|
104
|
+
export default () => {
|
|
105
|
+
const virtual_id = "virtual:svgVar.css",
|
|
106
|
+
resolved_virtual_id = "\0" + virtual_id;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
name: "vite-plugin-svg-var",
|
|
110
|
+
resolveId: (id) => (id === virtual_id ? resolved_virtual_id : null),
|
|
111
|
+
load: (id) => (id === resolved_virtual_id ? render() : null),
|
|
112
|
+
transform(code, id) {
|
|
113
|
+
const clean_id = id.split("?")[0];
|
|
114
|
+
if (!/\.(styl|svelte|css|js|ts)$/.test(clean_id)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const new_code =
|
|
119
|
+
clean_id.includes("/page/entry/") && clean_id.endsWith(".js")
|
|
120
|
+
? 'import "virtual:svgVar.css";\n' + code
|
|
121
|
+
: code;
|
|
122
|
+
|
|
123
|
+
const replaced = replace(new_code);
|
|
124
|
+
if (replaced !== code) {
|
|
125
|
+
return {
|
|
126
|
+
code: replaced,
|
|
127
|
+
map: null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
},
|
|
132
|
+
configureServer: (server) => {
|
|
133
|
+
server.watcher.add(SVG_DIR);
|
|
134
|
+
server.watcher.on("all", (event, file) => {
|
|
135
|
+
if (file.endsWith(".svg") && file.startsWith(SVG_DIR)) {
|
|
136
|
+
scan();
|
|
137
|
+
const { moduleGraph, ws } = server,
|
|
138
|
+
mod = moduleGraph.getModuleById(resolved_virtual_id);
|
|
139
|
+
if (mod) {
|
|
140
|
+
moduleGraph.invalidateModule(mod);
|
|
141
|
+
}
|
|
142
|
+
ws.send({ type: "full-reload" });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"vite-plugin-svg-var","version":"0.1.1","keywords":[],"description":"","repository":{"type":"git","url":"git+https://github.com/i18n-site/lib.git"},"homepage":"https://github.com/i18n-site/lib/tree/dev/vite-plugin-svg-var","author":"i18n.site@gmail.com","license":"MulanPSL-2.0","exports":{".":"./lib.js","./*":"./*"},"files":["./*"],"devDependencies":{},"scripts":{},"type":"module","dependencies":{"@3-/binset":"^0.1.3"}}
|