mini-icon 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 ADDED
@@ -0,0 +1,91 @@
1
+ # mini-icon
2
+
3
+ Build SVG icons into CSS mask classes, an SVG sprite, and TypeScript union types.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install mini-icon
9
+ ```
10
+
11
+ ## CLI
12
+
13
+ Add to `package.json`:
14
+
15
+ ```json
16
+ {
17
+ "scripts": {
18
+ "icons": "mini-icon"
19
+ }
20
+ }
21
+ ```
22
+
23
+ Run:
24
+
25
+ ```bash
26
+ npm run icons
27
+ ```
28
+
29
+ ## Config
30
+
31
+ Create `mini-icon.config.js` in your project root:
32
+
33
+ ```js
34
+ export default {
35
+ iconsDir: "_icons",
36
+ spriteOut: "public/icons.svg",
37
+ cssOut: "src/icons.css",
38
+ typesOut: "src/types/icon.ts",
39
+ htmlFile: "index.html",
40
+ injectSpriteLink: true,
41
+ spriteHref: "/icons.svg",
42
+ };
43
+ ```
44
+
45
+ JSON config (`mini-icon.config.json`) is also supported.
46
+
47
+ ## CLI flags
48
+
49
+ ```bash
50
+ mini-icon --icons-dir assets/icons --css-out src/icons.css
51
+ mini-icon --cwd ./apps/web
52
+ mini-icon --inject-html --html-file index.html
53
+ ```
54
+
55
+ ## Programmatic API
56
+
57
+ ```js
58
+ import { buildIcons } from "mini-icon";
59
+
60
+ const result = buildIcons({
61
+ cwd: process.cwd(),
62
+ iconsDir: "_icons",
63
+ spriteOut: "public/icons.svg",
64
+ cssOut: "src/icons.css",
65
+ typesOut: "src/types/icon.ts",
66
+ });
67
+
68
+ console.log(result.names);
69
+ ```
70
+
71
+ ## Usage in HTML
72
+
73
+ Put SVG source files in `_icons/` (or your configured folder), then:
74
+
75
+ ```html
76
+ <link rel="preload" href="/icons.svg" as="image" type="image/svg+xml" />
77
+ ```
78
+
79
+ ```css
80
+ @import "./icons.css";
81
+ ```
82
+
83
+ ```html
84
+ <i class="icon search-lg"></i>
85
+ ```
86
+
87
+ Color follows `currentColor`.
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from "../src/cli.js";
4
+
5
+ runCli();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "mini-icon",
3
+ "version": "1.0.0",
4
+ "description": "Build SVG icons into CSS mask classes, SVG sprite, and TypeScript types",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "",
8
+ "keywords": [
9
+ "svg",
10
+ "icons",
11
+ "sprite",
12
+ "css",
13
+ "mask",
14
+ "cli"
15
+ ],
16
+ "bin": {
17
+ "mini-icon": "./bin/mini-icon.js"
18
+ },
19
+ "main": "./src/index.js",
20
+ "exports": {
21
+ ".": "./src/index.js"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "src",
26
+ "README.md"
27
+ ],
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "scripts": {
32
+ "test": "node test/run.mjs"
33
+ }
34
+ }
package/src/build.js ADDED
@@ -0,0 +1,186 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const DEFAULTS = {
5
+ iconsDir: "_icons",
6
+ spriteOut: "public/icons.svg",
7
+ cssOut: "src/icons.css",
8
+ typesOut: "src/types/icon.ts",
9
+ htmlFile: null,
10
+ injectSpriteLink: false,
11
+ spriteHref: "/icons.svg",
12
+ spriteMarker: "<!-- icon-sprite -->",
13
+ cssBanner: "/* Auto-generated — do not edit. Run: mini-icon */",
14
+ };
15
+
16
+ function extractSvgParts(content) {
17
+ const svgOpenMatch = content.match(/<svg([^>]*)>/i);
18
+ const svgAttrs = svgOpenMatch?.[1] ?? "";
19
+ const viewBoxMatch = svgAttrs.match(/viewBox="([^"]+)"/i);
20
+ const viewBox = viewBoxMatch?.[1] ?? "0 0 24 24";
21
+
22
+ let inner = content.replace(/<svg[^>]*>/i, "").replace(/<\/svg>\s*$/i, "").trim();
23
+
24
+ const presentationAttrs = ["fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin", "fill-rule"];
25
+ const inherited = presentationAttrs.flatMap((attr) => {
26
+ const match = svgAttrs.match(new RegExp(`${attr}="([^"]*)"`, "i"));
27
+ return match ? [`${attr}="${match[1]}"`] : [];
28
+ });
29
+
30
+ if (inherited.length > 0 && !/<(?:g|path|circle|rect|line|polyline|polygon)[^>]*\bstroke=/.test(inner)) {
31
+ inner = `<g ${inherited.join(" ")}>${inner}</g>`;
32
+ }
33
+
34
+ return { viewBox, inner };
35
+ }
36
+
37
+ function toMaskSvg(content) {
38
+ return content
39
+ .replace(/currentColor/g, "#000")
40
+ .replace(/fill="(?!none|currentColor)[^"]*"/gi, 'fill="#000"')
41
+ .replace(/stroke="(?!none|currentColor)[^"]*"/gi, 'stroke="#000"');
42
+ }
43
+
44
+ function toDataUri(content) {
45
+ const encoded = encodeURIComponent(toMaskSvg(content)).replace(/'/g, "%27").replace(/"/g, "%22");
46
+ return `url("data:image/svg+xml,${encoded}")`;
47
+ }
48
+
49
+ function injectSpriteLink(htmlFile, spriteHref, spriteMarker) {
50
+ if (!htmlFile || !fs.existsSync(htmlFile)) return;
51
+
52
+ const linkTag = `<link rel="preload" href="${spriteHref}" as="image" type="image/svg+xml" />`;
53
+ let html = fs.readFileSync(htmlFile, "utf8");
54
+
55
+ if (html.includes(spriteMarker)) {
56
+ html = html.replace(
57
+ new RegExp(`${escapeRegExp(spriteMarker)}[\\s\\S]*?${escapeRegExp(spriteMarker)}`),
58
+ `${spriteMarker}\n ${linkTag}\n ${spriteMarker}`,
59
+ );
60
+ } else if (!html.includes(spriteHref)) {
61
+ html = html.replace("</head>", ` ${spriteMarker}\n ${linkTag}\n ${spriteMarker}\n </head>`);
62
+ }
63
+
64
+ fs.writeFileSync(htmlFile, html);
65
+ }
66
+
67
+ function escapeRegExp(value) {
68
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
69
+ }
70
+
71
+ function resolveFromCwd(cwd, target) {
72
+ return path.isAbsolute(target) ? target : path.join(cwd, target);
73
+ }
74
+
75
+ /**
76
+ * @param {Partial<typeof DEFAULTS> & { cwd?: string }} userOptions
77
+ */
78
+ export function buildIcons(userOptions = {}) {
79
+ const cwd = userOptions.cwd ? path.resolve(userOptions.cwd) : process.cwd();
80
+ const options = { ...DEFAULTS, ...userOptions };
81
+
82
+ const iconsDir = resolveFromCwd(cwd, options.iconsDir);
83
+ const spriteOut = resolveFromCwd(cwd, options.spriteOut);
84
+ const cssOut = resolveFromCwd(cwd, options.cssOut);
85
+ const typesOut = resolveFromCwd(cwd, options.typesOut);
86
+ const htmlFile = options.htmlFile ? resolveFromCwd(cwd, options.htmlFile) : null;
87
+
88
+ if (!fs.existsSync(iconsDir)) {
89
+ fs.mkdirSync(iconsDir, { recursive: true });
90
+ return {
91
+ ok: true,
92
+ createdIconsDir: true,
93
+ count: 0,
94
+ names: [],
95
+ outputs: {},
96
+ message: `Created ${options.iconsDir}/ — add .svg files and run mini-icon again.`,
97
+ };
98
+ }
99
+
100
+ const files = fs.readdirSync(iconsDir).filter((file) => file.endsWith(".svg"));
101
+ if (files.length === 0) {
102
+ return {
103
+ ok: false,
104
+ count: 0,
105
+ names: [],
106
+ outputs: {},
107
+ message: `No SVG files found in ${options.iconsDir}/`,
108
+ };
109
+ }
110
+
111
+ fs.mkdirSync(path.dirname(spriteOut), { recursive: true });
112
+ fs.mkdirSync(path.dirname(cssOut), { recursive: true });
113
+ fs.mkdirSync(path.dirname(typesOut), { recursive: true });
114
+
115
+ const names = [];
116
+ const rules = [];
117
+ const symbols = [];
118
+
119
+ for (const file of files.sort()) {
120
+ const name = path.basename(file, ".svg");
121
+ const content = fs.readFileSync(path.join(iconsDir, file), "utf8");
122
+ const { viewBox, inner } = extractSvgParts(content);
123
+
124
+ names.push(name);
125
+ symbols.push(` <symbol id="${name}" viewBox="${viewBox}">${inner}</symbol>`);
126
+ rules.push(`.icon.${name}::before {
127
+ -webkit-mask-image: ${toDataUri(content)};
128
+ mask-image: ${toDataUri(content)};
129
+ }`);
130
+ }
131
+
132
+ const sprite = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="display:none">\n${symbols.join("\n")}\n</svg>\n`;
133
+ fs.writeFileSync(spriteOut, sprite);
134
+
135
+ const css = `${options.cssBanner}
136
+ .icon {
137
+ display: inline-block;
138
+ width: 1em;
139
+ height: 1em;
140
+ min-width: 1em;
141
+ min-height: 1em;
142
+ line-height: 1;
143
+ flex-shrink: 0;
144
+ vertical-align: middle;
145
+ font-style: normal;
146
+ }
147
+
148
+ .icon::before {
149
+ content: "";
150
+ display: block;
151
+ width: 100%;
152
+ height: 100%;
153
+ background-color: currentColor;
154
+ mask-repeat: no-repeat;
155
+ mask-position: center;
156
+ mask-size: contain;
157
+ -webkit-mask-repeat: no-repeat;
158
+ -webkit-mask-position: center;
159
+ -webkit-mask-size: contain;
160
+ }
161
+
162
+ ${rules.join("\n\n")}
163
+ `;
164
+
165
+ fs.writeFileSync(cssOut, css);
166
+ fs.writeFileSync(typesOut, `export type Icon =\n${names.map((name) => ` | '${name}'`).join("\n")};\n`);
167
+
168
+ if (options.injectSpriteLink && htmlFile) {
169
+ injectSpriteLink(htmlFile, options.spriteHref, options.spriteMarker);
170
+ }
171
+
172
+ return {
173
+ ok: true,
174
+ count: names.length,
175
+ names,
176
+ outputs: {
177
+ spriteOut,
178
+ cssOut,
179
+ typesOut,
180
+ htmlFile: options.injectSpriteLink ? htmlFile : null,
181
+ },
182
+ message: `Built ${names.length} icon(s): ${names.join(", ")}`,
183
+ };
184
+ }
185
+
186
+ export { DEFAULTS };
package/src/cli.js ADDED
@@ -0,0 +1,55 @@
1
+ import path from "path";
2
+ import { buildIcons } from "./build.js";
3
+ import { loadConfig, parseCliArgs } from "./load-config.js";
4
+
5
+ function printHelp() {
6
+ console.log(`mini-icon — build SVG icons into CSS masks, sprite, and TypeScript types
7
+
8
+ Usage:
9
+ mini-icon [options]
10
+
11
+ Options:
12
+ --cwd <dir> Working directory (default: process.cwd())
13
+ --icons-dir <path> Source SVG folder (default: _icons)
14
+ --sprite-out <path> Output sprite path (default: public/icons.svg)
15
+ --css-out <path> Output CSS path (default: src/icons.css)
16
+ --types-out <path> Output TypeScript path (default: src/types/icon.ts)
17
+ --html-file <path> HTML file for optional sprite preload injection
18
+ --sprite-href <url> Sprite href used in HTML preload (default: /icons.svg)
19
+ --inject-html Inject sprite preload link into HTML
20
+ --no-inject-html Disable HTML injection
21
+ -h, --help Show help
22
+
23
+ Config:
24
+ mini-icon.config.js | .mjs | .json in project root
25
+ `);
26
+ }
27
+
28
+ export async function runCli(argv = process.argv) {
29
+ const { options: cliOptions, showHelp } = parseCliArgs(argv);
30
+ if (showHelp) {
31
+ printHelp();
32
+ return 0;
33
+ }
34
+
35
+ const cwd = cliOptions.cwd ?? process.cwd();
36
+ const fileConfig = await loadConfig(cwd);
37
+ const options = {
38
+ ...fileConfig,
39
+ ...cliOptions,
40
+ cwd,
41
+ };
42
+
43
+ const result = buildIcons(options);
44
+
45
+ if (result.message) {
46
+ if (result.ok) console.log(result.message);
47
+ else console.warn(result.message);
48
+ }
49
+
50
+ if (result.outputs?.spriteOut) {
51
+ console.log(`Sprite: ${path.relative(cwd, result.outputs.spriteOut)}`);
52
+ }
53
+
54
+ return result.ok || result.createdIconsDir ? 0 : 1;
55
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { buildIcons, DEFAULTS } from "./build.js";
2
+ export { loadConfig, parseCliArgs } from "./load-config.js";
3
+ export { runCli } from "./cli.js";
@@ -0,0 +1,78 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { pathToFileURL } from "url";
4
+ import { DEFAULTS } from "./build.js";
5
+
6
+ const CONFIG_NAMES = ["mini-icon.config.js", "mini-icon.config.mjs", "mini-icon.config.json"];
7
+
8
+ export async function loadConfig(cwd = process.cwd()) {
9
+ for (const name of CONFIG_NAMES) {
10
+ const filePath = path.join(cwd, name);
11
+ if (!fs.existsSync(filePath)) continue;
12
+
13
+ if (name.endsWith(".json")) {
14
+ return {
15
+ ...DEFAULTS,
16
+ ...JSON.parse(fs.readFileSync(filePath, "utf8")),
17
+ configFile: filePath,
18
+ };
19
+ }
20
+
21
+ const module = await import(`${pathToFileURL(filePath).href}?t=${Date.now()}`);
22
+ const config = module.default ?? module.config ?? module;
23
+ return {
24
+ ...DEFAULTS,
25
+ ...config,
26
+ configFile: filePath,
27
+ };
28
+ }
29
+
30
+ return { ...DEFAULTS, configFile: null };
31
+ }
32
+
33
+ export function parseCliArgs(argv) {
34
+ const args = argv.slice(2);
35
+ const options = { cwd: process.cwd() };
36
+ let showHelp = false;
37
+
38
+ for (let index = 0; index < args.length; index += 1) {
39
+ const arg = args[index];
40
+
41
+ if (arg === "--help" || arg === "-h") {
42
+ showHelp = true;
43
+ continue;
44
+ }
45
+
46
+ if (arg === "--cwd") {
47
+ options.cwd = path.resolve(args[index + 1] ?? process.cwd());
48
+ index += 1;
49
+ continue;
50
+ }
51
+
52
+ const map = {
53
+ "--icons-dir": "iconsDir",
54
+ "--sprite-out": "spriteOut",
55
+ "--css-out": "cssOut",
56
+ "--types-out": "typesOut",
57
+ "--html-file": "htmlFile",
58
+ "--sprite-href": "spriteHref",
59
+ };
60
+
61
+ if (arg in map) {
62
+ options[map[arg]] = args[index + 1];
63
+ index += 1;
64
+ continue;
65
+ }
66
+
67
+ if (arg === "--inject-html") {
68
+ options.injectSpriteLink = true;
69
+ continue;
70
+ }
71
+
72
+ if (arg === "--no-inject-html") {
73
+ options.injectSpriteLink = false;
74
+ }
75
+ }
76
+
77
+ return { options, showHelp };
78
+ }