sinatra-inertia 0.1.3 → 0.1.4
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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +44 -35
- data/lib/sinatra/inertia/deferred.rb +1 -4
- data/lib/sinatra/inertia/helpers.rb +64 -4
- data/lib/sinatra/inertia/version.rb +1 -1
- data/lib/sinatra/inertia.rb +20 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf6ac9b17727011296d39f42de745b6c9cd6e27b6cd6ed73ced25c0795dd0c34
|
|
4
|
+
data.tar.gz: a9c6f651bf9017f99e3baf93f849f68c22844c8dce63ae6645a570de65d77a42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9178b8948981c792d0d40fe9c3bfc35649e3ca4f561c7c26dda9bbc63bb28fb6f7af82806ba035aa7e0c9dbe79d2d8427086325c0bbe485fea9ce9344bd12729
|
|
7
|
+
data.tar.gz: 913198985434b0955bfb790967406400491ec356f732df35bd1a793de4f28131a0a6b97672cb8f35f6f73e588a3f905f35550f1839a89c1cd071abb879dbf865
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4 — 2026-05-03
|
|
4
|
+
|
|
5
|
+
- Add the recommended Sinatra-native page API: `render 'Component', props`,
|
|
6
|
+
`share_props`, `set :page_version`, `set :page_layout`, and route helpers
|
|
7
|
+
`defer`, `always`, `optional`, `lazy`, `merge`, `page_errors`,
|
|
8
|
+
`clear_history!`, and `encrypt_history!`.
|
|
9
|
+
- Keep existing `inertia_*` helpers/settings and `Inertia.*` prop wrappers
|
|
10
|
+
working for compatibility.
|
|
11
|
+
|
|
3
12
|
## 0.1.3 — 2026-04-29
|
|
4
13
|
|
|
5
14
|
- `lib/sinatra/inertia/async_sources.rb` registers under
|
data/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A Sinatra extension that implements the full **Inertia.js v2** wire protocol —
|
|
4
4
|
page-object responses, version-mismatch detection, partial reloads,
|
|
5
|
-
deferred / lazy / always / optional / merge
|
|
6
|
-
|
|
5
|
+
deferred / lazy / always / optional / merge props, encrypted history, 303
|
|
6
|
+
redirect promotion, and error/flash session sweeps.
|
|
7
7
|
|
|
8
8
|
Pure Sinatra-compatible: depends only on `sinatra` and `rack`. Runs on
|
|
9
9
|
MRI Ruby and on the [homura](https://github.com/kazuph/homura) Cloudflare
|
|
@@ -13,11 +13,12 @@ Workers + Opal stack.
|
|
|
13
13
|
|
|
14
14
|
Two principles drove the API:
|
|
15
15
|
|
|
16
|
-
1. **
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
2. **Class-level config uses
|
|
20
|
-
|
|
16
|
+
1. **Routes render pages.** Application code should read like ordinary
|
|
17
|
+
Sinatra: `render 'Pages/Show', record: record`. The Inertia protocol stays
|
|
18
|
+
in the extension.
|
|
19
|
+
2. **Class-level config uses Sinatra nouns.** Use `set :page_version`,
|
|
20
|
+
`set :page_layout`, and `share_props do ... end`. The older `inertia_*`
|
|
21
|
+
names still work for existing apps.
|
|
21
22
|
|
|
22
23
|
## Installation
|
|
23
24
|
|
|
@@ -33,10 +34,12 @@ gem 'sinatra-inertia'
|
|
|
33
34
|
require 'sinatra'
|
|
34
35
|
require 'sinatra/inertia'
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
register Sinatra::Inertia
|
|
38
|
+
|
|
39
|
+
set :page_version, -> { ENV.fetch('ASSETS_VERSION', '1') }
|
|
37
40
|
|
|
38
41
|
get '/' do
|
|
39
|
-
|
|
42
|
+
render 'Pages/Hello', name: 'world'
|
|
40
43
|
end
|
|
41
44
|
```
|
|
42
45
|
|
|
@@ -61,42 +64,49 @@ end
|
|
|
61
64
|
|
|
62
65
|
| helper | purpose |
|
|
63
66
|
|---|---|
|
|
64
|
-
| `
|
|
65
|
-
| `render
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
67
|
+
| `render 'Comp', props_hash` | Render an Inertia response (HTML on first hit, JSON on Inertia visit). |
|
|
68
|
+
| `render 'Comp', key: value` | Keyword-prop form of the same page render. |
|
|
69
|
+
| `page_request?` | True when `X-Inertia: true` header is present. |
|
|
70
|
+
| `page_errors(payload = nil)` | Read or write validation errors that survive one redirect. |
|
|
71
|
+
| `clear_history!` | Mark the next response's history as cleared. |
|
|
72
|
+
| `encrypt_history!` | Mark the next response's history as encrypted. |
|
|
73
|
+
| `always`, `defer`, `optional`, `lazy`, `merge` | Prop wrappers for Inertia v2 transport modes. |
|
|
74
|
+
|
|
75
|
+
Compatibility aliases remain available: `inertia`, `render(inertia: ...)`,
|
|
76
|
+
`inertia_request?`, `inertia_errors`, `inertia_clear_history!`,
|
|
77
|
+
`inertia_encrypt_history!`, and `Inertia.defer` / `Inertia.merge` / etc.
|
|
70
78
|
|
|
71
79
|
### Class-level DSL
|
|
72
80
|
|
|
73
81
|
| DSL | purpose |
|
|
74
82
|
|---|---|
|
|
75
|
-
| `set :
|
|
76
|
-
| `set :
|
|
83
|
+
| `set :page_version, -> { ... }` | Asset version. Mismatch on Inertia GET → 409 + `X-Inertia-Location`. |
|
|
84
|
+
| `set :page_layout, :layout` | ERB layout used for full-page rendering (default `:layout`). |
|
|
77
85
|
| `set :inertia_encrypt_history, true` | Default `encryptHistory: true` on every page. |
|
|
78
|
-
| `
|
|
86
|
+
| `share_props do … end` | Block whose return Hash is merged into every page's props. |
|
|
87
|
+
|
|
88
|
+
Compatibility aliases remain available: `set :inertia_version`,
|
|
89
|
+
`set :inertia_layout`, and `inertia_share do ... end`.
|
|
79
90
|
|
|
80
91
|
### Prop wrappers (Inertia v2 transport modes)
|
|
81
92
|
|
|
82
93
|
```ruby
|
|
83
|
-
|
|
94
|
+
render 'Page',
|
|
84
95
|
todos: -> { Todo.all }, # plain lazy
|
|
85
|
-
csrf:
|
|
86
|
-
stats:
|
|
87
|
-
filter:
|
|
88
|
-
feed:
|
|
89
|
-
}
|
|
96
|
+
csrf: always { csrf_token }, # always sent
|
|
97
|
+
stats: defer(group: 'meta') { stats }, # excluded from initial response
|
|
98
|
+
filter: optional { params[:f] }, # only on partial-reload request
|
|
99
|
+
feed: merge(page_items) # client-side array merge
|
|
90
100
|
```
|
|
91
101
|
|
|
92
102
|
| wrapper | semantics |
|
|
93
103
|
|---|---|
|
|
94
104
|
| bare `Proc`/`->` | resolved every request when included; partial-reload aware. |
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
105
|
+
| `always { … }` | always included, even on partials that omit it. |
|
|
106
|
+
| `defer(group:) { … }` | excluded on initial visit; client refetches in second roundtrip. |
|
|
107
|
+
| `optional { … }` | only resolved when explicitly requested via `X-Inertia-Partial-Data`. |
|
|
108
|
+
| `lazy { … }` | alias of `optional` (Inertia v1 name). |
|
|
109
|
+
| `merge(value)` | sent as a merge prop (`mergeProps` array on page object). Honours `X-Inertia-Reset: prop1,prop2` (Inertia 2.0) — reset props are emitted as plain values and dropped from `mergeProps`. |
|
|
100
110
|
|
|
101
111
|
## Protocol features
|
|
102
112
|
|
|
@@ -110,9 +120,9 @@ inertia 'Page', props: {
|
|
|
110
120
|
303 so the browser follows with GET.
|
|
111
121
|
* **Partial reloads** — `X-Inertia-Partial-Component` + `X-Inertia-Partial-Data`
|
|
112
122
|
/ `X-Inertia-Partial-Except` headers narrow which props are resolved.
|
|
113
|
-
* **Encrypted history / clear history** — set per-app via setting or
|
|
114
|
-
|
|
115
|
-
* **Errors session** — `
|
|
123
|
+
* **Encrypted history / clear history** — set per-app via setting or per-route
|
|
124
|
+
via `encrypt_history!` / `clear_history!`.
|
|
125
|
+
* **Errors session** — `page_errors(field: 'msg')` survives one redirect
|
|
116
126
|
and is automatically swept on render.
|
|
117
127
|
|
|
118
128
|
## Validation pattern (no 422, no client state)
|
|
@@ -120,7 +130,7 @@ inertia 'Page', props: {
|
|
|
120
130
|
```ruby
|
|
121
131
|
post '/todos' do
|
|
122
132
|
if params[:title].to_s.strip.empty?
|
|
123
|
-
|
|
133
|
+
page_errors title: "can't be blank"
|
|
124
134
|
redirect back # 303 by middleware; Inertia client follows
|
|
125
135
|
else
|
|
126
136
|
Todo.create(params)
|
|
@@ -129,10 +139,9 @@ post '/todos' do
|
|
|
129
139
|
end
|
|
130
140
|
|
|
131
141
|
get '/' do
|
|
132
|
-
|
|
142
|
+
render 'Todos/Index',
|
|
133
143
|
todos: Todo.all,
|
|
134
144
|
values: { title: params[:title] }
|
|
135
|
-
}
|
|
136
145
|
end
|
|
137
146
|
```
|
|
138
147
|
|
|
@@ -13,8 +13,7 @@ module Sinatra
|
|
|
13
13
|
# stats: Inertia.defer { compute_stats } # excluded from initial response, fetched in 2nd request
|
|
14
14
|
# csrf: Inertia.always { csrf_token } # included even on partial reloads that omit it
|
|
15
15
|
# filter: Inertia.optional { params[:f] } # only included when explicitly requested via partial
|
|
16
|
-
# feed:
|
|
17
|
-
# once: Inertia.once { current_time } # delivered exactly once; subsequent visits suppress
|
|
16
|
+
# feed: merge(page_items) # array merged with existing client-side feed
|
|
18
17
|
# }
|
|
19
18
|
class Prop
|
|
20
19
|
attr_reader :block, :value, :group
|
|
@@ -45,8 +44,6 @@ module Sinatra
|
|
|
45
44
|
# existing array (Inertia 2 merge semantics)?
|
|
46
45
|
def merge? = false
|
|
47
46
|
|
|
48
|
-
# Once-only delivery (cleared from session/state after first emission).
|
|
49
|
-
def once? = false
|
|
50
47
|
end
|
|
51
48
|
|
|
52
49
|
class AlwaysProp < Prop
|
|
@@ -10,7 +10,7 @@ module Sinatra
|
|
|
10
10
|
module Helpers
|
|
11
11
|
# Render an Inertia response.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
13
|
+
# render 'Todos/Index', todos: -> { Todo.all }
|
|
14
14
|
#
|
|
15
15
|
# Layout selection: the configured layout (default `:layout`) is
|
|
16
16
|
# rendered for full HTML responses. The view receives `@page_json`
|
|
@@ -18,7 +18,7 @@ module Sinatra
|
|
|
18
18
|
# attribute) and `@page` (the underlying Hash, useful for SSR or
|
|
19
19
|
# custom rendering).
|
|
20
20
|
def inertia(component, props: {}, layout: nil)
|
|
21
|
-
layout =
|
|
21
|
+
layout = current_page_layout if layout.nil?
|
|
22
22
|
|
|
23
23
|
version = current_inertia_version
|
|
24
24
|
shared = current_inertia_shared
|
|
@@ -72,7 +72,9 @@ module Sinatra
|
|
|
72
72
|
erb layout, layout: false
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
#
|
|
75
|
+
# Natural page-rendering API: `render 'Component', props_hash`.
|
|
76
|
+
# `render inertia: 'Component', props: {...}` remains available for
|
|
77
|
+
# existing apps, while non-page render calls still delegate to Sinatra.
|
|
76
78
|
# We must preserve Sinatra's `render(engine, data = nil, options = {}, locals = {}, &block)`
|
|
77
79
|
# signature for the non-inertia path, so we forward *args/**kwargs.
|
|
78
80
|
def render(*args, **kwargs, &block)
|
|
@@ -81,6 +83,14 @@ module Sinatra
|
|
|
81
83
|
inertia(first[:inertia], props: first[:props] || {}, layout: first[:layout])
|
|
82
84
|
elsif kwargs.key?(:inertia) && args.empty?
|
|
83
85
|
inertia(kwargs[:inertia], props: kwargs[:props] || {}, layout: kwargs[:layout])
|
|
86
|
+
elsif first.is_a?(String) && args.length <= 2 && (args.length == 1 || args[1].is_a?(Hash))
|
|
87
|
+
layout = kwargs.delete(:layout)
|
|
88
|
+
props = {}
|
|
89
|
+
props.merge!(args[1]) if args[1].is_a?(Hash)
|
|
90
|
+
explicit_props = kwargs.delete(:props)
|
|
91
|
+
props.merge!(explicit_props) if explicit_props.is_a?(Hash)
|
|
92
|
+
props.merge!(kwargs)
|
|
93
|
+
inertia(first, props: props, layout: layout)
|
|
84
94
|
else
|
|
85
95
|
super(*args, **kwargs, &block)
|
|
86
96
|
end
|
|
@@ -90,6 +100,10 @@ module Sinatra
|
|
|
90
100
|
request.env['HTTP_X_INERTIA'] == 'true'
|
|
91
101
|
end
|
|
92
102
|
|
|
103
|
+
def page_request?
|
|
104
|
+
inertia_request?
|
|
105
|
+
end
|
|
106
|
+
|
|
93
107
|
# CSRF token for the current request. Mounted by CSRFMiddleware
|
|
94
108
|
# (`set :inertia_csrf_protection, true` by default). Pair this with
|
|
95
109
|
# `inertia_share { { csrfToken: csrf_token } }` so the React/Vue
|
|
@@ -101,6 +115,26 @@ module Sinatra
|
|
|
101
115
|
request.env['sinatra.inertia.csrf_token']
|
|
102
116
|
end
|
|
103
117
|
|
|
118
|
+
def always(value = nil, &block)
|
|
119
|
+
Sinatra::Inertia.always(value, &block)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def defer(group: 'default', &block)
|
|
123
|
+
Sinatra::Inertia.defer(group: group, &block)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def optional(&block)
|
|
127
|
+
Sinatra::Inertia.optional(&block)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def lazy(&block)
|
|
131
|
+
Sinatra::Inertia.lazy(&block)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def merge(value = nil, &block)
|
|
135
|
+
Sinatra::Inertia.merge(value, &block)
|
|
136
|
+
end
|
|
137
|
+
|
|
104
138
|
# ------------------------------------------------------------------
|
|
105
139
|
# Shared props — runtime accessors (the `inertia_share` class DSL is
|
|
106
140
|
# in extension.rb, this is the per-request resolver).
|
|
@@ -119,7 +153,11 @@ module Sinatra
|
|
|
119
153
|
# ------------------------------------------------------------------
|
|
120
154
|
# Asset version
|
|
121
155
|
def current_inertia_version
|
|
122
|
-
v = settings.respond_to?(:
|
|
156
|
+
v = if settings.respond_to?(:page_version)
|
|
157
|
+
settings.page_version
|
|
158
|
+
elsif settings.respond_to?(:inertia_version)
|
|
159
|
+
settings.inertia_version
|
|
160
|
+
end
|
|
123
161
|
v.respond_to?(:call) ? v.call.to_s : v.to_s
|
|
124
162
|
end
|
|
125
163
|
|
|
@@ -137,14 +175,26 @@ module Sinatra
|
|
|
137
175
|
end
|
|
138
176
|
end
|
|
139
177
|
|
|
178
|
+
def page_errors(payload = nil)
|
|
179
|
+
inertia_errors(payload)
|
|
180
|
+
end
|
|
181
|
+
|
|
140
182
|
def inertia_clear_history!
|
|
141
183
|
@inertia_clear_history = true
|
|
142
184
|
end
|
|
143
185
|
|
|
186
|
+
def clear_history!
|
|
187
|
+
inertia_clear_history!
|
|
188
|
+
end
|
|
189
|
+
|
|
144
190
|
def inertia_encrypt_history!(flag = true)
|
|
145
191
|
@inertia_encrypt_history_override = flag
|
|
146
192
|
end
|
|
147
193
|
|
|
194
|
+
def encrypt_history!(flag = true)
|
|
195
|
+
inertia_encrypt_history!(flag)
|
|
196
|
+
end
|
|
197
|
+
|
|
148
198
|
def inertia_errors_payload
|
|
149
199
|
errors = session[:_inertia_errors]
|
|
150
200
|
return nil if errors.nil?
|
|
@@ -167,6 +217,16 @@ module Sinatra
|
|
|
167
217
|
|
|
168
218
|
private
|
|
169
219
|
|
|
220
|
+
def current_page_layout
|
|
221
|
+
if settings.respond_to?(:page_layout)
|
|
222
|
+
settings.page_layout
|
|
223
|
+
elsif settings.respond_to?(:inertia_layout)
|
|
224
|
+
settings.inertia_layout
|
|
225
|
+
else
|
|
226
|
+
:layout
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
170
230
|
def deep_merge(a, b)
|
|
171
231
|
a.merge(b) do |_k, av, bv|
|
|
172
232
|
(av.is_a?(Hash) && bv.is_a?(Hash)) ? deep_merge(av, bv) : bv
|
data/lib/sinatra/inertia.rb
CHANGED
|
@@ -16,22 +16,23 @@ module Sinatra
|
|
|
16
16
|
# class App < Sinatra::Base
|
|
17
17
|
# register Sinatra::Inertia
|
|
18
18
|
#
|
|
19
|
-
# set :
|
|
20
|
-
# set :
|
|
19
|
+
# set :page_version, -> { ASSETS_VERSION }
|
|
20
|
+
# set :page_layout, :layout
|
|
21
21
|
#
|
|
22
|
-
#
|
|
22
|
+
# share_props do
|
|
23
23
|
# { auth: { user: current_user }, flash: flash_payload }
|
|
24
24
|
# end
|
|
25
25
|
#
|
|
26
26
|
# get '/' do
|
|
27
|
-
#
|
|
27
|
+
# render 'Todos/Index', todos: -> { Todo.all }
|
|
28
28
|
# end
|
|
29
29
|
# end
|
|
30
30
|
#
|
|
31
31
|
# See README.md for the full feature matrix.
|
|
32
32
|
module Inertia
|
|
33
33
|
def self.registered(app)
|
|
34
|
-
# Default settings
|
|
34
|
+
# Default settings. `page_*` is the recommended application-facing API;
|
|
35
|
+
# `inertia_*` remains supported for existing apps and lower-level tuning.
|
|
35
36
|
app.set :inertia_version, '1' unless app.respond_to?(:inertia_version)
|
|
36
37
|
app.set :inertia_layout, :layout unless app.respond_to?(:inertia_layout)
|
|
37
38
|
app.set :inertia_encrypt_history, false unless app.respond_to?(:inertia_encrypt_history)
|
|
@@ -47,15 +48,26 @@ module Sinatra
|
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# Mount protocol middleware (version mismatch + 303 redirect promotion).
|
|
50
|
-
app.use Sinatra::Inertia::Middleware, version:
|
|
51
|
+
app.use Sinatra::Inertia::Middleware, version: lambda {
|
|
52
|
+
version = if app.settings.respond_to?(:page_version)
|
|
53
|
+
app.settings.page_version
|
|
54
|
+
else
|
|
55
|
+
app.settings.inertia_version
|
|
56
|
+
end
|
|
57
|
+
version.respond_to?(:call) ? version.call.to_s : version.to_s
|
|
58
|
+
}
|
|
51
59
|
|
|
52
|
-
# Class-level DSL: `
|
|
53
|
-
#
|
|
60
|
+
# Class-level DSL: `share_props { ... }` registers a block whose return
|
|
61
|
+
# value is merged into every page's props.
|
|
54
62
|
app.define_singleton_method(:inertia_share) do |&block|
|
|
55
63
|
raise ArgumentError, 'inertia_share requires a block' unless block
|
|
56
64
|
settings.inertia_share_blocks = settings.inertia_share_blocks + [block]
|
|
57
65
|
end
|
|
58
66
|
|
|
67
|
+
app.define_singleton_method(:share_props) do |&block|
|
|
68
|
+
inertia_share(&block)
|
|
69
|
+
end
|
|
70
|
+
|
|
59
71
|
app.helpers Sinatra::Inertia::Helpers
|
|
60
72
|
end
|
|
61
73
|
|
|
@@ -72,4 +84,3 @@ end
|
|
|
72
84
|
# overwrite an existing `::Inertia` constant — if your app has one, use
|
|
73
85
|
# `Sinatra::Inertia.defer` instead.
|
|
74
86
|
::Inertia = Sinatra::Inertia unless defined?(::Inertia)
|
|
75
|
-
|