funicular 0.2.1 → 0.4.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/CHANGELOG.md +111 -0
  3. data/README.md +2 -2
  4. data/Rakefile +23 -15
  5. data/demo/test_chartjs.html +8 -8
  6. data/demo/test_component.html +8 -8
  7. data/demo/test_error_boundary.html +8 -5
  8. data/demo/test_router.html +11 -11
  9. data/demo/tic-tac-toe.html +4 -4
  10. data/docs/architecture.md +80 -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 +3 -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} +626 -486
  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 +675 -449
  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 +2 -2
  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 +860 -567
  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 +25 -25
  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/dsl_test.rb +237 -0
  37. data/minitest/fixtures/funicular_app/components/greeting_component.rb +2 -2
  38. data/minitest/funicular_test.rb +28 -2
  39. data/minitest/middleware_test.rb +154 -0
  40. data/minitest/picoruby_helper_test.rb +139 -0
  41. data/minitest/route_parser_test.rb +139 -0
  42. data/minitest/schema_test.rb +23 -0
  43. data/minitest/sig_tags_test.rb +30 -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/0_tags.rb +62 -0
  50. data/mrblib/component.rb +255 -244
  51. data/mrblib/debug.rb +7 -6
  52. data/mrblib/differ.rb +3 -1
  53. data/mrblib/error_boundary.rb +11 -13
  54. data/mrblib/form_builder.rb +28 -24
  55. data/mrblib/funicular.rb +2 -1
  56. data/mrblib/html_serializer.rb +14 -13
  57. data/mrblib/http.rb +12 -1
  58. data/mrblib/patcher.rb +12 -9
  59. data/mrblib/router.rb +9 -5
  60. data/mrblib/runtime.rb +28 -0
  61. data/mrblib/styles.rb +107 -21
  62. data/mrblib/vdom.rb +90 -9
  63. data/mrblib/view_context.rb +106 -0
  64. data/sig/component.rbs +47 -84
  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 +19 -7
  72. data/sig/tags.rbs +54 -0
  73. data/sig/vdom.rbs +11 -2
  74. data/sig/view_context.rbs +60 -0
  75. metadata +22 -5
  76. data/lib/funicular/vendor/picorbc/VERSION +0 -1
  77. 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,106 @@
