funicular 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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +10 -2
  4. data/Rakefile +29 -0
  5. data/docs/architecture.md +113 -404
  6. data/lib/funicular/assets/funicular.css +23 -0
  7. data/lib/funicular/compiler.rb +23 -15
  8. data/lib/funicular/helpers/picoruby_helper.rb +65 -3
  9. data/lib/funicular/middleware.rb +34 -9
  10. data/lib/funicular/plugin.rb +147 -0
  11. data/lib/funicular/schema.rb +167 -0
  12. data/lib/funicular/ssr/runtime.rb +101 -0
  13. data/lib/funicular/ssr.rb +51 -0
  14. data/lib/funicular/testing/node_runner.mjs +293 -0
  15. data/lib/funicular/testing/node_runner.rb +190 -0
  16. data/lib/funicular/testing.rb +22 -0
  17. data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
  18. data/lib/funicular/vendor/picoruby/debug/picoruby.js +94 -75
  19. data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
  20. data/lib/funicular/vendor/picoruby/dist/picoruby.js +1 -1
  21. data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
  22. data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -0
  23. data/lib/funicular/vendor/picoruby-test-node/picoruby.js +6885 -0
  24. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
  25. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -0
  26. data/lib/funicular/version.rb +1 -1
  27. data/lib/funicular.rb +3 -0
  28. data/lib/generators/funicular/chat/chat_generator.rb +104 -0
  29. data/lib/generators/funicular/chat/templates/application_cable_channel.rb.tt +4 -0
  30. data/lib/generators/funicular/chat/templates/application_cable_connection.rb.tt +4 -0
  31. data/lib/generators/funicular/chat/templates/create_funicular_chat_messages.rb.tt +10 -0
  32. data/lib/generators/funicular/chat/templates/funicular_chat.css.tt +141 -0
  33. data/lib/generators/funicular/chat/templates/funicular_chat_channel.rb.tt +5 -0
  34. data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +135 -0
  35. data/lib/generators/funicular/chat/templates/funicular_chat_component_picotest.rb.tt +64 -0
  36. data/lib/generators/funicular/chat/templates/funicular_chat_controller.rb.tt +4 -0
  37. data/lib/generators/funicular/chat/templates/funicular_chat_message.rb.tt +13 -0
  38. data/lib/generators/funicular/chat/templates/funicular_chat_messages_controller.rb.tt +23 -0
  39. data/lib/generators/funicular/chat/templates/initializer.rb.tt +4 -0
  40. data/lib/generators/funicular/chat/templates/show.html.erb.tt +6 -0
  41. data/lib/tasks/funicular.rake +87 -4
  42. data/minitest/fixtures/funicular_app/components/greeting_component.rb +16 -0
  43. data/minitest/fixtures/funicular_app/initializer.rb +5 -0
  44. data/minitest/hydration_test.rb +87 -0
  45. data/minitest/plugin_test.rb +51 -0
  46. data/minitest/schema_test.rb +106 -0
  47. data/minitest/ssr_test.rb +94 -0
  48. data/minitest/validations_test.rb +183 -0
  49. data/mrbgem.rake +1 -0
  50. data/mrblib/0_validations.rb +206 -0
  51. data/mrblib/1_validators.rb +180 -0
  52. data/mrblib/cable.rb +24 -9
  53. data/mrblib/component.rb +172 -33
  54. data/mrblib/debug.rb +3 -0
  55. data/mrblib/differ.rb +47 -37
  56. data/mrblib/file_upload.rb +9 -1
  57. data/mrblib/form_builder.rb +21 -5
  58. data/mrblib/funicular.rb +97 -8
  59. data/mrblib/html_serializer.rb +121 -0
  60. data/mrblib/http.rb +123 -29
  61. data/mrblib/model.rb +50 -0
  62. data/mrblib/patcher.rb +74 -8
  63. data/mrblib/router.rb +40 -3
  64. data/mrblib/store.rb +304 -0
  65. data/mrblib/store_collection.rb +171 -0
  66. data/mrblib/store_singleton.rb +79 -0
  67. data/sig/cable.rbs +1 -0
  68. data/sig/component.rbs +13 -5
  69. data/sig/funicular.rbs +14 -1
  70. data/sig/html_serializer.rbs +20 -0
  71. data/sig/http.rbs +21 -6
  72. data/sig/model.rbs +6 -1
  73. data/sig/patcher.rbs +4 -1
  74. data/sig/router.rbs +3 -2
  75. data/sig/store.rbs +89 -0
  76. data/sig/store_collection.rbs +43 -0
  77. data/sig/store_singleton.rbs +19 -0
  78. data/sig/validations.rbs +103 -0
  79. data/sig/vdom.rbs +6 -6
  80. metadata +47 -12
  81. data/docs/README.md +0 -419
  82. data/docs/advanced-features.md +0 -632
  83. data/docs/components-and-state.md +0 -539
  84. data/docs/data-fetching.md +0 -528
  85. data/docs/forms.md +0 -446
  86. data/docs/rails-integration.md +0 -426
  87. data/docs/realtime.md +0 -543
  88. data/docs/routing-and-navigation.md +0 -427
  89. data/docs/styling.md +0 -285
