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 +7 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/Rakefile +12 -0
- data/lib/inertia/configuration.rb +110 -0
- data/lib/inertia/controller_helpers.rb +159 -0
- data/lib/inertia/frontend.rb +161 -0
- data/lib/inertia/inertia.rb +87 -0
- data/lib/inertia/middleware/assets.rb +40 -0
- data/lib/inertia/middleware/version.rb +36 -0
- data/lib/inertia/props.rb +31 -0
- data/lib/inertia/protocol_builder.rb +209 -0
- data/lib/inertia/rage_extension.rb +48 -0
- data/lib/inertia/renderer.rb +69 -0
- data/lib/inertia/request_context.rb +85 -0
- data/lib/inertia/rspec.rb +171 -0
- data/lib/inertia/version.rb +5 -0
- data/lib/inertia/vite_dev_server.rb +27 -0
- data/lib/inertia-rage.rb +1 -0
- metadata +77 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inertia
|
|
4
|
+
##
|
|
5
|
+
# Encapsulates request-specific context for Inertia partial reloads.
|
|
6
|
+
class RequestContext
|
|
7
|
+
EMPTY = [].freeze
|
|
8
|
+
private_constant :EMPTY
|
|
9
|
+
|
|
10
|
+
# Creates a new request context.
|
|
11
|
+
#
|
|
12
|
+
# @param request [Rage::Request] the HTTP request object
|
|
13
|
+
# @param component [String] the Inertia component name being rendered
|
|
14
|
+
def initialize(request, component:)
|
|
15
|
+
@request = request
|
|
16
|
+
@component = component
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns the full request path including query string.
|
|
20
|
+
# @return [String] the request URL path
|
|
21
|
+
def url
|
|
22
|
+
@request.fullpath
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Determines the inclusion status of a prop based on partial reload headers.
|
|
26
|
+
#
|
|
27
|
+
# @param prop_name [String] the full path of the prop (e.g., "user.profile")
|
|
28
|
+
# @return [Symbol] the prop status:
|
|
29
|
+
# - `:unspecified` - no partial reload filtering applies
|
|
30
|
+
# - `:requested` - prop is explicitly requested in partial reload
|
|
31
|
+
# - `:excluded` - prop is excluded from partial reload
|
|
32
|
+
def prop_status(prop_name)
|
|
33
|
+
return :unspecified unless partial_render?
|
|
34
|
+
|
|
35
|
+
is_excluded = partial_except.any? do |except_prop_name|
|
|
36
|
+
prop_name == except_prop_name ||
|
|
37
|
+
(prop_name.start_with?(except_prop_name) && prop_name.start_with?("#{except_prop_name}."))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return :excluded if is_excluded
|
|
41
|
+
return :requested if partial_only.empty? && partial_except.any?
|
|
42
|
+
return :unspecified if partial_only.empty?
|
|
43
|
+
|
|
44
|
+
is_included_in_partial_reload = partial_only.any? do |only_prop_name|
|
|
45
|
+
prop_name == only_prop_name ||
|
|
46
|
+
(only_prop_name.start_with?(prop_name) && only_prop_name.start_with?("#{prop_name}.")) ||
|
|
47
|
+
(prop_name.start_with?(only_prop_name) && prop_name.start_with?("#{only_prop_name}."))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
is_included_in_partial_reload ? :requested : :excluded
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Checks if a once prop should be excluded based on the client's cached props.
|
|
54
|
+
#
|
|
55
|
+
# @param prop_name [String] the cache key of the once prop
|
|
56
|
+
# @return [Boolean] `true` if the client already has this prop cached
|
|
57
|
+
def once_prop_excluded?(prop_name)
|
|
58
|
+
except_once.include?(prop_name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Checks if this is a partial reload request for the current component.
|
|
62
|
+
# @return [Boolean] `true` if the client requested a partial reload for this component
|
|
63
|
+
def partial_render?
|
|
64
|
+
partial_component == @component
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def partial_except
|
|
70
|
+
@partial_except ||= @request.env["HTTP_X_INERTIA_PARTIAL_EXCEPT"]&.split(",") || EMPTY
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def partial_only
|
|
74
|
+
@partial_only ||= @request.env["HTTP_X_INERTIA_PARTIAL_DATA"]&.split(",") || EMPTY
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def except_once
|
|
78
|
+
@except_once ||= @request.env["HTTP_X_INERTIA_EXCEPT_ONCE_PROPS"]&.split(",") || EMPTY
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def partial_component
|
|
82
|
+
@partial_component ||= @request.env["HTTP_X_INERTIA_PARTIAL_COMPONENT"]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rage/rspec"
|
|
4
|
+
|
|
5
|
+
module Inertia
|
|
6
|
+
module RSpec
|
|
7
|
+
# Wraps an Inertia response for test assertions.
|
|
8
|
+
#
|
|
9
|
+
# @example Accessing response data
|
|
10
|
+
# require "inertia/rspec"
|
|
11
|
+
#
|
|
12
|
+
# RSpec.describe UsersController, type: :request do
|
|
13
|
+
# it "renders user posts" do
|
|
14
|
+
# get "posts"
|
|
15
|
+
#
|
|
16
|
+
# expect(inertia.component).to eq("Posts/Index")
|
|
17
|
+
# expect(inertia.props).to have_key(:posts)
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
class TestResponse
|
|
21
|
+
# @param data [Hash] the raw Inertia response data
|
|
22
|
+
def initialize(data)
|
|
23
|
+
@data = data
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the props with symbolized keys.
|
|
27
|
+
# @note To comply with the Inertia protocol, props always include the `errors` object.
|
|
28
|
+
#
|
|
29
|
+
# @return [Hash{Symbol => Object}] the props hash
|
|
30
|
+
def props
|
|
31
|
+
sybmolize_keys(@data[:props])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the component name.
|
|
35
|
+
#
|
|
36
|
+
# @return [String] the Inertia component name
|
|
37
|
+
def component
|
|
38
|
+
@data[:component]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the deferred props configuration.
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash] the deferred props, or an empty hash if none
|
|
44
|
+
def deferred_props
|
|
45
|
+
@data[:deferredProps] || {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def sybmolize_keys(data)
|
|
51
|
+
data.each_with_object({}).each do |(k, v), memo|
|
|
52
|
+
transformed_value = if v.is_a?(Hash)
|
|
53
|
+
sybmolize_keys(v)
|
|
54
|
+
elsif v.is_a?(Array)
|
|
55
|
+
v.map { |el| el.is_a?(Hash) ? sybmolize_keys(el) : el }
|
|
56
|
+
else
|
|
57
|
+
v
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
memo[k.to_sym] = transformed_value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module TestHelpers
|
|
66
|
+
# Returns the Inertia response from the last request.
|
|
67
|
+
#
|
|
68
|
+
# @return [TestResponse, nil] the wrapped Inertia response, or nil if the last
|
|
69
|
+
# request was not an Inertia response
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# get "/users/1"
|
|
73
|
+
# expect(inertia.component).to eq("Users/Show")
|
|
74
|
+
# expect(inertia.props[:user]).to include(name: "John")
|
|
75
|
+
def inertia
|
|
76
|
+
inertia_response = Fiber.current.instance_variable_get(:@__inertia_current_response)
|
|
77
|
+
return unless inertia_response
|
|
78
|
+
|
|
79
|
+
TestResponse.new(inertia_response)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
module RequestHelpers
|
|
84
|
+
# Performs a GET request with optional Inertia partial reload headers.
|
|
85
|
+
#
|
|
86
|
+
# @param inertia [Hash] partial reload options
|
|
87
|
+
# @option inertia [String, Symbol, Array<String, Symbol>] :only request only these props
|
|
88
|
+
# @option inertia [String, Symbol, Array<String, Symbol>] :except request all props except these
|
|
89
|
+
# @param headers [Hash] additional request headers
|
|
90
|
+
# @raise [ArgumentError] if unknown options are passed in the inertia hash
|
|
91
|
+
#
|
|
92
|
+
# @example Request only specific props
|
|
93
|
+
# get "/users/1", inertia: { only: [:user, :permissions] }
|
|
94
|
+
#
|
|
95
|
+
# @example Request all props except some
|
|
96
|
+
# get "/users/1", inertia: { except: :audit_log }
|
|
97
|
+
def get(*, inertia: nil, headers: {}, **)
|
|
98
|
+
if inertia
|
|
99
|
+
inertia = {} unless inertia.is_a?(Hash)
|
|
100
|
+
|
|
101
|
+
if inertia.except(:only, :except).any?
|
|
102
|
+
raise ArgumentError, "Unknown :inertia option. Supported values are :only and :except"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
headers = headers.merge({
|
|
106
|
+
"X-Inertia" => "true",
|
|
107
|
+
"X-Inertia-Partial-Component" => self.inertia&.component || double(:== => true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if only = inertia[:only]
|
|
111
|
+
headers["X-Inertia-Partial-Data"] = Array(only).join(",")
|
|
112
|
+
elsif except = inertia[:except]
|
|
113
|
+
headers["X-Inertia-Partial-Except"] = Array(except).join(",")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
super(*, headers:, **)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
RSpec::Matchers.matcher :redirect_to do |expected, external: false|
|
|
124
|
+
failure_message do |response|
|
|
125
|
+
header = external ? "x-inertia-location" : "location"
|
|
126
|
+
actual = response.headers[header]
|
|
127
|
+
|
|
128
|
+
if expected.is_a?(Regexp)
|
|
129
|
+
"expected response to be a redirect to /#{expected.source}/ but was a redirect to \"#{actual}\""
|
|
130
|
+
else
|
|
131
|
+
"expected response to be a redirect to \"#{expected}\" but was a redirect to \"#{actual}\""
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
failure_message_when_negated do |response|
|
|
136
|
+
if expected.is_a?(Regexp)
|
|
137
|
+
"expected not to redirect to /#{expected.source}/, but did"
|
|
138
|
+
else
|
|
139
|
+
"expected not to redirect to \"#{expected}\", but did"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
match do |response|
|
|
144
|
+
header = external ? "x-inertia-location" : "location"
|
|
145
|
+
actual = response.headers[header]
|
|
146
|
+
|
|
147
|
+
if expected.is_a?(Regexp)
|
|
148
|
+
actual&.match?(expected)
|
|
149
|
+
else
|
|
150
|
+
actual == expected
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
RSpec.configure do |config|
|
|
156
|
+
config.include(Inertia::RSpec::TestHelpers, type: :request)
|
|
157
|
+
config.prepend(Inertia::RSpec::RequestHelpers, type: :request)
|
|
158
|
+
|
|
159
|
+
config.before(:each) do
|
|
160
|
+
Fiber.current.instance_variable_set(:@__inertia_current_response, nil)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
config.before(:each) do
|
|
164
|
+
test_fiber = Fiber.current
|
|
165
|
+
|
|
166
|
+
allow_any_instance_of(Inertia::ProtocolBuilder).to receive(:call).and_wrap_original do |m, *args, **kwargs|
|
|
167
|
+
data = m.call(*args, **kwargs)
|
|
168
|
+
test_fiber.instance_variable_set(:@__inertia_current_response, data)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Inertia
|
|
2
|
+
##
|
|
3
|
+
# A daemon that runs the Vite development server alongside the Rage process.
|
|
4
|
+
#
|
|
5
|
+
class ViteDevServer < Rage::Daemon
|
|
6
|
+
def perform
|
|
7
|
+
puts "INFO: Starting Vite dev server"
|
|
8
|
+
|
|
9
|
+
config = Inertia.config.dev_server
|
|
10
|
+
host, port = config.host, config.port
|
|
11
|
+
|
|
12
|
+
command = "#{Frontend.package_runner} vite dev --clearScreen false --host #{host} --port #{port}"
|
|
13
|
+
@pid = Process.spawn(command, chdir: Frontend.root)
|
|
14
|
+
Process.wait(@pid)
|
|
15
|
+
|
|
16
|
+
# Vite child process sometimes receives a shutdown signal first. The supervisor sees that
|
|
17
|
+
# the Vite process has exited and schedules a restart before Rage processes its own shutdown.
|
|
18
|
+
# The delay allows Rage to handle the signal, stop supervision, and avoid a confusing log message.
|
|
19
|
+
sleep 0.1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cleanup
|
|
23
|
+
Process.kill("TERM", @pid)
|
|
24
|
+
rescue Errno::ESRCH
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/inertia-rage.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "inertia/inertia"
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: inertia-rage
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Roman Samoilov
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rage-rb
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.26'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.26'
|
|
26
|
+
email:
|
|
27
|
+
- developers@rage-rb.dev
|
|
28
|
+
executables: []
|
|
29
|
+
extensions: []
|
|
30
|
+
extra_rdoc_files: []
|
|
31
|
+
files:
|
|
32
|
+
- ".rspec"
|
|
33
|
+
- ".yardopts"
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- CODE_OF_CONDUCT.md
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- Rakefile
|
|
39
|
+
- lib/inertia-rage.rb
|
|
40
|
+
- lib/inertia/configuration.rb
|
|
41
|
+
- lib/inertia/controller_helpers.rb
|
|
42
|
+
- lib/inertia/frontend.rb
|
|
43
|
+
- lib/inertia/inertia.rb
|
|
44
|
+
- lib/inertia/middleware/assets.rb
|
|
45
|
+
- lib/inertia/middleware/version.rb
|
|
46
|
+
- lib/inertia/props.rb
|
|
47
|
+
- lib/inertia/protocol_builder.rb
|
|
48
|
+
- lib/inertia/rage_extension.rb
|
|
49
|
+
- lib/inertia/renderer.rb
|
|
50
|
+
- lib/inertia/request_context.rb
|
|
51
|
+
- lib/inertia/rspec.rb
|
|
52
|
+
- lib/inertia/version.rb
|
|
53
|
+
- lib/inertia/vite_dev_server.rb
|
|
54
|
+
homepage: https://rage-rb.dev
|
|
55
|
+
licenses:
|
|
56
|
+
- MIT
|
|
57
|
+
metadata:
|
|
58
|
+
homepage_uri: https://rage-rb.dev
|
|
59
|
+
source_code_uri: https://github.com/rage-rb/inertia-rage
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 3.3.0
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 4.0.10
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: Inertia.js adapter for the Rage framework
|
|
77
|
+
test_files: []
|