@number10/jsx-icon-generator 5.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,74 @@
1
+ # @number10/jsx-icon-generator
2
+
3
+ CLI tooling and Vite plugin for generating type-safe icon types and dynamic loaders from icon libraries.
4
+
5
+ ## Features
6
+
7
+ - **Type generation** — Generate TypeScript union types from SVG icons in packages or directories
8
+ - **Loader generation** — Scan your codebase for icon usage and generate dynamic import loaders
9
+ - **Vite plugin** — Automatic icon generation during development and build
10
+ - **Multi-source** — Combine icons from multiple packages and directories
11
+ - **Watch mode** — Regenerate on file changes during development
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pnpm add -D @number10/jsx-icon-generator
17
+ ```
18
+
19
+ ## CLI Usage
20
+
21
+ ```bash
22
+ # Generate icon types
23
+ generate-icon-types -p bootstrap-icons -o ./src/icon-types.generated.ts
24
+
25
+ # Generate icon loaders
26
+ generate-icon-loaders -s ./src -o ./src/icon-loaders.generated.ts -p bootstrap-icons
27
+
28
+ # Unified generator with config file
29
+ generate-icons --config ./icon-generator.config.ts
30
+ ```
31
+
32
+ ## Vite Plugin
33
+
34
+ ```typescript
35
+ import { defineConfig } from 'vite'
36
+ import { iconGeneratorPlugin } from '@number10/jsx-icon-generator/vite-plugin-icons'
37
+
38
+ export default defineConfig({
39
+ plugins: [
40
+ iconGeneratorPlugin({
41
+ configPath: './icon-generator.config.ts',
42
+ }),
43
+ ],
44
+ })
45
+ ```
46
+
47
+ ## Configuration
48
+
49
+ ```typescript
50
+ import { defineIconConfig } from '@number10/jsx-icon-generator/icon-generator-config'
51
+
52
+ export default defineIconConfig({
53
+ source: {
54
+ package: 'bootstrap-icons',
55
+ iconsPath: 'icons',
56
+ },
57
+ types: {
58
+ enabled: true,
59
+ output: './src/icon-types.generated.ts',
60
+ typeName: 'IconType',
61
+ },
62
+ loaders: {
63
+ enabled: true,
64
+ output: './src/icon-loaders.generated.ts',
65
+ scanDir: './src',
66
+ componentNames: ['Icon'],
67
+ validate: true,
68
+ },
69
+ })
70
+ ```
71
+
72
+ ## License
73
+
74
+ GPL-3.0-only
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, readdir, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { parseArgs } from "node:util";
5
+ //#region src/generate-icon-loaders.ts
6
+ /**
7
+ * Auto-generates icon loader registry from actual icon usage in the codebase
8
+ * Scans TSX/TS files for Icon component usage and generates loaders
9
+ *
10
+ * Usage:
11
+ * generate-icon-loaders --source ./src --output ./src/components/icon-loaders.generated.ts --package bootstrap-icons
12
+ */
13
+ /**
14
+ * Recursively find all TypeScript files
15
+ */
16
+ async function findTsFiles(dir) {
17
+ const files = [];
18
+ const entries = await readdir(dir, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ const fullPath = join(dir, entry.name);
21
+ if (entry.isDirectory()) {
22
+ if (entry.name === "node_modules" || entry.name === "dist") continue;
23
+ files.push(...await findTsFiles(fullPath));
24
+ } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) files.push(fullPath);
25
+ }
26
+ return files;
27
+ }
28
+ /**
29
+ * Extract icon names from file content
30
+ */
31
+ function escapeRegExp(value) {
32
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33
+ }
34
+ function addStringLiteralIcons(expression, iconNames) {
35
+ const stringLiteralPattern = /(["'])([^"'\r\n]+)\1/g;
36
+ let match;
37
+ while ((match = stringLiteralPattern.exec(expression)) !== null) {
38
+ const iconName = match[2];
39
+ if (iconName) iconNames.add(iconName);
40
+ }
41
+ }
42
+ function addIconTypeDeclarationIcons(content, iconNames) {
43
+ const iconTypeLiteralDeclarationPattern = /\b(?:const|let|var)\s+\w+\s*:\s*[^=;\r\n]*\bIconType\b[^=;\r\n]*=\s*(["'])([^"']+)\1/g;
44
+ let match;
45
+ while ((match = iconTypeLiteralDeclarationPattern.exec(content)) !== null) {
46
+ const iconName = match[2];
47
+ if (iconName) iconNames.add(iconName);
48
+ }
49
+ }
50
+ function extractIconNames(content, componentNames) {
51
+ const iconNames = /* @__PURE__ */ new Set();
52
+ let match;
53
+ for (const componentName of componentNames) {
54
+ const escapedComponentName = escapeRegExp(componentName);
55
+ const typePattern = new RegExp(`<${escapedComponentName}\\s+[^>]*\\btype=(?:["']([^"']+)["']|\\{['"]([^"']+)["']\\})`, "g");
56
+ while ((match = typePattern.exec(content)) !== null) {
57
+ const iconName = match[1] || match[2];
58
+ if (iconName) iconNames.add(iconName);
59
+ }
60
+ const typeExpressionPattern = new RegExp(`<${escapedComponentName}\\s+[^>]*\\btype=\\{([^}]*)\\}`, "g");
61
+ while ((match = typeExpressionPattern.exec(content)) !== null) {
62
+ const expression = match[1];
63
+ if (expression) addStringLiteralIcons(expression, iconNames);
64
+ }
65
+ }
66
+ addIconTypeDeclarationIcons(content, iconNames);
67
+ const themedPattern = /themed\.(\w*[Ii]con)\s*\?\?\s*["']([^"']+)["']/g;
68
+ while ((match = themedPattern.exec(content)) !== null) if (match[2]) iconNames.add(match[2]);
69
+ const iconPropPattern = /(?:icon|iconType):\s*["']([^"']+)["']/g;
70
+ while ((match = iconPropPattern.exec(content)) !== null) if (match[1]) iconNames.add(match[1]);
71
+ return iconNames;
72
+ }
73
+ /**
74
+ * Load available icon names from generated types file
75
+ */
76
+ async function loadAvailableIcons(typesFile) {
77
+ try {
78
+ const match = (await readFile(typesFile, "utf-8")).match(/(?:export\s+)?type\s+\w+\s*=\s*([\s\S]+?)(?=\n\nexport|\n\/\/|$)/s);
79
+ if (!match?.[1]) return null;
80
+ const typeContent = match[1];
81
+ const icons = /* @__PURE__ */ new Set();
82
+ const iconPattern = /'([^']+)'/g;
83
+ let iconMatch;
84
+ while ((iconMatch = iconPattern.exec(typeContent)) !== null) {
85
+ const iconName = iconMatch[1];
86
+ if (iconName) icons.add(iconName);
87
+ }
88
+ return icons;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Main generator function
95
+ */
96
+ async function generateIconLoaders(options) {
97
+ try {
98
+ const cwd = process.cwd();
99
+ const srcDir = resolve(cwd, options.source);
100
+ const componentNames = options.componentNames || [options.componentName || "Icon"];
101
+ console.log(`📁 Scanning directory: ${srcDir}`);
102
+ console.log(`🔍 Looking for components: ${componentNames.join(", ")}`);
103
+ let availableIcons = null;
104
+ if (options.typesFile) {
105
+ availableIcons = await loadAvailableIcons(resolve(cwd, options.typesFile));
106
+ if (availableIcons) console.log(`✓ Loaded ${availableIcons.size} available icons from ${options.typesFile}`);
107
+ else console.warn(`⚠️ Could not parse types file: ${options.typesFile}`);
108
+ }
109
+ const files = await findTsFiles(srcDir);
110
+ console.log(`📄 Found ${files.length} TypeScript files`);
111
+ const allIconNames = /* @__PURE__ */ new Set();
112
+ for (const file of files) extractIconNames(await readFile(file, "utf-8"), componentNames).forEach((icon) => allIconNames.add(icon));
113
+ let iconArray = Array.from(allIconNames).sort();
114
+ if (availableIcons) {
115
+ const invalidIcons = iconArray.filter((icon) => !availableIcons.has(icon));
116
+ if (invalidIcons.length > 0) {
117
+ console.warn(`⚠️ Found ${invalidIcons.length} icons not in types file:`);
118
+ console.warn(` ${invalidIcons.slice(0, 5).join(", ")}${invalidIcons.length > 5 ? "..." : ""}`);
119
+ }
120
+ iconArray = iconArray.filter((icon) => availableIcons.has(icon));
121
+ }
122
+ console.log(`✓ Found ${iconArray.length} unique icons`);
123
+ if (iconArray.length > 0) console.log(` Icons: ${iconArray.slice(0, 10).join(", ")}${iconArray.length > 10 ? "..." : ""}`);
124
+ const iconsPath = options.iconsPath || "icons";
125
+ const loaders = iconArray.map((icon) => ` '${icon}': () => import('${options.package}/${iconsPath}/${icon}.svg'),`).join("\n");
126
+ const output = `/**
127
+ * Auto-generated icon loader registry
128
+ * Package: ${options.package}
129
+ * Icons found: ${iconArray.length}
130
+ *
131
+ * @generated by @number10/jsx-icon-generator/generate-icon-loaders
132
+ * @cspell: disable
133
+ */
134
+
135
+ /**
136
+ * Icon loaders - only icons used in the codebase are registered
137
+ * Bundler will create separate chunks for each icon
138
+ */
139
+ export const iconLoaders: Record<string, () => Promise<{ default: string }>> = {
140
+ ${loaders || " // No icons found"}
141
+ }
142
+ `;
143
+ const outputPath = resolve(cwd, options.output);
144
+ await writeFile(outputPath, output, "utf-8");
145
+ console.log(`✓ Generated: ${outputPath}`);
146
+ console.log(`✓ Registered ${iconArray.length} icon loaders`);
147
+ } catch (error) {
148
+ console.error("❌ Error generating icon loaders:", error);
149
+ process.exit(1);
150
+ }
151
+ }
152
+ var { values } = parseArgs({ options: {
153
+ source: {
154
+ type: "string",
155
+ short: "s"
156
+ },
157
+ output: {
158
+ type: "string",
159
+ short: "o"
160
+ },
161
+ package: {
162
+ type: "string",
163
+ short: "p"
164
+ },
165
+ iconsPath: {
166
+ type: "string",
167
+ short: "i"
168
+ },
169
+ componentName: {
170
+ type: "string",
171
+ short: "c"
172
+ },
173
+ componentNames: {
174
+ type: "string",
175
+ multiple: true
176
+ },
177
+ typesFile: {
178
+ type: "string",
179
+ short: "t"
180
+ },
181
+ help: {
182
+ type: "boolean",
183
+ short: "h"
184
+ }
185
+ } });
186
+ if (values.help || !values.source || !values.output || !values.package) {
187
+ console.log(`
188
+ Usage: generate-icon-loaders [options]
189
+
190
+ Options:
191
+ -s, --source <path> Source directory to scan (e.g., ./src)
192
+ -o, --output <path> Output file path (e.g., ./src/components/icon-loaders.generated.ts)
193
+ -p, --package <name> Icon package name (e.g., bootstrap-icons)
194
+ -i, --iconsPath <path> Relative path to icons in package (default: icons)
195
+ -c, --componentName <name> Icon component name to search for (default: Icon)
196
+ --componentNames <names...> Multiple component names (e.g., Icon BootstrapIcon)
197
+ -t, --typesFile <path> Types file for validation (e.g., ./src/icon-types.generated.ts)
198
+ -h, --help Show this help message
199
+
200
+ Examples:
201
+ # Single component name
202
+ generate-icon-loaders -s ./src -o ./src/icon-loaders.generated.ts -p bootstrap-icons
203
+
204
+ # Multiple component names
205
+ generate-icon-loaders -s ./src -o ./src/icon-loaders.generated.ts -p bootstrap-icons \\
206
+ --componentNames Icon BootstrapIcon CustomIcon
207
+
208
+ # With validation
209
+ generate-icon-loaders -s ./src -o ./src/icon-loaders.generated.ts -p bootstrap-icons \\
210
+ -t ./src/icon-types.generated.ts
211
+ `);
212
+ process.exit(values.help ? 0 : 1);
213
+ }
214
+ var options = {
215
+ source: values.source,
216
+ output: values.output,
217
+ package: values.package
218
+ };
219
+ if (values.iconsPath) options.iconsPath = values.iconsPath;
220
+ if (values.componentName) options.componentName = values.componentName;
221
+ if (values.componentNames && values.componentNames.length > 0) options.componentNames = values.componentNames;
222
+ if (values.typesFile) options.typesFile = values.typesFile;
223
+ generateIconLoaders(options);
224
+ //#endregion
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ import { readdir, writeFile } from "node:fs/promises";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { parseArgs } from "node:util";
5
+ //#region src/generate-icon-types.ts
6
+ /**
7
+ * Generator for icon type definitions from any icon library
8
+ * Scans an icon package and generates TypeScript union type
9
+ *
10
+ * Usage:
11
+ * generate-icon-types --package bootstrap-icons --output ./src/types/icons.ts
12
+ */
13
+ /**
14
+ * Find icon package in node_modules
15
+ */
16
+ async function findIconPackage(packageName, cwd) {
17
+ const fs = await import("node:fs");
18
+ const pnpmBase = join(cwd, "node_modules/.pnpm");
19
+ if (fs.existsSync(pnpmBase)) {
20
+ const entries = await readdir(pnpmBase);
21
+ for (const entry of entries) if (entry.startsWith(`${packageName}@`)) {
22
+ const packagePath = join(pnpmBase, entry, "node_modules", packageName);
23
+ if (fs.existsSync(packagePath)) return packagePath;
24
+ }
25
+ }
26
+ const standardPath = join(cwd, "node_modules", packageName);
27
+ if (fs.existsSync(standardPath)) return standardPath;
28
+ throw new Error(`Could not find package: ${packageName}`);
29
+ }
30
+ /**
31
+ * Main generator function
32
+ */
33
+ async function generateIconTypes(options) {
34
+ try {
35
+ const cwd = process.cwd();
36
+ let iconsDir;
37
+ let sourceName;
38
+ if (options.directory) {
39
+ iconsDir = resolve(cwd, options.directory);
40
+ sourceName = options.directory;
41
+ console.log(`📁 Scanning directory: ${options.directory}`);
42
+ } else if (options.package) {
43
+ iconsDir = join(await findIconPackage(options.package, cwd), options.iconsPath || "icons");
44
+ sourceName = options.package;
45
+ console.log(`📦 Scanning package: ${options.package}`);
46
+ } else throw new Error("Either --package or --directory must be specified");
47
+ console.log(`📁 Icons directory: ${iconsDir}`);
48
+ const iconNames = (await readdir(iconsDir)).filter((file) => file.endsWith(".svg")).map((file) => basename(file, ".svg")).sort();
49
+ console.log(`✓ Found ${iconNames.length} icons`);
50
+ const typeDefinition = iconNames.map((name) => ` | '${name}'`).join("\n");
51
+ const typeName = options.typeName || "IconType";
52
+ const output = `/**
53
+ * Auto-generated icon type definitions
54
+ * Source: ${sourceName}
55
+ * Total icons: ${iconNames.length}
56
+ *
57
+ * @generated by @number10/jsx-icon-generator/generate-icon-types
58
+ * @cspell: disable
59
+ */
60
+
61
+ /**
62
+ * All available icon names from ${sourceName}
63
+ * Type-only definition - no runtime imports, zero bundle impact
64
+ */
65
+ export type ${typeName} =
66
+ ${typeDefinition}
67
+ `;
68
+ const outputPath = resolve(cwd, options.output);
69
+ await writeFile(outputPath, output, "utf-8");
70
+ console.log(`✓ Generated: ${outputPath}`);
71
+ console.log(`✓ Type name: ${typeName}`);
72
+ console.log(`✓ Total icons: ${iconNames.length}`);
73
+ if (iconNames.length > 0) {
74
+ console.log(` First: ${iconNames[0]}`);
75
+ console.log(` Last: ${iconNames[iconNames.length - 1]}`);
76
+ }
77
+ } catch (error) {
78
+ console.error("❌ Error generating icon types:", error);
79
+ process.exit(1);
80
+ }
81
+ }
82
+ var { values } = parseArgs({ options: {
83
+ package: {
84
+ type: "string",
85
+ short: "p"
86
+ },
87
+ directory: {
88
+ type: "string",
89
+ short: "d"
90
+ },
91
+ output: {
92
+ type: "string",
93
+ short: "o"
94
+ },
95
+ typeName: {
96
+ type: "string",
97
+ short: "t"
98
+ },
99
+ iconsPath: {
100
+ type: "string",
101
+ short: "i"
102
+ },
103
+ help: {
104
+ type: "boolean",
105
+ short: "h"
106
+ }
107
+ } });
108
+ if (values.help || !values.package && !values.directory || !values.output) {
109
+ console.log(`
110
+ Usage: generate-icon-types [options]
111
+
112
+ Options:
113
+ -p, --package <name> Icon package name (e.g., bootstrap-icons)
114
+ -d, --directory <path> Local directory with SVG files (alternative to --package)
115
+ -o, --output <path> Output file path (e.g., ./src/types/icons.ts)
116
+ -t, --typeName <name> TypeScript type name (default: IconType)
117
+ -i, --iconsPath <path> Relative path to icons in package (default: icons)
118
+ -h, --help Show this help message
119
+
120
+ Examples:
121
+ # From npm package
122
+ generate-icon-types -p bootstrap-icons -o ./src/icon-types.generated.ts
123
+
124
+ # From local directory
125
+ generate-icon-types -d ./public/icons -o ./src/icon-types.generated.ts
126
+ `);
127
+ process.exit(values.help ? 0 : 1);
128
+ }
129
+ var options = { output: values.output };
130
+ if (values.package) options.package = values.package;
131
+ if (values.directory) options.directory = values.directory;
132
+ if (values.typeName) options.typeName = values.typeName;
133
+ if (values.iconsPath) options.iconsPath = values.iconsPath;
134
+ generateIconTypes(options);
135
+ //#endregion
@@ -0,0 +1,36 @@
1
+ import type { IconGeneratorConfig } from './icon-generator-config';
2
+ /**
3
+ * Generate icon types
4
+ * Exported for use by Vite plugin
5
+ */
6
+ export declare function generateTypes(config: IconGeneratorConfig, cwd: string): Promise<{
7
+ iconNames: string[];
8
+ sourceIconSets: Map<number, Set<string>>;
9
+ } | null>;
10
+ /**
11
+ * Scan icon sources without generating the types file
12
+ * Used by Vite plugin to get sourceIconSets for loader generation without full type regeneration
13
+ * @param config - Icon generator configuration
14
+ * @param cwd - Current working directory
15
+ * @returns Icon names and source mapping
16
+ */
17
+ export declare function scanIconSources(config: IconGeneratorConfig, cwd: string): Promise<{
18
+ iconNames: string[];
19
+ sourceIconSets: Map<number, Set<string>>;
20
+ }>;
21
+ /**
22
+ * Generate icon loaders
23
+ * Exported for use by Vite plugin
24
+ */
25
+ export declare function generateLoaders(config: IconGeneratorConfig, cwd: string, typesResult?: {
26
+ iconNames: string[];
27
+ sourceIconSets: Map<number, Set<string>>;
28
+ }): Promise<void>;
29
+ /**
30
+ * Load config file
31
+ */
32
+ /**
33
+ * Load icon generator config file
34
+ * Exported for use by Vite plugin and other tools
35
+ */
36
+ export declare function loadConfig(configPath: string, cwd?: string): Promise<IconGeneratorConfig>;