@mandujs/core 0.3.4 โ†’ 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.
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Mandu Bundler Module ๐Ÿ“ฆ
3
+ * Bun.build ๊ธฐ๋ฐ˜ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค๋ง
4
+ */
5
+
6
+ export * from "./types";
7
+ export * from "./build";
8
+ export * from "./dev";
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Mandu Bundler Types
3
+ */
4
+
5
+ /**
6
+ * ๋ฒˆ๋“ค ๋นŒ๋“œ ๊ฒฐ๊ณผ
7
+ */
8
+ export interface BundleResult {
9
+ success: boolean;
10
+ outputs: BundleOutput[];
11
+ errors: string[];
12
+ manifest: BundleManifest;
13
+ stats: BundleStats;
14
+ }
15
+
16
+ /**
17
+ * ๊ฐœ๋ณ„ ๋ฒˆ๋“ค ์ถœ๋ ฅ
18
+ */
19
+ export interface BundleOutput {
20
+ /** ๋ผ์šฐํŠธ ID */
21
+ routeId: string;
22
+ /** ์›๋ณธ ์—”ํŠธ๋ฆฌํฌ์ธํŠธ */
23
+ entrypoint: string;
24
+ /** ์ถœ๋ ฅ ๊ฒฝ๋กœ (์„œ๋ฒ„ ๊ธฐ์ค€) */
25
+ outputPath: string;
26
+ /** ํŒŒ์ผ ํฌ๊ธฐ (bytes) */
27
+ size: number;
28
+ /** gzip ์••์ถ• ํฌ๊ธฐ (bytes) */
29
+ gzipSize: number;
30
+ }
31
+
32
+ /**
33
+ * ๋ฒˆ๋“ค ๋งค๋‹ˆํŽ˜์ŠคํŠธ
34
+ */
35
+ export interface BundleManifest {
36
+ /** ๋งค๋‹ˆํŽ˜์ŠคํŠธ ๋ฒ„์ „ */
37
+ version: number;
38
+ /** ๋นŒ๋“œ ์‹œ๊ฐ„ */
39
+ buildTime: string;
40
+ /** ํ™˜๊ฒฝ */
41
+ env: "development" | "production";
42
+ /** ๋ผ์šฐํŠธ๋ณ„ ๋ฒˆ๋“ค ์ •๋ณด */
43
+ bundles: Record<
44
+ string,
45
+ {
46
+ /** JavaScript ๋ฒˆ๋“ค ๊ฒฝ๋กœ */
47
+ js: string;
48
+ /** CSS ๋ฒˆ๋“ค ๊ฒฝ๋กœ (์žˆ๋Š” ๊ฒฝ์šฐ) */
49
+ css?: string;
50
+ /** ์˜์กดํ•˜๋Š” ๊ณต์œ  ์ฒญํฌ */
51
+ dependencies: string[];
52
+ /** Hydration ์šฐ์„ ์ˆœ์œ„ */
53
+ priority: "immediate" | "visible" | "idle" | "interaction";
54
+ }
55
+ >;
56
+ /** ๊ณต์œ  ์ฒญํฌ */
57
+ shared: {
58
+ /** Hydration ๋Ÿฐํƒ€์ž„ */
59
+ runtime: string;
60
+ /** ๋ฒค๋” ๋ฒˆ๋“ค (React ๋“ฑ) */
61
+ vendor: string;
62
+ };
63
+ }
64
+
65
+ /**
66
+ * ๋ฒˆ๋“ค ํ†ต๊ณ„
67
+ */
68
+ export interface BundleStats {
69
+ /** ์ „์ฒด ํฌ๊ธฐ */
70
+ totalSize: number;
71
+ /** ์ „์ฒด gzip ํฌ๊ธฐ */
72
+ totalGzipSize: number;
73
+ /** ๊ฐ€์žฅ ํฐ ๋ฒˆ๋“ค */
74
+ largestBundle: {
75
+ routeId: string;
76
+ size: number;
77
+ };
78
+ /** ๋นŒ๋“œ ์‹œ๊ฐ„ (ms) */
79
+ buildTime: number;
80
+ /** ๋ฒˆ๋“ค ์ˆ˜ */
81
+ bundleCount: number;
82
+ }
83
+
84
+ /**
85
+ * ๋ฒˆ๋“ค๋Ÿฌ ์˜ต์…˜
86
+ */
87
+ export interface BundlerOptions {
88
+ /** ์ฝ”๋“œ ์••์ถ• ์—ฌ๋ถ€ (๊ธฐ๋ณธ: production์—์„œ true) */
89
+ minify?: boolean;
90
+ /** ์†Œ์Šค๋งต ์ƒ์„ฑ ์—ฌ๋ถ€ */
91
+ sourcemap?: boolean;
92
+ /** ํŒŒ์ผ ๊ฐ์‹œ ๋ชจ๋“œ */
93
+ watch?: boolean;
94
+ /** ์ถœ๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ (๊ธฐ๋ณธ: .mandu/client) */
95
+ outDir?: string;
96
+ /** ์™ธ๋ถ€ ๋ชจ๋“ˆ (๋ฒˆ๋“ค์—์„œ ์ œ์™ธ) */
97
+ external?: string[];
98
+ /** ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ฃผ์ž… */
99
+ define?: Record<string, string>;
100
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Mandu Client Module ๐Ÿ๏ธ
3
+ * ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ hydration์„ ์œ„ํ•œ API
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * // spec/slots/todos.client.ts
8
+ * import { Mandu } from "@mandujs/core/client";
9
+ *
10
+ * export default Mandu.island<TodosData>({
11
+ * setup: (data) => { ... },
12
+ * render: (props) => <TodoList {...props} />
13
+ * });
14
+ * ```
15
+ */
16
+
17
+ // Island API
18
+ export {
19
+ island,
20
+ useServerData,
21
+ useHydrated,
22
+ useIslandEvent,
23
+ fetchApi,
24
+ type IslandDefinition,
25
+ type IslandMetadata,
26
+ type CompiledIsland,
27
+ type FetchOptions,
28
+ } from "./island";
29
+
30
+ // Runtime API
31
+ export {
32
+ registerIsland,
33
+ getRegisteredIslands,
34
+ getServerData,
35
+ hydrateIslands,
36
+ getHydrationState,
37
+ unmountIsland,
38
+ unmountAllIslands,
39
+ initializeRuntime,
40
+ type IslandLoader,
41
+ } from "./runtime";
42
+
43
+ // Re-export as Mandu namespace for consistent API
44
+ import { island } from "./island";
45
+ import { hydrateIslands, initializeRuntime } from "./runtime";
46
+
47
+ /**
48
+ * Mandu Client namespace
49
+ */
50
+ export const Mandu = {
51
+ /**
52
+ * Create an island component
53
+ * @see island
54
+ */
55
+ island,
56
+
57
+ /**
58
+ * Hydrate all islands on the page
59
+ * @see hydrateIslands
60
+ */
61
+ hydrate: hydrateIslands,
62
+
63
+ /**
64
+ * Initialize the hydration runtime
65
+ * @see initializeRuntime
66
+ */
67
+ init: initializeRuntime,
68
+ };