sparkle-design-cli 1.1.8 → 1.2.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 CHANGED
@@ -83,6 +83,40 @@ sparkle-design-cli -c ./config/custom.json -o ./dist/styles.css
83
83
  - カスタム出力先: `-o` オプションで指定可能
84
84
  - 実行場所を基準として相対パスで処理されます
85
85
 
86
+ ## 自動フォント管理
87
+
88
+ **v1.2.0 以降の新機能**
89
+
90
+ CLI は自動的にフォント管理を行います:
91
+
92
+ 1. **フォント検出**: `sparkle-design.css` から Google Fonts の `@import` 文を検出
93
+ 2. **globals.css への移動**: フォントの `@import` を `globals.css` の先頭に移動
94
+ 3. **sparkle-design.css から削除**: 元のファイルからフォント `@import` を削除
95
+
96
+ ### なぜこの処理が必要なのか?
97
+
98
+ CSS の仕様では、`@import` 文は `@charset` と `@layer` 以外のすべてのルールより前に記述する必要があります。Tailwind CSS v4 と組み合わせた場合、この順序が正しくないとビルド時に警告が発生します。
99
+
100
+ この機能により、以下の警告が自動的に解決されます:
101
+
102
+ ```
103
+ ⚠️ @import rules must precede all rules aside from @charset and @layer statements
104
+ ```
105
+
106
+ ### 動作条件
107
+
108
+ - `globals.css` が `sparkle-design.css` と同じディレクトリに存在する場合のみ実行されます
109
+ - `globals.css` が存在しない場合は、フォント管理処理はスキップされます
110
+
111
+ ### 出力例
112
+
113
+ ```
114
+ 📦 フォント管理処理を開始します...
115
+ 📝 3個のフォントimportを検出しました
116
+ ✅ globals.css にフォントimportを追加しました
117
+ ✅ sparkle-design.css からフォントimportを削除しました
118
+ ```
119
+
86
120
  ## 開発
87
121
 
88
122
  ### セットアップ
