salvia 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +152 -0
- data/assets/components/Island.tsx +15 -0
- data/assets/islands/Counter.tsx +17 -0
- data/assets/javascripts/islands.js +55 -0
- data/assets/pages/Home.tsx +19 -0
- data/assets/scripts/build.ts +298 -0
- data/assets/scripts/deno.json +15 -0
- data/assets/scripts/deno.lock +56 -0
- data/assets/scripts/sidecar.ts +167 -0
- data/assets/scripts/vendor_setup.ts +25 -0
- data/exe/salvia +8 -0
- data/lib/salvia/cli.rb +189 -0
- data/lib/salvia/compiler/adapters/deno_sidecar.rb +23 -0
- data/lib/salvia/compiler.rb +29 -0
- data/lib/salvia/core/configuration.rb +30 -0
- data/lib/salvia/core/error.rb +7 -0
- data/lib/salvia/core/import_map.rb +36 -0
- data/lib/salvia/core/path_resolver.rb +43 -0
- data/lib/salvia/helpers/island.rb +251 -0
- data/lib/salvia/helpers/tag.rb +47 -0
- data/lib/salvia/helpers.rb +8 -0
- data/lib/salvia/railtie.rb +33 -0
- data/lib/salvia/server/dev_server.rb +83 -0
- data/lib/salvia/server/sidecar.rb +136 -0
- data/lib/salvia/server/sidecar.ts +167 -0
- data/lib/salvia/ssr/dom_mock.rb +46 -0
- data/lib/salvia/ssr/quickjs.rb +282 -0
- data/lib/salvia/ssr.rb +119 -0
- data/lib/salvia/version.rb +5 -0
- data/lib/salvia.rb +68 -0
- metadata +165 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Salvia
|
|
6
|
+
module Helpers
|
|
7
|
+
module Island
|
|
8
|
+
# Island マニフェストをキャッシュ
|
|
9
|
+
@manifest = nil
|
|
10
|
+
@manifest_mtime = nil
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# マニフェストを読み込む
|
|
14
|
+
def load_manifest
|
|
15
|
+
manifest_path = File.join(Dir.pwd, "salvia/server/manifest.json")
|
|
16
|
+
return {} unless File.exist?(manifest_path)
|
|
17
|
+
|
|
18
|
+
mtime = File.mtime(manifest_path)
|
|
19
|
+
if @manifest.nil? || @manifest_mtime != mtime
|
|
20
|
+
@manifest = JSON.parse(File.read(manifest_path))
|
|
21
|
+
@manifest_mtime = mtime
|
|
22
|
+
end
|
|
23
|
+
@manifest
|
|
24
|
+
rescue => e
|
|
25
|
+
{}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Island が client only かどうか
|
|
29
|
+
def client_only?(name)
|
|
30
|
+
manifest = load_manifest
|
|
31
|
+
manifest.dig(name, "clientOnly") == true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Island が server only かどうか
|
|
35
|
+
def server_only?(name)
|
|
36
|
+
manifest = load_manifest
|
|
37
|
+
manifest.dig(name, "serverOnly") == true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Import Map タグを生成する
|
|
42
|
+
def salvia_import_map(additional_map = {})
|
|
43
|
+
default_map = {
|
|
44
|
+
"imports" => {
|
|
45
|
+
"preact" => "https://esm.sh/preact@10.19.6",
|
|
46
|
+
"preact/hooks" => "https://esm.sh/preact@10.19.6/hooks",
|
|
47
|
+
"preact/jsx-runtime" => "https://esm.sh/preact@10.19.6/jsx-runtime",
|
|
48
|
+
"@hotwired/turbo" => "https://esm.sh/@hotwired/turbo@8.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Islands path mapping
|
|
53
|
+
islands_path = if defined?(Salvia.env) && Salvia.env == "development"
|
|
54
|
+
"/salvia/assets/islands/"
|
|
55
|
+
else
|
|
56
|
+
"/assets/islands/"
|
|
57
|
+
end
|
|
58
|
+
default_map["imports"]["@/islands/"] = islands_path
|
|
59
|
+
|
|
60
|
+
# deno.json から imports を読み込む
|
|
61
|
+
begin
|
|
62
|
+
deno_json_path = File.join(Salvia.root, "salvia/deno.json")
|
|
63
|
+
if File.exist?(deno_json_path)
|
|
64
|
+
deno_config = JSON.parse(File.read(deno_json_path))
|
|
65
|
+
if deno_config["imports"]
|
|
66
|
+
# npm: スキームを https://esm.sh/ に変換してブラウザで使えるようにする
|
|
67
|
+
imports = deno_config["imports"].transform_values do |v|
|
|
68
|
+
if v.is_a?(String) && v.start_with?("npm:")
|
|
69
|
+
package = v.sub("npm:", "")
|
|
70
|
+
"https://esm.sh/#{package}"
|
|
71
|
+
else
|
|
72
|
+
v
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
default_map["imports"].merge!(imports)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
rescue => e
|
|
79
|
+
# 読み込みエラー時は無視
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if additional_map.key?("imports")
|
|
83
|
+
default_map["imports"].merge!(additional_map["imports"])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
additional_map.each do |k, v|
|
|
87
|
+
next if k == "imports"
|
|
88
|
+
default_map[k] = v
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
html = <<~HTML
|
|
92
|
+
<script type="importmap">
|
|
93
|
+
#{default_map.to_json}
|
|
94
|
+
</script>
|
|
95
|
+
HTML
|
|
96
|
+
|
|
97
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Island コンポーネントをレンダリングする
|
|
101
|
+
#
|
|
102
|
+
# SSR が有効な場合はサーバーサイドで HTML を生成し、
|
|
103
|
+
# クライアントサイドでハイドレーションを行います。
|
|
104
|
+
#
|
|
105
|
+
# @param name [String] コンポーネント名 (例: "Counter")
|
|
106
|
+
# @param props [Hash] コンポーネントに渡すプロパティ
|
|
107
|
+
# @param options [Hash] オプション
|
|
108
|
+
# @option options [String] :id 要素のID
|
|
109
|
+
# @option options [String] :tag ラッパータグ (デフォルト: div)
|
|
110
|
+
# @option options [Boolean] :ssr SSR を有効にするか (デフォルト: auto)
|
|
111
|
+
# @option options [Boolean] :hydrate クライアントサイドでハイドレーションするか (デフォルト: true)
|
|
112
|
+
# @return [String] レンダリングされた HTML
|
|
113
|
+
#
|
|
114
|
+
# @example 基本的な使用法
|
|
115
|
+
# <%= island "Counter", count: 5 %>
|
|
116
|
+
#
|
|
117
|
+
# @example SSR を明示的に無効化
|
|
118
|
+
# <%= island "HeavyChart", data: @data, ssr: false %>
|
|
119
|
+
#
|
|
120
|
+
# @example ハイドレーションを無効化 (静的 HTML のみ)
|
|
121
|
+
# <%= island "StaticCard", title: "Hello", hydrate: false %>
|
|
122
|
+
#
|
|
123
|
+
def island(name, props = {}, options = {})
|
|
124
|
+
tag_name = options.delete(:tag) || :div
|
|
125
|
+
|
|
126
|
+
# デフォルトの hydrate 値を決定
|
|
127
|
+
# serverOnly (app/pages) の場合はデフォルトで false
|
|
128
|
+
default_hydrate = !Island.server_only?(name)
|
|
129
|
+
hydrate = options.fetch(:hydrate, default_hydrate)
|
|
130
|
+
|
|
131
|
+
# SSR 有効/無効の判定
|
|
132
|
+
# 1. options[:ssr] が明示的に指定されていればそれを使う
|
|
133
|
+
# 2. マニフェストで "client only" ならば SSR 無効
|
|
134
|
+
# 3. デフォルトは SSR 有効
|
|
135
|
+
ssr_enabled = if options.key?(:ssr)
|
|
136
|
+
options[:ssr]
|
|
137
|
+
else
|
|
138
|
+
!Island.client_only?(name)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# 開発モードかどうか
|
|
142
|
+
development = defined?(Salvia.env) ? Salvia.env == "development" : true
|
|
143
|
+
|
|
144
|
+
# SSR でコンテンツを生成
|
|
145
|
+
inner_html = ""
|
|
146
|
+
begin
|
|
147
|
+
if ssr_enabled && defined?(Salvia::SSR) && Salvia::SSR.respond_to?(:configured?) && Salvia::SSR.configured?
|
|
148
|
+
inner_html = Salvia::SSR.render(name, props)
|
|
149
|
+
end
|
|
150
|
+
rescue => e
|
|
151
|
+
# SSR 失敗時はエラーをログに出力し、CSR にフォールバック
|
|
152
|
+
if development
|
|
153
|
+
inner_html = ssr_error_inline(name, e.message)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# データ属性を構築
|
|
158
|
+
data_attrs = {}
|
|
159
|
+
|
|
160
|
+
if hydrate
|
|
161
|
+
data_attrs[:island] = name
|
|
162
|
+
data_attrs[:props] = props.to_json
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# 開発モードではデバッグ用の属性を追加
|
|
166
|
+
if development
|
|
167
|
+
data_attrs[:salvia_debug] = true
|
|
168
|
+
data_attrs[:salvia_component] = name
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# HTML オプションを構築
|
|
172
|
+
html_options = options.dup
|
|
173
|
+
html_options.delete(:ssr)
|
|
174
|
+
html_options.delete(:hydrate)
|
|
175
|
+
html_options[:data] = (html_options[:data] || {}).merge(data_attrs)
|
|
176
|
+
|
|
177
|
+
# 開発モードではインスペクター用のクラスを追加
|
|
178
|
+
if development
|
|
179
|
+
html_options[:class] = [html_options[:class], "salvia-island"].compact.join(" ")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
result = build_tag(tag_name, html_options) { inner_html }
|
|
183
|
+
result.respond_to?(:html_safe) ? result.html_safe : result
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# ページコンポーネントをレンダリングする (Full JSX Architecture用)
|
|
187
|
+
#
|
|
188
|
+
# <!DOCTYPE html> を付与し、ルート要素としてレンダリングします。
|
|
189
|
+
# また、<head> タグが存在する場合、自動的に Import Map を注入します。
|
|
190
|
+
#
|
|
191
|
+
# @param name [String] ページコンポーネント名 (例: "pages/Home")
|
|
192
|
+
# @param props [Hash] プロパティ
|
|
193
|
+
# @return [String] 完全な HTML 文字列
|
|
194
|
+
def ssr(name, props = {})
|
|
195
|
+
# SSR で HTML を生成
|
|
196
|
+
html = Salvia::SSR.render(name, props)
|
|
197
|
+
|
|
198
|
+
# <head> がある場合、Import Map を自動注入
|
|
199
|
+
if html.include?("</head>")
|
|
200
|
+
import_map_html = salvia_import_map
|
|
201
|
+
html = html.sub("</head>", "#{import_map_html}</head>")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
result = "<!DOCTYPE html>\n" + html
|
|
205
|
+
result.respond_to?(:html_safe) ? result.html_safe : result
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# 後方互換性のため
|
|
209
|
+
alias_method :salvia_page, :ssr
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def build_tag(name, options)
|
|
214
|
+
content = block_given? ? yield : ""
|
|
215
|
+
attrs = options.map do |key, value|
|
|
216
|
+
if key == :data && value.is_a?(Hash)
|
|
217
|
+
value.map { |k, v| " data-#{k.to_s.gsub('_', '-')}=\"#{escape_html(v)}\"" }.join
|
|
218
|
+
else
|
|
219
|
+
" #{key}=\"#{escape_html(value)}\""
|
|
220
|
+
end
|
|
221
|
+
end.join
|
|
222
|
+
|
|
223
|
+
"<#{name}#{attrs}>#{content}</#{name}>"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# インラインエラー表示 (開発モード用、軽量版)
|
|
227
|
+
def ssr_error_inline(name, message)
|
|
228
|
+
<<~HTML
|
|
229
|
+
<div style="
|
|
230
|
+
background: #fee;
|
|
231
|
+
border: 1px solid #fcc;
|
|
232
|
+
border-radius: 4px;
|
|
233
|
+
padding: 8px 12px;
|
|
234
|
+
font-size: 12px;
|
|
235
|
+
color: #900;
|
|
236
|
+
">
|
|
237
|
+
<strong>⚠️ SSR Error:</strong> #{escape_html(name)} - #{escape_html(message)}
|
|
238
|
+
</div>
|
|
239
|
+
HTML
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def escape_html(str)
|
|
243
|
+
str.to_s
|
|
244
|
+
.gsub("&", "&")
|
|
245
|
+
.gsub("<", "<")
|
|
246
|
+
.gsub(">", ">")
|
|
247
|
+
.gsub('"', """)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Salvia
|
|
4
|
+
module Helpers
|
|
5
|
+
module Tag
|
|
6
|
+
# HTML タグを生成する
|
|
7
|
+
#
|
|
8
|
+
# @param name [Symbol, String] タグ名
|
|
9
|
+
# @param options [Hash] 属性
|
|
10
|
+
# @param block [Proc] コンテンツブロック
|
|
11
|
+
# @return [String] HTML 文字列
|
|
12
|
+
def tag(name, options = {}, &block)
|
|
13
|
+
html_options = options.map do |key, value|
|
|
14
|
+
next if value.nil?
|
|
15
|
+
|
|
16
|
+
if value.is_a?(Hash) && key == :data
|
|
17
|
+
value.map { |k, v| %(data-#{k.to_s.gsub("_", "-")}="#{escape_html(v)}") }.join(" ")
|
|
18
|
+
elsif value == true
|
|
19
|
+
key
|
|
20
|
+
else
|
|
21
|
+
%(#{key}="#{escape_html(value)}")
|
|
22
|
+
end
|
|
23
|
+
end.compact.join(" ")
|
|
24
|
+
|
|
25
|
+
html_options = " " + html_options unless html_options.empty?
|
|
26
|
+
|
|
27
|
+
if block_given?
|
|
28
|
+
content = block.call
|
|
29
|
+
"<#{name}#{html_options}>#{content}</#{name}>"
|
|
30
|
+
else
|
|
31
|
+
"<#{name}#{html_options} />"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def escape_html(str)
|
|
38
|
+
str.to_s
|
|
39
|
+
.gsub("&", "&")
|
|
40
|
+
.gsub("<", "<")
|
|
41
|
+
.gsub(">", ">")
|
|
42
|
+
.gsub('"', """)
|
|
43
|
+
.gsub("'", "'")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Salvia
|
|
4
|
+
class Railtie < ::Rails::Railtie
|
|
5
|
+
initializer "salvia.helpers" do
|
|
6
|
+
ActiveSupport.on_load(:action_view) do
|
|
7
|
+
include Salvia::Helpers
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
ActiveSupport.on_load(:action_controller) do
|
|
11
|
+
include Salvia::Helpers
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
ActiveSupport.on_load(:action_controller_api) do
|
|
15
|
+
include Salvia::Helpers
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
initializer "salvia.configure" do |app|
|
|
20
|
+
# Default configuration for Rails
|
|
21
|
+
Salvia.configure do |config|
|
|
22
|
+
# Use Rails logger
|
|
23
|
+
Salvia.logger = Rails.logger
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
initializer "salvia.middleware" do |app|
|
|
28
|
+
if Rails.env.development?
|
|
29
|
+
app.middleware.use Salvia::Server::DevServer
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Salvia
|
|
2
|
+
module Server
|
|
3
|
+
class DevServer
|
|
4
|
+
def initialize(app)
|
|
5
|
+
@app = app
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
# Only active in development
|
|
10
|
+
unless ENV["RACK_ENV"] == "development" || ENV["RAILS_ENV"] == "development"
|
|
11
|
+
return @app.call(env)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
request = Rack::Request.new(env)
|
|
15
|
+
|
|
16
|
+
if request.path.start_with?("/salvia/assets/")
|
|
17
|
+
handle_asset_request(request)
|
|
18
|
+
else
|
|
19
|
+
@app.call(env)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def handle_asset_request(request)
|
|
26
|
+
# /salvia/assets/islands/Counter.js -> islands/Counter.tsx
|
|
27
|
+
path_info = request.path.sub("/salvia/assets/", "")
|
|
28
|
+
|
|
29
|
+
# Special handling for islands.js (Client Entry)
|
|
30
|
+
if path_info == "javascripts/islands.js"
|
|
31
|
+
return serve_islands_js
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Remove .js extension to find source
|
|
35
|
+
base_name = path_info.sub(/\.js$/, "")
|
|
36
|
+
|
|
37
|
+
source_path = resolve_source_path(base_name)
|
|
38
|
+
|
|
39
|
+
unless source_path
|
|
40
|
+
return [404, { "content-type" => "text/plain" }, ["Not Found: #{path_info}"]]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
# Bundle for browser (ESM)
|
|
45
|
+
# We externalize dependencies that should be handled by Import Map
|
|
46
|
+
externals = Salvia::Core::ImportMap.new.keys
|
|
47
|
+
# Always externalize framework aliases just in case
|
|
48
|
+
externals += ["framework", "framework/hooks", "framework/jsx-runtime"]
|
|
49
|
+
|
|
50
|
+
js_code = Salvia::Compiler.bundle(
|
|
51
|
+
source_path,
|
|
52
|
+
externals: externals.uniq,
|
|
53
|
+
format: "esm"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
[200, { "content-type" => "application/javascript" }, [js_code]]
|
|
57
|
+
rescue => e
|
|
58
|
+
[500, { "content-type" => "text/plain" }, ["Build Error: #{e.message}"]]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def resolve_source_path(name)
|
|
63
|
+
Salvia::Core::PathResolver.resolve(name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def serve_islands_js
|
|
67
|
+
# Check user's islands.js
|
|
68
|
+
user_path = File.join(Salvia.root, "salvia/assets/javascripts/islands.js")
|
|
69
|
+
if File.exist?(user_path)
|
|
70
|
+
return [200, { "content-type" => "application/javascript" }, [File.read(user_path)]]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Fallback to internal islands.js
|
|
74
|
+
internal_path = File.expand_path("../../../assets/javascripts/islands.js", __dir__)
|
|
75
|
+
if File.exist?(internal_path)
|
|
76
|
+
return [200, { "content-type" => "application/javascript" }, [File.read(internal_path)]]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
[404, { "content-type" => "text/plain" }, ["islands.js not found"]]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module Salvia
|
|
7
|
+
module Server
|
|
8
|
+
class Sidecar
|
|
9
|
+
SCRIPT_PATH = File.join(__dir__, "sidecar.ts")
|
|
10
|
+
|
|
11
|
+
def self.instance
|
|
12
|
+
@instance ||= new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@pid = nil
|
|
17
|
+
@port = nil
|
|
18
|
+
at_exit { stop }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
return if running?
|
|
23
|
+
|
|
24
|
+
cmd = ["deno", "run", "--allow-all", SCRIPT_PATH]
|
|
25
|
+
|
|
26
|
+
puts "🚀 Starting Salvia Sidecar..."
|
|
27
|
+
# Spawn process and capture stdout to find the port
|
|
28
|
+
# We use IO.popen to read the output stream
|
|
29
|
+
@io = IO.popen(cmd)
|
|
30
|
+
@pid = @io.pid
|
|
31
|
+
|
|
32
|
+
# Wait for "Listening on http://localhost:PORT/"
|
|
33
|
+
wait_for_port
|
|
34
|
+
|
|
35
|
+
# Detach so it runs in background, but we keep the IO open to read logs if needed
|
|
36
|
+
# Actually, for IO.popen, we shouldn't detach if we want to read from it.
|
|
37
|
+
# But we need to read in a non-blocking way or in a separate thread after finding the port.
|
|
38
|
+
|
|
39
|
+
Thread.new do
|
|
40
|
+
begin
|
|
41
|
+
while line = @io.gets
|
|
42
|
+
# Forward Deno logs to stdout/logger
|
|
43
|
+
puts "[Deno] #{line}"
|
|
44
|
+
end
|
|
45
|
+
rescue IOError
|
|
46
|
+
# Stream closed
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def stop
|
|
52
|
+
return unless @pid
|
|
53
|
+
Process.kill("TERM", @pid)
|
|
54
|
+
@pid = nil
|
|
55
|
+
@port = nil
|
|
56
|
+
@io.close if @io && !@io.closed?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def running?
|
|
60
|
+
return false unless @pid
|
|
61
|
+
Process.getpgid(@pid)
|
|
62
|
+
true
|
|
63
|
+
rescue Errno::ESRCH
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def bundle(entry_point, externals: [], format: "esm", global_name: nil, config_path: nil)
|
|
68
|
+
start unless running?
|
|
69
|
+
|
|
70
|
+
resolved_config_path = File.expand_path(config_path || Salvia.config.deno_config_path, Salvia.root)
|
|
71
|
+
puts "[Salvia] Bundle config path: #{resolved_config_path}"
|
|
72
|
+
|
|
73
|
+
response = request("bundle", {
|
|
74
|
+
entryPoint: entry_point,
|
|
75
|
+
externals: externals,
|
|
76
|
+
format: format,
|
|
77
|
+
globalName: global_name,
|
|
78
|
+
configPath: resolved_config_path
|
|
79
|
+
})
|
|
80
|
+
if response["error"]
|
|
81
|
+
raise "Sidecar Bundle Error: #{response["error"]}"
|
|
82
|
+
end
|
|
83
|
+
response["code"]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def check(entry_point, config_path: nil)
|
|
87
|
+
start unless running?
|
|
88
|
+
resolved_config_path = File.expand_path(config_path || Salvia.config.deno_config_path, Salvia.root)
|
|
89
|
+
request("check", { entryPoint: entry_point, configPath: resolved_config_path })
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def fmt(entry_point, config_path: nil)
|
|
93
|
+
start unless running?
|
|
94
|
+
resolved_config_path = File.expand_path(config_path || Salvia.config.deno_config_path, Salvia.root)
|
|
95
|
+
request("fmt", { entryPoint: entry_point, configPath: resolved_config_path })
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def wait_for_port
|
|
101
|
+
Timeout.timeout(10) do
|
|
102
|
+
while line = @io.gets
|
|
103
|
+
puts "[Deno Init] #{line}"
|
|
104
|
+
if match = line.match(/Listening on http:\/\/localhost:(\d+)\//)
|
|
105
|
+
@port = match[1].to_i
|
|
106
|
+
puts "✅ Salvia Sidecar connected on port #{@port}"
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
rescue Timeout::Error
|
|
112
|
+
stop
|
|
113
|
+
raise "Sidecar failed to start (Timeout waiting for port)"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def request(command, params = {})
|
|
117
|
+
uri = URI("http://localhost:#{@port}/")
|
|
118
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
119
|
+
|
|
120
|
+
request = Net::HTTP::Post.new(uri)
|
|
121
|
+
request.content_type = 'application/json'
|
|
122
|
+
request.body = { command: command, params: params }.to_json
|
|
123
|
+
|
|
124
|
+
response = http.request(request)
|
|
125
|
+
|
|
126
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
127
|
+
JSON.parse(response.body)
|
|
128
|
+
else
|
|
129
|
+
raise "Sidecar Request Failed: #{response.code} #{response.message}"
|
|
130
|
+
end
|
|
131
|
+
rescue => e
|
|
132
|
+
raise "Sidecar Request Error: #{e.message}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|