ruact 0.0.16 → 0.0.18
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 +47 -1
- data/README.md +1 -1
- data/lib/generators/ruact/install/templates/AGENTS.md.tt +6 -6
- data/lib/ruact/client_manifest.rb +29 -2
- data/lib/ruact/component_contract.rb +4 -2
- data/lib/ruact/controller.rb +3 -1
- data/lib/ruact/doctor.rb +64 -4
- data/lib/ruact/erb_preprocessor.rb +240 -137
- data/lib/ruact/errors.rb +15 -15
- data/lib/ruact/flight/renderer.rb +23 -9
- data/lib/ruact/flight/request.rb +31 -2
- data/lib/ruact/flight/row_emitter.rb +9 -0
- data/lib/ruact/flight/serializer.rb +22 -7
- data/lib/ruact/html_converter.rb +18 -0
- data/lib/ruact/render_pipeline.rb +16 -8
- data/lib/ruact/testing/component_query.rb +11 -4
- data/lib/ruact/testing/flight_wire_parser.rb +2 -2
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +13 -0
- data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +44 -2
- data/vendor/javascript/vite-plugin-ruact/flight-conformance.test.mjs +306 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +89 -14
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +804 -3
- data/vendor/javascript/vite-plugin-ruact/package.json +5 -1
- data/vendor/javascript/vite-plugin-ruact/ruact-router.test.mjs +53 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +46 -17
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-modules.js +40 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-webpack-globals.js +15 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/root-boundary.js +41 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +65 -83
- data/vendor/javascript/vite-plugin-ruact/runtime/suspense-boundary.js +83 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/transport.js +41 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/vendor/react-server-dom-webpack/LICENSE +21 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/vendor/react-server-dom-webpack/VERSION.json +5 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/vendor/react-server-dom-webpack/client.browser.development.js +5473 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/vendor/react-server-dom-webpack/client.browser.production.js +2126 -0
- data/vendor/javascript/vite-plugin-ruact/scripts/vendor-flight-client.mjs +124 -0
- data/vendor/javascript/vite-plugin-ruact/vitest.config.mjs +14 -0
- metadata +13 -4
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +0 -499
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +0 -436
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afeffed3bf00af43d750b220dbffbdd62758269d578bb649912d7fbee157df86
|
|
4
|
+
data.tar.gz: 2bae60da180b2a5b6c85fe49c3adc42d3431c1cfee7666c9fdbf79b92815049b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79f5785e73237b512b1fce27e9b085538c57954921bf92544fc8d4f80c2646f27e5e46baa23b1a565beb567b8e4762a108a0f2885b3ec85f9b822216889afadd
|
|
7
|
+
data.tar.gz: 3421862773a8f0669533bd70e39aed531b9ae2c4ce42205bc6939bc6311d76f5c45e4527d39752793bebc343af29ba96ebffd3fd58a6f414bb16fba879cfcbeb
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.18] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Client components take children from ERB.** Content between a component's opening and closing tags reaches it as `children`, evaluated on the server like the rest of the template: HTML, `<%= %>` output (escaped as usual), loops, conditionals, partials, and other components, the same one included.
|
|
15
|
+
|
|
16
|
+
```erb
|
|
17
|
+
<Card title={@post.title}>
|
|
18
|
+
<p><%= @post.body %></p>
|
|
19
|
+
<LikeButton postId={@post.id} />
|
|
20
|
+
</Card>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A component that declares a contract (`__ruactContract`) has to list `children` among its `props` or `slots` to take them. `<Suspense>` works inside a component and around one.
|
|
24
|
+
|
|
25
|
+
- **A misspelled component name is caught where it is written.** When a manifest is available, a template naming a component that does not exist now fails while it compiles, with `Ruact::UnknownComponentError` naming the tag, the template's file and line, and the closest component (`Did you mean "LikeButton"?`). It used to fail at render with no location. Without a manifest the render reports it, as before.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Every component tag must close.** `<Dialog open={true}>` with no closing tag used to render as a component without children. It now raises `Ruact::ComponentTagError` at file:line: write `<Dialog open={true} />`, or close it with `</Dialog>`. The same error names a closing tag that closes nothing, and a tag given both content and a `children={...}` prop.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
- **`Ruact::ChildrenNotSupportedError`.** It was raised for content between a component's tags, which is now supported. Rescue `Ruact::ComponentTagError` for tags that do not pair, or `Ruact::PreprocessorError` for any template error.
|
|
34
|
+
|
|
35
|
+
## [0.0.17] - 2026-10-06
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **The browser reads the page with React's own Flight client.** ruact had its own decoder, and most of the transport bugs fixed in 0.0.12–0.0.16 lived in it. The runtime now uses React's `react-server-dom-webpack` client (`client.browser`), which ships inside the gem: version 19.3.0, MIT, regenerated by `vendor/javascript/vite-plugin-ruact/scripts/vendor-flight-client.mjs`. Nothing is added to the app's `package.json`. The npm package would also install `webpack`, which this avoids. An app that needs a different version adds `react-server-dom-webpack` to its own `package.json`, and the Vite plugin uses that copy instead; a copy that is only installed as another package's dependency is not used. Vite's startup and build output and `rails ruact:doctor` say which copy is in use, and the doctor warns when the app's copy is built for another React minor than the app's `react`. Supported React: `^19.2.8`, checked in CI against 19.2.8 and 19.3.0. The bundle grows: the nav-islands example went from 74.35 kB to 80.82 kB gzipped (+6.47 kB), and the `.gem` from 373 KB to 427 KB.
|
|
40
|
+
|
|
41
|
+
- **The Flight wire changed to the format React's client reads.** The server and the browser runtime change together; a page served by this version needs this version's runtime. **When you deploy it,** a tab still open on the previous version cannot read the new responses: its next in-place navigation fails and the user has to reload. Code that parses ruact's wire itself, rather than through `Ruact::Testing`, needs updating:
|
|
42
|
+
- import rows are `[id, chunks, name]`, with `chunks` always `[]` (the runtime registers every component up front);
|
|
43
|
+
- a string of 1024 bytes or more is referenced as `"$<hex>"`, not `"$T<hex>"`;
|
|
44
|
+
- a `<Suspense>` boundary's type is a `"$Sreact.suspense"` symbol row, and its deferred content is a lazy child, `"children":"$L<hex>"`, inside a boundary the runtime registers as `ruact:boundary`;
|
|
45
|
+
- an error row is an object, `{"digest","name","message","stack","env"}`, whose `digest` is a stable code (`ruact:suspense-timeout`): React's production client keeps only the digest, and the runtime turns it back into the message;
|
|
46
|
+
- in development only, elements carry React's three development slots, so React does not warn about keys on ERB siblings and loops. The test environment keeps the production wire.
|
|
47
|
+
|
|
48
|
+
`Ruact::Testing`'s matchers read the new wire, and `have_ruact_component` ignores the runtime's own boundary.
|
|
49
|
+
|
|
50
|
+
- **An error in a `<Suspense>` child no longer risks the whole page.** React unmounts the page on a render error no component catches. Each Suspense child now sits inside a boundary that keeps the fallback on screen and passes the error to the router's `onError` as `[ruact] Server error: …`, in production too, as before. The same goes for a response that ends before the Suspense row arrives (a server error mid-stream, a dropped connection): `[ruact] Server error: the response ended before the whole page arrived`. The boundary handles only what the server or the stream produced; a crash in your own component still reaches your error boundaries. The next response for the page (a navigation, `revalidate()`) renders normally.
|
|
51
|
+
|
|
52
|
+
- **A render error shows on the page instead of emptying it.** React's client looks a client component up when it renders, so a component missing from the registry (no `"use client"`, a stale build) fails then, and React would remove the whole page. The page now shows `[ruact] Error: client component not registered: …` in its place, as the first load did before; the next navigation replaces it.
|
|
53
|
+
|
|
10
54
|
## [0.0.16] - 2026-10-03
|
|
11
55
|
|
|
12
56
|
### Fixed
|
|
@@ -436,7 +480,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
436
480
|
|
|
437
481
|
- **E2E test app** — `e2e/` Rails app (no DB, in-memory Post model) with full CRUD system tests validating the complete request cycle.
|
|
438
482
|
|
|
439
|
-
[Unreleased]: https://github.com/luizcg/ruact/compare/v0.0.
|
|
483
|
+
[Unreleased]: https://github.com/luizcg/ruact/compare/v0.0.18...HEAD
|
|
484
|
+
[0.0.18]: https://github.com/luizcg/ruact/releases/tag/v0.0.18
|
|
485
|
+
[0.0.17]: https://github.com/luizcg/ruact/releases/tag/v0.0.17
|
|
440
486
|
[0.0.16]: https://github.com/luizcg/ruact/releases/tag/v0.0.16
|
|
441
487
|
[0.0.15]: https://github.com/luizcg/ruact/releases/tag/v0.0.15
|
|
442
488
|
[0.0.14]: https://github.com/luizcg/ruact/releases/tag/v0.0.14
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
scripted and live with the project's other capture harnesses, and the message the demo
|
|
12
12
|
shows is pinned here by `spec/readme_demo_message_spec.rb`. -->
|
|
13
13
|
<img src="https://ruact.dev/readme-write-verify.gif" width="800"
|
|
14
|
-
alt="An ERB template holding a <LikeButton likes={@likes} /> tag, and the "use client" React component that tag resolves to. The component renders in a browser and its count changes when it is clicked.
|
|
14
|
+
alt="An ERB template holding a <LikeButton likes={@likes} /> tag, and the "use client" React component that tag resolves to. The component renders in a browser and its count changes when it is clicked. The tag's name is then misspelled, and the next request stops server-side with Ruact::UnknownComponentError, which names the tag, the template file and line, and the component it was probably meant to be. The name is corrected and the page renders." />
|
|
15
15
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
@@ -77,14 +77,14 @@ READ IT instead of simulating the name generation. Regenerate after routes/queri
|
|
|
77
77
|
|
|
78
78
|
## Five traps
|
|
79
79
|
|
|
80
|
-
1. **
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
`<Suspense fallback="..."
|
|
80
|
+
1. **Every component tag closes.** `<Badge label={@x} />` has no children;
|
|
81
|
+
`<Card title={@t}>...</Card>` passes the ERB between its tags — HTML, loops,
|
|
82
|
+
partials, other components — to the component as `children`. An opening tag
|
|
83
|
+
that never closes, or a closing tag with no opening, fails LOUDLY with a
|
|
84
|
+
`PreprocessorError` naming file:line. `<Suspense fallback="...">` pairs too.
|
|
85
85
|
2. **`{}` props are Ruby, not JavaScript.** `<Badge label={@post.title} />`
|
|
86
86
|
evaluates `@post.title` as a Ruby expression in the ERB. No JS ternaries,
|
|
87
|
-
no `{...spread}
|
|
87
|
+
no `{...spread}`. A quoted value (`title="Hi"`) is a
|
|
88
88
|
plain string and a bare attribute is `true`, as in JSX.
|
|
89
89
|
3. **One action, two response shapes.** A `Ruact::Server` non-GET action
|
|
90
90
|
answers JSON — its instance variables, or `204`, or `{"$redirect": path}` —
|
|
@@ -38,13 +38,22 @@ module Ruact
|
|
|
38
38
|
# Story 13.5 preprocess-time call-site validator; +nil+ means "no contract →
|
|
39
39
|
# no validation" (fail open).
|
|
40
40
|
class ClientManifest
|
|
41
|
+
# The +chunks+ of every import row (see {#resolve}). One shared frozen
|
|
42
|
+
# array: a render resolves each component once, and the allocation guard
|
|
43
|
+
# counts every array.
|
|
44
|
+
NO_CHUNKS = [].freeze
|
|
45
|
+
|
|
41
46
|
# Used by Flight::Serializer to produce I rows.
|
|
42
|
-
# Returns the metadata array
|
|
47
|
+
# Returns the metadata array React's Flight client reads: [id, chunks, name]
|
|
48
|
+
# (Story 18-1). +chunks+ is always empty: the browser runtime registers
|
|
49
|
+
# every client component eagerly (`virtual:ruact/registry`), so there is
|
|
50
|
+
# nothing to load — and React reads +chunks+ as `[chunkId, filename]` pairs,
|
|
51
|
+
# which the manifest's one-URL list is not.
|
|
43
52
|
def resolve(module_id, _export_name)
|
|
44
53
|
entry = by_module_id(module_id)
|
|
45
54
|
raise "ClientManifest: no entry for module_id=#{module_id.inspect}" unless entry
|
|
46
55
|
|
|
47
|
-
[entry["id"],
|
|
56
|
+
[entry["id"], NO_CHUNKS, entry["name"]]
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
# Returns true if +name+ is a top-level key in the manifest data.
|
|
@@ -80,6 +89,24 @@ module Ruact
|
|
|
80
89
|
end
|
|
81
90
|
end
|
|
82
91
|
|
|
92
|
+
# Story 18-2 — whether +name+ resolves for SOME controller: the shared key,
|
|
93
|
+
# or a co-located one in any folder (`posts/_like_button`). The ERB
|
|
94
|
+
# preprocessor asks while it compiles a template, before it knows which
|
|
95
|
+
# controller will render it, so a co-located component is never refused
|
|
96
|
+
# for being looked up from a shared partial.
|
|
97
|
+
def component?(name)
|
|
98
|
+
return true if include?(name)
|
|
99
|
+
|
|
100
|
+
suffix = "/_#{pascal_to_snake_case(name)}"
|
|
101
|
+
entries_by_name.keys.any? { |key| key.end_with?(suffix) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Story 18-2 — the "not found" message {#reference_for} raises, for the
|
|
105
|
+
# preprocessor to put the template's file:line in front of.
|
|
106
|
+
def unknown_component_message(name, controller_path: nil)
|
|
107
|
+
build_unknown_component_message(name, controller_path)
|
|
108
|
+
end
|
|
109
|
+
|
|
83
110
|
# Story 13.5 (FR100) — return the optional component +contract+ Hash for
|
|
84
111
|
# +name+, or +nil+ when the component declared none (or is absent from the
|
|
85
112
|
# manifest). Honors the same co-located/shared +resolve_key+ precedence as
|
|
@@ -103,8 +103,10 @@ module Ruact
|
|
|
103
103
|
# @param detail [String] the violation clause
|
|
104
104
|
# @param where [Hash] keys +component+, +file+, +line+, +snippet+
|
|
105
105
|
def self.raise_error(detail, where)
|
|
106
|
-
location = [where[:file], where[:line]].compact.join(":")
|
|
107
|
-
|
|
106
|
+
location = if where[:file] then [where[:file], where[:line]].compact.join(":")
|
|
107
|
+
elsif where[:line] then "line #{where[:line]}"
|
|
108
|
+
else "(unknown location)"
|
|
109
|
+
end
|
|
108
110
|
message = "ruact: <#{where[:component]}> at #{location} #{detail}."
|
|
109
111
|
message += "\n in: #{where[:snippet]}" if where[:snippet] && !where[:snippet].empty?
|
|
110
112
|
raise ComponentContractError, message
|
data/lib/ruact/controller.rb
CHANGED
|
@@ -168,7 +168,9 @@ module Ruact
|
|
|
168
168
|
# its headers on the first row.
|
|
169
169
|
self.status = status if status
|
|
170
170
|
|
|
171
|
-
pipeline = RenderPipeline.new(ruact_manifest,
|
|
171
|
+
pipeline = RenderPipeline.new(ruact_manifest,
|
|
172
|
+
controller_path: controller_path, logger: logger,
|
|
173
|
+
development: Flight::Renderer.development_default)
|
|
172
174
|
streaming = ruact_request? && self.class.ancestors.include?(ActionController::Live)
|
|
173
175
|
|
|
174
176
|
# Allocate a per-render context and expose it to the view via a normal
|
data/lib/ruact/doctor.rb
CHANGED
|
@@ -8,8 +8,8 @@ module Ruact
|
|
|
8
8
|
# Runs a suite of installation health checks and prints ✓/✗ per check.
|
|
9
9
|
# Extracted from the ruact:doctor Rake task for direct testability (FR27).
|
|
10
10
|
class Doctor # rubocop:disable Metrics/ClassLength
|
|
11
|
-
CHECKS = %i[manifest vite controller layout head_assets streaming legacy_constant
|
|
12
|
-
flight_middleware].freeze
|
|
11
|
+
CHECKS = %i[manifest vite flight_client controller layout head_assets streaming legacy_constant
|
|
12
|
+
serialize_only flight_middleware].freeze
|
|
13
13
|
# Built via Array#join so the gem-CI `name-propagation` guard does not
|
|
14
14
|
# match these literals against itself (Story 5.1 review F4 — the doctor
|
|
15
15
|
# file participates in the guard with no exclusion).
|
|
@@ -42,8 +42,8 @@ module Ruact
|
|
|
42
42
|
/\b#{%w[from flight].join('_')}\b/,
|
|
43
43
|
/\b#{%w[parse flight].join('_')}\b/,
|
|
44
44
|
/\b#{%w[decode flight].join('_')}\b/,
|
|
45
|
-
# React Flight reader entry points invoked from Ruby (NOT
|
|
46
|
-
#
|
|
45
|
+
# React Flight reader entry points invoked from Ruby (NOT the browser
|
|
46
|
+
# runtime's own calls, which read the server's own trusted payload)
|
|
47
47
|
/\b#{%w[create From].join}(?:NodeStream|ReadableStream|Fetch)\b/
|
|
48
48
|
].freeze
|
|
49
49
|
DESERIALIZE_SIGNAL_RE = Regexp.union(DESERIALIZE_SIGNALS)
|
|
@@ -163,6 +163,66 @@ module Ruact
|
|
|
163
163
|
"Run npm run dev (or bin/dev) to start the Vite dev server."]
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
+
# Story 18-1 — which copy of React's Flight client the browser runtime
|
|
167
|
+
# uses, decided as the Vite plugin decides it (`resolveFlightClient`): the
|
|
168
|
+
# app's `react-server-dom-webpack` when the app's package.json declares it
|
|
169
|
+
# and Node finds it from the app (a workspace may hoist it to a parent
|
|
170
|
+
# directory), otherwise the one vendored in the gem. The plugin prints the
|
|
171
|
+
# same line when Vite starts. The client is built for one React minor; the
|
|
172
|
+
# app's copy is checked against the app's `react`.
|
|
173
|
+
def check_flight_client
|
|
174
|
+
vendored = [:pass, "Flight client: react-server-dom-webpack #{vendored_flight_client_version} vendored in ruact"]
|
|
175
|
+
return vendored unless app_declares?("react-server-dom-webpack")
|
|
176
|
+
|
|
177
|
+
app_package = node_package_json("react-server-dom-webpack")
|
|
178
|
+
return vendored unless app_package
|
|
179
|
+
|
|
180
|
+
version = JSON.parse(app_package.read)["version"]
|
|
181
|
+
react_package = node_package_json("react")
|
|
182
|
+
react = react_package && JSON.parse(react_package.read)["version"]
|
|
183
|
+
message = "Flight client: react-server-dom-webpack #{version} from the app (replaces the copy in ruact)"
|
|
184
|
+
return [:pass, message] if react.nil? || minor(react) == minor(version)
|
|
185
|
+
|
|
186
|
+
[:warn, "#{message}, but the app's react is #{react}",
|
|
187
|
+
"Install the react-server-dom-webpack matching your react (npm install react-server-dom-webpack@#{react}), " \
|
|
188
|
+
"or uninstall it to use the copy vendored in ruact."]
|
|
189
|
+
rescue JSON::ParserError => e
|
|
190
|
+
[:warn, "Flight client: a package.json under node_modules is not valid JSON (#{e.message})",
|
|
191
|
+
"Reinstall it (npm install), or remove react-server-dom-webpack to use the copy vendored in ruact."]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# As the Vite plugin reads it: a non-empty entry in dependencies or
|
|
195
|
+
# devDependencies; an unreadable package.json declares nothing.
|
|
196
|
+
def app_declares?(name)
|
|
197
|
+
manifest = Rails.root.join("package.json")
|
|
198
|
+
return false unless manifest.exist?
|
|
199
|
+
|
|
200
|
+
json = JSON.parse(manifest.read)
|
|
201
|
+
[json["dependencies"], json["devDependencies"]].any? { |deps| deps.is_a?(Hash) && !deps[name].to_s.empty? }
|
|
202
|
+
rescue JSON::ParserError
|
|
203
|
+
false
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The first `node_modules/<name>/package.json` from Rails.root upward, the
|
|
207
|
+
# way Node resolves a package.
|
|
208
|
+
def node_package_json(name)
|
|
209
|
+
Pathname(Rails.root).expand_path.ascend do |dir|
|
|
210
|
+
candidate = dir.join("node_modules", name, "package.json")
|
|
211
|
+
return candidate if candidate.exist?
|
|
212
|
+
end
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def minor(version)
|
|
217
|
+
version.to_s.split(".").first(2).join(".")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def vendored_flight_client_version
|
|
221
|
+
path = File.join(File.dirname(Ruact.vite_plugin_path), "runtime", "vendor", "react-server-dom-webpack",
|
|
222
|
+
"VERSION.json")
|
|
223
|
+
JSON.parse(File.read(path))["version"]
|
|
224
|
+
end
|
|
225
|
+
|
|
166
226
|
# Story 17.0g (FR116) — reports the ADOPTION MODE instead of demanding one.
|
|
167
227
|
#
|
|
168
228
|
# Whole-app (`ruact:install --app`): ApplicationController includes the
|