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/syncer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "assert";
|
|
2
2
|
import { EventEmitter } from "events";
|
|
3
|
-
import {
|
|
3
|
+
import { unlink } from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
5
|
|
|
6
6
|
import { hot } from "@sonamu-kit/hmr-hook";
|
|
@@ -13,7 +13,6 @@ import { registeredApis } from "../api/decorators";
|
|
|
13
13
|
import { Sonamu } from "../api/sonamu";
|
|
14
14
|
import { EntityManager } from "../entity/entity-manager";
|
|
15
15
|
import { type EntityNamesRecord } from "../entity/entity-manager";
|
|
16
|
-
import { AlreadyProcessedException } from "../exceptions/so-exceptions";
|
|
17
16
|
import { Naite } from "../naite/naite";
|
|
18
17
|
import { type WorkflowMetadata } from "../tasks/decorator";
|
|
19
18
|
import { TemplateManager } from "../template/template-manager";
|
|
@@ -23,13 +22,13 @@ import { type TemplateOptions } from "../types/types";
|
|
|
23
22
|
import { mapAsync, reduceAsync } from "../utils/async-utils";
|
|
24
23
|
import { centerText } from "../utils/console-util";
|
|
25
24
|
import { isTest } from "../utils/controller";
|
|
26
|
-
import {
|
|
25
|
+
import { exists } from "../utils/fs-utils";
|
|
27
26
|
import { type AbsolutePath } from "../utils/path-utils";
|
|
28
27
|
import { runWithGracefulShutdown } from "../utils/process-utils";
|
|
29
28
|
import { findChangedFilesUsingChecksums, renewChecksums } from "./checksum";
|
|
30
29
|
import { generateTemplate, renderTemplate } from "./code-generator";
|
|
31
30
|
import { createEntity, delEntity } from "./entity-operations";
|
|
32
|
-
import {
|
|
31
|
+
import { getChecksumPatternGroup, getChecksumPatternGroupInAbsolutePath } from "./file-patterns";
|
|
33
32
|
import { type FileType } from "./file-patterns";
|
|
34
33
|
import { loadApis, loadModels, loadTypes, loadWorkflows } from "./module-loader";
|
|
35
34
|
import { type LoadedApis, type LoadedModels, type LoadedTypes } from "./module-loader";
|
|
@@ -48,35 +47,25 @@ export class Syncer {
|
|
|
48
47
|
|
|
49
48
|
/**
|
|
50
49
|
* 체크섬이 변경된 부분에 대해 싱크를 진행합니다.
|
|
51
|
-
* sonamu
|
|
50
|
+
* dev 서버가 처음 떴을 때, sonamu sync 할 때 실행됩니다. 이후에는 syncFromWatcher 경로를 타요.
|
|
52
51
|
* @returns
|
|
53
52
|
*/
|
|
54
53
|
async sync(): Promise<void> {
|
|
55
|
-
|
|
54
|
+
// 초기 부트스트랩! 얘네들은 idempotent하고 가볍기 때문에 무지성 실행해도 됩니다.
|
|
55
|
+
// 얘네들은 sonamu.lock에 들어가지도 않고 따라서 HMR 경로를 타지도 않는 친구들입니다.
|
|
56
|
+
// 그래서 아무 때나 그냥 돌려주면 되는데, syncFromWatcher에서 매번 하는 것은 낭비이니 여기서 한 번만 합니다.
|
|
57
|
+
await SyncerActions.actionCopySharedToTargetsIfNotExists();
|
|
58
|
+
await SyncerActions.actionGenerateSsrEntryServerIfNotExists();
|
|
56
59
|
|
|
57
|
-
//
|
|
58
|
-
await this.copySharedToTargets(targets);
|
|
59
|
-
|
|
60
|
-
// 그 다음부터는 변경된 파일을 찾아서 동기화 작업을 실행합니다.
|
|
60
|
+
// 바뀐 것이 없으면 그냥 넘어가요.
|
|
61
61
|
const changedFiles = await findChangedFilesUsingChecksums();
|
|
62
62
|
if (changedFiles.length === 0) {
|
|
63
63
|
console.log(chalk.black.bgGreen(centerText("All files are synced!")));
|
|
64
|
-
|
|
65
|
-
// 변경사항이 없어도 SSR 템플릿은 생성 (초기 설정 시, 이미 존재하면 스킵)
|
|
66
|
-
try {
|
|
67
|
-
await generateTemplate("queries", {}, { overwrite: false });
|
|
68
|
-
await generateTemplate("entry_server", {}, { overwrite: false });
|
|
69
|
-
} catch (e) {
|
|
70
|
-
// 파일이 이미 존재하면 무시
|
|
71
|
-
if (!(e instanceof AlreadyProcessedException)) {
|
|
72
|
-
console.error("Failed to generate SSR templates:", e);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
64
|
return;
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
//
|
|
67
|
+
// 여기서 실제 싱크 동작을 수행합니다.
|
|
68
|
+
// 다만 싱크 중에 프로세스가 죽으면 꼬여버리기 때문에,
|
|
80
69
|
// 시그널에도 잠시 버틸 수 있는 환경 속에서 싱크를 실행합니다.
|
|
81
70
|
await runWithGracefulShutdown(
|
|
82
71
|
async () => {
|
|
@@ -91,89 +80,147 @@ export class Syncer {
|
|
|
91
80
|
}
|
|
92
81
|
|
|
93
82
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
83
|
+
* 강제 풀-싱크: lock을 무시하고 처음부터 다시 싱크합니다.
|
|
84
|
+
*
|
|
85
|
+
* **사용처**: git post-merge hook, CI, dev 서버의 `f` 핫키.
|
|
86
|
+
* **실패 안전성**: 도중에 프로세스가 죽어 lock 없는 상태로 남아도 무해 — 다음 sync에서
|
|
87
|
+
* lock 없으면 자연스럽게 풀-싱크가 트리거되어 새 lock이 작성됨.
|
|
98
88
|
*/
|
|
99
|
-
async
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
async forceSync(): Promise<void> {
|
|
90
|
+
const lockPath = path.join(Sonamu.apiRootPath, "sonamu.lock");
|
|
91
|
+
if (await exists(lockPath)) {
|
|
92
|
+
await unlink(lockPath);
|
|
102
93
|
}
|
|
94
|
+
await this.sync();
|
|
95
|
+
}
|
|
103
96
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Watcher가 batch로 모은 변경 파일들에 대해 한 번의 HMR/sync 사이클을 돕니다.
|
|
99
|
+
*
|
|
100
|
+
* HMR은 api/src 안에서 일어나는 모든 파일들에 대해서 수행합니다.
|
|
101
|
+
* checksumPatternGroup 매칭 여부와 무관하게 api/src 전체 대상입니다.
|
|
102
|
+
* 가령 api/src/utils/subset-loaders.ts 같은 파일도 변경되면 HMR은 해줍니다.
|
|
103
|
+
*
|
|
104
|
+
* Sync는 checksumPatternGroup으로 매칭되는 파일들에 대해서만 수행합니다.
|
|
105
|
+
* 여기에는 web/src나 app/src 같은 다른 target의 파일이 포함될 수 있습니다.
|
|
106
|
+
* 이런 non-api 경로의 파일들은 HMR과는 아무 상관이 없으므로, invalidate을 하지 않습니다.
|
|
107
|
+
*
|
|
108
|
+
* @param fileEvents - path → event 맵. event는 "change" | "add".
|
|
109
|
+
*/
|
|
110
|
+
async hmrAndSync(fileEvents: Map<AbsolutePath, "change" | "add">): Promise<void> {
|
|
111
|
+
const hmrActionRequiredEvents = this.extractHmrActionRequiredFileEvents(fileEvents);
|
|
112
|
+
const syncTriggeringPaths = await this.extractSyncTriggeringFileEventPaths(fileEvents);
|
|
113
|
+
|
|
114
|
+
// HMR 영역: 파일 이벤트 중 api의 모듈 그래프에 있는 파일들에 대한 변동은 hmrActionRequiredEvents로 잡힙니다.
|
|
115
|
+
// 이 친구들은 invalidate 처리해줍니다.
|
|
116
|
+
// 이 호출은 아래 sync보다 무조건 먼저 일어나야 합니다!
|
|
117
|
+
// 왜냐하면 sync에서는 변경된 model 코드를 새로 import해서 처리해야 하는 경우도 있기 때문입니다.
|
|
118
|
+
await this.invalidateDependentsAffectedByFileEvents(hmrActionRequiredEvents);
|
|
119
|
+
|
|
120
|
+
// Sync 영역: checksumPatternGroup에 명시된 파일들에 대한 변동은 syncTriggeringPaths로 잡힙니다.
|
|
121
|
+
// 이 친구들은 적절한 sync 작업으로 대응합니다.
|
|
122
|
+
if (syncTriggeringPaths.length > 0) {
|
|
123
|
+
await this.doSyncActions(syncTriggeringPaths);
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
//
|
|
117
|
-
// 한
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
console.error(
|
|
143
|
-
chalk.red(`Failed to remove invalidated registered APIs for ${invalidatedPath}`),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
126
|
+
// 싱크 작업이 끝났으면 무지성 로드를 수행합니다.
|
|
127
|
+
// 싱크를 안 한 경우에도 로드는 해야 해요. 위에서 관련있는 친구들은 다 invalidate 되었거든요!
|
|
128
|
+
//
|
|
129
|
+
// 변경된 파일들에 대해서 새롭게 당겨오는(load) 행위는 doSyncActions에서 하지 않습니다.
|
|
130
|
+
// doSyncActions에서는 파일을 읽고 만드는 싱크 행위만 합니다.
|
|
131
|
+
// 거기서 딱 변경된 부분에 영향받는 autoload만 선별해서 수행하려면 너무 지저분하고 복잡해집니다.
|
|
132
|
+
//
|
|
133
|
+
// 퍼포먼스 영향은 무시해도 좋습니다.
|
|
134
|
+
// 어차피 hmr-hook에 의해 invalidate된 부분들이 아니라면 캐시 그대로 유지합니다.
|
|
135
|
+
await this.autoloadTypes();
|
|
136
|
+
await this.autoloadModels();
|
|
137
|
+
await this.autoloadApis();
|
|
138
|
+
await this.autoloadWorkflows();
|
|
139
|
+
await this.autoloadSsrRoutes();
|
|
140
|
+
|
|
141
|
+
this.eventEmitter.emit("onHMRCompleted");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private extractHmrActionRequiredFileEvents(
|
|
145
|
+
fileEvents: Map<AbsolutePath, "change" | "add">,
|
|
146
|
+
): Map<AbsolutePath, "change" | "add"> {
|
|
147
|
+
const apiSrc = path.join(Sonamu.apiRootPath, "src");
|
|
148
|
+
const result = new Map<AbsolutePath, "change" | "add">();
|
|
149
|
+
for (const [filePath, event] of fileEvents) {
|
|
150
|
+
if (!filePath.startsWith(apiSrc)) {
|
|
151
|
+
continue;
|
|
147
152
|
}
|
|
153
|
+
result.set(filePath, event);
|
|
148
154
|
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
149
157
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
private async extractSyncTriggeringFileEventPaths(
|
|
159
|
+
fileEvents: Map<AbsolutePath, "change" | "add">,
|
|
160
|
+
): Promise<AbsolutePath[]> {
|
|
161
|
+
const checkPatternGroup = getChecksumPatternGroupInAbsolutePath();
|
|
162
|
+
const syncTriggeringPaths: AbsolutePath[] = [];
|
|
163
|
+
for (const [diffFilePath] of fileEvents) {
|
|
164
|
+
const isInCheckPatternGroup = Object.values(checkPatternGroup).some((pattern) =>
|
|
165
|
+
minimatch(diffFilePath, pattern),
|
|
157
166
|
);
|
|
167
|
+
if (!isInCheckPatternGroup) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
syncTriggeringPaths.push(diffFilePath);
|
|
158
171
|
}
|
|
159
172
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
// 할 일(sync)이 있으면 합니다.
|
|
165
|
-
if (isInCheckPatternGroup) {
|
|
166
|
-
await this.doSyncActions([diffFilePath]);
|
|
167
|
-
}
|
|
173
|
+
return syncTriggeringPaths;
|
|
174
|
+
}
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
private async invalidateDependentsAffectedByFileEvents(
|
|
177
|
+
fileEvents: Map<AbsolutePath, "change" | "add">,
|
|
178
|
+
) {
|
|
179
|
+
for (const [diffFilePath, event] of fileEvents) {
|
|
180
|
+
// 변경된 파일과 dependent 파일들을 invalidate 합니다.
|
|
181
|
+
// 한 번 이상 import된 친구들에 대해서만 실제 작업이 일어납니다.
|
|
182
|
+
// 그러니 안심하고 invalidate 해도 됩니다.
|
|
183
|
+
// 테스트 환경에서는 hot.invalidateFile시 초기 에러가 발생하기 때문에 invalidate 하지 않습니다.
|
|
184
|
+
if (!isTest()) {
|
|
185
|
+
const invalidatedPaths = (await hot.invalidateFile(diffFilePath, event)) as AbsolutePath[];
|
|
186
|
+
|
|
187
|
+
if (invalidatedPaths.length > 0) {
|
|
188
|
+
console.log(chalk.bold(`🔄 Invalidated:`));
|
|
189
|
+
|
|
190
|
+
for (const invalidatedPath of invalidatedPaths) {
|
|
191
|
+
try {
|
|
192
|
+
// 만약 model.ts 파일이 변경(invalidate)되었다? 그러면 registeredApis 중에서 이 모델에 해당하는 api들은 지워줘요.
|
|
193
|
+
// registeredApis는 통으로 다 날려버릴 수 없습니다. registeredApis에 올라오는 친구들은 초기 로드시 또는 HMR시에만 등록되기 때문입니다.
|
|
194
|
+
// 따라서 model.ts 파일의 변경으로 다음번 새로운 eval이 예상되는 이 시점에서만, 이 모델에서 나온 registeredApis들을 지워줄 수 있습니다.
|
|
195
|
+
const removedApis = this.removeInvalidatedRegisteredApis(invalidatedPath);
|
|
196
|
+
if (removedApis.length > 0) {
|
|
197
|
+
console.log(
|
|
198
|
+
chalk.blue(`- ${path.relative(Sonamu.apiRootPath, invalidatedPath)}`),
|
|
199
|
+
chalk.gray(`(with ${removedApis.length} APIs)`),
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
console.log(chalk.blue(`- ${path.relative(Sonamu.apiRootPath, invalidatedPath)}`));
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error(e);
|
|
206
|
+
console.error(
|
|
207
|
+
chalk.red(`Failed to remove invalidated registered APIs for ${invalidatedPath}`),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
175
213
|
|
|
176
|
-
|
|
214
|
+
// devRunner 활성화 시, 변경된 소스 파일을 Vitest 모듈 그래프에서도 무효화합니다.
|
|
215
|
+
// Vite의 moduleGraph.invalidateModule()이 importer 방향으로 재귀적 cascade하므로,
|
|
216
|
+
// 소스 파일 하나만 무효화하면 이를 import하는 테스트 파일도 자동으로 무효화됩니다.
|
|
217
|
+
if (!isTest() && Sonamu.config.test?.devRunner?.enabled && Sonamu.devVitestManager) {
|
|
218
|
+
Sonamu.devVitestManager.invalidateFiles([diffFilePath]);
|
|
219
|
+
console.log(
|
|
220
|
+
chalk.dim(`Test invalidated: ${path.relative(Sonamu.apiRootPath, diffFilePath)}`),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
removeInvalidatedRegisteredApis(
|
|
@@ -195,73 +242,6 @@ export class Syncer {
|
|
|
195
242
|
return toRemove;
|
|
196
243
|
}
|
|
197
244
|
|
|
198
|
-
async copySharedToTargets(targets: string[]): Promise<void> {
|
|
199
|
-
// plural.ts 내용을 읽어서 shared 파일에 삽입합니다.
|
|
200
|
-
const dictUtilsPath = path.join(
|
|
201
|
-
import.meta.dirname.replace("/dist/", "/src/"),
|
|
202
|
-
"../dict/utils.ts",
|
|
203
|
-
);
|
|
204
|
-
const dictUtilsCode = (await exists(dictUtilsPath))
|
|
205
|
-
? await readFile(dictUtilsPath, "utf-8")
|
|
206
|
-
: "";
|
|
207
|
-
|
|
208
|
-
// 특정 변수 치환을 위해서 사용합니다.
|
|
209
|
-
const convertMap = {
|
|
210
|
-
baseUrl:
|
|
211
|
-
Sonamu.config.server.baseUrl ??
|
|
212
|
-
`http://${Sonamu.config.server.listen?.host ?? "localhost"}:${Sonamu.config.server.listen?.port ?? 3000}`,
|
|
213
|
-
dictUtils: dictUtilsCode,
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
for (const target of targets) {
|
|
217
|
-
// 지금 가져가려는 이 파일은 Sonamu 코드베이스의 일부입니다.
|
|
218
|
-
// 그런데 dist 속 빌드된 소스 코드 파일이 필요한 것이 아니고, src에만 있는 텍스트 파일이 필요합니다.
|
|
219
|
-
// 따라서 /src/에서 찾습니다.
|
|
220
|
-
const srcPath = path.join(
|
|
221
|
-
import.meta.dirname.replace("/dist/", "/src/"),
|
|
222
|
-
`../shared/${target}.shared.ts.txt`,
|
|
223
|
-
);
|
|
224
|
-
if (!(await exists(srcPath))) {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (!(await exists(path.join(Sonamu.appRootPath, target)))) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
`Tried to copy sonamu.shared.ts to target '${target}' but the target directory does not exist. Please check your project directory structure.`,
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const fullText = await readFile(srcPath, "utf-8");
|
|
234
|
-
const convertedText = Object.entries(convertMap).reduce(
|
|
235
|
-
(acc, [key, value]) => acc.replace(`$[[${key}]]`, value),
|
|
236
|
-
fullText,
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
// 이건 프로젝트에 .ts 소스 코드 파일을 생성하는 것이므로 src의 .ts 경로로 갑니다.
|
|
240
|
-
const destPath = path.join(Sonamu.appRootPath, target, "src/services/sonamu.shared.ts");
|
|
241
|
-
|
|
242
|
-
// 정말 혹시나지만 target 디렉토리는 있어도 src/services 디렉토리는 없을 수 있으므로 미리 생성해줍니다.
|
|
243
|
-
if (!(await exists(path.dirname(destPath)))) {
|
|
244
|
-
await mkdir(path.dirname(destPath), { recursive: true });
|
|
245
|
-
console.warn(`Created directory '${path.dirname(destPath)}' because it did not exist.`);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// 파일이 이미 존재하면 건너뜁니다.
|
|
249
|
-
// sonamu.shared.ts는 프로젝트에서 자유롭게 커스터마이징할 수 있어야 하므로,
|
|
250
|
-
// 최초 1회만 생성하고 이후에는 덮어쓰지 않습니다.
|
|
251
|
-
// 템플릿 내용($[[dictUtils]] 등)이 변경되었을 때 반영이 필요하면,
|
|
252
|
-
// 해당 파일을 삭제한 뒤 `pnpm sonamu sync`로 재생성하면 됩니다.
|
|
253
|
-
if (await exists(destPath)) {
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
await writeFile(destPath, convertedText);
|
|
258
|
-
!isTest() &&
|
|
259
|
-
console.log(
|
|
260
|
-
chalk.bold("Copied: ") + chalk.blue(path.relative(Sonamu.appRootPath, destPath)),
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
245
|
async autoloadTypes() {
|
|
266
246
|
this.types = await loadTypes();
|
|
267
247
|
}
|
|
@@ -279,7 +259,7 @@ export class Syncer {
|
|
|
279
259
|
await Sonamu.workflows.synchronize(this.workflows);
|
|
280
260
|
}
|
|
281
261
|
|
|
282
|
-
async
|
|
262
|
+
async autoloadSsrRoutes(): Promise<void> {
|
|
283
263
|
const ssrConfigPath = path.join(Sonamu.apiRootPath, "src/ssr");
|
|
284
264
|
|
|
285
265
|
// 기존 routes 초기화
|
|
@@ -312,52 +292,46 @@ export class Syncer {
|
|
|
312
292
|
/**
|
|
313
293
|
* 실제 싱크를 수행하는 본체입니다.
|
|
314
294
|
* 변경된 파일들을 타입별로 분류하고 각 타입에 맞는 액션을 실행합니다.
|
|
295
|
+
*
|
|
315
296
|
* @param diffFilePaths - 변경된 파일들의 절대 경로 목록
|
|
316
297
|
* @returns diffTypes - 변경된 파일의 타입 목록 (entity, types, model 등)
|
|
317
298
|
*/
|
|
318
|
-
async doSyncActions(diffFilePaths: AbsolutePath[]): Promise<{ diffTypes:
|
|
299
|
+
async doSyncActions(diffFilePaths: AbsolutePath[]): Promise<{ diffTypes: FileType[] }> {
|
|
319
300
|
const diffGroups = this.calculateDiffGroups(diffFilePaths);
|
|
320
301
|
const diffTypes = Object.keys(diffGroups) as FileType[];
|
|
321
302
|
|
|
322
|
-
//
|
|
323
|
-
if
|
|
324
|
-
|
|
325
|
-
|
|
303
|
+
// 여기는 별로 중요한 파트는 아닙니다.
|
|
304
|
+
// 아래의 if 전개를 깔끔하게 하려고 만든 DSL 같은 거라서, 무시하셔도 됩니다.
|
|
305
|
+
const { changeMatches, nothingMatches, unhandledPaths } = this.changeMatcher(
|
|
306
|
+
diffTypes,
|
|
307
|
+
diffGroups,
|
|
308
|
+
);
|
|
326
309
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
diffTypes.includes("types") ||
|
|
330
|
-
diffTypes.includes("functions") ||
|
|
331
|
-
diffTypes.includes("generated")
|
|
332
|
-
) {
|
|
333
|
-
await this.handleSyncableFileChanges(diffGroups);
|
|
310
|
+
if (changeMatches("entity", "types")) {
|
|
311
|
+
await this.handleTruthSourceChanges(diffGroups);
|
|
334
312
|
}
|
|
335
313
|
|
|
336
|
-
|
|
337
|
-
if (diffTypes.includes("model") || diffTypes.includes("frame")) {
|
|
314
|
+
if (changeMatches("model", "frame")) {
|
|
338
315
|
await this.handleImplementationChanges(diffGroups);
|
|
339
316
|
}
|
|
340
317
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
await SyncerActions.actionSyncConfig();
|
|
318
|
+
if (changeMatches("types", "functions")) {
|
|
319
|
+
await this.handleAuxiliarySymbolChanges(diffGroups);
|
|
344
320
|
}
|
|
345
321
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
await this.autoloadWorkflows();
|
|
322
|
+
if (changeMatches("config")) {
|
|
323
|
+
await this.handleConfigChanges(diffGroups);
|
|
349
324
|
}
|
|
350
325
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
await this.syncSD();
|
|
326
|
+
if (changeMatches("i18n", "entity" /*레이블*/, "config" /*defaultLocale등*/)) {
|
|
327
|
+
await this.handleSonamuDictionaryRelatedChanges(diffGroups);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (nothingMatches()) {
|
|
331
|
+
// 파일 변경은 감지되었으나 저 위 어느 changeMatches에도 걸리지 않은 파일들이 drifts입니다.
|
|
332
|
+
// syncer는 소스의 변경에는 반응하지만 산출물의 변경(drift)에는 직접적으로 반응하지 않습니다.
|
|
333
|
+
// 대신 이 drift에 대해 경고 정도만 출력해줍니다.
|
|
334
|
+
await this.handleDrifts(unhandledPaths());
|
|
361
335
|
}
|
|
362
336
|
|
|
363
337
|
return {
|
|
@@ -366,16 +340,16 @@ export class Syncer {
|
|
|
366
340
|
}
|
|
367
341
|
|
|
368
342
|
calculateDiffGroups(diffFiles: AbsolutePath[]): DiffGroups {
|
|
369
|
-
const
|
|
343
|
+
const patternGroup = getChecksumPatternGroup();
|
|
344
|
+
const fileTypes = Object.keys(patternGroup) as FileType[];
|
|
370
345
|
|
|
371
346
|
return group(diffFiles, (filePath) => {
|
|
372
|
-
// 절대
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
const relativePath = filePath.slice(srcIndex + 1); // "src/..." 형태
|
|
347
|
+
// 절대 경로 → appRoot 기준 상대 경로 (예: "api/src/...", "web/src/...")
|
|
348
|
+
const relativePath = path.relative(Sonamu.appRootPath, filePath);
|
|
349
|
+
if (relativePath.startsWith("..")) return "unknown";
|
|
376
350
|
|
|
377
351
|
for (const fileType of fileTypes) {
|
|
378
|
-
if (minimatch(relativePath,
|
|
352
|
+
if (minimatch(relativePath, patternGroup[fileType])) {
|
|
379
353
|
return fileType;
|
|
380
354
|
}
|
|
381
355
|
}
|
|
@@ -383,8 +357,38 @@ export class Syncer {
|
|
|
383
357
|
}) as unknown as DiffGroups;
|
|
384
358
|
}
|
|
385
359
|
|
|
386
|
-
|
|
387
|
-
|
|
360
|
+
private changeMatcher(diffTypes: FileType[], diffGroups: DiffGroups) {
|
|
361
|
+
const handled = new Set<FileType>();
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 변경 사항이 인자로 받은 FileType들 중 하나 이상을 포함하는지 확인합니다.
|
|
365
|
+
* 가령 ["entity"]가 변경된 호출에서 changeMatches("entity")는 trye를 반환하며,
|
|
366
|
+
* ["types", "i18n"]이 변경된 호출에서 changeMatches("types", "functions")도 true를 반환하지만,
|
|
367
|
+
* ["functions"]가 변경된 호출에서 changeMatches("frame")은 false를 반환합니다.
|
|
368
|
+
* @param types
|
|
369
|
+
*/
|
|
370
|
+
const changeMatches = (...types: FileType[]) => {
|
|
371
|
+
const matching = types.filter((t) => diffTypes.includes(t));
|
|
372
|
+
matching.forEach((t) => handled.add(t));
|
|
373
|
+
return matching.length > 0;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* changeMatches로 매칭된 것이 하나도 없는지 여부를 가져옵니다.
|
|
378
|
+
*/
|
|
379
|
+
const nothingMatches = () => handled.size === 0;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 어떤 changeMatches 호출에도 걸리지 않은 FileType들의 실제 파일 경로를 모아서 반환합니다.
|
|
383
|
+
*/
|
|
384
|
+
const unhandledPaths = (): AbsolutePath[] =>
|
|
385
|
+
diffTypes.filter((t) => !handled.has(t)).flatMap((t) => diffGroups[t] ?? []);
|
|
386
|
+
|
|
387
|
+
return { changeMatches, nothingMatches, unhandledPaths };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async handleTruthSourceChanges(diffGroups: DiffGroups): Promise<void> {
|
|
391
|
+
Naite.t("handleTruthSourceChanges", { diffGroups });
|
|
388
392
|
|
|
389
393
|
await EntityManager.reload();
|
|
390
394
|
|
|
@@ -394,54 +398,33 @@ export class Syncer {
|
|
|
394
398
|
if (entityPath !== undefined) {
|
|
395
399
|
const entityId = EntityManager.getEntityIdFromPath(entityPath);
|
|
396
400
|
const entity = EntityManager.get(entityId);
|
|
401
|
+
|
|
397
402
|
// 프로젝트에 생성되어야 하는 .ts 파일의 경로입니다.
|
|
398
403
|
const typeFilePath = path.join(
|
|
399
404
|
Sonamu.apiRootPath,
|
|
400
405
|
`src/application/${entity.names.fs}/${entity.names.fs}.types.ts`,
|
|
401
|
-
);
|
|
406
|
+
) as AbsolutePath;
|
|
407
|
+
|
|
402
408
|
if (entity.parentId === undefined && !(await exists(typeFilePath))) {
|
|
403
|
-
|
|
409
|
+
// *.types.ts가 만들어집니다.
|
|
410
|
+
const types = await SyncerActions.actionGenerateInitialTypes(entityId);
|
|
411
|
+
|
|
412
|
+
// 그걸 타겟에 갖다둬요.
|
|
413
|
+
await SyncerActions.actionSyncFilesToTargets(types);
|
|
404
414
|
}
|
|
405
415
|
}
|
|
406
416
|
|
|
407
|
-
|
|
417
|
+
// sonamu.generated.ts가 만들어집니다.
|
|
418
|
+
const generated = await SyncerActions.actionGenerateSchemas();
|
|
408
419
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
path.join(Sonamu.apiRootPath, "src/application/sonamu.generated.ts") as AbsolutePath,
|
|
412
|
-
]);
|
|
413
|
-
diffTypes.push("generated");
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async handleSyncableFileChanges(diffGroups: DiffGroups): Promise<FileType[]> {
|
|
417
|
-
const tsPaths = unique([
|
|
418
|
-
...(diffGroups.types ?? []),
|
|
419
|
-
...(diffGroups.functions ?? []),
|
|
420
|
-
...(diffGroups.generated ?? []),
|
|
421
|
-
]);
|
|
422
|
-
Naite.t("handleSyncableFileChanges", { diffGroups });
|
|
423
|
-
|
|
424
|
-
// console.log(
|
|
425
|
-
// chalk.gray(
|
|
426
|
-
// `[Processing] Handling types/functions/generated changes: ${tsPaths.map((p) => path.relative(Sonamu.apiRootPath, p)).join(", ")}`
|
|
427
|
-
// )
|
|
428
|
-
// );
|
|
429
|
-
|
|
430
|
-
await SyncerActions.actionSyncFilesToTargets(tsPaths);
|
|
431
|
-
|
|
432
|
-
return [];
|
|
420
|
+
// 그걸 target들에도 보내요.
|
|
421
|
+
await SyncerActions.actionSyncFilesToTargets(generated);
|
|
433
422
|
}
|
|
434
423
|
|
|
435
424
|
async handleImplementationChanges(diffGroups: DiffGroups): Promise<void> {
|
|
436
425
|
Naite.t("handleImplementationChanges", { diffGroups });
|
|
437
426
|
const mergedGroup = [...(diffGroups.model ?? []), ...(diffGroups.frame ?? [])];
|
|
438
427
|
|
|
439
|
-
// console.log(
|
|
440
|
-
// chalk.gray(
|
|
441
|
-
// `[Processing] Handling model/frame changes: ${mergedGroup.map((p) => path.relative(Sonamu.apiRootPath, p)).join(", ")}`
|
|
442
|
-
// )
|
|
443
|
-
// );
|
|
444
|
-
|
|
445
428
|
// generated_http.template.ts에서 syncer.types를 씁니다.
|
|
446
429
|
// service.template.ts에서 syncer.apis를 씁니다.
|
|
447
430
|
await this.autoloadModels();
|
|
@@ -461,9 +444,43 @@ export class Syncer {
|
|
|
461
444
|
throw new Error("not reachable");
|
|
462
445
|
});
|
|
463
446
|
|
|
447
|
+
// services.generated.ts를 target들에, sonamu.generated.http를 api에 만들어줘요.
|
|
464
448
|
await SyncerActions.actionGenerateServices(params);
|
|
465
449
|
await SyncerActions.actionGenerateHttps();
|
|
466
|
-
|
|
450
|
+
|
|
451
|
+
// queries.generated.ts가 만들어집니다.
|
|
452
|
+
const queries = await SyncerActions.actionGenerateSsrQueries();
|
|
453
|
+
|
|
454
|
+
// 그걸 target들에도 보내요.
|
|
455
|
+
await SyncerActions.actionSyncFilesToTargets(queries);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async handleAuxiliarySymbolChanges(diffGroups: DiffGroups): Promise<void> {
|
|
459
|
+
Naite.t("handleAuxiliarySymbolChanges", { diffGroups });
|
|
460
|
+
const tsPaths = unique([...(diffGroups.types ?? []), ...(diffGroups.functions ?? [])]);
|
|
461
|
+
await SyncerActions.actionSyncFilesToTargets(tsPaths);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async handleConfigChanges(_: DiffGroups): Promise<void> {
|
|
465
|
+
await SyncerActions.actionSyncConfig();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async handleSonamuDictionaryRelatedChanges(_: DiffGroups): Promise<void> {
|
|
469
|
+
await SyncerActions.actionSyncSonamuDictionary();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async handleDrifts(drifts: AbsolutePath[]): Promise<void> {
|
|
473
|
+
if (drifts.length > 0) {
|
|
474
|
+
console.warn(
|
|
475
|
+
chalk.yellow(
|
|
476
|
+
"⚠️ Sonamu가 자동 생성한 파일에 대한 변경이 감지되었습니다. 파일이 Sonamu watcher 외부에서 변경된 것으로 추정됩니다.",
|
|
477
|
+
),
|
|
478
|
+
);
|
|
479
|
+
for (const p of drifts) {
|
|
480
|
+
console.warn(chalk.yellow(` - ${path.relative(Sonamu.appRootPath, p)}`));
|
|
481
|
+
}
|
|
482
|
+
console.warn(chalk.dim(" → `pnpm sonamu sync --force`를 권장합니다."));
|
|
483
|
+
}
|
|
467
484
|
}
|
|
468
485
|
|
|
469
486
|
/**
|
|
@@ -581,58 +598,4 @@ export class Syncer {
|
|
|
581
598
|
async renewChecksums(): Promise<void> {
|
|
582
599
|
return await renewChecksums();
|
|
583
600
|
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* SD(Sonamu Dictionary) 템플릿을 생성합니다.
|
|
587
|
-
*/
|
|
588
|
-
async syncSD(): Promise<void> {
|
|
589
|
-
const { targets } = Sonamu.config.sync;
|
|
590
|
-
const i18nConfig = Sonamu.config.i18n;
|
|
591
|
-
|
|
592
|
-
const targetList = ["api", ...targets] as ("api" | "web" | "app")[];
|
|
593
|
-
|
|
594
|
-
const apiI18nDir = path.join(Sonamu.appRootPath, Sonamu.config.api.dir, "src/i18n");
|
|
595
|
-
|
|
596
|
-
for (const target of targetList) {
|
|
597
|
-
try {
|
|
598
|
-
// web/app의 경우 locale 파일들을 api에서 복사
|
|
599
|
-
if (target !== "api") {
|
|
600
|
-
await this.syncLocaleFiles(target, apiI18nDir, i18nConfig.supportedLocales);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
await generateTemplate("sd", { target }, { overwrite: true });
|
|
604
|
-
} catch (e) {
|
|
605
|
-
console.error(`Failed to generate SD template for ${target}:`, e);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* api의 locale 파일을 web/app으로 복사합니다.
|
|
612
|
-
*/
|
|
613
|
-
private async syncLocaleFiles(
|
|
614
|
-
target: string,
|
|
615
|
-
apiI18nDir: string,
|
|
616
|
-
locales: string[],
|
|
617
|
-
): Promise<void> {
|
|
618
|
-
const targetI18nDir = path.join(Sonamu.appRootPath, target, "src/i18n");
|
|
619
|
-
|
|
620
|
-
// 디렉토리가 없으면 생성
|
|
621
|
-
await mkdir(targetI18nDir, { recursive: true });
|
|
622
|
-
|
|
623
|
-
for (const locale of locales) {
|
|
624
|
-
const sourceFile = path.join(apiI18nDir, `${locale}.ts`);
|
|
625
|
-
const targetFile = path.join(targetI18nDir, `${locale}.ts`);
|
|
626
|
-
|
|
627
|
-
const syncHeader = [
|
|
628
|
-
"/**",
|
|
629
|
-
" * @generated",
|
|
630
|
-
" * API에서 동기화된 파일입니다. 직접 수정하지 마세요.",
|
|
631
|
-
" */",
|
|
632
|
-
].join("\n");
|
|
633
|
-
await copyFileWithReplaceCoreToShared(sourceFile, targetFile, syncHeader);
|
|
634
|
-
!isTest() &&
|
|
635
|
-
console.log(chalk.bold("Copied: ") + chalk.cyan(`${target}/src/i18n/${locale}.ts`));
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
601
|
}
|