sh-ui-cli 0.70.0 → 0.71.1
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/data/changelog/versions.json +29 -0
- package/package.json +1 -1
- package/src/add.mjs +73 -2
- package/src/css-bundle.mjs +105 -0
- package/src/doctor.mjs +27 -0
- package/src/mcp.mjs +37 -0
- package/src/remove.mjs +57 -8
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
|
|
4
4
|
"versions": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.71.1",
|
|
7
|
+
"date": "2026-05-09",
|
|
8
|
+
"title": "chore(mcp): instructions 에 v0.68~0.71 신규 명령 가이드 반영",
|
|
9
|
+
"type": "patch",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**MCP `instructions` 갱신** — `sh-ui doctor` (v0.68), `sh-ui tokens diff/upgrade --apply/--replace` (v0.69), `sh-ui theme extract` (v0.70), `cssStrategy: bundled` (v0.71) 사용 시점을 명시. AI 가 사용자 의도 (\"focus 표시 안 보임\", \"새 토큰 받아줘\", \"색 base64 로 뽑아줘\", \"styles.css 너무 많아\") 와 Bash 명령 호출을 직접 매핑.",
|
|
12
|
+
"**진단 흐름** — UI 작업 후 시각적 깨짐 또는 사용자가 토큰을 손댄 직후 doctor 권장. 누락 토큰 신호 시 tokens upgrade --apply 로 incremental 적용.",
|
|
13
|
+
"**토큰 마이그레이션 가이드** — tokens diff 로 미리보기 → --apply (사용자 편집 보존) 또는 --replace (리셋) 선택. buildable preset 제약 + custom/rich preset 은 encode/decode round-trip 으로 우회.",
|
|
14
|
+
"**theme extract 활용 패턴** — encode 의 역방향. tokens.css 직접 편집 후 base64 추출 → 다른 앱에 동일 톤 박기 또는 디자인 시스템 문서 보존.",
|
|
15
|
+
"**cssStrategy: bundled 전환 가이드** — config 옵션 + paths.cssBundle 안내 + globals.css import 책임 + 마이그레이션 시 force=true 재실행."
|
|
16
|
+
],
|
|
17
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.71.1"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.71.0",
|
|
21
|
+
"date": "2026-05-09",
|
|
22
|
+
"title": "cssStrategy: bundled — 단일 CSS 번들 모드 (Phase D)",
|
|
23
|
+
"type": "minor",
|
|
24
|
+
"highlights": [
|
|
25
|
+
"**`cssStrategy: \"bundled\"` 옵션 신설** — `sh-ui.config.json` 에서 켜면 모든 컴포넌트 CSS 가 `paths.cssBundle` 에 지정된 단일 파일로 누적. 컴포넌트별 `styles.css` 파일이 컴포넌트 폴더에 떨어지지 않음. 기본값은 `per-component` (기존 동작) — 옵트인.",
|
|
26
|
+
"**마커 기반 섹션 — `/* sh-ui:component:NAME-start ... -end */`** — sh-ui 가 마커 사이만 관리. 사용자 custom CSS 를 마커 밖에 추가해도 add/remove 가 그대로 보존. 같은 컴포넌트 재 add 면 안 내용만 교체.",
|
|
27
|
+
"**add 동작** — bundled 모드에서 컴포넌트 .tsx 의 `import \"./styles.css\";` 라인을 자동 제거 (번들이 한 번 import 되므로 불필요). conflict resolver 우회 (마커 사이는 sh-ui 영역).",
|
|
28
|
+
"**remove 동작** — .tsx 삭제 + 번들에서 해당 섹션 제거. 사용자 수정 검사도 stripStylesImport 변환 적용해 false positive 회피. 같은 dest 의 변종 중복 처리 fix.",
|
|
29
|
+
"**doctor 통합** — bundled 모드일 때 `paths.cssBundle` 존재 검증. 미설정/누락 시 fail.",
|
|
30
|
+
"**제약** — `cssFramework: \"plain\"` 에서만 동작 (tailwind/css-modules/vanilla-extract 는 자체 스코프 메커니즘). 사용자가 번들 파일을 globals.css 같은 곳에서 한 번 import 해야 스타일 적용 — 자동화 안 함."
|
|
31
|
+
],
|
|
32
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.71.0"
|
|
33
|
+
},
|
|
5
34
|
{
|
|
6
35
|
"version": "0.70.0",
|
|
7
36
|
"date": "2026-05-09",
|
package/package.json
CHANGED
package/src/add.mjs
CHANGED
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
findMissingTokens,
|
|
12
12
|
loadDefinedVarsFromConfig,
|
|
13
13
|
} from "./tokens-validate.mjs";
|
|
14
|
+
import {
|
|
15
|
+
upsertSection,
|
|
16
|
+
stripStylesImport,
|
|
17
|
+
isStyleFile,
|
|
18
|
+
isTsxFile,
|
|
19
|
+
} from "./css-bundle.mjs";
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* 기존 파일과 registry 파일 내용이 다를 때 keep/overwrite 결정.
|
|
@@ -224,6 +230,50 @@ async function addTokens(config, cwd, diffMode, summary, conflictResolver) {
|
|
|
224
230
|
}
|
|
225
231
|
}
|
|
226
232
|
|
|
233
|
+
/**
|
|
234
|
+
* bundled 모드 — config.paths.cssBundle 에 컴포넌트 섹션 upsert.
|
|
235
|
+
* 파일이 없으면 헤더 주석과 함께 새로 만든다.
|
|
236
|
+
*
|
|
237
|
+
* 마커 사이만 sh-ui 가 관리하므로 conflict resolver 를 거치지 않는다 — 사용자가
|
|
238
|
+
* 손댄 다른 섹션 / 마커 밖 custom CSS 는 보존되고, 같은 이름 섹션만 교체.
|
|
239
|
+
*/
|
|
240
|
+
async function writeBundleSection({ name, css, config, cwd, diffMode, summary }) {
|
|
241
|
+
const bundleRel = config.paths?.cssBundle;
|
|
242
|
+
if (!bundleRel) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"cssStrategy='bundled' 인데 paths.cssBundle 이 sh-ui.config.json 에 없습니다.\n" +
|
|
245
|
+
" 예: \"paths\": { ..., \"cssBundle\": \"src/shared/styles/sh-ui-components.css\" }",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const bundlePath = resolve(cwd, bundleRel);
|
|
249
|
+
const exists = existsSync(bundlePath);
|
|
250
|
+
const existingText = exists
|
|
251
|
+
? await readFile(bundlePath, "utf8")
|
|
252
|
+
: `/* sh-ui — 컴포넌트 CSS 번들 (cssStrategy: bundled). 마커 사이는 sh-ui 가 관리, 그 밖은 사용자 자유. */\n\n`;
|
|
253
|
+
const nextText = upsertSection(existingText, name, css);
|
|
254
|
+
|
|
255
|
+
if (existingText === nextText) {
|
|
256
|
+
return; // 변경 없음
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (diffMode) {
|
|
260
|
+
const rel = relative(cwd, bundlePath);
|
|
261
|
+
const { text, addCount, delCount } = formatUnifiedDiff(existingText, nextText, {
|
|
262
|
+
useColor: canUseColor(),
|
|
263
|
+
});
|
|
264
|
+
summary.push(
|
|
265
|
+
exists
|
|
266
|
+
? { kind: "modified", rel, addCount, delCount, diff: text }
|
|
267
|
+
: { kind: "new", rel },
|
|
268
|
+
);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await ensureDir(bundlePath);
|
|
273
|
+
await writeFile(bundlePath, nextText, "utf8");
|
|
274
|
+
console.log(`✓ ${name} → bundle ${relative(cwd, bundlePath)}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
227
277
|
/**
|
|
228
278
|
* registry 엔트리의 frameworks 필드와 현재 cssFramework 가 호환되는지.
|
|
229
279
|
* 필드가 없으면 "모든 프레임워크에 적용" — 기본 케이스.
|
|
@@ -297,12 +347,29 @@ async function addComponent(name, config, cwd, installed, pendingDeps, diffMode,
|
|
|
297
347
|
await addOne(dep, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver, validationCtx);
|
|
298
348
|
}
|
|
299
349
|
|
|
350
|
+
// bundled 모드 여부. plain 변종에서만 의미 있음 (tailwind/css-modules/vanilla-extract
|
|
351
|
+
// 는 자체 스코프가 있어 단일 파일 합산이 부적절).
|
|
352
|
+
const bundled = config.cssStrategy === "bundled" && cssFramework === "plain";
|
|
353
|
+
let bundleAccumulated = "";
|
|
354
|
+
|
|
300
355
|
for (const file of entry.files) {
|
|
301
356
|
if (!frameworkMatches(file, cssFramework)) continue;
|
|
302
357
|
const src = resolve(registryRoot, file.src);
|
|
303
|
-
const dest = resolve(cwd, resolveDest(file.dest, config));
|
|
304
358
|
const raw = await readFile(src, "utf8");
|
|
305
|
-
|
|
359
|
+
let content = substitutePlaceholders(raw, config, file.src);
|
|
360
|
+
|
|
361
|
+
if (bundled && isStyleFile(file)) {
|
|
362
|
+
// 컴포넌트별 styles.css 파일은 쓰지 않고 누적만 — 마지막에 bundle 에 upsert.
|
|
363
|
+
bundleAccumulated += (bundleAccumulated ? "\n\n" : "") + content.trim();
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (bundled && isTsxFile(file)) {
|
|
367
|
+
// .tsx 의 `import "./styles.css";` 제거 — bundled 모드는 사용자가 globals.css 에서
|
|
368
|
+
// bundle 을 한 번 import 하는 책임을 진다.
|
|
369
|
+
content = stripStylesImport(content);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const dest = resolve(cwd, resolveDest(file.dest, config));
|
|
306
373
|
const result = await writeOrDiff({ dest, content, cwd, diffMode, summary, conflictResolver });
|
|
307
374
|
if (!diffMode && result !== "unchanged") {
|
|
308
375
|
const prefix = result === "kept" ? "↷" : "✓";
|
|
@@ -311,6 +378,10 @@ async function addComponent(name, config, cwd, installed, pendingDeps, diffMode,
|
|
|
311
378
|
}
|
|
312
379
|
}
|
|
313
380
|
|
|
381
|
+
if (bundled && bundleAccumulated) {
|
|
382
|
+
await writeBundleSection({ name, css: bundleAccumulated, config, cwd, diffMode, summary, conflictResolver });
|
|
383
|
+
}
|
|
384
|
+
|
|
314
385
|
for (const dep of entry.dependencies ?? []) {
|
|
315
386
|
// dep 은 string ("react-hook-form") 또는 object ({name, frameworks?: string[]}).
|
|
316
387
|
// 후자는 cssFramework 에 따라 의존성을 분기 (예: cva 는 tailwind 변종에만 필요).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// `cssStrategy: "bundled"` 모드에서 컴포넌트 CSS 를 단일 파일에 마커 기반 섹션으로 누적.
|
|
2
|
+
//
|
|
3
|
+
// 섹션 포맷:
|
|
4
|
+
// /* sh-ui:component:button-start */
|
|
5
|
+
// .sh-ui-button { ... }
|
|
6
|
+
// /* sh-ui:component:button-end */
|
|
7
|
+
//
|
|
8
|
+
// add 시 섹션을 append/replace, remove 시 섹션 제거. 마커 사이의 내용만 sh-ui 가 관리하고
|
|
9
|
+
// 파일의 다른 부분 (사용자가 추가한 custom CSS) 은 그대로 둔다.
|
|
10
|
+
|
|
11
|
+
const SEC_START = (name) => `/* sh-ui:component:${name}-start */`;
|
|
12
|
+
const SEC_END = (name) => `/* sh-ui:component:${name}-end */`;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* bundle 텍스트에서 컴포넌트 섹션을 append 하거나 replace.
|
|
16
|
+
* 동일 이름 섹션이 이미 있으면 그 안의 내용만 교체.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} bundleText 현재 번들 파일 내용 (없으면 빈 문자열)
|
|
19
|
+
* @param {string} name 컴포넌트 이름 (예: "button")
|
|
20
|
+
* @param {string} css 컴포넌트의 CSS (마커 제외)
|
|
21
|
+
* @returns {string} 갱신된 번들 텍스트
|
|
22
|
+
*/
|
|
23
|
+
export function upsertSection(bundleText, name, css) {
|
|
24
|
+
const start = SEC_START(name);
|
|
25
|
+
const end = SEC_END(name);
|
|
26
|
+
const startIdx = bundleText.indexOf(start);
|
|
27
|
+
const endIdx = bundleText.indexOf(end);
|
|
28
|
+
const trimmed = css.trim();
|
|
29
|
+
const block = `${start}\n${trimmed}\n${end}`;
|
|
30
|
+
|
|
31
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
32
|
+
return (
|
|
33
|
+
bundleText.slice(0, startIdx) +
|
|
34
|
+
block +
|
|
35
|
+
bundleText.slice(endIdx + end.length)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// append — 기존 내용이 있으면 빈 줄 한 개로 분리.
|
|
40
|
+
const sep = bundleText.length === 0
|
|
41
|
+
? ""
|
|
42
|
+
: bundleText.endsWith("\n\n")
|
|
43
|
+
? ""
|
|
44
|
+
: bundleText.endsWith("\n")
|
|
45
|
+
? "\n"
|
|
46
|
+
: "\n\n";
|
|
47
|
+
return bundleText + sep + block + "\n";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* bundle 텍스트에서 컴포넌트 섹션 제거. 없으면 그대로 반환.
|
|
52
|
+
*/
|
|
53
|
+
export function removeSection(bundleText, name) {
|
|
54
|
+
const start = SEC_START(name);
|
|
55
|
+
const end = SEC_END(name);
|
|
56
|
+
const startIdx = bundleText.indexOf(start);
|
|
57
|
+
const endIdx = bundleText.indexOf(end);
|
|
58
|
+
if (startIdx < 0 || endIdx <= startIdx) return bundleText;
|
|
59
|
+
// 섹션 + 다음 줄바꿈 한 번까지 제거 (중복 빈 줄 방지).
|
|
60
|
+
let cutEnd = endIdx + end.length;
|
|
61
|
+
if (bundleText[cutEnd] === "\n") cutEnd++;
|
|
62
|
+
return bundleText.slice(0, startIdx) + bundleText.slice(cutEnd);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* bundle 텍스트에서 등록된 컴포넌트 이름 목록 추출 (시작 마커 기준).
|
|
67
|
+
*/
|
|
68
|
+
export function listSections(bundleText) {
|
|
69
|
+
const re = /\/\*\s*sh-ui:component:([a-z0-9-]+)-start\s*\*\//g;
|
|
70
|
+
const out = [];
|
|
71
|
+
let m;
|
|
72
|
+
while ((m = re.exec(bundleText))) out.push(m[1]);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 컴포넌트 .tsx 의 `import "./styles.css";` 라인을 제거.
|
|
78
|
+
* 번들 모드에서는 styles.css 가 없으므로 이 import 는 빌드 깨뜨림.
|
|
79
|
+
*
|
|
80
|
+
* 다양한 quote / 공백을 허용. 다른 import 는 보존.
|
|
81
|
+
*/
|
|
82
|
+
export function stripStylesImport(tsxText) {
|
|
83
|
+
// import "./styles.css"; / import './styles.css' / import "./styles.module.css" 등
|
|
84
|
+
// 줄 끝 newline 은 있을 수도/없을 수도 있어 (?:\n|$) 로 처리.
|
|
85
|
+
return tsxText.replace(
|
|
86
|
+
/^[ \t]*import\s+['"]\.\/styles\.(?:module\.)?css['"];?\s*(?:\n|$)/gm,
|
|
87
|
+
"",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* registry 의 file 엔트리가 CSS 변종인지 (bundled 모드에서 별도 처리 대상).
|
|
93
|
+
*/
|
|
94
|
+
export function isStyleFile(file) {
|
|
95
|
+
const src = file?.src ?? "";
|
|
96
|
+
return /\.(css|module\.css|css\.ts)$/.test(src);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* registry 의 file 엔트리가 .tsx 변종인지 (stripStylesImport 적용 대상).
|
|
101
|
+
*/
|
|
102
|
+
export function isTsxFile(file) {
|
|
103
|
+
const src = file?.src ?? "";
|
|
104
|
+
return /\.tsx$/.test(src);
|
|
105
|
+
}
|
package/src/doctor.mjs
CHANGED
|
@@ -256,6 +256,32 @@ function resolveDest(template, config) {
|
|
|
256
256
|
});
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* cssStrategy='bundled' 인 경우 paths.cssBundle 파일 존재 + 마커 sanity 검사.
|
|
261
|
+
* per-component 모드면 검사 자체를 스킵 (필드가 없어도 정상).
|
|
262
|
+
*/
|
|
263
|
+
function checkCssBundle(config, cwd, report) {
|
|
264
|
+
if (config.cssStrategy !== "bundled") return;
|
|
265
|
+
const rel = config.paths?.cssBundle;
|
|
266
|
+
if (!rel) {
|
|
267
|
+
report.fail(
|
|
268
|
+
"paths.cssBundle",
|
|
269
|
+
"cssStrategy='bundled' 인데 paths.cssBundle 이 미설정. " +
|
|
270
|
+
"예: \"cssBundle\": \"src/shared/styles/sh-ui-components.css\"",
|
|
271
|
+
);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const bundlePath = resolve(cwd, rel);
|
|
275
|
+
if (!existsSync(bundlePath)) {
|
|
276
|
+
report.warn(
|
|
277
|
+
`paths.cssBundle — ${rel}`,
|
|
278
|
+
"파일 없음 — 컴포넌트를 add 하면 자동 생성됩니다.",
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
report.ok(`paths.cssBundle — ${rel}`, "(bundled 모드)");
|
|
283
|
+
}
|
|
284
|
+
|
|
259
285
|
function effectiveFramework(entry, cssFramework) {
|
|
260
286
|
if (cssFramework === "plain") return cssFramework;
|
|
261
287
|
const hasVariant = (entry.files ?? []).some(
|
|
@@ -276,6 +302,7 @@ export async function doctor({ cwd }) {
|
|
|
276
302
|
|
|
277
303
|
const tokensPath = checkTokensFile(config, cwd, report);
|
|
278
304
|
await checkCssEntry(config, cwd, tokensPath, report);
|
|
305
|
+
checkCssBundle(config, cwd, report);
|
|
279
306
|
|
|
280
307
|
let definedVars = null;
|
|
281
308
|
if (tokensPath) {
|
package/src/mcp.mjs
CHANGED
|
@@ -194,6 +194,43 @@ function buildServerInstructions(cliName) {
|
|
|
194
194
|
- \`sh_ui_add_component\` / \`sh_ui_remove_component\` — 설치/삭제
|
|
195
195
|
- \`sh_ui_get_changelog\` — 최근 변경 내역
|
|
196
196
|
|
|
197
|
+
## 진단 — sh-ui doctor (v0.68+)
|
|
198
|
+
|
|
199
|
+
UI 작업이 끝났는데 컴포넌트가 시각적으로 깨졌거나 ("focus outline 안 보임", "hover 색 변화 없음" 류) 사용자가 sh-ui 버전을 올린 직후 / 토큰을 손댄 직후라면 \`npx ${cliName} doctor\` 를 Bash 로 실행해 한 번에 점검:
|
|
200
|
+
- sh-ui.config.json / paths.tokens / paths.cssEntry import / 설치된 컴포넌트의 토큰 의존성 검증
|
|
201
|
+
- 누락 토큰 (silent breakage) 을 fail 로 표시 — 화면이 깨진 이유를 즉시 파악
|
|
202
|
+
- exit 1 이면 사용자에게 어떤 토큰이 누락됐는지 보고 + \`sh-ui tokens upgrade --apply\` 권장
|
|
203
|
+
|
|
204
|
+
## 토큰 마이그레이션 — tokens diff / upgrade (v0.69+)
|
|
205
|
+
|
|
206
|
+
사용자가 sh-ui 버전을 올린 뒤 "새 토큰이 들어왔어?" / "tokens.css 업그레이드해줘" 류 요청 또는 doctor 가 누락 토큰을 신호하면:
|
|
207
|
+
|
|
208
|
+
1. **\`npx ${cliName} tokens diff\`** — 현재 tokens.css vs buildTokens 결과의 added/changed/removed 미리보기. 사용자에게 결과를 보여주고 어떤 모드로 적용할지 확인.
|
|
209
|
+
2. **\`npx ${cliName} tokens upgrade --apply\`** — added 변수만 incremental 추가 (사용자가 손댄 색은 보존). 대부분의 경우 이쪽.
|
|
210
|
+
3. **\`npx ${cliName} tokens upgrade --replace\`** — 사용자가 "표준값으로 리셋" 명시할 때만. 모든 편집이 사라짐.
|
|
211
|
+
|
|
212
|
+
제약: theme.base 가 buildable preset (neutral/zinc/slate) 일 때만 동작. custom (base64) / rich preset (rose/emerald/violet) 은 친절 에러로 종료 — 그땐 \`sh_ui_decode_theme\` → 객체 수정 → \`sh_ui_encode_theme\` round-trip 으로 새 base64 만들어 재스캐폴드.
|
|
213
|
+
|
|
214
|
+
## 테마 추출 — theme extract (v0.70+)
|
|
215
|
+
|
|
216
|
+
사용자가 "지금 색 그대로 다른 앱에 박고 싶어" / "현재 토큰 base64 로 뽑아줘" / "디자인 시스템 문서에 색 스냅샷 저장" 같은 요청에는 \`npx ${cliName} theme extract\` (Bash):
|
|
217
|
+
- 현재 tokens.css 의 light/dark + radius 를 sh-ui base64 로 추출 (stdout, stderr 에 정보 분리)
|
|
218
|
+
- 추출된 base64 를 \`sh_ui_create_project\` 의 \`theme\` 인자에 그대로 넘기면 동일 톤의 새 앱 생성
|
|
219
|
+
- \`sh_ui_encode_theme\` 의 역방향 — 사용자가 tokens.css 를 직접 손댄 결과를 다시 base64 화
|
|
220
|
+
|
|
221
|
+
제약: tokens.css 모든 필수 색이 #RRGGBB 여야 함. color-mix() / var() / rgba() 가 섞이면 친절 에러 — 먼저 \`tokens upgrade --replace\` 로 표준값 hex 화 후 재시도 권장.
|
|
222
|
+
|
|
223
|
+
## CSS 번들 모드 — cssStrategy: bundled (v0.71+)
|
|
224
|
+
|
|
225
|
+
사용자가 "컴포넌트 폴더에 styles.css 너무 많이 떨어진다" / "한 파일로 합치고 싶어" 류 요청 또는 50개+ 컴포넌트 깐 모노레포에서 파일 폭증을 호소하면:
|
|
226
|
+
|
|
227
|
+
1. **\`sh-ui.config.json\` 에 \`cssStrategy: "bundled"\` + \`paths.cssBundle: "src/styles/sh-ui-components.css"\` 추가**.
|
|
228
|
+
2. 사용자가 \`paths.cssBundle\` 을 globals.css 에서 한 번 import (자동화 안 함 — 사용자에게 안내).
|
|
229
|
+
3. 이후 \`sh_ui_add_component\` 가 컴포넌트 styles.css 를 cssBundle 의 \`/* sh-ui:component:NAME-start ... -end */\` 마커 섹션으로 누적. 컴포넌트 .tsx 의 styles.css import 는 자동 제거.
|
|
230
|
+
4. \`sh_ui_remove_component\` 도 .tsx + 번들 섹션을 같이 정리.
|
|
231
|
+
|
|
232
|
+
제약: \`cssFramework: "plain"\` 에서만 동작. tailwind/css-modules/vanilla-extract 는 자체 스코프가 있어 bundled 무시. 기존 per-component 프로젝트에서 bundled 로 마이그레이션은 자동화 없음 — config 추가 후 모든 컴포넌트 \`sh_ui_add_component force=true\` 재실행 안내.
|
|
233
|
+
|
|
197
234
|
### 모노레포 라우팅 (v0.65+)
|
|
198
235
|
|
|
199
236
|
monorepo 에서 \`sh_ui_add_component\` 호출 시:
|
package/src/remove.mjs
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { readFile, rm, rmdir, readdir } from "node:fs/promises";
|
|
1
|
+
import { readFile, rm, rmdir, readdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve, relative } from "node:path";
|
|
4
4
|
import { getRegistryRoot } from "./paths.mjs";
|
|
5
|
+
import { removeSection, isStyleFile, stripStylesImport, isTsxFile } from "./css-bundle.mjs";
|
|
6
|
+
|
|
7
|
+
/** registry file 엔트리가 현재 cssFramework 와 호환되는지 (add.mjs 와 동일 규칙). */
|
|
8
|
+
function frameworkMatches(entry, cssFramework) {
|
|
9
|
+
if (!entry.frameworks) return true;
|
|
10
|
+
return entry.frameworks.includes(cssFramework);
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
function resolveDest(template, config) {
|
|
7
14
|
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (m, key) => {
|
|
@@ -27,13 +34,16 @@ async function loadRegistry(platform) {
|
|
|
27
34
|
return JSON.parse(await readFile(registryPath, "utf8"));
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
37
|
+
/**
|
|
38
|
+
* 설치된 파일이 레지스트리 원본과 동일한지(= 사용자가 수정하지 않았는지).
|
|
39
|
+
* bundled 모드에서 .tsx 는 add 시 `import "./styles.css";` 가 제거된 상태로 저장됐으므로
|
|
40
|
+
* 비교 전에 같은 변환을 src 에 적용해 false positive 회피.
|
|
41
|
+
*/
|
|
42
|
+
async function isUnmodified(srcAbs, destAbs, { transform } = {}) {
|
|
32
43
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
]);
|
|
44
|
+
let src = await readFile(srcAbs, "utf8");
|
|
45
|
+
if (transform) src = transform(src);
|
|
46
|
+
const dest = await readFile(destAbs, "utf8");
|
|
37
47
|
return src === dest;
|
|
38
48
|
} catch {
|
|
39
49
|
return false;
|
|
@@ -71,13 +81,34 @@ export async function remove({ cwd, names, force = false, dryRun = false }) {
|
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
const registryRoot = getRegistryRoot(config.platform);
|
|
84
|
+
const bundled = config.cssStrategy === "bundled";
|
|
85
|
+
const cssFramework = config.cssFramework ?? "plain";
|
|
86
|
+
const seenDest = new Set();
|
|
74
87
|
for (const file of entry.files) {
|
|
88
|
+
if (!frameworkMatches(file, cssFramework)) continue;
|
|
89
|
+
// bundled 모드에선 CSS 파일이 sh-ui-components.css 내 섹션이라 개별 파일이 없음.
|
|
90
|
+
// .tsx 만 일반 삭제 흐름을 타고, 섹션 제거는 별도 단계에서 처리.
|
|
91
|
+
if (bundled && isStyleFile(file)) continue;
|
|
75
92
|
const srcAbs = resolve(registryRoot, file.src);
|
|
76
93
|
const destAbs = resolve(cwd, resolveDest(file.dest, config));
|
|
94
|
+
// 같은 dest 가 여러 file 변종에서 나올 수 있음 (예: index.tsx 와 index.tailwind.tsx
|
|
95
|
+
// 가 동일 dest 로 매핑) — 한 번만 처리.
|
|
96
|
+
if (seenDest.has(destAbs)) continue;
|
|
97
|
+
seenDest.add(destAbs);
|
|
77
98
|
|
|
78
99
|
if (!existsSync(destAbs)) continue;
|
|
79
100
|
|
|
80
|
-
|
|
101
|
+
// dest 는 add 시 substitutePlaceholders + (bundled.tsx 면) stripStylesImport 가
|
|
102
|
+
// 적용된 채로 저장됐다. unmodified 비교는 같은 변환을 src 에 먹여 false positive 회피.
|
|
103
|
+
const transform = (text) => {
|
|
104
|
+
let out = text;
|
|
105
|
+
if (config.aliases?.utils) {
|
|
106
|
+
out = out.replaceAll("@SH_UI_UTILS@", config.aliases.utils);
|
|
107
|
+
}
|
|
108
|
+
if (bundled && isTsxFile(file)) out = stripStylesImport(out);
|
|
109
|
+
return out;
|
|
110
|
+
};
|
|
111
|
+
const unmodified = await isUnmodified(srcAbs, destAbs, { transform });
|
|
81
112
|
if (!unmodified && !force) {
|
|
82
113
|
modifiedBlocked.push({ name, dest: destAbs });
|
|
83
114
|
continue;
|
|
@@ -127,6 +158,24 @@ export async function remove({ cwd, names, force = false, dryRun = false }) {
|
|
|
127
158
|
touchedDirs.add(dirname(d.dest));
|
|
128
159
|
}
|
|
129
160
|
|
|
161
|
+
// bundled 모드 — 각 컴포넌트의 CSS 섹션을 번들에서 제거.
|
|
162
|
+
if (config.cssStrategy === "bundled" && config.paths?.cssBundle) {
|
|
163
|
+
const bundlePath = resolve(cwd, config.paths.cssBundle);
|
|
164
|
+
if (existsSync(bundlePath)) {
|
|
165
|
+
let text = await readFile(bundlePath, "utf8");
|
|
166
|
+
let removedAny = false;
|
|
167
|
+
for (const name of names) {
|
|
168
|
+
const next = removeSection(text, name);
|
|
169
|
+
if (next !== text) {
|
|
170
|
+
text = next;
|
|
171
|
+
removedAny = true;
|
|
172
|
+
console.log(`✓ 번들 섹션 제거: ${name} → ${relative(cwd, bundlePath)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (removedAny) await writeFile(bundlePath, text, "utf8");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
130
179
|
// 컴포넌트 폴더가 비면 정리 (paths.components 상위까지)
|
|
131
180
|
const componentsRoot = config.paths?.components
|
|
132
181
|
? resolve(cwd, config.paths.components)
|