funicular 0.2.1 → 0.3.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +2 -2
  4. data/Rakefile +23 -15
  5. data/demo/test_chartjs.html +17 -17
  6. data/demo/test_component.html +15 -15
  7. data/demo/test_error_boundary.html +41 -41
  8. data/demo/test_router.html +57 -57
  9. data/demo/tic-tac-toe.html +29 -29
  10. data/docs/architecture.md +43 -30
  11. data/lib/funicular/compiler.rb +13 -13
  12. data/lib/funicular/helpers/picoruby_helper.rb +24 -1
  13. data/lib/funicular/ssr/runtime.rb +2 -0
  14. data/lib/funicular/ssr.rb +2 -1
  15. data/lib/funicular/testing/node_runner.rb +1 -1
  16. data/lib/funicular/vendor/mrbc/VERSION +1 -0
  17. data/lib/funicular/vendor/{picorbc/picorbc.js → mrbc/mrbc.js} +14 -1
  18. data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
  19. data/lib/funicular/vendor/picoruby/VERSION +1 -1
  20. data/lib/funicular/vendor/picoruby/debug/init.iife.js +19 -4
  21. data/lib/funicular/vendor/picoruby/debug/picoruby.js +16 -10
  22. data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
  23. data/lib/funicular/vendor/picoruby/dist/init.iife.js +19 -4
  24. data/lib/funicular/vendor/picoruby/dist/picoruby.js +1 -1
  25. data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
  26. data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -1
  27. data/lib/funicular/vendor/picoruby-test-node/picoruby.js +85 -84
  28. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
  29. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -1
  30. data/lib/funicular/version.rb +1 -1
  31. data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +47 -46
  32. data/lib/tasks/funicular.rake +1 -1
  33. data/minitest/commands_routes_test.rb +97 -0
  34. data/minitest/compiler_test.rb +195 -0
  35. data/minitest/configuration_test.rb +64 -0
  36. data/minitest/fixtures/funicular_app/components/greeting_component.rb +6 -6
  37. data/minitest/form_for_test.rb +2 -2
  38. data/minitest/funicular_test.rb +28 -2
  39. data/minitest/hydration_test.rb +2 -2
  40. data/minitest/middleware_test.rb +154 -0
  41. data/minitest/picoruby_helper_test.rb +139 -0
  42. data/minitest/route_parser_test.rb +139 -0
  43. data/minitest/schema_test.rb +23 -0
  44. data/minitest/ssr_test.rb +57 -0
  45. data/minitest/support/rails_stub.rb +59 -0
  46. data/minitest/test_helper.rb +20 -0
  47. data/minitest/testing_test.rb +267 -0
  48. data/minitest/view_context_test.rb +101 -0
  49. data/mrblib/component.rb +158 -237
  50. data/mrblib/debug.rb +7 -6
  51. data/mrblib/differ.rb +3 -1
  52. data/mrblib/error_boundary.rb +19 -27
  53. data/mrblib/form_builder.rb +28 -24
  54. data/mrblib/funicular.rb +2 -1
  55. data/mrblib/html_serializer.rb +14 -13
  56. data/mrblib/http.rb +12 -1
  57. data/mrblib/patcher.rb +12 -9
  58. data/mrblib/router.rb +9 -5
  59. data/mrblib/runtime.rb +28 -0
  60. data/mrblib/styles.rb +13 -17
  61. data/mrblib/vdom.rb +90 -9
  62. data/mrblib/view_context.rb +135 -0
  63. data/sig/component.rbs +34 -85
  64. data/sig/error_boundary.rbs +4 -4
  65. data/sig/form_builder.rbs +2 -1
  66. data/sig/html_serializer.rbs +2 -1
  67. data/sig/http.rbs +1 -0
  68. data/sig/patcher.rbs +1 -1
  69. data/sig/router.rbs +2 -13
  70. data/sig/runtime.rbs +13 -0
  71. data/sig/styles.rbs +3 -4
  72. data/sig/vdom.rbs +11 -2
  73. data/sig/view_context.rbs +47 -0
  74. metadata +18 -5
  75. data/lib/funicular/vendor/picorbc/VERSION +0 -1
  76. data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
