@mandujs/core 0.54.2 → 0.54.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.
@@ -46,7 +46,7 @@ export interface SSROptions {
46
46
  title?: string;
47
47
  lang?: string;
48
48
  /** 서버에서 로드한 데이터 (클라이언트로 전달) */
49
- serverData?: Record<string, unknown>;
49
+ serverData?: unknown;
50
50
  /** Hydration 설정 */
51
51
  hydration?: HydrationConfig;
52
52
  /** 번들 매니페스트 */
@@ -255,19 +255,26 @@ function generateHydrationScripts(
255
255
  ? Object.values(manifest.islands).filter((ib) => ib.route === routeId)
256
256
  : [];
257
257
 
258
- if (routeIslands.length > 0) {
259
- for (const ib of routeIslands) {
260
- const cacheBust = `${ib.js}${ib.js.includes('?') ? '&' : '?'}v=${Date.now()}`;
261
- scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(cacheBust)}">`);
262
- }
258
+ if (routeIslands.length > 0) {
259
+ for (const ib of routeIslands) {
260
+ const cacheBust = `${ib.js}${ib.js.includes('?') ? '&' : '?'}v=${Date.now()}`;
261
+ scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(cacheBust)}">`);
262
+ }
263
263
  } else {
264
264
  // Fallback: route-level bundle (backward compat)
265
265
  const bundle = manifest.bundles[routeId];
266
266
  if (bundle) {
267
267
  const cacheBust = `${bundle.js}${bundle.js.includes('?') ? '&' : '?'}v=${Date.now()}`;
268
- scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(cacheBust)}">`);
269
- }
270
- }
268
+ scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(cacheBust)}">`);
269
+ }
270
+ }
271
+
272
+ if (manifest.partials) {
273
+ for (const partial of Object.values(manifest.partials)) {
274
+ const cacheBust = `${partial.js}${partial.js.includes('?') ? '&' : '?'}v=${Date.now()}`;
275
+ scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(cacheBust)}">`);
276
+ }
277
+ }
271
278
 
272
279
  // Runtime 로드 (hydrateIslands 실행 - dynamic import 사용)
