inertia-rage 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b02bfd40eee71d4f47b7aa88f120895a078d8f08adfef18002172e9ce7e5a988
4
- data.tar.gz: de0d2658ebc07bcbdfed14aa6d9fe2cc17ca1f8f35f1b17d49a9a28f34c788a1
3
+ metadata.gz: 9d633e20c984eefe3e5c928a6328e17964fb879dc285a81bcda592d9a57e9ea5
4
+ data.tar.gz: a8079309590a94cdb78cb91fe6b380aadb992b0ccd8816a7ff79cb4bcd48c640
5
5
  SHA512:
6
- metadata.gz: f0add134abde83e42eb7a9137654b25a3b8f94d8bbb2f5e81b22b5fe2ee7d99c66cb8df059a4e2085d2752d064ffdb4a69af1c299844ff60474f3e3224d77bb0
7
- data.tar.gz: ce8bcfa86c0a2eba062f04755b005ebfee024f4c6075373a109996f8e862d5b99132ef6692e8cae95475f1b49fd9f1c31657441f5f46c66a5c34d4bf012fb32d
6
+ metadata.gz: e3f2d8f819e89054d3c655eef9784b02f840f12fcf39a7bcadd952b4b5a01e965806d22fdd1baa20714cc8fc021fee44292cd3f2c6fe34a74d5e63e2ecccca58
7
+ data.tar.gz: dc09e729d8f5859faeef881184eb1fa5ec5b2863b2fcd45849ce02f75dcfd53fcf1d726cfaf92772984bd54dd712fbf87c169d1f46affea4404d7da41a1491ae
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2026-09-15
4
+
5
+ - Add support for SSR.
6
+
3
7
  ## [1.0.0] - 2026-07-27
4
8
 
5
9
  - Initial release
