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,1336 @@
1
+ import { execSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const APP_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6
+ const TEMPLATE_DIRECTORY = path.join(
7
+ process.cwd(),
8
+ "turbo",
9
+ "generators",
10
+ "templates",
11
+ );
12
+ const NEXT_ESLINT_FILES = [
13
+ "eslint.config.js",
14
+ "eslint.config.mjs",
15
+ "eslint.config.ts",
16
+ ];
17
+ const VITE_ESLINT_FILES = [
18
+ "eslint.config.js",
19
+ "eslint.config.mjs",
20
+ "eslint.config.ts",
21
+ ];
22
+ const VERSION_REFERENCE_FILES = [
23
+ path.join(process.cwd(), "apps", "web", "package.json"),
24
+ path.join(process.cwd(), "apps", "project", "package.json"),
25
+ path.join(process.cwd(), "packages", "eslint-config", "package.json"),
26
+ path.join(process.cwd(), "package.json"),
27
+ ];
28
+ const NEXT_SITE_ID_ENV_KEY = "NEXT_PUBLIC_SITE_ID";
29
+ const NEXT_BASE_URL_ENV_KEY = "NEXT_PUBLIC_API_BASE_URL";
30
+ const VITE_SITE_ID_ENV_KEY = "VITE_SITE_ID";
31
+ const VITE_BASE_URL_ENV_KEY = "VITE_API_BASE_URL";
32
+ const KNOWN_NON_PLAIN_FRAMEWORKS = [
33
+ "react",
34
+ "react-dom",
35
+ "vue",
36
+ "svelte",
37
+ "preact",
38
+ "solid-js",
39
+ "lit",
40
+ "@builder.io/qwik",
41
+ ];
42
+
43
+ function readJson(filePath) {
44
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
45
+ }
46
+
47
+ function readTemplate(templatePath) {
48
+ return fs.readFileSync(path.join(TEMPLATE_DIRECTORY, templatePath), "utf-8");
49
+ }
50
+
51
+ function writeJson(filePath, value) {
52
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
54
+ }
55
+
56
+ function writeText(filePath, contents) {
57
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
58
+ fs.writeFileSync(filePath, `${contents.replace(/\n?$/, "\n")}`, "utf-8");
59
+ }
60
+
61
+ function toPosixPath(filePath) {
62
+ return filePath.split(path.sep).join("/");
63
+ }
64
+
65
+ function toRepoRelativePath(filePath) {
66
+ return toPosixPath(path.relative(process.cwd(), filePath));
67
+ }
68
+
69
+ function toAppRelativePath(appDirectory, filePath) {
70
+ return toPosixPath(path.relative(appDirectory, filePath));
71
+ }
72
+
73
+ function serializeEnvValue(value) {
74
+ return JSON.stringify(value ?? "");
75
+ }
76
+
77
+ function renderTemplate(templatePath, replacements = {}) {
78
+ return Object.entries(replacements).reduce(
79
+ (contents, [pattern, value]) => contents.replaceAll(pattern, value),
80
+ readTemplate(templatePath),
81
+ );
82
+ }
83
+
84
+ function sortRecord(record = {}) {
85
+ return Object.fromEntries(
86
+ Object.entries(record).sort(([left], [right]) => left.localeCompare(right)),
87
+ );
88
+ }
89
+
90
+ function getWorkspacePackages({ includeRuntimeExcluded = true } = {}) {
91
+ const packagesPath = path.join(process.cwd(), "packages");
92
+
93
+ if (!fs.existsSync(packagesPath)) {
94
+ return [];
95
+ }
96
+
97
+ return fs
98
+ .readdirSync(packagesPath)
99
+ .filter((dir) => {
100
+ const fullPath = path.join(packagesPath, dir);
101
+
102
+ return (
103
+ fs.statSync(fullPath).isDirectory() &&
104
+ fs.existsSync(path.join(fullPath, "package.json"))
105
+ );
106
+ })
107
+ .map((dir) => {
108
+ const pkgJson = readJson(path.join(packagesPath, dir, "package.json"));
109
+
110
+ return {
111
+ name: pkgJson.name,
112
+ value: pkgJson.name,
113
+ };
114
+ })
115
+ .filter(
116
+ (pkg) =>
117
+ includeRuntimeExcluded ||
118
+ !["@repo/eslint-config", "@repo/typescript-config"].includes(pkg.value),
119
+ )
120
+ .sort((left, right) => left.name.localeCompare(right.name));
121
+ }
122
+
123
+ function getAppDirectory(appName) {
124
+ return path.join(process.cwd(), "apps", appName);
125
+ }
126
+
127
+ function validateAppName(value) {
128
+ const appName = value.trim();
129
+
130
+ if (!appName) {
131
+ return "앱 이름을 입력해주세요.";
132
+ }
133
+
134
+ if (!APP_NAME_PATTERN.test(appName)) {
135
+ return "앱 이름은 소문자, 숫자, 하이픈만 사용할 수 있습니다.";
136
+ }
137
+
138
+ if (fs.existsSync(getAppDirectory(appName))) {
139
+ return `apps/${appName} 가 이미 존재합니다.`;
140
+ }
141
+
142
+ return true;
143
+ }
144
+
145
+ function getSelectedWorkspaceDependencies(answers) {
146
+ const runtimeDeps = Array.from(new Set(answers.workspaceDeps ?? []));
147
+ const runtimeDepSet = new Set(runtimeDeps);
148
+ const devDeps = Array.from(
149
+ new Set(
150
+ (answers.workspaceDevDeps ?? []).filter(
151
+ (dependency) => !runtimeDepSet.has(dependency),
152
+ ),
153
+ ),
154
+ );
155
+
156
+ return {
157
+ runtimeDeps,
158
+ devDeps,
159
+ };
160
+ }
161
+
162
+ function getConfiguredSiteId(answers) {
163
+ return answers.siteId?.trim() || "a";
164
+ }
165
+
166
+ function getConfiguredBaseUrl(answers) {
167
+ return answers.baseUrl?.trim() || "";
168
+ }
169
+
170
+ function createEnvFileContents(entries) {
171
+ return Object.entries(entries)
172
+ .map(([key, value]) => `${key}=${serializeEnvValue(value)}`)
173
+ .join("\n");
174
+ }
175
+
176
+ function writeEnvLocalFile(appName, entries) {
177
+ writeText(
178
+ path.join(getAppDirectory(appName), ".env.local"),
179
+ createEnvFileContents(entries),
180
+ );
181
+ }
182
+
183
+ function getNextEnvConfig(answers) {
184
+ const siteId = getConfiguredSiteId(answers);
185
+ const baseUrl = getConfiguredBaseUrl(answers);
186
+
187
+ return {
188
+ siteId,
189
+ baseUrl,
190
+ siteIdEnvKey: NEXT_SITE_ID_ENV_KEY,
191
+ baseUrlEnvKey: NEXT_BASE_URL_ENV_KEY,
192
+ envEntries: {
193
+ [NEXT_SITE_ID_ENV_KEY]: siteId,
194
+ [NEXT_BASE_URL_ENV_KEY]: baseUrl,
195
+ },
196
+ };
197
+ }
198
+
199
+ function getViteEnvConfig(answers) {
200
+ const siteId = getConfiguredSiteId(answers);
201
+ const baseUrl = getConfiguredBaseUrl(answers);
202
+
203
+ return {
204
+ siteId,
205
+ baseUrl,
206
+ siteIdEnvKey: VITE_SITE_ID_ENV_KEY,
207
+ baseUrlEnvKey: VITE_BASE_URL_ENV_KEY,
208
+ envEntries: {
209
+ [VITE_SITE_ID_ENV_KEY]: siteId,
210
+ [VITE_BASE_URL_ENV_KEY]: baseUrl,
211
+ },
212
+ };
213
+ }
214
+
215
+ function ensureDependencies(pkg, field, dependencyMap) {
216
+ if (Object.keys(dependencyMap).length === 0) {
217
+ return;
218
+ }
219
+
220
+ pkg[field] = pkg[field] || {};
221
+
222
+ Object.entries(dependencyMap).forEach(([dependency, version]) => {
223
+ pkg[field][dependency] = version;
224
+ });
225
+
226
+ pkg[field] = sortRecord(pkg[field]);
227
+ }
228
+
229
+ function updateGeneratedPackageJson(
230
+ appName,
231
+ answers,
232
+ {
233
+ dependencies = {},
234
+ devDependencies = {},
235
+ scripts = {},
236
+ forcePrivate = true,
237
+ } = {},
238
+ ) {
239
+ const packageJsonPath = path.join(getAppDirectory(appName), "package.json");
240
+ const pkg = readJson(packageJsonPath);
241
+ const { runtimeDeps, devDeps } = getSelectedWorkspaceDependencies(answers);
242
+
243
+ pkg.name = appName;
244
+
245
+ if (forcePrivate) {
246
+ pkg.private = true;
247
+ }
248
+
249
+ pkg.scripts = {
250
+ ...(pkg.scripts || {}),
251
+ ...scripts,
252
+ };
253
+
254
+ ensureDependencies(pkg, "dependencies", dependencies);
255
+ ensureDependencies(pkg, "devDependencies", devDependencies);
256
+ ensureDependencies(
257
+ pkg,
258
+ "dependencies",
259
+ Object.fromEntries(
260
+ runtimeDeps.map((dependency) => [dependency, "workspace:*"]),
261
+ ),
262
+ );
263
+ ensureDependencies(
264
+ pkg,
265
+ "devDependencies",
266
+ Object.fromEntries(
267
+ devDeps.map((dependency) => [dependency, "workspace:*"]),
268
+ ),
269
+ );
270
+
271
+ if (pkg.dependencies) {
272
+ pkg.dependencies = sortRecord(pkg.dependencies);
273
+ }
274
+
275
+ if (pkg.devDependencies) {
276
+ pkg.devDependencies = sortRecord(pkg.devDependencies);
277
+ }
278
+
279
+ writeJson(packageJsonPath, pkg);
280
+ }
281
+
282
+ function setJsonLikeExtends(filePath, extendsPath) {
283
+ if (!fs.existsSync(filePath)) {
284
+ return;
285
+ }
286
+
287
+ const current = fs.readFileSync(filePath, "utf-8");
288
+ const next = /"extends"\s*:/.test(current)
289
+ ? current.replace(/"extends"\s*:\s*"[^"]+"/, `"extends": "${extendsPath}"`)
290
+ : current.replace(/\{\s*/, `{\n "extends": "${extendsPath}",\n `);
291
+
292
+ writeText(filePath, next);
293
+ }
294
+
295
+ function getDependencyVersion(packageJsonPath, dependencyName) {
296
+ if (!fs.existsSync(packageJsonPath)) {
297
+ return undefined;
298
+ }
299
+
300
+ const pkg = readJson(packageJsonPath);
301
+
302
+ return (
303
+ pkg.dependencies?.[dependencyName] ||
304
+ pkg.devDependencies?.[dependencyName] ||
305
+ undefined
306
+ );
307
+ }
308
+
309
+ function getReferenceVersion(dependencyName, fallback) {
310
+ for (const filePath of VERSION_REFERENCE_FILES) {
311
+ const version = getDependencyVersion(filePath, dependencyName);
312
+
313
+ if (version) {
314
+ return version;
315
+ }
316
+ }
317
+
318
+ return fallback;
319
+ }
320
+
321
+ function removeFiles(directory, fileNames) {
322
+ fileNames.forEach((fileName) => {
323
+ const filePath = path.join(directory, fileName);
324
+
325
+ if (fs.existsSync(filePath)) {
326
+ fs.rmSync(filePath, { force: true });
327
+ }
328
+ });
329
+ }
330
+
331
+ function removeNestedPnpmWorkspace(appName) {
332
+ const nestedWorkspacePath = path.join(
333
+ getAppDirectory(appName),
334
+ "pnpm-workspace.yaml",
335
+ );
336
+
337
+ if (fs.existsSync(nestedWorkspacePath)) {
338
+ fs.rmSync(nestedWorkspacePath, { force: true });
339
+ }
340
+ }
341
+
342
+ function removeNestedPackageManagerFiles(appName) {
343
+ removeFiles(getAppDirectory(appName), [
344
+ "pnpm-lock.yaml",
345
+ "package-lock.json",
346
+ "yarn.lock",
347
+ "bun.lock",
348
+ "bun.lockb",
349
+ ]);
350
+ }
351
+
352
+ function removeGeneratedAgentDocs(appName) {
353
+ removeFiles(getAppDirectory(appName), ["AGENTS.md", "CLAUDE.md"]);
354
+ }
355
+
356
+ function hasGeneratedAppPackageJson(appName) {
357
+ return fs.existsSync(path.join(getAppDirectory(appName), "package.json"));
358
+ }
359
+
360
+ function cleanupAbortedGeneratedApp(appName) {
361
+ const appDirectory = getAppDirectory(appName);
362
+
363
+ if (!fs.existsSync(appDirectory) || hasGeneratedAppPackageJson(appName)) {
364
+ return;
365
+ }
366
+
367
+ fs.rmSync(appDirectory, {
368
+ recursive: true,
369
+ force: true,
370
+ });
371
+ }
372
+
373
+ function markScaffoldAborted(appName, appLabel, answers) {
374
+ cleanupAbortedGeneratedApp(appName);
375
+ answers.__scaffoldReady = false;
376
+
377
+ console.log(
378
+ `${appName} ${appLabel} 앱 생성이 취소되어 후속 설정을 건너뜁니다.`,
379
+ );
380
+
381
+ return false;
382
+ }
383
+
384
+ function confirmGeneratedAppScaffold(appName, appLabel, answers) {
385
+ if (hasGeneratedAppPackageJson(appName)) {
386
+ answers.__scaffoldReady = true;
387
+ return true;
388
+ }
389
+
390
+ return markScaffoldAborted(appName, appLabel, answers);
391
+ }
392
+
393
+ function installAppDependencies(appName) {
394
+ execSync(`pnpm install --filter "./apps/${appName}"`, {
395
+ cwd: process.cwd(),
396
+ stdio: "inherit",
397
+ });
398
+ }
399
+
400
+ function fixGeneratedAppLint(appName) {
401
+ const appDirectory = getAppDirectory(appName);
402
+ const eslintConfigFiles = new Set([
403
+ ...NEXT_ESLINT_FILES,
404
+ ...VITE_ESLINT_FILES,
405
+ ]);
406
+ const hasEslintConfig = [...eslintConfigFiles].some((fileName) =>
407
+ fs.existsSync(path.join(appDirectory, fileName)),
408
+ );
409
+
410
+ if (!hasEslintConfig) {
411
+ return;
412
+ }
413
+
414
+ execSync(`pnpm --filter "./apps/${appName}" exec eslint . --fix`, {
415
+ cwd: process.cwd(),
416
+ stdio: "inherit",
417
+ });
418
+ }
419
+
420
+ function writeAppReadme(appName, templatePath, replacements) {
421
+ const appDirectory = getAppDirectory(appName);
422
+ const rootReadmePath = path.join(process.cwd(), "README.md");
423
+
424
+ writeText(
425
+ path.join(appDirectory, "README.md"),
426
+ renderTemplate(templatePath, {
427
+ "[[APPNAME]]": appName,
428
+ "[[APP_PATH]]": toRepoRelativePath(appDirectory),
429
+ "[[ROOT_README_PATH]]": toAppRelativePath(appDirectory, rootReadmePath),
430
+ ...replacements,
431
+ }),
432
+ );
433
+ }
434
+
435
+ function resolveNextAppDirectory(appDirectory) {
436
+ const srcAppDirectory = path.join(appDirectory, "src", "app");
437
+ const rootAppDirectory = path.join(appDirectory, "app");
438
+
439
+ if (fs.existsSync(srcAppDirectory)) {
440
+ return srcAppDirectory;
441
+ }
442
+
443
+ if (fs.existsSync(rootAppDirectory)) {
444
+ return rootAppDirectory;
445
+ }
446
+
447
+ fs.mkdirSync(srcAppDirectory, { recursive: true });
448
+ return srcAppDirectory;
449
+ }
450
+
451
+ function resolveNextExtension(appRootDirectory) {
452
+ const candidates = [".tsx", ".jsx", ".js", ".ts"];
453
+
454
+ for (const extension of candidates) {
455
+ if (
456
+ fs.existsSync(path.join(appRootDirectory, `layout${extension}`)) ||
457
+ fs.existsSync(path.join(appRootDirectory, `page${extension}`))
458
+ ) {
459
+ return extension;
460
+ }
461
+ }
462
+
463
+ return ".tsx";
464
+ }
465
+
466
+ function createNextLayoutSource(appName, isTypeScript) {
467
+ if (isTypeScript) {
468
+ return renderTemplate("next-app/app/layout.tsx.tpl", {
469
+ __APPNAME__: appName,
470
+ });
471
+ }
472
+
473
+ return `import "./globals.css";
474
+
475
+ import { CoreBootstrap } from "@repo/core-next/bootstrap";
476
+
477
+ import { Providers } from "./providers";
478
+
479
+ export const metadata = {
480
+ title: "${appName}",
481
+ description: "${appName} application",
482
+ };
483
+
484
+ const SITE_ID = process.env.${NEXT_SITE_ID_ENV_KEY} ?? "a";
485
+ const BASE_URL = process.env.${NEXT_BASE_URL_ENV_KEY} || undefined;
486
+
487
+ export default function RootLayout({ children }) {
488
+ return (
489
+ <html lang="ko">
490
+ <body>
491
+ <CoreBootstrap siteId={SITE_ID} baseUrl={BASE_URL} />
492
+ <Providers>{children}</Providers>
493
+ </body>
494
+ </html>
495
+ );
496
+ }
497
+ `;
498
+ }
499
+
500
+ function createNextProvidersSource(isTypeScript) {
501
+ if (isTypeScript) {
502
+ return readTemplate("next-app/app/providers.tsx.tpl");
503
+ }
504
+
505
+ return `"use client";
506
+
507
+ import {
508
+ AppProviders,
509
+ createClientQueryErrorHandler,
510
+ } from "@repo/core-next";
511
+
512
+ const handleClientQueryError = createClientQueryErrorHandler({
513
+ onUnauthorized: () => {
514
+ window.location.href = "/login";
515
+ },
516
+ });
517
+
518
+ export function Providers({ children }) {
519
+ return (
520
+ <AppProviders onGlobalError={handleClientQueryError}>
521
+ {children}
522
+ </AppProviders>
523
+ );
524
+ }
525
+ `;
526
+ }
527
+
528
+ function createNextGlobalErrorSource(isTypeScript) {
529
+ if (isTypeScript) {
530
+ return readTemplate("next-app/app/global-error.tsx.tpl");
531
+ }
532
+
533
+ return `"use client";
534
+
535
+ export default function GlobalError({ error }) {
536
+ return (
537
+ <html>
538
+ <body>
539
+ <h2>Error</h2>
540
+ <pre>{error.message}</pre>
541
+ </body>
542
+ </html>
543
+ );
544
+ }
545
+ `;
546
+ }
547
+
548
+ function writeNextReadme(appName, appRootDirectory, extension, answers) {
549
+ const envConfig = getNextEnvConfig(answers);
550
+
551
+ writeAppReadme(appName, "next-app/README.md.tpl", {
552
+ "[[LAYOUT_PATH]]": toRepoRelativePath(
553
+ path.join(appRootDirectory, `layout${extension}`),
554
+ ),
555
+ "[[PROVIDERS_PATH]]": toRepoRelativePath(
556
+ path.join(appRootDirectory, `providers${extension}`),
557
+ ),
558
+ "[[PAGE_PATH]]": toRepoRelativePath(
559
+ path.join(appRootDirectory, `page${extension}`),
560
+ ),
561
+ "[[GLOBAL_ERROR_PATH]]": toRepoRelativePath(
562
+ path.join(appRootDirectory, `global-error${extension}`),
563
+ ),
564
+ "[[ENV_FILE_PATH]]": toRepoRelativePath(
565
+ path.join(getAppDirectory(appName), ".env.local"),
566
+ ),
567
+ "[[SITE_ID]]": envConfig.siteId,
568
+ "[[BASE_URL]]": envConfig.baseUrl || "(비워둠)",
569
+ "[[SITE_ID_ENV_KEY]]": envConfig.siteIdEnvKey,
570
+ "[[BASE_URL_ENV_KEY]]": envConfig.baseUrlEnvKey,
571
+ "[[ENV_SNIPPET]]": createEnvFileContents(envConfig.envEntries),
572
+ });
573
+ }
574
+
575
+ function addImportIfMissing(source, statement) {
576
+ if (source.includes(statement)) {
577
+ return source;
578
+ }
579
+
580
+ const importMatches = [...source.matchAll(/^import .*;$/gm)];
581
+
582
+ if (importMatches.length === 0) {
583
+ return `${statement}\n${source}`;
584
+ }
585
+
586
+ const lastImport = importMatches.at(-1);
587
+ const insertIndex = lastImport.index + lastImport[0].length;
588
+
589
+ return `${source.slice(0, insertIndex)}\n${statement}${source.slice(insertIndex)}`;
590
+ }
591
+
592
+ function addDeclarationIfMissing(source, marker, declaration) {
593
+ if (source.includes(declaration)) {
594
+ return source;
595
+ }
596
+
597
+ const markerIndex = source.indexOf(marker);
598
+
599
+ if (markerIndex === -1) {
600
+ return `${source.trimEnd()}\n\n${declaration}\n`;
601
+ }
602
+
603
+ return `${source.slice(0, markerIndex).trimEnd()}\n\n${declaration}\n\n${source.slice(markerIndex)}`;
604
+ }
605
+
606
+ function upsertConstDeclaration(source, name, declaration, marker) {
607
+ const pattern = new RegExp(`const ${name} = [^\\n]+;`);
608
+
609
+ if (pattern.test(source)) {
610
+ return source.replace(pattern, declaration);
611
+ }
612
+
613
+ return addDeclarationIfMissing(source, marker, declaration);
614
+ }
615
+
616
+ function normalizeNextLayoutImports(source) {
617
+ const importMatches = [...source.matchAll(/^import .*;$/gm)];
618
+
619
+ if (importMatches.length === 0) {
620
+ return source;
621
+ }
622
+
623
+ const firstImport = importMatches[0];
624
+ const lastImport = importMatches.at(-1);
625
+ const knownImportOrder = [
626
+ 'import type { Metadata } from "next";',
627
+ 'import { Geist, Geist_Mono } from "next/font/google";',
628
+ 'import "./globals.css";',
629
+ 'import { CoreBootstrap } from "@repo/core-next/bootstrap";',
630
+ 'import { Providers } from "./providers";',
631
+ ];
632
+ const importLines = importMatches.map((match) => match[0]);
633
+ const orderedLines = [
634
+ ...knownImportOrder.filter((statement) => importLines.includes(statement)),
635
+ ...importLines.filter((statement) => !knownImportOrder.includes(statement)),
636
+ ];
637
+ const beforeImports = source.slice(0, firstImport.index);
638
+ const afterImports = source
639
+ .slice(lastImport.index + lastImport[0].length)
640
+ .replace(/^\n+/, "\n\n");
641
+
642
+ return `${beforeImports}${orderedLines.join("\n")}${afterImports}`;
643
+ }
644
+
645
+ function applyNextLayoutBootstrap(source, appName, isTypeScript) {
646
+ let nextSource =
647
+ source.trim().length > 0
648
+ ? source
649
+ : createNextLayoutSource(appName, isTypeScript);
650
+
651
+ nextSource = addImportIfMissing(
652
+ nextSource,
653
+ 'import { CoreBootstrap } from "@repo/core-next/bootstrap";',
654
+ );
655
+ nextSource = addImportIfMissing(
656
+ nextSource,
657
+ 'import { Providers } from "./providers";',
658
+ );
659
+ nextSource = upsertConstDeclaration(
660
+ nextSource,
661
+ "SITE_ID",
662
+ `const SITE_ID = process.env.${NEXT_SITE_ID_ENV_KEY} ?? "a";`,
663
+ "export default function",
664
+ );
665
+ nextSource = upsertConstDeclaration(
666
+ nextSource,
667
+ "BASE_URL",
668
+ `const BASE_URL = process.env.${NEXT_BASE_URL_ENV_KEY} || undefined;`,
669
+ "export default function",
670
+ );
671
+
672
+ if (!nextSource.includes("<CoreBootstrap")) {
673
+ nextSource = nextSource.replace(
674
+ /(<body[^>]*>)/,
675
+ `$1\n <CoreBootstrap siteId={SITE_ID} />`,
676
+ );
677
+ }
678
+
679
+ nextSource = nextSource.replace(
680
+ /<CoreBootstrap([^>]*?)siteId=\{SITE_ID\}(?![^>]*baseUrl=)([^>]*?)\/>/,
681
+ `<CoreBootstrap$1siteId={SITE_ID} baseUrl={BASE_URL}$2 />`,
682
+ );
683
+
684
+ if (!nextSource.includes("<Providers>")) {
685
+ if (
686
+ /<body([^>]*)>\s*(?:<CoreBootstrap[^>]*siteId=\{SITE_ID\}[^>]*\/>\s*)?\{children\}\s*<\/body>/.test(
687
+ nextSource,
688
+ )
689
+ ) {
690
+ nextSource = nextSource.replace(
691
+ /<body([^>]*)>\s*(?:<CoreBootstrap[^>]*siteId=\{SITE_ID\}[^>]*\/>\s*)?\{children\}\s*<\/body>/,
692
+ `<body$1>\n <CoreBootstrap siteId={SITE_ID} baseUrl={BASE_URL} />\n <Providers>{children}</Providers>\n </body>`,
693
+ );
694
+ } else if (/^(\s*)\{children\}$/m.test(nextSource)) {
695
+ nextSource = nextSource.replace(
696
+ /^(\s*)\{children\}$/m,
697
+ (_match, indent) =>
698
+ `${indent}<Providers>\n${indent} {children}\n${indent}</Providers>`,
699
+ );
700
+ } else if (nextSource.includes("{children}")) {
701
+ nextSource = nextSource.replace(
702
+ "{children}",
703
+ "<Providers>{children}</Providers>",
704
+ );
705
+ } else {
706
+ nextSource = nextSource.replace(
707
+ /(<body[^>]*>[\s\S]*?<CoreBootstrap[^>]*siteId=\{SITE_ID\}[^>]*\/>\s*)([\s\S]*?)(\s*<\/body>)/,
708
+ (_match, prefix, bodyContent, suffix) => {
709
+ const trimmedBodyContent = bodyContent.trim();
710
+
711
+ if (!trimmedBodyContent) {
712
+ return `${prefix}\n <Providers />${suffix}`;
713
+ }
714
+
715
+ return `${prefix}\n <Providers>\n${trimmedBodyContent}\n </Providers>${suffix}`;
716
+ },
717
+ );
718
+ }
719
+ }
720
+
721
+ nextSource = normalizeNextLayoutImports(nextSource);
722
+
723
+ return `${nextSource.trimEnd()}\n`;
724
+ }
725
+
726
+ function injectInitCoreIntoEntry(filePath) {
727
+ const importStatement = 'import { initCore } from "@repo/core";';
728
+ let source = readTextIfExists(filePath);
729
+
730
+ if (!source || source.includes("initCore(")) {
731
+ return;
732
+ }
733
+
734
+ if (!source.includes(importStatement)) {
735
+ source = `${importStatement}\n${source}`;
736
+ }
737
+
738
+ const importLines = [...source.matchAll(/^import .*;$/gm)];
739
+ const insertIndex =
740
+ importLines.length > 0
741
+ ? importLines.at(-1).index + importLines.at(-1)[0].length
742
+ : 0;
743
+
744
+ source = `${source.slice(0, insertIndex)}\n\nconst SITE_ID = import.meta.env.${VITE_SITE_ID_ENV_KEY} || "a";\nconst BASE_URL = import.meta.env.${VITE_BASE_URL_ENV_KEY} || undefined;\n\ninitCore(SITE_ID, BASE_URL);${source.slice(insertIndex)}`;
745
+
746
+ writeText(filePath, source);
747
+ }
748
+
749
+ function applyNextDefaults(appName, answers) {
750
+ const appDirectory = getAppDirectory(appName);
751
+ const appRootDirectory = resolveNextAppDirectory(appDirectory);
752
+ const extension = resolveNextExtension(appRootDirectory);
753
+ const isTypeScript = extension === ".tsx" || extension === ".ts";
754
+ const layoutPath = path.join(appRootDirectory, `layout${extension}`);
755
+ const providersPath = path.join(appRootDirectory, `providers${extension}`);
756
+ const globalErrorPath = path.join(
757
+ appRootDirectory,
758
+ `global-error${extension}`,
759
+ );
760
+
761
+ updateGeneratedPackageJson(appName, answers, {
762
+ dependencies: {
763
+ "@repo/core": "workspace:*",
764
+ "@repo/core-next": "workspace:*",
765
+ },
766
+ devDependencies: {
767
+ "@repo/eslint-config": "workspace:*",
768
+ eslint: getReferenceVersion("eslint", "^9.39.1"),
769
+ ...(isTypeScript ? { "@repo/typescript-config": "workspace:*" } : {}),
770
+ ...(isTypeScript
771
+ ? {
772
+ typescript: getReferenceVersion("typescript", "5.9.2"),
773
+ }
774
+ : {}),
775
+ },
776
+ scripts: {
777
+ lint: "eslint --max-warnings 0",
778
+ ...(isTypeScript
779
+ ? { "check-types": "next typegen && tsc --noEmit" }
780
+ : {}),
781
+ },
782
+ });
783
+ writeEnvLocalFile(appName, getNextEnvConfig(answers).envEntries);
784
+
785
+ if (isTypeScript) {
786
+ setJsonLikeExtends(
787
+ path.join(appDirectory, "tsconfig.json"),
788
+ "@repo/typescript-config/nextjs.json",
789
+ );
790
+ }
791
+
792
+ removeFiles(appDirectory, NEXT_ESLINT_FILES);
793
+ writeText(
794
+ path.join(appDirectory, "eslint.config.mjs"),
795
+ renderTemplate("next-app/eslint.config.js.tpl"),
796
+ );
797
+ writeText(
798
+ layoutPath,
799
+ applyNextLayoutBootstrap(
800
+ readTextIfExists(layoutPath),
801
+ appName,
802
+ isTypeScript,
803
+ ),
804
+ );
805
+ writeText(providersPath, createNextProvidersSource(isTypeScript));
806
+
807
+ if (!fs.existsSync(globalErrorPath)) {
808
+ writeText(globalErrorPath, createNextGlobalErrorSource(isTypeScript));
809
+ }
810
+
811
+ writeNextReadme(appName, appRootDirectory, extension, answers);
812
+ }
813
+
814
+ function readTextIfExists(filePath) {
815
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
816
+ }
817
+
818
+ function resolveViteMainEntry(appDirectory) {
819
+ const candidates = [
820
+ path.join(appDirectory, "src", "main.tsx"),
821
+ path.join(appDirectory, "src", "main.jsx"),
822
+ path.join(appDirectory, "src", "main.ts"),
823
+ path.join(appDirectory, "src", "main.js"),
824
+ path.join(appDirectory, "index.ts"),
825
+ path.join(appDirectory, "index.js"),
826
+ ];
827
+
828
+ return candidates.find((filePath) => fs.existsSync(filePath));
829
+ }
830
+
831
+ function isReactViteApp(pkg, mainEntryPath) {
832
+ return (
833
+ Boolean(pkg.dependencies?.react && pkg.dependencies?.["react-dom"]) ||
834
+ mainEntryPath.endsWith(".tsx") ||
835
+ mainEntryPath.endsWith(".jsx")
836
+ );
837
+ }
838
+
839
+ function isPlainViteApp(pkg) {
840
+ const dependencyNames = new Set([
841
+ ...Object.keys(pkg.dependencies ?? {}),
842
+ ...Object.keys(pkg.devDependencies ?? {}),
843
+ ]);
844
+
845
+ return !KNOWN_NON_PLAIN_FRAMEWORKS.some((dependency) =>
846
+ dependencyNames.has(dependency),
847
+ );
848
+ }
849
+
850
+ function extractCssImport(source) {
851
+ const match = source.match(/import\s+["'](.+?\.css)["'];?/);
852
+
853
+ return match?.[1] ?? null;
854
+ }
855
+
856
+ function resolveAppComponentImport(mainEntryPath) {
857
+ const directory = path.dirname(mainEntryPath);
858
+ const candidates = ["App.tsx", "App.jsx", "App.ts", "App.js"];
859
+
860
+ for (const fileName of candidates) {
861
+ if (fs.existsSync(path.join(directory, fileName))) {
862
+ return `./${fileName}`;
863
+ }
864
+ }
865
+
866
+ return null;
867
+ }
868
+
869
+ function createReactProvidersSource(isTypeScript) {
870
+ if (isTypeScript) {
871
+ return `"use client";
872
+
873
+ import type { ReactNode } from "react";
874
+
875
+ import {
876
+ AppProviders,
877
+ createClientQueryErrorHandler,
878
+ } from "@repo/core-react";
879
+
880
+ const handleClientQueryError = createClientQueryErrorHandler({
881
+ onUnauthorized: () => {
882
+ window.location.href = "/login";
883
+ },
884
+ });
885
+
886
+ type ProvidersProps = {
887
+ children: ReactNode;
888
+ };
889
+
890
+ export function Providers({ children }: ProvidersProps) {
891
+ return (
892
+ <AppProviders onGlobalError={handleClientQueryError}>
893
+ {children}
894
+ </AppProviders>
895
+ );
896
+ }
897
+ `;
898
+ }
899
+
900
+ return `"use client";
901
+
902
+ import {
903
+ AppProviders,
904
+ createClientQueryErrorHandler,
905
+ } from "@repo/core-react";
906
+
907
+ const handleClientQueryError = createClientQueryErrorHandler({
908
+ onUnauthorized: () => {
909
+ window.location.href = "/login";
910
+ },
911
+ });
912
+
913
+ export function Providers({ children }) {
914
+ return (
915
+ <AppProviders onGlobalError={handleClientQueryError}>
916
+ {children}
917
+ </AppProviders>
918
+ );
919
+ }
920
+ `;
921
+ }
922
+
923
+ function createReactMainSource({
924
+ cssImportPath,
925
+ appImportPath,
926
+ providersImportPath,
927
+ }) {
928
+ const cssImport = cssImportPath ? `import "${cssImportPath}";\n` : "";
929
+
930
+ return `import { initCore } from "@repo/core";
931
+ import { StrictMode } from "react";
932
+ import { createRoot } from "react-dom/client";
933
+ ${cssImport}import App from "${appImportPath}";
934
+ import { Providers } from "${providersImportPath}";
935
+
936
+ const SITE_ID = import.meta.env.${VITE_SITE_ID_ENV_KEY} || "a";
937
+ const BASE_URL = import.meta.env.${VITE_BASE_URL_ENV_KEY} || undefined;
938
+
939
+ initCore(SITE_ID, BASE_URL);
940
+
941
+ const rootElement = document.getElementById("root");
942
+
943
+ if (!rootElement) {
944
+ throw new Error("Root element not found.");
945
+ }
946
+
947
+ createRoot(rootElement).render(
948
+ <StrictMode>
949
+ <Providers>
950
+ <App />
951
+ </Providers>
952
+ </StrictMode>,
953
+ );
954
+ `;
955
+ }
956
+
957
+ function createViteReactEslintSource() {
958
+ return `import { config as reactConfig } from "@repo/eslint-config/react-internal";
959
+
960
+ /** @type {import("eslint").Linter.Config[]} */
961
+ export default reactConfig;
962
+ `;
963
+ }
964
+
965
+ function createViteBrowserEslintSource() {
966
+ return `import { defineConfig, globalIgnores } from "eslint/config";
967
+ import globals from "globals";
968
+
969
+ import { config as baseConfig } from "@repo/eslint-config/base";
970
+
971
+ export default defineConfig([
972
+ globalIgnores(["dist"]),
973
+ ...baseConfig,
974
+ {
975
+ languageOptions: {
976
+ globals: {
977
+ ...globals.browser,
978
+ },
979
+ },
980
+ },
981
+ ]);
982
+ `;
983
+ }
984
+
985
+ function writeViteReactReadme(appName, mainEntryPath, providersPath, answers) {
986
+ const envConfig = getViteEnvConfig(answers);
987
+
988
+ writeAppReadme(appName, "vite-app/README.react.md.tpl", {
989
+ "[[ENTRY_PATH]]": toRepoRelativePath(mainEntryPath),
990
+ "[[PROVIDERS_PATH]]": toRepoRelativePath(providersPath),
991
+ "[[ENV_FILE_PATH]]": toRepoRelativePath(
992
+ path.join(getAppDirectory(appName), ".env.local"),
993
+ ),
994
+ "[[SITE_ID]]": envConfig.siteId,
995
+ "[[BASE_URL]]": envConfig.baseUrl || "(비워둠)",
996
+ "[[SITE_ID_ENV_KEY]]": envConfig.siteIdEnvKey,
997
+ "[[BASE_URL_ENV_KEY]]": envConfig.baseUrlEnvKey,
998
+ "[[ENV_SNIPPET]]": createEnvFileContents(envConfig.envEntries),
999
+ });
1000
+ }
1001
+
1002
+ function writeVitePlainReadme(appName, mainEntryPath, answers) {
1003
+ const envConfig = getViteEnvConfig(answers);
1004
+
1005
+ writeAppReadme(appName, "vite-app/README.plain.md.tpl", {
1006
+ "[[ENTRY_PATH]]": toRepoRelativePath(mainEntryPath),
1007
+ "[[ENV_FILE_PATH]]": toRepoRelativePath(
1008
+ path.join(getAppDirectory(appName), ".env.local"),
1009
+ ),
1010
+ "[[SITE_ID]]": envConfig.siteId,
1011
+ "[[BASE_URL]]": envConfig.baseUrl || "(비워둠)",
1012
+ "[[SITE_ID_ENV_KEY]]": envConfig.siteIdEnvKey,
1013
+ "[[BASE_URL_ENV_KEY]]": envConfig.baseUrlEnvKey,
1014
+ "[[ENV_SNIPPET]]": createEnvFileContents(envConfig.envEntries),
1015
+ });
1016
+ }
1017
+
1018
+ function applyViteReactDefaults(appName, answers, mainEntryPath) {
1019
+ const appDirectory = getAppDirectory(appName);
1020
+ const envConfig = getViteEnvConfig(answers);
1021
+ const isTypeScript = mainEntryPath.endsWith(".tsx");
1022
+ const appImportPath = resolveAppComponentImport(mainEntryPath) ?? "./App";
1023
+ const providersExtension = isTypeScript ? ".tsx" : ".jsx";
1024
+ const providersImportPath = `./providers${providersExtension}`;
1025
+ const providersPath = path.join(
1026
+ path.dirname(mainEntryPath),
1027
+ `providers${providersExtension}`,
1028
+ );
1029
+ const cssImportPath = extractCssImport(readTextIfExists(mainEntryPath));
1030
+ const typecheckScript = fs.existsSync(
1031
+ path.join(appDirectory, "tsconfig.app.json"),
1032
+ )
1033
+ ? "tsc -b"
1034
+ : "tsc --noEmit";
1035
+
1036
+ updateGeneratedPackageJson(appName, answers, {
1037
+ dependencies: {
1038
+ "@repo/core": "workspace:*",
1039
+ "@repo/core-react": "workspace:*",
1040
+ },
1041
+ devDependencies: {
1042
+ "@repo/eslint-config": "workspace:*",
1043
+ eslint: getReferenceVersion("eslint", "^9.39.1"),
1044
+ ...(isTypeScript ? { "@repo/typescript-config": "workspace:*" } : {}),
1045
+ ...(isTypeScript
1046
+ ? {
1047
+ typescript: getReferenceVersion("typescript", "5.9.2"),
1048
+ }
1049
+ : {}),
1050
+ },
1051
+ scripts: {
1052
+ lint: "eslint . --max-warnings 0",
1053
+ ...(isTypeScript ? { "check-types": typecheckScript } : {}),
1054
+ },
1055
+ });
1056
+ writeEnvLocalFile(appName, envConfig.envEntries);
1057
+
1058
+ if (isTypeScript) {
1059
+ if (fs.existsSync(path.join(appDirectory, "tsconfig.app.json"))) {
1060
+ setJsonLikeExtends(
1061
+ path.join(appDirectory, "tsconfig.app.json"),
1062
+ "@repo/typescript-config/base.json",
1063
+ );
1064
+ } else if (fs.existsSync(path.join(appDirectory, "tsconfig.json"))) {
1065
+ setJsonLikeExtends(
1066
+ path.join(appDirectory, "tsconfig.json"),
1067
+ "@repo/typescript-config/base.json",
1068
+ );
1069
+ }
1070
+ }
1071
+
1072
+ removeFiles(appDirectory, VITE_ESLINT_FILES);
1073
+ writeText(
1074
+ path.join(appDirectory, "eslint.config.js"),
1075
+ createViteReactEslintSource(),
1076
+ );
1077
+ writeText(providersPath, createReactProvidersSource(isTypeScript));
1078
+ writeText(
1079
+ mainEntryPath,
1080
+ createReactMainSource({
1081
+ cssImportPath,
1082
+ appImportPath,
1083
+ providersImportPath,
1084
+ }),
1085
+ );
1086
+ writeViteReactReadme(appName, mainEntryPath, providersPath, answers);
1087
+ }
1088
+
1089
+ function applyVitePlainDefaults(appName, answers, mainEntryPath) {
1090
+ const appDirectory = getAppDirectory(appName);
1091
+ const isTypeScript = mainEntryPath.endsWith(".ts");
1092
+
1093
+ updateGeneratedPackageJson(appName, answers, {
1094
+ dependencies: {
1095
+ "@repo/core": "workspace:*",
1096
+ },
1097
+ devDependencies: {
1098
+ "@repo/eslint-config": "workspace:*",
1099
+ eslint: getReferenceVersion("eslint", "^9.39.1"),
1100
+ globals: getReferenceVersion("globals", "^16.5.0"),
1101
+ ...(isTypeScript ? { "@repo/typescript-config": "workspace:*" } : {}),
1102
+ ...(isTypeScript
1103
+ ? {
1104
+ typescript: getReferenceVersion("typescript", "5.9.2"),
1105
+ }
1106
+ : {}),
1107
+ },
1108
+ scripts: {
1109
+ lint: "eslint . --max-warnings 0",
1110
+ ...(isTypeScript ? { "check-types": "tsc --noEmit" } : {}),
1111
+ },
1112
+ });
1113
+ writeEnvLocalFile(appName, getViteEnvConfig(answers).envEntries);
1114
+
1115
+ if (isTypeScript && fs.existsSync(path.join(appDirectory, "tsconfig.json"))) {
1116
+ setJsonLikeExtends(
1117
+ path.join(appDirectory, "tsconfig.json"),
1118
+ "@repo/typescript-config/base.json",
1119
+ );
1120
+ }
1121
+
1122
+ removeFiles(appDirectory, VITE_ESLINT_FILES);
1123
+ writeText(
1124
+ path.join(appDirectory, "eslint.config.mjs"),
1125
+ createViteBrowserEslintSource(),
1126
+ );
1127
+ injectInitCoreIntoEntry(mainEntryPath);
1128
+ writeVitePlainReadme(appName, mainEntryPath, answers);
1129
+ }
1130
+
1131
+ function applyViteDefaults(appName, answers) {
1132
+ const appDirectory = getAppDirectory(appName);
1133
+ const packageJsonPath = path.join(appDirectory, "package.json");
1134
+ const pkg = readJson(packageJsonPath);
1135
+ const mainEntryPath = resolveViteMainEntry(appDirectory);
1136
+
1137
+ if (!mainEntryPath) {
1138
+ updateGeneratedPackageJson(appName, answers);
1139
+ return;
1140
+ }
1141
+
1142
+ if (isReactViteApp(pkg, mainEntryPath)) {
1143
+ applyViteReactDefaults(appName, answers, mainEntryPath);
1144
+ return;
1145
+ }
1146
+
1147
+ if (isPlainViteApp(pkg)) {
1148
+ applyVitePlainDefaults(appName, answers, mainEntryPath);
1149
+ return;
1150
+ }
1151
+
1152
+ updateGeneratedPackageJson(appName, answers);
1153
+ }
1154
+
1155
+ function runCreateNextApp(appName) {
1156
+ const appPath = path.join("apps", appName);
1157
+ const command = `npm create next-app@latest ${appPath}`;
1158
+
1159
+ execSync(command, {
1160
+ cwd: process.cwd(),
1161
+ stdio: "inherit",
1162
+ });
1163
+ }
1164
+
1165
+ function runCreateViteApp(appName) {
1166
+ const appPath = path.join("apps", appName);
1167
+ const command = `npm create vite@latest ${appPath}`;
1168
+
1169
+ execSync(command, {
1170
+ cwd: process.cwd(),
1171
+ stdio: "inherit",
1172
+ });
1173
+ }
1174
+
1175
+ function getAppPrompts(appLabel) {
1176
+ return [
1177
+ {
1178
+ type: "input",
1179
+ name: "appName",
1180
+ message: `${appLabel} 앱 이름을 입력하세요:`,
1181
+ filter: (value) => value.trim(),
1182
+ validate: validateAppName,
1183
+ },
1184
+ {
1185
+ type: "input",
1186
+ name: "siteId",
1187
+ message: "siteId를 입력하세요:",
1188
+ default: "a",
1189
+ filter: (value) => value.trim(),
1190
+ validate: (value) => {
1191
+ if (!value.trim()) {
1192
+ return "siteId는 비워둘 수 없습니다.";
1193
+ }
1194
+
1195
+ return true;
1196
+ },
1197
+ },
1198
+ {
1199
+ type: "input",
1200
+ name: "baseUrl",
1201
+ message:
1202
+ "API base URL을 입력하세요. 비워두면 나중에 .env.local에서 직접 설정합니다:",
1203
+ filter: (value) => value.trim(),
1204
+ validate: (value) => {
1205
+ const trimmedValue = value.trim();
1206
+
1207
+ if (!trimmedValue) {
1208
+ return true;
1209
+ }
1210
+
1211
+ if (
1212
+ trimmedValue.startsWith("http://") ||
1213
+ trimmedValue.startsWith("https://")
1214
+ ) {
1215
+ return true;
1216
+ }
1217
+
1218
+ return "base URL은 비우거나 http(s) 절대 URL로 입력해주세요.";
1219
+ },
1220
+ },
1221
+ {
1222
+ type: "checkbox",
1223
+ name: "workspaceDeps",
1224
+ message: "런타임 의존성(dependencies)에 추가할 패키지:",
1225
+ choices: getWorkspacePackages({ includeRuntimeExcluded: false }),
1226
+ },
1227
+ {
1228
+ type: "checkbox",
1229
+ name: "workspaceDevDeps",
1230
+ message: "개발 의존성(devDependencies)에 추가할 패키지:",
1231
+ choices: getWorkspacePackages(),
1232
+ },
1233
+ ];
1234
+ }
1235
+
1236
+ export default function generator(plop) {
1237
+ plop.setGenerator("create-next", {
1238
+ description: "현재 워크스페이스에 Next.js 앱을 생성합니다.",
1239
+ prompts: getAppPrompts("Next.js"),
1240
+ actions: (data) => {
1241
+ const actions = [];
1242
+
1243
+ if (!data) {
1244
+ return actions;
1245
+ }
1246
+
1247
+ actions.push((answers) => {
1248
+ console.log(
1249
+ `${answers.appName} Next.js 앱을 인터랙티브하게 생성하는 중...`,
1250
+ );
1251
+ try {
1252
+ runCreateNextApp(answers.appName);
1253
+ } catch (error) {
1254
+ if (!hasGeneratedAppPackageJson(answers.appName)) {
1255
+ markScaffoldAborted(answers.appName, "Next.js", answers);
1256
+ return `${answers.appName} Next.js 앱 생성이 취소되었습니다.`;
1257
+ }
1258
+
1259
+ throw error;
1260
+ }
1261
+
1262
+ if (!confirmGeneratedAppScaffold(answers.appName, "Next.js", answers)) {
1263
+ return `${answers.appName} Next.js 앱 생성이 취소되었습니다.`;
1264
+ }
1265
+
1266
+ return `${answers.appName} Next.js 앱 기본 생성이 완료되었습니다.`;
1267
+ });
1268
+
1269
+ actions.push((answers) => {
1270
+ if (!answers.__scaffoldReady) {
1271
+ return `${answers.appName} Next.js 앱 후속 설정을 건너뜁니다.`;
1272
+ }
1273
+
1274
+ applyNextDefaults(answers.appName, answers);
1275
+ removeNestedPnpmWorkspace(answers.appName);
1276
+ removeNestedPackageManagerFiles(answers.appName);
1277
+ removeGeneratedAgentDocs(answers.appName);
1278
+ installAppDependencies(answers.appName);
1279
+ fixGeneratedAppLint(answers.appName);
1280
+
1281
+ return `${answers.appName} Next.js 앱 기본 설정이 반영되었습니다.`;
1282
+ });
1283
+
1284
+ return actions;
1285
+ },
1286
+ });
1287
+
1288
+ plop.setGenerator("create-vite", {
1289
+ description: "현재 워크스페이스에 Vite 앱을 생성합니다.",
1290
+ prompts: getAppPrompts("Vite"),
1291
+ actions: (data) => {
1292
+ const actions = [];
1293
+
1294
+ if (!data) {
1295
+ return actions;
1296
+ }
1297
+
1298
+ actions.push((answers) => {
1299
+ console.log(
1300
+ `${answers.appName} Vite 앱을 인터랙티브하게 생성하는 중...`,
1301
+ );
1302
+ try {
1303
+ runCreateViteApp(answers.appName);
1304
+ } catch (error) {
1305
+ if (!hasGeneratedAppPackageJson(answers.appName)) {
1306
+ markScaffoldAborted(answers.appName, "Vite", answers);
1307
+ return `${answers.appName} Vite 앱 생성이 취소되었습니다.`;
1308
+ }
1309
+
1310
+ throw error;
1311
+ }
1312
+
1313
+ if (!confirmGeneratedAppScaffold(answers.appName, "Vite", answers)) {
1314
+ return `${answers.appName} Vite 앱 생성이 취소되었습니다.`;
1315
+ }
1316
+
1317
+ return `${answers.appName} Vite 앱 기본 생성이 완료되었습니다.`;
1318
+ });
1319
+
1320
+ actions.push((answers) => {
1321
+ if (!answers.__scaffoldReady) {
1322
+ return `${answers.appName} Vite 앱 후속 설정을 건너뜁니다.`;
1323
+ }
1324
+
1325
+ applyViteDefaults(answers.appName, answers);
1326
+ removeNestedPackageManagerFiles(answers.appName);
1327
+ installAppDependencies(answers.appName);
1328
+ fixGeneratedAppLint(answers.appName);
1329
+
1330
+ return `${answers.appName} Vite 앱 기본 설정이 반영되었습니다.`;
1331
+ });
1332
+
1333
+ return actions;
1334
+ },
1335
+ });
1336
+ }