create-web-0to1 0.1.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 (193) hide show
  1. package/README.md +68 -0
  2. package/internal/engine/create-feature-crud-script.ts +134 -0
  3. package/internal/engine/create-feature-crud.template.mjs +601 -0
  4. package/internal/engine/create-feature-script.ts +142 -0
  5. package/internal/engine/generator-engine.ts +546 -0
  6. package/internal/engine/standalone-feature-preset.ts +34 -0
  7. package/internal/meta/preset-plan.ts +41 -0
  8. package/internal/meta/runtime-copy-plan.ts +220 -0
  9. package/internal/meta/runtime-layout.ts +262 -0
  10. package/internal/meta/scaffold-manifest.ts +169 -0
  11. package/internal/meta/standalone-dependency-manifest.ts +75 -0
  12. package/package.json +45 -0
  13. package/scripts/create-app.mjs +1612 -0
  14. package/source/core/auth/auth-events.ts +13 -0
  15. package/source/core/error/app-error.ts +85 -0
  16. package/source/core/error/handle-app-error.client.ts +35 -0
  17. package/source/core/lib/dayjs.ts +25 -0
  18. package/source/core/query/query-client.ts +126 -0
  19. package/source/core/request/request-core.ts +210 -0
  20. package/source/core/routes/route-paths.ts +4 -0
  21. package/source/core/ui/button.tsx +24 -0
  22. package/source/core/ui/modal-store.ts +32 -0
  23. package/source/core/ui/text-input-field.tsx +36 -0
  24. package/source/core/utils/build-query-string.ts +30 -0
  25. package/source/core/utils/format/date.ts +41 -0
  26. package/source/core/utils/format/index.ts +3 -0
  27. package/source/core/utils/format/number.ts +13 -0
  28. package/source/core/utils/format/text.ts +15 -0
  29. package/source/core/utils/schema-utils.ts +27 -0
  30. package/source/wrappers/monorepo/core/internal.ts +21 -0
  31. package/source/wrappers/monorepo/core/src/index.ts +4 -0
  32. package/source/wrappers/monorepo/core-next/src/auth.client.ts +1 -0
  33. package/source/wrappers/monorepo/core-next/src/auth.server.ts +94 -0
  34. package/source/wrappers/monorepo/core-next/src/bootstrap.client.tsx +21 -0
  35. package/source/wrappers/monorepo/core-next/src/bootstrap.tsx +18 -0
  36. package/source/wrappers/monorepo/core-next/src/index.ts +1 -0
  37. package/source/wrappers/monorepo/core-react/src/app-providers.tsx +36 -0
  38. package/source/wrappers/monorepo/core-react/src/auth.ts +42 -0
  39. package/source/wrappers/monorepo/core-react/src/hydration.tsx +21 -0
  40. package/source/wrappers/monorepo/core-react/src/index.ts +7 -0
  41. package/source/wrappers/monorepo/core-react/src/provider.tsx +49 -0
  42. package/source/wrappers/monorepo/core-react/src/query-client.ts +48 -0
  43. package/source/wrappers/monorepo/core-react/src/query-error-handler.ts +62 -0
  44. package/source/wrappers/monorepo/core-react/src/query-keys.ts +22 -0
  45. package/source/wrappers/monorepo/request/core-fetch.ts +27 -0
  46. package/source/wrappers/monorepo/request/core-request.ts +93 -0
  47. package/source/wrappers/next/auth/auth-error-listener.tsx +34 -0
  48. package/source/wrappers/next/error/handle-app-error.server.ts +41 -0
  49. package/source/wrappers/next/query/hydration.tsx +20 -0
  50. package/source/wrappers/next/query/providers.tsx +35 -0
  51. package/source/wrappers/next/request/request.client.ts +24 -0
  52. package/source/wrappers/next/request/request.server.ts +64 -0
  53. package/source/wrappers/next/request/request.ts +52 -0
  54. package/source/wrappers/next/ui/global-modal.tsx +29 -0
  55. package/source/wrappers/react/auth/auth-error-listener.tsx +34 -0
  56. package/source/wrappers/react/query/providers.tsx +31 -0
  57. package/source/wrappers/react/request/request.client.ts +24 -0
  58. package/source/wrappers/react/request/request.ts +51 -0
  59. package/source/wrappers/react/ui/global-modal.tsx +27 -0
  60. package/templates/monorepo/.dockerignore +38 -0
  61. package/templates/monorepo/README.md +292 -0
  62. package/templates/monorepo/_gitignore +38 -0
  63. package/templates/monorepo/_npmrc +1 -0
  64. package/templates/monorepo/apps/project/Dockerfile +32 -0
  65. package/templates/monorepo/apps/project/eslint.config.mjs +4 -0
  66. package/templates/monorepo/apps/project/index.html +14 -0
  67. package/templates/monorepo/apps/project/index.ts +15 -0
  68. package/templates/monorepo/apps/project/package.json +21 -0
  69. package/templates/monorepo/apps/project/tsconfig.json +9 -0
  70. package/templates/monorepo/apps/project/vite.config.ts +6 -0
  71. package/templates/monorepo/apps/web/Dockerfile +43 -0
  72. package/templates/monorepo/apps/web/README.md +111 -0
  73. package/templates/monorepo/apps/web/_gitignore +36 -0
  74. package/templates/monorepo/apps/web/app/favicon.ico +0 -0
  75. package/templates/monorepo/apps/web/app/global-error.tsx +12 -0
  76. package/templates/monorepo/apps/web/app/globals.css +0 -0
  77. package/templates/monorepo/apps/web/app/layout.tsx +28 -0
  78. package/templates/monorepo/apps/web/app/page.tsx +7 -0
  79. package/templates/monorepo/apps/web/app/providers.tsx +25 -0
  80. package/templates/monorepo/apps/web/eslint.config.js +4 -0
  81. package/templates/monorepo/apps/web/next-env.d.ts +6 -0
  82. package/templates/monorepo/apps/web/next.config.js +4 -0
  83. package/templates/monorepo/apps/web/package.json +31 -0
  84. package/templates/monorepo/apps/web/public/file-text.svg +3 -0
  85. package/templates/monorepo/apps/web/public/globe.svg +10 -0
  86. package/templates/monorepo/apps/web/public/next.svg +1 -0
  87. package/templates/monorepo/apps/web/public/turborepo-dark.svg +19 -0
  88. package/templates/monorepo/apps/web/public/turborepo-light.svg +19 -0
  89. package/templates/monorepo/apps/web/public/vercel.svg +10 -0
  90. package/templates/monorepo/apps/web/public/window.svg +3 -0
  91. package/templates/monorepo/apps/web/tsconfig.json +20 -0
  92. package/templates/monorepo/package.json +24 -0
  93. package/templates/monorepo/packages/core/eslint.config.mjs +4 -0
  94. package/templates/monorepo/packages/core/package.json +32 -0
  95. package/templates/monorepo/packages/core/tsconfig.json +8 -0
  96. package/templates/monorepo/packages/core-next/eslint.config.mjs +13 -0
  97. package/templates/monorepo/packages/core-next/package.json +43 -0
  98. package/templates/monorepo/packages/core-next/tsconfig.json +8 -0
  99. package/templates/monorepo/packages/core-react/eslint.config.mjs +4 -0
  100. package/templates/monorepo/packages/core-react/package.json +34 -0
  101. package/templates/monorepo/packages/core-react/tsconfig.json +8 -0
  102. package/templates/monorepo/packages/eslint-config/README.md +3 -0
  103. package/templates/monorepo/packages/eslint-config/base.js +57 -0
  104. package/templates/monorepo/packages/eslint-config/next.js +22 -0
  105. package/templates/monorepo/packages/eslint-config/package.json +25 -0
  106. package/templates/monorepo/packages/eslint-config/react-internal.js +33 -0
  107. package/templates/monorepo/packages/typescript-config/base.json +19 -0
  108. package/templates/monorepo/packages/typescript-config/nextjs.json +12 -0
  109. package/templates/monorepo/packages/typescript-config/package.json +9 -0
  110. package/templates/monorepo/packages/typescript-config/react-library.json +7 -0
  111. package/templates/monorepo/packages/ui/eslint.config.mjs +4 -0
  112. package/templates/monorepo/packages/ui/package.json +26 -0
  113. package/templates/monorepo/packages/ui/src/button.tsx +20 -0
  114. package/templates/monorepo/packages/ui/src/card.tsx +27 -0
  115. package/templates/monorepo/packages/ui/src/code.tsx +11 -0
  116. package/templates/monorepo/packages/ui/tsconfig.json +8 -0
  117. package/templates/monorepo/pnpm-workspace.yaml +9 -0
  118. package/templates/monorepo/turbo/generators/config.js +1336 -0
  119. package/templates/monorepo/turbo/generators/templates/next-app/Dockerfile.tpl +30 -0
  120. package/templates/monorepo/turbo/generators/templates/next-app/README.md.tpl +118 -0
  121. package/templates/monorepo/turbo/generators/templates/next-app/app/global-error.tsx.tpl +12 -0
  122. package/templates/monorepo/turbo/generators/templates/next-app/app/globals.css.tpl +1 -0
  123. package/templates/monorepo/turbo/generators/templates/next-app/app/layout.tsx.tpl +29 -0
  124. package/templates/monorepo/turbo/generators/templates/next-app/app/page.tsx.tpl +7 -0
  125. package/templates/monorepo/turbo/generators/templates/next-app/app/providers.tsx.tpl +25 -0
  126. package/templates/monorepo/turbo/generators/templates/next-app/eslint.config.js.tpl +4 -0
  127. package/templates/monorepo/turbo/generators/templates/next-app/next.config.js.tpl +6 -0
  128. package/templates/monorepo/turbo/generators/templates/next-app/tsconfig.json.tpl +18 -0
  129. package/templates/monorepo/turbo/generators/templates/vite-app/Dockerfile.tpl +22 -0
  130. package/templates/monorepo/turbo/generators/templates/vite-app/README.plain.md.tpl +90 -0
  131. package/templates/monorepo/turbo/generators/templates/vite-app/README.react.md.tpl +107 -0
  132. package/templates/monorepo/turbo/generators/templates/vite-app/eslint.config.mjs.tpl +4 -0
  133. package/templates/monorepo/turbo/generators/templates/vite-app/index.html.tpl +12 -0
  134. package/templates/monorepo/turbo/generators/templates/vite-app/index.ts.tpl +22 -0
  135. package/templates/monorepo/turbo/generators/templates/vite-app/tsconfig.json.tpl +9 -0
  136. package/templates/monorepo/turbo/generators/templates/vite-app/vite.config.ts.tpl +6 -0
  137. package/templates/monorepo/turbo.json +28 -0
  138. package/templates/next/.env.example +2 -0
  139. package/templates/next/.prettierignore +9 -0
  140. package/templates/next/.prettierrc.json +9 -0
  141. package/templates/next/README.md +246 -0
  142. package/templates/next/_gitignore +44 -0
  143. package/templates/next/eslint.config.mjs +51 -0
  144. package/templates/next/next.config.ts +7 -0
  145. package/templates/next/package.json +24 -0
  146. package/templates/next/postcss.config.mjs +7 -0
  147. package/templates/next/scripts/create-feature-crud.mjs +5 -0
  148. package/templates/next/scripts/create-feature.mjs +5 -0
  149. package/templates/next/src/app/error.tsx +33 -0
  150. package/templates/next/src/app/globals.css +35 -0
  151. package/templates/next/src/app/layout.tsx +39 -0
  152. package/templates/next/src/app/login/page.tsx +17 -0
  153. package/templates/next/src/app/page.tsx +32 -0
  154. package/templates/next/src/app/providers.tsx +20 -0
  155. package/templates/next/tsconfig.json +34 -0
  156. package/templates/react/.env.example +1 -0
  157. package/templates/react/.prettierignore +10 -0
  158. package/templates/react/.prettierrc.json +9 -0
  159. package/templates/react/README.md +250 -0
  160. package/templates/react/_gitignore +31 -0
  161. package/templates/react/eslint.config.mjs +64 -0
  162. package/templates/react/package.json +19 -0
  163. package/templates/react/scripts/create-feature-crud.mjs +5 -0
  164. package/templates/react/scripts/create-feature.mjs +5 -0
  165. package/templates/react/src/app/app.tsx +15 -0
  166. package/templates/react/src/app/error-boundary.tsx +59 -0
  167. package/templates/react/src/app/frame.tsx +32 -0
  168. package/templates/react/src/app/globals.css +43 -0
  169. package/templates/react/src/app/not-found-page.tsx +23 -0
  170. package/templates/react/src/app/providers.tsx +16 -0
  171. package/templates/react/src/app/router.tsx +62 -0
  172. package/templates/react/src/main.tsx +12 -0
  173. package/templates/react/src/pages/index/page.tsx +36 -0
  174. package/templates/react/src/pages/login/page.tsx +18 -0
  175. package/templates/react/tsconfig.app.json +30 -0
  176. package/templates/react/tsconfig.json +4 -0
  177. package/templates/react/tsconfig.node.json +24 -0
  178. package/templates/react/vite.config.ts +14 -0
  179. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/00-/354/240/204/353/260/230/354/240/201/354/235/270-/355/217/264/353/215/224/352/265/254/354/241/260.md +150 -0
  180. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/01-/352/265/254/354/241/260/354/231/200-/353/235/274/354/232/260/355/214/205.md +186 -0
  181. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/02-/354/204/234/353/262/204/354/231/200-/355/201/264/353/235/274/354/235/264/354/226/270/355/212/270.md +86 -0
  182. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/03-/354/203/201/355/203/234/352/264/200/353/246/254.md +84 -0
  183. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/04-API/354/231/200-/353/215/260/354/235/264/355/204/260.md +199 -0
  184. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/05-/354/227/220/353/237/254/354/231/200-UI-/354/203/201/355/203/234.md +159 -0
  185. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/06-/355/217/274.md +116 -0
  186. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/07-/354/212/244/355/203/200/354/235/274/353/247/201/352/263/274-/354/240/221/352/267/274/354/204/261.md +73 -0
  187. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/08-/353/204/244/354/235/264/353/260/215-/354/204/244/354/240/225-/355/217/254/353/247/267/355/214/205.md +98 -0
  188. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/09-/353/235/274/354/232/260/355/212/270-/354/240/225/354/235/230.md +169 -0
  189. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/10-/354/273/244/353/260/213-/354/273/250/353/262/244/354/205/230.md +64 -0
  190. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/11-/352/270/260/355/203/200-/354/233/220/354/271/231.md +187 -0
  191. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/12-/354/213/244/353/254/264-/353/215/260/354/235/264/355/204/260-/355/214/250/355/204/264.md +302 -0
  192. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/13-/354/204/261/353/212/245-/354/233/220/354/271/231.md +175 -0
  193. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/README.md +39 -0
