diff --git a/.github/workflows/a11y.yml b/.github/workflows/a11y.yml index e7a0fa6c..6ffa33e7 100644 --- a/.github/workflows/a11y.yml +++ b/.github/workflows/a11y.yml @@ -43,6 +43,8 @@ jobs: run: npm ci --omit=dev - name: Set up Database run: bundle exec rails db:setup + - name: Build CSS + run: bundle exec rails dartsass:build - name: Create separate Chrome data directories run: | mkdir -p /tmp/chrome-data-rspec diff --git a/.github/workflows/cucumber.yml b/.github/workflows/cucumber.yml index 0bbd6d09..368a7e04 100644 --- a/.github/workflows/cucumber.yml +++ b/.github/workflows/cucumber.yml @@ -50,6 +50,8 @@ jobs: chrome-version: stable - name: Setup Database run: bundle exec rails db:setup + - name: Build CSS + run: bundle exec rails dartsass:build - name: Create Chrome data directory run: mkdir -p /tmp/chrome-data-cucumber - name: Run non-JavaScript Cucumber tests diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 1c1476e4..6b333e9f 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -47,6 +47,8 @@ jobs: - name: Setup Database run: | bundle exec rails db:test:prepare + - name: Build CSS + run: bundle exec rails dartsass:build - name: Create Chrome data directory run: mkdir -p /tmp/chrome-data-rspec - name: Run RSpec Tests diff --git a/.gitignore b/.gitignore index b8b4a5cd..6567000b 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,10 @@ node_modules/ /public/packs-test /public/assets +# Dart Sass build output (app/assets/builds/application.css) +/app/assets/builds/* +!/app/assets/builds/.keep + # Ignore yarn files /yarn-error.log yarn-debug.log* diff --git a/Gemfile b/Gemfile index 9808f109..d439468b 100644 --- a/Gemfile +++ b/Gemfile @@ -81,8 +81,9 @@ gem 'hypershield' gem 'good_job', '~> 4.19' #### Frontend related tools -# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem 'sprockets-rails' +# Rails 8's default asset pipeline: digests and serves what's on disk, and does +# no compiling of its own [https://github.com/rails/propshaft] +gem 'propshaft' # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] gem 'importmap-rails' @@ -91,19 +92,15 @@ gem 'importmap-rails' gem 'turbo-rails' gem 'bootstrap', '~> 5.3.8' -# Compiles the SCSS in app/assets/stylesheets with Dart Sass (the only Sass -# implementation still maintained) via Sprockets. Replaces sassc-rails, which -# was deprecated along with its LibSass backend. Everything else about the -# pipeline is unchanged: same stylesheet_link_tag, same manifest.js. -# -# `sassc` itself is still resolved as a transitive dependency (of both -# sassc-embedded and font-awesome-sass), but only as an API shim — LibSass no -# longer compiles anything. +# Compiles app/assets/stylesheets/application.scss to app/assets/builds with +# Dart Sass (the only Sass implementation still maintained), which Propshaft +# then digests and serves. Replaces sassc-rails, deprecated along with its +# LibSass backend. # # Bootstrap 5.3 still uses `@import`, which Dart Sass deprecated; those warnings # are silenced in config/initializers/dartsass.rb. Bootstrap 6 moves to `@use`, # at which point the silencing can go away. -gem 'dartsass-sprockets', '~> 3.2' +gem 'dartsass-rails', '~> 0.5' # Font Awesome for icons gem 'font-awesome-sass' diff --git a/Gemfile.lock b/Gemfile.lock index 29d77f77..42cd43b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,14 +85,10 @@ GEM uri (>= 0.13.1) addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) - annotaterb (4.24.0) + annotaterb (4.23.0) activerecord (>= 6.0.0) activesupport (>= 6.0.0) - anonymous_loader (0.1.3) - version_gem (~> 1.1, >= 1.1.14) ast (2.4.3) - auth-sanitizer (0.2.3) - version_gem (~> 1.1, >= 1.1.14) axe-core-api (4.12.0) dumb_delegator axe-core-cucumber (4.12.0) @@ -139,7 +135,7 @@ GEM bigdecimal rexml crass (1.0.7) - csv (3.3.6) + csv (3.3.5) cucumber (11.1.1) base64 (~> 0.2) builder (~> 3.2) @@ -169,12 +165,9 @@ GEM cucumber (>= 7, < 12) railties (>= 6.1, < 9) cucumber-tag-expressions (8.1.0) - dartsass-sprockets (3.2.1) - railties (>= 4.0.0) - sassc-embedded (~> 1.80.1) - sprockets (> 3.0) - sprockets-rails - tilt + dartsass-rails (0.5.1) + railties (>= 6.0.0) + sass-embedded (~> 1.63) database_cleaner-active_record (2.2.2) activerecord (>= 5.a) database_cleaner-core (~> 2.0) @@ -191,9 +184,9 @@ GEM railties (>= 6.1) drb (2.2.3) dumb_delegator (1.1.0) - erb (6.0.7) + erb (6.0.6) erubi (1.13.1) - et-orbi (1.4.1) + et-orbi (1.4.0) tzinfo factory_bot (6.5.6) activesupport (>= 6.1.0) @@ -254,7 +247,7 @@ GEM google-protobuf (4.35.1-x86_64-linux-gnu) bigdecimal rake (~> 13.3) - guard (2.20.2) + guard (2.20.1) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) logger (~> 1.6) @@ -273,7 +266,7 @@ GEM hashdiff (1.2.1) hashie (5.1.0) logger - http-cookie (1.1.6) + http-cookie (1.1.0) domain_name (~> 0.5) httparty (0.24.2) csv @@ -296,7 +289,7 @@ GEM jbuilder (2.15.1) actionview (>= 7.0.0) activesupport (>= 7.0.0) - json (2.21.2) + json (2.21.1) json_schemer (2.5.0) bigdecimal hana (~> 1.3) @@ -342,7 +335,7 @@ GEM net-smtp marcel (1.2.1) matrix (0.4.3) - mcp (0.25.0) + mcp (0.22.0) json_schemer (>= 2.4) memoist3 (1.0.0) method_source (1.1.0) @@ -351,9 +344,9 @@ GEM minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) - msgpack (1.8.4) + msgpack (1.8.3) multi_test (1.1.0) - multi_xml (0.9.1) + multi_xml (0.8.1) bigdecimal (>= 3.1, < 5) nenv (0.3.0) net-http (0.9.1) @@ -384,16 +377,14 @@ GEM notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - oauth2 (2.0.25) - anonymous_loader (~> 0.1, >= 0.1.3) - auth-sanitizer (~> 0.2, >= 0.2.3) + oauth2 (2.0.18) faraday (>= 0.17.3, < 4.0) jwt (>= 1.0, < 4.0) logger (~> 1.2) multi_xml (~> 0.5) rack (>= 1.2, < 4) - snaky_hash (~> 2.0, >= 2.0.7) - version_gem (~> 1.1, >= 1.1.14) + snaky_hash (~> 2.0, >= 2.0.3) + version_gem (~> 1.1, >= 1.1.9) omniauth (2.1.4) hashie (>= 3.4.6) logger @@ -410,7 +401,7 @@ GEM omniauth (~> 2.0) ostruct (0.6.3) parallel (2.1.0) - parser (3.3.12.0) + parser (3.3.11.1) ast (~> 2.4.1) racc pg (1.6.3) @@ -423,6 +414,10 @@ GEM prettyprint prettyprint (0.2.0) prism (1.9.0) + propshaft (1.3.2) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack pry (0.16.0) coderay (~> 1.1) method_source (~> 1.0) @@ -486,7 +481,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rbs (4.1.2) + rbs (4.1.0) logger prism (>= 1.6.0) tsort @@ -574,8 +569,6 @@ GEM google-protobuf (~> 4.31) sassc (2.4.0) ffi (~> 1.9) - sassc-embedded (1.80.9) - sass-embedded (~> 1.80) securerandom (0.4.1) selenium-webdriver (4.46.0) base64 (~> 0.2) @@ -595,17 +588,9 @@ GEM activesupport (>= 7.2) simplecov (1.0.3) simpleidn (0.2.3) - snaky_hash (2.0.7) + snaky_hash (2.0.3) hashie (>= 0.1.0, < 6) - version_gem (~> 1.1, >= 1.1.14) - sprockets (4.2.2) - concurrent-ruby (~> 1.0) - logger - rack (>= 2.2.4, < 4) - sprockets-rails (3.5.2) - actionpack (>= 6.1) - activesupport (>= 6.1) - sprockets (>= 3.0.0) + version_gem (>= 1.1.8, < 3) stimulus-rails (1.3.4) railties (>= 6.0.0) strong_migrations (2.8.0) @@ -618,7 +603,6 @@ GEM memoist3 (~> 1.0.0) win32ole thor (1.5.0) - tilt (2.8.0) timecop (0.9.11) timeout (0.6.1) tsort (0.2.0) @@ -627,14 +611,14 @@ GEM railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2026.3) + tzinfo-data (1.2026.1) tzinfo (>= 1.0.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) uri (1.1.1) useragent (0.16.11) - version_gem (1.1.15) + version_gem (1.1.9) web-console (4.3.0) actionview (>= 8.0.0) bindex (>= 0.4.0) @@ -651,7 +635,7 @@ GEM win32ole (1.9.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.8.3) + zeitwerk (2.8.2) PLATFORMS aarch64-linux @@ -674,7 +658,7 @@ DEPENDENCIES capybara-screenshot codeclimate-test-reporter cucumber-rails - dartsass-sprockets (~> 3.2) + dartsass-rails (~> 0.5) database_cleaner-active_record debug dotenv-rails @@ -699,6 +683,7 @@ DEPENDENCIES omniauth-rails_csrf_protection ostruct pg + propshaft puma (>= 6.0) rack_session_access rails (~> 8.1.3) @@ -714,7 +699,6 @@ DEPENDENCIES sentry-ruby shoulda-matchers (~> 8.0) simplecov (~> 1.0.3) - sprockets-rails stimulus-rails strong_migrations timecop diff --git a/Procfile.dev b/Procfile.dev index 9c5377e0..4f961872 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1 +1,2 @@ web: bundle exec rails s -p ${PORT:-3000} +css: bundle exec rails dartsass:watch diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js deleted file mode 100644 index ad05f0d7..00000000 --- a/app/assets/config/manifest.js +++ /dev/null @@ -1,9 +0,0 @@ -//= link_tree ../images -// Only the manifest stylesheet is compiled standalone. Do NOT link the whole -// directory: Sprockets would also compile the _*.scss partials on their own, -// where the variables from application.scss are not defined. -//= link application.css -//= link_tree ../../javascript .js -//= link_tree ../../../vendor/javascript .js - -//= link stimulus-loading.js diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 5b38bc4b..ffceaace 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -112,6 +112,13 @@ $escaped-characters: ( (")", "%29"), ); +// font-awesome-sass emits `font-path(...)` when Sprockets' Sass helpers are +// available and falls back to `#{$fa-font-path}/#{$file}` otherwise. Dart Sass +// runs as a plain CLI here, so the fallback is what we get — point it at the +// logical path of the gem's webfonts (its assets/fonts directory is on the +// Propshaft load path) and let Propshaft rewrite the url() to a digested one. +$fa-font-path: "font-awesome"; + @import "bootstrap"; // DataTables' Bootstrap 5 theme, imported straight out of node_modules (see diff --git a/bin/setup b/bin/setup index e5a15a6e..817e65bf 100755 --- a/bin/setup +++ b/bin/setup @@ -29,6 +29,12 @@ FileUtils.chdir APP_ROOT do puts "\n== Preparing database ==" system! "bin/rails db:prepare" + # application.css is a build artifact now, so a fresh checkout has none and + # the first page render would raise Propshaft::MissingAssetError. `make dev` + # keeps it up to date after this via the css process in Procfile.dev. + puts "\n== Building CSS ==" + system! "bin/rails dartsass:build" + puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" diff --git a/config/environments/development.rb b/config/environments/development.rb index 1db08070..cd591f66 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -68,14 +68,7 @@ # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true - # Suppress logger output for asset requests. - config.assets.quiet = false - config.assets.debug = true - # TODO: WARNING -- As of April 2026 / Rails 7.2 / sprockets + importmaps - # This must be set to true or JS does not seem to load in development. - config.assets.digest = true config.cache_classes = false - config.assets.compile = true # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true diff --git a/config/environments/production.rb b/config/environments/production.rb index 50628b54..b8cc70ff 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -23,12 +23,6 @@ # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. # config.public_file_server.enabled = false - # Compress CSS using a preprocessor. - # config.assets.css_compressor = :sass - - # Do not fall back to assets pipeline if a precompiled asset is missed. - config.assets.compile = false - # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index be3a10c5..f47ac388 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,14 +1,12 @@ # Be sure to restart your server when you modify this file. -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = "1.0" - -# Add additional assets to the asset load path. -# Rails.application.config.assets.paths << Emoji.images_path - -Rails.application.config.assets.precompile += %w(bootstrap.min.js popper.js) - -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in the app/assets -# folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) +# Propshaft serves everything it finds on the load path and digests it by +# content, so there is no precompile list to maintain — that was a Sprockets +# concept, as was the manifest.js this app used to carry. +# +# Rails adds every app/assets/* subdirectory to the load path automatically, so +# app/assets/builds (where Dart Sass writes application.css) needs no config. + +# The Sass sources are inputs to the build, not assets to serve; excluding them +# keeps unbuilt .scss out of public/assets. +Rails.application.config.assets.excluded_paths << Rails.root.join('app/assets/stylesheets') diff --git a/config/initializers/dartsass.rb b/config/initializers/dartsass.rb index 62cc8459..b25306b5 100644 --- a/config/initializers/dartsass.rb +++ b/config/initializers/dartsass.rb @@ -1,28 +1,48 @@ # Be sure to restart your server when you modify this file. -# Configuration for dartsass-sprockets, which compiles app/assets/stylesheets -# with Dart Sass. It keeps sassc-rails' `config.sass` namespace, so the options -# below are the ones sassc-rails understood plus Dart Sass' deprecation -# controls. - -# Don't report deprecations raised inside gem-provided stylesheets (Bootstrap, -# Font Awesome). We can't fix those, and Bootstrap 5.3 alone emits thousands of -# lines of them per compile. -Rails.application.config.sass.quiet_deps = true - -# `quiet_deps` only covers files loaded from the load path, so the `@import` -# lines in application.scss itself still warn. Silence that one deprecation -# everywhere; every other deprecation in our own stylesheets stays visible. +# dartsass-rails shells out to the Dart Sass CLI, so its configuration is a +# list of command-line flags rather than a Ruby options hash. # -# Drop this once Bootstrap 6 ships and the `@import`s can become `@use`. -Rails.application.config.sass.silence_deprecations = [ 'import' ] +# --quiet-deps drops deprecations raised inside gem-provided stylesheets +# (Bootstrap, Font Awesome); we can't fix those, and Bootstrap 5.3 alone emits +# thousands of lines of them per build. --silence-deprecation=import covers the +# `@import` lines in application.scss itself, which --quiet-deps does not reach. +# +# Drop the silencing once Bootstrap 6 ships and the `@import`s can become +# `@use`. +Rails.application.config.dartsass.build_options += [ + '--quiet-deps', + '--silence-deprecation=import' +] -# DataTables ships its Bootstrap 5 theme as plain CSS on npm. Put the two -# package directories on the Sass load path so application.scss can @import -# the stylesheets by name and Dart Sass inlines them into application.css, -# rather than the app carrying hand-maintained copies that drift from the -# JavaScript pinned in config/importmap.rb. -Rails.application.config.sass.load_paths += [ - Rails.root.join('node_modules/datatables.net-bs5/css').to_s, - Rails.root.join('node_modules/datatables.net-responsive-bs5/css').to_s +# The gem's default is --style=compressed --no-source-map for every +# environment. Sprockets only compressed outside development, so keep the +# built CSS readable and mapped back to the .scss sources while developing. +if Rails.env.development? + Rails.application.config.dartsass.build_options -= [ '--style=compressed', '--no-source-map' ] + Rails.application.config.dartsass.build_options += [ '--style=expanded', '--embed-sources' ] +end + +# DataTables ships its Bootstrap 5 theme as plain CSS on npm, so application.scss +# imports it out of node_modules. Nothing puts those directories on the Sass +# load path for us. +Rails.application.config.dartsass.build_options += [ + '--load-path', Rails.root.join('node_modules/datatables.net-bs5/css').to_s, + '--load-path', Rails.root.join('node_modules/datatables.net-responsive-bs5/css').to_s ] + +# The bootstrap and font-awesome-sass gems put their Sass sources on the asset +# load path so application.scss can @import them. Propshaft has one load path +# and serves everything on it, so leaving them there digests ~110 .scss +# partials into public/assets. dartsass-rails builds its --load-path list from +# config.assets.paths, so hand Dart Sass the directories directly and then take +# them off Propshaft's path. (font-awesome-sass' assets/fonts directory has to +# stay — that one holds the webfonts the built CSS points at.) +sass_gem_paths = %w[bootstrap font-awesome-sass].filter_map do |name| + spec = Gem.loaded_specs[name] + File.join(spec.gem_dir, 'assets', 'stylesheets') if spec +end + +Rails.application.config.dartsass.build_options += + sass_gem_paths.flat_map { |path| [ '--load-path', path ] } +Rails.application.config.assets.excluded_paths += sass_gem_paths