@mandujs/core 0.9.41 → 0.9.42

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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/bundler/build.ts +91 -73
  3. package/src/bundler/dev.ts +21 -14
  4. package/src/client/globals.ts +44 -0
  5. package/src/client/index.ts +5 -4
  6. package/src/client/island.ts +8 -13
  7. package/src/client/router.ts +33 -41
  8. package/src/client/runtime.ts +23 -51
  9. package/src/client/window-state.ts +101 -0
  10. package/src/config/index.ts +1 -0
  11. package/src/config/mandu.ts +45 -9
  12. package/src/config/validate.ts +158 -0
  13. package/src/constants.ts +25 -0
  14. package/src/contract/client.ts +4 -3
  15. package/src/contract/define.ts +459 -0
  16. package/src/devtools/ai/context-builder.ts +375 -0
  17. package/src/devtools/ai/index.ts +25 -0
  18. package/src/devtools/ai/mcp-connector.ts +465 -0
  19. package/src/devtools/client/catchers/error-catcher.ts +327 -0
  20. package/src/devtools/client/catchers/index.ts +18 -0
  21. package/src/devtools/client/catchers/network-proxy.ts +363 -0
  22. package/src/devtools/client/components/index.ts +39 -0
  23. package/src/devtools/client/components/kitchen-root.tsx +362 -0
  24. package/src/devtools/client/components/mandu-character.tsx +241 -0
  25. package/src/devtools/client/components/overlay.tsx +368 -0
  26. package/src/devtools/client/components/panel/errors-panel.tsx +259 -0
  27. package/src/devtools/client/components/panel/guard-panel.tsx +244 -0
  28. package/src/devtools/client/components/panel/index.ts +32 -0
  29. package/src/devtools/client/components/panel/islands-panel.tsx +304 -0
  30. package/src/devtools/client/components/panel/network-panel.tsx +292 -0
  31. package/src/devtools/client/components/panel/panel-container.tsx +259 -0
  32. package/src/devtools/client/filters/context-filters.ts +282 -0
  33. package/src/devtools/client/filters/index.ts +16 -0
  34. package/src/devtools/client/index.ts +63 -0
  35. package/src/devtools/client/persistence.ts +335 -0
  36. package/src/devtools/client/state-manager.ts +478 -0
  37. package/src/devtools/design-tokens.ts +263 -0
  38. package/src/devtools/hook/create-hook.ts +207 -0
  39. package/src/devtools/hook/index.ts +13 -0
  40. package/src/devtools/index.ts +439 -0
  41. package/src/devtools/init.ts +266 -0
  42. package/src/devtools/protocol.ts +237 -0
  43. package/src/devtools/server/index.ts +17 -0
  44. package/src/devtools/server/source-context.ts +444 -0
  45. package/src/devtools/types.ts +319 -0
  46. package/src/devtools/worker/index.ts +25 -0
  47. package/src/devtools/worker/redaction-worker.ts +222 -0
  48. package/src/devtools/worker/worker-manager.ts +409 -0
  49. package/src/error/formatter.ts +28 -24
  50. package/src/error/index.ts +13 -9
  51. package/src/error/result.ts +46 -0
  52. package/src/error/types.ts +6 -4
  53. package/src/filling/filling.ts +6 -5
  54. package/src/guard/check.ts +60 -56
  55. package/src/guard/types.ts +3 -1
  56. package/src/guard/watcher.ts +10 -1
  57. package/src/index.ts +81 -0
  58. package/src/intent/index.ts +310 -0
  59. package/src/island/index.ts +304 -0
  60. package/src/router/fs-patterns.ts +7 -0
  61. package/src/router/fs-routes.ts +20 -8
  62. package/src/router/fs-scanner.ts +117 -133
  63. package/src/runtime/server.ts +261 -201
  64. package/src/runtime/ssr.ts +5 -4
  65. package/src/runtime/streaming-ssr.ts +5 -4
  66. package/src/utils/bun.ts +8 -0
  67. package/src/utils/lru-cache.ts +75 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Mandu Kitchen DevTools - Components Module