data/mrblib/vdom.rb CHANGED
@@ -6,6 +6,69 @@ module Funicular
6
6
 
7
7
  URL_ATTRIBUTES = %w[href src action formaction data poster xlink:href]
8
8
 
9
+ # VDOM input is normally authored by application code, but props and tags
10
+ # can also be assembled from hashes. Validate names before either the DOM
11
+ # renderer or the SSR serializer sees them so an attacker-controlled key
12
+ # cannot become HTML syntax.
13
+ SCRIPTING_ELEMENTS = %w[script]
14
+
15
+ def self.valid_tag_name?(name)
16
+ str = name.to_s
17
+ return false if str.empty?
18
+
19
+ index = 0
20
+ str.each_byte do |byte|
21
+ alpha = (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122)
22
+ digit = byte >= 48 && byte <= 57
23
+ return false unless alpha || (index > 0 && (digit || byte == 45))
24
+ index += 1
25
+ end
26
+ true
27
+ end
28
+
29
+ def self.valid_attribute_name?(name)
30
+ str = name.to_s
31
+ return false if str.empty?
32
+
33
+ str.each_byte do |byte|
34
+ alpha = (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122)
35
+ digit = byte >= 48 && byte <= 57
36
+ punctuation = byte == 45 || byte == 46 || byte == 58 || byte == 95
37
+ return false unless alpha || digit || punctuation
38
+ end
39
+ true
40
+ end
41
+
42
+ def self.event_attribute?(name)
43
+ name.to_s.downcase.start_with?('on')
44
+ end
45
+
46
+ def self.unsafe_url?(name, value)
47
+ return false unless URL_ATTRIBUTES.include?(name.to_s.downcase)
48
+
49
+ # Browsers discard ASCII tabs and newlines while parsing URLs, so test
50
+ # the same canonical form rather than only a literal "javascript:".
51
+ raw = value.to_s
52
+ start = 0
53
+ start += 1 while (byte = raw.getbyte(start)) && byte <= 32
54
+
55
+ normalized = (raw[start..-1] || "")
56
+ .gsub("\t", "")
57
+ .gsub("\n", "")
58
+ .gsub("\r", "")
59
+ .gsub("\f", "")
60
+ .strip
61
+ .downcase
62
+ normalized.start_with?('javascript:') || normalized.start_with?('vbscript:')
63
+ end
64
+
65
+ def self.blocked_attribute?(name, value)
66
+ normalized_name = name.to_s.downcase
67
+ event_attribute?(normalized_name) ||
68
+ normalized_name == 'srcdoc' ||
69
+ unsafe_url?(normalized_name, value)
70
+ end
71
+
9
72
  class VNode
10
73
  attr_reader :type, :key
11
74
 
@@ -20,8 +83,20 @@ module Funicular
20
83
  def initialize(tag, props = {}, children = [])
21
84
  super(:element)
22
85
  @tag = tag.to_s
86
+ unless VDOM.valid_tag_name?(@tag)
87
+ raise ArgumentError, "Invalid VDOM tag name: #{@tag.inspect}"
88
+ end
89
+ if SCRIPTING_ELEMENTS.include?(@tag.downcase)
90
+ raise ArgumentError, "Unsafe VDOM tag: #{@tag.inspect}"
91
+ end
92
+
23
93
  @key = props.delete(:key)
24
94
  @props = props || {}
95
+ @props.each_key do |name|
96
+ unless VDOM.valid_attribute_name?(name)
97
+ raise ArgumentError, "Invalid VDOM attribute name: #{name.inspect}"
98
+ end
99
+ end
25
100
  @children = normalize_children(children || [])
26
101
  end
27
102
 
@@ -71,26 +146,29 @@ module Funicular
71
146
  end
72
147
 
73
148
  class Component < VNode
74
- attr_reader :component_class, :props
75
- attr_accessor :instance
149
+ attr_reader :component_class, :props, :children
150
+ attr_accessor :instance, :runtime
76
151
 
