sparkle-design-cli 2.0.7-beta.0 → 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 +52 -7
- package/lib/check.js +93 -11
- package/lib/constants.js +45 -3
- package/lib/font-manager.js +348 -58
- package/lib/generate-css.js +151 -15
- package/lib/setup.js +385 -28
- package/package.json +15 -5
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
|
// スクリプトが直接実行された場合のみメイン処理を実行
|