@rozenite/vite-plugin 1.12.0 → 2.0.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 (47) hide show
  1. package/README.md +12 -0
  2. package/dist/bundle-dts.d.ts.map +1 -1
  3. package/dist/client-plugin.d.ts.map +1 -1
  4. package/dist/dev-config-module.d.ts.map +1 -1
  5. package/dist/dev-host/assets/index-DU3zXL2c.css +1 -0
  6. package/dist/dev-host/assets/index-Xsb6uYhI.js +13 -0
  7. package/dist/dev-host/index.html +3 -3
  8. package/dist/dev-host/manifest.json +2 -2
  9. package/dist/index.cjs +24 -8
  10. package/dist/index.d.ts +9 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +24 -8
  13. package/dist/react-native-plugin.d.ts.map +1 -1
  14. package/dist/sdk-plugin.d.ts.map +1 -1
  15. package/dist/server-plugin.d.ts.map +1 -1
  16. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  17. package/package.json +10 -10
  18. package/src/bundle-dts.ts +126 -0
  19. package/src/client-plugin.ts +394 -0
  20. package/src/dev-config-module.ts +32 -0
  21. package/src/dev-host/App.tsx +373 -0
  22. package/src/dev-host/components/DispatchForm.tsx +155 -0
  23. package/src/dev-host/components/FlowList.tsx +122 -0
  24. package/src/dev-host/components/MessageDetailsPane.tsx +82 -0
  25. package/src/dev-host/components/MessageLogPane.tsx +102 -0
  26. package/src/dev-host/components/PanelTabs.tsx +41 -0
  27. package/src/dev-host/config.ts +111 -0
  28. package/src/dev-host/constants.ts +12 -0
  29. package/src/dev-host/flow-runtime.ts +318 -0
  30. package/src/dev-host/index.html +12 -0
  31. package/src/dev-host/main.tsx +22 -0
  32. package/src/dev-host/server.ts +61 -0
  33. package/src/dev-host/styles.css +301 -0
  34. package/src/dev-host/types.ts +56 -0
  35. package/src/dev-host/utils.ts +99 -0
  36. package/src/dev-host/vite.config.mts +22 -0
  37. package/src/index.ts +124 -0
  38. package/src/load-config.ts +117 -0
  39. package/src/package-json.ts +16 -0
  40. package/src/react-native-plugin.ts +52 -0
  41. package/src/require-plugin.ts +221 -0
  42. package/src/sdk-plugin.ts +47 -0
  43. package/src/server-plugin.ts +39 -0
  44. package/src/utils.ts +31 -0
  45. package/src/virtual-modules.d.ts +7 -0
  46. package/dist/dev-host/assets/index-CkxvBMpp.css +0 -1
  47. package/dist/dev-host/assets/index-cNKHRwej.js +0 -11
