sh-ui-cli 0.56.4 → 0.58.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.
Files changed (152) hide show
  1. package/bin/sh-ui.mjs +21 -2
  2. package/data/changelog/versions.json +32 -0
  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 +53 -44
  15. package/src/create/plugins/pluginSchema.js +26 -10
  16. package/src/create/plugins/sentry.js +9 -5
  17. package/src/mcp.mjs +50 -0
  18. package/src/rename-app.mjs +321 -0
  19. package/templates/nextjs-app/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
  20. package/templates/nextjs-app/_arch/flat/app/layout.tsx +16 -0
  21. package/templates/nextjs-app/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
  22. package/templates/nextjs-app/_arch/flat/components/layouts/RootLayout.tsx +11 -0
  23. package/templates/nextjs-app/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
  24. package/templates/nextjs-app/_arch/flat/lib/utils/getQueryClient.ts +14 -0
  25. package/templates/nextjs-app/_arch/flat/tsconfig.json +25 -0
  26. package/templates/nextjs-standalone/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
  27. package/templates/nextjs-standalone/_arch/flat/app/layout.tsx +16 -0
  28. package/templates/nextjs-standalone/_arch/flat/components/common/FallbackBoundary/index.tsx +89 -0
  29. package/templates/nextjs-standalone/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
  30. package/templates/nextjs-standalone/_arch/flat/components/layouts/RootLayout.tsx +11 -0
  31. package/templates/nextjs-standalone/_arch/flat/components/providers/GlobalProvider/index.tsx +23 -0
  32. package/templates/nextjs-standalone/_arch/flat/components/providers/index.tsx +1 -0
  33. package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
  34. package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  35. package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProviders.tsx +12 -0
  36. package/templates/nextjs-standalone/_arch/flat/lib/api/apiTypes.ts +21 -0
  37. package/templates/nextjs-standalone/_arch/flat/lib/api/clientFetch.ts +40 -0
  38. package/templates/nextjs-standalone/_arch/flat/lib/api/error.ts +12 -0
  39. package/templates/nextjs-standalone/_arch/flat/lib/api/http.ts +13 -0
  40. package/templates/nextjs-standalone/_arch/flat/lib/api/observability.ts +20 -0
  41. package/templates/nextjs-standalone/_arch/flat/lib/api/queryClient.ts +30 -0
  42. package/templates/nextjs-standalone/_arch/flat/lib/api/serverFetch.ts +59 -0
  43. package/templates/nextjs-standalone/_arch/flat/lib/hooks/useAppMutation.ts +52 -0
  44. package/templates/nextjs-standalone/_arch/flat/lib/test/createTestQueryClient.ts +18 -0
  45. package/templates/nextjs-standalone/_arch/flat/lib/test/index.ts +2 -0
  46. package/templates/nextjs-standalone/_arch/flat/lib/test/renderWithProviders.tsx +40 -0
  47. package/templates/nextjs-standalone/_arch/flat/lib/utils/formatDate.ts +22 -0
  48. package/templates/nextjs-standalone/_arch/flat/lib/utils/formatPrice.ts +10 -0
  49. package/templates/nextjs-standalone/_arch/flat/lib/utils/getQueryClient.ts +14 -0
  50. package/templates/nextjs-standalone/_arch/flat/sh-ui.config.json +19 -0
  51. package/templates/nextjs-standalone/_arch/flat/tsconfig.json +41 -0
  52. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +23 -0
  53. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/index.tsx +1 -0
  54. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  55. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +12 -0
  56. package/templates/nextjs-standalone/_arch/fsd/src/entities/.gitkeep +0 -0
  57. package/templates/nextjs-standalone/_arch/fsd/src/features/.gitkeep +0 -0
  58. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/.gitkeep +0 -0
  59. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/apiTypes.ts +21 -0
  60. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/clientFetch.ts +40 -0
  61. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/error.ts +12 -0
  62. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/http.ts +13 -0
  63. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/observability.ts +20 -0
  64. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/queryClient.ts +30 -0
  65. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/serverFetch.ts +59 -0
  66. package/templates/nextjs-standalone/_arch/fsd/src/shared/config/.gitkeep +0 -0
  67. package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/.gitkeep +0 -0
  68. package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/useAppMutation.ts +52 -0
  69. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatDate.ts +22 -0
  70. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatPrice.ts +10 -0
  71. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/utils.ts +6 -0
  72. package/templates/nextjs-standalone/_arch/fsd/src/shared/model/.gitkeep +0 -0
  73. package/templates/nextjs-standalone/_arch/fsd/src/shared/styles/tokens.css +135 -0
  74. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/createTestQueryClient.ts +18 -0
  75. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/index.ts +2 -0
  76. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/renderWithProviders.tsx +40 -0
  77. package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/.gitkeep +0 -0
  78. package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/FallbackBoundary/index.tsx +89 -0
  79. package/templates/nextjs-standalone/_arch/fsd/src/views/.gitkeep +0 -0
  80. package/templates/nextjs-standalone/_arch/fsd/src/widgets/.gitkeep +0 -0
  81. /package/templates/nextjs-app/{src/entities → _arch/flat/components/common}/.gitkeep +0 -0
  82. /package/templates/nextjs-app/{src/shared/ui → _arch/flat/components/common}/FallbackBoundary/index.tsx +0 -0
  83. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/GlobalProvider/index.tsx +0 -0
  84. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/index.tsx +0 -0
  85. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
  86. /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/theme/ThemeProviders.tsx +0 -0
  87. /package/templates/nextjs-app/{src/features → _arch/flat/lib/api}/.gitkeep +0 -0
  88. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/apiTypes.ts +0 -0
  89. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/clientFetch.ts +0 -0
  90. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/error.ts +0 -0
  91. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/http.ts +0 -0
  92. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/observability.ts +0 -0
  93. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/queryClient.ts +0 -0
  94. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/serverFetch.ts +0 -0
  95. /package/templates/nextjs-app/{src/shared/api → _arch/flat/lib/config}/.gitkeep +0 -0
  96. /package/templates/nextjs-app/{src/shared/config → _arch/flat/lib/hooks}/.gitkeep +0 -0
  97. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/hooks/useAppMutation.ts +0 -0
  98. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/createTestQueryClient.ts +0 -0
  99. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/index.ts +0 -0
  100. /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/renderWithProviders.tsx +0 -0
  101. /package/templates/nextjs-app/{src/shared/hooks → _arch/flat/lib/utils}/.gitkeep +0 -0
  102. /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatDate.ts +0 -0
  103. /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatPrice.ts +0 -0
  104. /package/templates/nextjs-app/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
  105. /package/templates/nextjs-app/{app → _arch/fsd/app}/layout.tsx +0 -0
  106. /package/templates/nextjs-app/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
  107. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/GlobalProvider/index.tsx +0 -0
  108. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/index.tsx +0 -0
  109. /package/templates/nextjs-app/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
  110. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
  111. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/theme/ThemeProviders.tsx +0 -0
  112. /package/templates/nextjs-app/{src/shared/lib → _arch/fsd/src/entities}/.gitkeep +0 -0
  113. /package/templates/nextjs-app/{src/shared/model → _arch/fsd/src/features}/.gitkeep +0 -0
  114. /package/templates/nextjs-app/{src/shared/ui → _arch/fsd/src/shared/api}/.gitkeep +0 -0
  115. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/apiTypes.ts +0 -0
  116. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/clientFetch.ts +0 -0
  117. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/error.ts +0 -0
  118. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/http.ts +0 -0
  119. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/observability.ts +0 -0
  120. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/queryClient.ts +0 -0
  121. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/serverFetch.ts +0 -0
  122. /package/templates/nextjs-app/{src/views → _arch/fsd/src/shared/config}/.gitkeep +0 -0
  123. /package/templates/nextjs-app/{src/widgets → _arch/fsd/src/shared/hooks}/.gitkeep +0 -0
  124. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/hooks/useAppMutation.ts +0 -0
  125. /package/templates/{nextjs-standalone/src/entities → nextjs-app/_arch/fsd/src/shared/lib}/.gitkeep +0 -0
  126. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatDate.ts +0 -0
  127. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatPrice.ts +0 -0
  128. /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
  129. /package/templates/{nextjs-standalone/src/features → nextjs-app/_arch/fsd/src/shared/model}/.gitkeep +0 -0
  130. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/createTestQueryClient.ts +0 -0
  131. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/index.ts +0 -0
  132. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/renderWithProviders.tsx +0 -0
  133. /package/templates/{nextjs-standalone/src/shared/api → nextjs-app/_arch/fsd/src/shared/ui}/.gitkeep +0 -0
  134. /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/ui/FallbackBoundary/index.tsx +0 -0
  135. /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
  136. /package/templates/{nextjs-standalone/src/shared/config → nextjs-app/_arch/fsd/src/views}/.gitkeep +0 -0
  137. /package/templates/{nextjs-standalone/src/shared/hooks → nextjs-app/_arch/fsd/src/widgets}/.gitkeep +0 -0
  138. /package/templates/nextjs-app/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
  139. /package/templates/nextjs-standalone/{src/shared/model → _arch/flat/components/common}/.gitkeep +0 -0
  140. /package/templates/nextjs-standalone/{src/shared/ui → _arch/flat/lib/api}/.gitkeep +0 -0
  141. /package/templates/nextjs-standalone/{src/views → _arch/flat/lib/config}/.gitkeep +0 -0
  142. /package/templates/nextjs-standalone/{src/widgets → _arch/flat/lib/hooks}/.gitkeep +0 -0
  143. /package/templates/nextjs-standalone/{src/shared → _arch/flat/lib}/styles/tokens.css +0 -0
  144. /package/templates/nextjs-standalone/{src/shared/lib → _arch/flat/lib/utils}/utils.ts +0 -0
  145. /package/templates/nextjs-standalone/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
  146. /package/templates/nextjs-standalone/{app → _arch/fsd/app}/layout.tsx +0 -0
  147. /package/templates/nextjs-standalone/{sh-ui.config.json → _arch/fsd/sh-ui.config.json} +0 -0
  148. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
  149. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
  150. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
  151. /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
  152. /package/templates/nextjs-standalone/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
