vite-plugin-css-svg-mask-image 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/README.md +90 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +142 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# vite-plugin-css-svg-mask-image
|
|
2
|
+
|
|
3
|
+
A Vite plugin that rewrites `mask-image: svg(...)` calls in your CSS/SCSS into a complete, cross-browser mask setup (mask image, repeat, size, and optional color via background-color). It also generates a single `icons.scss` file with `:root` CSS variables for each SVG icon, so the SVG data is stored once and reused everywhere, no duplicated inline SVG blobs across your stylesheets.
|
|
4
|
+
|
|
5
|
+
Best used with icons that are a single compound shape (solid silhouette). Multi-shape or multi-color SVGs can produce unexpected masking results.
|
|
6
|
+
|
|
7
|
+
## Description
|
|
8
|
+
|
|
9
|
+
This plugin processes CSS and SCSS files to transform `mask-image: svg()` function calls into standard CSS mask properties. It automatically:
|
|
10
|
+
|
|
11
|
+
- Converts SVG files to data URIs
|
|
12
|
+
- Generates CSS variables in `:root` for each icon
|
|
13
|
+
- Replaces `svg()` calls with the appropriate CSS properties
|
|
14
|
+
- Writes the `:root` declarations to a separate file (default: `icons.scss`)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install vite-plugin-css-svg-mask-image --save-dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Basic Setup
|
|
23
|
+
|
|
24
|
+
Add the plugin to your `vite.config.ts`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { defineConfig } from "vite";
|
|
28
|
+
import { svgMaskPlugin } from "vite-plugin-css-svg-mask-image";
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
plugins: [
|
|
32
|
+
svgMaskPlugin({
|
|
33
|
+
svg_dir: "src/icons", // Directory containing SVG files
|
|
34
|
+
icons_file: "icons.scss", // Output file for :root declarations
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Configuration Options
|
|
41
|
+
|
|
42
|
+
- `svg_dir` (string, optional): Directory where SVG files are stored. Default: `'src/icons'`
|
|
43
|
+
- `icons_file` (string, optional): Path to the output file containing `:root` declarations. Default: `'icons.scss'`
|
|
44
|
+
|
|
45
|
+
### CSS/SCSS Usage
|
|
46
|
+
|
|
47
|
+
In your CSS or SCSS files, use the `svg()` function:
|
|
48
|
+
|
|
49
|
+
```scss
|
|
50
|
+
.a {
|
|
51
|
+
mask-image: svg("arrow-right");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.b {
|
|
55
|
+
mask-image: svg("arrow-right", "red");
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The plugin will transform this to:
|
|
60
|
+
|
|
61
|
+
```scss
|
|
62
|
+
:root {
|
|
63
|
+
--icon-arrow-right: url("data:image/svg+xml,...");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.a {
|
|
67
|
+
background-color: currentColor;
|
|
68
|
+
mask-image: var(--icon-arrow-right);
|
|
69
|
+
mask-repeat: no-repeat;
|
|
70
|
+
mask-size: 100% 100%;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.b {
|
|
74
|
+
background-color: red;
|
|
75
|
+
mask-image: var(--icon-arrow-right);
|
|
76
|
+
mask-repeat: no-repeat;
|
|
77
|
+
mask-size: 100% 100%;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The `:root` declarations are automatically written to the `icons.scss` file (or your configured output file). Make sure to import this file in your main stylesheet.
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
- Vite 4.0+ or 5.0+
|
|
86
|
+
- Node.js 22+
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join, resolve, dirname } from "path";
|
|
3
|
+
import postcss, { Declaration } from "postcss";
|
|
4
|
+
function parse_svg_function(value) {
|
|
5
|
+
const match = value.match(/^['"]?([^'",]+)['"]?\s*(?:,\s*['"]?([^'"]+)['"]?)?$/);
|
|
6
|
+
if (!match) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
name: match[1].trim(),
|
|
11
|
+
color: match[2]?.trim(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function to_kebab_name(input) {
|
|
15
|
+
// basename without extension -> kebab-ish
|
|
16
|
+
return input
|
|
17
|
+
.replace(/\.[a-z0-9]+$/i, "")
|
|
18
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
19
|
+
.replace(/[^a-z0-9]+/gi, "-")
|
|
20
|
+
.replace(/^-+|-+$/g, "")
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function svg_to_data_uri(svg) {
|
|
24
|
+
const cleaned = svg.replace(/^\uFEFF/, "").trim();
|
|
25
|
+
const encoded = encodeURIComponent(cleaned)
|
|
26
|
+
.replace(/%0A/g, "")
|
|
27
|
+
.replace(/%0D/g, "")
|
|
28
|
+
.replace(/%20/g, " ")
|
|
29
|
+
.replace(/%3D/g, "=")
|
|
30
|
+
.replace(/%3A/g, ":")
|
|
31
|
+
.replace(/%2F/g, "/");
|
|
32
|
+
return `data:image/svg+xml,${encoded}`;
|
|
33
|
+
}
|
|
34
|
+
function get_css_variable_name(icon_name) {
|
|
35
|
+
const kebab_name = to_kebab_name(icon_name);
|
|
36
|
+
return `--icon-${kebab_name}`;
|
|
37
|
+
}
|
|
38
|
+
export function svgMaskPlugin(options = {}) {
|
|
39
|
+
const svg_dir = options.svg_dir || "src/icons";
|
|
40
|
+
const icons_file = options.icons_file || "icons.scss";
|
|
41
|
+
const root_dir = process.cwd();
|
|
42
|
+
const resolved_svg_dir = resolve(root_dir, svg_dir);
|
|
43
|
+
const resolved_icons_file = resolve(root_dir, icons_file);
|
|
44
|
+
const icon_variables = new Map();
|
|
45
|
+
const processed_files = new Set();
|
|
46
|
+
const write_icons_file = () => {
|
|
47
|
+
const icons_dir = dirname(resolved_icons_file);
|
|
48
|
+
if (!existsSync(icons_dir)) {
|
|
49
|
+
mkdirSync(icons_dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
if (icon_variables.size === 0) {
|
|
52
|
+
if (!existsSync(resolved_icons_file)) {
|
|
53
|
+
writeFileSync(resolved_icons_file, ":root {\n}\n", "utf-8");
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let root_content = ":root {\n";
|
|
58
|
+
for (const [name, data_uri] of icon_variables.entries()) {
|
|
59
|
+
const var_name = get_css_variable_name(name);
|
|
60
|
+
root_content += ` ${var_name}: url("${data_uri}");\n`;
|
|
61
|
+
}
|
|
62
|
+
root_content += "}\n";
|
|
63
|
+
writeFileSync(resolved_icons_file, root_content, "utf-8");
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
name: "vite-plugin-css-svg-mask-image",
|
|
67
|
+
enforce: "pre",
|
|
68
|
+
buildStart() {
|
|
69
|
+
const icons_dir = dirname(resolved_icons_file);
|
|
70
|
+
if (!existsSync(icons_dir)) {
|
|
71
|
+
mkdirSync(icons_dir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
if (!existsSync(resolved_icons_file)) {
|
|
74
|
+
writeFileSync(resolved_icons_file, ":root {\n}\n", "utf-8");
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
async transform(code, id) {
|
|
78
|
+
if (!id.match(/\.(css|scss|sass)$/)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const ast = postcss.parse(code);
|
|
82
|
+
let has_svg_functions = false;
|
|
83
|
+
ast.walkDecls("mask-image", (decl) => {
|
|
84
|
+
const value = decl.value.trim();
|
|
85
|
+
if (!value.startsWith("svg(")) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const match = value.match(/^svg\(([^)]+)\)$/);
|
|
89
|
+
if (!match) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const parsed = parse_svg_function(match[1]);
|
|
93
|
+
if (!parsed) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const { name, color } = parsed;
|
|
97
|
+
const svg_path = join(resolved_svg_dir, `${name}.svg`);
|
|
98
|
+
if (!existsSync(svg_path)) {
|
|
99
|
+
console.warn(`[vite-plugin-css-svg-mask-image] SVG file not found: ${svg_path}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const svg_content = readFileSync(svg_path, "utf-8");
|
|
103
|
+
const data_uri = svg_to_data_uri(svg_content);
|
|
104
|
+
const var_name = get_css_variable_name(name);
|
|
105
|
+
icon_variables.set(name, data_uri);
|
|
106
|
+
has_svg_functions = true;
|
|
107
|
+
const rule = decl.parent;
|
|
108
|
+
if (rule && rule.type === "rule") {
|
|
109
|
+
decl.value = `var(${var_name})`;
|
|
110
|
+
const bg_color = color || "currentColor";
|
|
111
|
+
const has_bg_color = rule.nodes?.some((node) => node.type === "decl" && node.prop === "background-color");
|
|
112
|
+
if (!has_bg_color) {
|
|
113
|
+
rule.insertAfter(decl, new Declaration({ prop: "background-color", value: bg_color }));
|
|
114
|
+
}
|
|
115
|
+
const has_mask_repeat = rule.nodes?.some((node) => node.type === "decl" && node.prop === "mask-repeat");
|
|
116
|
+
if (!has_mask_repeat) {
|
|
117
|
+
rule.insertAfter(decl, new Declaration({ prop: "mask-repeat", value: "no-repeat" }));
|
|
118
|
+
}
|
|
119
|
+
const has_mask_size = rule.nodes?.some((node) => node.type === "decl" && node.prop === "mask-size");
|
|
120
|
+
if (!has_mask_size) {
|
|
121
|
+
rule.insertAfter(decl, new Declaration({ prop: "mask-size", value: "100% 100%" }));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (!has_svg_functions) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const result = await postcss().process(ast, { from: id, to: id });
|
|
129
|
+
processed_files.add(id);
|
|
130
|
+
return {
|
|
131
|
+
code: result.css,
|
|
132
|
+
map: result.map ? result.map.toJSON() : null,
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
buildEnd() {
|
|
136
|
+
write_icons_file();
|
|
137
|
+
},
|
|
138
|
+
generateBundle() {
|
|
139
|
+
write_icons_file();
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-css-svg-mask-image",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vite plugin to transform CSS/SCSS files with svg() mask-image function calls to CSS variables",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"test": "vitest",
|
|
15
|
+
"test:run": "vitest run",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"vite",
|
|
20
|
+
"plugin",
|
|
21
|
+
"svg",
|
|
22
|
+
"mask",
|
|
23
|
+
"css",
|
|
24
|
+
"scss"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.1.0",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"vite": "^7.3.1",
|
|
32
|
+
"vitest": "^4.0.18"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"postcss": "^8.5.6"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"vite": "^4.0.0 || ^5.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|