@mandujs/core 0.4.0 → 0.4.2
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 +41 -41
- package/src/bundler/build.ts +3 -1
- package/src/bundler/dev.ts +362 -0
- package/src/bundler/index.ts +1 -0
- package/src/runtime/server.ts +24 -3
- package/src/runtime/ssr.ts +57 -0
package/package.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./src/index.ts",
|
|
7
|
-
"types": "./src/index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./src/index.ts",
|
|
10
|
-
"./client": "./src/client/index.ts",
|
|
11
|
-
"./*": "./src/*"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"src/**/*"
|
|
15
|
-
],
|
|
16
|
-
"keywords": [
|
|
17
|
-
"mandu",
|
|
18
|
-
"framework",
|
|
19
|
-
"agent",
|
|
20
|
-
"ai",
|
|
21
|
-
"code-generation"
|
|
22
|
-
],
|
|
23
|
-
"repository": {
|
|
24
|
-
"type": "git",
|
|
25
|
-
"url": "https://github.com/konamgil/mandu.git",
|
|
26
|
-
"directory": "packages/core"
|
|
27
|
-
},
|
|
28
|
-
"author": "konamgil",
|
|
29
|
-
"license": "MIT",
|
|
30
|
-
"publishConfig": {
|
|
31
|
-
"access": "public"
|
|
32
|
-
},
|
|
33
|
-
"engines": {
|
|
34
|
-
"bun": ">=1.0.0"
|
|
35
|
-
},
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"react": ">=18.0.0",
|
|
38
|
-
"react-dom": ">=18.0.0",
|
|
39
|
-
"zod": ">=3.0.0"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@mandujs/core",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./client": "./src/client/index.ts",
|
|
11
|
+
"./*": "./src/*"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src/**/*"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mandu",
|
|
18
|
+
"framework",
|
|
19
|
+
"agent",
|
|
20
|
+
"ai",
|
|
21
|
+
"code-generation"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/konamgil/mandu.git",
|
|
26
|
+
"directory": "packages/core"
|
|
27
|
+
},
|
|
28
|
+
"author": "konamgil",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"bun": ">=1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": ">=18.0.0",
|
|
38
|
+
"react-dom": ">=18.0.0",
|
|
39
|
+
"zod": ">=3.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/bundler/build.ts
CHANGED
|
@@ -220,12 +220,14 @@ export * as ReactDOMClient from 'react-dom/client';
|
|
|
220
220
|
* Island 엔트리 래퍼 생성
|
|
221
221
|
*/
|
|
222
222
|
function generateIslandEntry(routeId: string, clientModulePath: string): string {
|
|
223
|
+
// Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
|
|
224
|
+
const normalizedPath = clientModulePath.replace(/\\/g, "/");
|
|
223
225
|
return `
|
|
224
226
|
/**
|
|
225
227
|
* Mandu Island Entry: ${routeId} (Generated)
|
|
226
228
|
*/
|
|
227
229
|
|
|
228
|
-
import island from "${
|
|
230
|
+
import island from "${normalizedPath}";
|
|
229
231
|
import { registerIsland } from "./_runtime.js";
|
|
230
232
|
|
|
231
233
|
registerIsland("${routeId}", () => island);
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Dev Bundler 🔥
|
|
3
|
+
* 개발 모드 번들링 + HMR (Hot Module Replacement)
|
|
4
|
+
*/
|
|
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";
|
|
11
|
+
|
|
12
|
+
export interface DevBundlerOptions {
|
|
13
|
+
/** 프로젝트 루트 */
|
|
14
|
+
rootDir: string;
|
|
15
|
+
/** 라우트 매니페스트 */
|
|
16
|
+
manifest: RoutesManifest;
|
|
17
|
+
/** 재빌드 콜백 */
|
|
18
|
+
onRebuild?: (result: RebuildResult) => void;
|
|
19
|
+
/** 에러 콜백 */
|
|
20
|
+
onError?: (error: Error, routeId?: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RebuildResult {
|
|
24
|
+
routeId: string;
|
|
25
|
+
success: boolean;
|
|
26
|
+
buildTime: number;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DevBundler {
|
|
31
|
+
/** 초기 빌드 결과 */
|
|
32
|
+
initialBuild: BundleResult;
|
|
33
|
+
/** 파일 감시 중지 */
|
|
34
|
+
close: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 개발 모드 번들러 시작
|
|
39
|
+
* 파일 변경 감시 및 자동 재빌드
|
|
40
|
+
*/
|
|
41
|
+
export async function startDevBundler(options: DevBundlerOptions): Promise<DevBundler> {
|
|
42
|
+
const { rootDir, manifest, onRebuild, onError } = options;
|
|
43
|
+
const slotsDir = path.join(rootDir, "spec", "slots");
|
|
44
|
+
|
|
45
|
+
// 초기 빌드
|
|
46
|
+
console.log("🔨 Initial client bundle build...");
|
|
47
|
+
const initialBuild = await buildClientBundles(manifest, rootDir, {
|
|
48
|
+
minify: false,
|
|
49
|
+
sourcemap: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (initialBuild.success) {
|
|
53
|
+
console.log(`✅ Built ${initialBuild.stats.bundleCount} islands`);
|
|
54
|
+
} else {
|
|
55
|
+
console.error("⚠️ Initial build had errors:", initialBuild.errors);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 파일 감시 설정
|
|
59
|
+
let watcher: fs.FSWatcher | null = null;
|
|
60
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await fs.promises.access(slotsDir);
|
|
64
|
+
|
|
65
|
+
watcher = fs.watch(slotsDir, { recursive: true }, async (event, filename) => {
|
|
66
|
+
if (!filename) return;
|
|
67
|
+
|
|
68
|
+
// .client.ts 파일만 감시
|
|
69
|
+
if (!filename.endsWith(".client.ts")) return;
|
|
70
|
+
|
|
71
|
+
// Debounce - 연속 변경 무시
|
|
72
|
+
if (debounceTimer) {
|
|
73
|
+
clearTimeout(debounceTimer);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
debounceTimer = setTimeout(async () => {
|
|
77
|
+
const routeId = filename.replace(".client.ts", "").replace(/\\/g, "/").split("/").pop();
|
|
78
|
+
if (!routeId) return;
|
|
79
|
+
|
|
80
|
+
const route = manifest.routes.find((r) => r.id === routeId);
|
|
81
|
+
if (!route || !route.clientModule) return;
|
|
82
|
+
|
|
83
|
+
console.log(`\n🔄 Rebuilding: ${routeId}`);
|
|
84
|
+
const startTime = performance.now();
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const result = await buildClientBundles(manifest, rootDir, {
|
|
88
|
+
minify: false,
|
|
89
|
+
sourcemap: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const buildTime = performance.now() - startTime;
|
|
93
|
+
|
|
94
|
+
if (result.success) {
|
|
95
|
+
console.log(`✅ Rebuilt in ${buildTime.toFixed(0)}ms`);
|
|
96
|
+
onRebuild?.({
|
|
97
|
+
routeId,
|
|
98
|
+
success: true,
|
|
99
|
+
buildTime,
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
console.error(`❌ Build failed:`, result.errors);
|
|
103
|
+
onRebuild?.({
|
|
104
|
+
routeId,
|
|
105
|
+
success: false,
|
|
106
|
+
buildTime,
|
|
107
|
+
error: result.errors.join(", "),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
112
|
+
console.error(`❌ Build error:`, err.message);
|
|
113
|
+
onError?.(err, routeId);
|
|
114
|
+
}
|
|
115
|
+
}, 100); // 100ms debounce
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.log("👀 Watching for client slot changes...");
|
|
119
|
+
} catch {
|
|
120
|
+
console.warn(`⚠️ Slots directory not found: ${slotsDir}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
initialBuild,
|
|
125
|
+
close: () => {
|
|
126
|
+
if (debounceTimer) {
|
|
127
|
+
clearTimeout(debounceTimer);
|
|
128
|
+
}
|
|
129
|
+
if (watcher) {
|
|
130
|
+
watcher.close();
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* HMR WebSocket 서버
|
|
138
|
+
*/
|
|
139
|
+
export interface HMRServer {
|
|
140
|
+
/** 연결된 클라이언트 수 */
|
|
141
|
+
clientCount: number;
|
|
142
|
+
/** 모든 클라이언트에게 메시지 전송 */
|
|
143
|
+
broadcast: (message: HMRMessage) => void;
|
|
144
|
+
/** 서버 중지 */
|
|
145
|
+
close: () => void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface HMRMessage {
|
|
149
|
+
type: "connected" | "reload" | "island-update" | "error" | "ping";
|
|
150
|
+
data?: {
|
|
151
|
+
routeId?: string;
|
|
152
|
+
message?: string;
|
|
153
|
+
timestamp?: number;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* HMR WebSocket 서버 생성
|
|
159
|
+
*/
|
|
160
|
+
export function createHMRServer(port: number): HMRServer {
|
|
161
|
+
const clients = new Set<any>();
|
|
162
|
+
const hmrPort = port + 1;
|
|
163
|
+
|
|
164
|
+
const server = Bun.serve({
|
|
165
|
+
port: hmrPort,
|
|
166
|
+
fetch(req, server) {
|
|
167
|
+
// WebSocket 업그레이드
|
|
168
|
+
if (server.upgrade(req)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 일반 HTTP 요청은 상태 반환
|
|
173
|
+
return new Response(
|
|
174
|
+
JSON.stringify({
|
|
175
|
+
status: "ok",
|
|
176
|
+
clients: clients.size,
|
|
177
|
+
port: hmrPort,
|
|
178
|
+
}),
|
|
179
|
+
{
|
|
180
|
+
headers: { "Content-Type": "application/json" },
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
websocket: {
|
|
185
|
+
open(ws) {
|
|
186
|
+
clients.add(ws);
|
|
187
|
+
ws.send(
|
|
188
|
+
JSON.stringify({
|
|
189
|
+
type: "connected",
|
|
190
|
+
data: { timestamp: Date.now() },
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
close(ws) {
|
|
195
|
+
clients.delete(ws);
|
|
196
|
+
},
|
|
197
|
+
message(ws, message) {
|
|
198
|
+
// 클라이언트로부터의 ping 처리
|
|
199
|
+
try {
|
|
200
|
+
const data = JSON.parse(String(message));
|
|
201
|
+
if (data.type === "ping") {
|
|
202
|
+
ws.send(JSON.stringify({ type: "pong", data: { timestamp: Date.now() } }));
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// 무시
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
console.log(`🔥 HMR server running on ws://localhost:${hmrPort}`);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
get clientCount() {
|
|
215
|
+
return clients.size;
|
|
216
|
+
},
|
|
217
|
+
broadcast: (message: HMRMessage) => {
|
|
218
|
+
const payload = JSON.stringify(message);
|
|
219
|
+
for (const client of clients) {
|
|
220
|
+
try {
|
|
221
|
+
client.send(payload);
|
|
222
|
+
} catch {
|
|
223
|
+
clients.delete(client);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
close: () => {
|
|
228
|
+
for (const client of clients) {
|
|
229
|
+
try {
|
|
230
|
+
client.close();
|
|
231
|
+
} catch {
|
|
232
|
+
// 무시
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
clients.clear();
|
|
236
|
+
server.stop();
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* HMR 클라이언트 스크립트 생성
|
|
243
|
+
* 브라우저에서 실행되어 HMR 서버와 연결
|
|
244
|
+
*/
|
|
245
|
+
export function generateHMRClientScript(port: number): string {
|
|
246
|
+
const hmrPort = port + 1;
|
|
247
|
+
|
|
248
|
+
return `
|
|
249
|
+
(function() {
|
|
250
|
+
const HMR_PORT = ${hmrPort};
|
|
251
|
+
let ws = null;
|
|
252
|
+
let reconnectAttempts = 0;
|
|
253
|
+
const maxReconnectAttempts = 10;
|
|
254
|
+
const reconnectDelay = 1000;
|
|
255
|
+
|
|
256
|
+
function connect() {
|
|
257
|
+
try {
|
|
258
|
+
ws = new WebSocket('ws://localhost:' + HMR_PORT);
|
|
259
|
+
|
|
260
|
+
ws.onopen = function() {
|
|
261
|
+
console.log('[Mandu HMR] Connected');
|
|
262
|
+
reconnectAttempts = 0;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
ws.onmessage = function(event) {
|
|
266
|
+
try {
|
|
267
|
+
const message = JSON.parse(event.data);
|
|
268
|
+
handleMessage(message);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.error('[Mandu HMR] Invalid message:', e);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
ws.onclose = function() {
|
|
275
|
+
console.log('[Mandu HMR] Disconnected');
|
|
276
|
+
scheduleReconnect();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
ws.onerror = function(error) {
|
|
280
|
+
console.error('[Mandu HMR] Error:', error);
|
|
281
|
+
};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error('[Mandu HMR] Connection failed:', error);
|
|
284
|
+
scheduleReconnect();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function scheduleReconnect() {
|
|
289
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
290
|
+
reconnectAttempts++;
|
|
291
|
+
console.log('[Mandu HMR] Reconnecting... (' + reconnectAttempts + '/' + maxReconnectAttempts + ')');
|
|
292
|
+
setTimeout(connect, reconnectDelay * reconnectAttempts);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function handleMessage(message) {
|
|
297
|
+
switch (message.type) {
|
|
298
|
+
case 'connected':
|
|
299
|
+
console.log('[Mandu HMR] Ready');
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 'reload':
|
|
303
|
+
console.log('[Mandu HMR] Full reload requested');
|
|
304
|
+
location.reload();
|
|
305
|
+
break;
|
|
306
|
+
|
|
307
|
+
case 'island-update':
|
|
308
|
+
const routeId = message.data?.routeId;
|
|
309
|
+
console.log('[Mandu HMR] Island updated:', routeId);
|
|
310
|
+
|
|
311
|
+
// 현재 페이지의 island인지 확인
|
|
312
|
+
const island = document.querySelector('[data-mandu-island="' + routeId + '"]');
|
|
313
|
+
if (island) {
|
|
314
|
+
console.log('[Mandu HMR] Reloading page for island update');
|
|
315
|
+
location.reload();
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
|
|
319
|
+
case 'error':
|
|
320
|
+
console.error('[Mandu HMR] Build error:', message.data?.message);
|
|
321
|
+
showErrorOverlay(message.data?.message);
|
|
322
|
+
break;
|
|
323
|
+
|
|
324
|
+
case 'pong':
|
|
325
|
+
// 연결 확인
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function showErrorOverlay(message) {
|
|
331
|
+
// 기존 오버레이 제거
|
|
332
|
+
const existing = document.getElementById('mandu-hmr-error');
|
|
333
|
+
if (existing) existing.remove();
|
|
334
|
+
|
|
335
|
+
const overlay = document.createElement('div');
|
|
336
|
+
overlay.id = 'mandu-hmr-error';
|
|
337
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);color:#ff6b6b;font-family:monospace;padding:40px;z-index:99999;overflow:auto;';
|
|
338
|
+
overlay.innerHTML = '<h2 style="color:#ff6b6b;margin:0 0 20px;">🔥 Build Error</h2><pre style="white-space:pre-wrap;word-break:break-all;">' + (message || 'Unknown error') + '</pre><button onclick="this.parentElement.remove()" style="position:fixed;top:20px;right:20px;background:#333;color:#fff;border:none;padding:10px 20px;cursor:pointer;">Close</button>';
|
|
339
|
+
document.body.appendChild(overlay);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 페이지 로드 시 연결
|
|
343
|
+
if (document.readyState === 'loading') {
|
|
344
|
+
document.addEventListener('DOMContentLoaded', connect);
|
|
345
|
+
} else {
|
|
346
|
+
connect();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 페이지 이탈 시 정리
|
|
350
|
+
window.addEventListener('beforeunload', function() {
|
|
351
|
+
if (ws) ws.close();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Ping 전송 (연결 유지)
|
|
355
|
+
setInterval(function() {
|
|
356
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
357
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
358
|
+
}
|
|
359
|
+
}, 30000);
|
|
360
|
+
})();
|
|
361
|
+
`;
|
|
362
|
+
}
|
package/src/bundler/index.ts
CHANGED
package/src/runtime/server.ts
CHANGED
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
export interface ServerOptions {
|
|
15
15
|
port?: number;
|
|
16
16
|
hostname?: string;
|
|
17
|
+
/** 개발 모드 여부 */
|
|
18
|
+
isDev?: boolean;
|
|
19
|
+
/** HMR 포트 (개발 모드에서 사용) */
|
|
20
|
+
hmrPort?: number;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export interface ManduServer {
|
|
@@ -40,6 +44,9 @@ const pageLoaders: Map<string, PageLoader> = new Map();
|
|
|
40
44
|
const routeComponents: Map<string, RouteComponent> = new Map();
|
|
41
45
|
let createAppFn: CreateAppFn | null = null;
|
|
42
46
|
|
|
47
|
+
// Dev mode settings (module-level for handleRequest access)
|
|
48
|
+
let devModeSettings: { isDev: boolean; hmrPort?: number } = { isDev: false };
|
|
49
|
+
|
|
43
50
|
export function registerApiHandler(routeId: string, handler: ApiHandler): void {
|
|
44
51
|
apiHandlers.set(routeId, handler);
|
|
45
52
|
}
|
|
@@ -126,7 +133,11 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
126
133
|
params,
|
|
127
134
|
});
|
|
128
135
|
|
|
129
|
-
return renderSSR(app, {
|
|
136
|
+
return renderSSR(app, {
|
|
137
|
+
title: `${route.id} - Mandu`,
|
|
138
|
+
isDev: devModeSettings.isDev,
|
|
139
|
+
hmrPort: devModeSettings.hmrPort,
|
|
140
|
+
});
|
|
130
141
|
} catch (err) {
|
|
131
142
|
const ssrError = createSSRErrorResponse(
|
|
132
143
|
route.id,
|
|
@@ -159,7 +170,10 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
export function startServer(manifest: RoutesManifest, options: ServerOptions = {}): ManduServer {
|
|
162
|
-
const { port = 3000, hostname = "localhost" } = options;
|
|
173
|
+
const { port = 3000, hostname = "localhost", isDev = false, hmrPort } = options;
|
|
174
|
+
|
|
175
|
+
// Dev mode settings 저장
|
|
176
|
+
devModeSettings = { isDev, hmrPort };
|
|
163
177
|
|
|
164
178
|
const router = new Router(manifest.routes);
|
|
165
179
|
|
|
@@ -169,7 +183,14 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
169
183
|
fetch: (req) => handleRequest(req, router),
|
|
170
184
|
});
|
|
171
185
|
|
|
172
|
-
|
|
186
|
+
if (isDev) {
|
|
187
|
+
console.log(`🥟 Mandu Dev Server running at http://${hostname}:${port}`);
|
|
188
|
+
if (hmrPort) {
|
|
189
|
+
console.log(`🔥 HMR enabled on port ${hmrPort + 1}`);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
console.log(`🥟 Mandu server running at http://${hostname}:${port}`);
|
|
193
|
+
}
|
|
173
194
|
|
|
174
195
|
return {
|
|
175
196
|
server,
|
package/src/runtime/ssr.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface SSROptions {
|
|
|
18
18
|
headTags?: string;
|
|
19
19
|
/** 추가 body 끝 태그 */
|
|
20
20
|
bodyEndTags?: string;
|
|
21
|
+
/** 개발 모드 여부 */
|
|
22
|
+
isDev?: boolean;
|
|
23
|
+
/** HMR 포트 (개발 모드에서 사용) */
|
|
24
|
+
hmrPort?: number;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/**
|
|
@@ -86,6 +90,8 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
|
|
|
86
90
|
routeId,
|
|
87
91
|
headTags = "",
|
|
88
92
|
bodyEndTags = "",
|
|
93
|
+
isDev = false,
|
|
94
|
+
hmrPort,
|
|
89
95
|
} = options;
|
|
90
96
|
|
|
91
97
|
let content = renderToString(element);
|
|
@@ -116,6 +122,12 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
|
|
|
116
122
|
hydrationScripts = generateHydrationScripts(routeId, bundleManifest);
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
// HMR 스크립트 (개발 모드)
|
|
126
|
+
let hmrScript = "";
|
|
127
|
+
if (isDev && hmrPort) {
|
|
128
|
+
hmrScript = generateHMRScript(hmrPort);
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
return `<!doctype html>
|
|
120
132
|
<html lang="${lang}">
|
|
121
133
|
<head>
|
|
@@ -128,11 +140,56 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
|
|
|
128
140
|
<div id="root">${content}</div>
|
|
129
141
|
${dataScript}
|
|
130
142
|
${hydrationScripts}
|
|
143
|
+
${hmrScript}
|
|
131
144
|
${bodyEndTags}
|
|
132
145
|
</body>
|
|
133
146
|
</html>`;
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
/**
|
|
150
|
+
* HMR 스크립트 생성
|
|
151
|
+
*/
|
|
152
|
+
function generateHMRScript(port: number): string {
|
|
153
|
+
const hmrPort = port + 1;
|
|
154
|
+
return `<script>
|
|
155
|
+
(function() {
|
|
156
|
+
var ws = null;
|
|
157
|
+
var reconnectAttempts = 0;
|
|
158
|
+
var maxReconnectAttempts = 10;
|
|
159
|
+
|
|
160
|
+
function connect() {
|
|
161
|
+
try {
|
|
162
|
+
ws = new WebSocket('ws://localhost:${hmrPort}');
|
|
163
|
+
ws.onopen = function() {
|
|
164
|
+
console.log('[Mandu HMR] Connected');
|
|
165
|
+
reconnectAttempts = 0;
|
|
166
|
+
};
|
|
167
|
+
ws.onmessage = function(e) {
|
|
168
|
+
try {
|
|
169
|
+
var msg = JSON.parse(e.data);
|
|
170
|
+
if (msg.type === 'reload' || msg.type === 'island-update') {
|
|
171
|
+
console.log('[Mandu HMR] Reloading...');
|
|
172
|
+
location.reload();
|
|
173
|
+
} else if (msg.type === 'error') {
|
|
174
|
+
console.error('[Mandu HMR] Build error:', msg.data?.message);
|
|
175
|
+
}
|
|
176
|
+
} catch(err) {}
|
|
177
|
+
};
|
|
178
|
+
ws.onclose = function() {
|
|
179
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
180
|
+
reconnectAttempts++;
|
|
181
|
+
setTimeout(connect, 1000 * reconnectAttempts);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
} catch(err) {
|
|
185
|
+
setTimeout(connect, 1000);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
connect();
|
|
189
|
+
})();
|
|
190
|
+
</script>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
136
193
|
export function createHTMLResponse(html: string, status: number = 200): Response {
|
|
137
194
|
return new Response(html, {
|
|
138
195
|
status,
|