77
- def initialize(component_class, props = {})
152
+ def initialize(component_class, props = {}, children = [])
78
153
  super(:component)
79
154
  @component_class = component_class
80
155
  @key = props.delete(:key)
81
156
  @props = props
157
+ @children = children || []
82
158
  @instance = nil
159
+ @runtime = nil
83
160
  end
84
161
 
85
162
  def ==(other)
86
163
  return false unless other.is_a?(Component)
87
- @component_class == other.component_class && @props == other.props
164
+ @component_class == other.component_class && @props == other.props && @children == other.children
88
165
  end
89
166
  end
90
167
 
91
168
  class Renderer
92
- def initialize(doc = nil)
169
+ def initialize(doc = nil, runtime = nil)
93
170
  @doc = doc || JS.document
171
+ @runtime = runtime
94
172
  @error_boundary_stack = []
95
173
  end
96
174
 
@@ -122,13 +200,14 @@ module Funicular
122
200
 
123
201
  element.props.each do |key, value|
124
202
  key_str = key.to_s
125
- if key_str.start_with?('on')
203
+ normalized_key = key_str.downcase
204
+ if VDOM.event_attribute?(normalized_key)
126
205
  # Event handlers are handled by Funicular::Component and should not be set as attributes.
127
206
  # warn "Funicular: Attempted to set event handler '#{key_str}' as an attribute. This will be ignored."
128
- elsif URL_ATTRIBUTES.include?(key_str) && value.to_s.strip.downcase.start_with?('javascript:')
129
- # Prevent XSS attacks by blocking javascript: URIs in URL attributes
207
+ elsif VDOM.blocked_attribute?(normalized_key, value)
208
+ # Prevent active HTML content and unsafe URL schemes.
130
209
  puts "[WARN] Funicular: Blocked potentially malicious value for attribute '#{key_str}'."
131
- elsif BOOLEAN_ATTRIBUTES.include?(key_str)
210
+ elsif BOOLEAN_ATTRIBUTES.include?(normalized_key)
132
211
  # Handle boolean attributes
133
212
  if value.nil? || value.to_s == "false"
134
213
  # Do not set attribute (leave it absent)
@@ -176,6 +255,8 @@ module Funicular
176
255
 
177
256
  def render_component(component_vnode, parent)
178
257
  instance = component_vnode.component_class.new(component_vnode.props)
258
+ instance.runtime = component_vnode.runtime || @runtime || Funicular::Runtime.new
259
+ instance.children = component_vnode.children
179
260
  component_vnode.instance = instance
180
261
 
181
262
  is_error_boundary = instance.is_a?(Funicular::ErrorBoundary)