@@ -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);
@@ -3,11 +3,15 @@
3
3
  import { parseArgs } from './cli-args.js';
4
4
  import { createProject, addApp, addComponent } from './generator.js';
5
5
  import { allPlugins } from './plugins/index.js';
6
+ import { allArchitectures, getArchesForPlatform } from './architectures/index.js';
6
7
  import { CREATE_PLATFORMS, CREATE_STRUCTURES, CSS_FRAMEWORKS_SUPPORTED } from '../constants.js';
7
8
  import { THEME_PRESET_NAMES } from './theme/presets.js';
8
9
 
9
10
  const PLUGIN_NAMES = allPlugins.map((p) => p.name);
10
11
  const PLUGINS_LIST = PLUGIN_NAMES.join(', ');
12
+ const ARCH_NAMES = allArchitectures.map((a) => a.name);
13
+ const ARCHES_LIST = ARCH_NAMES.join('|');
14
+ const NEXT_ARCHES = getArchesForPlatform('next').map((a) => a.name).join(', ');
11
15
  const THEME_PRESETS_LIST = THEME_PRESET_NAMES.join('|');
12
16
 
13
17
  export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next.js / Flutter)
@@ -20,6 +24,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
20
24
  옵션:
21
25
  --platform <${CREATE_PLATFORMS.join('|')}> 타겟 플랫폼
