sparkle-design-cli 2.0.7-beta.1 → 2.0.7-beta.10
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 +135 -24
- package/bin/sparkle-design.js +12 -1
- package/lib/anti-pattern-rules.js +29 -15
- package/lib/check.js +93 -11
- package/lib/constants.js +19 -2
- package/lib/font-manager.js +188 -37
- package/lib/generate-css.js +151 -15
- package/lib/setup.js +379 -16
- package/package.json +15 -5
package/lib/font-manager.js
CHANGED
|
@@ -88,22 +88,64 @@ function createCustomCssImportBlock(customCssPath, globalsPath) {
|
|
|
88
88
|
return [COMMENTS.CUSTOM_CSS, `@import "${relativePath}";`].join('\n');
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* entry CSS (globals.css / index.css) から sparkle-design.css への相対 path を
|
|
93
|
+
* 計算し、`@import` 文を組み立てる。同一ディレクトリなら `./sparkle-design.css`、
|
|
94
|
+
* 異なるなら `./app/sparkle-design.css` のような相対 path になる。Vite の
|
|
95
|
+
* `src/index.css` → `src/app/sparkle-design.css` レイアウトで正しい path が
|
|
96
|
+
* 出るようにするために追加された関数(beta.7 まではハードコード)。
|
|
97
|
+
* en: Compute the relative path from the entry CSS to sparkle-design.css.
|
|
98
|
+
* Handles layouts like Vite's `src/index.css` → `src/app/sparkle-design.css`,
|
|
99
|
+
* where the two files live in different directories.
|
|
100
|
+
*/
|
|
101
|
+
function buildSparkleImportStatement(globalsPath, sparkleDesignPath) {
|
|
102
|
+
if (!globalsPath || !sparkleDesignPath) {
|
|
103
|
+
return IMPORTS.SPARKLE_DESIGN;
|
|
104
|
+
}
|
|
105
|
+
const globalsDir = path.dirname(globalsPath);
|
|
106
|
+
const rel = path.relative(globalsDir, sparkleDesignPath).split(path.sep).join('/');
|
|
107
|
+
// path.relative が空文字を返すのは globalsPath === sparkleDesignPath のときのみ
|
|
108
|
+
// (想定外)。念のため fallback として同一 dir 前提の default import を返す。
|
|
109
|
+
// en: path.relative returns '' only when paths match; use the same-dir default.
|
|
110
|
+
if (!rel) return IMPORTS.SPARKLE_DESIGN;
|
|
111
|
+
const importPath = rel.startsWith('.') ? rel : `./${rel}`;
|
|
112
|
+
return `@import "${importPath}";`;
|
|
113
|
+
}
|
|
114
|
+
|
|
91
115
|
/**
|
|
92
116
|
* sparkle-design.css importブロックを生成する
|
|
93
117
|
* @param {Array<string>} sourcePackages 追加パッケージ名の配列
|
|
94
118
|
* @param {string} globalsPath globals.css の絶対パス(@source パス計算用)
|
|
95
119
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
120
|
+
* @param {string|null} sparkleDesignPath sparkle-design.css の絶対パス(相対 import 計算用)
|
|
96
121
|
* @returns {string} sparkle-design.css importブロック
|
|
97
122
|
*/
|
|
98
|
-
function createSparkleImportBlock(
|
|
123
|
+
function createSparkleImportBlock(
|
|
124
|
+
sourcePackages = [],
|
|
125
|
+
globalsPath = null,
|
|
126
|
+
customCssPath = null,
|
|
127
|
+
sparkleDesignPath = null
|
|
128
|
+
) {
|
|
99
129
|
const parts = [''];
|
|
100
130
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
// CSS 仕様上 `@import` は `@charset` / `@layer` 以外の at-rule より前に
|
|
132
|
+
// 書く必要がある。`@source` を `@import` より前に置くと、PostCSS 処理系に
|
|
133
|
+
// よっては後続の `@import "./sparkle-design.css"` が silent に drop され、
|
|
134
|
+
// sparkle tokens が最終 CSS に出ない回帰になる(beta.6 以前で再現)。
|
|
135
|
+
// そのため順序は:
|
|
136
|
+
// 1. @import "./sparkle-design.css" (Sparkle 本体のトークン)
|
|
137
|
+
// 2. @import "./custom-tokens.css" (ユーザーのカスタムトークン、任意)
|
|
138
|
+
// 3. @source "../node_modules/..." (Tailwind スキャン対象、任意)
|
|
139
|
+
// の順で出力する。
|
|
140
|
+
// en: Per CSS spec, all `@import` rules must precede any other at-rule
|
|
141
|
+
// (except `@charset` / `@layer`). Placing `@source` between imports makes
|
|
142
|
+
// downstream `@import "./sparkle-design.css"` silently drop in some
|
|
143
|
+
// PostCSS toolchains, which removes Sparkle tokens from the output.
|
|
144
|
+
// We now emit @import blocks first, then any @source directives.
|
|
145
|
+
parts.push(
|
|
146
|
+
COMMENTS.SPARKLE_IMPORT,
|
|
147
|
+
buildSparkleImportStatement(globalsPath, sparkleDesignPath)
|
|
148
|
+
);
|
|
107
149
|
|
|
108
150
|
// カスタムCSS import を sparkle-design.css の後に配置
|
|
109
151
|
const customBlock = createCustomCssImportBlock(customCssPath, globalsPath);
|
|
@@ -111,6 +153,11 @@ function createSparkleImportBlock(sourcePackages = [], globalsPath = null, custo
|
|
|
111
153
|
parts.push(customBlock);
|
|
112
154
|
}
|
|
113
155
|
|
|
156
|
+
if (sourcePackages !== null && sourcePackages !== undefined) {
|
|
157
|
+
const nodeModulesRel = globalsPath ? resolveNodeModulesRelPath(globalsPath) : '../node_modules';
|
|
158
|
+
parts.push(createSourceBlock(sourcePackages, nodeModulesRel));
|
|
159
|
+
}
|
|
160
|
+
|
|
114
161
|
parts.push('');
|
|
115
162
|
return parts.join('\n');
|
|
116
163
|
}
|
|
@@ -190,24 +237,27 @@ function reconstructGlobalsCss(globalsContent, sparkleImportBlock, tailwindInfo)
|
|
|
190
237
|
* - sparkle-design.css importをTailwindの後に配置
|
|
191
238
|
* - 自動検出した sourcePackages から @source ディレクティブを挿入
|
|
192
239
|
*
|
|
193
|
-
* **v2.0.7-beta.
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* patch されるようにした。
|
|
240
|
+
* **v2.0.7-beta.2**: 戻り値を真偽値から `{ status, reason }` の状態付きに変更。
|
|
241
|
+
* 呼び出し側が「作業不要でスキップ」「失敗したがデフォルトは warn 継続」を
|
|
242
|
+
* 区別できるようにし、`--strict` モードで失敗を exit code に反映できるよう
|
|
243
|
+
* にした(#31)。`skipped` は後方互換のため `false` に、`updated` は `true`
|
|
244
|
+
* に toBoolean で扱える想定。
|
|
199
245
|
*
|
|
200
246
|
* @param {Array<string>} fontImports フォントimport文の配列
|
|
201
247
|
* @param {string} globalsPath globals.cssのパス
|
|
202
248
|
* @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
|
|
203
249
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
204
|
-
* @returns {
|
|
250
|
+
* @returns {{ status: 'skipped' | 'updated' | 'failed', reason?: string }}
|
|
251
|
+
* skipped: 触る理由がない (fonts/sourcePackages/customCss すべて空)
|
|
252
|
+
* updated: globals.css を正常に書き換えた
|
|
253
|
+
* failed: 書き換え対象だったが失敗した (TAILWIND_IMPORT 欠落、write 失敗等)
|
|
205
254
|
*/
|
|
206
255
|
export function updateGlobalsWithFonts(
|
|
207
256
|
fontImports,
|
|
208
257
|
globalsPath,
|
|
209
258
|
sourcePackages = null,
|
|
210
|
-
customCssPath = null
|
|
259
|
+
customCssPath = null,
|
|
260
|
+
sparkleDesignPath = null
|
|
211
261
|
) {
|
|
212
262
|
const hasFonts = fontImports.length > 0;
|
|
213
263
|
const hasSourcePackages = sourcePackages !== null && sourcePackages !== undefined;
|
|
@@ -217,7 +267,7 @@ export function updateGlobalsWithFonts(
|
|
|
217
267
|
// 理由がないので skip。
|
|
218
268
|
// en: Nothing to inject, so leave globals.css alone.
|
|
219
269
|
if (!hasFonts && !hasSourcePackages && !hasCustomCss) {
|
|
220
|
-
return
|
|
270
|
+
return { status: 'skipped', reason: 'no-work' };
|
|
221
271
|
}
|
|
222
272
|
|
|
223
273
|
try {
|
|
@@ -227,15 +277,42 @@ export function updateGlobalsWithFonts(
|
|
|
227
277
|
// 2. 既存のimportを削除
|
|
228
278
|
globalsContent = removeExistingImports(globalsContent);
|
|
229
279
|
|
|
230
|
-
// 3. Tailwind import
|
|
231
|
-
|
|
280
|
+
// 3. Tailwind import の位置を見つける。無ければ先頭に自動で prepend する。
|
|
281
|
+
// ここで silent に失敗すると `create-vite` 等が生成した既存 index.css に
|
|
282
|
+
// `@import "tailwindcss"` が無いプロジェクトで setup が no-op に終わり、
|
|
283
|
+
// AI が後から手書きで import を補うが `@source` を知らず落とす、という
|
|
284
|
+
// 連鎖的な崩れが起きていた(beta.8 試用で確認)。canonical な import を
|
|
285
|
+
// 自動投入することでその連鎖を根絶する。
|
|
286
|
+
// en: If `@import "tailwindcss"` is missing from an existing entry CSS
|
|
287
|
+
// (e.g. untouched `create-vite` default), auto-prepend it instead of
|
|
288
|
+
// silently warning. Without this recovery, the whole patch becomes a no-op
|
|
289
|
+
// and AI ends up hand-writing imports without `@source`, producing drifted
|
|
290
|
+
// output (observed in beta.8 user testing).
|
|
291
|
+
let tailwindInfo = findTailwindImport(globalsContent);
|
|
232
292
|
if (!tailwindInfo) {
|
|
233
|
-
|
|
234
|
-
|
|
293
|
+
const displayPath = path.relative(process.cwd(), globalsPath) || globalsPath;
|
|
294
|
+
console.warn(
|
|
295
|
+
`⚠️ ${displayPath} に \`@import "tailwindcss";\` が見つからなかったため先頭に追記します(canonical な Tailwind v4 entry point にするため)。`
|
|
296
|
+
);
|
|
297
|
+
globalsContent = `${IMPORTS.TAILWIND}\n${globalsContent.startsWith('\n') ? '' : ''}${globalsContent}`;
|
|
298
|
+
tailwindInfo = findTailwindImport(globalsContent);
|
|
299
|
+
if (!tailwindInfo) {
|
|
300
|
+
// prepend したのに正規表現で拾えないのは `REGEX.TAILWIND_IMPORT` が
|
|
301
|
+
// 想定と違う(= コードの不整合)。fail-fast する。
|
|
302
|
+
// en: We just wrote the canonical import but the regex doesn't see it —
|
|
303
|
+
// this is an internal inconsistency, not a user error. Fail loud.
|
|
304
|
+
console.error(MESSAGES.TAILWIND_NOT_FOUND(displayPath));
|
|
305
|
+
return { status: 'failed', reason: 'tailwind-import-missing-after-prepend' };
|
|
306
|
+
}
|
|
235
307
|
}
|
|
236
308
|
|
|
237
309
|
// 4. importブロックを生成(フォント @import は SparkleHead に移行したため生成しない)
|
|
238
|
-
const sparkleImportBlock = createSparkleImportBlock(
|
|
310
|
+
const sparkleImportBlock = createSparkleImportBlock(
|
|
311
|
+
sourcePackages,
|
|
312
|
+
globalsPath,
|
|
313
|
+
customCssPath,
|
|
314
|
+
sparkleDesignPath
|
|
315
|
+
);
|
|
239
316
|
|
|
240
317
|
// 5. globals.css を再構築(フォント @import なし)
|
|
241
318
|
const reconstructedContent = reconstructGlobalsCss(
|
|
@@ -247,11 +324,10 @@ export function updateGlobalsWithFonts(
|
|
|
247
324
|
// 6. 更新したglobals.cssを書き込む
|
|
248
325
|
fs.writeFileSync(globalsPath, reconstructedContent, 'utf8');
|
|
249
326
|
console.log(MESSAGES.GLOBALS_UPDATED(globalsPath));
|
|
250
|
-
return
|
|
327
|
+
return { status: 'updated' };
|
|
251
328
|
} catch (error) {
|
|
252
329
|
console.error(MESSAGES.GLOBALS_UPDATE_FAILED(error.message));
|
|
253
|
-
|
|
254
|
-
return false;
|
|
330
|
+
return { status: 'failed', reason: `write-error: ${error.code ?? error.message}` };
|
|
255
331
|
}
|
|
256
332
|
}
|
|
257
333
|
|
|
@@ -355,8 +431,17 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
355
431
|
if (fs.existsSync(resolved)) {
|
|
356
432
|
return { path: resolved, source: 'explicit' };
|
|
357
433
|
}
|
|
358
|
-
|
|
359
|
-
|
|
434
|
+
// 明示指定は settings bug / typo なので silent warn ではなく throw に
|
|
435
|
+
// 昇格させる。--strict の有無に関わらず呼び出し元で伝播させたいので、
|
|
436
|
+
// error.code を付けて上流で識別できるようにする。
|
|
437
|
+
// en: An explicit --globals-path value that doesn't exist is almost
|
|
438
|
+
// always a typo. We tag the error so manageFontImports's catch knows
|
|
439
|
+
// to re-throw regardless of strict mode.
|
|
440
|
+
const err = new Error(
|
|
441
|
+
`指定された globals パスが見つかりません: ${explicitGlobalsPath} (cwd 基準で解決: ${resolved})`
|
|
442
|
+
);
|
|
443
|
+
err.code = 'E_EXPLICIT_GLOBALS_PATH_NOT_FOUND';
|
|
444
|
+
throw err;
|
|
360
445
|
}
|
|
361
446
|
|
|
362
447
|
// 1. 自動検出: sparkle-design.css と同じディレクトリで @import "tailwindcss" を含む
|
|
@@ -384,11 +469,22 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
384
469
|
const candidates = isViteProject(cwd)
|
|
385
470
|
? GLOBALS_CSS_CANDIDATES.slice().sort((a, b) => vitePriority(a) - vitePriority(b))
|
|
386
471
|
: GLOBALS_CSS_CANDIDATES;
|
|
472
|
+
// 2-a) まず既知候補の中で `@import "tailwindcss"` を持つファイルを探す。
|
|
473
|
+
// 持っているファイルが第一候補(canonical Tailwind entry CSS)。
|
|
474
|
+
// 2-b) 2-a で見つからなかった場合、`@import` 無しで既存の候補ファイルが
|
|
475
|
+
// あればそれを拾う(Vite の `create-vite` 直後のように entry CSS は
|
|
476
|
+
// あるが Tailwind 未設定のプロジェクト)。これで上流の
|
|
477
|
+
// `updateGlobalsWithFonts` が auto-prepend で tailwind import を
|
|
478
|
+
// 補えるため、silent に no-op 終了する回帰を避けられる(beta.8 で再現)。
|
|
479
|
+
// en: First pick candidates that already contain `@import "tailwindcss"`.
|
|
480
|
+
// If none match, fall back to the first existing candidate anyway so the
|
|
481
|
+
// upstream patcher can auto-prepend the canonical tailwind import. Without
|
|
482
|
+
// this fallback, a Vite project created by `create-vite` with an untouched
|
|
483
|
+
// `src/index.css` would cause setup to end as a silent no-op.
|
|
484
|
+
let fallbackExistingPath = null;
|
|
387
485
|
for (const candidate of candidates) {
|
|
388
486
|
const absolute = path.resolve(cwd, candidate);
|
|
389
487
|
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
488
|
if (path.resolve(absolute) === path.resolve(sparkleDesignPath)) continue;
|
|
393
489
|
try {
|
|
394
490
|
const content = fs.readFileSync(absolute, 'utf8');
|
|
@@ -396,6 +492,9 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
396
492
|
console.log(`📝 Tailwind エントリポイントを検出しました(project root): ${candidate}`);
|
|
397
493
|
return { path: absolute, source: 'project' };
|
|
398
494
|
}
|
|
495
|
+
if (!fallbackExistingPath) {
|
|
496
|
+
fallbackExistingPath = { path: absolute, candidate };
|
|
497
|
+
}
|
|
399
498
|
} catch (err) {
|
|
400
499
|
// ENOENT 系は existsSync で既に弾いているので、ここに来るのは権限不足や
|
|
401
500
|
// ディレクトリ衝突などデバッグ価値のあるケース。silent に消さずに理由を出す。
|
|
@@ -407,6 +506,13 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
407
506
|
}
|
|
408
507
|
}
|
|
409
508
|
|
|
509
|
+
if (fallbackExistingPath) {
|
|
510
|
+
console.log(
|
|
511
|
+
`📝 Tailwind import 未設定の既存 entry CSS を検出しました(project root): ${fallbackExistingPath.candidate}。後続処理で \`@import "tailwindcss";\` を自動追記します。`
|
|
512
|
+
);
|
|
513
|
+
return { path: fallbackExistingPath.path, source: 'existing-no-tailwind' };
|
|
514
|
+
}
|
|
515
|
+
|
|
410
516
|
// 3. デフォルト: sparkle-design.css と同じディレクトリの globals.css
|
|
411
517
|
// en: Last resort — a globals.css next to sparkle-design.css.
|
|
412
518
|
const defaultPath = path.join(dir, 'globals.css');
|
|
@@ -420,24 +526,54 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
420
526
|
/**
|
|
421
527
|
* フォント管理の自動処理を実行する
|
|
422
528
|
* sparkle-design.css からフォントimportを抽出し、Tailwind エントリポイント CSS に @source 等を挿入する
|
|
529
|
+
*
|
|
530
|
+
* **v2.0.7-beta.2**: 戻り値を `{ status, reason? }` に変更し、呼び出し側が
|
|
531
|
+
* 「作業不要のスキップ」「作業すべき状態だが失敗」を区別できるようにした
|
|
532
|
+
* (#31)。`options.strict` が true のとき、`status === 'failed'` になる状態は
|
|
533
|
+
* throw に昇格させて `bin/sparkle-design.js` の outer catch で exit 1 に
|
|
534
|
+
* 繋げる。
|
|
535
|
+
*
|
|
423
536
|
* @param {string} sparkleDesignPath sparkle-design.cssのパス
|
|
424
537
|
* @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
|
|
425
538
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
426
539
|
* @param {string|null} globalsPathOverride 明示的に指定された globals パス
|
|
540
|
+
* @param {{ strict?: boolean }} [options] strict=true のとき失敗を throw する
|
|
541
|
+
* @returns {{ status: 'skipped' | 'updated' | 'failed', reason?: string }}
|
|
427
542
|
*/
|
|
428
543
|
export function manageFontImports(
|
|
429
544
|
sparkleDesignPath,
|
|
430
545
|
sourcePackages = null,
|
|
431
546
|
customCssPath = null,
|
|
432
|
-
globalsPathOverride = null
|
|
547
|
+
globalsPathOverride = null,
|
|
548
|
+
options = {}
|
|
433
549
|
) {
|
|
550
|
+
const strict = Boolean(options.strict);
|
|
551
|
+
const hasWork =
|
|
552
|
+
(sourcePackages !== null && sourcePackages !== undefined) || Boolean(customCssPath);
|
|
553
|
+
|
|
554
|
+
const raiseOrReturn = (result) => {
|
|
555
|
+
if (strict && result.status === 'failed') {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`globals.css の更新に失敗しました (${result.reason ?? 'unknown'})。--strict モードでは exit 1 で終了します。`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
};
|
|
562
|
+
|
|
434
563
|
try {
|
|
435
564
|
// 1. globals パスを解決
|
|
436
565
|
const resolved = resolveGlobalsPath(sparkleDesignPath, globalsPathOverride);
|
|
437
566
|
|
|
438
567
|
if (!resolved) {
|
|
568
|
+
// 作業すべき状態(sourcePackages 等あり)で entry CSS が見つからないのは
|
|
569
|
+
// 事実上の不具合。strict なら throw、非 strict なら従来どおり情報ログ。
|
|
570
|
+
// en: With work queued, missing entry CSS is effectively a misconfiguration.
|
|
571
|
+
if (hasWork) {
|
|
572
|
+
console.warn(MESSAGES.GLOBALS_NOT_FOUND);
|
|
573
|
+
return raiseOrReturn({ status: 'failed', reason: 'entry-css-not-found' });
|
|
574
|
+
}
|
|
439
575
|
console.log(MESSAGES.GLOBALS_NOT_FOUND);
|
|
440
|
-
return;
|
|
576
|
+
return { status: 'skipped', reason: 'entry-css-not-found-no-work' };
|
|
441
577
|
}
|
|
442
578
|
|
|
443
579
|
const globalsPath = resolved.path;
|
|
@@ -460,17 +596,25 @@ export function manageFontImports(
|
|
|
460
596
|
}
|
|
461
597
|
|
|
462
598
|
// 4. globals.css に import / @source を追加
|
|
463
|
-
|
|
599
|
+
// sparkle-design.css の絶対パスを渡すことで、globalsPath と異なる
|
|
600
|
+
// ディレクトリに置かれている場合でも相対 import が正しく計算される
|
|
601
|
+
// (Vite の src/index.css → src/app/sparkle-design.css 等)。
|
|
602
|
+
// en: Pass the absolute sparkle-design.css path so the import can be
|
|
603
|
+
// computed relative to the entry CSS, even in Vite-style layouts where
|
|
604
|
+
// the two files live in different directories.
|
|
605
|
+
const result = updateGlobalsWithFonts(
|
|
464
606
|
fontImports,
|
|
465
607
|
globalsPath,
|
|
466
608
|
sourcePackages,
|
|
467
|
-
customCssPath
|
|
609
|
+
customCssPath,
|
|
610
|
+
sparkleDesignPath
|
|
468
611
|
);
|
|
469
612
|
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
613
|
+
if (result.status === 'skipped') {
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
if (result.status === 'failed') {
|
|
617
|
+
return raiseOrReturn(result);
|
|
474
618
|
}
|
|
475
619
|
|
|
476
620
|
// 5. fonts が sparkle-design.css 側に残っていれば削除(globals 側に移動済みの前提)
|
|
@@ -480,8 +624,15 @@ export function manageFontImports(
|
|
|
480
624
|
fs.writeFileSync(sparkleDesignPath, cleanedSparkleContent, 'utf8');
|
|
481
625
|
console.log(MESSAGES.FONT_REMOVED);
|
|
482
626
|
}
|
|
627
|
+
return result;
|
|
483
628
|
} catch (error) {
|
|
484
629
|
console.error(MESSAGES.FONT_MANAGEMENT_ERROR(error.message));
|
|
485
|
-
//
|
|
630
|
+
// strict モード、または明示指定された globals path の not-found は
|
|
631
|
+
// ユーザーが必ず気付くべきなので非 strict でも再 throw する。
|
|
632
|
+
// en: Always re-throw explicit --globals-path typos; respect strict for the rest.
|
|
633
|
+
if (strict || error.code === 'E_EXPLICIT_GLOBALS_PATH_NOT_FOUND') {
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
return { status: 'failed', reason: `manage-error: ${error.code ?? error.message}` };
|
|
486
637
|
}
|
|
487
638
|
}
|
package/lib/generate-css.js
CHANGED
|
@@ -68,7 +68,7 @@ function dedupeFontImports(cssContent) {
|
|
|
68
68
|
|
|
69
69
|
return cssContent
|
|
70
70
|
.split('\n')
|
|
71
|
-
.filter(line => {
|
|
71
|
+
.filter((line) => {
|
|
72
72
|
if (!line.includes('fonts.googleapis.com')) {
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
@@ -96,7 +96,7 @@ function normalizeFontsEntry(entry) {
|
|
|
96
96
|
return [{ family: entry, weights: FONT_DEFAULTS.WEIGHTS }];
|
|
97
97
|
}
|
|
98
98
|
if (Array.isArray(entry)) {
|
|
99
|
-
return entry.map(item => {
|
|
99
|
+
return entry.map((item) => {
|
|
100
100
|
if (typeof item === 'string') {
|
|
101
101
|
return { family: item, weights: FONT_DEFAULTS.WEIGHTS };
|
|
102
102
|
}
|
|
@@ -132,7 +132,7 @@ function resolveFontConfig(config) {
|
|
|
132
132
|
* @returns {string} CSS font-family 値(例: 'Montserrat', 'Noto Sans JP', sans-serif)
|
|
133
133
|
*/
|
|
134
134
|
function generateFontFamilyValue(fonts, genericFamily) {
|
|
135
|
-
const quoted = fonts.map(f => `'${f.family}'`);
|
|
135
|
+
const quoted = fonts.map((f) => `'${f.family}'`);
|
|
136
136
|
return [...quoted, genericFamily].join(', ');
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -169,9 +169,10 @@ function generateMergedFontImports(allFonts) {
|
|
|
169
169
|
* @returns {string} フォント import ブロック
|
|
170
170
|
*/
|
|
171
171
|
function generateFontImportsBlock(configOrResolved) {
|
|
172
|
-
const { pro, mono } =
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
const { pro, mono } =
|
|
173
|
+
configOrResolved.pro && configOrResolved.mono
|
|
174
|
+
? configOrResolved
|
|
175
|
+
: resolveFontConfig(configOrResolved);
|
|
175
176
|
|
|
176
177
|
const imports = [
|
|
177
178
|
FONT_DEFAULTS.MATERIAL_SYMBOLS_IMPORT,
|
|
@@ -206,7 +207,7 @@ function generateSparkleHeadContent(resolvedFonts) {
|
|
|
206
207
|
` <link rel="preconnect" href="${FONT_DOMAINS.GOOGLEAPIS}" />`,
|
|
207
208
|
` <link rel="preconnect" href="${FONT_DOMAINS.GSTATIC}" crossOrigin="anonymous" />`,
|
|
208
209
|
` <link rel="stylesheet" href="${materialSymbolsUrl}" />`,
|
|
209
|
-
...fontUrls.map(url => ` <link rel="stylesheet" href="${url}" />`),
|
|
210
|
+
...fontUrls.map((url) => ` <link rel="stylesheet" href="${url}" />`),
|
|
210
211
|
];
|
|
211
212
|
|
|
212
213
|
return `/**
|
|
@@ -255,6 +256,92 @@ function writeSparkleHead(content, sparkleDesignCssPath) {
|
|
|
255
256
|
console.log(' → ルートレイアウトの <head> 内に <SparkleHead /> を追加してください');
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
const VITE_INDEX_HTML_BLOCK_START = '<!-- sparkle-design-cli:fonts:start -->';
|
|
260
|
+
const VITE_INDEX_HTML_BLOCK_END = '<!-- sparkle-design-cli:fonts:end -->';
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* SparkleHead.tsx と同じフォント <link> タグ群を HTML 文字列として生成する。
|
|
264
|
+
* Vite のように `<head>` が `index.html` にある環境では、React コンポーネントを
|
|
265
|
+
* 挟めないため HTML に直接注入する必要がある。
|
|
266
|
+
* en: Return the font/link tags as HTML strings so we can inject them into
|
|
267
|
+
* Vite's `index.html`, where a React SparkleHead component can't reach the head.
|
|
268
|
+
*/
|
|
269
|
+
function buildSparkleFontLinksHtml(resolvedFonts) {
|
|
270
|
+
const materialSymbolsUrl = extractUrlFromImport(FONT_DEFAULTS.MATERIAL_SYMBOLS_IMPORT);
|
|
271
|
+
const fontImportLines = generateMergedFontImports([...resolvedFonts.pro, ...resolvedFonts.mono]);
|
|
272
|
+
const fontUrls = fontImportLines.map(extractUrlFromImport).filter(Boolean);
|
|
273
|
+
return [
|
|
274
|
+
`<link rel="preconnect" href="${FONT_DOMAINS.GOOGLEAPIS}" />`,
|
|
275
|
+
`<link rel="preconnect" href="${FONT_DOMAINS.GSTATIC}" crossorigin />`,
|
|
276
|
+
`<link rel="stylesheet" href="${materialSymbolsUrl}" />`,
|
|
277
|
+
...fontUrls.map((url) => `<link rel="stylesheet" href="${url}" />`),
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Vite プロジェクトの `index.html` に managed block で Sparkle のフォント
|
|
283
|
+
* `<link>` タグを upsert する。存在しない場合は `</head>` 直前に挿入、
|
|
284
|
+
* 既にマーカー付きブロックがあれば内容を置換する。
|
|
285
|
+
*
|
|
286
|
+
* - Vite 以外(Next.js の src/app など)は index.html を持たないので呼ばれない
|
|
287
|
+
* - 既存ユーザーの head 内に書いた自作の link は触らない(managed block の
|
|
288
|
+
* 範囲だけを管理するので non-destructive)
|
|
289
|
+
*
|
|
290
|
+
* en: Upsert a managed block of Sparkle font `<link>` tags into Vite's
|
|
291
|
+
* `index.html`, injecting before `</head>` on first run and replacing the
|
|
292
|
+
* block content on re-runs. Non-destructive against user-authored links
|
|
293
|
+
* outside the markers.
|
|
294
|
+
*/
|
|
295
|
+
function upsertViteIndexHtmlFonts(cwd, resolvedFonts) {
|
|
296
|
+
const indexHtmlPath = path.resolve(cwd, 'index.html');
|
|
297
|
+
if (!fs.existsSync(indexHtmlPath)) {
|
|
298
|
+
return { status: 'skipped', reason: 'no-index-html' };
|
|
299
|
+
}
|
|
300
|
+
let html;
|
|
301
|
+
try {
|
|
302
|
+
html = fs.readFileSync(indexHtmlPath, 'utf8');
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return { status: 'failed', reason: `read-error: ${error.code ?? error.message}` };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const links = buildSparkleFontLinksHtml(resolvedFonts);
|
|
308
|
+
// ブロック内部の各行は innerIndent で統一する。マーカー行自体も含める。
|
|
309
|
+
// en: Apply innerIndent to every line of the managed block, markers included.
|
|
310
|
+
const innerIndent = ' ';
|
|
311
|
+
const block = [VITE_INDEX_HTML_BLOCK_START, ...links, VITE_INDEX_HTML_BLOCK_END]
|
|
312
|
+
.map((line) => innerIndent + line)
|
|
313
|
+
.join('\n');
|
|
314
|
+
|
|
315
|
+
if (html.includes(VITE_INDEX_HTML_BLOCK_START) && html.includes(VITE_INDEX_HTML_BLOCK_END)) {
|
|
316
|
+
// 既存ブロックの各行インデントを尊重しつつ中身だけ置換すると複雑になるので、
|
|
317
|
+
// BLOCK_START の前にある空白を再利用してブロックごと丸ごと差し替える。
|
|
318
|
+
// en: Replace the entire managed block while preserving the leading
|
|
319
|
+
// whitespace that already sits in front of BLOCK_START.
|
|
320
|
+
const existingBlockRegex = new RegExp(
|
|
321
|
+
`([^\\S\\n]*)${VITE_INDEX_HTML_BLOCK_START}[\\s\\S]*?${VITE_INDEX_HTML_BLOCK_END}`
|
|
322
|
+
);
|
|
323
|
+
const replaced = html.replace(existingBlockRegex, (_match, leadingWs) => {
|
|
324
|
+
// 既存 leading を innerIndent に揃える。マーカー前の空白が行頭にある想定。
|
|
325
|
+
return [VITE_INDEX_HTML_BLOCK_START, ...links, VITE_INDEX_HTML_BLOCK_END]
|
|
326
|
+
.map((line, idx) => (idx === 0 ? leadingWs : innerIndent) + line)
|
|
327
|
+
.join('\n');
|
|
328
|
+
});
|
|
329
|
+
if (replaced === html) return { status: 'unchanged', path: indexHtmlPath };
|
|
330
|
+
fs.writeFileSync(indexHtmlPath, replaced, 'utf8');
|
|
331
|
+
return { status: 'updated', path: indexHtmlPath };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const headCloseRegex = /(\s*)<\/head>/i;
|
|
335
|
+
const match = html.match(headCloseRegex);
|
|
336
|
+
if (!match) {
|
|
337
|
+
return { status: 'failed', reason: 'no-head-close-tag' };
|
|
338
|
+
}
|
|
339
|
+
const leading = match[1]; // 通常 `\n ` (`</head>` 直前の改行とインデント)
|
|
340
|
+
const replaced = html.replace(headCloseRegex, `\n${block}${leading}</head>`);
|
|
341
|
+
fs.writeFileSync(indexHtmlPath, replaced, 'utf8');
|
|
342
|
+
return { status: 'created', path: indexHtmlPath };
|
|
343
|
+
}
|
|
344
|
+
|
|
258
345
|
/**
|
|
259
346
|
* テンプレート変数を設定値で置換する
|
|
260
347
|
* @param {string} template CSSテンプレート
|
|
@@ -289,7 +376,15 @@ function processTemplate(template, config, grayMapping, radiusMapping, colors) {
|
|
|
289
376
|
// 5. 基本的な設定値による置換(オブジェクト・配列・拡張フィールドはスキップ)
|
|
290
377
|
Object.entries(config).forEach(([key, value]) => {
|
|
291
378
|
// 配列・オブジェクト・拡張フィールドはスキップ
|
|
292
|
-
if (
|
|
379
|
+
if (
|
|
380
|
+
Array.isArray(value) ||
|
|
381
|
+
(typeof value === 'object' && value !== null) ||
|
|
382
|
+
key === 'custom-css' ||
|
|
383
|
+
key === 'fonts' ||
|
|
384
|
+
key === 'extend' ||
|
|
385
|
+
key === 'source-packages' ||
|
|
386
|
+
key === 'globals-path'
|
|
387
|
+
) {
|
|
293
388
|
return;
|
|
294
389
|
}
|
|
295
390
|
// 通常のプレースホルダー(CSS用 - スペースはそのまま)
|
|
@@ -336,9 +431,7 @@ function processTemplate(template, config, grayMapping, radiusMapping, colors) {
|
|
|
336
431
|
// `sparkle-design` は CLI 側のデフォルトパッケージなのでリストには加えず「存在判定」にだけ使う。
|
|
337
432
|
// en: Known design-system packages to auto-detect. `sparkle-design` is the CLI's
|
|
338
433
|
// default source package, so it only acts as an "enables @source" signal.
|
|
339
|
-
const KNOWN_DESIGN_SYSTEM_PACKAGES = [
|
|
340
|
-
'@goodpatch/sparkle-design-internal',
|
|
341
|
-
];
|
|
434
|
+
const KNOWN_DESIGN_SYSTEM_PACKAGES = ['@goodpatch/sparkle-design-internal'];
|
|
342
435
|
|
|
343
436
|
/**
|
|
344
437
|
* package.json の dependencies / devDependencies から既知のデザインシステムパッケージを検出する。
|
|
@@ -394,8 +487,17 @@ function writeCSS(cssContent, outputPath = null) {
|
|
|
394
487
|
* メイン処理
|
|
395
488
|
* @param {string|null} configPath カスタム設定ファイルのパス(オプション)
|
|
396
489
|
* @param {string|null} outputPath カスタム出力パス(オプション)
|
|
490
|
+
* @param {string|null} globalsPath 明示指定の globals.css パス(オプション)
|
|
491
|
+
* @param {{ strict?: boolean }} [options] strict=true のとき、
|
|
492
|
+
* globals.css パッチ失敗などを throw に昇格させる(exit 1 に繋げるため)
|
|
493
|
+
* @returns {{ globalsResult: { status: 'skipped'|'updated'|'failed', reason?: string } }}
|
|
397
494
|
*/
|
|
398
|
-
export function generateCSS(
|
|
495
|
+
export function generateCSS(
|
|
496
|
+
configPath = null,
|
|
497
|
+
outputPath = null,
|
|
498
|
+
globalsPath = null,
|
|
499
|
+
options = {}
|
|
500
|
+
) {
|
|
399
501
|
console.log(MESSAGES.START);
|
|
400
502
|
|
|
401
503
|
// 1. 設定ファイルを読み込み
|
|
@@ -413,7 +515,13 @@ export function generateCSS(configPath = null, outputPath = null, globalsPath =
|
|
|
413
515
|
const radiusMapping = loadRadiusMapping();
|
|
414
516
|
|
|
415
517
|
// 5. テンプレートを設定値で処理(resolvedFonts も返す)
|
|
416
|
-
const { css: processedCSS, resolvedFonts } = processTemplate(
|
|
518
|
+
const { css: processedCSS, resolvedFonts } = processTemplate(
|
|
519
|
+
template,
|
|
520
|
+
config,
|
|
521
|
+
grayMapping,
|
|
522
|
+
radiusMapping,
|
|
523
|
+
colors
|
|
524
|
+
);
|
|
417
525
|
|
|
418
526
|
// 6. CSSファイルを書き出し
|
|
419
527
|
const defaultOutputPath = path.resolve(
|
|
@@ -428,6 +536,27 @@ export function generateCSS(configPath = null, outputPath = null, globalsPath =
|
|
|
428
536
|
const sparkleHeadContent = generateSparkleHeadContent(resolvedFonts);
|
|
429
537
|
writeSparkleHead(sparkleHeadContent, resolvedOutputPath);
|
|
430
538
|
|
|
539
|
+
// 7.5 Vite プロジェクトの index.html に Sparkle のフォント <link> タグを upsert
|
|
540
|
+
// する。Vite は <head> が index.html 側にあるため、React コンポーネントの
|
|
541
|
+
// SparkleHead を挟めない。managed block でマーカー間だけを管理し、既存の
|
|
542
|
+
// head 内容は非破壊に保つ。
|
|
543
|
+
// en: Vite ships its <head> in index.html, outside React's reach, so mirror
|
|
544
|
+
// the SparkleHead link tags into a managed block there.
|
|
545
|
+
const cwdForHtml = process.cwd();
|
|
546
|
+
if (fs.existsSync(path.resolve(cwdForHtml, 'index.html'))) {
|
|
547
|
+
const htmlResult = upsertViteIndexHtmlFonts(cwdForHtml, resolvedFonts);
|
|
548
|
+
if (htmlResult.status === 'created' || htmlResult.status === 'updated') {
|
|
549
|
+
const displayPath = path.relative(cwdForHtml, htmlResult.path) || htmlResult.path;
|
|
550
|
+
console.log(
|
|
551
|
+
`✅ index.html の Sparkle フォント <link> ブロックを${htmlResult.status === 'created' ? '挿入' : '更新'}しました: ${displayPath}`
|
|
552
|
+
);
|
|
553
|
+
} else if (htmlResult.status === 'failed') {
|
|
554
|
+
console.warn(
|
|
555
|
+
`⚠️ index.html への Sparkle フォント <link> 挿入に失敗しました (${htmlResult.reason})。手動で <head> 内に SparkleHead.tsx 相当の <link> タグを追加してください。`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
431
560
|
// 8. フォント管理の自動処理を実行(globals.css にはフォント @import を差し込まない)
|
|
432
561
|
// source-packages は以下の合成で決まる:
|
|
433
562
|
// 1. package.json から既知のデザインシステムパッケージを自動検出(baseline)
|
|
@@ -437,16 +566,23 @@ export function generateCSS(configPath = null, outputPath = null, globalsPath =
|
|
|
437
566
|
// with explicit config entries. If neither surfaces anything, @source is skipped.
|
|
438
567
|
console.log(MESSAGES.FONT_MANAGEMENT_START);
|
|
439
568
|
const detectedPackages = detectSourcePackagesFromPackageJson();
|
|
440
|
-
const explicitPackages = 'source-packages' in config ?
|
|
569
|
+
const explicitPackages = 'source-packages' in config ? config['source-packages'] || [] : null;
|
|
441
570
|
const sourcePackages =
|
|
442
571
|
detectedPackages !== null || explicitPackages !== null
|
|
443
572
|
? [...new Set([...(detectedPackages ?? []), ...(explicitPackages ?? [])])]
|
|
444
573
|
: null;
|
|
445
574
|
const customCssPath = config['custom-css'] || null;
|
|
446
575
|
const globalsPathOverride = globalsPath || config['globals-path'] || null;
|
|
447
|
-
|
|
576
|
+
const globalsResult = manageFontImports(
|
|
577
|
+
resolvedOutputPath,
|
|
578
|
+
sourcePackages,
|
|
579
|
+
customCssPath,
|
|
580
|
+
globalsPathOverride,
|
|
581
|
+
{ strict: Boolean(options.strict) }
|
|
582
|
+
);
|
|
448
583
|
|
|
449
584
|
console.log(MESSAGES.SUCCESS);
|
|
585
|
+
return { globalsResult };
|
|
450
586
|
}
|
|
451
587
|
|
|
452
588
|
// スクリプトが直接実行された場合のみメイン処理を実行
|