zephyr_rb 1.0.1 → 1.0.3
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.
- checksums.yaml +4 -4
- data/exe/zephyr-rb +6 -0
- data/lib/{cli.rb → zephyr_rb/cli.rb} +3 -3
- data/lib/zephyr_rb/version.rb +1 -1
- data/src/browser.script.iife.js +3052 -0
- data/src/component.rb +132 -0
- data/src/components.rb +934 -0
- data/src/dom_builder.rb +94 -0
- data/src/registry.rb +30 -0
- data/src/zephyr-bridge.js +69 -0
- data/src/zephyr_wasm.rb +86 -0
- metadata +26 -8
- /data/lib/{zephyr_rb/zephyr_rb.rb → zephyr_rb.rb} +0 -0
data/src/dom_builder.rb
ADDED
@@ -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
|
+
})();
|
data/src/zephyr_wasm.rb
ADDED
@@ -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.
|
4
|
+
version: 1.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jesse Glover
|
8
|
-
bindir:
|
8
|
+
bindir: exe
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
|
-
|
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
|
-
-
|
23
|
+
- exe/zephyr-rb
|
24
|
+
- lib/zephyr_rb.rb
|
25
|
+
- lib/zephyr_rb/cli.rb
|
19
26
|
- lib/zephyr_rb/version.rb
|
20
|
-
-
|
21
|
-
|
22
|
-
|
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:
|
48
|
+
version: 2.7.0
|
31
49
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
32
50
|
requirements:
|
33
51
|
- - ">="
|
File without changes
|