sparkle-design-cli 2.2.3 → 2.3.1

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
@@ -457,10 +457,10 @@ Next.js / Vite 以外で導入したい方で困ったときは Issue で教え
457
457
 
458
458
  #### Core(プラグインが出力)
459
459
 
460
- - `primary`: プライマリカラー(blue, red, orange など)
460
+ - `primary`: プライマリカラー(必須)。次の7色のいずれかを指定してください: `blue` / `red` / `orange` / `yellow` / `purple` / `green` / `pink`。未指定、またはこれら以外の値を指定した場合、`generate` は例外を投げて停止します(そのブランドカラー専用の `--color-primary-*` / `--color-gray-*` を用意していないため、`var()` の参照先が存在せず壊れた CSS になってしまうのを防ぐためのバリデーションです)。7色にないブランドカラーを使いたい場合は [extend.custom-css](#extendcustom-css) を参照してください。
461
461
  - `font-pro`: プロポーショナルフォント([Google Fonts](https://fonts.google.com/) の名前)
462
462
  - `font-mono`: モノスペースフォント([Google Fonts](https://fonts.google.com/) の名前)
463
- - `radius`: 角丸設定(sm, md, lg など)
463
+ - `radius`: 角丸設定(必須)。次のいずれかを指定してください: `none` / `xs` / `sm` / `md` / `lg` / `xl` / `2xl` / `3xl`。未指定、またはこれら以外の値を指定した場合、`primary` と同様に `generate` は例外を投げて停止します(無効な値だと `--radius-action` が壊れた参照のまま出力されるのを防ぐためです)。
464
464
 
465
465
  ### extend セクション(拡張設定)
466
466
 
@@ -510,6 +510,74 @@ Figma プラグインが出力する基本4項目に加えて、プロジェク
510
510
 
511
511
  プロジェクト固有のカスタムトークンの CSS ファイルパス。Tailwind エントリ CSS に `@import` が自動挿入されます。`sparkle-design.css` に直接追加すると `generate` 実行時に上書きされるため、必ず別ファイルに分離してください。
512
512
 
513
+ この `@import` は `sparkle-design.css` の直後に挿入されるため、`custom-css` 側で書いた `:root { --color-xxx: ... }` は CSS の cascade(後勝ち)でそのまま `sparkle-design.css` のトークンを上書きします。属性セレクタや `!important` を使う必要はありません。
514
+
515
+ ##### カスタムブランドカラーを primary にしたい場合
516
+
517
+ `primary` は 7 色のいずれかしか受け付けません(前述の Core セクション参照)。7 色にないブランドカラーを使いたい場合は、`primary` は 7 色から見た目が近いものを仮に選んだ上で、`extend.custom-css` で `--color-primary-*` と `--color-gray-*` をブランドカラー基準の値で丸ごと再定義してください。`primary` の選択自体は `custom-css` 側の定義で完全に上書きされるため実質的な意味を持たなくなりますが、フィールドとしては有効な値を入れておく必要があります。
518
+
519
+ ```css
520
+ /* custom-tokens.css */
521
+ :root {
522
+ /* ブランドカラー基準の primary パレット(50〜900) */
523
+ --color-primary-50: oklch(97% 0.02 250);
524
+ /* ... */
525
+ --color-primary-500: oklch(55% 0.18 250);
526
+ /* ... */
527
+ --color-primary-900: oklch(20% 0.08 250);
528
+
529
+ /* primary に合わせた gray パレット(50〜900)も必ず一緒に定義する。
530
+ gray だけ 7 色デフォルトのままだと、primary だけ浮いて見えるトーンずれが起きる */
531
+ --color-gray-50: oklch(98% 0.005 250);
532
+ /* ... */
533
+ --color-gray-900: oklch(22% 0.01 250);
534
+ }
535
+ ```
536
+
537
+ `primary` だけを差し替えて `gray` を既定のままにすると、コンポーネントの枠線・背景・テキストに使われる gray 系トークンと primary のトーンが揃わなくなるため、gray も必ずセットで再定義してください。
538
+
539
+ ## 複数のテーマ配色を 1 デプロイでサポートしたい場合
540
+
541
+ 管理画面と一般ユーザー画面で配色を出し分けたい、テナントごとに固定の配色バリアントを切り替えたいなど、「少数(数種類程度)の固定配色を 1 つのデプロイでサポートする」ケースの推奨パターンです。
542
+
543
+ ### 推奨: config を分割して複数の CSS を生成する
544
+
545
+ `generate` は `-c/--config` と `-o/--output` を組み合わせることで、任意の config から任意の出力先へ CSS を生成できます。バリアントの数だけ config ファイルを用意し、バリアントの数だけ `generate` を実行してください(1 config に複数テーマをまとめて生成する機能はありませんが、CI やスクリプトで複数回呼び出せば十分に運用できます)。
546
+
547
+ ```jsonc
548
+ // config/sparkle.admin.json
549
+ { "primary": "blue", "font-pro": "Inter", "font-mono": "JetBrains Mono", "radius": "md" }
550
+ ```
551
+
552
+ ```jsonc
553
+ // config/sparkle.employee.json
554
+ { "primary": "green", "font-pro": "Inter", "font-mono": "JetBrains Mono", "radius": "md" }
555
+ ```
556
+
557
+ ```bash
558
+ npx sparkle-design-cli generate -c ./config/sparkle.admin.json -o ./src/styles/sparkle-admin.css
559
+ npx sparkle-design-cli generate -c ./config/sparkle.employee.json -o ./src/styles/sparkle-employee.css
560
+ ```
561
+
562
+ あとは consumer 側で、表示するバリアントに応じてどちらの CSS を読み込むかを切り替えるだけです(例: role / tenant を判定できるレイアウト単位で `import "./sparkle-admin.css"` と `import "./sparkle-employee.css"` を出し分ける、Next.js のルートグループごとに読み込む entry CSS を分ける、など)。**同時に両方を読み込まない**(1 画面につき常に 1 バリアントだけを読み込む)ようにすると、後述のファイルサイズ増加の影響を最小化できます。
563
+
564
+ ### 非推奨: 単一 CSS + 属性スコープでの Tailwind クラス上書き
565
+
566
+ `sparkle-design.css` を 1 本のまま、`[data-theme="xxx"] .bg-primary-500 { ... }` のような属性セレクタで Tailwind ユーティリティクラスを個別に上書きする方式は **推奨しません**。
567
+
568
+ - コンポーネント側の実装詳細(クラス名の付け方や CSS の specificity)に依存するため、Sparkle 側の内部実装(例: 状態別カラーリングで compound attribute selector を使っているコンポーネント)が変わると、`!important` を使っても上書きが効かなくなることがあります。上書き対象は Sparkle が公開している契約(config / トークン)ではなく非公開の実装詳細なので、バージョンアップのたびに壊れるリスクを consumer 側が抱え続けることになります。
569
+ - 上書きが必要なクラス/コンポーネントが増えるたびに、手動でセレクタを足していく必要があり、考慮漏れに気づきにくいです。
570
+
571
+ 上記のリスクを踏まえると、テーマ切り替えは config 分割 + `extend.custom-css` によるトークンレベルの上書き(cascade 順で確実に勝つ)で完結させる方が、Sparkle のバージョンアップに対しても壊れにくく、見通しの良い実装になります。
572
+
573
+ ### トレードオフ
574
+
575
+ | 観点 | config 分割(推奨) | 属性スコープ上書き(非推奨) |
576
+ | --- | --- | --- |
577
+ | ファイルサイズ | バリアントの数だけ CSS が増える(各 CSS は 7 色パレット全体を含むため、1 バリアントあたりのベースコストは一定)。1 画面 1 バリアントのみ読み込む運用なら実質的な増分は小さい | 単一 CSS + 上書き分の差分のみで増分は小さい |
578
+ | FOUC リスク | サーバー側 / ビルド時にバリアントを決定できるならほぼ無し。クライアント側でバリアント判定後に切り替える場合は他方式と同程度のリスクがある | クライアント側で属性を付与するタイミング次第でリスクがある(初期描画後に属性が付くと一瞬デフォルト配色が見える) |
579
+ | 保守性 | Sparkle が公開している config / トークンだけに依存するため、コンポーネント内部実装の変更に影響されにくい | コンポーネントの内部実装(クラス名・specificity)に依存するため、Sparkle 側の変更で上書きが効かなくなるリスクがある |
580
+
513
581
  ## 出力
514
582
 
515
583
  - デフォルト出力先: `src/app/sparkle-design.css`
@@ -531,6 +599,10 @@ CSS 仕様上 `@import` は他の at-rule より前に書く必要があるた
531
599
  ### セットアップ
532
600
 
533
601
  ```bash
602
+ # submodule(templates/sparkle-variables)を取得
603
+ # ※ 未初期化だと generate とそれに依存するテストが失敗する
604
+ git submodule update --init --recursive
605
+
534
606
  # 依存関係をインストール
535
607
  npm install
536
608
 
@@ -541,11 +613,17 @@ npm link
541
613
  ### 開発用コマンド
542
614
 
543
615
  ```bash
544
- npm test # Node.js test runner
545
- npm run lint # ESLint
546
- npm run format # Prettier
616
+ npm test # Node.js test runner
617
+ npm run test:watch # テストの watch 実行
618
+ npm run lint # ESLint
619
+ npm run lint:fix # ESLint 自動修正
620
+ npm run format # Prettier
621
+ npm run format:check # Prettier チェックのみ
622
+ npm run sync:anti-pattern-docs # check ルール一覧を README と隣接リポジトリの JSDoc に同期
547
623
  ```
548
624
 
625
+ > **`sync:anti-pattern-docs` の注意:** このスクリプトは README だけでなく、ワークスペース内の隣接リポジトリ(`../sparkle-design` / `../sparkle-design-internal`)のファイルも書き換える横断スクリプトです。両リポジトリが隣にある Sparkle ワークスペース内で実行してください。
626
+
549
627
  ### リリース手順(メンテナ向け)
550
628
 
551
629
  publish は GitHub Actions の **Publish to npm** workflow 経由。ローカル `npm publish` は禁止。
@@ -184,10 +184,14 @@ Generate options:
184
184
  sparkle.config.json の設定フィールド:
185
185
 
186
186
  基本設定(Figma プラグインで出力可能):
187
- primary プライマリカラー (blue, red, orange, green, purple, pink, yellow など)
187
+ primary プライマリカラー (必須。blue, red, orange, yellow, purple, green, pink の
188
+ いずれか。未指定、またはそれ以外の値は generate 実行時にエラーになります。
189
+ 7色にないブランドカラーを使いたい場合は extend.custom-css で
190
+ --color-primary-* / --color-gray-* を再定義してください。詳細は README を参照)
188
191
  font-pro プロポーショナルフォント (Google Fonts の名前)
189
192
  font-mono モノスペースフォント (Google Fonts の名前)
190
- radius 角丸設定 (none, sm, md, lg, xl, full)
193
+ radius 角丸設定 (必須。none, xs, sm, md, lg, xl, 2xl, 3xl のいずれか。
194
+ 未指定、またはそれ以外の値は primary と同様に generate 実行時にエラーになります)
191
195
 
192
196
  拡張設定(extend セクション、任意):
193
197
  extend オブジェクト直書き、またはファイルパス(例: "./sparkle.extend.json")
@@ -72,14 +72,99 @@ export function hexToOklch(hex) {
72
72
  return `oklch(${l} ${c} ${h})`;
73
73
  }
74
74
 
75
+ /**
76
+ * primary に指定できる有効な色名の一覧を返す。
77
+ * colors.json(パレット定義)と gray.json(グレーマッピング)の両方に
78
+ * キーが存在する色だけを「primary として使える色」とみなす。
79
+ * colors.json のキー順(= README / CLI help の記載順)をそのまま維持する
80
+ * ため、`Object.keys(colors)` を起点に走査する(`Object.keys(grayMapping)`
81
+ * を起点にすると gray.json 側のキー順に引きずられ、ドキュメントの記載順と
82
+ * 食い違うエラーメッセージになってしまう)。
83
+ * en: A color is a valid `primary` choice only if it has both a palette
84
+ * definition (colors.json) and a matching gray mapping (gray.json). Iterate
85
+ * `colors` (not `grayMapping`) so the result preserves colors.json's key
86
+ * order, matching the order documented in README/CLI help.
87
+ * @param {Object} colors colors.jsonから読み込んだ色の定義
88
+ * @param {Object} grayMapping gray.jsonから読み込んだグレーの定義
89
+ * @returns {string[]} 有効な primary 色名の配列
90
+ */
91
+ export function getValidPrimaryColors(colors, grayMapping) {
92
+ return Object.keys(colors).filter(
93
+ (name) =>
94
+ typeof colors[name] === 'object' &&
95
+ colors[name] !== null &&
96
+ Object.prototype.hasOwnProperty.call(grayMapping, name)
97
+ );
98
+ }
99
+
100
+ /**
101
+ * sparkle.config.json の `primary` を検証する。
102
+ *
103
+ * `primary` は `--color-primary-*: var(--color-{{PRIMARY}}-*)` のエイリアス先
104
+ * として使われるため、未知の値を渡すと `var()` の参照先が存在せず、gray token
105
+ * も生成されないまま「壊れているが構文的には正しい CSS」が silent に出力されて
106
+ * しまう(issue #222 発見3)。ここで早期に loud throw することで、consumer が
107
+ * 気付かないまま壊れた配色を deploy してしまう事態を防ぐ。
108
+ *
109
+ * ブランド独自色を primary にしたいケースは、`primary` 自体を拡張するのではなく
110
+ * `extend.custom-css` で `--color-primary-*` / `--color-gray-*` を直接定義して
111
+ * 上書きする方式を案内する(README.md 参照)。
112
+ *
113
+ * en: `primary` is only ever used to alias `--color-primary-*` to one of the
114
+ * shipped palettes (`var(--color-{{PRIMARY}}-*)`). An unknown value silently
115
+ * produces syntactically-valid-but-broken CSS (issue #222, finding 3) — no
116
+ * gray tokens, and primary tokens that resolve to nothing. Fail loud instead.
117
+ * For real custom brand colors, direct users to override the primary/gray
118
+ * tokens wholesale via `extend.custom-css` rather than stretching `primary`.
119
+ *
120
+ * @param {string} primaryColor config.primary の値
121
+ * @param {Object} colors colors.jsonから読み込んだ色の定義
122
+ * @param {Object} grayMapping gray.jsonから読み込んだグレーの定義
123
+ * @throws {Error} primaryColor が有効な色名でない場合(`err.code === 'E_INVALID_PRIMARY_COLOR'`)
124
+ */
125
+ export function assertValidPrimaryColor(primaryColor, colors, grayMapping) {
126
+ const validColors = getValidPrimaryColors(colors, grayMapping);
127
+
128
+ if (typeof primaryColor === 'string' && validColors.includes(primaryColor)) {
129
+ return;
130
+ }
131
+
132
+ const err = new Error(
133
+ `sparkle.config.json の "primary" に無効な値が指定されています: ${JSON.stringify(primaryColor)}\n` +
134
+ ` 有効な値: ${validColors.join(', ')}\n` +
135
+ ` これら以外のブランドカラーを primary にしたい場合は、primary は上記いずれかのままにし、\n` +
136
+ ` extend.custom-css 側で --color-primary-*(50〜900)と --color-gray-*(50〜900)を\n` +
137
+ ` 独自の値で定義してオーバーライドしてください。primary の値だけでは任意色を表現できません。\n` +
138
+ ` 詳細: README.md の「カスタムブランドカラーを primary にしたい場合」を参照してください。`
139
+ );
140
+ err.code = 'E_INVALID_PRIMARY_COLOR';
141
+ throw err;
142
+ }
143
+
75
144
  /**
76
145
  * 色のトークンを生成する
146
+ *
147
+ * `generateColorTokens` はこのパッケージの public export(package.json の
148
+ * `main`/`exports["."]` が指す `lib/generate-css.js` から再エクスポートされる)
149
+ * なので、`generateCSS()` を経由しない直接呼び出しでも primary の検証が
150
+ * 効くよう、ここで `assertValidPrimaryColor` を呼ぶ。以前は `generateCSS()`
151
+ * 側だけで検証していたため、`generateColorTokens` を直接 import して不正な
152
+ * primaryColor を渡すと検証をすり抜けて壊れた CSS を返せてしまっていた。
153
+ * en: `generateColorTokens` is part of this package's public export surface
154
+ * (re-exported from `lib/generate-css.js`, which `package.json`'s
155
+ * `main`/`exports["."]` point to). Validate here — not only in
156
+ * `generateCSS()` — so a direct import of this function can't bypass the
157
+ * primary-color check and silently return broken CSS.
158
+ *
77
159
  * @param {Object} colors colors.jsonから読み込んだ色の定義
78
160
  * @param {Object} grayMapping gray.jsonから読み込んだグレーの定義
79
161
  * @param {string} primaryColor プライマリカラーの名前
80
162
  * @returns {string} 色のトークンのCSS文字列
163
+ * @throws {Error} primaryColor が有効な色名でない場合(`assertValidPrimaryColor` 参照)
81
164
  */
82
165
  export function generateColorTokens(colors, grayMapping, primaryColor) {
166
+ assertValidPrimaryColor(primaryColor, colors, grayMapping);
167
+
83
168
  const colorTokens = [];
84
169
 
85
170
  // 通常の色トークンを生成
@@ -96,15 +181,15 @@ export function generateColorTokens(colors, grayMapping, primaryColor) {
96
181
  }
97
182
  });
98
183
 
99
- // グレーの色トークンを生成(プライマリカラーに基づく)
100
- if (primaryColor && grayMapping[primaryColor]) {
101
- const grayColors = grayMapping[primaryColor];
102
- Object.entries(grayColors).forEach(([key, hexValue]) => {
103
- const grayLevel = GRAY_LEVEL_MAP[key] || key;
104
- const oklchValue = hexToOklch(hexValue);
105
- colorTokens.push(` --color-gray-${grayLevel}: ${oklchValue};`);
106
- });
107
- }
184
+ // グレーの色トークンを生成(プライマリカラーに基づく)。primaryColor は
185
+ // 上の assertValidPrimaryColor で検証済みなので grayMapping[primaryColor]
186
+ // の存在は保証されている。
187
+ const grayColors = grayMapping[primaryColor];
188
+ Object.entries(grayColors).forEach(([key, hexValue]) => {
189
+ const grayLevel = GRAY_LEVEL_MAP[key] || key;
190
+ const oklchValue = hexToOklch(hexValue);
191
+ colorTokens.push(` --color-gray-${grayLevel}: ${oklchValue};`);
192
+ });
108
193
 
109
194
  return colorTokens.join('\n');
110
195
  }
@@ -15,7 +15,13 @@ import {
15
15
  loadGrayMapping,
16
16
  loadRadiusMapping,
17
17
  } from './file-loader.js';
18
- import { generateColorTokens, hexToRgba, hexToOklch } from './color-utils.js';
18
+ import {
19
+ generateColorTokens,
20
+ hexToRgba,
21
+ hexToOklch,
22
+ assertValidPrimaryColor,
23
+ getValidPrimaryColors,
24
+ } from './color-utils.js';
19
25
  import {
20
26
  manageFontImports,
21
27
  extractFontImports,
@@ -434,6 +440,49 @@ function upsertViteIndexHtmlFonts(cwd, resolvedFonts) {
434
440
  return { status: 'created', path: indexHtmlPath };
435
441
  }
436
442
 
443
+ /**
444
+ * radius に指定できる有効な値の一覧を返す(radius.csv の name 列)。
445
+ * @param {Object} radiusMapping radius.csvから読み込んだ Radius マッピング
446
+ * @returns {string[]} 有効な radius 値の配列
447
+ */
448
+ function getValidRadiusValues(radiusMapping) {
449
+ return Object.keys(radiusMapping);
450
+ }
451
+
452
+ /**
453
+ * sparkle.config.json の `radius` を検証する。
454
+ *
455
+ * `radius` が radius.csv に無い値だと `--radius-action: var(--radius-<不正な
456
+ * 値>)` が dangling reference のまま出力され、`radius` が config に無いと
457
+ * `{{RADIUS}}` プレースホルダーがそもそも置換されず、テンプレート内に生の
458
+ * `{{RADIUS}}`(`{` `}` を含む)が残って CSS 構文自体が壊れる。primary
459
+ * (issue #222 発見3)と同じ「型強制・validation が無いことによる silent
460
+ * breakage」がここにも存在するため、同じパターンで検証する。
461
+ *
462
+ * en: An unknown `radius` value ships a dangling `var(--radius-<bad-value>)`;
463
+ * a missing `radius` leaves the raw `{{RADIUS}}` placeholder (including its
464
+ * braces) unreplaced, which is invalid CSS syntax. Same silent-breakage class
465
+ * as the `primary` bug (issue #222, finding 3) — validate it the same way.
466
+ *
467
+ * @param {string} radius config.radius の値
468
+ * @param {Object} radiusMapping radius.csvから読み込んだ Radius マッピング
469
+ * @throws {Error} radius が有効な値でない場合(`err.code === 'E_INVALID_RADIUS'`)
470
+ */
471
+ function assertValidRadius(radius, radiusMapping) {
472
+ const validValues = getValidRadiusValues(radiusMapping);
473
+
474
+ if (typeof radius === 'string' && validValues.includes(radius)) {
475
+ return;
476
+ }
477
+
478
+ const err = new Error(
479
+ `sparkle.config.json の "radius" に無効な値が指定されています: ${JSON.stringify(radius)}\n` +
480
+ ` 有効な値: ${validValues.join(', ')}`
481
+ );
482
+ err.code = 'E_INVALID_RADIUS';
483
+ throw err;
484
+ }
485
+
437
486
  /**
438
487
  * テンプレート変数を設定値で置換する
439
488
  * @param {string} template CSSテンプレート
@@ -799,6 +848,23 @@ export function generateCSS(
799
848
  const grayMapping = loadGrayMapping();
800
849
  const radiusMapping = loadRadiusMapping();
801
850
 
851
+ // 4.5 radius の値を検証する(issue #222 発見3の横展開)。無効な値、または
852
+ // radius 自体が未指定だと `var(--radius-<不正な名前>)` への dangling
853
+ // reference、あるいは `{{RADIUS}}` プレースホルダーが置換されず CSS 構文が
854
+ // 壊れたまま出力されてしまう。primary と同種のバグだが、radius の置換は
855
+ // `processTemplate` 内にインライン実装されており(`generateColorTokens` の
856
+ // ような専用の公開関数を持たない)、`processTemplate` はフォント処理など
857
+ // 他のテストから部分的な config で直接呼ばれる汎用関数でもあるため、検証は
858
+ // `processTemplate` の内部にではなく、実際の CLI 実行経路であるここ
859
+ // (`generateCSS`)に置く。
860
+ // en: Validate radius before generation (same bug class as primary, applied
861
+ // to the sibling enum field). Unlike primary/generateColorTokens, radius
862
+ // substitution is inlined in processTemplate — a general-purpose function
863
+ // also called directly, with partial configs, by unrelated font tests — so
864
+ // validating inside processTemplate would break that flexibility. Validate
865
+ // here instead, at the actual CLI entry point.
866
+ assertValidRadius(config.radius, radiusMapping);
867
+
802
868
  // 5. テンプレートを設定値で処理(resolvedFonts も返す)
803
869
  const { css: processedCSS, resolvedFonts } = processTemplate(
804
870
  template,
@@ -923,6 +989,10 @@ export {
923
989
  hexToRgba,
924
990
  hexToOklch,
925
991
  generateColorTokens,
992
+ assertValidPrimaryColor,
993
+ getValidPrimaryColors,
994
+ assertValidRadius,
995
+ getValidRadiusValues,
926
996
  dedupeFontImports,
927
997
  // 設定解決
928
998
  resolveExtend,
@@ -61,7 +61,12 @@ function extractPluginEntry(pkg) {
61
61
  }
62
62
 
63
63
  /**
64
- * Discover and load anti-pattern plugins from the consumer's package.json deps.
64
+ * Discover and load anti-pattern plugins from the consumer's package.json deps, plus
65
+ * a self-reference path: if the consumer's own package.json declares
66
+ * `sparkleCli.antiPatterns`, that entry is loaded too (packageName is recorded as the
67
+ * consumer's own `name`, or `"(self)"` if unnamed). This lets a library check its own
68
+ * source against the anti-patterns it defines for its consumers, without needing to
69
+ * list itself in its own dependencies.
65
70
  *
66
71
  * Returns an object of the shape:
67
72
  * {
@@ -92,18 +97,8 @@ export async function loadAntiPatternPlugins({ cwd = process.cwd() } = {}) {
92
97
  const groups = [];
93
98
  const reminders = [];
94
99
 
95
- for (const depName of depNames) {
96
- const depPkgPath = resolvePluginPackageJson(depName, cwd);
97
- if (!depPkgPath) continue;
98
-
99
- const depPkg = readJsonSafe(depPkgPath);
100
- if (!depPkg) continue;
101
-
102
- const entry = extractPluginEntry(depPkg);
103
- if (!entry) continue;
104
-
105
- const resolvedFile = path.resolve(path.dirname(depPkgPath), entry);
106
- const record = { packageName: depName, entry, resolvedFile, status: 'loaded' };
100
+ const loadEntry = async (packageName, entry, resolvedFile) => {
101
+ const record = { packageName, entry, resolvedFile, status: 'loaded' };
107
102
 
108
103
  try {
109
104
  if (!fs.existsSync(resolvedFile)) {
@@ -115,7 +110,7 @@ export async function loadAntiPatternPlugins({ cwd = process.cwd() } = {}) {
115
110
  // plain object を export する plugin にも同じ厳しさを適用したい。
116
111
  // en: Share shape validation with `defineAntiPatternPlugin` so plugins that skip
117
112
  // the helper are still rejected at load time.
118
- validatePluginShape(candidate, `${depName} (${entry})`);
113
+ validatePluginShape(candidate, `${packageName} (${entry})`);
119
114
  groups.push(...candidate.groups);
120
115
  record.groupIds = candidate.groups.map((group) => group.id);
121
116
  if (Array.isArray(candidate.manualReviewReminders)) {
@@ -125,11 +120,43 @@ export async function loadAntiPatternPlugins({ cwd = process.cwd() } = {}) {
125
120
  record.status = 'error';
126
121
  record.error = error?.message ?? String(error);
127
122
  console.warn(
128
- `⚠️ sparkle-design-cli: failed to load anti-pattern plugin from ${depName} (entry: ${entry}, error: ${record.error})`
123
+ `⚠️ sparkle-design-cli: failed to load anti-pattern plugin from ${packageName} (entry: ${entry}, error: ${record.error})`
129
124
  );
130
125
  }
131
126
 
132
127
  discovered.push(record);
128
+ };
129
+
130
+ // 自己参照パス: consumer 自身が sparkleCli.antiPatterns を宣言しているケース
131
+ // (自分自身のソースに対して check を実行する場合、自分自身は自分の dependencies には
132
+ // 現れないため、依存パッケージ経由の discovery だけでは見つからない)。
133
+ // en: Self-reference path — a package checking its own source won't list itself in
134
+ // its own dependencies, so dependency-based discovery alone would miss it.
135
+ const selfEntry = extractPluginEntry(consumerPkg);
136
+ if (selfEntry) {
137
+ const resolvedFile = path.resolve(cwd, selfEntry);
138
+ await loadEntry(consumerPkg.name || '(self)', selfEntry, resolvedFile);
139
+ }
140
+
141
+ for (const depName of depNames) {
142
+ // 自分自身の名前を自分の dependencies に含むケース(workspace の self-link 等)では、
143
+ // 上の自己参照パスと重複してロードされてしまうためスキップする。
144
+ // en: Skip the consumer's own name if it also appears in its own dependencies (e.g. a
145
+ // workspace self-link) — otherwise the self-reference path above and this loop would
146
+ // load the same plugin entry twice.
147
+ if (depName === consumerPkg.name) continue;
148
+
149
+ const depPkgPath = resolvePluginPackageJson(depName, cwd);
150
+ if (!depPkgPath) continue;
151
+
152
+ const depPkg = readJsonSafe(depPkgPath);
153
+ if (!depPkg) continue;
154
+
155
+ const entry = extractPluginEntry(depPkg);
156
+ if (!entry) continue;
157
+
158
+ const resolvedFile = path.resolve(path.dirname(depPkgPath), entry);
159
+ await loadEntry(depName, entry, resolvedFile);
133
160
  }
134
161
 
135
162
  return { groups, reminders, discovered };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparkle-design-cli",
3
- "version": "2.2.3",
3
+ "version": "2.3.1",
4
4
  "description": "Sparkle Design CLI — プロジェクトセットアップ、CSS・フォント生成、アンチパターン検査、AI エージェント(Claude Code / Cursor / Codex)向けのガードと hook 設定まで一括で行う sparkle-design 公式 CLI。",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",