sparkle-design-cli 2.0.8 → 2.1.0
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 +66 -0
- package/bin/sparkle-design.js +44 -8
- package/lib/anti-pattern-rules.js +39 -28
- package/lib/check.js +70 -32
- package/lib/load-plugins.js +136 -0
- package/lib/plugin-api.js +253 -0
- package/lib/stop-hook.js +2 -2
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -167,6 +167,72 @@ sparkle-design-cli check --help
|
|
|
167
167
|
|
|
168
168
|
AI エージェントや CI からこの `lint:sparkle` を呼ぶ運用にすると、ガイドラインの注意書きだけに頼らず機械的に検査できます。
|
|
169
169
|
|
|
170
|
+
#### アンチパターン検知の拡張(プラグイン)
|
|
171
|
+
|
|
172
|
+
他のコンポーネントライブラリ(社内拡張など)に固有のアンチパターン検知を、`sparkle-design-cli` 本体に変更を加えず追加できる拡張機構があります。
|
|
173
|
+
|
|
174
|
+
##### 仕組み
|
|
175
|
+
|
|
176
|
+
プラグインを提供するパッケージは、自身の `package.json` に検知ルールのエントリを宣言します:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"name": "@your-org/your-design-extensions",
|
|
181
|
+
"sparkleCli": {
|
|
182
|
+
"antiPatterns": "./anti-patterns/index.js"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
エントリファイルは `defineAntiPatternPlugin` を使ってプラグインを default export します:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
import { defineAntiPatternPlugin } from 'sparkle-design-cli/plugin';
|
|
191
|
+
|
|
192
|
+
export default defineAntiPatternPlugin({
|
|
193
|
+
groups: [
|
|
194
|
+
{
|
|
195
|
+
id: 'your-org-foocard-nesting',
|
|
196
|
+
check: {
|
|
197
|
+
description: 'FooCard を別の FooCard でラップしないでください。',
|
|
198
|
+
recommendation: '入れ子表示が必要なら FooStack を使ってください。',
|
|
199
|
+
pattern: /<FooCard[^>]*>[\s\S]*?<FooCard/g,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
manualReviewReminders: [
|
|
204
|
+
{
|
|
205
|
+
id: 'your-org-foocard-color',
|
|
206
|
+
message: 'FooCard の color token が brand に揃っているか確認してください。',
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
##### 自動 discovery
|
|
213
|
+
|
|
214
|
+
`sparkle-design-cli check` 実行時、CLI は consumer プロジェクトの `package.json` の `dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies` を走査し、`sparkleCli.antiPatterns` を宣言しているパッケージを発見次第ロードしてビルトインのルールにマージします。
|
|
215
|
+
|
|
216
|
+
- プラグインパッケージが install されていなければ何も起きません
|
|
217
|
+
- ロードや評価で失敗したプラグインは warn を出してスキップし、残りのルールで check は継続されます
|
|
218
|
+
- ルール ID はビルトイン・他プラグインと名前空間を共有するため、`your-org-` のようなプレフィックスを付けて衝突を避けてください
|
|
219
|
+
|
|
220
|
+
> ⚠️ **信頼境界**: プラグインのエントリファイルは `import()` で **任意の JavaScript を実行** します。これは Node.js の通常のパッケージ依存と同じ性質ですが、`sparkle-design-cli check` 実行時に走るコードが増えるという点で意識しておく必要があります。プラグインは信頼できる org / 著者のパッケージのみインストールしてください。
|
|
221
|
+
|
|
222
|
+
##### 契約仕様の確認
|
|
223
|
+
|
|
224
|
+
最新の契約仕様は CLI から直接出力できます:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# 契約 shape のドキュメントを出力
|
|
228
|
+
npx --yes sparkle-design-cli plugin-spec
|
|
229
|
+
|
|
230
|
+
# 現在のプロジェクトで discover されたプラグインを一覧
|
|
231
|
+
npx --yes sparkle-design-cli plugin-spec --list
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
完全な型定義は `node_modules/sparkle-design-cli/lib/plugin-api.js` の JSDoc を参照してください。
|
|
235
|
+
|
|
170
236
|
### setup: プロジェクトのフルセットアップ
|
|
171
237
|
|
|
172
238
|
`sparkle-design-cli setup` は、Sparkle Design の導入に必要な作業をまとめて行います:
|
package/bin/sparkle-design.js
CHANGED
|
@@ -4,8 +4,10 @@ import { generateCSS } from '../lib/generate-css.js';
|
|
|
4
4
|
import { checkProject } from '../lib/check.js';
|
|
5
5
|
import { setupAssistant } from '../lib/setup.js';
|
|
6
6
|
import { runStopHook } from '../lib/stop-hook.js';
|
|
7
|
+
import { PLUGIN_SPEC_MARKDOWN } from '../lib/plugin-api.js';
|
|
8
|
+
import { loadAntiPatternPlugins } from '../lib/load-plugins.js';
|
|
7
9
|
|
|
8
|
-
const SUBCOMMANDS = new Set(['generate', 'check', 'setup', 'stop-hook']);
|
|
10
|
+
const SUBCOMMANDS = new Set(['generate', 'check', 'setup', 'stop-hook', 'plugin-spec']);
|
|
9
11
|
|
|
10
12
|
function requireOptionValue(args, index, flags) {
|
|
11
13
|
const value = args[index + 1];
|
|
@@ -136,10 +138,11 @@ Sparkle Design CLI
|
|
|
136
138
|
sparkle-design-cli <command> [options]
|
|
137
139
|
|
|
138
140
|
Commands:
|
|
139
|
-
generate
|
|
140
|
-
check
|
|
141
|
-
setup
|
|
142
|
-
stop-hook
|
|
141
|
+
generate sparkle.config.json から CSS を生成
|
|
142
|
+
check Sparkle Design のアンチパターンを検査
|
|
143
|
+
setup Sparkle Design プロジェクトをセットアップ(パッケージ導入 + 初期ファイル + AI ガード + generate)
|
|
144
|
+
stop-hook AI assistant の Stop hook 用 internal subcommand(findings 検出時に exit 2 で 1 度だけ停止をブロック / 再発火は自動回避)
|
|
145
|
+
plugin-spec アンチパターン拡張プラグインの契約仕様を出力(--list で導入済みプラグインを表示)
|
|
143
146
|
|
|
144
147
|
Generate:
|
|
145
148
|
sparkle-design-cli generate
|
|
@@ -153,6 +156,10 @@ Check:
|
|
|
153
156
|
sparkle-design-cli check src --format json
|
|
154
157
|
sparkle-design-cli check src/components src/features
|
|
155
158
|
|
|
159
|
+
Plugin spec:
|
|
160
|
+
sparkle-design-cli plugin-spec # アンチパターン拡張プラグインの契約仕様を出力
|
|
161
|
+
sparkle-design-cli plugin-spec --list # 現在のプロジェクトで発見されたプラグインを表示
|
|
162
|
+
|
|
156
163
|
Setup:
|
|
157
164
|
sparkle-design-cli setup # フルセットアップ(推奨・新規導入時)
|
|
158
165
|
sparkle-design-cli setup --assistant claude # Claude 向けガードも同時にセットアップ
|
|
@@ -250,7 +257,31 @@ generate で生成されるファイル:
|
|
|
250
257
|
`);
|
|
251
258
|
}
|
|
252
259
|
|
|
253
|
-
function
|
|
260
|
+
async function runPluginSpec(args) {
|
|
261
|
+
// `--list` prints the discovered plugins; default prints the contract markdown so authors
|
|
262
|
+
// can `npx sparkle-design-cli plugin-spec > PLUGIN.md` to seed their docs.
|
|
263
|
+
// en: With --list, summarize what was discovered in the cwd; otherwise print the spec.
|
|
264
|
+
if (args.includes('--list')) {
|
|
265
|
+
const { discovered } = await loadAntiPatternPlugins();
|
|
266
|
+
if (discovered.length === 0) {
|
|
267
|
+
console.log('No anti-pattern plugins discovered in the current project.');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
for (const record of discovered) {
|
|
271
|
+
const status = record.status === 'loaded' ? 'ok' : `error: ${record.error}`;
|
|
272
|
+
console.log(`- ${record.packageName} (${status})`);
|
|
273
|
+
console.log(` entry: ${record.entry}`);
|
|
274
|
+
console.log(` resolved: ${record.resolvedFile}`);
|
|
275
|
+
if (record.groupIds && record.groupIds.length > 0) {
|
|
276
|
+
console.log(` rules: ${record.groupIds.join(', ')}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
console.log(PLUGIN_SPEC_MARKDOWN);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function main() {
|
|
254
285
|
const args = process.argv.slice(2);
|
|
255
286
|
const command = args[0];
|
|
256
287
|
|
|
@@ -282,7 +313,7 @@ function main() {
|
|
|
282
313
|
process.exit(0);
|
|
283
314
|
}
|
|
284
315
|
|
|
285
|
-
const hasFindings = checkProject(options.targets, {
|
|
316
|
+
const hasFindings = await checkProject(options.targets, {
|
|
286
317
|
strict: options.strict,
|
|
287
318
|
format: options.format,
|
|
288
319
|
});
|
|
@@ -310,10 +341,15 @@ function main() {
|
|
|
310
341
|
// en: Internal subcommand invoked by agent stop hooks. The single positional
|
|
311
342
|
// arg is the lint target path determined at setup time.
|
|
312
343
|
const target = args[1];
|
|
313
|
-
const exitCode = runStopHook(target);
|
|
344
|
+
const exitCode = await runStopHook(target);
|
|
314
345
|
process.exit(exitCode);
|
|
315
346
|
}
|
|
316
347
|
|
|
348
|
+
if (command === 'plugin-spec') {
|
|
349
|
+
await runPluginSpec(args.slice(1));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
317
353
|
// 未知のコマンド: エラー終了ではなく note + help 表示にする(typo 時の迷子を防ぐ)
|
|
318
354
|
// en: Unknown command: show a short note + help instead of throwing
|
|
319
355
|
console.warn(
|
|
@@ -23,7 +23,7 @@ function renderJSDocSection(section) {
|
|
|
23
23
|
return output.map((line) => (line ? ` * ${line}` : ' *')).join('\n');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const BUILTIN_MANUAL_REVIEW_REMINDERS = [
|
|
27
27
|
{
|
|
28
28
|
id: 'badge-tag-semantics',
|
|
29
29
|
message:
|
|
@@ -46,7 +46,7 @@ const MANUAL_REVIEW_REMINDERS = [
|
|
|
46
46
|
},
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
-
const
|
|
49
|
+
const BUILTIN_ANTI_PATTERN_GROUPS = [
|
|
50
50
|
{
|
|
51
51
|
id: 'card-description',
|
|
52
52
|
featureSection: lines([
|
|
@@ -1200,51 +1200,62 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
1200
1200
|
},
|
|
1201
1201
|
];
|
|
1202
1202
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1203
|
+
// Built-in groups always come before plugin-supplied groups in this priority order.
|
|
1204
|
+
// Plugin-supplied IDs that aren't in this list fall to the end (stable insertion order).
|
|
1205
|
+
// en: Determines `check` finding emission order so the most-important built-in rules surface first.
|
|
1206
|
+
const BUILTIN_CHECK_ORDER = [
|
|
1207
|
+
'dialog-form',
|
|
1208
|
+
'dialog-button-wrap',
|
|
1209
|
+
'button-icon-only',
|
|
1210
|
+
'material-symbols-direct',
|
|
1211
|
+
'shadcn-token',
|
|
1212
|
+
'tailwind-typography',
|
|
1213
|
+
'card-title-typography',
|
|
1214
|
+
'card-control-non-button',
|
|
1215
|
+
'card-padding-override',
|
|
1216
|
+
'aschild-with-icon-props',
|
|
1217
|
+
'disabled-vs-is-disabled',
|
|
1218
|
+
'button-prefixicon-jsx',
|
|
1219
|
+
'icon-children-text',
|
|
1220
|
+
];
|
|
1219
1221
|
|
|
1220
|
-
|
|
1222
|
+
function getCheckRules(groups = BUILTIN_ANTI_PATTERN_GROUPS) {
|
|
1223
|
+
return groups
|
|
1224
|
+
.filter((group) => group.check)
|
|
1221
1225
|
.map((group) => ({
|
|
1222
1226
|
id: group.id,
|
|
1223
1227
|
...group.check,
|
|
1224
1228
|
}))
|
|
1225
1229
|
.sort((left, right) => {
|
|
1226
|
-
const leftIndex =
|
|
1227
|
-
const rightIndex =
|
|
1230
|
+
const leftIndex = BUILTIN_CHECK_ORDER.indexOf(left.id);
|
|
1231
|
+
const rightIndex = BUILTIN_CHECK_ORDER.indexOf(right.id);
|
|
1228
1232
|
const normalizedLeftIndex = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex;
|
|
1229
1233
|
const normalizedRightIndex = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex;
|
|
1230
1234
|
return normalizedLeftIndex - normalizedRightIndex;
|
|
1231
1235
|
});
|
|
1232
1236
|
}
|
|
1233
1237
|
|
|
1234
|
-
function getAllJSDocTargets() {
|
|
1235
|
-
return
|
|
1238
|
+
function getAllJSDocTargets(groups = BUILTIN_ANTI_PATTERN_GROUPS) {
|
|
1239
|
+
return groups.flatMap((group) => group.jsdocTargets ?? []);
|
|
1236
1240
|
}
|
|
1237
1241
|
|
|
1238
|
-
function renderFeatureSections() {
|
|
1239
|
-
return
|
|
1242
|
+
function renderFeatureSections(groups = BUILTIN_ANTI_PATTERN_GROUPS) {
|
|
1243
|
+
return groups
|
|
1244
|
+
.map((group) => group.featureSection)
|
|
1245
|
+
.filter(Boolean)
|
|
1246
|
+
.join('\n\n');
|
|
1240
1247
|
}
|
|
1241
1248
|
|
|
1242
|
-
function getManualReviewReminders() {
|
|
1243
|
-
return
|
|
1249
|
+
function getManualReviewReminders(reminders = BUILTIN_MANUAL_REVIEW_REMINDERS) {
|
|
1250
|
+
return reminders;
|
|
1244
1251
|
}
|
|
1245
1252
|
|
|
1246
1253
|
export {
|
|
1247
|
-
|
|
1254
|
+
BUILTIN_ANTI_PATTERN_GROUPS,
|
|
1255
|
+
BUILTIN_MANUAL_REVIEW_REMINDERS,
|
|
1256
|
+
// Backward-compatible aliases — keep until external callers (sync-anti-pattern-docs, tests) migrate.
|
|
1257
|
+
BUILTIN_ANTI_PATTERN_GROUPS as ANTI_PATTERN_GROUPS,
|
|
1258
|
+
BUILTIN_MANUAL_REVIEW_REMINDERS as MANUAL_REVIEW_REMINDERS,
|
|
1248
1259
|
COMMON_INTRO,
|
|
1249
1260
|
getAllJSDocTargets,
|
|
1250
1261
|
getCheckRules,
|
package/lib/check.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BUILTIN_ANTI_PATTERN_GROUPS,
|
|
6
|
+
BUILTIN_MANUAL_REVIEW_REMINDERS,
|
|
7
|
+
getCheckRules,
|
|
8
|
+
getManualReviewReminders,
|
|
9
|
+
} from './anti-pattern-rules.js';
|
|
10
|
+
import { loadAntiPatternPlugins } from './load-plugins.js';
|
|
5
11
|
import { REGEX, FONT_DOMAINS } from './constants.js';
|
|
6
12
|
|
|
7
13
|
const DEFAULT_TARGET = 'src';
|
|
8
14
|
const TEXT_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
|
|
9
15
|
const CSS_EXTENSIONS = new Set(['.css']);
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
|
|
17
|
+
// Built-ins resolved at module load so test code that imports { RULES } directly still works.
|
|
18
|
+
// en: Synchronous built-in snapshot for backward-compatible export below.
|
|
19
|
+
const BUILTIN_RULES = getCheckRules(BUILTIN_ANTI_PATTERN_GROUPS);
|
|
12
20
|
|
|
13
21
|
const SPARKLE_HEAD_JSX_PATTERN = /<SparkleHead\s*\/?\s*>/;
|
|
14
22
|
// CSP key が「実際の設定 key として記述されている」コンテキストに絞る。
|
|
@@ -102,7 +110,8 @@ function formatSnippet(text) {
|
|
|
102
110
|
* out when character-* / Sparkle tokens don't map cleanly (e.g. sub-token
|
|
103
111
|
* font sizes). Same-line or previous-line comment is honored.
|
|
104
112
|
*/
|
|
105
|
-
const SUPPRESS_LINE =
|
|
113
|
+
const SUPPRESS_LINE =
|
|
114
|
+
/(?:\/\/|\/\*|\{\/\*)\s*sparkle-disable-line\s+([A-Za-z0-9,\s_-]+?)\s*(?:\*\/\}?|$)/;
|
|
106
115
|
const SUPPRESS_NEXT_LINE =
|
|
107
116
|
/(?:\/\/|\/\*|\{\/\*)\s*sparkle-disable-next-line\s+([A-Za-z0-9,\s_-]+?)\s*(?:\*\/\}?|$)/;
|
|
108
117
|
|
|
@@ -130,7 +139,7 @@ function isSuppressed(ruleId, contentLines, lineNumber) {
|
|
|
130
139
|
return false;
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
function collectFindings(filePath, content) {
|
|
142
|
+
function collectFindings(filePath, content, rules = BUILTIN_RULES) {
|
|
134
143
|
const findings = [];
|
|
135
144
|
const contentLines = content.split(/\r?\n/);
|
|
136
145
|
const pushFinding = (rule, index, snippet) => {
|
|
@@ -146,18 +155,40 @@ function collectFindings(filePath, content) {
|
|
|
146
155
|
});
|
|
147
156
|
};
|
|
148
157
|
|
|
149
|
-
for (const rule of
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
for (const rule of rules) {
|
|
159
|
+
// rule ごとに try/catch で隔離する。1 つのプラグイン rule の throw / malformed
|
|
160
|
+
// RegExp で sparkle-design-cli check 全体が落ちるのを防ぐ。loadAntiPatternPlugins の
|
|
161
|
+
// 「warn して skip」と同じ failure mode に揃える。
|
|
162
|
+
// en: Isolate each rule. A broken plugin rule (throw / non-global regex / wrong
|
|
163
|
+
// type) must not crash the whole check pipeline.
|
|
164
|
+
try {
|
|
165
|
+
if (typeof rule.match === 'function') {
|
|
166
|
+
// rule.match(content) -> Array<{ index: number, text: string }>
|
|
167
|
+
// 複雑な照合が必要なルール向けの opt-in API。2-pass 方式などで regex 単体の
|
|
168
|
+
// backtracking リスクを回避したいときに使う。index は content 内の絶対オフセット。
|
|
169
|
+
const hits = rule.match(content);
|
|
170
|
+
if (!Array.isArray(hits)) {
|
|
171
|
+
throw new TypeError(`rule.match must return an array, got ${typeof hits}`);
|
|
172
|
+
}
|
|
173
|
+
for (const hit of hits) {
|
|
174
|
+
pushFinding(rule, hit?.index ?? 0, formatSnippet(hit?.text ?? ''));
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
156
177
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
178
|
+
if (!(rule.pattern instanceof RegExp)) {
|
|
179
|
+
throw new TypeError('rule.pattern must be a RegExp (or define rule.match instead)');
|
|
180
|
+
}
|
|
181
|
+
if (!rule.pattern.global) {
|
|
182
|
+
throw new TypeError('rule.pattern must include the global flag (e.g. /foo/g)');
|
|
183
|
+
}
|
|
184
|
+
for (const match of content.matchAll(rule.pattern)) {
|
|
185
|
+
pushFinding(rule, match.index ?? 0, formatMatch(match));
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const message = error?.message ?? String(error);
|
|
189
|
+
console.warn(
|
|
190
|
+
`⚠️ sparkle-design-cli: rule "${rule.id ?? '(unknown)'}" failed on ${toRelativeReportPath(filePath)} — skipping (${message})`
|
|
191
|
+
);
|
|
161
192
|
}
|
|
162
193
|
}
|
|
163
194
|
|
|
@@ -267,7 +298,9 @@ function collectNextjsCspFindings(cwd) {
|
|
|
267
298
|
return findings;
|
|
268
299
|
}
|
|
269
300
|
|
|
270
|
-
function createCheckReport(targets = []) {
|
|
301
|
+
function createCheckReport(targets = [], options = {}) {
|
|
302
|
+
const rules = options.rules ?? BUILTIN_RULES;
|
|
303
|
+
const baseReminders = options.baseReminders ?? BUILTIN_MANUAL_REVIEW_REMINDERS;
|
|
271
304
|
const resolvedTargets = targets.length > 0 ? targets : [DEFAULT_TARGET];
|
|
272
305
|
const textFiles = new Set();
|
|
273
306
|
const cssFiles = new Set();
|
|
@@ -289,7 +322,7 @@ function createCheckReport(targets = []) {
|
|
|
289
322
|
}
|
|
290
323
|
|
|
291
324
|
const findings = [...fileContents.entries()]
|
|
292
|
-
.flatMap(([filePath, content]) => collectFindings(filePath, content))
|
|
325
|
+
.flatMap(([filePath, content]) => collectFindings(filePath, content, rules))
|
|
293
326
|
.map((finding) => ({
|
|
294
327
|
...finding,
|
|
295
328
|
filePath: toRelativeReportPath(finding.filePath),
|
|
@@ -308,7 +341,7 @@ function createCheckReport(targets = []) {
|
|
|
308
341
|
return left.id.localeCompare(right.id);
|
|
309
342
|
});
|
|
310
343
|
|
|
311
|
-
const manualReviewReminders = [...
|
|
344
|
+
const manualReviewReminders = [...baseReminders];
|
|
312
345
|
if (!hasSparkleHeadUsage(fileContents)) {
|
|
313
346
|
manualReviewReminders.push({
|
|
314
347
|
id: 'sparkle-head-missing',
|
|
@@ -352,15 +385,9 @@ function printTextReport(report, options = {}) {
|
|
|
352
385
|
// exit code — reminders are judgment calls, not hard failures.
|
|
353
386
|
console.log('');
|
|
354
387
|
console.log('=== Manual review reminders (must acknowledge each ID in your response) ===');
|
|
355
|
-
console.log(
|
|
356
|
-
|
|
357
|
-
);
|
|
358
|
-
console.log(
|
|
359
|
-
'explicitly state the reminder ID in your reply together with whether the'
|
|
360
|
-
);
|
|
361
|
-
console.log(
|
|
362
|
-
'current code already satisfies it, or what change is needed. Silence = skipped.'
|
|
363
|
-
);
|
|
388
|
+
console.log('These are judgment calls the linter cannot detect. For every item below,');
|
|
389
|
+
console.log('explicitly state the reminder ID in your reply together with whether the');
|
|
390
|
+
console.log('current code already satisfies it, or what change is needed. Silence = skipped.');
|
|
364
391
|
console.log('');
|
|
365
392
|
for (const reminder of reminders) {
|
|
366
393
|
console.log(`- [${reminder.id}] ${reminder.message}`);
|
|
@@ -413,8 +440,19 @@ function printJsonReport(report, options = {}) {
|
|
|
413
440
|
);
|
|
414
441
|
}
|
|
415
442
|
|
|
416
|
-
export function checkProject(targets = [], options = {}) {
|
|
417
|
-
|
|
443
|
+
export async function checkProject(targets = [], options = {}) {
|
|
444
|
+
// Plugins are discovered from the consumer project's package.json deps. Errors are
|
|
445
|
+
// already warn-and-skip inside loadAntiPatternPlugins, so we just consume the result.
|
|
446
|
+
// en: Auto-discover plugins from cwd; loader handles its own failure reporting.
|
|
447
|
+
const { groups: pluginGroups, reminders: pluginReminders } = await loadAntiPatternPlugins();
|
|
448
|
+
const mergedGroups = [...BUILTIN_ANTI_PATTERN_GROUPS, ...pluginGroups];
|
|
449
|
+
const rules = getCheckRules(mergedGroups);
|
|
450
|
+
const baseReminders = getManualReviewReminders([
|
|
451
|
+
...BUILTIN_MANUAL_REVIEW_REMINDERS,
|
|
452
|
+
...pluginReminders,
|
|
453
|
+
]);
|
|
454
|
+
|
|
455
|
+
const report = createCheckReport(targets, { rules, baseReminders });
|
|
418
456
|
|
|
419
457
|
if (options.format === 'json') {
|
|
420
458
|
printJsonReport(report, options);
|
|
@@ -426,8 +464,8 @@ export function checkProject(targets = [], options = {}) {
|
|
|
426
464
|
}
|
|
427
465
|
|
|
428
466
|
export {
|
|
429
|
-
RULES,
|
|
467
|
+
BUILTIN_RULES as RULES,
|
|
430
468
|
collectFindings,
|
|
431
469
|
createCheckReport,
|
|
432
|
-
|
|
470
|
+
BUILTIN_MANUAL_REVIEW_REMINDERS as MANUAL_REVIEW_REMINDERS,
|
|
433
471
|
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
PLUGIN_PACKAGE_FIELD,
|
|
8
|
+
PLUGIN_ANTI_PATTERNS_KEY,
|
|
9
|
+
validatePluginShape,
|
|
10
|
+
} from './plugin-api.js';
|
|
11
|
+
|
|
12
|
+
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
13
|
+
|
|
14
|
+
function readJsonSafe(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function collectDependencyNames(consumerPkg) {
|
|
23
|
+
const names = new Set();
|
|
24
|
+
for (const field of DEP_FIELDS) {
|
|
25
|
+
const block = consumerPkg[field];
|
|
26
|
+
if (block && typeof block === 'object') {
|
|
27
|
+
for (const name of Object.keys(block)) {
|
|
28
|
+
names.add(name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [...names];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolvePluginPackageJson(depName, cwd) {
|
|
36
|
+
// cwd を基点に dep の package.json を解決する。tsup / pkg-roll で生成される strict な
|
|
37
|
+
// `exports` フィールドのみのパッケージは subpath `./package.json` が export されておらず
|
|
38
|
+
// require.resolve が `ERR_PACKAGE_PATH_NOT_EXPORTED` を投げる。その場合は node_modules
|
|
39
|
+
// 配下の `package.json` を fs で直接読みに行く fallback を取る(pnpm の symlink は OS 透過)。
|
|
40
|
+
// en: Primary resolve via Node's resolver; if the plugin package's `exports` does not list
|
|
41
|
+
// `./package.json`, fall back to a direct fs read so plugins are not silently skipped.
|
|
42
|
+
const requireFromCwd = createRequire(path.join(cwd, 'package.json'));
|
|
43
|
+
try {
|
|
44
|
+
return requireFromCwd.resolve(`${depName}/package.json`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
|
|
47
|
+
const fallback = path.join(cwd, 'node_modules', ...depName.split('/'), 'package.json');
|
|
48
|
+
if (fs.existsSync(fallback)) {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractPluginEntry(pkg) {
|
|
57
|
+
const block = pkg?.[PLUGIN_PACKAGE_FIELD];
|
|
58
|
+
if (!block || typeof block !== 'object') return null;
|
|
59
|
+
const entry = block[PLUGIN_ANTI_PATTERNS_KEY];
|
|
60
|
+
return typeof entry === 'string' && entry.length > 0 ? entry : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Discover and load anti-pattern plugins from the consumer's package.json deps.
|
|
65
|
+
*
|
|
66
|
+
* Returns an object of the shape:
|
|
67
|
+
* {
|
|
68
|
+
* groups: AntiPatternGroup[],
|
|
69
|
+
* reminders: ManualReviewReminder[],
|
|
70
|
+
* discovered: Array<{
|
|
71
|
+
* packageName: string,
|
|
72
|
+
* entry: string, // path declared in package.json (relative)
|
|
73
|
+
* resolvedFile: string, // absolute resolved file path
|
|
74
|
+
* status: 'loaded' | 'error',
|
|
75
|
+
* groupIds?: string[], // populated when status === 'loaded'
|
|
76
|
+
* error?: string,
|
|
77
|
+
* }>,
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* The function never throws — broken or missing plugins are reported in `discovered`
|
|
81
|
+
* and surfaced to stderr as warnings so the rest of the check can proceed.
|
|
82
|
+
*/
|
|
83
|
+
export async function loadAntiPatternPlugins({ cwd = process.cwd() } = {}) {
|
|
84
|
+
const consumerPkgPath = path.join(cwd, 'package.json');
|
|
85
|
+
const consumerPkg = readJsonSafe(consumerPkgPath);
|
|
86
|
+
if (!consumerPkg) {
|
|
87
|
+
return { groups: [], reminders: [], discovered: [] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const depNames = collectDependencyNames(consumerPkg);
|
|
91
|
+
const discovered = [];
|
|
92
|
+
const groups = [];
|
|
93
|
+
const reminders = [];
|
|
94
|
+
|
|
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' };
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.existsSync(resolvedFile)) {
|
|
110
|
+
throw new Error(`plugin entry file not found at ${resolvedFile}`);
|
|
111
|
+
}
|
|
112
|
+
const mod = await import(pathToFileURL(resolvedFile).href);
|
|
113
|
+
const candidate = mod?.default ?? mod;
|
|
114
|
+
// shape の検証は plugin-api.js と共有する。`defineAntiPatternPlugin` を呼ばずに
|
|
115
|
+
// plain object を export する plugin にも同じ厳しさを適用したい。
|
|
116
|
+
// en: Share shape validation with `defineAntiPatternPlugin` so plugins that skip
|
|
117
|
+
// the helper are still rejected at load time.
|
|
118
|
+
validatePluginShape(candidate, `${depName} (${entry})`);
|
|
119
|
+
groups.push(...candidate.groups);
|
|
120
|
+
record.groupIds = candidate.groups.map((group) => group.id);
|
|
121
|
+
if (Array.isArray(candidate.manualReviewReminders)) {
|
|
122
|
+
reminders.push(...candidate.manualReviewReminders);
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
record.status = 'error';
|
|
126
|
+
record.error = error?.message ?? String(error);
|
|
127
|
+
console.warn(
|
|
128
|
+
`⚠️ sparkle-design-cli: failed to load anti-pattern plugin from ${depName} (entry: ${entry}, error: ${record.error})`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
discovered.push(record);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { groups, reminders, discovered };
|
|
136
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sparkle-design-cli anti-pattern plugin contract.
|
|
3
|
+
*
|
|
4
|
+
* Plugin packages declare themselves in their own package.json:
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "name": "@your-org/your-design-extensions",
|
|
8
|
+
* "sparkleCli": {
|
|
9
|
+
* "antiPatterns": "./anti-patterns/index.js"
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* The referenced file must export a plugin object as the default export,
|
|
14
|
+
* typically produced by `defineAntiPatternPlugin({ ... })`.
|
|
15
|
+
*
|
|
16
|
+
* ## Plugin shape
|
|
17
|
+
*
|
|
18
|
+
* defineAntiPatternPlugin({
|
|
19
|
+
* groups: AntiPatternGroup[],
|
|
20
|
+
* manualReviewReminders?: ManualReviewReminder[],
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* ## AntiPatternGroup
|
|
24
|
+
*
|
|
25
|
+
* {
|
|
26
|
+
* id: string, // unique stable identifier (also used in --disable-line comments)
|
|
27
|
+
* featureSection?: string, // markdown emitted into the published anti-pattern doc (esa)
|
|
28
|
+
* jsdocTargets?: JSDocTarget[], // optional — JSDoc injection targets in the plugin's own source
|
|
29
|
+
* check?: {
|
|
30
|
+
* description: string, // shown in `check` findings
|
|
31
|
+
* recommendation: string, // shown in `check` findings
|
|
32
|
+
* pattern?: RegExp, // simple regex check. MUST have the global (`g`) flag.
|
|
33
|
+
* // mutually exclusive with `match`.
|
|
34
|
+
* match?: (content: string) => Array<{ index: number, text: string }>,
|
|
35
|
+
* // opt-in API for complex matching (2-pass, AST, etc.)
|
|
36
|
+
* },
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* ## JSDocTarget
|
|
40
|
+
*
|
|
41
|
+
* {
|
|
42
|
+
* file: string, // path relative to the plugin package root
|
|
43
|
+
* targetName: string, // symbol whose JSDoc block receives the anti-pattern section
|
|
44
|
+
* section: {
|
|
45
|
+
* bullets: Array<{ ja: string, en: string }>,
|
|
46
|
+
* example?: string, // tsx snippet
|
|
47
|
+
* },
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* ## ManualReviewReminder
|
|
51
|
+
*
|
|
52
|
+
* {
|
|
53
|
+
* id: string, // unique stable identifier echoed by the AI in acknowledgments
|
|
54
|
+
* message: string, // human-readable instruction
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* ## Notes
|
|
58
|
+
*
|
|
59
|
+
* - Built-in rules for the public `@goodpatch/sparkle-design` library are bundled with the CLI
|
|
60
|
+
* and not exposed as a plugin. Plugins are merged on top of the built-ins.
|
|
61
|
+
* - Plugins are auto-discovered from the consumer's `package.json` dependencies, so a plugin
|
|
62
|
+
* is active only when the consuming project actually installs the plugin package.
|
|
63
|
+
* - Plugin IDs share a namespace with built-ins. Pick a prefix (e.g. `internal-`) to avoid clashes.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
const PLUGIN_PACKAGE_FIELD = 'sparkleCli';
|
|
67
|
+
const PLUGIN_ANTI_PATTERNS_KEY = 'antiPatterns';
|
|
68
|
+
|
|
69
|
+
function isNonEmptyString(value) {
|
|
70
|
+
return typeof value === 'string' && value.length > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateGroup(group, context) {
|
|
74
|
+
if (!group || typeof group !== 'object' || Array.isArray(group)) {
|
|
75
|
+
throw new TypeError(`${context}: group entry must be a non-null object`);
|
|
76
|
+
}
|
|
77
|
+
if (!isNonEmptyString(group.id)) {
|
|
78
|
+
throw new TypeError(`${context}: group.id must be a non-empty string`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (group.check !== undefined) {
|
|
82
|
+
const { check } = group;
|
|
83
|
+
if (!check || typeof check !== 'object') {
|
|
84
|
+
throw new TypeError(`${context} (group "${group.id}"): check must be an object`);
|
|
85
|
+
}
|
|
86
|
+
if (!isNonEmptyString(check.description)) {
|
|
87
|
+
throw new TypeError(
|
|
88
|
+
`${context} (group "${group.id}"): check.description must be a non-empty string`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (!isNonEmptyString(check.recommendation)) {
|
|
92
|
+
throw new TypeError(
|
|
93
|
+
`${context} (group "${group.id}"): check.recommendation must be a non-empty string`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const hasPattern = check.pattern !== undefined;
|
|
97
|
+
const hasMatch = check.match !== undefined;
|
|
98
|
+
if (hasPattern === hasMatch) {
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
`${context} (group "${group.id}"): check must define exactly one of \`pattern\` (RegExp) or \`match\` (function)`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (hasPattern) {
|
|
104
|
+
if (!(check.pattern instanceof RegExp)) {
|
|
105
|
+
throw new TypeError(`${context} (group "${group.id}"): check.pattern must be a RegExp`);
|
|
106
|
+
}
|
|
107
|
+
// matchAll は global flag が無い RegExp で TypeError を投げるので、ここで明示的に弾く。
|
|
108
|
+
// en: Bail early so the check pipeline never reaches `content.matchAll(nonGlobal)`.
|
|
109
|
+
if (!check.pattern.global) {
|
|
110
|
+
throw new TypeError(
|
|
111
|
+
`${context} (group "${group.id}"): check.pattern must include the global flag (e.g. /foo/g)`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
} else if (typeof check.match !== 'function') {
|
|
115
|
+
throw new TypeError(`${context} (group "${group.id}"): check.match must be a function`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateReminders(reminders, context) {
|
|
121
|
+
if (reminders === undefined) return;
|
|
122
|
+
if (!Array.isArray(reminders)) {
|
|
123
|
+
throw new TypeError(`${context}: manualReviewReminders must be an array if present`);
|
|
124
|
+
}
|
|
125
|
+
for (const [index, reminder] of reminders.entries()) {
|
|
126
|
+
if (!reminder || typeof reminder !== 'object') {
|
|
127
|
+
throw new TypeError(`${context}: manualReviewReminders[${index}] must be a non-null object`);
|
|
128
|
+
}
|
|
129
|
+
if (!isNonEmptyString(reminder.id)) {
|
|
130
|
+
throw new TypeError(
|
|
131
|
+
`${context}: manualReviewReminders[${index}].id must be a non-empty string`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (!isNonEmptyString(reminder.message)) {
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
`${context}: manualReviewReminders[${index}].message must be a non-empty string`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate that the given object matches the documented plugin shape. Throws TypeError
|
|
144
|
+
* with an actionable message on the first violation. Exported so the loader can apply
|
|
145
|
+
* the same checks to plugins that don't go through `defineAntiPatternPlugin`.
|
|
146
|
+
*
|
|
147
|
+
* @param {unknown} plugin
|
|
148
|
+
* @param {string} [context] source identifier shown in error messages (e.g. `@org/pkg (./file.js)`)
|
|
149
|
+
*/
|
|
150
|
+
function validatePluginShape(plugin, context = 'plugin') {
|
|
151
|
+
if (!plugin || typeof plugin !== 'object' || Array.isArray(plugin)) {
|
|
152
|
+
throw new TypeError(`${context}: plugin must be a non-null object`);
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(plugin.groups)) {
|
|
155
|
+
throw new TypeError(`${context}: plugin.groups must be an array`);
|
|
156
|
+
}
|
|
157
|
+
for (const [index, group] of plugin.groups.entries()) {
|
|
158
|
+
validateGroup(group, `${context} groups[${index}]`);
|
|
159
|
+
}
|
|
160
|
+
validateReminders(plugin.manualReviewReminders, context);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function defineAntiPatternPlugin(plugin) {
|
|
164
|
+
validatePluginShape(plugin, 'defineAntiPatternPlugin');
|
|
165
|
+
return plugin;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const PLUGIN_SPEC_MARKDOWN = `# sparkle-design-cli anti-pattern plugin spec
|
|
169
|
+
|
|
170
|
+
Plugin packages extend \`sparkle-design-cli check\` with additional anti-pattern rules.
|
|
171
|
+
Built-in rules cover the public \`@goodpatch/sparkle-design\` library; plugins are auto-discovered
|
|
172
|
+
from the consuming project's dependencies, so private or org-specific component libraries can ship
|
|
173
|
+
their own detection rules without bundling them into the public CLI.
|
|
174
|
+
|
|
175
|
+
## Declaring a plugin
|
|
176
|
+
|
|
177
|
+
In the plugin package's own \`package.json\`:
|
|
178
|
+
|
|
179
|
+
\`\`\`json
|
|
180
|
+
{
|
|
181
|
+
"name": "@your-org/your-design-extensions",
|
|
182
|
+
"sparkleCli": {
|
|
183
|
+
"antiPatterns": "./anti-patterns/index.js"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
The referenced file must default-export a plugin object:
|
|
189
|
+
|
|
190
|
+
\`\`\`js
|
|
191
|
+
import { defineAntiPatternPlugin } from 'sparkle-design-cli/plugin';
|
|
192
|
+
|
|
193
|
+
export default defineAntiPatternPlugin({
|
|
194
|
+
groups: [
|
|
195
|
+
{
|
|
196
|
+
id: 'internal-foo-misuse',
|
|
197
|
+
featureSection: '### FooCard はネスト禁止\\n...',
|
|
198
|
+
check: {
|
|
199
|
+
description: 'FooCard を別の FooCard でラップしないでください。',
|
|
200
|
+
recommendation: '入れ子表示が必要なら FooStack を使ってください。',
|
|
201
|
+
pattern: /<FooCard[^>]*>[\\s\\S]*?<FooCard/g,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
manualReviewReminders: [
|
|
206
|
+
{
|
|
207
|
+
id: 'internal-foo-color',
|
|
208
|
+
message: 'FooCard の color token が brand に揃っているか確認してください。',
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
\`\`\`
|
|
213
|
+
|
|
214
|
+
## Discovery
|
|
215
|
+
|
|
216
|
+
When \`sparkle-design-cli\` runs in a project, it reads that project's \`package.json\`,
|
|
217
|
+
walks \`dependencies\` / \`devDependencies\` / \`peerDependencies\` / \`optionalDependencies\`,
|
|
218
|
+
and resolves each entry's own \`package.json\`. Packages whose \`package.json\` declares
|
|
219
|
+
\`sparkleCli.antiPatterns\` are loaded and merged into the check rules.
|
|
220
|
+
|
|
221
|
+
End users do not have to configure anything — installing a plugin package is enough to activate it.
|
|
222
|
+
|
|
223
|
+
## Shape reference
|
|
224
|
+
|
|
225
|
+
See the JSDoc in \`lib/plugin-api.js\` for the full type contract:
|
|
226
|
+
|
|
227
|
+
- \`AntiPatternGroup\`: \`{ id, featureSection?, jsdocTargets?, check? }\`
|
|
228
|
+
- \`AntiPatternGroup.check\`: \`{ description, recommendation, pattern | match }\`
|
|
229
|
+
- \`pattern\` is a RegExp and **must include the global flag** (\`/foo/g\`); otherwise
|
|
230
|
+
the rule is rejected at load time.
|
|
231
|
+
- \`pattern\` and \`match\` are mutually exclusive — provide exactly one.
|
|
232
|
+
- \`JSDocTarget\`: \`{ file, targetName, section: { bullets, example? } }\`
|
|
233
|
+
- \`ManualReviewReminder\`: \`{ id, message }\`
|
|
234
|
+
|
|
235
|
+
## ID namespace
|
|
236
|
+
|
|
237
|
+
Plugin rule IDs share a namespace with built-in rules and with each other. Choose a stable prefix
|
|
238
|
+
(e.g. \`internal-\`, \`acme-\`) so suppression comments like \`// sparkle-disable-next-line internal-foo-misuse\`
|
|
239
|
+
remain unambiguous.
|
|
240
|
+
|
|
241
|
+
## Failure mode
|
|
242
|
+
|
|
243
|
+
Errors loading or evaluating a plugin are logged as warnings; the rest of the check continues with
|
|
244
|
+
the rules that did load. The CLI never fails because a plugin is missing or broken.
|
|
245
|
+
`;
|
|
246
|
+
|
|
247
|
+
export {
|
|
248
|
+
PLUGIN_PACKAGE_FIELD,
|
|
249
|
+
PLUGIN_ANTI_PATTERNS_KEY,
|
|
250
|
+
PLUGIN_SPEC_MARKDOWN,
|
|
251
|
+
defineAntiPatternPlugin,
|
|
252
|
+
validatePluginShape,
|
|
253
|
+
};
|
package/lib/stop-hook.js
CHANGED
|
@@ -19,7 +19,7 @@ import { checkProject } from './check.js';
|
|
|
19
19
|
* Claude Code sets `stop_hook_active: true` on re-fires so the hook can
|
|
20
20
|
* exit cleanly instead of looping forever (see Claude Code hooks docs).
|
|
21
21
|
*/
|
|
22
|
-
export function runStopHook(target) {
|
|
22
|
+
export async function runStopHook(target) {
|
|
23
23
|
const payload = readStdinJsonSafely();
|
|
24
24
|
|
|
25
25
|
if (payload && payload.stop_hook_active === true) {
|
|
@@ -32,7 +32,7 @@ export function runStopHook(target) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const targets = target ? [target] : [];
|
|
35
|
-
const hasFindings = checkProject(targets, { strict: true, format: 'text' });
|
|
35
|
+
const hasFindings = await checkProject(targets, { strict: true, format: 'text' });
|
|
36
36
|
|
|
37
37
|
if (hasFindings) {
|
|
38
38
|
process.stderr.write(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparkle-design-cli",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
},
|
|
9
9
|
"main": "lib/generate-css.js",
|
|
10
10
|
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./lib/generate-css.js",
|
|
13
|
+
"./plugin": "./lib/plugin-api.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
11
16
|
"bin": {
|
|
12
17
|
"sparkle-design-cli": "./bin/sparkle-design.js"
|
|
13
18
|
},
|