Skip to content

Commit 1a55df4

Browse files
committed
fix icon export names, add test verifying import matches lucide
1 parent a50928c commit 1a55df4

File tree

14 files changed

+246
-59
lines changed

14 files changed

+246
-59
lines changed
Lines changed: 114 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
#!/usr/bin/env node
2+
/**
3+
* Generate icon wrapper components.
4+
*
5+
* Strategy:
6+
* 1. Get all SVGs from lucide-static
7+
* 2. Get export names from lucide-react
8+
* 3. For icons in lucide-react: use their exact name (for drop-in compatibility)
9+
* 4. For icons only in lucide-static: derive name from filename
10+
*
11+
* This ensures:
12+
* - All lucide-react imports work as drop-in replacement
13+
* - Extra icons from lucide-static are also available
14+
* - Future icon packs can be added
15+
*/
216
import fs from "fs";
317
import path from "path";
418

519
const __dirname = path.resolve(process.cwd());
620

7-
// 1️⃣ Where Lucide's raw SVGs live
8-
const ICON_SVG_DIR = path.resolve(__dirname, "node_modules/lucide-static/icons");
9-
10-
// 2️⃣ Where to write your wrappers
21+
// Where to write wrappers
1122
const OUT_DIR = path.resolve("src/icons");
1223

1324
// Preserve manually maintained files
@@ -25,43 +36,112 @@ if (fs.existsSync(OUT_DIR)) {
2536
fs.mkdirSync(OUT_DIR, { recursive: true });
2637
}
2738

28-
// 3️⃣ Read every SVG filename
29-
const files = fs.readdirSync(ICON_SVG_DIR).filter((f) => f.endsWith(".svg"));
39+
// ============================================================================
40+
// Helper functions
41+
// ============================================================================
3042

31-
// 4️⃣ Helper: kebab ⇄ Pascal
32-
const toPascal = (s) =>
33-
s
43+
// Convert kebab-case to PascalCase
44+
function kebabToPascal(str) {
45+
return str
3446
.split("-")
3547
.map((w) => w[0].toUpperCase() + w.slice(1))
3648
.join("");
49+
}
3750

38-
// 5️⃣ Track generated names to detect case collisions (macOS is case-insensitive)
39-
const generatedNames = new Map(); // lowercase -> original pascal name
51+
// Convert PascalCase to kebab-case
52+
function pascalToKebab(str) {
53+
return str
54+
.replace(/([a-z])([A-Z])/g, "$1-$2")
55+
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
56+
.replace(/([a-zA-Z])(\d)/g, "$1-$2")
57+
.replace(/(\d)([a-zA-Z])/g, "$1-$2")
58+
.toLowerCase();
59+
}
4060

