ruact 0.0.7 → 0.0.8
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 +24 -0
- data/docs/internal/decisions/server-functions-api.md +55 -0
- data/lib/generators/ruact/install/install_generator.rb +114 -1
- data/lib/generators/ruact/install/templates/AGENTS.md.tt +159 -0
- data/lib/ruact/controller.rb +9 -0
- data/lib/ruact/doctor.rb +67 -9
- data/lib/ruact/erb_preprocessor.rb +113 -0
- data/lib/ruact/errors.rb +16 -0
- data/lib/ruact/manifest_resolver.rb +2 -2
- data/lib/ruact/serializable.rb +98 -6
- data/lib/ruact/server.rb +163 -0
- data/lib/ruact/server_functions/introspection.rb +81 -0
- data/lib/ruact/server_functions.rb +26 -4
- data/lib/ruact/testing/component_query.rb +113 -0
- data/lib/ruact/testing/flight_extractor.rb +221 -0
- data/lib/ruact/testing/flight_structure_diff.rb +267 -0
- data/lib/ruact/testing/flight_wire_parser.rb +138 -0
- data/lib/ruact/testing.rb +90 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/tasks/ruact.rake +55 -2
- data/spec/ruact/doctor_spec.rb +141 -0
- data/spec/ruact/erb_preprocessor_spec.rb +145 -0
- data/spec/ruact/install_generator_spec.rb +285 -0
- data/spec/ruact/manifest_resolver_spec.rb +15 -0
- data/spec/ruact/serializable_spec.rb +126 -0
- data/spec/ruact/server_bucket_request_spec.rb +291 -0
- data/spec/ruact/server_functions/introspection_spec.rb +135 -0
- data/spec/ruact/tasks_json_introspection_spec.rb +141 -0
- data/spec/ruact/testing/have_ruact_component_spec.rb +170 -0
- data/spec/ruact/testing/no_production_load_spec.rb +41 -0
- data/spec/support/flight_wire_parser.rb +12 -126
- data/spec/support/matchers/flight_fixture_matcher.rb +7 -258
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +25 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.js +104 -7
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +173 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/auto-revalidate.test-d.ts +25 -0
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9a45065af0d01daed8548084a9667fb066f9e8c19a685f99b0f101a8e825ca0
|
|
4
|
+
data.tar.gz: 99206ecd7d61a44b4cdfe52bb5b40b046f66e2938f7bf060e603b98ef17971b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d303e5aa55881c70b88a6a7f132450712c8f35adb7b00f0172d3503784dbe82b1373ae7ecc30db05a98832f2c2154134e49bbba43ba403497cdc61b90401b91a
|
|
7
|
+
data.tar.gz: 8b8fcc40cca7167dece277333fdd56562a62800d150af0be85a50033fc6a2ea3dc8aeebf7650dbb220891f73a843a80d2b0becd1f1d7483b4a2afa6af8a1404e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.8] - 2026-07-11
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`ManifestError` message is now in English (was Portuguese)** ([Story 15.0](../_bmad-output/implementation-artifacts/15-0-agent-era-hygiene-pass.md)). When the Vite dev server is unreachable AND no `public/react-client-manifest.json` exists on disk, `Ruact::ManifestResolver` now raises `[ruact] Vite dev server unreachable at <url> and no react-client-manifest.json found at <path> — run `bin/dev`.` — same raise site, same `ManifestError` class, same HTTP→file fallback order, same diagnostic content (dev-server URL + manifest path + the `bin/dev` fix); only the language changed. Error messages are prompts — agents (and humans) regex-match them — so the stable tokens (`Vite dev server`, `react-client-manifest.json`, `bin/dev`) are preserved and now spec-pinned.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`ruact_props` now works directly on an ActiveRecord model — loud check deferred from class-load to first serialize for lazy attributes (Story 13.7)** ([Story 13.7](../_bmad-output/implementation-artifacts/13-7-ruact-props-on-activerecord-models.md)). `include Ruact::Serializable; ruact_props :id, :title` on an `ActiveRecord` model used to raise `ArgumentError: method 'title' is not defined` at **class-load**, because ActiveRecord defines its attribute reader methods **lazily** (on first instance access) — so the eager `method_defined?` check fired before the reader existed, even for a real column. The check is now **hybrid**: a **PORO** is still validated eagerly at class-load (byte-identical — a typo still raises immediately), while for a **lazy-attribute (ActiveRecord) class** the not-yet-defined names are recorded and their loud check is **deferred to the first `ruact_serialize`** (via `respond_to?` on the instance, where the DB is up and the reader exists). **The loud-omission guarantee is preserved** (Epic 13.1/13.2 spine): a bogus/typo'd prop still raises the same clean `ArgumentError` — only the *timing* moved for AR (first render of that model instead of boot). No boot-time DB dependency is introduced (`attribute_names`/`column_names` would need a live connection at class-load — rejected), the gem stays **single-dep `nokogiri`** (the ActiveRecord discriminator `defined?(ActiveRecord::Base) && self < ActiveRecord::Base` references the constant only when the host already defined it), and there is **no new config key, no serializer-pipeline change** (an AR model that includes the module already dispatched to `serialize_serializable` → `ruact_serialize`), and **`strict_serialization` is unaffected** (an AR `Serializable` model serializes identically under strict true/false, as a PORO does). This reverses the doc-only guidance shipped in the AGENTS.md/`llms.txt` serialization-contract fix (gem PR #48): `ruact_props` on an AR model is now a first-class pattern again; the manual row hash (`{ id: p.id, title: p.title }`) remains the equivalent for a no-model / partial shape.
|
|
19
|
+
|
|
20
|
+
- **Auto-revalidate opt-in: one `await` folds a mutation and its in-place refresh into a single call (FR110)** ([Story 15.6](../_bmad-output/implementation-artifacts/15-6-auto-revalidate-opt-in.md)). A mutation whose result is shown *elsewhere* on the page has always needed a manual two-step — `await createPost(fd); await revalidate()` — and the second call is easy to forget, leaving a stale page that reads like a silent bug. There are now two runtime-only, opt-in ways to fold the second call in: an app-wide default via `configureRuactRuntime({ autoRevalidate: true })` (mirrors the existing `defaultHeaders` config), and a per-call `withRefresh(accessor)` wrapper (`await withRefresh(createPost)(fd)`) that forces the refresh for one call and **wins over** the global default. When opted in, a **successful, non-redirecting** mutation `await`s an in-place Flight refresh of the current path (the existing `revalidate()` machinery) **before** the promise resolves, then resolves with the action's JSON result. This is **pure client-side composition** of the two requests that already exist (the POST-JSON mutation + the GET-Flight refresh): **no new wire shape and no inbound Flight deserialization — the serialize-only invariant is untouched.** A `$redirect` response **wins**: the redirect is followed and the refresh is skipped (the destination already re-renders; refreshing the old path would be wrong). A **failed** mutation throws before any refresh runs, so no refresh happens. With no router installed, the descriptive `revalidate()` "wire `setupRouter()`" error surfaces (it is not swallowed) — so a mutation that succeeds but whose refresh rejects **rejects the returned promise**, consistent with `revalidate()`'s loud-by-default stance. Opt-out (default) behavior is **byte-identical** to before, and the generated `.ruact/server-functions.ts` is unchanged — `withRefresh` is imported directly from `ruact/server-functions-runtime` (like `configureRuactRuntime`), so the codegen output and the Ruby↔JS byte-parity fixtures are untouched.
|
|
21
|
+
|
|
22
|
+
- **Dev-only `[ruact]` log line naming the negotiated response shape per request (FR109)** ([Story 15.5](../_bmad-output/implementation-artifacts/15-5-response-shape-legibility-docs-and-dev-log.md)). The single most confusing thing about a `Ruact::Server` action is that the same action, at the same URL, answers different body shapes depending only on the request's `Accept` header + verb — never on the Ruby. In **development only**, `Ruact::Server` now emits exactly one `[ruact]` line per ruact-negotiated request naming the chosen bucket and the deciding signal — e.g. `[ruact] PostsController#create — Accept: application/json + POST → function-call JSON` — so the dual-bucket negotiation is observable at runtime without a debugger. It covers the function-call-JSON bucket (including its `204` and `$redirect` sub-shapes) and, for a page render on a controller that also includes `Ruact::Controller`, the Flight-page and HTML-shell shapes. Log-only: response body, status, and headers are byte-identical in every environment. It is SILENT outside development and on a plain Rails action on the same controller (a response ruact did not negotiate). Mirrors the F6 warning: a private, dev-gated `after_action` beside it, no new config key. The log vocabulary matches the new canonical caller→shape table in the server-actions docs. Coverage is `Ruact::Server`-only for now — queries (separate `GET /q` dispatch) and page-only controllers are documented in the table but not yet logged (deferred).
|
|
23
|
+
|
|
24
|
+
- **Public server-side render-assertion helpers: `have_ruact_component` (FR108)** ([Story 15.4](../_bmad-output/implementation-artifacts/15-4-server-side-render-assertion-helpers.md)). A host request/controller spec can now prove a page rendered a given component with given props against the decoded Flight tree — no browser, no brittle string spelunking on the wire bytes. Load it explicitly (`require "ruact/testing"` from your `spec_helper`/`rails_helper` — it is NOT auto-loaded by `require "ruact"`, so a production boot carries no RSpec dependency), then assert `expect(response).to have_ruact_component("PostList")` or `...have_ruact_component("PostList").with_props(a_hash_including("posts" => ...))`. It accepts an ActionDispatch/Rack response (its `.body` is read) or a raw String, in either page shape — a raw `text/x-component` body or the HTML shell embedding `__FLIGHT_DATA`. The component name resolves against the import row the wire actually carries (its export name or module basename); props are asserted in their serialized wire form (string keys, serialized values) via `===`/`hash_including`. Passing a `Ruact::Server` function-call/query response (plain JSON) raises a clear `Ruact::Testing::NotAFlightResponseError` pointing you at `JSON.parse(response.body)` instead. This is a STABLE public matcher that **wraps, not forks**, the internal Story-7.5 structural parser/diff — those were promoted onto the shipped load path as `Ruact::Testing::FlightWireParser` / `Ruact::Testing::FlightStructureDiff`, and the gem's own `Ruact::Spec` matchers now delegate to that single implementation (one source of truth, no drift). New docs "Testing" page covers all three response shapes (pages, function calls, queries).
|
|
25
|
+
|
|
26
|
+
- **Machine-readable introspection: `ruact:doctor -- --json` and `ruact:routes -- --json` (FR107)** ([Story 15.3](../_bmad-output/implementation-artifacts/15-3-machine-readable-introspection-doctor-routes-json.md)). A coding agent (or CI gate) can now read the two ground-truth surfaces as JSON instead of parsing terminal prose. `ruact:doctor -- --json` emits one document — `schema_version`, an overall `status`, and every health check as `name`/`status`/`message`/`remediation` (a separate, nullable machine-readable fix) — reusing the exact same check results the human path prints (no double-run) and preserving the exit code (0 when all checks pass or warn, 1 on any failure). The new `ruact:routes` task emits the accessor/route table — each entry's `accessor` name, `kind` (action or query), `verb`, `path`, `segments`, and declared `params` (queries from their keyword arguments, actions from their required path segments) — derived from the very same `RouteSource` + `QuerySource` collectors codegen consumes (a shared `Ruact::ServerFunctions.introspect` combine point), so the introspection can never drift from what codegen emits; it writes no bridge or TS file (a CI gate can run it without mutating the tree). Bare `ruact:routes` prints a compact human table. The default human `ruact:doctor` output is unchanged. Both JSON documents are marked EXPERIMENTAL and carry `schema_version: 0` — the shape may change without a major bump while the agent-facing surface is iterated, so parsers should gate on it. The flag is passed after a `--` separator (`bin/rails ruact:doctor -- --json`) so neither Rails nor Rake treats it as an option. The emitted `AGENTS.md`/`llms.txt` "Verify your work" section now references both commands, flagged EXPERIMENTAL.
|
|
27
|
+
|
|
28
|
+
- **Children inside a component tag now fail loudly at preprocess time instead of degrading silently (FR106)** ([Story 15.2](../_bmad-output/implementation-artifacts/15-2-loud-children-error-in-erb-preprocessor.md)). ruact PascalCase component tags are self-closing only — a component receives a props Hash, never a children element tree — but the #1 predictable JSX-habit mistake, `<Card>Hello</Card>`, used to **degrade silently**: the opening tag became a `__ruact_component__("Card", {})` call with no children while the inner content leaked into the surrounding HTML and the `</Card>` passed through as literal garbage. The ERB preprocessor now raises a dedicated `Ruact::ChildrenNotSupportedError` (a `Ruact::PreprocessorError`, so it flows through the same dev error overlay) naming the component, the template `file:line`, and the exact fix — pass content as a prop, e.g. a self-closing `<Card content={...} />`. Detection keys on a **matching PascalCase closing tag** (`</Card>`), NOT on "opening tag lacks `/>`", so a bare non-self-closing opening with no closing tag (`<Dialog open={true}>`) stays a valid call. It runs **after** Suspense normalization (Step 1), so `<Suspense>...</Suspense>` — the one legitimate paired PascalCase tag, already lowercased to `<ruact-suspense>` — never trips. Output is byte-identical for every valid pattern. The shipped `AGENTS.md`/`llms.txt` trap #1 wording is updated from "fails silently" to "fails loudly" now that the artifact exists.
|
|
29
|
+
|
|
30
|
+
- **Dev-only `[ruact]` warning when an action registers `ruact_errors` and then renders explicitly on a function-call request (F6)** ([Story 15.0](../_bmad-output/implementation-artifacts/15-0-agent-era-hygiene-pass.md)). The FR98 `errors` injection rides the implicit `default_render`, so an explicit `render json: {...}` on the failed-save branch of a Bucket-2 (function-call) request silently drops the registered validation errors from the JSON body — a legibility trap for agents following the docs literally. In **development only**, `Ruact::Server` now logs exactly one `[ruact]` warning naming the `controller#action`, explaining the opt-out, and pointing at the fix (fall through, bind `errors={ruact_errors}` on a page render, or `redirect_to`). It never fires on the documented-correct patterns: a Bucket-1 `ruact_errors(record); render :new` page render, a `redirect_to` on either bucket, the fall-through itself, or an action that never touches the collector. Log-only — response bodies and statuses are byte-identical in every environment.
|
|
31
|
+
|
|
32
|
+
- **`rails generate ruact:install` now emits an `AGENTS.md` teaching coding agents the ruact conventions, traps, and verification commands (FR105)** ([Story 15.1](../_bmad-output/implementation-artifacts/15-1-agents-md-install-emission-and-llms-txt.md)). ruact is out-of-distribution for every LLM, so a fresh app now ships the agent context in-repo: the mental model (Rails routes as the single source of truth; ERB as the server component; PascalCase self-closing tags), the verb rule, queries/`useQuery`, the codegen ground truth (`app/javascript/.ruact/server-functions.ts` + `bin/rails ruact:server_functions:generate`), the five traps (children, Ruby-in-braces, Accept-header dual shape, `ruact_errors` fall-through, name derivation), the `ruact_props`/`strict_serialization` allowlist, the SGID helpers, and the verification loop (`bin/rails ruact:doctor`). The emitted content is delimited by `<!-- ruact:begin -->`/`<!-- ruact:end -->` markers and the new `create_agents_md` action is **idempotent and append-aware**: no file → create; a user-authored `AGENTS.md` without markers → the marked section is appended with every pre-existing byte preserved; markers present → skip (re-running install is zero-diff); `--force` refreshes **only** the between-marker content, never user bytes outside it. The same compact reference is served by the docs site at `https://ruact.dev/llms.txt` (kept in sync editorially). New `templates/AGENTS.md.tt` + `:story_15_1` specs (including a ≤160-line template-budget tripwire).
|
|
33
|
+
|
|
10
34
|
## [0.0.7] - 2026-06-30
|
|
11
35
|
|
|
12
36
|
### Fixed
|
|
@@ -2179,3 +2179,58 @@ TS-AST `Props` parse; a Ruby DSL / JSON sidecar; a new slot runtime; moving
|
|
|
2179
2179
|
validation to render time. The **Epic 10 scaffold** consuming contracts and the
|
|
2180
2180
|
**playground build-fails CI proof** are **Story 13.6** (`epics-phase-2.md`).
|
|
2181
2181
|
ADR addendum (2026-06-26).
|
|
2182
|
+
|
|
2183
|
+
### 2026-07-11 — Story 13.7 — `ruact_props` works on ActiveRecord models (deferred loud check for lazy attrs)
|
|
2184
|
+
|
|
2185
|
+
Story 13.7 reopens **option (A)** that `deferred-work.md` #6 parked (the fix-B
|
|
2186
|
+
doc-only route) and makes `include Ruact::Serializable; ruact_props :id, :title`
|
|
2187
|
+
boot **directly on an ActiveRecord model**. It also closes the FR100 producer-side
|
|
2188
|
+
note (*"hoje `ruact_props` valida só o lado produtor, em class-load"*) by making the
|
|
2189
|
+
producer-side check itself AR-aware.
|
|
2190
|
+
|
|
2191
|
+
**The bug.** ActiveRecord defines its attribute reader methods **lazily** (via
|
|
2192
|
+
`define_attribute_methods`, triggered on first instance access / `method_missing`),
|
|
2193
|
+
so at the moment the `ruact_props :title` macro runs, `method_defined?(:title)` is
|
|
2194
|
+
`false` **even for a real column**. The eager class-load check
|
|
2195
|
+
(`serializable.rb`) raised there — an AR model that included the module crashed at
|
|
2196
|
+
boot. (`attribute_names`/`column_names` would tell the truth, but they require a
|
|
2197
|
+
**live DB connection** — with none they raise `ActiveRecord::ConnectionNotDefined`
|
|
2198
|
+
— so any boot-time check based on them adds a class-load DB dependency, a Rails
|
|
2199
|
+
anti-pattern. Rejected.)
|
|
2200
|
+
|
|
2201
|
+
**Decision — HYBRID timing (approach iv, locked by Luiz).** The loud check stays,
|
|
2202
|
+
but **when** it fires now depends on the class:
|
|
2203
|
+
|
|
2204
|
+
- **PORO** — checked **eagerly at class-load** exactly as before. The non-lazy code
|
|
2205
|
+
path is **byte-identical**; a PORO typo still raises `ArgumentError` immediately
|
|
2206
|
+
(the existing PORO spec passes unedited — the C3 regression pin).
|
|
2207
|
+
- **ActiveRecord (lazy-attribute) class** — a name whose reader is not-yet-defined
|
|
2208
|
+
is **recorded** at declaration and its loud check is **deferred to the first
|
|
2209
|
+
`ruact_serialize`**, validated via `instance.respond_to?(attr)` (AR's
|
|
2210
|
+
`respond_to?` returns `true` for a real column, `false` for a bogus name — a
|
|
2211
|
+
genuine typo still raises the **same clean `ArgumentError`**, just at first render
|
|
2212
|
+
of that model instead of at boot). The result is memoized (`respond_to?` runs once
|
|
2213
|
+
per class, not per serialize).
|
|
2214
|
+
|
|
2215
|
+
**Why this satisfies all four constraints.** C1 loud-omission preserved (PORO at
|
|
2216
|
+
boot, AR at first use — still loud, still `ArgumentError`); C2 **no DB dependency at
|
|
2217
|
+
boot** (`respond_to?` runs at first-serialize, when the DB is up); C3 PORO
|
|
2218
|
+
byte-identical; C4 `strict_serialization` untouched (an AR `Serializable` model
|
|
2219
|
+
already dispatches through `serialize_serializable` → `ruact_serialize`, verified
|
|
2220
|
+
under strict true/false).
|
|
2221
|
+
|
|
2222
|
+
**No new dependency / no new config.** The lazy-attribute discriminator is
|
|
2223
|
+
`defined?(ActiveRecord::Base) && self < ActiveRecord::Base` — it references the
|
|
2224
|
+
constant **only when the host already defined it**, so the gem stays single-dep
|
|
2225
|
+
`nokogiri`. **No eager escape hatch** was added (Luiz chose simplicity — no config
|
|
2226
|
+
or macro to force an AR boot check); the natural first-serialize timing is the
|
|
2227
|
+
contract. No serializer-pipeline change (dispatch already routed AR → `ruact_serialize`).
|
|
2228
|
+
|
|
2229
|
+
**Contract note (the one thing that moved).** For an AR model, a typo'd/omitted prop
|
|
2230
|
+
is now caught at the **first render of that model**, not at boot. This is the
|
|
2231
|
+
deliberate, documented cost of not adding a boot DB dependency; loudness itself is
|
|
2232
|
+
unchanged. The AGENTS.md/`llms.txt`/website reversal (`ruact_props` on AR is a
|
|
2233
|
+
first-class pattern again, manual row-hash kept as the no-model equivalent) ships in
|
|
2234
|
+
the same story, resolving `deferred-work.md` #7.
|
|
2235
|
+
|
|
2236
|
+
ADR addendum (2026-07-11).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "erb"
|
|
3
4
|
require "pathname"
|
|
4
5
|
require "rails/generators"
|
|
5
6
|
require "ruact"
|
|
@@ -19,7 +20,10 @@ module Ruact
|
|
|
19
20
|
# 7. Creates Procfile.dev + bin/dev (foreman) so the single `bin/dev`
|
|
20
21
|
# command boots BOTH Rails and the Vite dev server (Story 14.6, Epic 14
|
|
21
22
|
# DoD — a ruact app needs both processes).
|
|
22
|
-
# 8.
|
|
23
|
+
# 8. Emits AGENTS.md (marker-delimited, append-aware, idempotent) so coding
|
|
24
|
+
# agents working in the app have ruact's conventions, traps, and
|
|
25
|
+
# verification commands in context by default (Story 15.1, FR105).
|
|
26
|
+
# 9. Runs `npm install` so JavaScript dependencies are ready (FR101);
|
|
23
27
|
# skippable via --skip-npm.
|
|
24
28
|
#
|
|
25
29
|
# Story 14.2 (FR104) — the generator no longer writes a bootstrap entry into
|
|
@@ -205,6 +209,40 @@ module Ruact
|
|
|
205
209
|
say ""
|
|
206
210
|
end
|
|
207
211
|
|
|
212
|
+
# Story 15.1 (FR105) — emit AGENTS.md so coding agents working in the app
|
|
213
|
+
# have ruact's conventions, traps, and verification commands in context
|
|
214
|
+
# by default. The ruact content is delimited by explicit markers
|
|
215
|
+
# (`<!-- ruact:begin -->` / `<!-- ruact:end -->`) so the action can be
|
|
216
|
+
# append-aware and idempotent:
|
|
217
|
+
#
|
|
218
|
+
# no file → create it (the template IS the section)
|
|
219
|
+
# file without markers → APPEND the marked section, every
|
|
220
|
+
# pre-existing user byte preserved
|
|
221
|
+
# markers present → skip (re-running install is zero-diff)
|
|
222
|
+
# markers present + --force → refresh ONLY the between-marker content —
|
|
223
|
+
# deliberately narrower than the vite.config
|
|
224
|
+
# full-overwrite posture, because a user's
|
|
225
|
+
# AGENTS.md may carry their own project
|
|
226
|
+
# instructions above/below ruact's section.
|
|
227
|
+
#
|
|
228
|
+
# Later stories (15.2 loud children error, 15.3 --json introspection,
|
|
229
|
+
# 15.4 test helpers) evolve the template; `--force` after a gem upgrade
|
|
230
|
+
# is the designed refresh path.
|
|
231
|
+
def create_agents_md
|
|
232
|
+
destination = Pathname(destination_root).join("AGENTS.md")
|
|
233
|
+
|
|
234
|
+
return template("AGENTS.md.tt", "AGENTS.md") unless destination.exist?
|
|
235
|
+
|
|
236
|
+
content = destination.read
|
|
237
|
+
if agents_md_markers_well_formed?(content)
|
|
238
|
+
refresh_or_skip_agents_md_section
|
|
239
|
+
elsif agents_md_markers_broken?(content)
|
|
240
|
+
warn_agents_md_broken_markers
|
|
241
|
+
else
|
|
242
|
+
append_agents_md_section(content)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
208
246
|
# Story 14.1 (FR101) — install JavaScript dependencies so a fresh app is
|
|
209
247
|
# runnable in one command. Runs LAST among the file-producing actions
|
|
210
248
|
# (after every file is written) so a failure here leaves the generated
|
|
@@ -261,6 +299,81 @@ module Ruact
|
|
|
261
299
|
|
|
262
300
|
private
|
|
263
301
|
|
|
302
|
+
# Story 15.1 — the exact marker tokens delimiting the ruact-managed
|
|
303
|
+
# section of AGENTS.md. For a prose file the only safe idempotency key is
|
|
304
|
+
# an explicit marker pair (`append_gitignore_entries`-style exact-line
|
|
305
|
+
# dedup would misfire on edited prose).
|
|
306
|
+
AGENTS_MD_BEGIN_MARKER = "<!-- ruact:begin -->"
|
|
307
|
+
AGENTS_MD_END_MARKER = "<!-- ruact:end -->"
|
|
308
|
+
|
|
309
|
+
# The full marked section, non-greedy so nothing outside the marker pair
|
|
310
|
+
# is ever captured (--force replaces exactly this range).
|
|
311
|
+
AGENTS_MD_SECTION_RE = /#{Regexp.escape(AGENTS_MD_BEGIN_MARKER)}.*?#{Regexp.escape(AGENTS_MD_END_MARKER)}/m
|
|
312
|
+
private_constant :AGENTS_MD_BEGIN_MARKER, :AGENTS_MD_END_MARKER, :AGENTS_MD_SECTION_RE
|
|
313
|
+
|
|
314
|
+
# The rendered ruact section — the full AGENTS.md.tt body, begin marker
|
|
315
|
+
# first line through end marker last line (the template IS the section).
|
|
316
|
+
# Rendered through ERB in the generator's context so the template may
|
|
317
|
+
# interpolate like any other Thor template (today it is fully static).
|
|
318
|
+
def agents_md_section
|
|
319
|
+
@agents_md_section ||= ERB.new(
|
|
320
|
+
File.read(File.expand_path("templates/AGENTS.md.tt", __dir__)),
|
|
321
|
+
trim_mode: "-"
|
|
322
|
+
).result(binding)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Story 15.1 — append the marked ruact section to a user-authored
|
|
326
|
+
# AGENTS.md, separated by exactly one blank line, preserving every
|
|
327
|
+
# pre-existing byte.
|
|
328
|
+
def append_agents_md_section(existing_content)
|
|
329
|
+
separator = existing_content.end_with?("\n") ? "\n" : "\n\n"
|
|
330
|
+
append_to_file "AGENTS.md", "#{separator}#{agents_md_section}"
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Story 15.1 — a WELL-FORMED marker pair is present (the caller matched
|
|
334
|
+
# {AGENTS_MD_SECTION_RE}): skip (idempotent re-run) unless --force, which
|
|
335
|
+
# replaces ONLY the content between (and including) the marker pair.
|
|
336
|
+
# Bytes outside the markers are never touched.
|
|
337
|
+
def refresh_or_skip_agents_md_section
|
|
338
|
+
unless options[:force]
|
|
339
|
+
say_status "skip", "AGENTS.md already carries the ruact section " \
|
|
340
|
+
"(re-run with --force to refresh it)", :yellow
|
|
341
|
+
return
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Block form so `\`/`\1` sequences in the section are never treated as
|
|
345
|
+
# backreferences by String#gsub.
|
|
346
|
+
gsub_file("AGENTS.md", AGENTS_MD_SECTION_RE) { agents_md_section.chomp }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Story 15.1 (Codex R1/R2) — the ONLY marker state the action manages:
|
|
350
|
+
# exactly one begin marker, exactly one end marker, begin before end.
|
|
351
|
+
# Anything else — a lone marker, end-before-begin, a stray extra marker
|
|
352
|
+
# alongside a valid pair, multiple pairs — makes the section boundary
|
|
353
|
+
# ambiguous, so it is handled as broken (warn + no-op) rather than
|
|
354
|
+
# guessed at.
|
|
355
|
+
def agents_md_markers_well_formed?(content)
|
|
356
|
+
content.scan(AGENTS_MD_BEGIN_MARKER).length == 1 &&
|
|
357
|
+
content.scan(AGENTS_MD_END_MARKER).length == 1 &&
|
|
358
|
+
AGENTS_MD_SECTION_RE.match?(content)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Story 15.1 (Codex R1) — SOME marker text is present but not in the one
|
|
362
|
+
# well-formed shape above. The boundary is ambiguous in BOTH directions —
|
|
363
|
+
# appending would duplicate content next to stray markers, and replacing
|
|
364
|
+
# would have to guess the range — so the only byte-safe move is to warn
|
|
365
|
+
# and leave every byte alone.
|
|
366
|
+
def agents_md_markers_broken?(content)
|
|
367
|
+
content.include?(AGENTS_MD_BEGIN_MARKER) || content.include?(AGENTS_MD_END_MARKER)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def warn_agents_md_broken_markers
|
|
371
|
+
say_status "warn", "AGENTS.md has an incomplete ruact marker pair " \
|
|
372
|
+
"(#{AGENTS_MD_BEGIN_MARKER} … #{AGENTS_MD_END_MARKER}) — " \
|
|
373
|
+
"leaving the file untouched; restore both markers (or delete " \
|
|
374
|
+
"the partial ruact section) and re-run", :yellow
|
|
375
|
+
end
|
|
376
|
+
|
|
264
377
|
# Story 14.6 (live clean-room fix) — ruact OWNS `bin/dev`. The foreman
|
|
265
378
|
# launcher is load-bearing: it boots BOTH Rails AND the Vite dev server,
|
|
266
379
|
# and Vite is what writes `public/react-client-manifest.json`. Rails'
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<!-- ruact:begin -->
|
|
2
|
+
<!-- Managed by `rails generate ruact:install`. Re-run it with --force after a
|
|
3
|
+
gem upgrade to refresh ONLY this marked section; everything you write
|
|
4
|
+
outside the markers is preserved. Kept in sync manually with the online
|
|
5
|
+
copy at https://ruact.dev/llms.txt -->
|
|
6
|
+
|
|
7
|
+
# ruact — conventions for coding agents
|
|
8
|
+
|
|
9
|
+
ruact renders React Server Components from Rails. ERB templates ARE the server
|
|
10
|
+
components (serialized to the React Flight wire format), Rails routes are the
|
|
11
|
+
single source of truth, and there is no Node server — Vite only bundles the
|
|
12
|
+
client components. ruact is NOT Inertia, react-rails, or Next.js: do not
|
|
13
|
+
transplant their patterns. When unsure, read the generated code (see "Ground
|
|
14
|
+
truth" below) instead of guessing.
|
|
15
|
+
|
|
16
|
+
## Mental model
|
|
17
|
+
|
|
18
|
+
- A page is a normal Rails controller action rendering a normal `.html.erb`.
|
|
19
|
+
- Interactive components live in `app/javascript/components/` as `"use client"`
|
|
20
|
+
files, mounted from ERB with a PascalCase self-closing tag:
|
|
21
|
+
`<LikeButton postId={@post.id} />`.
|
|
22
|
+
- Mutations are ordinary non-GET controller actions on a controller that does
|
|
23
|
+
`include Ruact::Server`. Reads are `Ruact::Query` classes in `app/queries/`.
|
|
24
|
+
- ruact generates a typed TypeScript module from the route table; React imports
|
|
25
|
+
server functions from `@/.ruact/server-functions`.
|
|
26
|
+
|
|
27
|
+
## Server functions — the verb rule
|
|
28
|
+
|
|
29
|
+
`include Ruact::Server` makes a controller's routed non-GET actions
|
|
30
|
+
(`POST`/`PATCH`/`PUT`/`DELETE`) callable from React. GET page actions are
|
|
31
|
+
untouched. There is no per-action DSL and no synthetic endpoint: the action IS
|
|
32
|
+
the function, reached at its real route, visible in `bin/rails routes`. The JS
|
|
33
|
+
runtime forwards the CSRF token automatically; keep `include Ruact::Server`
|
|
34
|
+
AFTER `protect_from_forgery` in the ancestor chain.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
class PostsController < ApplicationController
|
|
38
|
+
include Ruact::Server
|
|
39
|
+
|
|
40
|
+
def create
|
|
41
|
+
@post = Post.create!(post_params) # ivars become the JSON result
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { createPost } from "@/.ruact/server-functions";
|
|
48
|
+
const { post } = await createPost({ post: { title: "Hi" } });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Reads — queries and useQuery
|
|
52
|
+
|
|
53
|
+
Reads are public methods on `Ruact::Query` subclasses in `app/queries/`,
|
|
54
|
+
mounted in `config/routes.rb` with `ruact_queries CatalogQuery` (one named GET
|
|
55
|
+
route per public method, default prefix `/q`). Call them with `useQuery`:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { categories, useQuery } from "@/.ruact/server-functions";
|
|
59
|
+
const { data, loading, error } = useQuery(categories);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Query params come from the method's keyword arguments and accept ONLY
|
|
63
|
+
`string | number | boolean | null` (arrays/objects are rejected). Queries run
|
|
64
|
+
the host controller's callback chain (authentication etc.) before the query
|
|
65
|
+
object is instantiated.
|
|
66
|
+
|
|
67
|
+
## Ground truth — read the generated file
|
|
68
|
+
|
|
69
|
+
`app/javascript/.ruact/server-functions.ts` is regenerated from the route
|
|
70
|
+
table (it is gitignored). It is the authoritative list of every accessor and
|
|
71
|
+
its typed params — READ THAT FILE instead of simulating the name generation.
|
|
72
|
+
Regenerate it after changing routes or queries:
|
|
73
|
+
|
|
74
|
+
bin/rails ruact:server_functions:generate
|
|
75
|
+
|
|
76
|
+
## Five traps
|
|
77
|
+
|
|
78
|
+
1. **Component tags take no children.** Client component tags are self-closing
|
|
79
|
+
ONLY: `<Card content={@body} />`, never `<Card>...</Card>`. Children (a
|
|
80
|
+
matching closing tag) fail LOUDLY with a `PreprocessorError` at preprocess
|
|
81
|
+
time — pass content as a prop instead. Sole exception: the built-in
|
|
82
|
+
`<Suspense fallback="...">...</Suspense>` pair.
|
|
83
|
+
2. **`{}` props are Ruby, not JavaScript.** `<Badge label={@post.title} />`
|
|
84
|
+
evaluates `@post.title` as a Ruby expression in the ERB. No JS ternaries,
|
|
85
|
+
no `{...spread}`, no JSX children. Plain unbraced string props are not
|
|
86
|
+
supported either — always use braces.
|
|
87
|
+
3. **One action, two response shapes.** A `Ruact::Server` non-GET action
|
|
88
|
+
answers JSON — its instance variables, or `204`, or `{"$redirect": path}` —
|
|
89
|
+
when the request's `Accept` header is exactly `application/json`, which is
|
|
90
|
+
what every generated-accessor call sends (including
|
|
91
|
+
`<form action={createPost}>`). Any other request shape renders normally
|
|
92
|
+
(a Flight stream for client-side navigation, an HTML page otherwise). You
|
|
93
|
+
cannot infer the response shape from the controller body alone — the
|
|
94
|
+
caller picks it.
|
|
95
|
+
4. **`ruact_errors` requires fall-through.** On the `if @post.save ... else`
|
|
96
|
+
path, call `ruact_errors(@post)` and let the action END there — ruact's
|
|
97
|
+
implicit render injects `errors: { attribute: [messages] }` into the JSON.
|
|
98
|
+
An explicit `render` on that branch opts out of the injection. In the
|
|
99
|
+
redirect-back flow, `ruact_errors(@post)` then `redirect_to` carries the
|
|
100
|
+
errors through flash to the next render.
|
|
101
|
+
5. **Accessor names are derived, not declared.** `posts#create` → `createPost`,
|
|
102
|
+
`posts#publish_all` → `publishAllPosts`; query methods camelCase the same
|
|
103
|
+
way (`search_users` → `searchUsers`). Collisions fail loudly at boot;
|
|
104
|
+
break an action tie with `ruact_function_name :action, as: "otherName"` —
|
|
105
|
+
a query tie by renaming the query method. Never guess a name — read
|
|
106
|
+
`app/javascript/.ruact/server-functions.ts`.
|
|
107
|
+
|
|
108
|
+
## Serialization is an allowlist
|
|
109
|
+
|
|
110
|
+
Only the fields you name cross to the client. `include Ruact::Serializable` +
|
|
111
|
+
`ruact_props :id, :title` is that allowlist — on a PORO OR an ActiveRecord model
|
|
112
|
+
(lazy AR readers checked on first serialize, not class-load; a typo still raises):
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
class Post < ApplicationRecord
|
|
116
|
+
include Ruact::Serializable
|
|
117
|
+
ruact_props :id, :title # other columns never cross
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The manual row hash `{ id: p.id, title: p.title }` is the equivalent for a
|
|
122
|
+
no-model shape. An object with no allowlist warns + falls back to `as_json` in
|
|
123
|
+
dev; production (`strict_serialization`) raises, as does `as_json` returning `self`.
|
|
124
|
+
|
|
125
|
+
## Record references — signed and explicit
|
|
126
|
+
|
|
127
|
+
Do not put raw record ids in props and trust them later. Mint a scoped signed
|
|
128
|
+
token and resolve it back:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
token = Ruact.signed_global_id(@post, for: :editing, expires_in: 1.hour)
|
|
132
|
+
post = Ruact.locate_signed(params[:token], for: :editing) # tampered → 400
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The purpose (`for:`) is always required; the expiry must be explicit — a
|
|
136
|
+
duration, or a deliberate `expires_in: nil` for a non-expiring token.
|
|
137
|
+
|
|
138
|
+
## Verify your work
|
|
139
|
+
|
|
140
|
+
- `bin/rails ruact:doctor` — checks install/config health; exits 1 on failure.
|
|
141
|
+
Append `-- --json` for the same checks as a machine-readable report.
|
|
142
|
+
- `bin/rails ruact:routes -- --json` — the accessor/route table (each accessor's
|
|
143
|
+
name, kind, verb, path, params) as JSON, from the same route table codegen
|
|
144
|
+
reads; bare `ruact:routes` prints a compact human table.
|
|
145
|
+
- Both `--json` outputs are EXPERIMENTAL — the shape may change; gate on their
|
|
146
|
+
`schema_version` field (currently `0`), do not treat it as a stable contract.
|
|
147
|
+
- `bin/rails ruact:server_functions:generate` — regenerates the TS module;
|
|
148
|
+
exits 1 on a naming collision or an invalid name.
|
|
149
|
+
- `bin/dev` — boots Rails AND Vite (both are required: Vite serves the client
|
|
150
|
+
components and writes the client manifest).
|
|
151
|
+
- If your app has TypeScript tooling configured, `npx tsc --noEmit`
|
|
152
|
+
type-checks call sites against the generated accessor types (a fresh
|
|
153
|
+
install does not ship a tsconfig).
|
|
154
|
+
- In request specs, `require "ruact/testing"` then assert a page rendered a
|
|
155
|
+
component — `expect(response).to have_ruact_component("PostList").with_props(including("posts"))`
|
|
156
|
+
— no browser. Function-call/query responses are JSON: `JSON.parse(response.body)`.
|
|
157
|
+
|
|
158
|
+
Docs: https://ruact.dev — compact agent reference at https://ruact.dev/llms.txt
|
|
159
|
+
<!-- ruact:end -->
|
data/lib/ruact/controller.rb
CHANGED
|
@@ -157,6 +157,12 @@ module Ruact
|
|
|
157
157
|
# (matching the legacy `#from_html` ordering) before any streaming
|
|
158
158
|
# response headers are mutated.
|
|
159
159
|
def emit_ruact_response(pipeline, html, render_context, streaming:)
|
|
160
|
+
# Story 15.5 (FR109) — record which page shape ruact rendered so the
|
|
161
|
+
# `Ruact::Server` dev log (`__ruact_log_response_shape!`) can name it and,
|
|
162
|
+
# crucially, stay SILENT on a plain Rails render (this flag is never set
|
|
163
|
+
# there). `@__`-prefixed → never leaks into `view_assigns`.
|
|
164
|
+
@__ruact_negotiated_page = ruact_request? ? :flight : :html_shell
|
|
165
|
+
|
|
160
166
|
if ruact_request? && streaming
|
|
161
167
|
enumerator = pipeline.render({ html: html, render_context: render_context }, mode: :stream)
|
|
162
168
|
response.headers["Content-Type"] = "text/x-component; charset=utf-8"
|
|
@@ -209,6 +215,9 @@ module Ruact
|
|
|
209
215
|
# Flight redirect and re-render as an `errors` prop (no-op when untouched).
|
|
210
216
|
__ruact_stash_errors_in_flash
|
|
211
217
|
|
|
218
|
+
# Story 15.5 (FR109) — mark the Flight-redirect sub-shape for the dev log.
|
|
219
|
+
@__ruact_negotiated_page = :flight_redirect
|
|
220
|
+
|
|
212
221
|
render plain: "0:#{JSON.generate({ 'redirectUrl' => redirect_url, 'redirectType' => 'push' })}\n",
|
|
213
222
|
content_type: "text/x-component"
|
|
214
223
|
end
|
data/lib/ruact/doctor.rb
CHANGED
|
@@ -6,7 +6,7 @@ require "pathname"
|
|
|
6
6
|
module Ruact
|
|
7
7
|
# Runs a suite of installation health checks and prints ✓/✗ per check.
|
|
8
8
|
# Extracted from the ruact:doctor Rake task for direct testability (FR27).
|
|
9
|
-
class Doctor
|
|
9
|
+
class Doctor # rubocop:disable Metrics/ClassLength
|
|
10
10
|
CHECKS = %i[manifest vite controller layout streaming legacy_constant serialize_only flight_middleware].freeze
|
|
11
11
|
# Built via Array#join so the gem-CI `name-propagation` guard does not
|
|
12
12
|
# match these literals against itself (Story 5.1 review F4 — the doctor
|
|
@@ -64,6 +64,14 @@ module Ruact
|
|
|
64
64
|
# future status) is treated as a failure (review finding R1).
|
|
65
65
|
SUCCESS_STATUSES = %i[pass warn].freeze
|
|
66
66
|
|
|
67
|
+
# Story 15.3 (FR107) — version of the machine-readable `ruact:doctor --json`
|
|
68
|
+
# document (see {#as_json}). This is an **EXPERIMENTAL / UNSTABLE** contract:
|
|
69
|
+
# `0` signals the shape may change without a major version bump while the
|
|
70
|
+
# agent-facing introspection surface is iterated. Gate any parser on it.
|
|
71
|
+
# Distinct from {Ruact::ServerFunctions::Snapshot::VERSION_V2} (the internal
|
|
72
|
+
# codegen bridge version) — do not conflate the two.
|
|
73
|
+
SCHEMA_VERSION = 0
|
|
74
|
+
|
|
67
75
|
# @param serialize_only_root [String] directory whose `**/*.rb` is scanned
|
|
68
76
|
# for the serialize-only tripwire. Defaults to the gem's own `lib/`;
|
|
69
77
|
# injectable so specs can point it at a fixture tree.
|
|
@@ -78,16 +86,61 @@ module Ruact
|
|
|
78
86
|
|
|
79
87
|
def run
|
|
80
88
|
puts "[ruact] Health check"
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
computed = results
|
|
90
|
+
computed.each { |status, message| puts format_result(status, message) }
|
|
83
91
|
# A :warn must NOT fail the run (Story 13.1 AC3); only :pass / :warn are
|
|
84
92
|
# success. An unexpected status (rendered `✗`) fails loudly rather than
|
|
85
93
|
# being silently treated as a pass (review finding R1).
|
|
86
|
-
passed =
|
|
94
|
+
passed = passed?(computed)
|
|
87
95
|
puts "Run rails generate ruact:install to fix configuration issues" unless passed
|
|
88
96
|
passed
|
|
89
97
|
end
|
|
90
98
|
|
|
99
|
+
# Runs every check ONCE and returns the raw result tuples, index-aligned with
|
|
100
|
+
# {CHECKS}. Each tuple is `[status, message]` or `[status, message,
|
|
101
|
+
# remediation]` (the optional 3rd element is a machine-readable fix string,
|
|
102
|
+
# nil when the check has no cleanly separable remediation). Shared by {#run}
|
|
103
|
+
# (human path) and {#as_json} (JSON path) so a check like `check_vite` — which
|
|
104
|
+
# opens a socket — never runs twice. (Story 15.3)
|
|
105
|
+
#
|
|
106
|
+
# @return [Array<Array>] one tuple per check, in {CHECKS} order.
|
|
107
|
+
def results
|
|
108
|
+
CHECKS.map { |check| send(:"check_#{check}") }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param computed [Array<Array>] result tuples (defaults to a fresh {#results} run).
|
|
112
|
+
# @return [Boolean] true when NO check failed — only `:pass`/`:warn` are
|
|
113
|
+
# success (mirrors {#run}'s rule; an unexpected status fails).
|
|
114
|
+
def passed?(computed = results)
|
|
115
|
+
computed.all? { |status, _| SUCCESS_STATUSES.include?(status) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Story 15.3 (FR107) — the machine-readable `ruact:doctor --json` document.
|
|
119
|
+
# Reuses the SAME {#results} tuples the human path prints (no double-run), so
|
|
120
|
+
# the JSON never disagrees with the `✓/⚠/✗` output. EXPERIMENTAL shape — gate
|
|
121
|
+
# parsers on `schema_version` (see {SCHEMA_VERSION}). Emits NO prose — the
|
|
122
|
+
# rake task prints ONLY this document in JSON mode.
|
|
123
|
+
#
|
|
124
|
+
# @return [Hash] `{ "schema_version" => Integer, "status" =>
|
|
125
|
+
# "pass"|"fail", "checks" => [{ "name" =>, "status" =>, "message" =>,
|
|
126
|
+
# "remediation" => (String|nil) }] }`.
|
|
127
|
+
def as_json
|
|
128
|
+
computed = results
|
|
129
|
+
{
|
|
130
|
+
"schema_version" => SCHEMA_VERSION,
|
|
131
|
+
"status" => passed?(computed) ? "pass" : "fail",
|
|
132
|
+
"checks" => CHECKS.each_index.map do |i|
|
|
133
|
+
status, message, remediation = computed[i]
|
|
134
|
+
{
|
|
135
|
+
"name" => CHECKS[i].to_s,
|
|
136
|
+
"status" => status.to_s,
|
|
137
|
+
"message" => message,
|
|
138
|
+
"remediation" => remediation
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
91
144
|
private
|
|
92
145
|
|
|
93
146
|
def check_manifest
|
|
@@ -95,7 +148,8 @@ module Ruact
|
|
|
95
148
|
if Pathname(path).exist?
|
|
96
149
|
[:pass, "Manifest found at #{path}"]
|
|
97
150
|
else
|
|
98
|
-
[:fail, "Manifest not found — run vite build"
|
|
151
|
+
[:fail, "Manifest not found — run vite build",
|
|
152
|
+
"Run vite build (or bin/dev) to generate the client manifest."]
|
|
99
153
|
end
|
|
100
154
|
end
|
|
101
155
|
|
|
@@ -103,7 +157,8 @@ module Ruact
|
|
|
103
157
|
TCPSocket.new("localhost", 5173).close
|
|
104
158
|
[:pass, "Vite accessible at localhost:5173"]
|
|
105
159
|
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
|
106
|
-
[:fail, "Vite not accessible at localhost:5173 — run npm run dev"
|
|
160
|
+
[:fail, "Vite not accessible at localhost:5173 — run npm run dev",
|
|
161
|
+
"Run npm run dev (or bin/dev) to start the Vite dev server."]
|
|
107
162
|
end
|
|
108
163
|
|
|
109
164
|
def check_controller
|
|
@@ -111,7 +166,8 @@ module Ruact
|
|
|
111
166
|
if File.exist?(path) && File.read(path).include?("Ruact::Controller")
|
|
112
167
|
[:pass, "Ruact::Controller included in ApplicationController"]
|
|
113
168
|
else
|
|
114
|
-
[:fail, "Ruact::Controller not included in ApplicationController"
|
|
169
|
+
[:fail, "Ruact::Controller not included in ApplicationController",
|
|
170
|
+
"Run rails generate ruact:install to include Ruact::Controller in ApplicationController."]
|
|
115
171
|
end
|
|
116
172
|
end
|
|
117
173
|
|
|
@@ -120,7 +176,8 @@ module Ruact
|
|
|
120
176
|
if File.exist?(path) && File.read(path).include?("ruact: root")
|
|
121
177
|
[:pass, "React shell present in application.html.erb"]
|
|
122
178
|
else
|
|
123
|
-
[:fail, "React shell missing from application.html.erb"
|
|
179
|
+
[:fail, "React shell missing from application.html.erb",
|
|
180
|
+
"Run rails generate ruact:install to add the React shell to application.html.erb."]
|
|
124
181
|
end
|
|
125
182
|
end
|
|
126
183
|
|
|
@@ -198,7 +255,8 @@ module Ruact
|
|
|
198
255
|
[:warn,
|
|
199
256
|
"#{present.join(', ')} is mounted and may transform `text/x-component` (Flight) responses, " \
|
|
200
257
|
"breaking the wire contract / streaming. Exclude Flight responses from compression " \
|
|
201
|
-
"(don't compress `text/x-component`) or mount it so it does not wrap the Flight routes."
|
|
258
|
+
"(don't compress `text/x-component`) or mount it so it does not wrap the Flight routes.",
|
|
259
|
+
"Exclude text/x-component from compression, or mount the middleware so it does not wrap Flight routes."]
|
|
202
260
|
end
|
|
203
261
|
|
|
204
262
|
# Returns the app middleware stack to scan, or nil when unavailable (no
|