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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70ff13eac6ccfdaafcad7bd36db33adf6efbf91e84d22967527c84625373b921
4
- data.tar.gz: a6c599b3967660e6761e92caf071f3d81b9a0c9db074261a8e79322b8955644c
3
+ metadata.gz: 598c9e49f00183da45e7b370c394445e9efddfe5952e3aabd701bbf954aa9712
4
+ data.tar.gz: 949ed81bd787d7bee47284e7f7b5905af3de4ae4057a90a4d0afa707959f5cbb
5
5
  SHA512:
6
- metadata.gz: 32326a6f81978625cc3447284b023e19c79b5b051adb115fe13291112f2db9b815a06dedef990ee9b903e93288023d0a97b5f319da6d01fe23229bfe9e5ab47a
7
- data.tar.gz: 17f9361bd3222c82d4f098acb87a16244260c41b9ef13d990986351905d6ae7968448ce8639f460616a56c7fbdfeca64b789fbccbd4253e4080403581cbe0754
6
+ metadata.gz: ae8f8f8a2916254068757d79d352df0a0148d4693f92238b456cecc1cb90d9f6cc77087f2b486e3359f5f0042779aecc9112df1da9898fa8efba6c9ed425ef39
7
+ data.tar.gz: 14fcbf6f880396176503008239e6517415977b19193f3f0d8600226acb7cf53d54a18bb79a42012646a1746d3202a62b73479715b6d088f69c208ce1c143aa92
data/.rubocop.yml CHANGED
@@ -13,6 +13,7 @@ AllCops:
13
13
  - bin/*
14
14
  - db/**/*
15
15
  - config/**/*
16
+ - demo_app/**/*
16
17
 
17
18
  Style/StringLiterals:
18
19
  EnforcedStyle: double_quotes
@@ -27,13 +28,13 @@ Metrics/BlockLength:
27
28
  Metrics/MethodLength:
28
29
  Exclude:
29
30
  - spec/**/*
30
- Max: 25
31
+ Max: 36
31
32
  CountComments: false
32
33
 
33
34
  Metrics/ClassLength:
34
35
  Exclude:
35
36
  - spec/**/*
36
- Max: 200
37
+ Max: 250
37
38
 
38
39
  Style/Documentation:
39
40
  Exclude:
@@ -96,3 +97,10 @@ RSpec/DescribeClass:
96
97
 
97
98
  RSpec/AnyInstance:
98
99
  Enabled: false
100
+
101
+ Naming/VariableNumber:
102
+ Enabled: false
103
+
104
+ Metrics/ModuleLength:
105
+ Max: 200
106
+
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A dynamic, dependency-resolving saga orchestrator for Ruby. Ruby Reactor implements the Saga pattern with compensation-based error handling and DAG-based execution planning. It leverages **Sidekiq** for asynchronous execution and **Redis** for state persistence.
4
4
 
5
+ ![Payment workflow reactor](documentation/images/payment_workflow.png)
6
+
5
7
  ## Features
6
8
 
7
9
  - **DAG-based Execution**: Steps are executed based on their dependencies, allowing for parallel execution of independent steps.
@@ -9,6 +11,7 @@ A dynamic, dependency-resolving saga orchestrator for Ruby. Ruby Reactor impleme
9
11
  - **Map & Parallel Execution**: Iterate over collections in parallel with the `map` step, distributing work across multiple workers.
10
12
  - **Retries**: Configurable retry logic for failed steps, with exponential backoff.
11
13
  - **Compensation**: Automatic rollback of completed steps when a failure occurs.
14
+ - **Interrupts**: Pause and resume workflows to wait for external events (webhooks, user approvals).
12
15
  - **Input Validation**: Integrated with `dry-validation` for robust input checking.
13
16
 
14
17
  ## Installation
@@ -47,6 +50,25 @@ RubyReactor.configure do |config|
47
50
  end
