diff --git a/README.md b/README.md index 2db81e81..08022ab8 100644 --- a/README.md +++ b/README.md @@ -232,11 +232,19 @@ the import and the `Extension()` call from the package name, which every extension package follows. `branch` and `tag` work in place of `ref`, and `path` takes a local checkout instead of a repository. +At build time the managed Flutter client keeps the core `flet` package plus +only the local packages selected by `extensions:` and `services:`. Packages +removed for one build are restored automatically from the client template if a +later configuration declares them again. + ### Protected capabilities are explicit Camera, microphone, location, and motion access are declared in `services.yaml`. Ruflet activates the required client integrations and writes -the matching Android and iOS permission descriptions during the build. +the matching Android and iOS permission descriptions during the build. Ruflet +also removes its previously managed permissions when the capability is no +longer declared, so stale camera, microphone, location, or motion access does +not ship in the next build. ```yaml services: @@ -396,6 +404,9 @@ ruflet build apk --self ruflet build ios --self ``` +`ruflet build ios --self` prepares both the physical-device and simulator app +bundles. You do not need a separate simulator build command. + Install the latest mobile build on a connected device: ```bash @@ -404,6 +415,10 @@ ruflet install ruflet install --device DEVICE_ID ``` +With one compatible mobile target connected, plain `ruflet install` selects it +automatically; `--device` remains available when several matching targets are +connected. + Ruflet uses the canonical external Flutter template, resolves packages, applies app identity, generates splash screens and launcher icons, prepares native dependencies, and then runs the platform build. diff --git a/packages/ruflet/lib/ruflet/cli/build_command.rb b/packages/ruflet/lib/ruflet/cli/build_command.rb index 92de01fe..ddd3664f 100644 --- a/packages/ruflet/lib/ruflet/cli/build_command.rb +++ b/packages/ruflet/lib/ruflet/cli/build_command.rb @@ -29,6 +29,7 @@ module BuildCommand "qrcode_scanner" => { package: "ruflet_qrcode_scanner", alias: "ruflet_qrcode_scanner" }, "rive" => { package: "flet_rive", alias: "ruflet_rive" }, "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" }, + "spinkit" => { package: "flet_spinkit", alias: "ruflet_spinkit" }, "video" => { package: "flet_video", alias: "ruflet_video" }, "webview" => { package: "flet_webview", alias: "ruflet_webview" } }.freeze @@ -53,6 +54,14 @@ module BuildCommand "location" => "NSLocationWhenInUseUsageDescription", "motion" => "NSMotionUsageDescription" }.freeze + MANAGED_ANDROID_PERMISSIONS = ( + ANDROID_SERVICE_PERMISSIONS.values.flatten + + %w[android.permission.FLASHLIGHT android.permission.MODIFY_AUDIO_SETTINGS] + ).uniq.freeze + MANAGED_IOS_USAGE_KEYS = ( + IOS_SERVICE_USAGE_KEYS.values + + %w[NSLocationAlwaysAndWhenInUseUsageDescription NSPhotoLibraryUsageDescription] + ).uniq.freeze # Clients that are told which server to use at launch rather than at build # time, so they may be built without a configured backend_url. RUNTIME_RESOLVED_BACKEND_PLATFORMS = %w[web macos windows linux].freeze @@ -156,6 +165,25 @@ def command_build(args) build_note("Running Flutter #{build_args.join(' ')}") ok = run_external_command(command_env, tools[:flutter], *build_args, chdir: client_dir, unbundled: true) + + # A physical iPhone app and a simulator app are different Apple + # products, but Ruflet users should not have to learn that distinction. + # Keep both outputs behind the ordinary self-contained iOS build so the + # following `ruflet install` can target whichever mobile device is + # connected. An explicit --simulator remains a simulator-only build. + if ok && self_contained && requested_platform == "ios" && !build_args.include?("--simulator") + simulator_args = build_args.dup + simulator_args.delete("--codesign") + simulator_args.insert(simulator_args.index("ios") + 1, "--simulator") + stage_ios_simulator_ruby_runtime(client_dir, simulator_args, verbose: !!verbose) + build_log(verbose, "command=#{([tools[:flutter]] + simulator_args).join(' ')}") + build_note("Running Flutter #{simulator_args.join(' ')}") + ok = run_external_command( + command_env, tools[:flutter], *simulator_args, + chdir: client_dir, unbundled: true + ) + end + export_platform_build_outputs(client_dir, platform, verbose: !!verbose) if ok ok ? 0 : 1 end @@ -1554,6 +1582,7 @@ def apply_service_extension_config(client_dir, config = {}, self_contained: @ruf prune_client_pubspec(pubspec_path, extension_packages) sync_external_extension_dependencies(pubspec_path, external) end + sync_client_package_directories(client_dir, extension_packages) client_entrypoint_paths(client_dir).each do |entrypoint| next unless File.file?(entrypoint) @@ -1748,8 +1777,6 @@ def apply_native_service_permissions(client_dir, config) extension_services.each do |service| entries << { name: service, description: "" } unless entries.any? { |entry| entry[:name] == service } end - return if entries.empty? - apply_android_service_permissions(client_dir, entries) apply_ios_service_usage_descriptions(client_dir, entries) end @@ -1759,6 +1786,12 @@ def apply_android_service_permissions(client_dir, entries) return unless File.file?(path) content = read_text_file(path) + MANAGED_ANDROID_PERMISSIONS.each do |permission| + content.gsub!( + %r{^\s*]*android:name="#{Regexp.escape(permission)}"[^>]*/>\s*\n?}, + "" + ) + end entries.flat_map { |entry| ANDROID_SERVICE_PERMISSIONS.fetch(entry[:name], []) }.uniq.each do |permission| next if content.include?(%(android:name="#{permission}")) @@ -1772,6 +1805,12 @@ def apply_ios_service_usage_descriptions(client_dir, entries) return unless File.file?(path) content = read_text_file(path) + MANAGED_IOS_USAGE_KEYS.each do |key| + content.gsub!( + %r{\s*#{Regexp.escape(key)}\s*.*?}m, + "" + ) + end entries.each do |entry| key = IOS_SERVICE_USAGE_KEYS[entry[:name]] next unless key @@ -1841,8 +1880,6 @@ def sync_client_pubspec_for_runtime_mode(client_dir, self_contained:) data = YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {} dependencies = data["dependencies"] dependencies = data["dependencies"] = {} unless dependencies.is_a?(Hash) - spinkit_dependency = template_client_pubspec_dependencies["flet_spinkit"] - dependencies["flet_spinkit"] = spinkit_dependency if spinkit_dependency flutter = data["flutter"] flutter = data["flutter"] = {} unless flutter.is_a?(Hash) assets = Array(flutter["assets"]).map(&:to_s) @@ -1909,7 +1946,6 @@ def refresh_managed_client_template_files(client_dir, verbose: false) "lib/main.dart", "lib/main.self.dart", "lib/main.server.dart", - "lib/ruflet_file_picker_service.dart", "lib/connection_probe.dart", "lib/connection_probe_io.dart", "lib/connection_probe_stub.dart", @@ -1934,6 +1970,11 @@ def refresh_managed_client_template_files(client_dir, verbose: false) FileUtils.cp(source, destination) build_log(verbose, "refreshed template file #{relative_path}") end + + # Older managed clients carried a macOS-only FilePicker override. Flet's + # core service owns FilePicker now, so this duplicate must not survive a + # template refresh. + FileUtils.rm_f(File.join(client_dir, "lib", "ruflet_file_picker_service.dart")) end def write_pubspec_yaml(path, data) @@ -2214,6 +2255,40 @@ def prune_client_pubspec(path, selected_packages) write_pubspec_yaml(path, data) end + def sync_client_package_directories(client_dir, selected_packages) + packages_root = File.join(client_dir, "flet_packages") + return unless Dir.exist?(packages_root) + + kept_packages = (["flet"] + selected_packages).uniq + template_root = + if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true) + Ruflet::CLI.send(:resolve_ruflet_client_template_root) + end + template_packages_root = template_root && File.join(template_root, "flet_packages") + + # A declaration can change between builds. Restore newly selected local + # packages from the immutable template before pruning the old selection. + kept_packages.each do |package_name| + destination = File.join(packages_root, package_name) + next if Dir.exist?(destination) + next unless template_packages_root + + source = File.join(template_packages_root, package_name) + next unless Dir.exist?(source) + next if File.expand_path(source) == File.expand_path(destination) + + FileUtils.cp_r(source, destination) + end + + Dir.children(packages_root).each do |entry| + path = File.join(packages_root, entry) + next unless Dir.exist?(path) + next if kept_packages.include?(entry) + + FileUtils.rm_rf(path) + end + end + def sync_client_extension_dependencies(path, selected_packages) return if selected_packages.empty? diff --git a/packages/ruflet/test/build_package_pruning_test.rb b/packages/ruflet/test/build_package_pruning_test.rb new file mode 100644 index 00000000..483398ca --- /dev/null +++ b/packages/ruflet/test/build_package_pruning_test.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "fileutils" + +class RufletCliBuildPackagePruningTest < Minitest::Test + class DummyBuilder + include Ruflet::CLI::BuildCommand + end + + def test_keeps_only_core_and_packages_declared_by_services_and_extensions + builder = DummyBuilder.new + + Dir.mktmpdir do |dir| + template_root = File.join(dir, "template") + client_dir = File.join(dir, "client") + package_names = %w[ + flet flet_audio_recorder flet_permission_handler flet_spinkit flet_video + ] + package_names.each do |name| + FileUtils.mkdir_p(File.join(template_root, "flet_packages", name)) + File.write(File.join(template_root, "flet_packages", name, "marker"), name) + FileUtils.mkdir_p(File.join(client_dir, "flet_packages", name)) + end + FileUtils.mkdir_p(File.join(client_dir, "flet_packages", "flet_ads")) + FileUtils.mkdir_p(File.join(client_dir, "lib")) + File.write( + File.join(template_root, "pubspec.yaml"), + <<~YAML + dependencies: + flet_audio_recorder: + path: flet_packages/flet_audio_recorder + flet_permission_handler: + path: flet_packages/flet_permission_handler + flet_spinkit: + path: flet_packages/flet_spinkit + flet_video: + path: flet_packages/flet_video + YAML + ) + File.write( + File.join(client_dir, "pubspec.yaml"), + "dependencies:\n flet:\n path: flet_packages/flet\n" + ) + + Ruflet::CLI.stub(:resolve_ruflet_client_template_root, template_root) do + builder.send( + :apply_service_extension_config, + client_dir, + { "services" => ["microphone"], "extensions" => ["spinkit"] } + ) + + assert_equal( + %w[flet flet_audio_recorder flet_permission_handler flet_spinkit], + package_directories(client_dir) + ) + + # A later build with a different declaration restores its package from + # the immutable template before pruning packages no longer selected. + builder.send( + :apply_service_extension_config, + client_dir, + { "extensions" => ["video"] } + ) + assert_equal %w[flet flet_video], package_directories(client_dir) + assert_equal "flet_video", File.read(File.join(client_dir, "flet_packages", "flet_video", "marker")) + end + end + end + + def test_native_permissions_are_replaced_by_current_service_declarations + builder = DummyBuilder.new + + Dir.mktmpdir do |dir| + manifest = File.join(dir, "android", "app", "src", "main", "AndroidManifest.xml") + plist = File.join(dir, "ios", "Runner", "Info.plist") + FileUtils.mkdir_p(File.dirname(manifest)) + FileUtils.mkdir_p(File.dirname(plist)) + File.write( + manifest, + <<~XML + + + + + + + + + XML + ) + File.write( + plist, + <<~XML + + NSCameraUsageDescriptionold camera + NSMicrophoneUsageDescriptionold microphone + NSLocationWhenInUseUsageDescriptionold location + NSPhotoLibraryUsageDescriptionold photos + + XML + ) + + builder.send( + :apply_native_service_permissions, + dir, + { "services" => [{ "microphone" => { "description" => "Record voice notes." } }] } + ) + + android = File.read(manifest) + assert_includes android, "android.permission.INTERNET" + assert_includes android, "android.permission.RECORD_AUDIO" + refute_includes android, "android.permission.CAMERA" + refute_includes android, "android.permission.ACCESS_FINE_LOCATION" + refute_includes android, "android.permission.FLASHLIGHT" + + ios = File.read(plist) + assert_includes ios, "NSMicrophoneUsageDescription" + assert_includes ios, "Record voice notes." + refute_includes ios, "NSCameraUsageDescription" + refute_includes ios, "NSLocationWhenInUseUsageDescription" + refute_includes ios, "NSPhotoLibraryUsageDescription" + end + end + + private + + def package_directories(client_dir) + Dir.children(File.join(client_dir, "flet_packages")) + .select { |entry| Dir.exist?(File.join(client_dir, "flet_packages", entry)) } + .sort + end +end diff --git a/packages/ruflet/test/update_command_test.rb b/packages/ruflet/test/update_command_test.rb index 880dd72d..9dd50b79 100644 --- a/packages/ruflet/test/update_command_test.rb +++ b/packages/ruflet/test/update_command_test.rb @@ -281,7 +281,7 @@ def test_prepare_flutter_client_uses_source_ruby_runtime_dependency pubspec = File.read(File.join(client_dir, "pubspec.yaml")) ruby_runtime = YAML.safe_load(pubspec, aliases: true).dig("dependencies", "ruby_runtime") assert_equal File.expand_path("../../../ruby_runtime", __dir__), ruby_runtime["path"] - assert YAML.safe_load(pubspec, aliases: true).dig("dependencies", "flet_spinkit") + refute YAML.safe_load(pubspec, aliases: true).dig("dependencies", "flet_spinkit") refute_path_exists File.join(client_dir, "lib", "ruflet_spinkit.dart") assert_includes calls, client_dir end @@ -876,7 +876,8 @@ def test_command_build_ios_uses_unbundled_env_for_flutter assert_equal 0, code refute calls.first[:env].key?("BUNDLE_GEMFILE") - assert_equal ["flutter", "build", "ios", "--codesign", "--target", "lib/main.self.dart"], calls.first[:args] + assert_equal ["flutter", "build", "ios", "--codesign", "--target", "lib/main.self.dart"], calls.first[:args] + assert_equal ["flutter", "build", "ios", "--simulator", "--target", "lib/main.self.dart"], calls.last[:args] refute_includes calls.first[:env]["PATH"], "/Users/macbookpro/.gem/ruby/3.4.0/bin" assert_includes calls.first[:env]["PATH"], File.join(client_dir, ".ruflet", "bin") assert File.executable?(File.join(client_dir, ".ruflet", "bin", "pod")) @@ -934,6 +935,7 @@ def test_command_install_syncs_ios_build_outputs_and_runs_flutter_install ios_app_dir = File.join(dir, "build", "ios", "iphonesimulator", "Runner.app") FileUtils.mkdir_p(ios_app_dir) File.write(File.join(ios_app_dir, "Info.plist"), "plist") + File.write(File.join(ios_app_dir, "Runner"), "simulator executable") builder.define_singleton_method(:detect_flutter_client_dir) { client_dir } builder.define_singleton_method(:load_ruflet_config) { {} } @@ -941,7 +943,6 @@ def test_command_install_syncs_ios_build_outputs_and_runs_flutter_install { flutter: "flutter", dart: "dart", env: {} } end builder.define_singleton_method(:prepare_flutter_client) { |_client_dir, **_kwargs| flunk("install should not run build preparation") } - calls = [] builder.define_singleton_method(:system) do |_env, *_args, chdir: nil| calls << { env: _env, args: _args, chdir: chdir } @@ -1005,6 +1006,8 @@ def test_command_install_fails_when_no_built_outputs_exist builder = DummyBuilder.new Dir.mktmpdir do |dir| + previous_dir = Dir.pwd + Dir.chdir(dir) client_dir = File.join(dir, "ruflet_client") FileUtils.mkdir_p(File.join(client_dir, "lib")) File.write(File.join(client_dir, "lib", "main.server.dart"), "void main() {}\n") @@ -1027,6 +1030,7 @@ def test_command_install_fails_when_no_built_outputs_exist assert_includes err.string, "Could not find built app outputs under ./build" ensure $stderr = original_stderr + Dir.chdir(previous_dir) if previous_dir end end diff --git a/packages/ruflet_rails/Gemfile b/packages/ruflet_rails/Gemfile new file mode 100644 index 00000000..9398fdbd --- /dev/null +++ b/packages/ruflet_rails/Gemfile @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +# Develop against the sibling packages in this repo, not the released gems. +# The HTML DSL leans on current ruflet_core (ControlFactory, the control +# registry the transformer builds every tag from) and on ruflet_server's +# WireCodec, so resolving those to a published version would test the gem +# against code it is not actually shipping with. +gem "ruflet", path: "../ruflet" +gem "ruflet_core", path: "../ruflet_core" +gem "ruflet_server", path: "../ruflet_server" + +group :development, :test do + gem "minitest", "~> 5.0" + gem "rake", "~> 13.0" +end diff --git a/packages/ruflet_rails/Gemfile.lock b/packages/ruflet_rails/Gemfile.lock new file mode 100644 index 00000000..81032e49 --- /dev/null +++ b/packages/ruflet_rails/Gemfile.lock @@ -0,0 +1,338 @@ +PATH + remote: ../ruflet_core + specs: + ruflet_core (0.0.21) + +PATH + remote: ../ruflet_server + specs: + ruflet_server (0.0.21) + ruflet_core (= 0.0.21) + +PATH + remote: ../ruflet + specs: + ruflet (0.0.21) + rqrcode (~> 2.2) + +PATH + remote: . + specs: + ruflet_rails (0.0.16) + rails (>= 7.0) + ruflet (>= 0.0.21) + ruflet_core (>= 0.0.21) + ruflet_server (>= 0.0.21) + +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.19) + railties + actioncable (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) + mail (>= 2.8.0) + actionmailer (8.1.3.1) + actionpack (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activesupport (= 8.1.3.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.3.1) + actionview (= 8.1.3.1) + activesupport (= 8.1.3.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.3.1) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.3.1) + activesupport (= 8.1.3.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.3.1) + activesupport (= 8.1.3.1) + globalid (>= 0.3.6) + activemodel (8.1.3.1) + activesupport (= 8.1.3.1) + activerecord (8.1.3.1) + activemodel (= 8.1.3.1) + activesupport (= 8.1.3.1) + timeout (>= 0.4.0) + activestorage (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activesupport (= 8.1.3.1) + marcel (~> 1.0) + activesupport (8.1.3.1) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + base64 (0.3.0) + bigdecimal (4.1.2) + builder (3.3.0) + chunky_png (1.4.0) + concurrent-ruby (1.3.8) + connection_pool (3.0.2) + crass (1.0.7) + date (3.5.1) + drb (2.2.3) + erb (6.0.6) + erubi (1.13.1) + globalid (1.4.0) + activesupport (>= 6.1) + i18n (1.15.2) + concurrent-ruby (~> 1.0) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.21.1) + logger (1.7.0) + loofah (2.25.2) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.1) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.2.1) + mini_mime (1.1.5) + minitest (5.27.0) + net-imap (0.6.6) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-musl) + racc (~> 1.4) + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3.1) + actioncable (= 8.1.3.1) + actionmailbox (= 8.1.3.1) + actionmailer (= 8.1.3.1) + actionpack (= 8.1.3.1) + actiontext (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activemodel (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) + bundler (>= 1.15.0) + railties (= 8.1.3.1) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rake (13.4.2) + rbs (4.1.1) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + reline (0.6.3) + io-console (~> 0.5) + rqrcode (2.2.0) + chunky_png (~> 1.0) + rqrcode_core (~> 1.0) + rqrcode_core (1.2.0) + securerandom (0.4.1) + thor (1.5.0) + timeout (0.6.1) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.1.1) + useragent (0.16.11) + websocket-driver (0.8.2) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.8.2) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + minitest (~> 5.0) + rake (~> 13.0) + ruflet! + ruflet_core! + ruflet_rails! + ruflet_server! + +CHECKSUMS + action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 + actioncable (8.1.3.1) sha256=e318528295c878a3efdfe25f0f2267c80cb7a76eba41bb5f64d44aa380a3d91b + actionmailbox (8.1.3.1) sha256=5f704972097d843ade8e435e93694a1dac732b926df1717aceba1f3840082b1c + actionmailer (8.1.3.1) sha256=88ea441b28ff02a0c6c006468892642a3d9942affce9d294e81a74504aa5c43c + actionpack (8.1.3.1) sha256=974cb7154548e81f470b1b0f247b99cb38e87825899dca58610596e2817723d0 + actiontext (8.1.3.1) sha256=5da729d833d1a29cddb1eee938878e55e503d2613e00e735f5daf58c2ba98af2 + actionview (8.1.3.1) sha256=2da68b8414c47b43bfbed1ce69c5afe1c04f78c267aacb5660a4cab5ca12cfb6 + activejob (8.1.3.1) sha256=1c8dd275df930df40deecffec63d913a550a33fd94bd298f69721dd96939954a + activemodel (8.1.3.1) sha256=99cc02ce2faec371d14440949d85787ebd23a907c9baef0a9d4bcd4d21888f88 + activerecord (8.1.3.1) sha256=0a2fb6c28f4938f6b013a3a549bec0a7e37d535f3dc8990e804bcc3258c0403b + activestorage (8.1.3.1) sha256=f555254f387b1cffa499d2fd3115d12635eadc5b15206a8534316a67036163ef + activesupport (8.1.3.1) sha256=85458765f25ea48b9019c46b6bb3fa5683197bf4280d9f06710a6e8d7a831376 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + chunky_png (1.4.0) sha256=89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe + concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + erb (6.0.6) sha256=a9b24986700f5bf127c4f297c5403c3ca41b83b0a316c0cd09a096b56e644ae5 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + globalid (1.4.0) sha256=037f12fbf1d9d7a014d501c2d5c77356fd4ddd96d7a7991d6700bba96706f427 + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918 + mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8 + marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5 + net-imap (0.6.6) sha256=96aa4ee50df3060203e649efc341f53480b791d49e150f2fdebf68beb141a8df + net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 + net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 + net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f + nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af + nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca + nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb + nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527 + nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551 + nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a + nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b + pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2 + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails (8.1.3.1) sha256=ccd11a36bfc171bf9c66d585d14c0ece91c0c9dde840aae60c0118d6f5c9c52a + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + railties (8.1.3.1) sha256=2388a232579a00cefea4487de66c8553c3408c1300abdc6cf1799d86ffb04487 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rbs (4.1.1) sha256=1bf118f2f95d1cd3956357645d495152b661e0257e57781d18ceecd461622b9e + rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + rqrcode (2.2.0) sha256=23eea88bb44c7ee6d6cab9354d08c287f7ebcdc6112e1fe7bcc2d010d1ffefc1 + rqrcode_core (1.2.0) sha256=cf4989dc82d24e2877984738c4ee569308625fed2a810960f1b02d68d0308d1a + ruflet (0.0.21) + ruflet_core (0.0.21) + ruflet_rails (0.0.16) + ruflet_server (0.0.21) + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 + +BUNDLED WITH + 4.0.11 diff --git a/packages/ruflet_rails/README.md b/packages/ruflet_rails/README.md index d6b4f4c8..85d05a62 100644 --- a/packages/ruflet_rails/README.md +++ b/packages/ruflet_rails/README.md @@ -60,7 +60,7 @@ bundle exec rake ruflet:web Mount the installed client and a developer-owned Ruflet entrypoint: ```ruby -mount Ruflet::Rails.web_app(app_file: Rails.root.join("app/views/ruflet/main.rb")), at: "/app" +mount Ruflet::Rails.web(app_file: Rails.root.join("app/views/ruflet/main.rb")), at: "/app" ``` The install generator adds the same mount at `/ruflet` when `--web` is used. @@ -120,11 +120,214 @@ bundle exec rake ruflet:install bundle exec rake ruflet:install[DEVICE_ID] ``` +## HTML as the UI DSL (no WebView) + +Beyond the WebView shell, Ruflet can treat HTML itself as the UI language: +Rails views describe screens with markup, and `Ruflet::Rails.html_to_native` +compiles each page into **real native Ruflet controls** — no WebView anywhere. +This is HTML-over-the-wire for native UI: state lives in Rails, every +interaction is a request, and the response markup re-renders the screen. + +```ruby +# app/views/ruflet/main.rb +Ruflet.run do |page| + Ruflet::Rails.html_to_native(page, start_url: "#{Ruflet::Rails.backend_url}/native") +end +``` + +Screens are ordinary Rails views (ERB, controllers, sessions, and redirects +all work — requests carry the Rails session cookie and CSRF token, plus an +`X-Ruflet-Native: 1` header so a controller can render native markup and a +browser page from the same action): + +```erb +<%# app/views/counters/show.html.erb %> + + + + <%= @count %> + + + + + Settings + +``` + +- **Layout** — ``, ``, ``, `
`/`
` (container), + ``, `
`, ``, ``, ``. +- **Content** — ``, `

`–`

`, `

`, ``, ``, ``, + `


`, `