@mandujs/core 0.9.40 β†’ 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,292 @@
1
+ /**
2
+ * Mandu Kitchen DevTools - Network Panel
3
+ * @version 1.0.3
4
+ */
5
+
6
+ import React, { useMemo, useState } from 'react';
7
+ import type { NetworkRequest } from '../../../types';
8
+ import { colors, typography, spacing, borderRadius, animation } from '../../../design-tokens';
9
+
10
+ // ============================================================================
11
+ // Styles
12
+ // ============================================================================
13
+
14
+ const styles = {
15
+ container: {
16
+ padding: spacing.md,
17
+ height: '100%',
18
+ display: 'flex',
19
+ flexDirection: 'column' as const,
20
+ gap: spacing.md,
21
+ },
22
+ header: {
23
+ display: 'flex',
24
+ alignItems: 'center',
25
+ justifyContent: 'space-between',
26
+ gap: spacing.md,
27
+ },
28
+ filterGroup: {
29
+ display: 'flex',
30
+ gap: spacing.xs,
31
+ },
32
+ filterButton: {
33
+ padding: `${spacing.xs} ${spacing.sm}`,
34
+ borderRadius: borderRadius.sm,
35
+ border: 'none',
36
+ backgroundColor: colors.background.light,
37
+ color: colors.text.secondary,
38
+ fontSize: typography.fontSize.xs,
39
+ cursor: 'pointer',
40
+ transition: `all ${animation.duration.fast}`,
41
+ },
42
+ filterButtonActive: {
43
+ backgroundColor: colors.brand.accent,
44
+ color: colors.background.dark,
45
+ },
46
+ stats: {
47
+ display: 'flex',
48
+ gap: spacing.md,
49
+ fontSize: typography.fontSize.xs,
50
+ color: colors.text.muted,
51
+ },
52
+ list: {
53
+ flex: 1,
54
+ overflow: 'auto',
55
+ display: 'flex',
56
+ flexDirection: 'column' as const,
57
+ gap: spacing.xs,
58
+ },
59
+ emptyState: {
60
+ flex: 1,
61
+ display: 'flex',
62
+ flexDirection: 'column' as const,
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
65
+ gap: spacing.md,
66
+ padding: spacing.xl,
67
+ color: colors.text.muted,
68
+ fontSize: typography.fontSize.sm,
69
+ textAlign: 'center' as const,
70
+ },
71
+ requestItem: {
72
+ display: 'flex',
73
+ alignItems: 'center',
74
+ gap: spacing.sm,
75
+ padding: spacing.sm,
76
+ borderRadius: borderRadius.md,
77
+ backgroundColor: colors.background.medium,
78
+ transition: `all ${animation.duration.fast}`,
79
+ cursor: 'pointer',
80
+ },
81
+ method: {
82
+ padding: `2px ${spacing.xs}`,
83
+ borderRadius: borderRadius.sm,
84
+ fontSize: typography.fontSize.xs,
85
+ fontWeight: typography.fontWeight.semibold,
86
+ fontFamily: typography.fontFamily.mono,
87
+ minWidth: '50px',
88
+ textAlign: 'center' as const,
89
+ },
90
+ url: {
91
+ flex: 1,
92
+ fontSize: typography.fontSize.sm,
93
+ color: colors.text.primary,
94
+ fontFamily: typography.fontFamily.mono,
95
+ whiteSpace: 'nowrap' as const,
96
+ overflow: 'hidden',
97
+ textOverflow: 'ellipsis',
98
+ },
99
+ status: {
100
+ padding: `2px ${spacing.xs}`,
101
+ borderRadius: borderRadius.sm,
102
+ fontSize: typography.fontSize.xs,
103
+ fontWeight: typography.fontWeight.medium,
104
+ minWidth: '36px',
105
+ textAlign: 'center' as const,
106
+ },
107
+ timing: {
108
+ fontSize: typography.fontSize.xs,
109
+ color: colors.text.muted,
110
+ fontFamily: typography.fontFamily.mono,
111
+ minWidth: '50px',
112
+ textAlign: 'right' as const,
113
+ },
114
+ streamingBadge: {
115
+ padding: `2px ${spacing.xs}`,
116
+ borderRadius: borderRadius.sm,
117
+ backgroundColor: `${colors.semantic.info}20`,
118
+ color: colors.semantic.info,
119
+ fontSize: typography.fontSize.xs,
120
+ fontWeight: typography.fontWeight.medium,
121
+ },
122
+ };
123
+
124
+ const methodColors: Record<string, { bg: string; color: string }> = {
125
+ GET: { bg: `${colors.semantic.success}20`, color: colors.semantic.success },
126
+ POST: { bg: `${colors.semantic.info}20`, color: colors.semantic.info },
127
+ PUT: { bg: `${colors.semantic.warning}20`, color: colors.semantic.warning },
128
+ PATCH: { bg: `${colors.semantic.warning}20`, color: colors.semantic.warning },
129
+ DELETE: { bg: `${colors.semantic.error}20`, color: colors.semantic.error },
130
+ };
131
+
132
+ type FilterType = 'all' | 'fetch' | 'sse' | 'error';
133
+
134
+ // ============================================================================
135
+ // Props
136
+ // ============================================================================
137
+
138
+ export interface NetworkPanelProps {
139
+ requests: NetworkRequest[];
140
+ }
141
+
142
+ // ============================================================================
143
+ // Component
144
+ // ============================================================================
145
+
146
+ export function NetworkPanel({ requests }: NetworkPanelProps): React.ReactElement {
147
+ const [filter, setFilter] = useState<FilterType>('all');
148
+
149
+ // Filter requests
150
+ const filteredRequests = useMemo(() => {
151
+ return requests.filter((req) => {
152
+ switch (filter) {
153
+ case 'fetch':
154
+ return !req.isStreaming;
155
+ case 'sse':
156
+ return req.isStreaming;
157
+ case 'error':
158
+ return req.status && req.status >= 400;
159
+ default:
160
+ return true;
161
+ }
162
+ });
163
+ }, [requests, filter]);
164
+
165
+ // Stats
166
+ const stats = useMemo(() => {
167
+ const total = requests.length;
168
+ const streaming = requests.filter((r) => r.isStreaming).length;
169
+ const errors = requests.filter((r) => r.status && r.status >= 400).length;
170
+ const totalTime = requests.reduce((sum, r) => {
171
+ if (r.startTime && r.endTime) {
172
+ return sum + (r.endTime - r.startTime);
173
+ }
174
+ return sum;
175
+ }, 0);
176
+
177
+ return { total, streaming, errors, totalTime };
178
+ }, [requests]);
179
+
180
+ const getStatusStyle = (status?: number): React.CSSProperties => {
181
+ if (!status) {
182
+ return { backgroundColor: colors.background.light, color: colors.text.muted };
183
+ }
184
+ if (status >= 500) {
185
+ return { backgroundColor: `${colors.semantic.error}20`, color: colors.semantic.error };
186
+ }
187
+ if (status >= 400) {
188
+ return { backgroundColor: `${colors.semantic.warning}20`, color: colors.semantic.warning };
189
+ }
190
+ if (status >= 300) {
191
+ return { backgroundColor: `${colors.semantic.info}20`, color: colors.semantic.info };
192
+ }
193
+ return { backgroundColor: `${colors.semantic.success}20`, color: colors.semantic.success };
194
+ };
195
+
196
+ const formatUrl = (url: string): string => {
197
+ try {
198
+ const parsed = new URL(url);
199
+ return parsed.pathname + parsed.search;
200
+ } catch {
201
+ return url;
202
+ }
203
+ };
204
+
205
+ const formatDuration = (start: number, end?: number): string => {
206
+ if (!end) return '...';
207
+ const duration = end - start;
208
+ if (duration < 1000) return `${duration.toFixed(0)}ms`;
209
+ return `${(duration / 1000).toFixed(1)}s`;
210
+ };
211
+
212
+ if (requests.length === 0) {
213
+ return (
214
+ <div style={styles.container}>
215
+ <div style={styles.emptyState}>
216
+ πŸ“‘
217
+ <p>
218
+ 아직 λ„€νŠΈμ›Œν¬ μš”μ²­μ΄ μ—†μ–΄μš”.<br />
219
+ API 호좜이 λ°œμƒν•˜λ©΄ 여기에 ν‘œμ‹œλ©λ‹ˆλ‹€.
220
+ </p>
221
+ </div>
222
+ </div>
223
+ );
224
+ }
225
+
226
+ return (
227
+ <div style={styles.container}>
228
+ {/* Header */}
229
+ <div style={styles.header}>
230
+ <div style={styles.filterGroup}>
231
+ {(['all', 'fetch', 'sse', 'error'] as FilterType[]).map((f) => (
232
+ <button
233
+ key={f}
234
+ style={{
235
+ ...styles.filterButton,
236
+ ...(filter === f ? styles.filterButtonActive : {}),
237
+ }}
238
+ onClick={() => setFilter(f)}
239
+ >
240
+ {f === 'all' && `전체 (${stats.total})`}
241
+ {f === 'fetch' && 'Fetch'}
242
+ {f === 'sse' && `SSE (${stats.streaming})`}
243
+ {f === 'error' && `μ—λŸ¬ (${stats.errors})`}
244
+ </button>
245
+ ))}
246
+ </div>
247
+ <div style={styles.stats}>
248
+ <span>총 μ‹œκ°„: {(stats.totalTime / 1000).toFixed(1)}s</span>
249
+ </div>
250
+ </div>
251
+
252
+ {/* Request List */}
253
+ <div style={styles.list}>
254
+ {filteredRequests.map((request) => {
255
+ const methodStyle = methodColors[request.method] ?? methodColors.GET;
256
+
257
+ return (
258
+ <div key={request.id} style={styles.requestItem}>
259
+ <span
260
+ style={{
261
+ ...styles.method,
262
+ backgroundColor: methodStyle.bg,
263
+ color: methodStyle.color,
264
+ }}
265
+ >
266
+ {request.method}
267
+ </span>
268
+
269
+ <span style={styles.url} title={request.url}>
270
+ {formatUrl(request.url)}
271
+ </span>
272
+
273
+ {request.isStreaming && (
274
+ <span style={styles.streamingBadge}>
275
+ SSE {request.chunkCount && `(${request.chunkCount})`}
276
+ </span>
277
+ )}
278
+
279
+ <span style={{ ...styles.status, ...getStatusStyle(request.status) }}>
280
+ {request.status ?? '...'}
281
+ </span>
282
+
283
+ <span style={styles.timing}>
284
+ {formatDuration(request.startTime, request.endTime)}
285
+ </span>
286
+ </div>
287
+ );
288
+ })}
289
+ </div>
290
+ </div>
291
+ );
292
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Mandu Kitchen DevTools - Panel Container
3
+ * @version 1.0.3
4
+ */
5
+
6
+ import React, { useState, useCallback } from 'react';
7
+ import { colors, typography, spacing, borderRadius, shadows, zIndex, animation, testIds } from '../../../design-tokens';
8
+ import type { KitchenState } from '../../state-manager';
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export type TabId = 'errors' | 'islands' | 'network' | 'guard';
15
+
16
+ export interface TabDefinition {
17
+ id: TabId;
18
+ label: string;
19
+ icon: string;
20
+ testId: string;
21
+ }
22
+
23
+ export interface PanelContainerProps {
24
+ state: KitchenState;
25
+ activeTab: TabId;
26
+ onTabChange: (tab: TabId) => void;
27
+ onClose: () => void;
28
+ position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
29
+ children: React.ReactNode;
30
+ }
31
+
32
+ // ============================================================================
33
+ // Constants
34
+ // ============================================================================
35
+
36
+ export const TABS: TabDefinition[] = [
37
+ { id: 'errors', label: 'μ—λŸ¬', icon: 'πŸ”₯', testId: testIds.tabErrors },
38
+ { id: 'islands', label: 'Islands', icon: '🏝️', testId: testIds.tabIslands },
39
+ { id: 'network', label: 'Network', icon: 'πŸ“‘', testId: testIds.tabNetwork },
40
+ { id: 'guard', label: 'Guard', icon: 'πŸ›‘οΈ', testId: testIds.tabGuard },
41
+ ];
42
+
43
+ // ============================================================================
44
+ // Styles
45
+ // ============================================================================
46
+
47
+ const styles = {
48
+ container: {
49
+ position: 'fixed' as const,
50
+ width: '420px',
51
+ maxHeight: '70vh',
52
+ backgroundColor: colors.background.dark,
53
+ borderRadius: borderRadius.lg,
54
+ boxShadow: shadows.xl,
55
+ display: 'flex',
56
+ flexDirection: 'column' as const,
57
+ overflow: 'hidden',
58
+ zIndex: zIndex.devtools,
59
+ fontFamily: typography.fontFamily.sans,
60
+ transition: `all ${animation.duration.normal} ${animation.easing.easeOut}`,
61
+ },
62
+ header: {
63
+ display: 'flex',
64
+ alignItems: 'center',
65
+ justifyContent: 'space-between',
66
+ padding: `${spacing.sm} ${spacing.md}`,
67
+ borderBottom: `1px solid ${colors.background.light}`,
68
+ backgroundColor: colors.background.medium,
69
+ },
70
+ title: {
71
+ display: 'flex',
72
+ alignItems: 'center',
73
+ gap: spacing.sm,
74
+ fontSize: typography.fontSize.sm,
75
+ fontWeight: typography.fontWeight.semibold,
76
+ color: colors.text.primary,
77
+ },
78
+ logo: {
79
+ fontSize: typography.fontSize.lg,
80
+ },
81
+ closeButton: {
82
+ background: 'transparent',
83
+ border: 'none',
84
+ color: colors.text.secondary,
85
+ fontSize: typography.fontSize.lg,
86
+ cursor: 'pointer',
87
+ padding: spacing.xs,
88
+ borderRadius: borderRadius.sm,
89
+ lineHeight: 1,
90
+ transition: `color ${animation.duration.fast}`,
91
+ },
92
+ tabs: {
93
+ display: 'flex',
94
+ borderBottom: `1px solid ${colors.background.light}`,
95
+ backgroundColor: colors.background.dark,
96
+ },
97
+ tab: {
98
+ flex: 1,
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ gap: spacing.xs,
103
+ padding: `${spacing.sm} ${spacing.md}`,
104
+ background: 'transparent',
105
+ border: 'none',
106
+ borderBottom: '2px solid transparent',
107
+ color: colors.text.secondary,
108
+ fontSize: typography.fontSize.sm,
109
+ cursor: 'pointer',
110
+ transition: `all ${animation.duration.fast}`,
111
+ },
112
+ tabActive: {
113
+ color: colors.brand.accent,
114
+ borderBottomColor: colors.brand.accent,
115
+ backgroundColor: colors.background.medium,
116
+ },
117
+ tabIcon: {
118
+ fontSize: typography.fontSize.md,
119
+ },
120
+ tabBadge: {
121
+ minWidth: '18px',
122
+ height: '18px',
123
+ padding: '0 4px',
124
+ borderRadius: borderRadius.full,
125
+ backgroundColor: colors.semantic.error,
126
+ color: colors.text.primary,
127
+ fontSize: typography.fontSize.xs,
128
+ fontWeight: typography.fontWeight.semibold,
129
+ display: 'flex',
130
+ alignItems: 'center',
131
+ justifyContent: 'center',
132
+ },
133
+ content: {
134
+ flex: 1,
135
+ overflow: 'auto',
136
+ minHeight: '200px',
137
+ maxHeight: '50vh',
138
+ },
139
+ resizeHandle: {
140
+ position: 'absolute' as const,
141
+ width: '100%',
142
+ height: '4px',
143
+ cursor: 'ns-resize',
144
+ backgroundColor: 'transparent',
145
+ },
146
+ };
147
+
148
+ const positionStyles: Record<string, React.CSSProperties> = {
149
+ 'bottom-right': { bottom: '80px', right: '16px' },
150
+ 'bottom-left': { bottom: '80px', left: '16px' },
151
+ 'top-right': { top: '16px', right: '16px' },
152
+ 'top-left': { top: '16px', left: '16px' },
153
+ };
154
+
155
+ // ============================================================================
156
+ // Component
157
+ // ============================================================================
158
+
159
+ export function PanelContainer({
160
+ state,
161
+ activeTab,
162
+ onTabChange,
163
+ onClose,
164
+ position,
165
+ children,
166
+ }: PanelContainerProps): React.ReactElement {
167
+ const [isResizing, setIsResizing] = useState(false);
168
+ const [height, setHeight] = useState(400);
169
+
170
+ const getTabBadgeCount = useCallback((tabId: TabId): number => {
171
+ switch (tabId) {
172
+ case 'errors':
173
+ return state.errors.filter(e => e.severity === 'error' || e.severity === 'critical').length;
174
+ case 'islands':
175
+ return Array.from(state.islands.values()).filter(i => i.status === 'error').length;
176
+ case 'guard':
177
+ return state.guardViolations.length;
178
+ default:
179
+ return 0;
180
+ }
181
+ }, [state]);
182
+
183
+ const containerStyle: React.CSSProperties = {
184
+ ...styles.container,
185
+ ...positionStyles[position],
186
+ height: `${height}px`,
187
+ };
188
+
189
+ return (
190
+ <div
191
+ data-testid={testIds.panel}
192
+ style={containerStyle}
193
+ >
194
+ {/* Resize Handle (top) */}
195
+ {position.startsWith('bottom') && (
196
+ <div
197
+ style={{ ...styles.resizeHandle, top: 0 }}
198
+ onMouseDown={() => setIsResizing(true)}
199
+ />
200
+ )}
201
+
202
+ {/* Header */}
203
+ <div style={styles.header}>
204
+ <div style={styles.title}>
205
+ <span style={styles.logo}>πŸ₯Ÿ</span>
206
+ <span>Mandu Kitchen</span>
207
+ </div>
208
+ <button
209
+ style={styles.closeButton}
210
+ onClick={onClose}
211
+ aria-label="νŒ¨λ„ λ‹«κΈ°"
212
+ >
213
+ Γ—
214
+ </button>
215
+ </div>
216
+
217
+ {/* Tabs */}
218
+ <div style={styles.tabs} role="tablist">
219
+ {TABS.map((tab) => {
220
+ const isActive = activeTab === tab.id;
221
+ const badgeCount = getTabBadgeCount(tab.id);
222
+
223
+ return (
224
+ <button
225
+ key={tab.id}
226
+ data-testid={tab.testId}
227
+ role="tab"
228
+ aria-selected={isActive}
229
+ style={{
230
+ ...styles.tab,
231
+ ...(isActive ? styles.tabActive : {}),
232
+ }}
233
+ onClick={() => onTabChange(tab.id)}
234
+ >
235
+ <span style={styles.tabIcon}>{tab.icon}</span>
236
+ <span>{tab.label}</span>
237
+ {badgeCount > 0 && (
238
+ <span style={styles.tabBadge}>{badgeCount > 99 ? '99+' : badgeCount}</span>
239
+ )}
240
+ </button>
241
+ );
242
+ })}
243
+ </div>
244
+
245
+ {/* Content */}
246
+ <div style={styles.content} role="tabpanel">
247
+ {children}
248
+ </div>
249
+
250
+ {/* Resize Handle (bottom) */}
251
+ {position.startsWith('top') && (
252
+ <div
253
+ style={{ ...styles.resizeHandle, bottom: 0 }}
254
+ onMouseDown={() => setIsResizing(true)}
255
+ />
256
+ )}
257
+ </div>
258
+ );
259
+ }