zephyr_rb 1.0.1 → 1.0.2

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.
@@ -0,0 +1,94 @@
1
+ # dom_builder.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'js'
5
+
6
+ module ZephyrWasm
7
+ class DOMBuilder
8
+ attr_reader :root, :component
9
+
10
+ def initialize(root, component)
11
+ @root = root
12
+ @component = component
13
+ @doc = JS.global[:document]
14
+ @fragment = @doc.call(:createDocumentFragment)
15
+ @stack = [@fragment] # current parent stack
16
+ end
17
+
18
+ def method_missing(method_name, *args, **attrs, &block)
19
+ tag(method_name.to_s, *args, **attrs, &block)
20
+ end
21
+
22
+ def tag(name, *args, **attrs, &block)
23
+ el = @doc.call(:createElement, name)
24
+
25
+ # attributes & event handlers
26
+ attrs.each do |key, value|
27
+ next if value.nil?
28
+
29
+ if key.to_s.start_with?('on_') # event handler
30
+ event = key.to_s.sub('on_', '')
31
+ handler =
32
+ if value.respond_to?(:to_proc)
33
+ # Ruby lambda/proc provided (common case before you call .to_js)
34
+ value.to_js
35
+ elsif value.respond_to?(:to_js)
36
+ # Ruby object with .to_js (e.g., you passed ->{}.to_js explicitly)
37
+ value.to_js
38
+ else
39
+ # Already a JS::Object / native function
40
+ value
41
+ end
42
+ el.call(:addEventListener, event, handler)
43
+
44
+ elsif key == :class || key == :classes
45
+ el[:className] = value.to_s
46
+
47
+ elsif key == :style && value.is_a?(Hash)
48
+ value.each { |k, v| el[:style][k.to_s.gsub('_', '-')] = v }
49
+
50
+ elsif key == :checked || key == :disabled || key == :selected
51
+ # boolean properties should be set on the property, not attribute
52
+ el[key] = !!value
53
+
54
+ else
55
+ el.call(:setAttribute, key.to_s, value.to_s)
56
+ end
57
+ end
58
+
59
+ el[:textContent] = args.first.to_s if args.any?
60
+
61
+ # append to current parent; descend if block given
62
+ parent = @stack.last
63
+ parent.call(:appendChild, el)
64
+ if block
65
+ @stack.push(el)
66
+ yield
67
+ @stack.pop
68
+ end
69
+
70
+ el
71
+ end
72
+
73
+ def text(content)
74
+ node = @doc.call(:createTextNode, content.to_s)
75
+ @stack.last.call(:appendChild, node)
76
+ node
77
+ end
78
+
79
+ def apply
80
+ @root.call(:replaceChildren, @fragment)
81
+ # reset for the next render
82
+ @fragment = @doc.call(:createDocumentFragment)
83
+ @stack = [@fragment]
84
+ end
85
+
86
+ # Helpers
87
+ def render_if(condition, &block)
88
+ yield if condition
89
+ end
90
+ def render_each(collection, &block)
91
+ collection.each { |item| block.call(item) }
92
+ end
93
+ end
94
+ end
data/src/registry.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZephyrWasm
4
+ module Registry
5
+ class << self
6
+ def components
7
+ @components ||= {}
8
+ end
9
+
10
+ def register(tag_name, component_class)
11
+ if components.key?(tag_name)
12
+ warn "Component '#{tag_name}' is already registered. Overwriting."
13
+ end
14
+ components[tag_name] = component_class
15
+ end
16
+
17
+ def get(tag_name)
18
+ components[tag_name]
19
+ end
20
+
21
+ def all
22
+ components
23
+ end
24
+
25
+ def clear
26
+ @components = {}
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,69 @@
1
+ // zephyr-bridge.js
2
+ (() => {
3
+ const defined = new Set();
4
+ const pending = new Map();
5
+ let scheduled = false;
6
+
7
+ function defineOne(tag, meta) {
8
+ if (!tag || customElements.get(tag) || defined.has(tag)) return;
9
+
10
+ class RubyBackedElement extends HTMLElement {
11
+ static get observedAttributes() {
12
+ return (meta && meta.observedAttributes) || [];
13
+ }
14
+ connectedCallback() {
15
+ // Safe: this runs in a clean JS task after our scheduler fires
16
+ window.ZephyrWasm?.initComponent?.(this, tag);
17
+ }
18
+ disconnectedCallback() {
19
+ window.ZephyrWasm?.disconnectComponent?.(this);
20
+ }
21
+ attributeChangedCallback(name, oldValue, newValue) {
22
+ window.ZephyrWasm?.attributeChangedComponent?.(this, name, oldValue, newValue);
23
+ }
24
+ }
25
+
26
+ customElements.define(tag, RubyBackedElement);
27
+ defined.add(tag);
28
+ window.log?.(`✅ Registered component: ${tag}`);
29
+ }
30
+
31
+ function flush() {
32
+ scheduled = false;
33
+ for (const [tag, meta] of pending) defineOne(tag, meta);
34
+ pending.clear();
35
+ // Optional: reveal content area if you hide UI while loading
36
+ const loading = document.getElementById('loading');
37
+ const content = document.getElementById('content');
38
+ if (loading && content) {
39
+ loading.style.display = 'none';
40
+ content.style.display = 'block';
41
+ window.log?.('🎉 Zephyr WASM is ready!');
42
+ }
43
+ }
44
+
45
+ function scheduleDefine(tag, meta) {
46
+ pending.set(tag, meta);
47
+ if (!scheduled) {
48
+ scheduled = true;
49
+ // Use a macrotask (setTimeout) to ensure Ruby has fully unwound
50
+ setTimeout(flush, 0);
51
+ }
52
+ }
53
+
54
+ // Wrap ZephyrWasmRegistry so Ruby writes schedule a later define
55
+ const existing = window.ZephyrWasmRegistry || {};
56
+ const proxy = new Proxy(existing, {
57
+ set(target, prop, value) {
58
+ target[prop] = value;
59
+ scheduleDefine(prop, value);
60
+ return true;
61
+ }
62
+ });
63
+ window.ZephyrWasmRegistry = proxy;
64
+
65
+ // If Ruby populated entries before this script loaded, schedule them now
66
+ for (const [tag, meta] of Object.entries(existing)) {
67
+ scheduleDefine(tag, meta);
68
+ }
69
+ })();
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'js'
4
+ require 'json'
5
+
6
+ # Zephyr WASM - Ruby Web Components compiled to WebAssembly
7
+ # This runs entirely in the browser using ruby.wasm
8
+ module ZephyrWasm
9
+ @@instance_map = ObjectSpace::WeakMap.new
10
+
11
+ class << self
12
+ def component(tag_name, &block)
13
+ raise ArgumentError, "Tag name must contain a hyphen" unless tag_name.include?('-')
14
+
15
+ component_class = Class.new(Component)
16
+ component_class.tag_name = tag_name
17
+ component_class.class_eval(&block) if block_given?
18
+
19
+ Registry.register(tag_name, component_class)
20
+
21
+ # Define the custom element in the browser
22
+ define_custom_element(tag_name, component_class)
23
+
24
+ component_class
25
+ end
26
+
27
+ def define_custom_element(tag_name, component_class)
28
+ # Store observed attributes for this component
29
+ observed_attrs = component_class.observed_attrs || []
30
+
31
+ # Initialize registry if needed (safe operation)
32
+ unless JS.global[:ZephyrWasmRegistry]
33
+ JS.global[:ZephyrWasmRegistry] = {}.to_js
34
+ end
35
+
36
+ # Store component metadata (safe - just property assignment)
37
+ JS.global[:ZephyrWasmRegistry][tag_name] = {
38
+ observedAttributes: observed_attrs
39
+ }.to_js
40
+
41
+ # JavaScript will poll this registry and register the custom elements
42
+ # This avoids nested VM operations during initialization
43
+ end
44
+
45
+ def init_component(element, tag_name)
46
+ component_class = Registry.get(tag_name)
47
+ return unless component_class
48
+
49
+ # Create Ruby component instance
50
+ instance = component_class.new(element)
51
+
52
+ # Store in weak map instead of on element
53
+ @@instance_map[element] = instance
54
+
55
+ # Call lifecycle method
56
+ instance.connected
57
+
58
+ # Initial render
59
+ instance.render
60
+ end
61
+
62
+ def disconnect_component(element)
63
+ instance = @@instance_map[element]
64
+ instance.disconnected if instance
65
+ @@instance_map.delete(element)
66
+ end
67
+
68
+ def attribute_changed_component(element, name, old_value, new_value)
69
+ instance = @@instance_map[element]
70
+ instance.attribute_changed(name, old_value, new_value) if instance
71
+ end
72
+ end
73
+ end
74
+
75
+ # Expose to JavaScript
76
+ JS.global[:ZephyrWasm] = {
77
+ initComponent: ->(element, tag_name) {
78
+ ZephyrWasm.init_component(element, tag_name.to_s)
79
+ }.to_js,
80
+ disconnectComponent: ->(element) {
81
+ ZephyrWasm.disconnect_component(element)
82
+ }.to_js,
83
+ attributeChangedComponent: ->(element, name, old_value, new_value) {
84
+ ZephyrWasm.attribute_changed_component(element, name.to_s, old_value, new_value)
85
+ }.to_js
86
+ }.to_js
metadata CHANGED
@@ -1,25 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zephyr_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Glover
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- executables: []
12
+ description: ZephyrRb lets you build reactive web components using Ruby and WebAssembly.
13
+ Write your UI logic in Ruby with a declarative template syntax.
14
+ email:
15
+ - jesco@rpdevstudios.com
16
+ executables:
17
+ - zephyr-rb
13
18
  extensions: []
