sparkle-design-cli 2.0.7-beta.0 → 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/lib/anti-pattern-rules.js +38 -7
- package/lib/constants.js +26 -1
- package/lib/font-manager.js +174 -35
- package/lib/setup.js +7 -13
- package/package.json +1 -1
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 = [
|
|
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:
|
|
134
|
+
SOURCE_DIRECTIVE:
|
|
135
|
+
'/* npm パッケージのコンテンツスキャン(Tailwind がクラスを検出するために必要) */',
|
|
111
136
|
CUSTOM_CSS: '/* プロジェクト固有のカスタムトークン */',
|
|
112
137
|
};
|
|
113
138
|
|
package/lib/font-manager.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import { REGEX, COMMENTS, IMPORTS, MESSAGES } 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文を抽出する
|
|
@@ -58,8 +58,8 @@ function resolveNodeModulesRelPath(globalsPath) {
|
|
|
58
58
|
*/
|
|
59
59
|
function createSourceBlock(sourcePackages = [], nodeModulesRel = '../node_modules') {
|
|
60
60
|
const defaultPackage = 'sparkle-design';
|
|
61
|
-
const allPackages = [defaultPackage, ...sourcePackages.filter(p => p !== defaultPackage)];
|
|
62
|
-
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";`);
|
|
63
63
|
return [COMMENTS.SOURCE_DIRECTIVE, ...sourceLines].join('\n');
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -126,8 +126,14 @@ function removeExistingImports(globalsContent) {
|
|
|
126
126
|
// 既存のフォントimportを削除
|
|
127
127
|
cleaned = cleaned.replace(REGEX.EXISTING_FONT_IMPORT_BLOCK, '');
|
|
128
128
|
|
|
129
|
-
//
|
|
130
|
-
|
|
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.
|
|
131
137
|
cleaned = cleaned.replace(REGEX.SOURCE_COMMENT, '');
|
|
132
138
|
|
|
133
139
|
// 既存のsparkle-design.css importを削除
|
|
@@ -174,27 +180,43 @@ function reconstructGlobalsCss(globalsContent, sparkleImportBlock, tailwindInfo)
|
|
|
174
180
|
const afterTailwind = globalsContent.substring(tailwindInfo.afterIndex);
|
|
175
181
|
|
|
176
182
|
// Tailwind + sparkle-design.css + 残りのコンテンツ(フォント @import なし)
|
|
177
|
-
return (
|
|
178
|
-
beforeTailwind +
|
|
179
|
-
tailwindInfo.match +
|
|
180
|
-
sparkleImportBlock +
|
|
181
|
-
afterTailwind.trimStart()
|
|
182
|
-
);
|
|
183
|
+
return beforeTailwind + tailwindInfo.match + sparkleImportBlock + afterTailwind.trimStart();
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
/**
|
|
186
187
|
* globals.css を構造化して管理する
|
|
187
|
-
* - フォントimport
|
|
188
|
+
* - フォントimportを先頭に配置(v2.0.0 以降は SparkleHead.tsx に移行したため空配列が渡る)
|
|
188
189
|
* - Tailwind importを確保
|
|
189
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
|
+
*
|
|
190
200
|
* @param {Array<string>} fontImports フォントimport文の配列
|
|
191
201
|
* @param {string} globalsPath globals.cssのパス
|
|
192
202
|
* @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
|
|
193
203
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
194
204
|
* @returns {boolean} globals.css の更新に成功した場合は true
|
|
195
205
|
*/
|
|
196
|
-
export function updateGlobalsWithFonts(
|
|
197
|
-
|
|
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) {
|
|
198
220
|
return false;
|
|
199
221
|
}
|
|
200
222
|
|
|
@@ -233,6 +255,38 @@ export function updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages
|
|
|
233
255
|
}
|
|
234
256
|
}
|
|
235
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
|
+
|
|
236
290
|
/**
|
|
237
291
|
* sparkle-design.css と同じディレクトリで Tailwind のエントリポイント CSS を自動検出する
|
|
238
292
|
* @param {string} dir 検索対象ディレクトリ
|
|
@@ -242,7 +296,16 @@ function detectTailwindEntrypoint(dir) {
|
|
|
242
296
|
let entries;
|
|
243
297
|
try {
|
|
244
298
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
245
|
-
} 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
|
+
}
|
|
246
309
|
return null;
|
|
247
310
|
}
|
|
248
311
|
|
|
@@ -251,9 +314,20 @@ function detectTailwindEntrypoint(dir) {
|
|
|
251
314
|
if (entry.name === 'sparkle-design.css') continue;
|
|
252
315
|
|
|
253
316
|
const filePath = path.join(dir, entry.name);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
);
|
|
257
331
|
}
|
|
258
332
|
}
|
|
259
333
|
|
|
@@ -262,10 +336,16 @@ function detectTailwindEntrypoint(dir) {
|
|
|
262
336
|
|
|
263
337
|
/**
|
|
264
338
|
* globals パスを解決する
|
|
265
|
-
* 優先順位: 明示的指定 >
|
|
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
|
+
*
|
|
266
346
|
* @param {string} sparkleDesignPath sparkle-design.css のパス
|
|
267
347
|
* @param {string|null} explicitGlobalsPath 明示的に指定された globals パス
|
|
268
|
-
* @returns {{ path: string, source: 'explicit' | 'detected' | 'default' } | null}
|
|
348
|
+
* @returns {{ path: string, source: 'explicit' | 'detected' | 'project' | 'default' } | null}
|
|
269
349
|
*/
|
|
270
350
|
function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
271
351
|
const dir = path.dirname(sparkleDesignPath);
|
|
@@ -279,7 +359,10 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
279
359
|
return null;
|
|
280
360
|
}
|
|
281
361
|
|
|
282
|
-
// 自動検出: @import "tailwindcss" を含む
|
|
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/`.
|
|
283
366
|
const detected = detectTailwindEntrypoint(dir);
|
|
284
367
|
if (detected) {
|
|
285
368
|
const basename = path.basename(detected);
|
|
@@ -289,7 +372,43 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
289
372
|
return { path: detected, source: 'detected' };
|
|
290
373
|
}
|
|
291
374
|
|
|
292
|
-
//
|
|
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.
|
|
293
412
|
const defaultPath = path.join(dir, 'globals.css');
|
|
294
413
|
if (fs.existsSync(defaultPath)) {
|
|
295
414
|
return { path: defaultPath, source: 'default' };
|
|
@@ -306,7 +425,12 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
306
425
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
307
426
|
* @param {string|null} globalsPathOverride 明示的に指定された globals パス
|
|
308
427
|
*/
|
|
309
|
-
export function manageFontImports(
|
|
428
|
+
export function manageFontImports(
|
|
429
|
+
sparkleDesignPath,
|
|
430
|
+
sourcePackages = null,
|
|
431
|
+
customCssPath = null,
|
|
432
|
+
globalsPathOverride = null
|
|
433
|
+
) {
|
|
310
434
|
try {
|
|
311
435
|
// 1. globals パスを解決
|
|
312
436
|
const resolved = resolveGlobalsPath(sparkleDesignPath, globalsPathOverride);
|
|
@@ -322,25 +446,40 @@ export function manageFontImports(sparkleDesignPath, sourcePackages = null, cust
|
|
|
322
446
|
const sparkleContent = fs.readFileSync(sparkleDesignPath, 'utf8');
|
|
323
447
|
const fontImports = extractFontImports(sparkleContent);
|
|
324
448
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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));
|
|
328
460
|
}
|
|
329
461
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
462
|
+
// 4. globals.css に import / @source を追加
|
|
463
|
+
const globalsUpdated = updateGlobalsWithFonts(
|
|
464
|
+
fontImports,
|
|
465
|
+
globalsPath,
|
|
466
|
+
sourcePackages,
|
|
467
|
+
customCssPath
|
|
468
|
+
);
|
|
334
469
|
|
|
335
470
|
if (!globalsUpdated) {
|
|
336
|
-
|
|
471
|
+
// fonts が元々無く sourcePackages / customCss も空なら更新不要。
|
|
472
|
+
// en: Nothing to do — not an error.
|
|
337
473
|
return;
|
|
338
474
|
}
|
|
339
475
|
|
|
340
|
-
// 5. sparkle-design.css
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
+
}
|
|
344
483
|
} catch (error) {
|
|
345
484
|
console.error(MESSAGES.FONT_MANAGEMENT_ERROR(error.message));
|
|
346
485
|
// フォント管理は必須ではないので、エラーでも処理を続行
|
package/lib/setup.js
CHANGED
|
@@ -2,6 +2,7 @@ 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
|
|
@@ -41,16 +42,6 @@ const config = {
|
|
|
41
42
|
export default config;
|
|
42
43
|
`;
|
|
43
44
|
|
|
44
|
-
// globals.css 候補パス(優先順)
|
|
45
|
-
// en: globals.css candidate paths in priority order
|
|
46
|
-
const GLOBALS_CSS_CANDIDATES = [
|
|
47
|
-
'src/app/globals.css',
|
|
48
|
-
'app/globals.css',
|
|
49
|
-
'src/globals.css',
|
|
50
|
-
'src/index.css',
|
|
51
|
-
'src/styles/globals.css',
|
|
52
|
-
];
|
|
53
|
-
|
|
54
45
|
const ASSISTANT_CONFIG = {
|
|
55
46
|
claude: {
|
|
56
47
|
path: 'CLAUDE.md',
|
|
@@ -192,12 +183,13 @@ function buildInstructionBlock(target, assistant) {
|
|
|
192
183
|
BLOCK_START,
|
|
193
184
|
heading,
|
|
194
185
|
'',
|
|
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.",
|
|
195
188
|
'- **必ず実行**: UI コンポーネントを作成・変更した後は、終了前に `lint:sparkle` を実行してください。Sparkle Design のアンチパターンを検出します。',
|
|
196
189
|
'- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
|
|
197
190
|
`- For AI review, prefer \`lint:sparkle:json\` or run \`npx --yes sparkle-design-cli check ${target} --format json\` directly.`,
|
|
198
191
|
'- JSON 出力の `findings` に加えて、**`manualReviewReminders` の内容も必ず 1 項目ずつ確認**してください。機械検出できない Badge/Tag の使い分けなどが含まれます。',
|
|
199
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.',
|
|
200
|
-
'- If semantic guidance is still needed, consult Sparkle Design docs and JSDoc examples.',
|
|
201
193
|
BLOCK_END,
|
|
202
194
|
].join('\n');
|
|
203
195
|
}
|
|
@@ -562,9 +554,11 @@ function printPostSetupReminder(target, packageManager) {
|
|
|
562
554
|
const lines = [
|
|
563
555
|
'',
|
|
564
556
|
'📝 次のステップ / Next steps:',
|
|
565
|
-
|
|
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\`) を実行してください。`,
|
|
566
560
|
' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
|
|
567
|
-
'
|
|
561
|
+
' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
|
|
568
562
|
' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
|
|
569
563
|
'',
|
|
570
564
|
];
|