view_component_css_dsl 0.1.3 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a27107bec7822ef21cc584498087a8d9cdf1ecf044bc5123754f05d5833dd869
4
- data.tar.gz: 38f741bebdd9e6768008eaa64dfdb9b1bb9b07be4d2acdb038d3f79348a91d40
3
+ metadata.gz: 494769078a576e9a1f7d3be9b2819ed2c93bf148da58154c68356c01410d9e8f
4
+ data.tar.gz: 529f35969651591c73f2c21012ab828c84305f94df5317f612fc4dc13b75cc39
5
5
  SHA512:
6
- metadata.gz: d263fbe75515058bf95d6f3286dc9a59df3112b3c61008a7c44dc3e5ffc0c80bc23b7986801fc2abef88457aa36607689bef05643094c00d199ad28dfd94a8cc
7
- data.tar.gz: ffe238ba9cbb6e398001a36fa533ead12f15e1244d9596481356b0d3e478ed8844b2768eadca5be96a90e4677b538e3f46919ef359d7efc6fad93aaf25d076f6
6
+ metadata.gz: 365ce4701b070176cfda8d7a7dcb8005011326d3b888315ce0d37ba80cd2026bc8b5e3dcf8833747911e1ad5e5ca4d752ec28cf6dfa6b058240b847950bd1ca3
7
+ data.tar.gz: fc2ff970cc016c1fe8c788c622af0894c8d0c701afa9861fc40bc34c7926c3d2c19912c3d51287cf43da6d68c826fb4f4163e75ccb1714b874fb7c0621ff48ac
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
 
4
4
 
5
+ ## [0.1.4] - 2026-06-04
6
+
7
+ ### Added
8
+
9
+ - ViewComponentCssDsl::Verifier — six static checks for component declarations: Tailwind class validity (via a compiled-CSS oracle), self-conflicting declarations, method-rule resolution, axis settability, variant-matrix smoke, and template html_attrs splat coverage (4be2024)
10
+
5
11
  ## [0.1.3] - 2026-05-18
6
12
 
7
13
  ### Changed
@@ -11,9 +17,3 @@
11
17
  ### Breaking
12
18
 
13
19
  - `hidden` is now treated as part of the display-class conflict group. Strings like `"flex hidden"` collapse to `"hidden"` rather than keeping both. Previous behavior was non-standard — `tailwind-merge` (JS) and `tailwind_merge` (Ruby) have always treated `hidden` as a display utility. For JS-toggle visibility patterns, use the HTML5 `hidden` attribute (e.g., `attribute hidden: -> { … }` or pass `hidden: true` as an html_attr) and toggle it with `element.toggleAttribute('hidden')` instead of relying on the class merger.
14
-
15
- ## [0.1.2] - 2026-05-15
16
-
17
- ### Added
18
-
19
- - data, aria, and attribute DSL declarators for first-class HTML attribute declarations (cf37050)
data/README.md CHANGED
@@ -458,6 +458,119 @@ end
458
458
 
459
459
  Axis, method, and proc rules are appended, not overridden.
460
460
 
