sparkle-design-cli 2.0.6 → 2.0.7-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/README.md CHANGED
@@ -31,6 +31,25 @@ npm install -g sparkle-design-cli
31
31
 
32
32
  > `npx --yes sparkle-design-cli ...` で都度実行する運用を推奨します(常に最新版が利用されます)。
33
33
 
34
+ ### リリースチャネル
35
+
36
+ npm の dist-tag でチャネルを分離しています。通常は latest を使い、品質保証中の変更を試したい場合のみ beta を指定してください。
37
+
38
+ | チャネル | dist-tag | 用途 | 指定方法 |
39
+ |---------|---------|------|---------|
40
+ | 安定版 | `latest` | 本番運用向け。`npx --yes sparkle-design-cli` は常にこれを取得 | (デフォルト) |
41
+ | Beta | `beta` | 品質保証中の検証用 | `npx --yes sparkle-design-cli@beta` |
42
+
43
+ ```bash
44
+ # beta で setup を試す
45
+ npx --yes sparkle-design-cli@beta setup --assistant claude
46
+
47
+ # beta で check を試す
48
+ npx --yes sparkle-design-cli@beta check src --strict
49
+ ```
50
+
51
+ Beta バージョンは `X.Y.Z-beta.N` 形式で publish されます。GA(正式版)へ昇格するときは、beta の -beta.N を外した `X.Y.Z` を改めて publish します。
52
+
34
53
  ## 使用方法
35
54
 
36
55
  ### サブコマンド
@@ -362,6 +381,39 @@ sparkle-design-cli generate --help
362
381
  sparkle-design-cli check src --strict
