Class: Stylesheet::Manager::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/stylesheet/manager/builder.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(target: :desktop, theme: nil, color_scheme: nil, manager:) ⇒ Builder

Returns a new instance of Builder.



6
7
8
9
10
11
# File 'lib/stylesheet/manager/builder.rb', line 6

def initialize(target: :desktop, theme: nil, color_scheme: nil, manager:)
  @target = target
  @theme = theme
  @color_scheme = color_scheme
  @manager = manager
end

Instance Attribute Details

#themeObject (readonly)

Returns the value of attribute theme.



4
5
6
# File 'lib/stylesheet/manager/builder.rb', line 4

def theme
  @theme
end

Instance Method Details

#cache_fullpathObject



81
82
83
# File 'lib/stylesheet/manager/builder.rb', line 81

def cache_fullpath
  Stylesheet::Manager.cache_fullpath
end

#color_scheme_digestObject



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/stylesheet/manager/builder.rb', line 240

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

#compile(opts = {}) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/stylesheet/manager/builder.rb', line 13

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.end_with?("_rtl")
  css, source_map =
    with_load_paths do |load_paths|
      Stylesheet::Compiler.compile_asset(
        @target.to_s.gsub(/_rtl\z/, "").to_sym,
        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

#current_hostnameObject



77
78
79
# File 'lib/stylesheet/manager/builder.rb', line 77

def current_hostname
  Discourse.current_hostname
end

#default_digestObject



236
237
238
# File 'lib/stylesheet/manager/builder.rb', line 236

def default_digest
  Digest::SHA1.hexdigest "default-#{Stylesheet::Manager.fs_asset_cachebuster}-#{plugins_digest}-#{current_hostname}"
end

#digestObject

digest encodes the things that trigger a recompile



154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/stylesheet/manager/builder.rb', line 154

def digest
  @digest ||=
    begin
      if is_theme?
        theme_digest
      elsif is_color_scheme?
        color_scheme_digest
      else
        default_digest
      end
    end
end

#is_color_scheme?Boolean

Returns:

  • (Boolean)


145
146
147
# File 'lib/stylesheet/manager/builder.rb', line 145

def is_color_scheme?
  !!(@target.to_s == Stylesheet::Manager::COLOR_SCHEME_STYLESHEET)
end

#is_theme?Boolean

Returns:

  • (Boolean)


141
142
143
# File 'lib/stylesheet/manager/builder.rb', line 141

def is_theme?
  !!(@target.to_s =~ Stylesheet::Manager::THEME_REGEX)
end

#plugins_digestObject

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



195
196
197
198
199
200
201
# File 'lib/stylesheet/manager/builder.rb', line 195

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

#qualified_targetObject



121
122
123
124
125
126
127
128
129
130
# File 'lib/stylesheet/manager/builder.rb', line 121

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

#resolve_baked_field(target, name) ⇒ Object



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/stylesheet/manager/builder.rb', line 267

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

#root_pathObject



109
110
111
# File 'lib/stylesheet/manager/builder.rb', line 109

def root_path
  "#{GlobalSetting.relative_url_root}/"
end

#scheme_slugObject



149
150
151
# File 'lib/stylesheet/manager/builder.rb', line 149

def scheme_slug
  Slug.for(ActiveSupport::Inflector.transliterate(@color_scheme.name), "scheme")
end

#scss_digestObject



175
176
177
178
179
180
181
182
183
# File 'lib/stylesheet/manager/builder.rb', line 175

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

#settings_digestObject



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/stylesheet/manager/builder.rb', line 203

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

#source_map_filenameObject



93
94
95
# File 'lib/stylesheet/manager/builder.rb', line 93

def source_map_filename
  "#{stylesheet_filename}.map"
end

#source_map_fullpathObject



89
90
91
# File 'lib/stylesheet/manager/builder.rb', line 89

def source_map_fullpath
  "#{cache_fullpath}/#{source_map_filename}"
end

#source_map_url_relative_from_stylesheetObject



97
98
99
# File 'lib/stylesheet/manager/builder.rb', line 97

def source_map_url_relative_from_stylesheet
  "#{source_map_filename}?__ws=#{current_hostname}"
end

#stylesheet_absolute_urlObject



105
106
107
# File 'lib/stylesheet/manager/builder.rb', line 105

def stylesheet_absolute_url
  "#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{current_hostname}"
end

#stylesheet_filename(with_digest = true) ⇒ Object



132
133
134
135
# File 'lib/stylesheet/manager/builder.rb', line 132

def stylesheet_filename(with_digest = true)
  digest_string = "_#{self.digest}" if with_digest
  "#{qualified_target}#{digest_string}.css"
end

#stylesheet_filename_no_digestObject



137
138
139
# File 'lib/stylesheet/manager/builder.rb', line 137

def stylesheet_filename_no_digest
  stylesheet_filename(_with_digest = false)
end

#stylesheet_fullpathObject



85
86
87
# File 'lib/stylesheet/manager/builder.rb', line 85

def stylesheet_fullpath
  "#{cache_fullpath}/#{stylesheet_filename}"
end

#stylesheet_fullpath_no_digestObject



101
102
103
# File 'lib/stylesheet/manager/builder.rb', line 101

def stylesheet_fullpath_no_digest
  "#{cache_fullpath}/#{stylesheet_filename_no_digest}"
end

#stylesheet_relpathObject



113
114
115
# File 'lib/stylesheet/manager/builder.rb', line 113

def stylesheet_relpath
  "#{root_path}stylesheets/#{stylesheet_filename}"
end

#stylesheet_relpath_no_digestObject



117
118
119
# File 'lib/stylesheet/manager/builder.rb', line 117

def stylesheet_relpath_no_digest
  "#{root_path}stylesheets/#{stylesheet_filename_no_digest}"
end

#theme_digestObject



185
186
187
188
189
190
# File 'lib/stylesheet/manager/builder.rb', line 185

def theme_digest
  Digest::SHA1.hexdigest(
    scss_digest.to_s + color_scheme_digest.to_s + settings_digest + uploads_digest +
      current_hostname,
  )
end

#uploads_digestObject



228
229
230
231
232
233
234
# File 'lib/stylesheet/manager/builder.rb', line 228

def uploads_digest
  sha1s = []

  (theme&.upload_fields || []).map { |upload_field| sha1s << upload_field.upload&.sha1 }

  Digest::SHA1.hexdigest(sha1s.compact.sort!.join("\n"))
end

#with_load_pathsObject



167
168
169
170
171
172
173
# File 'lib/stylesheet/manager/builder.rb', line 167

def with_load_paths
  if theme
    theme.with_scss_load_paths { |p| yield p }
  else
    yield nil
  end
end