@@ -0,0 +1,135 @@
1
+ module Funicular
2
+ class ViewContext
3
+ HTML_TAGS = %w[
4
+ div span p a data
5
+ h1 h2 h3 h4 h5 h6
6
+ ul ol li
7
+ table thead tbody tr th td
8
+ form input textarea button select option label
9
+ header footer nav section article aside
10
+ img video audio canvas
11
+ br hr
12
+ ]
13
+
14
+ def initialize(component)
15
+ @component = component
16
+ end
17
+
18
+ def state
19
+ @component.state
20
+ end
21
+
22
+ def props
23
+ @component.props
24
+ end
25
+
26
+ def resources
27
+ @component.resources
28
+ end
29
+
30
+ def styles
31
+ @component.styles
32
+ end
33
+
34
+ def routes
35
+ @component.runtime.routes
36
+ end
37
+
38
+ def tag(name, props = {}, &block)
39
+ build_element(name, props, &block)
40
+ end
41
+
42
+ HTML_TAGS.each do |tag_name|
43
+ define_method(tag_name) do |props = {}, &block|
44
+ tag(tag_name, props, &block) # steep:ignore
45
+ end
46
+ end
47
+
48
+ def component(component_class, props = {}, &block)
49
+ unless component_class.is_a?(Class) && component_class.ancestors.include?(Funicular::Component)
50
+ raise "h.component expects a Funicular::Component class"
51
+ end
52
+
53
+ children = if block
54
+ capture(&block)
55
+ else
56
+ [] #: Array[Funicular::VDOM::child_t]
57
+ end
58
+ vnode = VDOM::Component.new(component_class, props, children)
59
+ vnode.runtime = @component.runtime
60
+ add_child(vnode)
61
+ vnode
62
+ end
63
+
64
+ def form_for(model_key, options = {}, &block)
65
+ @component.build_form_for(self, model_key, options, &block)
66
+ end
67
+
68
+ def suspense(name, fallback:, error: nil, &block)
69
+ @component.render_suspense(self, name, fallback: fallback, error: error, &block)
70
+ end
71
+
72
+ def link_to(path, **options, &block)
73
+ @component.build_link_to(self, path, **options, &block)
74
+ end
75
+
76
+ def button_to(path, method: :post, **options, &block)
77
+ @component.build_button_to(self, path, method: method, **options, &block)
78
+ end
79
+
80
+ def capture(&block)
81
+ children = [] #: Array[Funicular::VDOM::child_t]
82
+ previous = @component.current_children
83
+ @component.current_children = children
84
+ result = block.call(self)
85
+ @component.current_children = previous
86
+
87
+ if result && !result.equal?(children) && children.empty?
88
+ if result.is_a?(Array)
89
+ result.each do |item|
90
+ if item.is_a?(String)
91
+ children << item
92
+ else
93
+ normalized = @component.normalize_vnode_for_view(item)
94
+ children << normalized if normalized
95
+ end
96
+ end
97
+ elsif result.is_a?(String)
98
+ children << result
99
+ else
100
+ normalized = @component.normalize_vnode_for_view(result)
101
+ children << normalized if normalized
102
+ end
103
+ end
104
+ children
105
+ ensure
106
+ @component.current_children = previous
107
+ end
108
+
109
+ def add_child(child)
110
+ @component.add_child_from_view(child)
111
+ end
112
+
113
+ private
114
+
115
+ def build_element(tag_name, props = {}, &block)
116
+ children = if block
117
+ capture(&block)
118
+ else
119
+ [] #: Array[Funicular::VDOM::child_t]
120
+ end
121
+ normalized_props = normalize_props(props || {})
122
+ element = VDOM::Element.new(tag_name.to_s, normalized_props, children)
123
+ add_child(element)
124
+ element
125
+ end
126
+
127
+ def normalize_props(props)
128
+ normalized = {} #: Hash[Symbol, untyped]
129
+ props.each do |key, value|
130
+ normalized[key] = key == :class && value.is_a?(StyleValue) ? value.to_s : value
131
+ end
132
+ normalized
133
+ end
134
+ end
135
+ end
data/sig/component.rbs CHANGED
@@ -3,65 +3,66 @@ module Funicular
3
3
  class StateAccessor
4
4
  def initialize: (Hash[Symbol, untyped] state_hash) -> void
5
5
  def []: (Symbol key) -> untyped
6
- def method_missing: (Symbol method, *untyped args) -> untyped
7
- def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
8
- def errors: () -> Hash[Symbol, String]
6
+ def fetch: (Symbol key, ?untyped default) -> untyped
7
+ def key?: (Symbol key) -> bool
8
+ def to_h: () -> Hash[Symbol, untyped]
9
9
  end
10
10
 
11
- HTML_TAGS: Array[String]
11
+ class ResourceAccessor
12
+ def initialize: (Hash[Symbol, untyped] data, Hash[Symbol, Symbol] states, Hash[Symbol, untyped] errors) -> void
13
+ def []: (Symbol key) -> untyped
14
+ def fetch: (Symbol key, ?untyped default) -> untyped
15
+ def loading?: (Symbol key) -> bool
16
+ def error?: (Symbol key) -> bool
17
+ def error: (Symbol key) -> untyped
18
+ end
12
19
 
13
20
  attr_accessor props: Hash[Symbol, untyped]
14
21
  attr_accessor vdom: VDOM::VNode | VDOM::Text | nil
15
22
  attr_accessor dom_element: JS::Element
16
23
  attr_accessor mounted: bool
