From 95f738593e7752eb63792251596d6ea969edc3a3 Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 16:01:24 +0900 Subject: [PATCH 01/13] refactor: move tests to spec/ruby_wasm_ui/runtime directory --- spec/ruby_wasm_ui/{ => runtime}/component_spec.rb | 0 spec/ruby_wasm_ui/{ => runtime}/dom/scheduler_spec.rb | 0 spec/ruby_wasm_ui/{ => runtime}/nodes_equal_spec.rb | 0 .../{ => runtime}/template/build_conditional_group_spec.rb | 0 spec/ruby_wasm_ui/{ => runtime}/template/build_for_group_spec.rb | 0 spec/ruby_wasm_ui/{ => runtime}/template/build_vdom_spec.rb | 0 spec/ruby_wasm_ui/{ => runtime}/template/parser_spec.rb | 0 spec/ruby_wasm_ui/{ => runtime}/utils/arrays_spec.rb | 0 spec/ruby_wasm_ui/{ => runtime}/utils/objects_spec.rb | 0 spec/ruby_wasm_ui/{ => runtime}/utils/props_spec.rb | 0 spec/ruby_wasm_ui/{ => runtime}/utils/strings_spec.rb | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename spec/ruby_wasm_ui/{ => runtime}/component_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/dom/scheduler_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/nodes_equal_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/template/build_conditional_group_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/template/build_for_group_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/template/build_vdom_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/template/parser_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/utils/arrays_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/utils/objects_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/utils/props_spec.rb (100%) rename spec/ruby_wasm_ui/{ => runtime}/utils/strings_spec.rb (100%) diff --git a/spec/ruby_wasm_ui/component_spec.rb b/spec/ruby_wasm_ui/runtime/component_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/component_spec.rb rename to spec/ruby_wasm_ui/runtime/component_spec.rb diff --git a/spec/ruby_wasm_ui/dom/scheduler_spec.rb b/spec/ruby_wasm_ui/runtime/dom/scheduler_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/dom/scheduler_spec.rb rename to spec/ruby_wasm_ui/runtime/dom/scheduler_spec.rb diff --git a/spec/ruby_wasm_ui/nodes_equal_spec.rb b/spec/ruby_wasm_ui/runtime/nodes_equal_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/nodes_equal_spec.rb rename to spec/ruby_wasm_ui/runtime/nodes_equal_spec.rb diff --git a/spec/ruby_wasm_ui/template/build_conditional_group_spec.rb b/spec/ruby_wasm_ui/runtime/template/build_conditional_group_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/template/build_conditional_group_spec.rb rename to spec/ruby_wasm_ui/runtime/template/build_conditional_group_spec.rb diff --git a/spec/ruby_wasm_ui/template/build_for_group_spec.rb b/spec/ruby_wasm_ui/runtime/template/build_for_group_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/template/build_for_group_spec.rb rename to spec/ruby_wasm_ui/runtime/template/build_for_group_spec.rb diff --git a/spec/ruby_wasm_ui/template/build_vdom_spec.rb b/spec/ruby_wasm_ui/runtime/template/build_vdom_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/template/build_vdom_spec.rb rename to spec/ruby_wasm_ui/runtime/template/build_vdom_spec.rb diff --git a/spec/ruby_wasm_ui/template/parser_spec.rb b/spec/ruby_wasm_ui/runtime/template/parser_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/template/parser_spec.rb rename to spec/ruby_wasm_ui/runtime/template/parser_spec.rb diff --git a/spec/ruby_wasm_ui/utils/arrays_spec.rb b/spec/ruby_wasm_ui/runtime/utils/arrays_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/utils/arrays_spec.rb rename to spec/ruby_wasm_ui/runtime/utils/arrays_spec.rb diff --git a/spec/ruby_wasm_ui/utils/objects_spec.rb b/spec/ruby_wasm_ui/runtime/utils/objects_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/utils/objects_spec.rb rename to spec/ruby_wasm_ui/runtime/utils/objects_spec.rb diff --git a/spec/ruby_wasm_ui/utils/props_spec.rb b/spec/ruby_wasm_ui/runtime/utils/props_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/utils/props_spec.rb rename to spec/ruby_wasm_ui/runtime/utils/props_spec.rb diff --git a/spec/ruby_wasm_ui/utils/strings_spec.rb b/spec/ruby_wasm_ui/runtime/utils/strings_spec.rb similarity index 100% rename from spec/ruby_wasm_ui/utils/strings_spec.rb rename to spec/ruby_wasm_ui/runtime/utils/strings_spec.rb From b4100f22dceb618595db17b54de2423f0ee62c3a Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 16:08:53 +0900 Subject: [PATCH 02/13] feat: Add setup command --- examples/.gitignore | 3 + exe/ruby-wasm-ui | 6 ++ lib/ruby_wasm_ui/cli/command.rb | 48 +++++++++ lib/ruby_wasm_ui/cli/command/base.rb | 103 ++++++++++++++++++++ lib/ruby_wasm_ui/cli/command/setup.rb | 34 +++++++ spec/ruby_wasm_ui/cli/command/base_spec.rb | 94 ++++++++++++++++++ spec/ruby_wasm_ui/cli/command/setup_spec.rb | 55 +++++++++++ spec/ruby_wasm_ui/cli/command_spec.rb | 64 ++++++++++++ 8 files changed, 407 insertions(+) create mode 100644 examples/.gitignore create mode 100755 exe/ruby-wasm-ui create mode 100644 lib/ruby_wasm_ui/cli/command.rb create mode 100644 lib/ruby_wasm_ui/cli/command/base.rb create mode 100644 lib/ruby_wasm_ui/cli/command/setup.rb create mode 100644 spec/ruby_wasm_ui/cli/command/base_spec.rb create mode 100644 spec/ruby_wasm_ui/cli/command/setup_spec.rb create mode 100644 spec/ruby_wasm_ui/cli/command_spec.rb diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..64fd3c0 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,3 @@ +*.wasm +/rubies +/build diff --git a/exe/ruby-wasm-ui b/exe/ruby-wasm-ui new file mode 100755 index 0000000..f4ff316 --- /dev/null +++ b/exe/ruby-wasm-ui @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../lib/ruby_wasm_ui/cli/command" + +RubyWasmUi::Cli::Command.run(ARGV) diff --git a/lib/ruby_wasm_ui/cli/command.rb b/lib/ruby_wasm_ui/cli/command.rb new file mode 100644 index 0000000..3bf0463 --- /dev/null +++ b/lib/ruby_wasm_ui/cli/command.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative "command/setup" +# To add a new command (e.g., dev): +# 1. Create lib/ruby_wasm_ui/cli/command/dev.rb inheriting from Command::Base +# 2. Add require_relative "command/dev" above +# 3. Add "dev" => Command::Dev to COMMANDS hash below + +module RubyWasmUi + module Cli + class Command + COMMANDS = { + "setup" => Command::Setup + # Add new commands here, e.g.: + # "dev" => Command::Dev + }.freeze + + def self.run(argv) + command_name = argv[0] + + if command_name.nil? + show_usage + exit 1 + end + + command_class = COMMANDS[command_name] + if command_class.nil? + puts "Unknown command: #{command_name}" + puts "" + show_usage + exit 1 + end + + command_class.new.run(argv[1..-1]) + end + + def self.show_usage + puts "Usage: ruby-wasm-ui " + puts "" + puts "Commands:" + COMMANDS.each do |name, klass| + description = klass.description || "" + puts " #{name.ljust(12)}#{description}" + end + end + end + end +end diff --git a/lib/ruby_wasm_ui/cli/command/base.rb b/lib/ruby_wasm_ui/cli/command/base.rb new file mode 100644 index 0000000..4d233ff --- /dev/null +++ b/lib/ruby_wasm_ui/cli/command/base.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require "open3" + +module RubyWasmUi + module Cli + class Command + class Base + def self.description + nil + end + + def run(_argv) + raise NotImplementedError, "Subclasses must implement #run" + end + + protected + + def run_command(command) + log_debug("Executing command: #{command}") + + Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| + stdin.close + + # Read stdout and stderr in separate threads for real-time output + stdout_thread = Thread.new do + stdout.each_line do |line| + print line + $stdout.flush + end + end + + stderr_thread = Thread.new do + stderr.each_line do |line| + $stderr.print line + $stderr.flush + end + end + + # Wait for both threads to finish reading + stdout_thread.join + stderr_thread.join + + # Wait for the process to finish + exit_status = wait_thr.value + + unless exit_status.success? + log_error("Command failed: #{command}") + exit exit_status.exitstatus + end + end + end + + def update_gitignore(entries_to_add) + gitignore_path = ".gitignore" + + # Read existing .gitignore or create new content + if File.exist?(gitignore_path) + content = File.read(gitignore_path) + lines = content.lines.map(&:chomp) + else + lines = [] + end + + # Add entries that don't already exist + added_entries = [] + entries_to_add.each do |entry| + unless lines.include?(entry) + lines << entry + added_entries << entry + end + end + + # Write back to .gitignore + File.write(gitignore_path, lines.join("\n") + "\n") + if added_entries.any? + log_info("Added to .gitignore: #{added_entries.join(', ')}") + else + log_info("No new entries added to .gitignore (all entries already exist)") + end + end + + protected + + def log_info(message) + puts "[INFO] #{message}" + end + + def log_success(message) + puts "[SUCCESS] #{message}" + end + + def log_error(message) + puts "[ERROR] #{message}" + end + + def log_debug(message) + puts "[DEBUG] #{message}" + end + end + end + end +end diff --git a/lib/ruby_wasm_ui/cli/command/setup.rb b/lib/ruby_wasm_ui/cli/command/setup.rb new file mode 100644 index 0000000..0ffb738 --- /dev/null +++ b/lib/ruby_wasm_ui/cli/command/setup.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative "base" + +module RubyWasmUi + module Cli + class Command + class Setup < Base + def self.description + "Set up the project for ruby-wasm-ui" + end + + def run(_argv) + log_info("Setting up ruby-wasm-ui project...") + puts "" + + # Build Ruby WASM + log_info("Step 1/2: Building Ruby WASM...") + log_info("Running: bundle exec rbwasm build --ruby-version 3.4 -o ruby.wasm") + run_command("bundle exec rbwasm build --ruby-version 3.4 -o ruby.wasm") + log_success("✓ Ruby WASM build completed") + + # Update .gitignore + puts "" + log_info("Step 2/2: Updating .gitignore...") + update_gitignore(["*.wasm", "/rubies", "/build"]) + + puts "" + log_success("Setup completed successfully!") + end + end + end + end +end diff --git a/spec/ruby_wasm_ui/cli/command/base_spec.rb b/spec/ruby_wasm_ui/cli/command/base_spec.rb new file mode 100644 index 0000000..d4a05fa --- /dev/null +++ b/spec/ruby_wasm_ui/cli/command/base_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'fileutils' +require 'tmpdir' + +RSpec.describe RubyWasmUi::Cli::Command::Base do + let(:base_instance) { described_class.new } + + describe '#run' do + it 'raises NotImplementedError' do + expect { base_instance.run([]) }.to raise_error(NotImplementedError, /Subclasses must implement #run/) + end + end + + describe '#update_gitignore' do + let(:temp_dir) { Dir.mktmpdir } + let(:original_dir) { Dir.pwd } + + around do |example| + Dir.chdir(temp_dir) do + example.run + end + ensure + FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir) + end + + context 'when .gitignore does not exist' do + it 'creates .gitignore with new entries' do + base_instance.send(:update_gitignore, ['*.wasm', '/rubies']) + content = File.read('.gitignore') + expect(content).to include('*.wasm') + expect(content).to include('/rubies') + end + end + + context 'when .gitignore exists' do + before do + File.write('.gitignore', "node_modules/\n*.log\n") + end + + it 'adds new entries to existing .gitignore' do + base_instance.send(:update_gitignore, ['*.wasm', '/rubies']) + content = File.read('.gitignore') + expect(content).to include('node_modules/') + expect(content).to include('*.log') + expect(content).to include('*.wasm') + expect(content).to include('/rubies') + end + + it 'does not add duplicate entries' do + File.write('.gitignore', "*.wasm\n") + base_instance.send(:update_gitignore, ['*.wasm', '/rubies']) + content = File.read('.gitignore') + expect(content.scan(/^\*\.wasm$/).count).to eq(1) + end + + it 'outputs message when entries are added' do + expect { base_instance.send(:update_gitignore, ['*.wasm']) }.to output( + /Added to .gitignore: \*\.wasm/ + ).to_stdout + end + + it 'outputs message when no entries are added' do + File.write('.gitignore', "*.wasm\n") + expect { base_instance.send(:update_gitignore, ['*.wasm']) }.to output( + /No new entries added to .gitignore/ + ).to_stdout + end + end + end + + describe '#run_command' do + context 'when command succeeds' do + it 'executes the command and outputs stdout' do + expect { base_instance.send(:run_command, 'echo "test output"') }.to output( + /test output/ + ).to_stdout + end + + it 'does not raise error' do + expect { base_instance.send(:run_command, 'echo "test"') }.not_to raise_error + end + end + + context 'when command fails' do + it 'outputs error message and exits' do + expect { base_instance.send(:run_command, 'false') }.to output( + /Command failed: false/ + ).to_stdout.and raise_error(SystemExit) + end + end + end +end diff --git a/spec/ruby_wasm_ui/cli/command/setup_spec.rb b/spec/ruby_wasm_ui/cli/command/setup_spec.rb new file mode 100644 index 0000000..d3aa8cf --- /dev/null +++ b/spec/ruby_wasm_ui/cli/command/setup_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RubyWasmUi::Cli::Command::Setup do + let(:setup_instance) { described_class.new } + + describe '.description' do + it 'returns the description' do + expect(described_class.description).to eq('Set up the project for ruby-wasm-ui') + end + end + + describe '#run' do + before do + allow(setup_instance).to receive(:run_command) + allow(setup_instance).to receive(:update_gitignore) + end + + it 'outputs setup message' do + expect { setup_instance.run([]) }.to output( + /Setting up ruby-wasm-ui project/ + ).to_stdout + end + + it 'executes rbwasm build command' do + expect(setup_instance).to receive(:run_command).with( + 'bundle exec rbwasm build --ruby-version 3.4 -o ruby.wasm' + ) + setup_instance.run([]) + end + + it 'updates .gitignore with required entries' do + expect(setup_instance).to receive(:update_gitignore).with( + ['*.wasm', '/rubies', '/build'] + ) + setup_instance.run([]) + end + + it 'outputs completion message' do + expect { setup_instance.run([]) }.to output( + /Setup completed successfully!/ + ).to_stdout + end + + it 'outputs progress messages' do + expect { setup_instance.run([]) }.to output( + /Building Ruby WASM/ + ).to_stdout + expect { setup_instance.run([]) }.to output( + /Updating .gitignore/ + ).to_stdout + end + end +end diff --git a/spec/ruby_wasm_ui/cli/command_spec.rb b/spec/ruby_wasm_ui/cli/command_spec.rb new file mode 100644 index 0000000..8651e55 --- /dev/null +++ b/spec/ruby_wasm_ui/cli/command_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RubyWasmUi::Cli::Command do + describe '.run' do + context 'when command is provided' do + context 'with valid command' do + let(:setup_instance) { instance_double(RubyWasmUi::Cli::Command::Setup) } + + before do + allow(RubyWasmUi::Cli::Command::Setup).to receive(:new).and_return(setup_instance) + allow(setup_instance).to receive(:run) + end + + it 'executes the setup command' do + expect(setup_instance).to receive(:run).with([]) + described_class.run(['setup']) + end + + it 'passes remaining arguments to the command' do + expect(setup_instance).to receive(:run).with(['arg1', 'arg2']) + described_class.run(['setup', 'arg1', 'arg2']) + end + end + + context 'with invalid command' do + it 'prints error message and shows usage' do + expect { described_class.run(['unknown']) }.to output( + /Unknown command: unknown/ + ).to_stdout.and raise_error(SystemExit) + end + end + end + + context 'when no command is provided' do + it 'shows usage and exits' do + expect { described_class.run([]) }.to output( + /Usage: ruby-wasm-ui / + ).to_stdout.and raise_error(SystemExit) + end + end + end + + describe '.show_usage' do + it 'displays usage information' do + expect { described_class.show_usage }.to output( + /Usage: ruby-wasm-ui / + ).to_stdout + end + + it 'lists available commands' do + expect { described_class.show_usage }.to output( + /setup.*Set up the project for ruby-wasm-ui/ + ).to_stdout + end + end + + describe 'COMMANDS' do + it 'contains setup command' do + expect(described_class::COMMANDS).to include('setup' => RubyWasmUi::Cli::Command::Setup) + end + end +end From 6336c337729b70e8ab4a3f2dc1c50d210f6c066b Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 18:40:25 +0900 Subject: [PATCH 03/13] feat: Add dev command --- Gemfile.lock | 20 ++ examples/Gemfile.lock | 30 +- lib/ruby_wasm_ui/cli/command.rb | 10 +- lib/ruby_wasm_ui/cli/command/dev.rb | 205 ++++++++++++++ ruby_wasm_ui.gemspec | 3 + spec/ruby_wasm_ui/cli/command/dev_spec.rb | 325 ++++++++++++++++++++++ spec/ruby_wasm_ui/cli/command_spec.rb | 50 +++- 7 files changed, 609 insertions(+), 34 deletions(-) create mode 100644 lib/ruby_wasm_ui/cli/command/dev.rb create mode 100644 spec/ruby_wasm_ui/cli/command/dev_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 9d1fe2f..cdf5b22 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,9 @@ PATH specs: ruby_wasm_ui (0.8.3) js (~> 2.7) + listen (~> 3.8) + puma (~> 6.0) + rack (~> 3.0) ruby_wasm (~> 2.7) GEM @@ -11,20 +14,37 @@ GEM date (3.5.0) diff-lcs (1.6.2) erb (6.0.0) + ffi (1.17.2) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) io-console (0.8.1) irb (1.15.3) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) js (2.7.2) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) + nio4r (2.7.5) pp (0.6.3) prettyprint prettyprint (0.2.0) psych (5.2.6) date stringio + puma (6.6.1) + nio4r (~> 2.0) + rack (3.2.4) rake (13.3.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) rdoc (6.15.1) erb psych (>= 4.0.0) diff --git a/examples/Gemfile.lock b/examples/Gemfile.lock index 96a3b2b..c3cac2c 100644 --- a/examples/Gemfile.lock +++ b/examples/Gemfile.lock @@ -3,36 +3,34 @@ PATH specs: ruby_wasm_ui (0.8.3) js (~> 2.7) + listen (~> 3.8) + puma (~> 6.0) + rack (~> 3.0) ruby_wasm (~> 2.7) GEM remote: https://rubygems.org/ specs: + ffi (1.17.2-arm64-darwin) js (2.7.2) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) + nio4r (2.7.5) + puma (6.6.1) + nio4r (~> 2.0) + rack (3.2.4) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) ruby_wasm (2.7.2) logger - ruby_wasm (2.7.2-aarch64-linux) - logger - ruby_wasm (2.7.2-aarch64-linux-musl) - logger ruby_wasm (2.7.2-arm64-darwin) logger - ruby_wasm (2.7.2-x86_64-darwin) - logger - ruby_wasm (2.7.2-x86_64-linux) - logger - ruby_wasm (2.7.2-x86_64-linux-musl) - logger PLATFORMS - aarch64-linux - aarch64-linux-musl arm64-darwin - ruby - x86_64-darwin - x86_64-linux - x86_64-linux-musl DEPENDENCIES ruby_wasm_ui! diff --git a/lib/ruby_wasm_ui/cli/command.rb b/lib/ruby_wasm_ui/cli/command.rb index 3bf0463..dc0d1ba 100644 --- a/lib/ruby_wasm_ui/cli/command.rb +++ b/lib/ruby_wasm_ui/cli/command.rb @@ -1,18 +1,14 @@ # frozen_string_literal: true require_relative "command/setup" -# To add a new command (e.g., dev): -# 1. Create lib/ruby_wasm_ui/cli/command/dev.rb inheriting from Command::Base -# 2. Add require_relative "command/dev" above -# 3. Add "dev" => Command::Dev to COMMANDS hash below +require_relative "command/dev" module RubyWasmUi module Cli class Command COMMANDS = { - "setup" => Command::Setup - # Add new commands here, e.g.: - # "dev" => Command::Dev + "setup" => Command::Setup, + "dev" => Command::Dev }.freeze def self.run(argv) diff --git a/lib/ruby_wasm_ui/cli/command/dev.rb b/lib/ruby_wasm_ui/cli/command/dev.rb new file mode 100644 index 0000000..032df2b --- /dev/null +++ b/lib/ruby_wasm_ui/cli/command/dev.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require_relative "base" +require "listen" +require "rack" +require "thread" + +module RubyWasmUi + module Cli + class Command + class Dev < Base + def self.description + "Start development server with file watching and auto-build" + end + + def run(_argv) + log_info("Starting development server...") + puts "" + + # Check if src directory exists + unless Dir.exist?("src") + log_error("src directory not found. Please run 'ruby-wasm-ui setup' first.") + exit 1 + end + + # Initial build + log_info("Performing initial build...") + build + log_success("✓ Initial build completed") + puts "" + + # Start file watcher in a separate thread + @build_lock = Mutex.new + @build_queue = Queue.new + @listener = nil + @watcher_thread = nil + + @watcher_thread = Thread.new do + start_file_watcher + end + + # Register cleanup hook to ensure cleanup runs on exit + @cleanup_done = false + at_exit do + cleanup unless @cleanup_done + end + + # Start development server + begin + start_server + ensure + cleanup + end + end + + private + + def cleanup + return if @cleanup_done + @cleanup_done = true + @listener&.stop + @watcher_thread&.kill + end + + def build + command = "bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm" + log_info("Building: #{command}") + + Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| + stdin.close + + stdout_thread = Thread.new do + stdout.each_line do |line| + print line + $stdout.flush + end + end + + stderr_thread = Thread.new do + stderr.each_line do |line| + $stderr.print line + $stderr.flush + end + end + + stdout_thread.join + stderr_thread.join + + exit_status = wait_thr.value + + unless exit_status.success? + log_error("Build failed") + return false + end + + log_success("✓ Build completed") + true + end + end + + def start_file_watcher + @listener = Listen.to("src") do |modified, added, removed| + files_changed = (modified + added + removed).reject do |file| + # Ignore temporary files and common build artifacts + file.end_with?(".swp", ".tmp", "~", ".wasm") + end + + if files_changed.any? + log_info("Files changed: #{files_changed.join(', ')}") + + # Debounce: add to queue + @build_queue << :build + + # Process queue with debouncing + Thread.new do + sleep 0.5 # Debounce delay + begin + if @build_queue.pop(true) + @build_queue.clear + @build_lock.synchronize do + build + end + end + rescue ThreadError + # Queue is empty, ignore + end + end + end + end + + @listener.start + rescue => e + log_error("File watcher error: #{e.message}") + end + + def start_server + port = 8080 + log_info("Starting development server on http://localhost:#{port}") + log_info("Press Ctrl+C to stop") + puts "" + + # Static file server application + static_app = lambda do |env| + path = env["PATH_INFO"] + file_path = File.join(Dir.pwd, path == "/" ? "index.html" : path) + + if File.exist?(file_path) && File.file?(file_path) + content_type = Rack::Mime.mime_type(File.extname(file_path), "text/html") + [200, { "Content-Type" => content_type }, [File.read(file_path)]] + else + [404, { "Content-Type" => "text/plain" }, ["File not found: #{path}"]] + end + end + + # CORS middleware + cors_middleware = lambda do |env| + if env["REQUEST_METHOD"] == "OPTIONS" + [ + 200, + { + "Access-Control-Allow-Origin" => "*", + "Access-Control-Allow-Methods" => "GET, POST, OPTIONS", + "Access-Control-Allow-Headers" => "Content-Type" + }, + [] + ] + else + status, headers, body = static_app.call(env) + headers["Access-Control-Allow-Origin"] = "*" + headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" + headers["Access-Control-Allow-Headers"] = "Content-Type" + [status, headers, body] + end + end + + # Create Rack application + app = Rack::Builder.new do + run cors_middleware + end + + # Handle shutdown + trap("INT") do + log_info("\nShutting down server...") + cleanup + exit 0 + end + + # Use Puma handler (Rack 3.0 compatible) + begin + require "rack/handler/puma" + log_info("Using handler: puma") + Rack::Handler::Puma.run(app, Port: port, Host: "0.0.0.0") + rescue LoadError => e + log_error("Puma handler not available: #{e.message}") + log_error("Please ensure puma gem is installed: gem install puma") + exit 1 + end + rescue => e + log_error("Server error: #{e.message}") + exit 1 + end + end + end + end +end diff --git a/ruby_wasm_ui.gemspec b/ruby_wasm_ui.gemspec index b7982c8..b1f7904 100644 --- a/ruby_wasm_ui.gemspec +++ b/ruby_wasm_ui.gemspec @@ -34,6 +34,9 @@ Gem::Specification.new do |spec| # Uncomment to register a new dependency of your gem spec.add_dependency "ruby_wasm", "~> 2.7" spec.add_dependency "js", "~> 2.7" + spec.add_dependency "listen", "~> 3.8" + spec.add_dependency "rack", "~> 3.0" + spec.add_dependency "puma", "~> 6.0" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/spec/ruby_wasm_ui/cli/command/dev_spec.rb b/spec/ruby_wasm_ui/cli/command/dev_spec.rb new file mode 100644 index 0000000..6fc5709 --- /dev/null +++ b/spec/ruby_wasm_ui/cli/command/dev_spec.rb @@ -0,0 +1,325 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'fileutils' +require 'tmpdir' + +RSpec.describe RubyWasmUi::Cli::Command::Dev do + let(:dev_instance) { described_class.new } + let(:temp_dir) { Dir.mktmpdir } + let(:original_dir) { Dir.pwd } + + around do |example| + Dir.chdir(temp_dir) do + example.run + end + ensure + Dir.chdir(original_dir) + FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir) + end + + describe '.description' do + it 'returns the description' do + expect(described_class.description).to eq('Start development server with file watching and auto-build') + end + end + + describe '#run' do + let(:watcher_thread) { instance_double(Thread, kill: nil) } + + before do + allow(dev_instance).to receive(:build).and_return(true) + allow(dev_instance).to receive(:start_file_watcher) + allow(dev_instance).to receive(:start_server) + allow(Thread).to receive(:new).and_return(watcher_thread) + end + + context 'when src directory does not exist' do + it 'outputs error message and exits' do + expect { dev_instance.run([]) }.to output( + /src directory not found. Please run 'ruby-wasm-ui setup' first./ + ).to_stdout.and raise_error(SystemExit) + end + + it 'exits with status 1' do + expect { dev_instance.run([]) }.to raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end + end + end + + context 'when src directory exists' do + before do + FileUtils.mkdir_p('src') + end + + it 'outputs starting message' do + expect { dev_instance.run([]) }.to output( + /Starting development server/ + ).to_stdout + end + + it 'performs initial build' do + expect(dev_instance).to receive(:build).and_return(true) + dev_instance.run([]) + end + + it 'outputs initial build completion message' do + expect { dev_instance.run([]) }.to output( + /Initial build completed/ + ).to_stdout + end + + it 'starts file watcher' do + file_watcher_called = false + allow(dev_instance).to receive(:start_file_watcher) { file_watcher_called = true } + allow(dev_instance).to receive(:start_server) do + # Simulate blocking server + sleep 0.01 + end + # Allow Thread.new to actually create threads so start_file_watcher gets called + allow(Thread).to receive(:new).and_call_original + Thread.new { sleep 0.02; Process.kill('INT', Process.pid) } + begin + dev_instance.run([]) + rescue SystemExit + # Expected when server is killed + end + expect(file_watcher_called).to be true + end + + it 'starts development server' do + allow(dev_instance).to receive(:start_file_watcher).and_return(nil) + expect(dev_instance).to receive(:start_server) + dev_instance.run([]) + end + + context 'when build fails' do + before do + allow(dev_instance).to receive(:build).and_return(false) + end + + it 'still starts the server' do + expect(dev_instance).to receive(:start_server) + dev_instance.run([]) + end + end + end + end + + describe '#build' do + before do + FileUtils.mkdir_p('src') + end + + context 'when command succeeds' do + let(:stdin) { double(close: nil) } + let(:stdout) { double(each_line: ['output'].to_enum) } + let(:stderr) { double(each_line: [].to_enum) } + let(:wait_thr) { double(value: double(success?: true)) } + let(:stdout_thread) { instance_double(Thread, join: nil) } + let(:stderr_thread) { instance_double(Thread, join: nil) } + + before do + allow(Open3).to receive(:popen3).and_yield(stdin, stdout, stderr, wait_thr) + allow(Thread).to receive(:new).and_return(stdout_thread, stderr_thread) + end + + it 'executes rbwasm pack command' do + expect(Open3).to receive(:popen3).with( + 'bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm' + ) + dev_instance.send(:build) + end + + it 'outputs build message' do + expect { dev_instance.send(:build) }.to output( + /Building: bundle exec rbwasm pack/ + ).to_stdout + end + + it 'outputs success message' do + expect { dev_instance.send(:build) }.to output( + /Build completed/ + ).to_stdout + end + + it 'returns true' do + result = dev_instance.send(:build) + expect(result).to be true + end + end + + context 'when command fails' do + let(:stdin) { double(close: nil) } + let(:stdout) { double(each_line: [].to_enum) } + let(:stderr) { double(each_line: ['error'].to_enum) } + let(:wait_thr) { double(value: double(success?: false, exitstatus: 1)) } + let(:stdout_thread) { instance_double(Thread, join: nil) } + let(:stderr_thread) { instance_double(Thread, join: nil) } + + before do + allow(Open3).to receive(:popen3).and_yield(stdin, stdout, stderr, wait_thr) + allow(Thread).to receive(:new).and_return(stdout_thread, stderr_thread) + end + + it 'outputs error message' do + expect { dev_instance.send(:build) }.to output( + /Build failed/ + ).to_stdout + end + + it 'returns false' do + result = dev_instance.send(:build) + expect(result).to be false + end + end + end + + describe '#start_file_watcher' do + before do + FileUtils.mkdir_p('src') + @listener = instance_double(Listen::Listener, start: nil, stop: nil) + allow(Listen).to receive(:to).and_return(@listener) + end + + it 'starts listening to src directory' do + expect(Listen).to receive(:to).with('src') + dev_instance.send(:start_file_watcher) + end + + it 'starts the listener' do + expect(@listener).to receive(:start) + dev_instance.send(:start_file_watcher) + end + + context 'when file changes occur' do + let(:callback_container) { {} } + + before do + allow(Listen).to receive(:to) do |_dir, &block| + callback_container[:callback] = block + @listener + end + dev_instance.instance_variable_set(:@build_lock, Mutex.new) + dev_instance.instance_variable_set(:@build_queue, Queue.new) + allow(dev_instance).to receive(:build) + allow(dev_instance).to receive(:log_info) + end + + it 'triggers rebuild on file change' do + dev_instance.send(:start_file_watcher) + callback = callback_container[:callback] + expect(callback).not_to be_nil + + # Allow Thread.new to create actual threads for the debounce logic + allow(Thread).to receive(:new).and_call_original + + callback.call(['src/app.rb'], [], []) + sleep 0.6 # Wait for debounce + expect(dev_instance).to have_received(:build) + end + + it 'ignores temporary files' do + dev_instance.send(:start_file_watcher) + callback = callback_container[:callback] + expect(callback).not_to be_nil + + # Allow Thread.new to create actual threads for the debounce logic + allow(Thread).to receive(:new).and_call_original + + callback.call(['src/app.rb.swp'], [], []) + sleep 0.6 + expect(dev_instance).not_to have_received(:build) + end + end + + context 'when listener raises an error' do + before do + allow(Listen).to receive(:to).and_raise(StandardError.new('Listener error')) + end + + it 'outputs error message' do + expect { dev_instance.send(:start_file_watcher) }.to output( + /File watcher error: Listener error/ + ).to_stdout + end + end + end + + describe '#start_server' do + let(:puma_handler_class) do + Class.new do + def self.run(app, options = {}) + # Blocking call - will be mocked + end + end + end + + before do + FileUtils.mkdir_p('src') + stub_const('Rack::Handler::Puma', puma_handler_class) + allow(Kernel).to receive(:require).with('rack/handler/puma') + end + + it 'outputs server start message' do + allow(Rack::Handler::Puma).to receive(:run) + expect { dev_instance.send(:start_server) }.to output( + /Starting development server on http:\/\/localhost:8080/ + ).to_stdout + end + + it 'outputs instructions message' do + allow(Rack::Handler::Puma).to receive(:run) + expect { dev_instance.send(:start_server) }.to output( + /Press Ctrl+C to stop/ + ).to_stdout + end + + it 'starts Puma server' do + expect(Rack::Handler::Puma).to receive(:run).with( + anything, + Port: 8080, + Host: '0.0.0.0' + ) + dev_instance.send(:start_server) + end + + it 'outputs handler info message' do + allow(Rack::Handler::Puma).to receive(:run) + expect { dev_instance.send(:start_server) }.to output( + /Using handler: puma/ + ).to_stdout + end + + context 'when Puma handler is not available' do + before do + allow(Kernel).to receive(:require).with('rack/handler/puma').and_raise(LoadError.new('cannot load such file')) + end + + it 'outputs error message' do + expect { dev_instance.send(:start_server) }.to output( + /Puma handler not available/ + ).to_stdout.and raise_error(SystemExit) + end + + it 'exits with status 1' do + expect { dev_instance.send(:start_server) }.to raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end + end + end + + context 'when server raises an error' do + before do + allow(Rack::Handler::Puma).to receive(:run).and_raise(StandardError.new('Server error')) + end + + it 'outputs error message and exits' do + expect { dev_instance.send(:start_server) }.to output( + /Server error: Server error/ + ).to_stdout.and raise_error(SystemExit) + end + end + end +end diff --git a/spec/ruby_wasm_ui/cli/command_spec.rb b/spec/ruby_wasm_ui/cli/command_spec.rb index 8651e55..a09d8d0 100644 --- a/spec/ruby_wasm_ui/cli/command_spec.rb +++ b/spec/ruby_wasm_ui/cli/command_spec.rb @@ -6,21 +6,42 @@ describe '.run' do context 'when command is provided' do context 'with valid command' do - let(:setup_instance) { instance_double(RubyWasmUi::Cli::Command::Setup) } + context 'setup command' do + let(:setup_instance) { instance_double(RubyWasmUi::Cli::Command::Setup) } - before do - allow(RubyWasmUi::Cli::Command::Setup).to receive(:new).and_return(setup_instance) - allow(setup_instance).to receive(:run) - end + before do + allow(RubyWasmUi::Cli::Command::Setup).to receive(:new).and_return(setup_instance) + allow(setup_instance).to receive(:run) + end + + it 'executes the setup command' do + expect(setup_instance).to receive(:run).with([]) + described_class.run(['setup']) + end - it 'executes the setup command' do - expect(setup_instance).to receive(:run).with([]) - described_class.run(['setup']) + it 'passes remaining arguments to the command' do + expect(setup_instance).to receive(:run).with(['arg1', 'arg2']) + described_class.run(['setup', 'arg1', 'arg2']) + end end - it 'passes remaining arguments to the command' do - expect(setup_instance).to receive(:run).with(['arg1', 'arg2']) - described_class.run(['setup', 'arg1', 'arg2']) + context 'dev command' do + let(:dev_instance) { instance_double(RubyWasmUi::Cli::Command::Dev) } + + before do + allow(RubyWasmUi::Cli::Command::Dev).to receive(:new).and_return(dev_instance) + allow(dev_instance).to receive(:run) + end + + it 'executes the dev command' do + expect(dev_instance).to receive(:run).with([]) + described_class.run(['dev']) + end + + it 'passes remaining arguments to the command' do + expect(dev_instance).to receive(:run).with(['arg1', 'arg2']) + described_class.run(['dev', 'arg1', 'arg2']) + end end end @@ -53,6 +74,9 @@ expect { described_class.show_usage }.to output( /setup.*Set up the project for ruby-wasm-ui/ ).to_stdout + expect { described_class.show_usage }.to output( + /dev.*Start development server with file watching and auto-build/ + ).to_stdout end end @@ -60,5 +84,9 @@ it 'contains setup command' do expect(described_class::COMMANDS).to include('setup' => RubyWasmUi::Cli::Command::Setup) end + + it 'contains dev command' do + expect(described_class::COMMANDS).to include('dev' => RubyWasmUi::Cli::Command::Dev) + end end end From 82f18877dac6d63647809b2ed5fc5e9738aecfea Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 18:56:50 +0900 Subject: [PATCH 04/13] update: Set ruby version for user local --- lib/ruby_wasm_ui/cli/command/setup.rb | 15 +++++- spec/ruby_wasm_ui/cli/command/setup_spec.rb | 55 ++++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/lib/ruby_wasm_ui/cli/command/setup.rb b/lib/ruby_wasm_ui/cli/command/setup.rb index 0ffb738..3eae030 100644 --- a/lib/ruby_wasm_ui/cli/command/setup.rb +++ b/lib/ruby_wasm_ui/cli/command/setup.rb @@ -14,10 +14,21 @@ def run(_argv) log_info("Setting up ruby-wasm-ui project...") puts "" + # Check Ruby version + ruby_version = RUBY_VERSION.split('.').map(&:to_i) + major, minor = ruby_version[0], ruby_version[1] + + if major < 3 || (major == 3 && minor < 2) + log_error("Ruby WASM requires Ruby 3.2 or higher. Current version: #{RUBY_VERSION}") + exit 1 + end + + ruby_version_str = "#{major}.#{minor}" + # Build Ruby WASM log_info("Step 1/2: Building Ruby WASM...") - log_info("Running: bundle exec rbwasm build --ruby-version 3.4 -o ruby.wasm") - run_command("bundle exec rbwasm build --ruby-version 3.4 -o ruby.wasm") + log_info("Running: bundle exec rbwasm build --ruby-version #{ruby_version_str} -o ruby.wasm") + run_command("bundle exec rbwasm build --ruby-version #{ruby_version_str} -o ruby.wasm") log_success("✓ Ruby WASM build completed") # Update .gitignore diff --git a/spec/ruby_wasm_ui/cli/command/setup_spec.rb b/spec/ruby_wasm_ui/cli/command/setup_spec.rb index d3aa8cf..ac409b7 100644 --- a/spec/ruby_wasm_ui/cli/command/setup_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/setup_spec.rb @@ -24,8 +24,9 @@ end it 'executes rbwasm build command' do + ruby_version = RUBY_VERSION.split('.')[0..1].join('.') expect(setup_instance).to receive(:run_command).with( - 'bundle exec rbwasm build --ruby-version 3.4 -o ruby.wasm' + "bundle exec rbwasm build --ruby-version #{ruby_version} -o ruby.wasm" ) setup_instance.run([]) end @@ -51,5 +52,57 @@ /Updating .gitignore/ ).to_stdout end + + context 'when Ruby version is less than 3.2' do + before do + allow(Kernel).to receive(:exit) + end + + context 'when Ruby version is 3.1' do + before do + stub_const('RUBY_VERSION', '3.1.5') + end + + it 'outputs error message and exits' do + expect { setup_instance.run([]) }.to output( + /\[ERROR\] Ruby WASM requires Ruby 3.2 or higher. Current version: 3.1.5/ + ).to_stdout + expect(Kernel).to have_received(:exit).with(1) + end + + it 'does not execute rbwasm build command' do + expect(setup_instance).not_to receive(:run_command) + setup_instance.run([]) + end + end + + context 'when Ruby version is 2.7' do + before do + stub_const('RUBY_VERSION', '2.7.8') + end + + it 'outputs error message and exits' do + expect { setup_instance.run([]) }.to output( + /\[ERROR\] Ruby WASM requires Ruby 3.2 or higher. Current version: 2.7.8/ + ).to_stdout + expect(Kernel).to have_received(:exit).with(1) + end + end + end + + context 'when Ruby version is 3.2' do + before do + stub_const('RUBY_VERSION', '3.2.0') + allow(setup_instance).to receive(:run_command) + allow(setup_instance).to receive(:update_gitignore) + end + + it 'executes rbwasm build command with correct version' do + expect(setup_instance).to receive(:run_command).with( + 'bundle exec rbwasm build --ruby-version 3.2 -o ruby.wasm' + ) + setup_instance.run([]) + end + end end end From 125d4c069fc9ce48a5b509be0debbc520d3e5db9 Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 19:12:58 +0900 Subject: [PATCH 05/13] feat: Exclude unnecessary gems for build --- lib/ruby_wasm_ui/cli/command/setup.rb | 55 +++++++++++- spec/ruby_wasm_ui/cli/command/setup_spec.rb | 97 +++++++++++++++++++-- 2 files changed, 139 insertions(+), 13 deletions(-) diff --git a/lib/ruby_wasm_ui/cli/command/setup.rb b/lib/ruby_wasm_ui/cli/command/setup.rb index 3eae030..435097f 100644 --- a/lib/ruby_wasm_ui/cli/command/setup.rb +++ b/lib/ruby_wasm_ui/cli/command/setup.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require "bundler/setup" +require "ruby_wasm" +require "ruby_wasm/cli" require_relative "base" module RubyWasmUi @@ -25,20 +28,64 @@ def run(_argv) ruby_version_str = "#{major}.#{minor}" + # Configure excluded gems for WASM build + log_info("Step 1/3: Configuring excluded gems...") + configure_excluded_gems + log_success("✓ Excluded gems configured") + # Build Ruby WASM - log_info("Step 1/2: Building Ruby WASM...") - log_info("Running: bundle exec rbwasm build --ruby-version #{ruby_version_str} -o ruby.wasm") - run_command("bundle exec rbwasm build --ruby-version #{ruby_version_str} -o ruby.wasm") + puts "" + log_info("Step 2/3: Building Ruby WASM...") + log_info("Running: rbwasm build --ruby-version #{ruby_version_str} -o ruby.wasm") + build_ruby_wasm(ruby_version_str) log_success("✓ Ruby WASM build completed") # Update .gitignore puts "" - log_info("Step 2/2: Updating .gitignore...") + log_info("Step 3/3: Updating .gitignore...") update_gitignore(["*.wasm", "/rubies", "/build"]) puts "" log_success("Setup completed successfully!") end + + private + + def configure_excluded_gems + # Get all gems from Bundler definition including all dependencies + definition = Bundler.definition + resolved = definition.resolve + + # Get all resolved specs (including transitive dependencies) + all_specs = resolved.materialize(definition.requested_dependencies) + gem_names = all_specs.map(&:name).uniq + + # Always exclude gems that cause WASM build errors + # These gems have native extensions that don't work in WASM environment + always_excluded = %w[nio4r puma rack listen ffi] + + # Exclude gems that cause build errors or are unnecessary for WASM + # Keep essential gems like 'js' for WASM + excluded_gems = gem_names.select do |gem_name| + # Exclude gems that cause WASM build errors + always_excluded.include?(gem_name) || + # Exclude development/test gems + gem_name.start_with?("rspec", "rubocop", "rake") + end + + # Always add always_excluded gems to ensure they're excluded even if not in dependencies + excluded_gems.concat(always_excluded) + excluded_gems.uniq! + + # Add to EXCLUDED_GEMS + RubyWasm::Packager::EXCLUDED_GEMS.concat(excluded_gems) + end + + def build_ruby_wasm(ruby_version_str) + command = ["build", "--ruby-version", ruby_version_str, "-o", "ruby.wasm"] + cli = RubyWasm::CLI.new(stdout: $stdout, stderr: $stderr) + cli.run(command) + end end end end diff --git a/spec/ruby_wasm_ui/cli/command/setup_spec.rb b/spec/ruby_wasm_ui/cli/command/setup_spec.rb index ac409b7..635792f 100644 --- a/spec/ruby_wasm_ui/cli/command/setup_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/setup_spec.rb @@ -13,7 +13,8 @@ describe '#run' do before do - allow(setup_instance).to receive(:run_command) + allow(setup_instance).to receive(:configure_excluded_gems) + allow(setup_instance).to receive(:build_ruby_wasm) allow(setup_instance).to receive(:update_gitignore) end @@ -23,11 +24,14 @@ ).to_stdout end + it 'configures excluded gems' do + expect(setup_instance).to receive(:configure_excluded_gems) + setup_instance.run([]) + end + it 'executes rbwasm build command' do ruby_version = RUBY_VERSION.split('.')[0..1].join('.') - expect(setup_instance).to receive(:run_command).with( - "bundle exec rbwasm build --ruby-version #{ruby_version} -o ruby.wasm" - ) + expect(setup_instance).to receive(:build_ruby_wasm).with(ruby_version) setup_instance.run([]) end @@ -45,6 +49,9 @@ end it 'outputs progress messages' do + expect { setup_instance.run([]) }.to output( + /Configuring excluded gems/ + ).to_stdout expect { setup_instance.run([]) }.to output( /Building Ruby WASM/ ).to_stdout @@ -71,7 +78,8 @@ end it 'does not execute rbwasm build command' do - expect(setup_instance).not_to receive(:run_command) + expect(setup_instance).not_to receive(:build_ruby_wasm) + expect(setup_instance).not_to receive(:configure_excluded_gems) setup_instance.run([]) end end @@ -93,16 +101,87 @@ context 'when Ruby version is 3.2' do before do stub_const('RUBY_VERSION', '3.2.0') - allow(setup_instance).to receive(:run_command) + allow(setup_instance).to receive(:configure_excluded_gems) + allow(setup_instance).to receive(:build_ruby_wasm) allow(setup_instance).to receive(:update_gitignore) end it 'executes rbwasm build command with correct version' do - expect(setup_instance).to receive(:run_command).with( - 'bundle exec rbwasm build --ruby-version 3.2 -o ruby.wasm' - ) + expect(setup_instance).to receive(:build_ruby_wasm).with('3.2') setup_instance.run([]) end end end + + describe '#configure_excluded_gems' do + before do + # Clear EXCLUDED_GEMS before each test + RubyWasm::Packager::EXCLUDED_GEMS.clear if defined?(RubyWasm::Packager::EXCLUDED_GEMS) + end + + it 'excludes rack, puma, nio4r, listen, and ffi gems' do + allow(Bundler).to receive(:definition).and_return( + double( + resolve: double( + materialize: [ + double(name: 'rack'), + double(name: 'puma'), + double(name: 'nio4r'), + double(name: 'listen'), + double(name: 'ffi'), + double(name: 'js'), + double(name: 'ruby_wasm') + ] + ), + requested_dependencies: [] + ) + ) + + setup_instance.send(:configure_excluded_gems) + + expect(RubyWasm::Packager::EXCLUDED_GEMS).to include('rack', 'puma', 'nio4r', 'listen', 'ffi') + expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js', 'ruby_wasm') + end + + it 'always excludes nio4r, puma, rack, listen, and ffi even if not in dependencies' do + allow(Bundler).to receive(:definition).and_return( + double( + resolve: double( + materialize: [ + double(name: 'js'), + double(name: 'ruby_wasm') + ] + ), + requested_dependencies: [] + ) + ) + + setup_instance.send(:configure_excluded_gems) + + # Always excluded gems should be in the list even if not in dependencies + expect(RubyWasm::Packager::EXCLUDED_GEMS).to include('nio4r', 'puma', 'rack', 'listen', 'ffi') + expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js', 'ruby_wasm') + end + + it 'excludes development/test gems' do + allow(Bundler).to receive(:definition).and_return( + double( + resolve: double( + materialize: [ + double(name: 'rspec'), + double(name: 'rubocop'), + double(name: 'rake'), + double(name: 'js') + ] + ), + requested_dependencies: [] + ) + ) + + setup_instance.send(:configure_excluded_gems) + + expect(RubyWasm::Packager::EXCLUDED_GEMS).to include('rspec', 'rubocop', 'rake') + expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js') + end + end end From c63c6e330c46087a2e14fd15d53ba7158b099126 Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 20:04:56 +0900 Subject: [PATCH 06/13] feat: Create initial file with setup command --- examples/src/index.html | 21 +++++ examples/src/index.rb | 26 ++++++ lib/ruby_wasm_ui/cli/command/setup.rb | 87 ++++++++++++++++++++- spec/ruby_wasm_ui/cli/command/setup_spec.rb | 76 ++++++++++++++++++ 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 examples/src/index.html create mode 100644 examples/src/index.rb diff --git a/examples/src/index.html b/examples/src/index.html new file mode 100644 index 0000000..d671269 --- /dev/null +++ b/examples/src/index.html @@ -0,0 +1,21 @@ + + + + + My App + + + +

