sparkle-design-cli 2.5.0-beta.1 → 2.5.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +9 -0
- package/README.md +4 -4
- package/bin/sparkle-design.js +29 -11
- package/docs/config.md +27 -2
- package/lib/anti-pattern-rules.js +137 -3
- package/lib/check.js +25 -4
- package/lib/load-plugins.js +6 -0
- package/lib/path-utils.js +120 -0
- package/lib/rules-report.js +42 -7
- package/lib/stop-hook.js +116 -3
- package/lib/token-migration.js +210 -0
- package/package.json +5 -5
package/CONTRIBUTING.md
CHANGED
|
@@ -27,6 +27,15 @@ npm run sync:anti-pattern-docs # ルールの解説を隣接リポジトリの
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
> **`sync:anti-pattern-docs` の注意:** ワークスペース内の隣接リポジトリ(`../sparkle-design` / `../sparkle-design-internal`)のファイルを書き換える横断スクリプトです。両リポジトリが隣にある Sparkle ワークスペース内で実行してください(worktree からは sibling が解決できず落ちます)。**このリポジトリの README は書き換えません** — ルール一覧は `rules` コマンドが出すためです。
|
|
30
|
+
>
|
|
31
|
+
> **共有チェックアウトに向けて実行しないこと。** 隣接リポジトリで誰かが作業中だと、その未コミット変更に自分の生成物が混ざります。実行後に `git checkout -- .` で戻そうとして**他人の変更まで消す事故が実際に起きました**。同期専用の worktree を切り、`SPARKLE_DESIGN_ROOT` / `SPARKLE_DESIGN_INTERNAL_ROOT` でそこを指してください。
|
|
32
|
+
>
|
|
33
|
+
> ```bash
|
|
34
|
+
> git -C ../sparkle-design worktree add .claude/worktrees/docs-sync -b chore/sync-anti-pattern-docs
|
|
35
|
+
> SPARKLE_DESIGN_ROOT=../sparkle-design/.claude/worktrees/docs-sync npm run sync:anti-pattern-docs
|
|
36
|
+
> ```
|
|
37
|
+
>
|
|
38
|
+
> 実行前に対象リポジトリが clean であることを `git status` で確認し、戻すときも**自分が触ったファイルだけを明示的に指定**してください(`git checkout -- <path>`)。
|
|
30
39
|
|
|
31
40
|
## リリース手順(メンテナ向け)
|
|
32
41
|
|
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ npx sparkle-design-cli rules
|
|
|
94
94
|
npx sparkle-design-cli generate
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
3. `src/app/sparkle-design.css`
|
|
97
|
+
3. CSS ファイルが生成されます(既定は `src/app/sparkle-design.css`。`extend.globals-path` や `--globals-path` で Tailwind エントリ CSS を明示していて、それが実在する場合はそのディレクトリに出ます)。
|
|
98
98
|
|
|
99
99
|
### generate: コマンドオプション
|
|
100
100
|
|
|
@@ -125,7 +125,7 @@ sparkle-design-cli generate -c ./config/sparkle.admin.json --scope '[data-tenant
|
|
|
125
125
|
|
|
126
126
|
- `-h, --help`: ヘルプメッセージを表示
|
|
127
127
|
- `-c, --config <パス>`: 設定ファイルのパス(デフォルト: `./sparkle.config.json`)
|
|
128
|
-
- `-o, --output <パス>`:
|
|
128
|
+
- `-o, --output <パス>`: 出力ファイルのパス。未指定時は、明示された Tailwind エントリ CSS(`--globals-path` / `extend.globals-path`)が実在すればそのディレクトリ、無ければ `./src/app/sparkle-design.css`
|
|
129
129
|
- `--globals-path <パス>`: Tailwind エントリポイント CSS のパス(デフォルト: 自動検出)。
|
|
130
130
|
**指定したパスが存在しない場合は `--strict` の有無に関わらず常に exit 1**(`sparkle.config.json` の `extend.globals-path` も同様)
|
|
131
131
|
- `--strict`: 以下を warn ではなく **exit 1** に昇格させる(CI 向け。既定は warn + 継続で後方互換を維持):
|
|
@@ -274,7 +274,7 @@ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaf
|
|
|
274
274
|
|
|
275
275
|
## 出力
|
|
276
276
|
|
|
277
|
-
-
|
|
277
|
+
- 既定の出力先: `src/app/sparkle-design.css`(Tailwind エントリ CSS を明示していて実在する場合はそのディレクトリ)
|
|
278
278
|
- カスタム出力先: `-o` オプションで指定可能
|
|
279
279
|
- 実行場所を基準として相対パスで処理されます
|
|
280
280
|
|
|
@@ -283,7 +283,7 @@ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaf
|
|
|
283
283
|
CLI は **Tailwind エントリ CSS(`@import "tailwindcss"` を含む CSS ファイル)** を自動検出し、以下を 1 回で揃えます:
|
|
284
284
|
|
|
285
285
|
- `sparkle-design.css` の `@import`
|
|
286
|
-
- `@source "
|
|
286
|
+
- `@source "<エントリ CSS から node_modules への相対パス>/sparkle-design/dist"`(v4 が node_modules のクラスを拾うのに必要)。相対パスはエントリ CSS の位置から毎回計算されるので、`src/app/globals.css` なら `../../node_modules/...` になります
|
|
287
287
|
- フォント `<link>` タグ(React 向けは `SparkleHead.tsx`、Vite 向けは `index.html` の managed block に自動注入)
|
|
288
288
|
|
|
289
289
|
CSS 仕様上 `@import` は他の at-rule より前に書く必要があるため、順序も適切に整えます。`@import "tailwindcss"` が欠けている場合は先頭に自動追記されます。
|
package/bin/sparkle-design.js
CHANGED
|
@@ -146,11 +146,15 @@ function parseSetupOptions(args) {
|
|
|
146
146
|
* and exit 0, handing non-JSON stdout to a caller that asked for JSON.
|
|
147
147
|
*/
|
|
148
148
|
function parseRulesOptions(args) {
|
|
149
|
-
const options = { format: 'text' };
|
|
149
|
+
const options = { format: 'text', help: false };
|
|
150
150
|
const FORMATS = new Set(['text', 'json']);
|
|
151
151
|
|
|
152
152
|
for (let i = 0; i < args.length; i += 1) {
|
|
153
153
|
const arg = args[i];
|
|
154
|
+
if (arg === '-h' || arg === '--help') {
|
|
155
|
+
options.help = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
154
158
|
if (arg === '--format') {
|
|
155
159
|
const value = requireOptionValue(args, i, '--format');
|
|
156
160
|
if (!FORMATS.has(value)) {
|
|
@@ -189,7 +193,7 @@ Generate:
|
|
|
189
193
|
sparkle-design-cli generate --output ./styles/design.css
|
|
190
194
|
sparkle-design-cli generate -c ./config/custom.json -o ./dist/styles.css
|
|
191
195
|
|
|
192
|
-
# 単一バンドル内でランタイムにテーマ切替したい場合(--scope。詳細は
|
|
196
|
+
# 単一バンドル内でランタイムにテーマ切替したい場合(--scope。詳細は docs/theming.md 参照)
|
|
193
197
|
sparkle-design-cli generate -c ./config/sparkle.employee.json -o ./src/styles/sparkle-employee.css
|
|
194
198
|
sparkle-design-cli generate -c ./config/sparkle.admin.json --scope '[data-tenant-theme="admin"]' -o ./src/styles/sparkle-admin-scope.css
|
|
195
199
|
|
|
@@ -232,8 +236,7 @@ Generate options:
|
|
|
232
236
|
だけを実値までリテラル化して指定セレクタの中にラップ出力する
|
|
233
237
|
(例: '[data-tenant-theme="admin"]')。-o/--output の指定が必須。
|
|
234
238
|
--strict / --globals-path とは併用不可(グローバル CSS を
|
|
235
|
-
パッチしないため)。詳細は
|
|
236
|
-
切替する場合」を参照
|
|
239
|
+
パッチしないため)。詳細は docs/theming.md の「ケース B」を参照
|
|
237
240
|
|
|
238
241
|
sparkle.config.json の設定フィールド:
|
|
239
242
|
|
|
@@ -241,7 +244,7 @@ sparkle.config.json の設定フィールド:
|
|
|
241
244
|
primary プライマリカラー (必須。blue, red, orange, yellow, purple, green, pink の
|
|
242
245
|
いずれか。未指定、またはそれ以外の値は generate 実行時にエラーになります。
|
|
243
246
|
7色にないブランドカラーを使いたい場合は extend.custom-css で
|
|
244
|
-
--color-primary-* / --color-gray-* を再定義してください。詳細は
|
|
247
|
+
--color-primary-* / --color-gray-* を再定義してください。詳細は docs/config.md を参照)
|
|
245
248
|
font-pro プロポーショナルフォント (Google Fonts の名前)
|
|
246
249
|
font-mono モノスペースフォント (Google Fonts の名前)
|
|
247
250
|
radius 角丸設定 (必須。none, xs, sm, md, lg, xl, 2xl, 3xl のいずれか。
|
|
@@ -409,16 +412,31 @@ async function main() {
|
|
|
409
412
|
|
|
410
413
|
if (command === 'stop-hook') {
|
|
411
414
|
// setup で各 agent の hook 設定ファイルから呼ばれる internal subcommand。
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
// 以降の引数はすべて lint 対象 path。setup が書き出す hook は 1 つだが、
|
|
416
|
+
// 利用側が `stop-hook apps/web/app apps/web/components` のように手で複数
|
|
417
|
+
// 並べている実例があり、以前は args[1] しか読まず 2 つ目以降が黙って
|
|
418
|
+
// 未検査になっていた(issue #85)。option flag は未対応なので、`-` 始まりは
|
|
419
|
+
// path として渡さず警告する。
|
|
420
|
+
// en: Forward every positional arg. Previously only the first was used, so
|
|
421
|
+
// extra paths in a hand-written hook command were silently never checked.
|
|
422
|
+
const positional = args.slice(1).filter((arg) => !arg.startsWith('-'));
|
|
423
|
+
const flags = args.slice(1).filter((arg) => arg.startsWith('-'));
|
|
424
|
+
if (flags.length > 0) {
|
|
425
|
+
console.warn(
|
|
426
|
+
`⚠️ stop-hook はオプションを受け付けません。無視します: ${flags.join(' ')} / stop-hook takes target paths only.`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const exitCode = await runStopHook(positional);
|
|
417
430
|
process.exit(exitCode);
|
|
418
431
|
}
|
|
419
432
|
|
|
420
433
|
if (command === 'rules') {
|
|
421
|
-
|
|
434
|
+
const options = parseRulesOptions(args.slice(1));
|
|
435
|
+
if (options.help) {
|
|
436
|
+
showHelp();
|
|
437
|
+
process.exit(0);
|
|
438
|
+
}
|
|
439
|
+
await runRules(options);
|
|
422
440
|
return;
|
|
423
441
|
}
|
|
424
442
|
|
package/docs/config.md
CHANGED
|
@@ -34,13 +34,38 @@ Figma プラグインが出力する基本4項目に加えて、プロジェク
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## extend.globals-path
|
|
38
|
+
|
|
39
|
+
Tailwind のエントリポイント CSS(`@import "tailwindcss";` を書いているファイル)のパスです。未指定なら自動検出します。CLI の `--globals-path` でも同じ指定ができ、そちらが優先されます。
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"primary": "blue",
|
|
44
|
+
"extend": {
|
|
45
|
+
"globals-path": "src/styles/app.css"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
指定すると次の 2 つに効きます。
|
|
51
|
+
|
|
52
|
+
| 効き先 | 内容 |
|
|
53
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
54
|
+
| `@source` と `sparkle-design.css` の import 先 | そのファイルにパッチする(**フォントの `@import` は入りません**。フォント読み込みは `SparkleHead.tsx` 側に移っています) |
|
|
55
|
+
| `generate` の出力先 | `-o/--output` 未指定なら**そのファイルと同じディレクトリ**に `sparkle-design.css` と `SparkleHead.tsx` を出す |
|
|
56
|
+
|
|
57
|
+
後者が重要です。既定の `src/app/` に固定されるのは globals-path を明示していない場合だけなので、`src/index.css` や `src/styles/app.css` 構成のプロジェクトで意図しない `src/app/` ディレクトリが増えることはありません。
|
|
58
|
+
|
|
59
|
+
> [!WARNING]
|
|
60
|
+
> **指定したパスが存在しない場合は `--strict` の有無に関わらず常に exit 1** です。typo に気付かないまま誤ったディレクトリを掘るより、その場で止めるほうが安全なためです。相対パスのみで、`..` を含む traversal や絶対パスは弾かれます。
|
|
61
|
+
|
|
37
62
|
## extend.fonts
|
|
38
63
|
|
|
39
64
|
フォントごとにウェイトを個別指定。`extend.fonts` がない場合は `font-pro` / `font-mono` + デフォルトウェイト `[400, 700]` が使われます。`extend.fonts` が存在しても `fonts.pro` / `fonts.mono` のどちらかが未指定なら、そのスロットのみ `font-pro` / `font-mono` にフォールバックします。同じフォントファミリーが `pro` と `mono` で重複する場合、ウェイトはマージされ import は 1 行に統合されます。
|
|
40
65
|
|
|
41
66
|
## extend.source-packages
|
|
42
67
|
|
|
43
|
-
既知のデザインシステムパッケージ(`sparkle-design` など)は `package.json` から**自動検出**されるので、通常は指定不要です。クライアント固有のパッケージなど自動検出の対象外を追加したいときだけ指定してください。指定分は自動検出分とマージされ、Tailwind エントリ CSS(自動検出)に `@source`
|
|
68
|
+
既知のデザインシステムパッケージ(`sparkle-design` など)は `package.json` から**自動検出**されるので、通常は指定不要です。クライアント固有のパッケージなど自動検出の対象外を追加したいときだけ指定してください。指定分は自動検出分とマージされ、Tailwind エントリ CSS(自動検出)に `@source` ディレクティブとして挿入されます。`@source` をまったく出さないのは、**自動検出がゼロで、かつ `source-packages` キー自体を書いていない**ときだけです(キーがあれば空配列でも既定の `sparkle-design` 1 行が出ます)。
|
|
44
69
|
|
|
45
70
|
## extend.custom-css
|
|
46
71
|
|
|
@@ -50,7 +75,7 @@ Figma プラグインが出力する基本4項目に加えて、プロジェク
|
|
|
50
75
|
|
|
51
76
|
### カスタムブランドカラーを primary にしたい場合
|
|
52
77
|
|
|
53
|
-
`primary` は 7
|
|
78
|
+
`primary` は 7 色のいずれかしか受け付けません([README の「設定オプション」](../README.md#設定オプション) を参照)。7 色にないブランドカラーを使いたい場合は、`primary` は 7 色から見た目が近いものを仮に選んだ上で、`extend.custom-css` で `--color-primary-*` と `--color-gray-*` をブランドカラー基準の値で丸ごと再定義してください。`primary` の選択自体は `custom-css` 側の定義で完全に上書きされるため実質的な意味を持たなくなりますが、フィールドとしては有効な値を入れておく必要があります。
|
|
54
79
|
|
|
55
80
|
```css
|
|
56
81
|
/* custom-tokens.css */
|
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
buildLegacyCssVarPattern,
|
|
5
5
|
buildLegacyScalePattern,
|
|
6
6
|
buildLegacyTokenPattern,
|
|
7
|
+
buildTailwindPalettePattern,
|
|
7
8
|
resolveLegacyCssVar,
|
|
8
9
|
resolveLegacyUtility,
|
|
10
|
+
resolveTailwindPaletteUtility,
|
|
9
11
|
} from './token-migration.js';
|
|
10
12
|
import { buildSpacingUtilityPattern, pxToStep, resolveSpacingStep } from './spacing-scale.js';
|
|
11
13
|
|
|
@@ -378,7 +380,10 @@ const COMPONENT_ANTI_PATTERN_GROUPS = [
|
|
|
378
380
|
description: 'shadcn/ui 既定 token を Sparkle Design 内へ持ち込まない',
|
|
379
381
|
recommendation:
|
|
380
382
|
'text-muted-foreground / bg-background / border-border などは Sparkle Design token に置き換えてください。',
|
|
381
|
-
|
|
383
|
+
// 新セマンティックトークン(border-border-neutral-* 等)へ前方一致しないよう、
|
|
384
|
+
// 直後にハイフン/単語構成文字が続くケースを除外する
|
|
385
|
+
// en: exclude prefix matches against new semantic tokens (e.g. border-border-neutral-*)
|
|
386
|
+
pattern: /\b(text-muted-foreground|bg-background|border-border)(?![\w-])/g,
|
|
382
387
|
},
|
|
383
388
|
featureSection: lines([
|
|
384
389
|
'### shadcn/ui 由来の class / token をそのまま使わない',
|
|
@@ -1013,8 +1018,17 @@ const COMPONENT_ANTI_PATTERN_GROUPS = [
|
|
|
1013
1018
|
check: {
|
|
1014
1019
|
description: 'Tailwind デフォルト typography を Sparkle Design コンポーネント内で使わない',
|
|
1015
1020
|
recommendation:
|
|
1016
|
-
'text-
|
|
1017
|
-
|
|
1021
|
+
'text-xs 〜 text-9xl / font-medium / font-semibold / font-bold は character-* utility に置き換えてください。ただし character-* に対応する token が無いサイズ(text-[10px] 等の arbitrary value、あるいは意図的に token 外のサイズを使う場合)は、同一行または直前行に `// sparkle-disable-line tailwind-typography` コメントを付けて例外扱いとして残すこともできます。font-medium(500) / font-semibold(600) は character-* に対応する token が無いため、`extend.custom-css` で独自クラスを定義してください(詳細は README の「character-* に無いウェイトを使いたい場合」参照)。',
|
|
1022
|
+
// text-base は旧カラートークンの text-base-50 〜 text-base-900 へ前方一致するため、
|
|
1023
|
+
// shadcn-token と同様に直後のハイフン/単語構成文字を除外する
|
|
1024
|
+
// en: exclude prefix matches such as text-base-900 (legacy color token)
|
|
1025
|
+
//
|
|
1026
|
+
// `2xl` までしか見ておらず text-3xl 以上が漏れていた(issue #84)。見出しほど
|
|
1027
|
+
// 大きいサイズを使うので、抜けていた側のほうが目立つ誤りだった。Tailwind の
|
|
1028
|
+
// 既定スケールは text-9xl まで。
|
|
1029
|
+
// en: Sizes above 2xl were missed entirely; Tailwind's scale goes to 9xl.
|
|
1030
|
+
pattern:
|
|
1031
|
+
/\b(text-(?:xs|sm|base|lg|xl|[2-9]xl)|font-(?:medium|semibold|bold|normal|light))(?![\w-])/g,
|
|
1018
1032
|
},
|
|
1019
1033
|
featureSection: lines([
|
|
1020
1034
|
'### Tailwind デフォルト typography を使わない',
|
|
@@ -1402,6 +1416,70 @@ function migrationMatcher(pattern, resolve, label) {
|
|
|
1402
1416
|
};
|
|
1403
1417
|
}
|
|
1404
1418
|
|
|
1419
|
+
const TAILWIND_PALETTE_PATTERN = buildTailwindPalettePattern();
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* そのファイルが Sparkle Design を使っているか判定する材料。
|
|
1423
|
+
*
|
|
1424
|
+
* Tailwind 既定パレットの色は「Sparkle を導入したプロジェクトなのに色だけ移行が
|
|
1425
|
+
* 取り残されている」箇所を拾うためのルールなので、Sparkle と無関係なファイル
|
|
1426
|
+
* (素の React コード、管理用スクリプト、vendored なコード)まで報告すると
|
|
1427
|
+
* ノイズになる(issue #84)。
|
|
1428
|
+
*
|
|
1429
|
+
* 2 つの材料を見る:
|
|
1430
|
+
* 1. Sparkle パッケージからの import — `sparkle-design` / `@goodpatch/sparkle-design-internal`
|
|
1431
|
+
* のほか、`sparkle-design/components/...` のようなサブパスも拾えるよう部分一致にする
|
|
1432
|
+
* 2. `character-*` typography の使用 — Sparkle 固有のユーティリティで、
|
|
1433
|
+
* **これが使われている時点でそのファイルは Sparkle に載っている**。
|
|
1434
|
+
* issue #84 の実例はまさに「typography は character-* に移行済みなのに色だけ
|
|
1435
|
+
* Tailwind 既定パレットのまま」という形で、import 判定だけだと
|
|
1436
|
+
* re-export 経由(`@/components/ui/...`)のファイルを取りこぼす
|
|
1437
|
+
*
|
|
1438
|
+
* en: Gate the rule to files that actually use Sparkle — either an import from a
|
|
1439
|
+
* Sparkle package, or a `character-*` utility (Sparkle-specific, and the exact
|
|
1440
|
+
* signal in issue #84 where typography had migrated but colours had not).
|
|
1441
|
+
*/
|
|
1442
|
+
const SPARKLE_USAGE_PATTERNS = [
|
|
1443
|
+
/\b(?:from|import)\s*\(?\s*['"][^'"]*sparkle-design[^'"]*['"]/,
|
|
1444
|
+
/\brequire\(\s*['"][^'"]*sparkle-design[^'"]*['"]\s*\)/,
|
|
1445
|
+
/(?<![\w-])character-\d/,
|
|
1446
|
+
];
|
|
1447
|
+
|
|
1448
|
+
function usesSparkle(content) {
|
|
1449
|
+
return SPARKLE_USAGE_PATTERNS.some((pattern) => pattern.test(content));
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Tailwind 既定パレットの色ユーティリティを拾う。
|
|
1454
|
+
*
|
|
1455
|
+
* `shadcn-token` の説明文は Tailwind パレット色(`text-slate-500`)も Wrong として
|
|
1456
|
+
* 示していたのに、実際の `check.pattern` は shadcn 由来の 3 語しか見ていなかった。
|
|
1457
|
+
* 「説明が禁止しているものを検査が拾っていない」状態を解消する(issue #84)。
|
|
1458
|
+
*
|
|
1459
|
+
* 生の hex(`#6b7280`)は**検出しない**。SVG・チャート描画・ユーザー定義色など、
|
|
1460
|
+
* 移行対象ではない動的な用途が大半を占めるため(報告元のプロジェクトでは 43 件中
|
|
1461
|
+
* 32 件がこれ)、ルール化すると偽陽性で埋まる。
|
|
1462
|
+
* en: Raw hex values are intentionally out of scope — most of them are dynamic
|
|
1463
|
+
* (SVG, charts, user-defined colours) and would drown the report in false positives.
|
|
1464
|
+
*
|
|
1465
|
+
* `targets` を指定していないので対象はソースファイルのみ。`legacy-color-token` が
|
|
1466
|
+
* `.css` も見るのに対し、こちらは `@apply` を拾わない。CSS には import が無く、
|
|
1467
|
+
* `usesSparkle` の 2 材料のうち `character-*` しか効かないため、対象に加えると
|
|
1468
|
+
* 「CSS の一部だけ検査される」という説明しにくい半端なカバレッジになる。
|
|
1469
|
+
* **意図的な線引きなので、featureSection と description に明記してある。**
|
|
1470
|
+
* en: Source files only. CSS has no imports, so the Sparkle gate would work only
|
|
1471
|
+
* half the time there — an unexplainable partial coverage. The limitation is
|
|
1472
|
+
* stated in the rule's description and feature section rather than left implicit.
|
|
1473
|
+
*/
|
|
1474
|
+
function tailwindPaletteMatcher(rawContent) {
|
|
1475
|
+
if (!usesSparkle(maskBlockComments(rawContent))) return [];
|
|
1476
|
+
return migrationMatcher(
|
|
1477
|
+
TAILWIND_PALETTE_PATTERN,
|
|
1478
|
+
resolveTailwindPaletteUtility,
|
|
1479
|
+
'Tailwind 既定パレットの色'
|
|
1480
|
+
)(rawContent);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1405
1483
|
const SPACING_UTILITY_PATTERN = buildSpacingUtilityPattern();
|
|
1406
1484
|
|
|
1407
1485
|
/**
|
|
@@ -1505,6 +1583,61 @@ const TOKEN_MIGRATION_GROUPS = [
|
|
|
1505
1583
|
]),
|
|
1506
1584
|
jsdocTargets: [],
|
|
1507
1585
|
},
|
|
1586
|
+
{
|
|
1587
|
+
id: 'tailwind-palette-color',
|
|
1588
|
+
check: {
|
|
1589
|
+
severity: SEVERITY.WARNING,
|
|
1590
|
+
description:
|
|
1591
|
+
'Tailwind 既定パレットの色ユーティリティを使っています(Sparkle のセマンティックトークンに置き換えます)',
|
|
1592
|
+
recommendation:
|
|
1593
|
+
'text-gray-* / bg-slate-* / border-red-* のような Tailwind 既定パレットの色は、用途別セマンティックトークン(bg- → surface / text- → text / border- → border / fill- → object)に置き換えてください。検査対象はソースファイルのみで、.css の @apply と生の hex 値は見ていません。',
|
|
1594
|
+
match: tailwindPaletteMatcher,
|
|
1595
|
+
},
|
|
1596
|
+
featureSection: lines([
|
|
1597
|
+
'### Tailwind 既定パレットの色をそのまま使わない',
|
|
1598
|
+
'',
|
|
1599
|
+
'```tsx',
|
|
1600
|
+
'// ✅ Correct — 用途別セマンティックトークンを使う',
|
|
1601
|
+
'<main className="bg-surface-base-100">',
|
|
1602
|
+
' <h1 className="character-6-bold-pro text-text-negative-enabled">認証エラー</h1>',
|
|
1603
|
+
'</main>',
|
|
1604
|
+
'',
|
|
1605
|
+
'// ❌ Wrong — Tailwind 既定パレットの色を直接指定する',
|
|
1606
|
+
'<main className="bg-gray-50">',
|
|
1607
|
+
' <h1 className="character-6-bold-pro text-red-600">認証エラー</h1>',
|
|
1608
|
+
'</main>',
|
|
1609
|
+
'```',
|
|
1610
|
+
'',
|
|
1611
|
+
'Sparkle のプリミティブは Tailwind の同名変数をそのまま参照しているため',
|
|
1612
|
+
'(`--color-negative-600: var(--color-red-600)`)、`gray` / `red` / `green` / `yellow` / `blue`',
|
|
1613
|
+
'の 5 系統は**置き換えても値が変わらない**。それ以外(`slate` / `zinc` / `emerald` …)は',
|
|
1614
|
+
'色味が変わるので、移行先は候補として提示するだけで自動変換はしない。',
|
|
1615
|
+
'',
|
|
1616
|
+
'| if(状況) | then(移行先) |',
|
|
1617
|
+
'|---|---|',
|
|
1618
|
+
'| 背景に `bg-gray-100` | `bg-surface-base-100`(ページ地の専用トークン) |',
|
|
1619
|
+
'| 文字色に `text-red-600` | `text-text-negative-enabled` |',
|
|
1620
|
+
'| 枠線に `border-gray-200` | `border-border-neutral-*` |',
|
|
1621
|
+
'| アイコン色に `fill-red-600` | `fill-object-negative-enabled` |',
|
|
1622
|
+
'| `bg-blue-600` | ブランド色なら `surface-primary-*`、状態表示なら `surface-info-*`。**Figma を見て決める** |',
|
|
1623
|
+
'| `text-purple-500` など対応する意味が無い色 | 用途から選び直す。装飾用の面なら `bg-surface-accent-1` 〜 `3` |',
|
|
1624
|
+
'',
|
|
1625
|
+
'`surface-base-*` は `0` / `100` / `200` の 3 段しか無い。`bg-gray-50` のように対応する',
|
|
1626
|
+
'段が無いレベルは `surface-neutral-*` 側に案内される。**移行先は check の出力に従うこと**',
|
|
1627
|
+
'(この表は用途の考え方を示すもので、レベルごとの対応はコマンドが出す)。',
|
|
1628
|
+
'',
|
|
1629
|
+
'`text-neutral-*` は Sparkle の旧セマンティック層と同名なので、このルールではなく',
|
|
1630
|
+
'`legacy-color-token` が扱う(同じ箇所を二重に報告しないため)。',
|
|
1631
|
+
'',
|
|
1632
|
+
'このルールは **Sparkle を使っているファイルにだけ**適用される(Sparkle からの import か',
|
|
1633
|
+
'`character-*` の使用がある場合)。素の React コードや、Sparkle と無関係なユーティリティは対象外。',
|
|
1634
|
+
'',
|
|
1635
|
+
'検査対象は `.js` / `.jsx` / `.ts` / `.tsx` のみで、**`.css` の `@apply` は見ていない**',
|
|
1636
|
+
'(`legacy-color-token` は `.css` も見るので、そちらとは対象範囲が違う)。CSS 側に',
|
|
1637
|
+
'Tailwind 既定パレットの色が残っていないかは手で確認すること。',
|
|
1638
|
+
]),
|
|
1639
|
+
jsdocTargets: [],
|
|
1640
|
+
},
|
|
1508
1641
|
{
|
|
1509
1642
|
id: 'legacy-color-var',
|
|
1510
1643
|
check: {
|
|
@@ -1635,6 +1768,7 @@ const BUILTIN_CHECK_ORDER = [
|
|
|
1635
1768
|
// en: This only orders rule *evaluation*. Report ordering is by severity in
|
|
1636
1769
|
// check.js — don't read this list as a display-order guarantee.
|
|
1637
1770
|
'legacy-color-token',
|
|
1771
|
+
'tailwind-palette-color',
|
|
1638
1772
|
'legacy-color-var',
|
|
1639
1773
|
'deprecated-radius-alias',
|
|
1640
1774
|
'use-figma-spacing-scale',
|
package/lib/check.js
CHANGED
|
@@ -16,7 +16,9 @@ import { loadAntiPatternPlugins } from './load-plugins.js';
|
|
|
16
16
|
import { MATCH_HELPERS } from './plugin-helpers.js';
|
|
17
17
|
import { REGEX, FONT_DOMAINS } from './constants.js';
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// stop-hook 側も「target 未指定時の既定」を知る必要があるので export する。
|
|
20
|
+
// en: Exported so stop-hook can probe with the same default instead of hardcoding it.
|
|
21
|
+
export const DEFAULT_TARGET = 'src';
|
|
20
22
|
const TEXT_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
|
|
21
23
|
const CSS_EXTENSIONS = new Set(['.css']);
|
|
22
24
|
|
|
@@ -101,9 +103,28 @@ function toRelativeReportPath(filePath) {
|
|
|
101
103
|
/**
|
|
102
104
|
* ディレクトリを1回だけ走査し、テキストファイルと CSS ファイルを同時に収集する
|
|
103
105
|
*/
|
|
104
|
-
function collectFiles(targetPath, textFiles, cssFiles, visited) {
|
|
106
|
+
function collectFiles(targetPath, textFiles, cssFiles, visited, isUserTarget = false) {
|
|
105
107
|
if (!fs.existsSync(targetPath)) {
|
|
106
|
-
|
|
108
|
+
// cwd の案内を出してよいのは、ユーザーが渡した target を解決した 1 回目だけ。
|
|
109
|
+
// 再帰の途中で欠けるのは壊れた symlink や走査中の削除で、パスは既に絶対だから
|
|
110
|
+
// cwd は無関係。そこで「作業ディレクトリを確認してください」と言うと、
|
|
111
|
+
// 存在しない原因を追わせることになる。
|
|
112
|
+
// en: Only the user-supplied target can be a cwd problem. Deeper misses come
|
|
113
|
+
// from broken symlinks or concurrent deletion and are already absolute, so
|
|
114
|
+
// blaming cwd there sends the reader after a cause that does not exist.
|
|
115
|
+
if (!isUserTarget) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Target path does not exist: ${targetPath}\n` +
|
|
118
|
+
` 走査中にパスが解決できませんでした(壊れた symlink か、走査中に削除された可能性があります)。` +
|
|
119
|
+
` / Could not resolve this path while walking the tree (broken symlink or concurrent deletion).`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Target path does not exist: ${targetPath}\n` +
|
|
124
|
+
` 相対 path は現在の作業ディレクトリ(${process.cwd()})基準で解決しています。` +
|
|
125
|
+
`別のディレクトリから実行していないか確認してください。` +
|
|
126
|
+
` / Relative targets resolve against the current working directory.`
|
|
127
|
+
);
|
|
107
128
|
}
|
|
108
129
|
|
|
109
130
|
const realPath = fs.realpathSync(targetPath);
|
|
@@ -394,7 +415,7 @@ function createCheckReport(targets = [], options = {}) {
|
|
|
394
415
|
|
|
395
416
|
// 1回のディレクトリ走査でテキストファイルと CSS ファイルを同時に収集
|
|
396
417
|
for (const target of resolvedTargets) {
|
|
397
|
-
collectFiles(path.resolve(process.cwd(), target), textFiles, cssFiles, visited);
|
|
418
|
+
collectFiles(path.resolve(process.cwd(), target), textFiles, cssFiles, visited, true);
|
|
398
419
|
}
|
|
399
420
|
|
|
400
421
|
const checkedFiles = [...textFiles, ...cssFiles]
|
package/lib/load-plugins.js
CHANGED
|
@@ -113,6 +113,12 @@ export async function loadAntiPatternPlugins({ cwd = process.cwd() } = {}) {
|
|
|
113
113
|
validatePluginShape(candidate, `${packageName} (${entry})`);
|
|
114
114
|
groups.push(...candidate.groups);
|
|
115
115
|
record.groupIds = candidate.groups.map((group) => group.id);
|
|
116
|
+
// group の**実体**も持たせる。ID だけだと、別プラグインが同じ group ID を
|
|
117
|
+
// 宣言したときに所有者を復元できない(plugin-api は ID の名前空間が
|
|
118
|
+
// ビルトインとも他プラグインとも共有されると明記している)。
|
|
119
|
+
// en: Keep the group objects, not just ids — ids are not unique across
|
|
120
|
+
// plugins, so ownership can't be reconstructed from them afterwards.
|
|
121
|
+
record.groups = candidate.groups;
|
|
116
122
|
if (Array.isArray(candidate.manualReviewReminders)) {
|
|
117
123
|
reminders.push(...candidate.manualReviewReminders);
|
|
118
124
|
}
|
package/lib/path-utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
import path from 'path';
|
|
2
3
|
|
|
3
4
|
// setup.js 側と共通の「unsafe な shell メタ文字や quote」を弾く pattern。
|
|
@@ -64,3 +65,122 @@ export function assertSafeRelativePath(inputPath, label) {
|
|
|
64
65
|
}
|
|
65
66
|
return normalized;
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 相対 target を解決する基準ディレクトリの**候補**を、優先順に列挙する。
|
|
71
|
+
*
|
|
72
|
+
* 1. `startDir`(=通常は cwd)… 従来の挙動。ここで解決できるなら何も変えない
|
|
73
|
+
* 2. `CLAUDE_PROJECT_DIR` … Claude Code が hook 実行時に渡す絶対 path
|
|
74
|
+
* 3. `sparkle.config.json` を持つ祖先(近い順)… CLI 自身の設定ファイル
|
|
75
|
+
* 4. `package.json` を持つ祖先(近い順)… monorepo では workspace → repo root の順
|
|
76
|
+
*
|
|
77
|
+
* monorepo で「最も近い package.json」だけを見ると、cwd が `apps/web` のときに
|
|
78
|
+
* 基準も `apps/web` になり、hook に書かれた `apps/web/app` が二重化するという
|
|
79
|
+
* issue #85 と同じ壊れ方を再現してしまう。だから 1 つに決め打たず、repo root まで
|
|
80
|
+
* 含めて候補を並べ、呼び出し側が「target が実在するか」で選べるようにする。
|
|
81
|
+
*
|
|
82
|
+
* en: Enumerate candidate base directories in priority order. Picking a single
|
|
83
|
+
* "project root" is not enough: in a monorepo the nearest package.json is the
|
|
84
|
+
* workspace package, which reproduces the very bug this is meant to fix.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} [startDir]
|
|
87
|
+
* @param {{ existsSync?: (p: string) => boolean, env?: Record<string, string> }} [deps]
|
|
88
|
+
* @returns {Array<{ dir: string, source: string }>} 重複を除いた候補(優先順)
|
|
89
|
+
*/
|
|
90
|
+
export function projectRootCandidates(startDir = process.cwd(), deps = {}) {
|
|
91
|
+
const exists = deps.existsSync ?? fs.existsSync;
|
|
92
|
+
const env = deps.env ?? process.env;
|
|
93
|
+
const candidates = [{ dir: path.resolve(startDir), source: 'cwd' }];
|
|
94
|
+
|
|
95
|
+
const fromEnv = env.CLAUDE_PROJECT_DIR;
|
|
96
|
+
if (typeof fromEnv === 'string' && fromEnv.trim() && exists(fromEnv)) {
|
|
97
|
+
candidates.push({ dir: path.resolve(fromEnv), source: 'CLAUDE_PROJECT_DIR' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const marker of PROJECT_ROOT_MARKERS) {
|
|
101
|
+
for (const dir of ancestorsContaining(path.resolve(startDir), marker, exists)) {
|
|
102
|
+
candidates.push({ dir, source: marker });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
return candidates.filter(({ dir }) => {
|
|
108
|
+
if (seen.has(dir)) return false;
|
|
109
|
+
seen.add(dir);
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `targets` がすべて実在する最初の候補ディレクトリを選ぶ。
|
|
116
|
+
*
|
|
117
|
+
* hook は AI の作業途中に発火するため、実行時の cwd を前提にできない。AI が調査で
|
|
118
|
+
* `cd apps/web` したまま戻していないと `apps/web/app` が `apps/web/apps/web/app` に
|
|
119
|
+
* 解決されて落ちる(issue #85)。候補を順に当てて実在するものを採れば、配布済みの
|
|
120
|
+
* `.claude/settings.json` を書き換えずに直る。
|
|
121
|
+
*
|
|
122
|
+
* cwd を最優先に置いているので、**今まで動いていた呼び出しの挙動は一切変わらない**。
|
|
123
|
+
* どの候補でも解決できないときも cwd を返し、エラーメッセージは呼び出し側
|
|
124
|
+
* (`check` の "Target path does not exist")に任せる。
|
|
125
|
+
*
|
|
126
|
+
* en: Pick the first candidate base where every target exists. cwd comes first,
|
|
127
|
+
* so anything that already worked keeps working; the fallback only kicks in for
|
|
128
|
+
* the broken case this exists to fix.
|
|
129
|
+
*
|
|
130
|
+
* @param {string[]} targets 相対 path の配列(空なら cwd を返す)
|
|
131
|
+
* @param {{ startDir?: string, existsSync?: (p: string) => boolean, env?: Record<string, string> }} [options]
|
|
132
|
+
* @returns {{ dir: string, source: string, resolved: boolean }}
|
|
133
|
+
*/
|
|
134
|
+
export function resolveTargetBaseDir(targets, options = {}) {
|
|
135
|
+
const startDir = path.resolve(options.startDir ?? process.cwd());
|
|
136
|
+
const exists = options.existsSync ?? fs.existsSync;
|
|
137
|
+
const fallback = { dir: startDir, source: 'cwd', resolved: false };
|
|
138
|
+
|
|
139
|
+
if (!Array.isArray(targets) || targets.length === 0) return fallback;
|
|
140
|
+
|
|
141
|
+
for (const candidate of projectRootCandidates(startDir, options)) {
|
|
142
|
+
if (targets.every((target) => exists(path.resolve(candidate.dir, target)))) {
|
|
143
|
+
return { ...candidate, resolved: true };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return fallback;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const PROJECT_ROOT_MARKERS = ['sparkle.config.json', 'package.json'];
|
|
151
|
+
|
|
152
|
+
// 探索を止める境界。ここより上は「別のプロジェクト」とみなす。
|
|
153
|
+
//
|
|
154
|
+
// 境界を設けないと、プロジェクトが別の `package.json` を持つディレクトリの下に
|
|
155
|
+
// 置かれている場合(`/workspace/package.json` の下に `/workspace/project/`)、
|
|
156
|
+
// **target 名を打ち間違えたときに外側の同名 path が拾われて、無関係なファイルを
|
|
157
|
+
// 黙って検査する**。「target が無い」と報告されるべき場面で静かに別の場所を見る
|
|
158
|
+
// のは、このルール一式が防ごうとしている silent failure そのもの。
|
|
159
|
+
//
|
|
160
|
+
// git worktree では `.git` がファイル(`gitdir:` を書いた 1 行)になるが、
|
|
161
|
+
// `existsSync` はどちらでも true を返すので同じく境界として機能する。
|
|
162
|
+
// en: Bound the walk at the repository root. Without it, a typo'd target could
|
|
163
|
+
// resolve against an unrelated outer project and be checked silently.
|
|
164
|
+
const REPOSITORY_BOUNDARY = '.git';
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* `startDir` から root 方向へ辿り、`marker` を含むディレクトリを近い順に返す。
|
|
168
|
+
* リポジトリ境界(`.git`)を含むディレクトリまで見たら、そこで打ち切る。
|
|
169
|
+
* en: All ancestors (nearest first) that contain marker, stopping at the repo root.
|
|
170
|
+
*/
|
|
171
|
+
function ancestorsContaining(startDir, marker, exists) {
|
|
172
|
+
const found = [];
|
|
173
|
+
let current = startDir;
|
|
174
|
+
// path.dirname('/') === '/' なので、変化しなくなった時点が終端。
|
|
175
|
+
// en: dirname stops changing at the filesystem root — that's the loop guard.
|
|
176
|
+
for (;;) {
|
|
177
|
+
if (exists(path.join(current, marker))) found.push(current);
|
|
178
|
+
// 境界ディレクトリ自身は候補に含めてから打ち切る(repo root がまさに
|
|
179
|
+
// 探している基準であることが多いため)。
|
|
180
|
+
// en: Include the boundary directory itself, then stop.
|
|
181
|
+
if (exists(path.join(current, REPOSITORY_BOUNDARY))) return found;
|
|
182
|
+
const parent = path.dirname(current);
|
|
183
|
+
if (parent === current) return found;
|
|
184
|
+
current = parent;
|
|
185
|
+
}
|
|
186
|
+
}
|
package/lib/rules-report.js
CHANGED
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
} from './anti-pattern-rules.js';
|
|
28
28
|
import { loadAntiPatternPlugins } from './load-plugins.js';
|
|
29
29
|
|
|
30
|
+
/** 利用者の手元に `docs/` は無いので、案内は解決できる場所を指す。 */
|
|
31
|
+
const DOCS_URL = 'https://github.com/goodpatch/sparkle-design-cli/blob/main/docs';
|
|
32
|
+
|
|
30
33
|
/** 表示順。`check` のレポートと同じ「重いものが先」に揃える。 */
|
|
31
34
|
const SEVERITY_ORDER = [SEVERITY.ERROR, SEVERITY.WARNING, SEVERITY.INFO];
|
|
32
35
|
|
|
@@ -74,7 +77,24 @@ export async function collectActiveRules(options = {}) {
|
|
|
74
77
|
...getCheckRules(pluginGroups).map(shape('plugin')),
|
|
75
78
|
];
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
// 各プラグインが実際に出した rule は、**そのプラグインの group から直接**求める。
|
|
81
|
+
// 「全 rule の中に同じ id があるか」で絞ると、`check` を持たない group の id が
|
|
82
|
+
// 組み込みや別プラグインの rule id と衝突したときに、そのプラグインが所有して
|
|
83
|
+
// いない ID を並べてしまう(`check` は plugin-api で optional なので実際に起きる)。
|
|
84
|
+
// en: Derive each plugin's rule ids from its own groups. Matching against the
|
|
85
|
+
// merged list would attribute a built-in (or another plugin's) rule to a plugin
|
|
86
|
+
// whose `check`-less group merely reuses that id.
|
|
87
|
+
// 所有関係はローダーが持つ `record.groups`(group の実体)から取る。
|
|
88
|
+
// ID で引き直すと、別プラグインが同じ group ID を宣言したときに互いの分まで
|
|
89
|
+
// 拾ってしまう(実測で両方が 2 件ずつ持つ状態になった)。
|
|
90
|
+
// en: Ownership comes from the loader's group objects. Re-deriving it from ids
|
|
91
|
+
// credits each plugin with the other's rules when two declare the same id.
|
|
92
|
+
const plugins = discovered.map((record) => ({
|
|
93
|
+
...record,
|
|
94
|
+
ruleIds: getCheckRules(record.groups ?? []).map((rule) => rule.id),
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
return { rules, plugins };
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
function severityRank(severity) {
|
|
@@ -116,7 +136,11 @@ export function renderRulesJson({ rules, plugins }) {
|
|
|
116
136
|
packageName: record.packageName,
|
|
117
137
|
status: record.status,
|
|
118
138
|
error: record.error ?? null,
|
|
119
|
-
ruleIds
|
|
139
|
+
// `ruleIds` は collectActiveRules がプラグインごとに算出したもの
|
|
140
|
+
// (`check` を持つ group だけ)。`groupIds` は宣言した group 全部。
|
|
141
|
+
// en: `ruleIds` counts only groups that actually define a check.
|
|
142
|
+
ruleIds: record.ruleIds ?? [],
|
|
143
|
+
groupIds: record.groupIds ?? [],
|
|
120
144
|
})),
|
|
121
145
|
},
|
|
122
146
|
null,
|
|
@@ -151,11 +175,17 @@ export function renderRulesText({ rules, plugins }) {
|
|
|
151
175
|
),
|
|
152
176
|
];
|
|
153
177
|
if (clashes.length > 0) {
|
|
178
|
+
// 衝突は「組み込み × プラグイン」だけでなく「プラグイン同士」でも起きる
|
|
179
|
+
// (plugin-api が名前空間を共有すると明記している)。source を見ずに
|
|
180
|
+
// 「組み込みと同じ」と決め打つと、後者で嘘の案内になる。
|
|
181
|
+
// en: Clashes also happen plugin-vs-plugin; don't assume a built-in is involved.
|
|
154
182
|
lines.push('');
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
'
|
|
158
|
-
|
|
183
|
+
for (const id of clashes) {
|
|
184
|
+
const sources = rules.filter((rule) => rule.id === id).map((rule) => rule.source);
|
|
185
|
+
const kind = sources.includes('builtin') ? '組み込みと同じ ID' : 'プラグイン同士で同じ ID';
|
|
186
|
+
lines.push(`⚠️ ${kind}のルールがあります: ${id}(${sources.join(' + ')})`);
|
|
187
|
+
}
|
|
188
|
+
lines.push(' どちらも実行されます。指摘の出所が分からなくなるので ID を変えてください。');
|
|
159
189
|
}
|
|
160
190
|
|
|
161
191
|
lines.push('');
|
|
@@ -169,7 +199,12 @@ export function renderRulesText({ rules, plugins }) {
|
|
|
169
199
|
}
|
|
170
200
|
}
|
|
171
201
|
lines.push('');
|
|
172
|
-
|
|
202
|
+
// 利用者の cwd に `docs/` は無い。npm 同梱の実体か GitHub を指す。
|
|
203
|
+
// en: `docs/` doesn't exist in the consumer's cwd — point at the real locations.
|
|
204
|
+
lines.push(
|
|
205
|
+
`個別ルールの背景と対処: ${DOCS_URL}/anti-patterns.md` +
|
|
206
|
+
'(npm 経由なら node_modules/sparkle-design-cli/docs/anti-patterns.md)'
|
|
207
|
+
);
|
|
173
208
|
lines.push('抑制するには `sparkle-disable-next-line <rule-id>` を使います。');
|
|
174
209
|
|
|
175
210
|
return lines.join('\n');
|
package/lib/stop-hook.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_TARGET, countBySeverity, runCheck } from './check.js';
|
|
4
|
+
import { resolveTargetBaseDir } from './path-utils.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* AI assistant の Stop / stop hook 用エントリポイント。
|
|
@@ -14,10 +16,21 @@ import { countBySeverity, runCheck } from './check.js';
|
|
|
14
16
|
* 抜ける。これで「最初の 1 回だけ exit 2 で停止をブロックして findings
|
|
15
17
|
* を通知し、以降はユーザーの判断に委ねる」フローになる。
|
|
16
18
|
*
|
|
19
|
+
* 相対 path は cwd で解決できなければ**プロジェクトルート基準で解決し直す**。
|
|
20
|
+
* hook は AI の作業途中に発火するため、AI が調査で `cd` したまま戻していない
|
|
21
|
+
* ケースが普通に起きる(issue #85)。cwd を最優先で試すので、今まで動いていた
|
|
22
|
+
* 呼び出しの挙動は変わらない。`check` サブコマンドは人間が直接叩くものなので
|
|
23
|
+
* 従来どおり cwd 基準のみで、この救済は stop-hook 限定。
|
|
24
|
+
*
|
|
17
25
|
* en: Run `check --strict` once per session to surface findings, but stop
|
|
18
26
|
* blocking on subsequent invocations within the same Stop hook chain.
|
|
19
27
|
* Claude Code sets `stop_hook_active: true` on re-fires so the hook can
|
|
20
28
|
* exit cleanly instead of looping forever (see Claude Code hooks docs).
|
|
29
|
+
* Relative targets fall back to the project root when cwd cannot resolve them,
|
|
30
|
+
* because hooks fire mid-session when the shell may sit in a subdirectory.
|
|
31
|
+
*
|
|
32
|
+
* @param {string | string[]} [target] lint 対象 path(複数可)
|
|
33
|
+
* @returns {Promise<0 | 2>} hook の exit code
|
|
21
34
|
*/
|
|
22
35
|
export async function runStopHook(target) {
|
|
23
36
|
const payload = readStdinJsonSafely();
|
|
@@ -31,8 +44,20 @@ export async function runStopHook(target) {
|
|
|
31
44
|
return 0;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
const targets = target
|
|
35
|
-
const
|
|
47
|
+
const targets = normalizeTargets(target);
|
|
48
|
+
const restoreCwd = enterTargetBaseDir(targets);
|
|
49
|
+
|
|
50
|
+
let result;
|
|
51
|
+
try {
|
|
52
|
+
result = await runCheck(targets, { strict: true, format: 'text' });
|
|
53
|
+
} finally {
|
|
54
|
+
// check が throw しても cwd は必ず戻す。runStopHook は bin から呼ばれて
|
|
55
|
+
// すぐ process.exit する経路が主だが、テストや将来の埋め込み利用で
|
|
56
|
+
// プロセスが生き続ける場合に cwd を汚したままにしない。
|
|
57
|
+
// en: Always restore cwd, even when the check throws.
|
|
58
|
+
restoreCwd();
|
|
59
|
+
}
|
|
60
|
+
const { report, blocked } = result;
|
|
36
61
|
|
|
37
62
|
if (blocked) {
|
|
38
63
|
// ブロック理由は「error の findings」と「実行に失敗して未検査のルール」の
|
|
@@ -84,6 +109,94 @@ export async function runStopHook(target) {
|
|
|
84
109
|
return 0;
|
|
85
110
|
}
|
|
86
111
|
|
|
112
|
+
/**
|
|
113
|
+
* hook に渡された lint 対象を配列に正規化する。
|
|
114
|
+
*
|
|
115
|
+
* 2.5.0-beta.1 までは `bin/sparkle-design.js` が `args[1]` しか渡しておらず、
|
|
116
|
+
* 実運用で見られる `stop-hook apps/web/app apps/web/components` の**2つ目以降が
|
|
117
|
+
* 黙って未検査**になっていた(issue #85 の調査で判明)。無指定なら `runCheck`
|
|
118
|
+
* 側の既定 target(`src`)に委ねるので、空配列をそのまま返す。
|
|
119
|
+
*
|
|
120
|
+
* en: Accept multiple targets. Before this, only the first positional arg was
|
|
121
|
+
* forwarded, so extra paths in a hook command were silently never checked.
|
|
122
|
+
*/
|
|
123
|
+
function normalizeTargets(target) {
|
|
124
|
+
const list = Array.isArray(target) ? target : [target];
|
|
125
|
+
return list
|
|
126
|
+
.filter((value) => typeof value === 'string' && value.trim())
|
|
127
|
+
.map((value) => value.trim());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* target が実在する基準ディレクトリへ `chdir` し、元に戻す関数を返す。
|
|
132
|
+
*
|
|
133
|
+
* target path だけを解決し直すのでは足りない。`check` は cwd に依存する処理を
|
|
134
|
+
* 他にも持っているので、target だけずらすとそれらが元の cwd を見たままになる。
|
|
135
|
+
*
|
|
136
|
+
* - **アンチパターンプラグインの discovery**(`loadAntiPatternPlugins` が
|
|
137
|
+
* `cwd/package.json` の `sparkleCli.antiPatterns` を読む)。ここがずれると
|
|
138
|
+
* 「target は repo root のファイルを見ているのに、有効なルールは apps/web 基準」
|
|
139
|
+
* という、**検査したのに一部のルールが効いていない**状態になる
|
|
140
|
+
* - Next.js の CSP 設定検出(`cwd` から `next.config.*` を探す)
|
|
141
|
+
* - レポートの相対 path 表示(`path.relative(process.cwd(), ...)`)
|
|
142
|
+
*
|
|
143
|
+
* cwd ごと合わせるのが最も整合的。
|
|
144
|
+
*
|
|
145
|
+
* `resolveTargetBaseDir` は cwd を最優先で試すので、**今まで通り動いていたケースは
|
|
146
|
+
* chdir されず挙動が変わらない**。移動したときだけ stderr に 1 行出す。黙って別の
|
|
147
|
+
* ディレクトリを検査していると、findings の差分を追うときに原因が分からなくなる。
|
|
148
|
+
*
|
|
149
|
+
* `process.chdir()` はプロセス全体の状態なので、**1 プロセスにつき 1 回だけ
|
|
150
|
+
* `runStopHook` を呼ぶ**ことが前提。`bin/sparkle-design.js` は subcommand を 1 つ
|
|
151
|
+
* 処理して `process.exit` するのでこの前提は満たされている。将来 `runStopHook` を
|
|
152
|
+
* ライブラリとして並行に呼ぶ用途が出たら、cwd を動かさずに基準ディレクトリを
|
|
153
|
+
* `runCheck` へ引数で渡す形(`check` 側の cwd 依存を全部剥がす)に作り替えること。
|
|
154
|
+
*
|
|
155
|
+
* en: chdir to the base where the targets actually exist, rather than only
|
|
156
|
+
* rebasing target paths — the check pipeline reads config, CSP settings and
|
|
157
|
+
* report paths from cwd too. cwd is tried first, so working setups don't move.
|
|
158
|
+
* chdir is process-global, so this assumes one runStopHook per process — true
|
|
159
|
+
* for the CLI, which exits right after. Concurrent library use would need the
|
|
160
|
+
* base directory threaded through runCheck instead.
|
|
161
|
+
*/
|
|
162
|
+
function enterTargetBaseDir(targets) {
|
|
163
|
+
const originalCwd = process.cwd();
|
|
164
|
+
// target 未指定なら check 側の既定 target で探る。ここで `src` を決め打ちすると
|
|
165
|
+
// 既定が変わったときに静かにズレるので、check.js の定数をそのまま使う。
|
|
166
|
+
// en: Probe with check's own default target instead of hardcoding 'src'.
|
|
167
|
+
const probe = targets.length > 0 ? targets : [DEFAULT_TARGET];
|
|
168
|
+
const { dir, source, resolved } = resolveTargetBaseDir(probe, { startDir: originalCwd });
|
|
169
|
+
|
|
170
|
+
if (!resolved || dir === originalCwd) return () => {};
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
process.chdir(dir);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// chdir に失敗しても検査自体は続ける価値がある(従来どおり cwd 基準)。
|
|
176
|
+
// en: Fall back to cwd-relative behaviour instead of aborting the check.
|
|
177
|
+
process.stderr.write(
|
|
178
|
+
`sparkle-design-cli stop-hook: ${dir} へ移動できなかったため cwd 基準で検査します (${error.code ?? error.message})` +
|
|
179
|
+
` / Could not chdir to the resolved base dir; falling back to cwd.\n`
|
|
180
|
+
);
|
|
181
|
+
return () => {};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
`sparkle-design-cli stop-hook: 実行時の cwd (${originalCwd}) では ${probe.join(' / ')} が見つからないため、` +
|
|
186
|
+
`${dir} 基準で検査します(${source} から判定)。` +
|
|
187
|
+
` / Resolved relative targets against ${dir} instead of cwd.\n`
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return () => {
|
|
191
|
+
try {
|
|
192
|
+
process.chdir(originalCwd);
|
|
193
|
+
} catch {
|
|
194
|
+
// 元の cwd が消えている場合まで面倒は見ない。
|
|
195
|
+
// en: Ignore — the original cwd may no longer exist.
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
87
200
|
function readStdinJsonSafely() {
|
|
88
201
|
// stdin が tty / 空 / 非 JSON の場合は「初回呼び出し」として扱う。
|
|
89
202
|
// en: Treat missing or non-JSON stdin as a first invocation.
|
package/lib/token-migration.js
CHANGED
|
@@ -606,6 +606,216 @@ export function buildLegacyCssVarPattern() {
|
|
|
606
606
|
);
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
// --- Tailwind 既定パレット -> Sparkle セマンティックトークン ------------------
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Tailwind 既定パレットのファミリー → Sparkle の意味ファミリー。
|
|
613
|
+
*
|
|
614
|
+
* Sparkle のプリミティブは Tailwind の同名変数をそのまま参照している
|
|
615
|
+
* (`--color-neutral-600: var(--color-gray-600)` / `--color-negative-600:
|
|
616
|
+
* var(--color-red-600)` など)。そのため `gray` / `red` / `green` / `yellow` /
|
|
617
|
+
* `blue` の 5 系統は**値が完全に一致する**ので、置き換えても見た目が変わらない。
|
|
618
|
+
* それ以外(`slate` / `zinc` / `emerald` …)は近いだけで色味が変わるため、
|
|
619
|
+
* 自動変換可とは扱わず必ず要判断にする。
|
|
620
|
+
*
|
|
621
|
+
* - `family`: 対応する旧セマンティックファミリー。`null` は対応する意味が無い
|
|
622
|
+
* - `exact`: Sparkle のプリミティブが同じ値を指しているか
|
|
623
|
+
* - `ambiguousWith`: 用途上もう一方の意味も有力な場合(ブランド色かどうかは
|
|
624
|
+
* コードからは判断できないため、候補を両方出して人に選ばせる)
|
|
625
|
+
*
|
|
626
|
+
* `neutral` は**意図的に載せていない**。Tailwind の既定パレットにも Sparkle の
|
|
627
|
+
* 旧セマンティック層にも同名のファミリーがあり、`text-neutral-500` は
|
|
628
|
+
* `legacy-color-token` がすでに検出する。ここにも載せると同じ箇所が 2 回報告される。
|
|
629
|
+
* en: `neutral` is deliberately absent — it collides with Sparkle's own legacy
|
|
630
|
+
* family name and is already covered by `legacy-color-token`.
|
|
631
|
+
*
|
|
632
|
+
* `gray` を `neutral` ではなく `base` に寄せているのは、`base` が「gray と同値」
|
|
633
|
+
* かつ surface では Figma 専用の `surface/base/*`(ページ地)が優先されるため。
|
|
634
|
+
* text / border / object には `base` が無いので `neutral` に自動フォールバックする
|
|
635
|
+
* (`familyScaleMap` 参照)。
|
|
636
|
+
*/
|
|
637
|
+
export const TAILWIND_PALETTE_FAMILIES = {
|
|
638
|
+
// 値まで一致する系統
|
|
639
|
+
gray: { family: 'base', exact: true },
|
|
640
|
+
red: { family: 'negative', exact: true },
|
|
641
|
+
green: { family: 'success', exact: true },
|
|
642
|
+
yellow: { family: 'warning', exact: true },
|
|
643
|
+
blue: { family: 'info', exact: true, ambiguousWith: 'primary' },
|
|
644
|
+
// 意味は同じだが色味が変わる系統
|
|
645
|
+
slate: { family: 'base', exact: false },
|
|
646
|
+
zinc: { family: 'base', exact: false },
|
|
647
|
+
stone: { family: 'base', exact: false },
|
|
648
|
+
rose: { family: 'negative', exact: false },
|
|
649
|
+
emerald: { family: 'success', exact: false },
|
|
650
|
+
teal: { family: 'success', exact: false },
|
|
651
|
+
lime: { family: 'success', exact: false },
|
|
652
|
+
amber: { family: 'warning', exact: false },
|
|
653
|
+
orange: { family: 'warning', exact: false },
|
|
654
|
+
sky: { family: 'info', exact: false, ambiguousWith: 'primary' },
|
|
655
|
+
cyan: { family: 'info', exact: false, ambiguousWith: 'primary' },
|
|
656
|
+
indigo: { family: 'info', exact: false, ambiguousWith: 'primary' },
|
|
657
|
+
// 対応する意味が無い系統
|
|
658
|
+
violet: { family: null },
|
|
659
|
+
purple: { family: null },
|
|
660
|
+
fuchsia: { family: null },
|
|
661
|
+
pink: { family: null },
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Tailwind v4 のパレットは 950 まである。Sparkle の旧セマンティック層は 900 止まり
|
|
666
|
+
* なので、950 は機械的な対応先が無い(要判断として報告する)。
|
|
667
|
+
*/
|
|
668
|
+
const TAILWIND_ONLY_LEVELS = ['950'];
|
|
669
|
+
const TAILWIND_PALETTE_LEVELS = [...LEGACY_LEVELS, ...TAILWIND_ONLY_LEVELS];
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* `bg-gray-50` / `hover:text-red-600/50` のような Tailwind 既定パレットの
|
|
673
|
+
* 色ユーティリティを検出する正規表現。
|
|
674
|
+
*
|
|
675
|
+
* `legacy-color-token` の `buildLegacyScalePattern` と同じ境界・variant・modifier
|
|
676
|
+
* の扱いを共有する(`\b` ではなく `[\w-]` 境界を使う理由は同関数のコメント参照)。
|
|
677
|
+
*/
|
|
678
|
+
export function buildTailwindPalettePattern() {
|
|
679
|
+
const prefixes = alternation(Object.keys(UTILITY_PREFIX_TO_CATEGORY));
|
|
680
|
+
const families = alternation(Object.keys(TAILWIND_PALETTE_FAMILIES));
|
|
681
|
+
const levels = alternation(TAILWIND_PALETTE_LEVELS);
|
|
682
|
+
return new RegExp(
|
|
683
|
+
`${NOT_CLASS_CHAR_BEFORE}${VARIANT_CHAIN}${IMPORTANT_PREFIX}(?:${prefixes})-(?:${families})-(?:${levels})${NOT_CLASS_CHAR_AFTER}${MODIFIER_SUFFIX}`,
|
|
684
|
+
'g'
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Tailwind 既定パレットのユーティリティ 1 個を解析して移行結果を返す。
|
|
690
|
+
*
|
|
691
|
+
* 旧セマンティックファミリーに読み替えてから `resolveLegacyUtility` に委譲する。
|
|
692
|
+
* 対応表・状態の絞り込み・opacity modifier の持ち回りを二重に実装せずに済み、
|
|
693
|
+
* `legacy-color-token` と同じ移行先を必ず案内できる。
|
|
694
|
+
*
|
|
695
|
+
* ただし classification は**そのまま通さない**。
|
|
696
|
+
* - 値が一致しない系統(`slate` など)は見た目が変わるので必ず `review`
|
|
697
|
+
* - `info` / `primary` のどちらとも読める系統(`blue` など)は、ブランド色かどうかが
|
|
698
|
+
* コードから判断できないので候補を両方出して `review`
|
|
699
|
+
*
|
|
700
|
+
* en: Rewrite the Tailwind family to Sparkle's legacy family and delegate, so the
|
|
701
|
+
* mapping table, state narrowing and modifier handling are never duplicated.
|
|
702
|
+
* Classification is downgraded to `review` whenever the swap is not value-preserving
|
|
703
|
+
* or the semantic (info vs primary) cannot be decided from the code.
|
|
704
|
+
*
|
|
705
|
+
* @param {string} utility 例 `bg-gray-50` / `hover:text-red-600/50`
|
|
706
|
+
* @returns {null | { input: string, classification: string, replacement?: string, candidates?: string[], reason?: string }}
|
|
707
|
+
*/
|
|
708
|
+
export function resolveTailwindPaletteUtility(utility) {
|
|
709
|
+
if (typeof utility !== 'string' || utility.length === 0) return null;
|
|
710
|
+
|
|
711
|
+
const lastColon = utility.lastIndexOf(':');
|
|
712
|
+
const variantPrefix = lastColon === -1 ? '' : utility.slice(0, lastColon + 1);
|
|
713
|
+
let rawBare = lastColon === -1 ? utility : utility.slice(lastColon + 1);
|
|
714
|
+
|
|
715
|
+
const importantPrefix = rawBare.startsWith('!') ? '!' : '';
|
|
716
|
+
if (importantPrefix) rawBare = rawBare.slice(1);
|
|
717
|
+
|
|
718
|
+
const [, bare, modifier] = MODIFIER_SUFFIX_PATTERN.exec(rawBare);
|
|
719
|
+
|
|
720
|
+
const dashIndex = bare.indexOf('-');
|
|
721
|
+
if (dashIndex === -1) return null;
|
|
722
|
+
const prefix = bare.slice(0, dashIndex);
|
|
723
|
+
const rest = bare.slice(dashIndex + 1);
|
|
724
|
+
if (!UTILITY_PREFIX_TO_CATEGORY[prefix]) return null;
|
|
725
|
+
|
|
726
|
+
const scaleMatch = /^([a-z]+)-(\d{2,3})$/.exec(rest);
|
|
727
|
+
if (!scaleMatch) return null;
|
|
728
|
+
const [, paletteFamily, level] = scaleMatch;
|
|
729
|
+
|
|
730
|
+
const mapping = TAILWIND_PALETTE_FAMILIES[paletteFamily];
|
|
731
|
+
if (!mapping || !TAILWIND_PALETTE_LEVELS.includes(level)) return null;
|
|
732
|
+
|
|
733
|
+
const wrap = (result) => ({ input: utility, ...result });
|
|
734
|
+
|
|
735
|
+
if (!mapping.family) {
|
|
736
|
+
return wrap({
|
|
737
|
+
classification: MIGRATION_CLASS.REVIEW,
|
|
738
|
+
reason:
|
|
739
|
+
`${paletteFamily} に対応する意味のトークンが Sparkle にありません。` +
|
|
740
|
+
'Figma の該当箇所を見て用途(surface / text / border / object)から選び直してください。' +
|
|
741
|
+
'装飾目的の面であれば surface-accent-1 / 2 / 3 が候補になります。',
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (TAILWIND_ONLY_LEVELS.includes(level)) {
|
|
746
|
+
return wrap({
|
|
747
|
+
classification: MIGRATION_CLASS.REVIEW,
|
|
748
|
+
reason:
|
|
749
|
+
`Tailwind の ${level} に対応するレベルが Sparkle にありません(Sparkle は 900 まで)。` +
|
|
750
|
+
`${paletteFamily}-900 相当で足りるかを Figma で確認してください。`,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const delegate = (family) =>
|
|
755
|
+
resolveLegacyUtility(
|
|
756
|
+
`${variantPrefix}${importantPrefix}${prefix}-${family}-${level}${modifier}`
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
const primary = delegate(mapping.family);
|
|
760
|
+
if (!primary) return null;
|
|
761
|
+
|
|
762
|
+
// 用途上どちらの意味とも読める系統は、候補を両方並べて人に選ばせる。
|
|
763
|
+
// en: Surface both semantics when the code cannot tell brand colour from status.
|
|
764
|
+
if (mapping.ambiguousWith) {
|
|
765
|
+
const sides = [
|
|
766
|
+
{ family: mapping.family, result: primary },
|
|
767
|
+
{ family: mapping.ambiguousWith, result: delegate(mapping.ambiguousWith) },
|
|
768
|
+
];
|
|
769
|
+
// 片側に該当トークンが無いことがある(例: surface の info には 600 が無い)。
|
|
770
|
+
// 候補を並べただけでは「なぜ片方しか出ないのか」が分からず、AI が
|
|
771
|
+
// 残った候補を唯一の正解と誤解するので、欠けている側を明示する。
|
|
772
|
+
// en: One side can have no matching token; say so, or the remaining
|
|
773
|
+
// candidate reads as the single correct answer.
|
|
774
|
+
const missing = sides.filter((side) => toCandidateList(side.result).length === 0);
|
|
775
|
+
return wrap({
|
|
776
|
+
classification: MIGRATION_CLASS.REVIEW,
|
|
777
|
+
candidates: sides.flatMap((side) => toCandidateList(side.result)),
|
|
778
|
+
reason:
|
|
779
|
+
`${paletteFamily} は「${mapping.family}(状態・意味を表す色)」とも「${mapping.ambiguousWith}(ブランド色)」とも読めます。` +
|
|
780
|
+
'どちらの用途かはコードから判断できないため、Figma の該当箇所を見て選んでください。' +
|
|
781
|
+
(missing.length > 0
|
|
782
|
+
? `${missing.map((side) => side.family).join(' / ')} 側には対応するトークンがありません。`
|
|
783
|
+
: '') +
|
|
784
|
+
(mapping.exact ? '' : `なお ${paletteFamily} と Sparkle の色は完全には一致しません。`),
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (!mapping.exact) {
|
|
789
|
+
return wrap({
|
|
790
|
+
classification: MIGRATION_CLASS.REVIEW,
|
|
791
|
+
candidates: toCandidateList(primary),
|
|
792
|
+
reason:
|
|
793
|
+
`${paletteFamily} は Sparkle の ${mapping.family} に意味は対応しますが、色が完全には一致しません(置き換えると見た目が変わります)。` +
|
|
794
|
+
'Figma の該当箇所で意図した色かを確認してください。' +
|
|
795
|
+
(primary.reason ? ` ${primary.reason}` : ''),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// 値が一致する系統は、旧トークンからの移行と同じ確度で案内できる。
|
|
800
|
+
// en: Value-identical families keep the delegated classification as-is.
|
|
801
|
+
return wrap({
|
|
802
|
+
classification: primary.classification,
|
|
803
|
+
replacement: primary.replacement,
|
|
804
|
+
candidates: primary.candidates,
|
|
805
|
+
reason: primary.reason,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* `resolveLegacyUtility` の戻り値を候補リストに正規化する。
|
|
811
|
+
* en: Flatten a delegated result into a candidate list.
|
|
812
|
+
*/
|
|
813
|
+
function toCandidateList(result) {
|
|
814
|
+
if (!result) return [];
|
|
815
|
+
if (result.replacement) return [result.replacement];
|
|
816
|
+
return result.candidates ?? [];
|
|
817
|
+
}
|
|
818
|
+
|
|
609
819
|
/**
|
|
610
820
|
* CSS 変数名 1 個を解析して移行結果を返す。
|
|
611
821
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparkle-design-cli",
|
|
3
|
-
"version": "2.5.0-beta.
|
|
3
|
+
"version": "2.5.0-beta.2",
|
|
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",
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"test": "node --test test/*.test.js",
|
|
21
21
|
"test:watch": "node --test --watch test/*.test.js",
|
|
22
|
-
"lint": "eslint bin/ lib/ test/",
|
|
23
|
-
"lint:fix": "eslint bin/ lib/ test/ --fix",
|
|
24
|
-
"format": "prettier --write bin/ lib/ test/ *.md *.json",
|
|
25
|
-
"format:check": "prettier --check bin/ lib/ test/ *.md *.json",
|
|
22
|
+
"lint": "eslint bin/ lib/ scripts/ test/",
|
|
23
|
+
"lint:fix": "eslint bin/ lib/ scripts/ test/ --fix",
|
|
24
|
+
"format": "prettier --write bin/ lib/ scripts/ test/ docs/ \"*.md\" \"*.json\"",
|
|
25
|
+
"format:check": "prettier --check bin/ lib/ scripts/ test/ docs/ \"*.md\" \"*.json\"",
|
|
26
26
|
"sync:anti-pattern-docs": "node scripts/sync-anti-pattern-docs.mjs"
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|