273
280
  if (manifest.shared.runtime) {
@@ -706,12 +713,14 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
706
713
  const needsHydration =
707
714
  hydration && hydration.strategy !== "none" && routeId && bundleManifest;
708
715
 
709
- if (needsHydration && !islandPreWrapped) {
710
- // v0.8.0: bundleSrc를 data-mandu-src 속성으로 전달 (Runtime이 dynamic import로 로드)
711
- const bundle = bundleManifest.bundles[routeId];
712
- const bundleSrc = bundle?.js;
713
- content = wrapWithIsland(content, routeId, hydration.priority, bundleSrc);
714
- }
716
+ if (needsHydration && !islandPreWrapped) {
717
+ // v0.8.0: bundleSrc를 data-mandu-src 속성으로 전달 (Runtime이 dynamic import로 로드)
718
+ const bundle = bundleManifest.bundles[routeId];
719
+ const bundleSrc = bundle?.js;
720
+ if (bundleSrc) {
721
+ content = wrapWithIsland(content, routeId, hydration.priority, bundleSrc);
722
+ }
723
+ }
715
724
 
716
725
  // Zero-JS 모드: island이 없는 페이지에서는 클라이언트 JS 번들을 전송하지 않음
717
726
  // HMR/DevTools는 dev 환경에서만 유지 (CSS 핫리로드 등)
@@ -722,10 +731,10 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
722
731
 
723
732
  if (needsHydration) {
724
733
  // 서버 데이터 스크립트 (클라이언트 hydration에서 사용)
725
- if (serverData && routeId) {
726
- const wrappedData = {
727
- [routeId]: {
728
- serverData,
734
+ if (serverData !== undefined && routeId) {
735
+ const wrappedData = {
736
+ [routeId]: {
737
+ serverData,
729
738
  timestamp: Date.now(),
730
739
  },
731
740
  };
@@ -862,11 +871,11 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
862
871
  /**
863
872
  * Client-side Routing: 현재 라우트 정보 스크립트 생성
864
873
  */
865
- function generateRouteScript(
866
- routeId: string,
867
- pattern: string,
868
- _serverData?: Record<string, unknown>
869
- ): string {
874
+ function generateRouteScript(
875
+ routeId: string,
876
+ pattern: string,
877
+ _serverData?: unknown
878
+ ): string {
870
879
  const routeInfo = {
871
880
  id: routeId,
872
881
  pattern,
@@ -1167,12 +1176,12 @@ export function renderSSR(element: ReactElement, options: SSROptions = {}): Resp
1167
1176
  */
1168
1177
  export async function renderWithHydration(
1169
1178
  element: ReactElement,
1170
- options: SSROptions & {
1171
- routeId: string;
1172
- serverData: Record<string, unknown>;
1173
- hydration: HydrationConfig;
1174
- bundleManifest: BundleManifest;
1175
- }
1179
+ options: SSROptions & {
1180
+ routeId: string;
1181
+ serverData: unknown;
1182
+ hydration: HydrationConfig;
1183
+ bundleManifest: BundleManifest;
1184
+ }
1176
1185
  ): Promise<Response> {
1177
1186
  const html = renderToHTML(element, options);
1178
1187
  // Phase 7.2 R1 Agent C (H1) — same CSP header logic as renderSSR.
@@ -613,13 +613,16 @@ function generateHTMLShell(options: StreamingSSROptions): string {
613
613
  }
614
614
  </style>`;
615
615
 
616
- let islandOpenTag = "";
617
- if (needsHydration) {
618
- const bundle = bundleManifest.bundles[routeId];
619
- const bundleSrc = bundle?.js ? `${bundle.js}?t=${Date.now()}` : "";
620
- const priority = hydration.priority || "visible";
621
- islandOpenTag = `<div data-mandu-island="${escapeHtmlAttr(routeId)}" data-mandu-src="${escapeHtmlAttr(bundleSrc)}" data-mandu-priority="${escapeHtmlAttr(priority)}" style="display:contents">`;
622
- }
616
+ let islandOpenTag = "";
617
+ const hasRouteBundle = !!(needsHydration && bundleManifest.bundles[routeId]?.js);
618
+ if (needsHydration) {
619
+ const bundle = bundleManifest.bundles[routeId];
620
+ const bundleSrc = bundle?.js ? `${bundle.js}?t=${Date.now()}` : "";
621
+ const priority = hydration.priority || "visible";
622
+ if (hasRouteBundle) {
623
+ islandOpenTag = `<div data-mandu-island="${escapeHtmlAttr(routeId)}" data-mandu-src="${escapeHtmlAttr(bundleSrc)}" data-mandu-priority="${escapeHtmlAttr(priority)}" style="display:contents">`;
624
+ }
625
+ }
623
626
 
624
627
  // Phase 7.1 R2 Agent D: Fast Refresh preamble. Must land in <head>
625
628
  // BEFORE any island script evaluates — the stubs it installs for
@@ -697,7 +700,7 @@ function generateHTMLTailContent(options: StreamingSSROptions): string {
697
700
  // 1~8: hydration이 필요한 경우에만 클라이언트 JS 관련 스크립트 삽입
698
701
  if (needsHydration) {
699
702
  // 1. Critical 데이터 스크립트 (즉시 사용 가능)
700
- if (criticalData && routeId) {
703
+ if (criticalData !== undefined && routeId) {
701
704
  const wrappedData = {
702
705
  [routeId]: {
703
706
  serverData: criticalData,
@@ -746,10 +749,16 @@ function generateHTMLTailContent(options: StreamingSSROptions): string {
746
749
 
747
750
  // 6. Island modulepreload
748
751
  const bundle = bundleManifest.bundles[routeId];
749
- if (bundle) {
750
- const cacheBust = `${bundle.js}${bundle.js.includes('?') ? '&' : '?'}v=${Date.now()}`;
751
- scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(cacheBust)}">`);
752
- }
752
+ if (bundle) {
753
+ const cacheBust = `${bundle.js}${bundle.js.includes('?') ? '&' : '?'}v=${Date.now()}`;
754
+ scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(cacheBust)}">`);
755
+ }
756
+ if (bundleManifest.partials) {
757
+ for (const partial of Object.values(bundleManifest.partials)) {
758
+ const cacheBust = `${partial.js}${partial.js.includes('?') ? '&' : '?'}v=${Date.now()}`;
759
+ scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(cacheBust)}">`);
760
+ }
761
+ }
753
762
 
754
763
  // 7. Runtime 로드
755
764
  if (bundleManifest.shared.runtime) {
@@ -783,7 +792,7 @@ function generateHTMLTailContent(options: StreamingSSROptions): string {
783
792
  }
784
793
 
785
794
  // Island wrapper 닫기 (hydration이 필요한 경우)
786
- const islandCloseTag = needsHydration ? "</div>" : "";
795
+ const islandCloseTag = needsHydration && bundleManifest.bundles[routeId]?.js ? "</div>" : "";
787
796
 
788
797
  return `${islandCloseTag}</div>
789
798
  ${scripts.join("\n ")}`;