diff --git a/icon-sprite/README.md b/icon-sprite/README.md index 332573b..940c21d 100644 --- a/icon-sprite/README.md +++ b/icon-sprite/README.md @@ -1,19 +1,15 @@
- # @react-zero-ui/icon-sprite +# @react-zero-ui/icon-sprite - [![MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/react-zero-ui/icon-sprite/blob/main/LICENSE) [![npm](https://img.shields.io/npm/v/@react-zero-ui/icon-sprite.svg)](https://www.npmjs.com/package/@react-zero-ui/icon-sprite) +[![MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/react-zero-ui/icon-sprite/blob/main/LICENSE) [![npm](https://img.shields.io/npm/v/@react-zero-ui/icon-sprite.svg)](https://www.npmjs.com/package/@react-zero-ui/icon-sprite) - -
-> ![Note](https://img.shields.io/badge/Note-blue) -> **Generates one SVG sprite containing only the icons you used** - Lucide + custom SVGs. +> ![Note](https://img.shields.io/badge/Note-blue) > **Generates one SVG sprite containing only the icons you used** - Lucide + custom SVGs. > DX with real `` in dev ➑️ zero-runtime `` in prod. > Part of the [React Zero-UI](https://github.com/react-zero-ui) ecosystem. - --- @@ -34,26 +30,26 @@ Only icons actually used in your app are included. ## πŸ™ Custom Icon Support + Drop SVGs into **`/public/zero-ui-icons/`**, then use `` with the filename (no `.svg`). ->![Tip](https://img.shields.io/badge/Tip-green) ->```txt ->πŸ“/public +> ![Tip](https://img.shields.io/badge/Tip-green) +> +> ```txt +> πŸ“/public > β””β”€β”€πŸ“/zero-ui-icons/ > └──dog.svg > ``` +> > ```tsx ->import { CustomIcon } from "@react-zero-ui/icon-sprite"; ->//❗The name MUST match the name of the file name (no .svg extension). -> ->``` - +> import { CustomIcon } from '@react-zero-ui/icon-sprite'; +> //❗The name MUST match the name of the file name (no .svg extension). +> ; +> ``` ->![Info](https://img.shields.io/badge/Info-blue) +> ![Info](https://img.shields.io/badge/Info-blue) > In dev you may see a brief FOUC using custom icons; this is removed in production. - - --- ## πŸ“¦ Installation @@ -65,15 +61,18 @@ npm install @react-zero-ui/icon-sprite --- ## ❗ Build Command + > ![Caution](https://img.shields.io/badge/Caution-red) > Run this before your app build so the sprite exists. ->```bash ->npx zero-icons ->``` +> +> ```bash +> npx zero-icons +> ``` This command builds the icons sprite for production. Or add this to your `package.json` scripts: + ```json { "scripts": { @@ -82,14 +81,14 @@ Or add this to your `package.json` scripts: } } ``` + That's it! --- ## πŸ”¨ Usage -> ![Warning](https://img.shields.io/badge/Warning-orange) -> **Pass `size`, or both `width` and `height`, to ensure identical dev/prod rendering.** +> ![Warning](https://img.shields.io/badge/Warning-orange) > **Pass `size`, or both `width` and `height`, to ensure identical dev/prod rendering.** > Dev defaults (Lucide 24Γ—24) differ from sprite viewBoxes in production. Missing these props will **very likely** change the visual size in prod. ### For Lucide Icons: @@ -104,14 +103,13 @@ import { ArrowRight, Mail } from "@react-zero-ui/icon-sprite"; ### Custom Icons: Drop SVGs into **`/public/zero-ui-icons/`**, then use `` with the filename (no `.svg`). + ```tsx -import { CustomIcon } from "@react-zero-ui/icon-sprite"; +import { CustomIcon } from '@react-zero-ui/icon-sprite'; //❗The name MUST match the name of the file name (without .svg). - +; ``` - - --- ## πŸ§ͺ How It Works (Under the Hood) @@ -121,10 +119,10 @@ import { CustomIcon } from "@react-zero-ui/icon-sprite"; In dev, each icon wrapper looks like this: ```tsx -import { ArrowRight as DevIcon } from "lucide-react"; +import { ArrowRight as DevIcon } from 'lucide-react'; -export const ArrowRight = (props) => - process.env.NODE_ENV === "development" ? ( +export const ArrowRight = props => + process.env.NODE_ENV === 'development' ? ( ) : ( @@ -135,10 +133,10 @@ export const ArrowRight = (props) => This ensures: -* Dev uses Lucide's real React components (`lucide-react`) -* Full props support (e.g. `strokeWidth`, `className`) -* No caching issues from SVG sprites -* No FOUC (Flash of Unstyled Content) +- Dev uses Lucide's real React components (`lucide-react`) +- Full props support (e.g. `strokeWidth`, `className`) +- No caching issues from SVG sprites +- No FOUC (Flash of Unstyled Content) ### βš™οΈ Production Mode: Minimal Runtime, Maximum Speed @@ -150,6 +148,41 @@ At build time: --- +## βš™οΈ Configuration + +You can customize the scanner behavior by creating a `zero-ui.config.js` file in your project root: + +```js +// zero-ui.config.js +export default { + // Package name to scan for (default: "@react-zero-ui/icon-sprite") + IMPORT_NAME: '@react-zero-ui/icon-sprite', + + // Path where the sprite will be served (default: "/icons.svg") + SPRITE_PATH: '/icons.svg', + + // Directory to scan for icon usage (default: "src") + ROOT_DIR: 'src', + + // Directory containing custom SVG files (default: "zero-ui-icons") + CUSTOM_SVG_DIR: 'zero-ui-icons', + + // Output directory for the sprite (default: "public") + OUTPUT_DIR: 'public', + + // Icon names to ignore during scanning (default: ["CustomIcon"]) + IGNORE_ICONS: ['CustomIcon'], + + // Directories to exclude from scanning (default: ["node_modules", ".git", "dist", "build", ".next", "out"]) + EXCLUDE_DIRS: ['node_modules', '.git', 'dist', 'build', '.next', 'out'], +}; +``` + +> ![Note](https://img.shields.io/badge/Note-blue) +> The scanner now defaults to scanning only the `src` directory and automatically excludes `node_modules` and other common build directories. This prevents build failures from dependencies with unsupported syntax (e.g., TypeScript decorators). + +--- + ## ⚑️ Tooling To generate everything: @@ -160,22 +193,19 @@ npx zero-icons This runs the full pipeline: -| Script | Purpose | -| --- | --- | -| `scan-icons.js` | Parse your codebase for used icons (`Icon` usage or named imports) | -| `used-icons.js` | Collects a list of unique icon names | +| Script | Purpose | +| ----------------- | ------------------------------------------------------------------------------------------------------------ | +| `scan-icons.js` | Parse your codebase for used icons (`Icon` usage or named imports) | +| `used-icons.js` | Collects a list of unique icon names | | `build-sprite.js` | Uses [`svgstore`](https://github.com/DIYgod/svgstore) to generate `icons.svg` from used Lucide + custom SVGs | - ---- +--- ## ✨ Why This Beats Icon Libraries Everywhere -* **DX-first in dev**: No flicker. No sprite caching. Live updates. -* **Zero-runtime in production**: Sprites are native, fast, lightweight & highly Cached. -* **Only ships the icons you actually use** - smallest possible sprite. -* **Custom icon support**: Drop SVGs into `/public/zero-ui-icons/` and use `` - +- **DX-first in dev**: No flicker. No sprite caching. Live updates. +- **Zero-runtime in production**: Sprites are native, fast, lightweight & highly Cached. +- **Only ships the icons you actually use** - smallest possible sprite. +- **Custom icon support**: Drop SVGs into `/public/zero-ui-icons/` and use `` Made with ❀️ for the React community by [@austin1serb](https://github.com/austin1serb) - diff --git a/icon-sprite/package.json b/icon-sprite/package.json index 9ab81d3..f78faf1 100644 --- a/icon-sprite/package.json +++ b/icon-sprite/package.json @@ -37,7 +37,7 @@ ], "scripts": { "build": "rm -rf dist && node scripts/gen-wrappers.js && tsc && node scripts/gen-dist.js", - "test": "node tests/test-mapping.test.js", + "test": "node tests/run-all-tests.js", "prepare": "npm run build && npm run test", "type-check": "tsc --noEmit | tee type-errors.log" }, diff --git a/icon-sprite/scripts/scan-icons.js b/icon-sprite/scripts/scan-icons.js index 1d1c731..21ac96c 100644 --- a/icon-sprite/scripts/scan-icons.js +++ b/icon-sprite/scripts/scan-icons.js @@ -5,7 +5,7 @@ import { fileURLToPath } from "url"; import * as babel from "@babel/core"; import traverseImport from "@babel/traverse"; import * as t from "@babel/types"; -import { IMPORT_NAME, ROOT_DIR, IGNORE_ICONS } from "../dist/config.js"; +import { IMPORT_NAME, ROOT_DIR, IGNORE_ICONS, EXCLUDE_DIRS } from "../dist/config.js"; // ESM __dirname shim const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -30,6 +30,10 @@ function collect(dir) { for (const file of fs.readdirSync(dir)) { const full = path.join(dir, file); if (fs.statSync(full).isDirectory()) { + // Skip excluded directories + if (EXCLUDE_DIRS.includes(file)) { + continue; + } collect(full); continue; } diff --git a/icon-sprite/src/config.ts b/icon-sprite/src/config.ts index fe2d7c5..7bc38ca 100644 --- a/icon-sprite/src/config.ts +++ b/icon-sprite/src/config.ts @@ -1,42 +1,37 @@ // src/config.ts -// import fs from "fs"; -// import path from "path"; -// import { pathToFileURL } from "url"; +import fs from "fs"; +import path from "path"; +import { pathToFileURL } from "url"; -// const DEFAULT_CONFIG = { -// IMPORT_NAME: "@react-zero-ui/icon-sprite", -// SPRITE_PATH: "/icons.svg", -// ROOT_DIR: "", -// CUSTOM_SVG_DIR: "zero-ui-icons", -// OUTPUT_DIR: "public", -// IGNORE_ICONS: ["CustomIcon"], -// }; +const DEFAULT_CONFIG = { + IMPORT_NAME: "@react-zero-ui/icon-sprite", + SPRITE_PATH: "/icons.svg", + ROOT_DIR: "src", + CUSTOM_SVG_DIR: "zero-ui-icons", + OUTPUT_DIR: "public", + IGNORE_ICONS: ["CustomIcon"], + EXCLUDE_DIRS: ["node_modules", ".git", "dist", "build", ".next", "out"], +}; -// let userConfig = {}; -// const configFile = path.resolve(process.cwd(), "zero-ui.config.js"); +let userConfig = {}; +const configFile = path.resolve(process.cwd(), "zero-ui.config.js"); -// if (fs.existsSync(configFile)) { -// try { -// const mod = await import(pathToFileURL(configFile).href); -// userConfig = mod.default ?? mod; -// } catch (e) { -// // @ts-expect-error -// console.warn("⚠️ Failed to load zero-ui.config.js:", e.message); -// } -// } +if (fs.existsSync(configFile)) { + try { + const mod = await import(pathToFileURL(configFile).href); + userConfig = mod.default ?? mod; + } catch (e) { + // @ts-expect-error + console.warn("⚠️ Failed to load zero-ui.config.js:", e.message); + } +} -// const merged = { ...DEFAULT_CONFIG, ...userConfig }; +const merged = { ...DEFAULT_CONFIG, ...userConfig }; -// export const IMPORT_NAME = merged.IMPORT_NAME; -// export const SPRITE_PATH = merged.SPRITE_PATH; -// export const ROOT_DIR = merged.ROOT_DIR; -// export const CUSTOM_SVG_DIR = merged.CUSTOM_SVG_DIR; -// export const OUTPUT_DIR = merged.OUTPUT_DIR; -// export const IGNORE_ICONS = merged.IGNORE_ICONS; - -export const IMPORT_NAME = "@react-zero-ui/icon-sprite"; -export const SPRITE_PATH = "/icons.svg"; -export const ROOT_DIR = ""; -export const CUSTOM_SVG_DIR = "zero-ui-icons"; -export const OUTPUT_DIR = "public"; -export const IGNORE_ICONS = ["CustomIcon"]; +export const IMPORT_NAME = merged.IMPORT_NAME; +export const SPRITE_PATH = merged.SPRITE_PATH; +export const ROOT_DIR = merged.ROOT_DIR; +export const CUSTOM_SVG_DIR = merged.CUSTOM_SVG_DIR; +export const OUTPUT_DIR = merged.OUTPUT_DIR; +export const IGNORE_ICONS = merged.IGNORE_ICONS; +export const EXCLUDE_DIRS = merged.EXCLUDE_DIRS; diff --git a/icon-sprite/tests/run-all-tests.js b/icon-sprite/tests/run-all-tests.js new file mode 100644 index 0000000..67aacb6 --- /dev/null +++ b/icon-sprite/tests/run-all-tests.js @@ -0,0 +1,125 @@ +#!/usr/bin/env node +import { execSync } from "child_process"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Test suite configuration +const testSuites = [ + { + name: "Icon Mapping Tests", + file: "test-mapping.test.js", + description: "Verify all React icon components map to lucide-static SVGs", + }, + { + name: "Config Loading Tests", + file: "test-config.test.js", + description: "Verify config loading and merging with defaults", + }, + { + name: "Scanner Exclusion Tests", + file: "test-scanner-exclusion.test.js", + description: "Verify directory exclusion and ROOT_DIR functionality", + }, +]; + +// Color codes for terminal output +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + green: "\x1b[32m", + red: "\x1b[31m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + cyan: "\x1b[36m", +}; + +function printHeader() { + console.log("\n" + "=".repeat(60)); + console.log(colors.bright + colors.cyan + "πŸ§ͺ @react-zero-ui/icon-sprite Test Suite" + colors.reset); + console.log("=".repeat(60) + "\n"); +} + +function printTestSuiteHeader(suite, index, total) { + console.log(colors.bright + `\n[${ index + 1}/${total}] ${suite.name}` + colors.reset); + console.log(colors.blue + `πŸ“ ${suite.description}` + colors.reset); + console.log("-".repeat(60)); +} + +function runTest(testFile) { + const testPath = path.join(__dirname, testFile); + try { + const output = execSync(`node "${testPath}"`, { + cwd: __dirname, + encoding: "utf8", + stdio: "pipe", + }); + return { success: true, output }; + } catch (err) { + return { success: false, output: err.stdout || err.message }; + } +} + +async function main() { + printHeader(); + + const results = []; + let totalPassed = 0; + let totalFailed = 0; + + // Run each test suite + for (let i = 0; i < testSuites.length; i++) { + const suite = testSuites[i]; + printTestSuiteHeader(suite, i, testSuites.length); + + const result = runTest(suite.file); + results.push({ suite, result }); + + if (result.success) { + console.log(result.output); + console.log(colors.green + `βœ… ${suite.name} PASSED` + colors.reset); + totalPassed++; + } else { + console.log(result.output); + console.error(colors.red + `❌ ${suite.name} FAILED` + colors.reset); + totalFailed++; + } + } + + // Print summary + console.log("\n" + "=".repeat(60)); + console.log(colors.bright + "πŸ“Š Test Summary" + colors.reset); + console.log("=".repeat(60)); + + for (const { suite, result } of results) { + const status = result.success + ? colors.green + "βœ… PASS" + colors.reset + : colors.red + "❌ FAIL" + colors.reset; + console.log(`${status} - ${suite.name}`); + } + + console.log("-".repeat(60)); + console.log( + `Total: ${totalPassed + totalFailed} | ` + + colors.green + `Passed: ${totalPassed}` + colors.reset + " | " + + colors.red + `Failed: ${totalFailed}` + colors.reset + ); + console.log("=".repeat(60)); + + if (totalFailed === 0) { + console.log(colors.green + colors.bright + "\nπŸŽ‰ All tests passed!" + colors.reset); + console.log(colors.green + "✨ Your changes are ready to ship!\n" + colors.reset); + process.exit(0); + } else { + console.error(colors.red + colors.bright + "\n❌ Some tests failed!" + colors.reset); + console.error(colors.yellow + "Please fix the failing tests before committing.\n" + colors.reset); + process.exit(1); + } +} + +main().catch((err) => { + console.error(colors.red + "❌ Test runner error:" + colors.reset, err); + process.exit(1); +}); diff --git a/icon-sprite/tests/test-config.test.js b/icon-sprite/tests/test-config.test.js new file mode 100644 index 0000000..f0b4b8d --- /dev/null +++ b/icon-sprite/tests/test-config.test.js @@ -0,0 +1,280 @@ +#!/usr/bin/env node +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { execSync } from "child_process"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const TEST_PROJECT_ROOT = path.resolve(__dirname, "../test-project"); +const CONFIG_FILE = path.join(TEST_PROJECT_ROOT, "zero-ui.config.js"); +const PACKAGE_JSON = path.join(TEST_PROJECT_ROOT, "package.json"); + +// Cleanup and setup test project directory +function setupTestProject() { + if (fs.existsSync(TEST_PROJECT_ROOT)) { + fs.rmSync(TEST_PROJECT_ROOT, { recursive: true, force: true }); + } + fs.mkdirSync(TEST_PROJECT_ROOT, { recursive: true }); + + // Create package.json to make it a valid project root + fs.writeFileSync( + PACKAGE_JSON, + JSON.stringify({ name: "test-project", version: "1.0.0" }, null, 2), + "utf8" + ); +} + +function cleanup() { + if (fs.existsSync(TEST_PROJECT_ROOT)) { + fs.rmSync(TEST_PROJECT_ROOT, { recursive: true, force: true }); + } +} + +// Helper to run a test script in a child process +function runConfigTest() { + const testScript = ` +import { IMPORT_NAME, ROOT_DIR, SPRITE_PATH, CUSTOM_SVG_DIR, OUTPUT_DIR, IGNORE_ICONS, EXCLUDE_DIRS } from "../dist/config.js"; +console.log(JSON.stringify({ + IMPORT_NAME, + ROOT_DIR, + SPRITE_PATH, + CUSTOM_SVG_DIR, + OUTPUT_DIR, + IGNORE_ICONS, + EXCLUDE_DIRS +})); +`; + + const testScriptPath = path.join(TEST_PROJECT_ROOT, "test-runner.js"); + fs.writeFileSync(testScriptPath, testScript, "utf8"); + + const result = execSync(`node "${testScriptPath}"`, { + cwd: TEST_PROJECT_ROOT, + encoding: "utf8", + }); + + return JSON.parse(result.trim()); +} + +// Test 1: Default config values (no config file) +async function testDefaultConfig() { + console.log("\nπŸ§ͺ Test 1: Default config values (no config file)"); + setupTestProject(); + + try { + const config = runConfigTest(); + + const assertions = [ + { name: "IMPORT_NAME", expected: "@react-zero-ui/icon-sprite", actual: config.IMPORT_NAME }, + { name: "ROOT_DIR", expected: "src", actual: config.ROOT_DIR }, + { name: "SPRITE_PATH", expected: "/icons.svg", actual: config.SPRITE_PATH }, + { name: "CUSTOM_SVG_DIR", expected: "zero-ui-icons", actual: config.CUSTOM_SVG_DIR }, + { name: "OUTPUT_DIR", expected: "public", actual: config.OUTPUT_DIR }, + { name: "IGNORE_ICONS", expected: JSON.stringify(["CustomIcon"]), actual: JSON.stringify(config.IGNORE_ICONS) }, + { name: "EXCLUDE_DIRS", expected: JSON.stringify(["node_modules", ".git", "dist", "build", ".next", "out"]), actual: JSON.stringify(config.EXCLUDE_DIRS) }, + ]; + + let passed = true; + for (const assertion of assertions) { + if (assertion.expected === assertion.actual) { + console.log(` βœ… ${assertion.name}: ${assertion.actual}`); + } else { + console.error(` ❌ ${assertion.name}: expected ${assertion.expected}, got ${assertion.actual}`); + passed = false; + } + } + + return passed; + } catch (err) { + console.error(` ❌ Test failed: ${err.message}`); + return false; + } finally { + cleanup(); + } +} + +// Test 2: Custom config loading +async function testCustomConfig() { + console.log("\nπŸ§ͺ Test 2: Custom config loading from zero-ui.config.js"); + setupTestProject(); + + try { + // Create a custom config file + const customConfig = `export default { + IMPORT_NAME: "@my-custom/icons", + ROOT_DIR: "app", + SPRITE_PATH: "/custom-icons.svg", + CUSTOM_SVG_DIR: "my-icons", + OUTPUT_DIR: "static", + IGNORE_ICONS: ["MyCustomIcon", "AnotherIcon"], + EXCLUDE_DIRS: ["node_modules", "vendor", "tmp"], +};`; + fs.writeFileSync(CONFIG_FILE, customConfig, "utf8"); + + const config = runConfigTest(); + + const assertions = [ + { name: "IMPORT_NAME", expected: "@my-custom/icons", actual: config.IMPORT_NAME }, + { name: "ROOT_DIR", expected: "app", actual: config.ROOT_DIR }, + { name: "SPRITE_PATH", expected: "/custom-icons.svg", actual: config.SPRITE_PATH }, + { name: "CUSTOM_SVG_DIR", expected: "my-icons", actual: config.CUSTOM_SVG_DIR }, + { name: "OUTPUT_DIR", expected: "static", actual: config.OUTPUT_DIR }, + { name: "IGNORE_ICONS", expected: JSON.stringify(["MyCustomIcon", "AnotherIcon"]), actual: JSON.stringify(config.IGNORE_ICONS) }, + { name: "EXCLUDE_DIRS", expected: JSON.stringify(["node_modules", "vendor", "tmp"]), actual: JSON.stringify(config.EXCLUDE_DIRS) }, + ]; + + let passed = true; + for (const assertion of assertions) { + if (assertion.expected === assertion.actual) { + console.log(` βœ… ${assertion.name}: ${assertion.actual}`); + } else { + console.error(` ❌ ${assertion.name}: expected ${assertion.expected}, got ${assertion.actual}`); + passed = false; + } + } + + return passed; + } catch (err) { + console.error(` ❌ Test failed: ${err.message}`); + return false; + } finally { + cleanup(); + } +} + +// Test 3: Partial config override (merging with defaults) +async function testPartialConfig() { + console.log("\nπŸ§ͺ Test 3: Partial config override (merging with defaults)"); + setupTestProject(); + + try { + // Create a partial config file (only override ROOT_DIR and EXCLUDE_DIRS) + const partialConfig = `export default { + ROOT_DIR: "lib", + EXCLUDE_DIRS: ["node_modules", "coverage"], +};`; + fs.writeFileSync(CONFIG_FILE, partialConfig, "utf8"); + + const config = runConfigTest(); + + const assertions = [ + { name: "IMPORT_NAME (default)", expected: "@react-zero-ui/icon-sprite", actual: config.IMPORT_NAME }, + { name: "ROOT_DIR (override)", expected: "lib", actual: config.ROOT_DIR }, + { name: "SPRITE_PATH (default)", expected: "/icons.svg", actual: config.SPRITE_PATH }, + { name: "EXCLUDE_DIRS (override)", expected: JSON.stringify(["node_modules", "coverage"]), actual: JSON.stringify(config.EXCLUDE_DIRS) }, + ]; + + let passed = true; + for (const assertion of assertions) { + if (assertion.expected === assertion.actual) { + console.log(` βœ… ${assertion.name}: ${assertion.actual}`); + } else { + console.error(` ❌ ${assertion.name}: expected ${assertion.expected}, got ${assertion.actual}`); + passed = false; + } + } + + return passed; + } catch (err) { + console.error(` ❌ Test failed: ${err.message}`); + return false; + } finally { + cleanup(); + } +} + +// Test 4: Config file with syntax error (should warn and use defaults) +async function testInvalidConfig() { + console.log("\nπŸ§ͺ Test 4: Invalid config file (should warn and use defaults)"); + setupTestProject(); + + try { + // Create an invalid config file + const invalidConfig = `export default { + IMPORT_NAME: "unclosed string +};`; + fs.writeFileSync(CONFIG_FILE, invalidConfig, "utf8"); + + const testScript = ` +import { ROOT_DIR, EXCLUDE_DIRS } from "../dist/config.js"; +console.log(JSON.stringify({ ROOT_DIR, EXCLUDE_DIRS })); +`; + + const testScriptPath = path.join(TEST_PROJECT_ROOT, "test-runner.js"); + fs.writeFileSync(testScriptPath, testScript, "utf8"); + + // This should warn but still work with defaults + const result = execSync(`node "${testScriptPath}"`, { + cwd: TEST_PROJECT_ROOT, + encoding: "utf8", + stdio: ["pipe", "pipe", "pipe"] + }); + + const config = JSON.parse(result.trim()); + + // Should use defaults when config fails to load + const assertions = [ + { name: "ROOT_DIR (fallback to default)", expected: "src", actual: config.ROOT_DIR }, + { name: "EXCLUDE_DIRS (fallback to default)", expected: JSON.stringify(["node_modules", ".git", "dist", "build", ".next", "out"]), actual: JSON.stringify(config.EXCLUDE_DIRS) }, + ]; + + let passed = true; + for (const assertion of assertions) { + if (assertion.expected === assertion.actual) { + console.log(` βœ… ${assertion.name}`); + } else { + console.error(` ❌ ${assertion.name}: expected ${assertion.expected}, got ${assertion.actual}`); + passed = false; + } + } + + console.log(" ℹ️ Warning should have been displayed to stderr"); + return passed; + } catch (err) { + console.error(` ❌ Test failed: ${err.message}`); + return false; + } finally { + cleanup(); + } +} + +// Run all tests +async function runTests() { + console.log("πŸš€ Running config tests...\n"); + + const results = []; + + results.push({ name: "Default config", passed: await testDefaultConfig() }); + results.push({ name: "Custom config", passed: await testCustomConfig() }); + results.push({ name: "Partial config", passed: await testPartialConfig() }); + results.push({ name: "Invalid config", passed: await testInvalidConfig() }); + + console.log("\n" + "=".repeat(50)); + console.log("πŸ“Š Test Results:"); + console.log("=".repeat(50)); + + let allPassed = true; + for (const result of results) { + const status = result.passed ? "βœ… PASS" : "❌ FAIL"; + console.log(`${status} - ${result.name}`); + if (!result.passed) allPassed = false; + } + + console.log("=".repeat(50)); + + if (allPassed) { + console.log("\nβœ… All config tests passed!"); + process.exit(0); + } else { + console.error("\n❌ Some config tests failed!"); + process.exit(1); + } +} + +runTests().catch((err) => { + console.error("❌ Test runner error:", err); + cleanup(); + process.exit(1); +}); diff --git a/icon-sprite/tests/test-scanner-exclusion.test.js b/icon-sprite/tests/test-scanner-exclusion.test.js new file mode 100644 index 0000000..cf83428 --- /dev/null +++ b/icon-sprite/tests/test-scanner-exclusion.test.js @@ -0,0 +1,168 @@ +#!/usr/bin/env node +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Test 1: Verify EXCLUDE_DIRS is exported from config +async function testExcludeDirsExport() { + console.log("\nπŸ§ͺ Test 1: EXCLUDE_DIRS is exported from config"); + + try { + const { EXCLUDE_DIRS } = await import("../dist/config.js"); + + if (!Array.isArray(EXCLUDE_DIRS)) { + console.error(" ❌ EXCLUDE_DIRS is not an array"); + return false; + } + + if (!EXCLUDE_DIRS.includes("node_modules")) { + console.error(" ❌ EXCLUDE_DIRS doesn't include 'node_modules'"); + return false; + } + + console.log(` βœ… EXCLUDE_DIRS exported: ${JSON.stringify(EXCLUDE_DIRS)}`); + console.log(` βœ… Contains 'node_modules': ${EXCLUDE_DIRS.includes("node_modules")}`); + console.log(` βœ… Contains '.git': ${EXCLUDE_DIRS.includes(".git")}`); + console.log(` βœ… Contains 'dist': ${EXCLUDE_DIRS.includes("dist")}`); + + return true; + } catch (err) { + console.error(` ❌ Test failed: ${err.message}`); + return false; + } +} + +// Test 2: Verify scan-icons.js imports EXCLUDE_DIRS +async function testScannerImportsExcludeDirs() { + console.log("\nπŸ§ͺ Test 2: scan-icons.js imports EXCLUDE_DIRS"); + + try { + const scannerPath = path.resolve(__dirname, "../scripts/scan-icons.js"); + const scannerContent = fs.readFileSync(scannerPath, "utf8"); + + // Check if EXCLUDE_DIRS is imported + if (!scannerContent.includes("EXCLUDE_DIRS")) { + console.error(" ❌ scan-icons.js doesn't import EXCLUDE_DIRS"); + return false; + } + + // Check if the exclusion logic exists + if (!scannerContent.includes("EXCLUDE_DIRS.includes")) { + console.error(" ❌ scan-icons.js doesn't use EXCLUDE_DIRS.includes()"); + return false; + } + + console.log(" βœ… scan-icons.js imports EXCLUDE_DIRS"); + console.log(" βœ… scan-icons.js uses EXCLUDE_DIRS for exclusion logic"); + + return true; + } catch (err) { + console.error(` ❌ Test failed: ${err.message}`); + return false; + } +} + +// Test 3: Verify default ROOT_DIR is "src" +async function testDefaultRootDir() { + console.log("\nπŸ§ͺ Test 3: Default ROOT_DIR is 'src' (not empty string)"); + + try { + const { ROOT_DIR } = await import("../dist/config.js"); + + if (ROOT_DIR !== "src") { + console.error(` ❌ ROOT_DIR is "${ROOT_DIR}", expected "src"`); + return false; + } + + console.log(` βœ… ROOT_DIR: "${ROOT_DIR}"`); + console.log(" βœ… This prevents scanning from project root by default"); + + return true; + } catch (err) { + console.error(` ❌ Test failed: ${err.message}`); + return false; + } +} + +// Test 4: Verify config loading logic is uncommented +async function testConfigLoadingUncommented() { + console.log("\nπŸ§ͺ Test 4: Config loading logic is uncommented"); + + try { + const configPath = path.resolve(__dirname, "../src/config.ts"); + const configContent = fs.readFileSync(configPath, "utf8"); + + // Check that imports are not commented + if (configContent.includes("// import fs from") || configContent.includes("// import path from")) { + console.error(" ❌ Config imports are still commented"); + return false; + } + + // Check that DEFAULT_CONFIG is not commented + if (configContent.includes("// const DEFAULT_CONFIG")) { + console.error(" ❌ DEFAULT_CONFIG is still commented"); + return false; + } + + // Check that config file loading is not commented + if (configContent.includes("// if (fs.existsSync(configFile))")) { + console.error(" ❌ Config file loading is still commented"); + return false; + } + + console.log(" βœ… Config imports are uncommented"); + console.log(" βœ… Config loading logic is active"); + console.log(" βœ… zero-ui.config.js can be used"); + + return true; + } catch (err) { + console.error(` ❌ Test failed: ${err.message}`); + return false; + } +} + +// Run all tests +async function runTests() { + console.log("πŸš€ Running scanner exclusion tests...\n"); + + const results = []; + + results.push({ name: "EXCLUDE_DIRS export", passed: await testExcludeDirsExport() }); + results.push({ name: "Scanner imports EXCLUDE_DIRS", passed: await testScannerImportsExcludeDirs() }); + results.push({ name: "Default ROOT_DIR", passed: await testDefaultRootDir() }); + results.push({ name: "Config loading uncommented", passed: await testConfigLoadingUncommented() }); + + console.log("\n" + "=".repeat(50)); + console.log("πŸ“Š Test Results:"); + console.log("=".repeat(50)); + + let allPassed = true; + for (const result of results) { + const status = result.passed ? "βœ… PASS" : "❌ FAIL"; + console.log(`${status} - ${result.name}`); + if (!result.passed) allPassed = false; + } + + console.log("=".repeat(50)); + + if (allPassed) { + console.log("\nβœ… All scanner exclusion tests passed!"); + console.log("\nℹ️ These tests verify that:"); + console.log(" 1. EXCLUDE_DIRS is properly exported from config"); + console.log(" 2. The scanner uses EXCLUDE_DIRS to skip directories"); + console.log(" 3. ROOT_DIR defaults to 'src' to avoid scanning node_modules"); + console.log(" 4. Config loading from zero-ui.config.js is enabled"); + process.exit(0); + } else { + console.error("\n❌ Some scanner exclusion tests failed!"); + process.exit(1); + } +} + +runTests().catch((err) => { + console.error("❌ Test runner error:", err); + process.exit(1); +});