3
+ * @version 1.0.3
4
+ */
5
+
6
+ export {
7
+ ManduCharacter,
8
+ ManduBadge,
9
+ type ManduCharacterProps,
10
+ type ManduBadgeProps,
11
+ } from './mandu-character';
12
+
13
+ export {
14
+ ErrorOverlay,
15
+ type ErrorOverlayProps,
16
+ } from './overlay';
17
+
18
+ export {
19
+ mountKitchen,
20
+ unmountKitchen,
21
+ isKitchenMounted,
22
+ } from './kitchen-root';
23
+
24
+ // Panel Components
25
+ export {
26
+ PanelContainer,
27
+ ErrorsPanel,
28
+ IslandsPanel,
29
+ NetworkPanel,
30
+ GuardPanel,
31
+ TABS,
32
+ type TabId,
33
+ type TabDefinition,
34
+ type PanelContainerProps,
35
+ type ErrorsPanelProps,
36
+ type IslandsPanelProps,
37
+ type NetworkPanelProps,
38
+ type GuardPanelProps,
39
+ } from './panel';
@@ -0,0 +1,362 @@
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, GuardViolation } 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
+ .mk-badge-container {
47
+ position: fixed;
48
+ z-index: ${zIndex.devtools};
49
+ transition: all 0.3s ease;
50
+ }
51
+
52
+ .mk-badge-container.bottom-right {
53
+ bottom: 16px;
54
+ right: 16px;
55
+ }
56
+
57
+ .mk-badge-container.bottom-left {
58
+ bottom: 16px;
59
+ left: 16px;
60
+ }
61
+
62
+ .mk-badge-container.top-right {
63
+ top: 16px;
64
+ right: 16px;
65
+ }
66
+
67
+ .mk-badge-container.top-left {
68
+ top: 16px;
69
+ left: 16px;
70
+ }
71
+
72
+ .mk-badge-container.panel-open {
73
+ opacity: 0;
74
+ pointer-events: none;
75
+ }
76
+ `;
77
+
78
+ // ============================================================================
79
+ // Kitchen App Component
80
+ // ============================================================================
81
+
82
+ interface KitchenAppProps {
83
+ config: DevToolsConfig;
84
+ }
85
+
86
+ function KitchenApp({ config }: KitchenAppProps): React.ReactElement | null {
87
+ const [state, setState] = useState<KitchenState>(() => getStateManager().getState());
88
+
89
+ // Subscribe to state changes
90
+ useEffect(() => {
91
+ const stateManager = getStateManager();
92
+ const unsubscribe = stateManager.subscribe((newState) => {
93
+ setState(newState);
94
+ });
95
+
96
+ // Connect to hook
97
+ const hook = getOrCreateHook();
98
+ hook.connect((event) => {
99
+ stateManager.handleEvent(event);
100
+ });
101
+
102
+ return () => {
103
+ unsubscribe();
104
+ hook.disconnect();
105
+ };
106
+ }, []);
107
+
108
+ // Keyboard shortcuts
109
+ useEffect(() => {
110
+ const handleKeyDown = (e: KeyboardEvent) => {
111
+ // Ctrl+Shift+M: Toggle panel
112
+ if (e.ctrlKey && e.shiftKey && e.key === 'M') {
113
+ e.preventDefault();
114
+ getStateManager().toggle();
115
+ }
116
+ // Ctrl+Shift+E: Open errors tab
117
+ if (e.ctrlKey && e.shiftKey && e.key === 'E') {
118
+ e.preventDefault();
119
+ getStateManager().setActiveTab('errors');
120
+ getStateManager().open();
121
+ }
122
+ };
123
+
124
+ window.addEventListener('keydown', handleKeyDown);
125
+ return () => window.removeEventListener('keydown', handleKeyDown);
126
+ }, []);
127
+
128
+ // Handlers
129
+ const handleOverlayClose = useCallback(() => {
130
+ getStateManager().hideOverlay();
131
+ }, []);
132
+
133
+ const handleOverlayIgnore = useCallback(() => {
134
+ if (state.overlayError) {
135
+ getStateManager().ignoreError(state.overlayError.id);
136
+ }
137
+ }, [state.overlayError]);
138
+
139
+ const handleOverlayCopy = useCallback(async () => {
140
+ if (!state.overlayError) return;
141
+
142
+ const errorInfo = formatErrorForCopy(state.overlayError);
143
+ try {
144
+ await navigator.clipboard.writeText(errorInfo);
145
+ } catch {
146
+ // Fallback for older browsers
147
+ const textarea = document.createElement('textarea');
148
+ textarea.value = errorInfo;
149
+ document.body.appendChild(textarea);
150
+ textarea.select();
151
+ document.execCommand('copy');
152
+ document.body.removeChild(textarea);
153
+ }
154
+ }, [state.overlayError]);
155
+
156
+ const handleBadgeClick = useCallback(() => {
157
+ getStateManager().toggle();
158
+ }, []);
159
+
160
+ const handlePanelClose = useCallback(() => {
161
+ getStateManager().close();
162
+ }, []);
163
+
164
+ const handleTabChange = useCallback((tab: TabId) => {
165
+ getStateManager().setActiveTab(tab);
166
+ }, []);
167
+
168
+ const handleErrorClick = useCallback((error: NormalizedError) => {
169
+ getStateManager().showOverlay(error);
170
+ }, []);
171
+
172
+ const handleErrorIgnore = useCallback((id: string) => {
173
+ getStateManager().ignoreError(id);
174
+ }, []);
175
+
176
+ const handleClearErrors = useCallback(() => {
177
+ getStateManager().clearErrors();
178
+ }, []);
179
+
180
+ const handleClearGuard = useCallback(() => {
181
+ getStateManager().clearGuardViolations();
182
+ }, []);
183
+
184
+ // Calculate error count
185
+ const errorCount = useMemo(() => {
186
+ return state.errors.filter(
187
+ (e) => e.severity === 'error' || e.severity === 'critical'
188
+ ).length;
189
+ }, [state.errors]);
190
+
191
+ // Convert Maps to Arrays for panels
192
+ const islandsArray = useMemo(
193
+ () => Array.from(state.islands.values()),
194
+ [state.islands]
195
+ );
196
+
197
+ const networkArray = useMemo(
198
+ () => Array.from(state.networkRequests.values()),
199
+ [state.networkRequests]
200
+ );
201
+
202
+ const position = config.position ?? 'bottom-right';
203
+
204
+ // Don't render if disabled
205
+ if (config.enabled === false) {
206
+ return null;
207
+ }
208
+
209
+ // Render active panel content
210
+ const renderPanelContent = () => {
211
+ switch (state.activeTab) {
212
+ case 'errors':
213
+ return (
214
+ <ErrorsPanel
215
+ errors={state.errors}
216
+ onErrorClick={handleErrorClick}
217
+ onErrorIgnore={handleErrorIgnore}
218
+ onClearAll={handleClearErrors}
219
+ />
220
+ );
221
+ case 'islands':
222
+ return <IslandsPanel islands={islandsArray} />;
223
+ case 'network':
224
+ return <NetworkPanel requests={networkArray} />;
225
+ case 'guard':
226
+ return (
227
+ <GuardPanel
228
+ violations={state.guardViolations}
229
+ onClear={handleClearGuard}
230
+ />
231
+ );
232
+ default:
233
+ return null;
234
+ }
235
+ };
236
+
237
+ return (
238
+ <>
239
+ {/* Badge (hidden when panel is open) */}
240
+ <div className={`mk-badge-container ${position}${state.isOpen ? ' panel-open' : ''}`}>
241
+ <ManduBadge
242
+ state={state.manduState}
243
+ count={errorCount}
244
+ onClick={handleBadgeClick}
245
+ />
246
+ </div>
247
+
248
+ {/* Panel */}
249
+ {state.isOpen && (
250
+ <PanelContainer
251
+ state={state}
252
+ activeTab={state.activeTab}
253
+ onTabChange={handleTabChange}
254
+ onClose={handlePanelClose}
255
+ position={position}
256
+ >
257
+ {renderPanelContent()}
258
+ </PanelContainer>
259
+ )}
260
+
261
+ {/* Overlay */}
262
+ {state.overlayError && config.features?.errorOverlay !== false && (
263
+ <ErrorOverlay
264
+ error={state.overlayError}
265
+ onClose={handleOverlayClose}
266
+ onIgnore={handleOverlayIgnore}
267
+ onCopy={handleOverlayCopy}
268
+ />
269
+ )}
270
+ </>
271
+ );
272
+ }
273
+
274
+ // ============================================================================
275
+ // Helper Functions
276
+ // ============================================================================
277
+
278
+ function formatErrorForCopy(error: NormalizedError): string {
279
+ const lines = [
280
+ `[${error.severity.toUpperCase()}] ${error.type}`,
281
+ `Message: ${error.message}`,
282
+ `Time: ${new Date(error.timestamp).toISOString()}`,
283
+ `URL: ${error.url}`,
284
+ ];
285
+
286
+ if (error.source) {
287
+ lines.push(`Source: ${error.source}:${error.line ?? '?'}:${error.column ?? '?'}`);
288
+ }
289
+
290
+ if (error.stack) {
291
+ lines.push('', 'Stack Trace:', error.stack);
292
+ }
293
+
294
+ if (error.componentStack) {
295
+ lines.push('', 'Component Stack:', error.componentStack);
296
+ }
297
+
298
+ return lines.join('\n');
299
+ }
300
+
301
+ // ============================================================================
302
+ // Mount Function
303
+ // ============================================================================
304
+
305
+ let kitchenRoot: Root | null = null;
306
+ let hostElement: HTMLElement | null = null;
307
+
308
+ /**
309
+ * Mandu Kitchen DevTools 마운트
310
+ */
311
+ export function mountKitchen(config: DevToolsConfig = {}): void {
312
+ if (typeof window === 'undefined') return;
313
+ if (hostElement) return; // Already mounted
314
+
315
+ // Create host element
316
+ hostElement = document.createElement('div');
317
+ hostElement.setAttribute('data-testid', testIds.host);
318
+ hostElement.setAttribute('id', 'mandu-kitchen-host');
319
+ document.body.appendChild(hostElement);
320
+
321
+ // Create shadow root
322
+ const shadowRoot = hostElement.attachShadow({ mode: 'open' });
323
+
324
+ // Inject base styles
325
+ const styleElement = document.createElement('style');
326
+ styleElement.textContent = baseStyles;
327
+ shadowRoot.appendChild(styleElement);
328
+
329
+ // Create render container
330
+ const container = document.createElement('div');
331
+ container.setAttribute('data-testid', testIds.root);
332
+ shadowRoot.appendChild(container);
333
+
334
+ // Mount React
335
+ kitchenRoot = createRoot(container);
336
+ kitchenRoot.render(<KitchenApp config={config} />);
337
+
338
+ // Initialize state manager with config
339
+ getStateManager(config);
340
+ }
341
+
342
+ /**
343
+ * Mandu Kitchen DevTools 언마운트
344
+ */
345
+ export function unmountKitchen(): void {
346
+ if (kitchenRoot) {
347
+ kitchenRoot.unmount();
348
+ kitchenRoot = null;
349
+ }
350
+
351
+ if (hostElement) {
352
+ hostElement.remove();
353
+ hostElement = null;
354
+ }
355
+ }
356
+
357
+ /**
358
+ * DevTools 상태 확인
359
+ */
360
+ export function isKitchenMounted(): boolean {
361
+ return hostElement !== null;
362
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Mandu Kitchen DevTools - Mandu Character Component
3
+ * @version 1.0.3
4
+ */
5
+
6
+ import React from 'react';
7
+ import type { ManduState } from '../../types';
8
+ import { MANDU_CHARACTERS } from '../../types';
9
+ import { colors, animation, testIds } from '../../design-tokens';
10
+
11
+ // ============================================================================
12
+ // Styles
13
+ // ============================================================================
14
+
15
+ const styles = {
16
+ container: {
17
+ display: 'flex',
18
+ alignItems: 'center',
19
+ gap: '12px',
20
+ padding: '12px 16px',
21
+ borderRadius: '12px',
22
+ backgroundColor: colors.background.medium,
23
+ transition: `all ${animation.duration.normal} ${animation.easing.easeOut}`,
24
+ },
25
+ emoji: {
26
+ fontSize: '32px',
27
+ lineHeight: 1,
28
+ userSelect: 'none' as const,
29
+ },
30
+ content: {
31
+ display: 'flex',
32
+ flexDirection: 'column' as const,
33
+ gap: '2px',
34
+ },
35
+ message: {
36
+ fontSize: '14px',
37
+ color: colors.text.primary,
38
+ fontWeight: 500,
39
+ },
40
+ status: {
41
+ fontSize: '12px',
42
+ color: colors.text.secondary,
43
+ },
44
+ } as const;
45
+
46
+ const stateColors: Record<ManduState, string> = {
47
+ normal: colors.semantic.success,
48
+ warning: colors.semantic.warning,
49
+ error: colors.semantic.error,
50
+ loading: colors.semantic.info,
51
+ hmr: colors.brand.accent,
52
+ };
53
+
54
+ // ============================================================================
55
+ // Animation Keyframes (inline)
56
+ // ============================================================================
57
+
58
+ const bounceAnimation = `
59
+ @keyframes mk-bounce {
60
+ 0%, 100% { transform: translateY(0); }
61
+ 50% { transform: translateY(-4px); }
62
+ }
63
+ `;
64
+
65
+ const pulseAnimation = `
66
+ @keyframes mk-pulse {
67
+ 0%, 100% { opacity: 1; }
68
+ 50% { opacity: 0.6; }
69
+ }
70
+ `;
71
+
72
+ const shakeAnimation = `
73
+ @keyframes mk-shake {
74
+ 0%, 100% { transform: translateX(0); }
75
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
76
+ 20%, 40%, 60%, 80% { transform: translateX(2px); }
77
+ }
78
+ `;
79
+
80
+ const sparkleAnimation = `
81
+ @keyframes mk-sparkle {
82
+ 0% { transform: scale(1); }
83
+ 50% { transform: scale(1.1); }
84
+ 100% { transform: scale(1); }
85
+ }
86
+ `;
87
+
88
+ // ============================================================================
89
+ // Props
90
+ // ============================================================================
91
+
92
+ export interface ManduCharacterProps {
93
+ state: ManduState;
94
+ errorCount?: number;
95
+ className?: string;
96
+ compact?: boolean;
97
+ onClick?: () => void;
98
+ }
99
+
100
+ // ============================================================================
101
+ // Component
102
+ // ============================================================================
103
+
104
+ export function ManduCharacter({
105
+ state,
106
+ errorCount = 0,
107
+ className,
108
+ compact = false,
109
+ onClick,
110
+ }: ManduCharacterProps): React.ReactElement {
111
+ const character = MANDU_CHARACTERS[state];
112
+ const stateColor = stateColors[state];
113
+
114
+ const getAnimation = (): string => {
115
+ switch (state) {
116
+ case 'loading':
117
+ return 'mk-bounce 1s ease-in-out infinite';
118
+ case 'error':
119
+ return 'mk-shake 0.5s ease-in-out';
120
+ case 'hmr':
121
+ return 'mk-sparkle 0.6s ease-in-out';
122
+ case 'warning':
123
+ return 'mk-pulse 2s ease-in-out infinite';
124
+ default:
125
+ return 'none';
126
+ }
127
+ };
128
+
129
+ const containerStyle = {
130
+ ...styles.container,
131
+ borderLeft: `4px solid ${stateColor}`,
132
+ cursor: onClick ? 'pointer' : 'default',
133
+ ...(compact && {
134
+ padding: '8px 12px',
135
+ gap: '8px',
136
+ }),
137
+ };
138
+
139
+ const emojiStyle = {
140
+ ...styles.emoji,
141
+ animation: getAnimation(),
142
+ ...(compact && { fontSize: '24px' }),
143
+ };
144
+
145
+ return (
146
+ <>
147
+ {/* Inject keyframes */}
148
+ <style>
149
+ {bounceAnimation}
150
+ {pulseAnimation}
151
+ {shakeAnimation}
152
+ {sparkleAnimation}
153
+ </style>
154
+
155
+ <div
156
+ data-testid={testIds.mandu}
157
+ className={className}
158
+ style={containerStyle}
159
+ onClick={onClick}
160
+ role={onClick ? 'button' : undefined}
161
+ tabIndex={onClick ? 0 : undefined}
162
+ onKeyDown={(e) => {
163
+ if (onClick && (e.key === 'Enter' || e.key === ' ')) {
164
+ e.preventDefault();
165
+ onClick();
166
+ }
167
+ }}
168
+ >
169
+ <span style={emojiStyle} aria-hidden="true">
170
+ {character.emoji}
171
+ </span>
172
+
173
+ {!compact && (
174
+ <div style={styles.content}>
175
+ <span style={styles.message}>{character.message}</span>
176
+ {errorCount > 0 && (
177
+ <span style={styles.status}>
178
+ {errorCount}개의 {state === 'error' ? '에러' : '경고'}가 있어요
179
+ </span>
180
+ )}
181
+ </div>
182
+ )}
183
+ </div>
184
+ </>
185
+ );
186
+ }
187
+
188
+ // ============================================================================
189
+ // Badge Component (for mini display)
190
+ // ============================================================================
191
+
192
+ export interface ManduBadgeProps {
193
+ state: ManduState;
194
+ count?: number;
195
+ onClick?: () => void;
196
+ }
197
+
198
+ export function ManduBadge({
199
+ state,
200
+ count = 0,
201
+ onClick,
202
+ }: ManduBadgeProps): React.ReactElement {
203
+ const character = MANDU_CHARACTERS[state];
204
+ const stateColor = stateColors[state];
205
+
206
+ const badgeStyle: React.CSSProperties = {
207
+ display: 'flex',
208
+ alignItems: 'center',
209
+ justifyContent: 'center',
210
+ gap: '6px',
211
+ padding: '8px 12px',
212
+ borderRadius: '9999px',
213
+ backgroundColor: colors.background.dark,
214
+ border: `2px solid ${stateColor}`,
215
+ cursor: 'pointer',
216
+ transition: `all ${animation.duration.fast} ${animation.easing.easeOut}`,
217
+ boxShadow: colors.background.overlay,
218
+ fontSize: '16px',
219
+ userSelect: 'none',
220
+ };
221
+
222
+ const countStyle: React.CSSProperties = {
223
+ fontSize: '12px',
224
+ fontWeight: 600,
225
+ color: stateColor,
226
+ minWidth: '18px',
227
+ textAlign: 'center',
228
+ };
229
+
230
+ return (
231
+ <button
232
+ data-testid={testIds.badge}
233
+ style={badgeStyle}
234
+ onClick={onClick}
235
+ aria-label={`Mandu Kitchen: ${character.message}${count > 0 ? `, ${count} issues` : ''}`}
236
+ >
237
+ <span aria-hidden="true">{character.emoji}</span>
238
+ {count > 0 && <span style={countStyle}>{count}</span>}
239
+ </button>
240
+ );
241
+ }