sh-ui-cli 0.57.0 → 0.58.1

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.
Files changed (151) hide show
  1. package/data/changelog/versions.json +27 -0
  2. package/data/registry/react/components/sidebar/index.tsx +3 -3
  3. package/package.json +1 -1
  4. package/src/api.d.ts +34 -0
  5. package/src/api.js +7 -0
  6. package/src/create/architectures/archSchema.js +69 -0
  7. package/src/create/architectures/flat.js +51 -0
  8. package/src/create/architectures/fsd.js +42 -0
  9. package/src/create/architectures/index.js +55 -0
  10. package/src/create/cli-args.js +8 -1
  11. package/src/create/generator.js +101 -32
  12. package/src/create/index.mjs +7 -0
  13. package/src/create/plugins/authJwt.js +14 -8
  14. package/src/create/plugins/nextIntl.js +25 -17
  15. package/src/create/plugins/pluginSchema.js +26 -10
  16. package/src/create/plugins/sentry.js +9 -5
  17. package/src/mcp.mjs +10 -0
  18. package/templates/nextjs-app/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
  19. package/templates/nextjs-app/_arch/flat/app/layout.tsx +16 -0
  20. package/templates/nextjs-app/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
  21. package/templates/nextjs-app/_arch/flat/components/layouts/RootLayout.tsx +11 -0
  22. package/templates/nextjs-app/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
  23. package/templates/nextjs-app/_arch/flat/lib/utils/getQueryClient.ts +14 -0
  24. package/templates/nextjs-app/_arch/flat/tsconfig.json +25 -0
  25. package/templates/nextjs-standalone/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
  26. package/templates/nextjs-standalone/_arch/flat/app/layout.tsx +16 -0
  27. package/templates/nextjs-standalone/_arch/flat/components/common/FallbackBoundary/index.tsx +89 -0
  28. package/templates/nextjs-standalone/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
  29. package/templates/nextjs-standalone/_arch/flat/components/layouts/RootLayout.tsx +11 -0
  30. package/templates/nextjs-standalone/_arch/flat/components/providers/GlobalProvider/index.tsx +23 -0
  31. package/templates/nextjs-standalone/_arch/flat/components/providers/index.tsx +1 -0
  32. package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
  33. package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  34. package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProviders.tsx +12 -0
  35. package/templates/nextjs-standalone/_arch/flat/lib/api/apiTypes.ts +21 -0
  36. package/templates/nextjs-standalone/_arch/flat/lib/api/clientFetch.ts +40 -0
  37. package/templates/nextjs-standalone/_arch/flat/lib/api/error.ts +12 -0
  38. package/templates/nextjs-standalone/_arch/flat/lib/api/http.ts +13 -0
  39. package/templates/nextjs-standalone/_arch/flat/lib/api/observability.ts +20 -0
  40. package/templates/nextjs-standalone/_arch/flat/lib/api/queryClient.ts +30 -0
  41. package/templates/nextjs-standalone/_arch/flat/lib/api/serverFetch.ts +59 -0
  42. package/templates/nextjs-standalone/_arch/flat/lib/hooks/useAppMutation.ts +52 -0
  43. package/templates/nextjs-standalone/_arch/flat/lib/test/createTestQueryClient.ts +18 -0
  44. package/templates/nextjs-standalone/_arch/flat/lib/test/index.ts +2 -0
  45. package/templates/nextjs-standalone/_arch/flat/lib/test/renderWithProviders.tsx +40 -0
  46. package/templates/nextjs-standalone/_arch/flat/lib/utils/formatDate.ts +22 -0
  47. package/templates/nextjs-standalone/_arch/flat/lib/utils/formatPrice.ts +10 -0
  48. package/templates/nextjs-standalone/_arch/flat/lib/utils/getQueryClient.ts +14 -0
  49. package/templates/nextjs-standalone/_arch/flat/sh-ui.config.json +19 -0
  50. package/templates/nextjs-standalone/_arch/flat/tsconfig.json +41 -0
  51. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +23 -0
  52. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/index.tsx +1 -0
  53. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  54. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +12 -0
  55. package/templates/nextjs-standalone/_arch/fsd/src/entities/.gitkeep +0 -0
  56. package/templates/nextjs-standalone/_arch/fsd/src/features/.gitkeep +0 -0
  57. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/.gitkeep +0 -0
  58. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/apiTypes.ts +21 -0
  59. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/clientFetch.ts +40 -0
  60. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/error.ts +12 -0
  61. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/http.ts +13 -0
  62. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/observability.ts +20 -0
  63. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/queryClient.ts +30 -0
  64. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/serverFetch.ts +59 -0
  65. package/templates/nextjs-standalone/_arch/fsd/src/shared/config/.gitkeep +0 -0
  66. package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/.gitkeep +0 -0
  67. package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/useAppMutation.ts +52 -0
  68. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatDate.ts +22 -0
  69. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatPrice.ts +10 -0
  70. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/utils.ts +6 -0
  71. package/templates/nextjs-standalone/_arch/fsd/src/shared/model/.gitkeep +0 -0
  72. package/templates/nextjs-standalone/_arch/fsd/src/shared/styles/tokens.css +135 -0
  73. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/createTestQueryClient.ts +18 -0
  74. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/index.ts +2 -0
  75. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/renderWithProviders.tsx +40 -0
  76. package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/.gitkeep +0 -0
  77. package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/FallbackBoundary/index.tsx +89 -0
  78. package/templates/nextjs-standalone/_arch/fsd/src/views/.gitkeep +0 -0
  79. package/templates/nextjs-standalone/_arch/fsd/src/widgets/.gitkeep +0 -0
  80. /package/templates/nextjs-app/{src/entities → _arch/flat/components/common}/.gitkeep +0 -0
  81. /package/templates/nextjs-app/{src/shared/ui → _arch/flat/components/common}/FallbackBoundary/index.tsx +0 -0
  82. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/GlobalProvider/index.tsx +0 -0
  83. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/index.tsx +0 -0
  84. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
  85. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/theme/ThemeProviders.tsx +0 -0
  86. /package/templates/nextjs-app/{src/features → _arch/flat/lib/api}/.gitkeep +0 -0
  87. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/apiTypes.ts +0 -0
  88. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/clientFetch.ts +0 -0
  89. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/error.ts +0 -0
  90. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/http.ts +0 -0
  91. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/observability.ts +0 -0
  92. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/queryClient.ts +0 -0
  93. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/serverFetch.ts +0 -0
  94. /package/templates/nextjs-app/{src/shared/api → _arch/flat/lib/config}/.gitkeep +0 -0
  95. /package/templates/nextjs-app/{src/shared/config → _arch/flat/lib/hooks}/.gitkeep +0 -0
  96. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/hooks/useAppMutation.ts +0 -0
  97. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/createTestQueryClient.ts +0 -0
  98. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/index.ts +0 -0
  99. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/renderWithProviders.tsx +0 -0
  100. /package/templates/nextjs-app/{src/shared/hooks → _arch/flat/lib/utils}/.gitkeep +0 -0
  101. /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatDate.ts +0 -0
  102. /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatPrice.ts +0 -0
  103. /package/templates/nextjs-app/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
  104. /package/templates/nextjs-app/{app → _arch/fsd/app}/layout.tsx +0 -0
  105. /package/templates/nextjs-app/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
  106. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/GlobalProvider/index.tsx +0 -0
  107. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/index.tsx +0 -0
  108. /package/templates/nextjs-app/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
  109. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
  110. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/theme/ThemeProviders.tsx +0 -0
  111. /package/templates/nextjs-app/{src/shared/lib → _arch/fsd/src/entities}/.gitkeep +0 -0
  112. /package/templates/nextjs-app/{src/shared/model → _arch/fsd/src/features}/.gitkeep +0 -0
  113. /package/templates/nextjs-app/{src/shared/ui → _arch/fsd/src/shared/api}/.gitkeep +0 -0
  114. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/apiTypes.ts +0 -0
  115. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/clientFetch.ts +0 -0
  116. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/error.ts +0 -0
  117. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/http.ts +0 -0
  118. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/observability.ts +0 -0
  119. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/queryClient.ts +0 -0
  120. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/serverFetch.ts +0 -0
  121. /package/templates/nextjs-app/{src/views → _arch/fsd/src/shared/config}/.gitkeep +0 -0
  122. /package/templates/nextjs-app/{src/widgets → _arch/fsd/src/shared/hooks}/.gitkeep +0 -0
  123. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/hooks/useAppMutation.ts +0 -0
  124. /package/templates/{nextjs-standalone/src/entities → nextjs-app/_arch/fsd/src/shared/lib}/.gitkeep +0 -0
  125. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatDate.ts +0 -0
  126. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatPrice.ts +0 -0
  127. /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
  128. /package/templates/{nextjs-standalone/src/features → nextjs-app/_arch/fsd/src/shared/model}/.gitkeep +0 -0
  129. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/createTestQueryClient.ts +0 -0
  130. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/index.ts +0 -0
  131. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/renderWithProviders.tsx +0 -0
  132. /package/templates/{nextjs-standalone/src/shared/api → nextjs-app/_arch/fsd/src/shared/ui}/.gitkeep +0 -0
  133. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/ui/FallbackBoundary/index.tsx +0 -0
  134. /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
  135. /package/templates/{nextjs-standalone/src/shared/config → nextjs-app/_arch/fsd/src/views}/.gitkeep +0 -0
  136. /package/templates/{nextjs-standalone/src/shared/hooks → nextjs-app/_arch/fsd/src/widgets}/.gitkeep +0 -0
  137. /package/templates/nextjs-app/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
  138. /package/templates/nextjs-standalone/{src/shared/model → _arch/flat/components/common}/.gitkeep +0 -0
  139. /package/templates/nextjs-standalone/{src/shared/ui → _arch/flat/lib/api}/.gitkeep +0 -0
  140. /package/templates/nextjs-standalone/{src/views → _arch/flat/lib/config}/.gitkeep +0 -0
  141. /package/templates/nextjs-standalone/{src/widgets → _arch/flat/lib/hooks}/.gitkeep +0 -0
  142. /package/templates/nextjs-standalone/{src/shared → _arch/flat/lib}/styles/tokens.css +0 -0
  143. /package/templates/nextjs-standalone/{src/shared/lib → _arch/flat/lib/utils}/utils.ts +0 -0
  144. /package/templates/nextjs-standalone/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
  145. /package/templates/nextjs-standalone/{app → _arch/fsd/app}/layout.tsx +0 -0
  146. /package/templates/nextjs-standalone/{sh-ui.config.json → _arch/fsd/sh-ui.config.json} +0 -0
  147. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
  148. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
  149. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
  150. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
  151. /package/templates/nextjs-standalone/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
