rails-frontend-cli 1.0.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 +7 -0
- data/CHANGELOG.md +132 -0
- data/LICENSE +9 -0
- data/README.md +122 -0
- data/USER_MANUAL.md +502 -0
- data/exe/rails-frontend +7 -0
- data/lib/rails_frontend_cli/version.rb +5 -0
- data/lib/rails_frontend_cli.rb +1357 -0
- metadata +68 -0
|
@@ -0,0 +1,1357 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'optparse'
|
|
6
|
+
|
|
7
|
+
class RailsFrontendCLI
|
|
8
|
+
VERSION = "1.0.4"
|
|
9
|
+
AUTHOR = "Levent Özbilgiç"
|
|
10
|
+
LINKEDIN = "https://www.linkedin.com/in/leventozbilgic/"
|
|
11
|
+
GITHUB = "https://github.com/ozbilgic"
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@project_name = nil
|
|
15
|
+
@page_name = nil
|
|
16
|
+
@command = nil
|
|
17
|
+
@clean_mode = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run(args)
|
|
21
|
+
if args.empty?
|
|
22
|
+
show_help
|
|
23
|
+
exit 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@command = args[0]
|
|
27
|
+
|
|
28
|
+
case @command
|
|
29
|
+
when 'new', 'n'
|
|
30
|
+
@project_name = args[1]
|
|
31
|
+
if @project_name.nil? || @project_name.empty?
|
|
32
|
+
error_message("Project name not specified. Usage: rails-frontend new PROJECT_NAME [--clean]")
|
|
33
|
+
end
|
|
34
|
+
# Check for --clean parameter
|
|
35
|
+
@clean_mode = args.include?('--clean')
|
|
36
|
+
create_new_project
|
|
37
|
+
when 'add-page', 'ap'
|
|
38
|
+
@page_name = args[1]
|
|
39
|
+
if @page_name.nil? || @page_name.empty?
|
|
40
|
+
error_message("Page name not specified. Usage: rails-frontend add-page PAGE_NAME")
|
|
41
|
+
end
|
|
42
|
+
add_page
|
|
43
|
+
when 'remove-page', 'rp'
|
|
44
|
+
@page_name = args[1]
|
|
45
|
+
if @page_name.nil? || @page_name.empty?
|
|
46
|
+
error_message("Page name not specified. Usage: rails-frontend remove-page PAGE_NAME")
|
|
47
|
+
end
|
|
48
|
+
remove_page
|
|
49
|
+
when 'add-stimulus', 'as'
|
|
50
|
+
@controller_name = args[1]
|
|
51
|
+
if @controller_name.nil? || @controller_name.empty?
|
|
52
|
+
error_message("Controller name not specified. Usage: rails-frontend add-stimulus CONTROLLER_NAME")
|
|
53
|
+
end
|
|
54
|
+
add_stimulus
|
|
55
|
+
when 'remove-stimulus', 'rs'
|
|
56
|
+
@controller_name = args[1]
|
|
57
|
+
if @controller_name.nil? || @controller_name.empty?
|
|
58
|
+
error_message("Controller name not specified. Usage: rails-frontend remove-stimulus CONTROLLER_NAME")
|
|
59
|
+
end
|
|
60
|
+
remove_stimulus
|
|
61
|
+
when 'add-layout', 'al'
|
|
62
|
+
@layout_name = args[1]
|
|
63
|
+
if @layout_name.nil? || @layout_name.empty?
|
|
64
|
+
error_message("Layout name not specified. Usage: rails-frontend add-layout LAYOUT_NAME")
|
|
65
|
+
end
|
|
66
|
+
add_layout
|
|
67
|
+
when 'remove-layout', 'rl'
|
|
68
|
+
@layout_name = args[1]
|
|
69
|
+
if @layout_name.nil? || @layout_name.empty?
|
|
70
|
+
error_message("Layout name not specified. Usage: rails-frontend remove-layout LAYOUT_NAME")
|
|
71
|
+
end
|
|
72
|
+
remove_layout
|
|
73
|
+
when 'add-pin', 'pin'
|
|
74
|
+
@pin_name = args[1]
|
|
75
|
+
if @pin_name.nil? || @pin_name.empty?
|
|
76
|
+
error_message("Pin name not specified. Usage: rails-frontend add-pin PACKAGE_NAME")
|
|
77
|
+
end
|
|
78
|
+
add_pin
|
|
79
|
+
when 'remove-pin', 'unpin'
|
|
80
|
+
@pin_name = args[1]
|
|
81
|
+
if @pin_name.nil? || @pin_name.empty?
|
|
82
|
+
error_message("Pin name not specified. Usage: rails-frontend remove-pin PACKAGE_NAME")
|
|
83
|
+
end
|
|
84
|
+
remove_pin
|
|
85
|
+
when 'run', 'r'
|
|
86
|
+
run_server
|
|
87
|
+
when 'build', 'b'
|
|
88
|
+
build_static_site
|
|
89
|
+
when 'version', '-v', '--version'
|
|
90
|
+
puts "Rails Frontend CLI v#{VERSION}"
|
|
91
|
+
exit 0
|
|
92
|
+
when 'help', '-h', '--help'
|
|
93
|
+
show_help
|
|
94
|
+
exit 0
|
|
95
|
+
else
|
|
96
|
+
error_message("Unknown command: #{@command}")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def create_new_project
|
|
103
|
+
show_title("Creating New Rails Frontend Project: #{@project_name}")
|
|
104
|
+
|
|
105
|
+
# Check if Rails project already exists
|
|
106
|
+
if Dir.exist?(@project_name)
|
|
107
|
+
error_message("'#{@project_name}' directory already exists!")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Create Rails project
|
|
111
|
+
show_message("Creating Rails project...")
|
|
112
|
+
|
|
113
|
+
# Build command based on clean mode
|
|
114
|
+
if @clean_mode
|
|
115
|
+
rails_command = "rails new #{@project_name} --css=tailwind --javascript=importmap " \
|
|
116
|
+
"--skip-test --skip-system-test --skip-action-mailer " \
|
|
117
|
+
"--skip-action-mailbox --skip-action-text --skip-active-job " \
|
|
118
|
+
"--skip-action-cable --skip-active-storage --skip-active-record "\
|
|
119
|
+
"--skip-solid --skip-kamal --skip-docker"
|
|
120
|
+
else
|
|
121
|
+
rails_command = "rails new #{@project_name} --css=tailwind --javascript=importmap"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
unless system(rails_command)
|
|
125
|
+
error_message("Failed to create Rails project!")
|
|
126
|
+
end
|
|
127
|
+
success_message("Rails project created")
|
|
128
|
+
|
|
129
|
+
# Change to project directory
|
|
130
|
+
project_directory = File.expand_path(@project_name)
|
|
131
|
+
Dir.chdir(project_directory) do
|
|
132
|
+
# Clean unnecessary files (if --clean parameter exists)
|
|
133
|
+
if @clean_mode
|
|
134
|
+
show_message("Cleaning unnecessary files...")
|
|
135
|
+
clean_unnecessary_files
|
|
136
|
+
success_message("Unnecessary files cleaned")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Create Home controller and view
|
|
140
|
+
show_message("Creating Home controller and view...")
|
|
141
|
+
create_home_controller
|
|
142
|
+
success_message("Home controller and view created")
|
|
143
|
+
|
|
144
|
+
# Create shared components
|
|
145
|
+
show_message("Creating shared components...")
|
|
146
|
+
create_shared_components
|
|
147
|
+
success_message("Shared components created")
|
|
148
|
+
|
|
149
|
+
# Create CSS files
|
|
150
|
+
show_message("Creating CSS files...")
|
|
151
|
+
create_css_files
|
|
152
|
+
success_message("CSS files created")
|
|
153
|
+
|
|
154
|
+
# Create asset folders
|
|
155
|
+
show_message("Creating asset folders...")
|
|
156
|
+
create_asset_folders
|
|
157
|
+
success_message("Asset folders created")
|
|
158
|
+
|
|
159
|
+
# Update layout file
|
|
160
|
+
show_message("Updating layout file...")
|
|
161
|
+
update_layout
|
|
162
|
+
success_message("Layout file updated")
|
|
163
|
+
|
|
164
|
+
# Configure routes
|
|
165
|
+
show_message("Configuring routes...")
|
|
166
|
+
update_routes('home', 'index', root: true)
|
|
167
|
+
success_message("Routes configured")
|
|
168
|
+
|
|
169
|
+
# Configure Procfile.dev
|
|
170
|
+
show_message("Configuring Procfile.dev...")
|
|
171
|
+
update_procfile
|
|
172
|
+
success_message("Procfile.dev configured")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
completion_message
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def add_page
|
|
179
|
+
# Check if current directory is a Rails project
|
|
180
|
+
is_rails_project?
|
|
181
|
+
|
|
182
|
+
show_title("Adding New Page: #{@page_name}")
|
|
183
|
+
|
|
184
|
+
# Normalize page name (convert Turkish characters)
|
|
185
|
+
page_name_normalized = normalize_name(@page_name)
|
|
186
|
+
|
|
187
|
+
# Create view (in home folder)
|
|
188
|
+
show_message("Creating view file...")
|
|
189
|
+
create_view(page_name_normalized)
|
|
190
|
+
success_message("View file created")
|
|
191
|
+
|
|
192
|
+
# Create CSS file
|
|
193
|
+
show_message("Creating CSS file...")
|
|
194
|
+
create_css(page_name_normalized)
|
|
195
|
+
success_message("CSS file created")
|
|
196
|
+
|
|
197
|
+
# Add action to Home controller
|
|
198
|
+
show_message("Updating Home controller...")
|
|
199
|
+
add_home_controller_action(page_name_normalized)
|
|
200
|
+
success_message("Home controller updated")
|
|
201
|
+
|
|
202
|
+
# Add route
|
|
203
|
+
show_message("Adding route...")
|
|
204
|
+
update_routes(page_name_normalized, page_name_normalized)
|
|
205
|
+
success_message("Route added")
|
|
206
|
+
|
|
207
|
+
puts "\n #{colorize('Page successfully added!', :green)}"
|
|
208
|
+
puts "Page URL: #{colorize("/#{page_name_normalized}", :blue)}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def remove_page
|
|
212
|
+
is_rails_project?
|
|
213
|
+
|
|
214
|
+
show_title("Removing Page: #{@page_name}")
|
|
215
|
+
|
|
216
|
+
page_name_normalized = normalize_name(@page_name)
|
|
217
|
+
|
|
218
|
+
# Prevent deletion of home/index page
|
|
219
|
+
if page_name_normalized == 'home' || page_name_normalized == 'index'
|
|
220
|
+
error_message("Home page (home/index) cannot be deleted!")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Check file existence
|
|
224
|
+
view_path = "app/views/home/#{page_name_normalized}.html.erb"
|
|
225
|
+
unless File.exist?(view_path)
|
|
226
|
+
error_message("'#{page_name_normalized}' page not found!")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Get confirmation
|
|
230
|
+
print "#{colorize('Are you sure?', :yellow)} '#{page_name_normalized}' page will be deleted (y/n): "
|
|
231
|
+
confirmation = STDIN.gets.chomp.downcase
|
|
232
|
+
unless confirmation == 'y' || confirmation == 'yes'
|
|
233
|
+
puts "Operation cancelled."
|
|
234
|
+
exit 0
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Delete view file
|
|
238
|
+
show_message("Deleting view file...")
|
|
239
|
+
FileUtils.rm_f(view_path)
|
|
240
|
+
success_message("View file deleted")
|
|
241
|
+
|
|
242
|
+
# Delete CSS file
|
|
243
|
+
show_message("Deleting CSS file...")
|
|
244
|
+
FileUtils.rm_f("app/assets/stylesheets/#{page_name_normalized}.css")
|
|
245
|
+
success_message("CSS file deleted")
|
|
246
|
+
|
|
247
|
+
# Remove action from Home controller
|
|
248
|
+
show_message("Updating Home controller...")
|
|
249
|
+
remove_home_controller_action(page_name_normalized)
|
|
250
|
+
success_message("Home controller updated")
|
|
251
|
+
|
|
252
|
+
# Remove route
|
|
253
|
+
show_message("Removing route...")
|
|
254
|
+
remove_route(page_name_normalized)
|
|
255
|
+
success_message("Route removed")
|
|
256
|
+
|
|
257
|
+
puts "\n #{colorize('Page successfully deleted!', :green)}"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def add_stimulus
|
|
261
|
+
# Check if current directory is a Rails project
|
|
262
|
+
is_rails_project?
|
|
263
|
+
|
|
264
|
+
show_title("Creating Stimulus Controller: #{@controller_name}")
|
|
265
|
+
|
|
266
|
+
# Normalize controller name
|
|
267
|
+
controller_name_normalized = normalize_name(@controller_name)
|
|
268
|
+
controller_file = "app/javascript/controllers/#{controller_name_normalized}_controller.js"
|
|
269
|
+
|
|
270
|
+
# Check if controller file already exists
|
|
271
|
+
if File.exist?(controller_file)
|
|
272
|
+
error_message("Stimulus controller already exists: #{controller_file}")
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Create Stimulus controller
|
|
276
|
+
show_message("Creating Stimulus controller...")
|
|
277
|
+
create_stimulus_controller(controller_name_normalized)
|
|
278
|
+
success_message("Stimulus controller created")
|
|
279
|
+
|
|
280
|
+
puts "\n #{colorize('Stimulus controller successfully created!', :green)}"
|
|
281
|
+
puts "File: #{colorize("app/javascript/controllers/#{controller_name_normalized}_controller.js", :blue)}"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def remove_stimulus
|
|
285
|
+
# Check if current directory is a Rails project
|
|
286
|
+
is_rails_project?
|
|
287
|
+
|
|
288
|
+
show_title("Removing Stimulus Controller: #{@controller_name}")
|
|
289
|
+
|
|
290
|
+
# Normalize controller name
|
|
291
|
+
controller_name_normalized = normalize_name(@controller_name)
|
|
292
|
+
controller_file = "app/javascript/controllers/#{controller_name_normalized}_controller.js"
|
|
293
|
+
|
|
294
|
+
# Check controller file existence
|
|
295
|
+
unless File.exist?(controller_file)
|
|
296
|
+
error_message("Stimulus controller not found: #{controller_file}")
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Check usage in view files
|
|
300
|
+
show_message("Checking usage in view files...")
|
|
301
|
+
used_files = []
|
|
302
|
+
|
|
303
|
+
if Dir.exist?('app/views')
|
|
304
|
+
Dir.glob('app/views/**/*.html.erb').each do |view_file|
|
|
305
|
+
content = File.read(view_file)
|
|
306
|
+
# Check for data-controller="controller_name" or data-controller='controller_name'
|
|
307
|
+
# Also check for data: { controller: "controller_name" } pattern
|
|
308
|
+
if content.match?(/data-controller=["'].*#{controller_name_normalized}.*["']/) ||
|
|
309
|
+
content.match?(/data:\s*\{[^}]*controller:\s*["']#{controller_name_normalized}["'][^}]*\}/)
|
|
310
|
+
used_files << view_file
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
if used_files.any?
|
|
316
|
+
puts "\n"
|
|
317
|
+
puts colorize("WARNING: This controller is being used in the following files:", :yellow, bold: true)
|
|
318
|
+
used_files.each do |file|
|
|
319
|
+
puts " - #{file}"
|
|
320
|
+
end
|
|
321
|
+
puts "\n"
|
|
322
|
+
print colorize("Do you still want to delete it? (y/n): ", :yellow)
|
|
323
|
+
answer = STDIN.gets.chomp.downcase
|
|
324
|
+
unless answer == 'y' || answer == 'yes'
|
|
325
|
+
puts "\nOperation cancelled."
|
|
326
|
+
exit 0
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
success_message("Check completed")
|
|
330
|
+
|
|
331
|
+
# Delete controller
|
|
332
|
+
show_message("Deleting Stimulus controller...")
|
|
333
|
+
FileUtils.rm_f(controller_file)
|
|
334
|
+
success_message("Stimulus controller deleted")
|
|
335
|
+
|
|
336
|
+
puts "\n #{colorize('Stimulus controller successfully deleted!', :green)}"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def add_layout
|
|
340
|
+
# Check if current directory is a Rails project
|
|
341
|
+
is_rails_project?
|
|
342
|
+
|
|
343
|
+
show_title("Creating Layout: #{@layout_name}")
|
|
344
|
+
|
|
345
|
+
# Normalize layout name
|
|
346
|
+
layout_name_normalized = normalize_name(@layout_name)
|
|
347
|
+
|
|
348
|
+
# Check if layout file already exists
|
|
349
|
+
layout_file = "app/views/layouts/#{layout_name_normalized}.html.erb"
|
|
350
|
+
if File.exist?(layout_file)
|
|
351
|
+
error_message("Layout already exists: #{layout_file}")
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Scan files in app/views/home folder
|
|
355
|
+
show_message("Scanning view files...")
|
|
356
|
+
home_views = list_home_views
|
|
357
|
+
success_message("View files scanned")
|
|
358
|
+
|
|
359
|
+
# Check for matching view file
|
|
360
|
+
view_name = nil
|
|
361
|
+
if home_views.include?(layout_name_normalized)
|
|
362
|
+
# Matching view exists
|
|
363
|
+
view_name = layout_name_normalized
|
|
364
|
+
puts "\n#{colorize("Matching view file found: #{view_name}.html.erb", :green)}"
|
|
365
|
+
else
|
|
366
|
+
# No matching view, ask user
|
|
367
|
+
if home_views.empty?
|
|
368
|
+
error_message("No view files found in app/views/home folder!")
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
puts "\n#{colorize("Which view will this layout be used with?", :yellow, bold: true)}"
|
|
372
|
+
home_views.each_with_index do |view, index|
|
|
373
|
+
puts " #{index + 1}. #{view}"
|
|
374
|
+
end
|
|
375
|
+
print "\nChoice (1-#{home_views.length}): "
|
|
376
|
+
choice = STDIN.gets.chomp.to_i
|
|
377
|
+
|
|
378
|
+
if choice < 1 || choice > home_views.length
|
|
379
|
+
error_message("Invalid choice!")
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
view_name = home_views[choice - 1]
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Check for existing layout for the same view
|
|
386
|
+
show_message("Checking for existing layout...")
|
|
387
|
+
existing_layout = find_existing_layout_for_view(view_name)
|
|
388
|
+
if existing_layout
|
|
389
|
+
success_message("Check completed")
|
|
390
|
+
error_message("A layout is already defined for '#{view_name}' view: '#{existing_layout}'\nRemove the existing layout first: rails-frontend remove-layout #{existing_layout}")
|
|
391
|
+
end
|
|
392
|
+
success_message("Check completed")
|
|
393
|
+
|
|
394
|
+
# Create layout file
|
|
395
|
+
show_message("Creating layout file...")
|
|
396
|
+
create_layout_file(layout_name_normalized)
|
|
397
|
+
success_message("Layout file created")
|
|
398
|
+
|
|
399
|
+
# Add layout directive to controller
|
|
400
|
+
show_message("Updating Home controller...")
|
|
401
|
+
add_layout_directive(layout_name_normalized, view_name)
|
|
402
|
+
success_message("Home controller updated")
|
|
403
|
+
|
|
404
|
+
puts "\n #{colorize('Layout successfully created!', :green)}"
|
|
405
|
+
puts "Layout file: #{colorize(layout_file, :blue)}"
|
|
406
|
+
puts "Will be used for view: #{colorize("#{view_name}.html.erb", :blue)}"
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def remove_layout
|
|
410
|
+
# Check if current directory is a Rails project
|
|
411
|
+
is_rails_project?
|
|
412
|
+
|
|
413
|
+
show_title("Removing Layout: #{@layout_name}")
|
|
414
|
+
|
|
415
|
+
# Normalize layout name
|
|
416
|
+
layout_name_normalized = normalize_name(@layout_name)
|
|
417
|
+
layout_file = "app/views/layouts/#{layout_name_normalized}.html.erb"
|
|
418
|
+
|
|
419
|
+
# Check layout file existence
|
|
420
|
+
unless File.exist?(layout_file)
|
|
421
|
+
error_message("Layout not found: #{layout_file}")
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Request confirmation
|
|
425
|
+
print colorize("Are you sure you want to delete '#{layout_name_normalized}' layout? (y/n): ", :yellow)
|
|
426
|
+
answer = STDIN.gets.chomp.downcase
|
|
427
|
+
unless answer == 'y' || answer == 'yes'
|
|
428
|
+
puts "\nOperation cancelled."
|
|
429
|
+
exit 0
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Remove layout directive from controller
|
|
433
|
+
show_message("Updating Home controller...")
|
|
434
|
+
remove_layout_directive(layout_name_normalized)
|
|
435
|
+
success_message("Home controller updated")
|
|
436
|
+
|
|
437
|
+
# Delete layout file
|
|
438
|
+
show_message("Deleting layout file...")
|
|
439
|
+
FileUtils.rm_f(layout_file)
|
|
440
|
+
success_message("Layout file deleted")
|
|
441
|
+
|
|
442
|
+
puts "\n #{colorize('Layout successfully deleted!', :green)}"
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def add_pin
|
|
446
|
+
# Check if current directory is a Rails project
|
|
447
|
+
is_rails_project?
|
|
448
|
+
|
|
449
|
+
show_title("Adding Importmap Pin: #{@pin_name}")
|
|
450
|
+
|
|
451
|
+
# Check bin/importmap file existence
|
|
452
|
+
unless File.exist?('bin/importmap')
|
|
453
|
+
error_message("bin/importmap not found! This project may not be using importmap.")
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Run bin/importmap pin command
|
|
457
|
+
show_message("Adding pin...")
|
|
458
|
+
output = `bin/importmap pin #{@pin_name} 2>&1`
|
|
459
|
+
|
|
460
|
+
# Check for errors in output
|
|
461
|
+
if output.include?("Couldn't find") || output.include?("error") || output.include?("Error")
|
|
462
|
+
puts "" # New line
|
|
463
|
+
error_message("Failed to add pin! Package not found: #{@pin_name}")
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
success_message("Pin added")
|
|
467
|
+
|
|
468
|
+
puts "\n #{colorize('Pin successfully added!', :green)}"
|
|
469
|
+
puts " #{colorize('Don\'t forget to import it in your project!', :green)}"
|
|
470
|
+
puts "Package: #{colorize(@pin_name, :blue)}"
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def remove_pin
|
|
474
|
+
# Check if current directory is a Rails project
|
|
475
|
+
is_rails_project?
|
|
476
|
+
|
|
477
|
+
show_title("Removing Importmap Pin: #{@pin_name}")
|
|
478
|
+
|
|
479
|
+
# Check bin/importmap file existence
|
|
480
|
+
unless File.exist?('bin/importmap')
|
|
481
|
+
error_message("bin/importmap not found! This project may not be using importmap.")
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Check usage in JavaScript and HTML files
|
|
485
|
+
show_message("Checking usage...")
|
|
486
|
+
used_files = check_pin_usage(@pin_name)
|
|
487
|
+
success_message("Check completed")
|
|
488
|
+
|
|
489
|
+
if used_files.any?
|
|
490
|
+
puts "\n"
|
|
491
|
+
puts colorize("WARNING: This package is being used in the following files:", :yellow, bold: true)
|
|
492
|
+
used_files.each do |file|
|
|
493
|
+
puts " - #{file}"
|
|
494
|
+
end
|
|
495
|
+
puts "\n"
|
|
496
|
+
print colorize("Do you still want to delete it? (y/n): ", :yellow)
|
|
497
|
+
answer = STDIN.gets.chomp.downcase
|
|
498
|
+
unless answer == 'y' || answer == 'yes'
|
|
499
|
+
puts "\nOperation cancelled."
|
|
500
|
+
exit 0
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Check pin existence
|
|
505
|
+
show_message("Checking pin...")
|
|
506
|
+
importmap_file = 'config/importmap.rb'
|
|
507
|
+
unless File.exist?(importmap_file)
|
|
508
|
+
error_message("config/importmap.rb not found!")
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
importmap_content = File.read(importmap_file)
|
|
512
|
+
unless importmap_content.match?(/pin\s+["']#{Regexp.escape(@pin_name)}["']/)
|
|
513
|
+
puts "" # New line
|
|
514
|
+
error_message("Pin not found! '#{@pin_name}' is not defined in importmap.")
|
|
515
|
+
end
|
|
516
|
+
success_message("Pin found")
|
|
517
|
+
|
|
518
|
+
# Run bin/importmap unpin command
|
|
519
|
+
show_message("Removing pin...")
|
|
520
|
+
output = `bin/importmap unpin #{@pin_name} 2>&1`
|
|
521
|
+
success_message("Pin removed")
|
|
522
|
+
|
|
523
|
+
puts "\n #{colorize('Pin successfully removed!', :green)}"
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Helper methods
|
|
527
|
+
def is_rails_project?
|
|
528
|
+
unless File.exist?('config/routes.rb') && File.exist?('Gemfile')
|
|
529
|
+
error_message("This directory is not a Rails project! Please run inside a Rails project.")
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def run_server
|
|
534
|
+
is_rails_project?
|
|
535
|
+
|
|
536
|
+
unless File.exist?('bin/dev')
|
|
537
|
+
error_message("bin/dev file not found! This project may not have been created with Rails 7+.")
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
puts "\n#{colorize('Starting Rails server...', :green, bold: true)}"
|
|
541
|
+
puts "#{colorize('Use Ctrl+C to stop', :yellow)}\n\n"
|
|
542
|
+
|
|
543
|
+
exec('bin/dev')
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def build_static_site
|
|
547
|
+
is_rails_project?
|
|
548
|
+
show_title("Building Static Files")
|
|
549
|
+
|
|
550
|
+
# Server check
|
|
551
|
+
show_message("Checking server...")
|
|
552
|
+
server = check_server
|
|
553
|
+
unless server[:running]
|
|
554
|
+
error_message("Rails server is not running! Start it first with 'rails-frontend run'.")
|
|
555
|
+
end
|
|
556
|
+
success_message("Server is running")
|
|
557
|
+
|
|
558
|
+
# Mirror with wget
|
|
559
|
+
wget_mirror(server[:port])
|
|
560
|
+
|
|
561
|
+
# Prepare build folder
|
|
562
|
+
show_message("Preparing build folder...")
|
|
563
|
+
configure_build
|
|
564
|
+
success_message("Build folder prepared")
|
|
565
|
+
|
|
566
|
+
# Move files
|
|
567
|
+
show_message("Organizing files...")
|
|
568
|
+
move_asset_files
|
|
569
|
+
success_message("Files organized")
|
|
570
|
+
|
|
571
|
+
# Path corrections
|
|
572
|
+
show_message("Fixing paths...")
|
|
573
|
+
fix_html_paths(server[:port])
|
|
574
|
+
fix_css_paths
|
|
575
|
+
success_message("Path fixes completed")
|
|
576
|
+
|
|
577
|
+
# Cleanup
|
|
578
|
+
show_message("Removing unnecessary components...")
|
|
579
|
+
clean_html_files
|
|
580
|
+
success_message("Unnecessary components removed")
|
|
581
|
+
|
|
582
|
+
puts "\n#{colorize('✓ Static site successfully built!', :green)}"
|
|
583
|
+
puts "Folder: #{colorize('build/', :blue)}"
|
|
584
|
+
puts "\nTo test:"
|
|
585
|
+
puts " cd build && python3 -m http.server or npx http-server"
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Build helper methods
|
|
589
|
+
def check_server
|
|
590
|
+
pid_file = 'tmp/pids/server.pid'
|
|
591
|
+
return { running: false, port: nil } unless File.exist?(pid_file)
|
|
592
|
+
|
|
593
|
+
pid = File.read(pid_file).strip.to_i
|
|
594
|
+
|
|
595
|
+
begin
|
|
596
|
+
# Find port number
|
|
597
|
+
cmd = `ps -p #{pid} -o args=`.strip
|
|
598
|
+
port = cmd[/tcp:\/\/[^:]+:(\d+)/, 1]
|
|
599
|
+
|
|
600
|
+
{ running: true, port: port.to_i }
|
|
601
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
602
|
+
# Process not found or no access
|
|
603
|
+
{ running: false, port: nil }
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def wget_mirror(port)
|
|
608
|
+
# Delete previous build folder
|
|
609
|
+
FileUtils.rm_rf('build')
|
|
610
|
+
|
|
611
|
+
# Run wget silently
|
|
612
|
+
system("wget --mirror --convert-links --adjust-extension --page-requisites --no-parent --directory-prefix=build http://localhost:#{port}/ > /dev/null 2>&1")
|
|
613
|
+
|
|
614
|
+
# Move localhost:3000 folder into build/
|
|
615
|
+
if Dir.exist?("build/localhost:#{port}")
|
|
616
|
+
Dir.glob("build/localhost:#{port}/*").each do |file|
|
|
617
|
+
FileUtils.mv(file, 'build/')
|
|
618
|
+
end
|
|
619
|
+
FileUtils.rm_rf("build/localhost:#{port}")
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def configure_build
|
|
624
|
+
['img', 'js', 'css', 'fonts'].each do |dir|
|
|
625
|
+
FileUtils.mkdir_p("build/assets/#{dir}")
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def move_asset_files
|
|
630
|
+
extensions = [ "{jpg,jpeg,png,gif,svg,webp,ico}", "js", "css", "{woff,woff2,ttf,eot,otf}" ]
|
|
631
|
+
folders = [ "img", "js", "css", "fonts" ]
|
|
632
|
+
|
|
633
|
+
extensions.zip(folders).each do |extension, folder|
|
|
634
|
+
Dir.glob("build/**/*.#{extension}").each do |file|
|
|
635
|
+
basename = File.basename(file)
|
|
636
|
+
dest = "build/assets/#{folder}/#{basename}"
|
|
637
|
+
|
|
638
|
+
# Move if file exists and destination doesn't
|
|
639
|
+
if File.exist?(file) && !File.exist?(dest)
|
|
640
|
+
FileUtils.mv(file, dest)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# If controllers folder exists, move contents and delete
|
|
646
|
+
if Dir.exist?("build/assets/controllers")
|
|
647
|
+
Dir.glob("build/assets/controllers/*").each do |file|
|
|
648
|
+
FileUtils.mv(file, 'build/assets/js/')
|
|
649
|
+
end
|
|
650
|
+
FileUtils.rm_rf("build/assets/controllers")
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# Delete turbo files (commented out for now as it may be needed for stimulus)
|
|
654
|
+
# Dir.glob('**/*turbo.min*').each do |file|
|
|
655
|
+
# FileUtils.rm_rf(file)
|
|
656
|
+
# end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def fix_html_paths(port)
|
|
660
|
+
Dir.glob('build/**/*.html').each do |file|
|
|
661
|
+
content = File.read(file)
|
|
662
|
+
|
|
663
|
+
# 1. Add "js/" after "assets/" for lines containing "assets/" and ending with .js
|
|
664
|
+
# Example: assets/application-bfcdf840.js -> assets/js/application-bfcdf840.js
|
|
665
|
+
content.gsub!(/assets\/([^\/\s"']+\.js)/, 'assets/js/\1')
|
|
666
|
+
|
|
667
|
+
# 2. Add "js/" after "assets/controllers/" for lines containing "assets/controllers/" and ending with .js
|
|
668
|
+
# Example: assets/controllers/index-ee64e1f1.js -> assets/js/index-ee64e1f1.js
|
|
669
|
+
content.gsub!(/assets\/controllers\/([^\/\s"']+\.js)/, 'assets/js/\1')
|
|
670
|
+
|
|
671
|
+
# 3. Add "css/" after "assets/" for lines containing "assets/" and ending with .css
|
|
672
|
+
# Example: assets/application-72587725.css -> assets/css/application-72587725.css
|
|
673
|
+
content.gsub!(/assets\/([^\/\s"']+\.css)/, 'assets/css/\1')
|
|
674
|
+
|
|
675
|
+
# 4. Add "img/" after "assets/" for lines containing "assets/" and ending with image extensions
|
|
676
|
+
# Example: assets/app-image-72587725.jpg -> assets/img/app-image-72587725.jpg
|
|
677
|
+
content.gsub!(/assets\/([^\/\s"']+\.(jpg|jpeg|png|gif|svg|webp))/, 'assets/img/\1')
|
|
678
|
+
content.gsub!(/http:\/\/localhost:#{port}\/([^\/\s"']+\.(jpg|jpeg|png|gif|svg|webp))/, 'assets/img/\1')
|
|
679
|
+
|
|
680
|
+
File.write(file, content)
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def fix_css_paths
|
|
685
|
+
Dir.glob('build/assets/css/**/*.css').each do |file|
|
|
686
|
+
content = File.read(file)
|
|
687
|
+
|
|
688
|
+
# Font paths - use absolute path
|
|
689
|
+
# Example: url("LavishlyYours-Regular-c6da7860.ttf") -> url("/assets/fonts/LavishlyYours-Regular-c6da7860.ttf")
|
|
690
|
+
content.gsub!(/url\(["']?([^\/]["'()]*\.(woff2?|ttf|eot|otf))["']?\)/, 'url("/assets/fonts/\1")')
|
|
691
|
+
|
|
692
|
+
# Image paths - use absolute path
|
|
693
|
+
# Example: url("A-13904566-1761601378-8017-2b819c09.jpg") -> url("/assets/img/A-13904566-1761601378-8017-2b819c09.jpg")
|
|
694
|
+
content.gsub!(/url\(["']?([^\/]["'()]*\.(jpg|jpeg|png|gif|svg|webp))["']?\)/, 'url("/assets/img/\1")')
|
|
695
|
+
|
|
696
|
+
File.write(file, content)
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def clean_html_files
|
|
701
|
+
Dir.glob('build/**/*.html').each do |file|
|
|
702
|
+
content = File.read(file)
|
|
703
|
+
|
|
704
|
+
# index.html links
|
|
705
|
+
content.gsub!(/href="[^"]*\/index\.html"/, 'href="/"')
|
|
706
|
+
content.gsub!(/href="index\.html"/, 'href="/"')
|
|
707
|
+
|
|
708
|
+
# Remove Turbo (commented out for now as it may be needed for stimulus)
|
|
709
|
+
# content.gsub!(/^.*turbo\.min.*$\n?/, '')
|
|
710
|
+
|
|
711
|
+
# Remove CSRF
|
|
712
|
+
content.gsub!(/<meta name="csrf-param"[^>]*>/, '')
|
|
713
|
+
content.gsub!(/<meta name="csrf-token"[^>]*>/, '')
|
|
714
|
+
|
|
715
|
+
File.write(file, content)
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# Helper methods
|
|
720
|
+
def add_home_controller_action(page_name)
|
|
721
|
+
controller_path = 'app/controllers/home_controller.rb'
|
|
722
|
+
return unless File.exist?(controller_path)
|
|
723
|
+
|
|
724
|
+
# Add small delay (for consecutive operations)
|
|
725
|
+
sleep(0.1)
|
|
726
|
+
|
|
727
|
+
controller_content = File.read(controller_path)
|
|
728
|
+
|
|
729
|
+
# Don't add if action already exists - check with word boundary
|
|
730
|
+
# "def products" and "def product" should be detected separately
|
|
731
|
+
return if controller_content.match?(/^\s*def\s+#{Regexp.escape(page_name)}\s*$/)
|
|
732
|
+
|
|
733
|
+
# Find class definition and add before last end
|
|
734
|
+
lines = controller_content.split("\n")
|
|
735
|
+
|
|
736
|
+
# Find index of last end line
|
|
737
|
+
last_end_index = lines.rindex { |line| line.strip == 'end' }
|
|
738
|
+
|
|
739
|
+
if last_end_index
|
|
740
|
+
# Add new action before last end
|
|
741
|
+
new_action_lines = [
|
|
742
|
+
" def #{page_name}",
|
|
743
|
+
" end",
|
|
744
|
+
""
|
|
745
|
+
]
|
|
746
|
+
|
|
747
|
+
lines.insert(last_end_index, *new_action_lines)
|
|
748
|
+
controller_content = lines.join("\n")
|
|
749
|
+
File.write(controller_path, controller_content)
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def remove_home_controller_action(page_name)
|
|
754
|
+
controller_path = 'app/controllers/home_controller.rb'
|
|
755
|
+
return unless File.exist?(controller_path)
|
|
756
|
+
|
|
757
|
+
controller_content = File.read(controller_path)
|
|
758
|
+
|
|
759
|
+
# Remove action
|
|
760
|
+
# Find block starting with " def page_name" and ending with " end"
|
|
761
|
+
controller_content.gsub!(/^\s*def #{Regexp.escape(page_name)}\s*$.*?^\s*end\s*$/m, '')
|
|
762
|
+
|
|
763
|
+
# Clean excessive blank lines (reduce to 2 if more than 3 consecutive blank lines)
|
|
764
|
+
controller_content.gsub!(/\n{3,}/, "\n\n")
|
|
765
|
+
|
|
766
|
+
File.write(controller_path, controller_content)
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def clean_unnecessary_files
|
|
770
|
+
# Delete files and folders unnecessary for frontend
|
|
771
|
+
unnecessary_files = [
|
|
772
|
+
'.github',
|
|
773
|
+
'app/models',
|
|
774
|
+
'app/javascript/controllers/hello_controller.js',
|
|
775
|
+
'config/environments/production.rb',
|
|
776
|
+
'config/environments/test.rb',
|
|
777
|
+
'lib',
|
|
778
|
+
'public',
|
|
779
|
+
'script'
|
|
780
|
+
]
|
|
781
|
+
|
|
782
|
+
unnecessary_files.each do |file|
|
|
783
|
+
# Don't error if non-existent folder deletion
|
|
784
|
+
FileUtils.rm_rf(file) if File.exist?(file) || Dir.exist?(file)
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def normalize_name(name)
|
|
789
|
+
# Convert Turkish characters and lowercase
|
|
790
|
+
tr_map = {
|
|
791
|
+
'ç' => 'c', 'Ç' => 'c',
|
|
792
|
+
'ğ' => 'g', 'Ğ' => 'g',
|
|
793
|
+
'ı' => 'i', 'İ' => 'i',
|
|
794
|
+
'ö' => 'o', 'Ö' => 'o',
|
|
795
|
+
'ş' => 's', 'Ş' => 's',
|
|
796
|
+
'ü' => 'u', 'Ü' => 'u'
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
normalized = name.downcase
|
|
800
|
+
tr_map.each { |tr, en| normalized.gsub!(tr, en) }
|
|
801
|
+
normalized.gsub(/[^a-z0-9_]/, '_')
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def get_application_name
|
|
805
|
+
# Read application name from config/application.rb file
|
|
806
|
+
app_config_path = 'config/application.rb'
|
|
807
|
+
if File.exist?(app_config_path)
|
|
808
|
+
content = File.read(app_config_path)
|
|
809
|
+
# Search for "module ApplicationName" pattern
|
|
810
|
+
match = content.match(/module\s+([A-Z][a-zA-Z0-9_]*)/)
|
|
811
|
+
if match
|
|
812
|
+
# Convert CamelCase to title (e.g. MyApp -> My App)
|
|
813
|
+
return match[1].gsub(/([A-Z]+)([A-Z][a-z])/, '\1 \2')
|
|
814
|
+
.gsub(/([a-z\d])([A-Z])/, '\1 \2')
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# Fallback: Use current directory name
|
|
819
|
+
File.basename(Dir.pwd).split('_').map(&:capitalize).join(' ')
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def create_home_controller
|
|
823
|
+
controller_content = <<~RUBY
|
|
824
|
+
class HomeController < ApplicationController
|
|
825
|
+
def index
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
RUBY
|
|
829
|
+
|
|
830
|
+
FileUtils.mkdir_p('app/controllers')
|
|
831
|
+
File.write('app/controllers/home_controller.rb', controller_content)
|
|
832
|
+
|
|
833
|
+
# Create view folder and file
|
|
834
|
+
FileUtils.mkdir_p('app/views/home')
|
|
835
|
+
view_content = <<~HTML
|
|
836
|
+
<div>
|
|
837
|
+
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
|
838
|
+
<div class="container mx-auto px-4 py-16">
|
|
839
|
+
<div class="text-center">
|
|
840
|
+
<h1 class="text-5xl font-bold text-gray-900 mb-4">
|
|
841
|
+
Welcome! 👋
|
|
842
|
+
</h1>
|
|
843
|
+
<p class="text-xl text-gray-600 mb-8">
|
|
844
|
+
Created with Rails Frontend CLI
|
|
845
|
+
</p>
|
|
846
|
+
<div class="inline-block bg-white rounded-lg shadow-lg p-8">
|
|
847
|
+
<p class="text-gray-700 mb-4">
|
|
848
|
+
Your project has been successfully created and is ready to use!
|
|
849
|
+
</p>
|
|
850
|
+
<p class="text-sm text-gray-500">
|
|
851
|
+
You can start developing with Tailwind CSS and Stimulus.
|
|
852
|
+
</p>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
HTML
|
|
859
|
+
|
|
860
|
+
File.write('app/views/home/index.html.erb', view_content)
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
def create_shared_components
|
|
864
|
+
FileUtils.mkdir_p('app/views/shared')
|
|
865
|
+
|
|
866
|
+
# Header
|
|
867
|
+
header_content = <<~HTML
|
|
868
|
+
<header class="bg-white shadow-sm">
|
|
869
|
+
<div class="container mx-auto px-4 py-4">
|
|
870
|
+
<div class="flex items-center justify-between">
|
|
871
|
+
<div class="text-2xl font-bold text-indigo-600">
|
|
872
|
+
Logo
|
|
873
|
+
</div>
|
|
874
|
+
<%= render 'shared/navbar' %>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
</header>
|
|
878
|
+
HTML
|
|
879
|
+
File.write('app/views/shared/_header.html.erb', header_content)
|
|
880
|
+
|
|
881
|
+
# Navbar
|
|
882
|
+
navbar_content = <<~HTML
|
|
883
|
+
<nav class="hidden md:flex space-x-6">
|
|
884
|
+
<%= link_to "Home", root_path, class: "text-gray-700 hover:text-indigo-600 transition" %>
|
|
885
|
+
<!-- Other menu items will be added here -->
|
|
886
|
+
</nav>
|
|
887
|
+
HTML
|
|
888
|
+
File.write('app/views/shared/_navbar.html.erb', navbar_content)
|
|
889
|
+
|
|
890
|
+
# Footer - Simple and full width
|
|
891
|
+
footer_content = <<~HTML
|
|
892
|
+
<footer class="bg-gray-800 text-white py-6 text-center">
|
|
893
|
+
<p class="text-gray-400">
|
|
894
|
+
© <%= Time.current.year %> All rights reserved.
|
|
895
|
+
</p>
|
|
896
|
+
</footer>
|
|
897
|
+
HTML
|
|
898
|
+
File.write('app/views/shared/_footer.html.erb', footer_content)
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def create_css_files
|
|
902
|
+
FileUtils.mkdir_p('app/assets/stylesheets')
|
|
903
|
+
|
|
904
|
+
# Home CSS
|
|
905
|
+
home_css = <<~CSS
|
|
906
|
+
/* Home page custom styles */
|
|
907
|
+
.home-container {
|
|
908
|
+
/* Custom styles for home page can be added here */
|
|
909
|
+
}
|
|
910
|
+
CSS
|
|
911
|
+
File.write('app/assets/stylesheets/home.css', home_css)
|
|
912
|
+
|
|
913
|
+
# Header CSS
|
|
914
|
+
header_css = <<~CSS
|
|
915
|
+
/* Header custom styles */
|
|
916
|
+
header {
|
|
917
|
+
/* Custom styles for header can be added here */
|
|
918
|
+
}
|
|
919
|
+
CSS
|
|
920
|
+
File.write('app/assets/stylesheets/header.css', header_css)
|
|
921
|
+
|
|
922
|
+
# Navbar CSS
|
|
923
|
+
navbar_css = <<~CSS
|
|
924
|
+
/* Navbar custom styles */
|
|
925
|
+
nav {
|
|
926
|
+
/* Custom styles for navbar can be added here */
|
|
927
|
+
}
|
|
928
|
+
CSS
|
|
929
|
+
File.write('app/assets/stylesheets/navbar.css', navbar_css)
|
|
930
|
+
|
|
931
|
+
# Footer CSS
|
|
932
|
+
footer_css = <<~CSS
|
|
933
|
+
/* Footer custom styles */
|
|
934
|
+
footer {
|
|
935
|
+
/* Custom styles for footer can be added here */
|
|
936
|
+
}
|
|
937
|
+
CSS
|
|
938
|
+
File.write('app/assets/stylesheets/footer.css', footer_css)
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
def create_stimulus_controller(page_name)
|
|
942
|
+
FileUtils.mkdir_p('app/javascript/controllers')
|
|
943
|
+
|
|
944
|
+
controller_content = <<~JS
|
|
945
|
+
import { Controller } from "@hotwired/stimulus"
|
|
946
|
+
|
|
947
|
+
// Stimulus controller for #{page_name.capitalize} page
|
|
948
|
+
export default class extends Controller {
|
|
949
|
+
connect() {
|
|
950
|
+
console.log("#{page_name.capitalize} controller connected")
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
disconnect() {
|
|
954
|
+
console.log("#{page_name.capitalize} controller disconnected")
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Custom methods can be added here
|
|
958
|
+
}
|
|
959
|
+
JS
|
|
960
|
+
|
|
961
|
+
File.write("app/javascript/controllers/#{page_name}_controller.js", controller_content)
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
def create_asset_folders
|
|
965
|
+
# Images folder
|
|
966
|
+
FileUtils.mkdir_p('app/assets/images')
|
|
967
|
+
File.write('app/assets/images/.keep', '')
|
|
968
|
+
|
|
969
|
+
# Fonts folder
|
|
970
|
+
FileUtils.mkdir_p('app/assets/fonts')
|
|
971
|
+
File.write('app/assets/fonts/.keep', '')
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def update_layout
|
|
975
|
+
layout_path = 'app/views/layouts/application.html.erb'
|
|
976
|
+
return unless File.exist?(layout_path)
|
|
977
|
+
|
|
978
|
+
layout_content = File.read(layout_path)
|
|
979
|
+
|
|
980
|
+
# Add UTF-8 charset meta tag (if not present)
|
|
981
|
+
if !layout_content.match?(/<meta\s+charset\s*=\s*["']utf-8["']/i)
|
|
982
|
+
layout_content.gsub!(/<\/title>/) do
|
|
983
|
+
<<~HTML.chomp
|
|
984
|
+
</title>
|
|
985
|
+
<meta charset="utf-8">
|
|
986
|
+
HTML
|
|
987
|
+
end
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
# First clean existing main tags
|
|
991
|
+
layout_content.gsub!(/<main[^>]*>/, '')
|
|
992
|
+
layout_content.gsub!(/<\/main>/, '')
|
|
993
|
+
|
|
994
|
+
# Add shared components inside body
|
|
995
|
+
if layout_content.include?('<body>')
|
|
996
|
+
new_layout = layout_content.gsub(/<body>/) do
|
|
997
|
+
<<~HTML.chomp
|
|
998
|
+
<body>
|
|
999
|
+
<%= render 'shared/header' %>
|
|
1000
|
+
<main class="min-h-screen">
|
|
1001
|
+
HTML
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
# Add footer after yield
|
|
1005
|
+
new_layout = new_layout.gsub(/\s*<%= yield %>/) do
|
|
1006
|
+
<<~HTML.chomp
|
|
1007
|
+
<%= yield %>
|
|
1008
|
+
</main>
|
|
1009
|
+
<%= render 'shared/footer' %>
|
|
1010
|
+
HTML
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
File.write(layout_path, new_layout)
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def update_routes(page_name, action, root: false)
|
|
1018
|
+
routes_path = 'config/routes.rb'
|
|
1019
|
+
routes_content = File.read(routes_path)
|
|
1020
|
+
|
|
1021
|
+
if root
|
|
1022
|
+
# Add root route
|
|
1023
|
+
new_route = " root \"home##{action}\"\n"
|
|
1024
|
+
|
|
1025
|
+
# Replace if existing root route exists, otherwise add
|
|
1026
|
+
if routes_content.match?(/^\s*root/)
|
|
1027
|
+
routes_content.gsub!(/^\s*root.*$/, new_route.strip)
|
|
1028
|
+
else
|
|
1029
|
+
routes_content.gsub!(/Rails\.application\.routes\.draw do\n/) do |match|
|
|
1030
|
+
"#{match}#{new_route}"
|
|
1031
|
+
end
|
|
1032
|
+
end
|
|
1033
|
+
else
|
|
1034
|
+
# Add normal route (use home controller)
|
|
1035
|
+
new_route = " get \"/#{page_name}\", to: \"home##{action}\"\n"
|
|
1036
|
+
|
|
1037
|
+
# Don't add if route already exists
|
|
1038
|
+
unless routes_content.include?(new_route.strip)
|
|
1039
|
+
routes_content.gsub!(/Rails\.application\.routes\.draw do\n/) do |match|
|
|
1040
|
+
"#{match}#{new_route}"
|
|
1041
|
+
end
|
|
1042
|
+
end
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
File.write(routes_path, routes_content)
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
def remove_route(page_name)
|
|
1049
|
+
routes_path = 'config/routes.rb'
|
|
1050
|
+
routes_content = File.read(routes_path)
|
|
1051
|
+
|
|
1052
|
+
# Remove route line
|
|
1053
|
+
routes_content.gsub!(/^\s*get\s+['"]\/#{page_name}['"].*\n/, '')
|
|
1054
|
+
|
|
1055
|
+
File.write(routes_path, routes_content)
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
def create_controller(page_name)
|
|
1059
|
+
controller_content = <<~RUBY
|
|
1060
|
+
class #{page_name.capitalize}Controller < ApplicationController
|
|
1061
|
+
def index
|
|
1062
|
+
end
|
|
1063
|
+
end
|
|
1064
|
+
RUBY
|
|
1065
|
+
|
|
1066
|
+
File.write("app/controllers/#{page_name}_controller.rb", controller_content)
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
def create_view(page_name)
|
|
1070
|
+
# Create view in Home folder
|
|
1071
|
+
FileUtils.mkdir_p("app/views/home")
|
|
1072
|
+
|
|
1073
|
+
view_content = <<~HTML
|
|
1074
|
+
<div>
|
|
1075
|
+
<div class="container mx-auto px-4 py-16">
|
|
1076
|
+
<h1 class="text-4xl font-bold text-gray-900 mb-4">
|
|
1077
|
+
#{page_name.capitalize}
|
|
1078
|
+
</h1>
|
|
1079
|
+
<p class="text-gray-600">
|
|
1080
|
+
#{page_name.capitalize} page content will go here.
|
|
1081
|
+
</p>
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
HTML
|
|
1085
|
+
|
|
1086
|
+
File.write("app/views/home/#{page_name}.html.erb", view_content)
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
def create_css(page_name)
|
|
1090
|
+
css_content = <<~CSS
|
|
1091
|
+
/* #{page_name.capitalize} page custom styles */
|
|
1092
|
+
.#{page_name}-container {
|
|
1093
|
+
/* Custom styles for #{page_name} page can be added here */
|
|
1094
|
+
}
|
|
1095
|
+
CSS
|
|
1096
|
+
|
|
1097
|
+
File.write("app/assets/stylesheets/#{page_name}.css", css_content)
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
def update_procfile
|
|
1101
|
+
procfile_path = 'Procfile.dev'
|
|
1102
|
+
|
|
1103
|
+
procfile_content = <<~PROCFILE
|
|
1104
|
+
web: bin/rails server -b 0.0.0.0
|
|
1105
|
+
css: bin/rails tailwindcss:watch
|
|
1106
|
+
PROCFILE
|
|
1107
|
+
|
|
1108
|
+
File.write(procfile_path, procfile_content)
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
def list_home_views
|
|
1112
|
+
views = []
|
|
1113
|
+
if Dir.exist?('app/views/home')
|
|
1114
|
+
Dir.glob('app/views/home/*.html.erb').each do |file|
|
|
1115
|
+
basename = File.basename(file, '.html.erb')
|
|
1116
|
+
# Exclude index
|
|
1117
|
+
views << basename unless basename == 'index'
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
views.sort
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
def create_layout_file(layout_name)
|
|
1124
|
+
FileUtils.mkdir_p('app/views/layouts')
|
|
1125
|
+
|
|
1126
|
+
layout_content = <<~HTML
|
|
1127
|
+
<!DOCTYPE html>
|
|
1128
|
+
<html>
|
|
1129
|
+
<head>
|
|
1130
|
+
<title><%= content_for(:title) || "#{layout_name.capitalize}" %></title>
|
|
1131
|
+
<meta charset="utf-8">
|
|
1132
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1133
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
1134
|
+
<meta name="application-name" content="#{get_application_name}">
|
|
1135
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
1136
|
+
<%= yield :head %>
|
|
1137
|
+
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
|
1138
|
+
<%= javascript_importmap_tags %>
|
|
1139
|
+
</head>
|
|
1140
|
+
|
|
1141
|
+
<body>
|
|
1142
|
+
<main>
|
|
1143
|
+
<%= yield %>
|
|
1144
|
+
</main>
|
|
1145
|
+
</body>
|
|
1146
|
+
</html>
|
|
1147
|
+
HTML
|
|
1148
|
+
|
|
1149
|
+
File.write("app/views/layouts/#{layout_name}.html.erb", layout_content)
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
def add_layout_directive(layout_name, view_name)
|
|
1153
|
+
controller_file = 'app/controllers/home_controller.rb'
|
|
1154
|
+
unless File.exist?(controller_file)
|
|
1155
|
+
error_message("Home controller not found: #{controller_file}")
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
controller_content = File.read(controller_file)
|
|
1159
|
+
|
|
1160
|
+
# Check if layout directive already exists
|
|
1161
|
+
if controller_content.match?(/layout\s+["']#{layout_name}["']/)
|
|
1162
|
+
puts "\n#{colorize("WARNING: This layout directive already exists!", :yellow)}"
|
|
1163
|
+
return
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
# Add layout directive after class definition
|
|
1167
|
+
layout_line = " layout \"#{layout_name}\", only: :#{view_name}\n"
|
|
1168
|
+
|
|
1169
|
+
if controller_content.match?(/class\s+HomeController\s*<\s*ApplicationController\s*\n/)
|
|
1170
|
+
controller_content.sub!(/class\s+HomeController\s*<\s*ApplicationController\s*\n/) do |match|
|
|
1171
|
+
"#{match}#{layout_line}\n"
|
|
1172
|
+
end
|
|
1173
|
+
else
|
|
1174
|
+
error_message("HomeController class definition not found!")
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
File.write(controller_file, controller_content)
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
def remove_layout_directive(layout_name)
|
|
1181
|
+
controller_file = 'app/controllers/home_controller.rb'
|
|
1182
|
+
unless File.exist?(controller_file)
|
|
1183
|
+
error_message("Home controller not found: #{controller_file}")
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
controller_content = File.read(controller_file)
|
|
1187
|
+
|
|
1188
|
+
# Find and remove layout directive
|
|
1189
|
+
controller_content.gsub!(/^\s*layout\s+["']#{layout_name}["'].*\n/, '')
|
|
1190
|
+
|
|
1191
|
+
File.write(controller_file, controller_content)
|
|
1192
|
+
end
|
|
1193
|
+
|
|
1194
|
+
def find_existing_layout_for_view(view_name)
|
|
1195
|
+
controller_file = 'app/controllers/home_controller.rb'
|
|
1196
|
+
return nil unless File.exist?(controller_file)
|
|
1197
|
+
|
|
1198
|
+
controller_content = File.read(controller_file)
|
|
1199
|
+
|
|
1200
|
+
# Search for layout "layout_name", only: :view_name pattern
|
|
1201
|
+
match = controller_content.match(/layout\s+["']([^"']+)["'].*only:\s*:#{view_name}\b/)
|
|
1202
|
+
match ? match[1] : nil
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
def check_pin_usage(pin_name)
|
|
1206
|
+
used_files = []
|
|
1207
|
+
|
|
1208
|
+
# Search in JavaScript files
|
|
1209
|
+
if Dir.exist?('app/javascript')
|
|
1210
|
+
Dir.glob('app/javascript/**/*.js').each do |file|
|
|
1211
|
+
content = File.read(file)
|
|
1212
|
+
# Check for exact match with import or from
|
|
1213
|
+
# Example: from "alpinejs" or import "chart.js" or import Alpine from "alpinejs"
|
|
1214
|
+
if content.match?(/from\s+["']#{Regexp.escape(pin_name)}["']/) ||
|
|
1215
|
+
content.match?(/import\s+["']#{Regexp.escape(pin_name)}["']/) ||
|
|
1216
|
+
content.match?(/import\s+.+\s+from\s+["']#{Regexp.escape(pin_name)}["']/)
|
|
1217
|
+
used_files << file
|
|
1218
|
+
end
|
|
1219
|
+
end
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
# Search in HTML/ERB files
|
|
1223
|
+
if Dir.exist?('app/views')
|
|
1224
|
+
Dir.glob('app/views/**/*.html.erb').each do |file|
|
|
1225
|
+
content = File.read(file)
|
|
1226
|
+
# Check for exact match inside script tag or importmap
|
|
1227
|
+
# Looking for exact match in quotes
|
|
1228
|
+
if content.match?(/["']#{Regexp.escape(pin_name)}["']/)
|
|
1229
|
+
used_files << file
|
|
1230
|
+
end
|
|
1231
|
+
end
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
used_files.uniq
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
# Message methods
|
|
1238
|
+
def show_title(message)
|
|
1239
|
+
puts colorize(message, :blue, bold: true)
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1242
|
+
def show_message(message)
|
|
1243
|
+
# Just show message, no numbering
|
|
1244
|
+
print " #{message} "
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
def success_message(message)
|
|
1248
|
+
puts colorize('OK', :green)
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
def error_message(message)
|
|
1252
|
+
puts "\n#{colorize('ERROR:', :red)} #{message}\n"
|
|
1253
|
+
exit 1
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
def completion_message
|
|
1257
|
+
puts colorize("Project successfully created!", :green, bold: true)
|
|
1258
|
+
puts "\n#{colorize('Next steps:', :blue)}"
|
|
1259
|
+
puts " 1. cd #{@project_name}"
|
|
1260
|
+
puts " 2. rails-frontend run"
|
|
1261
|
+
puts " 3. Open http://localhost:3000 in your browser"
|
|
1262
|
+
puts "\n#{colorize('For help:', :blue)}"
|
|
1263
|
+
puts " rails-frontend --help"
|
|
1264
|
+
puts ""
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
def show_help
|
|
1268
|
+
show_title("Rails Frontend CLI v#{VERSION}")
|
|
1269
|
+
|
|
1270
|
+
puts colorize("Rails project management tool for frontend developers", :blue)
|
|
1271
|
+
puts ""
|
|
1272
|
+
puts colorize("USAGE:", :yellow, bold: true)
|
|
1273
|
+
puts " rails-frontend COMMAND [PARAMETERS]"
|
|
1274
|
+
puts ""
|
|
1275
|
+
|
|
1276
|
+
puts colorize("COMMANDS:", :yellow, bold: true)
|
|
1277
|
+
puts <<~COMMANDS
|
|
1278
|
+
Project Management:
|
|
1279
|
+
new, n PROJECT_NAME [--clean] Create new Rails frontend project
|
|
1280
|
+
build, b Build static site
|
|
1281
|
+
run, r Start server (bin/dev)
|
|
1282
|
+
|
|
1283
|
+
Page Management:
|
|
1284
|
+
add-page, ap PAGE_NAME Add new page (view + CSS + route)
|
|
1285
|
+
remove-page, rp PAGE_NAME Remove page
|
|
1286
|
+
|
|
1287
|
+
Stimulus Controller:
|
|
1288
|
+
add-stimulus, as CONTROLLER Add Stimulus controller
|
|
1289
|
+
remove-stimulus, rs CONTROLLER Remove Stimulus controller (checks usage)
|
|
1290
|
+
|
|
1291
|
+
Layout Management:
|
|
1292
|
+
add-layout, al LAYOUT_NAME Add layout (with view matching)
|
|
1293
|
+
remove-layout, rl LAYOUT_NAME Remove layout
|
|
1294
|
+
|
|
1295
|
+
JavaScript Libraries:
|
|
1296
|
+
add-pin, pin PACKAGE_NAME Add external JavaScript library
|
|
1297
|
+
remove-pin, unpin PACKAGE_NAME Remove external JavaScript library (checks usage)
|
|
1298
|
+
|
|
1299
|
+
Info:
|
|
1300
|
+
version, -v, --version Show version info
|
|
1301
|
+
help, -h, --help Show this help message
|
|
1302
|
+
COMMANDS
|
|
1303
|
+
|
|
1304
|
+
puts colorize("OPTIONS:", :yellow, bold: true)
|
|
1305
|
+
puts " --clean Clean unnecessary files for frontend"
|
|
1306
|
+
puts " (tests, mailers, jobs, channels, models etc.)"
|
|
1307
|
+
puts ""
|
|
1308
|
+
|
|
1309
|
+
puts colorize("EXAMPLES:", :yellow, bold: true)
|
|
1310
|
+
puts <<~EXAMPLES
|
|
1311
|
+
Create new project:
|
|
1312
|
+
rails-frontend new blog --clean
|
|
1313
|
+
|
|
1314
|
+
Add page:
|
|
1315
|
+
rails-frontend add-page about
|
|
1316
|
+
|
|
1317
|
+
Add layout:
|
|
1318
|
+
rails-frontend add-layout contact
|
|
1319
|
+
|
|
1320
|
+
Add JavaScript library:
|
|
1321
|
+
rails-frontend add-pin sweetalert2
|
|
1322
|
+
|
|
1323
|
+
Add Stimulus controller:
|
|
1324
|
+
rails-frontend add-stimulus dropdown
|
|
1325
|
+
|
|
1326
|
+
Start server:
|
|
1327
|
+
rails-frontend run
|
|
1328
|
+
EXAMPLES
|
|
1329
|
+
|
|
1330
|
+
puts colorize("MORE INFO:", :blue)
|
|
1331
|
+
puts " Detailed user manual: USER_MANUAL.md"
|
|
1332
|
+
puts " GitHub: https://github.com/ozbilgic/rails-frontend-cli"
|
|
1333
|
+
puts ""
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
def colorize(text, color, bold: false)
|
|
1337
|
+
colors = {
|
|
1338
|
+
red: 31,
|
|
1339
|
+
green: 32,
|
|
1340
|
+
yellow: 33,
|
|
1341
|
+
blue: 34,
|
|
1342
|
+
magenta: 35,
|
|
1343
|
+
cyan: 36
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
color_code = colors[color] || 37
|
|
1347
|
+
bold_code = bold ? '1;' : ''
|
|
1348
|
+
|
|
1349
|
+
"\e[#{bold_code}#{color_code}m#{text}\e[0m"
|
|
1350
|
+
end
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
# When run as a script
|
|
1354
|
+
if __FILE__ == $0
|
|
1355
|
+
cli = RailsFrontendCLI.new
|
|
1356
|
+
cli.run(ARGV)
|
|
1357
|
+
end
|