sonamu 0.9.5 → 0.9.6

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 (159) hide show
  1. package/dist/api/config.d.ts +13 -2
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +1 -1
  4. package/dist/api/context.d.ts +17 -7
  5. package/dist/api/context.d.ts.map +1 -1
  6. package/dist/api/context.js +1 -1
  7. package/dist/api/decorators.d.ts +18 -0
  8. package/dist/api/decorators.d.ts.map +1 -1
  9. package/dist/api/decorators.js +54 -3
  10. package/dist/api/index.js +8 -3
  11. package/dist/api/sonamu.d.ts +24 -9
  12. package/dist/api/sonamu.d.ts.map +1 -1
  13. package/dist/api/sonamu.js +365 -79
  14. package/dist/api/websocket-helpers.d.ts +24 -0
  15. package/dist/api/websocket-helpers.d.ts.map +1 -0
  16. package/dist/api/websocket-helpers.js +77 -0
  17. package/dist/bin/cli.js +12 -4
  18. package/dist/dict/sonamu-dictionary.js +5 -5
  19. package/dist/entity/entity-manager.js +1 -1
  20. package/dist/entity/entity.js +3 -3
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +16 -4
  24. package/dist/migration/code-generation.js +7 -7
  25. package/dist/stream/index.d.ts +6 -0
  26. package/dist/stream/index.d.ts.map +1 -1
  27. package/dist/stream/index.js +13 -2
  28. package/dist/stream/ws-audience-resolver.d.ts +15 -0
  29. package/dist/stream/ws-audience-resolver.d.ts.map +1 -0
  30. package/dist/stream/ws-audience-resolver.js +31 -0
  31. package/dist/stream/ws-audience.d.ts +28 -0
  32. package/dist/stream/ws-audience.d.ts.map +1 -0
  33. package/dist/stream/ws-audience.js +46 -0
  34. package/dist/stream/ws-cluster-bus.d.ts +23 -0
  35. package/dist/stream/ws-cluster-bus.d.ts.map +1 -0
  36. package/dist/stream/ws-cluster-bus.js +18 -0
  37. package/dist/stream/ws-core.d.ts +15 -0
  38. package/dist/stream/ws-core.d.ts.map +1 -0
  39. package/dist/stream/ws-core.js +1 -0
  40. package/dist/stream/ws-delivery.d.ts +24 -0
  41. package/dist/stream/ws-delivery.d.ts.map +1 -0
  42. package/dist/stream/ws-delivery.js +103 -0
  43. package/dist/stream/ws-local-connection-store.d.ts +10 -0
  44. package/dist/stream/ws-local-connection-store.d.ts.map +1 -0
  45. package/dist/stream/ws-local-connection-store.js +44 -0
  46. package/dist/stream/ws-presence-store.d.ts +61 -0
  47. package/dist/stream/ws-presence-store.d.ts.map +1 -0
  48. package/dist/stream/ws-presence-store.js +236 -0
  49. package/dist/stream/ws-registry.d.ts +42 -0
  50. package/dist/stream/ws-registry.d.ts.map +1 -0
  51. package/dist/stream/ws-registry.js +108 -0
  52. package/dist/stream/ws.d.ts +52 -0
  53. package/dist/stream/ws.d.ts.map +1 -0
  54. package/dist/stream/ws.js +397 -0
  55. package/dist/syncer/api-parser.d.ts.map +1 -1
  56. package/dist/syncer/api-parser.js +72 -2
  57. package/dist/syncer/checksum.d.ts.map +1 -1
  58. package/dist/syncer/checksum.js +13 -12
  59. package/dist/syncer/code-generator.d.ts.map +1 -1
  60. package/dist/syncer/code-generator.js +7 -4
  61. package/dist/syncer/event-batcher.d.ts +27 -0
  62. package/dist/syncer/event-batcher.d.ts.map +1 -0
  63. package/dist/syncer/event-batcher.js +69 -0
  64. package/dist/syncer/file-patterns.d.ts +48 -26
  65. package/dist/syncer/file-patterns.d.ts.map +1 -1
  66. package/dist/syncer/file-patterns.js +71 -23
  67. package/dist/syncer/file-tracking.d.ts +13 -0
  68. package/dist/syncer/file-tracking.d.ts.map +1 -0
  69. package/dist/syncer/file-tracking.js +33 -0
  70. package/dist/syncer/index.js +2 -2
  71. package/dist/syncer/module-loader.d.ts +2 -11
  72. package/dist/syncer/module-loader.d.ts.map +1 -1
  73. package/dist/syncer/module-loader.js +3 -3
  74. package/dist/syncer/syncer-actions.d.ts +39 -6
  75. package/dist/syncer/syncer-actions.d.ts.map +1 -1
  76. package/dist/syncer/syncer-actions.js +125 -10
  77. package/dist/syncer/syncer.d.ts +33 -19
  78. package/dist/syncer/syncer.d.ts.map +1 -1
  79. package/dist/syncer/syncer.js +168 -168
  80. package/dist/syncer/watcher.d.ts +8 -0
  81. package/dist/syncer/watcher.d.ts.map +1 -0
  82. package/dist/syncer/watcher.js +105 -0
  83. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  84. package/dist/tasks/workflow-manager.js +2 -1
  85. package/dist/template/implementations/services.template.d.ts.map +1 -1
  86. package/dist/template/implementations/services.template.js +36 -1
  87. package/dist/testing/bootstrap.d.ts.map +1 -1
  88. package/dist/testing/bootstrap.js +8 -1
  89. package/dist/testing/fixture-manager.js +1 -1
  90. package/dist/types/types.d.ts +2 -1
  91. package/dist/types/types.d.ts.map +1 -1
  92. package/dist/types/types.js +2 -2
  93. package/dist/ui/api.js +1 -1
  94. package/dist/ui/cdd-service.js +1 -1
  95. package/dist/ui-web/assets/{index-DzZ7vBk4.js → index-BmThfg-s.js} +37 -37
  96. package/dist/ui-web/index.html +1 -1
  97. package/dist/utils/async-utils.d.ts +27 -3
  98. package/dist/utils/async-utils.d.ts.map +1 -1
  99. package/dist/utils/async-utils.js +56 -6
  100. package/dist/utils/formatter.d.ts +7 -1
  101. package/dist/utils/formatter.d.ts.map +1 -1
  102. package/dist/utils/formatter.js +95 -60
  103. package/dist/utils/fs-utils.d.ts +2 -0
  104. package/dist/utils/fs-utils.d.ts.map +1 -1
  105. package/dist/utils/fs-utils.js +10 -2
  106. package/dist/utils/process-utils.d.ts +6 -0
  107. package/dist/utils/process-utils.d.ts.map +1 -1
  108. package/dist/utils/process-utils.js +16 -3
  109. package/dist/utils/utils.d.ts +1 -0
  110. package/dist/utils/utils.d.ts.map +1 -1
  111. package/dist/utils/utils.js +2 -2
  112. package/package.json +4 -2
  113. package/src/api/__tests__/sonamu.websocket.test.ts +64 -0
  114. package/src/api/__tests__/websocket-context.types.test.ts +58 -0
  115. package/src/api/config.ts +28 -2
  116. package/src/api/context.ts +21 -7
  117. package/src/api/decorators.ts +101 -1
  118. package/src/api/sonamu.ts +529 -127
  119. package/src/api/websocket-helpers.ts +122 -0
  120. package/src/bin/cli.ts +10 -2
  121. package/src/dict/sonamu-dictionary.ts +2 -2
  122. package/src/entity/entity.ts +1 -1
  123. package/src/index.ts +6 -0
  124. package/src/migration/code-generation.ts +5 -5
  125. package/src/shared/app.shared.ts.txt +254 -1
  126. package/src/shared/web.shared.ts.txt +282 -1
  127. package/src/stream/__tests__/ws-contracts.test.ts +381 -0
  128. package/src/stream/__tests__/ws.test.ts +449 -0
  129. package/src/stream/index.ts +6 -0
  130. package/src/stream/ws-audience-resolver.ts +35 -0
  131. package/src/stream/ws-audience.ts +62 -0
  132. package/src/stream/ws-cluster-bus.ts +32 -0
  133. package/src/stream/ws-core.ts +16 -0
  134. package/src/stream/ws-delivery.ts +138 -0
  135. package/src/stream/ws-local-connection-store.ts +44 -0
  136. package/src/stream/ws-presence-store.ts +326 -0
  137. package/src/stream/ws-registry.ts +138 -0
  138. package/src/stream/ws.ts +591 -0
  139. package/src/syncer/__tests__/api-parser.websocket-type-ref.test.ts +78 -0
  140. package/src/syncer/api-parser.ts +112 -1
  141. package/src/syncer/checksum.ts +23 -29
  142. package/src/syncer/code-generator.ts +4 -1
  143. package/src/syncer/event-batcher.ts +72 -0
  144. package/src/syncer/file-patterns.ts +98 -30
  145. package/src/syncer/file-tracking.ts +27 -0
  146. package/src/syncer/module-loader.ts +5 -12
  147. package/src/syncer/syncer-actions.ts +179 -17
  148. package/src/syncer/syncer.ts +250 -287
  149. package/src/syncer/watcher.ts +128 -0
  150. package/src/tasks/workflow-manager.ts +1 -0
  151. package/src/template/__tests__/services.template.websocket.test.ts +79 -0
  152. package/src/template/implementations/services.template.ts +69 -0
  153. package/src/testing/bootstrap.ts +8 -1
  154. package/src/types/types.ts +20 -2
  155. package/src/utils/async-utils.ts +71 -4
  156. package/src/utils/formatter.ts +114 -75
  157. package/src/utils/fs-utils.ts +9 -0
  158. package/src/utils/process-utils.ts +17 -0
  159. package/src/utils/utils.ts +1 -1
