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 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 / Inter / JetBrains Mono / md)
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
- | 安定版 | `latest` | 本番運用向け。`npx --yes sparkle-design-cli` は常にこれを取得 | (デフォルト) |
41
- | Beta | `beta` | 品質保証中の検証用 | `npx --yes sparkle-design-cli@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(text-sm, font-medium 等)を使わない
146
- - CardTitle に typography を上書きしない
157
+ - Tailwind デフォルト typography を Sparkle Design コンポーネント内で使わない
158
+ - CardTitle に typography 系クラスを付与しない
147
159
  - CardControl に Button / IconButton 以外を入れない
148
- - CardHeader / CardContent の padding を上書きしない
149
- - asChild と prefixIcon / suffixIcon / isLoading を併用しない
150
- - disabled を isDisabled の代わりに使わない
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 を `<button>` / `<a>` / `role="button"` でラップせず、`ClickableCard` を使う
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"
@@ -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
- // 以前の 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
+ // 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 = ['next.config.js', 'next.config.ts', 'next.config.mjs', 'next.config.cjs'];
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: 'CSS にフォント @import が残っています。SparkleHead コンポーネントに移行してください。',
100
- recommendation: 'sparkle-design-cli generate を再実行すると、フォント読み込みが SparkleHead.tsx に移行され、globals.css から @import が除去されます。',
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
- { domain: 'fonts.googleapis.com', directive: 'style-src', pattern: new RegExp(FONT_DOMAINS.GOOGLEAPIS.replace('https://', '')) },
144
- { domain: 'fonts.gstatic.com', directive: 'font-src', pattern: new RegExp(FONT_DOMAINS.GSTATIC.replace('https://', '')) },
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: 'SparkleHead がソースコード内に見つかりませんでした。ルートレイアウトの <head> 内に <SparkleHead /> を配置してフォントの早期読み込みを有効にしてください。',
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 { RULES, collectFindings, createCheckReport, BASE_MANUAL_REVIEW_REMINDERS as MANUAL_REVIEW_REMINDERS };
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
- TAILWIND_NOT_FOUND: '⚠️ globals.css に Tailwind import が見つかりません。',
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}`,
@@ -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.1 以前の不具合**: 早期 return が `fontImports.length === 0` 単独で
194
- * 行われていたため、フォントを SparkleHead に移行済みかつ既存 globals.css が
195
- * ある(Vite の `src/index.css` など)プロジェクトでは `@source` が一切挿入
196
- * されなかった。条件を「フォント・sourcePackages・customCssPath のすべてが
197
- * 空なら skip」に変更し、パッケージの自動検出結果だけでも globals.css が
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 {boolean} globals.css の更新に成功した場合は true
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 false;
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
- console.warn(MESSAGES.TAILWIND_NOT_FOUND);
234
- return false;
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 true;
256
+ return { status: 'updated' };
251
257
  } catch (error) {
252
258
  console.error(MESSAGES.GLOBALS_UPDATE_FAILED(error.message));
253
- // globals.cssの更新は必須ではないので、エラーでも処理を続行
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
- console.warn(`⚠️ 指定された globals パスが見つかりません: ${explicitGlobalsPath}`);
359
- return null;
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 globalsUpdated = updateGlobalsWithFonts(
464
- fontImports,
465
- globalsPath,
466
- sourcePackages,
467
- customCssPath
468
- );
507
+ const result = updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages, customCssPath);
469
508
 
470
- if (!globalsUpdated) {
471
- // fonts が元々無く sourcePackages / customCss も空なら更新不要。
472
- // en: Nothing to do — not an error.
473
- return;
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
  }
@@ -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 } = configOrResolved.pro && configOrResolved.mono
173
- ? configOrResolved
174
- : resolveFontConfig(configOrResolved);
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 (Array.isArray(value) || (typeof value === 'object' && value !== null) || key === 'custom-css' || key === 'fonts' || key === 'extend' || key === 'source-packages' || key === 'globals-path') {
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(configPath = null, outputPath = null, globalsPath = null) {
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(template, config, grayMapping, radiusMapping, colors);
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 ? (config['source-packages'] || []) : null;
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
- manageFontImports(resolvedOutputPath, sourcePackages, customCssPath, globalsPathOverride);
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({ skipGenerate: Boolean(options.skipGenerate), dryRun });
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparkle-design-cli",
3
- "version": "2.0.7-beta.1",
3
+ "version": "2.0.7-beta.4",
4
4
  "description": "Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",