funicular 0.0.1 → 0.1.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -1
  3. data/README.md +58 -20
  4. data/Rakefile +74 -2
  5. data/demo/keymap_editor.html +582 -0
  6. data/demo/test_cable.html +179 -0
  7. data/demo/test_chartjs.html +235 -0
  8. data/demo/test_component.html +201 -0
  9. data/demo/test_diff_patch.html +146 -0
  10. data/demo/test_error_boundary.html +284 -0
  11. data/demo/test_router.html +257 -0
  12. data/demo/test_vdom.html +100 -0
  13. data/demo/tic-tac-toe.html +201 -0
  14. data/docs/README.md +419 -0
  15. data/docs/advanced-features.md +632 -0
  16. data/docs/architecture.md +409 -0
  17. data/docs/components-and-state.md +539 -0
  18. data/docs/data-fetching.md +528 -0
  19. data/docs/forms.md +446 -0
  20. data/docs/rails-integration.md +426 -0
  21. data/docs/realtime.md +543 -0
  22. data/docs/routing-and-navigation.md +427 -0
  23. data/docs/styling.md +285 -0
  24. data/exe/funicular +32 -0
  25. data/lib/funicular/assets/funicular.rb +21 -0
  26. data/lib/funicular/assets/funicular_debug.css +73 -0
  27. data/lib/funicular/assets/funicular_debug.js +183 -0
  28. data/lib/funicular/commands/routes.rb +69 -0
  29. data/lib/funicular/compiler.rb +135 -0
  30. data/lib/funicular/configuration.rb +76 -0
  31. data/lib/funicular/helpers/picoruby_helper.rb +50 -0
  32. data/lib/funicular/middleware.rb +98 -0
  33. data/lib/funicular/railtie.rb +26 -0
  34. data/lib/funicular/route_parser.rb +137 -0
  35. data/lib/funicular/vendor/picorbc/VERSION +1 -0
  36. data/lib/funicular/vendor/picorbc/picorbc.js +5283 -0
  37. data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
  38. data/lib/funicular/vendor/picoruby/VERSION +1 -0
  39. data/lib/funicular/vendor/picoruby/debug/init.iife.js +130 -0
  40. data/lib/funicular/vendor/picoruby/debug/picoruby.js +6404 -0
  41. data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
  42. data/lib/funicular/vendor/picoruby/dist/init.iife.js +130 -0
  43. data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -0
  44. data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
  45. data/lib/funicular/version.rb +1 -1
  46. data/lib/funicular.rb +29 -1
  47. data/lib/tasks/funicular.rake +135 -0
  48. data/minitest/funicular_test.rb +13 -0
  49. data/minitest/test_helper.rb +7 -0
  50. data/mrbgem.rake +15 -0
  51. data/mrblib/cable.rb +417 -0
  52. data/mrblib/component.rb +911 -0
  53. data/mrblib/debug.rb +205 -0
  54. data/mrblib/differ.rb +244 -0
  55. data/mrblib/environment_inquirer.rb +34 -0
  56. data/mrblib/error_boundary.rb +125 -0
  57. data/mrblib/file_upload.rb +184 -0
  58. data/mrblib/form_builder.rb +284 -0
  59. data/mrblib/funicular.rb +156 -0
  60. data/mrblib/http.rb +89 -0
  61. data/mrblib/model.rb +146 -0
  62. data/mrblib/patcher.rb +203 -0
  63. data/mrblib/router.rb +229 -0
  64. data/mrblib/styles.rb +83 -0
  65. data/mrblib/vdom.rb +273 -0
  66. data/sig/cable.rbs +65 -0
  67. data/sig/component.rbs +141 -0
  68. data/sig/debug.rbs +28 -0
  69. data/sig/differ.rbs +18 -0
  70. data/sig/environment_iquirer.rbs +10 -0
  71. data/sig/error_boundary.rbs +14 -0
  72. data/sig/file_upload.rbs +18 -0
  73. data/sig/form_builder.rbs +29 -0
  74. data/sig/funicular.rbs +11 -1
  75. data/sig/http.rbs +22 -0
  76. data/sig/model.rbs +23 -0
  77. data/sig/patcher.rbs +15 -0
  78. data/sig/router.rbs +43 -0
  79. data/sig/styles.rbs +25 -0
  80. data/sig/vdom.rbs +59 -0
  81. metadata +119 -8