data/README.md CHANGED
@@ -213,7 +213,7 @@ See the [API documentation](https://inertia-api.rage-rb.dev/Inertia/Configuratio
213
213
  ## Learn More
214
214
 
215
215
  - [Props API Reference](https://inertia-api.rage-rb.dev/Inertia)
216
- - [Controller API Reference](https://inertia-api.rage-rb.dev/Inertia/ControllerHelpers.html) TODO
216
+ - [Controller API Reference](https://inertia-api.rage-rb.dev/RageController/Inertia.html)
217
217
  - [Configuration API Reference](https://inertia-api.rage-rb.dev/Inertia/Configuration.html)
218
218
 
219
219
  ## Contributing
@@ -90,6 +90,13 @@ module Inertia
90
90
  @dev_server ||= DevServer.new
91
91
  end
92
92
 
93
+ # Returns the SSR configuration.
94
+ #
95
+ # @return [Ssr] the SSR configuration object
96
+ def ssr
97
+ @ssr ||= Ssr.new
98
+ end
99
+
93
100
  ##
94
101
  # Stores configuration for the Vite development server.
95
102
  #
@@ -106,5 +113,48 @@ module Inertia
106
113
  @port = 5173
107
114
  end
108
115
  end
116
+
117
+ ##
118
+ # Stores configuration for server-side rendering.
119
+ #
120
+ class Ssr
121
+ # @return [Boolean] whether SSR is enabled
122
+ attr_accessor :enabled
123
+
124
+ # @return [String] the URL of the SSR server (default: `http://localhost:13714`)
125
+ attr_accessor :url
126
+
127
+ # @return [Pathname, nil] the path to the SSR build output
128
+ attr_accessor :build_path
129
+
130
+ # @private
131
+ def initialize
132
+ @url = "http://localhost:13714"
133
+ end
134
+
135
+ # Sets the path where SSR bundle is located.
136
+ #
137
+ # When set, this overrides the default `ssr` directory inside {Frontend.dist}.
138
+ # The path should be relative to the application root.
139
+ #
140
+ # @param path [String] path relative to the application root
141
+ # @raise [ArgumentError] if path resolves to the public directory
142
+ def build_path=(path)
143
+ candidate_path = Rage.root.join(path)
144
+ public_path = Rage.root.join("public")
145
+
146
+ if candidate_path.to_s.start_with?(public_path.to_s)
147
+ raise ArgumentError, "ssr.build_path cannot be inside public/"
148
+ end
149
+
150
+ @build_path = candidate_path
151
+ end
152
+
153
+ # @private
154
+ def local?
155
+ normalized = URI(@url).hostname.downcase
156
+ %w[localhost localhost. 0.0.0.0 ::1].include?(normalized) || normalized.start_with?("127.")
157
+ end
158
+ end
109
159
  end
110
160
  end
@@ -37,6 +37,26 @@ module Inertia
37
37
  @dist ||= Inertia.config.build_path || root.join("dist")
38
38
  end
39
39
 
40
+ # Returns the directory containing built SSR assets.
41
+ #
42
+ # Uses {Configuration::Ssr#build_path} if set, otherwise defaults to
43
+ # `dist/ssr` inside the frontend root.
44
+ #
45
+ # @return [Pathname] path to the build output directory
46
+ # @raise [RuntimeError] if no Vite config file is found
47
+ def ssr_dist
48
+ @ssr_dist ||= begin
49
+ path = Inertia.config.ssr.build_path || dist.join("ssr")
50
+ public_path = Rage.root.join("public")
51
+
52
+ if path.to_s.start_with?(public_path.to_s)
53
+ raise ArgumentError, "SSR build path cannot be inside public/; configure it explicitly via config.ssr.build_path"
54
+ end
55
+
56
+ path
57
+ end
58
+ end
59
+
40
60
  # Returns a version identifier for the frontend assets.
41
61
  #
42
62
  # Computes an MD5 hash of the Vite manifest or index.html to detect
@@ -76,6 +96,29 @@ module Inertia
76
96
  @package_runner
77
97
  end
78
98
 
99
+ # Returns the JavaScript runtime command.
100
+ #
101
+ # Detects the runtime by checking for lock files and returns
102
+ # the appropriate command to execute scripts.
103
+ #
104
+ # @return [String] runtime command (e.g., "node", "bun", "deno run")
105
+ # @raise [RuntimeError] if no supported runtime is detected
106
+ def runtime
107
+ @runtime ||= if root.join("package-lock.json").exist?
108
+ "node"
109
+ elsif root.join("pnpm-lock.yaml").exist?
110
+ "node"
111
+ elsif root.join("bun.lockb").exist? || root.join("bun.lock").exist?
112
+ "bun"
113
+ elsif root.join("yarn.lock").exist?
114
+ "yarn node"
115
+ elsif root.join("deno.lock").exist?
116
+ "deno run --allow-net --allow-env"
117
+ else
118
+ raise "No supported JavaScript runtime detected"
119
+ end
120
+ end
121
+
79
122
  # Renders the HTML layout with the Inertia page object embedded.
80
123
  #
81
124
  # In development, it fetches the layout from the Vite dev server and
@@ -122,7 +165,7 @@ module Inertia
122
165
  "from \"#{dev_server_url}/#{$2}\""
123
166
  end
124
167
 
125
- inject_page_data(layout, data)
168
+ hydrate_layout(layout, data)
126
169
  end
127
170
 
128
171
  # Returns the cached static layout with page data.
@@ -131,13 +174,46 @@ module Inertia
131
174
  # @return [String] HTML with page data injected
132
175
  # @raise [RuntimeError] if index.html does not exist in build path
133
176
  def build_static_layout(data)
134
- @layout ||= begin
177
+ @static_layout ||= begin
135
178
  layout = dist.join("index.html")
136
179
  raise "Production layout not found at #{layout}. Ensure the frontend has been built" unless layout.exist?
137
180
  layout.read
138
181
  end
139
182
 
140
- inject_page_data(@layout, data)
183
+ hydrate_layout(@static_layout, data)
184
+ end
185
+
186
+ def hydrate_layout(layout, data)
187
+ if Inertia.config.ssr.enabled
188
+ ssr_layout = inject_ssr_data(layout, data)
189
+ return ssr_layout if ssr_layout
190
+ end
191
+
192
+ inject_page_data(layout, data)
193
+ end
194
+
195
+ def inject_ssr_data(layout, data)
196
+ ssr_data = Inertia::SSR::Client.render(data)
197
+
198
+ processed_layout = layout.sub(/<div\s[^>]*\bid=(["'])app\1[^>]*>\s*<\/div>/i) { ssr_data["body"] }
199
+ # the app container is not empty - fallback to CSR
200
+ return nil if processed_layout == layout
201
+
202
+ if (head = ssr_data["head"]).any?
203
+ processed_layout.sub!(/<head([^>]*)>/i) do |head_tag|
204
+ <<~HTML
205
+ #{head_tag}
206
+ #{ssr_data["head"].join}
207
+ HTML
208
+ end
209
+ end
210
+
211
+ processed_layout
212
+
213
+ rescue => e
214
+ Rage.logger.error("SSR render failed", exception: e.message)
215
+ Rage::Errors.report(e)
216
+ nil
141
217
  end
142
218
 
143
219
  # Injects the page object JSON into the HTML body.
@@ -83,6 +83,14 @@ module Inertia
83
83
  # @!endgroup
84
84
 
85
85
  autoload :ViteDevServer, "inertia/vite_dev_server"
86
+
87
+ module SSR
88
+ autoload :Client, "inertia/ssr/client"
89
+ autoload :Server, "inertia/ssr/server"
90
+ end
91
+
92
+ class SSRServerError < StandardError
93
+ end
86
94
  end
87
95
 
88
96
  module RageController
@@ -15,6 +15,11 @@ module Inertia
15
15
  # Automatically start Vite dev server in development mode
16
16
  config.daemons << ViteDevServer if Rage.env.development?
17
17
 
18
+ # Automatically start the SSR server
19
+ after_initialize do
20
+ config.daemons << SSR::Server if Inertia.config.ssr.enabled && Inertia.config.ssr.local? && !Rage.env.development?
21
+ end
22
+
18
23
  # In production, serve prebuilt static assets via the public file server and Assets middleware
19
24
  unless Rage.env.development?
20
25
  config.public_file_server.enabled = true
@@ -36,6 +41,15 @@ module Inertia
36
41
  puts "INFO: Building frontend"
37
42
  puts ""
38
43
  system("#{Frontend.package_runner} vite build", chdir: Frontend.root) || abort("ERROR: Frontend build failed")
44
+
45
+ if Inertia.config.ssr.enabled
46
+ if Inertia.config.ssr.local?
47
+ out_dir = Frontend.ssr_dist.relative_path_from(Frontend.root)
48
+ system("#{Frontend.package_runner} vite build --ssr --outDir #{out_dir}", chdir: Frontend.root) || abort("ERROR: SSR build failed")
49
+ else
50
+ puts "INFO: SSR server is remote - skipping SSR build"
51
+ end
52
+ end
39
53
  end
40
54
  end
41
55
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module Inertia
6
+ module SSR
7
+ ##
8
+ # HTTP client for communicating with the SSR server.
9
+ #
10
+ # Sends page data to the SSR server and receives rendered HTML
11
+ # containing the `head` and `body` fragments.
12
+ #
13
+ class Client
14
+ class << self
15
+ # Renders the Inertia page data on the SSR server.
16
+ #
17
+ # @param data [Hash] the Inertia page object to render
18
+ # @return [Hash] parsed response containing "head" and "body" keys
19
+ def render(data)
20
+ response = begin
21
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", open_timeout: 1, read_timeout: 1) do |http|
22
+ http.post(uri.request_uri, data.to_json, "Content-Type" => "application/json")
23
+ end
24
+ rescue => e
25
+ raise Inertia::SSRServerError, e
26
+ end
27
+
28
+ unless response.code == "200"
29
+ raise Inertia::SSRServerError, "Request failed with status #{response.code}: #{response.body}"
30
+ end
31
+
32
+ JSON.parse(response.body)
33
+ end
34
+
35
+ private
36
+
37
+ # Returns the URI for the SSR endpoint.
38
+ #
39
+ # In development, uses the Vite dev server's SSR endpoint.
40
+ # In production, uses the configured {Configuration::Ssr#url}.
41
+ #
42
+ # @return [URI] the SSR server URI
43
+ def uri
44
+ @uri ||= if Rage.env.development?
45
+ dev_server_config = Inertia.config.dev_server
46
+ URI("http://#{dev_server_config.host}:#{dev_server_config.port}/__inertia_ssr")
47
+ else
48
+ uri = URI(Inertia.config.ssr.url)
49
+ uri.path = "/render" if uri.path.empty? || uri.path == "/"
50
+ uri
51
+ end
52
+ end
53
+ end # class << self
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inertia
4
+ module SSR
5
+ ##
6
+ # Background daemon that runs the Node.js SSR server.
7
+ #
8
+ class Server < Rage::Daemon
9
+ def perform
10
+ puts "INFO: Starting SSR server"
11
+
12
+ ssr_bundle = Frontend.ssr_dist.glob("*.{js,mjs}").first
13
+
14
+ unless ssr_bundle&.exist?
15
+ Rage.logger.fatal("Could not start SSR server - bundle not found")
16
+ return Rage::Daemon::Stop
17
+ end
18
+
19
+ @pid = Process.spawn("#{Frontend.runtime} #{ssr_bundle.relative_path_from(Frontend.root)}", chdir: Frontend.root)
20
+ Process.wait(@pid)
21
+ end
22
+
23
+ def cleanup
24
+ Process.kill("TERM", @pid) if @pid
25
+ rescue Errno::ESRCH
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Inertia
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inertia-rage
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Samoilov
@@ -49,6 +49,8 @@ files:
49
49
  - lib/inertia/renderer.rb
50
50
  - lib/inertia/request_context.rb
51
51
  - lib/inertia/rspec.rb
52
+ - lib/inertia/ssr/client.rb
53
+ - lib/inertia/ssr/server.rb
52
54
  - lib/inertia/version.rb
53
55
  - lib/inertia/vite_dev_server.rb
54
56
  homepage: https://rage-rb.dev