@@ -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.58.1",
7
+ "date": "2026-05-06",
8
+ "title": "fix — Sidebar focus trap 의 TS strict 위반 (firstEl/lastEl possibly undefined)",
9
+ "type": "patch",
10
+ "highlights": [
11
+ "**Sidebar `useFocusTrap` 의 TS strict 위반 해소** — `items.length === 0` 가드 후에도 `items[0]` / `items[items.length - 1]` 가 `possibly undefined` 로 잡혀 사용자 프로젝트 (strict tsconfig) 에서 typecheck 실패하던 문제. 가드를 `firstEl`/`lastEl` 직접 null-check 로 재구성 — 동작 동일, TS 만족. 사용자가 sh-ui Sidebar 컴포넌트를 add 후 본인 프로젝트에서 `tsc --noEmit` 돌릴 때 막히던 케이스 해소."
12
+ ],
13
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.58.1"
14
+ },
15
+ {
16
+ "version": "0.58.0",
17
+ "date": "2026-05-05",
18
+ "title": "아키텍처 (arch) 1급 시민 도입 + flat 추가",
19
+ "type": "minor",
20
+ "highlights": [
21
+ "**`--arch` CLI 플래그 + MCP `arch` 파라미터 신설** — 프로젝트의 *모양* (폴더 구조, import alias 컨벤션, tsconfig paths) 을 1급 개념으로 분리. 플러그인과 직교하는 별도 축 — 사용자는 arch 한 개 + 플러그인 N 개를 동시에 선택. 미지정 시 기본 `fsd` (v0.57 까지의 동작과 동일).",
22
+ "**아키텍처 디스크립터 모델** — `src/create/architectures/{fsd,flat}.js` 가 8개 논리 키 (`layouts`, `providers`, `api`, `config`, `hooks`, `utils`, `ui`, `test`) 의 fs 경로 / import alias / tsconfig paths 를 선언. 플러그인은 하드코딩된 `src/shared/api/...` 대신 `arch.paths.api` / `arch.aliases.api` 를 조회해 산출물 emit — 한 번 작성된 플러그인이 모든 arch 에서 동작.",
23
+ "**`flat` 아키텍처 추가** — 슬라이스 (entities/features/widgets/views) 없는 vanilla Next 관용 구조 (`lib/api`, `lib/utils`, `components/layouts`, `components/providers`, `components/common`). 토이 프로젝트, 데모, 일회성 도구처럼 FSD 6 레이어가 과한 경우용. tsconfig paths 는 카테고리별 scoped (`@/lib/*`, `@/components/*`, `@/app/*`) — FSD 의 catch-all `@/*` 와 의도적으로 다른 컨벤션.",
24
+ "**플러그인 3개 (sentry/next-intl/auth-jwt) 모두 arch-aware 로 리팩토링** — files / transforms / preExport / providerImports 가 `(arch) => ...` 함수형으로 전환. 회귀 가드: FSD 디스크립터 값이 v0.57 까지의 하드코딩과 1:1 일치하므로 FSD 사용자 입장 변화 0.",
25
+ "**Cross-platform 대비** — 디스크립터에 `platforms: ['next' | 'flutter']` 필드 박힘. 현재 fsd/flat 둘 다 next 만 지원하지만, 향후 `flutter-clean` / `flutter-bloc` 같은 Flutter arch 추가 시 generator/CLI 수정 0 — 디스크립터만 추가하면 됨.",
26
+ "**베이스 템플릿 재구조화** — `templates/nextjs-app` / `templates/nextjs-standalone` 이 arch-neutral 베이스 + `_arch/{fsd,flat}/` 오버레이로 분리. generator 가 base 카피 후 선택된 arch 의 오버레이를 머지. 새 arch 추가 절차는 `packages/cli/ARCHITECTURE.md` 에 문서화.",
27
+ "**docs 사이드바에 Architectures 그룹 신설** — `/architectures` 허브 + `/architectures/fsd` + `/architectures/flat` 페이지. arch 선택 가이드 + 폴더 구조 시각화 + FSD ↔ flat 마이그레이션 절차.",
28
+ "**스모크 매트릭스 9개 시나리오 추가** — flat × {none, sentry, next-intl, auth-jwt, all 3} + flat monorepo 1개 + FSD 회귀 가드 2개. 핵심 검증: flat 에 src/ 부재, 어떤 .ts/.tsx 파일에도 `@/src/...` import 누수 없음, 플러그인 산출물이 arch-correct 경로로 떨어짐."
29
+ ],
30
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.58.0"
31
+ },
5
32
  {
6
33
  "version": "0.57.0",
7
34
  "date": "2026-05-04",
@@ -51,12 +51,12 @@ function useFocusTrap(
51
51
  }
52
52
  if (e.key !== "Tab") return;
53
53
  const items = focusables();
54
- if (items.length === 0) {
54
+ const firstEl = items[0];
55
+ const lastEl = items[items.length - 1];
56
+ if (!firstEl || !lastEl) {
55
57
  e.preventDefault();
56
58
  return;
57
59
  }
58
- const firstEl = items[0];
59
- const lastEl = items[items.length - 1];
60
60
  if (e.shiftKey && document.activeElement === firstEl) {
61
61
  e.preventDefault();
62
62
  lastEl.focus();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.57.0",
3
+ "version": "0.58.1",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/api.d.ts CHANGED
@@ -56,6 +56,40 @@ export type PluginManifest = {
56
56
 
57
57
  export const allPlugins: readonly PluginManifest[];
58
58
 
59
+ /* ─────── 아키텍처 (arch) ─────── */
60
+
61
+ /** 논리 키 셋 — 모든 arch 디스크립터가 동일하게 노출하는 카테고리. */
62
+ export type ArchPathKey =
63
+ | 'layouts'
64
+ | 'providers'
65
+ | 'api'
66
+ | 'config'
67
+ | 'hooks'
68
+ | 'utils'
69
+ | 'ui'
70
+ | 'test';
71
+
72
+ export type ArchManifest = {
73
+ /** kebab-case 식별자 (예: "fsd", "flat"). */
74
+ name: string;
75
+ label: string;
76
+ description: string;
77
+ /** 이 arch 가 적용 가능한 플랫폼들. */
78
+ platforms: readonly CreatePlatform[];
79
+ /** 논리 키 → 파일시스템 경로 (앱 루트 기준). */
80
+ paths: Record<ArchPathKey, string>;
81
+ /** 논리 키 → import alias prefix (TS 코드 emit 시). */
82
+ aliases: Record<ArchPathKey, string>;
83
+ /** tsconfig.json 의 paths 블록에 그대로 들어갈 객체. */
84
+ tsconfigPaths: Record<string, string[]>;
85
+ };
86
+
87
+ export const allArchitectures: readonly ArchManifest[];
88
+ export const DEFAULT_ARCH: 'fsd';
89
+ export function getArchByName(name: string): ArchManifest;
90
+ export function getArchesForPlatform(platform: CreatePlatform): readonly ArchManifest[];
91
+ export function isKnownArch(name: string): boolean;
92
+
59
93
  /* ─────── 테마 프리셋 ─────── */
60
94
 
61
95
  export type ThemePresetName = 'neutral' | 'slate' | 'rose' | 'emerald' | 'violet';
package/src/api.js CHANGED
@@ -23,4 +23,11 @@ export {
23
23
  } from './constants.js';
24
24
 
25
25
  export { allPlugins } from './create/plugins/index.js';
26
+ export {
27
+ allArchitectures,
28
+ DEFAULT_ARCH,
29
+ getArchByName,
30
+ getArchesForPlatform,
31
+ isKnownArch,
32
+ } from './create/architectures/index.js';
26
33
  export { THEME_PRESETS, THEME_PRESET_NAMES } from './create/theme/presets.js';
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * 아키텍처 디스크립터 스키마.
5
+ * architectures/index.js 로딩 시 모든 디스크립터를 이 스키마로 validate 한다.
6
+ *
7
+ * arch 는 플러그인과 별개의 1급 개념이다 — 프로젝트의 *모양* (폴더 구조,
8
+ * import alias 컨벤션) 을 정의하고, 플러그인은 이 디스크립터의 논리 키
9
+ * (`paths.api`, `aliases.providers` 등) 를 조회해 자신의 산출물을 emit 한다.
10
+ *
11
+ * 디자인 원칙:
12
+ * - 논리 키는 arch 중립적 — `layouts`, `providers`, `api`, `config`, `hooks`,
13
+ * `utils`, `ui`, `test`. FSD 의 `shared/*` 같은 슬라이스 명칭이 키에 누수되지 않게.
14
+ * - 모든 arch 는 **동일한 키 셋** 을 노출해야 함 (플러그인이 안전하게 조회).
15
+ * 누락 시 스키마가 즉시 거부.
16
+ * - `paths` 는 fs 경로 (앱 루트 기준 상대), `aliases` 는 import 시 사용할 prefix.
17
+ * 둘은 1:1 대응 (paths.foo 디렉토리 = aliases.foo 가 가리키는 곳).
18
+ * - `tsconfigPaths` 는 tsconfig.json 의 paths 블록에 그대로 들어가는 객체.
19
+ * arch 마다 `@/*` 한 줄일 수도, 더 세분화될 수도 있음.
20
+ */
21
+
22
+ const PathKeys = z.object({
23
+ layouts: z.string().min(1),
24
+ providers: z.string().min(1),
25
+ api: z.string().min(1),
26
+ config: z.string().min(1),
27
+ hooks: z.string().min(1),
28
+ utils: z.string().min(1),
29
+ ui: z.string().min(1),
30
+ test: z.string().min(1),
31
+ });
32
+
33
+ export const ArchSchema = z.object({
34
+ name: z.string().regex(/^[a-z][a-z0-9-]*$/, {
35
+ message: 'Architecture name must be lowercase kebab-case (e.g., "fsd", "flat")',
36
+ }),
37
+ label: z.string().min(1),
38
+ description: z.string().min(1),
39
+
40
+ // 이 arch 가 적용 가능한 플랫폼들. CLI 에서 platform/arch 조합이 호환되는지 검증.
41
+ // 예: fsd/flat 은 ['next'], 미래에 추가될 flutter-clean 은 ['flutter'].
42
+ // 같은 이름의 arch 가 두 플랫폼 모두 지원하는 케이스도 가능 (예: 'flat' 가 양쪽).
43
+ platforms: z.array(z.enum(['next', 'flutter'])).min(1),
44
+
45
+ // 논리 키 → 파일시스템 경로 (앱 루트 기준 상대)
46
+ paths: PathKeys,
47
+
48
+ // 논리 키 → import alias prefix (TS 코드에서 emit 할 때 사용)
49
+ aliases: PathKeys,
50
+
51
+ // tsconfig.json 의 paths 필드에 그대로 들어가는 객체
52
+ // 예: FSD 는 { '@/*': ['./*'] }, flat 은 더 세분화
53
+ // Flutter arch 의 경우 이 필드는 사실상 무시됨 (Dart 는 tsconfig 가 없음).
54
+ tsconfigPaths: z.record(z.string(), z.array(z.string())),
55
+ });
56
+
57
+ export function validateArchitectures(architectures) {
58
+ for (const arch of architectures) {
59
+ const result = ArchSchema.safeParse(arch);
60
+ if (!result.success) {
61
+ const issues = result.error.issues
62
+ .map((i) => ` - ${i.path.join('.')}: ${i.message}`)
63
+ .join('\n');
64
+ throw new Error(
65
+ `Invalid architecture descriptor "${arch?.name ?? '(unknown)'}":\n${issues}`,
66
+ );
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Flat 아키텍처 디스크립터.
3
+ *
4
+ * 슬라이스 없이 vanilla Next.js 관용에 가까운 폴더 구조 — `lib/` (유틸/설정),
5
+ * `components/` (UI/레이아웃/프로바이더). 토이 프로젝트, 데모, 일회성 도구처럼
6
+ * FSD 의 6 레이어가 과한 경우에 적합.
7
+ *
8
+ * tsconfig 의 `paths` 는 FSD 의 전역 `@/*` 와 다르게 *카테고리별 scoped alias* 를
9
+ * 쓴다 (`@/lib/*`, `@/components/*`, `@/app/*`). 이유는 두 가지:
10
+ *
11
+ * 1. flat 은 import 가 짧은 게 강점 — `@/lib/api/x` 처럼 카테고리가 alias 에 박혀
12
+ * 있으면 한 번 보고 어디 파일인지 식별. 전역 `@/*` 보다 가독성 ↑.
13
+ * 2. 미래에 다른 arch 를 추가할 때 alias 컨벤션 차이가 arch 정체성의 일부가
14
+ * 될 수 있도록 — clean/MVC 등은 또 자기만의 alias 패턴을 가질 수 있다.
15
+ */
16
+ export const flatArch = {
17
+ name: 'flat',
18
+ label: 'Flat',
19
+ description:
20
+ '슬라이스 없는 관용적 Next.js 구조 (lib/components/app). 작은 프로젝트, 데모, 일회성 도구에 적합.',
21
+ platforms: ['next'],
22
+
23
+ paths: {
24
+ layouts: 'components/layouts',
25
+ providers: 'components/providers',
26
+ api: 'lib/api',
27
+ config: 'lib/config',
28
+ hooks: 'lib/hooks',
29
+ utils: 'lib/utils',
30
+ ui: 'components/common',
31
+ test: 'lib/test',
32
+ },
33
+
34
+ aliases: {
35
+ layouts: '@/components/layouts',
36
+ providers: '@/components/providers',
37
+ api: '@/lib/api',
38
+ config: '@/lib/config',
39
+ hooks: '@/lib/hooks',
40
+ utils: '@/lib/utils',
41
+ ui: '@/components/common',
42
+ test: '@/lib/test',
43
+ },
44
+
45
+ // Scoped alias — 카테고리별로 명시. FSD 의 catch-all `@/*` 와 의도적으로 다름.
46
+ tsconfigPaths: {
47
+ '@/lib/*': ['./lib/*'],
48
+ '@/components/*': ['./components/*'],
49
+ '@/app/*': ['./app/*'],
50
+ },
51
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Feature-Sliced Design 아키텍처 디스크립터.
3
+ *
4
+ * sh-ui 의 디폴트 arch — 슬라이스 (entities, features, widgets, views, shared, app) 기반.
5
+ * 플러그인이 emit 하는 산출물의 fs 경로/import alias 를 결정한다.
6
+ *
7
+ * 본 디스크립터의 값은 sh-ui v0.57 까지의 하드코딩된 경로와 1:1 일치 —
8
+ * 즉 v0.58 에서 FSD 사용자 입장 변화 0 이어야 한다 (회귀 가드는 smoke matrix 가).
9
+ */
10
+ export const fsdArch = {
11
+ name: 'fsd',
12
+ label: 'Feature-Sliced Design',
13
+ description:
14
+ '슬라이스 기반 폴더 구조 (entities/features/widgets/views/shared/app). 도메인이 큰 프로젝트에 적합.',
15
+ platforms: ['next'],
16
+
17
+ paths: {
18
+ layouts: 'src/app/layouts',
19
+ providers: 'src/app/providers',
20
+ api: 'src/shared/api',
21
+ config: 'src/shared/config',
22
+ hooks: 'src/shared/hooks',
23
+ utils: 'src/shared/lib',
24
+ ui: 'src/shared/ui',
25
+ test: 'src/shared/test',
26
+ },
27
+
28
+ aliases: {
29
+ layouts: '@/src/app/layouts',
30
+ providers: '@/src/app/providers',
31
+ api: '@/src/shared/api',
32
+ config: '@/src/shared/config',
33
+ hooks: '@/src/shared/hooks',
34
+ utils: '@/src/shared/lib',
35
+ ui: '@/src/shared/ui',
36
+ test: '@/src/shared/test',
37
+ },
38
+
39
+ tsconfigPaths: {
40
+ '@/*': ['./*'],
41
+ },
42
+ };
@@ -0,0 +1,55 @@
1
+ import { fsdArch } from './fsd.js';
2
+ import { flatArch } from './flat.js';
3
+ import { validateArchitectures } from './archSchema.js';
4
+
5
+ export const allArchitectures = [fsdArch, flatArch];
6
+
7
+ // 모듈 로드 시점에 모든 arch 디스크립터를 schema 로 검증.
8
+ // 누락된 키, 잘못된 형태가 있으면 즉시 에러.
9
+ validateArchitectures(allArchitectures);
10
+
11
+ export const DEFAULT_ARCH = 'fsd';
12
+
13
+ export function getArchChoices() {
14
+ return allArchitectures.map((a) => ({
15
+ name: `${a.label} — ${a.description}`,
16
+ value: a.name,
17
+ }));
18
+ }
19
+
20
+ export function getArchByName(name) {
21
+ const arch = allArchitectures.find((a) => a.name === name);
22
+ if (!arch) {
23
+ const known = allArchitectures.map((a) => a.name).join(', ');
24
+ throw new Error(`Unknown architecture "${name}". Available: ${known}`);
25
+ }
26
+ return arch;
27
+ }
28
+
29
+ export function isKnownArch(name) {
30
+ return allArchitectures.some((a) => a.name === name);
31
+ }
32
+
33
+ /**
34
+ * platform 에서 사용 가능한 arch 만 필터링.
35
+ * 예: 'next' → fsd, flat / 'flutter' → (현재 없음, 향후 flutter-* 추가 시 노출).
36
+ */
37
+ export function getArchesForPlatform(platform) {
38
+ return allArchitectures.filter((a) => a.platforms.includes(platform));
39
+ }
40
+
41
+ /**
42
+ * 주어진 arch 가 platform 과 호환되는지 검증. 호환 안 되면 친절한 에러.
43
+ * generator/cli-args 양쪽에서 호출.
44
+ */
45
+ export function assertArchPlatformCompat(archName, platform) {
46
+ const arch = getArchByName(archName);
47
+ if (!arch.platforms.includes(platform)) {
48
+ const supported = getArchesForPlatform(platform).map((a) => a.name).join(', ') || '(없음)';
49
+ throw new Error(
50
+ `--arch=${archName} 는 platform=${platform} 와 호환되지 않습니다. ` +
51
+ `${platform} 에서 사용 가능: ${supported}`,
52
+ );
53
+ }
54
+ return arch;
55
+ }
@@ -5,12 +5,14 @@ import {
5
5
  CSS_FRAMEWORKS_PLANNED,
6
6
  } from '../constants.js';
7
7
  import { allPlugins } from './plugins/index.js';
8
+ import { allArchitectures } from './architectures/index.js';
8
9
 
9
10
  const VALID_PLATFORMS = CREATE_PLATFORMS;
10
11
  const VALID_STRUCTURES = CREATE_STRUCTURES;
11
12
  const VALID_PLUGINS = allPlugins.map((p) => p.name);
13
+ const VALID_ARCHES = allArchitectures.map((a) => a.name);
12
14
 
13
- const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css'];
15
+ const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css', 'arch'];
14
16
  const BOOL_FLAGS = ['yes', 'help', 'dry-run'];
15
17
 
16
18
  const SUBCOMMANDS = ['add-app', 'add-component'];
@@ -68,6 +70,11 @@ export const parseArgs = (argv) => {
68
70
  if (name === 'structure' && !VALID_STRUCTURES.includes(value)) {
69
71
  throw new Error(`--structure 는 ${VALID_STRUCTURES.join('/')} 중 하나여야 함 (받은 값: ${value})`);
70
72
  }
73
+ if (name === 'arch' && !VALID_ARCHES.includes(value)) {
74
+ throw new Error(
75
+ `--arch 는 ${VALID_ARCHES.join('/')} 중 하나여야 함 (받은 값: ${value})`,
76
+ );
77
+ }
71
78
  if (name === 'css' && !CSS_FRAMEWORKS_SUPPORTED.includes(value)) {
72
79
  // planned 값은 '곧 옵니다' 신호로 분기 — 사용자 의도가 더 명확히 전달.
73
80
  if (CSS_FRAMEWORKS_PLANNED.includes(value)) {
@@ -4,6 +4,11 @@ import fs from 'fs-extra';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { getPluginChoices, getPluginsByNames } from './plugins/index.js';
7
+ import {
8
+ assertArchPlatformCompat,
9
+ getArchesForPlatform,
10
+ DEFAULT_ARCH,
11
+ } from './architectures/index.js';
7
12
  import { resolveTheme } from './theme/decode.js';
8
13
  import { THEME_PRESETS, getThemePreset } from './theme/presets.js';
9
14
  import {
@@ -90,6 +95,19 @@ export async function createProject(options = {}) {
90
95
  ],
91
96
  });
92
97
 
98
+ // arch 결정 — platform 확정 후. 사용자가 --arch 미지정 시:
99
+ // - next → DEFAULT_ARCH ('fsd')
100
+ // - flutter → 현재 Flutter arch 디스크립터 없음 → null. 미래에 flutter arch 추가되면
101
+ // 해당 platform 의 첫 번째 arch 또는 별도 default 로 변경.
102
+ // 명시한 경우 platform 호환성 검증 후 디스크립터 resolve. 잘못된 조합은 친절한 에러.
103
+ let arch = null;
104
+ if (platform === 'next') {
105
+ const archName = options.arch ?? DEFAULT_ARCH;
106
+ arch = assertArchPlatformCompat(archName, 'next');
107
+ } else if (platform === 'flutter' && options.arch) {
108
+ arch = assertArchPlatformCompat(options.arch, 'flutter');
109
+ }
110
+
93
111
  // CSS 프레임워크 — 현재는 plain 만 지원하지만, 곧 추가될 옵션을 disabled 로
94
112
  // 미리 노출해 사용자가 변종 시스템의 존재를 인지할 수 있게 한다.
95
113
  // Flutter 는 CSS 프레임워크 개념이 무의미하므로 자동 plain.
@@ -186,9 +204,9 @@ export async function createProject(options = {}) {
186
204
  plugins.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
187
205
 
188
206
  if (projectType === 'standalone') {
189
- await generateStandalone(targetDir, projectName, plugins, theme, cssFramework);
207
+ await generateStandalone(targetDir, projectName, plugins, theme, cssFramework, arch);
190
208
  } else {
191
- await generateMonorepo(targetDir, projectName, plugins, { yes: options.yes, theme, css: cssFramework });
209
+ await generateMonorepo(targetDir, projectName, plugins, { yes: options.yes, theme, css: cssFramework, arch });
192
210
  }
193
211
 
194
212
  await finalizeProject(targetDir, { dryRun: options.dryRun });
@@ -267,7 +285,13 @@ export async function addApp() {
267
285
  return;
268
286
  }
269
287
 
270
- await generateApp(appsDir, appName, port, plugins);
288
+ // addApp 기존 monorepo 의 새 앱 추가 — arch 는 일관성을 위해 모노레포가 처음
289
+ // 만들어질 때 정한 값과 같아야 한다. 현재는 root sh-ui.config.json 등에 별도 저장
290
+ // 안 해 두므로 일단 DEFAULT_ARCH (fsd) 로 fallback. 향후 root config 에 arch 박아두고
291
+ // 여기서 읽어오는 흐름으로 개선 가능.
292
+ const arch = assertArchPlatformCompat(DEFAULT_ARCH, 'next');
293
+
294
+ await generateApp(appsDir, appName, port, plugins, arch);
271
295
 
272
296
  console.log(`\n✅ apps/${appName} 이 추가되었습니다!`);
273
297
  console.log('\n pnpm install');
@@ -355,8 +379,16 @@ async function generateFlutter(targetDir, projectName, theme, css) {
355
379
  await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css);
356
380
  }
357
381
 
358
- async function generateStandalone(targetDir, projectName, plugins, theme, css) {
359
- await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-standalone'), targetDir);
382
+ async function generateStandalone(targetDir, projectName, plugins, theme, css, arch) {
383
+ // 베이스 (arch-neutral) + arch 오버레이 — generateApp 과 같은 패턴.
384
+ await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-standalone'), targetDir, {
385
+ filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
386
+ });
387
+ await fs.copy(
388
+ path.join(TEMPLATES_DIR, 'nextjs-standalone', '_arch', arch.name),
389
+ targetDir,
390
+ { overwrite: true },
391
+ );
360
392
 
361
393
  // Update package.json
362
394
  const pkgPath = path.join(targetDir, 'package.json');
@@ -372,16 +404,16 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css) {
372
404
  }
373
405
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
374
406
 
375
- await writeNextConfig(targetDir, plugins, { isMonorepo: false });
407
+ await writeNextConfig(targetDir, plugins, { isMonorepo: false, arch });
376
408
  await appendEnvVars(path.join(targetDir, '.env.example'), plugins);
377
- await writePluginFiles(targetDir, plugins);
378
- await composeProviders(targetDir, plugins);
379
- await applyTransforms(targetDir, plugins);
409
+ await writePluginFiles(targetDir, plugins, arch);
410
+ await composeProviders(targetDir, plugins, arch);
411
+ await applyTransforms(targetDir, plugins, arch);
380
412
  await injectCssTheme(targetDir, theme);
381
413
  await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css);
382
414
  }
383
415
 
384
- async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css } = {}) {
416
+ async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch } = {}) {
385
417
  await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
386
418
 
387
419
  // Update root package.json
@@ -412,14 +444,24 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
412
444
  });
413
445
 
414
446
  const appsDir = path.join(targetDir, 'apps', appName);
415
- await generateApp(appsDir, appName, port, plugins);
447
+ await generateApp(appsDir, appName, port, plugins, arch);
416
448
  const uiAppDir = path.join(targetDir, 'packages', 'ui', 'ui-apps', `ui-${appName}`);
417
449
  await injectCssTheme(uiAppDir, theme);
418
450
  await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css);
419
451
  }
420
452
 
421
- async function generateApp(targetDir, appName, port, plugins) {
422
- await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-app'), targetDir);
453
+ async function generateApp(targetDir, appName, port, plugins, arch) {
454
+ // 베이스 템플릿 (arch-neutral 파일들) 만 카피 — _arch/ 디렉토리는 스킵.
455
+ // 그 후 선택된 arch 의 오버레이를 위에 머지해 arch-coupled 파일들 (layout.tsx,
456
+ // src/ 또는 lib+components/, tsconfig.json paths 블록 등) 을 떨어뜨린다.
457
+ await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-app'), targetDir, {
458
+ filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
459
+ });
460
+ await fs.copy(
461
+ path.join(TEMPLATES_DIR, 'nextjs-app', '_arch', arch.name),
462
+ targetDir,
463
+ { overwrite: true },
464
+ );
423
465
 
424
466
  // Replace ui-app-name placeholder with actual app name in all files
425
467
  await replaceInAllFiles(targetDir, 'ui-app-name', `ui-${appName}`);
@@ -440,7 +482,7 @@ async function generateApp(targetDir, appName, port, plugins) {
440
482
  }
441
483
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
442
484
 
443
- await writeNextConfig(targetDir, plugins, { isMonorepo: true, appName });
485
+ await writeNextConfig(targetDir, plugins, { isMonorepo: true, appName, arch });
444
486
 
445
487
  // Update Dockerfile
446
488
  const dockerPath = path.join(targetDir, 'Dockerfile');
@@ -461,9 +503,9 @@ async function generateApp(targetDir, appName, port, plugins) {
461
503
  }
462
504
 
463
505
  await appendEnvVars(path.join(targetDir, '.env.example'), plugins);
464
- await writePluginFiles(targetDir, plugins);
465
- await composeProviders(targetDir, plugins);
466
- await applyTransforms(targetDir, plugins);
506
+ await writePluginFiles(targetDir, plugins, arch);
507
+ await composeProviders(targetDir, plugins, arch);
508
+ await applyTransforms(targetDir, plugins, arch);
467
509
  }
