Home > database >  Rails 7 engine how to make uncompiled stylesheets available to host app?
Rails 7 engine how to make uncompiled stylesheets available to host app?

Time:04-04

So I have a file not found problem. I have an engine that works in development mode in the engines test/dummy app, the engine allows the editing of sass variables and stores them in a theme table, the variables are used by a sass partial such as _banner.scss containing variables used in the main stylesheet such as $banner_color which is then imported into the main stylesheet which in turn is precompiled using an initializer in the engine.rb file and inclusion in the app/config/engine_name_manifest.js.

The files are all available in development with the local dummy app but not in the eventual host app due to the assets being compiled.

I have a rake task that takes the data, updates the relevant partial e.g. _banner.scss with the data from the theme table but of course the partials are not not available in a host app as the engine has already compiled them. I'm looking for a solution that will allow me to edit the raw, uncompiled stylesheets then recompile them. Obviously my Capistrano deploy script will need to reapply the stylesheet changes every deployment but that is just a rake task call. What approach should I take? Should I find a way to copy the css files to the host app in an engine initializer? Should I use a different approach entirely, I have started looking at propshaft but that is a massive step to replace sass rails and I'm not sure how that would help

The engine

require "deface"
require 'ccs_cms_admin_dashboard'
require 'ccs_cms_custom_page'
require 'ccs_cms_core'
require 'css_menu'
#require 'tinymce-rails'
require 'delayed_job_active_record'
require 'daemons'
require 'sprockets/railtie'
require 'sassc-rails'

module CcsCms
  module PublicTheme
    class Engine < ::Rails::Engine
      isolate_namespace CcsCms::PublicTheme
      paths["app/views"] << "app/views/ccs_cms/public_theme"

      initializer "ccs_cms.assets.precompile" do |app|
        app.config.assets.precompile  = %w( public_theme_manifest.js )
      end

      initializer :append_migrations do |app|
        unless app.root.to_s.match?(root.to_s)
          config.paths['db/migrate'].expanded.each do |p|
            app.config.paths['db/migrate'] << p
          end
        end
      end

      initializer :active_job_setup do |app|
        app.config.active_job.queue_adapter = :delayed_job
      end

      config.to_prepare do
        Dir.glob(Engine.root.join("app", "decorators", "**", "*_decorator*.rb")) do |c|
          Rails.configuration.cache_classes ? require(c) : load(c)
        end
      end

      config.generators do |g|
        g.test_framework :rspec,
          fixtures: false,
          request: false,
          view_specs: false,
          helper_specs: false,
          controller_specs: false,
          routing_specs: false
        g.fixture_replacement :factory_bot
        g.factory_bot dir: 'spec/factories'
      end

    end
  end
end

The Css class that writes the css

 class Css

  def get_stylesheet_path
    Rails.root.join("app/assets/stylesheets/ccs_cms/public_theme")
  end

  def write_css(theme)
    update_css_files_for(theme.banner, '_public_banner.scss', BANNER_ARRAY, BANNER_FIELD_MAP)
    update_css_files_for(theme.banner.font, '_public_banner_font.scss', BANNER_FONT_ARRAY, BANNER_FONT_FIELD_MAP)
  end

  private

    def update_css_files_for(model_record_to_use, css_file, array_to_use, field_map)
      amended_css = amend_css_for(model_record_to_use, css_file, array_to_use, field_map)
      create_css_files_for(css_file, amended_css)
    end

    def amend_css_for(model_record_to_use, file_name, array_to_use, field_map)
      original_css_array = IO.readlines("#{get_stylesheet_path}/#{file_name}")
      new_array = []
      original_css_array.each do |line|
        new_line = line
        array_to_use.each do |ma|
          if line.start_with?(ma)
            field_name = field_map[ma.to_sym]
            new_line = ma   ": #{model_record_to_use[field_name.to_sym]};"
            #puts("@@@@ original line: #{line}, ma: #{ma}, Field name: #{field_name}, value: #{theme[field_name]}")
            break
          end
        end
        new_array << new_line
      end
      new_array
    end

    # ---- File and I/O Handling ---- #

    def create_css_files_for(file_name, css_array)
      File.open("#{get_stylesheet_path}/#{file_name}", "w") do |file|
        file.puts css_array
      end
    end

end

CodePudding user response:

Thanks for clarifying. If I understood correctly here my take on it.

partials are not not available in a host app as the engine has already compiled them

Partials are still there, precompilation just outputs *.{css/js} files into public/assets/ that are declared in app/assets/config/manifest.js.

To get to engines files, instead of Rails.root use:

CcsCms::PublicTheme::Engine.root

In Css class, for example:

def get_stylesheet_path
 CcsCms::PublicTheme::Engine.root.join("app/assets/stylesheets/ccs_cms/public_theme")
end

To support changing theme engines. Theme root can be set in an engine initializer to something like Rails.configuration.theme_root and used in the main app.

Because your theme is also configurable, I think it's better to read theme's original sass files but not modify them, copy them into a tmp folder and update with values from theme table, then output a theme.css in the main app with sass. https://sass-lang.com/documentation/cli/dart-sass

​# Compiles all Sass
$ sass tmp/theme/application.scss:app/stylesheets/theme.css

Then let Rails take over the precompilation process.

Another option is to have one sass configuration file and only update this file. That way there is no dependency on the file structure of any particular theme.

import 'configuration' // sass variables with values from theme table
import 'banner'        // uses sass variables only
...

Also just use css variables, if that's an option, and avoid all of the above complexity; no precompilation, no redeploys when theme table changes.

Update for css variables.

Just so we're on the same page. I meant these css variables: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties. If Internet Explorer is not a priority for you, this is the best solution. The setup is something like this:

<!-- app/views/layouts/application.erb -->
<!-- NOTE: with turbo this loads only once; full page refresh is needed when @theme changes -->
<head>
  <style>
   :root { --text-color: <%= @theme.text_color %>; }
  </style>

  <%= stylesheet_link_tag 'application' %>
</head>
/* app/assets/stylesheets/application.css */
p { color: var(--text-color); }

Possible fix to avoid amending css files. Use erb interpolation inside the sass files. No need to amend every time a theme configuration is changed. In development compiles on the fly. In production it has to be precompiled again when theme configuration is changed; no amending.

// _banner.scss.erb
p { color: <%= Theme.text_color %>; }

You could even use amend_css_for function to insert literal erb code and save some time. For example

new_line = ma   ": <%= Theme.#{model_name}.#{field_name} %>;" 

Finally, if you don't want to touch engine files and because these files are not part of the main/host app (as in literally two separate folders in the filesystem). You have to make a copy when amending; read from CcsCms::PublicTheme::Engine.root write to Rails.root.

def get_stylesheet_path
  CcsCms::PublicTheme::Engine.root.join("app/assets/stylesheets/ccs_cms/public_theme")
end

# but save to main app

def create_css_files_for(file_name, css_array)
  File.open("#{Rails.root.join("app/assets/stylesheets/ccs_cms/public_theme")}/#{file_name}", "w") do |file|
    file.puts css_array
  end
end
  • Related