inertia-rage 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1a6b20a9b2fd94ff4fa3e0595f894f95c6fb59b8ea09f5565359c814bc755ce3
4
+ data.tar.gz: a3368121cb820f940c09199ad6ef3aed2006c5aeb5f7c41b00af848f26307eb5
5
+ SHA512:
6
+ metadata.gz: 1cb1310953698b5fd1a5274ba43eda6ceca9b7abaf5dcf850b9e589646638f8c4182f2d2694b852fd08a375744e37d3de2fd99fe5904316ed92701687f658352
7
+ data.tar.gz: 4e55f0e48d08c6bd63be5ea53b79659564bc20ee012ac27662595c89b0102888aa6ad06e87d18fba365a26d44a2c02b9de3f92512699fcc1cff07bdae81605f5
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-06-22
4
+
5
+ - Initial release
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "inertia-rage" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["developers@rage-rb.dev"](mailto:"developers@rage-rb.dev").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Roman Samoilov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,218 @@
1
+ <p align="center"><img height="200" alt="logo" src="https://github.com/user-attachments/assets/470e237b-f947-47f1-882c-1b7191709f50" /></p>
2
+
3
+ # Inertia Rage
4
+
5
+ The official [Inertia.js](https://inertiajs.com) adapter for the [Rage](https://github.com/rage-rb/rage) framework.
6
+
7
+ This gem handles Inertia page responses, provides flexible prop types, and integrates seamlessly with Vite.
8
+
9
+ ## Usage
10
+
11
+ Create a Rage app and add the gem:
12
+
13
+ ```
14
+ rage new my-app -d postgresql
15
+ bundle add inertia-rage
16
+ ```
17
+
18
+ Create a frontend project in any directory within your project's root or `app` folder:
19
+
20
+ ```
21
+ npm create vite@latest app/frontend
22
+ ```
23
+
24
+ Install the required packages and initialize your Inertia app. Refer to the Inertia documentation for [client-side setup](https://inertiajs.com/docs/v3/installation/client-side-setup).
25
+
26
+ <details>
27
+
28
+ <summary>If you are using React</summary>
29
+
30
+ Vite's React template uses `root` as the container element id, but Inertia expects `app`. Update `index.html` accordingly:
31
+
32
+ ```diff
33
+ <body>
34
+ - <div id="root"></div>
35
+ + <div id="app"></div>
36
+ <script type="module" src="/src/main.ts"></script>
37
+ </body>
38
+ ```
39
+
40
+ </details>
41
+
42
+ Start the app:
43
+
44
+ ```
45
+ rage s
46
+ ```
47
+
48
+ Rage automatically starts the Vite dev server in development and pre-builds assets in other environments. In most cases, `rage s` is all you need to run your app.
49
+
50
+ ## Rendering
51
+
52
+ Use `render inertia:` to render Inertia responses in your controller actions:
53
+
54
+ ```ruby
55
+ class PostsController < ApplicationController
56
+ def index
57
+ render inertia: "Posts/Index", props: { posts: current_user.posts }
58
+ end
59
+ end
60
+ ```
61
+
62
+ Rage can also infer the component name from the current controller and action. The following will render the `Posts/Index` component:
63
+
64
+ ```ruby
65
+ class PostsController < ApplicationController
66
+ def index
67
+ render inertia: { posts: current_user.posts }
68
+ end
69
+ end
70
+ ```
71
+
72
+ Redirects are supported via `redirect_to`, `redirect_back`, and `redirect_back_or_to`:
73
+
74
+ ```ruby
75
+ class PostsController < ApplicationController
76
+ def create
77
+ Post.create!(post_params)
78
+ redirect_back fallback_location: "/"
79
+ end
80
+ end
81
+ ```
82
+
83
+ ## Props
84
+
85
+ Inertia Rage supports several prop types to optimize data loading and improve performance.
86
+
87
+ ### Lazy Props
88
+
89
+ Define lazy props using procs:
90
+
91
+ ```ruby
92
+ render inertia: {
93
+ user:,
94
+ posts: -> { user.posts }
95
+ }
96
+ ```
97
+
98
+ Lazy props are evaluated on the initial page load just like regular props. The difference emerges during partial reloads: lazy props are only evaluated when explicitly requested. For example, if a partial reload requests only `user`, the `posts` query is skipped entirely.
99
+
100
+ ### Deferred Props
101
+
102
+ Deferred props are excluded from the initial page load and fetched automatically in a subsequent request:
103
+
104
+ ```ruby
105
+ render inertia: {
106
+ user:,
107
+ comments: Inertia.deferred { user.build_comments_tree }
108
+ }
109
+ ```
110
+
111
+ Use deferred props for expensive operations that would otherwise slow down the initial page load.
112
+
113
+ ### Optional Props
114
+
115
+ Optional props are never evaluated during the initial page load. The frontend must explicitly request them:
116
+
117
+ ```ruby
118
+ render inertia: {
119
+ user:,
120
+ last_posted_at: Inertia.optional { user.posts.last.created_at }
121
+ }
122
+ ```
123
+
124
+ ### Once Props
125
+
126
+ Once props are cached by the frontend after their first evaluation. On subsequent requests, the cached value is used and the prop is not re-evaluated. Since Inertia resets the cache for once props that are absent from the page, you'll typically want to use them inside `inertia_share` blocks:
127
+
128
+ ```ruby
129
+ class ApplicationController < RageController::API
130
+ inertia_share do
131
+ { permissions: Inertia.once { current_user.permissions } }
132
+ end
133
+ end
134
+ ```
135
+
136
+ ## Shared Data
137
+
138
+ Use `inertia_share` to share data across all Inertia responses:
139
+
140
+ ```ruby
141
+ class ApplicationController < RageController::API
142
+ inertia_share do
143
+ { has_new_notifications: current_user.notifications.unread.exists? }
144
+ end
145
+ end
146
+ ```
147
+
148
+ `inertia_share` accepts the same arguments as [`before_action`](https://api.rage-rb.dev/RageController/API#before_action-class_method):
149
+
150
+ ```ruby
151
+ class DashboardsController < ApplicationController
152
+ inertia_share if: :user_signed_in?, except: :destroy do
153
+ { all_dashboards: Dashboard.where.not(user: current_user) }
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## Testing with RSpec
159
+
160
+ The gem provides an [`inertia`](https://inertia-api.rage-rb.dev/Inertia/RSpec/TestResponse) helper for testing Inertia responses. Require `inertia/rspec` in your request specs to access it:
161
+
162
+ ```ruby
163
+ require "inertia/rspec"
164
+
165
+ RSpec.describe UsersController, type: :request do
166
+ it "renders user posts" do
167
+ get "posts"
168
+
169
+ expect(inertia.component).to eq("Posts/Index")
170
+ expect(inertia.props).to have_key(:posts)
171
+ end
172
+ end
173
+ ```
174
+
175
+ Partial reloading is supported via the [`inertia` option](https://inertia-api.rage-rb.dev/Inertia/RSpec/RequestHelpers.html) on the `get` method:
176
+
177
+ ```ruby
178
+ require "inertia/rspec"
179
+
180
+ RSpec.describe UsersController, type: :request do
181
+ it "renders post comments" do
182
+ get "posts/1", inertia: { only: :comments }
183
+
184
+ expect(inertia.props.keys).to eq(:comments)
185
+ end
186
+ end
187
+ ```
188
+
189
+ ## Configuration
190
+
191
+ Use `Inertia.configure` to customize the default behavior:
192
+
193
+ ```ruby
194
+ Inertia.configure do |config|
195
+ config.build_on_start = false
196
+ config.dev_server.port = 5000
197
+ end
198
+ ```
199
+
200
+ See the [API documentation](https://inertia-api.rage-rb.dev/Inertia/Configuration.html) for the complete list of configuration options.
201
+
202
+ ## Learn More
203
+
204
+ - [Props API Reference](https://inertia-api.rage-rb.dev/Inertia)
205
+ - [Controller API Reference](https://inertia-api.rage-rb.dev/Inertia/ControllerHelpers.html)
206
+ - [Configuration API Reference](https://inertia-api.rage-rb.dev/Inertia/Configuration.html)
207
+
208
+ ## Contributing
209
+
210
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rage-rb/inertia-rage. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/rage-rb/inertia-rage/blob/master/CODE_OF_CONDUCT.md).
211
+
212
+ ## License
213
+
214
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
215
+
216
+ ## Code of Conduct
217
+
218
+ Everyone interacting in the Inertia::Rage project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/rage-rb/inertia-rage/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inertia
4
+ ##
5
+ # Stores global configuration options for the Inertia integration.
6
+ #
7
+ # @example Configure via block
8
+ # Inertia.configure do |config|
9
+ # config.frontend_path = "client"
10
+ # config.build_path = "public/dist"
11
+ # config.build_on_start = false
12
+ #
13
+ # config.dev_server.host = "0.0.0.0"
14
+ # config.dev_server.port = 3000
15
+ # end
16
+ #
17
+ class Configuration
18
+ # @private
19
+ def initialize
20
+ @build_on_start = !Rage.env.development?
21
+ end
22
+
23
+ # Set whether to build frontend assets on server start.
24
+ #
25
+ # @param value [Boolean]
26
+ # @see #build_on_start?
27
+ def build_on_start=(value)
28
+ @build_on_start = value
29
+ end
30
+
31
+ # Returns whether frontend assets should be built when the server starts.
32
+ #
33
+ # Defaults to `true` in non-development environments.
34
+ #
35
+ # @return [Boolean]
36
+ def build_on_start?
37
+ !!@build_on_start
38
+ end
39
+
40
+ # Returns the configured frontend directory path.
41
+ #
42
+ # @return [Pathname, nil]
43
+ # @see #frontend_path=
44
+ def frontend_path
45
+ @frontend_path
46
+ end
47
+
48
+ # Sets the path to the frontend application directory.
49
+ #
50
+ # When set, this overrides automatic Vite config detection. The path should
51
+ # be relative to the application root.
52
+ #
53
+ # @param path [String] path relative to the application root
54
+ def frontend_path=(path)
55
+ @frontend_path = Rage.root.join(path)
56
+ end
57
+
58
+ # Returns the configured build output path.
59
+ #
60
+ # @return [Pathname, nil]
61
+ # @see #build_path=
62
+ def build_path
63
+ @build_path
64
+ end
65
+
66
+ # Sets the path where built frontend assets are located.
67
+ #
68
+ # When set, this overrides the default `dist` directory inside {#frontend_path}.
69
+ # The path should be relative to the application root.
70
+ #
71
+ # Building directly into `public/` is not supported because a root-level
72
+ # `index.html` would intercept all requests to the application's `/` endpoint.
73
+ # Use a nested directory instead, e.g., `public/dist`.
74
+ #
75
+ # @param path [String] path relative to the application root
76
+ # @raise [ArgumentError] if path resolves to the public directory root
77
+ # @note This setting should be paired with updating the `build.outDir` option in your Vite config.
78
+ def build_path=(path)
79
+ @build_path = Rage.root.join(path)
80
+
81
+ if @build_path == Rage.root.join("public")
82
+ raise ArgumentError, "build_path cannot be set to public/; use a nested directory instead, e.g., public/dist"
83
+ end
84
+ end
85
+
86
+ # Returns the dev server configuration.
87
+ #
88
+ # @return [DevServer] the dev server configuration object
89
+ def dev_server
90
+ @dev_server ||= DevServer.new
91
+ end
92
+
93
+ ##
94
+ # Stores configuration for the Vite development server.
95
+ #
96
+ class DevServer
97
+ # @return [String] the hostname for the Vite dev server (default: "localhost")
98
+ attr_accessor :host
99
+
100
+ # @return [Integer] the port for the Vite dev server (default: 5173)
101
+ attr_accessor :port
102
+
103
+ # @private
104
+ def initialize
105
+ @host = "localhost"
106
+ @port = 5173
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Inertia
6
+ ##
7
+ # Provides controller helper methods for Inertia.js integration.
8
+ module ControllerHelpers
9
+ module ClassMethods
10
+ # Shares data with all Inertia responses in a controller.
11
+ #
12
+ # This method registers a before_action that evaluates the given block
13
+ # in the controller instance context and merges the returned hash into
14
+ # the shared data. The shared data is automatically included in all
15
+ # Inertia responses rendered by the controller.
16
+ #
17
+ # Multiple `inertia_share` calls are cumulative - each block's data
18
+ # is merged into the existing shared data.
19
+ #
20
+ # @param options [Hash] options passed to `before_action` (e.g., `only:`, `except:`, `if:`, `unless:`)
21
+ # @yield Block evaluated in controller context that returns a Hash of data to share
22
+ # @yieldreturn [Hash] the data to merge into the shared props
23
+ #
24
+ # @example Share data for all actions
25
+ # class ApplicationController < RageController::API
26
+ # inertia_share do
27
+ # { current_user: current_user&.as_json }
28
+ # end
29
+ # end
30
+ #
31
+ # @example Share data only for specific actions
32
+ # class UsersController < ApplicationController
33
+ # inertia_share only: [:index, :show] do
34
+ # { permissions: current_user.permissions }
35
+ # end
36
+ # end
37
+ #
38
+ # @example Share data conditionally
39
+ # class DashboardController < ApplicationController
40
+ # inertia_share if: :user_signed_in? do
41
+ # { notifications: current_user.unread_notifications }
42
+ # end
43
+ # end
44
+ def inertia_share(**options, &block)
45
+ raise ArgumentError, "inertia_share requires a block" unless block
46
+
47
+ before_action(**options) do
48
+ data = instance_eval(&block)
49
+ return unless data
50
+
51
+ if self.inertia_shared_data
52
+ self.inertia_shared_data.merge!(data)
53
+ else
54
+ self.inertia_shared_data = data
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ # Redirects the client to the specified location.
61
+ #
62
+ # @param location [String] the URL to redirect to
63
+ # @param external [Boolean] whether to force an external (full page) redirect.
64
+ # When `true`, the browser will perform a full page visit instead of an Inertia visit
65
+ #
66
+ # @example Basic redirect
67
+ # redirect_to "/dashboard"
68
+ #
69
+ # @example Force an external redirect
70
+ # redirect_to "https://example.com", external: true
71
+ def redirect_to(location, external: false)
72
+ if external
73
+ headers["x-inertia-location"] = location
74
+ head 409
75
+ return
76
+ end
77
+
78
+ head(request.get? || request.post? ? 302 : 303)
79
+ headers["location"] = location
80
+ end
81
+
82
+ # Redirects the client back to the referring page, with a fallback location.
83
+ #
84
+ # @param fallback_location [String] the URL to redirect to if there is no referer
85
+ # @param external [Boolean] whether to force an external (full page) redirect.
86
+ # When `true`, the browser will perform a full page visit instead of an Inertia visit
87
+ #
88
+ # @example Redirect back with a fallback
89
+ # redirect_back fallback_location: "/dashboard"
90
+ #
91
+ # @see #redirect_back_or_to
92
+ def redirect_back(fallback_location:, external: false)
93
+ redirect_back_or_to fallback_location, external:
94
+ end
95
+
96
+ # Redirects the client back to the referring page, or to the specified fallback location.
97
+ #
98
+ # @param fallback_location [String] the URL to redirect to if there is no referer
99
+ # @param external [Boolean] whether to force an external (full page) redirect.
100
+ # When `true`, the browser will perform a full page visit instead of an Inertia visit
101
+ #
102
+ # @example Redirect back or to a fallback
103
+ # redirect_back_or_to "/dashboard"
104
+ #
105
+ # @example Force an external redirect
106
+ # redirect_back_or_to "/dashboard", external: true
107
+ def redirect_back_or_to(fallback_location, external: false)
108
+ referer = request.env["HTTP_REFERER"]
109
+
110
+ if referer
111
+ redirect_to referer, external:
112
+ else
113
+ redirect_to fallback_location, external:
114
+ end
115
+ end
116
+
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
+ private
124
+
125
+ # Appends additional request information to the log payload.
126
+ #
127
+ # This method is called by the framework to enrich log entries with
128
+ # request parameters. Parameters are only included in development mode
129
+ # to avoid logging sensitive data in production.
130
+ def append_info_to_payload(context)
131
+ context[:params] = params if Rage.env.development?
132
+ end
133
+
134
+ def protect_from_csrf
135
+ # safe methods are always allowed
136
+ method = request.env["REQUEST_METHOD"]
137
+ return if method == "GET" || method == "HEAD" || method == "OPTIONS"
138
+
139
+ sec_fetch_site = request.env["HTTP_SEC_FETCH_SITE"]
140
+ return if sec_fetch_site == "same-origin" || sec_fetch_site == "none"
141
+
142
+ # check the Origin header
143
+ if sec_fetch_site.nil?
144
+ origin = request.env["HTTP_ORIGIN"]
145
+ # either the request is same-origin or not a browser request
146
+ return unless origin
147
+
148
+ begin
149
+ origin_host = URI(origin).authority
150
+ return if origin_host == request.env["HTTP_HOST"]
151
+ rescue URI::InvalidURIError
152
+ # fallthrough
153
+ end
154
+ end
155
+
156
+ render plain: "CSRF rejected", status: 403
157
+ end
158
+ end
159
+ end