UNPKG

sparkle-design-cli

Version:

Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール

350 lines (298 loc) 11.2 kB
#!/usr/bin/env node import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { oklch } from 'culori'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * sparkle.config.jsonを読み込む */ function loadConfig(configPath = null) { // カスタムパスが指定されていない場合は実行場所のsparkle.config.jsonを探す const defaultConfigPath = path.resolve(process.cwd(), 'sparkle.config.json'); const resolvedConfigPath = configPath ? path.resolve(configPath) : defaultConfigPath; try { const configContent = fs.readFileSync(resolvedConfigPath, 'utf8'); const config = JSON.parse(configContent); console.log('✅ sparkle.config.json を読み込みました:', resolvedConfigPath); console.log(' 設定内容:', config); return config; } catch (error) { console.error('❌ sparkle.config.json の読み込みに失敗しました:', error.message); console.error('設定ファイルパス:', resolvedConfigPath); console.error('設定ファイルは専用プラグインから生成するか、手動で作成してください'); process.exit(1); } } /** * CSSテンプレートを読み込む */ function loadTemplate() { const templatePath = path.join( __dirname, '..', 'templates', 'sparkle-variables', 'sparkle-design.template.css' ); try { const templateContent = fs.readFileSync(templatePath, 'utf8'); console.log('✅ CSSテンプレートを読み込みました'); return templateContent; } catch (error) { console.error('❌ CSSテンプレートの読み込みに失敗しました:', error.message); console.error('テンプレートファイルが存在することを確認してください:', templatePath); process.exit(1); } } /** * 色定義(colors.json)を読み込む */ function loadColors() { const colorsPath = path.join(__dirname, '..', 'templates', 'sparkle-variables', 'colors.json'); try { const colorsContent = fs.readFileSync(colorsPath, 'utf8'); const colors = JSON.parse(colorsContent); console.log('✅ colors.json を読み込みました'); return colors; } catch (error) { console.error('❌ colors.json の読み込みに失敗しました:', error.message); process.exit(1); } } /** * gray.jsonを読み込む */ function loadGrayMapping() { const grayJsonPath = path.join(__dirname, '..', 'templates', 'sparkle-variables', 'gray.json'); try { const grayContent = fs.readFileSync(grayJsonPath, 'utf8'); const grayMapping = JSON.parse(grayContent); console.log('✅ gray.json を読み込みました'); return grayMapping; } catch (error) { console.error('❌ gray.json の読み込みに失敗しました:', error.message); process.exit(1); } } /** * radius.csvを読み込む */ function loadRadiusMapping() { const radiusCsvPath = path.join(__dirname, '..', 'templates', 'sparkle-variables', 'radius.csv'); try { const csvContent = fs.readFileSync(radiusCsvPath, 'utf8'); const lines = csvContent.trim().split('\n'); const headers = lines[0].split(','); const radiusMapping = {}; for (let i = 1; i < lines.length; i++) { const values = lines[i].split(','); const name = values[0]; radiusMapping[name] = {}; for (let j = 1; j < headers.length; j++) { radiusMapping[name][headers[j]] = values[j]; } } console.log('✅ radius.csv を読み込みました'); return radiusMapping; } catch (error) { console.error('❌ radius.csv の読み込みに失敗しました:', error.message); process.exit(1); } } /** * テンプレート変数を設定値で置換する */ function processTemplate(template, config, grayMapping, radiusMapping, colors) { let processedCSS = template; // 1. 色のトークンを生成して置換 const colorTokens = generateColorTokens(colors, grayMapping, config.primary); // プレースホルダーを色のトークンで置換(改行を含む) processedCSS = processedCSS.replace(/\s*\/\* \{\{COLOR_TOKENS\}\} \*\/\s*/, `\n${colorTokens}\n`); // 2. 基本的な設定値による置換 Object.entries(config).forEach(([key, value]) => { // 通常のプレースホルダー(CSS用 - スペースはそのまま) const placeholder = `{{${key.toUpperCase().replace('-', '_')}}}`; processedCSS = processedCSS.replace(new RegExp(placeholder, 'g'), value); // フォントファミリーの場合はURL用のプレースホルダーも生成(スペースを+に置き換え) if (key.toLowerCase().includes('font')) { const urlPlaceholder = `{{${key.toUpperCase().replace('-', '_')}_URL}}`; const urlValue = value.replace(/\s+/g, '+'); processedCSS = processedCSS.replace(new RegExp(urlPlaceholder, 'g'), urlValue); } }); // 3. Radiusの一括置換 if (config.radius && radiusMapping[config.radius]) { const radiusValues = radiusMapping[config.radius]; Object.entries(radiusValues).forEach(([semanticName, radiusValue]) => { if (semanticName === 'round') return; // roundは特別扱い const semanticPattern = new RegExp( `--radius-${semanticName}:\\s*var\\(--radius-[^)]+\\)`, 'g' ); processedCSS = processedCSS.replace( semanticPattern, `--radius-${semanticName}: var(--radius-${radiusValue})` ); }); console.log(`✅ ${config.radius}に対応するradius設定を適用しました`); } console.log('✅ テンプレートを設定値で処理しました'); return processedCSS; } /** * 16進数カラーをrgba形式に変換 */ function hexToRgba(hex) { // #を削除 hex = hex.replace('#', ''); // 3桁の場合は6桁に拡張 if (hex.length === 3) { hex = hex .split('') .map((char) => char + char) .join(''); } const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); return `rgba(${r}, ${g}, ${b}, 1)`; } /** * 16進数カラーをOKLCH形式に変換 * @param {string} hex 16進数のカラーコード(#付きまたは無し) * @returns {string} OKLCH形式の文字列 */ function hexToOklch(hex) { // #を削除 const cleanHex = hex.replace('#', ''); // 3桁の場合は6桁に拡張 const fullHex = cleanHex.length === 3 ? cleanHex .split('') .map((char) => char + char) .join('') : cleanHex; // culoriを使用してHEXからOKLCHに変換 const oklchColor = oklch(`#${fullHex}`); if (!oklchColor) { console.warn(`⚠️ 色の変換に失敗しました: ${hex}`); return `oklch(0 0 0)`; } // 精度を調整して出力 const l = Math.round((oklchColor.l || 0) * 10000) / 10000; const c = Math.round((oklchColor.c || 0) * 10000) / 10000; const h = oklchColor.h ? Math.round(oklchColor.h * 10) / 10 : 0; return `oklch(${l} ${c} ${h})`; } /** * 色のトークンを生成する * @param {Object} colors colors.jsonから読み込んだ色の定義 * @param {Object} grayMapping gray.jsonから読み込んだグレーの定義 * @param {string} primaryColor プライマリカラーの名前 * @returns {string} 色のトークンのCSS文字列 */ function generateColorTokens(colors, grayMapping, primaryColor) { const colorTokens = []; // 通常の色トークンを生成 Object.entries(colors).forEach(([colorName, colorValues]) => { if (typeof colorValues === 'object' && colorValues !== null) { Object.entries(colorValues).forEach(([level, hexValue]) => { const oklchValue = hexToOklch(hexValue); colorTokens.push(` --color-${colorName}-${level}: ${oklchValue};`); }); } else if (typeof colorValues === 'string') { // black, white, web-focusなどの単色 const oklchValue = hexToOklch(colorValues); colorTokens.push(` --color-${colorName}: ${oklchValue};`); } }); // グレーの色トークンを生成(プライマリカラーに基づく) if (primaryColor && grayMapping[primaryColor]) { const grayColors = grayMapping[primaryColor]; Object.entries(grayColors).forEach(([key, hexValue]) => { const grayLevel = key === '0' ? '50' : key === '1' ? '100' : key === '2' ? '200' : key === '3' ? '300' : key === '4' ? '400' : key === '5' ? '500' : key === '6' ? '600' : key === '7' ? '700' : key === '8' ? '800' : key === '9' ? '900' : key; const oklchValue = hexToOklch(hexValue); colorTokens.push(` --color-gray-${grayLevel}: ${oklchValue};`); }); } return colorTokens.join('\n'); } /** * 生成されたCSSをファイルに書き出す */ function writeCSS(cssContent, outputPath = null) { // カスタムパスが指定されていない場合は実行場所からsrc/appディレクトリに出力 const defaultOutputPath = path.resolve(process.cwd(), 'src', 'app', 'sparkle-design.css'); const resolvedOutputPath = outputPath ? path.resolve(outputPath) : defaultOutputPath; try { // 出力ディレクトリが存在しない場合は作成 const outputDir = path.dirname(resolvedOutputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } fs.writeFileSync(resolvedOutputPath, cssContent, 'utf8'); console.log('✅ sparkle-design.css を生成しました:', resolvedOutputPath); } catch (error) { console.error('❌ CSSファイルの書き出しに失敗しました:', error.message); process.exit(1); } } /** * メイン処理 */ export function generateCSS(configPath = null, outputPath = null) { console.log('🚀 Sparkle Design CSS Generator を開始します...\n'); // 1. 設定ファイルを読み込み const config = loadConfig(configPath); // 2. テンプレートを読み込み const template = loadTemplate(); // 3. colors.json、gray.json、radius.csvを読み込み const colors = loadColors(); const grayMapping = loadGrayMapping(); const radiusMapping = loadRadiusMapping(); // 4. テンプレートを設定値で処理 const processedCSS = processTemplate(template, config, grayMapping, radiusMapping, colors); // 5. CSSファイルを書き出し writeCSS(processedCSS, outputPath); console.log('\n🎉 CSS生成が完了しました!'); } // スクリプトが直接実行された場合のみメイン処理を実行 if (import.meta.url === `file://${process.argv[1]}`) { generateCSS(); } export { loadConfig, loadTemplate, loadColors, loadGrayMapping, loadRadiusMapping, processTemplate, writeCSS, hexToRgba, hexToOklch, generateColorTokens, };