salvia 0.2.0 → 0.2.2
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/.vscode/settings.json +4 -0
- data/Gemfile +8 -0
- data/Rakefile +12 -0
- data/assets/islands/Counter.tsx +2 -2
- data/assets/javascripts/islands.js +2 -1
- data/assets/scripts/build.ts +91 -41
- data/assets/scripts/deno.json +3 -3
- data/assets/scripts/sidecar.ts +47 -23
- data/assets/scripts/vendor_setup.ts +16 -6
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/salvia/cli.rb +40 -9
- data/lib/salvia/core/import_map.rb +120 -20
- data/lib/salvia/helpers/island.rb +8 -59
- data/lib/salvia/server/dev_server.rb +23 -9
- data/lib/salvia/server/sidecar.rb +72 -35
- data/lib/salvia/ssr/dom_mock.rb +11 -3
- data/lib/salvia/ssr/quickjs.rb +97 -34
- data/lib/salvia/ssr.rb +67 -5
- data/lib/salvia/version.rb +1 -1
- metadata +11 -6
- data/README.md +0 -152
data/lib/salvia/ssr/quickjs.rb
CHANGED
|
@@ -15,21 +15,13 @@ module Salvia
|
|
|
15
15
|
@last_build_error = nil
|
|
16
16
|
@development = options.fetch(:development, true)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
load_console_shim!
|
|
21
|
-
|
|
22
|
-
if @development
|
|
23
|
-
load_vendor_bundle!
|
|
24
|
-
else
|
|
25
|
-
load_ssr_bundle!
|
|
26
|
-
end
|
|
18
|
+
# VM initialization is deferred to thread-local access
|
|
27
19
|
|
|
28
20
|
mark_initialized!
|
|
29
21
|
end
|
|
30
22
|
|
|
31
23
|
def shutdown!
|
|
32
|
-
|
|
24
|
+
Thread.current[:salvia_quickjs_vm] = nil
|
|
33
25
|
@js_logs = []
|
|
34
26
|
@initialized = false
|
|
35
27
|
Salvia::Compiler.shutdown if @development
|
|
@@ -49,7 +41,24 @@ module Salvia
|
|
|
49
41
|
private
|
|
50
42
|
|
|
51
43
|
def vm
|
|
52
|
-
|
|
44
|
+
Thread.current[:salvia_quickjs_vm] ||= create_thread_local_vm
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reset_vm!
|
|
48
|
+
Thread.current[:salvia_quickjs_vm] = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create_thread_local_vm
|
|
52
|
+
new_vm = ::Quickjs::VM.new
|
|
53
|
+
load_console_shim!(new_vm)
|
|
54
|
+
|
|
55
|
+
if @development
|
|
56
|
+
load_vendor_bundle!(new_vm)
|
|
57
|
+
else
|
|
58
|
+
load_ssr_bundle!(new_vm)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
new_vm
|
|
53
62
|
end
|
|
54
63
|
|
|
55
64
|
def render_jit(component_name, props)
|
|
@@ -59,23 +68,57 @@ module Salvia
|
|
|
59
68
|
raise Error, "Component not found: #{component_name}"
|
|
60
69
|
end
|
|
61
70
|
|
|
62
|
-
# Bundle component
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
externals: ["preact", "preact/hooks", "preact-render-to-string"],
|
|
66
|
-
format: "iife",
|
|
67
|
-
global_name: "SalviaComponent"
|
|
68
|
-
)
|
|
71
|
+
# Bundle component (with simple memory cache)
|
|
72
|
+
@bundle_cache ||= {}
|
|
73
|
+
mtime = File.mtime(path)
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
if @bundle_cache[path].nil? || @bundle_cache[path][:mtime] != mtime
|
|
76
|
+
# Load externals from deno.json if possible, otherwise use defaults
|
|
77
|
+
externals = ["preact", "preact/hooks", "preact/jsx-runtime", "preact-render-to-string", "@preact/signals"]
|
|
78
|
+
|
|
72
79
|
begin
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
deno_json_path = File.join(Salvia.root, "salvia/deno.json")
|
|
81
|
+
if File.exist?(deno_json_path)
|
|
82
|
+
deno_config = JSON.parse(File.read(deno_json_path))
|
|
83
|
+
if deno_config["imports"]
|
|
84
|
+
externals = (externals + deno_config["imports"].keys).uniq
|
|
85
|
+
end
|
|
76
86
|
end
|
|
77
87
|
rescue => e
|
|
78
|
-
|
|
88
|
+
log_warn("Failed to load externals from deno.json: #{e.message}")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
js_code = Salvia::Compiler.bundle(
|
|
92
|
+
path,
|
|
93
|
+
externals: externals,
|
|
94
|
+
format: "iife",
|
|
95
|
+
global_name: "SalviaComponent"
|
|
96
|
+
)
|
|
97
|
+
@bundle_cache[path] = { code: js_code, mtime: mtime }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
js_code = @bundle_cache[path][:code]
|
|
101
|
+
|
|
102
|
+
# Async Type Check (Debounced)
|
|
103
|
+
@last_check_time ||= {}
|
|
104
|
+
now = Time.now.to_i
|
|
105
|
+
|
|
106
|
+
# Debounce: Check at most once every 5 seconds per file
|
|
107
|
+
if @last_check_time[path].nil? || (now - @last_check_time[path]) > 5
|
|
108
|
+
@last_check_time[path] = now
|
|
109
|
+
|
|
110
|
+
# Use a background thread but avoid spawning too many
|
|
111
|
+
# Ideally this should be a single worker queue, but for now simple detach is better than nothing
|
|
112
|
+
Thread.new do
|
|
113
|
+
begin
|
|
114
|
+
# Double check inside thread to handle race conditions slightly better
|
|
115
|
+
result = Salvia::Compiler.check(path)
|
|
116
|
+
unless result["success"]
|
|
117
|
+
log_warn("Type Check Failed for #{component_name}:\n#{result["message"]}")
|
|
118
|
+
end
|
|
119
|
+
rescue => e
|
|
120
|
+
log_debug("Type Check Error: #{e.message}")
|
|
121
|
+
end
|
|
79
122
|
end
|
|
80
123
|
end
|
|
81
124
|
|
|
@@ -148,7 +191,7 @@ module Salvia
|
|
|
148
191
|
end
|
|
149
192
|
|
|
150
193
|
def eval_js(code)
|
|
151
|
-
result =
|
|
194
|
+
result = vm.eval_code(code)
|
|
152
195
|
process_console_output
|
|
153
196
|
result
|
|
154
197
|
rescue => e
|
|
@@ -156,21 +199,24 @@ module Salvia
|
|
|
156
199
|
raise e
|
|
157
200
|
end
|
|
158
201
|
|
|
159
|
-
def load_console_shim!
|
|
202
|
+
def load_console_shim!(target_vm)
|
|
160
203
|
shim = generate_console_shim
|
|
161
|
-
|
|
162
|
-
|
|
204
|
+
target_vm.eval_code(shim)
|
|
205
|
+
target_vm.eval_code(Salvia::SSR::DomMock.generate_shim)
|
|
163
206
|
rescue => e
|
|
164
207
|
log_error("Failed to load console shim: #{e.message}")
|
|
165
208
|
end
|
|
166
209
|
|
|
167
|
-
def load_vendor_bundle!
|
|
210
|
+
def load_vendor_bundle!(target_vm)
|
|
168
211
|
# Use internal vendor_setup.ts for Zero Config
|
|
169
212
|
vendor_path = File.expand_path("../../../assets/scripts/vendor_setup.ts", __dir__)
|
|
170
213
|
|
|
171
214
|
if File.exist?(vendor_path)
|
|
215
|
+
# Ensure module.exports shim exists before loading vendor bundle
|
|
216
|
+
target_vm.eval_code("if(typeof module === 'undefined') { globalThis.module = { exports: {} }; }")
|
|
217
|
+
|
|
172
218
|
code = Salvia::Compiler.bundle(vendor_path, format: "iife")
|
|
173
|
-
|
|
219
|
+
target_vm.eval_code(code)
|
|
174
220
|
log_info("Loaded Vendor bundle (Internal)")
|
|
175
221
|
else
|
|
176
222
|
log_error("Internal vendor_setup.ts not found at #{vendor_path}")
|
|
@@ -179,15 +225,27 @@ module Salvia
|
|
|
179
225
|
log_error("Failed to load vendor bundle: #{e.message}")
|
|
180
226
|
end
|
|
181
227
|
|
|
182
|
-
def load_ssr_bundle!
|
|
228
|
+
def load_ssr_bundle!(target_vm)
|
|
183
229
|
bundle_path = options[:bundle_path] || default_bundle_path
|
|
184
230
|
|
|
185
231
|
unless File.exist?(bundle_path)
|
|
186
232
|
raise Error, "SSR bundle not found: #{bundle_path}"
|
|
187
233
|
end
|
|
188
234
|
|
|
235
|
+
# Check if bundle has changed (simple reload strategy)
|
|
236
|
+
mtime = File.mtime(bundle_path)
|
|
237
|
+
if @last_bundle_mtime && @last_bundle_mtime != mtime
|
|
238
|
+
log_info("SSR bundle changed, reloading...")
|
|
239
|
+
# Note: In a real scenario, we might need to reset the VM completely
|
|
240
|
+
# But since this is called inside create_thread_local_vm, we are creating a new VM anyway.
|
|
241
|
+
# However, if we want to support hot reload in production without restarting threads,
|
|
242
|
+
# we would need a mechanism to invalidate all thread-local VMs.
|
|
243
|
+
# For now, we just update the mtime tracking.
|
|
244
|
+
end
|
|
245
|
+
@last_bundle_mtime = mtime
|
|
246
|
+
|
|
189
247
|
bundle_content = File.read(bundle_path)
|
|
190
|
-
|
|
248
|
+
target_vm.eval_code(bundle_content)
|
|
191
249
|
log_info("Loaded SSR bundle: #{bundle_path}")
|
|
192
250
|
end
|
|
193
251
|
|
|
@@ -196,7 +254,7 @@ module Salvia
|
|
|
196
254
|
end
|
|
197
255
|
|
|
198
256
|
def process_console_output
|
|
199
|
-
logs_json =
|
|
257
|
+
logs_json = vm.eval_code("globalThis.__salvia_flush_logs__()")
|
|
200
258
|
return if logs_json.nil? || logs_json.empty?
|
|
201
259
|
|
|
202
260
|
begin
|
|
@@ -270,7 +328,12 @@ module Salvia
|
|
|
270
328
|
end
|
|
271
329
|
|
|
272
330
|
def escape_html(str)
|
|
273
|
-
str.to_s
|
|
331
|
+
str.to_s
|
|
332
|
+
.gsub("&", "&")
|
|
333
|
+
.gsub("<", "<")
|
|
334
|
+
.gsub(">", ">")
|
|
335
|
+
.gsub('"', """)
|
|
336
|
+
.gsub("'", "'")
|
|
274
337
|
end
|
|
275
338
|
|
|
276
339
|
def log_info(msg); defined?(Salvia.logger) ? Salvia.logger.info(msg) : puts("[SSR] #{msg}"); end
|
data/lib/salvia/ssr.rb
CHANGED
|
@@ -62,11 +62,20 @@ module Salvia
|
|
|
62
62
|
|
|
63
63
|
# SSR エンジンを設定
|
|
64
64
|
# @param options [Hash] エンジンオプション
|
|
65
|
+
# @option options [Symbol] :engine エンジン名 (:quickjs)
|
|
65
66
|
# @option options [String] :bundle_path SSR バンドルのパス
|
|
66
67
|
# @option options [Boolean] :development 開発モード
|
|
67
68
|
def configure(options = {})
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
engine = options.fetch(:engine, :quickjs)
|
|
70
|
+
|
|
71
|
+
case engine
|
|
72
|
+
when :quickjs
|
|
73
|
+
require_relative "ssr/quickjs"
|
|
74
|
+
@current_adapter = QuickJS.new(options)
|
|
75
|
+
else
|
|
76
|
+
raise EngineNotFoundError, "Unknown SSR engine: #{engine}"
|
|
77
|
+
end
|
|
78
|
+
|
|
70
79
|
@current_adapter.setup!
|
|
71
80
|
@current_adapter
|
|
72
81
|
end
|
|
@@ -76,19 +85,45 @@ module Salvia
|
|
|
76
85
|
# @param props [Hash] プロパティ
|
|
77
86
|
# @return [String] レンダリングされた HTML
|
|
78
87
|
def render(component_name, props = {})
|
|
79
|
-
|
|
88
|
+
ensure_configured!
|
|
80
89
|
current_adapter.render(component_name, props)
|
|
81
90
|
end
|
|
82
91
|
|
|
92
|
+
# ページコンポーネントをレンダリング (Import Map 注入 + DOCTYPE)
|
|
93
|
+
# @param component_name [String] コンポーネント名
|
|
94
|
+
# @param props [Hash] プロパティ
|
|
95
|
+
# @param options [Hash] オプション
|
|
96
|
+
# @return [String] 完全な HTML 文字列
|
|
97
|
+
def render_page(component_name, props = {}, options = {})
|
|
98
|
+
html = render(component_name, props)
|
|
99
|
+
|
|
100
|
+
# <head> がある場合、Import Map を自動注入
|
|
101
|
+
if html.include?("</head>")
|
|
102
|
+
map = Salvia::Core::ImportMap.generate
|
|
103
|
+
import_map_html = <<~HTML
|
|
104
|
+
<script type="importmap">
|
|
105
|
+
#{map.to_json}
|
|
106
|
+
</script>
|
|
107
|
+
HTML
|
|
108
|
+
html = html.sub("</head>", "#{import_map_html}</head>")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if options.fetch(:doctype, true)
|
|
112
|
+
html = "<!DOCTYPE html>\n" + html
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
html
|
|
116
|
+
end
|
|
117
|
+
|
|
83
118
|
# コンポーネントを登録
|
|
84
119
|
def register_component(name, code)
|
|
85
|
-
|
|
120
|
+
ensure_configured!
|
|
86
121
|
current_adapter.register_component(name, code)
|
|
87
122
|
end
|
|
88
123
|
|
|
89
124
|
# バンドルをリロード (開発モード用)
|
|
90
125
|
def reload!
|
|
91
|
-
return unless
|
|
126
|
+
return unless configured?
|
|
92
127
|
current_adapter.reload_bundle! if current_adapter.respond_to?(:reload_bundle!)
|
|
93
128
|
end
|
|
94
129
|
|
|
@@ -98,6 +133,16 @@ module Salvia
|
|
|
98
133
|
current_adapter.last_build_error = error if current_adapter&.respond_to?(:last_build_error=)
|
|
99
134
|
end
|
|
100
135
|
|
|
136
|
+
# ビルドエラーを取得
|
|
137
|
+
def build_error
|
|
138
|
+
@last_build_error || (current_adapter&.respond_to?(:last_build_error) ? current_adapter.last_build_error : nil)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ビルドエラーがあるか確認
|
|
142
|
+
def build_error?
|
|
143
|
+
!build_error.nil?
|
|
144
|
+
end
|
|
145
|
+
|
|
101
146
|
# ビルドエラーをクリア
|
|
102
147
|
def clear_build_error
|
|
103
148
|
@last_build_error = nil
|
|
@@ -114,6 +159,23 @@ module Salvia
|
|
|
114
159
|
def configured?
|
|
115
160
|
!current_adapter.nil? && current_adapter.initialized?
|
|
116
161
|
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def ensure_configured!
|
|
166
|
+
return if configured?
|
|
167
|
+
|
|
168
|
+
# Auto-configure using defaults from Salvia.config
|
|
169
|
+
# This handles cases where Salvia.configure wasn't explicitly called
|
|
170
|
+
# or called before SSR module was fully loaded
|
|
171
|
+
configure(
|
|
172
|
+
bundle_path: Salvia.config.ssr_bundle_path,
|
|
173
|
+
development: Salvia.development?
|
|
174
|
+
)
|
|
175
|
+
rescue => e
|
|
176
|
+
# If auto-configuration fails, we raise the original error to avoid masking it
|
|
177
|
+
raise Error, "SSR not configured and auto-configuration failed: #{e.message}. Call Salvia::SSR.configure explicitly."
|
|
178
|
+
end
|
|
117
179
|
end
|
|
118
180
|
end
|
|
119
181
|
end
|
data/lib/salvia/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: salvia
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hiroto Furugen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -94,8 +94,9 @@ dependencies:
|
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '5.0'
|
|
97
|
-
description: A standalone SSR engine for Ruby
|
|
98
|
-
Rack application without
|
|
97
|
+
description: A standalone SSR engine for Ruby that brings Islands Architecture to
|
|
98
|
+
any Rack application. Write JSX/TSX components and render them server-side without
|
|
99
|
+
Node.js, JSON APIs, or complex build steps.
|
|
99
100
|
email:
|
|
100
101
|
- hiro_genfuru0119@icloud.com
|
|
101
102
|
executables:
|
|
@@ -103,8 +104,10 @@ executables:
|
|
|
103
104
|
extensions: []
|
|
104
105
|
extra_rdoc_files: []
|
|
105
106
|
files:
|
|
107
|
+
- ".vscode/settings.json"
|
|
108
|
+
- Gemfile
|
|
106
109
|
- LICENSE.txt
|
|
107
|
-
-
|
|
110
|
+
- Rakefile
|
|
108
111
|
- assets/components/Island.tsx
|
|
109
112
|
- assets/islands/Counter.tsx
|
|
110
113
|
- assets/javascripts/islands.js
|
|
@@ -114,6 +117,8 @@ files:
|
|
|
114
117
|
- assets/scripts/deno.lock
|
|
115
118
|
- assets/scripts/sidecar.ts
|
|
116
119
|
- assets/scripts/vendor_setup.ts
|
|
120
|
+
- bin/console
|
|
121
|
+
- bin/setup
|
|
117
122
|
- exe/salvia
|
|
118
123
|
- lib/salvia.rb
|
|
119
124
|
- lib/salvia/cli.rb
|
|
@@ -161,5 +166,5 @@ requirements: []
|
|
|
161
166
|
rubygems_version: 3.4.19
|
|
162
167
|
signing_key:
|
|
163
168
|
specification_version: 4
|
|
164
|
-
summary: Ruby Islands Architecture Engine
|
|
169
|
+
summary: 'Ruby Islands Architecture Engine: SSR with JSX/TSX for Rails & Rack'
|
|
165
170
|
test_files: []
|
data/README.md
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# Salvia 🌿
|
|
2
|
-
|
|
3
|
-
> **The Future of Rails View Layer**
|
|
4
|
-
|
|
5
|
-
Salvia is a next-generation **Server-Side Rendering (SSR) engine** designed to replace ERB with **JSX/TSX** in Ruby on Rails. It brings the **Islands Architecture** and **True HTML First** philosophy to the Rails ecosystem.
|
|
6
|
-
|
|
7
|
-
<img src="https://img.shields.io/gem/v/salvia?style=flat-square&color=ff6347" alt="Gem">
|
|
8
|
-
|
|
9
|
-
## Vision: The Road to Sage
|
|
10
|
-
|
|
11
|
-
Salvia is the core engine for a future MVC framework called **Sage**.
|
|
12
|
-
While Sage will be a complete standalone framework, Salvia is available *today* as a drop-in replacement for the View layer in **Ruby on Rails**.
|
|
13
|
-
|
|
14
|
-
## Features
|
|
15
|
-
|
|
16
|
-
* 🏝️ **Islands Architecture**: Render interactive components (Preact/React) only where needed. Zero JS for static content.
|
|
17
|
-
* 🚀 **True HTML First**: Replace `app/views/**/*.erb` with `app/pages/**/*.tsx`.
|
|
18
|
-
* ⚡ **JIT Compilation**: No build steps during development. Just run `rails s`.
|
|
19
|
-
* 💎 **Rails Native**: Seamless integration with Controllers, Routes, and Models.
|
|
20
|
-
* 🦕 **Deno Powered**: Uses Deno for lightning-fast TypeScript compilation and formatting.
|
|
21
|
-
|
|
22
|
-
## Installation
|
|
23
|
-
|
|
24
|
-
Add this line to your Rails application's Gemfile:
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
gem 'salvia'
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
And then execute:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
$ bundle install
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Getting Started
|
|
37
|
-
|
|
38
|
-
### 1. Install Salvia
|
|
39
|
-
|
|
40
|
-
Run the interactive installer to set up Salvia for your Rails project:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
$ bundle exec salvia install
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
This creates the `salvia/` directory structure and configures your app with a **Zero Config** setup (Preact + Signals).
|
|
47
|
-
|
|
48
|
-
### 2. Create a Page (Server Component)
|
|
49
|
-
|
|
50
|
-
Delete `app/views/home/index.html.erb` and create `salvia/app/pages/home/Index.tsx`:
|
|
51
|
-
|
|
52
|
-
```tsx
|
|
53
|
-
import { h } from 'preact';
|
|
54
|
-
|
|
55
|
-
export default function Home({ title }) {
|
|
56
|
-
return (
|
|
57
|
-
<div class="p-10">
|
|
58
|
-
<h1 class="text-3xl font-bold">{title}</h1>
|
|
59
|
-
<p>This is rendered on the server with 0kb JavaScript sent to the client.</p>
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### 3. Render in Controller
|
|
66
|
-
|
|
67
|
-
In your Rails controller:
|
|
68
|
-
|
|
69
|
-
```ruby
|
|
70
|
-
class HomeController < ApplicationController
|
|
71
|
-
def index
|
|
72
|
-
# Renders salvia/app/pages/home/Index.tsx
|
|
73
|
-
render html: ssr("home/Index", title: "Hello Salvia")
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### 4. Add Interactivity (Islands)
|
|
79
|
-
|
|
80
|
-
Create an interactive component in `salvia/app/islands/Counter.tsx`:
|
|
81
|
-
|
|
82
|
-
```tsx
|
|
83
|
-
import { h } from 'preact';
|
|
84
|
-
import { useState } from 'preact/hooks';
|
|
85
|
-
|
|
86
|
-
export default function Counter() {
|
|
87
|
-
const [count, setCount] = useState(0);
|
|
88
|
-
return (
|
|
89
|
-
<button onClick={() => setCount(count + 1)} class="btn">
|
|
90
|
-
Count: {count}
|
|
91
|
-
</button>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Use it in your Page:
|
|
97
|
-
|
|
98
|
-
```tsx
|
|
99
|
-
import Counter from '../../islands/Counter.tsx';
|
|
100
|
-
|
|
101
|
-
export default function Home() {
|
|
102
|
-
return (
|
|
103
|
-
<div>
|
|
104
|
-
<h1>Interactive Island</h1>
|
|
105
|
-
<Counter />
|
|
106
|
-
</div>
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### 4. Turbo Drive (Optional)
|
|
112
|
-
|
|
113
|
-
Salvia works seamlessly with Turbo Drive for SPA-like navigation.
|
|
114
|
-
|
|
115
|
-
Add Turbo to your layout file (e.g., `app/pages/layouts/Main.tsx`):
|
|
116
|
-
|
|
117
|
-
```tsx
|
|
118
|
-
<head>
|
|
119
|
-
{/* ... */}
|
|
120
|
-
<script type="module">
|
|
121
|
-
import * as Turbo from "https://esm.sh/@hotwired/turbo@8.0.0";
|
|
122
|
-
Turbo.start();
|
|
123
|
-
</script>
|
|
124
|
-
</head>
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
This approach leverages Import Maps and browser-native modules, keeping your bundle size small and your architecture transparent.
|
|
128
|
-
|
|
129
|
-
## Documentation
|
|
130
|
-
|
|
131
|
-
* **English**:
|
|
132
|
-
* [**Wisdom for Salvia**](docs/en/DESIGN.md): Deep dive into the architecture, directory structure, and "True HTML First" philosophy.
|
|
133
|
-
* [**Architecture**](docs/en/ARCHITECTURE.md): Internal design of the gem.
|
|
134
|
-
* **Japanese (日本語)**:
|
|
135
|
-
* [**Salviaの知恵**](docs/ja/DESIGN.md): アーキテクチャ、ディレクトリ構造、「真のHTMLファースト」哲学についての詳細。
|
|
136
|
-
* [**アーキテクチャ**](docs/ja/ARCHITECTURE.md): Gemの内部設計。
|
|
137
|
-
|
|
138
|
-
## Framework Support
|
|
139
|
-
|
|
140
|
-
Salvia is primarily designed for **Ruby on Rails** to pave the way for the **Sage** framework.
|
|
141
|
-
|
|
142
|
-
* **Ruby on Rails**: First-class support.
|
|
143
|
-
|
|
144
|
-
## Requirements
|
|
145
|
-
|
|
146
|
-
* Ruby 3.1+
|
|
147
|
-
* Rails 7.0+ (Recommended)
|
|
148
|
-
* Deno (for JIT compilation and tooling)
|
|
149
|
-
|
|
150
|
-
## License
|
|
151
|
-
|
|
152
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|