style_capsule 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +71 -2
- data/SECURITY.md +16 -43
- data/lib/style_capsule/component.rb +2 -2
- data/lib/style_capsule/css_file_writer.rb +137 -9
- data/lib/style_capsule/css_processor.rb +80 -60
- data/lib/style_capsule/instrumentation.rb +248 -0
- data/lib/style_capsule/version.rb +1 -1
- data/lib/style_capsule.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0dc57ce596c8aec7767199f8c57b82db16a8f96b33ac06cba599febd89afa86a
|
|
4
|
+
data.tar.gz: b4767a7eed2a864357027386452ea6a2da24577be3376388569c8ceb61bff937
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a666c80dc9ddc70179e9992bc4a1c0c9b58a295e21e0fc64441227529c4b401fdc7c7a703af5e87403ca407d0e482a5b97c73b1cc81d385a021cea2e1186c8d5
|
|
7
|
+
data.tar.gz: 0d8523c1659c5467b6e0a1aa6fc1268f999f669772a2ff22b66b386ec5c6ed19d85a9b9dc2b915f744d691ad2d9ccaa2dd7bc33f5c132a3c7d1fe1df073dcd41
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.3.0 (2025-11-26)
|
|
4
|
+
|
|
5
|
+
- Added comprehensive instrumentation via `StyleCapsule::Instrumentation` using ActiveSupport::Notifications
|
|
6
|
+
- Instrumentation events for CSS processing (`style_capsule.css_processor.scope`) with duration and size metrics
|
|
7
|
+
- Instrumentation events for CSS file writing (`style_capsule.css_file_writer.write`) with duration and size metrics
|
|
8
|
+
- Added fallback directory support for CSS file writing when default location is read-only (e.g., Docker containers)
|
|
9
|
+
- Automatic fallback to `/tmp/style_capsule` when primary output directory is not writable
|
|
10
|
+
- Instrumentation events for fallback scenarios (`style_capsule.css_file_writer.fallback`, `style_capsule.css_file_writer.fallback_failure`)
|
|
11
|
+
- Instrumentation events for write failures (`style_capsule.css_file_writer.write_failure`)
|
|
12
|
+
- All instrumentation is zero-overhead when no subscribers are present (only calculates metrics when actively monitored)
|
|
13
|
+
- Improved test coverage reporting and analysis tools
|
|
14
|
+
- Added community guidelines and governance documents
|
|
15
|
+
|
|
3
16
|
## 1.2.0 (2025-11-24)
|
|
4
17
|
|
|
5
18
|
- Added `StyleCapsule::ClassRegistry` for Rails-friendly class tracking without ObjectSpace iteration
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# style_capsule
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/style_capsule) [](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml) [](https://codecov.io/gh/amkisko/style_capsule.rb)
|
|
4
4
|
|
|
5
5
|
CSS scoping extension for Ruby components. Provides attribute-based style encapsulation for Phlex, ViewComponent, and ERB templates to prevent style leakage between components. Works with Rails and can be used standalone in other Ruby frameworks (Sinatra, Hanami, etc.) or plain Ruby scripts. Includes configurable caching strategies for optimal performance.
|
|
6
6
|
|
|
@@ -28,6 +28,8 @@ Then run `bundle install`.
|
|
|
28
28
|
- **CSS Nesting support** (optional, more performant, requires modern browsers)
|
|
29
29
|
- **Stylesheet registry** with thread-safe head rendering, namespace support, and compatibility with Propshaft and other asset bundlers
|
|
30
30
|
- **Multiple cache strategies**: none, time-based, custom proc, and file-based (HTTP caching)
|
|
31
|
+
- **Comprehensive instrumentation** via ActiveSupport::Notifications for monitoring and metrics
|
|
32
|
+
- **Fallback directory support** for read-only filesystems (e.g., Docker containers)
|
|
31
33
|
- **Security protections**: path traversal protection, input validation, size limits
|
|
32
34
|
|
|
33
35
|
## Usage
|
|
@@ -265,10 +267,13 @@ StyleCapsule::CssFileWriter.configure(
|
|
|
265
267
|
output_dir: Rails.root.join("app/assets/builds/capsules"),
|
|
266
268
|
filename_pattern: ->(component_class, capsule_id) {
|
|
267
269
|
"capsule-#{capsule_id}.css"
|
|
268
|
-
}
|
|
270
|
+
},
|
|
271
|
+
fallback_dir: "/tmp/style_capsule" # Optional, defaults to /tmp/style_capsule
|
|
269
272
|
)
|
|
270
273
|
```
|
|
271
274
|
|
|
275
|
+
**Fallback Directory:** In production environments where the app directory is read-only (e.g., Docker containers), StyleCapsule automatically falls back to writing files to `/tmp/style_capsule` when the default location is not writable. When using the fallback directory, the gem gracefully falls back to inline CSS rendering, keeping the UI fully functional.
|
|
276
|
+
|
|
272
277
|
**Precompilation:**
|
|
273
278
|
|
|
274
279
|
```bash
|
|
@@ -280,6 +285,70 @@ Files are automatically built during `bin/rails assets:precompile`.
|
|
|
280
285
|
|
|
281
286
|
**Compatibility:** The stylesheet registry works with Propshaft, Sprockets, and other Rails asset bundlers. Static file paths are collected in a process-wide manifest (similar to Propshaft's approach), while inline CSS is stored per-request.
|
|
282
287
|
|
|
288
|
+
## Instrumentation
|
|
289
|
+
|
|
290
|
+
StyleCapsule provides comprehensive instrumentation via `ActiveSupport::Notifications` for monitoring CSS processing and file writing operations. All instrumentation is zero-overhead when no subscribers are present.
|
|
291
|
+
|
|
292
|
+
### Available Events
|
|
293
|
+
|
|
294
|
+
- `style_capsule.css_processor.scope` - CSS scoping operations with duration and size metrics
|
|
295
|
+
- `style_capsule.css_file_writer.write` - CSS file write operations with duration and size metrics
|
|
296
|
+
- `style_capsule.css_file_writer.fallback` - When fallback directory is used (read-only filesystem)
|
|
297
|
+
- `style_capsule.css_file_writer.fallback_failure` - When both primary and fallback directories fail
|
|
298
|
+
- `style_capsule.css_file_writer.write_failure` - Other write errors
|
|
299
|
+
|
|
300
|
+
### Example: Monitoring CSS Processing
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
# config/initializers/style_capsule.rb
|
|
304
|
+
ActiveSupport::Notifications.subscribe("style_capsule.css_processor.scope") do |name, start, finish, id, payload|
|
|
305
|
+
duration_ms = (finish - start) * 1000
|
|
306
|
+
Rails.logger.info "CSS scoped in #{duration_ms.round(2)}ms, input: #{payload[:input_size]} bytes, output: #{payload[:output_size]} bytes"
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Example: Monitoring File Writes
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.write") do |name, start, finish, id, payload|
|
|
314
|
+
duration_ms = (finish - start) * 1000
|
|
315
|
+
StatsD.timing("style_capsule.write.duration", duration_ms)
|
|
316
|
+
StatsD.histogram("style_capsule.write.size", payload[:size])
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Example: Monitoring Fallback Scenarios
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.fallback") do |name, start, finish, id, payload|
|
|
324
|
+
Rails.logger.warn "StyleCapsule fallback used: #{payload[:component_class]} -> #{payload[:fallback_path]}"
|
|
325
|
+
# Exception info available: payload[:exception] and payload[:exception_object]
|
|
326
|
+
StatsD.increment("style_capsule.css_file_writer.fallback", tags: [
|
|
327
|
+
"component:#{payload[:component_class]}",
|
|
328
|
+
"error:#{payload[:exception].first}"
|
|
329
|
+
])
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Example: Error Reporting
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.fallback_failure") do |name, start, finish, id, payload|
|
|
337
|
+
ActionReporter.notify(
|
|
338
|
+
"StyleCapsule: CSS write failure (both primary and fallback failed)",
|
|
339
|
+
context: {
|
|
340
|
+
component_class: payload[:component_class],
|
|
341
|
+
original_path: payload[:original_path],
|
|
342
|
+
fallback_path: payload[:fallback_path],
|
|
343
|
+
original_exception: payload[:original_exception],
|
|
344
|
+
fallback_exception: payload[:fallback_exception]
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
For more details, see the [ActiveSupport::Notifications documentation](https://guides.rubyonrails.org/active_support_instrumentation.html).
|
|
351
|
+
|
|
283
352
|
## Advanced Usage
|
|
284
353
|
|
|
285
354
|
### Database-Stored CSS
|
data/SECURITY.md
CHANGED
|
@@ -1,55 +1,28 @@
|
|
|
1
1
|
# SECURITY
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
| ------- | ------------------ |
|
|
7
|
-
| 1.0.x | :white_check_mark: |
|
|
8
|
-
| < 1.0 | :x: |
|
|
5
|
+
**Do NOT** open a public GitHub issue for security vulnerabilities.
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
Email security details to: **security@kiskolabs.com**
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
Include: description, steps to reproduce, potential impact, and suggested fix (if available).
|
|
13
10
|
|
|
14
|
-
###
|
|
11
|
+
### Response Timeline
|
|
15
12
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
13
|
+
- We will acknowledge receipt of your report
|
|
14
|
+
- We will provide an initial assessment
|
|
15
|
+
- We will keep you informed of our progress and resolution timeline
|
|
19
16
|
|
|
20
|
-
###
|
|
17
|
+
### Disclosure Policy
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
19
|
+
- We will work with you to understand and resolve the issue
|
|
20
|
+
- We will credit you for the discovery (unless you prefer to remain anonymous)
|
|
21
|
+
- We will publish a security advisory after the vulnerability is patched
|
|
22
|
+
- We will coordinate public disclosure with you
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
- HTML escaping is handled by Rails' built-in helpers (`content_tag`, etc.)
|
|
29
|
-
- Content Security Policy (CSP) must be configured at the application level
|
|
30
|
-
- File system permissions and access control are managed by the application
|
|
24
|
+
## Automation Security
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
* **Context Isolation:** It is strictly forbidden to include production credentials, API keys, or Personally Identifiable Information (PII) in prompts sent to third-party LLMs or automation services.
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
2. **Use Content Security Policy**: Configure CSP headers to restrict inline styles and external resources
|
|
36
|
-
3. **Sanitize User-Generated CSS**: If allowing user input, sanitize CSS before processing
|
|
37
|
-
4. **Keep Dependencies Updated**: Use supported Ruby (>= 3.0) and Rails versions with security patches
|
|
38
|
-
5. **Review Generated Files**: Periodically review files in `app/assets/builds/capsules/` if using file-based caching
|
|
39
|
-
|
|
40
|
-
### Reporting a Vulnerability
|
|
41
|
-
|
|
42
|
-
If you discover a security vulnerability, please **do not** open a public issue. Instead:
|
|
43
|
-
|
|
44
|
-
1. **Email**: contact@kiskolabs.com
|
|
45
|
-
2. **Subject**: `[SECURITY] style_capsule vulnerability report`
|
|
46
|
-
3. **Include**: Description, steps to reproduce, potential impact, and suggested fix (if any)
|
|
47
|
-
|
|
48
|
-
We will acknowledge receipt within 48 hours and provide an initial assessment within 7 days.
|
|
49
|
-
|
|
50
|
-
### Security Updates
|
|
51
|
-
|
|
52
|
-
Security updates are released as patch versions (e.g., 1.0.1, 1.0.2) and announced via:
|
|
53
|
-
- GitHub Security Advisories
|
|
54
|
-
- RubyGems release notes
|
|
55
|
-
- CHANGELOG.md
|
|
28
|
+
* **Supply Chain:** All automated dependencies must be verified.
|
|
@@ -463,9 +463,9 @@ module StyleCapsule
|
|
|
463
463
|
# Use the configured scoping strategy
|
|
464
464
|
scoped_css = case scoping_strategy
|
|
465
465
|
when :nesting
|
|
466
|
-
CssProcessor.scope_with_nesting(css_content, capsule_id)
|
|
466
|
+
CssProcessor.scope_with_nesting(css_content, capsule_id, component_class: self.class)
|
|
467
467
|
else # :selector_patching (default)
|
|
468
|
-
CssProcessor.scope_selectors(css_content, capsule_id)
|
|
468
|
+
CssProcessor.scope_selectors(css_content, capsule_id, component_class: self.class)
|
|
469
469
|
end
|
|
470
470
|
|
|
471
471
|
# Cache at class level (one style block per component type/scope/strategy combination)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "digest/sha1"
|
|
5
|
+
require_relative "instrumentation"
|
|
5
6
|
|
|
6
7
|
module StyleCapsule
|
|
7
8
|
# Writes inline CSS to files for HTTP caching
|
|
@@ -10,10 +11,28 @@ module StyleCapsule
|
|
|
10
11
|
# Files are written to a configurable output directory and can be precompiled
|
|
11
12
|
# via Rails asset pipeline.
|
|
12
13
|
#
|
|
14
|
+
# In production environments where the app directory is read-only (e.g., Docker containers),
|
|
15
|
+
# this class automatically falls back to writing files to /tmp/style_capsule when the
|
|
16
|
+
# default location is not writable. When using the fallback directory, write_css returns
|
|
17
|
+
# nil, causing StylesheetRegistry to fall back to inline CSS (keeping the UI functional).
|
|
18
|
+
#
|
|
19
|
+
# All fallback scenarios are instrumented via ActiveSupport::Notifications following
|
|
20
|
+
# Rails conventions (https://guides.rubyonrails.org/active_support_instrumentation.html):
|
|
21
|
+
# - style_capsule.css_file_writer.fallback: When fallback directory is used successfully
|
|
22
|
+
# - style_capsule.css_file_writer.fallback_failure: When both primary and fallback fail
|
|
23
|
+
# - style_capsule.css_file_writer.write_failure: When other write errors occur
|
|
24
|
+
#
|
|
25
|
+
# All events include exception information in the standard Rails format:
|
|
26
|
+
# - :exception: Array of [class_name, message]
|
|
27
|
+
# - :exception_object: The exception object itself
|
|
28
|
+
#
|
|
29
|
+
# These events can be subscribed to for monitoring, metrics collection, and error reporting.
|
|
30
|
+
#
|
|
13
31
|
# @example Configuration
|
|
14
32
|
# StyleCapsule::CssFileWriter.configure(
|
|
15
33
|
# output_dir: Rails.root.join(StyleCapsule::CssFileWriter::DEFAULT_OUTPUT_DIR),
|
|
16
|
-
# filename_pattern: ->(component_class, capsule_id) { "capsule-#{capsule_id}.css" }
|
|
34
|
+
# filename_pattern: ->(component_class, capsule_id) { "capsule-#{capsule_id}.css" },
|
|
35
|
+
# fallback_dir: "/tmp/style_capsule" # Optional, defaults to /tmp/style_capsule
|
|
17
36
|
# )
|
|
18
37
|
#
|
|
19
38
|
# @example Usage
|
|
@@ -22,13 +41,43 @@ module StyleCapsule
|
|
|
22
41
|
# component_class: MyComponent,
|
|
23
42
|
# capsule_id: "abc123"
|
|
24
43
|
# )
|
|
25
|
-
# # => "capsules/capsule-abc123"
|
|
44
|
+
# # => "capsules/capsule-abc123" (or nil if fallback was used)
|
|
45
|
+
#
|
|
46
|
+
# @example Listening to instrumentation events for monitoring
|
|
47
|
+
# ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.fallback") do |name, start, finish, id, payload|
|
|
48
|
+
# Rails.logger.warn "StyleCapsule fallback: #{payload[:component_class]} -> #{payload[:fallback_path]}"
|
|
49
|
+
# # Exception info available: payload[:exception] and payload[:exception_object]
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# @example Subscribing for error reporting
|
|
53
|
+
# ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.fallback_failure") do |name, start, finish, id, payload|
|
|
54
|
+
# ActionReporter.notify(
|
|
55
|
+
# "StyleCapsule: CSS write failure (both primary and fallback failed)",
|
|
56
|
+
# context: {
|
|
57
|
+
# component_class: payload[:component_class],
|
|
58
|
+
# original_path: payload[:original_path],
|
|
59
|
+
# fallback_path: payload[:fallback_path],
|
|
60
|
+
# original_exception: payload[:original_exception],
|
|
61
|
+
# fallback_exception: payload[:fallback_exception]
|
|
62
|
+
# }
|
|
63
|
+
# )
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# @example Subscribing for metrics collection
|
|
67
|
+
# ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.fallback") do |name, start, finish, id, payload|
|
|
68
|
+
# StatsD.increment("style_capsule.css_file_writer.fallback", tags: [
|
|
69
|
+
# "component:#{payload[:component_class]}",
|
|
70
|
+
# "error:#{payload[:exception].first}"
|
|
71
|
+
# ])
|
|
72
|
+
# end
|
|
26
73
|
class CssFileWriter
|
|
27
74
|
# Default output directory for CSS files (relative to Rails root)
|
|
28
75
|
DEFAULT_OUTPUT_DIR = "app/assets/builds/capsules"
|
|
76
|
+
# Fallback directory for when default location is read-only (absolute path)
|
|
77
|
+
FALLBACK_OUTPUT_DIR = "/tmp/style_capsule"
|
|
29
78
|
|
|
30
79
|
class << self
|
|
31
|
-
attr_accessor :output_dir, :filename_pattern, :enabled
|
|
80
|
+
attr_accessor :output_dir, :filename_pattern, :enabled, :fallback_dir
|
|
32
81
|
|
|
33
82
|
# Configure CSS file writer
|
|
34
83
|
#
|
|
@@ -38,6 +87,8 @@ module StyleCapsule
|
|
|
38
87
|
# Receives: (component_class, capsule_id) and should return filename string
|
|
39
88
|
# Default: `"capsule-#{capsule_id}.css"` (capsule_id is unique and deterministic)
|
|
40
89
|
# @param enabled [Boolean] Whether file writing is enabled (default: true)
|
|
90
|
+
# @param fallback_dir [String, Pathname, nil] Fallback directory when default location is read-only
|
|
91
|
+
# Default: `StyleCapsule::CssFileWriter::FALLBACK_OUTPUT_DIR` (/tmp/style_capsule)
|
|
41
92
|
# @example
|
|
42
93
|
# StyleCapsule::CssFileWriter.configure(
|
|
43
94
|
# output_dir: Rails.root.join(StyleCapsule::CssFileWriter::DEFAULT_OUTPUT_DIR),
|
|
@@ -47,7 +98,7 @@ module StyleCapsule
|
|
|
47
98
|
# StyleCapsule::CssFileWriter.configure(
|
|
48
99
|
# filename_pattern: ->(klass, capsule) { "#{klass.name.underscore}-#{capsule}.css" }
|
|
49
100
|
# )
|
|
50
|
-
def configure(output_dir: nil, filename_pattern: nil, enabled: true)
|
|
101
|
+
def configure(output_dir: nil, filename_pattern: nil, enabled: true, fallback_dir: nil)
|
|
51
102
|
@enabled = enabled
|
|
52
103
|
|
|
53
104
|
@output_dir = if output_dir
|
|
@@ -58,6 +109,12 @@ module StyleCapsule
|
|
|
58
109
|
Pathname.new(DEFAULT_OUTPUT_DIR)
|
|
59
110
|
end
|
|
60
111
|
|
|
112
|
+
@fallback_dir = if fallback_dir
|
|
113
|
+
fallback_dir.is_a?(Pathname) ? fallback_dir : Pathname.new(fallback_dir.to_s)
|
|
114
|
+
else
|
|
115
|
+
Pathname.new(FALLBACK_OUTPUT_DIR)
|
|
116
|
+
end
|
|
117
|
+
|
|
61
118
|
@filename_pattern = filename_pattern || default_filename_pattern
|
|
62
119
|
end
|
|
63
120
|
|
|
@@ -66,17 +123,73 @@ module StyleCapsule
|
|
|
66
123
|
# @param css_content [String] CSS content to write
|
|
67
124
|
# @param component_class [Class] Component class that generated the CSS
|
|
68
125
|
# @param capsule_id [String] Capsule ID for the component
|
|
69
|
-
# @return [String, nil] Relative file path (for stylesheet_link_tag) or nil if disabled
|
|
126
|
+
# @return [String, nil] Relative file path (for stylesheet_link_tag) or nil if disabled/failed
|
|
70
127
|
def write_css(css_content:, component_class:, capsule_id:)
|
|
71
128
|
return nil unless enabled?
|
|
72
129
|
|
|
73
|
-
ensure_output_directory
|
|
74
|
-
|
|
75
130
|
filename = generate_filename(component_class, capsule_id)
|
|
76
131
|
file_path = output_directory.join(filename)
|
|
132
|
+
used_fallback = false
|
|
77
133
|
|
|
78
|
-
|
|
79
|
-
|
|
134
|
+
begin
|
|
135
|
+
ensure_output_directory
|
|
136
|
+
# Write CSS to file with explicit UTF-8 encoding
|
|
137
|
+
Instrumentation.instrument_file_write(
|
|
138
|
+
component_class: component_class,
|
|
139
|
+
capsule_id: capsule_id,
|
|
140
|
+
file_path: file_path.to_s,
|
|
141
|
+
size: css_content.bytesize
|
|
142
|
+
) do
|
|
143
|
+
File.write(file_path, css_content, encoding: "UTF-8")
|
|
144
|
+
end
|
|
145
|
+
rescue Errno::EACCES, Errno::EROFS => e
|
|
146
|
+
# Permission denied or read-only filesystem - try fallback directory
|
|
147
|
+
fallback_path = fallback_directory.join(filename)
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
ensure_fallback_directory
|
|
151
|
+
File.write(fallback_path, css_content, encoding: "UTF-8")
|
|
152
|
+
used_fallback = true
|
|
153
|
+
file_path = fallback_path
|
|
154
|
+
|
|
155
|
+
# Instrument the fallback for visibility
|
|
156
|
+
Instrumentation.instrument_fallback(
|
|
157
|
+
component_class: component_class,
|
|
158
|
+
capsule_id: capsule_id,
|
|
159
|
+
original_path: output_directory.join(filename).to_s,
|
|
160
|
+
fallback_path: fallback_path.to_s,
|
|
161
|
+
exception: [e.class.name, e.message],
|
|
162
|
+
exception_object: e
|
|
163
|
+
)
|
|
164
|
+
rescue => fallback_error
|
|
165
|
+
# Even fallback failed - instrument and return nil (will fall back to inline CSS)
|
|
166
|
+
Instrumentation.instrument_fallback_failure(
|
|
167
|
+
component_class: component_class,
|
|
168
|
+
capsule_id: capsule_id,
|
|
169
|
+
original_path: output_directory.join(filename).to_s,
|
|
170
|
+
fallback_path: fallback_path.to_s,
|
|
171
|
+
original_exception: [e.class.name, e.message],
|
|
172
|
+
original_exception_object: e,
|
|
173
|
+
fallback_exception: [fallback_error.class.name, fallback_error.message],
|
|
174
|
+
fallback_exception_object: fallback_error
|
|
175
|
+
)
|
|
176
|
+
return nil
|
|
177
|
+
end
|
|
178
|
+
rescue => e
|
|
179
|
+
# Other errors - instrument and return nil (will fall back to inline CSS)
|
|
180
|
+
Instrumentation.instrument_write_failure(
|
|
181
|
+
component_class: component_class,
|
|
182
|
+
capsule_id: capsule_id,
|
|
183
|
+
file_path: file_path.to_s,
|
|
184
|
+
exception: [e.class.name, e.message],
|
|
185
|
+
exception_object: e
|
|
186
|
+
)
|
|
187
|
+
return nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# If we used fallback directory, return nil (can't serve via asset pipeline)
|
|
191
|
+
# This will cause StylesheetRegistry to fall back to inline CSS
|
|
192
|
+
return nil if used_fallback
|
|
80
193
|
|
|
81
194
|
# Return relative path for stylesheet_link_tag
|
|
82
195
|
# Path should be relative to app/assets
|
|
@@ -135,6 +248,16 @@ module StyleCapsule
|
|
|
135
248
|
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
136
249
|
end
|
|
137
250
|
|
|
251
|
+
# Ensure fallback directory exists
|
|
252
|
+
#
|
|
253
|
+
# @return [void]
|
|
254
|
+
def ensure_fallback_directory
|
|
255
|
+
return unless enabled?
|
|
256
|
+
|
|
257
|
+
dir = fallback_directory
|
|
258
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
259
|
+
end
|
|
260
|
+
|
|
138
261
|
# Clear all generated CSS files
|
|
139
262
|
#
|
|
140
263
|
# @return [void]
|
|
@@ -171,6 +294,11 @@ module StyleCapsule
|
|
|
171
294
|
end
|
|
172
295
|
end
|
|
173
296
|
|
|
297
|
+
# Get fallback directory (with default)
|
|
298
|
+
def fallback_directory
|
|
299
|
+
@fallback_dir ||= Pathname.new(FALLBACK_OUTPUT_DIR)
|
|
300
|
+
end
|
|
301
|
+
|
|
174
302
|
# Generate filename using pattern
|
|
175
303
|
#
|
|
176
304
|
# @param component_class [Class] Component class
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "instrumentation"
|
|
4
|
+
|
|
3
5
|
module StyleCapsule
|
|
4
6
|
# Shared CSS processing logic for scoping selectors with attribute selectors
|
|
5
7
|
#
|
|
@@ -35,9 +37,10 @@ module StyleCapsule
|
|
|
35
37
|
#
|
|
36
38
|
# @param css_string [String] Original CSS content
|
|
37
39
|
# @param capsule_id [String] The capsule ID to use in attribute selector
|
|
40
|
+
# @param component_class [Class, String, nil] Optional component class for instrumentation
|
|
38
41
|
# @return [String] CSS with scoped selectors
|
|
39
42
|
# @raise [ArgumentError] If CSS content exceeds maximum size or capsule_id is invalid
|
|
40
|
-
def self.scope_selectors(css_string, capsule_id)
|
|
43
|
+
def self.scope_selectors(css_string, capsule_id, component_class: nil)
|
|
41
44
|
return css_string if css_string.nil? || css_string.strip.empty?
|
|
42
45
|
|
|
43
46
|
# Validate CSS size to prevent DoS attacks
|
|
@@ -48,61 +51,69 @@ module StyleCapsule
|
|
|
48
51
|
# Validate capsule_id
|
|
49
52
|
validate_capsule_id!(capsule_id)
|
|
50
53
|
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
54
|
+
# Instrument CSS processing with timing and size metrics
|
|
55
|
+
Instrumentation.instrument_css_processing(
|
|
56
|
+
strategy: :selector_patching,
|
|
57
|
+
component_class: component_class || "Unknown",
|
|
58
|
+
capsule_id: capsule_id,
|
|
59
|
+
css_content: css_string
|
|
60
|
+
) do
|
|
61
|
+
css = css_string.dup
|
|
62
|
+
capsule_attr = %([data-capsule="#{capsule_id}"])
|
|
63
|
+
|
|
64
|
+
# Strip CSS comments to avoid interference with selector matching
|
|
65
|
+
# Simple approach: remove /* ... */ comments (including multi-line)
|
|
66
|
+
css_without_comments = strip_comments(css)
|
|
67
|
+
|
|
68
|
+
# Process CSS rule by rule
|
|
69
|
+
# Match: selector(s) { ... }
|
|
70
|
+
# Pattern: (start or closing brace) + (whitespace) + (selector text) + (opening brace)
|
|
71
|
+
# Note: Uses non-greedy quantifier ([^{}@]+?) to minimize backtracking
|
|
72
|
+
# MAX_CSS_SIZE limit (1MB) mitigates ReDoS risk from malicious input
|
|
73
|
+
css_without_comments.gsub!(/(^|\})(\s*)([^{}@]+?)(\{)/m) do |_|
|
|
74
|
+
prefix = Regexp.last_match(1) # Previous closing brace or start
|
|
75
|
+
whitespace = Regexp.last_match(2) # Whitespace between rules
|
|
76
|
+
selectors_raw = Regexp.last_match(3) # The selector group
|
|
77
|
+
selectors = selectors_raw.strip # Stripped for processing
|
|
78
|
+
opening_brace = Regexp.last_match(4) # The opening brace
|
|
79
|
+
|
|
80
|
+
# Skip at-rules (@media, @keyframes, etc.) - they should not be scoped at top level
|
|
81
|
+
next "#{prefix}#{whitespace}#{selectors_raw}#{opening_brace}" if selectors.start_with?("@")
|
|
82
|
+
|
|
83
|
+
# Skip if already scoped (avoid double-scoping)
|
|
84
|
+
next "#{prefix}#{whitespace}#{selectors_raw}#{opening_brace}" if selectors_raw.include?("[data-capsule=")
|
|
85
|
+
|
|
86
|
+
# Skip empty selectors
|
|
87
|
+
next "#{prefix}#{whitespace}#{selectors_raw}#{opening_brace}" if selectors.empty?
|
|
88
|
+
|
|
89
|
+
# Split selectors by comma and scope each one
|
|
90
|
+
scoped_selectors = selectors.split(",").map do |selector|
|
|
91
|
+
selector = selector.strip
|
|
92
|
+
next selector if selector.empty?
|
|
93
|
+
|
|
94
|
+
# Handle special component-scoped selectors (:host, :host-context)
|
|
95
|
+
if selector.start_with?(":host")
|
|
96
|
+
selector = selector
|
|
97
|
+
.gsub(/^:host-context\(([^)]+)\)/, "#{capsule_attr} \\1")
|
|
98
|
+
.gsub(/^:host\(([^)]+)\)/, "#{capsule_attr}\\1")
|
|
99
|
+
.gsub(/^:host\b/, capsule_attr)
|
|
100
|
+
selector
|
|
101
|
+
else
|
|
102
|
+
# Add capsule attribute with space before selector for descendant matching
|
|
103
|
+
# This ensures styles apply to elements inside the scoped wrapper
|
|
104
|
+
"#{capsule_attr} #{selector}"
|
|
105
|
+
end
|
|
106
|
+
end.compact.join(", ")
|
|
107
|
+
|
|
108
|
+
"#{prefix}#{whitespace}#{scoped_selectors}#{opening_brace}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Restore comments in their original positions
|
|
112
|
+
# Since we stripped comments, we need to put them back
|
|
113
|
+
# For simplicity, we'll just return the processed CSS without comments
|
|
114
|
+
# (comments are typically removed in production CSS anyway)
|
|
115
|
+
css_without_comments
|
|
99
116
|
end
|
|
100
|
-
|
|
101
|
-
# Restore comments in their original positions
|
|
102
|
-
# Since we stripped comments, we need to put them back
|
|
103
|
-
# For simplicity, we'll just return the processed CSS without comments
|
|
104
|
-
# (comments are typically removed in production CSS anyway)
|
|
105
|
-
css_without_comments
|
|
106
117
|
end
|
|
107
118
|
|
|
108
119
|
# Scope CSS using CSS nesting (wraps entire CSS in [data-capsule] { ... })
|
|
@@ -122,9 +133,10 @@ module StyleCapsule
|
|
|
122
133
|
#
|
|
123
134
|
# @param css_string [String] Original CSS content
|
|
124
135
|
# @param capsule_id [String] The capsule ID to use in attribute selector
|
|
136
|
+
# @param component_class [Class, String, nil] Optional component class for instrumentation
|
|
125
137
|
# @return [String] CSS wrapped in nesting selector
|
|
126
138
|
# @raise [ArgumentError] If CSS content exceeds maximum size or capsule_id is invalid
|
|
127
|
-
def self.scope_with_nesting(css_string, capsule_id)
|
|
139
|
+
def self.scope_with_nesting(css_string, capsule_id, component_class: nil)
|
|
128
140
|
return css_string if css_string.nil? || css_string.strip.empty?
|
|
129
141
|
|
|
130
142
|
# Validate CSS size to prevent DoS attacks
|
|
@@ -135,10 +147,18 @@ module StyleCapsule
|
|
|
135
147
|
# Validate capsule_id
|
|
136
148
|
validate_capsule_id!(capsule_id)
|
|
137
149
|
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
# Instrument CSS processing with timing and size metrics
|
|
151
|
+
Instrumentation.instrument_css_processing(
|
|
152
|
+
strategy: :nesting,
|
|
153
|
+
component_class: component_class || "Unknown",
|
|
154
|
+
capsule_id: capsule_id,
|
|
155
|
+
css_content: css_string
|
|
156
|
+
) do
|
|
157
|
+
# Simply wrap the entire CSS in the capsule attribute selector
|
|
158
|
+
# No parsing or transformation needed - much more performant
|
|
159
|
+
capsule_attr = %([data-capsule="#{capsule_id}"])
|
|
160
|
+
"#{capsule_attr} {\n#{css_string}\n}"
|
|
161
|
+
end
|
|
142
162
|
end
|
|
143
163
|
|
|
144
164
|
# Strip CSS comments (/* ... */) from the string
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StyleCapsule
|
|
4
|
+
# Centralized instrumentation module for StyleCapsule
|
|
5
|
+
#
|
|
6
|
+
# Provides efficient, non-blocking instrumentation using ActiveSupport::Notifications.
|
|
7
|
+
# Metrics (time, size) are only calculated when subscribers are present, ensuring
|
|
8
|
+
# zero performance impact when instrumentation is not being used.
|
|
9
|
+
#
|
|
10
|
+
# All events follow Rails naming convention: style_capsule.{module}.{event}
|
|
11
|
+
#
|
|
12
|
+
# @example Subscribing to CSS processing events
|
|
13
|
+
# ActiveSupport::Notifications.subscribe("style_capsule.css_processor.scope") do |name, start, finish, id, payload|
|
|
14
|
+
# duration_ms = (finish - start) * 1000
|
|
15
|
+
# input_size = payload[:input_size]
|
|
16
|
+
# Rails.logger.info "CSS scoped in #{duration_ms}ms, input: #{input_size} bytes"
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Using Event object for more details
|
|
20
|
+
# ActiveSupport::Notifications.subscribe("style_capsule.css_processor.scope") do |event|
|
|
21
|
+
# Rails.logger.info "CSS scoped in #{event.duration}ms, input: #{event.payload[:input_size]} bytes"
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @example Subscribing to file write events
|
|
25
|
+
# ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.write") do |name, start, finish, id, payload|
|
|
26
|
+
# StatsD.timing("style_capsule.write.duration", (finish - start) * 1000)
|
|
27
|
+
# StatsD.histogram("style_capsule.write.size", payload[:size])
|
|
28
|
+
# end
|
|
29
|
+
module Instrumentation
|
|
30
|
+
# Check if ActiveSupport::Notifications is available
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def self.available?
|
|
34
|
+
defined?(ActiveSupport::Notifications)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Instrument an operation with automatic timing and size metrics
|
|
38
|
+
#
|
|
39
|
+
# This method is highly efficient:
|
|
40
|
+
# - Only measures time if subscribers exist (ActiveSupport::Notifications is optimized for this)
|
|
41
|
+
# - Only calculates sizes if subscribers exist (lazy evaluation)
|
|
42
|
+
# - Uses monotonic time for accurate measurements
|
|
43
|
+
#
|
|
44
|
+
# @param event_name [String] Event name following Rails convention (e.g., "style_capsule.css_processor.scope")
|
|
45
|
+
# @param payload [Hash] Additional payload data
|
|
46
|
+
# @yield The operation to instrument
|
|
47
|
+
# @yieldreturn [Object] Result of the operation (used for output size calculation if needed)
|
|
48
|
+
# @return [Object] Return value of the block
|
|
49
|
+
# @example Instrumenting CSS processing
|
|
50
|
+
# result = Instrumentation.instrument(
|
|
51
|
+
# "style_capsule.css_processor.scope",
|
|
52
|
+
# component_class: component_class.name,
|
|
53
|
+
# capsule_id: capsule_id,
|
|
54
|
+
# strategy: :selector_patching,
|
|
55
|
+
# input_size: css_content.bytesize
|
|
56
|
+
# ) do
|
|
57
|
+
# CssProcessor.scope_selectors(css_content, capsule_id)
|
|
58
|
+
# end
|
|
59
|
+
def self.instrument(event_name, payload = {}, &block)
|
|
60
|
+
return yield unless available?
|
|
61
|
+
|
|
62
|
+
# Check if there are any subscribers (ActiveSupport optimizes this check)
|
|
63
|
+
# If no subscribers, just execute the block without any overhead
|
|
64
|
+
unless ActiveSupport::Notifications.notifier.listening?(event_name)
|
|
65
|
+
return yield
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Calculate input size if not provided but css_content/input is in payload
|
|
69
|
+
unless payload[:input_size]
|
|
70
|
+
if payload[:css_content]
|
|
71
|
+
payload[:input_size] = payload[:css_content].bytesize
|
|
72
|
+
elsif payload[:input]
|
|
73
|
+
payload[:input_size] = payload[:input].bytesize
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Use ActiveSupport::Notifications.instrument which automatically:
|
|
78
|
+
# - Measures duration using monotonic time (available in event.duration)
|
|
79
|
+
# - Only does work if subscribers exist (zero overhead if no subscribers)
|
|
80
|
+
# Note: output_size is not included in payload (subscribers can calculate from result if needed)
|
|
81
|
+
result = nil
|
|
82
|
+
ActiveSupport::Notifications.instrument(event_name, payload) do
|
|
83
|
+
result = yield
|
|
84
|
+
result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
result
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Instrument an event without a block (for events that don't have duration)
|
|
91
|
+
#
|
|
92
|
+
# @param event_name [String] Event name
|
|
93
|
+
# @param payload [Hash] Payload data
|
|
94
|
+
# @return [void]
|
|
95
|
+
# @example Instrumenting a fallback event
|
|
96
|
+
# Instrumentation.notify(
|
|
97
|
+
# "style_capsule.css_file_writer.fallback",
|
|
98
|
+
# component_class: component_class.name,
|
|
99
|
+
# original_path: original_path,
|
|
100
|
+
# fallback_path: fallback_path,
|
|
101
|
+
# exception: [e.class.name, e.message],
|
|
102
|
+
# exception_object: e
|
|
103
|
+
# )
|
|
104
|
+
def self.notify(event_name, payload = {})
|
|
105
|
+
return unless available?
|
|
106
|
+
|
|
107
|
+
# Only notify if subscribers exist
|
|
108
|
+
return unless ActiveSupport::Notifications.notifier.listening?(event_name)
|
|
109
|
+
|
|
110
|
+
ActiveSupport::Notifications.instrument(event_name, payload)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Instrument CSS processing operations
|
|
114
|
+
#
|
|
115
|
+
# @param strategy [Symbol] Scoping strategy (:selector_patching or :nesting)
|
|
116
|
+
# @param component_class [Class, String] Component class
|
|
117
|
+
# @param capsule_id [String] Capsule ID
|
|
118
|
+
# @param css_content [String] CSS content (for size calculation)
|
|
119
|
+
# @yield The CSS processing operation
|
|
120
|
+
# @return [String] Processed CSS
|
|
121
|
+
def self.instrument_css_processing(strategy:, component_class:, capsule_id:, css_content:, &block)
|
|
122
|
+
component_name = component_class.is_a?(Class) ? component_class.name : component_class.to_s
|
|
123
|
+
input_size = css_content.bytesize
|
|
124
|
+
|
|
125
|
+
instrument(
|
|
126
|
+
"style_capsule.css_processor.scope",
|
|
127
|
+
strategy: strategy,
|
|
128
|
+
component_class: component_name,
|
|
129
|
+
capsule_id: capsule_id,
|
|
130
|
+
input_size: input_size
|
|
131
|
+
) do
|
|
132
|
+
result = yield
|
|
133
|
+
# Output size will be calculated by subscribers if needed
|
|
134
|
+
# They can access it via: result.bytesize
|
|
135
|
+
result
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Instrument CSS file write operations
|
|
140
|
+
#
|
|
141
|
+
# @param component_class [Class] Component class
|
|
142
|
+
# @param capsule_id [String] Capsule ID
|
|
143
|
+
# @param file_path [String] File path
|
|
144
|
+
# @param size [Integer] CSS content size in bytes
|
|
145
|
+
# @yield The file write operation
|
|
146
|
+
# @return [Object] Return value of the block
|
|
147
|
+
def self.instrument_file_write(component_class:, capsule_id:, file_path:, size:, &block)
|
|
148
|
+
instrument(
|
|
149
|
+
"style_capsule.css_file_writer.write",
|
|
150
|
+
component_class: component_class.name,
|
|
151
|
+
capsule_id: capsule_id,
|
|
152
|
+
file_path: file_path,
|
|
153
|
+
size: size
|
|
154
|
+
) do
|
|
155
|
+
yield
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Instrument fallback to temporary directory
|
|
160
|
+
#
|
|
161
|
+
# @param component_class [Class] Component class
|
|
162
|
+
# @param capsule_id [String] Capsule ID
|
|
163
|
+
# @param original_path [String] Original file path that failed
|
|
164
|
+
# @param fallback_path [String] Fallback file path used
|
|
165
|
+
# @param exception [Array<String>] Exception [class_name, message]
|
|
166
|
+
# @param exception_object [Exception] The exception object
|
|
167
|
+
# @return [void]
|
|
168
|
+
def self.instrument_fallback(component_class:, capsule_id:, original_path:, fallback_path:, exception:, exception_object:)
|
|
169
|
+
notify(
|
|
170
|
+
"style_capsule.css_file_writer.fallback",
|
|
171
|
+
component_class: component_class.name,
|
|
172
|
+
capsule_id: capsule_id,
|
|
173
|
+
original_path: original_path,
|
|
174
|
+
fallback_path: fallback_path,
|
|
175
|
+
exception: exception,
|
|
176
|
+
exception_object: exception_object
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Instrument fallback failure (both original and fallback failed)
|
|
181
|
+
#
|
|
182
|
+
# @param component_class [Class] Component class
|
|
183
|
+
# @param capsule_id [String] Capsule ID
|
|
184
|
+
# @param original_path [String] Original file path that failed
|
|
185
|
+
# @param fallback_path [String] Fallback file path that also failed
|
|
186
|
+
# @param original_exception [Array<String>] Original exception [class_name, message]
|
|
187
|
+
# @param original_exception_object [Exception] Original exception object
|
|
188
|
+
# @param fallback_exception [Array<String>] Fallback exception [class_name, message]
|
|
189
|
+
# @param fallback_exception_object [Exception] Fallback exception object
|
|
190
|
+
# @return [void]
|
|
191
|
+
def self.instrument_fallback_failure(component_class:, capsule_id:, original_path:, fallback_path:, original_exception:, original_exception_object:, fallback_exception:, fallback_exception_object:)
|
|
192
|
+
notify(
|
|
193
|
+
"style_capsule.css_file_writer.fallback_failure",
|
|
194
|
+
component_class: component_class.name,
|
|
195
|
+
capsule_id: capsule_id,
|
|
196
|
+
original_path: original_path,
|
|
197
|
+
fallback_path: fallback_path,
|
|
198
|
+
original_exception: original_exception,
|
|
199
|
+
original_exception_object: original_exception_object,
|
|
200
|
+
fallback_exception: fallback_exception,
|
|
201
|
+
fallback_exception_object: fallback_exception_object
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Instrument write failure (non-permission errors)
|
|
206
|
+
#
|
|
207
|
+
# @param component_class [Class] Component class
|
|
208
|
+
# @param capsule_id [String] Capsule ID
|
|
209
|
+
# @param file_path [String] File path that failed
|
|
210
|
+
# @param exception [Array<String>] Exception [class_name, message]
|
|
211
|
+
# @param exception_object [Exception] The exception object
|
|
212
|
+
# @return [void]
|
|
213
|
+
def self.instrument_write_failure(component_class:, capsule_id:, file_path:, exception:, exception_object:)
|
|
214
|
+
notify(
|
|
215
|
+
"style_capsule.css_file_writer.write_failure",
|
|
216
|
+
component_class: component_class.name,
|
|
217
|
+
capsule_id: capsule_id,
|
|
218
|
+
file_path: file_path,
|
|
219
|
+
exception: exception,
|
|
220
|
+
exception_object: exception_object
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Instrument stylesheet registration
|
|
225
|
+
#
|
|
226
|
+
# @param namespace [Symbol, String] Namespace
|
|
227
|
+
# @param file_path [String, nil] File path (if file-based)
|
|
228
|
+
# @param inline_size [Integer, nil] Inline CSS size in bytes (if inline)
|
|
229
|
+
# @param cache_strategy [Symbol] Cache strategy
|
|
230
|
+
# @yield The registration operation
|
|
231
|
+
# @return [Object] Return value of the block
|
|
232
|
+
def self.instrument_registration(namespace:, file_path: nil, inline_size: nil, cache_strategy: :none, &block)
|
|
233
|
+
payload = {
|
|
234
|
+
namespace: namespace.to_s,
|
|
235
|
+
cache_strategy: cache_strategy
|
|
236
|
+
}
|
|
237
|
+
payload[:file_path] = file_path if file_path
|
|
238
|
+
payload[:inline_size] = inline_size if inline_size
|
|
239
|
+
|
|
240
|
+
instrument(
|
|
241
|
+
"style_capsule.stylesheet_registry.register",
|
|
242
|
+
payload
|
|
243
|
+
) do
|
|
244
|
+
yield
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
data/lib/style_capsule.rb
CHANGED
|
@@ -89,6 +89,7 @@ end
|
|
|
89
89
|
# # Or manually: bin/rails style_capsule:build
|
|
90
90
|
module StyleCapsule
|
|
91
91
|
require_relative "style_capsule/version"
|
|
92
|
+
require_relative "style_capsule/instrumentation"
|
|
92
93
|
require_relative "style_capsule/css_processor"
|
|
93
94
|
require_relative "style_capsule/css_file_writer"
|
|
94
95
|
require_relative "style_capsule/stylesheet_registry"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: style_capsule
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -268,6 +268,7 @@ files:
|
|
|
268
268
|
- lib/style_capsule/css_file_writer.rb
|
|
269
269
|
- lib/style_capsule/css_processor.rb
|
|
270
270
|
- lib/style_capsule/helper.rb
|
|
271
|
+
- lib/style_capsule/instrumentation.rb
|
|
271
272
|
- lib/style_capsule/phlex_helper.rb
|
|
272
273
|
- lib/style_capsule/railtie.rb
|
|
273
274
|
- lib/style_capsule/standalone_helper.rb
|