@mandujs/core 0.18.16 → 0.18.18

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.18.16",
3
+ "version": "0.18.18",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -347,6 +347,8 @@ export interface HMRServer {
347
347
  broadcast: (message: HMRMessage) => void;
348
348
  /** 서버 중지 */
349
349
  close: () => void;
350
+ /** 재시작 핸들러 등록 */
351
+ setRestartHandler: (handler: () => Promise<void>) => void;
350
352
  }
351
353
 
352
354
  export interface HMRMessage {
@@ -368,10 +370,49 @@ export interface HMRMessage {
368
370
  export function createHMRServer(port: number): HMRServer {
369
371
  const clients = new Set<any>();
370
372
  const hmrPort = port + PORTS.HMR_OFFSET;
373
+ let restartHandler: (() => Promise<void>) | null = null;
374
+
375
+ const corsHeaders: Record<string, string> = {
376
+ "Access-Control-Allow-Origin": `http://localhost:${port}`,
377
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
378
+ "Access-Control-Allow-Headers": "Content-Type",
379
+ };
371
380
 
372
381
  const server = Bun.serve({
373
382
  port: hmrPort,
374
- fetch(req, server) {
383
+ async fetch(req, server) {
384
+ const url = new URL(req.url);
385
+
386
+ // CORS preflight
387
+ if (req.method === "OPTIONS") {
388
+ return new Response(null, { status: 204, headers: corsHeaders });
389
+ }
390
+
391
+ // POST /restart → 재시작 핸들러 호출
392
+ if (req.method === "POST" && url.pathname === "/restart") {
393
+ if (!restartHandler) {
394
+ return new Response(
395
+ JSON.stringify({ error: "No restart handler registered" }),
396
+ { status: 503, headers: { ...corsHeaders, "Content-Type": "application/json" } }
397
+ );
398
+ }
399
+ try {
400
+ console.log("🔄 Full restart requested from DevTools");
401
+ await restartHandler();
402
+ return new Response(
403
+ JSON.stringify({ status: "restarted" }),
404
+ { headers: { ...corsHeaders, "Content-Type": "application/json" } }
405
+ );
406
+ } catch (err) {
407
+ const message = err instanceof Error ? err.message : String(err);
408
+ console.error("❌ Restart failed:", message);
409
+ return new Response(
410
+ JSON.stringify({ error: message }),
411
+ { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
412
+ );
413
+ }
414
+ }
415
+
375
416
  // WebSocket 업그레이드
376
417
  if (server.upgrade(req)) {
377
418
  return;
@@ -385,7 +426,7 @@ export function createHMRServer(port: number): HMRServer {
385
426
  port: hmrPort,
386
427
  }),
387
428
  {
388
- headers: { "Content-Type": "application/json" },
429
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
389
430
  }
390
431
  );
391
432
  },
@@ -443,6 +484,9 @@ export function createHMRServer(port: number): HMRServer {
443
484
  clients.clear();
444
485
  server.stop();
445
486
  },
487
+ setRestartHandler: (handler: () => Promise<void>) => {
488
+ restartHandler = handler;
489
+ },
446
490
  };
447
491
  }
448
492
 
@@ -455,6 +499,7 @@ export function generateHMRClientScript(port: number): string {
455
499
 
456
500
  return `
457
501
  (function() {
502
+ window.__MANDU_HMR_PORT__ = ${hmrPort};
458
503
  const HMR_PORT = ${hmrPort};
459
504
  let ws = null;
460
505
  let reconnectAttempts = 0;
@@ -56,6 +56,37 @@ const baseStyles = `
56
56
  margin: 0;
57
57
  }
58
58
 
59
+ * {
60
+ scrollbar-width: thin;
61
+ scrollbar-color: rgba(255, 255, 255, 0.12) transparent;
62
+ }
63
+
64
+ *:hover {
65
+ scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
66
+ }
67
+
68
+ *::-webkit-scrollbar {
69
+ width: 6px;
70
+ height: 6px;
71
+ }
72
+
73
+ *::-webkit-scrollbar-track {
74
+ background: transparent;
75
+ }
76
+
77
+ *::-webkit-scrollbar-thumb {
78
+ background: rgba(255, 255, 255, 0.12);
79
+ border-radius: 3px;
80
+ }
81
+
82
+ *::-webkit-scrollbar-thumb:hover {
83
+ background: rgba(255, 255, 255, 0.25);
84
+ }
85
+
86
+ *::-webkit-scrollbar-corner {
87
+ background: transparent;
88
+ }
89
+
59
90
  .mk-badge-container {
60
91
  position: fixed;
61
92
  z-index: ${zIndex.devtools};
@@ -213,6 +244,47 @@ function KitchenApp({ config }: KitchenAppProps): React.ReactElement | null {
213
244
  getStateManager().clearGuardViolations();
214
245
  }, []);
215
246
 
247
+ const handleRestart = useCallback(async () => {
248
+ try {
249
+ // 1. Service Worker 해제
250
+ if ('serviceWorker' in navigator) {
251
+ const registrations = await navigator.serviceWorker.getRegistrations();
252
+ for (const reg of registrations) {
253
+ await reg.unregister();
254
+ }
255
+ }
256
+
257
+ // 2. Cache API 클리어
258
+ if ('caches' in window) {
259
+ const cacheNames = await caches.keys();
260
+ for (const name of cacheNames) {
261
+ await caches.delete(name);
262
+ }
263
+ }
264
+
265
+ // 3. window.__MANDU_* globals 삭제
266
+ for (const key of Object.keys(window)) {
267
+ if (key.startsWith('__MANDU_')) {
268
+ delete (window as any)[key];
269
+ }
270
+ }
271
+
272
+ // 4. HMR 서버에 POST /restart
273
+ const hmrPort = (window as any).__MANDU_HMR_PORT__;
274
+ if (hmrPort) {
275
+ await fetch(`http://localhost:${hmrPort}/restart`, { method: 'POST' });
276
+ }
277
+
278
+ // 5. 3초 fallback reload (서버가 reload 브로드캐스트를 못 보낸 경우)
279
+ setTimeout(() => {
280
+ location.reload();
281
+ }, 3000);
282
+ } catch (err) {
283
+ console.error('[Mandu Kitchen] Restart failed:', err);
284
+ location.reload();
285
+ }
286
+ }, []);
287
+
216
288
  // Calculate error count
217
289
  const errorCount = useMemo(() => {
218
290
  return state.errors.filter(
@@ -284,6 +356,7 @@ function KitchenApp({ config }: KitchenAppProps): React.ReactElement | null {
284
356
  activeTab={state.activeTab}
285
357
  onTabChange={handleTabChange}
286
358
  onClose={handlePanelClose}
359
+ onRestart={handleRestart}
287
360
  position={position}
288
361
  >
289
362
  {renderPanelContent()}
@@ -6,7 +6,7 @@
6
6
  import React from 'react';
7
7
  import type { ManduState } from '../../types';
8
8
  import { MANDU_CHARACTERS } from '../../types';
9
- import { colors, animation, testIds } from '../../design-tokens';
9
+ import { colors, typography, animation, testIds } from '../../design-tokens';
10
10
 
11
11
  // ============================================================================
12
12
  // Styles
@@ -246,13 +246,16 @@ export function ManduBadge({
246
246
  : 'none',
247
247
  };
248
248
 
249
- const emojiStyle: React.CSSProperties = {
250
- fontFamily: "'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif",
251
- color: colors.text.primary,
252
- fontSize: '24px',
249
+ const textStyle: React.CSSProperties = {
250
+ fontFamily: typography.fontFamily.sans,
251
+ color: colors.brand.accent,
252
+ fontSize: '14px',
253
+ fontWeight: typography.fontWeight.bold,
253
254
  lineHeight: 1,
255
+ letterSpacing: '0.5px',
254
256
  transition: `transform 200ms ${animation.easing.spring}`,
255
257
  transform: isHovered ? 'rotate(-8deg)' : 'rotate(0deg)',
258
+ userSelect: 'none',
256
259
  };
257
260
 
258
261
  const countBubbleStyle: React.CSSProperties = {
@@ -290,7 +293,7 @@ export function ManduBadge({
290
293
  onBlur={() => { setIsHovered(false); setIsPressed(false); }}
291
294
  aria-label={`Mandu Kitchen: ${character.message}${count > 0 ? `, ${count} issues` : ''}`}
292
295
  >
293
- <span aria-hidden="true" style={emojiStyle}>🥟</span>
296
+ <span aria-hidden="true" style={textStyle}>MK</span>
294
297
  {count > 0 && (
295
298
  <span style={countBubbleStyle}>
296
299
  {count > 99 ? '99+' : count}
@@ -25,6 +25,7 @@ export interface PanelContainerProps {
25
25
  activeTab: TabId;
26
26
  onTabChange: (tab: TabId) => void;
27
27
  onClose: () => void;
28
+ onRestart?: () => void;
28
29
  position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
29
30
  children: React.ReactNode;
30
31
  }
@@ -78,6 +79,22 @@ const styles = {
78
79
  logo: {
79
80
  fontSize: typography.fontSize.lg,
80
81
  },
82
+ headerActions: {
83
+ display: 'flex',
84
+ alignItems: 'center',
85
+ gap: spacing.xs,
86
+ },
87
+ restartButton: {
88
+ background: 'transparent',
89
+ border: 'none',
90
+ color: colors.text.secondary,
91
+ fontSize: typography.fontSize.md,
92
+ cursor: 'pointer',
93
+ padding: spacing.xs,
94
+ borderRadius: borderRadius.sm,
95
+ lineHeight: 1,
96
+ transition: `all ${animation.duration.fast}`,
97
+ },
81
98
  closeButton: {
82
99
  background: 'transparent',
83
100
  border: 'none',
@@ -161,6 +178,7 @@ export function PanelContainer({
161
178
  activeTab,
162
179
  onTabChange,
163
180
  onClose,
181
+ onRestart,
164
182
  position,
165
183
  children,
166
184
  }: PanelContainerProps): React.ReactElement {
@@ -168,6 +186,7 @@ export function PanelContainer({
168
186
  const [height, setHeight] = useState(400);
169
187
  const [hoveredTab, setHoveredTab] = useState<TabId | null>(null);
170
188
  const [isCloseHovered, setIsCloseHovered] = useState(false);
189
+ const [isRestartHovered, setIsRestartHovered] = useState(false);
171
190
 
172
191
  const getTabBadgeCount = useCallback((tabId: TabId): number => {
173
192
  switch (tabId) {
@@ -207,18 +226,39 @@ export function PanelContainer({
207
226
  <span style={styles.logo}>🥟</span>
208
227
  <span>Mandu Kitchen</span>
209
228
  </div>
210
- <button
211
- style={{
212
- ...styles.closeButton,
213
- ...(isCloseHovered ? { color: colors.text.primary, backgroundColor: 'rgba(255, 255, 255, 0.08)' } : {}),
214
- }}
215
- onClick={onClose}
216
- onMouseEnter={() => setIsCloseHovered(true)}
217
- onMouseLeave={() => setIsCloseHovered(false)}
218
- aria-label="패널 닫기"
219
- >
220
- ×
221
- </button>
229
+ <div style={styles.headerActions}>
230
+ {onRestart && (
231
+ <button
232
+ data-testid={testIds.restartButton}
233
+ style={{
234
+ ...styles.restartButton,
235
+ ...(isRestartHovered ? {
236
+ color: colors.semantic.warning,
237
+ backgroundColor: `${colors.semantic.warning}18`,
238
+ } : {}),
239
+ }}
240
+ onClick={onRestart}
241
+ onMouseEnter={() => setIsRestartHovered(true)}
242
+ onMouseLeave={() => setIsRestartHovered(false)}
243
+ aria-label="캐시 지우고 완전 재시작"
244
+ title="캐시 지우고 완전 재시작"
245
+ >
246
+ 🔄
247
+ </button>
248
+ )}
249
+ <button
250
+ style={{
251
+ ...styles.closeButton,
252
+ ...(isCloseHovered ? { color: colors.text.primary, backgroundColor: 'rgba(255, 255, 255, 0.08)' } : {}),
253
+ }}
254
+ onClick={onClose}
255
+ onMouseEnter={() => setIsCloseHovered(true)}
256
+ onMouseLeave={() => setIsCloseHovered(false)}
257
+ aria-label="패널 닫기"
258
+ >
259
+ ×
260
+ </button>
261
+ </div>
222
262
  </div>
223
263
 
224
264
  {/* Tabs */}
@@ -258,6 +258,7 @@ export const testIds = {
258
258
  tabGuard: 'mk-tab-guard',
259
259
  errorList: 'mk-error-list',
260
260
  mandu: 'mk-mandu',
261
+ restartButton: 'mk-restart-button',
261
262
  } as const;
262
263
 
263
264
  export type TestId = (typeof testIds)[keyof typeof testIds];
@@ -316,6 +316,7 @@ function generateClientRouterScript(manifest: BundleManifest): string {
316
316
  function generateHMRScript(port: number): string {
317
317
  const hmrPort = port + PORTS.HMR_OFFSET;
318
318
  return `<script>
319
+ window.__MANDU_HMR_PORT__ = ${hmrPort};
319
320
  (function() {
320
321
  var ws = null;
321
322
  var reconnectAttempts = 0;
@@ -587,6 +587,7 @@ function generateDeferredDataScript(routeId: string, key: string, data: unknown)
587
587
  function generateHMRScript(port: number): string {
588
588
  const hmrPort = port + PORTS.HMR_OFFSET;
589
589
  return `<script>
590
+ window.__MANDU_HMR_PORT__ = ${hmrPort};
590
591
  (function() {
591
592
  var ws = null;
592
593
  var reconnectAttempts = 0;