railsforge 1.0.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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +528 -0
  4. data/bin/railsforge +8 -0
  5. data/lib/railsforge/analyzers/base_analyzer.rb +41 -0
  6. data/lib/railsforge/analyzers/controller_analyzer.rb +83 -0
  7. data/lib/railsforge/analyzers/database_analyzer.rb +55 -0
  8. data/lib/railsforge/analyzers/metrics_analyzer.rb +55 -0
  9. data/lib/railsforge/analyzers/model_analyzer.rb +74 -0
  10. data/lib/railsforge/analyzers/performance_analyzer.rb +161 -0
  11. data/lib/railsforge/analyzers/refactor_analyzer.rb +118 -0
  12. data/lib/railsforge/analyzers/security_analyzer.rb +169 -0
  13. data/lib/railsforge/analyzers/spec_analyzer.rb +58 -0
  14. data/lib/railsforge/api_generator.rb +397 -0
  15. data/lib/railsforge/audit.rb +289 -0
  16. data/lib/railsforge/cli.rb +671 -0
  17. data/lib/railsforge/config.rb +181 -0
  18. data/lib/railsforge/database_analyzer.rb +300 -0
  19. data/lib/railsforge/doctor.rb +250 -0
  20. data/lib/railsforge/feature_generator.rb +560 -0
  21. data/lib/railsforge/generator.rb +313 -0
  22. data/lib/railsforge/generators/base_generator.rb +70 -0
  23. data/lib/railsforge/generators/demo_generator.rb +307 -0
  24. data/lib/railsforge/generators/devops_generator.rb +287 -0
  25. data/lib/railsforge/generators/monitoring_generator.rb +134 -0
  26. data/lib/railsforge/generators/service_generator.rb +122 -0
  27. data/lib/railsforge/generators/stimulus_controller_generator.rb +129 -0
  28. data/lib/railsforge/generators/test_generator.rb +289 -0
  29. data/lib/railsforge/generators/view_component_generator.rb +169 -0
  30. data/lib/railsforge/graph.rb +270 -0
  31. data/lib/railsforge/loader.rb +56 -0
  32. data/lib/railsforge/mailer_generator.rb +191 -0
  33. data/lib/railsforge/plugins/plugin_loader.rb +60 -0
  34. data/lib/railsforge/plugins.rb +30 -0
  35. data/lib/railsforge/profiles/admin_app.yml +49 -0
  36. data/lib/railsforge/profiles/api_only.yml +47 -0
  37. data/lib/railsforge/profiles/blog.yml +47 -0
  38. data/lib/railsforge/profiles/standard.yml +44 -0
  39. data/lib/railsforge/profiles.rb +99 -0
  40. data/lib/railsforge/refactor_analyzer.rb +401 -0
  41. data/lib/railsforge/refactor_controller.rb +277 -0
  42. data/lib/railsforge/refactors/refactor_controller.rb +117 -0
  43. data/lib/railsforge/template_loader.rb +105 -0
  44. data/lib/railsforge/templates/v1/form/spec_template.rb +18 -0
  45. data/lib/railsforge/templates/v1/form/template.rb +28 -0
  46. data/lib/railsforge/templates/v1/job/spec_template.rb +17 -0
  47. data/lib/railsforge/templates/v1/job/template.rb +13 -0
  48. data/lib/railsforge/templates/v1/policy/spec_template.rb +41 -0
  49. data/lib/railsforge/templates/v1/policy/template.rb +57 -0
  50. data/lib/railsforge/templates/v1/presenter/spec_template.rb +12 -0
  51. data/lib/railsforge/templates/v1/presenter/template.rb +13 -0
  52. data/lib/railsforge/templates/v1/query/spec_template.rb +12 -0
  53. data/lib/railsforge/templates/v1/query/template.rb +16 -0
  54. data/lib/railsforge/templates/v1/serializer/spec_template.rb +13 -0
  55. data/lib/railsforge/templates/v1/serializer/template.rb +11 -0
  56. data/lib/railsforge/templates/v1/service/spec_template.rb +12 -0
  57. data/lib/railsforge/templates/v1/service/template.rb +25 -0
  58. data/lib/railsforge/templates/v1/stimulus_controller/template.rb +35 -0
  59. data/lib/railsforge/templates/v1/view_component/template.rb +24 -0
  60. data/lib/railsforge/templates/v2/job/template.rb +49 -0
  61. data/lib/railsforge/templates/v2/query/template.rb +66 -0
  62. data/lib/railsforge/templates/v2/service/spec_template.rb +33 -0
  63. data/lib/railsforge/templates/v2/service/template.rb +71 -0
  64. data/lib/railsforge/templates/v3/job/template.rb +72 -0
  65. data/lib/railsforge/templates/v3/query/spec_template.rb +54 -0
  66. data/lib/railsforge/templates/v3/query/template.rb +115 -0
  67. data/lib/railsforge/templates/v3/service/spec_template.rb +51 -0
  68. data/lib/railsforge/templates/v3/service/template.rb +84 -0
  69. data/lib/railsforge/version.rb +5 -0
  70. data/lib/railsforge/wizard.rb +265 -0
  71. data/lib/railsforge/wizard_tui.rb +286 -0
  72. data/lib/railsforge.rb +13 -0
  73. metadata +216 -0
