sh-ui-cli 0.110.0 → 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 +13 -0
- package/data/tokens/build.mjs +49 -14
- package/package.json +1 -1
- package/sh-ui.schema.json +10 -0
- package/src/create/generator.js +17 -0
- package/src/create/theme/inject.js +32 -7
- package/src/create/theme/resolveTheme.js +95 -0
- package/src/tokens-cmd.mjs +26 -8
|
@@ -2,6 +2,19 @@
|
|
|
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
|
+
},
|
|
5
18
|
{
|
|
6
19
|
"version": "0.110.0",
|
|
7
20
|
"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/generator.js
CHANGED
|
@@ -604,6 +604,23 @@ export async function addApp(options = {}) {
|
|
|
604
604
|
}
|
|
605
605
|
await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css, themeBase);
|
|
606
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
|
+
|
|
607
624
|
console.log(`\n✅ apps/${appName} 이 추가되었습니다!`);
|
|
608
625
|
console.log('\n pnpm install');
|
|
609
626
|
console.log(` pnpm --filter ${appName} dev\n`);
|
|
@@ -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/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
|
|