41-
// 6️⃣ Generate one .tsx per icon
42-
let skippedCount = 0;
43-
for (const file of files) {
44-
const id = file.replace(".svg", ""); // e.g. "arrow-right"
45-
const pascal = toPascal(id); // "ArrowRight"
46-
const lowerPascal = pascal.toLowerCase();
61+
// ============================================================================
62+
// Get data from lucide packages
63+
// ============================================================================
4764

48-
// Skip case collisions (e.g., ArrowDownAZ vs ArrowDownAz on case-insensitive filesystems)
49-
if (generatedNames.has(lowerPascal)) {
50-
skippedCount++;
51-
continue;
65+
// Get all SVG IDs from lucide-static
66+
function getStaticSvgIds() {
67+
const svgDir = path.resolve(__dirname, "node_modules/lucide-static/icons");
68+
const files = fs.readdirSync(svgDir).filter((f) => f.endsWith(".svg"));
69+
return new Set(files.map((f) => f.replace(".svg", "")));
70+
}
71+
72+
// Get export names from lucide-react (for compatibility mapping)
73+
function getLucideReactExports() {
74+
const lucideDts = path.resolve(__dirname, "node_modules/lucide-react/dist/lucide-react.d.ts");
75+
if (!fs.existsSync(lucideDts)) {
76+
console.warn("⚠️ lucide-react types not found. Using derived names only.");
77+
return new Map();
78+
}
79+
const content = fs.readFileSync(lucideDts, "utf8");
80+
81+
const exports = new Map(); // kebab-ish key -> exact export name
82+
83+
const matches = content.matchAll(/declare const (\w+): react\.ForwardRefExoticComponent/g);
84+
for (const match of matches) {
85+
const name = match[1];
86+
if (!name.startsWith("Lucide") && !name.endsWith("Icon")) {
87+
// Create a normalized key for matching
88+
const key = pascalToKebab(name).replace(/-/g, "");
89+
exports.set(key, name);
90+
}
5291
}
53-
generatedNames.set(lowerPascal, pascal);
92+
93+
return exports;
94+
}
5495

96+
// Find lucide-react's name for an SVG ID, or derive one
97+
function getComponentName(svgId, lucideReactExports) {
98+
// Normalize the SVG ID for lookup
99+
const normalizedKey = svgId.replace(/-/g, "");
100+
101+
// Check if lucide-react has this icon
102+
if (lucideReactExports.has(normalizedKey)) {
103+
return lucideReactExports.get(normalizedKey);
104+
}
105+
106+
// Derive name from SVG filename
107+
return kebabToPascal(svgId);
108+
}
109+
110+
// ============================================================================
111+
// Generate wrappers
112+
// ============================================================================
113+
114+
const staticSvgIds = getStaticSvgIds();
115+
const lucideReactExports = getLucideReactExports();
116+
117+
// Track generated names for case collision detection (macOS is case-insensitive)
118+
const generatedNames = new Map(); // lowercase -> { name, svgId }
119+
120+
let generatedCount = 0;
121+
let skippedCollisions = 0;
122+
123+
for (const svgId of staticSvgIds) {
124+
const componentName = getComponentName(svgId, lucideReactExports);
125+
const lowerName = componentName.toLowerCase();
126+
127+
// Skip case collisions on case-insensitive filesystems
128+
if (generatedNames.has(lowerName)) {
129+
skippedCollisions++;
130+
continue;
131+
}
132+
133+
generatedNames.set(lowerName, { name: componentName, svgId });
134+
55135
const wrapperTsx = `\
56136
import { SPRITE_PATH } from "../config.js";
57137
import { warnMissingIconSize } from "../utils.js";
58-
import { ${pascal} as DevIcon } from "lucide-react"
138+
import { ${componentName} as DevIcon } from "lucide-react"
59139
import { renderUse,type IconProps,} from "../_shared.js";
60140
61141
62142
63-
export function ${pascal}({ size, width, height, ...props }: IconProps) {
64-
warnMissingIconSize("${pascal}", size, width, height);
143+
export function ${componentName}({ size, width, height, ...props }: IconProps) {
144+
warnMissingIconSize("${componentName}", size, width, height);
65145
if (process.env.NODE_ENV !== "production" && DevIcon) {
66146
return (
67147
<DevIcon
@@ -72,11 +152,17 @@ export function ${pascal}({ size, width, height, ...props }: IconProps) {
72152
/>
73153
);
74154
}
75-
return renderUse("${id}", width, height, size, SPRITE_PATH, props)
155+
return renderUse("${svgId}", width, height, size, SPRITE_PATH, props)
76156
}
77157
`;
78158

79-
fs.writeFileSync(path.join(OUT_DIR, `${pascal}.tsx`), wrapperTsx);
159+
fs.writeFileSync(path.join(OUT_DIR, `${componentName}.tsx`), wrapperTsx);
160+
generatedCount++;
80161
}
81162

82-
console.log(`✅ Generated ${generatedNames.size} icon wrappers in ${OUT_DIR}${skippedCount > 0 ? ` (skipped ${skippedCount} case-collision aliases)` : ""}`);
163+
console.log(`✅ Generated ${generatedCount} icon wrappers in ${OUT_DIR}`);
164+
if (skippedCollisions > 0) {
165+
console.log(` (skipped ${skippedCollisions} case-collision aliases)`);
166+
}
167+
console.log(` lucide-react compatible: ${lucideReactExports.size} icons`);
168+
console.log(` Extra from lucide-static: ${generatedCount - lucideReactExports.size} icons`);

icon-sprite/src/icons/Axis3d.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { SPRITE_PATH } from "../config.js";
22
import { warnMissingIconSize } from "../utils.js";
3-
import { Axis3D as DevIcon } from "lucide-react"
3+
import { Axis3d as DevIcon } from "lucide-react"
44
import { renderUse,type IconProps,} from "../_shared.js";
55

66

77

8-
export function Axis3D({ size, width, height, ...props }: IconProps) {
9-
warnMissingIconSize("Axis3D", size, width, height);
8+
export function Axis3d({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("Axis3d", size, width, height);
1010
if (process.env.NODE_ENV !== "production" && DevIcon) {
1111
return (
1212
<DevIcon

icon-sprite/src/icons/FileAxis3d.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { SPRITE_PATH } from "../config.js";
22
import { warnMissingIconSize } from "../utils.js";
3-
import { FileAxis3D as DevIcon } from "lucide-react"
3+
import { FileAxis3d as DevIcon } from "lucide-react"
44
import { renderUse,type IconProps,} from "../_shared.js";
55

66

77

8-
export function FileAxis3D({ size, width, height, ...props }: IconProps) {
9-
warnMissingIconSize("FileAxis3D", size, width, height);
8+
export function FileAxis3d({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("FileAxis3d", size, width, height);
1010
if (process.env.NODE_ENV !== "production" && DevIcon) {
1111
return (
1212
<DevIcon

icon-sprite/src/icons/Grid2x2.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { SPRITE_PATH } from "../config.js";
22
import { warnMissingIconSize } from "../utils.js";
3-
import { Grid2X2 as DevIcon } from "lucide-react"
3+
import { Grid2x2 as DevIcon } from "lucide-react"
44
import { renderUse,type IconProps,} from "../_shared.js";
55

66

77

8-
export function Grid2X2({ size, width, height, ...props }: IconProps) {
9-
warnMissingIconSize("Grid2X2", size, width, height);
8+
export function Grid2x2({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("Grid2x2", size, width, height);
1010
if (process.env.NODE_ENV !== "production" && DevIcon) {
1111
return (
1212
<DevIcon

icon-sprite/src/icons/Grid2x2Check.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { SPRITE_PATH } from "../config.js";
22
import { warnMissingIconSize } from "../utils.js";
3-
import { Grid2X2Check as DevIcon } from "lucide-react"
3+
import { Grid2x2Check as DevIcon } from "lucide-react"
44
import { renderUse,type IconProps,} from "../_shared.js";
55

66

77

8-
export function Grid2X2Check({ size, width, height, ...props }: IconProps) {
9-
warnMissingIconSize("Grid2X2Check", size, width, height);
8+
export function Grid2x2Check({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("Grid2x2Check", size, width, height);
1010
if (process.env.NODE_ENV !== "production" && DevIcon) {
1111
return (
1212
<DevIcon

icon-sprite/src/icons/Grid2x2Plus.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { SPRITE_PATH } from "../config.js";
22
import { warnMissingIconSize } from "../utils.js";
3-
import { Grid2X2Plus as DevIcon } from "lucide-react"
3+
import { Grid2x2Plus as DevIcon } from "lucide-react"
44
import { renderUse,type IconProps,} from "../_shared.js";
55

66

77

8-
export function Grid2X2Plus({ size, width, height, ...props }: IconProps) {
9-
warnMissingIconSize("Grid2X2Plus", size, width, height);
8+
export function Grid2x2Plus({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("Grid2x2Plus", size, width, height);
1010
if (process.env.NODE_ENV !== "production" && DevIcon) {
1111
return (
1212
<DevIcon

icon-sprite/src/icons/Grid2x2X.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { SPRITE_PATH } from "../config.js";
22
import { warnMissingIconSize } from "../utils.js";
3-
import { Grid2X2X as DevIcon } from "lucide-react"
3+
import { Grid2x2X as DevIcon } from "lucide-react"
44
import { renderUse,type IconProps,} from "../_shared.js";
55

66

77

8-
export function Grid2X2X({ size, width, height, ...props }: IconProps) {
9-
warnMissingIconSize("Grid2X2X", size, width, height);
8+
export function Grid2x2X({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("Grid2x2X", size, width, height);
1010
if (process.env.NODE_ENV !== "production" && DevIcon) {
1111
return (
1212
<DevIcon

icon-sprite/src/icons/Grid3x3.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { SPRITE_PATH } from "../config.js";
22
import { warnMissingIconSize } from "../utils.js";
3-
import { Grid3X3 as DevIcon } from "lucide-react"
3+
import { Grid3x3 as DevIcon } from "lucide-react"
44
import { renderUse,type IconProps,} from "../_shared.js";
55

66

77

8-
export function Grid3X3({ size, width, height, ...props }: IconProps) {
9-
warnMissingIconSize("Grid3X3", size, width, height);
8+
export function Grid3x3({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("Grid3x3", size, width, height);
1010
if (process.env.NODE_ENV !== "production" && DevIcon) {
1111
return (
1212
<DevIcon

icon-sprite/src/icons/Move3d.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { SPRITE_PATH } from "../config.js";
22
import { warnMissingIconSize } from "../utils.js";
3-
import { Move3D as DevIcon } from "lucide-react"
3+
import { Move3d as DevIcon } from "lucide-react"
44
import { renderUse,type IconProps,} from "../_shared.js";
55

66

77

8-
export function Move3D({ size, width, height, ...props }: IconProps) {
9-
warnMissingIconSize("Move3D", size, width, height);
8+
export function Move3d({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("Move3d", size, width, height);
1010
if (process.env.NODE_ENV !== "production" && DevIcon) {
1111
return (
1212
<DevIcon

icon-sprite/src/icons/Rotate3d.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { SPRITE_PATH } from "../config.js";
22
import { warnMissingIconSize } from "../utils.js";
3-
import { Rotate3D as DevIcon } from "lucide-react"
3+
import { Rotate3d as DevIcon } from "lucide-react"
44
import { renderUse,type IconProps,} from "../_shared.js";
55

66

77

8-
export function Rotate3D({ size, width, height, ...props }: IconProps) {
9-
warnMissingIconSize("Rotate3D", size, width, height);
8+
export function Rotate3d({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("Rotate3d", size, width, height);
1010
if (process.env.NODE_ENV !== "production" && DevIcon) {
1111
return (
1212
<DevIcon

0 commit comments

Comments
 (0)