sparkle-design-cli 2.0.7-beta.0 → 2.0.7-beta.2
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/bin/sparkle-design.js +12 -1
- package/lib/anti-pattern-rules.js +38 -7
- package/lib/constants.js +31 -2
- package/lib/font-manager.js +234 -48
- package/lib/generate-css.js +44 -15
- package/lib/setup.js +33 -17
- package/package.json +1 -1
package/bin/sparkle-design.js
CHANGED
|
@@ -19,6 +19,7 @@ function parseGenerateOptions(args) {
|
|
|
19
19
|
configPath: null,
|
|
20
20
|
outputPath: null,
|
|
21
21
|
globalsPath: null,
|
|
22
|
+
strict: false,
|
|
22
23
|
help: false,
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -36,6 +37,8 @@ function parseGenerateOptions(args) {
|
|
|
36
37
|
} else if (arg === '--globals-path') {
|
|
37
38
|
options.globalsPath = requireOptionValue(args, i, '--globals-path');
|
|
38
39
|
i += 1;
|
|
40
|
+
} else if (arg === '--strict') {
|
|
41
|
+
options.strict = true;
|
|
39
42
|
} else {
|
|
40
43
|
throw new Error(`Unknown option for generate: ${arg}`);
|
|
41
44
|
}
|
|
@@ -86,6 +89,7 @@ function parseSetupOptions(args) {
|
|
|
86
89
|
skipInstall: false,
|
|
87
90
|
skipScaffold: false,
|
|
88
91
|
skipGenerate: false,
|
|
92
|
+
strict: false,
|
|
89
93
|
help: false,
|
|
90
94
|
};
|
|
91
95
|
|
|
@@ -113,6 +117,8 @@ function parseSetupOptions(args) {
|
|
|
113
117
|
options.skipScaffold = true;
|
|
114
118
|
} else if (arg === '--skip-generate') {
|
|
115
119
|
options.skipGenerate = true;
|
|
120
|
+
} else if (arg === '--strict') {
|
|
121
|
+
options.strict = true;
|
|
116
122
|
} else {
|
|
117
123
|
throw new Error(`Unknown option for setup: ${arg}`);
|
|
118
124
|
}
|
|
@@ -161,6 +167,8 @@ Generate options:
|
|
|
161
167
|
-c, --config <path> 設定ファイルのパス (default: ./sparkle.config.json)
|
|
162
168
|
-o, --output <path> 出力ファイルのパス (default: ./src/app/sparkle-design.css)
|
|
163
169
|
--globals-path <path> Tailwind エントリポイント CSS のパス (default: 自動検出)
|
|
170
|
+
--strict globals.css への @source 注入等が失敗したら exit 1
|
|
171
|
+
(CI 向け。既定は warn + 継続で後方互換を維持)
|
|
164
172
|
|
|
165
173
|
sparkle.config.json の設定フィールド:
|
|
166
174
|
|
|
@@ -204,6 +212,7 @@ Setup options:
|
|
|
204
212
|
--skip-install パッケージインストール(sparkle-design, tailwindcss)をスキップ
|
|
205
213
|
--skip-scaffold 初期ファイル(sparkle.config.json, postcss.config, エントリ CSS)生成をスキップ
|
|
206
214
|
--skip-generate generate 実行をスキップ
|
|
215
|
+
--strict generate の失敗を exit 1 に昇格(CI 向け)
|
|
207
216
|
|
|
208
217
|
Setup の動作:
|
|
209
218
|
1. パッケージマネージャー検出(pnpm / npm / yarn / bun)
|
|
@@ -257,7 +266,9 @@ function main() {
|
|
|
257
266
|
process.exit(0);
|
|
258
267
|
}
|
|
259
268
|
|
|
260
|
-
generateCSS(options.configPath, options.outputPath, options.globalsPath
|
|
269
|
+
generateCSS(options.configPath, options.outputPath, options.globalsPath, {
|
|
270
|
+
strict: options.strict,
|
|
271
|
+
});
|
|
261
272
|
return;
|
|
262
273
|
}
|
|
263
274
|
|
|
@@ -23,7 +23,6 @@ function renderJSDocSection(section) {
|
|
|
23
23
|
return output.map((line) => (line ? ` * ${line}` : ' *')).join('\n');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
const MANUAL_REVIEW_REMINDERS = [
|
|
28
27
|
{
|
|
29
28
|
id: 'badge-tag-semantics',
|
|
@@ -492,7 +491,8 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
492
491
|
{
|
|
493
492
|
id: 'card-clickable-wrap',
|
|
494
493
|
check: {
|
|
495
|
-
description:
|
|
494
|
+
description:
|
|
495
|
+
'クリック可能な Card は ClickableCard を使う(button / a / role="button" でラップしない)',
|
|
496
496
|
recommendation:
|
|
497
497
|
'`<Card>` を `<button>` / `<a>` / `role="button"` を持つ要素で包まず、`ClickableCard` を使ってください。`ClickableCard` が適切な role / keyboard 対応 / focus ring を担保します。',
|
|
498
498
|
// <button> / <a> / role="button" が直接 <Card> を子に持つケース
|
|
@@ -949,7 +949,8 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
949
949
|
description: 'CardTitle に typography 系クラスを付与しない',
|
|
950
950
|
recommendation:
|
|
951
951
|
'CardTitle は character-4-bold-pro を内蔵しています。className で typography を上書きしないでください。',
|
|
952
|
-
pattern:
|
|
952
|
+
pattern:
|
|
953
|
+
/<CardTitle\b[^>]*\bclassName\s*=\s*(?:"[^"]*\bcharacter-[^"]*"|'[^']*\bcharacter-[^']*'|\{[^}]*\bcharacter-[^}]*\})[^>]*>/g,
|
|
953
954
|
},
|
|
954
955
|
featureSection: lines([
|
|
955
956
|
'### CardTitle に typography を上書きしない',
|
|
@@ -970,7 +971,21 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
970
971
|
description: 'CardControl に Button / IconButton 以外を入れない',
|
|
971
972
|
recommendation:
|
|
972
973
|
'CardControl はアクションボタン用です。ステータス表示には CardDescription を使ってください。',
|
|
973
|
-
|
|
974
|
+
// 以前の regex は `[\s\S]*?` が `</CardControl>` を越えて貪欲に探索してしまい、
|
|
975
|
+
// 隣接する <CardContent> 等を非 Button 子要素として誤検知していた。
|
|
976
|
+
// 内部コンテンツが `</CardControl>` を越えないよう lazy 側に停止条件を入れ、
|
|
977
|
+
// 開始タグの直後から終了タグ直前までの範囲だけを検査する。
|
|
978
|
+
// 加えて (1) 開始タグ自体が自己閉じ `/>` で終わっていたら子要素は存在しない
|
|
979
|
+
// ので検査対象外とする(`[^>/]*[^>/]?`)、(2) ネストした `<CardControl>` も
|
|
980
|
+
// Button/IconButton 扱いのスキップリストに加えて誤発報を防ぐ。
|
|
981
|
+
// en: The previous pattern let `[\s\S]*?` leak past `</CardControl>`,
|
|
982
|
+
// flagging adjacent siblings (e.g. `<CardContent>`) as non-Button children.
|
|
983
|
+
// We now (a) constrain the lazy body so it cannot cross the closing tag,
|
|
984
|
+
// (b) skip self-closing tags `<CardControl />` entirely, and
|
|
985
|
+
// (c) treat a nested `<CardControl>` as an allowed child so we don't
|
|
986
|
+
// report the inner wrapper of a pathological nested structure.
|
|
987
|
+
pattern:
|
|
988
|
+
/<CardControl\b(?:[^>]*[^>/])?>(?:(?!<\/CardControl\b)[\s\S])*?<(?!Button\b|IconButton\b|CardControl\b|\/CardControl\b)[A-Za-z][\w.]*/g,
|
|
974
989
|
},
|
|
975
990
|
featureSection: lines([
|
|
976
991
|
'### CardControl にはアクションボタンのみを入れる',
|
|
@@ -1020,7 +1035,8 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
1020
1035
|
description: 'Card 系コンポーネントのデフォルト padding を安易に上書きしない',
|
|
1021
1036
|
recommendation:
|
|
1022
1037
|
'CardHeader / CardContent のデフォルト padding(px-6 py-2)をそのまま使ってください。上書きは本当に必要な場合のみ。',
|
|
1023
|
-
pattern:
|
|
1038
|
+
pattern:
|
|
1039
|
+
/<Card(?:Header|Content)\b[^>]*\bclassName\s*=\s*(?:"[^"]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^"]*"|'[^']*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^']*'|\{[^}]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^}]*\})[^>]*>/g,
|
|
1024
1040
|
},
|
|
1025
1041
|
featureSection: lines([
|
|
1026
1042
|
'### Card 系コンポーネントの padding を上書きしない',
|
|
@@ -1045,7 +1061,8 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
1045
1061
|
description: 'asChild 使用時に prefixIcon / suffixIcon / isLoading を使わない',
|
|
1046
1062
|
recommendation:
|
|
1047
1063
|
'asChild モードでは prefixIcon / suffixIcon / isLoading は無視されます。アイコン付きの Link が必要なら asChild を外してください。',
|
|
1048
|
-
pattern:
|
|
1064
|
+
pattern:
|
|
1065
|
+
/<Button\b(?=[^>]*\basChild\b)(?=[^>]*\b(?:prefixIcon|suffixIcon|isLoading)\b)[^>]*>/g,
|
|
1049
1066
|
},
|
|
1050
1067
|
featureSection: lines([
|
|
1051
1068
|
'### asChild と prefixIcon / suffixIcon / isLoading を併用しない',
|
|
@@ -1133,7 +1150,21 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
1133
1150
|
];
|
|
1134
1151
|
|
|
1135
1152
|
function getCheckRules() {
|
|
1136
|
-
const order = [
|
|
1153
|
+
const order = [
|
|
1154
|
+
'dialog-form',
|
|
1155
|
+
'dialog-button-wrap',
|
|
1156
|
+
'button-icon-only',
|
|
1157
|
+
'material-symbols-direct',
|
|
1158
|
+
'shadcn-token',
|
|
1159
|
+
'tailwind-typography',
|
|
1160
|
+
'card-title-typography',
|
|
1161
|
+
'card-control-non-button',
|
|
1162
|
+
'card-padding-override',
|
|
1163
|
+
'aschild-with-icon-props',
|
|
1164
|
+
'disabled-vs-is-disabled',
|
|
1165
|
+
'button-prefixicon-jsx',
|
|
1166
|
+
'icon-children-text',
|
|
1167
|
+
];
|
|
1137
1168
|
|
|
1138
1169
|
return ANTI_PATTERN_GROUPS.filter((group) => group.check)
|
|
1139
1170
|
.map((group) => ({
|
package/lib/constants.js
CHANGED
|
@@ -18,6 +18,21 @@ export const PATHS = {
|
|
|
18
18
|
TEMPLATE_DIR: ['templates', 'sparkle-variables'],
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
// プロジェクト root から Tailwind エントリ CSS を探索するときの候補パス(優先順)。
|
|
22
|
+
// `setup` の scaffold 判定と、`generate` の `resolveGlobalsPath` の project-root
|
|
23
|
+
// fallback で同じ配列を参照することで二重化 drift を防ぐ。
|
|
24
|
+
// en: Candidate paths (priority-ordered) used both by `setup` scaffold and by
|
|
25
|
+
// `generate`'s project-root fallback in `resolveGlobalsPath`. Sharing the array
|
|
26
|
+
// prevents drift between "where we create the entry CSS" and "where we look
|
|
27
|
+
// for it later".
|
|
28
|
+
export const GLOBALS_CSS_CANDIDATES = [
|
|
29
|
+
'src/app/globals.css',
|
|
30
|
+
'app/globals.css',
|
|
31
|
+
'src/globals.css',
|
|
32
|
+
'src/index.css',
|
|
33
|
+
'src/styles/globals.css',
|
|
34
|
+
];
|
|
35
|
+
|
|
21
36
|
// 正規表現パターン
|
|
22
37
|
export const REGEX = {
|
|
23
38
|
// フォント関連
|
|
@@ -34,8 +49,17 @@ export const REGEX = {
|
|
|
34
49
|
TAILWIND_IMPORT: /@import\s+['"]tailwindcss['"];?/,
|
|
35
50
|
|
|
36
51
|
// @source ディレクティブ関連
|
|
52
|
+
// ※ 単体の SOURCE_DIRECTIVE は「ユーザーが手書きした @source も含めて全部」
|
|
53
|
+
// マッチしてしまうので、removeExistingImports では使わない(ユーザー記述を
|
|
54
|
+
// 消してしまう)。CLI が挿入した block(コメント + それに続く連続 @source 行)
|
|
55
|
+
// だけを除去するために MANAGED_SOURCE_BLOCK を使う。
|
|
56
|
+
// en: SOURCE_DIRECTIVE alone matches user-authored @source lines too, so
|
|
57
|
+
// removeExistingImports uses MANAGED_SOURCE_BLOCK instead to remove only the
|
|
58
|
+
// comment-prefixed block that the CLI itself wrote.
|
|
37
59
|
SOURCE_DIRECTIVE: /@source\s+["'][^"']*["'];?\s*\n?/g,
|
|
38
60
|
SOURCE_COMMENT: /\/\*\s*npm パッケージのコンテンツスキャン[^*]*\*\/\s*\n?/g,
|
|
61
|
+
MANAGED_SOURCE_BLOCK:
|
|
62
|
+
/\/\*\s*npm パッケージのコンテンツスキャン[^*]*\*\/\s*\n?(?:@source\s+["'][^"']*["'];?\s*\n?)*/g,
|
|
39
63
|
|
|
40
64
|
// カスタムCSS関連
|
|
41
65
|
CUSTOM_CSS_IMPORT:
|
|
@@ -107,7 +131,8 @@ export const COMMENTS = {
|
|
|
107
131
|
FONT_IMPORT: '/* フォントのインポート(CSSの仕様上、@importは最初に記述する必要がある) */',
|
|
108
132
|
SPARKLE_IMPORT: '/* Sparkle Design のカスタム定義(Tailwindの後にインポート) */',
|
|
109
133
|
TAILWIND_IMPORT: '/* Tailwindのインポート */',
|
|
110
|
-
SOURCE_DIRECTIVE:
|
|
134
|
+
SOURCE_DIRECTIVE:
|
|
135
|
+
'/* npm パッケージのコンテンツスキャン(Tailwind がクラスを検出するために必要) */',
|
|
111
136
|
CUSTOM_CSS: '/* プロジェクト固有のカスタムトークン */',
|
|
112
137
|
};
|
|
113
138
|
|
|
@@ -145,7 +170,11 @@ export const MESSAGES = {
|
|
|
145
170
|
FONT_REMOVE_SKIPPED: 'ℹ️ globals.css の更新に失敗したため、フォントimport削除をスキップします。',
|
|
146
171
|
|
|
147
172
|
// 警告メッセージ
|
|
148
|
-
|
|
173
|
+
// 指定された globals.css に Tailwind import が無いと @source / sparkle-design.css
|
|
174
|
+
// の挿入ができないため、どのファイルに何を書けばよいかを示す actionable なメッセージにする。
|
|
175
|
+
// en: Make the warning actionable — tell the user which file to edit and what to add.
|
|
176
|
+
TAILWIND_NOT_FOUND: (path) =>
|
|
177
|
+
`⚠️ ${path} に Tailwind import が見つかりません。ファイル先頭に \`@import "tailwindcss";\` を追記してください。`,
|
|
149
178
|
GLOBALS_UPDATE_FAILED: (error) => `⚠️ globals.css の更新に失敗しました: ${error}`,
|
|
150
179
|
FONT_MANAGEMENT_ERROR: (error) => `⚠️ フォント管理処理でエラーが発生しました: ${error}`,
|
|
151
180
|
COLOR_CONVERSION_FAILED: (hex) => `⚠️ 色の変換に失敗しました: ${hex}`,
|
package/lib/font-manager.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import { REGEX, COMMENTS, IMPORTS, MESSAGES } from './constants.js';
|
|
8
|
+
import { REGEX, COMMENTS, IMPORTS, MESSAGES, GLOBALS_CSS_CANDIDATES } from './constants.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* sparkle-design.css からフォントの@import文を抽出する
|
|
@@ -58,8 +58,8 @@ function resolveNodeModulesRelPath(globalsPath) {
|
|
|
58
58
|
*/
|
|
59
59
|
function createSourceBlock(sourcePackages = [], nodeModulesRel = '../node_modules') {
|
|
60
60
|
const defaultPackage = 'sparkle-design';
|
|
61
|
-
const allPackages = [defaultPackage, ...sourcePackages.filter(p => p !== defaultPackage)];
|
|
62
|
-
const sourceLines = allPackages.map(pkg => `@source "${nodeModulesRel}/${pkg}/dist";`);
|
|
61
|
+
const allPackages = [defaultPackage, ...sourcePackages.filter((p) => p !== defaultPackage)];
|
|
62
|
+
const sourceLines = allPackages.map((pkg) => `@source "${nodeModulesRel}/${pkg}/dist";`);
|
|
63
63
|
return [COMMENTS.SOURCE_DIRECTIVE, ...sourceLines].join('\n');
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -126,8 +126,14 @@ function removeExistingImports(globalsContent) {
|
|
|
126
126
|
// 既存のフォントimportを削除
|
|
127
127
|
cleaned = cleaned.replace(REGEX.EXISTING_FONT_IMPORT_BLOCK, '');
|
|
128
128
|
|
|
129
|
-
//
|
|
130
|
-
|
|
129
|
+
// CLI が過去に書き込んだ「コメント + @source 連続行」ブロックのみを削除する。
|
|
130
|
+
// ユーザーが手書きした `@source "..."` は保持する(以前は SOURCE_DIRECTIVE
|
|
131
|
+
// を単体で全削除していたため、手書き @source が消える退行があった)。
|
|
132
|
+
// en: Remove only the managed comment + @source block that the CLI itself
|
|
133
|
+
// wrote in the past. User-authored @source lines must survive this step.
|
|
134
|
+
cleaned = cleaned.replace(REGEX.MANAGED_SOURCE_BLOCK, '');
|
|
135
|
+
// 念のため CLI コメント単独で孤立している残骸も掃除。
|
|
136
|
+
// en: Sweep up any orphan comment lines left over from historical writes.
|
|
131
137
|
cleaned = cleaned.replace(REGEX.SOURCE_COMMENT, '');
|
|
132
138
|
|
|
133
139
|
// 既存のsparkle-design.css importを削除
|
|
@@ -174,28 +180,46 @@ function reconstructGlobalsCss(globalsContent, sparkleImportBlock, tailwindInfo)
|
|
|
174
180
|
const afterTailwind = globalsContent.substring(tailwindInfo.afterIndex);
|
|
175
181
|
|
|
176
182
|
// Tailwind + sparkle-design.css + 残りのコンテンツ(フォント @import なし)
|
|
177
|
-
return (
|
|
178
|
-
beforeTailwind +
|
|
179
|
-
tailwindInfo.match +
|
|
180
|
-
sparkleImportBlock +
|
|
181
|
-
afterTailwind.trimStart()
|
|
182
|
-
);
|
|
183
|
+
return beforeTailwind + tailwindInfo.match + sparkleImportBlock + afterTailwind.trimStart();
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
/**
|
|
186
187
|
* globals.css を構造化して管理する
|
|
187
|
-
* - フォントimport
|
|
188
|
+
* - フォントimportを先頭に配置(v2.0.0 以降は SparkleHead.tsx に移行したため空配列が渡る)
|
|
188
189
|
* - Tailwind importを確保
|
|
189
190
|
* - sparkle-design.css importをTailwindの後に配置
|
|
191
|
+
* - 自動検出した sourcePackages から @source ディレクティブを挿入
|
|
192
|
+
*
|
|
193
|
+
* **v2.0.7-beta.2**: 戻り値を真偽値から `{ status, reason }` の状態付きに変更。
|
|
194
|
+
* 呼び出し側が「作業不要でスキップ」「失敗したがデフォルトは warn 継続」を
|
|
195
|
+
* 区別できるようにし、`--strict` モードで失敗を exit code に反映できるよう
|
|
196
|
+
* にした(#31)。`skipped` は後方互換のため `false` に、`updated` は `true`
|
|
197
|
+
* に toBoolean で扱える想定。
|
|
198
|
+
*
|
|
190
199
|
* @param {Array<string>} fontImports フォントimport文の配列
|
|
191
200
|
* @param {string} globalsPath globals.cssのパス
|
|
192
201
|
* @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
|
|
193
202
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
194
|
-
* @returns {
|
|
203
|
+
* @returns {{ status: 'skipped' | 'updated' | 'failed', reason?: string }}
|
|
204
|
+
* skipped: 触る理由がない (fonts/sourcePackages/customCss すべて空)
|
|
205
|
+
* updated: globals.css を正常に書き換えた
|
|
206
|
+
* failed: 書き換え対象だったが失敗した (TAILWIND_IMPORT 欠落、write 失敗等)
|
|
195
207
|
*/
|
|
196
|
-
export function updateGlobalsWithFonts(
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
export function updateGlobalsWithFonts(
|
|
209
|
+
fontImports,
|
|
210
|
+
globalsPath,
|
|
211
|
+
sourcePackages = null,
|
|
212
|
+
customCssPath = null
|
|
213
|
+
) {
|
|
214
|
+
const hasFonts = fontImports.length > 0;
|
|
215
|
+
const hasSourcePackages = sourcePackages !== null && sourcePackages !== undefined;
|
|
216
|
+
const hasCustomCss = Boolean(customCssPath);
|
|
217
|
+
|
|
218
|
+
// フォント import も @source も custom-css もないなら globals.css に触る
|
|
219
|
+
// 理由がないので skip。
|
|
220
|
+
// en: Nothing to inject, so leave globals.css alone.
|
|
221
|
+
if (!hasFonts && !hasSourcePackages && !hasCustomCss) {
|
|
222
|
+
return { status: 'skipped', reason: 'no-work' };
|
|
199
223
|
}
|
|
200
224
|
|
|
201
225
|
try {
|
|
@@ -208,8 +232,12 @@ export function updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages
|
|
|
208
232
|
// 3. Tailwind import の位置を見つける
|
|
209
233
|
const tailwindInfo = findTailwindImport(globalsContent);
|
|
210
234
|
if (!tailwindInfo) {
|
|
211
|
-
|
|
212
|
-
|
|
235
|
+
// 表示はプロジェクト相対パスにする。絶対パスだと「どのファイルをいじるのか」
|
|
236
|
+
// が一目で分かりにくく、リポジトリ間で絶対パスが変わるので diff も読みにくい。
|
|
237
|
+
// en: Show the project-relative path so it's obvious which file to edit.
|
|
238
|
+
const displayPath = path.relative(process.cwd(), globalsPath) || globalsPath;
|
|
239
|
+
console.warn(MESSAGES.TAILWIND_NOT_FOUND(displayPath));
|
|
240
|
+
return { status: 'failed', reason: 'tailwind-import-missing' };
|
|
213
241
|
}
|
|
214
242
|
|
|
215
243
|
// 4. importブロックを生成(フォント @import は SparkleHead に移行したため生成しない)
|
|
@@ -225,14 +253,45 @@ export function updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages
|
|
|
225
253
|
// 6. 更新したglobals.cssを書き込む
|
|
226
254
|
fs.writeFileSync(globalsPath, reconstructedContent, 'utf8');
|
|
227
255
|
console.log(MESSAGES.GLOBALS_UPDATED(globalsPath));
|
|
228
|
-
return
|
|
256
|
+
return { status: 'updated' };
|
|
229
257
|
} catch (error) {
|
|
230
258
|
console.error(MESSAGES.GLOBALS_UPDATE_FAILED(error.message));
|
|
231
|
-
|
|
232
|
-
return false;
|
|
259
|
+
return { status: 'failed', reason: `write-error: ${error.code ?? error.message}` };
|
|
233
260
|
}
|
|
234
261
|
}
|
|
235
262
|
|
|
263
|
+
// Vite プロジェクト判定用の config 候補。setup.js 側の `VITE_CONFIG_FILES` と
|
|
264
|
+
// 同じ対象を見る必要がある(判定ロジックが分かれると scaffold と generate で
|
|
265
|
+
// 挙動が食い違う)。
|
|
266
|
+
// en: Vite config candidates used for project detection. Must stay in sync with
|
|
267
|
+
// setup.js's VITE_CONFIG_FILES so scaffold and generate agree on the layout.
|
|
268
|
+
const VITE_CONFIG_FILES = [
|
|
269
|
+
'vite.config.ts',
|
|
270
|
+
'vite.config.js',
|
|
271
|
+
'vite.config.mjs',
|
|
272
|
+
'vite.config.cjs',
|
|
273
|
+
'vite.config.mts',
|
|
274
|
+
'vite.config.cts',
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
function isViteProject(cwd) {
|
|
278
|
+
return VITE_CONFIG_FILES.some((name) => fs.existsSync(path.join(cwd, name)));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Vite プロジェクトで候補をソートするときの優先度。setup の
|
|
282
|
+
// `defaultGlobalsCssTarget` に合わせて `src/index.css` を `src/globals.css`
|
|
283
|
+
// より手前に引き上げる。それ以外は元の順序(数字昇順)を保つ。
|
|
284
|
+
// en: When the project is Vite, promote `src/index.css` ahead of
|
|
285
|
+
// `src/globals.css` so that `generate` patches the same file `setup` scaffolds.
|
|
286
|
+
function vitePriority(candidate) {
|
|
287
|
+
if (candidate === 'src/index.css') return 0;
|
|
288
|
+
if (candidate === 'src/globals.css') return 1;
|
|
289
|
+
// それ以外の候補は元の GLOBALS_CSS_CANDIDATES 順序を保つため、index を
|
|
290
|
+
// オフセット付きで返す。
|
|
291
|
+
// en: Preserve the original order for everything else.
|
|
292
|
+
return 10 + GLOBALS_CSS_CANDIDATES.indexOf(candidate);
|
|
293
|
+
}
|
|
294
|
+
|
|
236
295
|
/**
|
|
237
296
|
* sparkle-design.css と同じディレクトリで Tailwind のエントリポイント CSS を自動検出する
|
|
238
297
|
* @param {string} dir 検索対象ディレクトリ
|
|
@@ -242,7 +301,16 @@ function detectTailwindEntrypoint(dir) {
|
|
|
242
301
|
let entries;
|
|
243
302
|
try {
|
|
244
303
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
245
|
-
} catch {
|
|
304
|
+
} catch (err) {
|
|
305
|
+
// ENOENT / ENOTDIR はよくある(dir 自体が無い / ファイルを渡された)ので
|
|
306
|
+
// 静かにスキップ。権限 (EACCES) などは debugging 価値があるので表に出す。
|
|
307
|
+
// en: ENOENT / ENOTDIR are expected (dir missing / path is a file). Surface
|
|
308
|
+
// permission-style failures so the user can debug.
|
|
309
|
+
if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
|
|
310
|
+
console.warn(
|
|
311
|
+
`⚠️ ${dir} の読み込みに失敗したため同階層 detection をスキップします (${err.code ?? err.message})`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
246
314
|
return null;
|
|
247
315
|
}
|
|
248
316
|
|
|
@@ -251,9 +319,20 @@ function detectTailwindEntrypoint(dir) {
|
|
|
251
319
|
if (entry.name === 'sparkle-design.css') continue;
|
|
252
320
|
|
|
253
321
|
const filePath = path.join(dir, entry.name);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
322
|
+
try {
|
|
323
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
324
|
+
if (REGEX.TAILWIND_IMPORT.test(content)) {
|
|
325
|
+
return filePath;
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
// 1 候補が読めなくても他の候補で続行できるようログを出してスキップする。
|
|
329
|
+
// 以前はここで throw させて外側の catch に落としていたため、同階層に
|
|
330
|
+
// 壊れた CSS が 1 つあると全部の detection が死んでいた。
|
|
331
|
+
// en: Keep scanning even if a single candidate file fails to read —
|
|
332
|
+
// previously a single unreadable CSS poisoned the whole detection.
|
|
333
|
+
console.warn(
|
|
334
|
+
`⚠️ ${entry.name} の読み込みに失敗したためスキップします (${err.code ?? err.message})`
|
|
335
|
+
);
|
|
257
336
|
}
|
|
258
337
|
}
|
|
259
338
|
|
|
@@ -262,10 +341,16 @@ function detectTailwindEntrypoint(dir) {
|
|
|
262
341
|
|
|
263
342
|
/**
|
|
264
343
|
* globals パスを解決する
|
|
265
|
-
* 優先順位: 明示的指定 >
|
|
344
|
+
* 優先順位: 明示的指定 > sparkle-design.css 同階層で自動検出 > プロジェクト root の既知候補 > デフォルト(globals.css)
|
|
345
|
+
*
|
|
346
|
+
* **v2.0.7-beta.1**: Vite プロジェクトのように `sparkle-design.css` を `src/app/`
|
|
347
|
+
* に、entry CSS を `src/index.css` に置くレイアウトで `@source` が挿入されない
|
|
348
|
+
* 不具合があったため、同階層で見つからない場合はプロジェクト root からも
|
|
349
|
+
* 既知の候補を探すように拡張した(setup が scaffold に使う候補と同一)。
|
|
350
|
+
*
|
|
266
351
|
* @param {string} sparkleDesignPath sparkle-design.css のパス
|
|
267
352
|
* @param {string|null} explicitGlobalsPath 明示的に指定された globals パス
|
|
268
|
-
* @returns {{ path: string, source: 'explicit' | 'detected' | 'default' } | null}
|
|
353
|
+
* @returns {{ path: string, source: 'explicit' | 'detected' | 'project' | 'default' } | null}
|
|
269
354
|
*/
|
|
270
355
|
function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
271
356
|
const dir = path.dirname(sparkleDesignPath);
|
|
@@ -275,11 +360,23 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
275
360
|
if (fs.existsSync(resolved)) {
|
|
276
361
|
return { path: resolved, source: 'explicit' };
|
|
277
362
|
}
|
|
278
|
-
|
|
279
|
-
|
|
363
|
+
// 明示指定は settings bug / typo なので silent warn ではなく throw に
|
|
364
|
+
// 昇格させる。--strict の有無に関わらず呼び出し元で伝播させたいので、
|
|
365
|
+
// error.code を付けて上流で識別できるようにする。
|
|
366
|
+
// en: An explicit --globals-path value that doesn't exist is almost
|
|
367
|
+
// always a typo. We tag the error so manageFontImports's catch knows
|
|
368
|
+
// to re-throw regardless of strict mode.
|
|
369
|
+
const err = new Error(
|
|
370
|
+
`指定された globals パスが見つかりません: ${explicitGlobalsPath} (cwd 基準で解決: ${resolved})`
|
|
371
|
+
);
|
|
372
|
+
err.code = 'E_EXPLICIT_GLOBALS_PATH_NOT_FOUND';
|
|
373
|
+
throw err;
|
|
280
374
|
}
|
|
281
375
|
|
|
282
|
-
// 自動検出: @import "tailwindcss" を含む
|
|
376
|
+
// 1. 自動検出: sparkle-design.css と同じディレクトリで @import "tailwindcss" を含む
|
|
377
|
+
// CSS ファイルを探す。Next.js App Router のように両者が同階層に居るケース用。
|
|
378
|
+
// en: Look next to sparkle-design.css for a Tailwind entry CSS. Covers the
|
|
379
|
+
// Next.js App Router layout where both live under `src/app/`.
|
|
283
380
|
const detected = detectTailwindEntrypoint(dir);
|
|
284
381
|
if (detected) {
|
|
285
382
|
const basename = path.basename(detected);
|
|
@@ -289,7 +386,43 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
289
386
|
return { path: detected, source: 'detected' };
|
|
290
387
|
}
|
|
291
388
|
|
|
292
|
-
//
|
|
389
|
+
// 2. プロジェクト root から既知の候補を探す。Vite のように sparkle-design.css
|
|
390
|
+
// が src/app/ に、entry CSS が src/index.css にあるレイアウトに対応。
|
|
391
|
+
// Vite プロジェクトでは `setup` の scaffold が `src/index.css` を選ぶので、
|
|
392
|
+
// `src/globals.css` と両方存在していても index.css を優先する(scaffold
|
|
393
|
+
// した場所と generate が patch する場所が食い違わないように)。
|
|
394
|
+
// en: Fall back to project-wide candidates. For Vite projects, promote
|
|
395
|
+
// `src/index.css` above `src/globals.css` so that `generate` patches the same
|
|
396
|
+
// file `setup` would have scaffolded.
|
|
397
|
+
const cwd = process.cwd();
|
|
398
|
+
const candidates = isViteProject(cwd)
|
|
399
|
+
? GLOBALS_CSS_CANDIDATES.slice().sort((a, b) => vitePriority(a) - vitePriority(b))
|
|
400
|
+
: GLOBALS_CSS_CANDIDATES;
|
|
401
|
+
for (const candidate of candidates) {
|
|
402
|
+
const absolute = path.resolve(cwd, candidate);
|
|
403
|
+
if (!fs.existsSync(absolute)) continue;
|
|
404
|
+
// 自身(sparkle-design.css)は除外
|
|
405
|
+
// en: Skip sparkle-design.css itself just in case a candidate points at it.
|
|
406
|
+
if (path.resolve(absolute) === path.resolve(sparkleDesignPath)) continue;
|
|
407
|
+
try {
|
|
408
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
409
|
+
if (REGEX.TAILWIND_IMPORT.test(content)) {
|
|
410
|
+
console.log(`📝 Tailwind エントリポイントを検出しました(project root): ${candidate}`);
|
|
411
|
+
return { path: absolute, source: 'project' };
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
// ENOENT 系は existsSync で既に弾いているので、ここに来るのは権限不足や
|
|
415
|
+
// ディレクトリ衝突などデバッグ価値のあるケース。silent に消さずに理由を出す。
|
|
416
|
+
// en: ENOENT is already filtered by existsSync above, so surfacing the
|
|
417
|
+
// error code here catches permission / type issues worth debugging.
|
|
418
|
+
console.warn(
|
|
419
|
+
`⚠️ 候補 ${candidate} の読み込みに失敗したためスキップします (${err.code ?? err.message})`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 3. デフォルト: sparkle-design.css と同じディレクトリの globals.css
|
|
425
|
+
// en: Last resort — a globals.css next to sparkle-design.css.
|
|
293
426
|
const defaultPath = path.join(dir, 'globals.css');
|
|
294
427
|
if (fs.existsSync(defaultPath)) {
|
|
295
428
|
return { path: defaultPath, source: 'default' };
|
|
@@ -301,19 +434,54 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
301
434
|
/**
|
|
302
435
|
* フォント管理の自動処理を実行する
|
|
303
436
|
* sparkle-design.css からフォントimportを抽出し、Tailwind エントリポイント CSS に @source 等を挿入する
|
|
437
|
+
*
|
|
438
|
+
* **v2.0.7-beta.2**: 戻り値を `{ status, reason? }` に変更し、呼び出し側が
|
|
439
|
+
* 「作業不要のスキップ」「作業すべき状態だが失敗」を区別できるようにした
|
|
440
|
+
* (#31)。`options.strict` が true のとき、`status === 'failed'` になる状態は
|
|
441
|
+
* throw に昇格させて `bin/sparkle-design.js` の outer catch で exit 1 に
|
|
442
|
+
* 繋げる。
|
|
443
|
+
*
|
|
304
444
|
* @param {string} sparkleDesignPath sparkle-design.cssのパス
|
|
305
445
|
* @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
|
|
306
446
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
307
447
|
* @param {string|null} globalsPathOverride 明示的に指定された globals パス
|
|
448
|
+
* @param {{ strict?: boolean }} [options] strict=true のとき失敗を throw する
|
|
449
|
+
* @returns {{ status: 'skipped' | 'updated' | 'failed', reason?: string }}
|
|
308
450
|
*/
|
|
309
|
-
export function manageFontImports(
|
|
451
|
+
export function manageFontImports(
|
|
452
|
+
sparkleDesignPath,
|
|
453
|
+
sourcePackages = null,
|
|
454
|
+
customCssPath = null,
|
|
455
|
+
globalsPathOverride = null,
|
|
456
|
+
options = {}
|
|
457
|
+
) {
|
|
458
|
+
const strict = Boolean(options.strict);
|
|
459
|
+
const hasWork =
|
|
460
|
+
(sourcePackages !== null && sourcePackages !== undefined) || Boolean(customCssPath);
|
|
461
|
+
|
|
462
|
+
const raiseOrReturn = (result) => {
|
|
463
|
+
if (strict && result.status === 'failed') {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`globals.css の更新に失敗しました (${result.reason ?? 'unknown'})。--strict モードでは exit 1 で終了します。`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return result;
|
|
469
|
+
};
|
|
470
|
+
|
|
310
471
|
try {
|
|
311
472
|
// 1. globals パスを解決
|
|
312
473
|
const resolved = resolveGlobalsPath(sparkleDesignPath, globalsPathOverride);
|
|
313
474
|
|
|
314
475
|
if (!resolved) {
|
|
476
|
+
// 作業すべき状態(sourcePackages 等あり)で entry CSS が見つからないのは
|
|
477
|
+
// 事実上の不具合。strict なら throw、非 strict なら従来どおり情報ログ。
|
|
478
|
+
// en: With work queued, missing entry CSS is effectively a misconfiguration.
|
|
479
|
+
if (hasWork) {
|
|
480
|
+
console.warn(MESSAGES.GLOBALS_NOT_FOUND);
|
|
481
|
+
return raiseOrReturn({ status: 'failed', reason: 'entry-css-not-found' });
|
|
482
|
+
}
|
|
315
483
|
console.log(MESSAGES.GLOBALS_NOT_FOUND);
|
|
316
|
-
return;
|
|
484
|
+
return { status: 'skipped', reason: 'entry-css-not-found-no-work' };
|
|
317
485
|
}
|
|
318
486
|
|
|
319
487
|
const globalsPath = resolved.path;
|
|
@@ -322,27 +490,45 @@ export function manageFontImports(sparkleDesignPath, sourcePackages = null, cust
|
|
|
322
490
|
const sparkleContent = fs.readFileSync(sparkleDesignPath, 'utf8');
|
|
323
491
|
const fontImports = extractFontImports(sparkleContent);
|
|
324
492
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
493
|
+
// v2.0.0 以降はフォント @import が SparkleHead.tsx に移行したため、
|
|
494
|
+
// sparkle-design.css に font @import が残らないのが通常状態。それでも
|
|
495
|
+
// 既存 globals.css には `@source` / sparkle-design.css import /
|
|
496
|
+
// custom-css import を挿入する必要があるので、単に fonts が空だからと
|
|
497
|
+
// いって早期 return せず、下流の update 関数に判断を委ねる。
|
|
498
|
+
// en: Fonts moved to SparkleHead in v2.0.0, so "no font imports in
|
|
499
|
+
// sparkle-design.css" is now the normal case. Don't bail out here —
|
|
500
|
+
// `updateGlobalsWithFonts` still needs to inject `@source` and the
|
|
501
|
+
// sparkle-design.css import into existing globals.css.
|
|
502
|
+
if (fontImports.length > 0) {
|
|
503
|
+
console.log(MESSAGES.FONT_DETECTED(fontImports.length));
|
|
328
504
|
}
|
|
329
505
|
|
|
330
|
-
|
|
506
|
+
// 4. globals.css に import / @source を追加
|
|
507
|
+
const result = updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages, customCssPath);
|
|
331
508
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
return;
|
|
509
|
+
if (result.status === 'skipped') {
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
if (result.status === 'failed') {
|
|
513
|
+
return raiseOrReturn(result);
|
|
338
514
|
}
|
|
339
515
|
|
|
340
|
-
// 5. sparkle-design.css
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
516
|
+
// 5. fonts が sparkle-design.css 側に残っていれば削除(globals 側に移動済みの前提)
|
|
517
|
+
// en: If there were fonts to move, strip them from sparkle-design.css.
|
|
518
|
+
if (fontImports.length > 0) {
|
|
519
|
+
const cleanedSparkleContent = removeFontImportsFromCSS(sparkleContent);
|
|
520
|
+
fs.writeFileSync(sparkleDesignPath, cleanedSparkleContent, 'utf8');
|
|
521
|
+
console.log(MESSAGES.FONT_REMOVED);
|
|
522
|
+
}
|
|
523
|
+
return result;
|
|
344
524
|
} catch (error) {
|
|
345
525
|
console.error(MESSAGES.FONT_MANAGEMENT_ERROR(error.message));
|
|
346
|
-
//
|
|
526
|
+
// strict モード、または明示指定された globals path の not-found は
|
|
527
|
+
// ユーザーが必ず気付くべきなので非 strict でも再 throw する。
|
|
528
|
+
// en: Always re-throw explicit --globals-path typos; respect strict for the rest.
|
|
529
|
+
if (strict || error.code === 'E_EXPLICIT_GLOBALS_PATH_NOT_FOUND') {
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
return { status: 'failed', reason: `manage-error: ${error.code ?? error.message}` };
|
|
347
533
|
}
|
|
348
534
|
}
|
package/lib/generate-css.js
CHANGED
|
@@ -68,7 +68,7 @@ function dedupeFontImports(cssContent) {
|
|
|
68
68
|
|
|
69
69
|
return cssContent
|
|
70
70
|
.split('\n')
|
|
71
|
-
.filter(line => {
|
|
71
|
+
.filter((line) => {
|
|
72
72
|
if (!line.includes('fonts.googleapis.com')) {
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
@@ -96,7 +96,7 @@ function normalizeFontsEntry(entry) {
|
|
|
96
96
|
return [{ family: entry, weights: FONT_DEFAULTS.WEIGHTS }];
|
|
97
97
|
}
|
|
98
98
|
if (Array.isArray(entry)) {
|
|
99
|
-
return entry.map(item => {
|
|
99
|
+
return entry.map((item) => {
|
|
100
100
|
if (typeof item === 'string') {
|
|
101
101
|
return { family: item, weights: FONT_DEFAULTS.WEIGHTS };
|
|
102
102
|
}
|
|
@@ -132,7 +132,7 @@ function resolveFontConfig(config) {
|
|
|
132
132
|
* @returns {string} CSS font-family 値(例: 'Montserrat', 'Noto Sans JP', sans-serif)
|
|
133
133
|
*/
|
|
134
134
|
function generateFontFamilyValue(fonts, genericFamily) {
|
|
135
|
-
const quoted = fonts.map(f => `'${f.family}'`);
|
|
135
|
+
const quoted = fonts.map((f) => `'${f.family}'`);
|
|
136
136
|
return [...quoted, genericFamily].join(', ');
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -169,9 +169,10 @@ function generateMergedFontImports(allFonts) {
|
|
|
169
169
|
* @returns {string} フォント import ブロック
|
|
170
170
|
*/
|
|
171
171
|
function generateFontImportsBlock(configOrResolved) {
|
|
172
|
-
const { pro, mono } =
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
const { pro, mono } =
|
|
173
|
+
configOrResolved.pro && configOrResolved.mono
|
|
174
|
+
? configOrResolved
|
|
175
|
+
: resolveFontConfig(configOrResolved);
|
|
175
176
|
|
|
176
177
|
const imports = [
|
|
177
178
|
FONT_DEFAULTS.MATERIAL_SYMBOLS_IMPORT,
|
|
@@ -206,7 +207,7 @@ function generateSparkleHeadContent(resolvedFonts) {
|
|
|
206
207
|
` <link rel="preconnect" href="${FONT_DOMAINS.GOOGLEAPIS}" />`,
|
|
207
208
|
` <link rel="preconnect" href="${FONT_DOMAINS.GSTATIC}" crossOrigin="anonymous" />`,
|
|
208
209
|
` <link rel="stylesheet" href="${materialSymbolsUrl}" />`,
|
|
209
|
-
...fontUrls.map(url => ` <link rel="stylesheet" href="${url}" />`),
|
|
210
|
+
...fontUrls.map((url) => ` <link rel="stylesheet" href="${url}" />`),
|
|
210
211
|
];
|
|
211
212
|
|
|
212
213
|
return `/**
|
|
@@ -289,7 +290,15 @@ function processTemplate(template, config, grayMapping, radiusMapping, colors) {
|
|
|
289
290
|
// 5. 基本的な設定値による置換(オブジェクト・配列・拡張フィールドはスキップ)
|
|
290
291
|
Object.entries(config).forEach(([key, value]) => {
|
|
291
292
|
// 配列・オブジェクト・拡張フィールドはスキップ
|
|
292
|
-
if (
|
|
293
|
+
if (
|
|
294
|
+
Array.isArray(value) ||
|
|
295
|
+
(typeof value === 'object' && value !== null) ||
|
|
296
|
+
key === 'custom-css' ||
|
|
297
|
+
key === 'fonts' ||
|
|
298
|
+
key === 'extend' ||
|
|
299
|
+
key === 'source-packages' ||
|
|
300
|
+
key === 'globals-path'
|
|
301
|
+
) {
|
|
293
302
|
return;
|
|
294
303
|
}
|
|
295
304
|
// 通常のプレースホルダー(CSS用 - スペースはそのまま)
|
|
@@ -336,9 +345,7 @@ function processTemplate(template, config, grayMapping, radiusMapping, colors) {
|
|
|
336
345
|
// `sparkle-design` は CLI 側のデフォルトパッケージなのでリストには加えず「存在判定」にだけ使う。
|
|
337
346
|
// en: Known design-system packages to auto-detect. `sparkle-design` is the CLI's
|
|
338
347
|
// default source package, so it only acts as an "enables @source" signal.
|
|
339
|
-
const KNOWN_DESIGN_SYSTEM_PACKAGES = [
|
|
340
|
-
'@goodpatch/sparkle-design-internal',
|
|
341
|
-
];
|
|
348
|
+
const KNOWN_DESIGN_SYSTEM_PACKAGES = ['@goodpatch/sparkle-design-internal'];
|
|
342
349
|
|
|
343
350
|
/**
|
|
344
351
|
* package.json の dependencies / devDependencies から既知のデザインシステムパッケージを検出する。
|
|
@@ -394,8 +401,17 @@ function writeCSS(cssContent, outputPath = null) {
|
|
|
394
401
|
* メイン処理
|
|
395
402
|
* @param {string|null} configPath カスタム設定ファイルのパス(オプション)
|
|
396
403
|
* @param {string|null} outputPath カスタム出力パス(オプション)
|
|
404
|
+
* @param {string|null} globalsPath 明示指定の globals.css パス(オプション)
|
|
405
|
+
* @param {{ strict?: boolean }} [options] strict=true のとき、
|
|
406
|
+
* globals.css パッチ失敗などを throw に昇格させる(exit 1 に繋げるため)
|
|
407
|
+
* @returns {{ globalsResult: { status: 'skipped'|'updated'|'failed', reason?: string } }}
|
|
397
408
|
*/
|
|
398
|
-
export function generateCSS(
|
|
409
|
+
export function generateCSS(
|
|
410
|
+
configPath = null,
|
|
411
|
+
outputPath = null,
|
|
412
|
+
globalsPath = null,
|
|
413
|
+
options = {}
|
|
414
|
+
) {
|
|
399
415
|
console.log(MESSAGES.START);
|
|
400
416
|
|
|
401
417
|
// 1. 設定ファイルを読み込み
|
|
@@ -413,7 +429,13 @@ export function generateCSS(configPath = null, outputPath = null, globalsPath =
|
|
|
413
429
|
const radiusMapping = loadRadiusMapping();
|
|
414
430
|
|
|
415
431
|
// 5. テンプレートを設定値で処理(resolvedFonts も返す)
|
|
416
|
-
const { css: processedCSS, resolvedFonts } = processTemplate(
|
|
432
|
+
const { css: processedCSS, resolvedFonts } = processTemplate(
|
|
433
|
+
template,
|
|
434
|
+
config,
|
|
435
|
+
grayMapping,
|
|
436
|
+
radiusMapping,
|
|
437
|
+
colors
|
|
438
|
+
);
|
|
417
439
|
|
|
418
440
|
// 6. CSSファイルを書き出し
|
|
419
441
|
const defaultOutputPath = path.resolve(
|
|
@@ -437,16 +459,23 @@ export function generateCSS(configPath = null, outputPath = null, globalsPath =
|
|
|
437
459
|
// with explicit config entries. If neither surfaces anything, @source is skipped.
|
|
438
460
|
console.log(MESSAGES.FONT_MANAGEMENT_START);
|
|
439
461
|
const detectedPackages = detectSourcePackagesFromPackageJson();
|
|
440
|
-
const explicitPackages = 'source-packages' in config ?
|
|
462
|
+
const explicitPackages = 'source-packages' in config ? config['source-packages'] || [] : null;
|
|
441
463
|
const sourcePackages =
|
|
442
464
|
detectedPackages !== null || explicitPackages !== null
|
|
443
465
|
? [...new Set([...(detectedPackages ?? []), ...(explicitPackages ?? [])])]
|
|
444
466
|
: null;
|
|
445
467
|
const customCssPath = config['custom-css'] || null;
|
|
446
468
|
const globalsPathOverride = globalsPath || config['globals-path'] || null;
|
|
447
|
-
|
|
469
|
+
const globalsResult = manageFontImports(
|
|
470
|
+
resolvedOutputPath,
|
|
471
|
+
sourcePackages,
|
|
472
|
+
customCssPath,
|
|
473
|
+
globalsPathOverride,
|
|
474
|
+
{ strict: Boolean(options.strict) }
|
|
475
|
+
);
|
|
448
476
|
|
|
449
477
|
console.log(MESSAGES.SUCCESS);
|
|
478
|
+
return { globalsResult };
|
|
450
479
|
}
|
|
451
480
|
|
|
452
481
|
// スクリプトが直接実行された場合のみメイン処理を実行
|
package/lib/setup.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { generateCSS } from './generate-css.js';
|
|
5
|
+
import { GLOBALS_CSS_CANDIDATES } from './constants.js';
|
|
5
6
|
|
|
6
7
|
// デフォルトの sparkle.config.json テンプレート
|
|
7
8
|
// en: Default sparkle.config.json template
|
|
@@ -41,16 +42,6 @@ const config = {
|
|
|
41
42
|
export default config;
|
|
42
43
|
`;
|
|
43
44
|
|
|
44
|
-
// globals.css 候補パス(優先順)
|
|
45
|
-
// en: globals.css candidate paths in priority order
|
|
46
|
-
const GLOBALS_CSS_CANDIDATES = [
|
|
47
|
-
'src/app/globals.css',
|
|
48
|
-
'app/globals.css',
|
|
49
|
-
'src/globals.css',
|
|
50
|
-
'src/index.css',
|
|
51
|
-
'src/styles/globals.css',
|
|
52
|
-
];
|
|
53
|
-
|
|
54
45
|
const ASSISTANT_CONFIG = {
|
|
55
46
|
claude: {
|
|
56
47
|
path: 'CLAUDE.md',
|
|
@@ -192,12 +183,13 @@ function buildInstructionBlock(target, assistant) {
|
|
|
192
183
|
BLOCK_START,
|
|
193
184
|
heading,
|
|
194
185
|
'',
|
|
186
|
+
'- **必ず読む**: Sparkle Design のコンポーネントを使う前に、インストール済みパッケージの型定義 `node_modules/<パッケージ名>/dist/components/ui/<コンポーネント名>/index.d.ts` を必ず読んでください。Prop 仕様・使用例・アンチパターンは JSDoc に書かれている(✅ / ❌ 例込み)ので、これが Source of Truth です。`node_modules/sparkle-design/dist/` や `node_modules/@goodpatch/sparkle-design-internal/dist/` を対象に、`index.d.ts` の JSDoc まで読み切ること。',
|
|
187
|
+
"- **Required reading**: Before using any Sparkle Design component, read the installed package's type definitions at `node_modules/<package>/dist/components/ui/<component>/index.d.ts` — prop specs, usage, and anti-patterns (with ✅ / ❌ examples) live in the JSDoc and are the source of truth. Target the installed package(s), e.g. `sparkle-design` and/or `@goodpatch/sparkle-design-internal`, and read the full JSDoc, not just the type signature.",
|
|
195
188
|
'- **必ず実行**: UI コンポーネントを作成・変更した後は、終了前に `lint:sparkle` を実行してください。Sparkle Design のアンチパターンを検出します。',
|
|
196
189
|
'- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
|
|
197
190
|
`- For AI review, prefer \`lint:sparkle:json\` or run \`npx --yes sparkle-design-cli check ${target} --format json\` directly.`,
|
|
198
191
|
'- JSON 出力の `findings` に加えて、**`manualReviewReminders` の内容も必ず 1 項目ずつ確認**してください。機械検出できない Badge/Tag の使い分けなどが含まれます。',
|
|
199
192
|
'- You must inspect every entry in both `findings` and `manualReviewReminders` — the reminders flag judgment calls (Badge vs Tag etc.) that the linter cannot detect.',
|
|
200
|
-
'- If semantic guidance is still needed, consult Sparkle Design docs and JSDoc examples.',
|
|
201
193
|
BLOCK_END,
|
|
202
194
|
].join('\n');
|
|
203
195
|
}
|
|
@@ -464,13 +456,31 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
464
456
|
};
|
|
465
457
|
}
|
|
466
458
|
|
|
467
|
-
function runGenerate({ skipGenerate, dryRun }) {
|
|
459
|
+
function runGenerate({ skipGenerate, dryRun, strict }) {
|
|
460
|
+
// --skip-generate / --dry-run と --strict が同時指定された場合、strict チェックを
|
|
461
|
+
// 走らせる対象 (generate) そのものがスキップされるため strict は事実上無意味。
|
|
462
|
+
// サイレントに無効化するとユーザーが CI で気付けないので警告を出す。
|
|
463
|
+
// en: --strict has no effect if generate is skipped — warn the user rather
|
|
464
|
+
// than silently no-op, which would mask CI misconfiguration.
|
|
465
|
+
if (strict && (skipGenerate || dryRun)) {
|
|
466
|
+
console.warn(
|
|
467
|
+
'⚠️ --strict は generate の失敗のみ検出します。--skip-generate / --dry-run と併用した場合は strict チェックは走りません。'
|
|
468
|
+
);
|
|
469
|
+
}
|
|
468
470
|
if (skipGenerate || dryRun) return { skipped: true, ran: false };
|
|
469
471
|
try {
|
|
470
472
|
console.log('🎨 sparkle-design.css を生成中...');
|
|
471
|
-
generateCSS();
|
|
472
|
-
return { skipped: false, ran: true };
|
|
473
|
+
const result = generateCSS(null, null, null, { strict: Boolean(strict) });
|
|
474
|
+
return { skipped: false, ran: true, globalsResult: result?.globalsResult };
|
|
473
475
|
} catch (error) {
|
|
476
|
+
// strict モード、または sparkle.config.json の `globals-path` typo などの
|
|
477
|
+
// 明示指定 not-found は、非 strict でもユーザーが必ず気付くべきなので
|
|
478
|
+
// 再 throw する(#33 の挙動を setup 経由でも保つ)。
|
|
479
|
+
// en: Always re-throw explicit --globals-path typos so `setup` surfaces
|
|
480
|
+
// them via exit code, matching the behaviour of `generate` directly.
|
|
481
|
+
if (strict || error.code === 'E_EXPLICIT_GLOBALS_PATH_NOT_FOUND') {
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
474
484
|
console.warn(`⚠️ generate 実行をスキップしました: ${error.message}`);
|
|
475
485
|
return { skipped: false, ran: false, error: error.message };
|
|
476
486
|
}
|
|
@@ -501,7 +511,11 @@ export function setupAssistant(options = {}) {
|
|
|
501
511
|
{ ...options, assistant, dryRun },
|
|
502
512
|
assistantConfig
|
|
503
513
|
);
|
|
504
|
-
const generate = runGenerate({
|
|
514
|
+
const generate = runGenerate({
|
|
515
|
+
skipGenerate: Boolean(options.skipGenerate),
|
|
516
|
+
dryRun,
|
|
517
|
+
strict: Boolean(options.strict),
|
|
518
|
+
});
|
|
505
519
|
|
|
506
520
|
const summary = {
|
|
507
521
|
assistant,
|
|
@@ -562,9 +576,11 @@ function printPostSetupReminder(target, packageManager) {
|
|
|
562
576
|
const lines = [
|
|
563
577
|
'',
|
|
564
578
|
'📝 次のステップ / Next steps:',
|
|
565
|
-
|
|
579
|
+
' 1. Sparkle Design のコンポーネントを使う前に、必ず `node_modules/<パッケージ名>/dist/components/ui/<コンポーネント名>/index.d.ts` の JSDoc を読んでください。Prop 仕様・使用例・アンチパターンは JSDoc が Source of Truth です。',
|
|
580
|
+
' Before using any Sparkle Design component, read `node_modules/<package>/dist/components/ui/<name>/index.d.ts` — the JSDoc includes prop specs, usage, and anti-pattern examples.',
|
|
581
|
+
` 2. UI コンポーネントを作成・変更したら、必ず \`${lintCmd}\` (または \`npx --yes sparkle-design-cli check ${lintTarget} --strict\`) を実行してください。`,
|
|
566
582
|
' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
|
|
567
|
-
'
|
|
583
|
+
' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
|
|
568
584
|
' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
|
|
569
585
|
'',
|
|
570
586
|
];
|