sonamu 0.7.21 → 0.7.23
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/agents/agent.d.ts +6 -1
- package/dist/ai/agents/agent.d.ts.map +1 -1
- package/dist/ai/agents/agent.js +20 -5
- package/dist/api/base-frame.d.ts +4 -0
- package/dist/api/base-frame.d.ts.map +1 -1
- package/dist/api/base-frame.js +9 -1
- package/dist/api/caster.d.ts.map +1 -1
- package/dist/api/caster.js +2 -2
- package/dist/api/config.d.ts +35 -3
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/decorators.d.ts +4 -4
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +80 -18
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -1
- package/dist/api/secret.d.ts +7 -0
- package/dist/api/secret.d.ts.map +1 -0
- package/dist/api/secret.js +17 -0
- package/dist/api/sonamu.d.ts +17 -8
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +265 -47
- package/dist/cache/cache-manager.d.ts +11 -0
- package/dist/cache/cache-manager.d.ts.map +1 -0
- package/dist/cache/cache-manager.js +22 -0
- package/dist/cache/decorator.d.ts +31 -0
- package/dist/cache/decorator.d.ts.map +1 -0
- package/dist/cache/decorator.js +86 -0
- package/dist/cache/drivers.d.ts +33 -0
- package/dist/cache/drivers.d.ts.map +1 -0
- package/dist/cache/drivers.js +36 -0
- package/dist/cache/index.d.ts +4 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/types.d.ts +28 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +6 -0
- package/dist/database/base-model.d.ts +4 -2
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +9 -4
- package/dist/database/code-generator.d.ts +3 -1
- package/dist/database/code-generator.d.ts.map +1 -1
- package/dist/database/code-generator.js +3 -2
- package/dist/database/db.d.ts +1 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +5 -5
- package/dist/database/knex.d.ts +3 -0
- package/dist/database/knex.d.ts.map +1 -0
- package/dist/database/knex.js +29 -0
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +1 -1
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/database/upsert-builder.js +49 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/logger/category.d.ts +4 -0
- package/dist/logger/category.d.ts.map +1 -0
- package/dist/logger/category.js +34 -0
- package/dist/logger/configure.d.ts +9 -0
- package/dist/logger/configure.d.ts.map +1 -0
- package/dist/logger/configure.js +115 -0
- package/dist/migration/code-generation.d.ts +5 -1
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +13 -7
- package/dist/migration/migrator.d.ts +1 -1
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +7 -7
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +5 -3
- package/dist/naite/naite.d.ts +0 -4
- package/dist/naite/naite.d.ts.map +1 -1
- package/dist/naite/naite.js +11 -19
- package/dist/ssr/index.d.ts +4 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/ssr/index.js +4 -0
- package/dist/ssr/registry.d.ts +10 -0
- package/dist/ssr/registry.d.ts.map +1 -0
- package/dist/ssr/registry.js +43 -0
- package/dist/ssr/renderer.d.ts +6 -0
- package/dist/ssr/renderer.d.ts.map +1 -0
- package/dist/ssr/renderer.js +70 -0
- package/dist/ssr/types.d.ts +19 -0
- package/dist/ssr/types.d.ts.map +1 -0
- package/dist/ssr/types.js +4 -0
- package/dist/syncer/syncer.d.ts +1 -0
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +58 -1
- package/dist/tasks/decorator.d.ts +1 -0
- package/dist/tasks/decorator.d.ts.map +1 -1
- package/dist/tasks/decorator.js +9 -7
- package/dist/tasks/step-wrapper.d.ts +5 -0
- package/dist/tasks/step-wrapper.d.ts.map +1 -1
- package/dist/tasks/step-wrapper.js +11 -6
- package/dist/tasks/workflow-manager.d.ts +2 -0
- package/dist/tasks/workflow-manager.d.ts.map +1 -1
- package/dist/tasks/workflow-manager.js +5 -2
- package/dist/template/implementations/entry-server.template.d.ts +17 -0
- package/dist/template/implementations/entry-server.template.d.ts.map +1 -0
- package/dist/template/implementations/entry-server.template.js +78 -0
- package/dist/template/implementations/model.template.d.ts.map +1 -1
- package/dist/template/implementations/model.template.js +5 -3
- package/dist/template/implementations/queries.template.d.ts +17 -0
- package/dist/template/implementations/queries.template.d.ts.map +1 -0
- package/dist/template/implementations/queries.template.js +83 -0
- package/dist/template/implementations/view_enums_select.template.d.ts.map +1 -1
- package/dist/template/implementations/view_enums_select.template.js +34 -20
- package/dist/template/implementations/view_form.template.d.ts +2 -1
- package/dist/template/implementations/view_form.template.d.ts.map +1 -1
- package/dist/template/implementations/view_form.template.js +301 -129
- package/dist/template/implementations/view_id_async_select.template.d.ts.map +1 -1
- package/dist/template/implementations/view_id_async_select.template.js +136 -57
- package/dist/template/implementations/view_list.template.d.ts +2 -0
- package/dist/template/implementations/view_list.template.d.ts.map +1 -1
- package/dist/template/implementations/view_list.template.js +392 -227
- package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
- package/dist/template/implementations/view_search_input.template.js +46 -30
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +2 -2
- package/dist/testing/bootstrap.d.ts +28 -0
- package/dist/testing/bootstrap.d.ts.map +1 -0
- package/dist/testing/bootstrap.js +120 -0
- package/dist/testing/fixture-loader.d.ts +21 -0
- package/dist/testing/fixture-loader.d.ts.map +1 -0
- package/dist/testing/fixture-loader.js +28 -0
- package/dist/testing/fixture-manager.d.ts +1 -1
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +7 -7
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/naite-vitest-reporter.d.ts +12 -0
- package/dist/testing/naite-vitest-reporter.d.ts.map +1 -0
- package/dist/testing/naite-vitest-reporter.js +17 -0
- package/dist/types/types.d.ts +5 -6
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +7 -8
- package/dist/ui/ai-client.d.ts +3 -1
- package/dist/ui/ai-client.d.ts.map +1 -1
- package/dist/ui/ai-client.js +27 -8
- package/dist/ui-web/assets/index-CTYv3qL6.js +92 -0
- package/dist/ui-web/index.html +1 -1
- package/package.json +43 -20
- package/src/ai/agents/agent.ts +38 -19
- package/src/api/base-frame.ts +8 -0
- package/src/api/caster.ts +6 -1
- package/src/api/config.ts +38 -4
- package/src/api/decorators.ts +106 -20
- package/src/api/index.ts +1 -0
- package/src/api/secret.ts +23 -0
- package/src/api/sonamu.ts +334 -61
- package/src/cache/cache-manager.ts +23 -0
- package/src/cache/decorator.ts +116 -0
- package/src/cache/drivers.ts +42 -0
- package/src/cache/index.ts +16 -0
- package/src/cache/types.ts +32 -0
- package/src/database/base-model.ts +7 -3
- package/src/database/code-generator.ts +3 -1
- package/src/database/db.ts +5 -5
- package/src/database/knex.ts +34 -0
- package/src/database/puri.types.ts +2 -3
- package/src/database/upsert-builder.ts +58 -4
- package/src/index.ts +4 -0
- package/src/logger/category.ts +42 -0
- package/src/logger/configure.ts +132 -0
- package/src/migration/code-generation.ts +19 -6
- package/src/migration/migrator.ts +7 -6
- package/src/migration/postgresql-schema-reader.ts +7 -2
- package/src/naite/naite.ts +10 -18
- package/src/shared/web.shared.ts.txt +1 -1
- package/src/ssr/index.ts +13 -0
- package/src/ssr/registry.ts +52 -0
- package/src/ssr/renderer.ts +105 -0
- package/src/ssr/types.ts +20 -0
- package/src/syncer/syncer.ts +59 -0
- package/src/tasks/decorator.ts +20 -4
- package/src/tasks/step-wrapper.ts +14 -5
- package/src/tasks/workflow-manager.ts +9 -1
- package/src/template/implementations/entry-server.template.ts +81 -0
- package/src/template/implementations/model.template.ts +4 -2
- package/src/template/implementations/queries.template.ts +111 -0
- package/src/template/implementations/view_enums_select.template.ts +33 -19
- package/src/template/implementations/view_form.template.ts +324 -145
- package/src/template/implementations/view_id_async_select.template.ts +145 -56
- package/src/template/implementations/view_list.template.ts +446 -236
- package/src/template/implementations/view_search_input.template.ts +45 -29
- package/src/template/zod-converter.ts +4 -1
- package/src/testing/bootstrap.ts +176 -0
- package/src/testing/fixture-loader.ts +28 -0
- package/src/testing/fixture-manager.ts +7 -6
- package/src/testing/index.ts +3 -0
- package/src/testing/naite-vitest-reporter.ts +18 -0
- package/src/types/types.ts +4 -5
- package/src/ui/ai-client.ts +82 -50
- package/dist/template/implementations/view_enums_dropdown.template.d.ts +0 -17
- package/dist/template/implementations/view_enums_dropdown.template.d.ts.map +0 -1
- package/dist/template/implementations/view_enums_dropdown.template.js +0 -50
- package/dist/ui-web/assets/index-B87IyofX.js +0 -92
- package/src/template/implementations/view_enums_dropdown.template.ts +0 -53
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SSRRoute } from "./types";
|
|
2
|
+
|
|
3
|
+
const ssrRoutes: SSRRoute[] = [];
|
|
4
|
+
|
|
5
|
+
export function registerSSR(route: SSRRoute): void {
|
|
6
|
+
ssrRoutes.push(route);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getSSRRoutes(): SSRRoute[] {
|
|
10
|
+
return ssrRoutes;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function clearSSRRoutes(): void {
|
|
14
|
+
ssrRoutes.length = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function matchSSRRoute(
|
|
18
|
+
url: string,
|
|
19
|
+
): { route: SSRRoute; params: Record<string, string> } | null {
|
|
20
|
+
for (const route of ssrRoutes) {
|
|
21
|
+
const params = matchPath(route.path, url);
|
|
22
|
+
if (params !== null) {
|
|
23
|
+
return { route, params };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 간단한 path matching
|
|
30
|
+
export function matchPath(pattern: string, url: string): Record<string, string> | null {
|
|
31
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
32
|
+
const urlParts = url.split("?")[0].split("/").filter(Boolean);
|
|
33
|
+
|
|
34
|
+
if (patternParts.length !== urlParts.length) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const params: Record<string, string> = {};
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
41
|
+
const patternPart = patternParts[i];
|
|
42
|
+
const urlPart = urlParts[i];
|
|
43
|
+
|
|
44
|
+
if (patternPart.startsWith(":")) {
|
|
45
|
+
params[patternPart.slice(1)] = urlPart;
|
|
46
|
+
} else if (patternPart !== urlPart) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return params;
|
|
52
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
3
|
+
import type { ViteDevServer } from "vite";
|
|
4
|
+
import type { SonamuFastifyConfig } from "../types/types";
|
|
5
|
+
import type { PreloadedData, SSRRoute } from "./types";
|
|
6
|
+
|
|
7
|
+
export async function renderSSR(
|
|
8
|
+
url: string,
|
|
9
|
+
route: SSRRoute,
|
|
10
|
+
params: Record<string, string>,
|
|
11
|
+
request: FastifyRequest,
|
|
12
|
+
reply: FastifyReply,
|
|
13
|
+
config: SonamuFastifyConfig,
|
|
14
|
+
vite?: ViteDevServer,
|
|
15
|
+
): Promise<string> {
|
|
16
|
+
const { Sonamu } = await import("../api/sonamu");
|
|
17
|
+
|
|
18
|
+
// 1. preload 실행 → SSRQuery[] 획득 (dev/prod 공통)
|
|
19
|
+
const preloadConfig = route.preload ? route.preload(params) : [];
|
|
20
|
+
const preloadedData: PreloadedData[] = [];
|
|
21
|
+
|
|
22
|
+
for (const { modelName, methodName, params: apiParams, serviceKey } of preloadConfig) {
|
|
23
|
+
const api = Sonamu.syncer.apis.find(
|
|
24
|
+
(a) => a.modelName === modelName && a.methodName === methodName,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!api) {
|
|
28
|
+
console.warn(`API not found: ${modelName}.${methodName}`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const result = await Sonamu.invokeApiForSSR(api, apiParams, config, request, reply);
|
|
34
|
+
preloadedData.push({
|
|
35
|
+
queryKey: [...serviceKey, ...apiParams],
|
|
36
|
+
data: result,
|
|
37
|
+
});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error(`Failed to preload ${modelName}.${methodName}:`, e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Dev/Prod 스크립트 추출
|
|
44
|
+
let viteScripts: string;
|
|
45
|
+
let render: (
|
|
46
|
+
url: string,
|
|
47
|
+
preloadedData: PreloadedData[],
|
|
48
|
+
) => Promise<{ html: string; dehydratedState: unknown }>;
|
|
49
|
+
|
|
50
|
+
if (vite) {
|
|
51
|
+
// Dev: Vite Dev Server
|
|
52
|
+
const fs = await import("node:fs/promises");
|
|
53
|
+
const indexHtmlPath = path.join(vite.config.root, "index.html");
|
|
54
|
+
const originalHtml = await fs.readFile(indexHtmlPath, "utf-8");
|
|
55
|
+
const transformedHtml = await vite.transformIndexHtml(url, originalHtml);
|
|
56
|
+
|
|
57
|
+
// Vite가 주입한 스크립트 추출
|
|
58
|
+
viteScripts = extractScriptTags(transformedHtml);
|
|
59
|
+
|
|
60
|
+
const entryModule = await vite.ssrLoadModule("/src/entry-server.generated.tsx");
|
|
61
|
+
render = entryModule.render;
|
|
62
|
+
} else {
|
|
63
|
+
// Prod: 빌드된 파일
|
|
64
|
+
const fs = await import("node:fs");
|
|
65
|
+
const webDistPath = path.join(Sonamu.apiRootPath, "public", "web");
|
|
66
|
+
const ssrPath = path.join(Sonamu.apiRootPath, "dist", "ssr");
|
|
67
|
+
|
|
68
|
+
// 빌드된 index.html에서 스크립트 추출
|
|
69
|
+
const builtHtml = fs.readFileSync(path.join(webDistPath, "index.html"), "utf-8");
|
|
70
|
+
viteScripts = extractScriptTags(builtHtml);
|
|
71
|
+
|
|
72
|
+
const entryModule = await import(path.join(ssrPath, "entry-server.generated.js"));
|
|
73
|
+
render = entryModule.render;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. RouterProvider 렌더링 (full document)
|
|
77
|
+
const { html: fullDocHtml, dehydratedState } = await render(url, preloadedData);
|
|
78
|
+
|
|
79
|
+
// 4. SSR 데이터 스크립트 생성
|
|
80
|
+
const ssrDataScript = dehydratedState
|
|
81
|
+
? `<script>window.__SONAMU_SSR__ = ${JSON.stringify(dehydratedState).replace(/</g, "\\u003c")};</script>`
|
|
82
|
+
: "";
|
|
83
|
+
|
|
84
|
+
// 5. SSR Config 스크립트 생성 (disableHydrate)
|
|
85
|
+
const ssrConfigScript = route.disableHydrate
|
|
86
|
+
? `<script>window.__SONAMU_SSR_CONFIG__ = ${JSON.stringify({ disableHydrate: true })};</script>`
|
|
87
|
+
: "";
|
|
88
|
+
|
|
89
|
+
// 6. Vite 스크립트와 SSR 데이터를 </body> 직전에 주입
|
|
90
|
+
const finalHtml = fullDocHtml.replace(
|
|
91
|
+
"</body>",
|
|
92
|
+
`${ssrConfigScript}\n${ssrDataScript}\n${viteScripts}\n</body>`,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return finalHtml;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* HTML에서 <script>, <link> 태그를 추출
|
|
100
|
+
*/
|
|
101
|
+
function extractScriptTags(html: string): string {
|
|
102
|
+
const scriptRegex = /<script[^>]*>[\s\S]*?<\/script>|<link[^>]*>/gi;
|
|
103
|
+
const matches = html.match(scriptRegex) || [];
|
|
104
|
+
return matches.join("\n");
|
|
105
|
+
}
|
package/src/ssr/types.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Branded type - 실수로 일반 객체 사용 방지
|
|
2
|
+
export type SSRQuery = {
|
|
3
|
+
modelName: string; // 'UserModel' - 서버 모델 호출용
|
|
4
|
+
methodName: string; // 'findById' - 서버 메서드 호출용
|
|
5
|
+
params: unknown[]; // [subset, id] - Context 제외한 실제 파라미터
|
|
6
|
+
serviceKey: [string, string]; // ['User', 'getUsers'] - React Query queryKey용
|
|
7
|
+
} & { __brand: "SSRQuery" };
|
|
8
|
+
|
|
9
|
+
export type PreloadConfig = SSRQuery[];
|
|
10
|
+
|
|
11
|
+
export type SSRRoute = {
|
|
12
|
+
path: string;
|
|
13
|
+
preload?: (params: Record<string, string>) => PreloadConfig;
|
|
14
|
+
disableHydrate?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type PreloadedData = {
|
|
18
|
+
queryKey: unknown[];
|
|
19
|
+
data: unknown;
|
|
20
|
+
};
|
package/src/syncer/syncer.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { WorkflowMetadata } from "..";
|
|
|
12
12
|
import { registeredApis } from "../api/decorators";
|
|
13
13
|
import { Sonamu } from "../api/sonamu";
|
|
14
14
|
import { EntityManager, type EntityNamesRecord } from "../entity/entity-manager";
|
|
15
|
+
import { AlreadyProcessedException } from "../exceptions/so-exceptions";
|
|
15
16
|
import { Naite } from "../naite/naite";
|
|
16
17
|
import { TemplateManager } from "../template/template-manager";
|
|
17
18
|
import type { GenerateOptions, PathAndCode } from "../types/types";
|
|
@@ -63,6 +64,18 @@ export class Syncer {
|
|
|
63
64
|
const changedFiles = await findChangedFilesUsingChecksums();
|
|
64
65
|
if (changedFiles.length === 0) {
|
|
65
66
|
console.log(chalk.black.bgGreen(centerText("All files are synced!")));
|
|
67
|
+
|
|
68
|
+
// 변경사항이 없어도 SSR 템플릿은 생성 (초기 설정 시, 이미 존재하면 스킵)
|
|
69
|
+
try {
|
|
70
|
+
await generateTemplate("queries", {}, { overwrite: false });
|
|
71
|
+
await generateTemplate("entry_server", {}, { overwrite: false });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// 파일이 이미 존재하면 무시
|
|
74
|
+
if (!(e instanceof AlreadyProcessedException)) {
|
|
75
|
+
console.error("Failed to generate SSR templates:", e);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
return;
|
|
67
80
|
}
|
|
68
81
|
|
|
@@ -91,6 +104,18 @@ export class Syncer {
|
|
|
91
104
|
return;
|
|
92
105
|
}
|
|
93
106
|
|
|
107
|
+
// SSR 설정 파일 변경 감지
|
|
108
|
+
if (diffFilePath.includes("/src/ssr/")) {
|
|
109
|
+
console.log(chalk.bold.yellow("SSR config changed - reloading..."));
|
|
110
|
+
// SSR 파일도 invalidate 후 reload
|
|
111
|
+
if (!isTest()) {
|
|
112
|
+
await hot.invalidateFile(diffFilePath, event);
|
|
113
|
+
}
|
|
114
|
+
await this.autoloadSSRRoutes();
|
|
115
|
+
this.eventEmitter.emit("onHMRCompleted");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
// 일단 변경된 파일과 dependent 파일들을 invalidate 합니다.
|
|
95
120
|
// 한 번 이상 import된 친구들에 대해서만 실제 작업이 일어납니다.
|
|
96
121
|
// 그러니 안심하고 invalidate 해도 됩니다.
|
|
@@ -229,6 +254,36 @@ export class Syncer {
|
|
|
229
254
|
await Sonamu.workflows.synchronize(this.workflows);
|
|
230
255
|
}
|
|
231
256
|
|
|
257
|
+
async autoloadSSRRoutes(): Promise<void> {
|
|
258
|
+
const ssrConfigPath = path.join(Sonamu.apiRootPath, "src/ssr");
|
|
259
|
+
|
|
260
|
+
// 기존 routes 초기화
|
|
261
|
+
const { clearSSRRoutes } = await import("../ssr");
|
|
262
|
+
clearSSRRoutes();
|
|
263
|
+
|
|
264
|
+
// ssr 폴더 없으면 스킵
|
|
265
|
+
if (!(await exists(ssrConfigPath))) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ssr 폴더 안의 모든 .ts 파일 로드
|
|
270
|
+
const { globAsync } = await import("../utils/async-utils");
|
|
271
|
+
const { importMembers } = await import("../utils/esm-utils");
|
|
272
|
+
const { runtimePath } = await import("../utils/path-utils");
|
|
273
|
+
|
|
274
|
+
// runtimePath를 사용하여 개발/프로덕션 환경에 맞는 확장자 처리
|
|
275
|
+
const files = await globAsync(path.join(ssrConfigPath, runtimePath("**/*.ts")));
|
|
276
|
+
|
|
277
|
+
for (const file of files) {
|
|
278
|
+
try {
|
|
279
|
+
// importMembers를 사용하면 파일의 side effect(registerSSR 호출)가 실행됨
|
|
280
|
+
await importMembers(file);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
console.error(`Failed to load SSR route: ${file}`, e);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
232
287
|
/**
|
|
233
288
|
* 실제 싱크를 수행하는 본체입니다.
|
|
234
289
|
* 변경된 파일들을 타입별로 분류하고 각 타입에 맞는 액션을 실행합니다.
|
|
@@ -371,6 +426,10 @@ export class Syncer {
|
|
|
371
426
|
|
|
372
427
|
await this.actionGenerateServices(params);
|
|
373
428
|
await this.actionGenerateHttps();
|
|
429
|
+
|
|
430
|
+
// queries.generated.ts 및 entry-server.generated.tsx 재생성
|
|
431
|
+
await generateTemplate("queries", {}, { overwrite: true });
|
|
432
|
+
await generateTemplate("entry_server", {}, { overwrite: true });
|
|
374
433
|
}
|
|
375
434
|
|
|
376
435
|
// web/.sonamu.env 에 현재 설정값 저장
|
package/src/tasks/decorator.ts
CHANGED
|
@@ -45,12 +45,22 @@ export type DefineWorkflowOptions<
|
|
|
45
45
|
// 이것들은 syncer에서 한번에 load한 다음, WorkflowManager에서 synchronize를 통해 등록됨.
|
|
46
46
|
export function workflow<Input, Output, TSchema extends StandardSchemaV1 | undefined = undefined>(
|
|
47
47
|
options: DefineWorkflowOptions<Input, Output, TSchema>,
|
|
48
|
-
)
|
|
49
|
-
|
|
48
|
+
): (fn: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>) => WorkflowMetadata;
|
|
49
|
+
export function workflow<Input, Output, TSchema extends StandardSchemaV1 | undefined = undefined>(
|
|
50
|
+
options: DefineWorkflowOptions<Input, Output, TSchema>,
|
|
51
|
+
fn: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>,
|
|
52
|
+
): WorkflowMetadata;
|
|
53
|
+
export function workflow<Input, Output, TSchema extends StandardSchemaV1 | undefined = undefined>(
|
|
54
|
+
options: DefineWorkflowOptions<Input, Output, TSchema>,
|
|
55
|
+
fn?: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>,
|
|
56
|
+
):
|
|
57
|
+
| WorkflowMetadata
|
|
58
|
+
| ((fn: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>) => WorkflowMetadata) {
|
|
59
|
+
const decorated = (fn: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>) => {
|
|
50
60
|
const id = randomUUID();
|
|
51
61
|
const workflowName = options.name ?? inflection.underscore(fn.name);
|
|
52
62
|
|
|
53
|
-
const
|
|
63
|
+
const metadata: WorkflowMetadata = {
|
|
54
64
|
type: "workflow" as const,
|
|
55
65
|
id,
|
|
56
66
|
name: workflowName,
|
|
@@ -66,6 +76,12 @@ export function workflow<Input, Output, TSchema extends StandardSchemaV1 | undef
|
|
|
66
76
|
fn: fn as WorkflowFunction<unknown, unknown>,
|
|
67
77
|
};
|
|
68
78
|
|
|
69
|
-
return
|
|
79
|
+
return metadata;
|
|
70
80
|
};
|
|
81
|
+
|
|
82
|
+
if (!fn) {
|
|
83
|
+
return decorated;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return decorated(fn);
|
|
71
87
|
}
|
|
@@ -70,11 +70,20 @@ export class StepWrapper {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
return {
|
|
73
|
-
run: ((stepApi: StepApi) => {
|
|
74
|
-
return (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
run: ((stepApi: StepApi, ...args: TArgs) => {
|
|
74
|
+
return stepApi.run(config, () => fn(...args));
|
|
75
|
+
}).bind(null, this.#stepApi),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
define<TArgs extends unknown[], TResult>(
|
|
80
|
+
config: { name: string },
|
|
81
|
+
fn: StepFunction<TArgs, TResult>,
|
|
82
|
+
) {
|
|
83
|
+
return {
|
|
84
|
+
run: ((stepApi: StepApi, ...args: TArgs) => {
|
|
85
|
+
return stepApi.run(config, () => fn(...args));
|
|
86
|
+
}).bind(null, this.#stepApi),
|
|
78
87
|
};
|
|
79
88
|
}
|
|
80
89
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger, type Logger } from "@logtape/logtape";
|
|
1
2
|
import { BackendPostgres, OpenWorkflow, type Worker } from "@sonamu-kit/tasks";
|
|
2
3
|
import type {
|
|
3
4
|
RunnableWorkflow,
|
|
@@ -14,6 +15,7 @@ import { schedule as cronSchedule, type ScheduledTask } from "node-cron";
|
|
|
14
15
|
import type { ZodObject } from "zod";
|
|
15
16
|
import type { Context } from "../api/context";
|
|
16
17
|
import { Sonamu } from "../api/sonamu";
|
|
18
|
+
import { convertDomainToCategory } from "../logger/category";
|
|
17
19
|
import { Naite } from "../naite/naite";
|
|
18
20
|
import { createMockSSEFactory } from "../stream/sse";
|
|
19
21
|
import type { Executable } from "../types/types";
|
|
@@ -34,6 +36,7 @@ export interface WorkflowOptions {
|
|
|
34
36
|
// Workflow 함수의 타입, @sonamu-kit/tasks와 다른 점은 step을 한번 감싼 형태.
|
|
35
37
|
export type WorkflowFunction<Input, Output> = (
|
|
36
38
|
params: Readonly<{
|
|
39
|
+
logger: Logger;
|
|
37
40
|
input: Input;
|
|
38
41
|
step: StepWrapper;
|
|
39
42
|
version: string | null;
|
|
@@ -266,7 +269,12 @@ export class WorkflowManager {
|
|
|
266
269
|
|
|
267
270
|
const step = new StepWrapper(params.step);
|
|
268
271
|
return Sonamu.asyncLocalStorage.run({ context }, () =>
|
|
269
|
-
options.function({
|
|
272
|
+
options.function({
|
|
273
|
+
input: params.input,
|
|
274
|
+
step,
|
|
275
|
+
version: params.version,
|
|
276
|
+
logger: getLogger(convertDomainToCategory(options.name, "workflow")),
|
|
277
|
+
}),
|
|
270
278
|
);
|
|
271
279
|
};
|
|
272
280
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { TemplateOptions } from "../../types/types";
|
|
2
|
+
import { Template } from "../template";
|
|
3
|
+
|
|
4
|
+
export class Template__entry_server extends Template {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("entry_server");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
getTargetAndPath() {
|
|
10
|
+
return {
|
|
11
|
+
target: "web/src",
|
|
12
|
+
path: `entry-server.generated.tsx`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
render({}: TemplateOptions["entry_server"]) {
|
|
17
|
+
const body = `
|
|
18
|
+
import { QueryClient, dehydrate } from '@tanstack/react-query';
|
|
19
|
+
import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router';
|
|
20
|
+
import { renderToString } from 'react-dom/server';
|
|
21
|
+
import { routeTree } from './routeTree.gen';
|
|
22
|
+
import { Suspense } from "react";
|
|
23
|
+
|
|
24
|
+
export type PreloadedData = {
|
|
25
|
+
queryKey: any[];
|
|
26
|
+
data: any;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function render(url: string, preloadedData: PreloadedData[] = []) {
|
|
30
|
+
// QueryClient 생성
|
|
31
|
+
const queryClient = new QueryClient({
|
|
32
|
+
defaultOptions: {
|
|
33
|
+
queries: {
|
|
34
|
+
staleTime: 5000,
|
|
35
|
+
retry: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Preloaded 데이터를 queryClient에 직접 주입
|
|
41
|
+
for (const { queryKey, data } of preloadedData) {
|
|
42
|
+
queryClient.setQueryData(queryKey, data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Dehydrate
|
|
46
|
+
const dehydratedState = dehydrate(queryClient);
|
|
47
|
+
|
|
48
|
+
// SSR용 메모리 히스토리 생성
|
|
49
|
+
const memoryHistory = createMemoryHistory({
|
|
50
|
+
initialEntries: [url],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Router 생성 (SSR 모드)
|
|
54
|
+
const router = createRouter({
|
|
55
|
+
routeTree,
|
|
56
|
+
context: { queryClient },
|
|
57
|
+
history: memoryHistory,
|
|
58
|
+
defaultPreload: 'intent',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 라우터 초기화: SSR에서 반드시 await router.load() 호출 필요
|
|
62
|
+
await router.load();
|
|
63
|
+
|
|
64
|
+
// RouterProvider만 렌더링 (Suspense로 래핑 - hydration mismatch 방지)
|
|
65
|
+
const appHtml = renderToString(<Suspense fallback={null}><RouterProvider router={router} /></Suspense>);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
html: appHtml,
|
|
69
|
+
dehydratedState,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
`.trim();
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...this.getTargetAndPath(),
|
|
76
|
+
body,
|
|
77
|
+
importKeys: [],
|
|
78
|
+
customHeaders: [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -61,7 +61,9 @@ class ${entityId}ModelClass extends BaseModelClass<
|
|
|
61
61
|
typeof ${names.camel}SubsetQueries,
|
|
62
62
|
typeof ${names.camel}LoaderQueries
|
|
63
63
|
> {
|
|
64
|
-
|
|
64
|
+
constructor() {
|
|
65
|
+
super("${entityId}", ${names.camel}SubsetQueries, ${names.camel}LoaderQueries);
|
|
66
|
+
}
|
|
65
67
|
|
|
66
68
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "${entityId}" })
|
|
67
69
|
async findById<T extends ${entityId}SubsetKey>(
|
|
@@ -188,7 +190,7 @@ class ${entityId}ModelClass extends BaseModelClass<
|
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
export const ${entityId}Model = new ${entityId}ModelClass(
|
|
193
|
+
export const ${entityId}Model = new ${entityId}ModelClass();
|
|
192
194
|
`.trim(),
|
|
193
195
|
importKeys: [],
|
|
194
196
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import inflection from "inflection";
|
|
2
|
+
import { diff, unique } from "radashi";
|
|
3
|
+
import { apiParamToTsCode, apiParamTypeToTsType } from "../../api/code-converters";
|
|
4
|
+
import type { ExtendedApi } from "../../api/decorators";
|
|
5
|
+
import { Sonamu } from "../../api/sonamu";
|
|
6
|
+
import type { TemplateOptions } from "../../types/types";
|
|
7
|
+
import { ApiParamType } from "../../types/types";
|
|
8
|
+
import { Template } from "../template";
|
|
9
|
+
|
|
10
|
+
export class Template__queries extends Template {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("queries");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getTargetAndPath() {
|
|
16
|
+
const { dir } = Sonamu.config.api;
|
|
17
|
+
return {
|
|
18
|
+
target: `${dir}/src/application`,
|
|
19
|
+
path: `queries.generated.ts`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
render({}: TemplateOptions["queries"]) {
|
|
24
|
+
const { apis } = Sonamu.syncer;
|
|
25
|
+
|
|
26
|
+
// tanstack-query를 포함한 API만 필터링
|
|
27
|
+
const queryApis = apis.filter((api) => api.options.clients?.includes("tanstack-query"));
|
|
28
|
+
|
|
29
|
+
// 모델별로 그룹화
|
|
30
|
+
const apisByModel = new Map<string, ExtendedApi[]>();
|
|
31
|
+
for (const api of queryApis) {
|
|
32
|
+
const modelName = api.modelName.replace(/Model$/, "").replace(/Frame$/, "");
|
|
33
|
+
if (!apisByModel.has(modelName)) {
|
|
34
|
+
apisByModel.set(modelName, []);
|
|
35
|
+
}
|
|
36
|
+
apisByModel.get(modelName)?.push(api);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const namespaces: string[] = [];
|
|
40
|
+
const importKeys: string[] = [];
|
|
41
|
+
let typeParamNames: string[] = [];
|
|
42
|
+
|
|
43
|
+
for (const [modelName, modelApis] of apisByModel) {
|
|
44
|
+
const functions: string[] = [];
|
|
45
|
+
|
|
46
|
+
for (const api of modelApis) {
|
|
47
|
+
// Context 제외한 파라미터
|
|
48
|
+
const paramsWithoutContext = api.parameters.filter(
|
|
49
|
+
(param) =>
|
|
50
|
+
!ApiParamType.isContext(param.type) &&
|
|
51
|
+
!ApiParamType.isRefKnex(param.type) &&
|
|
52
|
+
!(param.optional === true && param.name.startsWith("_")),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// 타입 파라미터 이름 수집
|
|
56
|
+
typeParamNames = typeParamNames.concat(api.typeParameters.map((tp) => tp.id));
|
|
57
|
+
|
|
58
|
+
// 타입 파라미터 정의
|
|
59
|
+
const typeParametersAsTsType = api.typeParameters
|
|
60
|
+
.map((typeParam) => {
|
|
61
|
+
return apiParamTypeToTsType(typeParam, importKeys);
|
|
62
|
+
})
|
|
63
|
+
.join(", ");
|
|
64
|
+
const typeParamsDef = typeParametersAsTsType ? `<${typeParametersAsTsType}>` : "";
|
|
65
|
+
|
|
66
|
+
// 파라미터 정의
|
|
67
|
+
const paramsDef = apiParamToTsCode(paramsWithoutContext, importKeys);
|
|
68
|
+
const paramNames = paramsWithoutContext.map((p) => p.name).join(", ");
|
|
69
|
+
|
|
70
|
+
// serviceMethodName 계산 (services.template.ts와 동일한 로직)
|
|
71
|
+
const serviceMethodName = api.options.resourceName
|
|
72
|
+
? `get${inflection.camelize(api.options.resourceName)}`
|
|
73
|
+
: api.methodName;
|
|
74
|
+
|
|
75
|
+
// SSRQuery 함수 생성 (함수명도 serviceMethodName 사용)
|
|
76
|
+
functions.push(
|
|
77
|
+
`
|
|
78
|
+
export const ${serviceMethodName} = ${typeParamsDef}(${paramsDef}): SSRQuery =>
|
|
79
|
+
createSSRQuery('${api.modelName}', '${api.methodName}', [${paramNames}], ['${modelName}', '${serviceMethodName}']);
|
|
80
|
+
`.trim(),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
namespaces.push(
|
|
85
|
+
`
|
|
86
|
+
export namespace ${modelName}Service {
|
|
87
|
+
${functions.join("\n\n")}
|
|
88
|
+
}
|
|
89
|
+
`.trim(),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...this.getTargetAndPath(),
|
|
95
|
+
body: namespaces.join("\n\n"),
|
|
96
|
+
importKeys: diff(unique(importKeys), typeParamNames),
|
|
97
|
+
customHeaders: [
|
|
98
|
+
"/** biome-ignore-all lint: generated는 무시 */",
|
|
99
|
+
"/** biome-ignore-all assist: generated는 무시 */",
|
|
100
|
+
"",
|
|
101
|
+
`import type { SSRQuery } from 'sonamu/ssr';`,
|
|
102
|
+
"",
|
|
103
|
+
`// SSRQuery 헬퍼 함수`,
|
|
104
|
+
`function createSSRQuery(modelName: string, methodName: string, params: any[], serviceKey: [string, string]): SSRQuery {`,
|
|
105
|
+
` return { modelName, methodName, params, serviceKey, __brand: 'SSRQuery' } as SSRQuery;`,
|
|
106
|
+
`}`,
|
|
107
|
+
"",
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -22,33 +22,47 @@ export class Template__view_enums_select extends Template {
|
|
|
22
22
|
return {
|
|
23
23
|
...this.getTargetAndPath(names, enumId),
|
|
24
24
|
body: `
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
Dropdown,
|
|
28
|
-
DropdownProps,
|
|
29
|
-
} from 'semantic-ui-react';
|
|
25
|
+
|
|
26
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@sonamu-kit/react-components/components';
|
|
30
27
|
|
|
31
28
|
import { ${enumId}, ${enumId}Label } from '@/services/sonamu.generated';
|
|
32
29
|
|
|
33
30
|
export type ${enumId}SelectProps = {
|
|
31
|
+
value?: string;
|
|
32
|
+
onValueChange?: (value: string | null | undefined) => void;
|
|
34
33
|
placeholder?: string;
|
|
35
34
|
textPrefix?: string;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
clearable?: boolean;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
className?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function ${enumId}Select({
|
|
41
|
+
value,
|
|
42
|
+
onValueChange,
|
|
43
|
+
placeholder,
|
|
44
|
+
textPrefix,
|
|
45
|
+
clearable,
|
|
46
|
+
disabled,
|
|
47
|
+
className,
|
|
48
|
+
}: ${enumId}SelectProps) {
|
|
49
|
+
// Filter out empty string from options (Radix UI doesn't allow empty string as SelectItem value)
|
|
50
|
+
const validOptions = ${enumId}.options.filter((key) => (key as string) !== "");
|
|
43
51
|
|
|
44
52
|
return (
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
<Select value={value ?? ""} onValueChange={onValueChange} disabled={disabled}>
|
|
54
|
+
<SelectTrigger className={className}>
|
|
55
|
+
<SelectValue placeholder={placeholder ?? "${label}"} />
|
|
56
|
+
</SelectTrigger>
|
|
57
|
+
<SelectContent>
|
|
58
|
+
{clearable && <SelectItem value="">전체</SelectItem>}
|
|
59
|
+
{validOptions.map((key) => (
|
|
60
|
+
<SelectItem key={key} value={key}>
|
|
61
|
+
{(textPrefix ?? "") + ${enumId}Label[key]}
|
|
62
|
+
</SelectItem>
|
|
63
|
+
))}
|
|
64
|
+
</SelectContent>
|
|
65
|
+
</Select>
|
|
52
66
|
);
|
|
53
67
|
}
|
|
54
68
|
`.trim(),
|