sparkle-design-cli 2.0.7-beta.0 → 2.0.7-beta.10

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,9 +225,117 @@ 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
 
234
+ ### 手動セットアップ(Next.js / Vite 以外のプロジェクト向け)
235
+
236
+ `setup` コマンドは **Next.js App Router** と **Vite** を前提に scaffold / entry CSS の配置を自動判定します。Remix / Astro / TanStack Start など他のフレームワークや、独自ディレクトリ構成のプロジェクトでは自動判定が外れるため、以下の手順で手動セットアップすることを推奨します。
237
+
238
+ #### 手順
239
+
240
+ **1. パッケージをインストール**
241
+
242
+ ```bash
243
+ # 本体
244
+ pnpm add sparkle-design # or npm install / yarn add / bun add
245
+
246
+ # Tailwind v4
247
+ pnpm add -D tailwindcss @tailwindcss/postcss
248
+ ```
249
+
250
+ **2. `sparkle.config.json` をプロジェクトルートに作成**
251
+
252
+ ```json
253
+ {
254
+ "primary": "blue",
255
+ "font-pro": "Inter",
256
+ "font-mono": "JetBrains Mono",
257
+ "radius": "md"
258
+ }
259
+ ```
260
+
261
+ 選択肢の詳細は本 README の「[設定オプション](#設定オプション)」を参照してください。
262
+
263
+ **3. `postcss.config.mjs` をプロジェクトルートに作成**
264
+
265
+ ```js
266
+ export default {
267
+ plugins: {
268
+ '@tailwindcss/postcss': {},
269
+ },
270
+ };
271
+ ```
272
+
273
+ **4. Tailwind エントリ CSS を自前で用意**
274
+
275
+ 既存プロジェクトに組み込む場合、`@import "tailwindcss";` を含む CSS ファイル(例: `src/styles/app.css`)があればそれを使います。無ければ作成してアプリケーションのエントリから import します。
276
+
277
+ **5. `generate` を実行**
278
+
279
+ ```bash
280
+ npx --yes sparkle-design-cli generate
281
+ ```
282
+
283
+ これで `sparkle-design.css` が `src/app/sparkle-design.css` に生成され、`SparkleHead.tsx` も同じ場所に出ます。エントリ CSS の検出に失敗する場合は `sparkle.config.json` の `extend.globals-path` に明示指定してください。
284
+
285
+ ```json
286
+ {
287
+ "primary": "blue",
288
+ "extend": {
289
+ "globals-path": "src/styles/app.css"
290
+ }
291
+ }
292
+ ```
293
+
294
+ **6. フォントの `<link>` タグを手動で配置**
295
+
296
+ Next.js / Vite 以外ではアプリケーションフレームワークごとに「`<head>` 相当」の配置方法が異なります。自動生成される `SparkleHead.tsx` はそのまま使えるはずですが、使えない場合は中身(`preconnect` + Google Fonts の `<link>` タグ群)を自前のレイアウトにコピーしてください。
297
+
298
+ たとえば Astro なら `src/layouts/BaseLayout.astro` の `<head>` に直接書きます:
299
+
300
+ ```html
301
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
302
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
303
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:FILL,wght@0..1,500&display=block" />
304
+ <!-- sparkle.config.json の font-pro / font-mono に合わせた Google Fonts URL -->
305
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" />
306
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" />
307
+ ```
308
+
309
+ **7. アンチパターン検査を package.json に追加(任意)**
310
+
311
+ ```json
312
+ {
313
+ "scripts": {
314
+ "lint:sparkle": "npx --yes sparkle-design-cli check src --strict",
315
+ "lint:sparkle:json": "npx --yes sparkle-design-cli check src --format json"
316
+ }
317
+ }
318
+ ```
319
+
320
+ **8. AI エージェントを使うなら Guard ブロックと hook を手動で配置**
321
+
322
+ AI ガード(`CLAUDE.md` / `AGENTS.md` / `.cursor/rules/*.mdc`)と hook 設定(`.claude/settings.json` / `.cursor/hooks.json` / `.codex/hooks.json`)は `setup` に任せるのが最も楽です。
323
+
324
+ ```bash
325
+ # ガード追加・hook 設定のみ(パッケージインストールや scaffold はスキップ)
326
+ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaffold --skip-generate
327
+ ```
328
+
329
+ この `--skip-*` 組み合わせは手動セットアップ済みのプロジェクトに対して Guard + hook だけ差し込むのに使えます。
330
+
331
+ #### 既知の未対応ケース
332
+
333
+ - **モノレポ(workspaces)**: `generate` は実行した cwd からパッケージマネージャーを遡及検出しないため、サブパッケージで直接実行するのが確実です。
334
+ - **CSS 以外のエントリ(CSS-in-JS / vanilla-extract 等)**: Sparkle Design は Tailwind v4 の `@source` + CSS variable 前提で作られているため、ビルドパイプラインの外で CSS を扱うソリューションは対象外です。
335
+ - **Tailwind v3**: `@source` ディレクティブ依存のため v4 必須です。
336
+
337
+ Next.js / Vite 以外で導入したい方で困ったときは Issue で教えていただけると助かります。
338
+
226
339
  ## 設定ファイル (sparkle.config.json)
227
340
 
228
341
  ### 設定ファイルの作成
@@ -259,9 +372,7 @@ Figma プラグインが出力する基本4項目に加えて、プロジェク
259
372
  { "family": "Montserrat", "weights": [500, 600, 700] },
260
373
  { "family": "Noto Sans JP", "weights": [400, 500, 600, 700] }
261
374
  ],
