Skip to content

Commit 940ed33

Browse files
committed
feat(icon-generator): added icons/utils
1 parent 0670ec3 commit 940ed33

File tree

11 files changed

+1815
-12
lines changed

11 files changed

+1815
-12
lines changed

packages/react-core/.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22
/deprecated
33
/components
44
/layouts
5-
/helpers
5+
/helpers
6+
7+
.env
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"figmaBaseUrl": "https://www.figma.com/design/VMEX8Xg2nzhBX8rfBx53jp/branch/H3LonYnwH26v9zNEa2SXFk/PatternFly-6%3A-Components",
3+
"defaultNodeId": "1-196"
4+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import path from 'path';
2+
import { existsSync, readFileSync } from 'fs';
3+
import logger from './logger.mjs';
4+
import { findRepoRoot } from './iconUtils.mjs';
5+
import yargs from 'yargs';
6+
import { hideBin } from 'yargs/helpers';
7+
8+
// Parse command line arguments
9+
const argv = yargs(hideBin(process.argv))
10+
.option('verbose', {
11+
alias: 'v',
12+
type: 'boolean',
13+
description: 'Run with verbose logging'
14+
})
15+
.option('dry-run', {
16+
type: 'boolean',
17+
description: 'Show what would be done without making changes'
18+
})
19+
.option('input', {
20+
alias: 'i',
21+
type: 'string',
22+
description: 'Custom input path for icons data'
23+
})
24+
.option('output', {
25+
alias: 'o',
26+
type: 'string',
27+
description: 'Custom output directory for generated icons'
28+
})
29+
.option('base-url', {
30+
type: 'string',
31+
description: 'Override figma base URL'
32+
})
33+
.help()
34+
.alias('help', 'h')
35+
.parseSync();
36+
37+
// Set environment variables based on CLI args
38+
if (argv.verbose) {
39+
process.env.VERBOSE = 'true';
40+
}
41+
42+
if (argv.dryRun) {
43+
process.env.DRY_RUN = 'true';
44+
logger.info('Running in dry-run mode - no files will be modified');
45+
}
46+
47+
/**
48+
* Load configuration from config file with validation
49+
* @param {string} configPath - Path to the configuration file
50+
* @returns {Object} Configuration object
51+
*/
52+
function loadConfig(configPath) {
53+
const defaultConfig = {
54+
figmaBaseUrl: 'https://www.figma.com/design/VMEX8Xg2nzhBX8rfBx53jp/branch/H3LonYnwH26v9zNEa2SXFk/PatternFly-6%3A-Components',
55+
defaultNodeId: '1-196'
56+
};
57+
58+
try {
59+
if (existsSync(configPath)) {
60+
const configContent = readFileSync(configPath, 'utf8');
61+
const config = JSON.parse(configContent);
62+
63+
// Validate required fields
64+
if (!config.figmaBaseUrl) {
65+
logger.warn('Configuration missing figmaBaseUrl, using default', {
66+
source: 'ConfigLoader',
67+
context: { defaultValue: defaultConfig.figmaBaseUrl }
68+
});
69+
config.figmaBaseUrl = defaultConfig.figmaBaseUrl;
70+
}
71+
72+
if (!config.defaultNodeId) {
73+
logger.warn('Configuration missing defaultNodeId, using default', {
74+
source: 'ConfigLoader',
75+
context: { defaultValue: defaultConfig.defaultNodeId }
76+
});
77+
config.defaultNodeId = defaultConfig.defaultNodeId;
78+
}
79+
80+
logger.debug('Loaded configuration from file', {
81+
source: 'ConfigLoader',
82+
context: { path: configPath, config }
83+
});
84+
85+
return { ...defaultConfig, ...config };
86+
}
87+
88+
logger.warn(`Config file not found at ${configPath}. Using default configuration.`, {
89+
source: 'ConfigLoader'
90+
});
91+
return defaultConfig;
92+
} catch (error) {
93+
logger.error('Error reading configuration', error, {
94+
source: 'ConfigLoader',
95+
context: { path: configPath }
96+
});
97+
return defaultConfig;
98+
}
99+
}
100+
101+
/**
102+
* Get and validate icon generation configuration
103+
* @returns {Object} Comprehensive icon generation configuration
104+
*/
105+
export function getIconConfig() {
106+
const root = findRepoRoot();
107+
logger.debug(`Repository root: ${root}`, { source: 'ConfigLoader' });
108+
109+
// Load base configuration
110+
const configPath = path.resolve(root, 'codeConnect/config.json');
111+
const config = loadConfig(configPath);
112+
113+
// Override with CLI arguments if provided
114+
if (argv.baseUrl) {
115+
logger.info(`Overriding figmaBaseUrl with CLI value: ${argv.baseUrl}`, {
116+
source: 'ConfigLoader'
117+
});
118+
config.figmaBaseUrl = argv.baseUrl;
119+
}
120+
121+
// Build the full configuration object
122+
const iconConfig = {
123+
// Input paths (with CLI overrides if provided)
124+
iconsDataPath: argv.input || path.resolve(root, 'codeConnect/data/iconsData.json'),
125+
iconTemplatePath: path.resolve(root, 'codeConnect/templates/iconTemplate.txt'),
126+
127+
// Output directories (with CLI overrides if provided)
128+
iconsGeneratedDir: argv.output || path.resolve(root, 'icons/generated'),
129+
iconsFigmaDir: path.resolve(root, 'icons'),
130+
131+
// Figma configuration
132+
figmaBaseUrl: config.figmaBaseUrl,
133+
defaultNodeId: config.defaultNodeId,
134+
135+
// Output files
136+
figmaOutputFile: 'icons.figma.tsx',
137+
generatedIndexFile: 'index.ts',
138+
139+
// CLI arguments
140+
dryRun: Boolean(process.env.DRY_RUN),
141+
verbose: Boolean(process.env.VERBOSE),
142+
143+
// Logging utility
144+
logger
145+
};
146+
147+
// Log full configuration in verbose mode
148+
logger.debug('Final icon configuration', {
149+
source: 'ConfigLoader',
150+
context: iconConfig
151+
});
152+
153+
return iconConfig;
154+
}
155+
156+
export default getIconConfig;
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Shared utilities for icon generation and connection
3+
*/
4+
import path from 'path';
5+
import { fileURLToPath } from 'url';
6+
import fs from 'fs/promises';
7+
import { existsSync } from 'fs';
8+
9+
/**
10+
* Extract node ID from a Figma URL
11+
* @param {string} url - Figma URL
12+
* @returns {string|null} - Node ID or null if not found
13+
*/
14+
export function extractNodeId(url) {
15+
if (!url) {
16+
return null;
17+
}
18+
const nodeIdMatch = url.match(/node-id=([^&]+)/);
19+
return nodeIdMatch ? nodeIdMatch[1] : null;
20+
}
21+
22+
/**
23+
* Extract icon names from import statement
24+
* @param {string} importStatement - The full import statement
25+
* @returns {string[]} Array of unique icon names
26+
*/
27+
export function extractIconNames(importStatement, logger) {
28+
if (!importStatement) {
29+
logger?.warn('Import statement is undefined or empty');
30+
return [];
31+
}
32+
33+
try {
34+
// Remove comments and extra whitespace
35+
const cleanImport = importStatement
36+
.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, '')
37+
.replace(/\s+/g, ' ')
38+
.trim();
39+
40+
// Extract icons between { }
41+
const matchIcons = cleanImport.match(/{\s*(.+?)\s*}/);
42+
if (!matchIcons) {
43+
logger?.warn('No icon matches found in import statement');
44+
return [];
45+
}
46+
47+
// Split icons and clean up
48+
return [
49+
...new Set(
50+
matchIcons[1]
51+
.split(',')
52+
.map((icon) => icon.trim())
53+
.filter((icon) => icon && !icon.includes('=') && !icon.startsWith('Icon Size') && !icon.includes('('))
54+
)
55+
];
56+
} catch (error) {
57+
logger?.error('Error extracting icon names', error);
58+
return [];
59+
}
60+
}
61+
62+
/**
63+
* Find repo root directory
64+
* @returns {string} Path to repository root
65+
*/
66+
export function findRepoRoot() {
67+
const __filename = fileURLToPath(import.meta.url);
68+
const __dirname = path.dirname(__filename);
69+
70+
// Traverse up the directory tree to find the repo root
71+
let currentDir = __dirname;
72+
while (currentDir !== path.dirname(currentDir)) {
73+
if (existsSync(path.join(currentDir, '.git')) || existsSync(path.join(currentDir, 'package.json'))) {
74+
return currentDir;
75+
}
76+
currentDir = path.dirname(currentDir);
77+
}
78+
return __dirname; // Fallback
79+
}
80+
81+
/**
82+
* Load and validate icon data
83+
* @param {string} iconsDataPath - Path to icon data JSON
84+
* @param {Object} config - Configuration object
85+
* @param {Object} logger - Logger instance
86+
* @returns {Array} Array of icon data objects
87+
*/
88+
export async function loadIconData(iconsDataPath, config, logger) {
89+
try {
90+
const iconsDataContent = await fs.readFile(iconsDataPath, 'utf8');
91+
const iconsData = JSON.parse(iconsDataContent);
92+
93+
if (!Array.isArray(iconsData)) {
94+
logger.warn('Icon data is not an array, initializing as empty array');
95+
return [];
96+
}
97+
98+
// Validate icon data
99+
const validIcons = iconsData.filter((icon) => {
100+
const isValid = icon && icon.iconName && icon.fileName && icon.reactName;
101+
if (!isValid) {
102+
logger.warn(`Found invalid icon data: ${JSON.stringify(icon)}`);
103+
}
104+
return isValid;
105+
});
106+
107+
logger.success(`Loaded ${validIcons.length} valid icons from ${iconsDataPath}`);
108+
return validIcons;
109+
} catch (error) {
110+
logger.error('Failed to read icons data', error);
111+
112+
// Provide example data if file is missing
113+
return [
114+
{
115+
iconName: 'angle-down',
116+
fileName: 'angle-down-icon',
117+
reactName: 'AngleDownIcon',
118+
url: `${config.figmaBaseUrl}?node-id=${config.defaultNodeId}&m=dev`,
119+
svgPath: '<path d="M12 15.5l-6-6 1.4-1.4 4.6 4.6 4.6-4.6 1.4 1.4z" />'
120+
}
121+
];
122+
}
123+
}
124+
125+
/**
126+
* Generate a figma.connect statement for an icon
127+
* @param {string} iconName - React component name
128+
* @param {string} url - Figma URL
129+
* @returns {string} - Formatted figma.connect statement
130+
*/
131+
export function generateConnectStatement(iconName, url) {
132+
return `figma.connect(${iconName}, "${url}", {
133+
props: {},
134+
example: (props) => <${iconName} {...props} />
135+
});`;
136+
}
137+
138+
/**
139+
* Find matching icon configuration by name
140+
* @param {string} iconName - React component name to find
141+
* @param {Array} iconsData - Array of icon configurations
142+
* @returns {Object|null} - Matching icon configuration or null
143+
*/
144+
export function findIconByName(iconName, iconsData) {
145+
if (!Array.isArray(iconsData)) {
146+
return null;
147+
}
148+
149+
return iconsData.find(
150+
(icon) =>
151+
icon.reactName === iconName ||
152+
(icon.fileName && icon.fileName.replace('-icon', '') === iconName.replace('Icon', '').toLowerCase())
153+
);
154+
}
155+
156+
/**
157+
* Generate summary statistics for icon generation
158+
* @param {Object} stats - Statistics object
159+
* @returns {string} - Formatted summary string
160+
*/
161+
export function generateSummary(stats) {
162+
const elapsedTime = stats.endTime - stats.startTime;
163+
164+
return `
165+
Icon Generation Summary:
166+
----------------------
167+
Total Icons: ${stats.totalIcons}
168+
New Icons: ${stats.newIcons}
169+
Updated Icons: ${stats.updatedIcons}
170+
Errors: ${stats.errors}
171+
----------------------
172+
Time Elapsed: ${elapsedTime}ms
173+
`;
174+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './config.mjs';
2+
export * from './logger.mjs';

0 commit comments

Comments
 (0)