@mandujs/core 0.4.0 โ 0.4.1
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/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.1",
|
|
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
|
+
}
|
|
@@ -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,
|