inertia_hanami 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c9dd275f387237e18c63c965c3aeb67ec946f4f3b949aaf80ebd381faa99385
4
- data.tar.gz: 932b6b557d24d5ccfdcf382070b59e3c6c7de7bbca19ea0b4bb60f049f4b8c20
3
+ metadata.gz: 5fdb1da127e8e1768f47439a5b5a71e6d17e68dc0f4fd0a0535200024e7fa076
4
+ data.tar.gz: f142913ca589c4409a9ba99c6cc4abce692d05e1e047c9be80ec5dc26bece953
5
5
  SHA512:
6
- metadata.gz: 14fb6ba4fd996b0b78047aab232392a1087604dc337f0d1bb476cecee2ff05b4074e5ca2f000d02c3d87cac729a7a87bb681edd9d50bd8e300a911c0017d1989
7
- data.tar.gz: d7b920f79d30cc0473b83a7887a9c99997b27759ed6ff5e29e17e7a7df34bc40209a7309c92e9d749ffa84a3b9c8554d14bc004450986c47a9db3efb8a1a014c
6
+ metadata.gz: e09beeb585c74bbe8a4cd73d9c0a89994595d2517a6e1ea64a5f401a8de9ce1367b647f9b383b113e63acbca5b9cf73908ceefc75a8c40e1e5fcaa540e3cff49
7
+ data.tar.gz: fe7db5d264688a5bce470b08851ed34416947084611a9794eb398d9a94693ba0c66c5c478b4cf223db703dde0b4e946dab0fa3ea556855c85621359d0c5fc498
data/CHANGELOG.md CHANGED
@@ -1,4 +1,8 @@
1
- ## [Unreleased]
1
+ ## [0.1.1] - 2026-08-29
2
+
3
+ - Exclude `PLAN.md` from the packaged gem.
4
+
5
+ ## [0.1.0] - 2026-08-28
2
6
 
3
7
  - Add encrypted history support: `Configuration#encrypt_history` global default, class-level
4
8
  `encrypt_history` macro (inherited down subclasses), and instance-level `encrypt_history`/
@@ -13,6 +17,6 @@
13
17
  per the `X-Inertia-Infinite-Scroll-Merge-Intent` header) instead of replacing the existing
14
18
  prop, and reports pagination metadata via the response's `scrollProps` map.
15
19
 
16
- ## [0.1.0] - 2026-08-20
20
+ ## [0.0.0] - 2026-08-20
17
21
 
18
22
  - Initial release
data/README.md CHANGED
@@ -398,6 +398,12 @@ inertia_reload_only("name")
398
398
  expect(inertia).to have_exact_props(name: "Ada")
