sinatra-inertia 0.1.3 → 0.1.5

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: d9172865c13484ad3352d36558c065514b1f3f175fd70ec8ce4f7cde77fbc3f8
4
+ data.tar.gz: f308c6741aed6cf48f7fc0588e7e0e11a7d5bad99b07000a20982fa01ee894b4
5
5
  SHA512:
6
- metadata.gz: 3ab6eda91b4f83b63d90d3cd56281edb83222e99692396748544497dd5c4637aedaf8f557bf7777b3f03c743ab15c1ec483d50f8b88e415d15aef071aade9a47
7
- data.tar.gz: 4d1b984e23f72254fad4e1a6da82498cb35ca8f26f5270c5ab1ddaa4149118926a1c9a00ce5c7c061318e9d9da7693d412b50c4952432ba9d909a82394376914
6
+ metadata.gz: 0c4c38a5f2a2a2f16b5f3d449d442a7bd378fb491ae078c67a71543c6501e4412b93fc85751d54a0da0dd907d5f66340602aeef51a0899a990de921c495a5e28
7
+ data.tar.gz: 73fabe7fc1fb6191b1c454785b5450f2889e784ea12890587d203e7ccf216ca1143f51241ab06ea7eff6347bd83fb0c7b5066fb0b95e4ac413af55f0b8cc77ab
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5 — 2026-06-25
4
+
5
+ - Extract the Ruby-way Kagero layer into the separate `sinatra-kagero` gem.
6
+ - Keep `sinatra-inertia` focused on the Inertia v2 wire protocol.
7
+ - Remove Phlex and Literal from the `sinatra-inertia` runtime dependency surface.
8
+
9
+ ## 0.1.4 — 2026-05-03
10
+
11
+ - Add the recommended Sinatra-native page API: `render 'Component', props`,
12
+ `share_props`, `set :page_version`, `set :page_layout`, and route helpers
13
+ `defer`, `always`, `optional`, `lazy`, `merge`, `page_errors`,
14
+ `clear_history!`, and `encrypt_history!`.
15
+ - Keep existing `inertia_*` helpers/settings and `Inertia.*` prop wrappers
16
+ working for compatibility.
17
+
3
18
  ## 0.1.3 — 2026-04-29
4
19
 
