sonamu 0.9.4 → 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.
- package/dist/ai/providers/rtzr/utils.js +2 -2
- package/dist/api/config.d.ts +13 -2
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/context.d.ts +17 -7
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/context.js +1 -1
- package/dist/api/decorators.d.ts +18 -0
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +54 -3
- package/dist/api/index.js +8 -3
- package/dist/api/sonamu.d.ts +24 -9
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +365 -79
- package/dist/api/websocket-helpers.d.ts +24 -0
- package/dist/api/websocket-helpers.d.ts.map +1 -0
- package/dist/api/websocket-helpers.js +77 -0
- package/dist/bin/cli.js +12 -4
- package/dist/database/upsert-builder.js +4 -4
- package/dist/dict/sonamu-dictionary.js +6 -6
- package/dist/entity/entity-manager.js +1 -1
- package/dist/entity/entity.js +3 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +8 -9
- package/dist/stream/index.d.ts +6 -0
- package/dist/stream/index.d.ts.map +1 -1
- package/dist/stream/index.js +13 -2
- package/dist/stream/ws-audience-resolver.d.ts +15 -0
- package/dist/stream/ws-audience-resolver.d.ts.map +1 -0
- package/dist/stream/ws-audience-resolver.js +31 -0
- package/dist/stream/ws-audience.d.ts +28 -0
- package/dist/stream/ws-audience.d.ts.map +1 -0
- package/dist/stream/ws-audience.js +46 -0
- package/dist/stream/ws-cluster-bus.d.ts +23 -0
- package/dist/stream/ws-cluster-bus.d.ts.map +1 -0
- package/dist/stream/ws-cluster-bus.js +18 -0
- package/dist/stream/ws-core.d.ts +15 -0
- package/dist/stream/ws-core.d.ts.map +1 -0
- package/dist/stream/ws-core.js +1 -0
- package/dist/stream/ws-delivery.d.ts +24 -0
- package/dist/stream/ws-delivery.d.ts.map +1 -0
- package/dist/stream/ws-delivery.js +103 -0
- package/dist/stream/ws-local-connection-store.d.ts +10 -0
- package/dist/stream/ws-local-connection-store.d.ts.map +1 -0
- package/dist/stream/ws-local-connection-store.js +44 -0
- package/dist/stream/ws-presence-store.d.ts +61 -0
- package/dist/stream/ws-presence-store.d.ts.map +1 -0
- package/dist/stream/ws-presence-store.js +236 -0
- package/dist/stream/ws-registry.d.ts +42 -0
- package/dist/stream/ws-registry.d.ts.map +1 -0
- package/dist/stream/ws-registry.js +108 -0
- package/dist/stream/ws.d.ts +52 -0
- package/dist/stream/ws.d.ts.map +1 -0
- package/dist/stream/ws.js +397 -0
- package/dist/syncer/api-parser.d.ts.map +1 -1
- package/dist/syncer/api-parser.js +72 -2
- package/dist/syncer/checksum.d.ts.map +1 -1
- package/dist/syncer/checksum.js +13 -12
- package/dist/syncer/code-generator.d.ts.map +1 -1
- package/dist/syncer/code-generator.js +7 -4
- package/dist/syncer/event-batcher.d.ts +27 -0
- package/dist/syncer/event-batcher.d.ts.map +1 -0
- package/dist/syncer/event-batcher.js +69 -0
- package/dist/syncer/file-patterns.d.ts +48 -26
- package/dist/syncer/file-patterns.d.ts.map +1 -1
- package/dist/syncer/file-patterns.js +71 -23
- package/dist/syncer/file-tracking.d.ts +13 -0
- package/dist/syncer/file-tracking.d.ts.map +1 -0
- package/dist/syncer/file-tracking.js +33 -0
- package/dist/syncer/index.js +2 -2
- package/dist/syncer/module-loader.d.ts +2 -11
- package/dist/syncer/module-loader.d.ts.map +1 -1
- package/dist/syncer/module-loader.js +3 -3
- package/dist/syncer/syncer-actions.d.ts +39 -6
- package/dist/syncer/syncer-actions.d.ts.map +1 -1
- package/dist/syncer/syncer-actions.js +125 -10
- package/dist/syncer/syncer.d.ts +33 -19
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +168 -168
- package/dist/syncer/watcher.d.ts +8 -0
- package/dist/syncer/watcher.d.ts.map +1 -0
- package/dist/syncer/watcher.js +105 -0
- package/dist/tasks/workflow-manager.d.ts.map +1 -1
- package/dist/tasks/workflow-manager.js +2 -1
- package/dist/template/implementations/services.template.d.ts.map +1 -1
- package/dist/template/implementations/services.template.js +36 -1
- package/dist/testing/bootstrap.d.ts.map +1 -1
- package/dist/testing/bootstrap.js +8 -1
- package/dist/testing/data-explorer.d.ts.map +1 -1
- package/dist/testing/data-explorer.js +5 -3
- package/dist/testing/fixture-manager.js +1 -1
- package/dist/types/types.d.ts +2 -1
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +2 -2
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +4 -3
- package/dist/ui/cdd-service.js +1 -1
- package/dist/ui-web/assets/{index-C5KUjXm0.js → index-BmThfg-s.js} +39 -39
- package/dist/ui-web/assets/index-D4rYm-Xz.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/dist/utils/async-utils.d.ts +27 -3
- package/dist/utils/async-utils.d.ts.map +1 -1
- package/dist/utils/async-utils.js +56 -6
- package/dist/utils/formatter.d.ts +7 -1
- package/dist/utils/formatter.d.ts.map +1 -1
- package/dist/utils/formatter.js +95 -60
- package/dist/utils/fs-utils.d.ts +2 -0
- package/dist/utils/fs-utils.d.ts.map +1 -1
- package/dist/utils/fs-utils.js +10 -2
- package/dist/utils/process-utils.d.ts +6 -0
- package/dist/utils/process-utils.d.ts.map +1 -1
- package/dist/utils/process-utils.js +16 -3
- package/dist/utils/utils.d.ts +1 -0
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +2 -2
- package/package.json +7 -5
- package/src/ai/providers/rtzr/utils.ts +1 -1
- package/src/api/__tests__/sonamu.websocket.test.ts +64 -0
- package/src/api/__tests__/websocket-context.types.test.ts +58 -0
- package/src/api/config.ts +28 -2
- package/src/api/context.ts +21 -7
- package/src/api/decorators.ts +101 -1
- package/src/api/sonamu.ts +529 -127
- package/src/api/websocket-helpers.ts +122 -0
- package/src/bin/cli.ts +10 -2
- package/src/database/upsert-builder.ts +3 -3
- package/src/dict/sonamu-dictionary.ts +3 -3
- package/src/entity/entity.ts +1 -1
- package/src/index.ts +6 -0
- package/src/migration/code-generation.ts +6 -11
- package/src/shared/app.shared.ts.txt +312 -4
- package/src/shared/web.shared.ts.txt +340 -4
- package/src/stream/__tests__/ws-contracts.test.ts +381 -0
- package/src/stream/__tests__/ws.test.ts +449 -0
- package/src/stream/index.ts +6 -0
- package/src/stream/ws-audience-resolver.ts +35 -0
- package/src/stream/ws-audience.ts +62 -0
- package/src/stream/ws-cluster-bus.ts +32 -0
- package/src/stream/ws-core.ts +16 -0
- package/src/stream/ws-delivery.ts +138 -0
- package/src/stream/ws-local-connection-store.ts +44 -0
- package/src/stream/ws-presence-store.ts +326 -0
- package/src/stream/ws-registry.ts +138 -0
- package/src/stream/ws.ts +591 -0
- package/src/syncer/__tests__/api-parser.websocket-type-ref.test.ts +78 -0
- package/src/syncer/api-parser.ts +112 -1
- package/src/syncer/checksum.ts +23 -29
- package/src/syncer/code-generator.ts +4 -1
- package/src/syncer/event-batcher.ts +72 -0
- package/src/syncer/file-patterns.ts +98 -30
- package/src/syncer/file-tracking.ts +27 -0
- package/src/syncer/module-loader.ts +5 -12
- package/src/syncer/syncer-actions.ts +179 -17
- package/src/syncer/syncer.ts +250 -287
- package/src/syncer/watcher.ts +128 -0
- package/src/tasks/workflow-manager.ts +1 -0
- package/src/template/__tests__/services.template.websocket.test.ts +79 -0
- package/src/template/implementations/services.template.ts +69 -0
- package/src/testing/bootstrap.ts +8 -1
- package/src/testing/data-explorer.ts +3 -2
- package/src/types/types.ts +20 -2
- package/src/ui/api.ts +10 -1
- package/src/utils/async-utils.ts +71 -4
- package/src/utils/formatter.ts +114 -75
- package/src/utils/fs-utils.ts +9 -0
- package/src/utils/process-utils.ts +17 -0
- package/src/utils/utils.ts +1 -1
- package/dist/ui-web/assets/index-Dr8pRJC_.css +0 -1
package/src/syncer/api-parser.ts
CHANGED
|
@@ -6,10 +6,24 @@ import ts from "typescript";
|
|
|
6
6
|
|
|
7
7
|
import { registeredApis } from "../api/decorators";
|
|
8
8
|
import { type ExtendedApi } from "../api/decorators";
|
|
9
|
+
import { type ResolvedWebSocketDecoratorOptions } from "../api/decorators";
|
|
9
10
|
import { validateMethodName } from "../api/validator";
|
|
10
11
|
import { type ApiParam, type ApiParamType } from "../types/types";
|
|
11
12
|
import { type AbsolutePath } from "../utils/path-utils";
|
|
12
13
|
|
|
14
|
+
type WebSocketTypeRefs = Pick<
|
|
15
|
+
ResolvedWebSocketDecoratorOptions,
|
|
16
|
+
"outEventsTypeRef" | "inEventsTypeRef"
|
|
17
|
+
>;
|
|
18
|
+
type ParsedMethod = {
|
|
19
|
+
modelName: string;
|
|
20
|
+
methodName: string;
|
|
21
|
+
typeParameters: ApiParamType.TypeParam[];
|
|
22
|
+
parameters: ApiParam[];
|
|
23
|
+
returnType: ApiParamType;
|
|
24
|
+
websocketTypeRefs: WebSocketTypeRefs;
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* TypeScript 파일을 파싱하여 API 메소드 정보를 추출합니다.
|
|
15
29
|
* @api 데코레이터가 붙은 메소드들의 타입 정보를 분석합니다.
|
|
@@ -29,7 +43,7 @@ export async function readApisFromFile(filePath: AbsolutePath): Promise<Extended
|
|
|
29
43
|
ts.ScriptTarget.Latest,
|
|
30
44
|
);
|
|
31
45
|
|
|
32
|
-
const methods:
|
|
46
|
+
const methods: ParsedMethod[] = [];
|
|
33
47
|
let modelName: string = "UnknownModel";
|
|
34
48
|
let methodName: string = "unknownMethod";
|
|
35
49
|
const visitor = (node: ts.Node) => {
|
|
@@ -74,6 +88,7 @@ export async function readApisFromFile(filePath: AbsolutePath): Promise<Extended
|
|
|
74
88
|
throw new Error(`리턴 타입이 기재되지 않은 메소드 ${modelName}.${methodName}`);
|
|
75
89
|
}
|
|
76
90
|
const returnType = resolveTypeNode(node.type);
|
|
91
|
+
const websocketTypeRefs = readWebSocketTypeRefs(node);
|
|
77
92
|
|
|
78
93
|
methods.push({
|
|
79
94
|
modelName,
|
|
@@ -81,6 +96,7 @@ export async function readApisFromFile(filePath: AbsolutePath): Promise<Extended
|
|
|
81
96
|
typeParameters,
|
|
82
97
|
parameters,
|
|
83
98
|
returnType,
|
|
99
|
+
websocketTypeRefs,
|
|
84
100
|
});
|
|
85
101
|
}
|
|
86
102
|
ts.forEachChild(node, visitor);
|
|
@@ -113,8 +129,16 @@ export async function readApisFromFile(filePath: AbsolutePath): Promise<Extended
|
|
|
113
129
|
if (!foundMethod) {
|
|
114
130
|
throw new Error(`API ${api.modelName}.${api.methodName} not found in ${filePath}`);
|
|
115
131
|
}
|
|
132
|
+
const websocketOptions = api.websocketOptions
|
|
133
|
+
? {
|
|
134
|
+
...api.websocketOptions,
|
|
135
|
+
...foundMethod.websocketTypeRefs,
|
|
136
|
+
}
|
|
137
|
+
: undefined;
|
|
138
|
+
|
|
116
139
|
return {
|
|
117
140
|
...api,
|
|
141
|
+
websocketOptions,
|
|
118
142
|
typeParameters: foundMethod?.typeParameters,
|
|
119
143
|
parameters: foundMethod?.parameters,
|
|
120
144
|
returnType: foundMethod?.returnType,
|
|
@@ -123,6 +147,93 @@ export async function readApisFromFile(filePath: AbsolutePath): Promise<Extended
|
|
|
123
147
|
return extendedApis;
|
|
124
148
|
}
|
|
125
149
|
|
|
150
|
+
function readWebSocketTypeRefs(node: ts.MethodDeclaration): WebSocketTypeRefs {
|
|
151
|
+
const optionsLiteral = getDecoratorOptionsObjectLiteral(node, "websocket");
|
|
152
|
+
if (!optionsLiteral) {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const refs: WebSocketTypeRefs = {};
|
|
157
|
+
for (const property of optionsLiteral.properties) {
|
|
158
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const propertyName = getPropertyNameText(property.name);
|
|
163
|
+
if (propertyName !== "outEvents" && propertyName !== "inEvents") {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const typeRef = resolveDecoratorTypeRef(property.initializer);
|
|
168
|
+
if (!typeRef) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (propertyName === "outEvents") {
|
|
173
|
+
refs.outEventsTypeRef = typeRef;
|
|
174
|
+
} else {
|
|
175
|
+
refs.inEventsTypeRef = typeRef;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return refs;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getDecoratorOptionsObjectLiteral(
|
|
183
|
+
node: ts.MethodDeclaration,
|
|
184
|
+
decoratorName: string,
|
|
185
|
+
): ts.ObjectLiteralExpression | undefined {
|
|
186
|
+
for (const modifier of node.modifiers ?? []) {
|
|
187
|
+
if (!ts.isDecorator(modifier)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const expression = modifier.expression;
|
|
192
|
+
if (!ts.isCallExpression(expression)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!ts.isIdentifier(expression.expression) || expression.expression.text !== decoratorName) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const [firstArg] = expression.arguments;
|
|
201
|
+
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
202
|
+
return firstArg;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getPropertyNameText(name: ts.PropertyName): string | undefined {
|
|
210
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
211
|
+
return name.text;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveDecoratorTypeRef(expression: ts.Expression): ApiParamType.Ref | undefined {
|
|
218
|
+
if (ts.isIdentifier(expression)) {
|
|
219
|
+
return {
|
|
220
|
+
t: "ref",
|
|
221
|
+
id: expression.text,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
ts.isAsExpression(expression) ||
|
|
227
|
+
ts.isParenthesizedExpression(expression) ||
|
|
228
|
+
ts.isNonNullExpression(expression) ||
|
|
229
|
+
ts.isTypeAssertionExpression(expression)
|
|
230
|
+
) {
|
|
231
|
+
return resolveDecoratorTypeRef(expression.expression);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
126
237
|
function resolveTypeNode(typeNode: ts.TypeNode): ApiParamType {
|
|
127
238
|
switch (typeNode?.kind) {
|
|
128
239
|
case ts.SyntaxKind.AnyKeyword:
|
package/src/syncer/checksum.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import crypto from "crypto";
|
|
2
|
-
import { type
|
|
3
|
-
import { createReadStream } from "fs";
|
|
4
|
-
import { type PathLike } from "fs";
|
|
1
|
+
import crypto, { type BinaryLike } from "crypto";
|
|
2
|
+
import { createReadStream, type PathLike } from "fs";
|
|
5
3
|
import { readFile, writeFile } from "fs/promises";
|
|
6
4
|
import path from "path";
|
|
7
5
|
|
|
@@ -11,9 +9,9 @@ import { isEqual } from "radashi";
|
|
|
11
9
|
import { Sonamu } from "../api/sonamu";
|
|
12
10
|
import { globAsync } from "../utils/async-utils";
|
|
13
11
|
import { exists } from "../utils/fs-utils";
|
|
14
|
-
import { type AbsolutePath, type
|
|
12
|
+
import { type AbsolutePath, type AppRelativePath } from "../utils/path-utils";
|
|
15
13
|
import { differenceWith } from "../utils/utils";
|
|
16
|
-
import { getChecksumPatternGroupInAbsolutePath } from "./file-patterns";
|
|
14
|
+
import { getChecksumPatternGroupInAbsolutePath, GLOB_EXCLUDE } from "./file-patterns";
|
|
17
15
|
|
|
18
16
|
type PathAndChecksum = {
|
|
19
17
|
path: AbsolutePath;
|
|
@@ -53,17 +51,19 @@ export async function renewChecksums(): Promise<void> {
|
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
async function getCurrentChecksums(): Promise<PathAndChecksum[]> {
|
|
56
|
-
const
|
|
54
|
+
const allPaths = (
|
|
57
55
|
await Promise.all(
|
|
58
|
-
Object.entries(getChecksumPatternGroupInAbsolutePath()).map(
|
|
59
|
-
return globAsync(pattern
|
|
56
|
+
Object.entries(getChecksumPatternGroupInAbsolutePath()).map(([_fileType, pattern]) => {
|
|
57
|
+
return globAsync(pattern, { exclude: GLOB_EXCLUDE });
|
|
60
58
|
}),
|
|
61
59
|
)
|
|
62
|
-
)
|
|
63
|
-
.flat()
|
|
64
|
-
.toSorted();
|
|
60
|
+
).flat();
|
|
65
61
|
|
|
66
|
-
|
|
62
|
+
// 동일 파일이 여러 패턴에 매치될 수 있으므로(예: sd.generated.ts는 generated와 sdGenerated에 모두 매치)
|
|
63
|
+
// 중복 제거 후 안정 정렬.
|
|
64
|
+
const filePaths = Array.from(new Set(allPaths)).toSorted() as AbsolutePath[];
|
|
65
|
+
|
|
66
|
+
return await Promise.all(
|
|
67
67
|
filePaths.map(async (filePath) => {
|
|
68
68
|
return {
|
|
69
69
|
path: filePath,
|
|
@@ -71,8 +71,6 @@ async function getCurrentChecksums(): Promise<PathAndChecksum[]> {
|
|
|
71
71
|
};
|
|
72
72
|
}),
|
|
73
73
|
);
|
|
74
|
-
|
|
75
|
-
return fileChecksums;
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
async function getPreviousChecksums(): Promise<PathAndChecksum[]> {
|
|
@@ -83,8 +81,8 @@ async function getPreviousChecksums(): Promise<PathAndChecksum[]> {
|
|
|
83
81
|
|
|
84
82
|
try {
|
|
85
83
|
const previousChecksums = JSON.parse(await readFile(checksumFilePath, "utf-8")).map(
|
|
86
|
-
(r: { path:
|
|
87
|
-
path: path.join(Sonamu.
|
|
84
|
+
(r: { path: AppRelativePath; checksum: string }) => ({
|
|
85
|
+
path: path.join(Sonamu.appRootPath, r.path), // 체크섬 파일에서 읽을 때: appRoot 상대 경로 → 절대 경로
|
|
88
86
|
checksum: r.checksum,
|
|
89
87
|
}),
|
|
90
88
|
) as PathAndChecksum[];
|
|
@@ -105,18 +103,14 @@ function getChecksumFilePath(): AbsolutePath {
|
|
|
105
103
|
|
|
106
104
|
async function saveChecksums(checksums: PathAndChecksum[]): Promise<void> {
|
|
107
105
|
const checksumFilePath = getChecksumFilePath();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
2,
|
|
117
|
-
),
|
|
118
|
-
"utf-8",
|
|
119
|
-
);
|
|
106
|
+
// appRoot 상대 경로로 직렬화 + 알파벳 안정 정렬 (PR diff 깨끗하게 유지)
|
|
107
|
+
const serialized = checksums
|
|
108
|
+
.map((r) => ({
|
|
109
|
+
path: path.relative(Sonamu.appRootPath, r.path), // 체크섬 파일에 저장할 때: 절대 경로 → appRoot 상대 경로
|
|
110
|
+
checksum: r.checksum,
|
|
111
|
+
}))
|
|
112
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
113
|
+
await writeFile(checksumFilePath, JSON.stringify(serialized, null, 2), "utf-8");
|
|
120
114
|
}
|
|
121
115
|
|
|
122
116
|
async function getChecksumOfFile(filePath: PathLike): Promise<string> {
|
|
@@ -24,6 +24,7 @@ import { formatCode } from "../utils/formatter";
|
|
|
24
24
|
import { exists } from "../utils/fs-utils";
|
|
25
25
|
import { wrapIf } from "../utils/lodash-able";
|
|
26
26
|
import { type AbsolutePath } from "../utils/path-utils";
|
|
27
|
+
import { trackWritten } from "./file-tracking";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* 템플릿을 렌더링하고 파일로 생성합니다.
|
|
@@ -179,7 +180,6 @@ async function resolveRenderedTemplate(
|
|
|
179
180
|
Naite.t("resolveRenderedTemplate:beforeFormat", { key, header, body });
|
|
180
181
|
const formatted = await formatCode(
|
|
181
182
|
[header, body].join("\n\n"),
|
|
182
|
-
key === "entity" ? "json" : "typescript",
|
|
183
183
|
`${Sonamu.appRootPath}/${filePath}`,
|
|
184
184
|
);
|
|
185
185
|
Naite.t(`resolveRenderedTemplate:formatted:${key}`, formatted);
|
|
@@ -208,6 +208,9 @@ async function writeCodeToPathEachTarget(pathAndCode: PathAndCode): Promise<Abso
|
|
|
208
208
|
await mkdir(dir, { recursive: true });
|
|
209
209
|
}
|
|
210
210
|
await writeFile(dstFilePath, pathAndCode.code);
|
|
211
|
+
// 방금 우리가 쓴 path를 등록 → dev watcher의 후속 change 이벤트가
|
|
212
|
+
// 외부 변경으로 오인되지 않도록 거름 가드 자료 제공.
|
|
213
|
+
await trackWritten(dstFilePath);
|
|
211
214
|
!isTest() &&
|
|
212
215
|
console.log(
|
|
213
216
|
chalk.bold("Generated: ") + chalk.blue(`${dstFilePath.replace(`${appRootPath}/`, "")}`),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type AbsolutePath } from "../utils/path-utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 파일 변경 이벤트를 batch로 모아 한 번에 처리하는 throttler를 만듭니다.
|
|
5
|
+
*
|
|
6
|
+
* 동작:
|
|
7
|
+
* - 같은 path가 여러 번 push되면 마지막 event로 dedupe됩니다.
|
|
8
|
+
* - 마지막 push 후 `delayMs` 동안 추가 push가 없으면 한 번에 flush됩니다 (trailing debounce).
|
|
9
|
+
* - flush 도중 들어오는 push는 다음 batch로 큐잉됩니다.
|
|
10
|
+
* - 한 시점에 onFlush는 단 하나만 진행됩니다 — 이로써 onFlush 내부에서 일어나는
|
|
11
|
+
* 작업이 다른 onFlush 호출과 인터리브되지 않음을 보장합니다.
|
|
12
|
+
*
|
|
13
|
+
* 호출자는 push만 하면 됩니다. 내부 큐/타이머 상태는 캡슐화되어 있고, onFlush가
|
|
14
|
+
* 받는 fileEvents는 dedupe된 Map입니다.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const push = createFileEventBatcher({
|
|
18
|
+
* delayMs: 100,
|
|
19
|
+
* onFlush: (fileEvents) => handleBatch(fileEvents),
|
|
20
|
+
* });
|
|
21
|
+
* push("/path/a.ts" as AbsolutePath, "change");
|
|
22
|
+
* push("/path/b.ts" as AbsolutePath, "change"); // 100ms 후 둘 다 한 번에 onFlush로
|
|
23
|
+
*/
|
|
24
|
+
export function createFileEventBatcher<FileEventT extends string>(options: {
|
|
25
|
+
delayMs: number;
|
|
26
|
+
onFlush: (fileEvents: Map<AbsolutePath, FileEventT>) => Promise<void>;
|
|
27
|
+
}): (path: AbsolutePath, event: FileEventT) => void {
|
|
28
|
+
const { delayMs, onFlush } = options;
|
|
29
|
+
const pending = new Map<AbsolutePath, FileEventT>();
|
|
30
|
+
let flushTimer: NodeJS.Timeout | null = null;
|
|
31
|
+
let isFlushing = false;
|
|
32
|
+
|
|
33
|
+
const scheduleFlush = (): void => {
|
|
34
|
+
if (flushTimer !== null) {
|
|
35
|
+
clearTimeout(flushTimer);
|
|
36
|
+
}
|
|
37
|
+
flushTimer = setTimeout(() => {
|
|
38
|
+
flushTimer = null;
|
|
39
|
+
void runFlush();
|
|
40
|
+
}, delayMs);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const runFlush = async (): Promise<void> => {
|
|
44
|
+
// flush 도중 추가 push로 또 다른 runFlush가 트리거된 경우, 한 번에 하나만 진행되게 합니다.
|
|
45
|
+
// 진행 중인 flush가 끝나면 finally에서 다음 batch를 자동으로 다시 스케줄합니다.
|
|
46
|
+
if (isFlushing || pending.size === 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
isFlushing = true;
|
|
50
|
+
try {
|
|
51
|
+
// 현재까지 모인 변경을 snapshot으로 잡고 큐는 비웁니다.
|
|
52
|
+
// 이 batch가 처리되는 동안 들어오는 새 변경은 다음 batch로 모입니다.
|
|
53
|
+
const batch = new Map(pending);
|
|
54
|
+
pending.clear();
|
|
55
|
+
try {
|
|
56
|
+
await onFlush(batch);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error(e);
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
isFlushing = false;
|
|
62
|
+
if (pending.size > 0) {
|
|
63
|
+
scheduleFlush();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (path, event) => {
|
|
69
|
+
pending.set(path, event);
|
|
70
|
+
scheduleFlush();
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -1,51 +1,119 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
|
|
3
3
|
import { Sonamu } from "../api/sonamu";
|
|
4
|
-
import { type AbsolutePath, type
|
|
4
|
+
import { type AbsolutePath, type AppRelativePath } from "../utils/path-utils";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Syncer가 관심 가지고 지켜보는 파일들입니다.
|
|
8
8
|
* 이 파일들에 변경이 생기면 추가적인 작업(이하 "싱크" 또는 "싱크 액션")을 수행합니다.
|
|
9
9
|
* 이 작업이라 함은 파일 복사 또는 템플릿 렌더링을 통한 code generation을 의미합니다.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* 경로 형식: appRoot 기준 상대 경로 (target 디렉토리로 시작, 예: "api/src/...", "web/src/...")
|
|
12
|
+
* 사용: getChecksumPatternGroupInAbsolutePath()로 절대 경로 변환 후 glob 사용
|
|
13
|
+
*
|
|
14
|
+
* 두 가지 의미적 영역:
|
|
15
|
+
* - 입력 (사용자 작성): api 디렉토리에만 위치. 사용자가 직접 편집.
|
|
16
|
+
* - 출력 (sonamu 생성/복사): api 또는 target 디렉토리에 sonamu가 만들어내는 파일.
|
|
17
|
+
*
|
|
18
|
+
* 추적 밖 자산 (부트스트랩 phase에서 매번 보장):
|
|
19
|
+
* - sonamu.shared.ts: 사용자가 커스터마이즈하는 자산. IfNotExists로 1회 생성 후 손대지 않음.
|
|
20
|
+
* - entry-server.generated.tsx: 입력 의존 없는 정적 코드. 매번 overwrite generate.
|
|
21
|
+
*
|
|
22
|
+
* 위 둘은 sync()의 부트스트랩 phase에서 변경 검출 사이클과 무관하게 보장됩니다.
|
|
23
|
+
* 추적 사이클 안에서 할 액션이 없는 자산이라 패턴 그룹에 포함되지 않습니다.
|
|
24
|
+
*
|
|
25
|
+
* 위치 카테고리는 api/targets/anywhere 헬퍼로 명시적으로 표현합니다.
|
|
26
|
+
*
|
|
27
|
+
* FileType은 이 함수의 반환 타입에서 자동 추론됩니다. 키 추가 시 별도 enum/배열을
|
|
28
|
+
* 동기화할 필요 없이 여기 한 군데만 수정하면 됩니다.
|
|
13
29
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
export function getChecksumPatternGroup() {
|
|
31
|
+
// 헬퍼 함수들 만들어서 가져옵니다.
|
|
32
|
+
const { api, targets, anywhere } = globBuilders();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
// Sonamu 입장에서 soruce가 되는 파일들입니다.
|
|
36
|
+
// 이 친구들이 변경되면 이들로부터 액션을 수행하고 파일을 생성합니다.
|
|
37
|
+
// 모노리포에서 이 source들은 api 프로젝트에 한정되므로, api에 있는 친구들만 리스팅합니다.
|
|
38
|
+
config: api("src/sonamu.config.ts"),
|
|
39
|
+
entity: api("src/application/**/*.entity.json"),
|
|
40
|
+
frame: api("src/application/**/*.frame.ts"),
|
|
41
|
+
functions: api("src/application/**/*.functions.ts"),
|
|
42
|
+
model: api("src/application/**/*.model.ts"),
|
|
43
|
+
types: api("src/application/**/*.types.ts"),
|
|
44
|
+
workflow: api("src/application/**/*.workflow.ts"),
|
|
45
|
+
i18n: api("src/i18n/**/!(sd.generated).ts"),
|
|
46
|
+
|
|
47
|
+
// Sonamu가 출력하는 생성 파일들입니다.
|
|
48
|
+
// 이 친구들도 정합성 검증 차원에서 sonamu.lock에 기록해야 하고,
|
|
49
|
+
// 또한 변경시 그 사실을 syncer가 알기는 해야 합니다(비록 별다른 처리가 없는 경우도 있지만).
|
|
50
|
+
//
|
|
51
|
+
// 자산 본성에 따라 위치 카테고리가 다르기 때문에, 본성별로 분리해서 표기합니다.
|
|
52
|
+
// - 양쪽-필요 자산: api에 정본이 만들어진 뒤 target에 복사됨 (sonamu.generated.*, queries.generated.ts).
|
|
53
|
+
// - api 전용 자산: api에만 만들어짐 (sonamu.generated.http).
|
|
54
|
+
// - target 전용 자산: target에만 만들어짐 (services.generated.ts는 services.template의 :target 분배).
|
|
55
|
+
//
|
|
56
|
+
// 여기에는 Sonamu의 모든 sync 산출물이 있는 것은 아닙니다.
|
|
57
|
+
// sonamu.shared.ts와 entry-server.generated.tsx와 같은
|
|
58
|
+
// sync 초반 1회성 부트스트랩 파일들은 관리 안 하기 때문에 여기에 리스팅도 안 합니다.
|
|
59
|
+
generated: api("src/application/**/*.generated.{ts,tsx,sso.ts}"),
|
|
60
|
+
generatedCopied: targets("src/services/**/{sonamu,queries}.generated.{ts,tsx,sso.ts}"),
|
|
61
|
+
httpGenerated: api("src/application/**/*.generated.http"),
|
|
62
|
+
servicesGenerated: targets("src/services/services.generated.ts"),
|
|
63
|
+
sdGenerated: anywhere("src/i18n/**/sd.generated.ts"),
|
|
64
|
+
typesCopied: targets("src/services/**/*.types.ts"),
|
|
65
|
+
functionsCopied: targets("src/services/**/*.functions.ts"),
|
|
66
|
+
i18nCopied: targets("src/i18n/**/!(sd.generated).ts"),
|
|
67
|
+
} satisfies Record<string, AppRelativePath>;
|
|
68
|
+
}
|
|
30
69
|
|
|
31
70
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
71
|
+
* 위치 카테고리별 글롭 빌더를 만들어 반환합니다.
|
|
72
|
+
* - api(rest): api 디렉토리에 한정
|
|
73
|
+
* - targets(rest): target 디렉토리들에 한정 (web, app 등)
|
|
74
|
+
* - anywhere(rest): api와 target 모두
|
|
75
|
+
*/
|
|
76
|
+
function globBuilders() {
|
|
77
|
+
const apiDir = Sonamu.config.api.dir;
|
|
78
|
+
const targetDirs = Sonamu.config.sync.targets;
|
|
79
|
+
|
|
80
|
+
// Node 내장 fs.glob의 brace expansion은 단일 멤버 {x}를 풀지 않으므로, 멤버가 1개일 때는 alternation 없이 직접 결합합니다.
|
|
81
|
+
const braceJoin = (dirs: readonly string[]) =>
|
|
82
|
+
dirs.length === 1 ? dirs[0] : `{${dirs.join(",")}}`;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
api: (pathFromApi: string) => `${apiDir}/${pathFromApi}` as AppRelativePath,
|
|
86
|
+
targets: (pathFromTarget: string) =>
|
|
87
|
+
`${braceJoin(targetDirs)}/${pathFromTarget}` as AppRelativePath,
|
|
88
|
+
anywhere: (pathFromAnywhere: string) =>
|
|
89
|
+
`${braceJoin([apiDir, ...targetDirs])}/${pathFromAnywhere}` as AppRelativePath,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* FileType은 getChecksumPatternGroup의 반환 객체 키에서 자동 추론됩니다.
|
|
95
|
+
* 별도 배열/enum 동기화 불필요 — 패턴 그룹 함수가 진실의 단일 원천.
|
|
96
|
+
*/
|
|
97
|
+
export type FileType = keyof ReturnType<typeof getChecksumPatternGroup>;
|
|
98
|
+
export type GlobPattern<T extends AppRelativePath | AbsolutePath> = Record<FileType, T>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 빌드 산출물 디렉토리는 alternation 글롭이 의도치 않게 휘말릴 수 있으므로 안전망으로 제외.
|
|
102
|
+
* Node 내장 fs.glob의 exclude 옵션과 함께 사용합니다.
|
|
103
|
+
*/
|
|
104
|
+
export const GLOB_EXCLUDE = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.turbo/**"];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* appRoot 기준 상대 경로 패턴을 절대 경로 패턴으로 변환합니다.
|
|
37
108
|
*
|
|
38
109
|
* @returns 절대 경로 기반 Glob 패턴 맵
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* // 입력: { entity: "src/application/**\/*.entity.json" }
|
|
42
|
-
* // 출력: { entity: "/Users/.../api/src/application/**\/*.entity.json" }
|
|
43
110
|
*/
|
|
44
111
|
export function getChecksumPatternGroupInAbsolutePath(): GlobPattern<AbsolutePath> {
|
|
112
|
+
const group = getChecksumPatternGroup();
|
|
45
113
|
return Object.fromEntries(
|
|
46
|
-
Object.entries(
|
|
114
|
+
Object.entries(group).map(([key, value]) => [
|
|
47
115
|
key,
|
|
48
|
-
path.join(Sonamu.
|
|
116
|
+
path.join(Sonamu.appRootPath, value), // appRoot 상대 경로 → 절대 경로
|
|
49
117
|
]),
|
|
50
118
|
) as GlobPattern<AbsolutePath>;
|
|
51
119
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { stat } from "fs/promises";
|
|
2
|
+
|
|
3
|
+
import { type AbsolutePath } from "../utils/path-utils";
|
|
4
|
+
|
|
5
|
+
export const fileWrittenAt = new Map<AbsolutePath, number>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 우리가 디스크에 write한 path를 등록합니다. write 직후 호출하세요.
|
|
9
|
+
* 디스크에서 mtime을 직접 읽어 정확한 값으로 등록합니다.
|
|
10
|
+
*/
|
|
11
|
+
export async function trackWritten(filePath: AbsolutePath): Promise<void> {
|
|
12
|
+
const fileStat = await stat(filePath);
|
|
13
|
+
fileWrittenAt.set(filePath, fileStat.mtimeMs);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 주어진 path가 가장 최근 우리 write 이후로 외부에서 수정된 적 없는지 확인합니다.
|
|
18
|
+
* true면 "내가 쓴 그대로 남아있음", false면 "외부에서 수정됐거나 우리가 쓴 적 없음".
|
|
19
|
+
*/
|
|
20
|
+
export async function isLastChangedByMe(filePath: AbsolutePath): Promise<boolean> {
|
|
21
|
+
const registered = fileWrittenAt.get(filePath);
|
|
22
|
+
if (registered === undefined) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const fileStat = await stat(filePath);
|
|
26
|
+
return fileStat.mtimeMs <= registered;
|
|
27
|
+
}
|
|
@@ -3,26 +3,17 @@ import path from "path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import { type BaseFrameClass } from "../api/base-frame";
|
|
6
|
-
import { type
|
|
6
|
+
import { type ExtendedApi } from "../api/decorators";
|
|
7
7
|
import { Sonamu } from "../api/sonamu";
|
|
8
8
|
import { type BaseModelClass } from "../database/base-model";
|
|
9
9
|
import { type WorkflowMetadata } from "../tasks/decorator";
|
|
10
|
-
import { type ApiParam, type ApiParamType } from "../types/types";
|
|
11
10
|
import { globAsync } from "../utils/async-utils";
|
|
12
11
|
import { importMembers } from "../utils/esm-utils";
|
|
13
12
|
import { runtimePath } from "../utils/path-utils";
|
|
14
13
|
import { type AbsolutePath } from "../utils/path-utils";
|
|
15
14
|
import { readApisFromFile } from "./api-parser";
|
|
16
15
|
|
|
17
|
-
export type LoadedApis =
|
|
18
|
-
typeParameters: ApiParamType.TypeParam[];
|
|
19
|
-
parameters: ApiParam[];
|
|
20
|
-
returnType: ApiParamType;
|
|
21
|
-
modelName: string;
|
|
22
|
-
methodName: string;
|
|
23
|
-
path: string;
|
|
24
|
-
options: ApiDecoratorOptions;
|
|
25
|
-
}[];
|
|
16
|
+
export type LoadedApis = ExtendedApi[];
|
|
26
17
|
|
|
27
18
|
export type LoadedTypes = { [typeName: string]: z.ZodType };
|
|
28
19
|
|
|
@@ -100,7 +91,9 @@ export async function loadTypes(): Promise<LoadedTypes> {
|
|
|
100
91
|
path.join(Sonamu.apiRootPath, runtimePath("src/application/**/*.types.ts")),
|
|
101
92
|
path.join(Sonamu.apiRootPath, runtimePath("src/application/**/*.generated.ts")),
|
|
102
93
|
];
|
|
103
|
-
const typePaths = (
|
|
94
|
+
const typePaths = (
|
|
95
|
+
await Promise.all(typePathsPatterns.map((pattern) => globAsync(pattern)))
|
|
96
|
+
).flat();
|
|
104
97
|
|
|
105
98
|
const types: LoadedTypes = {};
|
|
106
99
|
for (const filePath of typePaths) {
|