style_capsule 1.2.0 → 1.4.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 +22 -1
- data/README.md +117 -28
- data/SECURITY.md +16 -43
- data/lib/style_capsule/component.rb +134 -72
- data/lib/style_capsule/css_file_writer.rb +137 -9
- data/lib/style_capsule/css_processor.rb +80 -60
- data/lib/style_capsule/helper.rb +0 -4
- data/lib/style_capsule/instrumentation.rb +248 -0
- data/lib/style_capsule/phlex_helper.rb +17 -9
- data/lib/style_capsule/standalone_helper.rb +0 -4
- data/lib/style_capsule/version.rb +1 -1
- data/lib/style_capsule/view_component.rb +132 -70
- data/lib/style_capsule/view_component_helper.rb +16 -9
- data/lib/style_capsule.rb +6 -1
- data/sig/style_capsule.rbs +2 -7
- 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: 74203df88bc23dc0b1509f0e7adc5978fe327c982026916e990644c92f65a0db
|
|
4
|
+
data.tar.gz: 7f7783ef50b66e14859e1f7207262279f20e5f28eb29d3373a85fef9ae2f3a7c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bed787978d271158ca409ffe8445df1f6203e954000d153beae40368861e0077b566557c3f50456373d04998f85ab60ee86b3cd93ed558d31f3d482247fb0b7e
|
|
7
|
+
data.tar.gz: 22b900ebbf5ce4a219d487f7915b7dcd31ed4552d134af44f056ba79eff8551a4ef62e8070f03887fc4962f2073d06bd3b2d65d35e00f09183d97a730ad24ee2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.4.0 (2025-11-26)
|
|
4
|
+
|
|
5
|
+
- Added unified `style_capsule` class method for configuring all StyleCapsule settings (namespace, cache strategy, CSS scoping, head rendering) in a single call
|
|
6
|
+
- Added automatic namespace fallback in `register_stylesheet` helper methods - when namespace is not specified, uses the component's configured namespace from `style_capsule`
|
|
7
|
+
- Removed deprecated `head_rendering!` method (use `style_capsule` instead)
|
|
8
|
+
- Removed deprecated `stylesheet_registrymap_tags` alias (use `stylesheet_registry_tags` instead)
|
|
9
|
+
- Refactored namespace configuration to use instance variables instead of constants for better inheritance behavior
|
|
10
|
+
|
|
11
|
+
## 1.3.0 (2025-11-26)
|
|
12
|
+
|
|
13
|
+
- Added comprehensive instrumentation via `StyleCapsule::Instrumentation` using ActiveSupport::Notifications
|
|
14
|
+
- Instrumentation events for CSS processing (`style_capsule.css_processor.scope`) with duration and size metrics
|
|
15
|
+
- Instrumentation events for CSS file writing (`style_capsule.css_file_writer.write`) with duration and size metrics
|
|
16
|
+
- Added fallback directory support for CSS file writing when default location is read-only (e.g., Docker containers)
|
|
17
|
+
- Automatic fallback to `/tmp/style_capsule` when primary output directory is not writable
|
|
18
|
+
- Instrumentation events for fallback scenarios (`style_capsule.css_file_writer.fallback`, `style_capsule.css_file_writer.fallback_failure`)
|
|
19
|
+
- Instrumentation events for write failures (`style_capsule.css_file_writer.write_failure`)
|
|
20
|
+
- All instrumentation is zero-overhead when no subscribers are present (only calculates metrics when actively monitored)
|
|
21
|
+
- Improved test coverage reporting and analysis tools
|
|
22
|
+
- Added community guidelines and governance documents
|
|
23
|
+
|
|
3
24
|
## 1.2.0 (2025-11-24)
|
|
4
25
|
|
|
5
26
|
- Added `StyleCapsule::ClassRegistry` for Rails-friendly class tracking without ObjectSpace iteration
|
|
@@ -16,7 +37,7 @@
|
|
|
16
37
|
- Rails integration remains fully supported via Railtie
|
|
17
38
|
- Added `StyleCapsule::StandaloneHelper` for non-Rails frameworks
|
|
18
39
|
- `StylesheetRegistry` now works without `ActiveSupport::CurrentAttributes` using thread-local storage fallback
|
|
19
|
-
- Renamed `stylesheet_registrymap_tags` to `stylesheet_registry_tags` (
|
|
40
|
+
- Renamed `stylesheet_registrymap_tags` to `stylesheet_registry_tags` (deprecated alias removed in 1.4.0)
|
|
20
41
|
- Extracted CSS building logic from Rake tasks into `StyleCapsule::ComponentBuilder`
|
|
21
42
|
- Fixed XSS vulnerability in `escape_html_attr` by using `CGI.escapeHTML` for proper HTML entity escaping
|
|
22
43
|
- Optimized ActiveSupport require to avoid exception handling overhead in Rails apps
|
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
|
|
@@ -101,12 +103,12 @@ StyleCapsule supports two CSS scoping strategies:
|
|
|
101
103
|
|
|
102
104
|
### Configuration
|
|
103
105
|
|
|
104
|
-
**Per-component:**
|
|
106
|
+
**Per-component (using `style_capsule` - recommended):**
|
|
105
107
|
|
|
106
108
|
```ruby
|
|
107
109
|
class MyComponent < ApplicationComponent
|
|
108
110
|
include StyleCapsule::Component
|
|
109
|
-
|
|
111
|
+
style_capsule scoping_strategy: :nesting # Use CSS nesting
|
|
110
112
|
end
|
|
111
113
|
```
|
|
112
114
|
|
|
@@ -115,7 +117,7 @@ end
|
|
|
115
117
|
```ruby
|
|
116
118
|
class ApplicationComponent < Phlex::HTML
|
|
117
119
|
include StyleCapsule::Component
|
|
118
|
-
|
|
120
|
+
style_capsule scoping_strategy: :nesting # Enable for all components
|
|
119
121
|
end
|
|
120
122
|
```
|
|
121
123
|
|
|
@@ -127,12 +129,12 @@ MyComponent.clear_css_cache
|
|
|
127
129
|
|
|
128
130
|
## Stylesheet Registry
|
|
129
131
|
|
|
130
|
-
For better performance, register styles for head rendering instead of rendering `<style>` tags in the body:
|
|
132
|
+
For better performance, register styles for head rendering instead of rendering `<style>` tags in the body. Use the unified `style_capsule` method to configure all settings:
|
|
131
133
|
|
|
132
134
|
```ruby
|
|
133
135
|
class MyComponent < ApplicationComponent
|
|
134
136
|
include StyleCapsule::Component
|
|
135
|
-
|
|
137
|
+
style_capsule namespace: :admin # Configure namespace and enable head rendering
|
|
136
138
|
|
|
137
139
|
def component_styles
|
|
138
140
|
<<~CSS
|
|
@@ -142,12 +144,17 @@ class MyComponent < ApplicationComponent
|
|
|
142
144
|
end
|
|
143
145
|
```
|
|
144
146
|
|
|
145
|
-
With cache strategy:
|
|
147
|
+
With cache strategy and CSS scoping:
|
|
146
148
|
|
|
147
149
|
```ruby
|
|
148
150
|
class MyComponent < ApplicationComponent
|
|
149
151
|
include StyleCapsule::Component
|
|
150
|
-
|
|
152
|
+
style_capsule(
|
|
153
|
+
namespace: :admin,
|
|
154
|
+
cache_strategy: :time,
|
|
155
|
+
cache_ttl: 1.hour,
|
|
156
|
+
scoping_strategy: :nesting
|
|
157
|
+
)
|
|
151
158
|
|
|
152
159
|
def component_styles
|
|
153
160
|
<<~CSS
|
|
@@ -157,11 +164,10 @@ class MyComponent < ApplicationComponent
|
|
|
157
164
|
end
|
|
158
165
|
```
|
|
159
166
|
|
|
160
|
-
Then in your layout:
|
|
167
|
+
Then in your layout (render only the namespace you need):
|
|
161
168
|
|
|
162
169
|
```erb
|
|
163
170
|
<head>
|
|
164
|
-
<%= stylesheet_registry_tags %>
|
|
165
171
|
<%= stylesheet_registry_tags(namespace: :admin) %>
|
|
166
172
|
</head>
|
|
167
173
|
```
|
|
@@ -170,42 +176,58 @@ Or in Phlex (requires including `StyleCapsule::PhlexHelper`):
|
|
|
170
176
|
|
|
171
177
|
```ruby
|
|
172
178
|
head do
|
|
173
|
-
stylesheet_registry_tags
|
|
179
|
+
stylesheet_registry_tags(namespace: :admin)
|
|
174
180
|
end
|
|
175
181
|
```
|
|
176
182
|
|
|
183
|
+
**Namespace Isolation:** Using namespaces prevents stylesheet leakage between different application contexts. For example, login pages can use `namespace: :login`, ActiveAdmin can use `namespace: :active_admin`, and user components can use `namespace: :user`. Each namespace is rendered separately, improving caching efficiency and preventing style conflicts.
|
|
184
|
+
|
|
177
185
|
### Registering Stylesheet Files
|
|
178
186
|
|
|
179
|
-
You can also register external stylesheet files (not inline CSS) for head rendering:
|
|
187
|
+
You can also register external stylesheet files (not inline CSS) for head rendering. When a component has a configured namespace via `style_capsule`, you don't need to specify it every time:
|
|
180
188
|
|
|
181
189
|
**In ERB:**
|
|
182
190
|
|
|
183
191
|
```erb
|
|
184
|
-
<% register_stylesheet("stylesheets/user/
|
|
192
|
+
<% register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload") %>
|
|
185
193
|
<% register_stylesheet("stylesheets/admin/dashboard", namespace: :admin) %>
|
|
186
194
|
```
|
|
187
195
|
|
|
188
196
|
**In Phlex (requires including `StyleCapsule::PhlexHelper`):**
|
|
189
197
|
|
|
190
198
|
```ruby
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
class UserComponent < ApplicationComponent
|
|
200
|
+
include StyleCapsule::Component
|
|
201
|
+
include StyleCapsule::PhlexHelper
|
|
202
|
+
style_capsule namespace: :user # Set default namespace
|
|
203
|
+
|
|
204
|
+
def view_template
|
|
205
|
+
# Namespace automatically uses :user from style_capsule
|
|
206
|
+
register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload")
|
|
207
|
+
# Can still override namespace if needed
|
|
208
|
+
register_stylesheet("stylesheets/shared/common", namespace: :shared)
|
|
209
|
+
div { "Content" }
|
|
210
|
+
end
|
|
195
211
|
end
|
|
196
212
|
```
|
|
197
213
|
|
|
198
214
|
**In ViewComponent (requires including `StyleCapsule::ViewComponentHelper`):**
|
|
199
215
|
|
|
200
216
|
```ruby
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
217
|
+
class UserComponent < ApplicationComponent
|
|
218
|
+
include StyleCapsule::ViewComponent
|
|
219
|
+
include StyleCapsule::ViewComponentHelper
|
|
220
|
+
style_capsule namespace: :user # Set default namespace
|
|
221
|
+
|
|
222
|
+
def call
|
|
223
|
+
# Namespace automatically uses :user from style_capsule
|
|
224
|
+
register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload")
|
|
225
|
+
content_tag(:div, "Content")
|
|
226
|
+
end
|
|
205
227
|
end
|
|
206
228
|
```
|
|
207
229
|
|
|
208
|
-
Registered files are rendered via `stylesheet_registry_tags` in your layout, just like inline CSS.
|
|
230
|
+
Registered files are rendered via `stylesheet_registry_tags` in your layout, just like inline CSS. The namespace is automatically used from the component's `style_capsule` configuration when not explicitly specified.
|
|
209
231
|
|
|
210
232
|
## Caching Strategies
|
|
211
233
|
|
|
@@ -214,22 +236,22 @@ Registered files are rendered via `stylesheet_registry_tags` in your layout, jus
|
|
|
214
236
|
```ruby
|
|
215
237
|
class MyComponent < ApplicationComponent
|
|
216
238
|
include StyleCapsule::Component
|
|
217
|
-
|
|
239
|
+
style_capsule # No cache strategy set (default: :none)
|
|
218
240
|
end
|
|
219
241
|
```
|
|
220
242
|
|
|
221
243
|
### Time-Based Caching
|
|
222
244
|
|
|
223
245
|
```ruby
|
|
224
|
-
|
|
246
|
+
style_capsule cache_strategy: :time, cache_ttl: 1.hour # Using ActiveSupport::Duration
|
|
225
247
|
# Or using integer seconds:
|
|
226
|
-
|
|
248
|
+
style_capsule cache_strategy: :time, cache_ttl: 3600 # Cache for 1 hour
|
|
227
249
|
```
|
|
228
250
|
|
|
229
251
|
### Custom Proc Caching
|
|
230
252
|
|
|
231
253
|
```ruby
|
|
232
|
-
|
|
254
|
+
style_capsule cache_strategy: ->(css, capsule_id, namespace) {
|
|
233
255
|
cache_key = "css_#{capsule_id}_#{namespace}"
|
|
234
256
|
should_cache = css.length > 100
|
|
235
257
|
expires_at = Time.now + 1800
|
|
@@ -246,7 +268,7 @@ Writes CSS to files for HTTP caching. **Requires class method `def self.componen
|
|
|
246
268
|
```ruby
|
|
247
269
|
class MyComponent < ApplicationComponent
|
|
248
270
|
include StyleCapsule::Component
|
|
249
|
-
|
|
271
|
+
style_capsule cache_strategy: :file
|
|
250
272
|
|
|
251
273
|
# Must use class method for file caching
|
|
252
274
|
def self.component_styles
|
|
@@ -265,10 +287,13 @@ StyleCapsule::CssFileWriter.configure(
|
|
|
265
287
|
output_dir: Rails.root.join("app/assets/builds/capsules"),
|
|
266
288
|
filename_pattern: ->(component_class, capsule_id) {
|
|
267
289
|
"capsule-#{capsule_id}.css"
|
|
268
|
-
}
|
|
290
|
+
},
|
|
291
|
+
fallback_dir: "/tmp/style_capsule" # Optional, defaults to /tmp/style_capsule
|
|
269
292
|
)
|
|
270
293
|
```
|
|
271
294
|
|
|
295
|
+
**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.
|
|
296
|
+
|
|
272
297
|
**Precompilation:**
|
|
273
298
|
|
|
274
299
|
```bash
|
|
@@ -280,6 +305,70 @@ Files are automatically built during `bin/rails assets:precompile`.
|
|
|
280
305
|
|
|
281
306
|
**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
307
|
|
|
308
|
+
## Instrumentation
|
|
309
|
+
|
|
310
|
+
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.
|
|
311
|
+
|
|
312
|
+
### Available Events
|
|
313
|
+
|
|
314
|
+
- `style_capsule.css_processor.scope` - CSS scoping operations with duration and size metrics
|
|
315
|
+
- `style_capsule.css_file_writer.write` - CSS file write operations with duration and size metrics
|
|
316
|
+
- `style_capsule.css_file_writer.fallback` - When fallback directory is used (read-only filesystem)
|
|
317
|
+
- `style_capsule.css_file_writer.fallback_failure` - When both primary and fallback directories fail
|
|
318
|
+
- `style_capsule.css_file_writer.write_failure` - Other write errors
|
|
319
|
+
|
|
320
|
+
### Example: Monitoring CSS Processing
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
# config/initializers/style_capsule.rb
|
|
324
|
+
ActiveSupport::Notifications.subscribe("style_capsule.css_processor.scope") do |name, start, finish, id, payload|
|
|
325
|
+
duration_ms = (finish - start) * 1000
|
|
326
|
+
Rails.logger.info "CSS scoped in #{duration_ms.round(2)}ms, input: #{payload[:input_size]} bytes, output: #{payload[:output_size]} bytes"
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Example: Monitoring File Writes
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.write") do |name, start, finish, id, payload|
|
|
334
|
+
duration_ms = (finish - start) * 1000
|
|
335
|
+
StatsD.timing("style_capsule.write.duration", duration_ms)
|
|
336
|
+
StatsD.histogram("style_capsule.write.size", payload[:size])
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Example: Monitoring Fallback Scenarios
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.fallback") do |name, start, finish, id, payload|
|
|
344
|
+
Rails.logger.warn "StyleCapsule fallback used: #{payload[:component_class]} -> #{payload[:fallback_path]}"
|
|
345
|
+
# Exception info available: payload[:exception] and payload[:exception_object]
|
|
346
|
+
StatsD.increment("style_capsule.css_file_writer.fallback", tags: [
|
|
347
|
+
"component:#{payload[:component_class]}",
|
|
348
|
+
"error:#{payload[:exception].first}"
|
|
349
|
+
])
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Example: Error Reporting
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.fallback_failure") do |name, start, finish, id, payload|
|
|
357
|
+
ActionReporter.notify(
|
|
358
|
+
"StyleCapsule: CSS write failure (both primary and fallback failed)",
|
|
359
|
+
context: {
|
|
360
|
+
component_class: payload[:component_class],
|
|
361
|
+
original_path: payload[:original_path],
|
|
362
|
+
fallback_path: payload[:fallback_path],
|
|
363
|
+
original_exception: payload[:original_exception],
|
|
364
|
+
fallback_exception: payload[:fallback_exception]
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
For more details, see the [ActiveSupport::Notifications documentation](https://guides.rubyonrails.org/active_support_instrumentation.html).
|
|
371
|
+
|
|
283
372
|
## Advanced Usage
|
|
284
373
|
|
|
285
374
|
### 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.
|
|
@@ -227,23 +227,104 @@ module StyleCapsule
|
|
|
227
227
|
end
|
|
228
228
|
end
|
|
229
229
|
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
stylesheet_registry
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# Check if component uses head rendering
|
|
230
|
+
# Check if component uses head rendering (checks instance variable, then parent class, defaults to false)
|
|
231
|
+
#
|
|
232
|
+
# @return [Boolean] Whether head rendering is enabled (default: false)
|
|
237
233
|
def head_rendering?
|
|
238
|
-
|
|
239
|
-
|
|
234
|
+
if defined?(@head_rendering)
|
|
235
|
+
@head_rendering
|
|
236
|
+
elsif superclass.respond_to?(:head_rendering?, true)
|
|
237
|
+
superclass.head_rendering?
|
|
238
|
+
else
|
|
239
|
+
false
|
|
240
|
+
end
|
|
240
241
|
end
|
|
241
242
|
|
|
242
243
|
public :head_rendering?
|
|
243
244
|
|
|
244
|
-
# Get the namespace for stylesheet registry
|
|
245
|
+
# Get the namespace for stylesheet registry (checks instance variable, then parent class, defaults to nil)
|
|
246
|
+
#
|
|
247
|
+
# @return [Symbol, String, nil] The namespace identifier (default: nil)
|
|
245
248
|
def stylesheet_namespace
|
|
246
|
-
|
|
249
|
+
if defined?(@stylesheet_namespace) && @stylesheet_namespace
|
|
250
|
+
@stylesheet_namespace
|
|
251
|
+
elsif superclass.respond_to?(:stylesheet_namespace, true)
|
|
252
|
+
superclass.stylesheet_namespace
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Configure StyleCapsule settings
|
|
257
|
+
#
|
|
258
|
+
# All settings support class inheritance - child classes inherit settings from parent classes
|
|
259
|
+
# and can override them by calling style_capsule again with different values.
|
|
260
|
+
#
|
|
261
|
+
# @param namespace [Symbol, String, nil] Default namespace for stylesheets
|
|
262
|
+
# @param cache_strategy [Symbol, String, Proc, nil] Cache strategy: :none (default), :time, :proc, :file
|
|
263
|
+
# @param cache_ttl [Integer, ActiveSupport::Duration, nil] Time-to-live in seconds (for :time strategy)
|
|
264
|
+
# @param cache_proc [Proc, nil] Custom cache proc (for :proc strategy)
|
|
265
|
+
# @param scoping_strategy [Symbol, nil] CSS scoping strategy: :selector_patching (default) or :nesting
|
|
266
|
+
# @param head_rendering [Boolean, nil] Enable head rendering (default: true if any option is set, false otherwise)
|
|
267
|
+
# @return [void]
|
|
268
|
+
# @example Basic usage with namespace
|
|
269
|
+
# class AdminComponent < ApplicationComponent
|
|
270
|
+
# include StyleCapsule::Component
|
|
271
|
+
# style_capsule namespace: :admin
|
|
272
|
+
#
|
|
273
|
+
# def view_template
|
|
274
|
+
# register_stylesheet("stylesheets/admin/dashboard") # Uses :admin namespace automatically
|
|
275
|
+
# div { "Content" }
|
|
276
|
+
# end
|
|
277
|
+
# end
|
|
278
|
+
# @example With all options
|
|
279
|
+
# class MyComponent < ApplicationComponent
|
|
280
|
+
# include StyleCapsule::Component
|
|
281
|
+
# style_capsule(
|
|
282
|
+
# namespace: :user,
|
|
283
|
+
# cache_strategy: :time,
|
|
284
|
+
# cache_ttl: 1.hour,
|
|
285
|
+
# scoping_strategy: :nesting
|
|
286
|
+
# )
|
|
287
|
+
# end
|
|
288
|
+
# @example Inheritance - child class inherits parent settings
|
|
289
|
+
# class BaseComponent < ApplicationComponent
|
|
290
|
+
# include StyleCapsule::Component
|
|
291
|
+
# style_capsule namespace: :admin, cache_strategy: :time, cache_ttl: 1.hour
|
|
292
|
+
# end
|
|
293
|
+
#
|
|
294
|
+
# class ChildComponent < BaseComponent
|
|
295
|
+
# # Inherits namespace: :admin, cache_strategy: :time, cache_ttl: 1.hour
|
|
296
|
+
# # Can override specific settings:
|
|
297
|
+
# style_capsule namespace: :user # Overrides namespace, keeps cache settings
|
|
298
|
+
# end
|
|
299
|
+
def style_capsule(namespace: nil, cache_strategy: nil, cache_ttl: nil, cache_proc: nil, scoping_strategy: nil, head_rendering: nil)
|
|
300
|
+
# Set namespace (stored in instance variable, but getter checks parent class for inheritance)
|
|
301
|
+
if namespace
|
|
302
|
+
@stylesheet_namespace = namespace
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Configure cache strategy if provided
|
|
306
|
+
if cache_strategy || cache_ttl || cache_proc
|
|
307
|
+
normalized_strategy, normalized_proc = normalize_cache_strategy(cache_strategy || :none, cache_proc)
|
|
308
|
+
@inline_cache_strategy = normalized_strategy
|
|
309
|
+
# Explicitly set cache_ttl (even if nil) to override parent's value when cache settings are changed
|
|
310
|
+
@inline_cache_ttl = cache_ttl
|
|
311
|
+
@inline_cache_proc = normalized_proc
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Configure CSS scoping strategy if provided
|
|
315
|
+
if scoping_strategy
|
|
316
|
+
unless [:selector_patching, :nesting].include?(scoping_strategy)
|
|
317
|
+
raise ArgumentError, "scoping_strategy must be :selector_patching or :nesting (got: #{scoping_strategy.inspect})"
|
|
318
|
+
end
|
|
319
|
+
@css_scoping_strategy = scoping_strategy
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Enable head rendering if explicitly set or if any option is provided (except scoping_strategy)
|
|
323
|
+
if head_rendering.nil?
|
|
324
|
+
@head_rendering = true if namespace || cache_strategy || cache_ttl || cache_proc
|
|
325
|
+
else
|
|
326
|
+
@head_rendering = head_rendering
|
|
327
|
+
end
|
|
247
328
|
end
|
|
248
329
|
|
|
249
330
|
# Get the custom scope ID if set (alias for capsule_id getter)
|
|
@@ -251,22 +332,55 @@ module StyleCapsule
|
|
|
251
332
|
@custom_capsule_id if defined?(@custom_capsule_id)
|
|
252
333
|
end
|
|
253
334
|
|
|
254
|
-
# Get inline cache strategy
|
|
335
|
+
# Get inline cache strategy (checks instance variable, then parent class, defaults to nil)
|
|
336
|
+
#
|
|
337
|
+
# @return [Symbol, nil] The cache strategy (default: nil)
|
|
255
338
|
def inline_cache_strategy
|
|
256
|
-
|
|
339
|
+
if defined?(@inline_cache_strategy) && @inline_cache_strategy
|
|
340
|
+
@inline_cache_strategy
|
|
341
|
+
elsif superclass.respond_to?(:inline_cache_strategy, true)
|
|
342
|
+
superclass.inline_cache_strategy
|
|
343
|
+
end
|
|
257
344
|
end
|
|
258
345
|
|
|
259
|
-
# Get inline cache TTL
|
|
346
|
+
# Get inline cache TTL (checks instance variable, then parent class, defaults to nil)
|
|
347
|
+
#
|
|
348
|
+
# @return [Integer, ActiveSupport::Duration, nil] The cache TTL (default: nil)
|
|
260
349
|
def inline_cache_ttl
|
|
261
|
-
|
|
350
|
+
if defined?(@inline_cache_ttl)
|
|
351
|
+
@inline_cache_ttl
|
|
352
|
+
elsif superclass.respond_to?(:inline_cache_ttl, true)
|
|
353
|
+
superclass.inline_cache_ttl
|
|
354
|
+
end
|
|
262
355
|
end
|
|
263
356
|
|
|
264
|
-
# Get inline cache proc
|
|
357
|
+
# Get inline cache proc (checks instance variable, then parent class, defaults to nil)
|
|
358
|
+
#
|
|
359
|
+
# @return [Proc, nil] The cache proc (default: nil)
|
|
265
360
|
def inline_cache_proc
|
|
266
|
-
|
|
361
|
+
if defined?(@inline_cache_proc)
|
|
362
|
+
@inline_cache_proc
|
|
363
|
+
elsif superclass.respond_to?(:inline_cache_proc, true)
|
|
364
|
+
superclass.inline_cache_proc
|
|
365
|
+
end
|
|
267
366
|
end
|
|
268
367
|
|
|
269
|
-
public :head_rendering?, :stylesheet_namespace, :custom_capsule_id, :inline_cache_strategy, :inline_cache_ttl, :inline_cache_proc
|
|
368
|
+
public :head_rendering?, :stylesheet_namespace, :style_capsule, :custom_capsule_id, :inline_cache_strategy, :inline_cache_ttl, :inline_cache_proc
|
|
369
|
+
|
|
370
|
+
# Get CSS scoping strategy (checks instance variable, then parent class, defaults to :selector_patching)
|
|
371
|
+
#
|
|
372
|
+
# @return [Symbol] The current scoping strategy (default: :selector_patching)
|
|
373
|
+
def css_scoping_strategy
|
|
374
|
+
if defined?(@css_scoping_strategy) && @css_scoping_strategy
|
|
375
|
+
@css_scoping_strategy
|
|
376
|
+
elsif superclass.respond_to?(:css_scoping_strategy, true)
|
|
377
|
+
superclass.css_scoping_strategy
|
|
378
|
+
else
|
|
379
|
+
:selector_patching
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
public :css_scoping_strategy
|
|
270
384
|
|
|
271
385
|
# Set or get options for stylesheet_link_tag when using file-based caching
|
|
272
386
|
#
|
|
@@ -289,58 +403,6 @@ module StyleCapsule
|
|
|
289
403
|
end
|
|
290
404
|
|
|
291
405
|
public :stylesheet_link_options
|
|
292
|
-
|
|
293
|
-
# Set or get CSS scoping strategy
|
|
294
|
-
#
|
|
295
|
-
# @param strategy [Symbol, nil] Scoping strategy: :selector_patching (default) or :nesting (omit to get current value)
|
|
296
|
-
# - :selector_patching: Adds [data-capsule="..."] prefix to each selector (better browser support)
|
|
297
|
-
# - :nesting: Wraps entire CSS in [data-capsule="..."] { ... } (more performant, requires CSS nesting support)
|
|
298
|
-
# @return [Symbol] The current scoping strategy (default: :selector_patching)
|
|
299
|
-
# @example Using CSS nesting (requires Chrome 112+, Firefox 117+, Safari 16.5+)
|
|
300
|
-
# class MyComponent < ApplicationComponent
|
|
301
|
-
# include StyleCapsule::Component
|
|
302
|
-
# css_scoping_strategy :nesting # More performant, no CSS parsing needed
|
|
303
|
-
#
|
|
304
|
-
# def component_styles
|
|
305
|
-
# <<~CSS
|
|
306
|
-
# .section { color: red; }
|
|
307
|
-
# .heading:hover { opacity: 0.8; }
|
|
308
|
-
# CSS
|
|
309
|
-
# end
|
|
310
|
-
# end
|
|
311
|
-
# # Output: [data-capsule="abc123"] { .section { color: red; } .heading:hover { opacity: 0.8; } }
|
|
312
|
-
# @example Using selector patching (default, better browser support)
|
|
313
|
-
# class MyComponent < ApplicationComponent
|
|
314
|
-
# include StyleCapsule::Component
|
|
315
|
-
# css_scoping_strategy :selector_patching # Default
|
|
316
|
-
#
|
|
317
|
-
# def component_styles
|
|
318
|
-
# <<~CSS
|
|
319
|
-
# .section { color: red; }
|
|
320
|
-
# CSS
|
|
321
|
-
# end
|
|
322
|
-
# end
|
|
323
|
-
# # Output: [data-capsule="abc123"] .section { color: red; }
|
|
324
|
-
def css_scoping_strategy(strategy = nil)
|
|
325
|
-
if strategy.nil?
|
|
326
|
-
# Check if this class has a strategy set
|
|
327
|
-
if defined?(@css_scoping_strategy) && @css_scoping_strategy
|
|
328
|
-
@css_scoping_strategy
|
|
329
|
-
# Otherwise, check parent class (for inheritance)
|
|
330
|
-
elsif superclass.respond_to?(:css_scoping_strategy, true)
|
|
331
|
-
superclass.css_scoping_strategy
|
|
332
|
-
else
|
|
333
|
-
:selector_patching
|
|
334
|
-
end
|
|
335
|
-
else
|
|
336
|
-
unless [:selector_patching, :nesting].include?(strategy)
|
|
337
|
-
raise ArgumentError, "css_scoping_strategy must be :selector_patching or :nesting (got: #{strategy.inspect})"
|
|
338
|
-
end
|
|
339
|
-
@css_scoping_strategy = strategy
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
public :css_scoping_strategy
|
|
344
406
|
end
|
|
345
407
|
|
|
346
408
|
# Module that wraps view_template to add scoped wrapper
|
|
@@ -463,9 +525,9 @@ module StyleCapsule
|
|
|
463
525
|
# Use the configured scoping strategy
|
|
464
526
|
scoped_css = case scoping_strategy
|
|
465
527
|
when :nesting
|
|
466
|
-
CssProcessor.scope_with_nesting(css_content, capsule_id)
|
|
528
|
+
CssProcessor.scope_with_nesting(css_content, capsule_id, component_class: self.class)
|
|
467
529
|
else # :selector_patching (default)
|
|
468
|
-
CssProcessor.scope_selectors(css_content, capsule_id)
|
|
530
|
+
CssProcessor.scope_selectors(css_content, capsule_id, component_class: self.class)
|
|
469
531
|
end
|
|
470
532
|
|
|
471
533
|
# Cache at class level (one style block per component type/scope/strategy combination)
|