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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +61 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE +22 -0
- data/README.md +482 -0
- data/bin/generate +64 -0
- data/exe/static-site-builder +58 -0
- data/lib/generator.rb +933 -0
- data/lib/static_site_builder/builder.rb +456 -0
- data/lib/static_site_builder/version.rb +5 -0
- data/lib/static_site_builder/websocket_server.rb +126 -0
- data/lib/static_site_builder.rb +10 -0
- metadata +127 -0
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
|