@mandujs/core 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +304 -304
- package/README.md +653 -653
- package/package.json +8 -8
- package/src/brain/architecture/analyzer.ts +28 -26
- package/src/brain/doctor/analyzer.ts +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/bundler/dev.ts +0 -1
- package/src/change/history.ts +3 -3
- package/src/change/snapshot.ts +10 -9
- package/src/change/transaction.ts +2 -2
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +94 -96
- package/src/config/validate.ts +213 -215
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/classifier.ts +2 -2
- package/src/error/domains.ts +265 -265
- package/src/error/formatter.ts +32 -32
- package/src/error/result.ts +46 -46
- package/src/error/stack-analyzer.ts +5 -0
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +569 -569
- package/src/filling/deps.ts +238 -238
- package/src/generator/contract-glue.ts +2 -1
- package/src/generator/generate.ts +12 -10
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +80 -79
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +128 -128
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/healing.ts +2 -0
- package/src/guard/index.ts +2 -0
- package/src/guard/negotiation.ts +430 -4
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/cqrs.test.ts +175 -0
- package/src/guard/presets/cqrs.ts +107 -0
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -288
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -352
- package/src/guard/types.ts +348 -347
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +1 -0
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/paths.test.ts +47 -0
- package/src/paths.ts +47 -0
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/build.ts +1 -1
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-routes.ts +344 -401
- package/src/router/fs-scanner.ts +497 -497
- package/src/router/fs-types.ts +270 -278
- package/src/router/index.ts +81 -81
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +24 -24
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +367 -367
- package/src/runtime/streaming-ssr.ts +1245 -1245
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/rules.ts +5 -5
|
@@ -1,362 +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
|
-
}
|
|
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
|
+
}
|