48
51
  ```
49
52
 
53
+ ## Web Dashboard
54
+
55
+ RubyReactor comes with a built-in web dashboard to inspect reactor executions, view logs, and retry failed steps.
56
+
57
+ ### Rails Installation
58
+
59
+ Mount the dashboard engine in your `config/routes.rb`:
60
+
61
+ ```ruby
62
+ Rails.application.routes.draw do
63
+ # ... other routes
64
+ mount RubyReactor::Web::Application => '/ruby_reactor'
65
+ end
66
+ ```
67
+
68
+ ![RubyReactor Dashboard Screenshot](documentation/images/failed_order_processing.png)
69
+
70
+ You can secure the dashboard using standard Rails authentication methods (e.g., `authenticate` block with Devise).
71
+
50
72
  ## Usage
51
73
 
52
74
  RubyReactor allows you to define complex workflows as "reactors" with steps that can depend on each other, handle failures with compensations, and validate inputs.
@@ -198,6 +220,42 @@ def create(params)
198
220
  end
199
221
  ```
200
222
 
223
+ ### Interrupts (Pause & Resume)
224
+
225
+ Pause execution to wait for external events like webhooks or user approvals.
226
+
227
+ ```ruby
228
+ class ApprovalReactor < RubyReactor::Reactor
229
+ step :submit_request do
230
+ run { |args| RequestService.submit(args) }
231
+ end
232
+
233
+ interrupt :wait_for_manager do
234
+ wait_for :submit_request
235
+ # Resume using this ID
236
+ correlation_id { |ctx| "req-#{ctx.result(:submit_request)[:id]}" }
237
+ end
238
+
239
+ step :process_decision do
240
+ argument :decision, result(:wait_for_manager)
241
+ run do |args|
242
+ args[:decision] == 'approved' ? Success() : Failure("Rejected")
243
+ end
244
+ end
245
+ end
246
+
247
+ # Usage:
248
+ # 1. Start execution
249
+ execution = ApprovalReactor.run(params) # => Returns Paused status
250
+
251
+ # 2. Later, resume it via correlation ID
252
+ ApprovalReactor.continue_by_correlation_id(
253
+ correlation_id: "req-123",
254
+ payload: "approved",
255
+ step_name: :wait_for_manager
256
+ )
257
+ ```
258
+
201
259
  ### Map & Parallel Execution
202
260
 
203
261
  Process collections in parallel using the `map` step:
@@ -540,6 +598,9 @@ Master the `map` feature for processing collections. Learn about parallel execut
540
598
  ### [Retry Configuration](documentation/retry_configuration.md)
541
599
  Configure robust retry policies for your steps. This guide details the available backoff strategies (exponential, linear, fixed), how to configure retries at the reactor or step level, and how async retries work without blocking workers.
542
600
 
601
+ ### [Interrupts](documentation/interrupts.md)
602
+ Learn how to pause and resume reactors to handle long-running processes, manual approvals, and asynchronous callbacks. Patterns for correlation IDs, timeouts, and payload validation.
603
+
543
604
  ### Examples
544
605
  - [Order Processing](documentation/examples/order_processing.md) - Complete order processing workflow example
545
606
  - [Payment Processing](documentation/examples/payment_processing.md) - Payment handling with compensation
@@ -548,12 +609,20 @@ Configure robust retry policies for your steps. This guide details the available
548
609
  ## Future improvements
549
610
 
550
611
  - [X] Global id to serialize ActiveRecord classes
551
- - [ ] Middlewares
552
- - [ ] Descriptive errors
612
+ - [X] Descriptive errors
553
613
  - [X] `map` step to iterate over arrays in parallel
554
614
  - [X] `compose` special step to execute reactors as step
615
+ - [X] `interrupt` to pause and resume reactors
616
+ - [ ] Middlewares
555
617
  - [ ] Async ruby to parallelize same level steps
556
- - [ ] Dedicated interface to inspect reactor results and errors
618
+ - [x] Web dashboard to inspect reactor results and errors
619
+ - [ ] Multiple storage adapters
620
+ - [X] Redis
621
+ - [ ] ActiveRecord
622
+ - [ ] Multiple Async adapters
623
+ - [X] Sidekiq
624
+ - [ ] ActiveJob
625
+ - [ ] OpenTelemetry support
557
626
 
558
627
  ## Development
559
628
 
data/Rakefile CHANGED
@@ -5,8 +5,33 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
8
+ namespace :build do
9
+ desc "Build the UI assets"
10
+ task :ui do
11
+ puts "Building UI..."
12
+ system("cd gui && npm install && npm run build") || abort("UI build failed")
9
13
 
10
- RuboCop::RakeTask.new
14
+ # Copy assets to public
15
+ # Vite builds to dist by default. We want it in lib/ruby_reactor/web/public
16
+ # Actually, we should configure Vite to build to the right place or copy it.
17
+ # Let's copy.
18
+ FileUtils.rm_rf("lib/ruby_reactor/web/public")
19
+ FileUtils.mkdir_p("lib/ruby_reactor/web/public")
20
+ FileUtils.cp_r("gui/dist/.", "lib/ruby_reactor/web/public/")
21
+ puts "UI built and assets copied to lib/ruby_reactor/web/public"
22
+ end
23
+ end
24
+
25
+ namespace :server do
26
+ desc "Start the server"
27
+ task :start do
28
+ puts "Starting server..."
29
+ system("rackup -Ilib lib/ruby_reactor/web/config.ru -p 9292") || abort("Server failed to start")
30
+ end
31
+ end
32
+
33
+ # require "rubocop/rake_task"
34
+
35
+ # RuboCop::RakeTask.new
11
36
 
12
37
  task default: %i[spec rubocop]