22
26
  --structure <${CREATE_STRUCTURES.join('|')}> Next.js 프로젝트 구조 (next 일 때)
27
+ --arch <${ARCHES_LIST}> 프로젝트 아키텍처 — 폴더 구조/import alias 컨벤션. next 에서 사용 가능: ${NEXT_ARCHES}. 기본 fsd
23
28
  --plugins <a,b> 플러그인 (${PLUGINS_LIST}). 미지정/"" → 없음
24
29
  --theme <preset|base64> 프리셋 이름(${THEME_PRESETS_LIST}) 또는 playground base64. 선택
25
30
  --css <${CSS_FRAMEWORKS_SUPPORTED.join('|')}> CSS 프레임워크 (현재 plain만 지원, 향후 tailwind 등 추가 예정)
@@ -33,6 +38,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
33
38
  예 (비대화형 / 에이전트 / CI):
34
39
  sh-ui create my-app --platform next --structure standalone --yes
35
40
  sh-ui create my-app --platform next --structure monorepo --plugins ${PLUGIN_NAMES.slice(0, 3).join(',')} --yes
41
+ sh-ui create my-app --platform next --structure standalone --arch flat --yes
36
42
  sh-ui create my-app --platform next --structure standalone --theme rose --yes
37
43
  sh-ui create my-app --platform flutter --yes
