@mandujs/core 0.12.1 → 0.13.0

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