@mandujs/core 0.18.22 → 0.19.2

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.
Files changed (91) hide show
  1. package/README.ko.md +0 -14
  2. package/package.json +4 -1
  3. package/src/brain/architecture/analyzer.ts +4 -4
  4. package/src/brain/doctor/analyzer.ts +18 -14
  5. package/src/bundler/build.test.ts +127 -0
  6. package/src/bundler/build.ts +291 -113
  7. package/src/bundler/css.ts +20 -5
  8. package/src/bundler/dev.ts +55 -2
  9. package/src/bundler/prerender.ts +195 -0
  10. package/src/change/snapshot.ts +4 -23
  11. package/src/change/types.ts +2 -3
  12. package/src/client/Form.tsx +105 -0
  13. package/src/client/__tests__/use-sse.test.ts +153 -0
  14. package/src/client/hooks.ts +105 -6
  15. package/src/client/index.ts +35 -6
  16. package/src/client/router.ts +670 -433
  17. package/src/client/rpc.ts +140 -0
  18. package/src/client/runtime.ts +24 -21
  19. package/src/client/use-fetch.ts +239 -0
  20. package/src/client/use-head.ts +197 -0
  21. package/src/client/use-sse.ts +378 -0
  22. package/src/components/Image.tsx +162 -0
  23. package/src/config/mandu.ts +5 -0
  24. package/src/config/validate.ts +34 -0
  25. package/src/content/index.ts +5 -1
  26. package/src/devtools/client/catchers/error-catcher.ts +17 -0
  27. package/src/devtools/client/catchers/network-proxy.ts +390 -367
  28. package/src/devtools/client/components/kitchen-root.tsx +479 -467
  29. package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
  30. package/src/devtools/client/components/panel/guard-panel.tsx +374 -244
  31. package/src/devtools/client/components/panel/index.ts +45 -32
  32. package/src/devtools/client/components/panel/panel-container.tsx +332 -312
  33. package/src/devtools/client/components/panel/preview-panel.tsx +188 -0
  34. package/src/devtools/client/state-manager.ts +535 -478
  35. package/src/devtools/design-tokens.ts +265 -264
  36. package/src/devtools/types.ts +345 -319
  37. package/src/filling/context.ts +65 -0
  38. package/src/filling/filling.ts +336 -14
  39. package/src/filling/index.ts +5 -1
  40. package/src/filling/session.ts +216 -0
  41. package/src/filling/ws.ts +78 -0
  42. package/src/generator/generate.ts +2 -2
  43. package/src/guard/auto-correct.ts +0 -29
  44. package/src/guard/check.ts +14 -31
  45. package/src/guard/presets/index.ts +296 -294
  46. package/src/guard/rules.ts +15 -19
  47. package/src/guard/validator.ts +834 -834
  48. package/src/index.ts +5 -1
  49. package/src/island/index.ts +373 -304
  50. package/src/kitchen/api/contract-api.ts +225 -0
  51. package/src/kitchen/api/diff-parser.ts +108 -0
  52. package/src/kitchen/api/file-api.ts +273 -0
  53. package/src/kitchen/api/guard-api.ts +83 -0
  54. package/src/kitchen/api/guard-decisions.ts +100 -0
  55. package/src/kitchen/api/routes-api.ts +50 -0
  56. package/src/kitchen/index.ts +21 -0
  57. package/src/kitchen/kitchen-handler.ts +256 -0
  58. package/src/kitchen/kitchen-ui.ts +1732 -0
  59. package/src/kitchen/stream/activity-sse.ts +145 -0
  60. package/src/kitchen/stream/file-tailer.ts +99 -0
  61. package/src/middleware/compress.ts +62 -0
  62. package/src/middleware/cors.ts +47 -0
  63. package/src/middleware/index.ts +10 -0
  64. package/src/middleware/jwt.ts +134 -0
  65. package/src/middleware/logger.ts +58 -0
  66. package/src/middleware/timeout.ts +55 -0
  67. package/src/paths.ts +0 -4
  68. package/src/plugins/hooks.ts +64 -0
  69. package/src/plugins/index.ts +3 -0
  70. package/src/plugins/types.ts +5 -0
  71. package/src/report/build.ts +0 -6
  72. package/src/resource/__tests__/backward-compat.test.ts +0 -1
  73. package/src/router/fs-patterns.ts +11 -1
  74. package/src/router/fs-routes.ts +78 -14
  75. package/src/router/fs-scanner.ts +2 -2
  76. package/src/router/fs-types.ts +2 -1
  77. package/src/runtime/adapter-bun.ts +62 -0
  78. package/src/runtime/adapter.ts +47 -0
  79. package/src/runtime/cache.ts +310 -0
  80. package/src/runtime/handler.ts +65 -0
  81. package/src/runtime/image-handler.ts +195 -0
  82. package/src/runtime/index.ts +12 -0
  83. package/src/runtime/middleware.ts +263 -0
  84. package/src/runtime/server.ts +686 -92
  85. package/src/runtime/ssr.ts +55 -29
  86. package/src/runtime/streaming-ssr.ts +106 -82
  87. package/src/spec/index.ts +0 -1
  88. package/src/spec/schema.ts +1 -0
  89. package/src/testing/index.ts +144 -0
  90. package/src/watcher/watcher.ts +27 -1
  91. package/src/spec/lock.ts +0 -56
