@mandujs/core 0.5.4 → 0.5.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/README.ko.md +200 -200
- package/README.md +200 -200
- package/package.json +2 -2
- package/src/contract/validator.ts +2 -2
- package/src/filling/auth.ts +308 -0
- package/src/filling/context.ts +7 -1
- package/src/filling/filling.ts +83 -4
- package/src/filling/index.ts +15 -2
- package/src/generator/generate.ts +27 -6
- package/src/generator/index.ts +3 -3
- package/src/report/index.ts +1 -1
- package/src/runtime/index.ts +5 -5
- package/src/runtime/router.ts +83 -65
- package/src/runtime/server.ts +425 -425
- package/src/runtime/ssr.ts +248 -248
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
package/src/runtime/ssr.ts
CHANGED
|
@@ -1,248 +1,248 @@
|
|
|
1
|
-
import { renderToString } from "react-dom/server";
|
|
2
|
-
import type { ReactElement } from "react";
|
|
3
|
-
import type { BundleManifest } from "../bundler/types";
|
|
4
|
-
import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
5
|
-
|
|
6
|
-
export interface SSROptions {
|
|
7
|
-
title?: string;
|
|
8
|
-
lang?: string;
|
|
9
|
-
/** 서버에서 로드한 데이터 (클라이언트로 전달) */
|
|
10
|
-
serverData?: Record<string, unknown>;
|
|
11
|
-
/** Hydration 설정 */
|
|
12
|
-
hydration?: HydrationConfig;
|
|
13
|
-
/** 번들 매니페스트 */
|
|
14
|
-
bundleManifest?: BundleManifest;
|
|
15
|
-
/** 라우트 ID (island 식별용) */
|
|
16
|
-
routeId?: string;
|
|
17
|
-
/** 추가 head 태그 */
|
|
18
|
-
headTags?: string;
|
|
19
|
-
/** 추가 body 끝 태그 */
|
|
20
|
-
bodyEndTags?: string;
|
|
21
|
-
/** 개발 모드 여부 */
|
|
22
|
-
isDev?: boolean;
|
|
23
|
-
/** HMR 포트 (개발 모드에서 사용) */
|
|
24
|
-
hmrPort?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* SSR 데이터를 안전하게 직렬화
|
|
29
|
-
*/
|
|
30
|
-
function serializeServerData(data: Record<string, unknown>): string {
|
|
31
|
-
// XSS 방지를 위한 이스케이프
|
|
32
|
-
const json = JSON.stringify(data)
|
|
33
|
-
.replace(/</g, "\\u003c")
|
|
34
|
-
.replace(/>/g, "\\u003e")
|
|
35
|
-
.replace(/&/g, "\\u0026")
|
|
36
|
-
.replace(/'/g, "\\u0027");
|
|
37
|
-
|
|
38
|
-
return `<script id="__MANDU_DATA__" type="application/json">${json}</script>
|
|
39
|
-
<script>window.__MANDU_DATA__ = JSON.parse(document.getElementById('__MANDU_DATA__').textContent);</script>`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Import map 생성 (bare specifier 해결용)
|
|
44
|
-
*/
|
|
45
|
-
function generateImportMap(manifest: BundleManifest): string {
|
|
46
|
-
if (!manifest.importMap || Object.keys(manifest.importMap.imports).length === 0) {
|
|
47
|
-
return "";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const importMapJson = JSON.stringify(manifest.importMap, null, 2);
|
|
51
|
-
return `<script type="importmap">${importMapJson}</script>`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Hydration 스크립트 태그 생성
|
|
56
|
-
*/
|
|
57
|
-
function generateHydrationScripts(
|
|
58
|
-
routeId: string,
|
|
59
|
-
manifest: BundleManifest
|
|
60
|
-
): string {
|
|
61
|
-
const scripts: string[] = [];
|
|
62
|
-
|
|
63
|
-
// Import map 먼저 (반드시 module scripts 전에 위치해야 함)
|
|
64
|
-
const importMap = generateImportMap(manifest);
|
|
65
|
-
if (importMap) {
|
|
66
|
-
scripts.push(importMap);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Runtime 로드
|
|
70
|
-
if (manifest.shared.runtime) {
|
|
71
|
-
scripts.push(`<script type="module" src="${manifest.shared.runtime}"></script>`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Island 번들 로드
|
|
75
|
-
const bundle = manifest.bundles[routeId];
|
|
76
|
-
if (bundle) {
|
|
77
|
-
// Preload (선택적)
|
|
78
|
-
scripts.push(`<link rel="modulepreload" href="${bundle.js}">`);
|
|
79
|
-
scripts.push(`<script type="module" src="${bundle.js}"></script>`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return scripts.join("\n");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Island 래퍼로 컨텐츠 감싸기
|
|
87
|
-
*/
|
|
88
|
-
export function wrapWithIsland(
|
|
89
|
-
content: string,
|
|
90
|
-
routeId: string,
|
|
91
|
-
priority: HydrationPriority = "visible"
|
|
92
|
-
): string {
|
|
93
|
-
return `<div data-mandu-island="${routeId}" data-mandu-priority="${priority}">${content}</div>`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function renderToHTML(element: ReactElement, options: SSROptions = {}): string {
|
|
97
|
-
const {
|
|
98
|
-
title = "Mandu App",
|
|
99
|
-
lang = "ko",
|
|
100
|
-
serverData,
|
|
101
|
-
hydration,
|
|
102
|
-
bundleManifest,
|
|
103
|
-
routeId,
|
|
104
|
-
headTags = "",
|
|
105
|
-
bodyEndTags = "",
|
|
106
|
-
isDev = false,
|
|
107
|
-
hmrPort,
|
|
108
|
-
} = options;
|
|
109
|
-
|
|
110
|
-
let content = renderToString(element);
|
|
111
|
-
|
|
112
|
-
// Island 래퍼 적용 (hydration 필요 시)
|
|
113
|
-
const needsHydration =
|
|
114
|
-
hydration && hydration.strategy !== "none" && routeId && bundleManifest;
|
|
115
|
-
|
|
116
|
-
if (needsHydration) {
|
|
117
|
-
content = wrapWithIsland(content, routeId, hydration.priority);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 서버 데이터 스크립트
|
|
121
|
-
let dataScript = "";
|
|
122
|
-
if (serverData && routeId) {
|
|
123
|
-
const wrappedData = {
|
|
124
|
-
[routeId]: {
|
|
125
|
-
serverData,
|
|
126
|
-
timestamp: Date.now(),
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
dataScript = serializeServerData(wrappedData);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Hydration 스크립트
|
|
133
|
-
let hydrationScripts = "";
|
|
134
|
-
if (needsHydration && bundleManifest) {
|
|
135
|
-
hydrationScripts = generateHydrationScripts(routeId, bundleManifest);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// HMR 스크립트 (개발 모드)
|
|
139
|
-
let hmrScript = "";
|
|
140
|
-
if (isDev && hmrPort) {
|
|
141
|
-
hmrScript = generateHMRScript(hmrPort);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return `<!doctype html>
|
|
145
|
-
<html lang="${lang}">
|
|
146
|
-
<head>
|
|
147
|
-
<meta charset="UTF-8">
|
|
148
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
149
|
-
<title>${title}</title>
|
|
150
|
-
${headTags}
|
|
151
|
-
</head>
|
|
152
|
-
<body>
|
|
153
|
-
<div id="root">${content}</div>
|
|
154
|
-
${dataScript}
|
|
155
|
-
${hydrationScripts}
|
|
156
|
-
${hmrScript}
|
|
157
|
-
${bodyEndTags}
|
|
158
|
-
</body>
|
|
159
|
-
</html>`;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* HMR 스크립트 생성
|
|
164
|
-
*/
|
|
165
|
-
function generateHMRScript(port: number): string {
|
|
166
|
-
const hmrPort = port + 1;
|
|
167
|
-
return `<script>
|
|
168
|
-
(function() {
|
|
169
|
-
var ws = null;
|
|
170
|
-
var reconnectAttempts = 0;
|
|
171
|
-
var maxReconnectAttempts = 10;
|
|
172
|
-
|
|
173
|
-
function connect() {
|
|
174
|
-
try {
|
|
175
|
-
ws = new WebSocket('ws://localhost:${hmrPort}');
|
|
176
|
-
ws.onopen = function() {
|
|
177
|
-
console.log('[Mandu HMR] Connected');
|
|
178
|
-
reconnectAttempts = 0;
|
|
179
|
-
};
|
|
180
|
-
ws.onmessage = function(e) {
|
|
181
|
-
try {
|
|
182
|
-
var msg = JSON.parse(e.data);
|
|
183
|
-
if (msg.type === 'reload' || msg.type === 'island-update') {
|
|
184
|
-
console.log('[Mandu HMR] Reloading...');
|
|
185
|
-
location.reload();
|
|
186
|
-
} else if (msg.type === 'error') {
|
|
187
|
-
console.error('[Mandu HMR] Build error:', msg.data?.message);
|
|
188
|
-
}
|
|
189
|
-
} catch(err) {}
|
|
190
|
-
};
|
|
191
|
-
ws.onclose = function() {
|
|
192
|
-
if (reconnectAttempts < maxReconnectAttempts) {
|
|
193
|
-
reconnectAttempts++;
|
|
194
|
-
setTimeout(connect, 1000 * reconnectAttempts);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
} catch(err) {
|
|
198
|
-
setTimeout(connect, 1000);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
connect();
|
|
202
|
-
})();
|
|
203
|
-
</script>`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function createHTMLResponse(html: string, status: number = 200): Response {
|
|
207
|
-
return new Response(html, {
|
|
208
|
-
status,
|
|
209
|
-
headers: {
|
|
210
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function renderSSR(element: ReactElement, options: SSROptions = {}): Response {
|
|
216
|
-
const html = renderToHTML(element, options);
|
|
217
|
-
return createHTMLResponse(html);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Hydration이 포함된 SSR 렌더링
|
|
222
|
-
*
|
|
223
|
-
* @example
|
|
224
|
-
* ```typescript
|
|
225
|
-
* const response = await renderWithHydration(
|
|
226
|
-
* <TodoList todos={todos} />,
|
|
227
|
-
* {
|
|
228
|
-
* title: "할일 목록",
|
|
229
|
-
* routeId: "todos",
|
|
230
|
-
* serverData: { todos },
|
|
231
|
-
* hydration: { strategy: "island", priority: "visible" },
|
|
232
|
-
* bundleManifest,
|
|
233
|
-
* }
|
|
234
|
-
* );
|
|
235
|
-
* ```
|
|
236
|
-
*/
|
|
237
|
-
export async function renderWithHydration(
|
|
238
|
-
element: ReactElement,
|
|
239
|
-
options: SSROptions & {
|
|
240
|
-
routeId: string;
|
|
241
|
-
serverData: Record<string, unknown>;
|
|
242
|
-
hydration: HydrationConfig;
|
|
243
|
-
bundleManifest: BundleManifest;
|
|
244
|
-
}
|
|
245
|
-
): Promise<Response> {
|
|
246
|
-
const html = renderToHTML(element, options);
|
|
247
|
-
return createHTMLResponse(html);
|
|
248
|
-
}
|
|
1
|
+
import { renderToString } from "react-dom/server";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import type { BundleManifest } from "../bundler/types";
|
|
4
|
+
import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
5
|
+
|
|
6
|
+
export interface SSROptions {
|
|
7
|
+
title?: string;
|
|
8
|
+
lang?: string;
|
|
9
|
+
/** 서버에서 로드한 데이터 (클라이언트로 전달) */
|
|
10
|
+
serverData?: Record<string, unknown>;
|
|
11
|
+
/** Hydration 설정 */
|
|
12
|
+
hydration?: HydrationConfig;
|
|
13
|
+
/** 번들 매니페스트 */
|
|
14
|
+
bundleManifest?: BundleManifest;
|
|
15
|
+
/** 라우트 ID (island 식별용) */
|
|
16
|
+
routeId?: string;
|
|
17
|
+
/** 추가 head 태그 */
|
|
18
|
+
headTags?: string;
|
|
19
|
+
/** 추가 body 끝 태그 */
|
|
20
|
+
bodyEndTags?: string;
|
|
21
|
+
/** 개발 모드 여부 */
|
|
22
|
+
isDev?: boolean;
|
|
23
|
+
/** HMR 포트 (개발 모드에서 사용) */
|
|
24
|
+
hmrPort?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SSR 데이터를 안전하게 직렬화
|
|
29
|
+
*/
|
|
30
|
+
function serializeServerData(data: Record<string, unknown>): string {
|
|
31
|
+
// XSS 방지를 위한 이스케이프
|
|
32
|
+
const json = JSON.stringify(data)
|
|
33
|
+
.replace(/</g, "\\u003c")
|
|
34
|
+
.replace(/>/g, "\\u003e")
|
|
35
|
+
.replace(/&/g, "\\u0026")
|
|
36
|
+
.replace(/'/g, "\\u0027");
|
|
37
|
+
|
|
38
|
+
return `<script id="__MANDU_DATA__" type="application/json">${json}</script>
|
|
39
|
+
<script>window.__MANDU_DATA__ = JSON.parse(document.getElementById('__MANDU_DATA__').textContent);</script>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Import map 생성 (bare specifier 해결용)
|
|
44
|
+
*/
|
|
45
|
+
function generateImportMap(manifest: BundleManifest): string {
|
|
46
|
+
if (!manifest.importMap || Object.keys(manifest.importMap.imports).length === 0) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const importMapJson = JSON.stringify(manifest.importMap, null, 2);
|
|
51
|
+
return `<script type="importmap">${importMapJson}</script>`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hydration 스크립트 태그 생성
|
|
56
|
+
*/
|
|
57
|
+
function generateHydrationScripts(
|
|
58
|
+
routeId: string,
|
|
59
|
+
manifest: BundleManifest
|
|
60
|
+
): string {
|
|
61
|
+
const scripts: string[] = [];
|
|
62
|
+
|
|
63
|
+
// Import map 먼저 (반드시 module scripts 전에 위치해야 함)
|
|
64
|
+
const importMap = generateImportMap(manifest);
|
|
65
|
+
if (importMap) {
|
|
66
|
+
scripts.push(importMap);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Runtime 로드
|
|
70
|
+
if (manifest.shared.runtime) {
|
|
71
|
+
scripts.push(`<script type="module" src="${manifest.shared.runtime}"></script>`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Island 번들 로드
|
|
75
|
+
const bundle = manifest.bundles[routeId];
|
|
76
|
+
if (bundle) {
|
|
77
|
+
// Preload (선택적)
|
|
78
|
+
scripts.push(`<link rel="modulepreload" href="${bundle.js}">`);
|
|
79
|
+
scripts.push(`<script type="module" src="${bundle.js}"></script>`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return scripts.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Island 래퍼로 컨텐츠 감싸기
|
|
87
|
+
*/
|
|
88
|
+
export function wrapWithIsland(
|
|
89
|
+
content: string,
|
|
90
|
+
routeId: string,
|
|
91
|
+
priority: HydrationPriority = "visible"
|
|
92
|
+
): string {
|
|
93
|
+
return `<div data-mandu-island="${routeId}" data-mandu-priority="${priority}">${content}</div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function renderToHTML(element: ReactElement, options: SSROptions = {}): string {
|
|
97
|
+
const {
|
|
98
|
+
title = "Mandu App",
|
|
99
|
+
lang = "ko",
|
|
100
|
+
serverData,
|
|
101
|
+
hydration,
|
|
102
|
+
bundleManifest,
|
|
103
|
+
routeId,
|
|
104
|
+
headTags = "",
|
|
105
|
+
bodyEndTags = "",
|
|
106
|
+
isDev = false,
|
|
107
|
+
hmrPort,
|
|
108
|
+
} = options;
|
|
109
|
+
|
|
110
|
+
let content = renderToString(element);
|
|
111
|
+
|
|
112
|
+
// Island 래퍼 적용 (hydration 필요 시)
|
|
113
|
+
const needsHydration =
|
|
114
|
+
hydration && hydration.strategy !== "none" && routeId && bundleManifest;
|
|
115
|
+
|
|
116
|
+
if (needsHydration) {
|
|
117
|
+
content = wrapWithIsland(content, routeId, hydration.priority);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 서버 데이터 스크립트
|
|
121
|
+
let dataScript = "";
|
|
122
|
+
if (serverData && routeId) {
|
|
123
|
+
const wrappedData = {
|
|
124
|
+
[routeId]: {
|
|
125
|
+
serverData,
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
dataScript = serializeServerData(wrappedData);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Hydration 스크립트
|
|
133
|
+
let hydrationScripts = "";
|
|
134
|
+
if (needsHydration && bundleManifest) {
|
|
135
|
+
hydrationScripts = generateHydrationScripts(routeId, bundleManifest);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// HMR 스크립트 (개발 모드)
|
|
139
|
+
let hmrScript = "";
|
|
140
|
+
if (isDev && hmrPort) {
|
|
141
|
+
hmrScript = generateHMRScript(hmrPort);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return `<!doctype html>
|
|
145
|
+
<html lang="${lang}">
|
|
146
|
+
<head>
|
|
147
|
+
<meta charset="UTF-8">
|
|
148
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
149
|
+
<title>${title}</title>
|
|
150
|
+
${headTags}
|
|
151
|
+
</head>
|
|
152
|
+
<body>
|
|
153
|
+
<div id="root">${content}</div>
|
|
154
|
+
${dataScript}
|
|
155
|
+
${hydrationScripts}
|
|
156
|
+
${hmrScript}
|
|
157
|
+
${bodyEndTags}
|
|
158
|
+
</body>
|
|
159
|
+
</html>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* HMR 스크립트 생성
|
|
164
|
+
*/
|
|
165
|
+
function generateHMRScript(port: number): string {
|
|
166
|
+
const hmrPort = port + 1;
|
|
167
|
+
return `<script>
|
|
168
|
+
(function() {
|
|
169
|
+
var ws = null;
|
|
170
|
+
var reconnectAttempts = 0;
|
|
171
|
+
var maxReconnectAttempts = 10;
|
|
172
|
+
|
|
173
|
+
function connect() {
|
|
174
|
+
try {
|
|
175
|
+
ws = new WebSocket('ws://localhost:${hmrPort}');
|
|
176
|
+
ws.onopen = function() {
|
|
177
|
+
console.log('[Mandu HMR] Connected');
|
|
178
|
+
reconnectAttempts = 0;
|
|
179
|
+
};
|
|
180
|
+
ws.onmessage = function(e) {
|
|
181
|
+
try {
|
|
182
|
+
var msg = JSON.parse(e.data);
|
|
183
|
+
if (msg.type === 'reload' || msg.type === 'island-update') {
|
|
184
|
+
console.log('[Mandu HMR] Reloading...');
|
|
185
|
+
location.reload();
|
|
186
|
+
} else if (msg.type === 'error') {
|
|
187
|
+
console.error('[Mandu HMR] Build error:', msg.data?.message);
|
|
188
|
+
}
|
|
189
|
+
} catch(err) {}
|
|
190
|
+
};
|
|
191
|
+
ws.onclose = function() {
|
|
192
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
193
|
+
reconnectAttempts++;
|
|
194
|
+
setTimeout(connect, 1000 * reconnectAttempts);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
} catch(err) {
|
|
198
|
+
setTimeout(connect, 1000);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
connect();
|
|
202
|
+
})();
|
|
203
|
+
</script>`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function createHTMLResponse(html: string, status: number = 200): Response {
|
|
207
|
+
return new Response(html, {
|
|
208
|
+
status,
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function renderSSR(element: ReactElement, options: SSROptions = {}): Response {
|
|
216
|
+
const html = renderToHTML(element, options);
|
|
217
|
+
return createHTMLResponse(html);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Hydration이 포함된 SSR 렌더링
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* const response = await renderWithHydration(
|
|
226
|
+
* <TodoList todos={todos} />,
|
|
227
|
+
* {
|
|
228
|
+
* title: "할일 목록",
|
|
229
|
+
* routeId: "todos",
|
|
230
|
+
* serverData: { todos },
|
|
231
|
+
* hydration: { strategy: "island", priority: "visible" },
|
|
232
|
+
* bundleManifest,
|
|
233
|
+
* }
|
|
234
|
+
* );
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export async function renderWithHydration(
|
|
238
|
+
element: ReactElement,
|
|
239
|
+
options: SSROptions & {
|
|
240
|
+
routeId: string;
|
|
241
|
+
serverData: Record<string, unknown>;
|
|
242
|
+
hydration: HydrationConfig;
|
|
243
|
+
bundleManifest: BundleManifest;
|
|
244
|
+
}
|
|
245
|
+
): Promise<Response> {
|
|
246
|
+
const html = renderToHTML(element, options);
|
|
247
|
+
return createHTMLResponse(html);
|
|
248
|
+
}
|
package/src/spec/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./schema";
|
|
2
|
-
export * from "./load";
|
|
3
|
-
export * from "./lock";
|
|
1
|
+
export * from "./schema";
|
|
2
|
+
export * from "./load";
|
|
3
|
+
export * from "./lock";
|
package/src/spec/load.ts
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import { RoutesManifest, type RoutesManifest as RoutesManifestType } from "./schema";
|
|
2
|
-
import { ZodError } from "zod";
|
|
3
|
-
|
|
4
|
-
export interface LoadResult {
|
|
5
|
-
success: boolean;
|
|
6
|
-
data?: RoutesManifestType;
|
|
7
|
-
errors?: string[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function formatZodError(error: ZodError): string[] {
|
|
11
|
-
return error.errors.map((e) => {
|
|
12
|
-
const path = e.path.length > 0 ? `[${e.path.join(".")}] ` : "";
|
|
13
|
-
return `${path}${e.message}`;
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function loadManifest(filePath: string): Promise<LoadResult> {
|
|
18
|
-
try {
|
|
19
|
-
const file = Bun.file(filePath);
|
|
20
|
-
const exists = await file.exists();
|
|
21
|
-
|
|
22
|
-
if (!exists) {
|
|
23
|
-
return {
|
|
24
|
-
success: false,
|
|
25
|
-
errors: [`파일을 찾을 수 없습니다: ${filePath}`],
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const content = await file.text();
|
|
30
|
-
let json: unknown;
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
json = JSON.parse(content);
|
|
34
|
-
} catch {
|
|
35
|
-
return {
|
|
36
|
-
success: false,
|
|
37
|
-
errors: ["JSON 파싱 실패: 올바른 JSON 형식이 아닙니다"],
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const result = RoutesManifest.safeParse(json);
|
|
42
|
-
|
|
43
|
-
if (!result.success) {
|
|
44
|
-
return {
|
|
45
|
-
success: false,
|
|
46
|
-
errors: formatZodError(result.error),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
success: true,
|
|
52
|
-
data: result.data,
|
|
53
|
-
};
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return {
|
|
56
|
-
success: false,
|
|
57
|
-
errors: [`예상치 못한 오류: ${error instanceof Error ? error.message : String(error)}`],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function validateManifest(data: unknown): LoadResult {
|
|
63
|
-
const result = RoutesManifest.safeParse(data);
|
|
64
|
-
|
|
65
|
-
if (!result.success) {
|
|
66
|
-
return {
|
|
67
|
-
success: false,
|
|
68
|
-
errors: formatZodError(result.error),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
success: true,
|
|
74
|
-
data: result.data,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
1
|
+
import { RoutesManifest, type RoutesManifest as RoutesManifestType } from "./schema";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
|
|
4
|
+
export interface LoadResult {
|
|
5
|
+
success: boolean;
|
|
6
|
+
data?: RoutesManifestType;
|
|
7
|
+
errors?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatZodError(error: ZodError): string[] {
|
|
11
|
+
return error.errors.map((e) => {
|
|
12
|
+
const path = e.path.length > 0 ? `[${e.path.join(".")}] ` : "";
|
|
13
|
+
return `${path}${e.message}`;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadManifest(filePath: string): Promise<LoadResult> {
|
|
18
|
+
try {
|
|
19
|
+
const file = Bun.file(filePath);
|
|
20
|
+
const exists = await file.exists();
|
|
21
|
+
|
|
22
|
+
if (!exists) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
errors: [`파일을 찾을 수 없습니다: ${filePath}`],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = await file.text();
|
|
30
|
+
let json: unknown;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
json = JSON.parse(content);
|
|
34
|
+
} catch {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
errors: ["JSON 파싱 실패: 올바른 JSON 형식이 아닙니다"],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = RoutesManifest.safeParse(json);
|
|
42
|
+
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
errors: formatZodError(result.error),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
data: result.data,
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
errors: [`예상치 못한 오류: ${error instanceof Error ? error.message : String(error)}`],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function validateManifest(data: unknown): LoadResult {
|
|
63
|
+
const result = RoutesManifest.safeParse(data);
|
|
64
|
+
|
|
65
|
+
if (!result.success) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
errors: formatZodError(result.error),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
data: result.data,
|
|
75
|
+
};
|
|
76
|
+
}
|