@@ -0,0 +1,161 @@
1
+ # Interrupts (Pause & Resume)
2
+
3
+ RubyReactor introduces the `interrupt` mechanism to support long-running processes that require external input, such as user approvals, webhooks, or asynchronous job completions. Unlike standard steps that execute immediately, an `interrupt` pauses the reactor execution and persists its state, waiting for a signal to resume.
4
+
5
+ ## DSL Usage
6
+
7
+ Use the `interrupt` keyword to define a pause point in your reactor.
8
+
9
+ ```ruby
10
+ class ReportReactor < RubyReactor::Reactor
11
+ step :request_report do
12
+ run do
13
+ response = HTTP.post("https://api.example.com/reports")
14
+ Success(response.fetch(:id))
15
+ end
16
+ end
17
+
18
+ interrupt :wait_for_report do
19
+ # Declare dependency: execution must trigger this interrupt only after :request_report succeeds
20
+ wait_for :request_report
21
+
22
+ # Optional: deterministic correlation ID for looking up this execution later
23
+ correlation_id do |context|
24
+ "report-#{context.result(:request_report)}"
25
+ end
26
+
27
+ # Optional: timeout in seconds
28
+ # Strategies:
29
+ # - :lazy (default) - checked only when resume is attempted
30
+ # - :active - schedules a background job to wake up the reactor and fail it
31
+ timeout 1800, strategy: :active
32
+
33
+ # Optional: validate incoming payload immediately using dry-validation
34
+ validate do
35
+ required(:status).filled(:string)
36
+ required(:url).filled(:string)
37
+ end
38
+
39
+ # Optional: limit validation attempts (default: 1)
40
+ # If exhausted, the reactor is cancelled and compensated.
41
+ # Use :infinity for unlimited attempts.
42
+ max_attempts 3
43
+ end
44
+
45
+ step :process_report do
46
+ # The result of the interrupt step is the payload provided when resuming
47
+ argument :webhook_payload, result(:wait_for_report)
48
+
49
+ run do |args|
50
+ Success(ReportProcessor.call(args[:webhook_payload]))
51
+ end
52
+ end
53
+ end
54
+ ```
55
+
56
+ ### Options
57
+
58
+ * **`wait_for`**: declare dependencies similar to `step`.
59
+ * **`correlation_id`**: A block that returns a unique string to identify this execution. This allows you to resume the reactor using a business key (e.g., order ID) instead of the internal execution UUID.
60
+ * **`timeout`**: Set a time limit for the interrupt.
61
+ * **`validate`**: A `dry-validation` schema block to validate the payload provided when resuming.
62
+ * **`max_attempts`**: Limit the number of times `continue` can be called with an invalid payload before the reactor is automatically cancelled and compensated. Defaults to 1. Set to `:infinity` for unlimited retries.
63
+
64
+ ## Runtime Behavior
65
+
66
+ When a reactor encounters an `interrupt`:
67
+
68
+ 1. It executes any dependencies.
69
+ 2. It persists the full `Context` (results of previous steps) to the configured storage (e.g., Redis).
70
+ 3. It returns an `InterruptResult` and halts execution.
71
+
72
+ ```ruby
73
+ execution = ReportReactor.run(company_id: 1)
74
+
75
+ if execution.paused?
76
+ execution.id # => "uuid-123"
77
+ execution.status # => :paused
78
+ end
79
+ ```
80
+
81
+ ## Resuming Execution
82
+
83
+ You can resume a paused reactor using its UUID or the defined `correlation_id`.
84
+
85
+ ### By UUID
86
+
87
+ ```ruby
88
+ ReportReactor.continue(
89
+ id: "uuid-123",
90
+ payload: { status: "completed", url: "..." },
91
+ step_name: :wait_for_report
92
+ )
93
+ ```
94
+
95
+ ### By Correlation ID
96
+
97
+ ```ruby
98
+ ReportReactor.continue_by_correlation_id(
99
+ correlation_id: "report-999",
100
+ payload: { status: "completed", url: "..." },
101
+ step_name: :wait_for_report
102
+ )
103
+ ```
104
+
105
+ ### Resuming Method Styles
106
+
107
+ There are two ways to invoke continuation:
108
+
109
+ 1. **Strict / Fire-and-Forget (Class Method)**:
110
+ * `Reactor.continue(...)`
111
+ * If payload is invalid, it **automatically compensates (undo)** and cancels the reactor.
112
+ * Best for webhooks where you can't ask the sender to fix the payload.
113
+
114
+ 2. **Flexible (Instance Method)**:
115
+ * First find the reactor: `reactor = ReportReactor.find("uuid-123")`
116
+ * Then call: `result = reactor.continue(payload: ..., step_name: :wait_for_report)`
117
+ * If payload is invalid, it returns a failure result but **does not** cancel execution.
118
+ * Allows you to handle the error (e.g., show a form error to a user) and try again.
119
+
120
+ ## Cancellation & Undo
121
+
122
+ You can cancel a paused reactor if the operation is no longer needed.
123
+
124
+ ```ruby
125
+ # Undo: Runs defined compensations for completed steps in reverse order, then deletes execution.
126
+ ReportReactor.undo("uuid-123")
127
+
128
+ # Cancel: Stops execution immediately and marks the reactor as cancelled with the provided reason.
129
+ # The context is preserved for inspection, but resumption is disabled.
130
+ ReportReactor.cancel("uuid-123", reason: "User cancelled")
131
+ ```
132
+
133
+ ## Common Use Cases
134
+
135
+ ### Human Approvals
136
+
137
+ ```ruby
138
+ interrupt :wait_for_approval do
139
+ wait_for :submit_request
140
+ correlation_id { |ctx| "approval-#{ctx.input(:request_id)}" }
141
+ end
142
+
143
+ step :process_decision do
144
+ argument :decision, result(:wait_for_approval)
145
+ run do |args|
146
+ if args[:decision][:approved]
147
+ Success("Approved")
148
+ else
149
+ Failure("Rejected")
150
+ end
151
+ end
152
+ end
153
+ ```
154
+
155
+ ### Webhooks
156
+
157
+ Use `correlation_id` to map an external resource ID (like a Payment Intent ID) back to the reactor waiting for confirmation.
158
+
159
+ ### Scheduled Follow-ups
160
+
161
+ Using `timeout` with `strategy: :active` to wake up a reactor after a delay if no external event occurs (e.g., expiring a reservation).
data/gui/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
data/gui/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
@@ -0,0 +1,23 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
data/gui/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ui</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>