sparkle-design-cli 2.4.2 → 2.5.0-beta.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/CONTRIBUTING.md +51 -0
- package/README.md +44 -500
- package/bin/sparkle-design.js +59 -22
- package/docs/anti-patterns.md +92 -0
- package/docs/config.md +108 -0
- package/docs/manual-setup.md +117 -0
- package/docs/plugins.md +120 -0
- package/docs/theming.md +122 -0
- package/lib/anti-pattern-rules.js +427 -6
- package/lib/check.js +220 -15
- package/lib/plugin-api.js +17 -1
- package/lib/rules-report.js +181 -0
- package/lib/spacing-scale.js +228 -0
- package/lib/stop-hook.js +44 -6
- package/lib/token-migration.js +678 -0
- package/package.json +3 -1
- package/templates/sparkle-variables/.github/workflows/validate.yml +21 -0
- package/templates/sparkle-variables/README.md +151 -0
- package/templates/sparkle-variables/colors.json +87 -87
- package/templates/sparkle-variables/gray.json +70 -70
- package/templates/sparkle-variables/radius.csv +1 -1
- package/templates/sparkle-variables/scripts/validate-tokens.mjs +734 -0
- package/templates/sparkle-variables/sparkle-design.template.css +1107 -373
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma の `Spacing: Primitives` を「使ってよいステップ」として検査するためのデータ。
|
|
3
|
+
*
|
|
4
|
+
* ## なぜ CSS 変数ではなく check ルールなのか
|
|
5
|
+
*
|
|
6
|
+
* Tailwind v4 の `p-N` / `m-N` / `gap-N` は `calc(var(--spacing) * N)` に
|
|
7
|
+
* コンパイルされる。ここに `--spacing-<N>` という**名前付きキー**を定義すると、
|
|
8
|
+
* その N だけ名前付きの値が優先される。つまり Figma の値をそのまま CSS 変数に
|
|
9
|
+
* すると、既存 consumer の余白がビルドも警告も通ったまま静かに変わる。
|
|
10
|
+
*
|
|
11
|
+
* しかも数字の意味がずれている。Figma の `padding/16` は 16px だが、
|
|
12
|
+
* Tailwind の `p-16` は 64px(16 × 4px)で、同じ数字が 4 倍違う。
|
|
13
|
+
*
|
|
14
|
+
* そのため sparkle-variables はトークンを出さず(`--spacing-*` を定義しないことを
|
|
15
|
+
* validate-tokens.mjs が検査している)、代わりにここで「Figma のスケールに乗って
|
|
16
|
+
* いる値だけを使う」制約として検査する。
|
|
17
|
+
*
|
|
18
|
+
* en: Figma's spacing scale is enforced as a lint, not emitted as CSS variables.
|
|
19
|
+
* Defining `--spacing-<N>` would override Tailwind's dynamic spacing and silently
|
|
20
|
+
* rescale every existing consumer's padding — and Figma's `padding/16` (16px) is
|
|
21
|
+
* 4× smaller than Tailwind's `p-16` (64px), so the numbers don't even line up.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Figma `Spacing: Primitives` の 17 段(px)。
|
|
26
|
+
* これが唯一の正解データで、以下はすべてここから導出する。
|
|
27
|
+
*/
|
|
28
|
+
export const FIGMA_SPACING_PX = [0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 40, 48, 56, 72, 88, 104, 120];
|
|
29
|
+
|
|
30
|
+
/** Tailwind の 1 ステップは 0.25rem = 4px。`p-4` は 4 × 4px = 16px。 */
|
|
31
|
+
export const TAILWIND_STEP_PX = 4;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* px を Tailwind のステップに直した**数値**の集合(`0.5` / `1.5` など小数を含む)。
|
|
35
|
+
*
|
|
36
|
+
* 文字列ではなく数値で持つのは、`p-4` と `p-4.0` のような表記ゆれを別物として
|
|
37
|
+
* 扱わないため。判定側は書かれた字面を `Number.parseFloat` で数値化してから
|
|
38
|
+
* この集合と突き合わせる。
|
|
39
|
+
*/
|
|
40
|
+
export const FIGMA_SPACING_STEPS = new Set(FIGMA_SPACING_PX.map((px) => px / TAILWIND_STEP_PX));
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 数値ステップを取るユーティリティ接頭辞。
|
|
44
|
+
*
|
|
45
|
+
* Figma のコレクション名は `padding/*` だが、検査対象は padding だけにしない。
|
|
46
|
+
* `gap-7`(28px)のように Figma のスケールに無い値は、padding でなくても
|
|
47
|
+
* 同じだけレイアウトを崩すため。逆に言うと **これは「Figma がそう定義している」
|
|
48
|
+
* のではなく「同じスケールに乗せる」という運用上の判断**で、severity を `info` に
|
|
49
|
+
* しているのはそのため。
|
|
50
|
+
* en: Figma only names the collection `padding/*`; applying it to margin/gap/space
|
|
51
|
+
* is our operational call, which is why this ships at `info` severity.
|
|
52
|
+
*/
|
|
53
|
+
export const SPACING_UTILITY_PREFIXES = [
|
|
54
|
+
'p',
|
|
55
|
+
'px',
|
|
56
|
+
'py',
|
|
57
|
+
'pt',
|
|
58
|
+
'pr',
|
|
59
|
+
'pb',
|
|
60
|
+
'pl',
|
|
61
|
+
'ps',
|
|
62
|
+
'pe',
|
|
63
|
+
'm',
|
|
64
|
+
'mx',
|
|
65
|
+
'my',
|
|
66
|
+
'mt',
|
|
67
|
+
'mr',
|
|
68
|
+
'mb',
|
|
69
|
+
'ml',
|
|
70
|
+
'ms',
|
|
71
|
+
'me',
|
|
72
|
+
'gap',
|
|
73
|
+
'gap-x',
|
|
74
|
+
'gap-y',
|
|
75
|
+
'space-x',
|
|
76
|
+
'space-y',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 長い接頭辞を先に並べる。
|
|
81
|
+
*
|
|
82
|
+
* 正規表現の選択肢は先勝ちだが、この形(後続に `-<数字>` を要求する)なら
|
|
83
|
+
* 短い側が先に来てもバックトラックで長い側に到達するため、**並び順を変えても
|
|
84
|
+
* 検出結果は変わらない**。それでも並べ替えるのは、正しさをバックトラックの
|
|
85
|
+
* 挙動に依存させないため。
|
|
86
|
+
* en: Longest-first. With the trailing `-<digits>` requirement, backtracking
|
|
87
|
+
* would reach the longer alternative anyway — this just avoids depending on it.
|
|
88
|
+
*/
|
|
89
|
+
function alternation(values) {
|
|
90
|
+
if (values.length === 0) {
|
|
91
|
+
throw new Error('接頭辞が空です(検査が何にもマッチしなくなります)');
|
|
92
|
+
}
|
|
93
|
+
return [...values]
|
|
94
|
+
.sort((a, b) => b.length - a.length || a.localeCompare(b))
|
|
95
|
+
.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
96
|
+
.join('|');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* arbitrary variant の角括弧。**1 段だけ入れ子を許す**。
|
|
101
|
+
*
|
|
102
|
+
* `[^\]\s]*` だと最初の `]` で閉じてしまい、`[&:has([data-x])]:` や
|
|
103
|
+
* `[&_[data-slot=icon]]:` のような shadcn 系で頻出する形で variant を取りこぼす。
|
|
104
|
+
* 正規表現で任意段の入れ子は扱えないので 1 段で打ち切る(2 段以上は既知の制約)。
|
|
105
|
+
* en: Allows one level of nesting — `[&_[data-slot=icon]]:` is common in
|
|
106
|
+
* shadcn-style code. Arbitrary depth isn't expressible here; two levels or more
|
|
107
|
+
* is a known limitation.
|
|
108
|
+
*/
|
|
109
|
+
const VARIANT_BRACKET_BODY = '\\[(?:[^\\[\\]\\s]|\\[[^\\[\\]\\s]*\\])*\\]';
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* variant の名前部分。`hover` / `md` / `data-[state=open]` に加えて、
|
|
113
|
+
* container query バリアントの `@sm` / `@2xl` のように `@` で始まる形も取る。
|
|
114
|
+
*/
|
|
115
|
+
const VARIANT_NAME = `@?[a-z0-9][a-z0-9-]*(?:${VARIANT_BRACKET_BODY})?`;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 角括弧だけの arbitrary variant(`[&>*]` / `[@supports(display:grid)]`)と、
|
|
119
|
+
* 角括弧で幅を直接書く container query バリアント(`@[400px]`)。
|
|
120
|
+
* `@` を許さないと `@[400px]:p-7` の `@` だけが外れ、**存在しないクラス**
|
|
121
|
+
* `[400px]:p-6` を代替として提示してしまう。
|
|
122
|
+
*/
|
|
123
|
+
const VARIANT_BRACKET = `@?${VARIANT_BRACKET_BODY}`;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 直下の子 `*:` と全子孫 `**:`。どちらも実在する Tailwind の variant。
|
|
127
|
+
*
|
|
128
|
+
* ここを落とすと `*:p-7` が `p-7` としてマッチし、「**子要素の余白**を
|
|
129
|
+
* **自分自身の余白**に書き換えろ」という案内になる。名前でも角括弧でもないので
|
|
130
|
+
* 独立した選択肢として持つ必要がある。
|
|
131
|
+
* en: Child (`*:`) and descendant (`**:`) variants. Missing them turns
|
|
132
|
+
* "change the children's padding" into "change your own padding".
|
|
133
|
+
*/
|
|
134
|
+
const VARIANT_STAR = '\\*\\*?';
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 名前付き group / peer / container の `/edit` 部分
|
|
138
|
+
* (`group-hover/edit:` / `peer-focus/email:` / `@2xl/main:`)。
|
|
139
|
+
*
|
|
140
|
+
* 名前はユーザーが決めるので大文字・アンダースコアも入る(`group/menuItem`)。
|
|
141
|
+
* 小文字だけに絞ると、その variant を丸ごと取りこぼした案内になる。
|
|
142
|
+
*/
|
|
143
|
+
const VARIANT_MODIFIER = `(?:\\/(?:[A-Za-z0-9_-]+|${VARIANT_BRACKET_BODY}))?`;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* variant 1 段。
|
|
147
|
+
*
|
|
148
|
+
* ここを狭く取ると、**variant を取りこぼしたうえで残りにマッチする**という
|
|
149
|
+
* 一番たちの悪い失敗をする。たとえば `/edit` を知らないと
|
|
150
|
+
* `group-hover/edit:p-7` が `edit:p-7` としてマッチし、代替候補が
|
|
151
|
+
* `edit:p-6` という存在しないクラスになる。
|
|
152
|
+
*
|
|
153
|
+
* 各段は必ず `:` を消費し、角括弧の中身も `[` と非 `[` の排他な選択肢で
|
|
154
|
+
* 組んであるため、`*` の中に置いても曖昧なバックトラックにならない。
|
|
155
|
+
* en: Too narrow a segment doesn't just miss the variant — it matches the
|
|
156
|
+
* leftover (`group-hover/edit:p-7` → `edit:p-7`) and suggests a class that
|
|
157
|
+
* doesn't exist. Every repetition must consume a `:` and the bracket body is
|
|
158
|
+
* built from mutually exclusive alternatives, so the outer `*` stays unambiguous.
|
|
159
|
+
*/
|
|
160
|
+
const VARIANT_SEGMENT = `(?:${VARIANT_STAR}|${VARIANT_NAME}|${VARIANT_BRACKET})${VARIANT_MODIFIER}`;
|
|
161
|
+
|
|
162
|
+
/** `hover:md:` のような variant 連鎖。 */
|
|
163
|
+
const VARIANT_CHAIN = `(?:${VARIANT_SEGMENT}:)*`;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* spacing ユーティリティにマッチする正規表現を作る。
|
|
167
|
+
*
|
|
168
|
+
* - 先頭の `-` は負のマージン(`-mt-4`)
|
|
169
|
+
* - 先頭・末尾の `!` は important(`!p-4` / `p-4!`)
|
|
170
|
+
* - 前後の `(?<![\w\-/])` / `(?![\w-])` は、`top-7` の `p-7` や `p-4x` のような
|
|
171
|
+
* 部分一致を弾くための境界。
|
|
172
|
+
* - **`-` を含めるのが重要**。これが無いと `custom-mt-4` の末尾に紛れた
|
|
173
|
+
* `mt-4` を誤検出する。
|
|
174
|
+
* - **`/` を含めるのはパス対策**。`https://example.com/v1/p-7` や
|
|
175
|
+
* `import x from './gap-7'` を拾ってしまうため。クラス名の直前に `/` が
|
|
176
|
+
* 来ることは実際には無い(`group-hover/edit:` の `/` は variant の内側で
|
|
177
|
+
* 消費されるので、マッチ開始位置の直前には現れない)。
|
|
178
|
+
*
|
|
179
|
+
* 名前つきキャプチャ:
|
|
180
|
+
* - `lead` … 数値の直前までの字面(`hover:-mt-`)。代替候補はこれに数字を
|
|
181
|
+
* 付け直して作るので、variant も `-` も `!` も自動的に保たれる
|
|
182
|
+
* - `sign` … ユーティリティ直前の `!` / `-`(important と負値)
|
|
183
|
+
* - `step` … ステップ値
|
|
184
|
+
* - `important` … 末尾の `!`
|
|
185
|
+
*
|
|
186
|
+
* `sign` を**正規表現で切り出す**のが重要。`lead` 全体を `/-/` で調べる方式だと、
|
|
187
|
+
* `[&:-moz-any-link]:mt-7` のように variant の内側に `-` を含む形で
|
|
188
|
+
* 「負値である」と誤判定し、正の 28px を -28px と案内してしまう。
|
|
189
|
+
* en: Named captures. `sign` must come from the pattern itself — scanning the
|
|
190
|
+
* whole `lead` for a hyphen misreads variants like `[&:-moz-any-link]:mt-7`
|
|
191
|
+
* as negative and reports +28px as −28px.
|
|
192
|
+
*/
|
|
193
|
+
export function buildSpacingUtilityPattern() {
|
|
194
|
+
return new RegExp(
|
|
195
|
+
`(?<![\\w\\-/])(?<lead>${VARIANT_CHAIN}(?<sign>!?-?)(?:${alternation(SPACING_UTILITY_PREFIXES)})-)(?<step>\\d+(?:\\.\\d+)?)(?<important>!?)(?![\\w-])`,
|
|
196
|
+
'g'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* ステップ値が Figma のスケールに乗っているかを判定する。
|
|
202
|
+
*
|
|
203
|
+
* @param {string} step 字面のステップ(`"4"` / `"0.5"` / `"20"`)
|
|
204
|
+
* @returns {null | { px: number, lower: number | null, upper: number | null }}
|
|
205
|
+
* スケール内なら null。スケール外なら px 換算値と、前後の許容値を返す。
|
|
206
|
+
* 数値として読めない入力も null(=報告しない)を返す。検出パターンが
|
|
207
|
+
* `\d+(?:\.\d+)?` しか渡さないので実際には到達しないが、この関数を単体で
|
|
208
|
+
* 使うときは「null = 違反ではない」であって「null = 妥当な値」ではない。
|
|
209
|
+
*/
|
|
210
|
+
export function resolveSpacingStep(step) {
|
|
211
|
+
const value = Number.parseFloat(step);
|
|
212
|
+
if (!Number.isFinite(value)) return null;
|
|
213
|
+
if (FIGMA_SPACING_STEPS.has(value)) return null;
|
|
214
|
+
|
|
215
|
+
const px = value * TAILWIND_STEP_PX;
|
|
216
|
+
const below = FIGMA_SPACING_PX.filter((p) => p < px);
|
|
217
|
+
const above = FIGMA_SPACING_PX.filter((p) => p > px);
|
|
218
|
+
return {
|
|
219
|
+
px,
|
|
220
|
+
lower: below.length ? Math.max(...below) : null,
|
|
221
|
+
upper: above.length ? Math.min(...above) : null,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** px を Tailwind のステップ表記に戻す(`16` → `"4"`, `2` → `"0.5"`)。 */
|
|
226
|
+
export function pxToStep(px) {
|
|
227
|
+
return String(px / TAILWIND_STEP_PX);
|
|
228
|
+
}
|
package/lib/stop-hook.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import {
|
|
2
|
+
import { countBySeverity, runCheck } from './check.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* AI assistant の Stop / stop hook 用エントリポイント。
|
|
@@ -32,17 +32,55 @@ export async function runStopHook(target) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const targets = target ? [target] : [];
|
|
35
|
-
const
|
|
35
|
+
const { report, blocked } = await runCheck(targets, { strict: true, format: 'text' });
|
|
36
|
+
|
|
37
|
+
if (blocked) {
|
|
38
|
+
// ブロック理由は「error の findings」と「実行に失敗して未検査のルール」の
|
|
39
|
+
// 2 系統ある。後者のとき「findings を修正してください」とだけ書くと、
|
|
40
|
+
// 存在しない findings を AI が探し始めるので理由を書き分ける。
|
|
41
|
+
// en: Blocking has two causes — error findings and rules that failed to run.
|
|
42
|
+
// Say which, or the AI hunts for findings that don't exist.
|
|
43
|
+
const skipped = report.skippedRules ?? [];
|
|
44
|
+
const errorCount = countBySeverity(report.findings).error;
|
|
45
|
+
const reasons = [];
|
|
46
|
+
if (errorCount > 0) reasons.push(`severity=error の findings ${errorCount} 件`);
|
|
47
|
+
if (skipped.length > 0) {
|
|
48
|
+
reasons.push(
|
|
49
|
+
`実行に失敗して未検査のルール ${skipped.length} 件(${skipped.map((s) => s.ruleId).join(', ')})`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
36
52
|
|
|
37
|
-
if (hasFindings) {
|
|
38
53
|
process.stderr.write(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
`\nsparkle-design-cli stop-hook: ${reasons.join(' と ')} を検出したため応答終了を 1 度だけブロックしました。` +
|
|
55
|
+
(errorCount > 0
|
|
56
|
+
? '上記の findings を修正するか、対応しない判断であればユーザーに確認してください。'
|
|
57
|
+
: '') +
|
|
58
|
+
(skipped.length > 0
|
|
59
|
+
? 'ルールの実行失敗は検査自体が行われていないことを意味します。プラグインの不具合の可能性があるためユーザーに報告してください(findings を探す必要はありません)。'
|
|
60
|
+
: '') +
|
|
61
|
+
` / Blocked once: ${reasons.join(' and ')}.\n`
|
|
42
62
|
);
|
|
43
63
|
return 2;
|
|
44
64
|
}
|
|
45
65
|
|
|
66
|
+
// exit 0 の hook の stdout は AI に渡らない(Claude Code がフィードバックとして
|
|
67
|
+
// 読むのは exit 2 の stderr のみ)。warning / info は設計上ブロックしないので、
|
|
68
|
+
// このままだと printTextReport が出した移行ガイドが誰にも読まれずに消える。
|
|
69
|
+
// ブロックはせず、要約だけ stderr に出して人間の目に留まるようにする。
|
|
70
|
+
// 詳細は `sparkle-design-cli check --format json` で取得できる。
|
|
71
|
+
// en: A hook exiting 0 has its stdout dropped, so non-blocking findings would
|
|
72
|
+
// vanish. Emit a short summary on stderr instead of escalating to exit 2.
|
|
73
|
+
const counts = countBySeverity(report.findings);
|
|
74
|
+
const nonBlocking = counts.warning + counts.info;
|
|
75
|
+
if (nonBlocking > 0) {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
`\nsparkle-design-cli stop-hook: ブロックしない findings が ${nonBlocking} 件あります` +
|
|
78
|
+
`(warning ${counts.warning} / info ${counts.info})。` +
|
|
79
|
+
'詳細は `npx --yes sparkle-design-cli check src --format json` で確認できます。' +
|
|
80
|
+
` / ${nonBlocking} non-blocking finding(s); run check --format json for details.\n`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
return 0;
|
|
47
85
|
}
|
|
48
86
|
|