sh-ui-cli 0.93.0 → 0.94.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.
|
@@ -2,6 +2,20 @@
|
|
|
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.94.0",
|
|
7
|
+
"date": "2026-05-15",
|
|
8
|
+
"title": "vite 스캐폴드 P0 fix 묶음 — i18n locale 자동 미러 / monorepo .gitignore / CLAUDE.md platform 분기",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`--i18n` 시 vite-plugin-static-copy 자동 셋업** — emitI18n 이 `vite-plugin-static-copy` (devDeps) + `vite.config.ts` 의 plugins 배열 패치를 자동 처리. 사용자가 손으로 plugin 추가하지 않아도 dev/build 양쪽에서 `src/shared/i18n/locales/*` (또는 `src/lib/i18n/locales/*`) 가 `public/locales/*` 로 미러되어 `/locales/{{lng}}/{{ns}}.json` 404 없음. Tauri prod 빌드도 dist 에 자동 포함.",
|
|
12
|
+
"**Locale seed 콘텐츠 언어별 정합화** — ko = `{greeting: '안녕하세요', app_title: 'sh-ui 앱'}`, en = `{greeting: 'Hello', app_title: 'sh-ui app'}`, 그 외 locale 도 영어 placeholder 시드를 받아 translator 가 채울 키가 즉시 보임. 이전엔 fallback (보통 ko) 에만 영어 시드 + 나머지 빈 객체였음.",
|
|
13
|
+
"**monorepo `apps/<name>/gitignore` 도 dot-prefix** — finalizeProject 가 root 만 처리하던 동작을 recursive 로 변경. vite-app 처럼 sub-app 에 자체 gitignore 가 있는 케이스에서 점 없는 파일이 남아 node_modules / dist 가 git staged 될 위험 제거. 회귀 가드 smoke 추가.",
|
|
14
|
+
"**CLAUDE.md `{{PLATFORM_APP_DESCRIPTION}}` 분기** — monorepo CLAUDE.md 가 `apps/<name>` 을 'Next.js 앱' 으로 hardcode 하던 문제 해결. `--platform vite` 면 'Vite SPA' + Tauri 옵션 시 추가 문장이 자동 치환되어 AI 에이전트가 잘못된 컨벤션 (App Router/RSC) 으로 코드를 작성하는 회귀 차단.",
|
|
15
|
+
"**mes arch 옵션 docs 명시** — monorepo / nextjs-standalone CLAUDE.md 에 `--arch` 옵션 (`fsd` / `flat` / `mes`) 섹션 추가. 외부 에이전트가 `packages/eslint-config/mes.js` 를 deprecated 잔재로 오해하던 문제 해결 — mes 는 MES(Backoffice) 전용 별도 arch, 모두 emit 되는 건 라이브러리 형태이기 때문."
|
|
16
|
+
],
|
|
17
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.94.0"
|
|
18
|
+
},
|
|
5
19
|
{
|
|
6
20
|
"version": "0.93.0",
|
|
7
21
|
"date": "2026-05-14",
|
package/package.json
CHANGED
|
@@ -39,6 +39,21 @@ export function getArchesForPlatform(platform) {
|
|
|
39
39
|
return allArchitectures.filter((a) => a.platforms.includes(platform));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* MCP tool description / CLI --help / docs 어디서나 재사용 가능한 arch 설명 블록.
|
|
44
|
+
* "fsd (FSD) — ..., flat (Flat) — ..., mes (MES) — ..." 처럼 사람-읽기 좋은 한 줄로 직렬화.
|
|
45
|
+
*
|
|
46
|
+
* 외부 AI 에이전트 (Cursor / Codex / Copilot 등) 는 CLAUDE.md 를 읽지 않으므로
|
|
47
|
+
* MCP schema description 에 각 arch 의 의미를 노출해야 mes 같은 도메인-특화 arch
|
|
48
|
+
* 를 deprecated 잔재로 오해하지 않는다 (v0.94.0+).
|
|
49
|
+
*/
|
|
50
|
+
export function describeArchOptions(platformFilter) {
|
|
51
|
+
const arches = platformFilter ? getArchesForPlatform(platformFilter) : allArchitectures;
|
|
52
|
+
return arches
|
|
53
|
+
.map((a) => `${a.name} (${a.label}) — ${a.description}`)
|
|
54
|
+
.join(' | ');
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
/**
|
|
43
58
|
* 주어진 arch 가 platform 과 호환되는지 검증. 호환 안 되면 친절한 에러.
|
|
44
59
|
* generator/cli-args 양쪽에서 호출.
|
package/src/create/generator.js
CHANGED
|
@@ -1046,9 +1046,9 @@ import HttpBackend from 'i18next-http-backend';
|
|
|
1046
1046
|
import { initReactI18next } from 'react-i18next';
|
|
1047
1047
|
|
|
1048
1048
|
// 클라이언트 측 lazy-load — 빌드 산출물에서 public/locales/{lng}/{ns}.json 경로로 fetch.
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1049
|
+
// 원본 locale 파일은 ${i18nDirRel}/locales/* 에 두고, vite-plugin-static-copy 가
|
|
1050
|
+
// dev/build 양쪽에서 public/locales/* 로 자동 미러링한다 (vite.config.ts 참고).
|
|
1051
|
+
// Tauri 빌드의 경우도 dist/locales 에 그대로 포함된다.
|
|
1052
1052
|
|
|
1053
1053
|
i18n
|
|
1054
1054
|
.use(HttpBackend)
|
|
@@ -1078,12 +1078,17 @@ export default i18n;
|
|
|
1078
1078
|
`export { default } from './config';\n`,
|
|
1079
1079
|
);
|
|
1080
1080
|
|
|
1081
|
+
// locale 별 시드 — 같은 key 를 모든 locale 에 emit (translator 가 무엇을 채워야 하는지 즉시 보임).
|
|
1082
|
+
// 핵심 locale (ko, en) 만 사람-언어 값, 그 외 locale 은 영어 placeholder.
|
|
1083
|
+
const seedByLocale = {
|
|
1084
|
+
ko: { greeting: '안녕하세요', app_title: 'sh-ui 앱' },
|
|
1085
|
+
en: { greeting: 'Hello', app_title: 'sh-ui app' },
|
|
1086
|
+
};
|
|
1087
|
+
const fallbackSeed = { greeting: 'Hello', app_title: 'sh-ui app' };
|
|
1081
1088
|
for (const lng of locales) {
|
|
1082
1089
|
const localeDir = path.join(i18nDir, 'locales', lng);
|
|
1083
1090
|
await fs.ensureDir(localeDir);
|
|
1084
|
-
const seed = lng
|
|
1085
|
-
? { greeting: 'Hello World', app_title: 'sh-ui app' }
|
|
1086
|
-
: {};
|
|
1091
|
+
const seed = seedByLocale[lng] ?? fallbackSeed;
|
|
1087
1092
|
await fs.writeFile(
|
|
1088
1093
|
path.join(localeDir, 'common.json'),
|
|
1089
1094
|
JSON.stringify(seed, null, 2) + '\n',
|
|
@@ -1130,7 +1135,56 @@ export function GlobalProvider({ children }: { children: ReactNode }) {
|
|
|
1130
1135
|
pkg.dependencies['i18next-http-backend'] = '^2.7.1';
|
|
1131
1136
|
pkg.dependencies['react-i18next'] = '^15.1.0';
|
|
1132
1137
|
pkg.dependencies = sortObjectKeys(pkg.dependencies);
|
|
1138
|
+
pkg.devDependencies = pkg.devDependencies ?? {};
|
|
1139
|
+
// dev/build 양쪽에서 src/shared/i18n/locales/* (또는 src/lib/i18n/locales/*) 를
|
|
1140
|
+
// public/locales/* 로 자동 미러 → i18next-http-backend 의 /locales/{{lng}}/{{ns}}.json 이 동작.
|
|
1141
|
+
pkg.devDependencies['vite-plugin-static-copy'] = '^2.2.0';
|
|
1142
|
+
pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
|
|
1133
1143
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
1144
|
+
|
|
1145
|
+
// vite.config.ts 에 vite-plugin-static-copy 삽입.
|
|
1146
|
+
await patchViteConfigForI18n(targetDir, { i18nDirRel });
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* vite.config.ts 의 plugins 배열에 vite-plugin-static-copy 호출을 삽입한다.
|
|
1151
|
+
* i18n 의 locale 파일 (src/shared/i18n/locales/* 등) 을 public/locales/* 로 자동 미러.
|
|
1152
|
+
*
|
|
1153
|
+
* 이미 같은 호출이 있으면 no-op. parsing 실패하면 사용자에게 수동 작업을 알리고 abort 하지 않는다.
|
|
1154
|
+
*/
|
|
1155
|
+
async function patchViteConfigForI18n(targetDir, { i18nDirRel }) {
|
|
1156
|
+
const viteConfigPath = path.join(targetDir, 'vite.config.ts');
|
|
1157
|
+
if (!(await fs.pathExists(viteConfigPath))) return;
|
|
1158
|
+
let src = await fs.readFile(viteConfigPath, 'utf-8');
|
|
1159
|
+
|
|
1160
|
+
if (src.includes('vite-plugin-static-copy')) {
|
|
1161
|
+
return; // 이미 셋업됨 (재진입 안전).
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const importLine = `import { viteStaticCopy } from 'vite-plugin-static-copy';`;
|
|
1165
|
+
// 다른 import 블록 뒤에 삽입.
|
|
1166
|
+
const lastImportIdx = src.lastIndexOf('import ');
|
|
1167
|
+
const insertImportAt = src.indexOf('\n', lastImportIdx) + 1;
|
|
1168
|
+
src = src.slice(0, insertImportAt) + importLine + '\n' + src.slice(insertImportAt);
|
|
1169
|
+
|
|
1170
|
+
const pluginCall = ` viteStaticCopy({
|
|
1171
|
+
// i18n locale 파일을 public/locales 로 미러 — i18next-http-backend 의 loadPath 와 매칭.
|
|
1172
|
+
targets: [
|
|
1173
|
+
{ src: '${i18nDirRel}/locales/*', dest: 'locales' },
|
|
1174
|
+
],
|
|
1175
|
+
}),`;
|
|
1176
|
+
|
|
1177
|
+
// plugins: [ ... ] 의 시작 직후에 새 plugin 삽입.
|
|
1178
|
+
const pluginsMatch = src.match(/plugins:\s*\[/);
|
|
1179
|
+
if (pluginsMatch) {
|
|
1180
|
+
const insertAt = pluginsMatch.index + pluginsMatch[0].length;
|
|
1181
|
+
src = src.slice(0, insertAt) + '\n' + pluginCall + src.slice(insertAt);
|
|
1182
|
+
} else {
|
|
1183
|
+
// 형태가 예상과 다르면 패치 포기 — config 가 깨지지 않도록.
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
await fs.writeFile(viteConfigPath, src);
|
|
1134
1188
|
}
|
|
1135
1189
|
|
|
1136
1190
|
/**
|
|
@@ -1331,6 +1385,11 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
|
|
|
1331
1385
|
rootPkg.name = projectName;
|
|
1332
1386
|
await fs.writeJson(rootPkgPath, rootPkg, { spaces: 2 });
|
|
1333
1387
|
|
|
1388
|
+
// CLAUDE.md 의 platform 분기 placeholder 치환. AI 에이전트가 Next.js 가정으로
|
|
1389
|
+
// 잘못된 컨벤션을 적용하지 않도록 (v0.94.0+).
|
|
1390
|
+
const platformAppDesc = describeAppPlatform(platform, { tauri });
|
|
1391
|
+
await replaceInAllFiles(targetDir, '{{PLATFORM_APP_DESCRIPTION}}', platformAppDesc);
|
|
1392
|
+
|
|
1334
1393
|
// Update turbo.json
|
|
1335
1394
|
const turboPath = path.join(targetDir, 'turbo.json');
|
|
1336
1395
|
const turbo = await fs.readJson(turboPath);
|
|
@@ -2149,11 +2208,9 @@ function buildErrorModuleCss() {
|
|
|
2149
2208
|
* git init 은 dry-run 에서는 스킵하고, 실패해도(git 미설치 등) 조용히 넘어간다.
|
|
2150
2209
|
*/
|
|
2151
2210
|
async function finalizeProject(targetDir, { dryRun = false } = {}) {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
await fs.move(noDot, withDot, { overwrite: true });
|
|
2156
|
-
}
|
|
2211
|
+
// 모노레포 / sub-app 까지 모든 `gitignore` 를 `.gitignore` 로 rename.
|
|
2212
|
+
// root 만 처리하면 apps/<name>/gitignore 가 그대로 남아 node_modules/dist 가 staged 된다 (v0.93.0 버그).
|
|
2213
|
+
await renameAllGitignoreRecursive(targetDir);
|
|
2157
2214
|
|
|
2158
2215
|
if (dryRun) return;
|
|
2159
2216
|
|
|
@@ -2164,6 +2221,40 @@ async function finalizeProject(targetDir, { dryRun = false } = {}) {
|
|
|
2164
2221
|
}
|
|
2165
2222
|
}
|
|
2166
2223
|
|
|
2224
|
+
/**
|
|
2225
|
+
* CLAUDE.md 의 `{{PLATFORM_APP_DESCRIPTION}}` 치환용 문장 — AI 에이전트에게 어떤 플랫폼인지
|
|
2226
|
+
* 정확히 전달해서 잘못된 컨벤션 (예: vite 프로젝트에 App Router 가정) 적용 방지.
|
|
2227
|
+
*/
|
|
2228
|
+
function describeAppPlatform(platform, { tauri = false } = {}) {
|
|
2229
|
+
if (platform === 'vite') {
|
|
2230
|
+
const tauriSuffix = tauri
|
|
2231
|
+
? ' Tauri 데스크탑 셸이 동봉되어 있어 `src-tauri/` 가 native 진입점이다.'
|
|
2232
|
+
: '';
|
|
2233
|
+
return `Vite SPA (React + TypeScript). 라우트 + 비즈니스 로직. RSC / App Router 없음 — 모든 코드가 클라이언트 사이드 실행이다.${tauriSuffix}`;
|
|
2234
|
+
}
|
|
2235
|
+
// 디폴트: Next.js. 향후 platform 추가 시 분기 늘릴 것.
|
|
2236
|
+
return 'Next.js 앱 (App Router + Server Components). 라우트 + 비즈니스 로직.';
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
async function renameAllGitignoreRecursive(dir) {
|
|
2240
|
+
let entries;
|
|
2241
|
+
try {
|
|
2242
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2243
|
+
} catch {
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
for (const entry of entries) {
|
|
2247
|
+
const fullPath = path.join(dir, entry.name);
|
|
2248
|
+
if (entry.isDirectory()) {
|
|
2249
|
+
// 스캐폴드 직후엔 node_modules / .git 가 없지만 방어적으로.
|
|
2250
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
2251
|
+
await renameAllGitignoreRecursive(fullPath);
|
|
2252
|
+
} else if (entry.name === 'gitignore') {
|
|
2253
|
+
await fs.move(fullPath, path.join(dir, '.gitignore'), { overwrite: true });
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2167
2258
|
async function replaceInAllFiles(dir, search, replace) {
|
|
2168
2259
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2169
2260
|
for (const entry of entries) {
|
package/src/mcp.mjs
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
OBSERVABILITY_PROVIDERS,
|
|
52
52
|
} from "./constants.js";
|
|
53
53
|
import { allPlugins } from "./create/plugins/index.js";
|
|
54
|
-
import { allArchitectures } from "./create/architectures/index.js";
|
|
54
|
+
import { allArchitectures, describeArchOptions } from "./create/architectures/index.js";
|
|
55
55
|
import { describeTemplate } from "./create/describeTemplate.js";
|
|
56
56
|
import { THEME_PRESET_NAMES } from "./create/theme/presets.js";
|
|
57
57
|
import { decodeTheme } from "./create/theme/decode.js";
|
|
@@ -386,9 +386,10 @@ export async function startMcpServer() {
|
|
|
386
386
|
arch: z.enum(ARCH_NAMES).optional()
|
|
387
387
|
.describe(
|
|
388
388
|
`프로젝트 아키텍처 — 플랫폼별로 사용 가능한 값이 다름. ` +
|
|
389
|
-
|
|
390
|
-
`flutter 는 현재 arch 디스크립터 없음 (
|
|
391
|
-
`arch 와 플러그인은 별개 — arch 는 폴더 구조/import alias 컨벤션, 플러그인은
|
|
389
|
+
`사용 가능한 아키텍처 (의미 포함): ${describeArchOptions()}. ` +
|
|
390
|
+
`next 기본: fsd · vite 지원: fsd/flat · flutter 는 현재 arch 디스크립터 없음 (host 자체 default). ` +
|
|
391
|
+
`arch 와 플러그인은 별개 — arch 는 폴더 구조/import alias 컨벤션, 플러그인은 기능. ` +
|
|
392
|
+
`mes 는 폐기된 옵션이 아니라 별도 도메인-특화 아키텍처 (관리자/MES 류).`,
|
|
392
393
|
),
|
|
393
394
|
theme: z.string().optional()
|
|
394
395
|
.describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는 base64 테마 코드. 사용자가 톤을 직접 손본 결과를 영구 보관하려면 sh_ui_encode_theme 으로 base64 를 만들어 여기에 넘긴다.`),
|
|
@@ -930,7 +931,11 @@ export async function startMcpServer() {
|
|
|
930
931
|
structure: z.enum(CREATE_STRUCTURES).optional()
|
|
931
932
|
.describe("Next.js 구조. platform=next | vite 일 때 의미. 기본 standalone"),
|
|
932
933
|
arch: z.enum(ARCH_NAMES).optional()
|
|
933
|
-
.describe(
|
|
934
|
+
.describe(
|
|
935
|
+
`아키텍처. 기본 fsd. ` +
|
|
936
|
+
`옵션 (의미 포함): ${describeArchOptions()}. ` +
|
|
937
|
+
`mes 는 deprecated 가 아니라 도메인-특화 옵션.`,
|
|
938
|
+
),
|
|
934
939
|
plugins: z.array(z.enum(PLUGIN_NAMES)).optional()
|
|
935
940
|
.describe(`Next.js 플러그인 배열 (${PLUGIN_NAMES.join(', ')}). 미지정 빈 배열`),
|
|
936
941
|
cssFramework: z.enum(CSS_FRAMEWORKS).optional()
|
|
@@ -5,13 +5,26 @@ sh-ui CLI 가 스캐폴드한 monorepo (Turborepo + pnpm workspace). AI 에이
|
|
|
5
5
|
|
|
6
6
|
## 구조
|
|
7
7
|
|
|
8
|
-
- `apps/<name>/` —
|
|
8
|
+
- `apps/<name>/` — {{PLATFORM_APP_DESCRIPTION}}
|
|
9
9
|
- `packages/ui/ui-core/` — 모든 앱이 공유하는 sh-ui 컴포넌트 / 훅 / 유틸 SoT.
|
|
10
10
|
컴포넌트 추가는 여기에 한 번만.
|
|
11
11
|
- `packages/ui/ui-apps/ui-<name>/` — 앱별 토큰 (color/spacing/font) 만 보관.
|
|
12
12
|
컴포넌트는 두지 않음 (v0.65+ `tokens-only` 마커).
|
|
13
13
|
- `packages/eslint-config/` · `packages/typescript-config/` — 공용 설정.
|
|
14
14
|
|
|
15
|
+
## 아키텍처 옵션 (`--arch`)
|
|
16
|
+
|
|
17
|
+
- **`fsd`** (default) — Feature-Sliced Design. `src/{app,pages,widgets,features,entities,shared}`
|
|
18
|
+
레이어로 단방향 의존(상위→하위). 일반적 SPA / 서비스에 적합.
|
|
19
|
+
- **`flat`** — `src/{components,hooks,lib,pages}` 단순 구조. 작은 앱 / 학습용.
|
|
20
|
+
- **`mes`** — MES (Backoffice) 전용. 페이지 격리 + 단방향 의존 강제. ERP/내부 관리도구
|
|
21
|
+
처럼 페이지 간 분리도가 중요한 도메인. Next.js 만 지원 (vite-app 은 fsd/flat 만).
|
|
22
|
+
|
|
23
|
+
세 arch 모두 `packages/eslint-config/` 에 별도 ruleset 으로 들어가 있다 — 라이브러리
|
|
24
|
+
이므로 모두 emit 되지만, 본인 앱의 `eslint.config.js` 에서 선택한 arch 의 config 만
|
|
25
|
+
import 한다. 다른 arch 의 `.js` 파일이 보여도 deprecated 가 아니라 다른 앱이
|
|
26
|
+
쓸 수 있는 옵션이다.
|
|
27
|
+
|
|
15
28
|
## 날짜 / 숫자 포맷
|
|
16
29
|
|
|
17
30
|
- raw `Date.prototype.toLocaleDateString()` / `toLocaleString()` / `toLocaleTimeString()`
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
sh-ui CLI 가 스캐폴드한 Next.js standalone 프로젝트. AI 에이전트 (Claude / Cursor /
|
|
4
4
|
Codex 등) 가 이 파일을 컨텍스트로 읽고 아래 규칙을 따른다.
|
|
5
5
|
|
|
6
|
+
## 아키텍처 옵션 (`--arch`)
|
|
7
|
+
|
|
8
|
+
- **`fsd`** (default) — Feature-Sliced Design. `src/{app,pages,widgets,features,entities,shared}`
|
|
9
|
+
레이어로 단방향 의존(상위→하위). 일반적 SPA / 서비스에 적합.
|
|
10
|
+
- **`flat`** — `src/{components,hooks,lib}` 단순 구조. 작은 앱 / 학습용.
|
|
11
|
+
- **`mes`** — MES (Backoffice) 전용. 페이지 격리 + 단방향 의존 강제. ERP/내부 관리도구
|
|
12
|
+
처럼 페이지 간 분리도가 중요한 도메인.
|
|
13
|
+
|
|
14
|
+
선택한 arch 에 따라 `eslint.config.js` 가 다르게 emit 된다.
|
|
15
|
+
|
|
6
16
|
## 날짜 / 숫자 포맷
|
|
7
17
|
|
|
8
18
|
- raw `Date.prototype.toLocaleDateString()` / `toLocaleString()` / `toLocaleTimeString()`
|