262
- "mono": [
263
- { "family": "Roboto Mono", "weights": [400, 700] }
264
- ]
375
+ "mono": [{ "family": "Roboto Mono", "weights": [400, 700] }]
265
376
  },
266
377
  "source-packages": ["@goodpatch/sparkle-design-internal"],
267
378
  "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
 
@@ -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: 'クリック可能な Card は ClickableCard を使う(button / a / role="button" でラップしない)',
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: /<CardTitle\b[^>]*\bclassName\s*=\s*(?:"[^"]*\bcharacter-[^"]*"|'[^']*\bcharacter-[^']*'|\{[^}]*\bcharacter-[^}]*\})[^>]*>/g,
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,35 @@ const ANTI_PATTERN_GROUPS = [
970
971
  description: 'CardControl に Button / IconButton 以外を入れない',
971
972
  recommendation:
972
973
  'CardControl はアクションボタン用です。ステータス表示には CardDescription を使ってください。',
973
- pattern: /<CardControl\b[^>]*>[\s\S]*?<(?!Button\b|IconButton\b|\/CardControl)\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
+ },
974
1003
  },
975
1004
  featureSection: lines([
976
1005
  '### CardControl にはアクションボタンのみを入れる',
@@ -1020,7 +1049,8 @@ const ANTI_PATTERN_GROUPS = [
1020
1049
  description: 'Card 系コンポーネントのデフォルト padding を安易に上書きしない',
1021
1050
  recommendation:
1022
1051
  'CardHeader / CardContent のデフォルト padding(px-6 py-2)をそのまま使ってください。上書きは本当に必要な場合のみ。',
1023
- pattern: /<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,
1052
+ pattern:
1053
+ /<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
1054
  },
1025
1055
  featureSection: lines([
1026
1056
  '### Card 系コンポーネントの padding を上書きしない',
@@ -1045,7 +1075,8 @@ const ANTI_PATTERN_GROUPS = [
1045
1075
  description: 'asChild 使用時に prefixIcon / suffixIcon / isLoading を使わない',
1046
1076
  recommendation:
1047
1077
  'asChild モードでは prefixIcon / suffixIcon / isLoading は無視されます。アイコン付きの Link が必要なら asChild を外してください。',
1048
- pattern: /<Button\b(?=[^>]*\basChild\b)(?=[^>]*\b(?:prefixIcon|suffixIcon|isLoading)\b)[^>]*>/g,
1078
+ pattern:
1079
+ /<Button\b(?=[^>]*\basChild\b)(?=[^>]*\b(?:prefixIcon|suffixIcon|isLoading)\b)[^>]*>/g,
1049
1080
  },
1050
1081
  featureSection: lines([
1051
1082
  '### asChild と prefixIcon / suffixIcon / isLoading を併用しない',
@@ -1133,7 +1164,21 @@ const ANTI_PATTERN_GROUPS = [
1133
1164
  ];
1134
1165
 
1135
1166
  function getCheckRules() {
1136
- const order = ['dialog-form', 'dialog-button-wrap', 'button-icon-only', 'material-symbols-direct', 'shadcn-token', 'tailwind-typography', 'card-title-typography', 'card-control-non-button', 'card-padding-override', 'aschild-with-icon-props', 'disabled-vs-is-disabled', 'button-prefixicon-jsx', 'icon-children-text'];
1167
+ const order = [
1168
+ 'dialog-form',
1169
+ 'dialog-button-wrap',
1170
+ 'button-icon-only',
1171
+ 'material-symbols-direct',
1172
+ 'shadcn-token',
1173
+ 'tailwind-typography',
1174
+ 'card-title-typography',
1175
+ 'card-control-non-button',
1176
+ 'card-padding-override',
1177
+ 'aschild-with-icon-props',
1178
+ 'disabled-vs-is-disabled',
1179
+ 'button-prefixicon-jsx',
1180
+ 'icon-children-text',
1181
+ ];
1137
1182
 
1138
1183
  return ANTI_PATTERN_GROUPS.filter((group) => group.check)
1139
1184
  .map((group) => ({
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
 
@@ -232,9 +268,37 @@ function printTextReport(report, options = {}) {
232
268
  }
233
269
  }
234
270
 
235
- console.log('Manual review reminders:');
236
- for (const reminder of report.manualReviewReminders) {
237
- console.log(`- [${reminder.id}] ${reminder.message}`);
271
+ const reminders = report.manualReviewReminders ?? [];
272
+ if (reminders.length === 0) {
273
+ // 0 件のときは従来通り静かに済ます。
274
+ // en: Nothing to review — keep quiet.
275
+ } else {
276
+ // Guard block から参照される acknowledgment フォーマットと合わせるため、
277
+ // AI 向けに「各 ID を response に echo して確認したことを示してほしい」
278
+ // と明示的に要求する。hook による exit blocking はしない(判断事項のため)。
279
+ // en: Ask the AI to echo each reminder ID in its final response, so humans
280
+ // can see which reminders were considered. This is not enforced by hook
281
+ // exit code — reminders are judgment calls, not hard failures.
282
+ console.log('');
283
+ console.log('=== Manual review reminders (must acknowledge each ID in your response) ===');
284
+ console.log(
285
+ 'These are judgment calls the linter cannot detect. For every item below,'
286
+ );
287
+ console.log(
288
+ 'explicitly state the reminder ID in your reply together with whether the'
289
+ );
290
+ console.log(
291
+ 'current code already satisfies it, or what change is needed. Silence = skipped.'
292
+ );
293
+ console.log('');
294
+ for (const reminder of reminders) {
295
+ console.log(`- [${reminder.id}] ${reminder.message}`);
296
+ }
297
+ console.log('');
298
+ console.log(
299
+ 'Example acknowledgment: "[badge-tag-semantics] reviewed — Badge used only for counts (OK). [dialog-modal-ux] reviewed — changed Dialog to Modal for form case."'
300
+ );
301
+ console.log('=========================================================================');
238
302
  }
239
303
 
240
304
  if (options.strict && report.findings.length > 0) {
@@ -245,6 +309,7 @@ function printTextReport(report, options = {}) {
245
309
  function printJsonReport(report, options = {}) {
246
310
  const strictMode = Boolean(options.strict);
247
311
  const passed = !(strictMode && report.findings.length > 0);
312
+ const reminders = report.manualReviewReminders ?? [];
248
313
 
249
314
  console.log(
250
315
  JSON.stringify(
@@ -253,6 +318,18 @@ function printJsonReport(report, options = {}) {
253
318
  targetCount: report.targets.length,
254
319
  checkedFileCount: report.checkedFiles.length,
255
320
  findingCount: report.findings.length,
321
+ reminderCount: reminders.length,
322
+ // AI 向けの attention フラグ。reminder が 1 件でもあれば true になり、
323
+ // AI は最終 response で各 reminder ID を echo する必要がある。
324
+ // exit code には寄与しない(判断事項を hook で block するのは過剰)。
325
+ // en: AI attention flag. When reminders exist, the AI MUST echo each
326
+ // reminder ID in its final reply. Not tied to exit code — reminders
327
+ // are judgment calls, not hard errors.
328
+ manualReviewRequired: reminders.length > 0,
329
+ reminderAcknowledgmentFormat:
330
+ reminders.length > 0
331
+ ? 'Respond with each reminder ID followed by your review, e.g. `[badge-tag-semantics] reviewed — Badge is used only for counts (OK)`.'
332
+ : null,
256
333
  strictMode,
257
334
  passed,
258
335
  exitCode: passed ? 0 : 1,
@@ -277,4 +354,9 @@ export function checkProject(targets = [], options = {}) {
277
354
  return report.findings.length > 0;
278
355
  }
279
356
 
280
- export { RULES, collectFindings, createCheckReport, BASE_MANUAL_REVIEW_REMINDERS as MANUAL_REVIEW_REMINDERS };
357
+ export {
358
+ RULES,
359
+ collectFindings,
360
+ createCheckReport,
361
+ BASE_MANUAL_REVIEW_REMINDERS as MANUAL_REVIEW_REMINDERS,
362
+ };
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
  // フォント関連
@@ -29,13 +44,30 @@ export const REGEX = {
29
44
  /\/\*\s*フォントのインポート[^*]*\*\/\s*\n?(?:@import\s+(?:url\([^)]+fonts\.googleapis\.com[^)]+\)|['"][^"']*fonts\.googleapis\.com[^"']*['"]);?\s*\n?)*\n?/g,
30
45
 
31
46
  // Sparkle Design関連
47
+ // `@import "sparkle-design.css"` / `@import "./sparkle-design.css"` だけでなく
48
+ // `@import "./app/sparkle-design.css"` 等、任意の相対パスを含む variant も
49
+ // 除去対象にする。Vite のように entry CSS (src/index.css) と sparkle-design.css
50
+ // (src/app/sparkle-design.css) が別ディレクトリに置かれるレイアウトでは
51
+ // 後者のような path になるので、同一 dir 前提の regex だと AI が手で直したもの
52
+ // を除去できず二重 import が残ってしまう。
53
+ // en: Match any relative path ending in `sparkle-design.css`, including layouts
54
+ // like Vite where the entry CSS and sparkle-design.css live in different dirs.
32
55
  SPARKLE_IMPORT:
33
- /\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"](\.\/)?sparkle-design\.css['"];?\s*\n?/g,
56
+ /\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"][^"']*sparkle-design\.css['"];?\s*\n?/g,
34
57
  TAILWIND_IMPORT: /@import\s+['"]tailwindcss['"];?/,
35
58
 
36
59
  // @source ディレクティブ関連
60
+ // ※ 単体の SOURCE_DIRECTIVE は「ユーザーが手書きした @source も含めて全部」
61
+ // マッチしてしまうので、removeExistingImports では使わない(ユーザー記述を
62
+ // 消してしまう)。CLI が挿入した block(コメント + それに続く連続 @source 行)
63
+ // だけを除去するために MANAGED_SOURCE_BLOCK を使う。
64
+ // en: SOURCE_DIRECTIVE alone matches user-authored @source lines too, so
65
+ // removeExistingImports uses MANAGED_SOURCE_BLOCK instead to remove only the
66
+ // comment-prefixed block that the CLI itself wrote.
37
67
  SOURCE_DIRECTIVE: /@source\s+["'][^"']*["'];?\s*\n?/g,
38
68
  SOURCE_COMMENT: /\/\*\s*npm パッケージのコンテンツスキャン[^*]*\*\/\s*\n?/g,
69
+ MANAGED_SOURCE_BLOCK:
70
+ /\/\*\s*npm パッケージのコンテンツスキャン[^*]*\*\/\s*\n?(?:@source\s+["'][^"']*["'];?\s*\n?)*/g,
39
71
 
40
72
  // カスタムCSS関連
41
73
  CUSTOM_CSS_IMPORT:
@@ -107,11 +139,17 @@ export const COMMENTS = {
107
139
  FONT_IMPORT: '/* フォントのインポート(CSSの仕様上、@importは最初に記述する必要がある) */',
108
140
  SPARKLE_IMPORT: '/* Sparkle Design のカスタム定義(Tailwindの後にインポート) */',
109
141
  TAILWIND_IMPORT: '/* Tailwindのインポート */',
110
- SOURCE_DIRECTIVE: '/* npm パッケージのコンテンツスキャン(Tailwind がクラスを検出するために必要) */',
142
+ SOURCE_DIRECTIVE:
143
+ '/* npm パッケージのコンテンツスキャン(Tailwind がクラスを検出するために必要) */',
111
144
  CUSTOM_CSS: '/* プロジェクト固有のカスタムトークン */',
112
145
  };
113
146
 
114
147
  // インポート文のテンプレート
148
+ // SPARKLE_DESIGN は entry CSS と sparkle-design.css が同じディレクトリにある
149
+ // 場合のデフォルト。異なる場合は buildSparkleImportStatement() で相対 path を
150
+ // 計算して差し替える(Vite の src/index.css → src/app/sparkle-design.css 等)。
151
+ // en: Default when entry CSS and sparkle-design.css share a directory. Use
152
+ // buildSparkleImportStatement() for layouts that place them in different dirs.
115
153
  export const IMPORTS = {
116
154
  SPARKLE_DESIGN: '@import "./sparkle-design.css";',
117
155
  TAILWIND: '@import "tailwindcss";',
@@ -145,7 +183,11 @@ export const MESSAGES = {
145
183
  FONT_REMOVE_SKIPPED: 'ℹ️ globals.css の更新に失敗したため、フォントimport削除をスキップします。',
146
184
 
147
185
  // 警告メッセージ
148
- TAILWIND_NOT_FOUND: '⚠️ globals.css に Tailwind import が見つかりません。',
186
+ // 指定された globals.css に Tailwind import が無いと @source / sparkle-design.css
187
+ // の挿入ができないため、どのファイルに何を書けばよいかを示す actionable なメッセージにする。
188
+ // en: Make the warning actionable — tell the user which file to edit and what to add.
189
+ TAILWIND_NOT_FOUND: (path) =>
190
+ `⚠️ ${path} に Tailwind import が見つかりません。ファイル先頭に \`@import "tailwindcss";\` を追記してください。`,
149
191
  GLOBALS_UPDATE_FAILED: (error) => `⚠️ globals.css の更新に失敗しました: ${error}`,
150
192
  FONT_MANAGEMENT_ERROR: (error) => `⚠️ フォント管理処理でエラーが発生しました: ${error}`,
151
193
  COLOR_CONVERSION_FAILED: (hex) => `⚠️ 色の変換に失敗しました: ${hex}`,