mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 13:23:38 +08:00
6e522e4aad
This PR is a major change to Sass compilation in Discourse. The new version of sass-ruby moves to dart-sass putting we back on the supported version of Sass. It does so while keeping compatibility with the existing method signatures, so minimal change is needed in Discourse for this change. This moves us From: - sassc 2.0.1 (Feb 2019) - libsass 3.5.2 (May 2018) To: - dart-sass 1.58 This update applies the following breaking changes: > > These breaking changes are coming soon or have recently been released: > > [Functions are stricter about which units they allow](https://sass-lang.com/documentation/breaking-changes/function-units) beginning in Dart Sass 1.32.0. > > [Selectors with invalid combinators are invalid](https://sass-lang.com/documentation/breaking-changes/bogus-combinators) beginning in Dart Sass 1.54.0. > > [/ is changing from a division operation to a list separator](https://sass-lang.com/documentation/breaking-changes/slash-div) beginning in Dart Sass 1.33.0. > > [Parsing the special syntax of @-moz-document will be invalid](https://sass-lang.com/documentation/breaking-changes/moz-document) beginning in Dart Sass 1.7.2. > > [Compound selectors could not be extended](https://sass-lang.com/documentation/breaking-changes/extend-compound) in Dart Sass 1.0.0 and Ruby Sass 4.0.0. SCSS files have been migrated automatically using `sass-migrator division app/assets/stylesheets/**/*.scss`
300 lines
8.1 KiB
Ruby
300 lines
8.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Stylesheet::Manager::Builder
|
|
attr_reader :theme
|
|
|
|
def initialize(target: :desktop, theme: nil, color_scheme: nil, manager:)
|
|
@target = target
|
|
@theme = theme
|
|
@color_scheme = color_scheme
|
|
@manager = manager
|
|
end
|
|
|
|
def compile(opts = {})
|
|
if !opts[:force]
|
|
if File.exist?(stylesheet_fullpath)
|
|
if !StylesheetCache.where(target: qualified_target, digest: digest).exists?
|
|
begin
|
|
source_map =
|
|
begin
|
|
File.read(source_map_fullpath)
|
|
rescue Errno::ENOENT
|
|
end
|
|
|
|
StylesheetCache.add(
|
|
qualified_target,
|
|
digest,
|
|
File.read(stylesheet_fullpath),
|
|
source_map,
|
|
)
|
|
rescue => e
|
|
Rails.logger.warn "Completely unexpected error adding contents of '#{stylesheet_fullpath}' to cache #{e}"
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
rtl = @target.to_s =~ /_rtl\z/
|
|
css, source_map =
|
|
with_load_paths do |load_paths|
|
|
Stylesheet::Compiler.compile_asset(
|
|
@target,
|
|
rtl: rtl,
|
|
theme_id: theme&.id,
|
|
theme_variables: theme&.scss_variables.to_s,
|
|
source_map_file: source_map_url_relative_from_stylesheet,
|
|
color_scheme_id: @color_scheme&.id,
|
|
load_paths: load_paths,
|
|
)
|
|
rescue SassC::SyntaxError, SassC::NotRenderedError => e
|
|
if Stylesheet::Importer::THEME_TARGETS.include?(@target.to_s)
|
|
# no special errors for theme, handled in theme editor
|
|
["", nil]
|
|
elsif @target.to_s == Stylesheet::Manager::COLOR_SCHEME_STYLESHEET
|
|
# log error but do not crash for errors in color definitions SCSS
|
|
Rails.logger.error "SCSS compilation error: #{e.message}"
|
|
["", nil]
|
|
else
|
|
raise Discourse::ScssError, e.message
|
|
end
|
|
end
|
|
|
|
FileUtils.mkdir_p(cache_fullpath)
|
|
|
|
File.open(stylesheet_fullpath, "w") { |f| f.puts css }
|
|
|
|
File.open(source_map_fullpath, "w") { |f| f.puts source_map } if source_map.present?
|
|
|
|
begin
|
|
StylesheetCache.add(qualified_target, digest, css, source_map)
|
|
rescue => e
|
|
Rails.logger.warn "Completely unexpected error adding item to cache #{e}"
|
|
end
|
|
css
|
|
end
|
|
|
|
def current_hostname
|
|
Discourse.current_hostname
|
|
end
|
|
|
|
def cache_fullpath
|
|
Stylesheet::Manager.cache_fullpath
|
|
end
|
|
|
|
def stylesheet_fullpath
|
|
"#{cache_fullpath}/#{stylesheet_filename}"
|
|
end
|
|
|
|
def source_map_fullpath
|
|
"#{cache_fullpath}/#{source_map_filename}"
|
|
end
|
|
|
|
def source_map_filename
|
|
"#{stylesheet_filename}.map"
|
|
end
|
|
|
|
def source_map_url_relative_from_stylesheet
|
|
"#{source_map_filename}?__ws=#{current_hostname}"
|
|
end
|
|
|
|
def stylesheet_fullpath_no_digest
|
|
"#{cache_fullpath}/#{stylesheet_filename_no_digest}"
|
|
end
|
|
|
|
def stylesheet_absolute_url
|
|
"#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{current_hostname}"
|
|
end
|
|
|
|
def root_path
|
|
"#{GlobalSetting.relative_url_root}/"
|
|
end
|
|
|
|
def stylesheet_relpath
|
|
"#{root_path}stylesheets/#{stylesheet_filename}"
|
|
end
|
|
|
|
def stylesheet_relpath_no_digest
|
|
"#{root_path}stylesheets/#{stylesheet_filename_no_digest}"
|
|
end
|
|
|
|
def qualified_target
|
|
if is_theme?
|
|
"#{@target}_#{theme&.id}"
|
|
elsif @color_scheme
|
|
"#{@target}_#{scheme_slug}_#{@color_scheme&.id.to_s}_#{@theme&.id}"
|
|
else
|
|
scheme_string = theme&.color_scheme ? "_#{theme.color_scheme.id}" : ""
|
|
"#{@target}#{scheme_string}"
|
|
end
|
|
end
|
|
|
|
def stylesheet_filename(with_digest = true)
|
|
digest_string = "_#{self.digest}" if with_digest
|
|
"#{qualified_target}#{digest_string}.css"
|
|
end
|
|
|
|
def stylesheet_filename_no_digest
|
|
stylesheet_filename(_with_digest = false)
|
|
end
|
|
|
|
def is_theme?
|
|
!!(@target.to_s =~ Stylesheet::Manager::THEME_REGEX)
|
|
end
|
|
|
|
def is_color_scheme?
|
|
!!(@target.to_s == Stylesheet::Manager::COLOR_SCHEME_STYLESHEET)
|
|
end
|
|
|
|
def scheme_slug
|
|
Slug.for(ActiveSupport::Inflector.transliterate(@color_scheme.name), "scheme")
|
|
end
|
|
|
|
# digest encodes the things that trigger a recompile
|
|
def digest
|
|
@digest ||=
|
|
begin
|
|
if is_theme?
|
|
theme_digest
|
|
elsif is_color_scheme?
|
|
color_scheme_digest
|
|
else
|
|
default_digest
|
|
end
|
|
end
|
|
end
|
|
|
|
def with_load_paths
|
|
if theme
|
|
theme.with_scss_load_paths { |p| yield p }
|
|
else
|
|
yield nil
|
|
end
|
|
end
|
|
|
|
def scss_digest
|
|
if %i[mobile_theme desktop_theme].include?(@target)
|
|
resolve_baked_field(@target.to_s.sub("_theme", ""), :scss)
|
|
elsif @target == :embedded_theme
|
|
resolve_baked_field(:common, :embedded_scss)
|
|
else
|
|
raise "attempting to look up theme digest for invalid field"
|
|
end
|
|
end
|
|
|
|
def theme_digest
|
|
Digest::SHA1.hexdigest(
|
|
scss_digest.to_s + color_scheme_digest.to_s + settings_digest + uploads_digest +
|
|
current_hostname,
|
|
)
|
|
end
|
|
|
|
# this protects us from situations where new versions of a plugin removed a file
|
|
# old instances may still be serving CSS and not aware of the change
|
|
# so we could end up poisoning the cache with a bad file that can not be removed
|
|
def plugins_digest
|
|
assets = []
|
|
DiscoursePluginRegistry.stylesheets.each { |_, paths| assets += paths.to_a }
|
|
DiscoursePluginRegistry.mobile_stylesheets.each { |_, paths| assets += paths.to_a }
|
|
DiscoursePluginRegistry.desktop_stylesheets.each { |_, paths| assets += paths.to_a }
|
|
Digest::SHA1.hexdigest(assets.sort.join)
|
|
end
|
|
|
|
def settings_digest
|
|
themes =
|
|
if !theme
|
|
[]
|
|
elsif Theme.is_parent_theme?(theme.id)
|
|
@manager.load_themes(@manager.theme_ids)
|
|
else
|
|
[@manager.get_theme(theme.id)]
|
|
end
|
|
|
|
fields =
|
|
themes.each_with_object([]) do |theme, array|
|
|
array.concat(theme.yaml_theme_fields.map(&:updated_at))
|
|
end
|
|
|
|
settings =
|
|
themes.each_with_object([]) do |theme, array|
|
|
array.concat(theme.theme_settings.map(&:updated_at))
|
|
end
|
|
|
|
timestamps = fields.concat(settings).map!(&:to_f).sort!.join(",")
|
|
|
|
Digest::SHA1.hexdigest(timestamps)
|
|
end
|
|
|
|
def uploads_digest
|
|
sha1s = []
|
|
|
|
(theme&.upload_fields || []).map { |upload_field| sha1s << upload_field.upload&.sha1 }
|
|
|
|
Digest::SHA1.hexdigest(sha1s.compact.sort!.join("\n"))
|
|
end
|
|
|
|
def default_digest
|
|
Digest::SHA1.hexdigest "default-#{Stylesheet::Manager.fs_asset_cachebuster}-#{plugins_digest}-#{current_hostname}"
|
|
end
|
|
|
|
def color_scheme_digest
|
|
cs = @color_scheme || theme&.color_scheme
|
|
|
|
categories_updated =
|
|
Stylesheet::Manager
|
|
.cache
|
|
.defer_get_set("categories_updated") do
|
|
Category.where("uploaded_background_id IS NOT NULL").pluck(:updated_at).map(&:to_i).sum
|
|
end
|
|
|
|
fonts = "#{SiteSetting.base_font}-#{SiteSetting.heading_font}"
|
|
|
|
digest_string = "#{current_hostname}-"
|
|
if cs || categories_updated > 0
|
|
theme_color_defs = resolve_baked_field(:common, :color_definitions)
|
|
digest_string +=
|
|
"#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{categories_updated}-#{fonts}"
|
|
else
|
|
digest_string += "defaults-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"
|
|
|
|
if cdn_url = GlobalSetting.cdn_url
|
|
digest_string += "-#{cdn_url}"
|
|
end
|
|
end
|
|
Digest::SHA1.hexdigest digest_string
|
|
end
|
|
|
|
def resolve_baked_field(target, name)
|
|
theme_ids =
|
|
if !theme
|
|
[]
|
|
elsif Theme.is_parent_theme?(theme.id)
|
|
@manager.theme_ids
|
|
else
|
|
[theme.id]
|
|
end
|
|
|
|
theme_ids = [theme_ids.first] if name != :color_definitions
|
|
|
|
baked_fields = []
|
|
targets = [Theme.targets[target.to_sym], Theme.targets[:common]]
|
|
|
|
@manager
|
|
.load_themes(theme_ids)
|
|
.each do |theme|
|
|
theme.builder_theme_fields.each do |theme_field|
|
|
if theme_field.name == name.to_s && targets.include?(theme_field.target_id)
|
|
baked_fields << theme_field
|
|
end
|
|
end
|
|
end
|
|
|
|
baked_fields
|
|
.map do |f|
|
|
f.ensure_baked!
|
|
f.value_baked || f.value
|
|
end
|
|
.join("\n")
|
|
end
|
|
end
|