ruact 0.0.11 → 0.0.13
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 +147 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +18 -8
- data/RELEASING.md +125 -139
- data/lib/generators/ruact/install/install_generator.rb +305 -126
- data/lib/generators/ruact/install/templates/AGENTS.md.tt +14 -13
- data/lib/generators/ruact/install/templates/initializer.rb.tt +29 -7
- data/lib/generators/ruact/install/templates/tsconfig.json.tt +3 -0
- data/lib/generators/ruact/layout/layout_generator.rb +52 -0
- data/lib/generators/ruact/scaffold/templates/controller.rb.tt +5 -3
- data/lib/ruact/configuration.rb +65 -13
- data/lib/ruact/controller/document_rendering.rb +72 -16
- data/lib/ruact/controller/page_rendering.rb +134 -0
- data/lib/ruact/controller/pages.rb +116 -0
- data/lib/ruact/controller.rb +78 -11
- data/lib/ruact/doctor.rb +233 -25
- data/lib/ruact/layout_source.rb +29 -7
- data/lib/ruact/navigation_boundary.rb +240 -0
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/railtie.rb +30 -0
- data/lib/ruact/server.rb +10 -1
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +158 -1
- data/lib/ruact/views/layouts/ruact.html.erb +32 -0
- data/lib/ruact.rb +29 -0
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/ruact-router.test.mjs +433 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +170 -7
- metadata +12 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -12
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- data/spec/support/rails_stub.rb +0 -115
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"noEmit": true,
|
|
10
10
|
"esModuleInterop": true,
|
|
11
11
|
"skipLibCheck": true,
|
|
12
|
+
// Vite's client types. Without them a client component importing a
|
|
13
|
+
// stylesheet for its side effect has no declaration to resolve against.
|
|
14
|
+
"types": ["vite/client"],
|
|
12
15
|
"baseUrl": ".",
|
|
13
16
|
"paths": {
|
|
14
17
|
"@/*": ["app/javascript/*"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "ruact"
|
|
5
|
+
|
|
6
|
+
module Ruact
|
|
7
|
+
module Generators
|
|
8
|
+
# Copies the layout ruact pages render into (`layouts/ruact`, shipped with
|
|
9
|
+
# the gem) into the app, so the app owns it.
|
|
10
|
+
#
|
|
11
|
+
# Nothing needs configuring afterwards: the gem's view path is APPENDED by
|
|
12
|
+
# the Railtie, behind the app's, so `app/views/layouts/ruact.html.erb` wins
|
|
13
|
+
# by view-path order the moment it exists. That is the point of the ejected
|
|
14
|
+
# copy — add the app's JavaScript, fonts, meta tags or anything else
|
|
15
|
+
# `config.layout_stylesheets` cannot express.
|
|
16
|
+
#
|
|
17
|
+
# The cost, said once here and again when the command runs: an ejected
|
|
18
|
+
# layout no longer changes when the gem does.
|
|
19
|
+
#
|
|
20
|
+
# Run: rails generate ruact:layout
|
|
21
|
+
class LayoutGenerator < Rails::Generators::Base
|
|
22
|
+
source_root Ruact.views_path
|
|
23
|
+
|
|
24
|
+
desc "Copies ruact's layout into app/views/layouts/ruact.html.erb so your app owns it"
|
|
25
|
+
|
|
26
|
+
DESTINATION = "app/views/layouts/ruact.html.erb"
|
|
27
|
+
|
|
28
|
+
# `copy_file` already refuses to overwrite silently: an existing file
|
|
29
|
+
# prompts, or is skipped/overwritten under `--skip`/`--force`, which is
|
|
30
|
+
# the Rails convention every other generator follows.
|
|
31
|
+
def copy_layout
|
|
32
|
+
copy_file "layouts/ruact.html.erb", DESTINATION
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Only on `generate` (not `destroy`), and phrased for what is true: the copy
|
|
36
|
+
# renders ruact pages while `config.layout` is "ruact" — under `true` or
|
|
37
|
+
# `false` it is not used at all.
|
|
38
|
+
def explain
|
|
39
|
+
return unless behavior == :invoke
|
|
40
|
+
|
|
41
|
+
say ""
|
|
42
|
+
say " #{DESTINATION} renders ruact pages in place of the one ruact ships,"
|
|
43
|
+
say " as long as config.layout is \"ruact\". It will no longer change when you upgrade the gem —"
|
|
44
|
+
say " compare it with #{File.join(Ruact.views_path, 'layouts/ruact.html.erb')}"
|
|
45
|
+
say " after an upgrade. Keep `<%= ruact_head_assets %>` in <head>, and"
|
|
46
|
+
say " `<div id=\"root\"></div>` with `<%= ruact_js_assets %>` in <body>:"
|
|
47
|
+
say " `rails ruact:doctor` checks all three."
|
|
48
|
+
say ""
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# <%= controller_class_name %>Controller — a complete CRUD on the v2 route-driven contract.
|
|
4
4
|
#
|
|
5
|
-
# `include Ruact::
|
|
6
|
-
# ApplicationController
|
|
7
|
-
#
|
|
5
|
+
# `include Ruact::Controller` makes this controller's pages ruact pages — the
|
|
6
|
+
# install no longer puts it on ApplicationController (Story 17.0g: island mode
|
|
7
|
+
# is the default; in whole-app mode, `ruact:install --app`, including it again
|
|
8
|
+
# here is a no-op). `include Ruact::Server` then gives it two behaviours:
|
|
8
9
|
#
|
|
9
10
|
# - GET actions (index/show/new/edit) need NO explicit `ruact_render` —
|
|
10
11
|
# Ruact::Controller#default_render activates the RSC pipeline implicitly
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
# NOTE: `include Ruact::Server` must come AFTER `protect_from_forgery` (inherited
|
|
23
24
|
# from ApplicationController) so the CSRF check precedes these actions.
|
|
24
25
|
class <%= controller_class_name %>Controller < ApplicationController
|
|
26
|
+
include Ruact::Controller
|
|
25
27
|
include Ruact::Server
|
|
26
28
|
|
|
27
29
|
# --- GET pages (rendered as RSC) -----------------------------------------
|
data/lib/ruact/configuration.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Ruact
|
|
|
9
9
|
# `Ruact::ConfigurationError` with the offending attribute, the caller's
|
|
10
10
|
# file:line, and the suggested fix. Re-calling `Ruact.configure` after boot
|
|
11
11
|
# replaces the configuration atomically and emits a `[ruact]` warning.
|
|
12
|
-
class Configuration
|
|
12
|
+
class Configuration # rubocop:disable Metrics/ClassLength -- an attribute list with a validator per attribute; Doctor and InstallGenerator carry the same disable
|
|
13
13
|
# The set of public attributes; new attributes added here automatically
|
|
14
14
|
# inherit the freeze contract via the `define_method` writer below.
|
|
15
15
|
ATTRIBUTES = %i[
|
|
@@ -25,6 +25,7 @@ module Ruact
|
|
|
25
25
|
signed_global_id_default_expires_in
|
|
26
26
|
shadcn_compatible_versions
|
|
27
27
|
layout
|
|
28
|
+
layout_stylesheets
|
|
28
29
|
].freeze
|
|
29
30
|
|
|
30
31
|
# @!attribute [r] manifest_path
|
|
@@ -144,17 +145,21 @@ module Ruact
|
|
|
144
145
|
# full-document render a browser gets on a normal navigation.
|
|
145
146
|
#
|
|
146
147
|
# - `false` (default) — render the gem's built-in minimal shell.
|
|
148
|
+
# - `"ruact"` — render through the layout the gem ships
|
|
149
|
+
# (`lib/ruact/views/layouts/ruact.html.erb`, Story 17.0b): CSRF, CSP,
|
|
150
|
+
# the client-component CSS, then the app stylesheets named in
|
|
151
|
+
# {#layout_stylesheets}. What `rails generate ruact:install` writes.
|
|
152
|
+
# It edits no layout of the app; an app that puts its own
|
|
153
|
+
# `app/views/layouts/ruact.html.erb` in place (`rails generate
|
|
154
|
+
# ruact:layout` copies the gem's) wins by view-path order.
|
|
147
155
|
# - `true` — render through the controller's normal Rails layout.
|
|
148
|
-
# -
|
|
156
|
+
# - another String — render through that named layout.
|
|
149
157
|
#
|
|
150
|
-
# The
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
# the
|
|
154
|
-
# the `false` default a ruact page renders with no app CSS at all
|
|
155
|
-
# which is why `rails generate ruact:install` writes `config.layout =
|
|
156
|
-
# true` into the generated initializer and adds `<%= ruact_js_assets %>`
|
|
157
|
-
# to your layout in the same run.
|
|
158
|
+
# The trade-off between the last two and `"ruact"`: the app's own layout
|
|
159
|
+
# brings its whole `<head>` (favicons, fonts, analytics, `<head>`-writing
|
|
160
|
+
# gems) but has to be wired by hand; the gem's layout needs no wiring but
|
|
161
|
+
# brings only the stylesheets. The built-in shell carries neither — under
|
|
162
|
+
# the `false` default a ruact page renders with no app CSS at all.
|
|
158
163
|
#
|
|
159
164
|
# **This setting is deliberately explicit — there is no auto-detection.**
|
|
160
165
|
# ruact used to try to infer whether your layout was ready by inspecting
|
|
@@ -173,10 +178,26 @@ module Ruact
|
|
|
173
178
|
# @note A ruact view is rendered in its own pass (it produces the component
|
|
174
179
|
# tree), so `content_for` declared inside the view does NOT reach the
|
|
175
180
|
# layout. Set document metadata from the controller instead.
|
|
176
|
-
# @example
|
|
177
|
-
# Ruact.configure { |c| c.layout = true }
|
|
178
|
-
# @example Use a dedicated layout for ruact pages only
|
|
181
|
+
# @example Render through the layout ruact ships (what ruact:install writes)
|
|
179
182
|
# Ruact.configure { |c| c.layout = "ruact" }
|
|
183
|
+
# @example Let your own application layout own the document
|
|
184
|
+
# Ruact.configure { |c| c.layout = true }
|
|
185
|
+
#
|
|
186
|
+
# @!attribute [r] layout_stylesheets
|
|
187
|
+
# @return [Array<Symbol, String>] The app stylesheets the gem's layout
|
|
188
|
+
# (`layouts/ruact`, used when `layout` is `"ruact"`) links, passed as-is
|
|
189
|
+
# to `stylesheet_link_tag`. They come AFTER the client-component CSS
|
|
190
|
+
# (`ruact_head_assets`), so the app's own CSS loads last and wins ties.
|
|
191
|
+
#
|
|
192
|
+
# Defaults to `[:app]`, what `rails new` 8.x puts in its layout: under
|
|
193
|
+
# Propshaft it expands to every stylesheet on the load path, including a
|
|
194
|
+
# Tailwind build. Under Sprockets `:app` means a file called `app.css`,
|
|
195
|
+
# which is why `rails generate ruact:install` writes `["application"]`
|
|
196
|
+
# there instead. `[]` links none. Anything more than a list of names —
|
|
197
|
+
# a media attribute, fonts, other `<head>` tags — is what ejecting the
|
|
198
|
+
# layout is for: `rails generate ruact:layout`.
|
|
199
|
+
# @example A Sprockets app
|
|
200
|
+
# Ruact.configure { |c| c.layout_stylesheets = ["application"] }
|
|
180
201
|
ATTRIBUTES.each do |attr|
|
|
181
202
|
attr_reader attr
|
|
182
203
|
|
|
@@ -231,6 +252,7 @@ module Ruact
|
|
|
231
252
|
@signed_global_id_default_expires_in = nil
|
|
232
253
|
@shadcn_compatible_versions = [1, 2]
|
|
233
254
|
@layout = false
|
|
255
|
+
@layout_stylesheets = [:app]
|
|
234
256
|
end
|
|
235
257
|
end
|
|
236
258
|
|
|
@@ -266,6 +288,10 @@ module Ruact
|
|
|
266
288
|
# reference, but a caller probing `frozen?` would see the right answer.
|
|
267
289
|
if value.is_a?(Proc)
|
|
268
290
|
value.freeze
|
|
291
|
+
elsif value.is_a?(Array)
|
|
292
|
+
# Story 17.0b — `layout_stylesheets` holds Strings; freezing only the
|
|
293
|
+
# Array would leave `Ruact.config.layout_stylesheets.first << "x"` open.
|
|
294
|
+
instance_variable_set("@#{attr}", value.map { |item| item.frozen? ? item : item.dup.freeze }.freeze)
|
|
269
295
|
else
|
|
270
296
|
instance_variable_set("@#{attr}", value.dup.freeze)
|
|
271
297
|
end
|
|
@@ -293,6 +319,7 @@ module Ruact
|
|
|
293
319
|
when :query_parent_controller then validate_query_parent_controller!(value)
|
|
294
320
|
when :shadcn_compatible_versions then validate_shadcn_compatible_versions!(value)
|
|
295
321
|
when :layout then validate_layout!(value)
|
|
322
|
+
when :layout_stylesheets then validate_layout_stylesheets!(value)
|
|
296
323
|
end
|
|
297
324
|
end
|
|
298
325
|
|
|
@@ -325,6 +352,31 @@ module Ruact
|
|
|
325
352
|
"false uses ruact's built-in shell."
|
|
326
353
|
end
|
|
327
354
|
|
|
355
|
+
# Story 17.0b — the arguments `layouts/ruact` passes to `stylesheet_link_tag`.
|
|
356
|
+
# Checked at boot because the layout splats them straight into a Rails
|
|
357
|
+
# helper, where a stray nil or Hash would surface as a first-render error
|
|
358
|
+
# instead of a legible configuration one.
|
|
359
|
+
#
|
|
360
|
+
# Propshaft reads `:app` / `:all` ONLY as the first item, and then ignores
|
|
361
|
+
# every other one (`case sources.first when :app then sources = …`): so
|
|
362
|
+
# `[:app, "theme"]` silently drops "theme", and `["reset", :app]` looks for
|
|
363
|
+
# a file named app.css and raises. Either is allowed only on its own.
|
|
364
|
+
def validate_layout_stylesheets!(value)
|
|
365
|
+
if value.is_a?(Array) && value.length > 1 && value.intersect?(%i[app all])
|
|
366
|
+
raise Ruact::ConfigurationError,
|
|
367
|
+
"Ruact::Configuration#layout_stylesheets: :app and :all stand alone — Propshaft reads " \
|
|
368
|
+
"them only as the whole list and drops anything next to them; got #{value.inspect}. " \
|
|
369
|
+
"Use [:app], or list the stylesheets by name."
|
|
370
|
+
end
|
|
371
|
+
return if value.is_a?(Array) && value.all? { |name| name.is_a?(Symbol) || (name.is_a?(String) && !name.empty?) }
|
|
372
|
+
|
|
373
|
+
raise Ruact::ConfigurationError,
|
|
374
|
+
"Ruact::Configuration#layout_stylesheets must be an Array of stylesheet names " \
|
|
375
|
+
"(Symbols or non-empty Strings, as you would pass to stylesheet_link_tag), " \
|
|
376
|
+
"e.g. [:app] or [\"application\"]; got #{value.inspect} (#{value.class.name}). " \
|
|
377
|
+
"Use [] to link none of your app's stylesheets."
|
|
378
|
+
end
|
|
379
|
+
|
|
328
380
|
def validate_max_upload_bytes!(value)
|
|
329
381
|
return if value.nil?
|
|
330
382
|
return if value.is_a?(Integer) && value >= 0
|
|
@@ -7,11 +7,15 @@ module Ruact
|
|
|
7
7
|
# Flight body an in-app navigation gets.
|
|
8
8
|
#
|
|
9
9
|
# Split out of `Ruact::Controller` because it answers one self-contained
|
|
10
|
-
# question
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
10
|
+
# question: who owns the `<head>`? `Ruact.config.layout` decides.
|
|
11
|
+
# `rails generate ruact:install` writes `"ruact"` — the layout the gem
|
|
12
|
+
# ships (Story 17.0b), which links the client-component CSS and the app's
|
|
13
|
+
# stylesheets named in `config.layout_stylesheets`, and edits no file of
|
|
14
|
+
# the app. `true` renders through the app's own layout instead, so its whole
|
|
15
|
+
# `<head>` (favicons, fonts, analytics, `<head>`-writing gems) reaches the
|
|
16
|
+
# page, at the price of wiring that layout by hand. `#ruact_html_shell` is
|
|
17
|
+
# the `false` fallback — deliberately minimal, with no slot for the app's
|
|
18
|
+
# stylesheets.
|
|
15
19
|
module DocumentRendering
|
|
16
20
|
extend ActiveSupport::Concern
|
|
17
21
|
|
|
@@ -42,15 +46,26 @@ module Ruact
|
|
|
42
46
|
# - `false` (the default) → the built-in shell. Byte-identical to ruact's
|
|
43
47
|
# behaviour before the layout path existed, with no detection in the
|
|
44
48
|
# way, so an app that has not opted in cannot be affected by any of this.
|
|
45
|
-
# - `
|
|
46
|
-
#
|
|
47
|
-
#
|
|
49
|
+
# - `"ruact"` (what `ruact:install` writes) → the layout the gem ships,
|
|
50
|
+
# found by name through the view path the Railtie appends. An app that
|
|
51
|
+
# ejects it (`rails generate ruact:layout`) wins by view-path order.
|
|
52
|
+
# - `true` / another String → the app's own layout, which has to call
|
|
53
|
+
# `<%= ruact_js_assets %>` next to a `<div id="root"></div>` (and
|
|
54
|
+
# `<%= ruact_head_assets %>` in `<head>`); the install prints those lines
|
|
55
|
+
# rather than editing the file.
|
|
48
56
|
#
|
|
49
57
|
# An opted-in layout that does not actually emit the assets would produce
|
|
50
58
|
# a blank page, so the rendered document is checked before it is
|
|
51
59
|
# committed: that is a configuration error, raised in development/test and
|
|
52
60
|
# logged-and-degraded in production rather than served to real traffic.
|
|
53
61
|
def render_ruact_document(payload)
|
|
62
|
+
__ruact_warn_turbo_form_submission if __ruact_local_env?
|
|
63
|
+
# Set for EVERY branch, the shell included: besides carrying the payload
|
|
64
|
+
# to a layout's zero-argument `ruact_js_assets`, it is how
|
|
65
|
+
# `ruact_head_assets` knows this document is ruact's and emits the Turbo
|
|
66
|
+
# meta (Story 17.0f). Removed in `ensure`, so a later plain-Rails render
|
|
67
|
+
# through the same layout never inherits it.
|
|
68
|
+
@ruact_flight_payload = payload
|
|
54
69
|
layout = Ruact.config.layout
|
|
55
70
|
return render html: ruact_html_shell(payload).html_safe, layout: false if layout == false
|
|
56
71
|
|
|
@@ -59,10 +74,10 @@ module Ruact
|
|
|
59
74
|
return render html: ruact_html_shell(payload).html_safe, layout: false
|
|
60
75
|
end
|
|
61
76
|
|
|
62
|
-
#
|
|
63
|
-
# match the `/\A@_/` protected-ivar filter) —
|
|
64
|
-
# zero-argument `ruact_js_assets` reaches THIS
|
|
65
|
-
|
|
77
|
+
# `@ruact_flight_payload` is copied to the view by Rails' `view_assigns`
|
|
78
|
+
# plumbing (the name does not match the `/\A@_/` protected-ivar filter) —
|
|
79
|
+
# that is how the layout's zero-argument `ruact_js_assets` reaches THIS
|
|
80
|
+
# render's Flight payload.
|
|
66
81
|
document = render_to_string(html: "".html_safe, layout: layout)
|
|
67
82
|
|
|
68
83
|
if ruact_document_mountable?(document)
|
|
@@ -119,7 +134,9 @@ module Ruact
|
|
|
119
134
|
lookup_context.exists?(name.to_s.delete_prefix("layouts/"), ["layouts"])
|
|
120
135
|
end
|
|
121
136
|
|
|
122
|
-
def __ruact_handle_unready_layout(
|
|
137
|
+
def __ruact_handle_unready_layout(layout, reason)
|
|
138
|
+
return __ruact_handle_missing_gem_layout if reason == :missing && layout == Ruact::GEM_LAYOUT
|
|
139
|
+
|
|
123
140
|
detail =
|
|
124
141
|
if reason == :missing
|
|
125
142
|
"no layout could be resolved for it"
|
|
@@ -131,10 +148,10 @@ module Ruact
|
|
|
131
148
|
message = <<~MSG.strip
|
|
132
149
|
ruact: #{controller_path}##{action_name} fell back to ruact's built-in HTML shell — #{detail}.
|
|
133
150
|
`Ruact.config.layout` is set, so this is a configuration error, not a default:
|
|
134
|
-
the built-in shell has
|
|
151
|
+
the built-in shell has none of your stylesheets, so your app's CSS does not reach this page.
|
|
135
152
|
Add `<%= ruact_js_assets %>` next to the `<div id="root"></div>` in your layout
|
|
136
|
-
(`rails
|
|
137
|
-
|
|
153
|
+
(`rails ruact:doctor` names the layout and the missing line), use the layout ruact ships
|
|
154
|
+
with `config.layout = "ruact"`, or set `config.layout = false` for the built-in shell.
|
|
138
155
|
MSG
|
|
139
156
|
|
|
140
157
|
# A MISSING layout is a legitimate per-controller choice — an API-shaped
|
|
@@ -155,6 +172,44 @@ module Ruact
|
|
|
155
172
|
logger&.error(message)
|
|
156
173
|
end
|
|
157
174
|
|
|
175
|
+
# Story 17.0b — `layouts/ruact` ships with the gem and the Railtie appends
|
|
176
|
+
# its view path. Missing means that path never reached this controller: the
|
|
177
|
+
# Railtie did not run, or something REPLACED the view paths instead of
|
|
178
|
+
# adding to them. Not a per-controller choice, so it is logged in
|
|
179
|
+
# production too — a page served without the app's CSS and no trace of why
|
|
180
|
+
# is the failure this layout exists to prevent.
|
|
181
|
+
def __ruact_handle_missing_gem_layout
|
|
182
|
+
message = <<~MSG.strip
|
|
183
|
+
ruact: #{controller_path}##{action_name} fell back to ruact's built-in HTML shell — ruact's own
|
|
184
|
+
layout (`layouts/ruact`, shipped with the gem) could not be found for this controller.
|
|
185
|
+
Its view path is added by the Ruact Railtie: check that the app boots through it, and that
|
|
186
|
+
nothing replaces this controller's view paths (`self.view_paths = …`) instead of adding to them.
|
|
187
|
+
Or copy the layout into your app with `rails generate ruact:layout`.
|
|
188
|
+
MSG
|
|
189
|
+
__ruact_local_env? ? logger&.info(message) : logger&.error(message)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Story 17.0h — a form on a Turbo page that posts to a ruact action. Turbo
|
|
193
|
+
# fetches it and gets back a ruact document, which carries
|
|
194
|
+
# `turbo-visit-control=reload`. What Turbo then does depends on the status
|
|
195
|
+
# and on a surrounding Turbo Frame (a 4xx reloads the form's page, a 2xx
|
|
196
|
+
# is dropped, a 5xx is pasted into the Turbo page), but never shows the
|
|
197
|
+
# answer — the validation errors, what was typed. The browser, submitting
|
|
198
|
+
# natively, would. Said in development and test (decision of Luiz); every
|
|
199
|
+
# Turbo request carries `X-Turbo-Request-Id`.
|
|
200
|
+
def __ruact_warn_turbo_form_submission
|
|
201
|
+
return if request.nil? || request.get? || request.head?
|
|
202
|
+
return if request.headers["X-Turbo-Request-Id"].blank?
|
|
203
|
+
|
|
204
|
+
logger&.warn(<<~MSG.strip)
|
|
205
|
+
[ruact] #{self.class.name}##{action_name} answered a Turbo form submission
|
|
206
|
+
(#{request.request_method} #{request.path}) with a ruact page (#{response.status}). Turbo does not
|
|
207
|
+
show a ruact page as a form's answer — it reloads a page, drops the answer, or pastes this document
|
|
208
|
+
into its own — so validation errors and what was typed are lost. Add data-turbo="false" to that
|
|
209
|
+
form, so the browser submits it and shows the answer.
|
|
210
|
+
MSG
|
|
211
|
+
end
|
|
212
|
+
|
|
158
213
|
def __ruact_local_env?
|
|
159
214
|
Rails.env.development? || Rails.env.test?
|
|
160
215
|
rescue StandardError
|
|
@@ -175,6 +230,7 @@ module Ruact
|
|
|
175
230
|
<meta charset="UTF-8" />
|
|
176
231
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
177
232
|
#{ruact_csrf_meta_tag}
|
|
233
|
+
#{ruact_head_assets}
|
|
178
234
|
<title>Rails RSC</title>
|
|
179
235
|
</head>
|
|
180
236
|
<body>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruact
|
|
4
|
+
module Controller
|
|
5
|
+
# Story 17.0i — an explicit `render` of a ruact page goes through ruact.
|
|
6
|
+
#
|
|
7
|
+
# `render :new, status: :unprocessable_entity`, the Rails idiom for a failed
|
|
8
|
+
# save, used to answer 500 on a ruact page: Rails rendered the template on
|
|
9
|
+
# its own and the first client component raised. `render` is PUBLIC, as
|
|
10
|
+
# Rails has it — the `responders` gem calls `controller.render`; Rails
|
|
11
|
+
# excludes its own public methods from `action_methods`, so it is not
|
|
12
|
+
# routable.
|
|
13
|
+
module PageRendering
|
|
14
|
+
# Story 17.0i — `render :new, status: :unprocessable_entity`, the Rails idiom
|
|
15
|
+
# for a failed save, on a ruact page. Rails would render the template itself,
|
|
16
|
+
# outside a `ruact_render`, and the first client component in it would raise.
|
|
17
|
+
# So a `render` of a ruact PAGE of this controller — the same predicate
|
|
18
|
+
# `default_render` uses: `ruact_page_action?` and the template in this
|
|
19
|
+
# controller's own folder — goes through `ruact_render`, with its `status:`,
|
|
20
|
+
# `locals:` and `location:`, when that `.html.erb` is the template RAILS
|
|
21
|
+
# would pick for this request: the format it negotiated (a `.json` path,
|
|
22
|
+
# `params[:format]`, the `respond_to` branch, the Accept header) decides, so
|
|
23
|
+
# a scaffold's `format.json { render :show }` still renders `show.json`.
|
|
24
|
+
# Everything else is Rails' own: other renderers (`json:`, `plain:`,
|
|
25
|
+
# `partial:`…, including the `render plain:` / `html:` ruact itself issues),
|
|
26
|
+
# a template of another folder, an action outside `ruact_pages`. `layout:`
|
|
27
|
+
# and `content_type:` do not apply: a ruact page is rendered into
|
|
28
|
+
# `config.layout`, as HTML or Flight, like every ruact page.
|
|
29
|
+
def render(*args, &block)
|
|
30
|
+
page = block ? nil : __ruact_page_render(args)
|
|
31
|
+
return super unless page
|
|
32
|
+
|
|
33
|
+
self.location = url_for(page[:location]) if page[:location]
|
|
34
|
+
# As Rails' own `assigns:` does: instance variables of the view.
|
|
35
|
+
page[:assigns]&.each { |name, value| instance_variable_set(:"@#{name}", value) }
|
|
36
|
+
__ruact_render(**page.slice(:template, :locals, :status, :details))
|
|
37
|
+
ensure
|
|
38
|
+
# The declined-option note explains THIS render only (review R4): a
|
|
39
|
+
# later render in the request — a `rescue_from`'s — must not inherit it.
|
|
40
|
+
if instance_variable_defined?(:@__ruact_declined_render_options)
|
|
41
|
+
remove_instance_variable(:@__ruact_declined_render_options)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# The options of a plain template render — the only `render` ruact takes
|
|
48
|
+
# over. Anything else (`json:`, `plain:`, `partial:`, a registered renderer
|
|
49
|
+
# like `turbo_stream:`, an option a gem adds, like wicked_pdf's `pdf:`, or a
|
|
50
|
+
# key Rails ignores, like `alert:`) means the render is not ruact's to
|
|
51
|
+
# answer — Rails renders it, and the error that follows names the option
|
|
52
|
+
# (Story 17.0i review R3, decision of Luiz).
|
|
53
|
+
# `prefixes:` is not among them (review R4): it names the folders to look
|
|
54
|
+
# in, and a page is this controller's own folder only.
|
|
55
|
+
TEMPLATE_RENDER_OPTIONS = %i[action template locals status location layout content_type
|
|
56
|
+
formats variants variant handlers locale assigns].freeze
|
|
57
|
+
private_constant :TEMPLATE_RENDER_OPTIONS
|
|
58
|
+
|
|
59
|
+
# The `ruact_render` arguments for an explicit `render` of one of this
|
|
60
|
+
# controller's ruact pages, or nil. Rails' own `_normalize_args` reads the
|
|
61
|
+
# arguments (`render :new` → action, `render "posts/new"` → template), on a
|
|
62
|
+
# copy: it hands a Hash argument back as itself.
|
|
63
|
+
def __ruact_page_render(args)
|
|
64
|
+
options = _normalize_args(*args.map { |arg| arg.is_a?(Hash) ? arg.dup : arg })
|
|
65
|
+
action = __ruact_render_target(options)
|
|
66
|
+
return nil unless action && self.class.ruact_page_action?(action)
|
|
67
|
+
|
|
68
|
+
page = self.class.ruact_template_path(action)
|
|
69
|
+
return nil unless File.exist?(page)
|
|
70
|
+
return __ruact_decline(options) unless (options.keys - TEMPLATE_RENDER_OPTIONS).empty?
|
|
71
|
+
|
|
72
|
+
details = __ruact_render_details(options)
|
|
73
|
+
return nil unless __ruact_rails_would_render?(action, details, page)
|
|
74
|
+
|
|
75
|
+
{ template: "#{controller_path}/#{action}", locals: options[:locals] || {}, status: options[:status],
|
|
76
|
+
location: options[:location], assigns: options[:assigns], details: details }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A render of a ruact page with an option ruact does not take goes to Rails.
|
|
80
|
+
# When it named the page (`action:` / `template:`), the options are kept so
|
|
81
|
+
# the error that follows — a client component rendered outside ruact — says
|
|
82
|
+
# which ones (`Ruact::ViewHelper#__ruact_component__`). `render json:` names
|
|
83
|
+
# no template: nothing to explain.
|
|
84
|
+
def __ruact_decline(options)
|
|
85
|
+
unknown = options.keys - TEMPLATE_RENDER_OPTIONS
|
|
86
|
+
@__ruact_declined_render_options = unknown if options.key?(:action) || options.key?(:template)
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The lookup details of a render, as Rails' own render passes them: each an
|
|
91
|
+
# Array, a nil ignored; `variant:` is `variants:`.
|
|
92
|
+
def __ruact_render_details(options)
|
|
93
|
+
details = options.slice(:formats, :variants, :handlers, :locale)
|
|
94
|
+
details[:variants] = options[:variant] if options.key?(:variant) && !options.key?(:variants)
|
|
95
|
+
details.compact.transform_values { |value| Array(value) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Whether Rails, left alone, would render `page` for `action`: the lookup
|
|
99
|
+
# it does itself, with the formats it negotiated for this request (and a
|
|
100
|
+
# render's own `formats:` / `variants:` / `handlers:` / `locale:`). A Flight request
|
|
101
|
+
# negotiates every format, HTML first — the page; a `.json` request or a
|
|
102
|
+
# `format.json` branch negotiates JSON — the JSON template, or Rails' own
|
|
103
|
+
# MissingTemplate.
|
|
104
|
+
def __ruact_rails_would_render?(action, details, page)
|
|
105
|
+
template = lookup_context.find_template(action, [controller_path], false, [], details)
|
|
106
|
+
__ruact_page_template?(template.identifier, action, page)
|
|
107
|
+
rescue ActionView::MissingTemplate
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The page itself, or a locale/variant of it Rails picked (`new.pt-BR.html.erb`,
|
|
112
|
+
# `new.html+phone.erb`): the same folder, the same action, HTML, ERB.
|
|
113
|
+
def __ruact_page_template?(identifier, action, page)
|
|
114
|
+
File.identical?(File.dirname(identifier), File.dirname(page)) &&
|
|
115
|
+
File.basename(identifier).match?(/\A#{Regexp.escape(action)}(\.[\w-]+)?\.html(\+[\w-]+)?\.erb\z/)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The action whose template `options` name, when it is one of this
|
|
119
|
+
# controller's: `action: "new"`, `template: "<controller_path>/new"`, or
|
|
120
|
+
# nothing at all (`render status: 422` — the current action's template).
|
|
121
|
+
def __ruact_render_target(options)
|
|
122
|
+
if options.key?(:template)
|
|
123
|
+
directory, name = File.split(options[:template].to_s)
|
|
124
|
+
directory == controller_path ? name : nil
|
|
125
|
+
elsif options.key?(:action)
|
|
126
|
+
name = options[:action].to_s
|
|
127
|
+
name.include?("/") ? nil : name
|
|
128
|
+
else
|
|
129
|
+
action_name
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruact
|
|
4
|
+
module Controller
|
|
5
|
+
# Story 17.0g (FR116, decision D2) — a ruact "page" is a whole controller or
|
|
6
|
+
# the actions it declares.
|
|
7
|
+
#
|
|
8
|
+
# Including `Ruact::Controller` makes every action with an `.html.erb` a
|
|
9
|
+
# page; `ruact_pages only:` / `except:` narrows that. `default_render` and
|
|
10
|
+
# the navigation boundary (Ruact::NavigationBoundary) both read
|
|
11
|
+
# `ruact_page_action?` — one definition, so what renders through ruact and
|
|
12
|
+
# what the router treats as ruact cannot disagree.
|
|
13
|
+
module Pages
|
|
14
|
+
extend ActiveSupport::Concern
|
|
15
|
+
|
|
16
|
+
included do
|
|
17
|
+
# Story 17.0g — the pages this controller declared with `ruact_pages`, or
|
|
18
|
+
# nil (every action with a template is a page). Inherited, and
|
|
19
|
+
# redeclarable by a subclass.
|
|
20
|
+
class_attribute :__ruact_pages, instance_accessor: false, instance_predicate: false, default: nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class_methods do
|
|
24
|
+
# Story 17.0g (FR116) — narrow which actions of this controller are ruact
|
|
25
|
+
# pages. Without it, every action with a template is one; with it, only
|
|
26
|
+
# the declared actions are, and the rest render as ordinary Rails.
|
|
27
|
+
#
|
|
28
|
+
# An action listed in `only:` is a page even without a template of its
|
|
29
|
+
# own — the way to have `ruact_render(template: "posts/show")` treated as
|
|
30
|
+
# one by the navigation boundary. Names that are not actions of the
|
|
31
|
+
# declaring controller fail loudly the first time one of its pages is
|
|
32
|
+
# looked up: a typo must not quietly become "not a ruact page". A
|
|
33
|
+
# declaration a subclass inherits is not checked against it — on a base
|
|
34
|
+
# controller it is a policy for children that each have some of the
|
|
35
|
+
# actions.
|
|
36
|
+
#
|
|
37
|
+
# @param only [Symbol, String, Array<Symbol, String>] the page actions
|
|
38
|
+
# @param except [Symbol, String, Array<Symbol, String>] every action but these
|
|
39
|
+
# @return [void]
|
|
40
|
+
# @example Only `show` renders through ruact
|
|
41
|
+
# class PostsController < ApplicationController
|
|
42
|
+
# include Ruact::Controller
|
|
43
|
+
# ruact_pages only: %i[show]
|
|
44
|
+
# end
|
|
45
|
+
def ruact_pages(only: nil, except: nil)
|
|
46
|
+
raise ArgumentError, "ruact_pages takes exactly one of only: or except: (#{name})" if only.nil? == except.nil?
|
|
47
|
+
|
|
48
|
+
self.__ruact_pages = { only: only && Array(only).map(&:to_s).freeze,
|
|
49
|
+
except: except && Array(except).map(&:to_s).freeze,
|
|
50
|
+
declared_in: self }.freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether `action` is a page this controller renders through ruact:
|
|
54
|
+
# every action unless `ruact_pages` narrowed it. Calling `ruact_pages`
|
|
55
|
+
# again replaces the declaration (it does not accumulate). The navigation boundary
|
|
56
|
+
# (Ruact::NavigationBoundary) and `default_render` both read this.
|
|
57
|
+
#
|
|
58
|
+
# @param action [String, Symbol]
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
# @raise [Ruact::ConfigurationError] when `ruact_pages` names an action
|
|
61
|
+
# this controller does not have
|
|
62
|
+
def ruact_page_action?(action)
|
|
63
|
+
declared = __ruact_pages
|
|
64
|
+
return true if declared.nil?
|
|
65
|
+
|
|
66
|
+
Pages.check_declared!(self, declared) if declared[:declared_in].equal?(self)
|
|
67
|
+
return declared[:only].include?(action.to_s) if declared[:only]
|
|
68
|
+
|
|
69
|
+
!declared[:except].include?(action.to_s)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Listed in `ruact_pages only:` by THIS controller, as a method of it — a
|
|
73
|
+
# page by declaration, template or not (the method renders another one).
|
|
74
|
+
# Everything else needs the template in the controller's own folder,
|
|
75
|
+
# which is what `default_render` renders: a template reached through a
|
|
76
|
+
# parent's folder (an inherited declaration, an inherited action, a
|
|
77
|
+
# template-only name) renders as plain Rails, so the router must not be
|
|
78
|
+
# told it is a ruact page. A subclass that renders another template on
|
|
79
|
+
# purpose redeclares its pages.
|
|
80
|
+
#
|
|
81
|
+
# @param action [String, Symbol]
|
|
82
|
+
# @return [Boolean]
|
|
83
|
+
def ruact_declared_page?(action)
|
|
84
|
+
declared = __ruact_pages
|
|
85
|
+
return false unless declared && declared[:declared_in].equal?(self)
|
|
86
|
+
|
|
87
|
+
Array(declared[:only]).include?(action.to_s) && action_methods.include?(action.to_s)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The typo check, run for the class that DECLARED the pages (see
|
|
92
|
+
# `ruact_pages`). Only `only:` is checked: an `except:` naming a missing
|
|
93
|
+
# action excludes nothing and harms nothing. A template-only action (no
|
|
94
|
+
# method, a view Rails renders implicitly) is an action.
|
|
95
|
+
#
|
|
96
|
+
# @api private
|
|
97
|
+
# @param owner [Class] the controller that declared `declared`
|
|
98
|
+
# @param declared [Hash] a `__ruact_pages` declaration
|
|
99
|
+
# @return [void]
|
|
100
|
+
# @raise [Ruact::ConfigurationError] when `only:` names no action of it
|
|
101
|
+
def self.check_declared!(owner, declared)
|
|
102
|
+
names = Array(declared[:only])
|
|
103
|
+
return if names.empty?
|
|
104
|
+
|
|
105
|
+
unknown = names - owner.action_methods.to_a
|
|
106
|
+
unknown = unknown.reject { |action| File.exist?(owner.ruact_template_path(action)) }
|
|
107
|
+
return if unknown.empty?
|
|
108
|
+
|
|
109
|
+
what = unknown.one? ? "is not an action" : "are not actions"
|
|
110
|
+
raise Ruact::ConfigurationError,
|
|
111
|
+
"#{owner.name} declares ruact_pages for #{unknown.join(', ')}, which #{what} of it " \
|
|
112
|
+
"(no method and no template). Check the spelling in `ruact_pages`."
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|