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/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
|
|
|
@@ -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
|
}
|
|
@@ -126,8 +173,14 @@ function removeExistingImports(globalsContent) {
|
|
|
126
173
|
// 既存のフォントimportを削除
|
|
127
174
|
cleaned = cleaned.replace(REGEX.EXISTING_FONT_IMPORT_BLOCK, '');
|
|
128
175
|
|
|
129
|
-
//
|
|
130
|
-
|
|
176
|
+
// CLI が過去に書き込んだ「コメント + @source 連続行」ブロックのみを削除する。
|
|
177
|
+
// ユーザーが手書きした `@source "..."` は保持する(以前は SOURCE_DIRECTIVE
|
|
178
|
+
// を単体で全削除していたため、手書き @source が消える退行があった)。
|
|
179
|
+
// en: Remove only the managed comment + @source block that the CLI itself
|
|
180
|
+
// wrote in the past. User-authored @source lines must survive this step.
|
|
181
|
+
cleaned = cleaned.replace(REGEX.MANAGED_SOURCE_BLOCK, '');
|
|
182
|
+
// 念のため CLI コメント単独で孤立している残骸も掃除。
|
|
183
|
+
// en: Sweep up any orphan comment lines left over from historical writes.
|
|
131
184
|
cleaned = cleaned.replace(REGEX.SOURCE_COMMENT, '');
|
|
132
185
|
|
|
133
186
|
// 既存のsparkle-design.css importを削除
|
|
@@ -174,28 +227,47 @@ function reconstructGlobalsCss(globalsContent, sparkleImportBlock, tailwindInfo)
|
|
|
174
227
|
const afterTailwind = globalsContent.substring(tailwindInfo.afterIndex);
|
|
175
228
|
|
|
176
229
|
// Tailwind + sparkle-design.css + 残りのコンテンツ(フォント @import なし)
|
|
177
|
-
return (
|
|
178
|
-
beforeTailwind +
|
|
179
|
-
tailwindInfo.match +
|
|
180
|
-
sparkleImportBlock +
|
|
181
|
-
afterTailwind.trimStart()
|
|
182
|
-
);
|
|
230
|
+
return beforeTailwind + tailwindInfo.match + sparkleImportBlock + afterTailwind.trimStart();
|
|
183
231
|
}
|
|
184
232
|
|
|
185
233
|
/**
|
|
186
234
|
* globals.css を構造化して管理する
|
|
187
|
-
* - フォントimport
|
|
235
|
+
* - フォントimportを先頭に配置(v2.0.0 以降は SparkleHead.tsx に移行したため空配列が渡る)
|
|
188
236
|
* - Tailwind importを確保
|
|
189
237
|
* - sparkle-design.css importをTailwindの後に配置
|
|
238
|
+
* - 自動検出した sourcePackages から @source ディレクティブを挿入
|
|
239
|
+
*
|
|
240
|
+
* **v2.0.7-beta.2**: 戻り値を真偽値から `{ status, reason }` の状態付きに変更。
|
|
241
|
+
* 呼び出し側が「作業不要でスキップ」「失敗したがデフォルトは warn 継続」を
|
|
242
|
+
* 区別できるようにし、`--strict` モードで失敗を exit code に反映できるよう
|
|
243
|
+
* にした(#31)。`skipped` は後方互換のため `false` に、`updated` は `true`
|
|
244
|
+
* に toBoolean で扱える想定。
|
|
245
|
+
*
|
|
190
246
|
* @param {Array<string>} fontImports フォントimport文の配列
|
|
191
247
|
* @param {string} globalsPath globals.cssのパス
|
|
192
248
|
* @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
|
|
193
249
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
194
|
-
* @returns {
|
|
250
|
+
* @returns {{ status: 'skipped' | 'updated' | 'failed', reason?: string }}
|
|
251
|
+
* skipped: 触る理由がない (fonts/sourcePackages/customCss すべて空)
|
|
252
|
+
* updated: globals.css を正常に書き換えた
|
|
253
|
+
* failed: 書き換え対象だったが失敗した (TAILWIND_IMPORT 欠落、write 失敗等)
|
|
195
254
|
*/
|
|
196
|
-
export function updateGlobalsWithFonts(
|
|
197
|
-
|
|
198
|
-
|
|
255
|
+
export function updateGlobalsWithFonts(
|
|
256
|
+
fontImports,
|
|
257
|
+
globalsPath,
|
|
258
|
+
sourcePackages = null,
|
|
259
|
+
customCssPath = null,
|
|
260
|
+
sparkleDesignPath = null
|
|
261
|
+
) {
|
|
262
|
+
const hasFonts = fontImports.length > 0;
|
|
263
|
+
const hasSourcePackages = sourcePackages !== null && sourcePackages !== undefined;
|
|
264
|
+
const hasCustomCss = Boolean(customCssPath);
|
|
265
|
+
|
|
266
|
+
// フォント import も @source も custom-css もないなら globals.css に触る
|
|
267
|
+
// 理由がないので skip。
|
|
268
|
+
// en: Nothing to inject, so leave globals.css alone.
|
|
269
|
+
if (!hasFonts && !hasSourcePackages && !hasCustomCss) {
|
|
270
|
+
return { status: 'skipped', reason: 'no-work' };
|
|
199
271
|
}
|
|
200
272
|
|
|
201
273
|
try {
|
|
@@ -205,15 +277,42 @@ export function updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages
|
|
|
205
277
|
// 2. 既存のimportを削除
|
|
206
278
|
globalsContent = removeExistingImports(globalsContent);
|
|
207
279
|
|
|
208
|
-
// 3. Tailwind import
|
|
209
|
-
|
|
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);
|
|
210
292
|
if (!tailwindInfo) {
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
307
|
}
|
|
214
308
|
|
|
215
309
|
// 4. importブロックを生成(フォント @import は SparkleHead に移行したため生成しない)
|
|
216
|
-
const sparkleImportBlock = createSparkleImportBlock(
|
|
310
|
+
const sparkleImportBlock = createSparkleImportBlock(
|
|
311
|
+
sourcePackages,
|
|
312
|
+
globalsPath,
|
|
313
|
+
customCssPath,
|
|
314
|
+
sparkleDesignPath
|
|
315
|
+
);
|
|
217
316
|
|
|
218
317
|
// 5. globals.css を再構築(フォント @import なし)
|
|
219
318
|
const reconstructedContent = reconstructGlobalsCss(
|
|
@@ -225,14 +324,45 @@ export function updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages
|
|
|
225
324
|
// 6. 更新したglobals.cssを書き込む
|
|
226
325
|
fs.writeFileSync(globalsPath, reconstructedContent, 'utf8');
|
|
227
326
|
console.log(MESSAGES.GLOBALS_UPDATED(globalsPath));
|
|
228
|
-
return
|
|
327
|
+
return { status: 'updated' };
|
|
229
328
|
} catch (error) {
|
|
230
329
|
console.error(MESSAGES.GLOBALS_UPDATE_FAILED(error.message));
|
|
231
|
-
|
|
232
|
-
return false;
|
|
330
|
+
return { status: 'failed', reason: `write-error: ${error.code ?? error.message}` };
|
|
233
331
|
}
|
|
234
332
|
}
|
|
235
333
|
|
|
334
|
+
// Vite プロジェクト判定用の config 候補。setup.js 側の `VITE_CONFIG_FILES` と
|
|
335
|
+
// 同じ対象を見る必要がある(判定ロジックが分かれると scaffold と generate で
|
|
336
|
+
// 挙動が食い違う)。
|
|
337
|
+
// en: Vite config candidates used for project detection. Must stay in sync with
|
|
338
|
+
// setup.js's VITE_CONFIG_FILES so scaffold and generate agree on the layout.
|
|
339
|
+
const VITE_CONFIG_FILES = [
|
|
340
|
+
'vite.config.ts',
|
|
341
|
+
'vite.config.js',
|
|
342
|
+
'vite.config.mjs',
|
|
343
|
+
'vite.config.cjs',
|
|
344
|
+
'vite.config.mts',
|
|
345
|
+
'vite.config.cts',
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
function isViteProject(cwd) {
|
|
349
|
+
return VITE_CONFIG_FILES.some((name) => fs.existsSync(path.join(cwd, name)));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Vite プロジェクトで候補をソートするときの優先度。setup の
|
|
353
|
+
// `defaultGlobalsCssTarget` に合わせて `src/index.css` を `src/globals.css`
|
|
354
|
+
// より手前に引き上げる。それ以外は元の順序(数字昇順)を保つ。
|
|
355
|
+
// en: When the project is Vite, promote `src/index.css` ahead of
|
|
356
|
+
// `src/globals.css` so that `generate` patches the same file `setup` scaffolds.
|
|
357
|
+
function vitePriority(candidate) {
|
|
358
|
+
if (candidate === 'src/index.css') return 0;
|
|
359
|
+
if (candidate === 'src/globals.css') return 1;
|
|
360
|
+
// それ以外の候補は元の GLOBALS_CSS_CANDIDATES 順序を保つため、index を
|
|
361
|
+
// オフセット付きで返す。
|
|
362
|
+
// en: Preserve the original order for everything else.
|
|
363
|
+
return 10 + GLOBALS_CSS_CANDIDATES.indexOf(candidate);
|
|
364
|
+
}
|
|
365
|
+
|
|
236
366
|
/**
|
|
237
367
|
* sparkle-design.css と同じディレクトリで Tailwind のエントリポイント CSS を自動検出する
|
|
238
368
|
* @param {string} dir 検索対象ディレクトリ
|
|
@@ -242,7 +372,16 @@ function detectTailwindEntrypoint(dir) {
|
|
|
242
372
|
let entries;
|
|
243
373
|
try {
|
|
244
374
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
245
|
-
} catch {
|
|
375
|
+
} catch (err) {
|
|
376
|
+
// ENOENT / ENOTDIR はよくある(dir 自体が無い / ファイルを渡された)ので
|
|
377
|
+
// 静かにスキップ。権限 (EACCES) などは debugging 価値があるので表に出す。
|
|
378
|
+
// en: ENOENT / ENOTDIR are expected (dir missing / path is a file). Surface
|
|
379
|
+
// permission-style failures so the user can debug.
|
|
380
|
+
if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
|
|
381
|
+
console.warn(
|
|
382
|
+
`⚠️ ${dir} の読み込みに失敗したため同階層 detection をスキップします (${err.code ?? err.message})`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
246
385
|
return null;
|
|
247
386
|
}
|
|
248
387
|
|
|
@@ -251,9 +390,20 @@ function detectTailwindEntrypoint(dir) {
|
|
|
251
390
|
if (entry.name === 'sparkle-design.css') continue;
|
|
252
391
|
|
|
253
392
|
const filePath = path.join(dir, entry.name);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
393
|
+
try {
|
|
394
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
395
|
+
if (REGEX.TAILWIND_IMPORT.test(content)) {
|
|
396
|
+
return filePath;
|
|
397
|
+
}
|
|
398
|
+
} catch (err) {
|
|
399
|
+
// 1 候補が読めなくても他の候補で続行できるようログを出してスキップする。
|
|
400
|
+
// 以前はここで throw させて外側の catch に落としていたため、同階層に
|
|
401
|
+
// 壊れた CSS が 1 つあると全部の detection が死んでいた。
|
|
402
|
+
// en: Keep scanning even if a single candidate file fails to read —
|
|
403
|
+
// previously a single unreadable CSS poisoned the whole detection.
|
|
404
|
+
console.warn(
|
|
405
|
+
`⚠️ ${entry.name} の読み込みに失敗したためスキップします (${err.code ?? err.message})`
|
|
406
|
+
);
|
|
257
407
|
}
|
|
258
408
|
}
|
|
259
409
|
|
|
@@ -262,10 +412,16 @@ function detectTailwindEntrypoint(dir) {
|
|
|
262
412
|
|
|
263
413
|
/**
|
|
264
414
|
* globals パスを解決する
|
|
265
|
-
* 優先順位: 明示的指定 >
|
|
415
|
+
* 優先順位: 明示的指定 > sparkle-design.css 同階層で自動検出 > プロジェクト root の既知候補 > デフォルト(globals.css)
|
|
416
|
+
*
|
|
417
|
+
* **v2.0.7-beta.1**: Vite プロジェクトのように `sparkle-design.css` を `src/app/`
|
|
418
|
+
* に、entry CSS を `src/index.css` に置くレイアウトで `@source` が挿入されない
|
|
419
|
+
* 不具合があったため、同階層で見つからない場合はプロジェクト root からも
|
|
420
|
+
* 既知の候補を探すように拡張した(setup が scaffold に使う候補と同一)。
|
|
421
|
+
*
|
|
266
422
|
* @param {string} sparkleDesignPath sparkle-design.css のパス
|
|
267
423
|
* @param {string|null} explicitGlobalsPath 明示的に指定された globals パス
|
|
268
|
-
* @returns {{ path: string, source: 'explicit' | 'detected' | 'default' } | null}
|
|
424
|
+
* @returns {{ path: string, source: 'explicit' | 'detected' | 'project' | 'default' } | null}
|
|
269
425
|
*/
|
|
270
426
|
function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
271
427
|
const dir = path.dirname(sparkleDesignPath);
|
|
@@ -275,11 +431,23 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
275
431
|
if (fs.existsSync(resolved)) {
|
|
276
432
|
return { path: resolved, source: 'explicit' };
|
|
277
433
|
}
|
|
278
|
-
|
|
279
|
-
|
|
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;
|
|
280
445
|
}
|
|
281
446
|
|
|
282
|
-
// 自動検出: @import "tailwindcss" を含む
|
|
447
|
+
// 1. 自動検出: sparkle-design.css と同じディレクトリで @import "tailwindcss" を含む
|
|
448
|
+
// CSS ファイルを探す。Next.js App Router のように両者が同階層に居るケース用。
|
|
449
|
+
// en: Look next to sparkle-design.css for a Tailwind entry CSS. Covers the
|
|
450
|
+
// Next.js App Router layout where both live under `src/app/`.
|
|
283
451
|
const detected = detectTailwindEntrypoint(dir);
|
|
284
452
|
if (detected) {
|
|
285
453
|
const basename = path.basename(detected);
|
|
@@ -289,7 +457,64 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
289
457
|
return { path: detected, source: 'detected' };
|
|
290
458
|
}
|
|
291
459
|
|
|
292
|
-
//
|
|
460
|
+
// 2. プロジェクト root から既知の候補を探す。Vite のように sparkle-design.css
|
|
461
|
+
// が src/app/ に、entry CSS が src/index.css にあるレイアウトに対応。
|
|
462
|
+
// Vite プロジェクトでは `setup` の scaffold が `src/index.css` を選ぶので、
|
|
463
|
+
// `src/globals.css` と両方存在していても index.css を優先する(scaffold
|
|
464
|
+
// した場所と generate が patch する場所が食い違わないように)。
|
|
465
|
+
// en: Fall back to project-wide candidates. For Vite projects, promote
|
|
466
|
+
// `src/index.css` above `src/globals.css` so that `generate` patches the same
|
|
467
|
+
// file `setup` would have scaffolded.
|
|
468
|
+
const cwd = process.cwd();
|
|
469
|
+
const candidates = isViteProject(cwd)
|
|
470
|
+
? GLOBALS_CSS_CANDIDATES.slice().sort((a, b) => vitePriority(a) - vitePriority(b))
|
|
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;
|
|
485
|
+
for (const candidate of candidates) {
|
|
486
|
+
const absolute = path.resolve(cwd, candidate);
|
|
487
|
+
if (!fs.existsSync(absolute)) continue;
|
|
488
|
+
if (path.resolve(absolute) === path.resolve(sparkleDesignPath)) continue;
|
|
489
|
+
try {
|
|
490
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
491
|
+
if (REGEX.TAILWIND_IMPORT.test(content)) {
|
|
492
|
+
console.log(`📝 Tailwind エントリポイントを検出しました(project root): ${candidate}`);
|
|
493
|
+
return { path: absolute, source: 'project' };
|
|
494
|
+
}
|
|
495
|
+
if (!fallbackExistingPath) {
|
|
496
|
+
fallbackExistingPath = { path: absolute, candidate };
|
|
497
|
+
}
|
|
498
|
+
} catch (err) {
|
|
499
|
+
// ENOENT 系は existsSync で既に弾いているので、ここに来るのは権限不足や
|
|
500
|
+
// ディレクトリ衝突などデバッグ価値のあるケース。silent に消さずに理由を出す。
|
|
501
|
+
// en: ENOENT is already filtered by existsSync above, so surfacing the
|
|
502
|
+
// error code here catches permission / type issues worth debugging.
|
|
503
|
+
console.warn(
|
|
504
|
+
`⚠️ 候補 ${candidate} の読み込みに失敗したためスキップします (${err.code ?? err.message})`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
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
|
+
|
|
516
|
+
// 3. デフォルト: sparkle-design.css と同じディレクトリの globals.css
|
|
517
|
+
// en: Last resort — a globals.css next to sparkle-design.css.
|
|
293
518
|
const defaultPath = path.join(dir, 'globals.css');
|
|
294
519
|
if (fs.existsSync(defaultPath)) {
|
|
295
520
|
return { path: defaultPath, source: 'default' };
|
|
@@ -301,19 +526,54 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
|
|
|
301
526
|
/**
|
|
302
527
|
* フォント管理の自動処理を実行する
|
|
303
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
|
+
*
|
|
304
536
|
* @param {string} sparkleDesignPath sparkle-design.cssのパス
|
|
305
537
|
* @param {Array<string>|null} sourcePackages 追加パッケージ名の配列(null の場合は @source を生成しない)
|
|
306
538
|
* @param {string|null} customCssPath custom-css ファイルの相対パス
|
|
307
539
|
* @param {string|null} globalsPathOverride 明示的に指定された globals パス
|
|
540
|
+
* @param {{ strict?: boolean }} [options] strict=true のとき失敗を throw する
|
|
541
|
+
* @returns {{ status: 'skipped' | 'updated' | 'failed', reason?: string }}
|
|
308
542
|
*/
|
|
309
|
-
export function manageFontImports(
|
|
543
|
+
export function manageFontImports(
|
|
544
|
+
sparkleDesignPath,
|
|
545
|
+
sourcePackages = null,
|
|
546
|
+
customCssPath = null,
|
|
547
|
+
globalsPathOverride = null,
|
|
548
|
+
options = {}
|
|
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
|
+
|
|
310
563
|
try {
|
|
311
564
|
// 1. globals パスを解決
|
|
312
565
|
const resolved = resolveGlobalsPath(sparkleDesignPath, globalsPathOverride);
|
|
313
566
|
|
|
314
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
|
+
}
|
|
315
575
|
console.log(MESSAGES.GLOBALS_NOT_FOUND);
|
|
316
|
-
return;
|
|
576
|
+
return { status: 'skipped', reason: 'entry-css-not-found-no-work' };
|
|
317
577
|
}
|
|
318
578
|
|
|
319
579
|
const globalsPath = resolved.path;
|
|
@@ -322,27 +582,57 @@ export function manageFontImports(sparkleDesignPath, sourcePackages = null, cust
|
|
|
322
582
|
const sparkleContent = fs.readFileSync(sparkleDesignPath, 'utf8');
|
|
323
583
|
const fontImports = extractFontImports(sparkleContent);
|
|
324
584
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
585
|
+
// v2.0.0 以降はフォント @import が SparkleHead.tsx に移行したため、
|
|
586
|
+
// sparkle-design.css に font @import が残らないのが通常状態。それでも
|
|
587
|
+
// 既存 globals.css には `@source` / sparkle-design.css import /
|
|
588
|
+
// custom-css import を挿入する必要があるので、単に fonts が空だからと
|
|
589
|
+
// いって早期 return せず、下流の update 関数に判断を委ねる。
|
|
590
|
+
// en: Fonts moved to SparkleHead in v2.0.0, so "no font imports in
|
|
591
|
+
// sparkle-design.css" is now the normal case. Don't bail out here —
|
|
592
|
+
// `updateGlobalsWithFonts` still needs to inject `@source` and the
|
|
593
|
+
// sparkle-design.css import into existing globals.css.
|
|
594
|
+
if (fontImports.length > 0) {
|
|
595
|
+
console.log(MESSAGES.FONT_DETECTED(fontImports.length));
|
|
328
596
|
}
|
|
329
597
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
|
|
598
|
+
// 4. globals.css に import / @source を追加
|
|
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(
|
|
606
|
+
fontImports,
|
|
607
|
+
globalsPath,
|
|
608
|
+
sourcePackages,
|
|
609
|
+
customCssPath,
|
|
610
|
+
sparkleDesignPath
|
|
611
|
+
);
|
|
334
612
|
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
613
|
+
if (result.status === 'skipped') {
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
if (result.status === 'failed') {
|
|
617
|
+
return raiseOrReturn(result);
|
|
338
618
|
}
|
|
339
619
|
|
|
340
|
-
// 5. sparkle-design.css
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
620
|
+
// 5. fonts が sparkle-design.css 側に残っていれば削除(globals 側に移動済みの前提)
|
|
621
|
+
// en: If there were fonts to move, strip them from sparkle-design.css.
|
|
622
|
+
if (fontImports.length > 0) {
|
|
623
|
+
const cleanedSparkleContent = removeFontImportsFromCSS(sparkleContent);
|
|
624
|
+
fs.writeFileSync(sparkleDesignPath, cleanedSparkleContent, 'utf8');
|
|
625
|
+
console.log(MESSAGES.FONT_REMOVED);
|
|
626
|
+
}
|
|
627
|
+
return result;
|
|
344
628
|
} catch (error) {
|
|
345
629
|
console.error(MESSAGES.FONT_MANAGEMENT_ERROR(error.message));
|
|
346
|
-
//
|
|
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}` };
|
|
347
637
|
}
|
|
348
638
|
}
|