inertia-rage 0.0.1 → 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: 1a6b20a9b2fd94ff4fa3e0595f894f95c6fb59b8ea09f5565359c814bc755ce3
4
- data.tar.gz: a3368121cb820f940c09199ad6ef3aed2006c5aeb5f7c41b00af848f26307eb5
3
+ metadata.gz: 9d633e20c984eefe3e5c928a6328e17964fb879dc285a81bcda592d9a57e9ea5
4
+ data.tar.gz: a8079309590a94cdb78cb91fe6b380aadb992b0ccd8816a7ff79cb4bcd48c640
5
5
  SHA512:
6
- metadata.gz: 1cb1310953698b5fd1a5274ba43eda6ceca9b7abaf5dcf850b9e589646638f8c4182f2d2694b852fd08a375744e37d3de2fd99fe5904316ed92701687f658352
7
- data.tar.gz: 4e55f0e48d08c6bd63be5ea53b79659564bc20ee012ac27662595c89b0102888aa6ad06e87d18fba365a26d44a2c02b9de3f92512699fcc1cff07bdae81605f5
6
+ metadata.gz: e3f2d8f819e89054d3c655eef9784b02f840f12fcf39a7bcadd952b4b5a01e965806d22fdd1baa20714cc8fc021fee44292cd3f2c6fe34a74d5e63e2ecccca58
7
+ data.tar.gz: dc09e729d8f5859faeef881184eb1fa5ec5b2863b2fcd45849ce02f75dcfd53fcf1d726cfaf92772984bd54dd712fbf87c169d1f46affea4404d7da41a1491ae
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2026-06-22
3
+ ## [1.1.0] - 2026-09-15
4
+
5
+ - Add support for SSR.
6
+
7
+ ## [1.0.0] - 2026-07-27
4
8
 
5
9
  - Initial release
data/README.md CHANGED
@@ -8,6 +8,8 @@ This gem handles Inertia page responses, provides flexible prop types, and integ
8
8
 
9
9
  ## Usage
10
10
 
11
+ ### Server-side setup
12
+
11
13
  Create a Rage app and add the gem:
12
14
 
13
15
  ```
@@ -15,6 +17,15 @@ rage new my-app -d postgresql
15
17
  bundle add inertia-rage
16
18
  ```
17
19
 
20
+ Update the parent controller in your `ApplicationController` from `RageController::API` to `RageController::Inertia` to get access to CSRF protection and redirect helpers:
21
+
22
+ ```ruby
23
+ class ApplicationController < RageController::Inertia
24
+ end
25
+ ```
26
+
27
+ ### Client-side setup
28
+
18
29
  Create a frontend project in any directory within your project's root or `app` folder:
19
30
 
20
31
  ```
@@ -202,7 +213,7 @@ See the [API documentation](https://inertia-api.rage-rb.dev/Inertia/Configuratio
202
213
  ## Learn More
203
214
 
204
215
  - [Props API Reference](https://inertia-api.rage-rb.dev/Inertia)
205
- - [Controller API Reference](https://inertia-api.rage-rb.dev/Inertia/ControllerHelpers.html)
216
+ - [Controller API Reference](https://inertia-api.rage-rb.dev/RageController/Inertia.html)
206
217
  - [Configuration API Reference](https://inertia-api.rage-rb.dev/Inertia/Configuration.html)
207
218
 
208
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.
@@ -7,7 +7,6 @@ require_relative "frontend"
7
7
  require_relative "renderer"
8
8
  require_relative "request_context"
9
9
  require_relative "protocol_builder"
10
- require_relative "controller_helpers"
11
10
  require_relative "configuration"
12
11
  require_relative "middleware/version"
13
12
  require_relative "middleware/assets"
@@ -84,4 +83,16 @@ module Inertia
84
83
  # @!endgroup
85
84
 
86
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
94
+ end
95
+
96
+ module RageController
97
+ autoload :Inertia, "inertia/rage_controller/inertia"
87
98
  end
@@ -2,11 +2,9 @@
2
2
 
3
3
  require "uri"
4
4
 
5
- module Inertia
6
- ##
7
- # Provides controller helper methods for Inertia.js integration.
8
- module ControllerHelpers
9
- module ClassMethods
5
+ module RageController
6
+ class Inertia < RageController::API
7
+ class << self
10
8
  # Shares data with all Inertia responses in a controller.
11
9
  #
12
10
  # This method registers a before_action that evaluates the given block
@@ -22,7 +20,7 @@ module Inertia
22
20
  # @yieldreturn [Hash] the data to merge into the shared props
23
21
  #
24
22
  # @example Share data for all actions
25
- # class ApplicationController < RageController::API
23
+ # class ApplicationController < RageController::Inertia
26
24
  # inertia_share do
27
25
  # { current_user: current_user&.as_json }
28
26
  # end
@@ -57,6 +55,10 @@ module Inertia
57
55
  end
58
56
  end
59
57
 
58
+ # @private
59
+ attr_accessor :inertia_shared_data
60
+ before_action :protect_from_csrf
61
+
60
62
  # Redirects the client to the specified location.
61
63
  #
62
64
  # @param location [String] the URL to redirect to
@@ -114,12 +116,6 @@ module Inertia
114
116
  end
115
117
  end
116
118
 
117
- # @private
118
- def self.included(klass)
119
- klass.before_action :protect_from_csrf
120
- klass.attr_accessor :inertia_shared_data
121
- end
122
-
123
119
  private
124
120
 
125
121
  # Appends additional request information to the log payload.
@@ -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
@@ -30,18 +35,21 @@ module Inertia
30
35
  config.middleware.use Middleware::Version
31
36
  end
32
37
 
33
- # Include ControllerHelpers to add support for `redirect_to` and other Inertia-specific methods
34
- initializer "inertia.rage.controller_helpers" do
35
- RageController::API.include ControllerHelpers
36
- RageController::API.extend ControllerHelpers::ClassMethods
37
- end
38
-
39
38
  # Prebuild frontend assets before launching the server in production
40
39
  before_server_start do
41
40
  if Inertia.config.build_on_start?
42
41
  puts "INFO: Building frontend"
43
42
  puts ""
44
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
45
53
  end
46
54
  end
47
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 = "0.0.1"
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: 0.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Samoilov
@@ -38,17 +38,19 @@ files:
38
38
  - Rakefile
39
39
  - lib/inertia-rage.rb
40
40
  - lib/inertia/configuration.rb
41
- - lib/inertia/controller_helpers.rb
42
41
  - lib/inertia/frontend.rb
43
42
  - lib/inertia/inertia.rb
44
43
  - lib/inertia/middleware/assets.rb
45
44
  - lib/inertia/middleware/version.rb
46
45
  - lib/inertia/props.rb
47
46
  - lib/inertia/protocol_builder.rb
47
+ - lib/inertia/rage_controller/inertia.rb
48
48
  - lib/inertia/rage_extension.rb
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