Files
activeadmin-quill_editor/docs/propshaft-upgrade.md
Gleb Tv d7c3252c52 feat: Add ActiveAdmin 4 and Propshaft support
- Switch to ActiveAdmin 4 beta with Tailwind CSS
- Replace Sprockets with Propshaft for Rails 8
- Migrate from jQuery to vanilla JavaScript
- Implement proper module initialization without setTimeout hacks
- Add esbuild for JavaScript bundling with clean import aliases
- Configure Tailwind CSS build process with ActiveAdmin plugin
- Update engine configuration for both Propshaft and Sprockets
- Add comprehensive documentation for AA4 gem updates
- Maintain backward compatibility with legacy AA versions

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 21:14:49 +03:00

17 KiB

Upgrading from Sprockets to Propshaft

Propshaft has a smaller scope than Sprockets, therefore migrating to it will also require you to adopt the jsbundling-rails and cssbundling-rails gems. This guide will assume your project follows Rails 6.1 conventions of using webpacker to bundle javascript, sass-rails to bundle css and sprockets to digest assets. Finally, you will also need npx version 7.1.0 or later installed.

Propshaft depends on Rails 7, so you will need to upgrade to Rails 7+ before starting the migration.

1. Migrate from Webpacker to jsbundling-rails

Start by following these steps:

  1. Replace webpacker with jsbundling-rails in your Gemfile;
  2. Run ./bin/bundle install;
  3. Run ./bin/rails javascript:install:webpack;
  4. Remove the file config/initializers/assets.rb;
  5. Remove the file bin/webpack;
  6. Remove the file bin/webpack-dev-server;
  7. Remove the folder config/webpack (note: any custom configuration should be migrated to the new webpack.config.js file);
  8. Remove the file config/webpacker.yml;
  9. Replace all instances of javascript_pack_tag with javascript_include_tag and add defer: true to them.

After you are done you will notice that the install step added various files to your project and updated some of the existing ones.

The new 'bin/dev' and 'Procfile.dev' files

The ./bin/dev file is a shell script that uses foreman and Procfile.dev to start two processes in a single terminal: rails s and yarn build. The latter replaces webpack-dev-server for bundling and watching for changes in javascript files.

The 'build' attribute added to package.json

This is the command that yarn build will use to bundle javascript files.

The new 'webpack.config.js' file

In webpacker this file was hidden inside the gem, but now you can edit it directly. If you had custom configuration in config/webpack you can move them to here. Projects with multiple entrypoints will need to adjust the entry attribute:

module.exports = {
  entry: {
    application: "./app/javascript/application.js",
    admin: "./app/javascript/admin.js"
  }
}

The 'link_tree' directive added to 'app/assets/manifest.js'

This tells Sprockets to include the files in app/assets/builds during assets:precompile. This is the folder where yarn build will place the bundled files, so make sure you commit it to the repository and don't delete it when cleaning assets.

What about babel?

If you would like to continue using babel for transpiling, you will need to configure it manually. First, open webpack.config.js and add this:

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      }
    ]
  }
}

Then open package.json and add this:

"babel": {
  "presets": [
    "./webpack.babel.js"
  ]
}

Finally, download webpackers babel preset file and place it in the same directory as package.json with the name webpack.babel.js.

Module resolution

Webpacker included the source_path (default: app/javascript/) into module resolution, so a statement like import 'channels' imported app/javascript/channels/. After migrating to jsbundling-rails this is no longer the case. You will need to update your webpack.config.js to include the following if you wish to maintain that behavior:

module.exports = {
  // ...
  resolve: {
    modules: ["app/javascript", "node_modules"],
  },
  //...
}

Alternatively, you can change modules to use relative imports, for example:

- import 'channels'
+ import './channels'

Extracting Sass/SCSS from JavaScript

In webpacker it is possible to extract Sass/SCSS from JavaScript by enabling extract_css in webpacker.yml. This allows for including those source files in JavaScript, e.g. import '../scss/application.scss

