@mandujs/core 0.3.2 → 0.3.4
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/README.ko.md +200 -200
- package/README.md +200 -200
- package/package.json +4 -2
- package/src/change/history.ts +145 -0
- package/src/change/index.ts +40 -0
- package/src/change/integrity.ts +81 -0
- package/src/change/snapshot.ts +233 -0
- package/src/change/transaction.ts +293 -0
- package/src/change/types.ts +102 -0
- package/src/error/classifier.ts +314 -0
- package/src/error/formatter.ts +237 -0
- package/src/error/index.ts +25 -0
- package/src/error/stack-analyzer.ts +295 -0
- package/src/error/types.ts +140 -0
- package/src/filling/context.ts +228 -219
- package/src/filling/filling.ts +256 -234
- package/src/filling/index.ts +7 -7
- package/src/generator/generate.ts +85 -3
- package/src/generator/index.ts +2 -2
- package/src/guard/auto-correct.ts +257 -203
- package/src/index.ts +3 -0
- package/src/report/index.ts +1 -1
- package/src/runtime/index.ts +3 -3
- package/src/runtime/router.ts +65 -65
- package/src/runtime/server.ts +189 -139
- package/src/runtime/ssr.ts +38 -38
- package/src/slot/corrector.ts +282 -0
- package/src/slot/index.ts +18 -0
- package/src/slot/validator.ts +241 -0
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
package/src/runtime/server.ts
CHANGED
|
@@ -1,139 +1,189 @@
|
|
|
1
|
-
import type { Server } from "bun";
|
|
2
|
-
import type { RoutesManifest } from "../spec/schema";
|
|
3
|
-
import { Router } from "./router";
|
|
4
|
-
import { renderSSR } from "./ssr";
|
|
5
|
-
import React from "react";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
1
|
+
import type { Server } from "bun";
|
|
2
|
+
import type { RoutesManifest } from "../spec/schema";
|
|
3
|
+
import { Router } from "./router";
|
|
4
|
+
import { renderSSR } from "./ssr";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import {
|
|
7
|
+
formatErrorResponse,
|
|
8
|
+
createNotFoundResponse,
|
|
9
|
+
createHandlerNotFoundResponse,
|
|
10
|
+
createPageLoadErrorResponse,
|
|
11
|
+
createSSRErrorResponse,
|
|
12
|
+
} from "../error";
|
|
13
|
+
|
|
14
|
+
export interface ServerOptions {
|
|
15
|
+
port?: number;
|
|
16
|
+
hostname?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ManduServer {
|
|
20
|
+
server: Server;
|
|
21
|
+
router: Router;
|
|
22
|
+
stop: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ApiHandler = (req: Request, params: Record<string, string>) => Response | Promise<Response>;
|
|
26
|
+
export type PageLoader = () => Promise<{ default: React.ComponentType<{ params: Record<string, string> }> }>;
|
|
27
|
+
|
|
28
|
+
export interface AppContext {
|
|
29
|
+
routeId: string;
|
|
30
|
+
url: string;
|
|
31
|
+
params: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type RouteComponent = (props: { params: Record<string, string> }) => React.ReactElement;
|
|
35
|
+
type CreateAppFn = (context: AppContext) => React.ReactElement;
|
|
36
|
+
|
|
37
|
+
// Registry
|
|
38
|
+
const apiHandlers: Map<string, ApiHandler> = new Map();
|
|
39
|
+
const pageLoaders: Map<string, PageLoader> = new Map();
|
|
40
|
+
const routeComponents: Map<string, RouteComponent> = new Map();
|
|
41
|
+
let createAppFn: CreateAppFn | null = null;
|
|
42
|
+
|
|
43
|
+
export function registerApiHandler(routeId: string, handler: ApiHandler): void {
|
|
44
|
+
apiHandlers.set(routeId, handler);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function registerPageLoader(routeId: string, loader: PageLoader): void {
|
|
48
|
+
pageLoaders.set(routeId, loader);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerRouteComponent(routeId: string, component: RouteComponent): void {
|
|
52
|
+
routeComponents.set(routeId, component);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function setCreateApp(fn: CreateAppFn): void {
|
|
56
|
+
createAppFn = fn;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Default createApp implementation
|
|
60
|
+
function defaultCreateApp(context: AppContext): React.ReactElement {
|
|
61
|
+
const Component = routeComponents.get(context.routeId);
|
|
62
|
+
|
|
63
|
+
if (!Component) {
|
|
64
|
+
return React.createElement("div", null,
|
|
65
|
+
React.createElement("h1", null, "404 - Route Not Found"),
|
|
66
|
+
React.createElement("p", null, `Route ID: ${context.routeId}`)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return React.createElement(Component, { params: context.params });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
74
|
+
const url = new URL(req.url);
|
|
75
|
+
const pathname = url.pathname;
|
|
76
|
+
|
|
77
|
+
const match = router.match(pathname);
|
|
78
|
+
|
|
79
|
+
if (!match) {
|
|
80
|
+
const error = createNotFoundResponse(pathname);
|
|
81
|
+
const response = formatErrorResponse(error, {
|
|
82
|
+
isDev: process.env.NODE_ENV !== "production",
|
|
83
|
+
});
|
|
84
|
+
return Response.json(response, { status: 404 });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { route, params } = match;
|
|
88
|
+
|
|
89
|
+
if (route.kind === "api") {
|
|
90
|
+
const handler = apiHandlers.get(route.id);
|
|
91
|
+
if (!handler) {
|
|
92
|
+
const error = createHandlerNotFoundResponse(route.id, route.pattern);
|
|
93
|
+
const response = formatErrorResponse(error, {
|
|
94
|
+
isDev: process.env.NODE_ENV !== "production",
|
|
95
|
+
});
|
|
96
|
+
return Response.json(response, { status: 500 });
|
|
97
|
+
}
|
|
98
|
+
return handler(req, params);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (route.kind === "page") {
|
|
102
|
+
const loader = pageLoaders.get(route.id);
|
|
103
|
+
if (loader) {
|
|
104
|
+
try {
|
|
105
|
+
const module = await loader();
|
|
106
|
+
registerRouteComponent(route.id, module.default);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const pageError = createPageLoadErrorResponse(
|
|
109
|
+
route.id,
|
|
110
|
+
route.pattern,
|
|
111
|
+
err instanceof Error ? err : new Error(String(err))
|
|
112
|
+
);
|
|
113
|
+
console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
|
|
114
|
+
const response = formatErrorResponse(pageError, {
|
|
115
|
+
isDev: process.env.NODE_ENV !== "production",
|
|
116
|
+
});
|
|
117
|
+
return Response.json(response, { status: 500 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const appCreator = createAppFn || defaultCreateApp;
|
|
122
|
+
try {
|
|
123
|
+
const app = appCreator({
|
|
124
|
+
routeId: route.id,
|
|
125
|
+
url: req.url,
|
|
126
|
+
params,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return renderSSR(app, { title: `${route.id} - Mandu` });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const ssrError = createSSRErrorResponse(
|
|
132
|
+
route.id,
|
|
133
|
+
route.pattern,
|
|
134
|
+
err instanceof Error ? err : new Error(String(err))
|
|
135
|
+
);
|
|
136
|
+
console.error(`[Mandu] ${ssrError.errorType}:`, ssrError.message);
|
|
137
|
+
const response = formatErrorResponse(ssrError, {
|
|
138
|
+
isDev: process.env.NODE_ENV !== "production",
|
|
139
|
+
});
|
|
140
|
+
return Response.json(response, { status: 500 });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Response.json({
|
|
145
|
+
errorType: "FRAMEWORK_BUG",
|
|
146
|
+
code: "MANDU_F003",
|
|
147
|
+
message: `Unknown route kind: ${route.kind}`,
|
|
148
|
+
summary: "알 수 없는 라우트 종류 - 프레임워크 버그",
|
|
149
|
+
fix: {
|
|
150
|
+
file: "spec/routes.manifest.json",
|
|
151
|
+
suggestion: "라우트의 kind는 'api' 또는 'page'여야 합니다",
|
|
152
|
+
},
|
|
153
|
+
route: {
|
|
154
|
+
id: route.id,
|
|
155
|
+
pattern: route.pattern,
|
|
156
|
+
},
|
|
157
|
+
timestamp: new Date().toISOString(),
|
|
158
|
+
}, { status: 500 });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function startServer(manifest: RoutesManifest, options: ServerOptions = {}): ManduServer {
|
|
162
|
+
const { port = 3000, hostname = "localhost" } = options;
|
|
163
|
+
|
|
164
|
+
const router = new Router(manifest.routes);
|
|
165
|
+
|
|
166
|
+
const server = Bun.serve({
|
|
167
|
+
port,
|
|
168
|
+
hostname,
|
|
169
|
+
fetch: (req) => handleRequest(req, router),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
console.log(`🥟 Mandu server running at http://${hostname}:${port}`);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
server,
|
|
176
|
+
router,
|
|
177
|
+
stop: () => server.stop(),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Clear registries (useful for testing)
|
|
182
|
+
export function clearRegistry(): void {
|
|
183
|
+
apiHandlers.clear();
|
|
184
|
+
pageLoaders.clear();
|
|
185
|
+
routeComponents.clear();
|
|
186
|
+
createAppFn = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { apiHandlers, pageLoaders, routeComponents };
|
package/src/runtime/ssr.ts
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { renderToString } from "react-dom/server";
|
|
2
|
-
import type { ReactElement } from "react";
|
|
3
|
-
|
|
4
|
-
export interface SSROptions {
|
|
5
|
-
title?: string;
|
|
6
|
-
lang?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function renderToHTML(element: ReactElement, options: SSROptions = {}): string {
|
|
10
|
-
const { title = "Mandu App", lang = "ko" } = options;
|
|
11
|
-
const content = renderToString(element);
|
|
12
|
-
|
|
13
|
-
return `<!doctype html>
|
|
14
|
-
<html lang="${lang}">
|
|
15
|
-
<head>
|
|
16
|
-
<meta charset="UTF-8">
|
|
17
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18
|
-
<title>${title}</title>
|
|
19
|
-
</head>
|
|
20
|
-
<body>
|
|
21
|
-
<div id="root">${content}</div>
|
|
22
|
-
</body>
|
|
23
|
-
</html>`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function createHTMLResponse(html: string, status: number = 200): Response {
|
|
27
|
-
return new Response(html, {
|
|
28
|
-
status,
|
|
29
|
-
headers: {
|
|
30
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function renderSSR(element: ReactElement, options: SSROptions = {}): Response {
|
|
36
|
-
const html = renderToHTML(element, options);
|
|
37
|
-
return createHTMLResponse(html);
|
|
38
|
-
}
|
|
1
|
+
import { renderToString } from "react-dom/server";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
|
|
4
|
+
export interface SSROptions {
|
|
5
|
+
title?: string;
|
|
6
|
+
lang?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function renderToHTML(element: ReactElement, options: SSROptions = {}): string {
|
|
10
|
+
const { title = "Mandu App", lang = "ko" } = options;
|
|
11
|
+
const content = renderToString(element);
|
|
12
|
+
|
|
13
|
+
return `<!doctype html>
|
|
14
|
+
<html lang="${lang}">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="UTF-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18
|
+
<title>${title}</title>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<div id="root">${content}</div>
|
|
22
|
+
</body>
|
|
23
|
+
</html>`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createHTMLResponse(html: string, status: number = 200): Response {
|
|
27
|
+
return new Response(html, {
|
|
28
|
+
status,
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function renderSSR(element: ReactElement, options: SSROptions = {}): Response {
|
|
36
|
+
const html = renderToHTML(element, options);
|
|
37
|
+
return createHTMLResponse(html);
|
|
38
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot Content Corrector
|
|
3
|
+
* 슬롯 파일의 자동 수정 가능한 문제를 해결합니다.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SlotValidationIssue } from "./validator";
|
|
7
|
+
|
|
8
|
+
export interface CorrectionResult {
|
|
9
|
+
corrected: boolean;
|
|
10
|
+
content: string;
|
|
11
|
+
appliedFixes: AppliedFix[];
|
|
12
|
+
remainingIssues: SlotValidationIssue[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AppliedFix {
|
|
16
|
+
code: string;
|
|
17
|
+
description: string;
|
|
18
|
+
before?: string;
|
|
19
|
+
after?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 금지된 import를 대체할 안전한 패턴
|
|
23
|
+
const SAFE_ALTERNATIVES: Record<string, string> = {
|
|
24
|
+
fs: "// Use Bun.file() or Bun.write() instead of fs",
|
|
25
|
+
"node:fs": "// Use Bun.file() or Bun.write() instead of fs",
|
|
26
|
+
child_process: "// Use Bun.spawn() or Bun.spawnSync() instead",
|
|
27
|
+
"node:child_process": "// Use Bun.spawn() or Bun.spawnSync() instead",
|
|
28
|
+
cluster: "// Clustering should be handled at the infrastructure level",
|
|
29
|
+
"node:cluster": "// Clustering should be handled at the infrastructure level",
|
|
30
|
+
worker_threads: "// Use Bun workers or external job queues",
|
|
31
|
+
"node:worker_threads": "// Use Bun workers or external job queues",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 슬롯 내용을 자동 수정합니다.
|
|
36
|
+
*/
|
|
37
|
+
export function correctSlotContent(
|
|
38
|
+
content: string,
|
|
39
|
+
issues: SlotValidationIssue[]
|
|
40
|
+
): CorrectionResult {
|
|
41
|
+
let correctedContent = content;
|
|
42
|
+
const appliedFixes: AppliedFix[] = [];
|
|
43
|
+
const remainingIssues: SlotValidationIssue[] = [];
|
|
44
|
+
|
|
45
|
+
for (const issue of issues) {
|
|
46
|
+
if (!issue.autoFixable) {
|
|
47
|
+
remainingIssues.push(issue);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = applyFix(correctedContent, issue);
|
|
52
|
+
if (result.fixed) {
|
|
53
|
+
correctedContent = result.content;
|
|
54
|
+
appliedFixes.push({
|
|
55
|
+
code: issue.code,
|
|
56
|
+
description: issue.message,
|
|
57
|
+
before: result.before,
|
|
58
|
+
after: result.after,
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
remainingIssues.push(issue);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
corrected: appliedFixes.length > 0,
|
|
67
|
+
content: correctedContent,
|
|
68
|
+
appliedFixes,
|
|
69
|
+
remainingIssues,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface FixResult {
|
|
74
|
+
fixed: boolean;
|
|
75
|
+
content: string;
|
|
76
|
+
before?: string;
|
|
77
|
+
after?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function applyFix(content: string, issue: SlotValidationIssue): FixResult {
|
|
81
|
+
switch (issue.code) {
|
|
82
|
+
case "FORBIDDEN_IMPORT":
|
|
83
|
+
return fixForbiddenImport(content, issue);
|
|
84
|
+
|
|
85
|
+
case "MISSING_MANDU_IMPORT":
|
|
86
|
+
return fixMissingManduImport(content);
|
|
87
|
+
|
|
88
|
+
case "MISSING_DEFAULT_EXPORT":
|
|
89
|
+
return fixMissingDefaultExport(content);
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
return { fixed: false, content };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 금지된 import를 제거하고 주석으로 대체
|
|
98
|
+
*/
|
|
99
|
+
function fixForbiddenImport(
|
|
100
|
+
content: string,
|
|
101
|
+
issue: SlotValidationIssue
|
|
102
|
+
): FixResult {
|
|
103
|
+
const lines = content.split("\n");
|
|
104
|
+
|
|
105
|
+
if (!issue.line) {
|
|
106
|
+
return { fixed: false, content };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lineIndex = issue.line - 1;
|
|
110
|
+
const originalLine = lines[lineIndex];
|
|
111
|
+
|
|
112
|
+
// 어떤 금지된 모듈인지 찾기
|
|
113
|
+
let forbiddenModule = "";
|
|
114
|
+
for (const [module, alternative] of Object.entries(SAFE_ALTERNATIVES)) {
|
|
115
|
+
if (originalLine.includes(`'${module}'`) || originalLine.includes(`"${module}"`)) {
|
|
116
|
+
forbiddenModule = module;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!forbiddenModule) {
|
|
122
|
+
return { fixed: false, content };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// import 문을 주석으로 대체
|
|
126
|
+
const alternative = SAFE_ALTERNATIVES[forbiddenModule];
|
|
127
|
+
lines[lineIndex] = `// REMOVED: ${originalLine.trim()}\n${alternative}`;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
fixed: true,
|
|
131
|
+
content: lines.join("\n"),
|
|
132
|
+
before: originalLine,
|
|
133
|
+
after: lines[lineIndex],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Mandu import 추가
|
|
139
|
+
*/
|
|
140
|
+
function fixMissingManduImport(content: string): FixResult {
|
|
141
|
+
const manduImport = `import { Mandu } from "@mandujs/core";\n`;
|
|
142
|
+
|
|
143
|
+
// 이미 다른 import가 있는지 확인
|
|
144
|
+
const hasImports = /^import\s+/m.test(content);
|
|
145
|
+
|
|
146
|
+
let newContent: string;
|
|
147
|
+
if (hasImports) {
|
|
148
|
+
// 첫 번째 import 앞에 추가
|
|
149
|
+
newContent = content.replace(/^(import\s+)/m, `${manduImport}$1`);
|
|
150
|
+
} else {
|
|
151
|
+
// 파일 맨 앞에 추가
|
|
152
|
+
newContent = manduImport + content;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
fixed: true,
|
|
157
|
+
content: newContent,
|
|
158
|
+
before: "(없음)",
|
|
159
|
+
after: manduImport.trim(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* default export 추가
|
|
165
|
+
*/
|
|
166
|
+
function fixMissingDefaultExport(content: string): FixResult {
|
|
167
|
+
// Mandu.filling()이 있는지 확인
|
|
168
|
+
const fillingMatch = content.match(/Mandu\s*\.\s*filling\s*\(\s*\)/);
|
|
169
|
+
|
|
170
|
+
if (!fillingMatch) {
|
|
171
|
+
// filling 패턴이 없으면 수정 불가
|
|
172
|
+
return { fixed: false, content };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// export default가 없는 Mandu.filling() 찾기
|
|
176
|
+
// 예: const handler = Mandu.filling()... -> export default Mandu.filling()...
|
|
177
|
+
const patterns = [
|
|
178
|
+
// const/let/var handler = Mandu.filling()
|
|
179
|
+
/^(\s*)(const|let|var)\s+\w+\s*=\s*(Mandu\s*\.\s*filling\s*\(\s*\))/m,
|
|
180
|
+
// 단독 Mandu.filling()
|
|
181
|
+
/^(\s*)(Mandu\s*\.\s*filling\s*\(\s*\))/m,
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
for (const pattern of patterns) {
|
|
185
|
+
if (pattern.test(content)) {
|
|
186
|
+
const newContent = content.replace(pattern, "$1export default $3");
|
|
187
|
+
return {
|
|
188
|
+
fixed: true,
|
|
189
|
+
content: newContent,
|
|
190
|
+
before: content.match(pattern)?.[0],
|
|
191
|
+
after: newContent.match(/export default Mandu\.filling\(\)/)?.[0],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 파일 끝에 export default 추가 시도
|
|
197
|
+
if (content.includes("Mandu.filling()") && !content.includes("export default")) {
|
|
198
|
+
// 마지막 세미콜론 또는 중괄호 뒤에 추가
|
|
199
|
+
const lastLine = content.trimEnd();
|
|
200
|
+
if (!lastLine.endsWith(";") && !lastLine.endsWith("}")) {
|
|
201
|
+
return { fixed: false, content };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { fixed: false, content };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 여러 번의 수정 시도 (Self-correction loop)
|
|
210
|
+
*/
|
|
211
|
+
export async function runSlotCorrection(
|
|
212
|
+
content: string,
|
|
213
|
+
validateFn: (content: string) => { valid: boolean; issues: SlotValidationIssue[] },
|
|
214
|
+
maxRetries: number = 3
|
|
215
|
+
): Promise<{
|
|
216
|
+
success: boolean;
|
|
217
|
+
finalContent: string;
|
|
218
|
+
attempts: number;
|
|
219
|
+
allFixes: AppliedFix[];
|
|
220
|
+
remainingIssues: SlotValidationIssue[];
|
|
221
|
+
}> {
|
|
222
|
+
let currentContent = content;
|
|
223
|
+
let attempts = 0;
|
|
224
|
+
const allFixes: AppliedFix[] = [];
|
|
225
|
+
|
|
226
|
+
while (attempts < maxRetries) {
|
|
227
|
+
attempts++;
|
|
228
|
+
|
|
229
|
+
// 1. 검증
|
|
230
|
+
const validation = validateFn(currentContent);
|
|
231
|
+
|
|
232
|
+
if (validation.valid) {
|
|
233
|
+
return {
|
|
234
|
+
success: true,
|
|
235
|
+
finalContent: currentContent,
|
|
236
|
+
attempts,
|
|
237
|
+
allFixes,
|
|
238
|
+
remainingIssues: [],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 2. 자동 수정 가능한 문제가 있는지 확인
|
|
243
|
+
const autoFixable = validation.issues.filter((i) => i.autoFixable);
|
|
244
|
+
if (autoFixable.length === 0) {
|
|
245
|
+
// 자동 수정 불가능한 문제만 남음
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
finalContent: currentContent,
|
|
249
|
+
attempts,
|
|
250
|
+
allFixes,
|
|
251
|
+
remainingIssues: validation.issues,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 3. 수정 적용
|
|
256
|
+
const correction = correctSlotContent(currentContent, validation.issues);
|
|
257
|
+
allFixes.push(...correction.appliedFixes);
|
|
258
|
+
|
|
259
|
+
if (!correction.corrected) {
|
|
260
|
+
// 수정이 적용되지 않음
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
finalContent: currentContent,
|
|
264
|
+
attempts,
|
|
265
|
+
allFixes,
|
|
266
|
+
remainingIssues: validation.issues,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
currentContent = correction.content;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// maxRetries 도달
|
|
274
|
+
const finalValidation = validateFn(currentContent);
|
|
275
|
+
return {
|
|
276
|
+
success: finalValidation.valid,
|
|
277
|
+
finalContent: currentContent,
|
|
278
|
+
attempts,
|
|
279
|
+
allFixes,
|
|
280
|
+
remainingIssues: finalValidation.issues,
|
|
281
|
+
};
|
|
282
|
+
}
|