sh-ui-cli 0.109.1 → 0.111.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 +25 -0
- package/data/tokens/build.mjs +49 -14
- package/package.json +1 -1
- package/sh-ui.schema.json +10 -0
- package/src/create/cli-args.js +3 -1
- package/src/create/describeTemplate.js +9 -7
- package/src/create/generator.js +73 -21
- package/src/create/index.mjs +6 -0
- package/src/create/templateManifest.js +1 -1
- package/src/create/theme/inject.js +32 -7
- package/src/create/theme/resolveTheme.js +95 -0
- package/src/mcp.mjs +21 -3
- package/src/tokens-cmd.mjs +26 -8
- package/templates/monorepo/npmrc +5 -0
|
@@ -2,6 +2,31 @@
|
|
|
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.111.0",
|
|
7
|
+
"date": "2026-05-22",
|
|
8
|
+
"title": "theme — extraTokens 패스스루 + 모노레포 공유 theme",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`theme.extraTokens` 신규** — `sh-ui.config.json` 의 `theme.extraTokens: { root, light, dark }` 에 임의 토큰(`accent-soft`·`surface`·`cta-*`·`border-focus` 등)을 정의하면 tokens.css 와 `@theme inline` 양쪽에 자동 emit. TOKEN_KEYS/OPTIONAL_TOKEN_KEYS 의 고정 스키마를 벗어나는 디자인 시스템 확장을 config-driven 으로 — tokens.css 손-편집 후 `tokens upgrade`/재스캐폴드 시 날아가던 클로버 문제 해결. `root` 는 모드 무관 고정(CTA 같은 light/dark 공통), `light`/`dark` 는 모드별.",
|
|
12
|
+
"**모노레포 공유 theme** — `packages/ui/ui-core/sh-ui.config.json` 에 `theme` 블록을 두면 sibling `ui-apps/ui-*` 들이 묵시적 상속. ui-app 측 `theme` 가 있으면 deep-merge (app 키가 core 키 override). 같은 디자인 시스템을 쓰는 다중 앱이 토큰을 중복 정의하지 않음. `add_app` 가 ui-core 의 공유 theme 을 감지하면 새 ui-app 의 `theme` 블록을 자동으로 비워서 상속하게 만든다.",
|
|
13
|
+
"**`tokens upgrade` 가 공유 theme 반영** — `sh-ui tokens upgrade` 가 sibling ui-core 의 theme 을 머지한 effective config 로 buildTokens 호출 → ui-app 단독에 있는 토큰만 갖고 빌드하던 종전 동작 보강.",
|
|
14
|
+
"**Tailwind 유틸 자동 매핑** — `buildTokensCss` 의 `@theme inline` 블록이 extraTokens 키마다 `--color-<key>: var(--<key>)` 를 함께 emit → `bg-cta-bg`/`text-accent-soft-fg` 같은 Tailwind utility 가 별도 손-매핑 없이 즉시 사용 가능."
|
|
15
|
+
],
|
|
16
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.111.0"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"version": "0.110.0",
|
|
20
|
+
"date": "2026-05-22",
|
|
21
|
+
"title": "create — inPlace 비파괴 머지 + .npmrc 템플릿 parity",
|
|
22
|
+
"type": "minor",
|
|
23
|
+
"highlights": [
|
|
24
|
+
"**`inPlace` 옵션 신규** — `sh_ui_create_project` 의 `inPlace` 인자 / `sh-ui create --in-place`. 기존 디렉토리를 삭제하지 않고 비파괴 머지 — 이미 있는 파일은 보존하고 없는 파일만 채운다. 커스터마이즈된 monorepo 루트(`CLAUDE.md`·`.gitignore` 등)와 `.git` 을 지키며 `apps/`·`packages/` 를 재생성할 때 사용. tmpdir 에 스캐폴드 후 `overwrite:false` 머지로 구현 — 전체 generator 파이프라인은 그대로, 마지막 머지만 비파괴.",
|
|
25
|
+
"**`.npmrc` 템플릿 parity 수정** — npm publish 가 패키지 tarball 에서 `.npmrc` 를 항상 strip(레지스트리 토큰 유출 방지)해, published CLI 의 monorepo 스캐폴드에 `.npmrc` 가 누락되던 버그. `describe_template` 은 정적 매니페스트 기준이라 계속 `.npmrc` 를 보고 → dry-run ↔ 실제 산출물 불일치. `gitignore` 와 동일 패턴으로 템플릿을 `npmrc`(점 없음)로 두고 `finalizeProject` 가 `.npmrc` 로 복원하도록 수정 (`STRIPPED_DOTFILES` 맵으로 일반화).",
|
|
26
|
+
"**CLI `--git-init`/`--locale` 전달 버그 수정** — 두 플래그를 파싱·검증만 하고 `createProject` 로 넘기지 않아 무시되던 문제. `--in-place` 와 함께 연결."
|
|
27
|
+
],
|
|
28
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.110.0"
|
|
29
|
+
},
|
|
5
30
|
{
|
|
6
31
|
"version": "0.109.1",
|
|
7
32
|
"date": "2026-05-22",
|
package/data/tokens/build.mjs
CHANGED
|
@@ -85,13 +85,22 @@ function toCssValue(entry) {
|
|
|
85
85
|
return entry.value;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function emitCssBlock(selector, entries) {
|
|
89
|
-
const
|
|
90
|
-
.map(([p, entry]) => ` ${toCssVar(p)}: ${toCssValue(entry)};`)
|
|
91
|
-
|
|
88
|
+
function emitCssBlock(selector, entries, extraLines = []) {
|
|
89
|
+
const baseLines = Object.entries(entries)
|
|
90
|
+
.map(([p, entry]) => ` ${toCssVar(p)}: ${toCssValue(entry)};`);
|
|
91
|
+
const lines = [...baseLines, ...extraLines].join("\n");
|
|
92
92
|
return `${selector} {\n${lines}\n}`;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/** extraTokens 카테고리(plain {key: value}) → CSS 라인 배열. key 가 '--' 로 시작하면 그대로. */
|
|
96
|
+
function extraTokensToLines(map) {
|
|
97
|
+
if (!map || typeof map !== "object") return [];
|
|
98
|
+
return Object.entries(map).map(([rawKey, value]) => {
|
|
99
|
+
const key = rawKey.startsWith("--") ? rawKey.slice(2) : rawKey;
|
|
100
|
+
return ` --${key}: ${value};`;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
/**
|
|
96
105
|
* `prefers-color-scheme: dark` 자동 적용 블록.
|
|
97
106
|
*
|
|
@@ -105,10 +114,12 @@ function emitCssBlock(selector, entries) {
|
|
|
105
114
|
* .light 클래스 → :root 만 적용 (강제 라이트, OS 무관)
|
|
106
115
|
* .dark 클래스 → .dark 블록이 마지막에 와서 항상 승리 (강제 다크)
|
|
107
116
|
*/
|
|
108
|
-
function emitAutoDarkBlock(darkEntries) {
|
|
109
|
-
const
|
|
110
|
-
.map(([p, entry]) => ` ${toCssVar(p)}: ${toCssValue(entry)};`)
|
|
111
|
-
|
|
117
|
+
function emitAutoDarkBlock(darkEntries, extraLines = []) {
|
|
118
|
+
const baseLines = Object.entries(darkEntries)
|
|
119
|
+
.map(([p, entry]) => ` ${toCssVar(p)}: ${toCssValue(entry)};`);
|
|
120
|
+
// 미디어쿼리 안쪽은 한 단계 더 들여쓰기 — extraTokens 라인도 같은 들여쓰기 적용.
|
|
121
|
+
const indentedExtras = extraLines.map((line) => ` ${line}`);
|
|
122
|
+
const lines = [...baseLines, ...indentedExtras].join("\n");
|
|
112
123
|
return `@media (prefers-color-scheme: dark) {\n :root:not(.light):not(.dark) {\n${lines}\n }\n}`;
|
|
113
124
|
}
|
|
114
125
|
|
|
@@ -131,13 +142,28 @@ function mergeThemeIndependent(tokens) {
|
|
|
131
142
|
* radius 는 단일 토큰 (--radius) 이지만 Tailwind 의 rounded-{sm,md,lg,xl}
|
|
132
143
|
* 4 단계로 expand. 템플릿이 손으로 박아두던 패턴을 단일 소스로 옮긴 것.
|
|
133
144
|
*/
|
|
134
|
-
function buildTailwindThemeBlock(lightTokens) {
|
|
145
|
+
function buildTailwindThemeBlock(lightTokens, extraTokens) {
|
|
135
146
|
const lines = [];
|
|
136
147
|
for (const path of Object.keys(lightTokens)) {
|
|
137
148
|
const cssVar = toCssVar(path);
|
|
138
149
|
const themeKey = cssVar.replace(/^--/, "--color-");
|
|
139
150
|
lines.push(` ${themeKey}: var(${cssVar});`);
|
|
140
151
|
}
|
|
152
|
+
// extraTokens (v0.111.0+) → Tailwind utility 자동 생성용 매핑.
|
|
153
|
+
// root/light/dark 의 모든 키 합집합 — 각각 한 번씩만 --color-* 매핑 (var 가 모드별로 해석되므로).
|
|
154
|
+
if (extraTokens && typeof extraTokens === "object") {
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
for (const cat of ["root", "light", "dark"]) {
|
|
157
|
+
const map = extraTokens[cat];
|
|
158
|
+
if (!map || typeof map !== "object") continue;
|
|
159
|
+
for (const rawKey of Object.keys(map)) {
|
|
160
|
+
const key = rawKey.startsWith("--") ? rawKey.slice(2) : rawKey;
|
|
161
|
+
if (seen.has(key)) continue;
|
|
162
|
+
seen.add(key);
|
|
163
|
+
lines.push(` --color-${key}: var(--${key});`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
141
167
|
lines.push(` --radius-sm: calc(var(--radius) - 2px);`);
|
|
142
168
|
lines.push(` --radius-md: var(--radius);`);
|
|
143
169
|
lines.push(` --radius-lg: calc(var(--radius) + 2px);`);
|
|
@@ -149,18 +175,27 @@ export async function buildTokensCss(config) {
|
|
|
149
175
|
const tokens = await resolveTokens(config);
|
|
150
176
|
const mode = config.theme.mode;
|
|
151
177
|
const themeIndep = mergeThemeIndependent(tokens);
|
|
178
|
+
|
|
179
|
+
// extraTokens (v0.111.0+) — TOKEN_KEYS/OPTIONAL_TOKEN_KEYS 에 없는 자유 추가 토큰.
|
|
180
|
+
// root: 모드 무관 — light :root 에 emit (dark 블록이 덮어쓰지 않아 자동 cascade).
|
|
181
|
+
// light: light :root 에. dark: dark 블록 양쪽 (@media + .dark) 에.
|
|
182
|
+
const extra = config.theme?.extraTokens ?? {};
|
|
183
|
+
const extraRoot = extraTokensToLines(extra.root);
|
|
184
|
+
const extraLight = extraTokensToLines(extra.light);
|
|
185
|
+
const extraDark = extraTokensToLines(extra.dark);
|
|
186
|
+
|
|
152
187
|
const blocks = [];
|
|
153
188
|
if (mode === "light" || mode === "light-dark") {
|
|
154
|
-
blocks.push(emitCssBlock(":root", { ...tokens.light, ...themeIndep }));
|
|
189
|
+
blocks.push(emitCssBlock(":root", { ...tokens.light, ...themeIndep }, [...extraLight, ...extraRoot]));
|
|
155
190
|
}
|
|
156
191
|
if (mode === "dark") {
|
|
157
|
-
blocks.push(emitCssBlock(":root", { ...tokens.dark, ...themeIndep }));
|
|
192
|
+
blocks.push(emitCssBlock(":root", { ...tokens.dark, ...themeIndep }, [...extraDark, ...extraRoot]));
|
|
158
193
|
}
|
|
159
194
|
if (mode === "light-dark") {
|
|
160
|
-
blocks.push(emitAutoDarkBlock(tokens.dark));
|
|
161
|
-
blocks.push(emitCssBlock(".dark", tokens.dark));
|
|
195
|
+
blocks.push(emitAutoDarkBlock(tokens.dark, extraDark));
|
|
196
|
+
blocks.push(emitCssBlock(".dark", tokens.dark, extraDark));
|
|
162
197
|
}
|
|
163
|
-
blocks.push(buildTailwindThemeBlock(tokens.light));
|
|
198
|
+
blocks.push(buildTailwindThemeBlock(tokens.light, extra));
|
|
164
199
|
const header = `/* Generated by @sh-ui/tokens — do not edit directly */\n/* base=${config.theme.base} radius=${config.theme.radius} mode=${config.theme.mode} */\n`;
|
|
165
200
|
return header + "\n" + blocks.join("\n\n") + "\n";
|
|
166
201
|
}
|
package/package.json
CHANGED
package/sh-ui.schema.json
CHANGED
|
@@ -46,6 +46,16 @@
|
|
|
46
46
|
"mode": {
|
|
47
47
|
"enum": ["light", "dark", "light-dark"],
|
|
48
48
|
"description": "다크 모드 전략. light-dark = OS 자동 + .light/.dark 클래스 토글, light/dark = 강제 단일 모드."
|
|
49
|
+
},
|
|
50
|
+
"extraTokens": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"description": "v0.111.0+ 신규. 디자인 시스템 자유 추가 토큰 — TOKEN_KEYS/OPTIONAL_TOKEN_KEYS 에 없는 임의 키(`accent-soft`, `surface`, `cta-bg` 등) 를 tokens.css 와 globals.css `@theme` 에 자동 emit. root=모드 무관 고정, light/dark=모드별. 모노레포에서 ui-core 의 theme 에 두면 모든 ui-app 이 상속 (app 측 override 시 deep-merge).",
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"properties": {
|
|
55
|
+
"root": { "type": "object", "additionalProperties": { "type": "string" }, "description": "모드 무관 고정 토큰 (예: CTA 밴드 색 — light/dark 동일)." },
|
|
56
|
+
"light": { "type": "object", "additionalProperties": { "type": "string" }, "description": "라이트 모드 토큰." },
|
|
57
|
+
"dark": { "type": "object", "additionalProperties": { "type": "string" }, "description": "다크 모드 토큰." }
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
},
|
package/src/create/cli-args.js
CHANGED
|
@@ -18,7 +18,7 @@ const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css',
|
|
|
18
18
|
// `locale` (단수) 은 한국어/일본어 같은 사용자 지역 디폴트 가정 (폰트 등) 을 활성화하는
|
|
19
19
|
// 옵션. `locales` (복수) 와 다르다: locales 는 i18n 활성화 시 생성할 locale 코드 목록.
|
|
20
20
|
const VALID_LOCALES = ['default', 'ko'];
|
|
21
|
-
const BOOL_FLAGS = ['yes', 'help', 'dry-run', 'no-git-init', 'git-init'];
|
|
21
|
+
const BOOL_FLAGS = ['yes', 'help', 'dry-run', 'no-git-init', 'git-init', 'in-place'];
|
|
22
22
|
|
|
23
23
|
const SUBCOMMANDS = ['add-app', 'add-component'];
|
|
24
24
|
|
|
@@ -45,9 +45,11 @@ export const parseArgs = (argv) => {
|
|
|
45
45
|
const name = arg.slice(2);
|
|
46
46
|
if (BOOL_FLAGS.includes(name)) {
|
|
47
47
|
// dry-run → dryRun. --no-git-init → gitInit:false. --git-init → gitInit:true.
|
|
48
|
+
// --in-place → inPlace:true (기존 디렉토리 비파괴 머지).
|
|
48
49
|
if (name === 'dry-run') flags.dryRun = true;
|
|
49
50
|
else if (name === 'no-git-init') flags.gitInit = false;
|
|
50
51
|
else if (name === 'git-init') flags.gitInit = true;
|
|
52
|
+
else if (name === 'in-place') flags.inPlace = true;
|
|
51
53
|
else flags[name] = true;
|
|
52
54
|
continue;
|
|
53
55
|
}
|
|
@@ -375,18 +375,20 @@ function finalize(groups) {
|
|
|
375
375
|
* generator.js 의 `finalizeProject` 가 실제 fs 단계에서 적용하는 rename 을
|
|
376
376
|
* file plan 텍스트 레벨에서 mock-apply (v0.96.0+ — 피드백 #3 buglet).
|
|
377
377
|
*
|
|
378
|
-
* 현재
|
|
379
|
-
* -
|
|
380
|
-
*
|
|
381
|
-
* - 이미
|
|
378
|
+
* 현재 규칙 — npm publish 가 strip 하는 dotfile 의 점 없는 템플릿 이름을 복원:
|
|
379
|
+
* - 'gitignore' → '.gitignore' (npm 이 strip, generator 의 STRIPPED_DOTFILES 와 일치)
|
|
380
|
+
* - 'npmrc' → '.npmrc' (npm 이 publish 시 항상 strip)
|
|
381
|
+
* - 이미 점 붙은 경로는 그대로.
|
|
382
382
|
*
|
|
383
|
-
* 미래에 다른 fs-level rename 이 추가되면
|
|
384
|
-
* file-plan ↔ create_project
|
|
383
|
+
* 미래에 다른 fs-level rename 이 추가되면 generator.js 의 STRIPPED_DOTFILES 와
|
|
384
|
+
* 함께 여기에 등록 (describeTemplate 의 file-plan ↔ create_project 실제 emit 1:1 정합성).
|
|
385
385
|
*/
|
|
386
|
+
const STRIPPED_DOTFILES = { gitignore: '.gitignore', npmrc: '.npmrc' };
|
|
387
|
+
|
|
386
388
|
function applyFinalizeRenames(p) {
|
|
387
389
|
const slash = p.lastIndexOf('/');
|
|
388
390
|
const dir = slash === -1 ? '' : p.slice(0, slash + 1);
|
|
389
391
|
const base = slash === -1 ? p : p.slice(slash + 1);
|
|
390
|
-
if (base
|
|
392
|
+
if (STRIPPED_DOTFILES[base]) return dir + STRIPPED_DOTFILES[base];
|
|
391
393
|
return p;
|
|
392
394
|
}
|
package/src/create/generator.js
CHANGED
|
@@ -290,19 +290,28 @@ export async function createProject(options = {}) {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
//
|
|
294
|
-
//
|
|
293
|
+
// inPlace — 이미 커스터마이즈된 디렉토리(루트 docs·git 보존)에 비파괴 머지.
|
|
294
|
+
// generation 은 dry-run 과 동일하게 tmpdir 에서 돌린 뒤, 이미 있는 파일은
|
|
295
|
+
// 건드리지 않고 새 파일만 realTargetDir 로 복사한다.
|
|
296
|
+
const inPlace = options.inPlace === true && !options.dryRun;
|
|
297
|
+
const realTargetDir = path.resolve(process.cwd(), projectName);
|
|
298
|
+
|
|
299
|
+
// dry-run / inPlace 는 tmpdir 에 생성. dry-run 은 목록 출력 후 정리,
|
|
300
|
+
// inPlace 는 realTargetDir 로 비파괴 머지. 일반 모드는 cwd/name 에 직접 생성.
|
|
295
301
|
const targetDir = options.dryRun
|
|
296
302
|
? await fs.mkdtemp(path.join(os.tmpdir(), 'sh-ui-dry-'))
|
|
297
|
-
:
|
|
303
|
+
: inPlace
|
|
304
|
+
? await fs.mkdtemp(path.join(os.tmpdir(), 'sh-ui-inplace-'))
|
|
305
|
+
: realTargetDir;
|
|
298
306
|
|
|
299
|
-
// 방어 가드 — projectName 검증을 이미 통과했어도
|
|
300
|
-
// dry-run
|
|
307
|
+
// 방어 가드 — projectName 검증을 이미 통과했어도 fs 쓰기 직전에 한 번 더 확인.
|
|
308
|
+
// dry-run / inPlace 의 targetDir 는 tmpdir 이므로 실제 목적지를 검증한다.
|
|
301
309
|
if (!options.dryRun) {
|
|
302
|
-
assertWithin(process.cwd(),
|
|
310
|
+
assertWithin(process.cwd(), realTargetDir);
|
|
303
311
|
}
|
|
304
312
|
|
|
305
|
-
|
|
313
|
+
// 일반 모드만 기존 디렉토리 덮어쓰기 확인. inPlace 는 비파괴라 remove 하지 않는다.
|
|
314
|
+
if (!options.dryRun && !inPlace && await fs.pathExists(targetDir)) {
|
|
306
315
|
if (options.yes) {
|
|
307
316
|
await fs.remove(targetDir);
|
|
308
317
|
} else {
|
|
@@ -318,9 +327,29 @@ export async function createProject(options = {}) {
|
|
|
318
327
|
}
|
|
319
328
|
}
|
|
320
329
|
|
|
330
|
+
// finalizeProject + (inPlace 면) temp → realTargetDir 비파괴 머지.
|
|
331
|
+
// 세 플랫폼 경로(flutter/vite/next)가 공통으로 호출한다.
|
|
332
|
+
async function finalizeAndCommit() {
|
|
333
|
+
await finalizeProject(targetDir, {
|
|
334
|
+
dryRun: options.dryRun,
|
|
335
|
+
// inPlace 는 temp 에서 git init 해봐야 버려지므로 스킵 — realTargetDir 의
|
|
336
|
+
// 기존 .git 을 그대로 둔다 (gitignore/npmrc 복원은 finalize 안에서 수행됨).
|
|
337
|
+
gitInit: inPlace ? false : options.gitInit,
|
|
338
|
+
locale: options.locale,
|
|
339
|
+
});
|
|
340
|
+
if (inPlace) {
|
|
341
|
+
await fs.ensureDir(realTargetDir);
|
|
342
|
+
await fs.copy(targetDir, realTargetDir, { overwrite: false, errorOnExist: false });
|
|
343
|
+
await fs.remove(targetDir);
|
|
344
|
+
console.log(
|
|
345
|
+
`\n ℹ inPlace — 기존 디렉토리에 비파괴 머지 완료 (이미 있는 파일은 보존).`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
321
350
|
if (platform === 'flutter') {
|
|
322
351
|
await generateFlutter(targetDir, projectName, theme, cssFramework, themeBase);
|
|
323
|
-
await
|
|
352
|
+
await finalizeAndCommit();
|
|
324
353
|
console.log(`\n✅ ${projectName} Flutter 프로젝트가 생성되었습니다!`);
|
|
325
354
|
console.log(`\n cd ${projectName}`);
|
|
326
355
|
console.log(' flutter pub get');
|
|
@@ -355,7 +384,7 @@ export async function createProject(options = {}) {
|
|
|
355
384
|
});
|
|
356
385
|
}
|
|
357
386
|
|
|
358
|
-
await
|
|
387
|
+
await finalizeAndCommit();
|
|
359
388
|
|
|
360
389
|
if (options.dryRun) {
|
|
361
390
|
const files = await listAllFiles(targetDir);
|
|
@@ -401,7 +430,7 @@ export async function createProject(options = {}) {
|
|
|
401
430
|
});
|
|
402
431
|
}
|
|
403
432
|
|
|
404
|
-
await
|
|
433
|
+
await finalizeAndCommit();
|
|
405
434
|
|
|
406
435
|
if (options.dryRun) {
|
|
407
436
|
const files = await listAllFiles(targetDir);
|
|
@@ -575,6 +604,23 @@ export async function addApp(options = {}) {
|
|
|
575
604
|
}
|
|
576
605
|
await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css, themeBase);
|
|
577
606
|
|
|
607
|
+
// v0.111.0+ — ui-core 가 공유 theme 을 호스팅하고 사용자가 --theme 미지정 시,
|
|
608
|
+
// 새 ui-app 의 theme 블록을 제거해 ui-core 로부터 묵시적 상속. 같은 디자인 시스템을
|
|
609
|
+
// 쓰는 모노레포 다중 앱이 토큰을 중복 정의하지 않도록.
|
|
610
|
+
if (!options.theme) {
|
|
611
|
+
const uiAppConfigPath = path.join(uiAppDir, 'sh-ui.config.json');
|
|
612
|
+
const uiCoreConfigPath = path.resolve(cwd, 'packages', 'ui', 'ui-core', 'sh-ui.config.json');
|
|
613
|
+
if (await fs.pathExists(uiCoreConfigPath)) {
|
|
614
|
+
const coreCfg = await fs.readJson(uiCoreConfigPath);
|
|
615
|
+
if (coreCfg.theme && (coreCfg.theme.extraTokens || coreCfg.theme.base || coreCfg.theme.radius || coreCfg.theme.mode)) {
|
|
616
|
+
const appCfg = await fs.readJson(uiAppConfigPath);
|
|
617
|
+
delete appCfg.theme;
|
|
618
|
+
await fs.writeJson(uiAppConfigPath, appCfg, { spaces: 2 });
|
|
619
|
+
console.log(`\n ℹ ui-core 가 공유 theme 을 호스팅 — 새 ui-app 은 theme 블록을 두지 않고 상속.`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
578
624
|
console.log(`\n✅ apps/${appName} 이 추가되었습니다!`);
|
|
579
625
|
console.log('\n pnpm install');
|
|
580
626
|
console.log(` pnpm --filter ${appName} dev\n`);
|
|
@@ -1644,11 +1690,17 @@ async function stripTailwindFromPrettier(prettierPath) {
|
|
|
1644
1690
|
}
|
|
1645
1691
|
|
|
1646
1692
|
/**
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
* strip
|
|
1651
|
-
* 템플릿엔 `gitignore`
|
|
1693
|
+
* npm publish 가 패키지 tarball 에서 strip/변형하는 dotfile 들 — 템플릿엔 점 없는
|
|
1694
|
+
* 이름으로 두고 스캐폴드 직후 점을 복원한다.
|
|
1695
|
+
* - `.gitignore` → npm 이 strip (없으면 `.npmignore` fallback 으로 사용)
|
|
1696
|
+
* - `.npmrc` → npm 이 항상 strip (publish 시 레지스트리 토큰 유출 방지)
|
|
1697
|
+
* 둘 다 published CLI 엔 도착하지 않으므로 템플릿엔 `gitignore` / `npmrc` 로 둔다.
|
|
1698
|
+
*/
|
|
1699
|
+
const STRIPPED_DOTFILES = { gitignore: '.gitignore', npmrc: '.npmrc' };
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* 스캐폴드 마무리 — strip 된 dotfile(`gitignore`/`npmrc`) 을 점 붙은 이름으로
|
|
1703
|
+
* 되돌리고 `git init` 실행.
|
|
1652
1704
|
*
|
|
1653
1705
|
* gitInit 옵션:
|
|
1654
1706
|
* - undefined (auto): parent 가 이미 git tree 안이면 스킵, 아니면 init. nested .git
|
|
@@ -1659,9 +1711,9 @@ async function stripTailwindFromPrettier(prettierPath) {
|
|
|
1659
1711
|
* git init 은 dry-run 에서는 스킵하고, 실패해도(git 미설치 등) 조용히 넘어간다.
|
|
1660
1712
|
*/
|
|
1661
1713
|
async function finalizeProject(targetDir, { dryRun = false, gitInit, locale } = {}) {
|
|
1662
|
-
// 모노레포 / sub-app 까지
|
|
1714
|
+
// 모노레포 / sub-app 까지 strip 된 dotfile(gitignore/npmrc) 을 점 붙은 이름으로 복원.
|
|
1663
1715
|
// root 만 처리하면 apps/<name>/gitignore 가 그대로 남아 node_modules/dist 가 staged 된다 (v0.93.0 버그).
|
|
1664
|
-
await
|
|
1716
|
+
await restoreStrippedDotfilesRecursive(targetDir);
|
|
1665
1717
|
|
|
1666
1718
|
// locale 후처리 — 한국어면 globals.css 들에 Pretendard 자동 적용 (Aifice 피드백 3.1).
|
|
1667
1719
|
// dryRun 이면 skip — globals.css 가 디스크에 안 써졌을 수 있다.
|
|
@@ -1790,7 +1842,7 @@ function describeAppPlatform(platform) {
|
|
|
1790
1842
|
return 'Next.js 앱 (App Router + Server Components). 라우트 + 비즈니스 로직.';
|
|
1791
1843
|
}
|
|
1792
1844
|
|
|
1793
|
-
async function
|
|
1845
|
+
async function restoreStrippedDotfilesRecursive(dir) {
|
|
1794
1846
|
let entries;
|
|
1795
1847
|
try {
|
|
1796
1848
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -1802,9 +1854,9 @@ async function renameAllGitignoreRecursive(dir) {
|
|
|
1802
1854
|
if (entry.isDirectory()) {
|
|
1803
1855
|
// 스캐폴드 직후엔 node_modules / .git 가 없지만 방어적으로.
|
|
1804
1856
|
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
1805
|
-
await
|
|
1806
|
-
} else if (entry.name
|
|
1807
|
-
await fs.move(fullPath, path.join(dir,
|
|
1857
|
+
await restoreStrippedDotfilesRecursive(fullPath);
|
|
1858
|
+
} else if (STRIPPED_DOTFILES[entry.name]) {
|
|
1859
|
+
await fs.move(fullPath, path.join(dir, STRIPPED_DOTFILES[entry.name]), { overwrite: true });
|
|
1808
1860
|
}
|
|
1809
1861
|
}
|
|
1810
1862
|
}
|
package/src/create/index.mjs
CHANGED
|
@@ -32,6 +32,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
|
|
|
32
32
|
--locales <ko,en> i18n 활성화 시 생성할 locale 코드 (comma-separated). 기본 'ko,en'
|
|
33
33
|
--locale <default|ko> 사용자 지역 디폴트 가정 — 'ko' 선택 시 globals.css 에 Pretendard 자동 적용 (v0.103.0+)
|
|
34
34
|
--yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
|
|
35
|
+
--in-place 기존 디렉토리에 비파괴 머지 — 이미 있는 파일은 보존, 없는 파일만 채움. 커스텀 루트 docs·.git 보존하며 재생성 (디렉토리 삭제 안 함)
|
|
35
36
|
--dry-run 파일을 쓰지 않고 작성될 파일 목록만 출력
|
|
36
37
|
--no-git-init git init 스킵 (기존 git tree 안에서 호출 시 nested .git 충돌 방지). 기본 auto — parent 가 git tree 안이면 자동 스킵
|
|
37
38
|
--git-init git init 무조건 실행 (nested 가 의도된 경우)
|
|
@@ -105,6 +106,11 @@ export async function runCreate(rest) {
|
|
|
105
106
|
port: flags.port,
|
|
106
107
|
yes: flags.yes,
|
|
107
108
|
dryRun: flags.dryRun,
|
|
109
|
+
// gitInit / locale / inPlace — 이전엔 파싱만 하고 createProject 로
|
|
110
|
+
// 전달하지 않아 무시되던 버그. v0.110.0 에서 함께 연결.
|
|
111
|
+
gitInit: flags.gitInit,
|
|
112
|
+
locale: flags.locale,
|
|
113
|
+
inPlace: flags.inPlace,
|
|
108
114
|
});
|
|
109
115
|
}
|
|
110
116
|
}
|
|
@@ -17,11 +17,11 @@ export const TEMPLATE_MANIFEST = {
|
|
|
17
17
|
"monorepo": {
|
|
18
18
|
"base": [
|
|
19
19
|
".dockerignore",
|
|
20
|
-
".npmrc",
|
|
21
20
|
".prettierrc",
|
|
22
21
|
"CLAUDE.md",
|
|
23
22
|
"README.md",
|
|
24
23
|
"gitignore",
|
|
24
|
+
"npmrc",
|
|
25
25
|
"package.json",
|
|
26
26
|
"packages/eslint-config/base.js",
|
|
27
27
|
"packages/eslint-config/flat.js",
|
|
@@ -37,15 +37,31 @@ export const buildCssColorsBlock = (theme) => {
|
|
|
37
37
|
);
|
|
38
38
|
const allKeys = [...TOKEN_KEYS, ...optionalKeys];
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// extraTokens (v0.111.0+) — TOKEN_KEYS/OPTIONAL_TOKEN_KEYS 에 없는 자유 추가 토큰.
|
|
41
|
+
// root: 모드 무관 고정 — :root 에만 emit (dark 블록에 덮어쓰지 않아 자동 cascade).
|
|
42
|
+
// light: :root 에 emit (light 가 :root 기본). dark: dark 블록 두 곳에 emit.
|
|
43
|
+
const extra = theme.extraTokens ?? {};
|
|
44
|
+
const extraRootLines = entriesToCssLines(extra.root);
|
|
45
|
+
const extraLightLines = entriesToCssLines(extra.light);
|
|
46
|
+
const extraDarkLines = entriesToCssLines(extra.dark);
|
|
47
|
+
|
|
48
|
+
const lightSection = [
|
|
49
|
+
...allKeys.map((k) => cssColorLine(k, theme.light[k])),
|
|
50
|
+
...extraLightLines,
|
|
51
|
+
...extraRootLines,
|
|
52
|
+
].join('\n');
|
|
53
|
+
|
|
54
|
+
const darkLines = [
|
|
55
|
+
...allKeys.map((k) => cssColorLine(k, theme.dark[k])),
|
|
56
|
+
...extraDarkLines,
|
|
57
|
+
];
|
|
42
58
|
// 미디어쿼리 안의 다크 라인은 한 단계 더 들여쓰기 (`:root:not(...)` 안쪽).
|
|
43
|
-
const darkLinesIndented =
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
const darkLinesIndented = darkLines.map((line) => ` ${line}`).join('\n');
|
|
60
|
+
const darkSection = darkLines.join('\n');
|
|
61
|
+
|
|
46
62
|
return [
|
|
47
63
|
':root {',
|
|
48
|
-
|
|
64
|
+
lightSection,
|
|
49
65
|
'}',
|
|
50
66
|
'@media (prefers-color-scheme: dark) {',
|
|
51
67
|
' :root:not(.light):not(.dark) {',
|
|
@@ -53,11 +69,20 @@ export const buildCssColorsBlock = (theme) => {
|
|
|
53
69
|
' }',
|
|
54
70
|
'}',
|
|
55
71
|
'.dark {',
|
|
56
|
-
|
|
72
|
+
darkSection,
|
|
57
73
|
'}',
|
|
58
74
|
].join('\n');
|
|
59
75
|
};
|
|
60
76
|
|
|
77
|
+
/** extraTokens 카테고리(plain object) → CSS 라인 배열. key 가 '--' 로 시작하면 그대로, 아니면 prepend. */
|
|
78
|
+
function entriesToCssLines(map) {
|
|
79
|
+
if (!map || typeof map !== 'object') return [];
|
|
80
|
+
return Object.entries(map).map(([rawKey, value]) => {
|
|
81
|
+
const key = rawKey.startsWith('--') ? rawKey.slice(2) : rawKey;
|
|
82
|
+
return cssColorLine(key, value);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
export const buildCssRadiusBlock = (theme) => {
|
|
62
87
|
return ` --radius: ${theme.radius}rem;`;
|
|
63
88
|
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// resolveTheme — ui-core/sh-ui.config.json 의 공유 theme 과 ui-app/sh-ui.config.json 의
|
|
2
|
+
// app theme 을 머지해 effective theme 반환. v0.111.0+ 의 shared-theme 메커니즘.
|
|
3
|
+
//
|
|
4
|
+
// 머지 규칙:
|
|
5
|
+
// - 스칼라 (base/radius/mode): app 값 우선 (undefined 면 core)
|
|
6
|
+
// - extraTokens.{root,light,dark}: 키 단위 deep-merge — app 키가 core 키 override, 누락 키는 core 상속
|
|
7
|
+
// - extraTokens 카테고리 자체가 한쪽만 있으면 그쪽 그대로
|
|
8
|
+
//
|
|
9
|
+
// 모노레포 ui-core 의 theme 이 없으면 (or coreConfig 미제공) app theme 그대로 반환 — backward-compat.
|
|
10
|
+
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { promises as fsp } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
/** ui-app config 경로에서 sibling ui-core/sh-ui.config.json 을 찾는다. 모노레포 컨벤션 가정:
|
|
15
|
+
* .../packages/ui/ui-apps/ui-{name}/sh-ui.config.json
|
|
16
|
+
* .../packages/ui/ui-core/sh-ui.config.json
|
|
17
|
+
* 못 찾으면 null. */
|
|
18
|
+
export async function findUiCoreConfigPath(uiAppConfigPath) {
|
|
19
|
+
// ui-apps/ui-{name}/ -> ../../ui-core/
|
|
20
|
+
const appDir = path.dirname(path.resolve(uiAppConfigPath));
|
|
21
|
+
const candidate = path.resolve(appDir, '..', '..', 'ui-core', 'sh-ui.config.json');
|
|
22
|
+
try {
|
|
23
|
+
await fsp.access(candidate);
|
|
24
|
+
return candidate;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** sh-ui.config.json 을 읽어 객체 반환. 파일 없거나 파싱 실패 시 null. */
|
|
31
|
+
export async function readShUiConfig(configPath) {
|
|
32
|
+
if (!configPath) return null;
|
|
33
|
+
try {
|
|
34
|
+
const text = await fsp.readFile(configPath, 'utf-8');
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** extraTokens 한 카테고리(root/light/dark) deep-merge. app 키 우선. 둘 다 없으면 undefined. */
|
|
42
|
+
function mergeTokenMap(coreMap, appMap) {
|
|
43
|
+
if (!coreMap && !appMap) return undefined;
|
|
44
|
+
return { ...(coreMap ?? {}), ...(appMap ?? {}) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** extraTokens 블록 deep-merge. */
|
|
48
|
+
function mergeExtraTokens(coreExtra, appExtra) {
|
|
49
|
+
if (!coreExtra && !appExtra) return undefined;
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const cat of ['root', 'light', 'dark']) {
|
|
52
|
+
const merged = mergeTokenMap(coreExtra?.[cat], appExtra?.[cat]);
|
|
53
|
+
if (merged && Object.keys(merged).length > 0) result[cat] = merged;
|
|
54
|
+
}
|
|
55
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* theme block 두 개를 머지해 effective theme 반환.
|
|
60
|
+
* 둘 다 null/undefined 면 null. 한쪽만 있으면 그쪽 그대로.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveTheme(coreTheme, appTheme) {
|
|
63
|
+
if (!coreTheme && !appTheme) return null;
|
|
64
|
+
if (!coreTheme) return appTheme;
|
|
65
|
+
if (!appTheme) return coreTheme;
|
|
66
|
+
|
|
67
|
+
const merged = {
|
|
68
|
+
...coreTheme,
|
|
69
|
+
...appTheme, // app 의 스칼라(base/radius/mode) 가 core 를 override
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const extra = mergeExtraTokens(coreTheme.extraTokens, appTheme.extraTokens);
|
|
73
|
+
if (extra) merged.extraTokens = extra;
|
|
74
|
+
else delete merged.extraTokens;
|
|
75
|
+
|
|
76
|
+
return merged;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* ui-app config 객체 + (선택) ui-core config 객체로부터 effective theme 계산.
|
|
81
|
+
* config 자체에 theme 필드가 없으면 null 자리에서 시작.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveThemeFromConfigs(coreConfig, appConfig) {
|
|
84
|
+
return resolveTheme(coreConfig?.theme ?? null, appConfig?.theme ?? null);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* ui-app config 경로에서 sibling ui-core 를 찾아 머지된 theme 을 반환.
|
|
89
|
+
* 모노레포가 아니거나 sibling 없으면 app theme 그대로.
|
|
90
|
+
*/
|
|
91
|
+
export async function loadResolvedTheme(uiAppConfigPath, appConfig) {
|
|
92
|
+
const corePath = await findUiCoreConfigPath(uiAppConfigPath);
|
|
93
|
+
const coreConfig = corePath ? await readShUiConfig(corePath) : null;
|
|
94
|
+
return resolveThemeFromConfigs(coreConfig, appConfig);
|
|
95
|
+
}
|
package/src/mcp.mjs
CHANGED
|
@@ -488,7 +488,14 @@ export async function startMcpServer() {
|
|
|
488
488
|
cwd: z.string().optional()
|
|
489
489
|
.describe("부모 디렉토리. 기본 process.cwd()"),
|
|
490
490
|
force: z.boolean().optional()
|
|
491
|
-
.describe("기존 디렉토리
|
|
491
|
+
.describe("기존 디렉토리 덮어쓰기 — 디렉토리 전체를 삭제 후 새로 스캐폴드. 기본 false (안전)."),
|
|
492
|
+
inPlace: z.boolean().optional()
|
|
493
|
+
.describe(
|
|
494
|
+
"기존 디렉토리에 비파괴 머지 — 디렉토리를 삭제하지 않고, 이미 있는 파일은 보존한 채 " +
|
|
495
|
+
"없는 파일만 채워 넣는다. 커스터마이즈된 monorepo 루트(CLAUDE.md·.gitignore 등)·.git 을 " +
|
|
496
|
+
"지키면서 apps/·packages/ 를 재생성할 때 사용. force 와 상호배타 — force 는 전체 삭제, " +
|
|
497
|
+
"inPlace 는 보존. 재생성하려는 파일은 미리 지워두면 그 자리만 새로 채워진다. v0.110.0+ 신규.",
|
|
498
|
+
),
|
|
492
499
|
i18n: z.enum(I18N_LIBRARIES).optional()
|
|
493
500
|
.describe(
|
|
494
501
|
"i18n 라이브러리 — platform=vite 일 때만 의미. 'react-i18next' 로 설정 시 i18next + react-i18next + browser-languagedetector + http-backend 셋업 + " +
|
|
@@ -549,15 +556,25 @@ export async function startMcpServer() {
|
|
|
549
556
|
}],
|
|
550
557
|
};
|
|
551
558
|
}
|
|
559
|
+
if (input.force && input.inPlace) {
|
|
560
|
+
return {
|
|
561
|
+
isError: true,
|
|
562
|
+
content: [{
|
|
563
|
+
type: "text",
|
|
564
|
+
text: "force 와 inPlace 는 동시에 쓸 수 없습니다 — force 는 디렉토리 전체 삭제, inPlace 는 보존 머지.",
|
|
565
|
+
}],
|
|
566
|
+
};
|
|
567
|
+
}
|
|
552
568
|
const targetParent = resolveCwd(input);
|
|
553
569
|
const targetDir = resolve(targetParent, input.name);
|
|
554
|
-
|
|
570
|
+
// inPlace 는 기존 디렉토리를 전제로 하므로 '이미 존재' 가 에러가 아니다.
|
|
571
|
+
if (existsSync(targetDir) && !input.force && !input.inPlace) {
|
|
555
572
|
return {
|
|
556
573
|
isError: true,
|
|
557
574
|
content: [
|
|
558
575
|
{
|
|
559
576
|
type: "text",
|
|
560
|
-
text: `'${targetDir}' 가 이미 존재합니다. 덮어쓰려면 force: true.`,
|
|
577
|
+
text: `'${targetDir}' 가 이미 존재합니다. 덮어쓰려면 force: true, 보존 머지하려면 inPlace: true.`,
|
|
561
578
|
},
|
|
562
579
|
],
|
|
563
580
|
};
|
|
@@ -580,6 +597,7 @@ export async function startMcpServer() {
|
|
|
580
597
|
port: input.port,
|
|
581
598
|
gitInit: input.gitInit,
|
|
582
599
|
locale: input.locale,
|
|
600
|
+
inPlace: input.inPlace,
|
|
583
601
|
yes: true, // 사전 검사를 마쳤으니 generator 의 confirm 프롬프트 우회
|
|
584
602
|
}),
|
|
585
603
|
);
|
package/src/tokens-cmd.mjs
CHANGED
|
@@ -34,23 +34,41 @@ async function loadConfig(cwd) {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* config 로 expected tokens.css 를 생성. custom / non-buildable preset 은 throw.
|
|
37
|
+
*
|
|
38
|
+
* v0.111.0+ — 모노레포 ui-app 의 경우 sibling ui-core/sh-ui.config.json 의
|
|
39
|
+
* 공유 theme 을 merge (resolveTheme: app 키가 core 키 override).
|
|
37
40
|
*/
|
|
38
|
-
async function buildExpected(config) {
|
|
39
|
-
|
|
41
|
+
async function buildExpected(config, cwd) {
|
|
42
|
+
const effectiveConfig = await applySharedTheme(config, cwd);
|
|
43
|
+
const effectiveBase = effectiveConfig.theme?.base;
|
|
44
|
+
if (effectiveBase === "custom") {
|
|
40
45
|
throw new Error(
|
|
41
46
|
"custom theme 은 buildTokens 로 재생성 불가 (base64 가 단일 진실). " +
|
|
42
47
|
"tokens diff/upgrade 는 buildable preset (neutral/zinc/slate) 에서만 동작합니다.",
|
|
43
48
|
);
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
if (base && !THEME_BASES.includes(base)) {
|
|
50
|
+
if (effectiveBase && !THEME_BASES.includes(effectiveBase)) {
|
|
47
51
|
throw new Error(
|
|
48
|
-
`'${
|
|
52
|
+
`'${effectiveBase}' preset 은 buildTokens 로 재생성 불가 (primitives 미정의 — buildable: ${THEME_BASES.join("/")}). ` +
|
|
49
53
|
"diff/upgrade 는 base 가 neutral/zinc/slate 일 때만 사용 가능합니다.",
|
|
50
54
|
);
|
|
51
55
|
}
|
|
52
56
|
const { buildTokens } = await loadTokensBuilder();
|
|
53
|
-
return buildTokens(
|
|
57
|
+
return buildTokens(effectiveConfig);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 모노레포 ui-app 의 sibling ui-core 의 theme 을 머지한 config 를 반환. ui-core 없으면 그대로. */
|
|
61
|
+
async function applySharedTheme(config, cwd) {
|
|
62
|
+
const { findUiCoreConfigPath, readShUiConfig, resolveThemeFromConfigs } =
|
|
63
|
+
await import("./create/theme/resolveTheme.js");
|
|
64
|
+
const appConfigPath = resolve(cwd, "sh-ui.config.json");
|
|
65
|
+
const corePath = await findUiCoreConfigPath(appConfigPath);
|
|
66
|
+
// 같은 파일을 가리키면 (이게 곧 ui-core) merge 불필요.
|
|
67
|
+
if (!corePath || corePath === appConfigPath) return config;
|
|
68
|
+
const coreConfig = await readShUiConfig(corePath);
|
|
69
|
+
if (!coreConfig?.theme) return config;
|
|
70
|
+
const mergedTheme = resolveThemeFromConfigs(coreConfig, config);
|
|
71
|
+
return { ...config, theme: mergedTheme };
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
async function loadCurrent(config, cwd) {
|
|
@@ -106,7 +124,7 @@ function renderDiffReport({ added, removed, changed, unchangedCount }, rel) {
|
|
|
106
124
|
|
|
107
125
|
export async function runTokensDiff({ cwd }) {
|
|
108
126
|
const config = await loadConfig(cwd);
|
|
109
|
-
const expectedText = await buildExpected(config);
|
|
127
|
+
const expectedText = await buildExpected(config, cwd);
|
|
110
128
|
const { text: currentText, rel } = await loadCurrent(config, cwd);
|
|
111
129
|
const diff =
|
|
112
130
|
config.platform === "flutter"
|
|
@@ -117,7 +135,7 @@ export async function runTokensDiff({ cwd }) {
|
|
|
117
135
|
|
|
118
136
|
export async function runTokensUpgrade({ cwd, mode }) {
|
|
119
137
|
const config = await loadConfig(cwd);
|
|
120
|
-
const expectedText = await buildExpected(config);
|
|
138
|
+
const expectedText = await buildExpected(config, cwd);
|
|
121
139
|
const current = await loadCurrent(config, cwd);
|
|
122
140
|
const isFlutter = config.platform === "flutter";
|
|
123
141
|
|