Skip to content

Rails 8.0 / Ruby 3.4 compatibility (stacked on #460) - #462

Open
princejoseph wants to merge 22 commits into
hyperstack-org:edgefrom
princejoseph:rails-8-compatibility
Open

Rails 8.0 / Ruby 3.4 compatibility (stacked on #460)#462
princejoseph wants to merge 22 commits into
hyperstack-org:edgefrom
princejoseph:rails-8-compatibility

Conversation

@princejoseph

@princejoseph princejoseph commented Aug 19, 2026

Copy link
Copy Markdown

Extends #460 (Rails 7 / Ruby 3.2) to Rails 8.0 and Ruby 3.4. This branch includes all of #460's commits plus two new ones on top — merging this PR alone brings in both Rails 7 and Rails 8 support (and supersedes #460); alternatively I'm happy to rebase to just the two new commits once #460 lands.

What's in the two new commits

Gemspec constraints

  • rails < 8.0< 8.1 across all sub-gems
  • opal >= 0.11, < 1.1>= 1.7, < 2.0 in hyperstack-config and rails-hyperstack. Rails 8 requires opal-rails ≥ 3 by constraint, but opal-rails 3.x dropped the Sprockets pipeline that hyperstack-loader depends on. opal-rails 2.0.4 works unmodified on Rails 8 — only its gemspec caps rails at < 7.3 — so apps use a one-line-patched opal-rails (princejoseph/opal-rails@rails-8) and opal 1.8.3 satisfies both.

Ruby 3.4 / Opal 1.8 fixes

  • Removed remaining .untaint calls (no-op on ≤ 3.1, removed in Ruby 3.2+): hyper-component, hyper-model, hyper-operation, hyper-router, hyper-store entry files.
  • Internal::Component::Tags.included called define_method(nil) for top-level (non-namespaced) components — Opal ≤ 1.1 silently ignored it, Opal 1.8 raises TypeError: nil is not a symbol nor a string, which broke loading of every top-level component. Now guarded.

Dev-mode policy reload fix (applies to Rails 7 too)

  • In development, code reloading wipes the regulations policies install on model singleton classes, and Hyperstack.reset_operations only const_gets the policy once at boot — so the first code edit after boot makes every ReactiveRecord fetch fail with AccessViolation: scoped_permission_not_granted. Hyperstack::Engine now re-references ApplicationPolicy / Hyperstack::ApplicationPolicy in config.to_prepare.

Validation

Test app on Rails 8.0.5 / Ruby 3.4.9 / Opal 1.8.3 / opal-rails 2.0.4 / opal-sprockets 1.0.4 / react-rails 2.7.1: HyperModel over ActionCable (lazy fetch, live broadcasts, cross-association updates), components importing MUI v5 via NativeLibrary, hyper-spec + Capybara system specs all passing, including a broadcast-after-reload regression check for the policy fix.

Host-app note: react-rails 2.7 needs connection_pool < 3 pinned (v3 changed the constructor API).

princejoseph and others added 20 commits February 27, 2026 22:21
- Update `rails` version constraint from `< 7.0` to `< 8.0` in all gemspecs
- Update `react-rails` constraint from `< 2.5.0` to `< 3.0` (2.7.x added Rails 7 support)
- Remove `.untaint` calls removed in Ruby 3.2 (hyperstack-config, hyper-state, hyper-trace, hyper-component)
- Guard `config.assets` access in rail_tie.rb for Propshaft compatibility
- Guard `ActiveSupport::Dependencies.require_or_load` alias in server_side_auto_require.rb (removed in Rails 7.2)
- Loosen `sqlite3`, `timecop`, and `rspec` dev dependency constraints to allow modern versions
- Add `gem 'rack', '< 3'` to Gemfiles (puma 5.x requires rack < 3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hyper-operation

- Add **kwargs to has_many/belongs_to/composed_of wrapper methods in active_record_base.rb
  and permissions.rb so options are forwarded as keyword args (not positional hashes),
  fixing 'undefined method arity for Hash' errors in Rails 7.2
- Guard InternalMetadata.do_not_synchronize with respond_to? check since Rails 7.1+
  no longer makes InternalMetadata inherit from ActiveRecord::Base
- Add coder: YAML to serialize :data in QueuedMessage (required since Rails 7.1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Components are Opal/client-side code compiled by Sprockets and should
never be eager-loaded or autoloaded server-side. Without this, apps with
eager_load=true (e.g. in CI) fail with NameError because Zeitwerk
alphabetically loads component files before HyperComponent is defined.

The fix belongs in the railtie so all Hyperstack apps get it
automatically rather than requiring a manual workaround in each app's
initializer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`create_table(force: :cascade)` was passing the options hash as a
positional argument via *args, which Ruby 3 no longer allows.
Switch to **kwargs so the hash is forwarded as keyword arguments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…crets removed in Rails 7.2)

Rails.application.secrets was deprecated in Rails 7.1 and removed in 7.2,
causing a NoMethodError on every ActionCable auth request → 401 Unauthorized.
Use Rails.application.secret_key_base which works across all Rails 7.x versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Ruby 3, bare `key: val` syntax is always treated as keyword arguments.
`ActionCable::Server::Broadcasting#broadcast` expects its second argument
(`message`) as a positional parameter, not a keyword. The call:

  ActionCable.server.broadcast(channel, message: x, data: y)

silently raised `ArgumentError: wrong number of arguments (given 1, expected 2)`
in Ruby 3, which was rescued by the controller — causing all ActionCable
broadcasts to silently fail (no real-time updates to other connected clients).

Fix: wrap the hash in explicit braces so Ruby passes it as a positional arg.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add .github/workflows/ci.yml running hyperstack-config and hyper-state
  specs on Ruby 3.1 with headless Chrome via browser-actions/setup-chrome
- Add 'github' DRIVER mode to hyper-spec using CHROMEWEBDRIVER env var
- Remove chromedriver-helper (removed from rubygems) from all gemspecs
- Remove webdrivers (deprecated; Selenium Manager replaces it) from
  hyper-spec.gemspec and hyper-spec.rb
- Pin selenium-webdriver >= 4.11 (ships with Selenium Manager)
- Update Gemfile.locks for hyperstack-config and hyper-state to
  selenium-webdriver 4.32.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove manual driver_path in github driver; Selenium Manager
  (selenium-webdriver >= 4.11) finds chromedriver automatically
- Drop setup-chrome from workflow; Chrome is pre-installed on ubuntu-latest
- Remove `private` from Timecop monkey patch in time_cop.rb — the patch
  accidentally made travel/unmock! private, causing NoMethodError on
  Ruby 3.x where explicit-receiver calls to private methods are forbidden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
timecop 0.9.x replaced @_stack/@baseline instance variables with
stack/set_stack/baseline/set_baseline accessor methods (for thread-safe
support). Update the Lolex monkey patch to use the new API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…operation

Add five more gems to GitHub Actions CI:
- hyper-component (36 specs), hyper-store (5), hyper-router (1): SQLite,
  added to the existing matrix job — no service needed
- hyper-operation (14 specs): separate job with MySQL 8.0 service;
  database.yml updated to read credentials from DB_HOST/DB_USER/DB_PASSWORD
  env vars so it works both locally and in CI

Also add rack < 3 constraint to all four Gemfiles (puma <= 5.4 compat).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Comment out mini_racer in hyper-component and hyper-router gemspecs:
  mini_racer 0.3.x (constrained < 0.4.0) fails to compile native
  extensions on Ubuntu 24.04. Node.js is available on CI runners
  and serves as the ExecJS runtime instead.

- Add Redis 7 service to hyper-operation CI job:
  The spec_helper flushes Redis before each test (even when using
  active_record adapter), and all transport tests (Pusher-Fake,
  Action Cable, Simple Polling) fail with Redis::CannotConnectError.
  Adding Redis fixes ~47 of the 79 hyper-operation failures.

- Fix $to_json serialization in hyper-spec client_execution.rb:
  Replace Opal-specific window.hyper_spec_promise_result.$to_json()
  with native JSON.stringify(). The Opal $to_json method on Array
  prototype is not available in certain test contexts (Chrome 145
  / newer environments), causing ~26 browser tests to fail.
  JSON.stringify is always available and produces equivalent output
  for the primitive values tested (integers, strings, etc).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ration timeout

- hyper-router/spec/spec_helper.rb: Guard MiniRacer backup with `defined?`
  check so specs don't crash when mini_racer gem is not installed
- hyper-component/spec/spec_helper.rb: Same MiniRacer guard + change
  `fixture_path=` to `fixture_paths=` for rspec-rails 7.x compatibility
- hyper-spec/client_execution.rb: Use hybrid JS serializer — prefers Opal's
  `$to_json` when available (handles class objects), falls back to
  `JSON.stringify` for native arrays when Opal JSON module isn't loaded
- ci.yml: Add `timeout-minutes: 30` and Opal/Sprockets asset caching to
  hyper-operation job to prevent hangs and speed up subsequent runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- hyper-component.gemspec, hyper-router.gemspec: Re-enable mini_racer
  with >= 0.6.0 constraint. 0.6+ uses libv8-node and compiles fine on
  Ubuntu 24.04 (0.3.x with libv8 did not). Needed for prerendering_on
  (server-side rendering) specs.
- hyper-spec.gemspec: Pin unparser to < 0.6.4. Version 0.6.4 added
  Ruby 3.1 syntax support and emits shorthand hash syntax
  ({ controller: } instead of { controller: controller }) that Opal
  cannot parse, causing many "could not compile" errors in client specs.
  Also updated its own mini_racer constraint to >= 0.6.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0.20.0 (latest) fails with NoMethodError in extconf.rb on Ruby 3.1.
0.9.x explicitly supports Ruby >= 3.0 and compiles cleanly on Ubuntu 24.04.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mini_racer fails to compile on Ubuntu 24.04 (GitHub Actions) across all
tested versions (0.9.0, 0.20.0) due to a NoMethodError in extconf.rb.
Node.js is used as the ExecJS fallback for non-SSR tests.

For tests tagged :prerendering_on, replace the silent fallback (which ran
and failed) with an explicit skip so CI passes cleanly. These tests still
run locally when mini_racer is installed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tness

Opal 1.8.2 generates `(e = $err)\ntry { ... }` for begin/rescue in
expression position. The previous `.delete("\n")` stripped all newlines,
making this `(e = $err) try { ... }` — invalid JS since ASI rule hyperstack-org#4
requires a newline before `try`. Chrome 145 rejects this with
"Unexpected token 'try'". Removing `.delete("\n")` fixes 2 NativeLibrary
tests and likely the 11 prop-type validation tests whose validators also
contain rescue blocks.

Also:
- Update componentStack assertion to be format-agnostic (React 18 changed
  from "in ComponentName" to webpack source map format)
- Add goog:loggingPrefs to chrome_headless_github_actions driver so
  browser console logs are captured for prop-type warning assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
React 18 changed prop-type warnings from:
  console.error("Warning: Failed prop type: " + message)
to format-string style:
  console.error("Warning: Failed %s type: %s%s", "prop", message, stack)

Chrome captures the format string literally, so the old regex
/Warning: Failed prop( type|Type): In component.../ no longer matches.
Update assertions to match the error message content directly instead
of the React-version-specific prefix format.

Also change goog:loggingPrefs from 'ALL' to 'SEVERE' to avoid capturing
noisy console.log initialization messages (requires, React DevTools prompt)
which would cause the 'no spurious warnings' test to fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'SEVERE' only captured console.error, missing console.warn calls.
The 'did you mean to say Foo()' message uses console.warn (WARNING level).
'WARNING' captures WARNING + SEVERE but excludes INFO (console.log),
so initialization spam that broke the 'no spurious warnings' test is
still filtered out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- remove remaining .untaint calls (method removed in Ruby 3.2)
- tags.rb: skip tag-method installation for top-level components —
  Opal 1.8 raises TypeError on define_method(nil) where 1.x ignored it
- engine.rb: re-reference application policies on every to_prepare so
  dev-mode code reloads don't silently drop model regulations
  (symptom: every fetch 403s with scoped_permission_not_granted
  after the first code edit)
@princejoseph
princejoseph marked this pull request as ready for review August 19, 2026 14:03
On Ruby 3 / Rails 7+, passing the options Hash positionally lands it in
ActiveRecord's scope parameter and crashes with 'undefined method arity
for an instance of Hash'. Accepts a legacy positional opts hash too.
- public_columns_hash: resolve model constants by name instead of blindly
  requiring the public-directory files. Lets a host app keep client-only
  mirror files in app/hyperstack/models (Zeitwerk-ignored) while the real
  models live in app/models — requiring the mirror server-side reopened
  the class with a mismatched superclass.
- column_types: never let a failed type conversion abort a fetch merge.
  Attribute readers can return values that don't match the column type
  (state machines, enum overrides) — keep the server's raw value.
- imports: stub Inflections#acronym before inlining the host app's
  inflections.rb (opal-activesupport doesn't implement it; a host app
  using acronym crashed the client bundle at boot).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant