@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,244 @@
1
+ /**
2
+ * Mandu Kitchen DevTools - Guard Panel
3
+ * @version 1.0.3
4
+ */
5
+
6
+ import React, { useMemo } from 'react';
7
+ import type { GuardViolation } 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
+ },
27
+ clearButton: {
28
+ padding: `${spacing.xs} ${spacing.sm}`,
29
+ borderRadius: borderRadius.sm,
30
+ backgroundColor: colors.background.light,
31
+ border: 'none',
32
+ color: colors.text.secondary,
33
+ fontSize: typography.fontSize.xs,
34
+ cursor: 'pointer',
35
+ transition: `all ${animation.duration.fast}`,
36
+ },
37
+ list: {
38
+ flex: 1,
39
+ overflow: 'auto',
40
+ display: 'flex',
41
+ flexDirection: 'column' as const,
42
+ gap: spacing.sm,
43
+ },
44
+ emptyState: {
45
+ flex: 1,
46
+ display: 'flex',
47
+ flexDirection: 'column' as const,
48
+ alignItems: 'center',
49
+ justifyContent: 'center',
50
+ gap: spacing.md,
51
+ padding: spacing.xl,
52
+ color: colors.text.muted,
53
+ fontSize: typography.fontSize.sm,
54
+ textAlign: 'center' as const,
55
+ },
56
+ violationItem: {
57
+ padding: spacing.md,
58
+ borderRadius: borderRadius.md,
59
+ backgroundColor: colors.background.medium,
60
+ borderLeft: '3px solid',
61
+ transition: `all ${animation.duration.fast}`,
62
+ },
63
+ violationHeader: {
64
+ display: 'flex',
65
+ alignItems: 'flex-start',
66
+ justifyContent: 'space-between',
67
+ gap: spacing.sm,
68
+ marginBottom: spacing.xs,
69
+ },
70
+ ruleName: {
71
+ display: 'flex',
72
+ alignItems: 'center',
73
+ gap: spacing.xs,
74
+ },
75
+ badge: {
76
+ padding: `2px ${spacing.xs}`,
77
+ borderRadius: borderRadius.sm,
78
+ fontSize: typography.fontSize.xs,
79
+ fontWeight: typography.fontWeight.medium,
80
+ },
81
+ message: {
82
+ fontSize: typography.fontSize.sm,
83
+ color: colors.text.primary,
84
+ lineHeight: typography.lineHeight.normal,
85
+ marginBottom: spacing.sm,
86
+ },
87
+ location: {
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ gap: spacing.sm,
91
+ fontSize: typography.fontSize.xs,
92
+ fontFamily: typography.fontFamily.mono,
93
+ color: colors.text.muted,
94
+ },
95
+ arrow: {
96
+ color: colors.text.muted,
97
+ },
98
+ suggestion: {
99
+ marginTop: spacing.sm,
100
+ padding: spacing.sm,
101
+ borderRadius: borderRadius.sm,
102
+ backgroundColor: `${colors.semantic.info}10`,
103
+ borderLeft: `2px solid ${colors.semantic.info}`,
104
+ fontSize: typography.fontSize.xs,
105
+ color: colors.text.secondary,
106
+ lineHeight: typography.lineHeight.normal,
107
+ },
108
+ suggestionLabel: {
109
+ fontWeight: typography.fontWeight.semibold,
110
+ color: colors.semantic.info,
111
+ marginBottom: '4px',
112
+ },
113
+ };
114
+
115
+ const severityStyles: Record<string, { border: string; bg: string; color: string }> = {
116
+ error: {
117
+ border: colors.semantic.error,
118
+ bg: `${colors.semantic.error}20`,
119
+ color: colors.semantic.error,
120
+ },
121
+ warning: {
122
+ border: colors.semantic.warning,
123
+ bg: `${colors.semantic.warning}20`,
124
+ color: colors.semantic.warning,
125
+ },
126
+ };
127
+
128
+ // ============================================================================
129
+ // Props
130
+ // ============================================================================
131
+
132
+ export interface GuardPanelProps {
133
+ violations: GuardViolation[];
134
+ onClear: () => void;
135
+ }
136
+
137
+ // ============================================================================
138
+ // Component
139
+ // ============================================================================
140
+
141
+ export function GuardPanel({ violations, onClear }: GuardPanelProps): React.ReactElement {
142
+ // Group by rule
143
+ const groupedByRule = useMemo(() => {
144
+ const groups = new Map<string, GuardViolation[]>();
145
+ for (const v of violations) {
146
+ const existing = groups.get(v.ruleId) ?? [];
147
+ groups.set(v.ruleId, [...existing, v]);
148
+ }
149
+ return groups;
150
+ }, [violations]);
151
+
152
+ if (violations.length === 0) {
153
+ return (
154
+ <div style={styles.container}>
155
+ <div style={styles.emptyState}>
156
+ πŸ›‘οΈ
157
+ <p>
158
+ μ•„ν‚€ν…μ²˜ μœ„λ°˜μ΄ μ—†μ–΄μš”!<br />
159
+ μ½”λ“œκ°€ κ·œμΉ™μ„ 잘 λ”°λ₯΄κ³  μžˆμ–΄μš”.
160
+ </p>
161
+ </div>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ return (
167
+ <div style={styles.container}>
168
+ {/* Header */}
169
+ <div style={styles.header}>
170
+ <span style={{ fontSize: typography.fontSize.sm, color: colors.text.secondary }}>
171
+ {violations.length}개의 μœ„λ°˜ ({groupedByRule.size}개 κ·œμΉ™)
172
+ </span>
173
+ <button style={styles.clearButton} onClick={onClear}>
174
+ λͺ¨λ‘ μ§€μš°κΈ°
175
+ </button>
176
+ </div>
177
+
178
+ {/* Violation List */}
179
+ <div style={styles.list}>
180
+ {violations.map((violation) => {
181
+ const severity = severityStyles[violation.severity] ?? severityStyles.warning;
182
+
183
+ return (
184
+ <div
185
+ key={violation.id}
186
+ style={{
187
+ ...styles.violationItem,
188
+ borderLeftColor: severity.border,
189
+ }}
190
+ >
191
+ <div style={styles.violationHeader}>
192
+ <div style={styles.ruleName}>
193
+ <span
194
+ style={{
195
+ ...styles.badge,
196
+ backgroundColor: severity.bg,
197
+ color: severity.color,
198
+ }}
199
+ >
200
+ {violation.severity}
201
+ </span>
202
+ <span
203
+ style={{
204
+ ...styles.badge,
205
+ backgroundColor: colors.background.light,
206
+ color: colors.text.secondary,
207
+ }}
208
+ >
209
+ {violation.ruleName}
210
+ </span>
211
+ </div>
212
+ </div>
213
+
214
+ <p style={styles.message}>{violation.message}</p>
215
+
216
+ <div style={styles.location}>
217
+ <span>
218
+ {violation.source.file}
219
+ {violation.source.line && `:${violation.source.line}`}
220
+ </span>
221
+ {violation.target && (
222
+ <>
223
+ <span style={styles.arrow}>β†’</span>
224
+ <span>
225
+ {violation.target.file}
226
+ {violation.target.line && `:${violation.target.line}`}
227
+ </span>
228
+ </>
229
+ )}
230
+ </div>
231
+
232
+ {violation.suggestion && (
233
+ <div style={styles.suggestion}>
234
+ <div style={styles.suggestionLabel}>πŸ’‘ μ œμ•ˆ</div>
235
+ {violation.suggestion}
236
+ </div>
237
+ )}
238
+ </div>
239
+ );
240
+ })}
241
+ </div>
242
+ </div>
243
+ );
244
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Mandu Kitchen DevTools - Panel Components
3
+ * @version 1.0.3
4
+ */
5
+
6
+ export {
7
+ PanelContainer,
8
+ TABS,
9
+ type TabId,
10
+ type TabDefinition,
11
+ type PanelContainerProps,
12
+ } from './panel-container';
13
+
14
+ export {
15
+ ErrorsPanel,
16
+ type ErrorsPanelProps,
17
+ } from './errors-panel';
18
+
19
+ export {
20
+ IslandsPanel,
21
+ type IslandsPanelProps,
22
+ } from './islands-panel';
23
+
24
+ export {
25
+ NetworkPanel,
26
+ type NetworkPanelProps,
27
+ } from './network-panel';
28
+
29
+ export {
30
+ GuardPanel,
31
+ type GuardPanelProps,
32
+ } from './guard-panel';
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Mandu Kitchen DevTools - Islands Panel
3
+ * @version 1.0.3
4
+ */
5
+
6
+ import React, { useMemo } from 'react';
7
+ import type { IslandSnapshot } 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
+ },
27
+ stats: {
28
+ display: 'flex',
29
+ gap: spacing.md,
30
+ },
31
+ stat: {
32
+ display: 'flex',
33
+ alignItems: 'center',
34
+ gap: spacing.xs,
35
+ fontSize: typography.fontSize.xs,
36
+ color: colors.text.secondary,
37
+ },
38
+ statValue: {
39
+ fontWeight: typography.fontWeight.semibold,
40
+ color: colors.text.primary,
41
+ },
42
+ timeline: {
43
+ flex: 1,
44
+ overflow: 'auto',
45
+ display: 'flex',
46
+ flexDirection: 'column' as const,
47
+ gap: spacing.xs,
48
+ },
49
+ emptyState: {
50
+ flex: 1,
51
+ display: 'flex',
52
+ flexDirection: 'column' as const,
53
+ alignItems: 'center',
54
+ justifyContent: 'center',
55
+ gap: spacing.md,
56
+ padding: spacing.xl,
57
+ color: colors.text.muted,
58
+ fontSize: typography.fontSize.sm,
59
+ textAlign: 'center' as const,
60
+ },
61
+ islandItem: {
62
+ display: 'flex',
63
+ alignItems: 'center',
64
+ gap: spacing.md,
65
+ padding: spacing.sm,
66
+ borderRadius: borderRadius.md,
67
+ backgroundColor: colors.background.medium,
68
+ transition: `all ${animation.duration.fast}`,
69
+ },
70
+ islandIcon: {
71
+ fontSize: typography.fontSize.lg,
72
+ width: '28px',
73
+ textAlign: 'center' as const,
74
+ },
75
+ islandInfo: {
76
+ flex: 1,
77
+ minWidth: 0,
78
+ },
79
+ islandName: {
80
+ fontSize: typography.fontSize.sm,
81
+ fontWeight: typography.fontWeight.medium,
82
+ color: colors.text.primary,
83
+ whiteSpace: 'nowrap' as const,
84
+ overflow: 'hidden',
85
+ textOverflow: 'ellipsis',
86
+ },
87
+ islandMeta: {
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ gap: spacing.sm,
91
+ marginTop: '2px',
92
+ },
93
+ badge: {
94
+ padding: `1px ${spacing.xs}`,
95
+ borderRadius: borderRadius.sm,
96
+ fontSize: typography.fontSize.xs,
97
+ fontWeight: typography.fontWeight.medium,
98
+ },
99
+ timeBar: {
100
+ width: '80px',
101
+ height: '4px',
102
+ borderRadius: borderRadius.full,
103
+ backgroundColor: colors.background.light,
104
+ overflow: 'hidden',
105
+ },
106
+ timeBarFill: {
107
+ height: '100%',
108
+ borderRadius: borderRadius.full,
109
+ transition: `width ${animation.duration.normal}`,
110
+ },
111
+ timing: {
112
+ fontSize: typography.fontSize.xs,
113
+ color: colors.text.muted,
114
+ fontFamily: typography.fontFamily.mono,
115
+ minWidth: '50px',
116
+ textAlign: 'right' as const,
117
+ },
118
+ };
119
+
120
+ const statusConfig: Record<string, { icon: string; color: string; bg: string }> = {
121
+ ssr: { icon: 'πŸ“„', color: colors.text.muted, bg: colors.background.light },
122
+ pending: { icon: '⏳', color: colors.semantic.warning, bg: `${colors.semantic.warning}20` },
123
+ hydrating: { icon: 'πŸ’§', color: colors.semantic.info, bg: `${colors.semantic.info}20` },
124
+ hydrated: { icon: 'βœ…', color: colors.semantic.success, bg: `${colors.semantic.success}20` },
125
+ error: { icon: '❌', color: colors.semantic.error, bg: `${colors.semantic.error}20` },
126
+ };
127
+
128
+ const strategyLabels: Record<string, string> = {
129
+ load: 'μ¦‰μ‹œ',
130
+ idle: 'Idle',
131
+ visible: 'Visible',
132
+ media: 'Media',
133
+ never: 'Never',
134
+ };
135
+
136
+ // ============================================================================
137
+ // Props
138
+ // ============================================================================
139
+
140
+ export interface IslandsPanelProps {
141
+ islands: IslandSnapshot[];
142
+ }
143
+
144
+ // ============================================================================
145
+ // Component
146
+ // ============================================================================
147
+
148
+ export function IslandsPanel({ islands }: IslandsPanelProps): React.ReactElement {
149
+ // Statistics
150
+ const stats = useMemo(() => {
151
+ const total = islands.length;
152
+ const hydrated = islands.filter((i) => i.status === 'hydrated').length;
153
+ const pending = islands.filter((i) => i.status === 'pending' || i.status === 'hydrating').length;
154
+ const errors = islands.filter((i) => i.status === 'error').length;
155
+ const totalHydrateTime = islands.reduce((sum, i) => {
156
+ if (i.hydrateStartTime && i.hydrateEndTime) {
157
+ return sum + (i.hydrateEndTime - i.hydrateStartTime);
158
+ }
159
+ return sum;
160
+ }, 0);
161
+
162
+ return { total, hydrated, pending, errors, totalHydrateTime };
163
+ }, [islands]);
164
+
165
+ // Max hydration time for scaling
166
+ const maxHydrateTime = useMemo(() => {
167
+ return Math.max(
168
+ 100,
169
+ ...islands.map((i) =>
170
+ i.hydrateStartTime && i.hydrateEndTime
171
+ ? i.hydrateEndTime - i.hydrateStartTime
172
+ : 0
173
+ )
174
+ );
175
+ }, [islands]);
176
+
177
+ if (islands.length === 0) {
178
+ return (
179
+ <div style={styles.container}>
180
+ <div style={styles.emptyState}>
181
+ 🏝️
182
+ <p>
183
+ 아직 λ“±λ‘λœ Islandκ°€ μ—†μ–΄μš”.<br />
184
+ Island μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‚¬μš©ν•˜λ©΄ 여기에 ν‘œμ‹œλ©λ‹ˆλ‹€.
185
+ </p>
186
+ </div>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ return (
192
+ <div style={styles.container}>
193
+ {/* Header Stats */}
194
+ <div style={styles.header}>
195
+ <div style={styles.stats}>
196
+ <div style={styles.stat}>
197
+ <span>전체:</span>
198
+ <span style={styles.statValue}>{stats.total}</span>
199
+ </div>
200
+ <div style={styles.stat}>
201
+ <span>βœ…</span>
202
+ <span style={styles.statValue}>{stats.hydrated}</span>
203
+ </div>
204
+ {stats.pending > 0 && (
205
+ <div style={styles.stat}>
206
+ <span>⏳</span>
207
+ <span style={styles.statValue}>{stats.pending}</span>
208
+ </div>
209
+ )}
210
+ {stats.errors > 0 && (
211
+ <div style={styles.stat}>
212
+ <span>❌</span>
213
+ <span style={{ ...styles.statValue, color: colors.semantic.error }}>
214
+ {stats.errors}
215
+ </span>
216
+ </div>
217
+ )}
218
+ </div>
219
+ <div style={styles.stat}>
220
+ <span>총 μ‹œκ°„:</span>
221
+ <span style={styles.statValue}>{stats.totalHydrateTime.toFixed(0)}ms</span>
222
+ </div>
223
+ </div>
224
+
225
+ {/* Timeline */}
226
+ <div style={styles.timeline}>
227
+ {islands.map((island) => {
228
+ const status = statusConfig[island.status] ?? statusConfig.pending;
229
+ const hydrateTime =
230
+ island.hydrateStartTime && island.hydrateEndTime
231
+ ? island.hydrateEndTime - island.hydrateStartTime
232
+ : null;
233
+ const timePercent = hydrateTime ? (hydrateTime / maxHydrateTime) * 100 : 0;
234
+
235
+ return (
236
+ <div key={island.id} style={styles.islandItem}>
237
+ <span style={styles.islandIcon}>{status.icon}</span>
238
+
239
+ <div style={styles.islandInfo}>
240
+ <div style={styles.islandName}>{island.name}</div>
241
+ <div style={styles.islandMeta}>
242
+ <span
243
+ style={{
244
+ ...styles.badge,
245
+ backgroundColor: status.bg,
246
+ color: status.color,
247
+ }}
248
+ >
249
+ {island.status}
250
+ </span>
251
+ <span
252
+ style={{
253
+ ...styles.badge,
254
+ backgroundColor: colors.background.light,
255
+ color: colors.text.muted,
256
+ }}
257
+ >
258
+ {strategyLabels[island.strategy] ?? island.strategy}
259
+ </span>
260
+ {island.bundleSize && (
261
+ <span
262
+ style={{
263
+ ...styles.badge,
264
+ backgroundColor: colors.background.light,
265
+ color: colors.text.muted,
266
+ }}
267
+ >
268
+ {(island.bundleSize / 1024).toFixed(1)}KB
269
+ </span>
270
+ )}
271
+ </div>
272
+ </div>
273
+
274
+ {/* Time Bar */}
275
+ {hydrateTime !== null && (
276
+ <>
277
+ <div style={styles.timeBar}>
278
+ <div
279
+ style={{
280
+ ...styles.timeBarFill,
281
+ width: `${timePercent}%`,
282
+ backgroundColor:
283
+ hydrateTime > 100
284
+ ? colors.semantic.warning
285
+ : colors.semantic.success,
286
+ }}
287
+ />
288
+ </div>
289
+ <span style={styles.timing}>{hydrateTime.toFixed(0)}ms</span>
290
+ </>
291
+ )}
292
+
293
+ {hydrateTime === null && island.status === 'hydrating' && (
294
+ <span style={{ ...styles.timing, color: colors.semantic.info }}>
295
+ 진행쀑...
296
+ </span>
297
+ )}
298
+ </div>
299
+ );
300
+ })}
301
+ </div>
302
+ </div>
303
+ );
304
+ }