sparkle-design-cli 2.0.7-beta.8 → 2.0.7-rc.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/README.md +134 -113
- package/lib/anti-pattern-rules.js +34 -3
- package/lib/check.js +75 -7
- package/lib/constants.js +39 -10
- package/lib/file-loader.js +13 -1
- package/lib/font-manager.js +104 -29
- package/lib/generate-css.js +205 -37
- package/lib/path-utils.js +66 -0
- package/lib/setup.js +104 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,24 +4,13 @@ Sparkle Design のプロジェクト初期セットアップ、CSS 生成、導
|
|
|
4
4
|
|
|
5
5
|
## クイックスタート
|
|
6
6
|
|
|
7
|
-
既存の Next.js / Vite
|
|
7
|
+
既存の Next.js / Vite プロジェクトで以下を実行するだけで導入完了します。詳細は [`setup` セクション](#setup-プロジェクトのフルセットアップ) を参照してください。
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npx --yes sparkle-design-cli setup --assistant claude
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
1. パッケージマネージャー(pnpm / npm / yarn / bun)を自動検出
|
|
16
|
-
2. `sparkle-design` を dependencies、`tailwindcss` + `@tailwindcss/postcss` を devDependencies に追加
|
|
17
|
-
3. 未作成の場合のみ初期ファイルを生成:
|
|
18
|
-
- `sparkle.config.json`(デフォルトは blue / BIZ UDPGothic / BIZ UDGothic / md — sparkle-design 本体のデフォルトに合わせて統一)
|
|
19
|
-
- `postcss.config.mjs`
|
|
20
|
-
- Tailwind エントリ CSS — Next.js App Router なら `src/app/globals.css`、Vite なら `src/index.css`、それ以外は `src/globals.css`(プロジェクト構成から自動判定)
|
|
21
|
-
4. AI 指示ファイル(`CLAUDE.md` / `AGENTS.md` / Cursor rules)に Sparkle Design Guard ブロックを追加
|
|
22
|
-
5. `sparkle-design-cli generate` を実行し、`sparkle-design.css` と `SparkleHead.tsx` を生成
|
|
23
|
-
|
|
24
|
-
既存ファイルは上書きされません。
|
|
13
|
+
`--assistant` は `claude` / `cursor` / `codex` / `generic` から選べます。既存ファイルは上書きされません。
|
|
25
14
|
|
|
26
15
|
## インストール
|
|
27
16
|
|
|
@@ -33,22 +22,24 @@ npm install -g sparkle-design-cli
|
|
|
33
22
|
|
|
34
23
|
### リリースチャネル
|
|
35
24
|
|
|
36
|
-
npm の dist-tag
|
|
25
|
+
npm の dist-tag でチャネルを分けています。通常は `latest` を使い、RC や beta は先行試用時のみ指定してください。
|
|
37
26
|
|
|
38
|
-
| チャネル | dist-tag |
|
|
39
|
-
|
|
|
40
|
-
| 安定版
|
|
41
|
-
|
|
|
27
|
+
| チャネル | dist-tag | バージョン形式 | 用途 |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| 安定版 | `latest` | `X.Y.Z` | 本番運用向け(デフォルト) |
|
|
30
|
+
| Release Candidate | `next` | `X.Y.Z-rc.N` | GA 直前の検証、チーム展開前 |
|
|
31
|
+
| Beta | `beta` | `X.Y.Z-beta.N` | 品質保証中の検証用 |
|
|
42
32
|
|
|
43
33
|
```bash
|
|
44
|
-
#
|
|
45
|
-
npx --yes sparkle-design-cli
|
|
34
|
+
# latest を使う(デフォルト)
|
|
35
|
+
npx --yes sparkle-design-cli setup --assistant claude
|
|
46
36
|
|
|
47
|
-
#
|
|
48
|
-
npx --yes sparkle-design-cli@
|
|
49
|
-
```
|
|
37
|
+
# RC を使う
|
|
38
|
+
npx --yes sparkle-design-cli@next setup --assistant claude
|
|
50
39
|
|
|
51
|
-
|
|
40
|
+
# beta を使う
|
|
41
|
+
npx --yes sparkle-design-cli@beta setup --assistant claude
|
|
42
|
+
```
|
|
52
43
|
|
|
53
44
|
## 使用方法
|
|
54
45
|
|
|
@@ -231,6 +222,111 @@ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaf
|
|
|
231
222
|
|
|
232
223
|
`setup` は通常実行時も JSON サマリーを stdout に表示します。`--dry-run` を付けると、その JSON を表示したままファイル変更だけを抑止します。
|
|
233
224
|
|
|
225
|
+
### 手動セットアップ(Next.js / Vite 以外のプロジェクト向け)
|
|
226
|
+
|
|
227
|
+
`setup` コマンドは **Next.js App Router** と **Vite** を前提に scaffold / entry CSS の配置を自動判定します。Remix / Astro / TanStack Start など他のフレームワークや、独自ディレクトリ構成のプロジェクトでは自動判定が外れるため、以下の手順で手動セットアップすることを推奨します。
|
|
228
|
+
|
|
229
|
+
#### 手順
|
|
230
|
+
|
|
231
|
+
**1. パッケージをインストール**
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# 本体
|
|
235
|
+
pnpm add sparkle-design # or npm install / yarn add / bun add
|
|
236
|
+
|
|
237
|
+
# Tailwind v4
|
|
238
|
+
pnpm add -D tailwindcss @tailwindcss/postcss
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**2. `sparkle.config.json` をプロジェクトルートに作成**
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"primary": "blue",
|
|
246
|
+
"font-pro": "Inter",
|
|
247
|
+
"font-mono": "JetBrains Mono",
|
|
248
|
+
"radius": "md"
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
選択肢の詳細は本 README の「[設定オプション](#設定オプション)」を参照してください。
|
|
253
|
+
|
|
254
|
+
**3. `postcss.config.mjs` をプロジェクトルートに作成**
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
export default {
|
|
258
|
+
plugins: {
|
|
259
|
+
'@tailwindcss/postcss': {},
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**4. Tailwind エントリ CSS を自前で用意**
|
|
265
|
+
|
|
266
|
+
既存プロジェクトに組み込む場合、`@import "tailwindcss";` を含む CSS ファイル(例: `src/styles/app.css`)があればそれを使います。無ければ作成してアプリケーションのエントリから import します。
|
|
267
|
+
|
|
268
|
+
**5. `generate` を実行**
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npx --yes sparkle-design-cli generate
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
これで `sparkle-design.css` が `src/app/sparkle-design.css` に生成され、`SparkleHead.tsx` も同じ場所に出ます。エントリ CSS の検出に失敗する場合は `sparkle.config.json` の `extend.globals-path` に明示指定してください。
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"primary": "blue",
|
|
279
|
+
"extend": {
|
|
280
|
+
"globals-path": "src/styles/app.css"
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**6. フォントの `<link>` タグを手動で配置**
|
|
286
|
+
|
|
287
|
+
Next.js / Vite 以外ではアプリケーションフレームワークごとに「`<head>` 相当」の配置方法が異なります。自動生成される `SparkleHead.tsx` はそのまま使えるはずですが、使えない場合は中身(`preconnect` + Google Fonts の `<link>` タグ群)を自前のレイアウトにコピーしてください。
|
|
288
|
+
|
|
289
|
+
たとえば Astro なら `src/layouts/BaseLayout.astro` の `<head>` に直接書きます:
|
|
290
|
+
|
|
291
|
+
```html
|
|
292
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
293
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
294
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:FILL,wght@0..1,500&display=block" />
|
|
295
|
+
<!-- sparkle.config.json の font-pro / font-mono に合わせた Google Fonts URL -->
|
|
296
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" />
|
|
297
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" />
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**7. アンチパターン検査を package.json に追加(任意)**
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"scripts": {
|
|
305
|
+
"lint:sparkle": "npx --yes sparkle-design-cli check src --strict",
|
|
306
|
+
"lint:sparkle:json": "npx --yes sparkle-design-cli check src --format json"
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**8. AI エージェントを使うなら Guard ブロックと hook を手動で配置**
|
|
312
|
+
|
|
313
|
+
AI ガード(`CLAUDE.md` / `AGENTS.md` / `.cursor/rules/*.mdc`)と hook 設定(`.claude/settings.json` / `.cursor/hooks.json` / `.codex/hooks.json`)は `setup` に任せるのが最も楽です。
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# ガード追加・hook 設定のみ(パッケージインストールや scaffold はスキップ)
|
|
317
|
+
npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaffold --skip-generate
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
この `--skip-*` 組み合わせは手動セットアップ済みのプロジェクトに対して Guard + hook だけ差し込むのに使えます。
|
|
321
|
+
|
|
322
|
+
#### 既知の未対応ケース
|
|
323
|
+
|
|
324
|
+
- **モノレポ(workspaces)**: `generate` は実行した cwd からパッケージマネージャーを遡及検出しないため、サブパッケージで直接実行するのが確実です。
|
|
325
|
+
- **CSS 以外のエントリ(CSS-in-JS / vanilla-extract 等)**: Sparkle Design は Tailwind v4 の `@source` + CSS variable 前提で作られているため、ビルドパイプラインの外で CSS を扱うソリューションは対象外です。
|
|
326
|
+
- **Tailwind v3**: `@source` ディレクティブ依存のため v4 必須です。
|
|
327
|
+
|
|
328
|
+
Next.js / Vite 以外で導入したい方で困ったときは Issue で教えていただけると助かります。
|
|
329
|
+
|
|
234
330
|
## 設定ファイル (sparkle.config.json)
|
|
235
331
|
|
|
236
332
|
### 設定ファイルの作成
|
|
@@ -305,39 +401,15 @@ Figma プラグインが出力する基本4項目に加えて、プロジェク
|
|
|
305
401
|
- カスタム出力先: `-o` オプションで指定可能
|
|
306
402
|
- 実行場所を基準として相対パスで処理されます
|
|
307
403
|
|
|
308
|
-
##
|
|
309
|
-
|
|
310
|
-
**v1.2.0 以降の新機能**
|
|
311
|
-
|
|
312
|
-
CLI は自動的にフォント管理を行います:
|
|
313
|
-
|
|
314
|
-
1. **フォント検出**: `sparkle-design.css` から Google Fonts の `@import` 文を検出
|
|
315
|
-
2. **エントリ CSS への移動**: フォントの `@import` を Tailwind エントリ CSS(`globals.css` / `index.css` 等、自動検出)の先頭に移動
|
|
316
|
-
3. **sparkle-design.css から削除**: 元のファイルからフォント `@import` を削除
|
|
317
|
-
|
|
318
|
-
### なぜこの処理が必要なのか?
|
|
319
|
-
|
|
320
|
-
CSS の仕様では、`@import` 文は `@charset` と `@layer` 以外のすべてのルールより前に記述する必要があります。Tailwind CSS v4 と組み合わせた場合、この順序が正しくないとビルド時に警告が発生します。
|
|
321
|
-
|
|
322
|
-
この機能により、以下の警告が自動的に解決されます:
|
|
323
|
-
|
|
324
|
-
```text
|
|
325
|
-
⚠️ @import rules must precede all rules aside from @charset and @layer statements
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### 動作条件
|
|
404
|
+
## フォントと entry CSS の自動管理
|
|
329
405
|
|
|
330
|
-
|
|
331
|
-
- エントリ CSS が存在しない場合は、フォント管理処理はスキップされます
|
|
406
|
+
CLI は **Tailwind エントリ CSS(`@import "tailwindcss"` を含む CSS ファイル)** を自動検出し、以下を 1 回で揃えます:
|
|
332
407
|
|
|
333
|
-
|
|
408
|
+
- `sparkle-design.css` の `@import`
|
|
409
|
+
- `@source "../node_modules/sparkle-design/dist"`(v4 が node_modules のクラスを拾うのに必要)
|
|
410
|
+
- フォント `<link>` タグ(React 向けは `SparkleHead.tsx`、Vite 向けは `index.html` の managed block に自動注入)
|
|
334
411
|
|
|
335
|
-
|
|
336
|
-
📦 フォント管理処理を開始します...
|
|
337
|
-
📝 3個のフォントimportを検出しました
|
|
338
|
-
✅ エントリ CSS にフォントimportを追加しました
|
|
339
|
-
✅ sparkle-design.css からフォントimportを削除しました
|
|
340
|
-
```
|
|
412
|
+
CSS 仕様上 `@import` は他の at-rule より前に書く必要があるため、順序も適切に整えます。`@import "tailwindcss"` が欠けている場合は先頭に自動追記されます。
|
|
341
413
|
|
|
342
414
|
## 開発
|
|
343
415
|
|
|
@@ -354,71 +426,20 @@ npm link
|
|
|
354
426
|
### 開発用コマンド
|
|
355
427
|
|
|
356
428
|
```bash
|
|
357
|
-
#
|
|
358
|
-
npm
|
|
359
|
-
|
|
360
|
-
# テスト監視モード
|
|
361
|
-
npm run test:watch
|
|
362
|
-
|
|
363
|
-
# Lint実行
|
|
364
|
-
npm run lint
|
|
365
|
-
|
|
366
|
-
# Lintエラーを自動修正
|
|
367
|
-
npm run lint:fix
|
|
368
|
-
|
|
369
|
-
# コードフォーマット
|
|
370
|
-
npm run format
|
|
371
|
-
|
|
372
|
-
# フォーマットチェック
|
|
373
|
-
npm run format:check
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### CLI実行テスト
|
|
377
|
-
|
|
378
|
-
```bash
|
|
379
|
-
# CSS生成
|
|
380
|
-
sparkle-design-cli generate
|
|
381
|
-
|
|
382
|
-
# ヘルプ表示
|
|
383
|
-
sparkle-design-cli --help
|
|
384
|
-
sparkle-design-cli generate --help
|
|
385
|
-
|
|
386
|
-
# 検査
|
|
387
|
-
sparkle-design-cli check src --strict
|
|
429
|
+
npm test # Node.js test runner
|
|
430
|
+
npm run lint # ESLint
|
|
431
|
+
npm run format # Prettier
|
|
388
432
|
```
|
|
389
433
|
|
|
390
434
|
### リリース手順(メンテナ向け)
|
|
391
435
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
#### 安定版(latest)
|
|
395
|
-
|
|
396
|
-
1. `main` ブランチに変更をマージ
|
|
397
|
-
2. `package.json` の `version` を SemVer で更新(例: `2.0.6` → `2.1.0`)し、`CHANGELOG.md` に該当セクションを追加
|
|
398
|
-
3. PR をマージ後、Actions の **Publish to npm** を `channel: auto`(または `latest`)で手動実行
|
|
399
|
-
4. workflow は lint / test を流したうえで `npm publish`(dist-tag = latest)を実行
|
|
400
|
-
|
|
401
|
-
#### Beta(beta)
|
|
436
|
+
publish は GitHub Actions の **Publish to npm** workflow 経由。ローカル `npm publish` は禁止。
|
|
402
437
|
|
|
403
|
-
|
|
438
|
+
1. `package.json` の `version` を更新(安定版: `X.Y.Z` / RC: `X.Y.Z-rc.N` / Beta: `X.Y.Z-beta.N`)
|
|
439
|
+
2. `CHANGELOG.md` に該当セクションを追加
|
|
440
|
+
3. PR をマージ後、**Publish to npm** workflow を `channel: auto` で実行
|
|
404
441
|
|
|
405
|
-
|
|
406
|
-
2. `CHANGELOG.md` に `[X.Y.Z-beta.N]` セクションを追加
|
|
407
|
-
3. PR をマージ後、Actions の **Publish to npm** を `channel: auto`(または `beta`)で手動実行
|
|
408
|
-
4. workflow が version を判定して `npm publish --tag beta` を実行
|
|
409
|
-
5. 利用者側は `npx --yes sparkle-design-cli@beta ...` で検証
|
|
410
|
-
|
|
411
|
-
#### Beta から GA(latest)へ昇格
|
|
412
|
-
|
|
413
|
-
同じ成果物を latest にする場合は、新しい安定版 `X.Y.Z` を publish するか、既存 beta バージョンに latest タグを付け替えます。
|
|
414
|
-
|
|
415
|
-
```bash
|
|
416
|
-
# 選択肢 A: 新たに X.Y.Z を publish する(推奨)
|
|
417
|
-
# version を X.Y.Z に更新 → workflow を latest で実行
|
|
418
|
-
|
|
419
|
-
# 選択肢 B: 既存の X.Y.Z-beta.N に latest タグを付け替える
|
|
420
|
-
npm dist-tag add sparkle-design-cli@X.Y.Z-beta.N latest
|
|
421
|
-
```
|
|
442
|
+
`channel: auto` は `package.json` の version 形式から dist-tag を自動判定します(`-beta.N` → `beta` / `-rc.N` → `next` / それ以外 → `latest`)。既存 RC / Beta を latest に昇格させる場合は、新しい `X.Y.Z` として改めて publish します(`npm dist-tag add` での手動付け替えも可能ですが、version 管理が明確になる前者を推奨)。
|
|
422
443
|
|
|
423
444
|
## ライセンス
|
|
424
445
|
|
|
@@ -1049,8 +1049,28 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
1049
1049
|
description: 'Card 系コンポーネントのデフォルト padding を安易に上書きしない',
|
|
1050
1050
|
recommendation:
|
|
1051
1051
|
'CardHeader / CardContent のデフォルト padding(px-6 py-2)をそのまま使ってください。上書きは本当に必要な場合のみ。',
|
|
1052
|
-
pattern
|
|
1053
|
-
|
|
1052
|
+
// 旧 pattern は `[^"]*\b...[^"]*` の 2 つの `[^"]*` が同じ文字列を食い合い
|
|
1053
|
+
// quadratic backtracking になっていた(閉じ quote 無しの壊れた className
|
|
1054
|
+
// で n=80k / 12s の regression)。2-pass 化して linear time を保証する:
|
|
1055
|
+
// 1. `<CardHeader className="..." ...>` / `{...}` 形式の className 値
|
|
1056
|
+
// を確定マッチで抽出
|
|
1057
|
+
// 2. 抽出した className 値に padding utility が含まれているか
|
|
1058
|
+
// `\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)` で test する
|
|
1059
|
+
// en: Switch to a 2-pass matcher. The old regex had quadratic
|
|
1060
|
+
// backtracking on malformed input; now we first capture the whole
|
|
1061
|
+
// className value, then test for padding utilities in isolation.
|
|
1062
|
+
match: (content) => {
|
|
1063
|
+
const OPEN_TAG =
|
|
1064
|
+
/<Card(?:Header|Content)\b[^>]*\bclassName\s*=\s*("[^"]*"|'[^']*'|\{[^}]*\})[^>]*>/g;
|
|
1065
|
+
const PADDING_UTIL = /\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)/;
|
|
1066
|
+
const results = [];
|
|
1067
|
+
for (const m of content.matchAll(OPEN_TAG)) {
|
|
1068
|
+
if (PADDING_UTIL.test(m[1])) {
|
|
1069
|
+
results.push({ index: m.index, text: m[0] });
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return results;
|
|
1073
|
+
},
|
|
1054
1074
|
},
|
|
1055
1075
|
featureSection: lines([
|
|
1056
1076
|
'### Card 系コンポーネントの padding を上書きしない',
|
|
@@ -1146,7 +1166,18 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
1146
1166
|
description: 'Icon の children にテキストを渡さない',
|
|
1147
1167
|
recommendation:
|
|
1148
1168
|
'Icon には icon prop でアイコン名を渡してください。children にテキストを渡すのは旧 Material Icons の書き方です。',
|
|
1149
|
-
pattern
|
|
1169
|
+
// 旧 pattern は `(?:[^>]|\/(?!>))*` が `/ ` を両分岐にマッチさせるため、
|
|
1170
|
+
// 閉じ `>` が無い入力で exponential backtracking する ReDoS の種だった
|
|
1171
|
+
// (n=200 で 48s 消費)。属性部を `[^>]*` の単一分岐に戻して linear に
|
|
1172
|
+
// する。代償として自己閉じ `<Icon ... />` にも `<Icon ... >...</Icon>`
|
|
1173
|
+
// として表面的にはマッチし得るが、自己閉じは `/` が末尾に来るので
|
|
1174
|
+
// `>[^<]+` の後続(children text)に空白以外が必要になり、現実の JSX
|
|
1175
|
+
// では自己閉じと open+close タグが入り乱れる頻度は低い。1-pass で拾い
|
|
1176
|
+
// きれないケースは 2-pass に引き上げることで将来対応する。
|
|
1177
|
+
// en: Old pattern suffered exponential ReDoS because `/ ` fit both
|
|
1178
|
+
// branches of the alternation. Use a linear `[^>]*` and accept a
|
|
1179
|
+
// small risk of self-closing false positives (rare in practice).
|
|
1180
|
+
pattern: /<Icon\b[^>]*>[^<]+<\/Icon>/g,
|
|
1150
1181
|
},
|
|
1151
1182
|
featureSection: lines([
|
|
1152
1183
|
'### Icon の children にテキストを渡さない',
|
package/lib/check.js
CHANGED
|
@@ -11,7 +11,18 @@ const RULES = getCheckRules();
|
|
|
11
11
|
const BASE_MANUAL_REVIEW_REMINDERS = getManualReviewReminders();
|
|
12
12
|
|
|
13
13
|
const SPARKLE_HEAD_JSX_PATTERN = /<SparkleHead\s*\/?\s*>/;
|
|
14
|
-
|
|
14
|
+
// CSP key が「実際の設定 key として記述されている」コンテキストに絞る。
|
|
15
|
+
// 以前は単純に `/content-security-policy|.../i` だったため、コメント行
|
|
16
|
+
// (`// TODO: Add Content-Security-Policy later` 等)にだけマッチして
|
|
17
|
+
// false-positive の finding を emit していた。
|
|
18
|
+
// - `'Content-Security-Policy': ...` (object property with quoted string key)
|
|
19
|
+
// - `contentSecurityPolicy: ...` (JS camelCase property)
|
|
20
|
+
// - `"Content-Security-Policy"` を value として持つ header エントリ(`key: 'Content-Security-Policy'`)
|
|
21
|
+
// を許容し、単体コメント文字列には反応しないようにする。
|
|
22
|
+
// en: Restrict CSP detection to contexts that look like actual config entries,
|
|
23
|
+
// avoiding false positives from mere comment mentions.
|
|
24
|
+
const CSP_PATTERN =
|
|
25
|
+
/(?:['"]Content-Security-Policy['"]\s*[:,]|contentSecurityPolicy\s*[:=]|key\s*:\s*['"]Content-Security-Policy['"])/;
|
|
15
26
|
const NEXT_CONFIG_CANDIDATES = [
|
|
16
27
|
'next.config.js',
|
|
17
28
|
'next.config.ts',
|
|
@@ -160,22 +171,38 @@ function collectNextjsCspFindings(cwd) {
|
|
|
160
171
|
let content;
|
|
161
172
|
try {
|
|
162
173
|
content = fs.readFileSync(configPath, 'utf8');
|
|
163
|
-
} catch {
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// ENOENT は候補が存在しないだけなので静かに次へ。EACCES などは
|
|
176
|
+
// デバッグ価値があるので warn する(silent skip で「check したが
|
|
177
|
+
// CSP 設定が無かった」と誤解されないように)。
|
|
178
|
+
// en: ENOENT = config file absent, move on quietly. Other codes
|
|
179
|
+
// are worth surfacing so the user knows detection was skipped.
|
|
180
|
+
if (error.code !== 'ENOENT') {
|
|
181
|
+
console.warn(
|
|
182
|
+
`⚠️ ${candidate} の読み込みに失敗したため CSP 検査をスキップします (${error.code ?? error.message})`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
164
185
|
continue;
|
|
165
186
|
}
|
|
166
187
|
|
|
167
188
|
if (!CSP_PATTERN.test(content)) continue;
|
|
168
189
|
|
|
190
|
+
// host の `.` を regex escape しないと `fontsXgoogleapisXcom` などにも
|
|
191
|
+
// マッチし、本来足りない allowlist を「足りている」と誤判定する false
|
|
192
|
+
// negative が起きる。literal `.` として検査する。
|
|
193
|
+
// en: Escape `.` so we match the literal hostname and don't accept
|
|
194
|
+
// arbitrary strings as an allowlist match.
|
|
195
|
+
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
169
196
|
const missingDomains = [
|
|
170
197
|
{
|
|
171
198
|
domain: 'fonts.googleapis.com',
|
|
172
199
|
directive: 'style-src',
|
|
173
|
-
pattern: new RegExp(FONT_DOMAINS.GOOGLEAPIS.replace('https://', '')),
|
|
200
|
+
pattern: new RegExp(escapeRegex(FONT_DOMAINS.GOOGLEAPIS.replace('https://', ''))),
|
|
174
201
|
},
|
|
175
202
|
{
|
|
176
203
|
domain: 'fonts.gstatic.com',
|
|
177
204
|
directive: 'font-src',
|
|
178
|
-
pattern: new RegExp(FONT_DOMAINS.GSTATIC.replace('https://', '')),
|
|
205
|
+
pattern: new RegExp(escapeRegex(FONT_DOMAINS.GSTATIC.replace('https://', ''))),
|
|
179
206
|
},
|
|
180
207
|
].filter(({ pattern }) => !pattern.test(content));
|
|
181
208
|
|
|
@@ -268,9 +295,37 @@ function printTextReport(report, options = {}) {
|
|
|
268
295
|
}
|
|
269
296
|
}
|
|
270
297
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
298
|
+
const reminders = report.manualReviewReminders ?? [];
|
|
299
|
+
if (reminders.length === 0) {
|
|
300
|
+
// 0 件のときは従来通り静かに済ます。
|
|
301
|
+
// en: Nothing to review — keep quiet.
|
|
302
|
+
} else {
|
|
303
|
+
// Guard block から参照される acknowledgment フォーマットと合わせるため、
|
|
304
|
+
// AI 向けに「各 ID を response に echo して確認したことを示してほしい」
|
|
305
|
+
// と明示的に要求する。hook による exit blocking はしない(判断事項のため)。
|
|
306
|
+
// en: Ask the AI to echo each reminder ID in its final response, so humans
|
|
307
|
+
// can see which reminders were considered. This is not enforced by hook
|
|
308
|
+
// exit code — reminders are judgment calls, not hard failures.
|
|
309
|
+
console.log('');
|
|
310
|
+
console.log('=== Manual review reminders (must acknowledge each ID in your response) ===');
|
|
311
|
+
console.log(
|
|
312
|
+
'These are judgment calls the linter cannot detect. For every item below,'
|
|
313
|
+
);
|
|
314
|
+
console.log(
|
|
315
|
+
'explicitly state the reminder ID in your reply together with whether the'
|
|
316
|
+
);
|
|
317
|
+
console.log(
|
|
318
|
+
'current code already satisfies it, or what change is needed. Silence = skipped.'
|
|
319
|
+
);
|
|
320
|
+
console.log('');
|
|
321
|
+
for (const reminder of reminders) {
|
|
322
|
+
console.log(`- [${reminder.id}] ${reminder.message}`);
|
|
323
|
+
}
|
|
324
|
+
console.log('');
|
|
325
|
+
console.log(
|
|
326
|
+
'Example acknowledgment: "[badge-tag-semantics] reviewed — Badge used only for counts (OK). [dialog-modal-ux] reviewed — changed Dialog to Modal for form case."'
|
|
327
|
+
);
|
|
328
|
+
console.log('=========================================================================');
|
|
274
329
|
}
|
|
275
330
|
|
|
276
331
|
if (options.strict && report.findings.length > 0) {
|
|
@@ -281,6 +336,7 @@ function printTextReport(report, options = {}) {
|
|
|
281
336
|
function printJsonReport(report, options = {}) {
|
|
282
337
|
const strictMode = Boolean(options.strict);
|
|
283
338
|
const passed = !(strictMode && report.findings.length > 0);
|
|
339
|
+
const reminders = report.manualReviewReminders ?? [];
|
|
284
340
|
|
|
285
341
|
console.log(
|
|
286
342
|
JSON.stringify(
|
|
@@ -289,6 +345,18 @@ function printJsonReport(report, options = {}) {
|
|
|
289
345
|
targetCount: report.targets.length,
|
|
290
346
|
checkedFileCount: report.checkedFiles.length,
|
|
291
347
|
findingCount: report.findings.length,
|
|
348
|
+
reminderCount: reminders.length,
|
|
349
|
+
// AI 向けの attention フラグ。reminder が 1 件でもあれば true になり、
|
|
350
|
+
// AI は最終 response で各 reminder ID を echo する必要がある。
|
|
351
|
+
// exit code には寄与しない(判断事項を hook で block するのは過剰)。
|
|
352
|
+
// en: AI attention flag. When reminders exist, the AI MUST echo each
|
|
353
|
+
// reminder ID in its final reply. Not tied to exit code — reminders
|
|
354
|
+
// are judgment calls, not hard errors.
|
|
355
|
+
manualReviewRequired: reminders.length > 0,
|
|
356
|
+
reminderAcknowledgmentFormat:
|
|
357
|
+
reminders.length > 0
|
|
358
|
+
? 'Respond with each reminder ID followed by your review, e.g. `[badge-tag-semantics] reviewed — Badge is used only for counts (OK)`.'
|
|
359
|
+
: null,
|
|
292
360
|
strictMode,
|
|
293
361
|
passed,
|
|
294
362
|
exitCode: passed ? 0 : 1,
|
package/lib/constants.js
CHANGED
|
@@ -33,6 +33,21 @@ export const GLOBALS_CSS_CANDIDATES = [
|
|
|
33
33
|
'src/styles/globals.css',
|
|
34
34
|
];
|
|
35
35
|
|
|
36
|
+
// Vite プロジェクト判定に使う config ファイル候補。setup.js(scaffold
|
|
37
|
+
// 判定)と font-manager.js(resolveGlobalsPath / index.html 注入判定)
|
|
38
|
+
// で同じリストを参照する。片方だけ変えると scaffold と generate が別
|
|
39
|
+
// 判定をしてしまうので、唯一の source of truth としてここに置く。
|
|
40
|
+
// en: Single source of truth for Vite project detection. Reused by
|
|
41
|
+
// setup.js's scaffold logic and font-manager.js's resolver.
|
|
42
|
+
export const VITE_CONFIG_FILES = [
|
|
43
|
+
'vite.config.ts',
|
|
44
|
+
'vite.config.js',
|
|
45
|
+
'vite.config.mjs',
|
|
46
|
+
'vite.config.cjs',
|
|
47
|
+
'vite.config.mts',
|
|
48
|
+
'vite.config.cts',
|
|
49
|
+
];
|
|
50
|
+
|
|
36
51
|
// 正規表現パターン
|
|
37
52
|
export const REGEX = {
|
|
38
53
|
// フォント関連
|
|
@@ -44,16 +59,20 @@ export const REGEX = {
|
|
|
44
59
|
/\/\*\s*フォントのインポート[^*]*\*\/\s*\n?(?:@import\s+(?:url\([^)]+fonts\.googleapis\.com[^)]+\)|['"][^"']*fonts\.googleapis\.com[^"']*['"]);?\s*\n?)*\n?/g,
|
|
45
60
|
|
|
46
61
|
// Sparkle Design関連
|
|
47
|
-
// `@import "sparkle-design.css"` / `@import "./sparkle-design.css"`
|
|
48
|
-
// `@import "./app/sparkle-design.css"`
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
62
|
+
// `@import "sparkle-design.css"` / `@import "./sparkle-design.css"` だけでなく、
|
|
63
|
+
// `@import "./app/sparkle-design.css"` のように entry CSS と sparkle-design.css
|
|
64
|
+
// が別ディレクトリにある Vite 系レイアウトの variant も除去対象にする。
|
|
65
|
+
// ただし **path prefix を `./` または `../` に限定**し、末尾の `sparkle-design.css`
|
|
66
|
+
// は path セパレータ直後であることを要求することで、ユーザーが独自命名した
|
|
67
|
+
// `custom-sparkle-design.css` / `my-sparkle-design.css` などを誤削除しないように
|
|
68
|
+
// している(beta.10 まではここが緩く、suffix match で user-authored ファイルを
|
|
69
|
+
// 巻き込む silent data loss リスクがあった)。
|
|
70
|
+
// en: Match paths ending in `sparkle-design.css` but require a `./` or `../`
|
|
71
|
+
// prefix and a path separator just before the filename. This preserves user-
|
|
72
|
+
// authored files like `custom-sparkle-design.css` that would otherwise be
|
|
73
|
+
// deleted by a loose suffix match.
|
|
55
74
|
SPARKLE_IMPORT:
|
|
56
|
-
/\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"][^"']
|
|
75
|
+
/\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"](?:(?:\.{1,2}\/)[^"']*\/)?sparkle-design\.css['"];?\s*\n?/g,
|
|
57
76
|
TAILWIND_IMPORT: /@import\s+['"]tailwindcss['"];?/,
|
|
58
77
|
|
|
59
78
|
// @source ディレクティブ関連
|
|
@@ -70,8 +89,18 @@ export const REGEX = {
|
|
|
70
89
|
/\/\*\s*npm パッケージのコンテンツスキャン[^*]*\*\/\s*\n?(?:@source\s+["'][^"']*["'];?\s*\n?)*/g,
|
|
71
90
|
|
|
72
91
|
// カスタムCSS関連
|
|
92
|
+
// 除去対象は「CLI が書き込んだ custom CSS の managed block」のみ。以前は
|
|
93
|
+
// 文字列 "custom" を含む任意の import をすべて削っていたため、
|
|
94
|
+
// `customer-module.css` / `my-customer-ui.css` / `customizer.css` など
|
|
95
|
+
// ユーザー命名の import まで巻き込んで silent data loss していた。
|
|
96
|
+
// CLI が書くのは `/* プロジェクト固有のカスタムトークン */` コメント直後の
|
|
97
|
+
// 1 行 `@import` だけなので、そのペアだけをマッチする。
|
|
98
|
+
// en: Match only the managed block the CLI writes (comment + one `@import`
|
|
99
|
+
// line). Previously any `@import` containing the substring "custom" was
|
|
100
|
+
// deleted, which would silently remove user-authored files like
|
|
101
|
+
// `customer-module.css`.
|
|
73
102
|
CUSTOM_CSS_IMPORT:
|
|
74
|
-
/\/\*\s*プロジェクト固有のカスタムトークン[^*]*\*\/\s*\n
|
|
103
|
+
/\/\*\s*プロジェクト固有のカスタムトークン[^*]*\*\/\s*\n?(?:@import\s+['"][^"']+['"];?\s*\n?)?/g,
|
|
75
104
|
|
|
76
105
|
// テンプレート関連
|
|
77
106
|
COLOR_TOKENS_PLACEHOLDER: /[ \t]*\/\* \{\{COLOR_TOKENS\}\} \*\/[ \t]*\n?/,
|
package/lib/file-loader.js
CHANGED
|
@@ -46,11 +46,23 @@ function readFile(filePath, successMessage, errorMessage, additionalErrorHandler
|
|
|
46
46
|
* @param {Function} additionalErrorHandler 追加のエラーハンドラ(オプション)
|
|
47
47
|
* @returns {Object} パース済みのJSONオブジェクト
|
|
48
48
|
*/
|
|
49
|
+
// Prototype pollution 対策: `__proto__` / `constructor` / `prototype` を
|
|
50
|
+
// JSON から drop する reviver。本プロセス内の spread は安全だが、書き戻し
|
|
51
|
+
// 先(hook JSON / package.json 等)経由で下流 consumer に流れたときに
|
|
52
|
+
// プロトタイプ汚染が起きる古典パターンを断つ。
|
|
53
|
+
// en: Strip prototype-pollution keys at parse time so they cannot travel
|
|
54
|
+
// through serialized files to downstream consumers.
|
|
55
|
+
const POLLUTION_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
56
|
+
function jsonPollutionReviver(key, value) {
|
|
57
|
+
if (POLLUTION_KEYS.has(key)) return undefined;
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
function readJsonFile(filePath, successMessage, errorMessage, additionalErrorHandler = null) {
|
|
50
62
|
const content = readFile(filePath, successMessage, errorMessage, additionalErrorHandler);
|
|
51
63
|
|
|
52
64
|
try {
|
|
53
|
-
return JSON.parse(content);
|
|
65
|
+
return JSON.parse(content, jsonPollutionReviver);
|
|
54
66
|
} catch (error) {
|
|
55
67
|
const message =
|
|
56
68
|
typeof errorMessage === 'function'
|