Skip to content

Commit 881278f

Browse files
committed
v0.2.0 - Add config file option + new lucid icons to lib
1 parent c3a037c commit 881278f

File tree

293 files changed

+6126
-174
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

293 files changed

+6126
-174
lines changed

icon-sprite/package.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@react-zero-ui/icon-sprite",
3-
"version": "0.1.6",
3+
"version": "0.2.0",
44
"description": "Generate a single SVG sprite containing only the icons you used (Lucide + custom). Lucid to SVG sprite solution for React.",
55
"keywords": [
66
"react",
@@ -38,6 +38,7 @@
3838
"scripts": {
3939
"build": "rm -rf dist && node scripts/gen-wrappers.js && tsc && node scripts/gen-dist.js",
4040
"test": "node tests/run-all-tests.js",
41+
"test:integration": "node tests/integration-test.js",
4142
"prepare": "npm run build && npm run test",
4243
"type-check": "tsc --noEmit | tee type-errors.log"
4344
},
@@ -48,18 +49,18 @@
4849
"react": ">=17"
4950
},
5051
"dependencies": {
51-
"@babel/core": "^7.28.0",
52-
"@babel/preset-react": "^7.27.1",
53-
"@babel/preset-typescript": "^7.27.1",
54-
"@babel/traverse": "^7.28.0",
55-
"lucide-react": "^0.525.0",
56-
"lucide-static": "^0.525.0",
52+
"@babel/core": "^7.28.5",
53+
"@babel/preset-react": "^7.28.5",
54+
"@babel/preset-typescript": "^7.28.5",
55+
"@babel/traverse": "^7.28.5",
56+
"lucide-react": "^0.555.0",
57+
"lucide-static": "^0.555.0",
5758
"svgstore": "^3.0.1"
5859
},
5960
"devDependencies": {
60-
"@types/node": "^24.2.0",
61+
"@types/node": "^24.10.1",
6162
"@types/parse5": "^6.0.3",
62-
"@types/react": "^19.1.9",
63-
"typescript": "^5.0.0"
63+
"@types/react": "^19.2.7",
64+
"typescript": "^5.9.3"
6465
}
65-
}
66+
}

icon-sprite/scripts/build-sprite.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import path from "path";
44
import { createRequire } from "module";
55
import svgstore from "svgstore";
66
import { ICONS } from "./used-icons.js";
7-
import { SPRITE_PATH, CUSTOM_SVG_DIR } from "../dist/config.js";
7+
import { loadConfig } from "../dist/loadConfig.js";
88
import { componentNameToStaticId } from "../dist/utils.js";
99

1010
const require = createRequire(import.meta.url);
1111

12+
// Load user config (merged with defaults)
13+
const { SPRITE_PATH, CUSTOM_SVG_DIR, OUTPUT_DIR } = await loadConfig();
14+
1215
// 1️⃣ Resolve lucide-static icons
1316
const sample = require.resolve("../../../lucide-static/icons/mail.svg");
1417
const iconsDir = path.dirname(sample);
@@ -35,7 +38,7 @@ for (const file of fs.readdirSync(iconsDir)) {
3538
}
3639

3740
// 4️⃣ Optionally include *all* SVGs from your custom folder
38-
const customDir = path.resolve(process.cwd(), "public", CUSTOM_SVG_DIR);
41+
const customDir = path.resolve(process.cwd(), OUTPUT_DIR, CUSTOM_SVG_DIR);
3942
if (fs.existsSync(customDir)) {
4043
for (const file of fs.readdirSync(customDir)) {
4144
if (!file.endsWith(".svg")) continue;
@@ -56,7 +59,7 @@ if (missing.length) {
5659

5760
// 6️⃣ Write a fresh sprite (inline: true drops xml/doctype)
5861
const sprite = store.toString({ inline: true });
59-
const outDir = path.join(process.cwd(), "public");
62+
const outDir = path.join(process.cwd(), OUTPUT_DIR);
6063
const outFile = path.join(outDir, SPRITE_PATH);
6164

6265
fs.mkdirSync(outDir, { recursive: true });

icon-sprite/scripts/gen-dist.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@ const dtsOut = path.join(distDir, "index.d.ts");
1111

1212
// 1. Find every compiled icon wrapper
1313
const files = fs.readdirSync(iconsDir).filter((f) => f.endsWith(".js") && f !== "index.js");
14-
// 2. Build the JS barrel
14+
// 2. Build the JS barrel (icons only - types don't exist at runtime)
1515
const jsLines = files.map((file) => {
1616
const name = path.basename(file, ".js");
1717
return `export { ${name} } from "./icons/${name}.js";`;
1818
});
1919

20-
// 3. Build the TS declaration barrel
20+
// 3. Build the TS declaration barrel (icons + type export)
2121
const dtsLines = files.map((file) => {
2222
const name = path.basename(file, ".js");
2323
return `export { ${name} } from "./icons/${name}";`;
2424
});
25+
// Re-export ZeroUIConfig type for user config files (type-only, .d.ts only)
26+
dtsLines.push(`export type { ZeroUIConfig } from "./config";`);
2527

2628
// 4. Write both barrels
2729
fs.writeFileSync(jsOut, jsLines.join("\n") + "\n", "utf8");

icon-sprite/scripts/gen-wrappers.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,21 @@ const ICON_SVG_DIR = path.resolve(__dirname, "node_modules/lucide-static/icons")
99

1010
// 2️⃣ Where to write your wrappers
1111
const OUT_DIR = path.resolve("src/icons");
12-
fs.mkdirSync(OUT_DIR, { recursive: true });
12+
13+
// Preserve manually maintained files
14+
const PRESERVE_FILES = ["CustomIcon.tsx"];
15+
16+
// Clean old generated files before regenerating
17+
if (fs.existsSync(OUT_DIR)) {
18+
const existing = fs.readdirSync(OUT_DIR);
19+
for (const file of existing) {
20+
if (!PRESERVE_FILES.includes(file)) {
21+
fs.rmSync(path.join(OUT_DIR, file));
22+
}
23+
}
24+
} else {
25+
fs.mkdirSync(OUT_DIR, { recursive: true });
26+
}
1327

1428
// 3️⃣ Read every SVG filename
1529
const files = fs.readdirSync(ICON_SVG_DIR).filter((f) => f.endsWith(".svg"));
@@ -21,10 +35,23 @@ const toPascal = (s) =>
2135
.map((w) => w[0].toUpperCase() + w.slice(1))
2236
.join("");
2337

24-
// 5️⃣ Generate one .tsx per icon
38+
// 5️⃣ Track generated names to detect case collisions (macOS is case-insensitive)
39+
const generatedNames = new Map(); // lowercase -> original pascal name
40+
41+
// 6️⃣ Generate one .tsx per icon
42+
let skippedCount = 0;
2543
for (const file of files) {
2644
const id = file.replace(".svg", ""); // e.g. "arrow-right"
2745
const pascal = toPascal(id); // "ArrowRight"
46+
const lowerPascal = pascal.toLowerCase();
47+
48+
// Skip case collisions (e.g., ArrowDownAZ vs ArrowDownAz on case-insensitive filesystems)
49+
if (generatedNames.has(lowerPascal)) {
50+
skippedCount++;
51+
continue;
52+
}
53+
generatedNames.set(lowerPascal, pascal);
54+
2855
const wrapperTsx = `\
2956
import { SPRITE_PATH } from "../config.js";
3057
import { warnMissingIconSize } from "../utils.js";
@@ -52,4 +79,4 @@ export function ${pascal}({ size, width, height, ...props }: IconProps) {
5279
fs.writeFileSync(path.join(OUT_DIR, `${pascal}.tsx`), wrapperTsx);
5380
}
5481

55-
console.log(`✅ Generated ${files.length} icon wrappers in ${OUT_DIR}`);
82+
console.log(`✅ Generated ${generatedNames.size} icon wrappers in ${OUT_DIR}${skippedCount > 0 ? ` (skipped ${skippedCount} case-collision aliases)` : ""}`);

icon-sprite/scripts/scan-icons.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import { fileURLToPath } from "url";
55
import * as babel from "@babel/core";
66
import traverseImport from "@babel/traverse";
77
import * as t from "@babel/types";
8-
import { IMPORT_NAME, ROOT_DIR, IGNORE_ICONS, EXCLUDE_DIRS } from "../dist/config.js";
8+
import { loadConfig } from "../dist/loadConfig.js";
99

1010
// ESM __dirname shim
1111
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1212
const ICONS = new Set();
1313

14+
// Load user config (merged with defaults)
15+
const { IMPORT_NAME, ROOT_DIR, IGNORE_ICONS, EXCLUDE_DIRS } = await loadConfig();
16+
1417
// 1️⃣ Find the consuming app's root
1518
function findProjectRoot(dir = process.cwd()) {
1619
let current = dir;

icon-sprite/src/config.ts

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,29 @@
11
// src/config.ts
2-
import fs from "fs";
3-
import path from "path";
4-
import { pathToFileURL } from "url";
2+
// Client-safe config - NO fs, NO Node.js modules
3+
// These are the default values. User overrides are applied at build time via CLI.
54

6-
const DEFAULT_CONFIG = {
7-
IMPORT_NAME: "@react-zero-ui/icon-sprite",
8-
SPRITE_PATH: "/icons.svg",
9-
ROOT_DIR: "src",
10-
CUSTOM_SVG_DIR: "zero-ui-icons",
11-
OUTPUT_DIR: "public",
12-
IGNORE_ICONS: ["CustomIcon"],
13-
EXCLUDE_DIRS: ["node_modules", ".git", "dist", "build", ".next", "out"],
14-
};
15-
16-
let userConfig = {};
17-
const configFile = path.resolve(process.cwd(), "zero-ui.config.js");
18-
19-
if (fs.existsSync(configFile)) {
20-
try {
21-
const mod = await import(pathToFileURL(configFile).href);
22-
userConfig = mod.default ?? mod;
23-
} catch (e) {
24-
// @ts-expect-error
25-
console.warn("⚠️ Failed to load zero-ui.config.js:", e.message);
26-
}
5+
/** Configuration options for zero-ui.config.js or zero-ui.config.ts */
6+
export interface ZeroUIConfig {
7+
/** Package import name (default: "@react-zero-ui/icon-sprite") */
8+
IMPORT_NAME?: string;
9+
/** Path to the sprite file relative to public dir (default: "/icons.svg") */
10+
SPRITE_PATH?: string;
11+
/** Root directory to scan for icon imports (default: auto-detected from "src", "app", or "pages") */
12+
ROOT_DIR?: string;
13+
/** Directory for custom SVG icons inside OUTPUT_DIR (default: "zero-ui-icons") */
14+
CUSTOM_SVG_DIR?: string;
15+
/** Output directory for built assets (default: "public") */
16+
OUTPUT_DIR?: string;
17+
/** Icon names to ignore during scanning (default: ["CustomIcon"]) */
18+
IGNORE_ICONS?: string[];
19+
/** Directories to exclude from scanning (default: ["node_modules", ".git", "dist", "build", ".next", "out"]) */
20+
EXCLUDE_DIRS?: string[];
2721
}
2822

29-
const merged = { ...DEFAULT_CONFIG, ...userConfig };
30-
31-
export const IMPORT_NAME = merged.IMPORT_NAME;
32-
export const SPRITE_PATH = merged.SPRITE_PATH;
33-
export const ROOT_DIR = merged.ROOT_DIR;
34-
export const CUSTOM_SVG_DIR = merged.CUSTOM_SVG_DIR;
35-
export const OUTPUT_DIR = merged.OUTPUT_DIR;
36-
export const IGNORE_ICONS = merged.IGNORE_ICONS;
37-
export const EXCLUDE_DIRS = merged.EXCLUDE_DIRS;
23+
export const IMPORT_NAME = "@react-zero-ui/icon-sprite";
24+
export const SPRITE_PATH = "/icons.svg";
25+
export const ROOT_DIR = "src";
26+
export const CUSTOM_SVG_DIR = "zero-ui-icons";
27+
export const OUTPUT_DIR = "public";
28+
export const IGNORE_ICONS = ["CustomIcon"];
29+
export const EXCLUDE_DIRS = ["node_modules", ".git", "dist", "build", ".next", "out"];
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SPRITE_PATH } from "../config.js";
2+
import { warnMissingIconSize } from "../utils.js";
3+
import { ActivitySquare as DevIcon } from "lucide-react"
4+
import { renderUse,type IconProps,} from "../_shared.js";
5+
6+
7+
8+
export function ActivitySquare({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("ActivitySquare", size, width, height);
10+
if (process.env.NODE_ENV !== "production" && DevIcon) {
11+
return (
12+
<DevIcon
13+
{...(props as any)}
14+
{...(size != null ? { size } : {})}
15+
{...(width != null ? { width } : {})}
16+
{...(height != null ? { height } : {})}
17+
/>
18+
);
19+
}
20+
return renderUse("activity-square", width, height, size, SPRITE_PATH, props)
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SPRITE_PATH } from "../config.js";
2+
import { warnMissingIconSize } from "../utils.js";
3+
import { AlarmCheck as DevIcon } from "lucide-react"
4+
import { renderUse,type IconProps,} from "../_shared.js";
5+
6+
7+
8+
export function AlarmCheck({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("AlarmCheck", size, width, height);
10+
if (process.env.NODE_ENV !== "production" && DevIcon) {
11+
return (
12+
<DevIcon
13+
{...(props as any)}
14+
{...(size != null ? { size } : {})}
15+
{...(width != null ? { width } : {})}
16+
{...(height != null ? { height } : {})}
17+
/>
18+
);
19+
}
20+
return renderUse("alarm-check", width, height, size, SPRITE_PATH, props)
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SPRITE_PATH } from "../config.js";
2+
import { warnMissingIconSize } from "../utils.js";
3+
import { AlarmMinus as DevIcon } from "lucide-react"
4+
import { renderUse,type IconProps,} from "../_shared.js";
5+
6+
7+
8+
export function AlarmMinus({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("AlarmMinus", size, width, height);
10+
if (process.env.NODE_ENV !== "production" && DevIcon) {
11+
return (
12+
<DevIcon
13+
{...(props as any)}
14+
{...(size != null ? { size } : {})}
15+
{...(width != null ? { width } : {})}
16+
{...(height != null ? { height } : {})}
17+
/>
18+
);
19+
}
20+
return renderUse("alarm-minus", width, height, size, SPRITE_PATH, props)
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SPRITE_PATH } from "../config.js";
2+
import { warnMissingIconSize } from "../utils.js";
3+
import { AlarmPlus as DevIcon } from "lucide-react"
4+
import { renderUse,type IconProps,} from "../_shared.js";
5+
6+
7+
8+
export function AlarmPlus({ size, width, height, ...props }: IconProps) {
9+
warnMissingIconSize("AlarmPlus", size, width, height);
10+
if (process.env.NODE_ENV !== "production" && DevIcon) {
11+
return (
12+
<DevIcon
13+
{...(props as any)}
14+
{...(size != null ? { size } : {})}
15+
{...(width != null ? { width } : {})}
16+
{...(height != null ? { height } : {})}
17+
/>
18+
);
19+
}
20+
return renderUse("alarm-plus", width, height, size, SPRITE_PATH, props)
21+
}

0 commit comments

Comments
 (0)