24
+ attr_accessor runtime: Runtime
25
+ attr_accessor children: Array[VDOM::child_t]
26
+ attr_accessor current_children: Array[VDOM::child_t]?
17
27
  attr_reader refs: Hash[Symbol, JS::Element]
18
- @event_listeners: Array[Integer]
19
- @child_components: Array[Component]
20
- @suspense_data: Hash[Symbol, untyped]
21
- @suspense_states: Hash[Symbol, Symbol]
22
- @suspense_errors: Hash[Symbol, untyped]
23
- @suspense_pending_timers: Array[Integer]
24
- self.@suspense_definitions: Hash[Symbol, suspense_definition]
25
28
 
26
- def initialize: (?Hash[Symbol, untyped] props) -> void
29
+ type suspense_definition = { loader: untyped, on_resolve: untyped, min_delay: Integer? }
27
30
 
28
- # Styles DSL class methods
31
+ def initialize: (?Hash[Symbol, untyped] props) -> void
29
32
  def self.styles: () { (StyleBuilder) -> void } -> void
30
33
  def self.styles_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
31
-
32
- # Suspense DSL class methods
33
- # Note: Using untyped for Proc types to avoid instance_exec block type mismatch warnings
34
- type suspense_definition = { loader: untyped, on_resolve: untyped, min_delay: Integer? }
35
34
  def self.use_suspense: (Symbol name, untyped loader, ?on_resolve: untyped, ?min_delay: Integer) -> void
36
35
  def self.suspense_definitions: () -> Hash[Symbol, suspense_definition]
37
36
 
38
37
  def state: () -> StateAccessor
39
-
40
- # Styles DSL instance method
41
- def s: () -> StyleAccessor
42
-
43
- # Public API
38
+ def resources: () -> ResourceAccessor
39
+ def styles: () -> StyleAccessor
44
40
  def initialize_state: () -> Hash[Symbol, untyped]
41
+ def seed_state: (Hash[untyped, untyped]? state_hash) -> self
45
42
 
46
- # Suspense instance methods
47
43
  def load_suspense_data: () -> void
48
44
  def load_single_suspense: (Symbol name, ?suspense_definition? definition) -> void
49
45
  def reload_suspense: (Symbol name) -> void
50
46
  def suspense_loading?: (*Symbol names) -> bool
51
47
  def suspense_error?: (Symbol name) -> bool
52
48
  def suspense_error: (Symbol name) -> untyped
53
- def suspense: (fallback: ^() -> void, ?error: ^(untyped) -> void) { () -> void } -> void
49
+ def render_suspense: (ViewContext h, Symbol name, fallback: untyped, ?error: untyped) { (ViewContext, ResourceAccessor) -> untyped } -> untyped
54
50
 
55
51
  def patch: (Hash[Symbol, untyped] new_state) -> void
56
52
  def mount: (JS::Element container) -> void
57
53
  def hydrate: (JS::Element dom_element) -> void
58
- def seed_state: (Hash[untyped, untyped]? state_hash) -> self
59
54
  def unmount: () -> void
60
- def render: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
61
- def bind_events: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode) -> void
55
+ def render: (ViewContext h) -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
62
56
  def build_vdom: () -> (VDOM::VNode | VDOM::Text | nil)
63
57
 
