@mandujs/core 0.7.7 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.7.7",
3
+ "version": "0.8.0",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -47,44 +47,34 @@ function getHydratedRoutes(manifest: RoutesManifest): RouteSpec[] {
47
47
  }
48
48
 
49
49
  /**
50
- * Runtime 번들 소스 생성
50
+ * Runtime 번들 소스 생성 (v0.8.0 재설계)
51
+ *
52
+ * 설계 원칙:
53
+ * - 글로벌 레지스트리 없음 (Island가 스스로 등록 안함)
54
+ * - Runtime이 Island를 dynamic import()로 로드
55
+ * - HTML의 data-mandu-src 속성에서 번들 URL 읽기
56
+ * - 실행 순서 문제 완전 해결
51
57
  */
52
58
  function generateRuntimeSource(): string {
53
59
  return `
54
60
  /**
55
- * Mandu Hydration Runtime (Generated)
61
+ * Mandu Hydration Runtime v0.8.0 (Generated)
62
+ * Fresh-style dynamic import architecture
56
63
  */
57
64
 
58
- // 글로벌 레지스트리 사용 (Island 번들과 공유)
59
- // 주의: 변수로 캐싱하면 번들러가 인라인 시 new Map()으로 대체함
60
- // 항상 window.__MANDU_ISLANDS__를 직접 참조해야 함
61
- window.__MANDU_ISLANDS__ = window.__MANDU_ISLANDS__ || new Map();
62
- window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map();
65
+ // Hydrated roots 추적 (unmount용)
66
+ const hydratedRoots = new Map();
63
67
 
64
68
  // 서버 데이터
65
- const serverData = window.__MANDU_DATA__ || {};
66
-
67
- /**
68
- * Island 등록
69
- */
70
- export function registerIsland(id, loader) {
71
- window.__MANDU_ISLANDS__.set(id, loader);
72
- }
73
-
74
- /**
75
- * 서버 데이터 가져오기
76
- */
77
- export function getServerData(id) {
78
- return serverData[id]?.serverData;
79
- }
69
+ const getServerData = (id) => (window.__MANDU_DATA__ || {})[id]?.serverData || {};
80
70
 
81
71
  /**
82
72
  * Hydration 스케줄러
83
73
  */
84
- function scheduleHydration(element, id, priority, data) {
74
+ function scheduleHydration(element, src, priority) {
85
75
  switch (priority) {
86
76
  case 'immediate':
87
- hydrateIsland(element, id, data);
77
+ loadAndHydrate(element, src);
88
78
  break;
89
79
 
90
80
  case 'visible':
@@ -92,20 +82,20 @@ function scheduleHydration(element, id, priority, data) {
92
82
  const observer = new IntersectionObserver((entries) => {
93
83
  if (entries[0].isIntersecting) {
94
84
  observer.disconnect();
95
- hydrateIsland(element, id, data);
85
+ loadAndHydrate(element, src);
96
86
  }
97
87
  }, { rootMargin: '50px' });
98
88
  observer.observe(element);
99
89
  } else {
100
- hydrateIsland(element, id, data);
90
+ loadAndHydrate(element, src);
101
91
  }
102
92
  break;
103
93
 
104
94
  case 'idle':
105
95
  if ('requestIdleCallback' in window) {
106
- requestIdleCallback(() => hydrateIsland(element, id, data));
96
+ requestIdleCallback(() => loadAndHydrate(element, src));
107
97
  } else {
108
- setTimeout(() => hydrateIsland(element, id, data), 200);
98
+ setTimeout(() => loadAndHydrate(element, src), 200);
109
99
  }
110
100
  break;
111
101
 
@@ -114,7 +104,7 @@ function scheduleHydration(element, id, priority, data) {
114
104
  element.removeEventListener('mouseenter', hydrate);
115
105
  element.removeEventListener('focusin', hydrate);
116
106
  element.removeEventListener('touchstart', hydrate);
117
- hydrateIsland(element, id, data);
107
+ loadAndHydrate(element, src);
118
108
  };
119
109
  element.addEventListener('mouseenter', hydrate, { once: true, passive: true });
120
110
  element.addEventListener('focusin', hydrate, { once: true });
@@ -125,26 +115,26 @@ function scheduleHydration(element, id, priority, data) {
125
115
  }
126
116
 
127
117
  /**
128
- * 단일 Island hydrate (또는 mount)
129
- * SSR 플레이스홀더를 Island 컴포넌트로 교체
118
+ * Island 로드 및 hydrate (핵심 함수)
119
+ * Dynamic import로 Island 모듈 로드 후 렌더링
130
120
  */
131
- async function hydrateIsland(element, id, data) {
132
- const loader = window.__MANDU_ISLANDS__.get(id);
133
- if (!loader) {
134
- console.warn('[Mandu] Island not found:', id);
135
- return;
136
- }
121
+ async function loadAndHydrate(element, src) {
122
+ const id = element.getAttribute('data-mandu-island');
137
123
 
138
124
  try {
139
- const island = await Promise.resolve(loader());
125
+ // Dynamic import - 이 시점에 Island 모듈 로드
126
+ const module = await import(src);
127
+ const island = module.default;
140
128
 
141
- // Island 컴포넌트 가져오기
142
- const islandDef = island.default || island;
143
- if (!islandDef.__mandu_island) {
144
- throw new Error('[Mandu] Invalid island: ' + id);
129
+ // Island 유효성 검사
130
+ if (!island || !island.__mandu_island) {
131
+ throw new Error('[Mandu] Invalid island module: ' + id);
145
132
  }
146
133
 
147
- const { definition } = islandDef;
134
+ const { definition } = island;
135
+ const data = getServerData(id);
136
+
137
+ // React 동적 로드
148
138
  const { createRoot } = await import('react-dom/client');
149
139
  const React = await import('react');
150
140
 
@@ -154,11 +144,10 @@ async function hydrateIsland(element, id, data) {
154
144
  return definition.render(setupResult);
155
145
  }
156
146
 
157
- // Mount (createRoot 사용 - SSR 플레이스홀더 교체)
158
- // hydrateRoot 대신 createRoot 사용: Island는 SSR과 다른 컨텐츠를 렌더링할 수 있음
147
+ // Mount
159
148
  const root = createRoot(element);
160
149
  root.render(React.createElement(IslandComponent));
161
- window.__MANDU_ROOTS__.set(id, root);
150
+ hydratedRoots.set(id, root);
162
151
 
163
152
  // 완료 표시
164
153
  element.setAttribute('data-mandu-hydrated', 'true');
@@ -173,6 +162,8 @@ async function hydrateIsland(element, id, data) {
173
162
  bubbles: true,
174
163
  detail: { id, data }
175
164
  }));
165
+
166
+ console.log('[Mandu] Hydrated:', id);
176
167
  } catch (error) {
177
168
  console.error('[Mandu] Hydration failed for', id, error);
178
169
  element.setAttribute('data-mandu-error', 'true');
@@ -180,37 +171,47 @@ async function hydrateIsland(element, id, data) {
180
171
  }
181
172
 
182
173
  /**
183
- * 모든 Island hydrate
174
+ * 모든 Island hydrate 시작
184
175
  */
185
- export async function hydrateIslands() {
176
+ function hydrateIslands() {
186
177
  const islands = document.querySelectorAll('[data-mandu-island]');
187
178
 
188
179
  for (const el of islands) {
189
180
  const id = el.getAttribute('data-mandu-island');
190
- if (!id) continue;
191
-
181
+ const src = el.getAttribute('data-mandu-src');
192
182
  const priority = el.getAttribute('data-mandu-priority') || 'visible';
193
- const data = serverData[id]?.serverData || {};
194
183
 
195
- scheduleHydration(el, id, priority, data);
184
+ if (!id || !src) {
185
+ console.warn('[Mandu] Island missing id or src:', el);
186
+ continue;
187
+ }
188
+
189
+ scheduleHydration(el, src, priority);
196
190
  }
197
191
  }
198
192
 
199
193
  /**
200
- * 자동 초기화
201
- * queueMicrotask로 지연: Island 등록 코드가 먼저 실행되도록 함
194
+ * Island unmount
202
195
  */
203
- queueMicrotask(() => {
204
- if (document.readyState === 'loading') {
205
- document.addEventListener('DOMContentLoaded', hydrateIslands);
206
- } else {
207
- hydrateIslands();
196
+ function unmountIsland(id) {
197
+ const root = hydratedRoots.get(id);
198
+ if (root) {
199
+ root.unmount();
200
+ hydratedRoots.delete(id);
201
+ return true;
208
202
  }
209
- });
203
+ return false;
204
+ }
205
+
206
+ // 자동 초기화
207
+ if (document.readyState === 'loading') {
208
+ document.addEventListener('DOMContentLoaded', hydrateIslands);
209
+ } else {
210
+ hydrateIslands();
211
+ }
210
212
 
211
- // 글로벌 레지스트리 접근용 getter
212
- export const getIslandRegistry = () => window.__MANDU_ISLANDS__;
213
- export const getHydratedRoots = () => window.__MANDU_ROOTS__;
213
+ // Export for external use
214
+ export { hydrateIslands, unmountIsland, hydratedRoots };
214
215
  `;
215
216
  }
