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.
@@ -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