sh-ui-cli 0.59.9 → 0.61.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 +40 -0
- package/data/registry/flutter/widgets/sh_ui_input.dart +0 -79
- package/data/registry/react/components/input/index.module.tsx +0 -70
- package/data/registry/react/components/input/index.tailwind.tsx +0 -53
- package/data/registry/react/components/input/index.tsx +0 -70
- package/data/registry/react/components/input/index.vanilla-extract.tsx +0 -63
- package/data/summaries/react.json +1 -1
- package/package.json +2 -2
- package/src/create/architectures/flat.js +1 -1
- package/src/create/generator.js +717 -17
- package/src/create/index.mjs +1 -1
- package/src/create/plugins/authJwt.js +51 -1
- package/src/create/plugins/nextIntl.js +163 -17
- package/src/create/plugins/sentry.js +43 -23
- package/src/mcp.mjs +2 -2
- package/templates/flutter-standalone/sh-ui.config.json +0 -1
- package/templates/monorepo/README.md +14 -5
- package/templates/monorepo/packages/eslint-config/flat.js +71 -0
- package/templates/monorepo/packages/eslint-config/fsd.js +0 -21
- package/templates/monorepo/packages/eslint-config/package.json +2 -3
- package/templates/monorepo/packages/typescript-config/package.json +6 -1
- package/templates/monorepo/packages/ui/ui-core/tsconfig.json +1 -1
- package/templates/monorepo/pnpm-workspace.yaml +2 -1
- package/templates/nextjs-app/.env.example +3 -2
- package/templates/nextjs-app/Dockerfile +36 -5
- package/templates/nextjs-app/README.md +9 -7
- package/templates/nextjs-app/_arch/flat/app/api/proxy/[...path]/route.ts +1 -1
- package/templates/nextjs-app/_arch/flat/app/layout.tsx +2 -2
- package/templates/nextjs-app/_arch/flat/components/common/PrefetchBoundary/index.tsx +1 -1
- package/templates/nextjs-app/_arch/flat/components/layouts/RootLayout.tsx +2 -0
- package/templates/nextjs-app/_arch/flat/components/providers/GlobalProvider/index.tsx +3 -3
- package/templates/nextjs-app/_arch/flat/components/providers/theme/ThemeProvider.tsx +12 -0
- package/templates/nextjs-app/_arch/flat/eslint.config.js +10 -0
- package/templates/nextjs-app/_arch/flat/lib/api/clientFetch.ts +4 -4
- package/templates/nextjs-app/_arch/flat/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-app/_arch/flat/lib/hooks/useAppMutation.ts +14 -7
- package/templates/nextjs-app/_arch/flat/lib/test/renderWithProviders.tsx +32 -7
- package/templates/nextjs-app/_arch/flat/lib/utils/formatDate.ts +4 -0
- package/templates/nextjs-app/_arch/flat/lib/utils/formatPrice.ts +13 -5
- package/templates/nextjs-app/_arch/flat/lib/utils/getQueryClient.ts +1 -1
- package/templates/nextjs-app/_arch/flat/tsconfig.json +0 -1
- package/templates/nextjs-app/_arch/fsd/app/api/proxy/[...path]/route.ts +1 -1
- package/templates/nextjs-app/_arch/fsd/app/layout.tsx +2 -2
- package/templates/nextjs-app/_arch/fsd/src/app/layouts/RootLayout.tsx +2 -0
- package/templates/nextjs-app/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +3 -3
- package/templates/nextjs-app/_arch/fsd/src/app/providers/theme/ThemeProvider.tsx +12 -0
- package/templates/nextjs-app/_arch/fsd/src/shared/api/clientFetch.ts +4 -4
- package/templates/nextjs-app/_arch/fsd/src/shared/api/errorMessages.ts +37 -0
- package/templates/nextjs-app/_arch/fsd/src/shared/hooks/useAppMutation.ts +14 -7
- package/templates/nextjs-app/_arch/fsd/src/shared/lib/formatDate.ts +4 -0
- package/templates/nextjs-app/_arch/fsd/src/shared/lib/formatPrice.ts +13 -5
- package/templates/nextjs-app/_arch/fsd/src/shared/lib/getQueryClient.ts +1 -1
- package/templates/nextjs-app/_arch/fsd/src/shared/test/renderWithProviders.tsx +32 -7
- package/templates/nextjs-app/_arch/fsd/src/shared/ui/PrefetchBoundary/index.tsx +1 -1
- package/templates/nextjs-app/vitest.config.ts +4 -0
- package/templates/nextjs-standalone/.env.example +3 -2
- package/templates/nextjs-standalone/Dockerfile +35 -0
- package/templates/nextjs-standalone/_arch/flat/app/api/proxy/[...path]/route.ts +1 -1
- package/templates/nextjs-standalone/_arch/flat/app/layout.tsx +2 -2
- package/templates/nextjs-standalone/_arch/flat/components/common/PrefetchBoundary/index.tsx +1 -1
- package/templates/nextjs-standalone/_arch/flat/components/layouts/RootLayout.tsx +2 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/GlobalProvider/index.tsx +3 -3
- package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProvider.tsx +12 -0
- package/templates/nextjs-standalone/_arch/flat/eslint.config.js +123 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/clientFetch.ts +4 -4
- package/templates/nextjs-standalone/_arch/flat/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-standalone/_arch/flat/lib/hooks/useAppMutation.ts +14 -7
- package/templates/nextjs-standalone/_arch/flat/lib/test/renderWithProviders.tsx +32 -7
- package/templates/nextjs-standalone/_arch/flat/lib/utils/formatDate.ts +4 -0
- package/templates/nextjs-standalone/_arch/flat/lib/utils/formatPrice.ts +13 -5
- package/templates/nextjs-standalone/_arch/flat/lib/utils/getQueryClient.ts +1 -1
- package/templates/nextjs-standalone/_arch/flat/tsconfig.json +1 -2
- package/templates/nextjs-standalone/_arch/fsd/app/api/proxy/[...path]/route.ts +1 -1
- package/templates/nextjs-standalone/_arch/fsd/app/layout.tsx +2 -2
- package/templates/nextjs-standalone/_arch/fsd/src/app/layouts/RootLayout.tsx +2 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +3 -3
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProvider.tsx +12 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/clientFetch.ts +4 -4
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/errorMessages.ts +37 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/useAppMutation.ts +14 -7
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatDate.ts +4 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatPrice.ts +13 -5
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/getQueryClient.ts +1 -1
- package/templates/nextjs-standalone/_arch/fsd/src/shared/test/renderWithProviders.tsx +32 -7
- package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/PrefetchBoundary/index.tsx +1 -1
- package/templates/nextjs-standalone/eslint.config.js +0 -15
- package/templates/nextjs-standalone/package.json +0 -2
- package/templates/ui-app-template/package.json +2 -2
- package/templates/ui-app-template/postcss.config.mjs +1 -1
- package/templates/monorepo/.eslintrc.js +0 -8
- package/templates/nextjs-app/_arch/flat/components/providers/theme/ThemeProviders.tsx +0 -12
- package/templates/nextjs-app/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +0 -12
- package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProviders.tsx +0 -12
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +0 -12
package/src/create/generator.js
CHANGED
|
@@ -1,8 +1,76 @@
|
|
|
1
1
|
import { input, select, checkbox, confirm } from '@inquirer/prompts';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import
|
|
3
|
+
import * as fsp from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Node native fs/promises 위에 얹은 fs-extra 호환 슬림 어댑터.
|
|
9
|
+
*
|
|
10
|
+
* 왜 직접 어댑터를 두는가:
|
|
11
|
+
* 1. fs-extra v11 이 long-running 프로세스(MCP daemon) 에서 internal state 를
|
|
12
|
+
* 누적시켜 directory filter / recursive copy 가 부분 실패하는 회귀가 보고됨.
|
|
13
|
+
* Node 24 의 native `fs.cp` 는 그런 모듈 단위 캐시가 없어 안전.
|
|
14
|
+
* 2. 의존성 1개 제거 — 부트 시간/번들 크기 이득.
|
|
15
|
+
*
|
|
16
|
+
* 의도적으로 fs-extra 의 API 표면 일부만 재현 — 사용처(generator.js) 에서 쓰는 메서드만.
|
|
17
|
+
* `move` 는 cross-device 시 `rename` 이 EXDEV 를 던지므로 copy+remove fallback 포함.
|
|
18
|
+
*/
|
|
19
|
+
const fs = {
|
|
20
|
+
...fsp,
|
|
21
|
+
/** 파일/디렉토리 존재 여부 — fs-extra `pathExists` 동등. */
|
|
22
|
+
pathExists: async (p) => {
|
|
23
|
+
try {
|
|
24
|
+
await fsp.access(p);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
/** JSON 파일 읽기 — fs-extra `readJson` 동등. */
|
|
31
|
+
readJson: async (p) => JSON.parse(await fsp.readFile(p, 'utf-8')),
|
|
32
|
+
/** JSON 파일 쓰기 — fs-extra `writeJson` 동등 (`spaces` 만 지원). */
|
|
33
|
+
writeJson: async (p, obj, opts = {}) => {
|
|
34
|
+
const indent = opts.spaces ?? 2;
|
|
35
|
+
await fsp.writeFile(p, JSON.stringify(obj, null, indent) + '\n');
|
|
36
|
+
},
|
|
37
|
+
/** 재귀 삭제 — fs-extra `remove` 동등. 없으면 무시. */
|
|
38
|
+
remove: async (p) => fsp.rm(p, { recursive: true, force: true }),
|
|
39
|
+
/** 디렉토리 보장 (mkdir -p) — fs-extra `ensureDir` 동등. */
|
|
40
|
+
ensureDir: async (p) => fsp.mkdir(p, { recursive: true }),
|
|
41
|
+
/**
|
|
42
|
+
* 재귀 복사 — fs-extra `copy` 의 핵심 옵션 (`filter`, `overwrite`) 만 지원.
|
|
43
|
+
* Node `fs.cp` 의 `force/errorOnExist` 조합으로 fs-extra 의 `overwrite` 시맨틱 재현:
|
|
44
|
+
* - overwrite: true → force=true (기존 파일 덮어씀)
|
|
45
|
+
* - overwrite: false → force=false + errorOnExist=false (기존 파일 스킵, 충돌 throw 없음)
|
|
46
|
+
*/
|
|
47
|
+
copy: async (src, dest, opts = {}) => {
|
|
48
|
+
const overwrite = opts.overwrite ?? false;
|
|
49
|
+
return fsp.cp(src, dest, {
|
|
50
|
+
recursive: true,
|
|
51
|
+
filter: opts.filter,
|
|
52
|
+
force: overwrite,
|
|
53
|
+
errorOnExist: false,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
57
|
+
* 이동 — fs-extra `move` 동등. 같은 파일시스템이면 `rename`, cross-device 면 copy+remove.
|
|
58
|
+
* `overwrite: true` 시 기존 dest 를 먼저 제거.
|
|
59
|
+
*/
|
|
60
|
+
move: async (src, dest, opts = {}) => {
|
|
61
|
+
if (opts.overwrite) {
|
|
62
|
+
await fsp.rm(dest, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await fsp.rename(src, dest);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
if (e.code !== 'EXDEV') throw e;
|
|
68
|
+
// cross-device — fallback: copy + remove src.
|
|
69
|
+
await fsp.cp(src, dest, { recursive: true, force: true, errorOnExist: false });
|
|
70
|
+
await fsp.rm(src, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
6
74
|
import { getPluginChoices, getPluginsByNames } from './plugins/index.js';
|
|
7
75
|
import {
|
|
8
76
|
assertArchPlatformCompat,
|
|
@@ -47,15 +115,19 @@ const TEMPLATES_DIR = getTemplatesRoot();
|
|
|
47
115
|
|
|
48
116
|
/**
|
|
49
117
|
* 템플릿 복사 직후 sh-ui.config.json 의 cssFramework 필드를 갱신.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
118
|
+
*
|
|
119
|
+
* Flutter 는 cssFramework 가 의미 없으므로 platform=flutter 면 필드 자체를 안 쓴다.
|
|
120
|
+
* Next.js 는 plain/tailwind/css-modules 따라 컴포넌트 변종 결정 + base 파일도 분기 emit.
|
|
53
121
|
*/
|
|
54
122
|
async function patchShUiConfig(configPath, cssFramework) {
|
|
55
|
-
const fw = cssFramework ?? CSS_FRAMEWORK_DEFAULT;
|
|
56
123
|
if (!(await fs.pathExists(configPath))) return;
|
|
57
124
|
const config = await fs.readJson(configPath);
|
|
58
|
-
|
|
125
|
+
// Flutter 는 cssFramework 무관 — 필드 자체를 두지 않는다.
|
|
126
|
+
if (config.platform === 'flutter') {
|
|
127
|
+
delete config.cssFramework;
|
|
128
|
+
} else {
|
|
129
|
+
config.cssFramework = cssFramework ?? CSS_FRAMEWORK_DEFAULT;
|
|
130
|
+
}
|
|
59
131
|
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
60
132
|
}
|
|
61
133
|
|
|
@@ -384,11 +456,13 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
|
|
|
384
456
|
await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-standalone'), targetDir, {
|
|
385
457
|
filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
|
|
386
458
|
});
|
|
459
|
+
await ensureArchCleanup(targetDir);
|
|
387
460
|
await fs.copy(
|
|
388
461
|
path.join(TEMPLATES_DIR, 'nextjs-standalone', '_arch', arch.name),
|
|
389
462
|
targetDir,
|
|
390
463
|
{ overwrite: true },
|
|
391
464
|
);
|
|
465
|
+
await assertArchOverlayApplied(targetDir, arch);
|
|
392
466
|
|
|
393
467
|
// Update package.json
|
|
394
468
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
@@ -402,6 +476,9 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
|
|
|
402
476
|
Object.assign(pkg.devDependencies, plugin.devDependencies);
|
|
403
477
|
}
|
|
404
478
|
}
|
|
479
|
+
// 플러그인 deps 가 알파벳 정렬을 깨므로 마지막에 정렬해서 일관된 출력 보장.
|
|
480
|
+
if (pkg.dependencies) pkg.dependencies = sortObjectKeys(pkg.dependencies);
|
|
481
|
+
if (pkg.devDependencies) pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
|
|
405
482
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
406
483
|
|
|
407
484
|
await writeNextConfig(targetDir, plugins, { isMonorepo: false, arch });
|
|
@@ -409,6 +486,7 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
|
|
|
409
486
|
await writePluginFiles(targetDir, plugins, arch);
|
|
410
487
|
await composeProviders(targetDir, plugins, arch);
|
|
411
488
|
await applyTransforms(targetDir, plugins, arch);
|
|
489
|
+
await applyCssFrameworkVariant(targetDir, css, { isMonorepo: false, plugins, arch });
|
|
412
490
|
await injectCssTheme(targetDir, theme);
|
|
413
491
|
await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css);
|
|
414
492
|
}
|
|
@@ -444,24 +522,27 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
|
|
|
444
522
|
});
|
|
445
523
|
|
|
446
524
|
const appsDir = path.join(targetDir, 'apps', appName);
|
|
447
|
-
await generateApp(appsDir, appName, port, plugins, arch);
|
|
525
|
+
await generateApp(appsDir, appName, port, plugins, arch, css);
|
|
526
|
+
// generateApp 이 ui-{app} 패키지의 cssFramework 변종까지 처리. 여기선 theme + sh-ui.config.json 만.
|
|
448
527
|
const uiAppDir = path.join(targetDir, 'packages', 'ui', 'ui-apps', `ui-${appName}`);
|
|
449
528
|
await injectCssTheme(uiAppDir, theme);
|
|
450
529
|
await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css);
|
|
451
530
|
}
|
|
452
531
|
|
|
453
|
-
async function generateApp(targetDir, appName, port, plugins, arch) {
|
|
532
|
+
async function generateApp(targetDir, appName, port, plugins, arch, css = 'tailwind') {
|
|
454
533
|
// 베이스 템플릿 (arch-neutral 파일들) 만 카피 — _arch/ 디렉토리는 스킵.
|
|
455
534
|
// 그 후 선택된 arch 의 오버레이를 위에 머지해 arch-coupled 파일들 (layout.tsx,
|
|
456
535
|
// src/ 또는 lib+components/, tsconfig.json paths 블록 등) 을 떨어뜨린다.
|
|
457
536
|
await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-app'), targetDir, {
|
|
458
537
|
filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
|
|
459
538
|
});
|
|
539
|
+
await ensureArchCleanup(targetDir);
|
|
460
540
|
await fs.copy(
|
|
461
541
|
path.join(TEMPLATES_DIR, 'nextjs-app', '_arch', arch.name),
|
|
462
542
|
targetDir,
|
|
463
543
|
{ overwrite: true },
|
|
464
544
|
);
|
|
545
|
+
await assertArchOverlayApplied(targetDir, arch);
|
|
465
546
|
|
|
466
547
|
// Replace ui-app-name placeholder with actual app name in all files
|
|
467
548
|
await replaceInAllFiles(targetDir, 'ui-app-name', `ui-${appName}`);
|
|
@@ -480,6 +561,8 @@ async function generateApp(targetDir, appName, port, plugins, arch) {
|
|
|
480
561
|
Object.assign(pkg.devDependencies, plugin.devDependencies);
|
|
481
562
|
}
|
|
482
563
|
}
|
|
564
|
+
if (pkg.dependencies) pkg.dependencies = sortObjectKeys(pkg.dependencies);
|
|
565
|
+
if (pkg.devDependencies) pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
|
|
483
566
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
484
567
|
|
|
485
568
|
await writeNextConfig(targetDir, plugins, { isMonorepo: true, appName, arch });
|
|
@@ -506,10 +589,618 @@ async function generateApp(targetDir, appName, port, plugins, arch) {
|
|
|
506
589
|
await writePluginFiles(targetDir, plugins, arch);
|
|
507
590
|
await composeProviders(targetDir, plugins, arch);
|
|
508
591
|
await applyTransforms(targetDir, plugins, arch);
|
|
592
|
+
// monorepo 의 cssFramework 변종은 web app 디렉토리 + ui-app 패키지 디렉토리 양쪽에 적용.
|
|
593
|
+
await applyCssFrameworkVariant(targetDir, css, { isMonorepo: true, plugins, arch });
|
|
594
|
+
if (await fs.pathExists(uiPkgDir)) {
|
|
595
|
+
await applyCssFrameworkVariant(uiPkgDir, css, { isMonorepo: true, plugins, arch, isUiPackage: true });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* 베이스 템플릿 카피 직후 `_arch/` 잔여 정리.
|
|
601
|
+
*
|
|
602
|
+
* `fs.copy` 의 `filter` 가 어떤 환경(특히 long-running MCP daemon — fs-extra v11
|
|
603
|
+
* 내부 캐시가 누적될 때) 에서 디렉토리 제외에 실패해 `_arch/{flat,fsd}/...` 가
|
|
604
|
+
* 그대로 사용자 프로젝트에 들어가는 회귀가 보고됐다. 베이스 카피 직후 이 헬퍼가
|
|
605
|
+
* 명시적으로 `_arch/` 를 제거해 필터 실패와 무관하게 항상 깨끗한 상태를 보장한다.
|
|
606
|
+
*
|
|
607
|
+
* 정상 케이스에서는 no-op (디렉토리가 이미 없으므로).
|
|
608
|
+
*/
|
|
609
|
+
async function ensureArchCleanup(targetDir) {
|
|
610
|
+
const archDir = path.join(targetDir, '_arch');
|
|
611
|
+
if (await fs.pathExists(archDir)) {
|
|
612
|
+
await fs.remove(archDir);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* arch 오버레이 카피 직후 핵심 파일이 떨어졌는지 검증.
|
|
618
|
+
*
|
|
619
|
+
* 오버레이가 부분적으로만 동작하는 회귀(같은 fs-extra 회귀와 짝을 이뤄
|
|
620
|
+
* 두 번째 `fs.copy` 가 src/{app,entities,...} 를 빠뜨리는 케이스) 가 발생하면
|
|
621
|
+
* 곧바로 알아챌 수 있도록 sentinel 파일 존재 검사. 실패하면 명확한 메시지로
|
|
622
|
+
* throw — 한참 뒤 plugin transform 의 ENOENT 로 노출되는 것보다 진단이 쉽다.
|
|
623
|
+
*/
|
|
624
|
+
async function assertArchOverlayApplied(targetDir, arch) {
|
|
625
|
+
const sentinel = `${arch.paths.layouts}/RootLayout.tsx`;
|
|
626
|
+
const sentinelPath = path.join(targetDir, sentinel);
|
|
627
|
+
if (!(await fs.pathExists(sentinelPath))) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`arch 오버레이 누락: ${arch.name} 의 sentinel 파일(${sentinel}) 이 ${targetDir} 에 없습니다. ` +
|
|
630
|
+
`오버레이 카피가 정상적으로 동작하지 않은 것으로 보입니다. ` +
|
|
631
|
+
`(MCP daemon 환경이라면 daemon 재시작 후 재시도 — long-running fs-extra 인스턴스의 internal state 회귀 의심.)`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
509
634
|
}
|
|
510
635
|
|
|
511
636
|
// ─── Helpers ───
|
|
512
637
|
|
|
638
|
+
/**
|
|
639
|
+
* 객체의 key 를 알파벳 순으로 정렬한 새 객체 반환. package.json deps 같이 사용자가
|
|
640
|
+
* 자주 보는 필드의 일관된 출력에 사용.
|
|
641
|
+
*/
|
|
642
|
+
function sortObjectKeys(obj) {
|
|
643
|
+
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* cssFramework 별 base 파일 변환.
|
|
648
|
+
*
|
|
649
|
+
* 베이스 템플릿은 'tailwind' 기준으로 emit 되어 있다 (글로벌 css, postcss config,
|
|
650
|
+
* package.json deps, page.tsx, error.tsx 등 모두 Tailwind 가정). cssFramework 가
|
|
651
|
+
* 'plain' 또는 'css-modules' 면 이 함수가 후처리로 Tailwind 의존성을 제거하고
|
|
652
|
+
* inline-style / .module.css 변종으로 교체한다.
|
|
653
|
+
*
|
|
654
|
+
* 'tailwind' 일 때는 no-op (현재 기본값).
|
|
655
|
+
*/
|
|
656
|
+
async function applyCssFrameworkVariant(targetDir, cssFramework, { isMonorepo, plugins, arch, isUiPackage = false } = {}) {
|
|
657
|
+
if (cssFramework === 'tailwind') return;
|
|
658
|
+
if (cssFramework !== 'plain' && cssFramework !== 'css-modules') return;
|
|
659
|
+
|
|
660
|
+
// 1) package.json — Tailwind deps 제거.
|
|
661
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
662
|
+
if (await fs.pathExists(pkgPath)) {
|
|
663
|
+
const pkg = await fs.readJson(pkgPath);
|
|
664
|
+
const TAILWIND_DEPS = ['tailwindcss', '@tailwindcss/postcss', 'prettier-plugin-tailwindcss'];
|
|
665
|
+
let changed = false;
|
|
666
|
+
for (const key of TAILWIND_DEPS) {
|
|
667
|
+
if (pkg.dependencies && key in pkg.dependencies) {
|
|
668
|
+
delete pkg.dependencies[key];
|
|
669
|
+
changed = true;
|
|
670
|
+
}
|
|
671
|
+
if (pkg.devDependencies && key in pkg.devDependencies) {
|
|
672
|
+
delete pkg.devDependencies[key];
|
|
673
|
+
changed = true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (changed) await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 2) postcss.config.mjs — Tailwind plugin 제거. plain/cssmodules 둘 다 비움.
|
|
680
|
+
// (Next.js 가 css-modules 는 자동 처리)
|
|
681
|
+
const postcssPath = path.join(targetDir, 'postcss.config.mjs');
|
|
682
|
+
if (await fs.pathExists(postcssPath)) {
|
|
683
|
+
if (isUiPackage) {
|
|
684
|
+
// ui 패키지의 postcss.config.mjs 는 host app 의 것을 re-export — 비워두면 안 됨.
|
|
685
|
+
// 비-tailwind 일 땐 빈 plugins 객체로 교체.
|
|
686
|
+
await fs.writeFile(postcssPath, `const config = {\n plugins: {},\n};\n\nexport default config;\n`);
|
|
687
|
+
} else {
|
|
688
|
+
await fs.writeFile(postcssPath, `const config = {\n plugins: {},\n};\n\nexport default config;\n`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 3) globals.css — Tailwind import + @theme inline 제거. 토큰 import + 기본 reset 만.
|
|
693
|
+
const globalsCandidates = [
|
|
694
|
+
path.join(targetDir, 'app/globals.css'),
|
|
695
|
+
path.join(targetDir, 'src/styles/globals.css'),
|
|
696
|
+
];
|
|
697
|
+
for (const globals of globalsCandidates) {
|
|
698
|
+
if (await fs.pathExists(globals)) {
|
|
699
|
+
// tokens.css 위치 결정 — globals.css 와 같은 directory 내 또는 인접.
|
|
700
|
+
// standalone fsd: app/globals.css 와 src/shared/styles/tokens.css → '../src/shared/styles/tokens.css'
|
|
701
|
+
// standalone flat: app/globals.css 와 lib/styles/tokens.css → '../lib/styles/tokens.css'
|
|
702
|
+
// ui-app-template (monorepo ui pkg): src/styles/globals.css ↔ src/styles/tokens.css → './tokens.css'
|
|
703
|
+
// 기존 globals.css 의 import 줄을 보존해 그대로 사용 (이미 정확한 상대경로).
|
|
704
|
+
const existing = await fs.readFile(globals, 'utf-8');
|
|
705
|
+
const tokenImport = existing
|
|
706
|
+
.split('\n')
|
|
707
|
+
.find((l) => /@import\s+['"][^'"]+tokens\.css['"]/.test(l));
|
|
708
|
+
const newContent = `${tokenImport ?? "@import './tokens.css';"}
|
|
709
|
+
|
|
710
|
+
/* Plain CSS — Tailwind 미사용. tokens.css 의 변수만 노출. */
|
|
711
|
+
body {
|
|
712
|
+
margin: 0;
|
|
713
|
+
min-height: 100vh;
|
|
714
|
+
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto,
|
|
715
|
+
'Helvetica Neue', Arial, sans-serif;
|
|
716
|
+
background: var(--background);
|
|
717
|
+
color: var(--foreground);
|
|
718
|
+
-webkit-font-smoothing: antialiased;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
*,
|
|
722
|
+
*::before,
|
|
723
|
+
*::after {
|
|
724
|
+
box-sizing: border-box;
|
|
725
|
+
}
|
|
726
|
+
`;
|
|
727
|
+
await fs.writeFile(globals, newContent);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// 4) ui 패키지면 여기까지. host app 의 page/error 변환은 host 디렉토리에서만.
|
|
732
|
+
if (isUiPackage) return;
|
|
733
|
+
|
|
734
|
+
// 5) page.tsx — Tailwind 클래스 → inline-style (plain) 또는 .module.css (cssmodules).
|
|
735
|
+
// 위치는 plugin 활성 여부에 따라 달라짐.
|
|
736
|
+
const intlActive = plugins?.some((p) => p.name === 'next-intl');
|
|
737
|
+
const pageCandidates = intlActive
|
|
738
|
+
? [path.join(targetDir, 'app/[locale]/page.tsx')]
|
|
739
|
+
: [path.join(targetDir, 'app/page.tsx')];
|
|
740
|
+
|
|
741
|
+
for (const pagePath of pageCandidates) {
|
|
742
|
+
if (!(await fs.pathExists(pagePath))) continue;
|
|
743
|
+
if (cssFramework === 'plain') {
|
|
744
|
+
await fs.writeFile(pagePath, `export default function Home() {
|
|
745
|
+
return (
|
|
746
|
+
<main
|
|
747
|
+
style={{
|
|
748
|
+
display: 'flex',
|
|
749
|
+
minHeight: '100vh',
|
|
750
|
+
flexDirection: 'column',
|
|
751
|
+
alignItems: 'center',
|
|
752
|
+
justifyContent: 'center',
|
|
753
|
+
}}
|
|
754
|
+
>
|
|
755
|
+
<h1 style={{ fontSize: '2.25rem', fontWeight: 700, margin: 0 }}>
|
|
756
|
+
Hello World
|
|
757
|
+
</h1>
|
|
758
|
+
</main>
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
`);
|
|
762
|
+
} else {
|
|
763
|
+
// css-modules
|
|
764
|
+
const dir = path.dirname(pagePath);
|
|
765
|
+
await fs.writeFile(path.join(dir, 'page.module.css'), `.main {
|
|
766
|
+
display: flex;
|
|
767
|
+
min-height: 100vh;
|
|
768
|
+
flex-direction: column;
|
|
769
|
+
align-items: center;
|
|
770
|
+
justify-content: center;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.title {
|
|
774
|
+
font-size: 2.25rem;
|
|
775
|
+
font-weight: 700;
|
|
776
|
+
margin: 0;
|
|
777
|
+
}
|
|
778
|
+
`);
|
|
779
|
+
await fs.writeFile(pagePath, `import styles from './page.module.css';
|
|
780
|
+
|
|
781
|
+
export default function Home() {
|
|
782
|
+
return (
|
|
783
|
+
<main className={styles.main}>
|
|
784
|
+
<h1 className={styles.title}>Hello World</h1>
|
|
785
|
+
</main>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// 6) sentry plugin 의 error.tsx 변환 — Tailwind 클래스가 박혀있어서 plain/cssmodules 에서 작동 안 함.
|
|
793
|
+
// intl + sentry 면 nextIntl 이 [locale]/error.tsx 를 i18n-aware 로 replace 하므로 그것도 변환.
|
|
794
|
+
const sentryActive = plugins?.some((p) => p.name === 'sentry');
|
|
795
|
+
if (sentryActive) {
|
|
796
|
+
const errorCandidates = intlActive
|
|
797
|
+
? [path.join(targetDir, 'app/[locale]/error.tsx')]
|
|
798
|
+
: [path.join(targetDir, 'app/error.tsx')];
|
|
799
|
+
for (const errPath of errorCandidates) {
|
|
800
|
+
if (!(await fs.pathExists(errPath))) continue;
|
|
801
|
+
const useI18n = intlActive;
|
|
802
|
+
await fs.writeFile(errPath, buildErrorTsxNonTailwind({ useI18n, cssFramework, arch }));
|
|
803
|
+
if (cssFramework === 'css-modules') {
|
|
804
|
+
await fs.writeFile(
|
|
805
|
+
path.join(path.dirname(errPath), 'error.module.css'),
|
|
806
|
+
buildErrorModuleCss(),
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// 7) .prettierrc — tailwind plugin 제거.
|
|
813
|
+
await stripTailwindFromPrettier(path.join(targetDir, '.prettierrc'));
|
|
814
|
+
|
|
815
|
+
// 8) monorepo 인 경우 root .prettierrc 와 root package.json 도 정리 (root 의 prettier-plugin-tailwindcss).
|
|
816
|
+
// applyCssFrameworkVariant 는 apps/web 마다 호출되지만 root 정리는 1회면 충분 — idempotent 라 OK.
|
|
817
|
+
if (isMonorepo && !isUiPackage) {
|
|
818
|
+
const monorepoRoot = path.resolve(targetDir, '..', '..');
|
|
819
|
+
await stripTailwindFromPrettier(path.join(monorepoRoot, '.prettierrc'));
|
|
820
|
+
const rootPkgPath = path.join(monorepoRoot, 'package.json');
|
|
821
|
+
if (await fs.pathExists(rootPkgPath)) {
|
|
822
|
+
const rootPkg = await fs.readJson(rootPkgPath);
|
|
823
|
+
const TAILWIND_DEPS = ['prettier-plugin-tailwindcss'];
|
|
824
|
+
let changed = false;
|
|
825
|
+
for (const key of TAILWIND_DEPS) {
|
|
826
|
+
if (rootPkg.devDependencies && key in rootPkg.devDependencies) {
|
|
827
|
+
delete rootPkg.devDependencies[key];
|
|
828
|
+
changed = true;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (changed) await fs.writeJson(rootPkgPath, rootPkg, { spaces: 2 });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function stripTailwindFromPrettier(prettierPath) {
|
|
837
|
+
if (!(await fs.pathExists(prettierPath))) return;
|
|
838
|
+
const c = await fs.readJson(prettierPath);
|
|
839
|
+
if (!Array.isArray(c.plugins)) return;
|
|
840
|
+
c.plugins = c.plugins.filter((p) => p !== 'prettier-plugin-tailwindcss');
|
|
841
|
+
if (c.plugins.length === 0) delete c.plugins;
|
|
842
|
+
await fs.writeJson(prettierPath, c, { spaces: 2 });
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* sentry 의 error.tsx 를 plain/cssmodules 로 변환한 콘텐츠 생성.
|
|
847
|
+
* useI18n=true 면 next-intl 의 useTranslations + Link 사용.
|
|
848
|
+
*/
|
|
849
|
+
function buildErrorTsxNonTailwind({ useI18n, cssFramework, arch }) {
|
|
850
|
+
const i18nImports = useI18n
|
|
851
|
+
? `import { useTranslations } from 'next-intl';\n`
|
|
852
|
+
: '';
|
|
853
|
+
const configAlias = arch ? arch.aliases.config : '@/src/shared/config';
|
|
854
|
+
const linkImport = useI18n
|
|
855
|
+
? `import { Link } from '${configAlias}/i18n/navigation';\n`
|
|
856
|
+
: `import Link from 'next/link';\n`;
|
|
857
|
+
const tHook = useI18n ? ` const t = useTranslations('error');\n` : '';
|
|
858
|
+
const titleText = useI18n ? `{t('title')}` : `오류가 발생했습니다`;
|
|
859
|
+
const descText = useI18n
|
|
860
|
+
? `{t('description')}`
|
|
861
|
+
: `예상치 못한 오류가 발생했습니다. 다시 시도해주세요.`;
|
|
862
|
+
const fallback = useI18n ? `t('unexpectedError')` : `'알 수 없는 오류'`;
|
|
863
|
+
const tryAgain = useI18n ? `{t('button.tryAgain')}` : `다시 시도`;
|
|
864
|
+
const goHome = useI18n ? `{t('button.goHome')}` : `홈으로 이동`;
|
|
865
|
+
|
|
866
|
+
if (cssFramework === 'css-modules') {
|
|
867
|
+
return `'use client';
|
|
868
|
+
|
|
869
|
+
import * as Sentry from '@sentry/nextjs';
|
|
870
|
+
import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
|
|
871
|
+
${i18nImports}${linkImport}import { useEffect } from 'react';
|
|
872
|
+
|
|
873
|
+
import styles from './error.module.css';
|
|
874
|
+
|
|
875
|
+
export default function Error({
|
|
876
|
+
error,
|
|
877
|
+
reset,
|
|
878
|
+
}: {
|
|
879
|
+
error: Error & { digest?: string };
|
|
880
|
+
reset: () => void;
|
|
881
|
+
}) {
|
|
882
|
+
${tHook} useEffect(() => {
|
|
883
|
+
Sentry.captureException(error);
|
|
884
|
+
}, [error]);
|
|
885
|
+
|
|
886
|
+
return (
|
|
887
|
+
<div className={styles.wrapper}>
|
|
888
|
+
<div className={styles.card}>
|
|
889
|
+
<div className={styles.iconRow}>
|
|
890
|
+
<div className={styles.iconCircle}>
|
|
891
|
+
<AlertTriangle className={styles.icon} />
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
|
|
895
|
+
<h2 className={styles.title}>${titleText}</h2>
|
|
896
|
+
<p className={styles.description}>${descText}</p>
|
|
897
|
+
|
|
898
|
+
<div className={styles.errorBox}>
|
|
899
|
+
<p className={styles.errorText}>{error.message || ${fallback}}</p>
|
|
900
|
+
</div>
|
|
901
|
+
|
|
902
|
+
<div className={styles.actions}>
|
|
903
|
+
<button onClick={reset} className={styles.primaryButton}>
|
|
904
|
+
<RefreshCw className={styles.buttonIcon} />
|
|
905
|
+
${tryAgain}
|
|
906
|
+
</button>
|
|
907
|
+
|
|
908
|
+
<Link href='/' className={styles.secondaryButton}>
|
|
909
|
+
<Home className={styles.buttonIcon} />
|
|
910
|
+
${goHome}
|
|
911
|
+
</Link>
|
|
912
|
+
</div>
|
|
913
|
+
|
|
914
|
+
{process.env.NODE_ENV === 'development' && error.digest && (
|
|
915
|
+
<div className={styles.digest}>
|
|
916
|
+
<p className={styles.digestText}>Error ID: {error.digest}</p>
|
|
917
|
+
</div>
|
|
918
|
+
)}
|
|
919
|
+
</div>
|
|
920
|
+
</div>
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
`;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// plain — inline style (토큰 var 활용)
|
|
927
|
+
return `'use client';
|
|
928
|
+
|
|
929
|
+
import * as Sentry from '@sentry/nextjs';
|
|
930
|
+
import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
|
|
931
|
+
${i18nImports}${linkImport}import { useEffect } from 'react';
|
|
932
|
+
|
|
933
|
+
const wrapper: React.CSSProperties = {
|
|
934
|
+
display: 'flex',
|
|
935
|
+
minHeight: '100vh',
|
|
936
|
+
alignItems: 'center',
|
|
937
|
+
justifyContent: 'center',
|
|
938
|
+
padding: '0 16px',
|
|
939
|
+
};
|
|
940
|
+
const card: React.CSSProperties = {
|
|
941
|
+
width: '100%',
|
|
942
|
+
maxWidth: 448,
|
|
943
|
+
borderRadius: 8,
|
|
944
|
+
border: '1px solid var(--border)',
|
|
945
|
+
background: 'var(--background)',
|
|
946
|
+
padding: 24,
|
|
947
|
+
boxShadow: 'var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.15))',
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
export default function Error({
|
|
951
|
+
error,
|
|
952
|
+
reset,
|
|
953
|
+
}: {
|
|
954
|
+
error: Error & { digest?: string };
|
|
955
|
+
reset: () => void;
|
|
956
|
+
}) {
|
|
957
|
+
${tHook} useEffect(() => {
|
|
958
|
+
Sentry.captureException(error);
|
|
959
|
+
}, [error]);
|
|
960
|
+
|
|
961
|
+
return (
|
|
962
|
+
<div style={wrapper}>
|
|
963
|
+
<div style={card}>
|
|
964
|
+
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
|
|
965
|
+
<div
|
|
966
|
+
style={{
|
|
967
|
+
width: 64,
|
|
968
|
+
height: 64,
|
|
969
|
+
borderRadius: '50%',
|
|
970
|
+
background: 'color-mix(in srgb, var(--danger) 10%, transparent)',
|
|
971
|
+
display: 'flex',
|
|
972
|
+
alignItems: 'center',
|
|
973
|
+
justifyContent: 'center',
|
|
974
|
+
}}
|
|
975
|
+
>
|
|
976
|
+
<AlertTriangle style={{ width: 32, height: 32, color: 'var(--danger)' }} />
|
|
977
|
+
</div>
|
|
978
|
+
</div>
|
|
979
|
+
|
|
980
|
+
<h2
|
|
981
|
+
style={{
|
|
982
|
+
fontSize: 24,
|
|
983
|
+
fontWeight: 700,
|
|
984
|
+
textAlign: 'center',
|
|
985
|
+
color: 'var(--foreground)',
|
|
986
|
+
margin: '0 0 8px',
|
|
987
|
+
}}
|
|
988
|
+
>
|
|
989
|
+
${titleText}
|
|
990
|
+
</h2>
|
|
991
|
+
<p
|
|
992
|
+
style={{
|
|
993
|
+
fontSize: 14,
|
|
994
|
+
color: 'var(--foreground-muted)',
|
|
995
|
+
textAlign: 'center',
|
|
996
|
+
margin: '0 0 24px',
|
|
997
|
+
}}
|
|
998
|
+
>
|
|
999
|
+
${descText}
|
|
1000
|
+
</p>
|
|
1001
|
+
|
|
1002
|
+
<div
|
|
1003
|
+
style={{
|
|
1004
|
+
borderRadius: 6,
|
|
1005
|
+
border: '1px solid color-mix(in srgb, var(--danger) 30%, transparent)',
|
|
1006
|
+
background: 'color-mix(in srgb, var(--danger) 5%, transparent)',
|
|
1007
|
+
padding: 12,
|
|
1008
|
+
}}
|
|
1009
|
+
>
|
|
1010
|
+
<p style={{ fontSize: 14, color: 'var(--danger)', margin: 0 }}>
|
|
1011
|
+
{error.message || ${fallback}}
|
|
1012
|
+
</p>
|
|
1013
|
+
</div>
|
|
1014
|
+
|
|
1015
|
+
<div style={{ marginTop: 24, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
1016
|
+
<button
|
|
1017
|
+
onClick={reset}
|
|
1018
|
+
style={{
|
|
1019
|
+
width: '100%',
|
|
1020
|
+
display: 'flex',
|
|
1021
|
+
alignItems: 'center',
|
|
1022
|
+
justifyContent: 'center',
|
|
1023
|
+
gap: 8,
|
|
1024
|
+
padding: '8px 16px',
|
|
1025
|
+
borderRadius: 6,
|
|
1026
|
+
border: 'none',
|
|
1027
|
+
background: 'var(--primary)',
|
|
1028
|
+
color: 'var(--primary-foreground)',
|
|
1029
|
+
fontSize: 14,
|
|
1030
|
+
fontWeight: 500,
|
|
1031
|
+
cursor: 'pointer',
|
|
1032
|
+
}}
|
|
1033
|
+
>
|
|
1034
|
+
<RefreshCw style={{ width: 16, height: 16 }} />
|
|
1035
|
+
${tryAgain}
|
|
1036
|
+
</button>
|
|
1037
|
+
|
|
1038
|
+
<Link
|
|
1039
|
+
href='/'
|
|
1040
|
+
style={{
|
|
1041
|
+
width: '100%',
|
|
1042
|
+
display: 'flex',
|
|
1043
|
+
alignItems: 'center',
|
|
1044
|
+
justifyContent: 'center',
|
|
1045
|
+
gap: 8,
|
|
1046
|
+
padding: '8px 16px',
|
|
1047
|
+
borderRadius: 6,
|
|
1048
|
+
border: '1px solid var(--border)',
|
|
1049
|
+
color: 'var(--foreground)',
|
|
1050
|
+
fontSize: 14,
|
|
1051
|
+
fontWeight: 500,
|
|
1052
|
+
textDecoration: 'none',
|
|
1053
|
+
}}
|
|
1054
|
+
>
|
|
1055
|
+
<Home style={{ width: 16, height: 16 }} />
|
|
1056
|
+
${goHome}
|
|
1057
|
+
</Link>
|
|
1058
|
+
</div>
|
|
1059
|
+
|
|
1060
|
+
{process.env.NODE_ENV === 'development' && error.digest && (
|
|
1061
|
+
<div
|
|
1062
|
+
style={{
|
|
1063
|
+
marginTop: 16,
|
|
1064
|
+
borderRadius: 6,
|
|
1065
|
+
background: 'var(--background-subtle)',
|
|
1066
|
+
padding: 12,
|
|
1067
|
+
}}
|
|
1068
|
+
>
|
|
1069
|
+
<p style={{ fontSize: 12, color: 'var(--foreground-subtle)', margin: 0 }}>
|
|
1070
|
+
Error ID: {error.digest}
|
|
1071
|
+
</p>
|
|
1072
|
+
</div>
|
|
1073
|
+
)}
|
|
1074
|
+
</div>
|
|
1075
|
+
</div>
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function buildErrorModuleCss() {
|
|
1082
|
+
return `.wrapper {
|
|
1083
|
+
display: flex;
|
|
1084
|
+
min-height: 100vh;
|
|
1085
|
+
align-items: center;
|
|
1086
|
+
justify-content: center;
|
|
1087
|
+
padding: 0 16px;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.card {
|
|
1091
|
+
width: 100%;
|
|
1092
|
+
max-width: 448px;
|
|
1093
|
+
border-radius: 8px;
|
|
1094
|
+
border: 1px solid var(--border);
|
|
1095
|
+
background: var(--background);
|
|
1096
|
+
padding: 24px;
|
|
1097
|
+
box-shadow: var(--shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.iconRow {
|
|
1101
|
+
display: flex;
|
|
1102
|
+
justify-content: center;
|
|
1103
|
+
margin-bottom: 16px;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.iconCircle {
|
|
1107
|
+
width: 64px;
|
|
1108
|
+
height: 64px;
|
|
1109
|
+
border-radius: 50%;
|
|
1110
|
+
background: color-mix(in srgb, var(--danger) 10%, transparent);
|
|
1111
|
+
display: flex;
|
|
1112
|
+
align-items: center;
|
|
1113
|
+
justify-content: center;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.icon {
|
|
1117
|
+
width: 32px;
|
|
1118
|
+
height: 32px;
|
|
1119
|
+
color: var(--danger);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.title {
|
|
1123
|
+
font-size: 24px;
|
|
1124
|
+
font-weight: 700;
|
|
1125
|
+
text-align: center;
|
|
1126
|
+
color: var(--foreground);
|
|
1127
|
+
margin: 0 0 8px;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
.description {
|
|
1131
|
+
font-size: 14px;
|
|
1132
|
+
color: var(--foreground-muted);
|
|
1133
|
+
text-align: center;
|
|
1134
|
+
margin: 0 0 24px;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.errorBox {
|
|
1138
|
+
border-radius: 6px;
|
|
1139
|
+
border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
|
|
1140
|
+
background: color-mix(in srgb, var(--danger) 5%, transparent);
|
|
1141
|
+
padding: 12px;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.errorText {
|
|
1145
|
+
font-size: 14px;
|
|
1146
|
+
color: var(--danger);
|
|
1147
|
+
margin: 0;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.actions {
|
|
1151
|
+
margin-top: 24px;
|
|
1152
|
+
display: flex;
|
|
1153
|
+
flex-direction: column;
|
|
1154
|
+
gap: 12px;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.primaryButton,
|
|
1158
|
+
.secondaryButton {
|
|
1159
|
+
width: 100%;
|
|
1160
|
+
display: flex;
|
|
1161
|
+
align-items: center;
|
|
1162
|
+
justify-content: center;
|
|
1163
|
+
gap: 8px;
|
|
1164
|
+
padding: 8px 16px;
|
|
1165
|
+
border-radius: 6px;
|
|
1166
|
+
font-size: 14px;
|
|
1167
|
+
font-weight: 500;
|
|
1168
|
+
cursor: pointer;
|
|
1169
|
+
text-decoration: none;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.primaryButton {
|
|
1173
|
+
border: none;
|
|
1174
|
+
background: var(--primary);
|
|
1175
|
+
color: var(--primary-foreground);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.secondaryButton {
|
|
1179
|
+
border: 1px solid var(--border);
|
|
1180
|
+
color: var(--foreground);
|
|
1181
|
+
background: transparent;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.buttonIcon {
|
|
1185
|
+
width: 16px;
|
|
1186
|
+
height: 16px;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
.digest {
|
|
1190
|
+
margin-top: 16px;
|
|
1191
|
+
border-radius: 6px;
|
|
1192
|
+
background: var(--background-subtle);
|
|
1193
|
+
padding: 12px;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
.digestText {
|
|
1197
|
+
font-size: 12px;
|
|
1198
|
+
color: var(--foreground-subtle);
|
|
1199
|
+
margin: 0;
|
|
1200
|
+
}
|
|
1201
|
+
`;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
513
1204
|
/**
|
|
514
1205
|
* 스캐폴드 마무리 — `gitignore` 파일을 `.gitignore` 로 되돌리고 `git init` 실행.
|
|
515
1206
|
*
|
|
@@ -725,17 +1416,26 @@ async function composeProviders(targetDir, plugins, arch) {
|
|
|
725
1416
|
}
|
|
726
1417
|
}
|
|
727
1418
|
|
|
728
|
-
// wrapper 적용: <
|
|
1419
|
+
// wrapper 적용: <ThemeProvider>...</ThemeProvider> 블록 전체를 잡아 새 wrapper 로 감싼다.
|
|
1420
|
+
// 인덴트 일관성 보장 — 단순 prefix 삽입으로는 내부 라인의 들여쓰기가 wrapper 추가
|
|
1421
|
+
// 깊이만큼 따라 들어가지 않아 들쭉날쭉했다 (v0.59.x 까지의 이슈).
|
|
1422
|
+
// 이제는 블록을 통째로 매칭해 내부 모든 줄에 +2 spaces, 같은 인덴트 레벨에 wrapper
|
|
1423
|
+
// 태그를 두고 재구성한다.
|
|
1424
|
+
const blockRegex = /([ \t]*)(<ThemeProvider>[\s\S]*?<\/ThemeProvider>)/;
|
|
729
1425
|
for (const wrapper of wrappers) {
|
|
730
1426
|
if (content.includes(`<${wrapper}>`)) continue;
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1427
|
+
const match = content.match(blockRegex);
|
|
1428
|
+
if (!match) continue;
|
|
1429
|
+
const [, indent, block] = match;
|
|
1430
|
+
// 블록 내부 모든 줄에 2 space 추가. 첫 줄은 아래 템플릿의 prefix 가 처리하므로
|
|
1431
|
+
// newline 뒤에만 spaces 를 끼워넣는다.
|
|
1432
|
+
const indentedBlock = block.replace(/\n/g, '\n ');
|
|
1433
|
+
const replacement = [
|
|
1434
|
+
`${indent}<${wrapper}>`,
|
|
1435
|
+
`${indent} ${indentedBlock}`,
|
|
1436
|
+
`${indent}</${wrapper}>`,
|
|
1437
|
+
].join('\n');
|
|
1438
|
+
content = content.replace(blockRegex, replacement);
|
|
739
1439
|
}
|
|
740
1440
|
|
|
741
1441
|
await fs.writeFile(globalProviderPath, content);
|