468
510
 
469
511
  // ─── Helpers ───
@@ -509,7 +551,7 @@ async function replaceInAllFiles(dir, search, replace) {
509
551
  }
510
552
  }
511
553
 
512
- async function writeNextConfig(targetDir, plugins, { isMonorepo, appName }) {
554
+ async function writeNextConfig(targetDir, plugins, { isMonorepo, appName, arch }) {
513
555
  const imports = [`import type { NextConfig } from 'next';`];
514
556
  const preExport = [];
515
557
  let configBody;
@@ -535,7 +577,8 @@ async function writeNextConfig(targetDir, plugins, { isMonorepo, appName }) {
535
577
 
536
578
  for (const plugin of plugins) {
537
579
  if (plugin.imports) imports.push(...plugin.imports);
538
- if (plugin.preExport) preExport.push(...plugin.preExport);
580
+ const pluginPreExport = resolveArchField(plugin.preExport, arch);
581
+ if (pluginPreExport) preExport.push(...pluginPreExport);
539
582
  }
540
583
 
541
584
  let exportExpr = 'nextConfig';
@@ -569,10 +612,25 @@ async function appendEnvVars(envPath, plugins) {
569
612
  }
570
613
  }
571
614
 
572
- async function writePluginFiles(targetDir, plugins) {
615
+ /**
616
+ * 플러그인 manifest 의 함수형 필드를 arch 디스크립터로 평가해 정적 값으로 변환.
617
+ * Layer 1 에서는 모든 플러그인이 정적이라 사실상 no-op 분기. Layer 2 에서 플러그인이
618
+ * `files: (arch) => ({...})` 형태로 전환되면 이 헬퍼가 함수를 호출해 평가한다.
619
+ *
620
+ * arch 가 null (flutter 경로) 일 때는 함수형 필드는 빈 값으로 fallback —
621
+ * Flutter 에는 next-arch 가 없으므로 이런 플러그인이 호출되지 않아야 정상이지만
622
+ * 방어적 처리.
623
+ */
624
+ function resolveArchField(field, arch) {
625
+ if (typeof field === 'function') return field(arch);
626
+ return field;
627
+ }
628
+
629
+ async function writePluginFiles(targetDir, plugins, arch) {
573
630
  for (const plugin of plugins) {
574
- if (plugin.files) {
575
- for (const [filePath, content] of Object.entries(plugin.files)) {
631
+ const files = resolveArchField(plugin.files, arch);
632
+ if (files) {
633
+ for (const [filePath, content] of Object.entries(files)) {
576
634
  const fullPath = path.join(targetDir, filePath);
577
635
  await fs.ensureDir(path.dirname(fullPath));
578
636
  await fs.writeFile(fullPath, content);
@@ -582,12 +640,15 @@ async function writePluginFiles(targetDir, plugins) {
582
640
 
583
641
  // auth-jwt + next-intl 동시 활성화 시 proxy.ts 병합
584
642
  // (각 플러그인이 단독으로 깐 proxy.ts 를 합친 버전으로 덮어쓴다)
643
+ // i18n routing import 는 arch.aliases.config 기준 — FSD 면 @/src/shared/config,
644
+ // flat 이면 @/lib/config 로 해석.
585
645
  const names = new Set(plugins.map((p) => p.name));
586
646
  if (names.has('auth-jwt') && names.has('next-intl')) {
647
+ const configAlias = arch ? arch.aliases.config : '@/src/shared/config';
587
648
  const mergedProxy = `import createIntlMiddleware from 'next-intl/middleware';
588
649
  import { NextRequest, NextResponse } from 'next/server';
589
650
 
590
- import { routing } from '@/src/shared/config/i18n/routing';
651
+ import { routing } from '${configAlias}/i18n/routing';
591
652
 
592
653
  const AUTH_ROUTES = ['/sign-in', '/sign-up'];
593
654
 
@@ -635,18 +696,24 @@ export const config = {
635
696
  }
636
697
  }
637
698
 
638
- async function composeProviders(targetDir, plugins) {
699
+ async function composeProviders(targetDir, plugins, arch) {
639
700
  const extraImports = [];
640
701
  const wrappers = [];
641
702
 
642
703
  for (const plugin of plugins) {
643
- if (plugin.providerImports) extraImports.push(...plugin.providerImports);
644
- if (plugin.providerWrappers) wrappers.push(...plugin.providerWrappers);
704
+ const providerImports = resolveArchField(plugin.providerImports, arch);
705
+ const providerWrappers = resolveArchField(plugin.providerWrappers, arch);
706
+ if (providerImports) extraImports.push(...providerImports);
707
+ if (providerWrappers) wrappers.push(...providerWrappers);
645
708
  }
646
709
 
647
710
  if (extraImports.length === 0 && wrappers.length === 0) return;
648
711
 
649
- const globalProviderPath = path.join(targetDir, 'src/app/providers/GlobalProvider/index.tsx');
712
+ // GlobalProvider 위치는 arch.paths.providers 기준 — Layer 2 에서 플러그인이
713
+ // arch.aliases.providers 를 import 에 사용하게 되면 이 경로도 일관됨.
714
+ // arch 미지정 (legacy) 시 FSD 디폴트 fallback.
715
+ const providersDir = arch ? arch.paths.providers : 'src/app/providers';
716
+ const globalProviderPath = path.join(targetDir, providersDir, 'GlobalProvider', 'index.tsx');
650
717
  if (!(await fs.pathExists(globalProviderPath))) return;
651
718
 
652
719
  let content = await fs.readFile(globalProviderPath, 'utf-8');
@@ -674,11 +741,12 @@ async function composeProviders(targetDir, plugins) {
674
741
  await fs.writeFile(globalProviderPath, content);
675
742
  }
676
743
 
677
- async function applyTransforms(targetDir, plugins) {
744
+ async function applyTransforms(targetDir, plugins, arch) {
678
745
  for (const plugin of plugins) {
679
- if (!plugin.transforms) continue;
746
+ const transforms = resolveArchField(plugin.transforms, arch);
747
+ if (!transforms) continue;
680
748
 
681
- for (const transform of plugin.transforms) {
749
+ for (const transform of transforms) {
682
750
  const { type } = transform;
683
751
 
684
752
  if (type === 'move') {
@@ -746,8 +814,9 @@ const OPTIONAL_DART_INJECTORS = [
746
814
  async function injectCssTheme(projectDir, theme) {
747
815
  if (!theme) return;
748
816
  const candidates = [
749
- 'src/shared/styles/tokens.css',
750
- 'src/styles/tokens.css',
817
+ 'src/shared/styles/tokens.css', // FSD standalone
818
+ 'src/styles/tokens.css', // monorepo ui-app-template (arch-neutral)
819
+ 'lib/styles/tokens.css', // flat standalone
751
820
  ];
752
821
  for (const rel of candidates) {
753
822
  const abs = path.join(projectDir, rel);