static-site-builder 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/generator.rb ADDED
@@ -0,0 +1,933 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+ require "erb"
6
+ require "json"
7
+
8
+ module StaticSiteBuilder
9
+ # Generator for creating new static site projects
10
+ #
11
+ # @example Generate a site with default options
12
+ # generator = StaticSiteBuilder::Generator.new("my-site")
13
+ # generator.generate
14
+ #
15
+ # @example Generate a site with custom stack
16
+ # generator = StaticSiteBuilder::Generator.new("my-site", {
17
+ # template_engine: "phlex",
18
+ # js_bundler: "esbuild",
19
+ # css_framework: "shadcn",
20
+ # js_framework: "react"
21
+ # })
22
+ # generator.generate
23
+ class Generator
24
+ # Available template engines
25
+ TEMPLATE_ENGINES = %w[erb phlex].freeze
26
+
27
+ # Available JavaScript bundlers
28
+ JS_BUNDLERS = %w[importmap esbuild webpack vite none].freeze
29
+
30
+ # Available CSS frameworks
31
+ CSS_FRAMEWORKS = %w[tailwindcss shadcn plain].freeze
32
+
33
+ # Available JavaScript frameworks
34
+ JS_FRAMEWORKS = %w[stimulus react vue alpine vanilla].freeze
35
+
36
+ # Initialize a new generator
37
+ #
38
+ # @param app_name [String] Name of the application/site to generate
39
+ # @param options [Hash] Configuration options
40
+ # @option options [String] :template_engine ("erb") Template engine to use (erb or phlex)
41
+ # @option options [String] :js_bundler ("importmap") JavaScript bundler (importmap, esbuild, webpack, vite, none)
42
+ # @option options [String] :css_framework ("tailwindcss") CSS framework (tailwindcss, shadcn, plain)
43
+ # @option options [String] :js_framework ("stimulus") JavaScript framework (stimulus, react, vue, alpine, vanilla)
44
+ def initialize(app_name, options = {})
45
+ @app_name = app_name
46
+ @app_path = Pathname.new(app_name)
47
+ @options = {
48
+ template_engine: options[:template_engine] || "erb",
49
+ js_bundler: options[:js_bundler] || "importmap",
50
+ css_framework: options[:css_framework] || "tailwindcss",
51
+ js_framework: options[:js_framework] || "stimulus"
52
+ }
53
+ end
54
+
55
+ # Generate the static site project
56
+ #
57
+ # Creates all necessary files and directory structure for the project.
58
+ # Outputs instructions for next steps after generation.
59
+ #
60
+ # @return [void]
61
+ def generate
62
+ puts "Generating static site: #{@app_name}"
63
+ puts "Stack: #{@options[:template_engine]} + #{@options[:js_bundler]} + #{@options[:css_framework]} + #{@options[:js_framework]}"
64
+
65
+ create_directory_structure
66
+ create_gemfile
67
+ create_package_json
68
+ create_config_files
69
+ create_app_structure
70
+ create_build_files
71
+ create_example_pages
72
+ create_readme
73
+
74
+ puts "\n✓ Site generated successfully!"
75
+ puts "\nNext steps:"
76
+ puts " cd #{@app_name}"
77
+ puts " bundle install"
78
+ puts " npm install" if needs_npm?
79
+ puts " rake dev:server # Start development server with auto-rebuild"
80
+ puts " # or"
81
+ puts " rake build:all # Build for production"
82
+ end
83
+
84
+ private
85
+
86
+ def create_directory_structure
87
+ dirs = [
88
+ "app/views/layouts",
89
+ "app/views/pages",
90
+ "app/javascript",
91
+ "app/assets/stylesheets",
92
+ "config",
93
+ "lib",
94
+ "public"
95
+ ]
96
+
97
+ dirs.each do |dir|
98
+ FileUtils.mkdir_p(@app_path.join(dir))
99
+ end
100
+ end
101
+
102
+ def create_gemfile
103
+ gems = [
104
+ "rake",
105
+ "static-site-builder",
106
+ "webrick" # Required for dev server (removed from stdlib in Ruby 3.0+)
107
+ ]
108
+ gems << "importmap-rails" if @options[:js_bundler] == "importmap"
109
+ gems << "phlex-rails" if @options[:template_engine] == "phlex"
110
+ if @options[:edit_rails]
111
+ gems << "rails"
112
+ gems << "edit_rails"
113
+ end
114
+
115
+ content = <<~RUBY
116
+ # frozen_string_literal: true
117
+
118
+ source "https://rubygems.org"
119
+
120
+ #{gems.map { |g| %(gem "#{g}") }.join("\n")}
121
+ RUBY
122
+
123
+ write_file("Gemfile", content)
124
+ end
125
+
126
+ def create_package_json
127
+ return unless needs_npm?
128
+
129
+ deps = {}
130
+ dev_deps = {}
131
+
132
+ case @options[:js_bundler]
133
+ when "esbuild"
134
+ dev_deps["esbuild"] = "^0.19.0"
135
+ when "webpack"
136
+ dev_deps["webpack"] = "^5.0.0"
137
+ dev_deps["webpack-cli"] = "^5.0.0"
138
+ when "vite"
139
+ dev_deps["vite"] = "^5.0.0"
140
+ dev_deps["vite-plugin-ruby"] = "^3.0.0"
141
+ end
142
+
143
+ case @options[:css_framework]
144
+ when "tailwindcss", "shadcn"
145
+ dev_deps["tailwindcss"] = "^3.4.0"
146
+ dev_deps["autoprefixer"] = "^10.4.0"
147
+ dev_deps["postcss"] = "^8.4.0"
148
+ end
149
+
150
+ case @options[:js_framework]
151
+ when "react"
152
+ deps["react"] = "^18.0.0"
153
+ deps["react-dom"] = "^18.0.0"
154
+ when "vue"
155
+ deps["vue"] = "^3.0.0"
156
+ when "alpine"
157
+ deps["alpinejs"] = "^3.0.0"
158
+ when "stimulus"
159
+ deps["@hotwired/stimulus"] = "^3.2.0"
160
+ end
161
+
162
+ scripts = {}
163
+ scripts["build"] = build_script
164
+ scripts["build:css"] = css_build_script if needs_css_build?
165
+ scripts["watch:css"] = css_watch_script if needs_css_build?
166
+
167
+ content = {
168
+ name: @app_name,
169
+ version: "1.0.0",
170
+ description: "Static site generated with static-site-generator",
171
+ scripts: scripts,
172
+ dependencies: deps,
173
+ devDependencies: dev_deps
174
+ }
175
+
176
+ write_file("package.json", JSON.pretty_generate(content))
177
+ end
178
+
179
+ def create_config_files
180
+ create_importmap_config if @options[:js_bundler] == "importmap"
181
+ create_tailwind_config if @options[:css_framework] == "tailwindcss" || @options[:css_framework] == "shadcn"
182
+ create_esbuild_config if @options[:js_bundler] == "esbuild"
183
+ create_webpack_config if @options[:js_bundler] == "webpack"
184
+ create_vite_config if @options[:js_bundler] == "vite"
185
+ create_rails_config if @options[:edit_rails]
186
+ end
187
+
188
+ def create_importmap_config
189
+ content = <<~RUBY
190
+ # frozen_string_literal: true
191
+
192
+ pin "application", preload: true
193
+ #{stimulus_pin if @options[:js_framework] == "stimulus"}
194
+ RUBY
195
+
196
+ write_file("config/importmap.rb", content)
197
+ end
198
+
199
+ def stimulus_pin
200
+ <<~RUBY
201
+ pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
202
+ pin_all_from "app/javascript/controllers", under: "controllers"
203
+ RUBY
204
+ end
205
+
206
+
207
+ # Detect which package manager is available
208
+ def detect_package_manager
209
+ # Check in order: yarn, pnpm, bun, npm
210
+ return "yarn" if system("which yarn > /dev/null 2>&1")
211
+ return "pnpm" if system("which pnpm > /dev/null 2>&1")
212
+ return "bun" if system("which bun > /dev/null 2>&1")
213
+ return "npm" if system("which npm > /dev/null 2>&1")
214
+ nil
215
+ end
216
+
217
+ def create_tailwind_config
218
+ content = <<~JS
219
+ /** @type {import('tailwindcss').Config} */
220
+ module.exports = {
221
+ content: [
222
+ "./app/views/**/*.{html,erb,phlex}",
223
+ "./app/javascript/**/*.js",
224
+ ],
225
+ theme: {
226
+ extend: {},
227
+ },
228
+ plugins: [],
229
+ }
230
+ JS
231
+
232
+ write_file("tailwind.config.js", content)
233
+
234
+ # Create PostCSS config
235
+ postcss_content = <<~JS
236
+ module.exports = {
237
+ plugins: {
238
+ tailwindcss: {},
239
+ autoprefixer: {},
240
+ },
241
+ }
242
+ JS
243
+
244
+ write_file("postcss.config.js", postcss_content)
245
+ end
246
+
247
+ def create_esbuild_config
248
+ content = <<~JS
249
+ require('esbuild').build({
250
+ entryPoints: ['app/javascript/application.js'],
251
+ bundle: true,
252
+ outdir: 'dist/assets/javascripts',
253
+ format: 'esm',
254
+ minify: true,
255
+ }).catch(() => process.exit(1))
256
+ JS
257
+
258
+ write_file("esbuild.config.js", content)
259
+ end
260
+
261
+ def create_webpack_config
262
+ content = <<~JS
263
+ const path = require('path');
264
+
265
+ module.exports = {
266
+ entry: './app/javascript/application.js',
267
+ output: {
268
+ filename: 'application.js',
269
+ path: path.resolve(__dirname, 'dist/assets/javascripts'),
270
+ },
271
+ mode: 'production',
272
+ };
273
+ JS
274
+
275
+ write_file("webpack.config.js", content)
276
+ end
277
+
278
+ def create_vite_config
279
+ content = <<~JS
280
+ import { defineConfig } from 'vite';
281
+ import RubyPlugin from 'vite-plugin-ruby';
282
+
283
+ export default defineConfig({
284
+ plugins: [RubyPlugin()],
285
+ });
286
+ JS
287
+
288
+ write_file("vite.config.js", content)
289
+ end
290
+
291
+ def create_rails_config
292
+ create_boot_config
293
+ create_routes_config
294
+ create_application_config
295
+ create_environment_file
296
+ create_environment_configs
297
+ end
298
+
299
+ def create_boot_config
300
+ content = <<~RUBY
301
+ # frozen_string_literal: true
302
+
303
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
304
+
305
+ require "bundler/setup" # Set up gems listed in the Gemfile.
306
+ RUBY
307
+
308
+ write_file("config/boot.rb", content)
309
+ end
310
+
311
+ def create_routes_config
312
+ content = <<~RUBY
313
+ # frozen_string_literal: true
314
+
315
+ Rails.application.routes.draw do
316
+ mount EditRails::Engine => "/edit_rails", as: "edit_rails"
317
+ end
318
+ RUBY
319
+
320
+ write_file("config/routes.rb", content)
321
+ end
322
+
323
+ def create_application_config
324
+ content = <<~RUBY
325
+ # frozen_string_literal: true
326
+
327
+ require_relative "boot"
328
+
329
+ require "rails/all"
330
+
331
+ Bundler.require(*Rails.groups)
332
+
333
+ module StaticSite
334
+ class Application < Rails::Application
335
+ config.load_defaults Rails::VERSION::STRING.to_f
336
+
337
+ # Configuration for the application's routes
338
+ config.api_only = false
339
+
340
+ # Don't generate system test files
341
+ config.generators.system_tests = nil
342
+
343
+ # Serve static files from public directory
344
+ config.public_file_server.enabled = true
345
+
346
+ # Don't require database
347
+ config.active_record.maintain_test_schema = false
348
+ end
349
+ end
350
+ RUBY
351
+
352
+ write_file("config/application.rb", content)
353
+ end
354
+
355
+ def create_environment_file
356
+ content = <<~RUBY
357
+ # frozen_string_literal: true
358
+
359
+ # Load the Rails application.
360
+ require_relative "application"
361
+
362
+ # Initialize the Rails application.
363
+ Rails.application.initialize!
364
+ RUBY
365
+
366
+ write_file("config/environment.rb", content)
367
+ end
368
+
369
+ def create_environment_configs
370
+ # Create development environment config
371
+ dev_content = <<~RUBY
372
+ # frozen_string_literal: true
373
+
374
+ require "active_support/core_ext/integer/time"
375
+
376
+ Rails.application.configure do
377
+ config.cache_classes = false
378
+ config.eager_load = false
379
+ config.consider_all_requests_local = true
380
+ config.server_timing = true
381
+ config.public_file_server.enabled = true
382
+ config.public_file_server.headers = {
383
+ "Cache-Control" => "public, max-age=#{1.hour.to_i}"
384
+ }
385
+ end
386
+ RUBY
387
+
388
+ write_file("config/environments/development.rb", dev_content)
389
+ end
390
+
391
+ def create_app_structure
392
+ create_layout
393
+ create_javascript_entry
394
+ create_css_entry
395
+ end
396
+
397
+ def create_layout
398
+ if @options[:template_engine] == "phlex"
399
+ create_phlex_layout
400
+ else
401
+ create_erb_layout
402
+ end
403
+ end
404
+
405
+ def create_erb_layout
406
+ content = <<~ERB
407
+ <!DOCTYPE html>
408
+ <html lang="en">
409
+ <head>
410
+ <meta charset="UTF-8">
411
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
412
+ <title><%= frontmatter['title'] || 'Site' %></title>
413
+ <link rel="stylesheet" href="/assets/stylesheets/application.css">
414
+ </head>
415
+ <body>
416
+ <main>
417
+ <%= page_content %>
418
+ </main>
419
+
420
+ #{importmap_script if @options[:js_bundler] == "importmap"}
421
+ #{js_script}
422
+ </body>
423
+ </html>
424
+ ERB
425
+
426
+ write_file("app/views/layouts/application.html.erb", content)
427
+ end
428
+
429
+ def create_phlex_layout
430
+ content = <<~RUBY
431
+ # frozen_string_literal: true
432
+
433
+ class ApplicationLayout < Phlex::HTML
434
+ def initialize(title: "Site", **options)
435
+ @title = title
436
+ @options = options
437
+ end
438
+
439
+ def template
440
+ html do
441
+ head do
442
+ meta charset: "UTF-8"
443
+ meta name: "viewport", content: "width=device-width, initial-scale=1.0"
444
+ title { @title }
445
+ link rel: "stylesheet", href: "/assets/stylesheets/application.css"
446
+ end
447
+ body do
448
+ main { yield }
449
+ #{importmap_script if @options[:js_bundler] == "importmap"}
450
+ #{js_script}
451
+ end
452
+ end
453
+ end
454
+ end
455
+ RUBY
456
+
457
+ write_file("app/views/layouts/application.rb", content)
458
+ end
459
+
460
+ def importmap_script
461
+ <<~ERB
462
+ <script type="importmap">
463
+ <%= importmap_json %>
464
+ </script>
465
+ ERB
466
+ end
467
+
468
+ def js_script
469
+ case @options[:js_bundler]
470
+ when "importmap"
471
+ <<~ERB
472
+ <% if js_modules && !js_modules.empty? %>
473
+ <% js_modules.each do |module_name| %>
474
+ <script type="module">import "<%= module_name %>";</script>
475
+ <% end %>
476
+ <% else %>
477
+ <script type="module">import "application";</script>
478
+ <% end %>
479
+ ERB
480
+ when "esbuild", "webpack", "vite"
481
+ '<script type="module" src="/assets/javascripts/application.js"></script>'
482
+ else
483
+ '<script src="/assets/javascripts/application.js"></script>'
484
+ end
485
+ end
486
+
487
+ def create_javascript_entry
488
+ case @options[:js_framework]
489
+ when "stimulus"
490
+ create_stimulus_entry
491
+ when "react"
492
+ create_react_entry
493
+ when "vue"
494
+ create_vue_entry
495
+ when "alpine"
496
+ create_alpine_entry
497
+ else
498
+ create_vanilla_entry
499
+ end
500
+ end
501
+
502
+ def create_stimulus_entry
503
+ # Create controllers directory
504
+ FileUtils.mkdir_p(@app_path.join("app/javascript/controllers"))
505
+
506
+ # Main entry - starts Application and registers controllers (standard Stimulus pattern)
507
+ content = <<~JS
508
+ import { Application } from "@hotwired/stimulus"
509
+
510
+ window.Stimulus = Application.start()
511
+
512
+ // Register your controllers here
513
+ // import HelloController from "./controllers/hello_controller"
514
+ // Stimulus.register("hello", HelloController)
515
+ JS
516
+
517
+ write_file("app/javascript/application.js", content)
518
+ end
519
+
520
+ def create_react_entry
521
+ content = <<~JS
522
+ import React from 'react'
523
+ import { createRoot } from 'react-dom/client'
524
+
525
+ // Import your React components
526
+ // import App from './components/App'
527
+
528
+ document.addEventListener('DOMContentLoaded', () => {
529
+ const container = document.getElementById('app')
530
+ if (container) {
531
+ const root = createRoot(container)
532
+ root.render(<App />)
533
+ }
534
+ })
535
+ JS
536
+
537
+ write_file("app/javascript/application.js", content)
538
+ end
539
+
540
+ def create_vue_entry
541
+ content = <<~JS
542
+ import { createApp } from 'vue'
543
+
544
+ // Import your Vue components
545
+ // import App from './components/App.vue'
546
+
547
+ document.addEventListener('DOMContentLoaded', () => {
548
+ const app = createApp(App)
549
+ app.mount('#app')
550
+ })
551
+ JS
552
+
553
+ write_file("app/javascript/application.js", content)
554
+ end
555
+
556
+ def create_alpine_entry
557
+ content = <<~JS
558
+ import Alpine from 'alpinejs'
559
+
560
+ window.Alpine = Alpine
561
+ Alpine.start()
562
+ JS
563
+
564
+ write_file("app/javascript/application.js", content)
565
+ end
566
+
567
+ def create_vanilla_entry
568
+ content = <<~JS
569
+ // Your vanilla JavaScript code here
570
+ console.log("Application loaded")
571
+ JS
572
+
573
+ write_file("app/javascript/application.js", content)
574
+ end
575
+
576
+ def create_css_entry
577
+ case @options[:css_framework]
578
+ when "tailwindcss"
579
+ write_file("app/assets/stylesheets/application.css", <<~CSS)
580
+ @tailwind base;
581
+ @tailwind components;
582
+ @tailwind utilities;
583
+ CSS
584
+ when "shadcn"
585
+ write_file("app/assets/stylesheets/application.css", <<~CSS)
586
+ @tailwind base;
587
+ @tailwind components;
588
+ @tailwind utilities;
589
+
590
+ @layer base {
591
+ :root {
592
+ --background: 0 0% 100%;
593
+ --foreground: 222.2 84% 4.9%;
594
+ }
595
+ }
596
+ CSS
597
+ else
598
+ write_file("app/assets/stylesheets/application.css", <<~CSS)
599
+ /* Your custom CSS here */
600
+ body {
601
+ font-family: system-ui, sans-serif;
602
+ }
603
+ CSS
604
+ end
605
+ end
606
+
607
+ def create_build_files
608
+ create_rakefile
609
+ create_site_builder
610
+ end
611
+
612
+ def webrick_server_code
613
+ <<~'RUBY'
614
+ require "webrick"
615
+ require "fileutils"
616
+ require "static_site_builder/websocket_server"
617
+
618
+ port = ENV["PORT"] || 3000
619
+ ws_port = ENV["WS_PORT"] || 3001
620
+ dist_dir = Pathname.new(Dir.pwd).join("dist")
621
+ reload_file = Pathname.new(Dir.pwd).join(".reload")
622
+
623
+ # Start WebSocket server for live reload (before first build)
624
+ ws_server = StaticSiteBuilder::WebSocketServer.new(port: ws_port, reload_file: reload_file)
625
+ ws_server.start
626
+
627
+ # Build once before starting (with live reload enabled)
628
+ ENV["LIVE_RELOAD"] = "true"
629
+ ENV["WS_PORT"] = ws_port.to_s
630
+ Rake::Task["build:all"].invoke
631
+
632
+ puts "\n🚀 Starting development server at http://localhost:#{port}"
633
+ puts "📡 WebSocket server at ws://localhost:#{ws_port}"
634
+ puts "📝 Watching for changes... (Ctrl+C to stop)"
635
+ puts "🔄 Live reload enabled - pages will auto-refresh on changes\n"
636
+
637
+ # Simple file watcher - just rebuild when files change (no relaunch needed)
638
+ watcher_code = %q{watched = ['app', 'config']; exts = ['.erb', '.rb', '.js', '.css']; mtimes = {}; loop do; changed = false; watched.each do |dir|; Dir.glob(File.join(dir, '**', '*')).each do |f|; next unless File.file?(f) && exts.any? { |e| f.end_with?(e) }; mtime = File.mtime(f); if mtimes[f] != mtime; mtimes[f] = mtime; changed = true; end; end; end; system('rake build:all > /dev/null 2>&1') if changed; sleep 0.5; end}
639
+ watcher_pid = spawn("ruby", "-e", watcher_code, :err => File::NULL)
640
+
641
+ # Start web server
642
+ server = WEBrick::HTTPServer.new(
643
+ Port: port,
644
+ DocumentRoot: dist_dir.to_s,
645
+ BindAddress: "127.0.0.1"
646
+ )
647
+
648
+ trap("INT") do
649
+ puts "\n\nShutting down..."
650
+ Process.kill("TERM", watcher_pid) if watcher_pid
651
+ ws_server.stop
652
+ server.shutdown
653
+ end
654
+
655
+ server.start
656
+ RUBY
657
+ end
658
+
659
+ def rails_server_code
660
+ <<~'RUBY'
661
+ require "fileutils"
662
+ require "pathname"
663
+
664
+ # Load Rails environment
665
+ require_relative "config/environment"
666
+
667
+ port = ENV["PORT"] || 3000
668
+
669
+ # Build once before starting
670
+ ENV["LIVE_RELOAD"] = "true"
671
+ Rake::Task["build:all"].invoke
672
+
673
+ puts "\n🚀 Starting Rails development server at http://localhost:#{port}"
674
+ puts "📝 EditRails available at http://localhost:#{port}/edit_rails"
675
+ puts "📝 Watching for changes... (Ctrl+C to stop)\n"
676
+
677
+ # Start Rails server
678
+ exec "rails server -p #{port} -b 127.0.0.1"
679
+ RUBY
680
+ end
681
+
682
+ def create_rakefile
683
+ needs_npm = needs_npm?
684
+ has_edit_rails = @options[:edit_rails]
685
+ server_code = has_edit_rails ? rails_server_code : webrick_server_code
686
+
687
+ if needs_npm
688
+ content = <<~RUBY
689
+ # frozen_string_literal: true
690
+
691
+ require_relative "lib/site_builder"
692
+ require "fileutils"
693
+ require "pathname"
694
+
695
+ namespace :build do
696
+ desc "Build everything (assets + HTML)"
697
+ task :all => [:assets, :html] do
698
+ puts "\\n✓ Build complete!"
699
+ end
700
+
701
+ desc "Build JavaScript/CSS assets"
702
+ task :assets do
703
+ sh "npm run build" if File.exist?("package.json")
704
+ end
705
+
706
+ desc "Compile all pages to static HTML"
707
+ task :html do
708
+ load "lib/site_builder.rb"
709
+ end
710
+
711
+ desc "Clean dist directory"
712
+ task :clean do
713
+ dist_dir = Pathname.new(Dir.pwd).join("dist")
714
+ FileUtils.rm_rf(dist_dir) if dist_dir.exist?
715
+ puts "Cleaned \#{dist_dir}"
716
+ end
717
+ end
718
+
719
+ namespace :dev do
720
+ desc "Start development server with auto-rebuild and live reload"
721
+ task :server do
722
+ #{server_code}
723
+ end
724
+ end
725
+
726
+ task default: "build:all"
727
+ RUBY
728
+ else
729
+ content = <<~RUBY
730
+ # frozen_string_literal: true
731
+
732
+ require_relative "lib/site_builder"
733
+ require "fileutils"
734
+ require "pathname"
735
+
736
+ namespace :build do
737
+ desc "Build everything (HTML)"
738
+ task :all => [:html] do
739
+ puts "\\n✓ Build complete!"
740
+ end
741
+
742
+ desc "Compile all pages to static HTML"
743
+ task :html do
744
+ load "lib/site_builder.rb"
745
+ end
746
+
747
+ desc "Clean dist directory"
748
+ task :clean do
749
+ dist_dir = Pathname.new(Dir.pwd).join("dist")
750
+ FileUtils.rm_rf(dist_dir) if dist_dir.exist?
751
+ puts "Cleaned \#{dist_dir}"
752
+ end
753
+ end
754
+
755
+ namespace :dev do
756
+ desc "Start development server with auto-rebuild and live reload"
757
+ task :server do
758
+ #{server_code}
759
+ end
760
+ end
761
+
762
+ task default: "build:all"
763
+ RUBY
764
+ end
765
+
766
+ write_file("Rakefile", content)
767
+ end
768
+
769
+ def create_site_builder
770
+ # Generated sites use the static-site-builder gem
771
+ # This file just configures it for the chosen stack
772
+ phlex_require = @options[:template_engine] == "phlex" ? 'require "phlex-rails"' : ""
773
+ importmap_require = @options[:js_bundler] == "importmap" ? 'require "importmap-rails"' : ""
774
+ importmap_config_line = @options[:js_bundler] == "importmap" ? importmap_config : ""
775
+
776
+ content = <<~RUBY
777
+ # frozen_string_literal: true
778
+
779
+ require "static_site_builder"
780
+ #{phlex_require}
781
+ #{importmap_require}
782
+
783
+ # Configure the builder for your stack
784
+ builder = StaticSiteBuilder::Builder.new(
785
+ root: Dir.pwd,
786
+ template_engine: "#{@options[:template_engine]}",
787
+ js_bundler: "#{@options[:js_bundler]}",
788
+ #{importmap_config_line}
789
+ )
790
+
791
+ # Build the site
792
+ builder.build
793
+ RUBY
794
+
795
+ write_file("lib/site_builder.rb", content)
796
+ end
797
+
798
+ def importmap_config
799
+ <<~RUBY
800
+ importmap_config: "config/importmap.rb",
801
+ RUBY
802
+ end
803
+
804
+ def create_example_pages
805
+ if @options[:template_engine] == "phlex"
806
+ create_phlex_example
807
+ else
808
+ create_erb_example
809
+ end
810
+ end
811
+
812
+ def create_erb_example
813
+ content = <<~ERB
814
+ ---
815
+ title: Home Page
816
+ js: application
817
+ ---
818
+
819
+ <h1>Welcome</h1>
820
+ <p>This is your generated static site.</p>
821
+ ERB
822
+
823
+ write_file("app/views/pages/index.html.erb", content)
824
+ end
825
+
826
+ def create_phlex_example
827
+ content = <<~RUBY
828
+ # frozen_string_literal: true
829
+
830
+ class IndexPage < Phlex::HTML
831
+ def template
832
+ h1 { "Welcome" }
833
+ p { "This is your generated static site." }
834
+ end
835
+ end
836
+ RUBY
837
+
838
+ write_file("app/views/pages/index.rb", content)
839
+ end
840
+
841
+ def create_readme
842
+ content = <<~MD
843
+ # #{@app_name}
844
+
845
+ Generated static site using:
846
+ - Template: #{@options[:template_engine]}
847
+ - JS Bundler: #{@options[:js_bundler]}
848
+ - CSS: #{@options[:css_framework]}
849
+ - JS Framework: #{@options[:js_framework]}
850
+
851
+ ## Setup
852
+
853
+ ```bash
854
+ bundle install
855
+ #{'npm install' if needs_npm?}
856
+ ```
857
+
858
+ ## Development
859
+
860
+ Start the development server with auto-rebuild and live reload:
861
+
862
+ ```bash
863
+ rake dev:server
864
+ ```
865
+
866
+ This will:
867
+ - Build your site to `dist/`
868
+ - Start a web server at `http://localhost:3000`
869
+ - Watch for file changes and rebuild automatically
870
+ - Auto-refresh your browser when files change
871
+
872
+ Change the port with: `PORT=8080 rake dev:server`
873
+
874
+ ## Build
875
+
876
+ Build for production:
877
+
878
+ ```bash
879
+ rake build:all # Build everything (assets + HTML)
880
+ rake build:html # Build HTML only
881
+ ```
882
+
883
+ Output goes to `dist/` directory.
884
+ MD
885
+
886
+ write_file("README.md", content)
887
+ end
888
+
889
+ def build_script
890
+ case @options[:js_bundler]
891
+ when "esbuild"
892
+ "node esbuild.config.js"
893
+ when "webpack"
894
+ "webpack --mode production"
895
+ when "vite"
896
+ "vite build"
897
+ else
898
+ "echo 'No JS bundling needed'"
899
+ end
900
+ end
901
+
902
+ def css_build_script
903
+ if @options[:css_framework] == "tailwindcss" || @options[:css_framework] == "shadcn"
904
+ "tailwindcss -i ./app/assets/stylesheets/application.css -o ./dist/assets/stylesheets/application.css --minify"
905
+ end
906
+ end
907
+
908
+ def css_watch_script
909
+ if @options[:css_framework] == "tailwindcss" || @options[:css_framework] == "shadcn"
910
+ "tailwindcss -i ./app/assets/stylesheets/application.css -o ./dist/assets/stylesheets/application.css --watch"
911
+ end
912
+ end
913
+
914
+ def needs_npm?
915
+ @options[:js_bundler] != "none" ||
916
+ @options[:css_framework] == "tailwindcss" ||
917
+ @options[:css_framework] == "shadcn" ||
918
+ @options[:js_framework] == "react" ||
919
+ @options[:js_framework] == "vue" ||
920
+ @options[:js_framework] == "alpine"
921
+ end
922
+
923
+ def needs_css_build?
924
+ @options[:css_framework] == "tailwindcss" || @options[:css_framework] == "shadcn"
925
+ end
926
+
927
+ def write_file(path, content)
928
+ file_path = @app_path.join(path)
929
+ FileUtils.mkdir_p(file_path.dirname)
930
+ File.write(file_path, content)
931
+ end
932
+ end
933
+ end