@mandujs/core 0.9.40 → 0.9.42

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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/bundler/build.ts +91 -73
  3. package/src/bundler/dev.ts +21 -14
  4. package/src/client/globals.ts +44 -0
  5. package/src/client/index.ts +5 -4
  6. package/src/client/island.ts +8 -13
  7. package/src/client/router.ts +33 -41
  8. package/src/client/runtime.ts +23 -51
  9. package/src/client/window-state.ts +101 -0
  10. package/src/config/index.ts +1 -0
  11. package/src/config/mandu.ts +45 -9
  12. package/src/config/validate.ts +158 -0
  13. package/src/constants.ts +25 -0
  14. package/src/contract/client.ts +4 -3
  15. package/src/contract/define.ts +459 -0
  16. package/src/devtools/ai/context-builder.ts +375 -0
  17. package/src/devtools/ai/index.ts +25 -0
  18. package/src/devtools/ai/mcp-connector.ts +465 -0
  19. package/src/devtools/client/catchers/error-catcher.ts +327 -0
  20. package/src/devtools/client/catchers/index.ts +18 -0
  21. package/src/devtools/client/catchers/network-proxy.ts +363 -0
  22. package/src/devtools/client/components/index.ts +39 -0
  23. package/src/devtools/client/components/kitchen-root.tsx +362 -0
  24. package/src/devtools/client/components/mandu-character.tsx +241 -0
  25. package/src/devtools/client/components/overlay.tsx +368 -0
  26. package/src/devtools/client/components/panel/errors-panel.tsx +259 -0
  27. package/src/devtools/client/components/panel/guard-panel.tsx +244 -0
  28. package/src/devtools/client/components/panel/index.ts +32 -0
  29. package/src/devtools/client/components/panel/islands-panel.tsx +304 -0
  30. package/src/devtools/client/components/panel/network-panel.tsx +292 -0
  31. package/src/devtools/client/components/panel/panel-container.tsx +259 -0
  32. package/src/devtools/client/filters/context-filters.ts +282 -0
  33. package/src/devtools/client/filters/index.ts +16 -0
  34. package/src/devtools/client/index.ts +63 -0
  35. package/src/devtools/client/persistence.ts +335 -0
  36. package/src/devtools/client/state-manager.ts +478 -0
  37. package/src/devtools/design-tokens.ts +263 -0
  38. package/src/devtools/hook/create-hook.ts +207 -0
  39. package/src/devtools/hook/index.ts +13 -0
  40. package/src/devtools/index.ts +439 -0
  41. package/src/devtools/init.ts +266 -0
  42. package/src/devtools/protocol.ts +237 -0
  43. package/src/devtools/server/index.ts +17 -0
  44. package/src/devtools/server/source-context.ts +444 -0
  45. package/src/devtools/types.ts +319 -0
  46. package/src/devtools/worker/index.ts +25 -0
  47. package/src/devtools/worker/redaction-worker.ts +222 -0
  48. package/src/devtools/worker/worker-manager.ts +409 -0
  49. package/src/error/formatter.ts +28 -24
  50. package/src/error/index.ts +13 -9
  51. package/src/error/result.ts +46 -0
  52. package/src/error/types.ts +6 -4
  53. package/src/filling/filling.ts +6 -5
  54. package/src/guard/check.ts +60 -56
  55. package/src/guard/types.ts +3 -1
  56. package/src/guard/watcher.ts +10 -1
  57. package/src/index.ts +81 -0
  58. package/src/intent/index.ts +310 -0
  59. package/src/island/index.ts +304 -0
  60. package/src/router/fs-patterns.ts +7 -0
  61. package/src/router/fs-routes.ts +20 -8
  62. package/src/router/fs-scanner.ts +117 -133
  63. package/src/runtime/server.ts +261 -201
  64. package/src/runtime/ssr.ts +5 -4
  65. package/src/runtime/streaming-ssr.ts +5 -4
  66. package/src/utils/bun.ts +8 -0
  67. package/src/utils/lru-cache.ts +75 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.9.40",