My App

+
+ + diff --git a/examples/src/index.rb b/examples/src/index.rb new file mode 100644 index 0000000..4e540bb --- /dev/null +++ b/examples/src/index.rb @@ -0,0 +1,26 @@ +# Simple Hello World component +HelloComponent = RubyWasmUi.define_component( + state: ->(props) { + { message: props[:message] || "Hello, Ruby WASM UI!" } + }, + template: ->() { + RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding) +
+

{state[:message]}

+ +
+ HTML + }, + methods: { + update_message: ->() { + update_state(message: "You clicked the button!") + } + } +) + +# Create and mount the app +app = RubyWasmUi::App.create(HelloComponent, message: "Hello, Ruby WASM UI!") +app_element = JS.global[:document].getElementById("app") +app.mount(app_element) diff --git a/lib/ruby_wasm_ui/cli/command/setup.rb b/lib/ruby_wasm_ui/cli/command/setup.rb index 435097f..49c76b8 100644 --- a/lib/ruby_wasm_ui/cli/command/setup.rb +++ b/lib/ruby_wasm_ui/cli/command/setup.rb @@ -42,8 +42,14 @@ def run(_argv) # Update .gitignore puts "" - log_info("Step 3/3: Updating .gitignore...") + log_info("Step 3/4: Updating .gitignore...") update_gitignore(["*.wasm", "/rubies", "/build"]) + log_success("✓ .gitignore updated") + + # Create initial files + puts "" + log_info("Step 4/4: Creating initial files...") + create_initial_files puts "" log_success("Setup completed successfully!") @@ -86,6 +92,85 @@ def build_ruby_wasm(ruby_version_str) cli = RubyWasm::CLI.new(stdout: $stdout, stderr: $stderr) cli.run(command) end + + def create_initial_files + # Skip if src directory exists + if Dir.exist?("src") + log_info("src directory already exists, skipping initial file creation") + return + end + + # Skip if files already exist + if File.exist?("src/index.html") + log_info("src/index.html already exists, skipping initial file creation") + return + end + + if File.exist?("src/index.rb") + log_info("src/index.rb already exists, skipping initial file creation") + return + end + + # Create src directory + Dir.mkdir("src") + + # Create index.html + File.write("src/index.html", <<~HTML) + + + + + My App + + + +

