hibiki_rails 0.11.0 → 0.13.0
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 +51 -0
- data/README.md +2 -2
- data/lib/generators/hibiki/rails/multiselect_injections.rb +1 -1
- data/lib/generators/hibiki/rails/scaffold/USAGE +3 -2
- data/lib/generators/hibiki/rails/scaffold_controller/USAGE +1 -1
- data/lib/generators/hibiki/rails/scaffold_controller/scaffold_controller_generator.rb +1 -1
- data/lib/generators/hibiki/rails/scaffold_helpers.rb +13 -6
- data/lib/generators/hibiki/rails/scaffold_post_install.rb +11 -0
- data/lib/generators/hibiki/rails/upload_field_injections.rb +5 -4
- data/lib/hibiki/rails/helpers.rb +73 -7
- data/lib/hibiki/rails/version.rb +1 -1
- 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: dfd1da2a9d64baab573c47345a55fbe7f985c3e6781f2e0d04879043b0889dcc
|
|
4
|
+
data.tar.gz: f012a899f4c6b675b75102f217dc9e37feaa827e4c63097cc1a86de353dce543
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46110572a666ccd8f7c44114433259ebb901d50d7625ce7ff3468de7c0a34bbc6038766cf611eb977b1126d54fab8dbc018a55aa0a3aaab91553cf43fdcca048
|
|
7
|
+
data.tar.gz: 8b71c3b742e04e93b9e58707dcf7ebd1dda9319a0894108e07a5a560008f24591a43969e3ce1c4572e4c3ad76d0cf4fc025dd9fe3f518141bdf870e58261ab73
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,57 @@ The gem and the npm package are released in lockstep and share these version
|
|
|
4
4
|
numbers — `app/assets/javascripts/hibiki.js` is a single copy served both ways,
|
|
5
5
|
so importmap and bundler apps always resolve identical client code.
|
|
6
6
|
|
|
7
|
+
## 0.13.0 — 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
**The scaffold's query object lives in `app/queries`.** `hibiki:rails:scaffold`
|
|
12
|
+
and `scaffold_controller` write `app/queries/book_query.rb` where they wrote
|
|
13
|
+
`app/models/book_query.rb`. The constant is unchanged. `app/queries` is the
|
|
14
|
+
directory the query-object gems (ARQO, querifier, query_delegator) already
|
|
15
|
+
generate into, so a query one of them writes for the same model meets ours as
|
|
16
|
+
a file conflict instead of a second `BookQuery` that Rails loads first. The
|
|
17
|
+
move costs no extra restart: the scaffold already creates `app/forms`, and one
|
|
18
|
+
restart picks up both.
|
|
19
|
+
|
|
20
|
+
- Apps scaffolded earlier need nothing. Move the file with `git mv` when
|
|
21
|
+
convenient; `hibiki:rails:multiselect` and `upload_field` find it in either
|
|
22
|
+
place.
|
|
23
|
+
- A `--force` re-run writes `app/queries`. Rails autoloads `app/models` first,
|
|
24
|
+
so the post-install output asks you to delete the old file.
|
|
25
|
+
|
|
26
|
+
## 0.12.0 — 2026-09-06
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
**`island` — the ERB block helper for the island root.** One call
|
|
31
|
+
generates the cid, stamps the root with `hibiki_island`, and derives the
|
|
32
|
+
channel's `turbo_stream_from` inside it:
|
|
33
|
+
|
|
34
|
+
```erb
|
|
35
|
+
<%= island CounterChannel do |cid| %>
|
|
36
|
+
...
|
|
37
|
+
<% end %>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `cid:` defaults to a fresh UUID; the block receives it either way.
|
|
41
|
+
- The stream source is derived from the channel class
|
|
42
|
+
(`turbo_stream_from channel.channel_name, cid`), so renaming the channel
|
|
43
|
+
cannot leave a hand-typed streamable behind. `transport: :transmit`
|
|
44
|
+
leaves it out: for a channel that transmits, or one that overrides
|
|
45
|
+
`stream_name` and writes its own line in the block. (`:broadcast` is the
|
|
46
|
+
default; the option is a named value rather than a boolean so it cannot
|
|
47
|
+
be mistaken for Turbo's own `data-turbo="false"`.)
|
|
48
|
+
- `params:` passes through; `tag_name:` picks the root element; any other
|
|
49
|
+
keyword lands on it, with a `data:` hash merged beneath the island's own
|
|
50
|
+
keys.
|
|
51
|
+
- Class-only (`constantize` a dynamic name at the call site) and ERB-only
|
|
52
|
+
(it needs ActionView's `capture`). Phlex components keep
|
|
53
|
+
`div(**hibiki_island(...))`, which remains the primitive.
|
|
54
|
+
|
|
55
|
+
Generators are unchanged: their output keeps the explicit three-line form.
|
|
56
|
+
No client change; the npm release is the lockstep bump.
|
|
57
|
+
|
|
7
58
|
## 0.11.0 — 2026-09-05
|
|
8
59
|
|
|
9
60
|
### Changed
|
data/README.md
CHANGED
|
@@ -112,8 +112,8 @@ once per app, shared by every generated resource, and wired into your stylesheet
|
|
|
112
112
|
or layout automatically — the post-install output says which, or gives you the
|
|
113
113
|
line to add when it cannot tell.
|
|
114
114
|
|
|
115
|
-
Restart the server afterwards: `app/forms/`
|
|
116
|
-
autoload paths at boot.
|
|
115
|
+
Restart the server afterwards: `app/forms/` and `app/queries/` are new, and
|
|
116
|
+
Rails works out its autoload paths at boot.
|
|
117
117
|
|
|
118
118
|
### Render the reactive component
|
|
119
119
|
|
|
@@ -98,7 +98,7 @@ module Hibiki
|
|
|
98
98
|
|
|
99
99
|
# Rows are strict_loading, so the display line raises without these.
|
|
100
100
|
def inject_query_preload
|
|
101
|
-
inject_includes
|
|
101
|
+
inject_includes existing_query_path,
|
|
102
102
|
with_existing: /matched_scope\.includes\(([^)]*)\)/,
|
|
103
103
|
without: "apply_sort(matched_scope)",
|
|
104
104
|
wrapped: "apply_sort(matched_scope.includes(:#{association_name}))"
|
|
@@ -18,7 +18,7 @@ Example:
|
|
|
18
18
|
This will create:
|
|
19
19
|
db/migrate/XXXXXXXXXXXX_create_books.rb
|
|
20
20
|
app/models/book.rb
|
|
21
|
-
app/
|
|
21
|
+
app/queries/book_query.rb
|
|
22
22
|
app/channels/books_channel.rb
|
|
23
23
|
app/channels/book_channel.rb
|
|
24
24
|
app/forms/book_form.rb
|
|
@@ -28,7 +28,8 @@ Example:
|
|
|
28
28
|
And add to config/routes.rb:
|
|
29
29
|
resources :books
|
|
30
30
|
|
|
31
|
-
Restart the server afterwards if it is running — app/forms
|
|
31
|
+
Restart the server afterwards if it is running — app/forms and
|
|
32
|
+
app/queries are new.
|
|
32
33
|
|
|
33
34
|
Pass --phlex to emit Phlex components under app/views/books/*.rb instead of
|
|
34
35
|
ERB templates. The view layer is the only thing it changes; it needs the
|
|
@@ -86,7 +86,7 @@ module Hibiki
|
|
|
86
86
|
# Captured BEFORE anything is written, so post_install can tell which
|
|
87
87
|
# app/* directories are new — Rails computes autoload paths from that
|
|
88
88
|
# glob at boot, and a new one needs a restart.
|
|
89
|
-
@new_app_dirs = %w[app/forms app/channels app/models app/views].reject { exists?(it) }
|
|
89
|
+
@new_app_dirs = %w[app/forms app/queries app/channels app/models app/views].reject { exists?(it) }
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# One file per app, not per resource, and the only thing this generator
|
|
@@ -49,19 +49,26 @@ module Hibiki
|
|
|
49
49
|
def scaffold_controller_class_name = "#{controller_class_name}Controller"
|
|
50
50
|
|
|
51
51
|
# ---- destinations ----------------------------------------------------
|
|
52
|
-
#
|
|
53
|
-
# The query object goes in app/models, NOT a new app/queries: Rails
|
|
54
|
-
# computes autoload paths from the app/* glob at boot, so a new
|
|
55
|
-
# top-level directory is not autoloadable until a server restart.
|
|
56
|
-
# app/forms already costs one restart; two would be gratuitous.
|
|
57
52
|
|
|
58
53
|
def view_dir = File.join("app/views", controller_file_path)
|
|
59
54
|
def collection_channel_path = File.join("app/channels", "#{controller_file_path}_channel.rb")
|
|
60
55
|
def member_channel_path = File.join("app/channels", *class_path, "#{file_name}_channel.rb")
|
|
61
56
|
def model_path = File.join("app/models", *class_path, "#{file_name}.rb")
|
|
62
|
-
def query_path = File.join("app/
|
|
57
|
+
def query_path = File.join("app/queries", *class_path, "#{file_name}_query.rb")
|
|
63
58
|
def form_path = File.join("app/forms", *class_path, "#{file_name}_form.rb")
|
|
64
59
|
|
|
60
|
+
# Scaffolds before 0.13.0 wrote the query object to app/models. The
|
|
61
|
+
# add-on generators read existing_query_path so their injections land
|
|
62
|
+
# in the file the app loads; the scaffold itself always writes query_path.
|
|
63
|
+
def legacy_query_path = File.join("app/models", *class_path, "#{file_name}_query.rb")
|
|
64
|
+
|
|
65
|
+
def existing_query_path
|
|
66
|
+
return query_path if File.exist?(File.join(destination_root, query_path))
|
|
67
|
+
return legacy_query_path if File.exist?(File.join(destination_root, legacy_query_path))
|
|
68
|
+
|
|
69
|
+
query_path
|
|
70
|
+
end
|
|
71
|
+
|
|
65
72
|
def scaffold_controller_path
|
|
66
73
|
File.join("app/controllers", "#{controller_file_path}_controller.rb")
|
|
67
74
|
end
|
|
@@ -23,6 +23,7 @@ module Hibiki
|
|
|
23
23
|
# stylesheet, a leftover file. None of them is about the resource.
|
|
24
24
|
def app_notices
|
|
25
25
|
restart_notice
|
|
26
|
+
stale_query_notice
|
|
26
27
|
# These live with the code that chose their branch, like
|
|
27
28
|
# parent_notices — the outcomes are those modules' vocabulary.
|
|
28
29
|
stylesheet_notice
|
|
@@ -49,6 +50,16 @@ module Hibiki
|
|
|
49
50
|
"Please restart if the server is running.", :yellow
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
# Scaffolds before 0.13.0 wrote the query object to app/models, which
|
|
54
|
+
# autoloads ahead of app/queries: until the old copy goes, it is the one
|
|
55
|
+
# the app loads. A generator never deletes.
|
|
56
|
+
def stale_query_notice
|
|
57
|
+
return unless exists?(legacy_query_path)
|
|
58
|
+
|
|
59
|
+
say_status :stale, "#{legacy_query_path} has moved to #{query_path}. " \
|
|
60
|
+
"Please delete the old file.", :yellow
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
def rebuild_css_notice
|
|
53
64
|
return unless css?
|
|
54
65
|
|
|
@@ -103,13 +103,14 @@ module Hibiki
|
|
|
103
103
|
# Rows are strict_loading and frozen, so the thumbnail raises without
|
|
104
104
|
# these — on the index AND on show-page repaints (the member channel).
|
|
105
105
|
def inject_query_preload
|
|
106
|
-
|
|
106
|
+
path = existing_query_path
|
|
107
|
+
return if wired?(path, /\.#{with_attached}\b/)
|
|
107
108
|
|
|
108
|
-
unless wired?(
|
|
109
|
-
return manual_wiring(
|
|
109
|
+
unless wired?(path, QUERY_WINDOW_SCOPE)
|
|
110
|
+
return manual_wiring(path, " # preload the attachment:\n .#{with_attached}")
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
gsub_file(
|
|
113
|
+
gsub_file(path, QUERY_WINDOW_SCOPE) do |match|
|
|
113
114
|
head, scope, tail = match.match(QUERY_WINDOW_SCOPE).captures
|
|
114
115
|
"#{head}#{scope}.#{with_attached}#{tail}"
|
|
115
116
|
end
|
data/lib/hibiki/rails/helpers.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "cgi/escape"
|
|
4
4
|
require "json"
|
|
5
|
+
require "securerandom"
|
|
5
6
|
|
|
6
7
|
module Hibiki
|
|
7
8
|
module Rails
|
|
@@ -24,7 +25,8 @@ module Hibiki
|
|
|
24
25
|
# Both helpers return a `{ data: { ... } }` hash: splat it into Phlex
|
|
25
26
|
# element methods or Rails tag helpers (`tag.div(**hibiki_island(...))`).
|
|
26
27
|
# When the element needs other attributes on the same `data:` key, merge
|
|
27
|
-
# the hashes yourself (Phlex's `mix` does this).
|
|
28
|
+
# the hashes yourself (Phlex's `mix` does this). In ERB, #island wraps
|
|
29
|
+
# the root, the cid, and the Turbo stream source into one block helper.
|
|
28
30
|
#
|
|
29
31
|
# The emitted attribute names are a private contract between these
|
|
30
32
|
# helpers and the gem's JS — they version together; don't hand-write
|
|
@@ -53,7 +55,12 @@ module Hibiki
|
|
|
53
55
|
# the emitted markup instead of being an invisible default.
|
|
54
56
|
DEFAULT_INPUT_DEBOUNCE = 250
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
# How an island's channel sends HTML back: Turbo broadcasts to a named
|
|
59
|
+
# stream (the root needs a stream source inside), or transmit down the
|
|
60
|
+
# subscription itself (it does not).
|
|
61
|
+
TRANSPORTS = %i[broadcast transmit].freeze
|
|
62
|
+
|
|
63
|
+
private_constant :VALUE_NAME, :VALUE_TAG, :EVENT_NAME, :TRANSPORTS
|
|
57
64
|
|
|
58
65
|
# The shared name validator for both halves of a reactive value (the
|
|
59
66
|
# view-side data-hibiki-value placeholder and the channel's
|
|
@@ -67,6 +74,23 @@ module Hibiki
|
|
|
67
74
|
name
|
|
68
75
|
end
|
|
69
76
|
|
|
77
|
+
# A tag name lands in raw markup, so it is allowlisted wherever a
|
|
78
|
+
# helper takes one.
|
|
79
|
+
def self.tag_name(name, of)
|
|
80
|
+
name = name.to_s
|
|
81
|
+
raise ArgumentError, "#{of} tag #{name.inspect} must match #{VALUE_TAG.inspect}" unless VALUE_TAG.match?(name)
|
|
82
|
+
|
|
83
|
+
name
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# #island's transport option, allowlisted so a typo names itself.
|
|
87
|
+
def self.transport(value)
|
|
88
|
+
return value if TRANSPORTS.include?(value)
|
|
89
|
+
|
|
90
|
+
raise ArgumentError,
|
|
91
|
+
"island transport #{value.inspect} must be one of #{TRANSPORTS.map(&:inspect).join(', ')}"
|
|
92
|
+
end
|
|
93
|
+
|
|
70
94
|
# The shared validator for both halves of an `event->action` token.
|
|
71
95
|
def self.event_name(name)
|
|
72
96
|
name = name.to_s
|
|
@@ -105,6 +129,33 @@ module Hibiki
|
|
|
105
129
|
{ data: }
|
|
106
130
|
end
|
|
107
131
|
|
|
132
|
+
# The ERB spelling of the island root: a fresh cid, the #hibiki_island
|
|
133
|
+
# root, and the channel's own `turbo_stream_from channel_name, cid`
|
|
134
|
+
# inside it, in one block. The block receives the cid.
|
|
135
|
+
#
|
|
136
|
+
# <%= island CounterChannel do |cid| %>
|
|
137
|
+
# ...
|
|
138
|
+
# <% end %>
|
|
139
|
+
#
|
|
140
|
+
# `transport: :transmit` leaves the stream source out — for a channel
|
|
141
|
+
# that transmits, or one that overrides #stream_name and writes its own
|
|
142
|
+
# turbo_stream_from inside the block. Other keywords land on the root
|
|
143
|
+
# element; a `data:` hash is merged beneath the island's own keys.
|
|
144
|
+
# Class-only: a dynamic name is `"#{kind.camelize}Channel".constantize`
|
|
145
|
+
# at the call site, never taken from a request param. Phlex components
|
|
146
|
+
# keep `div(**hibiki_island(...))`.
|
|
147
|
+
def island(channel, cid: nil, params: nil, transport: :broadcast, tag_name: :div, **attributes,
|
|
148
|
+
&block)
|
|
149
|
+
island_guards!(channel, block)
|
|
150
|
+
tag_name = Helpers.tag_name(tag_name, "island")
|
|
151
|
+
transport = Helpers.transport(transport)
|
|
152
|
+
cid ||= SecureRandom.uuid
|
|
153
|
+
attributes[:data] = attributes[:data].to_h.merge(hibiki_island(channel, cid:, params:)[:data])
|
|
154
|
+
content = capture(cid, &block)
|
|
155
|
+
content = island_stream(channel, cid, content) if transport == :broadcast
|
|
156
|
+
tag.public_send(tag_name, content, **attributes)
|
|
157
|
+
end
|
|
158
|
+
|
|
108
159
|
# Forward an event on this element as a channel action.
|
|
109
160
|
#
|
|
110
161
|
# `event:` names the event, or a list of them — the left side of the
|
|
@@ -169,11 +220,7 @@ module Hibiki
|
|
|
169
220
|
# channels. Only the placeholder text is server-rendered: each site
|
|
170
221
|
# keeps its own tag, classes, and attributes across updates.
|
|
171
222
|
def reactive(name, placeholder = "", tag_name: :span)
|
|
172
|
-
tag_name = tag_name
|
|
173
|
-
unless VALUE_TAG.match?(tag_name)
|
|
174
|
-
raise ArgumentError,
|
|
175
|
-
"reactive value tag #{tag_name.inspect} must match #{VALUE_TAG.inspect}"
|
|
176
|
-
end
|
|
223
|
+
tag_name = Helpers.tag_name(tag_name, "reactive value")
|
|
177
224
|
html = %(<#{tag_name} data-hibiki-value="#{Helpers.value_name(name)}">) +
|
|
178
225
|
%(#{CGI.escapeHTML(placeholder.to_s)}</#{tag_name}>)
|
|
179
226
|
html.respond_to?(:html_safe) ? html.html_safe : html
|
|
@@ -185,6 +232,25 @@ module Hibiki
|
|
|
185
232
|
|
|
186
233
|
private
|
|
187
234
|
|
|
235
|
+
# #island's preconditions: a block, an ActionView receiver, a channel
|
|
236
|
+
# class.
|
|
237
|
+
def island_guards!(channel, block)
|
|
238
|
+
raise ArgumentError, "island needs a block" unless block
|
|
239
|
+
unless respond_to?(:output_buffer) && respond_to?(:tag)
|
|
240
|
+
raise ArgumentError, "island is ERB-only; use div(**hibiki_island(...)) in a Phlex component"
|
|
241
|
+
end
|
|
242
|
+
return if channel.is_a?(Class) && channel.respond_to?(:channel_name)
|
|
243
|
+
|
|
244
|
+
raise ArgumentError,
|
|
245
|
+
"island takes a channel class, got #{channel.inspect}; " \
|
|
246
|
+
"constantize a dynamic name at the call site"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# The broadcast transport's stream source, ahead of the block's content.
|
|
250
|
+
def island_stream(channel, cid, content)
|
|
251
|
+
safe_join([turbo_stream_from(channel.channel_name, cid), content])
|
|
252
|
+
end
|
|
253
|
+
|
|
188
254
|
# #on's per-control modifiers, kept out of the token grammar. Each is
|
|
189
255
|
# omitted when it matches the client's own default, so the common call
|
|
190
256
|
# still stamps exactly one attribute.
|
data/lib/hibiki/rails/version.rb
CHANGED