If you wish to keep this functionality follow these steps:

  1. Run yarn add mini-css-extract-plugin sass sass-loader css-loader;
  2. Update your webpack.config.js to require mini-css-extract-plugin and configure the loaders (see example below).

Example webpack.config.js:

const path    = require("path")
const webpack = require("webpack")
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.exports = {
  mode: "production",
  devtool: "source-map",
  entry: {
    application: "./app/javascript/application.js"
  },
  resolve: {
    modules: ["app/javascript", "node_modules"],
  },
  output: {
    filename: "[name].js",
    sourceMapFilename: "[file].map",
    path: path.resolve(__dirname, "app/assets/builds"),
  },
  plugins: [
    new MiniCssExtractPlugin(),
    new webpack.optimize.LimitChunkCountPlugin({
      maxChunks: 1
    })
  ],
  module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
      },
    ],
  },
}

2. Migrate from sass-rails to cssbundling-rails

Note: if your application used Webpacker's extract_css to build your CSS and did not require sass-rails, you can skip this section.

Start by following these steps:

  1. Add cssbundling-rails to your Gemfile;
  2. Run ./bin/bundle install;
  3. Run ./bin/rails css:install:sass.

After you are done you will notice that the install step updated some files.

The new process in 'Procfile.dev'

Just like the javascript process, this one will bundle and watch for changes in css files.

The 'build:css' attribute added to package.json

This is the command yarn build will use to bundle css files.

The 'link_tree' directive removed from 'app/assets/manifest.js'

Now that the CSS files will be placed into app/assets/build, Sprockets no longer needs to worry about the app/assets/stylesheets folder. If you have any other link_tree for css files, remove them too.

Configuring multiple entrypoints

Sprockets will only compile files in the root directories listed in manifest.js, but the sass package that yarn build uses will also check subfolders, which might cause compilation errors if your scss files are using features like @import and variables. This means that if you have multiple entry points in your app, you have some extra work ahead of you.

Let's assume you have the following structure in your app/asset/stylesheets folder:

stylesheets/admin.scss
stylesheets/admin/source_1.scss
stylesheets/admin/source_2.scss
stylesheets/application.scss
stylesheets/application/source_1.scss
stylesheets/application/source_2.scss

Start by your separating your entrypoints from your other files, and adjusting all @import for the new structure:

stylesheets/entrypoints/admin.scss
stylesheets/entrypoints/application.scss
stylesheets/sources/admin/source_1.scss
stylesheets/sources/admin/source_2.scss
stylesheets/sources/application/source_1.scss
stylesheets/sources/application/source_2.scss

Then adjust the build attribute in package.json:

"build:css": "sass ./app/assets/stylesheets/entrypoints:./app/assets/builds --no-source-map --load-path=node_modules"

Deprecation warnings

Sass might raise deprecation warnings depending on what features you are using (such as division), but the messages will explain how to fix them. If you are not sure, see more details in the official documentation.

3. Migrate from Sprockets to Propshaft

Start by following these steps:

  1. Remove sprockets, sprockets-rails, and sass-rails from the Gemfile and add propshaft;
  2. Run ./bin/bundle install;
  3. Check your Gemfile.lock, repeat steps 1 and 2 for gems that list sprockets or sprockets-rails as a dependency;
  4. Open config/application.rb and remove config.assets.paths << Rails.root.join('app','assets');
  5. Remove app/assets/config/manifest.js.
  6. Replace all asset_helpers (image_url, font_url) in css files with standard urls.
  7. If you are importing only the frameworks you need (instead of rails/all), remove require "sprockets/railtie";

Asset paths

Propshaft will automatically include in its search paths the folders vendor/assets, lib/assets and app/assets of your project and of all the gems in your Gemfile. You can see all included files by using the reveal rake task:

 rake assets:reveal

Asset helpers

Propshaft does not rely on asset_helpers (asset_path, asset_url, image_url, etc.) like Sprockets did. Instead, it will search for every url function in your css files, and adjust them to include the digest of the assets they reference.

Go through your css files, and make the necessary adjustments:

- background: image_url('hero.jpg');
+ background: url('/hero.jpg');

Notice that Propshaft's version starts with an / and Sprockets' version does not? That's because the latter uses absolute paths, and the former uses relative paths. To better illustrate that difference, let's assume you have the following structure:

assets/stylesheets/theme/main.scss
assets/images/hero.jpg

In Sprockets, main.scss can reference hero.jpg like this:

background: image_url('hero.jpg')

Using the same path with url in Propshaft will cause it to raise an error, saying it cannot locate theme/hero.jpg. That's because Propshaft assumes all paths are relative to the path of the file it's processing. Since it was processing a css file inside the theme folder, it will also look for hero.jpg in the same folder.

By adding a / at the start of the path we are telling Propshaft to consider this path as an absolute path. While this change in behavior increases the work a bit when upgrading, it makes external libraries like FontAwesome and Bootstrap themes work out-of-the-box.

Asset content

It's a common pattern in apps to inline small SVG files and low resolution versions of images that need to be displayed as quickly as possible. In Propshaft, the same line of code works for all environments:

Rails.application.assets.load_path.find('logo.svg').content

As Rails escapes html tags in views by default, in order to output a rendered svg you will need to specify rails not to escape the string using html_safe or raw.

Rails.application.assets.load_path.find('logo.svg').content.html_safe
raw Rails.application.assets.load_path.find('logo.svg').content

Precompilation in development

Propshaft uses a dynamic assets resolver in development mode. However, when you run assets:precompile locally Propshaft will then switch to a static assets resolver. Therefore, changes to assets will not be observed anymore and you will have to precompile the assets each time changes are made. This is different to Sprockets.

If you wish to have dynamic assets resolver enabled again, you need to clean your target folder (usually public/assets) and propshaft will start serving dynamic content from source. One way to do this is to run rails assets:clobber.

Another way to watch changes in your CSS & JS assets is by running bin/dev command instead of rails server that not only runs the server but also keeps looking for any changes in the assets and once it detects any changes, it compiles them while the server is running. This is possible because of the Procfile.dev.

4. Specific Configuration for Test Environments

Understanding Propshaft's Test Mode

Propshaft automatically configures itself for test environments with these defaults:

# Automatically enabled in test environment
config.assets.server = Rails.env.test?  # true for test environment
config.assets.sweep_cache = false       # Disabled for faster tests

Test Environment Configuration

# config/environments/test.rb
Rails.application.configure do
  # Asset server is automatically enabled - no precompilation needed
  config.assets.server = true
  
  # Optional: Add test-specific asset paths
  config.assets.paths << Rails.root.join('spec/fixtures/assets')
  
  # Optional: Disable SRI for faster test execution
  config.assets.integrity_hash_algorithm = nil
  
  # Optional: Customize asset prefix for isolated testing
  # config.assets.prefix = '/test-assets'
  
  # Performance: Use faster file watcher (if using listen gem)
  config.file_watcher = ActiveSupport::EventedFileUpdateChecker
end

Testing Asset Integration

RSpec Configuration

# spec/rails_helper.rb
RSpec.configure do |config|
  # Ensure assets are available in feature specs
  config.before(:suite) do
    # Warm up asset cache for faster test execution
    Rails.application.assets.load_path.assets
  end
  
  # Clean up assets between tests if needed
  config.after(:each) do
    # Only if you modify asset paths during tests
    # Rails.application.assets.load_path.clear_cache
  end
end

Testing Asset Helpers