216
217
 
@@ -612,23 +613,22 @@ async function buildRouterRuntime(
612
613
  }
613
614
 
614
615
  /**
615
- * Island 엔트리 래퍼 생성
616
- * 주의: 글로벌 레지스트리 직접 사용 (번들러 인라인 문제 방지)
616
+ * Island 엔트리 래퍼 생성 (v0.8.0 재설계)
617
+ *
618
+ * 설계 원칙:
619
+ * - 순수 export만 (부작용 없음)
620
+ * - Runtime이 dynamic import로 로드
621
+ * - 등록/초기화 코드 없음
617
622
  */
618
623
  function generateIslandEntry(routeId: string, clientModulePath: string): string {
619
624
  // Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
620
625
  const normalizedPath = clientModulePath.replace(/\\/g, "/");
621
626
  return `
622
627
  /**
623
- * Mandu Island Entry: ${routeId} (Generated)
628
+ * Mandu Island: ${routeId} (Generated)
629
+ * Pure export - no side effects
624
630
  */
625
-
626
631
  import island from "${normalizedPath}";
627
-
628
- // 글로벌 레지스트리에 직접 등록 (런타임과 공유)
629
- window.__MANDU_ISLANDS__ = window.__MANDU_ISLANDS__ || new Map();
630
- window.__MANDU_ISLANDS__.set("${routeId}", () => island);
631
-
632
632
  export default island;
633
633
  `;
634
634
  }