64
- # Lifecycle hooks (public, can be overridden in subclasses)
58
+ def bind_events: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode) -> void
59
+ def collect_refs: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode, ?Hash[Symbol, JS::Element] refs_map) -> Hash[Symbol, JS::Element]
60
+ def normalize_vnode_for_view: (untyped value) -> (VDOM::Element | VDOM::Text | VDOM::Component | nil)
61
+ def add_child_from_view: (untyped child) -> void
62
+ def build_form_for: (ViewContext h, Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
63
+ def build_link_to: (ViewContext h, String path, **untyped options) { (ViewContext) -> untyped } -> VDOM::Element
64
+ def build_button_to: (ViewContext h, String path, ?method: Symbol, **untyped options) { (ViewContext) -> untyped } -> VDOM::Element
65
+
65
66
  def component_will_mount: () -> void
66
67
  def component_mounted: () -> void
67
68
  def component_will_update: () -> void
@@ -70,22 +71,9 @@ module Funicular
70
71
  def component_unmounted: () -> void
71
72
  def component_raised: (Exception e) -> void
72
73
 
73
- # Child component helper
74
- def component: (Class component_class, ?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Component
75
-
76
- # Rails-style form helper
77
- def form_for: (Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
78
-
79
- # Rails-style routing helpers
80
- def link_to: (String path, ?method: Symbol, ?navigate: bool, **untyped options) { -> untyped } -> VDOM::Element
81
- def method_missing: (Symbol method, *untyped args) -> untyped
82
- def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
83
-
84
- # Transition helpers
85
74
  def remove_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
86
75
  def add_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
87
76
 
88
- # Private methods
89
77
  private def handle_link_click: (String path) -> void
90
78
  private def handle_link_with_method: (String path, Symbol method) -> void
91
79
  private def handle_link_response: (HTTP::Response response, String path, Symbol method) -> void
@@ -93,57 +81,18 @@ module Funicular
93
81
  private def re_render: () -> void
94
82
  private def normalize_vnode: (untyped value) -> (VDOM::Element | VDOM::Text | VDOM::Component | nil)
95
83
  private def add_data_component_attribute: (VDOM::VNode vnode) -> void
96
- private def collect_refs: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode, ?Hash[Symbol, JS::Element] refs_map) -> Hash[Symbol, JS::Element]
97
84
  private def cleanup_events: () -> void
98
85
  private def cleanup_suspense_timers: () -> void
99
- private def add_child: (untyped child) -> void
100
86
  private def collect_child_components: (VDOM::VNode | VDOM::Text | nil vnode) -> void
101
87
  private def collect_child_components_recursive: (VDOM::VNode | VDOM::Text | nil vnode, Array[Component] components) -> void
102
-
103
- # Hydration helpers
88
+ private def collect_form_data: (untyped event, Symbol model_key) -> Hash[Symbol, untyped]
89
+ private def collect_dom_form_data: (untyped event) -> Hash[Symbol, untyped]
90
+ private def collect_state_form_data: (Symbol model_key) -> Hash[Symbol, untyped]
91
+ private def event_target: (untyped event) -> untyped
92
+ private def add_form_field_value: (Hash[Symbol, untyped] data, untyped field) -> void
104
93
  private def hydration_match?: (untyped vnode, untyped dom_element) -> bool
105
94
  private def warn_hydration_mismatch: (untyped vnode, untyped dom_element) -> void
106
95
  private def full_render_fallback: (VDOM::VNode | VDOM::Text | nil new_vdom, untyped server_dom) -> JS::Element
107
96
  private def hydrate_child_components: (untyped vnode, untyped dom_element) -> void
108
-
109
- # HTML element methods (DSL)
110
- def div: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
111
- def span: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
112
- def p: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
113
- def a: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
114
- def h1: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
115
- def h2: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
116
- def h3: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
117
- def h4: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
118
- def h5: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
119
- def h6: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
120
- def ul: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
121
- def ol: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
122
- def li: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
123
- def table: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
124
- def thead: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
125
- def tbody: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
126
- def tr: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
127
- def th: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
128
- def td: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
129
- def form: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
130
- def input: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
131
- def textarea: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
132
- def button: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
133
- def select: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
134
- def option: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
135
- def label: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
136
- def header: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
137
- def footer: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
138
- def nav: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
139
- def section: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
140
- def article: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
141
- def aside: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
142
- def img: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
143
- def video: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
144
- def audio: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
145
- def canvas: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
146
- def br: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
147
- def hr: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
148
97
  end
149
98
  end
@@ -5,10 +5,10 @@ module Funicular
5
5
  def initialize_state: () -> Hash[Symbol, untyped]
6
6
  def catch_error: (Exception error, ?Hash[Symbol, untyped]? error_info) -> bool
7
7
  def reset: () -> void
8
- def render: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
8
+ def render: (ViewContext h) -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
9
9
 
10
- private def render_fallback: () -> (VDOM::VNode | String)
11
- private def default_fallback: () -> VDOM::VNode
12
- private def render_children: () -> VDOM::VNode
10
+ private def render_fallback: (ViewContext h) -> (VDOM::VNode | String)
11
+ private def default_fallback: (ViewContext h) -> VDOM::VNode
12
+ private def render_children: (ViewContext h) -> VDOM::VNode
13
13
  end
14
14
  end
data/sig/form_builder.rbs CHANGED
@@ -1,10 +1,11 @@
1
1
  module Funicular
2
2
  class FormBuilder
3
3
  attr_reader component: Component
4
+ attr_reader view_context: ViewContext
4
5
  attr_reader model_key: Symbol
5
6
  attr_reader options: Hash[Symbol, untyped]
6
7
 
7
- def initialize: (Component component, Symbol model_key, ?Hash[Symbol, untyped] options) -> void
8
+ def initialize: (Component component, ViewContext view_context, Symbol model_key, ?Hash[Symbol, untyped] options) -> void
8
9
 
9
10
  # Field builder methods
10
11
  def text_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
@@ -4,7 +4,8 @@ module Funicular
4
4
  VOID_ELEMENTS: Array[String]
5
5
  SKIP_PROPS: Array[Symbol]
6
6
 
7
- def self.serialize: (VNode? vnode) -> String
7
+ def self.serialize: (VNode? vnode, ?Runtime? runtime) -> String
8
+ def initialize: (?Runtime? runtime) -> void
8
9
  def render: (VNode? vnode) -> String
9
10
 
10
11
  private
data/sig/http.rbs CHANGED
@@ -32,6 +32,7 @@ module Funicular
32
32
  private def self.now_seconds: () -> Integer
33
33
  private def self.cache_hit?: (untyped entry, Integer ttl) -> bool
34
34
  private def self.serve_from_cache: (Hash[String, untyped] entry) { (Response) -> void } -> void
35
+ private def self.parse_response_body: (untyped text) -> untyped
35
36
  private def self.request: (String method, String url, Hash[untyped, untyped]? body, ?cache: Integer?) { (Response) -> void } -> void
36
37
  end
37
38
  end
data/sig/patcher.rbs CHANGED
@@ -3,7 +3,7 @@ module Funicular
3
3
  BOOLEAN_ATTRIBUTES: Array[String]
4
4
 
5
5
  class Patcher
6
- def initialize: (?JS::Element? doc) -> void
6
+ def initialize: (?JS::Element? doc, ?Runtime? runtime) -> void
7
7
  # `apply` accepts any DOM node so text-node patches (e.g. :replace)
8
8
  # can recurse into it; Element-only operations are narrowed inside.
9
9
  def apply: (JS::Object element, Array[patch_t] patches) -> JS::Object
data/sig/router.rbs CHANGED
@@ -2,22 +2,12 @@ module Funicular
2
2
  type route_constraints_t = Hash[Symbol, Regexp]
3
3
  type route_definition_t = { method: Symbol, path: String, component: singleton(Component), name: String?, pattern_segments: Array[String], constraints: route_constraints_t }
4
4
 
5
- # URL helper methods are dynamically generated based on route definitions
6
- # Example: router.get('/users/:id', to: UserComponent, as: 'user')
7
- # generates: user_path(id) -> String
8
- module RouteHelpers
9
- # Dynamic methods are generated at runtime by Router#generate_url_helper
10
- # Method signatures depend on route parameters:
11
- # - No parameters: def helper_name_path: () -> String
12
- # - With parameters: def helper_name_path: (Integer | String | untyped) -> String
13
- def method_missing: (Symbol method, *untyped args) -> String
14
- def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
15
- end
16
-
17
5
  class Router
18
6
  attr_reader routes: Array[route_definition_t]
19
7
  attr_reader current_component: Component?
20
8
  attr_reader current_path: String?
9
+ attr_reader url_helpers: Module
10
+ attr_reader route_helpers: untyped
21
11
 
22
12
  def initialize: (JS::Element? container) -> void
23
13
  def get: (String path, to: singleton(Component), ?as: String?, ?constraints: route_constraints_t?) -> void
@@ -31,7 +21,6 @@ module Funicular
31
21
  def start: (?hydrate: bool) -> void
32
22
  def stop: () -> void
33
23
  def navigate: (String path) -> void
34
- def current_hash_path: () -> String
35
24
  def current_location_path: () -> String
36
25
 
37
26
  private def add_route_with_method: (Symbol method, String path, singleton(Component) component_class, String? name, ?route_constraints_t? constraints) -> void
data/sig/runtime.rbs ADDED
@@ -0,0 +1,13 @@
1
+ module Funicular
2
+ class Runtime
3
+ attr_accessor router: Router?
4
+
5
+ def initialize: (?Router? router) -> void
6
+ def routes: () -> untyped
7
+ end
8
+
9
+ module EmptyRoutes
10
+ def self.method_missing: (Symbol name, *untyped args) -> untyped
11
+ def self.respond_to_missing?: (Symbol name, ?bool include_private) -> bool
12
+ end
13
+ end
data/sig/styles.rbs CHANGED
@@ -7,11 +7,10 @@ module Funicular
7
7
  def to_s: () -> String
8
8
  end
9
9
 
10
- # StyleAccessor provides access to defined styles via method calls
10
+ # StyleAccessor provides explicit access to defined styles.
11
11
  class StyleAccessor
12
12
  def initialize: (Hash[Symbol, Hash[Symbol, untyped]] definitions) -> void
13
- private def method_missing: (Symbol name, *untyped args) -> StyleValue
14
- private def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
13
+ def []: (Symbol name, ?(Symbol | bool) variant) -> StyleValue
15
14
  end
16
15
 
17
16
  # StyleBuilder is used to define styles within the styles {} block
@@ -19,7 +18,7 @@ module Funicular
19
18
  @definitions: Hash[Symbol, Hash[Symbol, untyped]]
20
19
 
21
20
  def initialize: () -> void
21
+ def define: (Symbol name, ?(String | Hash[Symbol, untyped]) value, **untyped options) -> void
22
22
  def to_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
23
- private def method_missing: (Symbol name, *untyped args) -> void
24
23
  end
25
24
  end
data/sig/vdom.rbs CHANGED
@@ -4,6 +4,13 @@ module Funicular
4
4
  type child_t = Element | Text | Component | String | Array[Element | Text | Component | String]
5
5
 
6
6
  URL_ATTRIBUTES: Array[String]
7
+ SCRIPTING_ELEMENTS: Array[String]
8
+
9
+ def self.valid_tag_name?: (untyped name) -> bool
10
+ def self.valid_attribute_name?: (untyped name) -> bool
11
+ def self.event_attribute?: (untyped name) -> bool
12
+ def self.unsafe_url?: (untyped name, untyped value) -> bool
13
+ def self.blocked_attribute?: (untyped name, untyped value) -> bool
7
14
 
8
15
  class VNode
9
16
  attr_reader type: Symbol
@@ -32,7 +39,7 @@ module Funicular
32
39
  class Renderer
33
40
  @error_boundary_stack: Array[Funicular::ErrorBoundary]
34
41
 
35
- def initialize: (?JS::Element? doc) -> void
42
+ def initialize: (?JS::Element? doc, ?Runtime? runtime) -> void
36
43
  def render: (VDOM::VNode | VDOM::Text | nil vnode, ?JS::Element? parent) -> JS::Element
37
44
 
38
45
  private def current_error_boundary: () -> ErrorBoundary?
@@ -50,9 +57,11 @@ module Funicular
50
57
  class Component < VNode
51
58
  attr_reader component_class: untyped
52
59
  attr_reader props: Hash[Symbol, untyped]
60
+ attr_reader children: Array[child_t]
53
61
  attr_accessor instance: untyped
62
+ attr_accessor runtime: Runtime?
54
63
 
55
- def initialize: (untyped component_class, ?Hash[Symbol, untyped] props) -> void
64
+ def initialize: (untyped component_class, ?Hash[Symbol, untyped] props, ?Array[child_t] children) -> void
56
65
  def ==: (untyped other) -> bool
57
66
  end
58
67
  end