461
+ ## Verifier
462
+
463
+ The DSL's worst failure modes are silent: a typo'd or hallucinated Tailwind class produces no CSS at all under JIT, a self-conflicting declaration quietly drops a class, and a rule referencing a missing method only raises at render time on the code path that hits it. `ViewComponentCssDsl::Verifier` catches all of these statically — fast enough to run on every edit.
464
+
465
+ `verify(component)` returns `Finding` structs (`component`, `check`, `severity`, `message`). Six checks run:
466
+
467
+ | Check | Asserts | Catches |
468
+ | --- | --- | --- |
469
+ | `class_validity` | Every declared class exists in the compiled Tailwind output | Typos, hallucinated classes, theme values that don't exist |
470
+ | `self_conflicts` | No declaration conflicts with itself | `css "block flex"` silently dropping `block` |
471
+ | `method_rules` | Every Symbol in `css`/`data`/`aria`/`attribute` rules resolves to a method | Render-time `NoMethodError`s |
472
+ | `axes_settable` | Every axis has an initialize param or `@ivar` assignment | Variant rules that can never fire |
473
+ | `variant_matrix` | `#css` builds cleanly for every axis-value combination | Anything the static checks miss, without rendering |
474
+ | `template_splat` | Every template references `html_attrs` | Components whose DSL output never reaches the DOM |
475
+
476
+ Notes:
477
+
478
+ - **Verify every class in the hierarchy**, abstract bases included. Declaration checks only inspect what each class itself declared, so a parent's mistakes are reported once — on the parent.
479
+ - `known_classes:` is anything responding to `include?(String)`. `CompiledCssOracle` parses class selectors out of a compiled Tailwind build; since Tailwind's JIT generates a rule for every valid class found in your content globs, a declared class missing from the output is invalid. Rebuild before verifying — the oracle is only as fresh as the build. Omit `known_classes:` to skip the check.
480
+ - `template_splat` covers sidecar files, inline templates (`erb_template "..."`), and hand-written `#call` methods.
481
+ - The variant matrix smoke-tests on bare (`allocate`d) instances. Method and proc rules that need `initialize` state report as warnings rather than errors.
482
+
483
+ ### Wiring it into your app
484
+
485
+ Three layers, from "can't forget" to "instant feedback". The spec is the only one you need — it makes violations fail CI with no one having to remember anything. The other two make the feedback faster.
486
+
487
+ #### 1. A spec — the floor
488
+
489
+ Add this once and every component, present and future, is verified on every test run. `descendants` discovers components dynamically, so new ones are covered the day they're written:
490
+
491
+ ```ruby
492
+ # spec/components/css_dsl_verification_spec.rb
493
+ require "rails_helper"
494
+ require "view_component_css_dsl/verifier"
495
+
496
+ RSpec.describe "CssDsl verification" do
497
+ it "has no verifier errors in any component" do
498
+ Rails.application.eager_load!
499
+
500
+ oracle = ViewComponentCssDsl::Verifier::CompiledCssOracle.new(
501
+ Rails.root.join("app/assets/builds/tailwind.css")
502
+ )
503
+ verifier = ViewComponentCssDsl::Verifier.new(known_classes: oracle)
504
+ findings = ApplicationComponent.descendants.flat_map { |c| verifier.verify(c) }
505
+
506
+ errors = findings.select(&:error?)
507
+ expect(errors).to be_empty, errors.join("\n")
508
+ end
509
+ end
510
+ ```
511
+
512
+ Point the oracle at your compiled Tailwind output and make sure your CI builds it before running specs.
513
+
514
+ #### 2. `bin/verify-css-dsl` — the edit loop
515
+
516
+ Verifies just the components you're working on (or everything, with no args) without waiting for the suite:
517
+
518
+ ```ruby
519
+ #!/usr/bin/env ruby
520
+ # Usage: bin/verify-css-dsl [path/to/component.rb ...]
521
+ # With no args, verifies every component.
522
+
523
+ require_relative "../config/environment"
524
+ require "view_component_css_dsl/verifier"
525
+
526
+ Rails.application.eager_load!
527
+
528
+ components = if ARGV.any?
529
+ components_root = Rails.root.join("app/components")
530
+ ARGV.map do |path|
531
+ rel = Pathname.new(path).expand_path.relative_path_from(components_root)
532
+ rel.to_s.delete_suffix(".rb").camelize.constantize
533
+ end
534
+ else
535
+ ApplicationComponent.descendants
536
+ end
537
+
538
+ oracle = ViewComponentCssDsl::Verifier::CompiledCssOracle.new(
539
+ Rails.root.join("app/assets/builds/tailwind.css")
540
+ )
541
+ verifier = ViewComponentCssDsl::Verifier.new(known_classes: oracle)
542
+
543
+ findings = components.flat_map { |component| verifier.verify(component) }
544
+ puts findings
545
+ exit 1 if findings.any?(&:error?)
546
+ ```
547
+
548
+ Make it executable with `chmod +x bin/verify-css-dsl`.
549
+
550
+ #### 3. A Claude Code hook — the agent loop
551
+
552
+ Runs the bin script automatically after Claude Code edits a component file and feeds any findings straight back to the model, so a hallucinated class is flagged the moment it's written rather than when the suite runs. In your project's `.claude/settings.json`:
553
+
554
+ ```json
555
+ {
556
+ "hooks": {
557
+ "PostToolUse": [
558
+ {
559
+ "matcher": "Edit|Write",
560
+ "hooks": [
561
+ {
562
+ "type": "command",
563
+ "command": "f=$(jq -r .tool_input.file_path); case \"$f\" in */app/components/*.rb) bin/verify-css-dsl \"$f\" >&2 || exit 2;; esac"
564
+ }
565
+ ]
566
+ }
567
+ ]
568
+ }
569
+ }
570
+ ```
571
+
572
+ Exit code 2 returns the findings to Claude as feedback to act on; clean edits stay silent. Note the hook boots your Rails app on each component edit — if that's slow, bootsnap helps.
573
+
461
574
  ## Development
