sh-ui-cli 0.70.0 → 0.71.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/data/changelog/versions.json +15 -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/remove.mjs +57 -8
|
@@ -2,6 +2,21 @@
|
|
|
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.0",
|
|
7
|
+
"date": "2026-05-09",
|
|
8
|
+
"title": "cssStrategy: bundled — 단일 CSS 번들 모드 (Phase D)",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`cssStrategy: \"bundled\"` 옵션 신설** — `sh-ui.config.json` 에서 켜면 모든 컴포넌트 CSS 가 `paths.cssBundle` 에 지정된 단일 파일로 누적. 컴포넌트별 `styles.css` 파일이 컴포넌트 폴더에 떨어지지 않음. 기본값은 `per-component` (기존 동작) — 옵트인.",
|
|
12
|
+
"**마커 기반 섹션 — `/* sh-ui:component:NAME-start ... -end */`** — sh-ui 가 마커 사이만 관리. 사용자 custom CSS 를 마커 밖에 추가해도 add/remove 가 그대로 보존. 같은 컴포넌트 재 add 면 안 내용만 교체.",
|
|
13
|
+
"**add 동작** — bundled 모드에서 컴포넌트 .tsx 의 `import \"./styles.css\";` 라인을 자동 제거 (번들이 한 번 import 되므로 불필요). conflict resolver 우회 (마커 사이는 sh-ui 영역).",
|
|
14
|
+
"**remove 동작** — .tsx 삭제 + 번들에서 해당 섹션 제거. 사용자 수정 검사도 stripStylesImport 변환 적용해 false positive 회피. 같은 dest 의 변종 중복 처리 fix.",
|
|
15
|
+
"**doctor 통합** — bundled 모드일 때 `paths.cssBundle` 존재 검증. 미설정/누락 시 fail.",
|
|
16
|
+
"**제약** — `cssFramework: \"plain\"` 에서만 동작 (tailwind/css-modules/vanilla-extract 는 자체 스코프 메커니즘). 사용자가 번들 파일을 globals.css 같은 곳에서 한 번 import 해야 스타일 적용 — 자동화 안 함."
|
|
17
|
+
],
|
|
18
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.71.0"
|
|
19
|
+
},
|
|
5
20
|
{
|
|
6
21
|
"version": "0.70.0",
|
|
7
22
|
"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/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)
|