@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 CHANGED
@@ -1,41 +1,41 @@
1
- {
2
- "name": "@mandujs/core",
3
- "version": "0.4.0",
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
+ }
@@ -5,3 +5,4 @@
5
5
 
6
6
  export * from "./types";
7
7
  export * from "./build";
8
+ export * from "./dev";
@@ -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, { title: `${route.id} - Mandu` });
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
- console.log(`๐ŸฅŸ Mandu server running at http://${hostname}:${port}`);
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,
@@ -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,