# spec/helpers/application_helper_spec.rb
RSpec.describe ApplicationHelper, type: :helper do
  describe "asset helpers" do
    it "resolves asset paths correctly" do
      expect(helper.asset_path('application.js')).to match(%r{^/assets/application-\w+\.js$})
    end
    
    it "includes integrity hashes when configured" do
      allow(Rails.application.config.assets).to receive(:integrity_hash_algorithm).and_return('sha384')
      result = helper.javascript_include_tag('application', integrity: true)
      expect(result).to include('integrity="sha384-')
    end
  end
end

Feature Spec Asset Testing

# spec/features/assets_spec.rb
RSpec.describe "Asset loading", type: :feature do
  it "serves JavaScript assets correctly" do
    visit root_path
    expect(page).to have_css('script[src*="/assets/application-"]')
  end
  
  it "serves CSS assets correctly" do
    visit root_path  
    expect(page).to have_css('link[href*="/assets/application-"][rel="stylesheet"]')
  end
end

Common Test Environment Issues and Solutions

Issue 1: Assets Not Found in Tests

Symptom: Propshaft::MissingAssetError in test environment Solution:

# Ensure asset server is enabled in test.rb
config.assets.server = true

# Check that build artifacts exist
# For jsbundling-rails/cssbundling-rails:
bundle exec rake assets:precompile  # If assets need building

Issue 2: Slow Test Startup

Symptom: Tests take long to start due to asset discovery Solution:

# config/environments/test.rb
# Disable sweep_cache (should be default)
config.assets.sweep_cache = false

# Exclude unnecessary paths
config.assets.excluded_paths += [
  Rails.root.join("app/assets/stylesheets"),  # If using cssbundling
  Rails.root.join("app/javascript")           # If using jsbundling
]

Issue 3: Inconsistent Asset Paths Between Environments

Symptom: Tests pass but development/production fails with asset references Solution:

# Use consistent asset path helpers across environments
# In views, always use:
<%= asset_path('image.png') %>        # Good
# Instead of:
"/assets/image.png"                   # Bad - won't work with digests

Performance Optimization for Tests

Precompile Once Strategy

For CI/CD environments where you can precompile once:

# In CI setup
bundle exec rake assets:precompile
RAILS_ENV=test bundle exec rspec

Asset Path Caching

# config/initializers/assets.rb (test environment)
if Rails.env.test?
  # Warm asset cache on initialization to avoid repeated discovery
  Rails.application.config.after_initialize do
    Rails.application.assets.load_path.assets
  end
end

Integration with Test Coverage Tools

SimpleCov Configuration

# spec/spec_helper.rb
require 'simplecov'
SimpleCov.start 'rails' do
  # Exclude built assets from coverage
  add_filter 'app/assets/builds/'
  add_filter 'vendor/assets/'
end

Docker and Containerized Testing

Dockerfile Considerations

# Dockerfile
FROM ruby:3.2

# Install Node.js for asset building
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
RUN apt-get install -y nodejs

# Install dependencies
COPY Gemfile Gemfile.lock package.json package-lock.json ./
RUN bundle install && npm install

# Copy source
COPY . .

# Build assets once for all test runs
RUN bundle exec rake assets:precompile

# Run tests
CMD ["bundle", "exec", "rspec"]

Migration Testing Strategy

Before/After Asset Comparison

# spec/migration/sprockets_to_propshaft_spec.rb
RSpec.describe "Sprockets to Propshaft migration" do
  let(:expected_assets) do
    %w[application.js application.css logo.png favicon.ico]
  end
  
  it "serves all expected assets" do
    expected_assets.each do |asset|
      expect(Rails.application.assets.resolver.resolve(asset)).to be_present
    end
  end
  
  it "maintains asset content integrity" do
    # Test that specific assets contain expected content
    asset = Rails.application.assets.load_path.find('application.js')
    expect(asset.content).to include('expected_javascript_content')
  end
end

This comprehensive guide covers the complete migration from Sprockets to Propshaft with special attention to test environment configuration and common pitfalls.