1
+ module Funicular
2
+ class ViewContext
3
+ HTML_TAGS = Funicular::Tags::HTML_TAGS
4
+
5
+ def initialize(component)
6
+ @component = component
7
+ end
8
+
9
+ def tag(name, props = {}, &block)
10
+ build_element(name, props, &block)
11
+ end
12
+
13
+ HTML_TAGS.each do |tag_name|
14
+ define_method(tag_name) do |props = {}, &block|
15
+ tag(tag_name, props, &block) # steep:ignore
16
+ end
17
+ end
18
+
19
+ def component(component_class, props = {}, &block)
20
+ unless component_class.is_a?(Class) && component_class.ancestors.include?(Funicular::Component)
21
+ raise "h.component expects a Funicular::Component class"
22
+ end
23
+
24
+ children = if block
25
+ capture(&block)
26
+ else
27
+ [] #: Array[Funicular::VDOM::child_t]
28
+ end
29
+ vnode = VDOM::Component.new(component_class, props, children)
30
+ vnode.runtime = @component.runtime
31
+ add_child(vnode)
32
+ vnode
33
+ end
34
+
35
+ def form_for(model_key, options = {}, &block)
36
+ @component.build_form_for(self, model_key, options, &block)
37
+ end
38
+
39
+ def suspense(name, fallback:, error: nil, &block)
40
+ @component.render_suspense(name, fallback: fallback, error: error, &block)
41
+ end
42
+
43
+ def link_to(path, **options, &block)
44
+ @component.build_link_to(self, path, **options, &block)
45
+ end
46
+
47
+ def button_to(path, method: :post, **options, &block)
48
+ @component.build_button_to(self, path, method: method, **options, &block)
49
+ end
50
+
51
+ def capture(&block)
52
+ children = [] #: Array[Funicular::VDOM::child_t]
53
+ previous = @component.current_children
54
+ @component.current_children = children
55
+ result = block.call
56
+ @component.current_children = previous
57
+
58
+ if result && !result.equal?(children) && children.empty?
59
+ if result.is_a?(Array)
60
+ result.each do |item|
61
+ if item.is_a?(String)
62
+ children << item
63
+ else
64
+ normalized = @component.normalize_vnode_for_view(item)
65
+ children << normalized if normalized
66
+ end
67
+ end
68
+ elsif result.is_a?(String)
69
+ children << result
70
+ else
71
+ normalized = @component.normalize_vnode_for_view(result)
72
+ children << normalized if normalized
73
+ end
74
+ end
75
+ children
76
+ ensure
77
+ @component.current_children = previous
78
+ end
79
+
80
+ def add_child(child)
81
+ @component.add_child_from_view(child)
82
+ end
83
+
84
+ private
85
+
86
+ def build_element(tag_name, props = {}, &block)
87
+ children = if block
88
+ capture(&block)
89
+ else
90
+ [] #: Array[Funicular::VDOM::child_t]
91
+ end
92
+ normalized_props = normalize_props(props || {})
93
+ element = VDOM::Element.new(tag_name.to_s, normalized_props, children)
94
+ add_child(element)
95
+ element
96
+ end
97
+
98
+ def normalize_props(props)
99
+ normalized = {} #: Hash[Symbol, untyped]
100
+ props.each do |key, value|
101
+ normalized[key] = key == :class && value.is_a?(StyleValue) ? value.to_s : value
102
+ end
103
+ normalized
104
+ end
105
+ end
106
+ end
data/sig/component.rbs CHANGED
@@ -3,65 +3,80 @@ 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
19
+
20
+ include Tags
12
21
 
13
22
  attr_accessor props: Hash[Symbol, untyped]
14
23
  attr_accessor vdom: VDOM::VNode | VDOM::Text | nil
15
24
  attr_accessor dom_element: JS::Element
16
25
  attr_accessor mounted: bool
26
+ attr_accessor runtime: Runtime
27
+ attr_accessor children: Array[VDOM::child_t]
28
+ attr_accessor current_children: Array[VDOM::child_t]?
17
29
  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
30
 
26
- def initialize: (?Hash[Symbol, untyped] props) -> void
31
+ type suspense_definition = { loader: untyped, on_resolve: untyped, min_delay: Integer? }
27
32
 
28
- # Styles DSL class methods
33
+ def initialize: (?Hash[Symbol, untyped] props) -> void
34
+ def self.allow_dsl_override: (*Symbol names) -> void
35
+ def self.dsl_overrides: () -> Array[Symbol]
36
+ def self.method_added: (Symbol name) -> void
37
+ def self.validate_dsl_conflicts!: () -> void
29
38
  def self.styles: () { (StyleBuilder) -> void } -> void
30
39
  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? }
40
+ def self.style_accessor_class: () -> singleton(StyleAccessor)
35
41
  def self.use_suspense: (Symbol name, untyped loader, ?on_resolve: untyped, ?min_delay: Integer) -> void
36
42
  def self.suspense_definitions: () -> Hash[Symbol, suspense_definition]
37
43
 
38
44
  def state: () -> StateAccessor
39
-
40
- # Styles DSL instance method
41
- def s: () -> StyleAccessor
42
-
43
- # Public API
45
+ def resources: () -> ResourceAccessor
46
+ def styles: () -> StyleAccessor
44
47
  def initialize_state: () -> Hash[Symbol, untyped]
48
+ def seed_state: (Hash[untyped, untyped]? state_hash) -> self
45
49
 
46
- # Suspense instance methods
47
50
  def load_suspense_data: () -> void
48
51
  def load_single_suspense: (Symbol name, ?suspense_definition? definition) -> void
49
52
  def reload_suspense: (Symbol name) -> void
50
53
  def suspense_loading?: (*Symbol names) -> bool
51
54
  def suspense_error?: (Symbol name) -> bool
52
55
  def suspense_error: (Symbol name) -> untyped
53
- def suspense: (fallback: ^() -> void, ?error: ^(untyped) -> void) { () -> void } -> void
56
+ def render_suspense: (Symbol name, fallback: untyped, ?error: untyped) { (ResourceAccessor) -> untyped } -> untyped
54
57
 
55
58
  def patch: (Hash[Symbol, untyped] new_state) -> void
56
59
  def mount: (JS::Element container) -> void
57
60
  def hydrate: (JS::Element dom_element) -> void
58
- def seed_state: (Hash[untyped, untyped]? state_hash) -> self
59
61
  def unmount: () -> void
60
62
  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
62
63
  def build_vdom: () -> (VDOM::VNode | VDOM::Text | nil)
63
64
 
64
- # Lifecycle hooks (public, can be overridden in subclasses)
65
+ def bind_events: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode) -> void
66
+ def collect_refs: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode, ?Hash[Symbol, JS::Element] refs_map) -> Hash[Symbol, JS::Element]
67
+ def normalize_vnode_for_view: (untyped value) -> (VDOM::Element | VDOM::Text | VDOM::Component | nil)
68
+ def __view__: () -> ViewContext
69
+ def routes: () -> untyped
70
+ def component: (singleton(Component) component_class, ?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Component
71
+ def form_for: (Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
72
+ def link_to: (String path, **untyped options) ?{ -> untyped } -> VDOM::Element
73
+ def button_to: (String path, ?method: Symbol, **untyped options) ?{ -> untyped } -> VDOM::Element
74
+ def suspense: (Symbol name, fallback: untyped, ?error: untyped) { (ResourceAccessor) -> untyped } -> untyped
75
+ def add_child_from_view: (untyped child) -> void
76
+ def build_form_for: (ViewContext h, Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
77
+ def build_link_to: (ViewContext h, String path, **untyped options) ?{ () -> untyped } -> VDOM::Element
78
+ def build_button_to: (ViewContext h, String path, ?method: Symbol, **untyped options) ?{ () -> untyped } -> VDOM::Element
79
+
65
80
  def component_will_mount: () -> void
66
81
  def component_mounted: () -> void
67
82
  def component_will_update: () -> void
@@ -70,22 +85,9 @@ module Funicular
70
85
  def component_unmounted: () -> void
71
86
  def component_raised: (Exception e) -> void
72
87
 
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
88
  def remove_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
86
89
  def add_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
87
90
 
88
- # Private methods
89
91
  private def handle_link_click: (String path) -> void
90
92
  private def handle_link_with_method: (String path, Symbol method) -> void
91
93
  private def handle_link_response: (HTTP::Response response, String path, Symbol method) -> void
@@ -93,57 +95,18 @@ module Funicular
93
95
  private def re_render: () -> void
94
96
  private def normalize_vnode: (untyped value) -> (VDOM::Element | VDOM::Text | VDOM::Component | nil)
95
97
  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
98
  private def cleanup_events: () -> void
98
99
  private def cleanup_suspense_timers: () -> void
99
- private def add_child: (untyped child) -> void
100
100
  private def collect_child_components: (VDOM::VNode | VDOM::Text | nil vnode) -> void
101
101
  private def collect_child_components_recursive: (VDOM::VNode | VDOM::Text | nil vnode, Array[Component] components) -> void
102
-
103
- # Hydration helpers
102
+ private def collect_form_data: (untyped event, Symbol model_key) -> Hash[Symbol, untyped]
103
+ private def collect_dom_form_data: (untyped event) -> Hash[Symbol, untyped]
104
+ private def collect_state_form_data: (Symbol model_key) -> Hash[Symbol, untyped]
105
+ private def event_target: (untyped event) -> untyped
106
+ private def add_form_field_value: (Hash[Symbol, untyped] data, untyped field) -> void
104
107
  private def hydration_match?: (untyped vnode, untyped dom_element) -> bool
105
108
  private def warn_hydration_mismatch: (untyped vnode, untyped dom_element) -> void
106
109
  private def full_render_fallback: (VDOM::VNode | VDOM::Text | nil new_vdom, untyped server_dom) -> JS::Element
107
110
  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
111
  end
149
112
  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,19 +7,31 @@ module Funicular
7
7
  def to_s: () -> String
8
8
  end
9
9
 
10
- # StyleAccessor provides access to defined styles via method calls
11
- class StyleAccessor
10
+ # Read-side accessor. A subclass is generated per component class with a
11
+ # real method per declared style name; those methods are app-defined and
12
+ # therefore untyped (use the [] form where static checking matters).
13
+ class StyleAccessor < BasicObject
14
+ @definitions: Hash[Symbol, Hash[Symbol, untyped]]
15
+
16
+ def self.accessor_for: (Hash[Symbol, Hash[Symbol, untyped]] definitions) -> singleton(StyleAccessor)
12
17
  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
18
+ def []: (Symbol name, ?(Symbol | bool) variant) -> StyleValue
19
+ def method_missing: (Symbol name, *untyped args) -> bot
20
+ def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
15
21
  end
16
22
 
17
- # StyleBuilder is used to define styles within the styles {} block
18
- class StyleBuilder
23
+ # Definition-side cleanroom builder for the class-level styles block.
24
+ # Barewords resolve through method_missing and define styles.
25
+ class StyleBuilder < BasicObject
26
+ RESERVED_NAMES: Array[Symbol]
27
+
19
28
  @definitions: Hash[Symbol, Hash[Symbol, untyped]]
20
29
 
21
30
  def initialize: () -> void
31
+ def define: (Symbol name, ?(String | Hash[Symbol, untyped])? value, **untyped options) -> self
32
+ def validate_options: (Symbol name, Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
22
33
  def to_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
23
- private def method_missing: (Symbol name, *untyped args) -> void
34
+ def method_missing: (Symbol name, ?untyped value, **untyped options) -> self
35
+ def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
24
36
  end
25
37
  end
data/sig/tags.rbs ADDED
@@ -0,0 +1,54 @@
1
+ module Funicular
2
+ class DSLCollisionError < StandardError
3
+ end
4
+
5
+ class RenderContextError < StandardError
6
+ end
7
+
8
+ module Tags
9
+ HTML_TAGS: Array[String]
10
+ HELPER_NAMES: Array[String]
11
+ RESERVED_DSL: Hash[Symbol, Symbol]
12
+
13
+ def tag: (Symbol | String name, ?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
14
+ def div: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
15
+ def span: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
16
+ def p: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
17
+ def a: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
18
+ def data: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
19
+ def h1: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
20
+ def h2: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
21
+ def h3: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
22
+ def h4: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
23
+ def h5: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
24
+ def h6: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
25
+ def ul: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
26
+ def ol: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
27
+ def li: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
28
+ def table: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
29
+ def thead: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
30
+ def tbody: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
31
+ def tr: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
32
+ def th: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
33
+ def td: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
34
+ def form: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
35
+ def input: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
36
+ def textarea: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
37
+ def button: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
38
+ def select: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
39
+ def option: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
40
+ def label: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
41
+ def header: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
42
+ def footer: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
43
+ def nav: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
44
+ def section: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
45
+ def article: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
46
+ def aside: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
47
+ def img: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
48
+ def video: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
49
+ def audio: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
50
+ def canvas: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
51
+ def br: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
52
+ def hr: (?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Element
53
+ end
54
+ end