363
382
  ```
364
383
 
384
+ ### リリース手順(メンテナ向け)
385
+
386
+ npm publish は GitHub Actions (`Publish to npm`) 経由で行います。ローカルから `npm publish` しないでください。
387
+
388
+ #### 安定版(latest)
389
+
390
+ 1. `main` ブランチに変更をマージ
391
+ 2. `package.json` の `version` を SemVer で更新(例: `2.0.6` → `2.1.0`)し、`CHANGELOG.md` に該当セクションを追加
392
+ 3. PR をマージ後、Actions の **Publish to npm** を `channel: auto`(または `latest`)で手動実行
393
+ 4. workflow は lint / test を流したうえで `npm publish`(dist-tag = latest)を実行
394
+
395
+ #### Beta(beta)
396
+
397
+ 品質保証中の変更を先行公開したいときに使います。latest には影響しません。
398
+
399
+ 1. `package.json` の `version` を `X.Y.Z-beta.N` 形式に更新(例: `2.1.0-beta.0`、次の beta は `2.1.0-beta.1`)
400
+ 2. `CHANGELOG.md` に `[X.Y.Z-beta.N]` セクションを追加
401
+ 3. PR をマージ後、Actions の **Publish to npm** を `channel: auto`(または `beta`)で手動実行
402
+ 4. workflow が version を判定して `npm publish --tag beta` を実行
403
+ 5. 利用者側は `npx --yes sparkle-design-cli@beta ...` で検証
404
+
405
+ #### Beta から GA(latest)へ昇格
406
+
407
+ 同じ成果物を latest にする場合は、新しい安定版 `X.Y.Z` を publish するか、既存 beta バージョンに latest タグを付け替えます。
408
+
409
+ ```bash
410
+ # 選択肢 A: 新たに X.Y.Z を publish する(推奨)
411
+ # version を X.Y.Z に更新 → workflow を latest で実行
412
+
413
+ # 選択肢 B: 既存の X.Y.Z-beta.N に latest タグを付け替える
414
+ npm dist-tag add sparkle-design-cli@X.Y.Z-beta.N latest
415
+ ```
416
+
365
417
  ## ライセンス
366
418
 
367
419
  MIT License - 詳細は [LICENSE](LICENSE) ファイルを参照してください。
@@ -23,7 +23,6 @@ function renderJSDocSection(section) {
23
23
  return output.map((line) => (line ? ` * ${line}` : ' *')).join('\n');
24
24
  }
25
25
 
26
-
27
26
  const MANUAL_REVIEW_REMINDERS = [
28
27
  {
29
28
  id: 'badge-tag-semantics',
@@ -492,7 +491,8 @@ const ANTI_PATTERN_GROUPS = [
492
491
  {
493
492
  id: 'card-clickable-wrap',
494
493
  check: {
495
- description: 'クリック可能な Card は ClickableCard を使う(button / a / role="button" でラップしない)',
494
+ description:
495
+ 'クリック可能な Card は ClickableCard を使う(button / a / role="button" でラップしない)',
496
496
  recommendation:
497
497
  '`<Card>` を `<button>` / `<a>` / `role="button"` を持つ要素で包まず、`ClickableCard` を使ってください。`ClickableCard` が適切な role / keyboard 対応 / focus ring を担保します。',
498
498
  // <button> / <a> / role="button" が直接 <Card> を子に持つケース
@@ -949,7 +949,8 @@ const ANTI_PATTERN_GROUPS = [
949
949
  description: 'CardTitle に typography 系クラスを付与しない',
950
950
  recommendation:
951
951
  'CardTitle は character-4-bold-pro を内蔵しています。className で typography を上書きしないでください。',
952
- pattern: /<CardTitle\b[^>]*\bclassName\s*=\s*(?:"[^"]*\bcharacter-[^"]*"|'[^']*\bcharacter-[^']*'|\{[^}]*\bcharacter-[^}]*\})[^>]*>/g,
952
+ pattern:
953
+ /<CardTitle\b[^>]*\bclassName\s*=\s*(?:"[^"]*\bcharacter-[^"]*"|'[^']*\bcharacter-[^']*'|\{[^}]*\bcharacter-[^}]*\})[^>]*>/g,
953
954
  },
954
955
  featureSection: lines([
955
956
  '### CardTitle に typography を上書きしない',
@@ -970,7 +971,21 @@ const ANTI_PATTERN_GROUPS = [
970
971
  description: 'CardControl に Button / IconButton 以外を入れない',
971
972
  recommendation:
972
973
  'CardControl はアクションボタン用です。ステータス表示には CardDescription を使ってください。',
973
- pattern: /<CardControl\b[^>]*>[\s\S]*?<(?!Button\b|IconButton\b|\/CardControl)\w+/g,
974
+ // 以前の regex は `[\s\S]*?` が `</CardControl>` を越えて貪欲に探索してしまい、
975
+ // 隣接する <CardContent> 等を非 Button 子要素として誤検知していた。
976
+ // 内部コンテンツが `</CardControl>` を越えないよう lazy 側に停止条件を入れ、
977
+ // 開始タグの直後から終了タグ直前までの範囲だけを検査する。
978
+ // 加えて (1) 開始タグ自体が自己閉じ `/>` で終わっていたら子要素は存在しない
979
+ // ので検査対象外とする(`[^>/]*[^>/]?`)、(2) ネストした `<CardControl>` も
980
+ // Button/IconButton 扱いのスキップリストに加えて誤発報を防ぐ。
981
+ // en: The previous pattern let `[\s\S]*?` leak past `</CardControl>`,
982
+ // flagging adjacent siblings (e.g. `<CardContent>`) as non-Button children.
983
+ // We now (a) constrain the lazy body so it cannot cross the closing tag,
984
+ // (b) skip self-closing tags `<CardControl />` entirely, and
985
+ // (c) treat a nested `<CardControl>` as an allowed child so we don't
986
+ // report the inner wrapper of a pathological nested structure.
987
+ pattern:
988
+ /<CardControl\b(?:[^>]*[^>/])?>(?:(?!<\/CardControl\b)[\s\S])*?<(?!Button\b|IconButton\b|CardControl\b|\/CardControl\b)[A-Za-z][\w.]*/g,
974
989
  },
975
990
  featureSection: lines([
976
991
  '### CardControl にはアクションボタンのみを入れる',
@@ -1020,7 +1035,8 @@ const ANTI_PATTERN_GROUPS = [
1020
1035
  description: 'Card 系コンポーネントのデフォルト padding を安易に上書きしない',
1021
1036
  recommendation:
1022
1037
  'CardHeader / CardContent のデフォルト padding(px-6 py-2)をそのまま使ってください。上書きは本当に必要な場合のみ。',
1023
- pattern: /<Card(?:Header|Content)\b[^>]*\bclassName\s*=\s*(?:"[^"]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^"]*"|'[^']*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^']*'|\{[^}]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^}]*\})[^>]*>/g,
1038
+ pattern:
1039
+ /<Card(?:Header|Content)\b[^>]*\bclassName\s*=\s*(?:"[^"]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^"]*"|'[^']*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^']*'|\{[^}]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^}]*\})[^>]*>/g,
1024
1040
  },
1025
1041
  featureSection: lines([
1026
1042
  '### Card 系コンポーネントの padding を上書きしない',
@@ -1045,7 +1061,8 @@ const ANTI_PATTERN_GROUPS = [
1045
1061
  description: 'asChild 使用時に prefixIcon / suffixIcon / isLoading を使わない',
1046
1062
  recommendation:
1047
1063
  'asChild モードでは prefixIcon / suffixIcon / isLoading は無視されます。アイコン付きの Link が必要なら asChild を外してください。',
1048
- pattern: /<Button\b(?=[^>]*\basChild\b)(?=[^>]*\b(?:prefixIcon|suffixIcon|isLoading)\b)[^>]*>/g,
1064
+ pattern:
1065
+ /<Button\b(?=[^>]*\basChild\b)(?=[^>]*\b(?:prefixIcon|suffixIcon|isLoading)\b)[^>]*>/g,
1049
1066
  },
1050
1067
  featureSection: lines([
1051
1068
  '### asChild と prefixIcon / suffixIcon / isLoading を併用しない',
@@ -1133,7 +1150,21 @@ const ANTI_PATTERN_GROUPS = [
1133
1150
  ];
1134
1151
 
1135
1152
  function getCheckRules() {
1136
- const order = ['dialog-form', 'dialog-button-wrap', 'button-icon-only', 'material-symbols-direct', 'shadcn-token', 'tailwind-typography', 'card-title-typography', 'card-control-non-button', 'card-padding-override', 'aschild-with-icon-props', 'disabled-vs-is-disabled', 'button-prefixicon-jsx', 'icon-children-text'];
1153
+ const order = [
1154
+ 'dialog-form',
1155
+ 'dialog-button-wrap',
1156
+ 'button-icon-only',
1157
+ 'material-symbols-direct',
1158
+ 'shadcn-token',
1159
+ 'tailwind-typography',
1160
+ 'card-title-typography',
1161
+ 'card-control-non-button',
1162
+ 'card-padding-override',
1163
+ 'aschild-with-icon-props',
1164
+ 'disabled-vs-is-disabled',
1165
+ 'button-prefixicon-jsx',
1166
+ 'icon-children-text',
1167
+ ];
1137
1168
 
1138
1169
  return ANTI_PATTERN_GROUPS.filter((group) => group.check)
1139
1170
  .map((group) => ({
package/lib/constants.js CHANGED
@@ -18,6 +18,21 @@ export const PATHS = {
18
18
  TEMPLATE_DIR: ['templates', 'sparkle-variables'],
19
19
  };
20
20
 
21
+ // プロジェクト root から Tailwind エントリ CSS を探索するときの候補パス(優先順)。
22
+ // `setup` の scaffold 判定と、`generate` の `resolveGlobalsPath` の project-root
23
+ // fallback で同じ配列を参照することで二重化 drift を防ぐ。
24
+ // en: Candidate paths (priority-ordered) used both by `setup` scaffold and by
25
+ // `generate`'s project-root fallback in `resolveGlobalsPath`. Sharing the array
26
+ // prevents drift between "where we create the entry CSS" and "where we look
27
+ // for it later".
28
+ export const GLOBALS_CSS_CANDIDATES = [
29
+ 'src/app/globals.css',
30
+ 'app/globals.css',
31
+ 'src/globals.css',
32
+ 'src/index.css',
33
+ 'src/styles/globals.css',
34
+ ];
35
+
21
36
  // 正規表現パターン
22
37
  export const REGEX = {
23
38
  // フォント関連
@@ -34,8 +49,17 @@ export const REGEX = {
34
49
  TAILWIND_IMPORT: /@import\s+['"]tailwindcss['"];?/,
35
50
 
36
51
  // @source ディレクティブ関連
52
+ // ※ 単体の SOURCE_DIRECTIVE は「ユーザーが手書きした @source も含めて全部」
53
+ // マッチしてしまうので、removeExistingImports では使わない(ユーザー記述を
54
+ // 消してしまう)。CLI が挿入した block(コメント + それに続く連続 @source 行)
55
+ // だけを除去するために MANAGED_SOURCE_BLOCK を使う。
56
+ // en: SOURCE_DIRECTIVE alone matches user-authored @source lines too, so
57
+ // removeExistingImports uses MANAGED_SOURCE_BLOCK instead to remove only the
58
+ // comment-prefixed block that the CLI itself wrote.
37
59
  SOURCE_DIRECTIVE: /@source\s+["'][^"']*["'];?\s*\n?/g,
38
60
  SOURCE_COMMENT: /\/\*\s*npm パッケージのコンテンツスキャン[^*]*\*\/\s*\n?/g,
61
+ MANAGED_SOURCE_BLOCK:
62
+ /\/\*\s*npm パッケージのコンテンツスキャン[^*]*\*\/\s*\n?(?:@source\s+["'][^"']*["'];?\s*\n?)*/g,
39
63
 
40
64
  // カスタムCSS関連
41
65
  CUSTOM_CSS_IMPORT:
@@ -107,7 +131,8 @@ export const COMMENTS = {
107
131
  FONT_IMPORT: '/* フォントのインポート(CSSの仕様上、@importは最初に記述する必要がある) */',
108
132
  SPARKLE_IMPORT: '/* Sparkle Design のカスタム定義(Tailwindの後にインポート) */',
109
133
  TAILWIND_IMPORT: '/* Tailwindのインポート */',
110
- SOURCE_DIRECTIVE: '/* npm パッケージのコンテンツスキャン(Tailwind がクラスを検出するために必要) */',
134
+ SOURCE_DIRECTIVE:
135
+ '/* npm パッケージのコンテンツスキャン(Tailwind がクラスを検出するために必要) */',
111
136
  CUSTOM_CSS: '/* プロジェクト固有のカスタムトークン */',
112
137
  };
113
138
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
- import { REGEX, COMMENTS, IMPORTS, MESSAGES, FONT_DEFAULTS } from './constants.js';
8
+ import { REGEX, COMMENTS, IMPORTS, MESSAGES, GLOBALS_CSS_CANDIDATES } from './constants.js';
9
9
 
10
10
  /**
11
11
  * sparkle-design.css からフォントの@import文を抽出する
@@ -26,15 +26,6 @@ export function removeFontImportsFromCSS(cssContent) {
26
26
  return cssContent.replace(REGEX.FONT_IMPORT_WITH_COMMENT, '');
27
27
  }
28
28
 
29
- /**
30
- * フォントimportブロックを生成する
31
- * @param {Array<string>} fontImports フォントimport文の配列
32
- * @returns {string} フォントimportブロック
33
- */
34
- function createFontImportBlock(fontImports) {
35
- return [COMMENTS.FONT_IMPORT, ...fontImports, ''].join('\n');
36
- }
37
-
38
29
  /**
39
30
  * globals.css から node_modules への相対パスを計算する
40
31
  * @param {string} globalsPath globals.css の絶対パス
@@ -67,8 +58,8 @@ function resolveNodeModulesRelPath(globalsPath) {
67
58
  */
68
59
  function createSourceBlock(sourcePackages = [], nodeModulesRel = '../node_modules') {
69
60
  const defaultPackage = 'sparkle-design';
70
- const allPackages = [defaultPackage, ...sourcePackages.filter(p => p !== defaultPackage)];
71
- const sourceLines = allPackages.map(pkg => `@source "${nodeModulesRel}/${pkg}/dist";`);
61
+ const allPackages = [defaultPackage, ...sourcePackages.filter((p) => p !== defaultPackage)];
62
+ const sourceLines = allPackages.map((pkg) => `@source "${nodeModulesRel}/${pkg}/dist";`);
72
63
  return [COMMENTS.SOURCE_DIRECTIVE, ...sourceLines].join('\n');
73
64
  }
74
65
 
@@ -135,8 +126,14 @@ function removeExistingImports(globalsContent) {
135
126
  // 既存のフォントimportを削除
136
127
  cleaned = cleaned.replace(REGEX.EXISTING_FONT_IMPORT_BLOCK, '');
137
128
 
138
- // 既存の @source ディレクティブを削除
139
- cleaned = cleaned.replace(REGEX.SOURCE_DIRECTIVE, '');
129
+ // CLI が過去に書き込んだ「コメント + @source 連続行」ブロックのみを削除する。
130
+ // ユーザーが手書きした `@source "..."` は保持する(以前は SOURCE_DIRECTIVE
131
+ // を単体で全削除していたため、手書き @source が消える退行があった)。
132
+ // en: Remove only the managed comment + @source block that the CLI itself
133
+ // wrote in the past. User-authored @source lines must survive this step.
134
+ cleaned = cleaned.replace(REGEX.MANAGED_SOURCE_BLOCK, '');
135
+ // 念のため CLI コメント単独で孤立している残骸も掃除。
136
+ // en: Sweep up any orphan comment lines left over from historical writes.
140
137
  cleaned = cleaned.replace(REGEX.SOURCE_COMMENT, '');
141
138
 
142
139
  // 既存のsparkle-design.css importを削除
@@ -183,27 +180,43 @@ function reconstructGlobalsCss(globalsContent, sparkleImportBlock, tailwindInfo)
183
180
  const afterTailwind = globalsContent.substring(tailwindInfo.afterIndex);
184
181
 
185
182
  // Tailwind + sparkle-design.css + 残りのコンテンツ(フォント @import なし)
186
- return (
187
- beforeTailwind +
188
- tailwindInfo.match +
189
- sparkleImportBlock +
190
- afterTailwind.trimStart()
191
- );
183
+ return beforeTailwind + tailwindInfo.match + sparkleImportBlock + afterTailwind.trimStart();
192
184
  }
193
185
 
194
186
  /**
195
187
  * globals.css を構造化して管理する
196
- * - フォントimportを先頭に配置
188
+ * - フォントimportを先頭に配置(v2.0.0 以降は SparkleHead.tsx に移行したため空配列が渡る)
197
189
  * - Tailwind importを確保
198
190
  * - sparkle-design.css importをTailwindの後に配置
191
+ * - 自動検出した sourcePackages から @source ディレクティブを挿入
192
+ *
193
+ * **v2.0.7-beta.1 以前の不具合**: 早期 return が `fontImports.length === 0` 単独で
194
+ * 行われていたため、フォントを SparkleHead に移行済みかつ既存 globals.css が
195
+ * ある(Vite の `src/index.css` など)プロジェクトでは `@source` が一切挿入
196
+ * されなかった。条件を「フォント・sourcePackages・customCssPath のすべてが
197
+ * 空なら skip」に変更し、パッケージの自動検出結果だけでも globals.css が
198
+ * patch されるようにした。
199
+ *
199
200
  * @param {Array<string>} fontImports フォントimport文の配列
200
201
  * @param {string} globalsPath globals.cssのパス
201
202
  * @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
202
203
  * @param {string|null} customCssPath custom-css ファイルの相対パス
203
204
  * @returns {boolean} globals.css の更新に成功した場合は true
204
205
  */
205
- export function updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages = null, customCssPath = null) {
206
- if (fontImports.length === 0) {
206
+ export function updateGlobalsWithFonts(
207
+ fontImports,
208
+ globalsPath,
209
+ sourcePackages = null,
210
+ customCssPath = null
211
+ ) {
212
+ const hasFonts = fontImports.length > 0;
213
+ const hasSourcePackages = sourcePackages !== null && sourcePackages !== undefined;
214
+ const hasCustomCss = Boolean(customCssPath);
215
+
216
+ // フォント import も @source も custom-css もないなら globals.css に触る
217
+ // 理由がないので skip。
218
+ // en: Nothing to inject, so leave globals.css alone.
219
+ if (!hasFonts && !hasSourcePackages && !hasCustomCss) {
207
220
  return false;
208
221
  }
209
222
 
@@ -242,6 +255,38 @@ export function updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages
242
255
  }
243
256
  }
244
257
 
258
+ // Vite プロジェクト判定用の config 候補。setup.js 側の `VITE_CONFIG_FILES` と
259
+ // 同じ対象を見る必要がある(判定ロジックが分かれると scaffold と generate で
260
+ // 挙動が食い違う)。
261
+ // en: Vite config candidates used for project detection. Must stay in sync with
262
+ // setup.js's VITE_CONFIG_FILES so scaffold and generate agree on the layout.
263
+ const VITE_CONFIG_FILES = [
264
+ 'vite.config.ts',
265
+ 'vite.config.js',
266
+ 'vite.config.mjs',
267
+ 'vite.config.cjs',
268
+ 'vite.config.mts',
269
+ 'vite.config.cts',
270
+ ];
271
+
272
+ function isViteProject(cwd) {
273
+ return VITE_CONFIG_FILES.some((name) => fs.existsSync(path.join(cwd, name)));
274
+ }
275
+
276
+ // Vite プロジェクトで候補をソートするときの優先度。setup の
277
+ // `defaultGlobalsCssTarget` に合わせて `src/index.css` を `src/globals.css`
278
+ // より手前に引き上げる。それ以外は元の順序(数字昇順)を保つ。
279
+ // en: When the project is Vite, promote `src/index.css` ahead of
280
+ // `src/globals.css` so that `generate` patches the same file `setup` scaffolds.
281
+ function vitePriority(candidate) {
282
+ if (candidate === 'src/index.css') return 0;
283
+ if (candidate === 'src/globals.css') return 1;
284
+ // それ以外の候補は元の GLOBALS_CSS_CANDIDATES 順序を保つため、index を
285
+ // オフセット付きで返す。
286
+ // en: Preserve the original order for everything else.
287
+ return 10 + GLOBALS_CSS_CANDIDATES.indexOf(candidate);
288
+ }
289
+
245
290
  /**
246
291
  * sparkle-design.css と同じディレクトリで Tailwind のエントリポイント CSS を自動検出する
247
292
  * @param {string} dir 検索対象ディレクトリ
@@ -251,7 +296,16 @@ function detectTailwindEntrypoint(dir) {
251
296
  let entries;
252
297
  try {
253
298
  entries = fs.readdirSync(dir, { withFileTypes: true });
254
- } catch {
299
+ } catch (err) {
300
+ // ENOENT / ENOTDIR はよくある(dir 自体が無い / ファイルを渡された)ので
301
+ // 静かにスキップ。権限 (EACCES) などは debugging 価値があるので表に出す。
302
+ // en: ENOENT / ENOTDIR are expected (dir missing / path is a file). Surface
303
+ // permission-style failures so the user can debug.
304
+ if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
305
+ console.warn(
306
+ `⚠️ ${dir} の読み込みに失敗したため同階層 detection をスキップします (${err.code ?? err.message})`
307
+ );
308
+ }
255
309
  return null;
256
310
  }
257
311
 
@@ -260,9 +314,20 @@ function detectTailwindEntrypoint(dir) {
260
314
  if (entry.name === 'sparkle-design.css') continue;
261
315
 
262
316
  const filePath = path.join(dir, entry.name);
263
- const content = fs.readFileSync(filePath, 'utf8');
264
- if (REGEX.TAILWIND_IMPORT.test(content)) {
265
- return filePath;
317
+ try {
318
+ const content = fs.readFileSync(filePath, 'utf8');
319
+ if (REGEX.TAILWIND_IMPORT.test(content)) {
320
+ return filePath;
321
+ }
322
+ } catch (err) {
323
+ // 1 候補が読めなくても他の候補で続行できるようログを出してスキップする。
324
+ // 以前はここで throw させて外側の catch に落としていたため、同階層に
325
+ // 壊れた CSS が 1 つあると全部の detection が死んでいた。
326
+ // en: Keep scanning even if a single candidate file fails to read —
327
+ // previously a single unreadable CSS poisoned the whole detection.
328
+ console.warn(
329
+ `⚠️ ${entry.name} の読み込みに失敗したためスキップします (${err.code ?? err.message})`
330
+ );
266
331
  }
267
332
  }
268
333
 
@@ -271,10 +336,16 @@ function detectTailwindEntrypoint(dir) {
271
336
 
272
337
  /**
273
338
  * globals パスを解決する
274
- * 優先順位: 明示的指定 > 自動検出 > デフォルト(globals.css)
339
+ * 優先順位: 明示的指定 > sparkle-design.css 同階層で自動検出 > プロジェクト root の既知候補 > デフォルト(globals.css)
340
+ *
341
+ * **v2.0.7-beta.1**: Vite プロジェクトのように `sparkle-design.css` を `src/app/`
342
+ * に、entry CSS を `src/index.css` に置くレイアウトで `@source` が挿入されない
343
+ * 不具合があったため、同階層で見つからない場合はプロジェクト root からも
344
+ * 既知の候補を探すように拡張した(setup が scaffold に使う候補と同一)。
345
+ *
275
346
  * @param {string} sparkleDesignPath sparkle-design.css のパス
276
347
  * @param {string|null} explicitGlobalsPath 明示的に指定された globals パス
277
- * @returns {{ path: string, source: 'explicit' | 'detected' | 'default' } | null}
348
+ * @returns {{ path: string, source: 'explicit' | 'detected' | 'project' | 'default' } | null}
278
349
  */
279
350
  function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
280
351
  const dir = path.dirname(sparkleDesignPath);
@@ -288,7 +359,10 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
288
359
  return null;
289
360
  }
290
361
 
291
- // 自動検出: @import "tailwindcss" を含む CSS ファイルを探す
362
+ // 1. 自動検出: sparkle-design.css と同じディレクトリで @import "tailwindcss" を含む
363
+ // CSS ファイルを探す。Next.js App Router のように両者が同階層に居るケース用。
364
+ // en: Look next to sparkle-design.css for a Tailwind entry CSS. Covers the
365
+ // Next.js App Router layout where both live under `src/app/`.
292
366
  const detected = detectTailwindEntrypoint(dir);
293
367
  if (detected) {
294
368
  const basename = path.basename(detected);
@@ -298,7 +372,43 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
298
372
  return { path: detected, source: 'detected' };
299
373
  }
300
374
 
301
- // デフォルト: globals.css
375
+ // 2. プロジェクト root から既知の候補を探す。Vite のように sparkle-design.css
376
+ // が src/app/ に、entry CSS が src/index.css にあるレイアウトに対応。
377
+ // Vite プロジェクトでは `setup` の scaffold が `src/index.css` を選ぶので、
378
+ // `src/globals.css` と両方存在していても index.css を優先する(scaffold
379
+ // した場所と generate が patch する場所が食い違わないように)。
380
+ // en: Fall back to project-wide candidates. For Vite projects, promote
381
+ // `src/index.css` above `src/globals.css` so that `generate` patches the same
382
+ // file `setup` would have scaffolded.
383
+ const cwd = process.cwd();
384
+ const candidates = isViteProject(cwd)
385
+ ? GLOBALS_CSS_CANDIDATES.slice().sort((a, b) => vitePriority(a) - vitePriority(b))
386
+ : GLOBALS_CSS_CANDIDATES;
387
+ for (const candidate of candidates) {
388
+ const absolute = path.resolve(cwd, candidate);
389
+ if (!fs.existsSync(absolute)) continue;
390
+ // 自身(sparkle-design.css)は除外
391
+ // en: Skip sparkle-design.css itself just in case a candidate points at it.
392
+ if (path.resolve(absolute) === path.resolve(sparkleDesignPath)) continue;
393
+ try {
394
+ const content = fs.readFileSync(absolute, 'utf8');
395
+ if (REGEX.TAILWIND_IMPORT.test(content)) {
396
+ console.log(`📝 Tailwind エントリポイントを検出しました(project root): ${candidate}`);
397
+ return { path: absolute, source: 'project' };
398
+ }
399
+ } catch (err) {
400
+ // ENOENT 系は existsSync で既に弾いているので、ここに来るのは権限不足や
401
+ // ディレクトリ衝突などデバッグ価値のあるケース。silent に消さずに理由を出す。
402
+ // en: ENOENT is already filtered by existsSync above, so surfacing the
403
+ // error code here catches permission / type issues worth debugging.
404
+ console.warn(
405
+ `⚠️ 候補 ${candidate} の読み込みに失敗したためスキップします (${err.code ?? err.message})`
406
+ );
407
+ }
408
+ }
409
+
410
+ // 3. デフォルト: sparkle-design.css と同じディレクトリの globals.css
411
+ // en: Last resort — a globals.css next to sparkle-design.css.
302
412
  const defaultPath = path.join(dir, 'globals.css');
303
413
  if (fs.existsSync(defaultPath)) {
304
414
  return { path: defaultPath, source: 'default' };
@@ -315,7 +425,12 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
315
425
  * @param {string|null} customCssPath custom-css ファイルの相対パス
316
426
  * @param {string|null} globalsPathOverride 明示的に指定された globals パス
317
427
  */
318
- export function manageFontImports(sparkleDesignPath, sourcePackages = null, customCssPath = null, globalsPathOverride = null) {
428
+ export function manageFontImports(
429
+ sparkleDesignPath,
430
+ sourcePackages = null,
431
+ customCssPath = null,
432
+ globalsPathOverride = null
433
+ ) {
319
434
  try {
320
435
  // 1. globals パスを解決
321
436
  const resolved = resolveGlobalsPath(sparkleDesignPath, globalsPathOverride);
@@ -331,25 +446,40 @@ export function manageFontImports(sparkleDesignPath, sourcePackages = null, cust
331
446
  const sparkleContent = fs.readFileSync(sparkleDesignPath, 'utf8');
332
447
  const fontImports = extractFontImports(sparkleContent);
333
448
 
334
- if (fontImports.length === 0) {
335
- console.log(MESSAGES.NO_FONT_IMPORTS);
336
- return;
449
+ // v2.0.0 以降はフォント @import が SparkleHead.tsx に移行したため、
450
+ // sparkle-design.css に font @import が残らないのが通常状態。それでも
451
+ // 既存 globals.css には `@source` / sparkle-design.css import /
452
+ // custom-css import を挿入する必要があるので、単に fonts が空だからと
453
+ // いって早期 return せず、下流の update 関数に判断を委ねる。
454
+ // en: Fonts moved to SparkleHead in v2.0.0, so "no font imports in
455
+ // sparkle-design.css" is now the normal case. Don't bail out here —
456
+ // `updateGlobalsWithFonts` still needs to inject `@source` and the
457
+ // sparkle-design.css import into existing globals.css.
458
+ if (fontImports.length > 0) {
459
+ console.log(MESSAGES.FONT_DETECTED(fontImports.length));
337
460
  }
338
461
 
339
- console.log(MESSAGES.FONT_DETECTED(fontImports.length));
340
-
341
- // 4. globals.css にフォントimportを追加
342
- const globalsUpdated = updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages, customCssPath);
462
+ // 4. globals.css に import / @source を追加
463
+ const globalsUpdated = updateGlobalsWithFonts(
464
+ fontImports,
465
+ globalsPath,
466
+ sourcePackages,
467
+ customCssPath
468
+ );
343
469
 
344
470
  if (!globalsUpdated) {
345
- console.log(MESSAGES.FONT_REMOVE_SKIPPED);
471
+ // fonts が元々無く sourcePackages / customCss も空なら更新不要。
472
+ // en: Nothing to do — not an error.
346
473
  return;
347
474
  }
348
475
 
349
- // 5. sparkle-design.css からフォントimportを削除
350
- const cleanedSparkleContent = removeFontImportsFromCSS(sparkleContent);
351
- fs.writeFileSync(sparkleDesignPath, cleanedSparkleContent, 'utf8');
352
- console.log(MESSAGES.FONT_REMOVED);
476
+ // 5. fonts が sparkle-design.css 側に残っていれば削除(globals 側に移動済みの前提)
477
+ // en: If there were fonts to move, strip them from sparkle-design.css.
478
+ if (fontImports.length > 0) {
479
+ const cleanedSparkleContent = removeFontImportsFromCSS(sparkleContent);
480
+ fs.writeFileSync(sparkleDesignPath, cleanedSparkleContent, 'utf8');
481
+ console.log(MESSAGES.FONT_REMOVED);
482
+ }
353
483
  } catch (error) {
354
484
  console.error(MESSAGES.FONT_MANAGEMENT_ERROR(error.message));
355
485
  // フォント管理は必須ではないので、エラーでも処理を続行
package/lib/setup.js CHANGED
@@ -2,17 +2,20 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { spawnSync } from 'child_process';
4
4
  import { generateCSS } from './generate-css.js';
5
+ import { GLOBALS_CSS_CANDIDATES } from './constants.js';
5
6
 
6
7
  // デフォルトの sparkle.config.json テンプレート
7
8
  // en: Default sparkle.config.json template
9
+ // sparkle-design 本体の既定値(BIZ UDPGothic / BIZ UDGothic)に揃えておくことで、
10
+ // README やドキュメントサイトと見た目が一致する状態で初回セットアップが完了する。
8
11
  // @source ディレクティブの生成は generate 側が package.json から既知のデザインシステム
9
12
  // パッケージ(sparkle-design / @goodpatch/sparkle-design-internal 等)を自動検出して
10
13
  // 行うため、ここで extend.source-packages を指定する必要はない。独自パッケージを
11
14
  // 追加する場合のみユーザーが extend.source-packages に追記する。
12
15
  const DEFAULT_SPARKLE_CONFIG = {
13
16
  primary: 'blue',
14
- 'font-pro': 'Inter',
15
- 'font-mono': 'JetBrains Mono',
17
+ 'font-pro': 'BIZ UDPGothic',
18
+ 'font-mono': 'BIZ UDGothic',
16
19
  radius: 'md',
17
20
  };
18
21
 
@@ -39,16 +42,6 @@ const config = {
39
42
  export default config;
40
43
  `;