@@ -0,0 +1,110 @@
1
+ /**
2
+ * カラー変換ユーティリティ
3
+ */
4
+
5
+ import { oklch } from 'culori';
6
+ import { COLOR_PRECISION, GRAY_LEVEL_MAP, MESSAGES } from './constants.js';
7
+
8
+ /**
9
+ * 16進数カラーをrgba形式に変換
10
+ * @param {string} hex 16進数のカラーコード(#付きまたは無し)
11
+ * @returns {string} rgba形式の文字列
12
+ */
13
+ export function hexToRgba(hex) {
14
+ // #を削除
15
+ let cleanHex = hex.replace('#', '');
16
+
17
+ // 3桁の場合は6桁に拡張
18
+ if (cleanHex.length === 3) {
19
+ cleanHex = cleanHex
20
+ .split('')
21
+ .map((char) => char + char)
22
+ .join('');
23
+ }
24
+
25
+ const r = parseInt(cleanHex.substring(0, 2), 16);
26
+ const g = parseInt(cleanHex.substring(2, 4), 16);
27
+ const b = parseInt(cleanHex.substring(4, 6), 16);
28
+
29
+ return `rgba(${r}, ${g}, ${b}, 1)`;
30
+ }
31
+
32
+ /**
33
+ * 16進数カラーをOKLCH形式に変換
34
+ * @param {string} hex 16進数のカラーコード(#付きまたは無し)
35
+ * @returns {string} OKLCH形式の文字列
36
+ */
37
+ export function hexToOklch(hex) {
38
+ // #を削除
39
+ const cleanHex = hex.replace('#', '');
40
+
41
+ // 3桁の場合は6桁に、4桁の場合は8桁に拡張
42
+ let fullHex = cleanHex;
43
+ if (cleanHex.length === 3 || cleanHex.length === 4) {
44
+ fullHex = cleanHex
45
+ .split('')
46
+ .map((char) => char + char)
47
+ .join('');
48
+ }
49
+
50
+ // culoriを使用してHEXからOKLCHに変換
51
+ const oklchColor = oklch(`#${fullHex}`);
52
+
53
+ if (!oklchColor) {
54
+ console.warn(MESSAGES.COLOR_CONVERSION_FAILED(hex));
55
+ return `oklch(0 0 0)`;
56
+ }
57
+
58
+ // 精度を調整して出力
59
+ const l = Math.round((oklchColor.l || 0) * COLOR_PRECISION.OKLCH_L) / COLOR_PRECISION.OKLCH_L;
60
+ const c = Math.round((oklchColor.c || 0) * COLOR_PRECISION.OKLCH_C) / COLOR_PRECISION.OKLCH_C;
61
+ const h = oklchColor.h
62
+ ? Math.round(oklchColor.h * COLOR_PRECISION.OKLCH_H) / COLOR_PRECISION.OKLCH_H
63
+ : 0;
64
+
65
+ // アルファ値がある場合(1未満の場合のみ出力に追加)
66
+ if (oklchColor.alpha !== undefined && oklchColor.alpha < 1) {
67
+ const a =
68
+ Math.round(oklchColor.alpha * COLOR_PRECISION.OKLCH_ALPHA) / COLOR_PRECISION.OKLCH_ALPHA;
69
+ return `oklch(${l} ${c} ${h} / ${a})`;
70
+ }
71
+
72
+ return `oklch(${l} ${c} ${h})`;
73
+ }
74
+
75
+ /**
76
+ * 色のトークンを生成する
77
+ * @param {Object} colors colors.jsonから読み込んだ色の定義
78
+ * @param {Object} grayMapping gray.jsonから読み込んだグレーの定義
79
+ * @param {string} primaryColor プライマリカラーの名前
80
+ * @returns {string} 色のトークンのCSS文字列
81
+ */
82
+ export function generateColorTokens(colors, grayMapping, primaryColor) {
83
+ const colorTokens = [];
84
+
85
+ // 通常の色トークンを生成
86
+ Object.entries(colors).forEach(([colorName, colorValues]) => {
87
+ if (typeof colorValues === 'object' && colorValues !== null) {
88
+ Object.entries(colorValues).forEach(([level, hexValue]) => {
89
+ const oklchValue = hexToOklch(hexValue);
90
+ colorTokens.push(` --color-${colorName}-${level}: ${oklchValue};`);
91
+ });
92
+ } else if (typeof colorValues === 'string') {
93
+ // black, white, web-focusなどの単色
94
+ const oklchValue = hexToOklch(colorValues);
95
+ colorTokens.push(` --color-${colorName}: ${oklchValue};`);
96
+ }
97
+ });
98
+
99
+ // グレーの色トークンを生成(プライマリカラーに基づく)
100
+ if (primaryColor && grayMapping[primaryColor]) {
101
+ const grayColors = grayMapping[primaryColor];
102
+ Object.entries(grayColors).forEach(([key, hexValue]) => {
103
+ const grayLevel = GRAY_LEVEL_MAP[key] || key;
104
+ const oklchValue = hexToOklch(hexValue);
105
+ colorTokens.push(` --color-gray-${grayLevel}: ${oklchValue};`);
106
+ });
107
+ }
108
+
109
+ return colorTokens.join('\n');
110
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * sparkle-design-cli で使用する定数定義
3
+ */
4
+
5
+ // ファイルパス関連
6
+ export const FILE_NAMES = {
7
+ CONFIG: 'sparkle.config.json',
8
+ OUTPUT: 'sparkle-design.css',
9
+ GLOBALS: 'globals.css',
10
+ TEMPLATE: 'sparkle-design.template.css',
11
+ COLORS: 'colors.json',
12
+ GRAY: 'gray.json',
13
+ RADIUS: 'radius.csv',
14
+ };
15
+
16
+ export const PATHS = {
17
+ DEFAULT_OUTPUT_DIR: ['src', 'app'],
18
+ TEMPLATE_DIR: ['templates', 'sparkle-variables'],
19
+ };
20
+
21
+ // 正規表現パターン
22
+ export const REGEX = {
23
+ // フォント関連
24
+ FONT_IMPORT: /@import\s+url\([^)]+fonts\.googleapis\.com[^)]+\);?/g,
25
+ FONT_IMPORT_WITH_COMMENT:
26
+ /\/\*\s*フォントのインポート\s*\*\/\s*\n?|@import\s+url\([^)]+fonts\.googleapis\.com[^)]+\);?\s*\n?/g,
27
+ EXISTING_FONT_IMPORT_BLOCK:
28
+ /\/\*\s*フォントのインポート[^*]*\*\/\s*\n?(?:@import\s+url\([^)]+fonts\.googleapis\.com[^)]+\);?\s*\n?)*\n?/g,
29
+
30
+ // Sparkle Design関連
31
+ SPARKLE_IMPORT:
32
+ /\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"](\.\/)?sparkle-design\.css['"];?\s*\n?/g,
33
+ TAILWIND_IMPORT: /@import\s+['"]tailwindcss['"];?/,
34
+
35
+ // テンプレート関連
36
+ COLOR_TOKENS_PLACEHOLDER: /\s*\/\* \{\{COLOR_TOKENS\}\} \*\/\s*/,
37
+ RADIUS_SEMANTIC_PATTERN: (semanticName) =>
38
+ new RegExp(`--radius-${semanticName}:\\s*var\\(--radius-[^)]+\\)`, 'g'),
39
+ };
40
+
41
+ // テンプレートプレースホルダー
42
+ export const PLACEHOLDERS = {
43
+ COLOR_TOKENS: '{{COLOR_TOKENS}}',
44
+ };
45
+
46
+ // カラー変換精度
47
+ export const COLOR_PRECISION = {
48
+ OKLCH_L: 10000,
49
+ OKLCH_C: 10000,
50
+ OKLCH_H: 10,
51
+ OKLCH_ALPHA: 10000,
52
+ };
53
+
54
+ // グレーレベルのマッピング
55
+ export const GRAY_LEVEL_MAP = {
56
+ 0: '50',
57
+ 1: '100',
58
+ 2: '200',
59
+ 3: '300',
60
+ 4: '400',
61
+ 5: '500',
62
+ 6: '600',
63
+ 7: '700',
64
+ 8: '800',
65
+ 9: '900',
66
+ };
67
+
68
+ // コメントテンプレート
69
+ export const COMMENTS = {
70
+ FONT_IMPORT: '/* フォントのインポート(CSSの仕様上、@importは最初に記述する必要がある) */',
71
+ SPARKLE_IMPORT: '/* Sparkle Design のカスタム定義(Tailwindの後にインポート) */',
72
+ TAILWIND_IMPORT: '/* Tailwindのインポート */',
73
+ };
74
+
75
+ // インポート文のテンプレート
76
+ export const IMPORTS = {
77
+ SPARKLE_DESIGN: '@import "./sparkle-design.css";',
78
+ TAILWIND: '@import "tailwindcss";',
79
+ };
80
+
81
+ // ログメッセージ
82
+ export const MESSAGES = {
83
+ START: '🚀 Sparkle Design CSS Generator を開始します...\n',
84
+ SUCCESS: '\n🎉 CSS生成が完了しました!',
85
+ FONT_MANAGEMENT_START: '\n📦 フォント管理処理を開始します...',
86
+
87
+ // 成功メッセージ
88
+ CONFIG_LOADED: (path) => `✅ sparkle.config.json を読み込みました: ${path}`,
89
+ TEMPLATE_LOADED: '✅ CSSテンプレートを読み込みました',
90
+ COLORS_LOADED: '✅ colors.json を読み込みました',
91
+ GRAY_LOADED: '✅ gray.json を読み込みました',
92
+ RADIUS_LOADED: '✅ radius.csv を読み込みました',
93
+ TEMPLATE_PROCESSED: '✅ テンプレートを設定値で処理しました',
94
+ CSS_GENERATED: (path) => `✅ sparkle-design.css を生成しました: ${path}`,
95
+ RADIUS_APPLIED: (radius) => `✅ ${radius}に対応するradius設定を適用しました`,
96
+ GLOBALS_UPDATED: (path) =>
97
+ `✅ globals.css にフォントimportとsparkle-design.css importを追加しました: ${path}`,
98
+ FONT_REMOVED: '✅ sparkle-design.css からフォントimportを削除しました',
99
+
100
+ // 情報メッセージ
101
+ GLOBALS_NOT_FOUND: 'ℹ️ globals.css が見つかりません。フォント処理をスキップします。',
102
+ NO_FONT_IMPORTS: 'ℹ️ sparkle-design.css にフォントimportが見つかりません。',
103
+ FONT_DETECTED: (count) => `📝 ${count}個のフォントimportを検出しました`,
104
+
105
+ // 警告メッセージ
106
+ TAILWIND_NOT_FOUND: '⚠️ globals.css に Tailwind import が見つかりません。',
107
+ GLOBALS_UPDATE_FAILED: (error) => `⚠️ globals.css の更新に失敗しました: ${error}`,
108
+ FONT_MANAGEMENT_ERROR: (error) => `⚠️ フォント管理処理でエラーが発生しました: ${error}`,
109
+ COLOR_CONVERSION_FAILED: (hex) => `⚠️ 色の変換に失敗しました: ${hex}`,
110
+
111
+ // エラーメッセージ
112
+ CONFIG_LOAD_FAILED: (error) => `❌ sparkle.config.json の読み込みに失敗しました: ${error}`,
113
+ CONFIG_PATH_INFO: (path) => `設定ファイルパス: ${path}`,
114
+ CONFIG_HELP: '設定ファイルは専用プラグインから生成するか、手動で作成してください',
115
+ TEMPLATE_LOAD_FAILED: (error) => `❌ CSSテンプレートの読み込みに失敗しました: ${error}`,
116
+ TEMPLATE_PATH_INFO: (path) => `テンプレートファイルが存在することを確認してください: ${path}`,
117
+ COLORS_LOAD_FAILED: (error) => `❌ colors.json の読み込みに失敗しました: ${error}`,
118
+ GRAY_LOAD_FAILED: (error) => `❌ gray.json の読み込みに失敗しました: ${error}`,
119
+ RADIUS_LOAD_FAILED: (error) => `❌ radius.csv の読み込みに失敗しました: ${error}`,
120
+ CSS_WRITE_FAILED: (error) => `❌ CSSファイルの書き出しに失敗しました: ${error}`,
121
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * ファイル読み込み機能
3
+ * 設定ファイル、テンプレート、カラー定義などの読み込み処理を統一
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { FILE_NAMES, PATHS, MESSAGES } from './constants.js';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ /**
15
+ * ファイルを読み込む共通関数
16
+ * @param {string} filePath ファイルパス
17
+ * @param {string} successMessage 成功時のメッセージ
18
+ * @param {string} errorMessage エラー時のメッセージ
19
+ * @param {Function} additionalErrorHandler 追加のエラーハンドラ(オプション)
20
+ * @returns {string} ファイル内容
21
+ */
22
+ function readFile(filePath, successMessage, errorMessage, additionalErrorHandler = null) {
23
+ try {
24
+ const content = fs.readFileSync(filePath, 'utf8');
25
+ console.log(successMessage);
26
+ return content;
27
+ } catch (error) {
28
+ console.error(errorMessage);
29
+ if (additionalErrorHandler) {
30
+ additionalErrorHandler(filePath, error);
31
+ }
32
+ process.exit(1);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * JSONファイルを読み込む共通関数
38
+ * @param {string} filePath ファイルパス
39
+ * @param {string} successMessage 成功時のメッセージ
40
+ * @param {string} errorMessage エラー時のメッセージ
41
+ * @param {Function} additionalErrorHandler 追加のエラーハンドラ(オプション)
42
+ * @returns {Object} パース済みのJSONオブジェクト
43
+ */
44
+ function readJsonFile(filePath, successMessage, errorMessage, additionalErrorHandler = null) {
45
+ const content = readFile(filePath, successMessage, errorMessage, additionalErrorHandler);
46
+ return JSON.parse(content);
47
+ }
48
+
49
+ /**
50
+ * テンプレートディレクトリのパスを取得する
51
+ * @returns {string} テンプレートディレクトリのパス
52
+ */
53
+ function getTemplateDir() {
54
+ return path.join(__dirname, '..', ...PATHS.TEMPLATE_DIR);
55
+ }
56
+
57
+ /**
58
+ * sparkle.config.jsonを読み込む
59
+ * @param {string|null} configPath カスタム設定ファイルのパス(オプション)
60
+ * @returns {Object} 設定オブジェクト
61
+ */
62
+ export function loadConfig(configPath = null) {
63
+ // カスタムパスが指定されていない場合は実行場所のsparkle.config.jsonを探す
64
+ const defaultConfigPath = path.resolve(process.cwd(), FILE_NAMES.CONFIG);
65
+ const resolvedConfigPath = configPath ? path.resolve(configPath) : defaultConfigPath;
66
+
67
+ const config = readJsonFile(
68
+ resolvedConfigPath,
69
+ MESSAGES.CONFIG_LOADED(resolvedConfigPath),
70
+ MESSAGES.CONFIG_LOAD_FAILED(''),
71
+ (filePath, _error) => {
72
+ console.error(MESSAGES.CONFIG_PATH_INFO(filePath));
73
+ console.error(MESSAGES.CONFIG_HELP);
74
+ }
75
+ );
76
+
77
+ console.log(' 設定内容:', config);
78
+ return config;
79
+ }
80
+
81
+ /**
82
+ * CSSテンプレートを読み込む
83
+ * @returns {string} テンプレート内容
84
+ */
85
+ export function loadTemplate() {
86
+ const templatePath = path.join(getTemplateDir(), FILE_NAMES.TEMPLATE);
87
+
88
+ return readFile(
89
+ templatePath,
90
+ MESSAGES.TEMPLATE_LOADED,
91
+ MESSAGES.TEMPLATE_LOAD_FAILED(''),
92
+ (filePath) => {
93
+ console.error(MESSAGES.TEMPLATE_PATH_INFO(filePath));
94
+ }
95
+ );
96
+ }
97
+
98
+ /**
99
+ * 色定義(colors.json)を読み込む
100
+ * @returns {Object} 色定義オブジェクト
101
+ */
102
+ export function loadColors() {
103
+ const colorsPath = path.join(getTemplateDir(), FILE_NAMES.COLORS);
104
+
105
+ return readJsonFile(colorsPath, MESSAGES.COLORS_LOADED, MESSAGES.COLORS_LOAD_FAILED(''));
106
+ }
107
+
108
+ /**
109
+ * gray.jsonを読み込む
110
+ * @returns {Object} グレーマッピングオブジェクト
111
+ */
112
+ export function loadGrayMapping() {
113
+ const grayJsonPath = path.join(getTemplateDir(), FILE_NAMES.GRAY);
114
+
115
+ return readJsonFile(grayJsonPath, MESSAGES.GRAY_LOADED, MESSAGES.GRAY_LOAD_FAILED(''));
116
+ }
117
+
118
+ /**
119
+ * radius.csvを読み込む
120
+ * @returns {Object} Radiusマッピングオブジェクト
121
+ */
122
+ export function loadRadiusMapping() {
123
+ const radiusCsvPath = path.join(getTemplateDir(), FILE_NAMES.RADIUS);
124
+
125
+ const csvContent = readFile(
126
+ radiusCsvPath,
127
+ MESSAGES.RADIUS_LOADED,
128
+ MESSAGES.RADIUS_LOAD_FAILED('')
129
+ );
130
+
131
+ // CSVをパース
132
+ const lines = csvContent.trim().split('\n');
133
+ const headers = lines[0].split(',');
134
+ const radiusMapping = {};
135
+
136
+ for (let i = 1; i < lines.length; i++) {
137
+ const values = lines[i].split(',');
138
+ const name = values[0];
139
+ radiusMapping[name] = {};
140
+
141
+ for (let j = 1; j < headers.length; j++) {
142
+ radiusMapping[name][headers[j]] = values[j];
143
+ }
144
+ }
145
+
146
+ return radiusMapping;
147
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * フォント管理機能
3
+ * sparkle-design.css からフォントimportを抽出し、globals.css に移動する
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { REGEX, COMMENTS, IMPORTS, MESSAGES } from './constants.js';
9
+
10
+ /**
11
+ * sparkle-design.css からフォントの@import文を抽出する
12
+ * @param {string} cssContent CSS内容
13
+ * @returns {Array<string>} フォントimport文の配列
14
+ */
15
+ export function extractFontImports(cssContent) {
16
+ const matches = cssContent.match(REGEX.FONT_IMPORT);
17
+ return matches || [];
18
+ }
19
+
20
+ /**
21
+ * sparkle-design.css からフォントの@import文を削除する
22
+ * @param {string} cssContent CSS内容
23
+ * @returns {string} フォントimportを削除したCSS
24
+ */
25
+ export function removeFontImportsFromCSS(cssContent) {
26
+ return cssContent.replace(REGEX.FONT_IMPORT_WITH_COMMENT, '');
27
+ }
28
+
29
+ /**
30
+ * フォントimportブロックを生成する
31
+ * @param {Array<string>} fontImports フォントimport文の配列
32
+ * @returns {string} フォントimportブロック
33
+ */
34
+ function createFontImportBlock(fontImports) {
35
+ return [COMMENTS.FONT_IMPORT, ...fontImports, ''].join('\n');
36
+ }
37
+
38
+ /**
39
+ * sparkle-design.css importブロックを生成する
40
+ * @returns {string} sparkle-design.css importブロック
41
+ */
42
+ function createSparkleImportBlock() {
43
+ return ['', COMMENTS.SPARKLE_IMPORT, IMPORTS.SPARKLE_DESIGN, ''].join('\n');
44
+ }
45
+
46
+ /**
47
+ * globals.css の既存のimportを削除する
48
+ * @param {string} globalsContent globals.cssの内容
49
+ * @returns {string} 既存importを削除したglobals.css
50
+ */
51
+ function removeExistingImports(globalsContent) {
52
+ let cleaned = globalsContent;
53
+
54
+ // 既存のフォントimportを削除
55
+ cleaned = cleaned.replace(REGEX.EXISTING_FONT_IMPORT_BLOCK, '');
56
+
57
+ // 既存のsparkle-design.css importを削除
58
+ cleaned = cleaned.replace(REGEX.SPARKLE_IMPORT, '');
59
+
60
+ return cleaned;
61
+ }
62
+
63
+ /**
64
+ * Tailwind import の位置を見つける
65
+ * @param {string} globalsContent globals.cssの内容
66
+ * @returns {{match: string, index: number, afterIndex: number} | null} Tailwind importの情報
67
+ */
68
+ function findTailwindImport(globalsContent) {
69
+ const tailwindMatch = globalsContent.match(REGEX.TAILWIND_IMPORT);
70
+
71
+ if (!tailwindMatch) {
72
+ return null;
73
+ }
74
+
75
+ const tailwindIndex = globalsContent.indexOf(tailwindMatch[0]);
76
+ const afterTailwindIndex = tailwindIndex + tailwindMatch[0].length;
77
+
78
+ return {
79
+ match: tailwindMatch[0],
80
+ index: tailwindIndex,
81
+ afterIndex: afterTailwindIndex,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * globals.css を再構築する
87
+ * @param {string} globalsContent 元のglobals.cssの内容
88
+ * @param {string} fontImportBlock フォントimportブロック
89
+ * @param {string} sparkleImportBlock sparkle-design.css importブロック
90
+ * @param {Object} tailwindInfo Tailwind importの情報
91
+ * @returns {string} 再構築されたglobals.css
92
+ */
93
+ function reconstructGlobalsCss(globalsContent, fontImportBlock, sparkleImportBlock, tailwindInfo) {
94
+ const beforeTailwind = globalsContent.substring(0, tailwindInfo.index);
95
+ const afterTailwind = globalsContent.substring(tailwindInfo.afterIndex);
96
+
97
+ // フォントimport + Tailwind + sparkle-design.css + 残りのコンテンツ
98
+ return (
99
+ fontImportBlock +
100
+ beforeTailwind +
101
+ tailwindInfo.match +
102
+ sparkleImportBlock +
103
+ afterTailwind.trimStart()
104
+ );
105
+ }
106
+
107
+ /**
108
+ * globals.css を構造化して管理する
109
+ * - フォントimportを先頭に配置
110
+ * - Tailwind importを確保
111
+ * - sparkle-design.css importをTailwindの後に配置
112
+ * @param {Array<string>} fontImports フォントimport文の配列
113
+ * @param {string} globalsPath globals.cssのパス
114
+ */
115
+ export function updateGlobalsWithFonts(fontImports, globalsPath) {
116
+ if (fontImports.length === 0) {
117
+ return;
118
+ }
119
+
120
+ try {
121
+ // 1. globals.css を読み込む
122
+ let globalsContent = fs.readFileSync(globalsPath, 'utf8');
123
+
124
+ // 2. 既存のimportを削除
125
+ globalsContent = removeExistingImports(globalsContent);
126
+
127
+ // 3. Tailwind import の位置を見つける
128
+ const tailwindInfo = findTailwindImport(globalsContent);
129
+ if (!tailwindInfo) {
130
+ console.warn(MESSAGES.TAILWIND_NOT_FOUND);
131
+ return;
132
+ }
133
+
134
+ // 4. importブロックを生成
135
+ const fontImportBlock = createFontImportBlock(fontImports);
136
+ const sparkleImportBlock = createSparkleImportBlock();
137
+
138
+ // 5. globals.css を再構築
139
+ const reconstructedContent = reconstructGlobalsCss(
140
+ globalsContent,
141
+ fontImportBlock,
142
+ sparkleImportBlock,
143
+ tailwindInfo
144
+ );
145
+
146
+ // 6. 更新したglobals.cssを書き込む
147
+ fs.writeFileSync(globalsPath, reconstructedContent, 'utf8');
148
+ console.log(MESSAGES.GLOBALS_UPDATED(globalsPath));
149
+ } catch (error) {
150
+ console.error(MESSAGES.GLOBALS_UPDATE_FAILED(error.message));
151
+ // globals.cssの更新は必須ではないので、エラーでも処理を続行
152
+ }
153
+ }
154
+
155
+ /**
156
+ * フォント管理の自動処理を実行する
157
+ * sparkle-design.css からフォントimportを抽出し、globals.css に移動する
158
+ * @param {string} sparkleDesignPath sparkle-design.cssのパス
159
+ */
160
+ export function manageFontImports(sparkleDesignPath) {
161
+ try {
162
+ // 1. globals.cssのパスを推定(sparkle-design.cssと同じディレクトリ)
163
+ const globalsPath = path.join(path.dirname(sparkleDesignPath), 'globals.css');
164
+
165
+ // 2. globals.cssが存在しない場合はスキップ
166
+ if (!fs.existsSync(globalsPath)) {
167
+ console.log(MESSAGES.GLOBALS_NOT_FOUND);
168
+ return;
169
+ }
170
+
171
+ // 3. sparkle-design.css からフォントimportを抽出
172
+ const sparkleContent = fs.readFileSync(sparkleDesignPath, 'utf8');
173
+ const fontImports = extractFontImports(sparkleContent);
174
+
175
+ if (fontImports.length === 0) {
176
+ console.log(MESSAGES.NO_FONT_IMPORTS);
177
+ return;
178
+ }
179
+
180
+ console.log(MESSAGES.FONT_DETECTED(fontImports.length));
181
+
182
+ // 4. globals.css にフォントimportを追加
183
+ updateGlobalsWithFonts(fontImports, globalsPath);
184
+
185
+ // 5. sparkle-design.css からフォントimportを削除
186
+ const cleanedSparkleContent = removeFontImportsFromCSS(sparkleContent);
187
+ fs.writeFileSync(sparkleDesignPath, cleanedSparkleContent, 'utf8');
188
+ console.log(MESSAGES.FONT_REMOVED);
189
+ } catch (error) {
190
+ console.error(MESSAGES.FONT_MANAGEMENT_ERROR(error.message));
191
+ // フォント管理は必須ではないので、エラーでも処理を続行
192
+ }
193
+ }