static-site-builder 0.1.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -85
- data/README.md +87 -309
- data/bin/generate +3 -40
- data/exe/static-site-builder +1 -40
- data/lib/generator.rb +613 -822
- data/lib/static_site_builder/builder.rb +266 -461
- data/lib/static_site_builder/dev_server.rb +119 -0
- data/lib/static_site_builder/version.rb +1 -1
- data/lib/static_site_builder/websocket_server.rb +97 -21
- data/lib/static_site_builder.rb +17 -4
- metadata +28 -14
- data/ARCHITECTURE.md +0 -61
data/lib/generator.rb
CHANGED
|
@@ -6,69 +6,43 @@ require "erb"
|
|
|
6
6
|
require "json"
|
|
7
7
|
|
|
8
8
|
module StaticSiteBuilder
|
|
9
|
-
#
|
|
9
|
+
# Generates new static site projects.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# generator.generate
|
|
11
|
+
# Creates a complete project structure including Gemfile, build
|
|
12
|
+
# configuration files, example pages, and development server setup.
|
|
14
13
|
#
|
|
15
|
-
# @example Generate a site
|
|
16
|
-
# generator = StaticSiteBuilder::Generator.new("my-site"
|
|
17
|
-
# template_engine: "phlex",
|
|
18
|
-
# js_bundler: "esbuild",
|
|
19
|
-
# css_framework: "shadcn",
|
|
20
|
-
# js_framework: "react"
|
|
21
|
-
# })
|
|
14
|
+
# @example Generate a site
|
|
15
|
+
# generator = StaticSiteBuilder::Generator.new("my-site")
|
|
22
16
|
# generator.generate
|
|
23
17
|
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
18
|
|
|
33
|
-
#
|
|
34
|
-
|
|
19
|
+
# Reference StaticSiteBuilder constants for consistency
|
|
20
|
+
DEFAULT_PORT = StaticSiteBuilder::DEFAULT_PORT
|
|
21
|
+
DEFAULT_WS_PORT = StaticSiteBuilder::DEFAULT_WS_PORT
|
|
35
22
|
|
|
36
|
-
#
|
|
23
|
+
# Initializes a new generator instance.
|
|
37
24
|
#
|
|
38
|
-
# @param app_name [String]
|
|
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)
|
|
25
|
+
# @param app_name [String] The name of the site to generate (will be used as directory name)
|
|
44
26
|
def initialize(app_name, options = {})
|
|
45
27
|
@app_name = app_name
|
|
46
28
|
@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
|
-
}
|
|
29
|
+
@options = {}
|
|
53
30
|
end
|
|
54
31
|
|
|
55
|
-
#
|
|
32
|
+
# Generates the complete static site project.
|
|
56
33
|
#
|
|
57
|
-
# Creates
|
|
58
|
-
#
|
|
34
|
+
# Creates the directory structure, configuration files, example pages, build
|
|
35
|
+
# scripts, and all necessary dependencies. Provides next steps after successful generation.
|
|
59
36
|
#
|
|
60
37
|
# @return [void]
|
|
61
38
|
def generate
|
|
62
39
|
puts "Generating static site: #{@app_name}"
|
|
63
|
-
puts "Stack: #{@options[:template_engine]} + #{@options[:js_bundler]} + #{@options[:css_framework]} + #{@options[:js_framework]}"
|
|
64
40
|
|
|
65
41
|
create_directory_structure
|
|
66
42
|
create_gemfile
|
|
67
|
-
create_package_json
|
|
68
43
|
create_config_files
|
|
69
44
|
create_app_structure
|
|
70
45
|
create_build_files
|
|
71
|
-
create_page_helpers
|
|
72
46
|
create_sitemap_config
|
|
73
47
|
create_example_pages
|
|
74
48
|
create_readme
|
|
@@ -78,10 +52,9 @@ module StaticSiteBuilder
|
|
|
78
52
|
puts "\nNext steps:"
|
|
79
53
|
puts " cd #{@app_name}"
|
|
80
54
|
puts " bundle install"
|
|
81
|
-
puts "
|
|
82
|
-
puts " rake dev:server # Start development server with auto-rebuild"
|
|
55
|
+
puts " bundle exec rake dev:server # Start development server with live reload"
|
|
83
56
|
puts " # or"
|
|
84
|
-
puts " rake build:all # Build for
|
|
57
|
+
puts " bundle exec rake build:all # Build for deployment"
|
|
85
58
|
end
|
|
86
59
|
|
|
87
60
|
private
|
|
@@ -90,6 +63,7 @@ module StaticSiteBuilder
|
|
|
90
63
|
dirs = [
|
|
91
64
|
"app/views/layouts",
|
|
92
65
|
"app/views/pages",
|
|
66
|
+
"app/helpers",
|
|
93
67
|
"app/javascript",
|
|
94
68
|
"app/assets/stylesheets",
|
|
95
69
|
"config",
|
|
@@ -104,142 +78,35 @@ module StaticSiteBuilder
|
|
|
104
78
|
|
|
105
79
|
def create_gemfile
|
|
106
80
|
gems = [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
81
|
+
'rake',
|
|
82
|
+
'actionview',
|
|
83
|
+
'base64', # Required for Ruby 3.4+ (removed from default gems)
|
|
84
|
+
'webrick', # Required for dev server (removed from stdlib in Ruby 3.0+)
|
|
85
|
+
'sitemap_generator' # For generating sitemaps from actual pages
|
|
111
86
|
]
|
|
112
|
-
gems << "importmap-rails" if @options[:js_bundler] == "importmap"
|
|
113
|
-
gems << "phlex-rails" if @options[:template_engine] == "phlex"
|
|
114
|
-
if @options[:edit_rails]
|
|
115
|
-
gems << "rails"
|
|
116
|
-
gems << "edit_rails"
|
|
117
|
-
end
|
|
118
87
|
|
|
119
88
|
content = <<~RUBY
|
|
120
89
|
# frozen_string_literal: true
|
|
121
90
|
|
|
122
|
-
source
|
|
91
|
+
source 'https://rubygems.org'
|
|
123
92
|
|
|
124
|
-
#{gems.map { |g| %(gem
|
|
93
|
+
#{gems.map { |g| %(gem '#{g}') }.join("\n")}
|
|
125
94
|
RUBY
|
|
126
95
|
|
|
127
|
-
write_file(
|
|
96
|
+
write_file('Gemfile', content)
|
|
128
97
|
end
|
|
129
98
|
|
|
130
|
-
def create_package_json
|
|
131
|
-
return unless needs_npm?
|
|
132
|
-
|
|
133
|
-
deps = {}
|
|
134
|
-
dev_deps = {}
|
|
135
|
-
|
|
136
|
-
case @options[:js_bundler]
|
|
137
|
-
when "esbuild"
|
|
138
|
-
dev_deps["esbuild"] = "^0.19.0"
|
|
139
|
-
when "webpack"
|
|
140
|
-
dev_deps["webpack"] = "^5.0.0"
|
|
141
|
-
dev_deps["webpack-cli"] = "^5.0.0"
|
|
142
|
-
when "vite"
|
|
143
|
-
dev_deps["vite"] = "^5.0.0"
|
|
144
|
-
dev_deps["vite-plugin-ruby"] = "^3.0.0"
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
case @options[:css_framework]
|
|
148
|
-
when "tailwindcss", "shadcn"
|
|
149
|
-
dev_deps["tailwindcss"] = "^3.4.0"
|
|
150
|
-
dev_deps["autoprefixer"] = "^10.4.0"
|
|
151
|
-
dev_deps["postcss"] = "^8.4.0"
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
case @options[:js_framework]
|
|
155
|
-
when "react"
|
|
156
|
-
deps["react"] = "^18.0.0"
|
|
157
|
-
deps["react-dom"] = "^18.0.0"
|
|
158
|
-
when "vue"
|
|
159
|
-
deps["vue"] = "^3.0.0"
|
|
160
|
-
when "alpine"
|
|
161
|
-
deps["alpinejs"] = "^3.0.0"
|
|
162
|
-
when "stimulus"
|
|
163
|
-
deps["@hotwired/stimulus"] = "^3.2.0"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
scripts = {}
|
|
167
|
-
scripts["build"] = build_script
|
|
168
|
-
scripts["build:css"] = css_build_script if needs_css_build?
|
|
169
|
-
scripts["watch:css"] = css_watch_script if needs_css_build?
|
|
170
|
-
|
|
171
|
-
content = {
|
|
172
|
-
name: @app_name,
|
|
173
|
-
version: "1.0.0",
|
|
174
|
-
description: "Static site generated with static-site-generator",
|
|
175
|
-
scripts: scripts,
|
|
176
|
-
dependencies: deps,
|
|
177
|
-
devDependencies: dev_deps
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
write_file("package.json", JSON.pretty_generate(content))
|
|
181
|
-
end
|
|
182
99
|
|
|
183
100
|
def create_config_files
|
|
184
|
-
create_importmap_config if @options[:js_bundler] == "importmap"
|
|
185
|
-
create_tailwind_config if @options[:css_framework] == "tailwindcss" || @options[:css_framework] == "shadcn"
|
|
186
|
-
create_esbuild_config if @options[:js_bundler] == "esbuild"
|
|
187
|
-
create_webpack_config if @options[:js_bundler] == "webpack"
|
|
188
|
-
create_vite_config if @options[:js_bundler] == "vite"
|
|
189
|
-
create_rails_config if @options[:edit_rails]
|
|
190
101
|
end
|
|
191
102
|
|
|
192
|
-
def create_page_helpers
|
|
193
|
-
content = <<~RUBY
|
|
194
|
-
# frozen_string_literal: true
|
|
195
|
-
|
|
196
|
-
module PageHelpers
|
|
197
|
-
# Page metadata configuration
|
|
198
|
-
# The builder automatically loads this and sets @title, @description, @url, and @image
|
|
199
|
-
# instance variables for use in your templates.
|
|
200
|
-
# This metadata is also used by sitemap_generator for generating sitemaps.
|
|
201
|
-
PAGES = {
|
|
202
|
-
'/' => {
|
|
203
|
-
title: 'Home',
|
|
204
|
-
description: 'Welcome to my site',
|
|
205
|
-
url: 'https://example.com',
|
|
206
|
-
image: 'https://example.com/image.jpg',
|
|
207
|
-
priority: 1.0,
|
|
208
|
-
changefreq: 'weekly'
|
|
209
|
-
}
|
|
210
|
-
}.freeze
|
|
211
|
-
|
|
212
|
-
def page_title(path = nil)
|
|
213
|
-
path ||= @current_page
|
|
214
|
-
PAGES[path]&.fetch(:title) || 'Site'
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def page_description(path = nil)
|
|
218
|
-
path ||= @current_page
|
|
219
|
-
PAGES[path]&.fetch(:description) || 'A static site'
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def page_url(path = nil)
|
|
223
|
-
path ||= @current_page
|
|
224
|
-
PAGES[path]&.fetch(:url) || 'https://example.com'
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def page_image(path = nil)
|
|
228
|
-
path ||= @current_page
|
|
229
|
-
PAGES[path]&.fetch(:image) || 'https://example.com/image.jpg'
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
RUBY
|
|
233
|
-
|
|
234
|
-
write_file("lib/page_helpers.rb", content)
|
|
235
|
-
end
|
|
236
103
|
|
|
237
104
|
def create_sitemap_config
|
|
238
105
|
content = <<~RUBY
|
|
239
106
|
# frozen_string_literal: true
|
|
240
107
|
|
|
241
108
|
require 'sitemap_generator'
|
|
242
|
-
|
|
109
|
+
require 'pathname'
|
|
243
110
|
|
|
244
111
|
# Configure sitemap generator
|
|
245
112
|
# Update default_host to your actual domain
|
|
@@ -247,222 +114,39 @@ module StaticSiteBuilder
|
|
|
247
114
|
SitemapGenerator::Sitemap.sitemaps_path = 'sitemaps'
|
|
248
115
|
SitemapGenerator::Sitemap.public_path = 'dist'
|
|
249
116
|
|
|
250
|
-
# Generate sitemap from
|
|
117
|
+
# Generate sitemap from templates in app/views (excluding layouts and partials)
|
|
251
118
|
SitemapGenerator::Sitemap.create do
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
RUBY
|
|
271
|
-
|
|
272
|
-
write_file("config/importmap.rb", content)
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def stimulus_pin
|
|
276
|
-
<<~RUBY
|
|
277
|
-
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
|
|
278
|
-
pin_all_from "app/javascript/controllers", under: "controllers"
|
|
279
|
-
RUBY
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
# Detect which package manager is available
|
|
284
|
-
def detect_package_manager
|
|
285
|
-
# Check in order: yarn, pnpm, bun, npm
|
|
286
|
-
return "yarn" if system("which yarn > /dev/null 2>&1")
|
|
287
|
-
return "pnpm" if system("which pnpm > /dev/null 2>&1")
|
|
288
|
-
return "bun" if system("which bun > /dev/null 2>&1")
|
|
289
|
-
return "npm" if system("which npm > /dev/null 2>&1")
|
|
290
|
-
nil
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def create_tailwind_config
|
|
294
|
-
content = <<~JS
|
|
295
|
-
/** @type {import('tailwindcss').Config} */
|
|
296
|
-
module.exports = {
|
|
297
|
-
content: [
|
|
298
|
-
"./app/views/**/*.{html,erb,phlex}",
|
|
299
|
-
"./app/javascript/**/*.js",
|
|
300
|
-
],
|
|
301
|
-
theme: {
|
|
302
|
-
extend: {},
|
|
303
|
-
},
|
|
304
|
-
plugins: [],
|
|
305
|
-
}
|
|
306
|
-
JS
|
|
307
|
-
|
|
308
|
-
write_file("tailwind.config.js", content)
|
|
309
|
-
|
|
310
|
-
# Create PostCSS config
|
|
311
|
-
postcss_content = <<~JS
|
|
312
|
-
module.exports = {
|
|
313
|
-
plugins: {
|
|
314
|
-
tailwindcss: {},
|
|
315
|
-
autoprefixer: {},
|
|
316
|
-
},
|
|
317
|
-
}
|
|
318
|
-
JS
|
|
319
|
-
|
|
320
|
-
write_file("postcss.config.js", postcss_content)
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
def create_esbuild_config
|
|
324
|
-
content = <<~JS
|
|
325
|
-
require('esbuild').build({
|
|
326
|
-
entryPoints: ['app/javascript/application.js'],
|
|
327
|
-
bundle: true,
|
|
328
|
-
outdir: 'dist/assets/javascripts',
|
|
329
|
-
format: 'esm',
|
|
330
|
-
minify: true,
|
|
331
|
-
}).catch(() => process.exit(1))
|
|
332
|
-
JS
|
|
333
|
-
|
|
334
|
-
write_file("esbuild.config.js", content)
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def create_webpack_config
|
|
338
|
-
content = <<~JS
|
|
339
|
-
const path = require('path');
|
|
340
|
-
|
|
341
|
-
module.exports = {
|
|
342
|
-
entry: './app/javascript/application.js',
|
|
343
|
-
output: {
|
|
344
|
-
filename: 'application.js',
|
|
345
|
-
path: path.resolve(__dirname, 'dist/assets/javascripts'),
|
|
346
|
-
},
|
|
347
|
-
mode: 'production',
|
|
348
|
-
};
|
|
349
|
-
JS
|
|
350
|
-
|
|
351
|
-
write_file("webpack.config.js", content)
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def create_vite_config
|
|
355
|
-
content = <<~JS
|
|
356
|
-
import { defineConfig } from 'vite';
|
|
357
|
-
import RubyPlugin from 'vite-plugin-ruby';
|
|
358
|
-
|
|
359
|
-
export default defineConfig({
|
|
360
|
-
plugins: [RubyPlugin()],
|
|
361
|
-
});
|
|
362
|
-
JS
|
|
363
|
-
|
|
364
|
-
write_file("vite.config.js", content)
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
def create_rails_config
|
|
368
|
-
create_boot_config
|
|
369
|
-
create_routes_config
|
|
370
|
-
create_application_config
|
|
371
|
-
create_environment_file
|
|
372
|
-
create_environment_configs
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
def create_boot_config
|
|
376
|
-
content = <<~RUBY
|
|
377
|
-
# frozen_string_literal: true
|
|
378
|
-
|
|
379
|
-
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
|
380
|
-
|
|
381
|
-
require "bundler/setup" # Set up gems listed in the Gemfile.
|
|
382
|
-
RUBY
|
|
383
|
-
|
|
384
|
-
write_file("config/boot.rb", content)
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
def create_routes_config
|
|
388
|
-
content = <<~RUBY
|
|
389
|
-
# frozen_string_literal: true
|
|
390
|
-
|
|
391
|
-
Rails.application.routes.draw do
|
|
392
|
-
mount EditRails::Engine => "/edit_rails", as: "edit_rails"
|
|
393
|
-
end
|
|
394
|
-
RUBY
|
|
395
|
-
|
|
396
|
-
write_file("config/routes.rb", content)
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
def create_application_config
|
|
400
|
-
content = <<~RUBY
|
|
401
|
-
# frozen_string_literal: true
|
|
402
|
-
|
|
403
|
-
require_relative "boot"
|
|
404
|
-
|
|
405
|
-
require "rails/all"
|
|
406
|
-
|
|
407
|
-
Bundler.require(*Rails.groups)
|
|
408
|
-
|
|
409
|
-
module StaticSite
|
|
410
|
-
class Application < Rails::Application
|
|
411
|
-
config.load_defaults Rails::VERSION::STRING.to_f
|
|
412
|
-
|
|
413
|
-
# Configuration for the application's routes
|
|
414
|
-
config.api_only = false
|
|
415
|
-
|
|
416
|
-
# Don't generate system test files
|
|
417
|
-
config.generators.system_tests = nil
|
|
418
|
-
|
|
419
|
-
# Serve static files from public directory
|
|
420
|
-
config.public_file_server.enabled = true
|
|
119
|
+
views_dir = Pathname.new('app/views')
|
|
120
|
+
if views_dir.exist?
|
|
121
|
+
Dir.glob(views_dir.join('**', '*.html.erb')).each do |erb_file|
|
|
122
|
+
relative_path = Pathname.new(erb_file).relative_path_from(views_dir)
|
|
123
|
+
file_name = Pathname.new(erb_file).basename.to_s
|
|
124
|
+
|
|
125
|
+
if file_name.start_with?('_') == false && relative_path.to_s.start_with?('layouts/') == false
|
|
126
|
+
page_name = relative_path.to_s.gsub(/\.html\.erb$/, '')
|
|
127
|
+
|
|
128
|
+
# Convert page name to URL path
|
|
129
|
+
# Handle index pages: 'index' -> '/', 'blog/index' -> '/blog/'
|
|
130
|
+
path = if page_name == 'index'
|
|
131
|
+
'/'
|
|
132
|
+
elsif page_name.end_with?('/index')
|
|
133
|
+
"/\#{page_name[0..-7]}/"
|
|
134
|
+
else
|
|
135
|
+
"/\#{page_name}"
|
|
136
|
+
end
|
|
421
137
|
|
|
422
|
-
|
|
423
|
-
|
|
138
|
+
add path, lastmod: File.mtime(erb_file), changefreq: 'weekly', priority: 0.5
|
|
139
|
+
end
|
|
140
|
+
end
|
|
424
141
|
end
|
|
425
142
|
end
|
|
426
143
|
RUBY
|
|
427
144
|
|
|
428
|
-
write_file(
|
|
145
|
+
write_file('config/sitemap.rb', content)
|
|
429
146
|
end
|
|
430
147
|
|
|
431
|
-
def create_environment_file
|
|
432
|
-
content = <<~RUBY
|
|
433
|
-
# frozen_string_literal: true
|
|
434
148
|
|
|
435
|
-
# Load the Rails application.
|
|
436
|
-
require_relative "application"
|
|
437
149
|
|
|
438
|
-
# Initialize the Rails application.
|
|
439
|
-
Rails.application.initialize!
|
|
440
|
-
RUBY
|
|
441
|
-
|
|
442
|
-
write_file("config/environment.rb", content)
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
def create_environment_configs
|
|
446
|
-
# Create development environment config
|
|
447
|
-
dev_content = <<~RUBY
|
|
448
|
-
# frozen_string_literal: true
|
|
449
|
-
|
|
450
|
-
require "active_support/core_ext/integer/time"
|
|
451
|
-
|
|
452
|
-
Rails.application.configure do
|
|
453
|
-
config.cache_classes = false
|
|
454
|
-
config.eager_load = false
|
|
455
|
-
config.consider_all_requests_local = true
|
|
456
|
-
config.server_timing = true
|
|
457
|
-
config.public_file_server.enabled = true
|
|
458
|
-
config.public_file_server.headers = {
|
|
459
|
-
"Cache-Control" => "public, max-age=#{1.hour.to_i}"
|
|
460
|
-
}
|
|
461
|
-
end
|
|
462
|
-
RUBY
|
|
463
|
-
|
|
464
|
-
write_file("config/environments/development.rb", dev_content)
|
|
465
|
-
end
|
|
466
150
|
|
|
467
151
|
def create_app_structure
|
|
468
152
|
create_layout
|
|
@@ -471,11 +155,7 @@ module StaticSiteBuilder
|
|
|
471
155
|
end
|
|
472
156
|
|
|
473
157
|
def create_layout
|
|
474
|
-
|
|
475
|
-
create_phlex_layout
|
|
476
|
-
else
|
|
477
|
-
create_erb_layout
|
|
478
|
-
end
|
|
158
|
+
create_erb_layout
|
|
479
159
|
end
|
|
480
160
|
|
|
481
161
|
def create_erb_layout
|
|
@@ -485,529 +165,677 @@ module StaticSiteBuilder
|
|
|
485
165
|
<head>
|
|
486
166
|
<meta charset="UTF-8">
|
|
487
167
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
488
|
-
<title><%= @title || 'Site' %></title>
|
|
489
168
|
<link rel="stylesheet" href="/assets/stylesheets/application.css">
|
|
490
169
|
</head>
|
|
491
170
|
<body>
|
|
492
171
|
<main>
|
|
493
|
-
<%=
|
|
172
|
+
<%= yield %>
|
|
494
173
|
</main>
|
|
495
174
|
|
|
496
|
-
|
|
497
|
-
|
|
175
|
+
<% if content_for?(:javascript) %>
|
|
176
|
+
<%= yield(:javascript) %>
|
|
177
|
+
<% end %>
|
|
178
|
+
|
|
179
|
+
<% if ENV['LIVE_RELOAD'] == 'true' %>
|
|
180
|
+
<script>
|
|
181
|
+
(function() {
|
|
182
|
+
function connect() {
|
|
183
|
+
var port = '<%= ENV['WS_PORT'] || #{DEFAULT_WS_PORT} %>';
|
|
184
|
+
var ws = new WebSocket('ws://localhost:' + port);
|
|
185
|
+
ws.onmessage = function(e) {
|
|
186
|
+
if (e.data === 'reload') window.location.reload();
|
|
187
|
+
};
|
|
188
|
+
ws.onclose = function() { setTimeout(connect, 500); };
|
|
189
|
+
ws.onerror = function() {};
|
|
190
|
+
}
|
|
191
|
+
connect();
|
|
192
|
+
})();
|
|
193
|
+
</script>
|
|
194
|
+
<% end %>
|
|
498
195
|
</body>
|
|
499
196
|
</html>
|
|
500
197
|
ERB
|
|
501
198
|
|
|
502
|
-
write_file(
|
|
199
|
+
write_file('app/views/layouts/application.html.erb', content)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def create_javascript_entry
|
|
203
|
+
content = <<~JS
|
|
204
|
+
// Your JavaScript code here
|
|
205
|
+
console.log("Application loaded")
|
|
206
|
+
JS
|
|
207
|
+
|
|
208
|
+
write_file('app/javascript/application.js', content)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def create_css_entry
|
|
212
|
+
write_file('app/assets/stylesheets/application.css', <<~CSS)
|
|
213
|
+
/* Your custom CSS here */
|
|
214
|
+
body {
|
|
215
|
+
font-family: system-ui, sans-serif;
|
|
216
|
+
}
|
|
217
|
+
CSS
|
|
503
218
|
end
|
|
504
219
|
|
|
505
|
-
def
|
|
220
|
+
def create_build_files
|
|
221
|
+
create_rakefile
|
|
222
|
+
create_site_builder
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def create_rakefile
|
|
506
226
|
content = <<~RUBY
|
|
507
227
|
# frozen_string_literal: true
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
228
|
+
# encoding: utf-8
|
|
229
|
+
|
|
230
|
+
# Set default external encoding to UTF-8
|
|
231
|
+
Encoding.default_external = Encoding::UTF_8
|
|
232
|
+
|
|
233
|
+
require_relative 'lib/site_builder'
|
|
234
|
+
require 'fileutils'
|
|
235
|
+
require 'pathname'
|
|
236
|
+
require 'json'
|
|
237
|
+
|
|
238
|
+
namespace :build do
|
|
239
|
+
desc 'Build everything (assets + HTML + CSS + sitemap)'
|
|
240
|
+
task :all do
|
|
241
|
+
Rake::Task['build:clean'].invoke
|
|
242
|
+
Rake::Task['build:assets'].invoke
|
|
243
|
+
Rake::Task['build:html'].invoke
|
|
244
|
+
Rake::Task['build:css'].invoke
|
|
245
|
+
Rake::Task['build:sitemap'].invoke
|
|
246
|
+
|
|
247
|
+
puts "\\n✓ Build complete!"
|
|
513
248
|
end
|
|
514
249
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
250
|
+
desc 'Build JavaScript assets'
|
|
251
|
+
task :assets do
|
|
252
|
+
package_json_path = Pathname.new(Dir.pwd).join('package.json')
|
|
253
|
+
if package_json_path.exist?
|
|
254
|
+
package_json = JSON.parse(File.read(package_json_path))
|
|
255
|
+
build_script = package_json.dig('scripts', 'build')
|
|
256
|
+
|
|
257
|
+
if build_script && !build_script.include?('build:css') && build_script != "echo 'No JS bundling needed'"
|
|
258
|
+
sh 'npm run build'
|
|
522
259
|
end
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
260
|
+
else
|
|
261
|
+
js_dir = Pathname.new(Dir.pwd).join('app', 'javascript')
|
|
262
|
+
if js_dir.exist?
|
|
263
|
+
dist_js = Pathname.new(Dir.pwd).join('dist', 'assets', 'javascripts')
|
|
264
|
+
FileUtils.mkdir_p(dist_js)
|
|
265
|
+
Dir.glob(js_dir.join('**', '*')).each do |item|
|
|
266
|
+
if File.file?(item)
|
|
267
|
+
dest = dist_js.join(Pathname.new(item).relative_path_from(js_dir))
|
|
268
|
+
FileUtils.mkdir_p(dest.dirname)
|
|
269
|
+
FileUtils.cp(item, dest)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
527
272
|
end
|
|
528
273
|
end
|
|
529
274
|
end
|
|
530
|
-
end
|
|
531
|
-
RUBY
|
|
532
275
|
|
|
533
|
-
|
|
534
|
-
|
|
276
|
+
desc 'Compile all pages to static HTML'
|
|
277
|
+
task :html => [:assets] do
|
|
278
|
+
builder = SiteBuilder::Builder.new(root: Dir.pwd)
|
|
279
|
+
builder.build
|
|
280
|
+
end
|
|
535
281
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
</script>
|
|
541
|
-
ERB
|
|
542
|
-
end
|
|
282
|
+
desc 'Build CSS (runs after HTML so dist directory exists)'
|
|
283
|
+
task :css do
|
|
284
|
+
package_json_path = Pathname.new(Dir.pwd).join('package.json')
|
|
285
|
+
dist_css = Pathname.new(Dir.pwd).join('dist', 'assets', 'stylesheets')
|
|
543
286
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
287
|
+
if package_json_path.exist?
|
|
288
|
+
package_json = JSON.parse(File.read(package_json_path))
|
|
289
|
+
if package_json.dig('scripts', 'build:css')
|
|
290
|
+
sh 'npm run build:css'
|
|
291
|
+
end
|
|
292
|
+
elsif File.exist?('tailwind.config.js')
|
|
293
|
+
if system('which tailwindcss > /dev/null 2>&1')
|
|
294
|
+
FileUtils.mkdir_p(dist_css)
|
|
295
|
+
sh 'tailwindcss -i ./app/assets/stylesheets/application.css -o ./dist/assets/stylesheets/application.css --minify'
|
|
296
|
+
end
|
|
297
|
+
else
|
|
298
|
+
css_dir = Pathname.new(Dir.pwd).join('app', 'assets', 'stylesheets')
|
|
299
|
+
if css_dir.exist?
|
|
300
|
+
FileUtils.mkdir_p(dist_css)
|
|
301
|
+
Dir.glob(css_dir.join('**', '*')).each do |item|
|
|
302
|
+
if File.file?(item)
|
|
303
|
+
dest = dist_css.join(Pathname.new(item).relative_path_from(css_dir))
|
|
304
|
+
FileUtils.mkdir_p(dest.dirname)
|
|
305
|
+
FileUtils.cp(item, dest)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
562
311
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
when "vue"
|
|
570
|
-
create_vue_entry
|
|
571
|
-
when "alpine"
|
|
572
|
-
create_alpine_entry
|
|
573
|
-
else
|
|
574
|
-
create_vanilla_entry
|
|
575
|
-
end
|
|
576
|
-
end
|
|
312
|
+
desc 'Clean dist directory'
|
|
313
|
+
task :clean do
|
|
314
|
+
dist_dir = Pathname.new(Dir.pwd).join('dist')
|
|
315
|
+
FileUtils.rm_rf(dist_dir) if dist_dir.exist?
|
|
316
|
+
puts "Cleaned \#{dist_dir}"
|
|
317
|
+
end
|
|
577
318
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
319
|
+
desc 'Generate sitemap from actual pages'
|
|
320
|
+
task :sitemap do
|
|
321
|
+
require './config/sitemap'
|
|
322
|
+
end
|
|
323
|
+
end
|
|
581
324
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
325
|
+
namespace :dev do
|
|
326
|
+
desc 'Start development server with auto-rebuild and live reload'
|
|
327
|
+
task :server do
|
|
328
|
+
require 'webrick'
|
|
329
|
+
|
|
330
|
+
port = ENV['PORT'] || #{DEFAULT_PORT}
|
|
331
|
+
ws_port = ENV['WS_PORT'] || #{DEFAULT_WS_PORT}
|
|
332
|
+
dist_dir = Pathname.new(Dir.pwd).join('dist')
|
|
333
|
+
reload_file = Pathname.new(Dir.pwd).join('.reload')
|
|
334
|
+
|
|
335
|
+
ws_server = SiteBuilder::WebSocketServer.new(port: ws_port.to_i, reload_file: reload_file)
|
|
336
|
+
ws_server.start
|
|
337
|
+
|
|
338
|
+
ENV['LIVE_RELOAD'] = 'true'
|
|
339
|
+
ENV['WS_PORT'] = ws_port.to_s
|
|
340
|
+
|
|
341
|
+
Rake::Task['build:html'].invoke
|
|
342
|
+
Rake::Task['build:css'].invoke
|
|
343
|
+
|
|
344
|
+
puts "\\n🚀 Starting development server at http://localhost:\#{port}"
|
|
345
|
+
puts "📡 WebSocket server at ws://localhost:\#{ws_port}"
|
|
346
|
+
puts "📝 Watching for changes... (Ctrl+C to stop)"
|
|
347
|
+
puts "🔄 Live reload enabled - pages will auto-refresh on changes\\n"
|
|
348
|
+
|
|
349
|
+
watcher_code = %q{
|
|
350
|
+
watched = ['app', 'config']
|
|
351
|
+
exts = ['.erb', '.rb', '.js', '.css']
|
|
352
|
+
mtimes = {}
|
|
353
|
+
last_build = Time.now
|
|
354
|
+
|
|
355
|
+
loop do
|
|
356
|
+
changed = false
|
|
357
|
+
|
|
358
|
+
watched.each do |dir|
|
|
359
|
+
Dir.glob(File.join(dir, '**', '*')).each do |f|
|
|
360
|
+
if File.file?(f) && exts.any? { |e| f.end_with?(e) }
|
|
361
|
+
mtime = File.mtime(f)
|
|
362
|
+
if mtimes[f] != mtime
|
|
363
|
+
mtimes[f] = mtime
|
|
364
|
+
changed = true
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
585
369
|
|
|
586
|
-
|
|
370
|
+
if changed && (Time.now - last_build) > 1
|
|
371
|
+
system('rake build:html > /dev/null 2>&1 && rake build:css > /dev/null 2>&1')
|
|
372
|
+
last_build = Time.now
|
|
373
|
+
end
|
|
587
374
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
JS
|
|
375
|
+
sleep 0.5
|
|
376
|
+
end
|
|
377
|
+
}
|
|
592
378
|
|
|
593
|
-
|
|
594
|
-
|
|
379
|
+
watcher_pid = spawn('ruby', '-e', watcher_code, err: File::NULL, out: File::NULL)
|
|
380
|
+
|
|
381
|
+
server = WEBrick::HTTPServer.new(
|
|
382
|
+
Port: port,
|
|
383
|
+
BindAddress: '127.0.0.1'
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
server.mount('/assets', WEBrick::HTTPServlet::FileHandler, File.join(dist_dir.to_s, 'assets'))
|
|
387
|
+
server.mount('/images', WEBrick::HTTPServlet::FileHandler, File.join(dist_dir.to_s, 'images'))
|
|
388
|
+
|
|
389
|
+
server.mount_proc('/') do |req, res|
|
|
390
|
+
path = req.path
|
|
391
|
+
|
|
392
|
+
if path == '/'
|
|
393
|
+
index_path = File.join(dist_dir.to_s, 'index.html')
|
|
394
|
+
if File.exist?(index_path)
|
|
395
|
+
res.status = 200
|
|
396
|
+
res['Content-Type'] = 'text/html'
|
|
397
|
+
res.body = File.read(index_path)
|
|
398
|
+
else
|
|
399
|
+
res.status = 404
|
|
400
|
+
res['Content-Type'] = 'text/plain'
|
|
401
|
+
res.body = "Not Found\\n"
|
|
402
|
+
end
|
|
403
|
+
else
|
|
404
|
+
clean_path = path.start_with?('/') ? path[1..] : path
|
|
405
|
+
served = false
|
|
406
|
+
|
|
407
|
+
if !path.include?('.') && !path.end_with?('/')
|
|
408
|
+
html_path = File.join(dist_dir.to_s, "\#{clean_path}.html")
|
|
409
|
+
if File.exist?(html_path)
|
|
410
|
+
res.status = 200
|
|
411
|
+
res['Content-Type'] = 'text/html'
|
|
412
|
+
res.body = File.read(html_path)
|
|
413
|
+
served = true
|
|
414
|
+
end
|
|
415
|
+
end
|
|
595
416
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
417
|
+
if served == false
|
|
418
|
+
file_path = File.join(dist_dir.to_s, clean_path)
|
|
419
|
+
|
|
420
|
+
if File.directory?(file_path)
|
|
421
|
+
nested_index = File.join(file_path, 'index.html')
|
|
422
|
+
if File.exist?(nested_index)
|
|
423
|
+
res.status = 200
|
|
424
|
+
res['Content-Type'] = 'text/html'
|
|
425
|
+
res.body = File.read(nested_index)
|
|
426
|
+
served = true
|
|
427
|
+
end
|
|
428
|
+
elsif File.exist?(file_path) && File.file?(file_path)
|
|
429
|
+
res.status = 200
|
|
430
|
+
res['Content-Type'] = WEBrick::HTTPUtils.mime_type(file_path, WEBrick::HTTPUtils::DefaultMimeTypes)
|
|
431
|
+
res.body = File.read(file_path)
|
|
432
|
+
served = true
|
|
433
|
+
end
|
|
434
|
+
end
|
|
612
435
|
|
|
613
|
-
|
|
614
|
-
|
|
436
|
+
if served == false
|
|
437
|
+
res.status = 404
|
|
438
|
+
res['Content-Type'] = 'text/plain'
|
|
439
|
+
res.body = "Not Found\\n"
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
615
443
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
444
|
+
trap('INT') do
|
|
445
|
+
puts "\\n\\nShutting down..."
|
|
446
|
+
Process.kill('TERM', watcher_pid) if watcher_pid
|
|
447
|
+
ws_server.stop
|
|
448
|
+
server.shutdown
|
|
449
|
+
end
|
|
619
450
|
|
|
620
|
-
|
|
621
|
-
|
|
451
|
+
server.start
|
|
452
|
+
end
|
|
453
|
+
end
|
|
622
454
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
app.mount('#app')
|
|
626
|
-
})
|
|
627
|
-
JS
|
|
455
|
+
task default: 'build:all'
|
|
456
|
+
RUBY
|
|
628
457
|
|
|
629
|
-
write_file(
|
|
458
|
+
write_file('Rakefile', content)
|
|
630
459
|
end
|
|
631
460
|
|
|
632
|
-
def create_alpine_entry
|
|
633
|
-
content = <<~JS
|
|
634
|
-
import Alpine from 'alpinejs'
|
|
635
461
|
|
|
636
|
-
window.Alpine = Alpine
|
|
637
|
-
Alpine.start()
|
|
638
|
-
JS
|
|
639
462
|
|
|
640
|
-
|
|
641
|
-
|
|
463
|
+
def create_site_builder
|
|
464
|
+
content = <<~RUBY
|
|
465
|
+
# frozen_string_literal: true
|
|
642
466
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
467
|
+
require 'action_view'
|
|
468
|
+
require 'action_view/helpers'
|
|
469
|
+
require 'fileutils'
|
|
470
|
+
require 'pathname'
|
|
471
|
+
require 'socket'
|
|
472
|
+
require 'base64'
|
|
473
|
+
require 'digest/sha1'
|
|
474
|
+
require 'json'
|
|
475
|
+
|
|
476
|
+
module SiteBuilder
|
|
477
|
+
DIST_DIR = 'dist'
|
|
478
|
+
DEFAULT_PORT = 3000
|
|
479
|
+
DEFAULT_WS_PORT = 3001
|
|
480
|
+
|
|
481
|
+
class Builder
|
|
482
|
+
def initialize(root: Dir.pwd)
|
|
483
|
+
@root = Pathname.new(root)
|
|
484
|
+
@dist_dir = @root.join(DIST_DIR)
|
|
485
|
+
@views_dir = @root.join('app', 'views')
|
|
486
|
+
end
|
|
648
487
|
|
|
649
|
-
|
|
650
|
-
|
|
488
|
+
def build
|
|
489
|
+
copy_javascript
|
|
490
|
+
copy_stylesheets
|
|
491
|
+
compile_views
|
|
492
|
+
copy_public
|
|
493
|
+
write_reload_file
|
|
494
|
+
end
|
|
651
495
|
|
|
652
|
-
|
|
653
|
-
case @options[:css_framework]
|
|
654
|
-
when "tailwindcss"
|
|
655
|
-
write_file("app/assets/stylesheets/application.css", <<~CSS)
|
|
656
|
-
@tailwind base;
|
|
657
|
-
@tailwind components;
|
|
658
|
-
@tailwind utilities;
|
|
659
|
-
|
|
660
|
-
@layer base {
|
|
661
|
-
html {
|
|
662
|
-
scroll-behavior: smooth;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
496
|
+
private
|
|
665
497
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
scroll-margin-top: 5rem;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
CSS
|
|
672
|
-
when "shadcn"
|
|
673
|
-
write_file("app/assets/stylesheets/application.css", <<~CSS)
|
|
674
|
-
@tailwind base;
|
|
675
|
-
@tailwind components;
|
|
676
|
-
@tailwind utilities;
|
|
677
|
-
|
|
678
|
-
@layer base {
|
|
679
|
-
:root {
|
|
680
|
-
--background: 0 0% 100%;
|
|
681
|
-
--foreground: 222.2 84% 4.9%;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
CSS
|
|
685
|
-
else
|
|
686
|
-
write_file("app/assets/stylesheets/application.css", <<~CSS)
|
|
687
|
-
/* Your custom CSS here */
|
|
688
|
-
body {
|
|
689
|
-
font-family: system-ui, sans-serif;
|
|
690
|
-
}
|
|
691
|
-
CSS
|
|
692
|
-
end
|
|
693
|
-
end
|
|
498
|
+
def compile_views
|
|
499
|
+
FileUtils.mkdir_p(@dist_dir)
|
|
694
500
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
end
|
|
501
|
+
if @views_dir.exist?
|
|
502
|
+
view = build_action_view
|
|
503
|
+
layout_virtual = 'layouts/application'
|
|
699
504
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
require "fileutils"
|
|
704
|
-
require "static_site_builder/websocket_server"
|
|
705
|
-
require "json"
|
|
706
|
-
|
|
707
|
-
port = ENV["PORT"] || 3000
|
|
708
|
-
ws_port = ENV["WS_PORT"] || 3001
|
|
709
|
-
dist_dir = Pathname.new(Dir.pwd).join("dist")
|
|
710
|
-
reload_file = Pathname.new(Dir.pwd).join(".reload")
|
|
711
|
-
|
|
712
|
-
# Start WebSocket server for live reload (before first build)
|
|
713
|
-
ws_server = StaticSiteBuilder::WebSocketServer.new(port: ws_port, reload_file: reload_file)
|
|
714
|
-
ws_server.start
|
|
715
|
-
|
|
716
|
-
# Build once before starting (with live reload enabled)
|
|
717
|
-
ENV["LIVE_RELOAD"] = "true"
|
|
718
|
-
ENV["WS_PORT"] = ws_port.to_s
|
|
719
|
-
Rake::Task["build:all"].invoke
|
|
720
|
-
|
|
721
|
-
# Check if we need to run Tailwind CSS watch (after initial build)
|
|
722
|
-
tailwind_pid = nil
|
|
723
|
-
package_json_path = Pathname.new(Dir.pwd).join("package.json")
|
|
724
|
-
if package_json_path.exist?
|
|
725
|
-
package_json = JSON.parse(File.read(package_json_path))
|
|
726
|
-
if package_json.dig("scripts", "watch:css")
|
|
727
|
-
puts "🎨 Starting Tailwind CSS watch mode..."
|
|
728
|
-
tailwind_pid = spawn("npm", "run", "watch:css", :err => File::NULL, :out => File::NULL)
|
|
729
|
-
# Touch the source file to trigger Tailwind watch to process CSS immediately
|
|
730
|
-
css_source = Pathname.new(Dir.pwd).join("app", "assets", "stylesheets", "application.css")
|
|
731
|
-
if css_source.exist?
|
|
732
|
-
FileUtils.touch(css_source)
|
|
733
|
-
end
|
|
734
|
-
# Give Tailwind a moment to process CSS
|
|
735
|
-
sleep 1.5
|
|
736
|
-
end
|
|
737
|
-
end
|
|
505
|
+
Dir.glob(@views_dir.join('**', '*.html.erb')).each do |erb_file|
|
|
506
|
+
relative = Pathname.new(erb_file).relative_path_from(@views_dir).to_s
|
|
507
|
+
file_name = Pathname.new(erb_file).basename.to_s
|
|
738
508
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
puts "📝 Watching for changes... (Ctrl+C to stop)"
|
|
742
|
-
puts "🔄 Live reload enabled - pages will auto-refresh on changes\n"
|
|
743
|
-
|
|
744
|
-
# Simple file watcher - rebuild HTML when non-CSS files change
|
|
745
|
-
# CSS changes are handled by Tailwind watch, so we skip rebuild for CSS files
|
|
746
|
-
# When HTML rebuilds, it cleans dist, so we need to rebuild CSS immediately after
|
|
747
|
-
watcher_code = %q{watched = ['app', 'config']; exts = ['.erb', '.rb', '.js']; 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) }; next if f.end_with?('.css'); mtime = File.mtime(f); if mtimes[f] != mtime; mtimes[f] = mtime; changed = true; end; end; end; if changed; system('rake build:html > /dev/null 2>&1 && rake build:css > /dev/null 2>&1'); end; sleep 0.5; end}
|
|
748
|
-
watcher_pid = spawn("ruby", "-e", watcher_code, :err => File::NULL)
|
|
749
|
-
|
|
750
|
-
# Start web server
|
|
751
|
-
server = WEBrick::HTTPServer.new(
|
|
752
|
-
Port: port,
|
|
753
|
-
DocumentRoot: dist_dir.to_s,
|
|
754
|
-
BindAddress: "127.0.0.1"
|
|
755
|
-
)
|
|
756
|
-
|
|
757
|
-
trap("INT") do
|
|
758
|
-
puts "\n\nShutting down..."
|
|
759
|
-
Process.kill("TERM", watcher_pid) if watcher_pid
|
|
760
|
-
Process.kill("TERM", tailwind_pid) if tailwind_pid
|
|
761
|
-
ws_server.stop
|
|
762
|
-
server.shutdown
|
|
763
|
-
end
|
|
509
|
+
is_partial = file_name.start_with?('_')
|
|
510
|
+
is_layout = relative.start_with?('layouts/')
|
|
764
511
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
512
|
+
if is_partial == false && is_layout == false
|
|
513
|
+
virtual_path = relative.gsub(/\.html\.erb$/, '')
|
|
514
|
+
output_rel = relative.gsub(/\.html\.erb$/, '.html')
|
|
515
|
+
output_path = @dist_dir.join(output_rel)
|
|
768
516
|
|
|
769
|
-
|
|
770
|
-
<<~'RUBY'
|
|
771
|
-
require "fileutils"
|
|
772
|
-
require "pathname"
|
|
517
|
+
rendered = view.render(template: virtual_path, layout: layout_virtual)
|
|
773
518
|
|
|
774
|
-
|
|
775
|
-
|
|
519
|
+
FileUtils.mkdir_p(output_path.dirname)
|
|
520
|
+
File.write(output_path, rendered)
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
776
525
|
|
|
777
|
-
|
|
526
|
+
def build_action_view
|
|
527
|
+
view_paths = ActionView::PathSet.new([@views_dir.to_s])
|
|
528
|
+
lookup_context = ActionView::LookupContext.new(view_paths)
|
|
529
|
+
view_class = ActionView::Base.with_empty_template_cache
|
|
778
530
|
|
|
779
|
-
|
|
780
|
-
ENV["LIVE_RELOAD"] = "true"
|
|
781
|
-
Rake::Task["build:all"].invoke
|
|
531
|
+
load_and_include_helpers(view_class)
|
|
782
532
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
533
|
+
view = view_class.new(lookup_context, {}, nil)
|
|
534
|
+
view
|
|
535
|
+
end
|
|
786
536
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
537
|
+
def load_and_include_helpers(view_class)
|
|
538
|
+
helpers_dir = @root.join('app', 'helpers')
|
|
539
|
+
if helpers_dir.exist? && helpers_dir.directory?
|
|
540
|
+
Dir.glob(helpers_dir.join('**', '*_helper.rb')).each do |helper_file|
|
|
541
|
+
load helper_file
|
|
791
542
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
has_edit_rails = @options[:edit_rails]
|
|
795
|
-
server_code = has_edit_rails ? rails_server_code : webrick_server_code
|
|
543
|
+
relative = Pathname.new(helper_file).relative_path_from(helpers_dir).to_s
|
|
544
|
+
module_name = helper_module_name(relative)
|
|
796
545
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
546
|
+
if Object.const_defined?(module_name)
|
|
547
|
+
view_class.include(Object.const_get(module_name))
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|
|
800
552
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
553
|
+
def helper_module_name(relative_path)
|
|
554
|
+
without_ext = relative_path.gsub(/\.rb$/, '')
|
|
555
|
+
parts = without_ext.split('/')
|
|
556
|
+
parts.map { |p| camelize(p) }.join('::')
|
|
557
|
+
end
|
|
804
558
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
task :all => [:html, :css, :sitemap] do
|
|
808
|
-
puts "\\n✓ Build complete!"
|
|
559
|
+
def camelize(value)
|
|
560
|
+
value.split('_').map { |part| part[0] ? part[0].upcase + part[1..] : '' }.join
|
|
809
561
|
end
|
|
810
562
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
563
|
+
def copy_javascript
|
|
564
|
+
package_json_path = @root.join('package.json')
|
|
565
|
+
should_copy = true
|
|
566
|
+
|
|
567
|
+
if package_json_path.exist?
|
|
568
|
+
begin
|
|
569
|
+
package_json = JSON.parse(File.read(package_json_path))
|
|
570
|
+
should_copy = package_json.dig('scripts', 'build').nil?
|
|
571
|
+
rescue JSON::ParserError
|
|
572
|
+
should_copy = true
|
|
819
573
|
end
|
|
820
574
|
end
|
|
821
|
-
end
|
|
822
575
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
576
|
+
if should_copy
|
|
577
|
+
js_dir = @root.join('app', 'javascript')
|
|
578
|
+
if js_dir.exist? && js_dir.directory?
|
|
579
|
+
dist_js = @dist_dir.join('assets', 'javascripts')
|
|
580
|
+
FileUtils.mkdir_p(dist_js)
|
|
581
|
+
copy_tree_skip_existing(js_dir, dist_js)
|
|
582
|
+
end
|
|
583
|
+
end
|
|
826
584
|
end
|
|
827
585
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
586
|
+
def copy_stylesheets
|
|
587
|
+
package_json_path = @root.join('package.json')
|
|
588
|
+
should_copy = true
|
|
589
|
+
|
|
590
|
+
if package_json_path.exist?
|
|
591
|
+
begin
|
|
592
|
+
package_json = JSON.parse(File.read(package_json_path))
|
|
593
|
+
should_copy = package_json.dig('scripts', 'build:css').nil?
|
|
594
|
+
rescue JSON::ParserError
|
|
595
|
+
should_copy = true
|
|
834
596
|
end
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
if should_copy
|
|
600
|
+
css_dir = @root.join('app', 'assets', 'stylesheets')
|
|
601
|
+
if css_dir.exist? && css_dir.directory?
|
|
602
|
+
dist_css = @dist_dir.join('assets', 'stylesheets')
|
|
603
|
+
FileUtils.mkdir_p(dist_css)
|
|
604
|
+
copy_tree_skip_existing(css_dir, dist_css)
|
|
840
605
|
end
|
|
841
606
|
end
|
|
842
607
|
end
|
|
843
608
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
609
|
+
def copy_public
|
|
610
|
+
public_dir = @root.join('public')
|
|
611
|
+
if public_dir.exist? && public_dir.directory?
|
|
612
|
+
Dir.glob(public_dir.join('**', '*')).each do |item|
|
|
613
|
+
if File.file?(item)
|
|
614
|
+
dest = @dist_dir.join(Pathname.new(item).relative_path_from(public_dir))
|
|
615
|
+
FileUtils.mkdir_p(dest.dirname)
|
|
616
|
+
FileUtils.cp(item, dest)
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
end
|
|
849
620
|
end
|
|
850
621
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
Rake::Task["build:all"].invoke
|
|
622
|
+
def write_reload_file
|
|
623
|
+
reload_file = @root.join('.reload')
|
|
624
|
+
File.write(reload_file, Time.now.to_f.to_s)
|
|
855
625
|
end
|
|
856
626
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
627
|
+
def copy_tree_skip_existing(source_dir, dest_dir)
|
|
628
|
+
Dir.glob(source_dir.join('**', '*')).each do |item|
|
|
629
|
+
if File.file?(item)
|
|
630
|
+
relative = Pathname.new(item).relative_path_from(source_dir)
|
|
631
|
+
dest = dest_dir.join(relative)
|
|
632
|
+
|
|
633
|
+
if dest.exist? == false
|
|
634
|
+
FileUtils.mkdir_p(dest.dirname)
|
|
635
|
+
FileUtils.cp(item, dest)
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
end
|
|
860
639
|
end
|
|
861
640
|
end
|
|
862
641
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
642
|
+
class WebSocketServer
|
|
643
|
+
ACCEPT_RETRY_INTERVAL = 0.1
|
|
644
|
+
WATCH_POLL_INTERVAL = 0.3
|
|
645
|
+
CLIENT_KEEPALIVE_INTERVAL = 1
|
|
646
|
+
|
|
647
|
+
def initialize(port: DEFAULT_WS_PORT, reload_file: nil)
|
|
648
|
+
@port = port
|
|
649
|
+
@reload_file = reload_file || Pathname.new(Dir.pwd).join('.reload')
|
|
650
|
+
@clients = []
|
|
651
|
+
@running = false
|
|
652
|
+
@server = nil
|
|
867
653
|
end
|
|
868
|
-
end
|
|
869
654
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
content = <<~RUBY
|
|
874
|
-
# frozen_string_literal: true
|
|
655
|
+
def start
|
|
656
|
+
@running = true
|
|
657
|
+
@server = TCPServer.new('127.0.0.1', @port)
|
|
875
658
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
659
|
+
if @reload_file.exist?
|
|
660
|
+
@last_mtime = @reload_file.mtime
|
|
661
|
+
else
|
|
662
|
+
File.write(@reload_file, Time.now.to_f.to_s)
|
|
663
|
+
@last_mtime = @reload_file.mtime
|
|
664
|
+
end
|
|
879
665
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
666
|
+
@accept_thread = Thread.new do
|
|
667
|
+
while @running
|
|
668
|
+
begin
|
|
669
|
+
client = @server.accept
|
|
670
|
+
Thread.new { handle_client(client) }
|
|
671
|
+
rescue IOError, Errno::EBADF, Errno::ECONNABORTED
|
|
672
|
+
sleep ACCEPT_RETRY_INTERVAL
|
|
673
|
+
if @running == false
|
|
674
|
+
break
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
end
|
|
885
679
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
680
|
+
@watch_thread = Thread.new do
|
|
681
|
+
while @running
|
|
682
|
+
begin
|
|
683
|
+
sleep WATCH_POLL_INTERVAL
|
|
684
|
+
if @reload_file.exist? && @reload_file.mtime > @last_mtime
|
|
685
|
+
@last_mtime = @reload_file.mtime
|
|
686
|
+
broadcast('reload')
|
|
687
|
+
end
|
|
688
|
+
rescue Errno::ENOENT, Errno::EACCES, SystemCallError
|
|
689
|
+
sleep WATCH_POLL_INTERVAL
|
|
690
|
+
if @running == false
|
|
691
|
+
break
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
end
|
|
889
696
|
end
|
|
890
697
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
dist_dir = Pathname.new(Dir.pwd).join("dist")
|
|
894
|
-
FileUtils.rm_rf(dist_dir) if dist_dir.exist?
|
|
895
|
-
puts "Cleaned \#{dist_dir}"
|
|
896
|
-
end
|
|
698
|
+
def stop
|
|
699
|
+
@running = false
|
|
897
700
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
Rake::Task["build:all"].invoke
|
|
902
|
-
end
|
|
701
|
+
@clients.each do |client|
|
|
702
|
+
safe_close(client)
|
|
703
|
+
end
|
|
903
704
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
end
|
|
908
|
-
end
|
|
705
|
+
if @server
|
|
706
|
+
safe_close(@server)
|
|
707
|
+
end
|
|
909
708
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
task :server do
|
|
913
|
-
#{server_code}
|
|
709
|
+
safe_kill_thread(@accept_thread)
|
|
710
|
+
safe_kill_thread(@watch_thread)
|
|
914
711
|
end
|
|
915
|
-
end
|
|
916
712
|
|
|
917
|
-
|
|
918
|
-
RUBY
|
|
919
|
-
end
|
|
713
|
+
private
|
|
920
714
|
|
|
921
|
-
|
|
922
|
-
|
|
715
|
+
def safe_close(io)
|
|
716
|
+
if io && io.closed? == false
|
|
717
|
+
begin
|
|
718
|
+
io.close
|
|
719
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::EBADF
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
end
|
|
923
723
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
724
|
+
def safe_kill_thread(thread)
|
|
725
|
+
if thread && thread.alive?
|
|
726
|
+
begin
|
|
727
|
+
thread.kill
|
|
728
|
+
rescue ThreadError
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
end
|
|
930
732
|
|
|
931
|
-
|
|
932
|
-
|
|
733
|
+
def handle_client(client)
|
|
734
|
+
begin
|
|
735
|
+
_request = client.gets
|
|
736
|
+
headers = {}
|
|
737
|
+
line = nil
|
|
738
|
+
|
|
739
|
+
while (line = client.gets)
|
|
740
|
+
stripped = line.chomp
|
|
741
|
+
if stripped == ''
|
|
742
|
+
break
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
key, value = stripped.split(': ', 2)
|
|
746
|
+
if key && value
|
|
747
|
+
headers[key] = value
|
|
748
|
+
end
|
|
749
|
+
end
|
|
933
750
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
751
|
+
if headers['Upgrade']&.downcase == 'websocket'
|
|
752
|
+
key = headers['Sec-WebSocket-Key']
|
|
753
|
+
accept = Base64.strict_encode64(Digest::SHA1.digest(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))
|
|
754
|
+
|
|
755
|
+
client.print "HTTP/1.1 101 Switching Protocols\r\n"
|
|
756
|
+
client.print "Upgrade: websocket\r\n"
|
|
757
|
+
client.print "Connection: Upgrade\r\n"
|
|
758
|
+
client.print "Sec-WebSocket-Accept: \#{accept}\r\n\r\n"
|
|
759
|
+
|
|
760
|
+
@clients << client
|
|
761
|
+
|
|
762
|
+
loop do
|
|
763
|
+
sleep CLIENT_KEEPALIVE_INTERVAL
|
|
764
|
+
if @running == false || client.closed?
|
|
765
|
+
break
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
else
|
|
769
|
+
safe_close(client)
|
|
770
|
+
end
|
|
771
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
|
|
772
|
+
@clients.delete(client)
|
|
773
|
+
safe_close(client)
|
|
774
|
+
end
|
|
775
|
+
end
|
|
937
776
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
777
|
+
def broadcast(message)
|
|
778
|
+
frame = create_frame(message)
|
|
779
|
+
@clients.dup.each do |client|
|
|
780
|
+
begin
|
|
781
|
+
if client.closed? == false
|
|
782
|
+
client.write(frame)
|
|
783
|
+
end
|
|
784
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
|
|
785
|
+
@clients.delete(client)
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
end
|
|
945
789
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
790
|
+
def create_frame(message)
|
|
791
|
+
data = message.dup.force_encoding('BINARY')
|
|
792
|
+
length = data.bytesize
|
|
949
793
|
|
|
950
|
-
|
|
951
|
-
|
|
794
|
+
if length < 126
|
|
795
|
+
[0x81, length].pack('C*') + data
|
|
796
|
+
elsif length < 65_536
|
|
797
|
+
[0x81, 126, length].pack('CCn') + data
|
|
798
|
+
else
|
|
799
|
+
[0x81, 127, length >> 32, length & 0xFFFFFFFF].pack('CCNN') + data
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
end
|
|
952
804
|
|
|
953
|
-
|
|
954
|
-
<<~RUBY
|
|
955
|
-
importmap_config: "config/importmap.rb",
|
|
805
|
+
# This file provides the build and live reload code for the generated project.
|
|
956
806
|
RUBY
|
|
807
|
+
|
|
808
|
+
write_file('lib/site_builder.rb', content)
|
|
957
809
|
end
|
|
958
810
|
|
|
959
811
|
def create_example_pages
|
|
960
|
-
|
|
961
|
-
create_phlex_example
|
|
962
|
-
else
|
|
963
|
-
create_erb_example
|
|
964
|
-
end
|
|
812
|
+
create_erb_example
|
|
965
813
|
end
|
|
966
814
|
|
|
967
815
|
def create_erb_example
|
|
968
816
|
content = <<~ERB
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
---
|
|
817
|
+
<% content_for(:javascript) do %>
|
|
818
|
+
<script src="/assets/javascripts/application.js"></script>
|
|
819
|
+
<% end %>
|
|
973
820
|
|
|
974
821
|
<h1>Welcome</h1>
|
|
975
822
|
<p>This is your generated static site.</p>
|
|
976
823
|
ERB
|
|
977
824
|
|
|
978
|
-
write_file(
|
|
825
|
+
write_file('app/views/index.html.erb', content)
|
|
979
826
|
end
|
|
980
827
|
|
|
981
|
-
def create_phlex_example
|
|
982
|
-
content = <<~RUBY
|
|
983
|
-
# frozen_string_literal: true
|
|
984
|
-
|
|
985
|
-
class IndexPage < Phlex::HTML
|
|
986
|
-
def template
|
|
987
|
-
h1 { "Welcome" }
|
|
988
|
-
p { "This is your generated static site." }
|
|
989
|
-
end
|
|
990
|
-
end
|
|
991
|
-
RUBY
|
|
992
|
-
|
|
993
|
-
write_file("app/views/pages/index.rb", content)
|
|
994
|
-
end
|
|
995
828
|
|
|
996
829
|
def create_readme
|
|
997
830
|
content = <<~MD
|
|
998
831
|
# #{@app_name}
|
|
999
832
|
|
|
1000
|
-
Generated static site using
|
|
1001
|
-
- Template: #{@options[:template_engine]}
|
|
1002
|
-
- JS Bundler: #{@options[:js_bundler]}
|
|
1003
|
-
- CSS: #{@options[:css_framework]}
|
|
1004
|
-
- JS Framework: #{@options[:js_framework]}
|
|
833
|
+
Generated static site using ERB templates.
|
|
1005
834
|
|
|
1006
835
|
## Setup
|
|
1007
836
|
|
|
1008
837
|
```bash
|
|
1009
838
|
bundle install
|
|
1010
|
-
#{'npm install' if needs_npm?}
|
|
1011
839
|
```
|
|
1012
840
|
|
|
1013
841
|
## Development
|
|
@@ -1028,69 +856,32 @@ module StaticSiteBuilder
|
|
|
1028
856
|
|
|
1029
857
|
## Build
|
|
1030
858
|
|
|
1031
|
-
Build for
|
|
859
|
+
Build for deployment:
|
|
1032
860
|
|
|
1033
861
|
```bash
|
|
1034
|
-
rake build:all #
|
|
1035
|
-
rake build:html # Build HTML only
|
|
862
|
+
rake build:all # Clean dist and build everything (assets + HTML + CSS + sitemap)
|
|
863
|
+
rake build:html # Build HTML only (still runs build:assets)
|
|
864
|
+
rake build:css # Build CSS only
|
|
865
|
+
rake build:sitemap # Build sitemap only
|
|
1036
866
|
```
|
|
1037
867
|
|
|
1038
868
|
Output goes to `dist/` directory.
|
|
1039
|
-
MD
|
|
1040
|
-
|
|
1041
|
-
write_file("README.md", content)
|
|
1042
|
-
end
|
|
1043
869
|
|
|
1044
|
-
|
|
1045
|
-
js_build = case @options[:js_bundler]
|
|
1046
|
-
when "esbuild"
|
|
1047
|
-
"node esbuild.config.js"
|
|
1048
|
-
when "webpack"
|
|
1049
|
-
"webpack --mode production"
|
|
1050
|
-
when "vite"
|
|
1051
|
-
"vite build"
|
|
1052
|
-
else
|
|
1053
|
-
nil
|
|
1054
|
-
end
|
|
870
|
+
## JavaScript Bundling
|
|
1055
871
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
else
|
|
1059
|
-
nil
|
|
1060
|
-
end
|
|
872
|
+
If you add a `package.json` with a `scripts.build`, `rake build:assets` will run `npm run build`.
|
|
873
|
+
If you do not use a bundler, it will copy files from `app/javascript/` into `dist/assets/javascripts/`.
|
|
1061
874
|
|
|
1062
|
-
|
|
1063
|
-
if builds.empty?
|
|
1064
|
-
"echo 'No bundling needed'"
|
|
1065
|
-
else
|
|
1066
|
-
builds.join(" && ")
|
|
1067
|
-
end
|
|
1068
|
-
end
|
|
875
|
+
## CSS
|
|
1069
876
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
end
|
|
1074
|
-
end
|
|
877
|
+
If you add a `package.json` with a `scripts.build:css`, `rake build:css` will run `npm run build:css`.
|
|
878
|
+
If you do not use a CSS build step, it will copy files from `app/assets/stylesheets/` into `dist/assets/stylesheets/`.
|
|
879
|
+
MD
|
|
1075
880
|
|
|
1076
|
-
|
|
1077
|
-
if @options[:css_framework] == "tailwindcss" || @options[:css_framework] == "shadcn"
|
|
1078
|
-
"tailwindcss -i ./app/assets/stylesheets/application.css -o ./dist/assets/stylesheets/application.css --watch"
|
|
1079
|
-
end
|
|
881
|
+
write_file('README.md', content)
|
|
1080
882
|
end
|
|
1081
883
|
|
|
1082
|
-
def needs_npm?
|
|
1083
|
-
@options[:js_bundler] != "none" ||
|
|
1084
|
-
@options[:css_framework] == "tailwindcss" ||
|
|
1085
|
-
@options[:css_framework] == "shadcn" ||
|
|
1086
|
-
@options[:js_framework] == "react" ||
|
|
1087
|
-
@options[:js_framework] == "vue" ||
|
|
1088
|
-
@options[:js_framework] == "alpine"
|
|
1089
|
-
end
|
|
1090
884
|
|
|
1091
|
-
def needs_css_build?
|
|
1092
|
-
@options[:css_framework] == "tailwindcss" || @options[:css_framework] == "shadcn"
|
|
1093
|
-
end
|
|
1094
885
|
|
|
1095
886
|
def create_gitignore
|
|
1096
887
|
content = <<~GITIGNORE
|