3
+ "version": "0.9.42",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -3,17 +3,18 @@
3
3
  * Bun.build 기반 클라이언트 번들 빌드
4
4
  */
5
5
 
6
- import type { RoutesManifest, RouteSpec } from "../spec/schema";
7
- import { needsHydration, getRouteHydration } from "../spec/schema";
8
- import type {
9
- BundleResult,
10
- BundleOutput,
11
- BundleManifest,
12
- BundleStats,
13
- BundlerOptions,
14
- } from "./types";
15
- import path from "path";
16
- import fs from "fs/promises";
6
+ import type { RoutesManifest, RouteSpec } from "../spec/schema";
7
+ import { needsHydration, getRouteHydration } from "../spec/schema";
8
+ import type {
9
+ BundleResult,
10
+ BundleOutput,
11
+ BundleManifest,
12
+ BundleStats,
13
+ BundlerOptions,
14
+ } from "./types";
15
+ import { HYDRATION } from "../constants";
16
+ import path from "path";
17
+ import fs from "fs/promises";
17
18
 
18
19
  /**
19
20
  * 빈 매니페스트 생성
@@ -281,7 +282,7 @@ function hydrateIslands() {
281
282
  for (const el of islands) {
282
283
  const id = el.getAttribute('data-mandu-island');
283
284
  const src = el.getAttribute('data-mandu-src');
284
- const priority = el.getAttribute('data-mandu-priority') || 'visible';
285
+ const priority = el.getAttribute('data-mandu-priority') || '${HYDRATION.DEFAULT_PRIORITY}';
285
286
 
286
287
  if (!id || !src) {
287
288
  console.warn('[Mandu] Island missing id or src:', el);
@@ -854,20 +855,21 @@ interface VendorBuildResult {
854
855
  * Vendor shim 번들 빌드
855
856
  * React, ReactDOM, ReactDOMClient를 각각의 shim으로 빌드
856
857
  */
