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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.67.2",
3
+ "version": "0.68.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
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
+ }
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "paths": {
10
10
  "tokens": "lib/styles/tokens.css",
11
+ "cssEntry": "app/globals.css",
11
12
  "styles": "lib/styles",
12
13
  "components": "components/common",
13
14
  "utils": "lib/utils/utils.ts"
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "paths": {
10
10
  "tokens": "src/shared/styles/tokens.css",
11
+ "cssEntry": "app/globals.css",
11
12
  "styles": "src/shared/styles",
12
13
  "components": "src/shared/ui",
13
14
  "utils": "src/shared/lib/utils.ts"
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "paths": {
11
11
  "tokens": "src/styles/tokens.css",
12
+ "cssEntry": "src/styles/globals.css",
12
13
  "styles": "src/styles"
13
14
  }
14
15
  }