sh-ui-cli 0.90.0 → 0.92.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 +27 -0
- package/package.json +1 -1
- package/src/api.d.ts +4 -0
- package/src/constants.js +6 -0
- package/src/create/cli-args.js +5 -1
- package/src/create/describeTemplate.js +39 -0
- package/src/create/generator.js +252 -8
- package/src/create/index.mjs +9 -1
- package/src/mcp.mjs +61 -2
|
@@ -2,6 +2,33 @@
|
|
|
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.92.0",
|
|
7
|
+
"date": "2026-05-14",
|
|
8
|
+
"title": "vite 프리셋 react-i18next i18n opt-in (--i18n + --locales)",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`--i18n react-i18next` 옵션** — vite preset 에 opt-in i18n 셋업 추가. `sh-ui-cli create --platform vite --i18n react-i18next --locales ko,en` 한 줄로 i18next + react-i18next + browser-languagedetector + http-backend 셋업 + `I18nProvider` + locale JSON 자동 emit. 디폴트 `'none'` 이라 기존 사용자 영향 X. ai-org 처럼 i18n 부터 들어가는 워크로드의 30 초 boilerplate 제거.",
|
|
12
|
+
"**Arch-aware 파일 위치** — fsd 일 때 `src/shared/i18n/` + `src/app/providers/I18nProvider.tsx`, flat 일 때 `src/lib/i18n/` + `src/components/providers/I18nProvider.tsx`. `emitI18n` 헬퍼가 arch descriptor 의 `paths`/`aliases` 기반 경로 자동 결정. 사용자가 `import i18n from '@/shared/i18n'` 또는 `'@/lib/i18n'` 즉시 가능.",
|
|
13
|
+
"**Monorepo + Tauri + i18n 조합 모두 동작** — `--structure monorepo --tauri --i18n react-i18next` 한 번에 OK. `apps/{appName}/` 안에 `src-tauri/` 와 `src/shared/i18n/` 가 같이 emit, 각 의존성이 app 의 `package.json` 에 정확히. `sh-ui-cli add-app` 도 `--i18n` 받음.",
|
|
14
|
+
"**Locale 시드 정책** — 첫 locale (fallbackLng) 만 `{ greeting: 'Hello World', app_title: 'sh-ui app' }` 시드, 나머지는 `{}` 빈 객체로 시작. 사용자가 키 채우는 흐름 자연스럽게. backend (`i18next-http-backend`) 가 `/locales/{lng}/{ns}.json` lazy-load 하므로 dev 부터 namespace 분리 + lazy 동작.",
|
|
15
|
+
"**가드 + 회귀** — `i18n + platform !== 'vite'` 조합은 CLI + MCP 양쪽에서 명시적 Korean 에러. Smoke V15 (fsd + ko,en happy path), V16 (i18n=none 디폴트 회귀 가드 — 어떤 i18n 파일도 안 들어감), V17 (flat + ko,en,ja — 3개 locale 중 첫 locale 만 시드, ja 는 빈 객체). docs `/create` 페이지도 i18n 토글 + locales 인풋 노출."
|
|
16
|
+
],
|
|
17
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.92.0"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.91.0",
|
|
21
|
+
"date": "2026-05-14",
|
|
22
|
+
"title": "add-app 가 platform (next/vite) + tauri 인식 — vite monorepo 에 새 앱 정상 추가",
|
|
23
|
+
"type": "minor",
|
|
24
|
+
"highlights": [
|
|
25
|
+
"**`sh-ui-cli add-app` + MCP `sh_ui_add_app` 가 `platform: vite | next` 인식** — 이전엔 Next.js 하드코딩이라 vite monorepo (v0.87.0+) 에 `add-app` 하면 잘못된 Next.js 앱이 들어가던 빈틈 해소. `--platform` 미지정 시 기존 `apps/*` 의 `package.json` deps 스캔해서 platform 추론 (모든 앱이 같은 플랫폼이면 그 값으로). 혼재 or 빈 apps/ 면 TTY 면 prompt, 비대화형이면 'next' fallback.",
|
|
26
|
+
"**`--tauri` 옵션이 add-app 에도 동작** — `sh-ui-cli add-app admin --tauri` 또는 MCP `tauri: true` 로 `apps/{name}/src-tauri/` 까지 한 번에 emit. v0.90.0 의 monorepo+tauri 와 동일하게 dev port 가 `tauri.conf.json` 의 `devUrl` 과 `vite.config.ts` 의 `server.port` 양쪽에 자동 박힘. tauri + platform=next 조합은 CLI + MCP 양쪽에서 명시적 Korean 에러.",
|
|
27
|
+
"**Smoke V13 + V14 회귀 가드** — V13 은 vite monorepo create 후 `add-app admin --port 3001 --tauri` (platform 미지정) 가 추론으로 vite 선택 + apps/admin/src-tauri/ + devUrl=3001 + crate name 까지 단언. V14 는 next monorepo 에 `add-app --platform next --tauri` 가 에러 throw 확인. 기존 scenarios 4 + 4b 도 새 platform 추론 분기에 맞춰 prompt 모의 갱신.",
|
|
28
|
+
"**Backward-compat 확인** — Next.js 모노레포에서 기존 `add-app` 호출 흐름은 platform 추론 결과 'next' 가 나와 동일 동작. 깨지는 호출 없음."
|
|
29
|
+
],
|
|
30
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.91.0"
|
|
31
|
+
},
|
|
5
32
|
{
|
|
6
33
|
"version": "0.90.0",
|
|
7
34
|
"date": "2026-05-14",
|
package/package.json
CHANGED
package/src/api.d.ts
CHANGED
|
@@ -141,6 +141,10 @@ export interface DescribeTemplateOptions {
|
|
|
141
141
|
appName?: string;
|
|
142
142
|
/** platform=vite + structure=standalone 일 때 Tauri 2.x 셸(`src-tauri/`) 동시 emit. */
|
|
143
143
|
tauri?: boolean;
|
|
144
|
+
/** vite 전용 — react-i18next opt-in. v0.92.0+ */
|
|
145
|
+
i18n?: 'none' | 'react-i18next';
|
|
146
|
+
/** i18n 활성화 시 생성할 locale 코드 (comma-separated). 기본 'ko,en' */
|
|
147
|
+
locales?: string;
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
export interface DescribeTemplateGroup {
|
package/src/constants.js
CHANGED
|
@@ -54,3 +54,9 @@ export const INIT_DEFAULTS = {
|
|
|
54
54
|
mode: 'light-dark',
|
|
55
55
|
cssFramework: CSS_FRAMEWORK_DEFAULT,
|
|
56
56
|
};
|
|
57
|
+
|
|
58
|
+
// ─── i18n (vite preset 전용 — v0.92.0+) ───
|
|
59
|
+
// 'none' 디폴트 — opt-in 으로 react-i18next 설치 + 셋업. 향후 lingui / react-intl 등 추가 시 enum 확장.
|
|
60
|
+
export const I18N_LIBRARIES = ['none', 'react-i18next'];
|
|
61
|
+
export const I18N_DEFAULT = 'none';
|
|
62
|
+
export const I18N_DEFAULT_LOCALES = 'ko,en';
|
package/src/create/cli-args.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CREATE_STRUCTURES,
|
|
4
4
|
CSS_FRAMEWORKS_SUPPORTED,
|
|
5
5
|
CSS_FRAMEWORKS_PLANNED,
|
|
6
|
+
I18N_LIBRARIES,
|
|
6
7
|
} from '../constants.js';
|
|
7
8
|
import { allPlugins } from './plugins/index.js';
|
|
8
9
|
import { allArchitectures } from './architectures/index.js';
|
|
@@ -12,7 +13,7 @@ const VALID_STRUCTURES = CREATE_STRUCTURES;
|
|
|
12
13
|
const VALID_PLUGINS = allPlugins.map((p) => p.name);
|
|
13
14
|
const VALID_ARCHES = allArchitectures.map((a) => a.name);
|
|
14
15
|
|
|
15
|
-
const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css', 'arch', 'port'];
|
|
16
|
+
const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css', 'arch', 'port', 'i18n', 'locales'];
|
|
16
17
|
const BOOL_FLAGS = ['yes', 'help', 'dry-run', 'tauri'];
|
|
17
18
|
|
|
18
19
|
const SUBCOMMANDS = ['add-app', 'add-component'];
|
|
@@ -75,6 +76,9 @@ export const parseArgs = (argv) => {
|
|
|
75
76
|
`--arch 는 ${VALID_ARCHES.join('/')} 중 하나여야 함 (받은 값: ${value})`,
|
|
76
77
|
);
|
|
77
78
|
}
|
|
79
|
+
if (name === 'i18n' && !I18N_LIBRARIES.includes(value)) {
|
|
80
|
+
throw new Error(`--i18n 은 ${I18N_LIBRARIES.join('/')} 중 하나여야 함 (받은 값: ${value})`);
|
|
81
|
+
}
|
|
78
82
|
if (name === 'css' && !CSS_FRAMEWORKS_SUPPORTED.includes(value)) {
|
|
79
83
|
// planned 값은 '곧 옵니다' 신호로 분기 — 사용자 의도가 더 명확히 전달.
|
|
80
84
|
if (CSS_FRAMEWORKS_PLANNED.includes(value)) {
|
|
@@ -36,6 +36,8 @@ import { CSS_FRAMEWORK_DEFAULT } from '../constants.js';
|
|
|
36
36
|
* @property {string} [projectName]
|
|
37
37
|
* @property {string} [appName] monorepo 첫 앱 이름. 기본 'web'
|
|
38
38
|
* @property {boolean} [tauri] platform=vite (standalone/monorepo 둘 다) 일 때 Tauri 2.x 셸 같이 emit
|
|
39
|
+
* @property {'none'|'react-i18next'} [i18n] vite 전용 — react-i18next 셋업
|
|
40
|
+
* @property {string} [locales] i18n 활성화 시 생성할 locale 코드 (comma-separated, default 'ko,en')
|
|
39
41
|
*/
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -64,6 +66,8 @@ export function describeTemplate(opts = {}) {
|
|
|
64
66
|
cssFramework = CSS_FRAMEWORK_DEFAULT,
|
|
65
67
|
appName: rawAppName = 'web',
|
|
66
68
|
tauri = false,
|
|
69
|
+
i18n = 'none',
|
|
70
|
+
locales = 'ko,en',
|
|
67
71
|
} = opts;
|
|
68
72
|
|
|
69
73
|
if (platform === 'flutter') {
|
|
@@ -99,6 +103,19 @@ export function describeTemplate(opts = {}) {
|
|
|
99
103
|
const tauriFiles = tauriTpl.base.map((p) => `src-tauri/${p}`);
|
|
100
104
|
groups.push(makeGroup('tauri', 'Tauri 셸 (src-tauri/)', tauriFiles));
|
|
101
105
|
}
|
|
106
|
+
if (i18n === 'react-i18next') {
|
|
107
|
+
const isFsd = safeArchName === 'fsd';
|
|
108
|
+
const i18nBase = isFsd ? 'src/shared/i18n' : 'src/lib/i18n';
|
|
109
|
+
const providersBase = isFsd ? 'src/app/providers' : 'src/components/providers';
|
|
110
|
+
const localesArr = parseLocalesString(locales);
|
|
111
|
+
const i18nFiles = [
|
|
112
|
+
`${i18nBase}/config.ts`,
|
|
113
|
+
`${i18nBase}/index.ts`,
|
|
114
|
+
...localesArr.map((lng) => `${i18nBase}/locales/${lng}/common.json`),
|
|
115
|
+
`${providersBase}/I18nProvider.tsx`,
|
|
116
|
+
];
|
|
117
|
+
groups.push(makeGroup('i18n', `i18n (${i18n})`, i18nFiles));
|
|
118
|
+
}
|
|
102
119
|
return finalize(groups);
|
|
103
120
|
}
|
|
104
121
|
|
|
@@ -149,6 +166,20 @@ export function describeTemplate(opts = {}) {
|
|
|
149
166
|
groups.push(makeGroup('tauri', `Tauri 셸 (apps/${appName}/src-tauri/)`, tauriFiles));
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
if (i18n === 'react-i18next') {
|
|
170
|
+
const isFsd = safeArchName === 'fsd';
|
|
171
|
+
const i18nBase = isFsd ? 'src/shared/i18n' : 'src/lib/i18n';
|
|
172
|
+
const providersBase = isFsd ? 'src/app/providers' : 'src/components/providers';
|
|
173
|
+
const localesArr = parseLocalesString(locales);
|
|
174
|
+
const i18nFiles = [
|
|
175
|
+
`apps/${appName}/${i18nBase}/config.ts`,
|
|
176
|
+
`apps/${appName}/${i18nBase}/index.ts`,
|
|
177
|
+
...localesArr.map((lng) => `apps/${appName}/${i18nBase}/locales/${lng}/common.json`),
|
|
178
|
+
`apps/${appName}/${providersBase}/I18nProvider.tsx`,
|
|
179
|
+
];
|
|
180
|
+
groups.push(makeGroup('i18n', `i18n (${i18n}, apps/${appName}/)`, i18nFiles));
|
|
181
|
+
}
|
|
182
|
+
|
|
152
183
|
return finalize(groups);
|
|
153
184
|
}
|
|
154
185
|
|
|
@@ -326,6 +357,14 @@ function makeGroup(id, label, paths) {
|
|
|
326
357
|
return { id, label, paths: paths.slice() };
|
|
327
358
|
}
|
|
328
359
|
|
|
360
|
+
/** locales (string or string[]) 를 정규화 — generator.js 의 parseLocales 와 동일 규칙. */
|
|
361
|
+
function parseLocalesString(s) {
|
|
362
|
+
if (Array.isArray(s)) return s;
|
|
363
|
+
if (typeof s !== 'string') return ['ko', 'en'];
|
|
364
|
+
const arr = s.split(',').map((x) => x.trim().toLowerCase()).filter((x) => /^[a-z]{2}(-[a-z]{2})?$/i.test(x));
|
|
365
|
+
return arr.length > 0 ? arr : ['ko', 'en'];
|
|
366
|
+
}
|
|
367
|
+
|
|
329
368
|
/**
|
|
330
369
|
* 후처리: 그룹 간 dedupe (같은 path 가 여러 그룹에 있으면 마지막 그룹이 소유),
|
|
331
370
|
* 그룹 안에서 path 정렬, 빈 그룹 제거, 전체 파일 목록 계산.
|
package/src/create/generator.js
CHANGED
|
@@ -224,6 +224,13 @@ export async function createProject(options = {}) {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// i18n 옵션도 vite preset 전용. v0.92.0+.
|
|
228
|
+
if (options.i18n && options.i18n !== 'none' && platform !== 'vite') {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`i18n='${options.i18n}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}). --i18n none 또는 --platform vite 사용.`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
227
234
|
// arch 결정 — platform 확정 후. 사용자가 --arch 미지정 시:
|
|
228
235
|
// - next → DEFAULT_ARCH ('fsd')
|
|
229
236
|
// - flutter → 현재 Flutter arch 디스크립터 없음 → null. 미래에 flutter arch 추가되면
|
|
@@ -344,9 +351,19 @@ export async function createProject(options = {}) {
|
|
|
344
351
|
});
|
|
345
352
|
|
|
346
353
|
if (projectType === 'standalone') {
|
|
347
|
-
await generateViteStandalone(targetDir, projectName, theme, cssFramework, arch, themeBase, {
|
|
354
|
+
await generateViteStandalone(targetDir, projectName, theme, cssFramework, arch, themeBase, {
|
|
355
|
+
tauri: !!options.tauri,
|
|
356
|
+
i18n: options.i18n ?? 'none',
|
|
357
|
+
locales: options.locales ?? 'ko,en',
|
|
358
|
+
});
|
|
348
359
|
} else {
|
|
349
|
-
await generateMonorepo(targetDir, projectName, [], {
|
|
360
|
+
await generateMonorepo(targetDir, projectName, [], {
|
|
361
|
+
yes: options.yes, theme, css: cssFramework, arch, themeBase,
|
|
362
|
+
platform: 'vite',
|
|
363
|
+
tauri: options.tauri,
|
|
364
|
+
i18n: options.i18n ?? 'none',
|
|
365
|
+
locales: options.locales ?? 'ko,en',
|
|
366
|
+
});
|
|
350
367
|
}
|
|
351
368
|
|
|
352
369
|
await finalizeProject(targetDir, { dryRun: options.dryRun });
|
|
@@ -477,6 +494,42 @@ export async function addApp(options = {}) {
|
|
|
477
494
|
throw new Error('비대화형 환경(TTY 없음)에서는 name 이 필요합니다.');
|
|
478
495
|
}
|
|
479
496
|
|
|
497
|
+
// platform 결정 — 명시 시 그대로, 미지정 시 기존 apps/* 의 package.json 스캔으로 추론.
|
|
498
|
+
let platform = options.platform;
|
|
499
|
+
if (!platform) {
|
|
500
|
+
platform = await inferMonorepoPlatform(cwd);
|
|
501
|
+
if (!platform) {
|
|
502
|
+
// 모노레포에 아직 앱이 없거나 혼재 — 안전한 디폴트 (next 가 기존 동작).
|
|
503
|
+
// 첫 앱은 user 가 명시하는 게 권장 (interactive prompt).
|
|
504
|
+
if (process.stdin.isTTY) {
|
|
505
|
+
platform = await select({
|
|
506
|
+
message: '플랫폼:',
|
|
507
|
+
choices: [
|
|
508
|
+
{ name: 'Next.js', value: 'next' },
|
|
509
|
+
{ name: 'Vite (SPA)', value: 'vite' },
|
|
510
|
+
],
|
|
511
|
+
});
|
|
512
|
+
} else {
|
|
513
|
+
platform = 'next';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (platform !== 'next' && platform !== 'vite') {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`add-app 는 platform=next 또는 vite 만 지원 (받은 값: ${platform}). flutter 는 standalone 만 지원.`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (options.tauri && platform !== 'vite') {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`tauri 는 platform=vite 일 때만 지원합니다 (현재 platform=${platform}). --platform vite 사용 또는 tauri 옵션 제거.`,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
if (options.i18n && options.i18n !== 'none' && platform !== 'vite') {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`i18n='${options.i18n}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}).`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
480
533
|
const appName = validateProjectName(
|
|
481
534
|
options.name ?? await input({
|
|
482
535
|
message: '앱 이름:',
|
|
@@ -523,9 +576,17 @@ export async function addApp(options = {}) {
|
|
|
523
576
|
// arch 는 모노레포가 처음 만들어질 때 정한 값과 같아야 한다. root config 에 별도
|
|
524
577
|
// 저장 안 해 두므로 일단 DEFAULT_ARCH (fsd) fallback. 향후 root config 에 arch
|
|
525
578
|
// 박아두고 여기서 읽어오는 흐름으로 개선 가능.
|
|
526
|
-
const arch = assertArchPlatformCompat(DEFAULT_ARCH,
|
|
579
|
+
const arch = assertArchPlatformCompat(DEFAULT_ARCH, platform);
|
|
527
580
|
|
|
528
|
-
|
|
581
|
+
if (platform === 'vite') {
|
|
582
|
+
await generateViteApp(appsDir, appName, port, arch, css, {
|
|
583
|
+
tauri: !!options.tauri,
|
|
584
|
+
i18n: options.i18n ?? 'none',
|
|
585
|
+
locales: options.locales ?? 'ko,en',
|
|
586
|
+
});
|
|
587
|
+
} else {
|
|
588
|
+
await generateApp(appsDir, appName, port, plugins, arch, css);
|
|
589
|
+
}
|
|
529
590
|
|
|
530
591
|
// monorepo 의 새 ui-app 패키지 — tokens-only role + theme 주입 + cssFramework 패치.
|
|
531
592
|
// generateMonorepo 의 흐름과 정확히 동일.
|
|
@@ -538,6 +599,41 @@ export async function addApp(options = {}) {
|
|
|
538
599
|
console.log(`\n✅ apps/${appName} 이 추가되었습니다!`);
|
|
539
600
|
console.log('\n pnpm install');
|
|
540
601
|
console.log(` pnpm --filter ${appName} dev\n`);
|
|
602
|
+
|
|
603
|
+
if (platform === 'vite' && options.tauri) {
|
|
604
|
+
console.log('Tauri 데스크탑 셸:');
|
|
605
|
+
console.log(` cd apps/${appName}`);
|
|
606
|
+
console.log(' pnpm tauri dev # Rust 처음 빌드는 5~10분 — 캐시 후 5~10초');
|
|
607
|
+
console.log(' (Rust 미설치 시 https://rustup.rs/ 참고)\n');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 모노레포의 기존 apps/* 들 package.json 을 스캔해 platform 추론.
|
|
613
|
+
* - 모든 앱이 vite — 'vite'
|
|
614
|
+
* - 모든 앱이 next — 'next'
|
|
615
|
+
* - 혼재 또는 다른 조합 — null (user 가 명시해야 함)
|
|
616
|
+
* - apps/ 가 없거나 비었으면 null
|
|
617
|
+
*/
|
|
618
|
+
async function inferMonorepoPlatform(cwd) {
|
|
619
|
+
const appsDir = path.resolve(cwd, 'apps');
|
|
620
|
+
if (!(await fs.pathExists(appsDir))) return null;
|
|
621
|
+
const entries = await fsp.readdir(appsDir, { withFileTypes: true });
|
|
622
|
+
const apps = entries.filter((e) => e.isDirectory());
|
|
623
|
+
if (apps.length === 0) return null;
|
|
624
|
+
|
|
625
|
+
const platforms = new Set();
|
|
626
|
+
for (const app of apps) {
|
|
627
|
+
const pkgPath = path.join(appsDir, app.name, 'package.json');
|
|
628
|
+
if (!(await fs.pathExists(pkgPath))) continue;
|
|
629
|
+
const pkg = await fs.readJson(pkgPath);
|
|
630
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
631
|
+
if (deps.next) platforms.add('next');
|
|
632
|
+
else if (deps.vite) platforms.add('vite');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (platforms.size === 1) return [...platforms][0];
|
|
636
|
+
return null;
|
|
541
637
|
}
|
|
542
638
|
|
|
543
639
|
// ─── Add component to ui packages ───
|
|
@@ -750,7 +846,7 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
|
|
|
750
846
|
await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
|
|
751
847
|
}
|
|
752
848
|
|
|
753
|
-
async function generateViteStandalone(targetDir, projectName, theme, css, arch, themeBase, { tauri = false } = {}) {
|
|
849
|
+
async function generateViteStandalone(targetDir, projectName, theme, css, arch, themeBase, { tauri = false, i18n = 'none', locales = 'ko,en' } = {}) {
|
|
754
850
|
// 베이스 (arch-neutral) + arch 오버레이 — generateStandalone 과 같은 패턴.
|
|
755
851
|
await fs.copy(path.join(TEMPLATES_DIR, 'vite-standalone'), targetDir, {
|
|
756
852
|
filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
|
|
@@ -791,6 +887,11 @@ async function generateViteStandalone(targetDir, projectName, theme, css, arch,
|
|
|
791
887
|
await emitTauri(targetDir, projectName, { devPort: 5173 });
|
|
792
888
|
await patchViteForTauri(targetDir, { port: 5173 });
|
|
793
889
|
}
|
|
890
|
+
|
|
891
|
+
if (i18n === 'react-i18next') {
|
|
892
|
+
const localesArr = parseLocales(locales);
|
|
893
|
+
await emitI18n(targetDir, { arch, locales: localesArr });
|
|
894
|
+
}
|
|
794
895
|
}
|
|
795
896
|
|
|
796
897
|
/**
|
|
@@ -888,7 +989,145 @@ export default defineConfig({
|
|
|
888
989
|
}
|
|
889
990
|
}
|
|
890
991
|
|
|
891
|
-
|
|
992
|
+
/**
|
|
993
|
+
* react-i18next 셋업을 emit. arch 에 따라 경로가 달라짐:
|
|
994
|
+
* - fsd: src/shared/i18n/* + src/app/providers/I18nProvider.tsx
|
|
995
|
+
* - flat: src/lib/i18n/* + src/components/providers/I18nProvider.tsx
|
|
996
|
+
*
|
|
997
|
+
* 1. i18n/config.ts + i18n/index.ts + locales/{locale}/common.json 생성
|
|
998
|
+
* 2. providers/I18nProvider.tsx 생성 (<I18nextProvider> wrapper)
|
|
999
|
+
* 3. GlobalProvider/index.tsx 를 I18nProvider 로 wrapping 하도록 rewrite
|
|
1000
|
+
* 4. package.json 에 i18next 패키지 deps 추가
|
|
1001
|
+
*
|
|
1002
|
+
* @param {string} targetDir — 앱 디렉토리 (standalone 이면 프로젝트 루트, monorepo 이면 apps/{name}/)
|
|
1003
|
+
* @param {object} opts
|
|
1004
|
+
* @param {object} opts.arch — arch descriptor (name + paths/aliases)
|
|
1005
|
+
* @param {string[]} opts.locales — 생성할 locale 코드 배열 (예: ['ko', 'en'])
|
|
1006
|
+
*/
|
|
1007
|
+
async function emitI18n(targetDir, { arch, locales }) {
|
|
1008
|
+
if (!locales || locales.length === 0) {
|
|
1009
|
+
throw new Error('emitI18n: locales 배열이 비어 있습니다.');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const isFsd = arch.name === 'fsd';
|
|
1013
|
+
const i18nDirRel = isFsd ? 'src/shared/i18n' : 'src/lib/i18n';
|
|
1014
|
+
const i18nAlias = isFsd ? '@/shared/i18n' : '@/lib/i18n';
|
|
1015
|
+
const providersDirRel = isFsd ? 'src/app/providers' : 'src/components/providers';
|
|
1016
|
+
const apiAlias = isFsd ? '@/shared/api/queryClient' : '@/lib/api/queryClient';
|
|
1017
|
+
|
|
1018
|
+
const i18nDir = path.join(targetDir, i18nDirRel);
|
|
1019
|
+
await fs.ensureDir(i18nDir);
|
|
1020
|
+
await fs.ensureDir(path.join(i18nDir, 'locales'));
|
|
1021
|
+
|
|
1022
|
+
const localesArr = JSON.stringify(locales);
|
|
1023
|
+
const fallbackLng = locales[0];
|
|
1024
|
+
const configTs = `import i18n from 'i18next';
|
|
1025
|
+
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
1026
|
+
import HttpBackend from 'i18next-http-backend';
|
|
1027
|
+
import { initReactI18next } from 'react-i18next';
|
|
1028
|
+
|
|
1029
|
+
// 클라이언트 측 lazy-load — 빌드 산출물에서 public/locales/{lng}/{ns}.json 경로로 fetch.
|
|
1030
|
+
// dev 에선 vite 가 ${i18nDirRel}/locales/* 를 /locales 로 serve (vite.config 의 publicDir 기본 'public').
|
|
1031
|
+
// 프로덕션 빌드 시 사용자가 vite-plugin-static-copy 등으로 public/locales 로 카피하거나
|
|
1032
|
+
// 처음부터 public/locales 에 두면 됨. 디폴트 경로 ${i18nDirRel}/locales 는 dev 편의용.
|
|
1033
|
+
|
|
1034
|
+
i18n
|
|
1035
|
+
.use(HttpBackend)
|
|
1036
|
+
.use(LanguageDetector)
|
|
1037
|
+
.use(initReactI18next)
|
|
1038
|
+
.init({
|
|
1039
|
+
fallbackLng: '${fallbackLng}',
|
|
1040
|
+
supportedLngs: ${localesArr},
|
|
1041
|
+
ns: ['common'],
|
|
1042
|
+
defaultNS: 'common',
|
|
1043
|
+
interpolation: { escapeValue: false },
|
|
1044
|
+
backend: {
|
|
1045
|
+
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
|
1046
|
+
},
|
|
1047
|
+
detection: {
|
|
1048
|
+
order: ['localStorage', 'navigator', 'htmlTag'],
|
|
1049
|
+
caches: ['localStorage'],
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
export default i18n;
|
|
1054
|
+
`;
|
|
1055
|
+
await fs.writeFile(path.join(i18nDir, 'config.ts'), configTs);
|
|
1056
|
+
|
|
1057
|
+
await fs.writeFile(
|
|
1058
|
+
path.join(i18nDir, 'index.ts'),
|
|
1059
|
+
`export { default } from './config';\n`,
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
for (const lng of locales) {
|
|
1063
|
+
const localeDir = path.join(i18nDir, 'locales', lng);
|
|
1064
|
+
await fs.ensureDir(localeDir);
|
|
1065
|
+
const seed = lng === fallbackLng
|
|
1066
|
+
? { greeting: 'Hello World', app_title: 'sh-ui app' }
|
|
1067
|
+
: {};
|
|
1068
|
+
await fs.writeFile(
|
|
1069
|
+
path.join(localeDir, 'common.json'),
|
|
1070
|
+
JSON.stringify(seed, null, 2) + '\n',
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const providersDir = path.join(targetDir, providersDirRel);
|
|
1075
|
+
await fs.ensureDir(providersDir);
|
|
1076
|
+
const i18nProvider = `import { type ReactNode } from 'react';
|
|
1077
|
+
import { I18nextProvider } from 'react-i18next';
|
|
1078
|
+
import i18n from '${i18nAlias}';
|
|
1079
|
+
|
|
1080
|
+
export function I18nProvider({ children }: { children: ReactNode }) {
|
|
1081
|
+
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
|
1082
|
+
}
|
|
1083
|
+
`;
|
|
1084
|
+
await fs.writeFile(path.join(providersDir, 'I18nProvider.tsx'), i18nProvider);
|
|
1085
|
+
|
|
1086
|
+
const globalProviderPath = path.join(targetDir, providersDirRel, 'GlobalProvider', 'index.tsx');
|
|
1087
|
+
const globalProvider = `import { QueryClientProvider } from '@tanstack/react-query';
|
|
1088
|
+
import { type ReactNode, useState } from 'react';
|
|
1089
|
+
import { createQueryClient } from '${apiAlias}';
|
|
1090
|
+
import { ThemeProvider } from '../theme/ThemeProvider';
|
|
1091
|
+
import { I18nProvider } from '../I18nProvider';
|
|
1092
|
+
|
|
1093
|
+
export function GlobalProvider({ children }: { children: ReactNode }) {
|
|
1094
|
+
const [queryClient] = useState(() => createQueryClient());
|
|
1095
|
+
return (
|
|
1096
|
+
<I18nProvider>
|
|
1097
|
+
<ThemeProvider>
|
|
1098
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
1099
|
+
</ThemeProvider>
|
|
1100
|
+
</I18nProvider>
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
`;
|
|
1104
|
+
await fs.writeFile(globalProviderPath, globalProvider);
|
|
1105
|
+
|
|
1106
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
1107
|
+
const pkg = await fs.readJson(pkgPath);
|
|
1108
|
+
pkg.dependencies = pkg.dependencies ?? {};
|
|
1109
|
+
pkg.dependencies['i18next'] = '^23.15.1';
|
|
1110
|
+
pkg.dependencies['i18next-browser-languagedetector'] = '^8.0.0';
|
|
1111
|
+
pkg.dependencies['i18next-http-backend'] = '^2.7.1';
|
|
1112
|
+
pkg.dependencies['react-i18next'] = '^15.1.0';
|
|
1113
|
+
pkg.dependencies = sortObjectKeys(pkg.dependencies);
|
|
1114
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* locales 인자 (string or string[]) 를 정규화. comma-separated 또는 array 모두 받음.
|
|
1119
|
+
* 빈 값 / 잘못된 형식이면 디폴트 ['ko', 'en'] fallback.
|
|
1120
|
+
*/
|
|
1121
|
+
function parseLocales(input) {
|
|
1122
|
+
let arr;
|
|
1123
|
+
if (Array.isArray(input)) arr = input;
|
|
1124
|
+
else if (typeof input === 'string') arr = input.split(',');
|
|
1125
|
+
else arr = ['ko', 'en'];
|
|
1126
|
+
const cleaned = arr.map((s) => s.trim().toLowerCase()).filter((s) => /^[a-z]{2}(-[a-z]{2})?$/i.test(s));
|
|
1127
|
+
return cleaned.length > 0 ? cleaned : ['ko', 'en'];
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase, platform = 'next', tauri = false, i18n = 'none', locales = 'ko,en' } = {}) {
|
|
892
1131
|
await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
|
|
893
1132
|
|
|
894
1133
|
// Update root package.json
|
|
@@ -920,7 +1159,7 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
|
|
|
920
1159
|
|
|
921
1160
|
const appsDir = path.join(targetDir, 'apps', appName);
|
|
922
1161
|
if (platform === 'vite') {
|
|
923
|
-
await generateViteApp(appsDir, appName, port, arch, css, { tauri });
|
|
1162
|
+
await generateViteApp(appsDir, appName, port, arch, css, { tauri, i18n, locales });
|
|
924
1163
|
} else {
|
|
925
1164
|
await generateApp(appsDir, appName, port, plugins, arch, css);
|
|
926
1165
|
}
|
|
@@ -1021,7 +1260,7 @@ async function generateApp(targetDir, appName, port, plugins, arch, css = 'tailw
|
|
|
1021
1260
|
}
|
|
1022
1261
|
}
|
|
1023
1262
|
|
|
1024
|
-
async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind', { tauri = false } = {}) {
|
|
1263
|
+
async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind', { tauri = false, i18n = 'none', locales = 'ko,en' } = {}) {
|
|
1025
1264
|
// 베이스 (arch-neutral) + arch 오버레이 — generateApp 과 동일 패턴.
|
|
1026
1265
|
await fs.copy(path.join(TEMPLATES_DIR, 'vite-app'), targetDir, {
|
|
1027
1266
|
filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
|
|
@@ -1089,6 +1328,11 @@ async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind',
|
|
|
1089
1328
|
await emitTauri(targetDir, appName, { devPort });
|
|
1090
1329
|
await patchViteForTauri(targetDir, { port: devPort });
|
|
1091
1330
|
}
|
|
1331
|
+
|
|
1332
|
+
if (i18n === 'react-i18next') {
|
|
1333
|
+
const localesArr = parseLocales(locales);
|
|
1334
|
+
await emitI18n(targetDir, { arch, locales: localesArr });
|
|
1335
|
+
}
|
|
1092
1336
|
}
|
|
1093
1337
|
|
|
1094
1338
|
/**
|
package/src/create/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
|
|
|
18
18
|
|
|
19
19
|
사용법:
|
|
20
20
|
sh-ui create [name] [options]
|
|
21
|
-
sh-ui create add-app [name] [--port <n>] [--plugins ..] [--theme ..] [--css ..]
|
|
21
|
+
sh-ui create add-app [name] [--port <n>] [--platform <next|vite>] [--plugins ..] [--theme ..] [--css ..] [--tauri] [--i18n <react-i18next|none>] [--locales ko,en]
|
|
22
22
|
sh-ui create add-component <name> [--app <name>]
|
|
23
23
|
|
|
24
24
|
옵션:
|
|
@@ -28,6 +28,8 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
|
|
|
28
28
|
--plugins <a,b> 플러그인 (${PLUGINS_LIST}). 미지정/"" → 없음
|
|
29
29
|
--theme <preset|base64> 프리셋 이름(${THEME_PRESETS_LIST}) 또는 playground base64. 선택
|
|
30
30
|
--css <${CSS_FRAMEWORKS_SUPPORTED.join('|')}> CSS 프레임워크. base 파일까지 분기 emit (tailwind/plain/css-modules)
|
|
31
|
+
--i18n <react-i18next|none> vite 전용 — react-i18next 셋업 emit (i18n config + I18nProvider). 기본 none (v0.92.0+)
|
|
32
|
+
--locales <ko,en> i18n 활성화 시 생성할 locale 코드 (comma-separated). 기본 'ko,en'
|
|
31
33
|
--yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
|
|
32
34
|
--dry-run 파일을 쓰지 않고 작성될 파일 목록만 출력
|
|
33
35
|
-h, --help 이 도움말
|
|
@@ -73,6 +75,10 @@ export async function runCreate(rest) {
|
|
|
73
75
|
plugins: flags.plugins,
|
|
74
76
|
theme: flags.theme,
|
|
75
77
|
css: flags.css,
|
|
78
|
+
platform: flags.platform,
|
|
79
|
+
tauri: flags.tauri,
|
|
80
|
+
i18n: flags.i18n,
|
|
81
|
+
locales: flags.locales,
|
|
76
82
|
});
|
|
77
83
|
} else if (command === 'add-component') {
|
|
78
84
|
// 호환 별칭 — 신규 진입점은 `sh-ui add <name>` (bin/sh-ui.mjs 가 walk-up 으로 라우팅).
|
|
@@ -90,6 +96,8 @@ export async function runCreate(rest) {
|
|
|
90
96
|
theme: flags.theme,
|
|
91
97
|
css: flags.css,
|
|
92
98
|
tauri: flags.tauri,
|
|
99
|
+
i18n: flags.i18n,
|
|
100
|
+
locales: flags.locales,
|
|
93
101
|
yes: flags.yes,
|
|
94
102
|
dryRun: flags.dryRun,
|
|
95
103
|
});
|
package/src/mcp.mjs
CHANGED
|
@@ -46,6 +46,8 @@ import {
|
|
|
46
46
|
THEME_RADII,
|
|
47
47
|
THEME_MODES,
|
|
48
48
|
CSS_FRAMEWORKS_SUPPORTED,
|
|
49
|
+
I18N_LIBRARIES,
|
|
50
|
+
I18N_DEFAULT_LOCALES,
|
|
49
51
|
} from "./constants.js";
|
|
50
52
|
import { allPlugins } from "./create/plugins/index.js";
|
|
51
53
|
import { allArchitectures } from "./create/architectures/index.js";
|
|
@@ -402,6 +404,16 @@ export async function startMcpServer() {
|
|
|
402
404
|
"Rust toolchain (`cargo`/`rustc`) 가 시스템에 설치되어 있어야 첫 `pnpm tauri dev` 가 동작. " +
|
|
403
405
|
"기본 false.",
|
|
404
406
|
),
|
|
407
|
+
i18n: z.enum(I18N_LIBRARIES).optional()
|
|
408
|
+
.describe(
|
|
409
|
+
"i18n 라이브러리 — platform=vite 일 때만 의미. 'react-i18next' 로 설정 시 i18next + react-i18next + browser-languagedetector + http-backend 셋업 + " +
|
|
410
|
+
"providers/I18nProvider.tsx + locales/{lng}/common.json 자동 emit. 기본 'none'. v0.92.0+ 신규.",
|
|
411
|
+
),
|
|
412
|
+
locales: z.string().optional()
|
|
413
|
+
.describe(
|
|
414
|
+
`i18n 활성화 시 생성할 locale 코드 (comma-separated, 2글자 또는 'ko-KR' 류). 기본 '${I18N_DEFAULT_LOCALES}'. 첫 locale 이 fallbackLng. ` +
|
|
415
|
+
"i18n='none' 이면 무시.",
|
|
416
|
+
),
|
|
405
417
|
},
|
|
406
418
|
},
|
|
407
419
|
async (input) => {
|
|
@@ -432,6 +444,15 @@ export async function startMcpServer() {
|
|
|
432
444
|
}],
|
|
433
445
|
};
|
|
434
446
|
}
|
|
447
|
+
if (input.i18n && input.i18n !== "none" && input.platform !== "vite") {
|
|
448
|
+
return {
|
|
449
|
+
isError: true,
|
|
450
|
+
content: [{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: `i18n='${input.i18n}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${input.platform}).`,
|
|
453
|
+
}],
|
|
454
|
+
};
|
|
455
|
+
}
|
|
435
456
|
const targetParent = resolveCwd(input);
|
|
436
457
|
const targetDir = resolve(targetParent, input.name);
|
|
437
458
|
if (existsSync(targetDir) && !input.force) {
|
|
@@ -458,6 +479,8 @@ export async function startMcpServer() {
|
|
|
458
479
|
theme: input.theme,
|
|
459
480
|
css: input.cssFramework,
|
|
460
481
|
tauri: input.tauri,
|
|
482
|
+
i18n: input.i18n,
|
|
483
|
+
locales: input.locales,
|
|
461
484
|
yes: true, // 사전 검사를 마쳤으니 generator 의 confirm 프롬프트 우회
|
|
462
485
|
}),
|
|
463
486
|
);
|
|
@@ -472,10 +495,11 @@ export async function startMcpServer() {
|
|
|
472
495
|
"sh_ui_add_app",
|
|
473
496
|
{
|
|
474
497
|
description:
|
|
475
|
-
"기존 모노레포에 새 Next.js
|
|
498
|
+
"기존 모노레포에 새 앱 (Next.js 또는 Vite) 추가 — apps/{name}/ + packages/ui/ui-apps/ui-{name}/ 동시 생성. " +
|
|
476
499
|
"사용자가 '앱 추가' / 'monorepo 에 새 앱' / 'add admin app' 류 요청을 하면 이 툴 사용 (Bash 로 npx " + cliName + " add-app 직접 호출보다 우선). " +
|
|
477
500
|
"v0.65+ 레이아웃 준수 — ui-{name} 은 tokens-only role, 컴포넌트는 sibling ui-core 가 SoT. " +
|
|
478
|
-
"theme/css 는 새 ui-app 에만 적용 (다른 앱 영향 없음). monorepo 가 아니면 (pnpm-workspace.yaml 없음) 에러."
|
|
501
|
+
"theme/css 는 새 ui-app 에만 적용 (다른 앱 영향 없음). monorepo 가 아니면 (pnpm-workspace.yaml 없음) 에러. " +
|
|
502
|
+
"platform 미지정 시 기존 apps/* 스캔해 추론 (모든 앱이 같은 플랫폼이면 그것으로). vite + tauri:true 면 apps/{name}/src-tauri/ 도 함께 emit.",
|
|
479
503
|
inputSchema: {
|
|
480
504
|
name: z.string().min(1)
|
|
481
505
|
.describe("앱 이름 — apps/{name}/ + packages/ui/ui-apps/ui-{name}/ 디렉토리명. 영숫자 + 하이픈."),
|
|
@@ -487,6 +511,19 @@ export async function startMcpServer() {
|
|
|
487
511
|
.describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는 base64 테마 코드. 새 ui-app 의 tokens.css 에만 주입.`),
|
|
488
512
|
cssFramework: z.enum(CSS_FRAMEWORKS).optional()
|
|
489
513
|
.describe("CSS 프레임워크. 기본 plain. 새 앱의 컴포넌트 변종 결정 — 같은 모노레포 내 다른 앱과 다른 값 가능."),
|
|
514
|
+
platform: z.enum(["next", "vite"]).optional()
|
|
515
|
+
.describe("플랫폼 — next | vite. 미지정 시 기존 apps/* 의 deps 로 추론 (모든 앱이 같은 플랫폼이면 그것으로, 혼재면 명시 필요)."),
|
|
516
|
+
tauri: z.boolean().optional()
|
|
517
|
+
.describe("Tauri 2.x 데스크탑 셸 — platform=vite 일 때만 의미. apps/{name}/src-tauri/ 에 emit. 기본 false."),
|
|
518
|
+
i18n: z.enum(I18N_LIBRARIES).optional()
|
|
519
|
+
.describe(
|
|
520
|
+
"i18n 라이브러리 — platform=vite 일 때만 의미. 'react-i18next' 로 설정 시 i18next + react-i18next + " +
|
|
521
|
+
"browser-languagedetector + http-backend 셋업 + providers/I18nProvider.tsx 자동 emit. 기본 'none'. v0.92.0+ 신규.",
|
|
522
|
+
),
|
|
523
|
+
locales: z.string().optional()
|
|
524
|
+
.describe(
|
|
525
|
+
`i18n 활성화 시 생성할 locale 코드 (comma-separated). 기본 '${I18N_DEFAULT_LOCALES}'. 첫 locale 이 fallbackLng. i18n='none' 이면 무시.`,
|
|
526
|
+
),
|
|
490
527
|
cwd: z.string().optional()
|
|
491
528
|
.describe("모노레포 루트 (pnpm-workspace.yaml 있는 곳). 기본 process.cwd()"),
|
|
492
529
|
},
|
|
@@ -497,6 +534,24 @@ export async function startMcpServer() {
|
|
|
497
534
|
} catch (e) {
|
|
498
535
|
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
499
536
|
}
|
|
537
|
+
if (input.tauri && input.platform === "next") {
|
|
538
|
+
return {
|
|
539
|
+
isError: true,
|
|
540
|
+
content: [{
|
|
541
|
+
type: "text",
|
|
542
|
+
text: "tauri 는 platform=vite 일 때만 지원합니다. --platform vite 사용 또는 tauri 옵션 제거.",
|
|
543
|
+
}],
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (input.i18n && input.i18n !== "none" && input.platform && input.platform !== "vite") {
|
|
547
|
+
return {
|
|
548
|
+
isError: true,
|
|
549
|
+
content: [{
|
|
550
|
+
type: "text",
|
|
551
|
+
text: `i18n='${input.i18n}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${input.platform}).`,
|
|
552
|
+
}],
|
|
553
|
+
};
|
|
554
|
+
}
|
|
500
555
|
const text = await captureConsole(() =>
|
|
501
556
|
addApp({
|
|
502
557
|
name: input.name,
|
|
@@ -504,6 +559,10 @@ export async function startMcpServer() {
|
|
|
504
559
|
plugins: input.plugins,
|
|
505
560
|
theme: input.theme,
|
|
506
561
|
css: input.cssFramework,
|
|
562
|
+
platform: input.platform,
|
|
563
|
+
tauri: input.tauri,
|
|
564
|
+
i18n: input.i18n,
|
|
565
|
+
locales: input.locales,
|
|
507
566
|
cwd: resolveCwd(input),
|
|
508
567
|
}),
|
|
509
568
|
);
|