5
20
  - `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
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  if defined?(::HomuraRuntime) && defined?(::HomuraRuntime::AsyncRegistry)
14
14
  ::HomuraRuntime::AsyncRegistry.register_async_source do
15
- async_method 'Sinatra::Inertia::Response', :to_h
16
- async_method 'Sinatra::Inertia::Response', :to_json
15
+ async_method("Sinatra::Inertia::Response", :to_h)
16
+ async_method("Sinatra::Inertia::Response", :to_json)
17
17
  end
18
18
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'securerandom'
4
- require 'rack/utils'
3
+ require "securerandom"
4
+ require "rack/utils"
5
5
 
6
6
  module Sinatra
7
7
  module Inertia
@@ -36,9 +36,9 @@ module Sinatra
36
36
  # this only when the consumer ships its own CSRF defence. Safer
37
37
  # defaults assume the gem is responsible.
38
38
  class CSRFMiddleware
39
- COOKIE_NAME = 'XSRF-TOKEN'
40
- HEADER_KEY = 'HTTP_X_XSRF_TOKEN'
41
- ENV_TOKEN_KEY = 'sinatra.inertia.csrf_token'
39
+ COOKIE_NAME = "XSRF-TOKEN"
40
+ HEADER_KEY = "HTTP_X_XSRF_TOKEN"
41
+ ENV_TOKEN_KEY = "sinatra.inertia.csrf_token"
42
42
  SAFE_METHODS = %w[GET HEAD OPTIONS].freeze
43
43
 
44
44
  def initialize(app, same_site: :Lax)
@@ -54,31 +54,34 @@ module Sinatra
54
54
  unless safe_method?(env)
55
55
  header = env[HEADER_KEY].to_s
56
56
  if existing.nil? || header.empty? || !secure_compare(header, existing)
57
- return forbidden('CSRF token mismatch (expected matching X-XSRF-TOKEN header to XSRF-TOKEN cookie)')
57
+ return (forbidden(
58
+ "CSRF token mismatch (expected matching X-XSRF-TOKEN header to XSRF-TOKEN cookie)"
59
+ ))
58
60
  end
59
61
  end
60
62
 
61
63
  status, headers, body = @app.call(env)
62
- unless existing == token
63
- set_cookie!(headers, token)
64
- end
64
+ set_cookie!(headers, token) unless existing == token
65
65
  [status, headers, body]
66
66
  end
67
67
 
68
68
  private
69
69
 
70
70
  def safe_method?(env)
71
- SAFE_METHODS.include?(env['REQUEST_METHOD'])
71
+ SAFE_METHODS.include?(env["REQUEST_METHOD"])
72
72
  end
73
73
 
74
74
  def read_cookie(env)
75
- cookie_header = env['HTTP_COOKIE'].to_s
75
+ cookie_header = env["HTTP_COOKIE"].to_s
76
76
  return nil if cookie_header.empty?
77
- cookie_header.split(/;\s*/).each do |pair|
78
- name, value = pair.split('=', 2)
79
- next unless name == COOKIE_NAME
80
- return value
81
- end
77
+ cookie_header
78
+ .split(/;\s*/)
79
+ .each do |pair|
80
+ name, value = pair.split("=", 2)
81
+ next unless name == COOKIE_NAME
82
+ return value
83
+ end
84
+
82
85
  nil
83
86
  end
84
87
 
@@ -99,26 +102,34 @@ module Sinatra
99
102
 
100
103
  def set_cookie!(headers, token)
101
104
  attrs = "#{COOKIE_NAME}=#{token}; Path=/; SameSite=#{@same_site}"
102
- existing = headers['Set-Cookie']
105
+ existing = headers["Set-Cookie"]
103
106
  # Normalise to a newline-joined String regardless of Rack 2/3
104
107
  # conventions or downstream worker-runtime quirks. The Cloudflare
105
108
  # Workers adapter that homura ships with serialises Array-shaped
106
109
  # `Set-Cookie` headers as a literal JSON array, which breaks
107
110
  # cookie parsing on the client.
108
111
  prev = case existing
109
- when nil, '' then nil
110
- when Array then existing.join("\n")
111
- else existing.to_s
112
- end
113
- headers['Set-Cookie'] = prev ? "#{prev}\n#{attrs}" : attrs
112
+ when nil, ""
113
+ nil
114
+ when Array
115
+ existing.join("\n")
116
+ else
117
+ existing.to_s
118
+ end
119
+
120
+ headers["Set-Cookie"] = prev ? "#{prev}\n#{attrs}" : attrs
114
121
  end
115
122
 
116
123
  def forbidden(message)
117
124
  body = "#{message}\n"
118
- [403, {
119
- 'Content-Type' => 'text/plain; charset=utf-8',
120
- 'Content-Length' => body.bytesize.to_s
121
- }, [body]]
125
+ [
126
+ 403,
127
+ {
128
+ "Content-Type" => "text/plain; charset=utf-8",
129
+ "Content-Length" => body.bytesize.to_s
130
+ },
131
+ [body]
132
+ ]
122
133
  end
123
134
  end
124
135
  end
@@ -13,13 +13,12 @@ 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
21
20
 
22
- def initialize(block: nil, value: nil, group: 'default')
21
+ def initialize(block: nil, value: nil, group: "default")
23
22
  @block = block
24
23
  @value = value
25
24
  @group = group
@@ -44,9 +43,6 @@ module Sinatra
44
43
  # Should arrays returned by this prop be merged with the client's
45
44
  # existing array (Inertia 2 merge semantics)?
46
45
  def merge? = false
47
-
48
- # Once-only delivery (cleared from session/state after first emission).
49
- def once? = false
50
46
  end
51
47
 
52
48
  class AlwaysProp < Prop
@@ -74,7 +70,7 @@ module Sinatra
74
70
  module_function
75
71
 
76
72
  def always(value = nil, &block) = AlwaysProp.new(block: block, value: value)
77
- def defer(group: 'default', &block) = DeferredProp.new(block: block, group: group)
73
+ def defer(group: "default", &block) = DeferredProp.new(block: block, group: group)
78
74
  def optional(&block) = OptionalProp.new(block: block)
79
75
  def lazy(&block) = LazyProp.new(block: block)
80
76
  def merge(value = nil, &block) = MergeProp.new(block: block, value: value)
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require_relative 'response'
3
+ require "json"
4
+
5
+ require_relative "response"
5
6
 
6
7
  module Sinatra
7
8
  module Inertia
@@ -10,7 +11,7 @@ module Sinatra
10
11
  module Helpers
11
12
  # Render an Inertia response.
12
13
  #
13
- # inertia 'Todos/Index', props: { todos: -> { Todo.all } }
14
+ # render 'Todos/Index', todos: -> { Todo.all }
14
15
  #
15
16
  # Layout selection: the configured layout (default `:layout`) is
16
17
  # rendered for full HTML responses. The view receives `@page_json`
@@ -18,17 +19,18 @@ module Sinatra
18
19
  # attribute) and `@page` (the underlying Hash, useful for SSR or
19
20
  # custom rendering).
20
21
  def inertia(component, props: {}, layout: nil)
21
- layout = settings.respond_to?(:inertia_layout) ? settings.inertia_layout : :layout if layout.nil?
22
+ layout = current_page_layout if layout.nil?
22
23
 
23
24
  version = current_inertia_version
24
25
  shared = current_inertia_shared
25
26
  encrypt = if !@inertia_encrypt_history_override.nil?
26
- @inertia_encrypt_history_override == true
27
- elsif settings.respond_to?(:inertia_encrypt_history)
28
- settings.inertia_encrypt_history == true
29
- else
30
- false
31
- end
27
+ @inertia_encrypt_history_override == true
28
+ elsif settings.respond_to?(:inertia_encrypt_history)
29
+ settings.inertia_encrypt_history == true
30
+ else
31
+ false
32
+ end
33
+
32
34
  clear = @inertia_clear_history == true
33
35
 
34
36
  # Set the protocol response headers BEFORE we touch any async
@@ -40,8 +42,8 @@ module Sinatra
40
42
  # regardless of how the underlying runtime schedules the
41
43
  # awaited continuation.
42
44
  if inertia_request?
43
- content_type 'application/json; charset=utf-8'
44
- headers 'X-Inertia' => 'true', 'Vary' => 'X-Inertia'
45
+ content_type("application/json; charset=utf-8")
46
+ headers("X-Inertia" => "true", "Vary" => "X-Inertia")
45
47
  end
46
48
 
47
49
  # Read errors *before* sweeping so the response carries them, then
@@ -69,25 +71,49 @@ module Sinatra
69
71
 
70
72
  @page = page_hash
71
73
  @page_json = ::Rack::Utils.escape_html(page_json)
72
- erb layout, layout: false
74
+ erb(layout, layout: false)
73
75
  end
74
76
 
75
- # Rails-flavored alias: `render inertia: 'Component', props: {...}`
77
+ # Natural page-rendering API: `render 'Component', props_hash`.
78
+ # `render inertia: 'Component', props: {...}` remains available for
79
+ # existing apps, while non-page render calls still delegate to Sinatra.
76
80
  # We must preserve Sinatra's `render(engine, data = nil, options = {}, locals = {}, &block)`
77
81
  # signature for the non-inertia path, so we forward *args/**kwargs.
78
82
  def render(*args, **kwargs, &block)
79
83
  first = args.first
80
84
  if args.length == 1 && first.is_a?(Hash) && first.key?(:inertia)
81
- inertia(first[:inertia], props: first[:props] || {}, layout: first[:layout])
85
+ inertia(
86
+ first[:inertia],
87
+ props: first[:props] || {},
88
+ layout: first[:layout]
89
+ )
82
90
  elsif kwargs.key?(:inertia) && args.empty?
83
- inertia(kwargs[:inertia], props: kwargs[:props] || {}, layout: kwargs[:layout])
91
+ inertia(
92
+ kwargs[:inertia],
93
+ props: kwargs[:props] || {},
94
+ layout: kwargs[:layout]
95
+ )
96
+ elsif first.is_a?(String) &&
97
+ args.length <= 2 &&
98
+ (args.length == 1 || args[1].is_a?(Hash))
99
+ layout = kwargs.delete(:layout)
100
+ props = {}
101
+ props.merge!(args[1]) if args[1].is_a?(Hash)
102
+ explicit_props = kwargs.delete(:props)
103
+ props.merge!(explicit_props) if explicit_props.is_a?(Hash)
104
+ props.merge!(kwargs)
105
+ inertia(first, props: props, layout: layout)
84
106
  else
85
107
  super(*args, **kwargs, &block)
86
108
  end
87
109
  end
88
110
 
89
111
  def inertia_request?
90
- request.env['HTTP_X_INERTIA'] == 'true'
112
+ request.env["HTTP_X_INERTIA"] == "true"
113
+ end
114
+
115
+ def page_request?
116
+ inertia_request?
91
117
  end
92
118
 
93
119
  # CSRF token for the current request. Mounted by CSRFMiddleware
@@ -98,7 +124,27 @@ module Sinatra
98
124
  # exchange is already handled by the Inertia client; this helper is
99
125
  # mainly for hidden-field forms or non-XHR submissions.
100
126
  def csrf_token
101
- request.env['sinatra.inertia.csrf_token']
127
+ request.env["sinatra.inertia.csrf_token"]
128
+ end
129
+
130
+ def always(value = nil, &block)
131
+ Sinatra::Inertia.always(value, &block)
132
+ end
133
+
134
+ def defer(group: "default", &block)
135
+ Sinatra::Inertia.defer(group: group, &block)
136
+ end
137
+
138
+ def optional(&block)
139
+ Sinatra::Inertia.optional(&block)
140
+ end
141
+
142
+ def lazy(&block)
143
+ Sinatra::Inertia.lazy(&block)
144
+ end
145
+
146
+ def merge(value = nil, &block)
147
+ Sinatra::Inertia.merge(value, &block)
102
148
  end
103
149
 
104
150
  # ------------------------------------------------------------------
@@ -109,17 +155,21 @@ module Sinatra
109
155
  merged = {}
110
156
  blocks.each do |b|
111
157
  v = instance_exec(&b)
112
- if v.is_a?(Hash)
113
- merged = deep_merge(merged, v)
114
- end
158
+ merged = deep_merge(merged, v) if v.is_a?(Hash)
115
159
  end
160
+
116
161
  merged
117
162
  end
118
163
 
119
164
  # ------------------------------------------------------------------
120
165
  # Asset version
121
166
  def current_inertia_version
122
- v = settings.respond_to?(:inertia_version) ? settings.inertia_version : nil
167
+ v = if settings.respond_to?(:page_version)
168
+ settings.page_version
169
+ elsif settings.respond_to?(:inertia_version)
170
+ settings.inertia_version
171
+ end
172
+
123
173
  v.respond_to?(:call) ? v.call.to_s : v.to_s
124
174
  end
125
175
 
@@ -137,14 +187,26 @@ module Sinatra
137
187
  end
138
188
  end
139
189
 
190
+ def page_errors(payload = nil)
191
+ inertia_errors(payload)
192
+ end
193
+
140
194
  def inertia_clear_history!
141
195
  @inertia_clear_history = true
142
196
  end
143
197
 
198
+ def clear_history!
199
+ inertia_clear_history!
200
+ end
201
+
144
202
  def inertia_encrypt_history!(flag = true)
145
203
  @inertia_encrypt_history_override = flag
146
204
  end
147
205
 
206
+ def encrypt_history!(flag = true)
207
+ inertia_encrypt_history!(flag)
208
+ end
209
+
148
210
  def inertia_errors_payload
149
211
  errors = session[:_inertia_errors]
150
212
  return nil if errors.nil?
@@ -160,13 +222,21 @@ module Sinatra
160
222
  # assigning nil instead, which the JSON encoder still emits as
161
223
  # `null` and makes `inertia_errors_payload` treat the field as
162
224
  # absent on the next visit.
163
- if session.respond_to?(:[]=)
164
- session[:_inertia_errors] = nil
165
- end
225
+ session[:_inertia_errors] = nil if session.respond_to?(:[]=)
166
226
  end
167
227
 
168
228
  private
169
229
 
230
+ def current_page_layout
231
+ if settings.respond_to?(:page_layout)
232
+ settings.page_layout
233
+ elsif settings.respond_to?(:inertia_layout)
234
+ settings.inertia_layout
235
+ else
236
+ :layout
237
+ end
238
+ end
239
+
170
240
  def deep_merge(a, b)
171
241
  a.merge(b) do |_k, av, bv|
172
242
  (av.is_a?(Hash) && bv.is_a?(Hash)) ? deep_merge(av, bv) : bv
@@ -19,8 +19,8 @@ module Sinatra
19
19
  # The middleware is `register`-ed automatically by `Sinatra::Inertia`
20
20
  # via `app.use`, so consumer apps don't need to wire it manually.
21
21
  class Middleware
22
- INERTIA_HEADER = 'HTTP_X_INERTIA'
23
- INERTIA_VERSION_HEADER = 'HTTP_X_INERTIA_VERSION'
22
+ INERTIA_HEADER = "HTTP_X_INERTIA"
23
+ INERTIA_VERSION_HEADER = "HTTP_X_INERTIA_VERSION"
24
24
 
25
25
  def initialize(app, version:)
26
26
  @app = app
@@ -29,10 +29,10 @@ module Sinatra
29
29
 
30
30
  def call(env)
31
31
  if inertia_get?(env) && version_mismatch?(env)
32
- location = env['REQUEST_URI'] || build_url(env)
32
+ location = env["REQUEST_URI"] || build_url(env)
33
33
  return [
34
34
  409,
35
- { 'X-Inertia-Location' => location, 'Vary' => 'X-Inertia' },
35
+ {"X-Inertia-Location" => location, "Vary" => "X-Inertia"},
36
36
  []
37
37
  ]
38
38
  end
@@ -44,8 +44,9 @@ module Sinatra
44
44
  # requests: a Sinatra app may serve plain REST endpoints alongside
45
45
  # Inertia pages, and rewriting their 302s would silently change
46
46
  # HTTP semantics for non-Inertia clients.
47
- if status == 302 && env[INERTIA_HEADER] == 'true' &&
48
- %w[POST PUT PATCH DELETE].include?(env['REQUEST_METHOD'])
47
+ if status == 302 &&
48
+ env[INERTIA_HEADER] == "true" &&
49
+ %w[POST PUT PATCH DELETE].include?(env["REQUEST_METHOD"])
49
50
  status = 303
50
51
  end
51
52
 
@@ -55,7 +56,7 @@ module Sinatra
55
56
  private
56
57
 
57
58
  def inertia_get?(env)
58
- env[INERTIA_HEADER] == 'true' && env['REQUEST_METHOD'] == 'GET'
59
+ env[INERTIA_HEADER] == "true" && env["REQUEST_METHOD"] == "GET"
59
60
  end
60
61
 
61
62
  def version_mismatch?(env)
@@ -70,10 +71,10 @@ module Sinatra
70
71
  end
71
72
 
72
73
  def build_url(env)
73
- scheme = env['rack.url_scheme'] || 'http'
74
- host = env['HTTP_HOST'] || env['SERVER_NAME']
75
- path = env['PATH_INFO']
76
- qs = env['QUERY_STRING']
74
+ scheme = env["rack.url_scheme"] || "http"
75
+ host = env["HTTP_HOST"] || env["SERVER_NAME"]
76
+ path = env["PATH_INFO"]
77
+ qs = env["QUERY_STRING"]
77
78
  full = +"#{scheme}://#{host}#{path}"
78
79
  full << "?#{qs}" if qs && !qs.empty?
79
80
  full
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'deferred'
3
+ require_relative "deferred"
4
4
 
5
5
  module Sinatra
6
6
  module Inertia
@@ -12,12 +12,29 @@ module Sinatra
12
12
  # responses, or interpolated into the layout's `data-page` attribute
13
13
  # for full HTML responses.
14
14
  class Response
15
- attr_reader :component, :props, :request, :version, :url,
16
- :encrypt_history, :clear_history, :shared, :errors
17
-
18
- def initialize(component:, props:, request:, version:, url: nil,
19
- encrypt_history: false, clear_history: false,
20
- shared: {}, errors: nil)
15
+ attr_reader(
16
+ :component,
17
+ :props,
18
+ :request,
19
+ :version,
20
+ :url,
21
+ :encrypt_history,
22
+ :clear_history,
23
+ :shared,
24
+ :errors
25
+ )
26
+
27
+ def initialize(
28
+ component:,
29
+ props:,
30
+ request:,
31
+ version:,
32
+ url: nil,
33
+ encrypt_history: false,
34
+ clear_history: false,
35
+ shared: {},
36
+ errors: nil
37
+ )
21
38
  @component = component
22
39
  @props = props || {}
23
40
  @request = request
@@ -64,6 +81,7 @@ module Sinatra
64
81
  elsif value.is_a?(Prop) && value.deferred?
65
82
  (deferred_groups[value.group] ||= []) << k.to_s
66
83
  end
84
+
67
85
  i += 1
68
86
  end
69
87
 
@@ -85,19 +103,19 @@ module Sinatra
85
103
  private
86
104
 
87
105
  def partial_request?
88
- request.env['HTTP_X_INERTIA_PARTIAL_COMPONENT'] == component
106
+ request.env["HTTP_X_INERTIA_PARTIAL_COMPONENT"] == component
89
107
  end
90
108
 
91
109
  def partial_data_keys
92
- raw = request.env['HTTP_X_INERTIA_PARTIAL_DATA'].to_s
110
+ raw = request.env["HTTP_X_INERTIA_PARTIAL_DATA"].to_s
93
111
  return nil if raw.empty?
94
- raw.split(',').map(&:strip).reject(&:empty?).map(&:to_sym)
112
+ raw.split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
95
113
  end
96
114
 
97
115
  def partial_except_keys
98
- raw = request.env['HTTP_X_INERTIA_PARTIAL_EXCEPT'].to_s
116
+ raw = request.env["HTTP_X_INERTIA_PARTIAL_EXCEPT"].to_s
99
117
  return nil if raw.empty?
100
- raw.split(',').map(&:strip).reject(&:empty?).map(&:to_sym)
118
+ raw.split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
101
119
  end
102
120
 
103
121
  # Inertia 2.0: `X-Inertia-Reset: a,b` tells the server "the client
@@ -106,29 +124,30 @@ module Sinatra
106
124
  # outbound `mergeProps` array — the value itself is still resolved
107
125
  # and emitted, so the client just doesn't accumulate it.
108
126
  def reset_keys
109
- raw = request.env['HTTP_X_INERTIA_RESET'].to_s
127
+ raw = request.env["HTTP_X_INERTIA_RESET"].to_s
110
128
  return [] if raw.empty?
111
- raw.split(',').map(&:strip).reject(&:empty?).map(&:to_sym)
129
+ raw.split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
112
130
  end
113
131
 
114
132
  # Returns [included?, resolved_value]
115
133
  def decide(value, key, partial, partial_data, partial_except)
116
134
  if value.is_a?(Prop)
117
135
  if value.always?
118
- return [true, value.resolve]
136
+ return true, value.resolve
119
137
  elsif value.deferred?
120
- return [false, nil] unless partial && partial_data&.include?(key)
121
- return [true, value.resolve]
138
+ return false, nil unless partial && partial_data&.include?(key)
139
+ return true, value.resolve
122
140
  elsif value.optional?
123
- return [false, nil] unless partial && partial_data&.include?(key)
124
- return [true, value.resolve]
141
+ return false, nil unless partial && partial_data&.include?(key)
142
+ return true, value.resolve
125
143
  else
126
144
  # merge / once / plain Prop
127
145
  if partial
128
- return [false, nil] if partial_data && !partial_data.include?(key)
129
- return [false, nil] if partial_except&.include?(key)
146
+ return false, nil if partial_data && !partial_data.include?(key)
147
+ return false, nil if partial_except&.include?(key)
130
148
  end
131
- return [true, value.resolve]
149
+
150
+ return true, value.resolve
132
151
  end
133
152
  end
134
153
 
@@ -136,17 +155,19 @@ module Sinatra
136
155
  # A bare Proc/Lambda is treated as plain lazy: resolved every
137
156
  # request, but only when included.
138
157
  if partial
139
- return [false, nil] if partial_data && !partial_data.include?(key)
140
- return [false, nil] if partial_except&.include?(key)
158
+ return false, nil if partial_data && !partial_data.include?(key)
159
+ return false, nil if partial_except&.include?(key)
141
160
  end
142
- return [true, value.call]
161
+
162
+ return true, value.call
143
163
  end
144
164
 
145
165
  # Plain value
146
166
  if partial
147
- return [false, nil] if partial_data && !partial_data.include?(key)
148
- return [false, nil] if partial_except&.include?(key)
167
+ return false, nil if partial_data && !partial_data.include?(key)
168
+ return false, nil if partial_except&.include?(key)
149
169
  end
170
+
150
171
  [true, value]
151
172
  end
152
173
 
@@ -167,7 +188,9 @@ module Sinatra
167
188
  # via `.__await__`. On MRI this branch is dead code (no Cloudflare
168
189
  # constant, no js_promise?), so plain Ruby tests are unaffected.
169
190
  def await_if_promise(value)
170
- if defined?(::Cloudflare) && ::Cloudflare.respond_to?(:js_promise?) && ::Cloudflare.js_promise?(value)
191
+ if defined?(::Cloudflare) &&
192
+ ::Cloudflare.respond_to?(:js_promise?) &&
193
+ ::Cloudflare.js_promise?(value)
171
194
  value.__await__
172
195
  else
173
196
  value
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sinatra
4
4
  module Inertia
5
- VERSION = '0.1.3'
5
+ VERSION = "0.1.5"
6
6
  end
7
7
  end
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'sinatra/base'
4
- require_relative 'inertia/version'
5
- require_relative 'inertia/errors'
6
- require_relative 'inertia/deferred'
7
- require_relative 'inertia/response'
8
- require_relative 'inertia/middleware'
9
- require_relative 'inertia/csrf_middleware'
10
- require_relative 'inertia/helpers'
11
- require_relative 'inertia/async_sources'
3
+ require "sinatra/base"
4
+
5
+ require_relative "inertia/version"
6
+ require_relative "inertia/errors"
7
+ require_relative "inertia/deferred"
8
+ require_relative "inertia/response"
9
+ require_relative "inertia/middleware"
10
+ require_relative "inertia/csrf_middleware"
11
+ require_relative "inertia/helpers"
12
+ require_relative "inertia/async_sources"
12
13
 
13
14
  module Sinatra
14
15
  # Inertia.js v2 protocol adapter for Sinatra.
@@ -16,47 +17,69 @@ module Sinatra
16
17
  # class App < Sinatra::Base
17
18
  # register Sinatra::Inertia
18
19
  #
19
- # set :inertia_version, -> { ASSETS_VERSION }
20
- # set :inertia_layout, :layout
20
+ # set :page_version, -> { ASSETS_VERSION }
21
+ # set :page_layout, :layout
21
22
  #
22
- # inertia_share do
23
+ # share_props do
23
24
  # { auth: { user: current_user }, flash: flash_payload }
24
25
  # end
25
26
  #
26
27
  # get '/' do
27
- # inertia 'Todos/Index', props: { todos: -> { Todo.all } }
28
+ # render 'Todos/Index', todos: -> { Todo.all }
28
29
  # end
29
30
  # end
30
31
  #
31
32
  # See README.md for the full feature matrix.
32
33
  module Inertia
33
34
  def self.registered(app)
34
- # Default settings consumers override with `set :inertia_*` in app.
35
- app.set :inertia_version, '1' unless app.respond_to?(:inertia_version)
36
- app.set :inertia_layout, :layout unless app.respond_to?(:inertia_layout)
37
- app.set :inertia_encrypt_history, false unless app.respond_to?(:inertia_encrypt_history)
38
- app.set :inertia_csrf_protection, true unless app.respond_to?(:inertia_csrf_protection)
39
- app.set :inertia_share_blocks, []
35
+ # Default settings. `page_*` is the recommended application-facing API;
36
+ # `inertia_*` remains supported for existing apps and lower-level tuning.
37
+ app.set(:inertia_version, "1") unless app.respond_to?(:inertia_version)
38
+ app.set(:inertia_layout, :layout) unless app.respond_to?(:inertia_layout)
39
+ unless app.respond_to?(:inertia_encrypt_history)
40
+ app.set(:inertia_encrypt_history, false)
41
+ end
42
+
43
+ unless app.respond_to?(:inertia_csrf_protection)
44
+ app.set(:inertia_csrf_protection, true)
45
+ end
46
+
47
+ app.set(:inertia_share_blocks, [])
40
48
 
41
49
  # Mount CSRF middleware (double-submit XSRF-TOKEN cookie that
42
50
  # @inertiajs/* clients honour). Opt-out via
43
51
  # `set :inertia_csrf_protection, false` when the consumer ships
44
52
  # its own (e.g. Rack::Protection::AuthenticityToken).
45
53
  if app.settings.inertia_csrf_protection
46
- app.use Sinatra::Inertia::CSRFMiddleware
54
+ app.use(Sinatra::Inertia::CSRFMiddleware)
47
55
  end
48
56
 
49
57
  # Mount protocol middleware (version mismatch + 303 redirect promotion).
50
- app.use Sinatra::Inertia::Middleware, version: -> { app.settings.inertia_version }
58
+ app.use(
59
+ Sinatra::Inertia::Middleware,
60
+ version: lambda {
61
+ version = if app.settings.respond_to?(:page_version)
62
+ app.settings.page_version
63
+ else
64
+ app.settings.inertia_version
65
+ end
51
66
 
52
- # Class-level DSL: `inertia_share { ... }` registers a block whose
53
- # return value is merged into every page's `props.shared` payload.
67
+ version.respond_to?(:call) ? version.call.to_s : version.to_s
68
+ }
69
+ )
70
+
71
+ # Class-level DSL: `share_props { ... }` registers a block whose return
72
+ # value is merged into every page's props.
54
73
  app.define_singleton_method(:inertia_share) do |&block|
55
- raise ArgumentError, 'inertia_share requires a block' unless block
74
+ raise ArgumentError, "inertia_share requires a block" unless block
56
75
  settings.inertia_share_blocks = settings.inertia_share_blocks + [block]
57
76
  end
58
77
 
59
- app.helpers Sinatra::Inertia::Helpers
78
+ app.define_singleton_method(:share_props) do |&block|
79
+ inertia_share(&block)
80
+ end
81
+
82
+ app.helpers(Sinatra::Inertia::Helpers)
60
83
  end
61
84
 
62
85
  # Convenience module-level constructors so code can write
@@ -72,4 +95,3 @@ end
72
95
  # overwrite an existing `::Inertia` constant — if your app has one, use
73
96
  # `Sinatra::Inertia.defer` instead.
74
97
  ::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.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro Homma