@mandujs/core 0.18.17 → 0.18.19
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 +1 -1
- package/src/bundler/dev.ts +47 -2
- package/src/devtools/client/components/kitchen-root.tsx +42 -0
- package/src/devtools/client/components/mandu-character.tsx +9 -6
- package/src/devtools/client/components/panel/panel-container.tsx +52 -12
- package/src/devtools/design-tokens.ts +1 -0
- package/src/router/fs-scanner.ts +549 -512
- package/src/router/fs-types.ts +1 -1
- package/src/runtime/ssr.ts +1 -0
- package/src/runtime/streaming-ssr.ts +1 -0
package/package.json
CHANGED
package/src/bundler/dev.ts
CHANGED
|
@@ -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;
|
|
@@ -244,6 +244,47 @@ function KitchenApp({ config }: KitchenAppProps): React.ReactElement | null {
|
|
|
244
244
|
getStateManager().clearGuardViolations();
|
|
245
245
|
}, []);
|
|
246
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
|
+
|
|
247
288
|
// Calculate error count
|
|
248
289
|
const errorCount = useMemo(() => {
|
|
249
290
|
return state.errors.filter(
|
|
@@ -315,6 +356,7 @@ function KitchenApp({ config }: KitchenAppProps): React.ReactElement | null {
|
|
|
315
356
|
activeTab={state.activeTab}
|
|
316
357
|
onTabChange={handleTabChange}
|
|
317
358
|
onClose={handlePanelClose}
|
|
359
|
+
onRestart={handleRestart}
|
|
318
360
|
position={position}
|
|
319
361
|
>
|
|
320
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
|
|
250
|
-
fontFamily:
|
|
251
|
-
color: colors.
|
|
252
|
-
fontSize: '
|
|
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={
|
|
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
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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 */}
|