@@ -1,467 +1,479 @@
1
- /**
2
- * Mandu Kitchen DevTools - Root Component
3
- * @version 1.0.3
4
- *
5
- * Shadow DOM을 사용하여 앱의 CSS와 격리된 DevTools 루트
6
- */
7
-
8
- import React, { useEffect, useState, useCallback, useMemo } from 'react';
9
- import { createRoot, type Root } from 'react-dom/client';
10
- import type { NormalizedError, DevToolsConfig, IslandSnapshot, NetworkRequest, DevToolsGuardViolation } from '../../types';
11
- import { generateCSSVariables, testIds, zIndex } from '../../design-tokens';
12
- import { getStateManager, type KitchenState } from '../state-manager';
13
- import { getOrCreateHook } from '../../hook';
14
- import { ErrorOverlay } from './overlay';
15
- import { ManduBadge } from './mandu-character';
16
- import {
17
- PanelContainer,
18
- ErrorsPanel,
19
- IslandsPanel,
20
- NetworkPanel,
21
- GuardPanel,
22
- type TabId,
23
- } from './panel';
24
-
25
- // ============================================================================
26
- // Base Styles
27
- // ============================================================================
28
-
29
- const baseStyles = `
30
- ${generateCSSVariables()}
31
-
32
- * {
33
- box-sizing: border-box;
34
- margin: 0;
35
- padding: 0;
36
- }
37
-
38
- :host {
39
- all: initial;
40
- font-family: var(--mk-font-sans);
41
- color: var(--mk-color-text-primary);
42
- font-size: var(--mk-font-size-md);
43
- line-height: 1.5;
44
- }
45
-
46
- button {
47
- background: none;
48
- border: none;
49
- cursor: pointer;
50
- font-family: inherit;
51
- color: inherit;
52
- font-size: inherit;
53
- -webkit-appearance: none;
54
- appearance: none;
55
- padding: 0;
56
- margin: 0;
57
- }
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
-
90
- .mk-badge-container {
91
- position: fixed;
92
- z-index: ${zIndex.devtools};
93
- transition: all 0.3s ease;
94
- }
95
-
96
- .mk-badge-container.bottom-right {
97
- bottom: 16px;
98
- right: 16px;
99
- }
100
-
101
- .mk-badge-container.bottom-left {
102
- bottom: 16px;
103
- left: 16px;
104
- }
105
-
106
- .mk-badge-container.top-right {
107
- top: 16px;
108
- right: 16px;
109
- }
110
-
111
- .mk-badge-container.top-left {
112
- top: 16px;
113
- left: 16px;
114
- }
115
-
116
- .mk-badge-container.panel-open {
117
- opacity: 0;
118
- pointer-events: none;
119
- transform: scale(0.8);
120
- }
121
-
122
- @keyframes mk-badge-breathe {
123
- 0%, 100% { transform: scale(1) translateY(0px); }
124
- 50% { transform: scale(1.04) translateY(0px); }
125
- }
126
-
127
- @keyframes mk-badge-attention {
128
- 0%, 100% { transform: scale(1) translateY(0px); }
129
- 15% { transform: scale(1.08) translateY(0px); }
130
- 30% { transform: scale(0.98) translateY(0px); }
131
- 45% { transform: scale(1.04) translateY(0px); }
132
- 60% { transform: scale(1) translateY(0px); }
133
- }
134
-
135
- @keyframes mk-badge-float {
136
- 0%, 100% { transform: scale(1) translateY(0px); }
137
- 50% { transform: scale(1) translateY(-3px); }
138
- }
139
- `;
140
-
141
- // ============================================================================
142
- // Kitchen App Component
143
- // ============================================================================
144
-
145
- interface KitchenAppProps {
146
- config: DevToolsConfig;
147
- }
148
-
149
- function KitchenApp({ config }: KitchenAppProps): React.ReactElement | null {
150
- const [state, setState] = useState<KitchenState>(() => getStateManager().getState());
151
-
152
- // Subscribe to state changes
153
- useEffect(() => {
154
- const stateManager = getStateManager();
155
- const unsubscribe = stateManager.subscribe((newState) => {
156
- setState(newState);
157
- });
158
-
159
- // Connect to hook
160
- const hook = getOrCreateHook();
161
- hook.connect((event) => {
162
- stateManager.handleEvent(event);
163
- });
164
-
165
- return () => {
166
- unsubscribe();
167
- hook.disconnect();
168
- };
169
- }, []);
170
-
171
- // Keyboard shortcuts
172
- useEffect(() => {
173
- const handleKeyDown = (e: KeyboardEvent) => {
174
- // Ctrl+Shift+M: Toggle panel
175
- if (e.ctrlKey && e.shiftKey && e.key === 'M') {
176
- e.preventDefault();
177
- getStateManager().toggle();
178
- }
179
- // Ctrl+Shift+E: Open errors tab
180
- if (e.ctrlKey && e.shiftKey && e.key === 'E') {
181
- e.preventDefault();
182
- getStateManager().setActiveTab('errors');
183
- getStateManager().open();
184
- }
185
- };
186
-
187
- window.addEventListener('keydown', handleKeyDown);
188
- return () => window.removeEventListener('keydown', handleKeyDown);
189
- }, []);
190
-
191
- // Handlers
192
- const handleOverlayClose = useCallback(() => {
193
- getStateManager().hideOverlay();
194
- }, []);
195
-
196
- const handleOverlayIgnore = useCallback(() => {
197
- if (state.overlayError) {
198
- getStateManager().ignoreError(state.overlayError.id);
199
- }
200
- }, [state.overlayError]);
201
-
202
- const handleOverlayCopy = useCallback(async () => {
203
- if (!state.overlayError) return;
204
-
205
- const errorInfo = formatErrorForCopy(state.overlayError);
206
- try {
207
- await navigator.clipboard.writeText(errorInfo);
208
- } catch {
209
- // Fallback for older browsers
210
- const textarea = document.createElement('textarea');
211
- textarea.value = errorInfo;
212
- document.body.appendChild(textarea);
213
- textarea.select();
214
- document.execCommand('copy');
215
- document.body.removeChild(textarea);
216
- }
217
- }, [state.overlayError]);
218
-
219
- const handleBadgeClick = useCallback(() => {
220
- getStateManager().toggle();
221
- }, []);
222
-
223
- const handlePanelClose = useCallback(() => {
224
- getStateManager().close();
225
- }, []);
226
-
227
- const handleTabChange = useCallback((tab: TabId) => {
228
- getStateManager().setActiveTab(tab);
229
- }, []);
230
-
231
- const handleErrorClick = useCallback((error: NormalizedError) => {
232
- getStateManager().showOverlay(error);
233
- }, []);
234
-
235
- const handleErrorIgnore = useCallback((id: string) => {
236
- getStateManager().ignoreError(id);
237
- }, []);
238
-
239
- const handleClearErrors = useCallback(() => {
240
- getStateManager().clearErrors();
241
- }, []);
242
-
243
- const handleClearGuard = useCallback(() => {
244
- getStateManager().clearDevToolsGuardViolations();
245
- }, []);
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
-
288
- // Calculate error count
289
- const errorCount = useMemo(() => {
290
- return state.errors.filter(
291
- (e) => e.severity === 'error' || e.severity === 'critical'
292
- ).length;
293
- }, [state.errors]);
294
-
295
- // Convert Maps to Arrays for panels
296
- const islandsArray = useMemo(
297
- () => Array.from(state.islands.values()),
298
- [state.islands]
299
- );
300
-
301
- const networkArray = useMemo(
302
- () => Array.from(state.networkRequests.values()),
303
- [state.networkRequests]
304
- );
305
-
306
- const position = config.position ?? 'bottom-right';
307
-
308
- // Don't render if disabled
309
- if (config.enabled === false) {
310
- return null;
311
- }
312
-
313
- // Render active panel content
314
- const renderPanelContent = () => {
315
- switch (state.activeTab) {
316
- case 'errors':
317
- return (
318
- <ErrorsPanel
319
- errors={state.errors}
320
- onErrorClick={handleErrorClick}
321
- onErrorIgnore={handleErrorIgnore}
322
- onClearAll={handleClearErrors}
323
- />
324
- );
325
- case 'islands':
326
- return <IslandsPanel islands={islandsArray} />;
327
- case 'network':
328
- return <NetworkPanel requests={networkArray} />;
329
- case 'guard':
330
- return (
331
- <GuardPanel
332
- violations={state.guardViolations}
333
- onClear={handleClearGuard}
334
- />
335
- );
336
- default:
337
- return null;
338
- }
339
- };
340
-
341
- return (
342
- <>
343
- {/* Badge (hidden when panel is open) */}
344
- <div className={`mk-badge-container ${position}${state.isOpen ? ' panel-open' : ''}`}>
345
- <ManduBadge
346
- state={state.manduState}
347
- count={errorCount}
348
- onClick={handleBadgeClick}
349
- />
350
- </div>
351
-
352
- {/* Panel */}
353
- {state.isOpen && (
354
- <PanelContainer
355
- state={state}
356
- activeTab={state.activeTab}
357
- onTabChange={handleTabChange}
358
- onClose={handlePanelClose}
359
- onRestart={handleRestart}
360
- position={position}
361
- >
362
- {renderPanelContent()}
363
- </PanelContainer>
364
- )}
365
-
366
- {/* Overlay */}
367
- {state.overlayError && config.features?.errorOverlay !== false && (
368
- <ErrorOverlay
369
- error={state.overlayError}
370
- onClose={handleOverlayClose}
371
- onIgnore={handleOverlayIgnore}
372
- onCopy={handleOverlayCopy}
373
- />
374
- )}
375
- </>
376
- );
377
- }
378
-
379
- // ============================================================================
380
- // Helper Functions
381
- // ============================================================================
382
-
383
- function formatErrorForCopy(error: NormalizedError): string {
384
- const lines = [
385
- `[${error.severity.toUpperCase()}] ${error.type}`,
386
- `Message: ${error.message}`,
387
- `Time: ${new Date(error.timestamp).toISOString()}`,
388
- `URL: ${error.url}`,
389
- ];
390
-
391
- if (error.source) {
392
- lines.push(`Source: ${error.source}:${error.line ?? '?'}:${error.column ?? '?'}`);
393
- }
394
-
395
- if (error.stack) {
396
- lines.push('', 'Stack Trace:', error.stack);
397
- }
398
-
399
- if (error.componentStack) {
400
- lines.push('', 'Component Stack:', error.componentStack);
401
- }
402
-
403
- return lines.join('\n');
404
- }
405
-
406
- // ============================================================================
407
- // Mount Function
408
- // ============================================================================
409
-
410
- let kitchenRoot: Root | null = null;
411
- let hostElement: HTMLElement | null = null;
412
-
413
- /**
414
- * Mandu Kitchen DevTools 마운트
415
- */
416
- export function mountKitchen(config: DevToolsConfig = {}): void {
417
- if (typeof window === 'undefined') return;
418
- if (hostElement) return; // Already mounted
419
-
420
- // Create host element
421
- hostElement = document.createElement('div');
422
- hostElement.setAttribute('data-testid', testIds.host);
423
- hostElement.setAttribute('id', 'mandu-kitchen-host');
424
- document.body.appendChild(hostElement);
425
-
426
- // Create shadow root
427
- const shadowRoot = hostElement.attachShadow({ mode: 'open' });
428
-
429
- // Inject base styles
430
- const styleElement = document.createElement('style');
431
- styleElement.textContent = baseStyles;
432
- shadowRoot.appendChild(styleElement);
433
-
434
- // Create render container
435
- const container = document.createElement('div');
436
- container.setAttribute('data-testid', testIds.root);
437
- shadowRoot.appendChild(container);
438
-
439
- // Mount React
440
- kitchenRoot = createRoot(container);
441
- kitchenRoot.render(<KitchenApp config={config} />);
442
-
443
- // Initialize state manager with config
444
- getStateManager(config);
445
- }
446
-
447
- /**
448
- * Mandu Kitchen DevTools 언마운트
449
- */
450
- export function unmountKitchen(): void {
451
- if (kitchenRoot) {
452
- kitchenRoot.unmount();
453
- kitchenRoot = null;
454
- }
455
-
456
- if (hostElement) {
457
- hostElement.remove();
458
- hostElement = null;
459
- }
460
- }
461
-
462
- /**
463
- * DevTools 상태 확인
464
- */
465
- export function isKitchenMounted(): boolean {
466
- return hostElement !== null;
467
- }
1
+ /**
2
+ * Mandu Kitchen DevTools - Root Component
3
+ * @version 1.0.3
4
+ *
5
+ * Shadow DOM을 사용하여 앱의 CSS와 격리된 DevTools 루트
6
+ */
7
+
8
+ import React, { useEffect, useState, useCallback, useMemo } from 'react';
9
+ import { createRoot, type Root } from 'react-dom/client';
10
+ import type { NormalizedError, DevToolsConfig, IslandSnapshot, NetworkRequest, DevToolsGuardViolation } from '../../types';
11
+ import { generateCSSVariables, testIds, zIndex } from '../../design-tokens';
12
+ import { getStateManager, type KitchenState } from '../state-manager';
13
+ import { getOrCreateHook } from '../../hook';
14
+ import { ErrorOverlay } from './overlay';
15
+ import { ManduBadge } from './mandu-character';
16
+ import {
17
+ PanelContainer,
18
+ ErrorsPanel,
19
+ IslandsPanel,
20
+ NetworkPanel,
21
+ GuardPanel,
22
+ PreviewPanel,
23
+ type TabId,
24
+ } from './panel';
25
+
26
+ // ============================================================================
27
+ // Base Styles
28
+ // ============================================================================
29
+
30
+ const baseStyles = `
31
+ ${generateCSSVariables()}
32
+
33
+ * {
34
+ box-sizing: border-box;
35
+ margin: 0;
36
+ padding: 0;
37
+ }
38
+
39
+ :host {
40
+ all: initial;
41
+ font-family: var(--mk-font-sans);
42
+ color: var(--mk-color-text-primary);
43
+ font-size: var(--mk-font-size-md);
44
+ line-height: 1.5;
45
+ }
46
+
47
+ button {
48
+ background: none;
49
+ border: none;
50
+ cursor: pointer;
51
+ font-family: inherit;
52
+ color: inherit;
53
+ font-size: inherit;
54
+ -webkit-appearance: none;
55
+ appearance: none;
56
+ padding: 0;
57
+ margin: 0;
58
+ }
59
+
60
+ * {
61
+ scrollbar-width: thin;
62
+ scrollbar-color: rgba(255, 255, 255, 0.12) transparent;
63
+ }
64
+
65
+ *:hover {
66
+ scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
67
+ }
68
+
69
+ *::-webkit-scrollbar {
70
+ width: 6px;
71
+ height: 6px;
72
+ }
73
+
74
+ *::-webkit-scrollbar-track {
75
+ background: transparent;
76
+ }
77
+
78
+ *::-webkit-scrollbar-thumb {
79
+ background: rgba(255, 255, 255, 0.12);
80
+ border-radius: 3px;
81
+ }
82
+
83
+ *::-webkit-scrollbar-thumb:hover {
84
+ background: rgba(255, 255, 255, 0.25);
85
+ }
86
+
87
+ *::-webkit-scrollbar-corner {
88
+ background: transparent;
89
+ }
90
+
91
+ .mk-badge-container {
92
+ position: fixed;
93
+ z-index: ${zIndex.devtools};
94
+ transition: all 0.3s ease;
95
+ }
96
+
97
+ .mk-badge-container.bottom-right {
98
+ bottom: 16px;
99
+ right: 16px;
100
+ }
101
+
102
+ .mk-badge-container.bottom-left {
103
+ bottom: 16px;
104
+ left: 16px;
105
+ }
106
+
107
+ .mk-badge-container.top-right {
108
+ top: 16px;
109
+ right: 16px;
110
+ }
111
+
112
+ .mk-badge-container.top-left {
113
+ top: 16px;
114
+ left: 16px;
115
+ }
116
+
117
+ .mk-badge-container.panel-open {
118
+ opacity: 0;
119
+ pointer-events: none;
120
+ transform: scale(0.8);
121
+ }
122
+
123
+ @keyframes mk-badge-breathe {
124
+ 0%, 100% { transform: scale(1) translateY(0px); }
125
+ 50% { transform: scale(1.04) translateY(0px); }
126
+ }
127
+
128
+ @keyframes mk-badge-attention {
129
+ 0%, 100% { transform: scale(1) translateY(0px); }
130
+ 15% { transform: scale(1.08) translateY(0px); }
131
+ 30% { transform: scale(0.98) translateY(0px); }
132
+ 45% { transform: scale(1.04) translateY(0px); }
133
+ 60% { transform: scale(1) translateY(0px); }
134
+ }
135
+
136
+ @keyframes mk-badge-float {
137
+ 0%, 100% { transform: scale(1) translateY(0px); }
138
+ 50% { transform: scale(1) translateY(-3px); }
139
+ }
140
+ `;
141
+
142
+ // ============================================================================
143
+ // Kitchen App Component
144
+ // ============================================================================
145
+
146
+ interface KitchenAppProps {
147
+ config: DevToolsConfig;
148
+ }
149
+
150
+ function KitchenApp({ config }: KitchenAppProps): React.ReactElement | null {
151
+ const [state, setState] = useState<KitchenState>(() => getStateManager().getState());
152
+
153
+ // Subscribe to state changes
154
+ useEffect(() => {
155
+ const stateManager = getStateManager();
156
+ const unsubscribe = stateManager.subscribe((newState) => {
157
+ setState(newState);
158
+ });
159
+
160
+ // Connect to hook
161
+ const hook = getOrCreateHook();
162
+ hook.connect((event) => {
163
+ stateManager.handleEvent(event);
164
+ });
165
+
166
+ return () => {
167
+ unsubscribe();
168
+ hook.disconnect();
169
+ };
170
+ }, []);
171
+
172
+ // Keyboard shortcuts
173
+ useEffect(() => {
174
+ const handleKeyDown = (e: KeyboardEvent) => {
175
+ // Ctrl+Shift+M: Toggle panel
176
+ if (e.ctrlKey && e.shiftKey && e.key === 'M') {
177
+ e.preventDefault();
178
+ getStateManager().toggle();
179
+ }
180
+ // Ctrl+Shift+E: Open errors tab
181
+ if (e.ctrlKey && e.shiftKey && e.key === 'E') {
182
+ e.preventDefault();
183
+ getStateManager().setActiveTab('errors');
184
+ getStateManager().open();
185
+ }
186
+ };
187
+
188
+ window.addEventListener('keydown', handleKeyDown);
189
+ return () => window.removeEventListener('keydown', handleKeyDown);
190
+ }, []);
191
+
192
+ // Handlers
193
+ const handleOverlayClose = useCallback(() => {
194
+ getStateManager().hideOverlay();
195
+ }, []);
196
+
197
+ const handleOverlayIgnore = useCallback(() => {
198
+ if (state.overlayError) {
199
+ getStateManager().ignoreError(state.overlayError.id);
200
+ }
201
+ }, [state.overlayError]);
202
+
203
+ const handleOverlayCopy = useCallback(async () => {
204
+ if (!state.overlayError) return;
205
+
206
+ const errorInfo = formatErrorForCopy(state.overlayError);
207
+ try {
208
+ await navigator.clipboard.writeText(errorInfo);
209
+ } catch {
210
+ // Fallback for older browsers
211
+ const textarea = document.createElement('textarea');
212
+ textarea.value = errorInfo;
213
+ document.body.appendChild(textarea);
214
+ textarea.select();
215
+ document.execCommand('copy');
216
+ document.body.removeChild(textarea);
217
+ }
218
+ }, [state.overlayError]);
219
+
220
+ const handleBadgeClick = useCallback(() => {
221
+ getStateManager().toggle();
222
+ }, []);
223
+
224
+ const handlePanelClose = useCallback(() => {
225
+ getStateManager().close();
226
+ }, []);
227
+
228
+ const handleTabChange = useCallback((tab: TabId) => {
229
+ getStateManager().setActiveTab(tab);
230
+ }, []);
231
+
232
+ const handleErrorClick = useCallback((error: NormalizedError) => {
233
+ getStateManager().showOverlay(error);
234
+ }, []);
235
+
236
+ const handleErrorIgnore = useCallback((id: string) => {
237
+ getStateManager().ignoreError(id);
238
+ }, []);
239
+
240
+ const handleClearErrors = useCallback(() => {
241
+ getStateManager().clearErrors();
242
+ }, []);
243
+
244
+ const handleClearGuard = useCallback(() => {
245
+ getStateManager().clearDevToolsGuardViolations();
246
+ }, []);
247
+
248
+ const handleClearRecentChanges = useCallback(() => {
249
+ getStateManager().clearRecentChanges();
250
+ }, []);
251
+
252
+ const handleRestart = useCallback(async () => {
253
+ try {
254
+ // 1. Service Worker 해제
255
+ if ('serviceWorker' in navigator) {
256
+ const registrations = await navigator.serviceWorker.getRegistrations();
257
+ for (const reg of registrations) {
258
+ await reg.unregister();
259
+ }
260
+ }
261
+
262
+ // 2. Cache API 클리어
263
+ if ('caches' in window) {
264
+ const cacheNames = await caches.keys();
265
+ for (const name of cacheNames) {
266
+ await caches.delete(name);
267
+ }
268
+ }
269
+
270
+ // 3. window.__MANDU_* globals 삭제
271
+ for (const key of Object.keys(window)) {
272
+ if (key.startsWith('__MANDU_')) {
273
+ delete (window as any)[key];
274
+ }
275
+ }
276
+
277
+ // 4. HMR 서버에 POST /restart
278
+ const hmrPort = (window as any).__MANDU_HMR_PORT__;
279
+ if (hmrPort) {
280
+ await fetch(`http://localhost:${hmrPort}/restart`, { method: 'POST' });
281
+ }
282
+
283
+ // 5. 3초 fallback reload (서버가 reload 브로드캐스트를 보낸 경우)
284
+ setTimeout(() => {
285
+ location.reload();
286
+ }, 3000);
287
+ } catch (err) {
288
+ console.error('[Mandu Kitchen] Restart failed:', err);
289
+ location.reload();
290
+ }
291
+ }, []);
292
+
293
+ // Calculate error count
294
+ const errorCount = useMemo(() => {
295
+ return state.errors.filter(
296
+ (e) => e.severity === 'error' || e.severity === 'critical'
297
+ ).length;
298
+ }, [state.errors]);
299
+
300
+ // Convert Maps to Arrays for panels
301
+ const islandsArray = useMemo(
302
+ () => Array.from(state.islands.values()),
303
+ [state.islands]
304
+ );
305
+
306
+ const networkArray = useMemo(
307
+ () => Array.from(state.networkRequests.values()),
308
+ [state.networkRequests]
309
+ );
310
+
311
+ const position = config.position ?? 'bottom-right';
312
+
313
+ // Don't render if disabled
314
+ if (config.enabled === false) {
315
+ return null;
316
+ }
317
+
318
+ // Render active panel content
319
+ const renderPanelContent = () => {
320
+ switch (state.activeTab) {
321
+ case 'errors':
322
+ return (
323
+ <ErrorsPanel
324
+ errors={state.errors}
325
+ onErrorClick={handleErrorClick}
326
+ onErrorIgnore={handleErrorIgnore}
327
+ onClearAll={handleClearErrors}
328
+ />
329
+ );
330
+ case 'islands':
331
+ return <IslandsPanel islands={islandsArray} />;
332
+ case 'network':
333
+ return <NetworkPanel requests={networkArray} />;
334
+ case 'guard':
335
+ return (
336
+ <GuardPanel
337
+ violations={state.guardViolations}
338
+ onClear={handleClearGuard}
339
+ />
340
+ );
341
+ case 'preview':
342
+ return (
343
+ <PreviewPanel
344
+ recentChanges={state.recentChanges}
345
+ onClearChanges={handleClearRecentChanges}
346
+ />
347
+ );
348
+ default:
349
+ return null;
350
+ }
351
+ };
352
+
353
+ return (
354
+ <>
355
+ {/* Badge (hidden when panel is open) */}
356
+ <div className={`mk-badge-container ${position}${state.isOpen ? ' panel-open' : ''}`}>
357
+ <ManduBadge
358
+ state={state.manduState}
359
+ count={errorCount}
360
+ onClick={handleBadgeClick}
361
+ />
362
+ </div>
363
+
364
+ {/* Panel */}
365
+ {state.isOpen && (
366
+ <PanelContainer
367
+ state={state}
368
+ activeTab={state.activeTab}
369
+ onTabChange={handleTabChange}
370
+ onClose={handlePanelClose}
371
+ onRestart={handleRestart}
372
+ position={position}
373
+ >
374
+ {renderPanelContent()}
375
+ </PanelContainer>
376
+ )}
377
+
378
+ {/* Overlay */}
379
+ {state.overlayError && config.features?.errorOverlay !== false && (
380
+ <ErrorOverlay
381
+ error={state.overlayError}
382
+ onClose={handleOverlayClose}
383
+ onIgnore={handleOverlayIgnore}
384
+ onCopy={handleOverlayCopy}
385
+ />
386
+ )}
387
+ </>
388
+ );
389
+ }
390
+
391
+ // ============================================================================
392
+ // Helper Functions
393
+ // ============================================================================
394
+
395
+ function formatErrorForCopy(error: NormalizedError): string {
396
+ const lines = [
397
+ `[${error.severity.toUpperCase()}] ${error.type}`,
398
+ `Message: ${error.message}`,
399
+ `Time: ${new Date(error.timestamp).toISOString()}`,
400
+ `URL: ${error.url}`,
401
+ ];
402
+
403
+ if (error.source) {
404
+ lines.push(`Source: ${error.source}:${error.line ?? '?'}:${error.column ?? '?'}`);
405
+ }
406
+
407
+ if (error.stack) {
408
+ lines.push('', 'Stack Trace:', error.stack);
409
+ }
410
+
411
+ if (error.componentStack) {
412
+ lines.push('', 'Component Stack:', error.componentStack);
413
+ }
414
+
415
+ return lines.join('\n');
416
+ }
417
+
418
+ // ============================================================================
419
+ // Mount Function
420
+ // ============================================================================
421
+
422
+ let kitchenRoot: Root | null = null;
423
+ let hostElement: HTMLElement | null = null;
424
+
425
+ /**
426
+ * Mandu Kitchen DevTools 마운트
427
+ */
428
+ export function mountKitchen(config: DevToolsConfig = {}): void {
429
+ if (typeof window === 'undefined') return;
430
+ if (hostElement) return; // Already mounted
431
+
432
+ // Create host element
433
+ hostElement = document.createElement('div');
434
+ hostElement.setAttribute('data-testid', testIds.host);
435
+ hostElement.setAttribute('id', 'mandu-kitchen-host');
436
+ document.body.appendChild(hostElement);
437
+
438
+ // Create shadow root
439
+ const shadowRoot = hostElement.attachShadow({ mode: 'open' });
440
+
441
+ // Inject base styles
442
+ const styleElement = document.createElement('style');
443
+ styleElement.textContent = baseStyles;
444
+ shadowRoot.appendChild(styleElement);
445
+
446
+ // Create render container
447
+ const container = document.createElement('div');
448
+ container.setAttribute('data-testid', testIds.root);
449
+ shadowRoot.appendChild(container);
450
+
451
+ // Mount React
452
+ kitchenRoot = createRoot(container);
453
+ kitchenRoot.render(<KitchenApp config={config} />);
454
+
455
+ // Initialize state manager with config
456
+ getStateManager(config);
457
+ }
458
+
459
+ /**
460
+ * Mandu Kitchen DevTools 언마운트
461
+ */
462
+ export function unmountKitchen(): void {
463
+ if (kitchenRoot) {
464
+ kitchenRoot.unmount();
465
+ kitchenRoot = null;
466
+ }
467
+
468
+ if (hostElement) {
469
+ hostElement.remove();
470
+ hostElement = null;
471
+ }
472
+ }
473
+
474
+ /**
475
+ * DevTools 상태 확인
476
+ */
477
+ export function isKitchenMounted(): boolean {
478
+ return hostElement !== null;
479
+ }