@@ -1,108 +1,147 @@
1
- import { execFileSync } from "child_process";
2
- import { readFileSync, unlinkSync, writeFileSync } from "fs";
1
+ import { createHash } from "crypto";
2
+ // 테스트 환경에서는 fs/promises가 mock되지만, 아래 runOxlint이 isTest 가드로 안 도니까
3
+ // 그냥 fs/promises 그대로 사용. (production에서만 임시파일 흐름이 돕니다.)
4
+ import { readFile, unlink, writeFile } from "fs/promises";
3
5
  import { createRequire } from "module";
4
- import { join } from "path";
6
+ import { tmpdir } from "os";
7
+ import path, { dirname, join } from "path";
5
8
 
6
- import { format } from "oxfmt";
9
+ import { format, type FormatConfig } from "oxfmt";
7
10
 
11
+ import { cached } from "./async-utils";
8
12
  import { isTest } from "./controller";
13
+ import { execute } from "./process-utils";
9
14
 
10
15
  const _require = createRequire(import.meta.url);
11
16
 
12
- const OXFMT_OPTIONS = {
13
- printWidth: 100,
14
- tabWidth: 2,
15
- useTabs: false,
16
- singleQuote: false,
17
- jsxSingleQuote: false,
18
- trailingComma: "all" as const,
19
- semi: true,
20
- endOfLine: "lf" as const,
21
- bracketSpacing: true,
22
- sortImports: true,
23
- };
17
+ /**
18
+ * 코드를 프로젝트의 oxfmt + oxlint 설정에 맞춰 포매팅한 문자열을 반환합니다.
19
+ *
20
+ * 캐싱도 있어요 ㅎㅎ 똑같은 입력에 대해서 캐시 커버됩니다.
21
+ * 수명은 프로세스 죽을때까지 ㅋ
22
+ */
23
+ export const formatCode = cached(formatCodeInternal, (code, filePath) => {
24
+ const ext = filePath.endsWith(".tsx") ? "tsx" : filePath.endsWith("json") ? "json" : "ts";
25
+ return `${ext}:${createHash("sha1").update(code).digest("hex")}`;
26
+ });
24
27
 
