mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-13 10:26:53 +08:00
Previously we were compiling core and theme CSS into two targets: Desktop and Mobile. The majority of both files was the 'common' css. This commit splits those common styles into their own targets so that there is less duplication. This should improve compilation times + cache reuse, as well as opening the door for experiments with media-query-based mobile-modes. The only functional change is that we can no longer use `@extend` to copy 'common' rules in core to mobile/desktop. This is probably for the best. Duplication and/or mixins are a more native-css pattern for this. Plugins already have a common / mobile / desktop pattern, so are unchanged by this commit.
424 lines
12 KiB
Ruby
424 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "distributed_cache"
|
|
require "stylesheet/compiler"
|
|
|
|
module Stylesheet
|
|
end
|
|
|
|
class Stylesheet::Manager
|
|
# Bump this number to invalidate all stylesheet caches (e.g. if you change something inside the compiler)
|
|
BASE_COMPILER_VERSION = 5
|
|
|
|
# Add any dependencies here which should automatically cause a global cache invalidation.
|
|
BASE_CACHE_KEY = "#{BASE_COMPILER_VERSION}::#{DiscourseFonts::VERSION}"
|
|
|
|
CACHE_PATH = "tmp/stylesheet-cache"
|
|
private_constant :CACHE_PATH
|
|
|
|
MANIFEST_DIR = "#{Rails.root}/tmp/cache/assets/#{Rails.env}"
|
|
THEME_REGEX = /_theme\z/
|
|
COLOR_SCHEME_STYLESHEET = "color_definitions"
|
|
|
|
@@lock = Mutex.new
|
|
|
|
def self.cache
|
|
@cache ||= DistributedCache.new("discourse_stylesheet")
|
|
end
|
|
|
|
def self.clear_theme_cache!
|
|
cache.clear_regex(/theme/)
|
|
end
|
|
|
|
def self.clear_color_scheme_cache!
|
|
cache.clear_regex(/color_definitions/)
|
|
end
|
|
|
|
def self.clear_core_cache!(targets)
|
|
cache.clear_regex(/#{targets.join("|")}/)
|
|
end
|
|
|
|
def self.clear_plugin_cache!(plugin)
|
|
cache.clear_regex(/#{plugin}/)
|
|
end
|
|
|
|
def self.color_scheme_cache_key(color_scheme, theme_id = nil, dark: false)
|
|
color_scheme_name = Slug.for(color_scheme.name) + color_scheme&.id.to_s
|
|
theme_string = theme_id ? "_theme#{theme_id}" : ""
|
|
dark_string = dark ? "_dark" : ""
|
|
"#{COLOR_SCHEME_STYLESHEET}_#{color_scheme_name}_#{theme_string}_#{Discourse.current_hostname}_#{GlobalSetting.relative_url_root}_#{dark_string}"
|
|
end
|
|
|
|
def self.precompile_css
|
|
targets = %i[common desktop mobile admin wizard]
|
|
targets += targets.map { |t| :"#{t}_rtl" }
|
|
|
|
targets +=
|
|
Discourse.find_plugin_css_assets(
|
|
include_disabled: true,
|
|
mobile_view: true,
|
|
desktop_view: true,
|
|
)
|
|
targets +=
|
|
Discourse.find_plugin_css_assets(
|
|
include_disabled: true,
|
|
mobile_view: true,
|
|
desktop_view: true,
|
|
rtl: true,
|
|
)
|
|
|
|
targets.each do |target|
|
|
$stderr.puts "precompile target: #{target}"
|
|
|
|
Stylesheet::Manager::Builder.new(target: target, manager: nil).compile(force: true)
|
|
end
|
|
end
|
|
|
|
def self.precompile_theme_css
|
|
themes =
|
|
Theme.where("user_selectable OR id = ?", SiteSetting.default_theme_id).pluck(
|
|
:id,
|
|
:color_scheme_id,
|
|
)
|
|
|
|
color_schemes = ColorScheme.where(user_selectable: true).to_a
|
|
color_schemes << ColorScheme.find_by(id: SiteSetting.default_dark_mode_color_scheme_id)
|
|
color_schemes << ColorScheme.base
|
|
color_schemes = color_schemes.compact.uniq
|
|
|
|
targets = %i[common_theme desktop_theme mobile_theme]
|
|
compiled = Set.new
|
|
|
|
themes.each do |theme_id, color_scheme_id|
|
|
manager = self.new(theme_id: theme_id)
|
|
|
|
targets.each do |target|
|
|
next if theme_id == -1
|
|
|
|
scss_checker = ScssChecker.new(target, manager.theme_ids)
|
|
|
|
manager
|
|
.load_themes(manager.theme_ids)
|
|
.each do |theme|
|
|
next if compiled.include?("#{target}_#{theme.id}")
|
|
|
|
builder =
|
|
Stylesheet::Manager::Builder.new(target: target, theme: theme, manager: manager)
|
|
|
|
next if !scss_checker.has_scss(theme.id)
|
|
$stderr.puts "precompile target: #{target} #{theme.name}"
|
|
builder.compile(force: true)
|
|
compiled << "#{target}_#{theme.id}"
|
|
end
|
|
end
|
|
|
|
theme_color_scheme = ColorScheme.find_by_id(color_scheme_id)
|
|
theme = manager.get_theme(theme_id)
|
|
|
|
[theme_color_scheme, *color_schemes].compact.uniq.each do |scheme|
|
|
[true, false].each do |dark|
|
|
mode = dark ? "dark" : "light"
|
|
$stderr.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{theme.name} (#{scheme.name}) (#{mode})"
|
|
Stylesheet::Manager::Builder.new(
|
|
target: COLOR_SCHEME_STYLESHEET,
|
|
theme: theme,
|
|
color_scheme: scheme,
|
|
manager: manager,
|
|
dark:,
|
|
).compile(force: true)
|
|
end
|
|
end
|
|
|
|
clear_color_scheme_cache!
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
def self.fs_asset_cachebuster
|
|
if use_file_hash_for_cachebuster?
|
|
@cachebuster ||=
|
|
if File.exist?(manifest_full_path)
|
|
File.readlines(manifest_full_path, "r")[0]
|
|
else
|
|
cachebuster = "#{BASE_CACHE_KEY}:#{fs_assets_hash}"
|
|
FileUtils.mkdir_p(MANIFEST_DIR)
|
|
File.open(manifest_full_path, "w") { |f| f.print(cachebuster) }
|
|
cachebuster
|
|
end
|
|
else
|
|
"#{BASE_CACHE_KEY}:#{max_file_mtime}"
|
|
end
|
|
end
|
|
|
|
def self.recalculate_fs_asset_cachebuster!
|
|
File.delete(manifest_full_path) if File.exist?(manifest_full_path)
|
|
@cachebuster = nil
|
|
fs_asset_cachebuster
|
|
end
|
|
|
|
def self.manifest_full_path
|
|
path = "#{MANIFEST_DIR}/stylesheet-manifest"
|
|
return path if !Rails.env.test?
|
|
"#{path}-test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}"
|
|
end
|
|
private_class_method :manifest_full_path
|
|
|
|
def self.use_file_hash_for_cachebuster?
|
|
Rails.env.production?
|
|
end
|
|
private_class_method :use_file_hash_for_cachebuster?
|
|
|
|
def self.list_files
|
|
globs = [
|
|
"#{Rails.root}/app/assets/stylesheets/**/*.*css",
|
|
"#{Rails.root}/app/assets/images/**/*.*",
|
|
"#{Rails.root}/lib/stylesheet/*.rb",
|
|
]
|
|
|
|
Discourse.plugins.each do |plugin|
|
|
path = File.dirname(plugin.path)
|
|
globs << "#{path}/plugin.rb"
|
|
globs << "#{path}/assets/stylesheets/**/*.*css"
|
|
end
|
|
|
|
globs.flat_map { |g| Dir.glob(g) }.compact
|
|
end
|
|
private_class_method :list_files
|
|
|
|
def self.max_file_mtime
|
|
list_files.map { |x| File.mtime(x) }.compact.max.to_i
|
|
end
|
|
private_class_method :max_file_mtime
|
|
|
|
def self.fs_assets_hash
|
|
hashes = list_files.sort.map { |x| Digest::SHA1.hexdigest("#{x}: #{File.read(x)}") }
|
|
Digest::SHA1.hexdigest(hashes.join("|"))
|
|
end
|
|
private_class_method :fs_assets_hash
|
|
|
|
def self.cache_fullpath
|
|
path = "#{Rails.root}/#{CACHE_PATH}"
|
|
return path if !Rails.env.test?
|
|
File.join(path, "test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}")
|
|
end
|
|
|
|
if Rails.env.test?
|
|
def self.rm_cache_folder
|
|
FileUtils.rm_rf(cache_fullpath)
|
|
end
|
|
end
|
|
|
|
attr_reader :theme_ids
|
|
|
|
def initialize(theme_id: nil)
|
|
@theme_id = theme_id
|
|
@theme_ids = Theme.transform_ids(@theme_id)
|
|
@themes_cache = {}
|
|
end
|
|
|
|
def cache
|
|
self.class.cache
|
|
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_preload_tag(target = :desktop, media = "all")
|
|
stylesheets = stylesheet_details(target, media)
|
|
stylesheets
|
|
.map do |stylesheet|
|
|
href = stylesheet[:new_href]
|
|
%[<link href="#{href}" rel="preload" as="style"/>]
|
|
end
|
|
.join("\n")
|
|
.html_safe
|
|
end
|
|
|
|
def stylesheet_link_tag(target = :desktop, media = "all", preload_callback = nil)
|
|
stylesheets = stylesheet_details(target, media)
|
|
stylesheets
|
|
.map do |stylesheet|
|
|
href = stylesheet[:new_href]
|
|
preload_callback.call(href, "style") if preload_callback
|
|
theme_id = stylesheet[:theme_id]
|
|
data_theme_id = theme_id ? "data-theme-id=\"#{theme_id}\"" : ""
|
|
theme_name = stylesheet[:theme_name]
|
|
data_theme_name = theme_name ? "data-theme-name=\"#{CGI.escapeHTML(theme_name)}\"" : ""
|
|
%[<link href="#{href}" media="#{media}" rel="stylesheet" data-target="#{target}" #{data_theme_id} #{data_theme_name}/>]
|
|
end
|
|
.join("\n")
|
|
.html_safe
|
|
end
|
|
|
|
def stylesheet_details(target = :desktop, media = "all")
|
|
target = target.to_sym
|
|
current_hostname = Discourse.current_hostname
|
|
relative_url_root = GlobalSetting.relative_url_root
|
|
is_theme_target = !!(target.to_s =~ THEME_REGEX)
|
|
|
|
array_cache_key =
|
|
(
|
|
if is_theme_target
|
|
"array_themes_#{@theme_ids.join(",")}_#{target}_#{current_hostname}_#{relative_url_root}"
|
|
else
|
|
"array_#{target}_#{current_hostname}_#{relative_url_root}"
|
|
end
|
|
)
|
|
|
|
cache.defer_get_set(array_cache_key) do
|
|
@@lock.synchronize do
|
|
stylesheets = []
|
|
|
|
if is_theme_target
|
|
scss_checker = ScssChecker.new(target, @theme_ids)
|
|
themes = load_themes(@theme_ids)
|
|
themes.each do |theme|
|
|
theme_id = theme&.id
|
|
data = {
|
|
target: target,
|
|
theme_id: theme_id,
|
|
theme_name: theme&.name&.downcase,
|
|
remote: theme.remote_theme_id?,
|
|
}
|
|
builder = Builder.new(target: target, theme: theme, manager: self)
|
|
|
|
next if !scss_checker.has_scss(theme_id)
|
|
builder.compile unless File.exist?(builder.stylesheet_fullpath)
|
|
href = builder.stylesheet_absolute_url
|
|
|
|
data[:new_href] = href
|
|
stylesheets << data
|
|
end
|
|
|
|
if stylesheets.size > 1
|
|
stylesheets =
|
|
stylesheets.sort_by do |s|
|
|
[s[:remote] ? 0 : 1, s[:theme_id] == @theme_id ? 1 : 0, s[:theme_name]]
|
|
end
|
|
end
|
|
else
|
|
builder = Builder.new(target: target, manager: self)
|
|
builder.compile unless File.exist?(builder.stylesheet_fullpath)
|
|
href = builder.stylesheet_absolute_url
|
|
|
|
data = { target: target, new_href: href }
|
|
stylesheets << data
|
|
end
|
|
|
|
stylesheets
|
|
end
|
|
end
|
|
end
|
|
|
|
def color_scheme_stylesheet_details(
|
|
color_scheme_id = nil,
|
|
dark: false,
|
|
fallback_to_base: true,
|
|
include_dark_scheme: false
|
|
)
|
|
theme_id = @theme_id || SiteSetting.default_theme_id
|
|
|
|
color_scheme = ColorScheme.find_by(id: color_scheme_id)
|
|
|
|
if !color_scheme
|
|
return if !fallback_to_base
|
|
color_scheme = get_theme(theme_id)&.color_scheme || ColorScheme.base
|
|
end
|
|
|
|
target = COLOR_SCHEME_STYLESHEET.to_sym
|
|
current_hostname = Discourse.current_hostname
|
|
cache_key = self.class.color_scheme_cache_key(color_scheme, theme_id, dark:)
|
|
|
|
cache.defer_get_set(cache_key) do
|
|
stylesheet = { color_scheme_id: color_scheme.id, dark: }
|
|
|
|
theme = get_theme(theme_id)
|
|
|
|
builder =
|
|
Builder.new(
|
|
target: target,
|
|
theme: get_theme(theme_id),
|
|
color_scheme: color_scheme,
|
|
manager: self,
|
|
dark:,
|
|
)
|
|
|
|
builder.compile unless File.exist?(builder.stylesheet_fullpath)
|
|
|
|
href = builder.stylesheet_absolute_url
|
|
stylesheet[:new_href] = href
|
|
|
|
if include_dark_scheme
|
|
dark_href =
|
|
self.color_scheme_stylesheet_link_tag_href(
|
|
color_scheme_id,
|
|
dark: true,
|
|
fallback_to_base: false,
|
|
)
|
|
|
|
stylesheet[:new_dark_href] = dark_href if dark_href
|
|
end
|
|
|
|
stylesheet.freeze
|
|
end
|
|
end
|
|
|
|
def color_scheme_stylesheet_preload_tag(
|
|
color_scheme_id = nil,
|
|
dark: false,
|
|
fallback_to_base: true
|
|
)
|
|
stylesheet = color_scheme_stylesheet_details(color_scheme_id, dark:, fallback_to_base:)
|
|
|
|
return "" if !stylesheet
|
|
|
|
href = stylesheet[:new_href]
|
|
|
|
%[<link href="#{href}" rel="preload" as="style"/>].html_safe
|
|
end
|
|
|
|
def color_scheme_stylesheet_link_tag_href(
|
|
color_scheme_id = nil,
|
|
dark: false,
|
|
fallback_to_base: true
|
|
)
|
|
stylesheet = color_scheme_stylesheet_details(color_scheme_id, dark:, fallback_to_base:)
|
|
|
|
return if !stylesheet
|
|
|
|
stylesheet[:new_href]
|
|
end
|
|
end
|