Class: StyleCapsule::CssFileWriter
- Inherits:
-
Object
- Object
- StyleCapsule::CssFileWriter
- Defined in:
- lib/style_capsule/css_file_writer.rb,
sig/style_capsule.rbs
Overview
Writes inline CSS to files for HTTP caching
This allows inline CSS to be cached by browsers and CDNs, improving performance. Files are written to a configurable output directory and can be precompiled via Rails asset pipeline.
In production environments where the app directory is read-only (e.g., Docker containers), this class automatically falls back to writing files to /tmp/style_capsule when the default location is not writable. When using the fallback directory, write_css returns nil, causing StylesheetRegistry to fall back to inline CSS (keeping the UI functional).
All fallback scenarios are instrumented via ActiveSupport::Notifications following Rails conventions (https://guides.rubyonrails.org/active_support_instrumentation.html):
- style_capsule.css_file_writer.fallback: When fallback directory is used successfully
- style_capsule.css_file_writer.fallback_failure: When both primary and fallback fail
- style_capsule.css_file_writer.write_failure: When other write errors occur
All events include exception information in the standard Rails format:
- :exception: Array of [class_name, message]
- :exception_object: The exception object itself
These events can be subscribed to for monitoring, metrics collection, and error reporting.
Constant Summary collapse
- DEFAULT_OUTPUT_DIR =
Default output directory for CSS files (relative to Rails root)
"app/assets/builds/capsules"- FALLBACK_OUTPUT_DIR =
Fallback directory for when default location is read-only (absolute path)
"/tmp/style_capsule"- FILE_PATH_CACHE_MAX =
Positive cache for resolved asset-relative paths (avoids repeated File.exist? on hot path)
512
Class Attribute Summary collapse
-
.enabled ⇒ Object
Returns the value of attribute enabled.
-
.fallback_dir ⇒ Object
Returns the value of attribute fallback_dir.
-
.filename_pattern ⇒ Object
Returns the value of attribute filename_pattern.
-
.output_dir ⇒ Object
Returns the value of attribute output_dir.
Class Method Summary collapse
-
.clear_files ⇒ void
Clear all generated CSS files.
-
.configure(output_dir: nil, filename_pattern: nil, enabled: true, fallback_dir: nil) ⇒ void
Configure CSS file writer.
-
.enabled? ⇒ Boolean
Check if file writing is enabled.
-
.ensure_fallback_directory ⇒ void
Ensure fallback directory exists.
-
.ensure_output_directory ⇒ void
Ensure output directory exists.
-
.file_exists?(component_class:, capsule_id:) ⇒ Boolean
Check if file exists for given component and capsule.
-
.file_path_for(component_class:, capsule_id:) ⇒ String?
Get file path for given component and capsule.
-
.rails_available? ⇒ Boolean
Check if Rails is available This method can be stubbed in tests to test fallback paths.
- .resolve_output_dir(output_dir) ⇒ Object
- .resolve_path(dir, default_path) ⇒ Object
-
.write_css(css_content:, component_class:, capsule_id:) ⇒ String?
Write CSS content to file.
Class Attribute Details
.enabled ⇒ Object
Returns the value of attribute enabled.
82 83 84 |
# File 'lib/style_capsule/css_file_writer.rb', line 82 def enabled @enabled end |
.fallback_dir ⇒ Object
Returns the value of attribute fallback_dir.
82 83 84 |
# File 'lib/style_capsule/css_file_writer.rb', line 82 def fallback_dir @fallback_dir end |
.filename_pattern ⇒ Object
Returns the value of attribute filename_pattern.
82 83 84 |
# File 'lib/style_capsule/css_file_writer.rb', line 82 def filename_pattern @filename_pattern end |
.output_dir ⇒ Object
Returns the value of attribute output_dir.
82 83 84 |
# File 'lib/style_capsule/css_file_writer.rb', line 82 def output_dir @output_dir end |
Class Method Details
.clear_files ⇒ void
This method returns an undefined value.
Clear all generated CSS files
267 268 269 270 271 272 273 274 275 |
# File 'lib/style_capsule/css_file_writer.rb', line 267 def clear_files return unless enabled? clear_file_path_hit_cache! dir = output_directory return unless Dir.exist?(dir) Dir.glob(dir.join("*.css")).each { |file| File.delete(file) } end |
.configure(output_dir: nil, filename_pattern: nil, enabled: true, fallback_dir: nil) ⇒ void
This method returns an undefined value.
Configure CSS file writer
103 104 105 106 107 108 109 |
# File 'lib/style_capsule/css_file_writer.rb', line 103 def configure(output_dir: nil, filename_pattern: nil, enabled: true, fallback_dir: nil) clear_file_path_hit_cache! @enabled = enabled @output_dir = resolve_output_dir(output_dir) @fallback_dir = resolve_path(fallback_dir, FALLBACK_OUTPUT_DIR) @filename_pattern = filename_pattern || default_filename_pattern end |
.enabled? ⇒ Boolean
Check if file writing is enabled
280 281 282 |
# File 'lib/style_capsule/css_file_writer.rb', line 280 def enabled? @enabled != false end |
.ensure_fallback_directory ⇒ void
This method returns an undefined value.
Ensure fallback directory exists
257 258 259 260 261 262 |
# File 'lib/style_capsule/css_file_writer.rb', line 257 def ensure_fallback_directory return unless enabled? dir = fallback_directory FileUtils.mkdir_p(dir) unless Dir.exist?(dir) end |
.ensure_output_directory ⇒ void
This method returns an undefined value.
Ensure output directory exists
247 248 249 250 251 252 |
# File 'lib/style_capsule/css_file_writer.rb', line 247 def ensure_output_directory return unless enabled? dir = output_directory FileUtils.mkdir_p(dir) unless Dir.exist?(dir) end |
.file_exists?(component_class:, capsule_id:) ⇒ Boolean
Check if file exists for given component and capsule
212 213 214 215 216 217 218 |
# File 'lib/style_capsule/css_file_writer.rb', line 212 def file_exists?(component_class:, capsule_id:) return false unless enabled? filename = generate_filename(component_class, capsule_id) file_path = output_directory.join(filename) File.exist?(file_path) end |
.file_path_for(component_class:, capsule_id:) ⇒ String?
Get file path for given component and capsule
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/style_capsule/css_file_writer.rb', line 225 def file_path_for(component_class:, capsule_id:) return nil unless enabled? cache_key = "#{component_class.name}:#{capsule_id}" hit = file_path_hit_cache_mutex.synchronize { file_path_hit_cache[cache_key] } return hit if hit filename = generate_filename(component_class, capsule_id) file_path = output_directory.join(filename) return nil unless File.exist?(file_path) # Return relative path for stylesheet_link_tag # Handle case where output directory is not under rails_assets_root (e.g., in tests) rel = asset_logical_path(file_path, filename) remember_file_path_hit(cache_key, rel) rel end |
.rails_available? ⇒ Boolean
Check if Rails is available This method can be stubbed in tests to test fallback paths
286 287 288 |
# File 'lib/style_capsule/css_file_writer.rb', line 286 def rails_available? defined?(Rails) && Rails.respond_to?(:root) && Rails.root end |
.resolve_output_dir(output_dir) ⇒ Object
111 112 113 114 115 116 |
# File 'lib/style_capsule/css_file_writer.rb', line 111 def resolve_output_dir(output_dir) return resolve_path(output_dir, DEFAULT_OUTPUT_DIR) if output_dir return Rails.root.join(DEFAULT_OUTPUT_DIR) if rails_available? Pathname.new(DEFAULT_OUTPUT_DIR) end |
.resolve_path(dir, default_path) ⇒ Object
118 119 120 121 122 |
# File 'lib/style_capsule/css_file_writer.rb', line 118 def resolve_path(dir, default_path) return Pathname.new(default_path) unless dir dir.is_a?(Pathname) ? dir : Pathname.new(dir.to_s) end |
.write_css(css_content:, component_class:, capsule_id:) ⇒ String?
Write CSS content to file
rubocop:disable Metrics/AbcSize -- writes asset file with fallback and instrumentation paths
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/style_capsule/css_file_writer.rb', line 131 def write_css(css_content:, component_class:, capsule_id:) return nil unless enabled? filename = generate_filename(component_class, capsule_id) file_path = output_directory.join(filename) used_fallback = false begin ensure_output_directory # Write CSS to file with explicit UTF-8 encoding Instrumentation.instrument_file_write( component_class: component_class, capsule_id: capsule_id, file_path: file_path.to_s, size: css_content.bytesize ) do File.write(file_path, css_content, encoding: "UTF-8") end rescue Errno::EACCES, Errno::EROFS => e # Permission denied or read-only filesystem - try fallback directory fallback_path = fallback_directory.join(filename) begin ensure_fallback_directory File.write(fallback_path, css_content, encoding: "UTF-8") used_fallback = true file_path = fallback_path # Instrument the fallback for visibility Instrumentation.instrument_fallback( component_class: component_class, capsule_id: capsule_id, original_path: output_directory.join(filename).to_s, fallback_path: fallback_path.to_s, exception: [e.class.name, e.], exception_object: e ) rescue => fallback_error # Even fallback failed - instrument and return nil (will fall back to inline CSS) Instrumentation.instrument_fallback_failure( component_class: component_class, capsule_id: capsule_id, original_path: output_directory.join(filename).to_s, fallback_path: fallback_path.to_s, original_exception: [e.class.name, e.], original_exception_object: e, fallback_exception: [fallback_error.class.name, fallback_error.], fallback_exception_object: fallback_error ) return nil end rescue => e # Other errors - instrument and return nil (will fall back to inline CSS) Instrumentation.instrument_write_failure( component_class: component_class, capsule_id: capsule_id, file_path: file_path.to_s, exception: [e.class.name, e.], exception_object: e ) return nil end # If we used fallback directory, return nil (can't serve via asset pipeline) # This will cause StylesheetRegistry to fall back to inline CSS return nil if used_fallback # Return relative path for stylesheet_link_tag # Path should be relative to app/assets # Handle case where output directory is not under rails_assets_root (e.g., in tests) rel = asset_logical_path(file_path, filename) remember_file_path_hit("#{component_class.name}:#{capsule_id}", rel) rel end |