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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e51ad95f421e2dc7d2eadc72b2c17b06377e2a72748a591e09d48106500786e
4
- data.tar.gz: 3f110beacd7e7eb6778440a800e6f31768d0cb44ff75546607a94ee22c50e066
3
+ metadata.gz: bf6ac9b17727011296d39f42de745b6c9cd6e27b6cd6ed73ced25c0795dd0c34
4
+ data.tar.gz: a9c6f651bf9017f99e3baf93f849f68c22844c8dce63ae6645a570de65d77a42
5
5
  SHA512:
6
- metadata.gz: 3ab6eda91b4f83b63d90d3cd56281edb83222e99692396748544497dd5c4637aedaf8f557bf7777b3f03c743ab15c1ec483d50f8b88e415d15aef071aade9a47
7
- data.tar.gz: 4d1b984e23f72254fad4e1a6da82498cb35ca8f26f5270c5ab1ddaa4149118926a1c9a00ce5c7c061318e9d9da7693d412b50c4952432ba9d909a82394376914
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 / once props, encrypted history,
6
- 303 redirect promotion, and error/flash session sweeps.
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. **Sinatra慣習に乗る.** Rendering a page is `inertia 'Component', props: {...}` —
17
- the same shape as Sinatra's `erb :index`. A `render inertia: 'Component'`
18
- alias is also provided for Rails refugees.
19
- 2. **Class-level config uses `set` and `inertia_share do ... end`,**
20
- matching Sinatra's existing DSL surface (`set :views`, `helpers do end`).
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
- set :inertia_version, -> { ENV.fetch('ASSETS_VERSION', '1') }
37
+ register Sinatra::Inertia
38
+
39
+ set :page_version, -> { ENV.fetch('ASSETS_VERSION', '1') }
37
40
 
38
41
  get '/' do
39
- inertia 'Pages/Hello', props: { name: 'world' }
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
- | `inertia(component, props:, layout:)` | Render an Inertia response (HTML on first hit, JSON on Inertia visit). |
65
- | `render(inertia: 'Comp', props: {...})` | Rails-style alias for the above. |
66
- | `inertia_request?` | True when `X-Inertia: true` header is present. |
67
- | `inertia_errors(payload = nil)` | Read or write validation errors that survive one redirect. |
68
- | `inertia_clear_history!` | Mark the next response's history as cleared. |
69
- | `inertia_encrypt_history!` | Mark the next response's history as encrypted. |
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 :inertia_version, -> { ... }` | Asset version. Mismatch on Inertia GET → 409 + `X-Inertia-Location`. |
76
- | `set :inertia_layout, :layout` | ERB layout used for full-page rendering (default `:layout`). |
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
- | `inertia_share do … end` | Block whose return Hash is merged into every page's props. |
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
- inertia 'Page', props: {
94
+ render 'Page',
84
95
  todos: -> { Todo.all }, # plain lazy
85
- csrf: Inertia.always { csrf_token }, # always sent
86
- stats: Inertia.defer(group: 'meta') { stats }, # excluded from initial response
87
- filter: Inertia.optional { params[:f] }, # only on partial-reload request
88
- feed: Inertia.merge(page_items) # client-side array merge
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
- | `Inertia.always { … }` | always included, even on partials that omit it. |
96
- | `Inertia.defer(group:) { … }` | excluded on initial visit; client refetches in second roundtrip. |
97
- | `Inertia.optional { … }` | only resolved when explicitly requested via `X-Inertia-Partial-Data`. |
98
- | `Inertia.lazy { … }` | alias of `optional` (Inertia v1 name). |
99
- | `Inertia.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`. |
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
- per-route via `inertia_encrypt_history!` / `inertia_clear_history!`.
115
- * **Errors session** — `inertia_errors(field: 'msg')` survives one redirect
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
- inertia_errors title: "can't be blank"
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
- inertia 'Todos/Index', props: {
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: Inertia.merge(page_items) # array merged with existing client-side 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
- # inertia 'Todos/Index', props: { todos: -> { Todo.all } }
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 = settings.respond_to?(:inertia_layout) ? settings.inertia_layout : :layout if layout.nil?
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
- # Rails-flavored alias: `render inertia: 'Component', props: {...}`
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?(:inertia_version) ? settings.inertia_version : nil
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sinatra
4
4
  module Inertia
5
- VERSION = '0.1.3'
5
+ VERSION = '0.1.4'
6
6
  end
7
7
  end
@@ -16,22 +16,23 @@ module Sinatra
16
16
  # class App < Sinatra::Base
17
17
  # register Sinatra::Inertia
18
18
  #
19
- # set :inertia_version, -> { ASSETS_VERSION }
20
- # set :inertia_layout, :layout
19
+ # set :page_version, -> { ASSETS_VERSION }
20
+ # set :page_layout, :layout
21
21
  #
22
- # inertia_share do
22
+ # share_props do
23
23
  # { auth: { user: current_user }, flash: flash_payload }
24
24
  # end
25
25
  #
26
26
  # get '/' do
27
- # inertia 'Todos/Index', props: { todos: -> { Todo.all } }
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 consumers override with `set :inertia_*` in app.
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: -> { app.settings.inertia_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: `inertia_share { ... }` registers a block whose
53
- # return value is merged into every page's `props.shared` payload.
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
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-inertia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro Homma