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
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+
3
+ const legacyFixtureOptionsSchema = z.object({
4
+ _: z.array(z.string()).optional(),
5
+ all: z.boolean().optional(),
6
+ include: z.string().optional(),
7
+ exclude: z.string().optional(),
8
+ count: z.union([z.string(), z.number()]).optional(),
9
+ strategy: z.string().optional(),
10
+ limit: z.union([z.string(), z.number()]).optional(),
11
+ saveTo: z.string().optional(),
12
+ useLlm: z.boolean().optional(),
13
+ noCache: z.boolean().optional(),
14
+ "save-to": z.string().optional(),
15
+ "use-llm": z.boolean().optional(),
16
+ "no-cache": z.boolean().optional(),
17
+ nonInteractive: z.boolean().optional(),
18
+ userMode: z.enum(["login", "dummy"]).optional(),
19
+ });
20
+
21
+ type LegacyFixtureOptions = z.infer<typeof legacyFixtureOptionsSchema>;
22
+ export type LegacyFixtureSource = z.input<typeof legacyFixtureOptionsSchema>;
23
+ export type LegacyFixtureCommand = "gen" | "fetch" | "explore";
24
+ export type NormalizedLegacyFixtureOptions = Omit<
25
+ LegacyFixtureOptions,
26
+ "saveTo" | "useLlm" | "noCache"
27
+ >;
28
+
29
+ export function normalizeLegacyFixtureOptions(
30
+ input: LegacyFixtureSource,
31
+ ): NormalizedLegacyFixtureOptions {
32
+ const { saveTo, useLlm, noCache, ...options } = legacyFixtureOptionsSchema.parse(input);
33
+ return {
34
+ ...options,
35
+ "save-to": saveTo ?? options["save-to"],
36
+ "use-llm": useLlm ?? options["use-llm"],
37
+ "no-cache": noCache ?? options["no-cache"],
38
+ };
39
+ }
40
+
41
+ function hasLegacyFixtureSelector(
42
+ command: LegacyFixtureCommand,
43
+ input: NormalizedLegacyFixtureOptions,
44
+ ): boolean {
45
+ const hasInclude = input.include !== undefined && input.include.trim().length > 0;
46
+ return command === "explore" ? hasInclude : input.all === true || hasInclude;
47
+ }
48
+
49
+ export function normalizeLegacyFixtureCommandOptions(
50
+ command: LegacyFixtureCommand,
51
+ input: LegacyFixtureSource,
52
+ ): NormalizedLegacyFixtureOptions {
53
+ const normalized = normalizeLegacyFixtureOptions(input);
54
+ if (normalized.nonInteractive !== true) return normalized;
55
+
56
+ if (!hasLegacyFixtureSelector(command, normalized)) {
57
+ throw Object.assign(new Error(`Missing fixture ${command} selector.`), {
58
+ code: "MISSING_ARGUMENT",
59
+ exitCode: 2,
60
+ });
61
+ }
62
+
63
+ if (command !== "gen") return normalized;
64
+ return {
65
+ ...normalized,
66
+ count: normalized.count || 5,
67
+ "save-to": normalized["save-to"] || "db",
68
+ "use-llm": normalized["use-llm"] ?? false,
69
+ "no-cache": normalized["no-cache"] ?? false,
70
+ userMode: normalized.userMode ?? "dummy",
71
+ };
72
+ }
@@ -0,0 +1 @@
1
+ export { tooling } from "./cli-tooling";
@@ -0,0 +1,196 @@
1
+ import { z } from "zod";
2
+
3
+ import { loadConfig } from "../api/config";
4
+ import { findApiRootPath } from "../utils/utils";
5
+
6
+ export interface TestRunRequest {
7
+ files?: string[];
8
+ pattern?: string;
9
+ traces?: boolean;
10
+ }
11
+
12
+ interface TestRunResult {
13
+ ok: boolean;
14
+ summary: {
15
+ total: number;
16
+ passed: number;
17
+ failed: number;
18
+ skipped: number;
19
+ durationMs: number;
20
+ };
21
+ results: z.infer<typeof jsonValueSchema>[];
22
+ }
23
+
24
+ export interface TestRunClient {
25
+ status(): Promise<JsonObject>;
26
+ run(input: TestRunRequest): Promise<TestRunResult>;
27
+ }
28
+
29
+ interface TestRunClientDependencies {
30
+ projectRoot: string;
31
+ loadConfig(projectRoot: string): Promise<object>;
32
+ fetch: typeof globalThis.fetch;
33
+ }
34
+
35
+ class TestRunClientError extends Error {
36
+ constructor(
37
+ readonly code:
38
+ | "TEST_RUN_CONFIG_ERROR"
39
+ | "TEST_RUN_FAILED"
40
+ | "TEST_RUN_HTTP_ERROR"
41
+ | "TEST_RUN_NETWORK_ERROR",
42
+ message: string,
43
+ ) {
44
+ super(message);
45
+ this.name = "TestRunClientError";
46
+ }
47
+ }
48
+
49
+ const jsonValueSchema = z.json();
50
+ const jsonObjectSchema = z.record(z.string(), jsonValueSchema);
51
+ type JsonObject = z.infer<typeof jsonObjectSchema>;
52
+ const configSchema = z.object({
53
+ server: z
54
+ .object({
55
+ listen: z.object({ host: z.string().optional(), port: z.number().optional() }).optional(),
56
+ })
57
+ .optional(),
58
+ test: z
59
+ .object({
60
+ devRunner: z
61
+ .object({ enabled: z.boolean().optional(), routePrefix: z.string().optional() })
62
+ .optional(),
63
+ })
64
+ .optional(),
65
+ });
66
+ const testRunResultSchema = z.object({
67
+ ok: z.boolean(),
68
+ summary: z.object({
69
+ total: z.number(),
70
+ passed: z.number(),
71
+ failed: z.number(),
72
+ skipped: z.number(),
73
+ durationMs: z.number(),
74
+ }),
75
+ results: z.array(jsonValueSchema),
76
+ });
77
+
78
+ /**
79
+ * sonamu.config를 소스(`src/sonamu.config.ts`)에서 읽도록 강제해 설정을 불러옵니다.
80
+ *
81
+ * loadConfig는 VITEST/HOT 환경변수로 dist/소스 설정을 고르므로, dist 빌드가 없는
82
+ * 일반 개발 셸에서도 소스 설정을 읽도록 VITEST를 잠시 켜고 원래 값을 그대로 복원합니다.
83
+ */
84
+ async function loadSourceConfig(dependencies: TestRunClientDependencies): Promise<object> {
85
+ const previousVitest = process.env.VITEST;
86
+ process.env.VITEST = "true";
87
+ try {
88
+ return await dependencies.loadConfig(dependencies.projectRoot);
89
+ } finally {
90
+ if (previousVitest === undefined) {
91
+ delete process.env.VITEST;
92
+ } else {
93
+ process.env.VITEST = previousVitest;
94
+ }
95
+ }
96
+ }
97
+
98
+ async function resolveBaseUrl(dependencies: TestRunClientDependencies): Promise<string> {
99
+ let config: object;
100
+ try {
101
+ config = await loadSourceConfig(dependencies);
102
+ } catch {
103
+ throw new TestRunClientError(
104
+ "TEST_RUN_CONFIG_ERROR",
105
+ "sonamu.config를 불러올 수 없습니다. 프로젝트 설정을 확인하세요.",
106
+ );
107
+ }
108
+ const parsedConfig = configSchema.parse(config);
109
+ const listen = parsedConfig.server?.listen;
110
+ const devRunner = parsedConfig.test?.devRunner;
111
+ if (devRunner?.enabled !== true) {
112
+ throw new TestRunClientError(
113
+ "TEST_RUN_CONFIG_ERROR",
114
+ "sonamu.config의 test.devRunner.enabled를 true로 설정해야 합니다.",
115
+ );
116
+ }
117
+ const configuredHost = listen?.host ?? "localhost";
118
+ const host = ["0.0.0.0", "::"].includes(configuredHost) ? "127.0.0.1" : configuredHost;
119
+ const port = listen?.port ?? 3000;
120
+ const routePrefix = devRunner.routePrefix ?? "/__test__";
121
+ const prefix = (routePrefix.startsWith("/") ? routePrefix : `/${routePrefix}`).replace(/\/$/, "");
122
+ return `http://${host}:${port}${prefix}`;
123
+ }
124
+
125
+ async function requestJson(
126
+ dependencies: TestRunClientDependencies,
127
+ url: string,
128
+ init?: RequestInit,
129
+ ): Promise<JsonObject> {
130
+ let response: Response;
131
+ try {
132
+ response = await dependencies.fetch(url, init);
133
+ } catch {
134
+ throw new TestRunClientError(
135
+ "TEST_RUN_NETWORK_ERROR",
136
+ "로컬 DevRunner에 연결할 수 없습니다. sonamu dev 실행 상태를 확인하세요.",
137
+ );
138
+ }
139
+ if (!response.ok) {
140
+ throw new TestRunClientError(
141
+ "TEST_RUN_HTTP_ERROR",
142
+ `DevRunner 요청이 HTTP ${response.status} 상태로 실패했습니다.`,
143
+ );
144
+ }
145
+ const object = jsonObjectSchema.safeParse(await response.json());
146
+ if (!object.success) {
147
+ throw new TestRunClientError("TEST_RUN_HTTP_ERROR", "DevRunner 응답이 JSON 객체가 아닙니다.");
148
+ }
149
+ return object.data;
150
+ }
151
+
152
+ function parseResult(value: JsonObject): TestRunResult {
153
+ const result = testRunResultSchema.safeParse(value);
154
+ if (!result.success) {
155
+ throw new TestRunClientError(
156
+ "TEST_RUN_HTTP_ERROR",
157
+ "DevRunner 테스트 결과의 구조가 올바르지 않습니다.",
158
+ );
159
+ }
160
+ return result.data;
161
+ }
162
+
163
+ function createTestRunClient(dependencies: TestRunClientDependencies): TestRunClient {
164
+ return {
165
+ async status() {
166
+ return requestJson(dependencies, `${await resolveBaseUrl(dependencies)}/status`);
167
+ },
168
+ async run(input) {
169
+ const payload: TestRunRequest = {};
170
+ if (input.files !== undefined) payload.files = input.files;
171
+ if (input.pattern !== undefined) payload.pattern = input.pattern;
172
+ const result = parseResult(
173
+ await requestJson(dependencies, `${await resolveBaseUrl(dependencies)}/run`, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify(payload),
177
+ }),
178
+ );
179
+ if (!result.ok) {
180
+ throw Object.assign(
181
+ new TestRunClientError("TEST_RUN_FAILED", "테스트 실행이 실패했습니다."),
182
+ { exitCode: 1, result },
183
+ );
184
+ }
185
+ return result;
186
+ },
187
+ };
188
+ }
189
+
190
+ export function createDefaultTestRunClient(projectRoot: string = findApiRootPath()): TestRunClient {
191
+ return createTestRunClient({
192
+ projectRoot,
193
+ loadConfig,
194
+ fetch: globalThis.fetch,
195
+ });
196
+ }
@@ -0,0 +1,99 @@
1
+ import { constants } from "node:fs";
2
+ import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { afterEach, describe, expect, it, vi } from "vitest";
8
+ import { z } from "zod";
9
+
10
+ import { resolveOxlintBin } from "../formatter";
11
+
12
+ const OxlintManifestSchema = z
13
+ .union([
14
+ z.object({ bin: z.string().min(1) }),
15
+ z.object({ bin: z.object({ oxlint: z.string().min(1) }) }).transform(({ bin }) => ({
16
+ bin: bin.oxlint,
17
+ })),
18
+ ])
19
+ .transform(({ bin }) => ({ bin }));
20
+
21
+ type ResolveOxlintBinOptions = {
22
+ resolveModule: (specifier: string) => string;
23
+ readFile: (filePath: string) => Promise<string>;
24
+ access: (filePath: string, mode: number) => Promise<void>;
25
+ };
26
+
27
+ const tempRoots: string[] = [];
28
+
29
+ async function createTempRoot() {
30
+ const root = await mkdtemp(path.join(os.tmpdir(), "sonamu-oxlint-manifest-"));
31
+ tempRoots.push(root);
32
+ return root;
33
+ }
34
+
35
+ afterEach(async () => {
36
+ vi.restoreAllMocks();
37
+ await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
38
+ });
39
+
40
+ describe("oxlint 실행 파일 해석 회귀", () => {
41
+ it("설치된 패키지 manifest에서 실행 가능한 로컬 절대 경로를 반환한다", async () => {
42
+ const requireFromFormatter = createRequire(new URL("../formatter.ts", import.meta.url));
43
+ const manifestPath = requireFromFormatter.resolve("oxlint/package.json");
44
+ const manifest = OxlintManifestSchema.parse(JSON.parse(await readFile(manifestPath, "utf8")));
45
+ const expectedBin = path.resolve(path.dirname(manifestPath), manifest.bin);
46
+ const options: ResolveOxlintBinOptions = {
47
+ resolveModule: (specifier) => requireFromFormatter.resolve(specifier),
48
+ readFile: (filePath) => readFile(filePath, "utf8"),
49
+ access,
50
+ };
51
+
52
+ const resolvedBin = await resolveOxlintBin(options);
53
+
54
+ expect(resolvedBin).toBe(expectedBin);
55
+ expect(path.isAbsolute(resolvedBin)).toBe(true);
56
+ await expect(access(resolvedBin, constants.F_OK | constants.X_OK)).resolves.toBeUndefined();
57
+ });
58
+
59
+ it.each([
60
+ ["manifest 파일이 없음", null],
61
+ ["manifest JSON이 잘못됨", "{ invalid json"],
62
+ ["manifest에 bin 필드가 없음", JSON.stringify({ name: "oxlint" })],
63
+ ])("임시 패키지의 %s이면 해결 방법이 있는 오류를 반환한다", async (_caseName, manifest) => {
64
+ const packageRoot = await createTempRoot();
65
+ const manifestPath = path.join(packageRoot, "package.json");
66
+ if (manifest !== null) await writeFile(manifestPath, manifest);
67
+ const options: ResolveOxlintBinOptions = {
68
+ resolveModule: vi.fn(() => manifestPath),
69
+ readFile: vi.fn((filePath) => readFile(filePath, "utf8")),
70
+ access: vi.fn(access),
71
+ };
72
+
73
+ await expect(resolveOxlintBin(options)).rejects.toMatchObject({
74
+ code: "OXLINT_BIN_RESOLUTION_FAILED",
75
+ message: expect.stringMatching(/oxlint.*(?:package\.json|bin)/i),
76
+ });
77
+ expect(options.resolveModule).toHaveBeenCalledWith("oxlint/package.json");
78
+ expect(options.readFile).toHaveBeenCalledWith(manifestPath);
79
+ expect(options.access).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it("manifest가 가리키는 실행 파일이 없으면 실행 가능 여부를 검사하고 오류를 반환한다", async () => {
83
+ const packageRoot = await createTempRoot();
84
+ const manifestPath = path.join(packageRoot, "package.json");
85
+ const missingBinPath = path.join(packageRoot, "bin", "oxlint");
86
+ await writeFile(manifestPath, JSON.stringify({ bin: { oxlint: "bin/oxlint" } }));
87
+ const options: ResolveOxlintBinOptions = {
88
+ resolveModule: vi.fn(() => manifestPath),
89
+ readFile: vi.fn((filePath) => readFile(filePath, "utf8")),
90
+ access: vi.fn(access),
91
+ };
92
+
93
+ await expect(resolveOxlintBin(options)).rejects.toMatchObject({
94
+ code: "OXLINT_BIN_RESOLUTION_FAILED",
95
+ message: expect.stringMatching(/실행 파일.*없거나 실행할 수 없습니다/),
96
+ });
97
+ expect(options.access).toHaveBeenCalledWith(missingBinPath, constants.F_OK | constants.X_OK);
98
+ });
99
+ });
@@ -1,11 +1,13 @@
1
- import { createHash } from "crypto";
1
+ import { createHash } from "node:crypto";
2
2
  // 테스트 환경에서는 fs/promises가 mock되지만, 아래 runOxlint이 isTest 가드로 안 도니까
3
3
  // 그냥 fs/promises 그대로 사용. (production에서만 임시파일 흐름이 돕니다.)
4
- import { readFile, unlink, writeFile } from "fs/promises";
5
- import { createRequire } from "module";
6
- import path, { dirname, join } from "path";
4
+ import { constants } from "node:fs";
5
+ import { access, readFile, unlink, writeFile } from "node:fs/promises";
6
+ import { createRequire } from "node:module";
7
+ import path, { dirname, join } from "node:path";
7
8
 
8
9
  import { format, type FormatConfig } from "oxfmt";
10
+ import { z } from "zod";
9
11
 
10
12
  import { cached } from "./async-utils";
11
13
  import { isTest } from "./controller";
@@ -14,6 +16,17 @@ import { isNumberValue } from "./runtime-value";
14
16
 
15
17
  const requireFromHere = createRequire(import.meta.url);
16
18
 
19
+ export type ResolveOxlintBinOptions = {
20
+ resolveModule: (specifier: string) => string;
21
+ readFile: (filePath: string) => Promise<string>;
22
+ access: (filePath: string, mode: number) => Promise<void>;
23
+ };
24
+
25
+ const OxlintStringBinManifestSchema = z.object({ bin: z.string().trim().min(1) });
26
+ const OxlintNamedBinManifestSchema = z.object({
27
+ bin: z.object({ oxlint: z.string().trim().min(1) }),
28
+ });
29
+
17
30
  /**
18
31
  * 코드를 프로젝트의 oxfmt + oxlint 설정에 맞춰 포매팅한 문자열을 반환합니다.
19
32
  *
@@ -119,9 +132,13 @@ async function runOxlint(code: string): Promise<string> {
119
132
  await writeFile(tmpFile, code, "utf-8");
120
133
 
121
134
  try {
122
- await execute(resolveOxlintBin(), ["--fix", "--fix-suggestions", "--type-aware", tmpFile], {
123
- timeout: 10000,
124
- });
135
+ await execute(
136
+ await resolveOxlintBin(),
137
+ ["--fix", "--fix-suggestions", "--type-aware", tmpFile],
138
+ {
139
+ timeout: 10000,
140
+ },
141
+ );
125
142
  } catch (e) {
126
143
  // lint 위반 시 exit code != 0이지만 --fix는 적용됨. exec 자체 실패만 throw.
127
144
  // SAFETY: 선행 분기와 함수 계약이 이 타입을 보장합니다.
@@ -140,10 +157,57 @@ async function runOxlint(code: string): Promise<string> {
140
157
  }
141
158
  }
142
159
 
143
- function resolveOxlintBin(): string {
160
+ export async function resolveOxlintBin(
161
+ options: ResolveOxlintBinOptions = {
162
+ resolveModule: (specifier) => requireFromHere.resolve(specifier),
163
+ readFile: (filePath) => readFile(filePath, "utf-8"),
164
+ access: (filePath, mode) => access(filePath, mode),
165
+ },
166
+ ): Promise<string> {
167
+ let manifestPath: string;
168
+ try {
169
+ manifestPath = options.resolveModule("oxlint/package.json");
170
+ } catch {
171
+ throw createOxlintBinResolutionError(
172
+ "oxlint package.json을 찾을 수 없습니다. oxlint 의존성을 설치한 뒤 다시 시도해 주세요.",
173
+ );
174
+ }
175
+
176
+ let manifest: unknown;
177
+ try {
178
+ manifest = JSON.parse(await options.readFile(manifestPath));
179
+ } catch {
180
+ throw createOxlintBinResolutionError(
181
+ "oxlint package.json을 읽거나 해석할 수 없습니다. 패키지를 다시 설치한 뒤 시도해 주세요.",
182
+ );
183
+ }
184
+
185
+ // npm의 bin은 단일 실행 파일 문자열과 명령 이름별 객체를 모두 허용합니다.
186
+ const stringBinManifest = OxlintStringBinManifestSchema.safeParse(manifest);
187
+ const namedBinManifest = OxlintNamedBinManifestSchema.safeParse(manifest);
188
+ let bin: string | null = null;
189
+ if (stringBinManifest.success) {
190
+ bin = stringBinManifest.data.bin;
191
+ } else if (namedBinManifest.success) {
192
+ bin = namedBinManifest.data.bin.oxlint;
193
+ }
194
+ if (bin === null) {
195
+ throw createOxlintBinResolutionError(
196
+ "oxlint package.json의 bin 필드가 없거나 올바르지 않습니다. 호환되는 oxlint 패키지를 설치해 주세요.",
197
+ );
198
+ }
199
+
200
+ const binPath = path.resolve(dirname(manifestPath), bin);
144
201
  try {
145
- return requireFromHere.resolve("oxlint/bin/oxlint");
202
+ await options.access(binPath, constants.F_OK | constants.X_OK);
146
203
  } catch {
147
- return "oxlint";
204
+ throw createOxlintBinResolutionError(
205
+ "oxlint package.json의 bin 실행 파일이 없거나 실행할 수 없습니다. 패키지를 다시 설치해 주세요.",
206
+ );
148
207
  }
208
+
209
+ return binPath;
210
+ }
211
+ function createOxlintBinResolutionError(message: string): NodeJS.ErrnoException {
212
+ return Object.assign(new Error(message), { code: "OXLINT_BIN_RESOLUTION_FAILED" });
149
213
  }
@@ -1,8 +1,6 @@
1
- import { readdirSync, statSync } from "node:fs";
1
+ import { readdir, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { defineConfig } from "tsdown";
5
-
6
4
  const srcRoot = path.resolve(process.cwd(), "src");
7
5
  const ignoredSuffixes = [".test.ts", ".test-hold.ts", ".ignore.ts", ".d.ts"];
8
6
  const ignoredDirectories = new Set(["__mocks__", "_templates", "wasted_src"]);
@@ -10,19 +8,19 @@ interface BuildEntries {
10
8
  [entryName: string]: string;
11
9
  }
12
10
 
13
- function collectEntries(directory: string): BuildEntries {
11
+ async function collectEntries(directory: string): Promise<BuildEntries> {
14
12
  const entries: BuildEntries = {};
15
13
 
16
- for (const entry of readdirSync(directory)) {
14
+ for (const entry of await readdir(directory)) {
17
15
  if (ignoredDirectories.has(entry)) {
18
16
  continue;
19
17
  }
20
18
 
21
19
  const absolutePath = path.join(directory, entry);
22
- const stats = statSync(absolutePath);
20
+ const stats = await stat(absolutePath);
23
21
 
24
22
  if (stats.isDirectory()) {
25
- Object.assign(entries, collectEntries(absolutePath));
23
+ Object.assign(entries, await collectEntries(absolutePath));
26
24
  continue;
27
25
  }
28
26
 
@@ -41,13 +39,13 @@ function collectEntries(directory: string): BuildEntries {
41
39
  return entries;
42
40
  }
43
41
 
44
- export default defineConfig({
42
+ export default {
45
43
  clean: true,
46
44
  deps: {
47
45
  neverBundle: [/^sonamu(?:\/.*)?$/],
48
46
  },
49
47
  dts: false,
50
- entry: collectEntries(srcRoot),
48
+ entry: await collectEntries(srcRoot),
51
49
  fixedExtension: false,
52
50
  format: "esm",
53
51
  outDir: path.resolve(process.cwd(), "dist"),
@@ -56,4 +54,4 @@ export default defineConfig({
56
54
  target: "esnext",
57
55
  treeshake: false,
58
56
  unbundle: true,
59
- });
57
+ };
package/bin/cli.js DELETED
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn } from "child_process";
4
- import { createRequire } from "module";
5
- import { fileURLToPath } from "url";
6
-
7
- const require = createRequire(import.meta.url);
8
- const sourceCliPath = fileURLToPath(new URL("../src/bin/cli.ts", import.meta.url));
9
- const tsxImportPath = (() => {
10
- try {
11
- return require.resolve("tsx");
12
- } catch {
13
- return "tsx";
14
- }
15
- })();
16
-
17
- function printCliError(error, fallbackError) {
18
- console.error(
19
- "Sonamu CLI를 실행하는 과정에 문제가 발생하였습니다. 보통은 dist/bin/cli.js 파일이 없는 경우입니다만, 아래 에러 메시지를 자세히 읽어보시면 힌트를 얻으실 수 있을 것입니다.",
20
- );
21
- console.error(
22
- "There was an error while executing Sonamu CLI. Usually it's because dist/bin/cli.js file is not found. Please read the error message below for more information.",
23
- );
24
- console.error("=".repeat(80));
25
- console.error(error);
26
-
27
- if (fallbackError) {
28
- console.error("-".repeat(80));
29
- console.error("Source CLI fallback also failed.");
30
- console.error(fallbackError);
31
- }
32
- }
33
-
34
- function runSourceCli(originalError) {
35
- const child = spawn(
36
- process.execPath,
37
- ["--import", tsxImportPath, sourceCliPath, ...process.argv.slice(2)],
38
- {
39
- env: process.env,
40
- stdio: "inherit",
41
- },
42
- );
43
-
44
- child.on("error", (fallbackError) => {
45
- printCliError(originalError, fallbackError);
46
- process.exit(1);
47
- });
48
-
49
- child.on("exit", (code) => {
50
- if ((code ?? 1) !== 0) {
51
- printCliError(originalError);
52
- }
53
- process.exit(code ?? 1);
54
- });
55
- }
56
-
57
- /**
58
- * 이 스크립트는 터미널 또는 기타 환경에서 pnpm sonamu 또는 sonamu 명령어를 호출하였을 때 실행되는 스크립트입니다.
59
- *
60
- * sonamu 패키지 package.json의 bin 필드는 dist/bin/cli.js를 직접 가리키지 않습니다. 대신 이 스크립트를 가리킵니다.
61
- * 이는 개발 환경(TypeScript + pnpm workspace + clone 받으면 dist가 없음)에서 발생할 수 있는 문제를 해결하기 위함입니다.
62
- *
63
- * pnpm은 패키지 설치 시점에 bin 필드가 가리키는 파일이 존재하기를 요구합니다.
64
- * 가령 bin에 dist/bin/cli.js가 명시되어 있다면, sonamu를 install 하는 시점에 해당 파일을 pnpm이 찾아서 읽습니다.
65
- * 그런데 install 시점에 sonamu의 src는 있어도 dist는 없는 상황이 발생할 수 있습니다(주로 새로 clone 받아서 작업할 때).
66
- * 이 경우에는 pnpm이 install 시점에 해당 bin(=dist/bin/cli.js)을 처리하지 못 하여 sonamu 명령을 사용할 수 없는 상태가 됩니다.
67
- * 이렇게 된다면 sonamu를 빌드한 후에 다시 install을 해야 하는 불편함이 발생합니다.
68
- *
69
- * 이를 해결하기 위해 dist 존재 유무와 무관하게 **항상** 존재하는 이 wrapper 스크립트를 추가하였습니다.
70
- * pnpm이 실행하는 실제 스크립트는 이 스크립트이며, 이 스크립트는 dist/bin/cli.js를 동적으로 찾아서 실행(eval)해줍니다.
71
- * 만약 sonamu가 아직 빌드되지 않은 상태에서 sonamu 명령을 사용하려 하면 아래와 같이 console.error를 출력하고 process.exit(1)을 호출합니다.
72
- *
73
- * 이 스크립트를 실행시킬 때 넘어온 인자와 환경변수 등은 모두 dist/bin/cli.js에 그대로 전달됩니다.
74
- */
75
- import("../dist/bin/cli.js").catch(runSourceCli);
package/dist/bin/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=cli.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/bin/cli.ts"],"names":[],"mappings":""}