@mandujs/core 0.9.41 → 0.9.43

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 (71) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/bundler/build.ts +91 -73
  5. package/src/bundler/css.ts +283 -0
  6. package/src/bundler/dev.ts +31 -6
  7. package/src/bundler/index.ts +1 -0
  8. package/src/client/globals.ts +44 -0
  9. package/src/client/index.ts +5 -4
  10. package/src/client/island.ts +8 -13
  11. package/src/client/router.ts +33 -41
  12. package/src/client/runtime.ts +23 -51
  13. package/src/client/window-state.ts +101 -0
  14. package/src/config/index.ts +1 -0
  15. package/src/config/mandu.ts +45 -9
  16. package/src/config/validate.ts +158 -0
  17. package/src/constants.ts +25 -0
  18. package/src/contract/client.ts +4 -3
  19. package/src/contract/define.ts +459 -0
  20. package/src/devtools/ai/context-builder.ts +375 -0
  21. package/src/devtools/ai/index.ts +25 -0
  22. package/src/devtools/ai/mcp-connector.ts +465 -0
  23. package/src/devtools/client/catchers/error-catcher.ts +327 -0
  24. package/src/devtools/client/catchers/index.ts +18 -0
  25. package/src/devtools/client/catchers/network-proxy.ts +363 -0
  26. package/src/devtools/client/components/index.ts +39 -0
  27. package/src/devtools/client/components/kitchen-root.tsx +362 -0
  28. package/src/devtools/client/components/mandu-character.tsx +241 -0
  29. package/src/devtools/client/components/overlay.tsx +368 -0
  30. package/src/devtools/client/components/panel/errors-panel.tsx +259 -0
  31. package/src/devtools/client/components/panel/guard-panel.tsx +244 -0
  32. package/src/devtools/client/components/panel/index.ts +32 -0
  33. package/src/devtools/client/components/panel/islands-panel.tsx +304 -0
  34. package/src/devtools/client/components/panel/network-panel.tsx +292 -0
  35. package/src/devtools/client/components/panel/panel-container.tsx +259 -0
  36. package/src/devtools/client/filters/context-filters.ts +282 -0
  37. package/src/devtools/client/filters/index.ts +16 -0
  38. package/src/devtools/client/index.ts +63 -0
  39. package/src/devtools/client/persistence.ts +335 -0
  40. package/src/devtools/client/state-manager.ts +478 -0
  41. package/src/devtools/design-tokens.ts +263 -0
  42. package/src/devtools/hook/create-hook.ts +207 -0
  43. package/src/devtools/hook/index.ts +13 -0
  44. package/src/devtools/index.ts +439 -0
  45. package/src/devtools/init.ts +266 -0
  46. package/src/devtools/protocol.ts +237 -0
  47. package/src/devtools/server/index.ts +17 -0
  48. package/src/devtools/server/source-context.ts +444 -0
  49. package/src/devtools/types.ts +319 -0
  50. package/src/devtools/worker/index.ts +25 -0
  51. package/src/devtools/worker/redaction-worker.ts +222 -0
  52. package/src/devtools/worker/worker-manager.ts +409 -0
  53. package/src/error/formatter.ts +28 -24
  54. package/src/error/index.ts +13 -9
  55. package/src/error/result.ts +46 -0
  56. package/src/error/types.ts +6 -4
  57. package/src/filling/filling.ts +6 -5
  58. package/src/guard/check.ts +60 -56
  59. package/src/guard/types.ts +3 -1
  60. package/src/guard/watcher.ts +10 -1
  61. package/src/index.ts +81 -0
  62. package/src/intent/index.ts +310 -0
  63. package/src/island/index.ts +304 -0
  64. package/src/router/fs-patterns.ts +7 -0
  65. package/src/router/fs-routes.ts +20 -8
  66. package/src/router/fs-scanner.ts +117 -133
  67. package/src/runtime/server.ts +189 -61
  68. package/src/runtime/ssr.ts +14 -4
  69. package/src/runtime/streaming-ssr.ts +15 -4
  70. package/src/utils/bun.ts +8 -0
  71. package/src/utils/lru-cache.ts +75 -0
package/README.ko.md CHANGED
@@ -215,7 +215,7 @@ import type {
215
215
  ## 요구 사항
216
216
 
217
217
  - Bun >= 1.0.0
218
- - React >= 18.0.0
218
+ - React >= 19.0.0
219
219
  - Zod >= 3.0.0
220
220
 
221
221
  ## 관련 패키지
package/README.md CHANGED
@@ -490,7 +490,7 @@ import type {
490
490
  ## Requirements
491
491
 
492
492
  - Bun >= 1.0.0
493
- - React >= 18.0.0
493
+ - React >= 19.0.0
494
494
  - Zod >= 3.0.0
495
495
 
496
496
  ## Related Packages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.9.41",
3
+ "version": "0.9.43",
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
 
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Mandu CSS Builder
3
+ * Tailwind CSS v4 CLI 기반 CSS 빌드 및 감시
4
+ *
5
+ * 특징:
6
+ * - Tailwind v4 Oxide Engine (Rust) 사용
7
+ * - Zero Config: @import "tailwindcss" 자동 감지
8
+ * - 출력: .mandu/client/globals.css
9
+ */
10
+
11
+ import { spawn, type Subprocess } from "bun";
12
+ import path from "path";
13
+ import fs from "fs/promises";
14
+
15
+ // ========== Types ==========
16
+
17
+ export interface CSSBuildOptions {
18
+ /** 프로젝트 루트 디렉토리 */
19
+ rootDir: string;
20
+ /** CSS 입력 파일 (기본: "app/globals.css") */
21
+ input?: string;
22
+ /** CSS 출력 파일 (기본: ".mandu/client/globals.css") */
23
+ output?: string;
24
+ /** Watch 모드 활성화 */
25
+ watch?: boolean;
26
+ /** Minify 활성화 (production) */
27
+ minify?: boolean;
28
+ /** 빌드 완료 콜백 */
29
+ onBuild?: (result: CSSBuildResult) => void;
30
+ /** 에러 콜백 */
31
+ onError?: (error: Error) => void;
32
+ }
33
+
34
+ export interface CSSBuildResult {
35
+ success: boolean;
36
+ outputPath: string;
37
+ buildTime?: number;
38
+ error?: string;
39
+ }
40
+
41
+ export interface CSSWatcher {
42
+ /** Tailwind CLI 프로세스 */
43
+ process: Subprocess;
44
+ /** 출력 파일 경로 (절대 경로) */
45
+ outputPath: string;
46
+ /** 서버 경로 (/.mandu/client/globals.css) */
47
+ serverPath: string;
48
+ /** 프로세스 종료 */
49
+ close: () => void;
50
+ }
51
+
52
+ // ========== Constants ==========
53
+
54
+ const DEFAULT_INPUT = "app/globals.css";
55
+ const DEFAULT_OUTPUT = ".mandu/client/globals.css";
56
+ const SERVER_CSS_PATH = "/.mandu/client/globals.css";
57
+
58
+ // ========== Detection ==========
59
+
60
+ /**
61
+ * Tailwind v4 프로젝트인지 감지
62
+ * app/globals.css에 @import "tailwindcss" 포함 여부 확인
63
+ */
64
+ export async function isTailwindProject(rootDir: string): Promise<boolean> {
65
+ const cssPath = path.join(rootDir, DEFAULT_INPUT);
66
+
67
+ try {
68
+ const content = await fs.readFile(cssPath, "utf-8");
69
+ // Tailwind v4: @import "tailwindcss"
70
+ // Tailwind v3: @tailwind base; @tailwind components; @tailwind utilities;
71
+ return (
72
+ content.includes('@import "tailwindcss"') ||
73
+ content.includes("@import 'tailwindcss'") ||
74
+ content.includes("@tailwind base")
75
+ );
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * CSS 입력 파일 존재 여부 확인
83
+ */
84
+ export async function hasCSSEntry(rootDir: string, input?: string): Promise<boolean> {
85
+ const cssPath = path.join(rootDir, input || DEFAULT_INPUT);
86
+ try {
87
+ await fs.access(cssPath);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ // ========== Build ==========
95
+
96
+ /**
97
+ * CSS 일회성 빌드 (production용)
98
+ */
99
+ export async function buildCSS(options: CSSBuildOptions): Promise<CSSBuildResult> {
100
+ const {
101
+ rootDir,
102
+ input = DEFAULT_INPUT,
103
+ output = DEFAULT_OUTPUT,
104
+ minify = true,
105
+ } = options;
106
+
107
+ const inputPath = path.join(rootDir, input);
108
+ const outputPath = path.join(rootDir, output);
109
+ const startTime = performance.now();
110
+
111
+ // 출력 디렉토리 생성
112
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
113
+
114
+ // Tailwind CLI 실행
115
+ const args = [
116
+ "@tailwindcss/cli",
117
+ "-i", inputPath,
118
+ "-o", outputPath,
119
+ ];
120
+
121
+ if (minify) {
122
+ args.push("--minify");
123
+ }
124
+
125
+ try {
126
+ const proc = spawn(["bunx", ...args], {
127
+ cwd: rootDir,
128
+ stdout: "pipe",
129
+ stderr: "pipe",
130
+ });
131
+
132
+ // 프로세스 완료 대기
133
+ const exitCode = await proc.exited;
134
+
135
+ if (exitCode !== 0) {
136
+ const stderr = await new Response(proc.stderr).text();
137
+ return {
138
+ success: false,
139
+ outputPath,
140
+ error: stderr || `Tailwind CLI exited with code ${exitCode}`,
141
+ };
142
+ }
143
+
144
+ const buildTime = performance.now() - startTime;
145
+
146
+ return {
147
+ success: true,
148
+ outputPath,
149
+ buildTime,
150
+ };
151
+ } catch (error) {
152
+ return {
153
+ success: false,
154
+ outputPath,
155
+ error: error instanceof Error ? error.message : String(error),
156
+ };
157
+ }
158
+ }
159
+
160
+ // ========== Watch ==========
161
+
162
+ /**
163
+ * CSS 감시 모드 시작 (development용)
164
+ * Tailwind CLI --watch 모드로 실행
165
+ */
166
+ export async function startCSSWatch(options: CSSBuildOptions): Promise<CSSWatcher> {
167
+ const {
168
+ rootDir,
169
+ input = DEFAULT_INPUT,
170
+ output = DEFAULT_OUTPUT,
171
+ minify = false,
172
+ onBuild,
173
+ onError,
174
+ } = options;
175
+
176
+ const inputPath = path.join(rootDir, input);
177
+ const outputPath = path.join(rootDir, output);
178
+
179
+ // 출력 디렉토리 생성
180
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
181
+
182
+ // Tailwind CLI 인자 구성
183
+ const args = [
184
+ "@tailwindcss/cli",
185
+ "-i", inputPath,
186
+ "-o", outputPath,
187
+ "--watch",
188
+ ];
189
+
190
+ if (minify) {
191
+ args.push("--minify");
192
+ }
193
+
194
+ console.log(`🎨 Tailwind CSS v4 빌드 시작...`);
195
+ console.log(` 입력: ${input}`);
196
+ console.log(` 출력: ${output}`);
197
+
198
+ // Bun subprocess로 Tailwind CLI 실행
199
+ const proc = spawn(["bunx", ...args], {
200
+ cwd: rootDir,
201
+ stdout: "pipe",
202
+ stderr: "pipe",
203
+ });
204
+
205
+ // stdout 모니터링 (빌드 완료 감지)
206
+ (async () => {
207
+ const reader = proc.stdout.getReader();
208
+ const decoder = new TextDecoder();
209
+
210
+ while (true) {
211
+ const { done, value } = await reader.read();
212
+ if (done) break;
213
+
214
+ const text = decoder.decode(value);
215
+ const lines = text.split("\n").filter((l) => l.trim());
216
+
217
+ for (const line of lines) {
218
+ // Tailwind v4 출력 패턴: "Done in Xms" 또는 빌드 완료 메시지
219
+ if (line.includes("Done in") || line.includes("Rebuilt in")) {
220
+ console.log(` ✅ CSS ${line.trim()}`);
221
+ onBuild?.({
222
+ success: true,
223
+ outputPath,
224
+ });
225
+ } else if (line.includes("warn") || line.includes("Warning")) {
226
+ console.log(` ⚠️ CSS ${line.trim()}`);
227
+ }
228
+ }
229
+ }
230
+ })();
231
+
232
+ // stderr 모니터링 (에러 감지)
233
+ (async () => {
234
+ const reader = proc.stderr.getReader();
235
+ const decoder = new TextDecoder();
236
+
237
+ while (true) {
238
+ const { done, value } = await reader.read();
239
+ if (done) break;
240
+
241
+ const text = decoder.decode(value).trim();
242
+ if (text) {
243
+ // bash_profile 경고는 무시
244
+ if (text.includes(".bash_profile") || text.includes("$'\\377")) {
245
+ continue;
246
+ }
247
+ console.error(` ❌ CSS Error: ${text}`);
248
+ onError?.(new Error(text));
249
+ }
250
+ }
251
+ })();
252
+
253
+ // 프로세스 종료 감지
254
+ proc.exited.then((code) => {
255
+ if (code !== 0 && code !== null) {
256
+ console.error(` ❌ Tailwind CLI exited with code ${code}`);
257
+ }
258
+ });
259
+
260
+ return {
261
+ process: proc,
262
+ outputPath,
263
+ serverPath: SERVER_CSS_PATH,
264
+ close: () => {
265
+ proc.kill();
266
+ },
267
+ };
268
+ }
269
+
270
+ /**
271
+ * CSS 서버 경로 반환
272
+ */
273
+ export function getCSSServerPath(): string {
274
+ return SERVER_CSS_PATH;
275
+ }
276
+
277
+ /**
278
+ * CSS 링크 태그 생성
279
+ */
280
+ export function generateCSSLinkTag(isDev: boolean = false): string {
281
+ const cacheBust = isDev ? `?t=${Date.now()}` : "";
282
+ return `<link rel="stylesheet" href="${SERVER_CSS_PATH}${cacheBust}">`;
283
+ }
@@ -6,6 +6,7 @@
6
6
  import type { RoutesManifest, RouteSpec } from "../spec/schema";
7
7
  import { buildClientBundles } from "./build";
8
8
  import type { BundleResult } from "./types";
9
+ import { PORTS, TIMEOUTS } from "../constants";
9
10
  import path from "path";
10
11
  import fs from "fs";
11
12
 
@@ -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);
@@ -312,12 +319,15 @@ export interface HMRServer {
312
319
  }
313
320
 
314
321
  export interface HMRMessage {
315
- type: "connected" | "reload" | "island-update" | "layout-update" | "error" | "ping";
322
+ type: "connected" | "reload" | "island-update" | "layout-update" | "css-update" | "error" | "ping" | "guard-violation";
316
323
  data?: {
317
324
  routeId?: string;
318
325
  layoutPath?: string;
326
+ cssPath?: string;
319
327
  message?: string;
320
328
  timestamp?: number;
329
+ file?: string;
330
+ violations?: Array<{ line: number; message: string }>;
321
331
  };
322
332
  }
323
333
 
@@ -326,7 +336,7 @@ export interface HMRMessage {
326
336
  */
327
337
  export function createHMRServer(port: number): HMRServer {
328
338
  const clients = new Set<any>();
329
- const hmrPort = port + 1;
339
+ const hmrPort = port + PORTS.HMR_OFFSET;
330
340
 
331
341
  const server = Bun.serve({
332
342
  port: hmrPort,
@@ -410,15 +420,15 @@ export function createHMRServer(port: number): HMRServer {
410
420
  * 브라우저에서 실행되어 HMR 서버와 연결
411
421
  */
412
422
  export function generateHMRClientScript(port: number): string {
413
- const hmrPort = port + 1;
423
+ const hmrPort = port + PORTS.HMR_OFFSET;
414
424
 
415
425
  return `
416
426
  (function() {
417
427
  const HMR_PORT = ${hmrPort};
418
428
  let ws = null;
419
429
  let reconnectAttempts = 0;
420
- const maxReconnectAttempts = 10;
421
- const reconnectDelay = 1000;
430
+ const maxReconnectAttempts = ${TIMEOUTS.HMR_MAX_RECONNECT};
431
+ const reconnectDelay = ${TIMEOUTS.HMR_RECONNECT_DELAY};
422
432
 
423
433
  function connect() {
424
434
  try {
@@ -490,6 +500,21 @@ export function generateHMRClientScript(port: number): string {
490
500
  location.reload();
491
501
  break;
492
502
 
503
+ case 'css-update':
504
+ console.log('[Mandu HMR] CSS updated');
505
+ // CSS 핫 리로드 (페이지 새로고침 없이 스타일시트만 교체)
506
+ var targetCssPath = message.data?.cssPath || '/.mandu/client/globals.css';
507
+ var links = document.querySelectorAll('link[rel="stylesheet"]');
508
+ links.forEach(function(link) {
509
+ var href = link.getAttribute('href') || '';
510
+ var baseHref = href.split('?')[0];
511
+ // 정확한 경로 매칭 우선, fallback으로 기존 패턴 매칭
512
+ if (baseHref === targetCssPath || href.includes('globals.css') || href.includes('.mandu/client')) {
513
+ link.setAttribute('href', baseHref + '?t=' + Date.now());
514
+ }
515
+ });
516
+ break;
517
+
493
518
  case 'error':
494
519
  console.error('[Mandu HMR] Build error:', message.data?.message);
495
520
  showErrorOverlay(message.data?.message);
@@ -6,3 +6,4 @@
6
6
  export * from "./types";
7
7
  export * from "./build";
8
8
  export * from "./dev";
9
+ export * from "./css";
@@ -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 {