38
44
 
@@ -70,6 +76,7 @@ export async function runCreate(rest) {
70
76
  platform: flags.platform,
71
77
  structure: flags.structure,
72
78
  plugins: flags.plugins,
79
+ arch: flags.arch,
73
80
  theme: flags.theme,
74
81
  css: flags.css,
75
82
  yes: flags.yes,
@@ -1,3 +1,9 @@
1
+ /**
2
+ * auth-jwt 플러그인 — Layer 2 부터 arch-aware.
3
+ *
4
+ * fs 경로 / import alias 가 arch.paths.api / arch.aliases.api 에서 파생.
5
+ * FSD 기준 v0.57 까지의 하드코딩과 1:1 일치 (회귀 가드는 smoke).
6
+ */
1
7
  export const authJwtPlugin = {
2
8
  name: 'auth-jwt',
3
9
  label: '쿠키 기반 JWT 인증 (refresh 자리표시자 포함)',
@@ -26,7 +32,7 @@ export const authJwtPlugin = {
26
32
  // refreshSession.ts 는 v1 placeholder — 백엔드 명세 확정 후 본문만 채우면
27
33
  // BFF 와 withAuthRetry 가 자동 활용한다.
28
34
 
29
- files: {
35
+ files: (arch) => ({
30
36
  'proxy.ts': `import { NextRequest, NextResponse } from 'next/server';
31
37
 
32
38
  const AUTH_ROUTES = ['/sign-in', '/sign-up'];
@@ -57,7 +63,7 @@ export const config = {
57
63
  };
58
64
  `,
59
65
 
60
- 'src/shared/api/refreshSession.ts': `type RefreshResult =
66
+ [`${arch.paths.api}/refreshSession.ts`]: `type RefreshResult =
61
67
  | { ok: true; accessToken: string; refreshToken: string }
62
68
  | { ok: false };
63
69
 
@@ -109,7 +115,7 @@ export async function refreshSession(
109
115
  }
110
116
  `,
111
117
 
112
- 'src/shared/api/withAuthRetry.ts': `import { cookies } from 'next/headers';
118
+ [`${arch.paths.api}/withAuthRetry.ts`]: `import { cookies } from 'next/headers';
113
119
 
114
120
  import { ApiError } from './error';
115
121
  import { refreshSession } from './refreshSession';
@@ -135,8 +141,8 @@ const COOKIE = {
135
141
  * 사용 예 (Server Action):
136
142
  *
137
143
  * 'use server';
138
- * import { serverFetch } from '@/src/shared/api/serverFetch';
139
- * import { withAuthRetry } from '@/src/shared/api/withAuthRetry';
144
+ * import { serverFetch } from '${arch.aliases.api}/serverFetch';
145
+ * import { withAuthRetry } from '${arch.aliases.api}/withAuthRetry';
140
146
  *
141
147
  * export async function toggleFavoriteAction(id: number) {
142
148
  * return withAuthRetry(() =>
@@ -171,8 +177,8 @@ import { NextResponse, type NextRequest } from 'next/server';
171
177
  import {
172
178
  captureApiError,
173
179
  logApiError,
174
- } from '@/src/shared/api/observability';
175
- import { refreshSession } from '@/src/shared/api/refreshSession';
180
+ } from '${arch.aliases.api}/observability';
181
+ import { refreshSession } from '${arch.aliases.api}/refreshSession';
176
182
 
177
183
  const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
178
184
  const ACCESS_TOKEN_COOKIE = 'accessToken';
@@ -338,5 +344,5 @@ export const DELETE = (
338
344
  ctx: { params: Promise<{ path: string[] }> },
339
345
  ) => proxyRequest(req, ctx, 'DELETE');
340
346
  `,
341
- },
347
+ }),
342
348
  };
@@ -1,3 +1,13 @@
1
+ /**
2
+ * next-intl 플러그인 — Layer 2 부터 arch-aware.
3
+ *
4
+ * 모든 fs 경로 / import alias 가 arch 디스크립터의 논리 키에서 파생된다:
5
+ * - i18n 설정 (request/routing/navigation/messages) → arch.paths.config + '/i18n'
6
+ * - 내부 import (RootLayout, GlobalProvider) → arch.aliases.layouts / providers
7
+ *
8
+ * FSD 에서는 v0.57 까지의 하드코딩과 1:1 일치 (회귀 가드는 smoke 시나리오 3).
9
+ * flat 에서는 자동으로 lib/config/i18n + components/layouts/RootLayout 로 emit.
10
+ */
1
11
  export const nextIntlPlugin = {
2
12
  name: 'next-intl',
3
13
  label: 'next-intl (다국어 지원)',
@@ -15,8 +25,8 @@ export const nextIntlPlugin = {
15
25
  `import createNextIntlPlugin from 'next-intl/plugin';`,
16
26
  ],
17
27
 
18
- preExport: [
19
- `const withNextIntl = createNextIntlPlugin('./src/shared/config/i18n/request.ts');`,
28
+ preExport: (arch) => [
29
+ `const withNextIntl = createNextIntlPlugin('./${arch.paths.config}/i18n/request.ts');`,
20
30
  ],
21
31
 
22
32
  wrapExport(expr) {
@@ -36,29 +46,47 @@ export const nextIntlPlugin = {
36
46
 
37
47
  // ─── 라우트 구조 변환 ───
38
48
 
39
- transforms: [
49
+ transforms: (arch) => [
40
50
  { type: 'move', from: 'app/page.tsx', to: 'app/[locale]/page.tsx' },
41
51
  { type: 'move', from: 'app/error.tsx', to: 'app/[locale]/error.tsx' },
42
52
  {
43
- // 기본 nextjs-app 템플릿의 app/layout.tsx globals.cssside-effect import 한다 —
44
- // next-intl 도입 layout 본체는 [locale]/layout.tsx 옮기지만, CSS import 는 root
45
- // layout 살아 있어야 사용자 프로젝트의 Tailwind 스타일이 동작한다. content 통째 교체
46
- // 대신 contentFn 으로 side-effect import (`import 'x';` 형태, binding 없음) 추출해
47
- // 새 본체 앞에 prepend. 이름 있는 import (예: `import { RootLayout } from ...`) 는 새 본체와
48
- // 식별자 충돌 가능성이 있어 제외.
53
+ // Next 16 부터 root layout (app/layout.tsx) 반드시 <html>/<body> 가져야 한다.
54
+ // next-intl 적용 시에는 [locale] 가 root 역할을 맡으므로, 기본 app/layout.tsx 그대로
55
+ // [locale]/layout.tsx 이동시켜 globals.css side-effect import 보존한 뒤,
56
+ // body locale-aware 버전으로 교체한다. 결과적으로 app/layout.tsx 존재하지 않게 되고
57
+ // [locale]/layout.tsx Next root layout 으로 인식된다.
58
+ type: 'move',
59
+ from: 'app/layout.tsx',
60
+ to: 'app/[locale]/layout.tsx',
61
+ },
62
+ {
63
+ // 위에서 옮겨진 [locale]/layout.tsx 는 비-locale 버전 — body 를 locale-aware 로 갈아끼운다.
64
+ // side-effect import (`import 'x';` 형태, binding 없음) 만 보존하고 나머지는 통째 교체.
65
+ // 이름 있는 import (예: `import { RootLayout } from ...`) 는 새 본체와 식별자 충돌 가능성이
66
+ // 있어 제외.
49
67
  type: 'replace',
50
- path: 'app/layout.tsx',
68
+ path: 'app/[locale]/layout.tsx',
51
69
  contentFn: (existing) => {
52
70
  const sideEffectImports = existing
53
71
  .split('\n')
54
72
  .filter((line) => /^\s*import\s+['"][^'"]+['"];?\s*$/.test(line))
55
73
  .join('\n');
56
- const body = `export default async function RootLayout({
74
+ const body = `import type { Metadata } from 'next';
75
+ import { RootLayout } from '${arch.aliases.layouts}/RootLayout';
76
+
77
+ export const metadata: Metadata = {
78
+ title: 'My App',
79
+ description: 'My App Description',
80
+ };
81
+
82
+ export default function Layout({
57
83
  children,
58
- }: {
84
+ params,
85
+ }: Readonly<{
59
86
  children: React.ReactNode;
60
- }) {
61
- return children;
87
+ params: Promise<{ locale: string }>;
88
+ }>) {
89
+ return <RootLayout params={params}>{children}</RootLayout>;
62
90
  }
63
91
  `;
64
92
  return sideEffectImports ? `${sideEffectImports}\n\n${body}` : body;
@@ -66,11 +94,11 @@ export const nextIntlPlugin = {
66
94
  },
67
95
  {
68
96
  type: 'replace',
69
- path: 'src/app/layouts/RootLayout.tsx',
97
+ path: `${arch.paths.layouts}/RootLayout.tsx`,
70
98
  content: `import { hasLocale } from 'next-intl';
71
99
  import { notFound } from 'next/navigation';
72
- import { GlobalProvider } from '@/src/app/providers';
73
- import { routing } from '@/src/shared/config/i18n/routing';
100
+ import { GlobalProvider } from '${arch.aliases.providers}';
101
+ import { routing } from '${arch.aliases.config}/i18n/routing';
74
102
 
75
103
  export async function RootLayout({
76
104
  children,
@@ -99,8 +127,8 @@ export async function RootLayout({
99
127
 
100
128
  // ─── 독립 파일 ───
101
129
 
102
- files: {
103
- 'src/shared/config/i18n/routing.ts': `import { defineRouting } from 'next-intl/routing';
130
+ files: (arch) => ({
131
+ [`${arch.paths.config}/i18n/routing.ts`]: `import { defineRouting } from 'next-intl/routing';
104
132
 
105
133
  export const routing = defineRouting({
106
134
  locales: ['ko', 'en'],
@@ -108,7 +136,7 @@ export const routing = defineRouting({
108
136
  });
109
137
  `,
110
138
 
111
- 'src/shared/config/i18n/request.ts': `import { getRequestConfig } from 'next-intl/server';
139
+ [`${arch.paths.config}/i18n/request.ts`]: `import { getRequestConfig } from 'next-intl/server';
112
140
  import { hasLocale } from 'next-intl';
113
141
  import { routing } from './routing';
114
142
 
@@ -125,14 +153,14 @@ export default getRequestConfig(async ({ requestLocale }) => {
125
153
  });
126
154
  `,
127
155
 
128
- 'src/shared/config/i18n/navigation.ts': `import { createNavigation } from 'next-intl/navigation';
156
+ [`${arch.paths.config}/i18n/navigation.ts`]: `import { createNavigation } from 'next-intl/navigation';
129
157
  import { routing } from './routing';
130
158
 
131
159
  export const { Link, redirect, usePathname, useRouter, getPathname } =
132
160
  createNavigation(routing);
133
161
  `,
134
162
 
135
- 'src/shared/config/i18n/messages/ko.json': `{
163
+ [`${arch.paths.config}/i18n/messages/ko.json`]: `{
136
164
  "common": {
137
165
  "loading": "로딩 중...",
138
166
  "error": "오류가 발생했습니다",
@@ -163,7 +191,7 @@ export const { Link, redirect, usePathname, useRouter, getPathname } =
163
191
  }
164
192
  `,
165
193
 
166
- 'src/shared/config/i18n/messages/en.json': `{
194
+ [`${arch.paths.config}/i18n/messages/en.json`]: `{
167
195
  "common": {
168
196
  "loading": "Loading...",
169
197
  "error": "An error occurred",
@@ -192,29 +220,10 @@ export const { Link, redirect, usePathname, useRouter, getPathname } =
192
220
  }
193
221
  }
194
222
  }
195
- `,
196
-
197
- 'app/[locale]/layout.tsx': `import type { Metadata } from 'next';
198
- import { RootLayout } from '@/src/app/layouts/RootLayout';
199
-
200
- export const metadata: Metadata = {
201
- title: 'My App',
202
- description: 'My App Description',
203
- };
204
-
205
- export default function Layout({
206
- children,
207
- params,
208
- }: Readonly<{
209
- children: React.ReactNode;
210
- params: Promise<{ locale: string }>;
211
- }>) {
212
- return <RootLayout params={params}>{children}</RootLayout>;
213
- }
214
223
  `,
215
224
 
216
225
  'proxy.ts': `import createIntlMiddleware from 'next-intl/middleware';
217
- import { routing } from '@/src/shared/config/i18n/routing';
226
+ import { routing } from '${arch.aliases.config}/i18n/routing';
218
227
 
219
228
  const intl = createIntlMiddleware(routing);
220
229
 
@@ -224,5 +233,5 @@ export const config = {
224
233
  matcher: '/((?!api|trpc|_next|_vercel|monitoring|.*\\\\..*).*)',
225
234
  };
226
235
  `,
227
- },
236
+ }),
228
237
  };
@@ -31,6 +31,13 @@ const wrapperFn = z
31
31
  })
32
32
  .optional();
33
33
 
34
+ // arch-aware 필드. 정적 값 OR `(arch) => 정적 값` 함수 모두 허용.
35
+ // 플러그인이 arch 디스크립터의 paths/aliases 를 조회해 자기 산출물의 fs 경로/import
36
+ // alias 를 결정할 때 사용. Layer 2 이전엔 모든 플러그인이 정적, Layer 2 이후엔 함수형.
37
+ const archAwareFn = z.custom((val) => typeof val === "function");
38
+ const archAwareRecord = z.union([z.record(filePath, z.string()), archAwareFn]);
39
+ const archAwareArray = z.union([z.array(z.any()), archAwareFn]);
40
+
34
41
  export const PluginSchema = z.object({
35
42
  name: z.string().regex(/^[a-z][a-z0-9-]*$/, {
36
43
  message: 'Plugin name must be lowercase kebab-case (e.g., "auth-jwt")',
@@ -43,26 +50,35 @@ export const PluginSchema = z.object({
43
50
  devDependencies: z.record(z.string(), z.string()).optional(),
44
51
 
45
52
  imports: z.array(z.string()).optional(),
53
+ // preExport 은 arch-aware (next.config.ts 의 export 직전 emit 되는 라인들 —
54
+ // 예: next-intl 의 `createNextIntlPlugin('./src/shared/config/i18n/request.ts')` 같이
55
+ // arch.paths.config 가 박혀야 하는 경로 포함).
56
+ preExport: z.union([z.array(z.string()), archAwareFn]).optional(),
46
57
  wrapExport: wrapperFn,
47
58
 
48
59
  envVars: z.array(z.string()).optional(),
49
60
  turboEnvVars: z.array(z.string()).optional(),
50
61
 
51
- providerImports: z.array(z.string()).optional(),
62
+ // providerImports/providerWrappers/files/transforms 는 arch-aware —
63
+ // 정적 값 또는 (arch) => 정적 값 함수 형태 둘 다 허용.
64
+ providerImports: z.union([z.array(z.string()), archAwareFn]).optional(),
52
65
  providerWrappers: z
53
- .array(
54
- z.union([
55
- z.object({ open: z.string(), close: z.string() }),
56
- z.string(),
57
- ]),
58
- )
66
+ .union([
67
+ z.array(
68
+ z.union([
69
+ z.object({ open: z.string(), close: z.string() }),
70
+ z.string(),
71
+ ]),
72
+ ),
73
+ archAwareFn,
74
+ ])
59
75
  .optional(),
60
76
 
61
- files: z.record(filePath, z.string()).optional(),
77
+ files: archAwareRecord.optional(),
62
78
 
63
79
  // 향후 확장 — moves, transforms, etc 는 nextIntl.js 에서 사용하므로 허용
64
- moves: z.array(z.any()).optional(),
65
- transforms: z.array(z.any()).optional(),
80
+ moves: archAwareArray.optional(),
81
+ transforms: archAwareArray.optional(),
66
82
  });
67
83
 
68
84
  /**
@@ -57,8 +57,12 @@ export const sentryPlugin = {
57
57
  // HTTP/proxy 인프라(http.ts, apiTypes.ts, error.ts, app/api/proxy 등)는
58
58
  // 베이스 템플릿이 소유한다. Sentry 는 베이스의 observability.ts 를
59
59
  // Sentry-aware 버전으로 덮어써서 캡처/로그를 활성화한다.
60
+ //
61
+ // arch 의존: FallbackBoundary 는 arch.paths.ui 에, observability 는 arch.paths.api 에
62
+ // 떨어진다. FallbackBoundary 안의 ApiError import 는 arch.aliases.api 로 fully-qualified —
63
+ // 상대 경로 (`../../api/error`) 는 FSD 에서만 동작하므로 alias 로 통일.
60
64
 
61
- files: {
65
+ files: (arch) => ({
62
66
  'sentry.server.config.ts': `import * as Sentry from '@sentry/nextjs';
63
67
 
64
68
  Sentry.init({
@@ -280,7 +284,7 @@ export default function Error({
280
284
  }
281
285
  `,
282
286
 
283
- 'src/shared/ui/FallbackBoundary/index.tsx': `import React, {
287
+ [`${arch.paths.ui}/FallbackBoundary/index.tsx`]: `import React, {
284
288
  Component,
285
289
  ComponentType,
286
290
  ErrorInfo,
@@ -290,7 +294,7 @@ export default function Error({
290
294
  import * as Sentry from '@sentry/nextjs';
291
295
  import { QueryErrorResetBoundary } from '@tanstack/react-query';
292
296
 
293
- import { ApiError } from '../../api/error';
297
+ import { ApiError } from '${arch.aliases.api}/error';
294
298
 
295
299
  interface ErrorFallbackProps {
296
300
  error: Error | null;
@@ -386,7 +390,7 @@ export function FallbackBoundary({
386
390
  // http.ts / serverFetch.ts / proxy/route.ts 가 이 모듈을 import 하므로,
387
391
  // Sentry 플러그인이 켜지면 자동으로 캡처가 활성화된다.
388
392
 
389
- 'src/shared/api/observability.ts': `import * as Sentry from '@sentry/nextjs';
393
+ [`${arch.paths.api}/observability.ts`]: `import * as Sentry from '@sentry/nextjs';
390
394
 
391
395
  type ApiCaptureParams = {
392
396
  url: string;
@@ -439,5 +443,5 @@ export const logApiError = (prefix: string, params: ApiLogParams): void => {
439
443
  if (responseBody) console.error('- Response Body:', responseBody);
440
444
  };
441
445
  `,
442
- },
446
+ }),
443
447
  };