@@ -42,15 +42,12 @@ export {
42
42
 
43
43
  // Runtime API
44
44
  export {
45
- registerIsland,
46
- getRegisteredIslands,
47
45
  getServerData,
48
- hydrateIslands,
49
46
  getHydrationState,
50
47
  unmountIsland,
51
48
  unmountAllIslands,
52
- initializeRuntime,
53
- type IslandLoader,
49
+ type HydrationState,
50
+ type HydrationPriority,
54
51
  } from "./runtime";
55
52
 
56
53
  // Client-side Router API
@@ -100,12 +97,12 @@ export {
100
97
 
101
98
  // Re-export as Mandu namespace for consistent API
102
99
  import { island, wrapComponent } from "./island";
103
- import { hydrateIslands, initializeRuntime } from "./runtime";
104
100
  import { navigate, prefetch, initializeRouter } from "./router";
105
101
  import { Link, NavLink } from "./Link";
106
102
 
107
103
  /**
108
104
  * Mandu Client namespace
105
+ * v0.8.0: Hydration은 자동으로 처리됨 (generateRuntimeSource에서 생성)
109
106
  */
110
107
  export const Mandu = {
111
108
  /**
@@ -120,18 +117,6 @@ export const Mandu = {
120
117
  */
121
118
  wrapComponent,
122
119
 
123
- /**
124
- * Hydrate all islands on the page
125
- * @see hydrateIslands
126
- */
127
- hydrate: hydrateIslands,
128
-
129
- /**
130
- * Initialize the hydration runtime
131
- * @see initializeRuntime
132
- */
133
- init: initializeRuntime,
134
-
135
120
  /**
136
121
  * Navigate to a URL (client-side)
137
122
  * @see navigate
@@ -1,67 +1,41 @@
1
1
  /**
2
2
  * Mandu Hydration Runtime 🌊
3
- * 브라우저에서 Island를 hydrate하는 런타임
3
+ * v0.8.0: Dynamic Import 기반 아키텍처
4
+ *
5
+ * 이 파일은 타입 정의와 유틸리티 함수를 제공합니다.
6
+ * 실제 Hydration Runtime은 bundler/build.ts의 generateRuntimeSource()에서 생성됩니다.
4
7
  */
5
8
 
6
- import { hydrateRoot } from "react-dom/client";
7
9
  import type { Root } from "react-dom/client";
8
- import type { CompiledIsland } from "./island";
9
- import type { ReactNode } from "react";
10
- import React from "react";
11
10
 
12
11
  /**
13
- * Island 로더 타입
14
- */
15
- export type IslandLoader = () => Promise<CompiledIsland<any, any>> | CompiledIsland<any, any>;
16
-
17
- /**
18
- * Island 레지스트리 (글로벌)
19
- * 주의: 로컬 Map 사용 시 번들러 인라인 문제로 별도 인스턴스 생성됨
20
- * 항상 window.__MANDU_ISLANDS__를 직접 참조해야 함
12
+ * Window 전역 타입 확장
21
13
  */
22
14
  declare global {
23
15
  interface Window {
24
- __MANDU_ISLANDS__: Map<string, IslandLoader>;
16
+ /** Hydrated React roots (unmount용) */
25
17
  __MANDU_ROOTS__: Map<string, Root>;
18
+ /** 서버 데이터 */
19
+ __MANDU_DATA__?: Record<string, { serverData: unknown; timestamp: number }>;
20
+ /** 직렬화된 서버 데이터 (raw JSON) */
21
+ __MANDU_DATA_RAW__?: string;
26
22
  }
27
23
  }
28
24
 
29
- // 글로벌 레지스트리 초기화
30
- if (typeof window !== "undefined") {
31
- window.__MANDU_ISLANDS__ = window.__MANDU_ISLANDS__ || new Map<string, IslandLoader>();
32
- window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map<string, Root>();
33
- }
34
-
35
25
  /**
36
26
  * Hydration 상태 추적
37
27
  */
38
- interface HydrationState {
28
+ export interface HydrationState {
39
29
  total: number;
40
30
  hydrated: number;
41
31
  failed: number;
42
32
  pending: Set<string>;
43
33
  }
44
34
 
45
- const hydrationState: HydrationState = {
46
- total: 0,
47
- hydrated: 0,
48
- failed: 0,
49
- pending: new Set(),
50
- };
51
-
52
- /**
53
- * Island 등록
54
- */
55
- export function registerIsland(id: string, loader: IslandLoader): void {
56
- window.__MANDU_ISLANDS__.set(id, loader);
57
- }
58
-
59
35
  /**
60
- * 등록된 모든 Island 가져오기
36
+ * Hydration 우선순위
61
37
  */
62
- export function getRegisteredIslands(): string[] {
63
- return Array.from(window.__MANDU_ISLANDS__.keys());
64
- }
38
+ export type HydrationPriority = "immediate" | "visible" | "idle" | "interaction";
65
39
 
66
40
  /**
67
41
  * 서버 데이터 가져오기
@@ -71,7 +45,7 @@ export function getServerData<T = unknown>(islandId: string): T | undefined {
71
45
  return undefined;
72
46
  }
73
47
 
74
- const manduData = (window as any).__MANDU_DATA__;
48
+ const manduData = window.__MANDU_DATA__;
75
49
  if (!manduData) {
76
50
  return undefined;
77
51
  }
@@ -80,186 +54,41 @@ export function getServerData<T = unknown>(islandId: string): T | undefined {
80
54
  }
81
55
 
82
56
  /**
83
- * Priority-based hydration 스케줄러
84
- */
85
- type HydrationPriority = "immediate" | "visible" | "idle" | "interaction";
86
-
87
- function scheduleHydration(
88
- element: HTMLElement,
89
- id: string,
90
- priority: HydrationPriority,
91
- serverData: unknown
92
- ): void {
93
- switch (priority) {
94
- case "immediate":
95
- hydrateIsland(element, id, serverData);
96
- break;
97
-
98
- case "visible":
99
- if ("IntersectionObserver" in window) {
100
- const observer = new IntersectionObserver(
101
- (entries) => {
102
- if (entries[0].isIntersecting) {
103
- observer.disconnect();
104
- hydrateIsland(element, id, serverData);
105
- }
106
- },
107
- { rootMargin: "50px" }
108
- );
109
- observer.observe(element);
110
- } else {
111
- // Fallback for older browsers
112
- hydrateIsland(element, id, serverData);
113
- }
114
- break;
115
-
116
- case "idle":
117
- if ("requestIdleCallback" in window) {
118
- (window as any).requestIdleCallback(() => {
119
- hydrateIsland(element, id, serverData);
120
- });
121
- } else {
122
- // Fallback
123
- setTimeout(() => hydrateIsland(element, id, serverData), 200);
124
- }
125
- break;
126
-
127
- case "interaction": {
128
- const hydrate = () => {
129
- element.removeEventListener("mouseenter", hydrate);
130
- element.removeEventListener("focusin", hydrate);
131
- element.removeEventListener("touchstart", hydrate);
132
- hydrateIsland(element, id, serverData);
133
- };
134
- element.addEventListener("mouseenter", hydrate, { once: true, passive: true });
135
- element.addEventListener("focusin", hydrate, { once: true });
136
- element.addEventListener("touchstart", hydrate, { once: true, passive: true });
137
- break;
138
- }
139
- }
140
- }
141
-
142
- /**
143
- * 단일 Island hydrate
144
- */
145
- async function hydrateIsland(
146
- element: HTMLElement,
147
- id: string,
148
- serverData: unknown
149
- ): Promise<void> {
150
- const loader = window.__MANDU_ISLANDS__.get(id);
151
- if (!loader) {
152
- console.warn(`[Mandu] Island not registered: ${id}`);
153
- hydrationState.failed++;
154
- hydrationState.pending.delete(id);
155
- return;
156
- }
157
-
158
- try {
159
- // 로더 실행 (dynamic import 또는 직접 참조)
160
- const island = await Promise.resolve(loader());
161
-
162
- if (!island.__mandu_island) {
163
- throw new Error(`[Mandu] Invalid island: ${id}`);
164
- }
165
-
166
- const { definition } = island;
167
-
168
- // Island 컴포넌트 생성
169
- function IslandComponent(): ReactNode {
170
- const setupResult = definition.setup(serverData);
171
- return definition.render(setupResult);
172
- }
173
-
174
- // ErrorBoundary가 있으면 감싸기
175
- let content: ReactNode;
176
- if (definition.errorBoundary) {
177
- content = React.createElement(
178
- ManduErrorBoundary,
179
- {
180
- fallback: (error: Error, reset: () => void) => definition.errorBoundary!(error, reset),
181
- },
182
- React.createElement(IslandComponent)
183
- );
184
- } else {
185
- content = React.createElement(IslandComponent);
186
- }
187
-
188
- // Hydrate
189
- const root = hydrateRoot(element, content);
190
- window.__MANDU_ROOTS__.set(id, root);
191
-
192
- // 상태 업데이트
193
- element.setAttribute("data-mandu-hydrated", "true");
194
- hydrationState.hydrated++;
195
- hydrationState.pending.delete(id);
196
-
197
- // 성능 마커
198
- if (typeof performance !== "undefined" && performance.mark) {
199
- performance.mark(`mandu-hydrated-${id}`);
200
- }
201
-
202
- // Hydration 완료 이벤트
203
- element.dispatchEvent(
204
- new CustomEvent("mandu:hydrated", {
205
- bubbles: true,
206
- detail: { id, serverData },
207
- })
208
- );
209
- } catch (error) {
210
- console.error(`[Mandu] Hydration failed for island ${id}:`, error);
211
- hydrationState.failed++;
212
- hydrationState.pending.delete(id);
213
-
214
- // 에러 상태 표시
215
- element.setAttribute("data-mandu-error", "true");
216
-
217
- // 에러 이벤트
218
- element.dispatchEvent(
219
- new CustomEvent("mandu:hydration-error", {
220
- bubbles: true,
221
- detail: { id, error },
222
- })
223
- );
224
- }
225
- }
226
-
227
- /**
228
- * 모든 Island hydrate 시작
57
+ * Hydration 상태 조회 (DOM 기반)
229
58
  */
230
- export async function hydrateIslands(): Promise<void> {
59
+ export function getHydrationState(): Readonly<HydrationState> {
231
60
  if (typeof document === "undefined") {
232
- return;
61
+ return { total: 0, hydrated: 0, failed: 0, pending: new Set() };
233
62
  }
234
63
 
235
64
  const islands = document.querySelectorAll<HTMLElement>("[data-mandu-island]");
236
- const manduData = (window as any).__MANDU_DATA__ || {};
237
-
238
- hydrationState.total = islands.length;
239
-
240
- for (const element of islands) {
241
- const id = element.getAttribute("data-mandu-island");
242
- if (!id) continue;
243
-
244
- const priority = (element.getAttribute("data-mandu-priority") || "visible") as HydrationPriority;
245
- const data = manduData[id]?.serverData || {};
246
-
247
- hydrationState.pending.add(id);
248
- scheduleHydration(element, id, priority, data);
249
- }
250
- }
65
+ const hydrated = document.querySelectorAll<HTMLElement>("[data-mandu-hydrated]");
66
+ const failed = document.querySelectorAll<HTMLElement>("[data-mandu-error]");
67
+
68
+ const pending = new Set<string>();
69
+ islands.forEach((el) => {
70
+ const id = el.getAttribute("data-mandu-island");
71
+ if (id && !el.hasAttribute("data-mandu-hydrated") && !el.hasAttribute("data-mandu-error")) {
72
+ pending.add(id);
73
+ }
74
+ });
251
75
 
252
- /**
253
- * Hydration 상태 조회
254
- */
255
- export function getHydrationState(): Readonly<HydrationState> {
256
- return { ...hydrationState };
76
+ return {
77
+ total: islands.length,
78
+ hydrated: hydrated.length,
79
+ failed: failed.length,
80
+ pending,
81
+ };
257
82
  }
258
83
 
259
84
  /**
260
85
  * 특정 Island unmount
261
86
  */
262
87
  export function unmountIsland(id: string): boolean {
88
+ if (typeof window === "undefined" || !window.__MANDU_ROOTS__) {
89
+ return false;
90
+ }
91
+
263
92
  const root = window.__MANDU_ROOTS__.get(id);
264
93
  if (!root) {
265
94
  return false;
@@ -274,71 +103,12 @@ export function unmountIsland(id: string): boolean {
274
103
  * 모든 Island unmount
275
104
  */
276
105
  export function unmountAllIslands(): void {
277
- for (const [id, root] of window.__MANDU_ROOTS__) {
278
- root.unmount();
279
- window.__MANDU_ROOTS__.delete(id);
280
- }
281
- }
282
-
283
- /**
284
- * 간단한 ErrorBoundary 컴포넌트
285
- */
286
- interface ErrorBoundaryProps {
287
- children: ReactNode;
288
- fallback: (error: Error, reset: () => void) => ReactNode;
289
- }
290
-
291
- interface ErrorBoundaryState {
292
- error: Error | null;
293
- }
294
-
295
- class ManduErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
296
- constructor(props: ErrorBoundaryProps) {
297
- super(props);
298
- this.state = { error: null };
299
- }
300
-
301
- static getDerivedStateFromError(error: Error): ErrorBoundaryState {
302
- return { error };
303
- }
304
-
305
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
306
- console.error("[Mandu] Island error:", error, errorInfo);
307
- }
308
-
309
- reset = (): void => {
310
- this.setState({ error: null });
311
- };
312
-
313
- render(): ReactNode {
314
- if (this.state.error) {
315
- return this.props.fallback(this.state.error, this.reset);
316
- }
317
- return this.props.children;
318
- }
319
- }
320
-
321
- /**
322
- * 자동 초기화 (스크립트 로드 시)
323
- */
324
- export function initializeRuntime(): void {
325
- if (typeof document === "undefined") {
106
+ if (typeof window === "undefined" || !window.__MANDU_ROOTS__) {
326
107
  return;
327
108
  }
328
109
 
329
- // DOM이 준비되면 hydration 시작
330
- if (document.readyState === "loading") {
331
- document.addEventListener("DOMContentLoaded", () => {
332
- hydrateIslands();
333
- });
334
- } else {
335
- // 이미 DOM이 준비된 경우
336
- hydrateIslands();
110
+ for (const [id, root] of window.__MANDU_ROOTS__) {
111
+ root.unmount();
112
+ window.__MANDU_ROOTS__.delete(id);
337
113
  }
338
114
  }
339
-
340
- // 자동 초기화 여부 (번들 시 설정)
341
- // queueMicrotask로 지연: 번들 내 Island 등록 코드가 먼저 실행되도록 함
342
- if (typeof window !== "undefined" && (window as any).__MANDU_AUTO_INIT__ !== false) {
343
- queueMicrotask(() => initializeRuntime());
344
- }
@@ -59,6 +59,7 @@ function generateImportMap(manifest: BundleManifest): string {
59
59
 
60
60
  /**
61
61
  * Hydration 스크립트 태그 생성
62
+ * v0.8.0: Island 직접 로드 제거 - Runtime이 data-mandu-src에서 dynamic import
62
63
  */
63
64
  function generateHydrationScripts(
64
65
  routeId: string,
@@ -72,15 +73,14 @@ function generateHydrationScripts(
72
73
  scripts.push(importMap);
73
74
  }
74
75
 
75
- // Island 번들 먼저 로드 (등록)
76
- // 주의: ES Module 병렬 로드로 인해 Island가 먼저 등록되어야 함
76
+ // Island 번들 modulepreload (성능 최적화 - prefetch only)
77
+ // v0.8.0: <script> 태그 제거 - Runtime이 dynamic import로 로드
77
78
  const bundle = manifest.bundles[routeId];
78
79
  if (bundle) {
79
80
  scripts.push(`<link rel="modulepreload" href="${bundle.js}">`);
80
- scripts.push(`<script type="module" src="${bundle.js}"></script>`);
81
81
  }
82
82
 
83
- // Runtime 나중에 로드 (hydrateIslands 실행)
83
+ // Runtime 로드 (hydrateIslands 실행 - dynamic import 사용)
84
84
  if (manifest.shared.runtime) {
85
85
  scripts.push(`<script type="module" src="${manifest.shared.runtime}"></script>`);
86
86
  }
@@ -90,13 +90,16 @@ function generateHydrationScripts(
90
90
 
91
91
  /**
92
92
  * Island 래퍼로 컨텐츠 감싸기
93
+ * v0.8.0: data-mandu-src 속성 추가 (Runtime이 dynamic import로 로드)
93
94
  */
94
95
  export function wrapWithIsland(
95
96
  content: string,
96
97
  routeId: string,
97
- priority: HydrationPriority = "visible"
98
+ priority: HydrationPriority = "visible",
99
+ bundleSrc?: string
98
100
  ): string {
99
- return `<div data-mandu-island="${routeId}" data-mandu-priority="${priority}">${content}</div>`;
101
+ const srcAttr = bundleSrc ? ` data-mandu-src="${bundleSrc}"` : "";
102
+ return `<div data-mandu-island="${routeId}"${srcAttr} data-mandu-priority="${priority}">${content}</div>`;
100
103
  }
101
104
 
102
105
  export function renderToHTML(element: ReactElement, options: SSROptions = {}): string {
@@ -122,7 +125,10 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
122
125
  hydration && hydration.strategy !== "none" && routeId && bundleManifest;
123
126
 
124
127
  if (needsHydration) {
125
- content = wrapWithIsland(content, routeId, hydration.priority);
128
+ // v0.8.0: bundleSrc를 data-mandu-src 속성으로 전달 (Runtime이 dynamic import로 로드)
129
+ const bundle = bundleManifest.bundles[routeId];
130
+ const bundleSrc = bundle?.js;
131
+ content = wrapWithIsland(content, routeId, hydration.priority, bundleSrc);
126
132
  }
127
133
 
128
134
  // 서버 데이터 스크립트