399
399
  ```
400
400
 
401
+ ## Example applications
402
+
403
+ - [book_react_hanami](https://github.com/kamalogudah/book_react_hanami) - React
404
+ - [book_svelte_hanami](https://github.com/kamalogudah/book_svelte_hanami) - Svelte
405
+ - [book_vue_hanami](https://github.com/kamalogudah/book_vue_hanami) - Vue
406
+
401
407
  ## Development
402
408
 
403
409
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InertiaHanami
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inertia_hanami
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Oguda
@@ -133,7 +133,6 @@ files:
133
133
  - CHANGELOG.md
134
134
  - CODE_OF_CONDUCT.md
135
135
  - LICENSE.txt
136
- - PLAN.md
137
136
  - README.md
138
137
  - Rakefile
139
138
  - lib/generators/inertia_hanami/install_generator.rb
data/PLAN.md DELETED
@@ -1,193 +0,0 @@
1
- # inertia_hanami — Implementation Plan
2
-
3
- Server-side adapter implementing the [Inertia.js protocol](https://inertiajs.com/docs/v2/installation/community-adapters)
4
- for [Hanami](https://hanakai.org/hanami) (targeting **Hanami 3.0+**, Ruby 3.3+).
5
-
6
- No prior Hanami/Inertia integration exists (verified via GitHub/forum search) — this is greenfield.
7
- Design is synthesized from two existing Ruby adapters:
8
-
9
- - **[inertia-rails](https://github.com/inertiajs/inertia-rails)** — the reference implementation. Full protocol
10
- coverage (deferred/merge/once props, SSR, encrypted history, testing helpers). Deeply coupled to Rails/ActiveSupport
11
- (`ActionController::Renderers`, `deep_merge!`, `HashWithIndifferentAccess`, Rails middleware stack, engine/railtie).
12
- - **[inertia-rage](https://github.com/rage-rb/inertia-rage)** — proves the protocol is portable to a non-Rails,
13
- non-ActiveSupport Ruby framework. Zero dependency on `inertia_rails`; clean-room reimplementation. Structurally the
14
- closer template for us, since Rage (like Hanami) is Rack-based with its own controller/config/plugin conventions
15
- and no ActiveSupport. It's missing SSR support and only ships RSpec helpers — gaps we should not blindly copy.
16
-
17
- ## What to take from each, concretely
18
-
19
- | Concern | Take from | Why |
20
- |---|---|---|
21
- | Protocol envelope shape, header names, prop-type taxonomy | inertia-rails | It's the canonical spec; inertia-rage's prop set is a subset |
22
- | Partial-reload resolution algorithm (only/except/reset/dot-paths) | inertia-rails (`props_resolver.rb`), cross-checked against inertia-rage's `protocol_builder.rb` | Same algorithm, inertia-rage's version is already ActiveSupport-free and shorter — better starting point to port |
23
- | Prop wrapper classes (`Optional`, `Defer`, `Merge`, `Always`, `Once`) | inertia-rage's `Data`-based implementation | Ruby's builtin `Data` class needs no ActiveSupport; inertia-rails uses plain classes + mixins, functionally equivalent but rage's is more idiomatic modern Ruby |
24
- | Controller/action integration pattern | Neither directly — Hanami's own `#auto_render?` + module `include` idiom | Hanami actions favor composition (`include Inertia::Action`) over subclassing a framework base class the way both Rails (`ActionController::Base`) and Rage (`RageController::Inertia`) do |
25
- | Middleware (version-mismatch 409, redirect-to-303 rewrite) | inertia-rails logic, inertia-rage's leaner Rack-only style | Same responsibilities, but implement as plain Rack middleware with no framework hooks, registered via `config.middleware.use` |
26
- | CSRF handling | **Neither** — reuse Hanami/Rack's own CSRF middleware | inertia-rails uses Rails' token CSRF; inertia-rage hand-rolls `Sec-Fetch-Site` checks. Hanami ships its own session/CSRF story via `hanami-controller` — bridge to that instead of reimplementing either |
27
- | Configuration mechanism | Hanami's own `register_provider` / container DI | Neither Rails initializers nor Rage's `Rage::Extension` apply; Hanami's provider system is the idiomatic equivalent |
28
- | Asset versioning | Concept from both (`ssr_bundle`/manifest hash, Vite manifest) | Adapted to `hanami-assets`' `assets.json` manifest instead of Vite's `manifest.json` |
29
- | SSR | inertia-rails only (inertia-rage has none) | Defer to a later phase; inertia-rails' `SSRRenderer` (POST page JSON to a Node process, cache by content hash) is the pattern to port |
30
- | Testing helpers | inertia-rails' RSpec matcher set (broader than inertia-rage's) | Port `have_props`, `have_exact_props`, `render_component`, `be_inertia_response`, etc. |
31
-
32
- ## Target Hanami integration points (from architecture research)
33
-
34
- - `Hanami::Action#handle(request, response)` is the Rack entrypoint. `response.format=`, `response.body=`,
35
- `response.headers`, `response[:key]=` (exposures) are all directly settable — no forced rendering step.
36
- - `#auto_render?(response)` returning `false` is the exact bypass point for JSON-only Inertia XHR responses (avoids
37
- Hanami auto-invoking the paired `Views::*` class for `X-Inertia: true` requests).
38
- - Layouts (`config.layout = "app"`, `app/templates/layouts/app.html.erb` convention — confirmed against
39
- `hanami-cli`'s app generator and `hanami-view`'s `layouts_dir`/`layout` config) are the analog of Rails'
40
- `application.html.erb` — used only for the **initial full-page load**, rendering `<div id="app" data-page="...">`.
41
- Note: `app/views/**/*.rb` holds `Hanami::View` subclasses; ERB markup (including layouts) lives under
42
- `app/templates/**`, not `app/views/**` as an earlier draft of this doc assumed.
43
- - View helpers meant to be callable unqualified from ERB templates/layouts (e.g. `inertia_root`) must be mixed
44
- into `<App>::Views::Helpers` (`app/views/helpers.rb`), not into the `View` base class — Hanami's
45
- `SliceConfiguredHelpers` auto-includes that module into the app's `Scope`/`Part` classes
46
- (`hanami/extensions/view/slice_configured_helpers.rb`), whereas including into `Hanami::View` itself has no
47
- effect on template scope.
48
- - **Action exposures do not automatically become template/layout locals.** `response[:key] = value` only reaches
49
- a view template as a local if the paired `Hanami::View` subclass declares `expose :key` (or a matching instance
50
- method); unexposed keys are silently dropped (`Hanami::View::Exposures#call`). Further, **the layout template
51
- receives a stricter, opt-in subset of the main template's locals** — only exposures declared
52
- `expose :key, layout: true` are forwarded into the layout's scope (`Exposure#for_layout?` defaults to `false`).
53
- So `inertia_root(page: page)` in a layout requires `expose :page, layout: true` in the view. (Verified
54
- empirically against installed `hanami-view` 3.0.2; its own YARD example implying automatic pass-through does
55
- not match runtime behavior.)
56
- - `config/providers/*.rb` + `Hanami.app.register_provider` is where gem-level configuration
57
- (`version`, `ssr_url`, `component_path_resolver`, etc.) gets registered as a container component
58
- (`"inertia.config"`), consumed via `include Deps["inertia.config"]` — the Hanami equivalent of
59
- `InertiaRails.configure`.
60
- - Verified against the installed `hanami` (3.0.2) / `dry-system` (1.2.5) / `dry-configurable` (1.4.0)
61
- gem source (Hanami's own built-in providers, e.g. `Hanami::Providers::DB`, `Hanami::Providers::I18n`,
62
- follow the same pattern):
63
- - **Namespacing is not automatic.** `register("config", obj)` inside a provider named `:inertia`
64
- only resolves to container key `"inertia.config"` when the provider is registered with
65
- `namespace: true` (`Hanami.app.register_provider(:inertia, namespace: true, source: InertiaHanami::Provider)`).
66
- Without it, the key would just be `"config"`.
67
- - **dry-configurable 1.4 does not delegate setting names onto the including instance.** A class that
68
- `include Dry::Configurable` and defines `setting :version` does *not* get an instance method
69
- `#version` — settings are only reachable via `#config` (e.g. `instance.config.version`). Confirmed
70
- empirically (`instance.respond_to?(:version) # => false`) and in every built-in Hanami provider,
71
- which reads its own settings as `config.foo`, never `self.foo`.
72
- - Consequence for this doc's literal usage below (`Hanami.app["inertia.config"].version`) to hold with
73
- no extra `.config.` hop: the provider must register the `Dry::Configurable::Config` object itself —
74
- `register("config", InertiaHanami::Configuration.new.config)` — not the `Configuration` instance.
75
- - `config.middleware.use` in `config/app.rb` registers Rack middleware app-wide; per-route middleware is also
76
- supported via `use` inside route `scope` blocks.
77
- - `hanami-assets` compiles to `public/assets/` with a manifest `assets.json` (source → hashed filename) — this
78
- supplies the Inertia asset **version** string (hash of `assets.json`), same role Vite's `manifest.json` plays for
79
- inertia-rage.
80
- - Action testing is DI-based (`Action.new(deps...).call(params)`, no HTTP roundtrip needed) — good fit for unit
81
- testing the prop-resolution engine directly; `rack-test` (bundled) covers full-stack request specs.
82
-
83
- ## Gem architecture
84
-
85
- ```
86
- lib/inertia_hanami.rb # entrypoint, requires + configure block
87
- lib/inertia_hanami/version.rb
88
- lib/inertia_hanami/configuration.rb # `include Dry::Configurable`; settings (version, ssr, root_view,
89
- # component_path_resolver, root_dom_id...) read via #config, dry-configurable 1.4 style
90
- lib/inertia_hanami/provider.rb # Hanami::Provider::Source; registers Configuration.new.config as "config" under
91
- # `register_provider(:inertia, namespace: true, ...)` -> resolves to "inertia.config"
92
- lib/inertia_hanami/props.rb # Optional/Defer/Merge/Always/Once wrappers, Ruby Data-based (port from inertia-rage)
93
- lib/inertia_hanami/prop_evaluator.rb # resolves Proc/BaseProp props against action instance
94
- lib/inertia_hanami/protocol_builder.rb # partial-reload resolution: only/except/reset/dot-paths (port+adapt from inertia-rage)
95
- lib/inertia_hanami/request_context.rb # parses X-Inertia-Partial-* / X-Inertia-Version headers off Rack env
96
- lib/inertia_hanami/renderer.rb # builds the page envelope hash, decides JSON vs HTML branch
97
- lib/inertia_hanami/action.rb # `Inertia::Action` module: include-able into Hanami::Action subclasses
98
- # - #auto_render?(response) => false when request.inertia?
99
- # - #inertia_render(component, props: {}, **opts)
100
- # - #inertia_share(**props) class + instance macro
101
- # - #inertia_location(url) for external redirects
102
- lib/inertia_hanami/middleware/version.rb # 409 + X-Inertia-Location on version mismatch (GET only)
103
- lib/inertia_hanami/middleware/redirects.rb # rewrite 301/302 -> 303 for PUT/PATCH/DELETE inertia requests
104
- lib/inertia_hanami/ssr_renderer.rb # (phase 3) POST page JSON to Node SSR server, cache by digest
105
- lib/inertia_hanami/helper.rb # view helper: inertia_root(page) -> data-page div/script tag
106
- lib/inertia_hanami/errors.rb
107
- lib/inertia_hanami/testing/rspec.rb # matchers: have_props, have_exact_props, be_inertia_response, render_component...
108
-
109
- app/templates/layouts/app.html.erb (generator template) -> renders <%= inertia_root(page: page) %>
110
- app/views/helpers.rb # `include InertiaHanami::Helper` into <App>::Views::Helpers,
111
- # so `inertia_root` is callable unqualified in templates
112
- lib/generators/inertia_hanami/install_generator.rb # (phase 2) scaffolds provider, layout, npm deps, example page
113
- ```
114
-
115
- ## Protocol mechanics (ported from inertia-rails spec, verified against inertia-rage's leaner implementation)
116
-
117
- - **Detection**: `request.env['HTTP_X_INERTIA'] == 'true'` (mirrors `ActionDispatch::Request#inertia?`).
118
- - **Response envelope**: `{component, props, url, version, encryptHistory, clearHistory}` (+ `deferredProps`,
119
- `mergeProps`, `deepMergeProps`, `matchPropsOn` metadata when relevant props are present).
120
- - **On Inertia request** (`X-Inertia: true` header present): set `X-Inertia: true` response header,
121
- `response.format = :json`, `response.body = [page.to_json]`. Skip Hanami's auto view rendering via `#auto_render? => false`.
122
- - **On initial load** (no `X-Inertia` header): render the layout with `inertia_root(page:)` embedding
123
- `<div id="app" data-page="#{page.to_json}"></div>`.
124
- - **Version mismatch**: `Middleware::Version` compares `X-Inertia-Version` request header against
125
- `Hanami.app["inertia.config"].version` on GET only; mismatch → `409` + `X-Inertia-Location` (empty body), so the
126
- client does a full browser visit and picks up new assets.
127
- - **Redirects**: rewrite `301/302` → `303` when the original method was PUT/PATCH/DELETE, so the browser doesn't
128
- resubmit the body on the client-side follow-up GET. External-origin redirects on an Inertia request become
129
- `409 + X-Inertia-Location` instead of a normal redirect (SPA can't follow cross-origin redirects via XHR).
130
- - **Partial reloads**: `X-Inertia-Partial-Component` must match the component about to render, else partial mode is
131
- ignored. `X-Inertia-Partial-Data` (only) / `X-Inertia-Partial-Except` (except) are comma-separated dot-paths.
132
- `AlwaysProp` bypasses filtering; `Optional`/`Defer` props are excluded unless explicitly requested; `Once` props
133
- are excluded once already sent (tracked via `X-Inertia-Reset` / an except-once header) — port inertia-rage's
134
- `ProtocolBuilder` (~180 lines, already ActiveSupport-free) rather than inertia-rails' more Rails-idiomatic version.
135
- - **Shared props**: `inertia_share(**hash, &block)` macro accumulates into an instance-level hash, block form
136
- `instance_exec`'d against the action at render time, merged (shallow, no need to reimplement `deep_merge!` unless
137
- a real use case demands it — start simple).
138
-
139
- ## Phased roadmap
140
-
141
- **Phase 0 — scaffolding & spec baseline**
142
- - Fill in gemspec (summary, homepage, dependencies: none required at runtime besides Ruby stdlib `json`; dev deps:
143
- `hanami`, `hanami-controller`, `hanami-view`, `rack-test`, `rspec`).
144
- - Set up a `spec/dummy_app` (minimal Hanami 3 app) for integration specs, matching how inertia-rails/inertia-rage
145
- test against real dummy apps rather than mocks.
146
-
147
- **Phase 1 — MVP protocol (no SSR, no generators)**
148
- - `Configuration` + provider registration.
149
- - `Props` wrappers + `PropEvaluator` + `ProtocolBuilder` (ported from inertia-rage, adapted naming).
150
- - `RequestContext` header parsing.
151
- - `Renderer` + `Action` module (`inertia_render`, `inertia_share`, `#auto_render?` override).
152
- - `Middleware::Version`, `Middleware::Redirects`.
153
- - `Helper#inertia_root` + example layout template.
154
- - ~~Manual end-to-end test: dummy app renders a component, partial reload works, version-mismatch 409 works.~~ —
155
- **Verified**: `spec/inertia_hanami/integration_spec.rb` exercises all three against the dummy app (both
156
- middlewares now registered via `config.middleware.use` in `spec/dummy_app/config/app.rb`).
157
-
158
- **Phase 2 — DX & polish**
159
- - Install generator (scaffolds provider file, layout, adds `@inertiajs/*` guidance to package.json, sample page).
160
- - Asset-version integration: derive `Configuration#version` automatically from `hanami-assets`' `assets.json` digest
161
- when not explicitly set.
162
- - RSpec testing helpers (`have_props`, `have_exact_props`, `have_no_prop`, `be_inertia_response`,
163
- `render_component`, partial-reload request helpers).
164
- - `flash`/`errors` prop wiring (Hanami session integration) analogous to inertia-rails' `always_include_errors_hash`.
165
-
166
- **Phase 3 — SSR (optional, follow inertia-rails' design)**
167
- - `SSRRenderer`: POST `page.to_json` to a configurable `ssr_url` (Node process), expects `{head:, body:}` JSON,
168
- render into the layout in place of the CSR div, cache by content digest, fall back to CSR on error
169
- (`ssr_raise_on_error` config toggle). No process-supervision plugin needed initially (unlike inertia-rails' Puma
170
- plugin) — document running the Node SSR server as a separate process.
171
-
172
- **Phase 4 — protocol completeness / parity audit**
173
- - ~~Encrypted history (`encryptHistory`/`clearHistory` session flags).~~ — **Done**: envelope
174
- plumbing (`Renderer`/`Action#inertia_render` kwargs) already existed; added the sticky
175
- mechanism — `Configuration#encrypt_history` global default, class-level `encrypt_history`
176
- macro (inherited down subclasses), instance-level `encrypt_history`/`clear_history`
177
- overrides, all resolved in `Action::InstanceMethods#inertia_history_encrypted?` /
178
- `#inertia_history_cleared?`. See `spec/inertia_hanami/action_spec.rb`'s "encrypted history"
179
- block and `spec/inertia_hanami/configuration_spec.rb`.
180
- - `Once`/scroll/infinite-scroll props if there's demand.
181
- - CSRF: confirm Hanami/Rack session middleware already covers the `X-XSRF-TOKEN` ↔ cookie handshake Inertia's
182
- client expects; add a thin bridge only if a gap is found — don't hand-roll CSRF from scratch like inertia-rage did.
183
-
184
- ## Open questions to resolve empirically against a real Hanami 3.0 app (docs were incomplete on these)
185
-
186
- 1. ~~Exact default layout file path/naming convention in Hanami 3.0~~ — **Resolved**: confirmed
187
- `app/templates/layouts/app.html.erb` (default layout name `"app"`) against `hanami-cli`'s app generator and
188
- `hanami-view`'s config; see "Target Hanami integration points" above.
189
- 2. Whether a provider can push onto `config.middleware` at boot time, or whether middleware registration must live
190
- in `config/app.rb` directly (affects whether the gem can self-register middleware via an install generator vs.
191
- requiring a manual `config.middleware.use InertiaHanami::Middleware::Version` line).
192
- 3. Per-route middleware/`use` block scoping semantics — needed if we want an opt-in per-route Inertia mode rather
193
- than app-wide.