@rozenite/vite-plugin 1.12.0 → 1.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.
Files changed (40) hide show
  1. package/package.json +2 -1
  2. package/src/bundle-dts.ts +125 -0
  3. package/src/client-plugin.ts +392 -0
  4. package/src/dev-config-module.ts +29 -0
  5. package/src/dev-host/App.tsx +567 -0
  6. package/src/dev-host/components/DispatchForm.tsx +169 -0
  7. package/src/dev-host/components/FlowList.tsx +142 -0
  8. package/src/dev-host/components/MessageDetailsPane.tsx +109 -0
  9. package/src/dev-host/components/MessageLogPane.tsx +84 -0
  10. package/src/dev-host/components/MessagePayloadDetail.tsx +32 -0
  11. package/src/dev-host/components/PanelTabs.tsx +22 -0
  12. package/src/dev-host/components/ResizeHandle.tsx +33 -0
  13. package/src/dev-host/components/icons.tsx +79 -0
  14. package/src/dev-host/components/ui/Button.tsx +91 -0
  15. package/src/dev-host/components/ui/DropdownMenu.tsx +84 -0
  16. package/src/dev-host/components/ui/IconButton.tsx +41 -0
  17. package/src/dev-host/components/ui/Input.tsx +73 -0
  18. package/src/dev-host/components/ui/ScrollArea.tsx +20 -0
  19. package/src/dev-host/components/ui/Tabs.tsx +166 -0
  20. package/src/dev-host/components/ui/Textarea.tsx +79 -0
  21. package/src/dev-host/components/ui/ToggleGroup.tsx +120 -0
  22. package/src/dev-host/config.ts +107 -0
  23. package/src/dev-host/constants.ts +31 -0
  24. package/src/dev-host/flow-runtime.ts +297 -0
  25. package/src/dev-host/index.html +12 -0
  26. package/src/dev-host/main.tsx +31 -0
  27. package/src/dev-host/server.ts +55 -0
  28. package/src/dev-host/styles.css +969 -0
  29. package/src/dev-host/types.ts +61 -0
  30. package/src/dev-host/utils.ts +96 -0
  31. package/src/dev-host/vite.config.mts +20 -0
  32. package/src/index.ts +114 -0
  33. package/src/load-config.ts +117 -0
  34. package/src/package-json.ts +16 -0
  35. package/src/react-native-plugin.ts +49 -0
  36. package/src/require-plugin.ts +221 -0
  37. package/src/sdk-plugin.ts +44 -0
  38. package/src/server-plugin.ts +36 -0
  39. package/src/utils.ts +31 -0
  40. package/src/virtual-modules.d.ts +7 -0
