salvia_rb 0.1.5
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/LICENSE.txt +22 -0
- data/README.md +235 -0
- data/assets/javascripts/islands.js +26 -0
- data/assets/scripts/build_ssr.ts +261 -0
- data/exe/salvia +7 -0
- data/lib/salvia_rb/application.rb +431 -0
- data/lib/salvia_rb/assets.rb +74 -0
- data/lib/salvia_rb/assets_middleware.rb +46 -0
- data/lib/salvia_rb/cli.rb +1398 -0
- data/lib/salvia_rb/component.rb +74 -0
- data/lib/salvia_rb/controller.rb +277 -0
- data/lib/salvia_rb/csrf.rb +130 -0
- data/lib/salvia_rb/database.rb +176 -0
- data/lib/salvia_rb/flash.rb +43 -0
- data/lib/salvia_rb/helpers/component.rb +31 -0
- data/lib/salvia_rb/helpers/csrf.rb +47 -0
- data/lib/salvia_rb/helpers/inspector.rb +192 -0
- data/lib/salvia_rb/helpers/island.rb +143 -0
- data/lib/salvia_rb/helpers/tag.rb +90 -0
- data/lib/salvia_rb/helpers.rb +11 -0
- data/lib/salvia_rb/plugins/base.rb +59 -0
- data/lib/salvia_rb/router.rb +181 -0
- data/lib/salvia_rb/ssr/quickjs.rb +404 -0
- data/lib/salvia_rb/ssr.rb +119 -0
- data/lib/salvia_rb/test.rb +20 -0
- data/lib/salvia_rb/version.rb +5 -0
- data/lib/salvia_rb.rb +250 -0
- metadata +344 -0
|
@@ -0,0 +1,1398 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "tty-prompt"
|
|
6
|
+
|
|
7
|
+
module Salvia
|
|
8
|
+
# Salvia フレームワークの CLI ツール
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# salvia new myapp
|
|
12
|
+
# salvia server
|
|
13
|
+
# salvia generate controller posts
|
|
14
|
+
#
|
|
15
|
+
class CLI < Thor
|
|
16
|
+
include Thor::Actions
|
|
17
|
+
|
|
18
|
+
# テンプレートディレクトリ
|
|
19
|
+
def self.source_root
|
|
20
|
+
File.join(__dir__, "templates")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "new APP_NAME", "Create a new Salvia application"
|
|
24
|
+
method_option :template, aliases: "-t", type: :string, desc: "Template: full, api, minimal"
|
|
25
|
+
method_option :islands, type: :boolean, desc: "Include SSR Islands"
|
|
26
|
+
method_option :skip_prompts, type: :boolean, default: false, desc: "Skip interactive prompts"
|
|
27
|
+
def new(app_name)
|
|
28
|
+
@app_name = app_name
|
|
29
|
+
@app_class_name = app_name.split(/[-_]/).map(&:capitalize).join
|
|
30
|
+
@prompt = TTY::Prompt.new
|
|
31
|
+
|
|
32
|
+
say ""
|
|
33
|
+
say "🌿 Creating Salvia app: #{@app_name}", :green
|
|
34
|
+
say ""
|
|
35
|
+
|
|
36
|
+
# 対話式プロンプト(スキップでなければ)
|
|
37
|
+
if options[:skip_prompts]
|
|
38
|
+
@template = options[:template] || "full"
|
|
39
|
+
@include_islands = options[:islands].nil? ? true : options[:islands]
|
|
40
|
+
else
|
|
41
|
+
@template = options[:template] || select_template
|
|
42
|
+
@include_islands = options[:islands].nil? ? prompt_islands : options[:islands]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
say ""
|
|
46
|
+
say "📦 Template: #{@template}", :cyan
|
|
47
|
+
say "🏝️ Islands: #{@include_islands ? 'Yes' : 'No'}", :cyan
|
|
48
|
+
say ""
|
|
49
|
+
|
|
50
|
+
# ディレクトリ構造を作成
|
|
51
|
+
create_directory_structure
|
|
52
|
+
create_config_files
|
|
53
|
+
create_app_files
|
|
54
|
+
create_public_assets
|
|
55
|
+
|
|
56
|
+
say ""
|
|
57
|
+
say "✨ Created #{@app_name}!", :green
|
|
58
|
+
say ""
|
|
59
|
+
say "Next steps:", :yellow
|
|
60
|
+
say " cd #{@app_name}"
|
|
61
|
+
say " bundle install"
|
|
62
|
+
say " salvia db:create"
|
|
63
|
+
say " salvia db:migrate"
|
|
64
|
+
say " salvia css:build"
|
|
65
|
+
if @include_islands
|
|
66
|
+
say " salvia ssr:build"
|
|
67
|
+
end
|
|
68
|
+
say " salvia server"
|
|
69
|
+
say ""
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
desc "generate GENERATOR NAME", "Generate controller, model, or migration (alias: g)"
|
|
73
|
+
map "g" => "generate"
|
|
74
|
+
def generate(generator, name, *args)
|
|
75
|
+
case generator.downcase
|
|
76
|
+
when "controller"
|
|
77
|
+
generate_controller(name, args)
|
|
78
|
+
when "model"
|
|
79
|
+
generate_model(name, args)
|
|
80
|
+
when "migration"
|
|
81
|
+
generate_migration(name, args)
|
|
82
|
+
else
|
|
83
|
+
say "Unknown generator: #{generator}", :red
|
|
84
|
+
say "Available: controller, model, migration", :yellow
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
desc "server", "Start development server (alias: s)"
|
|
89
|
+
map "s" => "server"
|
|
90
|
+
method_option :port, aliases: "-p", type: :numeric, default: 9292, desc: "Port number"
|
|
91
|
+
method_option :host, aliases: "-b", type: :string, default: "localhost", desc: "Host to bind"
|
|
92
|
+
def server
|
|
93
|
+
require_app_environment
|
|
94
|
+
|
|
95
|
+
say "🚀 Starting Salvia server: http://#{options[:host]}:#{options[:port]}", :green
|
|
96
|
+
exec "bundle exec rackup -p #{options[:port]} -o #{options[:host]}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
desc "console", "Start interactive console (alias: c)"
|
|
100
|
+
map "c" => "console"
|
|
101
|
+
def console
|
|
102
|
+
require_app_environment
|
|
103
|
+
|
|
104
|
+
require "irb"
|
|
105
|
+
ARGV.clear
|
|
106
|
+
IRB.start
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Database commands
|
|
110
|
+
desc "db:create", "Create database"
|
|
111
|
+
map "db:create" => :db_create
|
|
112
|
+
def db_create
|
|
113
|
+
require_app_environment
|
|
114
|
+
Salvia::Database.create!
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
desc "db:drop", "Drop database"
|
|
118
|
+
map "db:drop" => :db_drop
|
|
119
|
+
def db_drop
|
|
120
|
+
require_app_environment
|
|
121
|
+
Salvia::Database.drop!
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
desc "db:migrate", "Run pending migrations"
|
|
125
|
+
map "db:migrate" => :db_migrate
|
|
126
|
+
def db_migrate
|
|
127
|
+
require_app_environment
|
|
128
|
+
Salvia::Database.migrate!
|
|
129
|
+
say "Migration completed!", :green
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
desc "db:rollback", "Rollback last migration"
|
|
133
|
+
map "db:rollback" => :db_rollback
|
|
134
|
+
method_option :step, aliases: "-s", type: :numeric, default: 1, desc: "Steps to rollback"
|
|
135
|
+
def db_rollback
|
|
136
|
+
require_app_environment
|
|
137
|
+
Salvia::Database.rollback!(options[:step])
|
|
138
|
+
say "Rollback completed!", :green
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
desc "db:setup", "Create database and run migrations"
|
|
142
|
+
map "db:setup" => :db_setup
|
|
143
|
+
def db_setup
|
|
144
|
+
invoke :db_create
|
|
145
|
+
invoke :db_migrate
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# CSS commands
|
|
149
|
+
desc "css:build", "Build Tailwind CSS"
|
|
150
|
+
map "css:build" => :css_build
|
|
151
|
+
def css_build
|
|
152
|
+
say "🎨 Building Tailwind CSS...", :green
|
|
153
|
+
system "bundle exec tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./public/assets/stylesheets/tailwind.css --minify"
|
|
154
|
+
say "CSS build completed!", :green
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
desc "css:watch", "Watch and rebuild Tailwind CSS"
|
|
158
|
+
map "css:watch" => :css_watch
|
|
159
|
+
def css_watch
|
|
160
|
+
say "👀 Watching CSS changes...", :green
|
|
161
|
+
exec "bundle exec tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./public/assets/stylesheets/tailwind.css --watch"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
desc "assets:precompile", "Precompile assets with hash"
|
|
165
|
+
map "assets:precompile" => :assets_precompile
|
|
166
|
+
def assets_precompile
|
|
167
|
+
require_app_environment
|
|
168
|
+
Salvia::Assets.precompile!
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
desc "routes", "Display registered routes"
|
|
172
|
+
def routes
|
|
173
|
+
require_app_environment
|
|
174
|
+
|
|
175
|
+
say "Routes:", :green
|
|
176
|
+
Salvia::Router.instance.routes.each do |route|
|
|
177
|
+
method = route.method.to_s.upcase.ljust(7)
|
|
178
|
+
path = route.pattern.to_s.ljust(30)
|
|
179
|
+
target = "#{route.controller}##{route.action}"
|
|
180
|
+
say " #{method} #{path} => #{target}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
desc "version", "Display Salvia version"
|
|
185
|
+
def version
|
|
186
|
+
require "salvia_rb/version"
|
|
187
|
+
say "Salvia #{Salvia::VERSION}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# SSR commands
|
|
191
|
+
desc "ssr:build", "Build Island components for SSR"
|
|
192
|
+
map "ssr:build" => :ssr_build
|
|
193
|
+
method_option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Verbose output"
|
|
194
|
+
def ssr_build
|
|
195
|
+
check_deno_installed!
|
|
196
|
+
|
|
197
|
+
say "🏝️ Building Island components...", :green
|
|
198
|
+
|
|
199
|
+
script_path = build_script_path
|
|
200
|
+
cmd = "deno run --allow-all #{script_path}"
|
|
201
|
+
cmd += " --verbose" if options[:verbose]
|
|
202
|
+
|
|
203
|
+
success = system(cmd)
|
|
204
|
+
|
|
205
|
+
if success
|
|
206
|
+
say "✅ SSR build completed!", :green
|
|
207
|
+
else
|
|
208
|
+
say "❌ SSR build failed", :red
|
|
209
|
+
exit 1
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
desc "ssr:watch", "Watch and rebuild Island components"
|
|
214
|
+
map "ssr:watch" => :ssr_watch
|
|
215
|
+
method_option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Verbose output"
|
|
216
|
+
def ssr_watch
|
|
217
|
+
check_deno_installed!
|
|
218
|
+
|
|
219
|
+
say "👀 Watching Island components...", :green
|
|
220
|
+
|
|
221
|
+
script_path = build_script_path
|
|
222
|
+
cmd = "deno run --allow-all #{script_path} --watch"
|
|
223
|
+
cmd += " --verbose" if options[:verbose]
|
|
224
|
+
|
|
225
|
+
exec cmd
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
desc "dev", "Start server + SSR watch together"
|
|
229
|
+
method_option :port, aliases: "-p", type: :numeric, default: 9292, desc: "Port number"
|
|
230
|
+
method_option :host, aliases: "-b", type: :string, default: "localhost", desc: "Host to bind"
|
|
231
|
+
def dev
|
|
232
|
+
require_app_environment
|
|
233
|
+
|
|
234
|
+
say "🚀 Starting Salvia dev mode...", :green
|
|
235
|
+
say " Server: http://#{options[:host]}:#{options[:port]}", :cyan
|
|
236
|
+
say " SSR Watch: enabled", :cyan
|
|
237
|
+
say ""
|
|
238
|
+
|
|
239
|
+
# Deno SSR watch in background
|
|
240
|
+
deno_pid = nil
|
|
241
|
+
if deno_installed?
|
|
242
|
+
deno_pid = spawn("deno run --allow-all #{build_script_path} --watch",
|
|
243
|
+
out: "/dev/null", err: [:child, :out])
|
|
244
|
+
say "🏝️ SSR watch started (PID: #{deno_pid})", :blue
|
|
245
|
+
else
|
|
246
|
+
say "⚠️ Deno not found. Skipping SSR build.", :yellow
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Cleanup on exit
|
|
250
|
+
at_exit do
|
|
251
|
+
if deno_pid
|
|
252
|
+
Process.kill("TERM", deno_pid) rescue nil
|
|
253
|
+
Process.wait(deno_pid) rescue nil
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Tailwind CSS watch in background
|
|
258
|
+
tailwind_pid = spawn("bundle exec tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./public/assets/stylesheets/tailwind.css --watch",
|
|
259
|
+
out: "/dev/null", err: [:child, :out])
|
|
260
|
+
say "🎨 CSS watch started (PID: #{tailwind_pid})", :blue
|
|
261
|
+
|
|
262
|
+
at_exit do
|
|
263
|
+
Process.kill("TERM", tailwind_pid) rescue nil
|
|
264
|
+
Process.wait(tailwind_pid) rescue nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
say ""
|
|
268
|
+
|
|
269
|
+
# Start Ruby server
|
|
270
|
+
exec "bundle exec rackup -p #{options[:port]} -o #{options[:host]}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
private
|
|
274
|
+
|
|
275
|
+
# ========================================
|
|
276
|
+
# 対話式プロンプト
|
|
277
|
+
# ========================================
|
|
278
|
+
|
|
279
|
+
def select_template
|
|
280
|
+
@prompt.select("What template would you like?", cycle: true) do |menu|
|
|
281
|
+
menu.choice "Full app (ERB + Database + Views)", "full"
|
|
282
|
+
menu.choice "API only (JSON responses, no views)", "api"
|
|
283
|
+
menu.choice "Minimal (bare Rack app)", "minimal"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def prompt_islands
|
|
288
|
+
@prompt.yes?("Include SSR Islands? (Preact components)")
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# ========================================
|
|
292
|
+
# ジェネレーター
|
|
293
|
+
# ========================================
|
|
294
|
+
|
|
295
|
+
def generate_controller(name, actions)
|
|
296
|
+
@controller_name = name.downcase
|
|
297
|
+
@controller_class = name.split(/[-_]/).map(&:capitalize).join + "Controller"
|
|
298
|
+
@actions = actions.empty? ? ["index"] : actions
|
|
299
|
+
|
|
300
|
+
say "🎮 Generating controller: #{@controller_class}", :green
|
|
301
|
+
|
|
302
|
+
# コントローラーファイル
|
|
303
|
+
create_file "app/controllers/#{@controller_name}_controller.rb", controller_generator_content
|
|
304
|
+
|
|
305
|
+
# ビューファイル
|
|
306
|
+
@actions.each do |action|
|
|
307
|
+
empty_directory "app/views/#{@controller_name}"
|
|
308
|
+
create_file "app/views/#{@controller_name}/#{action}.html.erb", view_generator_content(action)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# テストファイル
|
|
312
|
+
empty_directory "test/controllers"
|
|
313
|
+
create_file "test/controllers/#{@controller_name}_controller_test.rb", controller_test_generator_content
|
|
314
|
+
|
|
315
|
+
say ""
|
|
316
|
+
say "Add routes to config/routes.rb:", :yellow
|
|
317
|
+
@actions.each do |action|
|
|
318
|
+
say " get \"/#{@controller_name}/#{action}\", to: \"#{@controller_name}##{action}\""
|
|
319
|
+
end
|
|
320
|
+
say ""
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def generate_model(name, fields)
|
|
324
|
+
@model_name = name.downcase
|
|
325
|
+
@model_class = name.split(/[-_]/).map(&:capitalize).join
|
|
326
|
+
@table_name = @model_name + "s"
|
|
327
|
+
@fields = parse_fields(fields)
|
|
328
|
+
|
|
329
|
+
say "📦 Generating model: #{@model_class}", :green
|
|
330
|
+
|
|
331
|
+
# モデルファイル
|
|
332
|
+
create_file "app/models/#{@model_name}.rb", model_generator_content
|
|
333
|
+
|
|
334
|
+
# マイグレーションファイル
|
|
335
|
+
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
|
336
|
+
empty_directory "db/migrate"
|
|
337
|
+
create_file "db/migrate/#{timestamp}_create_#{@table_name}.rb", model_migration_content
|
|
338
|
+
|
|
339
|
+
# テストファイル
|
|
340
|
+
empty_directory "test/models"
|
|
341
|
+
create_file "test/models/#{@model_name}_test.rb", model_test_generator_content
|
|
342
|
+
|
|
343
|
+
say ""
|
|
344
|
+
say "Run migration:", :yellow
|
|
345
|
+
say " salvia db:migrate"
|
|
346
|
+
say ""
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def generate_migration(name, fields)
|
|
350
|
+
@migration_name = name
|
|
351
|
+
@migration_class = name.split(/[-_]/).map(&:capitalize).join
|
|
352
|
+
@fields = parse_fields(fields)
|
|
353
|
+
|
|
354
|
+
say "📝 Generating migration: #{@migration_class}", :green
|
|
355
|
+
|
|
356
|
+
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
|
357
|
+
empty_directory "db/migrate"
|
|
358
|
+
create_file "db/migrate/#{timestamp}_#{name.downcase}.rb", migration_generator_content
|
|
359
|
+
|
|
360
|
+
say ""
|
|
361
|
+
say "Run migration:", :yellow
|
|
362
|
+
say " salvia db:migrate"
|
|
363
|
+
say ""
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def parse_fields(fields)
|
|
367
|
+
fields.map do |field|
|
|
368
|
+
parts = field.split(":")
|
|
369
|
+
{ name: parts[0], type: parts[1] || "string" }
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# ジェネレーターコンテンツメソッド
|
|
374
|
+
|
|
375
|
+
def controller_generator_content
|
|
376
|
+
actions_code = @actions.map do |action|
|
|
377
|
+
<<~RUBY
|
|
378
|
+
def #{action}
|
|
379
|
+
# TODO: implement #{action}
|
|
380
|
+
end
|
|
381
|
+
RUBY
|
|
382
|
+
end.join("\n")
|
|
383
|
+
|
|
384
|
+
<<~RUBY
|
|
385
|
+
class #{@controller_class} < ApplicationController
|
|
386
|
+
#{actions_code.lines.map { |l| " #{l}" }.join.chomp}
|
|
387
|
+
end
|
|
388
|
+
RUBY
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def view_generator_content(action)
|
|
392
|
+
<<~ERB
|
|
393
|
+
<div class="max-w-4xl mx-auto mt-8 px-4">
|
|
394
|
+
<h1 class="text-2xl font-bold mb-4">#{@controller_class}##{action}</h1>
|
|
395
|
+
<p class="text-slate-600">Edit this view at <code class="bg-slate-100 px-2 py-1 rounded">app/views/#{@controller_name}/#{action}.html.erb</code></p>
|
|
396
|
+
</div>
|
|
397
|
+
ERB
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def controller_test_generator_content
|
|
401
|
+
tests = @actions.map do |action|
|
|
402
|
+
<<~RUBY
|
|
403
|
+
def test_#{action}
|
|
404
|
+
get "/#{@controller_name}/#{action}"
|
|
405
|
+
assert last_response.ok?
|
|
406
|
+
end
|
|
407
|
+
RUBY
|
|
408
|
+
end.join("\n")
|
|
409
|
+
|
|
410
|
+
<<~RUBY
|
|
411
|
+
require_relative "../test_helper"
|
|
412
|
+
|
|
413
|
+
class #{@controller_class}Test < Minitest::Test
|
|
414
|
+
#{tests.lines.map { |l| " #{l}" }.join.chomp}
|
|
415
|
+
end
|
|
416
|
+
RUBY
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def model_generator_content
|
|
420
|
+
<<~RUBY
|
|
421
|
+
class #{@model_class} < ApplicationRecord
|
|
422
|
+
# Add validations and associations here
|
|
423
|
+
end
|
|
424
|
+
RUBY
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def model_migration_content
|
|
428
|
+
fields_code = @fields.map do |field|
|
|
429
|
+
" t.#{field[:type]} :#{field[:name]}"
|
|
430
|
+
end.join("\n")
|
|
431
|
+
|
|
432
|
+
<<~RUBY
|
|
433
|
+
class Create#{@table_name.capitalize} < ActiveRecord::Migration[7.0]
|
|
434
|
+
def change
|
|
435
|
+
create_table :#{@table_name} do |t|
|
|
436
|
+
#{fields_code}
|
|
437
|
+
t.timestamps
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
RUBY
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def model_test_generator_content
|
|
445
|
+
<<~RUBY
|
|
446
|
+
require_relative "../test_helper"
|
|
447
|
+
|
|
448
|
+
class #{@model_class}Test < Minitest::Test
|
|
449
|
+
def test_create
|
|
450
|
+
# TODO: implement test
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
RUBY
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def migration_generator_content
|
|
457
|
+
if @migration_name.start_with?("add_")
|
|
458
|
+
# add_X_to_Y pattern
|
|
459
|
+
match = @migration_name.match(/add_(.+)_to_(.+)/)
|
|
460
|
+
if match
|
|
461
|
+
table = match[2]
|
|
462
|
+
fields_code = @fields.map do |field|
|
|
463
|
+
" add_column :#{table}, :#{field[:name]}, :#{field[:type]}"
|
|
464
|
+
end.join("\n")
|
|
465
|
+
|
|
466
|
+
return <<~RUBY
|
|
467
|
+
class #{@migration_class} < ActiveRecord::Migration[7.0]
|
|
468
|
+
def change
|
|
469
|
+
#{fields_code}
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
RUBY
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Generic migration
|
|
477
|
+
<<~RUBY
|
|
478
|
+
class #{@migration_class} < ActiveRecord::Migration[7.0]
|
|
479
|
+
def change
|
|
480
|
+
# TODO: implement migration
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
RUBY
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# ========================================
|
|
487
|
+
# ユーティリティメソッド
|
|
488
|
+
# ========================================
|
|
489
|
+
|
|
490
|
+
def check_deno_installed!
|
|
491
|
+
unless deno_installed?
|
|
492
|
+
say "❌ Deno is not installed.", :red
|
|
493
|
+
say ""
|
|
494
|
+
say "Install:", :yellow
|
|
495
|
+
say " curl -fsSL https://deno.land/install.sh | sh"
|
|
496
|
+
say ""
|
|
497
|
+
say "Or visit: https://deno.land", :yellow
|
|
498
|
+
exit 1
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def deno_installed?
|
|
503
|
+
system("which deno > /dev/null 2>&1")
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# gem 内蔵のビルドスクリプトパスを返す
|
|
507
|
+
def build_script_path
|
|
508
|
+
File.expand_path("../../../assets/scripts/build_ssr.ts", __FILE__)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def require_app_environment
|
|
512
|
+
routes_file = File.join(Dir.pwd, "config", "routes.rb")
|
|
513
|
+
unless File.exist?(routes_file)
|
|
514
|
+
say "Error: config/routes.rb not found. Run this command in a Salvia app directory.", :red
|
|
515
|
+
exit 1
|
|
516
|
+
end
|
|
517
|
+
# Salvia アプリのルートを設定
|
|
518
|
+
Salvia.root = Dir.pwd
|
|
519
|
+
Salvia.env = ENV.fetch("RACK_ENV", "development")
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def create_directory_structure
|
|
523
|
+
# アプリディレクトリ
|
|
524
|
+
empty_directory "#{@app_name}/app/controllers"
|
|
525
|
+
empty_directory "#{@app_name}/app/models"
|
|
526
|
+
|
|
527
|
+
unless @template == "api"
|
|
528
|
+
empty_directory "#{@app_name}/app/views/layouts"
|
|
529
|
+
empty_directory "#{@app_name}/app/views/home"
|
|
530
|
+
empty_directory "#{@app_name}/app/components"
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
if @include_islands
|
|
534
|
+
empty_directory "#{@app_name}/app/islands"
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
empty_directory "#{@app_name}/app/assets/stylesheets"
|
|
538
|
+
|
|
539
|
+
# 設定 (最小構成)
|
|
540
|
+
empty_directory "#{@app_name}/config"
|
|
541
|
+
# empty_directory "#{@app_name}/config/environments" # オプション
|
|
542
|
+
|
|
543
|
+
# データベース(minimal以外)
|
|
544
|
+
unless @template == "minimal"
|
|
545
|
+
empty_directory "#{@app_name}/db/migrate"
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# ログ
|
|
549
|
+
empty_directory "#{@app_name}/log"
|
|
550
|
+
|
|
551
|
+
# 公開アセット
|
|
552
|
+
empty_directory "#{@app_name}/public/assets/javascripts"
|
|
553
|
+
empty_directory "#{@app_name}/public/assets/stylesheets"
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def create_config_files
|
|
557
|
+
# Gemfile
|
|
558
|
+
create_file "#{@app_name}/Gemfile", gemfile_content
|
|
559
|
+
|
|
560
|
+
# config.ru (ゼロコンフィグ - たった3行)
|
|
561
|
+
create_file "#{@app_name}/config.ru", config_ru_content
|
|
562
|
+
|
|
563
|
+
# config/routes.rb (これだけ必須)
|
|
564
|
+
create_file "#{@app_name}/config/routes.rb", routes_rb_content
|
|
565
|
+
|
|
566
|
+
# config/app.rb (オプション - カスタム設定用)
|
|
567
|
+
create_file "#{@app_name}/config/app.rb", app_rb_content
|
|
568
|
+
|
|
569
|
+
# config/database.yml (オプション - なくても動作)
|
|
570
|
+
unless @template == "minimal"
|
|
571
|
+
create_file "#{@app_name}/config/database.yml", database_yml_content
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# config/environments (オプション - カスタマイズ用)
|
|
575
|
+
# create_file "#{@app_name}/config/environments/development.rb", development_config_content
|
|
576
|
+
# create_file "#{@app_name}/config/environments/production.rb", production_config_content
|
|
577
|
+
|
|
578
|
+
# Rakefile
|
|
579
|
+
create_file "#{@app_name}/Rakefile", rakefile_content
|
|
580
|
+
|
|
581
|
+
# テスト
|
|
582
|
+
empty_directory "#{@app_name}/test"
|
|
583
|
+
create_file "#{@app_name}/test/test_helper.rb", test_helper_content
|
|
584
|
+
|
|
585
|
+
unless @template == "minimal"
|
|
586
|
+
create_file "#{@app_name}/test/controllers/home_controller_test.rb", home_controller_test_content
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# tailwind.config.js
|
|
590
|
+
create_file "#{@app_name}/tailwind.config.js", tailwind_config_content
|
|
591
|
+
|
|
592
|
+
# .gitignore
|
|
593
|
+
create_file "#{@app_name}/.gitignore", gitignore_content
|
|
594
|
+
|
|
595
|
+
# .env.example (環境変数テンプレート)
|
|
596
|
+
create_file "#{@app_name}/.env.example", env_example_content
|
|
597
|
+
|
|
598
|
+
# .env (開発用 - シークレットキー自動生成)
|
|
599
|
+
create_file "#{@app_name}/.env", env_content
|
|
600
|
+
|
|
601
|
+
# Docker (本番環境用)
|
|
602
|
+
create_file "#{@app_name}/Dockerfile", dockerfile_content
|
|
603
|
+
create_file "#{@app_name}/docker-compose.yml", docker_compose_content
|
|
604
|
+
create_file "#{@app_name}/.dockerignore", dockerignore_content
|
|
605
|
+
|
|
606
|
+
# Deno 設定ファイル (SSR ビルド用)
|
|
607
|
+
if @include_islands
|
|
608
|
+
create_file "#{@app_name}/deno.json", deno_json_content
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def create_app_files
|
|
613
|
+
# ApplicationController
|
|
614
|
+
create_file "#{@app_name}/app/controllers/application_controller.rb", application_controller_content
|
|
615
|
+
|
|
616
|
+
# HomeController(minimal以外)
|
|
617
|
+
unless @template == "minimal"
|
|
618
|
+
create_file "#{@app_name}/app/controllers/home_controller.rb", home_controller_content
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# ApplicationRecord(minimal以外)
|
|
622
|
+
unless @template == "minimal"
|
|
623
|
+
create_file "#{@app_name}/app/models/application_record.rb", application_record_content
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# ビュー(API/minimal以外)
|
|
627
|
+
unless @template == "api" || @template == "minimal"
|
|
628
|
+
create_file "#{@app_name}/app/views/layouts/application.html.erb", layout_content
|
|
629
|
+
create_file "#{@app_name}/app/views/home/index.html.erb", home_index_content
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Islands コンポーネント (JSX)
|
|
633
|
+
if @include_islands
|
|
634
|
+
create_file "#{@app_name}/app/islands/Counter.jsx", counter_island_content
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Tailwind ソース CSS
|
|
638
|
+
create_file "#{@app_name}/app/assets/stylesheets/application.tailwind.css", tailwind_css_content
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def create_public_assets
|
|
642
|
+
# アプリケーション JS
|
|
643
|
+
create_file "#{@app_name}/public/assets/javascripts/app.js", app_js_content
|
|
644
|
+
|
|
645
|
+
# Islands JS(ハイドレーション)- Islands を含む場合のみ
|
|
646
|
+
if @include_islands
|
|
647
|
+
create_file "#{@app_name}/public/assets/javascripts/islands.js", islands_js_content
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Tailwind CSS プレースホルダー
|
|
651
|
+
create_file "#{@app_name}/public/assets/stylesheets/tailwind.css", "/* Run 'salvia css:build' to generate */\n"
|
|
652
|
+
|
|
653
|
+
# エラーページ
|
|
654
|
+
create_file "#{@app_name}/public/404.html", error_404_content
|
|
655
|
+
create_file "#{@app_name}/public/500.html", error_500_content
|
|
656
|
+
|
|
657
|
+
# SSR 用ディレクトリ - Islands を含む場合のみ
|
|
658
|
+
if @include_islands
|
|
659
|
+
empty_directory "#{@app_name}/vendor/server"
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# ファイルコンテンツメソッド
|
|
664
|
+
def gemfile_content
|
|
665
|
+
<<~RUBY
|
|
666
|
+
source "https://rubygems.org"
|
|
667
|
+
|
|
668
|
+
gem "salvia_rb"
|
|
669
|
+
gem "sqlite3"
|
|
670
|
+
gem "dotenv" # .env 読み込み(開発環境のみ)
|
|
671
|
+
|
|
672
|
+
# Web サーバー
|
|
673
|
+
gem "puma" # 開発環境用 (スレッドベース)
|
|
674
|
+
gem "falcon" # 本番環境用 (async/fork、Linux/Docker推奨)
|
|
675
|
+
|
|
676
|
+
# 本番環境用データベース (Docker/PostgreSQL)
|
|
677
|
+
gem "pg", "~> 1.6"
|
|
678
|
+
|
|
679
|
+
group :development do
|
|
680
|
+
gem "debug"
|
|
681
|
+
end
|
|
682
|
+
RUBY
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def config_ru_content
|
|
686
|
+
<<~RUBY
|
|
687
|
+
# Salvia - ゼロコンフィグで動作
|
|
688
|
+
require "bundler/setup"
|
|
689
|
+
require "salvia_rb"
|
|
690
|
+
|
|
691
|
+
run Salvia::Application.new
|
|
692
|
+
RUBY
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def app_rb_content
|
|
696
|
+
<<~RUBY
|
|
697
|
+
# アプリケーション設定(オプション)
|
|
698
|
+
#
|
|
699
|
+
# このファイルは任意です。Salvia はゼロコンフィグで動作します。
|
|
700
|
+
# カスタマイズが必要な場合のみ編集してください。
|
|
701
|
+
|
|
702
|
+
Salvia.configure do |config|
|
|
703
|
+
# シークレットキー(本番では SECRET_KEY 環境変数を使用)
|
|
704
|
+
# config.secret_key = ENV["SECRET_KEY"]
|
|
705
|
+
|
|
706
|
+
# SSR バンドルパス(Islands Architecture 使用時)
|
|
707
|
+
# config.ssr_bundle_path = "vendor/server/ssr_bundle.js"
|
|
708
|
+
|
|
709
|
+
# テンプレートキャッシュ(本番ではデフォルトで有効)
|
|
710
|
+
# config.cache_templates = Salvia.env == "production"
|
|
711
|
+
end
|
|
712
|
+
RUBY
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def routes_rb_content
|
|
716
|
+
<<~RUBY
|
|
717
|
+
Salvia::Router.draw do
|
|
718
|
+
root to: "home#index"
|
|
719
|
+
|
|
720
|
+
# ルートを追加
|
|
721
|
+
# get "/about", to: "pages#about"
|
|
722
|
+
# resources :posts
|
|
723
|
+
end
|
|
724
|
+
RUBY
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def database_yml_content
|
|
728
|
+
<<~YAML
|
|
729
|
+
# データベース設定(オプション)
|
|
730
|
+
#
|
|
731
|
+
# このファイルは任意です。なくても Salvia は以下の規約で動作します:
|
|
732
|
+
# development: db/development.sqlite3
|
|
733
|
+
# test: db/test.sqlite3
|
|
734
|
+
# production: DATABASE_URL 環境変数、または db/production.sqlite3
|
|
735
|
+
#
|
|
736
|
+
# PostgreSQL を使う場合のみ本番環境を設定してください:
|
|
737
|
+
#
|
|
738
|
+
# production:
|
|
739
|
+
# adapter: postgresql
|
|
740
|
+
# url: <%= ENV["DATABASE_URL"] %>
|
|
741
|
+
|
|
742
|
+
default: &default
|
|
743
|
+
adapter: sqlite3
|
|
744
|
+
pool: 5
|
|
745
|
+
timeout: 5000
|
|
746
|
+
|
|
747
|
+
development:
|
|
748
|
+
<<: *default
|
|
749
|
+
database: db/development.sqlite3
|
|
750
|
+
|
|
751
|
+
test:
|
|
752
|
+
<<: *default
|
|
753
|
+
database: db/test.sqlite3
|
|
754
|
+
|
|
755
|
+
production:
|
|
756
|
+
<<: *default
|
|
757
|
+
database: db/production.sqlite3
|
|
758
|
+
# または PostgreSQL:
|
|
759
|
+
# adapter: postgresql
|
|
760
|
+
# url: <%= ENV["DATABASE_URL"] %>
|
|
761
|
+
YAML
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def rakefile_content
|
|
765
|
+
<<~RUBY
|
|
766
|
+
# Salvia Rakefile - ゼロコンフィグ
|
|
767
|
+
require "bundler/setup"
|
|
768
|
+
require "salvia_rb"
|
|
769
|
+
|
|
770
|
+
# アプリケーションルートを設定
|
|
771
|
+
Salvia.root = File.expand_path(__dir__)
|
|
772
|
+
|
|
773
|
+
namespace :db do
|
|
774
|
+
desc "データベースを作成"
|
|
775
|
+
task :create do
|
|
776
|
+
Salvia::Database.create!
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
desc "データベースを削除"
|
|
780
|
+
task :drop do
|
|
781
|
+
Salvia::Database.drop!
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
desc "マイグレーションを実行"
|
|
785
|
+
task :migrate do
|
|
786
|
+
Salvia::Database.setup!
|
|
787
|
+
Salvia::Database.migrate!
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
desc "直前のマイグレーションをロールバック"
|
|
791
|
+
task :rollback do
|
|
792
|
+
Salvia::Database.setup!
|
|
793
|
+
Salvia::Database.rollback!
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
desc "データベースの作成とマイグレーション"
|
|
797
|
+
task :setup => [:create, :migrate]
|
|
798
|
+
end
|
|
799
|
+
RUBY
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def tailwind_config_content
|
|
803
|
+
<<~JS
|
|
804
|
+
/** @type {import('tailwindcss').Config} */
|
|
805
|
+
module.exports = {
|
|
806
|
+
content: [
|
|
807
|
+
"./app/views/**/*.erb",
|
|
808
|
+
"./public/assets/javascripts/**/*.js"
|
|
809
|
+
],
|
|
810
|
+
theme: {
|
|
811
|
+
extend: {
|
|
812
|
+
colors: {
|
|
813
|
+
'salvia': {
|
|
814
|
+
50: '#f0f0ff',
|
|
815
|
+
100: '#e4e4ff',
|
|
816
|
+
200: '#cdcdff',
|
|
817
|
+
300: '#a8a8ff',
|
|
818
|
+
400: '#7c7cff',
|
|
819
|
+
500: '#6A5ACD', // Blue Salvia
|
|
820
|
+
600: '#5a4ab8',
|
|
821
|
+
700: '#4B0082', // Indigo
|
|
822
|
+
800: '#3d006b',
|
|
823
|
+
900: '#2d0050',
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
plugins: [],
|
|
829
|
+
}
|
|
830
|
+
JS
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def test_helper_content
|
|
834
|
+
<<~RUBY
|
|
835
|
+
ENV["RACK_ENV"] = "test"
|
|
836
|
+
require_relative "../config/environment"
|
|
837
|
+
require "minitest/autorun"
|
|
838
|
+
require "salvia_rb/test"
|
|
839
|
+
|
|
840
|
+
class Minitest::Test
|
|
841
|
+
include Salvia::Test::ControllerHelper
|
|
842
|
+
end
|
|
843
|
+
RUBY
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def home_controller_test_content
|
|
847
|
+
<<~RUBY
|
|
848
|
+
require_relative "../test_helper"
|
|
849
|
+
|
|
850
|
+
class HomeControllerTest < Minitest::Test
|
|
851
|
+
def test_index
|
|
852
|
+
get "/"
|
|
853
|
+
assert last_response.ok?
|
|
854
|
+
assert_includes last_response.body, "Salvia"
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
RUBY
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def gitignore_content
|
|
861
|
+
<<~TEXT
|
|
862
|
+
# データベース
|
|
863
|
+
db/*.sqlite3
|
|
864
|
+
|
|
865
|
+
# Bundler
|
|
866
|
+
/.bundle/
|
|
867
|
+
/vendor/bundle/
|
|
868
|
+
|
|
869
|
+
# 環境変数
|
|
870
|
+
.env
|
|
871
|
+
.env.local
|
|
872
|
+
|
|
873
|
+
# ログ
|
|
874
|
+
/log/*.log
|
|
875
|
+
|
|
876
|
+
# 一時ファイル
|
|
877
|
+
/tmp/
|
|
878
|
+
|
|
879
|
+
# ビルド出力
|
|
880
|
+
/public/assets/stylesheets/tailwind.css
|
|
881
|
+
/vendor/server/
|
|
882
|
+
/vendor/client/
|
|
883
|
+
|
|
884
|
+
# OS ファイル
|
|
885
|
+
.DS_Store
|
|
886
|
+
|
|
887
|
+
# IDE
|
|
888
|
+
.idea/
|
|
889
|
+
.vscode/
|
|
890
|
+
TEXT
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def env_example_content
|
|
894
|
+
<<~TEXT
|
|
895
|
+
# Salvia Environment Configuration
|
|
896
|
+
# Copy this file to .env and customize for your environment
|
|
897
|
+
# .env is auto-loaded in development mode only
|
|
898
|
+
# In production, set environment variables directly
|
|
899
|
+
|
|
900
|
+
# Environment: development | production | test
|
|
901
|
+
# RACK_ENV=development
|
|
902
|
+
|
|
903
|
+
# Secret Key (required in production)
|
|
904
|
+
# Generate with: ruby -e "require 'securerandom'; puts SecureRandom.hex(32)"
|
|
905
|
+
# SECRET_KEY=your_secret_key_here
|
|
906
|
+
|
|
907
|
+
# Server Configuration
|
|
908
|
+
# PORT=9292
|
|
909
|
+
# HOST=0.0.0.0
|
|
910
|
+
|
|
911
|
+
# Database URL (overrides database.yml)
|
|
912
|
+
# DATABASE_URL=sqlite3:db/development.sqlite3
|
|
913
|
+
# DATABASE_URL=postgres://user:pass@localhost/myapp_production
|
|
914
|
+
|
|
915
|
+
# Logging
|
|
916
|
+
# LOG_LEVEL=debug
|
|
917
|
+
TEXT
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
def env_content
|
|
921
|
+
require "securerandom"
|
|
922
|
+
secret = SecureRandom.hex(32)
|
|
923
|
+
<<~TEXT
|
|
924
|
+
# Development environment variables
|
|
925
|
+
# This file is gitignored - safe for local secrets
|
|
926
|
+
|
|
927
|
+
SECRET_KEY=#{secret}
|
|
928
|
+
TEXT
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def dockerfile_content
|
|
932
|
+
# アプリ名からベース名のみを抽出
|
|
933
|
+
safe_app_name = File.basename(@app_name).gsub(/[^a-zA-Z0-9_-]/, "_").downcase
|
|
934
|
+
|
|
935
|
+
<<~DOCKERFILE
|
|
936
|
+
# Salvia Production Dockerfile
|
|
937
|
+
# Falcon (async server) + YJIT enabled
|
|
938
|
+
FROM ruby:3.2.9-slim
|
|
939
|
+
|
|
940
|
+
# 環境変数
|
|
941
|
+
ENV RUBY_YJIT_ENABLE=1
|
|
942
|
+
ENV RACK_ENV=production
|
|
943
|
+
ENV BUNDLE_WITHOUT=development:test
|
|
944
|
+
ENV BUNDLE_DEPLOYMENT=1
|
|
945
|
+
|
|
946
|
+
# システム依存パッケージ
|
|
947
|
+
RUN apt-get update -qq && \\
|
|
948
|
+
apt-get install -y --no-install-recommends \\
|
|
949
|
+
build-essential \\
|
|
950
|
+
libpq-dev \\
|
|
951
|
+
nodejs \\
|
|
952
|
+
curl \\
|
|
953
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
954
|
+
|
|
955
|
+
# 作業ディレクトリ
|
|
956
|
+
WORKDIR /app
|
|
957
|
+
|
|
958
|
+
# Gemfile コピーと依存関係インストール
|
|
959
|
+
COPY Gemfile Gemfile.lock ./
|
|
960
|
+
RUN bundle install --jobs 4 --retry 3
|
|
961
|
+
|
|
962
|
+
# アプリケーションコード
|
|
963
|
+
COPY . .
|
|
964
|
+
|
|
965
|
+
# アセットのビルド (Tailwind CSS)
|
|
966
|
+
RUN bundle exec rake css:build || true
|
|
967
|
+
|
|
968
|
+
# 非 root ユーザーで実行
|
|
969
|
+
RUN useradd -m -s /bin/bash appuser && \\
|
|
970
|
+
chown -R appuser:appuser /app
|
|
971
|
+
USER appuser
|
|
972
|
+
|
|
973
|
+
# ポート公開
|
|
974
|
+
EXPOSE 9292
|
|
975
|
+
|
|
976
|
+
# Falcon で起動 (YJIT 有効)
|
|
977
|
+
CMD ["bundle", "exec", "falcon", "serve", "--bind", "http://0.0.0.0:9292", "--count", "4"]
|
|
978
|
+
DOCKERFILE
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def docker_compose_content
|
|
982
|
+
# アプリ名からベース名のみを抽出
|
|
983
|
+
safe_app_name = File.basename(@app_name).gsub(/[^a-zA-Z0-9_-]/, "_").downcase
|
|
984
|
+
|
|
985
|
+
<<~YAML
|
|
986
|
+
# Salvia Docker Compose Configuration
|
|
987
|
+
# Production environment with PostgreSQL
|
|
988
|
+
|
|
989
|
+
services:
|
|
990
|
+
db:
|
|
991
|
+
image: postgres:15-alpine
|
|
992
|
+
volumes:
|
|
993
|
+
- postgres_data:/var/lib/postgresql/data
|
|
994
|
+
environment:
|
|
995
|
+
POSTGRES_DB: #{safe_app_name}_production
|
|
996
|
+
POSTGRES_USER: #{safe_app_name}
|
|
997
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-changeme}
|
|
998
|
+
healthcheck:
|
|
999
|
+
test: ["CMD-SHELL", "pg_isready -U #{safe_app_name}"]
|
|
1000
|
+
interval: 5s
|
|
1001
|
+
timeout: 5s
|
|
1002
|
+
retries: 5
|
|
1003
|
+
|
|
1004
|
+
app:
|
|
1005
|
+
build: .
|
|
1006
|
+
ports:
|
|
1007
|
+
- "9292:9292"
|
|
1008
|
+
environment:
|
|
1009
|
+
RACK_ENV: production
|
|
1010
|
+
DATABASE_URL: postgres://#{safe_app_name}:\${POSTGRES_PASSWORD:-changeme}@db:5432/#{safe_app_name}_production
|
|
1011
|
+
SESSION_SECRET: \${SESSION_SECRET:-generate_a_secure_secret_here}
|
|
1012
|
+
RUBY_YJIT_ENABLE: "1"
|
|
1013
|
+
depends_on:
|
|
1014
|
+
db:
|
|
1015
|
+
condition: service_healthy
|
|
1016
|
+
# ヘルスチェック
|
|
1017
|
+
healthcheck:
|
|
1018
|
+
test: ["CMD", "curl", "-f", "http://localhost:9292/"]
|
|
1019
|
+
interval: 30s
|
|
1020
|
+
timeout: 10s
|
|
1021
|
+
retries: 3
|
|
1022
|
+
|
|
1023
|
+
volumes:
|
|
1024
|
+
postgres_data:
|
|
1025
|
+
YAML
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
def dockerignore_content
|
|
1029
|
+
<<~TEXT
|
|
1030
|
+
# Git
|
|
1031
|
+
.git
|
|
1032
|
+
.gitignore
|
|
1033
|
+
|
|
1034
|
+
# ドキュメント
|
|
1035
|
+
*.md
|
|
1036
|
+
docs/
|
|
1037
|
+
|
|
1038
|
+
# 開発用ファイル
|
|
1039
|
+
.env.local
|
|
1040
|
+
.env.development
|
|
1041
|
+
|
|
1042
|
+
# テスト
|
|
1043
|
+
test/
|
|
1044
|
+
spec/
|
|
1045
|
+
|
|
1046
|
+
# ログとデータベース
|
|
1047
|
+
log/
|
|
1048
|
+
db/*.sqlite3
|
|
1049
|
+
|
|
1050
|
+
# 一時ファイル
|
|
1051
|
+
tmp/
|
|
1052
|
+
.DS_Store
|
|
1053
|
+
|
|
1054
|
+
# Bundler (Docker 内で再インストール)
|
|
1055
|
+
vendor/bundle/
|
|
1056
|
+
.bundle/
|
|
1057
|
+
|
|
1058
|
+
# IDE
|
|
1059
|
+
.idea/
|
|
1060
|
+
.vscode/
|
|
1061
|
+
TEXT
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1064
|
+
def application_controller_content
|
|
1065
|
+
<<~RUBY
|
|
1066
|
+
class ApplicationController < Salvia::Controller
|
|
1067
|
+
# 共通のコントローラーロジックをここに追加
|
|
1068
|
+
end
|
|
1069
|
+
RUBY
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
def home_controller_content
|
|
1073
|
+
<<~RUBY
|
|
1074
|
+
class HomeController < ApplicationController
|
|
1075
|
+
def index
|
|
1076
|
+
@title = "Salvia へようこそ"
|
|
1077
|
+
end
|
|
1078
|
+
end
|
|
1079
|
+
RUBY
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
def application_record_content
|
|
1083
|
+
<<~RUBY
|
|
1084
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
1085
|
+
primary_abstract_class
|
|
1086
|
+
end
|
|
1087
|
+
RUBY
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
def layout_content
|
|
1091
|
+
<<~ERB
|
|
1092
|
+
<!DOCTYPE html>
|
|
1093
|
+
<html lang="en">
|
|
1094
|
+
<head>
|
|
1095
|
+
<meta charset="UTF-8">
|
|
1096
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1097
|
+
<title><%= @title || "#{@app_class_name}" %></title>
|
|
1098
|
+
|
|
1099
|
+
<%= csrf_meta_tags %>
|
|
1100
|
+
|
|
1101
|
+
<link rel="stylesheet" href="/assets/stylesheets/tailwind.css">
|
|
1102
|
+
<script type="module" src="/assets/javascripts/app.js"></script>
|
|
1103
|
+
<script type="module" src="/assets/javascripts/islands.js"></script>
|
|
1104
|
+
|
|
1105
|
+
<% if Salvia.development? && Salvia.config.island_inspector? %>
|
|
1106
|
+
<%= island_inspector_tags %>
|
|
1107
|
+
<% end %>
|
|
1108
|
+
</head>
|
|
1109
|
+
<body class="min-h-screen bg-slate-50 text-slate-900">
|
|
1110
|
+
<%= yield %>
|
|
1111
|
+
</body>
|
|
1112
|
+
</html>
|
|
1113
|
+
ERB
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
def home_index_content
|
|
1117
|
+
if @include_islands
|
|
1118
|
+
home_index_with_islands_content
|
|
1119
|
+
else
|
|
1120
|
+
home_index_basic_content
|
|
1121
|
+
end
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
def home_index_with_islands_content
|
|
1125
|
+
<<~ERB
|
|
1126
|
+
<div class="max-w-2xl mx-auto mt-16 px-4">
|
|
1127
|
+
<div class="text-center">
|
|
1128
|
+
<h1 class="text-4xl font-bold text-salvia-700 mb-4">
|
|
1129
|
+
🌿 Salvia へようこそ
|
|
1130
|
+
</h1>
|
|
1131
|
+
<p class="text-lg text-slate-600 mb-8">
|
|
1132
|
+
小さくて理解しやすい Ruby MVC フレームワーク
|
|
1133
|
+
</p>
|
|
1134
|
+
|
|
1135
|
+
<!-- SSR Islands Demo -->
|
|
1136
|
+
<div class="mb-8">
|
|
1137
|
+
<h2 class="text-xl font-semibold mb-4">🏝️ SSR Islands Demo</h2>
|
|
1138
|
+
<div class="flex justify-center">
|
|
1139
|
+
<%= island "Counter", initialCount: 0 %>
|
|
1140
|
+
</div>
|
|
1141
|
+
<p class="text-xs text-slate-500 mt-2">
|
|
1142
|
+
↑ Preact で動くインタラクティブコンポーネント
|
|
1143
|
+
</p>
|
|
1144
|
+
</div>
|
|
1145
|
+
|
|
1146
|
+
<div class="bg-white rounded-lg shadow-md p-6 text-left">
|
|
1147
|
+
<h2 class="text-xl font-semibold mb-4">はじめに</h2>
|
|
1148
|
+
|
|
1149
|
+
<div class="space-y-3 text-sm">
|
|
1150
|
+
<div class="flex items-start gap-3">
|
|
1151
|
+
<span class="bg-salvia-100 text-salvia-700 rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0">1</span>
|
|
1152
|
+
<div>
|
|
1153
|
+
<code class="bg-slate-100 px-2 py-1 rounded">config/routes.rb</code>
|
|
1154
|
+
<p class="text-slate-600 mt-1">ルーティングを定義</p>
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
|
|
1158
|
+
<div class="flex items-start gap-3">
|
|
1159
|
+
<span class="bg-salvia-100 text-salvia-700 rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0">2</span>
|
|
1160
|
+
<div>
|
|
1161
|
+
<code class="bg-slate-100 px-2 py-1 rounded">app/controllers/</code>
|
|
1162
|
+
<p class="text-slate-600 mt-1">コントローラーを追加</p>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
|
|
1166
|
+
<div class="flex items-start gap-3">
|
|
1167
|
+
<span class="bg-salvia-100 text-salvia-700 rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0">3</span>
|
|
1168
|
+
<div>
|
|
1169
|
+
<code class="bg-slate-100 px-2 py-1 rounded">app/islands/</code>
|
|
1170
|
+
<p class="text-slate-600 mt-1">Islands コンポーネントを追加</p>
|
|
1171
|
+
</div>
|
|
1172
|
+
</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
|
|
1176
|
+
<p class="mt-8 text-sm text-slate-500">
|
|
1177
|
+
<code class="bg-slate-100 px-2 py-0.5 rounded">app/views/home/index.html.erb</code> を編集してこのページを変更
|
|
1178
|
+
</p>
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
ERB
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
def home_index_basic_content
|
|
1185
|
+
<<~ERB
|
|
1186
|
+
<div class="max-w-2xl mx-auto mt-16 px-4">
|
|
1187
|
+
<div class="text-center">
|
|
1188
|
+
<h1 class="text-4xl font-bold text-salvia-700 mb-4">
|
|
1189
|
+
🌿 Salvia へようこそ
|
|
1190
|
+
</h1>
|
|
1191
|
+
<p class="text-lg text-slate-600 mb-8">
|
|
1192
|
+
小さくて理解しやすい Ruby MVC フレームワーク
|
|
1193
|
+
</p>
|
|
1194
|
+
|
|
1195
|
+
<div class="bg-white rounded-lg shadow-md p-6 text-left">
|
|
1196
|
+
<h2 class="text-xl font-semibold mb-4">はじめに</h2>
|
|
1197
|
+
|
|
1198
|
+
<div class="space-y-3 text-sm">
|
|
1199
|
+
<div class="flex items-start gap-3">
|
|
1200
|
+
<span class="bg-salvia-100 text-salvia-700 rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0">1</span>
|
|
1201
|
+
<div>
|
|
1202
|
+
<code class="bg-slate-100 px-2 py-1 rounded">config/routes.rb</code>
|
|
1203
|
+
<p class="text-slate-600 mt-1">ルーティングを定義</p>
|
|
1204
|
+
</div>
|
|
1205
|
+
</div>
|
|
1206
|
+
|
|
1207
|
+
<div class="flex items-start gap-3">
|
|
1208
|
+
<span class="bg-salvia-100 text-salvia-700 rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0">2</span>
|
|
1209
|
+
<div>
|
|
1210
|
+
<code class="bg-slate-100 px-2 py-1 rounded">app/controllers/</code>
|
|
1211
|
+
<p class="text-slate-600 mt-1">コントローラーを追加</p>
|
|
1212
|
+
</div>
|
|
1213
|
+
</div>
|
|
1214
|
+
|
|
1215
|
+
<div class="flex items-start gap-3">
|
|
1216
|
+
<span class="bg-salvia-100 text-salvia-700 rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0">3</span>
|
|
1217
|
+
<div>
|
|
1218
|
+
<code class="bg-slate-100 px-2 py-1 rounded">app/views/</code>
|
|
1219
|
+
<p class="text-slate-600 mt-1">ERB でビューを作成</p>
|
|
1220
|
+
</div>
|
|
1221
|
+
</div>
|
|
1222
|
+
</div>
|
|
1223
|
+
</div>
|
|
1224
|
+
|
|
1225
|
+
<p class="mt-8 text-sm text-slate-500">
|
|
1226
|
+
<code class="bg-slate-100 px-2 py-0.5 rounded">app/views/home/index.html.erb</code> を編集してこのページを変更
|
|
1227
|
+
</p>
|
|
1228
|
+
</div>
|
|
1229
|
+
</div>
|
|
1230
|
+
ERB
|
|
1231
|
+
end
|
|
1232
|
+
|
|
1233
|
+
def counter_island_content
|
|
1234
|
+
<<~JSX
|
|
1235
|
+
// Counter Island - インタラクティブカウンター (JSX)
|
|
1236
|
+
import { h, render, hydrate } from 'preact';
|
|
1237
|
+
import { useState } from 'preact/hooks';
|
|
1238
|
+
|
|
1239
|
+
export default function Counter({ initialCount = 0 }) {
|
|
1240
|
+
const [count, setCount] = useState(initialCount);
|
|
1241
|
+
|
|
1242
|
+
return (
|
|
1243
|
+
<div class="p-6 bg-white rounded-lg shadow-md">
|
|
1244
|
+
<h3 class="text-lg font-semibold mb-3 text-salvia-700">🏝️ Counter Island</h3>
|
|
1245
|
+
<p class="text-4xl font-bold text-salvia-600 mb-4">{count}</p>
|
|
1246
|
+
<div class="flex gap-2 justify-center">
|
|
1247
|
+
<button
|
|
1248
|
+
onClick={() => setCount(count - 1)}
|
|
1249
|
+
class="px-4 py-2 bg-slate-200 rounded hover:bg-slate-300 transition"
|
|
1250
|
+
>−</button>
|
|
1251
|
+
<button
|
|
1252
|
+
onClick={() => setCount(0)}
|
|
1253
|
+
class="px-4 py-2 bg-slate-100 rounded hover:bg-slate-200 transition"
|
|
1254
|
+
>Reset</button>
|
|
1255
|
+
<button
|
|
1256
|
+
onClick={() => setCount(count + 1)}
|
|
1257
|
+
class="px-4 py-2 bg-salvia-500 text-white rounded hover:bg-salvia-600 transition"
|
|
1258
|
+
>+</button>
|
|
1259
|
+
</div>
|
|
1260
|
+
</div>
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Salvia mount function
|
|
1265
|
+
export function mount(element, props, { hydrate: shouldHydrate } = {}) {
|
|
1266
|
+
const vnode = <Counter {...props} />;
|
|
1267
|
+
shouldHydrate ? hydrate(vnode, element) : render(vnode, element);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
export { Counter };
|
|
1271
|
+
JSX
|
|
1272
|
+
end
|
|
1273
|
+
|
|
1274
|
+
def deno_json_content
|
|
1275
|
+
<<~JSON
|
|
1276
|
+
{
|
|
1277
|
+
"compilerOptions": {
|
|
1278
|
+
"jsx": "react-jsx",
|
|
1279
|
+
"jsxImportSource": "preact"
|
|
1280
|
+
},
|
|
1281
|
+
"imports": {
|
|
1282
|
+
"preact": "https://esm.sh/preact@10.19.3",
|
|
1283
|
+
"preact/": "https://esm.sh/preact@10.19.3/"
|
|
1284
|
+
},
|
|
1285
|
+
"tasks": {
|
|
1286
|
+
"build": "deno run --allow-all $(salvia build_script) 2>/dev/null || deno run --allow-all build_ssr.ts",
|
|
1287
|
+
"watch": "deno run --allow-all $(salvia build_script) --watch 2>/dev/null || deno run --allow-all build_ssr.ts --watch"
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
JSON
|
|
1291
|
+
end
|
|
1292
|
+
|
|
1293
|
+
def tailwind_css_content
|
|
1294
|
+
<<~CSS
|
|
1295
|
+
@tailwind base;
|
|
1296
|
+
@tailwind components;
|
|
1297
|
+
@tailwind utilities;
|
|
1298
|
+
CSS
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
def app_js_content
|
|
1302
|
+
<<~JS
|
|
1303
|
+
// Salvia application JavaScript
|
|
1304
|
+
|
|
1305
|
+
// Add custom initialization code here
|
|
1306
|
+
console.log('🌿 Salvia app loaded');
|
|
1307
|
+
JS
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
def error_404_content
|
|
1311
|
+
<<~HTML
|
|
1312
|
+
<!DOCTYPE html>
|
|
1313
|
+
<html>
|
|
1314
|
+
<head>
|
|
1315
|
+
<title>Page Not Found (404)</title>
|
|
1316
|
+
<meta charset="utf-8">
|
|
1317
|
+
<style>
|
|
1318
|
+
body { font-family: system-ui, sans-serif; color: #333; text-align: center; padding: 100px 20px; }
|
|
1319
|
+
h1 { font-size: 3em; margin-bottom: 10px; color: #6A5ACD; }
|
|
1320
|
+
p { font-size: 1.2em; color: #666; }
|
|
1321
|
+
a { color: #6A5ACD; text-decoration: none; }
|
|
1322
|
+
a:hover { text-decoration: underline; }
|
|
1323
|
+
</style>
|
|
1324
|
+
</head>
|
|
1325
|
+
<body>
|
|
1326
|
+
<h1>404</h1>
|
|
1327
|
+
<p>The page you're looking for could not be found.</p>
|
|
1328
|
+
<p><a href="/">Back to Home</a></p>
|
|
1329
|
+
</body>
|
|
1330
|
+
</html>
|
|
1331
|
+
HTML
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
def error_500_content
|
|
1335
|
+
<<~HTML
|
|
1336
|
+
<!DOCTYPE html>
|
|
1337
|
+
<html>
|
|
1338
|
+
<head>
|
|
1339
|
+
<title>Server Error (500)</title>
|
|
1340
|
+
<meta charset="utf-8">
|
|
1341
|
+
<style>
|
|
1342
|
+
body { font-family: system-ui, sans-serif; color: #333; text-align: center; padding: 100px 20px; }
|
|
1343
|
+
h1 { font-size: 3em; margin-bottom: 10px; color: #dc2626; }
|
|
1344
|
+
p { font-size: 1.2em; color: #666; }
|
|
1345
|
+
</style>
|
|
1346
|
+
</head>
|
|
1347
|
+
<body>
|
|
1348
|
+
<h1>500</h1>
|
|
1349
|
+
<p>An internal server error occurred.</p>
|
|
1350
|
+
<p>Please try again later.</p>
|
|
1351
|
+
</body>
|
|
1352
|
+
</html>
|
|
1353
|
+
HTML
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1356
|
+
def development_config_content
|
|
1357
|
+
<<~RUBY
|
|
1358
|
+
# Development configuration
|
|
1359
|
+
#
|
|
1360
|
+
# 推奨サーバー: Puma (スレッドベース、macOS との互換性良好)
|
|
1361
|
+
# bundle exec puma -p 9292
|
|
1362
|
+
#
|
|
1363
|
+
Salvia.logger = Logger.new(STDOUT)
|
|
1364
|
+
Salvia.logger.level = Logger::DEBUG
|
|
1365
|
+
RUBY
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1368
|
+
def production_config_content
|
|
1369
|
+
<<~RUBY
|
|
1370
|
+
# Production configuration
|
|
1371
|
+
#
|
|
1372
|
+
# 推奨サーバー: Falcon (async/fork、高パフォーマンス)
|
|
1373
|
+
# bundle exec falcon serve --bind http://0.0.0.0:9292
|
|
1374
|
+
#
|
|
1375
|
+
# 注意: macOS + PostgreSQL 環境では Falcon は fork の問題があります。
|
|
1376
|
+
# 本番環境では Docker (Linux) を使用してください。
|
|
1377
|
+
#
|
|
1378
|
+
# Docker での起動:
|
|
1379
|
+
# docker-compose up --build
|
|
1380
|
+
#
|
|
1381
|
+
# YJIT の有効化 (Ruby 3.2+):
|
|
1382
|
+
# export RUBY_YJIT_ENABLE=1
|
|
1383
|
+
#
|
|
1384
|
+
log_dir = File.join(Salvia.root, "log")
|
|
1385
|
+
Dir.mkdir(log_dir) unless Dir.exist?(log_dir)
|
|
1386
|
+
|
|
1387
|
+
Salvia.logger = Logger.new(File.join(log_dir, "production.log"))
|
|
1388
|
+
Salvia.logger.level = Logger::INFO
|
|
1389
|
+
RUBY
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
def islands_js_content
|
|
1393
|
+
# gem assets からコピー
|
|
1394
|
+
assets_path = File.expand_path("../../../assets/javascripts/islands.js", __FILE__)
|
|
1395
|
+
File.read(assets_path)
|
|
1396
|
+
end
|
|
1397
|
+
end
|
|
1398
|
+
end
|