@@ -0,0 +1,82 @@
1
+ import { Button, JsonInspector, ScrollArea } from '@rozenite/ui';
2
+ import { Send, X } from 'lucide-react';
3
+ import type { MessageEntry } from '../types.js';
4
+ import { formatMessageDate, formatPayloadForCommandInput } from '../utils.js';
5
+
6
+ type MessageDetailsPaneProps = {
7
+ selectedMessage: MessageEntry;
8
+ onClose: () => void;
9
+ onUseMessage: (message: MessageEntry) => void;
10
+ };
11
+
12
+ export const MessageDetailsPane = ({
13
+ selectedMessage,
14
+ onClose,
15
+ onUseMessage,
16
+ }: MessageDetailsPaneProps) => {
17
+ return (
18
+ <section className="dev-host-pane">
19
+ <header className="dev-host-pane-header">
20
+ <h2 className="dev-host-pane-title">Message Details</h2>
21
+ <div className="dev-host-pane-actions">
22
+ <Button
23
+ variant="ghost"
24
+ size="icon"
25
+ onClick={() => onUseMessage(selectedMessage)}
26
+ aria-label="Use message in dispatcher"
27
+ title="Use message in dispatcher"
28
+ >
29
+ <Send />
30
+ </Button>
31
+ <Button
32
+ variant="ghost"
33
+ size="icon"
34
+ onClick={onClose}
35
+ aria-label="Close message details"
36
+ title="Close message details"
37
+ >
38
+ <X />
39
+ </Button>
40
+ </div>
41
+ </header>
42
+
43
+ <ScrollArea className="dev-host-scroll">
44
+ <div className="dev-host-details">
45
+ <div className="dev-host-detail-section">
46
+ <span className="dev-host-label">Date</span>
47
+ <div className="dev-host-detail-value">
48
+ {formatMessageDate(selectedMessage.date)}
49
+ </div>
50
+ </div>
51
+ <div className="dev-host-detail-section">
52
+ <span className="dev-host-label">Type</span>
53
+ <div className="dev-host-detail-value">{selectedMessage.type}</div>
54
+ </div>
55
+ <div className="dev-host-detail-section">
56
+ <span className="dev-host-label">Payload</span>
57
+ <div className="dev-host-detail-payload">
58
+ {typeof selectedMessage.payload === 'object' &&
59
+ selectedMessage.payload !== null ? (
60
+ <JsonInspector
61
+ data={selectedMessage.payload}
62
+ defaultExpandedDepth={2}
63
+ />
64
+ ) : (
65
+ <pre className="m-0 whitespace-pre-wrap break-words">
66
+ {formatPayloadForCommandInput(selectedMessage.payload)}
67
+ </pre>
68
+ )}
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </ScrollArea>
73
+ </section>
74
+ );
75
+ };
76
+
77
+ export const getDispatcherValuesFromMessage = (message: MessageEntry) => {
78
+ return {
79
+ commandType: message.type,
80
+ commandPayload: formatPayloadForCommandInput(message.payload),
81
+ };
82
+ };
@@ -0,0 +1,102 @@
1
+ import { Button, DataTable, ScrollArea } from '@rozenite/ui';
2
+ import { Trash2 } from 'lucide-react';
3
+ import { useMemo } from 'react';
4
+ import type { DataTableColumn } from '@rozenite/ui';
5
+ import type { MessageEntry } from '../types.js';
6
+ import { formatMessageTableDate, formatPayloadPreview } from '../utils.js';
7
+
8
+ type MessageLogPaneProps = {
9
+ messages: MessageEntry[];
10
+ onSelectMessage: (messageId: string) => void;
11
+ onClearMessages: () => void;
12
+ };
13
+
14
+ export const MessageLogPane = ({
15
+ messages,
16
+ onSelectMessage,
17
+ onClearMessages,
18
+ }: MessageLogPaneProps) => {
19
+ const columns = useMemo<DataTableColumn<MessageEntry>[]>(
20
+ () => [
21
+ {
22
+ accessorKey: 'direction',
23
+ header: 'Dir',
24
+ cell: ({ getValue }) => {
25
+ const direction = getValue<MessageEntry['direction']>();
26
+ const isSent = direction === 'in';
27
+
28
+ return (
29
+ <span
30
+ className={
31
+ isSent ? 'dev-host-direction-in' : 'dev-host-direction-out'
32
+ }
33
+ aria-label={isSent ? 'Sent message' : 'Received message'}
34
+ title={isSent ? 'Sent message' : 'Received message'}
35
+ >
36
+ {isSent ? '↑' : '↓'}
37
+ </span>
38
+ );
39
+ },
40
+ },
41
+ {
42
+ accessorKey: 'date',
43
+ header: 'Date',
44
+ cell: ({ row }) => (
45
+ <span className="text-muted-foreground">
46
+ {formatMessageTableDate(row.original.date)}
47
+ </span>
48
+ ),
49
+ },
50
+ {
51
+ accessorKey: 'type',
52
+ header: 'Type',
53
+ cell: ({ getValue }) => (
54
+ <span className="font-medium">{getValue<string>()}</span>
55
+ ),
56
+ },
57
+ {
58
+ id: 'payload',
59
+ accessorFn: (message) => formatPayloadPreview(message.payload),
60
+ header: 'Payload',
61
+ cell: ({ getValue }) => (
62
+ <span className="block max-w-160 truncate font-mono text-xs text-muted-foreground">
63
+ {getValue<string>()}
64
+ </span>
65
+ ),
66
+ },
67
+ ],
68
+ [],
69
+ );
70
+
71
+ return (
72
+ <section className="dev-host-pane">
73
+ <header className="dev-host-pane-header">
74
+ <h2 className="dev-host-pane-title">Message Log</h2>
75
+ <Button
76
+ variant="ghost"
77
+ size="icon"
78
+ onClick={onClearMessages}
79
+ disabled={messages.length === 0}
80
+ aria-label="Clear message log"
81
+ title="Clear message log"
82
+ >
83
+ <Trash2 />
84
+ </Button>
85
+ </header>
86
+
87
+ <ScrollArea
88
+ className="dev-host-scroll"
89
+ viewportClassName="dev-host-scroll-viewport"
90
+ >
91
+ <DataTable
92
+ className="dev-host-message-table"
93
+ columns={columns}
94
+ data={messages}
95
+ emptyMessage="No messages yet."
96
+ getRowId={(message) => message.id}
97
+ onRowClick={(message) => onSelectMessage(message.id)}
98
+ />
99
+ </ScrollArea>
100
+ </section>
101
+ );
102
+ };
@@ -0,0 +1,41 @@
1
+ import { Select } from '@rozenite/ui';
2
+ import type { DevHostPanelEntry } from '../types.js';
3
+
4
+ type PanelTabsProps = {
5
+ panels: DevHostPanelEntry[];
6
+ activeSource: string;
7
+ onValueChange: (value: string) => void;
8
+ };
9
+
10
+ export const PanelTabs = ({
11
+ panels,
12
+ activeSource,
13
+ onValueChange,
14
+ }: PanelTabsProps) => {
15
+ if (panels.length === 0) {
16
+ return null;
17
+ }
18
+
19
+ const activePanel = panels.find((panel) => panel.source === activeSource);
20
+
21
+ return (
22
+ <Select
23
+ value={activeSource}
24
+ onValueChange={(value) => onValueChange(String(value))}
25
+ >
26
+ <Select.Trigger
27
+ className="dev-host-panel-select"
28
+ aria-label="Plugin panel"
29
+ >
30
+ <Select.Value>{activePanel?.label}</Select.Value>
31
+ </Select.Trigger>
32
+ <Select.Content align="end">
33
+ {panels.map((panel) => (
34
+ <Select.Item key={panel.source} value={panel.source}>
35
+ {panel.label}
36
+ </Select.Item>
37
+ ))}
38
+ </Select.Content>
39
+ </Select>
40
+ );
41
+ };
@@ -0,0 +1,111 @@
1
+ import type { DevFlowEntry, DevPresetEntry } from '../load-config.js';
2
+ import { DEV_HOST_CONFIG_GLOBAL_KEY } from './constants.js';
3
+ import type { DevHostFlowEntry, DevHostPresetEntry } from './types.js';
4
+
5
+ type DevHostPresetSource = Omit<DevPresetEntry, 'name'> & {
6
+ name?: string;
7
+ };
8
+
9
+ type DevHostFlowSource = Omit<DevFlowEntry, 'name'> & {
10
+ name?: string;
11
+ };
12
+
13
+ const getEntryDisplayName = (value: unknown, fallback: string) => {
14
+ if (typeof value === 'string' && value.trim()) {
15
+ return value.trim();
16
+ }
17
+
18
+ return fallback;
19
+ };
20
+
21
+ const getDevHostConfig = () => {
22
+ const value = (window as unknown as Record<string, unknown>)[
23
+ DEV_HOST_CONFIG_GLOBAL_KEY
24
+ ];
25
+
26
+ if (typeof value !== 'object' || value === null) {
27
+ return {} as { presets?: unknown; flows?: unknown };
28
+ }
29
+
30
+ return value as { presets?: unknown; flows?: unknown };
31
+ };
32
+
33
+ const isDevHostPresetSource = (
34
+ value: unknown,
35
+ ): value is DevHostPresetSource => {
36
+ return (
37
+ typeof value === 'object' &&
38
+ value !== null &&
39
+ (!('name' in value) || typeof value.name === 'string') &&
40
+ 'type' in value &&
41
+ typeof value.type === 'string' &&
42
+ 'payload' in value
43
+ );
44
+ };
45
+
46
+ const isDevHostFlowSource = (value: unknown): value is DevHostFlowSource => {
47
+ return (
48
+ typeof value === 'object' &&
49
+ value !== null &&
50
+ (!('name' in value) || typeof value.name === 'string') &&
51
+ (!('autoRun' in value) || typeof value.autoRun === 'boolean') &&
52
+ 'run' in value &&
53
+ typeof value.run === 'function'
54
+ );
55
+ };
56
+
57
+ const toDevHostPresetEntry = (value: unknown): DevHostPresetEntry | null => {
58
+ if (!isDevHostPresetSource(value)) {
59
+ return null;
60
+ }
61
+
62
+ const displayName = getEntryDisplayName(value.name, 'Untitled preset');
63
+
64
+ return {
65
+ name: displayName,
66
+ displayName,
67
+ type: value.type,
68
+ payload: value.payload,
69
+ };
70
+ };
71
+
72
+ const toDevHostFlowEntry = (value: unknown): DevHostFlowEntry | null => {
73
+ if (!isDevHostFlowSource(value)) {
74
+ return null;
75
+ }
76
+
77
+ const displayName = getEntryDisplayName(value.name, 'Untitled flow');
78
+
79
+ return {
80
+ name: displayName,
81
+ displayName,
82
+ autoRun: value.autoRun ?? false,
83
+ run: value.run,
84
+ };
85
+ };
86
+
87
+ export const getDevHostPresets = (): DevHostPresetEntry[] => {
88
+ const presets = getDevHostConfig().presets;
89
+
90
+ if (!Array.isArray(presets)) {
91
+ return [];
92
+ }
93
+
94
+ return presets.flatMap((preset) => {
95
+ const entry = toDevHostPresetEntry(preset);
96
+ return entry ? [entry] : [];
97
+ });
98
+ };
99
+
100
+ export const getDevHostFlows = (): DevHostFlowEntry[] => {
101
+ const flows = getDevHostConfig().flows;
102
+
103
+ if (!Array.isArray(flows)) {
104
+ return [];
105
+ }
106
+
107
+ return flows.flatMap((flow) => {
108
+ const entry = toDevHostFlowEntry(flow);
109
+ return entry ? [entry] : [];
110
+ });
111
+ };
@@ -0,0 +1,12 @@
1
+ export const DEV_HOST_STATE_ELEMENT_ID = '__rozenite-dev-host__';
2
+ export const DEV_HOST_CONFIG_GLOBAL_KEY = '__rozenite-dev-config__';
3
+
4
+ export const DEFAULT_DEVTOOLS_HEIGHT = 288;
5
+ export const MIN_DEVTOOLS_HEIGHT = 180;
6
+ export const MIN_IFRAME_HEIGHT = 220;
7
+ export const MIN_NARROW_IFRAME_HEIGHT = 140;
8
+ export const DETAILS_PANEL_WIDTH = 360;
9
+ export const MIN_DETAILS_WIDTH = 280;
10
+ export const DEFAULT_COMMAND_WIDTH = 320;
11
+ export const MIN_COMMAND_WIDTH = 260;
12
+ export const SPLITTER_SIZE = 6;
@@ -0,0 +1,318 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type {
3
+ DevFlowContext,
4
+ DevFlowMessage,
5
+ DevFlowMessageMatcher,
6
+ } from '../load-config.js';
7
+ import type {
8
+ DevHostFlowEntry,
9
+ DevHostFlowRunState,
10
+ MessageEntry,
11
+ } from './types.js';
12
+
13
+ type FlowRunnerOptions = {
14
+ sendMessage: (type: string, payload: unknown) => void;
15
+ };
16
+
17
+ type ActiveFlowRun = {
18
+ id: string;
19
+ flowName: string;
20
+ flowDisplayName: string;
21
+ autoRun: boolean;
22
+ controller: AbortController;
23
+ listeners: Map<number, (message: DevFlowMessage) => void>;
24
+ cleanups: Set<() => void>;
25
+ };
26
+
27
+ const createAbortError = () => {
28
+ return new DOMException('Flow execution was stopped.', 'AbortError');
29
+ };
30
+
31
+ const isAbortError = (error: unknown) => {
32
+ return error instanceof DOMException
33
+ ? error.name === 'AbortError'
34
+ : error instanceof Error && error.name === 'AbortError';
35
+ };
36
+
37
+ const toFlowMessage = (message: MessageEntry): DevFlowMessage => {
38
+ return {
39
+ id: message.id,
40
+ direction: message.direction,
41
+ date: message.date,
42
+ type: message.type,
43
+ payload: message.payload,
44
+ };
45
+ };
46
+
47
+ const matchesMessage = (
48
+ message: DevFlowMessage,
49
+ matcher?: DevFlowMessageMatcher,
50
+ ) => {
51
+ if (!matcher) {
52
+ return true;
53
+ }
54
+
55
+ if (typeof matcher === 'string') {
56
+ return message.type === matcher;
57
+ }
58
+
59
+ if (typeof matcher === 'function') {
60
+ return matcher(message);
61
+ }
62
+
63
+ return (
64
+ (matcher.type == null || message.type === matcher.type) &&
65
+ (matcher.direction == null || message.direction === matcher.direction) &&
66
+ (matcher.predicate == null || matcher.predicate(message))
67
+ );
68
+ };
69
+
70
+ const formatFlowError = (error: unknown) => {
71
+ if (isAbortError(error)) {
72
+ return 'Flow execution was stopped.';
73
+ }
74
+
75
+ if (error instanceof Error) {
76
+ return error.message;
77
+ }
78
+
79
+ return String(error);
80
+ };
81
+
82
+ export const useFlowRunner = ({ sendMessage }: FlowRunnerOptions) => {
83
+ const [flowRuns, setFlowRuns] = useState<DevHostFlowRunState[]>([]);
84
+ const messagesRef = useRef<MessageEntry[]>([]);
85
+ const activeRunsRef = useRef<Map<string, ActiveFlowRun>>(new Map());
86
+ const listenerIdRef = useRef(0);
87
+
88
+ useEffect(() => {
89
+ return () => {
90
+ activeRunsRef.current.forEach((run) => {
91
+ run.controller.abort();
92
+ run.cleanups.forEach((cleanup) => cleanup());
93
+ });
94
+ activeRunsRef.current.clear();
95
+ };
96
+ }, []);
97
+
98
+ const stopFlow = (runId: string) => {
99
+ const activeRun = activeRunsRef.current.get(runId);
100
+ if (!activeRun) {
101
+ return;
102
+ }
103
+
104
+ activeRun.controller.abort();
105
+ };
106
+
107
+ const registerMessage = (message: MessageEntry) => {
108
+ messagesRef.current = [message, ...messagesRef.current];
109
+
110
+ const flowMessage = toFlowMessage(message);
111
+ activeRunsRef.current.forEach((activeRun) => {
112
+ activeRun.listeners.forEach((listener) => {
113
+ listener(flowMessage);
114
+ });
115
+ });
116
+ };
117
+
118
+ const resetMessages = () => {
119
+ messagesRef.current = [];
120
+ };
121
+
122
+ const runFlow = (flow: DevHostFlowEntry, options?: { autoRun?: boolean }) => {
123
+ const duplicateRun = [...activeRunsRef.current.values()].find(
124
+ (run) => run.flowName === flow.name,
125
+ );
126
+
127
+ if (duplicateRun) {
128
+ return duplicateRun.id;
129
+ }
130
+
131
+ const runId = crypto.randomUUID();
132
+ const controller = new AbortController();
133
+ const activeRun: ActiveFlowRun = {
134
+ id: runId,
135
+ flowName: flow.name,
136
+ flowDisplayName: flow.displayName,
137
+ autoRun: options?.autoRun ?? flow.autoRun,
138
+ controller,
139
+ listeners: new Map(),
140
+ cleanups: new Set(),
141
+ };
142
+
143
+ const throwIfAborted = () => {
144
+ if (controller.signal.aborted) {
145
+ throw createAbortError();
146
+ }
147
+ };
148
+
149
+ const cleanupRun = () => {
150
+ activeRun.cleanups.forEach((cleanup) => cleanup());
151
+ activeRun.cleanups.clear();
152
+ activeRun.listeners.clear();
153
+ activeRunsRef.current.delete(runId);
154
+ };
155
+
156
+ const onMessage: DevFlowContext['onMessage'] = (matcher, listener) => {
157
+ throwIfAborted();
158
+
159
+ const listenerId = listenerIdRef.current + 1;
160
+ listenerIdRef.current = listenerId;
161
+
162
+ const wrappedListener = (message: DevFlowMessage) => {
163
+ if (matchesMessage(message, matcher)) {
164
+ listener(message);
165
+ }
166
+ };
167
+
168
+ activeRun.listeners.set(listenerId, wrappedListener);
169
+
170
+ const remove = () => {
171
+ activeRun.listeners.delete(listenerId);
172
+ controller.signal.removeEventListener('abort', remove);
173
+ activeRun.cleanups.delete(remove);
174
+ };
175
+
176
+ controller.signal.addEventListener('abort', remove, { once: true });
177
+ activeRun.cleanups.add(remove);
178
+
179
+ return { remove };
180
+ };
181
+
182
+ const flowContext: DevFlowContext = {
183
+ signal: controller.signal,
184
+ send: (type, payload) => {
185
+ throwIfAborted();
186
+ sendMessage(type, payload);
187
+ },
188
+ onMessage,
189
+ waitForMessage: (matcher, options) => {
190
+ throwIfAborted();
191
+
192
+ return new Promise((resolve, reject) => {
193
+ let timeoutId: number | null = null;
194
+
195
+ const cleanup = () => {
196
+ subscription.remove();
197
+ controller.signal.removeEventListener('abort', handleAbort);
198
+
199
+ if (timeoutId !== null) {
200
+ window.clearTimeout(timeoutId);
201
+ }
202
+ };
203
+
204
+ const handleAbort = () => {
205
+ cleanup();
206
+ reject(createAbortError());
207
+ };
208
+
209
+ const subscription = onMessage(matcher, (message) => {
210
+ cleanup();
211
+ resolve(message);
212
+ });
213
+
214
+ controller.signal.addEventListener('abort', handleAbort, {
215
+ once: true,
216
+ });
217
+
218
+ if (options?.timeoutMs != null) {
219
+ timeoutId = window.setTimeout(() => {
220
+ cleanup();
221
+ reject(
222
+ new Error(
223
+ `Timed out waiting for a matching message after ${options.timeoutMs}ms.`,
224
+ ),
225
+ );
226
+ }, options.timeoutMs);
227
+ }
228
+ });
229
+ },
230
+ getMessages: (matcher) => {
231
+ throwIfAborted();
232
+
233
+ return messagesRef.current
234
+ .map(toFlowMessage)
235
+ .filter((message) => matchesMessage(message, matcher));
236
+ },
237
+ };
238
+
239
+ activeRunsRef.current.set(runId, activeRun);
240
+ setFlowRuns((current) => [
241
+ {
242
+ id: runId,
243
+ flowName: flow.name,
244
+ flowDisplayName: flow.displayName,
245
+ status: 'running',
246
+ result: null,
247
+ error: null,
248
+ autoRun: activeRun.autoRun,
249
+ },
250
+ ...current,
251
+ ]);
252
+
253
+ const updateRunState = (
254
+ nextState: Partial<
255
+ Omit<
256
+ DevHostFlowRunState,
257
+ 'id' | 'flowName' | 'flowDisplayName' | 'autoRun'
258
+ >
259
+ >,
260
+ ) => {
261
+ setFlowRuns((current) =>
262
+ current.map((run) =>
263
+ run.id === runId
264
+ ? {
265
+ ...run,
266
+ ...nextState,
267
+ }
268
+ : run,
269
+ ),
270
+ );
271
+ };
272
+
273
+ Promise.resolve(flow.run(flowContext))
274
+ .then((result) => {
275
+ if (controller.signal.aborted) {
276
+ updateRunState({
277
+ status: 'aborted',
278
+ result: null,
279
+ error: 'Flow execution was stopped.',
280
+ });
281
+ return;
282
+ }
283
+
284
+ updateRunState({
285
+ status: 'succeeded',
286
+ result,
287
+ error: null,
288
+ });
289
+ })
290
+ .catch((error) => {
291
+ updateRunState({
292
+ status: isAbortError(error) ? 'aborted' : 'failed',
293
+ result: null,
294
+ error: formatFlowError(error),
295
+ });
296
+ })
297
+ .finally(() => {
298
+ cleanupRun();
299
+ });
300
+
301
+ return runId;
302
+ };
303
+
304
+ const hasRunningFlow = (flowName: string) => {
305
+ return flowRuns.some(
306
+ (run) => run.flowName === flowName && run.status === 'running',
307
+ );
308
+ };
309
+
310
+ return {
311
+ flowRuns,
312
+ runFlow,
313
+ stopFlow,
314
+ hasRunningFlow,
315
+ registerMessage,
316
+ resetMessages,
317
+ };
318
+ };
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Rozenite Dev Host</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,22 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { App } from './App.js';
4
+ import { getDevHostFlows, getDevHostPresets } from './config.js';
5
+ import './styles.css';
6
+ import { readDevHostState } from './utils.js';
7
+
8
+ const rootElement = document.getElementById('root');
9
+
10
+ if (!rootElement) {
11
+ throw new Error('Rozenite dev host failed to initialize.');
12
+ }
13
+
14
+ const state = readDevHostState();
15
+ const flows = getDevHostFlows();
16
+ const presets = getDevHostPresets();
17
+
18
+ createRoot(rootElement).render(
19
+ <StrictMode>
20
+ <App {...state} flows={flows} presets={presets} />
21
+ </StrictMode>,
22
+ );