462
575
 
463
576
  ```sh
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../verifier"
4
+
5
+ # The class-validity oracle for Verifier, built from a compiled Tailwind CSS file.
6
+ # Tailwind's JIT generates a rule for every valid class it finds in your content
7
+ # globs — so as long as your component .rb files are in those globs, the compiled
8
+ # output contains exactly the valid classes among those you declared. A declared
9
+ # class missing from the output is a typo, a hallucination, or a value your theme
10
+ # doesn't define.
11
+ #
12
+ # oracle = ViewComponentCssDsl::Verifier::CompiledCssOracle.new(
13
+ # "app/assets/builds/tailwind.css"
14
+ # )
15
+ # oracle.include?("bg-blue-500") # => true
16
+ # oracle.include?("bg-blurple") # => false
17
+ #
18
+ # Caveat: the oracle is only as fresh as the build. Rebuild Tailwind before
19
+ # verifying, or a just-added valid class will be flagged as unknown.
20
+ class ViewComponentCssDsl::Verifier::CompiledCssOracle
21
+ # Class selector: a dot, then word chars / hyphens / CSS escapes. Tailwind
22
+ # escapes special chars with a backslash (`.hover\:bg-blue-500`) and leading
23
+ # digits as hex (`.\32 xl\:grid`).
24
+ CLASS_SELECTOR = /\.((?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w-])+)/
25
+
26
+ def initialize(css_path)
27
+ @classes = parse(File.read(css_path))
28
+ end
29
+
30
+ def include?(class_name) = @classes.include?(class_name)
31
+
32
+ def size = @classes.size
33
+
34
+ private
35
+
36
+ def parse(css)
37
+ css.scan(CLASS_SELECTOR).map { |(selector)| unescape(selector) }.to_set
38
+ end
39
+
40
+ # Hex escapes first (`\32 ` -> "2"), then simple escapes (`\:` -> ":").
41
+ def unescape(selector)
42
+ selector
43
+ .gsub(/\\([0-9a-fA-F]{1,6})\s?/) { $1.hex.chr(Encoding::UTF_8) }
44
+ .gsub(/\\(.)/, '\1')
45
+ end
46
+ end
@@ -0,0 +1,344 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "view_component_css_dsl"
4
+
5
+ # Static checks over a component's DSL declarations. Catches the mistakes the DSL
6
+ # itself can't surface until render time (or surfaces silently). Designed to be
7
+ # fast enough to run on every edit.
8
+ #
9
+ # The six checks:
10
+ #
11
+ # class_validity - every declared class exists in the compiled Tailwind output;
12
+ # catches typos, hallucinated classes, and theme values that
13
+ # don't exist (requires known_classes:)
14
+ # self_conflicts - no declaration conflicts with itself; catches e.g.
15
+ # css "block flex" silently dropping "block"
16
+ # method_rules - every Symbol in css/data/aria/attribute rules resolves to a
17
+ # method; catches render-time NoMethodErrors
18
+ # axes_settable - every axis has an initialize param or @ivar assignment;
19
+ # catches variant rules that can never fire
20
+ # variant_matrix - #css builds cleanly for every axis-value combination;
21
+ # smoke-catches anything the static checks miss, no rendering
22
+ # template_splat - every template (sidecar, inline erb_template, or manual
23
+ # #call) references html_attrs; catches components whose DSL
24
+ # output never reaches the DOM
25
+ #
26
+ # verifier = ViewComponentCssDsl::Verifier.new(known_classes: oracle)
27
+ # findings = verifier.verify(ButtonComponent)
28
+ #
29
+ # Verify every class in the component hierarchy: declaration-shape checks
30
+ # (class validity, self-conflicts) only inspect declarations the class itself
31
+ # added, so a parent's declarations are checked on the parent, not re-reported
32
+ # on every child.
33
+ class ViewComponentCssDsl::Verifier
34
+ Finding = Struct.new(
35
+ :component, :check, :severity, :message, keyword_init: true
36
+ ) do
37
+ def to_s
38
+ "#{component.name || component.inspect} [#{check}] #{severity}: #{message}"
39
+ end
40
+
41
+ def error? = severity == :error
42
+ end
43
+
44
+ TEMPLATE_EXTENSIONS = %w[erb haml slim].freeze
45
+
46
+ # Safety cap for pathological axis cartesian products.
47
+ VARIANT_MATRIX_CAP = 256
48
+
49
+ # Source file of the DSL itself, for classifying smoke-test backtraces.
50
+ DSL_SOURCE_FILE = File.expand_path("../view_component_css_dsl.rb", __dir__)
51
+
52
+ # known_classes: anything responding to include?(String) — a Set, or a
53
+ # CompiledCssOracle built from your app's compiled Tailwind output. When nil,
54
+ # the class-validity check is skipped.
55
+ def initialize(known_classes: nil)
56
+ @known_classes = known_classes
57
+ end
58
+
59
+ def verify(component)
60
+ check_class_validity(component) +
61
+ check_self_conflicts(component) +
62
+ check_method_rules(component) +
63
+ check_axes_settable(component) +
64
+ check_variant_matrix(component) +
65
+ check_template_splat(component)
66
+ end
67
+
68
+ private
69
+
70
+ # Every statically-declared class must exist in the known-classes oracle.
71
+ # Hallucinated or typo'd classes produce no CSS at all under Tailwind's JIT —
72
+ # no error, no warning — so this is the check that catches them.
73
+ def check_class_validity(component)
74
+ return [] unless @known_classes
75
+
76
+ own_declared_styles(component).flat_map do |label, styles|
77
+ styles.split.reject { |cls| @known_classes.include?(cls) }.map do |cls|
78
+ finding(component, :class_validity, :error,
79
+ "#{label}: unknown class \"#{cls}\" (not in the compiled Tailwind output)")
80
+ end
81
+ end
82
+ end
83
+
84
+ # A single declaration whose classes conflict with each other (e.g. "block flex")
85
+ # is almost always a mistake — smart_merge silently keeps only the last.
86
+ def check_self_conflicts(component)
87
+ own_declared_styles(component).flat_map do |label, styles|
88
+ tokens = styles.split
89
+ survivors = component.smart_merge(styles).split
90
+ results = []
91
+
92
+ duplicates = tokens.tally.select { |_cls, count| count > 1 }.keys
93
+ if duplicates.any?
94
+ results << finding(component, :self_conflicts, :error,
95
+ "#{label}: duplicate class(es) #{quote_list(duplicates)}")
96
+ end
97
+
98
+ dropped = tokens.uniq - survivors
99
+ if dropped.any?
100
+ results << finding(component, :self_conflicts, :error,
101
+ "#{label}: #{quote_list(dropped)} conflict with later classes in the " \
102
+ "same declaration and would be silently dropped")
103
+ end
104
+
105
+ results
106
+ end
107
+ end
108
+
109
+ # Symbols in css/data/aria/attribute rules are method references; each must
110
+ # resolve on the component. The DSL only raises for these at render time.
111
+ def check_method_rules(component)
112
+ method_references(component).filter_map do |label, method_name|
113
+ next if resolves?(component, method_name)
114
+
115
+ finding(component, :method_rules, :error,
116
+ "#{label} references undefined method `#{method_name}`")
117
+ end
118
+ end
119
+
120
+ # Each axis with css rules must be settable: an initialize param of the same
121
+ # name, or an @ivar assignment somewhere in the component's source. An axis
122
+ # nothing sets means its rules can never fire.
123
+ def check_axes_settable(component)
124
+ component._css_defined_axes.keys.filter_map do |axis|
125
+ next if initialize_param?(component, axis)
126
+ next if ivar_assigned_in_source?(component, axis)
127
+
128
+ finding(component, :axes_settable, :error,
129
+ "axis :#{axis} has css rules, but initialize has no #{axis} param and " \
130
+ "no @#{axis} assignment was found")
131
+ end
132
+ end
133
+
134
+ # Smoke test: build the css string for every combination of axis values
135
+ # (including unset) on a bare instance. Exercises validate_axes!, the merge
136
+ # caches, and method/proc rules end-to-end without rendering.
137
+ def check_variant_matrix(component)
138
+ combos = axis_combinations(component)
139
+ findings = []
140
+
141
+ if combos.size > VARIANT_MATRIX_CAP
142
+ findings << finding(component, :variant_matrix, :warning,
143
+ "#{combos.size} axis combinations; only the first #{VARIANT_MATRIX_CAP} " \
144
+ "were smoke-tested")
145
+ combos = combos.first(VARIANT_MATRIX_CAP)
146
+ end
147
+
148
+ combos.each do |combo|
149
+ error = smoke_css(component, combo)
150
+ next unless error
151
+
152
+ findings << finding(component, :variant_matrix, smoke_severity(error),
153
+ "#{combo_label(combo)}: #{error.class}: #{error.message}")
154
+ end
155
+
156
+ findings
157
+ end
158
+
159
+ # Every template must reference html_attrs — that splat is what carries the
160
+ # DSL's classes and attributes to the DOM. Covers sidecar files, inline
161
+ # templates (erb_template "..."), and hand-written #call methods.
162
+ def check_template_splat(component)
163
+ sources = template_sources(component)
164
+ if sources.empty?
165
+ return [finding(component, :template_splat, :warning,
166
+ "no template found (no sidecar file, inline template, or #call method)")]
167
+ end
168
+
169
+ sources.filter_map do |label, source|
170
+ next if source.match?(/\bhtml_attrs\b/)
171
+
172
+ finding(component, :template_splat, :error,
173
+ "#{label} does not reference html_attrs; DSL classes and attributes " \
174
+ "will not reach the DOM")
175
+ end
176
+ end
177
+
178
+ ##################################################################################
179
+ # Declaration collection
180
+ ##################################################################################
181
+
182
+ # [label, styles] pairs for declarations this class itself added. Inherited
183
+ # declarations are checked on the ancestor that declared them.
184
+ def own_declared_styles(component)
185
+ parent = component.superclass
186
+ base = own_rules(component, parent, :_css_base_declarations)
187
+ .map { |styles| ["css \"#{styles}\"", styles] }
188
+ axis = own_rules(component, parent, :_css_axis_rules)
189
+ .map { |rule| [axis_label(rule[:axes]), rule[:styles]] }
190
+ methods = own_rules(component, parent, :_css_method_rules)
191
+ .map { |rule| ["css :#{rule[:method]}", rule[:styles]] }
192
+ base + axis + methods
193
+ end
194
+
195
+ def own_rules(component, parent, attr)
196
+ inherited = parent.respond_to?(attr) ? parent.public_send(attr) : []
197
+ component.public_send(attr) - inherited
198
+ end
199
+
200
+ def axis_label(axes) = "css #{axes.map { |k, v| "#{k}: :#{v}" }.join(", ")}"
201
+
202
+ # [label, method_name] for every Symbol the rules will send to the instance.
203
+ # Inherited rules are included: they run on this class, so they must resolve
204
+ # on this class (a child may legitimately define the method a parent's rule
205
+ # references).
206
+ def method_references(component)
207
+ refs = component._css_method_rules
208
+ .map { |rule| ["css :#{rule[:method]}", rule[:method]] }
209
+
210
+ {data: :_data_rules, aria: :_aria_rules, attribute: :_attribute_rules}
211
+ .each do |namespace, attr|
212
+ component.public_send(attr).each do |rule|
213
+ if rule[:predicate].is_a?(Symbol)
214
+ refs << ["#{namespace} :#{rule[:predicate]} predicate", rule[:predicate]]
215
+ end
216
+ rule[:attrs].each do |key, value|
217
+ refs << ["#{namespace} #{key}: :#{value}", value] if value.is_a?(Symbol)
218
+ end
219
+ end
220
+ end
221
+
222
+ refs
223
+ end
224
+
225
+ def resolves?(component, method_name)
226
+ component.method_defined?(method_name) ||
227
+ component.private_method_defined?(method_name)
228
+ end
229
+
230
+ ##################################################################################
231
+ # Axis settability
232
+ ##################################################################################
233
+
234
+ def initialize_param?(component, axis)
235
+ params = component.instance_method(:initialize).parameters
236
+ params.any? { |_type, name| name == axis }
237
+ end
238
+
239
+ def ivar_assigned_in_source?(component, axis)
240
+ source_paths(component).any? do |path|
241
+ File.read(path).match?(/@#{axis}\b\s*(\|\|)?=[^=~]/)
242
+ end
243
+ end
244
+
245
+ # Source files of the component and its DSL-including ancestors. Stops before
246
+ # ViewComponent::Base — framework internals aren't where axes get assigned.
247
+ def source_paths(component)
248
+ component.ancestors
249
+ .take_while { |mod| !view_component_base?(mod) }
250
+ .filter_map { |mod| mod.identifier if mod.respond_to?(:identifier) }
251
+ .uniq
252
+ .select { |path| File.exist?(path) }
253
+ end
254
+
255
+ def view_component_base?(mod)
256
+ defined?(ViewComponent::Base) && mod == ViewComponent::Base
257
+ end
258
+
259
+ ##################################################################################
260
+ # Variant matrix
261
+ ##################################################################################
262
+
263
+ # Cartesian product over defined axes, each axis taking every declared value
264
+ # plus nil (axis unset). [{}] when no axes — base/method/proc paths still get
265
+ # one smoke run.
266
+ def axis_combinations(component)
267
+ component._css_defined_axes.reduce([{}]) do |combos, (axis, values)|
268
+ combos.flat_map do |combo|
269
+ (values.to_a + [nil]).map { |value| combo.merge(axis => value) }
270
+ end
271
+ end
272
+ end
273
+
274
+ def smoke_css(component, combo)
275
+ instance = component.allocate
276
+ instance.instance_variable_set(:@html_attrs, {})
277
+ combo.each { |axis, value| instance.instance_variable_set(:"@#{axis}", value) }
278
+ instance.css
279
+ nil
280
+ rescue => error
281
+ error
282
+ end
283
+
284
+ # Errors raised inside the DSL are real failures. Errors raised in component
285
+ # code usually mean a method/proc rule needs initialize state a bare instance
286
+ # doesn't have — report those as warnings, not failures.
287
+ def smoke_severity(error)
288
+ first_frame = error.backtrace&.first.to_s
289
+ first_frame.start_with?("#{DSL_SOURCE_FILE}:") ? :error : :warning
290
+ end
291
+
292
+ def combo_label(combo)
293
+ return "with no axes set" if combo.compact.empty?
294
+
295
+ "with #{combo.compact.map { |axis, value| "#{axis}: :#{value}" }.join(", ")}"
296
+ end
297
+
298
+ ##################################################################################
299
+ # Templates
300
+ ##################################################################################
301
+
302
+ def template_sources(component)
303
+ sources = []
304
+
305
+ if component.respond_to?(:__vc_inline_template)
306
+ inline = component.__vc_inline_template
307
+ sources << ["inline template", inline.source] if inline
308
+ end
309
+
310
+ if component.respond_to?(:sidecar_files)
311
+ component.sidecar_files(TEMPLATE_EXTENSIONS).each do |path|
312
+ sources << [File.basename(path), File.read(path)]
313
+ end
314
+ end
315
+
316
+ if sources.empty? && (call_path = manual_call_path(component))
317
+ sources << ["#call (#{File.basename(call_path)})", File.read(call_path)]
318
+ end
319
+
320
+ sources
321
+ end
322
+
323
+ # Path of a hand-written #call, if the component (or a DSL ancestor) defines
324
+ # one. ViewComponent compiles templates into #call too, but only at render
325
+ # time — run the verifier in a fresh process and only manual ones exist.
326
+ def manual_call_path(component)
327
+ return nil unless component.method_defined?(:call)
328
+
329
+ owner = component.instance_method(:call).owner
330
+ return nil if !owner.is_a?(Class) || view_component_base?(owner)
331
+ return nil unless owner.respond_to?(:_css_base)
332
+
333
+ path = component.instance_method(:call).source_location&.first
334
+ path if path && File.exist?(path)
335
+ end
336
+
337
+ def finding(component, check, severity, message)
338
+ Finding.new(component:, check:, severity:, message:)
339
+ end
340
+
341
+ def quote_list(classes) = classes.map { |cls| "\"#{cls}\"" }.join(", ")
342
+ end
343
+
344
+ require_relative "verifier/compiled_css_oracle"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ViewComponentCssDsl
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
@@ -36,6 +36,9 @@ module ViewComponentCssDsl
36
36
 
37
37
  included do
38
38
  class_attribute :_css_base, instance_writer: false, default: ""
39
+ # Base css strings as written, before inheritance merging. The merged _css_base
40
+ # is conflict-free by construction; the Verifier needs the raw declarations.
41
+ class_attribute :_css_base_declarations, instance_writer: false, default: []
39
42
  class_attribute :_css_axis_rules, instance_writer: false, default: []
40
43
  class_attribute :_css_method_rules, instance_writer: false, default: []
41
44
  class_attribute :_css_proc_rules, instance_writer: false, default: []
@@ -79,6 +82,7 @@ module ViewComponentCssDsl
79
82
  when String
80
83
  if options.empty?
81
84
  # Base CSS: css "rounded p-4"
85
+ self._css_base_declarations = _css_base_declarations + [args.first]
82
86
  parent_base_css = superclass.respond_to?(:_css_base) ? superclass._css_base : ""
83
87
  self._css_base = if parent_base_css.present?
84
88
  smart_merge(parent_base_css, args.first)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component_css_dsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Lange
@@ -114,6 +114,8 @@ files:
114
114
  - LICENSE.txt
115
115
  - README.md
116
116
  - lib/view_component_css_dsl.rb
117
+ - lib/view_component_css_dsl/verifier.rb
118
+ - lib/view_component_css_dsl/verifier/compiled_css_oracle.rb
117
119
  - lib/view_component_css_dsl/version.rb
118
120
  homepage: https://github.com/SOFware/view_component_css_dsl
119
121
  licenses: