sonamu 0.10.13 → 0.11.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 (230) hide show
  1. package/dist/ai/agents/agent.d.ts.map +1 -1
  2. package/dist/ai/providers/rtzr/error.d.ts.map +1 -1
  3. package/dist/ai/providers/rtzr/model.d.ts.map +1 -1
  4. package/dist/ai/providers/rtzr/utils.d.ts.map +1 -1
  5. package/dist/api/base-frame.d.ts.map +1 -1
  6. package/dist/api/decorators.d.ts.map +1 -1
  7. package/dist/api/sonamu.d.ts.map +1 -1
  8. package/dist/api/sonamu.js +3 -2
  9. package/dist/api/validator.d.ts.map +1 -1
  10. package/dist/auth/audit-log/events.d.ts.map +1 -1
  11. package/dist/auth/index.d.ts +1 -1
  12. package/dist/auth/index.d.ts.map +1 -1
  13. package/dist/auth/index.js +3 -3
  14. package/dist/auth/knex-adapter.d.ts.map +1 -1
  15. package/dist/auth/plugins/wrappers/admin.d.ts +4 -4
  16. package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
  17. package/dist/auth/plugins/wrappers/anonymous.d.ts.map +1 -1
  18. package/dist/auth/plugins/wrappers/api-key.d.ts.map +1 -1
  19. package/dist/auth/plugins/wrappers/jwt.d.ts.map +1 -1
  20. package/dist/auth/plugins/wrappers/organization.d.ts.map +1 -1
  21. package/dist/auth/plugins/wrappers/passkey.d.ts.map +1 -1
  22. package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
  23. package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -1
  24. package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
  25. package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
  26. package/dist/bin/build-config.d.ts +10 -0
  27. package/dist/bin/build-config.d.ts.map +1 -1
  28. package/dist/bin/build-config.js +130 -6
  29. package/dist/bin/fixture.d.ts +7 -4
  30. package/dist/bin/fixture.d.ts.map +1 -1
  31. package/dist/bin/fixture.js +161 -13
  32. package/dist/bin/test-command.d.ts +7 -0
  33. package/dist/bin/test-command.d.ts.map +1 -1
  34. package/dist/bin/test-command.js +18 -4
  35. package/dist/bin/typecheck-web.d.ts +2 -0
  36. package/dist/bin/typecheck-web.d.ts.map +1 -0
  37. package/dist/bin/typecheck-web.js +36 -0
  38. package/dist/cache/decorator.d.ts.map +1 -1
  39. package/dist/cache/drivers.d.ts.map +1 -1
  40. package/dist/cache-control/cache-control.d.ts.map +1 -1
  41. package/dist/compress/compress.d.ts.map +1 -1
  42. package/dist/cone/cone-generator.js +1 -1
  43. package/dist/database/base-model.d.ts.map +1 -1
  44. package/dist/database/base-model.types.d.ts +0 -1
  45. package/dist/database/base-model.types.d.ts.map +1 -1
  46. package/dist/database/db.d.ts.map +1 -1
  47. package/dist/database/puri-subset.types.d.ts +0 -1
  48. package/dist/database/puri-subset.types.d.ts.map +1 -1
  49. package/dist/database/puri-wrapper.d.ts +0 -1
  50. package/dist/database/puri-wrapper.d.ts.map +1 -1
  51. package/dist/database/puri.d.ts +0 -1
  52. package/dist/database/puri.d.ts.map +1 -1
  53. package/dist/database/puri.types.d.ts +0 -1
  54. package/dist/database/puri.types.d.ts.map +1 -1
  55. package/dist/database/transaction-context.d.ts.map +1 -1
  56. package/dist/database/upsert-builder.d.ts.map +1 -1
  57. package/dist/dict/en.d.ts +58 -58
  58. package/dist/dict/en.d.ts.map +1 -1
  59. package/dist/dict/ko.d.ts +58 -58
  60. package/dist/dict/ko.d.ts.map +1 -1
  61. package/dist/dict/rc-keys.d.ts.map +1 -1
  62. package/dist/dict/sonamu-dictionary.d.ts.map +1 -1
  63. package/dist/dict/sonamu-dictionary.js +2 -2
  64. package/dist/dict/utils.d.ts.map +1 -1
  65. package/dist/entity/entity-manager.d.ts +115 -113
  66. package/dist/entity/entity-manager.d.ts.map +1 -1
  67. package/dist/entity/entity-manager.js +25 -19
  68. package/dist/entity/entity-template-cone.d.ts +4 -4
  69. package/dist/entity/entity.d.ts +6 -1
  70. package/dist/entity/entity.d.ts.map +1 -1
  71. package/dist/entity/entity.js +8 -3
  72. package/dist/env.d.ts.map +1 -1
  73. package/dist/exceptions/so-exceptions.d.ts.map +1 -1
  74. package/dist/filter/types.d.ts.map +1 -1
  75. package/dist/index.js +6 -6
  76. package/dist/logger/configure.d.ts +10 -1
  77. package/dist/logger/configure.d.ts.map +1 -1
  78. package/dist/logger/configure.js +30 -5
  79. package/dist/migration/migrator.d.ts +12 -1
  80. package/dist/migration/migrator.d.ts.map +1 -1
  81. package/dist/migration/migrator.js +77 -30
  82. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  83. package/dist/migration/slack-confirm.d.ts.map +1 -1
  84. package/dist/naite/messaging-types.d.ts +0 -1
  85. package/dist/naite/messaging-types.d.ts.map +1 -1
  86. package/dist/naite/naite-reporter.d.ts +0 -1
  87. package/dist/naite/naite-reporter.d.ts.map +1 -1
  88. package/dist/naite/naite.d.ts.map +1 -1
  89. package/dist/storage/base-file.d.ts.map +1 -1
  90. package/dist/storage/buffered-file.d.ts.map +1 -1
  91. package/dist/storage/drivers.d.ts.map +1 -1
  92. package/dist/storage/s3-driver.d.ts.map +1 -1
  93. package/dist/storage/storage-manager.d.ts.map +1 -1
  94. package/dist/storage/uploaded-file.d.ts.map +1 -1
  95. package/dist/stream/ws-audience-resolver.d.ts.map +1 -1
  96. package/dist/stream/ws-audience.d.ts.map +1 -1
  97. package/dist/stream/ws-cluster-bus.d.ts.map +1 -1
  98. package/dist/stream/ws-delivery.d.ts.map +1 -1
  99. package/dist/stream/ws-local-connection-store.d.ts.map +1 -1
  100. package/dist/stream/ws-presence-store.d.ts.map +1 -1
  101. package/dist/stream/ws-registry.d.ts.map +1 -1
  102. package/dist/stream/ws-telemetry-memory.d.ts.map +1 -1
  103. package/dist/stream/ws-telemetry.d.ts.map +1 -1
  104. package/dist/stream/ws.d.ts.map +1 -1
  105. package/dist/syncer/api-parser.js +2 -2
  106. package/dist/syncer/file-patterns.d.ts +17 -17
  107. package/dist/syncer/filesystem-dependencies.js +1 -1
  108. package/dist/syncer/syncer.d.ts.map +1 -1
  109. package/dist/tasks/step-wrapper.d.ts.map +1 -1
  110. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  111. package/dist/template/implementations/entity.template.d.ts +2 -2
  112. package/dist/template/implementations/entity.template.d.ts.map +1 -1
  113. package/dist/template/implementations/entry-server.template.d.ts +2 -2
  114. package/dist/template/implementations/entry-server.template.d.ts.map +1 -1
  115. package/dist/template/implementations/generated.template.d.ts +2 -2
  116. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  117. package/dist/template/implementations/generated_http.template.d.ts +2 -2
  118. package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
  119. package/dist/template/implementations/generated_sso.template.d.ts +2 -2
  120. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  121. package/dist/template/implementations/http_validators.template.d.ts +2 -2
  122. package/dist/template/implementations/http_validators.template.d.ts.map +1 -1
  123. package/dist/template/implementations/init_types.template.d.ts +2 -2
  124. package/dist/template/implementations/init_types.template.d.ts.map +1 -1
  125. package/dist/template/implementations/model.template.d.ts +2 -2
  126. package/dist/template/implementations/model.template.d.ts.map +1 -1
  127. package/dist/template/implementations/model_test.template.d.ts +2 -2
  128. package/dist/template/implementations/model_test.template.d.ts.map +1 -1
  129. package/dist/template/implementations/queries.template.d.ts +2 -2
  130. package/dist/template/implementations/queries.template.d.ts.map +1 -1
  131. package/dist/template/implementations/sd.template.d.ts +2 -2
  132. package/dist/template/implementations/sd.template.d.ts.map +1 -1
  133. package/dist/template/implementations/sd.template.js +1 -1
  134. package/dist/template/implementations/services.template.d.ts +2 -2
  135. package/dist/template/implementations/services.template.d.ts.map +1 -1
  136. package/dist/template/implementations/view_form.template.d.ts +3 -3
  137. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  138. package/dist/template/implementations/view_id_all_select.template.d.ts +2 -2
  139. package/dist/template/implementations/view_id_all_select.template.d.ts.map +1 -1
  140. package/dist/template/implementations/view_list.template.d.ts +2 -2
  141. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  142. package/dist/template/implementations/view_search_input.template.d.ts +2 -2
  143. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
  144. package/dist/template/template-manager.d.ts.map +1 -1
  145. package/dist/template/template.d.ts.map +1 -1
  146. package/dist/template/zod-converter.d.ts.map +1 -1
  147. package/dist/testing/_relation-graph.d.ts.map +1 -1
  148. package/dist/testing/data-explorer.d.ts +2 -2
  149. package/dist/testing/data-explorer.d.ts.map +1 -1
  150. package/dist/testing/dev-vitest-manager.d.ts.map +1 -1
  151. package/dist/testing/faker-mappings.d.ts.map +1 -1
  152. package/dist/testing/fixture-generator.d.ts.map +1 -1
  153. package/dist/testing/fixture-generator.js +1 -1
  154. package/dist/testing/fixture-loader.d.ts.map +1 -1
  155. package/dist/testing/fixture-manager.d.ts +4 -0
  156. package/dist/testing/fixture-manager.d.ts.map +1 -1
  157. package/dist/testing/fixture-manager.js +99 -25
  158. package/dist/testing/naite-vitest-reporter.d.ts.map +1 -1
  159. package/dist/testing/parallel-db-manager.d.ts.map +1 -1
  160. package/dist/tooling/__tests__/helpers/sonamu-test-root.d.ts +10 -0
  161. package/dist/tooling/__tests__/helpers/sonamu-test-root.d.ts.map +1 -0
  162. package/dist/tooling/__tests__/helpers/sonamu-test-root.js +42 -0
  163. package/dist/tooling/cdd-service.d.ts +87 -0
  164. package/dist/tooling/cdd-service.d.ts.map +1 -0
  165. package/dist/tooling/cdd-service.js +215 -0
  166. package/dist/tooling/cli-tooling.d.ts +11 -0
  167. package/dist/tooling/cli-tooling.d.ts.map +1 -0
  168. package/dist/tooling/cli-tooling.js +901 -0
  169. package/dist/tooling/fixture-service.d.ts +29 -0
  170. package/dist/tooling/fixture-service.d.ts.map +1 -0
  171. package/dist/tooling/fixture-service.js +53 -0
  172. package/dist/tooling/index.d.ts +2 -0
  173. package/dist/tooling/index.d.ts.map +1 -0
  174. package/dist/tooling/index.js +2 -0
  175. package/dist/tooling/test-run-client.d.ts +27 -0
  176. package/dist/tooling/test-run-client.d.ts.map +1 -0
  177. package/dist/tooling/test-run-client.js +120 -0
  178. package/dist/tsdown.api.config.js +48 -0
  179. package/dist/types/types.d.ts +527 -527
  180. package/dist/types/types.d.ts.map +1 -1
  181. package/dist/ui/ai-client.d.ts.map +1 -1
  182. package/dist/utils/formatter.d.ts +6 -0
  183. package/dist/utils/formatter.d.ts.map +1 -1
  184. package/dist/utils/formatter.js +42 -11
  185. package/dist/utils/model.d.ts +1 -1
  186. package/dist/vector/chunking.d.ts.map +1 -1
  187. package/dist/vector/embedding.d.ts.map +1 -1
  188. package/package.json +13 -12
  189. package/src/api/sonamu.ts +2 -1
  190. package/src/auth/index.ts +1 -1
  191. package/src/bin/__tests__/zod-compiler-build.test.ts +67 -0
  192. package/src/bin/build-config.ts +151 -5
  193. package/src/bin/fixture.ts +204 -21
  194. package/src/bin/test-command.ts +27 -4
  195. package/src/bin/typecheck-web.ts +56 -0
  196. package/src/dict/sonamu-dictionary.ts +1 -1
  197. package/src/entity/__tests__/entity-manager-autoload.contract.test.ts +55 -0
  198. package/src/entity/__tests__/entity-manager-explicit-root-registration.contract.test.ts +107 -0
  199. package/src/entity/entity-manager.ts +31 -19
  200. package/src/entity/entity.ts +7 -2
  201. package/src/logger/__tests__/configure.test.ts +33 -0
  202. package/src/logger/configure.ts +45 -3
  203. package/src/migration/__tests__/migrator.test.ts +51 -0
  204. package/src/migration/migrator.ts +103 -34
  205. package/src/syncer/api-parser.ts +1 -1
  206. package/src/testing/fixture-manager.ts +124 -32
  207. package/src/tooling/__tests__/cdd-rule-bootstrap.contract.test.ts +118 -0
  208. package/src/tooling/__tests__/cli-tooling-auth.contract.test.ts +72 -0
  209. package/src/tooling/__tests__/cli-tooling-cone-locale.contract.test.ts +150 -0
  210. package/src/tooling/__tests__/cli-tooling-entity-patch.contract.test.ts +131 -0
  211. package/src/tooling/__tests__/cli-tooling-fixture-transfer.contract.test.ts +255 -0
  212. package/src/tooling/__tests__/cli-tooling-i18n-update.contract.test.ts +104 -0
  213. package/src/tooling/__tests__/cli-tooling-scaffold.contract.test.ts +86 -0
  214. package/src/tooling/__tests__/cli-tooling-stub-practice.contract.test.ts +147 -0
  215. package/src/tooling/__tests__/default-cdd-adapter.contract.test.ts +73 -0
  216. package/src/tooling/__tests__/helpers/sonamu-test-root.ts +42 -0
  217. package/src/tooling/__tests__/test-run-client-config.contract.test.ts +154 -0
  218. package/src/tooling/cdd-service.ts +256 -0
  219. package/src/tooling/cli-tooling.ts +937 -0
  220. package/src/tooling/fixture-service.ts +72 -0
  221. package/src/tooling/index.ts +1 -0
  222. package/src/tooling/test-run-client.ts +196 -0
  223. package/src/utils/__tests__/formatter-bin.test.ts +99 -0
  224. package/src/utils/formatter.ts +74 -10
  225. package/tsdown.api.config.ts +8 -10
  226. package/bin/cli.js +0 -75
  227. package/dist/bin/cli.d.ts +0 -2
  228. package/dist/bin/cli.d.ts.map +0 -1
  229. package/dist/bin/cli.js +0 -956
  230. package/src/bin/cli.ts +0 -1166
@@ -1,11 +1,23 @@
1
- import { readFile, writeFile } from "node:fs/promises";
1
+ import assert from "node:assert";
2
+ import { readFile, rm, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
5
 
6
+ import chalk from "chalk";
5
7
  import { type UserConfig } from "tsdown";
6
8
 
7
9
  import { type NormalizedZodCompilerPolicy } from "../api/config";
10
+ import {
11
+ execWithLinePrefix,
12
+ printBuildSummary,
13
+ printTaskFailed,
14
+ printTaskHeader,
15
+ printTaskStart,
16
+ printTaskSuccess,
17
+ } from "../utils/console-util";
8
18
  import { exists } from "../utils/fs-utils";
19
+ import { findApiRootPath, findAppRootPath } from "../utils/utils";
20
+ import { loadBuildCompilerPolicy } from "./compiler-policy";
9
21
 
10
22
  export type BuildArtifact<BuildCommandArgs = {}> = {
11
23
  name: string;
@@ -18,6 +30,21 @@ export type BuildArtifact<BuildCommandArgs = {}> = {
18
30
 
19
31
  export type ApiTsdownBuildConfig = Pick<UserConfig, "entry" | "plugins">;
20
32
 
33
+ export function quoteShellArgument(value: string): string {
34
+ // 작은따옴표 자체만 셸 경계를 닫지 못하도록 분리해 리터럴 인수를 보존합니다.
35
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
36
+ }
37
+
38
+ export function createWebTypecheckCommand(): string {
39
+ const isSourceModule = path.extname(import.meta.filename) === ".ts";
40
+ const scriptPath = path.join(
41
+ import.meta.dirname,
42
+ `typecheck-web.${isSourceModule ? "ts" : "js"}`,
43
+ );
44
+ const runner = isSourceModule ? "pnpm exec tsx" : quoteShellArgument(process.execPath);
45
+ return `${runner} ${quoteShellArgument(scriptPath)}`;
46
+ }
47
+
21
48
  type ZodCompilerPluginFactory = (typeof import("zod-compiler/rolldown"))["default"];
22
49
 
23
50
  export interface ApiZodCompilerBuildDependencies {
@@ -184,8 +211,11 @@ export const API_ARTIFACTS: BuildArtifact<{ configFilePath: string }>[] = [
184
211
  description: "API 프로젝트 빌드 산출물",
185
212
  projectPath: "api",
186
213
  preBuildCommand: () => "rm -rf dist",
187
- buildCommand: ({ configFilePath }) =>
188
- `tsc --noEmit && pnpm exec tsdown --config ${JSON.stringify(configFilePath)}`,
214
+ buildCommand: ({ configFilePath }) => {
215
+ const configExtension = path.extname(configFilePath);
216
+ const configLoader = [".ts", ".tsx"].includes(configExtension) ? " --config-loader tsx" : "";
217
+ return `tsc --noEmit && pnpm exec tsdown --config ${JSON.stringify(configFilePath)}${configLoader}`;
218
+ },
189
219
  },
190
220
  ];
191
221
 
@@ -200,7 +230,7 @@ export const WEB_ARTIFACTS: BuildArtifact[] = [
200
230
  projectPath: "web",
201
231
  preBuildCommand: () => "rm -rf dist/client",
202
232
  buildCommand: () =>
203
- "tsc -b --noEmit && vite build --config vite.config.ts --outDir dist/client",
233
+ `${createWebTypecheckCommand()} && vite build --config vite.config.ts --outDir dist/client`,
204
234
  },
205
235
  {
206
236
  name: "Web Server",
@@ -208,8 +238,124 @@ export const WEB_ARTIFACTS: BuildArtifact[] = [
208
238
  projectPath: "web",
209
239
  preBuildCommand: () => "rm -rf dist/server",
210
240
  buildCommand: () =>
211
- "tsc -b --noEmit && vite build --config vite.config.ts --ssr src/entry-server.generated.tsx --outDir dist/server",
241
+ `${createWebTypecheckCommand()} && vite build --config vite.config.ts --ssr src/entry-server.generated.tsx --outDir dist/server`,
212
242
  postBuildCommand: () =>
213
243
  "rm -rf ../api/web-dist && mkdir -p ../api/web-dist && cp -r dist/* ../api/web-dist",
214
244
  },
215
245
  ];
246
+
247
+ export async function resolveApiBuildConfigPath(
248
+ apiRootPath: string,
249
+ dependencies: {
250
+ exists: (candidatePath: string) => Promise<boolean>;
251
+ moduleDir: string;
252
+ } = {
253
+ exists,
254
+ moduleDir: import.meta.dirname,
255
+ },
256
+ ): Promise<string> {
257
+ const localConfigPath = path.join(apiRootPath, "tsdown.config.ts");
258
+ if (await dependencies.exists(localConfigPath)) {
259
+ console.log(chalk.dim("Using tsdown.config.ts from project root..."));
260
+ return localConfigPath;
261
+ }
262
+
263
+ console.log(chalk.dim("Using default tsdown API config from sonamu package..."));
264
+ const compiledConfigPath = path.join(dependencies.moduleDir, "..", "tsdown.api.config.js");
265
+ if (await dependencies.exists(compiledConfigPath)) {
266
+ return compiledConfigPath;
267
+ }
268
+ return path.join(dependencies.moduleDir, "..", "..", "tsdown.api.config.ts");
269
+ }
270
+
271
+ async function runBuildSteps<T>(
272
+ artifact: BuildArtifact<T>,
273
+ options: { cwd: string; buildCommandArgs: T },
274
+ ) {
275
+ const steps = [
276
+ { name: "pre-build", cmd: artifact.preBuildCommand?.() },
277
+ { name: "build", cmd: artifact.buildCommand(options.buildCommandArgs) },
278
+ { name: "post-build", cmd: artifact.postBuildCommand?.() },
279
+ ].filter((step) => step.cmd !== undefined);
280
+
281
+ for (let index = 0; index < steps.length; index++) {
282
+ const step = steps[index];
283
+ const isLast = index === steps.length - 1;
284
+ try {
285
+ assert(step.cmd);
286
+ printTaskStart(step.name, step.cmd, isLast);
287
+ await execWithLinePrefix(step.cmd, { cwd: options.cwd });
288
+ printTaskSuccess(step.name, isLast);
289
+ } catch (error) {
290
+ printTaskFailed(step.name, isLast);
291
+ throw new Error(`${step.name} failed`, { cause: error });
292
+ }
293
+ }
294
+ }
295
+
296
+ export async function buildApiCommand(): Promise<void> {
297
+ const apiRootPath = findApiRootPath();
298
+ const baseConfigPath = await resolveApiBuildConfigPath(apiRootPath);
299
+ const policy = await loadBuildCompilerPolicy(apiRootPath);
300
+ const registryPath = path.join(apiRootPath, HTTP_VALIDATOR_REGISTRY_SOURCE_PATH);
301
+ const configFilePath = await createApiZodCompilerBuildWrapper({
302
+ apiRootPath,
303
+ baseConfigPath,
304
+ policy,
305
+ registryPath,
306
+ });
307
+ const temporaryConfigPath = configFilePath === baseConfigPath ? undefined : configFilePath;
308
+ const startedAt = Date.now();
309
+
310
+ try {
311
+ for (const artifact of API_ARTIFACTS) {
312
+ printTaskHeader(artifact.name, artifact.description, apiRootPath);
313
+ await runBuildSteps(artifact, {
314
+ cwd: apiRootPath,
315
+ buildCommandArgs: { configFilePath },
316
+ });
317
+ }
318
+ if (policy.api === "aot") {
319
+ const outputPath = path.join(apiRootPath, HTTP_VALIDATOR_REGISTRY_BUILD_PATH);
320
+ if (!(await exists(outputPath))) {
321
+ throw new Error(`Built HTTP validator registry not found: ${outputPath}`);
322
+ }
323
+ assertApiZodCompilerBuildOutput([
324
+ { code: await readFile(outputPath, "utf8"), path: outputPath },
325
+ ]);
326
+ }
327
+ printBuildSummary("API", true, Date.now() - startedAt);
328
+ } catch (error) {
329
+ printBuildSummary("API", false, Date.now() - startedAt);
330
+ throw error;
331
+ } finally {
332
+ if (temporaryConfigPath !== undefined) await rm(temporaryConfigPath, { force: true });
333
+ }
334
+ }
335
+
336
+ export async function buildWebCommand({
337
+ skipIfMissing = false,
338
+ }: { skipIfMissing?: boolean } = {}): Promise<void> {
339
+ const appRootPath = findAppRootPath();
340
+ const webPath = path.join(appRootPath, "web");
341
+ if (!(await exists(webPath))) {
342
+ if (skipIfMissing) {
343
+ console.log(chalk.gray("Web 디렉토리가 없으므로 Web 빌드를 건너뜁니다."));
344
+ return;
345
+ }
346
+ throw new Error(`web 디렉토리를 찾을 수 없습니다: ${webPath}`);
347
+ }
348
+
349
+ const startedAt = Date.now();
350
+ try {
351
+ for (const artifact of WEB_ARTIFACTS) {
352
+ const cwd = path.join(appRootPath, artifact.projectPath);
353
+ printTaskHeader(artifact.name, artifact.description, cwd);
354
+ await runBuildSteps(artifact, { cwd, buildCommandArgs: {} });
355
+ }
356
+ printBuildSummary("Web", true, Date.now() - startedAt);
357
+ } catch (error) {
358
+ printBuildSummary("Web", false, Date.now() - startedAt);
359
+ throw error;
360
+ }
361
+ }
@@ -1,4 +1,8 @@
1
+ import { spawn, type SpawnOptions } from "node:child_process";
2
+ import { open } from "node:fs/promises";
3
+
1
4
  import chalk from "chalk";
5
+ import knex, { type Knex } from "knex";
2
6
  import prompts from "prompts";
3
7
  import { z } from "zod";
4
8
 
@@ -10,18 +14,22 @@ import { getSonamuEnvironment } from "../env";
10
14
  import { DataExplorer } from "../testing/data-explorer";
11
15
  import { type DataExplorerStrategy } from "../testing/data-explorer";
12
16
  import { FixtureGenerator } from "../testing/fixture-generator";
17
+ import { FixtureManager } from "../testing/fixture-manager";
18
+ import { exists } from "../utils/fs-utils";
13
19
 
14
- interface FixtureCommandOptions {
20
+ export interface FixtureCommandOptions {
15
21
  _?: string[];
16
22
  all?: boolean;
17
23
  include?: string;
18
24
  exclude?: string;
19
- count?: string;
25
+ count?: string | number;
20
26
  "save-to"?: string;
21
27
  strategy?: DataExplorerStrategy;
22
- limit?: string;
28
+ limit?: string | number;
23
29
  "use-llm"?: boolean;
24
30
  "no-cache"?: boolean;
31
+ nonInteractive?: boolean;
32
+ userMode?: "login" | "dummy";
25
33
  }
26
34
 
27
35
  interface SignupBody {
@@ -34,6 +42,160 @@ interface SignupBody {
34
42
 
35
43
  const SignupErrorResponse = z.object({ code: z.string().optional() }).passthrough();
36
44
 
45
+ function runProcess(
46
+ executable: string,
47
+ args: string[],
48
+ stdio: SpawnOptions["stdio"] = ["ignore", "inherit", "inherit"],
49
+ ): Promise<void> {
50
+ return new Promise((resolve, reject) => {
51
+ const child = spawn(executable, args, { stdio });
52
+ child.once("error", reject);
53
+ child.once("exit", (code, signal) => {
54
+ if (code === 0) resolve();
55
+ else
56
+ reject(
57
+ new Error(`${executable} 명령이 ${code ?? signal ?? "unknown"} 상태로 실패했습니다.`),
58
+ );
59
+ });
60
+ });
61
+ }
62
+
63
+ async function writeMysqlDump(args: string[], outputPath: string) {
64
+ const output = await open(outputPath, "w");
65
+ try {
66
+ await runProcess("mysqldump", args, ["ignore", output.fd, "inherit"]);
67
+ } finally {
68
+ await output.close();
69
+ }
70
+ }
71
+
72
+ async function runMysqlWithInput(args: string[], inputPath: string) {
73
+ const input = await open(inputPath, "r");
74
+ try {
75
+ await runProcess("mysql", args, [input.fd, "inherit", "inherit"]);
76
+ } finally {
77
+ await input.close();
78
+ }
79
+ }
80
+
81
+ function mysqlConnectionArgs(connection: Knex.ConnectionConfig) {
82
+ return [
83
+ `-h${String(connection.host)}`,
84
+ `-u${String(connection.user)}`,
85
+ `-p${String(connection.password)}`,
86
+ ];
87
+ }
88
+
89
+ export async function fixtureInitCommand(): Promise<void> {
90
+ const srcConfig = Sonamu.dbConfig[getSonamuEnvironment()];
91
+ const fixtureConnection =
92
+ /* SAFETY: Sonamu DB 구성은 초기화 시 Knex 연결 구성으로 검증됩니다. */ Sonamu.dbConfig.fixture
93
+ .connection as Knex.ConnectionConfig;
94
+ const testConnection =
95
+ /* SAFETY: Sonamu DB 구성은 초기화 시 Knex 연결 구성으로 검증됩니다. */ Sonamu.dbConfig.test
96
+ .connection as Knex.ConnectionConfig;
97
+ const targets: Array<{ label: string; config: Knex.Config; toSkip?: boolean }> = [
98
+ { label: "(REMOTE) Fixture DB", config: Sonamu.dbConfig.fixture },
99
+ {
100
+ label: "(LOCAL) Testing DB",
101
+ config: Sonamu.dbConfig.test,
102
+ // 같은 DB를 가리키면 원격 fixture를 다시 덮어쓰지 않습니다.
103
+ toSkip:
104
+ fixtureConnection.host === testConnection.host &&
105
+ fixtureConnection.database === testConnection.database,
106
+ },
107
+ ];
108
+
109
+ const dumpFilename = `/tmp/sonamu-fixture-init-${Date.now()}.sql`;
110
+ const migrationsDump = `/tmp/sonamu-fixture-init-migrations-${Date.now()}.sql`;
111
+ const srcConnection =
112
+ /* SAFETY: Sonamu DB 구성은 초기화 시 Knex 연결 구성으로 검증됩니다. */ srcConfig.connection as Knex.ConnectionConfig;
113
+ await writeMysqlDump(
114
+ [
115
+ ...mysqlConnectionArgs(srcConnection),
116
+ "--single-transaction",
117
+ "-d",
118
+ "--no-create-db",
119
+ "--triggers",
120
+ String(srcConnection.database),
121
+ ],
122
+ dumpFilename,
123
+ );
124
+
125
+ const sourceDb = knex(srcConfig);
126
+ try {
127
+ const [[migrations]] = await sourceDb.raw(
128
+ "SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = ? AND table_name = 'knex_migrations'",
129
+ [srcConnection.database],
130
+ );
131
+ if (migrations.count > 0) {
132
+ await writeMysqlDump(
133
+ [
134
+ ...mysqlConnectionArgs(srcConnection),
135
+ "--single-transaction",
136
+ "--no-create-db",
137
+ "--triggers",
138
+ String(srcConnection.database),
139
+ "knex_migrations",
140
+ "knex_migrations_lock",
141
+ ],
142
+ migrationsDump,
143
+ );
144
+ }
145
+
146
+ for (const { label, config, toSkip } of targets) {
147
+ const connection =
148
+ /* SAFETY: targets는 검증된 Sonamu DB 구성에서만 조합됩니다. */ config.connection as Knex.ConnectionConfig;
149
+ if (toSkip === true) {
150
+ console.log(chalk.red(`${label}: Skipped!`));
151
+ continue;
152
+ }
153
+
154
+ const databaseConnection =
155
+ /* SAFETY: targets는 검증된 Sonamu DB 구성에서만 조합됩니다. */ (config.connection ??
156
+ {}) as Knex.ConnectionConfig;
157
+ const db = knex({
158
+ ...config,
159
+ connection: { ...databaseConnection, database: undefined },
160
+ });
161
+ try {
162
+ const [[row]] = await db.raw(`SHOW DATABASES LIKE "${connection.database}"`);
163
+ if (row) {
164
+ console.log(chalk.yellow(`${label}: Database "${connection.database}" Already exists`));
165
+ continue;
166
+ }
167
+
168
+ const connectionArgs = mysqlConnectionArgs(connection);
169
+ const database = String(connection.database);
170
+ const escapedDatabase = database.replaceAll("`", "``");
171
+ await runProcess("mysql", [
172
+ ...connectionArgs,
173
+ "-e",
174
+ `DROP DATABASE IF EXISTS \`${escapedDatabase}\``,
175
+ ]);
176
+ await runProcess("mysql", [
177
+ ...connectionArgs,
178
+ "-e",
179
+ `CREATE DATABASE \`${escapedDatabase}\``,
180
+ ]);
181
+ await runMysqlWithInput([...connectionArgs, database], dumpFilename);
182
+ if (await exists(migrationsDump)) {
183
+ await runMysqlWithInput([...connectionArgs, database], migrationsDump);
184
+ }
185
+ } finally {
186
+ await db.destroy();
187
+ }
188
+ }
189
+ } finally {
190
+ await sourceDb.destroy();
191
+ }
192
+ }
193
+
194
+ export async function fixtureCleanupCommand(): Promise<void> {
195
+ await FixtureManager.destroy();
196
+ await Sonamu.destroy();
197
+ }
198
+
37
199
  /**
38
200
  * username을 일반적인 규칙(영문자 시작, 영문자/숫자만, 1-20자)에 맞도록 정규화합니다.
39
201
  */
@@ -70,6 +232,11 @@ export async function fixtureGenCommand(options: FixtureCommandOptions) {
70
232
  }
71
233
  } else if (options.include) {
72
234
  entityNames = options.include.split(",").map((s: string) => s.trim());
235
+ } else if (options.nonInteractive === true) {
236
+ throw Object.assign(new Error("Fixture gen selector is required."), {
237
+ code: "MISSING_ARGUMENT",
238
+ exitCode: 2,
239
+ });
73
240
  } else {
74
241
  const result = await prompts({
75
242
  type: "multiselect",
@@ -87,8 +254,8 @@ export async function fixtureGenCommand(options: FixtureCommandOptions) {
87
254
  entityNames = result.entities;
88
255
  }
89
256
 
90
- let count = options.count ? Number.parseInt(options.count, 10) : 5;
91
- if (!options.count) {
257
+ let count = options.count ? Number.parseInt(String(options.count), 10) : 5;
258
+ if (!options.count && options.nonInteractive !== true) {
92
259
  const result = await prompts({
93
260
  type: "number",
94
261
  name: "count",
@@ -109,25 +276,30 @@ export async function fixtureGenCommand(options: FixtureCommandOptions) {
109
276
  const hasUser = entityNames.includes("User");
110
277
 
111
278
  if (hasUser) {
112
- const userModeResult = await prompts({
113
- type: "select",
114
- name: "userMode",
115
- message: "User 엔티티가 포함되어 있습니다. 생성 방식을 선택하세요:",
116
- choices: [
117
- { title: "1. 로그인 가능한 사용자 fixture 생성", value: "login" },
118
- { title: "2. 확인용 데이터(로그인 불가) 생성", value: "dummy" },
119
- ],
120
- });
279
+ let userMode = options.userMode;
280
+ if (userMode === undefined && options.nonInteractive === true) userMode = "dummy";
281
+ if (userMode === undefined) {
282
+ const userModeResult = await prompts({
283
+ type: "select",
284
+ name: "userMode",
285
+ message: "User 엔티티가 포함되어 있습니다. 생성 방식을 선택하세요:",
286
+ choices: [
287
+ { title: "1. 로그인 가능한 사용자 fixture 생성", value: "login" },
288
+ { title: "2. 확인용 데이터(로그인 불가) 생성", value: "dummy" },
289
+ ],
290
+ });
291
+ userMode = userModeResult.userMode;
292
+ }
121
293
 
122
- if (!userModeResult.userMode) {
294
+ if (!userMode) {
123
295
  console.log(chalk.yellow("취소되었습니다."));
124
296
  return;
125
297
  }
126
298
 
127
- if (userModeResult.userMode === "login") {
299
+ if (userMode === "login") {
128
300
  // LLM 사용 여부
129
301
  let useLLM = options["use-llm"] ?? false;
130
- if (!options["use-llm"]) {
302
+ if (options["use-llm"] === undefined && options.nonInteractive !== true) {
131
303
  const llmResult = await prompts({
132
304
  type: "confirm",
133
305
  name: "useLLM",
@@ -232,7 +404,7 @@ export async function fixtureGenCommand(options: FixtureCommandOptions) {
232
404
  }
233
405
 
234
406
  let saveTarget = options["save-to"] || "db";
235
- if (!options["save-to"]) {
407
+ if (!options["save-to"] && options.nonInteractive !== true) {
236
408
  const result = await prompts({
237
409
  type: "select",
238
410
  name: "saveTarget",
@@ -266,7 +438,7 @@ export async function fixtureGenCommand(options: FixtureCommandOptions) {
266
438
 
267
439
  // LLM 사용 여부 결정
268
440
  let useLLM = options["use-llm"] ?? false;
269
- if (!options["use-llm"]) {
441
+ if (options["use-llm"] === undefined && options.nonInteractive !== true) {
270
442
  const llmResult = await prompts({
271
443
  type: "confirm",
272
444
  name: "useLLM",
@@ -387,6 +559,11 @@ export async function fixtureFetchCommand(options: FixtureCommandOptions) {
387
559
  }
388
560
  } else if (options.include) {
389
561
  entityNames = options.include.split(",").map((s: string) => s.trim());
562
+ } else if (options.nonInteractive === true) {
563
+ throw Object.assign(new Error("Fixture fetch selector is required."), {
564
+ code: "MISSING_ARGUMENT",
565
+ exitCode: 2,
566
+ });
390
567
  } else {
391
568
  const result = await prompts({
392
569
  type: "multiselect",
@@ -405,7 +582,7 @@ export async function fixtureFetchCommand(options: FixtureCommandOptions) {
405
582
  }
406
583
 
407
584
  const strategy: DataExplorerStrategy = options.strategy ?? "recent";
408
- const limit = options.limit ? Number.parseInt(options.limit, 10) : 10;
585
+ const limit = options.limit ? Number.parseInt(String(options.limit), 10) : 10;
409
586
 
410
587
  // fixture fetch: 현재 환경 데이터를 fixture DB로 import합니다
411
588
  const sourceDb = DB.getDB("r");
@@ -449,6 +626,12 @@ export async function fixtureExploreCommand(options: FixtureCommandOptions) {
449
626
 
450
627
  let entityName = options.include;
451
628
 
629
+ if (!entityName && options.nonInteractive === true) {
630
+ throw Object.assign(new Error("Fixture explore selector is required."), {
631
+ code: "MISSING_ARGUMENT",
632
+ exitCode: 2,
633
+ });
634
+ }
452
635
  if (!entityName) {
453
636
  const result = await prompts({
454
637
  type: "select",
@@ -470,7 +653,7 @@ export async function fixtureExploreCommand(options: FixtureCommandOptions) {
470
653
  }
471
654
 
472
655
  const strategy: DataExplorerStrategy = options.strategy ?? "sample";
473
- const limit = options.limit ? Number.parseInt(options.limit, 10) : 10;
656
+ const limit = options.limit ? Number.parseInt(String(options.limit), 10) : 10;
474
657
 
475
658
  const db = DB.getDB("r");
476
659
  const explorer = new DataExplorer(db, EntityManager);
@@ -40,6 +40,13 @@ export interface TestCommandDependencies {
40
40
  loadConfig: (apiRootPath: string) => Promise<TestCommandConfig>;
41
41
  }
42
42
 
43
+ export interface LegacyTestCommandInput {
44
+ files?: readonly string[];
45
+ pattern?: string;
46
+ traces?: boolean;
47
+ status?: boolean;
48
+ }
49
+
43
50
  const testCommandDependencies: TestCommandDependencies = {
44
51
  fetch: globalThis.fetch,
45
52
  findApiRootPath,
@@ -74,10 +81,6 @@ export async function testCommand(
74
81
  dependencies: TestCommandDependencies = testCommandDependencies,
75
82
  ): Promise<void> {
76
83
  const args = process.argv.slice(3);
77
-
78
- const config = await loadDevServerConfig(dependencies);
79
-
80
- // process.argv 파싱: sonamu test [file...] --pattern "이름" --traces --status
81
84
  const files: string[] = [];
82
85
  let pattern: string | undefined;
83
86
  let showTraces = false;
@@ -96,6 +99,26 @@ export async function testCommand(
96
99
  }
97
100
  }
98
101
 
102
+ return runTestCommand({ files, pattern, traces: showTraces, status: showStatus }, dependencies);
103
+ }
104
+
105
+ export async function testCommandWithInput(
106
+ input: LegacyTestCommandInput,
107
+ dependencies: TestCommandDependencies = testCommandDependencies,
108
+ ): Promise<void> {
109
+ return runTestCommand(input, dependencies);
110
+ }
111
+
112
+ async function runTestCommand(
113
+ input: LegacyTestCommandInput,
114
+ dependencies: TestCommandDependencies,
115
+ ): Promise<void> {
116
+ const config = await loadDevServerConfig(dependencies);
117
+ const files = [...(input.files ?? [])];
118
+ const pattern = input.pattern;
119
+ const showTraces = input.traces === true;
120
+ const showStatus = input.status === true;
121
+
99
122
  if (showStatus) {
100
123
  return testStatusCommand(config, dependencies.fetch);
101
124
  }
@@ -0,0 +1,56 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+
7
+ import ts from "@typescript/typescript6";
8
+ import { z } from "zod";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+ const requireFromHere = createRequire(import.meta.url);
12
+ const configPath = path.resolve("tsconfig.json");
13
+ const parsedConfig = ts.parseConfigFileTextToJson(configPath, await readFile(configPath, "utf8"));
14
+ if (parsedConfig.error) {
15
+ throw new Error(ts.flattenDiagnosticMessageText(parsedConfig.error.messageText, "\n"));
16
+ }
17
+ const configResult = z
18
+ .object({ references: z.array(z.object({ path: z.string() })).optional() })
19
+ .safeParse(parsedConfig.config);
20
+ if (!configResult.success) {
21
+ throw new Error("tsconfig.json의 project references를 읽을 수 없습니다.", {
22
+ cause: configResult.error,
23
+ });
24
+ }
25
+ const config = configResult.data;
26
+ const references = config.references ?? [];
27
+
28
+ if (references.length === 0) {
29
+ throw new Error("tsconfig.json에 typecheck할 project reference가 없습니다.");
30
+ }
31
+
32
+ const tscPath = path.join(
33
+ path.dirname(requireFromHere.resolve("typescript/package.json")),
34
+ "bin/tsc",
35
+ );
36
+ for (const reference of references) {
37
+ // Web 앱은 선언을 배포하지 않으므로 참조별 타입 검사에서 선언 직렬화만 비활성화합니다.
38
+ await execFileAsync(
39
+ process.execPath,
40
+ [
41
+ tscPath,
42
+ "--noEmit",
43
+ "--composite",
44
+ "false",
45
+ "--declaration",
46
+ "false",
47
+ "--declarationMap",
48
+ "false",
49
+ "--emitDeclarationOnly",
50
+ "false",
51
+ "-p",
52
+ path.resolve(reference.path),
53
+ ],
54
+ { cwd: process.cwd() },
55
+ );
56
+ }
@@ -3,7 +3,7 @@ import fs from "fs";
3
3
  import path from "path";
4
4
 
5
5
  import { Workbook } from "@sheetkit/node";
6
- import ts from "typescript";
6
+ import ts from "@typescript/typescript6";
7
7
 
8
8
  import { Sonamu } from "../api/sonamu";
9
9
  import { EntityManager } from "../entity/entity-manager";
@@ -0,0 +1,55 @@
1
+ /// <reference types="node" />
2
+
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { afterEach, describe, expect, it } from "vitest";
8
+
9
+ interface EntityManagerModule {
10
+ readonly EntityManager: {
11
+ isAutoloaded: boolean;
12
+ autoload(doSilent?: boolean, apiRootPath?: string): Promise<void>;
13
+ };
14
+ }
15
+
16
+ interface SonamuModule {
17
+ readonly Sonamu: {
18
+ readonly apiRootPath: string;
19
+ };
20
+ }
21
+
22
+ const temporaryRoots: string[] = [];
23
+
24
+ async function importProduction<T>(relativePath: string): Promise<T> {
25
+ const loaded: unknown = await import(
26
+ /* @vite-ignore */ new URL(relativePath, import.meta.url).href
27
+ );
28
+ // SAFETY: 테스트는 명시한 singleton 공개 API만 호출합니다.
29
+ return loaded as T;
30
+ }
31
+
32
+ afterEach(async () => {
33
+ await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true })));
34
+ });
35
+
36
+ describe("EntityManager 명시적 API 루트", () => {
37
+ it("Sonamu singleton이 초기화되지 않아도 전달한 apiRootPath로 autoload한다", async () => {
38
+ const [{ EntityManager }, { Sonamu }] = await Promise.all([
39
+ importProduction<EntityManagerModule>("../entity-manager.ts"),
40
+ importProduction<SonamuModule>("../../api/sonamu.ts"),
41
+ ]);
42
+ const apiRootPath = await mkdtemp(path.join(os.tmpdir(), "sonamu-entity-autoload-"));
43
+ temporaryRoots.push(apiRootPath);
44
+ const previousAutoloaded = EntityManager.isAutoloaded;
45
+ EntityManager.isAutoloaded = false;
46
+
47
+ expect(() => Sonamu.apiRootPath).toThrow(/init/i);
48
+ try {
49
+ await expect(EntityManager.autoload(true, apiRootPath)).resolves.toBeUndefined();
50
+ expect(EntityManager.isAutoloaded).toBe(true);
51
+ } finally {
52
+ EntityManager.isAutoloaded = previousAutoloaded;
53
+ }
54
+ });
55
+ });