@@ -0,0 +1,567 @@
1
+ import {
2
+ useEffect,
3
+ useRef,
4
+ useState,
5
+ type CSSProperties,
6
+ type FormEvent,
7
+ type PointerEvent as ReactPointerEvent,
8
+ } from 'react';
9
+ import {
10
+ DEFAULT_COMMAND_WIDTH,
11
+ DEFAULT_DEVTOOLS_HEIGHT,
12
+ DETAILS_PANEL_WIDTH,
13
+ MIN_COMMAND_WIDTH,
14
+ MIN_DETAILS_WIDTH,
15
+ MIN_DEVTOOLS_HEIGHT,
16
+ MIN_IFRAME_HEIGHT,
17
+ MIN_NARROW_IFRAME_HEIGHT,
18
+ SPLITTER_SIZE,
19
+ } from './constants.js';
20
+ import type {
21
+ DevHostFlowEntry,
22
+ DevHostPanelEntry,
23
+ DevHostPresetEntry,
24
+ DevHostState,
25
+ MessageEntry,
26
+ ResizeHandleId,
27
+ ResizeSession,
28
+ } from './types.js';
29
+ import { useFlowRunner } from './flow-runtime.js';
30
+ import {
31
+ clamp,
32
+ createMessageEntry,
33
+ formatPayloadForCommandInput,
34
+ getInitialPanel,
35
+ isPluginMessage,
36
+ } from './utils.js';
37
+ import { DispatchForm } from './components/DispatchForm.js';
38
+ import {
39
+ MessageDetailsPane,
40
+ getDispatcherValuesFromMessage,
41
+ } from './components/MessageDetailsPane.js';
42
+ import { MessageLogPane } from './components/MessageLogPane.js';
43
+ import { PanelTabs } from './components/PanelTabs.js';
44
+ import { ResizeHandle } from './components/ResizeHandle.js';
45
+ import { ToggleGroup } from './components/ui/ToggleGroup.js';
46
+ import { RozeniteLogo } from './components/icons.js';
47
+
48
+ type CSSVariables = CSSProperties & Record<`--${string}`, string>;
49
+
50
+ type AppProps = DevHostState & {
51
+ flows: DevHostFlowEntry[];
52
+ presets: DevHostPresetEntry[];
53
+ };
54
+
55
+ type MobileDevtoolsTab = 'log' | 'actions';
56
+
57
+ const getViewportMatch = () => {
58
+ return window.matchMedia('(max-width: 960px)').matches;
59
+ };
60
+
61
+ export const App = ({ packageName, packageDescription, panels, flows, presets }: AppProps) => {
62
+ const [activePanel, setActivePanel] = useState<DevHostPanelEntry | null>(() => {
63
+ return getInitialPanel(panels);
64
+ });
65
+ const [commandType, setCommandType] = useState('');
66
+ const [commandPayload, setCommandPayload] = useState('');
67
+ const [messages, setMessages] = useState<MessageEntry[]>([]);
68
+ const [selectedMessageId, setSelectedMessageId] = useState<string | null>(null);
69
+ const [isDetailsOpen, setIsDetailsOpen] = useState(false);
70
+ const [devToolsHeight, setDevToolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT);
71
+ const [commandWidth, setCommandWidth] = useState(DEFAULT_COMMAND_WIDTH);
72
+ const [detailsWidth, setDetailsWidth] = useState(DETAILS_PANEL_WIDTH);
73
+ const [activeResizeHandle, setActiveResizeHandle] = useState<ResizeHandleId | null>(null);
74
+ const [isNarrowViewport, setIsNarrowViewport] = useState(getViewportMatch);
75
+ const [activeMobileTab, setActiveMobileTab] = useState<MobileDevtoolsTab>('log');
76
+ const [iframeLoadNonce, setIframeLoadNonce] = useState(0);
77
+ const workspaceRef = useRef<HTMLElement | null>(null);
78
+ const logWorkspaceRef = useRef<HTMLDivElement | null>(null);
79
+ const devtoolsRef = useRef<HTMLElement | null>(null);
80
+ const resizeSessionRef = useRef<ResizeSession | null>(null);
81
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
82
+ const lastAutoRunLoadRef = useRef(0);
83
+ const { flowRuns, runFlow, stopFlow, hasRunningFlow, registerMessage, resetMessages } = useFlowRunner({
84
+ sendMessage: (type, payload) => {
85
+ iframeRef.current?.contentWindow?.postMessage(
86
+ {
87
+ pluginId: packageName,
88
+ type,
89
+ payload,
90
+ },
91
+ '*',
92
+ );
93
+
94
+ appendMessage({
95
+ direction: 'in',
96
+ type,
97
+ payload,
98
+ });
99
+ },
100
+ });
101
+
102
+ const activeSource = activePanel?.source ?? '';
103
+ const activeLabel = activePanel?.label ?? '';
104
+ const emptyState = panels.length === 0;
105
+ const selectedMessage = messages.find((message) => message.id === selectedMessageId) ?? null;
106
+ const trimmedCommandType = commandType.trim();
107
+ const trimmedCommandPayload = commandPayload.trim();
108
+ const hasCommandType = trimmedCommandType.length > 0;
109
+ const hasValidCommandPayload = (() => {
110
+ if (!trimmedCommandPayload) {
111
+ return false;
112
+ }
113
+
114
+ try {
115
+ JSON.parse(commandPayload);
116
+ return true;
117
+ } catch {
118
+ return false;
119
+ }
120
+ })();
121
+ const canDispatch = hasCommandType && hasValidCommandPayload;
122
+ const panelDescription = packageDescription.trim();
123
+ const isDetailsVisible = isDetailsOpen && selectedMessage !== null;
124
+ const iframeMinHeight = isNarrowViewport ? MIN_NARROW_IFRAME_HEIGHT : MIN_IFRAME_HEIGHT;
125
+
126
+ useEffect(() => {
127
+ document.title = `${packageName} Dev Host`;
128
+ }, [packageName]);
129
+
130
+ useEffect(() => {
131
+ if (iframeLoadNonce === 0) {
132
+ return;
133
+ }
134
+
135
+ if (lastAutoRunLoadRef.current === iframeLoadNonce) {
136
+ return;
137
+ }
138
+
139
+ lastAutoRunLoadRef.current = iframeLoadNonce;
140
+
141
+ flows.forEach((flow) => {
142
+ if (flow.autoRun) {
143
+ runFlow(flow, { autoRun: true });
144
+ }
145
+ });
146
+ }, [flows, iframeLoadNonce, runFlow]);
147
+
148
+ useEffect(() => {
149
+ const mediaQuery = window.matchMedia('(max-width: 960px)');
150
+
151
+ const handleChange = (event: MediaQueryListEvent) => {
152
+ setIsNarrowViewport(event.matches);
153
+ };
154
+
155
+ setIsNarrowViewport(mediaQuery.matches);
156
+ mediaQuery.addEventListener('change', handleChange);
157
+
158
+ return () => {
159
+ mediaQuery.removeEventListener('change', handleChange);
160
+ };
161
+ }, []);
162
+
163
+ useEffect(() => {
164
+ const workspace = workspaceRef.current;
165
+ if (!workspace) {
166
+ return;
167
+ }
168
+
169
+ const bounds = workspace.getBoundingClientRect();
170
+ const maxHeight = Math.max(MIN_DEVTOOLS_HEIGHT, bounds.height - iframeMinHeight - 12);
171
+
172
+ setDevToolsHeight((current) => clamp(current, MIN_DEVTOOLS_HEIGHT, maxHeight));
173
+ }, [iframeMinHeight]);
174
+
175
+ useEffect(() => {
176
+ if (!isNarrowViewport) {
177
+ return;
178
+ }
179
+
180
+ setActiveMobileTab('log');
181
+ }, [isNarrowViewport]);
182
+
183
+ const selectPanel = (value: string) => {
184
+ const nextPanel = panels.find((panel) => panel.source === value);
185
+ if (!nextPanel) {
186
+ return;
187
+ }
188
+
189
+ setActivePanel(nextPanel);
190
+ const nextUrl = new URL(window.location.href);
191
+ nextUrl.searchParams.set('panel', nextPanel.label);
192
+ window.history.replaceState(null, '', nextUrl);
193
+ };
194
+
195
+ const appendMessage = (input: Omit<MessageEntry, 'id' | 'date'>) => {
196
+ const nextEntry = createMessageEntry(input);
197
+
198
+ registerMessage(nextEntry);
199
+ setMessages((current) => [nextEntry, ...current]);
200
+ };
201
+
202
+ useEffect(() => {
203
+ const handlePointerMove = (event: PointerEvent) => {
204
+ resizeSessionRef.current?.onMove(event);
205
+ };
206
+
207
+ const stopDragging = () => {
208
+ const session = resizeSessionRef.current;
209
+ if (!session) {
210
+ return;
211
+ }
212
+
213
+ if (session.element.hasPointerCapture(session.pointerId)) {
214
+ session.element.releasePointerCapture(session.pointerId);
215
+ }
216
+
217
+ resizeSessionRef.current = null;
218
+ setActiveResizeHandle(null);
219
+ };
220
+
221
+ window.addEventListener('pointermove', handlePointerMove);
222
+ window.addEventListener('pointerup', stopDragging);
223
+ window.addEventListener('pointercancel', stopDragging);
224
+
225
+ return () => {
226
+ window.removeEventListener('pointermove', handlePointerMove);
227
+ window.removeEventListener('pointerup', stopDragging);
228
+ window.removeEventListener('pointercancel', stopDragging);
229
+ };
230
+ }, []);
231
+
232
+ const startResize = (
233
+ handleId: ResizeHandleId,
234
+ event: ReactPointerEvent<HTMLDivElement>,
235
+ onMove: (event: PointerEvent) => void,
236
+ ) => {
237
+ event.preventDefault();
238
+ event.currentTarget.setPointerCapture(event.pointerId);
239
+ resizeSessionRef.current = {
240
+ handleId,
241
+ pointerId: event.pointerId,
242
+ element: event.currentTarget,
243
+ onMove,
244
+ };
245
+ setActiveResizeHandle(handleId);
246
+ };
247
+
248
+ useEffect(() => {
249
+ const handleMessage = (event: MessageEvent) => {
250
+ if (event.source !== iframeRef.current?.contentWindow) {
251
+ return;
252
+ }
253
+
254
+ if (
255
+ typeof event.data !== 'object' ||
256
+ event.data === null ||
257
+ !('type' in event.data) ||
258
+ event.data.type !== 'rozenite-message' ||
259
+ !('payload' in event.data)
260
+ ) {
261
+ return;
262
+ }
263
+
264
+ const payload = event.data.payload;
265
+
266
+ if (!isPluginMessage(payload)) {
267
+ return;
268
+ }
269
+
270
+ appendMessage({
271
+ direction: 'out',
272
+ type: payload.type,
273
+ payload: payload.payload,
274
+ });
275
+ };
276
+
277
+ window.addEventListener('message', handleMessage);
278
+
279
+ return () => {
280
+ window.removeEventListener('message', handleMessage);
281
+ };
282
+ }, []);
283
+
284
+ const resetForm = () => {
285
+ setCommandType('');
286
+ setCommandPayload('');
287
+ };
288
+
289
+ const applyPreset = (preset: DevHostPresetEntry) => {
290
+ setCommandType(preset.type);
291
+ setCommandPayload(formatPayloadForCommandInput(preset.payload));
292
+ };
293
+
294
+ const clearMessages = () => {
295
+ resetMessages();
296
+ setMessages([]);
297
+ setSelectedMessageId(null);
298
+ setIsDetailsOpen(false);
299
+ setActiveMobileTab('log');
300
+ };
301
+
302
+ const resizeDevtoolsHeight = (event: PointerEvent) => {
303
+ const workspace = workspaceRef.current;
304
+ if (!workspace) {
305
+ return;
306
+ }
307
+
308
+ const bounds = workspace.getBoundingClientRect();
309
+ const nextHeight = bounds.bottom - event.clientY;
310
+ const maxHeight = Math.max(MIN_DEVTOOLS_HEIGHT, bounds.height - iframeMinHeight - 12);
311
+
312
+ setDevToolsHeight(clamp(nextHeight, MIN_DEVTOOLS_HEIGHT, maxHeight));
313
+ };
314
+
315
+ const resizeCommandPane = (event: PointerEvent) => {
316
+ const devtools = devtoolsRef.current;
317
+ if (!devtools) {
318
+ return;
319
+ }
320
+
321
+ const bounds = devtools.getBoundingClientRect();
322
+ const nextWidth = bounds.right - event.clientX;
323
+ const maxWidth = Math.max(MIN_COMMAND_WIDTH, bounds.width - 280);
324
+
325
+ setCommandWidth(clamp(nextWidth, MIN_COMMAND_WIDTH, maxWidth));
326
+ };
327
+
328
+ const resizeDetailsPane = (event: PointerEvent) => {
329
+ const logWorkspace = logWorkspaceRef.current;
330
+ if (!logWorkspace || !isDetailsVisible || isNarrowViewport) {
331
+ return;
332
+ }
333
+
334
+ const bounds = logWorkspace.getBoundingClientRect();
335
+ const nextWidth = bounds.right - event.clientX;
336
+ const maxWidth = Math.max(MIN_DETAILS_WIDTH, bounds.width - 280 - SPLITTER_SIZE);
337
+
338
+ setDetailsWidth(clamp(nextWidth, MIN_DETAILS_WIDTH, maxWidth));
339
+ };
340
+
341
+ const handleMessageSelect = (messageId: string) => {
342
+ setSelectedMessageId(messageId);
343
+ setIsDetailsOpen(true);
344
+
345
+ if (isNarrowViewport) {
346
+ setActiveMobileTab('log');
347
+ }
348
+ };
349
+
350
+ const handleDetailsClose = () => {
351
+ setIsDetailsOpen(false);
352
+
353
+ if (isNarrowViewport) {
354
+ setActiveMobileTab('log');
355
+ }
356
+ };
357
+
358
+ const handleMobileTabChange = (value: string) => {
359
+ const nextTab = value as MobileDevtoolsTab;
360
+ setActiveMobileTab(nextTab);
361
+ };
362
+
363
+ const handleDispatch = (event: FormEvent<HTMLFormElement>) => {
364
+ event.preventDefault();
365
+
366
+ if (!canDispatch) {
367
+ return;
368
+ }
369
+
370
+ const type = trimmedCommandType;
371
+
372
+ let payload: unknown;
373
+
374
+ try {
375
+ payload = JSON.parse(commandPayload);
376
+ } catch (error) {
377
+ window.alert(
378
+ error instanceof Error
379
+ ? `Payload must be valid JSON. ${error.message}`
380
+ : 'Payload must be valid JSON.',
381
+ );
382
+ return;
383
+ }
384
+
385
+ iframeRef.current?.contentWindow?.postMessage({ pluginId: packageName, type, payload }, '*');
386
+
387
+ appendMessage({ direction: 'in', type, payload });
388
+
389
+ resetForm();
390
+ };
391
+
392
+ return (
393
+ <div className="rz-shell">
394
+ <header className="rz-topbar">
395
+ <div className="rz-topbar-brand" aria-label="Rozenite">
396
+ <RozeniteLogo />
397
+ </div>
398
+
399
+ <div className="rz-topbar-panel-picker" title={panelDescription || undefined}>
400
+ <PanelTabs panels={panels} activeSource={activeSource} onValueChange={selectPanel} />
401
+ </div>
402
+ </header>
403
+
404
+ <main
405
+ ref={workspaceRef}
406
+ className="rz-workspace"
407
+ style={{ '--rz-devtools-height': `${devToolsHeight}px` } as CSSVariables}
408
+ >
409
+ <section className="rz-card">
410
+ {emptyState ? (
411
+ <div className="rz-empty-state">No panels were defined in rozenite.config.ts.</div>
412
+ ) : (
413
+ <iframe
414
+ key={activeSource}
415
+ ref={iframeRef}
416
+ title={activeLabel || 'Rozenite panel preview'}
417
+ src={activeSource}
418
+ className="rz-iframe"
419
+ data-resizing={activeResizeHandle === 'devtools-height'}
420
+ onLoad={() => setIframeLoadNonce((value) => value + 1)}
421
+ />
422
+ )}
423
+ </section>
424
+
425
+ <ResizeHandle
426
+ className="rz-resize-handle"
427
+ isDragging={activeResizeHandle === 'devtools-height'}
428
+ orientation="horizontal"
429
+ label="Resize DevTools"
430
+ onPointerDown={(event) => startResize('devtools-height', event, resizeDevtoolsHeight)}
431
+ />
432
+
433
+ {isNarrowViewport ? (
434
+ <section ref={devtoolsRef} className="rz-devtools-mobile">
435
+ <div className="rz-devtools-mobile-tabs">
436
+ <div className="rz-devtools-mobile-toggle">
437
+ <ToggleGroup
438
+ aria-label="DevTools sections"
439
+ value={activeMobileTab}
440
+ onChange={handleMobileTabChange}
441
+ options={[
442
+ { key: 'log', label: 'Log' },
443
+ { key: 'actions', label: 'Actions' },
444
+ ]}
445
+ />
446
+ </div>
447
+
448
+ <div className="rz-devtools-mobile-panel">
449
+ {activeMobileTab === 'log' ? (
450
+ isDetailsVisible ? (
451
+ <MessageDetailsPane
452
+ selectedMessage={selectedMessage}
453
+ isOpen={true}
454
+ isNarrowViewport={true}
455
+ activeResizeHandle={activeResizeHandle}
456
+ onClose={handleDetailsClose}
457
+ onUseMessage={(message) => {
458
+ const nextValues = getDispatcherValuesFromMessage(message);
459
+ setCommandType(nextValues.commandType);
460
+ setCommandPayload(nextValues.commandPayload);
461
+ setIsDetailsOpen(false);
462
+ setActiveMobileTab('actions');
463
+ }}
464
+ onResizeStart={(event) => startResize('details-width', event, resizeDetailsPane)}
465
+ />
466
+ ) : (
467
+ <MessageLogPane
468
+ messages={messages}
469
+ selectedMessageId={selectedMessageId}
470
+ onSelectMessage={handleMessageSelect}
471
+ onClearMessages={clearMessages}
472
+ />
473
+ )
474
+ ) : (
475
+ <DispatchForm
476
+ commandType={commandType}
477
+ commandPayload={commandPayload}
478
+ flows={flows}
479
+ flowRuns={flowRuns}
480
+ hasRunningFlow={hasRunningFlow}
481
+ presets={presets}
482
+ canDispatch={canDispatch}
483
+ onRunFlow={runFlow}
484
+ onStopFlow={stopFlow}
485
+ onCommandTypeChange={setCommandType}
486
+ onCommandPayloadChange={setCommandPayload}
487
+ onApplyPreset={applyPreset}
488
+ onReset={resetForm}
489
+ onSubmit={handleDispatch}
490
+ />
491
+ )}
492
+ </div>
493
+ </div>
494
+ </section>
495
+ ) : (
496
+ <section
497
+ ref={devtoolsRef}
498
+ className="rz-devtools"
499
+ style={
500
+ {
501
+ '--rz-command-width': `${commandWidth}px`,
502
+ '--rz-command-splitter-width': `${SPLITTER_SIZE}px`,
503
+ } as CSSVariables
504
+ }
505
+ >
506
+ <div
507
+ ref={logWorkspaceRef}
508
+ className="rz-log-workspace"
509
+ style={
510
+ {
511
+ '--rz-details-width': isDetailsVisible ? `${detailsWidth}px` : '0px',
512
+ '--rz-details-splitter-width': isDetailsVisible ? `${SPLITTER_SIZE}px` : '0px',
513
+ } as CSSVariables
514
+ }
515
+ >
516
+ <MessageLogPane
517
+ messages={messages}
518
+ selectedMessageId={selectedMessageId}
519
+ onSelectMessage={handleMessageSelect}
520
+ onClearMessages={clearMessages}
521
+ />
522
+
523
+ <MessageDetailsPane
524
+ selectedMessage={selectedMessage}
525
+ isOpen={isDetailsOpen}
526
+ isNarrowViewport={false}
527
+ activeResizeHandle={activeResizeHandle}
528
+ onClose={handleDetailsClose}
529
+ onUseMessage={(message) => {
530
+ const nextValues = getDispatcherValuesFromMessage(message);
531
+ setCommandType(nextValues.commandType);
532
+ setCommandPayload(nextValues.commandPayload);
533
+ }}
534
+ onResizeStart={(event) => startResize('details-width', event, resizeDetailsPane)}
535
+ />
536
+ </div>
537
+
538
+ <ResizeHandle
539
+ className="rz-column-resize-handle"
540
+ isDragging={activeResizeHandle === 'command-width'}
541
+ orientation="vertical"
542
+ label="Resize command dispatcher"
543
+ onPointerDown={(event) => startResize('command-width', event, resizeCommandPane)}
544
+ />
545
+
546
+ <DispatchForm
547
+ commandType={commandType}
548
+ commandPayload={commandPayload}
549
+ flows={flows}
550
+ flowRuns={flowRuns}
551
+ hasRunningFlow={hasRunningFlow}
552
+ presets={presets}
553
+ canDispatch={canDispatch}
554
+ onRunFlow={runFlow}
555
+ onStopFlow={stopFlow}
556
+ onCommandTypeChange={setCommandType}
557
+ onCommandPayloadChange={setCommandPayload}
558
+ onApplyPreset={applyPreset}
559
+ onReset={resetForm}
560
+ onSubmit={handleDispatch}
561
+ />
562
+ </section>
563
+ )}
564
+ </main>
565
+ </div>
566
+ );
567
+ };