857
- async function buildVendorShims(
858
- outDir: string,
859
- options: BundlerOptions
860
- ): Promise<VendorBuildResult> {
861
- const errors: string[] = [];
862
- const results: Record<string, string> = {
863
- react: "",
864
- reactDom: "",
865
- reactDomClient: "",
866
- jsxRuntime: "",
867
- jsxDevRuntime: "",
868
- };
869
-
870
- const shims = [
858
+ async function buildVendorShims(
859
+ outDir: string,
860
+ options: BundlerOptions
861
+ ): Promise<VendorBuildResult> {
862
+ const errors: string[] = [];
863
+ type VendorShimKey = "react" | "reactDom" | "reactDomClient" | "jsxRuntime" | "jsxDevRuntime";
864
+ const results: Record<VendorShimKey, string> = {
865
+ react: "",
866
+ reactDom: "",
867
+ reactDomClient: "",
868
+ jsxRuntime: "",
869
+ jsxDevRuntime: "",
870
+ };
871
+
872
+ const shims: Array<{ name: string; source: string; key: VendorShimKey }> = [
871
873
  { name: "_react", source: generateReactShimSource(), key: "react" },
872
874
  { name: "_react-dom", source: generateReactDOMShimSource(), key: "reactDom" },
873
875
  { name: "_react-dom-client", source: generateReactDOMClientShimSource(), key: "reactDomClient" },
@@ -875,52 +877,68 @@ async function buildVendorShims(
875
877
  { name: "_jsx-dev-runtime", source: generateJsxDevRuntimeShimSource(), key: "jsxDevRuntime" },
876
878
  ];
877
879
 
878
- for (const shim of shims) {
879
- const srcPath = path.join(outDir, `${shim.name}.src.js`);
880
- const outputName = `${shim.name}.js`;
881
-
882
- try {
883
- await Bun.write(srcPath, shim.source);
884
-
885
- // _react.js와 jsx-runtime들은 완전히 번들링 (external 없음)
886
- // _react-dom*, jsx-runtime은 react를 external로 처리하여 동일한 React 인스턴스 공유
887
- // jsx-runtime은 Fragment를 react에서 가져오므로 react만 external
888
- let shimExternal: string[] = [];
889
- if (shim.name === "_react-dom" || shim.name === "_react-dom-client") {
890
- shimExternal = ["react"];
891
- } else if (shim.name === "_jsx-runtime" || shim.name === "_jsx-dev-runtime") {
892
- // jsx-runtime react를 external로 (Fragment 때문에),
893
- // 하지만 react/jsx-runtime은 번들링되어야 함
894
- shimExternal = ["react"];
895
- }
896
- // _react.js는 external 없이 React 전체를 번들링
897
-
898
- const result = await Bun.build({
899
- entrypoints: [srcPath],
900
- outdir: outDir,
901
- naming: outputName,
902
- minify: options.minify ?? process.env.NODE_ENV === "production",
903
- sourcemap: options.sourcemap ? "external" : "none",
904
- target: "browser",
905
- external: shimExternal,
906
- define: {
907
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
908
- ...options.define,
909
- },
910
- });
911
-
912
- await fs.unlink(srcPath).catch(() => {});
913
-
914
- if (!result.success) {
915
- errors.push(`[${shim.name}] ${result.logs.map((l) => l.message).join(", ")}`);
916
- } else {
917
- results[shim.key] = `/.mandu/client/${outputName}`;
918
- }
919
- } catch (error) {
920
- await fs.unlink(srcPath).catch(() => {});
921
- errors.push(`[${shim.name}] ${String(error)}`);
922
- }
923
- }
880
+ const buildShim = async (
881
+ shim: { name: string; source: string; key: VendorShimKey }
882
+ ): Promise<{ key: VendorShimKey; outputPath?: string; error?: string }> => {
883
+ const srcPath = path.join(outDir, `${shim.name}.src.js`);
884
+ const outputName = `${shim.name}.js`;
885
+
886
+ try {
887
+ await Bun.write(srcPath, shim.source);
888
+
889
+ // _react.js는 external 없이 React 전체를 번들링
890
+ // _react-dom*, jsx-runtime은 react를 external로 처리하여 동일한 React 인스턴스 공유
891
+ let shimExternal: string[] = [];
892
+ if (shim.name === "_react-dom" || shim.name === "_react-dom-client") {
893
+ shimExternal = ["react"];
894
+ } else if (shim.name === "_jsx-runtime" || shim.name === "_jsx-dev-runtime") {
895
+ shimExternal = ["react"];
896
+ }
897
+
898
+ const result = await Bun.build({
899
+ entrypoints: [srcPath],
900
+ outdir: outDir,
901
+ naming: outputName,
902
+ minify: options.minify ?? process.env.NODE_ENV === "production",
903
+ sourcemap: options.sourcemap ? "external" : "none",
904
+ target: "browser",
905
+ external: shimExternal,
906
+ define: {
907
+ "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
908
+ ...options.define,
909
+ },
910
+ });
911
+
912
+ await fs.unlink(srcPath).catch(() => {});
913
+
914
+ if (!result.success) {
915
+ return {
916
+ key: shim.key,
917
+ error: `[${shim.name}] ${result.logs.map((l) => l.message).join(", ")}`,
918
+ };
919
+ }
920
+
921
+ return {
922
+ key: shim.key,
923
+ outputPath: `/.mandu/client/${outputName}`,
924
+ };
925
+ } catch (error) {
926
+ await fs.unlink(srcPath).catch(() => {});
927
+ return {
928
+ key: shim.key,
929
+ error: `[${shim.name}] ${String(error)}`,
930
+ };
931
+ }
932
+ };
933
+
934
+ const buildResults = await Promise.all(shims.map((shim) => buildShim(shim)));
935
+ for (const result of buildResults) {
936
+ if (result.error) {
937
+ errors.push(result.error);
938
+ } else if (result.outputPath) {
939
+ results[result.key] = result.outputPath;
940
+ }
941
+ }
924
942
 
925
943
  return {
926
944
  success: errors.length === 0,
@@ -1034,7 +1052,7 @@ function createBundleManifest(
1034
1052
  bundles[output.routeId] = {
1035
1053
  js: output.outputPath,
1036
1054
  dependencies: ["_runtime", "_react"],
1037
- priority: hydration?.priority || "visible",
1055
+ priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
1038
1056
  };
1039
1057
  }
1040
1058
 
@@ -3,11 +3,12 @@
3
3
  * 개발 모드 번들링 + HMR (Hot Module Replacement)
4
4
  */
5
5
 
6
- import type { RoutesManifest, RouteSpec } from "../spec/schema";
7
- import { buildClientBundles } from "./build";
8
- import type { BundleResult } from "./types";
9
- import path from "path";
10
- import fs from "fs";
6
+ import type { RoutesManifest, RouteSpec } from "../spec/schema";
7
+ import { buildClientBundles } from "./build";
8
+ import type { BundleResult } from "./types";
9
+ import { PORTS, TIMEOUTS } from "../constants";
10
+ import path from "path";
11
+ import fs from "fs";
11
12
 
12
13
  export interface DevBundlerOptions {
13
14
  /** 프로젝트 루트 */
@@ -57,6 +58,12 @@ const DEFAULT_COMMON_DIRS = [
57
58
  "hooks",
58
59
  "src/utils",
59
60
  "utils",
61
+ // Islands & Client 디렉토리
62
+ "src/client",
63
+ "client",
64
+ "src/islands",
65
+ "islands",
66
+ "apps/web",
60
67
  ];
61
68
 
62
69
  /**
@@ -267,7 +274,7 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
267
274
  clearTimeout(debounceTimer);
268
275
  }
269
276
 
270
- debounceTimer = setTimeout(() => handleFileChange(fullPath), 100);
277
+ debounceTimer = setTimeout(() => handleFileChange(fullPath), TIMEOUTS.WATCHER_DEBOUNCE);
271
278
  });
272
279
 
273
280
  watchers.push(watcher);
@@ -326,7 +333,7 @@ export interface HMRMessage {
326
333
  */
327
334
  export function createHMRServer(port: number): HMRServer {
328
335
  const clients = new Set<any>();
329
- const hmrPort = port + 1;
336
+ const hmrPort = port + PORTS.HMR_OFFSET;
330
337
 
331
338
  const server = Bun.serve({
332
339
  port: hmrPort,
@@ -409,16 +416,16 @@ export function createHMRServer(port: number): HMRServer {
409
416
  * HMR 클라이언트 스크립트 생성
410
417
  * 브라우저에서 실행되어 HMR 서버와 연결
411
418
  */
412
- export function generateHMRClientScript(port: number): string {
413
- const hmrPort = port + 1;
419
+ export function generateHMRClientScript(port: number): string {
420
+ const hmrPort = port + PORTS.HMR_OFFSET;
414
421
 
415
422
  return `
416
423
  (function() {
417
- const HMR_PORT = ${hmrPort};
418
- let ws = null;
419
- let reconnectAttempts = 0;
420
- const maxReconnectAttempts = 10;
421
- const reconnectDelay = 1000;
424
+ const HMR_PORT = ${hmrPort};
425
+ let ws = null;
426
+ let reconnectAttempts = 0;
427
+ const maxReconnectAttempts = ${TIMEOUTS.HMR_MAX_RECONNECT};
428
+ const reconnectDelay = ${TIMEOUTS.HMR_RECONNECT_DELAY};
422
429
 
423
430
  function connect() {
424
431
  try {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Mandu 전역 타입 선언
3
+ * 클라이언트 측 전역 상태의 타입 정의
4
+ */
5
+ import type { Root } from "react-dom/client";
6
+ import type { RouterState } from "./router";
7
+
8
+ interface ManduRouteInfo {
9
+ id: string;
10
+ pattern: string;
11
+ params: Record<string, string>;
12
+ }
13
+
14
+ interface ManduDataEntry {
15
+ serverData: unknown;
16
+ timestamp?: number;
17
+ }
18
+
19
+ declare global {
20
+ interface Window {
21
+ /** 서버에서 전달된 데이터 (routeId → data) */
22
+ __MANDU_DATA__?: Record<string, ManduDataEntry>;
23
+
24
+ /** 직렬화된 서버 데이터 (raw JSON) */
25
+ __MANDU_DATA_RAW__?: string;
26
+
27
+ /** 현재 라우트 정보 */
28
+ __MANDU_ROUTE__?: ManduRouteInfo;
29
+
30
+ /** 클라이언트 라우터 상태 */
31
+ __MANDU_ROUTER_STATE__?: RouterState;
32
+
33
+ /** 라우터 상태 변경 리스너 */
34
+ __MANDU_ROUTER_LISTENERS__?: Set<(state: RouterState) => void>;
35
+
36
+ /** Hydrated roots 추적 (unmount용) */
37
+ __MANDU_ROOTS__?: Map<string, Root>;
38
+
39
+ /** React 인스턴스 공유 */
40
+ __MANDU_REACT__?: typeof import("react");
41
+ }
42
+ }
43
+
44
+ export {};
@@ -1,6 +1,6 @@
1
- /**
2
- * Mandu Client Module 🏝️
3
- * 클라이언트 사이드 hydration 및 라우팅을 위한 API
1
+ /**
2
+ * Mandu Client Module 🏝️
3
+ * 클라이언트 사이드 hydration 및 라우팅을 위한 API
4
4
  *
5
5
  * @example
6
6
  * ```typescript
@@ -23,7 +23,8 @@
23
23
  * return <Link href="/about">About</Link>;
24
24
  * }
25
25
  * ```
26
- */
26
+ */
27
+ import "./globals";
27
28
 
28
29
  // Island API
29
30
  export {
@@ -3,7 +3,8 @@
3
3
  * Hydration을 위한 클라이언트 사이드 컴포넌트 정의
4
4
  */
5
5
 
6
- import type { ReactNode } from "react";
6
+ import type { ReactNode } from "react";
7
+ import { getServerData as getGlobalServerData } from "./window-state";
7
8
 
8
9
  /**
9
10
  * Island 정의 타입
@@ -117,18 +118,12 @@ export function island<TServerData, TSetupResult = TServerData>(
117
118
  * SSR 데이터에 안전하게 접근하는 훅
118
119
  * 서버 데이터가 없는 경우 fallback 반환
119
120
  */
120
- export function useServerData<T>(key: string, fallback: T): T {
121
- if (typeof window === "undefined") {
122
- return fallback;
123
- }
124
-
125
- const manduData = (window as any).__MANDU_DATA__;
126
- if (!manduData || !(key in manduData)) {
127
- return fallback;
128
- }
129
-
130
- return manduData[key] as T;
131
- }
121
+ export function useServerData<T>(key: string, fallback: T): T {
122
+ if (typeof window === "undefined") return fallback;
123
+
124
+ const data = getGlobalServerData<T>(key);
125
+ return data === undefined ? fallback : data;
126
+ }
132
127
 
133
128
  /**
134
129
  * Hydration 상태를 추적하는 훅
@@ -4,6 +4,16 @@
4
4
  */
5
5
 
6
6
  import type { ReactNode } from "react";
7
+ import {
8
+ getManduData,
9
+ getManduRoute,
10
+ getRouterListeners,
11
+ getRouterState as getWindowRouterState,
12
+ setRouterState as setWindowRouterState,
13
+ setServerData,
14
+ } from "./window-state";
15
+ import { LRUCache } from "../utils/lru-cache";
16
+ import { LIMITS } from "../constants";
7
17
 
8
18
  // ========== Types ==========
9
19
 
@@ -33,51 +43,38 @@ export interface NavigateOptions {
33
43
 
34
44
  type RouterListener = (state: RouterState) => void;
35
45
 
36
- // ========== Global Router State (모든 모듈에서 동일 인스턴스 공유) ==========
37
-
38
- declare global {
39
- interface Window {
40
- __MANDU_ROUTER_STATE__?: RouterState;
41
- __MANDU_ROUTER_LISTENERS__?: Set<RouterListener>;
42
- }
43
- }
44
-
45
46
  function getGlobalRouterState(): RouterState {
46
47
  if (typeof window === "undefined") {
47
48
  return { currentRoute: null, loaderData: undefined, navigation: { state: "idle" } };
48
49
  }
49
- if (!window.__MANDU_ROUTER_STATE__) {
50
+ if (!getWindowRouterState()) {
50
51
  // SSR에서 주입된 __MANDU_ROUTE__에서 초기화
51
- const route = (window as any).__MANDU_ROUTE__;
52
- const data = (window as any).__MANDU_DATA__;
53
-
54
- window.__MANDU_ROUTER_STATE__ = {
55
- currentRoute: route ? {
56
- id: route.id,
57
- pattern: route.pattern,
58
- params: route.params || {},
59
- } : null,
52
+ const route = getManduRoute();
53
+ const data = getManduData();
54
+
55
+ setWindowRouterState({
56
+ currentRoute: route
57
+ ? {
58
+ id: route.id,
59
+ pattern: route.pattern,
60
+ params: route.params || {},
61
+ }
62
+ : null,
60
63
  loaderData: route && data?.[route.id]?.serverData,
61
64
  navigation: { state: "idle" },
62
- };
65
+ });
63
66
  }
64
- return window.__MANDU_ROUTER_STATE__;
67
+ return getWindowRouterState()!;
65
68
  }
66
69
 
67
70
  function setGlobalRouterState(state: RouterState): void {
68
71
  if (typeof window !== "undefined") {
69
- window.__MANDU_ROUTER_STATE__ = state;
72
+ setWindowRouterState(state);
70
73
  }
71
74
  }
72
75
 
73
76
  function getGlobalListeners(): Set<RouterListener> {
74
- if (typeof window === "undefined") {
75
- return new Set();
76
- }
77
- if (!window.__MANDU_ROUTER_LISTENERS__) {
78
- window.__MANDU_ROUTER_LISTENERS__ = new Set();
79
- }
80
- return window.__MANDU_ROUTER_LISTENERS__;
77
+ return getRouterListeners();
81
78
  }
82
79
 
83
80
  // Getter for routerState (전역 상태 참조)
@@ -91,8 +88,8 @@ const listeners = { get current() { return getGlobalListeners(); } };
91
88
  function initializeFromServer(): void {
92
89
  if (typeof window === "undefined") return;
93
90
 
94
- const route = (window as any).__MANDU_ROUTE__;
95
- const data = (window as any).__MANDU_DATA__;
91
+ const route = getManduRoute();
92
+ const data = getManduData();
96
93
 
97
94
  if (route) {
98
95
  // URL에서 실제 params 추출
@@ -117,7 +114,7 @@ interface CompiledPattern {
117
114
  paramNames: string[];
118
115
  }
119
116
 
120
- const patternCache = new Map<string, CompiledPattern>();
117
+ const patternCache = new LRUCache<string, CompiledPattern>(LIMITS.ROUTER_PATTERN_CACHE);
121
118
 
122
119
  /**
123
120
  * 패턴을 정규식으로 컴파일
@@ -237,12 +234,7 @@ export async function navigate(
237
234
  });
238
235
 
239
236
  // __MANDU_DATA__ 업데이트
240
- if (typeof window !== "undefined") {
241
- (window as any).__MANDU_DATA__ = {
242
- ...(window as any).__MANDU_DATA__,
243
- [data.routeId]: { serverData: data.loaderData },
244
- };
245
- }
237
+ setServerData(data.routeId, data.loaderData);
246
238
 
247
239
  notifyListeners();
248
240
 
@@ -271,7 +263,7 @@ function handlePopState(event: PopStateEvent): void {
271
263
  });
272
264
  } else {
273
265
  // 직접 URL 입력 등으로 방문한 페이지 - 상태만 업데이트
274
- const route = (window as any).__MANDU_ROUTE__;
266
+ const route = getManduRoute();
275
267
  setGlobalRouterState({
276
268
  currentRoute: route ? {
277
269
  id: route.id,
@@ -380,7 +372,7 @@ function handleLinkClick(event: MouseEvent): void {
380
372
 
381
373
  // ========== Prefetch ==========
382
374
 
383
- const prefetchedUrls = new Set<string>();
375
+ const prefetchedUrls = new LRUCache<string, true>(LIMITS.ROUTER_PREFETCH_CACHE);
384
376
 
385
377
  /**
386
378
  * 페이지 데이터 미리 로드
@@ -391,7 +383,7 @@ export async function prefetch(url: string): Promise<void> {
391
383
  try {
392
384
  const dataUrl = `${url}${url.includes("?") ? "&" : "?"}_data=1`;
393
385
  await fetch(dataUrl, { priority: "low" } as RequestInit);
394
- prefetchedUrls.add(url);
386
+ prefetchedUrls.set(url, true);
395
387
  } catch {
396
388
  // Prefetch 실패는 무시
397
389
  }
@@ -6,21 +6,7 @@
6
6
  * 실제 Hydration Runtime은 bundler/build.ts의 generateRuntimeSource()에서 생성됩니다.
7
7
  */
8
8
 
9
- import type { Root } from "react-dom/client";
10
-
11
- /**
12
- * Window 전역 타입 확장
13
- */
14
- declare global {
15
- interface Window {
16
- /** Hydrated React roots (unmount용) */
17
- __MANDU_ROOTS__: Map<string, Root>;
18
- /** 서버 데이터 */
19
- __MANDU_DATA__?: Record<string, { serverData: unknown; timestamp: number }>;
20
- /** 직렬화된 서버 데이터 (raw JSON) */
21
- __MANDU_DATA_RAW__?: string;
22
- }
23
- }
9
+ import { getHydratedRoots, getServerData as getGlobalServerData } from "./window-state";
24
10
 
25
11
  /**
26
12
  * Hydration 상태 추적
@@ -40,18 +26,10 @@ export type HydrationPriority = "immediate" | "visible" | "idle" | "interaction"
40
26
  /**
41
27
  * 서버 데이터 가져오기
42
28
  */
43
- export function getServerData<T = unknown>(islandId: string): T | undefined {
44
- if (typeof window === "undefined") {
45
- return undefined;
46
- }
47
-
48
- const manduData = window.__MANDU_DATA__;
49
- if (!manduData) {
50
- return undefined;
51
- }
52
-
53
- return manduData[islandId]?.serverData as T;
54
- }
29
+ export function getServerData<T = unknown>(islandId: string): T | undefined {
30
+ if (typeof window === "undefined") return undefined;
31
+ return getGlobalServerData<T>(islandId);
32
+ }
55
33
 
56
34
  /**
57
35
  * Hydration 상태 조회 (DOM 기반)
@@ -84,31 +62,25 @@ export function getHydrationState(): Readonly<HydrationState> {
84
62
  /**
85
63
  * 특정 Island unmount
86
64
  */
87
- export function unmountIsland(id: string): boolean {
88
- if (typeof window === "undefined" || !window.__MANDU_ROOTS__) {
89
- return false;
90
- }
91
-
92
- const root = window.__MANDU_ROOTS__.get(id);
93
- if (!root) {
94
- return false;
95
- }
96
-
97
- root.unmount();
98
- window.__MANDU_ROOTS__.delete(id);
99
- return true;
100
- }
65
+ export function unmountIsland(id: string): boolean {
66
+ const roots = getHydratedRoots();
67
+ const root = roots.get(id);
68
+ if (!root) {
69
+ return false;
70
+ }
71
+
72
+ root.unmount();
73
+ roots.delete(id);
74
+ return true;
75
+ }
101
76
 
102
77
  /**
103
78
  * 모든 Island unmount
104
79
  */
105
- export function unmountAllIslands(): void {
106
- if (typeof window === "undefined" || !window.__MANDU_ROOTS__) {
107
- return;
108
- }
109
-
110
- for (const [id, root] of window.__MANDU_ROOTS__) {
111
- root.unmount();
112
- window.__MANDU_ROOTS__.delete(id);
113
- }
114
- }
80
+ export function unmountAllIslands(): void {
81
+ const roots = getHydratedRoots();
82
+ for (const [id, root] of roots) {
83
+ root.unmount();
84
+ roots.delete(id);
85
+ }
86
+ }