mirror of
https://github.com/discourse/discourse.git
synced 2025-09-06 10:50:21 +08:00
PERF: Eager load Theme associations in Stylesheet Manager.
Before this change, calling `StyleSheet::Manager.stylesheet_details` for the first time resulted in multiple queries to the database. This is because the code was modelled in a way where each `Theme` was loaded from the database one at a time. This PR restructures the code such that it allows us to load all the theme records in a single query. It also allows us to eager load the required associations upfront. In order to achieve this, I removed the support of loading multiple themes per request. It was initially added to support user selectable theme components but the feature was never completed and abandoned because it wasn't a feature that we thought was worth building.
This commit is contained in:
parent
53dab8cf1e
commit
8e3691d537
35 changed files with 983 additions and 668 deletions
|
@ -101,7 +101,7 @@ module Stylesheet
|
|||
end
|
||||
|
||||
theme_id = @theme_id || SiteSetting.default_theme_id
|
||||
resolved_ids = Theme.transform_ids([theme_id])
|
||||
resolved_ids = Theme.transform_ids(theme_id)
|
||||
|
||||
if resolved_ids
|
||||
theme = Theme.find_by_id(theme_id)
|
||||
|
|
|
@ -13,7 +13,7 @@ class Stylesheet::Manager
|
|||
THEME_REGEX ||= /_theme$/
|
||||
COLOR_SCHEME_STYLESHEET ||= "color_definitions"
|
||||
|
||||
@lock = Mutex.new
|
||||
@@lock = Mutex.new
|
||||
|
||||
def self.cache
|
||||
@cache ||= DistributedCache.new("discourse_stylesheet")
|
||||
|
@ -35,117 +35,6 @@ class Stylesheet::Manager
|
|||
cache.hash.keys.select { |k| k =~ /#{plugin}/ }.each { |k| cache.delete(k) }
|
||||
end
|
||||
|
||||
def self.stylesheet_data(target = :desktop, theme_ids = :missing)
|
||||
stylesheet_details(target, "all", theme_ids)
|
||||
end
|
||||
|
||||
def self.stylesheet_link_tag(target = :desktop, media = 'all', theme_ids = :missing)
|
||||
stylesheets = stylesheet_details(target, media, theme_ids)
|
||||
stylesheets.map do |stylesheet|
|
||||
href = stylesheet[:new_href]
|
||||
theme_id = stylesheet[:theme_id]
|
||||
data_theme_id = theme_id ? "data-theme-id=\"#{theme_id}\"" : ""
|
||||
%[<link href="#{href}" media="#{media}" rel="stylesheet" data-target="#{target}" #{data_theme_id}/>]
|
||||
end.join("\n").html_safe
|
||||
end
|
||||
|
||||
def self.stylesheet_details(target = :desktop, media = 'all', theme_ids = :missing)
|
||||
if theme_ids == :missing
|
||||
theme_ids = [SiteSetting.default_theme_id]
|
||||
end
|
||||
|
||||
target = target.to_sym
|
||||
|
||||
theme_ids = [theme_ids] unless Array === theme_ids
|
||||
theme_ids = [theme_ids.first] unless target =~ THEME_REGEX
|
||||
include_components = !!(target =~ THEME_REGEX)
|
||||
|
||||
theme_ids = Theme.transform_ids(theme_ids, extend: include_components)
|
||||
|
||||
current_hostname = Discourse.current_hostname
|
||||
|
||||
array_cache_key = "array_themes_#{theme_ids.join(",")}_#{target}_#{current_hostname}"
|
||||
stylesheets = cache[array_cache_key]
|
||||
return stylesheets if stylesheets.present?
|
||||
|
||||
@lock.synchronize do
|
||||
stylesheets = []
|
||||
theme_ids.each do |theme_id|
|
||||
data = { target: target }
|
||||
cache_key = "path_#{target}_#{theme_id}_#{current_hostname}"
|
||||
href = cache[cache_key]
|
||||
|
||||
unless href
|
||||
builder = self.new(target, theme_id)
|
||||
is_theme = builder.is_theme?
|
||||
has_theme = builder.theme.present?
|
||||
|
||||
if is_theme && !has_theme
|
||||
next
|
||||
else
|
||||
next if builder.theme&.component && !builder.theme&.has_scss(target)
|
||||
data[:theme_id] = builder.theme.id if has_theme && is_theme
|
||||
builder.compile unless File.exists?(builder.stylesheet_fullpath)
|
||||
href = builder.stylesheet_path(current_hostname)
|
||||
end
|
||||
|
||||
cache.defer_set(cache_key, href)
|
||||
end
|
||||
|
||||
data[:theme_id] = theme_id if theme_id.present? && data[:theme_id].blank?
|
||||
data[:new_href] = href
|
||||
stylesheets << data
|
||||
end
|
||||
|
||||
cache.defer_set(array_cache_key, stylesheets.freeze)
|
||||
stylesheets
|
||||
end
|
||||
end
|
||||
|
||||
def self.color_scheme_stylesheet_details(color_scheme_id = nil, media, theme_id)
|
||||
theme_id = theme_id || SiteSetting.default_theme_id
|
||||
|
||||
color_scheme = begin
|
||||
ColorScheme.find(color_scheme_id)
|
||||
rescue
|
||||
# don't load fallback when requesting dark color scheme
|
||||
return false if media != "all"
|
||||
|
||||
Theme.find_by_id(theme_id)&.color_scheme || ColorScheme.base
|
||||
end
|
||||
|
||||
return false if !color_scheme
|
||||
|
||||
target = COLOR_SCHEME_STYLESHEET.to_sym
|
||||
current_hostname = Discourse.current_hostname
|
||||
cache_key = color_scheme_cache_key(color_scheme, theme_id)
|
||||
stylesheets = cache[cache_key]
|
||||
return stylesheets if stylesheets.present?
|
||||
|
||||
stylesheet = { color_scheme_id: color_scheme&.id }
|
||||
|
||||
builder = self.new(target, theme_id, color_scheme)
|
||||
|
||||
builder.compile unless File.exists?(builder.stylesheet_fullpath)
|
||||
|
||||
href = builder.stylesheet_path(current_hostname)
|
||||
stylesheet[:new_href] = href
|
||||
cache.defer_set(cache_key, stylesheet.freeze)
|
||||
stylesheet
|
||||
end
|
||||
|
||||
def self.color_scheme_stylesheet_link_tag(color_scheme_id = nil, media = 'all', theme_ids = nil)
|
||||
theme_id = theme_ids&.first
|
||||
stylesheet = color_scheme_stylesheet_details(color_scheme_id, media, theme_id)
|
||||
return '' if !stylesheet
|
||||
|
||||
href = stylesheet[:new_href]
|
||||
|
||||
css_class = media == 'all' ? "light-scheme" : "dark-scheme"
|
||||
|
||||
%[<link href="#{href}" media="#{media}" rel="stylesheet" class="#{css_class}"/>].html_safe
|
||||
end
|
||||
|
||||
def self.color_scheme_cache_key(color_scheme, theme_id = nil)
|
||||
color_scheme_name = Slug.for(color_scheme.name) + color_scheme&.id.to_s
|
||||
theme_string = theme_id ? "_theme#{theme_id}" : ""
|
||||
|
@ -164,24 +53,30 @@ class Stylesheet::Manager
|
|||
targets += Discourse.find_plugin_css_assets(include_disabled: true, mobile_view: true, desktop_view: true)
|
||||
|
||||
themes.each do |id, name, color_scheme_id|
|
||||
targets.each do |target|
|
||||
theme_id = id || SiteSetting.default_theme_id
|
||||
theme_id = id || SiteSetting.default_theme_id
|
||||
manager = self.new(theme_id: theme_id)
|
||||
|
||||
targets.each do |target|
|
||||
if target =~ THEME_REGEX
|
||||
next if theme_id == -1
|
||||
|
||||
theme_ids = Theme.transform_ids([theme_id], extend: true)
|
||||
scss_checker = ScssChecker.new(target, manager.theme_ids)
|
||||
|
||||
manager.load_themes(manager.theme_ids).each do |theme|
|
||||
builder = Stylesheet::Manager::Builder.new(
|
||||
target: target, theme: theme, manager: manager
|
||||
)
|
||||
|
||||
theme_ids.each do |t_id|
|
||||
builder = self.new(target, t_id)
|
||||
STDERR.puts "precompile target: #{target} #{builder.theme.name}"
|
||||
next if builder.theme.component && !builder.theme.has_scss(target)
|
||||
next if theme.component && !scss_checker.has_scss(theme.id)
|
||||
builder.compile(force: true)
|
||||
end
|
||||
else
|
||||
STDERR.puts "precompile target: #{target} #{name}"
|
||||
builder = self.new(target, theme_id)
|
||||
builder.compile(force: true)
|
||||
|
||||
Stylesheet::Manager::Builder.new(
|
||||
target: target, theme: manager.get_theme(theme_id), manager: manager
|
||||
).compile(force: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -190,8 +85,12 @@ class Stylesheet::Manager
|
|||
[theme_color_scheme, *color_schemes].uniq.each do |scheme|
|
||||
STDERR.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{name} (#{scheme.name})"
|
||||
|
||||
builder = self.new(COLOR_SCHEME_STYLESHEET, id, scheme)
|
||||
builder.compile(force: true)
|
||||
Stylesheet::Manager::Builder.new(
|
||||
target: COLOR_SCHEME_STYLESHEET,
|
||||
theme: manager.get_theme(theme_id),
|
||||
color_scheme: scheme,
|
||||
manager: manager
|
||||
).compile(force: true)
|
||||
end
|
||||
clear_color_scheme_cache!
|
||||
end
|
||||
|
@ -232,245 +131,165 @@ class Stylesheet::Manager
|
|||
"#{Rails.root}/#{CACHE_PATH}"
|
||||
end
|
||||
|
||||
def initialize(target = :desktop, theme_id = nil, color_scheme = nil)
|
||||
@target = target
|
||||
@theme_id = theme_id
|
||||
@color_scheme = color_scheme
|
||||
attr_reader :theme_ids
|
||||
|
||||
def initialize(theme_id: nil)
|
||||
@theme_id = theme_id || SiteSetting.default_theme_id
|
||||
@theme_ids = Theme.transform_ids(@theme_id)
|
||||
@themes_cache = {}
|
||||
end
|
||||
|
||||
def compile(opts = {})
|
||||
unless opts[:force]
|
||||
if File.exists?(stylesheet_fullpath)
|
||||
unless StylesheetCache.where(target: qualified_target, digest: digest).exists?
|
||||
begin
|
||||
source_map = begin
|
||||
File.read(source_map_fullpath)
|
||||
rescue Errno::ENOENT
|
||||
end
|
||||
def cache
|
||||
self.class.cache
|
||||
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
|
||||
def get_theme(theme_id)
|
||||
if theme = @themes_cache[theme_id]
|
||||
theme
|
||||
else
|
||||
load_themes([theme_id]).first
|
||||
end
|
||||
end
|
||||
|
||||
def load_themes(theme_ids)
|
||||
themes = []
|
||||
to_load_theme_ids = []
|
||||
|
||||
theme_ids.each do |theme_id|
|
||||
if @themes_cache[theme_id]
|
||||
themes << @themes_cache[theme_id]
|
||||
else
|
||||
to_load_theme_ids << theme_id
|
||||
end
|
||||
end
|
||||
|
||||
Theme
|
||||
.where(id: to_load_theme_ids)
|
||||
.includes(:yaml_theme_fields, :theme_settings, :upload_fields, :builder_theme_fields)
|
||||
.each do |theme|
|
||||
|
||||
@themes_cache[theme.id] = theme
|
||||
themes << theme
|
||||
end
|
||||
|
||||
themes
|
||||
end
|
||||
|
||||
def stylesheet_data(target = :desktop)
|
||||
stylesheet_details(target, "all")
|
||||
end
|
||||
|
||||
def stylesheet_link_tag(target = :desktop, media = 'all')
|
||||
stylesheets = stylesheet_details(target, media)
|
||||
|
||||
stylesheets.map do |stylesheet|
|
||||
href = stylesheet[:new_href]
|
||||
theme_id = stylesheet[:theme_id]
|
||||
data_theme_id = theme_id ? "data-theme-id=\"#{theme_id}\"" : ""
|
||||
%[<link href="#{href}" media="#{media}" rel="stylesheet" data-target="#{target}" #{data_theme_id}/>]
|
||||
end.join("\n").html_safe
|
||||
end
|
||||
|
||||
def stylesheet_details(target = :desktop, media = 'all')
|
||||
target = target.to_sym
|
||||
current_hostname = Discourse.current_hostname
|
||||
|
||||
array_cache_key = "array_themes_#{@theme_ids.join(",")}_#{target}_#{current_hostname}"
|
||||
stylesheets = cache[array_cache_key]
|
||||
return stylesheets if stylesheets.present?
|
||||
|
||||
@@lock.synchronize do
|
||||
stylesheets = []
|
||||
stale_theme_ids = []
|
||||
|
||||
@theme_ids.each do |theme_id|
|
||||
cache_key = "path_#{target}_#{theme_id}_#{current_hostname}"
|
||||
|
||||
if href = cache[cache_key]
|
||||
stylesheets << {
|
||||
target: target,
|
||||
theme_id: theme_id,
|
||||
new_href: href
|
||||
}
|
||||
else
|
||||
stale_theme_ids << theme_id
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
rtl = @target.to_s =~ /_rtl$/
|
||||
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_filename,
|
||||
color_scheme_id: @color_scheme&.id,
|
||||
load_paths: load_paths
|
||||
)
|
||||
rescue SassC::SyntaxError => e
|
||||
if Stylesheet::Importer::THEME_TARGETS.include?(@target.to_s)
|
||||
# no special errors for theme, handled in theme editor
|
||||
["", nil]
|
||||
elsif @target.to_s == 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
|
||||
scss_checker = ScssChecker.new(target, stale_theme_ids)
|
||||
|
||||
load_themes(stale_theme_ids).each do |theme|
|
||||
theme_id = theme.id
|
||||
data = { target: target, theme_id: theme_id }
|
||||
builder = Builder.new(target: target, theme: theme, manager: self)
|
||||
|
||||
next if builder.theme.component && !scss_checker.has_scss(theme_id)
|
||||
builder.compile unless File.exists?(builder.stylesheet_fullpath)
|
||||
href = builder.stylesheet_path(current_hostname)
|
||||
|
||||
cache.defer_set("path_#{target}_#{theme_id}_#{current_hostname}", href)
|
||||
|
||||
data[:new_href] = href
|
||||
stylesheets << data
|
||||
end
|
||||
end
|
||||
|
||||
FileUtils.mkdir_p(cache_fullpath)
|
||||
|
||||
File.open(stylesheet_fullpath, "w") do |f|
|
||||
f.puts css
|
||||
end
|
||||
|
||||
if source_map.present?
|
||||
File.open(source_map_fullpath, "w") do |f|
|
||||
f.puts source_map
|
||||
end
|
||||
end
|
||||
|
||||
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 cache_fullpath
|
||||
self.class.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 stylesheet_fullpath_no_digest
|
||||
"#{cache_fullpath}/#{stylesheet_filename_no_digest}"
|
||||
end
|
||||
|
||||
def stylesheet_cdnpath(hostname)
|
||||
"#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{hostname}"
|
||||
end
|
||||
|
||||
def stylesheet_path(hostname)
|
||||
stylesheet_cdnpath(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}"
|
||||
else
|
||||
scheme_string = theme && theme.color_scheme ? "_#{theme.color_scheme.id}" : ""
|
||||
"#{@target}#{scheme_string}"
|
||||
cache.defer_set(array_cache_key, stylesheets.freeze)
|
||||
stylesheets
|
||||
end
|
||||
end
|
||||
|
||||
def stylesheet_filename(with_digest = true)
|
||||
digest_string = "_#{self.digest}" if with_digest
|
||||
"#{qualified_target}#{digest_string}.css"
|
||||
end
|
||||
def color_scheme_stylesheet_details(color_scheme_id = nil, media)
|
||||
theme_id = @theme_ids.first
|
||||
|
||||
def stylesheet_filename_no_digest
|
||||
stylesheet_filename(_with_digest = false)
|
||||
end
|
||||
color_scheme = begin
|
||||
ColorScheme.find(color_scheme_id)
|
||||
rescue
|
||||
# don't load fallback when requesting dark color scheme
|
||||
return false if media != "all"
|
||||
|
||||
def is_theme?
|
||||
!!(@target.to_s =~ THEME_REGEX)
|
||||
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
|
||||
else
|
||||
color_scheme_digest
|
||||
end
|
||||
get_theme(theme_id)&.color_scheme || ColorScheme.base
|
||||
end
|
||||
end
|
||||
|
||||
def theme
|
||||
@theme ||= Theme.find_by(id: @theme_id) || :nil
|
||||
@theme == :nil ? nil : @theme
|
||||
end
|
||||
return false if !color_scheme
|
||||
|
||||
target = COLOR_SCHEME_STYLESHEET.to_sym
|
||||
current_hostname = Discourse.current_hostname
|
||||
cache_key = self.class.color_scheme_cache_key(color_scheme, theme_id)
|
||||
stylesheets = cache[cache_key]
|
||||
return stylesheets if stylesheets.present?
|
||||
|
||||
stylesheet = { color_scheme_id: color_scheme.id }
|
||||
|
||||
theme = get_theme(theme_id)
|
||||
|
||||
def with_load_paths
|
||||
if theme
|
||||
theme.with_scss_load_paths { |p| yield p }
|
||||
builder = Builder.new(
|
||||
target: target,
|
||||
theme: get_theme(theme_id),
|
||||
color_scheme: color_scheme,
|
||||
manager: self
|
||||
)
|
||||
|
||||
builder.compile unless File.exists?(builder.stylesheet_fullpath)
|
||||
|
||||
href = builder.stylesheet_path(current_hostname)
|
||||
stylesheet[:new_href] = href
|
||||
cache.defer_set(cache_key, stylesheet.freeze)
|
||||
stylesheet
|
||||
else
|
||||
yield nil
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def theme_digest
|
||||
if [:mobile_theme, :desktop_theme].include?(@target)
|
||||
scss_digest = theme.resolve_baked_field(@target.to_s.sub("_theme", ""), :scss)
|
||||
elsif @target == :embedded_theme
|
||||
scss_digest = theme.resolve_baked_field(:common, :embedded_scss)
|
||||
else
|
||||
raise "attempting to look up theme digest for invalid field"
|
||||
end
|
||||
def color_scheme_stylesheet_link_tag(color_scheme_id = nil, media = 'all')
|
||||
stylesheet = color_scheme_stylesheet_details(color_scheme_id, media)
|
||||
|
||||
Digest::SHA1.hexdigest(scss_digest.to_s + color_scheme_digest.to_s + settings_digest + plugins_digest + uploads_digest)
|
||||
end
|
||||
return '' if !stylesheet
|
||||
|
||||
# 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
|
||||
href = stylesheet[:new_href]
|
||||
|
||||
def settings_digest
|
||||
theme_ids = Theme.components_for(@theme_id).dup
|
||||
theme_ids << @theme_id
|
||||
css_class = media == 'all' ? "light-scheme" : "dark-scheme"
|
||||
|
||||
fields = ThemeField.where(
|
||||
name: "yaml",
|
||||
type_id: ThemeField.types[:yaml],
|
||||
theme_id: theme_ids
|
||||
).pluck(:updated_at)
|
||||
|
||||
settings = ThemeSetting.where(theme_id: theme_ids).pluck(:updated_at)
|
||||
timestamps = fields.concat(settings).map!(&:to_f).sort!.join(",")
|
||||
|
||||
Digest::SHA1.hexdigest(timestamps)
|
||||
end
|
||||
|
||||
def uploads_digest
|
||||
sha1s =
|
||||
if (theme_ids = theme&.all_theme_variables).present?
|
||||
ThemeField
|
||||
.joins(:upload)
|
||||
.where(id: theme_ids)
|
||||
.pluck(:sha1)
|
||||
.join(",")
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
Digest::SHA1.hexdigest(sha1s)
|
||||
end
|
||||
|
||||
def color_scheme_digest
|
||||
cs = @color_scheme || theme&.color_scheme
|
||||
|
||||
categories_updated = self.class.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}"
|
||||
|
||||
if cs || categories_updated > 0
|
||||
theme_color_defs = theme&.resolve_baked_field(:common, :color_definitions)
|
||||
Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.last_file_updated}-#{categories_updated}-#{fonts}"
|
||||
else
|
||||
digest_string = "defaults-#{Stylesheet::Manager.last_file_updated}-#{fonts}"
|
||||
|
||||
if cdn_url = GlobalSetting.cdn_url
|
||||
digest_string = "#{digest_string}-#{cdn_url}"
|
||||
end
|
||||
|
||||
Digest::SHA1.hexdigest digest_string
|
||||
end
|
||||
%[<link href="#{href}" media="#{media}" rel="stylesheet" class="#{css_class}"/>].html_safe
|
||||
end
|
||||
end
|
||||
|
|
274
lib/stylesheet/manager/builder.rb
Normal file
274
lib/stylesheet/manager/builder.rb
Normal file
|
@ -0,0 +1,274 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Stylesheet::Manager::Builder
|
||||
attr_reader :theme
|
||||
|
||||
def initialize(target: :desktop, theme:, color_scheme: nil, manager:)
|
||||
@target = target
|
||||
@theme = theme
|
||||
@color_scheme = color_scheme
|
||||
@manager = manager
|
||||
end
|
||||
|
||||
def compile(opts = {})
|
||||
if !opts[:force]
|
||||
if File.exists?(stylesheet_fullpath)
|
||||
unless 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$/
|
||||
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_filename,
|
||||
color_scheme_id: @color_scheme&.id,
|
||||
load_paths: load_paths
|
||||
)
|
||||
rescue SassC::SyntaxError => 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") do |f|
|
||||
f.puts css
|
||||
end
|
||||
|
||||
if source_map.present?
|
||||
File.open(source_map_fullpath, "w") do |f|
|
||||
f.puts source_map
|
||||
end
|
||||
end
|
||||
|
||||
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 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 stylesheet_fullpath_no_digest
|
||||
"#{cache_fullpath}/#{stylesheet_filename_no_digest}"
|
||||
end
|
||||
|
||||
def stylesheet_cdnpath(hostname)
|
||||
"#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{hostname}"
|
||||
end
|
||||
|
||||
def stylesheet_path(hostname)
|
||||
stylesheet_cdnpath(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}"
|
||||
else
|
||||
scheme_string = theme && 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 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
|
||||
else
|
||||
color_scheme_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 [: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 + plugins_digest + uploads_digest)
|
||||
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
|
||||
theme_ids = Theme.is_parent_theme?(theme.id) ? @manager.theme_ids : [theme.id]
|
||||
|
||||
themes =
|
||||
if 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 do |upload_field|
|
||||
sha1s << upload_field.upload.sha1
|
||||
end
|
||||
|
||||
Digest::SHA1.hexdigest(sha1s.sort!.join("\n"))
|
||||
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}"
|
||||
|
||||
if cs || categories_updated > 0
|
||||
theme_color_defs = resolve_baked_field(:common, :color_definitions)
|
||||
Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.last_file_updated}-#{categories_updated}-#{fonts}"
|
||||
else
|
||||
digest_string = "defaults-#{Stylesheet::Manager.last_file_updated}-#{fonts}"
|
||||
|
||||
if cdn_url = GlobalSetting.cdn_url
|
||||
digest_string = "#{digest_string}-#{cdn_url}"
|
||||
end
|
||||
|
||||
Digest::SHA1.hexdigest digest_string
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_baked_field(target, name)
|
||||
theme_ids =
|
||||
if 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
|
35
lib/stylesheet/manager/scss_checker.rb
Normal file
35
lib/stylesheet/manager/scss_checker.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Stylesheet::Manager::ScssChecker
|
||||
def initialize(target, theme_ids)
|
||||
@target = target.to_sym
|
||||
@theme_ids = theme_ids
|
||||
end
|
||||
|
||||
def has_scss(theme_id)
|
||||
!!get_themes_with_scss[theme_id]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_themes_with_scss
|
||||
@themes_with_scss ||= begin
|
||||
theme_target = @target.to_sym
|
||||
theme_target = :mobile if theme_target == :mobile_theme
|
||||
theme_target = :desktop if theme_target == :desktop_theme
|
||||
name = @target == :embedded_theme ? :embedded_scss : :scss
|
||||
|
||||
results = Theme
|
||||
.where(id: @theme_ids)
|
||||
.left_joins(:theme_fields)
|
||||
.where(theme_fields: {
|
||||
target_id: [Theme.targets[theme_target], Theme.targets[:common]],
|
||||
name: name
|
||||
})
|
||||
.group(:id)
|
||||
.size
|
||||
|
||||
results
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue