ruby_reactor 0.1.0 → 0.2.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -2
  3. data/README.md +72 -3
  4. data/Rakefile +27 -2
  5. data/documentation/images/failed_order_processing.png +0 -0
  6. data/documentation/images/payment_workflow.png +0 -0
  7. data/documentation/interrupts.md +161 -0
  8. data/gui/.gitignore +24 -0
  9. data/gui/README.md +73 -0
  10. data/gui/eslint.config.js +23 -0
  11. data/gui/index.html +13 -0
  12. data/gui/package-lock.json +5925 -0
  13. data/gui/package.json +46 -0
  14. data/gui/postcss.config.js +6 -0
  15. data/gui/public/vite.svg +1 -0
  16. data/gui/src/App.css +42 -0
  17. data/gui/src/App.tsx +51 -0
  18. data/gui/src/assets/react.svg +1 -0
  19. data/gui/src/components/DagVisualizer.tsx +424 -0
  20. data/gui/src/components/Dashboard.tsx +163 -0
  21. data/gui/src/components/ErrorBoundary.tsx +47 -0
  22. data/gui/src/components/ReactorDetail.tsx +135 -0
  23. data/gui/src/components/StepInspector.tsx +492 -0
  24. data/gui/src/components/__tests__/DagVisualizer.test.tsx +140 -0
  25. data/gui/src/components/__tests__/ReactorDetail.test.tsx +111 -0
  26. data/gui/src/components/__tests__/StepInspector.test.tsx +408 -0
  27. data/gui/src/globals.d.ts +7 -0
  28. data/gui/src/index.css +14 -0
  29. data/gui/src/lib/utils.ts +13 -0
  30. data/gui/src/main.tsx +14 -0
  31. data/gui/src/test/setup.ts +11 -0
  32. data/gui/tailwind.config.js +11 -0
  33. data/gui/tsconfig.app.json +28 -0
  34. data/gui/tsconfig.json +7 -0
  35. data/gui/tsconfig.node.json +26 -0
  36. data/gui/vite.config.ts +8 -0
  37. data/gui/vitest.config.ts +13 -0
  38. data/lib/ruby_reactor/async_router.rb +6 -2
  39. data/lib/ruby_reactor/context.rb +35 -9
  40. data/lib/ruby_reactor/dependency_graph.rb +2 -0
  41. data/lib/ruby_reactor/dsl/compose_builder.rb +8 -0
  42. data/lib/ruby_reactor/dsl/interrupt_builder.rb +48 -0
  43. data/lib/ruby_reactor/dsl/interrupt_step_config.rb +21 -0
  44. data/lib/ruby_reactor/dsl/map_builder.rb +8 -0
  45. data/lib/ruby_reactor/dsl/reactor.rb +12 -0
  46. data/lib/ruby_reactor/dsl/step_builder.rb +4 -0
  47. data/lib/ruby_reactor/executor/compensation_manager.rb +60 -27
  48. data/lib/ruby_reactor/executor/graph_manager.rb +2 -0
  49. data/lib/ruby_reactor/executor/result_handler.rb +117 -39
  50. data/lib/ruby_reactor/executor/retry_manager.rb +1 -0
  51. data/lib/ruby_reactor/executor/step_executor.rb +38 -4
  52. data/lib/ruby_reactor/executor.rb +86 -13
  53. data/lib/ruby_reactor/interrupt_result.rb +20 -0
  54. data/lib/ruby_reactor/map/collector.rb +0 -2
  55. data/lib/ruby_reactor/map/element_executor.rb +3 -0
  56. data/lib/ruby_reactor/map/execution.rb +28 -1
  57. data/lib/ruby_reactor/map/helpers.rb +44 -6
  58. data/lib/ruby_reactor/reactor.rb +187 -1
  59. data/lib/ruby_reactor/registry.rb +25 -0
  60. data/lib/ruby_reactor/sidekiq_workers/worker.rb +1 -1
  61. data/lib/ruby_reactor/step/compose_step.rb +22 -6
  62. data/lib/ruby_reactor/step/map_step.rb +30 -3
  63. data/lib/ruby_reactor/storage/adapter.rb +32 -0
  64. data/lib/ruby_reactor/storage/redis_adapter.rb +154 -11
  65. data/lib/ruby_reactor/utils/code_extractor.rb +31 -0
  66. data/lib/ruby_reactor/version.rb +1 -1
  67. data/lib/ruby_reactor/web/api.rb +206 -0
  68. data/lib/ruby_reactor/web/application.rb +53 -0
  69. data/lib/ruby_reactor/web/config.ru +5 -0
  70. data/lib/ruby_reactor/web/public/assets/index-VdeLgH9k.js +19 -0
  71. data/lib/ruby_reactor/web/public/assets/index-_z-6BvuM.css +1 -0
  72. data/lib/ruby_reactor/web/public/index.html +14 -0
  73. data/lib/ruby_reactor/web/public/vite.svg +1 -0
  74. data/lib/ruby_reactor.rb +94 -28
  75. data/llms-full.txt +66 -0
  76. data/llms.txt +7 -0
  77. metadata +63 -2
