sparkle-design-cli
Version:
Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール
350 lines (298 loc) • 11.2 kB
JavaScript
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,
};