stimulus-audit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d97ee4d76118febf8f6d72f4434bdcef35e925dd5871bbc624bfb221e0cd0fd3
4
+ data.tar.gz: a54d0519141cced0080073a8713ae0d923e0452b0203817543cc1b99ed95c44d
5
+ SHA512:
6
+ metadata.gz: '0458a1420dfd0419ceefe18b4a70e8ba676690c99e700b21a594cedf7d397f41ac884555233fb6bf6dccf9e13bfc48f7f7f54b3dcd5cb30badb71ce387f42b89'
7
+ data.tar.gz: 67e254902d28bdf8abfa7d268cc6f603ffe6893420c7fab4652d9eed3b2cac0c5cd8899cf0387da938b25fb007db7fff8068f70215b7a4164da4d1108222a459
data/.rubocop.yml ADDED
@@ -0,0 +1,13 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-12-11
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # Stimulus Audit
2
+
3
+ A Ruby gem to analyze Stimulus.js controller usage in your Rails application. Find unused controllers, undefined controllers, and audit your Stimulus controller usage.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'stimulus-audit'
11
+ ```
12
+
13
+ And then execute:
14
+ ```bash
15
+ bundle install
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Audit All Controllers
21
+
22
+ Run an audit to see all defined and used controllers in your application:
23
+
24
+ ```bash
25
+ rails stimulus:audit
26
+ ```
27
+
28
+ This will show:
29
+ - Controllers that are defined but never used
30
+ - Controllers that are used but don't have corresponding files
31
+ - Active controllers and where they're being used
32
+ - Summary statistics
33
+
34
+ Example output:
35
+ ```
36
+ 📊 Stimulus Controller Audit
37
+
38
+ ❌ Defined but unused controllers:
39
+ unused_feature
40
+ └─ app/javascript/controllers/unused_feature_controller.js
41
+
42
+ ⚠️ Used but undefined controllers:
43
+ missing_controller
44
+ └─ app/views/products/show.html.erb (lines: 15, 23)
45
+
46
+ ✅ Active controllers:
47
+ products
48
+ └─ Defined in: app/javascript/controllers/products_controller.js
49
+ └─ Used in:
50
+ └─ app/views/products/index.html.erb (lines: 10, 45)
51
+ └─ app/components/product_card/component.html.erb (lines: 3)
52
+ ```
53
+
54
+ ### Scan for Specific Controller Usage
55
+
56
+ Find all uses of a specific controller:
57
+
58
+ ```bash
59
+ rails stimulus:scan[controller_name]
60
+ ```
61
+
62
+ Example:
63
+ ```bash
64
+ rails stimulus:scan[products]
65
+ rails stimulus:scan[users--name] # For namespaced controllers
66
+ ```
67
+
68
+ ### Configuration
69
+
70
+ You can customize the paths that are scanned in an initializer (`config/initializers/stimulus_audit.rb`):
71
+
72
+ ```ruby
73
+ StimulusAudit.configure do |config|
74
+ config.view_paths = [
75
+ Rails.root.join('app/views/**/*.{html,erb,haml}'),
76
+ Rails.root.join('app/javascript/**/*.{js,jsx}'),
77
+ Rails.root.join('app/components/**/*.{html,erb,haml,rb}')
78
+ ]
79
+
80
+ config.controller_paths = [
81
+ Rails.root.join('app/javascript/controllers/**/*.{js,ts}')
82
+ ]
83
+ end
84
+ ```
85
+
86
+ ## Features
87
+
88
+ - Finds unused Stimulus controllers
89
+ - Detects controllers used in views but missing controller files
90
+ - Supports namespaced controllers (e.g., `users--name`)
91
+ - Handles multiple syntax styles:
92
+ ```ruby
93
+ # HTML attribute syntax
94
+ <div data-controller="products">
95
+
96
+ # Ruby hash syntax
97
+ <%= f.submit 'Save', data: { controller: 'products' } %>
98
+
99
+ # Hash rocket syntax
100
+ <%= f.submit 'Save', data: { :controller => 'products' } %>
101
+ ```
102
+ - Scans ERB, HTML, and HAML files
103
+ - Works with both JavaScript and TypeScript controller files
104
+ - Supports component-based architectures
105
+
106
+ ## Development
107
+
108
+ After checking out the repo:
109
+
110
+ 1. Run `bundle install` to install dependencies
111
+ 2. Run `rake test` to run the tests
112
+ 3. Create a branch for your changes (`git checkout -b my-new-feature`)
113
+ 4. Make your changes and add tests
114
+ 5. Ensure tests pass
115
+
116
+ ## Contributing
117
+
118
+ Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration.
119
+
120
+ ## License
121
+
122
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusAudit
4
+ class Auditor
5
+ def initialize(config = StimulusAudit.configuration)
6
+ @config = config
7
+ end
8
+
9
+ def audit
10
+ defined_controllers = find_defined_controllers
11
+ used_controllers = find_used_controllers
12
+
13
+ AuditResult.new(
14
+ defined_controllers: defined_controllers,
15
+ used_controllers: used_controllers,
16
+ controller_locations: find_controller_locations,
17
+ usage_locations: find_usage_locations
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def find_defined_controllers
24
+ controllers = Set.new
25
+ @config.controller_paths.each do |path|
26
+ Dir.glob(path.to_s).each do |file|
27
+ # Extract relative path from controllers directory
28
+ full_path = Pathname.new(file)
29
+ controllers_dir = full_path.each_filename.find_index("controllers")
30
+
31
+ next unless controllers_dir
32
+
33
+ # Get path components after 'controllers'
34
+ controller_path = full_path.each_filename.to_a[(controllers_dir + 1)..]
35
+ # Remove _controller.js from the last component
36
+ controller_path[-1] = controller_path[-1].sub(/_controller\.(js|ts)$/, "")
37
+ # Join with -- for namespacing
38
+ name = controller_path.join("--")
39
+ controllers << name
40
+ end
41
+ end
42
+ controllers
43
+ end
44
+
45
+ def find_used_controllers
46
+ controllers = Set.new
47
+ patterns = [
48
+ /data-controller=["']([^"']+)["']/, # HTML attribute syntax
49
+ /data:\s*{\s*(?:controller:|:controller\s*=>)\s*["']([^"']+)["']/ # Both hash syntaxes
50
+ ]
51
+
52
+ @config.view_paths.each do |path|
53
+ Dir.glob(path.to_s).each do |file|
54
+ content = File.read(file)
55
+ patterns.each do |pattern|
56
+ content.scan(pattern) do |match|
57
+ # Split in case of multiple controllers: "users--name list toggle"
58
+ match[0].split(/\s+/).each do |controller|
59
+ controllers << controller
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ controllers
66
+ end
67
+
68
+ def find_controller_locations
69
+ locations = {}
70
+ @config.controller_paths.each do |path_pattern|
71
+ Dir.glob(path_pattern).each do |file|
72
+ relative_path = Pathname.new(file).relative_path_from(Dir.pwd)
73
+ controller_path = relative_path.to_s.gsub(%r{^app/javascript/controllers/|_controller\.(js|ts)$}, "")
74
+ name = controller_path.gsub("/", "--")
75
+ locations[name] = relative_path
76
+ end
77
+ end
78
+ locations
79
+ end
80
+
81
+ def find_usage_locations
82
+ locations = Hash.new { |h, k| h[k] = {} }
83
+ patterns = [
84
+ /data-controller=["']([^"']+)["']/,
85
+ /data:\s*{(?:[^}]*\s)?controller:\s*["']([^"']+)["']/,
86
+ /data:\s*{(?:[^}]*\s)?controller\s*=>\s*["']([^"']+)["']/
87
+ ]
88
+
89
+ @config.view_paths.each do |path_pattern|
90
+ Dir.glob(path_pattern).each do |file|
91
+ File.readlines(file).each_with_index do |line, index|
92
+ patterns.each do |pattern|
93
+ line.scan(pattern) do |match|
94
+ match[0].split(/\s+/).each do |controller|
95
+ relative_path = Pathname.new(file).relative_path_from(Dir.pwd)
96
+ locations[controller][relative_path] ||= []
97
+ locations[controller][relative_path] << index + 1
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ locations
105
+ end
106
+ end
107
+
108
+ class AuditResult
109
+ attr_reader :defined_controllers, :used_controllers,
110
+ :controller_locations, :usage_locations
111
+
112
+ def initialize(defined_controllers:, used_controllers:,
113
+ controller_locations:, usage_locations:)
114
+ @defined_controllers = defined_controllers
115
+ @used_controllers = used_controllers
116
+ @controller_locations = controller_locations
117
+ @usage_locations = usage_locations
118
+ end
119
+
120
+ def unused_controllers
121
+ defined_controllers - used_controllers
122
+ end
123
+
124
+ def undefined_controllers
125
+ used_controllers - defined_controllers
126
+ end
127
+
128
+ def active_controllers
129
+ defined_controllers & used_controllers
130
+ end
131
+
132
+ def to_console
133
+ puts "\n📊 Stimulus Controller Audit\n"
134
+
135
+ if unused_controllers.any?
136
+ puts "\n❌ Defined but unused controllers:"
137
+ unused_controllers.sort.each do |controller|
138
+ puts " #{controller}"
139
+ puts " └─ #{controller_locations[controller]}"
140
+ end
141
+ end
142
+
143
+ if undefined_controllers.any?
144
+ puts "\n⚠️ Used but undefined controllers:"
145
+ undefined_controllers.sort.each do |controller|
146
+ puts " #{controller}"
147
+ usage_locations[controller].each do |file, lines|
148
+ puts " └─ #{file} (lines: #{lines.join(", ")})"
149
+ end
150
+ end
151
+ end
152
+
153
+ if active_controllers.any?
154
+ puts "\n✅ Active controllers:"
155
+ active_controllers.sort.each do |controller|
156
+ puts " #{controller}"
157
+ puts " └─ Defined in: #{controller_locations[controller]}"
158
+ puts " └─ Used in:"
159
+ usage_locations[controller].each do |file, lines|
160
+ puts " └─ #{file} (lines: #{lines.join(", ")})"
161
+ end
162
+ end
163
+ end
164
+
165
+ puts "\n📈 Summary:"
166
+ puts " Total controllers defined: #{defined_controllers.size}"
167
+ puts " Total controllers in use: #{used_controllers.size}"
168
+ puts " Unused controllers: #{unused_controllers.size}"
169
+ puts " Undefined controllers: #{undefined_controllers.size}"
170
+ puts " Properly paired: #{active_controllers.size}"
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusAudit
4
+ class Configuration
5
+ attr_accessor :view_paths, :controller_paths
6
+
7
+ def initialize
8
+ reset
9
+ end
10
+
11
+ def reset
12
+ base_path = StimulusAudit.root
13
+
14
+ @view_paths = [
15
+ base_path.join('app/views/**/*.{html,erb,haml}').to_s,
16
+ base_path.join('app/javascript/**/*.{js,jsx}').to_s,
17
+ base_path.join('app/components/**/*.{html,erb,haml,rb}').to_s
18
+ ]
19
+
20
+ @controller_paths = [
21
+ base_path.join('app/javascript/controllers/**/*.{js,ts}').to_s
22
+ ]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusAudit
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load "tasks/stimulus_audit.rake"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusAudit
4
+ class Scanner
5
+ def initialize(config = StimulusAudit.configuration)
6
+ @config = config
7
+ end
8
+
9
+ def scan(controller)
10
+ matches = find_matches(controller)
11
+ print_results(controller, matches)
12
+ end
13
+
14
+ private
15
+
16
+ def find_matches(controller)
17
+ matches = []
18
+ patterns = [
19
+ /data-controller=["'](?:[^"']*\s)?#{Regexp.escape(controller)}(?:\s[^"']*)?["']/, # HTML attribute
20
+ /data:\s*{\s*(?:controller:|:controller\s*=>)\s*["'](?:[^"']*\s)?#{Regexp.escape(controller)}(?:\s[^"']*)?["']/ # Both hash syntaxes
21
+ ]
22
+
23
+ @config.view_paths.each do |path|
24
+ Dir.glob(path.to_s).each do |file|
25
+ content = File.readlines(file)
26
+ content.each_with_index do |line, index|
27
+ next unless patterns.any? { |pattern| line.match?(pattern) }
28
+
29
+ matches << {
30
+ file: Pathname.new(file).relative_path_from(Pathname.new(Dir.pwd)),
31
+ line_number: index + 1,
32
+ content: line.strip
33
+ }
34
+ end
35
+ end
36
+ end
37
+ matches
38
+ end
39
+
40
+ def print_results(controller, matches)
41
+ puts "\nSearching for stimulus controller: '#{controller}'\n\n"
42
+
43
+ if matches.empty?
44
+ puts "No matches found."
45
+ return
46
+ end
47
+
48
+ current_file = nil
49
+ matches.each do |match|
50
+ if current_file != match[:file]
51
+ puts "📁 #{match[:file]}"
52
+ current_file = match[:file]
53
+ end
54
+ puts " Line #{match[:line_number]}:"
55
+ puts " #{match[:content]}\n\n"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusAudit
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'pathname'
5
+ require "stimulus_audit/version"
6
+ require "stimulus_audit/configuration"
7
+ require "stimulus_audit/auditor"
8
+ require "stimulus_audit/scanner"
9
+
10
+ if defined?(Rails)
11
+ require "rails"
12
+ require "stimulus_audit/railtie"
13
+ end
14
+
15
+ module StimulusAudit
16
+ class << self
17
+ def configuration
18
+ @configuration ||= Configuration.new
19
+ end
20
+
21
+ def configure
22
+ yield(configuration)
23
+ end
24
+
25
+ def reset_configuration!
26
+ @configuration = Configuration.new
27
+ end
28
+
29
+ def root
30
+ if defined?(Rails)
31
+ Rails.root
32
+ else
33
+ Pathname.new(Dir.pwd)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :stimulus do
4
+ desc "Audit Stimulus controllers usage and find orphaned controllers"
5
+ task audit: :environment do
6
+ StimulusAudit::Auditor.new.audit.to_console
7
+ end
8
+
9
+ desc "Scan files for stimulus controller usage (e.g., rake stimulus:scan[users--name])"
10
+ task :scan, [:controller] => :environment do |_, args|
11
+ controller = args[:controller]
12
+ if controller.nil? || controller.empty?
13
+ puts "Please provide a controller name: rake stimulus:scan[controller_name]"
14
+ next
15
+ end
16
+
17
+ StimulusAudit::Scanner.new.scan(controller)
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ module StimulusAudit
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stimulus-audit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Toby
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-12-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A Ruby gem to analyze usage of Stimulus controllers, finding unused controllers
14
+ and undefined controllers
15
+ email:
16
+ - toby@darkroom.tech
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".rubocop.yml"
22
+ - CHANGELOG.md
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/stimulus_audit.rb
27
+ - lib/stimulus_audit/auditor.rb
28
+ - lib/stimulus_audit/configuration.rb
29
+ - lib/stimulus_audit/railtie.rb
30
+ - lib/stimulus_audit/scanner.rb
31
+ - lib/stimulus_audit/version.rb
32
+ - lib/tasks/stimulus_audit.rake
33
+ - sig/stimulus_audit.rbs
34
+ homepage: https://github.com/yourusername/stimulus-audit
35
+ licenses:
36
+ - MIT
37
+ metadata:
38
+ homepage_uri: https://github.com/yourusername/stimulus-audit
39
+ source_code_uri: https://github.com/yourusername/stimulus-audit
40
+ allowed_push_host: https://rubygems.org
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 3.3.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.5.3
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Audit Stimulus.js controllers in your Rails application
60
+ test_files: []