@@ -0,0 +1,111 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import ReactorDetail from '../ReactorDetail.tsx';
4
+ import useSWR from 'swr';
5
+
6
+ // Mock dependencies
7
+ vi.mock('swr');
8
+ vi.mock('react-router-dom', () => ({
9
+ useParams: () => ({ id: 'test-reactor-123' }),
10
+ useNavigate: () => vi.fn(),
11
+ Link: ({ children }: { children: React.ReactNode }) => <a>{children}</a>
12
+ }));
13
+
14
+ // Mock child components to isolate tests
15
+ vi.mock('../DagVisualizer.tsx', () => ({
16
+ default: ({ reactorStatus, error }: any) => (
17
+ <div data-testid="dag-visualizer">
18
+ <span data-testid="dag-status">{reactorStatus}</span>
19
+ <span data-testid="dag-error">{error?.message}</span>
20
+ </div>
21
+ )
22
+ }));
23
+
24
+ vi.mock('../StepInspector.tsx', () => ({
25
+ default: ({ error }: any) => (
26
+ <div data-testid="step-inspector">
27
+ <span data-testid="inspector-error">{error?.message}</span>
28
+ </div>
29
+ )
30
+ }));
31
+
32
+ describe('ReactorDetail', () => {
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ it('renders failure status correctly', () => {
38
+ const mockError = {
39
+ message: "Something went wrong",
40
+ step_name: "step2",
41
+ backtrace: ["line 1", "line 2"]
42
+ };
43
+
44
+ const mockData = {
45
+ id: "test-reactor-123",
46
+ class: "TestReactor",
47
+ status: "failed",
48
+ error: mockError,
49
+ inputs: {},
50
+ structure: {},
51
+ steps: []
52
+ };
53
+
54
+ // Mock useSWR response
55
+ (useSWR as any).mockReturnValue({
56
+ data: mockData,
57
+ error: null,
58
+ isLoading: false,
59
+ mutate: vi.fn()
60
+ });
61
+
62
+ render(<ReactorDetail />);
63
+
64
+ // 1. Check Status Badge
65
+ const statusBadges = screen.getAllByText('failed');
66
+ // Find the one that is the badge (has class text-red-400), not the one in the graph
67
+ const badge = statusBadges.find(el => el.classList.contains('text-red-400'));
68
+ expect(badge).toBeInTheDocument();
69
+
70
+ // 2. Check Failure Banner
71
+ expect(screen.getByText('Workflow Failed')).toBeInTheDocument();
72
+ // "Something went wrong" appears in multiple places (banner, mock props)
73
+ expect(screen.getAllByText('Something went wrong').length).toBeGreaterThan(0);
74
+
75
+ // 3. Check Props passed to DagVisualizer
76
+ expect(screen.getByTestId('dag-status')).toHaveTextContent('failed');
77
+ expect(screen.getByTestId('dag-error')).toHaveTextContent('Something went wrong');
78
+ });
79
+
80
+ it('renders success status correctly', () => {
81
+ const mockData = {
82
+ id: "test-reactor-123",
83
+ class: "TestReactor",
84
+ status: "completed",
85
+ error: null,
86
+ inputs: {},
87
+ structure: {},
88
+ steps: []
89
+ };
90
+
91
+ (useSWR as any).mockReturnValue({
92
+ data: mockData,
93
+ error: null,
94
+ isLoading: false,
95
+ mutate: vi.fn()
96
+ });
97
+
98
+ render(<ReactorDetail />);
99
+
100
+ // 1. Check Status Badge
101
+ const statusBadges = screen.getAllByText('completed');
102
+ const badge = statusBadges.find(el => el.classList.contains('text-emerald-400'));
103
+ expect(badge).toBeInTheDocument();
104
+
105
+ expect(screen.queryByText('Workflow Failed')).not.toBeInTheDocument();
106
+
107
+ // 2. Check Props passed to DagVisualizer
108
+ expect(screen.getByTestId('dag-status')).toHaveTextContent('completed');
109
+ expect(screen.getByTestId('dag-error')).toBeEmptyDOMElement();
110
+ });
111
+ });
@@ -0,0 +1,408 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { describe, it, expect } from 'vitest';
3
+ import StepInspector from '../StepInspector.tsx';
4
+
5
+ describe('StepInspector', () => {
6
+ const defaultProps = {
7
+ stepName: 'test_step',
8
+ structure: {
9
+ test_step: { type: 'step', depends_on: [] }
10
+ },
11
+ results: {},
12
+ trace: [],
13
+ inputs: {},
14
+ stepAttempts: {
15
+ test_step: 1
16
+ }
17
+ };
18
+
19
+ it('should not show retries if there is only 1 attempt', () => {
20
+ render(<StepInspector {...defaultProps} />);
21
+ expect(screen.queryByText(/Retries:/)).not.toBeInTheDocument();
22
+ });
23
+
24
+ it('should show Retries: 1 if there are 2 attempts', () => {
25
+ const props = {
26
+ ...defaultProps,
27
+ stepAttempts: {
28
+ test_step: 2
29
+ }
30
+ };
31
+ render(<StepInspector {...props} />);
32
+ expect(screen.getByText('Retries: 1')).toBeInTheDocument();
33
+ });
34
+
35
+ it('should show Retries: 2 if there are 3 attempts', () => {
36
+ const props = {
37
+ ...defaultProps,
38
+ stepAttempts: {
39
+ test_step: 3
40
+ }
41
+ };
42
+ render(<StepInspector {...props} />);
43
+ expect(screen.getByText('Retries: 2')).toBeInTheDocument();
44
+ });
45
+
46
+ it('should not show retries if stepAttempts is missing for the step', () => {
47
+ const props = {
48
+ ...defaultProps,
49
+ stepAttempts: {}
50
+ };
51
+ render(<StepInspector {...props} />);
52
+ expect(screen.queryByText(/Retries:/)).not.toBeInTheDocument();
53
+ });
54
+
55
+ describe('Compensation History', () => {
56
+ const propsWithUndo = {
57
+ ...defaultProps,
58
+ stepName: null, // Global View
59
+ trace: [
60
+ { type: 'run', step: 'step1', result: 'result1' },
61
+ { type: 'run', step: 'step2', result: 'result2' },
62
+ { type: 'compensate', step: 'step2', result: 'compensated step2' },
63
+ { type: 'undo', step: 'step1', result: 'undid step1' }
64
+ ]
65
+ };
66
+
67
+ it('should show compensation history in execution order (chronological)', () => {
68
+ render(<StepInspector {...propsWithUndo} />);
69
+
70
+ // Verify labels order (use exact match to avoid matching result blocks)
71
+ const labels = screen.queryAllByText(/^step\d$/, { selector: 'span' });
72
+ expect(labels[0]).toHaveTextContent('step2');
73
+ expect(labels[1]).toHaveTextContent('step1');
74
+
75
+ // Verify results are also in order
76
+ expect(screen.getByText(/"compensated step2"/)).toBeInTheDocument();
77
+ expect(screen.getByText(/"undid step1"/)).toBeInTheDocument();
78
+ });
79
+
80
+ it('should show the result of the undo/compensate operation', () => {
81
+ render(<StepInspector {...propsWithUndo} />);
82
+
83
+ expect(screen.getByText(/"compensated step2"/)).toBeInTheDocument();
84
+ expect(screen.getByText(/"undid step1"/)).toBeInTheDocument();
85
+ });
86
+
87
+ it('should show COMPENSATE and UNDO labels', () => {
88
+ render(<StepInspector {...propsWithUndo} />);
89
+
90
+ expect(screen.getByText('compensate')).toBeInTheDocument();
91
+ expect(screen.getByText('undo')).toBeInTheDocument();
92
+ });
93
+ });
94
+
95
+ describe('Nested Reactor Support', () => {
96
+ const mockStructure = {
97
+ sub_reactor: {
98
+ type: 'compose',
99
+ nested_structure: {
100
+ inner_step: { type: 'step' }
101
+ }
102
+ }
103
+ };
104
+
105
+ const mockComposedContexts = {
106
+ sub_reactor: {
107
+ context: {
108
+ value: {
109
+ intermediate_results: {
110
+ inner_step: 'inner_result_value',
111
+ deep_reactor: 'deep_done'
112
+ },
113
+ execution_trace: [
114
+ { step: 'inner_step', type: 'run', arguments: { val: 456 } }
115
+ ],
116
+ composed_contexts: {
117
+ deep_reactor: {
118
+ context: {
119
+ value: {
120
+ intermediate_results: {
121
+ deep_step: 'deep_result_value'
122
+ },
123
+ execution_trace: [
124
+ { step: 'deep_step', type: 'run', arguments: { deep_val: 789 } }
125
+ ]
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ };
134
+
135
+ it('correctly resolves and displays results for a nested step', () => {
136
+ render(
137
+ <StepInspector
138
+ stepName="sub_reactor.inner_step"
139
+ structure={mockStructure}
140
+ results={{}}
141
+ inputs={{}}
142
+ trace={[]}
143
+ composedContexts={mockComposedContexts}
144
+ />
145
+ );
146
+
147
+ // Should show step name (base name only)
148
+ expect(screen.getByText('inner_step')).toBeInTheDocument();
149
+
150
+ // Should show resolved result
151
+ expect(screen.getByText(/"inner_result_value"/)).toBeInTheDocument();
152
+
153
+ // Should show resolved arguments from nested trace
154
+ expect(screen.getByText(/456/)).toBeInTheDocument();
155
+ });
156
+
157
+ it('correctly resolves and displays results for a deeply nested step', () => {
158
+ render(
159
+ <StepInspector
160
+ stepName="sub_reactor.deep_reactor.deep_step"
161
+ structure={{
162
+ sub_reactor: {
163
+ type: 'compose',
164
+ nested_structure: {
165
+ deep_reactor: {
166
+ type: 'compose',
167
+ nested_structure: {
168
+ deep_step: { type: 'step' }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }}
174
+ results={{}}
175
+ inputs={{}}
176
+ trace={[]}
177
+ composedContexts={mockComposedContexts}
178
+ />
179
+ );
180
+
181
+ expect(screen.getByText('deep_step')).toBeInTheDocument();
182
+ expect(screen.getByText(/"deep_result_value"/)).toBeInTheDocument();
183
+ expect(screen.getByText(/789/)).toBeInTheDocument();
184
+ });
185
+
186
+ it('identifies failures in nested contexts', () => {
187
+ const failedNestedContexts = {
188
+ sub_reactor: {
189
+ context: {
190
+ value: {
191
+ failure_reason: { step_name: 'inner_step', message: 'Nested failure' },
192
+ intermediate_results: {},
193
+ execution_trace: []
194
+ }
195
+ }
196
+ }
197
+ };
198
+
199
+ render(
200
+ <StepInspector
201
+ stepName="sub_reactor.inner_step"
202
+ structure={mockStructure}
203
+ results={{}}
204
+ inputs={{}}
205
+ trace={[]}
206
+ composedContexts={failedNestedContexts}
207
+ />
208
+ );
209
+
210
+ expect(screen.getByText('Failure Details')).toBeInTheDocument();
211
+ expect(screen.getByText('Nested failure')).toBeInTheDocument();
212
+ });
213
+ });
214
+
215
+ describe('Recursive Compensation History', () => {
216
+ const mockComposedContexts = {
217
+ math_operation: {
218
+ context: {
219
+ value: {
220
+ execution_trace: [
221
+ { type: 'compensate', step: 'do_something', result: 'comp_val' },
222
+ { type: 'undo', step: 'multiply', result: 'undo_val' }
223
+ ],
224
+ undo_stack: [
225
+ { step_name: 'pending_step' }
226
+ ]
227
+ }
228
+ }
229
+ },
230
+ child_reactor: {
231
+ context: {
232
+ value: {
233
+ execution_trace: [
234
+ { type: 'undo', step: 'wait_for', result: 'child_undo' }
235
+ ]
236
+ }
237
+ }
238
+ }
239
+ };
240
+
241
+ const props = {
242
+ ...defaultProps,
243
+ stepName: null, // Global View
244
+ trace: [
245
+ { type: 'compensate', step: 'math_operation', result: 'math_res' },
246
+ { type: 'undo', step: 'child_reactor', result: 'child_res' }
247
+ ],
248
+ composedContexts: mockComposedContexts
249
+ };
250
+
251
+ it('should display grouped blocks for each reactor', () => {
252
+ render(<StepInspector {...props} />);
253
+
254
+ expect(screen.getByText('Root Reactor')).toBeInTheDocument();
255
+ // math_operation and child_reactor appear twice (header and step)
256
+ expect(screen.getAllByText('math_operation').length).toBe(2);
257
+ expect(screen.getAllByText('child_reactor').length).toBe(2);
258
+ });
259
+
260
+ it('should recursively collect executed undo and compensate events', () => {
261
+ render(<StepInspector {...props} />);
262
+
263
+ // Root level (found twice as discussed)
264
+ expect(screen.getAllByText('math_operation').length).toBe(2);
265
+ expect(screen.getAllByText('child_reactor').length).toBe(2);
266
+
267
+ // Nested math_operation level
268
+ expect(screen.getByText('do_something')).toBeInTheDocument();
269
+ expect(screen.getByText('multiply')).toBeInTheDocument();
270
+ expect(screen.getByText(/"comp_val"/)).toBeInTheDocument();
271
+ expect(screen.getByText(/"undo_val"/)).toBeInTheDocument();
272
+
273
+ // Nested child_reactor level
274
+ expect(screen.getByText('wait_for')).toBeInTheDocument();
275
+ expect(screen.getByText(/"child_undo"/)).toBeInTheDocument();
276
+ });
277
+
278
+ it('should recursively collect pending items from undo_stack', () => {
279
+ render(<StepInspector {...props} />);
280
+
281
+ expect(screen.getByText('pending_step')).toBeInTheDocument();
282
+ // Verify it's marked as pending
283
+ expect(screen.getAllByText('pending').length).toBeGreaterThan(0);
284
+ });
285
+ });
286
+
287
+ describe('Failure Information', () => {
288
+ const failedProps = {
289
+ ...defaultProps,
290
+ stepName: 'failed_step',
291
+ composedContexts: {
292
+ failed_step: {
293
+ context: {
294
+ value: {
295
+ status: 'failed',
296
+ failure_reason: {
297
+ message: 'Something went wrong',
298
+ step_name: 'failed_step',
299
+ exception_class: 'CustomError',
300
+ backtrace: [
301
+ 'line 1',
302
+ 'line 2',
303
+ 'line 3',
304
+ 'line 4',
305
+ 'line 5',
306
+ 'line 6',
307
+ 'line 7'
308
+ ]
309
+ },
310
+ intermediate_results: {},
311
+ execution_trace: []
312
+ }
313
+ }
314
+ }
315
+ },
316
+ error: {
317
+ message: 'Something went wrong',
318
+ step_name: 'failed_step',
319
+ exception_class: 'CustomError',
320
+ backtrace: [
321
+ 'line 1',
322
+ 'line 2',
323
+ 'line 3',
324
+ 'line 4',
325
+ 'line 5',
326
+ 'line 6',
327
+ 'line 7'
328
+ ]
329
+ }
330
+ };
331
+
332
+ it('should display the exception class', () => {
333
+ render(<StepInspector {...failedProps} />);
334
+ expect(screen.getByText('CustomError')).toBeInTheDocument();
335
+ });
336
+
337
+ it('should truncate the backtrace to 5 lines by default', () => {
338
+ render(<StepInspector {...failedProps} />);
339
+
340
+ const backtraceText = screen.getByText(/line 1/);
341
+ expect(backtraceText.textContent).toContain('line 5');
342
+ expect(backtraceText.textContent).not.toContain('line 6');
343
+
344
+ expect(screen.getByText(/Show More/)).toBeInTheDocument();
345
+ });
346
+
347
+ it('should expand the backtrace when clicking Show More', () => {
348
+ render(<StepInspector {...failedProps} />);
349
+
350
+ const showMoreButton = screen.getByText(/Show More/);
351
+ fireEvent.click(showMoreButton);
352
+
353
+ const backtraceText = screen.getByText(/line 1/);
354
+ expect(backtraceText.textContent).toContain('line 7');
355
+ expect(screen.getByText(/Show Less/)).toBeInTheDocument();
356
+ });
357
+ });
358
+
359
+ describe('Validation Errors', () => {
360
+ const validationErrorProps = {
361
+ ...defaultProps,
362
+ stepName: 'validation_step',
363
+ composedContexts: {
364
+ validation_step: {
365
+ context: {
366
+ value: {
367
+ status: 'failed',
368
+ failure_reason: {
369
+ message: 'Validation failed',
370
+ step_name: 'validation_step',
371
+ validation_errors: {
372
+ field1: ['Error 1', 'Error 2'],
373
+ field2: 'Single Error'
374
+ }
375
+ },
376
+ intermediate_results: {},
377
+ execution_trace: []
378
+ }
379
+ }
380
+ }
381
+ },
382
+ error: {
383
+ message: 'Validation failed',
384
+ step_name: 'validation_step',
385
+ validation_errors: {
386
+ field1: ['Error 1', 'Error 2'],
387
+ field2: 'Single Error'
388
+ }
389
+ }
390
+ };
391
+
392
+ it('should display validation errors section', () => {
393
+ render(<StepInspector {...validationErrorProps} />);
394
+ expect(screen.getByText('Validation Errors')).toBeInTheDocument();
395
+ });
396
+
397
+ it('should display field names and error messages', () => {
398
+ render(<StepInspector {...validationErrorProps} />);
399
+
400
+ expect(screen.getByText('field1:')).toBeInTheDocument();
401
+ expect(screen.getByText('- Error 1')).toBeInTheDocument();
402
+ expect(screen.getByText('- Error 2')).toBeInTheDocument();
403
+
404
+ expect(screen.getByText('field2:')).toBeInTheDocument();
405
+ expect(screen.getByText('- Single Error')).toBeInTheDocument();
406
+ });
407
+ });
408
+ });
@@ -0,0 +1,7 @@
1
+ export { };
2
+
3
+ declare global {
4
+ interface Window {
5
+ RUBY_REACTOR_BASE?: string;
6
+ }
7
+ }
data/gui/src/index.css ADDED
@@ -0,0 +1,14 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
2
+ @import "tailwindcss";
3
+
4
+ @theme {
5
+ --font-sans: 'Inter', system-ui, sans-serif;
6
+ --color-slate-950: #020617;
7
+ --color-slate-900: #0f172a;
8
+ --color-slate-800: #1e293b;
9
+ --color-indigo-500: #6366f1;
10
+ }
11
+
12
+ body {
13
+ @apply bg-slate-950 text-slate-50 font-sans antialiased;
14
+ }
@@ -0,0 +1,13 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export function apiUrl(path: string) {
9
+ const base = window.RUBY_REACTOR_BASE || '/';
10
+ const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
11
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
12
+ return `${normalizedBase}${normalizedPath}`;
13
+ }
data/gui/src/main.tsx ADDED
@@ -0,0 +1,14 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ import { ErrorBoundary } from './components/ErrorBoundary.tsx'
7
+
8
+ createRoot(document.getElementById('root')!).render(
9
+ <StrictMode>
10
+ <ErrorBoundary>
11
+ <App />
12
+ </ErrorBoundary>
13
+ </StrictMode>,
14
+ )
@@ -0,0 +1,11 @@
1
+ import '@testing-library/jest-dom'
2
+ // import { vi } from 'vitest'
3
+
4
+ // Polyfill ResizeObserver
5
+ class ResizeObserver {
6
+ observe() { }
7
+ unobserve() { }
8
+ disconnect() { }
9
+ }
10
+ // @ts-ignore
11
+ globalThis.ResizeObserver = ResizeObserver
@@ -0,0 +1,11 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ },
27
+ "include": ["src"]
28
+ }
data/gui/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ base: './',
8
+ })
@@ -0,0 +1,13 @@
1
+ /// <reference types="vitest" />
2
+ import { defineConfig } from 'vite'
3
+ import react from '@vitejs/plugin-react'
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ test: {
8
+ globals: true,
9
+ environment: 'jsdom',
10
+ setupFiles: './src/test/setup.ts',
11
+ css: false,
12
+ },
13
+ })