41
44
 
42
- // globals.css 候補パス(優先順)
43
- // en: globals.css candidate paths in priority order
44
- const GLOBALS_CSS_CANDIDATES = [
45
- 'src/app/globals.css',
46
- 'app/globals.css',
47
- 'src/globals.css',
48
- 'src/index.css',
49
- 'src/styles/globals.css',
50
- ];
51
-
52
45
  const ASSISTANT_CONFIG = {
53
46
  claude: {
54
47
  path: 'CLAUDE.md',
@@ -74,7 +67,6 @@ function buildCheckScript(target, mode) {
74
67
  return `npx --yes sparkle-design-cli check ${target} ${trailingArgs}`;
75
68
  }
76
69
 
77
-
78
70
  function ensureDir(filePath) {
79
71
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
80
72
  }
@@ -191,10 +183,13 @@ function buildInstructionBlock(target, assistant) {
191
183
  BLOCK_START,
192
184
  heading,
193
185
  '',
194
- '- Run `lint:sparkle` when Sparkle Design changes are involved.',
186
+ '- **必ず読む**: Sparkle Design のコンポーネントを使う前に、インストール済みパッケージの型定義 `node_modules/<パッケージ名>/dist/components/ui/<コンポーネント名>/index.d.ts` を必ず読んでください。Prop 仕様・使用例・アンチパターンは JSDoc に書かれている(✅ / ❌ 例込み)ので、これが Source of Truth です。`node_modules/sparkle-design/dist/` や `node_modules/@goodpatch/sparkle-design-internal/dist/` を対象に、`index.d.ts` の JSDoc まで読み切ること。',
187
+ "- **Required reading**: Before using any Sparkle Design component, read the installed package's type definitions at `node_modules/<package>/dist/components/ui/<component>/index.d.ts` — prop specs, usage, and anti-patterns (with ✅ / ❌ examples) live in the JSDoc and are the source of truth. Target the installed package(s), e.g. `sparkle-design` and/or `@goodpatch/sparkle-design-internal`, and read the full JSDoc, not just the type signature.",
188
+ '- **必ず実行**: UI コンポーネントを作成・変更した後は、終了前に `lint:sparkle` を実行してください。Sparkle Design のアンチパターンを検出します。',
189
+ '- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
195
190
  `- For AI review, prefer \`lint:sparkle:json\` or run \`npx --yes sparkle-design-cli check ${target} --format json\` directly.`,
196
- '- Always inspect both `findings` and `manualReviewReminders` from the JSON output.',
197
- '- If semantic guidance is still needed, consult Sparkle Design docs and JSDoc examples.',
191
+ '- JSON 出力の `findings` に加えて、**`manualReviewReminders` の内容も必ず 1 項目ずつ確認**してください。機械検出できない Badge/Tag の使い分けなどが含まれます。',
192
+ '- You must inspect every entry in both `findings` and `manualReviewReminders` — the reminders flag judgment calls (Badge vs Tag etc.) that the linter cannot detect.',
198
193
  BLOCK_END,
199
194
  ].join('\n');
200
195
  }
@@ -226,7 +221,12 @@ function resolveScriptUpdate(existingValue, nextValue, mode, force) {
226
221
  }
227
222
 
228
223
  if (force || isManagedSparkleScript(existingValue, mode)) {
229
- return { value: nextValue, changed: true, conflict: false, reason: force ? 'forced' : 'updated' };
224
+ return {
225
+ value: nextValue,
226
+ changed: true,
227
+ conflict: false,
228
+ reason: force ? 'forced' : 'updated',
229
+ };
230
230
  }
231
231
 
232
232
  return { value: existingValue, changed: false, conflict: true, reason: 'preserved-custom' };
@@ -280,7 +280,8 @@ const PM_COMMANDS = {
280
280
  */
281
281
  function detectPackageManager(cwd) {
282
282
  if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
283
- if (fs.existsSync(path.join(cwd, 'bun.lockb')) || fs.existsSync(path.join(cwd, 'bun.lock'))) return 'bun';
283
+ if (fs.existsSync(path.join(cwd, 'bun.lockb')) || fs.existsSync(path.join(cwd, 'bun.lock')))
284
+ return 'bun';
284
285
  if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
285
286
  return 'npm';
286
287
  }
@@ -289,7 +290,12 @@ function detectPackageManager(cwd) {
289
290
  * package.json に未登録のパッケージだけをインストールする
290
291
  * en: Install only packages not yet listed in package.json
291
292
  */
292
- function ensurePackagesInstalled(cwd, packageManager, candidates, { dev = false, dryRun = false } = {}) {
293
+ function ensurePackagesInstalled(
294
+ cwd,
295
+ packageManager,
296
+ candidates,
297
+ { dev = false, dryRun = false } = {}
298
+ ) {
293
299
  const packageJson = readJson(path.join(cwd, 'package.json'));
294
300
  const deps = { ...(packageJson.dependencies ?? {}), ...(packageJson.devDependencies ?? {}) };
295
301
  const missing = candidates.filter((pkg) => !Object.prototype.hasOwnProperty.call(deps, pkg));
@@ -356,7 +362,12 @@ function buildScaffoldTargets(cwd) {
356
362
  },
357
363
  {
358
364
  name: 'postcssConfig',
359
- candidates: ['postcss.config.mjs', 'postcss.config.js', 'postcss.config.cjs', 'postcss.config.ts'],
365
+ candidates: [
366
+ 'postcss.config.mjs',
367
+ 'postcss.config.js',
368
+ 'postcss.config.cjs',
369
+ 'postcss.config.ts',
370
+ ],
360
371
  target: 'postcss.config.mjs',
361
372
  write: (abs) => fs.writeFileSync(abs, INITIAL_POSTCSS_CONFIG, 'utf8'),
362
373
  },
@@ -400,7 +411,10 @@ function runInstall(cwd, packageManager, { skipInstall, dryRun }) {
400
411
  return {
401
412
  skipped: false,
402
413
  sparkle: ensurePackagesInstalled(cwd, packageManager, SPARKLE_PACKAGES, { dryRun }),
403
- tailwind: ensurePackagesInstalled(cwd, packageManager, TAILWIND_PACKAGES, { dev: true, dryRun }),
414
+ tailwind: ensurePackagesInstalled(cwd, packageManager, TAILWIND_PACKAGES, {
415
+ dev: true,
416
+ dryRun,
417
+ }),
404
418
  };
405
419
  }
406
420
 
@@ -415,7 +429,10 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
415
429
  ? normalizePath(cwd, options.target, 'Target path')
416
430
  : normalizePath(cwd, detectTarget(cwd), 'Target path');
417
431
  const instructionPath = options.instructionsPath
418
- ? path.resolve(cwd, normalizePath(cwd, options.instructionsPath, 'Instructions path', { checkParentOnly: true }))
432
+ ? path.resolve(
433
+ cwd,
434
+ normalizePath(cwd, options.instructionsPath, 'Instructions path', { checkParentOnly: true })
435
+ )
419
436
  : path.resolve(cwd, assistantConfig.path);
420
437
 
421
438
  const packageResult = updatePackageJson(packageJsonPath, target, force);
@@ -465,9 +482,17 @@ export function setupAssistant(options = {}) {
465
482
  const dryRun = Boolean(options.dryRun);
466
483
  const packageManager = detectPackageManager(cwd);
467
484
 
468
- const install = runInstall(cwd, packageManager, { skipInstall: Boolean(options.skipInstall), dryRun });
485
+ const install = runInstall(cwd, packageManager, {
486
+ skipInstall: Boolean(options.skipInstall),
487
+ dryRun,
488
+ });
469
489
  const scaffold = runScaffold(cwd, { skipScaffold: Boolean(options.skipScaffold), dryRun });
470
- const guard = runAssistantGuard(cwd, packageJsonPath, { ...options, assistant, dryRun }, assistantConfig);
490
+ const guard = runAssistantGuard(
491
+ cwd,
492
+ packageJsonPath,
493
+ { ...options, assistant, dryRun },
494
+ assistantConfig
495
+ );
471
496
  const generate = runGenerate({ skipGenerate: Boolean(options.skipGenerate), dryRun });
472
497
 
473
498
  const summary = {
@@ -497,5 +522,47 @@ export function setupAssistant(options = {}) {
497
522
  };
498
523
 
499
524
  console.log(JSON.stringify(summary, null, 2));
525
+
526
+ // セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
527
+ // en: Post-setup reminder. Written to stderr so it stays in the AI transcript
528
+ // without polluting the JSON stdout payload that tooling parses.
529
+ printPostSetupReminder(guard.target, packageManager);
530
+
500
531
  return summary;
501
532
  }
533
+
534
+ // 検出した package manager に応じて `lint:sparkle` を呼び出すコマンドを返す。
535
+ // en: Returns the shell invocation for the `lint:sparkle` script based on the
536
+ // detected package manager.
537
+ function buildLintCommand(packageManager) {
538
+ switch (packageManager) {
539
+ case 'pnpm':
540
+ return 'pnpm lint:sparkle';
541
+ case 'yarn':
542
+ return 'yarn lint:sparkle';
543
+ case 'bun':
544
+ return 'bun run lint:sparkle';
545
+ case 'npm':
546
+ default:
547
+ return 'npm run lint:sparkle';
548
+ }
549
+ }
550
+
551
+ function printPostSetupReminder(target, packageManager) {
552
+ const lintTarget = target || 'src';
553
+ const lintCmd = buildLintCommand(packageManager);
554
+ const lines = [
555
+ '',
556
+ '📝 次のステップ / Next steps:',
557
+ ' 1. Sparkle Design のコンポーネントを使う前に、必ず `node_modules/<パッケージ名>/dist/components/ui/<コンポーネント名>/index.d.ts` の JSDoc を読んでください。Prop 仕様・使用例・アンチパターンは JSDoc が Source of Truth です。',
558
+ ' Before using any Sparkle Design component, read `node_modules/<package>/dist/components/ui/<name>/index.d.ts` — the JSDoc includes prop specs, usage, and anti-pattern examples.',
559
+ ` 2. UI コンポーネントを作成・変更したら、必ず \`${lintCmd}\` (または \`npx --yes sparkle-design-cli check ${lintTarget} --strict\`) を実行してください。`,
560
+ ' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
561
+ ' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
562
+ ' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
563
+ '',
564
+ ];
565
+ for (const line of lines) {
566
+ console.error(line);
567
+ }
568
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparkle-design-cli",
3
- "version": "2.0.6",
3
+ "version": "2.0.7-beta.1",
4
4
  "description": "Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",