Skip to content

Commit 39b674e

Browse files
committed
added tests
1 parent 9d79df5 commit 39b674e

File tree

10 files changed

+1292
-33
lines changed

10 files changed

+1292
-33
lines changed

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,36 @@ This is one of the **most optimized** icon systems for serious frontends:
187187
* Fully compatible with Next.js 15+, Vite, or any modern React stack.
188188

189189
---
190-
190+
191+
192+
193+
<!--
194+
📂 icon-sprite/
195+
├── 📂 node_modules
196+
│ └── 📂 lucide-static
197+
│ │ └── 📂 icons
198+
│ │ └── *icon-name*.svg
199+
├── 📂 dist
200+
│ │── config.js
201+
│ │── index.js
202+
│ │── used-icons.js
203+
│ │── utils.js
204+
│ └── icons.svg
205+
│── 📂 scripts
206+
│ │── build-sprite.js
207+
│ │── gen-dist.js
208+
│ │── gen-wrappers.js
209+
│ │── index.js
210+
│ │── scan-icons.js
211+
│ └── used-icons.js
212+
│── 📂 src
213+
│ │── 📂 icons
214+
│ │ │── *IconName*.tsx
215+
│ │── config.ts
216+
│ └── utils.ts
217+
│── README.md
218+
│── package-lock.json
219+
│── package.json
220+
│── react-zero-ui-icon-sprite-0.1.3.tgz
221+
└── tsconfig.json
222+
-->

