sh-ui-cli 0.67.2 → 0.68.0
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/bin/sh-ui.mjs +6 -0
- package/data/changelog/versions.json +26 -0
- package/data/registry/react/components/sidebar/index.tsx +7 -2
- package/data/registry/react/components/sidebar/styles.css +14 -2
- package/data/registry/react/tokens-used.json +1983 -0
- package/package.json +1 -1
- package/src/add.mjs +53 -5
- package/src/doctor.mjs +289 -0
- package/src/init.mjs +4 -0
- package/src/paths.mjs +11 -0
- package/src/tokens-validate.mjs +86 -0
- package/templates/nextjs-standalone/_arch/flat/sh-ui.config.json +1 -0
- package/templates/nextjs-standalone/_arch/fsd/sh-ui.config.json +1 -0
- package/templates/ui-app-template/sh-ui.config.json +1 -0
package/package.json
CHANGED
package/src/add.mjs
CHANGED
|
@@ -7,6 +7,10 @@ import { select } from "@inquirer/prompts";
|
|
|
7
7
|
import { formatUnifiedDiff } from "./diff.mjs";
|
|
8
8
|
import { getRegistryRoot, getTokensRoot, getPeerVersionsPath } from "./paths.mjs";
|
|
9
9
|
import { THEME_BASES } from "./constants.js";
|
|
10
|
+
import {
|
|
11
|
+
findMissingTokens,
|
|
12
|
+
loadDefinedVarsFromConfig,
|
|
13
|
+
} from "./tokens-validate.mjs";
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* 기존 파일과 registry 파일 내용이 다를 때 keep/overwrite 결정.
|
|
@@ -245,7 +249,7 @@ function effectiveFramework(entry, cssFramework) {
|
|
|
245
249
|
return hasVariant ? cssFramework : "plain";
|
|
246
250
|
}
|
|
247
251
|
|
|
248
|
-
async function addComponent(name, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver) {
|
|
252
|
+
async function addComponent(name, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver, validationCtx) {
|
|
249
253
|
const registryRoot = getRegistryRoot(config.platform);
|
|
250
254
|
const registry = JSON.parse(
|
|
251
255
|
await readFile(resolve(registryRoot, "registry.json"), "utf8"),
|
|
@@ -260,6 +264,20 @@ async function addComponent(name, config, cwd, installed, pendingDeps, diffMode,
|
|
|
260
264
|
const requestedFw = config.cssFramework ?? "plain";
|
|
261
265
|
const cssFramework = effectiveFramework(entry, requestedFw);
|
|
262
266
|
|
|
267
|
+
// 컴포넌트가 요구하는 CSS 변수가 사용자 tokens.css 에 정의돼 있는지 검증.
|
|
268
|
+
// tokens.css 자체가 없거나, registry 에 메타가 없으면 검사 스킵 (정상 케이스).
|
|
269
|
+
if (validationCtx?.definedVars && !diffMode) {
|
|
270
|
+
const missing = await findMissingTokens({
|
|
271
|
+
platform: config.platform,
|
|
272
|
+
name,
|
|
273
|
+
framework: cssFramework,
|
|
274
|
+
defined: validationCtx.definedVars,
|
|
275
|
+
});
|
|
276
|
+
if (missing && missing.length > 0) {
|
|
277
|
+
validationCtx.missingTokenReports.push({ name, framework: cssFramework, missing });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
263
281
|
// 사용자가 plain 외 변종을 골랐는데 이 컴포넌트는 plain 으로 fallback 된 경우 한 줄 알림.
|
|
264
282
|
// 동작에 문제는 없지만 일관성에 대한 기대를 정확히 셋업하기 위함.
|
|
265
283
|
if (requestedFw !== "plain" && cssFramework === "plain" && !diffMode) {
|
|
@@ -276,7 +294,7 @@ async function addComponent(name, config, cwd, installed, pendingDeps, diffMode,
|
|
|
276
294
|
}
|
|
277
295
|
|
|
278
296
|
for (const dep of entry.registryDependencies ?? []) {
|
|
279
|
-
await addOne(dep, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver);
|
|
297
|
+
await addOne(dep, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver, validationCtx);
|
|
280
298
|
}
|
|
281
299
|
|
|
282
300
|
for (const file of entry.files) {
|
|
@@ -408,8 +426,21 @@ export async function add({
|
|
|
408
426
|
const installed = new Set();
|
|
409
427
|
const pendingDeps = new Set();
|
|
410
428
|
const summary = [];
|
|
429
|
+
// tokens.css 정의 변수는 한 번만 읽어서 모든 컴포넌트 검증에 재사용.
|
|
430
|
+
// tokens 가 같이 add 되는 경우엔 처리 후 컴포넌트가 add 되도록 names 가 보통
|
|
431
|
+
// [tokens, …components…] 순이라 미리 읽어도 OK — 누락 경고는 사용자가 실제로
|
|
432
|
+
// tokens.css 를 안 만들었을 때만 의미 있으므로.
|
|
433
|
+
const definedVars = await loadDefinedVarsFromConfig(config, cwd);
|
|
434
|
+
const missingTokenReports = [];
|
|
411
435
|
for (const name of names) {
|
|
412
|
-
await addOne(name, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver
|
|
436
|
+
await addOne(name, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver, {
|
|
437
|
+
definedVars,
|
|
438
|
+
missingTokenReports,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!diffMode && missingTokenReports.length > 0) {
|
|
443
|
+
renderMissingTokenReport(missingTokenReports, config);
|
|
413
444
|
}
|
|
414
445
|
|
|
415
446
|
if (diffMode) {
|
|
@@ -452,16 +483,33 @@ export async function add({
|
|
|
452
483
|
}
|
|
453
484
|
}
|
|
454
485
|
|
|
455
|
-
async function addOne(name, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver) {
|
|
486
|
+
async function addOne(name, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver, validationCtx) {
|
|
456
487
|
if (installed.has(name)) return;
|
|
457
488
|
installed.add(name);
|
|
458
489
|
if (name === "tokens") {
|
|
459
490
|
await addTokens(config, cwd, diffMode, summary, conflictResolver);
|
|
460
491
|
} else {
|
|
461
|
-
await addComponent(name, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver);
|
|
492
|
+
await addComponent(name, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver, validationCtx);
|
|
462
493
|
}
|
|
463
494
|
}
|
|
464
495
|
|
|
496
|
+
/**
|
|
497
|
+
* 컴포넌트가 요구하는 CSS 변수 중 사용자 tokens.css 에 없는 것들을 한 번에 안내.
|
|
498
|
+
* fatal 이 아닌 경고 — 실제 silent breakage 는 시각적으로만 나타나므로 미리 짚어 준다.
|
|
499
|
+
*/
|
|
500
|
+
function renderMissingTokenReport(reports, config) {
|
|
501
|
+
const tokensRel = config.paths?.tokens ?? "(paths.tokens 미설정)";
|
|
502
|
+
console.log(`\n⚠ 일부 컴포넌트가 요구하는 CSS 변수가 ${tokensRel} 에 없습니다:`);
|
|
503
|
+
for (const r of reports) {
|
|
504
|
+
const preview = r.missing.slice(0, 6).join(", ");
|
|
505
|
+
const more = r.missing.length > 6 ? ` (+${r.missing.length - 6} more)` : "";
|
|
506
|
+
console.log(` · ${r.name} [${r.framework}] — ${preview}${more}`);
|
|
507
|
+
}
|
|
508
|
+
console.log(
|
|
509
|
+
` → 해결: \`sh-ui add tokens\` 로 토큰을 다시 빌드하거나, ${tokensRel} 을 직접 수정.`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
465
513
|
function renderDiffReport(summary) {
|
|
466
514
|
const created = summary.filter((s) => s.kind === "new");
|
|
467
515
|
const modified = summary.filter((s) => s.kind === "modified");
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// sh-ui doctor — 프로젝트 정합성 점검.
|
|
2
|
+
//
|
|
3
|
+
// 점검 항목:
|
|
4
|
+
// 1) sh-ui.config.json 존재 + 필수 필드 (platform, theme, paths)
|
|
5
|
+
// 2) paths.tokens 파일 존재
|
|
6
|
+
// 3) paths.cssEntry (선택) 가 tokens.css 를 import 하는지
|
|
7
|
+
// 4) 설치된 컴포넌트가 요구하는 CSS 변수가 tokens.css 에 모두 정의돼 있는지
|
|
8
|
+
//
|
|
9
|
+
// 출력: 항목별 ✓ / ⚠ / ✗ + 해결 힌트. 모든 검사가 통과하면 exit 0,
|
|
10
|
+
// 하나라도 ✗ 면 exit 1 (CI 통합 가능).
|
|
11
|
+
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { resolve, relative, dirname, posix } from "node:path";
|
|
15
|
+
import { getRegistryRoot } from "./paths.mjs";
|
|
16
|
+
import {
|
|
17
|
+
extractDefinedVars,
|
|
18
|
+
findMissingTokens,
|
|
19
|
+
} from "./tokens-validate.mjs";
|
|
20
|
+
|
|
21
|
+
const ICON = { ok: "✓", warn: "⚠", fail: "✗" };
|
|
22
|
+
|
|
23
|
+
class Report {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.lines = [];
|
|
26
|
+
this.failCount = 0;
|
|
27
|
+
this.warnCount = 0;
|
|
28
|
+
}
|
|
29
|
+
ok(label, detail) {
|
|
30
|
+
this.lines.push({ kind: "ok", label, detail });
|
|
31
|
+
}
|
|
32
|
+
warn(label, detail) {
|
|
33
|
+
this.warnCount++;
|
|
34
|
+
this.lines.push({ kind: "warn", label, detail });
|
|
35
|
+
}
|
|
36
|
+
fail(label, detail) {
|
|
37
|
+
this.failCount++;
|
|
38
|
+
this.lines.push({ kind: "fail", label, detail });
|
|
39
|
+
}
|
|
40
|
+
render() {
|
|
41
|
+
for (const line of this.lines) {
|
|
42
|
+
const icon = ICON[line.kind === "ok" ? "ok" : line.kind === "warn" ? "warn" : "fail"];
|
|
43
|
+
const head = `${icon} ${line.label}`;
|
|
44
|
+
if (line.detail) {
|
|
45
|
+
console.log(head);
|
|
46
|
+
for (const d of line.detail.split("\n")) console.log(` ${d}`);
|
|
47
|
+
} else {
|
|
48
|
+
console.log(head);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
console.log("");
|
|
52
|
+
if (this.failCount === 0 && this.warnCount === 0) {
|
|
53
|
+
console.log(`✓ 모든 검사 통과`);
|
|
54
|
+
} else {
|
|
55
|
+
console.log(
|
|
56
|
+
`검사 완료 — ${this.failCount} 실패, ${this.warnCount} 경고`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function loadConfig(cwd, report) {
|
|
63
|
+
const configPath = resolve(cwd, "sh-ui.config.json");
|
|
64
|
+
if (!existsSync(configPath)) {
|
|
65
|
+
report.fail(
|
|
66
|
+
"sh-ui.config.json",
|
|
67
|
+
`${relative(cwd, configPath)} 가 없습니다. \`sh-ui init\` 으로 생성하세요.`,
|
|
68
|
+
);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
73
|
+
const issues = [];
|
|
74
|
+
if (!config.platform) issues.push("platform 미설정");
|
|
75
|
+
if (!config.theme) issues.push("theme 미설정");
|
|
76
|
+
if (!config.paths) issues.push("paths 미설정");
|
|
77
|
+
if (issues.length > 0) {
|
|
78
|
+
report.fail("sh-ui.config.json", `필수 필드 누락: ${issues.join(", ")}`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
report.ok(
|
|
82
|
+
"sh-ui.config.json",
|
|
83
|
+
`platform=${config.platform} theme.base=${config.theme?.base ?? "?"} ` +
|
|
84
|
+
`cssFramework=${config.cssFramework ?? "(기본 plain)"}`,
|
|
85
|
+
);
|
|
86
|
+
return config;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
report.fail("sh-ui.config.json", `JSON 파싱 실패: ${err.message}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function checkTokensFile(config, cwd, report) {
|
|
94
|
+
const rel = config.paths?.tokens;
|
|
95
|
+
if (!rel) {
|
|
96
|
+
// 모노레포 ui-core 처럼 tokens 를 가지지 않는 패키지 — 이 경우 검사 스킵.
|
|
97
|
+
report.warn(
|
|
98
|
+
"paths.tokens",
|
|
99
|
+
"config 에 paths.tokens 가 없습니다 — 토큰 검증 스킵.",
|
|
100
|
+
);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const tokensPath = resolve(cwd, rel);
|
|
104
|
+
if (!existsSync(tokensPath)) {
|
|
105
|
+
report.fail(
|
|
106
|
+
`paths.tokens — ${rel}`,
|
|
107
|
+
`파일이 없습니다. \`sh-ui add tokens\` 로 생성하세요.`,
|
|
108
|
+
);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
report.ok(`paths.tokens — ${rel}`);
|
|
112
|
+
return tokensPath;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* cssEntry 가 설정돼 있으면 그 파일이 tokens.css 를 import 하는지 검사.
|
|
117
|
+
* @import './tokens.css' 또는 @import "../shared/styles/tokens.css" 형태 모두 인정.
|
|
118
|
+
* 명시 import 없이 빌드 도구가 알아서 합치는 경우(Tailwind v4 의 @import "tailwindcss"
|
|
119
|
+
* 만 있는 경우) 도 있어 fail 이 아니라 warn.
|
|
120
|
+
*/
|
|
121
|
+
async function checkCssEntry(config, cwd, tokensPath, report) {
|
|
122
|
+
const entryRel = config.paths?.cssEntry;
|
|
123
|
+
if (!entryRel) {
|
|
124
|
+
report.ok(
|
|
125
|
+
"paths.cssEntry",
|
|
126
|
+
"(미설정 — 검사 스킵. 설정하면 tokens.css import 정합성을 검증합니다.)",
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const entryPath = resolve(cwd, entryRel);
|
|
131
|
+
if (!existsSync(entryPath)) {
|
|
132
|
+
report.fail(
|
|
133
|
+
`paths.cssEntry — ${entryRel}`,
|
|
134
|
+
`파일이 없습니다. config 의 paths.cssEntry 를 실제 globals.css 위치로 수정하세요.`,
|
|
135
|
+
);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!tokensPath) {
|
|
139
|
+
report.warn(
|
|
140
|
+
`paths.cssEntry — ${entryRel}`,
|
|
141
|
+
"tokens.css 가 없어 import 검증 불가.",
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const text = await readFile(entryPath, "utf8");
|
|
146
|
+
const tokensFilename = posix.basename(entryRel.replaceAll("\\", "/"));
|
|
147
|
+
// 단순 휴리스틱: tokens 파일 베이스네임을 포함하는 @import 가 있는지.
|
|
148
|
+
const tokensBase = posix.basename(config.paths.tokens.replaceAll("\\", "/"));
|
|
149
|
+
const importRe = new RegExp(
|
|
150
|
+
`@import\\s+['"][^'"]*${tokensBase.replace(/\./g, "\\.")}['"]`,
|
|
151
|
+
);
|
|
152
|
+
if (importRe.test(text)) {
|
|
153
|
+
report.ok(
|
|
154
|
+
`paths.cssEntry — ${entryRel}`,
|
|
155
|
+
`${tokensBase} 를 import 합니다.`,
|
|
156
|
+
);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// import 가 없어도 빌드 도구가 합치는 경우가 있으므로 fail 이 아닌 warn.
|
|
160
|
+
report.warn(
|
|
161
|
+
`paths.cssEntry — ${entryRel}`,
|
|
162
|
+
`${tokensBase} 를 import 하는 줄이 보이지 않습니다. ` +
|
|
163
|
+
`CSS 변수가 unresolved 로 남을 수 있으니 \`@import './${tokensBase}';\` 같은 줄을 추가하거나, ` +
|
|
164
|
+
`빌드 도구가 자동 합치는 게 맞다면 무시하세요. (현재 파일: ${entryRel})`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 설치된 컴포넌트들의 토큰 의존성을 한 번에 검증.
|
|
170
|
+
* registry.json 을 순회하며 destination 파일이 존재하는 컴포넌트를 "설치됨" 으로 간주.
|
|
171
|
+
*/
|
|
172
|
+
async function checkInstalledComponents(config, cwd, definedVars, report) {
|
|
173
|
+
if (!definedVars) {
|
|
174
|
+
report.warn(
|
|
175
|
+
"컴포넌트 토큰 의존성",
|
|
176
|
+
"tokens.css 가 없어 검증 스킵.",
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const registryRoot = getRegistryRoot(config.platform);
|
|
181
|
+
const registryPath = resolve(registryRoot, "registry.json");
|
|
182
|
+
if (!existsSync(registryPath)) {
|
|
183
|
+
report.warn(
|
|
184
|
+
"컴포넌트 토큰 의존성",
|
|
185
|
+
`registry.json 을 찾을 수 없어 검증 스킵 (${registryPath}).`,
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const registry = JSON.parse(await readFile(registryPath, "utf8"));
|
|
190
|
+
const requestedFw = config.cssFramework ?? "plain";
|
|
191
|
+
|
|
192
|
+
let installedCount = 0;
|
|
193
|
+
const issues = [];
|
|
194
|
+
|
|
195
|
+
for (const [name, entry] of Object.entries(registry.components ?? {})) {
|
|
196
|
+
if (entry.type && entry.type !== "component") continue;
|
|
197
|
+
// 컴포넌트가 "설치됨" 으로 인정되려면 destination 파일 중 하나라도 존재해야 함.
|
|
198
|
+
let installed = false;
|
|
199
|
+
for (const file of entry.files ?? []) {
|
|
200
|
+
try {
|
|
201
|
+
const dest = resolveDest(file.dest, config);
|
|
202
|
+
if (existsSync(resolve(cwd, dest))) {
|
|
203
|
+
installed = true;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// paths placeholder 해석 실패 — 이 컴포넌트는 이 config 에서 install 불가
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!installed) continue;
|
|
211
|
+
installedCount++;
|
|
212
|
+
|
|
213
|
+
// file 변종 분포로 effective framework 추정.
|
|
214
|
+
const cssFramework = effectiveFramework(entry, requestedFw);
|
|
215
|
+
const missing = await findMissingTokens({
|
|
216
|
+
platform: config.platform,
|
|
217
|
+
name,
|
|
218
|
+
framework: cssFramework,
|
|
219
|
+
defined: definedVars,
|
|
220
|
+
});
|
|
221
|
+
if (missing && missing.length > 0) {
|
|
222
|
+
issues.push({ name, framework: cssFramework, missing });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (installedCount === 0) {
|
|
227
|
+
report.ok("설치된 컴포넌트 — 0개", "검증 대상 없음.");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (issues.length === 0) {
|
|
232
|
+
report.ok(
|
|
233
|
+
`설치된 컴포넌트 — ${installedCount}개`,
|
|
234
|
+
"모두 요구 토큰이 tokens.css 에 정의돼 있습니다.",
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const lines = issues.map((i) => {
|
|
240
|
+
const preview = i.missing.slice(0, 4).join(", ");
|
|
241
|
+
const more = i.missing.length > 4 ? ` (+${i.missing.length - 4} more)` : "";
|
|
242
|
+
return `· ${i.name} [${i.framework}] — ${preview}${more}`;
|
|
243
|
+
});
|
|
244
|
+
report.fail(
|
|
245
|
+
`설치된 컴포넌트 — ${installedCount}개 중 ${issues.length}개에 누락된 토큰`,
|
|
246
|
+
lines.join("\n") +
|
|
247
|
+
`\n해결: \`sh-ui add tokens\` 로 토큰을 다시 빌드하거나 ${config.paths.tokens} 를 직접 수정.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveDest(template, config) {
|
|
252
|
+
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (m, key) => {
|
|
253
|
+
const v = config.paths?.[key];
|
|
254
|
+
if (!v) throw new Error(`paths.${key} 미설정`);
|
|
255
|
+
return v;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function effectiveFramework(entry, cssFramework) {
|
|
260
|
+
if (cssFramework === "plain") return cssFramework;
|
|
261
|
+
const hasVariant = (entry.files ?? []).some(
|
|
262
|
+
(f) => f.frameworks && f.frameworks.includes(cssFramework),
|
|
263
|
+
);
|
|
264
|
+
return hasVariant ? cssFramework : "plain";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function doctor({ cwd }) {
|
|
268
|
+
console.log(`sh-ui doctor — ${relative(process.cwd(), cwd) || "."}\n`);
|
|
269
|
+
const report = new Report();
|
|
270
|
+
|
|
271
|
+
const config = await loadConfig(cwd, report);
|
|
272
|
+
if (!config) {
|
|
273
|
+
report.render();
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const tokensPath = checkTokensFile(config, cwd, report);
|
|
278
|
+
await checkCssEntry(config, cwd, tokensPath, report);
|
|
279
|
+
|
|
280
|
+
let definedVars = null;
|
|
281
|
+
if (tokensPath) {
|
|
282
|
+
const text = await readFile(tokensPath, "utf8");
|
|
283
|
+
definedVars = extractDefinedVars(text);
|
|
284
|
+
}
|
|
285
|
+
await checkInstalledComponents(config, cwd, definedVars, report);
|
|
286
|
+
|
|
287
|
+
report.render();
|
|
288
|
+
if (report.failCount > 0) process.exit(1);
|
|
289
|
+
}
|
package/src/init.mjs
CHANGED
|
@@ -26,6 +26,10 @@ const DEFAULTS = INIT_DEFAULTS;
|
|
|
26
26
|
const PATHS = {
|
|
27
27
|
react: {
|
|
28
28
|
tokens: "src/styles/tokens.css",
|
|
29
|
+
// cssEntry — tokens.css 를 import 하는 글로벌 CSS 진입점.
|
|
30
|
+
// doctor 가 import 정합성 검증에 사용. Next.js App Router 기본값 가정 —
|
|
31
|
+
// 다른 위치라면 사용자가 sh-ui.config.json 에서 수정.
|
|
32
|
+
cssEntry: "app/globals.css",
|
|
29
33
|
styles: "src/styles",
|
|
30
34
|
components: "src/components/ui",
|
|
31
35
|
utils: "src/lib/utils.ts",
|
package/src/paths.mjs
CHANGED
|
@@ -40,6 +40,17 @@ export function getPeerVersionsPath(platform) {
|
|
|
40
40
|
: resolve(MONOREPO_PACKAGES, "registry", platform, "peer-versions.json");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 컴포넌트별 토큰 의존성 매니페스트.
|
|
45
|
+
* scripts/build-registry-tokens.mjs 가 컴포넌트 CSS 의 var(--*) 참조를 추출해 생성.
|
|
46
|
+
* add 시점 검증 + sh-ui doctor 가 사용.
|
|
47
|
+
*/
|
|
48
|
+
export function getTokensUsedPath(platform) {
|
|
49
|
+
return isBundled
|
|
50
|
+
? resolve(BUNDLED_DATA, "registry", platform, "tokens-used.json")
|
|
51
|
+
: resolve(MONOREPO_PACKAGES, "registry", platform, "tokens-used.json");
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
/** llms 요약 JSON (platform별) */
|
|
44
55
|
export function getSummariesPath(platform) {
|
|
45
56
|
return isBundled
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// 컴포넌트가 요구하는 CSS 변수와 사용자 tokens.css 가 정의한 변수를 비교한다.
|
|
2
|
+
//
|
|
3
|
+
// 호출:
|
|
4
|
+
// - sh-ui add <component> 진입 시 (silent breakage 방지)
|
|
5
|
+
// - sh-ui doctor 가 설치된 컴포넌트 전체에 대해 (정합성 점검)
|
|
6
|
+
//
|
|
7
|
+
// 정책:
|
|
8
|
+
// - tokens.css 가 아예 없으면 누락 검사를 건너뛴다 (사용자가 곧 add tokens 할 거라
|
|
9
|
+
// 가정 — add 시점 안내는 별도 메시지로 처리).
|
|
10
|
+
// - 컴포넌트 또는 framework 가 tokens-used.json 에 없으면 검사 스킵 (없는 게 정상).
|
|
11
|
+
// - Flutter platform 은 검사 대상 아님 — CSS var() 와 다른 추출 방식이라 별도 도구.
|
|
12
|
+
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import { getTokensUsedPath } from "./paths.mjs";
|
|
17
|
+
|
|
18
|
+
let tokensUsedCache = null;
|
|
19
|
+
|
|
20
|
+
/** tokens-used.json 캐시 로드. 없거나 깨지면 null. */
|
|
21
|
+
async function loadTokensUsed(platform) {
|
|
22
|
+
if (tokensUsedCache) return tokensUsedCache;
|
|
23
|
+
const path = getTokensUsedPath(platform);
|
|
24
|
+
if (!existsSync(path)) return null;
|
|
25
|
+
try {
|
|
26
|
+
const data = JSON.parse(await readFile(path, "utf8"));
|
|
27
|
+
tokensUsedCache = data;
|
|
28
|
+
return data;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** tokens.css 텍스트에서 정의된 --변수명 집합 추출 (선언만 — 참조는 무시). */
|
|
35
|
+
export function extractDefinedVars(cssText) {
|
|
36
|
+
const out = new Set();
|
|
37
|
+
// `--name:` 선언만 잡고, var(--name) 참조는 무시한다.
|
|
38
|
+
// 음수 lookbehind 가 var( 인 경우를 제외.
|
|
39
|
+
const re = /(?<!var\(\s*)(--[a-zA-Z0-9_-]+)\s*:/g;
|
|
40
|
+
let m;
|
|
41
|
+
while ((m = re.exec(cssText))) out.add(m[1]);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 컴포넌트의 framework 별 토큰 요구사항 조회.
|
|
47
|
+
* 못 찾으면 null — 호출부는 검사를 스킵한다.
|
|
48
|
+
*/
|
|
49
|
+
export async function getRequiredTokens(platform, name, framework) {
|
|
50
|
+
if (platform === "flutter") return null;
|
|
51
|
+
const data = await loadTokensUsed(platform);
|
|
52
|
+
if (!data) return null;
|
|
53
|
+
const entry = data.components?.[name];
|
|
54
|
+
if (!entry) return null;
|
|
55
|
+
return entry[framework] ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 사용자 tokens.css 를 읽어 정의된 변수 집합을 반환.
|
|
60
|
+
* 파일이 없으면 null — 호출부는 "tokens.css 미생성" 메시지를 따로 처리.
|
|
61
|
+
*/
|
|
62
|
+
export async function loadDefinedVarsFromConfig(config, cwd) {
|
|
63
|
+
const tokensRel = config.paths?.tokens;
|
|
64
|
+
if (!tokensRel) return null;
|
|
65
|
+
const tokensPath = resolve(cwd, tokensRel);
|
|
66
|
+
if (!existsSync(tokensPath)) return null;
|
|
67
|
+
const text = await readFile(tokensPath, "utf8");
|
|
68
|
+
return extractDefinedVars(text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 한 컴포넌트의 누락 토큰 목록 계산.
|
|
73
|
+
* @returns {Promise<string[]|null>} 누락 토큰 정렬 배열, 또는 null (검사 불가/스킵)
|
|
74
|
+
*/
|
|
75
|
+
export async function findMissingTokens({
|
|
76
|
+
platform,
|
|
77
|
+
name,
|
|
78
|
+
framework,
|
|
79
|
+
defined,
|
|
80
|
+
}) {
|
|
81
|
+
const required = await getRequiredTokens(platform, name, framework);
|
|
82
|
+
if (!required || required.length === 0) return null;
|
|
83
|
+
if (!defined) return null;
|
|
84
|
+
const missing = required.filter((v) => !defined.has(v));
|
|
85
|
+
return missing.sort();
|
|
86
|
+
}
|