My App

+
+ + + HTML + + # Create index.rb + File.write("src/index.rb", <<~RUBY) + # Simple Hello World component + HelloComponent = RubyWasmUi.define_component( + state: ->(props) { + { message: props[:message] || "Hello, Ruby WASM UI!" } + }, + template: ->() { + RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding) +
+

{state[:message]}

+ +
+ HTML + }, + methods: { + update_message: ->() { + update_state(message: "You clicked the button!") + } + } + ) + + # Create and mount the app + app = RubyWasmUi::App.create(HelloComponent, message: "Hello, Ruby WASM UI!") + app_element = JS.global[:document].getElementById("app") + app.mount(app_element) + RUBY + + log_success("✓ Initial files created: src/index.html, src/index.rb") + end end end end diff --git a/spec/ruby_wasm_ui/cli/command/setup_spec.rb b/spec/ruby_wasm_ui/cli/command/setup_spec.rb index 635792f..75bdfe3 100644 --- a/spec/ruby_wasm_ui/cli/command/setup_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/setup_spec.rb @@ -16,6 +16,7 @@ allow(setup_instance).to receive(:configure_excluded_gems) allow(setup_instance).to receive(:build_ruby_wasm) allow(setup_instance).to receive(:update_gitignore) + allow(setup_instance).to receive(:create_initial_files) end it 'outputs setup message' do @@ -58,6 +59,14 @@ expect { setup_instance.run([]) }.to output( /Updating .gitignore/ ).to_stdout + expect { setup_instance.run([]) }.to output( + /Creating initial files/ + ).to_stdout + end + + it 'creates initial files' do + expect(setup_instance).to receive(:create_initial_files) + setup_instance.run([]) end context 'when Ruby version is less than 3.2' do @@ -104,6 +113,7 @@ allow(setup_instance).to receive(:configure_excluded_gems) allow(setup_instance).to receive(:build_ruby_wasm) allow(setup_instance).to receive(:update_gitignore) + allow(setup_instance).to receive(:create_initial_files) end it 'executes rbwasm build command with correct version' do @@ -184,4 +194,70 @@ expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js') end end + + describe '#create_initial_files' do + let(:temp_dir) { Dir.mktmpdir } + let(:src_dir) { File.join(temp_dir, 'src') } + + before do + allow(Dir).to receive(:pwd).and_return(temp_dir) + allow(Dir).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:write).and_call_original + allow(Dir).to receive(:mkdir).and_call_original + end + + after do + FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir) + end + + context 'when src directory does not exist' do + it 'creates src directory and initial files' do + expect(Dir).to receive(:exist?).with('src').and_return(false) + expect(File).to receive(:exist?).with('src/index.html').and_return(false) + expect(File).to receive(:exist?).with('src/index.rb').and_return(false) + expect(Dir).to receive(:mkdir).with('src') + expect(File).to receive(:write).with('src/index.html', anything) + expect(File).to receive(:write).with('src/index.rb', anything) + + setup_instance.send(:create_initial_files) + end + end + + context 'when src directory already exists' do + it 'skips file creation' do + expect(Dir).to receive(:exist?).with('src').and_return(true) + expect(File).not_to receive(:write) + + expect { setup_instance.send(:create_initial_files) }.to output( + /src directory already exists, skipping initial file creation/ + ).to_stdout + end + end + + context 'when src/index.html already exists' do + it 'skips file creation' do + expect(Dir).to receive(:exist?).with('src').and_return(false) + expect(File).to receive(:exist?).with('src/index.html').and_return(true) + expect(File).not_to receive(:write) + + expect { setup_instance.send(:create_initial_files) }.to output( + /src\/index.html already exists, skipping initial file creation/ + ).to_stdout + end + end + + context 'when src/index.rb already exists' do + it 'skips file creation' do + expect(Dir).to receive(:exist?).with('src').and_return(false) + expect(File).to receive(:exist?).with('src/index.html').and_return(false) + expect(File).to receive(:exist?).with('src/index.rb').and_return(true) + expect(File).not_to receive(:write) + + expect { setup_instance.send(:create_initial_files) }.to output( + /src\/index.rb already exists, skipping initial file creation/ + ).to_stdout + end + end + end end From bfa6e2c394fd0348b3d637438c1db657954215df Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 20:16:27 +0900 Subject: [PATCH 07/13] feat: Open index.html with dev command --- lib/ruby_wasm_ui/cli/command/dev.rb | 30 +++++++- spec/ruby_wasm_ui/cli/command/dev_spec.rb | 84 +++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/lib/ruby_wasm_ui/cli/command/dev.rb b/lib/ruby_wasm_ui/cli/command/dev.rb index 032df2b..81be1ce 100644 --- a/lib/ruby_wasm_ui/cli/command/dev.rb +++ b/lib/ruby_wasm_ui/cli/command/dev.rb @@ -4,6 +4,7 @@ require "listen" require "rack" require "thread" +require "rbconfig" module RubyWasmUi module Cli @@ -55,6 +56,22 @@ def run(_argv) private + def open_browser(url) + case RbConfig::CONFIG["host_os"] + when /darwin/ + system("open", url) + when /linux/ + system("xdg-open", url) + when /mswin|mingw|cygwin/ + system("start", url) + else + log_info("Please open #{url} in your browser") + end + rescue => e + log_info("Could not open browser automatically: #{e.message}") + log_info("Please open #{url} in your browser") + end + def cleanup return if @cleanup_done @cleanup_done = true @@ -142,7 +159,11 @@ def start_server # Static file server application static_app = lambda do |env| path = env["PATH_INFO"] - file_path = File.join(Dir.pwd, path == "/" ? "index.html" : path) + if path == "/" + file_path = File.join(Dir.pwd, "src", "index.html") + else + file_path = File.join(Dir.pwd, path) + end if File.exist?(file_path) && File.file?(file_path) content_type = Rack::Mime.mime_type(File.extname(file_path), "text/html") @@ -189,6 +210,13 @@ def start_server begin require "rack/handler/puma" log_info("Using handler: puma") + + # Open browser after a short delay to ensure server is ready + Thread.new do + sleep 1 + open_browser("http://localhost:#{port}/src/index.html") + end + Rack::Handler::Puma.run(app, Port: port, Host: "0.0.0.0") rescue LoadError => e log_error("Puma handler not available: #{e.message}") diff --git a/spec/ruby_wasm_ui/cli/command/dev_spec.rb b/spec/ruby_wasm_ui/cli/command/dev_spec.rb index 6fc5709..378106c 100644 --- a/spec/ruby_wasm_ui/cli/command/dev_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/dev_spec.rb @@ -292,6 +292,27 @@ def self.run(app, options = {}) ).to_stdout end + it 'opens browser after server starts' do + allow(Rack::Handler::Puma).to receive(:run) + browser_thread_called = false + + allow(Thread).to receive(:new) do |&block| + if block + browser_thread_called = true + # Execute block immediately for testing (without sleep) + allow(Kernel).to receive(:sleep) + block.call + end + instance_double(Thread) + end + + expect(dev_instance).to receive(:open_browser).with('http://localhost:8080/src/index.html') + + dev_instance.send(:start_server) + + expect(browser_thread_called).to be true + end + context 'when Puma handler is not available' do before do allow(Kernel).to receive(:require).with('rack/handler/puma').and_raise(LoadError.new('cannot load such file')) @@ -322,4 +343,67 @@ def self.run(app, options = {}) end end end + + describe '#open_browser' do + before do + FileUtils.mkdir_p('src') + end + + context 'on macOS' do + before do + allow(RbConfig::CONFIG).to receive(:[]).with('host_os').and_return('darwin') + end + + it 'calls open command' do + expect(dev_instance).to receive(:system).with('open', 'http://localhost:8080/src/index.html') + dev_instance.send(:open_browser, 'http://localhost:8080/src/index.html') + end + + context 'when system call fails' do + before do + allow(dev_instance).to receive(:system).and_raise(StandardError.new('Command failed')) + end + + it 'outputs fallback message' do + expect { dev_instance.send(:open_browser, 'http://localhost:8080/src/index.html') }.to output( + /Please open http:\/\/localhost:8080\/src\/index.html in your browser/ + ).to_stdout + end + end + end + + context 'on Linux' do + before do + allow(RbConfig::CONFIG).to receive(:[]).with('host_os').and_return('linux') + end + + it 'calls xdg-open command' do + expect(dev_instance).to receive(:system).with('xdg-open', 'http://localhost:8080/src/index.html') + dev_instance.send(:open_browser, 'http://localhost:8080/src/index.html') + end + end + + context 'on Windows' do + before do + allow(RbConfig::CONFIG).to receive(:[]).with('host_os').and_return('mswin') + end + + it 'calls start command' do + expect(dev_instance).to receive(:system).with('start', 'http://localhost:8080/src/index.html') + dev_instance.send(:open_browser, 'http://localhost:8080/src/index.html') + end + end + + context 'on unknown OS' do + before do + allow(RbConfig::CONFIG).to receive(:[]).with('host_os').and_return('unknown') + end + + it 'outputs manual instruction' do + expect { dev_instance.send(:open_browser, 'http://localhost:8080/src/index.html') }.to output( + /Please open http:\/\/localhost:8080\/src\/index.html in your browser/ + ).to_stdout + end + end + end end From 3ba75d35af5f067565567338733acf6480794097 Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 20:21:42 +0900 Subject: [PATCH 08/13] feat: Add pack command --- lib/ruby_wasm_ui/cli/command.rb | 4 +- lib/ruby_wasm_ui/cli/command/pack.rb | 72 +++++++++ spec/ruby_wasm_ui/cli/command/pack_spec.rb | 174 +++++++++++++++++++++ 3 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 lib/ruby_wasm_ui/cli/command/pack.rb create mode 100644 spec/ruby_wasm_ui/cli/command/pack_spec.rb diff --git a/lib/ruby_wasm_ui/cli/command.rb b/lib/ruby_wasm_ui/cli/command.rb index dc0d1ba..bcfa549 100644 --- a/lib/ruby_wasm_ui/cli/command.rb +++ b/lib/ruby_wasm_ui/cli/command.rb @@ -2,13 +2,15 @@ require_relative "command/setup" require_relative "command/dev" +require_relative "command/pack" module RubyWasmUi module Cli class Command COMMANDS = { "setup" => Command::Setup, - "dev" => Command::Dev + "dev" => Command::Dev, + "pack" => Command::Pack }.freeze def self.run(argv) diff --git a/lib/ruby_wasm_ui/cli/command/pack.rb b/lib/ruby_wasm_ui/cli/command/pack.rb new file mode 100644 index 0000000..59849c9 --- /dev/null +++ b/lib/ruby_wasm_ui/cli/command/pack.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require_relative "base" + +module RubyWasmUi + module Cli + class Command + class Pack < Base + def self.description + "Pack WASM file by packing Ruby source files" + end + + def run(_argv) + log_info("Packing WASM file...") + puts "" + + # Check if src directory exists + unless Dir.exist?("src") + log_error("src directory not found. Please run 'ruby-wasm-ui setup' first.") + exit 1 + end + + # Check if ruby.wasm exists + unless File.exist?("ruby.wasm") + log_error("ruby.wasm not found. Please run 'ruby-wasm-ui setup' first.") + exit 1 + end + + # Pack WASM file + pack + end + + private + + def pack + command = "bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm" + log_info("Packing: #{command}") + + Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| + stdin.close + + stdout_thread = Thread.new do + stdout.each_line do |line| + print line + $stdout.flush + end + end + + stderr_thread = Thread.new do + stderr.each_line do |line| + $stderr.print line + $stderr.flush + end + end + + stdout_thread.join + stderr_thread.join + + exit_status = wait_thr.value + + unless exit_status.success? + log_error("Pack failed") + exit 1 + end + + log_success("✓ Pack completed") + end + end + end + end + end +end diff --git a/spec/ruby_wasm_ui/cli/command/pack_spec.rb b/spec/ruby_wasm_ui/cli/command/pack_spec.rb new file mode 100644 index 0000000..742bbed --- /dev/null +++ b/spec/ruby_wasm_ui/cli/command/pack_spec.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'fileutils' +require 'tmpdir' + +RSpec.describe RubyWasmUi::Cli::Command::Pack do + let(:pack_instance) { described_class.new } + let(:temp_dir) { Dir.mktmpdir } + let(:original_dir) { Dir.pwd } + + around do |example| + Dir.chdir(temp_dir) do + example.run + end + ensure + Dir.chdir(original_dir) + FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir) + end + + describe '.description' do + it 'returns the description' do + expect(described_class.description).to eq('Pack WASM file by packing Ruby source files') + end + end + + describe '#run' do + context 'when src directory does not exist' do + it 'outputs error message and exits' do + expect { pack_instance.run([]) }.to output( + /src directory not found. Please run 'ruby-wasm-ui setup' first./ + ).to_stdout.and raise_error(SystemExit) + end + + it 'exits with status 1' do + expect { pack_instance.run([]) }.to raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end + end + end + + context 'when src directory exists but ruby.wasm does not exist' do + before do + FileUtils.mkdir_p('src') + end + + it 'outputs error message and exits' do + expect { pack_instance.run([]) }.to output( + /ruby.wasm not found. Please run 'ruby-wasm-ui setup' first./ + ).to_stdout.and raise_error(SystemExit) + end + + it 'exits with status 1' do + expect { pack_instance.run([]) }.to raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end + end + end + + context 'when src directory and ruby.wasm exist' do + before do + FileUtils.mkdir_p('src') + FileUtils.touch('ruby.wasm') + allow(pack_instance).to receive(:pack) + end + + it 'outputs packing message' do + expect { pack_instance.run([]) }.to output( + /Packing WASM file/ + ).to_stdout + end + + it 'calls pack method' do + expect(pack_instance).to receive(:pack) + pack_instance.run([]) + end + end + end + + describe '#pack' do + before do + FileUtils.mkdir_p('src') + FileUtils.touch('ruby.wasm') + end + + context 'when command succeeds' do + let(:stdin) { double(close: nil) } + let(:stdout) { double(each_line: ['output'].to_enum) } + let(:stderr) { double(each_line: [].to_enum) } + let(:wait_thr) { double(value: double(success?: true)) } + let(:stdout_thread) { instance_double(Thread, join: nil) } + let(:stderr_thread) { instance_double(Thread, join: nil) } + + before do + allow(Open3).to receive(:popen3).and_yield(stdin, stdout, stderr, wait_thr) + allow(Thread).to receive(:new).and_return(stdout_thread, stderr_thread) + end + + it 'executes rbwasm pack command' do + expect(Open3).to receive(:popen3).with( + 'bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm' + ) + pack_instance.send(:pack) + end + + it 'outputs pack message' do + expect { pack_instance.send(:pack) }.to output( + /Packing: bundle exec rbwasm pack/ + ).to_stdout + end + + it 'outputs success message' do + expect { pack_instance.send(:pack) }.to output( + /Pack completed/ + ).to_stdout + end + + it 'closes stdin' do + expect(stdin).to receive(:close) + pack_instance.send(:pack) + end + + it 'reads stdout in a thread' do + expect(Thread).to receive(:new).and_yield.and_return(stdout_thread) + allow(stdout_thread).to receive(:join) + allow(Thread).to receive(:new).and_call_original.once + allow(Thread).to receive(:new).and_return(stderr_thread).once + + pack_instance.send(:pack) + end + + it 'reads stderr in a thread' do + allow(Thread).to receive(:new).and_return(stdout_thread).once + expect(Thread).to receive(:new).and_yield.and_return(stderr_thread) + allow(stderr_thread).to receive(:join) + allow(Thread).to receive(:new).and_call_original.once + + pack_instance.send(:pack) + end + + it 'waits for threads to complete' do + expect(stdout_thread).to receive(:join) + expect(stderr_thread).to receive(:join) + pack_instance.send(:pack) + end + end + + context 'when command fails' do + let(:stdin) { double(close: nil) } + let(:stdout) { double(each_line: [].to_enum) } + let(:stderr) { double(each_line: ['error'].to_enum) } + let(:wait_thr) { double(value: double(success?: false, exitstatus: 1)) } + let(:stdout_thread) { instance_double(Thread, join: nil) } + let(:stderr_thread) { instance_double(Thread, join: nil) } + + before do + allow(Open3).to receive(:popen3).and_yield(stdin, stdout, stderr, wait_thr) + allow(Thread).to receive(:new).and_return(stdout_thread, stderr_thread) + end + + it 'outputs error message' do + expect { pack_instance.send(:pack) }.to output( + /Pack failed/ + ).to_stdout.and raise_error(SystemExit) + end + + it 'exits with status 1' do + expect { pack_instance.send(:pack) }.to raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end + end + end + end +end From 559fc63d40aed2f77cc67ca245e5580be4782417 Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 20:28:22 +0900 Subject: [PATCH 09/13] refactor: add util methods --- lib/ruby_wasm_ui/cli/command/base.rb | 36 +++++-- lib/ruby_wasm_ui/cli/command/dev.rb | 39 ++----- lib/ruby_wasm_ui/cli/command/pack.rb | 44 +------- spec/ruby_wasm_ui/cli/command/base_spec.rb | 113 ++++++++++++++++++++- spec/ruby_wasm_ui/cli/command/dev_spec.rb | 27 ++--- spec/ruby_wasm_ui/cli/command/pack_spec.rb | 59 ++--------- 6 files changed, 162 insertions(+), 156 deletions(-) diff --git a/lib/ruby_wasm_ui/cli/command/base.rb b/lib/ruby_wasm_ui/cli/command/base.rb index 4d233ff..571a6a4 100644 --- a/lib/ruby_wasm_ui/cli/command/base.rb +++ b/lib/ruby_wasm_ui/cli/command/base.rb @@ -16,12 +16,12 @@ def run(_argv) protected - def run_command(command) + def run_command(command, exit_on_error: true) log_debug("Executing command: #{command}") - + Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| stdin.close - + # Read stdout and stderr in separate threads for real-time output stdout_thread = Thread.new do stdout.each_line do |line| @@ -29,25 +29,45 @@ def run_command(command) $stdout.flush end end - + stderr_thread = Thread.new do stderr.each_line do |line| $stderr.print line $stderr.flush end end - + # Wait for both threads to finish reading stdout_thread.join stderr_thread.join - + # Wait for the process to finish exit_status = wait_thr.value - + unless exit_status.success? log_error("Command failed: #{command}") - exit exit_status.exitstatus + if exit_on_error + exit exit_status.exitstatus + else + return false + end end + + true + end + end + + def ensure_src_directory + unless Dir.exist?("src") + log_error("src directory not found. Please run 'ruby-wasm-ui setup' first.") + exit 1 + end + end + + def ensure_ruby_wasm + unless File.exist?("ruby.wasm") + log_error("ruby.wasm not found. Please run 'ruby-wasm-ui setup' first.") + exit 1 end end diff --git a/lib/ruby_wasm_ui/cli/command/dev.rb b/lib/ruby_wasm_ui/cli/command/dev.rb index 81be1ce..4cf0fc9 100644 --- a/lib/ruby_wasm_ui/cli/command/dev.rb +++ b/lib/ruby_wasm_ui/cli/command/dev.rb @@ -18,11 +18,7 @@ def run(_argv) log_info("Starting development server...") puts "" - # Check if src directory exists - unless Dir.exist?("src") - log_error("src directory not found. Please run 'ruby-wasm-ui setup' first.") - exit 1 - end + ensure_src_directory # Initial build log_info("Performing initial build...") @@ -83,36 +79,13 @@ def build command = "bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm" log_info("Building: #{command}") - Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| - stdin.close - - stdout_thread = Thread.new do - stdout.each_line do |line| - print line - $stdout.flush - end - end - - stderr_thread = Thread.new do - stderr.each_line do |line| - $stderr.print line - $stderr.flush - end - end - - stdout_thread.join - stderr_thread.join - - exit_status = wait_thr.value - - unless exit_status.success? - log_error("Build failed") - return false - end - + success = run_command(command, exit_on_error: false) + if success log_success("✓ Build completed") - true + else + log_error("Build failed") end + success end def start_file_watcher diff --git a/lib/ruby_wasm_ui/cli/command/pack.rb b/lib/ruby_wasm_ui/cli/command/pack.rb index 59849c9..b2c313a 100644 --- a/lib/ruby_wasm_ui/cli/command/pack.rb +++ b/lib/ruby_wasm_ui/cli/command/pack.rb @@ -14,17 +14,8 @@ def run(_argv) log_info("Packing WASM file...") puts "" - # Check if src directory exists - unless Dir.exist?("src") - log_error("src directory not found. Please run 'ruby-wasm-ui setup' first.") - exit 1 - end - - # Check if ruby.wasm exists - unless File.exist?("ruby.wasm") - log_error("ruby.wasm not found. Please run 'ruby-wasm-ui setup' first.") - exit 1 - end + ensure_src_directory + ensure_ruby_wasm # Pack WASM file pack @@ -36,35 +27,8 @@ def pack command = "bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm" log_info("Packing: #{command}") - Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| - stdin.close - - stdout_thread = Thread.new do - stdout.each_line do |line| - print line - $stdout.flush - end - end - - stderr_thread = Thread.new do - stderr.each_line do |line| - $stderr.print line - $stderr.flush - end - end - - stdout_thread.join - stderr_thread.join - - exit_status = wait_thr.value - - unless exit_status.success? - log_error("Pack failed") - exit 1 - end - - log_success("✓ Pack completed") - end + run_command(command) + log_success("✓ Pack completed") end end end diff --git a/spec/ruby_wasm_ui/cli/command/base_spec.rb b/spec/ruby_wasm_ui/cli/command/base_spec.rb index d4a05fa..7fdaaf6 100644 --- a/spec/ruby_wasm_ui/cli/command/base_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/base_spec.rb @@ -81,14 +81,125 @@ it 'does not raise error' do expect { base_instance.send(:run_command, 'echo "test"') }.not_to raise_error end + + it 'returns true' do + result = base_instance.send(:run_command, 'echo "test"') + expect(result).to be true + end end context 'when command fails' do - it 'outputs error message and exits' do + it 'outputs error message and exits by default' do expect { base_instance.send(:run_command, 'false') }.to output( /Command failed: false/ ).to_stdout.and raise_error(SystemExit) end + + it 'exits with correct status code' do + expect { base_instance.send(:run_command, 'false') }.to raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end + end + + context 'when exit_on_error is false' do + it 'outputs error message but does not exit' do + expect { base_instance.send(:run_command, 'false', exit_on_error: false) }.to output( + /Command failed: false/ + ).to_stdout + expect { base_instance.send(:run_command, 'false', exit_on_error: false) }.not_to raise_error + end + + it 'returns false' do + result = base_instance.send(:run_command, 'false', exit_on_error: false) + expect(result).to be false + end + end + end + end + + describe '#ensure_src_directory' do + let(:temp_dir) { Dir.mktmpdir } + let(:original_dir) { Dir.pwd } + + around do |example| + Dir.chdir(temp_dir) do + example.run + end + ensure + FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir) + end + + context 'when src directory does not exist' do + it 'outputs error message and exits' do + expect { base_instance.send(:ensure_src_directory) }.to output( + /src directory not found. Please run 'ruby-wasm-ui setup' first./ + ).to_stdout.and raise_error(SystemExit) + end + + it 'exits with status 1' do + expect { base_instance.send(:ensure_src_directory) }.to raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end + end + end + + context 'when src directory exists' do + before do + FileUtils.mkdir_p('src') + end + + it 'does not raise error' do + expect { base_instance.send(:ensure_src_directory) }.not_to raise_error + end + + it 'does not output error message' do + expect { base_instance.send(:ensure_src_directory) }.not_to output( + /src directory not found/ + ).to_stdout + end + end + end + + describe '#ensure_ruby_wasm' do + let(:temp_dir) { Dir.mktmpdir } + let(:original_dir) { Dir.pwd } + + around do |example| + Dir.chdir(temp_dir) do + example.run + end + ensure + FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir) + end + + context 'when ruby.wasm does not exist' do + it 'outputs error message and exits' do + expect { base_instance.send(:ensure_ruby_wasm) }.to output( + /ruby.wasm not found. Please run 'ruby-wasm-ui setup' first./ + ).to_stdout.and raise_error(SystemExit) + end + + it 'exits with status 1' do + expect { base_instance.send(:ensure_ruby_wasm) }.to raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end + end + end + + context 'when ruby.wasm exists' do + before do + FileUtils.touch('ruby.wasm') + end + + it 'does not raise error' do + expect { base_instance.send(:ensure_ruby_wasm) }.not_to raise_error + end + + it 'does not output error message' do + expect { base_instance.send(:ensure_ruby_wasm) }.not_to output( + /ruby.wasm not found/ + ).to_stdout + end end end end diff --git a/spec/ruby_wasm_ui/cli/command/dev_spec.rb b/spec/ruby_wasm_ui/cli/command/dev_spec.rb index 378106c..a575c7c 100644 --- a/spec/ruby_wasm_ui/cli/command/dev_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/dev_spec.rb @@ -113,21 +113,14 @@ end context 'when command succeeds' do - let(:stdin) { double(close: nil) } - let(:stdout) { double(each_line: ['output'].to_enum) } - let(:stderr) { double(each_line: [].to_enum) } - let(:wait_thr) { double(value: double(success?: true)) } - let(:stdout_thread) { instance_double(Thread, join: nil) } - let(:stderr_thread) { instance_double(Thread, join: nil) } - before do - allow(Open3).to receive(:popen3).and_yield(stdin, stdout, stderr, wait_thr) - allow(Thread).to receive(:new).and_return(stdout_thread, stderr_thread) + allow(dev_instance).to receive(:run_command).and_return(true) end - it 'executes rbwasm pack command' do - expect(Open3).to receive(:popen3).with( - 'bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm' + it 'executes rbwasm pack command via run_command' do + expect(dev_instance).to receive(:run_command).with( + 'bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm', + exit_on_error: false ) dev_instance.send(:build) end @@ -151,16 +144,8 @@ end context 'when command fails' do - let(:stdin) { double(close: nil) } - let(:stdout) { double(each_line: [].to_enum) } - let(:stderr) { double(each_line: ['error'].to_enum) } - let(:wait_thr) { double(value: double(success?: false, exitstatus: 1)) } - let(:stdout_thread) { instance_double(Thread, join: nil) } - let(:stderr_thread) { instance_double(Thread, join: nil) } - before do - allow(Open3).to receive(:popen3).and_yield(stdin, stdout, stderr, wait_thr) - allow(Thread).to receive(:new).and_return(stdout_thread, stderr_thread) + allow(dev_instance).to receive(:run_command).and_return(false) end it 'outputs error message' do diff --git a/spec/ruby_wasm_ui/cli/command/pack_spec.rb b/spec/ruby_wasm_ui/cli/command/pack_spec.rb index 742bbed..4937b4e 100644 --- a/spec/ruby_wasm_ui/cli/command/pack_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/pack_spec.rb @@ -84,20 +84,12 @@ end context 'when command succeeds' do - let(:stdin) { double(close: nil) } - let(:stdout) { double(each_line: ['output'].to_enum) } - let(:stderr) { double(each_line: [].to_enum) } - let(:wait_thr) { double(value: double(success?: true)) } - let(:stdout_thread) { instance_double(Thread, join: nil) } - let(:stderr_thread) { instance_double(Thread, join: nil) } - before do - allow(Open3).to receive(:popen3).and_yield(stdin, stdout, stderr, wait_thr) - allow(Thread).to receive(:new).and_return(stdout_thread, stderr_thread) + allow(pack_instance).to receive(:run_command).and_return(true) end - it 'executes rbwasm pack command' do - expect(Open3).to receive(:popen3).with( + it 'executes rbwasm pack command via run_command' do + expect(pack_instance).to receive(:run_command).with( 'bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm' ) pack_instance.send(:pack) @@ -114,54 +106,15 @@ /Pack completed/ ).to_stdout end - - it 'closes stdin' do - expect(stdin).to receive(:close) - pack_instance.send(:pack) - end - - it 'reads stdout in a thread' do - expect(Thread).to receive(:new).and_yield.and_return(stdout_thread) - allow(stdout_thread).to receive(:join) - allow(Thread).to receive(:new).and_call_original.once - allow(Thread).to receive(:new).and_return(stderr_thread).once - - pack_instance.send(:pack) - end - - it 'reads stderr in a thread' do - allow(Thread).to receive(:new).and_return(stdout_thread).once - expect(Thread).to receive(:new).and_yield.and_return(stderr_thread) - allow(stderr_thread).to receive(:join) - allow(Thread).to receive(:new).and_call_original.once - - pack_instance.send(:pack) - end - - it 'waits for threads to complete' do - expect(stdout_thread).to receive(:join) - expect(stderr_thread).to receive(:join) - pack_instance.send(:pack) - end end context 'when command fails' do - let(:stdin) { double(close: nil) } - let(:stdout) { double(each_line: [].to_enum) } - let(:stderr) { double(each_line: ['error'].to_enum) } - let(:wait_thr) { double(value: double(success?: false, exitstatus: 1)) } - let(:stdout_thread) { instance_double(Thread, join: nil) } - let(:stderr_thread) { instance_double(Thread, join: nil) } - before do - allow(Open3).to receive(:popen3).and_yield(stdin, stdout, stderr, wait_thr) - allow(Thread).to receive(:new).and_return(stdout_thread, stderr_thread) + allow(pack_instance).to receive(:run_command).and_raise(SystemExit.new(1)) end - it 'outputs error message' do - expect { pack_instance.send(:pack) }.to output( - /Pack failed/ - ).to_stdout.and raise_error(SystemExit) + it 'outputs error message and exits' do + expect { pack_instance.send(:pack) }.to raise_error(SystemExit) end it 'exits with status 1' do From 36a5ff2acaac6a8013783582f124ed9e709144a4 Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 20:36:04 +0900 Subject: [PATCH 10/13] feat: Add rebuild command --- lib/ruby_wasm_ui/cli/command.rb | 4 +- lib/ruby_wasm_ui/cli/command/base.rb | 51 ++++++ lib/ruby_wasm_ui/cli/command/rebuild.rb | 38 +++++ lib/ruby_wasm_ui/cli/command/setup.rb | 49 +----- spec/ruby_wasm_ui/cli/command/base_spec.rb | 155 ++++++++++++++++++ spec/ruby_wasm_ui/cli/command/rebuild_spec.rb | 95 +++++++++++ spec/ruby_wasm_ui/cli/command/setup_spec.rb | 135 ++++----------- spec/ruby_wasm_ui/cli/command_spec.rb | 26 +++ 8 files changed, 398 insertions(+), 155 deletions(-) create mode 100644 lib/ruby_wasm_ui/cli/command/rebuild.rb create mode 100644 spec/ruby_wasm_ui/cli/command/rebuild_spec.rb diff --git a/lib/ruby_wasm_ui/cli/command.rb b/lib/ruby_wasm_ui/cli/command.rb index bcfa549..907535e 100644 --- a/lib/ruby_wasm_ui/cli/command.rb +++ b/lib/ruby_wasm_ui/cli/command.rb @@ -3,6 +3,7 @@ require_relative "command/setup" require_relative "command/dev" require_relative "command/pack" +require_relative "command/rebuild" module RubyWasmUi module Cli @@ -10,7 +11,8 @@ class Command COMMANDS = { "setup" => Command::Setup, "dev" => Command::Dev, - "pack" => Command::Pack + "pack" => Command::Pack, + "rebuild" => Command::Rebuild }.freeze def self.run(argv) diff --git a/lib/ruby_wasm_ui/cli/command/base.rb b/lib/ruby_wasm_ui/cli/command/base.rb index 571a6a4..df7e815 100644 --- a/lib/ruby_wasm_ui/cli/command/base.rb +++ b/lib/ruby_wasm_ui/cli/command/base.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true require "open3" +require "bundler/setup" +require "ruby_wasm" +require "ruby_wasm/cli" module RubyWasmUi module Cli @@ -117,6 +120,54 @@ def log_error(message) def log_debug(message) puts "[DEBUG] #{message}" end + + def check_ruby_version + ruby_version = RUBY_VERSION.split('.').map(&:to_i) + major, minor = ruby_version[0], ruby_version[1] + + if major < 3 || (major == 3 && minor < 2) + log_error("Ruby WASM requires Ruby 3.2 or higher. Current version: #{RUBY_VERSION}") + exit 1 + end + + "#{major}.#{minor}" + end + + def configure_excluded_gems + # Get all gems from Bundler definition including all dependencies + definition = Bundler.definition + resolved = definition.resolve + + # Get all resolved specs (including transitive dependencies) + all_specs = resolved.materialize(definition.requested_dependencies) + gem_names = all_specs.map(&:name).uniq + + # Always exclude gems that cause WASM build errors + # These gems have native extensions that don't work in WASM environment + always_excluded = %w[nio4r puma rack listen ffi] + + # Exclude gems that cause build errors or are unnecessary for WASM + # Keep essential gems like 'js' for WASM + excluded_gems = gem_names.select do |gem_name| + # Exclude gems that cause WASM build errors + always_excluded.include?(gem_name) || + # Exclude development/test gems + gem_name.start_with?("rspec", "rubocop", "rake") + end + + # Always add always_excluded gems to ensure they're excluded even if not in dependencies + excluded_gems.concat(always_excluded) + excluded_gems.uniq! + + # Add to EXCLUDED_GEMS + RubyWasm::Packager::EXCLUDED_GEMS.concat(excluded_gems) + end + + def build_ruby_wasm(ruby_version_str) + command = ["build", "--ruby-version", ruby_version_str, "-o", "ruby.wasm"] + cli = RubyWasm::CLI.new(stdout: $stdout, stderr: $stderr) + cli.run(command) + end end end end diff --git a/lib/ruby_wasm_ui/cli/command/rebuild.rb b/lib/ruby_wasm_ui/cli/command/rebuild.rb new file mode 100644 index 0000000..6128853 --- /dev/null +++ b/lib/ruby_wasm_ui/cli/command/rebuild.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require_relative "base" + +module RubyWasmUi + module Cli + class Command + class Rebuild < Base + def self.description + "Rebuild Ruby WASM file (useful when gems are added)" + end + + def run(_argv) + log_info("Rebuilding Ruby WASM...") + puts "" + + # Check Ruby version + ruby_version_str = check_ruby_version + + # Configure excluded gems for WASM build + log_info("Step 1/2: Configuring excluded gems...") + configure_excluded_gems + log_success("✓ Excluded gems configured") + + # Build Ruby WASM + puts "" + log_info("Step 2/2: Building Ruby WASM...") + log_info("Running: rbwasm build --ruby-version #{ruby_version_str} -o ruby.wasm") + build_ruby_wasm(ruby_version_str) + log_success("✓ Ruby WASM build completed") + + puts "" + log_success("Rebuild completed successfully!") + end + end + end + end +end diff --git a/lib/ruby_wasm_ui/cli/command/setup.rb b/lib/ruby_wasm_ui/cli/command/setup.rb index 49c76b8..d750df4 100644 --- a/lib/ruby_wasm_ui/cli/command/setup.rb +++ b/lib/ruby_wasm_ui/cli/command/setup.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require "bundler/setup" -require "ruby_wasm" -require "ruby_wasm/cli" require_relative "base" module RubyWasmUi @@ -18,15 +15,7 @@ def run(_argv) puts "" # Check Ruby version - ruby_version = RUBY_VERSION.split('.').map(&:to_i) - major, minor = ruby_version[0], ruby_version[1] - - if major < 3 || (major == 3 && minor < 2) - log_error("Ruby WASM requires Ruby 3.2 or higher. Current version: #{RUBY_VERSION}") - exit 1 - end - - ruby_version_str = "#{major}.#{minor}" + ruby_version_str = check_ruby_version # Configure excluded gems for WASM build log_info("Step 1/3: Configuring excluded gems...") @@ -57,42 +46,6 @@ def run(_argv) private - def configure_excluded_gems - # Get all gems from Bundler definition including all dependencies - definition = Bundler.definition - resolved = definition.resolve - - # Get all resolved specs (including transitive dependencies) - all_specs = resolved.materialize(definition.requested_dependencies) - gem_names = all_specs.map(&:name).uniq - - # Always exclude gems that cause WASM build errors - # These gems have native extensions that don't work in WASM environment - always_excluded = %w[nio4r puma rack listen ffi] - - # Exclude gems that cause build errors or are unnecessary for WASM - # Keep essential gems like 'js' for WASM - excluded_gems = gem_names.select do |gem_name| - # Exclude gems that cause WASM build errors - always_excluded.include?(gem_name) || - # Exclude development/test gems - gem_name.start_with?("rspec", "rubocop", "rake") - end - - # Always add always_excluded gems to ensure they're excluded even if not in dependencies - excluded_gems.concat(always_excluded) - excluded_gems.uniq! - - # Add to EXCLUDED_GEMS - RubyWasm::Packager::EXCLUDED_GEMS.concat(excluded_gems) - end - - def build_ruby_wasm(ruby_version_str) - command = ["build", "--ruby-version", ruby_version_str, "-o", "ruby.wasm"] - cli = RubyWasm::CLI.new(stdout: $stdout, stderr: $stderr) - cli.run(command) - end - def create_initial_files # Skip if src directory exists if Dir.exist?("src") diff --git a/spec/ruby_wasm_ui/cli/command/base_spec.rb b/spec/ruby_wasm_ui/cli/command/base_spec.rb index 7fdaaf6..b7f4032 100644 --- a/spec/ruby_wasm_ui/cli/command/base_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/base_spec.rb @@ -202,4 +202,159 @@ end end end + + describe '#check_ruby_version' do + context 'when Ruby version is 3.2 or higher' do + before do + stub_const('RUBY_VERSION', '3.2.0') + end + + it 'returns the version string' do + result = base_instance.send(:check_ruby_version) + expect(result).to eq('3.2') + end + + it 'does not exit' do + expect { base_instance.send(:check_ruby_version) }.not_to raise_error + end + end + + context 'when Ruby version is 3.4' do + before do + stub_const('RUBY_VERSION', '3.4.0') + end + + it 'returns the version string' do + result = base_instance.send(:check_ruby_version) + expect(result).to eq('3.4') + end + end + + context 'when Ruby version is less than 3.2' do + before do + allow(Kernel).to receive(:exit) + end + + context 'when Ruby version is 3.1' do + before do + stub_const('RUBY_VERSION', '3.1.5') + end + + it 'outputs error message and exits' do + expect { base_instance.send(:check_ruby_version) }.to output( + /\[ERROR\] Ruby WASM requires Ruby 3.2 or higher. Current version: 3.1.5/ + ).to_stdout + expect(Kernel).to have_received(:exit).with(1) + end + end + + context 'when Ruby version is 2.7' do + before do + stub_const('RUBY_VERSION', '2.7.8') + end + + it 'outputs error message and exits' do + expect { base_instance.send(:check_ruby_version) }.to output( + /\[ERROR\] Ruby WASM requires Ruby 3.2 or higher. Current version: 2.7.8/ + ).to_stdout + expect(Kernel).to have_received(:exit).with(1) + end + end + end + end + + describe '#configure_excluded_gems' do + before do + # Clear EXCLUDED_GEMS before each test + RubyWasm::Packager::EXCLUDED_GEMS.clear if defined?(RubyWasm::Packager::EXCLUDED_GEMS) + end + + it 'excludes rack, puma, nio4r, listen, and ffi gems' do + allow(Bundler).to receive(:definition).and_return( + double( + resolve: double( + materialize: [ + double(name: 'rack'), + double(name: 'puma'), + double(name: 'nio4r'), + double(name: 'listen'), + double(name: 'ffi'), + double(name: 'js'), + double(name: 'ruby_wasm') + ] + ), + requested_dependencies: [] + ) + ) + + base_instance.send(:configure_excluded_gems) + + expect(RubyWasm::Packager::EXCLUDED_GEMS).to include('rack', 'puma', 'nio4r', 'listen', 'ffi') + expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js', 'ruby_wasm') + end + + it 'always excludes nio4r, puma, rack, listen, and ffi even if not in dependencies' do + allow(Bundler).to receive(:definition).and_return( + double( + resolve: double( + materialize: [ + double(name: 'js'), + double(name: 'ruby_wasm') + ] + ), + requested_dependencies: [] + ) + ) + + base_instance.send(:configure_excluded_gems) + + # Always excluded gems should be in the list even if not in dependencies + expect(RubyWasm::Packager::EXCLUDED_GEMS).to include('nio4r', 'puma', 'rack', 'listen', 'ffi') + expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js', 'ruby_wasm') + end + + it 'excludes development/test gems' do + allow(Bundler).to receive(:definition).and_return( + double( + resolve: double( + materialize: [ + double(name: 'rspec'), + double(name: 'rubocop'), + double(name: 'rake'), + double(name: 'js') + ] + ), + requested_dependencies: [] + ) + ) + + base_instance.send(:configure_excluded_gems) + + expect(RubyWasm::Packager::EXCLUDED_GEMS).to include('rspec', 'rubocop', 'rake') + expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js') + end + end + + describe '#build_ruby_wasm' do + let(:cli_instance) { instance_double(RubyWasm::CLI) } + + before do + allow(RubyWasm::CLI).to receive(:new).and_return(cli_instance) + allow(cli_instance).to receive(:run) + end + + it 'executes rbwasm build command with correct arguments' do + expect(RubyWasm::CLI).to receive(:new).with(stdout: $stdout, stderr: $stderr).and_return(cli_instance) + expect(cli_instance).to receive(:run).with(['build', '--ruby-version', '3.2', '-o', 'ruby.wasm']) + + base_instance.send(:build_ruby_wasm, '3.2') + end + + it 'executes rbwasm build command with different version' do + expect(RubyWasm::CLI).to receive(:new).with(stdout: $stdout, stderr: $stderr).and_return(cli_instance) + expect(cli_instance).to receive(:run).with(['build', '--ruby-version', '3.4', '-o', 'ruby.wasm']) + + base_instance.send(:build_ruby_wasm, '3.4') + end + end end diff --git a/spec/ruby_wasm_ui/cli/command/rebuild_spec.rb b/spec/ruby_wasm_ui/cli/command/rebuild_spec.rb new file mode 100644 index 0000000..bf646dc --- /dev/null +++ b/spec/ruby_wasm_ui/cli/command/rebuild_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RubyWasmUi::Cli::Command::Rebuild do + let(:rebuild_instance) { described_class.new } + + describe '.description' do + it 'returns the description' do + expect(described_class.description).to eq('Rebuild Ruby WASM file (useful when gems are added)') + end + end + + describe '#run' do + before do + allow(rebuild_instance).to receive(:check_ruby_version).and_return(RUBY_VERSION.split('.')[0..1].join('.')) + allow(rebuild_instance).to receive(:configure_excluded_gems) + allow(rebuild_instance).to receive(:build_ruby_wasm) + end + + it 'outputs rebuild message' do + expect { rebuild_instance.run([]) }.to output( + /Rebuilding Ruby WASM/ + ).to_stdout + end + + it 'checks Ruby version' do + expect(rebuild_instance).to receive(:check_ruby_version) + rebuild_instance.run([]) + end + + it 'configures excluded gems' do + expect(rebuild_instance).to receive(:configure_excluded_gems) + rebuild_instance.run([]) + end + + it 'executes rbwasm build command' do + ruby_version = RUBY_VERSION.split('.')[0..1].join('.') + expect(rebuild_instance).to receive(:build_ruby_wasm).with(ruby_version) + rebuild_instance.run([]) + end + + it 'outputs completion message' do + expect { rebuild_instance.run([]) }.to output( + /Rebuild completed successfully!/ + ).to_stdout + end + + it 'outputs progress messages' do + expect { rebuild_instance.run([]) }.to output( + /Configuring excluded gems/ + ).to_stdout + expect { rebuild_instance.run([]) }.to output( + /Building Ruby WASM/ + ).to_stdout + end + + it 'does not update .gitignore' do + expect(rebuild_instance).not_to respond_to(:update_gitignore) + rebuild_instance.run([]) + end + + it 'does not create initial files' do + expect(rebuild_instance).not_to respond_to(:create_initial_files) + rebuild_instance.run([]) + end + + context 'when Ruby version check fails' do + before do + allow(rebuild_instance).to receive(:check_ruby_version).and_raise(SystemExit.new(1)) + allow(Kernel).to receive(:exit) + end + + it 'does not execute subsequent steps' do + expect(rebuild_instance).not_to receive(:configure_excluded_gems) + expect(rebuild_instance).not_to receive(:build_ruby_wasm) + expect { rebuild_instance.run([]) }.to raise_error(SystemExit) + end + end + + context 'when Ruby version is 3.2' do + before do + stub_const('RUBY_VERSION', '3.2.0') + allow(rebuild_instance).to receive(:check_ruby_version).and_return('3.2') + allow(rebuild_instance).to receive(:configure_excluded_gems) + allow(rebuild_instance).to receive(:build_ruby_wasm) + end + + it 'executes rbwasm build command with correct version' do + expect(rebuild_instance).to receive(:build_ruby_wasm).with('3.2') + rebuild_instance.run([]) + end + end + end +end diff --git a/spec/ruby_wasm_ui/cli/command/setup_spec.rb b/spec/ruby_wasm_ui/cli/command/setup_spec.rb index 75bdfe3..442b757 100644 --- a/spec/ruby_wasm_ui/cli/command/setup_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/setup_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require 'tmpdir' RSpec.describe RubyWasmUi::Cli::Command::Setup do let(:setup_instance) { described_class.new } @@ -13,6 +14,7 @@ describe '#run' do before do + allow(setup_instance).to receive(:check_ruby_version).and_return(RUBY_VERSION.split('.')[0..1].join('.')) allow(setup_instance).to receive(:configure_excluded_gems) allow(setup_instance).to receive(:build_ruby_wasm) allow(setup_instance).to receive(:update_gitignore) @@ -25,6 +27,11 @@ ).to_stdout end + it 'checks Ruby version' do + expect(setup_instance).to receive(:check_ruby_version) + setup_instance.run([]) + end + it 'configures excluded gems' do expect(setup_instance).to receive(:configure_excluded_gems) setup_instance.run([]) @@ -69,47 +76,25 @@ setup_instance.run([]) end - context 'when Ruby version is less than 3.2' do + context 'when Ruby version check fails' do before do + allow(setup_instance).to receive(:check_ruby_version).and_raise(SystemExit.new(1)) allow(Kernel).to receive(:exit) end - context 'when Ruby version is 3.1' do - before do - stub_const('RUBY_VERSION', '3.1.5') - end - - it 'outputs error message and exits' do - expect { setup_instance.run([]) }.to output( - /\[ERROR\] Ruby WASM requires Ruby 3.2 or higher. Current version: 3.1.5/ - ).to_stdout - expect(Kernel).to have_received(:exit).with(1) - end - - it 'does not execute rbwasm build command' do - expect(setup_instance).not_to receive(:build_ruby_wasm) - expect(setup_instance).not_to receive(:configure_excluded_gems) - setup_instance.run([]) - end - end - - context 'when Ruby version is 2.7' do - before do - stub_const('RUBY_VERSION', '2.7.8') - end - - it 'outputs error message and exits' do - expect { setup_instance.run([]) }.to output( - /\[ERROR\] Ruby WASM requires Ruby 3.2 or higher. Current version: 2.7.8/ - ).to_stdout - expect(Kernel).to have_received(:exit).with(1) - end + it 'does not execute subsequent steps' do + expect(setup_instance).not_to receive(:configure_excluded_gems) + expect(setup_instance).not_to receive(:build_ruby_wasm) + expect(setup_instance).not_to receive(:update_gitignore) + expect(setup_instance).not_to receive(:create_initial_files) + expect { setup_instance.run([]) }.to raise_error(SystemExit) end end context 'when Ruby version is 3.2' do before do stub_const('RUBY_VERSION', '3.2.0') + allow(setup_instance).to receive(:check_ruby_version).and_return('3.2') allow(setup_instance).to receive(:configure_excluded_gems) allow(setup_instance).to receive(:build_ruby_wasm) allow(setup_instance).to receive(:update_gitignore) @@ -123,78 +108,6 @@ end end - describe '#configure_excluded_gems' do - before do - # Clear EXCLUDED_GEMS before each test - RubyWasm::Packager::EXCLUDED_GEMS.clear if defined?(RubyWasm::Packager::EXCLUDED_GEMS) - end - - it 'excludes rack, puma, nio4r, listen, and ffi gems' do - allow(Bundler).to receive(:definition).and_return( - double( - resolve: double( - materialize: [ - double(name: 'rack'), - double(name: 'puma'), - double(name: 'nio4r'), - double(name: 'listen'), - double(name: 'ffi'), - double(name: 'js'), - double(name: 'ruby_wasm') - ] - ), - requested_dependencies: [] - ) - ) - - setup_instance.send(:configure_excluded_gems) - - expect(RubyWasm::Packager::EXCLUDED_GEMS).to include('rack', 'puma', 'nio4r', 'listen', 'ffi') - expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js', 'ruby_wasm') - end - - it 'always excludes nio4r, puma, rack, listen, and ffi even if not in dependencies' do - allow(Bundler).to receive(:definition).and_return( - double( - resolve: double( - materialize: [ - double(name: 'js'), - double(name: 'ruby_wasm') - ] - ), - requested_dependencies: [] - ) - ) - - setup_instance.send(:configure_excluded_gems) - - # Always excluded gems should be in the list even if not in dependencies - expect(RubyWasm::Packager::EXCLUDED_GEMS).to include('nio4r', 'puma', 'rack', 'listen', 'ffi') - expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js', 'ruby_wasm') - end - - it 'excludes development/test gems' do - allow(Bundler).to receive(:definition).and_return( - double( - resolve: double( - materialize: [ - double(name: 'rspec'), - double(name: 'rubocop'), - double(name: 'rake'), - double(name: 'js') - ] - ), - requested_dependencies: [] - ) - ) - - setup_instance.send(:configure_excluded_gems) - - expect(RubyWasm::Packager::EXCLUDED_GEMS).to include('rspec', 'rubocop', 'rake') - expect(RubyWasm::Packager::EXCLUDED_GEMS).not_to include('js') - end - end - describe '#create_initial_files' do let(:temp_dir) { Dir.mktmpdir } let(:src_dir) { File.join(temp_dir, 'src') } @@ -212,13 +125,23 @@ end context 'when src directory does not exist' do + before do + # Ensure the directory doesn't exist before the test + FileUtils.rm_rf('src') if Dir.exist?('src') + end + + after do + # Clean up after test + FileUtils.rm_rf('src') if Dir.exist?('src') + end + it 'creates src directory and initial files' do expect(Dir).to receive(:exist?).with('src').and_return(false) expect(File).to receive(:exist?).with('src/index.html').and_return(false) expect(File).to receive(:exist?).with('src/index.rb').and_return(false) - expect(Dir).to receive(:mkdir).with('src') - expect(File).to receive(:write).with('src/index.html', anything) - expect(File).to receive(:write).with('src/index.rb', anything) + expect(Dir).to receive(:mkdir).with('src').and_call_original + expect(File).to receive(:write).with('src/index.html', anything).and_call_original + expect(File).to receive(:write).with('src/index.rb', anything).and_call_original setup_instance.send(:create_initial_files) end diff --git a/spec/ruby_wasm_ui/cli/command_spec.rb b/spec/ruby_wasm_ui/cli/command_spec.rb index a09d8d0..b754976 100644 --- a/spec/ruby_wasm_ui/cli/command_spec.rb +++ b/spec/ruby_wasm_ui/cli/command_spec.rb @@ -43,6 +43,25 @@ described_class.run(['dev', 'arg1', 'arg2']) end end + + context 'rebuild command' do + let(:rebuild_instance) { instance_double(RubyWasmUi::Cli::Command::Rebuild) } + + before do + allow(RubyWasmUi::Cli::Command::Rebuild).to receive(:new).and_return(rebuild_instance) + allow(rebuild_instance).to receive(:run) + end + + it 'executes the rebuild command' do + expect(rebuild_instance).to receive(:run).with([]) + described_class.run(['rebuild']) + end + + it 'passes remaining arguments to the command' do + expect(rebuild_instance).to receive(:run).with(['arg1', 'arg2']) + described_class.run(['rebuild', 'arg1', 'arg2']) + end + end end context 'with invalid command' do @@ -77,6 +96,9 @@ expect { described_class.show_usage }.to output( /dev.*Start development server with file watching and auto-build/ ).to_stdout + expect { described_class.show_usage }.to output( + /rebuild.*Rebuild Ruby WASM file/ + ).to_stdout end end @@ -88,5 +110,9 @@ it 'contains dev command' do expect(described_class::COMMANDS).to include('dev' => RubyWasmUi::Cli::Command::Dev) end + + it 'contains rebuild command' do + expect(described_class::COMMANDS).to include('rebuild' => RubyWasmUi::Cli::Command::Rebuild) + end end end From 0ee68dd882f40655783102d0d80417ae57476b22 Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 21:03:57 +0900 Subject: [PATCH 11/13] fix: xdescribe for execute all tests --- spec/ruby_wasm_ui/cli/command/base_spec.rb | 5 ++++- spec/ruby_wasm_ui/cli/command/dev_spec.rb | 23 +++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/spec/ruby_wasm_ui/cli/command/base_spec.rb b/spec/ruby_wasm_ui/cli/command/base_spec.rb index b7f4032..687ceee 100644 --- a/spec/ruby_wasm_ui/cli/command/base_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/base_spec.rb @@ -4,7 +4,9 @@ require 'fileutils' require 'tmpdir' -RSpec.describe RubyWasmUi::Cli::Command::Base do +# TODO: 一時的にコメントアウト - 他のテストが実行されるか確認 +# RSpec.describe RubyWasmUi::Cli::Command::Base do +RSpec.xdescribe RubyWasmUi::Cli::Command::Base do let(:base_instance) { described_class.new } describe '#run' do @@ -357,4 +359,5 @@ base_instance.send(:build_ruby_wasm, '3.4') end end +# end end diff --git a/spec/ruby_wasm_ui/cli/command/dev_spec.rb b/spec/ruby_wasm_ui/cli/command/dev_spec.rb index a575c7c..6ab4092 100644 --- a/spec/ruby_wasm_ui/cli/command/dev_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/dev_spec.rb @@ -4,18 +4,26 @@ require 'fileutils' require 'tmpdir' -RSpec.describe RubyWasmUi::Cli::Command::Dev do +# TODO: 一時的にスキップ - 他のテストが実行されるか確認(dev_spec.rbが実行されるとRSpecが中断される) +RSpec.xdescribe RubyWasmUi::Cli::Command::Dev do let(:dev_instance) { described_class.new } let(:temp_dir) { Dir.mktmpdir } let(:original_dir) { Dir.pwd } around do |example| - Dir.chdir(temp_dir) do - example.run + begin + Dir.chdir(temp_dir) do + example.run + end + ensure + # 確実に元のディレクトリに戻る + begin + Dir.chdir(original_dir) if Dir.exist?(original_dir) + rescue => e + # ディレクトリが存在しない場合は無視 + end + FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir) end - ensure - Dir.chdir(original_dir) - FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir) end describe '.description' do @@ -70,7 +78,8 @@ ).to_stdout end - it 'starts file watcher' do + # TODO: 一時的にスキップ - Process.kill('INT', Process.pid)がRSpec全体を中断させるため + xit 'starts file watcher' do file_watcher_called = false allow(dev_instance).to receive(:start_file_watcher) { file_watcher_called = true } allow(dev_instance).to receive(:start_server) do From 2cdd82f933f4bff2f2640524bede845f469a06bd Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 21:47:29 +0900 Subject: [PATCH 12/13] fix: test skip problem --- lib/ruby_wasm_ui/cli/command.rb | 4 +- lib/ruby_wasm_ui/cli/command/base.rb | 8 +- lib/ruby_wasm_ui/cli/command/dev.rb | 6 +- spec/ruby_wasm_ui/cli/command/base_spec.rb | 19 +-- spec/ruby_wasm_ui/cli/command/dev_spec.rb | 147 +++++++++++---------- 5 files changed, 94 insertions(+), 90 deletions(-) diff --git a/lib/ruby_wasm_ui/cli/command.rb b/lib/ruby_wasm_ui/cli/command.rb index 907535e..4b0a2a8 100644 --- a/lib/ruby_wasm_ui/cli/command.rb +++ b/lib/ruby_wasm_ui/cli/command.rb @@ -20,7 +20,7 @@ def self.run(argv) if command_name.nil? show_usage - exit 1 + raise SystemExit.new(1) end command_class = COMMANDS[command_name] @@ -28,7 +28,7 @@ def self.run(argv) puts "Unknown command: #{command_name}" puts "" show_usage - exit 1 + raise SystemExit.new(1) end command_class.new.run(argv[1..-1]) diff --git a/lib/ruby_wasm_ui/cli/command/base.rb b/lib/ruby_wasm_ui/cli/command/base.rb index df7e815..7113e19 100644 --- a/lib/ruby_wasm_ui/cli/command/base.rb +++ b/lib/ruby_wasm_ui/cli/command/base.rb @@ -50,7 +50,7 @@ def run_command(command, exit_on_error: true) unless exit_status.success? log_error("Command failed: #{command}") if exit_on_error - exit exit_status.exitstatus + raise SystemExit.new(exit_status.exitstatus) else return false end @@ -63,14 +63,14 @@ def run_command(command, exit_on_error: true) def ensure_src_directory unless Dir.exist?("src") log_error("src directory not found. Please run 'ruby-wasm-ui setup' first.") - exit 1 + raise SystemExit.new(1) end end def ensure_ruby_wasm unless File.exist?("ruby.wasm") log_error("ruby.wasm not found. Please run 'ruby-wasm-ui setup' first.") - exit 1 + raise SystemExit.new(1) end end @@ -127,7 +127,7 @@ def check_ruby_version if major < 3 || (major == 3 && minor < 2) log_error("Ruby WASM requires Ruby 3.2 or higher. Current version: #{RUBY_VERSION}") - exit 1 + raise SystemExit.new(1) end "#{major}.#{minor}" diff --git a/lib/ruby_wasm_ui/cli/command/dev.rb b/lib/ruby_wasm_ui/cli/command/dev.rb index 4cf0fc9..3b0be3e 100644 --- a/lib/ruby_wasm_ui/cli/command/dev.rb +++ b/lib/ruby_wasm_ui/cli/command/dev.rb @@ -176,7 +176,7 @@ def start_server trap("INT") do log_info("\nShutting down server...") cleanup - exit 0 + raise SystemExit.new(0) end # Use Puma handler (Rack 3.0 compatible) @@ -194,11 +194,11 @@ def start_server rescue LoadError => e log_error("Puma handler not available: #{e.message}") log_error("Please ensure puma gem is installed: gem install puma") - exit 1 + raise SystemExit.new(1) end rescue => e log_error("Server error: #{e.message}") - exit 1 + raise SystemExit.new(1) end end end diff --git a/spec/ruby_wasm_ui/cli/command/base_spec.rb b/spec/ruby_wasm_ui/cli/command/base_spec.rb index 687ceee..cd749f4 100644 --- a/spec/ruby_wasm_ui/cli/command/base_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/base_spec.rb @@ -4,9 +4,7 @@ require 'fileutils' require 'tmpdir' -# TODO: 一時的にコメントアウト - 他のテストが実行されるか確認 -# RSpec.describe RubyWasmUi::Cli::Command::Base do -RSpec.xdescribe RubyWasmUi::Cli::Command::Base do +RSpec.describe RubyWasmUi::Cli::Command::Base do let(:base_instance) { described_class.new } describe '#run' do @@ -233,10 +231,6 @@ end context 'when Ruby version is less than 3.2' do - before do - allow(Kernel).to receive(:exit) - end - context 'when Ruby version is 3.1' do before do stub_const('RUBY_VERSION', '3.1.5') @@ -245,8 +239,9 @@ it 'outputs error message and exits' do expect { base_instance.send(:check_ruby_version) }.to output( /\[ERROR\] Ruby WASM requires Ruby 3.2 or higher. Current version: 3.1.5/ - ).to_stdout - expect(Kernel).to have_received(:exit).with(1) + ).to_stdout.and raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end end end @@ -258,8 +253,9 @@ it 'outputs error message and exits' do expect { base_instance.send(:check_ruby_version) }.to output( /\[ERROR\] Ruby WASM requires Ruby 3.2 or higher. Current version: 2.7.8/ - ).to_stdout - expect(Kernel).to have_received(:exit).with(1) + ).to_stdout.and raise_error(SystemExit) do |error| + expect(error.status).to eq(1) + end end end end @@ -359,5 +355,4 @@ base_instance.send(:build_ruby_wasm, '3.4') end end -# end end diff --git a/spec/ruby_wasm_ui/cli/command/dev_spec.rb b/spec/ruby_wasm_ui/cli/command/dev_spec.rb index 6ab4092..edcca7e 100644 --- a/spec/ruby_wasm_ui/cli/command/dev_spec.rb +++ b/spec/ruby_wasm_ui/cli/command/dev_spec.rb @@ -4,8 +4,7 @@ require 'fileutils' require 'tmpdir' -# TODO: 一時的にスキップ - 他のテストが実行されるか確認(dev_spec.rbが実行されるとRSpecが中断される) -RSpec.xdescribe RubyWasmUi::Cli::Command::Dev do +RSpec.describe RubyWasmUi::Cli::Command::Dev do let(:dev_instance) { described_class.new } let(:temp_dir) { Dir.mktmpdir } let(:original_dir) { Dir.pwd } @@ -78,22 +77,20 @@ ).to_stdout end - # TODO: 一時的にスキップ - Process.kill('INT', Process.pid)がRSpec全体を中断させるため - xit 'starts file watcher' do + it 'starts file watcher' do file_watcher_called = false - allow(dev_instance).to receive(:start_file_watcher) { file_watcher_called = true } - allow(dev_instance).to receive(:start_server) do - # Simulate blocking server - sleep 0.01 - end # Allow Thread.new to actually create threads so start_file_watcher gets called allow(Thread).to receive(:new).and_call_original - Thread.new { sleep 0.02; Process.kill('INT', Process.pid) } - begin - dev_instance.run([]) - rescue SystemExit - # Expected when server is killed + allow(dev_instance).to receive(:start_file_watcher) do + file_watcher_called = true + end + # Mock start_server to block briefly to allow thread to execute + allow(dev_instance).to receive(:start_server) do + sleep 0.1 # Give thread time to execute end + + dev_instance.run([]) + expect(file_watcher_called).to be true end @@ -205,10 +202,10 @@ dev_instance.send(:start_file_watcher) callback = callback_container[:callback] expect(callback).not_to be_nil - + # Allow Thread.new to create actual threads for the debounce logic allow(Thread).to receive(:new).and_call_original - + callback.call(['src/app.rb'], [], []) sleep 0.6 # Wait for debounce expect(dev_instance).to have_received(:build) @@ -218,10 +215,10 @@ dev_instance.send(:start_file_watcher) callback = callback_container[:callback] expect(callback).not_to be_nil - + # Allow Thread.new to create actual threads for the debounce logic allow(Thread).to receive(:new).and_call_original - + callback.call(['src/app.rb.swp'], [], []) sleep 0.6 expect(dev_instance).not_to have_received(:build) @@ -252,64 +249,74 @@ def self.run(app, options = {}) before do FileUtils.mkdir_p('src') - stub_const('Rack::Handler::Puma', puma_handler_class) - allow(Kernel).to receive(:require).with('rack/handler/puma') end - it 'outputs server start message' do - allow(Rack::Handler::Puma).to receive(:run) - expect { dev_instance.send(:start_server) }.to output( - /Starting development server on http:\/\/localhost:8080/ - ).to_stdout - end + context 'when Puma handler is available' do + before do + stub_const('Rack::Handler::Puma', puma_handler_class) + # Mock require to prevent LoadError for rack/handler/puma + allow(dev_instance).to receive(:require).with('rack/handler/puma').and_return(true) + # Prevent browser from opening in all tests by default + allow(dev_instance).to receive(:open_browser) + # Prevent Thread.new from creating actual threads by default + allow(Thread).to receive(:new).and_return(instance_double(Thread)) + end - it 'outputs instructions message' do - allow(Rack::Handler::Puma).to receive(:run) - expect { dev_instance.send(:start_server) }.to output( - /Press Ctrl+C to stop/ - ).to_stdout - end + it 'outputs server start message' do + allow(Rack::Handler::Puma).to receive(:run) + expect { dev_instance.send(:start_server) }.to output( + a_string_including('Starting development server on http://localhost:8080') + ).to_stdout + end - it 'starts Puma server' do - expect(Rack::Handler::Puma).to receive(:run).with( - anything, - Port: 8080, - Host: '0.0.0.0' - ) - dev_instance.send(:start_server) - end + it 'outputs instructions message' do + allow(Rack::Handler::Puma).to receive(:run) + expect { dev_instance.send(:start_server) }.to output( + a_string_including('Press Ctrl+C to stop') + ).to_stdout + end - it 'outputs handler info message' do - allow(Rack::Handler::Puma).to receive(:run) - expect { dev_instance.send(:start_server) }.to output( - /Using handler: puma/ - ).to_stdout - end + it 'starts Puma server' do + expect(Rack::Handler::Puma).to receive(:run).with( + anything, + Port: 8080, + Host: '0.0.0.0' + ) + dev_instance.send(:start_server) + end + + it 'outputs handler info message' do + allow(Rack::Handler::Puma).to receive(:run) + expect { dev_instance.send(:start_server) }.to output( + a_string_including('Using handler: puma') + ).to_stdout + end + + it 'opens browser after server starts' do + allow(Rack::Handler::Puma).to receive(:run) + browser_thread_called = false - it 'opens browser after server starts' do - allow(Rack::Handler::Puma).to receive(:run) - browser_thread_called = false - - allow(Thread).to receive(:new) do |&block| - if block - browser_thread_called = true - # Execute block immediately for testing (without sleep) - allow(Kernel).to receive(:sleep) - block.call + allow(Thread).to receive(:new) do |&block| + if block + browser_thread_called = true + # Execute block immediately for testing (without sleep) + allow(Kernel).to receive(:sleep) + block.call + end + instance_double(Thread) end - instance_double(Thread) - end - - expect(dev_instance).to receive(:open_browser).with('http://localhost:8080/src/index.html') - - dev_instance.send(:start_server) - - expect(browser_thread_called).to be true - end + + expect(dev_instance).to receive(:open_browser).with('http://localhost:8080/src/index.html').and_return(nil) + + dev_instance.send(:start_server) + + expect(browser_thread_called).to be true + end + end context 'when Puma handler is not available' do before do - allow(Kernel).to receive(:require).with('rack/handler/puma').and_raise(LoadError.new('cannot load such file')) + allow(dev_instance).to receive(:require).with('rack/handler/puma').and_raise(LoadError.new('cannot load such file')) end it 'outputs error message' do @@ -327,6 +334,8 @@ def self.run(app, options = {}) context 'when server raises an error' do before do + stub_const('Rack::Handler::Puma', puma_handler_class) + allow(dev_instance).to receive(:require).with('rack/handler/puma').and_return(true) allow(Rack::Handler::Puma).to receive(:run).and_raise(StandardError.new('Server error')) end @@ -349,7 +358,7 @@ def self.run(app, options = {}) end it 'calls open command' do - expect(dev_instance).to receive(:system).with('open', 'http://localhost:8080/src/index.html') + expect(dev_instance).to receive(:system).with('open', 'http://localhost:8080/src/index.html').and_return(true) dev_instance.send(:open_browser, 'http://localhost:8080/src/index.html') end @@ -372,7 +381,7 @@ def self.run(app, options = {}) end it 'calls xdg-open command' do - expect(dev_instance).to receive(:system).with('xdg-open', 'http://localhost:8080/src/index.html') + expect(dev_instance).to receive(:system).with('xdg-open', 'http://localhost:8080/src/index.html').and_return(true) dev_instance.send(:open_browser, 'http://localhost:8080/src/index.html') end end @@ -383,7 +392,7 @@ def self.run(app, options = {}) end it 'calls start command' do - expect(dev_instance).to receive(:system).with('start', 'http://localhost:8080/src/index.html') + expect(dev_instance).to receive(:system).with('start', 'http://localhost:8080/src/index.html').and_return(true) dev_instance.send(:open_browser, 'http://localhost:8080/src/index.html') end end From ea5b14f299f2db0b8e16b50d5a65620cfbf37772 Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 8 Dec 2025 22:59:10 +0900 Subject: [PATCH 13/13] update: Build only runtime modules --- packages/npm-packages/runtime/rollup.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/npm-packages/runtime/rollup.config.mjs b/packages/npm-packages/runtime/rollup.config.mjs index ba92262..b45b8c9 100644 --- a/packages/npm-packages/runtime/rollup.config.mjs +++ b/packages/npm-packages/runtime/rollup.config.mjs @@ -18,7 +18,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // Find all Ruby files in ruby_wasm_ui directory const rubyFiles = glob - .sync("src/ruby_wasm_ui/**/*.rb") + .sync("src/ruby_wasm_ui/runtime/**/*.rb") .map((file) => file.replace("src/", "")) .sort((a, b) => { // Files in root directory should be loaded first