@@ -0,0 +1,287 @@
1
+ # DevOps generator for RailsForge
2
+ # Generates Dockerfile, docker-compose, and CI/CD configs
3
+
4
+ require 'fileutils'
5
+
6
+ module RailsForge
7
+ module Generators
8
+ # DevOps generator
9
+ class DevopsGenerator < BaseGenerator
10
+ # Initialize the generator
11
+ def initialize(name = "app", options = {})
12
+ super(name, options)
13
+ @docker_image = options[:docker_image] || "railsforge/#{name}"
14
+ @ci_provider = options[:ci_provider] || "github"
15
+ @ruby_version = options[:ruby_version] || "3.2.0"
16
+ @node_version = options[:node_version] || "18"
17
+ end
18
+
19
+ # Generate DevOps files
20
+ def generate
21
+ return "Not in a Rails application directory" unless @base_path
22
+
23
+ results = []
24
+ results << create_dockerfile
25
+ results << create_docker_compose
26
+ results << create_dockerignore
27
+ results << create_github_workflow if @ci_provider == "github"
28
+ results << create_gitlab_ci if @ci_provider == "gitlab"
29
+
30
+ results.join("\n")
31
+ end
32
+
33
+ private
34
+
35
+ # Create Dockerfile
36
+ def create_dockerfile
37
+ dockerfile_path = File.join(@base_path, "Dockerfile")
38
+ return "Skipping Dockerfile (already exists)" if File.exist?(dockerfile_path)
39
+
40
+ content = <<~DOCKERFILE
41
+ # Ruby version
42
+ FROM ruby:#{@ruby_version}-slim
43
+
44
+ # Install system dependencies
45
+ RUN apt-get update && apt-get install -y \\
46
+ build-essential \\
47
+ libpq-dev \\
48
+ curl \\
49
+ git \\
50
+ nodejs \\
51
+ npm \\
52
+ && rm -rf /var/lib/apt/lists/*
53
+
54
+ # Set working directory
55
+ WORKDIR /app
56
+
57
+ # Copy Gemfile
58
+ COPY Gemfile Gemfile.lock ./
59
+
60
+ # Install gems
61
+ RUN bundle install --jobs 4 --retry 3
62
+
63
+ # Copy package.json for assets
64
+ COPY package.json package-lock.json ./
65
+ RUN npm install --production
66
+
67
+ # Copy application code
68
+ COPY . .
69
+
70
+ # Precompile assets
71
+ RUN RAILS_ENV=production SECRET_KEY_BASE=dummy bundle exec rails assets:precompile
72
+
73
+ # Expose port
74
+ EXPOSE 3000
75
+
76
+ # Start server
77
+ CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
78
+ DOCKERFILE
79
+
80
+ File.write(dockerfile_path, content)
81
+ "Created Dockerfile"
82
+ end
83
+
84
+ # Create docker-compose.yml
85
+ def create_docker_compose
86
+ compose_path = File.join(@base_path, "docker-compose.yml")
87
+ return "Skipping docker-compose.yml (already exists)" if File.exist?(compose_path)
88
+
89
+ content = <<~YAML
90
+ version: '3.8'
91
+
92
+ services:
93
+ app:
94
+ build: .
95
+ ports:
96
+ - "3000:3000"
97
+ environment:
98
+ - RAILS_ENV=development
99
+ - DATABASE_HOST=postgres
100
+ - REDIS_HOST=redis
101
+ depends_on:
102
+ - postgres
103
+ - redis
104
+ volumes:
105
+ - .:/app
106
+
107
+ postgres:
108
+ image: postgres:15-alpine
109
+ environment:
110
+ - POSTGRES_USER=rails
111
+ - POSTGRES_PASSWORD=password
112
+ - POSTGRES_DB=app_development
113
+ ports:
114
+ - "5432:5432"
115
+ volumes:
116
+ - postgres_data:/var/lib/postgresql/data
117
+
118
+ redis:
119
+ image: redis:7-alpine
120
+ ports:
121
+ - "6379:6379"
122
+
123
+ sidekiq:
124
+ build: .
125
+ command: bundle exec sidekiq
126
+ environment:
127
+ - RAILS_ENV=development
128
+ - DATABASE_HOST=postgres
129
+ - REDIS_HOST=redis
130
+ depends_on:
131
+ - postgres
132
+ - redis
133
+ volumes:
134
+ - .:/app
135
+
136
+ volumes:
137
+ postgres_data:
138
+ YAML
139
+
140
+ File.write(compose_path, content)
141
+ "Created docker-compose.yml"
142
+ end
143
+
144
+ # Create .dockerignore
145
+ def create_dockerignore
146
+ ignore_path = File.join(@base_path, ".dockerignore")
147
+ return "Skipping .dockerignore (already exists)" if File.exist?(ignore_path)
148
+
149
+ content = <<~IGNORE
150
+ .git
151
+ .github
152
+ .gitignore
153
+ log/*.log
154
+ tmp/
155
+ .bundle/
156
+ node_modules/
157
+ public/assets/
158
+ .DS_Store
159
+ *.swp
160
+ *.swo
161
+ IGNORE
162
+
163
+ File.write(ignore_path, content)
164
+ "Created .dockerignore"
165
+ end
166
+
167
+ # Create GitHub Actions workflow
168
+ def create_github_workflow
169
+ workflow_dir = File.join(@base_path, ".github", "workflows")
170
+ FileUtils.mkdir_p(workflow_dir)
171
+
172
+ workflow_path = File.join(workflow_dir, "ci.yml")
173
+ return "Skipping GitHub workflow (already exists)" if File.exist?(workflow_path)
174
+
175
+ content = <<~YAML
176
+ name: CI
177
+
178
+ on:
179
+ push:
180
+ branches: [main, develop]
181
+ pull_request:
182
+ branches: [main, develop]
183
+
184
+ jobs:
185
+ test:
186
+ runs-on: ubuntu-latest
187
+ services:
188
+ postgres:
189
+ image: postgres:15-alpine
190
+ env:
191
+ POSTGRES_USER: rails
192
+ POSTGRES_PASSWORD: password
193
+ POSTGRES_DB: app_test
194
+ ports:
195
+ - 5432:5432
196
+ redis:
197
+ image: redis:7-alpine
198
+ ports:
199
+ - 6379:6379
200
+
201
+ steps:
202
+ - uses: actions/checkout@v3
203
+ - name: Set up Ruby
204
+ uses: ruby/setup-ruby@v1
205
+ with:
206
+ ruby-version: #{@ruby_version}
207
+ bundler-cache: true
208
+ - name: Install dependencies
209
+ run: |
210
+ bundle install
211
+ npm install
212
+ - name: Setup database
213
+ env:
214
+ RAILS_ENV: test
215
+ POSTGRES_HOST: localhost
216
+ POSTGRES_USER: rails
217
+ POSTGRES_PASSWORD: password
218
+ POSTGRES_DB: app_test
219
+ run: bundle exec rails db:create db:schema:load
220
+ - name: Run tests
221
+ env:
222
+ RAILS_ENV: test
223
+ POSTGRES_HOST: localhost
224
+ POSTGRES_USER: rails
225
+ POSTGRES_PASSWORD: password
226
+ POSTGRES_DB: app_test
227
+ run: bundle exec rspec
228
+ - name: Run linter
229
+ run: bundle exec rubocop
230
+ YAML
231
+
232
+ File.write(workflow_path, content)
233
+ "Created GitHub Actions workflow"
234
+ end
235
+
236
+ # Create GitLab CI config
237
+ def create_gitlab_ci
238
+ ci_path = File.join(@base_path, ".gitlab-ci.yml")
239
+ return "Skipping .gitlab-ci.yml (already exists)" if File.exist?(ci_path)
240
+
241
+ content = <<~YAML
242
+ stages:
243
+ - test
244
+ - lint
245
+ - deploy
246
+
247
+ variables:
248
+ RUBY_VERSION: #{@ruby_version}
249
+ POSTGRES_DB: app_test
250
+ POSTGRES_USER: rails
251
+ POSTGRES_PASSWORD: password
252
+
253
+ test:
254
+ stage: test
255
+ image: ruby:#{@ruby_version}
256
+ services:
257
+ - postgres:15-alpine
258
+ - redis:7-alpine
259
+ before_script:
260
+ - apt-get update -qq && apt-get install -y -qq nodejs npm
261
+ - bundle install
262
+ - npm install
263
+ script:
264
+ - RAILS_ENV=test bundle exec rails db:create db:schema:load
265
+ - bundle exec rspec
266
+ only:
267
+ - main
268
+ - develop
269
+
270
+ rubocop:
271
+ stage: lint
272
+ image: ruby:#{@ruby_version}
273
+ before_script:
274
+ - bundle install
275
+ script:
276
+ - bundle exec rubocop
277
+ only:
278
+ - main
279
+ - develop
280
+ YAML
281
+
282
+ File.write(ci_path, content)
283
+ "Created GitLab CI config"
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,134 @@
1
+ # Monitoring generator for RailsForge
2
+ # Generates Sentry and Lograge configurations
3
+
4
+ require 'fileutils'
5
+
6
+ module RailsForge
7
+ module Generators
8
+ # Monitoring generator
9
+ class MonitoringGenerator < BaseGenerator
10
+ # Initialize the generator
11
+ def initialize(name = "monitoring", options = {})
12
+ super(name, options)
13
+ @sentry_dsn = options[:sentry_dsn] || ""
14
+ @environment = options[:environment] || (defined?(Rails) && Rails.env) || 'development'
15
+ end
16
+
17
+ # Generate monitoring configs
18
+ def generate
19
+ return "Not in a Rails application directory" unless @base_path
20
+
21
+ results = []
22
+ results << create_sentry_initializer
23
+ results << create_lograge_config
24
+ results << create_log_formatter
25
+ results << create_environments_config
26
+
27
+ results.join("\n")
28
+ end
29
+
30
+ private
31
+
32
+ # Create Sentry initializer
33
+ def create_sentry_initializer
34
+ init_dir = File.join(@base_path, "config", "initializers")
35
+ FileUtils.mkdir_p(init_dir)
36
+
37
+ sentry_path = File.join(init_dir, "sentry.rb")
38
+ return "Skipping Sentry initializer (already exists)" if File.exist?(sentry_path)
39
+
40
+ content = <<~RUBY
41
+ # Sentry error tracking configuration
42
+ # Install sentry-ruby and add to Gemfile: gem 'sentry-ruby', '~> 4.0'
43
+ Sentry.init do |config|
44
+ config.dsn = ENV.fetch('SENTRY_DSN', '#{@sentry_dsn}')
45
+ config.breadcrumb_logger = :active_support
46
+ config.environments = %w[staging production]
47
+ config.traces_sample_rate = 0.1
48
+ config.before_send = lambda do |event, hint|
49
+ # Filter out specific errors
50
+ if hint[:exception]
51
+ # Add custom filtering logic here
52
+ end
53
+ event
54
+ end
55
+ end
56
+ RUBY
57
+
58
+ File.write(sentry_path, content)
59
+ "Created config/initializers/sentry.rb"
60
+ end
61
+
62
+ # Create Lograge configuration
63
+ def create_lograge_config
64
+ init_dir = File.join(@base_path, "config", "initializers")
65
+ FileUtils.mkdir_p(init_dir)
66
+
67
+ lograge_path = File.join(init_dir, "lograge.rb")
68
+ return "Skipping Lograge initializer (already exists)" if File.exist?(lograge_path)
69
+
70
+ content = <<~RUBY
71
+ # Lograge configuration for structured logging
72
+ # Add to Gemfile: gem 'lograge'
73
+ Rails.application.configure do
74
+ config.lograge.enabled = true
75
+ config.lograge.base_controller_class = 'ActionController::API'
76
+ config.lograge.custom_options = lambda do |event|
77
+ {
78
+ # Add custom data to logs
79
+ host: Socket.gethostname,
80
+ pid: Process.pid,
81
+ # Add timing data
82
+ request_id: event.payload[:request_id],
83
+ user_id: event.payload[:user_id]
84
+ }
85
+ end
86
+ config.lograge.formatter = Lograge::Formatters::Json.new
87
+ end
88
+ RUBY
89
+
90
+ File.write(lograge_path, content)
91
+ "Created config/initializers/lograge.rb"
92
+ end
93
+
94
+ # Create log formatter
95
+ def create_log_formatter
96
+ config_path = File.join(@base_path, "config")
97
+
98
+ # Check if application.rb exists
99
+ app_rb = File.join(config_path, "application.rb")
100
+ return "Not a Rails app" unless File.exist?(app_rb)
101
+
102
+ content = File.read(app_rb)
103
+
104
+ unless content.include?("config.log_formatter")
105
+ # Add log formatter to application.rb
106
+ File.write(app_rb, content.gsub(/class Application < Rails::Application/,
107
+ "class Application < Rails::Application\n config.log_formatter = proc { |severity, datetime, progname, msg| \"#{datetime.utc_iso8601} #{severity}: #{msg}\\n\" }"))
108
+ end
109
+
110
+ "Updated application.rb with custom log formatter"
111
+ end
112
+
113
+ # Create environment-specific configs
114
+ def create_environments_config
115
+ env_dir = File.join(@base_path, "config", "environments")
116
+ return "Not a Rails app" unless Dir.exist?(env_dir)
117
+
118
+ results = []
119
+
120
+ # Production config
121
+ prod_path = File.join(env_dir, "production.rb")
122
+ if File.exist?(prod_path)
123
+ content = File.read(prod_path)
124
+ unless content.include?("Lograge") || content.include?("Sentry")
125
+ File.write(prod_path, content + "\n\n# Monitoring\nconfig.lograge.enabled = true\n")
126
+ results << "Updated production.rb"
127
+ end
128
+ end
129
+
130
+ results.any? ? results.join("\n") : "No environment updates needed"
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,122 @@
1
+ # Service generator for RailsForge
2
+ # Generates service objects
3
+
4
+ require_relative 'base_generator'
5
+
6
+ module RailsForge
7
+ module Generators
8
+ # ServiceGenerator creates service objects
9
+ class ServiceGenerator < BaseGenerator
10
+ # Error class for invalid service names
11
+ class InvalidServiceNameError < StandardError; end
12
+
13
+ # Validates the service name
14
+ # @param name [String] Service name to validate
15
+ def validate_name!(name)
16
+ super(name, /\A[A-Z][a-zA-Z0-9]*\z/, InvalidServiceNameError)
17
+ end
18
+
19
+ # Generate service file and optionally spec
20
+ # @return [String] Success message
21
+ def generate
22
+ validate_name!(@name)
23
+
24
+ unless @base_path
25
+ raise "Not in a Rails application directory"
26
+ end
27
+
28
+ service_file_path = generate_service_file
29
+
30
+ if @options[:with_spec] != false
31
+ generate_spec_file
32
+ end
33
+
34
+ "Service '#{@name}' generated successfully!"
35
+ end
36
+
37
+ # Generate the service file
38
+ # @return [String] Path to generated file
39
+ def generate_service_file
40
+ service_dir = File.join(@base_path, "app", "services")
41
+ FileUtils.mkdir_p(service_dir)
42
+
43
+ file_name = "#{underscore}.rb"
44
+ file_path = File.join(service_dir, file_name)
45
+
46
+ if File.exist?(file_path)
47
+ puts " Skipping service (already exists)"
48
+ return file_path
49
+ end
50
+
51
+ File.write(file_path, service_template)
52
+ puts " Created app/services/#{file_name}"
53
+ file_path
54
+ end
55
+
56
+ # Generate the spec file
57
+ # @return [String] Path to generated spec
58
+ def generate_spec_file
59
+ spec_dir = File.join(@base_path, "spec", "services")
60
+ FileUtils.mkdir_p(spec_dir)
61
+
62
+ file_name = "#{underscore}_spec.rb"
63
+ file_path = File.join(spec_dir, file_name)
64
+
65
+ if File.exist?(file_path)
66
+ puts " Skipping spec (already exists)"
67
+ return file_path
68
+ end
69
+
70
+ File.write(file_path, spec_template)
71
+ puts " Created spec/services/#{file_name}"
72
+ file_path
73
+ end
74
+
75
+ # Service template
76
+ # @return [String] Template content
77
+ def service_template
78
+ <<~RUBY
79
+ # Service class for #{underscore}
80
+ # Encapsulates business logic related to #{underscore}
81
+ class #{@name}
82
+ def initialize(**args)
83
+ @args = args
84
+ end
85
+
86
+ def call
87
+ # TODO: Implement service logic here
88
+ end
89
+
90
+ def self.call(**args)
91
+ new(**args).call
92
+ end
93
+
94
+ private
95
+ end
96
+ RUBY
97
+ end
98
+
99
+ # Spec template
100
+ # @return [String] Spec template content
101
+ def spec_template
102
+ <<~RUBY
103
+ # frozen_string_literal: true
104
+
105
+ RSpec.describe #{@name} do
106
+ describe '#call' do
107
+ it 'returns expected result' do
108
+ result = described_class.call
109
+ expect(result).to be_nil
110
+ end
111
+ end
112
+ end
113
+ RUBY
114
+ end
115
+
116
+ # Class method for CLI
117
+ def self.generate(service_name, with_spec: true, template_version: "v1")
118
+ new(service_name, with_spec: with_spec, template_version: template_version).generate
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,129 @@
1
+ # Stimulus controller generator for RailsForge
2
+ # Generates Stimulus JavaScript controllers
3
+
4
+ require 'fileutils'
5
+
6
+ module RailsForge
7
+ module Generators
8
+ # Stimulus controller generator
9
+ class StimulusControllerGenerator < BaseGenerator
10
+ # Template version
11
+ TEMPLATE_VERSION = "v1"
12
+
13
+ # Initialize the generator
14
+ # @param name [String] Controller name
15
+ # @param options [Hash] Generator options
16
+ def initialize(name, options = {})
17
+ super(name, options)
18
+ validate_name!
19
+ end
20
+
21
+ # Generate Stimulus controller files
22
+ # @return [String] Success message
23
+ def generate
24
+ return "Not in a Rails application directory" unless @base_path
25
+
26
+ results = []
27
+ results << generate_controller
28
+ results << generate_spec if with_spec?
29
+
30
+ results.join("\n")
31
+ end
32
+
33
+ private
34
+
35
+ # Validate controller name
36
+ def validate_name!
37
+ validate_name!(@name, /\A[A-Z][a-zA-Z0-9]*\z/, InvalidNameError)
38
+ end
39
+
40
+ # Check if should generate spec
41
+ def with_spec?
42
+ @options[:spec] != false
43
+ end
44
+
45
+ # Generate the JavaScript controller file
46
+ def generate_controller
47
+ controller_dir = File.join(@base_path, "app", "javascript", "controllers")
48
+ FileUtils.mkdir_p(controller_dir)
49
+
50
+ file_name = "#{underscore}_controller.js"
51
+ file_path = File.join(controller_dir, file_name)
52
+
53
+ return "Skipping controller (already exists)" if File.exist?(file_path)
54
+
55
+ # Try to load template
56
+ template_content = load_template
57
+ content = apply_template(template_content)
58
+
59
+ File.write(file_path, content)
60
+ "Created app/javascript/controllers/#{file_name}"
61
+ end
62
+
63
+ # Generate the controller spec
64
+ def generate_spec
65
+ spec_dir = File.join(@base_path, "spec", "javascripts", "controllers")
66
+ FileUtils.mkdir_p(spec_dir)
67
+
68
+ file_name = "#{underscore}_controller_spec.js"
69
+ file_path = File.join(spec_dir, file_name)
70
+
71
+ return "Skipping spec (already exists)" if File.exist?(file_path)
72
+
73
+ content = <<~JS
74
+ import { Application } from "@hotwired/stimulus"
75
+ import #{camelize}Controller from "../../../app/javascript/controllers/#{underscore}_controller"
76
+
77
+ describe("#{camelize}Controller", () => {
78
+ beforeEach(() => {
79
+ this.application = new Application()
80
+ this.application.register("#{underscore}", #{camelize}Controller)
81
+ })
82
+ })
83
+ JS
84
+
85
+ File.write(file_path, content)
86
+ "Created spec/javascripts/controllers/#{file_name}"
87
+ end
88
+
89
+ # Load template content
90
+ def load_template
91
+ template_path = File.join(
92
+ File.dirname(__FILE__),
93
+ "..",
94
+ "templates",
95
+ template_version,
96
+ "stimulus_controller",
97
+ "template.rb"
98
+ )
99
+
100
+ if File.exist?(template_path)
101
+ File.read(template_path)
102
+ else
103
+ default_template
104
+ end
105
+ end
106
+
107
+ # Default template content
108
+ def default_template
109
+ <<~JS
110
+ // Stimulus controller: <%= class_name %>
111
+ import { Controller } from "@hotwired/stimulus"
112
+
113
+ export default class <%= class_name %> extends Controller {
114
+ connect() {
115
+ console.log("<%= class_name %> connected")
116
+ }
117
+ }
118
+ JS
119
+ end
120
+
121
+ # Apply template variables
122
+ def apply_template(content)
123
+ content
124
+ .gsub("<%= class_name %>", camelize)
125
+ .gsub("<%= underscore_name %>", underscore)
126
+ end
127
+ end
128
+ end
129
+ end