@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.
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -73
- package/src/bundler/css.ts +283 -0
- package/src/bundler/dev.ts +31 -6
- package/src/bundler/index.ts +1 -0
- 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 +189 -61
- package/src/runtime/ssr.ts +14 -4
- package/src/runtime/streaming-ssr.ts +15 -4
- package/src/utils/bun.ts +8 -0
- package/src/utils/lru-cache.ts +75 -0
package/README.ko.md
CHANGED
package/README.md
CHANGED
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
|
|
|
@@ -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
|
+
}
|
package/src/bundler/dev.ts
CHANGED
|
@@ -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),
|
|
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 +
|
|
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 +
|
|
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 =
|
|
421
|
-
const reconnectDelay =
|
|
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);
|
package/src/bundler/index.ts
CHANGED
|
@@ -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 {
|