sparkle-design-cli 2.5.0-beta.2 → 2.5.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/bin/sparkle-design.js +136 -2
- package/docs/anti-patterns.md +21 -0
- package/lib/anti-pattern-rules.js +344 -3
- package/lib/auth.js +302 -0
- package/lib/check.js +2 -0
- package/lib/migrate.js +460 -0
- package/lib/token-migration.js +3 -1
- package/package.json +1 -1
- package/templates/sparkle-variables/README.md +6 -2
- package/templates/sparkle-variables/scripts/validate-tokens.mjs +161 -0
- package/templates/sparkle-variables/sparkle-design.template.css +3 -25
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveTailwindPaletteUtility,
|
|
11
11
|
} from './token-migration.js';
|
|
12
12
|
import { buildSpacingUtilityPattern, pxToStep, resolveSpacingStep } from './spacing-scale.js';
|
|
13
|
+
import { findOpeningTagEnd } from './plugin-helpers.js';
|
|
13
14
|
|
|
14
15
|
const COMMON_INTRO =
|
|
15
16
|
'以下は頻繁に発生する誤用パターンです。コンポーネントが提供する専用 props・サブコンポーネントを使ってください。';
|
|
@@ -136,6 +137,93 @@ const BUILTIN_MANUAL_REVIEW_REMINDERS = [
|
|
|
136
137
|
},
|
|
137
138
|
];
|
|
138
139
|
|
|
140
|
+
// --- JSX 開きタグを扱う match 用の小道具 ----------------------------------------
|
|
141
|
+
//
|
|
142
|
+
// フォーム系の a11y ルール(form-control-select-root / handwritten-radio-role /
|
|
143
|
+
// handwritten-form-error)は「どのタグの属性か」「直下の子は何か」を見る必要があり、
|
|
144
|
+
// 1 本の regex では prop 値中の `=>` や `{...}` で打ち切られる。プラグインに注入して
|
|
145
|
+
// いる findOpeningTagEnd を組み込みルールでも使い、同じパース実装に揃える。
|
|
146
|
+
// en: Helpers for the form a11y rules, built on the same findOpeningTagEnd that
|
|
147
|
+
// plugins receive, so built-in and plugin rules share one tag scanner.
|
|
148
|
+
|
|
149
|
+
// カスタム要素(`<my-radio>`)も拾えるよう `-` を許す
|
|
150
|
+
// en: Allow `-` so custom elements such as `<my-radio>` are scanned too.
|
|
151
|
+
const JSX_OPENING_TAG = /<([A-Za-z][\w.-]*)(?=[\s/>])/g;
|
|
152
|
+
|
|
153
|
+
function blankOut(text) {
|
|
154
|
+
return text.replace(/[^\n]/g, ' ');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* コメント中の疑似 JSX(`// Radix は <button role="radio"> を描画する` のような説明)を
|
|
159
|
+
* 検出しないよう、コメントを同じ長さの空白で潰す。index と行番号はずれない。
|
|
160
|
+
* 行頭からのブロックコメント(maskBlockComments と同じ前提)・JSX コメント
|
|
161
|
+
* `{/* ... *\/}`・行頭からの `//` コメントが対象。
|
|
162
|
+
* en: Blank out comments (same length, so offsets stay valid) so explanatory
|
|
163
|
+
* pseudo-JSX inside comments is not reported.
|
|
164
|
+
*/
|
|
165
|
+
function maskSourceComments(content) {
|
|
166
|
+
return maskBlockComments(content)
|
|
167
|
+
.replace(/\{\s*\/\*(?:(?!\*\/)[\s\S])*\*\/\s*\}/g, blankOut)
|
|
168
|
+
.replace(/^[ \t]*\/\/.*$/gm, blankOut);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* `index` から始まる開きタグを読む。読めなければ null。
|
|
173
|
+
* en: Read the opening tag starting at `index`, or null.
|
|
174
|
+
*/
|
|
175
|
+
function readOpeningTag(content, index, tagName) {
|
|
176
|
+
const bodyStart = index + 1 + tagName.length;
|
|
177
|
+
const end = findOpeningTagEnd(content, bodyStart);
|
|
178
|
+
if (end === -1) return null;
|
|
179
|
+
const props = content.slice(bodyStart, end);
|
|
180
|
+
return {
|
|
181
|
+
index,
|
|
182
|
+
end,
|
|
183
|
+
tagName,
|
|
184
|
+
props,
|
|
185
|
+
selfClosing: props.trimEnd().endsWith('/'),
|
|
186
|
+
text: content.slice(index, end + 1),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* `index` の直前にある「閉じていない開きタグ」(=親要素)を返す。直前が
|
|
192
|
+
* whitespace を挟んで開きタグの `>` で終わっている場合だけを親とみなす
|
|
193
|
+
* (テキストや兄弟要素を挟む場合は追わない。ヒューリスティック用途のため)。
|
|
194
|
+
* en: The opening tag that immediately precedes `index` (only whitespace in
|
|
195
|
+
* between), i.e. the direct parent in the common `<span><Icon /></span>` shape.
|
|
196
|
+
*/
|
|
197
|
+
function findImmediateParentTag(content, index) {
|
|
198
|
+
const windowStart = Math.max(0, index - 2000);
|
|
199
|
+
const before = content.slice(windowStart, index);
|
|
200
|
+
const trimmed = before.trimEnd();
|
|
201
|
+
if (!trimmed.endsWith('>') || trimmed.endsWith('/>')) return null;
|
|
202
|
+
const closeAt = windowStart + trimmed.length - 1;
|
|
203
|
+
// 走査を closeAt までに切る。content 全体を渡すと、閉じ `>` が遠い候補(TS の
|
|
204
|
+
// ジェネリクスなど)ごとにファイル末尾まで走査してしまう。
|
|
205
|
+
// en: Bound the scan at closeAt so each candidate can't walk to EOF.
|
|
206
|
+
const bounded = content.slice(0, closeAt + 1);
|
|
207
|
+
let parent = null;
|
|
208
|
+
for (const opener of before.matchAll(JSX_OPENING_TAG)) {
|
|
209
|
+
const tag = readOpeningTag(bounded, windowStart + opener.index, opener[1]);
|
|
210
|
+
if (tag && tag.end === closeAt && !tag.selfClosing) parent = tag;
|
|
211
|
+
}
|
|
212
|
+
return parent;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// role="radio" / role="radiogroup"(JSX 式 `role={"radio"}` も)。`aria-role` /
|
|
216
|
+
// `data-role` を拾わないよう、直前を空白か先頭に限る。
|
|
217
|
+
// en: role="radio" / role="radiogroup", excluding aria-role / data-role.
|
|
218
|
+
const RADIO_ROLE_ATTR =
|
|
219
|
+
/(?:^|\s)role\s*=\s*(?:(["'])(?:radio|radiogroup)\1|\{\s*(["'`])(?:radio|radiogroup)\2\s*\})/;
|
|
220
|
+
|
|
221
|
+
const ERROR_ICON_ATTR = /(?:^|\s)icon\s*=\s*(?:(["'])error\1|\{\s*(["'`])error\2\s*\})/;
|
|
222
|
+
|
|
223
|
+
// 旧体系 `text-negative-500` と新体系 `text-text-negative-enabled` の両方
|
|
224
|
+
// en: Both the legacy (`text-negative-*`) and new (`text-text-negative-*`) tokens.
|
|
225
|
+
const NEGATIVE_TEXT_COLOR = /(?<![\w-])text-(?:text-)?negative-[\w-]+/;
|
|
226
|
+
|
|
139
227
|
const COMPONENT_ANTI_PATTERN_GROUPS = [
|
|
140
228
|
{
|
|
141
229
|
id: 'card-description',
|
|
@@ -1302,6 +1390,239 @@ const COMPONENT_ANTI_PATTERN_GROUPS = [
|
|
|
1302
1390
|
]),
|
|
1303
1391
|
jsdocTargets: [],
|
|
1304
1392
|
},
|
|
1393
|
+
{
|
|
1394
|
+
id: 'form-control-select-root',
|
|
1395
|
+
check: {
|
|
1396
|
+
description: 'FormControl で Select のルートを包まない(ラベルが関連付かない)',
|
|
1397
|
+
recommendation:
|
|
1398
|
+
'FormControl は SelectTrigger を包んでください(<Select><FormControl><SelectTrigger>…</SelectTrigger></FormControl><SelectContent>…</SelectContent></Select>)。FormControl は id / aria-describedby / aria-invalid を直下の子へ渡しますが、Select のルートは DOM を持たないため id がどこにも付かず、FormHeader の <label for> が何も指さなくなります。',
|
|
1399
|
+
// 直下の最初の子要素が <Select> のときだけ。<Select> の中の <FormControl>
|
|
1400
|
+
// (正しい形)や、<FormControl> 直下が <SelectTrigger> のものは拾わない。
|
|
1401
|
+
// en: Only when the first direct child of <FormControl> is <Select>.
|
|
1402
|
+
match: (rawContent) => {
|
|
1403
|
+
const content = maskSourceComments(rawContent);
|
|
1404
|
+
const hits = [];
|
|
1405
|
+
for (const opener of content.matchAll(/<FormControl(?=[\s/>])/g)) {
|
|
1406
|
+
const tag = readOpeningTag(content, opener.index, 'FormControl');
|
|
1407
|
+
if (!tag || tag.selfClosing) continue;
|
|
1408
|
+
const child = /^\s*<([A-Za-z][\w.-]*)(?=[\s/>])/.exec(content.slice(tag.end + 1));
|
|
1409
|
+
if (child?.[1] !== 'Select') continue;
|
|
1410
|
+
hits.push({
|
|
1411
|
+
index: tag.index,
|
|
1412
|
+
text: content.slice(tag.index, tag.end + 1 + child[0].length),
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
return hits;
|
|
1416
|
+
},
|
|
1417
|
+
},
|
|
1418
|
+
featureSection: lines([
|
|
1419
|
+
'### Select をフォームで使うときは FormControl で SelectTrigger を包む',
|
|
1420
|
+
'',
|
|
1421
|
+
'```tsx',
|
|
1422
|
+
'// ✅ Correct — FormControl は SelectTrigger を包む',
|
|
1423
|
+
'<FormItem>',
|
|
1424
|
+
' <FormHeader label="プラン" />',
|
|
1425
|
+
' <Select value={field.value} onValueChange={field.onChange}>',
|
|
1426
|
+
' <FormControl>',
|
|
1427
|
+
' <SelectTrigger>',
|
|
1428
|
+
' <SelectValue placeholder="選択してください" />',
|
|
1429
|
+
' </SelectTrigger>',
|
|
1430
|
+
' </FormControl>',
|
|
1431
|
+
' <SelectContent>',
|
|
1432
|
+
' <SelectItem value="free">Free</SelectItem>',
|
|
1433
|
+
' </SelectContent>',
|
|
1434
|
+
' </Select>',
|
|
1435
|
+
'</FormItem>',
|
|
1436
|
+
'',
|
|
1437
|
+
'// ❌ Wrong — Select のルートを FormControl で包まない',
|
|
1438
|
+
'<FormControl>',
|
|
1439
|
+
' <Select value={field.value} onValueChange={field.onChange}>',
|
|
1440
|
+
' <SelectTrigger>…</SelectTrigger>',
|
|
1441
|
+
' <SelectContent>…</SelectContent>',
|
|
1442
|
+
' </Select>',
|
|
1443
|
+
'</FormControl>',
|
|
1444
|
+
'```',
|
|
1445
|
+
'',
|
|
1446
|
+
'`FormControl` は Slot で `id` / `aria-describedby` / `aria-invalid` を直下の子に渡す。`Select` のルートは DOM を持たないので id がどこにも付かず、`FormHeader` の `<label for>` が何も指さない(ラベルを押してもフォーカスせず、読み上げでも関連が伝わらない)。',
|
|
1447
|
+
]),
|
|
1448
|
+
jsdocTargets: [
|
|
1449
|
+
{
|
|
1450
|
+
file: 'src/components/ui/form/index.tsx',
|
|
1451
|
+
targetName: 'FormControl',
|
|
1452
|
+
section: {
|
|
1453
|
+
bullets: [
|
|
1454
|
+
{
|
|
1455
|
+
ja: '`Select` と組み合わせるときは `Select` のルートではなく `SelectTrigger` を包んでください。`Select` のルートは DOM を持たないため、渡した `id` が付かずラベルが関連付きません。',
|
|
1456
|
+
en: 'With `Select`, wrap `SelectTrigger` rather than the `Select` root. The root renders no DOM, so the forwarded `id` lands nowhere and the label is not associated.',
|
|
1457
|
+
},
|
|
1458
|
+
],
|
|
1459
|
+
example: lines([
|
|
1460
|
+
'// ✅ Correct',
|
|
1461
|
+
'<Select value={field.value} onValueChange={field.onChange}>',
|
|
1462
|
+
' <FormControl>',
|
|
1463
|
+
' <SelectTrigger>',
|
|
1464
|
+
' <SelectValue placeholder="選択してください" />',
|
|
1465
|
+
' </SelectTrigger>',
|
|
1466
|
+
' </FormControl>',
|
|
1467
|
+
' <SelectContent>...</SelectContent>',
|
|
1468
|
+
'</Select>',
|
|
1469
|
+
'',
|
|
1470
|
+
'// ❌ Wrong - Select のルートを包まない',
|
|
1471
|
+
'<FormControl>',
|
|
1472
|
+
' <Select>...</Select>',
|
|
1473
|
+
'</FormControl>',
|
|
1474
|
+
]),
|
|
1475
|
+
},
|
|
1476
|
+
},
|
|
1477
|
+
],
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
id: 'handwritten-radio-role',
|
|
1481
|
+
check: {
|
|
1482
|
+
// 原則ダメだが、Radio / Select / SegmentedControl のどれにも収まらない
|
|
1483
|
+
// 見た目の選択 UI(色見本など)で手組みを選ぶ判断はありうるので warning。
|
|
1484
|
+
// en: warning, not error — a visual picker that none of the components fit
|
|
1485
|
+
// can be a deliberate exception (suppress it with a comment).
|
|
1486
|
+
severity: SEVERITY.WARNING,
|
|
1487
|
+
description: '単一選択を role="radio" / role="radiogroup" で手組みしない',
|
|
1488
|
+
recommendation:
|
|
1489
|
+
'単一選択には Radio(選択肢が少なく常に 1 つ選ばれている)/ Select(選択肢が多い・表示スペースが狭い)/ sparkle-design-internal の SegmentedControl(2〜5 個の切り替え)を使ってください。手組みの role="radio" は矢印キーでの移動・roving tabindex・aria-checked の同期を自前で正しく実装する必要があります。どうしても手組みが必要な場合は `// sparkle-disable-next-line handwritten-radio-role` で理由を添えて残してください。',
|
|
1490
|
+
match: (rawContent) => {
|
|
1491
|
+
if (!/role\s*=\s*\{?\s*["'`]radio/.test(rawContent)) return [];
|
|
1492
|
+
const content = maskSourceComments(rawContent);
|
|
1493
|
+
const hits = [];
|
|
1494
|
+
for (const opener of content.matchAll(JSX_OPENING_TAG)) {
|
|
1495
|
+
// Radio / RadioGroup / SegmentedControl 自身(とその実装の Radix
|
|
1496
|
+
// primitive)は role を持つのが正しいので対象外
|
|
1497
|
+
// en: The components themselves legitimately carry these roles.
|
|
1498
|
+
if (/Radio|SegmentedControl/.test(opener[1])) continue;
|
|
1499
|
+
const tag = readOpeningTag(content, opener.index, opener[1]);
|
|
1500
|
+
if (tag && RADIO_ROLE_ATTR.test(tag.props)) {
|
|
1501
|
+
hits.push({ index: tag.index, text: tag.text });
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return hits;
|
|
1505
|
+
},
|
|
1506
|
+
},
|
|
1507
|
+
featureSection: lines([
|
|
1508
|
+
'### 単一選択を role="radio" で手組みしない',
|
|
1509
|
+
'',
|
|
1510
|
+
'```tsx',
|
|
1511
|
+
'// ✅ Correct — 選択肢が少なく常に 1 つ選ばれている',
|
|
1512
|
+
'<Radio value={value} onValueChange={setValue}>',
|
|
1513
|
+
' <RadioItem value="sm" label="小" />',
|
|
1514
|
+
' <RadioItem value="md" label="中" />',
|
|
1515
|
+
'</Radio>',
|
|
1516
|
+
'',
|
|
1517
|
+
'// ✅ Correct — 選択肢が多い・表示スペースが狭い',
|
|
1518
|
+
'<Select value={value} onValueChange={setValue}>…</Select>',
|
|
1519
|
+
'',
|
|
1520
|
+
'// ❌ Wrong — button に role="radio" を付けて手組みしない',
|
|
1521
|
+
'<div role="radiogroup">',
|
|
1522
|
+
' <button type="button" role="radio" aria-checked={value === "sm"}>小</button>',
|
|
1523
|
+
' <button type="button" role="radio" aria-checked={value === "md"}>中</button>',
|
|
1524
|
+
'</div>',
|
|
1525
|
+
'```',
|
|
1526
|
+
'',
|
|
1527
|
+
'単一選択は `Radio` / `Select` / sparkle-design-internal の `SegmentedControl`(2〜5 個の切り替え)から選ぶ。手組みの `role="radio"` は矢印キーでの移動や `aria-checked` の同期を自前で実装することになり、漏れやすい。',
|
|
1528
|
+
]),
|
|
1529
|
+
jsdocTargets: [],
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
id: 'handwritten-form-error',
|
|
1533
|
+
check: {
|
|
1534
|
+
// 「error アイコン + negative 系の文字色が並ぶ」というヒューリスティックで、
|
|
1535
|
+
// 入力欄に紐づかないページ全体のエラー表示なども拾いうるので info に留める。
|
|
1536
|
+
// en: Heuristic (error icon next to a negative text color) — it can also hit
|
|
1537
|
+
// page-level errors that aren't tied to a field, so keep it at info.
|
|
1538
|
+
severity: SEVERITY.INFO,
|
|
1539
|
+
description: 'フォームのエラー表示を Icon + negative 色で手組みしない',
|
|
1540
|
+
recommendation:
|
|
1541
|
+
'入力欄のエラーは FormErrorMessage を使ってください。FormErrorMessage は aria-describedby / aria-invalid で入力欄と関連付くため、読み上げでどの欄のエラーかが伝わります(手組みでは伝わりません)。入力欄に紐づかないエラー(ページ全体の失敗など)なら InlineMessage を検討し、意図的な手組みであれば `// sparkle-disable-next-line handwritten-form-error` で残してください。',
|
|
1542
|
+
// `<Icon icon="error">` の (1) 自身 (2) 直上の親 (3) 直後の兄弟 のいずれかに
|
|
1543
|
+
// negative 系の文字色があれば手組みのエラー表示とみなす。親に data-slot が
|
|
1544
|
+
// 付いているもの(FormErrorMessage など Sparkle 本体の実装)は対象外。
|
|
1545
|
+
// en: Flag `<Icon icon="error">` when the icon itself, its immediate parent,
|
|
1546
|
+
// or its next sibling carries a negative text color. Parents with
|
|
1547
|
+
// `data-slot` are Sparkle's own implementation (FormErrorMessage) — skip.
|
|
1548
|
+
match: (rawContent) => {
|
|
1549
|
+
if (!/icon\s*=\s*\{?\s*["'`]error/.test(rawContent)) return [];
|
|
1550
|
+
const content = maskSourceComments(rawContent);
|
|
1551
|
+
const hits = [];
|
|
1552
|
+
for (const opener of content.matchAll(/<Icon(?=[\s/>])/g)) {
|
|
1553
|
+
const icon = readOpeningTag(content, opener.index, 'Icon');
|
|
1554
|
+
if (!icon || !ERROR_ICON_ATTR.test(icon.props)) continue;
|
|
1555
|
+
|
|
1556
|
+
const parent = findImmediateParentTag(content, icon.index);
|
|
1557
|
+
if (
|
|
1558
|
+
parent &&
|
|
1559
|
+
(/(?:^|\s)data-slot\s*=/.test(parent.props) || parent.tagName === 'FormErrorMessage')
|
|
1560
|
+
) {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
let sibling = null;
|
|
1565
|
+
if (icon.selfClosing) {
|
|
1566
|
+
const next = /^\s*<([A-Za-z][\w.-]*)(?=[\s/>])/.exec(content.slice(icon.end + 1));
|
|
1567
|
+
if (next) {
|
|
1568
|
+
sibling = readOpeningTag(content, icon.end + 1 + next[0].indexOf('<'), next[1]);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if ([icon, parent, sibling].some((tag) => tag && NEGATIVE_TEXT_COLOR.test(tag.props))) {
|
|
1573
|
+
hits.push({ index: icon.index, text: icon.text });
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return hits;
|
|
1577
|
+
},
|
|
1578
|
+
},
|
|
1579
|
+
featureSection: lines([
|
|
1580
|
+
'### フォームのエラー表示は FormErrorMessage を使う',
|
|
1581
|
+
'',
|
|
1582
|
+
'```tsx',
|
|
1583
|
+
'// ✅ Correct — 入力欄と aria-describedby / aria-invalid で関連付く',
|
|
1584
|
+
'<FormItem>',
|
|
1585
|
+
' <FormHeader label="招待コード" />',
|
|
1586
|
+
' <FormControl>',
|
|
1587
|
+
' <Input {...field} isInvalid={fieldState.invalid} />',
|
|
1588
|
+
' </FormControl>',
|
|
1589
|
+
' <FormErrorMessage />',
|
|
1590
|
+
'</FormItem>',
|
|
1591
|
+
'',
|
|
1592
|
+
'// ❌ Wrong — Icon と negative 色でエラー表示を手組みしない',
|
|
1593
|
+
'<span className="flex items-center gap-1 text-text-negative-enabled">',
|
|
1594
|
+
' <Icon icon="error" size={3} />',
|
|
1595
|
+
' コードが見つかりません',
|
|
1596
|
+
'</span>',
|
|
1597
|
+
'```',
|
|
1598
|
+
'',
|
|
1599
|
+
'手組みのエラー表示は見た目が同じでも入力欄との関連が無く、スクリーンリーダーではどの欄のエラーかが伝わらない。入力欄に紐づかないエラーは `InlineMessage` を使う。',
|
|
1600
|
+
]),
|
|
1601
|
+
jsdocTargets: [
|
|
1602
|
+
{
|
|
1603
|
+
file: 'src/components/ui/form/index.tsx',
|
|
1604
|
+
targetName: 'FormErrorMessage',
|
|
1605
|
+
section: {
|
|
1606
|
+
bullets: [
|
|
1607
|
+
{
|
|
1608
|
+
ja: '`<Icon icon="error" />` と negative 系の文字色でエラー表示を手組みしないでください。`FormErrorMessage` は `aria-describedby` / `aria-invalid` で入力欄と関連付きます。',
|
|
1609
|
+
en: 'Do not hand-roll error text with `<Icon icon="error" />` and a negative text color. `FormErrorMessage` is linked to the field via `aria-describedby` / `aria-invalid`.',
|
|
1610
|
+
},
|
|
1611
|
+
],
|
|
1612
|
+
example: lines([
|
|
1613
|
+
'// ✅ Correct',
|
|
1614
|
+
'<FormErrorMessage />',
|
|
1615
|
+
'',
|
|
1616
|
+
'// ❌ Wrong - 手組みのエラー表示',
|
|
1617
|
+
'<span className="text-text-negative-enabled">',
|
|
1618
|
+
' <Icon icon="error" size={3} />',
|
|
1619
|
+
' コードが見つかりません',
|
|
1620
|
+
'</span>',
|
|
1621
|
+
]),
|
|
1622
|
+
},
|
|
1623
|
+
},
|
|
1624
|
+
],
|
|
1625
|
+
},
|
|
1305
1626
|
];
|
|
1306
1627
|
|
|
1307
1628
|
// Built-in groups always come before plugin-supplied groups in this priority order.
|
|
@@ -1351,6 +1672,23 @@ function maskBlockComments(content) {
|
|
|
1351
1672
|
return content.replace(/^[ \t]*\/\*[\s\S]*?\*\//gm, (comment) => comment.replace(/[^\n]/g, ' '));
|
|
1352
1673
|
}
|
|
1353
1674
|
|
|
1675
|
+
/**
|
|
1676
|
+
* そのファイル自身が宣言している CSS カスタムプロパティ名の集合。
|
|
1677
|
+
*
|
|
1678
|
+
* `check` の移行ルールと `migrate` サブコマンドの両方が「定義側は移行対象の
|
|
1679
|
+
* 使用ではない」という同じ判定を使う。片方だけ直すと、check は黙っているのに
|
|
1680
|
+
* migrate が生成物の定義を書き換える(あるいはその逆)というずれが生まれるため、
|
|
1681
|
+
* 1 箇所にまとめている。
|
|
1682
|
+
* en: Custom properties declared by this file. Shared by `check` and `migrate`
|
|
1683
|
+
* so both agree that definitions are not usages to migrate.
|
|
1684
|
+
*
|
|
1685
|
+
* @param {string} content ブロックコメントをマスク済みの内容
|
|
1686
|
+
* @returns {Set<string>}
|
|
1687
|
+
*/
|
|
1688
|
+
function collectDeclaredCustomProperties(content) {
|
|
1689
|
+
return new Set([...content.matchAll(/(?:^|[{;])\s*(--[\w-]+)\s*:/gm)].map((m) => m[1]));
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1354
1692
|
function migrationMatcher(pattern, resolve, label) {
|
|
1355
1693
|
return (rawContent) => {
|
|
1356
1694
|
const content = maskBlockComments(rawContent);
|
|
@@ -1369,9 +1707,7 @@ function migrationMatcher(pattern, resolve, label) {
|
|
|
1369
1707
|
// CSS や minify された theme ファイルで**定義側まで「使用」として報告**する。
|
|
1370
1708
|
// en: Also treat `{`/`;` as declaration starts so single-line/minified CSS
|
|
1371
1709
|
// doesn't get its own definitions reported as usages.
|
|
1372
|
-
const declaredHere =
|
|
1373
|
-
[...content.matchAll(/(?:^|[{;])\s*(--[\w-]+)\s*:/gm)].map((m) => m[1])
|
|
1374
|
-
);
|
|
1710
|
+
const declaredHere = collectDeclaredCustomProperties(content);
|
|
1375
1711
|
|
|
1376
1712
|
const hits = [];
|
|
1377
1713
|
for (const match of content.matchAll(pattern)) {
|
|
@@ -1761,6 +2097,9 @@ const BUILTIN_CHECK_ORDER = [
|
|
|
1761
2097
|
'disabled-vs-is-disabled',
|
|
1762
2098
|
'button-prefixicon-jsx',
|
|
1763
2099
|
'icon-children-text',
|
|
2100
|
+
'form-control-select-root',
|
|
2101
|
+
'handwritten-radio-role',
|
|
2102
|
+
'handwritten-form-error',
|
|
1764
2103
|
// NOTE: この配列が決めるのは **ルールの評価順** だけで、レポートの表示順では
|
|
1765
2104
|
// ない(findings は check.js 側で severity 優先にソートされる)。移行ルールを
|
|
1766
2105
|
// 末尾に置いているのは評価順を安定させるためで、「warning が error を埋もれ
|
|
@@ -1814,9 +2153,11 @@ export {
|
|
|
1814
2153
|
BUILTIN_ANTI_PATTERN_GROUPS as ANTI_PATTERN_GROUPS,
|
|
1815
2154
|
BUILTIN_MANUAL_REVIEW_REMINDERS as MANUAL_REVIEW_REMINDERS,
|
|
1816
2155
|
COMMON_INTRO,
|
|
2156
|
+
collectDeclaredCustomProperties,
|
|
1817
2157
|
getAllJSDocTargets,
|
|
1818
2158
|
getCheckRules,
|
|
1819
2159
|
getManualReviewReminders,
|
|
2160
|
+
maskBlockComments,
|
|
1820
2161
|
renderFeatureSections,
|
|
1821
2162
|
renderJSDocSection,
|
|
1822
2163
|
};
|