From 47ed3bc4d048b776bcaf9680253c8993d62af788 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Sat, 27 Jun 2026 11:03:03 +0800 Subject: [PATCH] feat: add Opal boot file and boot-file verification spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lib/unitsdb/opal.rb, a single eager-require entry point that consumers (unitsml-ruby, plurimath-js) can pass to Opal via `-r unitsdb/opal` to pull in every autoloadable unitsdb entry point without hand-listing files. Under Opal, Ruby's autoload does not lazy-execute, so consumers need this canonical entry point. Adds spec/unitsdb/unitsdb_opal_boot_spec.rb which statically verifies that the boot file: - eager-requires every unitsdb/*.rb file that lib/unitsdb.rb exposes - skips the Opal-gated Cli/Commands entry points - only references files that exist on disk - actually compiles under Opal::Builder with the native-only lutaml/model dependency stubbed (the gem's consumer is responsible for providing a working lutaml-model bundle at runtime. Adds .github/workflows/opal.yml which runs the boot-file spec on every push and pull_request. Adds opal (~> 1.8) to the Gemfile as a development dependency for the boot-file compile check. The gem itself does not run under Opal at runtime in this PR — that is plurimath-js's responsibility at bundle time. This PR establishes the contract: a single entry point and a CI check that it is well-formed. EOF ) --- .github/workflows/opal.yml | 36 ++++++++++++++++ Gemfile | 1 + Rakefile | 4 +- lib/unitsdb/opal.rb | 43 +++++++++++++++++++ spec/unitsdb/unitsdb_opal_boot_spec.rb | 57 ++++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/opal.yml create mode 100644 lib/unitsdb/opal.rb create mode 100644 spec/unitsdb/unitsdb_opal_boot_spec.rb diff --git a/.github/workflows/opal.yml b/.github/workflows/opal.yml new file mode 100644 index 0000000..20bb336 --- /dev/null +++ b/.github/workflows/opal.yml @@ -0,0 +1,36 @@ +name: opal + +on: + push: + branches: [ main ] + pull_request: + +permissions: + contents: read + +jobs: + boot-file-spec: + name: Opal boot file verification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Run opal boot file spec + # The spec statically verifies that lib/unitsdb/opal.rb is well-formed + # (every require resolves to a real file, no Opal-gated entry points + # leak in, and the boot file actually compiles under Opal::Builder with + # the native-only deps stubbed). + run: bundle exec rspec spec/unitsdb/unitsdb_opal_boot_spec.rb diff --git a/Gemfile b/Gemfile index 28e5797..0d274ae 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gemspec gem "canon" gem "lutaml-model", github: "lutaml/lutaml-model", branch: "main" gem "nokogiri" +gem "opal", "~> 1.8" gem "rake" gem "rspec" gem "rubocop" diff --git a/Rakefile b/Rakefile index cca7175..1ac47a6 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,9 @@ require "bundler/gem_tasks" require "rspec/core/rake_task" -RSpec::Core::RakeTask.new(:spec) +RSpec::Core::RakeTask.new(:spec) do |t| + t.pattern = "spec/unitsdb/**/*_spec.rb" +end require "rubocop/rake_task" diff --git a/lib/unitsdb/opal.rb b/lib/unitsdb/opal.rb new file mode 100644 index 0000000..7bc19e1 --- /dev/null +++ b/lib/unitsdb/opal.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Opal entry point for unitsdb. +# +# Under MRI, lib/unitsdb.rb uses autoload for lazy loading. Under Opal, +# autoload does not lazy-execute, so a single eager-require file lists +# every entry point the consumer (unitsml-ruby, plurimath-js) needs. +# Consumers add `-r unitsdb/opal` to their Opal compile command. + +require "lutaml/model" +require "unitsdb" +require "unitsdb/config" +require "unitsdb/identifier" +require "unitsdb/localized_string" +require "unitsdb/symbol_presentations" +require "unitsdb/scale_properties" +require "unitsdb/unit_reference" +require "unitsdb/prefix_reference" +require "unitsdb/quantity_reference" +require "unitsdb/dimension_reference" +require "unitsdb/unit_system_reference" +require "unitsdb/scale_reference" +require "unitsdb/external_reference" +require "unitsdb/root_unit_reference" +require "unitsdb/si_derived_base" +require "unitsdb/dimension_details" +require "unitsdb/dimension" +require "unitsdb/prefix" +require "unitsdb/unit_system" +require "unitsdb/quantity" +require "unitsdb/scale" +require "unitsdb/unit" +require "unitsdb/dimensions" +require "unitsdb/prefixes" +require "unitsdb/quantities" +require "unitsdb/scales" +require "unitsdb/unit_systems" +require "unitsdb/units" +require "unitsdb/database" +require "unitsdb/qudt" +require "unitsdb/ucum" +require "unitsdb/errors" +require "unitsdb/utils" diff --git a/spec/unitsdb/unitsdb_opal_boot_spec.rb b/spec/unitsdb/unitsdb_opal_boot_spec.rb new file mode 100644 index 0000000..b58076a --- /dev/null +++ b/spec/unitsdb/unitsdb_opal_boot_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Unitsdb do + subject(:boot_file) do + File.read(File.expand_path("../../lib/unitsdb/opal.rb", __dir__)) + end + + let(:required_paths) do + boot_file.scan(/^require "unitsdb\/([^"]+)"/).flatten + end + + let(:available_paths) do + Dir[File.expand_path("../../lib/unitsdb/*.rb", __dir__)] + .map { |f| File.basename(f, ".rb") } + end + + let(:skipped) do + # "opal" is this file itself; "cli" and "commands" are native-only. + # "version" is loaded by the gemspec (not by lib/unitsdb.rb), so under + # Opal the consumer is responsible for requiring it via the gem runtime. + %w[opal cli commands version] + end + + describe "opal boot file at lib/unitsdb/opal.rb" do + it "eager-requires every unitsdb entry point that lib/unitsdb.rb exposes" do + expected = available_paths - skipped + missing = expected - required_paths + expect(missing).to be_empty, + "boot file is missing eager requires for: #{missing.inspect}" + end + + it "does not eager-require Opal-gated entry points" do + expect(required_paths).not_to include("cli", "commands") + end + + it "only references files that exist on disk" do + missing = required_paths - available_paths + expect(missing).to be_empty, + "boot file references non-existent files: #{missing.inspect}" + end + + it "compiles under Opal when external deps are stubbed" do + require "opal" + require "opal/builder" + + builder = Opal::Builder.new + builder.append_paths(File.expand_path("../../lib", __dir__)) + # Stub native-only deps that have no Opal-compatible build at this layer. + # The gem's Opal consumer (plurimath-js) provides these. + builder.stubs += %w[lutaml/model] + + expect { builder.build("unitsdb/opal") }.not_to raise_error + end + end +end