@@ -0,0 +1,1612 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { promises as fs } from 'node:fs';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline/promises';
7
+ import { stdin as input, stdout as output } from 'node:process';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ import {
11
+ buildProjectGenerationPlan,
12
+ executeProjectGenerationPlan,
13
+ } from '../internal/engine/generator-engine.ts';
14
+ import { getStandaloneDependencyManifest } from '../internal/meta/standalone-dependency-manifest.ts';
15
+
16
+ const PRESET_CHOICES = [
17
+ {
18
+ label: 'React standalone',
19
+ value: 'react',
20
+ description: 'create-vite 기반 app starter에 runtime/template를 적용합니다.',
21
+ },
22
+ {
23
+ label: 'Next standalone',
24
+ value: 'next',
25
+ description: 'create-next-app 기반 app starter에 runtime/template를 적용합니다.',
26
+ },
27
+ {
28
+ label: 'Monorepo workspace',
29
+ value: 'monorepo',
30
+ description: 'workspace scaffold를 직접 생성합니다.',
31
+ },
32
+ ];
33
+
34
+ const EXECUTION_CHOICES = {
35
+ react: [
36
+ {
37
+ label: '자동 scaffold 후 생성',
38
+ value: 'auto-scaffold',
39
+ description: 'create-vite를 먼저 실행한 뒤 runtime/template를 자동 적용합니다.',
40
+ },
41
+ {
42
+ label: 'scaffolded dir에 적용',
43
+ value: 'scaffolded-apply',
44
+ description: '이미 create-vite로 만들어진 디렉터리에 runtime/template를 덮어씁니다.',
45
+ },
46
+ ],
47
+ next: [
48
+ {
49
+ label: '자동 scaffold 후 생성',
50
+ value: 'auto-scaffold',
51
+ description: 'create-next-app을 먼저 실행한 뒤 runtime/template를 자동 적용합니다.',
52
+ },
53
+ {
54
+ label: 'scaffolded dir에 적용',
55
+ value: 'scaffolded-apply',
56
+ description: '이미 create-next-app으로 만들어진 디렉터리에 runtime/template를 덮어씁니다.',
57
+ },
58
+ ],
59
+ monorepo: [
60
+ {
61
+ label: '바로 생성',
62
+ value: 'apply',
63
+ description: '비어 있는 디렉터리에 monorepo 결과물을 바로 생성합니다.',
64
+ },
65
+ ],
66
+ };
67
+
68
+ const ansi = {
69
+ reset: '\x1b[0m',
70
+ bold: '\x1b[1m',
71
+ dim: '\x1b[2m',
72
+ red: '\x1b[31m',
73
+ green: '\x1b[32m',
74
+ yellow: '\x1b[33m',
75
+ blue: '\x1b[34m',
76
+ magenta: '\x1b[35m',
77
+ cyan: '\x1b[36m',
78
+ gray: '\x1b[90m',
79
+ };
80
+
81
+ function supportsAnsi() {
82
+ if (process.env.NO_COLOR === '1' || process.env.NO_COLOR === 'true') {
83
+ return false;
84
+ }
85
+
86
+ return Boolean(output.isTTY);
87
+ }
88
+
89
+ function paint(text, ...codes) {
90
+ if (!supportsAnsi()) {
91
+ return text;
92
+ }
93
+
94
+ return `${codes.join('')}${text}${ansi.reset}`;
95
+ }
96
+
97
+ function title(text) {
98
+ return paint(text, ansi.bold, ansi.cyan);
99
+ }
100
+
101
+ function success(text) {
102
+ return paint(text, ansi.bold, ansi.green);
103
+ }
104
+
105
+ function warning(text) {
106
+ return paint(text, ansi.bold, ansi.yellow);
107
+ }
108
+
109
+ function danger(text) {
110
+ return paint(text, ansi.bold, ansi.red);
111
+ }
112
+
113
+ function subtle(text) {
114
+ return paint(text, ansi.dim, ansi.gray);
115
+ }
116
+
117
+ function accent(text) {
118
+ return paint(text, ansi.bold, ansi.magenta);
119
+ }
120
+
121
+ function info(text) {
122
+ return paint(text, ansi.bold, ansi.blue);
123
+ }
124
+
125
+ function createStepTitleBuilder() {
126
+ let step = 1;
127
+
128
+ return (text) => `${step++}. ${text}`;
129
+ }
130
+
131
+ function formatPresetLabel(preset) {
132
+ if (preset === 'react') {
133
+ return 'React standalone';
134
+ }
135
+
136
+ if (preset === 'next') {
137
+ return 'Next standalone';
138
+ }
139
+
140
+ return 'Monorepo workspace';
141
+ }
142
+
143
+ function printHeader() {
144
+ console.log('');
145
+ console.log(title('0to1 Generator Wizard ✨'));
146
+ console.log(subtle('preset 선택부터 생성 확인까지 전부 인터랙션으로 진행합니다.'));
147
+ console.log('');
148
+ }
149
+
150
+ function formatSummary({
151
+ plan,
152
+ result,
153
+ dryRun,
154
+ fromScaffoldedDir,
155
+ autoScaffold,
156
+ includeDocs,
157
+ useStyles,
158
+ withCommitlint,
159
+ }) {
160
+ return {
161
+ preset: plan.preset,
162
+ targetDir: plan.targetDir,
163
+ templateDir: plan.templateDir,
164
+ dryRun,
165
+ fromScaffoldedDir,
166
+ autoScaffold,
167
+ includeDocs,
168
+ useStyles,
169
+ withCommitlint,
170
+ removePaths: plan.removePaths,
171
+ templateFileCount: plan.templateFiles.length,
172
+ runtimeFileCount: plan.runtimeFiles.length,
173
+ copiedTemplateFileCount: result.copiedTemplateFiles.length,
174
+ copiedRuntimeFileCount: result.copiedRuntimeFiles.length,
175
+ };
176
+ }
177
+
178
+ async function ensureTargetDir(targetDir) {
179
+ await fs.mkdir(targetDir, {
180
+ recursive: true,
181
+ });
182
+ }
183
+
184
+ async function ensureParentDir(filePath) {
185
+ await fs.mkdir(path.dirname(filePath), {
186
+ recursive: true,
187
+ });
188
+ }
189
+
190
+ async function ensureTargetDirIsEmpty(targetDir) {
191
+ const entries = await fs.readdir(targetDir);
192
+
193
+ return entries.length === 0;
194
+ }
195
+
196
+ function getCommandName(command) {
197
+ if (process.platform === 'win32') {
198
+ return `${command}.cmd`;
199
+ }
200
+
201
+ return command;
202
+ }
203
+
204
+ function run(command, args, { cwd = process.cwd() } = {}) {
205
+ const result = spawnSync(getCommandName(command), args, {
206
+ cwd,
207
+ stdio: 'inherit',
208
+ });
209
+
210
+ if (result.status !== 0) {
211
+ throw new Error(`${command} ${args.join(' ')} 실행에 실패했습니다.`);
212
+ }
213
+ }
214
+
215
+ function getCreateViteTarget(targetDir) {
216
+ return path.relative(process.cwd(), targetDir) || path.basename(targetDir);
217
+ }
218
+
219
+ function hasDependency(packageJson, packageName) {
220
+ return Boolean(
221
+ packageJson?.dependencies?.[packageName] ??
222
+ packageJson?.devDependencies?.[packageName],
223
+ );
224
+ }
225
+
226
+ async function validateReactScaffold(targetDir) {
227
+ const packageJson = await readPackageJson(targetDir);
228
+ const isReactProject =
229
+ hasDependency(packageJson, 'react') && hasDependency(packageJson, '@vitejs/plugin-react');
230
+ const isTypeScriptProject = hasDependency(packageJson, 'typescript');
231
+
232
+ if (isReactProject && isTypeScriptProject) {
233
+ return;
234
+ }
235
+
236
+ throw new Error(
237
+ '이 generator는 현재 create-vite에서 React + TypeScript 계열만 지원합니다. create-vite 질문에서 React 프레임워크와 TypeScript variant를 선택해 주세요.',
238
+ );
239
+ }
240
+
241
+ async function validateNextScaffold(targetDir, { useStyles = true } = {}) {
242
+ const packageJson = await readPackageJson(targetDir);
243
+ const srcAppDirExists = await fileExists(path.join(targetDir, 'src/app'));
244
+ const tsconfigPath = path.join(targetDir, 'tsconfig.json');
245
+ const tsconfigText = (await fileExists(tsconfigPath))
246
+ ? await fs.readFile(tsconfigPath, 'utf8')
247
+ : '';
248
+ const hasRequiredDependencies =
249
+ hasDependency(packageJson, 'next') &&
250
+ hasDependency(packageJson, 'react') &&
251
+ hasDependency(packageJson, 'react-dom') &&
252
+ hasDependency(packageJson, 'typescript') &&
253
+ hasDependency(packageJson, 'eslint') &&
254
+ hasDependency(packageJson, 'eslint-config-next') &&
255
+ (!useStyles ||
256
+ (hasDependency(packageJson, 'tailwindcss') &&
257
+ hasDependency(packageJson, '@tailwindcss/postcss')));
258
+ const hasSupportedAlias =
259
+ tsconfigText.includes('"@/*"') &&
260
+ (tsconfigText.includes('"./src/*"') || tsconfigText.includes('["./src/*"]'));
261
+
262
+ if (hasRequiredDependencies && srcAppDirExists && hasSupportedAlias) {
263
+ return;
264
+ }
265
+
266
+ throw new Error(
267
+ useStyles
268
+ ? '이 generator는 현재 create-next-app에서 TypeScript + App Router + src dir + Tailwind + ESLint + @/* alias 조합만 지원합니다. create-next-app 질문에서 해당 조합으로 선택해 주세요.'
269
+ : '이 generator는 현재 create-next-app에서 TypeScript + App Router + src dir + ESLint + @/* alias 조합을 요구합니다. no-style을 선택해도 create-next-app 기본 질문에서는 해당 조건을 맞춰 주세요.',
270
+ );
271
+ }
272
+
273
+ async function fileExists(filePath) {
274
+ try {
275
+ await fs.access(filePath);
276
+ return true;
277
+ } catch {
278
+ return false;
279
+ }
280
+ }
281
+
282
+ async function readPackageJson(targetDir) {
283
+ const packageJsonPath = path.join(targetDir, 'package.json');
284
+
285
+ if (!(await fileExists(packageJsonPath))) {
286
+ return null;
287
+ }
288
+
289
+ const text = await fs.readFile(packageJsonPath, 'utf8');
290
+
291
+ return JSON.parse(text);
292
+ }
293
+
294
+ function getPackageManagerFromField(packageJson) {
295
+ const packageManagerField = packageJson?.packageManager;
296
+
297
+ if (typeof packageManagerField !== 'string') {
298
+ return null;
299
+ }
300
+
301
+ const packageManagerName = packageManagerField.split('@')[0];
302
+
303
+ if (['npm', 'pnpm', 'yarn', 'bun'].includes(packageManagerName)) {
304
+ return packageManagerName;
305
+ }
306
+
307
+ return null;
308
+ }
309
+
310
+ async function detectPackageManager(targetDir) {
311
+ const packageJson = await readPackageJson(targetDir);
312
+ const packageManagerFromField = getPackageManagerFromField(packageJson);
313
+
314
+ if (packageManagerFromField) {
315
+ return packageManagerFromField;
316
+ }
317
+
318
+ const lockfileChecks = [
319
+ ['pnpm-lock.yaml', 'pnpm'],
320
+ ['yarn.lock', 'yarn'],
321
+ ['bun.lock', 'bun'],
322
+ ['bun.lockb', 'bun'],
323
+ ['package-lock.json', 'npm'],
324
+ ['npm-shrinkwrap.json', 'npm'],
325
+ ];
326
+
327
+ for (const [lockfileName, packageManager] of lockfileChecks) {
328
+ if (await fileExists(path.join(targetDir, lockfileName))) {
329
+ return packageManager;
330
+ }
331
+ }
332
+
333
+ return 'npm';
334
+ }
335
+
336
+ function buildAddDependencyArgs(packageManager, packages, { dev = false } = {}) {
337
+ if (packages.length === 0) {
338
+ return null;
339
+ }
340
+
341
+ if (packageManager === 'npm') {
342
+ return ['install', ...(dev ? ['-D'] : []), ...packages];
343
+ }
344
+
345
+ if (packageManager === 'pnpm') {
346
+ return ['add', ...(dev ? ['-D'] : []), ...packages];
347
+ }
348
+
349
+ if (packageManager === 'yarn') {
350
+ return ['add', ...(dev ? ['-D'] : []), ...packages];
351
+ }
352
+
353
+ if (packageManager === 'bun') {
354
+ return ['add', ...(dev ? ['-d'] : []), ...packages];
355
+ }
356
+
357
+ throw new Error(`지원하지 않는 package manager입니다: ${packageManager}`);
358
+ }
359
+
360
+ function buildRemoveDependencyArgs(packageManager, packages) {
361
+ if (packages.length === 0) {
362
+ return null;
363
+ }
364
+
365
+ if (packageManager === 'npm') {
366
+ return ['uninstall', ...packages];
367
+ }
368
+
369
+ if (packageManager === 'pnpm') {
370
+ return ['remove', ...packages];
371
+ }
372
+
373
+ if (packageManager === 'yarn') {
374
+ return ['remove', ...packages];
375
+ }
376
+
377
+ if (packageManager === 'bun') {
378
+ return ['remove', ...packages];
379
+ }
380
+
381
+ throw new Error(`지원하지 않는 package manager입니다: ${packageManager}`);
382
+ }
383
+
384
+ async function readJsonFile(filePath) {
385
+ if (!(await fileExists(filePath))) {
386
+ return null;
387
+ }
388
+
389
+ const text = await fs.readFile(filePath, 'utf8');
390
+
391
+ return JSON.parse(text);
392
+ }
393
+
394
+ async function writeTextFile(filePath, value, { mode } = {}) {
395
+ await ensureParentDir(filePath);
396
+ await fs.writeFile(filePath, value, 'utf8');
397
+
398
+ if (typeof mode === 'number') {
399
+ await fs.chmod(filePath, mode);
400
+ }
401
+ }
402
+
403
+ async function writeJsonFile(filePath, value) {
404
+ await writeTextFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
405
+ }
406
+
407
+ async function updateJsonFile(filePath, updater) {
408
+ const currentValue = (await readJsonFile(filePath)) ?? {};
409
+ const nextValue = updater(currentValue);
410
+
411
+ await writeJsonFile(filePath, nextValue);
412
+ }
413
+
414
+ function appendScript(existingScript, appendedScript) {
415
+ if (typeof existingScript !== 'string' || existingScript.trim().length === 0) {
416
+ return appendedScript;
417
+ }
418
+
419
+ if (existingScript.includes(appendedScript)) {
420
+ return existingScript;
421
+ }
422
+
423
+ return `${existingScript} && ${appendedScript}`;
424
+ }
425
+
426
+ function getHuskyScriptName(packageManager) {
427
+ return packageManager === 'yarn' ? 'postinstall' : 'prepare';
428
+ }
429
+
430
+ function getPreCommitCommand(packageManager) {
431
+ if (packageManager === 'pnpm') return 'pnpm lint';
432
+ if (packageManager === 'yarn') return 'yarn lint';
433
+ if (packageManager === 'bun') return 'bun run lint';
434
+ return 'npm run lint';
435
+ }
436
+
437
+ function getCommitMsgCommand(packageManager) {
438
+ if (packageManager === 'pnpm') return 'pnpm exec commitlint --edit "$1"';
439
+ if (packageManager === 'yarn') return 'yarn exec commitlint --edit "$1"';
440
+ if (packageManager === 'bun') return 'bunx commitlint --edit "$1"';
441
+ return 'npx --no -- commitlint --edit "$1"';
442
+ }
443
+
444
+ function getStyleDependencyNames(preset) {
445
+ if (preset === 'react') {
446
+ return ['@tailwindcss/vite', 'tailwindcss'];
447
+ }
448
+
449
+ if (preset === 'next') {
450
+ return ['@tailwindcss/postcss', 'tailwindcss'];
451
+ }
452
+
453
+ return [];
454
+ }
455
+
456
+ function getReactNoStyleViteConfig() {
457
+ return `import path from 'node:path';
458
+
459
+ import react from '@vitejs/plugin-react';
460
+ import { defineConfig } from 'vite';
461
+
462
+ export default defineConfig({
463
+ plugins: [react()],
464
+ resolve: {
465
+ alias: {
466
+ '@': path.resolve(__dirname, './src'),
467
+ },
468
+ },
469
+ });
470
+ `;
471
+ }
472
+
473
+ function getReactNoStyleMainEntry() {
474
+ return `import { StrictMode } from 'react';
475
+ import { createRoot } from 'react-dom/client';
476
+
477
+ import App from '@/app/app';
478
+
479
+ createRoot(document.getElementById('root')!).render(
480
+ <StrictMode>
481
+ <App />
482
+ </StrictMode>,
483
+ );
484
+ `;
485
+ }
486
+
487
+ function getNoStyleButton() {
488
+ return `import type { ButtonHTMLAttributes, ReactNode } from 'react';
489
+
490
+ export type ButtonProps = {
491
+ children: ReactNode;
492
+ isLoading?: boolean;
493
+ } & ButtonHTMLAttributes<HTMLButtonElement>;
494
+
495
+ export function Button({
496
+ children,
497
+ disabled,
498
+ isLoading = false,
499
+ className,
500
+ ...props
501
+ }: ButtonProps) {
502
+ return (
503
+ <button disabled={disabled || isLoading} className={className} {...props}>
504
+ {isLoading ? '저장 중...' : children}
505
+ </button>
506
+ );
507
+ }
508
+ `;
509
+ }
510
+
511
+ function getNoStyleTextInputField() {
512
+ return `import type { InputHTMLAttributes } from 'react';
513
+
514
+ export type TextInputFieldProps = {
515
+ label: string;
516
+ required?: boolean;
517
+ error?: string;
518
+ helperText?: string;
519
+ } & InputHTMLAttributes<HTMLInputElement>;
520
+
521
+ export function TextInputField({
522
+ label,
523
+ required,
524
+ error,
525
+ helperText,
526
+ id,
527
+ className,
528
+ ...props
529
+ }: TextInputFieldProps) {
530
+ const inputId = id ?? props.name;
531
+
532
+ return (
533
+ <div>
534
+ <label htmlFor={inputId}>
535
+ {label}
536
+ {required ? ' *' : null}
537
+ </label>
538
+ <input id={inputId} className={className} aria-invalid={error ? true : undefined} {...props} />
539
+ {error ? <p role="alert">{error}</p> : null}
540
+ {!error && helperText ? <p>{helperText}</p> : null}
541
+ </div>
542
+ );
543
+ }
544
+ `;
545
+ }
546
+
547
+ function getReactNoStyleGlobalModal() {
548
+ return `import { useModalStore } from '@/shared/ui/modal-store';
549
+
550
+ export function GlobalModal() {
551
+ const { isOpen, title, description, closeModal } = useModalStore();
552
+
553
+ if (!isOpen) return null;
554
+
555
+ return (
556
+ <div role="dialog" aria-modal="true" aria-labelledby="global-modal-title">
557
+ <div>
558
+ <h2 id="global-modal-title">{title}</h2>
559
+ {description ? <p>{description}</p> : null}
560
+ <div>
561
+ <button type="button" onClick={closeModal}>
562
+ 닫기
563
+ </button>
564
+ </div>
565
+ </div>
566
+ </div>
567
+ );
568
+ }
569
+ `;
570
+ }
571
+
572
+ function getNextNoStyleGlobalModal() {
573
+ return `'use client';
574
+
575
+ import { useModalStore } from '@/shared/ui/modal-store';
576
+
577
+ export function GlobalModal() {
578
+ const { isOpen, title, description, closeModal } = useModalStore();
579
+
580
+ if (!isOpen) return null;
581
+
582
+ return (
583
+ <div role="dialog" aria-modal="true" aria-labelledby="global-modal-title">
584
+ <div>
585
+ <h2 id="global-modal-title">{title}</h2>
586
+ {description ? <p>{description}</p> : null}
587
+ <div>
588
+ <button type="button" onClick={closeModal}>
589
+ 닫기
590
+ </button>
591
+ </div>
592
+ </div>
593
+ </div>
594
+ );
595
+ }
596
+ `;
597
+ }
598
+
599
+ function getReactNoStyleHomePage() {
600
+ return `/**
601
+ * 템플릿 첫 화면 예시입니다.
602
+ * 프로젝트 소개나 대시보드 시작 화면으로 교체해서 사용하세요.
603
+ */
604
+ export default function HomePage() {
605
+ return (
606
+ <main>
607
+ <section>
608
+ <p>템플릿</p>
609
+ <h1>필요한 기본값과 필수 전역 처리를 갖춘 Vite React 프론트엔드 스타터</h1>
610
+ <p>
611
+ 이 템플릿은 feature-first 구조, React Router, TanStack Query, request helper, shared
612
+ utilities처럼 초기에 바로 필요한 것만 포함합니다.
613
+ </p>
614
+ </section>
615
+
616
+ <section>
617
+ <h2>기본 라우팅 흐름까지 바로 시작</h2>
618
+ <ul>
619
+ <li>
620
+ <code>src/pages/**/page.tsx</code> 파일은 자동으로 React Router 경로가 됩니다.
621
+ </li>
622
+ <li>
623
+ <code>react-router-dom</code>, Query, request helper를 기본으로 포함했습니다.
624
+ </li>
625
+ <li>
626
+ 폼/목록 예제가 필요하면 <code>npm run create:feature -- products</code>로 생성할 수
627
+ 있습니다.
628
+ </li>
629
+ </ul>
630
+ </section>
631
+ </main>
632
+ );
633
+ }
634
+ `;
635
+ }
636
+
637
+ function getReactNoStyleLoginPage() {
638
+ return `/**
639
+ * 인증 흐름 예시를 위한 로그인 플레이스홀더 화면입니다.
640
+ * 프로젝트의 실제 로그인 페이지로 교체해서 사용하세요.
641
+ */
642
+ export default function LoginPage() {
643
+ return (
644
+ <main>
645
+ <section>
646
+ <p>로그인</p>
647
+ <h1>로그인이 필요합니다</h1>
648
+ <p>
649
+ 기본 템플릿은 인증 오류가 발생하면 이 경로로 이동할 수 있습니다. 프로젝트 로그인 화면으로
650
+ 교체하세요.
651
+ </p>
652
+ </section>
653
+ </main>
654
+ );
655
+ }
656
+ `;
657
+ }
658
+
659
+ function getReactNoStyleFrame() {
660
+ return `import { Outlet, useNavigation } from 'react-router-dom';
661
+
662
+ /**
663
+ * 앱 공통 레이아웃 예시입니다.
664
+ * 공통 헤더, 인증 리스너, 전역 모달 연결이 필요하면 여기서 추가해서 사용하세요.
665
+ */
666
+ export function AppFrame() {
667
+ const navigation = useNavigation();
668
+ const isRoutePending = navigation.state !== 'idle';
669
+
670
+ return (
671
+ <div>
672
+ {isRoutePending ? (
673
+ <div role="status" aria-live="polite">
674
+ 페이지를 불러오는 중입니다.
675
+ </div>
676
+ ) : null}
677
+ <Outlet />
678
+ </div>
679
+ );
680
+ }
681
+ `;
682
+ }
683
+
684
+ function getReactNoStyleErrorBoundary() {
685
+ return `import { Component, type ErrorInfo, type ReactNode } from 'react';
686
+
687
+ type AppErrorBoundaryProps = {
688
+ children: ReactNode;
689
+ };
690
+
691
+ type AppErrorBoundaryState = {
692
+ error: Error | null;
693
+ retryKey: number;
694
+ };
695
+
696
+ export class AppErrorBoundary extends Component<AppErrorBoundaryProps, AppErrorBoundaryState> {
697
+ state: AppErrorBoundaryState = {
698
+ error: null,
699
+ retryKey: 0,
700
+ };
701
+
702
+ static getDerivedStateFromError(error: Error): Partial<AppErrorBoundaryState> {
703
+ return {
704
+ error,
705
+ };
706
+ }
707
+
708
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
709
+ console.error(error, errorInfo);
710
+ }
711
+
712
+ handleRetry = () => {
713
+ this.setState((prevState) => ({
714
+ error: null,
715
+ retryKey: prevState.retryKey + 1,
716
+ }));
717
+ };
718
+
719
+ render() {
720
+ if (this.state.error) {
721
+ return (
722
+ <main>
723
+ <section>
724
+ <p>오류</p>
725
+ <h1>문제가 발생했습니다.</h1>
726
+ <p>요청을 처리하는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</p>
727
+ <button type="button" onClick={this.handleRetry}>
728
+ 다시 시도
729
+ </button>
730
+ </section>
731
+ </main>
732
+ );
733
+ }
734
+
735
+ return <div key={this.state.retryKey}>{this.props.children}</div>;
736
+ }
737
+ }
738
+ `;
739
+ }
740
+
741
+ function getReactNoStyleNotFoundPage() {
742
+ return `import { Link } from 'react-router-dom';
743
+
744
+ import { routePaths } from '@/shared/routes/route-paths';
745
+
746
+ export function NotFoundPage() {
747
+ return (
748
+ <main>
749
+ <section>
750
+ <p>404</p>
751
+ <h1>페이지를 찾을 수 없습니다</h1>
752
+ <p>주소가 잘못되었거나, 페이지가 이동되었을 수 있습니다.</p>
753
+ <Link to={routePaths.home}>홈으로 이동</Link>
754
+ </section>
755
+ </main>
756
+ );
757
+ }
758
+ `;
759
+ }
760
+
761
+ function getNextNoStyleLayout() {
762
+ return `import type { Metadata } from 'next';
763
+
764
+ import { AppProviders } from '@/app/providers';
765
+
766
+ export const metadata: Metadata = {
767
+ title: 'Next 프론트엔드 템플릿',
768
+ description: '가벼운 기본값과 필수 전역 에러 처리를 갖춘 Next.js 시작 템플릿',
769
+ };
770
+
771
+ /**
772
+ * 앱 공통 레이아웃 예시입니다.
773
+ * 메타데이터와 전역 provider 구성을 프로젝트에 맞게 바꿔서 사용하세요.
774
+ */
775
+ export default function RootLayout({
776
+ children,
777
+ }: Readonly<{
778
+ children: React.ReactNode;
779
+ }>) {
780
+ return (
781
+ <html lang="ko">
782
+ <body>
783
+ <AppProviders>{children}</AppProviders>
784
+ </body>
785
+ </html>
786
+ );
787
+ }
788
+ `;
789
+ }
790
+
791
+ function getNextNoStyleHomePage() {
792
+ return `/**
793
+ * 템플릿 첫 화면 예시입니다.
794
+ * 프로젝트 소개나 대시보드 시작 화면으로 교체해서 사용하세요.
795
+ */
796
+ export default function Home() {
797
+ return (
798
+ <main>
799
+ <section>
800
+ <p>템플릿</p>
801
+ <h1>필요한 기본값과 필수 전역 처리를 갖춘 Next.js 프론트엔드 스타터</h1>
802
+ <p>
803
+ 이 템플릿은 feature-first 구조, TanStack Query, request helper, shared utilities와
804
+ 공통 인증/에러 반응처럼 초기에 바로 필요한 것만 포함합니다.
805
+ </p>
806
+ </section>
807
+
808
+ <section>
809
+ <h2>최소 기능도 끊기지 않게</h2>
810
+ <ul>
811
+ <li>기본 provider는 Query와 공통 에러 반응만 연결해 두어 시작 비용을 낮췄습니다.</li>
812
+ <li>인증 만료 redirect와 네트워크/권한 오류 모달은 기본 동작으로 포함합니다.</li>
813
+ <li>
814
+ 폼/목록 예제가 필요하면 <code>npm run create:feature -- products</code>로 생성할 수
815
+ 있습니다.
816
+ </li>
817
+ </ul>
818
+ </section>
819
+ </main>
820
+ );
821
+ }
822
+ `;
823
+ }
824
+
825
+ function getNextNoStyleLoginPage() {
826
+ return `/**
827
+ * 인증 흐름 예시를 위한 로그인 플레이스홀더 화면입니다.
828
+ * 프로젝트의 실제 로그인 페이지로 교체해서 사용하세요.
829
+ */
830
+ export default function LoginPage() {
831
+ return (
832
+ <main>
833
+ <section>
834
+ <p>로그인</p>
835
+ <h1>로그인이 필요합니다</h1>
836
+ <p>
837
+ 기본 템플릿은 인증 오류가 발생하면 이 경로로 이동합니다. 프로젝트 로그인 화면으로
838
+ 교체하세요.
839
+ </p>
840
+ </section>
841
+ </main>
842
+ );
843
+ }
844
+ `;
845
+ }
846
+
847
+ function getNextNoStyleErrorPage() {
848
+ return `'use client';
849
+
850
+ import { useEffect } from 'react';
851
+
852
+ type AppErrorPageProps = {
853
+ error: Error & { digest?: string };
854
+ unstable_retry: () => void;
855
+ };
856
+
857
+ export default function AppErrorPage({ error, unstable_retry }: AppErrorPageProps) {
858
+ useEffect(() => {
859
+ console.error(error);
860
+ }, [error]);
861
+
862
+ return (
863
+ <main>
864
+ <section>
865
+ <p>오류</p>
866
+ <h1>문제가 발생했습니다.</h1>
867
+ <p>요청을 처리하는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</p>
868
+ <button type="button" onClick={() => unstable_retry()}>
869
+ 다시 시도
870
+ </button>
871
+ </section>
872
+ </main>
873
+ );
874
+ }
875
+ `;
876
+ }
877
+
878
+ async function removeTargetPath(filePath) {
879
+ await fs.rm(filePath, {
880
+ force: true,
881
+ recursive: true,
882
+ });
883
+ }
884
+
885
+ async function applyReactNoStyleFiles(targetDir) {
886
+ await writeTextFile(path.join(targetDir, 'vite.config.ts'), getReactNoStyleViteConfig());
887
+ await writeTextFile(path.join(targetDir, 'src', 'main.tsx'), getReactNoStyleMainEntry());
888
+ await writeTextFile(path.join(targetDir, 'src', 'app', 'frame.tsx'), getReactNoStyleFrame());
889
+ await writeTextFile(
890
+ path.join(targetDir, 'src', 'app', 'error-boundary.tsx'),
891
+ getReactNoStyleErrorBoundary(),
892
+ );
893
+ await writeTextFile(
894
+ path.join(targetDir, 'src', 'app', 'not-found-page.tsx'),
895
+ getReactNoStyleNotFoundPage(),
896
+ );
897
+ await writeTextFile(
898
+ path.join(targetDir, 'src', 'pages', 'index', 'page.tsx'),
899
+ getReactNoStyleHomePage(),
900
+ );
901
+ await writeTextFile(
902
+ path.join(targetDir, 'src', 'pages', 'login', 'page.tsx'),
903
+ getReactNoStyleLoginPage(),
904
+ );
905
+ await writeTextFile(path.join(targetDir, 'src', 'shared', 'ui', 'button.tsx'), getNoStyleButton());
906
+ await writeTextFile(
907
+ path.join(targetDir, 'src', 'shared', 'ui', 'text-input-field.tsx'),
908
+ getNoStyleTextInputField(),
909
+ );
910
+ await writeTextFile(
911
+ path.join(targetDir, 'src', 'shared', 'ui', 'global-modal.tsx'),
912
+ getReactNoStyleGlobalModal(),
913
+ );
914
+ await removeTargetPath(path.join(targetDir, 'src', 'app', 'globals.css'));
915
+ }
916
+
917
+ async function applyNextNoStyleFiles(targetDir) {
918
+ await writeTextFile(path.join(targetDir, 'src', 'app', 'layout.tsx'), getNextNoStyleLayout());
919
+ await writeTextFile(path.join(targetDir, 'src', 'app', 'page.tsx'), getNextNoStyleHomePage());
920
+ await writeTextFile(
921
+ path.join(targetDir, 'src', 'app', 'login', 'page.tsx'),
922
+ getNextNoStyleLoginPage(),
923
+ );
924
+ await writeTextFile(path.join(targetDir, 'src', 'app', 'error.tsx'), getNextNoStyleErrorPage());
925
+ await writeTextFile(path.join(targetDir, 'src', 'shared', 'ui', 'button.tsx'), getNoStyleButton());
926
+ await writeTextFile(
927
+ path.join(targetDir, 'src', 'shared', 'ui', 'text-input-field.tsx'),
928
+ getNoStyleTextInputField(),
929
+ );
930
+ await writeTextFile(
931
+ path.join(targetDir, 'src', 'shared', 'ui', 'global-modal.tsx'),
932
+ getNextNoStyleGlobalModal(),
933
+ );
934
+ await removeTargetPath(path.join(targetDir, 'src', 'app', 'globals.css'));
935
+ await removeTargetPath(path.join(targetDir, 'postcss.config.mjs'));
936
+ }
937
+
938
+ async function applyStandaloneStyleMode(preset, targetDir, { useStyles = true } = {}) {
939
+ if (preset !== 'react' && preset !== 'next') {
940
+ return;
941
+ }
942
+
943
+ if (useStyles) {
944
+ return;
945
+ }
946
+
947
+ if (preset === 'react') {
948
+ await applyReactNoStyleFiles(targetDir);
949
+ return;
950
+ }
951
+
952
+ await applyNextNoStyleFiles(targetDir);
953
+ }
954
+
955
+ async function copyDevelopmentPrinciplesDocs(workspaceRoot, targetDir) {
956
+ const sourceDir = path.join(
957
+ workspaceRoot,
958
+ 'templates',
959
+ 'shared',
960
+ 'docs',
961
+ '개발원칙',
962
+ );
963
+ const targetDocsDir = path.join(targetDir, 'docs', '개발원칙');
964
+
965
+ if (!(await fileExists(sourceDir))) {
966
+ throw new Error(`개발 원칙 문서를 찾지 못했습니다: ${sourceDir}`);
967
+ }
968
+
969
+ await fs.mkdir(path.dirname(targetDocsDir), {
970
+ recursive: true,
971
+ });
972
+ await fs.cp(sourceDir, targetDocsDir, {
973
+ recursive: true,
974
+ });
975
+ }
976
+
977
+ function runPackageScript(packageManager, scriptName, cwd) {
978
+ if (packageManager === 'yarn') {
979
+ run('yarn', ['run', scriptName], { cwd });
980
+ return;
981
+ }
982
+
983
+ run(packageManager, ['run', scriptName], { cwd });
984
+ }
985
+
986
+ async function enableCommitlint(targetDir, packageManager) {
987
+ const packageJsonPath = path.join(targetDir, 'package.json');
988
+ const huskyScriptName = getHuskyScriptName(packageManager);
989
+ const huskyInstallScript = 'node .husky/install.mjs';
990
+
991
+ await updateJsonFile(packageJsonPath, (packageJson) => ({
992
+ ...packageJson,
993
+ scripts: {
994
+ ...(packageJson.scripts ?? {}),
995
+ [huskyScriptName]: appendScript(packageJson.scripts?.[huskyScriptName], huskyInstallScript),
996
+ },
997
+ }));
998
+
999
+ await writeTextFile(
1000
+ path.join(targetDir, '.husky', 'install.mjs'),
1001
+ `import fs from 'node:fs';
1002
+ import path from 'node:path';
1003
+
1004
+ const rootDir = process.cwd();
1005
+ const gitPath = path.join(rootDir, '.git');
1006
+
1007
+ if (
1008
+ process.env.NODE_ENV === 'production' ||
1009
+ process.env.CI === 'true' ||
1010
+ process.env.HUSKY === '0'
1011
+ ) {
1012
+ process.exit(0);
1013
+ }
1014
+
1015
+ if (!fs.existsSync(gitPath)) {
1016
+ process.exit(0);
1017
+ }
1018
+
1019
+ try {
1020
+ const husky = (await import('husky')).default;
1021
+ husky();
1022
+ } catch {
1023
+ process.exit(0);
1024
+ }
1025
+ `,
1026
+ );
1027
+ await writeTextFile(
1028
+ path.join(targetDir, '.husky', 'pre-commit'),
1029
+ `${getPreCommitCommand(packageManager)}\n`,
1030
+ { mode: 0o755 },
1031
+ );
1032
+ await writeTextFile(
1033
+ path.join(targetDir, '.husky', 'commit-msg'),
1034
+ `${getCommitMsgCommand(packageManager)}\n`,
1035
+ { mode: 0o755 },
1036
+ );
1037
+ await writeTextFile(
1038
+ path.join(targetDir, 'commitlint.config.mjs'),
1039
+ `const commitlintConfig = {
1040
+ extends: ['@commitlint/config-conventional'],
1041
+ rules: {
1042
+ 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']],
1043
+ },
1044
+ };
1045
+
1046
+ export default commitlintConfig;
1047
+ `,
1048
+ );
1049
+
1050
+ runPackageScript(packageManager, huskyScriptName, targetDir);
1051
+ }
1052
+
1053
+ async function syncStandaloneDependencies(
1054
+ preset,
1055
+ targetDir,
1056
+ { useStyles = true, withCommitlint = false } = {},
1057
+ ) {
1058
+ if (preset !== 'react' && preset !== 'next') {
1059
+ return {
1060
+ packageManager: null,
1061
+ dependencyCount: 0,
1062
+ devDependencyCount: 0,
1063
+ };
1064
+ }
1065
+
1066
+ const dependencyManifest = getStandaloneDependencyManifest(preset);
1067
+ const packageManager = await detectPackageManager(targetDir);
1068
+ const styleDependencyNames = getStyleDependencyNames(preset);
1069
+ const packageJsonBeforeSync = await readPackageJson(targetDir);
1070
+ const removableStyleDependencies = !useStyles
1071
+ ? styleDependencyNames.filter((packageName) =>
1072
+ hasDependency(packageJsonBeforeSync, packageName),
1073
+ )
1074
+ : [];
1075
+ const filteredDevDependencies = dependencyManifest.devDependencies.filter(
1076
+ (packageName) => useStyles || !styleDependencyNames.includes(packageName),
1077
+ );
1078
+ const optionalDevDependencies = withCommitlint
1079
+ ? [
1080
+ ...(filteredDevDependencies.includes('husky') ? [] : ['husky']),
1081
+ '@commitlint/cli',
1082
+ '@commitlint/config-conventional',
1083
+ ]
1084
+ : [];
1085
+ const removeDependencyArgs = buildRemoveDependencyArgs(
1086
+ packageManager,
1087
+ removableStyleDependencies,
1088
+ );
1089
+ const dependencyArgs = buildAddDependencyArgs(
1090
+ packageManager,
1091
+ dependencyManifest.dependencies,
1092
+ );
1093
+ const devDependencyArgs = buildAddDependencyArgs(
1094
+ packageManager,
1095
+ filteredDevDependencies,
1096
+ { dev: true },
1097
+ );
1098
+ const optionalDevDependencyArgs = buildAddDependencyArgs(
1099
+ packageManager,
1100
+ optionalDevDependencies,
1101
+ { dev: true },
1102
+ );
1103
+
1104
+ console.log('');
1105
+ console.log(
1106
+ info(
1107
+ `${packageManager} 기준으로 ${preset} 추가 의존성을 최신 버전으로 동기화합니다.`,
1108
+ ),
1109
+ );
1110
+ console.log(
1111
+ subtle(
1112
+ `deps ${dependencyManifest.dependencies.length}개, devDeps ${filteredDevDependencies.length + optionalDevDependencies.length}개를 package manager 기본 정책으로 반영합니다.`,
1113
+ ),
1114
+ );
1115
+ console.log('');
1116
+
1117
+ if (removeDependencyArgs) {
1118
+ run(packageManager, removeDependencyArgs, { cwd: targetDir });
1119
+ }
1120
+
1121
+ // 버전 문자열을 템플릿에 고정하지 않고 생성 시점의 package manager가
1122
+ // 현재 레지스트리 기준 범위를 package.json에 기록하도록 맡깁니다.
1123
+ if (dependencyArgs) {
1124
+ run(packageManager, dependencyArgs, { cwd: targetDir });
1125
+ }
1126
+
1127
+ if (devDependencyArgs) {
1128
+ run(packageManager, devDependencyArgs, { cwd: targetDir });
1129
+ }
1130
+
1131
+ if (optionalDevDependencyArgs) {
1132
+ run(packageManager, optionalDevDependencyArgs, { cwd: targetDir });
1133
+ }
1134
+
1135
+ return {
1136
+ packageManager,
1137
+ dependencyCount: dependencyManifest.dependencies.length,
1138
+ devDependencyCount: filteredDevDependencies.length + optionalDevDependencies.length,
1139
+ };
1140
+ }
1141
+
1142
+ function scaffoldReactProject(targetDir) {
1143
+ console.log('');
1144
+ console.log(info('create-vite 기본 인터랙션으로 React scaffold를 준비합니다.'));
1145
+ console.log('');
1146
+
1147
+ run('npx', [
1148
+ 'create-vite@latest',
1149
+ getCreateViteTarget(targetDir),
1150
+ '--no-immediate',
1151
+ ]);
1152
+ }
1153
+
1154
+ function scaffoldNextProject(targetDir) {
1155
+ console.log('');
1156
+ console.log(info('create-next-app 기본 인터랙션으로 Next scaffold를 준비합니다.'));
1157
+ console.log('');
1158
+
1159
+ // create-next-app은 옵션이 하나라도 들어가면 내부적으로 defaults mode로
1160
+ // 넘어가서 기본 질문을 건너뛸 수 있습니다. Next native 질문을 살리려면
1161
+ // project path 외에는 넘기지 않는 편이 가장 안전합니다.
1162
+ run('npx', [
1163
+ 'create-next-app@latest',
1164
+ targetDir,
1165
+ ]);
1166
+ }
1167
+
1168
+ async function askQuestion(rl, promptText, { defaultValue = '' } = {}) {
1169
+ const suffix = defaultValue ? ` ${subtle(`(${defaultValue})`)}` : '';
1170
+ const answer = await rl.question(`${paint('›', ansi.bold, ansi.blue)} ${promptText}${suffix}: `);
1171
+ const trimmed = answer.trim();
1172
+
1173
+ if (!trimmed && defaultValue) {
1174
+ return defaultValue;
1175
+ }
1176
+
1177
+ return trimmed;
1178
+ }
1179
+
1180
+ async function askChoice(rl, heading, choices) {
1181
+ console.log(title(heading));
1182
+
1183
+ choices.forEach((choice, index) => {
1184
+ console.log(` ${accent(String(index + 1))}. ${choice.label}`);
1185
+ console.log(` ${subtle(choice.description)}`);
1186
+ });
1187
+
1188
+ console.log('');
1189
+
1190
+ while (true) {
1191
+ const answer = await askQuestion(rl, '번호를 입력해 주세요');
1192
+ const selectedIndex = Number.parseInt(answer, 10) - 1;
1193
+
1194
+ if (Number.isInteger(selectedIndex) && choices[selectedIndex]) {
1195
+ console.log('');
1196
+ return choices[selectedIndex];
1197
+ }
1198
+
1199
+ console.log(danger('올바른 번호를 입력해 주세요.'));
1200
+ console.log('');
1201
+ }
1202
+ }
1203
+
1204
+ async function askStyleMode(rl, heading) {
1205
+ const styleChoice = await askChoice(rl, heading, [
1206
+ {
1207
+ label: 'Tailwind 포함',
1208
+ value: 'tailwind',
1209
+ description: '기본 스타일과 Tailwind 구성을 함께 생성합니다.',
1210
+ },
1211
+ {
1212
+ label: 'No Style',
1213
+ value: 'no-style',
1214
+ description: '스타일 파일과 Tailwind 의존성 없이 구조와 로직 위주로 생성합니다.',
1215
+ },
1216
+ ]);
1217
+
1218
+ return styleChoice.value === 'tailwind';
1219
+ }
1220
+
1221
+ async function askCommitlintInclusion(rl, heading) {
1222
+ const commitlintChoice = await askChoice(rl, heading, [
1223
+ {
1224
+ label: '포함 안 함',
1225
+ value: 'exclude',
1226
+ description: 'husky와 commitlint 설정을 추가하지 않습니다.',
1227
+ },
1228
+ {
1229
+ label: '같이 포함',
1230
+ value: 'include',
1231
+ description: 'commitlint 설정과 husky hook을 함께 구성합니다.',
1232
+ },
1233
+ ]);
1234
+
1235
+ return commitlintChoice.value === 'include';
1236
+ }
1237
+
1238
+ async function askDocsInclusion(rl, heading) {
1239
+ const docsChoice = await askChoice(rl, heading, [
1240
+ {
1241
+ label: '포함 안 함',
1242
+ value: 'exclude',
1243
+ description: '문서는 generator 안에서만 중앙 관리하고 생성물에는 넣지 않습니다.',
1244
+ },
1245
+ {
1246
+ label: '같이 포함',
1247
+ value: 'include',
1248
+ description: '생성물 루트에 docs/개발원칙을 같이 복사합니다.',
1249
+ },
1250
+ ]);
1251
+
1252
+ return docsChoice.value === 'include';
1253
+ }
1254
+
1255
+ async function askFinalConfirmation(rl, heading) {
1256
+ const confirmationChoice = await askChoice(rl, heading, [
1257
+ {
1258
+ label: '바로 진행',
1259
+ value: 'confirm',
1260
+ description: '지금 고른 설정으로 scaffold와 generator 적용을 진행합니다.',
1261
+ },
1262
+ {
1263
+ label: '취소',
1264
+ value: 'cancel',
1265
+ description: '아무 작업도 하지 않고 wizard를 종료합니다.',
1266
+ },
1267
+ ]);
1268
+
1269
+ return confirmationChoice.value === 'confirm';
1270
+ }
1271
+
1272
+ function printExecutionNotes(preset, executionChoice) {
1273
+ if (preset === 'react' || preset === 'next') {
1274
+ if (executionChoice.value === 'scaffolded-apply') {
1275
+ console.log(
1276
+ warning(
1277
+ `${preset}는 기존 scaffolded dir에도 적용할 수 있습니다. 현재 디렉터리 상태를 확인하고 진행해 주세요.`,
1278
+ ),
1279
+ );
1280
+ console.log('');
1281
+ }
1282
+
1283
+ if (executionChoice.value === 'auto-scaffold') {
1284
+ if (preset === 'react') {
1285
+ console.log(
1286
+ subtle(
1287
+ 'React auto scaffold는 create-vite 기본 인터랙션을 그대로 넘깁니다.',
1288
+ ),
1289
+ );
1290
+ console.log(
1291
+ subtle(
1292
+ '현재 generator 계약상 create-vite 안에서는 React 프레임워크와 TypeScript 계열 variant를 골라야 합니다.',
1293
+ ),
1294
+ );
1295
+ }
1296
+
1297
+ if (preset === 'next') {
1298
+ console.log(
1299
+ subtle(
1300
+ 'Next auto scaffold는 create-next-app 기본 인터랙션을 그대로 넘깁니다.',
1301
+ ),
1302
+ );
1303
+ console.log(
1304
+ subtle(
1305
+ '옵션을 미리 넘기면 defaults mode로 넘어갈 수 있어서, generator도 path 외에는 create-next-app 옵션을 강제로 넣지 않습니다.',
1306
+ ),
1307
+ );
1308
+ console.log(
1309
+ subtle(
1310
+ '현재 generator 계약상 TypeScript + App Router + src dir + Tailwind + ESLint + @/* alias 조합으로 선택해 주셔야 합니다.',
1311
+ ),
1312
+ );
1313
+ console.log(
1314
+ subtle(
1315
+ '참고로 create-next-app의 recommended defaults에는 현재 No src/ directory가 포함되어 있어서, customize settings로 들어가시는 편이 안전합니다.',
1316
+ ),
1317
+ );
1318
+ }
1319
+
1320
+ console.log('');
1321
+ }
1322
+ }
1323
+ }
1324
+
1325
+ function printPlannedSelections({
1326
+ preset,
1327
+ executionChoice,
1328
+ targetArg,
1329
+ useStyles,
1330
+ withCommitlint,
1331
+ includeDocs,
1332
+ }) {
1333
+ console.log(title('선택한 설정'));
1334
+ console.log(` ${title('preset')} ${formatPresetLabel(preset)}`);
1335
+ console.log(` ${title('실행 방식')} ${executionChoice.label}`);
1336
+ console.log(` ${title('target')} ${targetArg}`);
1337
+
1338
+ if (preset === 'react' || preset === 'next') {
1339
+ console.log(` ${title('스타일')} ${useStyles ? 'Tailwind 포함' : 'No Style'}`);
1340
+ console.log(` ${title('commitlint')} ${withCommitlint ? '같이 포함' : '포함 안 함'}`);
1341
+ }
1342
+
1343
+ console.log(` ${title('docs/개발원칙')} ${includeDocs ? '같이 포함' : '포함 안 함'}`);
1344
+ console.log('');
1345
+ }
1346
+
1347
+ async function collectInteractiveOptions() {
1348
+ const rl = readline.createInterface({
1349
+ input,
1350
+ output,
1351
+ });
1352
+
1353
+ try {
1354
+ printHeader();
1355
+ const nextStepTitle = createStepTitleBuilder();
1356
+
1357
+ const presetChoice = await askChoice(
1358
+ rl,
1359
+ nextStepTitle('생성할 preset을 선택해 주세요'),
1360
+ PRESET_CHOICES,
1361
+ );
1362
+ const executionChoice = await askChoice(
1363
+ rl,
1364
+ nextStepTitle('실행 방식을 선택해 주세요'),
1365
+ EXECUTION_CHOICES[presetChoice.value],
1366
+ );
1367
+
1368
+ printExecutionNotes(presetChoice.value, executionChoice);
1369
+
1370
+ const targetArg = await askQuestion(
1371
+ rl,
1372
+ nextStepTitle('target 디렉터리 경로를 입력해 주세요'),
1373
+ {
1374
+ defaultValue: `./playground/${presetChoice.value}-app`,
1375
+ },
1376
+ );
1377
+ const useStyles =
1378
+ presetChoice.value === 'react' || presetChoice.value === 'next'
1379
+ ? await askStyleMode(rl, nextStepTitle('스타일 모드를 선택해 주세요'))
1380
+ : true;
1381
+ const withCommitlint =
1382
+ presetChoice.value === 'react' || presetChoice.value === 'next'
1383
+ ? await askCommitlintInclusion(
1384
+ rl,
1385
+ nextStepTitle('commitlint와 husky 포함 여부를 선택해 주세요'),
1386
+ )
1387
+ : false;
1388
+ const includeDocs = await askDocsInclusion(
1389
+ rl,
1390
+ nextStepTitle('docs/개발원칙 포함 여부를 선택해 주세요'),
1391
+ );
1392
+
1393
+ if (
1394
+ presetChoice.value === 'next' &&
1395
+ executionChoice.value === 'auto-scaffold' &&
1396
+ !useStyles
1397
+ ) {
1398
+ console.log(
1399
+ subtle(
1400
+ 'Next no-style을 선택해도 create-next-app 질문에서는 TypeScript + App Router + src dir + ESLint + @/* alias는 꼭 맞춰 주세요.',
1401
+ ),
1402
+ );
1403
+ console.log('');
1404
+ }
1405
+
1406
+ printPlannedSelections({
1407
+ preset: presetChoice.value,
1408
+ executionChoice,
1409
+ targetArg,
1410
+ useStyles,
1411
+ withCommitlint,
1412
+ includeDocs,
1413
+ });
1414
+
1415
+ const confirmed = await askFinalConfirmation(
1416
+ rl,
1417
+ nextStepTitle('이 설정으로 진행할까요?'),
1418
+ );
1419
+
1420
+ if (!confirmed) {
1421
+ throw new Error('사용자 취소로 종료했습니다.');
1422
+ }
1423
+
1424
+ return {
1425
+ preset: presetChoice.value,
1426
+ targetArg,
1427
+ dryRun: false,
1428
+ fromScaffoldedDir: executionChoice.value === 'scaffolded-apply',
1429
+ autoScaffold: executionChoice.value === 'auto-scaffold',
1430
+ useStyles,
1431
+ withCommitlint,
1432
+ includeDocs,
1433
+ };
1434
+ } finally {
1435
+ rl.close();
1436
+ }
1437
+ }
1438
+
1439
+ function printSummary(summary) {
1440
+ const modeLabel = summary.dryRun
1441
+ ? '계획만 확인'
1442
+ : summary.autoScaffold
1443
+ ? '자동 scaffold 후 생성'
1444
+ : summary.fromScaffoldedDir
1445
+ ? 'scaffolded dir에 적용'
1446
+ : '바로 생성';
1447
+
1448
+ console.log('');
1449
+ console.log(success(summary.dryRun ? '계획 확인이 완료되었습니다.' : '파일 생성이 완료되었습니다.'));
1450
+ console.log('');
1451
+ console.log(`${title('preset')} ${formatPresetLabel(summary.preset)}`);
1452
+ console.log(`${title('target')} ${summary.targetDir}`);
1453
+ console.log(`${title('실행 방식')} ${modeLabel}`);
1454
+ if (summary.preset === 'react' || summary.preset === 'next') {
1455
+ console.log(`${title('스타일')} ${summary.useStyles ? 'Tailwind 포함' : 'No Style'}`);
1456
+ console.log(`${title('commitlint')} ${summary.withCommitlint ? '같이 포함' : '포함 안 함'}`);
1457
+ }
1458
+ console.log(`${title('docs/개발원칙')} ${summary.includeDocs ? '같이 포함' : '포함 안 함'}`);
1459
+ console.log(`${title('정리 대상 경로')} ${summary.removePaths.length}`);
1460
+ console.log(`${title('template 파일')} ${summary.templateFileCount}`);
1461
+ console.log(`${title('runtime 파일')} ${summary.runtimeFileCount}`);
1462
+ console.log('');
1463
+ }
1464
+
1465
+ async function main() {
1466
+ if (process.argv.length > 2) {
1467
+ throw new Error('이 create 명령은 이제 인자 대신 인터랙션으로만 동작합니다. 그냥 실행해 주세요.');
1468
+ }
1469
+
1470
+ const options = await collectInteractiveOptions();
1471
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
1472
+ const workspaceRoot = path.resolve(scriptDir, '..');
1473
+ const targetDir = path.resolve(process.cwd(), options.targetArg);
1474
+
1475
+ await ensureTargetDir(targetDir);
1476
+ const targetDirIsEmpty = await ensureTargetDirIsEmpty(targetDir);
1477
+
1478
+ if (
1479
+ !options.dryRun &&
1480
+ (options.preset === 'react' || options.preset === 'next') &&
1481
+ !options.fromScaffoldedDir &&
1482
+ !options.autoScaffold
1483
+ ) {
1484
+ throw new Error(
1485
+ `${options.preset} preset은 auto scaffold 또는 scaffolded dir apply 모드에서만 실행할 수 있습니다.`,
1486
+ );
1487
+ }
1488
+
1489
+ if (
1490
+ !options.dryRun &&
1491
+ (options.preset === 'react' || options.preset === 'next') &&
1492
+ options.fromScaffoldedDir &&
1493
+ targetDirIsEmpty
1494
+ ) {
1495
+ throw new Error(
1496
+ 'scaffolded dir apply는 이미 create-vite 또는 create-next-app 결과가 들어 있는 디렉터리에서만 실행할 수 있습니다. 현재 target dir이 비어 있습니다.',
1497
+ );
1498
+ }
1499
+
1500
+ if (
1501
+ !options.dryRun &&
1502
+ (options.preset === 'react' || options.preset === 'next') &&
1503
+ options.autoScaffold &&
1504
+ !targetDirIsEmpty
1505
+ ) {
1506
+ throw new Error(
1507
+ 'auto scaffold 모드는 비어 있는 target dir에서만 실행할 수 있습니다.',
1508
+ );
1509
+ }
1510
+
1511
+ if (!options.dryRun && options.preset === 'monorepo' && !targetDirIsEmpty) {
1512
+ throw new Error(
1513
+ 'monorepo preset은 비어 있는 target dir에서만 바로 생성할 수 있습니다.',
1514
+ );
1515
+ }
1516
+
1517
+ if (!options.dryRun && options.autoScaffold) {
1518
+ if (options.preset === 'react') {
1519
+ scaffoldReactProject(targetDir);
1520
+ await validateReactScaffold(targetDir);
1521
+ }
1522
+
1523
+ if (options.preset === 'next') {
1524
+ scaffoldNextProject(targetDir);
1525
+ await validateNextScaffold(targetDir, {
1526
+ useStyles: options.useStyles,
1527
+ });
1528
+ }
1529
+ }
1530
+
1531
+ const plan = await buildProjectGenerationPlan({
1532
+ workspaceRoot,
1533
+ targetDir,
1534
+ preset: options.preset,
1535
+ });
1536
+
1537
+ const result = await executeProjectGenerationPlan({
1538
+ plan,
1539
+ dryRun: options.dryRun,
1540
+ });
1541
+
1542
+ let dependencySyncResult = {
1543
+ packageManager: null,
1544
+ dependencyCount: 0,
1545
+ devDependencyCount: 0,
1546
+ };
1547
+
1548
+ if (!options.dryRun && options.includeDocs) {
1549
+ await copyDevelopmentPrinciplesDocs(workspaceRoot, targetDir);
1550
+ }
1551
+
1552
+ if (!options.dryRun && (options.preset === 'react' || options.preset === 'next')) {
1553
+ await applyStandaloneStyleMode(options.preset, targetDir, {
1554
+ useStyles: options.useStyles,
1555
+ });
1556
+ }
1557
+
1558
+ if (!options.dryRun && (options.preset === 'react' || options.preset === 'next')) {
1559
+ dependencySyncResult = await syncStandaloneDependencies(options.preset, targetDir, {
1560
+ useStyles: options.useStyles,
1561
+ withCommitlint: options.withCommitlint,
1562
+ });
1563
+ }
1564
+
1565
+ if (
1566
+ !options.dryRun &&
1567
+ (options.preset === 'react' || options.preset === 'next') &&
1568
+ options.withCommitlint &&
1569
+ dependencySyncResult.packageManager
1570
+ ) {
1571
+ await enableCommitlint(targetDir, dependencySyncResult.packageManager);
1572
+ }
1573
+
1574
+ const summary = formatSummary({
1575
+ plan,
1576
+ result,
1577
+ dryRun: options.dryRun,
1578
+ fromScaffoldedDir: options.fromScaffoldedDir,
1579
+ autoScaffold: options.autoScaffold,
1580
+ includeDocs: options.includeDocs,
1581
+ useStyles: options.useStyles,
1582
+ withCommitlint: options.withCommitlint,
1583
+ });
1584
+
1585
+ printSummary(summary);
1586
+
1587
+ if (options.autoScaffold && !options.dryRun) {
1588
+ console.log(subtle('upstream scaffold -> generator apply 흐름까지 완료되었습니다.'));
1589
+ console.log('');
1590
+ }
1591
+
1592
+ if (dependencySyncResult.packageManager) {
1593
+ console.log(
1594
+ subtle(
1595
+ `${dependencySyncResult.packageManager}로 deps ${dependencySyncResult.dependencyCount}개 / devDeps ${dependencySyncResult.devDependencyCount}개 최신 버전 동기화를 마쳤습니다.`,
1596
+ ),
1597
+ );
1598
+ console.log('');
1599
+ }
1600
+
1601
+ if (!options.dryRun && options.includeDocs) {
1602
+ console.log(subtle('docs/개발원칙 문서를 생성물에 함께 복사했습니다.'));
1603
+ console.log('');
1604
+ }
1605
+ }
1606
+
1607
+ main().catch((error) => {
1608
+ console.log('');
1609
+ console.log(danger(error instanceof Error ? error.message : String(error)));
1610
+ console.log('');
1611
+ process.exit(1);
1612
+ });