sparkle-design-cli 2.0.7-beta.1 → 2.0.7-beta.4
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 +30 -24
- package/bin/sparkle-design.js +12 -1
- package/lib/anti-pattern-rules.js +29 -15
- package/lib/check.js +49 -8
- package/lib/constants.js +5 -1
- package/lib/font-manager.js +75 -28
- package/lib/generate-css.js +44 -15
- package/lib/setup.js +26 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ setup は次を自動で行います:
|
|
|
15
15
|
1. パッケージマネージャー(pnpm / npm / yarn / bun)を自動検出
|
|
16
16
|
2. `sparkle-design` を dependencies、`tailwindcss` + `@tailwindcss/postcss` を devDependencies に追加
|
|
17
17
|
3. 未作成の場合のみ初期ファイルを生成:
|
|
18
|
-
- `sparkle.config.json`(デフォルトは blue /
|
|
18
|
+
- `sparkle.config.json`(デフォルトは blue / BIZ UDPGothic / BIZ UDGothic / md — sparkle-design 本体のデフォルトに合わせて統一)
|
|
19
19
|
- `postcss.config.mjs`
|
|
20
20
|
- Tailwind エントリ CSS — Next.js App Router なら `src/app/globals.css`、Vite なら `src/index.css`、それ以外は `src/globals.css`(プロジェクト構成から自動判定)
|
|
21
21
|
4. AI 指示ファイル(`CLAUDE.md` / `AGENTS.md` / Cursor rules)に Sparkle Design Guard ブロックを追加
|
|
@@ -35,10 +35,10 @@ npm install -g sparkle-design-cli
|
|
|
35
35
|
|
|
36
36
|
npm の dist-tag でチャネルを分離しています。通常は latest を使い、品質保証中の変更を試したい場合のみ beta を指定してください。
|
|
37
37
|
|
|
38
|
-
| チャネル | dist-tag | 用途
|
|
39
|
-
|
|
40
|
-
| 安定版
|
|
41
|
-
| Beta
|
|
38
|
+
| チャネル | dist-tag | 用途 | 指定方法 |
|
|
39
|
+
| -------- | -------- | ------------------------------------------------------------- | ----------------------------------- |
|
|
40
|
+
| 安定版 | `latest` | 本番運用向け。`npx --yes sparkle-design-cli` は常にこれを取得 | (デフォルト) |
|
|
41
|
+
| Beta | `beta` | 品質保証中の検証用 | `npx --yes sparkle-design-cli@beta` |
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
# beta で setup を試す
|
|
@@ -103,6 +103,12 @@ sparkle-design-cli generate --output ./styles/design.css
|
|
|
103
103
|
|
|
104
104
|
# 設定ファイルと出力先を両方指定
|
|
105
105
|
sparkle-design-cli generate -c ./config/custom.json -o ./dist/styles.css
|
|
106
|
+
|
|
107
|
+
# Tailwind エントリ CSS を明示指定
|
|
108
|
+
sparkle-design-cli generate --globals-path src/styles/app.css
|
|
109
|
+
|
|
110
|
+
# CI 向け: @source 注入や Tailwind import 欠落などの失敗を exit 1 にする
|
|
111
|
+
sparkle-design-cli generate --strict
|
|
106
112
|
```
|
|
107
113
|
|
|
108
114
|
#### generate オプション一覧
|
|
@@ -110,6 +116,12 @@ sparkle-design-cli generate -c ./config/custom.json -o ./dist/styles.css
|
|
|
110
116
|
- `-h, --help`: ヘルプメッセージを表示
|
|
111
117
|
- `-c, --config <パス>`: 設定ファイルのパス(デフォルト: `./sparkle.config.json`)
|
|
112
118
|
- `-o, --output <パス>`: 出力ファイルのパス(デフォルト: `./src/app/sparkle-design.css`)
|
|
119
|
+
- `--globals-path <パス>`: Tailwind エントリポイント CSS のパス(デフォルト: 自動検出)。
|
|
120
|
+
**指定したパスが存在しない場合は `--strict` の有無に関わらず常に exit 1**(`sparkle.config.json` の `extend.globals-path` も同様)
|
|
121
|
+
- `--strict`: 以下を warn ではなく **exit 1** に昇格させる(CI 向け。既定は warn + 継続で後方互換を維持):
|
|
122
|
+
- デザインシステムパッケージが `package.json` に入っているのに Tailwind エントリ CSS が見つからない
|
|
123
|
+
- エントリ CSS はあるが `@import "tailwindcss";` が書かれていない(警告メッセージに追記すべき行まで actionable に表示)
|
|
124
|
+
- エントリ CSS への書き込みに失敗した
|
|
113
125
|
|
|
114
126
|
### check: アンチパターン検査
|
|
115
127
|
|
|
@@ -142,24 +154,15 @@ sparkle-design-cli check --help
|
|
|
142
154
|
- children なしの Button に prefixIcon / suffixIcon を使わない
|
|
143
155
|
- Material Symbols を className 直書きで使わない
|
|
144
156
|
- shadcn/ui 既定 token を Sparkle Design 内へ持ち込まない
|
|
145
|
-
- Tailwind デフォルト typography
|
|
146
|
-
- CardTitle に typography
|
|
157
|
+
- Tailwind デフォルト typography を Sparkle Design コンポーネント内で使わない
|
|
158
|
+
- CardTitle に typography 系クラスを付与しない
|
|
147
159
|
- CardControl に Button / IconButton 以外を入れない
|
|
148
|
-
-
|
|
149
|
-
- asChild
|
|
150
|
-
-
|
|
151
|
-
- Button の prefixIcon に JSX を渡さない
|
|
160
|
+
- Card 系コンポーネントのデフォルト padding を安易に上書きしない
|
|
161
|
+
- asChild 使用時に prefixIcon / suffixIcon / isLoading を使わない
|
|
162
|
+
- Sparkle Design コンポーネントでは isDisabled を使う
|
|
163
|
+
- Button の prefixIcon / suffixIcon に JSX を渡さない
|
|
152
164
|
- Icon の children にテキストを渡さない
|
|
153
|
-
- クリック可能な Card
|
|
154
|
-
|
|
155
|
-
#### Manual Review Reminders
|
|
156
|
-
|
|
157
|
-
`--format json` では、機械検出できない観点も `manualReviewReminders` として返します。現在は次を含みます。
|
|
158
|
-
|
|
159
|
-
- Badge と Tag の意味的な使い分け
|
|
160
|
-
- CardDescription の typography / color token 明示
|
|
161
|
-
- Dialog と Modal の UX 上の使い分け
|
|
162
|
-
- 手書き span が Tag で置き換え可能か
|
|
165
|
+
- クリック可能な Card は ClickableCard を使う(button / a / role="button" でラップしない)
|
|
163
166
|
|
|
164
167
|
#### 導入先での推奨設定
|
|
165
168
|
|
|
@@ -198,6 +201,7 @@ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaf
|
|
|
198
201
|
```
|
|
199
202
|
|
|
200
203
|
既存ファイルは上書きされません:
|
|
204
|
+
|
|
201
205
|
- `sparkle.config.json` / `postcss.config.*` / Tailwind エントリ CSS が存在する場合はそのまま保持
|
|
202
206
|
- 既に依存関係に入っているパッケージはインストールをスキップ
|
|
203
207
|
- `package.json` の既存の `lint:sparkle*` 独自 script は保持(`--force-script-update` で上書き)
|
|
@@ -205,6 +209,7 @@ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaf
|
|
|
205
209
|
`--target` を省略した場合、lint 対象は `src`, `src/components`, `src/features`, `app`, `components` の順で自動検出されます。
|
|
206
210
|
|
|
207
211
|
利用できる assistant:
|
|
212
|
+
|
|
208
213
|
- `claude`: `CLAUDE.md`
|
|
209
214
|
- `codex`: `AGENTS.md`
|
|
210
215
|
- `cursor`: `.cursor/rules/sparkle-design-guard.mdc`
|
|
@@ -220,6 +225,9 @@ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaf
|
|
|
220
225
|
- `--skip-generate`: `generate` 実行をスキップ
|
|
221
226
|
- `--dry-run`: ファイルを変更せず結果だけ表示
|
|
222
227
|
- `--force-script-update`: 既存の `lint:sparkle` 系 script も上書き
|
|
228
|
+
- `--strict`: `generate` ステージの失敗を exit 1 に昇格(CI 向け)。
|
|
229
|
+
現状は `generate` の失敗のみ対象(install / scaffold / guard の失敗は従来どおり warn)。
|
|
230
|
+
`--skip-generate` / `--dry-run` と併用した場合は strict チェック対象が無くなるため警告を出します。
|
|
223
231
|
|
|
224
232
|
`setup` は通常実行時も JSON サマリーを stdout に表示します。`--dry-run` を付けると、その JSON を表示したままファイル変更だけを抑止します。
|
|
225
233
|
|
|
@@ -259,9 +267,7 @@ Figma プラグインが出力する基本4項目に加えて、プロジェク
|
|
|
259
267
|
{ "family": "Montserrat", "weights": [500, 600, 700] },
|
|
260
268
|
{ "family": "Noto Sans JP", "weights": [400, 500, 600, 700] }
|
|
261
269
|
],
|
|
262
|
-
"mono": [
|
|
263
|
-
{ "family": "Roboto Mono", "weights": [400, 700] }
|
|
264
|
-
]
|
|
270
|
+
"mono": [{ "family": "Roboto Mono", "weights": [400, 700] }]
|
|
265
271
|
},
|
|
266
272
|
"source-packages": ["@goodpatch/sparkle-design-internal"],
|
|
267
273
|
"custom-css": "./src/app/custom-tokens.css"
|
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
|
|
|
@@ -971,21 +971,35 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
971
971
|
description: 'CardControl に Button / IconButton 以外を入れない',
|
|
972
972
|
recommendation:
|
|
973
973
|
'CardControl はアクションボタン用です。ステータス表示には CardDescription を使ってください。',
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
//
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
//
|
|
981
|
-
// en:
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
|
|
988
|
-
/<CardControl\b
|
|
974
|
+
// 2-pass matcher。1 本の regex に入れ子 negative lookahead を押し込む代わりに、
|
|
975
|
+
// まず <CardControl>…</CardControl> ブロックを抽出し、次にブロック内部だけを
|
|
976
|
+
// スキャンして Button / IconButton / 入れ子 CardControl 以外の開始タグを見つける。
|
|
977
|
+
// 1. 入れ子 negative lookahead が無くなり読みやすい
|
|
978
|
+
// 2. 自己閉じ `<CardControl />` は 1st pass でそもそも拾わない(special case 不要)
|
|
979
|
+
// 3. O(総コンテンツ長) で評価が済むため、大規模 JSX でも backtracking で
|
|
980
|
+
// 処理時間が跳ねない
|
|
981
|
+
// en: Two-pass matcher. First pass extracts `<CardControl>…</CardControl>`
|
|
982
|
+
// blocks (self-closing variants are skipped because the opening-tag regex
|
|
983
|
+
// requires the previous char not to be `/`). Second pass scans the inner
|
|
984
|
+
// content for any element that is not Button / IconButton / nested
|
|
985
|
+
// CardControl. This replaces the nested tempered-lazy regex with a shape
|
|
986
|
+
// that is easier to reason about and stays linear in total content length.
|
|
987
|
+
match: (content) => {
|
|
988
|
+
const BLOCK_PATTERN = /<CardControl\b[^>]*(?<!\/)>([\s\S]*?)<\/CardControl\b[^>]*>/g;
|
|
989
|
+
const NON_BUTTON_CHILD = /<(?!Button\b|IconButton\b|CardControl\b)[A-Za-z][\w.]*/g;
|
|
990
|
+
const results = [];
|
|
991
|
+
for (const blockMatch of content.matchAll(BLOCK_PATTERN)) {
|
|
992
|
+
const openingEnd = blockMatch.index + blockMatch[0].indexOf('>') + 1;
|
|
993
|
+
const inner = blockMatch[1];
|
|
994
|
+
for (const childMatch of inner.matchAll(NON_BUTTON_CHILD)) {
|
|
995
|
+
results.push({
|
|
996
|
+
index: openingEnd + childMatch.index,
|
|
997
|
+
text: childMatch[0],
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return results;
|
|
1002
|
+
},
|
|
989
1003
|
},
|
|
990
1004
|
featureSection: lines([
|
|
991
1005
|
'### CardControl にはアクションボタンのみを入れる',
|
package/lib/check.js
CHANGED
|
@@ -12,7 +12,12 @@ const BASE_MANUAL_REVIEW_REMINDERS = getManualReviewReminders();
|
|
|
12
12
|
|
|
13
13
|
const SPARKLE_HEAD_JSX_PATTERN = /<SparkleHead\s*\/?\s*>/;
|
|
14
14
|
const CSP_PATTERN = /content-security-policy|contentSecurityPolicy|Content-Security-Policy/i;
|
|
15
|
-
const NEXT_CONFIG_CANDIDATES = [
|
|
15
|
+
const NEXT_CONFIG_CANDIDATES = [
|
|
16
|
+
'next.config.js',
|
|
17
|
+
'next.config.ts',
|
|
18
|
+
'next.config.mjs',
|
|
19
|
+
'next.config.cjs',
|
|
20
|
+
];
|
|
16
21
|
|
|
17
22
|
function toRelativeReportPath(filePath) {
|
|
18
23
|
return path.relative(process.cwd(), filePath).split(path.sep).join('/');
|
|
@@ -65,10 +70,30 @@ function formatMatch(match) {
|
|
|
65
70
|
return match[0].replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
function formatSnippet(text) {
|
|
74
|
+
return text.replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
function collectFindings(filePath, content) {
|
|
69
78
|
const findings = [];
|
|
70
79
|
|
|
71
80
|
for (const rule of RULES) {
|
|
81
|
+
if (typeof rule.match === 'function') {
|
|
82
|
+
// rule.match(content) -> Array<{ index: number, text: string }>
|
|
83
|
+
// 複雑な照合が必要なルール向けの opt-in API。2-pass 方式などで regex 単体の
|
|
84
|
+
// backtracking リスクを回避したいときに使う。index は content 内の絶対オフセット。
|
|
85
|
+
for (const hit of rule.match(content)) {
|
|
86
|
+
findings.push({
|
|
87
|
+
filePath,
|
|
88
|
+
id: rule.id,
|
|
89
|
+
description: rule.description,
|
|
90
|
+
recommendation: rule.recommendation,
|
|
91
|
+
line: getLineNumber(content, hit.index ?? 0),
|
|
92
|
+
snippet: formatSnippet(hit.text ?? ''),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
72
97
|
for (const match of content.matchAll(rule.pattern)) {
|
|
73
98
|
findings.push({
|
|
74
99
|
filePath,
|
|
@@ -96,8 +121,10 @@ function collectFontImportFindings(cssFiles) {
|
|
|
96
121
|
findings.push({
|
|
97
122
|
filePath,
|
|
98
123
|
id: 'font-import-in-css',
|
|
99
|
-
description:
|
|
100
|
-
|
|
124
|
+
description:
|
|
125
|
+
'CSS にフォント @import が残っています。SparkleHead コンポーネントに移行してください。',
|
|
126
|
+
recommendation:
|
|
127
|
+
'sparkle-design-cli generate を再実行すると、フォント読み込みが SparkleHead.tsx に移行され、globals.css から @import が除去されます。',
|
|
101
128
|
line: getLineNumber(content, match.index ?? 0),
|
|
102
129
|
snippet: match[0].slice(0, 120),
|
|
103
130
|
});
|
|
@@ -140,12 +167,20 @@ function collectNextjsCspFindings(cwd) {
|
|
|
140
167
|
if (!CSP_PATTERN.test(content)) continue;
|
|
141
168
|
|
|
142
169
|
const missingDomains = [
|
|
143
|
-
{
|
|
144
|
-
|
|
170
|
+
{
|
|
171
|
+
domain: 'fonts.googleapis.com',
|
|
172
|
+
directive: 'style-src',
|
|
173
|
+
pattern: new RegExp(FONT_DOMAINS.GOOGLEAPIS.replace('https://', '')),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
domain: 'fonts.gstatic.com',
|
|
177
|
+
directive: 'font-src',
|
|
178
|
+
pattern: new RegExp(FONT_DOMAINS.GSTATIC.replace('https://', '')),
|
|
179
|
+
},
|
|
145
180
|
].filter(({ pattern }) => !pattern.test(content));
|
|
146
181
|
|
|
147
182
|
if (missingDomains.length > 0) {
|
|
148
|
-
const missing = missingDomains.map(d => `${d.domain} (${d.directive})`).join(', ');
|
|
183
|
+
const missing = missingDomains.map((d) => `${d.domain} (${d.directive})`).join(', ');
|
|
149
184
|
findings.push({
|
|
150
185
|
filePath: configPath,
|
|
151
186
|
id: 'csp-font-block',
|
|
@@ -206,7 +241,8 @@ function createCheckReport(targets = []) {
|
|
|
206
241
|
if (!hasSparkleHeadUsage(fileContents)) {
|
|
207
242
|
manualReviewReminders.push({
|
|
208
243
|
id: 'sparkle-head-missing',
|
|
209
|
-
message:
|
|
244
|
+
message:
|
|
245
|
+
'SparkleHead がソースコード内に見つかりませんでした。ルートレイアウトの <head> 内に <SparkleHead /> を配置してフォントの早期読み込みを有効にしてください。',
|
|
210
246
|
});
|
|
211
247
|
}
|
|
212
248
|
|
|
@@ -277,4 +313,9 @@ export function checkProject(targets = [], options = {}) {
|
|
|
277
313
|
return report.findings.length > 0;
|
|
278
314
|
}
|
|
279
315
|
|
|
280
|
-
export {
|
|
316
|
+
export {
|
|
317
|
+
RULES,
|
|
318
|
+
collectFindings,
|
|
319
|
+
createCheckReport,
|
|
320
|
+
BASE_MANUAL_REVIEW_REMINDERS as MANUAL_REVIEW_REMINDERS,
|
|
321
|
+
};
|
package/lib/constants.js
CHANGED
|
@@ -170,7 +170,11 @@ export const MESSAGES = {
|
|
|
170
170
|
FONT_REMOVE_SKIPPED: 'ℹ️ globals.css の更新に失敗したため、フォントimport削除をスキップします。',
|
|
171
171
|
|
|
172
172
|
// 警告メッセージ
|
|
173
|
-
|
|
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";\` を追記してください。`,
|
|
174
178
|
GLOBALS_UPDATE_FAILED: (error) => `⚠️ globals.css の更新に失敗しました: ${error}`,
|
|
175
179
|
FONT_MANAGEMENT_ERROR: (error) => `⚠️ フォント管理処理でエラーが発生しました: ${error}`,
|
|
176
180
|
COLOR_CONVERSION_FAILED: (hex) => `⚠️ 色の変換に失敗しました: ${hex}`,
|
package/lib/font-manager.js
CHANGED
|
@@ -190,18 +190,20 @@ function reconstructGlobalsCss(globalsContent, sparkleImportBlock, tailwindInfo)
|
|
|
190
190
|
* - sparkle-design.css importをTailwindの後に配置
|
|
191
191
|
* - 自動検出した sourcePackages から @source ディレクティブを挿入
|
|
192
192
|
*
|
|
193
|
-
* **v2.0.7-beta.
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* patch されるようにした。
|
|
193
|
+
* **v2.0.7-beta.2**: 戻り値を真偽値から `{ status, reason }` の状態付きに変更。
|
|
194
|
+
* 呼び出し側が「作業不要でスキップ」「失敗したがデフォルトは warn 継続」を
|
|
195
|
+
* 区別できるようにし、`--strict` モードで失敗を exit code に反映できるよう
|
|
196
|
+
* にした(#31)。`skipped` は後方互換のため `false` に、`updated` は `true`
|
|
197
|
+
* に toBoolean で扱える想定。
|
|
199
198
|
*
|
|
200
199
|
* @param {Array<string>} fontImports フォントimport文の配列
|
|
201
200
|
* @param {string} globalsPath globals.cssのパス
|
|
202
201
|
* @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
|
|
203
202
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
204
|
-
* @returns {
|
|
203
|
+
* @returns {{ status: 'skipped' | 'updated' | 'failed', reason?: string }}
|
|
204
|
+
* skipped: 触る理由がない (fonts/sourcePackages/customCss すべて空)
|
|
205
|
+
* updated: globals.css を正常に書き換えた
|
|
206
|
+
* failed: 書き換え対象だったが失敗した (TAILWIND_IMPORT 欠落、write 失敗等)
|
|
205
207
|
*/
|
|
206
208
|
export function updateGlobalsWithFonts(
|
|
207
209
|
fontImports,
|
|
@@ -217,7 +219,7 @@ export function updateGlobalsWithFonts(
|
|
|
217
219
|
// 理由がないので skip。
|
|
218
220
|
// en: Nothing to inject, so leave globals.css alone.
|
|
219
221
|
if (!hasFonts && !hasSourcePackages && !hasCustomCss) {
|
|
220
|
-
return
|
|
222
|
+
return { status: 'skipped', reason: 'no-work' };
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
try {
|
|
@@ -230,8 +232,12 @@ export function updateGlobalsWithFonts(
|
|
|
230
232
|
// 3. Tailwind import の位置を見つける
|
|
231
233
|
const tailwindInfo = findTailwindImport(globalsContent);
|
|
232
234
|
if (!tailwindInfo) {
|
|
233
|
-
|
|
234
|
-
|
|
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' };
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
// 4. importブロックを生成(フォント @import は SparkleHead に移行したため生成しない)
|
|
@@ -247,11 +253,10 @@ export function updateGlobalsWithFonts(
|
|
|
247
253
|
// 6. 更新したglobals.cssを書き込む
|
|
248
254
|
fs.writeFileSync(globalsPath, reconstructedContent, 'utf8');
|
|
249
255
|
console.log(MESSAGES.GLOBALS_UPDATED(globalsPath));
|
|
250
|
-
return
|
|
256
|
+
return { status: 'updated' };
|
|
251
257
|
} catch (error) {
|
|
252
258
|
console.error(MESSAGES.GLOBALS_UPDATE_FAILED(error.message));
|
|
253
|
-
|
|
254
|
-
return false;
|
|
259
|
+
return { status: 'failed', reason: `write-error: ${error.code ?? error.message}` };
|
|
255
260
|
}
|
|
256
261
|
}
|
|
257
262
|
|
|
@@ -355,8 +360,17 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
355
360
|
if (fs.existsSync(resolved)) {
|
|
356
361
|
return { path: resolved, source: 'explicit' };
|
|
357
362
|
}
|
|
358
|
-
|
|
359
|
-
|
|
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;
|
|
360
374
|
}
|
|
361
375
|
|
|
362
376
|
// 1. 自動検出: sparkle-design.css と同じディレクトリで @import "tailwindcss" を含む
|
|
@@ -420,24 +434,54 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
420
434
|
/**
|
|
421
435
|
* フォント管理の自動処理を実行する
|
|
422
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
|
+
*
|
|
423
444
|
* @param {string} sparkleDesignPath sparkle-design.cssのパス
|
|
424
445
|
* @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
|
|
425
446
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
426
447
|
* @param {string|null} globalsPathOverride 明示的に指定された globals パス
|
|
448
|
+
* @param {{ strict?: boolean }} [options] strict=true のとき失敗を throw する
|
|
449
|
+
* @returns {{ status: 'skipped' | 'updated' | 'failed', reason?: string }}
|
|
427
450
|
*/
|
|
428
451
|
export function manageFontImports(
|
|
429
452
|
sparkleDesignPath,
|
|
430
453
|
sourcePackages = null,
|
|
431
454
|
customCssPath = null,
|
|
432
|
-
globalsPathOverride = null
|
|
455
|
+
globalsPathOverride = null,
|
|
456
|
+
options = {}
|
|
433
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
|
+
|
|
434
471
|
try {
|
|
435
472
|
// 1. globals パスを解決
|
|
436
473
|
const resolved = resolveGlobalsPath(sparkleDesignPath, globalsPathOverride);
|
|
437
474
|
|
|
438
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
|
+
}
|
|
439
483
|
console.log(MESSAGES.GLOBALS_NOT_FOUND);
|
|
440
|
-
return;
|
|
484
|
+
return { status: 'skipped', reason: 'entry-css-not-found-no-work' };
|
|
441
485
|
}
|
|
442
486
|
|
|
443
487
|
const globalsPath = resolved.path;
|
|
@@ -460,17 +504,13 @@ export function manageFontImports(
|
|
|
460
504
|
}
|
|
461
505
|
|
|
462
506
|
// 4. globals.css に import / @source を追加
|
|
463
|
-
const
|
|
464
|
-
fontImports,
|
|
465
|
-
globalsPath,
|
|
466
|
-
sourcePackages,
|
|
467
|
-
customCssPath
|
|
468
|
-
);
|
|
507
|
+
const result = updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages, customCssPath);
|
|
469
508
|
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
509
|
+
if (result.status === 'skipped') {
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
if (result.status === 'failed') {
|
|
513
|
+
return raiseOrReturn(result);
|
|
474
514
|
}
|
|
475
515
|
|
|
476
516
|
// 5. fonts が sparkle-design.css 側に残っていれば削除(globals 側に移動済みの前提)
|
|
@@ -480,8 +520,15 @@ export function manageFontImports(
|
|
|
480
520
|
fs.writeFileSync(sparkleDesignPath, cleanedSparkleContent, 'utf8');
|
|
481
521
|
console.log(MESSAGES.FONT_REMOVED);
|
|
482
522
|
}
|
|
523
|
+
return result;
|
|
483
524
|
} catch (error) {
|
|
484
525
|
console.error(MESSAGES.FONT_MANAGEMENT_ERROR(error.message));
|
|
485
|
-
//
|
|
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}` };
|
|
486
533
|
}
|
|
487
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
|
@@ -456,13 +456,31 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
456
456
|
};
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
-
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
|
+
}
|
|
460
470
|
if (skipGenerate || dryRun) return { skipped: true, ran: false };
|
|
461
471
|
try {
|
|
462
472
|
console.log('🎨 sparkle-design.css を生成中...');
|
|
463
|
-
generateCSS();
|
|
464
|
-
return { skipped: false, ran: true };
|
|
473
|
+
const result = generateCSS(null, null, null, { strict: Boolean(strict) });
|
|
474
|
+
return { skipped: false, ran: true, globalsResult: result?.globalsResult };
|
|
465
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
|
+
}
|
|
466
484
|
console.warn(`⚠️ generate 実行をスキップしました: ${error.message}`);
|
|
467
485
|
return { skipped: false, ran: false, error: error.message };
|
|
468
486
|
}
|
|
@@ -493,7 +511,11 @@ export function setupAssistant(options = {}) {
|
|
|
493
511
|
{ ...options, assistant, dryRun },
|
|
494
512
|
assistantConfig
|
|
495
513
|
);
|
|
496
|
-
const generate = runGenerate({
|
|
514
|
+
const generate = runGenerate({
|
|
515
|
+
skipGenerate: Boolean(options.skipGenerate),
|
|
516
|
+
dryRun,
|
|
517
|
+
strict: Boolean(options.strict),
|
|
518
|
+
});
|
|
497
519
|
|
|
498
520
|
const summary = {
|
|
499
521
|
assistant,
|