@mandujs/core 0.9.41 → 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.
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -73
- package/src/bundler/dev.ts +21 -14
- package/src/client/globals.ts +44 -0
- package/src/client/index.ts +5 -4
- package/src/client/island.ts +8 -13
- package/src/client/router.ts +33 -41
- package/src/client/runtime.ts +23 -51
- package/src/client/window-state.ts +101 -0
- package/src/config/index.ts +1 -0
- package/src/config/mandu.ts +45 -9
- package/src/config/validate.ts +158 -0
- package/src/constants.ts +25 -0
- package/src/contract/client.ts +4 -3
- package/src/contract/define.ts +459 -0
- package/src/devtools/ai/context-builder.ts +375 -0
- package/src/devtools/ai/index.ts +25 -0
- package/src/devtools/ai/mcp-connector.ts +465 -0
- package/src/devtools/client/catchers/error-catcher.ts +327 -0
- package/src/devtools/client/catchers/index.ts +18 -0
- package/src/devtools/client/catchers/network-proxy.ts +363 -0
- package/src/devtools/client/components/index.ts +39 -0
- package/src/devtools/client/components/kitchen-root.tsx +362 -0
- package/src/devtools/client/components/mandu-character.tsx +241 -0
- package/src/devtools/client/components/overlay.tsx +368 -0
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -0
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -0
- package/src/devtools/client/components/panel/index.ts +32 -0
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -0
- package/src/devtools/client/components/panel/network-panel.tsx +292 -0
- package/src/devtools/client/components/panel/panel-container.tsx +259 -0
- package/src/devtools/client/filters/context-filters.ts +282 -0
- package/src/devtools/client/filters/index.ts +16 -0
- package/src/devtools/client/index.ts +63 -0
- package/src/devtools/client/persistence.ts +335 -0
- package/src/devtools/client/state-manager.ts +478 -0
- package/src/devtools/design-tokens.ts +263 -0
- package/src/devtools/hook/create-hook.ts +207 -0
- package/src/devtools/hook/index.ts +13 -0
- package/src/devtools/index.ts +439 -0
- package/src/devtools/init.ts +266 -0
- package/src/devtools/protocol.ts +237 -0
- package/src/devtools/server/index.ts +17 -0
- package/src/devtools/server/source-context.ts +444 -0
- package/src/devtools/types.ts +319 -0
- package/src/devtools/worker/index.ts +25 -0
- package/src/devtools/worker/redaction-worker.ts +222 -0
- package/src/devtools/worker/worker-manager.ts +409 -0
- package/src/error/formatter.ts +28 -24
- package/src/error/index.ts +13 -9
- package/src/error/result.ts +46 -0
- package/src/error/types.ts +6 -4
- package/src/filling/filling.ts +6 -5
- package/src/guard/check.ts +60 -56
- package/src/guard/types.ts +3 -1
- package/src/guard/watcher.ts +10 -1
- package/src/index.ts +81 -0
- package/src/intent/index.ts +310 -0
- package/src/island/index.ts +304 -0
- package/src/router/fs-patterns.ts +7 -0
- package/src/router/fs-routes.ts +20 -8
- package/src/router/fs-scanner.ts +117 -133
- package/src/runtime/server.ts +261 -201
- package/src/runtime/ssr.ts +5 -4
- package/src/runtime/streaming-ssr.ts +5 -4
- package/src/utils/bun.ts +8 -0
- package/src/utils/lru-cache.ts +75 -0
package/package.json
CHANGED
package/src/bundler/build.ts
CHANGED
|
@@ -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
|
|
16
|
-
import
|
|
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') || '
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
//
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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 ||
|
|
1055
|
+
priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
|
|
1038
1056
|
};
|
|
1039
1057
|
}
|
|
1040
1058
|
|
package/src/bundler/dev.ts
CHANGED
|
@@ -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
|
|
10
|
-
import
|
|
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),
|
|
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 +
|
|
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 +
|
|
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 =
|
|
421
|
-
const reconnectDelay =
|
|
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 {};
|
package/src/client/index.ts
CHANGED
|
@@ -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 {
|
package/src/client/island.ts
CHANGED
|
@@ -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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 상태를 추적하는 훅
|
package/src/client/router.ts
CHANGED
|
@@ -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 (!
|
|
50
|
+
if (!getWindowRouterState()) {
|
|
50
51
|
// SSR에서 주입된 __MANDU_ROUTE__에서 초기화
|
|
51
|
-
const route = (
|
|
52
|
-
const data = (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
currentRoute: route
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
67
|
+
return getWindowRouterState()!;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function setGlobalRouterState(state: RouterState): void {
|
|
68
71
|
if (typeof window !== "undefined") {
|
|
69
|
-
|
|
72
|
+
setWindowRouterState(state);
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
function getGlobalListeners(): Set<RouterListener> {
|
|
74
|
-
|
|
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 = (
|
|
95
|
-
const 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
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
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.
|
|
386
|
+
prefetchedUrls.set(url, true);
|
|
395
387
|
} catch {
|
|
396
388
|
// Prefetch 실패는 무시
|
|
397
389
|
}
|
package/src/client/runtime.ts
CHANGED
|
@@ -6,21 +6,7 @@
|
|
|
6
6
|
* 실제 Hydration Runtime은 bundler/build.ts의 generateRuntimeSource()에서 생성됩니다.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|