@@ -0,0 +1,409 @@
1
+ # Funicular Architecture
2
+
3
+ This document provides a comprehensive analysis of Funicular's architectural design decisions and how they compare to other modern SPA frameworks.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Architecture Design Decisions](#architecture-design-decisions)
9
+ - [1. Data Flow](#1-data-flow)
10
+ - [2. Rendering Strategy](#2-rendering-strategy)
11
+ - [3. Reactivity Model](#3-reactivity-model)
12
+ - [4. Component Model](#4-component-model)
13
+ - [5. State Management](#5-state-management)
14
+ - [6. Routing](#6-routing)
15
+ - [7. Rendering Mode](#7-rendering-mode)
16
+ - [8. Template System](#8-template-system)
17
+ - [9. Build Strategy](#9-build-strategy)
18
+ - [10. Concurrency](#10-concurrency)
19
+ - [11. Event System](#11-event-system)
20
+ - [12. Error Handling](#12-error-handling)
21
+ - [13. Data Fetching](#13-data-fetching)
22
+ - [Comparison with Other Frameworks](#comparison-with-other-frameworks)
23
+ - [Design Philosophy](#design-philosophy)
24
+ - [Trade-offs](#trade-offs)
25
+ - [Best Use Cases](#best-use-cases)
26
+
27
+ ## Overview
28
+
29
+ Funicular is a **unidirectional, Virtual DOM-based SPA framework** for PicoRuby.wasm. It adopts design patterns similar to React while embracing Ruby's expressiveness and integrating seamlessly with Rails.
30
+
31
+ **Core Principle**: Pure Ruby browser applications with explicit, predictable data flow.
32
+
33
+ ## Architecture Design Decisions
34
+
35
+ ### 1. Data Flow
36
+
37
+ **Choice**: Unidirectional (One-way) Data Binding
38
+
39
+ ```ruby
40
+ # State flows DOWN to UI
41
+ state -> VDOM -> DOM rendering
42
+
43
+ # Events flow UP to trigger state updates
44
+ DOM events -> event handlers -> patch() -> state update -> re-render
45
+ ```
46
+
47
+ **Evidence**:
48
+ - State is immutable, updatable only via `patch()`
49
+ - Direct state mutation is blocked by `StateAccessor`
50
+ - Form inputs require explicit event handlers + `patch()` calls
51
+
52
+ **Comparison**:
53
+
54
+ | Framework | Binding Type | Update Mechanism |
55
+ |-----------|--------------|------------------|
56
+ | **Funicular** | Unidirectional | `patch()` + VDOM diffing |
57
+ | React | Unidirectional | `setState()` + VDOM diffing |
58
+ | Vue 2 | Bidirectional (v-model) | Reactivity proxy + VDOM |
59
+ | Angular | Bidirectional | Two-way data binding |
60
+ | Svelte | Unidirectional | Compile-time reactivity |
61
+
62
+ ### 2. Rendering Strategy
63
+
64
+ **Choice**: Virtual DOM with Diffing Algorithm
65
+
66
+ **Implementation**:
67
+ - Custom VDOM implementation (`vdom.rb`)
68
+ - `Differ.diff()` calculates minimal changes
69
+ - `Patcher.apply()` applies patches to real DOM
70
+ - **Key-based reconciliation** for efficient list updates
71
+
72
+ **Code Reference**:
73
+ ```ruby
74
+ # Component re-renders when state changes
75
+ def re_render
76
+ new_vdom = build_vdom
77
+ patches = VDOM::Differ.diff(@vdom, new_vdom)
78
+ @dom_element = VDOM::Patcher.apply(@dom_element, patches)
79
+ @vdom = new_vdom
80
+ end
81
+ ```
82
+
83
+ **Benefits**:
84
+ - Efficient DOM updates (minimal mutations)
85
+ - Declarative UI (describe what, not how)
86
+ - Cross-platform potential (VDOM is abstraction layer)
87
+
88
+ ### 3. Reactivity Model
89
+
90
+ **Choice**: Explicit Updates (Manual Trigger)
91
+
92
+ | Approach | Mechanism | Examples |
93
+ |----------|-----------|----------|
94
+ | **Explicit Updates** | Manual `setState()`/`patch()` | React, **Funicular** |
95
+ | Auto-tracking | Proxy/Getter dependency tracking | Vue 3, Solid.js |
96
+ | Compile-time | Compiler analyzes dependencies | Svelte |
97
+
98
+ **Example**:
99
+ ```ruby
100
+ def handle_input(event)
101
+ patch(username: event.target[:value]) # Explicit call
102
+ end
103
+ ```
104
+
105
+ **Benefits**:
106
+ - Simple, predictable
107
+ - Low runtime overhead
108
+ - Easy to debug (explicit control flow)
109
+
110
+ ### 4. Component Model
111
+
112
+ **Choice**: Class-based Components
113
+
114
+ ```ruby
115
+ class ChatComponent < Funicular::Component
116
+ def initialize_state
117
+ { messages: [] }
118
+ end
119
+
120
+ def component_mounted
121
+ # Lifecycle hook
122
+ end
123
+
124
+ def render
125
+ div { "Chat UI" }
126
+ end
127
+ end
128
+ ```
129
+
130
+ **Features**:
131
+ - Lifecycle hooks: `component_mounted`, `component_unmounted`
132
+ - Instance variables for component-local data
133
+ - Suspense support for async data loading
134
+ - Similar to React Class Components
135
+
136
+ ### 5. State Management
137
+
138
+ **Choice**: Local State + Props Drilling + Model Layer
139
+
140
+ | Approach | Description | Example |
141
+ |----------|-------------|---------|
142
+ | **Local State** | Component-scoped `@state` | **Funicular** |
143
+ | Global Store | Centralized state tree | Redux, Vuex |
144
+ | Context/DI | Share state within tree | React Context |
145
+
146
+ **Architecture**:
147
+ - Components manage their own state (`@state`)
148
+ - Parent-to-child communication via `props`
149
+ - Server state managed by `Model` layer (ActiveRecord-style API)
150
+
151
+ **No Global Store**: Funicular intentionally omits Redux-style global state to keep things simple. For shared state, use:
152
+ - Props drilling
153
+ - Model layer for server data
154
+ - Component composition
155
+
156
+ ### 6. Routing
157
+
158
+ **Choice**: Client-Side Routing (History API)
159
+
160
+ ```ruby
161
+ Funicular.start(container: 'app') do |router|
162
+ router.get('/chat/:channel_id', to: ChatComponent, as: 'chat_channel')
163
+ end
164
+ ```
165
+
166
+ **Features**:
167
+ - Rails-style DSL
168
+ - URL parameter extraction (`/chat/:id` -> `{ id: "123" }`)
169
+ - Auto-generated URL helpers: `chat_channel_path(id)`
170
+ - History API for SPA navigation
171
+
172
+ ### 7. Rendering Mode
173
+
174
+ **Choice**: Pure Client-Side Rendering (CSR)
175
+
176
+ | Mode | Description | Funicular Support |
177
+ |------|-------------|-------------------|
178
+ | **CSR** | Render in browser | ✅ Yes |
179
+ | SSR | Server-side rendering | ❌ No (for now...) |
180
+ | SSG | Static site generation | ❌ No |
181
+ | ISR | Incremental static regen | ❌ No |
182
+
183
+ **Reason**: PicoRuby.wasm runs only in browsers. Rails serves JSON APIs + assets.
184
+
185
+ ### 8. Template System
186
+
187
+ **Choice**: Ruby DSL (Not JSX or Template Strings)
188
+
189
+ ```ruby
190
+ def render
191
+ div(class: 'container') do
192
+ h1 { 'Welcome' }
193
+ input(value: state.username, oninput: :handle_input)
194
+ end
195
+ end
196
+ ```
197
+
198
+ **Features**:
199
+ - HTML tag names are Ruby methods
200
+ - Blocks for child elements
201
+ - Event handlers as symbols or Procs
202
+ - Full Ruby expressiveness (loops, conditionals, etc.)
203
+
204
+ **Comparison**:
205
+
206
+ | Framework | Template Syntax |
207
+ |-----------|-----------------|
208
+ | React | JSX (XML-like) |
209
+ | Vue | Template strings with directives |
210
+ | Svelte | HTML-like template syntax |
211
+ | **Funicular** | **Ruby DSL** |
212
+
213
+ ### 9. Build Strategy
214
+
215
+ **Choice**: Runtime Execution (No Build Step)
216
+
217
+ | Approach | Description | Examples |
218
+ |----------|-------------|----------|
219
+ | **Runtime** | Code runs directly in browser | Vue (CDN), **Funicular** |
220
+ | Compile-time | Pre-build required | Svelte, Angular |
221
+ | Hybrid | JSX compiled, runtime VDOM | React |
222
+
223
+ **Funicular's Approach**:
224
+ - `app/funicular/**/*.rb` files are compiled to mruby bytecode (`.mrb`) via `picorbc`
225
+ - Compilation output: `app/assets/builds/app.mrb`
226
+ - Rails autoloading is explicitly disabled for `app/funicular/` directory
227
+ - Asset pipeline automatically handles compilation (hooks into `assets:precompile`)
228
+ - Developers don't need to manually compile (transparent automation)
229
+
230
+ **Build Process**:
231
+ ```bash
232
+ # Automatic in production
233
+ rake assets:precompile # -> calls funicular:compile
234
+
235
+ # Manual compilation (if needed)
236
+ rake funicular:compile
237
+ ```
238
+
239
+ **Benefits**:
240
+ - Automated compilation via asset pipeline
241
+ - Developers work with plain Ruby files
242
+ - Production serves optimized bytecode
243
+ - No separate build tool required (uses existing Rails toolchain)
244
+
245
+ ### 10. Concurrency
246
+
247
+ **Choice**: Synchronous Rendering + Async Data Fetching
248
+
249
+ - `patch()` triggers immediate `re_render()`
250
+ - No batching or scheduling
251
+ - Suspense feature for async data with loading states
252
+ - `min_delay` option prevents spinner flickering
253
+
254
+ ```ruby
255
+ use_suspense :user,
256
+ ->(resolve, reject) { User.find(id, &resolve) },
257
+ min_delay: 300 # Minimum loading spinner duration
258
+ ```
259
+
260
+ ### 11. Event System
261
+
262
+ **Choice**: Native DOM Events (Not Synthetic Events)
263
+
264
+ - Direct `addEventListener` usage
265
+ - Event listeners re-bound on each re-render
266
+ - `cleanup_events()` prevents memory leaks
267
+ - No event pooling or synthetic event objects
268
+
269
+ **Unified Callback Handling**:
270
+
271
+ All event handlers accept `Symbol | Method | Proc`:
272
+
273
+ ```ruby
274
+ # All valid:
275
+ button(onclick: :handle_click) # Symbol (recommended)
276
+ button(onclick: method(:handle_click)) # Method (explicit)
277
+ button(onclick: -> { handle_click }) # Proc (inline)
278
+ button(onclick: -> { patch(count: count + 1) }) # Proc (inline logic)
279
+ ```
280
+
281
+ **Recommendation**:
282
+ - Use `:symbol` for method references (concise)
283
+ - Use `-> { }` for inline logic
284
+ - Use `method(:name)` when passing callbacks to child components
285
+
286
+ ### 12. Error Handling
287
+
288
+ **Choice**: Error Boundary Pattern (React-style)
289
+
290
+ ```ruby
291
+ component(Funicular::ErrorBoundary,
292
+ fallback: ->(error) { div { "Error: #{error.message}" } }
293
+ ) do
294
+ component(RiskyComponent)
295
+ end
296
+ ```
297
+
298
+ - Catches errors from child components
299
+ - Displays fallback UI
300
+ - Prevents entire app crash
301
+ - Stack-based boundary resolution
302
+
303
+ ### 13. Data Fetching
304
+
305
+ **Choice**: Manual Fetch + Model Abstraction Layer
306
+
307
+ **Low-level**: `HTTP.get`, `HTTP.post`
308
+ **High-level**: `Model.all`, `Model.find`, `Model.create`
309
+
310
+ ```ruby
311
+ # ActiveRecord-style API
312
+ User.all do |users, error|
313
+ patch(users: users)
314
+ end
315
+ ```
316
+
317
+ **Features**:
318
+ - Callback-based (not Promise-based)
319
+ - Automatic CSRF token handling
320
+ - Rails API integration
321
+ - No built-in caching (like SWR/React Query)
322
+
323
+ ## Comparison with Other Frameworks
324
+
325
+ ### Funicular vs React
326
+
327
+ | Aspect | Funicular | React |
328
+ |--------|-----------|-------|
329
+ | Language | Ruby | JavaScript/JSX |
330
+ | Components | Class-based | Function (Hooks) or Class |
331
+ | State Update | `patch()` | `setState()` / `useState()` |
332
+ | VDOM | Custom implementation | Custom implementation |
333
+ | Data Fetching | Model layer | Manual / libraries |
334
+ | Routing | Built-in | Separate library |
335
+ | Build Step | Asset Pipeline integration | Required (Babel/JSX) |
336
+
337
+ ### Funicular vs Vue
338
+
339
+ | Aspect | Funicular | Vue |
340
+ |--------|-----------|-----|
341
+ | Data Binding | Unidirectional | Bidirectional (v-model) |
342
+ | Reactivity | Explicit `patch()` | Auto-tracking (Proxy) |
343
+ | Templates | Ruby DSL | HTML-like templates |
344
+ | SSR | No | Yes |
345
+
346
+ ### Funicular vs Svelte
347
+
348
+ | Aspect | Funicular | Svelte |
349
+ |--------|-----------|--------|
350
+ | VDOM | Yes | No (compiles to imperative code) |
351
+ | Reactivity | Explicit | Compile-time analysis |
352
+ | Build Step | Asset Pipeline integration | Required (compiler) |
353
+
354
+ ## Design Philosophy
355
+
356
+ Funicular embodies **"PicoRuby.wasm-based React"** with these principles:
357
+
358
+ 1. **Ruby First**: Leverage Ruby's expressiveness for frontend development
359
+ 2. **Explicit Over Magic**: Predictable, explicit state updates
360
+ 3. **Rails Integration**: Seamless Rails API + ActionCable + Asset Pipeline integration
361
+ 4. **Simple by Default**: No global state, no complex build tools
362
+ 5. **Progressive Enhancement**: Start simple, add complexity when needed
363
+
364
+ ## Trade-offs
365
+
366
+ ### Strengths
367
+
368
+ ✅ **Ruby Expressiveness**: Full Ruby syntax in templates
369
+ ```ruby
370
+ state.messages.map { |msg| div(key: msg.id) { msg.content } }
371
+ ```
372
+
373
+ ✅ **No Build Step**: Instant development feedback
374
+
375
+ ✅ **Rails Integration**: CSRF tokens, ActiveRecord-style APIs
376
+
377
+ ✅ **Lightweight Dependencies**: Minimal gem dependencies
378
+
379
+ ### Limitations
380
+
381
+ ❌ **No SSR**: PicoRuby.wasm is browser-only (for now...)
382
+
383
+ ❌ **No npm Ecosystem**: Limited to Ruby gems
384
+
385
+ ❌ **State Management**: No global store (can be complex at scale)
386
+
387
+ ❌ **Performance Overhead**: WebAssembly Ruby execution slower than native JS
388
+
389
+ ## Best Use Cases
390
+
391
+ ### ✅ Well-Suited For:
392
+
393
+ - **Rails SPA Features**: Adding interactive UI to Rails apps
394
+ - **Small to Medium SPAs**: Dashboard, admin panels, chat apps
395
+ - **Ruby Teams**: Frontend work by Ruby developers
396
+ - **Rapid Prototyping**: Quick interactive UI development
397
+
398
+ ### ❌ Not Ideal For:
399
+
400
+ - **SEO-Critical Apps**: Needs SSR (use Rails views or Next.js)
401
+ - **Large-Scale SPAs**: Complex state management requirements
402
+ - **Mobile Apps**: Use React Native or native solutions
403
+ - **Performance-Critical**: Real-time games, high-frequency updates
404
+
405
+ ## Conclusion
406
+
407
+ Funicular adopts proven patterns from React (unidirectional flow, VDOM) while providing unique value through **Ruby ecosystem integration**. It's not trying to replace JavaScript frameworks for all use cases, but rather offers a compelling alternative for Ruby teams building interactive web applications.
408
+
409
+ The architecture prioritizes **simplicity, predictability, and Rails integration** over features like maximum performance. This makes it an excellent choice for its target use cases: Ruby teams building SPAs as part of Rails applications.