ruby_wasm_ui 0.8.3 → 0.9.1

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +0 -2
  3. data/Makefile +56 -0
  4. data/README.md +26 -6
  5. data/examples/.gitignore +4 -0
  6. data/examples/Gemfile.lock +15 -17
  7. data/examples/src/index.html +21 -0
  8. data/examples/src/index.rb +26 -0
  9. data/exe/ruby-wasm-ui +6 -0
  10. data/lib/ruby_wasm_ui/cli/command/base.rb +192 -0
  11. data/lib/ruby_wasm_ui/cli/command/dev.rb +207 -0
  12. data/lib/ruby_wasm_ui/cli/command/pack.rb +36 -0
  13. data/lib/ruby_wasm_ui/cli/command/rebuild.rb +38 -0
  14. data/lib/ruby_wasm_ui/cli/command/setup.rb +159 -0
  15. data/lib/ruby_wasm_ui/cli/command.rb +48 -0
  16. data/lib/ruby_wasm_ui/version.rb +1 -1
  17. data/lib/ruby_wasm_ui.rb +8 -8
  18. data/package-lock.json +2 -2
  19. data/package.json +1 -1
  20. data/packages/npm-packages/runtime/package-lock.json +2 -2
  21. data/packages/npm-packages/runtime/package.json +1 -1
  22. data/packages/npm-packages/runtime/rollup.config.mjs +1 -1
  23. data/spec/ruby_wasm_ui/cli/command/base_spec.rb +503 -0
  24. data/spec/ruby_wasm_ui/cli/command/dev_spec.rb +442 -0
  25. data/spec/ruby_wasm_ui/cli/command/pack_spec.rb +131 -0
  26. data/spec/ruby_wasm_ui/cli/command/rebuild_spec.rb +95 -0
  27. data/spec/ruby_wasm_ui/cli/command/setup_spec.rb +251 -0
  28. data/spec/ruby_wasm_ui/cli/command_spec.rb +118 -0
  29. data/{packages/npm-packages/runtime/spec → spec}/spec_helper.rb +1 -1
  30. metadata +96 -38
  31. data/packages/npm-packages/runtime/Gemfile +0 -3
  32. data/packages/npm-packages/runtime/Gemfile.lock +0 -26
  33. /data/lib/ruby_wasm_ui/{app.rb → runtime/app.rb} +0 -0
  34. /data/lib/ruby_wasm_ui/{component.rb → runtime/component.rb} +0 -0
  35. /data/lib/ruby_wasm_ui/{dispatcher.rb → runtime/dispatcher.rb} +0 -0
  36. /data/lib/ruby_wasm_ui/{dom → runtime/dom}/attributes.rb +0 -0
  37. /data/lib/ruby_wasm_ui/{dom → runtime/dom}/destroy_dom.rb +0 -0
  38. /data/lib/ruby_wasm_ui/{dom → runtime/dom}/events.rb +0 -0
  39. /data/lib/ruby_wasm_ui/{dom → runtime/dom}/mount_dom.rb +0 -0
  40. /data/lib/ruby_wasm_ui/{dom → runtime/dom}/patch_dom.rb +0 -0
  41. /data/lib/ruby_wasm_ui/{dom → runtime/dom}/scheduler.rb +0 -0
  42. /data/lib/ruby_wasm_ui/{dom.rb → runtime/dom.rb} +0 -0
  43. /data/lib/ruby_wasm_ui/{nodes_equal.rb → runtime/nodes_equal.rb} +0 -0
  44. /data/lib/ruby_wasm_ui/{template → runtime/template}/build_conditional_group.rb +0 -0
  45. /data/lib/ruby_wasm_ui/{template → runtime/template}/build_for_group.rb +0 -0
  46. /data/lib/ruby_wasm_ui/{template → runtime/template}/build_vdom.rb +0 -0
  47. /data/lib/ruby_wasm_ui/{template → runtime/template}/parser.rb +0 -0
  48. /data/lib/ruby_wasm_ui/{template.rb → runtime/template.rb} +0 -0
  49. /data/lib/ruby_wasm_ui/{utils → runtime/utils}/arrays.rb +0 -0
  50. /data/lib/ruby_wasm_ui/{utils → runtime/utils}/objects.rb +0 -0
  51. /data/lib/ruby_wasm_ui/{utils → runtime/utils}/props.rb +0 -0
  52. /data/lib/ruby_wasm_ui/{utils → runtime/utils}/strings.rb +0 -0
  53. /data/lib/ruby_wasm_ui/{utils.rb → runtime/utils.rb} +0 -0
  54. /data/lib/ruby_wasm_ui/{vdom.rb → runtime/vdom.rb} +0 -0
  55. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/component_spec.rb +0 -0
  56. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/dom/scheduler_spec.rb +0 -0
  57. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/nodes_equal_spec.rb +0 -0
  58. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/template/build_conditional_group_spec.rb +0 -0
  59. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/template/build_for_group_spec.rb +0 -0
  60. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/template/build_vdom_spec.rb +0 -0
  61. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/template/parser_spec.rb +0 -0
  62. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/utils/arrays_spec.rb +0 -0
  63. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/utils/objects_spec.rb +0 -0
  64. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/utils/props_spec.rb +0 -0
  65. /data/{packages/npm-packages/runtime/spec/ruby_wasm_ui → spec/ruby_wasm_ui/runtime}/utils/strings_spec.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b322433e65bc63155825e97695c798bce90602a54e52797cd040aea67b909751
