@mandujs/core 0.12.2 → 0.13.1
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 +304 -304
- package/package.json +1 -1
- package/src/brain/architecture/analyzer.ts +28 -26
- package/src/brain/doctor/analyzer.ts +1 -1
- package/src/bundler/dev.ts +0 -1
- package/src/change/history.ts +3 -3
- package/src/change/snapshot.ts +10 -9
- package/src/change/transaction.ts +2 -2
- package/src/config/mandu.ts +103 -96
- package/src/config/validate.ts +225 -215
- package/src/error/classifier.ts +2 -2
- package/src/error/formatter.ts +32 -32
- package/src/error/stack-analyzer.ts +5 -0
- package/src/filling/context.ts +592 -569
- package/src/filling/index.ts +2 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/contract-glue.ts +2 -1
- package/src/generator/generate.ts +12 -10
- package/src/generator/templates.ts +80 -79
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +128 -128
- package/src/guard/presets/cqrs.test.ts +35 -14
- package/src/index.ts +7 -1
- package/src/paths.test.ts +47 -0
- package/src/paths.ts +47 -0
- package/src/report/build.ts +1 -1
- package/src/router/fs-routes.ts +344 -401
- package/src/router/fs-types.ts +270 -278
- package/src/router/index.ts +81 -81
- package/src/runtime/escape.ts +44 -0
- package/src/runtime/server.ts +281 -24
- package/src/runtime/ssr.ts +362 -367
- package/src/runtime/streaming-ssr.ts +1236 -1245
- package/src/watcher/rules.ts +5 -5
package/src/runtime/ssr.ts
CHANGED
|
@@ -1,367 +1,362 @@
|
|
|
1
|
-
import { renderToString } from "react-dom/server";
|
|
2
|
-
import { serializeProps } from "../client/serialize";
|
|
3
|
-
import type { ReactElement } from "react";
|
|
4
|
-
import type { BundleManifest } from "../bundler/types";
|
|
5
|
-
import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
6
|
-
import { PORTS, TIMEOUTS } from "../constants";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
type
|
|
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
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (manifest.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (imports["react-dom"]
|
|
102
|
-
scripts.push(`<link rel="modulepreload" href="${imports["react-dom"]}">`);
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
${
|
|
227
|
-
${
|
|
228
|
-
${
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
ws =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
): Promise<Response> {
|
|
365
|
-
const html = renderToHTML(element, options);
|
|
366
|
-
return createHTMLResponse(html);
|
|
367
|
-
}
|
|
1
|
+
import { renderToString } from "react-dom/server";
|
|
2
|
+
import { serializeProps } from "../client/serialize";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
import type { BundleManifest } from "../bundler/types";
|
|
5
|
+
import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
6
|
+
import { PORTS, TIMEOUTS } from "../constants";
|
|
7
|
+
import { escapeHtmlAttr, escapeJsonForInlineScript } from "./escape";
|
|
8
|
+
|
|
9
|
+
// Re-export streaming SSR utilities
|
|
10
|
+
export {
|
|
11
|
+
renderToStream,
|
|
12
|
+
renderStreamingResponse,
|
|
13
|
+
renderWithDeferredData,
|
|
14
|
+
SuspenseIsland,
|
|
15
|
+
DeferredData,
|
|
16
|
+
createStreamingLoader,
|
|
17
|
+
defer,
|
|
18
|
+
type StreamingSSROptions,
|
|
19
|
+
type StreamingLoaderResult,
|
|
20
|
+
type StreamingError,
|
|
21
|
+
type StreamingMetrics,
|
|
22
|
+
} from "./streaming-ssr";
|
|
23
|
+
|
|
24
|
+
export interface SSROptions {
|
|
25
|
+
title?: string;
|
|
26
|
+
lang?: string;
|
|
27
|
+
/** 서버에서 로드한 데이터 (클라이언트로 전달) */
|
|
28
|
+
serverData?: Record<string, unknown>;
|
|
29
|
+
/** Hydration 설정 */
|
|
30
|
+
hydration?: HydrationConfig;
|
|
31
|
+
/** 번들 매니페스트 */
|
|
32
|
+
bundleManifest?: BundleManifest;
|
|
33
|
+
/** 라우트 ID (island 식별용) */
|
|
34
|
+
routeId?: string;
|
|
35
|
+
/** 추가 head 태그 */
|
|
36
|
+
headTags?: string;
|
|
37
|
+
/** 추가 body 끝 태그 */
|
|
38
|
+
bodyEndTags?: string;
|
|
39
|
+
/** 개발 모드 여부 */
|
|
40
|
+
isDev?: boolean;
|
|
41
|
+
/** HMR 포트 (개발 모드에서 사용) */
|
|
42
|
+
hmrPort?: number;
|
|
43
|
+
/** Client-side Routing 활성화 여부 */
|
|
44
|
+
enableClientRouter?: boolean;
|
|
45
|
+
/** 라우트 패턴 (Client-side Routing용) */
|
|
46
|
+
routePattern?: string;
|
|
47
|
+
/** CSS 파일 경로 (자동 주입, 기본: /.mandu/client/globals.css) */
|
|
48
|
+
cssPath?: string | false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* SSR 데이터를 안전하게 직렬화 (Fresh 스타일 고급 직렬화)
|
|
53
|
+
* Date, Map, Set, URL, RegExp, BigInt, 순환참조 지원
|
|
54
|
+
*/
|
|
55
|
+
function serializeServerData(data: Record<string, unknown>): string {
|
|
56
|
+
// serializeProps로 고급 직렬화 (Date, Map, Set 등 지원)
|
|
57
|
+
const json = escapeJsonForInlineScript(serializeProps(data));
|
|
58
|
+
|
|
59
|
+
return `<script id="__MANDU_DATA__" type="application/json">${json}</script>
|
|
60
|
+
<script>window.__MANDU_DATA_RAW__ = document.getElementById('__MANDU_DATA__').textContent;</script>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Import map 생성 (bare specifier 해결용)
|
|
65
|
+
*/
|
|
66
|
+
function generateImportMap(manifest: BundleManifest): string {
|
|
67
|
+
if (!manifest.importMap || Object.keys(manifest.importMap.imports).length === 0) {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const importMapJson = escapeJsonForInlineScript(JSON.stringify(manifest.importMap, null, 2));
|
|
72
|
+
return `<script type="importmap">${importMapJson}</script>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Hydration 스크립트 태그 생성
|
|
77
|
+
* v0.9.0: vendor, runtime 모두 modulepreload로 성능 최적화
|
|
78
|
+
*/
|
|
79
|
+
function generateHydrationScripts(
|
|
80
|
+
routeId: string,
|
|
81
|
+
manifest: BundleManifest
|
|
82
|
+
): string {
|
|
83
|
+
const scripts: string[] = [];
|
|
84
|
+
|
|
85
|
+
// Import map 먼저 (반드시 module scripts 전에 위치해야 함)
|
|
86
|
+
const importMap = generateImportMap(manifest);
|
|
87
|
+
if (importMap) {
|
|
88
|
+
scripts.push(importMap);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Vendor modulepreload (React, ReactDOM 등 - 캐시 효율 극대화)
|
|
92
|
+
if (manifest.shared.vendor) {
|
|
93
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(manifest.shared.vendor)}">`);
|
|
94
|
+
}
|
|
95
|
+
if (manifest.importMap?.imports) {
|
|
96
|
+
const imports = manifest.importMap.imports;
|
|
97
|
+
// react-dom, react-dom/client 등 추가 preload
|
|
98
|
+
if (imports["react-dom"] && imports["react-dom"] !== manifest.shared.vendor) {
|
|
99
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(imports["react-dom"])}">`);
|
|
100
|
+
}
|
|
101
|
+
if (imports["react-dom/client"]) {
|
|
102
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(imports["react-dom/client"])}">`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Runtime modulepreload (hydration 실행 전 미리 로드)
|
|
107
|
+
if (manifest.shared.runtime) {
|
|
108
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(manifest.shared.runtime)}">`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Island 번들 modulepreload (성능 최적화 - prefetch only)
|
|
112
|
+
const bundle = manifest.bundles[routeId];
|
|
113
|
+
if (bundle) {
|
|
114
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(bundle.js)}">`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Runtime 로드 (hydrateIslands 실행 - dynamic import 사용)
|
|
118
|
+
if (manifest.shared.runtime) {
|
|
119
|
+
scripts.push(`<script type="module" src="${escapeHtmlAttr(manifest.shared.runtime)}"></script>`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return scripts.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Island 래퍼로 컨텐츠 감싸기
|
|
127
|
+
* v0.8.0: data-mandu-src 속성 추가 (Runtime이 dynamic import로 로드)
|
|
128
|
+
*/
|
|
129
|
+
export function wrapWithIsland(
|
|
130
|
+
content: string,
|
|
131
|
+
routeId: string,
|
|
132
|
+
priority: HydrationPriority = "visible",
|
|
133
|
+
bundleSrc?: string
|
|
134
|
+
): string {
|
|
135
|
+
const srcAttr = bundleSrc ? ` data-mandu-src="${escapeHtmlAttr(bundleSrc)}"` : "";
|
|
136
|
+
return `<div data-mandu-island="${escapeHtmlAttr(routeId)}"${srcAttr} data-mandu-priority="${escapeHtmlAttr(priority)}">${content}</div>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function renderToHTML(element: ReactElement, options: SSROptions = {}): string {
|
|
140
|
+
const {
|
|
141
|
+
title = "Mandu App",
|
|
142
|
+
lang = "ko",
|
|
143
|
+
serverData,
|
|
144
|
+
hydration,
|
|
145
|
+
bundleManifest,
|
|
146
|
+
routeId,
|
|
147
|
+
headTags = "",
|
|
148
|
+
bodyEndTags = "",
|
|
149
|
+
isDev = false,
|
|
150
|
+
hmrPort,
|
|
151
|
+
enableClientRouter = false,
|
|
152
|
+
routePattern,
|
|
153
|
+
cssPath,
|
|
154
|
+
} = options;
|
|
155
|
+
|
|
156
|
+
// CSS 링크 태그 생성
|
|
157
|
+
// - cssPath가 string이면 해당 경로 사용
|
|
158
|
+
// - cssPath가 false 또는 undefined이면 링크 미삽입 (404 방지)
|
|
159
|
+
const cssLinkTag = cssPath && cssPath !== false
|
|
160
|
+
? `<link rel="stylesheet" href="${escapeHtmlAttr(`${cssPath}${isDev ? `?t=${Date.now()}` : ""}`)}">`
|
|
161
|
+
: "";
|
|
162
|
+
|
|
163
|
+
let content = renderToString(element);
|
|
164
|
+
|
|
165
|
+
// Island 래퍼 적용 (hydration 필요 시)
|
|
166
|
+
const needsHydration =
|
|
167
|
+
hydration && hydration.strategy !== "none" && routeId && bundleManifest;
|
|
168
|
+
|
|
169
|
+
if (needsHydration) {
|
|
170
|
+
// v0.8.0: bundleSrc를 data-mandu-src 속성으로 전달 (Runtime이 dynamic import로 로드)
|
|
171
|
+
const bundle = bundleManifest.bundles[routeId];
|
|
172
|
+
const bundleSrc = bundle?.js;
|
|
173
|
+
content = wrapWithIsland(content, routeId, hydration.priority, bundleSrc);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 서버 데이터 스크립트
|
|
177
|
+
let dataScript = "";
|
|
178
|
+
if (serverData && routeId) {
|
|
179
|
+
const wrappedData = {
|
|
180
|
+
[routeId]: {
|
|
181
|
+
serverData,
|
|
182
|
+
timestamp: Date.now(),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
dataScript = serializeServerData(wrappedData);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Client-side Routing: 라우트 정보 주입
|
|
189
|
+
let routeScript = "";
|
|
190
|
+
if (enableClientRouter && routeId) {
|
|
191
|
+
routeScript = generateRouteScript(routeId, routePattern || "", serverData);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Hydration 스크립트
|
|
195
|
+
let hydrationScripts = "";
|
|
196
|
+
if (needsHydration && bundleManifest) {
|
|
197
|
+
hydrationScripts = generateHydrationScripts(routeId, bundleManifest);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Client-side Router 스크립트
|
|
201
|
+
let routerScript = "";
|
|
202
|
+
if (enableClientRouter && bundleManifest) {
|
|
203
|
+
routerScript = generateClientRouterScript(bundleManifest);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// HMR 스크립트 (개발 모드)
|
|
207
|
+
let hmrScript = "";
|
|
208
|
+
if (isDev && hmrPort) {
|
|
209
|
+
hmrScript = generateHMRScript(hmrPort);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return `<!doctype html>
|
|
213
|
+
<html lang="${escapeHtmlAttr(lang)}">
|
|
214
|
+
<head>
|
|
215
|
+
<meta charset="UTF-8">
|
|
216
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
217
|
+
<title>${escapeHtmlAttr(title)}</title>
|
|
218
|
+
${cssLinkTag}
|
|
219
|
+
${headTags}
|
|
220
|
+
</head>
|
|
221
|
+
<body>
|
|
222
|
+
<div id="root">${content}</div>
|
|
223
|
+
${dataScript}
|
|
224
|
+
${routeScript}
|
|
225
|
+
${hydrationScripts}
|
|
226
|
+
${routerScript}
|
|
227
|
+
${hmrScript}
|
|
228
|
+
${bodyEndTags}
|
|
229
|
+
</body>
|
|
230
|
+
</html>`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Client-side Routing: 현재 라우트 정보 스크립트 생성
|
|
235
|
+
*/
|
|
236
|
+
function generateRouteScript(
|
|
237
|
+
routeId: string,
|
|
238
|
+
pattern: string,
|
|
239
|
+
serverData?: Record<string, unknown>
|
|
240
|
+
): string {
|
|
241
|
+
const routeInfo = {
|
|
242
|
+
id: routeId,
|
|
243
|
+
pattern,
|
|
244
|
+
params: extractParamsFromUrl(pattern),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const json = escapeJsonForInlineScript(JSON.stringify(routeInfo));
|
|
248
|
+
|
|
249
|
+
return `<script>window.__MANDU_ROUTE__ = ${json};</script>`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* URL 패턴에서 파라미터 추출 (클라이언트에서 사용)
|
|
254
|
+
*/
|
|
255
|
+
function extractParamsFromUrl(pattern: string): Record<string, string> {
|
|
256
|
+
// 서버에서는 실제 params를 전달받으므로 빈 객체 반환
|
|
257
|
+
// 실제 params는 serverData나 별도 전달
|
|
258
|
+
return {};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Client-side Router 스크립트 로드
|
|
263
|
+
*/
|
|
264
|
+
function generateClientRouterScript(manifest: BundleManifest): string {
|
|
265
|
+
// Import map 먼저 (이미 hydration에서 추가되었을 수 있음)
|
|
266
|
+
const scripts: string[] = [];
|
|
267
|
+
|
|
268
|
+
// 라우터 번들이 있으면 로드
|
|
269
|
+
if (manifest.shared?.router) {
|
|
270
|
+
scripts.push(`<script type="module" src="${escapeHtmlAttr(manifest.shared.router)}"></script>`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return scripts.join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* HMR 스크립트 생성
|
|
278
|
+
*/
|
|
279
|
+
function generateHMRScript(port: number): string {
|
|
280
|
+
const hmrPort = port + PORTS.HMR_OFFSET;
|
|
281
|
+
return `<script>
|
|
282
|
+
(function() {
|
|
283
|
+
var ws = null;
|
|
284
|
+
var reconnectAttempts = 0;
|
|
285
|
+
var maxReconnectAttempts = ${TIMEOUTS.HMR_MAX_RECONNECT};
|
|
286
|
+
|
|
287
|
+
function connect() {
|
|
288
|
+
try {
|
|
289
|
+
ws = new WebSocket('ws://localhost:${hmrPort}');
|
|
290
|
+
ws.onopen = function() {
|
|
291
|
+
console.log('[Mandu HMR] Connected');
|
|
292
|
+
reconnectAttempts = 0;
|
|
293
|
+
};
|
|
294
|
+
ws.onmessage = function(e) {
|
|
295
|
+
try {
|
|
296
|
+
var msg = JSON.parse(e.data);
|
|
297
|
+
if (msg.type === 'reload' || msg.type === 'island-update') {
|
|
298
|
+
console.log('[Mandu HMR] Reloading...');
|
|
299
|
+
location.reload();
|
|
300
|
+
} else if (msg.type === 'error') {
|
|
301
|
+
console.error('[Mandu HMR] Build error:', msg.data?.message);
|
|
302
|
+
}
|
|
303
|
+
} catch(err) {}
|
|
304
|
+
};
|
|
305
|
+
ws.onclose = function() {
|
|
306
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
307
|
+
reconnectAttempts++;
|
|
308
|
+
setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY} * reconnectAttempts);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
} catch(err) {
|
|
312
|
+
setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
connect();
|
|
316
|
+
})();
|
|
317
|
+
</script>`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function createHTMLResponse(html: string, status: number = 200): Response {
|
|
321
|
+
return new Response(html, {
|
|
322
|
+
status,
|
|
323
|
+
headers: {
|
|
324
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function renderSSR(element: ReactElement, options: SSROptions = {}): Response {
|
|
330
|
+
const html = renderToHTML(element, options);
|
|
331
|
+
return createHTMLResponse(html);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Hydration이 포함된 SSR 렌더링
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* const response = await renderWithHydration(
|
|
340
|
+
* <TodoList todos={todos} />,
|
|
341
|
+
* {
|
|
342
|
+
* title: "할일 목록",
|
|
343
|
+
* routeId: "todos",
|
|
344
|
+
* serverData: { todos },
|
|
345
|
+
* hydration: { strategy: "island", priority: "visible" },
|
|
346
|
+
* bundleManifest,
|
|
347
|
+
* }
|
|
348
|
+
* );
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
export async function renderWithHydration(
|
|
352
|
+
element: ReactElement,
|
|
353
|
+
options: SSROptions & {
|
|
354
|
+
routeId: string;
|
|
355
|
+
serverData: Record<string, unknown>;
|
|
356
|
+
hydration: HydrationConfig;
|
|
357
|
+
bundleManifest: BundleManifest;
|
|
358
|
+
}
|
|
359
|
+
): Promise<Response> {
|
|
360
|
+
const html = renderToHTML(element, options);
|
|
361
|
+
return createHTMLResponse(html);
|
|
362
|
+
}
|