rails-css_unused 0.1.0 → 0.2.1
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 +53 -24
- data/LICENSE.txt +21 -21
- data/README.md +132 -132
- data/lib/rails/css_unused/configuration.rb +97 -45
- data/lib/rails/css_unused/railtie.rb +25 -24
- data/lib/rails/css_unused/report.rb +111 -50
- data/lib/rails/css_unused/spinner.rb +80 -0
- data/lib/rails/css_unused/stylesheet_scanner.rb +149 -82
- data/lib/rails/css_unused/tasks.rake +32 -12
- data/lib/rails/css_unused/version.rb +7 -7
- data/lib/rails/css_unused/view_scanner.rb +324 -117
- data/lib/rails/css_unused.rb +40 -36
- data/lib/rails-css_unused.rb +6 -3
- metadata +10 -7
|
@@ -1,117 +1,324 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Rails
|
|
4
|
-
module CssUnused
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module CssUnused
|
|
5
|
+
# Scans Rails view templates, ViewComponent files, Phlex components,
|
|
6
|
+
# Stimulus controllers, and Ruby files for referenced CSS class names.
|
|
7
|
+
#
|
|
8
|
+
# Handles:
|
|
9
|
+
# ERB: class="foo bar", class: "foo", class: ["foo", "bar"]
|
|
10
|
+
# HAML: .foo.bar, %div.foo
|
|
11
|
+
# Slim: div.foo, .foo
|
|
12
|
+
# Ruby: html_class: "foo", css_classes("foo bar"), "foo bar"
|
|
13
|
+
# Stimulus: this.element.classList.add("foo"), "foo" string literals
|
|
14
|
+
# Dynamic: class="<%= cond ? 'foo' : 'bar' %>" — literal parts extracted
|
|
15
|
+
# Dynamic vars: status_class = "foo-bar" — string assigned to *_class/*_classes var
|
|
16
|
+
class ViewScanner
|
|
17
|
+
# ── ERB / HTML patterns ──────────────────────────────────────────────
|
|
18
|
+
# class="foo bar baz" or class='foo bar'
|
|
19
|
+
HTML_CLASS_ATTR = /class\s*=\s*["']([^"'<>]+)["']/i
|
|
20
|
+
# class: "foo bar" or class: 'foo'
|
|
21
|
+
RUBY_CLASS_KV = /class:\s*["']([^"']+)["']/
|
|
22
|
+
# class: ["foo", "bar"] or class: %w[foo bar]
|
|
23
|
+
RUBY_CLASS_ARRAY = /class:\s*(?:\[|%w\[)\s*([^\]\n]+)/
|
|
24
|
+
# tag.div(class: "foo") content_tag(:div, class: "foo")
|
|
25
|
+
TAG_HELPER = /(?:content_tag|tag\.\w+)\s*[({][^)}\n]*class:\s*["']([^"']+)["']/
|
|
26
|
+
|
|
27
|
+
# ── HAML patterns ───────────────────────────────────────────────────
|
|
28
|
+
# .foo, %div.foo.bar, %span.foo#id
|
|
29
|
+
HAML_IMPLICIT = /^[ \t]*(?:%[\w:-]+)?(\.[a-zA-Z][a-zA-Z0-9_-]*(?:\.[a-zA-Z][a-zA-Z0-9_-]*)*)/
|
|
30
|
+
# Inline { class: "foo" }
|
|
31
|
+
HAML_HASH_CLASS = /class:\s*["']([^"']+)["']/
|
|
32
|
+
|
|
33
|
+
# ── Slim patterns ───────────────────────────────────────────────────
|
|
34
|
+
# div.foo.bar or .foo.bar on its own line
|
|
35
|
+
SLIM_CLASS = /^[ \t]*(?:[\w-]*)(\.[a-zA-Z][a-zA-Z0-9_-]*(?:\.[a-zA-Z][a-zA-Z0-9_-]*)*)/
|
|
36
|
+
|
|
37
|
+
# ── ERB dynamic interpolation ────────────────────────────────────────
|
|
38
|
+
# class="<%= expr %>", class="prefix-<%= var %>" — extracts static parts
|
|
39
|
+
ERB_DYNAMIC_CLASS = /class\s*=\s*["'][^"']*<%=[^%]+%>[^"']*["']/m
|
|
40
|
+
|
|
41
|
+
# ── Dynamic class variable detection (v0.2.1) ────────────────────────
|
|
42
|
+
# Detects string literals assigned to variables whose name ends with
|
|
43
|
+
# _class, _classes, _style, or _css — and the string contains hyphens
|
|
44
|
+
# (Ruby variable names cannot contain hyphens, so it must be a value).
|
|
45
|
+
#
|
|
46
|
+
# Matches patterns like:
|
|
47
|
+
# status_class = "foo-bar"
|
|
48
|
+
# button_classes = "btn btn-primary"
|
|
49
|
+
# ["Active", "status-active"] (array element with hyphenated string)
|
|
50
|
+
# ["Cancelled", "status-cancelled"]
|
|
51
|
+
#
|
|
52
|
+
# Rule: any double- or single-quoted string containing at least one
|
|
53
|
+
# hyphen is unambiguously a string value (not a Ruby identifier), so
|
|
54
|
+
# we can safely extract it as a potential class name.
|
|
55
|
+
#
|
|
56
|
+
# Pattern 1: variable ending in _class/_classes/_style/_css = "value"
|
|
57
|
+
DYNAMIC_CLASS_VAR = /\b\w+_(?:class(?:es)?|style|css)\s*=\s*["']([^"'\n]+)["']/
|
|
58
|
+
#
|
|
59
|
+
# Pattern 2: any quoted string with hyphens in array/tuple context
|
|
60
|
+
# e.g. ["Active", "status-active"] — the hyphenated strings are CSS classes
|
|
61
|
+
HYPHENATED_STRING = /["']([a-zA-Z][a-zA-Z0-9]*(?:-[a-zA-Z0-9]+)+)["']/
|
|
62
|
+
|
|
63
|
+
# ── Ruby / Stimulus string literals ─────────────────────────────────
|
|
64
|
+
# Any double-quoted string that looks like a space-separated class list
|
|
65
|
+
JS_ADD_CLASS = /(?:classList\.add|classList\.toggle|classList\.replace)\s*\(\s*["']([^"']+)["']/
|
|
66
|
+
JS_REMOVE_CLASS = /(?:classList\.remove)\s*\(\s*["']([^"']+)["']/
|
|
67
|
+
RUBY_STRING_CLASSES = /["']([a-zA-Z][a-zA-Z0-9_-]*(?:\s+[a-zA-Z][a-zA-Z0-9_-]*)*)[\"']/
|
|
68
|
+
|
|
69
|
+
def initialize(root:, config: CssUnused.configuration)
|
|
70
|
+
@root = Pathname(root)
|
|
71
|
+
@config = config
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns a Set of class name strings referenced across all view files.
|
|
75
|
+
def used_classes
|
|
76
|
+
classes = Set.new
|
|
77
|
+
ignore_set = @config.ignore_classes.map(&:to_s).to_set
|
|
78
|
+
ignore_pats = Array(@config.ignore_patterns)
|
|
79
|
+
|
|
80
|
+
each_view_file do |path, content|
|
|
81
|
+
extract_from(content, path).each do |cls|
|
|
82
|
+
next if ignore_set.include?(cls)
|
|
83
|
+
next if ignore_pats.any? { |p| cls.match?(p) }
|
|
84
|
+
classes << cls
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if @config.scan_javascript_for_classes
|
|
89
|
+
each_js_file do |path, content|
|
|
90
|
+
extract_js_classes(content).each { |cls| classes << cls }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
classes
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# ── File iteration ───────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
def each_view_file
|
|
102
|
+
paths = (@config.view_paths + @config.component_paths).uniq
|
|
103
|
+
paths.each do |rel|
|
|
104
|
+
dir = @root.join(rel)
|
|
105
|
+
next unless dir.directory?
|
|
106
|
+
|
|
107
|
+
Dir.glob(dir.join("**", "*")).each do |f|
|
|
108
|
+
path = Pathname(f)
|
|
109
|
+
next unless path.file? && view_file?(path)
|
|
110
|
+
|
|
111
|
+
content = safe_read(path)
|
|
112
|
+
yield path, content if content
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def each_js_file
|
|
118
|
+
@config.javascript_paths.each do |rel|
|
|
119
|
+
dir = @root.join(rel)
|
|
120
|
+
next unless dir.directory?
|
|
121
|
+
|
|
122
|
+
Dir.glob(dir.join("**", "*.{js,ts,jsx,tsx}")).each do |f|
|
|
123
|
+
path = Pathname(f)
|
|
124
|
+
content = safe_read(path)
|
|
125
|
+
yield path, content if content
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
return unless @config.scan_ruby_components
|
|
130
|
+
|
|
131
|
+
@config.component_paths.each do |rel|
|
|
132
|
+
dir = @root.join(rel)
|
|
133
|
+
next unless dir.directory?
|
|
134
|
+
|
|
135
|
+
Dir.glob(dir.join("**", "*.rb")).each do |f|
|
|
136
|
+
path = Pathname(f)
|
|
137
|
+
content = safe_read(path)
|
|
138
|
+
yield path, content if content
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def view_file?(path)
|
|
144
|
+
name = path.basename.to_s
|
|
145
|
+
Configuration::VIEW_EXTENSIONS.any? { |ext| name.end_with?(ext) } ||
|
|
146
|
+
Configuration::COMPOUND_VIEW_ENDINGS.any? { |ext| name.end_with?(ext) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# ── Class extraction ─────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
def extract_from(content, path)
|
|
152
|
+
found = Set.new
|
|
153
|
+
ext = path.extname
|
|
154
|
+
base = path.basename.to_s
|
|
155
|
+
|
|
156
|
+
if ext == ".erb" || base.end_with?(".html.erb")
|
|
157
|
+
found.merge extract_erb(content)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if ext == ".haml" || base.end_with?(".html.haml")
|
|
161
|
+
found.merge extract_haml(content)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if ext == ".slim" || base.end_with?(".html.slim")
|
|
165
|
+
found.merge extract_slim(content)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if ext == ".rb"
|
|
169
|
+
found.merge extract_ruby(content)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
found
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def extract_erb(content)
|
|
176
|
+
found = Set.new
|
|
177
|
+
|
|
178
|
+
# Standard class attributes
|
|
179
|
+
[HTML_CLASS_ATTR, RUBY_CLASS_KV, TAG_HELPER].each do |pat|
|
|
180
|
+
content.scan(pat) { |m| found.merge tokenize(m[0]) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# class: ["foo", "bar"] / class: %w[foo bar]
|
|
184
|
+
content.scan(RUBY_CLASS_ARRAY) do |m|
|
|
185
|
+
found.merge tokenize(m[0].gsub(/["',]/, " "))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# ERB dynamic class attributes — extract static string parts
|
|
189
|
+
content.scan(ERB_DYNAMIC_CLASS) do
|
|
190
|
+
chunk = Regexp.last_match(0)
|
|
191
|
+
stripped = chunk.gsub(/<%.*?%>/m, " ")
|
|
192
|
+
stripped.scan(/["']([^"']+)["']/) { |m| found.merge tokenize(m[0]) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ── v0.2.1: Dynamic class variable detection ──────────────────────
|
|
196
|
+
# Detects: status_class = "foo-bar" or button_classes = "btn btn-primary"
|
|
197
|
+
found.merge extract_dynamic_class_vars(content)
|
|
198
|
+
|
|
199
|
+
found
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def extract_haml(content)
|
|
203
|
+
found = Set.new
|
|
204
|
+
|
|
205
|
+
content.scan(HAML_IMPLICIT) do |m|
|
|
206
|
+
m[0].split(".").reject(&:empty?).each do |cls|
|
|
207
|
+
found << cls if valid_class?(cls)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
content.scan(HAML_HASH_CLASS) { |m| found.merge tokenize(m[0]) }
|
|
212
|
+
content.scan(RUBY_CLASS_KV) { |m| found.merge tokenize(m[0]) }
|
|
213
|
+
|
|
214
|
+
# v0.2.1: detect dynamic class vars in HAML Ruby blocks too
|
|
215
|
+
found.merge extract_dynamic_class_vars(content)
|
|
216
|
+
|
|
217
|
+
found
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def extract_slim(content)
|
|
221
|
+
found = Set.new
|
|
222
|
+
|
|
223
|
+
content.scan(SLIM_CLASS) do |m|
|
|
224
|
+
m[0].split(".").reject(&:empty?).each do |cls|
|
|
225
|
+
found << cls if valid_class?(cls)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
content.scan(RUBY_CLASS_KV) { |m| found.merge tokenize(m[0]) }
|
|
230
|
+
|
|
231
|
+
# v0.2.1: detect dynamic class vars in Slim Ruby blocks too
|
|
232
|
+
found.merge extract_dynamic_class_vars(content)
|
|
233
|
+
|
|
234
|
+
found
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def extract_ruby(content)
|
|
238
|
+
found = Set.new
|
|
239
|
+
|
|
240
|
+
content.scan(RUBY_CLASS_KV) { |m| found.merge tokenize(m[0]) }
|
|
241
|
+
content.scan(RUBY_CLASS_ARRAY) { |m| found.merge tokenize(m[0].gsub(/["',]/, " ")) }
|
|
242
|
+
|
|
243
|
+
content.scan(RUBY_STRING_CLASSES) do |m|
|
|
244
|
+
tokens = tokenize(m[0])
|
|
245
|
+
found.merge(tokens) if tokens.all? { |t| valid_class?(t) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# v0.2.1: detect dynamic class vars in Ruby component files
|
|
249
|
+
found.merge extract_dynamic_class_vars(content)
|
|
250
|
+
|
|
251
|
+
found
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def extract_js_classes(content)
|
|
255
|
+
found = Set.new
|
|
256
|
+
content.scan(JS_ADD_CLASS) { |m| found.merge tokenize(m[0]) }
|
|
257
|
+
content.scan(JS_REMOVE_CLASS) { |m| found.merge tokenize(m[0]) }
|
|
258
|
+
found
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# ── v0.2.1: Smart dynamic class variable extraction ──────────────────
|
|
262
|
+
#
|
|
263
|
+
# Scans content for two patterns:
|
|
264
|
+
#
|
|
265
|
+
# 1. Variables named *_class, *_classes, *_style, *_css assigned a string:
|
|
266
|
+
# status_class = "foo-bar" => extracts "foo-bar"
|
|
267
|
+
# button_classes = "btn btn-sm" => extracts "btn", "btn-sm"
|
|
268
|
+
#
|
|
269
|
+
# 2. Any quoted string containing a hyphen (unambiguous: Ruby variable
|
|
270
|
+
# names cannot contain hyphens, so hyphenated quoted strings MUST be
|
|
271
|
+
# string values, never variable names):
|
|
272
|
+
# ["Active", "status-active"] => extracts "status-active"
|
|
273
|
+
# ["Cancelled", "status-cancelled"] => extracts "status-cancelled"
|
|
274
|
+
#
|
|
275
|
+
# This directly solves the false-positive problem where classes like
|
|
276
|
+
# status-approved, status-cancelled, status-requested were flagged as
|
|
277
|
+
# ghost classes because they were assigned to a variable (status_class)
|
|
278
|
+
# and used via <%= status_class %> interpolation.
|
|
279
|
+
def extract_dynamic_class_vars(content)
|
|
280
|
+
found = Set.new
|
|
281
|
+
|
|
282
|
+
# Pattern 1: *_class/*_classes variable assignments
|
|
283
|
+
content.scan(DYNAMIC_CLASS_VAR) do |m|
|
|
284
|
+
tokenize(m[0]).each { |cls| found << cls if valid_class?(cls) }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Pattern 2: hyphenated string literals (unambiguously CSS class values)
|
|
288
|
+
content.scan(HYPHENATED_STRING) do |m|
|
|
289
|
+
cls = m[0].strip
|
|
290
|
+
found << cls if valid_class?(cls)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
found
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# ── Helpers ───────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
def tokenize(raw)
|
|
299
|
+
return Set.new if raw.nil?
|
|
300
|
+
|
|
301
|
+
raw
|
|
302
|
+
.gsub(/["']/, " ")
|
|
303
|
+
.split(/[\s,]+/)
|
|
304
|
+
.map { |t| t.strip.delete_prefix(".") }
|
|
305
|
+
.reject(&:empty?)
|
|
306
|
+
.reject { |t| t.include?("<%") || t.include?('#{') }
|
|
307
|
+
.select { |t| valid_class?(t) }
|
|
308
|
+
.to_set
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def valid_class?(token)
|
|
312
|
+
token.match?(/\A-?[a-zA-Z_][a-zA-Z0-9_-]*\z/)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def safe_read(path)
|
|
316
|
+
path.read(encoding: "UTF-8")
|
|
317
|
+
rescue ArgumentError, Encoding::UndefinedConversionError
|
|
318
|
+
path.read(encoding: "BINARY").encode("UTF-8", invalid: :replace, undef: :replace)
|
|
319
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
320
|
+
nil
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
data/lib/rails/css_unused.rb
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "pathname"
|
|
4
|
-
require "set"
|
|
5
|
-
|
|
6
|
-
require_relative "css_unused/version"
|
|
7
|
-
require_relative "css_unused/configuration"
|
|
8
|
-
require_relative "css_unused/
|
|
9
|
-
require_relative "css_unused/
|
|
10
|
-
require_relative "css_unused/report"
|
|
11
|
-
|
|
12
|
-
module Rails
|
|
13
|
-
module CssUnused
|
|
14
|
-
class Error < StandardError; end
|
|
15
|
-
|
|
16
|
-
class << self
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
require_relative "css_unused/version"
|
|
7
|
+
require_relative "css_unused/configuration"
|
|
8
|
+
require_relative "css_unused/stylesheet_scanner"
|
|
9
|
+
require_relative "css_unused/view_scanner"
|
|
10
|
+
require_relative "css_unused/report"
|
|
11
|
+
|
|
12
|
+
module Rails
|
|
13
|
+
module CssUnused
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Prints a full report to output (default: $stdout).
|
|
18
|
+
# Returns the exit code (0 = clean, 1 = ghosts found + fail_on_unused).
|
|
19
|
+
def report(root: default_root, output: $stdout)
|
|
20
|
+
Report.new(root: root, output: output).print_summary
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns an array of unused class name strings.
|
|
24
|
+
def ghost_classes(root: default_root)
|
|
25
|
+
Report.new(root: root).ghost_classes.map(&:class_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the project root: Rails.root if inside Rails, else cwd.
|
|
29
|
+
def default_root
|
|
30
|
+
if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
|
|
31
|
+
::Rails.root
|
|
32
|
+
else
|
|
33
|
+
Pathname.new(Dir.pwd)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
require_relative "css_unused/railtie" if defined?(Rails::Railtie)
|
data/lib/rails-css_unused.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-css_unused
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- sghani001
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -59,9 +59,11 @@ dependencies:
|
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
60
|
version: '3.12'
|
|
61
61
|
description: |
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
rails-css_unused scans your stylesheets (CSS/SCSS/Sass) and all view templates
|
|
63
|
+
(ERB, HAML, Slim), ViewComponents, Phlex components, and Stimulus JS controllers
|
|
64
|
+
to report CSS class selectors that are defined but never referenced. Supports
|
|
65
|
+
BEM naming, dynamic class detection, source-file attribution, CI exit codes,
|
|
66
|
+
and configurable ignore lists — zero runtime overhead.
|
|
65
67
|
email:
|
|
66
68
|
- sghani001@users.noreply.github.com
|
|
67
69
|
executables: []
|
|
@@ -76,6 +78,7 @@ files:
|
|
|
76
78
|
- lib/rails/css_unused/configuration.rb
|
|
77
79
|
- lib/rails/css_unused/railtie.rb
|
|
78
80
|
- lib/rails/css_unused/report.rb
|
|
81
|
+
- lib/rails/css_unused/spinner.rb
|
|
79
82
|
- lib/rails/css_unused/stylesheet_scanner.rb
|
|
80
83
|
- lib/rails/css_unused/tasks.rake
|
|
81
84
|
- lib/rails/css_unused/version.rb
|
|
@@ -107,5 +110,5 @@ requirements: []
|
|
|
107
110
|
rubygems_version: 3.5.3
|
|
108
111
|
signing_key:
|
|
109
112
|
specification_version: 4
|
|
110
|
-
summary: Find unused CSS classes in Rails apps
|
|
113
|
+
summary: Find unused CSS classes in Rails apps — fast, accurate static analysis
|
|
111
114
|
test_files: []
|