4
- data.tar.gz: 1655781e0e93952e5f2ecc2e450d5a9286718979f3201f0169ae35dc94979872
3
+ metadata.gz: bd9d0a33dc10a59959527fb70eb2080fb612f5c1d07cea603eb23ca5c957f342
4
+ data.tar.gz: c5dad3fabee2912ea43fe45d0fdbc899357323e127edd572b5572d4cc474a3cd
5
5
  SHA512:
6
- metadata.gz: 97e3cf3322340377c5cb75d74796bd20096d604d9ef1efe87ed9598d8394ae76ad6d7b481c43c3e721b683208489083164b6c7730b09106f4dceefdb8647a52c
7
- data.tar.gz: 7d04075d81325a78375ffbd9d711e82af3a48531acc9eec340f6d71df76501741d4573bae368eca7ae14c752c1f4fb8c894ee028007c028df39cab2595ef10a1
6
+ metadata.gz: 7cd4c88f0f24adb3521448e94eae17a69836b517bf916e752e8dcb69793d8ec22930eda80a5492f4f5118fabeb546112e6bf142854beee415574ba55474f7b9c
7
+ data.tar.gz: 316e8854f16c214e0f0ce488584d5c3c0ace723fe7b5cd47f45e3961367c29cb72073772519944c71e6d1e8d50db3020e25844c3f6f2224c2dda4412fe414ccf
@@ -24,10 +24,8 @@ jobs:
24
24
 
25
25
  - name: Install dependencies
26
26
  run: |
27
- cd packages/npm-packages/runtime
28
27
  bundle install
29
28
 
30
29
  - name: Run RSpec
31
30
  run: |
32
- cd packages/npm-packages/runtime
33
31
  bundle exec rspec
data/Makefile ADDED
@@ -0,0 +1,56 @@
1
+ .PHONY: bump help
2
+
3
+ help:
4
+ @echo "Usage: make bump <version>"
5
+ @echo " make bump VERSION=<version>"
6
+ @echo "Example: make bump 0.9.0"
7
+ @echo ""
8
+ @echo "This command updates the version number in the following files:"
9
+ @echo " - lib/ruby_wasm_ui/version.rb"
10
+ @echo " - package.json"
11
+ @echo " - packages/npm-packages/runtime/package.json"
12
+ @echo " - README.md"
13
+ @echo " - package-lock.json (via npm install)"
14
+ @echo " - packages/npm-packages/runtime/package-lock.json (via npm install)"
15
+
16
+ # Get version from position argument (if VERSION is not already set)
17
+ VERSION_ARG := $(word 2,$(MAKECMDGOALS))
18
+ ifndef VERSION
19
+ ifneq ($(VERSION_ARG),)
20
+ VERSION := $(VERSION_ARG)
21
+ endif
22
+ endif
23
+
24
+ bump:
25
+ @if [ -z "$(VERSION)" ]; then \
26
+ echo "Error: VERSION is required. Usage: make bump 0.9.0"; \
27
+ exit 1; \
28
+ fi
29
+ @echo "Bumping version to $(VERSION)..."
30
+ @# Update lib/ruby_wasm_ui/version.rb
31
+ @sed -i '' 's/VERSION = ".*"/VERSION = "$(VERSION)"/' lib/ruby_wasm_ui/version.rb
32
+ @echo "✓ Updated lib/ruby_wasm_ui/version.rb"
33
+ @# Update Gemfile.lock
34
+ @bundle install
35
+ @echo "✓ Updated Gemfile.lock"
36
+ @# Update root package.json
37
+ @sed -i '' 's/"version": ".*"/"version": "$(VERSION)"/' package.json
38
+ @echo "✓ Updated package.json"
39
+ @# Update packages/npm-packages/runtime/package.json
40
+ @sed -i '' 's/"version": ".*"/"version": "$(VERSION)"/' packages/npm-packages/runtime/package.json
41
+ @echo "✓ Updated packages/npm-packages/runtime/package.json"
42
+ @# Update README.md (unpkg.com URL)
43
+ @sed -i '' 's|unpkg.com/ruby-wasm-ui@[0-9.]*|unpkg.com/ruby-wasm-ui@$(VERSION)|' README.md
44
+ @echo "✓ Updated README.md"
45
+ @echo ""
46
+ @echo "Updating package-lock.json files..."
47
+ @npm install --package-lock-only
48
+ @echo "✓ Updated root package-lock.json"
49
+ @cd packages/npm-packages/runtime && npm install --package-lock-only && cd ../../..
50
+ @echo "✓ Updated packages/npm-packages/runtime/package-lock.json"
51
+ @echo ""
52
+ @echo "Version bumped to $(VERSION) successfully!"
53
+
54
+ # Prevent make from treating the version argument as a target
55
+ %:
56
+ @:
data/README.md CHANGED
@@ -21,7 +21,7 @@ Create an HTML file:
21
21
  <!DOCTYPE html>
22
22
  <html>
23
23
  <head>
24
- <script src="https://unpkg.com/ruby-wasm-ui@0.8.3"></script>
24
+ <script src="https://unpkg.com/ruby-wasm-ui@0.9.1"></script>
25
25
  <script defer type="text/ruby" src="app.rb"></script>
26
26
  </head>
27
27
  <body>
@@ -112,19 +112,39 @@ bundle install
112
112
 
113
113
  ### Building Your Application
114
114
 
115
- 1. Build Ruby WASM:
115
+ 1. Set up your project (first time only):
116
116
 
117
117
  ```bash
118
- bundle exec rbwasm build --ruby-version 3.4 -o ruby.wasm
118
+ bundle exec ruby-wasm-ui setup
119
119
  ```
120
120
 
121
- 2. Pack your application files:
121
+ This command will:
122
+ - Configure excluded gems for WASM build
123
+ - Build Ruby WASM file
124
+ - Update `.gitignore`
125
+ - Create initial `src/index.html` and `src/index.rb` files
126
+
127
+ **Additional Commands:**
128
+
129
+ - **Development server**: Start a development server with file watching and auto-build:
130
+ ```bash
131
+ bundle exec ruby-wasm-ui dev
132
+ ```
133
+
134
+ - **Rebuild Ruby WASM**: Rebuild the Ruby WASM file when you add new gems:
135
+ ```bash
136
+ bundle exec ruby-wasm-ui rebuild
137
+ ```
138
+
139
+ ### Deployment
140
+
141
+ Pack your application files for deployment:
122
142
 
123
143
  ```bash
124
- bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm
144
+ bundle exec ruby-wasm-ui pack
125
145
  ```
126
146
 
127
- This command packs your Ruby files from the `./src` directory into the WASM file.
147
+ This command packs your Ruby files from the `./src` directory into the WASM file and outputs to the `dist` directory for deployment.
128
148
 
129
149
  ### Creating Your HTML File
130
150
 
@@ -0,0 +1,4 @@
1
+ ruby.wasm
2
+ /rubies
3
+ /build
4
+ /dist
@@ -1,38 +1,36 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- ruby_wasm_ui (0.8.3)
4
+ ruby_wasm_ui (0.9.0)
5
5
  js (~> 2.7)
6
+ listen (~> 3.8)
7
+ puma (~> 6.0)
8
+ rack (~> 3.0)
6
9
  ruby_wasm (~> 2.7)
7
10
 
8
11
  GEM
9
12
  remote: https://rubygems.org/
10
13
  specs:
14
+ ffi (1.17.2-arm64-darwin)
11
15
  js (2.7.2)
16
+ listen (3.9.0)
17
+ rb-fsevent (~> 0.10, >= 0.10.3)
18
+ rb-inotify (~> 0.9, >= 0.9.10)
12
19
  logger (1.7.0)
20
+ nio4r (2.7.5)
21
+ puma (6.6.1)
22
+ nio4r (~> 2.0)
23
+ rack (3.2.4)
24
+ rb-fsevent (0.11.2)
25
+ rb-inotify (0.11.1)
26
+ ffi (~> 1.0)
13
27
  ruby_wasm (2.7.2)
14
28
  logger
15
- ruby_wasm (2.7.2-aarch64-linux)
16
- logger
17
- ruby_wasm (2.7.2-aarch64-linux-musl)
18
- logger
19
29
  ruby_wasm (2.7.2-arm64-darwin)
20
30
  logger
21
- ruby_wasm (2.7.2-x86_64-darwin)
22
- logger
23
- ruby_wasm (2.7.2-x86_64-linux)
24
- logger
25
- ruby_wasm (2.7.2-x86_64-linux-musl)
26
- logger
27
31
 
28
32
  PLATFORMS
29
- aarch64-linux
30
- aarch64-linux-musl
31
33
  arm64-darwin
32
- ruby
33
- x86_64-darwin
34
- x86_64-linux
35
- x86_64-linux-musl
36
34
 
37
35
  DEPENDENCIES
38
36
  ruby_wasm_ui!
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>My App</title>
6
+ <script type="module">
7
+ import { DefaultRubyVM } from "https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.7.2/dist/browser/+esm";
8
+ const response = await fetch("./src.wasm");
9
+ const module = await WebAssembly.compileStreaming(response);
10
+ const { vm } = await DefaultRubyVM(module);
11
+ vm.evalAsync(`
12
+ require "ruby_wasm_ui"
13
+ require_relative './src/index.rb'
14
+ `);
15
+ </script>
16
+ </head>
17
+ <body>
18
+ <h1>My App</h1>
19
+ <div id="app"></div>
20
+ </body>
21
+ </html>
@@ -0,0 +1,26 @@
1
+ # Simple Hello World component
2
+ HelloComponent = RubyWasmUi.define_component(
3
+ state: ->(props) {
4
+ { message: props[:message] || "Hello, Ruby WASM UI!" }
5
+ },
6
+ template: ->() {
7
+ RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding)
8
+ <div>
9
+ <h2>{state[:message]}</h2>
10
+ <button on="{ click: -> { update_message } }">
11
+ Click me!
12
+ </button>
13
+ </div>
14
+ HTML
15
+ },
16
+ methods: {
17
+ update_message: ->() {
18
+ update_state(message: "You clicked the button!")
19
+ }
20
+ }
21
+ )
22
+
23
+ # Create and mount the app
24
+ app = RubyWasmUi::App.create(HelloComponent, message: "Hello, Ruby WASM UI!")
25
+ app_element = JS.global[:document].getElementById("app")
26
+ app.mount(app_element)
data/exe/ruby-wasm-ui ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/ruby_wasm_ui/cli/command"
5
+
6
+ RubyWasmUi::Cli::Command.run(ARGV)
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "open3"
5
+ require "bundler/setup"
6
+ require "ruby_wasm"
7
+ require "ruby_wasm/cli"
8
+
9
+ module RubyWasmUi
10
+ module Cli
11
+ class Command
12
+ class Base
13
+ def self.description
14
+ nil
15
+ end
16
+
17
+ def run(_argv)
18
+ raise NotImplementedError, "Subclasses must implement #run"
19
+ end
20
+
21
+ protected
22
+
23
+ def run_command(command, exit_on_error: true)
24
+ log_debug("Executing command: #{command}")
25
+
26
+ Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
27
+ stdin.close
28
+
29
+ # Read stdout and stderr in separate threads for real-time output
30
+ stdout_thread = Thread.new do
31
+ stdout.each_line do |line|
32
+ print line
33
+ $stdout.flush
34
+ end
35
+ end
36
+
37
+ stderr_thread = Thread.new do
38
+ stderr.each_line do |line|
39
+ $stderr.print line
40
+ $stderr.flush
41
+ end
42
+ end
43
+
44
+ # Wait for both threads to finish reading
45
+ stdout_thread.join
46
+ stderr_thread.join
47
+
48
+ # Wait for the process to finish
49
+ exit_status = wait_thr.value
50
+
51
+ unless exit_status.success?
52
+ log_error("Command failed: #{command}")
53
+ if exit_on_error
54
+ raise SystemExit.new(exit_status.exitstatus)
55
+ else
56
+ return false
57
+ end
58
+ end
59
+
60
+ true
61
+ end
62
+ end
63
+
64
+ def ensure_src_directory
65
+ unless Dir.exist?("src")
66
+ log_error("src directory not found. Please run 'ruby-wasm-ui setup' first.")
67
+ raise SystemExit.new(1)
68
+ end
69
+ end
70
+
71
+ def ensure_ruby_wasm
72
+ unless File.exist?("ruby.wasm")
73
+ log_error("ruby.wasm not found. Please run 'ruby-wasm-ui setup' first.")
74
+ raise SystemExit.new(1)
75
+ end
76
+ end
77
+
78
+ protected
79
+
80
+ def log_info(message)
81
+ puts "[INFO] #{message}"
82
+ end
83
+
84
+ def log_success(message)
85
+ puts "[SUCCESS] #{message}"
86
+ end
87
+
88
+ def log_error(message)
89
+ puts "[ERROR] #{message}"
90
+ end
91
+
92
+ def log_debug(message)
93
+ puts "[DEBUG] #{message}"
94
+ end
95
+
96
+ def check_ruby_version
97
+ ruby_version = RUBY_VERSION.split('.').map(&:to_i)
98
+ major, minor = ruby_version[0], ruby_version[1]
99
+
100
+ if major < 3 || (major == 3 && minor < 2)
101
+ log_error("Ruby WASM requires Ruby 3.2 or higher. Current version: #{RUBY_VERSION}")
102
+ raise SystemExit.new(1)
103
+ end
104
+
105
+ "#{major}.#{minor}"
106
+ end
107
+
108
+ def configure_excluded_gems
109
+ # Get all gems from Bundler definition including all dependencies
110
+ definition = Bundler.definition
111
+ resolved = definition.resolve
112
+
113
+ # Get all resolved specs (including transitive dependencies)
114
+ all_specs = resolved.materialize(definition.requested_dependencies)
115
+ gem_names = all_specs.map(&:name).uniq
116
+
117
+ # Always exclude gems that cause WASM build errors
118
+ # These gems have native extensions that don't work in WASM environment
119
+ always_excluded = %w[nio4r puma rack listen ffi]
120
+
121
+ # Exclude gems that cause build errors or are unnecessary for WASM
122
+ # Keep essential gems like 'js' for WASM
123
+ excluded_gems = gem_names.select do |gem_name|
124
+ # Exclude gems that cause WASM build errors
125
+ always_excluded.include?(gem_name) ||
126
+ # Exclude development/test gems
127
+ gem_name.start_with?("rspec", "rubocop", "rake")
128
+ end
129
+
130
+ # Always add always_excluded gems to ensure they're excluded even if not in dependencies
131
+ excluded_gems.concat(always_excluded)
132
+ excluded_gems.uniq!
133
+
134
+ # Add to EXCLUDED_GEMS
135
+ RubyWasm::Packager::EXCLUDED_GEMS.concat(excluded_gems)
136
+ end
137
+
138
+ def build_ruby_wasm(ruby_version_str)
139
+ command = ["build", "--ruby-version", ruby_version_str, "-o", "ruby.wasm"]
140
+ cli = RubyWasm::CLI.new(stdout: $stdout, stderr: $stderr)
141
+ cli.run(command)
142
+ end
143
+
144
+ def ensure_dist_directory
145
+ unless Dir.exist?("dist")
146
+ Dir.mkdir("dist")
147
+ log_info("Created dist directory")
148
+ end
149
+ end
150
+
151
+ def copy_non_ruby_files
152
+ log_info("Copying non-Ruby files from src to dist...")
153
+
154
+ copied_files = []
155
+ Dir.glob("src/**/*").each do |src_path|
156
+ next if File.directory?(src_path)
157
+ next if src_path.end_with?(".rb")
158
+
159
+ # Get relative path from src directory
160
+ relative_path = src_path.sub(/^src\//, "")
161
+ dest_path = File.join("dist", relative_path)
162
+
163
+ # Create destination directory if needed
164
+ dest_dir = File.dirname(dest_path)
165
+ FileUtils.mkdir_p(dest_dir) unless Dir.exist?(dest_dir)
166
+
167
+ # Copy file
168
+ FileUtils.cp(src_path, dest_path)
169
+ copied_files << relative_path
170
+ end
171
+
172
+ if copied_files.any?
173
+ log_success("✓ Copied #{copied_files.size} file(s): #{copied_files.join(', ')}")
174
+ else
175
+ log_info("No non-Ruby files to copy")
176
+ end
177
+ end
178
+
179
+ def pack_wasm(exit_on_error: true, log_prefix: "Packing")
180
+ command = "bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o dist/src.wasm"
181
+ log_info("#{log_prefix}: #{command}")
182
+
183
+ success = run_command(command, exit_on_error: exit_on_error)
184
+ if success
185
+ log_success("✓ Pack completed")
186
+ end
187
+ success
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require "listen"
5
+ require "rack"
6
+ require "thread"
7
+ require "rbconfig"
8
+
9
+ module RubyWasmUi
10
+ module Cli
11
+ class Command
12
+ class Dev < Base
13
+ def self.description
14
+ "Start development server with file watching and auto-build"
15
+ end
16
+
17
+ def run(_argv)
18
+ log_info("Starting development server...")
19
+ puts ""
20
+
21
+ ensure_src_directory
22
+ ensure_ruby_wasm
23
+
24
+ # Initial build
25
+ log_info("Performing initial build...")
26
+ build
27
+ log_success("✓ Initial build completed")
28
+ puts ""
29
+
30
+ # Start file watcher in a separate thread
31
+ @build_lock = Mutex.new
32
+ @build_queue = Queue.new
33
+ @listener = nil
34
+ @watcher_thread = nil
35
+
36
+ @watcher_thread = Thread.new do
37
+ start_file_watcher
38
+ end
39
+
40
+ # Register cleanup hook to ensure cleanup runs on exit
41
+ @cleanup_done = false
42
+ at_exit do
43
+ cleanup unless @cleanup_done
44
+ end
45
+
46
+ # Start development server
47
+ begin
48
+ start_server
49
+ ensure
50
+ cleanup
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def open_browser(url)
57
+ case RbConfig::CONFIG["host_os"]
58
+ when /darwin/
59
+ system("open", url)
60
+ when /linux/
61
+ system("xdg-open", url)
62
+ when /mswin|mingw|cygwin/
63
+ system("start", url)
64
+ else
65
+ log_info("Please open #{url} in your browser")
66
+ end
67
+ rescue => e
68
+ log_info("Could not open browser automatically: #{e.message}")
69
+ log_info("Please open #{url} in your browser")
70
+ end
71
+
72
+ def cleanup
73
+ return if @cleanup_done
74
+ @cleanup_done = true
75
+ @listener&.stop
76
+ @watcher_thread&.kill
77
+ end
78
+
79
+ def build
80
+ ensure_dist_directory
81
+
82
+ success = pack_wasm(exit_on_error: false, log_prefix: "Building")
83
+ if success
84
+ copy_non_ruby_files
85
+ log_success("✓ Build completed")
86
+ else
87
+ log_error("Build failed")
88
+ end
89
+ success
90
+ end
91
+
92
+ def start_file_watcher
93
+ @listener = Listen.to("src") do |modified, added, removed|
94
+ files_changed = (modified + added + removed).reject do |file|
95
+ # Ignore temporary files and common build artifacts
96
+ file.end_with?(".swp", ".tmp", "~", ".wasm")
97
+ end
98
+
99
+ if files_changed.any?
100
+ log_info("Files changed: #{files_changed.join(', ')}")
101
+
102
+ # Debounce: add to queue
103
+ @build_queue << :build
104
+
105
+ # Process queue with debouncing
106
+ Thread.new do
107
+ sleep 0.5 # Debounce delay
108
+ begin
109
+ if @build_queue.pop(true)
110
+ @build_queue.clear
111
+ @build_lock.synchronize do
112
+ build
113
+ end
114
+ end
115
+ rescue ThreadError
116
+ # Queue is empty, ignore
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ @listener.start
123
+ rescue => e
124
+ log_error("File watcher error: #{e.message}")
125
+ end
126
+
127
+ def start_server
128
+ port = 8080
129
+ log_info("Starting development server on http://localhost:#{port}")
130
+ log_info("Press Ctrl+C to stop")
131
+ puts ""
132
+
133
+ # Static file server application
134
+ static_app = lambda do |env|
135
+ path = env["PATH_INFO"]
136
+ # Remove leading slash and handle root path
137
+ relative_path = path == "/" ? "index.html" : path.sub(/^\//, "")
138
+
139
+ # Serve files from dist directory
140
+ file_path = File.join(Dir.pwd, "dist", relative_path)
141
+
142
+ if File.exist?(file_path) && File.file?(file_path)
143
+ content_type = Rack::Mime.mime_type(File.extname(file_path), "text/html")
144
+ [200, { "Content-Type" => content_type }, [File.read(file_path)]]
145
+ else
146
+ [404, { "Content-Type" => "text/plain" }, ["File not found: #{path}"]]
147
+ end
148
+ end
149
+
150
+ # CORS middleware
151
+ cors_middleware = lambda do |env|
152
+ if env["REQUEST_METHOD"] == "OPTIONS"
153
+ [
154
+ 200,
155
+ {
156
+ "Access-Control-Allow-Origin" => "*",
157
+ "Access-Control-Allow-Methods" => "GET, POST, OPTIONS",
158
+ "Access-Control-Allow-Headers" => "Content-Type"
159
+ },
160
+ []
161
+ ]
162
+ else
163
+ status, headers, body = static_app.call(env)
164
+ headers["Access-Control-Allow-Origin"] = "*"
165
+ headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
166
+ headers["Access-Control-Allow-Headers"] = "Content-Type"
167
+ [status, headers, body]
168
+ end
169
+ end
170
+
171
+ # Create Rack application
172
+ app = Rack::Builder.new do
173
+ run cors_middleware
174
+ end
175
+
176
+ # Handle shutdown
177
+ trap("INT") do
178
+ log_info("\nShutting down server...")
179
+ cleanup
180
+ raise SystemExit.new(0)
181
+ end
182
+
183
+ # Use Puma handler (Rack 3.0 compatible)
184
+ begin
185
+ require "rack/handler/puma"
186
+ log_info("Using handler: puma")
187
+
188
+ # Open browser after a short delay to ensure server is ready
189
+ Thread.new do
190
+ sleep 1
191
+ open_browser("http://localhost:#{port}/index.html")
192
+ end
193
+
194
+ Rack::Handler::Puma.run(app, Port: port, Host: "0.0.0.0")
195
+ rescue LoadError => e
196
+ log_error("Puma handler not available: #{e.message}")
197
+ log_error("Please ensure puma gem is installed: gem install puma")
198
+ raise SystemExit.new(1)
199
+ end
200
+ rescue => e
201
+ log_error("Server error: #{e.message}")
202
+ raise SystemExit.new(1)
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module RubyWasmUi
6
+ module Cli
7
+ class Command
8
+ class Pack < Base
9
+ def self.description
10
+ "Pack WASM file by packing Ruby source files"
11
+ end
12
+
13
+ def run(_argv)
14
+ log_info("Packing WASM file...")
15
+ puts ""
16
+
17
+ ensure_src_directory
18
+ ensure_ruby_wasm
19
+ ensure_dist_directory
20
+
21
+ # Pack WASM file
22
+ pack
23
+
24
+ # Copy non-Ruby files from src to dist
25
+ copy_non_ruby_files
26
+ end
27
+
28
+ private
29
+
30
+ def pack
31
+ pack_wasm(exit_on_error: true, log_prefix: 'Packing')
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end