14
19
  extra_rdoc_files: []
15
20
  files:
16
21
  - README.md
17
22
  - dist/zephyrRB.js
18
- - lib/cli.rb
23
+ - exe/zephyr-rb
24
+ - lib/zephyr_rb.rb
25
+ - lib/zephyr_rb/cli.rb
19
26
  - lib/zephyr_rb/version.rb
20
- - lib/zephyr_rb/zephyr_rb.rb
21
- licenses: []
22
- metadata: {}
27
+ - src/browser.script.iife.js
28
+ - src/component.rb
29
+ - src/components.rb
30
+ - src/dom_builder.rb
31
+ - src/registry.rb
32
+ - src/zephyr-bridge.js
33
+ - src/zephyr_wasm.rb
34
+ homepage: https://github.com/RPDevJesco/ZephyrRB
35
+ licenses:
36
+ - MIT
37
+ metadata:
38
+ homepage_uri: https://github.com/RPDevJesco/ZephyrRB
39
+ source_code_uri: https://github.com/RPDevJesco/ZephyrRB
40
+ bug_tracker_uri: https://github.com/RPDevJesco/ZephyrRB/issues
23
41
  rdoc_options: []
24
42
  require_paths:
25
43
  - lib
@@ -27,7 +45,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
27
45
  requirements:
28
46
  - - ">="
29
47
  - !ruby/object:Gem::Version
30
- version: '0'
48
+ version: 2.7.0
31
49
  required_rubygems_version: !ruby/object:Gem::Requirement
32
50
  requirements:
33
51
  - - ">="
File without changes