data/docs/README.md DELETED
@@ -1,419 +0,0 @@
1
- # Funicular
2
-
3
- Funicular is named after a cable-driven railway, it lets you build apps using pure Ruby, with no JavaScript or HTML required.[^1]
4
-
5
- [^1]: 子曰知止而后有定 / Confucius, *When one knows where to stop, one can be steady.*
6
-
7
- ## Architecture Overview
8
-
9
- Funicular is a **unidirectional, Virtual DOM-based SPA framework** that adopts design patterns similar to React while embracing Ruby's expressiveness and integrating seamlessly with Rails.
10
-
11
- ### Core Design Decisions
12
-
13
- | Aspect | Choice | Philosophy |
14
- |--------|--------|------------|
15
- | **Data Flow** | Unidirectional | Explicit state updates via `patch()` for predictability |
16
- | **Rendering** | Virtual DOM + Diffing | Efficient DOM updates, declarative UI |
17
- | **Components** | Class-based | Clear organization with lifecycle hooks |
18
- | **State Management** | Local + Props | Simple by default, no global store |
19
- | **Routing** | Client-side (History API) | SPA navigation with Rails-style DSL |
20
- | **Templates** | Ruby DSL | Full Ruby expressiveness, no JSX |
21
- | **Build Strategy** | Runtime (No build step) | Instant feedback during development |
22
- | **Reactivity** | Explicit | Manual `patch()` calls, no auto-tracking magic |
23
-
24
- ### Framework Comparison
25
-
26
- **Funicular vs React**:
27
- - Similar: Unidirectional flow, VDOM, component-based
28
- - Different: Ruby DSL instead of JSX, no build step, Rails integration
29
-
30
- **Funicular vs Vue**:
31
- - Similar: Component-based, developer-friendly
32
- - Different: Unidirectional (not v-model), explicit updates (not reactive proxy)
33
-
34
- **Key Differentiator**: Pure Ruby browser applications with zero JavaScript, powered by PicoRuby.wasm.
35
-
36
- **Read more**: [Architecture Deep Dive](docs/architecture.md)
37
-
38
- ## Features
39
-
40
- - **[Pure Ruby Browser App](#pure-ruby-browser-app)** - Write frontend code entirely in Ruby
41
- - **[Object-REST Mapper](#object-rest-mapper-orm)** - ActiveRecord-style API for REST backends
42
- - **[ActionCable WebSocket](#actioncable-compatible-websocket)** - Real-time features with Rails integration
43
- - **[Rails Integration](#rails-integration)** - Seamless Rails API communication with CSRF handling
44
- - **[CSS-in-Ruby](#css-in-ruby-with-styles-dsl)** - Scoped styles with conditional variants
45
- - **[Form Builder](#rails-style-form-builder)** - Rails-style forms with validation errors
46
- - **[Routing & Navigation](#routing-and-navigation)** - Client-side routing with URL helpers
47
- - **[Error Boundary](#error-boundary)** - Graceful error handling for component failures
48
- - **[Suspense](#suspense--loading-state)** - Declarative async data loading with loading states
49
- - **[JS Integration](#js-integration)** - Delegation model for Chart.js, D3.js, etc.
50
-
51
- ## Quick Start
52
-
53
- ### Hello World
54
-
55
- ```ruby
56
- class Counter < Funicular::Component
57
- def initialize_state
58
- { count: 0 }
59
- end
60
-
61
- def increment
62
- patch(count: state.count + 1)
63
- end
64
-
65
- def decrement
66
- patch(count: state.count - 1)
67
- end
68
-
69
- def render
70
- div do
71
- h1 { "Counter" }
72
- p { "Current count: #{state.count}" }
73
- button(onclick: :increment) { "Increment" }
74
- button(onclick: :decrement) { "Decrement" }
75
- # Or use inline lambda for simple logic
76
- # button(onclick: -> { patch(count: state.count + 1) }) { "Increment" }
77
- end
78
- end
79
- end
80
-
81
- Funicular.start(Counter, container: "app")
82
- ```
83
-
84
- ### With Router
85
-
86
- ```ruby
87
- Funicular.start(container: 'app') do |router|
88
- router.get('/login', to: LoginComponent, as: 'login')
89
- router.get('/dashboard', to: DashboardComponent, as: 'dashboard')
90
- router.set_default('/login')
91
- end
92
- ```
93
-
94
- ## Pure Ruby Browser App
95
-
96
- Funicular is a component-based frontend framework that allows you to build browser applications entirely in Ruby, powered by PicoRuby.wasm. It uses a Virtual DOM (VDOM) to efficiently update the UI.
97
-
98
- ```ruby
99
- class TodoApp < Funicular::Component
100
- def initialize_state
101
- { todos: [], input: "" }
102
- end
103
-
104
- def handle_input(event)
105
- patch(input: event.target[:value])
106
- end
107
-
108
- def handle_add
109
- new_todo = { id: Time.now.to_i, text: state.input, done: false }
110
- patch(todos: state.todos + [new_todo], input: "")
111
- end
112
-
113
- def render
114
- div do
115
- h1 { "Todo List" }
116
- input(
117
- value: state.input,
118
- oninput: :handle_input
119
- )
120
- button(onclick: :handle_add) { "Add" }
121
-
122
- state.todos.each do |todo|
123
- div(key: todo[:id]) { todo[:text] }
124
- end
125
- end
126
- end
127
- end
128
- ```
129
-
130
- **Learn more**: [Components and State Management](docs/components-and-state.md)
131
-
132
- ## Object-REST Mapper (O-R-M)
133
-
134
- Funicular includes a built-in Object-REST Mapper that provides an ActiveRecord-like interface for interacting with REST APIs.
135
-
136
- ```ruby
137
- class User < Funicular::Model
138
- end
139
-
140
- # Fetch all users
141
- User.all do |users, error|
142
- patch(users: users)
143
- end
144
-
145
- # Find specific user
146
- User.find(123) do |user, error|
147
- patch(user: user)
148
- end
149
-
150
- # Create user
151
- User.create(name: "Alice", email: "alice@example.com") do |user, errors|
152
- if errors
153
- patch(errors: errors)
154
- else
155
- patch(user: user, success: "User created!")
156
- end
157
- end
158
-
159
- # Update user
160
- user.update(email: "newemail@example.com") do |updated_user, errors|
161
- patch(user: updated_user)
162
- end
163
- ```
164
-
165
- **Learn more**: [Data Fetching](docs/data-fetching.md)
166
-
167
- ## ActionCable-compatible WebSocket
168
-
169
- Real-time features powered by ActionCable-compatible WebSocket client.
170
-
171
- ```ruby
172
- class ChatComponent < Funicular::Component
173
- def component_mounted
174
- consumer = Funicular::Cable.create_consumer("/cable")
175
-
176
- @subscription = consumer.subscriptions.create(
177
- channel: "ChatChannel",
178
- room: "lobby"
179
- ) do |message|
180
- patch(messages: state.messages + [message])
181
- end
182
- end
183
-
184
- def handle_input(event)
185
- patch(input: event.target[:value])
186
- end
187
-
188
- def handle_send
189
- @subscription.perform("speak", message: state.input)
190
- patch(input: "")
191
- end
192
-
193
- def render
194
- div do
195
- state.messages.each { |msg| div { msg["content"] } }
196
- input(value: state.input, oninput: :handle_input)
197
- button(onclick: :handle_send) { "Send" }
198
- end
199
- end
200
- end
201
- ```
202
-
203
- **Learn more**: [ActionCable Integration](docs/realtime.md)
204
-
205
- ## Rails Integration
206
-
207
- - **CSRF Token Handling**: Automatic inclusion in POST/PATCH/PUT/DELETE requests
208
- - **ActionCable Compatible**: Real-time features with Rails channels
209
- - **Schema Loading**: Dynamic model attribute definitions from Rails API
210
- - **Zero JS Workflow**: Full-stack Ruby development
211
-
212
- **Learn more**: [Data Fetching](docs/data-fetching.md), [Real-time Features](docs/realtime.md)
213
-
214
- ## CSS-in-Ruby with Styles DSL
215
-
216
- Keep your styles organized and scoped within each component.
217
-
218
- ```ruby
219
- class LoginComponent < Funicular::Component
220
- styles do
221
- container "min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600"
222
- card "bg-white p-8 rounded-lg shadow-2xl w-96"
223
- title "text-3xl font-bold text-center mb-8 text-gray-800"
224
-
225
- # Conditional styles
226
- button base: "px-4 py-2 rounded font-semibold",
227
- variants: {
228
- primary: "bg-blue-600 text-white hover:bg-blue-700",
229
- danger: "bg-red-600 text-white hover:bg-red-700"
230
- }
231
- end
232
-
233
- def render
234
- div(class: s.container) do
235
- div(class: s.card) do
236
- h1(class: s.title) { "Welcome" }
237
- button(class: s.button(:primary)) { "Login" }
238
- end
239
- end
240
- end
241
- end
242
- ```
243
-
244
- **Learn more**: [Styling Guide](docs/styling.md)
245
-
246
- ## Rails-style Form Builder
247
-
248
- Automatic form state management and error display.
249
-
250
- ```ruby
251
- class SignupComponent < Funicular::Component
252
- def initialize_state
253
- { user: { username: "", email: "" }, errors: {} }
254
- end
255
-
256
- def handle_submit(form_data)
257
- User.create(form_data) do |user, errors|
258
- if errors
259
- patch(errors: errors)
260
- else
261
- Funicular.router.navigate('/dashboard')
262
- end
263
- end
264
- end
265
-
266
- def render
267
- form_for(:user, on_submit: :handle_submit) do |f|
268
- f.label(:username)
269
- f.text_field(:username, autofocus: true)
270
-
271
- f.label(:email)
272
- f.email_field(:email)
273
-
274
- f.submit("Sign Up")
275
- end
276
- end
277
- end
278
- ```
279
-
280
- **Learn more**: [Forms Guide](docs/forms.md)
281
-
282
- ## Routing and Navigation
283
-
284
- Client-side routing with Rails-style DSL and URL helpers.
285
-
286
- ```ruby
287
- Funicular.start(container: 'app') do |router|
288
- router.get('/users/:id', to: UserProfileComponent, as: 'user')
289
- router.get('/settings', to: SettingsComponent, as: 'settings')
290
- end
291
-
292
- # Use URL helpers
293
- include Funicular::RouteHelpers
294
-
295
- link_to user_path(user), navigate: true do
296
- span { user.name }
297
- end
298
-
299
- link_to settings_path, navigate: true, class: "nav-link" do
300
- span { "Settings" }
301
- end
302
- ```
303
-
304
- **Learn more**: [Routing and Navigation](docs/routing-and-navigation.md)
305
-
306
- ## Error Boundary
307
-
308
- Catch errors from child components and display fallback UI.
309
-
310
- ```ruby
311
- component(Funicular::ErrorBoundary,
312
- fallback: ->(error) {
313
- div(class: "error") do
314
- h3 { "Oops! Something went wrong" }
315
- p { "Error: #{error.message}" }
316
- end
317
- }
318
- ) do
319
- component(RiskyComponent)
320
- end
321
- ```
322
-
323
- **Learn more**: [Advanced Features - Error Boundary](docs/advanced-features.md#error-boundary)
324
-
325
- ## Suspense / Loading State
326
-
327
- Declarative async data loading with loading states.
328
-
329
- ```ruby
330
- class UserProfile < Funicular::Component
331
- use_suspense :user,
332
- ->(resolve, reject) {
333
- User.find(props[:id]) do |user, error|
334
- error ? reject.call(error) : resolve.call(user)
335
- end
336
- }
337
-
338
- def render
339
- suspense(
340
- fallback: -> { div { "Loading..." } },
341
- error: ->(e) { div { "Failed: #{e}" } }
342
- ) do
343
- div do
344
- h1 { user.name }
345
- p { user.email }
346
- end
347
- end
348
- end
349
- end
350
- ```
351
-
352
- **Learn more**: [Data Fetching - Suspense](docs/data-fetching.md#suspense--loading-states)
353
-
354
- ## JS Integration
355
-
356
- Delegation model for integrating JavaScript libraries (Chart.js, D3.js, etc.).
357
-
358
- ```ruby
359
- class ChartComponent < Funicular::Component
360
- def component_mounted
361
- canvas = refs[:chart_canvas]
362
- @chart = JS.global.Chart.new(canvas, {
363
- type: 'bar',
364
- data: { labels: state.labels, datasets: [...] }
365
- })
366
- end
367
-
368
- def component_unmounted
369
- @chart&.destroy()
370
- end
371
-
372
- def render
373
- div { canvas(ref: :chart_canvas) }
374
- end
375
- end
376
- ```
377
-
378
- **Learn more**: [Advanced Features - JS Integration](docs/advanced-features.md#js-integration-via-delegation-model)
379
-
380
- ## Documentation
381
-
382
- ### Architecture
383
- - [**Architecture Deep Dive**](docs/architecture.md) - Design decisions, comparisons, trade-offs
384
-
385
- ### Core Concepts
386
- - [**Components and State**](docs/components-and-state.md) - Component lifecycle, state management, props
387
- - [**Styling**](docs/styling.md) - CSS-in-Ruby Styles DSL, conditional styles, variants
388
- - [**Forms**](docs/forms.md) - Form builder, validation, error handling
389
-
390
- ### Features
391
- - [**Routing and Navigation**](docs/routing-and-navigation.md) - Router, URL helpers, link_to
392
- - [**Data Fetching**](docs/data-fetching.md) - HTTP client, Model (O-R-M), Suspense
393
- - [**Real-time**](docs/realtime.md) - ActionCable WebSocket integration
394
-
395
- ### Advanced
396
- - [**Advanced Features**](docs/advanced-features.md) - Error Boundary, CSS Transitions, JS Integration
397
-
398
- ### Related Documentation
399
- - [picoruby-wasm/docs/callback.md](../picoruby-wasm/docs/callback.md) - Callback system
400
- - [picoruby-wasm/docs/interoperability_between_js_and_ruby.md](../picoruby-wasm/docs/interoperability_between_js_and_ruby.md) - JS::Bridge for Ruby/JS interop
401
-
402
- ## Best Use Cases
403
-
404
- ### Well-Suited For
405
-
406
- - Rails applications with SPA features
407
- - Small to medium SPAs (dashboards, admin panels, chat apps)
408
- - Ruby teams doing frontend development
409
- - Rapid prototyping of interactive UIs
410
-
411
- ### Not Ideal For
412
-
413
- - SEO-critical applications (no SSR support for now, but planning...)
414
- - Large-scale SPAs with complex state management needs
415
- - Performance-critical applications (WebAssembly overhead)
416
- - Mobile applications (use React Native or native solutions)
417
-
418
- **Read more**: [Architecture - Trade-offs](docs/architecture.md#trade-offs)
419
-