sh-ui-cli 0.61.0 → 0.61.2
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/package.json +1 -1
- package/src/create/generator.js +27 -22
- package/src/init.mjs +1 -0
- package/src/rename-app.mjs +3 -3
- package/templates/nextjs-app/README.md +0 -2
- package/templates/nextjs-standalone/_arch/flat/sh-ui.config.json +1 -0
- package/templates/nextjs-standalone/_arch/fsd/sh-ui.config.json +1 -0
- package/templates/ui-app-template/sh-ui.config.json +1 -0
- package/templates/nextjs-app/Dockerfile +0 -42
- package/templates/nextjs-standalone/Dockerfile +0 -35
|
@@ -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.61.2",
|
|
7
|
+
"date": "2026-05-08",
|
|
8
|
+
"title": "fix — create 산출물의 sh-ui.config.json 누수 두 건 (paths.styles · theme.base)",
|
|
9
|
+
"type": "patch",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**템플릿의 `sh-ui.config.json` 에 `paths.styles` 추가** — `base.css` 같은 스타일 산출물이 `{styles}` placeholder 를 참조하는데 기존 템플릿(ui-app-template, nextjs-standalone fsd/flat) 이 `paths.styles` 를 안 박아 줘서 `sh-ui add base` 가 'paths.styles 가 sh-ui.config.json에 없습니다' 로 실패하던 문제. 이제 `create` 직후 추가 패치 없이 `add` 가 동작.",
|
|
12
|
+
"**`create --theme <preset|base64>` 결과가 `sh-ui.config.json` 의 `theme.base` 에 반영** — 기존엔 어떤 테마를 골라도 `theme.base` 가 템플릿 기본값(`neutral`)으로 남아 있어 의미 불일치. 이제 프리셋 이름이면 그 이름(`violet`/`rose`/...), base64 입력이면 `custom` 으로 기록.",
|
|
13
|
+
"**`init.mjs` 의 `PATHS.react` 에도 `styles` 추가** — `sh-ui init` 으로 만든 config 도 같은 시드 가짐."
|
|
14
|
+
],
|
|
15
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.61.2"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"version": "0.61.1",
|
|
19
|
+
"date": "2026-05-07",
|
|
20
|
+
"title": "chore — 사용자 프로젝트 템플릿에서 Dockerfile 제거",
|
|
21
|
+
"type": "patch",
|
|
22
|
+
"highlights": [
|
|
23
|
+
"**`nextjs-app` / `nextjs-standalone` 템플릿의 `Dockerfile` 삭제** — sh-ui 가 1급 docker 지원이 아니고 그동안 emit 한 Dockerfile 도 검증 없이 multi-stage build 만 쓰여 있었음. 사용자가 docker 가 필요하면 자신의 배포 환경에 맞춰 직접 작성하는 게 정확.",
|
|
24
|
+
"**`generateApp` 의 Dockerfile EXPOSE/PORT 자동 갱신 로직 제거** — emit 안 하니 갱신할 대상도 없음.",
|
|
25
|
+
"**README 의 Docker 언급 정리** — 'Docker 지원' 항목과 구조 도해의 Dockerfile 라인 제거.",
|
|
26
|
+
"**`rename-app` 은 그대로 동작** — 사용자가 직접 Dockerfile 을 추가했을 때를 대비해 rename 패턴은 유지 (TEXT_BASENAMES 에 Dockerfile/Dockerfile.dev/Dockerfile.prod 그대로)."
|
|
27
|
+
],
|
|
28
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.61.1"
|
|
29
|
+
},
|
|
5
30
|
{
|
|
6
31
|
"version": "0.61.0",
|
|
7
32
|
"date": "2026-05-07",
|
package/package.json
CHANGED
package/src/create/generator.js
CHANGED
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
DEFAULT_ARCH,
|
|
79
79
|
} from './architectures/index.js';
|
|
80
80
|
import { resolveTheme } from './theme/decode.js';
|
|
81
|
-
import { THEME_PRESETS, getThemePreset } from './theme/presets.js';
|
|
81
|
+
import { THEME_PRESETS, THEME_PRESET_NAMES, getThemePreset } from './theme/presets.js';
|
|
82
82
|
import {
|
|
83
83
|
replaceSection,
|
|
84
84
|
buildCssColorsBlock,
|
|
@@ -114,12 +114,15 @@ import {
|
|
|
114
114
|
const TEMPLATES_DIR = getTemplatesRoot();
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* 템플릿 복사 직후 sh-ui.config.json 의 cssFramework 필드를 갱신.
|
|
117
|
+
* 템플릿 복사 직후 sh-ui.config.json 의 cssFramework + theme.base 필드를 갱신.
|
|
118
118
|
*
|
|
119
119
|
* Flutter 는 cssFramework 가 의미 없으므로 platform=flutter 면 필드 자체를 안 쓴다.
|
|
120
120
|
* Next.js 는 plain/tailwind/css-modules 따라 컴포넌트 변종 결정 + base 파일도 분기 emit.
|
|
121
|
+
*
|
|
122
|
+
* themeBase 는 사용자가 고른 프리셋 이름(neutral/slate/rose/emerald/violet) 또는
|
|
123
|
+
* 커스텀 base64 였을 때 'custom'. null 이면 템플릿 기본값 유지.
|
|
121
124
|
*/
|
|
122
|
-
async function patchShUiConfig(configPath, cssFramework) {
|
|
125
|
+
async function patchShUiConfig(configPath, cssFramework, themeBase) {
|
|
123
126
|
if (!(await fs.pathExists(configPath))) return;
|
|
124
127
|
const config = await fs.readJson(configPath);
|
|
125
128
|
// Flutter 는 cssFramework 무관 — 필드 자체를 두지 않는다.
|
|
@@ -128,6 +131,10 @@ async function patchShUiConfig(configPath, cssFramework) {
|
|
|
128
131
|
} else {
|
|
129
132
|
config.cssFramework = cssFramework ?? CSS_FRAMEWORK_DEFAULT;
|
|
130
133
|
}
|
|
134
|
+
if (themeBase != null) {
|
|
135
|
+
config.theme = config.theme ?? {};
|
|
136
|
+
config.theme.base = themeBase;
|
|
137
|
+
}
|
|
131
138
|
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -208,8 +215,12 @@ export async function createProject(options = {}) {
|
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
let theme = null;
|
|
218
|
+
// themeBase: 'neutral'|'slate'|'rose'|'emerald'|'violet'|'custom'|null.
|
|
219
|
+
// sh-ui.config.json 의 theme.base 가 실제 사용된 팔레트를 반영하게 한다 (이전엔 항상 템플릿 기본값으로 남아 있던 문제).
|
|
220
|
+
let themeBase = null;
|
|
211
221
|
if (options.theme) {
|
|
212
222
|
theme = resolveTheme(options.theme);
|
|
223
|
+
themeBase = THEME_PRESET_NAMES.includes(options.theme) ? options.theme : 'custom';
|
|
213
224
|
} else if (process.stdin.isTTY && !options.yes) {
|
|
214
225
|
// --yes 는 "선택 옵션은 기본값으로" 의미. 테마는 옵션이므로 prompt 우회.
|
|
215
226
|
const NONE = '__none__';
|
|
@@ -224,7 +235,10 @@ export async function createProject(options = {}) {
|
|
|
224
235
|
})),
|
|
225
236
|
],
|
|
226
237
|
});
|
|
227
|
-
if (choice !== NONE)
|
|
238
|
+
if (choice !== NONE) {
|
|
239
|
+
theme = getThemePreset(choice);
|
|
240
|
+
themeBase = choice;
|
|
241
|
+
}
|
|
228
242
|
}
|
|
229
243
|
|
|
230
244
|
// dry-run 은 tmpdir 에 그대로 생성한 뒤 파일 목록 출력 + 정리.
|
|
@@ -250,7 +264,7 @@ export async function createProject(options = {}) {
|
|
|
250
264
|
}
|
|
251
265
|
|
|
252
266
|
if (platform === 'flutter') {
|
|
253
|
-
await generateFlutter(targetDir, projectName, theme, cssFramework);
|
|
267
|
+
await generateFlutter(targetDir, projectName, theme, cssFramework, themeBase);
|
|
254
268
|
await finalizeProject(targetDir, { dryRun: options.dryRun });
|
|
255
269
|
console.log(`\n✅ ${projectName} Flutter 프로젝트가 생성되었습니다!`);
|
|
256
270
|
console.log(`\n cd ${projectName}`);
|
|
@@ -276,9 +290,9 @@ export async function createProject(options = {}) {
|
|
|
276
290
|
plugins.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
277
291
|
|
|
278
292
|
if (projectType === 'standalone') {
|
|
279
|
-
await generateStandalone(targetDir, projectName, plugins, theme, cssFramework, arch);
|
|
293
|
+
await generateStandalone(targetDir, projectName, plugins, theme, cssFramework, arch, themeBase);
|
|
280
294
|
} else {
|
|
281
|
-
await generateMonorepo(targetDir, projectName, plugins, { yes: options.yes, theme, css: cssFramework, arch });
|
|
295
|
+
await generateMonorepo(targetDir, projectName, plugins, { yes: options.yes, theme, css: cssFramework, arch, themeBase });
|
|
282
296
|
}
|
|
283
297
|
|
|
284
298
|
await finalizeProject(targetDir, { dryRun: options.dryRun });
|
|
@@ -444,14 +458,14 @@ export async function addComponent(componentName, appName) {
|
|
|
444
458
|
|
|
445
459
|
// ─── Generators ───
|
|
446
460
|
|
|
447
|
-
async function generateFlutter(targetDir, projectName, theme, css) {
|
|
461
|
+
async function generateFlutter(targetDir, projectName, theme, css, themeBase) {
|
|
448
462
|
await fs.copy(path.join(TEMPLATES_DIR, 'flutter-standalone'), targetDir);
|
|
449
463
|
await replaceInAllFiles(targetDir, '{{project_name}}', projectName);
|
|
450
464
|
await injectDartTheme(targetDir, theme);
|
|
451
|
-
await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css);
|
|
465
|
+
await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
|
|
452
466
|
}
|
|
453
467
|
|
|
454
|
-
async function generateStandalone(targetDir, projectName, plugins, theme, css, arch) {
|
|
468
|
+
async function generateStandalone(targetDir, projectName, plugins, theme, css, arch, themeBase) {
|
|
455
469
|
// 베이스 (arch-neutral) + arch 오버레이 — generateApp 과 같은 패턴.
|
|
456
470
|
await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-standalone'), targetDir, {
|
|
457
471
|
filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
|
|
@@ -488,10 +502,10 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
|
|
|
488
502
|
await applyTransforms(targetDir, plugins, arch);
|
|
489
503
|
await applyCssFrameworkVariant(targetDir, css, { isMonorepo: false, plugins, arch });
|
|
490
504
|
await injectCssTheme(targetDir, theme);
|
|
491
|
-
await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css);
|
|
505
|
+
await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
|
|
492
506
|
}
|
|
493
507
|
|
|
494
|
-
async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch } = {}) {
|
|
508
|
+
async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase } = {}) {
|
|
495
509
|
await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
|
|
496
510
|
|
|
497
511
|
// Update root package.json
|
|
@@ -526,7 +540,7 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
|
|
|
526
540
|
// generateApp 이 ui-{app} 패키지의 cssFramework 변종까지 처리. 여기선 theme + sh-ui.config.json 만.
|
|
527
541
|
const uiAppDir = path.join(targetDir, 'packages', 'ui', 'ui-apps', `ui-${appName}`);
|
|
528
542
|
await injectCssTheme(uiAppDir, theme);
|
|
529
|
-
await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css);
|
|
543
|
+
await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css, themeBase);
|
|
530
544
|
}
|
|
531
545
|
|
|
532
546
|
async function generateApp(targetDir, appName, port, plugins, arch, css = 'tailwind') {
|
|
@@ -567,15 +581,6 @@ async function generateApp(targetDir, appName, port, plugins, arch, css = 'tailw
|
|
|
567
581
|
|
|
568
582
|
await writeNextConfig(targetDir, plugins, { isMonorepo: true, appName, arch });
|
|
569
583
|
|
|
570
|
-
// Update Dockerfile
|
|
571
|
-
const dockerPath = path.join(targetDir, 'Dockerfile');
|
|
572
|
-
if (await fs.pathExists(dockerPath)) {
|
|
573
|
-
let dockerfile = await fs.readFile(dockerPath, 'utf-8');
|
|
574
|
-
dockerfile = dockerfile.replace(/EXPOSE \d+/, `EXPOSE ${port}`);
|
|
575
|
-
dockerfile = dockerfile.replace(/ENV PORT=\d+/, `ENV PORT=${port}`);
|
|
576
|
-
await fs.writeFile(dockerPath, dockerfile);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
584
|
// Create packages/ui/ui-apps/ui-{appName}/ from ui-app-template
|
|
580
585
|
const monorepoRoot = path.resolve(targetDir, '..', '..');
|
|
581
586
|
const uiPkgDir = path.join(monorepoRoot, 'packages', 'ui', 'ui-apps', `ui-${appName}`);
|
package/src/init.mjs
CHANGED
package/src/rename-app.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// monorepo 의 앱 이름 (apps/<old>/ + packages/ui/ui-apps/ui-<old>/) 을 일괄 변경.
|
|
2
2
|
//
|
|
3
|
-
// 디렉토리 이동 + 정해진
|
|
4
|
-
// 6~10 군데 (package.json 이름, tsconfig paths,
|
|
5
|
-
//
|
|
3
|
+
// 디렉토리 이동 + 정해진 패턴 치환을 자동화. 사용자가 손으로
|
|
4
|
+
// 6~10 군데 (package.json 이름, tsconfig paths, next.config transpilePackages,
|
|
5
|
+
// sh-ui.config aliases, README, 필요 시 사용자가 추가한 Dockerfile WORKDIR ...) 를
|
|
6
6
|
// 일일이 갈아엎지 않도록.
|
|
7
7
|
//
|
|
8
8
|
// false-positive 방지를 위해 bare 단어 (`web`) 는 절대 치환하지 않고,
|
|
@@ -14,7 +14,6 @@ UI 컴포넌트는 `@workspace/ui-{name}` 패키지를 참조하며, sh-ui 설
|
|
|
14
14
|
- **next-themes** + **Sonner**
|
|
15
15
|
- **Zod**
|
|
16
16
|
- **Vitest** + **Testing Library**
|
|
17
|
-
- **Docker** 지원
|
|
18
17
|
|
|
19
18
|
## 프로젝트 구조
|
|
20
19
|
|
|
@@ -37,7 +36,6 @@ UI 컴포넌트는 `@workspace/ui-{name}` 패키지를 참조하며, sh-ui 설
|
|
|
37
36
|
├── postcss.config.mjs # @workspace/ui-{name}/postcss.config 재사용
|
|
38
37
|
├── next.config.ts
|
|
39
38
|
├── vitest.config.ts
|
|
40
|
-
├── Dockerfile
|
|
41
39
|
└── .env.example
|
|
42
40
|
```
|
|
43
41
|
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Multi-stage Dockerfile for monorepo Next.js app (output: 'standalone')
|
|
2
|
-
#
|
|
3
|
-
# 빌드 시 monorepo 루트를 컨텍스트로 사용하세요:
|
|
4
|
-
# docker build -f apps/app-name/Dockerfile -t app-name .
|
|
5
|
-
#
|
|
6
|
-
# Next.js 의 `output: 'standalone'` 옵션이 .next/standalone 안에 server.js
|
|
7
|
-
# 와 최소 dependencies 만 추출합니다. 이 Dockerfile 은 그 산출물을 그대로
|
|
8
|
-
# 복사해 final stage 의 이미지 크기를 100MB 이하로 유지합니다.
|
|
9
|
-
|
|
10
|
-
# ── deps + build stage ────────────────────────────────────────────────
|
|
11
|
-
FROM node:22-alpine AS builder
|
|
12
|
-
WORKDIR /repo
|
|
13
|
-
|
|
14
|
-
RUN corepack enable && corepack prepare pnpm@10.4.1 --activate
|
|
15
|
-
|
|
16
|
-
# pnpm-lock 과 manifest 들만 먼저 복사 → 의존성 캐시 hit 률 최대화
|
|
17
|
-
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
|
|
18
|
-
COPY apps/app-name/package.json ./apps/app-name/package.json
|
|
19
|
-
COPY packages packages
|
|
20
|
-
|
|
21
|
-
RUN pnpm install --frozen-lockfile
|
|
22
|
-
|
|
23
|
-
# 소스 전체 복사 후 turbo build (workspace ^build 자동)
|
|
24
|
-
COPY . .
|
|
25
|
-
RUN pnpm turbo build --filter=app-name
|
|
26
|
-
|
|
27
|
-
# ── runtime stage ─────────────────────────────────────────────────────
|
|
28
|
-
FROM node:22-alpine AS runner
|
|
29
|
-
WORKDIR /app
|
|
30
|
-
|
|
31
|
-
ENV NODE_ENV=production
|
|
32
|
-
ENV PORT=3000
|
|
33
|
-
ENV HOSTNAME="0.0.0.0"
|
|
34
|
-
|
|
35
|
-
# Next standalone 산출물 (server.js + node_modules + traced deps)
|
|
36
|
-
COPY --from=builder /repo/apps/app-name/.next/standalone ./
|
|
37
|
-
COPY --from=builder /repo/apps/app-name/.next/static ./apps/app-name/.next/static
|
|
38
|
-
COPY --from=builder /repo/apps/app-name/public ./apps/app-name/public 2>/dev/null || true
|
|
39
|
-
|
|
40
|
-
EXPOSE 3000
|
|
41
|
-
|
|
42
|
-
CMD ["node", "apps/app-name/server.js"]
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# Multi-stage Dockerfile for standalone Next.js app
|
|
2
|
-
#
|
|
3
|
-
# 빌드:
|
|
4
|
-
# docker build -t my-app .
|
|
5
|
-
#
|
|
6
|
-
# Next.js 가 `.next/standalone` 으로 server.js + 최소 dependencies 만 추출하도록
|
|
7
|
-
# next.config.ts 에 `output: 'standalone'` 을 추가하세요.
|
|
8
|
-
|
|
9
|
-
FROM node:22-alpine AS builder
|
|
10
|
-
WORKDIR /app
|
|
11
|
-
|
|
12
|
-
RUN corepack enable && corepack prepare pnpm@10.4.1 --activate
|
|
13
|
-
|
|
14
|
-
COPY pnpm-lock.yaml package.json ./
|
|
15
|
-
RUN pnpm install --frozen-lockfile
|
|
16
|
-
|
|
17
|
-
COPY . .
|
|
18
|
-
RUN pnpm build
|
|
19
|
-
|
|
20
|
-
FROM node:22-alpine AS runner
|
|
21
|
-
WORKDIR /app
|
|
22
|
-
|
|
23
|
-
ENV NODE_ENV=production
|
|
24
|
-
ENV PORT=3000
|
|
25
|
-
ENV HOSTNAME="0.0.0.0"
|
|
26
|
-
|
|
27
|
-
# `output: 'standalone'` 이 활성일 때 server.js + 추적된 deps 가 .next/standalone 에 생성됨.
|
|
28
|
-
# 옵션 비활성 시엔 .next/ 전체를 복사하고 `next start` 로 실행해야 합니다.
|
|
29
|
-
COPY --from=builder /app/.next/standalone ./
|
|
30
|
-
COPY --from=builder /app/.next/static ./.next/static
|
|
31
|
-
COPY --from=builder /app/public ./public 2>/dev/null || true
|
|
32
|
-
|
|
33
|
-
EXPOSE 3000
|
|
34
|
-
|
|
35
|
-
CMD ["node", "server.js"]
|