25
- function resolveOxlintBin(): string {
26
- try {
27
- return _require.resolve("oxlint/bin/oxlint");
28
- } catch {
29
- return "oxlint";
28
+ /**
29
+ * 캐시 없는 포맷함수 엔트리.
30
+ */
31
+ async function formatCodeInternal(code: string, filePath: string): Promise<string> {
32
+ // json은 포맷만 하면 됩니다.
33
+ if (filePath.endsWith("json")) {
34
+ return runOxfmt(code, filePath);
30
35
  }
36
+
37
+ // 린트 먼저 한 다음에 포맷으로 마무리해요.
38
+ return runOxfmt(await runOxlint(code), filePath);
31
39
  }
32
40
 
33
- export async function formatCode(
34
- code: string,
35
- parser: "typescript" | "json",
36
- _filePath: string,
37
- ): Promise<string> {
38
- const fileName = parser === "json" ? "file.json" : "file.ts";
39
-
40
- // oxfmt 포맷팅
41
- const formatted = await format(fileName, code, OXFMT_OPTIONS);
42
- if (formatted.errors.length > 0) {
43
- const errorMessages = formatted.errors
44
- .filter((e) => e.severity === "Error")
45
- .map((e) => e.message);
46
- if (errorMessages.length > 0) {
47
- // 파싱 에러가 있는 코드는 포맷팅 없이 원본 반환 (Biome formatWithErrors: false와 동일)
48
- return code;
41
+ /**
42
+ * 프로젝트 설정을 찾아서 이에 맞춰서 코드를 포맷합니다.
43
+ */
44
+ async function runOxfmt(code: string, filePath: string): Promise<string> {
45
+ const result = await format(path.basename(filePath), code, await loadOxfmtConfig());
46
+
47
+ const errors = result.errors.filter((e) => e.severity === "Error");
48
+ if (errors.length > 0) {
49
+ if (!isTest()) {
50
+ console.error(`oxfmt errors (${filePath}):`);
51
+ for (const err of errors) {
52
+ const label = err.labels[0];
53
+ if (label) {
54
+ const before = code.slice(Math.max(0, label.start - 80), label.start);
55
+ const at = code.slice(label.start, label.end);
56
+ const after = code.slice(label.end, Math.min(code.length, label.end + 80));
57
+ console.error(` - ${err.message} (offset ${label.start}-${label.end})`);
58
+ console.error(` around: ...${before}»${at}«${after}...`);
59
+ } else {
60
+ console.error(` - ${err.message}`);
61
+ }
62
+ }
63
+ }
64
+ return code;
65
+ }
66
+ return result.code;
67
+ }
68
+
69
+ let cachedOxfmtConfig: FormatConfig | null = null;
70
+ async function loadOxfmtConfig(): Promise<FormatConfig> {
71
+ if (cachedOxfmtConfig !== null) {
72
+ return cachedOxfmtConfig;
73
+ }
74
+
75
+ let dir = process.cwd();
76
+ while (true) {
77
+ const candidate = join(dir, ".oxfmtrc.json");
78
+ try {
79
+ cachedOxfmtConfig = JSON.parse(await readFile(candidate, "utf-8")) as FormatConfig;
80
+ return cachedOxfmtConfig;
81
+ } catch (e) {
82
+ if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
83
+ !isTest() && console.error(`Failed to load ${candidate}:`, e);
84
+ break;
85
+ }
49
86
  }
87
+ const parent = dirname(dir);
88
+ if (parent === dir) break;
89
+ dir = parent;
50
90
  }
51
91
 
52
- // JSON은 포맷팅만 수행
53
- if (parser === "json") {
54
- return formatted.code;
92
+ cachedOxfmtConfig = {};
93
+ return cachedOxfmtConfig;
94
+ }
95
+
96
+ /**
97
+ * 프로젝트 설정에 맞춰 코드를 lint합니다.
98
+ *
99
+ * 프로젝트 설정을 적용받는 oxlint cli를 찾아 띄워서,
100
+ * 임시 파일에 in-place로 써서 그 결과를 빼오는 방식으로 작동합니다.
101
+ * 왜 이렇게 하느냐? oxlint가 node api도 안 주고 cli에서 stdin 옵션도 안 주기 때문...
102
+ */
103
+ async function runOxlint(code: string): Promise<string> {
104
+ if (isTest()) {
105
+ // 테스트 환경에서는 느려지기만 하고 검증할 가치도 없어서 안 합니다.
106
+ // GitHub Actions 환경에서 lint가 오래 걸려서 뻗기도 했어요. (https://github.com/cartanova-ai/sonamu/actions/runs/25267214027/job/74083630169)
107
+ return code;
55
108
  }
56
109
 
57
- // TypeScript: oxlint --fix로 lint fix 수행 (unused import 제거, type import 변환 등)
58
- // cwd 아래에 생성해야 nested config(.oxlintrc.json)과 tsconfig를 찾을있음
110
+ // OS tmp dir에 두는 이유: process.cwd()가 watcher 스코프(api/src 아래)일 가능성을 차단합니다.
111
+ // cwd 위치라면 write/unlink가 짧은 순간 watcher 이벤트로 잡혀 batch에 흘러들어갈 있어요.
59
112
  const tmpFile = join(
60
- process.cwd(),
113
+ tmpdir(),
61
114
  `.sonamu-fmt-${Date.now()}-${Math.random().toString(36).slice(2)}.ts`,
62
115
  );
116
+
63
117
  try {
64
- writeFileSync(tmpFile, formatted.code, "utf-8");
118
+ await writeFile(tmpFile, code, "utf-8");
65
119
 
66
- const oxlintBin = resolveOxlintBin();
67
120
  try {
68
- execFileSync(oxlintBin, ["--fix", "--fix-suggestions", "--type-aware", tmpFile], {
69
- stdio: "pipe",
121
+ await execute(resolveOxlintBin(), ["--fix", "--fix-suggestions", "--type-aware", tmpFile], {
70
122
  timeout: 10000,
71
123
  });
72
- } catch (execError: unknown) {
73
- // oxlint은 lint 에러가 있으면 exit code != 0으로 종료하지만 --fix는 적용됨
74
- if (execError instanceof Error) {
75
- const errObj = execError as Error & { status?: number | null; code?: string };
76
- if (typeof errObj.status === "number") {
77
- void errObj.status;
78
- } else {
79
- throw execError;
80
- }
81
- } else {
82
- throw execError;
83
- }
84
- }
85
-
86
- const lintFixed = readFileSync(tmpFile, "utf-8");
87
-
88
- // lint fix 후 재포맷 (import 구문 변경으로 인한 정렬 등)
89
- const reformatted = await format(fileName, lintFixed, OXFMT_OPTIONS);
90
- if (reformatted.errors.length > 0) {
91
- const errorMessages = reformatted.errors
92
- .filter((e) => e.severity === "Error")
93
- .map((e) => e.message);
94
- if (errorMessages.length > 0) {
95
- !isTest() && console.error("oxfmt reformat errors:", errorMessages);
96
- throw new Error(`oxfmt reformat error: ${errorMessages.join(", ")}`);
124
+ } catch (e) {
125
+ // lint 위반 exit code != 0이지만 --fix는 적용됨. exec 자체 실패만 throw.
126
+ if (typeof (e as Error & { code?: number }).code !== "number") {
127
+ throw e;
97
128
  }
98
129
  }
99
130
 
100
- return reformatted.code;
131
+ return await readFile(tmpFile, "utf-8");
101
132
  } finally {
102
133
  try {
103
- unlinkSync(tmpFile);
134
+ await unlink(tmpFile);
104
135
  } catch {
105
- // 임시 파일 정리 실패는 무시
136
+ // 삭제 실패해도 어차피 ignore됨.
106
137
  }
107
138
  }
108
139
  }
140
+
141
+ function resolveOxlintBin(): string {
142
+ try {
143
+ return _require.resolve("oxlint/bin/oxlint");
144
+ } catch {
145
+ return "oxlint";
146
+ }
147
+ }
@@ -3,6 +3,8 @@ import { type PathLike } from "fs";
3
3
  import { access, readFile, stat, writeFile } from "fs/promises";
4
4
  import path, { dirname } from "path";
5
5
 
6
+ import { formatCode } from "./formatter";
7
+
6
8
  /**
7
9
  * fs/promises에는 exists가 없어요. 대신 access가 있습니다.
8
10
  * 근데 얘는 인터페이스가 쓰기 불편해요. 그래서 감싸주었습니다.
@@ -37,6 +39,8 @@ export async function fileExists(path: PathLike): Promise<boolean> {
37
39
  * - services/user/user.types.ts → ../sonamu.shared
38
40
  * - i18n/ko.ts → ../services/sonamu.shared
39
41
  *
42
+ * ts/tsx 파일은 쓰기 전에 oxfmt/oxlint 한번 돌려줍니다!
43
+ *
40
44
  * @param fromPath 원본 파일 경로
41
45
  * @param toPath 대상 파일 경로
42
46
  * @param syncHeader 동기화 시 파일 최상단에 삽입할 주석 블록. 기존 @generated 블록이 있으면 교체하고, 없으면 최상단에 추가합니다.
@@ -83,6 +87,11 @@ export async function copyFileWithReplaceCoreToShared(
83
87
  }
84
88
  }
85
89
 
90
+ // .ts/.tsx 산출물은 쓰기 전에 포맷도 해줘요 ㅎㅎ
91
+ if (toPath.endsWith(".ts") || toPath.endsWith(".tsx")) {
92
+ newFileContent = await formatCode(newFileContent, toPath);
93
+ }
94
+
86
95
  await writeFile(toPath, newFileContent);
87
96
  return true;
88
97
  }
@@ -1,7 +1,24 @@
1
+ import { execFile, type ExecFileOptions } from "child_process";
1
2
  import { setTimeout as setTimeoutPromises } from "timers/promises";
3
+ import { promisify } from "util";
2
4
 
3
5
  import chalk from "chalk";
4
6
 
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ /**
10
+ * exexFileSync의 비동기 버전입니다.
11
+ * exit code가 non-zero이면 reject해요.
12
+ */
13
+ export async function execute(
14
+ bin: string,
15
+ args: string[],
16
+ options?: ExecFileOptions,
17
+ ): Promise<string> {
18
+ const { stdout } = await execFileAsync(bin, args, options);
19
+ return typeof stdout === "string" ? stdout : stdout.toString();
20
+ }
21
+
5
22
  /**
6
23
  * 주어진 작업을 실행합니다.
7
24
  * 주어진 프로세스 이벤트(=시그널)가 발생하였을 때에도 최대 한계(delayBeforeShutdown) 동안 작업을 기다린 후 종료합니다.
@@ -118,7 +118,7 @@ export function merge<T extends Record<string, any>>(defaultObj: T, userObj: T):
118
118
 
119
119
  // plain object 판별 헬퍼 함수
120
120
  // (배열, null, Date 등을 제외한 순수 객체만 true)
121
- function isPlainObject(value: unknown): value is Record<string, unknown> {
121
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
122
122
  return (
123
123
  value !== null &&
124
124
  typeof value === "object" &&