icon-sprite/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"sideEffects": false,
2424
"license": "MIT",
2525
"private": false,
26-
"version": "0.1.3",
26+
"version": "0.1.4",
2727
"type": "module",
2828
"main": "dist/index.js",
2929
"module": "dist/index.js",
@@ -37,7 +37,8 @@
3737
],
3838
"scripts": {
3939
"build": "rm -rf dist && node scripts/gen-wrappers.js && tsc && node scripts/gen-dist.js",
40-
"prepare": "npm run build",
40+
"test": "node tests/test-mapping.test.js",
41+
"prepare": "npm run build && npm run test",
4142
"type-check": "tsc --noEmit | tee type-errors.log"
4243
},
4344
"bin": {

icon-sprite/scripts/build-sprite.js

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,16 @@ import { createRequire } from "module";
55
import svgstore from "svgstore";
66
import { ICONS } from "./used-icons.js";
77
import { SPRITE_PATH, CUSTOM_SVG_DIR } from "../dist/config.js";
8+
import { componentNameToStaticId } from "../dist/utils.js";
89

910
const require = createRequire(import.meta.url);
1011

1112
// 1️⃣ Resolve lucide-static icons
1213
const sample = require.resolve("../../../lucide-static/icons/mail.svg");
1314
const iconsDir = path.dirname(sample);
1415

15-
// 2️⃣ pascal→kebab
16-
function toKebab(s) {
17-
return s
18-
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
19-
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
20-
.toLowerCase();
21-
}
22-
23-
// 3️⃣ Gather your needed Lucide IDs
24-
const needed = new Set(ICONS.map(toKebab));
16+
// 2️⃣ Gather your needed Lucide IDs
17+
const needed = new Set(ICONS.map(componentNameToStaticId));
2518
const store = svgstore({
2619
copyAttrs: ["viewBox", "fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin", "style", "size"],
2720
svgAttrs: {
@@ -32,7 +25,7 @@ const store = svgstore({
3225
});
3326
const found = new Set();
3427

35-
// 4️⃣ Add only the Lucide icons you actually use
28+
// 3️⃣ Add only the Lucide icons you actually use
3629
for (const file of fs.readdirSync(iconsDir)) {
3730
if (!file.endsWith(".svg")) continue;
3831
const id = file.slice(0, -4);
@@ -41,7 +34,7 @@ for (const file of fs.readdirSync(iconsDir)) {
4134
store.add(id, fs.readFileSync(path.join(iconsDir, file), "utf8"));
4235
}
4336

44-
// 5️⃣ Optionally include *all* SVGs from your custom folder
37+
// 4️⃣ Optionally include *all* SVGs from your custom folder
4538
const customDir = path.resolve(process.cwd(), CUSTOM_SVG_DIR);
4639
if (fs.existsSync(customDir)) {
4740
for (const file of fs.readdirSync(customDir)) {
@@ -55,13 +48,13 @@ if (fs.existsSync(customDir)) {
5548
}
5649
}
5750

58-
// 6️⃣ Error on any missing Lucide icon
51+
// 5️⃣ Error on any missing Lucide icon
5952
const missing = [...needed].filter((id) => !found.has(id));
6053
if (missing.length) {
6154
throw new Error(`❌ Missing icons: ${missing.join(", ")}`);
6255
}
6356

64-
// 7️⃣ Write a fresh sprite (inline: true drops xml/doctype)
57+
// 6️⃣ Write a fresh sprite (inline: true drops xml/doctype)
6558
const sprite = store.toString({ inline: true });
6659
const outDir = path.join(process.cwd(), "public");
6760
const outFile = path.join(outDir, SPRITE_PATH);

icon-sprite/src/utils.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,77 @@ export function warnMissingIconSize(name: string, size: number | string | undefi
2121
);
2222
}
2323
}
24+
25+
// Tokenize PascalCase + digits with Lucide quirks
26+
export // src/utils.ts
27+
/**
28+
* Generate the lucide-static filename for a given Lucide React component.
29+
* If `available` is provided, returns the first candidate that exists.
30+
*/
31+
function componentNameToStaticId(name: string, available?: Set<string>): string {
32+
// 1) Tokenize PascalCase + digits + lowercase, preserving special forms
33+
// - "3d" stays as "3d"
34+
// - uppercase runs "AZ" become ["a","z"] (primary), but we’ll also try keeping them
35+
// - digits "10" default to per-digit ["1","0"] (primary), but we also try keeping "10"
36+
// - merge pattern DIGITS + 'x' + DIGITS into "2x2" (primary)
37+
const re = /(\d+d)|([A-Z]+(?![a-z]))|([A-Z][a-z]+)|(\d+)|([a-z])/g;
38+
const raw: string[] = [];
39+
let m: RegExpExecArray | null;
40+
while ((m = re.exec(name))) {
41+
const [, dPlusD, upperRun, capWord, digits, lower] = m;
42+
if (dPlusD) raw.push(dPlusD.toLowerCase());
43+
else if (upperRun) raw.push(upperRun); // keep run for now, lower later
44+
else if (capWord) raw.push(capWord);
45+
else if (digits) raw.push(digits);
46+
else if (lower) raw.push(lower);
47+
}
48+
49+
// Merge DIGITS 'x' DIGITS -> "2x2"
50+
const merged: string[] = [];
51+
for (let i = 0; i < raw.length; i++) {
52+
const a = raw[i],
53+
b = raw[i + 1],
54+
c = raw[i + 2];
55+
if (a && b === "x" && c && /^\d+$/.test(a) && /^\d+$/.test(c)) {
56+
merged.push(`${a}x${c}`);
57+
i += 2;
58+
} else {
59+
merged.push(a);
60+
}
61+
}
62+
63+
// helper transforms
64+
const lower = (t: string) => t.toLowerCase();
65+
const splitUpperRun = (t: string) => (/^[A-Z]+$/.test(t) ? t.toLowerCase().split("") : [t]);
66+
const splitDigits = (t: string) => (/^\d+$/.test(t) ? t.split("") : [t]);
67+
const keepDigits = (t: string) => [t]; // no-op, kept as is
68+
const keepUpperRun = (t: string) => (/^[A-Z]+$/.test(t) ? [t.toLowerCase()] : [t]);
69+
70+
// Build candidate variants in descending preference
71+
// Primary matches ArrowUp10 (1-0) & AZ (a-z) & 2x2 merged
72+
const v1 = merged.flatMap(splitUpperRun).flatMap(splitDigits).map(lower).join("-");
73+
// Keep digit groups (Clock10 -> 10), split caps (AZ -> a-z)
74+
const v2 = merged.flatMap(splitUpperRun).flatMap(keepDigits).map(lower).join("-");
75+
// Split digits, keep cap runs (AZ -> az)
76+
const v3 = merged.flatMap(keepUpperRun).flatMap(splitDigits).map(lower).join("-");
77+
// Keep both caps & digits
78+
const v4 = merged.flatMap(keepUpperRun).flatMap(keepDigits).map(lower).join("-");
79+
80+
const candidates = dedupe([v1, v2, v3, v4]);
81+
82+
if (available && available.size) {
83+
for (const id of candidates) if (available.has(id)) return id;
84+
}
85+
return candidates[0]; // deterministic fallback
86+
}
87+
88+
function dedupe<T>(arr: T[]): T[] {
89+
const s = new Set<T>();
90+
const out: T[] = [];
91+
for (const x of arr)
92+
if (!s.has(x)) {
93+
s.add(x);
94+
out.push(x);
95+
}
96+
return out;
97+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env node
2+
import fs from "fs";
3+
import path from "path";
4+
import { createRequire } from "module";
5+
import { fileURLToPath } from "url";
6+
7+
const require = createRequire(import.meta.url);
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = path.dirname(__filename);
10+
11+
// --- mapper (same logic your lib uses) -------------------------------------
12+
import { componentNameToStaticId } from "../dist/utils.js";
13+
// --- locate dirs ------------------------------------------------------------
14+
function resolveLucideStaticIconsDir() {
15+
const anyIcon = require.resolve("lucide-static/icons/arrow-up.svg");
16+
return path.dirname(anyIcon);
17+
}
18+
19+
const SRC_ICONS_DIR = path.resolve(__dirname, "../src/icons");
20+
const STATIC_DIR = resolveLucideStaticIconsDir();
21+
22+
// --- read component names from filenames -----------------------------------
23+
function getReactIconComponentNames() {
24+
if (!fs.existsSync(SRC_ICONS_DIR)) {
25+
throw new Error(`Missing dir: ${SRC_ICONS_DIR}. Generate wrappers first.`);
26+
}
27+
const exts = new Set([".tsx", ".jsx", ".ts", ".js"]);
28+
return (
29+
fs
30+
.readdirSync(SRC_ICONS_DIR)
31+
.filter((f) => exts.has(path.extname(f)))
32+
.map((f) => path.basename(f, path.extname(f)))
33+
// exclude non-lucide helpers if present
34+
.filter((n) => n !== "CustomIcon" && n !== "index")
35+
);
36+
}
37+
38+
// --- run checks -------------------------------------------------------------
39+
const availableStatic = new Set(
40+
fs
41+
.readdirSync(STATIC_DIR)
42+
.filter((f) => f.endsWith(".svg"))
43+
.map((f) => f.slice(0, -4))
44+
);
45+
46+
const reactNames = getReactIconComponentNames();
47+
const missing = [];
48+
for (const comp of reactNames) {
49+
const id = componentNameToStaticId(comp, availableStatic);
50+
if (!availableStatic.has(id)) missing.push(`${comp} -> ${id} (no svg)`);
51+
52+
// format sanity
53+
if (!/^[a-z0-9-]+$/.test(id)) {
54+
missing.push(`${comp} -> ${id} (invalid chars)`);
55+
continue;
56+
}
57+
if (!availableStatic.has(id)) {
58+
missing.push(`${comp} -> ${id} (no svg)`);
59+
}
60+
}
61+
62+
if (missing.length) {
63+
const sample = missing.slice(0, 50).join("\n ");
64+
console.error(`❌ Mapping failures (${missing.length}):\n ${sample}\n` + `Search path: ${STATIC_DIR}\n` + `Wrappers dir: ${SRC_ICONS_DIR}`);
65+
process.exit(1);
66+
}
67+
68+
console.log(`✅ ${reactNames.length} components map to existing lucide-static SVGs.`);

next-app/.husky/pre-commit

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)